[ble_device_base] Keep MAC filter lists in shared flash tables (#20088)

This commit is contained in:
J. Nick Koston
2026-10-05 19:28:36 -05:00
committed by GitHub
parent 3a3707926d
commit 8d8bd73112
9 changed files with 128 additions and 30 deletions
@@ -11,29 +11,34 @@
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <initializer_list>
namespace esphome::ble_device_base {
/// True if `address` is in `table`, a list of MACs ended by 0.
inline bool mac_in_table(const uint64_t *table, uint64_t address) {
for (; *table != 0; table++) {
if (*table == address)
return true;
}
return false;
}
// on_ble_advertise: fires on every BLE advertisement, optionally filtered to one or more MACs.
class ESPBTAdvertiseTrigger final : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
template<typename Hub> explicit ESPBTAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
void set_addresses(std::initializer_list<uint64_t> addresses) { this->addresses_ = addresses; }
/// Table of MACs ended by 0; must outlive the trigger.
void set_addresses(const uint64_t *addresses) { this->addresses_ = addresses; }
bool parse_device(const ESPBTDevice &device) override {
if (!this->addresses_.empty() && std::find(this->addresses_.begin(), this->addresses_.end(),
device.address_uint64()) == this->addresses_.end()) {
if (this->addresses_ != nullptr && !mac_in_table(this->addresses_, device.address_uint64()))
return false;
}
this->trigger(device);
return true;
}
protected:
FixedVector<uint64_t> addresses_;
const uint64_t *addresses_{nullptr};
};
// on_ble_service_data_advertise: fires when an advertisement contains service
@@ -69,13 +69,24 @@ def uuid_trigger_schema(
)
def _filter_mac(value: Any) -> Any:
mac = cv.mac_address(value)
if not any(mac.parts):
# 0 ends the flash MAC table, and it is never a real device address.
raise cv.Invalid("00:00:00:00:00:00 cannot be used as a MAC address filter")
return mac
MAC_FILTER_LIST = cv.ensure_list(_filter_mac)
def advertise_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any]:
"""on_ble_advertise schema: multi-mac list filter, unlike the single-mac
uuid_trigger_schema() — pairs with advertise_trigger_to_code()."""
return automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class),
cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address),
cv.Optional(CONF_MAC_ADDRESS): MAC_FILTER_LIST,
}
)
@@ -94,11 +105,18 @@ def scan_end_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any]
_count_listener = cg.slot_counter(LISTENER_COUNT_DEFINE)
def mac_filter_table(macs: list) -> cg.MockObj:
"""Shared flash table of MACs ended by 0 (never a valid address), so triggers store a pointer."""
return cg.shared_progmem_array(
"ble_mac_filter", cg.uint64, [*(mac.as_hex for mac in macs), 0]
)
async def advertise_trigger_to_code(conf: ConfigType, var: cg.MockObj) -> None:
"""Build an on_ble_advertise trigger (optional multi-mac filter)."""
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if (macs := conf.get(CONF_MAC_ADDRESS)) is not None:
cg.add(trigger.set_addresses([it.as_hex for it in macs]))
if macs := conf.get(CONF_MAC_ADDRESS):
cg.add(trigger.set_addresses(mac_filter_table(macs)))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
_count_listener()
@@ -8,6 +8,10 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import ble_device_base, esp32_ble, ota
from esphome.components.ble_device_base import CONF_CONNECTION_SCAN_WINDOW
from esphome.components.ble_device_base.automation import (
MAC_FILTER_LIST,
mac_filter_table,
)
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
@@ -277,7 +281,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESPBTAdvertiseTrigger
),
cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address),
cv.Optional(CONF_MAC_ADDRESS): MAC_FILTER_LIST,
}
),
cv.Optional(
@@ -381,9 +385,8 @@ async def to_code(config: ConfigType) -> None:
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
_request_listener_slot()
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
if macs := conf.get(CONF_MAC_ADDRESS):
cg.add(trigger.set_addresses(mac_filter_table(macs)))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
_request_listener_slot()
@@ -1,10 +1,9 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/components/ble_device_base/automation.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include <vector>
#ifdef USE_ESP32
namespace esphome::esp32_ble_tracker {
@@ -12,22 +11,18 @@ namespace esphome::esp32_ble_tracker {
class ESPBTAdvertiseTrigger final : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
void set_addresses(std::initializer_list<uint64_t> addresses) { this->address_vec_ = addresses; }
/// Table of MACs ended by 0; must outlive the trigger.
void set_addresses(const uint64_t *addresses) { this->addresses_ = addresses; }
bool parse_device(const ESPBTDevice &device) override {
uint64_t u64_addr = device.address_uint64();
if (!address_vec_.empty()) {
if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) {
return false;
}
}
if (this->addresses_ != nullptr && !ble_device_base::mac_in_table(this->addresses_, device.address_uint64()))
return false;
this->trigger(device);
return true;
}
protected:
std::vector<uint64_t> address_vec_;
const uint64_t *addresses_{nullptr};
};
class BLEServiceDataAdvertiseTrigger final : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {
@@ -22,7 +22,11 @@ def test_trigger_codegen(
main_cpp = generate_main(component_config_path("test_automations.yaml"))
# on_ble_advertise: multi-mac filter (two addresses in one initializer list)
assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp
assert (
"static constexpr uint64_t ble_mac_filter[] PROGMEM = "
"{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp
)
assert "set_addresses(ble_mac_filter)" in main_cpp
# 128-bit service uuid goes out reversed (BLE wire order); single-mac filter
assert (
"set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
@@ -0,0 +1,27 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
esp32_ble_tracker:
on_ble_advertise:
- mac_address:
- AC:37:43:77:5F:4C
- 11:22:33:44:55:66
then:
- logger.log: one
- mac_address:
- AC:37:43:77:5F:4C
- 11:22:33:44:55:66
then:
- logger.log: two
- then:
- logger.log: any
- mac_address: []
then:
- logger.log: empty
@@ -0,0 +1,38 @@
"""Tests for the esp32_ble_tracker on_ble_advertise MAC filter codegen."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome import config_validation as cv
from esphome.components.ble_device_base.automation import MAC_FILTER_LIST
def test_mac_filters_share_one_progmem_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Identical MAC lists share one flash table ended by 0; no or an empty filter sets none."""
main_cpp = generate_main(component_config_path("mac_filter.yaml"))
assert (
"static constexpr uint64_t ble_mac_filter[] PROGMEM = "
"{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp
)
assert main_cpp.count("set_addresses(ble_mac_filter);") == 2
assert main_cpp.count("set_addresses(") == 2
def test_zero_mac_is_rejected() -> None:
"""00:00:00:00:00:00 ends the flash table, so it cannot be a filter entry."""
assert MAC_FILTER_LIST(["11:22:33:44:55:66"])[0].parts == (
0x11,
0x22,
0x33,
0x44,
0x55,
0x66,
)
with pytest.raises(cv.Invalid):
MAC_FILTER_LIST(["00:00:00:00:00:00"])
@@ -17,7 +17,11 @@ def test_trigger_codegen(
main_cpp = generate_main(component_config_path("test_automations.yaml"))
# on_ble_advertise: multi-mac filter (two addresses in one initializer list)
assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp
assert (
"static constexpr uint64_t ble_mac_filter[] PROGMEM = "
"{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp
)
assert "set_addresses(ble_mac_filter)" in main_cpp
# 128-bit service uuid goes out reversed (BLE wire order); single-mac filter
assert (
"set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"
@@ -19,8 +19,12 @@ def test_trigger_codegen(
) -> None:
main_cpp = generate_main(component_config_path("test_automations.yaml"))
# on_ble_advertise: multi-mac filter (two addresses in one initializer list)
assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp
# on_ble_advertise: multi-mac filter, a shared flash table ended by 0
assert (
"static constexpr uint64_t ble_mac_filter[] PROGMEM = "
"{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp
)
assert "set_addresses(ble_mac_filter)" in main_cpp
# 128-bit service uuid goes out reversed (BLE wire order); single-mac filter
assert (
"set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"