Transport bindings and the relay role#
13. Transport Bindings (normative)#
13.0 What conformance binds: duties, not topology (RFC-056)#
Conformance is defined over the wire and the duties, never over topology. A conformant hub MAY be implemented across multiple processors, cores or physical devices in any arrangement, provided the composite satisfies the golden vectors (§17.2), the §6.3 session lifecycle, the role and trust layers, and the §13.1 property declarations for whichever bindings it exposes. A client MUST NOT be able to tell the difference, and MUST NOT probe for it. An internal link between hub components is not a Valence binding and has no conformance duty of its own: it may be any transport at all, including a §13.5 serial link carrying Valence frames, and it is invisible to conformance. The two HTTP escapees of §1 item 8 are duties of the composite hub in the same sense (RFC-057).
13.1 The binding contract#
A binding implements four operations — open, close, write(frame), read → frame — and declares its properties. Valence above the binding line is transport-blind. The matrix every implementation codes against:
| Binding | max_frame (header-incl.) |
Payload MTU | Ordered | Reliable | Congestion signal | ESTOP preempt point | Worst-case added ESTOP delay* |
|---|---|---|---|---|---|---|---|
| WebSocket | 512 | 504 | yes | yes (TCP) | egress queue watermark | front of egress queue | in-flight TCP bytes |
| ESP-NOW | 250 | 242 | no | no | ACK-bitmask loss % | front of radio queue | one airtime slot (~1 ms) |
| BLE GATT | 244 (ATT_MTU 247 − 3) | 236 | notifications: yes | no (notify) / yes (write-rsp) | notify queue depth | front of notify queue | one connection interval |
| Serial (COBS) | 512 | 504 | yes | yes† | TX buffer watermark | byte-level injection | one frame length |
| In-process | configurable (default 250) | configurable | configurable | configurable | simulated | simulated | simulated |
* added by the binding, beyond queue-front admission — see §11.2's honesty clause H2. † USB CDC; raw UART is reliable in practice, CRC-carrying frames (ESTOP) self-protect, and the STATE/STREAM classes tolerate loss by design.
The ESP-NOW line is the normative floor: min_transport_payload = 242 comes directly from it, every mandatory control message and every STATE payload MUST fit it (§9.1), and anything relying on more is a per-binding luxury. A hub MAY advertise a smaller max_frame than its binding permits; it MUST NOT advertise a larger one.
Conformance profiles (RFC-043). Which bindings a hub MUST offer depends on what it is:
- Base profile (simulators, hosted hubs, relays, in-process test hubs): any single binding conforms. A hub with no radio at all — a desktop simulator talking only in-process, a hub behind an existing gateway — is a fully legitimate Valence citizen.
- Hardware hub profile (an embedded hub on radio-bearing silicon — every known target is ESP32-class WiFi+BLE): BLE GATT is SHOULD, and MUST where config mode is offered (§13.4.1; RFC-056, RFC-079). It is the infrastructure-free discovery and provisioning path and stays RECOMMENDED wherever the silicon has a radio going spare; a hardware hub that ships WiFi, UDP discovery (§13.8) and a provisioning path (§13.9 over serial, or any other) and no BLE is fully conformant. WebSocket is SHOULD, the preferred high-throughput path (dense streams, fat catalogs, multiple clients) and expected on all ESP32-class hardware. ESP-NOW is the supported ESP32-peer/remote binding: deliberately trivial to enable, not itself conformance-relevant.
- Serving a UI is a capability, never a conformance requirement. A hub with no web assets to serve is fully conformant, and a client MUST NOT assume the hub it is talking to serves one.
- Clients SHOULD auto-upgrade BLE→WS where the hub has BLE and both ends can: BLE is how a client finds and provisions a machine, WS is how it streams to one (§6.3's transport migration carries the session across the hop). More generally, where a hub exposes several bindings, a client SHOULD prefer the highest-throughput one the matrix above declares (RFC-056).
- The credential-entry duty (RFC-056). A client that can provision a hardware hub MUST provide a way for the user to enter WiFi credentials: the §13.9 push over BLE or serial (RFC-069), a form, a QR scan, an SD card, whatever suits it. The spec mandates the capability, never the mechanism; without it a factory-fresh hub with no BLE would have no route onto a network.
All of the above is availability policy, stated so a client knows what to expect from an arbitrary hardware hub — it changes no wire format.
13.2 WebSocket#
Subprotocol valence.v1 in the upgrade handshake — this is version negotiation for free, and it lets a legacy protocol coexist on a different path or subprotocol during migration. The server MUST perform RFC 6455 subprotocol selection and echo valence.v1 in the upgrade response. Strict clients hard-fail without the echo — two independent WS server libraries had to be patched to comply, which is why this is a sentence rather than an assumption. One Valence frame = one WS binary message; no batching at the WS layer, since bundles already amortize. Text messages on a valence.v1 socket are a protocol error (close 1002). The server is the hub. RECOMMENDED endpoint: /valence on the primary HTTP port.
13.3 ESP-NOW#
Datagram binding: 250-byte payload − 8-byte header = 242. Unicast per peer where peers are few; broadcast segments follow §10.6.
Reliability layer: every data frame carries its header seq; receivers emit a batched ACKMASK frame (0x16, raw, channel 0) every 10 ms — payload base_seq:u16, mask:u32 — acknowledging seqs base..base+31. Senders use the resulting loss rate as the §10.3 congestion signal. There is no retransmission of STATE or STREAM (those classes do not need it); control-plane frames use stop-and-wait retransmit (3×, 100 ms) keyed on the ACK mask. Discovery and pairing broadcast: §13.7.
13.3.1 ESP-NOW spoke: the accessory profile (RFC-075)#
Accessories (a pump, a vibrator, a motorized stand) attach to a hub over ESP-NOW in a star: the accessory host (a hub) is the center, each accessory is a spoke (duty sets: §17.1). Frames flow only between the host and each accessory, never accessory to accessory, and no client session runs on the spoke. The §13.3 client-session binding is unchanged and MAY run beside the spoke on the same radio. Spoke frames are ordinary Valence frames (§5.1 header), one per ESP-NOW payload; channel ids on the spoke are accessory-relative (§8.10).
- ESP-NOW v1 framing is pinned (MUST). A spoke frame MUST fit one ESP-NOW v1 payload, 250 bytes including the 8-byte header, even where the silicon supports v2 payloads, so v1-only silicon interoperates. §5.6 fragmentation MUST NOT be used on the spoke, so an accessory never implements reassembly; anything larger moves only over the blob verb (§8.4), which is chunked by construction.
- Unencrypted, by ruling (H13). Spoke frames carry no ESP-NOW encryption and no Valence authentication: the setting is private, the range a room or a house, comparable consumer accessories are open BLE, and per-peer encryption would cap the peer list at 17 and add a key ceremony. What bounds the consequences, as obligations: an accessory MUST clamp every received actuating value into its declared
min/max, so a forged command can do nothing a legitimate one could not; a host MUST NOT treat accessory telemetry as safety-grade input (no accessory value may clear a safety latch, satisfy an interlock, or gate machine motion); a client MUST NOT present spoke traffic as authenticated or private. - BEACON is the hub heartbeat. BEACON (
0x17, raw, header channel0x0000) carries the pinned payload, little-endian and tail-extensible per §5.4:boot_id:u32 + catalog_etag:8B + flags:u8 + hub_instance_id:u64 + wifi_channel:u8= 22 bytes (a 30-byte frame). The first three fields are the §13.7 prefix. The headerseqis the beacon sequence, incremented once per beacon per boot and compared per §7.3.flagsare registrybeacon_flags: bit0pairing_window_open, bit1datagram_estop, bit2accessory_host(this hub runs the spoke and accepts accessory joins), bit3estop_latched(this hub'ssafetysnapshot shows ESTOP latched now); bits 4-7 MUST be zero. There is no PAUSE bit: pause reaches accessories through the relationship interlock as commanded safe values (§11.6), and onlyestop_latchedis broadcast. A hub without ahub_instance_id(§6.1) MUST NOT act as an accessory host.wifi_channelis the primary channel the hub transmits on (1 to 14); an accessory that hears a beacon leaked from an adjacent channel retunes to the named one. Cadence: an accessory host MUST broadcast BEACON everyspoke_beacon_interval_ms(1000) for as long as the spoke is up, whether or not a pairing window is open, and every 500 ms while one is. - Channel follow. The host never changes channel for its accessories; it follows its access point. At boot and on every deadman fire the accessory scans channels 1 to 13 (a regional accessory MAY scan a subset), on each channel broadcasting one DISCOVER_PROBE (
0x1E, the §13.8 payload unchanged) and listeningspoke_scan_dwell_ms(150) for a BEACON from its hub (source address andhub_instance_idboth match its stored pairing) or, in pairing state, any BEACON withaccessory_hostandpairing_window_openset. An accessory host that receives a DISCOVER_PROBE on the spoke MUST answer with an immediate, out-of-cadence broadcast BEACON, at most one perspoke_scan_dwell_ms(broadcast, because a unicast answer to an unknown address would spend a peer-list entry). An accessory SHOULD scan actively (about 2 s cold join); passive listening (up to about 13 s) is also conformant. The probe only finds the hub; the beacon alone is the deadman's heartbeat. Rescan after beacon loss is the boot code path. - The accessory deadman (MUST). An accessory MUST enter its safe state when: (a) no BEACON from its hub (source address and
hub_instance_idmatch) has arrived forspoke_deadman_ms(5000) or the shorter window its declaration carries, never below 2 ×spoke_beacon_interval_ms; (b) it receives any GOODBYE (0x11) from its hub, whatever its code and even if the payload fails to decode; (c) it receives a valid, CRC-checked ESTOP frame (§5.5) from any source, or a BEACON from its hub withestop_latchedset; or (d) it detects a local fault. Only BEACON refreshes the deadman; unicast commands do not, so one clock answers "is my hub alive". The safe state holds every actuating field (every value-bearing field of a declared INTENT schema or c2h STREAM layout) at its declaredsafevalue (§8.10) and abandons any verb in progress. Leaving it: from deadman, GOODBYE or fault, only on a fresh actuating command from its hub received after a matching BEACON; from ESTOP, only after a BEACON withestop_latchedclear, and then only on a fresh command. An accessory MUST NOT restore its pre-safe values on its own. A host SHOULD broadcast GOODBYE (REBOOTINGorNORMAL_CLOSURE) on the spoke before a planned reboot or spoke shutdown. - Reliability. §13.3's ACKMASK loss signal and stop-and-wait control retransmit (3 × 100 ms) apply to each host-accessory link unchanged. STATE and STREAM are not retransmitted.
- ESTOP on the spoke. A host accepts a valid ESTOP frame from any peer on its channel. On latching ESTOP the host MUST broadcast the ESTOP frame on the spoke, repeating at
estop_repeat_interval_msup toestop_repeat_max(§11.2), and MUST keepestop_latchedset in every BEACON while the latch holds (the 1 Hz loss-recovery path for an accessory that missed every repeat). The acknowledgment is the accessory's status snapshot reportingsafe_estop; the host stops repeating once every joined accessory has reported it and shows any that have not as unconfirmed on its accessory roster. An accessory MUST NOT rebroadcast an ESTOP frame: the spoke has no relay role. - Peer limit. ESP-IDF caps the peer list at 20 entries and the broadcast entry the beacon needs takes one, so a host addresses at most 19 accessories by unicast, fewer if the §13.3 client binding shares the list. A host declares its accessory capacity (§8.10); rotating peer entries to exceed it is permitted, not required.
- Radio coexistence. The spoke rides the station interface on the access point's channel. A hub never operates a softAP (§13.4.1).
13.3.2 Accessory join (§8.10)#
- The pairing window. Accessory joins use the §12.3 association window: one window, one timer (
pairing_window_default_s), reported by BEACONpairing_window_openandble_adv_flagsbit0. It opens by the host's pairing control or by theaccessory-adminwindow_openop from aconfiguresession. An accepted join is the window's single grant, exactly as a push-to-pair knock is: the window closes after it. One press, one device. - JOIN_REQ (
0x21, raw, accessory to host, unicast). An accessory enters pairing state by its own local gesture (device-defined) or, when factory-fresh, on its own; it scans (§13.3.1) for any BEACON withaccessory_hostandpairing_window_open, then sends JOIN_REQ to that beacon's source address, once per BEACON received, until answered. A paired accessory sends the same frame to its stored host on every reacquisition. Payload (little-endian):accessory_id:u64 + proto_ver:u8 + declaration_etag:8B + deadman_ms:u16 + flags:u8 + fw_version:str16 + product:str16= 52 bytes.deadman_msis the declared deadman window (0 =spoke_deadman_ms), bounded per §13.3.1.flagsbit0pairing_state(asking to pair, as opposed to rejoining); bits 1-7 zero. - JOIN_REPLY (
0x22, raw, host to accessory, unicast). Payload:hub_instance_id:u64 + accessory_id:u64(echoed)+ result:u8 + flags:u8= 18 bytes.resultis ajoin_resultsvalue: 0accepted; 1window_closed(unknown accessory, no window open); 2capacity(no free slice, no peer entry, or the declaration would exceed the host's advertised capacity); 3unsupported(proto_ver); 4declaration_invalid(§8.10); 5not_paired(a rejoin from an accessory the host has forgotten).flagsbit0declaration_needed(the host holds no declaration with this etag and is about to fetch it). Every JOIN_REQ is answered (§4.5); answers to unknown accessories are rate-limited per source address atudp_discovery.reply_rate_limit_per_source_s. - Declaration fetch. On
declaration_neededthe host sends BLOB_REQ (ns = 0); the accessory answers BLOB_CHUNK with §8.4 pacing; the host verifies SHA-256 againstdeclaration_etag, sends BLOB_DONE, validates (§8.10), and grows its catalog (§8.6). - Rejoin after power loss, no window. A host holding a record for that
accessory_idaccepts its JOIN_REQ whether or not a window is open. Equaldeclaration_etag: no transfer. Different etag (reflashed, same identity): the host refetches, revalidates and replaces the declaration in the same slice; the catalog changes per §8.6, and relationships whose target field no longer exists are disabled. After any join the accessory is in its safe state until its host commands it. A host reboot needs nothing extra: every accessory's deadman fires, it rescans, it rejoins.
Implementation risk (informative). Where the radio sits on a co-processor behind esp_hosted (the reference Flagship pairs an ESP32-P4 host with an ESP32-C6), ESP-NOW reaches the host through a shim with three known bugs: a send can stall; the peer table goes stale after a co-processor restart; a full peer table reports success. The deadman makes the first two fail safe on the accessory side; for the third, a host SHOULD count its own peer entries rather than trust the add result.
13.4 BLE GATT#
A NUS-shaped service (one write characteristic c→h, one notify characteristic h→c) carrying Valence frames as characteristic values, each ≤ ATT_MTU − 3.
Identity is pinned, not per-implementation (RFC-046 item 1). Every conformant BLE hub advertises the same GATT service, so a client scans for exactly one thing: service UUID 56414C45-4E43-4531-8000-000000000001 (ble_identity.service_uuid; the first three groups spell VALE/NC/E1 in ASCII, deliberately, so the UUID is greppable and mnemonic rather than an opaque v4), write characteristic (c2h) ...-8000-000000000002, notify characteristic (h2c) ...-8000-000000000003. This is the phone-facing twin of the ESP-NOW BEACON frame's own per-binding discovery (§13.7).
Advertising payload (RFC-046 item 2). Within the legacy ≤ 31-byte advertising budget: the service UUID and a shortened hub name. The fuller name and one flags byte (ble_adv_flags) ride the scan response instead (active scan required to read it) — the advertisement's own budget (service UUID + shortened name = 27 B) has no room left for a 5 B Manufacturer-Specific-Data record alongside them. The record layout is pinned (RFC-072): AD length 0x04, AD type 0xFF (Manufacturer Specific Data), then company_id:u16le = 0xFFFF (ble_identity.msd_company_id) and flags:u8; a client reads the flags byte only from the record carrying that company id. (Whether to keep 0xFFFF, the Bluetooth SIG testing id, is an open question pre-v1.0.) The flags: bit0 pairing_window_open (a §12.3 association window is open right now — same meaning as the BEACON frame's pairing-open flag), bit1 ws_available (the hub currently has a live IP and a listening WebSocket port — RFC-043's signal that a connected client SHOULD auto-upgrade to WS, §6.3), bit2 config_mode (the hub is in config mode for the whole mode, §13.4.1, RFC-079). Bits 3–7 are reserved and MUST be zero. The endpoint itself — which port, which address — is not squeezed into this byte; it rides WELCOME's ws_port/ipv4 (§6.3) once the client has connected and can spend CBOR map keys on it.
Clients SHOULD negotiate MTU ≥ 250 and enable data-length extension before catalog transfer; below that the binding declares its real MTU and the 242-byte STATE-fit rule still governs catalog design, while control frames fragment per §5.6 and data frames are sized to the declared MTU at grant time by bundling less. A client stuck at the legacy 23-byte MTU cannot carry a full STATE frame at all and pays a long one-time catalog transfer (visibly SYNCING) or ships the §8.5 static profile. Static-profile clients are the expected BLE norm.
13.4.1 Config mode (RFC-079)#
A hub enters config mode when it boots with its pairing control held (a hub with a pairing button binds this gesture; the §12.3(c) power-cycle gesture is unchanged and does not enter config mode). Config mode is how a factory-fresh hub, or one moved to a new network, waits safely for credentials. In config mode the hub:
- opens the §12.3 association window at boot (
pairing_window_default_s), and the pairing control re-opens it; - advertises BLE GATT with
ble_adv_flagsbit0pairing_window_openwhile the window is open and bit2config_modefor the whole mode; - activates the §13.5 USB serial binding alongside BLE GATT; both are first-class provisioning paths and both accept the §13.9
wifi_join; - does not associate to WiFi and runs no WebSocket listener (
ws_availableclear); - does not run the ESP-NOW spoke: accessory pairing waits until the hub has joined WiFi, because the spoke follows the access point's channel and there is none in config mode;
- grants
configureto the window's first knock even whenconfiguretokens exist (§12.3).
No softAP, in any mode (MUST NOT). A hub never operates a WiFi softAP: a softAP pins the radio to its own channel, which the ESP-NOW spoke (§13.3) cannot follow. A hub that offers config mode MUST implement BLE GATT. Config mode wipes nothing: factory reset stays the deliberately harder gesture §12.3(c) requires.
Leaving config mode. On a successful wifi_join the hub SHOULD leave config mode without a reboot: bring up the station and the WebSocket listener, set ws_available, clear config_mode, so the client migrates per §6.3. A hub MAY instead commit by rebooting (ECHO reboot_in_ms, §9.3). A reboot without the gesture always leaves config mode.
Clients. The advertisement bit is the whole signal; no in-session indicator exists. A client connected to a hub whose advertisement carried config_mode SHOULD open on the setup category (ui_categories 15), presented with the wizard pattern (RENDERING.md §10): one step per entry in authoring order (§8.9 item 4), the provisioning step showing the §13.9 ECHO or NACK outcome, ending with the §6.3 migration to WS (from BLE or serial). It MUST render the category generically from the catalog, never a hub-specific screen and never a channel found by name. A client listing discovered hubs SHOULD mark a config_mode hub distinctly ("needs setup"). The catalog does not change with the mode (§8.6): setup entries carry setup in every mode.
13.5 Serial#
Byte pipe → COBS framing, delimiter 0x00: encode each Valence frame with COBS and append 0x00.
ESTOP scanning: the §5.5 magic is matched on the decoded stream; additionally, because COBS never produces 0x00 inside a frame and re-synchronizes at every delimiter, a receiver in an unsynced or corrupt state MUST still run the four-0xE5 scanner on raw bytes between delimiters. 0xE5 survives COBS encoding unchanged when no zero bytes occur in the window, and the CRC validates any candidate either way.
13.6 In-process (the conformance binding)#
The in-process binding connects hub and client roles inside one process (desktop simulator, unit tests). It is a first-class conformance instrument and therefore MUST support: configurable MTU (down to 242 and below), injected loss/reorder/duplication rates, injected latency and jitter, and a deterministic mode (seeded fault schedule plus injected clock) in which a run is bit-reproducible. The behavioral tests of §17.3 run against it; an implementation without fault injection cannot claim conformance testing.
13.7 Discovery#
- No mDNS service record; a hostname responder only (RFC-072). No DNS-SD service type and no TXT record exist: native clients discover WS-side hubs by the §13.8 UDP probe, which carries
hub_instance_id. A hub that serves a page SHOULD answer mDNS hostname queries for its own name (<name>.local;machine.localin the reference) so a browser can reach the served page by name. That is hostname resolution only: nothing a client browses or parses. A manually-entered address MUST always work — discovery is a convenience, never a requirement. - BLE: advertising payload and identity are pinned in §13.4. BLE advertisement is the primary discovery path in general: it is physically present, needs no network, and works before the hub is even provisioned onto a WiFi network at all.
- ESP-NOW: the hub or its relay broadcasts a BEACON frame (
0x17, raw, channel 0; payload prefix:boot_id, catalog etag, flags; pinned in full by §13.3.1) every 500 ms only while a pairing window is open, unless it is an accessory host, which beacons continuously per §13.3.1. New peers respond to beacons, then run PAIR_REQ over unicast. Outside the window, peers must already know the segment from a previous pairing.
Discovery is an untrusted input. A client that auto-connects to a discovered service is one malicious hub away from parsing hostile bytes; §5.8-5 and §12.5 are what bound the consequences.
13.8 UDP discovery (RFC-046 item 5)#
A minimal broadcast probe/reply pair, and the canonical WS-side discovery path for a LAN client without BLE (a desktop shell, a streaming-application plugin, Intiface) — plain UDP sockets both ends, immune to the multicast/mesh-AP/Android failure modes that make mDNS unreliable in real homes, and simple enough to retire a hand-rolled DNS-SD query.
- Port and magic are registry-pinned (
udp_discovery): port22096(0x5650, ASCIIVPfor "Valence Probe"), magic bytes56 4C 4E 43(ASCIIVLNC) opening every probe and reply, mirroring the ESTOP frame's own magic-byte convention (§5.5). - DISCOVER_PROBE (
0x1E, raw, c2h): a client broadcastsmagic(4B) + proto_ver:u8 + nonce:u32(client entropy, echoed in the reply so a client running several probes at once can match them). - DISCOVER_REPLY (
0x1F, raw, h2c): the hub unicasts back to the probe's source address:magic(4B) + nonce:u32(echoed)+ hub_name:str32 + hub_instance_id:u64(the hub's durable cross-boot identity, §6.1/§6.3 — distinguishing two hubs sharing a name across reboots, which a per-boot value cannot do)+ proto_ver:u8 + ws_port:u16 + fw_version:str16 + catalog_etag:8B + flags:u8(bit0pairing_window_open, the same philosophy as the ESP-NOW BEACON payload, plus the endpoint a BEACON has no room for).str16/str32are the fixed-width zero-padded field types of §5.4/RFC-026. RFC-048 correction (operator veto of an RFC-046 decision, at landing): this field originally carried the hub'sboot_id(u32); RFC-046's own entry flagged that choice for veto because a boot-scoped id cannot deduplicate two hubs sharing a name across a reboot, which is this field's entire job. The reply's total payload grows from 72 to 76 bytes (the four-byteu32→u64widening); no other field moves. This layout landed with zero implementations, so the correction is free. - Read-only identity, no control surface. A probe cannot command anything, and a reply discloses nothing a passive observer of a normal WELCOME could not already learn. Replies are rate-limited to
udp_discovery.reply_rate_limit_per_source_s(1) per source address, so a probe storm cannot load the hub — the same posture as the BEACON frame's own broadcast cadence. - A manually-entered address MUST still always work; this, like every discovery mechanism, is a convenience.
sequenceDiagram
autonumber
participant C as Client
participant N as LAN (broadcast)
participant H as Hub
Note over C: ENTRY POINT — client wants a hub, has no address
C->>N: DISCOVER_PROBE (broadcast) magic + proto_ver + nonce
N->>H: delivered to every hub on the segment
H-->>C: DISCOVER_REPLY (unicast) hub_name, hub_instance_id,\nws_port, fw_version, catalog_etag, flags
Note over H: rate-limited to 1 reply / source / s
C->>C: matches reply's nonce to its own probe
C->>H: ordinary HELLO over WS to ws_port
A probe storm from one source only ever gets one reply per second — the loop that would otherwise exist (retry until an answer arrives) is a client policy, not a protocol requirement, because a manually-entered address is always an equally valid entry point.
Discovery doctrine, restated for this binding: BLE advertisement remains primary where BLE is available at all (§13.7); the UDP probe is the WS-side discovery path for a LAN client without BLE, and no mDNS service record exists to compete with it (§13.7, RFC-072).
13.9 Provisioning: client-pushed network credentials (RFC-069)#
A factory-fresh hub has no network; the operator's phone or computer has the credentials. The spec-core INTENT channel provisioning (0x000F), configure access, carries them in.
- Schema (the channel's own keys): 1
op, an op select with roleaction.provisionoverprovisioning_ops(wifi_join1; index 0 is filler, §8.9); 2ssid(tstr); 3passphrase(tstr, MAY be empty for an open network); 4ipv4and 5ws_port(uint, result keys of the ECHO).ssidandpassphrasecarry thesecretflag. A core id, not a device channel plus role, because the first-run client binds by identity and a headless hub has no other surface. - Gate (MUST). The hub accepts
wifi_joinonly when all hold: the session holdsconfigure; a §12.3 association window is open, or the hub is factory-fresh (zeroconfiguretokens, §12.3); and the frame arrived on a BLE GATT (§13.4), serial (§13.5) or in-process (§13.6) binding. Otherwise NACKACCESS_DENIED. A network binding is refused because it rides the network being changed and is cleartext (H4). The serial binding is a first-class provisioning path beside BLE: where config mode is offered (§13.4.1), both are active at once and both acceptwifi_join. A hub SHOULD require LE Secure Connections on a BLE link (§12.9). - Unicast result. The hub defers its answer until the join concludes or
provision_join_timeout_mselapses. Success: ECHO whoseappliedcarriesop, each credential key with the valuetrue(§8.8 Secrets), and the resultingipv4andws_port. Failure: NACKNETWORK_JOIN_FAILED, whosedetailMUST NOT contain either credential. A duplicateintent_idduring the attempt joins it and MUST NOT start a second attempt. - No stranding (MUST). A hub that already has working credentials keeps them until the new ones join; a failed join leaves the prior configuration in effect.
- Never disclosed (MUST). Credentials never appear in STATE, in any EVENT (including the log channel, §16.2), in GOODBYE or NACK
detail, in any diagnostic surface, or in the ECHO of any other session. Their public consequences ride their existing homes:ipv4andws_portin WELCOME (§6.3),ble_adv_flags.ws_available(§13.4). A client MUST NOT log or persist the credentials beyond the send, and enters the SSID itself: the hub publishes no scan list. - Then the upgrade. On success the client SHOULD perform the §6.3 BLE-to-WS migration using the returned endpoint.
14. Relay Role (normative)#
14.1 Forwarding#
A relay bridges the hub's reachable transports to segments it cannot reach. Rules:
- A relay forwards frames, not sessions: it does not parse control-plane CBOR, does not hold grants, and is invisible to the session layer except as specified here. Clients behind a relay hold ordinary sessions with the hub.
- Priority-aware buffering: a relay MUST maintain at least two queues per direction — critical (the never-shed set plus the ESTOP fast path) and everything else — and MUST apply §10.4-style shedding when its downstream is slower than its upstream, including the segment exception: it decimates
samples-kind streams and conflates STATE by replacing queued frames for the same channel with newer ones, but it MUST NOT decimate asegments-kind stream. A relay that blindly FIFOs is non-conformant: it converts congestion into latency, which for motion data is the worst outcome (§9.2). - A relay MAY further decimate below granted rates when its segment demands it; the hub's congestion machinery observes the resulting loss and re-grants honestly (§10.3), so the system converges without the relay speaking the grant protocol.
14.2 ACK aggregation and the ESTOP fast path#
- Reliability is hop-by-hop. The relay acknowledges what it receives from its segment and takes responsibility for upstream delivery, and vice versa. There are no end-to-end transport acknowledgments across a relay. HONESTY CLAUSE (H10), stated plainly: the hub knowing a frame reached the relay does not mean the client got it. This is safe because no protocol correctness depends on transport delivery — STATE re-pushes, STREAM tolerates loss, and the only end-to-end confirmations that exist are protocol-level: INTENT ⇒ ECHO and ESTOP ⇒ observed latch.
- ESTOP fast path: on matching the four-
0xE5magic with a raw scanner — no deframing, no queueing — a relay MUST transmit the frame onward on all attached segments ahead of every queued frame, then resume normal operation. CRC validation MAY be deferred to endpoints when the relay's budget is tight: forwarding a corrupt candidate costs 12 bytes; dropping a real one costs much more.
14.3 Timestamp correction and limits#
A relay that buffers — adds more than 1 ms of asymmetric delay — MUST satisfy exactly one of:
(a) correct — stamp arrival and, on transmit, rewrite STREAM t_base by its holding time;
(b) be CLOCK-transparent — forward CLOCK frames with strict priority, under 1 ms of added delay;
(c) drop CLOCK frames entirely (§7.1), degrading its clients to WELCOME-bootstrap accuracy.
Silent uncorrected buffering of CLOCK is non-conformant. Note that (a) matters doubly for segments-kind streams, where t_base is a schedule, not an observation (§5.4): an uncorrected relay does not merely blur a graph, it moves commands in time.
The relay ESTOP latency budget (RFC-049f). A relay MUST forward an ESTOP-class frame ahead of every buffered frame on all attached segments (§14.2) and MUST add no more than one binding-native frame-transmission time in doing so. Composed with H2's per-hop accounting, the end-to-end ESTOP guarantee across a single relay is the binding's own worst-case added latency (§13.1) plus exactly one relay-hop budget — never an unbounded function of the relay's queue depth. This is the same H2/§13.1-style worst-case accounting the transport matrix already states, extended one hop further.
Relays MUST NOT chain — one relay hop maximum in v1 — and the reason is architectural, not arbitrary (RFC-049f). The ESTOP budget above and §13.1's worst-case-added-latency accounting are bounded specifically because there is exactly one hop to bound: a chain of relays would compound that worst case with no ceiling the spec states — two 250 ms buffering relays in series could turn a binding's 50 ms guarantee into 550+ ms and nothing here would call it non-conformant. v1 also defines no routing or loop-protection protocol a relay could use to discover, bound, or refuse a chain it finds itself part of; chaining without that missing machinery is unsafe, not merely unspecified. Multi-hop is a v2 problem nobody currently has, and it gets its own RFC precisely when a real topology needs the routing and multi-hop latency accounting this one lacks — not because chaining is conceptually forbidden.