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 Discovery request command codes.
136enum class DiscoveryCommand : uint8_t {
137 DISCOVER = 0x28, ///< Standard broadcast discovery request (to 0x00003B).
138 DISCOVER_SPE = 0x2A, ///< SPE roll-call request (self-authenticating; see CMD_DISCOVER_SPE_REQ).
139 ///< Deliberately **not** offered as a `pairing_discovery_commands` preset
140 ///< in tuning.py: only devices that already hold the system key answer it,
141 ///< so it can never help reach a device in learning mode. Retained here
142 ///< because the command byte is still needed to *send* a roll-call, and
143 ///< because discovery_command_from_string() stays permissive enough to
144 ///< parse it — do not remove either as dead code.
145 DISCOVER_ALT = 0x2E, ///< Alternate broadcast discovery (to 0x00003F), with optional payload byte.
146};
147
148// Chip-specific radio defaults live here beside the TuningConfig fields they initialize — the
149// single source of truth for defaults. proto_timing.h holds only chip-neutral protocol values;
150// the radio drivers consume these via apply_tuning / hop_dwell_ms.
151
152/// SX1262-specific preamble for response/continuation frames within an exchange.
153///
154/// Applies to any tight-turnaround frame sent immediately after receiving from the device —
155/// 0x3D challenge responses, 0x32 key transfers after receiving 0x3C, and any future protocol
156/// frame in that position — giving the peer's receiver time to lock back on after the SX1262's
157/// own TX→RX turnaround.
158///
159/// 8 bytes was validated on real hardware (Heltec V3.2 ↔ Device actuator) as the minimum that
160/// gives reliable lock-on without perturbing exchange timing; it matches the protocol's own
161/// nominal SHORT_PREAMBLE floor.
162///
163/// This constant is byte-denominated, like every other preamble value in this codebase
164/// (LONG_PREAMBLE, SHORT_PREAMBLE) — `RadioSX1262::set_packet_params_()` is the one place that
165/// converts to the chip's bit-denominated SetPacketParams field, right before the value leaves
166/// for the wire.
167static constexpr uint16_t SX1262_RESPONSE_PREAMBLE = 8;
168
169/// SX1262-specific post-TX settling delay before re-entering RX.
170///
171/// The SX1262 GFSK demodulator needs time to stabilize after TX before it can
172/// reliably receive the next frame. A 500 µs delay was validated as the minimum
173/// that prevents challenge-byte corruption during pairing and tight-turnaround
174/// authenticated exchanges.
175static constexpr uint16_t SX1262_POST_TX_SETTLE_US = 500;
176
177/// SX1276 preamble for response/continuation frames within an exchange.
178///
179/// Defaults to 12 bytes — longer than the protocol's 8-byte SHORT_PREAMBLE. On real hardware the
180/// extra length improves the peer device's lock-on without measurably affecting exchange timing.
181/// Runtime-tunable down to SHORT_PREAMBLE or up for a marginal-range install.
182static constexpr uint16_t SX1276_RESPONSE_PREAMBLE = 12;
183
184/// Per-channel dwell while SX1276 pairing discovery hops across channels.
185///
186/// The SX1276 supports FastHop (no standby transition), so a short slice keeps
187/// discovery sweeping all three channels quickly.
188static constexpr uint16_t SX1276_DISCOVERY_HOP_SLICE_MS = 5;
189
190/// Per-channel dwell while SX1262 pairing discovery hops across channels.
191///
192/// SX1262 frequency changes require a standby→SetRfFrequency→RX cycle, unlike the SX1276's
193/// FastHop, but that changes only how much a single retune costs, not how long the radio should
194/// then sit still: a short dwell fits more retunes into the listen window than a long one, so the
195/// receiver is more often already parked on the right channel by the time a reply starts. The
196/// preamble/sync linger guard (`ListenSpec::linger_on_preamble`) is what makes a dwell this short
197/// safe — it keeps a caught reply from being cut off mid-reception by extending the dwell instead
198/// of hopping away. A dwell in the low single-digit milliseconds performs best; shorter than that,
199/// coverage degrades gradually and only truly collapses at `0`, where `wait_for_packet(..., 0)`
200/// returns before any guard can observe activity at all. `7` is the best-performing value found in
201/// that range.
202static constexpr uint16_t SX1262_DISCOVERY_HOP_SLICE_MS = 7;
203
204/// LR1121-specific preamble for response/continuation frames within an exchange.
205///
206/// Seeded from the SX1262-validated default, on the principle that a validated timing value
207/// encodes protocol-side reality more than a chip quirk. Not yet independently validated on
208/// LR1121 hardware; a value measured by loopback tuning would fold back here.
210
211/// LR1121-specific post-TX settling delay before re-entering RX.
212///
213/// Seeded from the SX1262-validated default; same rationale as @ref LR1121_RESPONSE_PREAMBLE.
215
216/// Per-channel dwell while LR1121 pairing discovery hops across channels.
217///
218/// Measured independently on LR1121 hardware rather than inherited from
219/// @ref SX1262_DISCOVERY_HOP_SLICE_MS — that constant's short-dwell reasoning applies equally
220/// here, but the two chips are validated separately and could in principle diverge, so this stays
221/// its own literal rather than an alias.
222static constexpr uint16_t LR1121_DISCOVERY_HOP_SLICE_MS = 7;
223
224/// Default directed start preamble for drivers built on the shared software PHY (SX1262, LR1121).
225///
226/// 48 bytes rather than the protocol's documented 32. Measured: on a Somfy Izymo dimmer an SX1262
227/// answers 56/71 = 78.9% of first tries at 32 bytes and 60/60 at 48, 64, 128 and 256 — a step, not
228/// a ramp — while an SX1276 sending the same programmed 32 bytes answers 60/60. The deficit tracks
229/// the shared PHY rather than the chip, the link or the device, so the compensation belongs with
230/// the PHY. The cause of the shortfall is not identified; see ADR 0042.
231static constexpr uint16_t SOFT_PHY_START_PREAMBLE = 48;
232
233/// @brief All runtime tunable parameters for pairing and radio diagnostics.
234///
235/// Values reset to their defaults on every boot. Each field initializes from a
236/// canonical constant — chip-neutral defaults live in `proto_timing.h`, chip-specific
237/// defaults in this header — shared with the ESPHome YAML schema. The hub stores a single
238/// `TuningConfig` instance and passes it to the radio driver and pairing flow. UI
239/// callbacks update the hub instance directly; the snapshot helpers emit the current
240/// values in YAML-compatible form so a working combination can be copied back into the
241/// configuration file.
243 // --- Radio / physical layer ---
244 /// SX1262 RX bandwidth selector.
245 ///
246 /// 58.6 kHz (double-sideband). This is below the 76.8 kHz sizing floor for this waveform (see
247 /// `SX1262RxBandwidth`), so it trims the edges of even an on-frequency signal; it is the
248 /// default because it made a real solar shutter (Somfy RS100) respond reliably where 117.3 kHz
249 /// did not. Because the SX1262 has no GFSK AFC, a peer whose carrier sits off nominal needs a
250 /// wider setting instead: a Somfy LightVar_Wh_io dimmer went from mostly missed to mostly
251 /// confirmed replies at 156.2 kHz. The setting is global, so the right value is a per-install trade-off.
253 uint16_t sx1262_response_preamble{SX1262_RESPONSE_PREAMBLE}; ///< SX1262 response preamble in bytes.
254 uint16_t sx1262_post_tx_settle_us{SX1262_POST_TX_SETTLE_US}; ///< Delay after SX1262 TX before RX (µs).
256 uint16_t sx1276_response_preamble{SX1276_RESPONSE_PREAMBLE}; ///< SX1276 response preamble in bytes.
258 SX1276_DISCOVERY_HOP_SLICE_MS}; ///< Per-channel dwell while SX1276 discovery hops.
260 SX1262_DISCOVERY_HOP_SLICE_MS}; ///< Per-channel dwell while SX1262 discovery hops.
262 uint16_t lr1121_response_preamble{LR1121_RESPONSE_PREAMBLE}; ///< LR1121 response preamble in bytes.
263 uint16_t lr1121_post_tx_settle_us{LR1121_POST_TX_SETTLE_US}; ///< Delay after LR1121 TX before RX (µs).
265 LR1121_DISCOVERY_HOP_SLICE_MS}; ///< Per-channel dwell while LR1121 discovery hops.
267 COLD_BROADCAST_REPLY_PREAMBLE}; ///< Preamble for a start-flagged key-extraction broadcast reply (0x29).
269 NORMAL_START_PREAMBLE}; ///< Preamble for a directed start frame to a non-low-power target.
270 ///< Resolved at setup from RadioDriver::default_start_preamble()
271 ///< unless `normal_start_preamble_from_yaml` says the user set it.
272 /// True when `normal_start_preamble:` appeared in YAML. Codegen emits this alongside the value;
273 /// it exists so setup() can tell "the user chose 32" from "nobody chose anything", which decides
274 /// whether the driver's default applies (ADR 0042). Only read once, at setup — a later change
275 /// through the Home Assistant number simply sets the value.
277 uint8_t lbt_max_retries{LBT_MAX_RETRIES}; ///< LBT retries before forced TX.
278 int16_t lbt_rssi_threshold_dbm{LBT_RSSI_THRESHOLD_DBM}; ///< LBT channel-free threshold (dBm).
279 bool low_power_wake_belief{true}; ///< Order a `low_power` device's start-frame tries by its wake belief (short
280 ///< preamble first when it is believed awake). False restores the fixed
281 ///< LONG_PREAMBLE on every try. A diagnostic off-switch, on by default.
282
283 // --- Exchange response windows ---
284 // How long the hub listens for a device's reply. Tunable because the right value is a property
285 // of the *device*, not of the radio or the protocol: observed reply latencies on one network
286 // span two orders of magnitude (a mains Oximo 40 at ~23 ms, a solar RS100 at 29-3052 ms). See
287 // RESPONSE_START_WAIT_MS for the measurements behind the defaults.
289 RESPONSE_START_WAIT_MS}; ///< Reply window for a start frame (wakes a sleeping device).
291 RESPONSE_WAIT_MS}; ///< Reply window for continuation frames and the post-auth final response.
292 uint16_t exchange_total_budget_ms{EXCHANGE_TOTAL_BUDGET_MS}; ///< Ceiling on one whole exchange, retries included.
293
294 // --- Pairing protocol ---
295 std::vector<DiscoveryCommand> pairing_discovery_commands{
296 DiscoveryCommand::DISCOVER}; ///< Ordered discovery commands.
297 std::vector<uint8_t> pairing_discovery_destination{0x00, 0x00, 0x00}; ///< Destination when auto=false.
298 bool pairing_discovery_destination_auto{true}; ///< When true, map commands to conventional addresses.
299 uint8_t pairing_discovery_payload{0}; ///< Optional payload byte (used for 0x2E).
300 bool pairing_discovery_payload_enabled{false}; ///< Whether the optional payload is enabled.
301 bool pairing_discovery_low_power{false}; ///< Set LOW_POWER flag in discovery frames.
302 bool pairing_discovery_ack_capable{false}; ///< Set ACK (CTRL1_ACK) on the discovery broadcast only.
303 ///< Opt-in — see the init_frame() doc in
304 ///< proto_frame.h; default off.
306 PAIRING_DISCOVERY_PREAMBLE}; ///< Preamble for the discovery broadcast (0x28/0x2E) start frame.
308 PAIRING_DISCOVERY_WAIT_MS}; ///< Total wait window after sending discovery commands.
310 PAIRING_DISCOVERY_INITIAL_DWELL_MS}; ///< Initial dwell on CH2 before discovery hopping begins.
312 PAIRING_KEY_EXCHANGE_RETRIES}; ///< Retries for the authenticated key exchange phase.
314 ScanPowerClasses::BOTH}; ///< Power classes the scan_paired_devices roll-call calls.
316 DiscoverConfirmMode::SEND}; ///< Whether/how to send CMD_DISCOVER_CONFIRM (0x2C) during pairing.
320 PAIRING_KEY_INIT_DELAY_MS}; ///< Pause after the discover-confirm step, before CMD_KEY_INIT (0x31).
321
322 // --- Internal state ---
323 bool active{false}; ///< True when the YAML `tuning:` block is present.
324};
325
326/// @brief One selectable RX bandwidth: the chip's register byte and its nominal kHz value.
327///
328/// The per-chip bandwidth conversion functions below (SX1262/SX1276/LR1121) are all the same
329/// logic over a different table of these; they are thin typed wrappers around the three generic
330/// helpers that follow, which own the register-byte lookup, the "%.1f" formatting, and the
331/// input normalization once instead of three times.
333 uint8_t reg;
334 float khz;
335};
336
337/// @brief Look up the kHz value for a register byte in a bandwidth table.
338/// @param table Bandwidth option table.
339/// @param n Number of entries in @p table.
340/// @param reg Register byte to look up.
341/// @param fallback kHz value returned when @p reg is not in the table.
342float bandwidth_to_khz(const BandwidthOption *table, size_t n, uint8_t reg, float fallback);
343
344/// @brief Format a kHz value as its YAML/UI option string (bare number, one decimal, e.g. "117.3").
345std::string bandwidth_to_string(float khz);
346
347/// @brief Parse a YAML/UI bandwidth string against a table, returning the matching register byte.
348///
349/// Accepts surrounding whitespace and a trailing "kHz" suffix in any case, and accepts both the
350/// one-decimal spelling ("39.0") and the integer-truncated spelling ("39") of every table entry.
351/// @return The register byte, or std::nullopt when the string matches no entry.
352std::optional<uint8_t> bandwidth_from_string(const BandwidthOption *table, size_t n, const std::string &value);
353
354/// @brief A pointer/size view over one chip's RX-bandwidth option table.
359
360/// @brief The SX1262 RX-bandwidth option table. Exposed so tests derive their legal-option
361/// lists from the same source production uses rather than re-listing them by hand.
363/// @brief The SX1276 RX-bandwidth option table (see sx1262_bandwidth_table()).
365/// @brief The LR1121 RX-bandwidth option table (see sx1262_bandwidth_table()).
367
368/// @brief Convert a bandwidth enum to the numeric kHz value used in YAML/logs.
369/// @param bw SX1262 bandwidth enum.
370/// @return Floating-point kHz value (39.0, 46.9, 58.6, 78.2, 117.3, 156.2, or 187.2).
372
373/// @brief Format a bandwidth enum as its YAML/UI option string (bare kHz number, e.g. "117.3").
374/// @param bw SX1262 bandwidth enum.
375/// @return Bandwidth rendered with one decimal place and a "kHz" suffix.
377
378/// @brief Convert a YAML bandwidth string to the enum value.
379/// @param value YAML string such as "117.3kHz" or "156.2kHz".
380/// @return Bandwidth enum, or std::nullopt on invalid input.
381std::optional<SX1262RxBandwidth> sx1262_bandwidth_from_string(const std::string &value);
382
383/// @brief Convert an SX1276 bandwidth enum to the numeric kHz value used in YAML/logs.
384/// @param bw SX1276 bandwidth enum.
385/// @return Floating-point kHz value (20.8, 41.7, 62.5, 83.3, or 125.0).
387
388/// @brief Format an SX1276 bandwidth enum as its YAML/UI option string (bare kHz number).
389/// @param bw SX1276 bandwidth enum.
390/// @return Bandwidth rendered with one decimal place (e.g. "41.7").
392
393/// @brief Convert a YAML SX1276 bandwidth string to the enum value.
394/// @param value YAML string such as "41.7" or "83.3kHz".
395/// @return Bandwidth enum, or std::nullopt on invalid input.
396std::optional<SX1276RxBandwidth> sx1276_bandwidth_from_string(const std::string &value);
397
398/// @brief Convert an LR1121 bandwidth enum to the numeric kHz value used in YAML/logs.
399/// @param bw LR1121 bandwidth enum.
400/// @return Floating-point kHz value (39.0, 46.9, 58.6, 78.2, 117.3, 156.2, or 187.2).
402
403/// @brief Format an LR1121 bandwidth enum as its YAML/UI option string (bare kHz number).
404/// @param bw LR1121 bandwidth enum.
405/// @return Bandwidth rendered with one decimal place (e.g. "117.3").
407
408/// @brief Convert a YAML LR1121 bandwidth string to the enum value.
409/// @param value YAML string such as "117.3" or "39.0kHz".
410/// @return Bandwidth enum, or std::nullopt on invalid input.
411std::optional<LR1121RxBandwidth> lr1121_bandwidth_from_string(const std::string &value);
412
413/// @brief Format the current tuning configuration as a one-line YAML-compatible snapshot.
414///
415/// Only values that differ from the default are emitted, so the line can be copied
416/// back into YAML with minimal editing. When all values are default, an empty string
417/// is returned.
418///
419/// @param cfg Current tuning configuration.
420/// @return One-line snapshot string, or empty when no overrides are active.
421std::string tuning_config_snapshot(const TuningConfig &cfg);
422
423/// @brief Format the current tuning configuration as a full one-line snapshot.
424///
425/// Emits every non-default value, regardless of whether it was changed from YAML or
426/// from the HA UI. This is logged at the start of every pairing attempt.
427///
428/// @param cfg Current tuning configuration.
429/// @return One-line snapshot string, or a "defaults" marker when nothing is overridden.
430std::string tuning_config_full_snapshot(const TuningConfig &cfg);
431
432/// @brief Format a single tuning update for the log.
433///
434/// @param name YAML key name of the updated parameter.
435/// @param value YAML-compatible string representation of the new value.
436/// @return One-line log string suitable for ESP_LOGI.
437std::string tuning_update_log_line(const std::string &name, const std::string &value);
438
439/// @brief Resolve the destination address for a discovery command.
440///
441/// When `destination_auto` is true, returns the conventional address for the
442/// given command code. Otherwise returns the configured `destination` value.
443///
444/// @param command Discovery command code.
445/// @param destination_auto Whether to use the automatic mapping.
446/// @param destination Explicit 3-byte destination when auto is false.
447/// @return Pointer to the resolved 3-byte node ID.
448const uint8_t *resolve_discovery_destination(uint8_t command, bool destination_auto,
449 const uint8_t destination[NODE_ID_SIZE]);
450
451/// @brief Parse a discovery command string (e.g., "0x28") into the enum.
452/// @param value String to parse.
453/// @return Discovery command enum, or std::nullopt on invalid input.
454std::optional<DiscoveryCommand> discovery_command_from_string(const std::string &value);
455
456/// @brief Format a discovery command enum for YAML/logs.
457/// @param cmd Discovery command enum.
458/// @return Lowercase hex string such as "0x28".
460
461/// @brief Format a destination option for YAML/logs.
462///
463/// @param destination_auto Whether automatic destination mapping is used.
464/// @param destination Explicit 3-byte destination when auto is false.
465/// @return "auto" or a hex address such as "0x00003B".
466std::string discovery_destination_to_string(bool destination_auto, const uint8_t destination[NODE_ID_SIZE]);
467
468/// @brief Format a payload option for YAML/logs.
469///
470/// @param payload_enabled Whether the optional payload is enabled.
471/// @param payload The payload byte when enabled.
472/// @return "none" or a hex byte such as "0x00".
473std::string discovery_payload_to_string(bool payload_enabled, uint8_t payload);
474
475/// @brief Format the ordered discovery command list for logs.
476///
477/// @param commands Ordered list of discovery commands.
478/// @return Bracketed comma-separated list such as "[0x28,0x2E]".
479std::string discovery_commands_to_string(const std::vector<DiscoveryCommand> &commands);
480
481/// @brief Format the ordered discovery command list as a UI/preset option string.
482///
483/// Matches the comma-separated `select` option labels (no brackets) so a boot-time
484/// snapshot round-trips to a selectable dropdown value.
485///
486/// @param commands Ordered list of discovery commands.
487/// @return Comma-separated list such as "0x28,0x2E" (empty string when the list is empty).
488std::string discovery_commands_to_csv(const std::vector<DiscoveryCommand> &commands);
489
490/// @brief Format a ScanPowerClasses value for YAML/logs.
491///
492/// `BOTH` is its own literal; the two single-class values return
493/// `power_save_mode_name(POWER_SAVE_ALWAYS_ALIVE)` / `power_save_mode_name(POWER_SAVE_LOW_POWER)`
494/// so the roll-call report and this tunable share one vocabulary rather than a second spelling.
495/// @param value Selection to format.
496/// @return "both", "always_alive", or "low_power".
498
499/// @brief Parse a scan_power_classes string into the enum.
500///
501/// Exact, case-sensitive match against the same three strings scan_power_classes_to_string()
502/// produces.
503/// @param value String to parse.
504/// @return The matching selection, or std::nullopt on invalid input.
505std::optional<ScanPowerClasses> scan_power_classes_from_string(const std::string &value);
506
507/// @brief Whether `selection` calls the roll-call pass for `power_save_mode`.
508///
509/// Pure predicate, used both by the sweep loop (which pass to skip) and by the report (which
510/// class to name in the selection NOTE) — one source of truth for "does this selection include
511/// that class" instead of two.
512/// @param selection Current `scan_power_classes` tuning value.
513/// @param power_save_mode POWER_SAVE_ALWAYS_ALIVE or POWER_SAVE_LOW_POWER.
514/// @return true if `selection` calls that class; false for an unrecognized power_save_mode.
515bool scan_power_classes_include(ScanPowerClasses selection, uint8_t power_save_mode);
516
517/// @brief Format a DiscoverConfirmMode value for YAML/logs.
518/// @param value Selection to format.
519/// @return "skip", "send", or "send_with_ack".
521
522/// @brief Parse a `pairing_discover_confirm` string into the enum.
523///
524/// Exact, case-sensitive match against the same three strings discover_confirm_mode_to_string()
525/// produces.
526/// @param value String to parse.
527/// @return The matching mode, or std::nullopt on invalid input.
528std::optional<DiscoverConfirmMode> discover_confirm_mode_from_string(const std::string &value);
529
530/// @brief Render a DiscoveryListenChannels as its YAML/select option string.
531/// @param value Selection to render.
532/// @return `"skip_request"` or `"all"`.
534
535/// @brief Parse a DiscoveryListenChannels from its YAML/select option string.
536///
537/// Exact, case-sensitive match against the same strings discovery_listen_channels_to_string()
538/// emits, so the two stay each other's inverse.
539/// @param value Option string to parse.
540/// @return The selection, or `std::nullopt` if `value` is not one of the options.
541std::optional<DiscoveryListenChannels> discovery_listen_channels_from_string(const std::string &value);
542
543} // namespace home_io_control
544} // 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::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::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.
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.
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 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).
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.