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 <atomic>
14#include <cstdint>
15#include "esphome/core/hal.h"
16
17namespace esphome {
18namespace home_io_control {
19
20inline constexpr uint8_t RADIO_PACKET_BUFFER_SIZE =
21 64; ///< Scratch buffer size for raw radio packets and recovered frames.
22
23/// Interface for SPI bus access.
24/// The ESPHome component implements this by delegating to its SPIDevice methods,
25/// allowing radio drivers to perform SPI transactions without depending on the
26/// ESPHome SPI framework directly.
27/// @ingroup hioc_radio
28class SpiAccess {
29 public:
30 virtual ~SpiAccess() = default;
31 /// Enable the SPI bus (assert CS low).
32 virtual void spi_enable() = 0;
33 /// Disable the SPI bus (deassert CS).
34 virtual void spi_disable() = 0;
35 /// Transfer one byte full‑duplex (MOSI→MISO).
36 /// @param data Byte to send.
37 /// @return Byte received from MISO.
38 virtual uint8_t spi_transfer(uint8_t data) = 0;
39 /// Write one byte (MOSI only, MISO ignored).
40 /// @param data Byte to send.
41 virtual void spi_write(uint8_t data) = 0;
42 /// Read one byte (MISO only, MOSI driven with 0).
43 /// @return Byte received.
44 virtual uint8_t spi_read() = 0;
45};
46
47/// Configuration for transmitting a packet: carrier frequency and preamble length.
49 uint32_t freq_hz{FREQ_CH2}; ///< Carrier frequency in Hz.
50 uint16_t preamble_len{SHORT_PREAMBLE}; ///< Preamble length in symbol periods (bytes).
51};
52
53/// Raw packet received from the radio.
55 uint32_t freq_hz{0}; ///< Frequency the packet was received on (Hz).
56 uint8_t len{0}; ///< Length of packet in bytes.
57 uint8_t data[RADIO_PACKET_BUFFER_SIZE]{}; ///< Raw packet data buffer.
58};
59
60/// Diagnostic capture from a radio operation.
61///
62/// Populated after every wait_for_packet / check_for_packet. Contains both the
63/// raw bytes reported by the chip (before any protocol-specific recovery) and
64/// the parsed frame handed to the protocol layer.
66 bool valid{false}; ///< True if capture is valid.
67 bool blocking_wait{false}; ///< True if captured during a blocking wait.
68 bool rx_done{false}; ///< True if RxDone IRQ fired.
69 bool crc_error{false}; ///< True if CRC error detected (SX1276: never set in IoHomeOn mode; SX1262: set on bad CRC).
70 uint32_t timestamp_ms{0}; ///< Timestamp of capture (millis).
71 uint32_t freq_hz{0}; ///< RF frequency of capture (Hz).
72 int16_t rssi_dbm{0}; ///< Received signal strength (dBm).
73 uint16_t irq_status{0}; ///< Raw IRQ status register value.
74 uint8_t irq_flags1{0}; ///< IRQ flags group 1 (chip-specific).
75 uint8_t irq_flags2{0}; ///< IRQ flags group 2 (chip-specific, includes CRC flag).
76 uint8_t packet_status{0}; ///< Packet status byte (chip-specific).
77 uint8_t rx_offset{0}; ///< RX buffer offset where frame starts (SX1262).
78 uint8_t reported_len{0}; ///< Length reported by the radio chip.
79 // raw[] preserves the chip-reported bytes before any protocol-specific recovery, while
80 // frame[] stores the bytes handed to parse(). Keeping both made it possible to compare
81 // SX1262 recovery output against SX1276 captures during bring-up.
82 uint8_t raw_len{0}; ///< Number of valid bytes in raw[].
83 uint8_t frame_len{0}; ///< Number of valid bytes in frame[].
84 uint8_t raw[RADIO_PACKET_BUFFER_SIZE]{}; ///< Raw radio buffer bytes.
85 uint8_t frame[RADIO_PACKET_BUFFER_SIZE]{}; ///< Parsed protocol frame bytes.
86};
87
88/// Abstract radio driver for IO-Homecontrol.
89///
90/// Encapsulates all chip-specific operations: initialization, packet TX/RX,
91/// frequency control, and mode switching. Concrete implementations (RadioSX1276,
92/// RadioSX1262) handle the register-level details for each chip.
93/// @ingroup hioc_radio
95 public:
96 explicit RadioDriver(InternalGPIOPin *rst_pin = nullptr) : rst_pin_(rst_pin) {}
97 virtual ~RadioDriver() = default;
98
99 /// Initialize the radio hardware. Returns true on success.
100 virtual bool init() = 0;
101
102 /// Send a packet using the specified carrier frequency and preamble settings.
103 /// The radio handles CRC automatically (IoHomeOn mode for SX1276).
104 virtual bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) = 0;
105
106 /// Wait (blocking) for a packet with timeout. Returns true if a packet was received.
107 /// Contract:
108 /// - Clears last_capture_ and output packet before waiting.
109 /// - On success: populates packet and last_capture_, returns true.
110 /// - On timeout/failure: may populate last_capture_ for diagnostics, returns false.
111 /// - Radio remains in RX mode on return (regardless of outcome).
112 virtual bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) = 0;
113
114 /// Non-blocking check for a received packet. Called from loop().
115 /// Returns true if a packet was read into packet.
116 /// Contract:
117 /// - Returns false immediately if no DIO interrupt has fired.
118 /// - On success: populates packet and last_capture_, returns true.
119 /// - On failure: may populate last_capture_ for diagnostics, returns false.
120 virtual bool check_for_packet(RadioRxPacket &packet) = 0;
121
122 /// Read instantaneous RSSI (in dBm) while in RX mode.
123 /// Used for listen-before-talk (LBT) carrier sense before transmitting.
124 /// @return RSSI in dBm (negative value).
125 virtual int16_t read_rssi() = 0;
126
127 /// Change the carrier frequency using fast hop (no standby transition needed).
128 virtual void change_frequency(uint32_t freq_hz) = 0;
129
130 /// Switch to continuous receive mode.
131 virtual void set_mode_rx() = 0;
132
133 /// Switch to standby mode.
134 virtual void set_mode_standby() = 0;
135
136 /// Returns true if the radio failed to initialize or encountered a fatal error.
137 /// @return true on failure.
138 [[nodiscard]] virtual bool is_failed() const = 0;
139
140 /// @brief Get a human‑readable chip name.
141 /// @return "sx1276" or "sx1262".
142 [[nodiscard]] virtual const char *chip_name() const = 0;
143
144 /// Optional chip-specific diagnostics emitted from dump_config.
145 virtual void dump_debug() {}
146
147 /// @brief Get the current RF frequency.
148 /// @return Frequency in Hz.
149 [[nodiscard]] uint32_t get_current_freq() const { return this->current_freq_; }
150 /// @brief Get the most recent radio capture info.
151 /// @return const reference to RadioCaptureInfo.
152 [[nodiscard]] const RadioCaptureInfo &get_last_capture() const { return this->last_capture_; }
153
154 /// Set by the ISR when DIO fires. Using access helpers instead of touching the flag directly
155 /// keeps the ISR/main-loop handoff explicit and lets ESP32 builds use atomic storage.
156 [[nodiscard]] bool is_dio_fired() const {
157#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
158 return this->dio_fired_.load(std::memory_order_acquire);
159#else
160 return this->dio_fired_;
161#endif
162 }
163
165 // The wait/check loops clear the latch only after they have observed it. That avoids losing
166 // an edge when TX completion and the next RX event happen close together.
167#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
168 this->dio_fired_.store(false, std::memory_order_release);
169#else
170 this->dio_fired_ = false;
171#endif
172 }
173
175 // Keep the ISR work to a single flag store so the interrupt path remains deterministic.
176#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
177 this->dio_fired_.store(true, std::memory_order_release);
178#else
179 this->dio_fired_ = true;
180#endif
181 }
182
183 protected:
184 /// Clear the last capture info (resets diagnostic buffer).
186
187 /// Common preamble for blocking receive: clear diagnostics and output packet.
188 /// @param packet Output packet buffer to zero and prepare.
190 this->clear_last_capture_();
191 packet = RadioRxPacket{};
192 }
193
194 /// Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
195 /// @param packet Output packet buffer to zero and prepare.
197 this->clear_last_capture_();
198 packet = RadioRxPacket{};
199 this->clear_dio_fired();
200 }
201
202 /// Hardware reset sequence common to all SX chips.
203 /// Drives RST pin low → 10 ms → high → 10 ms. Called from derived driver init().
204 void reset_hardware_();
205
206 /// Populate the common fields of RadioCaptureInfo from raw telemetry.
207 /// Chip‑specific fields (rx_done, crc_error, irq_flags*, irq_status, packet_status, etc.)
208 /// must be set by the derived driver after calling this helper.
209 /// @param blocking_wait if this was a blocking receive.
210 /// @param freq_hz RF frequency of the capture.
211 /// @param rssi_dbm Received signal strength.
212 /// @param raw Pointer to raw bytes (may be nullptr).
213 /// @param raw_len Length of raw buffer.
214 /// @param frame Pointer to parsed frame bytes (may be nullptr).
215 /// @param frame_len Length of parsed frame.
216 void populate_capture_base_(bool blocking_wait, uint32_t freq_hz, int16_t rssi_dbm, const uint8_t *raw,
217 uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
219 this->last_capture_.valid = true;
220 this->last_capture_.blocking_wait = blocking_wait;
221 this->last_capture_.timestamp_ms = millis();
222 this->last_capture_.freq_hz = freq_hz;
223 this->last_capture_.rssi_dbm = rssi_dbm;
224 if (raw != nullptr && raw_len > 0) {
225 this->last_capture_.raw_len = std::min(raw_len, (uint8_t) sizeof(this->last_capture_.raw));
226 memcpy(this->last_capture_.raw, raw, this->last_capture_.raw_len);
227 }
228 if (frame != nullptr && frame_len > 0) {
229 this->last_capture_.frame_len = std::min(frame_len, (uint8_t) sizeof(this->last_capture_.frame));
230 memcpy(this->last_capture_.frame, frame, this->last_capture_.frame_len);
231 }
232 }
233
236 InternalGPIOPin *rst_pin_{nullptr};
237
238#if defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
239 std::atomic<bool> dio_fired_{false};
240#else
241 volatile bool dio_fired_{false};
242#endif
243};
244
245} // namespace home_io_control
246} // 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)
void reset_hardware_()
Hardware reset sequence common to all SX chips.
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 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.
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).
Definition proto_frame.h:32
static constexpr uint16_t SHORT_PREAMBLE
8 bytes for response/continuation frames
Definition proto_frame.h:41
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
IO-Homecontrol 2W protocol definitions.
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 CRC error detected (SX1276: never set in IoHomeOn mode; SX1262: set on bad CRC).
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 frame starts (SX1262).
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.