15#include "esphome/core/log.h"
16#include "esphome/core/application.h"
23static const char *
const TAG =
"home_io_control.soft_phy";
31bool wait_for_air_time(uint32_t sync_us, uint8_t raw_bytes, uint32_t start_ms, uint32_t timeout_ms) {
33 while (micros() - sync_us < needed_us) {
36 if (millis() - start_ms > timeout_ms)
55 uint32_t
const start = millis();
57 if (millis() - start > this->busy_timeout_ms_) {
58 ESP_LOGE(
TAG,
"BUSY timeout");
73 uint32_t
const start = millis();
77 if (!this->poll_until_activity_(start, timeout_ms, irq)) {
83 bool early_completed =
false;
84 if (!this->resolve_sync_race_(start, timeout_ms, irq, packet, early_completed)) {
87 if (early_completed) {
92 return this->finalize_receive_(packet, irq);
100bool SoftPhyDriverBase::poll_until_activity_(uint32_t start, uint32_t timeout_ms, uint32_t &irq) {
107 this->preamble_latched_at_timeout_ =
false;
110 if (millis() - start > timeout_ms) {
127bool SoftPhyDriverBase::resolve_sync_race_(uint32_t start, uint32_t timeout_ms, uint32_t &irq,
RadioRxPacket &packet,
128 bool &early_completed) {
129 early_completed =
false;
137 uint32_t
const sync_us = micros();
141 if (this->try_early_completion_(packet, sync_us, irq, start, timeout_ms)) {
142 early_completed =
true;
145 while (millis() - start <= timeout_ms) {
177bool SoftPhyDriverBase::try_early_completion_(
RadioRxPacket &packet, uint32_t sync_us, uint32_t irq_status,
178 uint32_t start_ms, uint32_t timeout_ms) {
184 auto const offset = (uint8_t) base;
199 if (!wait_for_air_time(sync_us, frame_raw_len, start_ms, timeout_ms))
202 auto const read_len =
211 memcpy(packet.data, probe.decoded + probe.frame_start, probe.frame_len);
212 packet.len = probe.frame_len;
216 this->
fill_capture_info(
true, irq_status, offset, read_len, raw, read_len, packet.data, packet.len);
218#ifdef IOHOME_FRAME_LOG
219 ESP_LOGD(
TAG,
"Early RX: frame_len=%u raw_len=%u air_us=%" PRIu32, frame_len, read_len,
220 (uint32_t) (micros() - sync_us));
221 log_frame(
"RX", packet.data, packet.len, this->current_freq_);
232bool SoftPhyDriverBase::finalize_receive_(
RadioRxPacket &packet, uint32_t irq) {
273 out[0] =
static_cast<uint8_t
>(preamble_bits >> 8);
274 out[1] =
static_cast<uint8_t
>(preamble_bits);
302 uint8_t raw_reported_len = 0;
303 uint8_t rx_offset = 0;
308 uint8_t
const reported_len = std::min(raw_reported_len, (uint8_t)
sizeof(rx_buf));
309 uint8_t raw_probe_len = reported_len;
310 if (reported_len > 0 && reported_len < 32) {
314 raw_probe_len =
sizeof(rx_buf);
318 if (raw_probe_len > 0)
330#ifdef IOHOME_FRAME_LOG
335 ESP_LOGD(
TAG,
"UART probe: valid=1 bit_offset=%u frame_start=%u frame_len=%u decoded_len=%u rx_offset=%u",
339#ifdef IOHOME_FRAME_LOG
342 char hex_buf[97] = {0};
343 uint8_t dump_len = std::min(raw_probe_len, (uint8_t) 32);
344 for (uint8_t i = 0; i < dump_len; i++)
345 snprintf(hex_buf + (i * 3), 4,
"%02X ", rx_buf[i]);
346 ESP_LOGW(
TAG,
"UART probe: valid=0 decoded_len=%u raw_probe_len=%u", probe.
decoded_len, raw_probe_len);
347 ESP_LOGW(
TAG,
" raw[0..%u]: %s", dump_len - 1, hex_buf);
362 uint16_t received = (uint16_t) probe.
decoded[cl] | ((uint16_t) probe.
decoded[cl + 1] << 8);
363 ESP_LOGW(
TAG,
" CRC check: candidate_len=%d cmd=0x%02X crc_computed=0x%04X crc_received=0x%04X %s", cl,
364 test_frame.
cmd, computed, received, computed == received ?
"MATCH" :
"MISMATCH");
376 memcpy(packet.
data, rx_buf, copy_len);
377 packet.
len = copy_len;
380 this->
fill_capture_info(blocking_wait, irq_status, rx_offset, reported_len, rx_buf, raw_probe_len, packet.
data,
383#ifdef IOHOME_FRAME_LOG
385 log_frame(
"RX", packet.
data, packet.
len, this->current_freq_);
388 return packet.
len > 0;
429 uint32_t
const sync_us = micros();
430 uint32_t
const start_ms = millis();
442 bool const completed =
464#ifdef IOHOME_FRAME_LOG
476 if ((uint16_t) len + 2 > (uint16_t)
sizeof(frame_with_crc))
479 memcpy(frame_with_crc, data, len);
480 const uint16_t crc =
crc_ccitt(data, len);
481 frame_with_crc[len] = crc & 0xFF;
482 frame_with_crc[len + 1] = (crc >> 8) & 0xFF;
484 const uint8_t encoded_len =
uart_encode_packet(frame_with_crc, len + 2, tx_buf,
sizeof(tx_buf));
485 if (encoded_len == 0)
501 uint32_t
const start = millis();
505 if (millis() - start > 4000) {
506 ESP_LOGE(
TAG,
"TX timeout — DIO/IRQ pin never fired");
511 delayMicroseconds(100);
524 if (millis() - start > 4000) {
525 ESP_LOGE(
TAG,
"TX timeout — no TX_DONE IRQ (last_irq=0x%08" PRIX32
")", tx_irq);
534#ifdef IOHOME_FRAME_LOG
535 uint32_t
const tx_done_us = micros();
544 delayMicroseconds(this->post_tx_settle_us_);
551#ifdef IOHOME_FRAME_LOG
552 ESP_LOGD(
TAG,
"TX->RX re-arm: %" PRIu32
" us (+%u us settle)", micros() - tx_done_us, this->post_tx_settle_us_);
575 if (this->preamble_latched_at_timeout_) {
576 this->preamble_latched_at_timeout_ =
false;
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 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_.
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.
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.
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
bool failed_
Set on a BUSY timeout or a chip-identity check failing; see is_failed.
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.
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).
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_WIRE_SIZE
Largest number of bytes a buffer must hold to receive or transmit any frame this project knows about,...
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).
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.