Commit Graph

39 Commits

Author SHA1 Message Date
justin fa7225d6dc Fix #9: DTC/freeze-frame parsing (phantom codes, Mode 02, hex frame index)
- parse_dtcs CAN branch is now message-aware: each ECU reply '<svc> <count>
  <pairs>' has its header stripped per-message, instead of flattening all lines
  and stripping svc+count once. With multiple ECUs the old code ate the second
  header as a DTC pair -> phantom codes. Critically, it does NOT blind-scan for
  svc (0x43 is a legal DTC first byte: C03xx) — a numbered ISO-TP continuation
  is distinguished by its 'N:' frame-index prefix, not by value.
- _line_bytes strips hex frame indices A:-F: (ISO-TP index cycles 0-F), not just
  0-9, so consecutive frames past the 10th aren't dropped.
- read_freeze_frame sends the correct '020200' (svc 02, PID 02, frame 00) and
  skips SID+PID+frame (+3), fixing the off-by-one that mis-read the freeze DTC.
- tests/test_dtc_parse.py: single-frame, multi-ECU (no phantom), numbered
  multiframe with a real C03xx continuation, hex index, non-CAN legacy.

Closes #9

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 19:36:35 -04:00
justin 23c92018c1 Fix #8: scheduler survives link death; timed-out one-offs cancelled
- A transport exception in the poll loop killed the thread silently, leaving the
  GUI on a frozen 'Connected' dashboard and blocking run_oneoff callers for the
  full timeout. _loop now catches it -> stops, fails pending one-offs with the
  real error, and calls an on_error callback. Controller wires on_error to flag
  the connection dead; the GUI detects it in _tick and tears down with a
  'Connection lost' dialog.
- A run_oneoff that timed out left its job queued, so it executed LATER on the
  shared link -- a ghost/duplicate vehicle command. Jobs now carry
  cancelled/started flags under a lock; on timeout a not-yet-started job is
  cancelled (skipped by _drain_oneoffs), and a started one reports 'still
  running -- do NOT retry'. stop() also frees stranded submitters.
- tests/test_scheduler.py: cancel-on-timeout, freed-on-death, loop-survives.

Closes #8

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 19:33:33 -04:00
justin b5e0c96763 Fix #7: derive action risk from UDS SIDs; fix response parsing
Untrusted profiles could bypass the confirmation and responses were mis-parsed:

- effective_risk(action): risk is now DERIVED from the actual service IDs the
  steps send — any write/actuator/reset/transfer SID (2F/31/11/14/2E/27/34-37/…)
  forces 'danger'; unknown SID / non-default session / security block force
  'caution'. A profile can only RAISE risk, never label a reflash 'safe'. GUI
  gates the confirmation (and the risk badge) on this derived value.
- Response checks use CONTIGUOUS subsequence matching + a hard '7F <sid>'
  negative-response guard, so an NRC data byte (e.g. 0x7E) can't false-pass as a
  positive response; applied to session/security/step checks.
- 0x78 (responsePending) is treated as in-progress, not terminal failure.
- controller.run_action restores slow ELM timing for the run (0x78 window).
- Tests: risk-cannot-be-downgraded, NRC false-positive rejected, pending handled.

Closes #7

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 19:29:44 -04:00
justin 0f029b724a Fix #6: bound formula evaluation to stop untrusted-profile DoS
The AST sandbox whitelisted ** and << with no magnitude bound, so a hostile
profile formula (9**9**9, 1<<10**9) computed a multi-hundred-MB integer on the
scheduler thread -> CPU pin + OOM. The scheduler except clause never catches a
runaway/OOM (not a raised exception), and a derived PID with empty deps fires
every tick on connect.

- _apply() guards each BinOp: shift amount <= 256, exponent <= 64, and any int
  result bit_length > 512 raises FormulaError (caught by the scheduler -> sample
  dropped, thread survives).
- compile-time caps: expr length <= 500, AST depth <= 60; parse also catches
  RecursionError.
- test_formula_dos_bounded: giant-int payloads rejected in <0.5s; legit bit ops
  and scaling still work.

Closes #6

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 19:26:05 -04:00
justin d435384b58 #2 (framework): bi-directional / service-function engine
Profile-defined UDS action sequences, run safely -- the framework for #2 (real
per-vehicle actuator tests/resets are follow-on, added as verified profile data).

