# 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`; 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.
