ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
scan_response_merger.h
Go to the documentation of this file.
1// Shared support for trackers whose controller delivers advertisement and
2// scan response as SEPARATE reports (ln882h, rp2, bk72xx; ESP-IDF concatenates
3// both into one result before ESPHome sees it):
4//
5// ScanResponseMerger — Bluedroid-style merge: a scannable advertisement is
6// held briefly, its scan response is appended on arrival and the pair is
7// delivered as ONE merged frame. Merged delivery is what the receiving side
8// is built around: Home Assistant keeps the latest raw frame per device and
9// skips re-parsing when it is unchanged — split delivery alternates two raw
10// frames per device and defeats both.
11//
12// AdvDispatcher — the delivery half every such tracker repeats: raw
13// callback, listener parsing, discovered-device log. Trackers delegate
14// their BLEHub register_listener / set_raw_advertisement_callback here.
15//
16// The merger delivers straight into the tracker's AdvDispatcher — bind() wires
17// the pair once in setup(). Single-task use only (every tracker calls this on
18// the ESPHome main task). The clock is caller-provided: pass the same clock to
19// stash_adv() and sweep() (millis() or App.get_loop_component_start_time(),
20// never mixed).
21
22#pragma once
23
25
26// Emitted (cg.add_define) by each tracker that adopts the merger, so builds
27// whose tracker merges in-stack (esp32) never compile this code.
28#ifdef USE_BLE_SCAN_RESPONSE_MERGER
29
30#include "ble_device.h"
31#include "ble_hub.h"
33
34#include <cstdint>
35
37
43 public:
45#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
46 this->listeners_.push_back(listener);
47#endif
48 }
57 void dispatch(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len,
58 bool raw_only, const char *log_unclaimed_tag);
60 void on_scan_end();
61
62 protected:
64#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
65 // Parsed-advertisement consumers registered through ble_device_base.
66 // Codegen-sized: no heap allocation, no std::vector template instantiations.
68 // Per-period "Found device" DEBUG log with MAC dedup. Guarded like its only
69 // writer so a no-listener build does not carry an unused vector.
71#endif
72};
73
75 public:
81 void bind(AdvDispatcher *dispatcher, const bool *scan_continuous, const char *log_tag) {
82 this->dispatcher_ = dispatcher;
83 this->scan_continuous_ = scan_continuous;
84 this->log_tag_ = log_tag;
85 }
92 void stash_adv(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len,
93 uint32_t now);
100 void submit_scan_rsp(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len);
104 void sweep(uint32_t now);
107 void flush();
110 bool empty() const { return this->pending_count_ == 0; }
111
112 private:
116 void deliver_(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len,
117 bool raw_only);
118
119 // 62 bytes = legacy adv (31) + scan response (31), the same merged maximum
120 // as ESP-IDF delivers on ESP32.
121 struct PendingAdv {
122 bool used{false};
123 uint8_t mac[MAC_ADDRESS_SIZE];
124 uint8_t addr_type;
125 int8_t rssi;
126 uint8_t data_len; // <= sizeof(data)
127 uint8_t data[62];
128 uint32_t stored_ms;
129 };
130 // Sized for the unanswered case: a pair that IS answered normally matches
131 // within one report-queue drain, so a slot is held for the full timeout only
132 // by scannable devices that never reply. 8 concurrent such advertisers
133 // before the merge degrades (frames still delivered, just unmerged) at
134 // ~80 B each.
135 static constexpr size_t MAX_PENDING_ADV = 8;
136 // On air a scan response follows its advertisement by T_IFS (150 µs) — the
137 // timeout only covers HOST-side report queuing under WiFi/BLE coexistence,
138 // measured on-device (ln882h) at up to ~136 ms. 300 ms = >2x that margin,
139 // while staying below any device's re-advertising period.
140 static constexpr uint32_t PENDING_ADV_TIMEOUT_MS = 300;
141 AdvDispatcher *dispatcher_{nullptr};
142 const bool *scan_continuous_{nullptr}; // read at delivery; see bind()
143 const char *log_tag_{nullptr};
144 // pending_count_ mirrors the number of set `used` flags; both are updated
145 // together on every transition.
146 PendingAdv pending_adv_[MAX_PENDING_ADV];
147 uint8_t pending_count_{0};
148};
149
150} // namespace esphome::ble_device_base
151
152#endif // USE_BLE_SCAN_RESPONSE_MERGER
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:227
The delivery half of a split-report tracker, shared so the dispatch contract (raw-callback ordering,...
void set_raw_advertisement_callback(RawAdvertisementCallback callback)
void register_listener(ESPBTDeviceListener *listener)
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...
Per-scan-period "Found device" DEBUG logger, deduplicated by MAC address.
Definition ble_device.h:269
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...
bool empty() const
Lets loop() skip the cross-TU sweep() call in the common case (empty: passive scan,...
void bind(AdvDispatcher *dispatcher, const bool *scan_continuous, const char *log_tag)
Wire the merger's output; call once in the tracker's setup().
static void uint32_t
Subscriber slot for the raw-advertisement stream (the bluetooth_proxy path).
Definition ble_hub.h:43