Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
hub_pairing.cpp
Go to the documentation of this file.
1#include "hub_pairing.h"
2
3#include "hub_decisions.h"
4#include "hub_core.h"
5#include "hub_internal.h"
6#include "proto_commands.h"
7#include "esphome/core/log.h"
8
9#include <algorithm>
10#include <cstdio>
11#include <cstring>
12
13/// @file hub_pairing.cpp
14/// @brief Device pairing orchestration — discovery, key exchange, and finalization.
15/// @ingroup hioc_hub
16///
17/// Implements IOHomeControlComponent::discover_and_pair() and the three-phase
18/// helper methods: run_discovery_phase_(), run_key_exchange_phase_(),
19/// finalize_pairing_configuration_(), plus low-level waiters
20/// wait_for_discovery_response_() and wait_for_key_challenge_().
21/// Also contains parse_device_from_discovery() to extract device metadata from
22/// discovery frames.
23///
24/// Pairing is separated from the normal exchange path to keep authenticated
25/// command execution independent from the one-time key-establishment flow.
26/// @todo Validate the full discovery and re-pair flow on freshly reset SX1276-backed devices,
27/// including discovery response capture, key exchange, and the final configuration step.
28/// @todo Validate the full discovery and re-pair flow on freshly reset SX1262-backed devices,
29/// including discovery response capture, key exchange, and the final configuration step.
30/// @todo Add first-class platform coverage for additional paired device classes once real
31/// hardware is available to validate their command semantics and status reporting.
32
33namespace esphome {
34namespace home_io_control {
35
36namespace {
37
38const char *const TAG = "home_io_control";
39constexpr size_t DEVICE_TYPE_HEX_STRING_BUFFER_SIZE = 8; ///< Buffer for strings such as "0x11" plus terminator.
40
41/// Map PairingState enum to string for debug logging.
42const char *pairing_stage_name(pairing::PairingState state) {
43 switch (state) {
45 return "idle";
47 return "tx_discover";
49 return "wait_discover_response";
51 return "tx_key_init";
53 return "wait_key_challenge";
55 return "tx_key_transfer";
57 return "wait_key_confirm";
59 return "register_device";
61 return "complete";
63 default:
64 return "failed";
65 }
66}
67
68// response_wait_slice_ms provided by decisions namespace (hub_decisions.h)
69
70/// Check if frame is a 0x33 key‑confirm message.
71bool frame_is_key_confirm(const IoFrame &frame) { return frame.cmd == CMD_KEY_CONFIRM; }
72
73/// Log discovery‑phase failure reason based on disposition.
74///
75/// Called by `discover_and_pair()` when the discovery phase does not return
76/// ACCEPT. The messages distinguish between no traffic at all and traffic that
77/// was seen but no valid discovery response arrived.
78///
79/// @param disp Discovery disposition value (NO_RESPONSE or INVALID).
80void log_discovery_diagnostic(decisions::PairingDiscoveryDisposition disp) {
81 switch (disp) {
83 ESP_LOGW(TAG, "No device responded to discovery");
84 break;
86 ESP_LOGW(TAG, "No valid discovery response received");
87 break;
89 break;
90 }
91}
92
93/// Return the YAML-friendly device-type name when the schema exposes a symbolic alias.
94/// Types without a symbolic alias can still be configured via raw numeric values such as 0x11.
95const char *yaml_device_type_name(DeviceType type) {
96 switch (type) {
98 return "venetian_blind";
100 return "roller_shutter";
102 return "awning";
104 return "window_opener";
106 return "garage_opener";
108 return "light";
110 return "gate_opener";
112 return "rolling_door_opener";
113 case DeviceType::LOCK:
114 return "lock";
116 return "blind";
118 return "screen";
120 return "heating_temperature_interface";
122 return "on_off_switch";
124 return "horizontal_awning";
126 return "curtain_track";
128 return "intrusion_alarm";
129 default:
130 return nullptr;
131 }
132}
133
134/// Format a raw device type as hexadecimal for YAML and diagnostics.
135std::string format_device_type_hex(DeviceType type) {
136 char buf[DEVICE_TYPE_HEX_STRING_BUFFER_SIZE];
137 snprintf(buf, sizeof(buf), "0x%02X", static_cast<uint8_t>(type));
138 return std::string(buf);
139}
140
141/// Return a human-readable device type string including the raw numeric value.
142std::string format_device_type_diagnostic(DeviceType type) {
143 const char *name = device_type_name(type);
144 std::string raw = format_device_type_hex(type);
145 if (name != nullptr && strcmp(name, "unknown") != 0) {
146 return std::string(name) + " (" + raw + ")";
147 }
148 return raw;
149}
150
151/// Build the YAML value for io_device_type.
152/// Supported symbolic aliases stay readable; all other types fall back to raw hex.
153std::string format_device_type_for_yaml(DeviceType type) {
154 const char *name = yaml_device_type_name(type);
155 if (name != nullptr) {
156 return std::string("\"") + name + "\"";
157 }
158 return format_device_type_hex(type);
159}
160
161/// Map a supported capability class to the corresponding YAML platform name.
162const char *pairing_platform_name(DeviceCapabilityClass capability_class) {
163 switch (capability_class) {
165 return "cover";
167 return "light";
169 return "switch";
170 default:
171 return nullptr;
172 }
173}
174
175} // namespace
176
177// --- Pairing helpers ---
178
179/// Wait for a valid discovery response (0x29) within the timeout.
180///
181/// Listens for incoming packets and parses them. Frames that parse successfully
182/// are passed to `decisions::classify_pairing_discovery_response()`. Only a
183/// frame classified as ACCEPT (CMD_DISCOVER_RESP) returns true. All other
184/// frames are ignored until the deadline expires.
185///
186/// The function distinguishes between NO_RESPONSE (no packets seen at all) and
187/// INVALID (some packets seen but none were valid discovery responses).
189 RadioRxPacket &packet,
190 IoFrame &response_frame) {
191 bool saw_traffic = false;
192 const uint32_t deadline = millis() + timeout_ms;
193 while ((int32_t) (deadline - millis()) > 0) {
194 const uint32_t remaining_ms = deadline - millis();
195 const uint32_t slice = decisions::response_wait_slice_ms(remaining_ms);
196 if (!this->radio_->wait_for_packet(packet, slice))
197 continue;
198 saw_traffic = true;
199 if (!parse(packet.data, packet.len, response_frame))
200 continue;
201 auto disp = decisions::classify_pairing_discovery_response(response_frame);
203 return disp;
204 }
205 if (!saw_traffic)
208}
209
210/// Wait for a valid key‑challenge frame (0x3C from the target device).
211///
212/// During key exchange the device responds to our key‑init with a random 6‑byte
213/// challenge. This helper loops until such a frame is received and validated
214/// by `decisions::classify_pairing_key_challenge()` — it must be a 0x3C
215/// command, 6 bytes long, and come from the discovered device node ID.
217 IoFrame &challenge_frame,
218 const uint8_t device_node_id[NODE_ID_SIZE]) {
219 bool saw_traffic = false;
220 const uint32_t deadline = millis() + timeout_ms;
221 while ((int32_t) (deadline - millis()) > 0) {
222 const uint32_t remaining_ms = deadline - millis();
223 const uint32_t slice = decisions::response_wait_slice_ms(remaining_ms);
224 if (!this->radio_->wait_for_packet(packet, slice))
225 continue;
226 saw_traffic = true;
227 if (!parse(packet.data, packet.len, challenge_frame))
228 continue;
229 if (decisions::classify_pairing_key_challenge(challenge_frame, device_node_id, this->node_id_) !=
231 continue;
232 return true;
233 }
234 ESP_LOGW(TAG, saw_traffic ? "Key exchange: no valid challenge received" : "Key exchange: no challenge received");
235 return false;
236}
237
238/// Parse a discovery response frame into device metadata and ID.
239///
240/// Extracts node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
241/// The two-byte payload uses the shared packed device metadata layout defined in proto_frame.h.
242/// The inversion flag is derived from the type via `default_inverted_for_type()`.
244 std::string &device_id) {
245 memcpy(device.node_id, frame.src, NODE_ID_SIZE);
246 if (frame.data_len >= DEVICE_METADATA_SIZE) {
247 device.type = decode_packed_device_type(frame.data[0], frame.data[1]);
248 device.subtype = decode_packed_device_subtype(frame.data[1]);
249 device.inverted = default_inverted_for_type(device.type);
250 } else {
251 device.type = DeviceType::UNKNOWN;
252 device.subtype = 0;
253 device.inverted = false;
254 }
255 device.position = UNKNOWN_POSITION;
256 device.target = UNKNOWN_POSITION;
257 device.is_stopped = true;
258 device_id = node_id_to_string(device.node_id);
259}
260
261/// Execute Phase 1: discover a pairable device on channel 2.
262///
263/// Transmits a discovery broadcast (0x28) and waits up to the configured discovery timeout for a valid
264/// discovery response (0x29). On success the response is parsed into
265/// `context.device` and `context.device_id` and the function returns ACCEPT.
266/// On failure returns NO_RESPONSE (no packets) or INVALID (packets seen but
267/// none were valid discovery frames).
270 this->record_exchange_debug_(pairing_stage_name(context.state), 1, false);
271 if (!create_discover(context.req, this->node_id_) || !this->transmit_frame_(context.req, FREQ_CH2, LONG_PREAMBLE)) {
273 }
274
276 this->record_exchange_debug_(pairing_stage_name(context.state), 1, false);
277 auto result =
280 parse_device_from_discovery(context.rx, context.device, context.device_id);
282 }
283 return result;
284}
285
286/// Execute Phase 2: perform authenticated key exchange with the discovered device.
287///
288/// Performs the full four‑step key exchange:
289/// 1. TX_KEY_INIT — send key‑init frame (0x31) to device
290/// 2. WAIT_KEY_CHALLENGE — wait for device's challenge (0x3C)
291/// 3. TX_KEY_TRANSFER — send challenge response + our key share (0x32)
292/// 4. WAIT_KEY_CONFIRM — wait for device's key‑confirm (0x33) using the
293/// normal authenticated exchange (`send_and_receive_`) to inherit retry logic.
294///
295/// Each step updates the pairing state for observability. Any failure returns
296/// false immediately; the orchestrator will clean up and abort pairing.
299 this->record_exchange_debug_(pairing_stage_name(context.state), 1, false);
300 if (!create_key_init(context.key_init, this->node_id_, context.device.node_id) ||
301 !this->transmit_frame_(context.key_init, FREQ_CH2, LONG_PREAMBLE)) {
302 return false;
303 }
304
306 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
308 context.device.node_id)) {
309 return false;
310 }
311
313 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
314 if (!create_key_transfer(context.req, context.key_init, context.device.node_id, this->node_id_, this->system_key_,
315 context.rx.data)) {
316 return false;
317 }
318
320 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
321 if (!this->send_and_receive_(context.req, context.resp, FREQ_CH2) || !frame_is_key_confirm(context.resp)) {
322 ESP_LOGW(TAG, "Key exchange failed");
323 return false;
324 }
325 return true;
326}
327
328/// Execute Phase 3: finalize pairing configuration on the device.
329///
330/// Sends a SetConfig1 command (0x6F) using the authenticated exchange path.
331/// This step is optional in the sense that the command may be skipped if
332/// `create_set_config1()` returns false, but the pairing itself is already
333/// complete at this point. The function always returns true to avoid aborting
334/// a successfully paired device.
336 if (create_set_config1(context.req, this->node_id_, context.device.node_id))
337 this->send_and_receive_(context.req, context.resp, FREQ_CH2);
338 return true;
339}
340
341/// Pairing orchestrator — high‑level three‑phase flow.
342///
343/// Phase 1: Discovery (run_discovery_phase_) finds a device in pairing mode.
344/// Phase 2: Key exchange (run_key_exchange_phase_) performs authenticated
345/// key establishment using the challenge‑response protocol.
346/// Phase 3: Finalization (finalize_pairing_configuration_) sends SetConfig1.
347///
348/// On success the device is added to the current runtime registry and either a valid
349/// YAML snippet or a follow-up guidance message is printed in the logs. Any phase
350/// failure aborts early, logging an appropriate warning.
351///
352/// @return true if pairing completed; false otherwise.
354 if (!this->initialized_)
355 return false;
356 ESP_LOGI(TAG, "Starting device discovery...");
357
358 this->busy_ = true;
360
361 // Phase 1: Discovery — find a pairable device
362 auto disc_disp = this->run_discovery_phase_(context);
364 log_discovery_diagnostic(disc_disp);
365 this->busy_ = false;
366 return false;
367 }
368
369 // Phase 2: Key exchange — establish shared system key
370 if (!this->run_key_exchange_phase_(context)) {
371 this->busy_ = false;
372 return false;
373 }
374
375 // Phase 3: Final configuration — best-effort SetConfig1
376 this->finalize_pairing_configuration_(context);
377
379 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
380
381 // Register device in runtime (user still needs to add it to YAML manually)
382 this->devices_[context.device_id] = context.device;
383
384 const auto capability_class = device_capability_class(context.device.type);
385 const char *platform = pairing_platform_name(capability_class);
386 const std::string type_diag = format_device_type_diagnostic(context.device.type);
387 const std::string type_yaml = format_device_type_for_yaml(context.device.type);
388 std::string extra_lines;
389
390 if (platform == nullptr) {
391 if (context.discovery_metadata_complete) {
392 ESP_LOGW(TAG,
393 "Device %s paired successfully, but this repo does not yet expose an ESPHome platform for type=%s "
394 "class=%s subtype=%u.",
395 context.device_id.c_str(), type_diag.c_str(), device_capability_class_name(context.device.type),
396 context.device.subtype);
397 ESP_LOGW(TAG,
398 "No ready-to-paste YAML was generated. If you want to experiment manually, choose the most likely "
399 "platform and set io_device_type: %s.",
400 type_yaml.c_str());
401 ESP_LOGW(TAG, "Please file a GitHub issue with this device type, subtype, model, and the pairing log so support "
402 "can be added.");
403 } else {
404 ESP_LOGW(TAG, "Device %s paired successfully, but the discovery response did not include type/subtype metadata.",
405 context.device_id.c_str());
406 ESP_LOGW(TAG, "No ready-to-paste YAML was generated. Add the device ID to the correct cover/light/switch entry "
407 "manually and leave io_device_type/io_subtype unset for now.");
408 ESP_LOGW(TAG, "Please file a GitHub issue with the pairing log and device model so this discovery edge case can "
409 "be investigated.");
410 }
411
413 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
414 this->busy_ = false;
415 return true;
416 }
417
418 if (capability_class == DeviceCapabilityClass::COVER && context.device.inverted)
419 extra_lines += " invert_position: true\n";
420
421 std::string subtype_line;
422 if (context.discovery_metadata_complete) {
423 subtype_line = " io_subtype: " + std::to_string(context.device.subtype) + "\n";
424 }
425
426 ESP_LOGI(TAG,
427 "Device %s paired successfully! Add this to your YAML:\n"
428 " %s:\n"
429 " - platform: home_io_control\n"
430 " id: my_device\n"
431 " home_io_control_id: home_io_hub\n"
432 " io_device_id: \"%s\"\n"
433 " io_device_type: %s\n"
434 "%s"
435 "%s",
436 context.device_id.c_str(), platform, context.device_id.c_str(), type_yaml.c_str(), subtype_line.c_str(),
437 extra_lines.c_str());
438
439 if (!context.discovery_metadata_complete) {
440 ESP_LOGW(TAG, "This device did not report a subtype during discovery, so io_subtype was omitted. The controller "
441 "will try to learn it later at runtime.");
442 } else if (yaml_device_type_name(context.device.type) == nullptr) {
443 ESP_LOGW(TAG,
444 "This snippet uses the raw device type %s because the project does not yet expose a named YAML alias "
445 "for %s.",
446 type_yaml.c_str(), type_diag.c_str());
447 ESP_LOGW(TAG, "Please file a GitHub issue with this type, subtype, device model, and the pairing log so support "
448 "can be added.");
449 }
450
452 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
453 this->busy_ = false;
454 return true;
455}
456
457} // namespace home_io_control
458} // namespace esphome
std::map< std::string, IoDevice > devices_
Definition hub_core.h:441
bool send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq)
Main request/response exchange with retry and automatic authentication.
decisions::PairingDiscoveryDisposition run_discovery_phase_(pairing::PairingContext &context)
Phase 1: broadcast discovery (0x28) and wait for a device response (0x29).
void record_exchange_debug_(const char *stage, uint8_t tries, bool saw_challenge)
Definition hub_core.cpp:42
bool finalize_pairing_configuration_(pairing::PairingContext &context)
Phase 3: send SetConfig1 (0x6F) to finalize device configuration.
bool wait_for_key_challenge_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &challenge_frame, const uint8_t device_node_id[NODE_ID_SIZE])
Wait for a key-challenge (0x3C) from target device during pairing key exchange.
bool run_key_exchange_phase_(pairing::PairingContext &context)
Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
virtual bool discover_and_pair()
Discover and pair a device that is in pairing mode.
decisions::PairingDiscoveryDisposition wait_for_discovery_response_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &response_frame)
Wait for a discovery response (0x29) during pairing.
static void parse_device_from_discovery(const IoFrame &frame, IoDevice &device, std::string &device_id)
Parse a discovery response frame into device metadata and ID.
IO-Homecontrol ESPHome component — protocol controller.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal helpers shared by the hub implementation .cpp files.
Internal pairing-state model for hub‑owned discovery and key‑exchange flows.
PairingDiscoveryDisposition
Disposition during pairing discovery phase.
@ NO_RESPONSE
No packets received on the channel within timeout.
@ INVALID
Packets seen but none were valid discovery (0x29) frames.
PairingKeyChallengeDisposition classify_pairing_key_challenge(const IoFrame &candidate, const uint8_t device_id[NODE_ID_SIZE], const uint8_t controller_id[NODE_ID_SIZE])
Decide if a frame is a valid key-challenge (0x3C) during pairing key exchange.
PairingDiscoveryDisposition classify_pairing_discovery_response(const IoFrame &candidate)
Decide if a frame is a valid discovery response (0x29) during pairing.
uint32_t response_wait_slice_ms(uint32_t remaining_ms)
Slice remaining wait time into bounded intervals to allow frequency hopping.
constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS
Discovery wait window after sending 0x28.
constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS
Wait window for the device's 0x3C challenge.
PairingState
State machine for the three‑phase pairing flow.
Definition hub_pairing.h:41
@ REGISTER_DEVICE
Registering device in the runtime registry for the current boot.
Definition hub_pairing.h:49
@ TX_DISCOVER
Discovery broadcast (0x28) sent; awaiting device response.
Definition hub_pairing.h:43
@ WAIT_DISCOVER_RESPONSE
Listening for discovery response (0x29) from a device in pairing mode.
Definition hub_pairing.h:44
@ COMPLETE
Pairing completed successfully; device ready for use.
Definition hub_pairing.h:50
@ WAIT_KEY_CHALLENGE
Waiting for challenge (0x3C) from device as part of key transfer.
Definition hub_pairing.h:46
@ WAIT_KEY_CONFIRM
Waiting for key‑confirm (0x33) from device (key receipt acknowledgement).
Definition hub_pairing.h:48
@ TX_KEY_INIT
Key‑init (0x31) sent to the discovered device.
Definition hub_pairing.h:45
@ TX_KEY_TRANSFER
Key‑transfer (0x32) sent with encrypted system key.
Definition hub_pairing.h:47
@ IDLE
No pairing in progress; idle state.
Definition hub_pairing.h:42
@ FAILED
Pairing failed (timeout, radio error, or protocol violation).
Definition hub_pairing.h:51
static constexpr uint8_t DEVICE_METADATA_SIZE
Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the low byt...
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_frame.h:81
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ UNKNOWN
Unknown/unspecified device.
@ WINDOW_OPENER
Window opening actuator.
@ HORIZONTAL_AWNING
Horizontal awning (open/close inverted).
@ ROLLING_DOOR_OPENER
Rolling door opener.
@ ON_OFF_SWITCH
Generic on/off switch.
@ SCREEN
Insect/privacy screen.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
static constexpr uint8_t CMD_KEY_CONFIRM
Device confirms key was received.
bool create_set_config1(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a set-config command (0x6F) to tell the device to automatically send status updates when contro...
bool create_key_init(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build a key-init request (0x31) to start the pairing key exchange with a discovered device.
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode a protocol-packed device type from two metadata bytes.
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
Definition proto_frame.h:32
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
bool create_discover(IoFrame &f, const uint8_t *own)
Build a discovery broadcast (0x28).
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
@ COVER
Position‑controlled cover (shutter/blind/awning).
static constexpr uint16_t LONG_PREAMBLE
Preamble is a sequence of 0xAA bytes that precedes every frame.
Definition proto_frame.h:40
static const char *const TAG
Definition hub_core.cpp:35
bool create_key_transfer(IoFrame &f, IoFrame &old_frame, const uint8_t *dst, const uint8_t *src, const uint8_t key[AES_KEY_SIZE], const uint8_t challenge[HMAC_SIZE])
Build a key-transfer frame (0x32) containing the system key encrypted with the transfer key.
Command builders for the IO‑Homecontrol protocol.
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
DeviceType type
Device type (shutter, awning, etc.).
bool is_stopped
True if device is not moving.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t data_len
Actual length of data.
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.
Context object that lives for the duration of a single pairing attempt.
Definition hub_pairing.h:55
IoFrame req
Outbound frame buffer (reused across all phases).
Definition hub_pairing.h:58
PairingState state
Current state machine state.
Definition hub_pairing.h:56
IoFrame resp
Inbound frame buffer (holds key‑confirm response).
Definition hub_pairing.h:59
RadioRxPacket packet
Raw radio capture for the current phase.
Definition hub_pairing.h:62
IoDevice device
Resolved device metadata after discovery (node_id, type, subtype, etc.).
Definition hub_pairing.h:57
IoFrame key_init
Key‑init frame retained for key‑transfer IV derivation.
Definition hub_pairing.h:61
std::string device_id
Hex string representation of the paired node ID.
Definition hub_pairing.h:63
bool discovery_metadata_complete
True when discovery carried type/subtype bytes.
Definition hub_pairing.h:64
IoFrame rx
Raw RX frame during waiting phases (discovery, challenge, confirm).
Definition hub_pairing.h:60