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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
This commit is contained in:
2026-06-29 19:17:35 -04:00
commit 0491d37a2e
5 changed files with 651 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.pyc
.venv/
+49
View File
@@ -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 <https://www.python.org/downloads/> — 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.
+43
View File
@@ -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.
+23
View File
@@ -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
+533
View File
@@ -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 -> '<svc> <count> <pairs...>'. 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 <pid> <data...>
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()