ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
scan_response_merger.cpp
Go to the documentation of this file.
2
3#ifdef USE_BLE_SCAN_RESPONSE_MERGER
4
6
7#include <cstring>
8
10
11void ScanResponseMerger::deliver_(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data,
12 uint8_t data_len, bool raw_only) {
13 // A partial bind is treated as unbound; never dereference half a binding.
14 if (this->dispatcher_ == nullptr || this->scan_continuous_ == nullptr)
15 return;
16 this->dispatcher_->dispatch(mac, rssi, addr_type, data, data_len, raw_only,
17 *this->scan_continuous_ ? nullptr : this->log_tag_);
18}
19
20void ScanResponseMerger::stash_adv(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data,
21 uint8_t data_len, uint32_t now) {
22 // One pass: find a same-device entry (deliver + reuse) while remembering the
23 // first free slot as the fallback.
24 PendingAdv *slot = nullptr;
25 PendingAdv *free_slot = nullptr;
26 for (auto &p : this->pending_adv_) {
27 if (!p.used) {
28 if (free_slot == nullptr)
29 free_slot = &p;
30 continue;
31 }
32 if (p.addr_type == addr_type && memcmp(p.mac, mac, MAC_ADDRESS_SIZE) == 0) {
33 // Same device advertised again before its scan response arrived — deliver
34 // the previous advertisement (its scan response is not coming) and reuse
35 // the slot, so no frame is ever lost.
36 p.used = false;
37 this->pending_count_--;
38 this->deliver_(p.mac, p.rssi, p.addr_type, p.data, p.data_len, /*raw_only=*/false);
39 slot = &p;
40 break;
41 }
42 }
43 if (slot == nullptr)
44 slot = free_slot;
45 if (slot == nullptr) {
46 // Table full — degrade gracefully: deliver the advertisement unmerged.
47 this->deliver_(mac, rssi, addr_type, data, data_len, /*raw_only=*/false);
48 return;
49 }
50 slot->used = true;
51 this->pending_count_++;
52 memcpy(slot->mac, mac, MAC_ADDRESS_SIZE);
53 slot->addr_type = addr_type;
54 slot->rssi = rssi;
55 slot->data_len = (data_len <= sizeof(slot->data)) ? data_len : sizeof(slot->data);
56 memcpy(slot->data, data, slot->data_len);
57 slot->stored_ms = now;
58}
59
60void ScanResponseMerger::submit_scan_rsp(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data,
61 uint8_t data_len) {
62 // Fast-out on the empty table (sweep/flush use the same guard); this is the
63 // hottest caller.
64 if (this->pending_count_ != 0) {
65 for (auto &p : this->pending_adv_) {
66 if (p.used && p.addr_type == addr_type && memcmp(p.mac, mac, MAC_ADDRESS_SIZE) == 0) {
67 // Append in place: the slot is released on delivery, so its 62-byte
68 // buffer (legacy adv + scan response) holds the merged frame directly.
69 const uint8_t room = sizeof(p.data) - p.data_len;
70 const uint8_t add = (data_len <= room) ? data_len : room;
71 memcpy(p.data + p.data_len, data, add);
72 p.used = false;
73 this->pending_count_--;
74 // The advertisement's RSSI, not the scan response's (header contract).
75 this->deliver_(mac, p.rssi, addr_type, p.data, p.data_len + add, /*raw_only=*/false);
76 return;
77 }
78 }
79 }
80 // Unmatched scan-response: goes out on the raw callback only (HA merges per
81 // address); local listeners/triggers receive each advertisement exactly once
82 // via the merged/plain path above.
83 this->deliver_(mac, rssi, addr_type, data, data_len, /*raw_only=*/true);
84}
85
87 if (this->pending_count_ == 0)
88 return;
89 for (auto &p : this->pending_adv_) {
90 if (p.used && now - p.stored_ms > PENDING_ADV_TIMEOUT_MS) {
91 p.used = false;
92 this->pending_count_--;
93 this->deliver_(p.mac, p.rssi, p.addr_type, p.data, p.data_len, /*raw_only=*/false);
94 }
95 }
96}
97
99 if (this->pending_count_ == 0)
100 return;
101 for (auto &p : this->pending_adv_) {
102 if (p.used) {
103 p.used = false;
104 this->pending_count_--;
105 this->deliver_(p.mac, p.rssi, p.addr_type, p.data, p.data_len, /*raw_only=*/false);
106 }
107 }
108}
109
110void AdvDispatcher::dispatch(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len,
111 bool raw_only, const char *log_unclaimed_tag) {
112 // Raw callback (the raw-advertisement path). Both full advertisements and
113 // unmatched scan responses (raw_only) are forwarded.
114 if (this->raw_callback_.is_set()) {
115 const RawAdvertisement adv{.address = mac_lsb_first_to_uint64(mac),
116 .data = data,
117 .data_len = data_len,
118 .rssi = rssi,
119 .addr_type = addr_type};
120 this->raw_callback_.invoke(adv);
121 }
122
123#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
124 // Scan-response-only frames are never parsed for local sensors/triggers.
125 if (raw_only)
126 return;
127 ESPBTDevice device;
128 device.from_scan_result(mac, rssi, addr_type, data, data_len);
129 // The listener list holds sensors AND the tracker's automation triggers
130 // (the triggers are listeners, exactly like esp32_ble_tracker), so one
131 // loop feeds both and ORs into `found`.
132 bool found = false;
133 for (auto *listener : this->listeners_) {
134 if (listener->parse_device(device)) {
135 found = true;
136 }
137 }
138 if (!found && log_unclaimed_tag != nullptr)
139 this->discovered_log_.log_device(log_unclaimed_tag, device);
140#endif // ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
141}
142
144#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
145 for (auto *listener : this->listeners_)
146 listener->on_scan_end();
147 this->discovered_log_.clear(); // reset per-scan "Found device" dedup (esp32_ble_tracker parity)
148#endif
149}
150
151} // namespace esphome::ble_device_base
152
153#endif // USE_BLE_SCAN_RESPONSE_MERGER
StaticVector< ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT > listeners_
void on_scan_end()
Fire listeners' on_scan_end and reset the per-scan discovered-log dedup.
void dispatch(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len, bool raw_only, const char *log_unclaimed_tag)
Dispatch one (possibly merged) advertisement: the raw callback, and — unless raw_only — parsing for l...
void clear()
Reset the per-period dedup list (call when a scan period ends).
Definition ble_device.h:274
void log_device(const char *tag, const ESPBTDevice &device)
Log the device at DEBUG the first time its MAC is seen this scan period.
void from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len)
Populate from a raw scan result delivered by a BLE tracker backend.
void stash_adv(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len, uint32_t now)
Hold a scannable advertisement, waiting for its scan response.
void sweep(uint32_t now)
Timeout flush (call from loop() with the stash_adv() clock): deliver held advertisements whose scan r...
void flush()
Deliver every held advertisement now (scan period/scan is ending, before on_scan_end fires): unmerged...
void submit_scan_rsp(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len)
A scan response arrived: append it to the held advertisement from the same device and deliver the pai...
uint64_t mac_lsb_first_to_uint64(const uint8_t *mac)
Pack a controller-order (LSB-first) MAC into the uint64 the API speaks.
Definition ble_device.h:160
static void uint32_t
bool is_set() const
A default-constructed slot is "no subscriber"; hubs must guard on this.
Definition ble_hub.h:47
void invoke(const RawAdvertisement &adv) const
Definition ble_hub.h:48
One raw advertisement as delivered by the controller — a borrowed view, valid only for the duration o...
Definition ble_hub.h:24