I started this project in 2017 while looking into a digital replacement for the analog instrument cluster in my BMW Z3. I wanted to read fuel level, engine speed, odometer, road speed, and coolant temperature from the car's network and display them on a computer.

I built a working prototype with an Arduino, a CAN shield, and a Qt desktop application. I ran it connected to the car and displayed live values. I did not build a finished replacement dashboard, and some of the signal interpretations still needed better checks.

The original notes date from January 2 and June 6, 2017. Together with the saved captures, they show how I went from recording messages to testing possible meanings for the data.

Finding a way to read the car's data

CAN is a communication network used by the car's electronic modules. I reviewed the electrical documentation and investigated a CAN connection associated with the engine and instrument cluster, rather than assuming that the familiar diagnostic connection was the place to start.

I used a Linksprite CAN shield with an Arduino and began with the MCP_CAN library's receive example. The first job was to capture messages. After that, I could compare the data with changes in the car, such as engine speed and fuel state.

Community research gave me possible interpretations to try. I compared them with the original cluster, known operating conditions, and repeated observations on this vehicle. A value changing in the expected direction was useful, but it did not establish that the conversion was correct.

Displaying live values

I wrote a Qt application that opened the Arduino's serial connection, read the captured data, applied the candidate conversions, and displayed the results in real time.

I ran the laptop, Arduino, and CAN shield together in the car while connected to its live CAN network. That demonstrated the complete path from the car to the desktop display. It did not establish the accuracy of every value on the screen.

BMW instrument cluster with fuel, speed, tachometer, and odometer displays during live CAN-bus decoder testing.
Instrument cluster photographed during live CAN-bus decoder testing in the vehicle.

The prototype in use

These images show the early interface, the harness connections I investigated, and the decoder running in the car and in Qt Creator.

MS43 CAN Decoder Qt desktop window showing fields for fuel level, RPM, odometer, speed, and temperature.
Early Qt interface for the CAN decoder prototype, used to display captured fuel, RPM, odometer, speed, and temperature values.

Annotated 2017 bench photograph of the cluster harness used to identify the CAN connections for the capture setup. Historical test documentation, not current wiring guidance.
In-vehicle validation: the original cluster alongside the live decoder display during a real CAN-bus test.
The MS43 CAN Decoder running in Qt Creator, with application output visible during early prototype work.

Checking the candidate signals

The saved data includes idle and 2,000-RPM captures, two fuel-state captures, and a capture from an actual drive. Those records let me check more than whether a number looked plausible on the screen.

The CAN ID identifies the message. In the formulas below, B0 through B7 refer to its data bytes, numbered from zero. These interpretations come from this BMW Z3; they are not a general reference for every BMW.

Fuel level: ID 0x613

The candidate fuel value is B2. It stayed at 47 in the controlled full-tank capture and 49 in the topped-off/full-drive capture.

That gave me a useful indication of fuel state. It did not tell me how to convert every possible value into a calibrated fuel level.

Google Sheets worksheet from the 2017 full-tank idle capture, showing CAN ID 0x613 and B2 values holding at 47.
Fuel level — original 2017 full-tank worksheet: ID 0x613 is shown with B2 holding at 47 during the idle capture.

Engine speed: ID 0x316

The candidate conversion is ((B3 × 256) + B2) ÷ 6.42.

This was the strongest result. The controlled 2,000-RPM capture produced 151 readings, ranging from 1989.72 to 2040.50 RPM, with a median of 2015.58 RPM. The idle capture had a median of 713.71 RPM.

Those captures tied the calculation to deliberately held engine conditions. They support using it as a vehicle-specific candidate, with further checks needed before treating it as a complete specification.

Google Sheets worksheet from the controlled 2,000 RPM capture, showing the B3 and B2 conversion near 2,000 RPM.
Engine speed — original controlled 2,000-RPM worksheet: the B3/B2 conversion clusters around 2,000 RPM, while outliers show why range checks and continuity filtering matter.

Odometer: ID 0x613

The candidate conversion is ((B1 × 256) + B0) × 10 km, then ÷ 1.609344 for miles.

The results from the saved files were within a few miles of the mileage recorded in the notes. The original notes also corrected an early byte-order typo. The agreement was useful, but it did not establish a universal odometer definition.

Google Sheets worksheet from the 2017 capture showing the candidate BMW odometer byte calculation.
Odometer — original worksheet showing the byte-concatenation calculation. The corrected B1/B0 interpretation produces a value close to the recorded mileage context.

Road speed: ID 0x153

The candidate conversion is ((B2 × 256) + B1) ÷ 128.

The drive capture reached 90.38 in the calculated unit. I did not preserve an independent speed reference, and my original interpretation did not agree cleanly with my recollection of the car's speed. I later suspected the result was kilometres per hour rather than miles per hour, but that remains unconfirmed.

Road speed — original drive plot from the candidate speed formula. It rises and falls with the drive, peaking near 90 in the calculated unit; km/h remains plausible but unconfirmed because no independent speed reference was recorded.

Coolant temperature: ID 0x329

The candidate conversion is (0.75 × B1) − 48.373 °C.

The plotted value rose from roughly 36°C to 65°C during the capture. That looked plausible as the engine warmed up, but no independent temperature trace survives. The formula remains a hypothesis, not a confirmed calibration.

Original 2017 Google Sheets plot of the candidate BMW coolant-temperature signal rising from roughly 36 to 65 degrees Celsius during the capture.
Coolant temperature — original 2017 warm-up plot from the candidate coolant formula. It rises from roughly 36°C to 65°C over the capture: a plausible trend, not an independently calibrated measurement.

Design review: what I would change now

The capture hardware and desktop application worked together in the car. The weaker part was how I checked the signals. Some early matches made it into the original notes before I had clear test conditions and a repeatable record of the results.

If I returned to the project, I would:

  1. Define a test for each signal, including its expected range, vehicle conditions, independent reference, acceptable error, and repeat count.
  2. Save timestamped raw messages alongside the reference measurements. Road speed needs an independent speed reference, and coolant needs a logged temperature reference.
  3. Keep the vehicle's signal definitions in a separate, tested decoder rather than putting the formulas directly in the interface.
  4. Handle incomplete serial reads, malformed messages, stale data, and implausible readings explicitly.
  5. Keep exploratory formulas separate from confirmed definitions in the code and documentation.

What I learned

I could capture the car's messages, turn candidate values into readable units, and display them live. The controlled RPM tests were the strongest check of that work. Fuel and odometer gave useful matches, while speed and coolant still needed better references.

The main lesson was to keep those results separate. A changing number on a working display can help find a signal, but checking its meaning and accuracy takes a more deliberate test.

Project facts

  • Role: Independent project; capture hardware, data investigation, and Qt application.
  • Period: 2017.
  • Technology: Arduino, Linksprite CAN shield, MCP_CAN, serial communication, C++, Qt, and vehicle electrical documentation.
  • Status: Historical prototype tested in the vehicle. Road-speed units and coolant calibration remain unresolved; the other conversions are vehicle-specific candidates with the limits described above.
  • Source: MS43_CAN_DECODER on GitHub.

Capture-derived plot of BMW MS43 CAN candidate signals showing controlled RPM results and recorded fuel-state observations.
Static analysis derived from the surviving 2017 capture workbooks. Controlled RPM and recorded fuel-state captures support useful candidates; road-speed units and coolant calibration remain unverified.