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