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:
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user