ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
ln882h_ble_tracker.cpp
Go to the documentation of this file.
1#ifdef USE_LIBRETINY
2
4
5#include <cinttypes>
6
7#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
11
12static const char *const TAG = "ln882h_ble_tracker";
13
14static constexpr float BLE_SCAN_UNIT_MS = 0.625f;
15
16// ---------------------------------------------------------------------------
17// Component lifecycle
18// ---------------------------------------------------------------------------
19
21 // Receive the controller's scan reports; the controller queues them from the
22 // rw task and delivers here on the main task.
24 // Merged (and unmerged) frames go to the shared dispatcher; scan_continuous_
25 // is read at each delivery to decide unclaimed-device logging.
26 this->merger_.bind(&this->dispatcher_, &this->scan_continuous_, TAG);
27 // scan_running_ check: an on_boot start_scan action (priority 600) runs
28 // before this setup() (200) and enable_loop() is a no-op pre-setup — parking
29 // the loop here would strand that already-running scan.
30 if (!this->scan_continuous_ && !this->scan_running_ && !this->pending_start_) {
31 // Say so once: with continuous: false nothing scans until an explicit
32 // start_scan() — silence here reads as a broken scanner.
33 ESP_LOGD(TAG, "Scanning not started (continuous: false) - waiting for an explicit start_scan()");
34 // Nothing to time until then; start_scan_() re-enables the loop.
35 this->disable_loop();
36 }
37#ifdef USE_OTA_STATE_LISTENER
38 // Pause scanning while an OTA update is in flight — on the single-core LN882H the
39 // BLE scan competes with the OTA flash writes. Mirrors esp32_ble_tracker.
41#endif
42}
43
44#ifdef USE_OTA_STATE_LISTENER
45void LN882HBLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
46 ota::OTAComponent *comp) {
47 if (state == ota::OTA_STARTED) {
50 this->stop_scan();
51 } else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
52 // On success the device reboots, so restore only on a failed/aborted
53 // update. Continuous mode resumes via loop()'s idle branch; a one-shot
54 // scan that was running is restarted explicitly (bk72xx sibling parity —
55 // stop_scan() cleared it and nothing else would bring it back).
57 this->scan_continuous_ = true;
58 this->enable_loop(); // stop_scan() disabled it; loop()'s idle branch restarts the scan
59 } else if (this->scan_running_before_ota_) {
60 this->start_scan();
61 }
62 this->scan_continuous_before_ota_ = false;
63 this->scan_running_before_ota_ = false;
64 }
65}
66#endif // USE_OTA_STATE_LISTENER
67
69 if (this->pending_start_) {
70 // A start_scan latched before the controller's setup(); safe now — loop()
71 // only runs after every component set up.
72 this->pending_start_ = false;
73 if (!this->scan_running_) {
74 this->start_scan_();
75 }
76 }
77 // Deliver held scannable advertisements whose scan response never arrived —
78 // unmerged after the merger's timeout. Main-task only, like every merger call.
79 const uint32_t now = millis();
80 if (!this->merger_.empty())
81 this->merger_.sweep(now);
82
83 if (this->scan_continuous_) {
84 if (!this->scan_running_) {
85 this->start_scan_();
86 // start_scan_() re-anchors scan_period_start_ from a later millis() than
87 // the cached `now`; resume the period timer next iteration.
88 return;
89 }
90 // Period timer: once per scan_duration_ window, restart the controller scan
91 // and fire on_scan_end(), mirroring esp32_ble_tracker::cleanup_scan_state_().
92 // The restart is the recovery path for the coexistence failure documented in
93 // the header. scan_start() re-enters cleanly on its own: it stops an
94 // in-flight scan and grants the controller's 10 ms GAPM settle before
95 // restarting — an explicit scan_stop() first would clear the controller's
96 // re-entry guard and skip that settle.
97 if (now - this->scan_period_start_ >= this->scan_duration_) {
98 ESP_LOGD(TAG, "Scan period elapsed - restarting scan");
99 this->parent_->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_);
100 // Keep both clocks anchored to the restart: a runtime switch to
101 // non-continuous then times out the current period, not the whole run.
102 this->scan_start_time_ = now;
103 this->end_scan_period_(now);
104 }
105 return;
106 }
107
108 // Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end.
109 // Restart is driven externally (e.g. wifi: on_connect:).
110 if (this->scan_running_ && now - this->scan_start_time_ >= this->scan_duration_) {
111 this->stop_scan_();
112 }
113}
114
116 if (this->scan_active_ == active)
117 return true;
118 this->scan_active_ = active;
119 // V: the proxy's "Setting scanner mode" line already narrates this at D.
120 ESP_LOGV(TAG, "Scan mode %s", active ? "active" : "passive");
121 // scan_start() re-enters cleanly (stops + GAPM settle). No on_scan_end and
122 // no period reset: the scan logically continues, only the mode changes.
123 if (this->scan_running_) {
124 this->parent_->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_);
125 }
126 return true;
127}
128
130 ESP_LOGCONFIG(TAG,
131 "LN882H BLE Tracker:\n"
132 " Scan Duration: %" PRIu32 " s\n"
133 " Scan Interval: %.0f ms (%" PRIu16 " BLE units)\n"
134 " Scan Window: %.0f ms (%" PRIu16 " BLE units)\n"
135 " Scan Type: %s\n"
136 " Continuous Scanning: %s",
137 this->scan_duration_ / 1000, this->scan_interval_ * BLE_SCAN_UNIT_MS, this->scan_interval_,
138 this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_window_, this->scan_active_ ? "ACTIVE" : "PASSIVE",
139 YESNO(this->scan_continuous_));
140}
141
142// ---------------------------------------------------------------------------
143// Adv/scan-response demux into the shared merger (ble_device_base): the LN
144// controller delivers the pair as separate reports; a scannable advertisement
145// is held until its scan response arrives and delivered as one merged frame.
146// ---------------------------------------------------------------------------
147
149 if (report.is_scan_response) {
150 this->merger_.submit_scan_rsp(report.mac, report.rssi, report.addr_type, report.data, report.data_len);
151 return;
152 }
153 // Stash only while the scan runs: after a one-shot stop the loop is
154 // disabled and nothing would sweep the merger, so a late report would
155 // surface minutes later as a fresh advertisement.
156 if (this->scan_running_ && this->scan_active_ && report.scannable) {
157 this->merger_.stash_adv(report.mac, report.rssi, report.addr_type, report.data, report.data_len, millis());
158 return;
159 }
160 this->dispatcher_.dispatch(report.mac, report.rssi, report.addr_type, report.data, report.data_len,
161 /*raw_only=*/false, this->scan_continuous_ ? nullptr : TAG);
162}
163
164// ---------------------------------------------------------------------------
165// Public scan actions
166// ---------------------------------------------------------------------------
167
169 // Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via
170 // set_scan_continuous() first, then calls start_scan() to begin scanning.
171 if (!this->parent_->is_ready()) {
172 // An on_boot automation (priority 600) runs before the controller's
173 // setup() has resolved the BLE MAC; scan_start() now would rw_init() the
174 // all-zero address and bring BLE up before WiFi. Latch; loop() applies
175 // the start once every setup() has run.
176 this->pending_start_ = true;
177 return;
178 }
179 if (!this->scan_running_) {
180 this->start_scan_();
181 }
182}
183
185 if (!this->scan_running_)
186 return;
187 // Re-anchor only the one-shot duration clock. scan_period_start_ (the
188 // continuous-mode on_scan_end period) is deliberately left alone: a
189 // start_scan action fired more often than scan_duration_ would otherwise
190 // suppress on_scan_end indefinitely — and absence detection (ble_rssi's NAN
191 // publish) rides on that period.
192 this->scan_start_time_ = millis();
193}
194
196 // Cancel a start latched before the controller's setup(); without this an
197 // on_boot start_scan/stop_scan pair would still start at the first loop().
198 this->pending_start_ = false;
199 this->scan_continuous_ = false;
200 this->stop_scan_();
201}
202
203// ---------------------------------------------------------------------------
204// Internal scan start / stop
205// ---------------------------------------------------------------------------
206
208 if (this->scan_running_)
209 return;
210
211 // The controller enables the stack on first use and owns the report queue;
212 // this call is all the SDK interaction the tracker ever needs.
213 this->parent_->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_);
214 const uint32_t now = millis();
215 this->scan_running_ = true;
216 this->scan_start_time_ = now;
217 this->enable_loop(); // an idle non-continuous tracker disabled it in stop_scan_()
218 // Log every explicit start at DEBUG — stop_scan_() logs every stop at DEBUG, and
219 // in non-continuous mode each period is an explicit start, so asymmetric logging
220 // would read as the scanner failing to come back up.
221 ESP_LOGD(TAG, "BLE scan started (%s, window=%.0fms, interval=%.0fms)", this->scan_active_ ? "active" : "passive",
222 this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_interval_ * BLE_SCAN_UNIT_MS);
223 // Re-anchor the on_scan_end period to every successful start, so a restart
224 // later than scan_duration (e.g. a failed OTA restoring continuous mode)
225 // does not fire on_scan_end before an advertisement can arrive.
226 this->scan_period_start_ = now;
227}
228
230 if (!this->scan_running_)
231 return;
232 this->parent_->scan_stop();
233 this->scan_running_ = false;
234 // DEBUG like start_scan_() — a per-period stop at INFO would read as the
235 // scanner failing to come back up.
236 ESP_LOGD(TAG, "BLE scan stopped");
237 this->end_scan_period_(millis()); // also resets the period clock so on_scan_end does not double-fire
238 // scan_running_ re-check: an on_scan_end automation runs synchronously inside
239 // end_scan_period_() and may have called start_scan() — parking the loop then
240 // would leave the radio scanning with no period timing or pending-adv sweep.
241 if (!this->scan_continuous_ && !this->scan_running_) {
242 // Nothing left to time; start_scan_() re-enables the loop.
243 this->disable_loop();
244 }
245}
246
247// Close a scan period: deliver held advertisements whose scan response never
248// came (unmerged) BEFORE on_scan_end fires, then re-anchor the period clock.
250 this->merger_.flush();
251 this->dispatcher_.on_scan_end();
252 this->scan_period_start_ = now;
253}
254
255} // namespace esphome::ln882h_ble_tracker
256
257#endif // USE_LIBRETINY
bool is_ready() const
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
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 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().
void register_scan_listener(BLEScanListener *listener)
Register a consumer for scan reports (delivered on the main task via loop()).
Definition ln882h_ble.h:95
void scan_start(uint16_t interval, uint16_t window, bool active)
Start the controller scan.
void scan_stop()
Stop the controller scan (no-op when not scanning).
ble_device_base::ScanResponseMerger merger_
void restart_scan_duration()
Re-anchor the one-shot duration clock of a running scan to now — used when an action changes the scan...
void on_scan_report(const ln882h_ble::BLEScanReport &report) override
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override
void add_global_state_listener(OTAGlobalStateListener *listener)
bool state
Definition fan.h:2
OTAGlobalCallback * get_global_ota_callback()
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
One scan report from the controller, decoded from the SDK's rw-task event (RSSI already sign-correcte...
Definition ln882h_ble.h:25
uint8_t mac[MAC_ADDRESS_SIZE]
Definition ln882h_ble.h:26