diff --git a/canbus/HANDOFF.md b/canbus/HANDOFF.md new file mode 100644 index 0000000..ef5b781 --- /dev/null +++ b/canbus/HANDOFF.md @@ -0,0 +1,103 @@ +# Handoff — finish & flash the OneControl IDS-CAN node + +**Goal:** complete and flash an ESP32 firmware that puts our RV's Lippert +OneControl system (tanks, lights, switches) into Home Assistant as native +entities, talking directly to the OneControl CAN network. This replaces the +slower Bluetooth integration in `../src/` + `../custom_components/`. + +Read `README.md` first — it has the full message-format documentation, the node +map for this coach, and the wiring procedure. This file is just the current +state + what's left. + +## Current status (2026-06-12) + +- **Message format documented and confirmed against live captures.** The bus is + Lippert **IDS-CAN** (250 kbit/s, 11-bit IDs), not RV-C. Every tank, light, + pump, heater, the awning, and battery voltage is decoded — see the node map in + `README.md`. +- **Read path:** modules broadcast their state continuously, no authentication. +- **Command path:** each command is preceded by a short challenge/response + authentication exchange (the same one the OEM app uses). Implemented in + `ids_can_auth.py` (reference) and `esphome/ids_can_auth.h` (the firmware copy, + verified **bit-exact** against the Python and against captured/live values). +- **Proven working end to end.** `idscan_cmd.py` ran the full exchange over a USB + CAN adapter and operated the interior lights (node `F8`) on/off/on, each + answering a distinct fresh challenge, with the module's own status broadcast + confirming the result. A bare command with no exchange is ignored. +- **Firmware compiled successfully** last night (`esphome/.esphome/build/…`). + +## Hardware (assembly-ready) + +- ESP32 WROOM devboard (`esp32dev`) + Waveshare **SN65HVD230** transceiver + (3.3 V logic, onboard 120 Ω terminator → this node is the bus-END node). +- **Power:** buck converter dialed to **5 V**, tapped from the panel's 12 V + supply (common ground with the bus — good). Feed the ESP32 5 V pin. +- **Connection:** Molex Mini-Fit Jr. pigtail into the panel's CAN **data** port + (the one with the terminator), per `README.md` → "Physical connection". + ⚠️ Meter the port first: data idles ~2.5 V, power reads ~12 V; 12 V on CAN-H/L + destroys the transceiver. +- **GPIO:** transceiver `CTX/D` ← ESP32 `GPIO5` (tx_pin), `CRX/R` → `GPIO4` + (rx_pin). Adjust `substitutions:` in the YAML if you wire differently. + +## Remaining work (in order) + +1. **`secrets.yaml`** — copy `esphome/secrets.yaml.example`, fill in WiFi, the + ESPHome `api_key`, and the fallback-AP password. It's git-ignored. +2. **First flash over USB:** `esphome run esphome/onecontrol-canbus.yaml` (pick + the serial port). OTA after that. Mirrors the gazebo-fan-proxy workflow. +3. **Confirm the read entities populate** in HA once it's on the bus: the four + tanks and the two light switches publish from the page-3 broadcasts. Watch the + DEBUG frame dump (`logger: level: DEBUG`) to confirm frames are decoded; drop + to INFO when happy. +4. **Finish the two open read items in the YAML lambda:** + - **Battery voltage** — rides a 29-bit telemetry frame (src `7D`/`AE`, + page `0x11`), bytes 2–3 big-endian / 256 = volts. Match that frame and + publish `battery_voltage`. (See README "29-bit extended frames".) + - Optionally add **water pump (`61`)** and **water heater (`95`)** — both are + ordinary switched loads, same decode + command path as the lights. +5. **Verify the command path** from HA: toggle Interior/Exterior Lights. The + `send_load_command` script + `on_frame` handler do the exchange and send the + opcode. The node allowlist is **lights only** — keep movement nodes + (awning/slides/jacks, type `0x21`) off the switch list until a careful + attended first test. +6. **DSI fault `binary_sensor`** — see the section below (pending a capture). +7. **(Optional) Surface at the campsite HA + bridge home** like the gazebo fans / + OneControl BLE devices, if you want these in the home dashboard too. + +## File map + +| File | What it is | +|------|-----------| +| `esphome/onecontrol-canbus.yaml` | the ESP32 firmware (read dispatch + command path) — the thing to finish & flash | +| `esphome/ids_can_auth.h` | command-authentication response, used by the YAML lambda | +| `esphome/secrets.yaml.example` | template for the git-ignored secrets | +| `ids_can_auth.py` | Python reference for the same authentication + 51/51 self-test | +| `idscan_cmd.py` | desktop tool that proved the command path over a USB CAN adapter | +| `captures/` | raw bus logs + the challenge/response pairs + `analyze_auth.py` | +| `captures/log-can.sh` | bring up the USB CAN adapter and log frames | +| `README.md` | full message-format documentation + node map + wiring | + +## Safety notes + +- **Command path is lights-only by allowlist.** Movement nodes use the same + authentication but are untested from this node — don't add them until you can + watch the motor on the first actuation. +- The physical connection is fully reversible: unplug, re-seat the terminator. +- One transceiver = one bus-end terminator. Never add a terminated node in the + middle of the bus (would make three terminators). + +## DSI fault — PENDING a fault capture + +> The water-heater DSI (gas ignition) fault is almost certainly on the bus, but +> every capture so far is of a healthy heater, so the exact byte is unknown. +> +> **Capture plan (this session):** close the propane valve, run the water heater +> on gas until it locks out (DSI fault light on the panel), capture ~20 s, and +> diff against a healthy baseline (`captures/baseline-2026-06-11_223823.log`). +> Prime suspects (both sit at a constant "all-clear" value today): +> - node `95` (heater) page-3 `b1` — always `0xFF`; expect a bit to drop on fault. +> - node `AE` (type 0x27, ?LP-gas/diagnostics) page-3 — always `0x00`; expect non-zero on fault. +> +> **TODO once decoded:** record the node/page/byte/bit here, then add a +> `binary_sensor` to the YAML (`device_class: problem`) that reads it — the DSI +> fault the Bluetooth app never exposed.