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.

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.




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.

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.

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.

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.

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.

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:
- Define a test for each signal, including its expected range, vehicle conditions, independent reference, acceptable error, and repeat count.
- Save timestamped raw messages alongside the reference measurements. Road speed needs an independent speed reference, and coolant needs a logged temperature reference.
- Keep the vehicle's signal definitions in a separate, tested decoder rather than putting the formulas directly in the interface.
- Handle incomplete serial reads, malformed messages, stale data, and implausible readings explicitly.
- 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.

Discussion