Commit Graph

8 Commits

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