Skip to content

IEEE register generated

Pairing modes#

A hub advertises the modes it currently offers as a bitmask in the WELCOME trust sub-map. The trust ledger records the single bit a paired device used.

All three modes end in the same PAIR_GRANT {token, role}. The role is an attribute of the grant, never of the ceremony. A hub has zero or one PIN, never one secret per tier.

Mask Bit Name Notes
0x01 bit 0 knock_approve PRIMARY and capability-agnostic: bare PAIR_REQ with no proof -> bounded pending list (pairing_pending_max) exposed as protocol state (0x000A/0x000B) -> ANY configure session approves {instance_id, role}. The joiner needs one button and no display; the trusted surface is any configure client, which kills the circular 'the WebUI is trusted because it's the WebUI' dependency. RECOMMENDED partly because its approval surface shows the knocker's identity on hardware the attacker does not control.
0x02 bit 1 pin_proof the HMAC-PIN flow, for keyboard-bearing joiners when no configure session exists. HONESTY CLAUSE (normative): 4 digits = 10^4 offline HMACs, so a passive observer of the exchange can brute-force it. Acceptable for the v1 threat model (casual/drive-by prevention) and MUST be stated plainly. SPAKE2 is the reserved v2 upgrade: not v1, because WebCrypto has no PAKE and mandating it would exile the browser client.
0x04 bit 2 push_to_pair PHYSICAL-PRESENCE proof opens a short SINGLE-GRANT window. The spec requires the PROOF, not a GPIO; minimum hardware is NONE, because the power cord is the button: factory-fresh (zero configure tokens) boots claimable and the first knock gets configure (possession is root); later, N=3 consecutive boots with uptime <10 s opens the window (NVS counter only; cannot collide with a session, since any power loss already stops motion and forces re-home). A hub with a real button MAY bind it: a UX upgrade, never required. FACTORY RESET MUST BE A HARDER GESTURE than opening pairing.

Administration operations#

These are the value map key 1 of the session-admin channel (0x0009). The channel requires configure.

The trusted surface is a tier, not an app. Any configure session reaches every operation here. Nothing in the protocol knows or cares whether that session is the machine's own web page, a phone, or a command line.

A configure session may grant up to its own tier, configure included. The paired-device roster is the audit trail, rather than a ceiling that would leave the first administrator unable to appoint a second.

Op Name Meaning
1 evict RFC-018: GOODBYE the session named by session_id with SESSION_EVICTED. Runs the full §6.8/RFC-005 teardown: the evicted session's source ownership is released under its §11.3 loss policy exactly as if it had crashed, because 'no unmonitored path to motion' does not get an exception for admin actions. Evicting your OWN session is legal and is just a rude GOODBYE to yourself.
2 pair_approve RFC-027(a): approve the pending knock named by instance_id at role, issuing PAIR_GRANT {token, role} to the knocker. ALSO the RFC-029 item-2 RE-APPROVAL verb: applied to a device in recognized_pending it re-records the observed version and restores the suspended role: approving a knock and re-trusting a changed device are the same decision ('this identity may do this'), so they are the same op rather than two that could drift.
3 pair_deny RFC-027(a): drop the pending knock named by instance_id without issuing a token; emits pairing_events denied. On a recognized_pending device this is a REVOKE (op 4) in effect: deny means 'no', and leaving a suspended entry in the ledger after an operator said no would be a lie the roster tells forever.
4 revoke RFC-027(4)/029: delete instance_id from the trust ledger. Revocation is PROTOCOL, not a WebUI feature: that is the entire point of putting it here. Takes effect at the next HELLO (an already-live session keeps the role it was admitted with until it reconnects or is evicted; use evict to end it now). Emits pairing_events revoked.

Trust ledger states#

This is the state field of a paired-devices item. A revoked device has no entry at all, so revocation is an absence and never a third state.

Value State Meaning
0 trusted paired, and the version observed at the last HELLO matches the version recorded when the role was approved. The granted role applies in full.
1 recognized_pending RFC-029 item 2's tripwire fired: this device presented a DIFFERENT client_ver than the ledger recorded. The session is admitted at watch, the granted role is SUSPENDED (not revoked), and a re-approval is surfaced to configure sessions via 0x000A/0x000B. HONESTY CLAUSE, normative: the version is SELF-REPORTED, so this catches an honest update and nothing else: a deliberately malicious update lies about its version and keeps its token. The real bounds on a hostile client are role scoping, instant revocation, roster visibility and the role-exempt safety ops. Do not let a UI imply this is attestation.

Token presentation modes#

This is the trust sub-map's presentation_mode. The ledger records it per device, so an operator can see the security posture.

bearer is the floor and the default. proof is recommended for any client that already has SHA-256, and is never required of anyone.

