84 const uint8_t expected_dst[NODE_ID_SIZE]) {
85 return std::memcmp(frame.
src, expected_src, NODE_ID_SIZE) == 0 &&
86 std::memcmp(frame.
dst, expected_dst, NODE_ID_SIZE) == 0;
112 if (candidate.
cmd == CMD_CHALLENGE_REQ)
137 return request.
cmd == CMD_EXECUTE && request.
data_len > EXECUTE_MAIN_BYTE_OFFSET &&
138 request.
data[EXECUTE_MAIN_BYTE_OFFSET] != POS_FAVORITE;
162 uint8_t unconfirmed_tries) {
163 if (request.
cmd != CMD_EXECUTE)
166 unconfirmed_tries <= UNCONFIRMED_EXECUTE_MAX_RESENDS;
184 const uint8_t controller_id[NODE_ID_SIZE]) {
185 return candidate.
cmd == CMD_DISCOVER_RESP && std::memcmp(candidate.
dst, controller_id, NODE_ID_SIZE) == 0
203 const uint8_t device_id[NODE_ID_SIZE],
204 const uint8_t controller_id[NODE_ID_SIZE]) {
208 return candidate.
cmd == CMD_CHALLENGE_REQ && candidate.
data_len == HMAC_SIZE &&
225 const IoFrame &candidate,
const uint8_t device_id[NODE_ID_SIZE],
const uint8_t controller_id[NODE_ID_SIZE]) {
228 if (candidate.
cmd == CMD_DISCOVER_CONFIRM_ACK)
230 if (candidate.
cmd == CMD_ERROR_RESP)
248 if (candidate.
cmd == CMD_KEY_CONFIRM)
295 uint8_t
dst[NODE_ID_SIZE]{};
310 if (!incoming.
has_intent && std::memcmp(last.
dst, incoming.
dst, NODE_ID_SIZE) != 0)
343 uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms,
344 uint32_t max_defer_ms) {
345 if (!next_op_is_background || last_1w_activity_ms == 0)
347 if (now - first_1w_activity_ms >= max_defer_ms)
349 return (now - last_1w_activity_ms) < quiet_ms;
371 bool settles_a_stop =
false) {
373 return STOP_SETTLE_POLL_TRIES;
374 if (auth_poll_failures != 0)
375 return SCHEDULED_POLL_MAX_TRIES;
376 if (status_poll_failures < SCHEDULED_POLL_RETRY_GRACE_FIRST_FAILURE ||
377 status_poll_failures > SCHEDULED_POLL_RETRY_GRACE_LAST_FAILURE)
378 return SCHEDULED_POLL_MAX_TRIES;
379 return EXCHANGE_RETRY_COUNT;
419 return request.
cmd == CMD_EXECUTE && request.
data_len > EXECUTE_MAIN_BYTE_OFFSET &&
420 request.
data[EXECUTE_MAIN_BYTE_OFFSET] == POS_STOP;
431 if (is_stop || (moving != 0 && (now - moving) < LOW_POWER_MAX_TRAVEL_MS))
448 uint16_t wake_preamble) {
449 const uint8_t try_1based = std::max<uint8_t>(1, std::min<uint8_t>(try_index, EXCHANGE_RETRY_COUNT));
451 return short_preamble;
452 return wake_preamble;
481 if (std::memcmp(dst, BROADCAST_DISCOVER_ALT, NODE_ID_SIZE) != 0)
483 return cmd == CMD_WRITE_PRIVATE || cmd == CMD_ONEWAY_REMOVE || cmd == CMD_DISCOVER_ALT_REQ;
497 return last_1w_activity_ms == 0 || (now - last_1w_activity_ms) >= quiet_ms;
524 (
static_cast<uint32_t
>(node_id[1]) << BITS_PER_BYTE) |
static_cast<uint32_t
>(node_id[2]);
559inline bool next_node_id(
const uint8_t node[NODE_ID_SIZE], uint8_t next[NODE_ID_SIZE]) {
560 uint8_t up[NODE_ID_SIZE];
561 memcpy(up, node, NODE_ID_SIZE);
563 for (
size_t i = NODE_ID_SIZE; i-- > 0;) {
565 memcpy(next, up, NODE_ID_SIZE);
619 return "rain_originator";
621 return "clamped_command";
639 "the raw rain threshold must be an exact multiple of one percent");
648 if (value_raw > STATUS_POS_MAX)
bool is_repeatable_execute(const IoFrame &request)
True for a CMD_EXECUTE that has the same effect sent twice as sent once.
static constexpr uint16_t POSITION_RAW_PER_PERCENT
Raw position units per percent (512).
bool pairing_node_has_heater_at_next_address(DeviceType type, uint8_t manufacturer)
Does a pairing node that answered discovery this way control a heater at the next address?
PairingDiscoveryDisposition
Disposition during pairing discovery phase.
@ ACCEPT
Valid discovery response received.
@ NO_RESPONSE
No packets received on the channel within timeout.
@ INVALID
Packets seen but none were valid discovery (0x29) frames.
bool is_one_way_pairing_gesture(bool oneway, const uint8_t dst[NODE_ID_SIZE], uint8_t cmd)
True if a frame's shape matches a 1W remote's pairing gesture (issue #27/#65): CTRL0 1W bit set,...
bool frame_matches_nodes(const IoFrame &frame, const uint8_t expected_src[NODE_ID_SIZE], const uint8_t expected_dst[NODE_ID_SIZE])
Check if two frames have identical src/dst node IDs.
RainSensorState rain_state_from_limitation_reply(const LimitationStatus &status)
Rain state from a decoded limitation reply: the one place that knows which reply fields decide.
bool is_duplicate_1w_frame(const OneWayDedupState &last, const OneWayDedupState &incoming, uint32_t window_ms)
Decide whether an incoming 1W frame repeats the previous one inside the burst window.
bool is_exchange_internal_command(uint8_t cmd)
Returns true for commands that are internal to an exchange handshake and carry no useful information ...
static constexpr uint16_t POSITION_FULL_PERCENT
Percent value of a fully limited position, the divisor of the raw position coding.
bool next_node_id(const uint8_t node[NODE_ID_SIZE], uint8_t next[NODE_ID_SIZE])
The node address one above node, as a 24-bit number (B6BDFB -> B6BDFC).
static constexpr uint16_t RAIN_MINIMUM_LIMIT_RAW
RAIN_MINIMUM_LIMIT_PERCENT in the raw position coding (0xB200).
const char * rain_limitation_rule_name(RainLimitationRule rule)
Name of a rain-limitation rule for log lines.
PairingKeyChallengeDisposition classify_pairing_key_challenge(const IoFrame &candidate, const uint8_t device_id[NODE_ID_SIZE], const uint8_t controller_id[NODE_ID_SIZE])
Decide if a frame is a valid key-challenge (0x3C) during pairing key exchange.
ExchangeFirstResponseDisposition classify_exchange_first_response(const IoFrame &request, const IoFrame &candidate)
Decide how to handle the first response packet in an authenticated exchange.
bool retry_after_unconfirmed_accept_is_safe(const IoFrame &request, bool target_confirms_execute, uint8_t unconfirmed_tries)
Whether an authenticated-but-unanswered request may be sent again.
uint16_t low_power_try_preamble(WakeBelief belief, uint8_t try_index, uint16_t short_preamble, uint16_t wake_preamble)
Start-preamble length for one try of a directed exchange to a low-power receiver.
ExchangeFinalResponseDisposition
Disposition for the final response after authentication.
@ ACCEPT
Frame matches expected response — exchange succeeds.
@ IGNORE_UNRELATED
Frame doesn't match endpoints — ignore.
bool oneway_burst_started_fresh(uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms)
Whether a 1W frame arriving at now starts a new burst rather than extending the current one — true if...
PairingKeyChallengeDisposition
Disposition during pairing key-challenge phase.
@ ACCEPT
Valid 0x3C challenge from target device.
@ IGNORE
Not a valid challenge (wrong cmd, length, or sender).
bool is_stop_request(const IoFrame &request)
True for a CMD_EXECUTE whose main byte is POS_STOP.
ExchangeFirstResponseDisposition
Disposition for the first response in an authenticated exchange.
@ REQUIRE_AUTH
Matching 0x3C challenge — device demands authentication.
@ IGNORE_UNRELATED
Frame doesn't match endpoints or failed parse — keep waiting.
@ COMPLETE_DIRECT
Matching non-challenge frame — operation complete, no auth needed.
PairingKeyConfirmDisposition classify_pairing_key_confirm_reply(const IoFrame &request, const IoFrame &candidate)
Decide how to handle a candidate reply to the key-transfer (0x32) confirm wait (wait_for_key_confirm_...
PairingKeyConfirmDisposition
Disposition for a candidate reply to the key-transfer (0x32) confirm wait — the slow-turnaround path'...
@ REFUSE
Anything else (including CMD_ERROR_RESP) — an explicit or implicit refusal; ends the whole wait,...
@ CHALLENGE
A fresh CMD_CHALLENGE_REQ (0x3C) — the device may challenge the key transfer before confirming it; an...
@ IGNORE
Not from/to the expected endpoints — keep waiting.
@ CONFIRM
CMD_KEY_CONFIRM (0x33) — the device accepted the key.
TargetEvidence target_evidence(const IoDevice &dev)
Build the exchange engine's view of a device record.
bool defer_background_poll_for_1w_activity(bool next_op_is_background, uint32_t first_1w_activity_ms, uint32_t last_1w_activity_ms, uint32_t now, uint32_t quiet_ms, uint32_t max_defer_ms)
Decide whether to hold back a queued background poll because a 1W remote is still transmitting.
const char * wake_belief_name(WakeBelief belief)
Lowercase name of a belief for log lines.
PairingDiscoveryDisposition classify_pairing_discovery_response(const IoFrame &candidate, const uint8_t controller_id[NODE_ID_SIZE])
Decide if a frame is a valid discovery response (0x29) during pairing.
PairingDiscoverConfirmDisposition classify_pairing_discover_confirm_reply(const IoFrame &candidate, const uint8_t device_id[NODE_ID_SIZE], const uint8_t controller_id[NODE_ID_SIZE])
Decide how to handle a candidate reply to a discovery-confirm request (0x2C) during pairing.
WakeBelief
Whether a low-power receiver is believed to be travelling right now, judged from what this hub has se...
@ ASLEEP
Not known to be moving — lead with the wake-up preamble.
@ AWAKE
Moving, or about to be stopped — the short preamble is the one it hears.
static constexpr uint8_t RAIN_MINIMUM_LIMIT_PERCENT
A window with a wired rain sensor limits its own opening while it rains.
constexpr uint32_t REMOTE_POLL_TIMER_ID_TAG
Namespace tag for remote_poll_timer_id() below: the node address occupies the low 24 bits,...
WakeBelief wake_belief(const TargetEvidence &evidence, uint32_t now, bool is_stop)
Judge whether a low-power receiver is travelling.
uint32_t heating_poll_timer_id(const uint8_t node_id[NODE_ID_SIZE])
Numeric set_timeout() id for a heater's regular-cadence poll timer (IOHomeControlComponent::schedule_...
bool is_rain_limited(const RainLimitationInput &in)
True when rain_limitation_rule() finds evidence of a rain limitation.
PairingDiscoverConfirmDisposition
Disposition for a candidate reply to a discovery-confirm request (0x2C).
@ ACK
CMD_DISCOVER_CONFIRM_ACK (0x2D) from the device — it wants to proceed.
@ IGNORE
Not from/to the expected endpoints, or a frame the step does not act on (e.g.
@ ERROR
CMD_ERROR_RESP from the device — an explicit, named refusal.
constexpr uint32_t HEATING_POLL_TIMER_ID_TAG
Namespace tag for heating_poll_timer_id(): the same node-address scheme under its own bit,...
RainSensorState rain_state_from_minimum_limit(uint16_t value_raw)
Rain state from the raw value of a minimum-limitation reply.
bool discover_confirm_try_rotates(uint8_t try_index)
Whether discover-confirm try try_index (1-based) should rotate channels rather than hold the request ...
RainLimitationRule rain_limitation_rule(const RainLimitationInput &in)
Decide which rain-limitation evidence, if any, a status reply provides.
bool frame_matches_exchange_endpoints(const IoFrame &request, const IoFrame &candidate)
Check if candidate frame endpoints are the reverse of the request (dst==request.src,...
RainLimitationRule
Which evidence path labelled a device as limited by rain.
@ RAIN_ORIGINATOR
The device names the rain sensor as the originator of its last command.
@ NONE
No evidence of a rain limitation.
@ CLAMPED_COMMAND
A position command was accepted but the stopped device ended elsewhere, while rain protection was rec...
uint32_t remote_poll_timer_id(const uint8_t node_id[NODE_ID_SIZE])
Numeric set_timeout() id for a device's remote-activity poll timer (hub_status.cpp's schedule_status_...
ExchangeFinalResponseDisposition classify_exchange_final_response(const IoFrame &request, const IoFrame &candidate)
Decide if a candidate frame is an acceptable final response after authentication.
uint8_t scheduled_poll_max_tries(uint8_t status_poll_failures, uint8_t auth_poll_failures, bool settles_a_stop=false)
Transmit-attempt budget for a scheduler-owned status poll, by backoff-ladder position.
RainSensorState
What the hub's latest minimum-limitation read says about a rain-protection limit.
@ UNKNOWN
No usable reading: never read, or the last reads went unanswered.
@ DRY
The device answered and reports no rain-sized opening limit.
@ RAIN
The device answered and reports an opening limit of rain size.
DeviceType
IO-Homecontrol node types.
@ REMOTE_CONTROLLER
Remote controller (all ten type bits set, packed bytes FF C0).
bool has_reached_target_position(float target, float position)
Has the device reached its target within tolerance?
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol device-type model, capabilities and runtime device state.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Physical-layer radio and timing parameters for the IO-Homecontrol protocol.
Runtime state of a paired IO‑Homecontrol device.
uint32_t last_moving_evidence_ms
millis() of the last sign this device is travelling (see note_moving_evidence(), clear_moving_evidenc...
uint32_t last_seen_ms
millis() of the last frame received from this device (any command), 0 = never.
bool confirms_execute
True once this device has answered a CMD_EXECUTE with a reply that closed the exchange (a status,...
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
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.
One decoded CMD_LIMITATION_STATUS_RESP payload.
uint8_t parameter_id
data[0]; LIMITATION_PARAM_MP for the main parameter.
uint16_t value_raw
data[1..2], big-endian, position coding (STATUS_POS_MAX = 100 %).
Key fields of the last processed 1W frame, used to collapse a remote's repeat burst.
uint8_t dst[NODE_ID_SIZE]
Destination address; compared only when has_intent is false.
uint8_t main1
Second main byte.
std::string src_id
Source node ID of the last processed frame; empty before the first.
uint8_t main0
First main byte — what distinguishes a move from a stop.
uint32_t timestamp
millis() when the frame was processed.
bool has_intent
Whether main0/main1 were decoded (execute / activate-mode only).
What the hub knows about the device an exchange is addressed to, as the exchange engine sees it.
uint32_t last_seen_ms
Last frame received from the receiver, any command.
bool confirms_execute
The device has closed a CMD_EXECUTE exchange with a reply before.
uint32_t last_moving_evidence_ms
Last sign the receiver is travelling (see note_moving_evidence(), clear_moving_evidence()).