Case study · 2026

CAN bus and FMS data decoding

I built a Teensy-based receiver that decodes vehicle data from two CAN buses and shows the results in a live browser dashboard. The work covers message parsing, signal scaling, multi-frame payloads, and making missing or stale data visible.

CAN bus J1939 / FMS Teensy 4.1 C++ Web Serial
2026

Context

This project started with a practical question: what information is available on a vehicle's FMS and bodybuilder interfaces, and how can I inspect both together? The current implementation reads two buses through a Teensy 4.1 and presents the decoded data over USB serial.

Problem

A raw CAN frame needs more interpretation than a byte-to-number conversion. Message identifiers carry routing information, signals have their own scales and unavailable-value codes, and some payloads span several frames. The same signal can also appear on both buses, so combining everything into one value would hide useful differences.

Approach

I separated the bus readers, J1939 message handling, and signal decoders. Each bus keeps its own state and multi-frame assembly, while shared message types reuse decoder code. The firmware emits a combined JSON snapshot with separate fields for each source. The browser uses Web Serial to read the device directly and display the values alongside PGN activity.

Making the data inspectable

The dashboard keeps the two bus views separate, marks values as stale when updates stop, and shows observed message groups even when there is no decoder for them. Stored browser state can restore the previous view after a reload, with restored values marked stale. That makes it easier to distinguish a signal that is absent, a signal that has stopped updating, and a real zero.

Multi-frame messages

The receive path assembles broadcast transport-protocol messages by source address and packet sequence. It tracks received packets so a duplicate does not count as new data. This supports fields that do not fit into one CAN frame, while leaving signal-specific interpretation in the decoder layer.

Outcome

The current result is a receiver and an inspection dashboard, with decoded vehicle, engine, fuel, brake, and other signal groups. Fleet deployment, cellular transport, and a long-term analytics backend are separate follow-on work; this version gives me a concrete way to examine the data before building those services.

What I learned

The most useful part of the interface is showing what is unknown or out of date. A dashboard can look convincing while quietly displaying old values. Keeping the source bus and update state visible makes it much easier to investigate what the decoder is actually receiving.