18 int16_t rssi_dbm, uint8_t key_slot,
const ScanResponder *&entry) {
19 for (uint8_t i = 0; i < count; i++) {
20 if (memcmp(responders[i].src, frame.
src, NODE_ID_SIZE) == 0) {
21 entry = &responders[i];
25 if (count >= capacity) {
35 std::string unused_device_id;
39 memcpy(new_entry.
src, frame.
src, NODE_ID_SIZE);
66struct RollCallAttemptContext {
67 const RollCallPass *pass;
80void record_roll_call_reply(ScanState &state,
const RollCallAttemptContext &ctx,
const IoFrame &frame,
81 const ExchangeEngine::BroadcastReplyInfo &info) {
82 const ScanResponder *entry =
nullptr;
85 state.truncated =
true;
88 const char *power_save = (entry !=
nullptr && entry->has_extended)
92 "Roll-call reply src=%s pass=%s attempt=%u tx=%" PRIu32
" rx=%" PRIu32
" +%" PRIu32
93 "ms rssi=%d power_save=%s",
95 ctx.tx_freq_hz, info.rx_freq_hz, info.after_tx_ms, info.rssi_dbm, power_save);
106 uint32_t tx_freq_hz, uint8_t attempt, uint8_t total_attempts,
109 const bool low_power = pass.power_save_mode == POWER_SAVE_LOW_POWER;
110 if (!
create_discovery_request(request, node_id, CMD_DISCOVER_SPE_REQ, BROADCAST_DISCOVER, low_power, pass.ack_capable,
115 const uint8_t before = state.count;
116 RollCallAttemptContext ctx{&pass, attempt, tx_freq_hz, key_slot};
117 const uint8_t heard = engine.collect_broadcast_responses(
118 request, tx_freq_hz, CMD_DISCOVER_SPE_RESP, tuning.pairing_discovery_wait_ms,
120 record_roll_call_reply(state, ctx, frame, info);
128 const uint8_t new_count = state.count - before;
130 "Roll-call attempt %u/%u (pass=%s, ctrl1=0x%02X, tx %" PRIu32
" Hz, %u ms window): %u repl%s heard, %u new",
132 tuning.pairing_discovery_wait_ms, heard, heard == 1 ?
"y" :
"ies", new_count);
140 uint8_t &attempt, uint8_t total_attempts) {
147 if (run_roll_call_attempt(engine, node_id, key, key_slot, tuning, pass, tx_freq_hz, attempt, total_attempts,
166std::string known_responder_hints(
const ScanResponder &responder,
const IoDevice ®istered,
167 const RollCallReplyKeys &keys, uint8_t power_save) {
169 if (responder.has_extended) {
170 if (power_save == POWER_SAVE_LOW_POWER && !registered.low_power) {
172 " but its YAML has no low_power: true; add it or directed commands may miss it while it "
174 }
else if (power_save == POWER_SAVE_ALWAYS_ALIVE && registered.low_power) {
176 " but its YAML sets low_power: true; remove it, the " + std::to_string(LONG_PREAMBLE) +
177 "-byte wake-up preamble can stop an always-alive receiver answering\n";
182 const uint8_t *yaml_key = registered.has_system_key ? registered.system_key : keys.hub;
183 if (memcmp(yaml_key, keys.answered, AES_KEY_SIZE) != 0) {
186 "; fix its system_key:\n";
193std::string unknown_responder_block(
const ScanResponder &responder,
const std::string &device_id,
const uint8_t *key,
196 const std::string snippet =
199 if (snippet.empty()) {
201 return " not supported yet. Its system_key, for when support lands: \"" +
detail::format_key_hex(key) +
"\"\n";
203 return " not supported yet\n";
206 return " Paste this into your YAML to register it:\n" + snippet;
208 return " Paste this into your YAML to register it, and add this device's system_key: (the key is printed in "
209 "the device log, not here).\n" +
214 return " DEVICE KEY BELOW -- DO NOT SHARE IT. Keep it in secrets.yaml and write `system_key: !secret ...`.\n"
215 " Paste this into your YAML to register it:\n" +
216 snippet +
" END OF DEVICE KEY BLOCK\n";
229 line +=
" rssi=" + std::to_string(responder.
rssi_dbm) +
"dBm";
230 uint8_t power_save = 0;
237 line += registered !=
nullptr ?
" [known]\n" :
" [unknown]\n";
239 if (registered !=
nullptr)
240 return line + known_responder_hints(responder, *registered, keys, power_save);
241 const bool low_power = responder.
has_extended && power_save == POWER_SAVE_LOW_POWER;
242 return line + unknown_responder_block(responder, device_id, key, display, low_power);
247 if (state.
count == 0)
251 ") answered the key pull with this hub's own key. " + std::to_string(state.
count) +
" device" +
252 (state.
count == 1 ?
" that holds" :
"s that hold") +
" it answered:\n";
254 for (uint8_t i = 0; i < state.
count; i++) {
260 report +=
"NOTE: more devices answered than the list holds; it is truncated.\n";
266 std::string report =
"DEVICE KEY RECEIVED -- DO NOT SHARE IT\n";
269 ") holds a different system key than this hub (key check value " +
271 report +=
"Anyone with this key can control every device that holds it. Keep it in secrets.yaml and write\n"
272 "`system_key: !secret ...` in the entity of each device.\n";
273 if (state.
count == 0) {
274 report +=
"A roll-call under that key found no device. Put this key into the system_key: of the entity that\n"
275 "controls the device:\n"
279 report += std::to_string(state.
count) +
" device" + (state.
count == 1 ?
" that holds" :
"s that hold") +
280 " it answered. Paste a block per device:\n";
282 for (uint8_t i = 0; i < state.
count; i++) {
288 report +=
"NOTE: more devices answered than the list holds; it is truncated.\n";
290 report +=
"END OF DEVICE KEY REPORT";
Owns the per-hub device table, update callbacks, and linked-remote associations.
IoDevice * get_declared(const std::string &device_id)
Retrieve a device the YAML declares, ignoring one a pairing added in this boot.
Hub-layer log tag and log/format helpers shared by the hub and its collaborators.
constexpr const char * TAG
Shared log tag for hub-level messages.
std::string format_key_hex(const uint8_t key[AES_KEY_SIZE])
Format a 16-byte key as an uppercase, unseparated hex string for display.
std::string format_key_check_value(const uint8_t key[AES_KEY_SIZE])
A key check value: the first three bytes of the key's AES encryption of a zero block.
RollCallAttemptResult run_roll_call_sweep(ExchangeEngine &engine, const uint8_t *node_id, const uint8_t *key, uint8_t key_slot, const TuningConfig &tuning, ScanState &state, uint8_t &attempt, uint8_t total_attempts)
Run the full roll-call sweep under one key: every enabled pass on every channel.
const char * manufacturer_name(uint8_t id)
Get a human-readable manufacturer name from the protocol manufacturer byte.
ScanAddResult add_scan_responder(ScanResponder *responders, uint8_t &count, uint8_t capacity, const IoFrame &frame, int16_t rssi_dbm, uint8_t key_slot, const ScanResponder *&entry)
Record a responder unless its address is already present.
uint8_t count_enabled_passes(ScanPowerClasses selection)
How many of ROLL_CALL_PASSES selection actually calls.
uint8_t discovery_power_save_mode(uint8_t flags)
Extract the power save mode field from a discovery response's Multi Information Byte.
ScanAddResult
Outcome of add_scan_responder(), so callers can tell a harmless repeat from real loss.
@ ADDED
New responder recorded.
@ DUPLICATE
Already recorded from an earlier reply; nothing changed.
@ FULL
Dropped: SCAN_MAX_REPLIES distinct responders already recorded.
const char * att_class_name(uint8_t att_class)
Get a human-readable turnaround time string for an ATT class value.
std::string format_device_ids(DeviceType type, uint8_t subtype)
Wire values of a (type, subtype), for logs that must stay readable without the table.
const char * power_save_mode_name(uint8_t mode)
Get a human-readable power save mode name.
constexpr RollCallPass ROLL_CALL_PASSES[]
The roll-call's two fixed frame shapes, low-power first.
ScanPowerClasses
Which power classes scan_paired_devices() calls.
bool scan_power_classes_include(ScanPowerClasses selection, uint8_t power_save_mode)
Whether selection calls the roll-call pass for power_save_mode.
std::string format_roll_call_responder(const ScanResponder &responder, const std::string &device_id, const IoDevice *registered, const RollCallReplyKeys &keys, RollCallKeyDisplay display)
Format one roll-call responder's report line(s).
RollCallAttemptResult
Outcome of one roll-call attempt or sweep, so the caller can surface a build failure.
@ BUILD_FAILED
create_discovery_request() failed; the caller should abort the whole scan.
@ OK
Request built and transmitted (with or without replies).
std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id, bool metadata_complete, bool inverted, bool low_power, const std::string &system_key_hex)
Build the ready-to-paste YAML block describing a device, for both a fully-decoded device and one whos...
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power, bool ack_capable, bool payload_enabled, uint8_t payload, const uint8_t *system_key)
Build a configurable discovery request command (0x28, 0x2A, or 0x2E).
uint8_t discovery_att_class(uint8_t flags)
Extract the ATT class field from a discovery response's Multi Information Byte.
std::string build_hub_key_pull_report(const PulledKeyReportInfo &info, const ScanState &state, DeviceRegistry ®istry)
Build the report for a pull that returned the hub's own key from a node no platform drives.
constexpr uint8_t SCAN_MAX_REPLIES
Maximum distinct responders reported by one scan_paired_devices() call.
std::string build_pulled_key_report(const PulledKeyReportInfo &info, const ScanState &state, DeviceRegistry ®istry)
Build the one report a proven key pull prints after looking for the devices that hold the key.
DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id)
Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B — both carr...
std::string format_device_kind(DeviceType type, uint8_t subtype)
Human-readable description of a (type, subtype) for logs and reports.
RollCallKeyDisplay
How a responder line shows the key it answered under.
@ HIDDEN
Never print the key: its check value only. For anything Home Assistant records.
@ PLAIN
Print the key in a snippet with no framing: the caller's report supplies one banner.
constexpr uint32_t SCAN_CHANNELS[]
Channels scan_paired_devices() transmits its roll-call request on, one attempt per channel,...
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol device-type model, capabilities and runtime device state.
The roll-call sweep: one keyed 0x2A broadcast, repeated over channels and power classes.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
bool has_extended
data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
bool metadata_complete
data_len >= DEVICE_METADATA_SIZE (type/subtype present).
uint8_t manufacturer
Raw manufacturer ID; name via manufacturer_name().
uint8_t flags
Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
Per-reply facts collect_broadcast_responses() hands its caller alongside the frame.
Runtime state of a paired IO‑Homecontrol device.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceType type
Device type (shutter, awning, etc.).
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
What build_pulled_key_report() says about the device that handed over a key.
std::string pairing_device_id
Hex node ID of the device that sent the key.
const uint8_t * hub_key
The hub's own key.
uint8_t subtype
Its subtype from discovery.
const uint8_t * key
The pulled key (AES_KEY_SIZE bytes) it proved it holds.
DeviceType type
Its node type from discovery.
One roll-call frame shape, and how to listen for its replies.
The keys a responder line needs: the one a responder answered under, and the hub's.
bool per_device
True when answered is a per-device key, not the hub's.
const uint8_t * answered
The key of the sweep the responder answered in (always set).
One roll-call responder, decoded on arrival.
uint8_t src[NODE_ID_SIZE]
Responder's node ID; also the dedup key.
uint8_t manufacturer
Raw manufacturer ID; name via manufacturer_name().
bool inverted
Decoded position-inversion flag.
DeviceType type
Decoded device type.
bool has_extended
Whether manufacturer/flags above are present.
int16_t rssi_dbm
RSSI of the reply that produced this entry.
bool metadata_complete
Whether type/subtype were present in the payload.
uint8_t key_slot
Which key of the sweep it answered under: 0 is the hub key, n the n-th per-device key (RollCallKeys).
uint8_t flags
Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
uint8_t subtype
Decoded device subtype.
Accumulated state of one scan, threaded through every sweep.
uint8_t count
Entries in use.
bool truncated
A responder was dropped because the array was full.
ScanResponder responders[SCAN_MAX_REPLIES]
Distinct responders, in arrival order.
All runtime tunable parameters for pairing and radio diagnostics.
ScanPowerClasses scan_power_classes
Power classes the scan_paired_devices roll-call calls.