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