3 Commits

Author SHA1 Message Date
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 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 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