mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 10:52:56 +08:00
[ln882h_ble_tracker] BLE 5.x scanner for LN882H (#16691)
CI / Create common environment (push) Canceled after 0s
CI / Determine which jobs to run (push) Canceled after 0s
CI / Run script/ci-custom (push) Canceled after 0s
CI / Check pylint (push) Canceled after 0s
CI / Check lint and formatting (push) Canceled after 0s
CI / Run pytest (macOS-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (macOS-latest, 3.14) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.13) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.14) (push) Canceled after 0s
CI / Run pytest (windows-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (windows-latest, 3.14) (push) Canceled after 0s
CI / Report no coverage to Codecov (push) Canceled after 0s
CI / Run integration tests () (push) Canceled after 0s
CI / Check import esphome.__main__ time (push) Canceled after 0s
CI / Run CodSpeed benchmarks (push) Canceled after 0s
CI / Run C++ unit tests (push) Canceled after 0s
CI / Run script/clang-tidy for LibreTiny (push) Canceled after 0s
CI / Run script/clang-tidy for ESP8266 (push) Canceled after 0s
CI / Run script/clang-tidy for RP2 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 Arduino (push) Canceled after 0s
CI / Run script/clang-tidy for ZEPHYR (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 1/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 2/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 3/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 C6 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 P4 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 S3 (push) Canceled after 0s
CI / Test components batch () (push) Canceled after 0s
CI / Test esp32 components with PlatformIO (push) Canceled after 0s
CI / Test downstream esphome/device-builder (push) Canceled after 0s
CI / Build target branch for memory impact (push) Canceled after 0s
CI / Build PR branch for memory impact (push) Canceled after 0s
CI / Comment memory impact (push) Canceled after 0s
CI / CI Status (push) Canceled after 0s
CI / Create common environment (push) Canceled after 0s
CI / Determine which jobs to run (push) Canceled after 0s
CI / Run script/ci-custom (push) Canceled after 0s
CI / Check pylint (push) Canceled after 0s
CI / Check lint and formatting (push) Canceled after 0s
CI / Run pytest (macOS-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (macOS-latest, 3.14) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.13) (push) Canceled after 0s
CI / Run pytest (ubuntu-latest, 3.14) (push) Canceled after 0s
CI / Run pytest (windows-latest, 3.12) (push) Canceled after 0s
CI / Run pytest (windows-latest, 3.14) (push) Canceled after 0s
CI / Report no coverage to Codecov (push) Canceled after 0s
CI / Run integration tests () (push) Canceled after 0s
CI / Check import esphome.__main__ time (push) Canceled after 0s
CI / Run CodSpeed benchmarks (push) Canceled after 0s
CI / Run C++ unit tests (push) Canceled after 0s
CI / Run script/clang-tidy for LibreTiny (push) Canceled after 0s
CI / Run script/clang-tidy for ESP8266 (push) Canceled after 0s
CI / Run script/clang-tidy for RP2 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 Arduino (push) Canceled after 0s
CI / Run script/clang-tidy for ZEPHYR (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 1/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 2/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 IDF 3/3 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 C6 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 P4 (push) Canceled after 0s
CI / Run script/clang-tidy for ESP32 S3 (push) Canceled after 0s
CI / Test components batch () (push) Canceled after 0s
CI / Test esp32 components with PlatformIO (push) Canceled after 0s
CI / Test downstream esphome/device-builder (push) Canceled after 0s
CI / Build target branch for memory impact (push) Canceled after 0s
CI / Build PR branch for memory impact (push) Canceled after 0s
CI / Comment memory impact (push) Canceled after 0s
CI / CI Status (push) Canceled after 0s
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
1f5df20cb2
commit
1bae91abaa
@@ -296,6 +296,7 @@ esphome/components/lightwaverf/* @max246
|
||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||
esphome/components/lm75b/* @beormund
|
||||
esphome/components/ln882h_ble/* @Bl00d-B0b
|
||||
esphome/components/ln882h_ble_tracker/* @Bl00d-B0b
|
||||
esphome/components/ln882x/* @lamauny
|
||||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
|
||||
@@ -108,7 +108,7 @@ static constexpr uint8_t GAPM_SCAN_PROP_ACTIVE_1M_BIT = 1 << 2;
|
||||
// GAPM extended-advertising report types (bits 2:0 of ble_scan_report_t::info).
|
||||
// 0 = ADV_EXT (extended advertisement), 1 = ADV_LEG (legacy advertisement),
|
||||
// 2 = SCAN_RSP_EXT (scan response to extended adv), 3 = SCAN_RSP_LEG (scan response to legacy adv).
|
||||
static constexpr uint8_t GAPM_REPORT_TYPE_SCAN_RSP_EXT = 2;
|
||||
static constexpr uint8_t GAPM_REPORT_TYPE_ADV_LEG = 1;
|
||||
static constexpr uint8_t GAPM_REPORT_TYPE_SCAN_RSP_LEG = 3;
|
||||
// Bit 5 of ble_scan_report_t::info: the advertisement is scannable, i.e. a scan
|
||||
// response may follow (enum gapm_adv_report_info, GAPM_REPORT_INFO_SCAN_ADV_BIT).
|
||||
@@ -211,14 +211,22 @@ static void ble_scan_callback(void *param) {
|
||||
return;
|
||||
const auto *info = reinterpret_cast<const ble_scan_report_t *>(param);
|
||||
|
||||
// Only legacy framing is supported (see scan_start(): legacy 1M PHY only):
|
||||
// an extended report does not fit BLEScanReport::data and would reach
|
||||
// consumers as a truncated legacy frame. Reject before allocating so these
|
||||
// do not burn pool slots either.
|
||||
const uint8_t report_type = info->info & 0x07;
|
||||
if (report_type != GAPM_REPORT_TYPE_ADV_LEG && report_type != GAPM_REPORT_TYPE_SCAN_RSP_LEG) {
|
||||
s_ble->count_rejected_report();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill the pool slot in place (the bk72xx_ble shape): no report on the rw
|
||||
// task's stack — its size is fixed by the prebuilt stack — one copy of the
|
||||
// payload instead of two, and only data_len bytes ever leave this frame.
|
||||
BLEScanReport *slot = s_ble->allocate_scan_report();
|
||||
if (slot == nullptr)
|
||||
return; // pool exhausted — counted as dropped in allocate_scan_report()
|
||||
|
||||
const uint8_t report_type = info->info & 0x07;
|
||||
return; // no slot — counted as dropped in allocate_scan_report()
|
||||
|
||||
// BLE RSSI sign fix. The LN882H controller intermittently reports the RSSI with
|
||||
// a flipped sign: a real -58 dBm arrives as +58, above the SDK's documented
|
||||
@@ -231,7 +239,7 @@ static void ble_scan_callback(void *param) {
|
||||
memcpy(slot->mac, info->trans_addr, 6);
|
||||
slot->rssi = (raw > 20) ? static_cast<int8_t>(-raw) : raw;
|
||||
slot->addr_type = info->trans_addr_type;
|
||||
slot->is_scan_response = report_type == GAPM_REPORT_TYPE_SCAN_RSP_EXT || report_type == GAPM_REPORT_TYPE_SCAN_RSP_LEG;
|
||||
slot->is_scan_response = report_type == GAPM_REPORT_TYPE_SCAN_RSP_LEG;
|
||||
slot->scannable = (info->info & GAPM_REPORT_INFO_SCAN_ADV_BIT) != 0;
|
||||
slot->data_len = (info->length <= sizeof(slot->data)) ? static_cast<uint8_t>(info->length)
|
||||
: static_cast<uint8_t>(sizeof(slot->data));
|
||||
@@ -243,7 +251,8 @@ static void ble_scan_callback(void *param) {
|
||||
BLEScanReport *LN882HBLE::allocate_scan_report() {
|
||||
BLEScanReport *slot = this->report_pool_.allocate();
|
||||
if (slot == nullptr) {
|
||||
// Pool exhausted — the queue is full; count and drop.
|
||||
// No slot: pool exhausted (queue full) or the pool's on-demand RAM
|
||||
// allocation failed; count and drop either way.
|
||||
this->report_queue_.increment_dropped_count();
|
||||
}
|
||||
return slot;
|
||||
@@ -319,24 +328,46 @@ void LN882HBLE::enable() {
|
||||
}
|
||||
|
||||
void LN882HBLE::loop() {
|
||||
// Log dropped reports before the empty-queue return: a drop can also mean
|
||||
// EventPool::allocate() failed on heap exhaustion, and that can happen with
|
||||
// the queue empty — from the very first report on. Checking here keeps that
|
||||
// failure visible instead of producing a scanner that is silently dead.
|
||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||
if (dropped > 0)
|
||||
ESP_LOGW(TAG, "Dropped %u scan reports (queue full or out of memory for a report slot)", dropped);
|
||||
// Drain the lock-free ring filled by the rw task; all per-report work runs
|
||||
// here on the main task, then the report returns to the pool.
|
||||
BLEScanReport *report = this->report_queue_.pop();
|
||||
if (report == nullptr)
|
||||
return;
|
||||
do {
|
||||
if (report != nullptr) {
|
||||
this->reject_diagnosis_done_ = true;
|
||||
do {
|
||||
#ifdef LN882H_BLE_SCAN_LISTENER_COUNT
|
||||
for (auto *listener : this->scan_listeners_)
|
||||
listener->on_scan_report(*report);
|
||||
for (auto *listener : this->scan_listeners_)
|
||||
listener->on_scan_report(*report);
|
||||
#endif
|
||||
this->report_pool_.release(report);
|
||||
} while ((report = this->report_queue_.pop()) != nullptr);
|
||||
this->report_pool_.release(report);
|
||||
} while ((report = this->report_queue_.pop()) != nullptr);
|
||||
}
|
||||
|
||||
// Log dropped reports — only reachable when reports were processed; drops can
|
||||
// only occur while the queue is full, and only this loop drains it.
|
||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||
if (dropped > 0)
|
||||
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
|
||||
// Rejected-report accounting AFTER the drain: a stray non-legacy frame
|
||||
// arriving ahead of the first good one must not latch the dead-scanner
|
||||
// warning; the threshold keeps one-off boot noise below it while a truly
|
||||
// dead scanner (~200 reports/s all rejected) crosses it within a second.
|
||||
// Avoid the sub-word CAS in the common case (LockFreeQueue's dropped-count
|
||||
// pattern): rejects are rare, the load is cheap.
|
||||
uint16_t rejected = this->rejected_reports_.load(std::memory_order_relaxed);
|
||||
if (rejected > 0) {
|
||||
rejected = this->rejected_reports_.exchange(0, std::memory_order_relaxed);
|
||||
if (!this->reject_diagnosis_done_) {
|
||||
this->rejected_before_delivery_ += rejected;
|
||||
if (this->rejected_before_delivery_ >= REJECTED_DEAD_SCANNER_THRESHOLD) {
|
||||
this->reject_diagnosis_done_ = true;
|
||||
ESP_LOGW(TAG, "Rejected %u scan reports before any was delivered - unexpected report encoding?",
|
||||
static_cast<unsigned>(this->rejected_before_delivery_));
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Rejected %u non-legacy scan reports", rejected);
|
||||
}
|
||||
}
|
||||
|
||||
void LN882HBLE::get_mac_lsb_first(uint8_t out[6]) const { memcpy(out, this->ble_mac_, sizeof(this->ble_mac_)); }
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::ln882h_ble {
|
||||
@@ -61,6 +62,10 @@ class BLEScanListener {
|
||||
// during such stalls.
|
||||
static constexpr uint8_t MAX_SCAN_REPORT_QUEUE_SIZE = 64;
|
||||
|
||||
// Rejected frames tolerated before the first delivered report without
|
||||
// declaring the scanner dead (boot-time stray extended frames are normal).
|
||||
static constexpr uint16_t REJECTED_DEAD_SCANNER_THRESHOLD = 16;
|
||||
|
||||
class LN882HBLE final : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -105,6 +110,10 @@ class LN882HBLE final : public Component {
|
||||
/// Internal: hand a filled slot to the main-task queue (cannot fail — the
|
||||
/// pool is sized to the queue capacity).
|
||||
void push_scan_report(BLEScanReport *report);
|
||||
/// Internal, rw-task context: count a report rejected by the legacy-only
|
||||
/// filter, so a wrong assumption about the stack's report encoding shows up
|
||||
/// in verbose logs instead of as a scanner that silently reports nothing.
|
||||
void count_rejected_report() { this->rejected_reports_.fetch_add(1, std::memory_order_relaxed); }
|
||||
|
||||
protected:
|
||||
void resolve_mac_();
|
||||
@@ -126,10 +135,17 @@ class LN882HBLE final : public Component {
|
||||
// allocate() returns nullptr before push() can fail. This prevents leaking a
|
||||
// pool slot on a failed push and keeps release() off the producer path.
|
||||
esphome::EventPool<BLEScanReport, MAX_SCAN_REPORT_QUEUE_SIZE - 1> report_pool_;
|
||||
// Reports rejected by the legacy-only filter (rw-task producer, main-task
|
||||
// consumer via exchange in loop()).
|
||||
std::atomic<uint16_t> rejected_reports_{0};
|
||||
uint8_t ble_mac_[6]{0}; // controller (LSB-first) order, as ln_bd_addr_t stores it
|
||||
BLEComponentState state_{BLEComponentState::STATE_OFF};
|
||||
bool enable_on_boot_{false};
|
||||
bool scanning_{false}; // controller scan running (re-entry guard for scan_start)
|
||||
// Dead-scanner diagnosis: done once a report is delivered or the one-shot
|
||||
// warning has fired, whichever comes first.
|
||||
bool reject_diagnosis_done_{false};
|
||||
uint32_t rejected_before_delivery_{0}; // drives the dead-scanner warning
|
||||
};
|
||||
|
||||
} // namespace esphome::ln882h_ble
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""LN882H BLE scanner implementing the ble_device_base BLEHub contract on
|
||||
top of the ln882h_ble controller. With continuous: false nothing scans until
|
||||
an explicit start_scan() call."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base, ln882h_ble, ota
|
||||
from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTIVE,
|
||||
CONF_CONTINUOUS,
|
||||
CONF_DURATION,
|
||||
CONF_ID,
|
||||
CONF_INTERVAL,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_LN882H_BLE_ID = "ln882h_ble_id"
|
||||
|
||||
DEPENDENCIES = ["ln882x"]
|
||||
AUTO_LOAD = ["ble_device_base", "ln882h_ble"]
|
||||
CODEOWNERS = ["@Bl00d-B0b"]
|
||||
|
||||
ln882h_ble_tracker_ns = cg.esphome_ns.namespace("ln882h_ble_tracker")
|
||||
LN882HBLETracker = ln882h_ble_tracker_ns.class_(
|
||||
"LN882HBLETracker", ble_device_base.BLEHub, cg.Component
|
||||
)
|
||||
|
||||
|
||||
# LN882H SDK reference scan rate: 100 ms interval / 50 ms window (50 % duty).
|
||||
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema(
|
||||
"100ms", window_default="50ms", supports_active=True
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LN882HBLETracker),
|
||||
cv.GenerateID(CONF_LN882H_BLE_ID): cv.use_id(ln882h_ble.LN882HBLE),
|
||||
cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[CONF_LN882H_BLE_ID])
|
||||
cg.add(var.set_parent(parent))
|
||||
# The tracker registers itself as a controller scan listener in setup();
|
||||
# request the codegen-sized StaticVector slot for it.
|
||||
ln882h_ble.request_scan_listener_slot()
|
||||
|
||||
# Get notified when an OTA update starts, to pause scanning (esp32_ble_tracker parity)
|
||||
ota.request_ota_state_listeners()
|
||||
|
||||
scan = config[CONF_SCAN_PARAMETERS]
|
||||
cg.add(var.set_scan_interval(ble_device_base.to_ble_units(scan[CONF_INTERVAL])))
|
||||
cg.add(var.set_scan_window(ble_device_base.to_ble_units(scan[CONF_WINDOW])))
|
||||
cg.add(var.set_scan_duration(scan[CONF_DURATION].total_milliseconds))
|
||||
cg.add(var.set_scan_active(scan[CONF_ACTIVE]))
|
||||
cg.add(var.set_scan_continuous(scan[CONF_CONTINUOUS]))
|
||||
@@ -0,0 +1,345 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "ln882h_ble_tracker.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ln882h_ble_tracker {
|
||||
|
||||
static const char *const TAG = "ln882h_ble_tracker";
|
||||
|
||||
static constexpr float BLE_SCAN_UNIT_MS = 0.625f;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LN882HBLETracker::setup() {
|
||||
// Receive the controller's scan reports; the controller queues them from the
|
||||
// rw task and delivers here on the main task.
|
||||
this->parent_->register_scan_listener(this);
|
||||
if (!this->scan_continuous_) {
|
||||
// Say so once: with continuous: false nothing scans until an explicit
|
||||
// start_scan() — silence here reads as a broken scanner.
|
||||
ESP_LOGD(TAG, "Scanning not started (continuous: false) - waiting for an explicit start_scan()");
|
||||
// Nothing to time until then; start_scan_() re-enables the loop.
|
||||
this->disable_loop();
|
||||
}
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Pause scanning while an OTA update is in flight — on the single-core LN882H the
|
||||
// BLE scan competes with the OTA flash writes. Mirrors esp32_ble_tracker.
|
||||
ota::get_global_ota_callback()->add_global_state_listener(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
void LN882HBLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
|
||||
ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->scan_continuous_before_ota_ = this->scan_continuous_;
|
||||
this->scan_running_before_ota_ = this->scan_running_;
|
||||
this->stop_scan();
|
||||
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
|
||||
// On success the device reboots, so restore only on a failed/aborted
|
||||
// update. Continuous mode resumes via loop()'s idle branch; a one-shot
|
||||
// scan that was running is restarted explicitly (bk72xx sibling parity —
|
||||
// stop_scan() cleared it and nothing else would bring it back).
|
||||
if (this->scan_continuous_before_ota_) {
|
||||
this->scan_continuous_ = true;
|
||||
this->enable_loop(); // stop_scan() disabled it; loop()'s idle branch restarts the scan
|
||||
} else if (this->scan_running_before_ota_) {
|
||||
this->start_scan();
|
||||
}
|
||||
this->scan_continuous_before_ota_ = false;
|
||||
this->scan_running_before_ota_ = false;
|
||||
}
|
||||
}
|
||||
#endif // USE_OTA_STATE_LISTENER
|
||||
|
||||
void LN882HBLETracker::loop() {
|
||||
// Flush pending scannable advertisements whose scan response never arrived
|
||||
// (device didn't answer / frame lost) — delivered unmerged after the timeout.
|
||||
// Main-task only, like every consumer of pending_adv_.
|
||||
const uint32_t now = millis();
|
||||
if (this->pending_count_ != 0) {
|
||||
for (auto &p : this->pending_adv_) {
|
||||
if (p.used && now - p.stored_ms > PENDING_ADV_TIMEOUT_MS) {
|
||||
p.used = false;
|
||||
this->pending_count_--;
|
||||
this->process_adv_(p.mac, p.rssi, p.addr_type, p.data, p.data_len, /*raw_only=*/false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->scan_continuous_) {
|
||||
if (!this->scan_running_) {
|
||||
this->start_scan_();
|
||||
// start_scan_() re-anchors scan_period_start_ from a later millis() than
|
||||
// the cached `now`; resume the period timer next iteration.
|
||||
return;
|
||||
}
|
||||
// Period timer: once per scan_duration_ window, restart the controller scan
|
||||
// and fire on_scan_end(), mirroring esp32_ble_tracker::cleanup_scan_state_().
|
||||
// The restart is the recovery path for the coexistence failure documented in
|
||||
// the header. scan_start() re-enters cleanly on its own: it stops an
|
||||
// in-flight scan and grants the controller's 10 ms GAPM settle before
|
||||
// restarting — an explicit scan_stop() first would clear the controller's
|
||||
// re-entry guard and skip that settle.
|
||||
if (now - this->scan_period_start_ >= this->scan_duration_) {
|
||||
ESP_LOGD(TAG, "Scan period elapsed - restarting scan");
|
||||
this->parent_->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_);
|
||||
// Keep both clocks anchored to the restart: a runtime switch to
|
||||
// non-continuous then times out the current period, not the whole run.
|
||||
this->scan_start_time_ = now;
|
||||
this->end_scan_period_(now);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end.
|
||||
// Restart is driven externally (e.g. wifi: on_connect:).
|
||||
if (this->scan_running_ && now - this->scan_start_time_ >= this->scan_duration_) {
|
||||
this->stop_scan_();
|
||||
}
|
||||
}
|
||||
|
||||
bool LN882HBLETracker::request_scan_mode(bool active) {
|
||||
if (this->scan_active_ == active)
|
||||
return true;
|
||||
this->scan_active_ = active;
|
||||
ESP_LOGD(TAG, "Scan mode %s", active ? "active" : "passive");
|
||||
// scan_start() re-enters cleanly (stops + GAPM settle). No on_scan_end and
|
||||
// no period reset: the scan logically continues, only the mode changes.
|
||||
if (this->scan_running_) {
|
||||
this->parent_->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void LN882HBLETracker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LN882H BLE Tracker:\n"
|
||||
" Scan Duration: %" PRIu32 " s\n"
|
||||
" Scan Interval: %.0f ms (%" PRIu16 " BLE units)\n"
|
||||
" Scan Window: %.0f ms (%" PRIu16 " BLE units)\n"
|
||||
" Scan Type: %s\n"
|
||||
" Continuous Scanning: %s",
|
||||
this->scan_duration_ / 1000, this->scan_interval_ * BLE_SCAN_UNIT_MS, this->scan_interval_,
|
||||
this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_window_, this->scan_active_ ? "ACTIVE" : "PASSIVE",
|
||||
YESNO(this->scan_continuous_));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adv/scan-response demux with Bluedroid-style merge: the LN controller
|
||||
// delivers the pair as separate reports; a scannable advertisement is held
|
||||
// until its scan response arrives and delivered as one merged frame.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LN882HBLETracker::on_scan_report(const ln882h_ble::BLEScanReport &report) {
|
||||
if (report.is_scan_response) {
|
||||
this->deliver_scan_rsp_(report);
|
||||
return;
|
||||
}
|
||||
// Stash only while the scan runs: after a one-shot stop the loop is
|
||||
// disabled and nothing would sweep the table, so a late report would
|
||||
// surface minutes later as a fresh advertisement.
|
||||
if (this->scan_running_ && this->scan_active_ && report.scannable) {
|
||||
this->stash_adv_(report);
|
||||
return;
|
||||
}
|
||||
this->process_adv_(report.mac, report.rssi, report.addr_type, report.data, report.data_len, /*raw_only=*/false);
|
||||
}
|
||||
|
||||
// Hold a scannable advertisement, waiting (≤ PENDING_ADV_TIMEOUT_MS) for its
|
||||
// scan response.
|
||||
void LN882HBLETracker::stash_adv_(const ln882h_ble::BLEScanReport &report) {
|
||||
// One pass: find a same-device entry (deliver + reuse) while remembering the
|
||||
// first free slot as the fallback.
|
||||
PendingAdv *slot = nullptr;
|
||||
PendingAdv *free_slot = nullptr;
|
||||
for (auto &p : this->pending_adv_) {
|
||||
if (!p.used) {
|
||||
if (free_slot == nullptr)
|
||||
free_slot = &p;
|
||||
continue;
|
||||
}
|
||||
if (p.addr_type == report.addr_type && memcmp(p.mac, report.mac, 6) == 0) {
|
||||
// Same device advertised again before its scan response arrived — deliver
|
||||
// the previous advertisement (its scan response is not coming) and reuse
|
||||
// the slot, so no frame is ever lost.
|
||||
p.used = false;
|
||||
this->pending_count_--;
|
||||
this->process_adv_(p.mac, p.rssi, p.addr_type, p.data, p.data_len, /*raw_only=*/false);
|
||||
slot = &p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = free_slot;
|
||||
if (slot == nullptr) {
|
||||
// Table full — degrade gracefully: deliver the advertisement unmerged.
|
||||
this->process_adv_(report.mac, report.rssi, report.addr_type, report.data, report.data_len, /*raw_only=*/false);
|
||||
return;
|
||||
}
|
||||
slot->used = true;
|
||||
this->pending_count_++;
|
||||
memcpy(slot->mac, report.mac, 6);
|
||||
slot->addr_type = report.addr_type;
|
||||
slot->rssi = report.rssi;
|
||||
slot->data_len = (report.data_len <= sizeof(slot->data)) ? report.data_len : sizeof(slot->data);
|
||||
memcpy(slot->data, report.data, slot->data_len);
|
||||
slot->stored_ms = millis();
|
||||
}
|
||||
|
||||
// Scan response arrived: merge it with the pending advertisement from the same
|
||||
// device into ONE frame (ESP-IDF/Bluedroid semantics).
|
||||
void LN882HBLETracker::deliver_scan_rsp_(const ln882h_ble::BLEScanReport &report) {
|
||||
// Fast-out on the empty table (loop()/flush use the same guard); this is
|
||||
// the hottest caller.
|
||||
if (this->pending_count_ != 0) {
|
||||
for (auto &p : this->pending_adv_) {
|
||||
if (p.used && p.addr_type == report.addr_type && memcmp(p.mac, report.mac, 6) == 0) {
|
||||
// Append in place: the slot is released on delivery, so its 62-byte
|
||||
// buffer (legacy adv + scan response) holds the merged frame directly.
|
||||
const uint8_t room = sizeof(p.data) - p.data_len;
|
||||
const uint8_t add = (report.data_len <= room) ? report.data_len : room;
|
||||
memcpy(p.data + p.data_len, report.data, add);
|
||||
p.used = false;
|
||||
this->pending_count_--;
|
||||
// The advertisement's RSSI, not the scan response's: every unmerged path
|
||||
// reports the advertisement's measurement, so a device's RSSI must not
|
||||
// jump between two measurements depending on merge timing.
|
||||
this->process_adv_(report.mac, p.rssi, report.addr_type, p.data, p.data_len + add, /*raw_only=*/false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unmatched scan-response: goes out on the raw callback only (HA merges per
|
||||
// address); local listeners/triggers receive each advertisement exactly once
|
||||
// via the merged/plain path above.
|
||||
this->process_adv_(report.mac, report.rssi, report.addr_type, report.data, report.data_len, /*raw_only=*/true);
|
||||
}
|
||||
|
||||
void LN882HBLETracker::process_adv_(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data,
|
||||
uint8_t data_len, bool raw_only) {
|
||||
// Raw callback (the raw-advertisement path). Both full advertisements and
|
||||
// unmatched scan responses (raw_only) are forwarded.
|
||||
if (this->raw_advertisement_callback_.is_set()) {
|
||||
const ble_device_base::RawAdvertisement adv{
|
||||
.mac = mac, .data = data, .data_len = data_len, .rssi = rssi, .addr_type = addr_type};
|
||||
this->raw_advertisement_callback_.invoke(adv);
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
// Scan-response-only frames are never parsed for local sensors/triggers.
|
||||
if (raw_only)
|
||||
return;
|
||||
ble_device_base::ESPBTDevice device;
|
||||
device.from_scan_result(mac, rssi, addr_type, data, data_len);
|
||||
// The listener list holds sensors AND this tracker's automation triggers
|
||||
// (the triggers are listeners, exactly like esp32_ble_tracker), so one
|
||||
// loop feeds both and ORs into `found`.
|
||||
bool found = false;
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->parse_device(device)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// Mirror esp32_ble_tracker: log a newly-seen device only when nothing claimed
|
||||
// it and the scan is one-shot (continuous scans would spam).
|
||||
if (!found && !this->scan_continuous_)
|
||||
this->discovered_log_.log_device(TAG, device);
|
||||
#endif // ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public scan actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LN882HBLETracker::start_scan() {
|
||||
// Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via
|
||||
// set_scan_continuous() first, then calls start_scan() to begin scanning.
|
||||
if (!this->scan_running_) {
|
||||
this->start_scan_();
|
||||
}
|
||||
}
|
||||
|
||||
void LN882HBLETracker::stop_scan() {
|
||||
this->scan_continuous_ = false;
|
||||
this->stop_scan_();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal scan start / stop
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LN882HBLETracker::start_scan_() {
|
||||
if (this->scan_running_)
|
||||
return;
|
||||
|
||||
// The controller enables the stack on first use and owns the report queue;
|
||||
// this call is all the SDK interaction the tracker ever needs.
|
||||
this->parent_->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_);
|
||||
const uint32_t now = millis();
|
||||
this->scan_running_ = true;
|
||||
this->scan_start_time_ = now;
|
||||
this->enable_loop(); // an idle non-continuous tracker disabled it in stop_scan_()
|
||||
// Log every explicit start at DEBUG — stop_scan_() logs every stop at DEBUG, and
|
||||
// in non-continuous mode each period is an explicit start, so asymmetric logging
|
||||
// would read as the scanner failing to come back up.
|
||||
ESP_LOGD(TAG, "BLE scan started (%s, window=%.0fms, interval=%.0fms)", this->scan_active_ ? "active" : "passive",
|
||||
this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_interval_ * BLE_SCAN_UNIT_MS);
|
||||
// Re-anchor the on_scan_end period to every successful start, so a restart
|
||||
// later than scan_duration (e.g. a failed OTA restoring continuous mode)
|
||||
// does not fire on_scan_end before an advertisement can arrive.
|
||||
this->scan_period_start_ = now;
|
||||
}
|
||||
|
||||
void LN882HBLETracker::stop_scan_() {
|
||||
if (!this->scan_running_)
|
||||
return;
|
||||
this->parent_->scan_stop();
|
||||
this->scan_running_ = false;
|
||||
// DEBUG like start_scan_() — a per-period stop at INFO would read as the
|
||||
// scanner failing to come back up.
|
||||
ESP_LOGD(TAG, "BLE scan stopped");
|
||||
this->end_scan_period_(millis()); // also resets the period clock so on_scan_end does not double-fire
|
||||
if (!this->scan_continuous_) {
|
||||
// Nothing left to time; start_scan_() re-enables the loop.
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
// Close a scan period: deliver held advertisements whose scan response never
|
||||
// came (unmerged) BEFORE on_scan_end fires, then re-anchor the period clock.
|
||||
void LN882HBLETracker::end_scan_period_(uint32_t now) {
|
||||
this->flush_pending_adv_();
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
this->discovered_log_.clear(); // reset per-scan "Found device" dedup (esp32_ble_tracker parity)
|
||||
#endif
|
||||
this->scan_period_start_ = now;
|
||||
}
|
||||
|
||||
// Deliver every held advertisement now (scan period/scan is ending): unmerged
|
||||
// delivery, same as the timeout path in loop(). Main-task only.
|
||||
void LN882HBLETracker::flush_pending_adv_() {
|
||||
if (this->pending_count_ == 0)
|
||||
return;
|
||||
for (auto &p : this->pending_adv_) {
|
||||
if (p.used) {
|
||||
p.used = false;
|
||||
this->process_adv_(p.mac, p.rssi, p.addr_type, p.data, p.data_len, /*raw_only=*/false);
|
||||
}
|
||||
}
|
||||
this->pending_count_ = 0;
|
||||
}
|
||||
|
||||
} // namespace esphome::ln882h_ble_tracker
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -0,0 +1,176 @@
|
||||
// BLE scanner for LN882H: implements ble_device_base::BLEHub on top of the
|
||||
// ln882h_ble controller (which owns all SDK calls and delivers scan reports on
|
||||
// the main task). Scan policy lives here: parameters, period timers with
|
||||
// per-period restart, and the adv+scan-response merge.
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
#include "esphome/components/ln882h_ble/ln882h_ble.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ln882h_ble_tracker {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LN882HBLETracker
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LN882HBLETracker : public Component,
|
||||
public ble_device_base::BLEHub,
|
||||
public Parented<ln882h_ble::LN882HBLE>,
|
||||
public ln882h_ble::BLEScanListener
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
,
|
||||
public ota::OTAGlobalStateListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
// ---- ESPHome Component ----
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Pause scanning while an OTA update runs (single-core WiFi/BLE/flash contention);
|
||||
// mirrors esp32_ble_tracker.
|
||||
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
|
||||
#endif
|
||||
|
||||
// ---- YAML configuration setters ----
|
||||
void set_scan_active(bool scan_active) { this->scan_active_ = scan_active; }
|
||||
void set_scan_interval(uint16_t scan_interval) { this->scan_interval_ = scan_interval; }
|
||||
void set_scan_window(uint16_t scan_window) { this->scan_window_ = scan_window; }
|
||||
void set_scan_duration(uint32_t scan_duration) { this->scan_duration_ = scan_duration; }
|
||||
void set_scan_continuous(bool scan_continuous) { this->scan_continuous_ = scan_continuous; }
|
||||
|
||||
// ---- Public scan control ----
|
||||
// Mirrors esp32_ble_tracker: set_scan_continuous() + start_scan() / stop_scan().
|
||||
void start_scan();
|
||||
void stop_scan();
|
||||
|
||||
// ---- ble_device_base::BLEHub contract ----
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override {
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
this->listeners_.push_back(listener);
|
||||
#endif
|
||||
}
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||
this->raw_advertisement_callback_ = callback;
|
||||
}
|
||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||
// The LN882H controller supports active scanning; adv + scan response arrive
|
||||
// as separate reports and are merged by this tracker (Bluedroid semantics).
|
||||
// The SDK's GATT client is not exposed.
|
||||
return {.active_scan = true, .merges_scan_response = true, .gatt = false};
|
||||
}
|
||||
// The controller stores the address LSB-first (BLE convention); the contract
|
||||
// wants printable (MSB-first) order.
|
||||
void get_adapter_mac(uint8_t out[6]) override {
|
||||
uint8_t mac[6];
|
||||
this->parent_->get_mac_lsb_first(mac);
|
||||
for (int i = 0; i < 6; i++)
|
||||
out[i] = mac[5 - i];
|
||||
}
|
||||
bool scan_running() override { return this->scan_running_; }
|
||||
bool scan_active() override { return this->scan_active_; }
|
||||
bool request_scan_mode(bool active) override;
|
||||
|
||||
// ---- ln882h_ble::BLEScanListener ----
|
||||
// Delivered by the controller's loop() on the ESPHome main task — the
|
||||
// rw-task → main-task handoff already happened in the controller's queue.
|
||||
// Demultiplexes advertisements vs scan responses and drives the merge.
|
||||
void on_scan_report(const ln882h_ble::BLEScanReport &report) override;
|
||||
|
||||
protected:
|
||||
// Bluedroid-style adv + scan-response merging (ESP-IDF concatenates both into
|
||||
// one result before ESPHome sees it; the LN controller reports them separately):
|
||||
// a scannable advertisement is held here briefly, its scan response is appended
|
||||
// on arrival and the pair is delivered as ONE merged frame. Held entries whose
|
||||
// scan response never arrives are flushed by loop() after PENDING_ADV_TIMEOUT_MS.
|
||||
// All of this runs on the main task (the controller queue already crossed tasks),
|
||||
// so no locking is involved.
|
||||
void stash_adv_(const ln882h_ble::BLEScanReport &report);
|
||||
void deliver_scan_rsp_(const ln882h_ble::BLEScanReport &report);
|
||||
// Dispatch one (possibly merged) advertisement: the raw
|
||||
// callback, and — unless raw_only — parsing for listeners/triggers. raw_only
|
||||
// marks unmatched scan-response frames: forwarded on the raw callback only,
|
||||
// never to local sensors/triggers (HA merges per address).
|
||||
void process_adv_(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len,
|
||||
bool raw_only);
|
||||
void start_scan_();
|
||||
void stop_scan_();
|
||||
// Close a scan period: flush held advertisements (unmerged) BEFORE
|
||||
// on_scan_end fires, then re-anchor the period clock to `now`.
|
||||
void end_scan_period_(uint32_t now);
|
||||
void flush_pending_adv_();
|
||||
|
||||
bool scan_running_{false};
|
||||
bool scan_active_{false};
|
||||
// Defaults are the LN882H SDK's recommended scan parameters
|
||||
// (ln_ble_scan.h: SCAN_INTERVAL_DEF 0xA0, SCAN_WINDOW_DEF 0x50 → 50 % duty).
|
||||
// uint16_t matches the controller's scan_start() parameters.
|
||||
uint16_t scan_interval_{160}; // 160 × 0.625 ms = 100 ms (SDK SCAN_INTERVAL_DEF)
|
||||
uint16_t scan_window_{80}; // 80 × 0.625 ms = 50 ms (SDK SCAN_WINDOW_DEF; 50/100 = 50 %)
|
||||
uint32_t scan_duration_{300000};
|
||||
bool scan_continuous_{true};
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
bool scan_continuous_before_ota_{false}; // continuous mode saved at OTA start, restored on OTA failure
|
||||
bool scan_running_before_ota_{false}; // one-shot scan running at OTA start, restarted on OTA failure
|
||||
#endif
|
||||
uint32_t scan_start_time_{0};
|
||||
|
||||
// Pending scannable advertisements awaiting their scan response (active scan).
|
||||
// 62 bytes = legacy adv (31) + scan response (31), the same merged maximum as
|
||||
// ESP-IDF delivers on ESP32. Main-task only.
|
||||
struct PendingAdv {
|
||||
bool used{false};
|
||||
uint8_t mac[6];
|
||||
uint8_t addr_type;
|
||||
int8_t rssi;
|
||||
uint8_t data_len; // <= sizeof(data)
|
||||
uint8_t data[62];
|
||||
uint32_t stored_ms;
|
||||
};
|
||||
// Sized for the unanswered case: a pair that IS answered normally matches
|
||||
// within one queue drain, so a slot is held for the full timeout only by
|
||||
// scannable devices that never reply. 8 concurrent such advertisers before
|
||||
// the merge degrades (frames still delivered, just unmerged) at ~80 B each.
|
||||
static constexpr size_t MAX_PENDING_ADV = 8;
|
||||
// On air a scan response follows its advertisement by T_IFS (150 µs) — the
|
||||
// timeout only covers HOST-side report queuing in rw_task under WiFi/BLE
|
||||
// coexistence, measured on-device at up to ~136 ms. 300 ms = >2x that margin,
|
||||
// while staying below any device's re-advertising period.
|
||||
static constexpr uint32_t PENDING_ADV_TIMEOUT_MS = 300;
|
||||
PendingAdv pending_adv_[MAX_PENDING_ADV];
|
||||
// Occupied pending_adv_ slots — lets loop()'s timeout sweep skip the table
|
||||
// in the common case (empty: passive scan, or every pair already matched).
|
||||
uint8_t pending_count_{0};
|
||||
|
||||
uint32_t scan_period_start_{0}; // millis() at start of current scan period; used to rate-limit on_scan_end()
|
||||
|
||||
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{};
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
// Parsed-advertisement consumers registered through ble_device_base.
|
||||
// Codegen-sized: no heap allocation, no std::vector template instantiations.
|
||||
StaticVector<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> listeners_;
|
||||
// Per-period "Found device" DEBUG log with MAC dedup — shared implementation
|
||||
// in ble_device_base, identical output on every tracker backend. Guarded like
|
||||
// its only writer so a no-listener build does not carry an unused vector.
|
||||
ble_device_base::DiscoveredDeviceLog discovered_log_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::ln882h_ble_tracker
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -458,6 +458,7 @@
|
||||
#define BK72XX_BLE_SCAN_LISTENER_COUNT 1
|
||||
#define USE_LN882H_BLE
|
||||
#define LN882H_BLE_SCAN_LISTENER_COUNT 1
|
||||
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_WIFI_SCAN_RESULTS_LOCK
|
||||
#define USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
|
||||
@@ -10,6 +10,9 @@ from esphome.components.bk72xx_ble_tracker import (
|
||||
)
|
||||
from esphome.components.ble_device_base import to_ble_units
|
||||
from esphome.components.esp32_ble_tracker import SCAN_PARAMETERS_SCHEMA as ESP32_SCHEMA
|
||||
from esphome.components.ln882h_ble_tracker import (
|
||||
SCAN_PARAMETERS_SCHEMA as LN882H_SCHEMA,
|
||||
)
|
||||
from esphome.components.rp2_ble_tracker import SCAN_PARAMETERS_SCHEMA as RP2_SCHEMA
|
||||
|
||||
|
||||
@@ -69,6 +72,15 @@ def test_rp2_defaults_are_valid() -> None:
|
||||
assert config["active"] is True
|
||||
|
||||
|
||||
def test_ln882h_defaults_are_valid() -> None:
|
||||
"""ln882h pins the LN SDK reference rate — 100 ms interval / 50 ms window
|
||||
(50 % duty) — and exposes active (default on)."""
|
||||
config = LN882H_SCHEMA({})
|
||||
assert to_ble_units(config["interval"]) == 160
|
||||
assert to_ble_units(config["window"]) == 80
|
||||
assert config["active"] is True
|
||||
|
||||
|
||||
def test_esp32_active_can_disable() -> None:
|
||||
config = ESP32_SCHEMA({"active": False})
|
||||
assert config["active"] is False
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
ln882h_ble_tracker:
|
||||
id: ble_tracker
|
||||
scan_parameters:
|
||||
# Boundary coverage: the documented 2.5 ms floor on window (expressible only
|
||||
# via the microsecond-accurate validation), a non-round interval exercising the
|
||||
# 0.625 ms unit conversion without collapsing onto the window's unit count,
|
||||
# and the non-continuous config path.
|
||||
interval: 5000us
|
||||
window: 2500us
|
||||
duration: 5min
|
||||
active: false
|
||||
continuous: false
|
||||
@@ -0,0 +1,16 @@
|
||||
ln882h_ble_tracker:
|
||||
id: ble_tracker
|
||||
scan_parameters:
|
||||
interval: 100ms
|
||||
window: 50ms
|
||||
duration: 5min
|
||||
continuous: true
|
||||
|
||||
# Pulls in USE_OTA_STATE_LISTENER so the OTA scan-pause path compiles in CI
|
||||
# (same coverage arrangement as the rp2_ble_tracker tests).
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include common.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include common-boundary.yaml
|
||||
Reference in New Issue
Block a user