Files
obdash/ARCHITECTURE.md
T
justin 01de18a568 Document cross-platform support (Windows/macOS/Linux)
The stack is portable by construction: PySide6/pyqtgraph/numpy/pyserial all
ship wheels for all three OSes (incl. Apple Silicon); obdcore has no
OS-specific code; the terminal dashboard's only platform code is guarded
(os.name=='nt' vs termios for POSIX = macOS+Linux).

- ARCHITECTURE.md: Cross-platform section -- portability rules (list_ports
  only, pathlib, no shelling out, platformdirs for config), the three per-OS
  seams (CH340 driver, PyInstaller per-OS packaging, Gatekeeper/SmartScreen).
- README: Setup now covers Windows (CH341SER), macOS (CH34xVCPDriver), Linux
  (in-kernel ch341 + dialout group) instead of Windows-only.

No code changes; obdcore tests still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
2026-06-30 14:06:49 -04:00

175 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
## 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.