alertLog decode (DU + PCS) + DIR 2022 CAN support #13

Closed
outlandnish wants to merge 0 commits from feat/odin-support into main
Owner

Integrates the alertLog decode work + DIR 2022 CAN support that accumulated on
feat/odin-support since PR #11 (16 commits, 33 files).

Highlights

  • Tesla alert + CAN catalog extraction from the MCU .so files (so_alerts,
    so_candata), feeding a new alerts/faults layer.
  • alertLog decode framework (alert_log.py): decodes <NODE>_alertLog CAN
    frames from firmware-recovered bit layouts. Drive-unit (DI/DIR) coverage +
    PCS 0x424 decode, 63/84 alerts (PR #12). Rationality decoder matches the
    offending-id / errorType / badValues by field name (order varies across ECUs).
  • can_live: Alerts tab + dash faults / alert-log panels sharing one renderer.
  • DIR 2022.45.15 CAN fixes: clear canDataBusA + brake/vcfront MIAs; CP_status
    is 0x210 (not 0x25D) across revs.
  • Firmware-derived layouts live in the private tree (docs/private/alerts/,
    rev-tagged); extractor tooling stays local/gitignored.

Tests

Sim golden inventory refreshed; ODJ shape tests made dump-agnostic; alert_log
tests (24) pass.

Private-only: carries docs/private/ RE material — this branch does not converge
with the sanitized public origin.

Integrates the alertLog decode work + DIR 2022 CAN support that accumulated on `feat/odin-support` since PR #11 (16 commits, 33 files). ## Highlights - **Tesla alert + CAN catalog extraction** from the MCU `.so` files (`so_alerts`, `so_candata`), feeding a new alerts/faults layer. - **alertLog decode framework** (`alert_log.py`): decodes `<NODE>_alertLog` CAN frames from firmware-recovered bit layouts. Drive-unit (DI/DIR) coverage + **PCS 0x424 decode, 63/84 alerts** (PR #12). Rationality decoder matches the offending-id / errorType / badValues by field name (order varies across ECUs). - **can_live**: Alerts tab + dash faults / alert-log panels sharing one renderer. - **DIR 2022.45.15 CAN fixes**: clear canDataBusA + brake/vcfront MIAs; CP_status is 0x210 (not 0x25D) across revs. - Firmware-derived layouts live in the private tree (`docs/private/alerts/`, rev-tagged); extractor tooling stays local/gitignored. ## Tests Sim golden inventory refreshed; ODJ shape tests made dump-agnostic; alert_log tests (24) pass. Private-only: carries `docs/private/` RE material — this branch does not converge with the sanitized public `origin`.
Bench-verified against a gen-26 (12603) 2022.45.15 DIR/PMR. Three
independent root causes cleared the full DIR alert matrix (a094
canDataBusA, a110 brakeMIA, a155 vcfrontMIA):

- ui: UI_vehicleModes 0x284 must be DLC8, not DLC5. The DIR length-gate
  (FUN_000b3d0c, run for every bus-A RX id) raises canDataBusA on any DLC
  mismatch; 5<8 fired a094 on every frame. -> clears a094.

- tesla_frames: VCFRONT_vehicleStatus 0x3A1 checksum magic was reseeded
  0xA4 -> 0x2A in 2022 (2020 used id_lo+id_hi). The additive-checksum
  family lives at cksum@byte7 / counter@byte6[4:7] (packed 2-bytes/word RX
  buffer; SLEIGH-emulation confirmed), already correct at (52,56).

- vcfront: 0x3A1 and 0x3C2 rates 10Hz -> 20Hz to match the 2022 CANData
  cycle_time (50ms). Under-sending aged out the DIR freshness supervisor
  -> vcfrontMIA. 0x321 gains its 2022 (52,56) checksum (plain in 2020).
  -> clears a155.

- ibst: add IBST_status 0x39D on bus A for 2022 via fw-variant (its bus-A
  cksumCtr validator feeds brakeMIA); 2020 sent 0x39D party-only.
  -> contributes to clearing a110.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/cp/cp.py shipped CP_status on 0x25D -- a bench-derived id that
matches no firmware. In every extracted CANData catalog (2020.8.1 ->
2026.8.3) CP_status is 0x210 and 0x25D is APP_trafficControl, so the
DI/DIR never received a valid CP_status liveness frame; that most likely
defaulted DI_driveBlocked to DRIVE_BLOCKED_PROX (no valid cable-not-
connected report). The bit layout was already correct (verbatim from
Model3_ETH.compact.json) -- only the arbitration id was wrong, and it is
wrong for all revs, so no fw-variant is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The infotainment stack ships the full per-build catalogs as exported relocated
data, richer than anything else in the dump:

- so_candata: libQtCarCANData.so holds the 446-message ETH signal catalog
  (names, units, value tables) vs 140 messages in a per-revision compact.json.
  No bit layout -- pair with candata_to_dbc to overlay a compact.json donor.
- so_alerts: libQtCarAlerts.so holds 6308 alerts with names, descriptions,
  cause / clear / effect, and the signals each one logs -- in the clear, where
  bus-alerts-map.json stores only salted hashes (dump_alerts).
- tesla_alertlog: decodes <NODE>_alertLog frames (38 ids). byte0 = alert code
  mux -> NODE_aNNN. CAN-rationality alerts resolve the offending message and
  error type on the reversed DU and PCS layouts.

One field decodes for ANY alert: a094 pins the payload's first log signal to
bit 16, so masking to its value table's maximum bit width reads it correctly
without knowing the field's true width -- every defined value fits. That field
is the alert's reason on the 507 alerts that lead with an enum:

    0x527 A2 80 04 00 00 01 22 7C
      -> DI_a162_shiftDenied, shiftDeniedReason = SYS_STATE_NOT_ENABLED

The remaining per-alert field widths exist in no shipped artifact (the .so
catalog has no bit layout; neither compact.json nor the year DBCs carry an
alertLog message), so they are reported by name as undecoded rather than
guessed at -- brute force over a real a162 frame left 61 self-consistent
packings, so a single frame cannot pin them.

can_decoder overlays the decode onto alertLog frames, lazily and best-effort,
so a viewer without a firmware root is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turns raw alert bits into readable faults, and surfaces what the ECUs
themselves logged about them. Both pages read the same /api/alerts payload and
share one renderer (can_live_ui/alerts.js, served at /alerts.js), so the viewer
and the HUD stay identical:

- Active faults: every alert-matrix bit currently set, as a card with a human
  title and Tesla's own description. Click for cause / clears-when / effect /
  audience plus the raw NODE_aNNN_name. Bench hardware faults stay dimmed and
  tagged hw. The Alerts tab button carries the active count, so a fault
  appearing is visible from any tab.
- Alert log: decoded <NODE>_alertLog broadcasts. Identical payloads fold into
  one entry with first/last-seen and a count -- an ECU re-broadcasts a logged
  alert continuously, so a raw stream would be unreadable. The idle all-zero
  frame every ECU sends is skipped. POST /api/alerts/clear drops the log and
  the latched fault bits.

Bench-verified against a gen-26 2022.45.15 DIR/PMR: DIR_a090_pmMIA -> TOOSLOW,
and PMR_a057_canDataBusB naming 0x256 / DI_chassisControl with SEQUENCE errors.

The catalog is optional: without a firmware root the panels still render, with
the name-derived title and no prose. It loads once at startup rather than on
the first poll, so the ~0.7 s .so parse never lands inside a request.

Note the Active faults panel needs alert-matrix messages in the loaded signal
DB -- a per-revision compact.json is partial (2022.4.15 carries only
DIR_alertMatrix3 and none of its bits), so populating it wants --dbc and a year
DBC. The Alert log panel does not depend on the signal DB at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2022.45.15 bench fixes (7db419e) and the CP_status id move (0b6fceb)
changed what goes on the wire without updating the fixtures that lock it, so
five tests were failing on a clean checkout.

GOLDEN is now keyed by (can_id, bus) rather than can_id alone. IBST's 2022
variant deliberately sends IBST_status 0x39D on BOTH buses -- party for a158
ibstMIA, and vehicle because the 2022 DIR validates it on bus A for a110
brakeMIA -- which a can_id-keyed golden cannot express at all; it showed up
only as a bare "frame count changed: 58 == 57".

Folded in, each matching a documented root cause from 7db419e:
- 0x3A1 / 0x3C2 10Hz -> 20Hz (2022 CANData cycle_time is 50ms; under-sending
  aged out the DIR freshness supervisor -> a155 vcfrontMIA)
- 0x321 gains its 2022 checksum + counter and its 100ms rate (plain in 2020)
- 0x284 DLC 5 -> 8 (the DIR length-gate raised a094 on every short frame)
- CP_status 0x25D -> 0x210
and IBST joins the fw-varied node set in test_fw_versioning.

The ODJ entry-shape tests hardcoded CP, but which node's ODJ carries DIDs
varies by dump -- 2022.45.15's CP.odj is routines-only (6 routines, 0 DIDs).
They now source DIDs from whichever ODJ in the active dump has them (APS here),
keeping real coverage everywhere instead of asserting a dump-specific shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The module decodes a CAN message layout, not anything vendor-specific to name
in the import line; alert_log reads better next to can_decoder and so_alerts.
tests/test_tesla_alertlog.py follows to tests/test_alert_log.py.

Docstrings no longer name firmware symbols. The wire format, the on-vehicle
captures that confirm it, and the reasoning behind the leading-field decode all
stay -- they are why the packing is what it is, and the code is unmaintainable
without them.

can_live binds the alertLog store as alertlog_store now: the app key is still
"alert_log", but the old local shadowed the freshly imported module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shipped artifacts name an alert's log signals but never place them (the .so
catalog has no bit layout; compact.json and the year DBCs carry no alertLog
message at all), so until now everything past the leading enum was reported as
undecoded. Statically extracting each alert's snapshot packer from the drive-unit
firmware supplies the missing bit positions.

