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 <cstdint>
28
29namespace esphome {
30namespace home_io_control {
31
32/// Fixed raw-RX probe length: chosen from captures of 23-25 byte protocol frames after UART
33/// packing and CRC appending — the longest frame (25 bytes + 2 CRC) UART-packs to 34 raw bytes,
34/// so 48 bytes preserves complete traffic (with margin for leading noise before the frame start)
35/// without relying on either chip's variable-length engine. This is a protocol-frame-size
36/// property, not a chip quirk, so both drivers share one value — used here for the raw-probe
37/// threshold in @ref SoftPhyDriverBase::read_rx_packet and by each driver's own
38/// `set_rx_packet_params()` for the configured RX payload length.
39static constexpr uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN = 48;
40
41/// Sentinel meaning "every IRQ bit counts as activity" — the default for @ref
42/// SoftPhyDriverBase::activity_irq_mask. Neither concrete driver uses it any more: both SX1262
43/// and LR1121 unmask PreambleDetected at the hardware level and override this to exclude it (see
44/// SX1262_IRQ_ACTIVITY_MASK / LR1121_IRQ_ACTIVITY_MASK). Kept as the base-class default for a
45/// hypothetical future driver that never unmasks PreambleDetected in the first place, where "any
46/// bit" is genuinely safe again.
47static constexpr uint32_t SOFT_PHY_ALL_IRQ_BITS = 0xFFFFFFFF;
48
49/// Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART
50/// cell (10 bits) at any of the probe's bit alignments (up to 9 bits of slack).
51static constexpr uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES = 3;
52
53/// Air-time margin added before every mid-reception buffer read, in raw bytes.
54///
55/// Covers the lag between a byte finishing on air and the chip having it in its data buffer, plus
56/// the granularity of the polled sync-word observation. Two byte-times is generous at this line
57/// rate and still leaves a length-driven receive far ahead of the fixed-length RX_DONE.
58static constexpr uint8_t SOFT_PHY_EARLY_READ_MARGIN_BYTES = 2;
59
60/// Poll interval while waiting out a frame's remaining air time, in microseconds.
61static constexpr uint32_t SOFT_PHY_EARLY_POLL_US = 100;
62
63/// Smallest receive window a length-driven receive will be attempted in, in milliseconds.
64///
65/// The longest possible frame (FRAME_MAX_SIZE + CRC, UART-packed) occupies ~9.4 ms of air time, so
66/// a caller with less than this left cannot finish one either way. Declining up front keeps the
67/// early path from spending a short window's whole budget on a receive it cannot complete.
68static constexpr uint32_t SOFT_PHY_EARLY_MIN_WINDOW_MS = 12;
69
70/// Blocking budget @ref SoftPhyDriverBase::check_for_packet gives a length-driven receive, in
71/// milliseconds — the `timeout_ms` passed to `try_early_completion_()` from the non-blocking
72/// idle-loop RX path (issue #81). The real bound on how long that call can block is the frame's
73/// own air time (~9.4 ms worst case, see @ref SOFT_PHY_EARLY_MIN_WINDOW_MS), not this number; this
74/// value only has to be large enough not to cut a genuine frame short. It is not itself a tight
75/// bound on loop() latency — the actual air time is what stays well inside a loop() tick
76/// (~16-30 ms) — and either way it is dwarfed by the 1-3 s a blocking exchange already costs
77/// `loop()`.
78static constexpr uint32_t SOFT_PHY_IDLE_RX_COMPLETION_BUDGET_MS = 20;
79
81 "a budget below the minimum window makes try_early_completion_() decline every "
82 "idle-path call, silently turning issue #81's fix into a no-op");
83
84/// Protocol line rate. The same 38400 bps every driver programs into its own bitrate register.
85static constexpr uint32_t SOFT_PHY_LINE_RATE_BPS = 38400;
86/// Microseconds in a second, for the air-time arithmetic below.
87static constexpr uint32_t SOFT_PHY_US_PER_SECOND = 1000000;
88
89/// @brief On-air time in microseconds for `raw_bytes` bytes at the protocol's line rate.
90///
91/// One byte is 8 / 38400 s = 208.333 µs. Computed as an integer division rounded *up*, so the
92/// result never falls short of a whole byte's air time and a caller that waits on it never reads
93/// the chip's buffer early. The numerator peaks around 360 million for the longest frame this is
94/// ever asked about, well inside uint32_t.
95constexpr uint32_t soft_phy_air_time_us(uint32_t raw_bytes) {
96 const uint32_t bit_periods = raw_bytes * BITS_PER_BYTE * SOFT_PHY_US_PER_SECOND;
97 return (bit_periods + SOFT_PHY_LINE_RATE_BPS - 1) / SOFT_PHY_LINE_RATE_BPS;
98}
99
100// RX_HOP_HOLDOFF_US (radio_interface.h) exists to outlast the fixed-length RX_DONE on the
101// software-PHY chips. Tied here, in the one header that can see both sides of the arithmetic,
102// so a change to either constant that breaks the relationship fails the build instead of
103// silently turning issue #81's gate back into the bug it fixes.
105 "the hop holdoff must outlast the fixed-length RX_DONE it exists to wait for — a "
106 "shorter bound lets the hop fire while the frame it is protecting is still on air, "
107 "silently turning issue #81's gate back into the bug");
108
109/// @brief Shared RX/TX driver flow for the software-PHY radios (SX1262, LR1121).
110/// @ingroup hioc_radio
112 public:
113 /// @param rst_pin Active-low hardware reset pin, forwarded to RadioDriver.
114 /// @param busy_pin BUSY line polled by @ref wait_busy_ before every SPI transaction — both
115 /// concrete drivers are opcode-based chips that require this, unlike the register-based
116 /// SX1276.
117 /// @param busy_timeout_ms How long @ref wait_busy_ waits for BUSY to drop before declaring the
118 /// chip failed. Chip-specific (SX1262: 10 ms: RC-oscillator timing; LR1121: 3000 ms, matched
119 /// to RadioLib's post-reset boot-ROM wait) — this class has no opinion on the value, only on
120 /// where it's stored and how it's used.
121 /// @param default_response_preamble Chip-specific default for @ref response_preamble (each
122 /// concrete driver passes its own validated constant — this class has no opinion on the
123 /// value, only on where it's stored).
124 /// @param default_post_tx_settle_us Chip-specific default post-TX settling delay, same rationale.
125 SoftPhyDriverBase(InternalGPIOPin *rst_pin, InternalGPIOPin *busy_pin, uint32_t busy_timeout_ms,
126 uint16_t default_response_preamble, uint16_t default_post_tx_settle_us)
127 : RadioDriver(rst_pin),
128 busy_pin_(busy_pin),
129 response_preamble_(default_response_preamble),
130 post_tx_settle_us_(default_post_tx_settle_us),
131 busy_timeout_ms_(busy_timeout_ms) {}
132
133 /// @copydoc RadioDriver::send_packet
134 bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override;
135 /// @copydoc RadioDriver::wait_for_packet
136 bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override;
137 /// @copydoc RadioDriver::check_for_packet
138 bool check_for_packet(RadioRxPacket &packet) override;
139 /// @copydoc RadioDriver::change_frequency
140 void change_frequency(uint32_t freq_hz) override;
141 /// @copydoc RadioDriver::read_rssi
142 ///
143 /// Same formula on both chips (`-(int16_t) raw / 2`); only the opcode used to read the single
144 /// raw byte differs, via @ref read_rssi_raw_byte.
145 int16_t read_rssi() override;
146 /// @copydoc RadioDriver::is_sync_detected
147 bool is_sync_detected() override;
148 /// @copydoc RadioDriver::is_preamble_detected
149 ///
150 /// Consults the private `preamble_latched_at_timeout_` flag first; see that member's declaration
151 /// for why.
152 bool is_preamble_detected() override;
153 /// @brief Preamble for response/continuation frames — shared storage, see the concrete
154 /// drivers' constructors/tuning defaults for the chip-specific rationale and value.
155 [[nodiscard]] uint16_t response_preamble() const override { return this->response_preamble_; }
156 /// @copydoc RadioDriver::is_failed
157 ///
158 /// Shared storage: both concrete drivers only ever set @ref failed_ from within their own
159 /// SPI/opcode helpers (a BUSY timeout, a device-identity mismatch, ...), so there is nothing
160 /// chip-specific left in the accessor itself.
161 [[nodiscard]] bool is_failed() const override { return this->failed_; }
162
163 protected:
164 // --- Tuning helpers shared by both drivers (values/defaults stay chip-specific) ---
165 /// Set the preamble length used for response/continuation frames within an exchange.
166 void set_response_preamble_(uint16_t preamble) { this->response_preamble_ = preamble; }
167 /// Set the delay between TX completion and re-entering RX.
168 void set_post_tx_settle_us_(uint16_t delay_us) { this->post_tx_settle_us_ = delay_us; }
169 /// @brief Wait until @ref busy_pin_ reads low, feeding the watchdog while polling.
170 ///
171 /// Shared verbatim between SX1262 and LR1121 — the two chips differ only in how long they're
172 /// willing to wait (`busy_timeout_ms_`). A call short-circuits once the driver has latched
173 /// `failed_`: without that guard, every remaining `configure_radio_()` step after the first
174 /// failure would re-run the full timeout, turning one bad boot into tens of seconds of hang at
175 /// the LR1121's 3000 ms timeout (harmless but still pointless at the SX1262's 10 ms one).
176 void wait_busy_();
177 /// Set on a BUSY timeout or a chip-identity check failing; see @ref is_failed.
178 bool failed_{false};
179 /// BUSY line, read directly by both concrete drivers' own `dump_debug()` in addition to
180 /// @ref wait_busy_, so this stays protected rather than folding entirely into the private
181 /// wait-loop state below.
182 InternalGPIOPin *busy_pin_;
183
184 // --- Shared RX/TX orchestration (moved verbatim from RadioSX1262/RadioLR1121) ---
185 /// Read a received packet from the buffer and return the raw bytes reported by the chip.
186 /// Virtual to allow test doubles (both concrete drivers' tests override this).
187 virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status);
188 /// Reset RX state machine and buffer. Optionally force standby first.
189 void reset_rx_state_(bool force_standby = true);
190 /// Minimal path back into RX immediately after a transmission — see the definition for why this
191 /// is deliberately not @ref reset_rx_state_.
192 void rearm_rx_after_tx_();
193
194 /// @brief The GFSK SetPacketParams fields both software-PHY chips program identically.
195 ///
196 /// Both drivers build the same nine-byte SetPacketParams payload in the same order; only the
197 /// preamble-detector length and the sync-word selector are chip constants, and only the opcode
198 /// and transport differ. Everything else — the field order and the byte/bit preamble conversion
199 /// that the `63e2502` fix had to patch in two places — lives in @ref build_gfsk_packet_params.
201 uint16_t preamble_bytes; ///< Byte-denominated, like every other preamble value in this codebase.
202 uint8_t preamble_detector; ///< Chip-specific detector-length selector.
203 uint8_t sync_word_param; ///< Chip-specific 24-bit sync-word selector.
204 uint8_t packet_type; ///< Known-length / fixed-length selector.
205 uint8_t payload_len; ///< Configured payload length.
206 uint8_t crc_type; ///< Chip-specific CRC-mode selector.
207 };
208 /// Byte count of the GFSK SetPacketParams payload — identical on both software-PHY chips.
209 static constexpr uint8_t GFSK_PACKET_PARAMS_LEN = 9;
210 /// @brief Fill the nine-byte GFSK SetPacketParams payload shared by both chips.
211 ///
212 /// Owns the field order and the ×8 preamble conversion: the SetPacketParams preamble field is
213 /// bit-denominated on both chips (Semtech names it `preamble_len_in_bits` /
214 /// `pbl_len_in_bit`), but every caller in this codebase passes a byte count. Address comparison
215 /// and whitening are always off.
216 /// @param p The chip-independent field values (byte-denominated preamble, chip constants).
217 /// @param out Buffer for exactly @ref GFSK_PACKET_PARAMS_LEN bytes.
218 ///
219 /// Named without the trailing `_` the file's other protected helpers carry: it uses no instance
220 /// state, so clang-tidy's `readability-convert-member-functions-to-static` wants it `static`, and
221 /// `.clang-tidy`'s `ClassMethodCase = lower_case` then rejects a trailing `_` on a `static`
222 /// method. Leaving it `static` and dropping the underscore keeps clang-tidy quiet without a
223 /// suppression.
224 static void build_gfsk_packet_params(const SoftPhyPacketParams &p, uint8_t out[GFSK_PACKET_PARAMS_LEN]);
225
226 /// @brief Read the raw IRQ status word from the radio.
227 /// Virtual to allow test doubles (both concrete drivers' tests override this).
228 virtual uint32_t read_irq_status_raw() = 0;
229 /// Clear IRQ status bits.
230 /// @param irq_mask Bitmask of IRQs to clear (each driver narrows to its own IRQ word width).
231 virtual void clear_irq_status(uint32_t irq_mask) = 0;
232
233 /// @name Chip-specific IRQ bit values
234 /// Each driver's own IRQ bit constants, exposed as accessors so the shared RX/TX orchestration
235 /// never needs to name a chip-specific constant directly.
236 ///@{
237 [[nodiscard]] virtual uint32_t sync_word_valid_bit() const = 0;
238 [[nodiscard]] virtual uint32_t rx_done_bit() const = 0;
239 [[nodiscard]] virtual uint32_t tx_done_bit() const = 0;
240 [[nodiscard]] virtual uint32_t preamble_detected_bit() const = 0;
241 ///@}
242
243 /// @brief IRQ bits that count as "activity" for the internal `poll_until_activity_()` helper
244 /// and @ref check_for_packet.
245 ///
246 /// Default is "any bit" — safe only for a driver whose `SetDioIrqParams`-equivalent mask never
247 /// includes `PreambleDetected` in the first place, so a preamble-only reading can never reach
248 /// this check. Neither current driver qualifies: both SX1262 and LR1121 unmask
249 /// `PreambleDetected` (each for its own reason) and override this to exclude it — a
250 /// preamble-only reading means a frame may still be arriving, and treating it as terminal
251 /// activity would tear down RX mid-reception.
252 [[nodiscard]] virtual uint32_t activity_irq_mask() const { return SOFT_PHY_ALL_IRQ_BITS; }
253
254 /// @brief Data-buffer offset an in-flight reception is being written to, or a negative value
255 /// when this chip must not be read before RX_DONE.
256 ///
257 /// Neither chip's RX_DONE marks the end of the *frame*: with no hardware framing, RX runs in
258 /// fixed-length mode at @ref SOFT_PHY_RX_PROBE_PACKET_LEN, so RX_DONE arrives a fixed ~10 ms
259 /// after the sync word no matter how short the frame actually was. That delay lands squarely on
260 /// the protocol's tightest turnaround — the hub's reply to a device's challenge — so a driver
261 /// that can read its buffer while reception is still running opts in here and the shared flow
262 /// finishes on the frame's own air time instead (see `try_early_completion_`).
263 ///
264 /// Default is -1: wait for RX_DONE exactly as before. SX1262 overrides it with the RX base
265 /// address it programs in configure_buffer_base(), which is where a single in-flight packet
266 /// always starts. LR1121 also opts in, at the base of its one shared TX/RX buffer — see
267 /// RadioLR1121::early_rx_read_offset for why a wrong guess there is safe only in combination
268 /// with @ref invalidate_stale_rx_content_after_tx.
269 [[nodiscard]] virtual int16_t early_rx_read_offset() const { return -1; }
270
271 /// @brief Blocking budget for the idle-path length-driven receive, in milliseconds (issue #81).
272 ///
273 /// Virtual only so tests can widen it. The host clock stubs advance `millis()`/`micros()` by one
274 /// unit per call (`tests/include/esphome/core/hal.h`), so a frame's few thousand microseconds of
275 /// air time also burns a few thousand fake milliseconds — any production-sized budget expires
276 /// inside `wait_for_air_time()`'s first stage and the whole path becomes untestable. The existing
277 /// blocking-path early-completion tests dodge this by passing `wait_for_packet()` a 20000 ms
278 /// timeout; this path has no caller-supplied timeout to widen, so the seam has to live here.
279 [[nodiscard]] virtual uint32_t idle_rx_completion_budget_ms() const { return SOFT_PHY_IDLE_RX_COMPLETION_BUDGET_MS; }
280
281 /// Set RF frequency via the chip's own frequency register/opcode encoding, and update
282 /// `current_freq_`. Called from both @ref change_frequency and the shared `send_packet()`.
283 virtual void set_frequency_register(uint32_t freq_hz) = 0;
284 /// Configure RX-specific packet parameters (preamble detector length, fixed probe length).
285 virtual void set_rx_packet_params() = 0;
286 /// Configure TX packet parameters for one outgoing UART-encoded frame.
287 /// @param preamble_len Preamble length in symbols, from the caller's RadioTxConfig.
288 /// @param payload_len UART-encoded payload length in bytes.
289 virtual void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len) = 0;
290 /// Read the single raw RSSI byte (chip-specific opcode); formula is shared, see @ref read_rssi.
291 virtual uint8_t read_rssi_raw_byte() = 0;
292 /// Write the UART-encoded TX payload into the chip's TX buffer.
293 virtual void write_tx_buffer(const uint8_t *data, uint8_t len) = 0;
294 /// Read the chip-reported RX length and buffer offset (raw, before any clamping).
295 virtual void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset) = 0;
296 /// Read `len` bytes from the RX buffer starting at `offset`.
297 virtual void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len) = 0;
298 /// Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,
299 /// differing only in opcode/transport, so this stays a thin chip-specific wrapper.
300 virtual void start_tx() = 0;
301 /// Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,
302 /// and IRQ-word-width narrowing all differ per chip).
303 virtual void fill_capture_info(bool blocking_wait, uint32_t irq_status, uint8_t rx_offset, uint8_t reported_len,
304 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) = 0;
305
306 /// @brief Hook run immediately before every `SetTx`. No-op by default; LR1121 overrides this to
307 /// apply its high-ACP TX-quality workaround, which Semtech's own reference applies unconditionally
308 /// before every SetRx/SetTx.
309 virtual void before_tx_arm() {}
310 /// @brief Hook run as part of @ref reset_rx_state_, before re-entering RX. No-op by default;
311 /// SX1262 overrides this to (re-)write its buffer base address, which LR1121 doesn't need.
312 virtual void configure_buffer_base() {}
313 /// @brief Hook run from @ref rearm_rx_after_tx_, after a transmission and before re-entering RX.
314 /// No-op by default.
315 ///
316 /// SX1262 has a real address split written once at init (@ref configure_buffer_base): TX always
317 /// builds at its own base, RX always lands at a different one, so nothing a transmission wrote
318 /// can ever appear where a length-driven receive (@ref early_rx_read_offset) later reads from.
319 /// LR1121 has no such split — one shared 256-byte buffer for both directions, and `WriteBuffer`
320 /// always starts from the buffer base (see `write_buffer_`'s doc comment) — so after every LR1121
321 /// transmission, the offset a length-driven receive will read from holds a real, CRC-valid,
322 /// UART-encoded copy of the hub's own last-sent frame. That is not noise: if
323 /// `early_rx_read_offset()`'s offset guess ever turns out to be wrong, reading that residue back
324 /// would pass every stage of `try_early_completion_()` and hand back the hub's own transmission as
325 /// a phantom received packet — worse than doing nothing, since `check_for_packet()` would then
326 /// tear down and re-arm RX (issue #81's `force_standby` path) over whatever real reception was
327 /// actually in progress. RadioLR1121 overrides this to overwrite that offset with a few
328 /// non-frame-shaped bytes after every TX, so a wrong offset guess degrades back to reading genuine
329 /// garbage — which correctly fails the length-driven receive's stage 1 or stage 3 — instead of a
330 /// valid-looking phantom frame.
332
333 private:
334 // === wait_for_packet/check_for_packet state-machine helpers (private) ===
335 /// Poll for first *terminal* radio activity (IRQ pin or an IRQ status bit within
336 /// @ref activity_irq_mask) within timeout.
337 bool poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq);
338 /// Resolve the SYNC_WORD_VALID → RX_DONE race condition common to both chips, and — on a chip
339 /// that opts into @ref early_rx_read_offset — give the length-driven receive its chance first.
340 /// @param early_completed Set when a whole CRC-valid frame was recovered without waiting for
341 /// RX_DONE; `packet` is then already populated and the caller is done.
342 bool resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq, RadioRxPacket &packet,
343 bool &early_completed);
344 /// Finish a reception on the frame's own air time rather than on the chip's fixed-length
345 /// RX_DONE. Returns true only when a CRC-valid frame was recovered.
346 bool try_early_completion_(RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status, uint32_t start_ms,
347 uint32_t timeout_ms);
348 /// Finalize receive: read the packet if RX_DONE is set, otherwise record failure.
349 bool finalize_receive_(RadioRxPacket &packet, uint32_t irq);
350
351 uint16_t response_preamble_;
352 uint16_t post_tx_settle_us_;
353 uint32_t busy_timeout_ms_;
354
355 /// @brief Whether `PreambleDetected` was latched at the exact moment a dwell's
356 /// `poll_until_activity_()` gave up waiting, captured before `reset_rx_state_()` wipes the IRQ
357 /// word out from under it.
358 ///
359 /// `PreambleDetected` is deliberately excluded from @ref activity_irq_mask (a bare preamble must
360 /// not look like terminal activity), and that exclusion is exactly why a dwell *can* time out
361 /// while the bit is still latched — the timeout branch has no way to notice, since
362 /// activity_irq_mask() never told it to look. What happens next is a separate problem:
363 /// `reset_rx_state_()` runs immediately afterward and clears the whole IRQ word, so a live
364 /// re-read a few tens of microseconds later (in `listen()`'s `preamble_or_sync_incoming()` guard)
365 /// would need the chip to re-observe a fresh preamble from scratch — which takes a little longer
366 /// on a chip with a longer preamble-detector length — and it almost never has done so by the time
367 /// the guard asks. This flag is the only way that information survives the reset long enough for
368 /// the guard to read it.
369 ///
370 /// `is_sync_detected()` needs no equivalent snapshot — not because no path clears sync before a
371 /// timeout (`resolve_sync_race_()` does exactly that: it clears `SYNC_WORD_VALID` up front once a
372 /// reception is underway, and can still time out later waiting for RX_DONE), but because that
373 /// path never calls `reset_rx_state_()` on its way out. Whatever `PreambleDetected` state existed
374 /// going into it survives untouched, which is all the guard actually depends on there.
375 ///
376 /// Lifetime is bounded to a single `poll_until_activity_()` call so a stale detection can never
377 /// suppress a later, unrelated hop: every exit from that function's loop sets this flag from
378 /// scratch (true only on the timeout branch, and only when the bit was actually set on that exact
379 /// read), and @ref is_preamble_detected consumes (clears) it the moment it is read as true.
380 bool preamble_latched_at_timeout_{false};
381};
382
383} // namespace home_io_control
384} // 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_.
bool is_failed() const override
Returns true if the radio failed to initialize or encountered a fatal error.
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).
bool is_preamble_detected() override
Check if preamble has been detected (while in RX).
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.
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.
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
bool failed_
Set on a BUSY timeout or a chip-identity check failing; see is_failed.
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 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 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 BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_sizes.h:27
static constexpr uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN
Fixed raw-RX probe length: chosen from captures of 23-25 byte protocol frames after UART packing and ...
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 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.
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 uint32_t SOFT_PHY_EARLY_POLL_US
Poll interval while waiting out a frame's remaining air time, in microseconds.
static constexpr uint32_t SOFT_PHY_ALL_IRQ_BITS
Sentinel meaning "every IRQ bit counts as activity" — the default for SoftPhyDriverBase::activity_irq...
Radio abstraction layer for IO-Homecontrol.
Software PHY for radios without IoHomeOn hardware framing.
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.