Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_sx1276.cpp
Go to the documentation of this file.
1/// @file radio_sx1276.cpp
2/// @brief SX1276 radio driver implementation for IO-Homecontrol.
3/// @ingroup hioc_radio
4
5// This file is mostly chip-register programming and packed bit masks. Naming every literal that
6// mirrors the datasheet hurts readability more than it helps, so the magic-number checks are
7// suppressed at file scope here.
8// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
9
10#include "radio_sx1276.h"
11#include "log_frame.h"
12#include "radio_poll.h"
13#include "esphome/core/log.h"
14#include "esphome/core/application.h"
15
16#include <cinttypes>
17
18namespace esphome {
19namespace home_io_control {
20
21static const char *const TAG = "home_io_control.sx1276";
22
23// === SPI register access ===
24
25uint8_t RadioSX1276::read_register_(uint8_t reg) {
26 this->spi_->spi_enable();
27 this->spi_->spi_write(reg & 0x7F);
28 uint8_t const value = this->spi_->spi_read();
29 this->spi_->spi_disable();
30 return value;
31}
32
33void RadioSX1276::write_register_(uint8_t reg, uint8_t value) {
34 this->spi_->spi_enable();
35 this->spi_->spi_write(reg | 0x80);
36 this->spi_->spi_write(value);
37 this->spi_->spi_disable();
38}
39
40// === Radio mode control ===
41
42void RadioSX1276::set_mode_(uint8_t mode) {
43 uint8_t const current = this->read_register_(REG_OP_MODE);
44 this->write_register_(REG_OP_MODE, (current & ~MODE_MASK) | (mode & MODE_MASK));
45 // Judged on a register sample taken after the deadline (see poll_until), so a task preempted past
46 // the timeout cannot fail a chip that has already reached the mode.
47 PollResult const r = poll_until(
48 [this, mode] {
49 uint8_t const cur_mode = this->read_register_(REG_OP_MODE) & MODE_MASK;
50 return cur_mode == mode || (mode == MODE_RX && cur_mode == 0x04);
51 },
52 50);
53 if (!r.ready) {
54 ESP_LOGE(TAG, "Radio mode change timeout");
55 this->fail_("SX1276 never reached the requested operating mode");
56 }
57}
58
59void RadioSX1276::set_mode_rx() { this->set_mode_(MODE_RX); }
60void RadioSX1276::set_mode_standby() { this->set_mode_(MODE_STDBY); }
61
63 this->set_mode_(MODE_STDBY);
64 this->write_register_(REG_IMAGE_CAL, 0x40);
65 PollResult const r = poll_until([this] { return (this->read_register_(REG_IMAGE_CAL) & 0x20) == 0; }, 20);
66 if (!r.ready) {
67 ESP_LOGE(TAG, "Image calibration timeout");
68 this->fail_("SX1276 image calibration never completed");
69 }
70}
71
73
74void RadioSX1276::fill_capture_info_(bool blocking_wait, uint8_t irq1, uint8_t irq2, uint8_t rssi, const uint8_t *raw,
75 uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
76 this->populate_capture_base_(blocking_wait, this->current_freq_, -(int16_t) rssi / 2, raw, raw_len, frame, frame_len);
77 this->last_capture_.rx_done = (irq2 & 0x04) != 0;
78 this->last_capture_.irq_flags1 = irq1;
79 this->last_capture_.irq_flags2 = irq2;
80 // IRQ_FLAGS2 bit 1 = PayloadCrcError. In IoHomeOn mode the chip filters
81 // bad-CRC frames in hardware before raising RxDone, so this bit is never
82 // set when a packet is delivered. Always false on SX1276.
83 this->last_capture_.crc_error = false;
84 this->last_capture_.reported_len = raw_len;
85}
86
87void RadioSX1276::change_frequency(uint32_t freq_hz) {
88 uint64_t const frf = ((uint64_t) freq_hz << 19) / FXOSC;
89 this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
90 this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
91 this->write_register_(REG_FRF_LSB, (uint8_t) (frf & 0xFF));
92 this->current_freq_ = freq_hz;
93 // A retune in RX (FastHop: FRF only, no mode change) keeps the receiver's state from the old
94 // channel: PreambleDetect stays latched and AGC/AFC stay where a preamble, or noise, left them.
95 // Restarting the chain clears both, so the next channel's preamble detection is its own. Without
96 // it the idle hop cannot tell a preamble arriving here from one seen long ago, and on the bench a
97 // V2 waiting for a hub missed 30 of 52 clean 213 ms-preamble searches; with it and the preamble
98 // hold in reception_in_progress(), 0 of 41.
99 if ((this->read_register_(REG_OP_MODE) & MODE_MASK) == MODE_RX)
100 this->write_register_(REG_RX_CONFIG, RX_CONFIG_VALUE | RX_CONFIG_RESTART_RX_WITH_PLL_LOCK);
101}
102
103int16_t RadioSX1276::read_rssi() { return -(int16_t) this->read_register_(REG_RSSI_VALUE) / 2; }
104
106 return (this->read_register_(REG_IRQ_FLAGS1) & 0x01) != 0; // Bit 0 = SyncAddressMatch
107}
108
110 return (this->read_register_(REG_IRQ_FLAGS1) & 0x02) != 0; // Bit 1 = PreambleDetect
111}
112
114 if (!this->is_preamble_detected()) {
115 this->preamble_hold_since_us_ = 0;
116 return false;
117 }
118 uint32_t const now = micros();
119 if (this->preamble_hold_since_us_ == 0) {
120 this->preamble_hold_since_us_ = now; // first sighting of this preamble
121 return true;
122 }
123 if (now - this->preamble_hold_since_us_ < SX1276_PREAMBLE_HOLD_US)
124 return true;
125 // No sync word followed within the longest start preamble: noise or a frame we cannot have. Let
126 // the hop go on; its receiver restart (change_frequency()) clears the flag.
127 this->preamble_hold_since_us_ = 0;
128 return false;
129}
130
132 if (!this->is_sync_detected()) {
133 this->sync_hold_since_us_ = 0;
134 return this->preamble_hold_();
135 }
136 this->preamble_hold_since_us_ = 0;
137 uint32_t const now = micros();
138 if (this->sync_hold_since_us_ == 0) {
139 this->sync_hold_since_us_ = now; // first sighting of this reception
140 return true;
141 }
142 if (now - this->sync_hold_since_us_ < RX_HOP_HOLDOFF_US)
143 return true;
144 // The bit has outlived any real frame. SyncAddressMatch is read-only in packet mode and clears
145 // only on leaving Rx or emptying the FIFO, and a hop does neither (change_frequency() writes FRF
146 // only) — so without this the stale bit would block hopping for good. Cycle out of Rx once,
147 // which is the clear the datasheet guarantees, then let the hop proceed. Costs the same
148 // standby->RX transition the other two radios pay on *every* hop, and only on this path.
149 this->set_mode_standby();
150 this->set_mode_rx();
151 this->sync_hold_since_us_ = 0;
152 return false;
153}
154
155// === Initialization ===
156
158 this->rst_pin_->setup();
159 this->dio0_pin_->setup();
160 // Attach DIO0 interrupt
161 this->dio0_pin_->attach_interrupt(&RadioSX1276::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
162 if (this->dio4_pin_ != nullptr)
163 this->dio4_pin_->setup();
164
165 // Hardware reset
166 this->reset_hardware_();
167
168 // Version check — SX1276 should return 0x12
169 if (this->read_register_(REG_VERSION) != 0x12) {
170 ESP_LOGE(TAG, "SX1276 not found");
171 this->fail_("SX1276 not found (version register is not 0x12) -- check SPI wiring and radio_type");
172 return false;
173 }
174
175 this->configure_radio_();
176 if (this->failed_)
177 return false;
178
179 ESP_LOGI(TAG, "SX1276 initialized");
180 return true;
181}
182
184 // This routine programs the SX1276 into FSK mode with the IoHomeOn feature enabled.
185 // IoHomeOn is critical: it enables hardware CRC (CCITT) and enforces the exact
186 // frame structure expected by the IO‑Homecontrol protocol. Without it, the radio
187 // would not recognize frames or compute correct CRCs. Every register value below
188 // has been validated against working Somfy/Velux captures and Semtech's recommended
189 // configuration for IoHomeOn. Do not change unless you have on‑air captures to
190 // prove compatibility.
191 this->write_register_(REG_OP_MODE, 0x00); // FSK + Sleep
192 delay(10);
193
194 // Frequency: channel 2 (868.95 MHz)
195 uint64_t const frf = ((uint64_t) FREQ_CH2 << 19) / FXOSC;
196 this->write_register_(REG_FRF_MSB, (uint8_t) ((frf >> 16) & 0xFF));
197 this->write_register_(REG_FRF_MID, (uint8_t) ((frf >> 8) & 0xFF));
198 this->write_register_(REG_FRF_LSB, (uint8_t) (frf & 0xFF));
199
200 this->set_mode_(MODE_STDBY);
201 this->run_image_cal_();
202 if (this->failed_)
203 return;
204 this->set_mode_(MODE_STDBY);
205
206 this->write_register_(REG_OSC, 0x07); // Clock out off
207 this->write_register_(REG_PACKET_CONFIG1,
208 0x90); // Variable len, CRC on, CCITT
209 // IoHomeOn is the crucial difference from a generic FSK setup: Semtech's SX1276 can
210 // speak the protocol natively enough to handle CRC and frame boundaries for us. This
211 // path serves as the baseline against which SX1262 captures are compared.
212 this->write_register_(REG_PACKET_CONFIG2, 0x70); // Packet mode, IoHomeOn, PowerFrame
213 this->write_register_(REG_SYNC_CONFIG, 0x50); // Auto restart PLL off, AA, sync on
214 // DIO0: PayloadReady/PacketSent. DIO2 is also mapped to SyncAddress here, but the component
215 // only exposes dio0_pin_/dio4_pin_ in its pin config — the component never wires or reads DIO2,
216 // so this mapping is inert. This chip's idle-hop gating (reception_in_progress()) reads
217 // SyncAddressMatch via SPI polling instead (see RadioSX1276::reception_in_progress()).
218 this->write_register_(REG_DIO_MAPPING1, 0x3D);
219 this->write_register_(REG_DIO_MAPPING2, 0xF1); // DIO4: PreambleDetect
220 this->write_register_(REG_PLLHOP, this->read_register_(REG_PLLHOP) | 0x80); // Fast hop
221 this->write_register_(REG_PA_RAMP, 0x2D); // Gaussian BT=1.0 shaping (0x20) | 15μs ramp (0x0D)
222 this->write_register_(REG_FIFO_THRESH, 0x80); // TX start FIFO not empty
223 this->write_register_(REG_PAYLOAD_LENGTH, 0xFF); // Max payload
224 this->write_register_(REG_RSSI_CONFIG, 0x02); // RSSI smoothing 8
225 this->write_register_(REG_RX_CONFIG, RX_CONFIG_VALUE); // Restart collision, AFC, AGC, preamble
226 this->write_register_(REG_AFC_FEI, 0x01); // AFC auto clear
227 this->write_register_(REG_LNA, 0x23); // Max gain, boost on
228 this->write_register_(REG_PREAMBLE_DETECT, 0xAA); // Detect on, 2 bytes, tol 10
229 // RX/AFC bandwidth: sourced from the runtime-tunable rx_bandwidth_ (default BW_41_7_KHZ = 0x13).
230 // Carson-rule bandwidth for 38400 bps + 19200 Hz deviation is ~77 kHz; the default is tighter
231 // than theoretical but maximizes sensitivity by rejecting out-of-band noise. Validated against
232 // real devices. See SX1276RxBandwidth for the selectable options.
233 this->set_rx_bandwidth_(this->rx_bandwidth_);
234
235 // Bitrate 38400 bps
236 uint32_t const br = FXOSC / 38400;
237 this->write_register_(REG_BITRATE_MSB, (br >> 8) & 0xFF);
238 this->write_register_(REG_BITRATE_LSB, br & 0xFF);
239
240 // Deviation 19200 Hz
241 auto fd = (uint32_t) ((19200.0F / FXOSC) * (1 << 19));
242 this->write_register_(REG_FDEV_MSB, (fd >> 8) & 0xFF);
243 this->write_register_(REG_FDEV_LSB, fd & 0xFF);
244
245 // PA config
246 if (this->uses_pa_boost_()) {
247 uint8_t p = std::max(this->tx_power_, (uint8_t) 2);
248 p = std::min(p, (uint8_t) 17);
249 this->write_register_(REG_PA_CONFIG, SX1276_PA_SELECT_PA_BOOST | (p - 2));
250 } else {
251 this->write_register_(REG_PA_CONFIG, std::min(this->tx_power_, (uint8_t) 14));
252 }
253 this->write_register_(0x0B, 0x3B); // OCP on, 240mA
254
255 // Preamble 1024 bytes default (changed per-packet)
256 this->write_register_(REG_PREAMBLE_MSB, 0x04);
257 this->write_register_(REG_PREAMBLE_LSB, 0x00);
258
259 // Sync word: 0x55, 0xFF, 0x33 in registers → on‑air `55 FF 33`. This is the real-Deal confirmed-working sync for
260 // IO‑Homecontrol with the SX1276. SyncSize=2 (REG_SYNC_CONFIG bits 2:0 = 0x2) matches the first 2 bytes {0x55, 0xFF}.
261 // The 3rd byte 0x33 is transmitted on‑air but not compared by the chip in SyncSize=2 mode.
262 uint8_t const sc = this->read_register_(REG_SYNC_CONFIG);
263 this->write_register_(REG_SYNC_CONFIG, (sc & 0xF8) | 0x02); // SyncSize=2: 2 matched bytes `55 FF`; 3rd `33` unused
264 this->write_register_(REG_SYNC_VALUE1, 0x55);
265 this->write_register_(REG_SYNC_VALUE1 + 1, 0xFF);
266 this->write_register_(REG_SYNC_VALUE1 + 2, 0x33);
267
268 this->set_mode_rx();
269}
270
272 this->rx_bandwidth_ = bandwidth;
273 // The enum value is the RegRxBw register byte; AFC bandwidth uses the same encoding.
274 this->write_register_(REG_RX_BW, static_cast<uint8_t>(bandwidth));
275 this->write_register_(REG_AFC_BW, static_cast<uint8_t>(bandwidth));
276}
277
278// === Packet TX/RX ===
279
280TxResult RadioSX1276::send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) {
281 if (len == 0 || len > RADIO_PACKET_BUFFER_SIZE)
282 return TxResult::failed();
283 // Measured in RX, before standby: the check needs the receiver running on the TX channel.
284 if (auto busy = this->channel_busy_level_(tx_config, SX1276_CCA_SETTLE_US))
285 return TxResult::channel_busy(*busy);
286#ifdef IOHOME_FRAME_LOG
287 log_frame("TX", data, len, tx_config.freq_hz, tx_config.preamble_len);
288#endif
289 this->set_mode_standby();
290 this->write_register_(REG_PREAMBLE_MSB, tx_config.preamble_len >> 8);
291 this->write_register_(REG_PREAMBLE_LSB, tx_config.preamble_len & 0xFF);
292 this->change_frequency(tx_config.freq_hz);
293
294 // Write data to FIFO
295 this->spi_->spi_enable();
296 this->spi_->spi_write(REG_FIFO | 0x80);
297 for (uint8_t i = 0; i < len; i++)
298 this->spi_->spi_transfer(data[i]);
299 this->spi_->spi_disable();
300
301 this->clear_dio_fired();
302 uint8_t const opmode = this->read_register_(REG_OP_MODE);
303 this->write_register_(REG_OP_MODE, (opmode & 0xF8) | MODE_TX);
304
305 // Bounded by the frame's own air time plus a guard, so a chip that stopped transmitting costs a
306 // fraction of a second instead of blocking the loop.
307 uint32_t const timeout_ms = tx_done_timeout_ms(this->tx_air_time_us(len, tx_config));
308 uint32_t const start = millis();
309 while (!this->is_dio_fired()) {
310 if (millis() - start > timeout_ms) {
311 ESP_LOGE(TAG, "TX timeout after %" PRIu32 " ms — DIO0 never fired (RegOpMode 0x%02X, RegIrqFlags2 0x%02X)",
312 timeout_ms, this->read_register_(REG_OP_MODE), this->read_register_(REG_IRQ_FLAGS2));
313 this->set_mode_standby();
314 return TxResult::failed();
315 }
316 App.feed_wdt();
317 delayMicroseconds(100);
318 }
319
320 // Reset preamble to short for RX
321 this->write_register_(REG_PREAMBLE_MSB, 0x00);
322 this->write_register_(REG_PREAMBLE_LSB, 0x08);
323 this->set_mode_rx();
324 return TxResult::ok();
325}
326
327bool RadioSX1276::poll_until_payload_ready_(uint32_t timeout_ms, bool &saw_dio0, uint8_t &irq1, uint8_t &irq2) {
328 uint32_t const start = millis();
329 while (true) {
330 if (this->is_dio_fired()) {
331 saw_dio0 = true;
332 break;
333 }
334 irq1 = this->read_register_(REG_IRQ_FLAGS1);
335 irq2 = this->read_register_(REG_IRQ_FLAGS2);
336 if ((irq2 & 0x04) != 0)
337 break;
338 if (millis() - start > timeout_ms)
339 return false;
340 App.feed_wdt();
341 delay(1);
342 }
343 this->clear_dio_fired();
344 if (saw_dio0) {
345 irq1 = this->read_register_(REG_IRQ_FLAGS1);
346 irq2 = this->read_register_(REG_IRQ_FLAGS2);
347 }
348 return true;
349}
350
351uint8_t RadioSX1276::read_fifo_packet_(uint8_t *buf, uint8_t buf_size) {
352 uint8_t len = 0;
353 while (((this->read_register_(REG_IRQ_FLAGS2) & 0x40) == 0) && len < buf_size)
354 buf[len++] = this->read_register_(REG_FIFO);
355 return len;
356}
357
358bool RadioSX1276::wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) {
359 this->prepare_blocking_receive_(packet);
360 this->clear_dio_fired();
361
362 bool saw_dio0 = false;
363 uint8_t irq1 = 0;
364 uint8_t irq2 = 0;
365 if (!this->poll_until_payload_ready_(timeout_ms, saw_dio0, irq1, irq2))
366 return false;
367
368 uint8_t const rssi = this->read_register_(REG_RSSI_VALUE);
369 if ((irq2 & 0x04) == 0) {
370 this->fill_capture_info_(true, irq1, irq2, rssi, nullptr, 0, nullptr, 0);
371 return false;
372 }
373
374 packet.len = this->read_fifo_packet_(packet.data, sizeof(packet.data));
375 packet.freq_hz = this->current_freq_;
376 this->fill_capture_info_(true, irq1, irq2, rssi, packet.data, packet.len, packet.data, packet.len);
377#ifdef IOHOME_FRAME_LOG
378 if (packet.len > 0)
379 log_frame("RX", packet.data, packet.len, this->current_freq_);
380#endif
381 return packet.len > 0;
382}
383
385 if (!this->is_dio_fired())
386 return false;
387 this->prepare_nonblocking_receive_(packet);
388
389 uint8_t const irq1 = this->read_register_(REG_IRQ_FLAGS1);
390 uint8_t const irq2 = this->read_register_(REG_IRQ_FLAGS2);
391 uint8_t const rssi = this->read_register_(REG_RSSI_VALUE);
392 if ((irq2 & 0x04) != 0) {
393 packet.len = this->read_fifo_packet_(packet.data, sizeof(packet.data));
394 packet.freq_hz = this->current_freq_;
395 this->fill_capture_info_(false, irq1, irq2, rssi, packet.data, packet.len, packet.data, packet.len);
396#ifdef IOHOME_FRAME_LOG
397 if (packet.len > 0)
398 log_frame("RX", packet.data, packet.len, this->current_freq_);
399#endif
400 return packet.len > 0;
401 }
402 this->fill_capture_info_(false, irq1, irq2, rssi, nullptr, 0, nullptr, 0);
403 if ((irq2 & 0x10) != 0) { // FifoOverrun — clear it
404 this->write_register_(REG_IRQ_FLAGS2, 0x10);
405 }
406 return false;
407}
408
410 ESP_LOGCONFIG(TAG, " SX1276 Diagnostic:");
411 ESP_LOGCONFIG(TAG, " PA output: %s", this->uses_pa_boost_() ? "PA_BOOST" : "RFO");
412 ESP_LOGCONFIG(TAG, " Opmode=0x%02X irq1=0x%02X irq2=0x%02X freq=%" PRIu32, this->read_register_(REG_OP_MODE),
413 this->read_register_(REG_IRQ_FLAGS1), this->read_register_(REG_IRQ_FLAGS2), this->current_freq_);
414}
415
416} // namespace home_io_control
417} // namespace esphome
418
419// NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
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 reset_hardware_()
Shared hardware reset sequence for chips with an active-low RST pin.
bool is_dio_fired() const
Set by the ISR when DIO fires.
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().
void fail_(const char *reason)
Latch the failed state and record why, so is_failed and failure_reason can never disagree.
void prepare_nonblocking_receive_(RadioRxPacket &packet)
Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
bool failed_
Latched by fail_; see is_failed.
void prepare_blocking_receive_(RadioRxPacket &packet)
Common preamble for blocking receive: clear diagnostics and output packet.
void configure_radio_()
Perform full radio configuration (called during init).
uint32_t sync_hold_since_us_
First-sighting timestamp for reception_in_progress()'s un-stick bound.
int16_t read_rssi() override
Read instantaneous RSSI (dBm) while in RX mode (used for LBT).
bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override
Blocking wait for a packet with timeout.
uint8_t read_register_(uint8_t reg)
Read an SX1276 register over SPI.
SX1276RxBandwidth rx_bandwidth_
Runtime-tunable RX bandwidth.
void fill_capture_info_(bool blocking_wait, uint8_t irq1, uint8_t irq2, uint8_t rssi, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len)
Populate last_capture_ from raw telemetry.
uint32_t preamble_hold_since_us_
First-sighting timestamp for reception_in_progress()'s preamble hold.
void set_rx_bandwidth_(SX1276RxBandwidth bandwidth)
Apply the RX bandwidth selector to REG_RX_BW and REG_AFC_BW.
void set_mode_(uint8_t mode)
Set the operating mode (sleep/standby/tx/rx) and wait for mode completion.
RadioSX1276(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio0_pin, InternalGPIOPin *dio4_pin, uint8_t tx_power, uint8_t pa_pin)
void set_mode_standby() override
Switch radio into standby mode.
static void gpio_intr(RadioSX1276 *arg)
DIO0 ISR — sets dio_fired flag. Runs in interrupt context.
bool poll_until_payload_ready_(uint32_t timeout_ms, bool &saw_dio0, uint8_t &irq1, uint8_t &irq2)
Wait until FIFO payload is ready (polling for TX/RX readiness).
void dump_debug() override
Dump radio‑specific debug info to log.
void change_frequency(uint32_t freq_hz) override
Change RF frequency using fast hop (no standby needed).
bool init() override
Initialize the SX1276 radio (reset, calibrate, configure registers).
bool reception_in_progress() override
True while a frame is arriving on the current channel and retuning would destroy it.
void write_register_(uint8_t reg, uint8_t value)
Write an SX1276 register over SPI.
bool uses_pa_boost_() const
True when TX goes out through PA_BOOST rather than RFO; the PA config and the dump both key on it.
void set_mode_rx() override
Switch radio into continuous receive mode.
bool is_sync_detected() override
Check if sync word has been detected (while in RX).
uint8_t read_fifo_packet_(uint8_t *buf, uint8_t buf_size)
Read a packet from the RX FIFO into a buffer.
bool preamble_hold_()
The PreambleDetect half of reception_in_progress(): true while a fresh preamble detection is younger ...
uint32_t tx_air_time_us(uint8_t len, const RadioTxConfig &cfg) const override
Transmit time (SX1276): the preamble, sync word and the IoHomeOn-coded frame at the protocol line rat...
bool check_for_packet(RadioRxPacket &packet) override
Non‑blocking check for a received packet (called from loop).
TxResult send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override
Transmit a frame with specified frequency and preamble.
void run_image_cal_()
Run image calibration routine (required after reset).
bool is_preamble_detected() override
Check if preamble has been detected (while in RX).
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).
Shared frame logging helpers for IO-Homecontrol.
constexpr uint32_t RX_HOP_HOLDOFF_US
Longest a frame arriving on the current channel may hold off an idle-path channel hop,...
static constexpr const char * TAG
PollResult poll_until(Ready ready, uint32_t timeout_ms)
Poll ready() until it is true or timeout_ms has passed, feeding the watchdog between samples.
Definition radio_poll.h:40
constexpr uint32_t tx_done_timeout_ms(uint32_t air_time_us)
How long a driver waits for TX-done after starting a transmission.
SX1276RxBandwidth
Valid SX1276 RX bandwidth options (RegRxBw register bytes).
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
static const char *const TAG
Preemption-safe "poll a condition until it holds or a deadline passes" helper.
SX1276 radio driver for IO-Homecontrol.
bool ready
The condition held on the deciding sample.
Definition radio_poll.h:23
uint8_t irq_flags2
IRQ flags group 2 (chip-specific, includes CRC flag).
bool crc_error
True if a CRC error was detected.
uint8_t reported_len
Length reported by the radio chip.
bool rx_done
True if RxDone IRQ fired.
uint8_t irq_flags1
IRQ flags group 1 (chip-specific).
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.
What send_packet() did with a frame.
static TxResult channel_busy(int16_t level_dbm)