Flash-session firmware work, live on the node since 2026-06-12:
- battery voltage decode (29-bit page-0x11 telemetry, b2..b3 BE / 256),
gated to extended frames so an 11-bit node 0x11 can never spoof it,
with a delta/throttle filter to stop 1/256-V jitter churning both
HA recorders and the MQTT bridge
- second on_frame trigger (use_extended_id: false) sharing the decode
lambda via YAML anchor — this esp32_can build filters triggers by
frame type, so a single trigger silently dropped the 11-bit reads
- switches optimistic:false now the page-3 read-back is verified live
- arm retry widened to 8x150ms; module-side ~2s post-success cooldown
documented
- canbus component logs to INFO (per-frame DEBUG dump saturated serial)
- toolchain fixes: named std::array initializer, namespaced cover enums
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per updated policy:
- Water heater (node 95): new switch entity + on/off state read-back.
- Awning (node 75): new cover entity (open=op01 / close=op02 / stop=op00), with
current_operation published from the page-3 motion byte (C2/C3/C0). First
actuation must be attended; single-shot commands can't run the motor away.
- Water pump (node 61): added to command_guard denylist (winterizing-only,
panel/app only) alongside slides/jacks. Guard re-tested 8/8 (host g++).
Switch/cover comments + HANDOFF safety notes and remaining-work updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per policy, the firmware must never command a slide or jack. command_guard.h adds
command_blocked(node, type): refuses any motor-class (0x21) node except the
awning (0x75), with an explicit slide/jack denylist (6A/7F/9C) that holds even
before a node's page-2 identity is observed.
Enforced in two independent places — the command-entry script (send_load_command)
and the actual transmit point (on_frame, right before TX) — so loosening one
can't open the other. Adding a slide/jack switch entity cannot actuate it. Node
device class is learned from page-2 broadcasts (g_node_type). Predicate
unit-tested 9/9 (host g++). Switch comments + HANDOFF safety notes updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Command path proven end to end on the bus (node F8 interior lights, on/off/on),
each answering a distinct fresh challenge; bare opcodes without the exchange are
ignored. ids_can_auth.h verified bit-exact against ids_can_auth.py and the
captured/live pairs.
- idscan_cmd.py: stdlib socketcan tool running the full page-42/43 exchange
- esphome/onecontrol-canbus.yaml: correct IDS-CAN read dispatch (was stale RV-C
DGN code) + command path wired to the auth header
- README/memory: document the read map + command authentication; rename
sniff/ -> captures/; neutral device-integration framing throughout
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CAN write gate (page-42/43 challenge/response) is a 32-round TEA/XTEA-family
Feistel keyed by a per-session 32-bit key; REMOTE_CONTROL = 0xB16B00B5. Verified
51/51 against captured challenge/response pairs across nodes 2A/61/75/F8 (one
global key, not per-node), so the CAN path can now actuate, not just sense.
- ids_can_auth.py Python reference + self-test (51/51)
- esphome/ids_can_auth.h C++ port for the ESP32 node (host-tested 8/8)
- sniff/analyze_auth.py structural analysis (rules out affine; confirms keyed cipher)
- sniff/auth-pairs-multinode-2026-06-11.txt +9 pairs across 4 nodes
- README document the cipher, session keys, unlock sequence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tapped the X180T's CAN bus via CANable 2.0 at the monitor panel's terminator
port. The bus is NOT RV-C — it's Lippert's proprietary IDS-CAN (250k, 11-bit
IDs, (page<<8)|node, 1 Hz broadcasts).
Read side fully mapped from live captures:
- device classes (page-2 type byte: 0x0A tank, 0x1E switched load, 0x21 motor)
- node map for this rig (Catalina 263BHSCK): tanks 27/E2/7D/FE, lights 2A/F8,
heater 95, pump 61, awning 75 (+ direction & live motor current)
- battery voltage on 29-bit extended frames
Write side: commands are DLC-0 ext frames 0006<node><op>, but auth-gated by a
rolling challenge-response (page 42/43). Replay confirmed dead (spoofed cansend
did not actuate). Not the BLE TEA cypher. response=f(challenge) is deterministic
(no session state) so crackable offline later — seeded 42 pairs in
sniff/2A-auth-pairs.txt.
Includes raw captures (sniff/*.log, force-added past *.log ignore), a read-only
esp32_can ESPHome skeleton, and the log-can.sh sniff helper. Full writeup in
canbus/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>