- obdcore/actions.py: Action model + run_action() executing session (Mode 10) ->
  security (Mode 27 seed->key) -> command steps (2F/31/11/3E/... any hex) with
  positive/negative response checks. Security KEY algorithms are per-vehicle
  secrets and NOT bundled -- only trivial transforms (xor-ff/invert/add-ff)
  known; an action naming an unknown algorithm is BLOCKED (fails safe). Never
  synthesizes bytes -- runs only what the profile defines. validate_action()
  rejects malformed hex at load.
- profile.py: load/save an actions[] block; ElmLink/MockLink read_raw(hex).
- GUI: Diagnostics -> Service & Bi-directional dialog -- lists the profile's
  actions with risk badges; caution/danger gated behind a warning confirmation.
- generic-obd2: two safe STANDARD actions (Tester-Present ping; ECU-Reset,
  caution + engine-off warning). PROFILE_SPEC.md documents the actions schema
  + safety rules.
- tests/test_actions.py: runner, session+reset, security handshake, unknown-algo
  block, hex validation, profile load. All 5 suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 16:33:51 -04:00
justin 74bfa2e146 Add 2 Honda vehicle profiles (2007 CR-V 2.4, 2022 Odyssey 3.5)
Built by the honda-profile-research workflow (per-vehicle research ->
synthesize -> adversarial-review) and validated through the loader (every
formula compiles, presets resolve, decoders sane):

- honda-crv-2.4-2007.json    ISO 15765 CAN, MAF + wideband A/F (lambda),
                             single-bank K24Z1 i-VTEC, 19 PIDs, 39 DTCs.
- honda-odyssey-3.5-2022.json ISO 15765 CAN, MAF, dual-bank J35 V6 (STFT/LTFT
                             B1&B2 + 4 O2/AF sensors), 31 PIDs, 83 DTCs.

Standard SAE Mode-01 PIDs (all verified) + gauge zones on ECT/RPM/BATT;
Honda enhanced Mode-22 PIDs omitted (no public source pairs a documented PID
with a verified formula for these -- Honda uses proprietary HDS). Web-
researched, not yet read on the actual vehicles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 08:45:57 -04:00
justin 7bda758f88 Tier 2: WiFi + Bluetooth ELM327 transports
- obdcore/transport.py: pluggable byte transports -- SerialTransport,
  TcpTransport (WiFi ELM327, stdlib socket), BleTransport (experimental, via
  optional 'bleak'; background asyncio loop buffering notifications). ble_scan().
- ElmLink refactored onto a transport with .serial()/.tcp()/.ble() factories
  (close/cmd now go through self.io); no behavior change for serial.
- Controller.connect(conn={kind:serial|wifi|ble,...}); GUI connection bar gains
  a transport selector (Serial/USB/BT-SPP | WiFi host:port | Bluetooth LE + Scan).
- Classic-Bluetooth needs no new code (pairs as a serial port); WiFi needs no
  extra deps; BLE is opt-in (bleak not bundled, so CI binaries keep building).
- tests/test_transport.py: drives ElmLink over a fake ELM TCP server end-to-end
  (connect, RPM, readiness, VIN). All suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-07-01 08:24:51 -04:00
justin 6548cf7fbe Section 1 GUI: Vehicle Info, Emissions Readiness, Freeze Frame, Trip/Performance
- Diagnostics menu: Vehicle Info (VIN/cal/ECU), Emissions Readiness (I/M
  monitors + MIL -> pass/fail), Freeze Frame (snapshot + capturing DTC).
  All routed through the scheduler one-off path; dialogs, no docked panels.
- New Trip / Performance view (View menu, center page): live + average MPG,
  trip distance/fuel/time, and 0-60 / 1/4-mile timers. The controller keeps
  SPEED + MAF polled in the background and feeds TripComputer/PerformanceMeter
  every tick, so trips accumulate regardless of the active view. Honest MAF
  caveat shown for speed-density/diesel vehicles.

Validated headless against MockLink: VIN dialog, readiness dialog, freeze-frame
dialog, and the live trip page (28.8 mpg / distance accruing). All tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 19:43:31 -04:00
justin 4a4daf3fa0 Add generic SAE DTC database (1409 codes) with profile-priority fallback
Compiled by the generic-dtc-db workflow (P0/U0/C0/B0 standard codes, system
tags + no-start flags). Lives in profiles/_data/generic-dtcs.json (bundled with
profiles/, not listed as a vehicle profile). DtcDatabase.get now falls back:
profile code -> generic code -> unknown, so any standard code resolves to a
description while vehicle profiles still override (e.g. P0148 keeps the 6.0 text).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 19:40:02 -04:00
justin 6c1ee0c81d Section 1 backend: VIN/Mode-09, readiness monitors, freeze-frame, trip/perf
obdcore additions (all standard SAE J1979, vehicle-agnostic, hardware-free
tested):
- obdservices.py: decode_vin (Mode 09), decode_readiness (Mode 01 PID 01 I-M
  monitors + MIL + DTC count, spark/diesel monitor sets), freeze-frame PID set.
