feat(sim): simulate the AWD front drive unit (DIF + PMF) #26

Merged
outlandnish merged 7 commits from feat/sim-dif-front-unit into main 2026-09-12 21:22:08 -05:00
Owner

Closes step 3 of #25 — lets a single AWD rear drive unit run on the bench with a simulated front.

An AWD rear ("Master", usage digit 0) expects a front drive unit on the bus: it supervises the front's frames for MIA and raises difMIA (DIR_a040) / frontUnitDisabled (DI_a138) without them. A RWD rear ("Single", usage 2) subscribes to none of them. Two new nodes stand the front in.

The front is two nodes, not one

It is one physical unit running two cores, so both halves are needed:

node core frames
DIF CPU2 0x186 DIF_torque, 0x187, 0x2D5 DIF_status, 0x2E5 DIF_power
PMF CPU1 0x1D5 PMF_state4

Enumerated off the 2022.45.15 AWD DIR (di/12804, 28-65-0) rx handlers and diffed against the RWD di/12803 (28-65-2) at the same revision, so the set is the AWD delta and nothing else. Full write-up in docs/private/awd-dir-dif-can-interface.md.

Three things the DBC gets wrong, all firmware-read

  • 0x2D5 is DLC 7 on 2022.45.15, where every DBC says 8 (and it is 8 again on 2026.8.3). checkRxDlc is an exact match, so a DLC-8 frame is rejected outright and the frame goes MIA looking healthy on the wire. This is why the revision pin in the scenarios is load-bearing rather than cosmetic.
  • The DIF frames put the checksum in byte 0 and the counter in byte 1's low nibble, not the byte7/byte6 layout most validated frames use.
  • 0x1D5's counter is 3 bits at bit 53, not 4. A 4-bit counter there would run into the checksum byte. A test sends nine frames and asserts the ninth equals the first, so a silent widening fails.

0x1D5 also nearly shipped with the wrong comment: it looks 2026-only because the 2022 rear DIR does not subscribe to it — but the rear PMR does, and the PMR is real hardware on a drive bench. On a dual-core node, sweep both cores.

absent is now separate from real

DIF forced the issue: the same node is real on a bench with a physical front and not-fitted-at-all on a RWD car. Both mean "don't simulate", so drive.toml was overloading real to mean the opposite of what it says — and a bench with two physical units exists, so the two meanings collide on one lever.

[nodes] absent = [...] drops a node because the car does not have that ECU; real keeps its meaning. Listing a node in both raises rather than silently picking one, since the failure mode is the sim transmitting over a real inverter.

Three drive profiles

drive.toml            real=[DI,DIR,PMR]          absent=[DIF,PMF]  60 frames  drivetrain=RWD
drive-awd.toml        real=[DI,DIR,PMR]          absent=[]         65 frames  drivetrain=AWD, front simulated
drive-awd-both.toml   real=[DI,DIR,PMR,DIF,PMF]  absent=[]         60 frames  drivetrain=AWD, front physical

drive.toml gains an explicit RWD car config — already the GTW defaults, so no behaviour change; it just puts the one thing separating the profiles in the same visible place in each. All three use enum labels ("AWD", "3_CHASSIS") rather than raw numbers so they survive a revision renumbering.

A bench with both units physical needs nothing extra from the sim: all seven rear→front frames are in the rear's own TX descriptor tables, and 0x1D5 comes from the physical front's own PMF.

Verification

3211 tests pass, ruff clean. All three profiles were expanded against the real DBC and checked frame by frame (bus, DLC, counter/checksum placement). The golden inventory locks the new frames; separate tests pin the 2022-vs-2026 DLC split, the seed and counter placement, and the real/absent exclusivity.

Judgement calls worth a second opinion

  • number_hvil_nodes left unset on the AWD profiles. A two-DU car really does have more HVIL nodes, but the 2022 images are not gated on it and it only feeds a144 configMismatch, so I matched the working RWD bench rather than guess. Flagged in-file to revisit if a144 appears.
  • A 2024.8.9 target inherits the 2022 entry and so sends 0x2D5 at DLC 7. Unverified — no 2024 AWD DIR is imported, and the DBC is not evidence here since it says 8 for 2022 too. Called out in dif.py.
  • PMF's 10 ms cycle time is DBC-sourced, not firmware-read. The 2022 DBC has no 0x1D5; the PMF image has no checkRxDlc-style helper to read a rate from. The error is one-sided — too fast cannot cause an MIA — so it is safe unless the true rate is faster.

Not in scope

