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