15#include "esphome/core/log.h"
16#include "esphome/core/application.h"
24static const char *
const TAG =
"home_io_control.soft_phy";
32bool wait_for_air_time(uint32_t sync_us, uint8_t raw_bytes, uint32_t start_ms, uint32_t timeout_ms) {
33 uint32_t
const needed_us =
soft_phy_air_time_us((uint32_t) raw_bytes + SOFT_PHY_EARLY_READ_MARGIN_BYTES);
34 while (micros() - sync_us < needed_us) {
37 if (millis() - start_ms > timeout_ms)
40 delayMicroseconds(SOFT_PHY_EARLY_POLL_US);
56 uint32_t
const start = millis();
58 if (millis() - start > this->busy_timeout_ms_) {
59 ESP_LOGE(
TAG,
"BUSY timeout");
60 this->
fail_(
"BUSY pin stayed high -- check busy_pin, the SPI wiring and tcxo_voltage");
74 uint32_t
const start = millis();
78 if (!this->poll_until_activity_(start, timeout_ms, irq)) {
84 bool early_completed =
false;
85 if (!this->resolve_sync_race_(start, timeout_ms, irq, packet, early_completed)) {
88 if (early_completed) {
93 return this->finalize_receive_(packet, irq);
101bool SoftPhyDriverBase::poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq) {
108 this->preamble_latched_at_timeout_ =
false;
111 if (millis() - start > timeout_ms) {
128bool SoftPhyDriverBase::resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq,
RadioRxPacket &packet,
129 bool &early_completed) {
130 early_completed =
false;
138 uint32_t
const sync_us = micros();
142 if (this->try_early_completion_(packet, sync_us, irq, start, timeout_ms)) {
143 early_completed =
true;
146 while (millis() - start <= timeout_ms) {
178bool SoftPhyDriverBase::try_early_completion_(
RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status,
179 uint32_t start_ms, uint32_t timeout_ms) {
183 if (timeout_ms < SOFT_PHY_EARLY_MIN_WINDOW_MS)
185 auto const offset = (uint8_t) base;
188 if (!wait_for_air_time(sync_us, SOFT_PHY_EARLY_HEADER_RAW_BYTES, start_ms, timeout_ms))
190 uint8_t header[SOFT_PHY_EARLY_HEADER_RAW_BYTES] = {0};
200 if (!wait_for_air_time(sync_us, frame_raw_len, start_ms, timeout_ms))
203 auto const read_len =
204 (uint8_t) std::min<uint16_t>((uint16_t) frame_raw_len + SOFT_PHY_EARLY_READ_MARGIN_BYTES,
sizeof(raw));
212 memcpy(packet.data, probe.decoded + probe.frame_start, probe.frame_len);
213 packet.len = probe.frame_len;
217 this->
fill_capture_info(
true, irq_status, offset, read_len, raw, read_len, packet.data, packet.len);
219#ifdef IOHOME_FRAME_LOG
220 ESP_LOGD(
TAG,
"Early RX: frame_len=%u raw_len=%u air_us=%" PRIu32, frame_len, read_len,
221 (uint32_t) (micros() - sync_us));
222 log_frame(
"RX", packet.data, packet.len, this->current_freq_);
233bool SoftPhyDriverBase::finalize_receive_(
RadioRxPacket &packet, uint32_t irq) {
274 out[0] =
static_cast<uint8_t
>(preamble_bits >> 8);
275 out[1] =
static_cast<uint8_t
>(preamble_bits);
303 uint8_t raw_reported_len = 0;
304 uint8_t rx_offset = 0;
309 uint8_t
const reported_len = std::min(raw_reported_len, (uint8_t)
sizeof(rx_buf));
310 uint8_t raw_probe_len = reported_len;
311 if (reported_len > 0 && reported_len < 32) {
315 raw_probe_len =
sizeof(rx_buf);
317 if (reported_len == SOFT_PHY_RX_PROBE_PACKET_LEN)
318 raw_probe_len = SOFT_PHY_RX_PROBE_PACKET_LEN;
319 if (raw_probe_len > 0)
331#ifdef IOHOME_FRAME_LOG
336 ESP_LOGD(
TAG,
"UART probe: valid=1 bit_offset=%u frame_start=%u frame_len=%u decoded_len=%u rx_offset=%u",
340#ifdef IOHOME_FRAME_LOG
343 char hex_buf[97] = {0};
344 uint8_t dump_len = std::min(raw_probe_len, (uint8_t) 32);
345 for (uint8_t i = 0; i < dump_len; i++)
346 snprintf(hex_buf + (i * 3), 4,
"%02X ", rx_buf[i]);
347 ESP_LOGW(
TAG,
"UART probe: valid=0 decoded_len=%u raw_probe_len=%u", probe.
decoded_len, raw_probe_len);
348 ESP_LOGW(
TAG,
" raw[0..%u]: %s", dump_len - 1, hex_buf);
356 int best_len = std::min<int>(probe.
decoded_len, FRAME_MAX_WIRE_SIZE);
358 for (
int cl = best_len; cl >= FRAME_MIN_SIZE; cl--) {
363 uint16_t received = (uint16_t) probe.
decoded[cl] | ((uint16_t) probe.
decoded[cl + 1] << 8);
364 ESP_LOGW(
TAG,
" CRC check: candidate_len=%d cmd=0x%02X crc_computed=0x%04X crc_received=0x%04X %s", cl,
365 test_frame.
cmd, computed, received, computed == received ?
"MATCH" :
"MISMATCH");
375 uint8_t
const copy_len = std::min(reported_len, FRAME_MAX_WIRE_SIZE);
377 memcpy(packet.
data, rx_buf, copy_len);
378 packet.
len = copy_len;
381 this->
fill_capture_info(blocking_wait, irq_status, rx_offset, reported_len, rx_buf, raw_probe_len, packet.
data,
384#ifdef IOHOME_FRAME_LOG
386 log_frame(
"RX", packet.
data, packet.
len, this->current_freq_);
389 return packet.
len > 0;
430 uint32_t
const sync_us = micros();
431 uint32_t
const start_ms = millis();
443 bool const completed =
465#ifdef IOHOME_FRAME_LOG
475 uint8_t frame_with_crc[FRAME_MAX_WIRE_SIZE] = {0};
477 if ((uint16_t) len + 2 > (uint16_t)
sizeof(frame_with_crc))
480 memcpy(frame_with_crc, data, len);
481 const uint16_t crc =
crc_ccitt(data, len);
482 frame_with_crc[len] = crc & 0xFF;
483 frame_with_crc[len + 1] = (crc >> 8) & 0xFF;
485 const uint8_t encoded_len =
uart_encode_packet(frame_with_crc, len + 2, tx_buf,
sizeof(tx_buf));
486 if (encoded_len == 0)
502 uint32_t
const start = millis();
506 if (millis() - start > 4000) {
507 ESP_LOGE(
TAG,
"TX timeout — DIO/IRQ pin never fired");
512 delayMicroseconds(100);
525 if (millis() - start > 4000) {
526 ESP_LOGE(
TAG,
"TX timeout — no TX_DONE IRQ (last_irq=0x%08" PRIX32
")", tx_irq);
535#ifdef IOHOME_FRAME_LOG
536 uint32_t
const tx_done_us = micros();
545 delayMicroseconds(this->post_tx_settle_us_);
552#ifdef IOHOME_FRAME_LOG
553 ESP_LOGD(
TAG,
"TX->RX re-arm: %" PRIu32
" us (+%u us settle)", micros() - tx_done_us, this->post_tx_settle_us_);
576 if (this->preamble_latched_at_timeout_) {
577 this->preamble_latched_at_timeout_ =
false;
587 if (buf ==
nullptr || buf_size == 0)
591 snprintf(buf, buf_size,
"none");
597 for (
size_t i = 0; i < bit_count; i++) {
598 if ((errors & bits[i].mask) == 0)
600 named |= bits[i].
mask;
601 const int n = snprintf(buf + pos, buf_size - pos,
"%s%s", pos > 0 ?
"|" :
"", bits[i].name);
602 if (n <= 0 ||
static_cast<size_t>(n) >= buf_size - pos)
604 pos +=
static_cast<size_t>(n);
607 const uint16_t unknown = errors &
static_cast<uint16_t
>(~named);
609 snprintf(buf + pos, buf_size - pos,
"%sUNKNOWN_0x%04X", pos > 0 ?
"|" :
"", unknown);
618 char errbuf[DEVICE_ERROR_STR_SIZE];
619 format(errors, errbuf,
sizeof(errbuf));
620 ESP_LOGW(tag,
"%s device errors after init: 0x%04X (%s)", chip_label, errors, errbuf);
628 char errbuf[DEVICE_ERROR_STR_SIZE];
630 ESP_LOGCONFIG(tag,
" Init device errors (cleared after init): 0x%04X (%s)", this->
init_device_errors_, errbuf);
bool is_dio_fired() const
Set by the ISR when DIO fires.
void note_reception_in_progress_()
Record that a frame is arriving right now.
virtual void set_mode_standby()=0
Switch to standby mode.
void clear_reception_in_progress_()
Drop the holdoff: the reception ended, was delivered, or was torn down deliberately.
void fail_(const char *reason)
Latch the failed state and record why, so is_failed and failure_reason can never disagree.
void prepare_nonblocking_receive_(RadioRxPacket &packet)
Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
bool failed_
Latched by fail_; see is_failed.
void prepare_blocking_receive_(RadioRxPacket &packet)
Common preamble for blocking receive: clear diagnostics and output packet.
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_.
void wait_busy_()
Wait until busy_pin_ reads low, feeding the watchdog while polling.
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.
void dump_init_device_errors_(const char *tag, DeviceErrorFormatter format) const
Log the init-time device errors under tag, or nothing when there were none.
int16_t raw_rssi_to_dbm_(uint8_t raw) const
Convert a chip's raw RSSI byte to an antenna-referred dBm reading.
virtual uint32_t rx_done_bit() const =0
void rearm_rx_after_tx_()
Minimal path back into RX immediately after a transmission — see the definition for why this is delib...
bool is_sync_detected() override
Check if sync word has been detected (while in RX).
virtual uint32_t preamble_detected_bit() const =0
static void build_gfsk_packet_params(const SoftPhyPacketParams &p, uint8_t out[GFSK_PACKET_PARAMS_LEN])
Fill the nine-byte GFSK SetPacketParams payload shared by both chips.
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.
uint16_t init_device_errors_
Device-error word a driver reads at the end of configure_radio_(), before it clears the chip's regist...
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 invalidate_stale_rx_content_after_tx()
Hook run from rearm_rx_after_tx_, after a transmission and before re-entering RX.
InternalGPIOPin * busy_pin_
BUSY line, read directly by both concrete drivers' own dump_debug() in addition to wait_busy_,...
static constexpr uint8_t GFSK_PACKET_PARAMS_LEN
Byte count of the GFSK SetPacketParams payload — identical on both software-PHY chips.
virtual uint32_t idle_rx_completion_budget_ms() const
Blocking budget for the idle-path length-driven receive, in milliseconds (issue #81).
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.
void record_init_device_errors_(const char *tag, const char *chip_label, uint16_t errors, DeviceErrorFormatter format)
Keep the device-error word read at the end of configure_radio_() and warn if it is non-zero.
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.
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.
uint16_t crc_ccitt(const uint8_t *data, uint8_t len)
CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
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).
void format_device_error_bits(uint16_t errors, const DeviceErrorBit *bits, size_t bit_count, char *buf, size_t buf_size)
Expand a device-error word into a human-readable NAME|NAME|... string.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
void(*)(uint16_t errors, char *buf, size_t buf_size) DeviceErrorFormatter
A chip's own decoder, as passed to SoftPhyDriverBase::dump_init_device_errors_().
static const char *const TAG
Shared driver flow for radios using the software PHY (SX1262, LR1121).
One named bit of a chip's device-error word.
uint16_t mask
The bit (or bits) of the error word this row names.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
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.
The GFSK SetPacketParams fields both software-PHY chips program identically.
uint16_t preamble_bytes
Byte-denominated, like every other preamble value in this codebase.
uint8_t crc_type
Chip-specific CRC-mode selector.
uint8_t payload_len
Configured payload length.
uint8_t sync_word_param
Chip-specific 24-bit sync-word selector.
uint8_t preamble_detector
Chip-specific detector-length selector.
uint8_t packet_type
Known-length / fixed-length selector.
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.