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.

Design rule: parse to a normalized frame table first, then apply signal definitions. Never let a DBC-specific detail leak into your analysis layer.

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:

OffsetSizeFieldType
0x004Magic 0x41494247 ("AIBG")uint32
0x042Format versionuint16
0x062Record lengthuint16
0x088Start timestamp (µs)uint64
0x10NRecordsstruct

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.

Airbag ECU Log Parsing Pipeline DBC + CAN log schema + raw frames MDF4 (.mf4) channel groups + time Proprietary binary header + fixed records cantools db.decode(frame) asammdf mdf.get(channel) struct.unpack validate magic/version Normalized Frame Table timestamp_us signal · value · unit pandas DataFrame Validation gates: magic/version · record length · monotonic time · file boundary Fail fast — never let a corrupt record reach the analysis layer.

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.

Common trap: assuming all timestamps share the same epoch. CAN logs often use host time, MDF4 uses the measurement start time, and proprietary logs use ECU uptime. Convert everything to a common reference (e.g., time since first crash event) before comparing.

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.