Payload content. Every frame is zeros, per the existing zeros() contract: MIA gates on DLC + checksum/counter, not values. That is enough to clear difMIA, but not enough for the rear's VDC cross-checks against the front (DI_a207 vdcPowertrainTorque_dif, DI_a211 vdcMotorSpeed_dif) — those compare the front's torque and speed to its own, and no amount of frame coverage substitutes for consistent content.

🤖 Generated with Claude Code

Closes step 3 of #25 — lets a single AWD rear drive unit run on the bench with a simulated front. An AWD rear ("Master", usage digit 0) expects a front drive unit on the bus: it supervises the front's frames for MIA and raises `difMIA` (DIR_a040) / `frontUnitDisabled` (DI_a138) without them. A RWD rear ("Single", usage 2) subscribes to none of them. Two new nodes stand the front in. ## The front is two nodes, not one It is one physical unit running two cores, so both halves are needed: | node | core | frames | |---|---|---| | **DIF** | CPU2 | `0x186 DIF_torque`, `0x187`, `0x2D5 DIF_status`, `0x2E5 DIF_power` | | **PMF** | CPU1 | `0x1D5 PMF_state4` | Enumerated off the 2022.45.15 AWD DIR (`di/12804`, 28-65-0) rx handlers and diffed against the RWD `di/12803` (28-65-2) at the same revision, so the set is the AWD delta and nothing else. Full write-up in `docs/private/awd-dir-dif-can-interface.md`. ## Three things the DBC gets wrong, all firmware-read - **`0x2D5` is DLC 7 on 2022.45.15**, where every DBC says 8 (and it is 8 again on 2026.8.3). `checkRxDlc` is an exact match, so a DLC-8 frame is rejected outright and the frame goes MIA looking healthy on the wire. This is why the revision pin in the scenarios is load-bearing rather than cosmetic. - **The DIF frames put the checksum in byte 0 and the counter in byte 1's low nibble**, not the byte7/byte6 layout most validated frames use. - **`0x1D5`'s counter is 3 bits at bit 53**, not 4. A 4-bit counter there would run into the checksum byte. A test sends nine frames and asserts the ninth equals the first, so a silent widening fails. `0x1D5` also nearly shipped with the wrong comment: it looks 2026-only because the 2022 rear **DIR** does not subscribe to it — but the rear **PMR** does, and the PMR is real hardware on a drive bench. On a dual-core node, sweep both cores. ## `absent` is now separate from `real` DIF forced the issue: the same node is `real` on a bench with a physical front and not-fitted-at-all on a RWD car. Both mean "don't simulate", so `drive.toml` was overloading `real` to mean the opposite of what it says — and a bench with two physical units exists, so the two meanings collide on one lever. `[nodes] absent = [...]` drops a node because the car does not have that ECU; `real` keeps its meaning. Listing a node in both raises rather than silently picking one, since the failure mode is the sim transmitting over a real inverter. ## Three drive profiles ``` drive.toml real=[DI,DIR,PMR] absent=[DIF,PMF] 60 frames drivetrain=RWD drive-awd.toml real=[DI,DIR,PMR] absent=[] 65 frames drivetrain=AWD, front simulated drive-awd-both.toml real=[DI,DIR,PMR,DIF,PMF] absent=[] 60 frames drivetrain=AWD, front physical ``` `drive.toml` gains an explicit RWD car config — already the GTW defaults, so no behaviour change; it just puts the one thing separating the profiles in the same visible place in each. All three use enum labels (`"AWD"`, `"3_CHASSIS"`) rather than raw numbers so they survive a revision renumbering. A bench with both units physical needs nothing extra from the sim: all seven rear→front frames are in the rear's own TX descriptor tables, and `0x1D5` comes from the physical front's own PMF. ## Verification 3211 tests pass, ruff clean. All three profiles were expanded against the real DBC and checked frame by frame (bus, DLC, counter/checksum placement). The golden inventory locks the new frames; separate tests pin the 2022-vs-2026 DLC split, the seed and counter placement, and the `real`/`absent` exclusivity. ## Judgement calls worth a second opinion - **`number_hvil_nodes` left unset** on the AWD profiles. A two-DU car really does have more HVIL nodes, but the 2022 images are not gated on it and it only feeds `a144 configMismatch`, so I matched the working RWD bench rather than guess. Flagged in-file to revisit if a144 appears. - **A 2024.8.9 target inherits the 2022 entry** and so sends `0x2D5` at DLC 7. Unverified — no 2024 AWD DIR is imported, and the DBC is not evidence here since it says 8 for 2022 too. Called out in `dif.py`. - **PMF's 10 ms cycle time is DBC-sourced, not firmware-read.** The 2022 DBC has no `0x1D5`; the PMF image has no `checkRxDlc`-style helper to read a rate from. The error is one-sided — too fast cannot cause an MIA — so it is safe unless the true rate is faster. ## Not in scope Payload content. Every frame is zeros, per the existing `zeros()` contract: MIA gates on DLC + checksum/counter, not values. That is enough to clear `difMIA`, but **not** enough for the rear's VDC cross-checks against the front (`DI_a207 vdcPowertrainTorque_dif`, `DI_a211 vdcMotorSpeed_dif`) — those compare the front's torque and speed to its own, and no amount of frame coverage substitutes for consistent content. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
An AWD ("Master") rear rx's four front-inverter frames and supervises all four for
MIA, so a lone AWD rear on the bench flags difMIA. A RWD ("Single") rear rx's none
of them. New DIF node sends exactly those four.

