DIR learns (dir_learn), UDS robustness, sim fixes for the client DU bench #27

Merged
outlandnish merged 53 commits from wip/client-du-learn into main 2026-09-23 15:46:24 -05:00
Owner

Client DU bench work: DIR learns, UDS robustness, sim fixes.

Learns

  • scripts/di/dir_learn.py (replaces offset_learn.py): offset / flux / all / resolver-error (rolls, ODIN) and resolver (dyno 0x407, raw UDS). Brake/shift/traction choreography, DIR resets (--reset), DID backup before START, rotor temp auto.
  • Logs spin profile, DI_systemLimits, DI_systemState changes, DIR vBat/current, and the DU alerts raised during the learn. Never shifts to P while the motor spins.
  • ODJ linear map for ROTOR_TEMPERATURE (-40 °C was sent as +176 °C); START-param overrides via /api/odin/run.
  • Offset learn verified on the client DU (SUCCESS).

UDS

  • TesterPresent: DLC 8, deferred only while a transfer is on the wire, keep-alive restarted in a tester-present context, stats + gap logging.
  • Positive replies must echo the DID / routine id (a late reply shifted a DID backup by one).
  • 20 ms ECU turnaround; resend when FC never came; never resend SecurityAccess.

Sim

  • VCFRONT 0x3A1 multiplexed on 2022+; UI 0x334 2022 layout + UI_limitMode.
  • ESP 0x175 rear wheel speeds follow DIR_axleSpeed (tire size from GTW_wheelType); SNA-safe.
  • GTW_carConfig: any signal by DBC name from a scenario (case/underscores ignored).
  • Scenario keys vetted against the loaded CAN DB.
  • BMS pack voltage mirrors the measured bus (DIR 0x126, else PCS 0x2B4, else 0 V); min_bus_voltage key.
  • scenarios/drive-learn.toml.

Tests: full suite green (3284).

🤖 Generated with Claude Code

