Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_timing.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_timing.h
4/// @brief Physical-layer radio and timing parameters for the IO-Homecontrol protocol.
5/// @ingroup hioc_protocol
6
7#include <cstdint>
8
9namespace esphome {
10namespace home_io_control {
11
12// ============================================================================
13// Physical Layer — Radio Parameters
14// ============================================================================
15
16/// The protocol uses 3 frequency channels in the 868 MHz ISM band.
17/// IO-Homecontrol uses 3 channels in the 868 MHz SRD band. In 1W (one-way) mode,
18/// only CH2 is used. In 2W (two-way) mode, the controller hops across all three
19/// channels every ~2.7ms when idle. Commands are sent on CH2; responses may arrive
20/// on any channel within the exchange wait window.
21static constexpr uint32_t FREQ_CH1 = 868250000; ///< Channel 1: 868.25 MHz (2W only)
22static constexpr uint32_t FREQ_CH2 = 868950000; ///< Channel 2: 868.95 MHz (1W and 2W, TX channel)
23static constexpr uint32_t FREQ_CH3 = 869850000; ///< Channel 3: 869.85 MHz (2W only)
24
25/// Length of one discovery answer slot. A discovery request opens a window of N such slots; the
26/// key-extraction responder answers once, in a random slot from 1 to N, so it never answers at
27/// once; the controller collects answers for the whole window before it confirms one.
28///
29/// Evidence: in our corpus's discovery captures, devices answer a 32-slot search between 13 and
30/// 1353 ms after it, inside 33 × 41 ms; a Freebox Delta repeats its 128-slot search about every
31/// 5.3 s, which is 128 × 41 ms plus its own turnaround (issue #215).
32static constexpr uint32_t DISCOVERY_SLOT_MS = 41;
33
34/// Preamble is a sequence of 0xAA bytes that precedes every frame.
35/// Any start frame carrying `CTRL1_LOW_POWER` — a directed frame to a low-power target, or the
36/// SPE roll-call's low-power frame shape — uses the long preamble (1024 bytes = 8192 bits) as a
37/// wake-up burst for a duty-cycled receiver; every other start frame uses the runtime-tunable
38/// `normal_start_preamble`, which an always-listening receiver detects fine. Every continuation
39/// frame (a reply, our challenge or challenge answer, a status-update ACK) uses the radio driver's
40/// `response_preamble()`, since both sides are already on the same channel; SHORT_PREAMBLE is that
41/// value's protocol default and the tuning floor. The exchange engine derives all of this from the
42/// frame (exchange_engine.cpp).
43static constexpr uint16_t LONG_PREAMBLE = 1024; ///< 1024 bytes: wake-up burst for a low-power start frame
44static constexpr uint16_t SHORT_PREAMBLE = 8; ///< Protocol default and floor for continuation frames
45
46/// Default for `TuningConfig::cold_broadcast_reply_preamble` — preamble for a *broadcast* reply to
47/// a frame the peer caught via a rotating/hopping listen (currently: only the key-extraction
48/// responder's 0x29, the sole `start=true` frame any device-role builder in this codebase
49/// produces) — long enough to be reliably caught by a hopping receiver, short enough that
50/// sending it doesn't block the loop the way LONG_PREAMBLE does.
51///
52/// Sized above the ~13.9 ms real-device discovery-reply preamble this project's own SX1262/LR1121
53/// discovery hop-slice tuning was measured against (issue #65 SDR analysis) — that measurement is
54/// what "reliably caught by a hopping receiver" is calibrated to here. 80 bytes ≈ 16.7 ms at the
55/// protocol's 38400 bps line rate (soft_phy_air_time_us()),
56/// budgeted as a starting point, not yet hardware-validated for this exact chip/scenario
57/// combination. Runtime-tunable via `cold_broadcast_reply_preamble` for exactly that reason.
58static constexpr uint16_t COLD_BROADCAST_REPLY_PREAMBLE = 80;
59
60/// Default for `TuningConfig::normal_start_preamble` — the preamble in front of a directed *start*
61/// frame whose target is **not** a low-power / duty-cycled device (`CTRL1_LOW_POWER` clear). An
62/// always-listening receiver does not need the ~213 ms `LONG_PREAMBLE` wake-up burst, and some
63/// receivers never lock onto one that long; a normal start frame gets this shorter preamble
64/// instead, matching what a reference hub sends to an always-alive device.
65///
66/// 32 bytes = 256 bits is well above the ~12-byte response preamble a
67/// short-turnaround chip uses, ~6.7 ms of air time, and two orders of magnitude below the 1024-byte
68/// burst. 8 bytes is a proven lower bound against paired always-alive devices but nothing bounds
69/// where a start frame stops being heard, so 32 is the defensible middle — and `normal_start_preamble`
70/// is a live tuning knob so a wrong guess costs a number change, not a rebuild.
71static constexpr uint16_t NORMAL_START_PREAMBLE = 32;
72
73/// How hard a transmission has to work to be heard: the wake-up level the receiver needs. A radio
74/// that wakes receivers by preamble length gets the same information from
75/// `RadioTxConfig::preamble_len` and may ignore this; a radio that wakes them some other way (a
76/// train of frames, for example) reads it instead of guessing from a preamble length.
77enum class TxWake : uint8_t {
78 NONE, ///< The receiver is already listening on this channel: the frame continues an exchange.
79 SHORT, ///< The receiver is awake, but may be scanning channels.
80 LONG, ///< The receiver may be duty-cycling.
81};
82
83/// True when a preamble this long is the wake-up burst for a duty-cycled receiver. Every preamble
84/// tunable except `pairing_discovery_preamble` is capped well below `LONG_PREAMBLE`, and that one
85/// is capped at it (an explicit opt-in to the wake-up burst for discovery), so only a real wake-up
86/// burst reaches the threshold. The discovery preamble has no compiled default: it follows
87/// `normal_start_preamble` (see `effective_discovery_preamble()` in tuning_config.h).
88constexpr bool is_wake_preamble(uint16_t len) { return len >= LONG_PREAMBLE; }
89
90/// The wake-up level of a frame, from the two facts that decide it: whether it starts an exchange
91/// (a continuation reaches a receiver that is already listening) and whether its preamble is the
92/// wake-up burst.
93constexpr TxWake tx_wake_for(bool start, uint16_t preamble) {
94 if (!start)
95 return TxWake::NONE;
96 return is_wake_preamble(preamble) ? TxWake::LONG : TxWake::SHORT;
97}
98
99/// Preamble/sync linger extension for a rotating listen (`ListenSpec::linger_dwell_ms`): how much
100/// longer to stay on a channel once a frame is visibly incoming, so a hop doesn't cut it off
101/// mid-reception. Sized to a frame's air time, not to a hop slice, so it does not need to change
102/// when a chip's hop slice does. Shared by every rotating listen (pairing discovery, broadcast
103/// roll-call): both wait for the same class of short protocol frame, so there is no measured
104/// reason for them to differ.
105static constexpr uint32_t PREAMBLE_LINGER_DWELL_MS = 15;
106
107/// Base of how long a node acting as a device holds the session's channel after answering a
108/// controller, before it scans all channels again. The full hold adds the turnaround time the node
109/// advertises in its `0x29` (att_class_ms()), so a controller that waits out that turnaround still
110/// finds the node listening. 35 ms is our margin, not a measured device timer: with the default
111/// advertised turnaround the hold is 75 ms, above the 16-52 ms in which third-party controllers
112/// answered a device's challenge in our corpus (`somfy_oximo40_statuspoll_sx1262`,
113/// `somfy_rs100_statuspoll_kig300_sx1276`).
114static constexpr uint32_t SESSION_HOLD_BASE_MS = 35;
115
116// Chip-specific defaults (response preamble, post-TX settle, per-chip discovery hop slices
117// for SX1262 and LR1121) live beside their TuningConfig fields in tuning_config.h; the
118// SX1262/LR1121 exchange dwell constants live in radio_sx1262.h / radio_lr1121.h respectively.
119// Kept out of the generic protocol layer per the radio-timing layering cleanup — this header
120// holds only chip-neutral protocol values.
121
122/// Timing constants for frequency hopping and response waiting.
123static constexpr int32_t HOP_TIME_US = 2700; ///< Time per channel when hopping (2.7ms)
124static constexpr int32_t RESPONSE_WAIT_MS = 500; ///< Wait for response to non-start frame
125
126/// Wait for a response to a start frame — the first frame of an exchange, and the one a sleeping
127/// device has just been woken by.
128///
129/// This device class replies within a few milliseconds of the carrier dropping, or not at all —
130/// it is fast-or-never, not slow. A failure therefore shows up as `saw_challenge=0` with no frame
131/// received at all, rather than as a late arrival, so a longer window cannot fix a device that
132/// genuinely fails to answer.
133///
134/// 400 ms sits comfortably above every directly measured reply while keeping a failed exchange
135/// inside EXCHANGE_TOTAL_BUDGET_MS, so a dead device does not block the ESPHome loop past its own
136/// warning threshold (ADR 0013). Raise `exchange_start_response_wait_ms` from YAML if a device ever
137/// genuinely answers late — but check `wait_ms` in the logs first, since a fast-or-never device is a
138/// turnaround problem that a longer window cannot fix.
139static constexpr int32_t RESPONSE_START_WAIT_MS = 400;
140
141static constexpr int32_t RESPONSE_AUTH_WAIT_MS =
142 RESPONSE_WAIT_MS; ///< Wait for final response after challenge response
143static constexpr int32_t EXCHANGE_RETRY_DELAY_MS = 250; ///< Gap between retries within one HA command
144/// Attempts per exchange for every caller that has not asked for an attempt plan
145/// (decisions::attempt_plan()): a command that is not repeat-safe (decisions::is_repeat_safe_command()),
146/// heater registers, management actions and the pairing loops. It is also the ceiling of the
147/// `max_tries` overload of ExchangeEngine::send_and_receive(), and the most copies a challenged
148/// request that never closes is followed by, whatever its plan.
149static constexpr uint8_t EXCHANGE_RETRY_COUNT = 3;
150/// Most attempts any exchange makes, whatever the budget allows: a plan with a long
151/// `exchange_total_budget_ms` still stops here, so a dead device is not hammered with 25 copies of
152/// a read.
153static constexpr uint8_t EXCHANGE_MAX_ATTEMPTS = 9;
154/// Attempts a settling read makes to a low-power receiver. Each one leads with the wake-up
155/// preamble (~0.24 s on air), so three already fill the exchange budget.
156static constexpr uint8_t LOW_POWER_SETTLING_ATTEMPTS = 3;
157/// How long the short lead attempt of a read to a low-power receiver in an unknown state listens after
158/// the end of its own transmission (decisions::WakeBelief::UNKNOWN, ADR 0079).
159///
160/// The lead attempt is aimed at a receiver that is travelling: such a VELUX screen answered a short
161/// start frame 41 to 47 ms after it (issue #114), and a resting one does
162/// not answer it at all, so a longer wait is spent on silence. 100 ms is about twice the slowest of
163/// those answers, and keeps the cost of the lead (its transmission, this window and
164/// EXCHANGE_ATTEMPT_OVERHEAD_MS) near 0.12 s.
165static constexpr uint16_t LOW_POWER_SHORT_LEAD_WINDOW_MS = 100;
166/// How long a read listens for a low-power receiver's reply after the end of its own transmission,
167/// on every attempt.
168///
169/// Of the 93 low-power-flagged requests this hub transmitted in the corpus (83 captures, see
170/// scripts/corpus/reply_latency.py), the median answer came 248 ms after the end of the request,
171/// the 90th percentile 299 ms, the 95th 354 ms; 89 came within 400 ms, and the four slower ones took
172/// 647 ms (a third-party gateway's pairing), 819 ms and 1639 ms (an RS100 io motor's key exchange)
173/// and 4375 ms (a stalled key extraction). 550 ms sits about 1.5 times above the 95th percentile and
174/// below every slower sample, so one attempt's window covers a reply that arrives late without
175/// waiting for the outliers. Three such attempts with their wake-up preambles still fit
176/// EXCHANGE_TOTAL_BUDGET_MS when nothing is waited between them.
177static constexpr uint16_t LOW_POWER_RESPONSE_WAIT_MS = 550;
178/// How long an attempt listens for a mains device's reply when another attempt follows it, after
179/// the end of its own transmission.
180///
181/// A mains-powered device answers within a few tens of milliseconds or not at all. Across the
182/// corpus, 47 of the 52 start requests this hub sent to a device that is not declared low-power drew
183/// their first reply within 98 ms (median 66 ms, scripts/corpus/reply_latency.py --mains); the other
184/// five are a stalled key extraction and replies that belong to a later request. On the bench, an
185/// Izymo dimmer answered 36 to 42 ms after the start of the hub's transmission (400 exchanges, none
186/// retried), its carrier coming up within about 0.1 ms of the end of our frame; a Sunea awning, on
187/// every radio the hub drives, answered its challenged status reads 33 to 49 ms after it.
188/// 200 ms is twice the slowest of those corpus replies and about four times the bench maximum, and
189/// the largest round value for which nine attempts still fit EXCHANGE_TOTAL_BUDGET_MS: the early
190/// attempts listen this long and the last one listens the whole start window, so a device that does
191/// answer late still gets one full-length listen. Used by decisions::attempt_plan() for mains
192/// targets only (a low-power target keeps one window for every attempt).
193static constexpr uint16_t EXCHANGE_EARLY_RESPONSE_WAIT_MS = 200;
194/// Time an attempt costs beyond its transmission and its listen: the channel-busy check, the
195/// frequency hop and the SPI traffic around each transmit. Measured on the bench boards, one per
196/// radio the hub drives: a read to an address no device answers made attempts 422 ms apart where the
197/// transmit estimate and the 400 ms window add up to 412 ms. The budget guard and the attempt plan
198/// both add it to every attempt (decisions::attempt_fits()), so the plan promises exactly the
199/// attempts the guard admits.
200static constexpr uint32_t EXCHANGE_ATTEMPT_OVERHEAD_MS = 10;
201/// Re-sends allowed for a CMD_EXECUTE the target accepted without a closing reply, within the same
202/// exchange and its retry budget. One: a second silent try says the loss is not a one-off, and a
203/// third copy of a movement command would only lengthen the blocked loop. See
204/// decisions::retry_after_unconfirmed_accept_is_safe().
205static constexpr uint8_t UNCONFIRMED_EXECUTE_MAX_RESENDS = 1;
206/// Re-sends allowed for a CMD_SUBSCRIBE_EVENT the target accepted without a closing reply. The
207/// answer carries the reading (an RS100 Solar io answers the subscription with the event's current
208/// value), so a lost answer is worth one more copy; the device accepts repeated subscriptions
209/// (issue #156). One, as for an EXECUTE: a second silent try is not a one-off. See
210/// decisions::retry_after_unconfirmed_accept_is_safe().
211static constexpr uint8_t UNCONFIRMED_SUBSCRIBE_MAX_RESENDS = 1;
212/// Gap before that re-send, in place of the plan's retry gap (decisions::AttemptPlan::gap_ms). A
213/// device that did act on the first copy is often deaf for about a second while its motor or load
214/// switches: on a Somfy awning, a re-send 0.77 s after the first copy drew no challenge at all in
215/// 3 of 14 cases. With this gap the re-send goes out about 1.3 s after the first copy, and the
216/// whole exchange still fits EXCHANGE_TOTAL_BUDGET_MS. The gap is the total wait: the engine sleeps
217/// only the part the plan's own gap does not already cover, so a plan without a gap still re-sends
218/// at 750 ms.
219static constexpr uint32_t UNCONFIRMED_EXECUTE_RESEND_DELAY_MS = 750;
221 "the re-send gap extends the default retry gap, it never shortens it");
222
223/// How long evidence that a low-power receiver is moving keeps it believed awake enough to hear the
224/// short start preamble first. A moving VELUX solar receiver ignores the 1024-byte wake-up
225/// preamble but answers the short one, so a command, STOP or poll sent mid-travel must lead with
226/// the short one. Sized above the longest cover travel this project has measured; a first estimate
227/// that field logs will correct. Past it the receiver is presumed to have finished and fallen back
228/// asleep.
229static constexpr uint32_t LOW_POWER_MAX_TRAVEL_MS = 120000;
230
231/// Tries for pairing's phase-3 SetConfig1 (0x6F). One: no device on record accepts it (a Somfy
232/// Izymo answers `FE 28`, a VELUX SSL solar roller shutter stays silent) and no real controller
233/// sends it, so a retry only adds 0.7–0.9 s of blocking and a frame of airtime to every pairing
234/// with a silent device. A challenged try still completes its 0x3C/0x3D round within the one try.
235static constexpr uint8_t PAIRING_SET_CONFIG1_MAX_TRIES = 1;
236
237/// Exchange tries for a status poll the scheduler owns that only looks at a device that is probably
238/// still busy: every poll of a tracked device that is not a settling read
239/// (decisions::scheduled_poll_read_kind()).
240///
241/// Such a poll's failure is re-armed by the backoff ladder (STATUS_RETRY_AFTER_FAIL_MS and its
242/// successors), so the ladder *is* its retry mechanism; stacking blocking in-exchange tries on top
243/// of it buys no freshness and costs blocked loop() time while a device is unresponsive (e.g. an
244/// actuator mid-manoeuvre). The progress poll fires seconds after a command, squarely inside the
245/// manoeuvre, so keeping it a single try is what lets a STOP a user presses mid-move dispatch
246/// promptly, and it spares the air a burst of requests the device cannot answer yet. This is a
247/// deliberate airtime choice, not a default. A heater's poll takes this count outside the ladder
248/// band too (decisions::scheduled_poll_max_tries()).
249static constexpr uint8_t SCHEDULED_POLL_MAX_TRIES = 1;
250
251/// Ladder positions at which a scheduler-owned status poll is a settling read again.
252///
253/// The single try above is right at both ends of the backoff ladder and wrong in the middle. At the
254/// first slot after a command the device is still executing the manoeuvre: its silence is expected,
255/// retries cannot change that, and blocking loop() would delay a STOP the user presses mid-move.
256/// Once a device has missed several slots in a row it is unreachable rather than merely asleep, and
257/// retries are just as pointless. In between sits the slot where the manoeuvre has just ended and
258/// the device is awake again but duty-cycled: one listen samples its receive window once, while a
259/// settling read (decisions::attempt_plan() with decisions::ReadKind::SETTLING) samples it as often
260/// as the exchange budget allows, and a success there also clears the failure streak and ends the
261/// backoff.
262///
263/// Counted in consecutive silent failures already recorded when the poll is dispatched, so 0 is the
264/// post-command settle poll and 1..3 are the ~5 s / ~15 s / ~30 s ladder slots after it, roughly
265/// t+8 s to t+53 s, spanning every cover travel time this project has measured. For a device
266/// believed travelling the ladder, and so this band, starts only after its first
267/// MOVING_POLL_MISSES_SKIPPED misses, which keep the normal cadence. An auth-shaped streak is
268/// excluded entirely: a device that answers with a 0x3C challenge is awake, so extra attempts buy no
269/// wake-up, and an auth try is the most expensive shape the engine runs.
270///
271/// The poll after an accepted STOP and the poll confirming a stop off target are settling reads
272/// whatever the counters say: nothing is moving then, and the user's next action waits on exactly
273/// that reading.
274static constexpr uint8_t SCHEDULED_POLL_RETRY_GRACE_FIRST_FAILURE = 1;
275static constexpr uint8_t SCHEDULED_POLL_RETRY_GRACE_LAST_FAILURE = 3;
276
277/// Delay before polling after overheard remote traffic: another controller's frame to a registered
278/// device, and, by default, a linked-remote press (its `remote_activity_poll_delay_ms` tuning
279/// default; ADR 0080).
280static constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS = 2000;
281
282/// Wall-clock ceiling on one whole exchange, retries included.
283///
284/// The attempt count times (long preamble + response window + retry gap) is what determines how
285/// long a failing command blocks the ESPHome loop, and that blocking also starves the receive path
286/// the rest of the exchange depends on. ESPHome itself warns when one operation takes longer than
287/// 2550 ms (ADR 0013); this budget must stay under that threshold.
288///
289/// So the attempt count is a maximum, not a promise: an attempt only starts if it can finish inside
290/// the budget. Elapsed time, plus the gap before the attempt, plus the radio's estimate of its
291/// transmit time (`RadioDriver::tx_air_time_us()`), plus the window it then listens, plus the final
292/// reply window when the target has already challenged in this exchange, must stay below the
293/// budget. Counting the window and the final wait keeps a slow listen from running an exchange past
294/// the ceiling; counting the transmission keeps a radio with a long wake-up from starting an
295/// attempt it cannot finish in time. At the default 400 ms window three attempts with the retry gap
296/// still fit (~2.3 s); an exchange the target challenges does not always (ADR 0073). A request to a
297/// mains device that makes several attempts with no gap listens EXCHANGE_EARLY_RESPONSE_WAIT_MS on
298/// all but the last and fits nine (decisions::attempt_plan(), ADR 0075).
299static constexpr uint16_t EXCHANGE_TOTAL_BUDGET_MS = 2500;
300
301/// One-way (1W) transmit cadence.
302///
303/// A 1W command is fire-and-forget: nothing replies, so there is no acknowledgement to retry on
304/// and no way to learn a frame was missed. Repetition *is* the reliability mechanism — real
305/// remotes send the same frame four times, and a receiving device treats the set as one command
306/// because all four carry the same sequence. These are protocol values shared by every 1W
307/// transmitter, not radio tuning: they do not vary by chip and must not be moved into a driver or
308/// a TuningConfig field.
309///
310/// Both values are assumed, not measured on this project's hardware: four copies, 40 ms apart.
311/// Receive-side timestamps of consecutive copies in a capture log can show a smaller gap (roughly
312/// 25 ms), which is not a contradiction: they measure when the receiver heard each copy, not the
313/// configured gap. Do not "correct" 40 down to 25 on the strength of them.
314///
315/// Erring long would in any case be the safe direction — a device needs only one of the four
316/// copies to land, so a wider spacing costs nothing and leaves more room on a shared band.
317///
318/// The resulting burst duration — see ONEWAY_BURST_INTERVAL_MS's own comment and send_burst()'s
319/// doxygen (oneway_transmitter.h) for what it decomposes into per power class — is what
320/// ONEWAY_QUIET_PERIOD_MS (status_poll_policy.h) is sized against when it holds background polls
321/// back during 1W activity.
322static constexpr uint8_t ONEWAY_BURST_REPEATS = 4; ///< Copies of each 1W command sent per press.
323/// Gap between those copies. Three gaps between four copies is 3 * 40 = 120 ms of pure *delay*,
324/// fixed regardless of the identity's power class. What that adds up to on the wire depends on
325/// each copy's own airtime, which is a per-identity, per-copy choice (`OneWayPowerClass`,
326/// oneway_controller.h; ADR 0038): a burst with the long preamble on every copy runs well over a
327/// second, one with the short preamble on every copy a few hundred ms. send_burst()'s doxygen
328/// (oneway_transmitter.h) has both real shapes.
329static constexpr uint32_t ONEWAY_BURST_INTERVAL_MS = 40;
330
331/// How long a status reply that named the rain sensor as the last commander stays usable as
332/// evidence that rain protection is active (see decisions::is_rain_limited(), rule b). It is the
333/// memory for recognising a *clamped* position command: while it is fresh, a stopped device that
334/// ended up somewhere other than the commanded target is labelled as limited by rain. Too short,
335/// and a long shower is no longer recognised for the second and later `open` attempts; too long,
336/// and an unrelated clamp (end stop, obstacle, another protection) after the rain has stopped is
337/// mislabelled. A first estimate: the window's own rain-limitation timer is not known, so this
338/// is 2 hours.
339static constexpr uint32_t RAIN_EVIDENCE_HOLD_MS = 2UL * 60UL * 60UL * 1000UL;
340
341/// Listen-before-talk (LBT) parameters: conservative defaults that keep the hub from opening an
342/// exchange over traffic it can hear.
343/// Before transmitting a frame that opens a session (START set), the radio checks
344/// that the channel RSSI is below the threshold. If the channel is busy, TX is
345/// deferred by LBT_RETRY_DELAY_MS up to LBT_MAX_RETRIES times. A frame without
346/// START answers within a session and is sent at once, unchecked.
347static constexpr int16_t LBT_RSSI_THRESHOLD_DBM = -90; ///< Channel-free threshold
348static constexpr uint8_t LBT_MAX_RETRIES = 5; ///< Max carrier-sense attempts before TX anyway
349static constexpr uint8_t LBT_RETRY_DELAY_MS = 5; ///< Backoff between LBT checks
350
351/// Canonical defaults for the chip-neutral runtime-tunable pairing/discovery parameters.
352///
353/// These are the single source of truth for the diagnostics tuning layer: `TuningConfig`
354/// initializes its fields from them, and the ESPHome YAML schema falls back to them when a
355/// key is omitted. Adjust a value here and both the compiled default and the documented
356/// YAML default follow. Chip-specific tuning defaults live in tuning_config.h instead.
357/// See @ref hioc_tuning.
358static constexpr uint16_t PAIRING_DISCOVERY_WAIT_MS = 2000; ///< Wait window after sending each discovery command.
359static constexpr uint16_t PAIRING_DISCOVERY_INITIAL_DWELL_MS = 300; ///< Dwell on CH2 before discovery hopping begins.
360static constexpr uint8_t PAIRING_KEY_EXCHANGE_RETRIES = 3; ///< Retries for the authenticated key-exchange phase.
361
362/// Default for `TuningConfig::pairing_key_init_delay_ms` — pause after the discover-confirm step
363/// (whether it was ACKed, refused, or silent) and before CMD_KEY_INIT (0x31). No capture
364/// precedent for 300 ms specifically: real hubs take several seconds longer here, and appear to
365/// spend that time re-broadcasting 0x28 to look for more devices, which this project's
366/// single-device discovery phase has no equivalent of. 300 ms is the short end: a Somfy Izymo
367/// dimmer pairs with both 300 ms and 5000 ms here, so the default keeps pairing fast, and
368/// `pairing_key_init_delay_ms` can lengthen it for a device that needs a later 0x31.
369static constexpr uint16_t PAIRING_KEY_INIT_DELAY_MS = 300;
370
371} // namespace home_io_control
372} // namespace esphome
static constexpr uint8_t PAIRING_KEY_EXCHANGE_RETRIES
Retries for the authenticated key-exchange phase.
static constexpr uint8_t ONEWAY_BURST_REPEATS
One-way (1W) transmit cadence.
static constexpr uint32_t ONEWAY_BURST_INTERVAL_MS
Gap between those copies.
static constexpr uint8_t LOW_POWER_SETTLING_ATTEMPTS
Attempts a settling read makes to a low-power receiver.
static constexpr uint8_t LBT_MAX_RETRIES
Max carrier-sense attempts before TX anyway.
static constexpr uint32_t FREQ_CH1
The protocol uses 3 frequency channels in the 868 MHz ISM band.
static constexpr uint8_t SCHEDULED_POLL_MAX_TRIES
Exchange tries for a status poll the scheduler owns that only looks at a device that is probably stil...
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
static constexpr int32_t HOP_TIME_US
Timing constants for frequency hopping and response waiting.
static constexpr uint16_t PAIRING_KEY_INIT_DELAY_MS
Default for TuningConfig::pairing_key_init_delay_ms — pause after the discover-confirm step (whether ...
static constexpr uint16_t NORMAL_START_PREAMBLE
Default for TuningConfig::normal_start_preamble — the preamble in front of a directed start frame who...
static constexpr uint32_t UNCONFIRMED_EXECUTE_RESEND_DELAY_MS
Gap before that re-send, in place of the plan's retry gap (decisions::AttemptPlan::gap_ms).
static constexpr uint16_t EXCHANGE_EARLY_RESPONSE_WAIT_MS
How long an attempt listens for a mains device's reply when another attempt follows it,...
static constexpr uint32_t REMOTE_ACTIVITY_STATUS_POLL_DELAY_MS
Delay before polling after overheard remote traffic: another controller's frame to a registered devic...
static constexpr uint8_t SCHEDULED_POLL_RETRY_GRACE_FIRST_FAILURE
Ladder positions at which a scheduler-owned status poll is a settling read again.
static constexpr int32_t EXCHANGE_RETRY_DELAY_MS
Gap between retries within one HA command.
static constexpr uint32_t EXCHANGE_ATTEMPT_OVERHEAD_MS
Time an attempt costs beyond its transmission and its listen: the channel-busy check,...
constexpr TxWake tx_wake_for(bool start, uint16_t preamble)
The wake-up level of a frame, from the two facts that decide it: whether it starts an exchange (a con...
TxWake
How hard a transmission has to work to be heard: the wake-up level the receiver needs.
@ SHORT
The receiver is awake, but may be scanning channels.
@ NONE
The receiver is already listening on this channel: the frame continues an exchange.
@ LONG
The receiver may be duty-cycling.
static constexpr uint8_t PAIRING_SET_CONFIG1_MAX_TRIES
Tries for pairing's phase-3 SetConfig1 (0x6F).
static constexpr uint8_t EXCHANGE_RETRY_COUNT
Attempts per exchange for every caller that has not asked for an attempt plan (decisions::attempt_pla...
static constexpr uint32_t DISCOVERY_SLOT_MS
Length of one discovery answer slot.
static constexpr uint32_t LOW_POWER_MAX_TRAVEL_MS
How long evidence that a low-power receiver is moving keeps it believed awake enough to hear the shor...
static constexpr uint32_t SESSION_HOLD_BASE_MS
Base of how long a node acting as a device holds the session's channel after answering a controller,...
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
static constexpr uint16_t PAIRING_DISCOVERY_WAIT_MS
Canonical defaults for the chip-neutral runtime-tunable pairing/discovery parameters.
static constexpr uint8_t UNCONFIRMED_SUBSCRIBE_MAX_RESENDS
Re-sends allowed for a CMD_SUBSCRIBE_EVENT the target accepted without a closing reply.
static constexpr uint8_t LBT_RETRY_DELAY_MS
Backoff between LBT checks.
static constexpr uint16_t COLD_BROADCAST_REPLY_PREAMBLE
Default for TuningConfig::cold_broadcast_reply_preamble — preamble for a broadcast reply to a frame t...
static constexpr uint16_t SHORT_PREAMBLE
Protocol default and floor for continuation frames.
static constexpr uint8_t UNCONFIRMED_EXECUTE_MAX_RESENDS
Re-sends allowed for a CMD_EXECUTE the target accepted without a closing reply, within the same excha...
static constexpr int32_t RESPONSE_WAIT_MS
Wait for response to non-start frame.
static constexpr uint8_t EXCHANGE_MAX_ATTEMPTS
Most attempts any exchange makes, whatever the budget allows: a plan with a long exchange_total_budge...
static constexpr uint32_t PREAMBLE_LINGER_DWELL_MS
Preamble/sync linger extension for a rotating listen (ListenSpec::linger_dwell_ms): how much longer t...
static constexpr uint16_t LOW_POWER_RESPONSE_WAIT_MS
How long a read listens for a low-power receiver's reply after the end of its own transmission,...
static constexpr uint16_t LONG_PREAMBLE
Preamble is a sequence of 0xAA bytes that precedes every frame.
static constexpr uint32_t RAIN_EVIDENCE_HOLD_MS
How long a status reply that named the rain sensor as the last commander stays usable as evidence tha...
constexpr bool is_wake_preamble(uint16_t len)
True when a preamble this long is the wake-up burst for a duty-cycled receiver.
static constexpr int32_t RESPONSE_AUTH_WAIT_MS
Wait for final response after challenge response.
static constexpr uint16_t LOW_POWER_SHORT_LEAD_WINDOW_MS
How long the short lead attempt of a read to a low-power receiver in an unknown state listens after t...
static constexpr uint8_t SCHEDULED_POLL_RETRY_GRACE_LAST_FAILURE
static constexpr uint16_t EXCHANGE_TOTAL_BUDGET_MS
Wall-clock ceiling on one whole exchange, retries included.
static constexpr int16_t LBT_RSSI_THRESHOLD_DBM
Listen-before-talk (LBT) parameters: conservative defaults that keep the hub from opening an exchange...
static constexpr uint16_t PAIRING_DISCOVERY_INITIAL_DWELL_MS
Dwell on CH2 before discovery hopping begins.
static constexpr int32_t RESPONSE_START_WAIT_MS
Wait for a response to a start frame — the first frame of an exchange, and the one a sleeping device ...