Enumerated off the 2022.45.15 AWD DIR (di/12804, 28-65-0) rx handlers and diffed
against the RWD di/12803 (28-65-2) at the same revision, so the set is the AWD delta
and nothing else.

Two details the DBC gets wrong, both firmware-read:
- 0x2D5 is DLC 7 on 2022.45.15 (DBC says 8 at every revision), DLC 8 again on
  2026.8.3. checkRxDlc is an exact match, so a DLC-8 frame is rejected outright.
- Checksum in byte 0, counter in byte 1 low nibble, not byte7/byte6.

Seeds are the plain id_lo+id_hi rule, so place_checksum needs no override. 0x2E5 is
ungated and is the only one of the four on the vehicle bus.

DIF is the first node authored past 2022, which the fw-versioning golden did not
allow for; its varied-node set becomes a node -> revision map.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drive-awd.toml benches an AWD rear ("Master") with the front unit simulated: car
config declares drivetrainType=AWD and DIF stays out of `real` so the sim stands in
for the front. Without that the rear raises difMIA (a040) + frontUnitDisabled (a138).

drive.toml gains the matching explicit RWD car config. Both values were already the
GTW defaults, so this is documentation rather than a behaviour change -- but it puts
the one thing that separates the two profiles in the same visible place in each.

drive.toml also lists DIF in `real`, for the opposite reason to the inverter: a RWD
car has no front unit at all, and `real` is the only lever that takes a node out of
the broadcast set. A RWD rear subscribes to none of the four DIF IDs, so simulating
them would only add unhandled traffic. Worth a dedicated "absent" key later.

The revision pin is load-bearing on the AWD side: DIF_status 0x2D5 is DLC 7 on
2022.45.15 where every DBC says 8, and the length check is an exact match.

The golden stub DB gains value_description for the two carconfig signals so a
scenario can name an enum label rather than a raw number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
DIF forced the issue: the same node is `real` on a bench with a physical front
inverter and not-there-at-all on a RWD car. Both mean "don't simulate", so drive.toml
was overloading `real` to mean the opposite of what it says -- and a bench that really
does have two units now exists, so the two meanings collide on one lever.

[nodes] absent = [...] drops a node because the simulated car does not have that ECU;
`real` keeps its meaning, hardware sources those frames. Listing a node in both raises
rather than silently picking one, since the failure mode is the sim transmitting over
a real inverter.

drive-awd-both.toml benches an AWD pair with both halves physical. It needs nothing
extra from the sim: all seven rear->front frames sit in the rear's own TX descriptor
tables (0x108 0x118 0x136 0x142 at word 0x000bb50e, 0x148 0x257 at 0x000bb7e2), so the
real DIR sources them, and 0x1D5 PMF_state4 comes from the front unit's own PMF.

That TX-table read also settles a question the earlier work left open: rear->front was
inferred from the front's rx set plus frame naming. It is now read off the rear.

Note 0x136 and 0x142 are NOT the ETH DBC's names for those IDs -- the front receives
them on the party bus at DLC 8 and 5, where the DBC describes vehicle-bus messages
(0x142 VCLEFT_liftgateStatus is DLC 8). Same ID, different bus, different message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The front drive unit is ONE physical unit running two cores, so standing it in needs
both halves: DIF is CPU2, PMF is CPU1. Simulating only DIF left a gap — from 2026 the
rear subscribes to 0x1D5 PMF_state4 and raises pmfMIA (DI_a042) without it. The 2022
rear does not subscribe to it, so this is latent rather than live on the pinned bench,
but the profile is now correct if the pin is raised.

