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
9.0 KiB
OBDash — 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.
Cross-platform support
Target: Windows, macOS, Linux from one codebase. The whole stack is
portable — PySide6, pyqtgraph, numpy, pyserial all ship wheels for
all three (incl. Apple Silicon). obdcore is pure Python/IO with no
OS-specific calls; the only platform code is the terminal dashboard's
ANSI/key handling, guarded os.name == "nt" (Windows) vs termios (POSIX =
macOS + Linux). Rules to keep it that way:
- Serial enumeration goes through
serial.tools.list_portsonly — never assumeCOMx. Port names differ per OS (COM5//dev/cu.usbserial-*//dev/ttyUSB0); the GUI shows a dropdown fromfind_ports(), no typing. - Paths:
pathlib/caller-supplied only; no hardcoded separators or drives. - No shelling out to OS tools in core; no
os.system. (The Windows VT enable viactypes.windllis the one exception, fully guarded.) - Config/recordings live under a per-OS app-data dir
(
platformdirs) rather than next to the script.
Three seams that are inherently per-OS (not code we can unify):
- CH340 USB driver (the adapter, not our app):
- Windows — install WCH
CH341SER. - macOS — install the WCH
CH34xVCPDriver(Mac App Store / WCH); recent macOS bundles a CH34x driver but clones often need WCH's. Port shows as/dev/cu.wchusbserial*or/dev/cu.usbserial*. - Linux —
ch341is in the kernel; zero install. Just add the user to thedialoutgroup for/dev/ttyUSB0access.
- Windows — install WCH
- Packaging — PyInstaller is per-OS (no cross-compile): build the
.exeon Windows, the.app/.dmgon macOS, an ELF/AppImage on Linux. We have all three hosts available, or do it in CI. (Briefcase is the alternative if we want macOS notarization/signing handled.) - macOS Gatekeeper / Windows SmartScreen — unsigned builds warn on first run; optional code-signing/notarization later (P5).
Primary dev/use target is the Windows laptop; macOS/Linux are first-class for
development (build the GUI against MockLink on any box) and supported targets.
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,platformdirs(per-OS config/recording dirs). - Dev:
pytest,pyinstaller.
All are cross-platform with wheels for Windows / macOS (incl. Apple Silicon) /
Linux, so pip install is the same everywhere.