- link.py: ElmLink.read_vehicle_info (VIN/cal/ECU), read_readiness, read_freeze_frame.
- trip.py: TripComputer (MAF-based MPG + trip totals) and PerformanceMeter
  (0-60 / 1/4-mile with launch detection).
- mock.py: speed/MAF/readiness + service stubs for GUI mock mode.
- tests/test_services.py: VIN, readiness bit decode, trip math, 0-60/quarter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 19:37:48 -04:00
justin 310d5a3497 README: rewrite as cross-platform multi-vehicle GUI app; add release checksums
- README now leads with the vehicle-agnostic GUI (download binaries, run from
  source, connect, vehicle profiles), with the Ford 6.0 CLI as a secondary
  section. Documents the unsigned-binary SmartScreen/Gatekeeper bypass.
- CI: each release binary now ships a .sha256 so downloads can be verified
  (free integrity check in lieu of code signing).
- Validated on real vehicles (Jeep 4.0, Mustang Cobra 4.6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 17:25:43 -04:00
justin 9ac85aea11 CI(windows): pip --no-cache-dir to reduce disk use
Build binaries / macos (push) Successful in 44s
Build binaries / linux-amd64 (push) Successful in 1m26s
Build binaries / linux-arm64 (push) Successful in 1m51s
Build binaries / windows (push) Successful in 5m59s
Windows runs were failing with OSError 28 (No space left on device) while
downloading PySide6 -- the runner disk is full (compounded by leftover act
caches from earlier failed runs). --no-cache-dir avoids keeping wheel copies.
The runner still needs disk freed; this just trims the footprint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
v0.1.0
2026-06-30 16:31:28 -04:00
justin 26a8f1a24c CI(windows): use embeddable Python (no MSI) — installer hit error 1603
Build binaries / macos (push) Successful in 43s
Build binaries / windows (push) Failing after 1m20s
Build binaries / linux-amd64 (push) Successful in 1m27s
Build binaries / linux-arm64 (push) Successful in 1m49s
The python.org MSI returns 1603 on this runner (half-registered state from
prior install attempts). Switch to the embeddable zip + get-pip: no MSI, no
admin, no registry — self-contained in LOCALAPPDATA. Falls through to a
pre-installed python if the runner already has one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 16:28:00 -04:00
justin 18ab3ef97e CI(windows): install Python to explicit TargetDir + log exit code
Build binaries / macos (push) Successful in 43s
Build binaries / windows (push) Failing after 55s
Build binaries / linux-amd64 (push) Successful in 1m25s
Build binaries / linux-arm64 (push) Successful in 1m50s
Per-user install landed somewhere my candidate-path check missed. Force the
install dir (LOCALAPPDATA\Python312) so python.exe is found deterministically.
(Linux/mac/arm64 builds succeeded; they only failed re-uploading to an existing
release -- softprops' asset-delete 404s on Gitea. A fresh release avoids that.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 16:24:00 -04:00
justin 43dd739e3c CI(windows): per-user Python install + robust python.exe path lookup
Build binaries / macos (push) Failing after 43s
Build binaries / windows (push) Failing after 1m19s
Build binaries / linux-amd64 (push) Failing after 1m47s
Build binaries / linux-arm64 (push) Failing after 1m46s
The installer's InstallAllUsers=1 needs admin (runner service isn't elevated)
so Python landed in the per-user LOCALAPPDATA dir, not C:\Program Files; the
hardcoded PATH prepend missed it -> 'python not recognized'. Switch to a
per-user install and locate python.exe across the known dirs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 16:20:02 -04:00
justin 0cc83884a5 CI: self-install Python on Windows; build Linux/arm64 in python+node container
Build binaries / macos (push) Successful in 44s
Build binaries / windows (push) Failing after 1m36s
Build binaries / linux-amd64 (push) Successful in 2m7s
Build binaries / linux-arm64 (push) Successful in 2m12s
First v0.1.0 run: Windows + Linux + arm64 failed (no Python on those runners;
the docker default image also lacks Python). macOS succeeded.
- windows: install Python 3.12 via the official installer in-job (idempotent).
- linux-amd64 / linux-arm64: run in nikolaik/python-nodejs:python3.12-nodejs20
  (has python+pip AND node for actions/checkout; multi-arch covers the Pi).
- macOS job unchanged (it worked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 16:13:47 -04:00
justin 58305cded4 CI: build cross-platform binaries + tagged release
Build binaries / linux-amd64 (push) Failing after 31s
Build binaries / linux-arm64 (push) Failing after 59s
Build binaries / macos (push) Successful in 1m9s
Build binaries / windows (push) Failing after 3m0s
.gitea/workflows/release.yml builds the PySide6 GUI with PyInstaller on each
self-hosted runner and, on a v* tag, publishes a Gitea Release with every
platform binary attached:
- windows-latest  -> OBDash-windows.exe   (onefile, --windowed)
- self-hosted-mac -> OBDash-macos.zip     (.app bundle, ditto-zipped)
- docker (linux)  -> OBDash-linux-x86_64  (onefile + patchelf/GL libs)
- arm64 (Pi)      -> OBDash-linux-aarch64
Uses softprops/action-gh-release with the auto GITEA_TOKEN; manual dispatch
build-only (no release).

profiles/ is bundled via --add-data; obdcore.profile.profiles_dir() now
resolves sys._MEIPASS when frozen so the binary finds its vehicle profiles.

Validated locally on Linux: PyInstaller onefile builds (88MB), launches
offscreen, and loads bundled profiles (6 found). Build artifacts gitignored.
Added .claude/gitea-ship.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 16:04:56 -04:00
justin d893ff383a Gauge redline zones + C/F units toggle + cleaner dials
Gauges:
- Optional per-metric warning zones (warn_hi/redline_hi/warn_lo/redline_lo) in
  the profile schema; gauges draw colored redline/warn bands and color the
  needle + readout by zone. Default neutral when unset (no false redline).
- Removed the value progress-arc fill (it dominated the dial / looked wrong) ->
  clean tach face: bezel, ticks, numeric scale, needle, redline band, readout.
- Auto-derivation rejected: bad direction/threshold vary per metric, so zones
  are config (with a sensible neutral default).

Units:
- New Units menu: Temperature C / F. Converts gauges, graph, table, and PID
  browser (values, scale, zones, unit labels) at display time; data stays C.

Ford 6.0 profile: zones for ICP (red<500), FICM (red<40/amber40-48/green48+),
ECT/EOT (high redline), RPM (redline 3800), boost, battery; tightened FICM
(38-52) and battery (9-15) ranges so redline bands land sensibly.

Docs: profiles/PROFILE_SPEC.md -- canonical, AI-agent-ready profile spec
(schema, formula language, zones, confidence, rules); README points to it.

Validated headless: zones parse/classify, F conversion (112C->233.6F, zones
converted), gauges render; obdcore + diagnostics tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 15:52:49 -04:00
justin deef305c63 Add MIT license
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 15:37:47 -04:00
justin 03223dfd6c Rename project to OBDash + per-metric colored multi-axis
Rename: the app is vehicle-agnostic, so 'ford-obd' was wrong. Rebranded all
code/docs/profile authors to OBDash; Gitea repo renamed justin/ford-obd ->
justin/obdash (remote + description updated). Ford the make and the
ford-6.0-powerstroke profile are unchanged (that vehicle really is a Ford).

Multi-axis upgrade (per request):
- MultiAxisPlot now gives each METRIC its own Y axis, each axis colored to
  match its line; the primary metric owns the LEFT axis, others stack right.
- Click a line to promote it to the left axis (sigClicked -> set_primary).
- Cleaner teardown (no removeItem warnings); axis label no longer doubles the
  unit; Normalize round-trips.

Validated headless: colored per-metric axes, promote-to-left, gauge view,
normalize toggle, profile switch; obdcore + diagnostics tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 15:25:26 -04:00
justin e7e04b740e Diagnostics as dialogs (no dock) + real car-style gauges
Diagnostics: removed the always-on right-side Diagnostics dock (it ate screen
space on every view). Read Codes now opens a modal dialog listing codes
grouped stored/pending/permanent with descriptions (no-start codes bold red);
Clear Codes keeps its confirmation, and on re-read pops a dialog only if codes
returned. Dropped the View > Show Diagnostics Panel toggle.

Gauges: replaced the plain progress-arc with a proper automotive dial -- round
face + bezel, major/minor tick marks, a numeric scale (rounded labels), a red
needle + center hub, a colored rim arc for the value, and an amber peak tick.
Digital readout + name/unit below.

Validated headless: no Diagnostics dock; Read Codes opens the Trouble Codes
dialog; gauge view renders. obdcore + diagnostics tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 15:12:49 -04:00
justin f2308cd4eb P2: true multi-axis overlay + gauge view
gui/widgets.py:
- MultiAxisPlot -- overlay with one Y axis PER UNIT (psi/V/rpm/C/%), linked
  ViewBoxes on X, so mixed-scale signals are readable at true values
  (base left axis + up to 4 right axes).
- SinglePlot -- one shared axis for the Normalize (% of range) mode.
- ArcGauge -- 270deg arc gauge with peak tick + numeric readout, own dark bg.
- GaugeGrid -- scrollable grid of gauges.

gui/main.py:
- Graph page is now a multi-axis/single-axis sub-stack; Normalize toggles
  between true multi-axis (raw) and single-axis (%). curves map key->color;
  plot ops route to the active graph widget.
- Gauge View menu enabled (3rd center page); gauges update on tick with peak.
- Theme applies to both plot widgets; profile switch clears graphs/gauges.

Fix: ArcGauge QPen built via setCapStyle (the QPen(...cap=...) kwarg segfaults
PySide6). Validated headless: driving preset -> 6 unit groups across 5 axes,
gauge view renders, normalize round-trips, profile-switch clears cleanly.
obdcore + diagnostics tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 15:07:57 -04:00
justin 717d160f65 Merge feat/diagnostics-menu: Tools/Diagnostics (DTC read/clear)
Built by an isolated-worktree agent. Adds a thread-safe one-off command path
to PollScheduler (DTC reads/clears never race the polling thread for the
serial link), Controller.read_dtcs()/clear_dtcs(), and a GUI Diagnostics menu
+ dock (codes grouped stored/pending/permanent, descriptions from the active
profile's DtcDatabase, no_start codes bold red, guarded clear with re-read).

Reviewed: threading path correct (drain at top of tick, inline when not
running, timeout + exception propagation); merges clean with the vehicle
profiles (disjoint files). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:55:29 -04:00
justin 0fea0908c8 Add Tools/Diagnostics: thread-safe DTC read/clear + Diagnostics panel
The polling thread owns the ELM327, so reading/clearing trouble codes from
the GUI thread would race PID reads and corrupt the stream. Add a one-off
command path that serializes ad-hoc link work onto the polling thread.

obdcore/scheduler.py:
- PollScheduler.run_oneoff(fn, timeout) enqueues a callable (queue.Queue +
  threading.Event) and blocks for its result, re-raising the callable's
  exception. tick() drains queued one-offs at its very top, so they run on
  the same thread that does PID reads -- never concurrently. When the
  scheduler thread isn't running, the job is drained inline on the caller
  (still serialized vs tick(), safe because nothing else touches the link).

gui/controller.py:
- Controller.read_dtcs() -> {"stored","pending","permanent"} (modes 03/07/0A,
  svc 0x43/0x47/0x4A) and clear_dtcs() -> bool. Both route through the
  scheduler one-off when a scheduler exists, else call the link directly.

gui/main.py:
- Diagnostics menu (Read Codes / Clear Codes...) and a right-side QDockWidget
  listing codes grouped Stored/Pending/Permanent. Each row is code +
  description + system from DtcDatabase; no_start codes are flagged bold red.
- Clear is guarded by a confirmation warning (erases codes + freeze frame;
  honest "the code comes right back" / permanent-codes-won't-clear tone from
  run_clear in obd_reader.py). On confirm: clear, then re-read immediately and
  show whatever returned, reporting active faults that came straight back.

tests/test_diagnostics.py:
- one-off returns its value, re-raises exceptions, is drained before a tick's
  PID reads, and runs on a live background thread while polling continues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:53:57 -04:00
justin bbd27739fc Add 4 vehicle profiles (Jeep 4.0, 2x Mustang 4.6, Mountaineer 4.6)
Built by the vehicle-profile-research workflow (per-vehicle research ->
synthesize -> adversarial-review pipeline) and validated through the real
profile loader (every formula compiles, presets reference valid keys):

- jeep-wrangler-4.0-1997.json   ISO 9141-2, speed-density MAP, 13 PIDs, 26 DTCs
- ford-mustang-cobra-4.6-1996   SAE J1850 PWM, MAF, 20 PIDs, 46 DTCs
- ford-mustang-gt-4.6-1996      SAE J1850 PWM, MAF, 18 PIDs, 57 DTCs
- mercury-mountaineer-4.6-2006  SAE J1850 PWM, MAF, 20 PIDs, 48 DTCs

All are standard SAE Mode-01 PIDs (vehicle-appropriate: MAP vs MAF, correct
O2/trim banks) + the BATT atrv pseudo-PID + manufacturer-specific DTCs.
Sourced from web research, NOT yet read on the actual vehicles -- confidence
reflects sourcing. Protocol is auto-negotiated by the ELM327 regardless, so
the Mountaineer PWM-vs-CAN note is informational.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:52:21 -04:00
justin f3f0bf2a77 Make app vehicle-agnostic: JSON vehicle profiles + menu bar
Vehicle data is now DATA, not code. PIDs/scaling/DTCs/presets live in
profiles/*.json; the app loads them at runtime, so it works across vehicles
and others can contribute profiles (open source).

Core:
- obdcore/formula.py: safe AST evaluator for scaling formulas (A/B/... byte
  vars, Torque/FORScan convention). Only arithmetic/bitwise + min/max/abs/
  round/int/float; names/attrs/arbitrary calls rejected at load -> a community
  profile CANNOT execute code.
- obdcore/profile.py: load/save/list profiles; compiles each formula into a
  decode callable. registry.py now profile-backed (PidRegistry/DtcDatabase
  take a Profile); hardcoded Ford table removed.
- store.py: clear()/snapshot()/export_csv() for capture management.

Profiles:
- profiles/ford-6.0-powerstroke.json (27 PIDs, verified formulas, DTCs)
- profiles/generic-obd2.json (standard SAE Mode-01 base, any vehicle)
- profiles/README.md (schema + formula language + contributing)

GUI:
- Menu bar: File (new/record/export/replay capture, quit), Profile (switch/
  load/import/reload/edit-JSON/export, live profile list), View (Graph/Table
  views, gauges P2, toggle PID dock, normalize, light/dark theme), Help
  (about/confidence legend/profile info).
- PID browser + presets rebuild on profile switch; added Table view; raw-JSON
  profile editor dialog (validates schema+formulas before saving).

Tests: profiles load+compile, formula sandbox rejects hostile input, decoders
still match real truck bytes, crank/derived/dead-PID/replay -- all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:34:33 -04:00
justin 45691334e1 registry: add remaining documented Ford 6.0 PIDs (20 -> 27)
The initial registry was a curated core; add the rest of the PIDs the
ford-60-pid-hunt workflow surfaced, with honest confidence tags:
- doc:       VGT duty (096D), Fan speed (099F)
- tentative: Injection timing (09CC, *10/64 not /10), PCM battery (1172),
             fuel-pump duty (1672), fuel level (16C1, uncalibrated),
             mass-fuel-desired (1411, raw only -- no verified GPH formula)
Add VGT to the driving preset. Tests still pass.

Tier reminder: 'verified' = multi-source/standard or truck-confirmed; the
on-truck-confirmed subset remains ICP/EBP/MAP/BARO/EOT/gear/TSS + FICM_M.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:19:57 -04:00
justin 4589904b97 P1: PySide6 + pyqtgraph GUI shell (PID browser + live overlay plot)
First graphical frontend on obdcore. Cross-platform (Win/mac/Linux).

- gui/controller.py: owns link/registry/store/scheduler; subscribe == poll ==
  plot; per-PID rates (ICP/FICM/RPM fast); optional CSV recording.
- gui/main.py: connection bar (port dropdown via find_ports, baud, Mock,
  connect), left PID browser grouped by system with live values + confidence
  badges + checkboxes, central pyqtgraph overlay plot with legend, Normalize
  (% of range) toggle for mixed-scale PIDs, Crank/Driving/Vitals presets,
  10Hz refresh reading the store off the acquisition thread.
- run_gui.py launcher; requirements-gui.txt.
- store.py: lock Channel push/series (GUI reads while scheduler writes).
- docs/gui-p1-preview.png: validated render (mock crank, ICP ramp to 540).

Validated headless (offscreen Qt): connect(mock) -> crank preset -> ICP
streams past 500 -> normalize -> uncheck removes curve -> clean disconnect.
obdcore tests still pass after the locking change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:13:49 -04:00
justin 01de18a568 Document cross-platform support (Windows/macOS/Linux)
The stack is portable by construction: PySide6/pyqtgraph/numpy/pyserial all
ship wheels for all three OSes (incl. Apple Silicon); obdcore has no
OS-specific code; the terminal dashboard's only platform code is guarded
(os.name=='nt' vs termios for POSIX = macOS+Linux).

- ARCHITECTURE.md: Cross-platform section -- portability rules (list_ports
  only, pathlib, no shelling out, platformdirs for config), the three per-OS
  seams (CH340 driver, PyInstaller per-OS packaging, Gatekeeper/SmartScreen).
- README: Setup now covers Windows (CH341SER), macOS (CH34xVCPDriver), Linux
  (in-kernel ch341 + dialout group) instead of Windows-only.

No code changes; obdcore tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:06:49 -04:00
justin 6bee9c0d7f Scaffold obdcore (headless acquisition core) + ARCHITECTURE.md
Foundation for the PySide6 + pyqtgraph Windows GUI, shared with the terminal
tool. Pure data/IO -- no Qt, no curses.

obdcore/
  link.py      ElmLink   -- ELM327 serial (Mode-01/22, ATRV, DTC read/clear)
  mock.py      MockLink  -- synthetic crank for tests + GUI dev (no truck)
  registry.py  PidRegistry (verified Ford 6.0 PIDs + confidence) + DtcDatabase
  scheduler.py PollScheduler -- prioritized round-robin polling, dead-PID park,
               derived channels; tick() is fake-clock test-drivable
  store.py     TimeSeriesStore (ring buffers + min/max) + CsvRecorder/replay

Design centers on the ELM327 bandwidth limit (~7-15 reads/sec): the active
view subscribes PIDs at chosen rates; acquisition runs off the UI thread;
the GUI only reads the store. FICM_M (09D0) promoted to verified after the
2026-06-30 on-truck crank read (48.0V, intermittent).

tests/test_obdcore.py: decoders vs real truck bytes, crank ramp + peak,
derived BOOST, dead-PID park/revive, record/replay roundtrip -- all pass.

ARCHITECTURE.md: layers, data model, GUI plan, 6.0 stock-PID limits
(no EGT/oil-PSI), feature backlog, P0-P5 roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 13:41:24 -04:00
justin 6eb449f354 diagnostics: confirm ICP bleed-off via --crank (peak 376 psi)
Direct on-truck measurement with the verified 1446 ICP PID: peak
cranking ICP 376 psi vs the ~500 psi firing threshold.  FICM Main min
48.0 V (healthy), battery min 10.3 V (adequate).  Diagnosis empirically
confirmed: high-pressure oil bleeds off during cranking.  376 psi is
"almost there" not zero, which fits a partial leak (IPR seat / oil rail
O-rings / partial STC crack) rather than a fully blown STC fitting.

- Capture the --crank output in crank-test-2026-06-30.txt
- Add EMPIRICAL CONFIRMATION section to diagnostics README
- Update handoff "resume at the truck" to lead with physical inspection
  (IPR -> STC -> oil rail O-rings), since the OBD-side diagnosis is done

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 13:03:45 -04:00
justin 899a82e596 handoff: catch up to verified PID set + --crank/--dash modes
Replaces the "PIDs are TENTATIVE" framing with pointers to the verified
Ford 6.0 PID table and pid-research.md.  Updates the "resume" section
to make --crank the top action at the truck, with --pid probes for the
09xx FICM family that was outside the original brute-scan window.
Open-follow-ups condensed to the live items from pid-research §5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 11:40:57 -04:00
justin a9b2e133c0 Add dedicated --crank monitor for no-start diagnosis
Big ICP readout focused on the cranking scenario:
- Wide ICP bar with the 500-psi firing threshold marked (|)
- Rolling ASCII trace chart of the ICP build-up (10 rows; renders anywhere,
  no unicode) -- clearly shows ICP climbing above/below the 500 firing line
- Peak-hold (the crank's max ICP, the money number) + pass/fail verdict
- FICM main / battery / RPM secondaries with sag (min) tracking
- --dash-log writes a CSV (t,icp,ficm,batt,rpm) while you watch
- On exit prints peak ICP + verdict (reached 500 / suspect oil bleed-off)

Validated end-to-end via a mock crank: ICP ramp past 500, peak capture,
battery-sag capture, trace resolution, CSV logging, clean terminal restore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 09:15:14 -04:00
justin c94caefd50 Add real-time live gauge dashboard (--dash)
In-place updating CLI dashboard for watching data while cranking/running.
Pure-ANSI (no new deps; works on Windows 10+ terminals).

- Color-coded gauges (green/yellow/red) by no-start thresholds
- Live min/max per gauge -> captures PEAK ICP during a crank
- ASCII bars for ICP and FICM main voltage
- Presets: crank (ICP/FICM/batt/RPM, fastest), vitals (default), full
- Dead-PID auto-skip keeps refresh rate up when 09xx FICM PIDs no-respond
- --dash-log PATH writes a CSV while you watch (streaming log preserved)
- q=quit, r=reset min/max; cross-platform non-blocking key input

Validated: render + decoders vs the truck's real scan bytes, and the full
dashboard() loop via a mock ELM (ICP climb across the 500psi firing
threshold, peak capture, battery-sag capture, CSV logging, clean exit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-29 23:27:29 -04:00
justin 36b233f02c Correct Ford 6.0 Mode-22 PID table from workflow research
The old 12xx PIDs (1209/1228/120B/...) were wrong addresses -- that's why
they returned 'no response' on the truck, NOT a bus/gateway problem. The real
Ford-enhanced DIDs are in the 09xx/14xx/16xx families. Confirmed by the truck's
own brute-scan: 1446=ICP, 1445=EBP, 1440=MAP, 1442=BARO, 1310=EOT, 11B3=gear,
11B4=TSS all decode to sane on-vehicle values.

- Rewrite FORD_60_PIDS with corrected addresses + [VERIFIED]/[DOC]/[TENTATIVE] tags
- FICM voltages -> 09D0/09CF/09CE/09CD (09D0 Main = the ~48V no-start metric)
- ICP=1446 *0.57, IPR=1434, ICP_V=16AD; EOT scaling fixed to /100-40
- watch --ford now streams 09D0/09CF/1446/1434 (FICM main V + ICP during crank)
- Add diagnostics/2026-06-29-no-start/pid-research.md (full workflow findings)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-29 22:29:46 -04:00
justin 2fe3f1100e diagnostics: FICM healthy (>48V), ether-start signature narrows to ICP path
- FICM measured >48V on M during cranking AND key-ON. Healthy. Removed
  as a suspect.
- Truck starts cleanly on starting fluid (ether) every time, then idles
  and runs normally until shut off -- then needs ether again, even when
  warm. This is a textbook signature for high-pressure oil (ICP) bleed
  during cranking that the HPOP can outrun at running RPM.
- Updated working hypothesis to focus on STC fitting / oil rail O-rings /
  HPOP / IPR. Compression, FICM, CMP/CKP, fuel supply all confirmed good
  by virtue of the engine running cleanly once started.
- Reordered open items to put visual inspection of valve covers + STC
  fitting first.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 21:51:58 -04:00
justin e15e22a825 Add --watch/--ford/--pid/--scan modes + 2026-06-29 session diagnostics
obd_reader.py:
- Mode 22 plumbing: ELM.mode22() sends a 16-bit PID request, parses both
  positive (62 ..) and negative (7F 22 NRC) responses.
- --ford runs a small TENTATIVE table of community-sourced Ford 6.0 PIDs
  (ICP/IPR/FICM/EBP/EOT). All printed with raw bytes for verification.
- --pid XXXX probes a single PID and prints multiple candidate decodings
  (u8, u16, mV, temp, duty) so we can eyeball the right scaling.
- --watch [N] streams ATRV + module voltage (PID 0142) for N seconds.
  Designed for capturing voltage sag during cranking.
- --scan AAAA-BBBB brute-force scans Mode-22 PIDs with --scan-log PATH
  for output. Uses fast ELM timing (ATAT2, ATST19) for ~3.5 PIDs/sec.

diagnostics/2026-06-29-no-start/:
- Captured cranking voltage trace, full Mode-22 scan (1000-14FF -> 46
  hits), and a session writeup. Working hypothesis: not batteries, not
  fuel -- ICP / FICM / CMP. FICM meter test still owed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 21:49:18 -04:00
justin 8bdb77cf53 Add handoff.md for in-cab pickup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-29 19:18:09 -04:00
justin 0491d37a2e ford-obd: ELM327 OBD-II reader + 6.0 Power Stroke no-start triage
Read stored/pending/permanent DTCs, decode with 6.0-relevant codes flagged,
guarded mode-04 clear (--clear), key live PIDs + battery voltage, and a
6.0 no-start triage checklist. Tested against a CH340 ELM327 v1.5 adapter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-29 19:17:35 -04:00