commit 0491d37a2e34c7c256b8522e146da2e9a27b9815 Author: Justin Paul Date: Mon Jun 29 19:17:35 2026 -0400 ford-obd: ELM327 OBD-II reader + 6.0 Power Stroke no-start triage Read stored/pending/permanent DTCs, decode with 6.0-relevant codes flagged, guarded mode-04 clear (--clear), key live PIDs + battery voltage, and a 6.0 no-start triage checklist. Tested against a CH340 ELM327 v1.5 adapter. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00f2d38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8604b95 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# 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](https://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 (Windows) + +1. Install the CH340 driver (WCH `CH341SER`) so the adapter appears as + `USB-SERIAL CH340 (COMx)` in Device Manager → Ports. +2. Install Python from — tick **Add Python to PATH**. + +## 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 +``` + +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. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..d487c55 --- /dev/null +++ b/README.txt @@ -0,0 +1,43 @@ +6.0 POWER STROKE OBD-II CODE READER (works on any OBD-II vehicle) +================================================================== +Built tonight for the CH340/ELM327 adapter. Reads trouble codes, key +live values, and prints a 6.0 no-start triage. Tested against your +actual adapter. + +WHAT YOU GET + obd_reader.py the tool + RUN_OBD.bat double-click launcher for Windows (auto-installs pyserial) + README.txt this file + +ONE-TIME SETUP ON THE WINDOWS LAPTOP + 1. Install the CH340 driver (you're already doing this) so the adapter + shows up as "USB-SERIAL CH340 (COMx)" in Device Manager > Ports. + 2. Install Python from https://www.python.org/downloads/ + IMPORTANT: tick "Add Python to PATH" on the first install screen. + +RUN IT + Easiest: double-click RUN_OBD.bat + Manual: open Command Prompt in this folder and run: + python obd_reader.py + force a port/baud if needed: + python obd_reader.py COM5 + python obd_reader.py COM5 9600 + add -v to see the raw ELM327 traffic (for troubleshooting): + python obd_reader.py COM5 -v + +ON THE TRUCK + - Plug the adapter into the OBD port (under the dash, driver side). + - Turn the key to RUN (not just ACC). Engine off is fine for codes. + - Run the tool. It reads STORED, PENDING and PERMANENT codes, + flags no-start suspects, shows battery voltage + key live values, + then prints the 6.0 triage checklist. + +SCOPE / HONESTY + A generic ELM327 reads standard OBD-II: codes, generic PIDs, voltage. + It does NOT read Ford-enhanced diesel PIDs (ICP, FICM main/sync volts, + IPR%). Those need FORScan. This covers reading codes + the basics so + you can triage tonight. For FICM/ICP numbers, measure at the FICM with + a meter, or get FORScan from the CyanLabs mirror when it's back up. + + Default baud is 38400 (measured on your adapter). If you get garbage, + try 9600. diff --git a/RUN_OBD.bat b/RUN_OBD.bat new file mode 100644 index 0000000..67c12d9 --- /dev/null +++ b/RUN_OBD.bat @@ -0,0 +1,23 @@ +@echo off +REM === 6.0 Power Stroke OBD reader launcher === +REM Double-click this on Windows. It installs pyserial (once) then runs. + +where python >nul 2>nul +if errorlevel 1 ( + echo Python is not installed or not on PATH. + echo Install it from https://www.python.org/downloads/ ^(check "Add Python to PATH"^), + echo then double-click this file again. + pause + exit /b 1 +) + +echo Ensuring pyserial is installed... +python -m pip install --quiet pyserial + +echo. +echo Starting OBD reader. Turn the truck key to RUN ^(engine off is fine^). +echo If it can't find the port, pass it like: RUN_OBD.bat COM5 +echo. +python "%~dp0obd_reader.py" %* +echo. +pause diff --git a/obd_reader.py b/obd_reader.py new file mode 100644 index 0000000..53a8524 --- /dev/null +++ b/obd_reader.py @@ -0,0 +1,533 @@ +#!/usr/bin/env python3 +""" +obd_reader.py -- Minimal ELM327 OBD-II code reader + no-start triage. + +Built for a Ford 6.0L Power Stroke (2003-2007 Super Duty/Excursion) using a +generic CH340 ELM327 adapter, but works on any OBD-II vehicle. + +Scope (what a generic ELM327 CAN do, engine off / KOEO): + * Read STORED (mode 03), PENDING (mode 07) and PERMANENT (mode 0A) DTCs + * Decode P/C/B/U codes, flag the ones that commonly cause a no-start + * 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. + +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 + +Requires: pip install pyserial +""" + +import sys +import time + +try: + import serial + from serial.tools import list_ports +except ImportError: + print("ERROR: pyserial is not installed. Run: pip install pyserial") + sys.exit(1) + + +# --------------------------------------------------------------------------- +# DTC description table. Generic SAE codes + notable 6.0 Power Stroke codes. +# Codes not listed still print (the 5-char code itself is searchable). +# --------------------------------------------------------------------------- +DTC_DB = { + # --- Fuel / rail --- + "P0087": "Fuel rail/system pressure too LOW", + "P0088": "Fuel rail/system pressure too HIGH", + "P0148": "Fuel delivery error (6.0: low fuel pressure / HPOP / IPR)", + "P0191": "Fuel rail pressure sensor range/performance", + # --- Air / sensors --- + "P0101": "MAF sensor range/performance", + "P0102": "MAF sensor circuit LOW", + "P0103": "MAF sensor circuit HIGH", + "P0107": "MAP/baro sensor circuit LOW", + "P0108": "MAP/baro sensor circuit HIGH", + "P0112": "Intake air temp (IAT) circuit LOW", + "P0113": "Intake air temp (IAT) circuit HIGH", + "P0117": "Engine coolant temp (ECT) circuit LOW", + "P0118": "Engine coolant temp (ECT) circuit HIGH", + "P0122": "Throttle/pedal position sensor LOW", + "P0123": "Throttle/pedal position sensor HIGH", + "P0128": "Coolant thermostat (below regulating temp)", + # --- Position sensors (NO-START critical) --- + "P0335": "Crankshaft position (CKP) sensor circuit <-- NO-START", + "P0336": "Crankshaft position (CKP) range/performance <-- NO-START", + "P0340": "Camshaft position (CMP) sensor circuit <-- NO-START", + "P0341": "Camshaft position (CMP) range/performance <-- NO-START", + "P0344": "Camshaft position sensor intermittent", + # --- Glow plugs / cold start --- + "P0380": "Glow plug / heater circuit 'A'", + "P0670": "Glow plug control module circuit", + "P0671": "Glow plug cylinder 1 circuit", + "P0672": "Glow plug cylinder 2 circuit", + "P0673": "Glow plug cylinder 3 circuit", + "P0674": "Glow plug cylinder 4 circuit", + "P0675": "Glow plug cylinder 5 circuit", + "P0676": "Glow plug cylinder 6 circuit", + "P0677": "Glow plug cylinder 7 circuit", + "P0678": "Glow plug cylinder 8 circuit", + # --- Injector electrical (6.0 uses these for the FICM/injectors) --- + "P0263": "Cyl 1 contribution/balance", + "P0266": "Cyl 2 contribution/balance", + "P0269": "Cyl 3 contribution/balance", + "P0272": "Cyl 4 contribution/balance", + "P0275": "Cyl 5 contribution/balance", + "P0278": "Cyl 6 contribution/balance", + "P0281": "Cyl 7 contribution/balance", + "P0284": "Cyl 8 contribution/balance", + "P0261": "Cyl 1 injector circuit LOW", + "P0264": "Cyl 2 injector circuit LOW", + "P0267": "Cyl 3 injector circuit LOW", + "P0270": "Cyl 4 injector circuit LOW", + "P0273": "Cyl 5 injector circuit LOW", + "P0276": "Cyl 6 injector circuit LOW", + "P0279": "Cyl 7 injector circuit LOW", + "P0282": "Cyl 8 injector circuit LOW", + # --- Control modules / power --- + "P0606": "PCM processor fault", + "P0611": "Fuel injector control module (FICM) performance <-- 6.0 no-start", + "P1316": "Injector circuit/FICM codes detected (check FICM) <-- 6.0 no-start", + # --- Communication (a dead module can cause a no-start) --- + "U0073": "Control module communication bus 'A' off", + "U0100": "Lost communication with PCM/ECM <-- NO-START possible", + "U0101": "Lost communication with TCM", + "U0107": "Lost communication with throttle actuator control module", +} + +# Codes we explicitly call out as no-start suspects +NO_START_CODES = { + "P0335", "P0336", "P0340", "P0341", "P0344", + "P0611", "P1316", "P0148", "P0087", + "U0100", "U0073", "P0606", +} + + +# --------------------------------------------------------------------------- +# ELM327 plumbing +# --------------------------------------------------------------------------- +class ELM: + PROMPT = b">" + + def __init__(self, port, baud, verbose=False): + self.verbose = verbose + self.ser = serial.Serial(port, baud, timeout=0.2) + self.protocol = "?" + time.sleep(0.3) + self.ser.reset_input_buffer() + + def cmd(self, s, settle=0.0, read_timeout=4.0): + """Send a command, read until the '>' prompt, return clean lines.""" + self.ser.reset_input_buffer() + self.ser.write((s + "\r").encode()) + if settle: + time.sleep(settle) + buf = bytearray() + deadline = time.time() + read_timeout + while time.time() < deadline: + chunk = self.ser.read(256) + if chunk: + buf += chunk + if self.PROMPT in buf: + break + else: + if self.PROMPT in buf: + break + raw = buf.decode("ascii", "replace") + if self.verbose: + print(f" [TX {s!r}] -> {raw!r}") + # Strip echo, prompt, blank lines + lines = [] + for ln in raw.replace(">", "").split("\r"): + ln = ln.strip() + if not ln or ln == s: + continue + lines.append(ln) + return lines + + def init(self): + self.cmd("ATZ", settle=1.0) # reset + self.cmd("ATE0") # echo off (clean parsing) + self.cmd("ATL0") # linefeeds off + self.cmd("ATS0") # spaces off + self.cmd("ATH0") # headers off + self.cmd("ATAT1") # adaptive timing + self.cmd("ATSP0") # auto protocol + + def identify(self): + ver = " ".join(self.cmd("ATI")) or "?" + volts = " ".join(self.cmd("ATRV")) or "?" + return ver, volts + + def connect_vehicle(self): + """Send 0100 to force protocol negotiation. Returns True if the ECU answers.""" + for _ in range(3): + resp = self.cmd("0100", read_timeout=8.0) + joined = "".join(resp).upper() + if "41" in joined and "00" in joined: + pn = " ".join(self.cmd("ATDPN")) + self.protocol = pn or "?" + return True + if any(x in joined for x in ("UNABLE", "NODATA", "NO DATA", "ERROR", "SEARCHING")): + time.sleep(0.5) + continue + # last protocol description even if no 0100 support + self.protocol = " ".join(self.cmd("ATDPN")) or "?" + return False + + def is_can(self): + # ATDPN returns e.g. "6" (or "A6" if auto). Protocols 6-9 are CAN. + d = self.protocol.replace("A", "").strip() + return d[:1] in ("6", "7", "8", "9") + + def close(self): + try: + self.ser.close() + except Exception: + pass + + +# --------------------------------------------------------------------------- +# DTC decoding +# --------------------------------------------------------------------------- +LETTER = {0: "P", 1: "C", 2: "B", 3: "U"} + + +def decode_dtc(b1, b2): + letter = LETTER[(b1 >> 6) & 0x3] + d1 = (b1 >> 4) & 0x3 + d2 = b1 & 0xF + return f"{letter}{d1}{d2:X}{b2:02X}" + + +def line_bytes(ln): + """Convert one response line to byte ints. Strips a CAN frame-index + prefix like '0:' / '1:' and ignores any non-hex line (status text).""" + ln = ln.replace(" ", "") + if len(ln) >= 2 and ln[1] == ":" and ln[0] in "0123456789": + ln = ln[2:] # drop multiframe index 'N:' + if not ln or any(c not in "0123456789ABCDEFabcdef" for c in ln): + return [] + return [int(ln[i:i + 2], 16) for i in range(0, len(ln) - 1, 2)] + + +def hexbytes(lines): + """Flatten response lines into a list of byte ints, dropping non-hex noise.""" + out = [] + for ln in lines: + out.extend(line_bytes(ln)) + return out + + +def parse_dtcs(lines, service_byte, is_can): + """ + service_byte: 0x43 for mode 03, 0x47 for 07, 0x4A for 0A. + Returns list of DTC strings. + + CAN : one logical message -> ' '. Reassemble all + frames, strip svc byte + count, then read pairs. + Legacy (ISO 9141 / KWP / J1850): EACH line is its own message that REPEATS + the service byte and carries up to 3 DTC pairs. Parse per line so the + repeated svc headers aren't mistaken for DTC data. + """ + pairs = [] + if is_can: + data = hexbytes(lines) + if service_byte in data: + data = data[data.index(service_byte) + 1:] + if data: + data = data[1:] # drop DTC-count byte + pairs = data + else: + for ln in lines: + data = line_bytes(ln) + if service_byte in data: + data = data[data.index(service_byte) + 1:] + elif data and data[0] == service_byte: + data = data[1:] + else: + continue # not a DTC data line for this mode + pairs.extend(data) + + dtcs = [] + for i in range(0, len(pairs) - 1, 2): + b1, b2 = pairs[i], pairs[i + 1] + if b1 == 0 and b2 == 0: + continue + dtcs.append(decode_dtc(b1, b2)) + # de-dup, keep order + seen, uniq = set(), [] + for d in dtcs: + if d not in seen: + seen.add(d) + uniq.append(d) + return uniq + + +# --------------------------------------------------------------------------- +# Live PIDs (mode 01) +# --------------------------------------------------------------------------- +def read_pid(elm, pid, nbytes): + lines = elm.cmd(f"01{pid}") + data = hexbytes(lines) + # response: 41 + if 0x41 in data: + i = data.index(0x41) + payload = data[i + 2:i + 2 + nbytes] + if len(payload) == nbytes: + return payload + return None + + +def live_data(elm): + rows = [] + + def add(label, payload, fn, unit): + if payload is not None: + try: + rows.append((label, f"{fn(payload)} {unit}")) + return + except Exception: + pass + rows.append((label, "n/a")) + + add("Engine RPM", read_pid(elm, "0C", 2), + lambda p: round(((p[0] << 8) + p[1]) / 4), "rpm") + add("Coolant temp", read_pid(elm, "05", 1), + lambda p: p[0] - 40, "C") + add("Intake air temp", read_pid(elm, "0F", 1), + lambda p: p[0] - 40, "C") + add("Intake MAP", read_pid(elm, "0B", 1), + lambda p: p[0], "kPa") + add("Engine load", read_pid(elm, "04", 1), + lambda p: round(p[0] * 100 / 255), "%") + add("Accel/throttle pos", read_pid(elm, "11", 1), + lambda p: round(p[0] * 100 / 255), "%") + add("Module voltage", read_pid(elm, "42", 2), + lambda p: round(((p[0] << 8) + p[1]) / 1000, 2), "V") + return rows + + +def clear_codes(elm): + """OBD-II mode 04: clear DTCs + freeze frame, reset monitors. + Returns True if the ECU acknowledged ('44').""" + lines = elm.cmd("04", read_timeout=6.0) + data = hexbytes(lines) + joined = "".join(lines).upper() + if 0x44 in data: + return True + if "OK" in joined and "NODATA" not in joined: + return True + return False + + +# --------------------------------------------------------------------------- +# Port discovery +# --------------------------------------------------------------------------- +def find_ports(): + ports = list(list_ports.comports()) + # Prioritise CH340 / serial converters + def score(p): + s = (p.description + " " + (p.manufacturer or "")).lower() + if "ch340" in s or "1a86" in s: + return 0 + if "serial" in s or "usb" in s: + return 1 + return 2 + return sorted(ports, key=score) + + +# --------------------------------------------------------------------------- +# 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 + + port = args[0] if len(args) >= 1 else None + baud = int(args[1]) if len(args) >= 2 else 38400 + + print("=" * 64) + print(" OBD-II Code Reader - ELM327 / 6.0 Power Stroke triage") + print("=" * 64) + + if not port: + cands = find_ports() + if not cands: + print("\nNo serial ports found. Plug in the adapter, install the") + print("CH340 driver, then pass the port: python obd_reader.py COM5") + return + port = cands[0].device + print(f"\nAuto-selected port: {port} ({cands[0].description})") + if len(cands) > 1: + others = ", ".join(p.device for p in cands[1:]) + print(f"(other ports seen: {others} -- pass one as an argument if wrong)") + + print(f"Opening {port} @ {baud} baud ...") + try: + elm = ELM(port, baud, verbose=verbose) + except Exception as e: + print(f" Could not open {port}: {e}") + print(" Try another COM port, or check the CH340 driver in Device Manager.") + return + + try: + elm.init() + ver, volts = elm.identify() + print(f" Adapter : {ver}") + print(f" Battery : {volts} (key ON; healthy 6.0 KOEO ~12.4-12.7V)") + + print("\nConnecting to vehicle (turn key to ON / RUN, engine off is fine)...") + ok = elm.connect_vehicle() + print(f" Protocol: {elm.protocol} (CAN={elm.is_can()})") + if not ok: + print(" >> No response from the ECU on 0100.") + print(" - Make sure the key is in RUN (not just ACC).") + print(" - Reseat the adapter in the OBD port under the dash.") + print(" - We'll still try to read codes below anyway.\n") + + is_can = elm.is_can() + + # ---- DTCs ---- + print("\n" + "-" * 64) + print(" TROUBLE CODES") + print("-" * 64) + + groups = [ + ("STORED (mode 03)", "03", 0x43), + ("PENDING (mode 07)", "07", 0x47), + ("PERMANENT (mode 0A)", "0A", 0x4A), + ] + all_codes = [] + for title, mode, svc in groups: + lines = elm.cmd(mode, read_timeout=5.0) + joined = "".join(lines).upper() + if "NODATA" in joined or "NO DATA" in joined: + print(f"\n {title}: none reported") + continue + dtcs = parse_dtcs(lines, svc, is_can) + if not dtcs: + print(f"\n {title}: none") + continue + print(f"\n {title}:") + for d in dtcs: + desc = DTC_DB.get(d, "(look up this code)") + flag = " *** NO-START SUSPECT ***" if d in NO_START_CODES else "" + print(f" {d} - {desc}{flag}") + all_codes.append(d) + + # ---- Live data ---- + print("\n" + "-" * 64) + print(" KEY LIVE VALUES (key ON)") + print("-" * 64) + for label, val in live_data(elm): + print(f" {label:22} {val}") + + # ---- Optional clear (mode 04), only with --clear ---- + if do_clear: + run_clear(elm, all_codes) + + # ---- Triage ---- + print_triage(all_codes) + + finally: + elm.close() + print("\nDone. (Re-run any time; codes persist until cleared.)") + + +def run_clear(elm, codes): + print("\n" + "=" * 64) + print(" CLEAR CODES (OBD-II mode 04)") + print("=" * 64) + print(""" + This erases stored + pending codes AND freeze-frame data, and resets + emissions monitors. Read on a no-start first: + * If the fault is still present, the code comes RIGHT BACK. + * Don't clear before you've written the codes down (shown above). + * Permanent codes (mode 0A) will NOT clear until the fault is fixed + and the vehicle self-clears them over several drive cycles. + * Engine should be OFF, key in RUN, for a clean clear. +""") + if codes: + print(" Codes currently set: " + ", ".join(codes)) + else: + print(" No stored codes were read above (nothing to clear, or key not in RUN).") + + try: + ans = input('\n Type "CLEAR" to confirm (anything else cancels): ').strip() + except (EOFError, KeyboardInterrupt): + ans = "" + if ans != "CLEAR": + print(" Cancelled. No codes cleared.") + return + + print(" Sending clear command...") + if clear_codes(elm): + print(" >> ECU acknowledged. Codes cleared.") + # Re-read to show what (if anything) came straight back + again = parse_dtcs(elm.cmd("03", read_timeout=5.0), 0x43, elm.is_can()) + if again: + print(" >> These codes RETURNED immediately (active fault present):") + for d in again: + print(f" {d} - {DTC_DB.get(d, '(look up this code)')}") + else: + print(" >> No stored codes on re-read.") + else: + print(" >> No clear acknowledgement from the ECU.") + print(" Make sure the key is in RUN and the vehicle is connected,") + print(" then try again.") + + +def print_triage(codes): + print("\n" + "=" * 64) + print(" 6.0 POWER STROKE -- NO-START QUICK TRIAGE") + print("=" * 64) + cset = set(codes) + + if cset & {"P0335", "P0336", "P0340", "P0341", "P0344"}: + print(""" + >> CAM/CRANK SENSOR code present. The 6.0 needs BOTH the CKP and CMP + signal to fire injectors. A failed CMP (very common) = crank, no-start. + Check: CMP sensor on front of engine, its connector, and wiring.""") + + if cset & {"P0611", "P1316"} or any(c in cset for c in + ("P0263","P0266","P0269","P0272","P0275","P0278","P0281","P0284")): + print(""" + >> FICM / INJECTOR codes. The FICM (Fuel Injector Control Module) drives + the injectors at ~48V. Weak FICM = hard/no start, esp. when cold. + Check FICM Main & Sync voltage (needs FORScan or a meter at the FICM); + should be ~48V cranking. <45V is a classic 6.0 no-start.""") + + if cset & {"P0087", "P0148", "P0191"}: + print(""" + >> FUEL PRESSURE code. Check low-pressure fuel (HFCM/lift pump, filters) + AND high-pressure oil (HPOP/IPR) -- the 6.0 needs ~500+ psi ICP to fire.""") + + if cset & {"U0100", "U0073", "P0606"}: + print(""" + >> MODULE COMMUNICATION / PCM fault. A PCM that isn't talking can prevent + start entirely. Check PCM power/grounds and the connector.""") + + print(""" + 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). +""") + + +if __name__ == "__main__": + main()