justin 0fea0908c8 Add Tools/Diagnostics: thread-safe DTC read/clear + Diagnostics panel
The polling thread owns the ELM327, so reading/clearing trouble codes from
the GUI thread would race PID reads and corrupt the stream. Add a one-off
command path that serializes ad-hoc link work onto the polling thread.

obdcore/scheduler.py:
- PollScheduler.run_oneoff(fn, timeout) enqueues a callable (queue.Queue +
  threading.Event) and blocks for its result, re-raising the callable's
  exception. tick() drains queued one-offs at its very top, so they run on
  the same thread that does PID reads -- never concurrently. When the
  scheduler thread isn't running, the job is drained inline on the caller
  (still serialized vs tick(), safe because nothing else touches the link).

gui/controller.py:
- Controller.read_dtcs() -> {"stored","pending","permanent"} (modes 03/07/0A,
  svc 0x43/0x47/0x4A) and clear_dtcs() -> bool. Both route through the
  scheduler one-off when a scheduler exists, else call the link directly.

gui/main.py:
- Diagnostics menu (Read Codes / Clear Codes...) and a right-side QDockWidget
  listing codes grouped Stored/Pending/Permanent. Each row is code +
  description + system from DtcDatabase; no_start codes are flagged bold red.
- Clear is guarded by a confirmation warning (erases codes + freeze frame;
  honest "the code comes right back" / permanent-codes-won't-clear tone from
  run_clear in obd_reader.py). On confirm: clear, then re-read immediately and
  show whatever returned, reporting active faults that came straight back.

tests/test_diagnostics.py:
- one-off returns its value, re-raises exceptions, is drained before a tick's
  PID reads, and runs on a live background thread while polling continues.

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

ford-obd

Minimal ELM327 OBD-II code reader with a Ford 6.0L Power Stroke no-start triage, built for a cheap CH340 ELM327 USB adapter. Works on any OBD-II vehicle for generic codes/PIDs; the triage notes are 6.0-specific.

Created as a stopgap while forscan.org was offline — it covers reading/clearing codes and the basics, not Ford-enhanced diesel PIDs (see Scope below).

Features

  • Read stored (mode 03), pending (mode 07), permanent (mode 0A) DTCs
  • Decode P/C/B/U codes, with common 6.0 codes described and no-start suspects flagged
  • Clear codes (mode 04) — guarded behind --clear + a typed CLEAR confirmation, then re-reads to show any code that returns immediately (active fault)
  • Key live values (coolant, IAT, MAP, module voltage, RPM, load, throttle) + battery voltage
  • 6.0 Power Stroke no-start triage checklist (FICM, ICP, cam/crank, batteries, fuel)

Setup

Runs on Windows, macOS, and Linux (Python + pyserial). The only per-OS difference is the CH340 USB driver:

  • Windows — install WCH CH341SER; adapter shows as USB-SERIAL CH340 (COMx) in Device Manager → Ports. Install Python from https://www.python.org/downloads/ (tick Add Python to PATH), or just double-click RUN_OBD.bat.
  • macOS — install WCH CH34xVCPDriver (Mac App Store or wch.cn). Port appears as /dev/cu.wchusbserial*. pip install pyserial.
  • Linuxch341 driver is built into the kernel (no install). Port is /dev/ttyUSB0; add yourself to the dialout group for access (sudo usermod -aG dialout $USER, then re-login). pip install pyserial.

The tool auto-detects the port on all three; pass it explicitly if needed (COM5, /dev/cu.usbserial-1420, /dev/ttyUSB0).

Usage

python obd_reader.py                # auto-detect the COM port
python obd_reader.py COM5           # force a port
python obd_reader.py COM5 9600      # force port + baud (default 38400)
python obd_reader.py COM5 --clear   # read, then optionally clear (asks to confirm)
python obd_reader.py COM5 -v        # verbose: show raw ELM327 traffic

Crank monitor (dedicated no-start view) — --crank

The one to use for a crank-but-won't-start. Big ICP readout with a wide bar (the | marks the 500-psi firing threshold), a rolling ASCII trace of the ICP build-up, peak-hold, FICM/battery/RPM with sag tracking, and a pass/fail verdict. Start it, then crank.

python obd_reader.py COM5 --crank                  # crank monitor
python obd_reader.py COM5 --crank --dash-log crank.csv   # + record a CSV
  ICP  [#################################|##----]   539.8 psi
  PEAK    540 psi   FIRING PRESSURE REACHED
  FICM Main 47.5V (min 47.5) [DOC]    Batt 12.6V (min 10.7)    RPM 200

  ICP trace (psi vs time, last 16 samples)
   600 |
   500 |----------------------------------------------####   <- firing line
       |                                            ######
       |                                          ########
       +--------------------------------------------------

Read it: ICP should climb past 500 psi within 12 s of cranking (FIRING PRESSURE REACHED, green). If it stalls below 500 (red, trace flat under the line), that's the high-pressure oil bleed-off — STC fitting / oil-rail O-rings. On exit it prints the peak and a verdict. q quits, r resets.

Graphical app (preview — P1)

A cross-platform desktop GUI (PySide6 + pyqtgraph) is in progress. P1 = PID browser + live overlay plot; see ARCHITECTURE.md for the roadmap (cranking/driving/diagnostics perspectives, record/playback, etc.).

pip install -r requirements-gui.txt
python run_gui.py        # tick "Mock" + Connect to explore with no adapter

P1 GUI

The whole app runs against simulated data (MockLink) so it can be developed on any machine and only needs the truck for real captures.


Live dashboard (real-time gauges)

Updates in place as you crank or run the engine — color-coded, with live min/max so a crank's peak ICP is captured. No extra dependencies (ANSI; works on any Windows 10+ terminal). q quits, r resets min/max.

python obd_reader.py COM5 --dash            # vitals preset (ICP, FICM, IPR, batt, RPM, temps)
python obd_reader.py COM5 --dash crank      # cranking preset: ICP / FICM main / batt / RPM (fastest)
python obd_reader.py COM5 --dash full       # every PID
python obd_reader.py COM5 --dash crank --dash-log crank.csv   # + write a CSV while you watch

No-start use: run --dash crank, then crank. A healthy 6.0 builds ~500+ psi ICP within 12 s; if ICP stalls below 500 (red), that confirms the high-pressure oil bleed-off. FICM Main should hold ~48V. The --dash-log CSV is your streaming log — paste it back for analysis.

Note: the FICM PIDs (09xx) are [DOC] (not yet confirmed on this truck); if they read --, they auto-drop after a few frames so the refresh rate stays up.

Or just double-click RUN_OBD.bat on Windows (auto-installs pyserial).

On the truck: plug into the OBD port under the dash, key to RUN (engine off is fine for codes), then run the tool.

Scope / honesty

A generic ELM327 reads standard OBD-II only: codes, generic PIDs, port voltage. It does not read Ford-enhanced diesel PIDs (ICP, FICM main/sync voltage, IPR%) — those need FORScan. For FICM/ICP numbers, measure at the FICM with a meter, or use FORScan when it's available. Default baud is 38400 (measured on the CH340 adapter); try 9600 if you get garbage.

Requirements

pyserial (pip install pyserial). Tested against a QinHeng CH340 ELM327 v1.5 clone.

S
Description
OBDash — open-source, vehicle-agnostic OBD-II scanner (Python/Qt). Live multi-axis graphs, car-style gauges, DTC read/clear, JSON vehicle profiles.
Readme MIT 1.8 MiB
v0.1.0 Latest
2026-06-30 16:53:36 -04:00
Languages
Python 99.7%
Batchfile 0.3%