16#include "esphome/core/log.h"
17#include "esphome/core/application.h"
26static const char *
const TAG =
"home_io_control.soft_phy";
34bool wait_for_air_time(uint32_t sync_us, uint8_t raw_bytes, uint32_t start_ms, uint32_t timeout_ms) {
35 uint32_t
const needed_us =
soft_phy_air_time_us((uint32_t) raw_bytes + SOFT_PHY_EARLY_READ_MARGIN_BYTES);
36 while (micros() - sync_us < needed_us) {
39 if (millis() - start_ms > timeout_ms)
42 delayMicroseconds(SOFT_PHY_EARLY_POLL_US);
63 ESP_LOGE(
TAG,
"BUSY timeout after %" PRIu32
" ms", r.
elapsed_ms);
64 this->
fail_(
"BUSY pin stayed high -- check busy_pin, the SPI wiring and tcxo_voltage");
68 this->busy_overruns_++;
69 ESP_LOGD(
TAG,
"BUSY wait overran to %" PRIu32
" ms; chip ready (task preempted)", r.
elapsed_ms);
80 uint32_t
const start = millis();
84 if (!this->poll_until_activity_(start, timeout_ms, irq)) {
90 bool early_completed =
false;
91 if (!this->resolve_sync_race_(start, timeout_ms, irq, packet, early_completed)) {
94 if (early_completed) {
99 return this->finalize_receive_(packet, irq);
107bool SoftPhyDriverBase::poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq) {
113 this->preamble_hold_active_ =
false;
116 if (millis() - start > timeout_ms) {
120 if (!this->hold_rx_for_preamble_(irq))
129bool SoftPhyDriverBase::hold_rx_for_preamble_(uint32_t irq) {
132 const uint32_t now = millis();
133 if (!this->preamble_hold_active_) {
134 this->preamble_hold_active_ =
true;
135 this->preamble_hold_since_ms_ = now;
140 return now - this->preamble_hold_since_ms_ < SOFT_PHY_PREAMBLE_HOLD_MS;
147bool SoftPhyDriverBase::resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq,
RadioRxPacket &packet,
148 bool &early_completed) {
149 early_completed =
false;
157 uint32_t
const sync_us = micros();
162 timeout_ms = std::max(timeout_ms, (millis() - start) + SOFT_PHY_POST_SYNC_GRACE_MS);
164 if (this->try_early_completion_(packet, sync_us, irq, start, timeout_ms)) {
165 early_completed =
true;
168 while (millis() - start <= timeout_ms) {
200bool SoftPhyDriverBase::try_early_completion_(
RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status,
201 uint32_t start_ms, uint32_t timeout_ms) {
205 if (timeout_ms < SOFT_PHY_EARLY_MIN_WINDOW_MS)
207 auto const offset = (uint8_t) base;
210 if (!wait_for_air_time(sync_us, SOFT_PHY_EARLY_HEADER_RAW_BYTES, start_ms, timeout_ms))
212 uint8_t header[SOFT_PHY_EARLY_HEADER_RAW_BYTES] = {0};
222 if (!wait_for_air_time(sync_us, frame_raw_len, start_ms, timeout_ms))
225 auto const read_len =
226 (uint8_t) std::min<uint16_t>((uint16_t) frame_raw_len + SOFT_PHY_EARLY_READ_MARGIN_BYTES,
sizeof(raw));
234 memcpy(packet.data, probe.decoded + probe.frame_start, probe.frame_len);
235 packet.len = probe.frame_len;
239 this->
fill_capture_info(
true, irq_status, offset, read_len, raw, read_len, packet.data, packet.len);
241#ifdef IOHOME_FRAME_LOG
242 frame_log_line(
FrameLogLevel::DEBUG,
TAG,
"Early RX: frame_len=%u raw_len=%u air_us=%" PRIu32, frame_len, read_len,
243 (uint32_t) (micros() - sync_us));
244 log_frame(
"RX", packet.data, packet.len, this->current_freq_);
255bool SoftPhyDriverBase::finalize_receive_(
RadioRxPacket &packet, uint32_t irq) {
283 this->preamble_hold_active_ =
false;
297 out[0] =
static_cast<uint8_t
>(preamble_bits >> 8);
298 out[1] =
static_cast<uint8_t
>(preamble_bits);
317 this->preamble_hold_active_ =
false;
327 uint8_t raw_reported_len = 0;
328 uint8_t rx_offset = 0;
333 uint8_t
const reported_len = std::min(raw_reported_len, (uint8_t)
sizeof(rx_buf));
334 uint8_t raw_probe_len = reported_len;
335 if (reported_len > 0 && reported_len < 32) {
339 raw_probe_len =
sizeof(rx_buf);
341 if (reported_len == SOFT_PHY_RX_PROBE_PACKET_LEN)
342 raw_probe_len = SOFT_PHY_RX_PROBE_PACKET_LEN;
343 if (raw_probe_len > 0)
355#ifdef IOHOME_FRAME_LOG
361 "UART probe: valid=1 bit_offset=%u frame_start=%u frame_len=%u decoded_len=%u rx_offset=%u",
365#ifdef IOHOME_FRAME_LOG
368 char hex_buf[97] = {0};
369 uint8_t dump_len = std::min(raw_probe_len, (uint8_t) 32);
370 for (uint8_t i = 0; i < dump_len; i++)
371 snprintf(hex_buf + (i * 3), 4,
"%02X ", rx_buf[i]);
382 int best_len = std::min<int>(probe.
decoded_len, FRAME_MAX_WIRE_SIZE);
384 for (
int cl = best_len; cl >= FRAME_MIN_SIZE; cl--) {
389 uint16_t received = (uint16_t) probe.
decoded[cl] | ((uint16_t) probe.
decoded[cl + 1] << 8);
391 " CRC check: candidate_len=%d cmd=0x%02X crc_computed=0x%04X crc_received=0x%04X %s", cl,
392 test_frame.
cmd, computed, received, computed == received ?
"MATCH" :
"MISMATCH");
402 uint8_t
const copy_len = std::min(reported_len, FRAME_MAX_WIRE_SIZE);
404 memcpy(packet.
data, rx_buf, copy_len);
405 packet.
len = copy_len;
408 this->
fill_capture_info(blocking_wait, irq_status, rx_offset, reported_len, rx_buf, raw_probe_len, packet.
data,
411#ifdef IOHOME_FRAME_LOG
413 log_frame(
"RX", packet.
data, packet.
len, this->current_freq_);
416 return packet.
len > 0;
457 uint32_t
const sync_us = micros();
458 uint32_t
const start_ms = millis();
470 bool const completed =
495#ifdef IOHOME_FRAME_LOG
505 uint8_t frame_with_crc[FRAME_MAX_WIRE_SIZE] = {0};
507 if ((uint16_t) len + 2 > (uint16_t)
sizeof(frame_with_crc))
510 memcpy(frame_with_crc, data, len);
511 const uint16_t crc =
crc_ccitt(data, len);
512 frame_with_crc[len] = crc & 0xFF;
513 frame_with_crc[len + 1] = (crc >> 8) & 0xFF;
515 const uint8_t encoded_len =
uart_encode_packet(frame_with_crc, len + 2, tx_buf,
sizeof(tx_buf));
516 if (encoded_len == 0)
533 uint32_t
const air_time_us = this->
tx_air_time_us(len, tx_config);
535 uint32_t
const start = millis();
539 if (millis() - start > timeout_ms) {
543 "TX timeout after %" PRIu32
" ms (air time %" PRIu32
" us) — DIO/IRQ pin never fired, IRQ status "
545 timeout_ms, air_time_us, tx_irq);
551 delayMicroseconds(100);
564 if (millis() - start > timeout_ms) {
565 ESP_LOGE(
TAG,
"TX timeout after %" PRIu32
" ms — no TX_DONE IRQ (last_irq=0x%08" PRIX32
")", timeout_ms, tx_irq);
575#ifdef IOHOME_FRAME_LOG
576 uint32_t
const tx_done_us = micros();
585 delayMicroseconds(this->post_tx_settle_us_);
592#ifdef IOHOME_FRAME_LOG
593 frame_log_line(
FrameLogLevel::DEBUG,
TAG,
"TX->RX re-arm: %" PRIu32
" us (+%u us settle)", micros() - tx_done_us,
594 this->post_tx_settle_us_);
603 this->preamble_hold_active_ =
false;
625 if (buf ==
nullptr || buf_size == 0)
629 snprintf(buf, buf_size,
"none");
635 for (
size_t i = 0; i < bit_count; i++) {
636 if ((errors & bits[i].mask) == 0)
638 named |= bits[i].
mask;
639 const int n = snprintf(buf + pos, buf_size - pos,
"%s%s", pos > 0 ?
"|" :
"", bits[i].name);
640 if (n <= 0 ||
static_cast<size_t>(n) >= buf_size - pos)
642 pos +=
static_cast<size_t>(n);
645 const uint16_t unknown = errors &
static_cast<uint16_t
>(~named);
647 snprintf(buf + pos, buf_size - pos,
"%sUNKNOWN_0x%04X", pos > 0 ?
"|" :
"", unknown);
656 char errbuf[DEVICE_ERROR_STR_SIZE];
657 format(errors, errbuf,
sizeof(errbuf));
658 ESP_LOGW(tag,
"%s device errors after init: 0x%04X (%s)", chip_label, errors, errbuf);
666 char errbuf[DEVICE_ERROR_STR_SIZE];
668 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.
std::optional< int16_t > channel_busy_level_(const RadioTxConfig &tx_config, uint32_t settle_us)
The clear-channel check a driver runs at the top of send_packet().
void fail_(const char *reason)
Latch the failed state and record why, so is_failed and failure_reason can never disagree.
void prepare_nonblocking_receive_(RadioRxPacket &packet)
Common preamble for non‑blocking receive: clear diagnostics, output packet, and DIO latch.
bool failed_
Latched by fail_; see is_failed.
void prepare_blocking_receive_(RadioRxPacket &packet)
Common preamble for blocking receive: clear diagnostics and output packet.
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).
TxResult 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 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).
uint32_t tx_air_time_us(uint8_t len, const RadioTxConfig &cfg) const override
Transmit time: the preamble, sync word and UART-coded frame at the protocol line rate.
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.
virtual void log_tx_timeout_state(uint32_t irq_status)
Log the chip's own state after a transmission that never reported TX-done, before the driver forces s...
int16_t read_rssi() override
Read instantaneous RSSI (in dBm) on the current channel, 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.
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).
PollResult poll_until(Ready ready, uint32_t timeout_ms)
Poll ready() until it is true or timeout_ms has passed, feeding the watchdog between samples.
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...
constexpr uint32_t tx_done_timeout_ms(uint32_t air_time_us)
How long a driver waits for TX-done after starting a transmission.
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
Preemption-safe "poll a condition until it holds or a deadline passes" helper.
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).
uint32_t elapsed_ms
Time from the first sample to the last clock read before the deciding one.
bool ready
The condition held on the deciding sample.
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint32_t freq_hz
Frequency the packet was received on (Hz).
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.
Configuration for transmitting a packet: carrier frequency, preamble length, wake-up level and clear-...
uint16_t preamble_len
Preamble length in symbol periods (bytes).
uint32_t freq_hz
Carrier frequency in Hz.
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.
What send_packet() did with a frame.
static TxResult channel_busy(int16_t level_dbm)
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.