0x1D5 field placement is firmware-read and identical in the 2022 DIF validator and the
2026 DIR one: seed 0xD6 (the plain id_lo+id_hi rule), checksum in byte 7, and a 3-BIT
counter at bit 53. A 4-bit counter there would run into the checksum byte.

Which exposed a bug in the rx enumerator: it matched counter masks of `& 0xf` only, so
every 3-bit counter read back as "no counter". That is why 0x1E5, 0x289, 0x2B9, 0x1D8,
0x1D5 and the 2026 0x209/0x20A/0x20E all looked counter-less. 0x1E5 is narrower still
at 2 bits. The existing golden already had 0x2B9 as (53, 56, 3), which corroborates the
corrected reading rather than relying on it.

drive-awd-both.toml lists PMF as `real` alongside DIF: a bench with a physical front
has both cores, and transmitting 0x1D5 over a real PMF is exactly the collision `real`
exists to prevent. drive.toml marks both `absent` — a RWD car has neither.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Importing the 2022 power modules corrected the PMF comment I just wrote. 0x1D5
PMF_state4 looked 2026-only because the 2022 rear DIR (CPU2) does not subscribe to it.
The 2022 rear PMR (CPU1) does. The PMR is real hardware on a drive bench, so a
simulated front that omits 0x1D5 leaves it in pmfMIA (DI_a042) on a 2022 bench, not
at some later revision.

The lesson generalises: the rear is a dual-core node and both cores must be swept.
Reading only the DIR gave a confidently wrong answer about a frame the unit does
supervise.

Also from those images (docs/private/awd-dir-dif-can-interface.md):
- 2022 PMR AWD delta, controlled RWD/AWD pair: AWD rx's 44 frames, RWD 40; delta is
  0x136, 0x186, 0x187, 0x1D5. 0x2D5 and 0x2E5 are DIR-only, so the front->rear set is
  not uniform across the rear's two cores.
- PMF (pm/12875) has NO checkRxDlc helper at all, so its rx set cannot be enumerated
  the same way and its transmit rate cannot be read off it. 0x1D5's 10 ms stays
  DBC-sourced; the error is one-sided, since transmitting faster than required cannot
  cause an MIA.

No behaviour change -- the PMF node already sent 0x1D5 at every revision. Only the
comments claiming it was inert before 2026 were wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs(sim): DI and PM are meta nodes, published by the master unit
Some checks failed
Tests / test (pull_request) Has been cancelled
0e5476f6c9
opt/odin/data/Model3/nodes.json is the authoritative diagnostic node table (33 nodes
at 2022, 37 at 2026). It lists DIR, DIF, PMR and PMF -- each with its own UDS
request/response pair, boot ID and tesla_hash security -- and has no DI and no PM
entry at all.

So DI_* and PM_* are message namespaces with no ECU behind them, published by the
master (rear) unit on the pair's behalf. That is why DI_speed and DI_odometerStatus
come out of a rear inverter, and why a RWD car carries a full DI_* set with one drive
unit fitted.

Read from the four 2022 gen-28 images:
  rear DIR (CPU2)  DIR_* (0x108) + DI_* aggregates (0x118 0x148 0x257 0x3B6) + 0x136 0x142
  rear PMR (CPU1)  PMR_* + PM_* aggregates (0x1E5 PM_locState) + 0x240
  front DIF (CPU2) DIF_* -- 0x186 0x187 0x2D5 0x2E5
  front PMF (CPU1) PMF_* -- 0x1D5

This explains a loose end: 0x1E5 and 0x240 have no vehicle_sim source and the drive
bench works anyway, because the real rear PMR sends them. It also confirms the sim's
separate DI and DIR nodes are correct modelling rather than redundancy -- both sets
come off one physical rear unit, which is why drive.toml marks both real.

Docs only; no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge private/main into feat/sim-dif-front-unit
Some checks failed
Tests / test (pull_request) Has been cancelled
d62fc2cda9
Picks up the 2026.8.3 message variants (#24), which landed on main while this branch
was in progress and touches the same two test files.

One conflict, in test_only_fw_varied_nodes_diverge_from_baseline_today: main split the
expectation into varied_2022 / varied_2026 sets and added a second assertion that a
2022 target still resolves to the 2022 set. Kept main's structure and put DIF in BOTH
sets -- it authors a 2022.45.15 variant (DIF_status 0x2D5 at DLC 7) and a 2026.8.3 one
(DLC 8), so it must resolve to each at its own target. PMF is baseline-only.

3215 tests pass, ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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!26
No description provided.