Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_interface.h
Go to the documentation of this file.
1#pragma once
2
3/// @file radio_interface.h
4/// @brief Radio abstraction layer for IO-Homecontrol.
5/// @ingroup hioc_radio
6///
7/// Defines the SpiAccess interface for SPI bus access and the RadioDriver abstract
8/// class that encapsulates all chip-specific radio operations. This allows the
9/// protocol layer to work with different radio chips (SX1276, SX1262, etc.)
10/// without knowing the hardware details.
11
12#include "proto_frame.h"
13#include "proto_timing.h"
14#include "tuning_config.h"
15#include <atomic>
16#include <cstdint>
17#include <iterator>
18#include <optional>
19#include "esphome/core/hal.h"
20
21namespace esphome {
22namespace home_io_control {
23
24inline constexpr uint8_t RADIO_PACKET_BUFFER_SIZE =
25 64; ///< Scratch buffer size for raw radio packets and recovered frames.
26
27/// @brief Longest a frame arriving on the current channel may hold off an idle-path channel hop,
28/// in microseconds.
29///
30/// Sized to outlast the slowest thing a hop could destroy. On the software-PHY chips that is the
31/// fixed-length RX_DONE, which lands 48 raw bytes = 10.0 ms after the sync word
32/// (SOFT_PHY_RX_PROBE_PACKET_LEN at 38400 bps; a static_assert in radio_soft_phy_driver_base.h
33/// ties this constant to that arithmetic, since neither header can see the other's constants).
34/// On the SX1276 it is the frame's own air time, at most ~9.4 ms for the longest possible frame.
35/// 12 ms covers both with margin for poll granularity.
36///
37/// It is a *bound*, not a target: every mechanism that sets the holdoff is expected to clear it
38/// early, and the bound exists only so that a sync detection with no frame behind it — noise, a
39/// truncated burst, a peer that gave up — cannot wedge channel hopping permanently.
40inline constexpr uint32_t RX_HOP_HOLDOFF_US = 12000;
41
42/// Sentinel `tcxo_voltage` code (YAML `none`) for a board with a bare crystal and no TCXO. The
43/// SX1262 and LR1121 drivers skip all DIO3/TCXO programming when they see it and calibrate off
44/// the plain crystal. Cannot collide with the real 0-based voltage codes (0x00-0x07).
45inline constexpr uint8_t TCXO_VOLTAGE_NONE = 0xFF;
46
47/// @brief Human-readable voltage for a `tcxo_voltage` code, for the config dump.
48/// @param code 0-based chip voltage code (0x00 = 1.6 V .. 0x07 = 3.3 V) or @ref TCXO_VOLTAGE_NONE.
49/// @return A static string; `"unknown"` for a code outside the table.
50///
51/// Mirrors TCXO_VOLTAGE_OPTIONS in `hub_validators.py`, the YAML-side source of truth for the codes.
52[[nodiscard]] inline const char *tcxo_voltage_label(uint8_t code) {
53 // Index = the 0-based chip voltage code.
54 static constexpr const char *LABELS[] = {"1.6 V", "1.7 V", "1.8 V", "2.2 V", "2.4 V", "2.7 V", "3.0 V", "3.3 V"};
55 if (code == TCXO_VOLTAGE_NONE)
56 return "none (bare crystal)";
57 return code < std::size(LABELS) ? LABELS[code] : "unknown";
58}
59
60/// @brief Which RF front-end module (if any) sits between the SX1262 and the antenna.
61///
62/// Selects two things: which of vfem_pin/fem_en_pin/fem_pa_pin the SX1262 driver's config-time
63/// validation requires present (see FEM_REQUIRED_PINS, components/home_io_control/hub_validators.py),
64/// and which level fem_pa_pin is driven to for the duration of a transmission
65/// (fem_tx_active_level_(), radio_sx1262.cpp). It never supplies a GPIO number -- every board
66/// package spells its own pins out explicitly, the same as every other radio pin in this schema.
67/// GC1109 and KCT8103L share one control polarity and one code path with nothing distinguishing
68/// them but their TX-gain estimate; XY16P35's LNA-control pin is active-HIGH-on-*receive*
69/// (the inverse sense), the one place this enum's value actually changes what gets written to a
70/// pin, via a single polarity flip rather than a structural branch. `NONE` is the default: no
71/// FEM behaviour is applied, and a configured fem_pa_pin (if any) is simply strapped HIGH once at
72/// init and never touched again -- the shape every raw-pin config on a non-FEM board already
73/// used before this enum existed.
74enum class FemProfile : uint8_t {
75 NONE = 0, ///< No FEM, or a board with the FEM pins wired but no behaviour profile set --
76 ///< see the NONE-specific note in fem_set_tx_mode_()'s doc comment.
77 GC1109, ///< Heltec WiFi LoRa 32 V4.2 -- Geo-chip GC1109. Mode pin active-HIGH during TX.
78 KCT8103L, ///< Heltec WiFi LoRa 32 V4.3 / V4 R8 -- Kangxi KCT8103L. Mode pin active-HIGH during TX.
79 XY16P35, ///< LilyGO T-Beam 1W SX1262 (868 MHz). Names the RF module, not a bare FEM IC --
80 ///< no single chip inside it is independently identifiable as "the FEM chip" from
81 ///< anything published. Mode pin active-LOW during TX -- the one profile with
82 ///< inverted polarity; see fem_tx_active_level_().
83};
84
85/// Interface for SPI bus access.
86/// The ESPHome component implements this by delegating to its SPIDevice methods,
87/// allowing radio drivers to perform SPI transactions without depending on the
88/// ESPHome SPI framework directly.
89/// @ingroup hioc_radio
90class SpiAccess {
91 public:
92 virtual ~SpiAccess() = default;
93 /// Enable the SPI bus (assert CS low).
94 virtual void spi_enable() = 0;
95 /// Disable the SPI bus (deassert CS).
96 virtual void spi_disable() = 0;
97 /// Transfer one byte full‑duplex (MOSI→MISO).
98 /// @param data Byte to send.
99 /// @return Byte received from MISO.
100 virtual uint8_t spi_transfer(uint8_t data) = 0;
101 /// Write one byte (MOSI only, MISO ignored).
102 /// @param data Byte to send.
103 virtual void spi_write(uint8_t data) = 0;
104 /// Read one byte (MISO only, MOSI driven with 0).
105 /// @return Byte received.
106 virtual uint8_t spi_read() = 0;
107};
108
109/// Configuration for transmitting a packet: carrier frequency, preamble length, wake-up level and
110/// clear-channel check.
112 uint32_t freq_hz{FREQ_CH2}; ///< Carrier frequency in Hz.
113 uint16_t preamble_len{SHORT_PREAMBLE}; ///< Preamble length in symbol periods (bytes).
114 /// Wake-up level the receiver needs (tx_wake_for()). A driver whose receivers are woken by the
115 /// preamble length alone may ignore it: `preamble_len` already carries the same decision.
117 /// If set, send only if the energy on `freq_hz`, measured right before the first transmitted
118 /// symbol, is below this level (dBm); otherwise send_packet() reports CHANNEL_BUSY and sends
119 /// nothing. Unset: send without a check.
120 std::optional<int16_t> cca_threshold_dbm;
121};
122
123/// What send_packet() did with a frame.
124struct TxResult {
125 enum class Status : uint8_t {
126 SENT, ///< The frame went out.
127 CHANNEL_BUSY, ///< The clear-channel check found the channel busy; nothing was sent.
128 FAILED, ///< The radio could not send (invalid frame, TX timeout).
129 };
131 int16_t cca_level_dbm{0}; ///< The level the clear-channel check measured; set with CHANNEL_BUSY.
132
133 [[nodiscard]] bool sent() const { return this->status == Status::SENT; }
134 [[nodiscard]] static TxResult ok() { return {Status::SENT, 0}; }
135 [[nodiscard]] static TxResult failed() { return {Status::FAILED, 0}; }
136 [[nodiscard]] static TxResult channel_busy(int16_t level_dbm) { return {Status::CHANNEL_BUSY, level_dbm}; }
137};
138
139/// Raw packet received from the radio.
141 uint32_t freq_hz{0}; ///< Frequency the packet was received on (Hz).
142 uint8_t len{0}; ///< Length of packet in bytes.
143 uint8_t data[RADIO_PACKET_BUFFER_SIZE]{}; ///< Raw packet data buffer.
144};
145
146/// Diagnostic capture from a radio operation.
147///
148/// Populated after every wait_for_packet / check_for_packet. Contains both the
149/// raw bytes reported by the chip (before any protocol-specific recovery) and
150/// the parsed frame handed to the protocol layer.
152 bool valid{false}; ///< True if capture is valid.
153 bool blocking_wait{false}; ///< True if captured during a blocking wait.
154 bool rx_done{false}; ///< True if RxDone IRQ fired.
155 bool crc_error{false}; ///< True if a CRC error was detected. Chip-dependent: some drivers cannot report
156 ///< CRC failures — see the concrete driver's capture documentation.
157 uint32_t timestamp_ms{0}; ///< Timestamp of capture (millis).
158 uint32_t freq_hz{0}; ///< RF frequency of capture (Hz).
159 int16_t rssi_dbm{0}; ///< Received signal strength (dBm).
160 uint16_t irq_status{0}; ///< Raw IRQ status register value.
161 uint8_t irq_flags1{0}; ///< IRQ flags group 1 (chip-specific).
162 uint8_t irq_flags2{0}; ///< IRQ flags group 2 (chip-specific, includes CRC flag).
163 uint8_t packet_status{0}; ///< Packet status byte (chip-specific).
164 uint8_t rx_offset{0}; ///< RX buffer offset where the frame starts (0 for chips without offset reporting).
165 uint8_t reported_len{0}; ///< Length reported by the radio chip.
166 // raw[] preserves the chip-reported bytes before any driver-specific recovery, while
167 // frame[] stores the bytes handed to parse(). Keeping both makes it possible to compare
168 // one driver's recovery output against reference captures from another.
169 uint8_t raw_len{0}; ///< Number of valid bytes in raw[].
170 uint8_t frame_len{0}; ///< Number of valid bytes in frame[].
171 uint8_t raw[RADIO_PACKET_BUFFER_SIZE]{}; ///< Raw radio buffer bytes.
172 uint8_t frame[RADIO_PACKET_BUFFER_SIZE]{}; ///< Parsed protocol frame bytes.
173};
174
175/// Abstract radio driver for IO-Homecontrol.
176///
177/// Encapsulates all chip-specific operations: initialization, packet TX/RX,
178/// frequency control, and mode switching. Concrete implementations (RadioSX1276,
179/// RadioSX1262, RadioLR1121) handle the register-level details for each chip.
180/// @ingroup hioc_radio
182 public:
183 explicit RadioDriver(InternalGPIOPin *rst_pin = nullptr) : rst_pin_(rst_pin) {}
184 virtual ~RadioDriver() = default;
185
186 /// Initialize the radio hardware. Returns true on success.
187 virtual bool init() = 0;
188
189 /// Send a packet using the specified carrier frequency and preamble settings.
190 /// The driver is responsible for appending the protocol CRC on the air
191 /// (in hardware or software, depending on the chip). With `tx_config.cca_threshold_dbm` set, the
192 /// driver first measures the energy on `tx_config.freq_hz` itself — retuning there if the
193 /// receiver is elsewhere — and reports CHANNEL_BUSY instead of sending when it is at or above
194 /// the threshold.
195 /// @return SENT, CHANNEL_BUSY (with the measured level) or FAILED.
196 virtual TxResult send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) = 0;
197
198 /// Wait (blocking) for a packet with timeout. Returns true if a packet was received.
199 /// Contract:
200 /// - Clears last_capture_ and output packet before waiting.
201 /// - On success: populates packet and last_capture_, returns true.
202 /// - On timeout/failure: may populate last_capture_ for diagnostics, returns false.
203 /// - Radio remains in RX mode on return (regardless of outcome).
204 virtual bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) = 0;
205
206 /// Non-blocking check for a received packet. Called from loop().
207 /// Returns true if a packet was read into packet.
208 /// Contract:
209 /// - Returns false immediately if no DIO interrupt has fired.
210 /// - On success: populates packet and last_capture_, returns true.
211 /// - On failure: may populate last_capture_ for diagnostics, returns false.
212 virtual bool check_for_packet(RadioRxPacket &packet) = 0;
213
214 /// Read instantaneous RSSI (in dBm) on the current channel, while in RX mode. Drivers take their
215 /// clear-channel reading inside send_packet() with it; otherwise it is for diagnostics.
216 /// @return RSSI in dBm (negative value).
217 virtual int16_t read_rssi() = 0;
218
219 /// @brief Check if sync word has been detected (while in RX).
220 /// Used to gate frequency hopping — prevents hopping away mid-frame.
221 virtual bool is_sync_detected() = 0;
222
223 /// @brief Check if preamble has been detected (while in RX).
224 /// Used together with sync detection to gate frequency hopping.
225 virtual bool is_preamble_detected() = 0;
226
227 /// @brief Return the preamble length for response/continuation frames.
228 ///
229 /// Callers use this instead of hardcoding SHORT_PREAMBLE for any frame sent as
230 /// an immediate reply within an exchange (challenge responses, key transfers,
231 /// and any future non-START continuation frames — i.e. tight RX→TX turnaround).
232 ///
233 /// The default is the protocol's standard SHORT_PREAMBLE. Drivers whose TX
234 /// waveform gives the peer device less synchronization margin override this
235 /// with a longer preamble (see the concrete drivers for the chip-specific
236 /// rationale).
237 ///
238 /// @return Preamble length in bytes.
239 [[nodiscard]] virtual uint16_t response_preamble() const { return SHORT_PREAMBLE; }
240
241 /// @brief Default preamble for a directed start frame, when the user has not set
242 /// `normal_start_preamble` in YAML.
243 ///
244 /// A start frame has to be found by a peer that is not yet listening to us, so it needs
245 /// enough preamble for that peer to lock. How much is enough is not purely a protocol
246 /// question: it also depends on what the driver's transmit path actually puts on air, which
247 /// is why this is a driver property rather than one constant. Drivers whose emitted preamble
248 /// gives the peer less usable synchronization than its programmed length suggests override
249 /// this with a longer value (see the override for the measurement behind it).
250 ///
251 /// The default is the protocol's documented preamble, 256 bits. An explicit
252 /// `normal_start_preamble:` always wins over this — including a shorter value, so a reporter
253 /// can still bisect downward in the field (ADR 0029).
254 ///
255 /// @return Preamble length in bytes.
256 [[nodiscard]] virtual uint16_t default_start_preamble() const { return NORMAL_START_PREAMBLE; }
257
258 /// @brief Apply runtime tuning parameters to the driver.
259 ///
260 /// Each driver consumes only the fields it understands; the default is a no-op for
261 /// chips with no runtime-tunable radio parameters. This keeps the hub free of
262 /// chip-specific tuning knowledge — it hands over the whole config and lets the
263 /// driver pick what it needs.
264 /// @param tuning Current tuning configuration.
265 virtual void apply_tuning(const TuningConfig &tuning) {}
266
267 /// @brief Per-channel dwell for a rotating listen that does not name its own dwell.
268 ///
269 /// Every @ref ListenPolicy::ROTATE_ALL_CHANNELS or @ref ListenPolicy::ROTATE_SKIPPING_REQUEST
270 /// listen falls back to this when @ref ListenSpec::dwell_ms is left at 0 — which is every call
271 /// site today: pairing discovery and the broadcast roll-call both rotate, and neither has a
272 /// measured reason to dwell differently from the other. The right dwell is inherently
273 /// chip-specific — it depends on how fast the chip can retune (fast hop vs. a
274 /// standby→retune→RX cycle) — so there is no generic default: each driver must return its
275 /// value, normally from its user-facing tuning field. This answers a chip question ("how long
276 /// must this radio sit on a channel before it can hear anything at all"), never a protocol one
277 /// — a loop with a measured reason to dwell differently sets @ref ListenSpec::dwell_ms instead
278 /// of asking for a second driver virtual.
279 /// @param tuning Current tuning configuration.
280 /// @return Dwell length in milliseconds.
281 [[nodiscard]] virtual uint16_t hop_dwell_ms(const TuningConfig &tuning) const = 0;
282
283 /// @brief Whether the chip re-enters RX fast enough after a TX to catch an
284 /// immediate reply through the standard exchange wait.
285 ///
286 /// Some chips need a standby/settle cycle between TX and RX, so a device's
287 /// immediate response (e.g. the pairing key-confirm 0x33) can arrive while the
288 /// receiver is still settling and be lost. Callers choose between the standard
289 /// exchange wait and a dedicated wait-and-retrigger strategy based on this.
290 /// There is no safe generic default — each driver must declare it.
291 /// @return true if an immediate reply after TX is reliably received.
292 [[nodiscard]] virtual bool has_fast_tx_rx_turnaround() const = 0;
293
294 /// @brief How long send_packet() with this length and config keeps the radio transmitting,
295 /// wake-up included.
296 ///
297 /// The exchange engine counts it against the exchange budget before it starts a try, so a try
298 /// whose transmission would end past the budget is not started. There is no generic default:
299 /// the estimate depends on the chip's line coding and wake-up mechanism, so each driver declares
300 /// it.
301 /// @param len Frame length in bytes, as handed to send_packet().
302 /// @param cfg The transmission's config.
303 /// @return Transmit time in microseconds.
304 [[nodiscard]] virtual uint32_t tx_air_time_us(uint8_t len, const RadioTxConfig &cfg) const = 0;
305
306 /// @brief Let the radio scan the hop channels by itself while the hub is idle.
307 ///
308 /// Called on every idle `loop()` pass. A driver whose radio can scan the hop channels by itself
309 /// starts or keeps that scan and returns true; the hub then does no host-side hopping for that
310 /// pass. Any explicit retune (`change_frequency()`, a transmission, a listen) ends the scan, and
311 /// the next idle pass starts it again. The default returns false: the hub hops from the host.
312 /// @return true if the radio is scanning by itself.
313 virtual bool run_idle_scan() { return false; }
314
315 /// Change the carrier frequency using fast hop (no standby transition needed).
316 virtual void change_frequency(uint32_t freq_hz) = 0;
317
318 /// Switch to continuous receive mode.
319 virtual void set_mode_rx() = 0;
320
321 /// Switch to standby mode.
322 virtual void set_mode_standby() = 0;
323
324 /// Returns true if the radio failed to initialize or encountered a fatal error.
325 /// @return true once a driver has latched a failure with @ref fail_; the state is sticky.
326 [[nodiscard]] virtual bool is_failed() const { return this->failed_; }
327
328 /// @brief Get a human‑readable chip name.
329 /// @return Short lowercase identifier (e.g. "sx1276").
330 [[nodiscard]] virtual const char *chip_name() const = 0;
331
332 /// @brief Short, human-readable reason the driver latched @ref is_failed, or `nullptr` if it has not.
333 ///
334 /// The first recorded reason wins, so it names the root cause rather than a later cascade (a dead
335 /// chip makes every later transaction fail too). Always a string literal: it outlives the driver,
336 /// which the hub deletes when init() fails. The hub prints it from dump_config, so it reaches log
337 /// clients that connect after boot and never saw the driver's own ESP_LOGE at the failure site.
338 [[nodiscard]] const char *failure_reason() const { return this->failure_reason_; }
339
340 /// Optional board front-end-module (external PA/LNA) summary emitted from dump_config, as its own
341 /// section ahead of dump_debug(). The front end belongs to the board rather than to the radio
342 /// chip, so it gets its own section instead of sharing the chip's diagnostic block.
343 virtual void dump_front_end() {}
344
345 /// Optional chip-specific diagnostics emitted from dump_config.
346 virtual void dump_debug() {}
347
348 /// @brief Get the current RF frequency.
349 /// @return Frequency in Hz.
350 [[nodiscard]] uint32_t get_current_freq() const { return this->current_freq_; }
351 /// @brief Get the most recent radio capture info.
352 /// @return const reference to RadioCaptureInfo.
353 [[nodiscard]] const RadioCaptureInfo &get_last_capture() const { return this->last_capture_; }
354
355 /// @brief Reset the diagnostic capture buffer.
356 ///
357 /// Public because ExchangeEngine blanks it at the start of every exchange: the radio only clears
358 /// this buffer when it actually begins a listen, so without an explicit reset a fully-silent
359 /// exchange's failure report would inherit the *previous* exchange's capture (frame length, RSSI,
360 /// IRQ bits) and claim "we heard something" when nothing was on air.
362
363 /// @brief True while a frame is arriving on the current channel and retuning would destroy it.
364 ///
365 /// Consulted by ExchangeEngine::maybe_hop() — the idle-path hop — which is purely time-gated and
366 /// otherwise fires on essentially every loop() pass (issue #81). Not consulted by
367 /// hop_frequency() itself: the blocking listen() loop does its own, differently-shaped gating
368 /// through preamble_or_sync_incoming(), and a caller that asked for a hop explicitly must get one.
369 ///
370 /// The default implementation is the recorded-state one: a driver reports a reception by calling
371 /// note_reception_in_progress_() from wherever it can actually observe one, and the holdoff
372 /// expires by itself after RX_HOP_HOLDOFF_US. That default is correct for any driver whose RX
373 /// path passes through check_for_packet() while the frame is still arriving, and it is what both
374 /// software-PHY drivers use. A driver that cannot observe a reception from check_for_packet()
375 /// overrides this and reads the chip at hop time instead — see RadioSX1276.
376 ///
377 /// Non-const because it expires its own latch, and because an override may do SPI.
378 [[nodiscard]] virtual bool reception_in_progress() {
379 if (!this->rx_hold_armed_)
380 return false;
381 if (micros() - this->rx_hold_since_us_ >= RX_HOP_HOLDOFF_US) {
382 this->rx_hold_armed_ = false;
383 return false;
384 }
385 return true;
386 }
387
388 /// Set by the ISR when DIO fires. Using access helpers instead of touching the flag directly
389 /// keeps the ISR/main-loop handoff explicit and lets ESP32 builds use atomic storage.
390 [[nodiscard]] bool is_dio_fired() const {
391#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
392 return this->dio_fired_.load(std::memory_order_acquire);
393#else
394 return this->dio_fired_;
395#endif
396 }
397
399 // The wait/check loops clear the latch only after they have observed it. That avoids losing
400 // an edge when TX completion and the next RX event happen close together.
401#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
402 this->dio_fired_.store(false, std::memory_order_release);
403#else
404 this->dio_fired_ = false;
405#endif
406 }
407
409 // Keep the ISR work to a single flag store so the interrupt path remains deterministic.
410#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
411 this->dio_fired_.store(true, std::memory_order_release);
412#else
413 this->dio_fired_ = true;
414#endif
415 }
416
417 protected:
418 /// Common preamble for blocking receive: clear diagnostics and output packet.
419 /// @param packet Output packet buffer to zero and prepare.
421 this->clear_last_capture();
422 packet = RadioRxPacket{};
423 }
424
425 /// Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
426 /// @param packet Output packet buffer to zero and prepare.
428 this->clear_last_capture();
429 packet = RadioRxPacket{};
430 this->clear_dio_fired();
431 }
432
433 /// Record that a frame is arriving right now. Re-arming refreshes the deadline, so a driver may
434 /// call this on every poll that still sees the reception.
436 this->rx_hold_armed_ = true;
437 this->rx_hold_since_us_ = micros();
438 }
439 /// Drop the holdoff: the reception ended, was delivered, or was torn down deliberately.
441
442 /// The clear-channel check a driver runs at the top of send_packet(). Without a threshold in
443 /// @p tx_config it does nothing. Otherwise it retunes the receiver to `tx_config.freq_hz` if it is
444 /// elsewhere and waits @p settle_us for the RSSI reading to describe the new channel, then reads
445 /// read_rssi() there.
446 /// @param tx_config The transmission about to be sent.
447 /// @param settle_us The chip's RSSI settle time after a retune (a driver constant).
448 /// @return The measured level if it is at or above the threshold (the channel is busy), else
449 /// nothing.
450 std::optional<int16_t> channel_busy_level_(const RadioTxConfig &tx_config, uint32_t settle_us);
451
452 /// Shared hardware reset sequence for chips with an active-low RST pin.
453 /// Drives RST pin low → 10 ms → high → 10 ms. Called from derived driver init().
454 void reset_hardware_();
455
456 /// Populate the common fields of RadioCaptureInfo from raw telemetry.
457 /// Chip‑specific fields (rx_done, crc_error, irq_flags*, irq_status, packet_status, etc.)
458 /// must be set by the derived driver after calling this helper.
459 /// @param blocking_wait if this was a blocking receive.
460 /// @param freq_hz RF frequency of the capture.
461 /// @param rssi_dbm Received signal strength.
462 /// @param raw Pointer to raw bytes (may be nullptr).
463 /// @param raw_len Length of raw buffer.
464 /// @param frame Pointer to parsed frame bytes (may be nullptr).
465 /// @param frame_len Length of parsed frame.
466 void populate_capture_base_(bool blocking_wait, uint32_t freq_hz, int16_t rssi_dbm, const uint8_t *raw,
467 uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
469 this->last_capture_.valid = true;
470 this->last_capture_.blocking_wait = blocking_wait;
471 this->last_capture_.timestamp_ms = millis();
472 this->last_capture_.freq_hz = freq_hz;
473 this->last_capture_.rssi_dbm = rssi_dbm;
474 if (raw != nullptr && raw_len > 0) {
475 this->last_capture_.raw_len = std::min(raw_len, (uint8_t) sizeof(this->last_capture_.raw));
476 memcpy(this->last_capture_.raw, raw, this->last_capture_.raw_len);
477 }
478 if (frame != nullptr && frame_len > 0) {
479 this->last_capture_.frame_len = std::min(frame_len, (uint8_t) sizeof(this->last_capture_.frame));
480 memcpy(this->last_capture_.frame, frame, this->last_capture_.frame_len);
481 }
482 }
483
484 /// Latch the failed state and record why, so @ref is_failed and @ref failure_reason can never
485 /// disagree. Only the first reason sticks; see @ref failure_reason.
486 /// @param reason String literal (stored by pointer, not copied).
487 void fail_(const char *reason) {
488 if (this->failure_reason_ == nullptr)
489 this->failure_reason_ = reason;
490 this->failed_ = true;
491 }
492
493 uint32_t current_freq_{FREQ_CH2};
495 InternalGPIOPin *rst_pin_{nullptr};
496
497 bool failed_{false}; ///< Latched by @ref fail_; see @ref is_failed.
498 const char *failure_reason_{nullptr}; ///< First failure cause, see @ref failure_reason.
499
500 bool cca_retune_logged_{false}; ///< The first clear-channel retune's cost has been logged.
501
502 bool rx_hold_armed_{false}; ///< Idle-hop holdoff latch — see reception_in_progress().
503 uint32_t rx_hold_since_us_{0}; ///< micros() timestamp the holdoff was last (re-)armed at.
504
505#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
506 std::atomic<bool> dio_fired_{false};
507#else
508 volatile bool dio_fired_{false};
509#endif
510};
511
512} // namespace home_io_control
513} // namespace esphome
void populate_capture_base_(bool blocking_wait, uint32_t freq_hz, int16_t rssi_dbm, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len)
Populate the common fields of RadioCaptureInfo from raw telemetry.
virtual void dump_front_end()
Optional board front-end-module (external PA/LNA) summary emitted from dump_config,...
void clear_last_capture()
Reset the diagnostic capture buffer.
uint32_t get_current_freq() const
Get the current RF frequency.
RadioDriver(InternalGPIOPin *rst_pin=nullptr)
virtual bool has_fast_tx_rx_turnaround() const =0
Whether the chip re-enters RX fast enough after a TX to catch an immediate reply through the standard...
void reset_hardware_()
Shared hardware reset sequence for chips with an active-low RST pin.
virtual uint16_t response_preamble() const
Return the preamble length for response/continuation frames.
virtual uint16_t default_start_preamble() const
Default preamble for a directed start frame, when the user has not set normal_start_preamble in YAML.
virtual bool reception_in_progress()
True while a frame is arriving on the current channel and retuning would destroy it.
virtual bool is_sync_detected()=0
Check if sync word has been detected (while in RX).
virtual void change_frequency(uint32_t freq_hz)=0
Change the carrier frequency using fast hop (no standby transition needed).
virtual TxResult send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config)=0
Send a packet using the specified carrier frequency and preamble settings.
const char * failure_reason() const
Short, human-readable reason the driver latched is_failed, or nullptr if it has not.
virtual bool run_idle_scan()
Let the radio scan the hop channels by itself while the hub is idle.
const RadioCaptureInfo & get_last_capture() const
Get the most recent radio capture info.
bool is_dio_fired() const
Set by the ISR when DIO fires.
uint32_t rx_hold_since_us_
micros() timestamp the holdoff was last (re-)armed at.
bool cca_retune_logged_
The first clear-channel retune's cost has been logged.
virtual const char * chip_name() const =0
Get a human‑readable chip name.
virtual uint16_t hop_dwell_ms(const TuningConfig &tuning) const =0
Per-channel dwell for a rotating listen that does not name its own dwell.
virtual bool is_failed() const
Returns true if the radio failed to initialize or encountered a fatal error.
void note_reception_in_progress_()
Record that a frame is arriving right now.
bool rx_hold_armed_
Idle-hop holdoff latch — see reception_in_progress().
virtual bool init()=0
Initialize the radio hardware. Returns true on success.
virtual void set_mode_standby()=0
Switch to standby mode.
void clear_reception_in_progress_()
Drop the holdoff: the reception ended, was delivered, or was torn down deliberately.
virtual void apply_tuning(const TuningConfig &tuning)
Apply runtime tuning parameters to the driver.
std::optional< int16_t > channel_busy_level_(const RadioTxConfig &tx_config, uint32_t settle_us)
The clear-channel check a driver runs at the top of send_packet().
virtual void dump_debug()
Optional chip-specific diagnostics emitted from dump_config.
void fail_(const char *reason)
Latch the failed state and record why, so is_failed and failure_reason can never disagree.
virtual bool check_for_packet(RadioRxPacket &packet)=0
Non-blocking check for a received packet.
virtual int16_t read_rssi()=0
Read instantaneous RSSI (in dBm) on the current channel, while in RX mode.
void prepare_nonblocking_receive_(RadioRxPacket &packet)
Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
const char * failure_reason_
First failure cause, see failure_reason.
bool failed_
Latched by fail_; see is_failed.
virtual bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms)=0
Wait (blocking) for a packet with timeout.
void prepare_blocking_receive_(RadioRxPacket &packet)
Common preamble for blocking receive: clear diagnostics and output packet.
virtual uint32_t tx_air_time_us(uint8_t len, const RadioTxConfig &cfg) const =0
How long send_packet() with this length and config keeps the radio transmitting, wake-up included.
virtual void set_mode_rx()=0
Switch to continuous receive mode.
virtual bool is_preamble_detected()=0
Check if preamble has been detected (while in RX).
Interface for SPI bus access.
virtual void spi_enable()=0
Enable the SPI bus (assert CS low).
virtual void spi_write(uint8_t data)=0
Write one byte (MOSI only, MISO ignored).
virtual uint8_t spi_transfer(uint8_t data)=0
Transfer one byte full‑duplex (MOSI→MISO).
virtual void spi_disable()=0
Disable the SPI bus (deassert CS).
virtual uint8_t spi_read()=0
Read one byte (MISO only, MOSI driven with 0).
constexpr uint32_t RX_HOP_HOLDOFF_US
Longest a frame arriving on the current channel may hold off an idle-path channel hop,...
FemProfile
Which RF front-end module (if any) sits between the SX1262 and the antenna.
@ XY16P35
LilyGO T-Beam 1W SX1262 (868 MHz).
@ KCT8103L
Heltec WiFi LoRa 32 V4.3 / V4 R8 – Kangxi KCT8103L. Mode pin active-HIGH during TX.
@ GC1109
Heltec WiFi LoRa 32 V4.2 – Geo-chip GC1109. Mode pin active-HIGH during TX.
TxWake
How hard a transmission has to work to be heard: the wake-up level the receiver needs.
@ NONE
The receiver is already listening on this channel: the frame continues an exchange.
@ FAILED
No usable reply; the device may never have heard the request.
const char * tcxo_voltage_label(uint8_t code)
Human-readable voltage for a tcxo_voltage code, for the config dump.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
constexpr uint8_t TCXO_VOLTAGE_NONE
Sentinel tcxo_voltage code (YAML none) for a board with a bare crystal and no TCXO.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Physical-layer radio and timing parameters for the IO-Homecontrol protocol.
Diagnostic capture from a radio operation.
uint32_t timestamp_ms
Timestamp of capture (millis).
uint8_t frame_len
Number of valid bytes in frame[].
uint16_t irq_status
Raw IRQ status register value.
uint8_t packet_status
Packet status byte (chip-specific).
uint8_t irq_flags2
IRQ flags group 2 (chip-specific, includes CRC flag).
bool blocking_wait
True if captured during a blocking wait.
bool crc_error
True if a CRC error was detected.
uint8_t frame[RADIO_PACKET_BUFFER_SIZE]
Parsed protocol frame bytes.
uint8_t reported_len
Length reported by the radio chip.
bool rx_done
True if RxDone IRQ fired.
uint32_t freq_hz
RF frequency of capture (Hz).
uint8_t irq_flags1
IRQ flags group 1 (chip-specific).
uint8_t raw[RADIO_PACKET_BUFFER_SIZE]
Raw radio buffer bytes.
uint8_t raw_len
Number of valid bytes in raw[].
int16_t rssi_dbm
Received signal strength (dBm).
uint8_t rx_offset
RX buffer offset where the frame starts (0 for chips without offset reporting).
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint32_t freq_hz
Frequency the packet was received on (Hz).
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.
Configuration for transmitting a packet: carrier frequency, preamble length, wake-up level and clear-...
uint16_t preamble_len
Preamble length in symbol periods (bytes).
uint32_t freq_hz
Carrier frequency in Hz.
std::optional< int16_t > cca_threshold_dbm
If set, send only if the energy on freq_hz, measured right before the first transmitted symbol,...
TxWake wake
Wake-up level the receiver needs (tx_wake_for()).
All runtime tunable parameters for pairing and radio diagnostics.
What send_packet() did with a frame.
static TxResult channel_busy(int16_t level_dbm)
@ CHANNEL_BUSY
The clear-channel check found the channel busy; nothing was sent.
@ FAILED
The radio could not send (invalid frame, TX timeout).
int16_t cca_level_dbm
The level the clear-channel check measured; set with CHANNEL_BUSY.
Runtime tuning configuration for pairing and radio diagnostics.