Value Mode Meaning
0 bearer raw 16-byte token in HELLO. LEGAL, DEFAULT, and the potato floor: a coin-cell client does exactly this and nothing more. v1 transports are cleartext, so a passive LAN observer who captures one HELLO owns the credential until it is revoked; §12.1 excludes that attacker, and this mode accepts that ceiling knowingly.
1 proof HMAC-SHA256(key = token, message = the WELCOME nonce) truncated to 16 B, presented in an AUTH (0x1C) frame after WELCOME. The token itself NEVER crosses the wire, so a sniffer captures a one-time proof and not the credential. Costs exactly one extra round trip per connect (HELLO -> WELCOME -> AUTH -> GRANT); the client is at watch in between, which is the correct posture for a client that has not yet proved anything. The 'reuse the previous session's nonce to skip the round trip' shortcut was DROPPED as replay-unsafe: see trust_keys.token_proof.

A reported client version is a tripwire, not an attestation. It catches an honest update. A deliberately malicious one reports whatever version it likes and keeps its token. What bounds a hostile client is role scoping, immediate revocation, its visibility in the roster, and the fact that safety operations are role-exempt for everyone.

Accessories#

Accessories join a hub over the ESP-NOW spoke (SPEC §13.3.1, §13.3.2) and their channels live in the user space (SPEC §8.10).

Join results#

The result byte of JOIN_REPLY (0x22).

Value Name Notes
0 accepted joined (or rejoined); the accessory waits in its safe state for a command
1 window_closed unknown accessory and no association window open
2 capacity no free slice, no peer entry, or the declaration exceeds the host's advertised capacity
3 unsupported proto_ver not servable
4 declaration_invalid the declaration failed §8.10 validation; nothing stored
5 not_paired a rejoin from an accessory this host has forgotten; the accessory MAY clear its stored hub

Accessory admin ops#

The action.accessory op select on core channel 0x0012. Index 0 is op-select filler.

Value Name Notes
1 window_open open the §12.3 association window for an accessory join: the in-band twin of the pairing button. One accepted join closes it.
2 forget GOODBYE the accessory, delete its record and every relationship targeting it, retire its slice, remove its channels. Destructive (§8.8 destructive_options on the reference op select).
3 rename set the hub-authored name of the accessory named by accessory_id

Accessory record keys#

The registered item grammar of the accessories store (0x0010).

Value Name Notes
1 accessory_id bstr 8: the accessory's durable identity (§8.10), the record's primary key
2 slice uint: slice index k; the accessory's channels are 0x8000 + 0x20*k + r
3 name tstr: the hub-authored name (renamed via accessory-admin rename)
4 product tstr: the JOIN_REQ product string (<= 16 B)
5 fw_version tstr: the JOIN_REQ fw_version string (<= 16 B)
6 declaration_etag bstr 8: etag of the stored declaration (§8.3)

Accessory status#

Relative id 0x01 of every slice: state u8 (accessory_states) + fault u8 (device-defined, 0 none) + beacon_seq u16 (header seq of the last accepted BEACON) = 4 B; pushed on change and at least every spoke_beacon_interval_ms.

Accessory states#

The accessory-status state byte (SPEC §17.1.1).

Value Name Notes
0 live commanded and running normally
1 safe_joined joined, holding safe values, awaiting its first command
2 safe_deadman safe: no matching BEACON for its deadman window (§13.3.1)
3 safe_goodbye safe: a GOODBYE arrived from its hub
4 safe_estop safe: an ESTOP frame or a BEACON with estop_latched; the host's acknowledgment that the e-stop reached it
5 safe_fault safe: a local fault (see fault)

Relationship maps#

A relationship item's map (SPEC §8.11). Every output is clamped into the target's declared range.

Value Name Notes
1 linear_clamp out = L(in) = out_min + (clamp(in, in_min, in_max) - in_min) * (out_max - out_min) / (in_max - in_min), in_min != in_max
2 invert out = out_max + out_min - L(in): in_min maps to out_max
3 threshold_hysteresis params on_above, off_below (source units, off_below <= on_above): out_max once the source rises to on_above, out_min once it falls to off_below, unchanged between; starts at out_min when armed
4 slew_limit L(in) with its rate of change bounded by params rise_per_s, fall_per_s (target units/s; one value serves both when the second is absent)
5 lowpass L(in) through a first-order low-pass, param tau_s (seconds)
6 gate out = L(in) while in_min <= in <= in_max, else the target's safe value
7 piecewise_table params: up to 8 (in, out) points flattened, in strictly ascending; linear between, held at the end values outside; the four bounds unused

Relationship keys#

The registered item grammar of the relationships store (0x0013).

Value Name Notes
1 rel_id uint: 0..relationships_max-1; bit index in the roster masks
2 name tstr: client-authored label
3 source_channel uint: absolute id of an h2c STATE or STREAM channel
4 source_field uint: layout index of a numeric field on source_channel
5 target_channel uint: absolute id of an accessory INTENT or c2h STREAM channel
6 target_field uint: schema key (INTENT) or layout index (STREAM) of a value-bearing field
7 map uint: a relationship_maps value
8 in_min float: source physical units
9 in_max float: source physical units
10 out_min float: target physical units
11 out_max float: target physical units
12 params array of up to 16 floats, in the order the map lists them
13 enabled bool: persisted. armed is volatile, never stored, false at boot (§11.6)