My car engine suddenly stopped one morning — luckily only 500 meters from home. The dashboard was showing « système antipollution défaillant », which is French for "give me money". I didn't find any open-source OBD software that could talk to my car, so I reverse-engineered the whole thing with GLM-5.3-Flash. This is the story of going from a silent USB dongle to reading my engine fuel pressure live, in pure C.
1. The problem
The car is a 2008 Citroën C3 1.6 HDi. One morning it cut out without warning, luckily close enough to home that it was an annoyance instead of a disaster. French cars of this era announce any trouble with the antipollution message, which can cover anything from a faulty sensor to a dying injection pump. A garage would plug in their DiagBox, read two codes, and charge you the price of a nice dinner.
Every car sold since 2001 has a standardized OBD-II port under the dash. Every developer story I had ever read said the same thing: buy a cheap ELM327 dongle, run some open-source scanner, read your codes. So I bought the dongle. Around fifteen euros. What could go wrong?
2. When the usual tools don't work
Well, almost everything went wrong. No open-source OBD software I tried could communicate with my car. I first assumed the dongle was broken, or that the port was simply dead. It wasn't. The real problem was somewhere else: my car doesn't speak the same protocol as modern cars.
Modern cars (roughly 2008 and later, depending on who you ask) use CAN bus at 500 kbit/s. Generic OBD apps open exactly that, hear silence, and give up. Mine answers on ISO 14230-4, KWP2000 with fast init, over the K-line, an older and slower protocol. That is why the usual apps found nothing: they never asked the right question.
3. Finding the protocol
This is where GLM-5.3-Flash entered the picture, driving a real terminal against the
real dongle. We didn't know what the car was speaking, so we just asked it, one
protocol at a time. The ELM327 accepts a protocol-per-request command, so we sent
0100 (the OBD "are you alive?" ping) under every ISO variant until
something answered. The whole discovery fits in a dozen lines of Python:
def cmd(s, c, timeout=4.0):
"""Send a command, read until the ELM prompt '>' or timeout."""
s.reset_input_buffer()
s.write((c + "\r").encode())
end = time.time() + timeout
buf = b""
while time.time() < end:
data = s.read(512)
if data:
buf += data
if b">" in buf:
break
return buf.decode(errors="replace")
protocols = [
("ATSP6", "CAN 11/500"), ("ATSP7", "CAN 29/500"),
("ATSP8", "CAN 11/250"), ("ATSP9", "CAN 29/250"),
("ATSP3", "ISO 9141-2"), ("ATSP4", "KWP 5-baud"),
("ATSP5", "KWP fast"),
]
for atsp, name in protocols:
cmd(s, atsp)
reply = cmd(s, "0100", timeout=12) # "are you alive?"
if "41 00" in reply: # positive ECU response!
print(f"{name}: ALIVE -> {reply}")
CAN 11/500 → NO DATA CAN 29/500 → NO DATA CAN 11/250 → CAN ERROR CAN 29/250 → CAN ERROR ISO 9141-2 → BUS INIT ERROR KWP 5-baud → BUS INIT ERROR KWP fast → BUS INIT: OK 86 F1 10 41 00 98 3B 40 11 EC
One survivor. One ECU answering at address 0x10, with valid framing and
checksums. A quick honesty note here: "reverse-engineering" is a generous word for
this part — the ELM327 command set and OBD-II services are public standards, so
finding the protocol by probing is just asking questions in order. What really felt
like reverse engineering is what comes next.
4. What the ECU can and cannot do
Once the connection worked, we probed every OBD mode and PID one by one to
map the ECU firmware. My car's engine computer turned out to be EOBD-minimal: it
accepts the legally required basics (read live data, read/clear codes, open a session)
and rejects everything else with a crisp 7F … 12 ("service not
supported"):
- accepted: mode
01(six PIDs, plus — surprisingly — rail pressure),03/07(stored/pending codes),10 81(diagnostic session); - rejected:
09(VIN!),02(freeze frame), KWP identifier services21/1A/22.
This map answers the two questions a car owner actually has. The VIN is unreadable over OBD on this car (PSA keeps it in the BSI, dealer tools only), just read the windshield plate. And the freeze frame doesn't exist here, no matter what the documentation promises. We measured all of this: nothing in this article is an assumption, everything comes from the car itself.
5. Writing the tools
Generic software was useless, so we wrote our own. Python probes for the exploratory
work, and then a pure C tool for daily use — raw termios, select() with
wall-clock deadlines, hex-token parsing of KWP frames, checksums and all. About 175
lines, no dependencies. Every command follows the same shape: flush, write, read
until the > prompt or the deadline kills it:
static void send_cmd(const char *cmd, char *buf, size_t bufsz, int timeout_ms)
{
char tmp[256];
tcflush(fd, TCIFLUSH);
snprintf(tmp, sizeof tmp, "%s\r", cmd);
write(fd, tmp, strlen(tmp));
read_reply(buf, bufsz, timeout_ms); /* select() + wall clock, no infinite hangs */
}
send_cmd("ATZ", buf, sizeof buf, 4000); /* -> ELM327 v1.5 */
send_cmd("ATSP5", buf, sizeof buf, 2000); /* KWP2000 fast init */
send_cmd("010C", buf, sizeof buf, 10000); /* -> 84 F1 10 41 0C 0A 5C D2 */
/* locate "41 0C" in the reply — RPM = ((A<<8)|B) / 4 */
for (int i = 0; i + 3 < n; i++) {
if (toks[i] == 0x41 && toks[i + 1] == 0x0C) {
*ok = 1;
return ((toks[i + 2] << 8) | toks[i + 3]) / 4.0;
}
}
$ ./obd_rpm /dev/cu.usbserial-A7BL6VXS 5
RPM: 758.0
RPM: 764.0
RPM: 762.0
RPM: 762.0
RPM: 763.0
Sitting in the driver seat, watching live engine data scroll in my own terminal, built by an AI I was arguing with five minutes earlier. That was the moment it became fun.
6. Reading the fault codes
The codes behind the dashboard message: P1166 and P1351.
Reading them is one more mode — 03 for stored codes, 07 for
pending — plus a few lines of bit-shuffling to turn raw bytes into human codes:
def decode_dtc(b1, b2):
"""Raw bytes -> human code: 0x11 0x66 -> P1166."""
letter = "PCBU"[(b1 >> 6) & 3]
return f"{letter}{(b1 >> 4) & 3}{b1 & 0xF}{b2 >> 4}{b2 & 0xF}"
cmd(s, "03")
# '86 F1 10 43 11 66 13 51 00 00 A6'
# └─ data: 43 = mode 03 echo, then three 2-byte slots:
# 11 66 -> P1166 rail pressure above maximum
# 13 51 -> P1351 glow relay coherence fault
# 00 00 -> empty slot
Generic code dictionaries were useless here, they listed Honda oxygen-sensor meanings. The real definitions are PSA-specific, and we recovered them with a deep-research pass:
P1166 — « Régulation haute pression carburant : pression rail mesurée supérieure à la pression maximale ». Rail pressure above maximum, held five seconds. The ECU's downgrade: kill the engine.
P1351 — « Circuit relais pré/post-chauffage : relais commandé et bougies non alimentées ». The glow-plug relay coherence fault — and the pending copy of this code kept coming back, meaning the fault was still live.
The research also surfaced a trap that probably saved me real money: there are documented cases of owners replacing injectors, regulators, sensors and even ECUs for this exact code, without fixing anything. The best story is a C3 Picasso owner whose "implausible" sensor reading had everyone reaching for the parts catalog, when the wiring was guilty. On this engine, the low-pressure fuel side (a €20 filter, a pin-holed primer bulb, a tired O-ring) explains most of these faults, because the pump has to suck diesel from the tank, and any micro-leak admits air instead of showing a drip.
7. Measuring the rail pressure
A stored code tells you what the ECU saw in the past, not what the car is doing now.
However, it turned out the ECU exposes one precious live value: fuel rail pressure
(PID 0x23). So we wrote a logger — same C discipline, timestamped CSV —
and I sat in the car for two minutes with the engine idling:
t= 0.0s rail= 27500 kPa rpm= 782.75 coolant=34 map=102 t= 30.2s rail= 25760 kPa rpm= 780.75 coolant=35 map=102 t= 60.7s rail= 26850 kPa rpm= 779.75 coolant=37 map=102 t= 90.9s rail= 26410 kPa rpm= 777.75 coolant=39 map=102 t= 119.2s rail= 25760 kPa rpm= 773.75 coolant=40 map=102
Mean rail pressure: 264 bar, wobbling ±6 bar around the idle setpoint, engine speed steady at 774–785 rpm for two minutes. In other words the high-pressure fuel system regulates perfectly at idle. So the fault lives in the regimes we didn't sample yet, most likely cold cranking, where air in the suction line shows up first, and where the glow plug fault (P1351) does the worst damage.
8. What this says about AI and the physical world
Step back from the repair for a second, because something bigger happened here. A general-purpose language model, with no fine-tuning and no Citroën wiring diagram in its training set, took a fifteen-euro dongle and did the job of a diagnostic bench: it interrogated a 2008 ECU, mapped what that ECU refuses to do, decoded two manufacturer-specific faults, and reduced a dashboard scare to one €20 part.
The bridge is the whole story. Give a model a terminal and it stays a very eloquent opinion. But give it a serial port that answers back, and every claim can be checked: send the command, read the answer, update the theory. Hallucination stops being the failure mode because the wire settles every argument. The job becomes designing the next measurement instead of knowing things: hypothesis, probe, evidence, repeat. That loop is what made science work. The physical world is the best fact-checker ever built: it doesn't care how confident the text sounded.
And why stop at cars ? The same loop works on a boiler, a 3D printer, a solar installation, a lab instrument — anywhere a sensor or a bus turns the physical world into bytes. A general model, a cheap adapter and a terminal become a diagnostic bench that costs almost nothing. It will not replace the people who turn the wrenches, it will hand them the measurement that used to require the dealer machine. And for everyone else it moves the line between "pay the garage" and "replace a fuel filter yourself".
I don't think the next jump in AI capability will come from a bigger context window. It will come from models that stop only reading about the world and start measuring it, one protocol and one serial line at a time. In my case it started with 500 meters of coasting and a quiet K-line nobody else would talk to.
If you try this yourself: get an ELM327 that speaks KWP2000, use
/dev/cu.* on macOS, and don't trust any software that assumes your car
was built this decade. And if you find an open-source OBD tool that handles
K-line relics properly — I owe you a coffee.