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
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 typedCLEARconfirmation, 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 asUSB-SERIAL CH340 (COMx)in Device Manager → Ports. Install Python from https://www.python.org/downloads/ (tick Add Python to PATH), or just double-clickRUN_OBD.bat. - macOS — install WCH
CH34xVCPDriver(Mac App Store or wch.cn). Port appears as/dev/cu.wchusbserial*.pip install pyserial. - Linux —
ch341driver is built into the kernel (no install). Port is/dev/ttyUSB0; add yourself to thedialoutgroup 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 1–2 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
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 1–2 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.