Client DU bench work: DIR learns, UDS robustness, sim fixes. **Learns** - `scripts/di/dir_learn.py` (replaces `offset_learn.py`): offset / flux / all / resolver-error (rolls, ODIN) and resolver (dyno 0x407, raw UDS). Brake/shift/traction choreography, DIR resets (`--reset`), DID backup before START, rotor temp auto. - Logs spin profile, DI_systemLimits, DI_systemState changes, DIR vBat/current, and the DU alerts raised during the learn. Never shifts to P while the motor spins. - ODJ linear map for ROTOR_TEMPERATURE (-40 °C was sent as +176 °C); START-param overrides via `/api/odin/run`. - Offset learn verified on the client DU (SUCCESS). **UDS** - TesterPresent: DLC 8, deferred only while a transfer is on the wire, keep-alive restarted in a tester-present context, stats + gap logging. - Positive replies must echo the DID / routine id (a late reply shifted a DID backup by one). - 20 ms ECU turnaround; resend when FC never came; never resend SecurityAccess. **Sim** - VCFRONT 0x3A1 multiplexed on 2022+; UI 0x334 2022 layout + UI_limitMode. - ESP 0x175 rear wheel speeds follow DIR_axleSpeed (tire size from GTW_wheelType); SNA-safe. - GTW_carConfig: any signal by DBC name from a scenario (case/underscores ignored). - Scenario keys vetted against the loaded CAN DB. - BMS pack voltage mirrors the measured bus (DIR 0x126, else PCS 0x2B4, else 0 V); `min_bus_voltage` key. - `scenarios/drive-learn.toml`. **Tests**: full suite green (3284). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A timeout and a broken multi-frame reassembly both reported "no response
received". Quote the node's response-id frames in the error to tell them apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Native scripts report routine results (ROUTINE_STATUS, LEARN_RESULT) as metric
metadata; the UI dropped it, hiding every failure reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The codec ignored linear scaling and wrapped a negative uint to two's complement,
so ROTOR_LEARNING's ROTOR_TEMPERATURE (-40 °C, offset -40) went out as 0xD8.
Encode now inverts raw*factor/denominator+offset (-40 -> 0x00) and refuses
out-of-range values; parsed decode returns physical units.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The keep-alive thread sent 3E 80 every 0.5 s regardless of a request in flight,
so it could land inside a multi-frame reply. It now waits for the exchange to
finish, and uds_tester_present_context applies the script's interval (0.1 s for
the DIR learns) for its block. Pins the learn procedures' request bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Holding the keep-alive for the whole exchange starved it for up to the 2 s
timeout on a silent ECU or a 0x78 wait. Now 3E 80 waits only for our own
request frames, an ECU multi-frame reply in progress, or up to 50 ms for a
reply to start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
py-uds sent the hand-built 3E 80 packet data-optimized (DLC 3), and the DIR
drops frames on its request id whose DLC isn't 8 -- so the keep-alive never
arrived and a learn's TesterPresent watchdog could lapse. Also start the reply
guard after the last request frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The keep-alive thread waited one interval (0.5 s) before its first 3E 80, so
on a fresh session the DIR saw ROTOR_LEARNING START with no TesterPresent yet
and killed the job at once (FAILED_OTHER_ROUTINE_RUNNING, then
FAIL_NO_TESTER_PRESENT). Send the first one immediately, apply interval changes
within 20 ms, and have uds_tester_present_context send one on entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The DIR needs the first detent held >= 1 s to leave R or D. A 1.0 s pulse at
100 ms frames put only ~0.9 s of held frames on the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drive.toml plus the UI state the DIR rotor-offset / resolver-error learns check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Starts the ODIN procedure via tm3web, and once START succeeds does brake on ->
D -> wait for DI_gear D -> brake off, so the CAN brake is released before the
wheels turn (otherwise FAIL_BRAKES).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
After the learn result: wait for DI_vehicleSpeed ~0 for 1 s, then brake on ->
P -> brake off. --no-park leaves it to the operator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
During the learn, log changes in the three inputs the DIR requires before the
axle may pass ~11 rpm (IBST_driverBrakeApply 0x39D == 1, EPB systemStatus
0x2A8/0x2E8 == 1, ESP 0x185 wheel speeds 0), decoded by firmware layout, and
warn when the axle spins while one is false. tm3web gains /api/frames (latest
raw payload per bus) to feed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pressing the CAN brake after START tripped FAIL_BRAKES as soon as the axle
turned (it read ~13 rpm in P). Shift first, wait for the spin permit with the
axle still, then start. --shift-after-start keeps the old order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A wrong rotor offset makes the enabled inverter creep the motor (~2 km/h) in
P/D, so don't wait for a still axle before START, and treat <= 3 km/h as
stopped before shifting to P.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
START in D failed MOTOR_SPEED_DETECTED: the un-learned offset creeps the rotor
whenever the inverter is on. Start in N with a still axle, then brake -> D ->
release as soon as DI_gear reads D (polled every 20 ms). --start-gear D|current
for the other orders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
N always used DOWN_1, which in D just means "stay in drive", so D -> N needed a
detour through R. SCCM now tracks DI_gear (0x118) and pushes UP_1 from D.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In N the DIR sends 0x8000 (SNA), which read as -3276.8 rpm and blocked START.
Treat it as unknown and check DI_vehicleSpeed for a still axle instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
N -> D didn't commit 0.5 s after brake on. Wait for DI_brakePedalState ON
(tm3web dash now exposes it), re-send the gear request every 2 s, 8 s timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
To tell a keep-alive that stalled from one the ECU didn't count when a learn
ends FAIL_NO_TESTER_PRESENT. Warns on a gap > 0.3 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once a second, with the peak, to see whether the spin-up reached the DIR's
coast trigger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DIR's 16-byte security key (sent right after its multi-frame seed reply)
got no flow control and the run aborted. Wait 20 ms after the ECU's last
frame before a request, and resend a multi-frame request up to twice on an FC
timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resending the stateful 27 05/27 06 handshake desyncs the seed/key, so cap its
FC retries at 0. And silence aiohttp.access so the keep-alive warnings aren't
buried under a line per /api poll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UdsSession counts 3E 80 sent, send failures, and the largest gap between sends;
tm3web exposes them at /api/uds-health and offset_learn.py prints them when the
learn result arrives. Separates our keep-alive stalling from the DIR not
counting frames we sent. Also surfaces DI_tractionControlMode (0x118) in the
spin log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
START refuses with INVALID_DI_STATE otherwise. The keep-alive warned on every tick at
the 0.5 s idle interval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The enum was wrong (2 = STANDBY per the DBC), so STANDBY blocked START.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The last learn reported only 1 TesterPresent sent. Log the thread's start, stop and
counters every 2 s, and report session id, thread liveness and phase so we can
tell a stalled thread from a stats read of the wrong session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The offset learn waits for the axle to pass ~443 rpm, then for DIR_torqueActual to hit
0 within ~1 s, then a coast back below 443; >~720 rpm fails. Last run peaked at 423 rpm
and timed out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ecu_reset stops the keep-alive thread and a long-lived bench session never restarted
it, so every learn after a reset failed FAIL_NO_TESTER_PRESENT. Only the keep-alive
thread now sets tp phase; offset_learn flags a dead keep-alive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2022 re-laid 0x334. The 2020 build on a 2022 DIR read as UI_limitMode=SERVICE plus a
real UI_speedLimit (motorOnMode/stoppingMode bits), capping motor speed. UI_speedLimit is
unlimited only at raw 0x1FF (DIR RX handler).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ROTOR_LEARNING learns offset, flux or both (LEARN_SELECT); Tesla's script hard-codes
ROTOR_OFFSET at a fake -40 C. /api/odin/run takes routine_params {routine: {param: value}},
merged into that routine's START params for the run and logged in the trace.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Older DUs report stator temperature (0x315 mux 0), newer only oil (0x395, high-confidence
QF). Implausible or absent readings are skipped; with neither, the learn is not started.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reads 0x306/0x309/0x30b (no security needed) into dir_cal_backup_<time>.json so a bad
learn can be written back; no START if the backup fails (--no-backup to skip).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The MCU (VehicleLinkUtils::limitMode) sends UI_limitMode=SERVICE in service mode unless
GUI_factoryModeLimitOverride, which gives NORMAL; the override has no CAN bit of its own.
The 2022 0x334 builder now derives limitMode that way, and the ODIN bench backend mirrors
cid_set GUI_serviceMode / GUI_factoryModeLimitOverride into vehicle_sim, restoring the
sim's values after the run. New sim UI setting factory_mode_limit_override.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
New ESP setting wheel_speeds = zero|axle (default zero). "axle" reports the rear wheels
turning with 0x108 DIR_axleSpeed, as the ESP would with the car on a lift; drive-learn
turns it on. 0x175 now packs the ETH DBC ESP_wheelSpeeds layout; 0x185 relabeled as
per-wheel brake torques.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ESP node reads GTW_wheelType (0x7FF mux3) and uses the stock tire for that rim
(18/19/20/21") instead of a fixed 235/45R18. GTW gains a wheel_type scenario key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
[scenario.GTW] now accepts GTW_* signal names (enum label or raw) alongside the curated
keys; unknown names or labels are errors. Drops the wheel_type key added in 8b1d466.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gtw_wheeltype / gtw_wheel_type resolve to GTW_wheelType.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From 2022 bit 0 is VCFRONT_vehicleStatusMuxIndex and VCFRONT_bmsHvChargeEnable is page 1
bit 1. The sim set bit 0 for charge-enable, which flipped the frame to page 1 and dropped
12vStatusForDrive while charging. VCFRONT now alternates pages on 2022+; BMS/HVP read
charge-enable from page 1 there (2020 unchanged).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scenario_signals.vet runs before each node's configure: for UI/ESP/VCFRONT keys that set one
DBC signal, the signal must exist and the value must be one the DBC defines. Values may be the
sim's option names or DBC value names, keys the sim key or DBC signal name (case/underscores
ignored). A sim option the DBC names differently is warned about.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces offset_learn.py. --learn offset|flux|all|resolver-error run the ODIN procedure with
the brake/shift choreography, latching ROLLS and using each learn's spin target (443/609 rpm).
--learn resolver drives LEGACY_RESOLVER_LEARNING (0x407) over /api/uds/op: latch DYNO (+
UI_developmentCar) in N, spin in D, shift N and retry START until the DIR arms it, then poll
results. --reset auto|before|after|both|never resets the DIR: before a dyno learn whose dyno
latch tripped or while DI_systemState is FAULT, and once if START is refused
INVALID_DI_STATE.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fake tm3web now serves /api/ui, /api/uds/op and /api/did/read and a DIR that follows
gear, traction mode, spin and resets. Covers rolls/flux/resolver-error ODIN runs, the
INVALID_DI_STATE reset + retry, --reset modes, the dyno learn (START refused until standby,
never armed, tripped dyno latch) and the pure decoders. RESET_SETTLE_S is now a constant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The DIR reads UI_developmentCar only as the dyno one-shot bypass, so rolls learns
(offset/flux/resolver-error) don't need it. drive-learn.toml no longer sets it and
dir_learn.py turns it off for rolls learns (on for --learn resolver).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The axle mode read raw 0x8000 as -3277 rpm and sent ~344 km/h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffs tm3web's /api/alerts log before/after the learn, so FAIL_DI_FAULT names its alert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
It shifted to P at 16 km/h after the 20 s standstill timeout. Now it keeps waiting
(up to 5 min), then leaves P to the operator.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A late 62 reply shifted a 0x306/0x309/0x30B backup by one DID. dir_learn also tells the
operator to stay off the accelerator until D (the DI denies the shift otherwise).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
follow_di_vbat mirrors DIR_vBat (0x126) into BMS_packVoltage so a test pack's sag under
load doesn't trip the DIR's bus-voltage cross-check (a038). drive-learn: 350 V + follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
BMS_packVoltage = DIR_vBat (0x126), else a real PCS's PCS_dcdcHvBusVolt (0x2B4), else 0 V.
Drops the fixed 373 V and the pack_voltage/follow_di_vbat keys.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- VCFRONT has a 2022 set; BMS/HVP decode the page-1 charge enable (2020: bit 0)
- BMS packVoltage mirrors DIR 0x126 / PCS 0x2B4 (else 0 V); min_bus_voltage key
- drive-learn: ESP axle wheel speeds, development_car off
- keep-alive fixture gets the TP counters; adapter restarts the keep-alive

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test: dir_learn alerts/state/vBat/park and the UDS stale-reply echo check
Some checks failed
Tests / test (pull_request) Has been cancelled
e556306422
- FAIL_DI_FAULT names the new/re-logged DU alerts and the FAULT transition (vBat, current)
- _park waits for standstill, never shifts P while spinning
- read_did/routine_control skip a late reply for another DID/routine

Co-Authored-By: Claude Opus 5.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!27
No description provided.