alertlog_layouts.json holds them as node -> aNNN -> field -> [bit, width,
signed], over the log-payload area (frame bytes 2-7, little-endian, bit 0 ==
byte2 bit0). 33 alerts across DI and DIR. Field counts are verified against the
catalog's log_signals, packings are gated on non-overlap and <= 48 bits, and
a162 cross-checks against an independent hand reverse-engineering.

decode() is now three tiers: a reversed layout decodes every field, else the
CAN-rationality packing, else the leading-enum reason alone. Enum and unit
labelling still comes from the per-revision catalog at decode time, so one
layout serves every revision that keeps the fields, and a field the catalog
drops is skipped. A missing or unreadable layouts file degrades to the
reason-only tier rather than failing.

    0x527 A2 80 04 00 00 01 46 7C
      -> DI_a162_shiftDenied, reason SYS_STATE_NOT_ENABLED,
         currentGear N, requestedGear D (a refused N->D shift)

WIP: the extractor output is checked in as data, but the extraction itself still
needs a pass to make it scalable -- coverage is 33 of ~3000 alerts with log
signals, and the signedness heuristic (unsigned unless wide torque/speed/accel
telemetry) is not yet derived from the firmware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alertlog_layouts.json sat at the repo root, where neither .gitignore nor the
githooks/pre-push guard covered it -- so a future push of this branch to the
public origin would have carried it through, along with the _meta block naming
the firmware it was extracted from. It is firmware-derived data, so it belongs
with the alert-hash recipe and the rest of the private layer.

