Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
esphome::home_io_control::ExchangeEngine Class Reference

#include <exchange_engine.h>

Collaboration diagram for esphome::home_io_control::ExchangeEngine:

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
ExchangeEngineoperator= (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 DebugInfoget_debug () const
 Read-only access to the current debug snapshot.
const Counterscounters () const
 Read-only access to the running counters.
void reset_counters ()
 Zero every counter (e.g.

Detailed Description

Definition at line 68 of file exchange_engine.h.

Member Typedef Documentation

◆ BroadcastReplyHandler

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.

Parameters
frameParsed reply frame (responder's address is frame.src).
rssi_dbmRSSI of this reply.

Definition at line 110 of file exchange_engine.h.

Constructor & Destructor Documentation

◆ ExchangeEngine() [1/2]

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).

Parameters
radio_ptrAddress of the hub's RadioDriver *radio_ member.
node_idPointer to the hub's node_id_[NODE_ID_SIZE] array.
system_keyPointer to the hub's system_key_[AES_KEY_SIZE] array.
tuningPointer to the hub's TuningConfig tuning_ member.

Definition at line 33 of file exchange_engine.cpp.

◆ ExchangeEngine() [2/2]

esphome::home_io_control::ExchangeEngine::ExchangeEngine ( const ExchangeEngine & )
delete
Here is the call graph for this function:

Member Function Documentation

◆ authenticate_request()

bool esphome::home_io_control::ExchangeEngine::authenticate_request ( const IoFrame & request,
uint32_t freq )

Authenticate an inbound device command via 0x3C challenge / 0x3D HMAC.

Parameters
requestThe received inbound frame (e.g., CMD_STATUS_UPDATE).
freqRF channel the frame arrived on.
Returns
true if HMAC verified; false on timeout or mismatch.

Definition at line 640 of file exchange_engine.cpp.

Here is the call graph for this function:

◆ collect_broadcast_responses()

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.

Parameters
requestFrame to transmit once (cmd + endpoints already filled).
freqRF channel frequency (Hz) for the initial transmit.
expected_cmdCommand byte a reply must carry to be considered.
window_msHow 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_replyInvoked 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.
Returns
Number of matching replies handed to on_reply (duplicates included).

Definition at line 473 of file exchange_engine.cpp.

Here is the call graph for this function:

◆ counters()

const Counters & esphome::home_io_control::ExchangeEngine::counters ( ) const
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.

◆ get_debug()

const DebugInfo & esphome::home_io_control::ExchangeEngine::get_debug ( ) const
inlinenodiscard

Read-only access to the current debug snapshot.

Definition at line 250 of file exchange_engine.h.

◆ hop_frequency()

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).

Parameters
skip_freqChannel 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.

Here is the call graph for this function:

◆ listen()

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.

Parameters
specHow this listen window is to be spent.
packetScratch 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).
frameSame 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_frameInvoked for every packet the radio delivers; decides whether to accept, abort, or keep listening. See ReplyHandler.
Returns
ACCEPTED or ABORTED as on_frame decided, or TIMED_OUT if spec.window_ms elapsed first.

Definition at line 581 of file exchange_engine.cpp.

Here is the call graph for this function:

◆ log_debug()

void esphome::home_io_control::ExchangeEngine::log_debug ( const char * device_id) const

Log the debug snapshot as a WARN-level structured line.

Parameters
device_idHuman-readable device identifier for the log line.

Definition at line 74 of file exchange_engine.cpp.

Here is the call graph for this function:

◆ maybe_hop()

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.

Here is the call graph for this function:

◆ operator=()

ExchangeEngine & esphome::home_io_control::ExchangeEngine::operator= ( const ExchangeEngine & )
delete
Here is the call graph for this function:

◆ record_debug()

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.

◆ reset_counters()

void esphome::home_io_control::ExchangeEngine::reset_counters ( )
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.

◆ reset_debug()

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.

◆ reset_hop_timestamp()

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.

◆ send_and_receive()

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.

Parameters
requestFrame to transmit (cmd + endpoints already filled).
responsePopulated only for ExchangeOutcome::SUCCESS_WITH_RESPONSE.
freqRF channel frequency (Hz).
max_triesCap 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.
Returns
What the device actually told us — see ExchangeOutcome.

Definition at line 251 of file exchange_engine.cpp.

Here is the call graph for this function:

◆ set_pairing_telemetry()

void esphome::home_io_control::ExchangeEngine::set_pairing_telemetry ( PairingTelemetry * telemetry)
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.

Parameters
telemetryNon-owning pointer, or nullptr to detach.

Definition at line 215 of file exchange_engine.h.

◆ transmit_frame()

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.

Parameters
frameFrame to transmit.
freqRF frequency in Hz.
preamblePreamble length in bytes (e.g. LONG_PREAMBLE, SHORT_PREAMBLE, or a tuning-configured value such as normal_start_preamble).
Returns
true if the radio accepted the packet; false otherwise.

Definition at line 130 of file exchange_engine.cpp.

Here is the call graph for this function:

The documentation for this class was generated from the following files: