# 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`; later `ICP_error = ICP_DES − ICP`, `FICM_sag = FICM_M − FICM_V`. ## GUI plan (PySide6 + pyqtgraph) - **Left dock**: `QTreeView` PID 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 `ViewBox` linking). - *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. - **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. - **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_ports` only — never assume `COMx`. Port names differ per OS (`COM5` / `/dev/cu.usbserial-*` / `/dev/ttyUSB0`); the GUI shows a dropdown from `find_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 via `ctypes.windll` is 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): 1. **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** — `ch341` is in the kernel; zero install. Just add the user to the `dialout` group for `/dev/ttyUSB0` access. 2. **Packaging** — PyInstaller is per-OS (no cross-compile): build the `.exe` on Windows, the `.app`/`.dmg` on 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.) 3. **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):** `obdcore` package + tests + this doc. *Next:* migrate `obd_reader.py` to import `obdcore` (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 `MockLink` first. - **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.