Now at docs/private/alerts/alertlog_layouts.json (gitignored, and matched by the
hook's PRIVATE_RE), resolved via TM3_ALERTLOG_LAYOUTS with that as the default
-- the same seam dump_alerts uses for TM3_ALERT_HASH_PROVIDER. A checkout
without the private layer loads no layouts and decodes the reason enum and the
CAN-rationality fields as before: the layouts are an enrichment, never a
dependency, and their absence is not an error.

Docs follow: the can_live alert table gains the full-layout tier it was missing
and says where the file lives, and .env.example documents the override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Another extractor pass: 55 -> 75 alerts (DI 29, DIR 34 -- 277 fields), adding
DIR a043/a056/a059/a062/a087/a090/a091/a092/a103/a133/a144/a155 and DI
a063/a104/a141/a180/a198/a204/a230/a245.

A field spec gains an optional 4th element marking a value that came from the
packing convention -- a zero-init word, or a missing aggregator bit -- rather
than from an observed store. Those 16 fields carry "inferred": true through
log_values() so a consumer can weight them lower instead of treating every
decoded field as equally well grounded. apply_layout unpacks the spec
positionally so a 3-element layout still loads unchanged.

Still WIP: the extractor needs its scalability pass, and the signedness
heuristic remains a heuristic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alertLog packing shifts between builds, so a single alertlog_layouts.json would
quietly decode a 2024 frame with 2022 bit positions and produce confident
nonsense. The file is now alertlog_layouts_<rev>.json, and this one is named for
the build it was extracted from: alertlog_layouts_2022.45.15.json.

Resolution order: TM3_ALERTLOG_LAYOUTS if set, else the file matching
config.FW_VERSION (the configured bench firmware), else the newest tagged file.
Nothing found still means no layouts and a reason-only decode -- unchanged, and
still not an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts the id half of 0b6fceb for the inverter. That commit moved CP_status
from 0x25D to 0x210 because the MCU CANData catalog says CP_status is 0x210
and 0x25D is APP_trafficControl. The catalog describes the MCU's bus map; the
inverter does not use it, and the move regressed the drive bench.

In the 2022.45.15 gen-26 DIR (dir_26_65_2_2022.45.15_swapped_0x00082000.bin)
the charge-port handler FUN_000acc1e gates on FUN_000b3d0c(8, 0x25d, dlc) --
id 0x25D, DLC 8 -- and 0x210 is absent from the DIR's whole 42-id RX table, so
the frame we were sending is never read. The handler consumes one 2-bit field:

    DAT_00014601 = word0 >> 14                  # frame bits 14-15, LE
    if (word0 & 0xC000) == 0: DAT_00014601 = 0  # 0 = SNA

feeding FUN_000b45b3: 1 -> not connected, 2 -> connected, anything else
INCLUDING the 0/SNA default -> connected. A missing 0x25D is therefore not
neutral: the DIR latches cable-connected, alertPacker_DI_a168_proximityDrive-
Denial fires, and DI_a162_shiftDenied reports chargeCableConnected=1 -- which
is what the bench showed (0x527 A2 80 04 00 00 01 46 7C). 0x25D is also the
DI_a105_cpMIA liveness member, so it must arrive at 100ms regardless.

Bit 14, not 16: 2019/2020 compact.json has CP_chargeCableState at bit 16 but
the field moved -- the 2026 DBC has 14|2@1+ and the 2022 firmware reads 14. A
live capture of a real controller driving a Model 3 DU confirms it on the wire:
0x25D [8] 24 48 44 C9 40 A1 02 90 -> byte1 0x48 & 0xC0 = 0x40 -> 1 =
NOT_CONNECTED. Only bits 14-15 are read, so the rest of the 0x25D payload stays
zero rather than guessing the 2022 positions of the other CP_status signals
(the 2020 CP_doorControlState @13 w3 would collide with bit 14 anyway). 0x210
keeps the 2020 layout and still ships for catalog-correct listeners.

  * cp.py: 0x25D added via fw_variants at 2022.45.15 (the 2020 DIR has no such
    subscription); cable state on both copies now tracks evse_connected instead
    of being hardcoded NOT_CONNECTED.
  * scenarios/drive.toml: pin [firmware] version = "2022.45.15". The comment
    calling the pin a no-op was stale -- eight nodes now carry 2022 variants.
  * tests: golden inventory + fw-delta sets gain 0x25D; new test asserts the
    DIR-visible encoding (DLC 8, byte1 & 0xC0 == 0x40) and that 0/SNA is never
    emitted.

Verified: DIR RX table dumped from all 42 FUN_000b3d0c call sites and diffed
against the sim inventory -- coverage 35/42, no DLC mismatches (that gate wants
an exact match and raises DIR_a094_canDataBusA otherwise). Still unsent:
0x353 0x500 (uiMIA a088), 0x1D8 0x31F 0x786 (unclassified), 0x25A 0x57D
(documented droppable). Full suite green (2661 passed).

This does NOT by itself clear the shift denial: SYS_STATE_NOT_ENABLED comes
from DAT_00013c15 in {0,1,2,8} in FUN_000a16bf, checked before any cable or
proximity logic, and that gate is an internal HV/HVIL precondition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RC 0x0201 (checkModuleProgrammedCorrectly) is a verify, not just a CRC check:
on legacy bootloaders it recomputes the image CRC, but on secure bootloaders
(e.g. 2026+ PMR) the same routine also enforces an ECC signature and SVN
rollback check, so a non-zero status can mean an unsigned/wrongly-signed/
rolled-back image rather than a CRC mismatch.

Rename step_verify_crc -> step_verify and _RC_VERIFY_CRC -> _RC_VERIFY, and
reword the progress/error output ("CRC check"/"MISMATCH" -> "Verify"/"Verify
FAILED (CRC mismatch, or signature/rollback rejection on secure bootloaders)").
Also renames the CLI named routine verify-crc -> verify and updates the docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewed-on: #10
Reverse-engineered the PCS (variant-411, F28377D dual-core) alertLog packing and
recovered per-alert log-field bit layouts, the PCS analog of the drive-unit work.

Each PCS alert builds a 4-word record = the 0x424 frame (word0 code/state, words
1-3 = the 48-bit payload over bytes 2-7, same little-endian view the framework
already decodes) then calls the setter with the alert id as an explicit immediate.
A dataflow interpreter over the mask/shift/OR + FPU-clamp packing idiom (local
tooling in docs/private/alertlog-extractor) recovers each field; field counts are
checked against libQtCarAlerts log_signals, non-overlap/<=48b gated, and anchored
on a030 = {canRxErrorType:[0,3], canID:[8,16]} (matches Damien Maguire's handle424)
plus a029. 60/84 alerts emitted into the shared, rev-tagged layouts file under a
"PCS" node (cross-rev names borrowed from 2024/2026 where the 2022 catalog is
silent; tail flag groups split into 1-bit fields). The remaining alerts either
have no published field names, are MIA source-bit markers, or are register-built
packers left to the reason-only fallback.

Also fixes a latent drive-unit bug: the CAN-rationality decoder paired the
offending-id / errorType / badValues positionally, but their catalog order varies
(DIR a094 = [canID, errorType, ...] vs a066 = [badValue1, badValue2, canID,
errorType]); now matched by field name, which fixes PCS a030 and the DU a066 family.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three bench-relevant charge/DC-DC alerts the interpreter couldn't cleanly
resolve, traced by hand from the CPU1/CPU2 integer store/shift/mask path:
- a018 chgUnknownGridConfig: recovers the lost L1L2Locked flag (bit 39), set via
  the final AND #0x1f7f + OR rather than a per-flag clearmask.
- a044 12vSupportRegulation: FaultReasonCode is one 2-bit field (was split into
  two 1-bit), plus shortTime_us / dcdcLvBusCurrent_A / vTankPeak_abs_V.
- a076 dcdcEnDeassertedErr: the DAT_15484[] table byte at bits 8-15 splits into
  dcdcEnableLine[8,1] + dcdcPwmEnableDeassertedCount[9,7] by the ascending-bit ==
  catalog-order convention.

Recorded as verified overrides in the (local) extractor. The remaining bench
alerts (a013/a053/a054/a073/a014/a038) have unverifiable internal field splits
(pre-combined source regs, scratch-staged overwrites, catalog gaps) and are left
to the reason-only fallback rather than guessed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed-on: #12
Merge branch 'main' into feat/odin-support
Some checks failed
Tests / test (pull_request) Has been cancelled
4193beab70
outlandnish closed this pull request 2026-08-28 18:12:31 -05:00
Some checks failed
Tests / test (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
outlandnish/tm3diag!13
No description provided.