Lab · A private owner of a BMW X1 F48

A software diagnostic suite for automotive telemetry: closed protocol, UDS, an AI mechanic assistant

A custom client for a proprietary protocol instead of dealer equipment: a direct, strictly read-only dialogue with the car's control units

Client
A private owner of a BMW X1 F48
Timeline
2025—2026
Role
Full cycle
Status
In production
96,433 km
Real KOMBI odometer read
odometer 22D10D read directly; one response returns two odometers, and cross-checking them is the rollback detector
96/100
Composite buy verdict
98/100 accounting for this car's expected tuning codes
6
ECU modules answering UDS
ZGW, DDE, EGS, DSC, FEM, KOMBI — confirmed on the live car over our own HSFZ client
23
Live sensors in the polling set
of 24 PIDs declared by the ECU support mask, parsed by our own byte decoder
4
Report export formats
JSON, CSV, HTML, PDF, plus verdict delivery to Telegram via the Bot API

Context

Automotive diagnostics is one of the most closed areas of consumer engineering. Manufacturers split data into two circuits: the public OBD-II minimum and proprietary services reachable only by dealer equipment. Community knowledge is fragmentary and often wrong — the textbook error assumes modern BMWs speak DoIP, false for the F-series.

The second layer is the used-car market: odometer rollback and hidden stored faults remain mass practice, so a buyer needs primary data from the control units rather than the dashboard reading — which is exactly what gets faked. Any interference with the inspected car's ECUs is unacceptable, so the tool must be provably read-only.

The task

The owner of a diesel BMW X1 F48 with a Stage 1 tune needed the car's real condition without dealer equipment: live sensors, an odometer cross-checked against a duplicate counter, faults across all modules, and a buy verdict aware of configuration — a tuned car legitimately returns codes that would be defects on stock. The hard constraint: strictly read-only by architecture — no ECU write operations anywhere in the code.

Approach

The first iteration ran on an ELM327 WiFi adapter and python-OBD. The adapter proved an OBDII v1.5 clone, surfacing two systemic problems: a thread race mixing responses between parallel polls, closed with a threading.RLock wrapper, and infinite recursion in python-OBD 0.7.3 on the clone's non-standard responses — live polling was rewritten onto raw mode-01 commands with our own byte parser.

Then came the ceiling of the standard itself: VIN, the KOMBI odometer, and diesel specifics live in proprietary services out of an ELM327's reach. A wired ENET cable delivered the key insight: the F-series speaks not DoIP but BMW's earlier, publicly undocumented HSFZ protocol. We wrote a clean HSFZ client behind a replaceable transport layer; the DoIP client stays as groundwork for the G-series.

Architecture

The bottom layer is the HSFZ client in a transport module: gateway auto-discovery via UDP broadcast on port 6811, diagnostic TCP on port 6801, VIN reading, and UDS requests to the units. An F-series feature we established ourselves: HSFZ needs no routing activation — mandatory in DoIP, documented nowhere. The middle layer, the OBDWorker poller, migrated fully off ELM327/python-OBD with the decoder logic unchanged — the payoff of layering.

The application layer is a PySide6 desktop app: live sensors, mileage with rollback detection, per-module diagnostics; on top, the verdict engine with EXPECTED_DTC, so codes expected for the configuration do not lower the score, and an AI-mechanic assessment from an Anthropic model with those codes flagged. Exports go to JSON, CSV, HTML, and PDF, the verdict to Telegram — all on urllib with no heavy SDKs. Resilience: auto-reconnect on TCP drop, link-local broadcast binding, an ARP fallback; a mock gateway answering the car's real bytes powers end-to-end and degradation tests.

Result

Confirmed on a live car: discovery finds the gateway and all six modules answer UDS — ZGW, DDE, EGS, DSC, FEM, KOMBI; 24 PIDs declared by support mask, 23 sensors in the live poll. Odometer 22D10D, read directly from KOMBI, returned 96,433 km; two counters arrive in one response — their cross-check is the rollback detector. The car showed two stored faults — 279100 (swirl flaps removed under Stage 1, expected) and 8040BB (weak battery) — and the verdict came out 96/100, or 98/100 adjusted for expected tuning codes.

The ready/pending line is drawn honestly: everything verified live — HSFZ link, mileage, faults, live PIDs, verdict — runs in production. Proprietary DIDs for boost, oil temperature, engine hours, the B47 diesel-health block, and CBM still return NRC 0x31, scheduled for calibration with the engine running — a roadmap step, not a gap.

What we built

  • Own HSFZ client

    A clean client for BMW's proprietary HSFZ protocol: gateway auto-discovery on UDP port 6811, diagnostic TCP on port 6801 without routing activation, VIN reading and UDS requests to the units. The DoIP client is kept in the codebase as groundwork for the G-series.

  • Rewritten transport layer

    The OBDWorker process was fully migrated from ELM327/python-OBD to HSFZ, and the python-OBD dependency was removed. The transport changed, the upper decoder logic stayed intact — a direct consequence of the layered architecture.

  • Live sensor polling

    Raw mode-01 commands with our own byte parser: coolant, intake, system voltage, atmosphere, outside temperature. 24 supported PIDs by mask, 23 sensors in the live set.

  • Real mileage and rollback detector

    Odometer 22D10D is read directly from the KOMBI instrument cluster (96,433 km). One response returns two counters — cross-checking them is built in as an odometer-rollback detector.

  • Verdict engine and diesel health

    A composite buy score (96/100, 98/100 accounting for expected codes), the B47 block: DPF regenerations, ash, oil dilution, injector corrections, VNT, EGR. A demo mode with an emulator works without a car.

  • EXPECTED_DTC for tuned configurations

    This car's expected codes (279100 — intake swirl flaps removed under Stage 1) don't lower the verdict and aren't counted as a defect, either in the report or in the AI assessment.

  • AI mechanic and exports

    The collected condition snapshot goes to an Anthropic model and comes back as a human assessment. Reports in JSON, CSV, HTML, and PDF, the verdict to Telegram via the Bot API, scans into a cloud table; integrations on urllib with no heavy SDKs.

  • Resilience circuit and offline tests

    Auto-reconnect on TCP drop, binding broadcast to link-local interfaces, an ARP fallback, correct thread teardown. A mock gateway answers with the car's real bytes: a full end-to-end run and a degradation test pass without a car.

Engineering challenges

The F-series speaks HSFZ, not DoIP

A common misconception is that the F48 is diagnosed over DoIP. In fact it's the proprietary BMW HSFZ; DoIP starts with the G/I-series. We reverse-engineered HSFZ and wrote our own client with gateway discovery and UDS requests; the F-series also doesn't require routing activation, which simplifies the connection — but this is documented nowhere.

python-OBD looped on the ELM clone

python-OBD 0.7.3 went into infinite recursion on the ELM327 v1.5 clone, unable to digest non-standard responses. Live polling was rewritten onto raw mode-01 commands with our own byte parser, and later python-OBD was removed entirely by moving to HSFZ.

A thread race on a single adapter

Parallel polls of one ELM327 mixed responses between requests. We introduced a threading.RLock and a thread-safe wrapper guaranteeing that exactly one thread accesses the transport at any moment.

Tuning codes broke the verdict

Intake swirl flaps removed under Stage 1 honestly throw fault 279100, but that's not a defect. The EXPECTED_DTC mechanism flags this car's expected codes so they don't lower the composite score or mislead the AI assessment.