Motion is hold-to-run (one authed opcode runs the motor ~1s). Reproduce the OEM
movement session -- auth once, then stream the opcode @110ms + a page-44
keepalive @510ms -- to sustain continuous retract, and watch node-75 page-3
motor current at 20Hz to stop at the fully-closed stall (~4200 vs ~<1550
running), then mark the cover CLOSED. Backstops: 70s timeout, motion-lost
detector, hold-to-run stop-on-silence. Proven live at the camper 2026-07-01.
- esphome: awning_auto_retract script + 100ms streamer interval + case-0x75
stall gate + "Awning Motor Current" sensor. Cover CLOSE=auto-retract,
STOP=abort. Removed open_action (1s jog is useless, no safe timed auto-open).
- bridge: optimistic:true keeps the home HA cover assumed-state so retract is
always pressable (was greyed when closed); payload_open:null drops OPEN on the
home (primary) dashboard. Also synced the 6h->15min discovery-cadence drift.
- captures + README: full-retract stall profile and the live auto-retract test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decode node 89 (furnace) read-only: Furnace Running (page-3 b0 bit0) and
Furnace DSI Fault (b0 bit5), same type-0x1E DSI encoding as the water
heater. The furnace is thermostat-controlled, not a Lippert load — it
sits on the bus only to report ignition state. Captures only ever showed
0x80 (off), so the 0x81 running / 0xA0 fault bytes are inferred by
parallel to node 95; confirm on the first real burn.
Add Water Heater Heating (node 95 page-3 x[3] bit7 = burner actively
firing) — distinct from the on/off switch, which only reflects "enabled".
Carry all three through the MQTT bridge to home HA, add the furnace
DSI-fault Octavia alert mirroring the water heater, and update the README
node map (89 was "unknown switched load"). Campsite + home Camper
dashboards updated live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>