Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
exchange_engine.h
Go to the documentation of this file.
1#pragma once
2
3/// @file exchange_engine.h
4/// @brief Self-contained authenticated exchange engine for IO-Homecontrol 2W.
5/// @ingroup hioc_hub
6///
7/// ExchangeEngine encapsulates the outbound authenticated exchange state
8/// machine and the inbound challenge-response authentication path. It owns:
9/// - `send_and_receive()` — retry loop with challenge/response support.
10/// - `authenticate_request()` — verify a device-initiated command via HMAC.
11/// - `collect_broadcast_responses()` — send once, report every matching reply in a window.
12/// - Transmit with LBT (listen-before-talk) and frequency hopping.
13/// - Exchange debug snapshot captured on every attempt.
14///
15/// The engine holds double-pointer and raw-pointer references to its owner's
16/// state (radio driver, node/system key, tuning config) so that changes made
17/// by the hub after construction (e.g., radio driver allocation in setup(),
18/// direct member writes in unit tests) are automatically visible.
19///
20/// All transmit and channel-hop traffic funnels through this engine: the hub's
21/// thin `transmit_frame_()` / `hop_frequency_()` wrappers delegate here, and
22/// `PairingEngine` holds a direct reference for its own exchanges.
23
24#include "hub_exchange.h"
25#include "hub_decisions.h"
26#include "pairing_telemetry.h"
27#include "proto_frame.h"
28#include "proto_timing.h"
29#include "radio_interface.h"
30#include "tuning_config.h"
31
32#include <cstdint>
33#include <functional>
34
35namespace esphome {
36namespace home_io_control {
37
38/// @brief Authenticated exchange engine — outbound and inbound protocol flows.
39///
40/// All timing constants (retry count/delay, response windows) come from
41/// proto_timing.h; per-chip dwell overrides are queried from the RadioDriver.
43 public:
44 /// Construct the engine with double-pointer indirection into the hub's
45 /// RadioDriver pointer and direct pointers to the node/key byte arrays and
46 /// the tuning config. Pointers must remain valid for the lifetime of the
47 /// engine (guaranteed because hub owns all referenced members).
48 /// @param radio_ptr Address of the hub's `RadioDriver *radio_` member.
49 /// @param node_id Pointer to the hub's `node_id_[NODE_ID_SIZE]` array.
50 /// @param system_key Pointer to the hub's `system_key_[AES_KEY_SIZE]` array.
51 /// @param tuning Pointer to the hub's `TuningConfig tuning_` member.
52 ExchangeEngine(RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key,
53 const TuningConfig *tuning);
54
55 // Non-copyable; the hub owns exactly one engine tied to its member addresses.
56 ExchangeEngine(const ExchangeEngine &) = delete;
58
59 // -------------------------------------------------------------------------
60 // Core exchange API
61 // -------------------------------------------------------------------------
62
63 /// Execute an outbound authenticated exchange with retry.
64 /// @param request Frame to transmit (cmd + endpoints already filled).
65 /// @param response Populated on success.
66 /// @param freq RF channel frequency (Hz).
67 /// @return true if device responded within retry budget; false otherwise.
68 bool send_and_receive(const IoFrame &request, IoFrame &response, uint32_t freq);
69
70 /// Authenticate an inbound device command via 0x3C challenge / 0x3D HMAC.
71 /// @param request The received inbound frame (e.g., CMD_STATUS_UPDATE).
72 /// @param freq RF channel the frame arrived on.
73 /// @return true if HMAC verified; false on timeout or mismatch.
74 bool authenticate_request(const IoFrame &request, uint32_t freq);
75
76 /// @brief Invoked for each matching broadcast reply, as it arrives.
77 /// @param frame Parsed reply frame (responder's address is `frame.src`).
78 /// @param rssi_dbm RSSI of this reply.
79 using BroadcastReplyHandler = std::function<void(const IoFrame &frame, int16_t rssi_dbm)>;
80
81 /// Transmit `request` once and hand every matching broadcast reply to `on_reply` within
82 /// `window_ms`.
83 ///
84 /// Unlike send_and_receive(), this neither retries the transmit nor performs any
85 /// authentication — a broadcast roll-call has no per-transaction proof, so replies are
86 /// informational only (see the caller's protocol notes). Every candidate packet is parsed
87 /// and checked against `expected_cmd` and `node_id_` (as the frame's destination); anything
88 /// that fails `parse()`, carries a different `cmd`, or is not addressed to us is ignored
89 /// without ending collection. Silence on a slice hops to the next channel, exactly like
90 /// wait_for_first_response_(), so replies arriving on any of the three channels are caught.
91 ///
92 /// This method stores nothing and imposes no capacity: it neither buffers replies nor
93 /// deduplicates them, so the same responder answering twice within one window invokes
94 /// `on_reply` twice. Storage, deduplication, and any capacity limit belong to the caller,
95 /// which knows how little of each reply it actually needs to keep — see
96 /// `ManagementActions::scan_paired_devices()`, which decodes each frame on arrival into a
97 /// compact record rather than retaining whole frames. Collection always runs to the deadline.
98 /// @param request Frame to transmit once (cmd + endpoints already filled).
99 /// @param freq RF channel frequency (Hz) for the initial transmit.
100 /// @param expected_cmd Command byte a reply must carry to be considered.
101 /// @param window_ms How long to listen after the transmit, in milliseconds. The caller
102 /// owns this budget explicitly (rather than this method reading
103 /// `tuning_->pairing_discovery_wait_ms` itself) because a caller that
104 /// transmits more than once needs to divide one total time budget across
105 /// several calls — see `ManagementActions::scan_paired_devices()`.
106 /// @param on_reply Invoked once per matching reply, before the next packet is awaited, so
107 /// the handler must be cheap and must not block. Keep captures to a few
108 /// pointers: small callables avoid std::function's heap fallback on the
109 /// implementations this project builds against.
110 /// @return Number of matching replies handed to `on_reply` (duplicates included).
111 uint8_t collect_broadcast_responses(const IoFrame &request, uint32_t freq, uint8_t expected_cmd, uint32_t window_ms,
112 const BroadcastReplyHandler &on_reply);
113
114 // -------------------------------------------------------------------------
115 // Infrastructure delegated from the hub
116 // -------------------------------------------------------------------------
117
118 /// Transmit a raw IoFrame with LBT and the given preamble length.
119 /// @param frame Frame to transmit.
120 /// @param freq RF frequency in Hz.
121 /// @param preamble Preamble length (LONG_PREAMBLE or SHORT_PREAMBLE).
122 /// @return true if the radio accepted the packet; false otherwise.
123 bool transmit_frame(const IoFrame &frame, uint32_t freq, uint16_t preamble);
124
125 /// Advance to the next IO-Homecontrol channel (CH1→CH2→CH3→CH1).
126 /// Respects the protocol-defined minimum dwell time (HOP_TIME_US).
127 void hop_frequency();
128
129 /// Unconditionally hop only if the minimum dwell has elapsed.
130 /// Called from the hub's `loop()` to honour passive channel scanning.
131 void maybe_hop();
132
133 /// Reset the hop-timer (called after radio init in hub setup()).
134 void reset_hop_timestamp();
135
136 // -------------------------------------------------------------------------
137 // Pairing telemetry hook
138 // -------------------------------------------------------------------------
139
140 /// Attach a telemetry recorder so transmit_frame()'s LBT loop records defer events.
141 /// Set by PairingEngine for the duration of a `discover_and_pair()` attempt only — nullptr
142 /// (the default) for every non-pairing exchange, which is the common case and stays a no-op.
143 /// @param telemetry Non-owning pointer, or nullptr to detach.
144 void set_pairing_telemetry(PairingTelemetry *telemetry) { this->pairing_telemetry_ = telemetry; }
145
146 // -------------------------------------------------------------------------
147 // Exchange debug snapshot
148 // -------------------------------------------------------------------------
149
150 /// @brief Snapshot of the last exchange attempt for diagnostics.
151 struct DebugInfo {
152 const char *stage{"idle"}; ///< Last recorded stage label.
153 uint8_t tries{0}; ///< Retry count (1-based).
154 uint8_t request_cmd{0}; ///< Command ID of the original request.
155 bool saw_challenge{false}; ///< True if a 0x3C was seen during this exchange.
156 bool capture_valid{false}; ///< True if radio capture is meaningful.
157 bool capture_rx_done{false}; ///< True if RxDone IRQ fired.
158 bool capture_crc_error{false}; ///< True if CRC error flagged (chip-dependent; see RadioCaptureInfo::crc_error).
159 uint32_t capture_freq_hz{0}; ///< RF frequency of the captured packet.
160 uint16_t capture_irq_status{0}; ///< Raw IRQ register value.
161 uint8_t capture_packet_status{0}; ///< Chip packet-status byte.
162 uint8_t capture_reported_len{0}; ///< Length reported by radio packet engine.
163 uint8_t capture_frame_len{0}; ///< Parsed protocol frame length.
164 int16_t capture_rssi_dbm{0}; ///< RSSI of the captured packet (dBm).
165 };
166
167 /// Clear the debug snapshot and record the upcoming request command.
168 void reset_debug(uint8_t request_cmd);
169
170 /// Update the debug snapshot with the current stage and radio capture.
171 void record_debug(const char *stage, uint8_t tries, bool saw_challenge);
172
173 /// Log the debug snapshot as a WARN-level structured line.
174 /// @param device_id Human-readable device identifier for the log line.
175 void log_debug(const char *device_id) const;
176
177 /// Read-only access to the current debug snapshot.
178 [[nodiscard]] const DebugInfo &get_debug() const { return debug_; }
179
180 private:
181 // --- Outbound exchange step helpers --------------------------------------
182
183 /// Transmit one request attempt and update context state on failure.
184 bool transmit_request_(const IoFrame &request, uint32_t freq, uint16_t preamble,
186
187 /// Block until the first response arrives or the wait window expires.
188 decisions::ExchangeFirstResponseDisposition wait_for_first_response_(const IoFrame &request,
190
191 /// Send the 0x3D challenge response after receiving a 0x3C from the device.
192 bool handle_authentication_(const IoFrame &request, uint32_t freq, exchange::OutboundExchangeContext &ctx);
193
194 /// Block until the final authenticated response arrives or the window expires.
195 decisions::ExchangeFinalResponseDisposition wait_for_final_response_(const IoFrame &request,
197
198 // --- Dependencies (back-references into the hub) -------------------------
199
200 RadioDriver **radio_ptr_; ///< Double-pointer: *radio_ptr_ is always the hub's active driver.
201 const uint8_t *node_id_; ///< Hub's node_id_[NODE_ID_SIZE] array.
202 const uint8_t *system_key_; ///< Hub's system_key_[AES_KEY_SIZE] array.
203 const TuningConfig *tuning_; ///< Hub's live TuningConfig (read on every LBT check).
204 PairingTelemetry *pairing_telemetry_{nullptr}; ///< Set only during a pairing attempt; see set_pairing_telemetry().
205
206 // --- Engine state --------------------------------------------------------
207
208 uint32_t last_hop_us_{0}; ///< Timestamp of the last channel hop (µs, from micros()).
209 DebugInfo debug_{}; ///< Snapshot updated throughout each exchange attempt.
210};
211
212} // namespace home_io_control
213} // namespace esphome
std::function< void(const IoFrame &frame, int16_t rssi_dbm)> BroadcastReplyHandler
Invoked for each matching broadcast reply, as it arrives.
void hop_frequency()
Advance to the next IO-Homecontrol channel (CH1→CH2→CH3→CH1).
void maybe_hop()
Unconditionally hop only if the minimum dwell has elapsed.
void reset_debug(uint8_t request_cmd)
Clear the debug snapshot and record the upcoming request command.
uint8_t collect_broadcast_responses(const IoFrame &request, uint32_t freq, uint8_t expected_cmd, uint32_t window_ms, const BroadcastReplyHandler &on_reply)
Transmit request once and hand every matching broadcast reply to on_reply within window_ms.
void log_debug(const char *device_id) const
Log the debug snapshot as a WARN-level structured line.
const DebugInfo & get_debug() const
Read-only access to the current debug snapshot.
void set_pairing_telemetry(PairingTelemetry *telemetry)
Attach a telemetry recorder so transmit_frame()'s LBT loop records defer events.
ExchangeEngine(RadioDriver **radio_ptr, const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning)
Construct the engine with double-pointer indirection into the hub's RadioDriver pointer and direct po...
bool transmit_frame(const IoFrame &frame, uint32_t freq, uint16_t preamble)
Transmit a raw IoFrame with LBT and the given preamble length.
bool authenticate_request(const IoFrame &request, uint32_t freq)
Authenticate an inbound device command via 0x3C challenge / 0x3D HMAC.
bool send_and_receive(const IoFrame &request, IoFrame &response, uint32_t freq)
Execute an outbound authenticated exchange with retry.
void reset_hop_timestamp()
Reset the hop-timer (called after radio init in hub setup()).
ExchangeEngine & operator=(const ExchangeEngine &)=delete
ExchangeEngine(const ExchangeEngine &)=delete
void record_debug(const char *stage, uint8_t tries, bool saw_challenge)
Update the debug snapshot with the current stage and radio capture.
Fixed-size per-attempt telemetry recorder for the pairing flow.
Abstract radio driver for IO-Homecontrol.
Pure transition helpers for hub-owned exchange and pairing frame decisions.
Internal exchange-state model for hub-owned authenticated non‑pairing flows.
ExchangeFinalResponseDisposition
Disposition for the final response after authentication.
ExchangeFirstResponseDisposition
Disposition for the first response in an authenticated exchange.
Structured per-attempt telemetry recorder for the pairing flow.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Physical-layer radio and timing parameters for the IO-Homecontrol protocol.
Radio abstraction layer for IO-Homecontrol.
Snapshot of the last exchange attempt for diagnostics.
bool saw_challenge
True if a 0x3C was seen during this exchange.
uint8_t capture_reported_len
Length reported by radio packet engine.
int16_t capture_rssi_dbm
RSSI of the captured packet (dBm).
bool capture_crc_error
True if CRC error flagged (chip-dependent; see RadioCaptureInfo::crc_error).
uint8_t capture_frame_len
Parsed protocol frame length.
uint8_t capture_packet_status
Chip packet-status byte.
bool capture_valid
True if radio capture is meaningful.
uint8_t request_cmd
Command ID of the original request.
const char * stage
Last recorded stage label.
uint32_t capture_freq_hz
RF frequency of the captured packet.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
All runtime tunable parameters for pairing and radio diagnostics.
Context carried across one outbound authenticated exchange.
Runtime tuning configuration for pairing and radio diagnostics.