canbus: reverse-engineer OneControl IDS-CAN bus (read fully mapped, write auth-gated)

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>
This commit is contained in:
wes
2026-06-11 23:20:49 -04:00
co-authored by Claude Fable 5
parent 34155fd7f9
commit b97401fec8
11 changed files with 43218 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
# OneControl via CANbus (IDS-CAN)
Direct **CANbus** integration for the Lippert OneControl (UNITY **X180T**)
system — the successor to the BLE-gateway approach in this repo's `src/` +
`custom_components/`. The BLE path works but is laggy and brittle (connection-
based GATT, ~30 s idle timeout, per-reconnect TEA auth, single shared Pi radio,
fragile SMP pairing). The OneControl panel is just a gateway bolted onto a CAN
backbone; tapping the bus gives **no bond/auth/timeout, instant latency, and
visibility into everything on the network** (incl. signals the BLE protocol never
exposed, like the DSI fault).
**Status:** first sniff done 2026-06-11 — **the bus is NOT RV-C.** It runs
Lippert's proprietary **IDS-CAN**: 250 kbit/s, but **11-bit standard IDs**
(plus a handful of 29-bit frames for telemetry/sync). The protocol structure
and device map below are from live captures in `sniff/*.log`.
---
## IDS-CAN findings (2026-06-11, captures: `sniff/baseline-*.log`, `sniff/toggletest-*.log`)
### Frame structure
11-bit ID = **`(page << 8) | node_addr`**. Every node broadcasts its pages at
**1 Hz** (plus immediate rebroadcast on change). Pages seen:
| Page | Content |
|------|---------|
| 0 | Node status: `b0` flags (bit2 = "state changing" transient), b1.. static (`14 00 00 00 1C 38 DF` common) |
| 1 | All-zero (4 bytes) for ordinary nodes |
| 2 | Identity: `00 A3 FE <type> 00 <b5> <b6> <b7>`**`<type>` = device class** |
| 3 | **The live value** — layout depends on device class (see below) |
| 6 | Single byte, only on special nodes `01`/`FC`/`FE` |
| 7 | Only `7FE`: byte3 = 1 Hz incrementing counter (uptime/heartbeat) |
### Device classes (page-2 `type` byte)
- **`0x0A` = tank.** Page 3 = **1 byte, level in percent** (0x42=66%, 0x21=33%).
- **`0x1E` = switched load** (lights/pump/heater). Page 3 = 6 bytes:
`b0` bit0 = **ON/OFF**, `b2..b3` (BE) = live **current/level reading** that
soft-ramps on switch-on and decays on switch-off (interior lights ramped
0x0001→0x028A over ~1 s).
- **`0x21` = H-bridge/movement** (slide/awning/jacks). Page 3 = 6 bytes:
`b0` = `0xC0` idle, **`0xC2` = extending (out), `0xC3` = retracting (in)**
(confirmed twice: wall-jog order + app commands 2026-06-11); `b2..b3` (BE) =
**live motor current** (~0x5000x620 while running, settles to 0 at stop).
- **`0x27`, `0x2B`** = unknown (nodes `AE`, `FC`).
### Node map (this rig — Catalina 263BHSCK, panel 28475)
| Node | Device | Evidence |
|------|--------|----------|
| `01` | controller (X180T?) | special pages; `301` status bit flickers at idle |
| `27` | **grey tank 1** | type 0x0A, page3 = 0x21 = 33% ✓ |
| `7D` | **grey tank 2** | type 0x0A; stayed 66% when black was drained |
| `FE` | **black tank** | type 0x0A; 66%→33% on drain (2026-06-11) ✓; also owns the 7FE counter |
| `E2` | **fresh tank** | type 0x0A, page3 = 0x00 = 0% ✓ |
| `2A` | **exterior lights** | type 0x1E; toggle test t≈6976 s |
| `F8` | **interior lights** | type 0x1E; toggle test t≈5161 s |
| `95` | **water heater** | type 0x1E; toggle test t≈8594 s |
| `61` | **water pump** | type 0x1E; toggle test 2026-06-11 (on 13.5s / off 23.8s) ✓ |
| `89` | unknown switched load | type 0x1E, never toggled (furnace? DSI?) |
| `75` | **awning** | type 0x21; jog test 2026-06-11 — b0 C0→C3 (in?) →C0→C2 (out?) with motor current on b2-3 |
| `6A`, `7F`, `9C` | slide / jacks / movement class | type 0x21, untested |
| `AE` | unknown (type 0x27, page3=0x00) | LP gas sensor? |
| `FC` | special node (type 0x2B, page 6) | panel/BLE gateway? |
### 29-bit extended frames (directed messages)
Extended ID = **`(src_node << 18) | flags? | (dest_node << 8) | page`**
(verified: pump event `0185FC42` = src `61` → dest `FC`; awning `01D5FC42` =
src `75` → dest `FC`; replies `03F0<node>43` = src `FC` → dest node, page 43).
- `01F5FC11` (src `7D``FC`) / `02B90111` (src `AE``01`) — periodic,
payload `00 2B 0D 4x <rolling>`: `b2..b3` ≈ 0x0D4647 → /256 = **13.27 V ⇒
battery voltage**, last byte looks like a checksum. (BLE read 13.09 V the
same day; charger float plausible.) Note the *source* being `7D`/`AE`
suggests those modules carry the battery-sense wire, not the controller.
- On every state change: a burst of `xxxxFC02` IDs (every node → dest `FC`)
flip a `55``AA` marker (state-change announce/sync flood), plus a per-event
handshake pair src-node→`FC` page 42 / `FC`→node page 43 with
random-looking bytes — not needed for sensing.
### Command path (DECODED 2026-06-11 — `sniff/app-commands-*.log`)
The command opcode is a **zero-payload (DLC 0) extended frame** `0x0006<node><op>`
(`op`: `01`=on, `00`=off/stop, `02`=movement-retract). The BLE app's taps appear
on the bus as these, ~300 ms before the page-3 state flips. BUT —
**⚠️ WRITE IS AUTH-GATED. Replay does NOT work.** Each command is wrapped in a
**rolling challenge-response** the bare opcode won't pass:
```
01 → node page42 "00 04" # controller: "arm me a challenge"
node → 01 page42 "00 04 <CC CC CC CC>" # module: random 4-byte challenge
01 → node page43 "00 04 <RR RR RR RR>" # controller: correct response
node → 01 page43 "00 04" # module: ack
01 → node 0x0006<node><op> ×3 # the actual command (now honored)
01 → node page45 / node → 01 page45 # post-status (00, then 0E)
```
The challenge is **fresh every time** (interior lights: `F7 74 0A 20` then
`ED C9 28 1A` on two presses → different responses), so captured frames can't
be replayed. **Verified empirically:** spoofing bare `cansend can0 00062A00#`
×3 (ext lights, no handshake) — frames hit the bus (TX confirmed, self-echo
seen) but the load **did not actuate**. The module ignores an unauthenticated
opcode.
It is **not** the BLE TEA cipher (`tea(612643285, 0x21CA0C06) = 0x87AC5CBD ≠`
the observed `0xCC18366B`) — different key/algorithm. So Lippert put a second,
separate auth on the CAN write path. Cracking it = its own reversing project.
**Dataset for the crack: `sniff/2A-auth-pairs.txt`** — 42 challenge→response
pairs from node `2A`, captured 2026-06-11 (app on/off ×~20). Analysis so far:
`response = f(challenge)` is **fully deterministic** (0 inconsistent responses
across the set ⇒ no counter/timestamp/session state — pure 32→32-bit block
transform), but **not** a constant XOR or ADD (42/42 distinct ⇒ a real cipher,
likely TEA/XTEA-family w/ unknown constants). Stateless+deterministic = solvable
offline: lift the constants from the X180T or Lippert-app firmware and verify
against this file. Collect more pairs (other nodes) anytime to widen the attack.
> Movement nodes (awning `75`) showed the page42/43/45 frames as **commander→node
> only, with no nonce reply** — possibly a weaker/no gate. NOT spoof-tested
> (don't actuate a motor unattended). Worth a careful look later.
**Bottom line: READ is fully open and is the deliverable here** (all sensors +
states from broadcasts, zero auth). WRITE stays on the BLE integration for now
(laggy but works) until/unless the CAN challenge-response is cracked.
Other app-session traffic (not control): `701` = controller heartbeat during a
BLE session; src 01 → node pages `30/31` = paged descriptor/table reads the app
uses to build its UI.
**Open read-side items:** identify node `89` (last untoggled 0x1E load) and
`6A`/`7F`/`9C` (movement — slide?), find battery SoC / the "4 green lights"
source.
---
## Hardware (BOM)
| Item | Notes |
|------|-------|
| **CANable 2.0** USB-CAN | RE/sniffing from xarl. candleLight/gs_usb fw → native socketcan (`can0`). |
| **Waveshare SN65HVD230** transceiver | 3.3 V, **onboard 120 Ω terminator** → use as the bus-END node. |
| **ESP32** devboard (`esp32dev` WROOM) | Native TWAI/CAN peripheral; ESPHome `esp32_can`. Spare from the gazebo build. |
| **Molex Mini-Fit Jr.** 2-pin pigtail (female) | Mates the panel's spare CAN **data** port. ~$20 assortment pack, not the $30 Lippert #331111. |
## System facts (from `lippert_control_panel_specs.pdf`, doc CCD-0004084, + web)
- **Controller:** UNITY **X180T**. Lippert brands it "RV-C" but the bus
actually runs **IDS-CAN** (proprietary): 250 kbit/s, 11-bit IDs — see
findings above.
- **Topology:** daisy-chain; each module has **two 2-pin CAN data ports**.
**CAN data = 2-wire pair (CAN-H/CAN-L)** on a **Molex Mini-Fit Jr.** (4.2 mm)
connector; Lippert's data pair is **red/black**. Power is a SEPARATE 2-pin
harness. Bus terminated at **both ends** by a 2-pin terminator plug (120 Ω H↔L).
- **Tank senders wire directly into the X180T** (DSI/FRESH/BLACK/GRAY/GRAY2
terminal block) → controller reads resistive senders and broadcasts levels on
CAN from its own source address (not separate tank modules).
## Physical tap (4 screws, fully reversible)
1. Pull the monitor panel (4 screws).
2. Find the **data** port — the one with the **terminating resistor** plugged in
(and/or where the controller's data harness lands). **NOT** the look-alike
2-pin **power** connector.
- ⚠️ **Multimeter check first:** data idles **~2.5 V** (recessive) across the
pins; **power reads ~12 V**. 12 V into CAN-H/L kills the SN65HVD230/CANable.
3. Unplug the **terminator**, plug your Mini-Fit Jr. pigtail into that port.
4. Land the two wires on the transceiver's CAN-H / CAN-L. The transceiver's
onboard 120 Ω re-terminates that end (keeps exactly 2 terminators: controller
+ your node). **Never** add a terminated node in the *middle* of the bus.
5. Revert = unplug, re-seat the terminator.
CAN-H vs CAN-L: can't hurt anything if swapped — bus just goes silent, flip the
two wires (or pop the Mini-Fit Jr. terminals and reorder).
---
## Sniffing workflow (do this first, before the ESP32 build)
On xarl with the CANable (see `sniff/log-can.sh`):
```sh
sudo pacman -S can-utils
sudo ip link set can0 up type can bitrate 250000
candump can0 # any traffic at 250k ⇒ confirmed RV-C
candump -ta -x can0 | tee sniff/$(date +%F)-idle.log # timestamped raw log
cansniffer -c can0 # color diff view — toggle a load, watch which bytes move
```
**Mapping method (same as the BLE RE, but easier — broadcast, no auth):**
flip ONE physical load (or watch ONE tank), see which **node + page + byte**
changes, record it in the node map above. Repeat per device.
Decode each 11-bit ID as:
```
page = (id >> 8) & 0x7 # message page
node = id & 0xFF # node address (which module)
```
> Note: the CANable 2.0 shipped with **slcan** firmware (enumerates as
> `ttyACM0`, not gs_usb). Bridge it: `sudo slcand -o -s5 /dev/ttyACM0 can0`
> (`-s5` = 250k) then `sudo ip link set can0 up`. `log-can.sh`'s plain
> `ip link ... type can bitrate` path only applies after a candleLight reflash.
### Known device inventory (from the BLE RE — what to hunt for on the bus)
| BLE DevID | Component | Expect on CAN |
|-----------|-----------|---------------|
| 4 | water pump | DC_DIMMER/switch instance |
| 5 | gas water heater | DC_DIMMER/switch instance |
| 6 | exterior lights | DC_DIMMER instance |
| 7 | interior lights | DC_DIMMER instance |
| 8 | grey tank 2 | TANK_STATUS instance |
| 9 | grey tank 1 | TANK_STATUS instance |
| 10 | black tank | TANK_STATUS instance |
| 11 | fresh water tank | TANK_STATUS instance |
| 2,3 | slide / awning | DC_MOTOR / window-shade DGN |
| — | battery voltage | DC_SOURCE_STATUS_1 |
> The BLE DevID numbering does **not** transfer to RV-C instance numbers — the
> table is just the checklist of loads to identify by sniffing.
---
## ESPHome node
`esphome/onecontrol-canbus.yaml` — ESP32 `esp32_can` listener (catch-all
`on_frame` → DGN dispatcher → template sensors/switches). Mirrors the
`gazebo-fan-proxy` pattern: USB flash once, OTA after; native HA entities on the
campsite Pi over the ESPHome API. While RE'ing, it logs every decoded frame at
`DEBUG` so the ESP can double as a sniffer. Fill in instances/byte-math in the
lambda as the DGN map firms up; wire the command DGN into the `switch` actions
last.
## References
- RV-C spec & DGN tables: <https://www.rv-c.com/>
- CoachProxy / coachproxyos (open RV-C decode prior art)
- `rvc2mqtt`, `rvc-monitor` (DGN→MQTT mappings to crib)
- Lippert OneControl (RV-C): <https://www.lippert.com/brands/onecontrol>
- BLE-side protocol (this repo): `../docs/PROTOCOL_FINDINGS.md`
+3
View File
@@ -0,0 +1,3 @@
# ESPHome build cache + real secrets (keep secrets.yaml.example tracked)
/.esphome/
/secrets.yaml
+175
View File
@@ -0,0 +1,175 @@
# OneControl RV-C CANbus node
#
# ESP32 (native TWAI/CAN) + external SN65HVD230 transceiver, tapped into the
# Lippert UNITY X180T RV-C bus at the monitor panel's spare CAN *data* port.
# Listens to RV-C broadcasts and republishes them as native HA entities; the
# command/write path (switches) is stubbed until the DGN map is RE'd.
#
# RV-C = 250 kbit/s, 29-bit extended IDs. See ../README.md for the tap procedure,
# the (unverified) DGN map, and the sniffing workflow. Flash over USB first
# (`esphome run onecontrol-canbus.yaml`), OTA thereafter.
substitutions:
name: onecontrol-canbus
friendly_name: OneControl CANbus
# ESP32 GPIOs to the transceiver. Any free non-strapping pins work; these match
# a common SN65HVD230 wiring. tx_pin -> transceiver D/CTX, rx_pin <- R/CRX.
tx_pin: GPIO5
rx_pin: GPIO4
esphome:
name: ${name}
friendly_name: ${friendly_name}
esp32:
board: esp32dev # classic WROOM-32
framework:
type: esp-idf
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "OneControl-CAN Fallback"
password: !secret fallback_ap_password
captive_portal:
logger:
level: DEBUG # DEBUG so the on_frame ESP_LOGD frame dump is visible
# while RE'ing. Drop to INFO once the map is solid.
api:
encryption:
key: !secret api_key
ota:
- platform: esphome
# ---------------------------------------------------------------------------
# CAN bus: ESP32 native TWAI controller + SN65HVD230 transceiver
# ---------------------------------------------------------------------------
canbus:
- platform: esp32_can
id: rvc_bus
tx_pin: ${tx_pin}
rx_pin: ${rx_pin}
bit_rate: 250kbps # RV-C is always 250k
can_id: 0 # our own TX id (only matters when we send)
use_extended_id: true # RV-C uses 29-bit IDs
on_frame:
# Catch-all: can_id_mask 0 = accept every frame, then dispatch by DGN in
# the lambda (RV-C buries the source address in the id's low byte, so a
# single decoder is cleaner than per-DGN hardware filters).
- can_id: 0
can_id_mask: 0
use_extended_id: true
then:
- lambda: |-
// `can_id` and `x` (data bytes) are provided by the trigger.
// (If your ESPHome is too old to expose `can_id` here, switch to
// per-DGN can_id/can_id_mask filters instead.)
uint32_t id = can_id;
uint8_t prio = (id >> 26) & 0x7;
uint32_t dgn = (id >> 8) & 0x1FFFF;
uint8_t sa = id & 0xFF;
// Frame dump — comment out once the map is trustworthy.
ESP_LOGD("rvc", "DGN=%05X SA=%02X len=%u %02X %02X %02X %02X %02X %02X %02X %02X",
dgn, sa, x.size(),
x.size()>0?x[0]:0, x.size()>1?x[1]:0, x.size()>2?x[2]:0, x.size()>3?x[3]:0,
x.size()>4?x[4]:0, x.size()>5?x[5]:0, x.size()>6?x[6]:0, x.size()>7?x[7]:0);
// ================= DISPATCH — ALL VALUES UNVERIFIED =================
// DGNs + byte math are standard-RV-C hypotheses. Confirm each
// against the sniff log (../README.md "DGN map") before trusting.
// ---- TANK_STATUS (std 0x1FFB7) ----
if (dgn == 0x1FFB7 && x.size() >= 2) {
uint8_t inst = x[0];
float pct = x[1] * 100.0f / 255.0f; // TODO verify level math
switch (inst) { // TODO verify instances
case 0: id(fresh_tank).publish_state(pct); break;
case 1: id(black_tank).publish_state(pct); break;
case 2: id(grey_tank_1).publish_state(pct); break;
case 3: id(grey_tank_2).publish_state(pct); break;
}
}
// ---- DC_SOURCE_STATUS_1 (std 0x1FFFD) battery ----
if (dgn == 0x1FFFD && x.size() >= 4) {
uint16_t raw = x[2] | (x[3] << 8); // 0.05 V/bit, LE (verify)
id(battery_voltage).publish_state(raw * 0.05f);
}
// ---- DC_DIMMER_STATUS_3 (std 0x1FEDA) light/switch state ----
if (dgn == 0x1FEDA && x.size() >= 3) {
uint8_t inst = x[0];
bool on = x[2] > 0; // byte2 = brightness
switch (inst) { // TODO verify instances
// case ?: id(interior_lights).publish_state(on); break;
// case ?: id(exterior_lights).publish_state(on); break;
// case ?: id(water_pump).publish_state(on); break;
// case ?: id(gas_water_heater).publish_state(on); break;
}
}
# ---------------------------------------------------------------------------
# Read-back sensors (published by the dispatcher above)
# ---------------------------------------------------------------------------
sensor:
- platform: template
name: "Battery Voltage"
id: battery_voltage
unit_of_measurement: "V"
device_class: voltage
state_class: measurement
accuracy_decimals: 2
- platform: template
name: "Fresh Water Tank"
id: fresh_tank
unit_of_measurement: "%"
accuracy_decimals: 0
- platform: template
name: "Black Tank"
id: black_tank
unit_of_measurement: "%"
accuracy_decimals: 0
- platform: template
name: "Grey Tank 1"
id: grey_tank_1
unit_of_measurement: "%"
accuracy_decimals: 0
- platform: template
name: "Grey Tank 2"
id: grey_tank_2
unit_of_measurement: "%"
accuracy_decimals: 0
# ---------------------------------------------------------------------------
# Switches — state comes from DC_DIMMER_STATUS_3 above; the command path is a
# PLACEHOLDER until DC_DIMMER_COMMAND_2 (std 0x1FEDB) instance + payload are RE'd.
# can_id below = prio(6)<<26 | DGN 0x1FEDB <<8 | source_addr. 0x18FEDB80 is a
# guess (prio 6, SA 0x80) — DO NOT trust until verified by sniffing a real
# command frame from the panel.
# ---------------------------------------------------------------------------
switch:
- platform: template
name: "Interior Lights"
id: interior_lights
optimistic: true # flip to false once read-back is wired
turn_on_action:
- canbus.send:
canbus_id: rvc_bus
use_extended_id: true
can_id: 0x18FEDB80 # TODO compute from real DGN+SA
data: [0x00, 0x00, 0xC8, 0x01] # TODO [instance, group, level, cmd]
turn_off_action:
- canbus.send:
canbus_id: rvc_bus
use_extended_id: true
can_id: 0x18FEDB80 # TODO
data: [0x00, 0x00, 0x00, 0x03] # TODO
# Duplicate the block above for: exterior_lights, water_pump, gas_water_heater
# once their command instances are known.
+7
View File
@@ -0,0 +1,7 @@
# Copy to secrets.yaml (gitignored) and fill in. Mirrors the gazebo-fan-proxy
# secrets so the same campsite WiFi + an ESPHome API key are used.
wifi_ssid: "OmnissiahsReach"
wifi_password: "REDACTED"
fallback_ap_password: "REDACTED"
# 32-byte base64 ESPHome API key (generate: `openssl rand -base64 32`)
api_key: "REDACTED"
+45
View File
@@ -0,0 +1,45 @@
# Lippert IDS-CAN command-auth challenge/response pairs
# node 2A (exterior lights), captured 2026-06-11, app-driven on/off x~20
# format: <challenge_hex> <response_hex> (both 4 bytes, from page42/page43 payload bytes 3-6)
9577DE9B C033B197
1ADF97F8 F48604B6
E09D5B01 44A5C15E
DEA757A0 04DD9CBA
8862C15F 29AA626F
E14DB9AE 25373B94
23A3ED5D 3250A48A
F3A3B602 A6E58995
FC5F3883 47780DAF
16B2AE10 28FE5A23
11C34FD9 6A1B9415
2FFC637D 0A5CE05B
DB284B0C 052D2687
FA14432F 71EDD5F4
B140D087 356CFD45
20337B7E 81D3136A
968F0543 F91AB4A3
28DA207A 781A423E
AB4040DE 5B652F23
7A3CA469 48DF5FCF
AFDFBC25 2E52F019
B7394203 C62B24EE
ECA9CD68 361924A5
83198E6B 7E9DF4D9
4D3F6429 86793E68
0804DD8B 953137D5
E1525A39 1091BB17
C77CAA9B F9746AAC
B98C209C 46385C66
BBE34815 F717E80E
2DAA8E0C BB1CF451
D4AAC54D FD806835
E1ECB36C FE26F014
BF066519 4F23CAAF
DA8EF86C ED575807
0FD2C9B9 6B0EF761
4B192BC2 0DC60551
A0421A64 54A5AD6D
2BE06274 BD49B339
8FF0B6E5 C18D49F2
45FE7D5E 0A44BB97
BAA07C27 54E5C64B
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Bring up the CANable at RV-C speed and log timestamped raw frames.
#
# Usage:
# ./log-can.sh # bring up can0 @ 250k, live candump
# ./log-can.sh rec NAME # also tee a timestamped log to NAME-<date>.log
# ./log-can.sh sniff # cansniffer color-diff view (toggle a load, watch bytes)
# ./log-can.sh down # take the interface down
#
# Requires: can-utils (pacman -S can-utils). CANable 2.0 with candleLight/gs_usb
# firmware enumerates as a native socketcan device (default can0).
set -euo pipefail
IFACE="${IFACE:-can0}"
BITRATE=250000 # RV-C is always 250k
CMD="${1:-up}"
up() {
if ! ip link show "$IFACE" &>/dev/null; then
echo "No $IFACE — is the CANable plugged in? (dmesg | grep -i gs_usb)" >&2
exit 1
fi
sudo ip link set "$IFACE" down 2>/dev/null || true
sudo ip link set "$IFACE" up type can bitrate "$BITRATE"
echo "$IFACE up @ ${BITRATE} bps"
}
case "$CMD" in
up) up; exec candump -ta -x "$IFACE" ;;
rec) up
name="${2:-onecontrol}"; out="$(dirname "$0")/${name}-$(date +%F_%H%M%S).log"
echo "logging -> $out (Ctrl-C to stop)"
exec candump -ta -x "$IFACE" | tee "$out" ;;
sniff) up; exec cansniffer -c "$IFACE" ;;
down) sudo ip link set "$IFACE" down; echo "$IFACE down" ;;
*) echo "usage: $0 {up|rec NAME|sniff|down}" >&2; exit 2 ;;
esac
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff