3. Parsing airbag ECU log files
Airbag ECU logs are the primary evidence source for deployment-result analysis. Before any statistical or automated reasoning can happen, the raw files must be turned into a clean, queryable structure: timestamped signals with physical units, event markers, and fault codes. This chapter covers the three format families you will meet in practice — DBC-described CAN logs, ASAM MDF4 measurement files, and vendor-proprietary binary logs — and shows how to parse each with Python into a common in-memory representation.
Why format matters before analysis
A DBC file is not a log; it is a schema. It describes CAN identifiers, message lengths, signal bit positions, byte order, scaling, offset, and units. The actual log is a stream of raw frames. MDF4 is the opposite: it is a self-describing container that stores channel groups, timestamps, and metadata, but it does not know what a "crash severity" signal means unless the channel name says so. Proprietary formats sit between the two — they usually carry their own header, a fixed or versioned record layout, and sometimes an embedded checksum.
The practical consequence is that your parser must separate transport decoding (bytes to raw values) from semantic mapping (raw values to engineering meaning). If you mix them, every new ECU variant forces a rewrite.
Parsing DBC-described CAN logs
For DBC, the standard tool is cantools. It reads the DBC, validates it, and decodes frames. A typical workflow reads a candump-format text log line by line, extracts the arbitration ID and payload, and decodes against the database.
import cantools, re
db = cantools.database.load_file("airbag_ecu.dbc")
LINE = re.compile(r"\((\d+\.\d+)\)\s+(\w+)\s+([0-9A-Fa-f]+)#([0-9A-Fa-f]+)")
def parse_candump(path):
for line in open(path):
m = LINE.match(line.strip())
if not m:
continue
ts, bus, arb_id, payload = m.groups()
data = bytes.fromhex(payload)
try:
msg = db.get_message_by_frame_id(int(arb_id, 16))
except KeyError:
continue
yield float(ts), msg.name, msg.decode(data, decode_choices=False)
Two details matter for airbag work. First, decode_choices=False keeps raw numeric values instead of symbolic names, which is what you want when computing deployment timing. Second, DBC scaling is applied automatically: a signal defined with factor=0.1, offset=-40 returns engineering units directly. Always record the DBC file hash alongside the parsed output — a changed DBC silently changes every downstream number.
Parsing MDF4 measurement files
MDF4 files from ETAS INCA, Vector CANape, or dSPACE tools are read with asammdf. The API is channel-oriented rather than message-oriented.
from asammdf import MDF
mdf = MDF("crash_run_0421.mf4")
print(mdf.channels_db.keys()) # discover available channels
t = mdf.get("time_crash").samples
acc = mdf.get("accel_longitudinal").samples
fire = mdf.get("airbag_fire_flag").samples
# align to a common time base
import numpy as np
t0 = t[0]
mask = (t - t0) <= 0.5 # first 500 ms window
window = {"t": t[mask], "acc": acc[mask], "fire": fire[mask]}
MDF4 channels may have different sample rates. Do not assume they share a time vector. Resample explicitly with mdf.resample() or interpolate onto a chosen grid before correlating acceleration with the fire flag. Also check mdf.header for the start time and channel.unit for units — a channel named accel_longitudinal could be in g or m/s², and the difference is a factor of 9.81.
Proprietary binary formats
Vendor logs often use a fixed header followed by fixed-length records. A representative layout for an airbag ECU might be:
| Offset | Size | Field | Type |
|---|---|---|---|
| 0x00 | 4 | Magic 0x41494247 ("AIBG") | uint32 |
| 0x04 | 2 | Format version | uint16 |
| 0x06 | 2 | Record length | uint16 |
| 0x08 | 8 | Start timestamp (µs) | uint64 |
| 0x10 | N | Records | struct |
Each record might be 16 bytes: a 4-byte relative timestamp, a 2-byte event code, a 2-byte status word, and two 4-byte signed accelerometer samples. Parse with struct and validate the magic and version before trusting anything.
import struct
HEADER = struct.Struct("<IHHQ") # magic, version, rec_len, start_us
RECORD = struct.Struct("<IHHii") # dt_us, event, status, ax, ay
def parse_proprietary(path):
with open(path, "rb") as f:
magic, version, rec_len, start_us = HEADER.unpack(f.read(HEADER.size))
if magic != 0x41494247:
raise ValueError("not an AIBG log")
if version != 2:
raise ValueError(f"unsupported version {version}")
while chunk := f.read(rec_len):
if len(chunk) != rec_len:
break
dt, event, status, ax, ay = RECORD.unpack(chunk)
yield start_us + dt, event, status, ax, ay
Proprietary parsers are where silent corruption happens. Always verify: record length matches the struct size, timestamps are monotonically increasing, and the file ends exactly on a record boundary. A truncated final record is common after an ECU power loss and should be flagged, not silently dropped.
Normalizing to a common frame table
All three parsers should emit the same shape: a list of records with timestamp_us, source, signal, value, and unit. Load into a pandas DataFrame for analysis.
import pandas as pd
rows = []
for ts, name, sigs in parse_candump("run0421.log"):
for sig, val in sigs.items():
rows.append((int(ts*1e6), "dbc", sig, val, None))
df = pd.DataFrame(rows, columns=["timestamp_us","source","signal","value","unit"])
df = df.sort_values("timestamp_us").reset_index(drop=True)
Once normalized, deployment timing, fault-code sequences, and cross-signal correlation become ordinary DataFrame operations. The format-specific complexity is confined to the parsers.
Validation and failure symptoms
Each format has characteristic failure modes. For DBC, a mismatched DBC version produces plausible but wrong values — the symptom is a deployment time that shifts by a constant offset. For MDF4, mixing channels with different sample rates without resampling produces misaligned peaks. For proprietary logs, a truncated final record or a version bump silently changes field offsets.
Practical checklist
- Record the DBC file hash and MDF4 header metadata with every parsed dataset.
- Validate magic bytes, version, and record length before decoding proprietary logs.
- Resample MDF4 channels onto a common time grid before correlation.
- Keep raw values (
decode_choices=False) for timing math; map to symbolic names only for reporting. - Flag truncated records and non-monotonic timestamps as data-quality issues, not parse errors.
With a normalized frame table in place, the next stage — detecting deployment events, measuring timing, and classifying outcomes — operates on a single consistent structure regardless of which format the ECU produced.