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
6.8 KiB
ford-obd — Architecture & Roadmap
Plan for growing the terminal obd_reader.py into a graphical Windows scan
tool for the 6.0L Power Stroke, on a shared headless core.
Vision
A desktop app with:
- a PID browser (searchable list, live values) on the side,
- a flexible graph workspace — overlay many metrics on one plot, split into a grid of single-metric plots, or show gauges (one metric each),
- purpose-built perspectives: Cranking, Driving, Diagnostics (DTCs), Logging,
- a Ford DTC database behind the codes page,
- session record + playback so intermittent faults can be reviewed offline.
The constraint that shapes everything: ELM327 bandwidth
The ELM327 is a one-request-at-a-time link: each Mode-22 round-trip is ~40–150 ms over the CH340, so total throughput is ~7–15 PID reads/sec, shared across the whole UI. Consequences baked into the design:
- A prioritized polling scheduler owns the link. The active view subscribes the PIDs it needs at the rates it needs (cranking = ICP fast, ignore the rest).
- Acquisition runs off the UI thread; the GUI only reads the store.
- Per-PID rates and dead-PID parking keep the sample rate from collapsing.
- A faster adapter (OBDLink SX/MX+, STN chip — batched PIDs, faster protocols) multiplies throughput; the link layer is abstracted so either works.
Layers
+------------------- GUI (PySide6 + pyqtgraph) -------------------+
| PID browser | Graph workspace | Perspectives | DTC page | Log |
+--------------------------------|-------------------------------+
reads only | (Qt thread)
+--------------------------------v-------------------------------+
| obdcore (headless) |
| PollScheduler --reads--> ElmLink / MockLink --serial--> PCM |
| | pushes samples |
| v |
| TimeSeriesStore <--- CsvRecorder / replay_csv |
| PidRegistry (verified PIDs) DtcDatabase |
+-----------------------------------------------------------------+
obdcore is pure data/IO — no Qt, no curses — so it's shared by the terminal
tool, the GUI, and tests.
obdcore modules (built, tested)
| Module | Responsibility |
|---|---|
link.py ElmLink |
ELM327 serial: init, protocol negotiate, Mode-01/22 reads, ATRV, DTC read/clear. Returns raw bytes. |
mock.py MockLink |
Synthetic crank (ICP ramp, FICM ~48V, batt sag) — same interface; powers tests + GUI dev with no truck. |
registry.py PidRegistry |
Verified Ford 6.0 PID table (corrected addresses + scaling + confidence) and subscription presets. DtcDatabase seeds the code DB. |
scheduler.py PollScheduler |
Prioritized round-robin polling; per-PID Hz; derived channels; dead-PID park/revive. tick() is test-drivable with a fake clock. |
store.py TimeSeriesStore |
Per-PID ring buffers + min/max; CsvRecorder (long format) + replay_csv for record/playback. |
Tests: tests/test_obdcore.py — decoders vs real truck bytes, crank ramp +
peak capture, derived BOOST, dead-PID parking, record/replay round-trip.
Data model
- Pid: key, name, mode (
01/22/atrv/derived), pid hex, nbytes, decode fn, unit, group, vmin/vmax, confidence (verified|doc|tentative), deps (for derived), notes. - Channel: rolling
(t, value)+ session min/max. - Derived/virtual channels: e.g.
BOOST = MAP − BARO; laterICP_error = ICP_DES − ICP,FICM_sag = FICM_M − FICM_V.
GUI plan (PySide6 + pyqtgraph)
- Left dock:
QTreeViewPID browser grouped by system (fuel/ficm/air/…), live value + confidence badge, checkbox to add to the focused panel. - Central workspace: dockable/tabbed panels. Panel types:
- Overlay plot — many PIDs, multiple Y-axes (pyqtgraph
ViewBoxlinking). - Split grid — one plot per PID.
- Gauge — radial/linear single metric with warn/crit bands. Drag a PID from the browser onto a panel to add it.
- Overlay plot — many PIDs, multiple Y-axes (pyqtgraph
- Perspectives (saved layouts):
- Cranking — ICP big readout + 500-psi firing line + peak-hold + trace
(port of terminal
--crank). - Driving — boost (MGP), EOT, ECT, EBP, load, RPM, IPR, FICM, trans temp.
- Diagnostics — DTC read/clear (guarded) + freeze frame, joined to the Ford DTC DB (description, causes, no-start relevance).
- Logging/Playback — record a session; scrub/replay through the graphs.
- Cranking — ICP big readout + 500-psi firing line + peak-hold + trace
(port of terminal
- Settings — COM port, baud, protocol, per-PID rates, units (psi/°F vs kPa/°C), dark/night theme.
- Bottom status — adapter, protocol, port voltage, dropped-response rate.
Honest 6.0 data-stream limits
Two commonly-wanted gauges are not in the stock 6.0 PCM stream from the OBD port and need aftermarket sensors:
- Engine oil PRESSURE — only ICP (injection oil) + EOT (oil temp) exist; lube pressure is an idiot-light switch, not a PID.
- EGT — only EBP (exhaust back pressure) exists; exhaust gas temperature is an add-on pyrometer.
Plan: present what the PCM exposes; design an aux-input path so external sensors (e.g. a serial/analog EGT/oil-PSI module) can feed extra channels later.
Additional features (backlog)
- Session record + playback/scrub (highest value; foundation already in
store). - Bi-directional tests: KOEO/KOER self-tests, injector buzz, cylinder contribution/balance.
- Alarms + min/max hold per PID (EOT>230°F, ICP<500 cranking).
- Timeline annotations ("started cranking", "stabbed throttle").
- Computed channels (ICP error, FICM sag).
- Multi-vehicle profiles + per-truck DTC history.
- Export/report (CSV/PDF, graph screenshots, one-click "share state").
- PID discovery scan in GUI (the brute-scan, auto-add hits).
- Units toggle, night theme, big-touch cab mode.
Roadmap
- P0 — core (this commit):
obdcorepackage + tests + this doc. Next: migrateobd_reader.pyto importobdcore(remove the duplicated ELM/PID logic) so terminal + GUI share one source of truth. - P1 — GUI shell: PySide6 window, connect dialog, PID browser, one overlay
plot fed by the scheduler/store. Validate against
MockLinkfirst. - P2 — panels + perspectives: split plots, gauges, Cranking + Driving views.
- P3 — diagnostics: DTC read/clear page + Ford DTC DB (built by the cross-verified workflow, same method as the PID hunt).
- P4 — record/playback + alarms + computed channels.
- P5 — packaging: PyInstaller one-file
.exe(+ CH340 driver note), optional code-signing; OBDLink/STN fast-path support.
Dependencies
- Runtime (core):
pyserial. - GUI:
PySide6,pyqtgraph,numpy. - Dev:
pytest,pyinstaller.