|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
#include <exchange_engine.h>
Classes | |
| struct | DebugInfo |
| Snapshot of the last exchange attempt for diagnostics. More... | |
| struct | Counters |
| Free-running counters for the engine's own retry/parse behavior — not per-device (see the RSSI/Exchange-Failures per-device sensors for that) and not per-attempt (see DebugInfo for that). More... | |
Public Types | |
| using | BroadcastReplyHandler = std::function<void(const IoFrame &frame, int16_t rssi_dbm)> |
| Invoked for each matching broadcast reply, as it arrives. | |
Public Member Functions | |
| 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 pointers to the node/key byte arrays and the tuning config. | |
| ExchangeEngine (const ExchangeEngine &)=delete | |
| ExchangeEngine & | operator= (const ExchangeEngine &)=delete |
| ExchangeOutcome | send_and_receive (const IoFrame &request, IoFrame &response, uint32_t freq, uint8_t max_tries=EXCHANGE_RETRY_COUNT) |
| Execute an outbound authenticated exchange with retry. | |
| bool | authenticate_request (const IoFrame &request, uint32_t freq) |
| Authenticate an inbound device command via 0x3C challenge / 0x3D HMAC. | |
| 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. | |
| ListenOutcome | listen (const ListenSpec &spec, RadioRxPacket &packet, IoFrame &frame, const ReplyHandler &on_frame) |
| The one listen primitive every radio wait loop in this project is built on. | |
| bool | transmit_frame (const IoFrame &frame, uint32_t freq, uint16_t preamble) |
| Transmit a raw IoFrame with LBT and the given preamble length. | |
| void | hop_frequency (uint32_t skip_freq=0) |
| Advance the receiver one step along the protocol's channel rotation (CH1→CH2→CH3→CH1). | |
| void | maybe_hop () |
| Hop only if the minimum dwell has elapsed and no frame is currently arriving on this channel — RadioDriver::reception_in_progress() gates the hop so a reception in progress is never destroyed mid-arrival (issue #81). | |
| void | reset_hop_timestamp () |
| Reset the hop-timer (called after radio init in hub setup()). | |
| void | set_pairing_telemetry (PairingTelemetry *telemetry) |
| Attach a telemetry recorder so transmit_frame()'s LBT loop records defer events. | |
| void | reset_debug (uint8_t request_cmd) |
| Clear the debug snapshot and record the upcoming request command. | |
| void | record_debug (const char *stage, uint8_t tries, bool saw_challenge) |
| Update the debug snapshot with the current stage and radio capture. | |
| 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. | |
| const Counters & | counters () const |
| Read-only access to the running counters. | |
| void | reset_counters () |
| Zero every counter (e.g. | |
Definition at line 68 of file exchange_engine.h.
| using esphome::home_io_control::ExchangeEngine::BroadcastReplyHandler = std::function<void(const IoFrame &frame, int16_t rssi_dbm)> |
Invoked for each matching broadcast reply, as it arrives.
| frame | Parsed reply frame (responder's address is frame.src). |
| rssi_dbm | RSSI of this reply. |
Definition at line 110 of file exchange_engine.h.
| esphome::home_io_control::ExchangeEngine::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 pointers to the node/key byte arrays and the tuning config.
Pointers must remain valid for the lifetime of the engine (guaranteed because hub owns all referenced members).
| radio_ptr | Address of the hub's RadioDriver *radio_ member. |
| node_id | Pointer to the hub's node_id_[NODE_ID_SIZE] array. |
| system_key | Pointer to the hub's system_key_[AES_KEY_SIZE] array. |
| tuning | Pointer to the hub's TuningConfig tuning_ member. |
Definition at line 33 of file exchange_engine.cpp.
|
delete |
| bool esphome::home_io_control::ExchangeEngine::authenticate_request | ( | const IoFrame & | request, |
| uint32_t | freq ) |
Authenticate an inbound device command via 0x3C challenge / 0x3D HMAC.
| request | The received inbound frame (e.g., CMD_STATUS_UPDATE). |
| freq | RF channel the frame arrived on. |
Definition at line 640 of file exchange_engine.cpp.
| uint8_t esphome::home_io_control::ExchangeEngine::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.
Unlike send_and_receive(), this neither retries the transmit nor performs any authentication — a broadcast roll-call has no per-transaction proof, so replies are informational only (see the caller's protocol notes). Every candidate packet is parsed and checked against expected_cmd and node_id_ (as the frame's destination); anything that fails parse(), carries a different cmd, or is not addressed to us is ignored without ending collection. The receiver leaves the request channel before the first listen and alternates between the other two for the rest of the window — unlike wait_for_first_response_(), which holds the request channel for the whole wait — because a broadcast reply does not come back on the channel that asked for it (1 of 149 measured), while a unicast reply does. Replies on the request channel are therefore not caught by this loop.
This method stores nothing and imposes no capacity: it neither buffers replies nor deduplicates them, so the same responder answering twice within one window invokes on_reply twice. Storage, deduplication, and any capacity limit belong to the caller, which knows how little of each reply it actually needs to keep — see ManagementActions::scan_paired_devices(), which decodes each frame on arrival into a compact record rather than retaining whole frames. Collection always runs to the deadline.
| request | Frame to transmit once (cmd + endpoints already filled). |
| freq | RF channel frequency (Hz) for the initial transmit. |
| expected_cmd | Command byte a reply must carry to be considered. |
| window_ms | How long to listen after the transmit, in milliseconds. The caller owns this budget explicitly (rather than this method reading tuning_->pairing_discovery_wait_ms itself) because a caller that transmits more than once needs to divide one total time budget across several calls — see ManagementActions::scan_paired_devices(). |
| on_reply | Invoked once per matching reply, before the next packet is awaited, so the handler must be cheap and must not block. Keep captures to a few pointers: small callables avoid std::function's heap fallback on the implementations this project builds against. |
Definition at line 473 of file exchange_engine.cpp.
|
inlinenodiscard |
Read-only access to the running counters.
No production caller yet — see the Counters doc comment above.
Definition at line 287 of file exchange_engine.h.
|
inlinenodiscard |
Read-only access to the current debug snapshot.
Definition at line 250 of file exchange_engine.h.
| void esphome::home_io_control::ExchangeEngine::hop_frequency | ( | uint32_t | skip_freq = 0 | ) |
Advance the receiver one step along the protocol's channel rotation (CH1→CH2→CH3→CH1).
| skip_freq | Channel to pass over, or 0 to rotate through all three. Used by the broadcast roll-call, whose replies never come back on the channel that asked. |
Definition at line 91 of file exchange_engine.cpp.
| ListenOutcome esphome::home_io_control::ExchangeEngine::listen | ( | const ListenSpec & | spec, |
| RadioRxPacket & | packet, | ||
| IoFrame & | frame, | ||
| const ReplyHandler & | on_frame ) |
The one listen primitive every radio wait loop in this project is built on.
Listens for up to spec.window_ms, applying spec.policy (hold the current channel, rotate all three, or rotate skipping the request channel), and hands every packet the radio delivers to on_frame before deciding whether to keep waiting. See ListenPolicy for the measurements behind each policy and ListenSpec for what each field controls.
Parses each received packet into frame, so on ListenOutcome::ACCEPTED the caller's frame already holds the accepted frame and packet already holds its raw bytes — no copy is needed. A packet that fails to parse is still handed to on_frame (with a null parsed pointer), so a caller that wants to log or count unparsable frames still can.
Any richer result than accept/refuse/timeout — a disposition with more than three values, a captured "did we see any traffic at all" flag — is the caller's business: capture it in on_frame's closure and return ACCEPT/IGNORE. ListenOutcome itself never grows a fourth value; that is how a shared primitive would turn back into one loop per caller.
| spec | How this listen window is to be spent. |
| packet | Scratch space for the whole listen: holds the last received packet on return. On ACCEPTED that is the accepted packet; on ABORTED, the one on_frame refused; on TIMED_OUT, whatever arrived last (or the caller's initial value, if nothing did). |
| frame | Same lifetime as packet, parsed from it: holds the last received frame on return, with the same ACCEPTED/ABORTED/TIMED_OUT correspondence as packet above. |
| on_frame | Invoked for every packet the radio delivers; decides whether to accept, abort, or keep listening. See ReplyHandler. |
Definition at line 581 of file exchange_engine.cpp.
| void esphome::home_io_control::ExchangeEngine::log_debug | ( | const char * | device_id | ) | const |
Log the debug snapshot as a WARN-level structured line.
| device_id | Human-readable device identifier for the log line. |
Definition at line 74 of file exchange_engine.cpp.
| void esphome::home_io_control::ExchangeEngine::maybe_hop | ( | ) |
Hop only if the minimum dwell has elapsed and no frame is currently arriving on this channel — RadioDriver::reception_in_progress() gates the hop so a reception in progress is never destroyed mid-arrival (issue #81).
A deferred hop does not reset the dwell timer: it fires on the first call after the reception clears, not a further HOP_TIME_US later. Called from the hub's loop() to honour passive channel scanning.
Definition at line 113 of file exchange_engine.cpp.
|
delete |
| void esphome::home_io_control::ExchangeEngine::record_debug | ( | const char * | stage, |
| uint8_t | tries, | ||
| bool | saw_challenge ) |
Update the debug snapshot with the current stage and radio capture.
Definition at line 46 of file exchange_engine.cpp.
|
inline |
Zero every counter (e.g.
to start a fresh measurement window). No production caller yet — see the Counters doc comment above.
Definition at line 291 of file exchange_engine.h.
| void esphome::home_io_control::ExchangeEngine::reset_debug | ( | uint8_t | request_cmd | ) |
Clear the debug snapshot and record the upcoming request command.
Definition at line 41 of file exchange_engine.cpp.
| void esphome::home_io_control::ExchangeEngine::reset_hop_timestamp | ( | ) |
Reset the hop-timer (called after radio init in hub setup()).
Definition at line 89 of file exchange_engine.cpp.
| ExchangeOutcome esphome::home_io_control::ExchangeEngine::send_and_receive | ( | const IoFrame & | request, |
| IoFrame & | response, | ||
| uint32_t | freq, | ||
| uint8_t | max_tries = EXCHANGE_RETRY_COUNT ) |
Execute an outbound authenticated exchange with retry.
| request | Frame to transmit (cmd + endpoints already filled). |
| response | Populated only for ExchangeOutcome::SUCCESS_WITH_RESPONSE. |
| freq | RF channel frequency (Hz). |
| max_tries | Cap on transmit attempts for this exchange, clamped to [1, EXCHANGE_RETRY_COUNT]. Callers whose failure is already re-armed elsewhere (a scheduler-owned status poll) pass SCHEDULED_POLL_MAX_TRIES so a dead device does not block loop() for the full retry product. |
Definition at line 251 of file exchange_engine.cpp.
|
inline |
Attach a telemetry recorder so transmit_frame()'s LBT loop records defer events.
Set by PairingEngine for the duration of a discover_and_pair() attempt only — nullptr (the default) for every non-pairing exchange, which is the common case and stays a no-op.
| telemetry | Non-owning pointer, or nullptr to detach. |
Definition at line 215 of file exchange_engine.h.
| bool esphome::home_io_control::ExchangeEngine::transmit_frame | ( | const IoFrame & | frame, |
| uint32_t | freq, | ||
| uint16_t | preamble ) |
Transmit a raw IoFrame with LBT and the given preamble length.
| frame | Frame to transmit. |
| freq | RF frequency in Hz. |
| preamble | Preamble length in bytes (e.g. LONG_PREAMBLE, SHORT_PREAMBLE, or a tuning-configured value such as normal_start_preamble). |
Definition at line 130 of file exchange_engine.cpp.