Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_sx1262.cpp
Go to the documentation of this file.
1/// @file radio_sx1262.cpp
2/// @brief SX1262 radio driver implementation for IO-Homecontrol.
3///
4/// Unlike the SX1276, the SX1262 does not provide Semtech's IoHomeOn mode for
5/// IO-Homecontrol. On SX1276 that mode handles key protocol details in hardware:
6/// CRC generation and checking, packet boundary handling, and delivery of
7/// already-decoded protocol bytes in the FIFO. On SX1262 those pieces have to
8/// be reproduced in software on top of generic GFSK support.
9///
10/// Concretely, this driver has to:
11/// - append and verify the IO-Homecontrol CRC in software,
12/// - UART-pack frames for TX and recover UART-packed on-air bytes on RX,
13/// - detect plausible frame boundaries before handing bytes to the parser,
14/// - preserve raw capture data and metadata for debugging against the SX1276
15/// baseline capture path.
16///
17/// The chip interface itself is also different: SX1262 uses opcode-based SPI
18/// instead of the SX1276 register model, and every transaction must respect the
19/// BUSY line. For experiment builds, the RX path prioritizes preserving the
20/// chip-reported bytes and metadata verbatim.
21
22// The SX1262 path is intentionally low-level: opcode payloads, line-coding widths, and recovery
23// thresholds are written in the same shape as the chip protocol and on-air framing.
24// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
25
26#include "radio_sx1262.h"
27#include "log_frame.h"
28#include "esphome/core/log.h"
29#include "esphome/core/application.h"
30
31namespace esphome {
32namespace home_io_control {
33
34static const char *const TAG = "home_io_control.sx1262";
35static const uint8_t SX1262_SYNC_WORD_PARAM_24_BITS = 0x18;
36// Fixed probe length chosen from captures of 23-25 byte protocol frames after UART packing
37// and CRC appending. 32 bytes is large enough to preserve complete traffic without relying on
38// the chip's variable-length engine, which consistently truncated the useful payload.
39static const uint8_t SX1262_RX_PROBE_PACKET_LEN = 32;
40/// Maximum bit offset to search for valid UART decode start position.
41/// The UART frame is 10 bits (start + 8 data). If the sync word is not aligned,
42/// we probe up to 10 bits offset to recover the correct framing.
43static const uint8_t UART_PROBE_MAX_BIT_OFFSET = 10;
44
45/// Extract a single bit (MSB‑first) from a byte buffer.
46/// Used by UART decoding to scan raw radio samples.
47/// @param data Input byte buffer.
48/// @param bit_pos Global bit index within buffer.
49/// @return The bit value (0 or 1).
50static uint8_t get_bit_msb(const uint8_t *data, uint16_t bit_pos) {
51 return (data[bit_pos / 8] >> (7 - (bit_pos % 8))) & 0x01;
52}
53
54/// Decode a raw UART‑encoded bitstream into bytes.
55/// IO‑Homecontrol uses a UART‑like encoding over the air: each byte is represented
56/// by a 10‑bit sequence (start bit 0, 8 data bits LSB‑first, stop bit 1). This
57/// function slides a window across the raw bitstream and attempts to recover the
58/// original bytes. It stops when the sync pattern (0 followed by 1) is not found.
59/// @param raw Raw bytes from the radio buffer.
60/// @param raw_len Number of raw bytes available.
61/// @param bit_offset Initial bit position to start decoding (probe offset).
62/// @param decoded Output buffer for decoded bytes.
63/// @param decoded_max_len Capacity of decoded buffer.
64/// @return Number of bytes successfully decoded.
65static uint8_t decode_uart_probe(const uint8_t *raw, uint8_t raw_len, uint8_t bit_offset, uint8_t *decoded,
66 uint8_t decoded_max_len) {
67 // Bit numbering: we read MSB-first across byte boundaries. The UART frame structure
68 // within the bitstream is: start(0), data0, data1, ..., data7, stop(1). Byte values
69 // are LSB-first within the 8 data bits (bit 0 arrives first after start).
70 // We verify the start bit is 0 and stop bit is 1; if not, the probe offset is wrong.
71 uint16_t bit_pos = bit_offset;
72 uint16_t const total_bits = raw_len * 8;
73 uint8_t decoded_len = 0;
74
75 while (bit_pos + 10 <= total_bits && decoded_len < decoded_max_len) {
76 if (get_bit_msb(raw, bit_pos) != 0 || get_bit_msb(raw, bit_pos + 9) != 1)
77 break;
78
79 uint8_t value = 0;
80 for (uint8_t index = 0; index < 8; index++)
81 value |= get_bit_msb(raw, bit_pos + 1 + index) << index;
82
83 decoded[decoded_len++] = value;
84 bit_pos += 10;
85 }
86
87 return decoded_len;
88}
89
90/// @brief Result of the UART probe: best candidate frame within a raw capture.
92 bool valid{false}; ///< A plausible frame was found.
93 uint8_t bit_offset{0}; ///< Bit offset where the best decode started.
94 uint8_t decoded_len{0}; ///< Total number of bytes decoded at that offset.
95 uint8_t frame_start{0}; ///< Index into decoded buffer where the frame begins.
96 uint8_t frame_len{0}; ///< Length of the candidate IoFrame (decoded bytes).
97 uint8_t decoded[RADIO_PACKET_BUFFER_SIZE]{}; ///< Full decoded UART stream at the chosen offset.
98};
99
100/// @brief Check if a command ID is one of the known IO‑Homecontrol commands.
101/// @param cmd Command byte.
102/// @return true if cmd matches a known command constant.
103static bool is_known_io_command(uint8_t cmd) {
104 switch (cmd) {
105 case CMD_EXECUTE:
106 case CMD_PRIVATE:
107 case CMD_PRIVATE_RESP:
108 case CMD_DISCOVER_REQ:
114 case CMD_KEY_INIT:
115 case CMD_KEY_TRANSFER:
116 case CMD_KEY_CONFIRM:
119 case CMD_GET_NAME:
121 case CMD_GET_INFO2:
123 case CMD_SET_CONFIG1:
127 case CMD_ERROR_RESP:
128 return true;
129 default:
130 return false;
131 }
132}
133
134static bool is_plausible_uart_frame(const IoFrame &frame, uint8_t candidate_len) {
135 if (candidate_len < 15)
136 return false;
137 if (is_known_io_command(frame.cmd))
138 return true;
139 return (frame.ctrl0 & CTRL0_PROTOCOL_1W) != 0;
140}
141
142/// @brief Search a raw capture for the most plausible IoFrame using UART decoding.
143/// Probes multiple bit offsets and candidate lengths to find a valid parse that
144/// looks like a real IO‑Homecontrol frame.
145/// @param raw Pointer to raw radio buffer bytes.
146/// @param raw_len Number of bytes in raw.
147/// @return UartProbeResult with best candidate (may have valid=false if none found).
148static UartProbeResult find_uart_probe(const uint8_t *raw, uint8_t raw_len) {
149 // The SX1262 RX buffer contains the raw GFSK‑demodulated bits packed as bytes.
150 // Due to unknown bit alignment, we probe up to UART_PROBE_MAX_BIT_OFFSET (10) different
151 // starting positions. For each offset we attempt UART decoding; if decoding yields
152 // a plausible frame length (>= minimum) and contains a known command ID or indicates
153 // a 1W frame, we keep it as a candidate. The best (longest valid) candidate wins.
154 // This approach tolerates the SX1262's lack of IoHomeOn framing assistance.
155 UartProbeResult best{};
156
157 // Current captures consistently decode at bit_offset=0, but keeping a short probe window makes
158 // the recovery path robust against future boards or slightly different front-end timing.
159 for (uint8_t bit_offset = 0; bit_offset < UART_PROBE_MAX_BIT_OFFSET; bit_offset++) {
160 uint8_t decoded[RADIO_PACKET_BUFFER_SIZE] = {0};
161 uint8_t const decoded_len = decode_uart_probe(raw, raw_len, bit_offset, decoded, sizeof(decoded));
162 if (decoded_len == 0)
163 continue;
164
165 if (decoded_len > best.decoded_len) {
166 best.bit_offset = bit_offset;
167 best.decoded_len = decoded_len;
168 memcpy(best.decoded, decoded, decoded_len);
169 }
170
171 for (uint8_t start = 0; start < decoded_len; start++) {
172 uint8_t const max_candidate_len = std::min<uint8_t>(decoded_len - start, FRAME_MAX_SIZE);
173 for (int candidate_len = max_candidate_len; candidate_len >= FRAME_MIN_SIZE; candidate_len--) {
174 IoFrame frame;
175 if (!parse(decoded + start, candidate_len, frame))
176 continue;
177 if (!is_plausible_uart_frame(frame, candidate_len))
178 continue;
179
180 best.valid = true;
181 best.bit_offset = bit_offset;
182 best.decoded_len = decoded_len;
183 best.frame_start = start;
184 best.frame_len = candidate_len;
185 memcpy(best.decoded, decoded, decoded_len);
186 return best;
187 }
188 }
189 }
190
191 return best;
192}
193
194// === Software CRC-CCITT ===
195
196uint8_t RadioSX1262::uart_encode_packet(const uint8_t *data, uint8_t len, uint8_t *encoded, uint8_t encoded_max_len) {
197 if (len == 0 || encoded_max_len == 0)
198 return 0;
199
200 memset(encoded, 0, encoded_max_len);
201 uint16_t bit_pos = 0;
202 const uint16_t total_bits = len * 10;
203 if (((total_bits + 7) / 8) > encoded_max_len)
204 return 0;
205
206 auto write_bit = [encoded](uint16_t pos, uint8_t bit) {
207 if (bit != 0)
208 encoded[pos / 8] |= 1U << (7 - (pos % 8));
209 };
210
211 for (uint8_t byte_index = 0; byte_index < len; byte_index++) {
212 const uint8_t value = data[byte_index];
213
214 write_bit(bit_pos++, 0); // UART start bit
215 for (uint8_t bit_index = 0; bit_index < 8; bit_index++)
216 write_bit(bit_pos++, (value >> bit_index) & 0x01);
217 write_bit(bit_pos++, 1); // UART stop bit
218 }
219
220 return (total_bits + 7) / 8;
221}
222
223// === SPI Communication (opcode-based) ===
224
226 uint32_t const start = millis();
227 while (this->busy_pin_->digital_read()) {
228 if (millis() - start > 10) {
229 ESP_LOGE(TAG, "BUSY timeout");
230 this->failed_ = true;
231 return;
232 }
233 App.feed_wdt();
234 }
235}
236
237void RadioSX1262::write_opcode_(uint8_t opcode, const uint8_t *params, uint8_t len) {
238 this->wait_busy_();
239 this->spi_->spi_enable();
240 this->spi_->spi_transfer(opcode);
241 for (uint8_t i = 0; i < len; i++)
242 this->spi_->spi_transfer(params[i]);
243 this->spi_->spi_disable();
244}
245
246void RadioSX1262::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t len) {
247 this->wait_busy_();
248 this->spi_->spi_enable();
249 this->spi_->spi_transfer(opcode);
250 this->spi_->spi_transfer(0x00); // NOP — status byte
251 for (uint8_t i = 0; i < len; i++)
252 data[i] = this->spi_->spi_transfer(0x00);
253 this->spi_->spi_disable();
254}
255
257 uint8_t irq_raw[2] = {0};
258 this->read_opcode_(SX1262_GET_IRQ_STATUS, irq_raw, 2);
259 return (uint16_t) (((uint16_t) irq_raw[0] << 8) | irq_raw[1]);
260}
261
262// === wait_for_packet static helpers ===
263
264/// Poll for first radio activity (DIO1 interrupt or any IRQ status) within timeout.
265///
266/// Checks the DIO1 pin latch and the raw IRQ status register repeatedly until
267/// either activity is detected or the timeout expires. On timeout, clears the
268/// DIO latch and resets the RX state machine for the next receive cycle.
269bool RadioSX1262::poll_until_activity_(uint32_t start, uint32_t timeout_ms, bool &saw_dio1, uint16_t &irq) {
270 while (true) {
271 if (this->is_dio_fired()) {
272 saw_dio1 = true;
273 return true;
274 }
275 irq = this->read_irq_status_raw();
276 if (irq != 0)
277 return true;
278 if (millis() - start > timeout_ms) {
279 this->clear_dio_fired();
280 this->reset_rx_state_();
281 return false;
282 }
283 App.feed_wdt();
284 delay(1);
285 }
286}
287
288/// Resolve the SYNC_WORD_VALID → RX_DONE race condition.
289///
290/// On SX1262 the SYNC_WORD_VALID IRQ can assert before the packet is fully
291/// received. If we observe SYNC without RX_DONE, clear the sticky SYNC flag
292/// and spin until RX_DONE arrives or the remaining timeout elapses.
293bool RadioSX1262::resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint16_t &irq) {
294 // If RX_DONE already set or SYNC not set, nothing to resolve.
295 if ((irq & SX1262_IRQ_SYNC_WORD_VALID) == 0 || (irq & SX1262_IRQ_RX_DONE) != 0) {
296 return true;
297 }
298 // SYNC seen without RX_DONE — clear sticky SYNC and wait for RX_DONE.
300 while (millis() - start <= timeout_ms) {
301 if (!this->is_dio_fired()) {
302 irq = this->read_irq_status_raw();
303 if ((irq & SX1262_IRQ_RX_DONE) != 0)
304 return true;
305 App.feed_wdt();
306 delay(1);
307 continue;
308 }
309 this->clear_dio_fired();
310 irq = this->read_irq_status_raw();
311 if ((irq & SX1262_IRQ_RX_DONE) != 0)
312 return true;
313 if (irq != 0)
314 this->clear_irq_status_(irq);
315 }
316 return false; // timeout
317}
318
319/// Finalize receive: read the packet if RX_DONE is set, otherwise record failure.
320bool RadioSX1262::finalize_receive_(RadioRxPacket &packet, uint16_t irq) {
321 if ((irq & SX1262_IRQ_RX_DONE) == 0) {
322 this->fill_capture_info_(true, irq, 0, 0, nullptr, 0, nullptr, 0);
323 this->reset_rx_state_();
324 return false;
325 }
326 return this->read_rx_packet(packet, true, irq);
327}
328
329void RadioSX1262::write_register_(uint16_t addr, const uint8_t *data, uint8_t len) {
330 this->wait_busy_();
331 this->spi_->spi_enable();
332 this->spi_->spi_transfer(SX1262_WRITE_REGISTER);
333 this->spi_->spi_transfer((addr >> 8) & 0xFF); // Address MSB
334 this->spi_->spi_transfer(addr & 0xFF); // Address LSB
335 for (uint8_t i = 0; i < len; i++)
336 this->spi_->spi_transfer(data[i]);
337 this->spi_->spi_disable();
338}
339
340void RadioSX1262::read_register_(uint16_t addr, uint8_t *data, uint8_t len) {
341 this->wait_busy_();
342 this->spi_->spi_enable();
343 this->spi_->spi_transfer(SX1262_READ_REGISTER);
344 this->spi_->spi_transfer((addr >> 8) & 0xFF); // Address MSB
345 this->spi_->spi_transfer(addr & 0xFF); // Address LSB
346 this->spi_->spi_transfer(0x00); // NOP — status byte
347 for (uint8_t i = 0; i < len; i++)
348 data[i] = this->spi_->spi_transfer(0x00);
349 this->spi_->spi_disable();
350}
351
352void RadioSX1262::write_buffer_(uint8_t offset, const uint8_t *data, uint8_t len) {
353 this->wait_busy_();
354 this->spi_->spi_enable();
355 this->spi_->spi_transfer(SX1262_WRITE_BUFFER);
356 this->spi_->spi_transfer(offset);
357 for (uint8_t i = 0; i < len; i++)
358 this->spi_->spi_transfer(data[i]);
359 this->spi_->spi_disable();
360}
361
362void RadioSX1262::read_buffer_(uint8_t offset, uint8_t *data, uint8_t len) {
363 this->wait_busy_();
364 this->spi_->spi_enable();
365 this->spi_->spi_transfer(SX1262_READ_BUFFER);
366 this->spi_->spi_transfer(offset);
367 this->spi_->spi_transfer(0x00); // NOP — status byte
368 for (uint8_t i = 0; i < len; i++)
369 data[i] = this->spi_->spi_transfer(0x00);
370 this->spi_->spi_disable();
371}
372
373// === Packet params helper ===
374
375void RadioSX1262::set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type,
376 uint8_t crc_type) {
377 uint8_t params[9] = {
378 (uint8_t) (preamble_len >> 8), // Preamble length MSB
379 (uint8_t) (preamble_len), // Preamble length LSB
380 0x04, // Preamble detector: 8 bits (1 byte)
381 SX1262_SYNC_WORD_PARAM_24_BITS, // Sync word length: 24 bits (3 bytes)
382 0x00, // Address comparison: off
383 packet_type, // GFSK packet type: known length or variable size
384 payload_len, // Configured payload length
385 crc_type, // CRC type: SX1262 GFSK CRC mode
386 0x00, // Whitening: off
387 };
388 this->write_opcode_(SX1262_SET_PACKET_PARAMS, params, sizeof(params));
389}
390
392 // The variable-size packet engine recovers a stable 15-byte boundary, but that boundary is
393 // too short for a full software UART decode and buffer reads past it are not trustworthy.
394 // Probe again with a fixed raw packet size that matches typical UART-packed 23-25 byte
395 // IO-homecontrol frames: ceil(25 * 10 / 8) = 32 bytes.
397}
398
399void RadioSX1262::clear_irq_status_(uint16_t irq_mask) {
400 uint8_t clear_irq[2] = {
401 (uint8_t) ((irq_mask >> 8) & 0xFF),
402 (uint8_t) (irq_mask & 0xFF),
403 };
404 this->write_opcode_(SX1262_CLEAR_IRQ_STATUS, clear_irq, sizeof(clear_irq));
405}
406
408 uint8_t errors_raw[2] = {0};
409 this->read_opcode_(SX1262_GET_DEVICE_ERRORS, errors_raw, sizeof(errors_raw));
410 return (uint16_t) (((uint16_t) errors_raw[0] << 8) | errors_raw[1]);
411}
412
414 uint8_t clear_errors[2] = {0x00, 0x00};
415 this->write_opcode_(SX1262_CLEAR_DEVICE_ERRORS, clear_errors, sizeof(clear_errors));
416}
417
418void RadioSX1262::reset_rx_state_(bool force_standby) {
419 uint8_t buf_base[2] = {0x00, 0x80};
420 if (force_standby)
421 this->set_mode_standby();
422 this->clear_irq_status_(0xFFFF);
423 this->write_opcode_(SX1262_SET_BUFFER_BASE_ADDRESS, buf_base, sizeof(buf_base));
424 this->set_rx_packet_params_();
425 this->set_mode_rx();
426}
427
428void RadioSX1262::fill_capture_info_(bool blocking_wait, uint16_t irq_status, uint8_t rx_offset, uint8_t reported_len,
429 const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len) {
430 uint8_t packet_status[3] = {0};
431 this->read_opcode_(SX1262_GET_PACKET_STATUS, packet_status, sizeof(packet_status));
432
433 this->populate_capture_base_(blocking_wait, this->current_freq_, -(int16_t) packet_status[1] / 2, raw, raw_len, frame,
434 frame_len);
435 this->last_capture_.rx_done = (irq_status & SX1262_IRQ_RX_DONE) != 0;
436 this->last_capture_.crc_error = (irq_status & SX1262_IRQ_CRC_ERR) != 0;
437 this->last_capture_.irq_status = irq_status;
438 this->last_capture_.packet_status = packet_status[0];
439 this->last_capture_.rx_offset = rx_offset;
440 this->last_capture_.reported_len = reported_len;
441}
442
443// === ISR ===
444
446
447// === Initialization ===
448
450 // --- Pin setup ---
451 this->rst_pin_->setup();
452 this->dio1_pin_->setup();
453 this->busy_pin_->setup();
454
455 // Front-end module pins (e.g., Heltec V4)
456 if (this->fem_en_pin_ != nullptr) {
457 this->fem_en_pin_->setup();
458 this->fem_en_pin_->digital_write(true);
459 }
460 if (this->vfem_pin_ != nullptr) {
461 this->vfem_pin_->setup();
462 this->vfem_pin_->digital_write(true);
463 }
464 if (this->fem_pa_pin_ != nullptr) {
465 this->fem_pa_pin_->setup();
466 this->fem_pa_pin_->digital_write(true);
467 }
468
469 // --- Hardware reset ---
470 this->reset_hardware_();
471 this->wait_busy_();
472 if (this->failed_)
473 return false;
474
475 this->configure_radio_();
476 if (this->failed_)
477 return false;
478
479 ESP_LOGI(TAG, "SX1262 initialized");
480 return true;
481}
482
484 this->wait_busy_();
485 this->spi_->spi_enable();
486 uint8_t const chip_status = this->spi_->spi_transfer(SX1262_GET_STATUS);
487 this->spi_->spi_transfer(0x00);
488 this->spi_->spi_disable();
489
490 uint8_t const chip_mode = (chip_status >> 4) & 0x07;
491 uint8_t const cmd_status = (chip_status >> 1) & 0x07;
492 const char *mode_str = "?";
493 switch (chip_mode) {
494 case 2:
495 mode_str = "STDBY_RC";
496 break;
497 case 3:
498 mode_str = "STDBY_XOSC";
499 break;
500 case 4:
501 mode_str = "FS";
502 break;
503 case 5:
504 mode_str = "RX";
505 break;
506 case 6:
507 mode_str = "TX";
508 break;
509 default:
510 break;
511 }
512
513 uint8_t sync[3];
514 this->read_register_(SX1262_REG_SYNC_WORD, sync, 3);
515
516 uint8_t irq_raw[2];
517 this->read_opcode_(SX1262_GET_IRQ_STATUS, irq_raw, 2);
518 uint16_t const irq = ((uint16_t) irq_raw[0] << 8) | irq_raw[1];
519 uint16_t const errors = this->get_device_errors_();
520
521 ESP_LOGCONFIG(TAG, " SX1262 Diagnostic:");
522 ESP_LOGCONFIG(TAG, " Chip status: 0x%02X (mode=%s, cmd=%u)", chip_status, mode_str, cmd_status);
523 ESP_LOGCONFIG(TAG, " BUSY=%d DIO1=%d", this->busy_pin_->digital_read(), this->dio1_pin_->digital_read());
524 ESP_LOGCONFIG(TAG, " Sync word: %02X %02X %02X (expect 57 FD 99)", sync[0], sync[1], sync[2]);
525 ESP_LOGCONFIG(TAG, " IRQ status: 0x%04X", irq);
526 ESP_LOGCONFIG(TAG, " Device errors: 0x%04X", errors);
527}
528
530 // 1. Standby on RC oscillator (safe starting point)
531 uint8_t const stdby_rc = 0x00;
532 this->write_opcode_(SX1262_SET_STANDBY, &stdby_rc, 1);
533
534 // 2. Configure TCXO via DIO3 — voltage + 5ms timeout (320 ticks at 15.625us/tick)
535 uint8_t tcxo_params[4] = {this->tcxo_voltage_, 0x00, 0x01, 0x40};
536 this->write_opcode_(SX1262_SET_DIO3_AS_TCXO_CTRL, tcxo_params, sizeof(tcxo_params));
537
538 // 3. Calibrate all blocks
539 uint8_t const cal = 0x7F;
540 this->write_opcode_(SX1262_CALIBRATE, &cal, 1);
541 delay(5); // Wait for calibration to complete
542
543 // 4. Standby on XOSC (TCXO now running)
544 uint8_t const stdby_xosc = 0x01;
545 this->write_opcode_(SX1262_SET_STANDBY, &stdby_xosc, 1);
546
547 // 5. Use DC-DC regulator for better efficiency
548 uint8_t const reg_mode = 0x01;
549 this->write_opcode_(SX1262_SET_REGULATOR_MODE, &reg_mode, 1);
550
551 // 6. DIO2 as RF switch control (for boards with integrated RF switch)
552 uint8_t const dio2_rf = 0x01;
554
555 // 6b. Keep the crystal path alive after RX/TX completion instead of relying on the chip default.
556 uint8_t const fallback_mode = SX1262_FALLBACK_STDBY_XOSC;
557 this->write_opcode_(SX1262_SET_RX_TX_FALLBACK_MODE, &fallback_mode, 1);
558
559 // 7. FSK packet type
560 uint8_t const pkt_type = 0x00;
561 this->write_opcode_(SX1262_SET_PACKET_TYPE, &pkt_type, 1);
562
563 // 8. Set frequency to channel 2 (868.95 MHz)
565
566 // 9. Calibrate image for 863-870 MHz band
567 uint8_t cal_img[2] = {0xD7, 0xDB};
568 this->write_opcode_(SX1262_CALIBRATE_IMAGE, cal_img, sizeof(cal_img));
569
570 // 9b. Use boosted RX gain for maximum sensitivity.
571 uint8_t const rx_gain = 0x96;
572 this->write_register_(SX1262_REG_RX_GAIN, &rx_gain, 1);
573
574 // 10. FSK modulation params:
575 // BitRate = 32 * Fxosc / BR_reg → BR_reg = 32 * 32MHz / 38400 = 26667 = 0x00682B
576 // Pulse shape: no shaping (0x00)
577 // Bandwidth: 312.0 kHz (0x19) — wider than needed but safe margin
578 // Fdev = fdev_hz * 2^25 / 32e6 → 19200 * 2^25 / 32e6 = 20133 = 0x004EA5
579 uint8_t mod_params[8] = {
580 0x00, 0x68, 0x2B, // Bitrate: 38400 bps
581 0x00, // Pulse shape: no shaping
582 0x19, // Bandwidth: 312.0 kHz
583 0x00, 0x4E, 0xA5, // Fdev: 19200 Hz
584 };
585 this->write_opcode_(SX1262_SET_MODULATION_PARAMS, mod_params, sizeof(mod_params));
586
587 // 11. Default RX packet params: variable-size GFSK with hardware CCITT CRC validation.
588 this->set_rx_packet_params_();
589
590 // 12. Sync word: 0x57 0xFD 0x99 (24-bit UART-derived IO-homecontrol hypothesis)
591 uint8_t sync_word[8] = {0x57, 0xFD, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00};
592 this->write_register_(SX1262_REG_SYNC_WORD, sync_word, sizeof(sync_word));
593
594 // 12b. CRC registers are configured for potential future hardware-CRC use, but RX uses
595 // CRC_OFF because the UART encoding makes hardware CRC checking impossible — the chip
596 // sees UART-packed bits, not raw protocol bytes.
597 uint8_t crc_init[2] = {0x1D, 0x0F};
598 this->write_register_(0x06BC, crc_init, 2);
599 uint8_t crc_poly[2] = {0x10, 0x21};
600 this->write_register_(0x06BE, crc_poly, 2);
601
602 // 13. Buffer base addresses: TX at 0x00, RX at 0x80
603 uint8_t buf_base[2] = {0x00, 0x80};
604 this->write_opcode_(SX1262_SET_BUFFER_BASE_ADDRESS, buf_base, sizeof(buf_base));
605
606 // 14. PA config: SX1262 high power PA (paDutyCycle=0x04, hpMax=0x07, deviceSel=0x00=SX1262, paLut=0x01)
607 uint8_t pa_config[4] = {0x04, 0x07, 0x00, 0x01};
608 this->write_opcode_(SX1262_SET_PA_CONFIG, pa_config, sizeof(pa_config));
609
610 // 14b. Apply Semtech's SX1262 clamp workaround for better tolerance of RF mismatch.
611 uint8_t tx_clamp = 0;
612 this->read_register_(SX1262_REG_TX_CLAMP_CONFIG, &tx_clamp, 1);
613 tx_clamp |= 0x1E;
614 this->write_register_(SX1262_REG_TX_CLAMP_CONFIG, &tx_clamp, 1);
615
616 // 15. TX params: power in dBm (SX1262 accepts -9 to +22 directly), ramp 200us (0x04)
617 int8_t const power = std::max((int8_t) -9, std::min((int8_t) 22, (int8_t) this->tx_power_));
618 uint8_t tx_params[2] = {(uint8_t) power, 0x04};
619 this->write_opcode_(SX1262_SET_TX_PARAMS, tx_params, sizeof(tx_params));
620
621 // 16. IRQ config: map TxDone + RxDone + SyncWordValid + CrcErr to DIO1
622 uint8_t irq_params[8] = {
623 0x00, 0x4B, // irqMask: TxDone(0x0001) | RxDone(0x0002) | SyncWordValid(0x0008) | CrcErr(0x0040)
624 0x00, 0x4B, // dio1Mask: same
625 0x00, 0x00, // dio2Mask: none
626 0x00, 0x00, // dio3Mask: none
627 };
628 this->write_opcode_(SX1262_SET_DIO_IRQ_PARAMS, irq_params, sizeof(irq_params));
629
630 // 17. Attach DIO1 interrupt
631 this->dio1_pin_->attach_interrupt(&RadioSX1262::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
632
633 // 18. Clear any pending IRQs
634 this->clear_irq_status_(0xFFFF);
635 this->clear_device_errors_();
636
637 // 19. Enter continuous receive
638 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF}; // 0xFFFFFF = continuous
639 this->write_opcode_(SX1262_SET_RX, rx_continuous, sizeof(rx_continuous));
640}
641
642// === Mode control ===
643
645 uint8_t const stdby = 0x01; // STDBY_XOSC
646 this->write_opcode_(SX1262_SET_STANDBY, &stdby, 1);
647}
648
650 uint8_t rx_continuous[3] = {0xFF, 0xFF, 0xFF};
651 this->write_opcode_(SX1262_SET_RX, rx_continuous, sizeof(rx_continuous));
652}
653
654// === Frequency control ===
655
657 auto freq_reg = (uint32_t) ((double) freq_hz * (1 << 25) / 32e6);
658 uint8_t params[4] = {
659 (uint8_t) (freq_reg >> 24),
660 (uint8_t) (freq_reg >> 16),
661 (uint8_t) (freq_reg >> 8),
662 (uint8_t) freq_reg,
663 };
664 this->write_opcode_(SX1262_SET_RF_FREQUENCY, params, sizeof(params));
665 this->current_freq_ = freq_hz;
666}
667
668void RadioSX1262::change_frequency(uint32_t freq_hz) {
669 this->set_mode_standby();
670 this->set_frequency_register_(freq_hz);
671 this->set_mode_rx();
672}
673
675 uint8_t raw = 0;
676 this->read_opcode_(SX1262_GET_RSSI_INST, &raw, 1);
677 return -(int16_t) raw / 2;
678}
679
680// === Packet TX ===
681
682bool RadioSX1262::send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) {
683 if (len == 0)
684 return false;
685
686#ifdef IOHOME_FRAME_LOG
687 log_frame("TX", data, len, tx_config.freq_hz, tx_config.preamble_len);
688#endif
689
690 this->set_mode_standby();
691
692 // Set frequency (already in standby, no need for full change_frequency cycle)
693 this->set_frequency_register_(tx_config.freq_hz);
694
695 uint8_t frame_with_crc[FRAME_MAX_SIZE + 2] = {0};
696 uint8_t tx_buf[RADIO_PACKET_BUFFER_SIZE];
697 if ((uint16_t) len + 2 > (uint16_t) sizeof(frame_with_crc))
698 return false;
699
700 memcpy(frame_with_crc, data, len);
701 const uint16_t crc = crc_ccitt(data, len);
702 frame_with_crc[len] = crc & 0xFF;
703 frame_with_crc[len + 1] = (crc >> 8) & 0xFF;
704
705 const uint8_t encoded_len = uart_encode_packet(frame_with_crc, len + 2, tx_buf, sizeof(tx_buf));
706 if (encoded_len == 0)
707 return false;
708
711
712 // Clear IRQs and write to TX buffer at offset 0
713 this->clear_irq_status_(0xFFFF);
714 this->write_buffer_(0x00, tx_buf, encoded_len);
715
716 // Start TX with 4s timeout (256000 ticks at 15.625us/tick = 0x03E800)
717 this->clear_dio_fired();
718 uint8_t tx_timeout[3] = {0x03, 0xE8, 0x00};
719 this->write_opcode_(SX1262_SET_TX, tx_timeout, sizeof(tx_timeout));
720
721 auto read_irq_status = [this]() {
722 uint8_t irq_raw[2] = {0};
723 this->read_opcode_(SX1262_GET_IRQ_STATUS, irq_raw, sizeof(irq_raw));
724 return (uint16_t) (((uint16_t) irq_raw[0] << 8) | irq_raw[1]);
725 };
726
727 // Wait for an actual TxDone IRQ. DIO1 is shared with RX-related events, so
728 // a stale or unrelated interrupt must not be treated as TX completion.
729 uint32_t const start = millis();
730 uint16_t tx_irq = 0;
731 while (true) {
732 if (!this->is_dio_fired()) {
733 if (millis() - start > 4000) {
734 ESP_LOGE(TAG, "TX timeout — DIO1 never fired");
735 this->set_mode_standby();
736 return false;
737 }
738 App.feed_wdt();
739 delayMicroseconds(100);
740 continue;
741 }
742
743 this->clear_dio_fired();
744 tx_irq = read_irq_status();
745 if ((tx_irq & SX1262_IRQ_TX_DONE) != 0)
746 break;
747
748 if (tx_irq != 0) {
749 this->clear_irq_status_(tx_irq);
750 }
751
752 if (millis() - start > 4000) {
753 ESP_LOGE(TAG, "TX timeout — no TX_DONE IRQ (last_irq=0x%04X)", tx_irq);
754 this->set_mode_standby();
755 return false;
756 }
757 }
758 // TxDone used the same DIO1 latch as RX. Clear the local latch before
759 // re-arming RX so an immediate reply remains visible to wait_for_packet().
760 this->clear_dio_fired();
761
762 // Clear IRQs, restore default packet params, return to RX. TxDone already
763 // left the radio in fallback standby mode, so avoid an extra explicit
764 // standby command here.
765 this->clear_irq_status_(0xFFFF);
766 this->reset_rx_state_(false);
767 return true;
768}
769
770// === Packet RX (blocking) ===
771
772bool RadioSX1262::wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) {
773 // Blocking receive with timeout. Returns true if a packet was successfully received.
774 // This orchestrator decomposes the state machine into three low‑complexity helpers.
775 this->prepare_blocking_receive_(packet);
776
777 uint32_t const start = millis();
778 bool saw_dio1 = false;
779 uint16_t irq = 0;
780
781 // Phase 1: Wait for first activity (DIO interrupt or any IRQ status change).
782 if (!this->poll_until_activity_(start, timeout_ms, saw_dio1, irq)) {
783 return false;
784 }
785
786 // If DIO fired, refresh IRQ status to capture the reason bits.
787 if (saw_dio1) {
788 this->clear_dio_fired();
789 irq = this->read_irq_status_raw();
790 }
791
792 // Phase 2: Resolve the SYNC_WORD_VALID → RX_DONE race condition.
793 if (!this->resolve_sync_race_(start, timeout_ms, irq)) {
794 return false;
795 }
796
797 // Phase 3: Finalize — either read the packet or treat as a failure.
798 return this->finalize_receive_(packet, irq);
799}
800
801// === Shared RX helper ===
802
803bool RadioSX1262::read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint16_t irq_status) {
804 uint8_t rx_status[2] = {0};
805 uint8_t rx_buf[RADIO_PACKET_BUFFER_SIZE] = {0};
806 uint8_t recovered_buf[RADIO_PACKET_BUFFER_SIZE] = {0};
807
808 this->read_opcode_(SX1262_GET_RX_BUFFER_STATUS, rx_status, sizeof(rx_status));
809 uint8_t const reported_len = std::min(rx_status[0], (uint8_t) sizeof(rx_buf));
810 uint8_t const rx_offset = rx_status[1];
811 uint8_t raw_probe_len = reported_len;
812 if (reported_len > 0 && reported_len < 32) {
813 // When the SX1262 reports a short packet length, still pull the full raw window. Earlier
814 // bring-up on this chip showed that trimming this probe too aggressively makes the recovered
815 // post-auth response less reliable because the useful UART-packed tail may sit past the
816 // chip-reported boundary.
817 raw_probe_len = sizeof(rx_buf);
818 }
819 if (reported_len == SX1262_RX_PROBE_PACKET_LEN)
820 raw_probe_len = SX1262_RX_PROBE_PACKET_LEN;
821 if (raw_probe_len > 0)
822 this->read_buffer_(rx_offset, rx_buf, raw_probe_len);
823
824 // SX1262 does not expose the already-decoded IO-homecontrol frame the way SX1276 does. We first
825 // capture the raw bytes exactly as reported by the chip, then recover the UART-packed protocol
826 // stream in software and only pass a plausible frame up to the parser. This software recovery
827 // path is the SX1262-specific adaptation to the same protocol.
828 UartProbeResult probe = find_uart_probe(rx_buf, raw_probe_len);
829 if (probe.valid) {
830 memcpy(recovered_buf, probe.decoded + probe.frame_start, probe.frame_len);
831 memcpy(packet.data, recovered_buf, probe.frame_len);
832 packet.len = probe.frame_len;
833 } else {
834 uint8_t const copy_len = std::min(reported_len, FRAME_MAX_SIZE);
835 if (copy_len > 0)
836 memcpy(packet.data, rx_buf, copy_len);
837 packet.len = copy_len;
838 }
839 packet.freq_hz = this->current_freq_;
840 this->fill_capture_info_(blocking_wait, irq_status, rx_offset, reported_len, rx_buf, raw_probe_len, packet.data,
841 packet.len);
842
843#ifdef IOHOME_FRAME_LOG
844 if (packet.len > 0)
845 log_frame("RX", packet.data, packet.len, this->current_freq_);
846#endif
847 this->reset_rx_state_();
848 return packet.len > 0;
849}
850
851// === Packet RX (non-blocking) ===
852
854 if (!this->is_dio_fired())
855 return false;
856 this->prepare_nonblocking_receive_(packet);
857
858 uint8_t irq_raw[2];
859 this->read_opcode_(SX1262_GET_IRQ_STATUS, irq_raw, 2);
860 uint16_t const irq = ((uint16_t) irq_raw[0] << 8) | irq_raw[1];
861
862 if ((irq & SX1262_IRQ_SYNC_WORD_VALID) != 0 && (irq & SX1262_IRQ_RX_DONE) == 0) {
863 this->clear_irq_status_(SX1262_IRQ_SYNC_WORD_VALID);
864 return false;
865 }
866
867 if ((irq & SX1262_IRQ_RX_DONE) != 0) {
868 return this->read_rx_packet(packet, false, irq);
869 }
870
871 this->fill_capture_info_(false, irq, 0, 0, nullptr, 0, nullptr, 0);
872 this->reset_rx_state_();
873 return false;
874}
875
876} // namespace home_io_control
877} // namespace esphome
878
879// 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_()
Hardware reset sequence common to all SX chips.
bool is_dio_fired() const
Set by the ISR when DIO fires.
void prepare_nonblocking_receive_(RadioRxPacket &packet)
Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
void prepare_blocking_receive_(RadioRxPacket &packet)
Common preamble for blocking receive: clear diagnostics and output packet.
void wait_busy_()
Wait until BUSY pin is low before any SPI transaction.
void set_mode_rx() override
Switch to continuous receive mode.
int16_t read_rssi() override
Read instantaneous RSSI (in dBm) while in RX mode.
void write_opcode_(uint8_t opcode, const uint8_t *params, uint8_t len)
Write an opcode with optional parameter bytes.
void set_mode_standby() override
Switch to standby mode.
virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint16_t irq_status)
Read a received packet from the buffer and return the raw bytes reported by the chip.
RadioSX1262(SpiAccess *spi, InternalGPIOPin *rst_pin, InternalGPIOPin *dio1_pin, InternalGPIOPin *busy_pin, uint8_t tx_power, uint8_t tcxo_voltage, InternalGPIOPin *fem_en_pin=nullptr, InternalGPIOPin *vfem_pin=nullptr, InternalGPIOPin *fem_pa_pin=nullptr)
void change_frequency(uint32_t freq_hz) override
Change the carrier frequency using fast hop (no standby transition needed).
void write_buffer_(uint8_t offset, const uint8_t *data, uint8_t len)
Write into the SX1262 TX/RX buffer at a given offset.
void reset_rx_state_(bool force_standby=true)
Reset RX state machine and buffer.
void fill_capture_info_(bool blocking_wait, uint16_t irq_status, uint8_t rx_offset, uint8_t reported_len, const uint8_t *raw, uint8_t raw_len, const uint8_t *frame, uint8_t frame_len)
Populate the RadioCaptureInfo from SX1262‑specific telemetry.
void set_frequency_register_(uint32_t freq_hz)
Set RF frequency via the frequency register.
void clear_device_errors_()
Clear device error flags.
void write_register_(uint16_t addr, const uint8_t *data, uint8_t len)
Write to a register (SX1262 uses opcodes for register access).
void set_packet_params_(uint16_t preamble_len, uint8_t payload_len, uint8_t packet_type, uint8_t crc_type)
Configure packet parameters (preamble, payload length, CRC).
void dump_debug() override
Dump SX1262‑specific debug info.
void configure_radio_()
Full radio initialization (called from init()).
virtual uint16_t read_irq_status_raw()
Read the raw IRQ status from the radio.
static void gpio_intr(RadioSX1262 *arg)
DIO1 ISR — sets dio_fired flag. Runs in interrupt context.
bool check_for_packet(RadioRxPacket &packet) override
Non-blocking check for a received packet.
static uint8_t uart_encode_packet(const uint8_t *data, uint8_t len, uint8_t *encoded, uint8_t encoded_max_len)
Software CRC helper kept for transmit framing parity with the current implementation.
void set_rx_packet_params_()
Apply RX‑specific packet parameters (calls set_packet_params_ for RX).
void read_opcode_(uint8_t opcode, uint8_t *data, uint8_t len)
Read response from an opcode.
bool send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) override
Send a packet using the specified carrier frequency and preamble settings.
bool init() override
Initialize the radio hardware. Returns true on success.
void clear_irq_status_(uint16_t irq_mask)
Clear all IRQ status bits.
void read_buffer_(uint8_t offset, uint8_t *data, uint8_t len)
Read from the SX1262 RX buffer.
void read_register_(uint16_t addr, uint8_t *data, uint8_t len)
Read from a register.
uint16_t get_device_errors_()
Read device error flags (and clear them).
bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override
Wait (blocking) for a packet with timeout.
Shared frame logging helpers for IO-Homecontrol.
static constexpr uint8_t SX1262_SET_DIO3_AS_TCXO_CTRL
static constexpr uint8_t SX1262_CALIBRATE
static constexpr uint16_t SX1262_REG_RX_GAIN
static constexpr uint8_t CMD_DISCOVER_REQ
Broadcast discovery request.
static constexpr uint8_t SX1262_SET_BUFFER_BASE_ADDRESS
static constexpr uint8_t CMD_SET_CONFIG1
Configure device to auto-send status updates.
static constexpr uint8_t CMD_KEY_TRANSFER
Send encrypted system key to device.
static constexpr uint8_t FRAME_MIN_SIZE
Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1).
Definition proto_frame.h:89
static constexpr uint8_t SX1262_CLEAR_DEVICE_ERRORS
static constexpr uint8_t SX1262_GFSK_CRC_OFF
static constexpr uint8_t CMD_ERROR_RESP
Error response to any command.
static constexpr uint8_t SX1262_READ_REGISTER
static constexpr uint8_t CMD_GET_NAME_RESP
Device name response.
static constexpr uint8_t SX1262_SET_PACKET_TYPE
static constexpr uint8_t CMD_DISCOVER_CONFIRM_ACK
Device acknowledges confirmation.
static constexpr uint16_t SX1262_IRQ_SYNC_WORD_VALID
static constexpr uint8_t SX1262_GET_DEVICE_ERRORS
static constexpr uint8_t CMD_STATUS_UPDATE
Device-initiated status update (needs auth).
static constexpr uint16_t SX1262_IRQ_CRC_ERR
static constexpr uint8_t SX1262_SET_MODULATION_PARAMS
static constexpr uint8_t SX1262_GET_RX_BUFFER_STATUS
static constexpr uint8_t CMD_GET_NAME
Request device name.
static constexpr uint8_t CTRL0_PROTOCOL_1W
Bit 5: 1=OneWay protocol, 0=TwoWay protocol.
static constexpr uint8_t CMD_DISCOVER_SPE_RESP
Sub-device response.
static constexpr uint8_t SX1262_CLEAR_IRQ_STATUS
static constexpr uint8_t SX1262_SET_RX
uint16_t crc_ccitt(const uint8_t *data, uint8_t len)
CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
static constexpr uint8_t SX1262_SET_TX
static constexpr uint8_t SX1262_GFSK_PACKET_TYPE_KNOWN_LENGTH
static const uint8_t SX1262_RX_PROBE_PACKET_LEN
static constexpr uint8_t FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
Definition proto_frame.h:90
static constexpr uint8_t CMD_KEY_CONFIRM
Device confirms key was received.
static constexpr uint8_t SX1262_CALIBRATE_IMAGE
static constexpr uint8_t SX1262_SET_DIO2_AS_RF_SWITCH_CTRL
static bool is_plausible_uart_frame(const IoFrame &frame, uint8_t candidate_len)
static constexpr uint8_t CMD_KEY_INIT
Initiate key transfer to device.
static constexpr uint8_t SX1262_WRITE_BUFFER
static constexpr uint8_t SX1262_READ_BUFFER
static constexpr uint16_t SX1262_IRQ_RX_DONE
static constexpr uint8_t SX1262_SET_PA_CONFIG
static constexpr uint8_t SX1262_SET_TX_PARAMS
static constexpr uint16_t SX1262_IRQ_TX_DONE
static constexpr uint8_t SX1262_SET_DIO_IRQ_PARAMS
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
static constexpr uint8_t CMD_DISCOVER_SPE_REQ
Discover sub-devices (e.g., light on garage door).
static constexpr uint8_t CMD_EXECUTE
Set position/open/close/stop — requires authentication.
static constexpr uint8_t CMD_PRIVATE_RESP
Response to 0x00 and 0x03 (contains position data).
static bool is_known_io_command(uint8_t cmd)
Check if a command ID is one of the known IO‑Homecontrol commands.
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
Definition proto_frame.h:31
static constexpr uint8_t SX1262_SET_PACKET_PARAMS
static constexpr uint8_t CMD_CHALLENGE_REQ
Device sends 6-byte random challenge.
static constexpr uint8_t CMD_STATUS_UPDATE_RESP
Acknowledge status update.
static constexpr uint8_t SX1262_GET_STATUS
static constexpr uint16_t SX1262_REG_TX_CLAMP_CONFIG
static uint8_t decode_uart_probe(const uint8_t *raw, uint8_t raw_len, uint8_t bit_offset, uint8_t *decoded, uint8_t decoded_max_len)
Decode a raw UART‑encoded bitstream into bytes.
static uint8_t get_bit_msb(const uint8_t *data, uint16_t bit_pos)
Extract a single bit (MSB‑first) from a byte buffer.
static constexpr uint8_t CMD_SET_CONFIG1_RESP
Config response.
static constexpr uint8_t CMD_DISCOVER_CONFIRM
Confirm discovery to device.
static constexpr uint8_t SX1262_SET_RX_TX_FALLBACK_MODE
static constexpr uint8_t CMD_DISCOVER_RESP
Device responds with its ID and type.
static constexpr uint8_t SX1262_GET_IRQ_STATUS
static constexpr uint8_t SX1262_SET_REGULATOR_MODE
static constexpr uint8_t SX1262_GET_PACKET_STATUS
static constexpr uint8_t CMD_PRIVATE
Get device status — no authentication needed.
static UartProbeResult find_uart_probe(const uint8_t *raw, uint8_t raw_len)
Search a raw capture for the most plausible IoFrame using UART decoding.
static constexpr uint8_t CMD_GET_INFO2_RESP
Device type/model response.
static constexpr uint16_t SX1262_REG_SYNC_WORD
static constexpr uint8_t CMD_CHALLENGE_RESP
Controller responds with HMAC proof.
static constexpr uint8_t SX1262_FALLBACK_STDBY_XOSC
static constexpr uint8_t CMD_GET_INFO2
Request device type/model info.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
static constexpr uint8_t SX1262_SET_RF_FREQUENCY
static const uint8_t SX1262_SYNC_WORD_PARAM_24_BITS
static constexpr uint8_t SX1262_SET_STANDBY
static constexpr uint8_t SX1262_WRITE_REGISTER
static const uint8_t UART_PROBE_MAX_BIT_OFFSET
Maximum bit offset to search for valid UART decode start position.
static const char *const TAG
Definition hub_core.cpp:34
static constexpr uint8_t SX1262_GET_RSSI_INST
SX1262 radio driver for IO-Homecontrol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t ctrl0
Control byte 0: flags + length.
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.
Result of the UART probe: best candidate frame within a raw capture.
uint8_t decoded_len
Total number of bytes decoded at that offset.
uint8_t frame_start
Index into decoded buffer where the frame begins.
bool valid
A plausible frame was found.
uint8_t bit_offset
Bit offset where the best decode started.
uint8_t frame_len
Length of the candidate IoFrame (decoded bytes).
uint8_t decoded[RADIO_PACKET_BUFFER_SIZE]
Full decoded UART stream at the chosen offset.