Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
tuning_config.h
Go to the documentation of this file.
1#pragma once
2
3/// @file tuning_config.h
4/// @brief Runtime tuning configuration for pairing and radio diagnostics.
5/// @ingroup hioc_tuning
6///
7/// Provides a centralized, non-persistent tuning layer used by the hub and radio
8/// drivers. The `TuningConfig` struct carries all tunable parameters and helpers
9/// for logging and applying defaults.
10
11#include "proto_frame.h"
12#include "proto_timing.h"
13
14#include <cstddef>
15#include <cstdint>
16#include <optional>
17#include <string>
18#include <vector>
19
20namespace esphome {
21namespace home_io_control {
22
23/// @brief Valid SX1262 RX bandwidth options (kHz register values).
24///
25/// The numeric values are the register-encoded (double-sideband) bandwidth selectors used by
26/// `RadioSX1262::set_rx_bandwidth()` — a regular (mantissa, exponent) grid, with the bandwidth
27/// roughly doubling per group.
28///
29/// Semtech's sizing rule for GFSK is `BW_DSB >= bitrate + 2 * Fdev + carrier offset`, which is
30/// 76.8 kHz for the IO-Homecontrol waveform (38.4 kbps, 19.2 kHz deviation) before any offset.
31/// The SX1262 has no AFC in GFSK mode, so this filter alone has to absorb the peer's carrier
32/// offset. These values are not comparable to `SX1276RxBandwidth`, whose values are
33/// single-sideband.
34///
35/// Byte-for-byte identical to `LR1121RxBandwidth` below, since both chips share the same Semtech
36/// GFSK bandwidth grid; the `Sx1262AndLr1121BandwidthTablesAgree` test pins that. If these two
37/// tables ever need to diverge for a real chip difference, say why here.
38enum class SX1262RxBandwidth : uint8_t {
39 BW_39_0_KHZ = 0x1C, ///< 39.0 kHz — narrowest; half the 76.8 kHz sizing floor, for probing only.
40 BW_46_9_KHZ = 0x14, ///< 46.9 kHz — narrow.
41 BW_58_6_KHZ = 0x0C, ///< 58.6 kHz — default; the narrowest value validated on real hardware here.
42 BW_78_2_KHZ = 0x1B, ///< 78.2 kHz — just above the 76.8 kHz sizing floor.
43 BW_117_3_KHZ = 0x0B, ///< 117.3 kHz — clears the sizing floor with ~20 kHz of offset margin.
44 BW_156_2_KHZ = 0x1A, ///< 156.2 kHz — the value a Somfy LightVar_Wh_io dimmer needs.
45 BW_187_2_KHZ = 0x12, ///< 187.2 kHz — widest selectable option.
46};
47
48/// @brief Valid SX1276 RX bandwidth options (RegRxBw register bytes).
49///
50/// The numeric values are the SX1276 RegRxBw encodings (RxBwMant in bits[4:3], RxBwExp in
51/// bits[2:0]) written verbatim to both REG_RX_BW and REG_AFC_BW. Single-sideband bandwidth =
52/// FXOSC / (RxBwMant * 2^(RxBwExp+2)) with FXOSC = 32 MHz; the filter's total (double-sideband)
53/// width is twice that, so the 41.7 kHz default spans 83.4 kHz — just above the 76.8 kHz sizing
54/// floor for this waveform. The SX1276's AFC re-centres the receiver on each packet's carrier.
55/// Narrower rejects more out-of-band noise (higher sensitivity); wider tolerates more LO
56/// frequency offset.
57enum class SX1276RxBandwidth : uint8_t {
58 BW_20_8_KHZ = 0x14, ///< 20.8 kHz — narrowest; maximal noise rejection, least LO-offset tolerance.
59 BW_41_7_KHZ = 0x13, ///< 41.7 kHz — default (validated against real devices).
60 BW_62_5_KHZ = 0x03, ///< 62.5 kHz.
61 BW_83_3_KHZ = 0x12, ///< 83.3 kHz.
62 BW_125_0_KHZ = 0x02, ///< 125.0 kHz — widest selectable option.
63};
64
65/// @brief Valid LR1121 RX bandwidth options (register values).
66///
67/// Byte-for-byte identical to `SX1262RxBandwidth` — both chips use the same Semtech GFSK
68/// bandwidth grid — and kept as a distinct enum only so each driver's options can diverge if a
69/// future chip's table does. See `Sx1262AndLr1121BandwidthTablesAgree`, which pins the two tables
70/// together; if they ever need to differ for a real chip difference, say why here.
71enum class LR1121RxBandwidth : uint8_t {
72 BW_39_0_KHZ = 0x1C, ///< 39.0 kHz — narrowest; half the 76.8 kHz sizing floor, for probing only.
73 BW_46_9_KHZ = 0x14, ///< 46.9 kHz — narrow.
74 BW_58_6_KHZ = 0x0C, ///< 58.6 kHz.
75 BW_78_2_KHZ = 0x1B, ///< 78.2 kHz.
76 BW_117_3_KHZ = 0x0B, ///< 117.3 kHz — default.
77 BW_156_2_KHZ = 0x1A, ///< 156.2 kHz — wider tolerance for peer carrier offset.
78 BW_187_2_KHZ = 0x12, ///< 187.2 kHz — widest selectable option.
79};
80
81/// @brief Which power classes scan_paired_devices() calls.
82///
83/// The roll-call calls both classes by default; this narrows it for bisecting an installation
84/// in the field (does a device answer the low-power call but not the always-alive one, or vice
85/// versa?) or for restoring the shorter single-pass scan on an install with no solar/battery
86/// devices. Values mirror `power_save_mode_name()` (proto_constants.h) so the report, this
87/// tunable, and a responder's own self-reported power-save byte share one vocabulary.
88enum class ScanPowerClasses : uint8_t {
89 BOTH, ///< Low-power pass, then always-alive pass (default).
90 ALWAYS_ALIVE, ///< Always-alive pass only: the single-shape roll-call, ~6 s.
91 LOW_POWER, ///< Low-power pass only.
92};
93
94/// @brief Whether and how PairingEngine sends CMD_DISCOVER_CONFIRM (0x2C) to a freshly-discovered
95/// device before proceeding to the key exchange (0x31).
96///
97/// Every real controller in this project's corpus sends 0x2C between the device's 0x29 and its
98/// own 0x31. An enum, not a bool reusing `pairing_discovery_ack_capable`: that knob's contract is
99/// scoped to the discovery broadcast only (see its own doc), and `skip`/`send`/`send_with_ack`
100/// avoid `on`/`off`, which YAML 1.1 would otherwise turn into booleans. The step never fails a
101/// pairing attempt in any mode — see PairingEngine::run_discover_confirm_step_()'s doc.
102enum class DiscoverConfirmMode : uint8_t {
103 SKIP, ///< Kill switch: sends no 0x2C and applies no post-step pause.
104 SEND, ///< Default. Sends 0x2C with CTRL1_ACK clear either way — `0x00` to an
105 ///< always-alive target, `0x20` (CTRL1_LOW_POWER only) to a low-power one. It is
106 ///< CTRL1_LOW_POWER, not CTRL1_ACK, that follows the target's power class — see
107 ///< create_discover_confirm()'s `ack` param doc and
108 ///< PairingEngine::run_discover_confirm_step_() for where that split lives.
109 ///< Hardware-validated on a Somfy Izymo dimmer (always-alive): it answers the
110 ///< `0x00` shape with 0x2D within ~25 ms.
111 SEND_WITH_ACK, ///< Like SEND, but also sets CTRL1_ACK for an always-alive target (`0x10`) — the
112 ///< shape every corpus hub (VELUX KLR200/KIG300, Somfy Connectivity Kit) uses
113 ///< there. A low-power target still gets `0x20`, identical to SEND. Not the
114 ///< default: the Somfy Izymo dimmer never answers the `0x10` shape with 0x2D
115 ///< (pairing still completes, after the full no-answer wait). Kept for an
116 ///< always-alive device from a hub ecosystem that sends this shape and does not
117 ///< answer SEND's.
118};
119
120/// @brief Which channels PairingEngine's discovery wait listens on.
121///
122/// A broadcast's answers are not pinned to the channel the request went out on: every device that
123/// replies is continuing its own hopping rather than joining a pinned conversation (see
124/// @ref ListenPolicy). Somfy always-alive roll-call replies land back on the request channel about
125/// 1 in 149 times, so skipping it buys a third more dwell on the two channels that carry almost
126/// all of them — hence the default. `all` exists because that measurement covers responders we
127/// have already heard from: a device that answers discovery *only* on the request channel is
128/// indistinguishable, from our side, from one that never answers at all.
129enum class DiscoveryListenChannels : uint8_t {
130 SKIP_REQUEST, ///< Default. The two channels that are not the request channel.
131 ALL, ///< CH1->CH2->CH3, including the request channel. Costs a third of the dwell on
132 ///< the other two; use it to rule out a responder that only answers on CH2.
133};
134
135/// @brief Which direction PairingEngine moves the system key in after discovery.
136///
137/// `auto` (the default) picks the direction from the device's discovery answer: a device that answers
138/// as a remote controller (node type 1023) is pulled, any other device is pushed. There is no switch to the
139/// other direction inside one attempt: a refusal repeats, and a retry costs pairing time. `push` and
140/// `pull` force the direction.
141///
142/// `push` is the usual pairing: we send our key to the device (0x31 key init, the device's 0x3C,
143/// our 0x32, its 0x33). `pull` asks the device for its own key instead: we send a 0x38 key-transfer
144/// request with six random bytes, the device answers with a 0x32 holding its key, and we challenge
145/// it (0x3C) to prove it holds that key before trusting it. An Atlantic AGILIA IO radiator in its
146/// display's pairing mode answers a pushed key with RESULT_COMMAND_UNKNOWN (tests/corpus/captures/
147/// pairing/atlantic_agilia_pairing_key_transfer_command_unknown.yaml); `auto` pulls such a device. A
148/// pulled key is carried by the devices that answer a roll-call under it, never adopted as the hub's:
149/// see PairingEngine::run_key_pull_phase_() and register_pulled_key_devices_().
150enum class KeyTransferMode : uint8_t {
151 AUTO, ///< Default. Pull from a device that answered discovery as a remote controller, push to any other.
152 PUSH, ///< Send our system key to the device.
153 PULL, ///< Ask the device for its key, then run a roll-call under it to find the devices that hold it.
154};
155
156/// @brief Discovery request command codes.
157enum class DiscoveryCommand : uint8_t {
158 DISCOVER = 0x28, ///< Standard broadcast discovery request (to 0x00003B).
159 DISCOVER_SPE = 0x2A, ///< SPE roll-call request (self-authenticating; see CMD_DISCOVER_SPE_REQ).
160 ///< Deliberately **not** offered as a `pairing_discovery_commands` preset
161 ///< in tuning.py: only devices that already hold the system key answer it,
162 ///< so it can never help reach a device in learning mode. Retained here
163 ///< because the command byte is still needed to *send* a roll-call, and
164 ///< because discovery_command_from_string() stays permissive enough to
165 ///< parse it — do not remove either as dead code.
166 DISCOVER_ALT = 0x2E, ///< Alternate broadcast discovery (to 0x00003F), with optional payload byte.
167};
168
169// Chip-specific radio defaults live here beside the TuningConfig fields they initialize — the
170// single source of truth for defaults. proto_timing.h holds only chip-neutral protocol values;
171// the radio drivers consume these via apply_tuning / hop_dwell_ms.
172
173/// SX1262-specific preamble for response/continuation frames within an exchange.
174///
175/// Applies to any tight-turnaround frame sent immediately after receiving from the device —
176/// 0x3D challenge responses, 0x32 key transfers after receiving 0x3C, and any future protocol
177/// frame in that position — giving the peer's receiver time to lock back on after the SX1262's
178/// own TX→RX turnaround.
179///
180/// 8 bytes was validated on real hardware (Heltec V3.2 ↔ Device actuator) as the minimum that
181/// gives reliable lock-on without perturbing exchange timing; it matches the protocol's own
182/// nominal SHORT_PREAMBLE floor.
183///
184/// This constant is byte-denominated, like every other preamble value in this codebase
185/// (LONG_PREAMBLE, SHORT_PREAMBLE) — `RadioSX1262::set_packet_params_()` is the one place that
186/// converts to the chip's bit-denominated SetPacketParams field, right before the value leaves
187/// for the wire.
188static constexpr uint16_t SX1262_RESPONSE_PREAMBLE = 8;
189
190/// SX1262-specific post-TX settling delay before re-entering RX.
191///
192/// The SX1262 GFSK demodulator needs time to stabilize after TX before it can
193/// reliably receive the next frame. A 500 µs delay was validated as the minimum
194/// that prevents challenge-byte corruption during pairing and tight-turnaround
195/// authenticated exchanges.
196static constexpr uint16_t SX1262_POST_TX_SETTLE_US = 500;
197
198/// SX1276 preamble for response/continuation frames within an exchange.
199///
200/// Defaults to 12 bytes — longer than the protocol's 8-byte SHORT_PREAMBLE. On real hardware the
201/// extra length improves the peer device's lock-on without measurably affecting exchange timing.
202/// Runtime-tunable down to SHORT_PREAMBLE or up for a marginal-range install.
203static constexpr uint16_t SX1276_RESPONSE_PREAMBLE = 12;
204
205/// Per-channel dwell while SX1276 pairing discovery hops across channels.
206///
207/// The SX1276 supports FastHop (no standby transition), so a short slice keeps
208/// discovery sweeping all three channels quickly.
209static constexpr uint16_t SX1276_DISCOVERY_HOP_SLICE_MS = 5;
210
211/// Per-channel dwell while SX1262 pairing discovery hops across channels.
212///
213/// SX1262 frequency changes require a standby→SetRfFrequency→RX cycle, unlike the SX1276's
214/// FastHop, but that changes only how much a single retune costs, not how long the radio should
215/// then sit still: a short dwell fits more retunes into the listen window than a long one, so the
216/// receiver is more often already parked on the right channel by the time a reply starts. The
217/// preamble/sync linger guard (`ListenSpec::linger_on_preamble`) is what makes a dwell this short
218/// safe — it keeps a caught reply from being cut off mid-reception by extending the dwell instead
219/// of hopping away. A dwell in the low single-digit milliseconds performs best; shorter than that,
220/// coverage degrades gradually and only truly collapses at `0`, where `wait_for_packet(..., 0)`
221/// returns before any guard can observe activity at all. `7` is the best-performing value found in
222/// that range.
223static constexpr uint16_t SX1262_DISCOVERY_HOP_SLICE_MS = 7;
224
225/// LR1121-specific preamble for response/continuation frames within an exchange.
226///
227/// Seeded from the SX1262-validated default, on the principle that a validated timing value
228/// encodes protocol-side reality more than a chip quirk. Not yet independently validated on
229/// LR1121 hardware; a value measured by loopback tuning would fold back here.
231
232/// LR1121-specific post-TX settling delay before re-entering RX.
233///
234/// Seeded from the SX1262-validated default; same rationale as @ref LR1121_RESPONSE_PREAMBLE.
236
237/// Per-channel dwell while LR1121 pairing discovery hops across channels.
238///
239/// Measured independently on LR1121 hardware rather than inherited from
240/// @ref SX1262_DISCOVERY_HOP_SLICE_MS — that constant's short-dwell reasoning applies equally
241/// here, but the two chips are validated separately and could in principle diverge, so this stays
242/// its own literal rather than an alias.
243static constexpr uint16_t LR1121_DISCOVERY_HOP_SLICE_MS = 7;
244
245/// Default directed start preamble for drivers built on the shared software PHY (SX1262, LR1121).
246///
247/// 48 bytes rather than the protocol's documented 32. Measured: on a Somfy Izymo dimmer an SX1262
248/// answers 56/71 = 78.9% of first tries at 32 bytes and 60/60 at 48, 64, 128 and 256 — a step, not
249/// a ramp — while an SX1276 sending the same programmed 32 bytes answers 60/60. The deficit tracks
250/// the shared PHY rather than the chip, the link or the device, so the compensation belongs with
251/// the PHY. The cause of the shortfall is not identified; see ADR 0042.
252static constexpr uint16_t SOFT_PHY_START_PREAMBLE = 48;
253
254/// @brief All runtime tunable parameters for pairing and radio diagnostics.
255///
256/// Values reset to their defaults on every boot. Each field initializes from a
257/// canonical constant — chip-neutral defaults live in `proto_timing.h`, chip-specific
258/// defaults in this header — shared with the ESPHome YAML schema. The hub stores a single
259/// `TuningConfig` instance and passes it to the radio driver and pairing flow. UI
260/// callbacks update the hub instance directly; the snapshot helpers emit the current
261/// values in YAML-compatible form so a working combination can be copied back into the
262/// configuration file.
264 // --- Radio / physical layer ---
265 /// SX1262 RX bandwidth selector.
266 ///
267 /// 58.6 kHz (double-sideband). This is below the 76.8 kHz sizing floor for this waveform (see
268 /// `SX1262RxBandwidth`), so it trims the edges of even an on-frequency signal; it is the
269 /// default because it made a real solar shutter (Somfy RS100) respond reliably where 117.3 kHz
270 /// did not. Because the SX1262 has no GFSK AFC, a peer whose carrier sits off nominal needs a
271 /// wider setting instead: a Somfy LightVar_Wh_io dimmer went from mostly missed to mostly
272 /// confirmed replies at 156.2 kHz. The setting is global, so the right value is a per-install trade-off.
274 uint16_t sx1262_response_preamble{SX1262_RESPONSE_PREAMBLE}; ///< SX1262 response preamble in bytes.
275 uint16_t sx1262_post_tx_settle_us{SX1262_POST_TX_SETTLE_US}; ///< Delay after SX1262 TX before RX (µs).
277 uint16_t sx1276_response_preamble{SX1276_RESPONSE_PREAMBLE}; ///< SX1276 response preamble in bytes.
279 SX1276_DISCOVERY_HOP_SLICE_MS}; ///< Per-channel dwell while SX1276 discovery hops.
281 SX1262_DISCOVERY_HOP_SLICE_MS}; ///< Per-channel dwell while SX1262 discovery hops.
283 uint16_t lr1121_response_preamble{LR1121_RESPONSE_PREAMBLE}; ///< LR1121 response preamble in bytes.
284 uint16_t lr1121_post_tx_settle_us{LR1121_POST_TX_SETTLE_US}; ///< Delay after LR1121 TX before RX (µs).
286 LR1121_DISCOVERY_HOP_SLICE_MS}; ///< Per-channel dwell while LR1121 discovery hops.
288 COLD_BROADCAST_REPLY_PREAMBLE}; ///< Preamble for a start-flagged key-extraction broadcast reply (0x29).
290 NORMAL_START_PREAMBLE}; ///< Preamble for a directed start frame to a non-low-power target.
291 ///< Resolved at setup from RadioDriver::default_start_preamble()
292 ///< unless `normal_start_preamble_from_yaml` says the user set it.
293 /// True when `normal_start_preamble:` appeared in YAML. Codegen emits this alongside the value;
294 /// it exists so setup() can tell "the user chose 32" from "nobody chose anything", which decides
295 /// whether the driver's default applies (ADR 0042). Only read once, at setup — a later change
296 /// through the Home Assistant number simply sets the value.
298 uint8_t lbt_max_retries{LBT_MAX_RETRIES}; ///< LBT retries before forced TX.
299 int16_t lbt_rssi_threshold_dbm{LBT_RSSI_THRESHOLD_DBM}; ///< LBT channel-free threshold (dBm).
300 bool low_power_wake_belief{true}; ///< Order a `low_power` device's start-frame tries by its wake belief (short
301 ///< preamble first when it is believed awake). False restores the fixed
302 ///< wake-up preamble on every try. A diagnostic off-switch, on by default.
303 uint16_t low_power_wake_preamble{LONG_PREAMBLE}; ///< Wake-up preamble (bytes) on a directed start frame to a
304 ///< `low_power` target believed at rest.
305
306 // --- Exchange response windows ---
307 // How long the hub listens for a device's reply. Tunable because the right value is a property
308 // of the *device*, not of the radio or the protocol: observed reply latencies on one network
309 // span two orders of magnitude (a mains Oximo 40 at ~23 ms, a solar RS100 at 29-3052 ms). See
310 // RESPONSE_START_WAIT_MS for the measurements behind the defaults.
312 RESPONSE_START_WAIT_MS}; ///< Reply window for a start frame (wakes a sleeping device).
314 RESPONSE_WAIT_MS}; ///< Reply window for continuation frames and the post-auth final response.
315 uint16_t exchange_total_budget_ms{EXCHANGE_TOTAL_BUDGET_MS}; ///< Ceiling on one whole exchange, retries included.
316
317 // --- Pairing protocol ---
318 std::vector<DiscoveryCommand> pairing_discovery_commands{
319 DiscoveryCommand::DISCOVER}; ///< Ordered discovery commands.
320 std::vector<uint8_t> pairing_discovery_destination{0x00, 0x00, 0x00}; ///< Destination when auto=false.
321 bool pairing_discovery_destination_auto{true}; ///< When true, map commands to conventional addresses.
322 uint8_t pairing_discovery_payload{0}; ///< Optional payload byte (used for 0x2E).
323 bool pairing_discovery_payload_enabled{false}; ///< Whether the optional payload is enabled.
324 bool pairing_discovery_low_power{false}; ///< Set LOW_POWER flag in discovery frames.
325 bool pairing_discovery_ack_capable{false}; ///< Set ACK (CTRL1_ACK) on the discovery broadcast only.
326 ///< Opt-in — see the init_frame() doc in
327 ///< proto_frame.h; default off.
329 PAIRING_DISCOVERY_PREAMBLE}; ///< Preamble for the discovery broadcast (0x28/0x2E) start frame.
331 PAIRING_DISCOVERY_WAIT_MS}; ///< Total wait window after sending discovery commands.
333 PAIRING_DISCOVERY_INITIAL_DWELL_MS}; ///< Initial dwell on CH2 before discovery hopping begins.
335 PAIRING_KEY_EXCHANGE_RETRIES}; ///< Retries for the authenticated key exchange phase.
337 ScanPowerClasses::BOTH}; ///< Power classes the scan_paired_devices roll-call calls.
339 DiscoverConfirmMode::SEND}; ///< Whether/how to send CMD_DISCOVER_CONFIRM (0x2C) during pairing.
342 KeyTransferMode pairing_key_transfer{KeyTransferMode::AUTO}; ///< Push our key, or pull the device's key.
344 PAIRING_KEY_INIT_DELAY_MS}; ///< Pause after the discover-confirm step, before CMD_KEY_INIT (0x31).
345
346 // --- Internal state ---
347 bool active{false}; ///< True when the YAML `tuning:` block is present.
348};
349
350/// @brief One selectable RX bandwidth: the chip's register byte and its nominal kHz value.
351///
352/// The per-chip bandwidth conversion functions below (SX1262/SX1276/LR1121) are all the same
353/// logic over a different table of these; they are thin typed wrappers around the three generic
354/// helpers that follow, which own the register-byte lookup, the "%.1f" formatting, and the
355/// input normalization once instead of three times.
357 uint8_t reg;
358 float khz;
359};
360
361/// @brief Look up the kHz value for a register byte in a bandwidth table.
362/// @param table Bandwidth option table.
363/// @param n Number of entries in @p table.
364/// @param reg Register byte to look up.
365/// @param fallback kHz value returned when @p reg is not in the table.
366float bandwidth_to_khz(const BandwidthOption *table, size_t n, uint8_t reg, float fallback);
367
368/// @brief Format a kHz value as its YAML/UI option string (bare number, one decimal, e.g. "117.3").
369std::string bandwidth_to_string(float khz);
370
371/// @brief Parse a YAML/UI bandwidth string against a table, returning the matching register byte.
372///
373/// Accepts surrounding whitespace and a trailing "kHz" suffix in any case, and accepts both the
374/// one-decimal spelling ("39.0") and the integer-truncated spelling ("39") of every table entry.
375/// @return The register byte, or std::nullopt when the string matches no entry.
376std::optional<uint8_t> bandwidth_from_string(const BandwidthOption *table, size_t n, const std::string &value);
377
378/// @brief A pointer/size view over one chip's RX-bandwidth option table.
383
384/// @brief The SX1262 RX-bandwidth option table. Exposed so tests derive their legal-option
385/// lists from the same source production uses rather than re-listing them by hand.
387/// @brief The SX1276 RX-bandwidth option table (see sx1262_bandwidth_table()).
389/// @brief The LR1121 RX-bandwidth option table (see sx1262_bandwidth_table()).
391
392/// @brief Convert a bandwidth enum to the numeric kHz value used in YAML/logs.
393/// @param bw SX1262 bandwidth enum.
394/// @return Floating-point kHz value (39.0, 46.9, 58.6, 78.2, 117.3, 156.2, or 187.2).
396
397/// @brief Format a bandwidth enum as its YAML/UI option string (bare kHz number, e.g. "117.3").
398/// @param bw SX1262 bandwidth enum.
399/// @return Bandwidth rendered with one decimal place and a "kHz" suffix.
401
402/// @brief Convert a YAML bandwidth string to the enum value.
403/// @param value YAML string such as "117.3kHz" or "156.2kHz".
404/// @return Bandwidth enum, or std::nullopt on invalid input.
405std::optional<SX1262RxBandwidth> sx1262_bandwidth_from_string(const std::string &value);
406
407/// @brief Convert an SX1276 bandwidth enum to the numeric kHz value used in YAML/logs.
408/// @param bw SX1276 bandwidth enum.
409/// @return Floating-point kHz value (20.8, 41.7, 62.5, 83.3, or 125.0).
411
412/// @brief Format an SX1276 bandwidth enum as its YAML/UI option string (bare kHz number).
413/// @param bw SX1276 bandwidth enum.
414/// @return Bandwidth rendered with one decimal place (e.g. "41.7").
416
417/// @brief Convert a YAML SX1276 bandwidth string to the enum value.
418/// @param value YAML string such as "41.7" or "83.3kHz".
419/// @return Bandwidth enum, or std::nullopt on invalid input.
420std::optional<SX1276RxBandwidth> sx1276_bandwidth_from_string(const std::string &value);
421
422/// @brief Convert an LR1121 bandwidth enum to the numeric kHz value used in YAML/logs.
423/// @param bw LR1121 bandwidth enum.
424/// @return Floating-point kHz value (39.0, 46.9, 58.6, 78.2, 117.3, 156.2, or 187.2).
426
427/// @brief Format an LR1121 bandwidth enum as its YAML/UI option string (bare kHz number).
428/// @param bw LR1121 bandwidth enum.
429/// @return Bandwidth rendered with one decimal place (e.g. "117.3").
431
432/// @brief Convert a YAML LR1121 bandwidth string to the enum value.
433/// @param value YAML string such as "117.3" or "39.0kHz".
434/// @return Bandwidth enum, or std::nullopt on invalid input.
435std::optional<LR1121RxBandwidth> lr1121_bandwidth_from_string(const std::string &value);
436
437/// @brief Format the current tuning configuration as a one-line YAML-compatible snapshot.
438///
439/// Only values that differ from the default are emitted, so the line can be copied
440/// back into YAML with minimal editing. When all values are default, an empty string
441/// is returned.
442///
443/// @param cfg Current tuning configuration.
444/// @return One-line snapshot string, or empty when no overrides are active.
445std::string tuning_config_snapshot(const TuningConfig &cfg);
446
447/// @brief Format the current tuning configuration as a full one-line snapshot.
448///
449/// Emits every non-default value, regardless of whether it was changed from YAML or
450/// from the HA UI. This is logged at the start of every pairing attempt.
451///
452/// @param cfg Current tuning configuration.
453/// @return One-line snapshot string, or a "defaults" marker when nothing is overridden.
454std::string tuning_config_full_snapshot(const TuningConfig &cfg);
455
456/// @brief Format a single tuning update for the log.
457///
458/// @param name YAML key name of the updated parameter.
459/// @param value YAML-compatible string representation of the new value.
460/// @return One-line log string suitable for ESP_LOGI.
461std::string tuning_update_log_line(const std::string &name, const std::string &value);
462
463/// @brief Resolve the destination address for a discovery command.
464///
465/// When `destination_auto` is true, returns the conventional address for the
466/// given command code. Otherwise returns the configured `destination` value.
467///
468/// @param command Discovery command code.
469/// @param destination_auto Whether to use the automatic mapping.
470/// @param destination Explicit 3-byte destination when auto is false.
471/// @return Pointer to the resolved 3-byte node ID.
472const uint8_t *resolve_discovery_destination(uint8_t command, bool destination_auto,
473 const uint8_t destination[NODE_ID_SIZE]);
474
475/// @brief Parse a discovery command string (e.g., "0x28") into the enum.
476/// @param value String to parse.
477/// @return Discovery command enum, or std::nullopt on invalid input.
478std::optional<DiscoveryCommand> discovery_command_from_string(const std::string &value);
479
480/// @brief Format a discovery command enum for YAML/logs.
481/// @param cmd Discovery command enum.
482/// @return Lowercase hex string such as "0x28".
484
485/// @brief Format a destination option for YAML/logs.
486///
487/// @param destination_auto Whether automatic destination mapping is used.
488/// @param destination Explicit 3-byte destination when auto is false.
489/// @return "auto" or a hex address such as "0x00003B".
490std::string discovery_destination_to_string(bool destination_auto, const uint8_t destination[NODE_ID_SIZE]);
491
492/// @brief Format a payload option for YAML/logs.
493///
494/// @param payload_enabled Whether the optional payload is enabled.
495/// @param payload The payload byte when enabled.
496/// @return "none" or a hex byte such as "0x00".
497std::string discovery_payload_to_string(bool payload_enabled, uint8_t payload);
498
499/// @brief Format the ordered discovery command list for logs.
500///
501/// @param commands Ordered list of discovery commands.
502/// @return Bracketed comma-separated list such as "[0x28,0x2E]".
503std::string discovery_commands_to_string(const std::vector<DiscoveryCommand> &commands);
504
505/// @brief Format the ordered discovery command list as a UI/preset option string.
506///
507/// Matches the comma-separated `select` option labels (no brackets) so a boot-time
508/// snapshot round-trips to a selectable dropdown value.
509///
510/// @param commands Ordered list of discovery commands.
511/// @return Comma-separated list such as "0x28,0x2E" (empty string when the list is empty).
512std::string discovery_commands_to_csv(const std::vector<DiscoveryCommand> &commands);
513
514/// @brief Format a ScanPowerClasses value for YAML/logs.
515///
516/// `BOTH` is its own literal; the two single-class values return
517/// `power_save_mode_name(POWER_SAVE_ALWAYS_ALIVE)` / `power_save_mode_name(POWER_SAVE_LOW_POWER)`
518/// so the roll-call report and this tunable share one vocabulary rather than a second spelling.
519/// @param value Selection to format.
520/// @return "both", "always_alive", or "low_power".
522
523/// @brief Parse a scan_power_classes string into the enum.
524///
525/// Exact, case-sensitive match against the same three strings scan_power_classes_to_string()
526/// produces.
527/// @param value String to parse.
528/// @return The matching selection, or std::nullopt on invalid input.
529std::optional<ScanPowerClasses> scan_power_classes_from_string(const std::string &value);
530
531/// @brief Whether `selection` calls the roll-call pass for `power_save_mode`.
532///
533/// Pure predicate, used both by the sweep loop (which pass to skip) and by the report (which
534/// class to name in the selection NOTE) — one source of truth for "does this selection include
535/// that class" instead of two.
536/// @param selection Current `scan_power_classes` tuning value.
537/// @param power_save_mode POWER_SAVE_ALWAYS_ALIVE or POWER_SAVE_LOW_POWER.
538/// @return true if `selection` calls that class; false for an unrecognized power_save_mode.
539bool scan_power_classes_include(ScanPowerClasses selection, uint8_t power_save_mode);
540
541/// @brief Format a DiscoverConfirmMode value for YAML/logs.
542/// @param value Selection to format.
543/// @return "skip", "send", or "send_with_ack".
545
546/// @brief Parse a `pairing_discover_confirm` string into the enum.
547///
548/// Exact, case-sensitive match against the same three strings discover_confirm_mode_to_string()
549/// produces.
550/// @param value String to parse.
551/// @return The matching mode, or std::nullopt on invalid input.
552std::optional<DiscoverConfirmMode> discover_confirm_mode_from_string(const std::string &value);
553
554/// @brief Render a DiscoveryListenChannels as its YAML/select option string.
555/// @param value Selection to render.
556/// @return `"skip_request"` or `"all"`.
558
559/// @brief Parse a DiscoveryListenChannels from its YAML/select option string.
560///
561/// Exact, case-sensitive match against the same strings discovery_listen_channels_to_string()
562/// emits, so the two stay each other's inverse.
563/// @param value Option string to parse.
564/// @return The selection, or `std::nullopt` if `value` is not one of the options.
565std::optional<DiscoveryListenChannels> discovery_listen_channels_from_string(const std::string &value);
566
567/// @brief Render a KeyTransferMode as its YAML/select option string.
568/// @param value Selection to render.
569/// @return `"push"` or `"pull"`.
571
572/// @brief Parse a KeyTransferMode from its YAML/select option string.
573///
574/// Exact, case-sensitive match against the strings key_transfer_mode_to_string() emits.
575/// @param value Option string to parse.
576/// @return The selection, or `std::nullopt` if `value` is not one of the options.
577std::optional<KeyTransferMode> key_transfer_mode_from_string(const std::string &value);
578
579} // namespace home_io_control
580} // namespace esphome
BandwidthTableView lr1121_bandwidth_table()
The LR1121 RX-bandwidth option table (see sx1262_bandwidth_table()).
std::string discovery_commands_to_csv(const std::vector< DiscoveryCommand > &commands)
Format the ordered discovery command list as a UI/preset option string.
DiscoveryListenChannels
Which channels PairingEngine's discovery wait listens on.
@ ALL
CH1->CH2->CH3, including the request channel.
@ SKIP_REQUEST
Default. The two channels that are not the request channel.
std::optional< LR1121RxBandwidth > lr1121_bandwidth_from_string(const std::string &value)
Convert a YAML LR1121 bandwidth string to the enum value.
SX1262RxBandwidth
Valid SX1262 RX bandwidth options (kHz register values).
@ BW_39_0_KHZ
39.0 kHz — narrowest; half the 76.8 kHz sizing floor, for probing only.
@ BW_58_6_KHZ
58.6 kHz — default; the narrowest value validated on real hardware here.
@ BW_78_2_KHZ
78.2 kHz — just above the 76.8 kHz sizing floor.
@ BW_156_2_KHZ
156.2 kHz — the value a Somfy LightVar_Wh_io dimmer needs.
@ BW_117_3_KHZ
117.3 kHz — clears the sizing floor with ~20 kHz of offset margin.
@ BW_187_2_KHZ
187.2 kHz — widest selectable option.
std::string discovery_payload_to_string(bool payload_enabled, uint8_t payload)
Format a payload option for YAML/logs.
std::optional< DiscoveryCommand > discovery_command_from_string(const std::string &value)
Parse a discovery command string (e.g., "0x28") into the enum.
std::optional< DiscoverConfirmMode > discover_confirm_mode_from_string(const std::string &value)
Parse a pairing_discover_confirm string into the enum.
LR1121RxBandwidth
Valid LR1121 RX bandwidth options (register values).
std::string lr1121_bandwidth_to_string(LR1121RxBandwidth bw)
Format an LR1121 bandwidth enum as its YAML/UI option string (bare kHz number).
std::string key_transfer_mode_to_string(KeyTransferMode value)
Render a KeyTransferMode as its YAML/select option string.
std::optional< ScanPowerClasses > scan_power_classes_from_string(const std::string &value)
Parse a scan_power_classes string into the enum.
static constexpr uint16_t SX1276_RESPONSE_PREAMBLE
SX1276 preamble for response/continuation frames within an exchange.
std::optional< KeyTransferMode > key_transfer_mode_from_string(const std::string &value)
Parse a KeyTransferMode from its YAML/select option string.
std::string tuning_config_snapshot(const TuningConfig &cfg)
Format the current tuning configuration as a one-line YAML-compatible snapshot.
std::optional< SX1262RxBandwidth > sx1262_bandwidth_from_string(const std::string &value)
Convert a YAML bandwidth string to the enum value.
ScanPowerClasses
Which power classes scan_paired_devices() calls.
@ BOTH
Low-power pass, then always-alive pass (default).
static constexpr uint16_t SOFT_PHY_START_PREAMBLE
Default directed start preamble for drivers built on the shared software PHY (SX1262,...
std::string sx1276_bandwidth_to_string(SX1276RxBandwidth bw)
Format an SX1276 bandwidth enum as its YAML/UI option string (bare kHz number).
bool scan_power_classes_include(ScanPowerClasses selection, uint8_t power_save_mode)
Whether selection calls the roll-call pass for power_save_mode.
float lr1121_bandwidth_to_khz(LR1121RxBandwidth bw)
Convert an LR1121 bandwidth enum to the numeric kHz value used in YAML/logs.
KeyTransferMode
Which direction PairingEngine moves the system key in after discovery.
@ PULL
Ask the device for its key, then run a roll-call under it to find the devices that hold it.
@ PUSH
Send our system key to the device.
@ AUTO
Default. Pull from a device that answered discovery as a remote controller, push to any other.
std::string discovery_commands_to_string(const std::vector< DiscoveryCommand > &commands)
Format the ordered discovery command list for logs.
std::optional< DiscoveryListenChannels > discovery_listen_channels_from_string(const std::string &value)
Parse a DiscoveryListenChannels from its YAML/select option string.
std::string bandwidth_to_string(float khz)
Format a kHz value as its YAML/UI option string (bare number, one decimal, e.g. "117....
std::string tuning_config_full_snapshot(const TuningConfig &cfg)
Format the current tuning configuration as a full one-line snapshot.
static constexpr uint16_t SX1262_DISCOVERY_HOP_SLICE_MS
Per-channel dwell while SX1262 pairing discovery hops across channels.
float sx1262_bandwidth_to_khz(SX1262RxBandwidth bw)
Convert a bandwidth enum to the numeric kHz value used in YAML/logs.
static constexpr uint16_t LR1121_RESPONSE_PREAMBLE
LR1121-specific preamble for response/continuation frames within an exchange.
std::string sx1262_bandwidth_to_string(SX1262RxBandwidth bw)
Format a bandwidth enum as its YAML/UI option string (bare kHz number, e.g.
DiscoverConfirmMode
Whether and how PairingEngine sends CMD_DISCOVER_CONFIRM (0x2C) to a freshly-discovered device before...
@ SKIP
Kill switch: sends no 0x2C and applies no post-step pause.
@ SEND_WITH_ACK
Like SEND, but also sets CTRL1_ACK for an always-alive target (0x10) — the shape every corpus hub (VE...
BandwidthTableView sx1262_bandwidth_table()
The SX1262 RX-bandwidth option table.
@ LOW_POWER
low_power: true: copy 1 LONG_PREAMBLE + CTRL1_LOW_POWER, repeats normal.
@ ALWAYS_ALIVE
low_power: false: normal start preamble on every copy, CTRL1 0x00.
static constexpr uint16_t SX1276_DISCOVERY_HOP_SLICE_MS
Per-channel dwell while SX1276 pairing discovery hops across channels.
std::string tuning_update_log_line(const std::string &name, const std::string &value)
Format a single tuning update for the log.
std::string discovery_listen_channels_to_string(DiscoveryListenChannels value)
Render a DiscoveryListenChannels as its YAML/select option string.
static constexpr uint16_t SX1262_RESPONSE_PREAMBLE
SX1262-specific preamble for response/continuation frames within an exchange.
std::optional< uint8_t > bandwidth_from_string(const BandwidthOption *table, size_t n, const std::string &value)
Parse a YAML/UI bandwidth string against a table, returning the matching register byte.
@ AUTO
Automatic mode; the device manages the setpoint itself.
DiscoveryCommand
Discovery request command codes.
@ DISCOVER_SPE
SPE roll-call request (self-authenticating; see CMD_DISCOVER_SPE_REQ).
@ DISCOVER
Standard broadcast discovery request (to 0x00003B).
@ DISCOVER_ALT
Alternate broadcast discovery (to 0x00003F), with optional payload byte.
float sx1276_bandwidth_to_khz(SX1276RxBandwidth bw)
Convert an SX1276 bandwidth enum to the numeric kHz value used in YAML/logs.
std::string discovery_command_to_string(DiscoveryCommand cmd)
Format a discovery command enum for YAML/logs.
SX1276RxBandwidth
Valid SX1276 RX bandwidth options (RegRxBw register bytes).
@ BW_41_7_KHZ
41.7 kHz — default (validated against real devices).
@ BW_125_0_KHZ
125.0 kHz — widest selectable option.
@ BW_20_8_KHZ
20.8 kHz — narrowest; maximal noise rejection, least LO-offset tolerance.
static constexpr uint16_t SX1262_POST_TX_SETTLE_US
SX1262-specific post-TX settling delay before re-entering RX.
std::optional< SX1276RxBandwidth > sx1276_bandwidth_from_string(const std::string &value)
Convert a YAML SX1276 bandwidth string to the enum value.
static constexpr uint16_t LR1121_POST_TX_SETTLE_US
LR1121-specific post-TX settling delay before re-entering RX.
static constexpr uint16_t LR1121_DISCOVERY_HOP_SLICE_MS
Per-channel dwell while LR1121 pairing discovery hops across channels.
const uint8_t * resolve_discovery_destination(uint8_t command, bool destination_auto, const uint8_t destination[NODE_ID_SIZE])
Resolve the destination address for a discovery command.
BandwidthTableView sx1276_bandwidth_table()
The SX1276 RX-bandwidth option table (see sx1262_bandwidth_table()).
std::string discovery_destination_to_string(bool destination_auto, const uint8_t destination[NODE_ID_SIZE])
Format a destination option for YAML/logs.
float bandwidth_to_khz(const BandwidthOption *table, size_t n, uint8_t reg, float fallback)
Look up the kHz value for a register byte in a bandwidth table.
std::string discover_confirm_mode_to_string(DiscoverConfirmMode value)
Format a DiscoverConfirmMode value for YAML/logs.
std::string scan_power_classes_to_string(ScanPowerClasses value)
Format a ScanPowerClasses value for YAML/logs.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Physical-layer radio and timing parameters for the IO-Homecontrol protocol.
One selectable RX bandwidth: the chip's register byte and its nominal kHz value.
A pointer/size view over one chip's RX-bandwidth option table.
All runtime tunable parameters for pairing and radio diagnostics.
bool active
True when the YAML tuning: block is present.
uint16_t pairing_discovery_initial_dwell_ms
Initial dwell on CH2 before discovery hopping begins.
bool pairing_discovery_destination_auto
When true, map commands to conventional addresses.
SX1276RxBandwidth sx1276_rx_bandwidth
SX1276 RX bandwidth selector.
uint16_t normal_start_preamble
Preamble for a directed start frame to a non-low-power target.
int16_t lbt_rssi_threshold_dbm
LBT channel-free threshold (dBm).
SX1262RxBandwidth sx1262_rx_bandwidth
SX1262 RX bandwidth selector.
uint16_t low_power_wake_preamble
Wake-up preamble (bytes) on a directed start frame to a low_power target believed at rest.
uint16_t cold_broadcast_reply_preamble
Preamble for a start-flagged key-extraction broadcast reply (0x29).
std::vector< uint8_t > pairing_discovery_destination
Destination when auto=false.
std::vector< DiscoveryCommand > pairing_discovery_commands
Ordered discovery commands.
DiscoveryListenChannels pairing_discovery_listen_channels
Channels the discovery response wait covers.
uint16_t lr1121_discovery_hop_slice_ms
Per-channel dwell while LR1121 discovery hops.
uint16_t sx1276_response_preamble
SX1276 response preamble in bytes.
uint16_t exchange_response_wait_ms
Reply window for continuation frames and the post-auth final response.
uint16_t sx1262_post_tx_settle_us
Delay after SX1262 TX before RX (µs).
uint8_t pairing_key_exchange_retries
Retries for the authenticated key exchange phase.
uint16_t pairing_key_init_delay_ms
Pause after the discover-confirm step, before CMD_KEY_INIT (0x31).
DiscoverConfirmMode pairing_discover_confirm
Whether/how to send CMD_DISCOVER_CONFIRM (0x2C) during pairing.
bool pairing_discovery_ack_capable
Set ACK (CTRL1_ACK) on the discovery broadcast only.
bool pairing_discovery_low_power
Set LOW_POWER flag in discovery frames.
uint16_t sx1276_discovery_hop_slice_ms
Per-channel dwell while SX1276 discovery hops.
uint16_t exchange_start_response_wait_ms
Reply window for a start frame (wakes a sleeping device).
KeyTransferMode pairing_key_transfer
Push our key, or pull the device's key.
uint16_t sx1262_response_preamble
SX1262 response preamble in bytes.
uint16_t pairing_discovery_preamble
Preamble for the discovery broadcast (0x28/0x2E) start frame.
uint16_t pairing_discovery_wait_ms
Total wait window after sending discovery commands.
bool pairing_discovery_payload_enabled
Whether the optional payload is enabled.
LR1121RxBandwidth lr1121_rx_bandwidth
LR1121 RX bandwidth selector.
uint16_t lr1121_post_tx_settle_us
Delay after LR1121 TX before RX (µs).
uint8_t lbt_max_retries
LBT retries before forced TX.
uint16_t sx1262_discovery_hop_slice_ms
Per-channel dwell while SX1262 discovery hops.
uint16_t exchange_total_budget_ms
Ceiling on one whole exchange, retries included.
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...
uint16_t lr1121_response_preamble
LR1121 response preamble in bytes.
bool normal_start_preamble_from_yaml
True when normal_start_preamble: appeared in YAML.
uint8_t pairing_discovery_payload
Optional payload byte (used for 0x2E).
ScanPowerClasses scan_power_classes
Power classes the scan_paired_devices roll-call calls.