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