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:
+338
-11
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user