Scaffold obdcore (headless acquisition core) + ARCHITECTURE.md

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
This commit is contained in:
2026-06-30 13:41:24 -04:00
parent 6eb449f354
commit 6bee9c0d7f
8 changed files with 939 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
# 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
~40150 ms over the CH340, so total throughput is **~715 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.
## 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`.
- Dev: `pytest`, `pyinstaller`.