Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_soft_phy_driver_base.h
Go to the documentation of this file.
1#pragma once
2
3/// @file radio_soft_phy_driver_base.h
4/// @brief Shared driver flow for radios using the software PHY (SX1262, LR1121).
5/// @ingroup hioc_radio
6///
7/// RadioSX1262 and RadioLR1121 both lack the SX1276's IoHomeOn hardware framing, so both
8/// reproduce IO-Homecontrol framing in software on top of generic GFSK support
9/// (`radio_soft_phy.h`'s UART bit-encode/probe). Beyond that shared bit-level codec, the two
10/// drivers' IRQ-driven RX state machine and TX orchestration are identical in every detail that
11/// isn't chip-specific transport or register encoding, so this class holds that shared flow once
12/// instead of each driver maintaining its own copy.
13///
14/// This class holds everything the two drivers do identically: `wait_for_packet()`/
15/// `check_for_packet()`'s IRQ polling and sync/RX-done race resolution, `read_rx_packet()`'s
16/// buffer-read and UART-probe recovery, `send_packet()`'s TX orchestration, `read_rssi()`'s
17/// formula, and the response-preamble/post-TX-settle tuning fields. What genuinely differs
18/// between the two chips — SPI opcode encoding/transport, IRQ bit values and word width,
19/// register-level packet/modulation parameter encoding, and the handful of one-off steps one
20/// chip needs that the other doesn't (SX1262's buffer-base-address write, LR1121's high-ACP
21/// pre-TX workaround and preamble-tolerant activity check) — stays behind virtual primitives and
22/// hooks implemented by `RadioSX1262`/`RadioLR1121`.
23
24#include "radio_interface.h"
25#include "radio_soft_phy.h"
26
27#include <cstddef>
28#include <cstdint>
29
30namespace esphome {
31namespace home_io_control {
32
33/// Fixed raw-RX probe length. Typical traffic is 23-25 byte protocol frames (34 raw bytes packed
34/// with CRC); the binding case is the largest frame the probe must still recover intact — a 1W CMD
35/// 0x30 add-controller with its out-of-length MAC trailer, which packs to 47 raw bytes and needs
36/// up to 9 more bits of headroom for the probe's leading bit-offset sweep. 48 bytes covers that
37/// (see the SOFT_PHY_MAX_WIRE_FRAME_RAW_BYTES static_assert below) without relying on either
38/// chip's variable-length engine. This is a protocol-frame-size property, not a chip quirk, so
39/// both drivers share one value — used here for the raw-probe threshold in @ref
40/// SoftPhyDriverBase::read_rx_packet and by each driver's own `set_rx_packet_params()` for the
41/// configured RX payload length.
42static constexpr uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN = 48;
43
44/// Sentinel meaning "every IRQ bit counts as activity" — the default for @ref
45/// SoftPhyDriverBase::activity_irq_mask. Neither concrete driver uses it any more: both SX1262
46/// and LR1121 unmask PreambleDetected at the hardware level and override this to exclude it (see
47/// SX1262_IRQ_ACTIVITY_MASK / LR1121_IRQ_ACTIVITY_MASK). Kept as the base-class default for a
48/// hypothetical future driver that never unmasks PreambleDetected in the first place, where "any
49/// bit" is genuinely safe again.
50static constexpr uint32_t SOFT_PHY_ALL_IRQ_BITS = 0xFFFFFFFF;
51
52/// Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART
53/// cell (10 bits) at any of the probe's bit alignments (up to 9 bits of slack).
54static constexpr uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES = 3;
55
56/// Air-time margin added before every mid-reception buffer read, in raw bytes.
57///
58/// Covers the lag between a byte finishing on air and the chip having it in its data buffer, plus
59/// the granularity of the polled sync-word observation. Two byte-times is generous at this line
60/// rate and still leaves a length-driven receive far ahead of the fixed-length RX_DONE.
61static constexpr uint8_t SOFT_PHY_EARLY_READ_MARGIN_BYTES = 2;
62
63/// Poll interval while waiting out a frame's remaining air time, in microseconds.
64static constexpr uint32_t SOFT_PHY_EARLY_POLL_US = 100;
65
66/// Smallest receive window a length-driven receive will be attempted in, in milliseconds.
67///
68/// The longest possible frame (FRAME_MAX_SIZE + CRC, UART-packed) occupies ~9.4 ms of air time, so
69/// a caller with less than this left cannot finish one either way. Declining up front keeps the
70/// early path from spending a short window's whole budget on a receive it cannot complete.
71static constexpr uint32_t SOFT_PHY_EARLY_MIN_WINDOW_MS = 12;
72
73/// Blocking budget @ref SoftPhyDriverBase::check_for_packet gives a length-driven receive, in
74/// milliseconds — the `timeout_ms` passed to `try_early_completion_()` from the non-blocking
75/// idle-loop RX path (issue #81). The real bound on how long that call can block is the frame's
76/// own air time (~9.4 ms worst case, see @ref SOFT_PHY_EARLY_MIN_WINDOW_MS), not this number; this
77/// value only has to be large enough not to cut a genuine frame short. It is not itself a tight
78/// bound on loop() latency — the actual air time is what stays well inside a loop() tick
79/// (~16-30 ms) — and either way it is dwarfed by the 1-3 s a blocking exchange already costs
80/// `loop()`.
81static constexpr uint32_t SOFT_PHY_IDLE_RX_COMPLETION_BUDGET_MS = 20;
82
83/// Wait after retuning to the TX channel before the clear-channel RSSI reading. A retune on these
84/// chips is standby → SetRfFrequency → SetRx (the TCXO stays on in XOSC standby), after which the
85/// PLL has to relock and the instantaneous RSSI has to average over the new channel; 1 ms covers
86/// that with margin and costs nothing next to a preamble.
87/// \todo Confirm on the bench (Heltec V3, T3-S3 LR1121): time the retune and compare the reading
88/// against one taken after a long dwell on the same channel.
89static constexpr uint32_t SOFT_PHY_CCA_SETTLE_US = 1000;
90
92 "a budget below the minimum window makes try_early_completion_() decline every "
93 "idle-path call, silently turning issue #81's fix into a no-op");
94
95// RX_HOP_HOLDOFF_US (radio_interface.h) exists to outlast the fixed-length RX_DONE on the
96// software-PHY chips. Tied here, in the one header that can see both sides of the arithmetic,
97// so a change to either constant that breaks the relationship fails the build instead of
98// silently turning issue #81's gate back into the bug it fixes.
100 "the hop holdoff must outlast the fixed-length RX_DONE it exists to wait for — a "
101 "shorter bound lets the hop fire while the frame it is protecting is still on air, "
102 "silently turning issue #81's gate back into the bug");
103
104/// Largest real IO-Homecontrol frame the fixed-length RX probe has to recover intact: a 1W CMD
105/// 0x30 "add controller". Its 9-byte header + 20-byte payload (wrapped key[16] + manufacturer[1] +
106/// data[1] + sequence[2]) = 29 declared bytes, and it carries a 6-byte authenticator as an
107/// out-of-length trailer *after* the declared length but still under the CRC — so the probe must
108/// hold 29 + 6 = 35 wire bytes. This is larger than any frame CTRL0's length field alone
109/// describes, which is why it, not FRAME_MAX_DECLARED_SIZE, is the binding case.
110static constexpr uint16_t SOFT_PHY_MAX_WIRE_FRAME_BYTES =
111 FRAME_MIN_SIZE + AES_KEY_SIZE + 1 /*manufacturer*/ + 1 /*data*/ + 2 /*sequence*/ + HMAC_SIZE;
112
113/// Raw on-air bytes that frame occupies once its 2-byte CRC is appended and the whole thing is
114/// UART-packed into 10-bit cells (start + 8 data + stop), rounded up to whole bytes — the same
115/// arithmetic as soft_phy_raw_bytes_for_frame(), spelled out here because that helper is not
116/// constexpr. Plus `UART_PROBE_MAX_BIT_OFFSET - 1` bits: the probe locates the frame start by
117/// sweeping leading alignments, so a recovered frame can begin that many bits into the raw buffer.
118/// 35 + 2 = 37 cells = 370 bits, + 9 slack bits = 379 → 48 raw bytes, exactly the probe length.
119static constexpr uint16_t SOFT_PHY_MAX_WIRE_FRAME_RAW_BYTES =
120 ((SOFT_PHY_MAX_WIRE_FRAME_BYTES + FRAME_CRC_SIZE) * UART_CELL_BITS + (UART_PROBE_MAX_BIT_OFFSET - 1) +
121 BITS_PER_BYTE - 1) /
122 BITS_PER_BYTE;
123
125 "the fixed-length RX probe must hold a UART-packed 1W 0x30 add-controller frame with "
126 "its out-of-length MAC trailer and CRC, at any of the probe's leading bit alignments "
127 "— a shorter probe clips the trailer (or CRC) off an enrollment frame mid-air and the "
128 "frame is silently dropped");
129
131 "the RX probe is read into RadioRxPacket::raw / ::data, which are "
132 "RADIO_PACKET_BUFFER_SIZE bytes — if a growing frame format pushes the probe length "
133 "past the buffer (via the assert above), those reads overflow");
134
135// FRAME_MAX_WIRE_SIZE (proto_sizes.h) — a full 32-byte declared frame *plus* a trailer plus CRC —
136// packs to 50 raw bytes (52 with the bit-offset slack applied above), past this 48-byte probe. It
137// is unreachable only because no real frame combines a 32-byte declared length with an
138// out-of-length trailer: the trailer rides exclusively on CMD 0x30, whose declared length is fixed
139// at 29. If a second trailer-bearing command with a longer declared length is ever added,
140// SOFT_PHY_MAX_WIRE_FRAME_BYTES above must grow to match and this probe length will need to grow
141// with it.
142
143// === Device-error decoding ===
144// Both chips report a 16-bit "device errors" word (SX1262 GetDeviceErrors, LR1121 GetErrors) whose
145// bit meanings differ; the decoding and the init-time capture below are shared, the tables are not.
146
147/// One named bit of a chip's device-error word.
149 uint16_t mask; ///< The bit (or bits) of the error word this row names.
150 const char *name; ///< Log name, e.g. `PLL_LOCK_ERR`.
151};
152
153/// Buffer size that always fits format_device_error_bits()'s longest output for either chip (every
154/// named bit, `|`-joined, plus an UNKNOWN_0x%04X tail).
155static constexpr size_t DEVICE_ERROR_STR_SIZE = 160;
156
157/// @brief Expand a device-error word into a human-readable `NAME|NAME|...` string.
158/// @param errors Raw device-error bitmask.
159/// @param bits The chip's bit → name table, in output order.
160/// @param bit_count Number of rows in @p bits.
161/// @param buf Caller-owned output buffer; always NUL-terminated on return.
162/// @param buf_size Size of @p buf. Use @ref DEVICE_ERROR_STR_SIZE.
163///
164/// Writes `"none"` when @p errors is zero, and appends `UNKNOWN_0x%04X` for any set bit with no
165/// name in the table so an undocumented flag still shows up in the log.
166void format_device_error_bits(uint16_t errors, const DeviceErrorBit *bits, size_t bit_count, char *buf,
167 size_t buf_size);
168
169/// @copybrief format_device_error_bits
170/// @tparam N Row count of the chip's table, deduced from the array so no caller counts it by hand.
171template<size_t N>
172void format_device_error_bits(uint16_t errors, const DeviceErrorBit (&bits)[N], char *buf, size_t buf_size) {
173 format_device_error_bits(errors, bits, N, buf, buf_size);
174}
175
176/// A chip's own decoder, as passed to SoftPhyDriverBase::dump_init_device_errors_().
177using DeviceErrorFormatter = void (*)(uint16_t errors, char *buf, size_t buf_size);
178
179/// @brief Shared RX/TX driver flow for the software-PHY radios (SX1262, LR1121).
180/// @ingroup hioc_radio
182 public:
183 /// @param rst_pin Active-low hardware reset pin, forwarded to RadioDriver.
184 /// @param busy_pin BUSY line polled by @ref wait_busy_ before every SPI transaction — both
185 /// concrete drivers are opcode-based chips that require this, unlike the register-based
186 /// SX1276.
187 /// @param busy_timeout_ms How long @ref wait_busy_ waits for BUSY to drop before declaring the
188 /// chip failed. Chip-specific (SX1262: 10 ms: RC-oscillator timing; LR1121: 3000 ms, matched
189 /// to RadioLib's post-reset boot-ROM wait) — this class has no opinion on the value, only on
190 /// where it's stored and how it's used.
191 /// @param default_response_preamble Chip-specific default for @ref response_preamble (each
192 /// concrete driver passes its own validated constant — this class has no opinion on the
193 /// value, only on where it's stored).
194 /// @param default_post_tx_settle_us Chip-specific default post-TX settling delay, same rationale.
195 SoftPhyDriverBase(InternalGPIOPin *rst_pin, InternalGPIOPin *busy_pin, uint32_t busy_timeout_ms,
196 uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
197 : RadioDriver(rst_pin),
198 busy_pin_(busy_pin),
199 response_preamble_(default_response_preamble),
200 post_tx_settle_us_(default_post_tx_settle_us),
201 busy_timeout_ms_(busy_timeout_ms) {}
202
203 /// @copydoc RadioDriver::send_packet
204 TxResult send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override;
205 /// @copydoc RadioDriver::wait_for_packet
206 bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override;
207 /// @copydoc RadioDriver::check_for_packet
208 bool check_for_packet(RadioRxPacket &packet) override;
209 /// @copydoc RadioDriver::change_frequency
210 void change_frequency(uint32_t freq_hz) override;
211 /// @copydoc RadioDriver::read_rssi
212 ///
213 /// Same formula on both chips (`-(int16_t) raw / 2`, see @ref raw_rssi_to_dbm_); only the opcode
214 /// used to read the single raw byte differs, via @ref read_rssi_raw_byte. Antenna-referred: the
215 /// gain of an external receive front end is removed, see @ref front_end_rx_gain_db.
216 int16_t read_rssi() override;
217 /// @copydoc RadioDriver::is_sync_detected
218 bool is_sync_detected() override;
219 /// @copydoc RadioDriver::is_preamble_detected
220 ///
221 /// Consults the private `preamble_latched_at_timeout_` flag first; see that member's declaration
222 /// for why.
223 bool is_preamble_detected() override;
224 /// @brief Preamble for response/continuation frames — shared storage, see the concrete
225 /// drivers' constructors/tuning defaults for the chip-specific rationale and value.
226 [[nodiscard]] uint16_t response_preamble() const override { return this->response_preamble_; }
227
228 /// @copydoc RadioDriver::default_start_preamble
229 ///
230 /// This PHY puts less usable preamble on air than its programmed length suggests, so it asks for
231 /// more than the protocol's documented value (`SOFT_PHY_START_PREAMBLE`, which carries the
232 /// measurement). The override belongs here rather than on each concrete driver because the
233 /// shared PHY is the level the effect follows: SX1262 and LR1121 measured the same as each other
234 /// and both differed from the register PHY, so a per-driver value would invent a difference the
235 /// evidence does not show. See ADR 0042.
236 [[nodiscard]] uint16_t default_start_preamble() const override { return SOFT_PHY_START_PREAMBLE; }
237
238 /// @brief Transmit time: the preamble, sync word and UART-coded frame at the protocol line rate.
239 [[nodiscard]] uint32_t tx_air_time_us(uint8_t len, const RadioTxConfig &cfg) const override {
240 return io868_tx_air_time_us(cfg.preamble_len, len);
241 }
242
243 protected:
244 // --- Tuning helpers shared by both drivers (values/defaults stay chip-specific) ---
245 /// Set the preamble length used for response/continuation frames within an exchange.
246 void set_response_preamble_(uint16_t preamble) { this->response_preamble_ = preamble; }
247 /// Set the delay between TX completion and re-entering RX.
248 void set_post_tx_settle_us_(uint16_t delay_us) { this->post_tx_settle_us_ = delay_us; }
249 /// @brief Current @ref wait_busy_ timeout, in milliseconds.
250 [[nodiscard]] uint32_t get_busy_timeout_ms_() const { return this->busy_timeout_ms_; }
251 /// @brief Set the @ref wait_busy_ timeout, in milliseconds.
252 ///
253 /// Exposed so a concrete driver can widen it around a bring-up step that legitimately holds BUSY
254 /// far longer than the steady-state value (SX1262 TCXO startup runs up to 50 ms against a 10 ms
255 /// default) and then restore it. Pair every widen with a restore.
256 void set_busy_timeout_ms_(uint32_t timeout_ms) { this->busy_timeout_ms_ = timeout_ms; }
257 /// @brief Wait until @ref busy_pin_ reads low, feeding the watchdog while polling.
258 ///
259 /// Shared verbatim between SX1262 and LR1121 — the two chips differ only in how long they're
260 /// willing to wait (`busy_timeout_ms_`). A call short-circuits once the driver has latched
261 /// `failed_`: without that guard, every remaining `configure_radio_()` step after the first
262 /// failure would re-run the full timeout, turning one bad boot into tens of seconds of hang at
263 /// the LR1121's 3000 ms timeout (harmless but still pointless at the SX1262's 10 ms one).
264 void wait_busy_();
265 /// @brief Keep the device-error word read at the end of `configure_radio_()` and warn if it is
266 /// non-zero.
267 /// @param tag Log tag of the calling driver.
268 /// @param chip_label Chip name for the warning, e.g. "SX1262".
269 /// @param errors The word read from the chip, before the caller clears its register.
270 /// @param format The calling chip's decoder for its device-error word.
271 void record_init_device_errors_(const char *tag, const char *chip_label, uint16_t errors,
272 DeviceErrorFormatter format);
273 /// @brief Gain (dB) of the board's external receive front end that the chip's RSSI includes.
274 ///
275 /// A low-noise amplifier ahead of the radio raises every RSSI reading by its gain, which makes an
276 /// idle channel look busy against a threshold defined at the antenna (listen-before-talk) and
277 /// makes signal levels incomparable between boards. Drivers whose board has such a front end
278 /// override this; @ref raw_rssi_to_dbm_ then reports antenna-referred values. Boards without one
279 /// keep the default of 0.
280 [[nodiscard]] virtual int8_t front_end_rx_gain_db() const { return 0; }
281 /// @brief Convert a chip's raw RSSI byte to an antenna-referred dBm reading.
282 /// @param raw Raw byte from the chip (`dBm = -raw / 2` at the radio's input, on both chips).
283 /// @return The reading with @ref front_end_rx_gain_db removed. Used for every RSSI this driver
284 /// reports — live (@ref read_rssi) and per received packet — so they share one scale.
285 [[nodiscard]] int16_t raw_rssi_to_dbm_(uint8_t raw) const {
286 return static_cast<int16_t>((-static_cast<int16_t>(raw) / 2) - this->front_end_rx_gain_db());
287 }
288 /// @brief Log the init-time device errors under @p tag, or nothing when there were none.
289 /// @param tag Log tag of the calling driver, so the line stays inside its own dump section.
290 /// @param format The calling chip's decoder for its device-error word.
291 void dump_init_device_errors_(const char *tag, DeviceErrorFormatter format) const;
292 /// Device-error word a driver reads at the end of `configure_radio_()`, before it clears the
293 /// chip's register. The chip still initializes and transmits with some of these set (PLL lock,
294 /// calibration, TCXO start), so the config dump reports the captured value: a later read of the
295 /// register would only ever see the cleared state.
297 /// BUSY line, read directly by both concrete drivers' own `dump_debug()` in addition to
298 /// @ref wait_busy_, so this stays protected rather than folding entirely into the private
299 /// wait-loop state below.
300 InternalGPIOPin *busy_pin_;
301
302 // --- Shared RX/TX orchestration (moved verbatim from RadioSX1262/RadioLR1121) ---
303 /// Read a received packet from the buffer and return the raw bytes reported by the chip.
304 /// Virtual to allow test doubles (both concrete drivers' tests override this).
305 virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status);
306 /// Reset RX state machine and buffer. Optionally force standby first.
307 void reset_rx_state_(bool force_standby = true);
308 /// Minimal path back into RX immediately after a transmission — see the definition for why this
309 /// is deliberately not @ref reset_rx_state_.
310 void rearm_rx_after_tx_();
311
312 /// @brief The GFSK SetPacketParams fields both software-PHY chips program identically.
313 ///
314 /// Both drivers build the same nine-byte SetPacketParams payload in the same order; only the
315 /// preamble-detector length and the sync-word selector are chip constants, and only the opcode
316 /// and transport differ. Everything else — the field order and the byte/bit preamble conversion
317 /// that the `63e2502` fix had to patch in two places — lives in @ref build_gfsk_packet_params.
319 uint16_t preamble_bytes; ///< Byte-denominated, like every other preamble value in this codebase.
320 uint8_t preamble_detector; ///< Chip-specific detector-length selector.
321 uint8_t sync_word_param; ///< Chip-specific 24-bit sync-word selector.
322 uint8_t packet_type; ///< Known-length / fixed-length selector.
323 uint8_t payload_len; ///< Configured payload length.
324 uint8_t crc_type; ///< Chip-specific CRC-mode selector.
325 };
326 /// Byte count of the GFSK SetPacketParams payload — identical on both software-PHY chips.
327 static constexpr uint8_t GFSK_PACKET_PARAMS_LEN = 9;
328 /// @brief Fill the nine-byte GFSK SetPacketParams payload shared by both chips.
329 ///
330 /// Owns the field order and the ×8 preamble conversion: the SetPacketParams preamble field is
331 /// bit-denominated on both chips (Semtech names it `preamble_len_in_bits` /
332 /// `pbl_len_in_bit`), but every caller in this codebase passes a byte count. Address comparison
333 /// and whitening are always off.
334 /// @param p The chip-independent field values (byte-denominated preamble, chip constants).
335 /// @param out Buffer for exactly @ref GFSK_PACKET_PARAMS_LEN bytes.
336 ///
337 /// Named without the trailing `_` the file's other protected helpers carry: it uses no instance
338 /// state, so clang-tidy's `readability-convert-member-functions-to-static` wants it `static`, and
339 /// `.clang-tidy`'s `ClassMethodCase = lower_case` then rejects a trailing `_` on a `static`
340 /// method. Leaving it `static` and dropping the underscore keeps clang-tidy quiet without a
341 /// suppression.
342 static void build_gfsk_packet_params(const SoftPhyPacketParams &p, uint8_t out[GFSK_PACKET_PARAMS_LEN]);
343
344 /// @brief Read the raw IRQ status word from the radio.
345 /// Virtual to allow test doubles (both concrete drivers' tests override this).
346 virtual uint32_t read_irq_status_raw() = 0;
347 /// Clear IRQ status bits.
348 /// @param irq_mask Bitmask of IRQs to clear (each driver narrows to its own IRQ word width).
349 virtual void clear_irq_status(uint32_t irq_mask) = 0;
350
351 /// @name Chip-specific IRQ bit values
352 /// Each driver's own IRQ bit constants, exposed as accessors so the shared RX/TX orchestration
353 /// never needs to name a chip-specific constant directly.
354 ///@{
355 [[nodiscard]] virtual uint32_t sync_word_valid_bit() const = 0;
356 [[nodiscard]] virtual uint32_t rx_done_bit() const = 0;
357 [[nodiscard]] virtual uint32_t tx_done_bit() const = 0;
358 [[nodiscard]] virtual uint32_t preamble_detected_bit() const = 0;
359 ///@}
360
361 /// @brief IRQ bits that count as "activity" for the internal `poll_until_activity_()` helper
362 /// and @ref check_for_packet.
363 ///
364 /// Default is "any bit" — safe only for a driver whose `SetDioIrqParams`-equivalent mask never
365 /// includes `PreambleDetected` in the first place, so a preamble-only reading can never reach
366 /// this check. Neither current driver qualifies: both SX1262 and LR1121 unmask
367 /// `PreambleDetected` (each for its own reason) and override this to exclude it — a
368 /// preamble-only reading means a frame may still be arriving, and treating it as terminal
369 /// activity would tear down RX mid-reception.
370 [[nodiscard]] virtual uint32_t activity_irq_mask() const { return SOFT_PHY_ALL_IRQ_BITS; }
371
372 // `SOFT_PHY_RX_PROBE_PACKET_LEN` below is a code span, not \ref: doxygen 1.18 can't resolve
373 // \ref to it in a whole-project build (details in proto_sizes.h). Autolinking still links it.
374 /// @brief Data-buffer offset an in-flight reception is being written to, or a negative value
375 /// when this chip must not be read before RX_DONE.
376 ///
377 /// Neither chip's RX_DONE marks the end of the *frame*: with no hardware framing, RX runs in
378 /// fixed-length mode at `SOFT_PHY_RX_PROBE_PACKET_LEN`, so RX_DONE arrives a fixed ~10 ms
379 /// after the sync word no matter how short the frame actually was. That delay lands squarely on
380 /// the protocol's tightest turnaround — the hub's reply to a device's challenge — so a driver
381 /// that can read its buffer while reception is still running opts in here and the shared flow
382 /// finishes on the frame's own air time instead (see `try_early_completion_`).
383 ///
384 /// Default is -1: wait for RX_DONE exactly as before. SX1262 overrides it with the RX base
385 /// address it programs in configure_buffer_base(), which is where a single in-flight packet
386 /// always starts. LR1121 also opts in, at the base of its one shared TX/RX buffer — see
387 /// RadioLR1121::early_rx_read_offset for why a wrong guess there is safe only in combination
388 /// with @ref invalidate_stale_rx_content_after_tx.
389 [[nodiscard]] virtual int16_t early_rx_read_offset() const { return -1; }
390
391 /// @brief Blocking budget for the idle-path length-driven receive, in milliseconds (issue #81).
392 ///
393 /// Virtual only so tests can widen it. The host clock stubs advance `millis()`/`micros()` by one
394 /// unit per call (`tests/include/esphome/core/hal.h`), so a frame's few thousand microseconds of
395 /// air time also burns a few thousand fake milliseconds — any production-sized budget expires
396 /// inside `wait_for_air_time()`'s first stage and the whole path becomes untestable. The existing
397 /// blocking-path early-completion tests dodge this by passing `wait_for_packet()` a 20000 ms
398 /// timeout; this path has no caller-supplied timeout to widen, so the seam has to live here.
399 [[nodiscard]] virtual uint32_t idle_rx_completion_budget_ms() const { return SOFT_PHY_IDLE_RX_COMPLETION_BUDGET_MS; }
400
401 /// Set RF frequency via the chip's own frequency register/opcode encoding, and update
402 /// `current_freq_`. Called from both @ref change_frequency and the shared `send_packet()`.
403 virtual void set_frequency_register(uint32_t freq_hz) = 0;
404 /// Configure RX-specific packet parameters (preamble detector length, fixed probe length).
405 virtual void set_rx_packet_params() = 0;
406 /// Configure TX packet parameters for one outgoing UART-encoded frame.
407 /// @param preamble_len Preamble length in symbols, from the caller's RadioTxConfig.
408 /// @param payload_len UART-encoded payload length in bytes.
409 virtual void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) = 0;
410 /// Read the single raw RSSI byte (chip-specific opcode); formula is shared, see @ref read_rssi.
411 virtual uint8_t read_rssi_raw_byte() = 0;
412 /// Write the UART-encoded TX payload into the chip's TX buffer.
413 virtual void write_tx_buffer(const uint8_t *data, uint8_t len) = 0;
414 /// Read the chip-reported RX length and buffer offset (raw, before any clamping).
415 virtual void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) = 0;
416 /// Read `len` bytes from the RX buffer starting at `offset`.
417 virtual void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) = 0;
418 /// Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,
419 /// differing only in opcode/transport, so this stays a thin chip-specific wrapper.
420 virtual void start_tx() = 0;
421 /// Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,
422 /// and IRQ-word-width narrowing all differ per chip).
423 virtual void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
424 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) = 0;
425
426 /// @brief Hook run immediately before every `SetTx`. No-op by default; LR1121 overrides this to
427 /// apply its high-ACP TX-quality workaround, which Semtech's own reference applies unconditionally
428 /// before every SetRx/SetTx.
429 virtual void before_tx_arm() {}
430 /// @brief Hook run as part of @ref reset_rx_state_, before re-entering RX. No-op by default;
431 /// SX1262 overrides this to (re-)write its buffer base address, which LR1121 doesn't need.
432 virtual void configure_buffer_base() {}
433 /// @brief Hook run from @ref rearm_rx_after_tx_, after a transmission and before re-entering RX.
434 /// No-op by default.
435 ///
436 /// SX1262 has a real address split written once at init (@ref configure_buffer_base): TX always
437 /// builds at its own base, RX always lands at a different one, so nothing a transmission wrote
438 /// can ever appear where a length-driven receive (@ref early_rx_read_offset) later reads from.
439 /// LR1121 has no such split — one shared 256-byte buffer for both directions, and `WriteBuffer`
440 /// always starts from the buffer base (see `write_buffer_`'s doc comment) — so after every LR1121
441 /// transmission, the offset a length-driven receive will read from holds a real, CRC-valid,
442 /// UART-encoded copy of the hub's own last-sent frame. That is not noise: if
443 /// `early_rx_read_offset()`'s offset guess ever turns out to be wrong, reading that residue back
444 /// would pass every stage of `try_early_completion_()` and hand back the hub's own transmission as
445 /// a phantom received packet — worse than doing nothing, since `check_for_packet()` would then
446 /// tear down and re-arm RX (issue #81's `force_standby` path) over whatever real reception was
447 /// actually in progress. RadioLR1121 overrides this to overwrite that offset with a few
448 /// non-frame-shaped bytes after every TX, so a wrong offset guess degrades back to reading genuine
449 /// garbage — which correctly fails the length-driven receive's stage 1 or stage 3 — instead of a
450 /// valid-looking phantom frame.
452
453 private:
454 // === wait_for_packet/check_for_packet state-machine helpers (private) ===
455 /// Poll for first *terminal* radio activity (IRQ pin or an IRQ status bit within
456 /// @ref activity_irq_mask) within timeout.
457 bool poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq);
458 /// Resolve the SYNC_WORD_VALID → RX_DONE race condition common to both chips, and — on a chip
459 /// that opts into @ref early_rx_read_offset — give the length-driven receive its chance first.
460 /// @param early_completed Set when a whole CRC-valid frame was recovered without waiting for
461 /// RX_DONE; `packet` is then already populated and the caller is done.
462 bool resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq, RadioRxPacket &packet,
463 bool &early_completed);
464 /// Finish a reception on the frame's own air time rather than on the chip's fixed-length
465 /// RX_DONE. Returns true only when a CRC-valid frame was recovered.
466 bool try_early_completion_(RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status, uint32_t start_ms,
467 uint32_t timeout_ms);
468 /// Finalize receive: read the packet if RX_DONE is set, otherwise record failure.
469 bool finalize_receive_(RadioRxPacket &packet, uint32_t irq);
470
471 uint16_t response_preamble_;
472 uint16_t post_tx_settle_us_;
473 uint32_t busy_timeout_ms_;
474
475 /// @brief Whether `PreambleDetected` was latched at the exact moment a dwell's
476 /// `poll_until_activity_()` gave up waiting, captured before `reset_rx_state_()` wipes the IRQ
477 /// word out from under it.
478 ///
479 /// `PreambleDetected` is deliberately excluded from @ref activity_irq_mask (a bare preamble must
480 /// not look like terminal activity), and that exclusion is exactly why a dwell *can* time out
481 /// while the bit is still latched — the timeout branch has no way to notice, since
482 /// activity_irq_mask() never told it to look. What happens next is a separate problem:
483 /// `reset_rx_state_()` runs immediately afterward and clears the whole IRQ word, so a live
484 /// re-read a few tens of microseconds later (in `listen()`'s `preamble_or_sync_incoming()` guard)
485 /// would need the chip to re-observe a fresh preamble from scratch — which takes a little longer
486 /// on a chip with a longer preamble-detector length — and it almost never has done so by the time
487 /// the guard asks. This flag is the only way that information survives the reset long enough for
488 /// the guard to read it.
489 ///
490 /// `is_sync_detected()` needs no equivalent snapshot — not because no path clears sync before a
491 /// timeout (`resolve_sync_race_()` does exactly that: it clears `SYNC_WORD_VALID` up front once a
492 /// reception is underway, and can still time out later waiting for RX_DONE), but because that
493 /// path never calls `reset_rx_state_()` on its way out. Whatever `PreambleDetected` state existed
494 /// going into it survives untouched, which is all the guard actually depends on there.
495 ///
496 /// Lifetime is bounded to a single `poll_until_activity_()` call so a stale detection can never
497 /// suppress a later, unrelated hop: every exit from that function's loop sets this flag from
498 /// scratch (true only on the timeout branch, and only when the bit was actually set on that exact
499 /// read), and @ref is_preamble_detected consumes (clears) it the moment it is read as true.
500 bool preamble_latched_at_timeout_{false};
501};
502
503} // namespace home_io_control
504} // namespace esphome
RadioDriver(InternalGPIOPin *rst_pin=nullptr)
virtual uint32_t read_irq_status_raw()=0
Read the raw IRQ status word from the radio.
virtual void set_frequency_register(uint32_t freq_hz)=0
Set RF frequency via the chip's own frequency register/opcode encoding, and update current_freq_.
void wait_busy_()
Wait until busy_pin_ reads low, feeding the watchdog while polling.
virtual void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset)=0
Read the chip-reported RX length and buffer offset (raw, before any clamping).
void set_busy_timeout_ms_(uint32_t timeout_ms)
Set the wait_busy_ timeout, in milliseconds.
bool is_preamble_detected() override
Check if preamble has been detected (while in RX).
uint32_t get_busy_timeout_ms_() const
Current wait_busy_ timeout, in milliseconds.
virtual void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len)=0
Read len bytes from the RX buffer starting at offset.
virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status)
Read a received packet from the buffer and return the raw bytes reported by the chip.
void dump_init_device_errors_(const char *tag, DeviceErrorFormatter format) const
Log the init-time device errors under tag, or nothing when there were none.
int16_t raw_rssi_to_dbm_(uint8_t raw) const
Convert a chip's raw RSSI byte to an antenna-referred dBm reading.
virtual uint32_t rx_done_bit() const =0
void rearm_rx_after_tx_()
Minimal path back into RX immediately after a transmission — see the definition for why this is delib...
SoftPhyDriverBase(InternalGPIOPin *rst_pin, InternalGPIOPin *busy_pin, uint32_t busy_timeout_ms, uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
bool is_sync_detected() override
Check if sync word has been detected (while in RX).
TxResult send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override
Send a packet using the specified carrier frequency and preamble settings.
virtual uint32_t preamble_detected_bit() const =0
static void build_gfsk_packet_params(const SoftPhyPacketParams &p, uint8_t out[GFSK_PACKET_PARAMS_LEN])
Fill the nine-byte GFSK SetPacketParams payload shared by both chips.
virtual void configure_buffer_base()
Hook run as part of reset_rx_state_, before re-entering RX.
virtual void set_rx_packet_params()=0
Configure RX-specific packet parameters (preamble detector length, fixed probe length).
uint32_t tx_air_time_us(uint8_t len, const RadioTxConfig &cfg) const override
Transmit time: the preamble, sync word and UART-coded frame at the protocol line rate.
virtual uint32_t sync_word_valid_bit() const =0
virtual void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len)=0
Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,...
virtual void clear_irq_status(uint32_t irq_mask)=0
Clear IRQ status bits.
void set_post_tx_settle_us_(uint16_t delay_us)
Set the delay between TX completion and re-entering RX.
uint16_t init_device_errors_
Device-error word a driver reads at the end of configure_radio_(), before it clears the chip's regist...
virtual void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len)=0
Configure TX packet parameters for one outgoing UART-encoded frame.
void reset_rx_state_(bool force_standby=true)
Reset RX state machine and buffer. Optionally force standby first.
virtual uint32_t tx_done_bit() const =0
virtual uint8_t read_rssi_raw_byte()=0
Read the single raw RSSI byte (chip-specific opcode); formula is shared, see read_rssi.
int16_t read_rssi() override
Read instantaneous RSSI (in dBm) on the current channel, while in RX mode.
virtual void before_tx_arm()
Hook run immediately before every SetTx.
virtual void invalidate_stale_rx_content_after_tx()
Hook run from rearm_rx_after_tx_, after a transmission and before re-entering RX.
InternalGPIOPin * busy_pin_
BUSY line, read directly by both concrete drivers' own dump_debug() in addition to wait_busy_,...
uint16_t response_preamble() const override
Preamble for response/continuation frames — shared storage, see the concrete drivers' constructors/tu...
static constexpr uint8_t GFSK_PACKET_PARAMS_LEN
Byte count of the GFSK SetPacketParams payload — identical on both software-PHY chips.
virtual uint32_t idle_rx_completion_budget_ms() const
Blocking budget for the idle-path length-driven receive, in milliseconds (issue #81).
virtual int8_t front_end_rx_gain_db() const
Gain (dB) of the board's external receive front end that the chip's RSSI includes.
uint16_t default_start_preamble() const override
Default preamble for a directed start frame, when the user has not set normal_start_preamble in YAML.
virtual void write_tx_buffer(const uint8_t *data, uint8_t len)=0
Write the UART-encoded TX payload into the chip's TX buffer.
void record_init_device_errors_(const char *tag, const char *chip_label, uint16_t errors, DeviceErrorFormatter format)
Keep the device-error word read at the end of configure_radio_() and warn if it is non-zero.
void set_response_preamble_(uint16_t preamble)
Set the preamble length used for response/continuation frames within an exchange.
virtual int16_t early_rx_read_offset() const
Data-buffer offset an in-flight reception is being written to, or a negative value when this chip mus...
virtual uint32_t activity_irq_mask() const
IRQ bits that count as "activity" for the internal poll_until_activity_() helper and check_for_packet...
bool check_for_packet(RadioRxPacket &packet) override
Non-blocking check for a received packet.
bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override
Wait (blocking) for a packet with timeout.
virtual void start_tx()=0
Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,...
void change_frequency(uint32_t freq_hz) override
Change the carrier frequency using fast hop (no standby transition needed).
static constexpr uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN
Fixed raw-RX probe length.
constexpr uint32_t soft_phy_air_time_us(uint32_t raw_bytes)
On-air time in microseconds for raw_bytes bytes at the protocol's line rate.
constexpr uint32_t RX_HOP_HOLDOFF_US
Longest a frame arriving on the current channel may hold off an idle-path channel hop,...
static constexpr uint16_t SOFT_PHY_MAX_WIRE_FRAME_BYTES
Largest real IO-Homecontrol frame the fixed-length RX probe has to recover intact: a 1W CMD 0x30 "add...
static constexpr uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES
Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART cell (10 b...
static constexpr uint32_t SOFT_PHY_IDLE_RX_COMPLETION_BUDGET_MS
Blocking budget SoftPhyDriverBase::check_for_packet gives a length-driven receive,...
static constexpr uint32_t SOFT_PHY_CCA_SETTLE_US
Wait after retuning to the TX channel before the clear-channel RSSI reading.
uint32_t io868_tx_air_time_us(uint16_t preamble_len, uint8_t frame_len)
Time on air of one 868 MHz transmission: preamble, sync word, and the frame plus its CRC,...
static constexpr uint32_t SOFT_PHY_EARLY_MIN_WINDOW_MS
Smallest receive window a length-driven receive will be attempted in, in milliseconds.
void format_device_error_bits(uint16_t errors, const DeviceErrorBit *bits, size_t bit_count, char *buf, size_t buf_size)
Expand a device-error word into a human-readable NAME|NAME|... string.
static constexpr uint8_t FRAME_CRC_SIZE
Size of the on-air CRC-CCITT trailer appended after every frame (declared bytes, plus the out-of-leng...
Definition proto_sizes.h:55
static constexpr size_t DEVICE_ERROR_STR_SIZE
Buffer size that always fits format_device_error_bits()'s longest output for either chip (every named...
static constexpr uint8_t SOFT_PHY_EARLY_READ_MARGIN_BYTES
Air-time margin added before every mid-reception buffer read, in raw bytes.
static constexpr uint16_t SOFT_PHY_MAX_WIRE_FRAME_RAW_BYTES
Raw on-air bytes that frame occupies once its 2-byte CRC is appended and the whole thing is UART-pack...
static constexpr uint32_t SOFT_PHY_EARLY_POLL_US
Poll interval while waiting out a frame's remaining air time, in microseconds.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
static constexpr uint32_t SOFT_PHY_ALL_IRQ_BITS
Sentinel meaning "every IRQ bit counts as activity" — the default for SoftPhyDriverBase::activity_irq...
void(*)(uint16_t errors, char *buf, size_t buf_size) DeviceErrorFormatter
A chip's own decoder, as passed to SoftPhyDriverBase::dump_init_device_errors_().
Radio abstraction layer for IO-Homecontrol.
Software PHY for radios without IoHomeOn hardware framing.
One named bit of a chip's device-error word.
const char * name
Log name, e.g. PLL_LOCK_ERR.
uint16_t mask
The bit (or bits) of the error word this row names.
Raw packet received from the radio.
Configuration for transmitting a packet: carrier frequency, preamble length, wake-up level and clear-...
uint16_t preamble_len
Preamble length in symbol periods (bytes).
The GFSK SetPacketParams fields both software-PHY chips program identically.
uint16_t preamble_bytes
Byte-denominated, like every other preamble value in this codebase.
What send_packet() did with a frame.