Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_soft_phy_driver_base.cpp
Go to the documentation of this file.
1/// @file radio_soft_phy_driver_base.cpp
2/// @brief Shared driver flow implementation for the software-PHY radios.
3/// @ingroup hioc_radio
4///
5/// See radio_soft_phy_driver_base.h for the architectural context. This file holds the shared
6/// RX/TX orchestration for both software-PHY drivers, parameterized over the small set of virtual
7/// primitives/hooks the two chips genuinely differ on.
8
9// Opcode payloads and recovery thresholds are written in the same shape as the chip protocol
10// and on-air framing they reproduce.
11// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
12
14#include "log_frame.h"
15#include "esphome/core/log.h"
16#include "esphome/core/application.h"
17
18#include <cinttypes>
19
20namespace esphome {
21namespace home_io_control {
22
23static const char *const TAG = "home_io_control.soft_phy";
24
25namespace {
26
27/// Block until `raw_bytes` (plus @ref SOFT_PHY_EARLY_READ_MARGIN_BYTES) have had time to arrive
28/// since the sync word was observed at `sync_us`. Pure wall-clock waiting against the protocol's
29/// line rate — it reads no chip state, so it lives here rather than on the driver.
30/// @return false if the caller's `timeout_ms` window closed first.
31bool wait_for_air_time(uint32_t sync_us, uint8_t raw_bytes, uint32_t start_ms, uint32_t timeout_ms) {
32 uint32_t const needed_us = soft_phy_air_time_us((uint32_t) raw_bytes + SOFT_PHY_EARLY_READ_MARGIN_BYTES);
33 while (micros() - sync_us < needed_us) {
34 // Never outstay the window the caller asked for: a receive that has run out of time falls back
35 // to the RX_DONE path (which will time out on its own terms) rather than silently overrunning.
36 if (millis() - start_ms > timeout_ms)
37 return false;
38 App.feed_wdt();
39 delayMicroseconds(SOFT_PHY_EARLY_POLL_US);
40 }
41 return true;
42}
43
44} // namespace
45
46// === Packet RX (blocking) ===
47
48bool SoftPhyDriverBase::wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) {
49 // Blocking receive with timeout. This orchestrator decomposes the state machine into three
50 // low-complexity helpers, shared verbatim between SX1262 and LR1121.
51 this->prepare_blocking_receive_(packet);
52
53 uint32_t const start = millis();
54 uint32_t irq = 0;
55
56 // Phase 1: wait for first *terminal* activity (see activity_irq_mask()'s doc comment).
57 if (!this->poll_until_activity_(start, timeout_ms, irq)) {
58 return false;
59 }
60
61 // Phase 2: resolve the SYNC_WORD_VALID → RX_DONE race condition, and take the length-driven
62 // shortcut when the chip supports it.
63 bool early_completed = false;
64 if (!this->resolve_sync_race_(start, timeout_ms, irq, packet, early_completed)) {
65 return false;
66 }
67 if (early_completed) {
68 return true;
69 }
70
71 // Phase 3: finalize — either read the packet or treat as a failure.
72 return this->finalize_receive_(packet, irq);
73}
74
75/// Poll for first terminal radio activity (IRQ pin or an IRQ status bit within
76/// activity_irq_mask()) within timeout. `irq` is always freshly read before this returns, so
77/// callers never need a separate refresh step. A reading outside activity_irq_mask() (e.g.
78/// LR1121's preamble-only case) keeps the loop going rather than returning — the loop re-reads
79/// chip status via SPI every iteration regardless of the DIO edge, so nothing is lost.
80bool SoftPhyDriverBase::poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq) {
81 while (true) {
82 if (this->is_dio_fired())
83 this->clear_dio_fired();
84 irq = this->read_irq_status_raw();
85 if ((irq & this->activity_irq_mask()) != 0)
86 return true;
87 if (millis() - start > timeout_ms) {
88 this->clear_dio_fired();
89 this->reset_rx_state_();
90 return false;
91 }
92 App.feed_wdt();
93 delay(1);
94 }
95}
96
97/// Resolve the SYNC_WORD_VALID → RX_DONE race condition common to both chips: the sync-word IRQ
98/// can assert before the packet is fully received. If we observe SYNC without RX_DONE, clear the
99/// sticky SYNC flag and spin until RX_DONE arrives or the remaining timeout elapses.
100bool SoftPhyDriverBase::resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq, RadioRxPacket &packet,
101 bool &early_completed) {
102 early_completed = false;
103 // If RX_DONE already set or SYNC not set, nothing to resolve.
104 if ((irq & this->sync_word_valid_bit()) == 0 || (irq & this->rx_done_bit()) != 0) {
105 return true;
106 }
107 // SYNC is the frame's own start marker, so from here everything about the frame's arrival is a
108 // question of air time. Timestamp it before any SPI work so the length-driven receive below
109 // measures from as close to the on-air event as this loop can see.
110 uint32_t const sync_us = micros();
111 // SYNC seen without RX_DONE — clear sticky SYNC and wait for RX_DONE.
113
114 if (this->try_early_completion_(packet, sync_us, irq, start, timeout_ms)) {
115 early_completed = true;
116 return true;
117 }
118 while (millis() - start <= timeout_ms) {
119 if (!this->is_dio_fired()) {
120 irq = this->read_irq_status_raw();
121 if ((irq & this->rx_done_bit()) != 0)
122 return true;
123 App.feed_wdt();
124 delay(1);
125 continue;
126 }
127 this->clear_dio_fired();
128 irq = this->read_irq_status_raw();
129 if ((irq & this->rx_done_bit()) != 0)
130 return true;
131 if (irq != 0)
132 this->clear_irq_status(irq);
133 }
134 return false; // timeout
135}
136
137/// Finish a reception on the frame's own air time instead of the chip's fixed-length RX_DONE.
138///
139/// Three stages, each of which can bail out harmlessly:
140/// 1. wait out the first UART cell and read it — CTRL0 carries the whole frame's length;
141/// 2. wait out exactly that frame's air time and read exactly its bytes;
142/// 3. run the normal UART probe and accept only a CRC-valid frame.
143///
144/// Stage 3 is the safety property that makes this worth doing at all. CRC-CCITT is a 1-in-65536
145/// gate, so a frame accepted here is a frame that would have been accepted at RX_DONE anyway —
146/// and *any* failure (a chip that turns out not to expose its buffer mid-reception, a spurious
147/// sync detect, a mis-guessed length, a bit error) simply returns false and leaves the caller on
148/// the original RX_DONE path, which re-reads the full buffer from scratch. A wrong guess here
149/// costs some latency; it can never cost a frame.
150bool SoftPhyDriverBase::try_early_completion_(RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status,
151 uint32_t start_ms, uint32_t timeout_ms) {
152 int16_t const base = this->early_rx_read_offset();
153 if (base < 0)
154 return false; // chip does not expose its data buffer mid-reception
155 if (timeout_ms < SOFT_PHY_EARLY_MIN_WINDOW_MS)
156 return false; // no room left in this window to receive a whole frame either way
157 auto const offset = (uint8_t) base;
158
159 // Stage 1: ten bits of air time is all it takes to learn how long the frame will be.
160 if (!wait_for_air_time(sync_us, SOFT_PHY_EARLY_HEADER_RAW_BYTES, start_ms, timeout_ms))
161 return false;
162 uint8_t header[SOFT_PHY_EARLY_HEADER_RAW_BYTES] = {0};
163 this->read_rx_buffer(offset, header, sizeof(header));
164 uint8_t const frame_len = soft_phy_peek_frame_length(header, sizeof(header));
165 if (frame_len == 0)
166 return false;
167
168 // Stage 2: wait out only what this frame needs, then take the whole thing. The margin bytes
169 // have been waited for either way, so read them too — they give the probe slack to work with
170 // when the stream is not byte-aligned.
171 uint8_t const frame_raw_len = soft_phy_raw_bytes_for_frame(frame_len);
172 if (!wait_for_air_time(sync_us, frame_raw_len, start_ms, timeout_ms))
173 return false;
174 uint8_t raw[RADIO_PACKET_BUFFER_SIZE] = {0};
175 auto const read_len =
176 (uint8_t) std::min<uint16_t>((uint16_t) frame_raw_len + SOFT_PHY_EARLY_READ_MARGIN_BYTES, sizeof(raw));
177 this->read_rx_buffer(offset, raw, read_len);
178
179 // Stage 3: CRC decides.
180 UartProbeResult const probe = find_uart_probe(raw, read_len);
181 if (!probe.valid)
182 return false;
183
184 memcpy(packet.data, probe.decoded + probe.frame_start, probe.frame_len);
185 packet.len = probe.frame_len;
186 packet.freq_hz = this->current_freq_;
187 // Reading packet status this early is still sound: the RSSI a driver reports from it is latched
188 // at sync-word detection, which by definition has already happened.
189 this->fill_capture_info(true, irq_status, offset, read_len, raw, read_len, packet.data, packet.len);
190
191#ifdef IOHOME_FRAME_LOG
192 ESP_LOGD(TAG, "Early RX: frame_len=%u raw_len=%u air_us=%" PRIu32, frame_len, read_len,
193 (uint32_t) (micros() - sync_us));
194 log_frame("RX", packet.data, packet.len, this->current_freq_);
195#endif
196
197 // The reception this latch refers to is being torn down deliberately, so drop it rather than
198 // let a stale edge look like a fresh packet to the next wait.
199 this->clear_dio_fired();
200 this->reset_rx_state_();
201 return true;
202}
203
204/// Finalize receive: read the packet if RX_DONE is set, otherwise record failure.
205bool SoftPhyDriverBase::finalize_receive_(RadioRxPacket &packet, uint32_t irq) {
206 if ((irq & this->rx_done_bit()) == 0) {
207 this->fill_capture_info(true, irq, 0, 0, nullptr, 0, nullptr, 0);
208 this->reset_rx_state_();
209 return false;
210 }
211 return this->read_rx_packet(packet, true, irq);
212}
213
214void SoftPhyDriverBase::reset_rx_state_(bool force_standby) {
215 if (force_standby)
216 this->set_mode_standby();
217 this->clear_irq_status(0xFFFFFFFF);
218 this->configure_buffer_base();
219 this->set_rx_packet_params();
220 this->set_mode_rx();
221}
222
223bool SoftPhyDriverBase::read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status) {
224 uint8_t raw_reported_len = 0;
225 uint8_t rx_offset = 0;
226 this->get_rx_buffer_status(raw_reported_len, rx_offset);
227
228 uint8_t rx_buf[RADIO_PACKET_BUFFER_SIZE] = {0};
229 uint8_t recovered_buf[RADIO_PACKET_BUFFER_SIZE] = {0};
230 uint8_t const reported_len = std::min(raw_reported_len, (uint8_t) sizeof(rx_buf));
231 uint8_t raw_probe_len = reported_len;
232 if (reported_len > 0 && reported_len < 32) {
233 // When the chip reports a short packet length, still pull the full raw window: the useful
234 // UART-packed tail (e.g. a post-auth response) can sit past the chip-reported boundary, so
235 // trimming the probe to that boundary would lose it.
236 raw_probe_len = sizeof(rx_buf);
237 }
238 if (reported_len == SOFT_PHY_RX_PROBE_PACKET_LEN)
239 raw_probe_len = SOFT_PHY_RX_PROBE_PACKET_LEN;
240 if (raw_probe_len > 0)
241 this->read_rx_buffer(rx_offset, rx_buf, raw_probe_len);
242
243 // Neither chip exposes the already-decoded IO-homecontrol frame the way SX1276 does. We first
244 // capture the raw bytes exactly as reported by the chip, then recover the UART-packed protocol
245 // stream in software and only pass a plausible frame up to the parser. This software recovery
246 // path is the soft-PHY-specific adaptation to the same protocol.
247 UartProbeResult probe = find_uart_probe(rx_buf, raw_probe_len);
248 if (probe.valid) {
249 memcpy(recovered_buf, probe.decoded + probe.frame_start, probe.frame_len);
250 memcpy(packet.data, recovered_buf, probe.frame_len);
251 packet.len = probe.frame_len;
252#ifdef IOHOME_FRAME_LOG
253 ESP_LOGD(TAG, "UART probe: valid=1 bit_offset=%u frame_start=%u frame_len=%u decoded_len=%u", probe.bit_offset,
254 probe.frame_start, probe.frame_len, probe.decoded_len);
255#endif
256 } else {
257#ifdef IOHOME_FRAME_LOG
258 // Log diagnostic info when CRC validation rejects all decode attempts — helps identify
259 // whether post-TX RX corruption is being correctly caught by CRC or slipping through.
260 char hex_buf[97] = {0}; // 32 bytes * 3 chars + null
261 uint8_t dump_len = std::min(raw_probe_len, (uint8_t) 32);
262 for (uint8_t i = 0; i < dump_len; i++)
263 snprintf(hex_buf + (i * 3), 4, "%02X ", rx_buf[i]);
264 ESP_LOGW(TAG, "UART probe: valid=0 decoded_len=%u raw_probe_len=%u", probe.decoded_len, raw_probe_len);
265 ESP_LOGW(TAG, " raw[0..%u]: %s", dump_len - 1, hex_buf);
266 // Try to show why CRC failed at best offset
267 if (probe.decoded_len >= FRAME_MIN_SIZE) {
268 int best_len = std::min<int>(probe.decoded_len, FRAME_MAX_SIZE);
269 IoFrame test_frame;
270 for (int cl = best_len; cl >= FRAME_MIN_SIZE; cl--) {
271 if (!parse(probe.decoded, cl, test_frame))
272 continue;
273 if (cl + 2 <= (int) probe.decoded_len) {
274 uint16_t computed = crc_ccitt(probe.decoded, cl);
275 uint16_t received = (uint16_t) probe.decoded[cl] | ((uint16_t) probe.decoded[cl + 1] << 8);
276 ESP_LOGW(TAG, " CRC check: candidate_len=%d cmd=0x%02X crc_computed=0x%04X crc_received=0x%04X %s", cl,
277 test_frame.cmd, computed, received, computed == received ? "MATCH" : "MISMATCH");
278 }
279 break;
280 }
281 }
282#endif
283 uint8_t const copy_len = std::min(reported_len, FRAME_MAX_SIZE);
284 if (copy_len > 0)
285 memcpy(packet.data, rx_buf, copy_len);
286 packet.len = copy_len;
287 }
288 packet.freq_hz = this->current_freq_;
289 this->fill_capture_info(blocking_wait, irq_status, rx_offset, reported_len, rx_buf, raw_probe_len, packet.data,
290 packet.len);
291
292#ifdef IOHOME_FRAME_LOG
293 if (packet.len > 0)
294 log_frame("RX", packet.data, packet.len, this->current_freq_);
295#endif
296 this->reset_rx_state_();
297 return packet.len > 0;
298}
299
300// === Packet RX (non-blocking) ===
301
303 if (!this->is_dio_fired())
304 return false;
305 this->prepare_nonblocking_receive_(packet);
306
307 uint32_t const irq = this->read_irq_status_raw();
308
309 if ((irq & this->activity_irq_mask()) == 0) {
310 // A reading outside activity_irq_mask() (e.g. preamble-only on a chip that routes it to the
311 // IRQ pin) means a frame may still be arriving. Clear just that bit instead of calling
312 // reset_rx_state_() below, which would tear down RX mid-reception. Unlike
313 // poll_until_activity_(), this function has no internal retry loop, so leaving the chip-level
314 // bit set would starve later loop() ticks of the DIO edge they need to notice the real
315 // RX_DONE. On chips whose activity_irq_mask() is "any bit" (the default), this branch can
316 // never trigger — that hardware has already filtered such readings out before they get here.
317 if (irq != 0)
318 this->clear_irq_status(irq);
319 return false;
320 }
321
322 if ((irq & this->sync_word_valid_bit()) != 0 && (irq & this->rx_done_bit()) == 0) {
324 return false;
325 }
326
327 if ((irq & this->rx_done_bit()) != 0) {
328 return this->read_rx_packet(packet, false, irq);
329 }
330
331 this->fill_capture_info(false, irq, 0, 0, nullptr, 0, nullptr, 0);
332 this->reset_rx_state_();
333 return false;
334}
335
336// === Packet TX ===
337
338bool SoftPhyDriverBase::send_packet(const uint8_t *data, uint8_t len, const RadioTxConfig &tx_config) {
339 if (len == 0)
340 return false;
341
342#ifdef IOHOME_FRAME_LOG
343 log_frame("TX", data, len, tx_config.freq_hz, tx_config.preamble_len);
344#endif
345
346 this->set_mode_standby();
347 this->set_frequency_register(tx_config.freq_hz);
348
349 uint8_t frame_with_crc[FRAME_MAX_SIZE + 2] = {0};
350 uint8_t tx_buf[RADIO_PACKET_BUFFER_SIZE];
351 if ((uint16_t) len + 2 > (uint16_t) sizeof(frame_with_crc))
352 return false;
353
354 memcpy(frame_with_crc, data, len);
355 const uint16_t crc = crc_ccitt(data, len);
356 frame_with_crc[len] = crc & 0xFF;
357 frame_with_crc[len + 1] = (crc >> 8) & 0xFF;
358
359 const uint8_t encoded_len = uart_encode_packet(frame_with_crc, len + 2, tx_buf, sizeof(tx_buf));
360 if (encoded_len == 0)
361 return false;
362
363 this->set_tx_packet_params(tx_config.preamble_len, encoded_len);
364
365 this->clear_irq_status(0xFFFFFFFF);
366 this->write_tx_buffer(tx_buf, encoded_len);
367
368 // Chip-specific pre-TX workaround hook (no-op on chips that don't need one).
369 this->before_tx_arm();
370
371 this->clear_dio_fired();
372 this->start_tx();
373
374 // Wait for an actual TxDone IRQ. The IRQ pin is shared with RX-related events, so a stale or
375 // unrelated interrupt must not be treated as TX completion.
376 uint32_t const start = millis();
377 uint32_t tx_irq = 0;
378 while (true) {
379 if (!this->is_dio_fired()) {
380 if (millis() - start > 4000) {
381 ESP_LOGE(TAG, "TX timeout — DIO/IRQ pin never fired");
382 this->set_mode_standby();
383 return false;
384 }
385 App.feed_wdt();
386 delayMicroseconds(100);
387 continue;
388 }
389
390 this->clear_dio_fired();
391 tx_irq = this->read_irq_status_raw();
392 if ((tx_irq & this->tx_done_bit()) != 0)
393 break;
394
395 if (tx_irq != 0) {
396 this->clear_irq_status(tx_irq);
397 }
398
399 if (millis() - start > 4000) {
400 ESP_LOGE(TAG, "TX timeout — no TX_DONE IRQ (last_irq=0x%08" PRIX32 ")", tx_irq);
401 this->set_mode_standby();
402 return false;
403 }
404 }
405 // TxDone used the same DIO/IRQ latch as RX. Clear the local latch before re-arming RX so an
406 // immediate reply remains visible to wait_for_packet().
407 this->clear_dio_fired();
408
409 this->clear_irq_status(0xFFFFFFFF);
410 this->reset_rx_state_(true);
411
412 // Post-TX settling delay: the GFSK demodulator needs time to stabilize after the TX→STDBY→RX
413 // transition. Without this, frames received immediately after TX (e.g. the 0x3C challenge
414 // during pairing) can suffer UART decode bit errors before the demodulator's frequency
415 // discrimination has settled. Runtime-tunable per chip.
416 delayMicroseconds(this->post_tx_settle_us_);
417
418 return true;
419}
420
421// === Frequency control ===
422
424 this->set_mode_standby();
425 this->set_frequency_register(freq_hz);
426 this->clear_irq_status(0xFFFFFFFF); // Clear stale preamble/sync bits from previous channel
427 this->clear_dio_fired(); // Clear stale IRQ latch from previous channel activity
428 this->set_mode_rx();
429}
430
431// === RSSI / sync / preamble ===
432
433int16_t SoftPhyDriverBase::read_rssi() { return -(int16_t) this->read_rssi_raw_byte() / 2; }
434
436
440
441} // namespace home_io_control
442} // namespace esphome
443
444// NOLINTEND(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
bool is_dio_fired() const
Set by the ISR when DIO fires.
virtual void set_mode_standby()=0
Switch to standby mode.
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.
virtual void set_mode_rx()=0
Switch to continuous receive mode.
virtual uint32_t read_irq_status_raw()=0
Read the raw IRQ status word from the radio.
virtual void set_frequency_register(uint32_t freq_hz)=0
Set RF frequency via the chip's own frequency register/opcode encoding, and update current_freq_.
virtual void get_rx_buffer_status(uint8_t &reported_len, uint8_t &rx_offset)=0
Read the chip-reported RX length and buffer offset (raw, before any clamping).
bool is_preamble_detected() override
Check if preamble has been detected (while in RX).
virtual void read_rx_buffer(uint8_t offset, uint8_t *data, uint8_t len)=0
Read len bytes from the RX buffer starting at offset.
virtual bool read_rx_packet(RadioRxPacket &packet, bool blocking_wait, uint32_t irq_status)
Read a received packet from the buffer and return the raw bytes reported by the chip.
virtual uint32_t rx_done_bit() const =0
bool is_sync_detected() override
Check if sync word has been detected (while in RX).
virtual uint32_t preamble_detected_bit() const =0
virtual void configure_buffer_base()
Hook run as part of reset_rx_state_, before re-entering RX.
virtual void set_rx_packet_params()=0
Configure RX-specific packet parameters (preamble detector length, fixed probe length).
virtual uint32_t sync_word_valid_bit() const =0
virtual void fill_capture_info(bool blocking_wait, uint32_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)=0
Populate the RadioCaptureInfo from chip-specific telemetry (RSSI opcode, packet-status byte,...
virtual void clear_irq_status(uint32_t irq_mask)=0
Clear IRQ status bits.
virtual void set_tx_packet_params(uint16_t preamble_len, uint8_t payload_len)=0
Configure TX packet parameters for one outgoing UART-encoded frame.
void reset_rx_state_(bool force_standby=true)
Reset RX state machine and buffer. Optionally force standby first.
virtual uint32_t tx_done_bit() const =0
virtual uint8_t read_rssi_raw_byte()=0
Read the single raw RSSI byte (chip-specific opcode); formula is shared, see read_rssi.
int16_t read_rssi() override
Read instantaneous RSSI (in dBm) while in RX mode.
virtual void before_tx_arm()
Hook run immediately before every SetTx.
virtual void write_tx_buffer(const uint8_t *data, uint8_t len)=0
Write the UART-encoded TX payload into the chip's TX buffer.
virtual int16_t early_rx_read_offset() const
Data-buffer offset an in-flight reception is being written to, or a negative value when this chip mus...
virtual uint32_t activity_irq_mask() const
IRQ bits that count as "activity" for the internal poll_until_activity_() helper and check_for_packet...
bool check_for_packet(RadioRxPacket &packet) override
Non-blocking check for a received packet.
bool wait_for_packet(RadioRxPacket &packet, uint32_t timeout_ms) override
Wait (blocking) for a packet with timeout.
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.
virtual void start_tx()=0
Issue the SetTx opcode with the fixed TX timeout — identical 3-byte payload on both chips,...
void change_frequency(uint32_t freq_hz) override
Change the carrier frequency using fast hop (no standby transition needed).
Shared frame logging helpers for IO-Homecontrol.
static constexpr uint8_t SOFT_PHY_RX_PROBE_PACKET_LEN
Fixed raw-RX probe length: chosen from captures of 23-25 byte protocol frames after UART packing and ...
static constexpr uint8_t FRAME_MIN_SIZE
Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1).
Definition proto_sizes.h:29
constexpr uint32_t soft_phy_air_time_us(uint32_t raw_bytes)
On-air time in microseconds for raw_bytes bytes at the protocol's line rate.
static constexpr const char * TAG
uint8_t soft_phy_peek_frame_length(const uint8_t *raw, uint8_t raw_len)
Recover a frame's total length from the very first UART cell of a reception.
uint8_t uart_encode_packet(const uint8_t *data, uint8_t len, uint8_t *encoded, uint8_t encoded_max_len)
UART-encode a buffer of bytes (start bit 0, 8 data bits LSB-first, stop bit 1).
UartProbeResult find_uart_probe(const uint8_t *raw, uint8_t raw_len)
Search raw RX buffer for the best CRC-validated IO-Homecontrol frame.
static constexpr uint8_t SOFT_PHY_EARLY_HEADER_RAW_BYTES
Raw bytes read in the first stage of a length-driven receive — enough to hold CTRL0's UART cell (10 b...
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 FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
Definition proto_sizes.h:30
uint8_t soft_phy_raw_bytes_for_frame(uint8_t frame_len)
Raw on-air bytes needed to carry a whole frame: frame_len protocol bytes plus the two trailing CRC by...
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 uint32_t SOFT_PHY_EARLY_MIN_WINDOW_MS
Smallest receive window a length-driven receive will be attempted in, in milliseconds.
static constexpr uint8_t SOFT_PHY_EARLY_READ_MARGIN_BYTES
Air-time margin added before every mid-reception buffer read, in raw bytes.
static constexpr uint32_t SOFT_PHY_EARLY_POLL_US
Poll interval while waiting out a frame's remaining air time, in microseconds.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
Shared driver flow for radios using the software PHY (SX1262, LR1121).
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
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.