Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
pairing_engine.h
Go to the documentation of this file.
1#pragma once
2
3/// @file pairing_engine.h
4/// @brief Device discovery and key-exchange engine for IO-Homecontrol pairing.
5/// @ingroup hioc_hub
6///
7/// PairingEngine encapsulates all three phases of the IO-Homecontrol pairing flow:
8///
9/// Phase 1 — Discovery (0x28 → 0x29 → 0x2C → 0x2D):
10/// Controller broadcasts a discovery packet. A device in pairing mode responds
11/// with its node ID and type/subtype metadata. The controller then sends a discover-confirm
12/// (0x2C) directly to that device and, when tuning allows, waits up to
13/// `PAIRING_DISCOVER_CONFIRM_TRIES` × `PAIRING_DISCOVER_CONFIRM_ACK_TIMEOUT_MS` for its 0x2D —
14/// every real controller in this project's corpus does this before proceeding; see
15/// `pairing_discover_confirm` (tuning_config.h) for the mode knob and `run_discover_confirm_step_()`
16/// for why the step never fails a pairing attempt.
17///
18/// Phase 2 — Authenticated Key Exchange (0x31 → 0x3C → 0x32 → 0x33):
19/// The controller sends CMD_KEY_INIT (0x31). The device challenges with 0x3C.
20/// The controller proves knowledge of the system key and simultaneously
21/// transfers the encrypted system key (0x32). The device confirms with 0x33.
22///
23/// Phase 3 — Configuration (0x6F):
24/// The controller sends SetConfig1 to enable automatic device status updates.
25///
26/// The engine is constructed once by IOHomeControlComponent. It holds double-pointer
27/// indirection for the radio driver so test assignments (`comp.radio_ = &mock`) propagate
28/// without calling setup(). All radio operations go through ExchangeEngine so that
29/// LBT, preamble selection, and frequency hopping are centralised.
30///
31/// PairingEngine is non-copyable and non-movable because it stores pointer and reference
32/// addresses that would dangle in a copy.
33
34#include "proto_frame.h"
35#include "proto_codecs.h"
36#include "roll_call.h"
37#include "hub_pairing.h"
38#include "hub_decisions.h"
39#include "exchange_engine.h"
40#include "device_registry.h"
41#include "pairing_advisor.h"
42#include "pairing_telemetry.h"
43#include "radio_interface.h"
44#include "tuning_config.h"
45
46#include <cstdint>
47#include <string>
48
49namespace esphome {
50namespace home_io_control {
51
52/// @name Pairing timing constants
53/// Timeouts and retry limits for the pairing flow's blocking waits.
54///@{
55inline constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS = 2000; ///< Discovery wait window after sending 0x28.
56inline constexpr uint8_t PAIRING_DISCOVERY_MAX_ATTEMPTS = 3; ///< Retry discovery TX up to this many times.
57inline constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS = 500; ///< Wait window for the device's 0x3C challenge.
58inline constexpr uint32_t PAIRING_KEY_CONFIRM_TIMEOUT_MS = 500; ///< Wait for 0x33 key confirm after sending 0x32.
59inline constexpr uint8_t PAIRING_DISCOVER_CONFIRM_TRIES = 3; ///< Max tries for the discover-confirm (0x2C) step.
60inline constexpr uint32_t PAIRING_DISCOVER_CONFIRM_ACK_TIMEOUT_MS = 1500; ///< Wait window per discover-confirm try.
61/// Tries for the authenticated name read that checks whether a device took our key although the
62/// key exchange did not confirm. Two, not EXCHANGE_RETRY_COUNT: the device answered moments ago, so
63/// one retry covers a lost frame, and the check runs only on an attempt that is already failing.
64inline constexpr uint8_t PAIRING_KEY_VERIFY_MAX_TRIES = 2;
65/// Wait for the device's key transfer (0x32) after our key transfer request (0x38). Same window as
66/// the 0x33 wait: both are a unicast reply to a unicast pairing frame.
68/// How recent a RecentOneWayPairingSighting has to be, relative to discover_and_pair() starting,
69/// to still count as evidence for this attempt. Generous relative to the doc's "a few seconds"
70/// PROG-then-press guidance: real field reports (issue #27) show gaps up to ~4-7 s between the
71/// PROG gesture and pressing "Discover & Pair" in the app, so a tight window would reintroduce
72/// the same miss it's meant to fix. Not tied to ONEWAY_QUIET_PERIOD_MS (status_poll_policy.h,
73/// 700 ms) — that constant is about collapsing one remote's repeat burst, a different, much
74/// shorter timescale than "how long ago did the user press PROG."
75inline constexpr uint32_t PAIRING_RECENT_ONE_WAY_SIGHTING_WINDOW_MS = 15000;
76///@}
77
78/// Owns and drives all three phases of the IO-Homecontrol device pairing flow.
79///
80/// Constructed once by IOHomeControlComponent; collaborators (radio, exchange engine,
81/// device registry) are injected as pointers/references so the engine never outlives them.
82/// @ingroup hioc_hub
84 public:
85 /// @brief What run_key_pull_phase_() ended with.
86 enum class KeyPullResult : uint8_t {
87 FAILED, ///< No key received, refused, or a different key the device did not prove (not printed).
88 OUR_KEY, ///< The device sent the hub's own key: it is paired with this hub.
89 OTHER_KEY, ///< The device sent a different key and proved it holds it (`context.pulled_key`).
90 };
91
92 /// Construct the engine with all required collaborators.
93 ///
94 /// @param radio_ptr Double pointer into the hub's `radio_` member — survives driver replacement in tests.
95 /// @param node_id Controller 3-byte node ID buffer, owned by the hub.
96 /// @param system_key 16-byte AES system key buffer, owned by the hub.
97 /// @param tuning Tuning configuration, owned by the hub.
98 /// @param engine Shared exchange engine for transmit/receive operations.
99 /// @param registry Device registry where paired devices are permanently registered.
100 /// @param telemetry Per-attempt telemetry recorder, owned by the hub.
101 /// @param recent_oneway_sighting Most recent 1W pairing-gesture sighting from the hub's normal
102 /// passive RX path, owned by the hub; see RecentOneWayPairingSighting.
103 PairingEngine(RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning,
104 ExchangeEngine &engine, DeviceRegistry &registry, PairingTelemetry &telemetry,
105 const RecentOneWayPairingSighting &recent_oneway_sighting);
106
107 /// Non-copyable — stores double-pointer and references into hub member addresses.
108 PairingEngine(const PairingEngine &) = delete;
110
111 /// Discover and pair a device currently in pairing mode (three-phase orchestrator).
112 /// @return true if all three phases completed successfully; false otherwise.
113 bool discover_and_pair();
114
115 /// Extract node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
116 /// @return The decoded extended discovery fields (manufacturer / Multi Information Byte / length
117 /// flags), so a caller can read the self-reported power class without decoding twice.
119 std::string &device_id);
120
121 protected:
122 // --- Phase helpers (protected; exposed to tests via TestablePairingEngine in test_helpers.h) ---
123
124 /// Phase 1: broadcast discovery command(s) and wait for a device response (0x29).
125 /// @param context Pairing context updated on success.
126 /// @return ACCEPT on success; NO_RESPONSE or INVALID otherwise.
128
129 /// @brief Discover-confirm step (0x2C → 0x2D): sent directly to the just-discovered device,
130 /// once per discover_and_pair() attempt, between discovery and the key-exchange retry loop.
131 ///
132 /// Every real controller in this project's corpus sends 0x2C here. The step **never fails a
133 /// pairing attempt**: a refusal, timeout, or `skip` tuning mode all still let the caller proceed
134 /// to `run_key_exchange_phase_()` — only the result and log line differ. Applies the
135 /// `pairing_key_init_delay_ms` pause afterward for every outcome except `SKIPPED` (see
136 /// `tuning_config.h`'s `pairing_discover_confirm`/`pairing_key_init_delay_ms` doc for the modes
137 /// and defaults).
138 /// @param context Pairing context populated by run_discovery_phase_(); `context.req`/`context.rx`
139 /// are reused as scratch space for the 0x2C/0x2D exchange, same as the other phases.
140 /// @return What the step actually observed — see @ref pairing::DiscoverConfirmResult. Not stored
141 /// in `context`: nothing downstream reads it.
143
144 /// Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
145 /// @param context Pairing context populated by run_discovery_phase_().
146 /// @return true if key exchange completes; false on any failure.
148
149 /// Phase 3: send SetConfig1 (0x6F) once; optional. The key exchange's 0x33 already completed
150 /// the pairing, so neither the attempt's outcome nor its telemetry depends on this step: no
151 /// device on record accepts the frame (see PAIRING_SET_CONFIG1_MAX_TRIES). An unanswered try
152 /// is logged as harmless so the preceding "no first response" line isn't read as a failure.
153 /// @param context Pairing context with device information from phases 1 and 2.
155
156 /// Wait for a discovery response (0x29) within timeout_ms with per-chip frequency hopping.
157 /// @param timeout_ms Maximum wait window.
158 /// @param packet Output: raw RadioRxPacket of the accepted discovery frame.
159 /// @param response_frame Output: parsed IoFrame of the accepted discovery frame.
160 /// @return ACCEPT on success; NO_RESPONSE (no traffic) or INVALID (wrong frames) otherwise.
162 IoFrame &response_frame);
163
164 /// Wait for a key-challenge (0x3C) or direct key-confirm (0x33) from the target device.
165 /// @param timeout_ms Maximum wait window.
166 /// @param packet Output: raw RadioRxPacket of the accepted frame.
167 /// @param challenge_frame Output: parsed IoFrame.
168 /// @param device_node_id Expected source node ID (devices paired to).
169 /// @return true if a valid challenge or confirm was received; false on timeout.
170 bool wait_for_key_challenge_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &challenge_frame,
171 const uint8_t device_node_id[NODE_ID_SIZE]);
172
173 /// Wait for one discover-confirm (0x2C) try's answer: a matching 0x2D, a matching
174 /// CMD_ERROR_RESP, or nothing recognisable before the window closes.
175 ///
176 /// Listen policy alternates by try: `discover_confirm_try_rotates(try_index)` selects
177 /// `ListenPolicy::ROTATE_ALL_CHANNELS` for the one try that hedges against an off-channel 0x2D,
178 /// and `ListenPolicy::HOLD_REQUEST_CHANNEL` (matching every other unicast pairing wait) for the
179 /// rest — see that decision's doc for why only one try rotates.
180 /// @param try_index 1-based try number, used only to pick the listen policy.
181 /// @param context Pairing context; `context.req` is the 0x2C just transmitted, `context.rx`/
182 /// `context.packet` receive the candidate reply.
183 /// @return ACK or ERROR for a matching reply; IGNORE only once the window closes with nothing
184 /// recognised — an unrelated frame arriving mid-window does not end the try, it keeps
185 /// listening.
187 pairing::PairingContext &context);
188
189 /// Transmit the 0x32 key transfer and wait for the 0x33 key confirm with retry.
190 ///
191 /// A device that challenges the key transfer (slow-turnaround chips answering with a fresh 0x3C
192 /// instead of confirming directly) is handled inline via `listen_for_key_confirm_()` and
193 /// `ExchangeEngine::answer_challenge()` — see their docs. An explicit refusal (REFUSE: a wrong
194 /// reply shape, or CMD_ERROR_RESP) must not spend the remaining retries; a challenge is not a
195 /// refusal, so it does not return false either — the try only ends without confirming.
197
198 /// Build CMD_KEY_TRANSFER against the current challenge and wait for the 0x33 confirm; see
199 /// run_key_exchange_phase_()'s doc comment for why this is a separate, replayable step.
200 /// @param context Pairing context; `context.rx.data` supplies the challenge bytes, `context.req`
201 /// is filled with the outbound 0x32, `context.resp` with the inbound 0x33 on success.
202 /// @return true if the device confirmed the key.
204
205 /// @brief Check whether the device took our key although the key exchange did not confirm it.
206 ///
207 /// A device can accept a key transfer whose 0x33 we never hear, then refuse the next one, so an
208 /// unconfirmed key exchange is not proof of failure. Sends one authenticated name read (0x50).
209 /// Only a device that challenges the read and then answers it with its name (0x51) has proven it
210 /// verifies our MAC with our key. A device that answers without challenging proves nothing, and
211 /// counts as "not held".
212 /// @param context Pairing context; `context.req`/`context.resp` are reused as scratch.
213 /// @return true if the device answered an authenticated request with our key.
215
216 /// @brief Key pull (`pairing_key_transfer: pull`, or `auto` for a remote controller): 0x38 ->
217 /// device's 0x32 -> our 0x3C -> device's 0x3D.
218 ///
219 /// Sends a key transfer request with 6 random bytes, waits for the device's 0x32 and decrypts the
220 /// key (recover_key_from_pull()). The log gets one "Key pull check" line with the key's check value,
221 /// never the key, so two attempts or two devices can be compared from the log alone.
222 ///
223 /// The hub's own key is accepted as is: no decrypt produces it by chance. A different key must be
224 /// proven by the device's 0x3D to our challenge (ExchangeEngine::authenticate_request() under that
225 /// key, up to EXCHANGE_RETRY_COUNT challenges while no 0x3D comes back; a 0x3D that does not verify ends
226 /// it); only then does it become `context.pulled_key`, for register_pulled_key_devices_(). The hub's
227 /// own key is never replaced. An unproven key is not printed. Hardware-validated on an Atlantic
228 /// AGILIA IO radiator (issue #165).
229 /// @param context Pairing context populated by run_discovery_phase_().
230 /// @return See KeyPullResult.
232
233 /// @brief Which direction this attempt moves the key in.
234 ///
235 /// `pull` and `push` force it; `auto` pulls when the device answered discovery as a remote
236 /// controller (node type 1023) and pushes otherwise. Logs the `auto` decision.
237 /// @param context Pairing context populated by run_discovery_phase_().
238 /// @return true to pull the device's key, false to push the hub's.
239 [[nodiscard]] bool key_pull_selected_(const pairing::PairingContext &context) const;
240
241 /// @brief Find the devices that hold `key`, log one report, register the ones that can be controlled.
242 ///
243 /// Asks the heater at the next address first (probe_heater_at_next_address_()); otherwise runs one
244 /// roll-call sweep (run_roll_call_sweep()) with `key`. Registers every device found that has a platform
245 /// and no YAML entry, until reboot (IoDevice::registered_by_pairing); a record an earlier pairing in this
246 /// boot added is replaced. With a device key the report is build_pulled_key_report() (device log
247 /// only, one banner, the responders carry the key as their own `system_key`); with the hub key it is
248 /// build_hub_key_pull_report() (no key, no banner, the responders use the hub key).
249 /// @param context Pairing context of the attempt.
250 /// @param key AES_KEY_SIZE bytes the roll-call is built with.
251 /// @param device_key true when `key` is a key the device handed over, false for the hub's own.
252 /// @return How many devices were registered.
253 uint8_t roll_call_and_register_(pairing::PairingContext &context, const uint8_t *key, bool device_key);
254
255 /// @brief Ask the heater at the address after an Atlantic pairing node for its maximum setpoint.
256 ///
257 /// Runs only for a node decisions::pairing_node_has_heater_at_next_address() names, and only when the
258 /// YAML declares no device at that address. One authenticated register read under `key` (the full
259 /// retry budget); an answer means the electrical heater with an adjustable setpoint is there, and it
260 /// becomes the only entry of `state`, as if a roll-call had found it. Leaves the registry as it was.
261 /// @param context Pairing context of the attempt (the discovered node and its manufacturer).
262 /// @param key AES_KEY_SIZE bytes the read is signed with.
263 /// @param device_key true when `key` is a key the device handed over (the entry uses key slot 1).
264 /// @param[out] state Filled with the heater when it answered; untouched otherwise.
265 /// @return true when the heater answered.
266 bool probe_heater_at_next_address_(const pairing::PairingContext &context, const uint8_t *key, bool device_key,
267 ScanState &state);
268
269 /// @brief Record a pairing that ends with devices found by roll-call: `paired`, `node=` unchanged.
271
272 /// @brief Pull the key and end the attempt wherever the pull decides it.
273 ///
274 /// A foreign key goes to register_pulled_key_devices_(); a failed pull ends the attempt as refused or
275 /// failed. The hub's own key from a node no platform drives looks for the devices that hold it and
276 /// ends as `paired`, or as `hub_key_returned` with none found. The hub's own key from a device with a
277 /// platform is left to the caller, which pairs it as after a push.
278 /// @param context Pairing context populated by run_discovery_phase_().
279 /// @param[out] paired The attempt's result when it ended here.
280 /// @return true when the attempt ended here; false when the caller continues with the push path's ending.
281 bool run_pull_and_finish_(pairing::PairingContext &context, bool &paired);
282
283 /// @brief After a proven pull of a different key: find the devices that hold it, then end the attempt.
284 ///
285 /// At least one registered responder ends the attempt as `paired`; none ends it as `key_received`.
286 /// Zeroes the pulled key afterwards.
287 /// @param context Pairing context holding the pulled key (`has_pulled_key`).
288 /// @return true when the attempt ended as `paired`.
290
291 private:
292 /// Preamble for a directed pairing start frame (0x2C, 0x31, 0x6F) to the device discovery just
293 /// found: the shorter of ExchangeEngine::request_preamble_for()'s rule and the preamble the
294 /// discovery request went out with (`pairing_discovery_preamble`).
295 ///
296 /// The device answered a discovery request carrying that preamble moments ago, so it is
297 /// listening and demonstrably hears it; pairing follows within seconds. The long wake-up
298 /// preamble the rule would otherwise pick for a low-power target (and that 0x31/0x6F always
299 /// carry) is not just unnecessary then: some VELUX receivers never detect a frame behind a
300 /// 1024-byte preamble at all (ADR 0029). With the default discovery preamble (LONG_PREAMBLE)
301 /// the result equals the rule, so default setups transmit exactly what they did before.
302 /// @param frame The start frame about to be sent.
303 /// @return Preamble length in bytes.
304 [[nodiscard]] uint16_t pairing_start_preamble_(const IoFrame &frame) const;
305
306 /// One `ListenPolicy::HOLD_REQUEST_CHANNEL` listen for the key-transfer confirm wait, called up
307 /// to twice per try in `wait_for_key_confirm_()` — once for the initial reply, once more (a
308 /// fresh window) after answering a device challenge — so the listen spec, logging, and telemetry
309 /// have exactly one owner instead of two copies of the same lambda.
310 /// @param context Pairing context; `context.req` is the outbound 0x32 (the challenge-response
311 /// transcript), `context.resp` receives the candidate reply.
312 /// @param try_number 1-based try number, for the timeout log line only.
313 /// @param after_challenge True if this is the second, post-challenge listen within the try
314 /// (labelled in the timeout log line so it isn't mistaken for the first).
315 /// @return CONFIRM/CHALLENGE/REFUSE for a matching reply; IGNORE on a timeout. A frame from the
316 /// wrong endpoints does not end the listen — it is ignored and the wait continues.
317 decisions::PairingKeyConfirmDisposition listen_for_key_confirm_(pairing::PairingContext &context, uint8_t try_number,
318 bool after_challenge);
319
320 /// Convenience accessor returning the current radio driver (dereferences double pointer).
321 [[nodiscard]] RadioDriver *radio_() const { return *radio_ptr_; }
322
323 /// Record the final outcome, detach telemetry from the exchange engine, and log the
324 /// end-of-attempt summary. Called once at every discover_and_pair() exit point.
325 /// @param outcome Final disposition of this attempt.
326 void finish_pairing_attempt_(PairingOutcome outcome);
327
328 /// Record an RX or RX_REJECT telemetry event for a discovery-response candidate frame.
329 /// Factored out of wait_for_discovery_response_() purely to keep that function's cognitive
330 /// complexity under the clang-tidy threshold — no behavior beyond the telemetry call.
331 /// @param frame Parsed candidate frame.
332 /// @param accepted true if the frame was classified as a valid discovery response.
333 /// @param rssi RSSI of the captured frame.
334 void record_discovery_rx_telemetry_(const IoFrame &frame, bool accepted, int16_t rssi);
335
336 /// Record a CMD_ERROR_RESP answer to the key step: context flag, telemetry, and a decoded log line.
337 /// @param context Pairing context to mark as refused.
338 /// @param reply The device's CMD_ERROR_RESP frame.
339 /// @param step Log label for the refused step (e.g. "Key transfer").
340 void note_key_refusal_(pairing::PairingContext &context, const IoFrame &reply, const char *step);
341
342 /// Wait for the device's 0x32 answer to our 0x38, holding the request channel.
343 /// @param context Pairing context; `context.rx` receives the accepted 0x32.
344 /// @return true if a well-formed 0x32 from the device arrived; false on timeout or refusal
345 /// (a refusal also sets `context.key_refused`).
346 bool wait_for_pulled_key_(pairing::PairingContext &context);
347
348 /// Run the push key exchange with its retry loop, then the key-held check if it did not confirm.
349 /// @param context Pairing context populated by the discovery phase.
350 /// @return true if the device holds our key (confirmed, or proven by verify_device_holds_key_()).
351 bool run_push_key_step_(pairing::PairingContext &context);
352
353 RadioDriver **radio_ptr_;
354 const uint8_t *node_id_;
355 const uint8_t *system_key_;
356 const TuningConfig *tuning_;
357 ExchangeEngine &engine_;
358 DeviceRegistry &registry_;
359 PairingTelemetry &telemetry_;
360 const RecentOneWayPairingSighting &recent_oneway_sighting_;
361};
362
363} // namespace home_io_control
364} // namespace esphome
Owns the per-hub device table, update callbacks, and linked-remote associations.
decisions::PairingDiscoveryDisposition run_discovery_phase_(pairing::PairingContext &context)
Phase 1: broadcast discovery command(s) and wait for a device response (0x29).
bool probe_heater_at_next_address_(const pairing::PairingContext &context, const uint8_t *key, bool device_key, ScanState &state)
Ask the heater at the address after an Atlantic pairing node for its maximum setpoint.
bool key_pull_selected_(const pairing::PairingContext &context) const
Which direction this attempt moves the key in.
static DiscoveryResponseInfo parse_device_from_discovery(const IoFrame &frame, IoDevice &device, std::string &device_id)
Extract node ID, device type, and subtype from a CMD_DISCOVER_RESP frame.
bool register_pulled_key_devices_(pairing::PairingContext &context)
After a proven pull of a different key: find the devices that hold it, then end the attempt.
decisions::PairingDiscoverConfirmDisposition wait_for_discover_confirm_ack_(uint8_t try_index, pairing::PairingContext &context)
Wait for one discover-confirm (0x2C) try's answer: a matching 0x2D, a matching CMD_ERROR_RESP,...
bool run_pull_and_finish_(pairing::PairingContext &context, bool &paired)
Pull the key and end the attempt wherever the pull decides it.
PairingEngine & operator=(const PairingEngine &)=delete
bool run_key_exchange_phase_(pairing::PairingContext &context)
Phase 2: authenticated key exchange (0x31 → 0x3C → 0x32 → 0x33).
bool wait_for_key_confirm_(pairing::PairingContext &context)
Transmit the 0x32 key transfer and wait for the 0x33 key confirm with retry.
PairingEngine(const PairingEngine &)=delete
Non-copyable — stores double-pointer and references into hub member addresses.
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) or direct key-confirm (0x33) from the target device.
bool discover_and_pair()
Discover and pair a device currently in pairing mode (three-phase orchestrator).
void finish_paired_by_roll_call_(pairing::PairingContext &context)
Record a pairing that ends with devices found by roll-call: paired, node= unchanged.
uint8_t roll_call_and_register_(pairing::PairingContext &context, const uint8_t *key, bool device_key)
Find the devices that hold key, log one report, register the ones that can be controlled.
bool transfer_key_and_wait_confirm_(pairing::PairingContext &context)
Build CMD_KEY_TRANSFER against the current challenge and wait for the 0x33 confirm; see run_key_excha...
decisions::PairingDiscoveryDisposition wait_for_discovery_response_(uint32_t timeout_ms, RadioRxPacket &packet, IoFrame &response_frame)
Wait for a discovery response (0x29) within timeout_ms with per-chip frequency hopping.
KeyPullResult
What run_key_pull_phase_() ended with.
KeyPullResult run_key_pull_phase_(pairing::PairingContext &context)
Key pull (pairing_key_transfer: pull, or auto for a remote controller): 0x38 -> device's 0x32 -> our ...
pairing::DiscoverConfirmResult run_discover_confirm_step_(pairing::PairingContext &context)
Discover-confirm step (0x2C → 0x2D): sent directly to the just-discovered device, once per discover_a...
bool verify_device_holds_key_(pairing::PairingContext &context)
Check whether the device took our key although the key exchange did not confirm it.
void finalize_pairing_configuration_(pairing::PairingContext &context)
Phase 3: send SetConfig1 (0x6F) once; optional.
PairingEngine(RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning, ExchangeEngine &engine, DeviceRegistry &registry, PairingTelemetry &telemetry, const RecentOneWayPairingSighting &recent_oneway_sighting)
Construct the engine with all required collaborators.
Fixed-size per-attempt telemetry recorder for the pairing flow.
Abstract radio driver for IO-Homecontrol.
Per-hub device table, update-callback fan-out, and linked-remote map.
Self-contained authenticated exchange engine for IO-Homecontrol 2W.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal pairing-state model for hub‑owned discovery and key‑exchange flows.
PairingDiscoveryDisposition
Disposition during pairing discovery phase.
PairingKeyConfirmDisposition
Disposition for a candidate reply to the key-transfer (0x32) confirm wait — the slow-turnaround path'...
PairingDiscoverConfirmDisposition
Disposition for a candidate reply to a discovery-confirm request (0x2C).
DiscoverConfirmResult
What PairingEngine::run_discover_confirm_step_() actually observed.
Definition hub_pairing.h:94
constexpr uint8_t PAIRING_DISCOVERY_MAX_ATTEMPTS
Retry discovery TX up to this many times.
constexpr uint8_t PAIRING_KEY_VERIFY_MAX_TRIES
Tries for the authenticated name read that checks whether a device took our key although the key exch...
constexpr uint32_t PAIRING_KEY_CONFIRM_TIMEOUT_MS
Wait for 0x33 key confirm after sending 0x32.
PairingOutcome
Final disposition of a pairing attempt, used by the result sensor string.
@ FAILED
No usable reply; the device may never have heard the request.
constexpr uint32_t PAIRING_DISCOVER_CONFIRM_ACK_TIMEOUT_MS
Wait window per discover-confirm try.
constexpr uint8_t PAIRING_DISCOVER_CONFIRM_TRIES
Max tries for the discover-confirm (0x2C) step.
constexpr uint32_t PAIRING_RECENT_ONE_WAY_SIGHTING_WINDOW_MS
How recent a RecentOneWayPairingSighting has to be, relative to discover_and_pair() starting,...
constexpr uint32_t PAIRING_KEY_PULL_REPLY_TIMEOUT_MS
Wait for the device's key transfer (0x32) after our key transfer request (0x38).
constexpr uint32_t PAIRING_KEY_CHALLENGE_TIMEOUT_MS
Wait window for the device's 0x3C challenge.
constexpr uint32_t PAIRING_DISCOVERY_RESPONSE_TIMEOUT_MS
Discovery wait window after sending 0x28.
Read-only advisor that turns PairingTelemetry into actionable diagnostics.
Structured per-attempt telemetry recorder for the pairing flow.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Radio abstraction layer for IO-Homecontrol.
The roll-call sweep: one keyed 0x2A broadcast, repeated over channels and power classes.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
Runtime state of a paired IO‑Homecontrol device.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93
Raw packet received from the radio.
A 1W pairing-gesture frame observed on the hub's normal passive RX path, remembered so a fresh discov...
Accumulated state of one scan, threaded through every sweep.
Definition roll_call.h:129
All runtime tunable parameters for pairing and radio diagnostics.
Context object that lives for the duration of a single pairing attempt.
Definition hub_pairing.h:70
Runtime tuning configuration for pairing and radio diagnostics.