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 "esphome/core/hal.h"
18
19namespace esphome {
20namespace home_io_control {
21
22inline constexpr uint8_t RADIO_PACKET_BUFFER_SIZE =
23 64; ///< Scratch buffer size for raw radio packets and recovered frames.
24
25/// Interface for SPI bus access.
26/// The ESPHome component implements this by delegating to its SPIDevice methods,
27/// allowing radio drivers to perform SPI transactions without depending on the
28/// ESPHome SPI framework directly.
29/// @ingroup hioc_radio
30class SpiAccess {
31 public:
32 virtual ~SpiAccess() = default;
33 /// Enable the SPI bus (assert CS low).
34 virtual void spi_enable() = 0;
35 /// Disable the SPI bus (deassert CS).
36 virtual void spi_disable() = 0;
37 /// Transfer one byte full‑duplex (MOSI→MISO).
38 /// @param data Byte to send.
39 /// @return Byte received from MISO.
40 virtual uint8_t spi_transfer(uint8_t data) = 0;
41 /// Write one byte (MOSI only, MISO ignored).
42 /// @param data Byte to send.
43 virtual void spi_write(uint8_t data) = 0;
44 /// Read one byte (MISO only, MOSI driven with 0).
45 /// @return Byte received.
46 virtual uint8_t spi_read() = 0;
47};
48
49/// Configuration for transmitting a packet: carrier frequency and preamble length.
51 uint32_t freq_hz{FREQ_CH2}; ///< Carrier frequency in Hz.
52 uint16_t preamble_len{SHORT_PREAMBLE}; ///< Preamble length in symbol periods (bytes).
53};
54
55/// Raw packet received from the radio.
57 uint32_t freq_hz{0}; ///< Frequency the packet was received on (Hz).
58 uint8_t len{0}; ///< Length of packet in bytes.
59 uint8_t data[RADIO_PACKET_BUFFER_SIZE]{}; ///< Raw packet data buffer.
60};
61
62/// Diagnostic capture from a radio operation.
63///
64/// Populated after every wait_for_packet / check_for_packet. Contains both the
65/// raw bytes reported by the chip (before any protocol-specific recovery) and
66/// the parsed frame handed to the protocol layer.
68 bool valid{false}; ///< True if capture is valid.
69 bool blocking_wait{false}; ///< True if captured during a blocking wait.
70 bool rx_done{false}; ///< True if RxDone IRQ fired.
71 bool crc_error{false}; ///< True if a CRC error was detected. Chip-dependent: some drivers cannot report
72 ///< CRC failures — see the concrete driver's capture documentation.
73 uint32_t timestamp_ms{0}; ///< Timestamp of capture (millis).
74 uint32_t freq_hz{0}; ///< RF frequency of capture (Hz).
75 int16_t rssi_dbm{0}; ///< Received signal strength (dBm).
76 uint16_t irq_status{0}; ///< Raw IRQ status register value.
77 uint8_t irq_flags1{0}; ///< IRQ flags group 1 (chip-specific).
78 uint8_t irq_flags2{0}; ///< IRQ flags group 2 (chip-specific, includes CRC flag).
79 uint8_t packet_status{0}; ///< Packet status byte (chip-specific).
80 uint8_t rx_offset{0}; ///< RX buffer offset where the frame starts (0 for chips without offset reporting).
81 uint8_t reported_len{0}; ///< Length reported by the radio chip.
82 // raw[] preserves the chip-reported bytes before any driver-specific recovery, while
83 // frame[] stores the bytes handed to parse(). Keeping both makes it possible to compare
84 // one driver's recovery output against reference captures from another.
85 uint8_t raw_len{0}; ///< Number of valid bytes in raw[].
86 uint8_t frame_len{0}; ///< Number of valid bytes in frame[].
87 uint8_t raw[RADIO_PACKET_BUFFER_SIZE]{}; ///< Raw radio buffer bytes.
88 uint8_t frame[RADIO_PACKET_BUFFER_SIZE]{}; ///< Parsed protocol frame bytes.
89};
90
91/// Abstract radio driver for IO-Homecontrol.
92///
93/// Encapsulates all chip-specific operations: initialization, packet TX/RX,
94/// frequency control, and mode switching. Concrete implementations (RadioSX1276,
95/// RadioSX1262, RadioLR1121) handle the register-level details for each chip.
96/// @ingroup hioc_radio
98 public:
99 explicit RadioDriver(InternalGPIOPin *rst_pin = nullptr) : rst_pin_(rst_pin) {}
100 virtual ~RadioDriver() = default;
101
102 /// Initialize the radio hardware. Returns true on success.
103 virtual bool init() = 0;
104
105 /// Send a packet using the specified carrier frequency and preamble settings.
106 /// The driver is responsible for appending the protocol CRC on the air
107 /// (in hardware or software, depending on the chip).
108 virtual bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) = 0;
109
110 /// Wait (blocking) for a packet with timeout. Returns true if a packet was received.
111 /// Contract:
112 /// - Clears last_capture_ and output packet before waiting.
113 /// - On success: populates packet and last_capture_, returns true.
114 /// - On timeout/failure: may populate last_capture_ for diagnostics, returns false.
115 /// - Radio remains in RX mode on return (regardless of outcome).
116 virtual bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) = 0;
117
118 /// Non-blocking check for a received packet. Called from loop().
119 /// Returns true if a packet was read into packet.
120 /// Contract:
121 /// - Returns false immediately if no DIO interrupt has fired.
122 /// - On success: populates packet and last_capture_, returns true.
123 /// - On failure: may populate last_capture_ for diagnostics, returns false.
124 virtual bool check_for_packet(RadioRxPacket &packet) = 0;
125
126 /// Read instantaneous RSSI (in dBm) while in RX mode.
127 /// Used for listen-before-talk (LBT) carrier sense before transmitting.
128 /// @return RSSI in dBm (negative value).
129 virtual int16_t read_rssi() = 0;
130
131 /// @brief Check if sync word has been detected (while in RX).
132 /// Used to gate frequency hopping — prevents hopping away mid-frame.
133 virtual bool is_sync_detected() = 0;
134
135 /// @brief Check if preamble has been detected (while in RX).
136 /// Used together with sync detection to gate frequency hopping.
137 virtual bool is_preamble_detected() = 0;
138
139 /// @brief Return the preamble length for response/continuation frames.
140 ///
141 /// Callers use this instead of hardcoding SHORT_PREAMBLE for any frame sent as
142 /// an immediate reply within an exchange (challenge responses, key transfers,
143 /// and any future non-START continuation frames — i.e. tight RX→TX turnaround).
144 ///
145 /// The default is the protocol's standard SHORT_PREAMBLE. Drivers whose TX
146 /// waveform gives the peer device less synchronization margin override this
147 /// with a longer preamble (see the concrete drivers for the chip-specific
148 /// rationale).
149 ///
150 /// @return Preamble length in bytes.
151 [[nodiscard]] virtual uint16_t response_preamble() const { return SHORT_PREAMBLE; }
152
153 /// @brief Apply runtime tuning parameters to the driver.
154 ///
155 /// Each driver consumes only the fields it understands; the default is a no-op for
156 /// chips with no runtime-tunable radio parameters. This keeps the hub free of
157 /// chip-specific tuning knowledge — it hands over the whole config and lets the
158 /// driver pick what it needs.
159 /// @param tuning Current tuning configuration.
160 virtual void apply_tuning(const TuningConfig &tuning) {}
161
162 /// @brief Per-channel dwell while waiting for an authenticated exchange response.
163 ///
164 /// The default RESPONSE_CHANNEL_WAIT_MS slice is correct for the baseline protocol
165 /// flow and pairing. A driver overrides this with a longer dwell when its RX path
166 /// needs more margin to catch the final post-auth response before hopping away
167 /// (see the concrete drivers for the chip-specific rationale).
168 /// @return Slice length in milliseconds.
169 [[nodiscard]] virtual uint32_t exchange_wait_slice_ms() const { return RESPONSE_CHANNEL_WAIT_MS; }
170
171 /// @brief Per-channel dwell while pairing discovery hops across channels.
172 ///
173 /// The right dwell is inherently chip-specific — it depends on how fast the chip
174 /// can retune (fast hop vs. a standby→retune→RX cycle) — so there is no generic
175 /// default: each driver must return its value, normally from its user-facing
176 /// tuning field.
177 /// @param tuning Current tuning configuration.
178 /// @return Slice length in milliseconds.
179 [[nodiscard]] virtual uint16_t discovery_hop_slice_ms(const TuningConfig &tuning) const = 0;
180
181 /// @brief Whether the chip re-enters RX fast enough after a TX to catch an
182 /// immediate reply through the standard exchange wait.
183 ///
184 /// Some chips need a standby/settle cycle between TX and RX, so a device's
185 /// immediate response (e.g. the pairing key-confirm 0x33) can arrive while the
186 /// receiver is still settling and be lost. Callers choose between the standard
187 /// exchange wait and a dedicated wait-and-retrigger strategy based on this.
188 /// There is no safe generic default — each driver must declare it.
189 /// @return true if an immediate reply after TX is reliably received.
190 [[nodiscard]] virtual bool has_fast_tx_rx_turnaround() const = 0;
191
192 /// Change the carrier frequency using fast hop (no standby transition needed).
193 virtual void change_frequency(uint32_t freq_hz) = 0;
194
195 /// Switch to continuous receive mode.
196 virtual void set_mode_rx() = 0;
197
198 /// Switch to standby mode.
199 virtual void set_mode_standby() = 0;
200
201 /// Returns true if the radio failed to initialize or encountered a fatal error.
202 /// @return true on failure.
203 [[nodiscard]] virtual bool is_failed() const = 0;
204
205 /// @brief Get a human‑readable chip name.
206 /// @return Short lowercase identifier (e.g. "sx1276").
207 [[nodiscard]] virtual const char *chip_name() const = 0;
208
209 /// Optional chip-specific diagnostics emitted from dump_config.
210 virtual void dump_debug() {}
211
212 /// @brief Get the current RF frequency.
213 /// @return Frequency in Hz.
214 [[nodiscard]] uint32_t get_current_freq() const { return this->current_freq_; }
215 /// @brief Get the most recent radio capture info.
216 /// @return const reference to RadioCaptureInfo.
217 [[nodiscard]] const RadioCaptureInfo &get_last_capture() const { return this->last_capture_; }
218
219 /// Set by the ISR when DIO fires. Using access helpers instead of touching the flag directly
220 /// keeps the ISR/main-loop handoff explicit and lets ESP32 builds use atomic storage.
221 [[nodiscard]] bool is_dio_fired() const {
222#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
223 return this->dio_fired_.load(std::memory_order_acquire);
224#else
225 return this->dio_fired_;
226#endif
227 }
228
230 // The wait/check loops clear the latch only after they have observed it. That avoids losing
231 // an edge when TX completion and the next RX event happen close together.
232#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
233 this->dio_fired_.store(false, std::memory_order_release);
234#else
235 this->dio_fired_ = false;
236#endif
237 }
238
240 // Keep the ISR work to a single flag store so the interrupt path remains deterministic.
241#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
242 this->dio_fired_.store(true, std::memory_order_release);
243#else
244 this->dio_fired_ = true;
245#endif
246 }
247
248 protected:
249 /// Clear the last capture info (resets diagnostic buffer).
251
252 /// Common preamble for blocking receive: clear diagnostics and output packet.
253 /// @param packet Output packet buffer to zero and prepare.
255 this->clear_last_capture_();
256 packet = RadioRxPacket{};
257 }
258
259 /// Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
260 /// @param packet Output packet buffer to zero and prepare.
262 this->clear_last_capture_();
263 packet = RadioRxPacket{};
264 this->clear_dio_fired();
265 }
266
267 /// Shared hardware reset sequence for chips with an active-low RST pin.
268 /// Drives RST pin low → 10 ms → high → 10 ms. Called from derived driver init().
269 void reset_hardware_();
270
271 /// Populate the common fields of RadioCaptureInfo from raw telemetry.
272 /// Chip‑specific fields (rx_done, crc_error, irq_flags*, irq_status, packet_status, etc.)
273 /// must be set by the derived driver after calling this helper.
274 /// @param blocking_wait if this was a blocking receive.
275 /// @param freq_hz RF frequency of the capture.
276 /// @param rssi_dbm Received signal strength.
277 /// @param raw Pointer to raw bytes (may be nullptr).
278 /// @param raw_len Length of raw buffer.
279 /// @param frame Pointer to parsed frame bytes (may be nullptr).
280 /// @param frame_len Length of parsed frame.
281 void populate_capture_base_(bool blocking_wait, uint32_t freq_hz, int16_t rssi_dbm, const uint8_t *raw,
282 uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
284 this->last_capture_.valid = true;
285 this->last_capture_.blocking_wait = blocking_wait;
286 this->last_capture_.timestamp_ms = millis();
287 this->last_capture_.freq_hz = freq_hz;
288 this->last_capture_.rssi_dbm = rssi_dbm;
289 if (raw != nullptr && raw_len > 0) {
290 this->last_capture_.raw_len = std::min(raw_len, (uint8_t) sizeof(this->last_capture_.raw));
291 memcpy(this->last_capture_.raw, raw, this->last_capture_.raw_len);
292 }
293 if (frame != nullptr && frame_len > 0) {
294 this->last_capture_.frame_len = std::min(frame_len, (uint8_t) sizeof(this->last_capture_.frame));
295 memcpy(this->last_capture_.frame, frame, this->last_capture_.frame_len);
296 }
297 }
298
301 InternalGPIOPin *rst_pin_{nullptr};
302
303#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
304 std::atomic<bool> dio_fired_{false};
305#else
306 volatile bool dio_fired_{false};
307#endif
308};
309
310} // namespace home_io_control
311} // 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.
void clear_last_capture_()
Clear the last capture info (resets diagnostic 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 discovery_hop_slice_ms(const TuningConfig &tuning) const =0
Per-channel dwell while pairing discovery hops across channels.
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 bool is_failed() const =0
Returns true if the radio failed to initialize or encountered a fatal error.
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.
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 bool init()=0
Initialize the radio hardware. Returns true on success.
virtual void set_mode_standby()=0
Switch to standby mode.
virtual void apply_tuning(const TuningConfig &tuning)
Apply runtime tuning parameters to the driver.
virtual uint32_t exchange_wait_slice_ms() const
Per-channel dwell while waiting for an authenticated exchange response.
virtual void dump_debug()
Optional chip-specific diagnostics emitted from dump_config.
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.
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).
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
static constexpr uint16_t SHORT_PREAMBLE
8 bytes for response/continuation frames
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
static constexpr int32_t RESPONSE_CHANNEL_WAIT_MS
Per-channel dwell while waiting for an exchange response.
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.
bool valid
True if capture is valid.
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.