7#include "esphome/core/application.h"
8#include "esphome/core/log.h"
16static const char *
const TAG =
"home_io_control.exchange";
43 return "wait_first_response";
45 return "build_auth_response";
47 return "tx_auth_response";
49 return "wait_final_response";
66 return "tx_challenge";
68 return "wait_challenge_response";
88uint16_t auth_response_preamble(
const RadioDriver *radio) {
104uint32_t exchange_response_wait_slice_ms(
const RadioDriver *radio, uint32_t remaining_ms) {
105 const uint32_t max_slice =
107 return std::min<uint32_t>(remaining_ms, max_slice);
122void log_exchange_frame(
const char *stage,
int tries,
const IoFrame &frame, uint8_t len) {
123 ESP_LOGD(
TAG,
"%s try=%d cmd=0x%02X src=%02X%02X%02X dst=%02X%02X%02X len=%u", stage, tries, frame.
cmd, frame.
src[0],
124 frame.
src[1], frame.
src[2], frame.
dst[0], frame.
dst[1], frame.
dst[2], len);
136bool is_valid_final_response(
const IoFrame &candidate,
const IoFrame &request) {
175 response = context.
rx;
196 response = context.
rx;
234 const uint32_t deadline = millis() + ctx.
wait_ms;
235 while ((int32_t) (deadline - millis()) > 0) {
236 const uint32_t remaining = deadline - millis();
237 const uint32_t slice = exchange_response_wait_slice_ms(this->
radio_, remaining);
238 if (!this->
radio_->wait_for_packet(packet, slice)) {
239 if ((int32_t) (deadline - millis()) > 0)
250 log_exchange_frame(
"Ignored first response", ctx.
try_index, ctx.
rx, packet.
len);
258 ESP_LOGI(
TAG,
"Try %d ended: no first response for cmd=0x%02X within %u ms", ctx.
try_index, request.
cmd, ctx.
wait_ms);
281 ESP_LOGI(
TAG,
"Auth challenge try=%d wait_ms=%u challenge=%02X%02X%02X%02X%02X%02X req_cmd=0x%02X req_len=%u",
306 while ((int32_t) (deadline - millis()) > 0) {
307 const uint32_t remaining = deadline - millis();
308 const uint32_t slice = exchange_response_wait_slice_ms(this->
radio_, remaining);
309 if (!this->
radio_->wait_for_packet(packet, slice)) {
310 if ((int32_t) (deadline - millis()) > 0)
318 if (is_valid_final_response(ctx.
rx, request)) {
322 log_exchange_frame(
"Ignored final response", ctx.
try_index, ctx.
rx, packet.
len);
326 ESP_LOGI(
TAG,
"Try %d ended: no matching final response for cmd=0x%02X within %u ms", ctx.
try_index, request.
cmd,
360 if (!
parse(packet.
data, packet.
len, rx) || !frame_is_challenge_response(rx)) {
367 frame_data[0] = request.
cmd;
void reset_exchange_debug_(uint8_t request_cmd)
bool send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq)
Main request/response exchange with retry and automatic authentication.
bool transmit_request_(const IoFrame &request, uint32_t freq, uint16_t preamble, exchange::OutboundExchangeContext &ctx)
Wrap transmit_frame_ and mark context failed on error.
void record_exchange_debug_(const char *stage, uint8_t tries, bool saw_challenge)
void hop_frequency_()
Hop to the next channel in the 3‑channel sequence: CH1 → CH2 → CH3 → CH1.
decisions::ExchangeFirstResponseDisposition wait_for_first_response_(const IoFrame &request, exchange::OutboundExchangeContext &ctx)
Wait loop for the first response packet; classifies via decisions::classify_exchange_first_response.
decisions::ExchangeFinalResponseDisposition wait_for_final_response_(const IoFrame &request, exchange::OutboundExchangeContext &ctx)
Wait loop for the final authenticated response; uses is_valid_final_response().
bool transmit_frame_(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame on the current frequency with given preamble length.
bool handle_authentication_(const IoFrame &request, uint32_t freq, exchange::OutboundExchangeContext &ctx)
Perform challenge-response (TX auth response) after a 0x3C is received.
bool authenticate_request_(const IoFrame &request, uint32_t freq)
Handle an inbound authenticated command from a device (status updates, etc.).
Abstract radio driver for IO-Homecontrol.
virtual const char * chip_name() const =0
Get a human‑readable chip name.
IO-Homecontrol ESPHome component — protocol controller.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal exchange-state model for hub-owned authenticated non‑pairing flows.
bool verify_hmac(const uint8_t *data, uint8_t len, const uint8_t hmac[HMAC_SIZE], const uint8_t challenge[HMAC_SIZE], const uint8_t key[AES_KEY_SIZE])
Verify a received HMAC using constant-time comparison.
ExchangeFirstResponseDisposition classify_exchange_first_response(const IoFrame &request, const IoFrame &candidate)
Decide how to handle the first response packet in an authenticated exchange.
ExchangeFinalResponseDisposition
Disposition for the final response after authentication.
@ ACCEPT
Frame matches expected response — exchange succeeds.
@ IGNORE_UNRELATED
Frame doesn't match endpoints — ignore.
ExchangeFirstResponseDisposition
Disposition for the first response in an authenticated exchange.
@ IGNORE_UNRELATED
Frame doesn't match endpoints or failed parse — keep waiting.
@ COMPLETE_DIRECT
Matching non-challenge frame — operation complete, no auth needed.
ExchangeFinalResponseDisposition classify_exchange_final_response(const IoFrame &request, const IoFrame &candidate)
Decide if a candidate frame is an acceptable final response after authentication.
InboundAuthState
State machine for inbound authentication (device‑initiated commands).
@ WAIT_CHALLENGE_RESPONSE
Timer running; waiting for device's HMAC proof (0x3D).
@ VERIFIED
Device successfully authenticated; command is trusted.
@ TX_CHALLENGE
Challenge (0x3C) sent to device; awaiting 0x3D response.
@ IDLE
No inbound authentication in progress.
@ FAILED
Authentication failed (timeout or HMAC mismatch).
OutboundExchangeState
State machine for an outbound authenticated exchange (non‑pairing).
@ TX_REQUEST
Request frame transmitted; awaiting first response from device.
@ TX_AUTH_RESPONSE
Auth response (0x3D) transmitted; awaiting device's final reply.
@ IDLE
No active exchange; idle state.
@ BUILD_AUTH_RESPONSE
Building the 0x3D challenge response after receiving 0x3C.
@ FAILED
Exchange failed (timeout, retries exhausted, or radio error).
@ SUCCESS
Exchange completed successfully; device acknowledged.
@ WAIT_FIRST_RESPONSE
Listening for first response. This may be a challenge (0x3C) or the final response.
@ WAIT_FINAL_RESPONSE
Listening for the authenticated final response (e.g., status frame).
bool is_start(const IoFrame &f)
Check START flag.
static constexpr uint8_t FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
static constexpr int32_t EXCHANGE_RETRY_DELAY_MS
Gap between retries within one HA command.
bool create_challenge_resp(IoFrame &f, const uint8_t *dst, const uint8_t *src, const uint8_t challenge[HMAC_SIZE], const IoFrame &origin, const uint8_t *key)
Build a challenge response (0x3D) proving we know the system key.
static constexpr int32_t SX1262_EXCHANGE_RESPONSE_WAIT_SLICE_MS
SX1262-specific per-channel dwell while waiting for authenticated exchange responses.
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 uint8_t EXCHANGE_RETRY_COUNT
Attempts per command before reporting failure.
bool create_challenge_req(IoFrame &f, const uint8_t *dst, const uint8_t *src)
Build a challenge request (0x3C) containing 6 random bytes.
static constexpr uint16_t SHORT_PREAMBLE
8 bytes for response/continuation frames
static constexpr int32_t RESPONSE_WAIT_MS
Wait for response to non-start frame.
static constexpr uint8_t CMD_CHALLENGE_RESP
Controller responds with HMAC proof.
static constexpr uint16_t LONG_PREAMBLE
Preamble is a sequence of 0xAA bytes that precedes every frame.
static constexpr int32_t RESPONSE_AUTH_WAIT_MS
Wait for final response after challenge response.
static constexpr int32_t RESPONSE_CHANNEL_WAIT_MS
Per-channel dwell while waiting for an exchange response.
static const char *const TAG
static constexpr uint16_t SX1262_AUTH_RESPONSE_PREAMBLE
SX1262-specific preamble for the outbound 0x3D challenge response.
static constexpr int32_t RESPONSE_START_WAIT_MS
Wait for response to start frame (longer).
Command builders for the IO‑Homecontrol protocol.
Cryptographic helpers for the IO‑Homecontrol protocol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
uint8_t data_len
Actual length of data.
Raw packet received from the radio.
uint8_t len
Length of packet in bytes.
uint8_t data[RADIO_PACKET_BUFFER_SIZE]
Raw packet data buffer.
Context for a single inbound authentication (device‑initiated command).
IoFrame challenge
The 0x3C challenge frame we sent (needed to verify 0x3D response).
InboundAuthState state
Current authentication state.
Context carried across one outbound authenticated exchange.
uint32_t first_response_ms
Timestamp when the first valid response arrived (for RTT/timing).
uint8_t try_index
Current retry attempt (1‑based within EXCHANGE_RETRY_COUNT).
IoFrame rx
Most recent candidate frame received during the exchange.
uint32_t exchange_start_ms
Timestamp when the exchange attempt began (millis).
uint32_t wait_ms
Current timeout window for the active wait (ms).
OutboundExchangeState state
Current state machine state.
bool saw_challenge
True if a 0x3C challenge was received during this exchange.