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///
16/// Implements IOHomeControlComponent::discover_and_pair() and the three-phase
17/// helper methods: run_discovery_phase_(), run_key_exchange_phase_(),
18/// finalize_pairing_configuration_(), plus low-level waiters
19/// wait_for_discovery_response_() and wait_for_key_challenge_().
20/// Also contains parse_device_from_discovery() to extract device metadata from
21/// discovery frames.
22///
23/// Pairing is separated from the normal exchange path to keep authenticated
24/// command execution independent from the one-time key-establishment flow.
25
26namespace esphome {
27namespace home_io_control {
28
29namespace {
30
31const char *const TAG = "home_io_control";
32constexpr size_t DEVICE_TYPE_HEX_STRING_BUFFER_SIZE = 8; ///< Buffer for strings such as "0x11" plus terminator.
33
34/// Map PairingState enum to string for debug logging.
35const char *pairing_stage_name(pairing::PairingState state) {
36 switch (state) {
38 return "idle";
40 return "tx_discover";
42 return "wait_discover_response";
44 return "tx_key_init";
46 return "wait_key_challenge";
48 return "tx_key_transfer";
50 return "wait_key_confirm";
52 return "register_device";
54 return "complete";
56 default:
57 return "failed";
58 }
59}
60
61// response_wait_slice_ms provided by decisions namespace (hub_decisions.h)
62
63/// Check if frame is a 0x33 key‑confirm message.
64bool frame_is_key_confirm(const IoFrame &frame) { return frame.cmd == CMD_KEY_CONFIRM; }
65
66/// Log discovery‑phase failure reason based on disposition.
67///
68/// Called by `discover_and_pair()` when the discovery phase does not return
69/// ACCEPT. The messages distinguish between no traffic at all and traffic that
70/// was seen but no valid discovery response arrived.
71///
72/// @param disp Discovery disposition value (NO_RESPONSE or INVALID).
73void log_discovery_diagnostic(decisions::PairingDiscoveryDisposition disp) {
74 switch (disp) {
76 ESP_LOGW(TAG, "No device responded to discovery");
77 break;
79 ESP_LOGW(TAG, "No valid discovery response received");
80 break;
82 break;
83 }
84}
85
86/// Return the YAML-friendly device-type name when the schema exposes a symbolic alias.
87/// Types without a symbolic alias can still be configured via raw numeric values such as 0x11.
88const char *yaml_device_type_name(DeviceType type) {
89 switch (type) {
91 return "venetian_blind";
93 return "roller_shutter";
95 return "awning";
97 return "window_opener";
99 return "garage_opener";
101 return "light";
103 return "gate_opener";
105 return "rolling_door_opener";
106 case DeviceType::LOCK:
107 return "lock";
109 return "blind";
111 return "screen";
113 return "heating_temperature_interface";
115 return "on_off_switch";
117 return "horizontal_awning";
119 return "curtain_track";
121 return "intrusion_alarm";
122 default:
123 return nullptr;
124 }
125}
126
127/// Format a raw device type as hexadecimal for YAML and diagnostics.
128std::string format_device_type_hex(DeviceType type) {
129 char buf[DEVICE_TYPE_HEX_STRING_BUFFER_SIZE];
130 snprintf(buf, sizeof(buf), "0x%02X", static_cast<uint8_t>(type));
131 return std::string(buf);
132}
133
134/// Return a human-readable device type string including the raw numeric value.
135std::string format_device_type_diagnostic(DeviceType type) {
136 const char *name = device_type_name(type);
137 std::string raw = format_device_type_hex(type);
138 if (name != nullptr && strcmp(name, "unknown") != 0) {
139 return std::string(name) + " (" + raw + ")";
140 }
141 return raw;
142}
143
144/// Build the YAML value for io_device_type.
145/// Supported symbolic aliases stay readable; all other types fall back to raw hex.
146std::string format_device_type_for_yaml(DeviceType type) {
147 const char *name = yaml_device_type_name(type);
148 if (name != nullptr) {
149 return std::string("\"") + name + "\"";
150 }
151 return format_device_type_hex(type);
152}
153
154/// Map a supported capability class to the corresponding YAML platform name.
155const char *pairing_platform_name(DeviceCapabilityClass capability_class) {
156 switch (capability_class) {
158 return "cover";
160 return "light";
162 return "switch";
163 default:
164 return nullptr;
165 }
166}
167
168} // namespace
169
170// --- Pairing helpers ---
171
172/// Wait for a valid discovery response (0x29) within the timeout.
173///
174/// Listens for incoming packets and parses them. Frames that parse successfully
175/// are passed to `decisions::classify_pairing_discovery_response()`. Only a
176/// frame classified as ACCEPT (CMD_DISCOVER_RESP) returns true. All other
177/// frames are ignored until the deadline expires.
178///
179/// The function distinguishes between NO_RESPONSE (no packets seen at all) and
180/// INVALID (some packets seen but none were valid discovery responses).
182 RadioRxPacket &packet,
183 IoFrame &response_frame) {
184 bool saw_traffic = false;
185 const uint32_t deadline = millis() + timeout_ms;
186 while ((int32_t) (deadline - millis()) > 0) {
187 const uint32_t remaining_ms = deadline - millis();
188 const uint32_t slice = decisions::response_wait_slice_ms(remaining_ms);
189 if (!this->radio_->wait_for_packet(packet, slice))
190 continue;
191 saw_traffic = true;
192 if (!parse(packet.data, packet.len, response_frame))
193 continue;
194 auto disp = decisions::classify_pairing_discovery_response(response_frame);
196 return disp;
197 }
198 if (!saw_traffic)
201}
202
203/// Wait for a valid key‑challenge frame (0x3C from the target device).
204///
205/// During key exchange the device responds to our key‑init with a random 6‑byte
206/// challenge. This helper loops until such a frame is received and validated
207/// by `decisions::classify_pairing_key_challenge()` — it must be a 0x3C
208/// command, 6 bytes long, and come from the discovered device node ID.
210 IoFrame &challenge_frame,
211 const uint8_t device_node_id[NODE_ID_SIZE]) {
212 bool saw_traffic = false;
213 const uint32_t deadline = millis() + timeout_ms;
214 while ((int32_t) (deadline - millis()) > 0) {
215 const uint32_t remaining_ms = deadline - millis();
216 const uint32_t slice = decisions::response_wait_slice_ms(remaining_ms);
217 if (!this->radio_->wait_for_packet(packet, slice))
218 continue;
219 saw_traffic = true;
220 if (!parse(packet.data, packet.len, challenge_frame))
221 continue;
222 if (decisions::classify_pairing_key_challenge(challenge_frame, device_node_id, this->node_id_) !=
224 continue;
225 return true;
226 }
227 ESP_LOGW(TAG, saw_traffic ? "Key exchange: no valid challenge received" : "Key exchange: no challenge received");
228 return false;
229}
230
231/// Parse a discovery response frame into device metadata and ID.
232///
233/// Extracts node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
234/// The two-byte payload uses the shared packed device metadata layout defined in proto_frame.h.
235/// The inversion flag is derived from the type via `default_inverted_for_type()`.
237 std::string &device_id) {
238 memcpy(device.node_id, frame.src, NODE_ID_SIZE);
239 if (frame.data_len >= DEVICE_METADATA_SIZE) {
240 device.type = decode_packed_device_type(frame.data[0], frame.data[1]);
241 device.subtype = decode_packed_device_subtype(frame.data[1]);
242 device.inverted = default_inverted_for_type(device.type);
243 } else {
244 device.type = DeviceType::UNKNOWN;
245 device.subtype = 0;
246 device.inverted = false;
247 }
248 device.position = UNKNOWN_POSITION;
249 device.target = UNKNOWN_POSITION;
250 device.is_stopped = true;
251 device_id = node_id_to_string(device.node_id);
252}
253
254/// Execute Phase 1: discover a pairable device on channel 2.
255///
256/// Transmits a discovery broadcast (0x28) and waits up to the configured discovery timeout for a valid
257/// discovery response (0x29). On success the response is parsed into
258/// `context.device` and `context.device_id` and the function returns ACCEPT.
259/// On failure returns NO_RESPONSE (no packets) or INVALID (packets seen but
260/// none were valid discovery frames).
263 this->record_exchange_debug_(pairing_stage_name(context.state), 1, false);
264 if (!create_discover(context.req, this->node_id_) || !this->transmit_frame_(context.req, FREQ_CH2, LONG_PREAMBLE)) {
266 }
267
269 this->record_exchange_debug_(pairing_stage_name(context.state), 1, false);
270 auto result =
273 parse_device_from_discovery(context.rx, context.device, context.device_id);
275 }
276 return result;
277}
278
279/// Execute Phase 2: perform authenticated key exchange with the discovered device.
280///
281/// Performs the full four‑step key exchange:
282/// 1. TX_KEY_INIT — send key‑init frame (0x31) to device
283/// 2. WAIT_KEY_CHALLENGE — wait for device's challenge (0x3C)
284/// 3. TX_KEY_TRANSFER — send challenge response + our key share (0x32)
285/// 4. WAIT_KEY_CONFIRM — wait for device's key‑confirm (0x33) using the
286/// normal authenticated exchange (`send_and_receive_`) to inherit retry logic.
287///
288/// Each step updates the pairing state for observability. Any failure returns
289/// false immediately; the orchestrator will clean up and abort pairing.
292 this->record_exchange_debug_(pairing_stage_name(context.state), 1, false);
293 if (!create_key_init(context.key_init, this->node_id_, context.device.node_id) ||
294 !this->transmit_frame_(context.key_init, FREQ_CH2, LONG_PREAMBLE)) {
295 return false;
296 }
297
299 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
301 context.device.node_id)) {
302 return false;
303 }
304
306 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
307 if (!create_key_transfer(context.req, context.key_init, context.device.node_id, this->node_id_, this->system_key_,
308 context.rx.data)) {
309 return false;
310 }
311
313 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
314 if (!this->send_and_receive_(context.req, context.resp, FREQ_CH2) || !frame_is_key_confirm(context.resp)) {
315 ESP_LOGW(TAG, "Key exchange failed");
316 return false;
317 }
318 return true;
319}
320
321/// Execute Phase 3: finalize pairing configuration on the device.
322///
323/// Sends a SetConfig1 command (0x6F) using the authenticated exchange path.
324/// This step is optional in the sense that the command may be skipped if
325/// `create_set_config1()` returns false, but the pairing itself is already
326/// complete at this point. The function always returns true to avoid aborting
327/// a successfully paired device.
329 if (create_set_config1(context.req, this->node_id_, context.device.node_id))
330 this->send_and_receive_(context.req, context.resp, FREQ_CH2);
331 return true;
332}
333
334/// Pairing orchestrator — high‑level three‑phase flow.
335///
336/// Phase 1: Discovery (run_discovery_phase_) finds a device in pairing mode.
337/// Phase 2: Key exchange (run_key_exchange_phase_) performs authenticated
338/// key establishment using the challenge‑response protocol.
339/// Phase 3: Finalization (finalize_pairing_configuration_) sends SetConfig1.
340///
341/// On success the device is added to the current runtime registry and either a valid
342/// YAML snippet or a follow-up guidance message is printed in the logs. Any phase
343/// failure aborts early, logging an appropriate warning.
344///
345/// @return true if pairing completed; false otherwise.
347 if (!this->initialized_)
348 return false;
349 ESP_LOGI(TAG, "Starting device discovery...");
350
351 this->busy_ = true;
353
354 // Phase 1: Discovery — find a pairable device
355 auto disc_disp = this->run_discovery_phase_(context);
357 log_discovery_diagnostic(disc_disp);
358 this->busy_ = false;
359 return false;
360 }
361
362 // Phase 2: Key exchange — establish shared system key
363 if (!this->run_key_exchange_phase_(context)) {
364 this->busy_ = false;
365 return false;
366 }
367
368 // Phase 3: Final configuration — best-effort SetConfig1
369 this->finalize_pairing_configuration_(context);
370
372 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
373
374 // Register device in runtime (user still needs to add it to YAML manually)
375 this->devices_[context.device_id] = context.device;
376
377 const auto capability_class = device_capability_class(context.device.type);
378 const char *platform = pairing_platform_name(capability_class);
379 const std::string type_diag = format_device_type_diagnostic(context.device.type);
380 const std::string type_yaml = format_device_type_for_yaml(context.device.type);
381 std::string extra_lines;
382
383 if (platform == nullptr) {
384 if (context.discovery_metadata_complete) {
385 ESP_LOGW(TAG,
386 "Device %s paired successfully, but this repo does not yet expose an ESPHome platform for type=%s "
387 "class=%s subtype=%u.",
388 context.device_id.c_str(), type_diag.c_str(), device_capability_class_name(context.device.type),
389 context.device.subtype);
390 ESP_LOGW(TAG,
391 "No ready-to-paste YAML was generated. If you want to experiment manually, choose the most likely "
392 "platform and set io_device_type: %s.",
393 type_yaml.c_str());
394 ESP_LOGW(TAG, "Please file a GitHub issue with this device type, subtype, model, and the pairing log so support "
395 "can be added.");
396 } else {
397 ESP_LOGW(TAG, "Device %s paired successfully, but the discovery response did not include type/subtype metadata.",
398 context.device_id.c_str());
399 ESP_LOGW(TAG, "No ready-to-paste YAML was generated. Add the device ID to the correct cover/light/switch entry "
400 "manually and leave io_device_type/io_subtype unset for now.");
401 ESP_LOGW(TAG, "Please file a GitHub issue with the pairing log and device model so this discovery edge case can "
402 "be investigated.");
403 }
404
406 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
407 this->busy_ = false;
408 return true;
409 }
410
411 if (capability_class == DeviceCapabilityClass::COVER && context.device.inverted)
412 extra_lines += " invert_position: true\n";
413
414 std::string subtype_line;
415 if (context.discovery_metadata_complete) {
416 subtype_line = " io_subtype: " + std::to_string(context.device.subtype) + "\n";
417 }
418
419 ESP_LOGI(TAG,
420 "Device %s paired successfully! Add this to your YAML:\n"
421 " %s:\n"
422 " - platform: home_io_control\n"
423 " id: my_device\n"
424 " home_io_control_id: home_io_hub\n"
425 " io_device_id: \"%s\"\n"
426 " io_device_type: %s\n"
427 "%s"
428 "%s",
429 context.device_id.c_str(), platform, context.device_id.c_str(), type_yaml.c_str(), subtype_line.c_str(),
430 extra_lines.c_str());
431
432 if (!context.discovery_metadata_complete) {
433 ESP_LOGW(TAG, "This device did not report a subtype during discovery, so io_subtype was omitted. The controller "
434 "will try to learn it later at runtime.");
435 } else if (yaml_device_type_name(context.device.type) == nullptr) {
436 ESP_LOGW(TAG,
437 "This snippet uses the raw device type %s because the project does not yet expose a named YAML alias "
438 "for %s.",
439 type_yaml.c_str(), type_diag.c_str());
440 ESP_LOGW(TAG, "Please file a GitHub issue with this type, subtype, device model, and the pairing log so support "
441 "can be added.");
442 }
443
445 this->record_exchange_debug_(pairing_stage_name(context.state), 1, true);
446 this->busy_ = false;
447 return true;
448}
449
450} // namespace home_io_control
451} // namespace esphome
std::map< std::string, IoDevice > devices_
Definition hub_core.h:383
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:41
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:40
@ REGISTER_DEVICE
Registering device in the runtime registry for the current boot.
Definition hub_pairing.h:48
@ TX_DISCOVER
Discovery broadcast (0x28) sent; awaiting device response.
Definition hub_pairing.h:42
@ WAIT_DISCOVER_RESPONSE
Listening for discovery response (0x29) from a device in pairing mode.
Definition hub_pairing.h:43
@ COMPLETE
Pairing completed successfully; device ready for use.
Definition hub_pairing.h:49
@ WAIT_KEY_CHALLENGE
Waiting for challenge (0x3C) from device as part of key transfer.
Definition hub_pairing.h:45
@ WAIT_KEY_CONFIRM
Waiting for key‑confirm (0x33) from device (key receipt acknowledgement).
Definition hub_pairing.h:47
@ TX_KEY_INIT
Key‑init (0x31) sent to the discovered device.
Definition hub_pairing.h:44
@ TX_KEY_TRANSFER
Key‑transfer (0x32) sent with encrypted system key.
Definition hub_pairing.h:46
@ IDLE
No pairing in progress; idle state.
Definition hub_pairing.h:41
@ FAILED
Pairing failed (timeout, radio error, or protocol violation).
Definition hub_pairing.h:50
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:80
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:31
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:39
static const char *const TAG
Definition hub_core.cpp:34
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:54
IoFrame req
Outbound frame buffer (reused across all phases).
Definition hub_pairing.h:57
PairingState state
Current state machine state.
Definition hub_pairing.h:55
IoFrame resp
Inbound frame buffer (holds key‑confirm response).
Definition hub_pairing.h:58
RadioRxPacket packet
Raw radio capture for the current phase.
Definition hub_pairing.h:61
IoDevice device
Resolved device metadata after discovery (node_id, type, subtype, etc.).
Definition hub_pairing.h:56
IoFrame key_init
Key‑init frame retained for key‑transfer IV derivation.
Definition hub_pairing.h:60
std::string device_id
Hex string representation of the paired node ID.
Definition hub_pairing.h:62
bool discovery_metadata_complete
True when discovery carried type/subtype bytes.
Definition hub_pairing.h:63
IoFrame rx
Raw RX frame during waiting phases (discovery, challenge, confirm).
Definition hub_pairing.h:59