Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
hub_core.cpp
Go to the documentation of this file.
1/// @file hub_core.cpp
2/// @brief Component lifecycle and main-loop scheduling.
3/// @ingroup hioc_hub
4///
5/// The core file owns the parts of IOHomeControlComponent that are primarily about
6/// runtime orchestration rather than protocol interpretation:
7/// - hardware/radio setup,
8/// - main loop scheduling,
9/// - device registry and callback fan-out.
10///
11/// Protocol exchange, pairing, inbound status handling, and outbound operations live
12/// in dedicated translation units so this file remains the place to understand how the
13/// component is brought up and driven over time.
14
15#include "hub_internal.h"
16
17#include "radio_sx1276.h"
18#include "radio_sx1262.h"
19#include "radio_lr1121.h"
20#include "tuning_config.h"
21#include "tuning_registry.h"
22
23#include "esphome/core/helpers.h"
24
25#include <algorithm>
26#include <array>
27#include <cinttypes>
28#include <new>
29#include <vector>
30
31namespace esphome {
32namespace home_io_control {
33
34namespace {
35
36/// Room for "3FF" and the terminator: an enrollment class is a ten-bit node type.
37constexpr size_t ENROLLMENT_CLASS_HEX_BUFFER_SIZE = 8;
38
39/// ESPHome's blocking-warning threshold for this component, in the unit of
40/// Component::warn_if_blocking_over_: centiseconds in a uint8_t. 250 cs = 2.5 s, the exchange
41/// budget (EXCHANGE_TOTAL_BUDGET_MS): a normal exchange blocks for a few hundred milliseconds and
42/// setup() for the radio init, so only a block longer than a whole exchange is worth a warning.
43constexpr uint8_t BLOCKING_WARNING_THRESHOLD_CS = 250;
44
45/// Random number for the periodic polls' jitter. ESPHome's random_uint32() on firmware, a settable
46/// value in host tests.
47uint32_t poll_jitter_random() { return random_uint32(); }
48
49/// Boot-log wording for whether a 1W identity mirrors open/close/position, and who decided it.
50const char *oneway_inverted_label(const OneWayControllerIdentity &identity) {
51 if (!effective_inverted(identity))
52 return "no";
53 return has_invert_override(identity) ? "yes (configured)" : "yes (from kind)";
54}
55
56} // namespace
57
58static const char *const TAG = detail::TAG;
59
60// === Setup ===
61
62/// Initialize the IO‑Homecontrol component and radio hardware.
63///
64/// This is the main setup entry point called by ESPHome during startup.
65/// The sequence:
66/// 1. Parse node_id and system_key from hex strings (fails early if malformed).
67/// 2. Initialize the SPI bus via spi_setup().
68/// 3. Construct the driver named by the required `radio_type` YAML field
69/// ("sx1276", "sx1262", or "lr1121").
70/// 4. Allocate the appropriate RadioDriver (SX1276 needs DIO0; SX1262/LR1121 need BUSY+DIO1,
71/// DIO1 carrying the LR1121's DIO9 IRQ line).
72/// 5. Call radio_->init() which performs chip reset, calibration, and register configuration.
73/// 6. Enter normal loop() operation with radio in RX mode.
74///
75/// @note setup() raises ESPHome's blocking-warning threshold (warn_if_blocking_over_) for this
76/// component to 2.5 s, because radio init and every exchange exceed the default 30–50 ms.
78 // IO-homecontrol exchanges are intentionally blocking and often take a few hundred
79 // milliseconds, so warn only past a whole exchange budget (the field is in centiseconds).
80 this->warn_if_blocking_over_ = BLOCKING_WARNING_THRESHOLD_CS;
81#ifdef IOHOME_UNSAFE_LOG_KEY_MATERIAL
82 // Loud, unconditional, every-boot warning so a build left with this flag on by accident can
83 // never stay quiet about it — see log_frame.h::render_frame_hex_redacted() for the full
84 // rationale and safe-use rules.
85 ESP_LOGE(detail::TAG, "########################################");
86 ESP_LOGE(detail::TAG, "IOHOME_UNSAFE_LOG_KEY_MATERIAL IS ENABLED -- FRAME LOGS EXPOSE YOUR SYSTEM KEY");
87 ESP_LOGE(detail::TAG, "This build is NOT safe to run normally or share logs from. Rebuild without this");
88 ESP_LOGE(detail::TAG, "flag as soon as you are done capturing.");
89 ESP_LOGE(detail::TAG, "########################################");
90#endif
91 ESP_LOGI(detail::TAG, "Initializing...");
92 if (!hex_to_bytes(this->node_id_str_, this->node_id_, NODE_ID_SIZE) ||
93 !hex_to_bytes(this->system_key_str_, this->system_key_, AES_KEY_SIZE)) {
94 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) — ESPHome's own LOG_STR() macro.
95 this->mark_failed(LOG_STR("Invalid node_id or system_key configuration"));
96 return;
97 }
98
99 // Open the 1W identities' persistent sequence counters. Not done from the generated
100 // add_oneway_controller() wiring, which runs before preferences are usable.
103 for (const auto &callback : this->oneway_report_callbacks_)
104 callback(report);
105 });
106
107 this->spi_setup();
108
109 const char *chip_name_for_log = nullptr;
110 this->radio_ = this->select_and_construct_radio_(&chip_name_for_log);
111 if (this->radio_ == nullptr) {
112 this->radio_failure_reason_ = "no driver for radio_type '" + this->radio_type_ +
113 "' (a required pin is missing, the type is unrecognized, or allocation failed)";
114 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) — ESPHome's own LOG_STR() macro.
115 this->mark_failed(LOG_STR("Radio driver setup failed (see 'Radio setup failed' in the config dump)"));
116 return;
117 }
118
119#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
120 // Boot-time bootloader-version excursion — deliberately before
121 // init(): nothing has configured the radio yet, so this costs one extra chip reset and needs no
122 // reboot afterward, unlike every other bootloader excursion this feature performs.
123 this->run_lr1121_boot_time_bootloader_read_();
124#endif
125
126 if (!this->radio_->init()) {
127 // Read before the delete: failure_reason() is a member call on the driver.
128 const char *const driver_reason = this->radio_->failure_reason();
130 driver_reason != nullptr ? driver_reason : "driver init failed without a recorded cause";
131 delete this->radio_;
132 this->radio_ = nullptr;
133#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
134 // Cache the verdict even on a failed init() — a null radio_ is exactly the case
135 // trigger_lr1121_firmware_update()'s guard 0 still allows an attempt for (reflashing is the
136 // recovery), and it needs a cached "installed version unknown" verdict to route through.
137 this->cache_lr1121_flash_verdict_();
138#endif
139 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) — ESPHome's own LOG_STR() macro.
140 this->mark_failed(LOG_STR("Radio hardware initialization failed (see 'Radio setup failed' in the config dump)"));
141 return;
142 }
143
144#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
145 this->cache_lr1121_flash_verdict_();
146#endif
147
148 this->initialized_ = true;
153 if (this->tuning_.active) {
154 std::string const snapshot = tuning_config_full_snapshot(this->tuning_);
155 ESP_LOGI(detail::TAG, "%s", snapshot.c_str());
156 }
157 ESP_LOGI(detail::TAG, "Radio initialized (%s), Node ID: %s", chip_name_for_log, this->node_id_str_.c_str());
158}
159
160// See the declaration in hub_core.h for the full contract. `radio_type_` is one of "lr1121",
161// "sx1262", or "sx1276" — the YAML schema requires the field and validates it against exactly
162// those three values, so the fallthrough below is unreachable in a config-driven build and
163// exists only to fail loudly rather than guess if this method is ever called some other way.
165 if (this->radio_type_ == "lr1121") {
166 *chip_name_out = "LR1121";
167 if (this->busy_pin_ == nullptr || this->dio1_pin_ == nullptr) {
168 ESP_LOGE(detail::TAG, "LR1121 requires busy_pin and dio1_pin (dio1_pin carries the chip's DIO9 IRQ line)");
169 return nullptr;
170 }
171 auto *radio = new (std::nothrow)
172 RadioLR1121(this, this->rst_pin_, this->dio1_pin_, this->busy_pin_, this->tx_power_, this->tcxo_voltage_);
173 if (radio == nullptr)
174 ESP_LOGE(detail::TAG, "Failed to allocate LR1121 radio driver");
175 return radio;
176 }
177
178 if (this->radio_type_ == "sx1262") {
179 *chip_name_out = "SX1262";
180 if (this->busy_pin_ == nullptr || this->dio1_pin_ == nullptr) {
181 ESP_LOGE(detail::TAG, "SX1262 requires busy_pin and dio1_pin");
182 return nullptr;
183 }
184 auto *radio = new (std::nothrow)
185 RadioSX1262(this, this->rst_pin_, this->dio1_pin_, this->busy_pin_, this->tx_power_, this->tcxo_voltage_,
186 this->fem_en_pin_, this->vfem_pin_, this->fem_pa_pin_, this->fem_profile_);
187 if (radio == nullptr)
188 ESP_LOGE(detail::TAG, "Failed to allocate SX1262 radio driver");
189 return radio;
190 }
191
192 if (this->radio_type_ == "sx1276") {
193 *chip_name_out = "SX1276";
194 if (this->dio0_pin_ == nullptr) {
195 ESP_LOGE(detail::TAG, "SX1276 requires dio0_pin");
196 return nullptr;
197 }
198 auto *radio = new (std::nothrow)
199 RadioSX1276(this, this->rst_pin_, this->dio0_pin_, this->dio4_pin_, this->tx_power_, this->pa_pin_);
200 if (radio == nullptr)
201 ESP_LOGE(detail::TAG, "Failed to allocate SX1276 radio driver");
202 return radio;
203 }
204
205 *chip_name_out = "unknown";
206 ESP_LOGE(detail::TAG, "Unrecognized radio_type '%s'", this->radio_type_.c_str());
207 return nullptr;
208}
209
210// === Tuning layer ===
211
212/// Apply the current tuning configuration to the active radio driver.
213///
214/// Only chip-specific parameters are forwarded; the rest are consumed by the
215/// pairing flow and LBT logic. This is called once at the end of setup() and
216/// again whenever a UI-driven change modifies a radio parameter.
218 if (this->radio_ == nullptr)
219 return;
220 this->radio_->apply_tuning(this->tuning_);
221}
222
223/// @brief Take the directed start preamble from the driver when YAML did not choose one.
224///
225/// Runs once, after the radio exists and before the first transmit. How much preamble a start
226/// frame needs depends on what the driver actually puts on air, so the default is the driver's to
227/// give (ADR 0042); an explicit `normal_start_preamble:` always wins, including a shorter value.
228/// The resolved value is reported by dump_config(), not from here — see the note in its body.
230 if (this->radio_ == nullptr || this->tuning_.normal_start_preamble_from_yaml)
231 return;
232 // Silent here on purpose: dump_config() reports the resolved value, because that is the only
233 // output a log client attaching after boot receives.
235}
236
237/// Update a numeric tuning parameter from a Home Assistant `number` entity.
238///
239/// Parses the parameter name and applies the new value to the in-memory tuning
240/// configuration. Radio-affecting parameters are forwarded to the active driver
241/// immediately; the change is logged in YAML-compatible form so it can be copied
242/// back into the configuration file.
243void IOHomeControlComponent::update_tuning_number(const std::string &name, float value) {
244 const TuningNumberParam *param = find_tuning_number(name);
245 if (param == nullptr) {
246 ESP_LOGW(detail::TAG, "Unknown tuning number parameter: %s", name.c_str());
247 return;
248 }
249 param->set(this->tuning_, value);
250 if (param->applies_to_radio)
252 ESP_LOGI(detail::TAG, "%s", tuning_update_log_line(name, std::to_string(static_cast<int>(value))).c_str());
253}
254
255/// Update a select tuning parameter from a Home Assistant `select` entity.
256///
257/// Parses the selected option string and applies it to the in-memory tuning
258/// configuration. Radio-affecting parameters are forwarded to the active driver
259/// immediately; the change is logged in YAML-compatible form.
260void IOHomeControlComponent::update_tuning_select(const std::string &name, const std::string &value) {
261 const TuningSelectParam *param = find_tuning_select(name);
262 if (param == nullptr) {
263 ESP_LOGW(detail::TAG, "Unknown tuning select parameter: %s", name.c_str());
264 return;
265 }
266 // Radio-affecting parameters re-apply only when the option string actually parsed; the
267 // update is still logged for any known parameter, matching the original dispatch.
268 if (param->set(this->tuning_, value) && param->applies_to_radio)
270 ESP_LOGI(detail::TAG, "%s", tuning_update_log_line(name, value).c_str());
271}
272
273/// Return the current value of a numeric tuning parameter.
274///
275/// Mirror of update_tuning_number(); used by IOHomeTuningNumber::setup() to publish
276/// the boot-time value so the Home Assistant slider reflects the active configuration
277/// (default or YAML override) without restating any default on the Python side.
278float IOHomeControlComponent::get_tuning_number_value(const std::string &name) const {
279 const TuningNumberParam *param = find_tuning_number(name);
280 if (param == nullptr) {
281 ESP_LOGW(detail::TAG, "Unknown tuning number parameter: %s", name.c_str());
282 return 0.0F;
283 }
284 return param->get(this->tuning_);
285}
286
287/// Return the current option string of a select tuning parameter.
288///
289/// Mirror of update_tuning_select(); used by IOHomeTuningSelect::setup() to publish the
290/// boot-time option so the Home Assistant dropdown reflects the active configuration. The
291/// returned strings match the YAML/UI option labels exactly. The command list is returned
292/// as a comma-separated preset string (e.g. "0x28,0x2E") matching the dropdown options.
293std::string IOHomeControlComponent::get_tuning_select_value(const std::string &name) const {
294 const TuningSelectParam *param = find_tuning_select(name);
295 if (param == nullptr) {
296 ESP_LOGW(detail::TAG, "Unknown tuning select parameter: %s", name.c_str());
297 return "";
298 }
299 return param->get(this->tuning_);
300}
301
302// === Protocol send/receive (thin wrappers delegating to ExchangeEngine) ===
303
304/// Delegate channel hop to ExchangeEngine (which owns last_hop_us_).
306
307/// Delegate LBT transmit to ExchangeEngine.
308bool IOHomeControlComponent::transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble) {
309 return this->exchange_engine_.transmit_frame(frame, freq, preamble);
310}
311
312/// Delegate outbound exchange to ExchangeEngine and manage the busy_ flag.
314 const decisions::AttemptRequest &attempts) {
315 this->busy_ = true;
316 // The read at a cover's predicted end leads with the wake-up preamble: the cover has come to rest
317 // by its own count, and a resting receiver hears only that one. The override also takes the wake
318 // belief out of the exchange, which would lead short while the moving evidence is still fresh.
319 const uint16_t preamble_override = (attempts.wake_preamble_first && (request.ctrl1 & CTRL1_LOW_POWER) != 0)
321 : 0;
322 ExchangeOutcome const outcome =
323 attempts.planned
325 request, response, freq,
326 this->exchange_engine_.plan_read(attempts.kind, request, freq, preamble_override,
327 attempts.state_unknown),
328 preamble_override)
329 : this->exchange_engine_.send_and_receive(request, response, freq, attempts.max_tries, preamble_override);
330 this->busy_ = false;
331 return outcome;
332}
333
334/// Delegate inbound authentication to ExchangeEngine.
335bool IOHomeControlComponent::authenticate_request_(const IoFrame &request, uint32_t freq) {
336 return this->exchange_engine_.authenticate_request(request, freq);
337}
338
339void IOHomeControlComponent::notify_device_update_(const std::string &id) { this->registry_.notify(id); }
340
341// === Device management ===
342
343void IOHomeControlComponent::set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms) {
344 if (this->get_device(device_id) == nullptr)
345 return;
346 this->poll_policy_.set_interval(device_id, poll_interval_ms);
347}
348
349void IOHomeControlComponent::set_device_rain_poll_interval(const std::string &device_id, uint32_t interval_ms) {
350 if (this->get_device(device_id) == nullptr)
351 return;
352 this->rain_poll_policy_.set_interval(device_id, interval_ms, millis(), poll_jitter_random());
353}
354
355void IOHomeControlComponent::set_device_sun_poll_interval(const std::string &device_id, uint32_t interval_ms) {
356 if (this->get_device(device_id) == nullptr)
357 return;
358 this->sun_poll_policy_.set_interval(device_id, interval_ms, millis(), poll_jitter_random());
359}
360
362 uint32_t interval_ms) {
363 if (this->get_device(device_id) == nullptr)
364 return;
365 this->battery_level_poll_policy_.set_interval(device_id, interval_ms, millis(), poll_jitter_random());
366}
367
368void IOHomeControlComponent::set_device_status_refresh_interval(const std::string &device_id, uint32_t interval_ms) {
369 if (this->get_device(device_id) == nullptr)
370 return;
371 this->status_refresh_policy_.set_interval(device_id, interval_ms, millis(), poll_jitter_random());
372}
373
374void IOHomeControlComponent::note_status_reported_(const std::string &device_id) {
375 this->status_refresh_policy_.restart_interval(device_id, millis(), poll_jitter_random());
376}
377
378void IOHomeControlComponent::schedule_background_poll_backoff_(const std::string &device_id, bool auth_like,
379 bool device_moving) {
380 uint32_t const now = millis();
381 uint8_t const skipped_before = this->poll_policy_.get_moving_poll_misses(device_id);
382 uint32_t const backoff_ms = this->poll_policy_.on_exchange_failed(device_id, auth_like, now, device_moving);
383 if (backoff_ms > 0 && this->poll_policy_.get_moving_poll_misses(device_id) > skipped_before) {
384 ESP_LOGD(TAG, "Status poll miss for moving device %s skipped (%u of %u): next poll in %" PRIu32 " ms",
385 device_id.c_str(), this->poll_policy_.get_moving_poll_misses(device_id), MOVING_POLL_MISSES_SKIPPED,
386 backoff_ms);
387 } else if (backoff_ms > 0) {
388 ESP_LOGD(TAG,
389 "Background status poll backoff for device %s: delay=%" PRIu32
390 " ms auth_like=%s status_failures=%u auth_failures=%u",
391 device_id.c_str(), backoff_ms, YESNO(auth_like), this->poll_policy_.get_status_poll_failures(device_id),
392 this->poll_policy_.get_auth_poll_failures(device_id));
393 }
394}
395
396void IOHomeControlComponent::add_device(const std::string &device_id) { this->registry_.add(device_id); }
397
398void IOHomeControlComponent::add_device(const std::string &device_id, const DeviceConfig &cfg) {
399 this->registry_.add(device_id, cfg);
400}
401
402IoDevice *IOHomeControlComponent::get_device(const std::string &device_id) { return this->registry_.get(device_id); }
403
404void IOHomeControlComponent::set_device_travel_speed(const std::string &device_id, TravelSpeed speed) {
405 this->registry_.set_travel_speed(device_id, speed);
406}
407
408// === Main loop ===
409
411 if (!this->initialized_)
412 return;
413 // A driver that latches a failure after setup (a runtime BUSY timeout) short-circuits every later
414 // command while the component keeps running. Say so once, when it happens: an error line, ESPHome's
415 // warning status (which drives a configured status_led) and every device's Active Issue sensor,
416 // rather than only in dump_config().
417 if (!this->radio_failure_reported_ && this->radio_->is_failed()) {
418 this->radio_failure_reported_ = true;
419 const char *const reason = this->radio_->failure_reason();
420 ESP_LOGE(TAG, "Radio stopped responding: %s; reboot the board", reason != nullptr ? reason : "no recorded cause");
421 this->status_set_warning("radio stopped responding");
422 // Every device's Active Issue sensor shows it: a dead radio breaks every device.
423 for (const auto &entry : this->registry_)
424 this->notify_device_update_(entry.first);
425 }
426 if (this->radio_test_mode_)
427 return;
428
429 // Check for received packets (non-blocking)
430 if (!this->busy_) {
431 RadioRxPacket packet{};
432 if (this->radio_->check_for_packet(packet))
433 this->process_received_packet_(packet);
434 }
435
436 // A blocking exchange makes the radio deaf for 1–3 s. When a linked remote's press schedules a
437 // status poll, dispatching it while that same remote is still transmitting would blind the hub
438 // to the rest of the press — so background polls yield for a moment. Control operations never do.
439 if (!this->busy_ && !this->defer_background_poll_()) {
441 }
442
443 // Frequency hopping — protocol specifies 2.7ms per channel, but ESPHome calls
444 // loop() every ~16-30ms. This is acceptable for a controller: a directed start frame to a
445 // low-power target still goes out with LONG_PREAMBLE (1024 bytes ≈ 218 ms airtime), long enough
446 // to be detected regardless of channel alignment; a start frame to an always-alive target uses
447 // the shorter normal_start_preamble (default 32 bytes), which such a receiver hears fine. This
448 // coarse idle hop causes no exchange to miss its channel either way, because every TX retunes
449 // explicitly to a named channel before sending. Precise hopping would only matter for a passive
450 // receiver scanning for unsolicited frames. A radio that scans the hop channels by itself
451 // (RadioDriver::run_idle_scan()) takes the idle pass over and the host does not hop.
452 // Diagnostics build flag: park the receiver on one channel instead of hopping. A hopping monitor
453 // is on any given channel roughly a third of the time, so "the capture never shows frame X" is
454 // weak evidence — locking to the channel under study makes an absence mean something. Define it
455 // to the channel in Hz, e.g. -DIOHOME_LOCK_CHANNEL_HZ=868950000 for CH2, the command channel.
456 // Only useful for a passive monitor: a hub that cannot hop will miss replies on other channels.
457#ifdef IOHOME_LOCK_CHANNEL_HZ
458 if (!this->busy_ && this->radio_ != nullptr && this->radio_->get_current_freq() != IOHOME_LOCK_CHANNEL_HZ)
459 this->radio_->change_frequency(IOHOME_LOCK_CHANNEL_HZ);
460#else
461 if (!this->busy_) {
462 if (this->key_extraction_awaiting_reply_()) {
464 } else if (!this->radio_->run_idle_scan()) {
466 }
467 }
468#endif
469
470 // A window that ran out ends a settling device's wait for its confirming poll. Radio-free, so
471 // not held back by busy_.
473
474 // Periodic status polling
475 if (!this->busy_) {
476 auto due = this->poll_policy_.pop_due_device(millis());
477 if (due.has_value())
478 this->queue_request_device_status(*due);
481 }
482}
483
484// One listen slice of the key-extraction responder's scan. Long enough that the gap between two
485// slices (the rest of the loop pass, about one ESPHome loop interval, when the radio stays on one
486// channel) is a small part of the time, short enough that other components keep running.
487static constexpr uint32_t KEY_EXTRACTION_SCAN_SLICE_MS = 200;
488
490 if (this->key_extraction_.in_session_hold()) {
491 // The peer answers on the session's channel within the session window, with a short preamble,
492 // so the radio waits there. Frequency test first, deliberately: reception_in_progress() is not
493 // a free predicate (on SX1276 it can force a set_mode_standby()/set_mode_rx() cycle).
494 const uint32_t freq = this->key_extraction_.session_freq();
495 if (this->radio_->get_current_freq() != freq && !this->radio_->reception_in_progress())
496 this->radio_->change_frequency(freq);
497 return;
498 }
499 // Outside the window the peer's next frame is a new request, which may go out on any channel, so
500 // scan all three as a device does between sessions: the same rotating listen as discovery (the
501 // chip's own dwell, staying on a channel while a preamble or sync word is coming in). The idle hop
502 // is too coarse for this: it moves once per loop pass, so it can sit on one channel for longer
503 // than a start preamble lasts.
504 // A discovery answer waiting for its slot is sent by the scheduler, which runs only after this
505 // pass returns: end the slice by its slot, and skip it once the slot has come.
506 uint32_t window_ms = KEY_EXTRACTION_SCAN_SLICE_MS;
508 window_ms = std::min(window_ms, this->key_extraction_.ms_until_discover_reply());
509 if (window_ms == 0)
510 return;
511 }
512 ListenSpec spec;
513 spec.window_ms = window_ms;
515 spec.linger_on_preamble = true;
516 spec.linger_dwell_ms = PREAMBLE_LINGER_DWELL_MS;
517 RadioRxPacket packet{};
518 IoFrame frame;
519 // Every frame ends the slice and goes through the normal receive path, so the responder, 1W
520 // remotes and status reports see it exactly as they would from the idle hop.
521 const ListenOutcome outcome = this->exchange_engine_.listen(
522 spec, packet, frame,
523 [](const IoFrame * /*parsed*/, const RadioRxPacket & /*pkt*/) { return ReplyDisposition::ACCEPT; });
524 if (outcome == ListenOutcome::ACCEPTED)
525 this->process_received_packet_(packet);
526}
527
529 if (!this->poll_policy_.take_due_catchup(millis()))
530 return;
531 const std::vector<std::string> devices = this->registry_.all_linked_devices();
532 ESP_LOGD(TAG, "No exchange for %" PRIu32 " ms after linked-remote activity: reading %zu linked device(s)",
533 this->tuning_.remote_activity_catchup_poll_ms, devices.size());
534 // One ordinary status read each, no follow-up window: a missed press may have set a device moving,
535 // so the read treats its state as unknown and leads with the short attempt a moving receiver hears;
536 // an answer that says moving opens the follow-up window by itself (follow_moving_report()).
537 for (const std::string &device_id : devices) {
538 this->poll_policy_.mark_state_unknown(device_id);
539 this->queue_request_device_status(device_id);
540 }
541}
542
544 // has_due() first, so a random number is drawn only when pop_due_device() uses it.
545 if (this->rain_poll_policy_.has_due(millis())) {
546 if (const auto due = this->rain_poll_policy_.pop_due_device(millis(), poll_jitter_random()); due.has_value())
548 }
549 if (this->sun_poll_policy_.has_due(millis())) {
550 if (const auto due = this->sun_poll_policy_.pop_due_device(millis(), poll_jitter_random()); due.has_value())
552 }
553 if (this->battery_level_poll_policy_.has_due(millis())) {
554 if (const auto due = this->battery_level_poll_policy_.pop_due_device(millis(), poll_jitter_random());
555 due.has_value())
557 }
558 // A device that reported nothing for a whole interval is read. Each status it reports moves its
559 // refresh a whole interval out (note_status_reported_()), so a device read often enough never gets here.
560 if (this->status_refresh_policy_.has_due(millis())) {
561 if (const auto due = this->status_refresh_policy_.pop_due_device(millis(), poll_jitter_random()); due.has_value()) {
562 ESP_LOGD(TAG, "Device %s: no status reported for a refresh interval (%" PRIu32 " s): reading it", due->c_str(),
563 this->status_refresh_policy_.get_interval(*due) / 1000);
564 this->queue_request_device_status(*due);
565 }
566 }
567}
568
570 ESP_LOGCONFIG(detail::TAG, "IO-Homecontrol:");
571 ESP_LOGCONFIG(detail::TAG, " Node ID: %s", this->node_id_str_.c_str());
572 ESP_LOGCONFIG(detail::TAG, " Radio: %s", this->radio_type_.c_str());
573 ESP_LOGCONFIG(detail::TAG, " TX Power: %u dBm", this->tx_power_);
574 // Printed for every board, from the dump rather than from setup(): the value decides whether a
575 // directed start frame is heard at all, it is chip-dependent (ADR 0042), and a reporter's pasted
576 // log is usually captured after boot, where setup()'s output is already gone.
577 ESP_LOGCONFIG(detail::TAG, " Start preamble: %u bytes (%s)", this->tuning_.normal_start_preamble,
578 this->tuning_.normal_start_preamble_from_yaml ? "set in YAML" : "from the radio driver");
579 ESP_LOGCONFIG(detail::TAG, " Discovery preamble: %u bytes (%s)", effective_discovery_preamble(this->tuning_),
581 ? "set in tuning"
582 : (this->tuning_.pairing_discovery_low_power ? "wake-up preamble, discovery is low-power"
583 : "follows the start preamble"));
584 if (!this->radio_failure_reason_.empty())
585 ESP_LOGE(detail::TAG, " Radio setup failed: %s", this->radio_failure_reason_.c_str());
586#ifdef IOHOME_UNSAFE_LOG_KEY_MATERIAL
587 // The setup() banner in short: a log client that connects after boot only receives this dump, and
588 // it is the reader who most needs the instruction to rebuild before sharing logs.
589 ESP_LOGE(detail::TAG, " IOHOME_UNSAFE_LOG_KEY_MATERIAL IS ENABLED -- FRAME LOGS EXPOSE YOUR SYSTEM KEY");
590 ESP_LOGE(detail::TAG, " Not safe to share logs from: rebuild without this flag as soon as you are done capturing.");
591#endif
592 if (this->tuning_.active) {
593 // Same snapshot setup() logs once at boot, for the same reason.
594 ESP_LOGCONFIG(detail::TAG, " %s", tuning_config_full_snapshot(this->tuning_).c_str());
595 }
596 LOG_PIN(" RST Pin: ", this->rst_pin_);
597 if (this->dio0_pin_ != nullptr)
598 LOG_PIN(" DIO0 Pin: ", this->dio0_pin_);
599 if (this->dio1_pin_ != nullptr)
600 LOG_PIN(" DIO1 Pin: ", this->dio1_pin_);
601 if (this->dio4_pin_ != nullptr)
602 LOG_PIN(" DIO4 Pin: ", this->dio4_pin_);
603 if (this->busy_pin_ != nullptr)
604 LOG_PIN(" BUSY Pin: ", this->busy_pin_);
605 // Stated on every boot/log connect, so a posted log shows whether low-power devices get the
606 // per-try preamble order (ADR 0040) without anyone having to share their tuning YAML.
607 ESP_LOGCONFIG(detail::TAG, " Low-power wake belief: %s",
608 this->tuning_.low_power_wake_belief ? "on" : "off (tuning: low_power_wake_belief: false)");
609 ESP_LOGCONFIG(detail::TAG, " Devices: %zu", this->registry_.size());
610 if (this->registry_.linked_remote_count() > 0) {
611 ESP_LOGCONFIG(detail::TAG, " Linked Remotes: %zu", this->registry_.linked_remote_count());
612 this->registry_.for_each_linked_remote([](const std::string &remote_id, const std::vector<std::string> &devices) {
613 for (const auto &device_id : devices)
614 ESP_LOGCONFIG("home_io_control", " - remote %s -> device %s", remote_id.c_str(), device_id.c_str());
615 });
616 }
617
619
620 if (this->radio_ != nullptr) {
621 // A driver that latched a failure after setup (a runtime BUSY timeout) short-circuits every
622 // later command while the component itself is not marked failed. loop() reports it once when it
623 // happens; repeat it here for log clients that connect later.
624 if (this->radio_->is_failed()) {
625 const char *const reason = this->radio_->failure_reason();
626 ESP_LOGE(detail::TAG, " Radio failed after setup: %s", reason != nullptr ? reason : "no recorded cause");
627 }
628 this->radio_->dump_front_end();
629 this->radio_->dump_debug();
630 }
631
632#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
633 this->dump_lr1121_firmware_update_debug_();
634#endif
635}
636
638 if (this->oneway_controllers().empty())
639 return;
640 ESP_LOGCONFIG(detail::TAG, " 1W Controllers: %zu", this->oneway_controllers().all().size());
641 for (const auto &identity : this->oneway_controllers().all()) {
642 // A derived address is reproducible from the YAML, but nothing in the YAML shows it — so
643 // print it, and mark it derived, or a user debugging a collision has nowhere to look. Keys
644 // are never printed here (ADR 0011); only addresses and classes. The resolved ACEI and
645 // destination let a user eyeball this against a capture of their real remote (ADR 0031).
646 const OneWayWireProfile profile = resolve_oneway_wire_profile(identity.manufacturer);
647 // The power class is printed for every identity, including the ones that never set the key, so
648 // a reporter's boot log always shows which shape their build actually transmits (ADR 0038).
649 // Where it came from is printed too: an unset key resolves from the manufacturer profile
650 // (ADR 0041), so "always-alive" alone would not tell a VELUX user whether they chose it.
651 ESP_LOGCONFIG(
652 detail::TAG, " - %s: node %s%s, class 0x%02X, acei 0x%02X%s, broadcast %s%s, power %s%s",
653 identity.id.c_str(), node_id_to_string(identity.node_id).c_str(), identity.node_id_derived ? " (derived)" : "",
654 static_cast<unsigned>(identity.io_device_type), static_cast<unsigned>(effective_execute_acei(identity)),
655 has_execute_acei_override(identity) ? " (override)" : "", identity.execute_broadcast_all ? "all" : "typed",
656 profile.profile_is_a_guess ? " [no vendor profile — Somfy-shaped]" : "",
658 has_power_class_override(identity) ? "" : " (from profile)");
659 // Whether open/close are mirrored on the wire, and who decided: the kind or `invert_position:`.
660 ESP_LOGCONFIG(detail::TAG, " inverted=%s", oneway_inverted_label(identity));
661
662 // The VELUX enrollment gesture ignores io_device_type and sweeps a fixed class set instead —
663 // the most surprising resolved value on the identity, so print it (ADR 0032).
665 std::string classes;
666 std::array<char, ENROLLMENT_CLASS_HEX_BUFFER_SIZE> byte_hex{};
667 for (const DeviceType c : effective_enrollment_classes(identity)) {
668 if (c == DeviceType::UNKNOWN)
669 continue;
670 snprintf(byte_hex.data(), byte_hex.size(), "%02X", static_cast<unsigned>(c));
671 classes += classes.empty() ? "0x" : " 0x";
672 classes += byte_hex.data();
673 }
674 ESP_LOGCONFIG(detail::TAG, " enroll: VELUX KLI gesture, 0x30 sweep -> %s", classes.c_str());
675 }
676 }
677}
678
679} // namespace home_io_control
680} // namespace esphome
std::vector< std::string > all_linked_devices() const
Every device with a linked_remotes entry, by remote id or by class:, once each, sorted.
void notify(const std::string &device_id)
Invoke all registered callbacks for device_id.
IoDevice * get(const std::string &device_id)
Retrieve a registered device by ID.
void for_each_linked_remote(const std::function< void(const std::string &, const std::vector< std::string > &)> &fn) const
Invoke fn(remote_id, device_id_list) for every linked-remote entry.
void add(const std::string &device_id)
Register a device by ID with default metadata (UNKNOWN type, subtype 0, no overrides).
void set_travel_speed(const std::string &device_id, TravelSpeed speed)
Select a device's travel speed.
ListenOutcome listen(const ListenSpec &spec, RadioRxPacket &packet, IoFrame &frame, const ReplyHandler &on_frame)
The one listen primitive every radio wait loop in this project is built on.
void maybe_hop()
Hop only if the minimum dwell has elapsed and no frame is currently arriving on this channel — RadioD...
bool transmit_frame(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame with the given preamble length.
bool authenticate_request(const IoFrame &request, uint32_t freq)
Authenticate an inbound device command via 0x3C challenge / 0x3D HMAC.
ExchangeOutcome send_and_receive(const IoFrame &request, IoFrame &response, uint32_t freq, uint8_t max_tries=EXCHANGE_RETRY_COUNT, uint16_t request_preamble_override=0)
Execute an outbound authenticated exchange with retry.
void reset_hop_timestamp()
Reset the hop-timer (called after radio init in hub setup()).
decisions::AttemptPlan plan_read(decisions::ReadKind kind, const IoFrame &request, uint32_t freq, uint16_t request_preamble_override=0, bool state_unknown=false) const
The attempt plan for a read of request's target, with the transmit time this engine would plan for th...
void hop_frequency(uint32_t skip_freq=0)
Advance the receiver one step along the protocol's channel rotation (CH1→CH2→CH3→CH1).
InternalGPIOPin * fem_en_pin_
Front-end module enable.
Definition hub_core.h:1576
InternalGPIOPin * fem_pa_pin_
Front-end module PA switch.
Definition hub_core.h:1578
void dump_oneway_controllers_config_() const
Emit the 1W controller identities to the config dump — node, class, and the resolved ACEI / broadcast...
Definition hub_core.cpp:637
void schedule_background_poll_backoff_(const std::string &device_id, bool auth_like, bool device_moving=false)
Apply backoff after a failed background status poll and log the result.
Definition hub_core.cpp:378
InternalGPIOPin * dio4_pin_
SX1276 DIO4 preamble detect (optional).
Definition hub_core.h:1573
void listen_for_key_extraction_peer_()
One idle pass of listening for the key-extraction responder's peer, in place of the generic idle hop ...
Definition hub_core.cpp:489
InternalGPIOPin * dio1_pin_
SX1262 DIO1 interrupt; also carries the LR1121's DIO9 IRQ line.
Definition hub_core.h:1574
virtual void queue_request_device_sun_level(const std::string &device_id)
Queue an async sun level poll; executed in loop() as a background operation.
void dump_config() override
Dump configuration and radio debug info to the log.
Definition hub_core.cpp:569
void update_tuning_select(const std::string &name, const std::string &value)
Receive a select tuning update from a HA select entity.
Definition hub_core.cpp:260
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:402
void note_status_reported_(const std::string &device_id)
The device just reported its status: move its status refresh (if it has one) a whole interval out.
Definition hub_core.cpp:374
void queue_due_catchup_reads_()
Read every device with linked_remotes once, when a burst of linked-remote activity has been followed ...
Definition hub_core.cpp:528
virtual void queue_request_device_status(const std::string &device_id)
Queue an async status request; returns immediately, executed in loop().
void update_tuning_number(const std::string &name, float value)
Receive a numeric tuning update from a HA number entity.
Definition hub_core.cpp:243
PeriodicPollPolicy battery_level_poll_policy_
Schedule of the opt-in battery level poll.
Definition hub_core.h:1622
virtual void add_device(const std::string &device_id)
Add a device to the registry by device ID only (undeclared/legacy path).
Definition hub_core.cpp:396
InternalGPIOPin * vfem_pin_
Front-end module power.
Definition hub_core.h:1577
TuningConfig tuning_
Runtime tuning overrides.
Definition hub_core.h:1604
virtual void set_device_status_poll_interval(const std::string &device_id, uint32_t poll_interval_ms)
Configure the optional follow-up polling interval for a registered device.
Definition hub_core.cpp:343
void process_pending_operation_()
Pop next pending operation from the queue and execute it (set position, request status,...
void hop_frequency_()
Delegate channel hop to ExchangeEngine (which owns last_hop_us_).
Definition hub_core.cpp:305
float get_tuning_number_value(const std::string &name) const
Current value of a numeric tuning parameter, used to seed a HA number entity on boot.
Definition hub_core.cpp:278
bool defer_background_poll_() const
Whether loop() should skip dispatching the queue this iteration because the pending work is a backgro...
Definition hub_core.h:1075
virtual void set_device_travel_speed(const std::string &device_id, TravelSpeed speed)
Select a device's travel speed at runtime (see IOHomeCoverTravelSpeedSelect and IOHomeCoverSilentSwit...
Definition hub_core.cpp:404
ExchangeOutcome send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq, const decisions::AttemptRequest &attempts=EXCHANGE_RETRY_COUNT)
Main request/response exchange with retry and automatic authentication.
Definition hub_core.cpp:313
virtual void queue_request_device_limitation(const std::string &device_id)
Queue an async minimum-limitation read for the rain sensor poll; executed in loop() as a background o...
void resolve_start_preamble_default_()
Resolve normal_start_preamble from the driver when YAML did not set it (ADR 0042).
Definition hub_core.cpp:229
void loop() override
Main loop: process pending operations and drive radio state machine.
Definition hub_core.cpp:410
OneWayTransmitter oneway_transmitter_
Owns the 1W controller identities, their rolling-sequence counters and the transmit burst.
Definition hub_core.h:1641
std::string radio_failure_reason_
Why setup() gave up on the radio, or empty.
Definition hub_core.h:1587
void settle_ended_status_tracking_()
Settle every device whose polling window ran out (StatusPollPolicy::pop_ended_device()).
virtual void set_device_rain_poll_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the periodic rain sensor poll (a read of its minimum limitation).
Definition hub_core.cpp:349
std::string radio_type_
"sx1276", "sx1262", or "lr1121"; required by the YAML schema.
Definition hub_core.h:1584
virtual void set_device_status_refresh_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the status refresh at rest: a status read whenever the device has not re...
Definition hub_core.cpp:368
bool key_extraction_awaiting_reply_() const
True while the key-extraction responder is mid-attempt and still within its bounded listen window.
Definition hub_core.h:986
void queue_due_periodic_polls_()
Queue every periodic poll that is due: the sensor polls (rain, sun level, battery level) and the stat...
Definition hub_core.cpp:543
virtual void queue_request_device_battery_level(const std::string &device_id)
Queue an async battery level poll; executed in loop() as a background operation.
RadioDriver * select_and_construct_radio_(const char **chip_name_out)
Select and construct the radio driver named by the required radio_type config field.
Definition hub_core.cpp:164
bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame on the current frequency with given preamble length.
Definition hub_core.cpp:308
bool radio_test_mode_
When true, loop() is suspended for loopback testing.
Definition hub_core.h:1603
ExchangeEngine exchange_engine_
Owns all authenticated exchange and LBT/hop logic.
Definition hub_core.h:1634
const OneWayControllerRegistry & oneway_controllers() const
Definition hub_core.h:336
PeriodicPollPolicy rain_poll_policy_
Schedule of the opt-in rain sensor poll.
Definition hub_core.h:1620
bool radio_failure_reported_
Set once loop() has reported a radio that latched a failure after setup, so the report (an error line...
Definition hub_core.h:1591
virtual void set_device_sun_poll_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the periodic sun level poll (a subscription to EVENT_LOCAL_SENSOR,...
Definition hub_core.cpp:355
InternalGPIOPin * dio0_pin_
SX1276 DIO0 interrupt.
Definition hub_core.h:1572
std::vector< OneWayCommandReportFn > oneway_report_callbacks_
Subscribers to the per-command 1W report; one per "Last 1W Command" sensor.
Definition hub_core.h:1662
void notify_device_update_(const std::string &id)
Fire all registered device update callbacks for the given device ID.
Definition hub_core.cpp:339
std::string get_tuning_select_value(const std::string &name) const
Current option string of a select tuning parameter, used to seed a HA select entity on boot.
Definition hub_core.cpp:293
void setup() override
Initialize hardware (radio and device registry).
Definition hub_core.cpp:77
KeyExtractionResponder key_extraction_
Device-role responder for the "Recover System Key" feature (key_extraction_responder....
Definition hub_core.h:1652
uint8_t tcxo_voltage_
SX1262/LR1121 TCXO voltage setting (default 1.8 V).
Definition hub_core.h:1596
void process_received_packet_(const RadioRxPacket &packet)
Parse a received frame, merge supported device state or metadata, and notify callbacks.
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
Definition hub_core.cpp:335
InternalGPIOPin * busy_pin_
SX1262/LR1121 BUSY pin.
Definition hub_core.h:1575
void register_management_actions_()
Register hub-level Home Assistant actions; called from setup().
Definition hub_core.h:1491
void apply_tuning_to_radio_()
Apply the current tuning configuration to the active radio driver.
Definition hub_core.cpp:217
FemProfile fem_profile_
Which FEM part fem_pa_pin_ belongs to, if any.
Definition hub_core.h:1579
PeriodicPollPolicy status_refresh_policy_
Schedule of the opt-in status refresh at rest.
Definition hub_core.h:1623
PeriodicPollPolicy sun_poll_policy_
Schedule of the opt-in sun level poll.
Definition hub_core.h:1621
virtual void set_device_battery_level_poll_interval(const std::string &device_id, uint32_t interval_ms)
Opt a registered device into the periodic battery level poll (a subscription to EVENT_BATTERY_LEVEL_C...
Definition hub_core.cpp:361
uint32_t session_freq() const
Channel of the session the responder last replied in: the channel the frame it answered came in on,...
bool discover_reply_pending() const
True while a discovery answer waits for its slot (schedule_discover_reply_()).
uint32_t ms_until_discover_reply() const
Milliseconds until the pending discovery answer's slot, 0 once it is due.
bool in_session_hold() const
True while awaiting_reply() and the frame the responder last answered was received less than the sess...
void setup()
Open each registered identity's persistent sequence counter.
void set_command_report_callback(OneWayCommandReportFn callback)
Register the callback that receives a report after every command attempt.
std::optional< std::string > pop_due_device(uint32_t now, uint32_t random)
Return one device whose poll is due and immediately schedule its next poll at periodic_poll_jittered_...
void restart_interval(const std::string &device_id, uint32_t now, uint32_t random)
The device has just given what the poll asks for without being polled: schedule its next poll periodi...
bool has_due(uint32_t now) const
True when any device's poll is due at now.
void set_interval(const std::string &device_id, uint32_t interval_ms, uint32_t now, uint32_t random)
Set the poll interval and schedule the first poll at periodic_poll_first_delay_ms(random) after now.
Abstract radio driver for IO-Homecontrol.
virtual void dump_front_end()
Optional board front-end-module (external PA/LNA) summary emitted from dump_config,...
uint32_t get_current_freq() const
Get the current RF frequency.
virtual uint16_t default_start_preamble() const
Default preamble for a directed start frame, when the user has not set normal_start_preamble in YAML.
virtual void change_frequency(uint32_t freq_hz)=0
Change the carrier frequency using fast hop (no standby transition needed).
const char * failure_reason() const
Short, human-readable reason the driver latched is_failed, or nullptr if it has not.
virtual bool run_idle_scan()
Let the radio scan the hop channels by itself while the hub is idle.
virtual bool is_failed() const
Returns true if the radio failed to initialize or encountered a fatal error.
virtual bool init()=0
Initialize the radio hardware. Returns true on success.
virtual void apply_tuning(const TuningConfig &tuning)
Apply runtime tuning parameters to the driver.
virtual void dump_debug()
Optional chip-specific diagnostics emitted from dump_config.
virtual bool check_for_packet(RadioRxPacket &packet)=0
Non-blocking check for a received packet.
RadioLR1121(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *irq_pin, InternalGPIOPin *busy_pin, uint8_t tx_power, uint8_t tcxo_voltage_code)
RadioSX1262(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio1_pin, InternalGPIOPin *busy_pin, uint8_t tx_power, uint8_t tcxo_voltage, InternalGPIOPin *fem_en_pin=nullptr, InternalGPIOPin *vfem_pin=nullptr, InternalGPIOPin *fem_pa_pin=nullptr, FemProfile fem_profile=FemProfile::NONE)
RadioSX1276(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio0_pin, InternalGPIOPin *dio4_pin, uint8_t tx_power, uint8_t pa_pin)
void set_interval(const std::string &device_id, uint32_t interval_ms)
Set the configured follow-up poll interval (ms; 0 = legacy one-shot settle only).
uint32_t on_exchange_failed(const std::string &device_id, bool auth_like, uint32_t now, bool device_moving=false)
Record a failed background poll; apply backoff or clear tracking if the window expired.
uint8_t get_moving_poll_misses(const std::string &device_id) const
Consecutive failed polls of a moving device skipped so far.
uint8_t get_status_poll_failures(const std::string &device_id) const
bool take_due_catchup(uint32_t now)
True once when the open burst's catch-up is due at now.
void mark_state_unknown(const std::string &device_id)
Mark the device's next status read as a catch-up read: a press the hub missed may have set it moving ...
std::optional< std::string > pop_due_device(uint32_t now)
Return and consume the first device with a poll overdue.
Helpers shared only by the hub's own implementation files (hub_*.cpp).
constexpr const char * TAG
Shared log tag for hub-level messages.
Definition log_helpers.h:32
static constexpr const char * TAG
OneWayPowerClass effective_power_class(const OneWayControllerIdentity &identity)
The burst shape this identity actually transmits with.
bool effective_inverted(const OneWayControllerIdentity &identity)
Whether this identity's OPEN/CLOSE/position values are mirrored on the wire.
static constexpr uint32_t KEY_EXTRACTION_SCAN_SLICE_MS
Definition hub_core.cpp:487
DeviceType
IO-Homecontrol node types.
@ UNKNOWN
Unknown/unspecified device.
@ ACCEPT
This is the frame the caller was waiting for — stop listening, return ACCEPTED.
std::array< DeviceType, 3 > effective_enrollment_classes(const OneWayControllerIdentity &identity)
The device classes this identity's 0x30 enrollment sweep will actually target.
uint8_t effective_execute_acei(const OneWayControllerIdentity &identity)
The ACEI byte a given identity will put on air for a 1W EXECUTE frame.
ExchangeOutcome
Authenticated exchange engine — outbound and inbound protocol flows.
std::string tuning_config_full_snapshot(const TuningConfig &cfg)
Format the current tuning configuration as a full one-line snapshot.
const TuningSelectParam * find_tuning_select(const std::string &name)
Look up a select tuning parameter by name; returns nullptr if unknown.
std::string tuning_update_log_line(const std::string &name, const std::string &value)
Format a single tuning update for the log.
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.
const char * oneway_power_class_name(OneWayPowerClass power_class)
Human-readable name for a OneWayPowerClass, as it appears in the boot log.
ListenOutcome
How one call to ExchangeEngine::listen() ended.
@ ACCEPTED
The handler returned ReplyDisposition::ACCEPT for some received frame.
const TuningNumberParam * find_tuning_number(const std::string &name)
Look up a numeric tuning parameter by name; returns nullptr if unknown.
uint16_t effective_discovery_preamble(const TuningConfig &cfg)
The preamble length (bytes) of the pairing discovery broadcast (0x28, 0x2E).
bool has_invert_override(const OneWayControllerIdentity &identity)
Whether this identity's inversion comes from an explicit invert_position: rather than the kind.
bool has_power_class_override(const OneWayControllerIdentity &identity)
Whether this identity's burst shape comes from an explicit low_power: rather than the profile.
TravelSpeed
The speed a cover's position moves ask the motor for (ADR 0065).
OneWayWireProfile resolve_oneway_wire_profile(uint8_t manufacturer)
Resolve an identity's 1W wire profile from its manufacturer byte.
bool has_execute_acei_override(const OneWayControllerIdentity &identity)
Whether this identity's ACEI comes from an explicit execute_acei: rather than the profile.
bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len)
Convert a hex string (e.g., "123ABC") to a byte array.
static const char *const TAG
Definition hub_core.cpp:58
LR1121 radio driver for IO-Homecontrol.
SX1262 radio driver for IO-Homecontrol.
SX1276 radio driver for IO-Homecontrol.
YAML-declared device metadata for registration; defaults match an undeclared device.
Runtime state of a paired IO‑Homecontrol device.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93
uint8_t ctrl1
Control byte 1: low power, beacon, etc.
Definition proto_frame.h:95
How one listen window is to be spent — everything ExchangeEngine::listen() needs; everything else is ...
uint32_t window_ms
Total time budget for this listen, in milliseconds.
uint32_t linger_dwell_ms
Length of the preamble/sync extension, in milliseconds.
ListenPolicy policy
Which channels this listen covers.
bool linger_on_preamble
Extend the listen instead of hopping while the chip reports a preamble or sync word,...
What a 1W command attempt did — the only feedback this feature can ever produce.
One configured 1W controller identity.
Vendor-divergent 1W wire settings for a controller identity.
bool profile_is_a_guess
true when manufacturer matched no known 1W wire profile.
EnrollGesture enroll_gesture
Which enrollment gesture this manufacturer's actuators expect.
Raw packet received from the radio.
bool active
True when the YAML tuning: block is present.
uint16_t normal_start_preamble
Preamble for a directed start frame to a non-low-power target.
uint32_t remote_activity_catchup_poll_ms
Read every linked device this long after the last exchange of a burst of linked-remote activity; 0 = ...
uint16_t low_power_wake_preamble
Wake-up preamble (bytes) on a directed start frame to a low_power target believed at rest.
bool pairing_discovery_preamble_set
True when pairing_discovery_preamble was set, from YAML or by a write through the Home Assistant numb...
bool low_power_wake_belief
Order a low_power device's start-frame tries by its wake belief (short preamble first when it is beli...
bool normal_start_preamble_from_yaml
True when normal_start_preamble: appeared in YAML.
One numeric tuning parameter: its wire name plus accessors over TuningConfig.
void(*) set(TuningConfig &, float)
Write a new value (narrowed to the field type).
bool applies_to_radio
True if changes must be re-applied to the radio.
float(*) get(const TuningConfig &)
Read the current value as a float.
One select tuning parameter: its wire name plus string accessors over TuningConfig.
bool(*) set(TuningConfig &, const std::string &)
Parse/apply an option; false if unparseable.
std::string(*) get(const TuningConfig &)
Read the current option string.
bool applies_to_radio
True if changes must be re-applied to the radio.
What a caller asks of one exchange's attempts: a plain cap on tries (today's shape),...
bool state_unknown
True for a read whose evidence about the receiver may be stale (with_state_unknown()).
bool wake_preamble_first
True for the read at a moving cover's predicted end of travel (with_wake_preamble_first()).
ReadKind kind
What the read is for; only read when planned.
bool planned
True: build the plan for kind instead of using a count.
uint8_t max_tries
Cap on attempts; ignored once kind is set.
Runtime tuning configuration for pairing and radio diagnostics.
Table-driven registry of runtime tuning parameters.