mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 19:44:08 +00:00
[ble_device_base] Keep MAC filter lists in shared flash tables (#20088)
This commit is contained in:
@@ -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,"
|
||||
|
||||
Reference in New Issue
Block a user