canbus: hard safety gate — slides/jacks are control-panel only, never over CAN
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>
This commit is contained in:
@@ -22,10 +22,12 @@ substitutions:
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: ${friendly_name}
|
||||
# Command authentication (REMOTE_CONTROL session, key 0xB16B00B5). Provides
|
||||
# ids_can_auth::remote_control_response_bytes() to the command path below.
|
||||
# Command authentication (REMOTE_CONTROL session, key 0xB16B00B5) provides
|
||||
# ids_can_auth::remote_control_response_bytes(); command_guard.h provides
|
||||
# onecontrol::command_blocked() — the slides/jacks-are-panel-only safety rule.
|
||||
includes:
|
||||
- ids_can_auth.h
|
||||
- command_guard.h
|
||||
|
||||
esp32:
|
||||
board: esp32dev # classic WROOM-32
|
||||
@@ -88,6 +90,19 @@ canbus:
|
||||
if (id(g_cmd_pending) && x.size() >= 6) {
|
||||
uint32_t chal_id = ((uint32_t) id(g_cmd_node) << 18) | 0x10000u | 0x0142u;
|
||||
if (id == chal_id) {
|
||||
// HARD SAFETY GATE (authoritative). This is the only place a
|
||||
// command is ever transmitted, so the slides/jacks-are-panel-
|
||||
// only rule is enforced here, right before TX, regardless of how
|
||||
// the command was queued. If blocked: send nothing — no
|
||||
// response, no opcode — and clear the pending command.
|
||||
uint8_t n = id(g_cmd_node);
|
||||
if (onecontrol::command_blocked(n, id(g_node_type)[n])) {
|
||||
ESP_LOGE("idscan", "SAFETY: refusing command to motor node %02X "
|
||||
"(slides/jacks are control-panel only)", n);
|
||||
id(g_cmd_pending) = false;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t resp[4];
|
||||
ids_can_auth::remote_control_response_bytes(&x[2], resp);
|
||||
|
||||
@@ -113,6 +128,13 @@ canbus:
|
||||
uint8_t page = (id >> 8) & 0xFF;
|
||||
uint8_t node = id & 0xFF;
|
||||
|
||||
// Track each node's device class from its page-2 identity broadcast
|
||||
// (x[3] = type byte). The safety gate uses this to refuse any
|
||||
// motor-class (0x21) node that isn't the awning.
|
||||
if (page == 2 && x.size() >= 4) {
|
||||
id(g_node_type)[node] = x[3];
|
||||
}
|
||||
|
||||
// Frame dump — comment out once the map is trustworthy.
|
||||
ESP_LOGD("idscan", "page=%u node=%02X len=%u %02X %02X %02X %02X %02X %02X",
|
||||
page, node, x.size(),
|
||||
@@ -169,6 +191,11 @@ globals:
|
||||
- id: g_cmd_pending
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
# Device class per node, learned from page-2 identity broadcasts (0 = not yet
|
||||
# seen). Feeds the motor-output safety gate in command_guard.h.
|
||||
- id: g_node_type
|
||||
type: 'std::array<uint8_t, 256>'
|
||||
initial_value: '{}'
|
||||
|
||||
script:
|
||||
- id: send_load_command
|
||||
@@ -179,9 +206,20 @@ script:
|
||||
op: int
|
||||
then:
|
||||
- lambda: |-
|
||||
// SAFETY: lights only. Refuse any node that isn't an allowlisted
|
||||
// switched load (2A ext lights, F8 int lights). This keeps the command
|
||||
// path off movement nodes (slides/jacks/awning).
|
||||
// SAFETY, layer 1 — motor-output gate. Slides/jacks are control-panel
|
||||
// only; refuse any motor node that isn't the awning. Same rule the
|
||||
// transmit point enforces (command_guard.h), checked here too so a
|
||||
// blocked command never even starts the exchange.
|
||||
if (onecontrol::command_blocked((uint8_t) node, id(g_node_type)[(uint8_t) node])) {
|
||||
ESP_LOGE("idscan", "SAFETY: refusing command to motor node %02X "
|
||||
"(slides/jacks are control-panel only)", (uint8_t) node);
|
||||
id(g_cmd_pending) = false;
|
||||
return;
|
||||
}
|
||||
// SAFETY, layer 2 — current scope is lights only. Only the allowlisted
|
||||
// switched loads (2A ext lights, F8 int lights) may be commanded. To add
|
||||
// the awning later, add 0x75 here; the motor gate above already permits
|
||||
// it. Do NOT add slide/jack nodes — the gate would refuse them anyway.
|
||||
if (node != 0x2A && node != 0xF8) {
|
||||
ESP_LOGW("idscan", "refusing command to non-allowlisted node %02X (lights only)", node);
|
||||
id(g_cmd_pending) = false;
|
||||
@@ -266,8 +304,13 @@ binary_sensor:
|
||||
# handler completes the page-42/43 challenge/response and sends the opcode.
|
||||
#
|
||||
# SAFETY: lights only. Only the allowlisted switched-load nodes (2A = exterior
|
||||
# lights, F8 = interior lights) may be wired here. Do NOT add movement nodes
|
||||
# (slides/jacks/awning, type 0x21) until a careful attended first test.
|
||||
# lights, F8 = interior lights) may be wired here.
|
||||
#
|
||||
# SLIDES AND JACKS ARE CONTROL-PANEL ONLY — never over CAN. This is a hard policy
|
||||
# (command_guard.h): the command path refuses any motor-class (0x21) node other
|
||||
# than the awning, enforced both here and at the transmit point, so even adding a
|
||||
# slide/jack switch below cannot actuate it. The awning is technically permitted
|
||||
# by the gate but is intentionally left unwired (operate it from the panel too).
|
||||
# optimistic:true for now — the page-3 (b0 bit0) read-back above already publishes
|
||||
# true module state, so these can switch to optimistic:false once verified.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user