Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
radio_soft_phy.h
Go to the documentation of this file.
1#pragma once
2
3/// @file radio_soft_phy.h
4/// @brief Software PHY for radios without IoHomeOn hardware framing.
5/// @ingroup hioc_radio
6///
7/// Chips such as the SX1262 and LR1121 have no hardware mode equivalent to the SX1276's
8/// IoHomeOn: they provide generic GFSK framing only. This header
9/// declares the chip-agnostic pieces such a driver needs to reproduce IO-Homecontrol framing
10/// in software — UART bit-encoding for TX, and a UART-decode probe (with CRC validation) to
11/// recover frame boundaries from an unaligned raw RX bitstream.
12
13#include "radio_interface.h"
14
15#include <cstdint>
16
17namespace esphome {
18namespace home_io_control {
19
20/// @brief Result of the UART probe: best candidate frame within a raw capture.
22 bool valid{false}; ///< A plausible frame was found.
23 uint8_t bit_offset{0}; ///< Bit offset where the best decode started.
24 uint8_t decoded_len{0}; ///< Total number of bytes decoded at that offset.
25 uint8_t frame_start{0}; ///< Index into decoded buffer where the frame begins.
26 uint8_t frame_len{0}; ///< Length of the candidate IoFrame (decoded bytes).
27 uint8_t decoded[RADIO_PACKET_BUFFER_SIZE]{}; ///< Full decoded UART stream at the chosen offset.
28};
29
30/// @brief Decode a UART-encoded bitstream from the given bit offset.
31uint8_t decode_uart_probe(const uint8_t *raw, uint8_t raw_len, uint8_t bit_offset, uint8_t *decoded,
32 uint8_t decoded_max_len);
33
34/// @brief Search raw RX buffer for the best CRC-validated IO-Homecontrol frame.
35UartProbeResult find_uart_probe(const uint8_t *raw, uint8_t raw_len);
36
37/// @brief Check if a command ID is one of the known IO-Homecontrol commands.
38///
39/// This is a gate, not a directory: find_uart_probe() only accepts a CRC-valid candidate whose
40/// cmd passes this check (or whose CTRL0_PROTOCOL_1W bit is set), so a command missing here makes
41/// every SX1262/LR1121 reception of that opcode silently unrecoverable on this software PHY —
42/// the frame is on air, its CRC matches, and it still never reaches the parser. Exposed (out of
43/// radio_soft_phy.cpp's anonymous namespace) so tests can iterate every accepted command directly
44/// instead of hand-maintaining a parallel list that can drift out of sync with this one.
45/// @param cmd Command byte.
46/// @return true if cmd matches a known command constant.
47bool is_known_io_command(uint8_t cmd);
48
49/// @brief Bits an on-air UART cell spends per protocol byte: start(1) + data(8) + stop(1).
50static constexpr uint8_t UART_CELL_BITS = 10;
51
52/// @brief Number of leading bit alignments the UART probe tries when locating a frame start.
53///
54/// The chip hands us raw bytes with no guaranteed cell alignment, so decode_uart_probe() sweeps
55/// starting positions 0..UART_PROBE_MAX_BIT_OFFSET-1. A recovered frame can therefore begin up to
56/// `UART_PROBE_MAX_BIT_OFFSET - 1` bits into the raw buffer — buffer-sizing asserts must budget
57/// for that slack on top of the frame's own packed length.
58static constexpr uint8_t UART_PROBE_MAX_BIT_OFFSET = 10;
59
60/// @brief Raw on-air bytes needed to carry a whole frame: `frame_len` protocol bytes plus the
61/// two trailing CRC bytes, each UART-packed into a 10-bit cell.
62///
63/// This is what makes a length-driven receive possible at all: a frame's own size is knowable
64/// from its first decoded byte, so the raw byte count it will occupy is knowable too — no chip
65/// needs to tell us where the frame ends.
66/// @param frame_len Protocol frame length in bytes (CTRL0's own length field, +1).
67/// @return Raw byte count, rounded up to whole bytes.
68uint8_t soft_phy_raw_bytes_for_frame(uint8_t frame_len);
69
70/// Protocol line rate. The same 38400 bps every driver programs into its own bitrate register.
71static constexpr uint32_t SOFT_PHY_LINE_RATE_BPS = 38400;
72/// Microseconds in a second, for the air-time arithmetic below.
73static constexpr uint32_t SOFT_PHY_US_PER_SECOND = 1000000;
74
75/// @brief On-air time in microseconds for `raw_bytes` bytes at the protocol's line rate.
76///
77/// One byte is 8 / 38400 s = 208.333 µs. Computed as an integer division rounded *up*, so the
78/// result never falls short of a whole byte's air time and a caller that waits on it never reads
79/// the chip's buffer early. The numerator is 64-bit: in 32 bits it would wrap above 536 bytes, and
80/// a 1024-byte wake-up preamble is a real transmission length.
81constexpr uint32_t soft_phy_air_time_us(uint32_t raw_bytes) {
82 const uint64_t bit_periods = static_cast<uint64_t>(raw_bytes) * BITS_PER_BYTE * SOFT_PHY_US_PER_SECOND;
83 return static_cast<uint32_t>((bit_periods + SOFT_PHY_LINE_RATE_BPS - 1) / SOFT_PHY_LINE_RATE_BPS);
84}
85
86/// Sync word bytes on air after the preamble (`55 FF 33` on the SX1276, the UART-coded equivalent
87/// on the software PHY): three on every 868 MHz chip.
88static constexpr uint16_t IO868_SYNC_WORD_BYTES = 3;
89
90/// @brief Time on air of one 868 MHz transmission: preamble, sync word, and the frame plus its CRC,
91/// each byte of the latter a 10-bit UART cell, at the protocol's line rate.
92///
93/// The same line coding applies to every 868 MHz chip (the SX1276's IoHomeOn coder and the
94/// software PHY produce the same waveform), so every 868 driver's `tx_air_time_us()` is this.
95/// @param preamble_len Preamble length in bytes.
96/// @param frame_len Frame length in bytes, without the CRC.
97/// @return Air time in microseconds, rounded up.
98uint32_t io868_tx_air_time_us(uint16_t preamble_len, uint8_t frame_len);
99
100/// @brief Recover a frame's total length from the very first UART cell of a reception.
101///
102/// CTRL0 bits [4:0] hold `frame_length - 1` (see proto_frame.h), and CTRL0 is the first byte
103/// after the sync word — so ten bits of air time are enough to learn how long the whole frame
104/// will be. Alignment is not yet known at that point, so every probe offset is tried and the
105/// largest plausible answer wins: over-waiting by a few bytes costs a little latency, whereas
106/// under-waiting would truncate the frame.
107/// @param raw Raw bytes read from the chip's data buffer, starting at the reception's own offset.
108/// @param raw_len Number of raw bytes available (three is enough at any alignment).
109/// @return Plausible frame length in bytes, or 0 when no offset yields one.
110uint8_t soft_phy_peek_frame_length(const uint8_t *raw, uint8_t raw_len);
111
112/// @brief UART-encode a buffer of bytes (start bit 0, 8 data bits LSB-first, stop bit 1).
113/// @return Number of encoded bytes, or 0 if the output buffer is too small.
114uint8_t uart_encode_packet(const uint8_t *data, uint8_t len, uint8_t *encoded, uint8_t encoded_max_len);
115
116} // namespace home_io_control
117} // namespace esphome
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.
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.
bool is_known_io_command(uint8_t cmd)
This list missing CMD_GET_GENERAL_INFO3_RESP (0x59) is exactly what turned a real Q2 probe reply into...
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).
static constexpr uint8_t UART_PROBE_MAX_BIT_OFFSET
Number of leading bit alignments the UART probe tries when locating a frame start.
static constexpr uint16_t IO868_SYNC_WORD_BYTES
Sync word bytes on air after the preamble (55 FF 33 on the SX1276, the UART-coded equivalent on the s...
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 uint32_t SOFT_PHY_US_PER_SECOND
Microseconds in a second, for the air-time arithmetic below.
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...
uint32_t io868_tx_air_time_us(uint16_t preamble_len, uint8_t frame_len)
Time on air of one 868 MHz transmission: preamble, sync word, and the frame plus its CRC,...
static constexpr uint32_t SOFT_PHY_LINE_RATE_BPS
Protocol line rate. The same 38400 bps every driver programs into its own bitrate register.
constexpr uint8_t RADIO_PACKET_BUFFER_SIZE
Scratch buffer size for raw radio packets and recovered frames.
static constexpr uint8_t UART_CELL_BITS
Bits an on-air UART cell spends per protocol byte: start(1) + data(8) + stop(1).
Radio abstraction layer for IO-Homecontrol.
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.