Add --watch/--ford/--pid/--scan modes + 2026-06-29 session diagnostics

obd_reader.py:
- Mode 22 plumbing: ELM.mode22() sends a 16-bit PID request, parses both
  positive (62 ..) and negative (7F 22 NRC) responses.
- --ford runs a small TENTATIVE table of community-sourced Ford 6.0 PIDs
  (ICP/IPR/FICM/EBP/EOT). All printed with raw bytes for verification.
- --pid XXXX probes a single PID and prints multiple candidate decodings
  (u8, u16, mV, temp, duty) so we can eyeball the right scaling.
- --watch [N] streams ATRV + module voltage (PID 0142) for N seconds.
  Designed for capturing voltage sag during cranking.
- --scan AAAA-BBBB brute-force scans Mode-22 PIDs with --scan-log PATH
  for output. Uses fast ELM timing (ATAT2, ATST19) for ~3.5 PIDs/sec.

diagnostics/2026-06-29-no-start/:
- Captured cranking voltage trace, full Mode-22 scan (1000-14FF -> 46
  hits), and a session writeup. Working hypothesis: not batteries, not
  fuel -- ICP / FICM / CMP. FICM meter test still owed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 21:49:18 -04:00
parent 8bdb77cf53
commit e15e22a825
6 changed files with 771 additions and 16 deletions
+115
View File
@@ -0,0 +1,115 @@
# 2026-06-29 — 6.0 Power Stroke no-start session
In-cab diagnostic session using the `ford-obd` tool on the truck.
Charger on "12V Hi" was attached for most of the session.
## Symptoms
- Cranks **fast** (starter spins the engine normally)
- **Zero combustion event** — no sputter, no cough, no kickback
- HFCM lift pump heard priming on key-RUN and shutting off normally
- Battery at OBD port: 11.2 12.7V key-RUN (varied with charger state)
## Codes pulled (multiple reads, slight variation between reads)
| Mode | Code | Description |
|---|---|---|
| Stored (03) | `C0300` | (chassis — not engine, not in our DB) |
| Pending (07) | `C0700` / `C0701` | (chassis — alternated between reads) |
| Pending (07) | `P0113` | IAT circuit HIGH — sensor wiring, not a no-start |
| Permanent (0A) | `C3F0A` | (chassis — not engine) |
No engine P-codes, no FICM codes, no injector codes, no CMP/CKP codes.
## What we ruled out
- **Batteries as primary cause** — cranks fast and voltage held adequately
(see `watch-cranking.log`). Bus sagged to 10.811.2V during ~11s crank,
borderline-low but not catastrophic. Healthy starter spin means batteries
have current to give.
- **Fuel supply** — HFCM lift pump audibly primes and shuts off correctly
on key-RUN.
- **Compression / mechanical** — fast even cranking, would feel different
with a low-compression cylinder.
## Working hypothesis (in order of suspicion)
1. **ICP / high-pressure oil leak** — STC fitting is the classic 6.0 culprit
for "cranks great, won't fire, no codes". Need ≥500 psi ICP to fire even
one injector.
2. **FICM weakness** — should boost to ~48V. Sometimes doesn't throw a code
even when failing. **Not yet measured** — see open items.
3. **CMP sensor** — would usually throw P0340/P0341 but not always.
## What we did with the tool this session
- Pulled DTCs across all 3 modes (multiple times).
- Captured cranking voltage with `--watch 30` (see `watch-cranking.log`):
- Pre-crank: bus 11.7→12.1V, VPCM 12.39→12.71V
- Cranking (~11s): bus 10.811.2V, VPCM 11.3211.70V
- Recovery: bus 11.812.2V, VPCM stopped responding (`?`) — possible
PCM brief recovery state after the dip.
- Ran first attempt at Mode-22 probes (`--ford`) with community-sourced
PIDs (ICP=1209, FICM=1228/1229/122A, IPR=120B, etc.). **All returned
"no response."** Either the PCM uses different PID numbers, or these
values live on Ford's SCP bus and aren't gatewayed to OBD-CAN.
- Brute-force scanned PIDs `0x1000-0x14FF` (1280 PIDs, ~6 min, 46 hits).
See `mode22-scan-hits.txt`. Data is noisy because **multiple modules on
the bus answer the same query** — positive responses from PCM mixed
with negative-response bytes (`7F 22 12`, `7F 22 31`) from other modules.
## Interesting PIDs from the scan to verify next session
Cleanest-looking PCM-side responses worth re-probing individually
(with normal timing, `python obd_reader.py --pid XXXX`):
```
1310 2 1C 92 (was 1C 69 earlier — value changes; maybe a counter?)
1440 2 01 89 = 393
1442 2 01 88 = 392 ← three closely-spaced ~390 values
1445 2 01 8F = 399 suggests a related sensor cluster
1446 2 00 16 = 22
1447 2 00 34 = 52
1448 2 00 11 = 17
11B3 1 02
11B4 2 00 00
11BA 2 FF 5C
```
Likely scaling guesses:
- raw / 100 → fits if these are voltage-tenths or pressure-tenths
- raw / 1000 → if mV (0.393V plausible for sensor outputs)
- raw - some offset → temperature
To know what each one *is*, compare against a known measurement:
- Run `--pid 1440` with engine off, write down value.
- Get engine running (eventually), write down value.
- Change condition, watch which PIDs move.
## Open items / what's next
1. **FICM meter test was started but no readings reported back yet.**
Measure M / S / L on the FICM body, key-ON and during a (short) crank:
- M (Main) target ~48V cranking, <45V suspect, <40V dead
- S (Sync) target ~48V cranking
- L (Logic) target ~12V (mirrors battery)
2. **Visual under-engine check** for STC-fitting oil leak (wet/oily mess
at the rear of the engine, around the turbo / bell housing).
3. **Verify scan-hit PIDs.** Re-probe the clean ones above with
`--pid XXXX` to get uncontaminated readings, log them against known
conditions.
4. **Try addressing other modules with `ATSH`** — particularly the FICM
on its CAN address if it has one (uncertain on 6.0 — likely on SCP).
5. Try **FORScan** when forscan.org / CyanLabs mirror is back, on the
same CH340 adapter, to capture FORScan's actual PID requests for
ICP/FICM/IPR. That gives us the ground truth.
## Tool state at end of session
`obd_reader.py` now has:
- `--ford` — try the (tentative) Ford 6.0 Mode-22 PID table
- `--pid XXXX` — one-shot probe of any 16-bit Mode-22 PID
- `--watch [N]` — stream ATRV + VPCM for N seconds (default 20)
- `--scan AAAA-BBBB` + `--scan-log PATH` — brute-force scan a PID range
All work on the CH340 ELM327 v1.5 clone at 38400 baud, HS-CAN switch.
@@ -0,0 +1,121 @@
================================================================
OBD-II Code Reader - ELM327 / 6.0 Power Stroke triage
================================================================
Auto-selected port: COM5 (USB-SERIAL CH340 (COM5))
(other ports seen: COM4, COM3 -- pass one as an argument if wrong)
Opening COM5 @ 38400 baud ...
Adapter : ELM327 v1.5
Battery : 12.5V (key ON; healthy 6.0 KOEO ~12.4-12.7V)
Connecting to vehicle (turn key to ON / RUN, engine off is fine)...
Protocol: A6 (CAN=True)
----------------------------------------------------------------
TROUBLE CODES
----------------------------------------------------------------
STORED (mode 03):
C0300 - (look up this code)
PENDING (mode 07):
P0113 - Intake air temp (IAT) circuit HIGH
C0700 - (look up this code)
PERMANENT (mode 0A):
C3F0A - (look up this code)
----------------------------------------------------------------
KEY LIVE VALUES (key ON)
----------------------------------------------------------------
Engine RPM 0 rpm
Coolant temp 33 C
Intake air temp 35 C
Intake MAP 98 kPa
Engine load 0 %
Accel/throttle pos n/a
Module voltage 13.1 V
================================================================
SCAN Mode-22 PIDs 1000 - 14FF (1280 PIDs)
================================================================
Logging hits to: C:\Users\justin\AppData\Local\Temp\ford-scan-hits.txt
Safe: Mode 22 is read-only. Will print every PID that answers.
... 100/1280 scanned in 28s (3.6/s), ~330s remaining, 0 hits so far
... 200/1280 scanned in 55s (3.6/s), ~298s remaining, 0 hits so far
HIT 1100 len= 7 raw=[01 00 40 05 7F 22 12]
HIT 1101 len= 5 raw=[00 62 11 01 00]
HIT 1102 len= 1 raw=[00]
HIT 1103 len= 5 raw=[00 62 11 03 00]
HIT 1104 len= 5 raw=[00 62 11 04 00]
HIT 1105 len= 1 raw=[00]
HIT 1106 len= 5 raw=[00 62 11 06 04]
HIT 1107 len= 5 raw=[00 62 11 07 00]
HIT 110C len= 4 raw=[00 7F 22 12]
HIT 1123 len= 5 raw=[1E 62 11 23 30]
HIT 1125 len= 1 raw=[FF]
HIT 1126 len= 5 raw=[00 62 11 26 00]
HIT 1127 len= 1 raw=[E7]
... 300/1280 scanned in 82s (3.6/s), ~269s remaining, 13 hits so far
HIT 1135 len= 2 raw=[FF FF]
HIT 1139 len= 5 raw=[2E 62 11 39 2E]
HIT 114A len= 5 raw=[7E 00 7F 22 12]
HIT 114B len= 5 raw=[A6 56 7F 22 12]
HIT 114D len= 2 raw=[81 40]
HIT 1155 len= 2 raw=[FF FF]
HIT 1165 len= 7 raw=[00 00 62 11 65 00 00]
HIT 1169 len= 2 raw=[00 00]
HIT 1172 len= 5 raw=[D1 62 11 72 D3]
HIT 1177 len= 5 raw=[00 C8 7F 22 12]
... 400/1280 scanned in 110s (3.6/s), ~241s remaining, 23 hits so far
HIT 11B3 len= 1 raw=[02]
HIT 11B4 len= 2 raw=[00 00]
HIT 11B5 len= 5 raw=[00 00 7F 22 31]
HIT 11B6 len= 4 raw=[8C 7F 22 31]
HIT 11B7 len= 5 raw=[00 00 7F 22 31]
HIT 11B8 len= 5 raw=[00 00 7F 22 31]
HIT 11BA len= 2 raw=[FF 5C]
HIT 11BD len= 5 raw=[87 1E 7F 22 31]
HIT 11C1 len= 7 raw=[00 00 62 11 C1 00 00]
... 500/1280 scanned in 138s (3.6/s), ~215s remaining, 32 hits so far
... 600/1280 scanned in 165s (3.6/s), ~187s remaining, 32 hits so far
... 700/1280 scanned in 192s (3.6/s), ~159s remaining, 32 hits so far
HIT 1310 len= 2 raw=[1C 92]
... 800/1280 scanned in 220s (3.6/s), ~132s remaining, 33 hits so far
... 900/1280 scanned in 247s (3.6/s), ~104s remaining, 33 hits so far
... 1000/1280 scanned in 274s (3.6/s), ~77s remaining, 33 hits so far
HIT 1410 len= 2 raw=[00 00]
HIT 1411 len= 5 raw=[00 00 7F 22 12]
HIT 1412 len= 2 raw=[00 00]
HIT 1434 len= 1 raw=[26]
HIT 1440 len= 2 raw=[01 89]
HIT 1441 len= 5 raw=[00 00 7F 22 12]
HIT 1442 len= 2 raw=[01 88]
HIT 1445 len= 2 raw=[01 8F]
HIT 1446 len= 2 raw=[00 16]
HIT 1447 len= 2 raw=[00 34]
HIT 1448 len= 2 raw=[00 11]
... 1100/1280 scanned in 303s (3.6/s), ~50s remaining, 44 hits so far
HIT 1450 len= 2 raw=[00 00]
HIT 1451 len= 2 raw=[00 00]
... 1200/1280 scanned in 331s (3.6/s), ~22s remaining, 46 hits so far
Done. 46 responding PIDs in 353s.
================================================================
6.0 POWER STROKE -- NO-START QUICK TRIAGE
================================================================
NO-CODE no-start basics to check by hand on a 6.0:
1. BATTERIES: both must be strong. Low voltage -> FICM won't boost ->
no injector fire. Load-test both; ~12.5V+ at rest, hold while cranking.
2. FICM voltage while cranking (~48V). The #1 6.0 cold no-start cause.
3. ICP (Injection Control Pressure): needs ~500 psi to fire. Big leaks =
STC fitting, oil rail O-rings, high-pressure oil hoses.
4. FUEL: lift pump (HFCM) priming, fuel filters, water-in-fuel.
5. CMP/CKP sensors (see codes above).
6. Glow plugs/relay if it's cold out (won't stop start, but hard start).
Done. (Re-run any time; codes persist until cleared.)
@@ -0,0 +1,48 @@
# Mode-22 scan 1000-14FF
# pid_hex len raw_bytes
1100 7 01 00 40 05 7F 22 12
1101 5 00 62 11 01 00
1102 1 00
1103 5 00 62 11 03 00
1104 5 00 62 11 04 00
1105 1 00
1106 5 00 62 11 06 04
1107 5 00 62 11 07 00
110C 4 00 7F 22 12
1123 5 1E 62 11 23 30
1125 1 FF
1126 5 00 62 11 26 00
1127 1 E7
1135 2 FF FF
1139 5 2E 62 11 39 2E
114A 5 7E 00 7F 22 12
114B 5 A6 56 7F 22 12
114D 2 81 40
1155 2 FF FF
1165 7 00 00 62 11 65 00 00
1169 2 00 00
1172 5 D1 62 11 72 D3
1177 5 00 C8 7F 22 12
11B3 1 02
11B4 2 00 00
11B5 5 00 00 7F 22 31
11B6 4 8C 7F 22 31
11B7 5 00 00 7F 22 31
11B8 5 00 00 7F 22 31
11BA 2 FF 5C
11BD 5 87 1E 7F 22 31
11C1 7 00 00 62 11 C1 00 00
1310 2 1C 92
1410 2 00 00
1411 5 00 00 7F 22 12
1412 2 00 00
1434 1 26
1440 2 01 89
1441 5 00 00 7F 22 12
1442 2 01 88
1445 2 01 8F
1446 2 00 16
1447 2 00 34
1448 2 00 11
1450 2 00 00
1451 2 00 00
@@ -0,0 +1,117 @@
================================================================
OBD-II Code Reader - ELM327 / 6.0 Power Stroke triage
================================================================
Auto-selected port: COM5 (USB-SERIAL CH340 (COM5))
(other ports seen: COM4, COM3 -- pass one as an argument if wrong)
Opening COM5 @ 38400 baud ...
Adapter : ELM327 v1.5
Battery : 11.7V (key ON; healthy 6.0 KOEO ~12.4-12.7V)
Connecting to vehicle (turn key to ON / RUN, engine off is fine)...
Protocol: A6 (CAN=True)
----------------------------------------------------------------
TROUBLE CODES
----------------------------------------------------------------
STORED (mode 03):
C0300 - (look up this code)
PENDING (mode 07):
C0701 - (look up this code)
P0113 - Intake air temp (IAT) circuit HIGH
PERMANENT (mode 0A):
C3F0A - (look up this code)
----------------------------------------------------------------
KEY LIVE VALUES (key ON)
----------------------------------------------------------------
Engine RPM 0 rpm
Coolant temp 33 C
Intake air temp 35 C
Intake MAP 98 kPa
Engine load 0 %
Accel/throttle pos n/a
Module voltage 12.39 V
================================================================
WATCH MODE (30s) -- Ctrl-C to stop early
================================================================
ATRV = battery V at OBD port, VPCM = PCM/module V (PID 0142).
TURN KEY TO START NOW (or whenever you're ready).
t(s) ATRV VPCM
-------- -------- --------
0.00 11.7 12.39
0.54 11.7 12.38
1.09 11.7 12.36
1.64 12.0 12.55
2.17 11.9 12.52
2.70 12.0 12.49
3.25 12.1 12.66
3.79 12.1 12.69
4.33 12.0 12.55
4.84 12.1 12.71
5.35 12.0 12.60
5.91 10.8 11.65
6.42 11.2 11.59
6.93 11.0 11.59
7.45 11.1 11.48
7.95 10.9 11.43
8.47 10.8 11.70
8.97 11.2 11.67
9.49 11.0 11.70
10.01 11.0 11.46
10.52 11.0 11.59
11.02 11.0 11.48
11.54 11.0 11.62
12.04 10.9 11.48
12.55 11.0 11.40
13.07 11.0 11.48
13.57 10.9 11.40
14.08 11.1 11.43
14.59 10.9 11.46
15.08 11.0 11.48
15.59 11.0 11.59
16.09 10.9 11.32
16.61 11.5 12.17
17.11 11.8 ?
17.74 11.8 ?
18.37 12.0 ?
19.02 11.8 ?
19.65 12.0 ?
20.27 11.9 ?
20.90 12.0 ?
21.54 12.0 ?
22.18 12.0 ?
22.81 12.1 ?
23.45 12.2 ?
24.10 12.1 ?
24.74 12.2 ?
25.38 12.0 ?
26.01 12.0 ?
26.66 12.0 ?
27.31 12.1 ?
27.95 12.2 ?
28.59 12.1 ?
29.23 12.2 ?
29.87 12.2 ?
================================================================
6.0 POWER STROKE -- NO-START QUICK TRIAGE
================================================================
NO-CODE no-start basics to check by hand on a 6.0:
1. BATTERIES: both must be strong. Low voltage -> FICM won't boost ->
no injector fire. Load-test both; ~12.5V+ at rest, hold while cranking.
2. FICM voltage while cranking (~48V). The #1 6.0 cold no-start cause.
3. ICP (Injection Control Pressure): needs ~500 psi to fire. Big leaks =
STC fitting, oil rail O-rings, high-pressure oil hoses.
4. FUEL: lift pump (HFCM) priming, fuel filters, water-in-fuel.
5. CMP/CKP sensors (see codes above).
6. Glow plugs/relay if it's cold out (won't stop start, but hard start).
Done. (Re-run any time; codes persist until cleared.)
+32 -5
View File
@@ -18,8 +18,14 @@ Pick-up notes for diagnosing the truck in the cab. Repo: `git.jpaul.io/justin/fo
## What the tool does / doesn't
- DOES: read stored/pending/permanent codes, decode them (6.0 codes flagged as
no-start suspects), clear codes (`--clear`), show battery voltage + key live PIDs.
- DOESN'T: Ford-enhanced diesel PIDs — **ICP, FICM main/sync voltage, IPR%**.
Those need FORScan. For those numbers tonight, measure at the FICM with a meter.
- EXPERIMENTAL (`--ford`): tries a small table of community-sourced Ford 6.0
Mode-22 PIDs (ICP, IPR%, FICM main/sync/logic V, EBP, EOT). **PID numbers and
scaling are TENTATIVE** — raw bytes are always printed; cross-check against a
meter or FORScan before trusting a value.
- `--pid XXXX`: one-shot probe of any 16-bit Mode-22 PID; prints raw bytes plus
a few common decodings so you can eyeball the right scaling.
- DOESN'T (yet): a verified Ford-enhanced PID set the way FORScan has. We're
building toward that — see follow-ups below.
## 6.0 no-start priority checklist (from the triage)
The 6.0 needs, to fire: **good batteries → FICM ~48V → ICP ~500 psi → fuel → cam/crank signal.**
@@ -42,14 +48,35 @@ The 6.0 needs, to fire: **good batteries → FICM ~48V → ICP ~500 psi → fuel
- Tool built + tested against the real adapter (init, all 3 DTC modes, live PIDs, clear flow).
- DTC parser unit-tested incl. a fixed bug: legacy ISO/PWM multi-frame responses repeat the
`43` header (was producing phantom codes) — fixed + regression-tested.
- **2026-06-29 in-cab session:** added `--watch`, `--ford`, `--pid`, and `--scan`
modes. Captured cranking voltages + ran a Mode-22 brute scan (46 PIDs hit).
Full session writeup + raw data in [diagnostics/2026-06-29-no-start/](diagnostics/2026-06-29-no-start/README.md).
**Headline:** not batteries, not fuel — almost certainly ICP / FICM / CMP.
**Unfinished:** FICM meter test on M/S/L was started but readings never logged.
- Pushed to `git.jpaul.io/justin/ford-obd`, branch `main`. Files: `obd_reader.py`,
`RUN_OBD.bat`, `README.md`, `README.txt`, `handoff.md`.
`RUN_OBD.bat`, `README.md`, `README.txt`, `handoff.md`, `diagnostics/`.
## To resume with Claude from the cab
Mention: "6.0 Power Stroke no-start, using the ford-obd tool (git.jpaul.io/justin/ford-obd)".
Then **paste the tool's full output** (codes + live values). Useful to also say: cranks vs.
no-crank, hot vs. cold, what changed before it died, and FICM/ICP readings if you metered them.
## To resume from the desktop (after the 2026-06-29 session)
Read [diagnostics/2026-06-29-no-start/README.md](diagnostics/2026-06-29-no-start/README.md)
first — that's the full state. Top open items:
1. Get FICM M/S/L meter readings (key-ON and during a short crank).
2. Re-probe the clean scan hits (1310, 1440-1451, 11Bx) with `--pid XXXX`
for uncontaminated data, then map them to known engine conditions.
3. Look at FORScan via the CyanLabs mirror to capture ground-truth PIDs.
## Open follow-ups (when off the truck)
- FORScan from the CyanLabs mirror once forscan.org is back, for ICP/FICM/IPR PIDs on the same adapter.
- Optional: add a few known Ford Mode-22 enhanced PIDs to the tool (verify PID numbers first).
- FORScan from the CyanLabs mirror once forscan.org is back — useful as the
ground truth for verifying our Mode-22 PID table.
- **Verify** every entry in `FORD_60_PIDS` (obd_reader.py) against either a
meter reading or FORScan, one at a time. As each is confirmed, drop the
"TENTATIVE" warning for that row.
- **Option 3**: brute-force scan of the Mode-22 PID space (e.g. `0x1000`-
`0x14FF`) and log which PIDs return data, so we can fingerprint what this
PCM exposes without external references. Hook it in as `--scan`.
- Add `--watch` mode that streams ATRV + PID 0142 (and the FICM PIDs once
verified) at ~5 Hz so we can watch the voltage sag during cranking.
+338 -11
View File
@@ -11,14 +11,20 @@ Scope (what a generic ELM327 CAN do, engine off / KOEO):
* Read key KOEO live PIDs (coolant, IAT, MAP, module voltage, RPM...)
* Battery voltage at the OBD port (ATRV)
What it CANNOT do: Ford-enhanced diesel PIDs (ICP, FICM sync/main volts, IPR%).
Those need FORScan's Ford Mode-22 PID set. This tool covers the generic basics
so you can read codes and triage tonight.
Experimental: with --ford it also tries a handful of community-sourced
Ford-enhanced Mode-22 PIDs (ICP, IPR%, FICM main/sync/logic voltage, EBP,
EOT). The PID numbers and scaling are TENTATIVE and need to be verified
against a known-good reading (meter or FORScan). Raw bytes are always
printed so you can sanity-check. Wrong PID = "no response", nothing
written to the truck.
Usage (Windows):
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
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
python obd_reader.py --ford # + try Ford 6.0 Mode-22 PIDs
python obd_reader.py --pid 1430 # probe one Mode-22 PID, show raw
python obd_reader.py --clear # erase stored + pending DTCs
Requires: pip install pyserial
"""
@@ -110,6 +116,61 @@ NO_START_CODES = {
}
# ---------------------------------------------------------------------------
# Ford 6.0L Power Stroke Mode-22 ("enhanced") PIDs.
#
# A generic ELM327 CAN'T natively decode Ford-enhanced data (ICP, FICM, IPR%)
# the way FORScan can, but the wire protocol is the same: we send Mode 22
# ("read data by identifier") with a 16-bit PID, and the PCM replies with
# raw bytes that we scale into engineering units.
#
# WARNING: every entry below is TENTATIVE. The PID numbers and scaling
# formulas are drawn from public 6.0 community references (FORScan extended
# PID dumps, diesel forum threads) and have NOT been verified against this
# specific truck. Treat decoded values as suspect until they're cross-
# checked against a meter or a known-good FORScan reading. To help with
# that, the tool ALWAYS prints the raw response bytes next to the decode.
#
# Wrong PIDs are safe -- the PCM just answers "no data" or a negative
# response (0x7F). Nothing is written to the truck.
#
# Each entry: (pid_hex, short_name, units, decoder|None, notes)
# pid_hex : 4 hex chars (the 16-bit Mode-22 PID)
# decoder : callable(list[int]) -> float, or None to show raw only
# ---------------------------------------------------------------------------
def _u16(b):
return (b[0] << 8) + b[1]
FORD_60_PIDS = [
# --- Injection Control Pressure (need ~500+ psi to fire) ---
("1209", "ICP", "psi", lambda b: _u16(b),
"Injection Control Pressure (need ~500+ psi to fire)"),
("121A", "ICP_DES", "psi", lambda b: _u16(b),
"ICP desired (commanded)"),
("1430", "ICP_V", "V", lambda b: _u16(b) / 1000.0,
"ICP sensor raw voltage"),
# --- Injection Pressure Regulator duty ---
("120B", "IPR", "%", lambda b: round(b[0] * 100 / 255, 1),
"IPR duty (high = trying hard to make ICP)"),
# --- FICM voltages -- THE 6.0 no-start metric (~48V cranking) ---
("1228", "FICM_MPWR", "V", lambda b: _u16(b) / 1000.0,
"FICM Main Power -- want ~48V cranking, <45V = suspect"),
("1229", "FICM_SYNC", "V", lambda b: _u16(b) / 1000.0,
"FICM Sync Power"),
("122A", "FICM_LPWR", "V", lambda b: _u16(b) / 1000.0,
"FICM Logic Power (~12V)"),
# --- Exhaust back pressure / oil temp (handy, not no-start critical) ---
("121C", "EBP", "psi", lambda b: _u16(b) / 100.0,
"Exhaust Back Pressure"),
("1310", "EOT", "C", lambda b: b[0] - 40,
"Engine Oil Temperature"),
]
# ---------------------------------------------------------------------------
# ELM327 plumbing
# ---------------------------------------------------------------------------
@@ -187,6 +248,32 @@ class ELM:
d = self.protocol.replace("A", "").strip()
return d[:1] in ("6", "7", "8", "9")
def mode22(self, pid_hex, timeout=3.0):
"""Send a Mode 22 ("read data by identifier") request.
pid_hex: 4 hex chars (e.g. '1430') -- accepts '0x', spaces, lowercase.
timeout: serial-read timeout in seconds (lower for scanning).
Returns the data bytes AFTER the '62 <hi> <lo>' echo, or None on
no-data / error / negative response (0x7F)."""
pid_hex = pid_hex.replace("0x", "").replace("0X", "").replace(" ", "").strip().upper()
if len(pid_hex) != 4 or any(c not in "0123456789ABCDEF" for c in pid_hex):
return None
lines = self.cmd("22" + pid_hex, read_timeout=timeout)
joined = "".join(lines).upper().replace(" ", "")
if any(s in joined for s in ("NODATA", "ERROR", "UNABLE", "STOPPED", "CANERROR", "BUSERROR")):
return None
data = hexbytes(lines)
p_hi = int(pid_hex[:2], 16)
p_lo = int(pid_hex[2:], 16)
# Positive response: 62 <pid_hi> <pid_lo> <data...>
for i in range(len(data) - 2):
if data[i] == 0x62 and data[i + 1] == p_hi and data[i + 2] == p_lo:
return data[i + 3:]
# Negative response: 7F 22 <nrc>
for i in range(len(data) - 1):
if data[i] == 0x7F and data[i + 1] == 0x22:
return None
return None
def close(self):
try:
self.ser.close()
@@ -315,6 +402,163 @@ def live_data(elm):
return rows
def read_ford_pids(elm, pids=None):
"""Run a batch of Ford 6.0 Mode-22 PIDs and print decoded + raw bytes.
pids: list of FORD_60_PIDS entries to query (default: all)."""
if pids is None:
pids = FORD_60_PIDS
print("\n" + "-" * 64)
print(" FORD 6.0 ENHANCED (Mode 22) -- TENTATIVE / UNVERIFIED")
print("-" * 64)
print(" PID numbers and scaling are community-sourced and NOT yet")
print(" verified on this truck. Raw bytes shown for sanity-checking.")
print(" 'no response' = wrong PID or this PCM doesn't expose it.")
print()
any_response = False
for pid, name, unit, decode, notes in pids:
raw = elm.mode22(pid)
if raw is None or len(raw) == 0:
print(f" {name:10} ({pid}) no response -- {notes}")
continue
any_response = True
raw_hex = " ".join(f"{b:02X}" for b in raw)
decoded = "(no decoder)"
if decode is not None:
try:
val = decode(raw)
decoded = f"{val:>8.2f} {unit}"
except Exception:
decoded = "(decode err)"
print(f" {name:10} ({pid}) {decoded:>14} raw=[{raw_hex}]")
if not any_response:
print("\n >> Nothing answered. Either none of these PID numbers")
print(" match this PCM, or the bus isn't accepting Mode 22 in")
print(" the current state. Try with the key in RUN and the")
print(" ECU connected (protocol negotiated above).")
def probe_pid(elm, pid_hex):
"""One-shot Mode-22 probe for manual exploration. Prints raw + a few
common decodings so you can eyeball which scaling fits."""
print("\n" + "-" * 64)
print(f" Mode-22 probe: PID {pid_hex.upper()}")
print("-" * 64)
raw = elm.mode22(pid_hex)
if raw is None:
print(" no response (wrong PID, or module doesn't expose it).")
return
raw_hex = " ".join(f"{b:02X}" for b in raw)
print(f" raw bytes : [{raw_hex}] (len={len(raw)})")
if len(raw) >= 1:
print(f" as u8 : {raw[0]} ({raw[0] * 100 / 255:.1f}% if duty)")
print(f" as temp : {raw[0] - 40} C (raw-40, OBD-II convention)")
if len(raw) >= 2:
u = (raw[0] << 8) + raw[1]
print(f" as u16 : {u} ({u/1000:.3f} V if mV, {u/100:.2f} if x100)")
if len(raw) >= 4:
u32 = (raw[0] << 24) + (raw[1] << 16) + (raw[2] << 8) + raw[3]
print(f" as u32 : {u32}")
def watch_loop(elm, seconds=20, with_ford=False):
"""Stream battery + module voltage as fast as we can for N seconds.
Designed for cranking: start it, then turn the key.
with_ford=True also streams Mode-22 FICM/ICP PIDs -- only enable once
those PIDs are verified for this PCM, otherwise each 'no response'
timeout starves the sample rate."""
stream_pids = []
if with_ford:
stream_pids = [
("1228", "FICM_M", "V", lambda b: (b[0] << 8 | b[1]) / 1000.0),
("1229", "FICM_S", "V", lambda b: (b[0] << 8 | b[1]) / 1000.0),
("122A", "FICM_L", "V", lambda b: (b[0] << 8 | b[1]) / 1000.0),
("1209", "ICP", "psi", lambda b: (b[0] << 8 | b[1])),
("120B", "IPR", "%", lambda b: round(b[0] * 100 / 255, 1)),
]
print("\n" + "=" * 64)
print(f" WATCH MODE ({seconds}s) -- Ctrl-C to stop early")
print("=" * 64)
print(" ATRV = battery V at OBD port, VPCM = PCM/module V (PID 0142).")
if with_ford:
print(" Mode-22 PIDs included -- '-' means no response.")
print(" TURN KEY TO START NOW (or whenever you're ready).\n")
headers = ["t(s)", "ATRV", "VPCM"] + [f"{n}({u})" for _, n, u, _ in stream_pids]
print(" " + " ".join(f"{h:>8}" for h in headers))
print(" " + " ".join("-" * 8 for _ in headers))
start = time.time()
end = start + seconds
try:
while time.time() < end:
t = time.time() - start
atrv = " ".join(elm.cmd("ATRV", read_timeout=1.0)).replace("V", "").strip()
vpcm_raw = read_pid(elm, "42", 2)
vpcm = "?" if vpcm_raw is None else f"{((vpcm_raw[0] << 8) + vpcm_raw[1]) / 1000:.2f}"
row = [f"{t:6.2f}", atrv or "?", vpcm]
for pid, _, _, decode in stream_pids:
raw = elm.mode22(pid)
if raw is None or len(raw) < 2:
row.append("-")
else:
try:
row.append(f"{decode(raw):.2f}")
except Exception:
row.append("err")
print(" " + " ".join(f"{c:>8}" for c in row), flush=True)
except KeyboardInterrupt:
print("\n (stopped)")
def scan_mode22(elm, start, end, log_path=None):
"""Brute-force scan Mode-22 PIDs over [start, end] (inclusive, ints).
Logs every PID that returns data. Safe: Mode 22 is read-only by
definition. Writes hits to log_path as it goes so we don't lose them
if the bus drops mid-scan."""
# Tune ELM for fast scanning: max adaptive timing, short response wait
elm.cmd("ATAT2")
elm.cmd("ATST19") # 25 * 4ms = 100ms ECU response wait
total = end - start + 1
print("\n" + "=" * 64)
print(f" SCAN Mode-22 PIDs {start:04X} - {end:04X} ({total} PIDs)")
print("=" * 64)
print(f" Logging hits to: {log_path}" if log_path else " (no log file)")
print(" Safe: Mode 22 is read-only. Will print every PID that answers.\n")
if log_path:
with open(log_path, "w") as f:
f.write(f"# Mode-22 scan {start:04X}-{end:04X}\n")
f.write("# pid_hex len raw_bytes\n")
hits = 0
t0 = time.time()
for i, pid in enumerate(range(start, end + 1)):
pid_hex = f"{pid:04X}"
raw = elm.mode22(pid_hex, timeout=0.6)
if raw is not None and len(raw) > 0:
raw_hex = " ".join(f"{b:02X}" for b in raw)
line = f" HIT {pid_hex} len={len(raw):2d} raw=[{raw_hex}]"
print(line, flush=True)
hits += 1
if log_path:
with open(log_path, "a") as f:
f.write(f"{pid_hex} {len(raw)} {raw_hex}\n")
if (i + 1) % 100 == 0:
elapsed = time.time() - t0
rate = (i + 1) / elapsed if elapsed > 0 else 0
remain = (total - i - 1) / rate if rate > 0 else 0
print(f" ... {i+1}/{total} scanned in {elapsed:.0f}s ({rate:.1f}/s), "
f"~{remain:.0f}s remaining, {hits} hits so far", flush=True)
elapsed = time.time() - t0
print(f"\n Done. {hits} responding PIDs in {elapsed:.0f}s.")
# Restore default-ish timing
elm.cmd("ATAT1")
elm.cmd("ATST32")
return hits
def clear_codes(elm):
"""OBD-II mode 04: clear DTCs + freeze frame, reset monitors.
Returns True if the ECU acknowledged ('44')."""
@@ -348,12 +592,79 @@ def find_ports():
# Main
# ---------------------------------------------------------------------------
def main():
args = [a for a in sys.argv[1:] if not a.startswith("-")]
verbose = "-v" in sys.argv or "--verbose" in sys.argv
do_clear = "--clear" in sys.argv
raw_args = sys.argv[1:]
verbose = "-v" in raw_args or "--verbose" in raw_args
do_clear = "--clear" in raw_args
do_ford = "--ford" in raw_args or "--diesel" in raw_args
port = args[0] if len(args) >= 1 else None
baud = int(args[1]) if len(args) >= 2 else 38400
# --pid XXXX : one-shot Mode-22 probe of an arbitrary 16-bit PID
probe = None
if "--pid" in raw_args:
i = raw_args.index("--pid")
if i + 1 < len(raw_args):
probe = raw_args[i + 1]
# --watch [N] : stream voltages + FICM/ICP PIDs for N seconds (default 20)
do_watch = "--watch" in raw_args
watch_secs = 20
if do_watch:
i = raw_args.index("--watch")
if i + 1 < len(raw_args):
try:
watch_secs = int(raw_args[i + 1])
except ValueError:
pass
# --scan START-END : brute-force scan Mode-22 PIDs. Default 1000-14FF.
do_scan = "--scan" in raw_args
scan_start, scan_end = 0x1000, 0x14FF
scan_log = None
if do_scan:
i = raw_args.index("--scan")
if i + 1 < len(raw_args) and "-" in raw_args[i + 1] and not raw_args[i + 1].startswith("-"):
try:
a, b = raw_args[i + 1].split("-", 1)
scan_start, scan_end = int(a, 16), int(b, 16)
except ValueError:
pass
if "--scan-log" in raw_args:
i = raw_args.index("--scan-log")
if i + 1 < len(raw_args):
scan_log = raw_args[i + 1]
# Positional args: [port] [baud] -- skip flags and their values
pos = []
i = 0
while i < len(raw_args):
a = raw_args[i]
if a == "--pid" and i + 1 < len(raw_args):
i += 2 # always consumes next arg
continue
if a == "--scan-log" and i + 1 < len(raw_args):
i += 2
continue
if a == "--watch":
if i + 1 < len(raw_args) and raw_args[i + 1].isdigit():
i += 2
else:
i += 1
continue
if a == "--scan":
# Consume next arg only if it looks like a range "AAAA-BBBB"
if (i + 1 < len(raw_args) and "-" in raw_args[i + 1]
and not raw_args[i + 1].startswith("-")):
i += 2
else:
i += 1
continue
if a.startswith("-"):
i += 1
continue
pos.append(a)
i += 1
port = pos[0] if len(pos) >= 1 else None
baud = int(pos[1]) if len(pos) >= 2 else 38400
print("=" * 64)
print(" OBD-II Code Reader - ELM327 / 6.0 Power Stroke triage")
@@ -431,6 +742,22 @@ def main():
for label, val in live_data(elm):
print(f" {label:22} {val}")
# ---- Ford 6.0 enhanced PIDs (Mode 22) ----
if do_ford:
read_ford_pids(elm)
# ---- One-shot manual Mode-22 probe ----
if probe:
probe_pid(elm, probe)
# ---- Watch mode: stream voltages for N seconds ----
if do_watch:
watch_loop(elm, seconds=watch_secs)
# ---- Brute-force Mode-22 scan ----
if do_scan:
scan_mode22(elm, scan_start, scan_end, log_path=scan_log)
# ---- Optional clear (mode 04), only with --clear ----
if do_clear:
run_clear(elm, all_codes)