[ble_device_base] Platform-neutral BLE layer; esp32_ble_tracker implements BLEHub (#17150)

This commit is contained in:
Edvard Filistovič
2026-07-21 12:32:14 -10:00
committed by GitHub
parent 37be04f58e
commit 4d3b4659d7
20 changed files with 1471 additions and 725 deletions
+1
View File
@@ -74,6 +74,7 @@ esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @dan-s-github @tobias- esphome/components/bl0940/* @dan-s-github @tobias-
esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_device_base/* @Bl00d-B0b
esphome/components/ble_nus/* @tomaszduda23 esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bm8563/* @abmantis esphome/components/bm8563/* @abmantis
@@ -0,0 +1,134 @@
"""
ble_device_base — the platform-neutral BLE layer.
Owns the shared advertisement types (ESPBTUUID / ESPBTDevice / ServiceData /
ESPBLEiBeacon / ESPBTDeviceListener, in ble_device.h) and the tracker contract
(BLEHub, in ble_hub.h) on every platform.
BLE consumers (sensor components, bluetooth_proxy) bind to whichever tracker the
configuration declares via `cv.use_id(BLEHub)` — ESPHome resolves any declared
subclass, so there is no platform table here and no dependency in either
direction. A sensor appends inject_ble_hub to its CONFIG_SCHEMA (via cv.All) and
calls register_ble_device() in to_code; a tracker component subclasses BLEHub
(C++ and codegen class). Adding a new BLE chip requires only a new tracker
component.
AES-CCM decryption for encrypted advertisements is provided portably in
ble_aes_ccm.h.
"""
import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@Bl00d-B0b"]
CONF_BLE_HUB_ID = "ble_hub_id"
# CORE.data key: number of parsed-advertisement listeners registered in this
# build. Trackers whose codegen sizes storage at compile time (esp32's
# StaticVector count define) read it in their final coroutine.
KEY_BLE_LISTENER_COUNT = "ble_device_base_listener_count"
ble_device_base_ns = cg.esphome_ns.namespace("ble_device_base")
# The neutral tracker contract. Every tracker's codegen class declares this as a
# parent, which is what lets cv.use_id(BLEHub) resolve any of them.
BLEHub = ble_device_base_ns.class_("BLEHub")
# The neutral listener base (C++: ble_device_base::ESPBTDeviceListener).
ESPBTDeviceListener = ble_device_base_ns.class_("ESPBTDeviceListener")
def inject_ble_hub(config: ConfigType) -> ConfigType:
"""Validator: auto-resolve the configured BLE tracker into the config.
Append via cv.All to a BLE consumer's CONFIG_SCHEMA. Uses cv.GenerateID +
cv.use_id(BLEHub): an omitted id resolves to the single declared tracker on
any platform; multiple trackers can be disambiguated with an explicit
ble_hub_id.
"""
return cv.Schema(
{cv.GenerateID(CONF_BLE_HUB_ID): cv.use_id(BLEHub)}, extra=cv.ALLOW_EXTRA
)(config)
def request_irk_support() -> None:
"""Compile in resolve_irk()'s software-AES path. Called by sensors with an
irk: option so builds without IRK do not carry the resolution code."""
cg.add_define("USE_BLE_DEVICE_IRK")
def get_listener_count() -> int:
"""Number of parsed listeners registered so far (for tracker codegen)."""
return CORE.data.get(KEY_BLE_LISTENER_COUNT, 0)
async def register_ble_device(var: cg.MockObj, config: ConfigType) -> cg.MockObj:
"""Register `var` as a parsed-advertisement listener on the configured hub."""
hub = await cg.get_variable(config[CONF_BLE_HUB_ID])
cg.add(hub.register_listener(var))
CORE.data[KEY_BLE_LISTENER_COUNT] = CORE.data.get(KEY_BLE_LISTENER_COUNT, 0) + 1
return var
# ---- shared validation / codegen helpers (platform-neutral) ----
BT_UUID16_FORMAT = "XXXX"
BT_UUID32_FORMAT = "XXXXXXXX"
BT_UUID128_FORMAT = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
_BT_UUID16_RE = re.compile("^[A-F0-9]{4,}$")
_BT_UUID32_RE = re.compile("^[A-F0-9]{8,}$")
_BT_UUID128_RE = re.compile(
"^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$"
)
# Validator table keyed by input length: (compiled pattern, label used in errors).
_BT_UUID_FORMATS = {
len(BT_UUID16_FORMAT): (_BT_UUID16_RE, "16 bit"),
len(BT_UUID32_FORMAT): (_BT_UUID32_RE, "32 bit"),
len(BT_UUID128_FORMAT): (_BT_UUID128_RE, "128"),
}
def bt_uuid(value: str) -> str:
in_value = cv.string_strict(value)
value = in_value.upper()
fmt = _BT_UUID_FORMATS.get(len(value))
if fmt is None:
raise cv.Invalid(
f"Bluetooth UUID must be in 16 bit '{BT_UUID16_FORMAT}', 32 bit '{BT_UUID32_FORMAT}', or 128 bit '{BT_UUID128_FORMAT}' format"
)
pattern, label = fmt
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for {label} UUID format: '{in_value}'"
)
return value
def as_hex(value: str) -> cg.RawExpression:
return cg.RawExpression(f"0x{value}ULL")
def _hex_array_expression(value: str, reverse: bool) -> cg.RawExpression:
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
]
if reverse:
cpp_array.reverse()
return cg.RawExpression(f"(uint8_t*)(const uint8_t[16]){{{','.join(cpp_array)}}}")
def as_hex_array(value: str) -> cg.RawExpression:
return _hex_array_expression(value, reverse=False)
def as_reversed_hex_array(value: str) -> cg.RawExpression:
return _hex_array_expression(value, reverse=True)
@@ -0,0 +1,202 @@
#include "ble_aes_ccm.h"
#include <algorithm>
#include <cstring>
namespace esphome::ble_device_base {
namespace {
// AES-128 forward cipher only — CCM uses the block cipher in the encrypt
// direction for both the CTR keystream and the CBC-MAC.
const uint8_t SBOX[256] = {
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, //
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, //
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, //
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, //
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, //
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, //
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, //
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, //
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, //
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, //
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, //
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, //
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, //
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, //
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, //
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, //
};
const uint8_t RCON[11] = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36};
inline uint8_t xtime(uint8_t x) { return static_cast<uint8_t>((x << 1) ^ ((x & 0x80) ? 0x1b : 0x00)); }
// AES-128 forward cipher with on-the-fly key schedule.
class Aes128 {
public:
explicit Aes128(const uint8_t key[16]) {
memcpy(this->rk_, key, 16);
for (size_t i = 16; i < 176; i += 4) {
uint8_t t[4] = {this->rk_[i - 4], this->rk_[i - 3], this->rk_[i - 2], this->rk_[i - 1]};
if (i % 16 == 0) {
const uint8_t tmp = t[0];
t[0] = static_cast<uint8_t>(SBOX[t[1]] ^ RCON[i / 16]);
t[1] = SBOX[t[2]];
t[2] = SBOX[t[3]];
t[3] = SBOX[tmp];
}
for (size_t j = 0; j < 4; j++)
this->rk_[i + j] = static_cast<uint8_t>(this->rk_[i - 16 + j] ^ t[j]);
}
}
void encrypt(const uint8_t in[16], uint8_t out[16]) const {
uint8_t s[16];
memcpy(s, in, 16);
for (size_t i = 0; i < 16; i++)
s[i] ^= this->rk_[i];
for (size_t round = 1; round < 10; round++) {
for (uint8_t &b : s)
b = SBOX[b];
shift_rows(s);
for (size_t c = 0; c < 4; c++) {
uint8_t *col = s + c * 4;
const uint8_t a0 = col[0], a1 = col[1], a2 = col[2], a3 = col[3];
const uint8_t h = static_cast<uint8_t>(a0 ^ a1 ^ a2 ^ a3);
col[0] ^= static_cast<uint8_t>(h ^ xtime(static_cast<uint8_t>(a0 ^ a1)));
col[1] ^= static_cast<uint8_t>(h ^ xtime(static_cast<uint8_t>(a1 ^ a2)));
col[2] ^= static_cast<uint8_t>(h ^ xtime(static_cast<uint8_t>(a2 ^ a3)));
col[3] ^= static_cast<uint8_t>(h ^ xtime(static_cast<uint8_t>(a3 ^ a0)));
}
for (size_t i = 0; i < 16; i++)
s[i] ^= this->rk_[round * 16 + i];
}
for (uint8_t &b : s)
b = SBOX[b];
shift_rows(s);
for (size_t i = 0; i < 16; i++)
s[i] ^= this->rk_[160 + i];
memcpy(out, s, 16);
}
protected:
static void shift_rows(uint8_t s[16]) {
uint8_t t = s[1];
s[1] = s[5];
s[5] = s[9];
s[9] = s[13];
s[13] = t;
t = s[2];
s[2] = s[10];
s[10] = t;
t = s[6];
s[6] = s[14];
s[14] = t;
t = s[3];
s[3] = s[15];
s[15] = s[11];
s[11] = s[7];
s[7] = t;
}
uint8_t rk_[176];
};
} // namespace
void aes128_encrypt_block(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]) {
Aes128 aes(key);
aes.encrypt(in, out);
}
bool aes_ccm_auth_decrypt(const uint8_t key[16], const uint8_t *nonce, size_t nonce_len, const uint8_t *aad,
size_t aad_len, const uint8_t *ciphertext, size_t ct_len, uint8_t *plaintext,
const uint8_t *tag, size_t tag_len) {
// CCM length field width L and tag width M (RFC 3610 §2.2). For a 13-byte
// nonce L = 2; BTHome uses M = 4.
if (nonce_len < 7 || nonce_len > 13 || tag_len < 4 || tag_len > 16)
return false;
const size_t l = 15 - nonce_len;
const size_t m = tag_len;
const Aes128 aes(key);
// Build CTR block A_i = [L-1] | nonce | counter(L bytes, big-endian).
uint8_t a[16];
auto build_ctr = [&](uint32_t counter) {
a[0] = static_cast<uint8_t>(l - 1);
memcpy(a + 1, nonce, nonce_len);
memset(a + 1 + nonce_len, 0, l);
for (size_t i = 0; i < l; i++)
a[15 - i] = static_cast<uint8_t>((counter >> (8 * i)) & 0xff);
};
// S_0 = E(A_0); its first m bytes mask the transmitted tag.
uint8_t s0[16];
build_ctr(0);
aes.encrypt(a, s0);
// CTR-decrypt ciphertext into plaintext using S_1, S_2, ...
uint8_t ks[16];
for (size_t off = 0; off < ct_len; off += 16) {
build_ctr(static_cast<uint32_t>(off / 16) + 1);
aes.encrypt(a, ks);
const size_t n = std::min(static_cast<size_t>(16), ct_len - off);
for (size_t i = 0; i < n; i++)
plaintext[off + i] = static_cast<uint8_t>(ciphertext[off + i] ^ ks[i]);
}
// CBC-MAC over B_0 | (formatted AAD) | plaintext.
uint8_t x[16];
uint8_t b0[16];
const uint8_t flags = static_cast<uint8_t>((aad_len > 0 ? 0x40 : 0x00) | (((m - 2) / 2) << 3) | (l - 1));
b0[0] = flags;
memcpy(b0 + 1, nonce, nonce_len);
memset(b0 + 1 + nonce_len, 0, l);
for (size_t i = 0; i < l; i++)
b0[15 - i] = static_cast<uint8_t>((ct_len >> (8 * i)) & 0xff);
aes.encrypt(b0, x); // X_1 = E(B_0)
if (aad_len > 0) {
// Only the < 2^16-2^8 encoding is needed for BLE-sized AAD.
uint8_t blk[16] = {0};
blk[0] = static_cast<uint8_t>((aad_len >> 8) & 0xff);
blk[1] = static_cast<uint8_t>(aad_len & 0xff);
size_t ai = 0;
size_t pos = 2;
while (pos < 16 && ai < aad_len)
blk[pos++] = aad[ai++];
for (size_t i = 0; i < 16; i++)
x[i] ^= blk[i];
aes.encrypt(x, x);
while (ai < aad_len) {
memset(blk, 0, 16);
const size_t n = std::min(static_cast<size_t>(16), aad_len - ai);
memcpy(blk, aad + ai, n);
ai += n;
for (size_t i = 0; i < 16; i++)
x[i] ^= blk[i];
aes.encrypt(x, x);
}
}
for (size_t off = 0; off < ct_len; off += 16) {
uint8_t blk[16] = {0};
const size_t n = std::min(static_cast<size_t>(16), ct_len - off);
memcpy(blk, plaintext + off, n);
for (size_t i = 0; i < 16; i++)
x[i] ^= blk[i];
aes.encrypt(x, x);
}
// Expected tag U = T XOR S_0[0..m). Constant-time compare with the received tag.
uint8_t diff = 0;
for (size_t i = 0; i < m; i++)
diff |= static_cast<uint8_t>((x[i] ^ s0[i]) ^ tag[i]);
return diff == 0;
}
} // namespace esphome::ble_device_base
@@ -0,0 +1,34 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::ble_device_base {
// Self-contained AES-128-CCM authenticated decryption (RFC 3610).
//
// Encrypted BLE advertisements (BTHome, several Xiaomi/ATC variants) use
// AES-128-CCM. The platform crypto that provides it is inconsistent across BLE
// targets: ESP-IDF exposes PSA/mbedtls, but a LibreTiny SDK may keep its mbedtls
// internal (e.g. the beken-72xx SDK ships mbedtls with CCM enabled but does not
// put it on the application include path), so a sensor cannot rely on
// <mbedtls/ccm.h> being available. This software implementation makes
// encrypted-advertisement decryption work on every BLE platform without a
// per-chip crypto dependency. Decryption volume is tiny (one short block per
// matching advertisement), so software AES is not a meaningful cost.
//
// Verifies the CCM authentication tag and, on success, writes `ct_len` decrypted
// bytes to `plaintext` and returns true. Returns false when authentication fails
// (the caller must then discard `plaintext`). The CCM parameters follow the
// caller (BTHome: 13-byte nonce, 4-byte tag, no associated data); `aad` may be
// null when `aad_len` is 0.
/// AES-128 single-block encrypt (the same software cipher CCM uses). Used by
/// ESPBTDevice::resolve_irk() for the Bluetooth "ah" RPA hash, so IRK matching
/// works identically on every platform with no chip crypto dependency.
void aes128_encrypt_block(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]);
bool aes_ccm_auth_decrypt(const uint8_t key[16], const uint8_t *nonce, size_t nonce_len, const uint8_t *aad,
size_t aad_len, const uint8_t *ciphertext, size_t ct_len, uint8_t *plaintext,
const uint8_t *tag, size_t tag_len);
} // namespace esphome::ble_device_base
@@ -0,0 +1,496 @@
// ble_device.cpp
//
// Platform-neutral implementation of the shared BLE advertisement types.
// Parses raw BLE advertisement data into ESPBTDevice.
#include "ble_device.h"
#include "ble_aes_ccm.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome::ble_device_base {
static const char *const TAG = "ble_device_base";
// Longest advertisement payload worth hex-dumping at VERY_VERBOSE
// (legacy advertising: 31-byte adv + 31-byte scan response).
static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62;
// ---------------------------------------------------------------------------
// ESPBTUUID
// ---------------------------------------------------------------------------
ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) {
ESPBTUUID ret;
ret.type_ = Type::UUID16;
ret.uuid_.uuid16 = uuid;
return ret;
}
ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) {
ESPBTUUID ret;
ret.type_ = Type::UUID32;
ret.uuid_.uuid32 = uuid;
return ret;
}
ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
ESPBTUUID ret;
ret.type_ = Type::UUID128;
memcpy(ret.uuid_.uuid128, data, 16);
return ret;
}
ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
ESPBTUUID ret;
ret.type_ = Type::UUID128;
for (int i = 0; i < 16; i++)
ret.uuid_.uuid128[i] = data[15 - i];
return ret;
}
ESPBTUUID ESPBTUUID::from_raw(const char *data, size_t length) {
// Same text-parsing semantics as the historical esp32_ble::ESPBTUUID::from_raw.
ESPBTUUID ret;
if (length == 4) {
// 16-bit UUID as 4-character hex string
auto parsed = parse_hex<uint16_t>(data, length);
if (parsed.has_value()) {
ret.type_ = Type::UUID16;
ret.uuid_.uuid16 = parsed.value();
}
} else if (length == 8) {
// 32-bit UUID as 8-character hex string
auto parsed = parse_hex<uint32_t>(data, length);
if (parsed.has_value()) {
ret.type_ = Type::UUID32;
ret.uuid_.uuid32 = parsed.value();
}
} else if (length == 16) {
// 16 raw bytes (little-endian 128-bit UUID)
ret.type_ = Type::UUID128;
memcpy(ret.uuid_.uuid128, reinterpret_cast<const uint8_t *>(data), 16);
} else if (length == 36) {
// Dashed text form XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
ret.type_ = Type::UUID128;
int n = 0;
for (size_t i = 0; i < length; i += 2) {
if (data[i] == '-')
i++;
uint8_t msb = data[i];
uint8_t lsb = data[i + 1];
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F);
}
} else {
ESP_LOGE(TAG, "ERROR: UUID value not 4, 8, 16 or 36 bytes - %s", data);
}
return ret;
}
#ifdef USE_ESP32
ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) {
if (uuid.len == ESP_UUID_LEN_16)
return ESPBTUUID::from_uint16(uuid.uuid.uuid16);
if (uuid.len == ESP_UUID_LEN_32)
return ESPBTUUID::from_uint32(uuid.uuid.uuid32);
return ESPBTUUID::from_raw(uuid.uuid.uuid128);
}
esp_bt_uuid_t ESPBTUUID::get_uuid() const {
esp_bt_uuid_t ret;
switch (this->type_) {
case Type::UUID16:
ret.len = ESP_UUID_LEN_16;
ret.uuid.uuid16 = this->uuid_.uuid16;
break;
case Type::UUID32:
ret.len = ESP_UUID_LEN_32;
ret.uuid.uuid32 = this->uuid_.uuid32;
break;
default:
case Type::UUID128:
ret.len = ESP_UUID_LEN_128;
memcpy(ret.uuid.uuid128, this->uuid_.uuid128, ESP_UUID_LEN_128);
break;
}
return ret;
}
void ESPBTDevice::parse_scan_rst(const esp32_ble::BLEScanResult &scan_result) {
this->scan_result_ = &scan_result;
// BLEScanResult's bda is most-significant octet first; the neutral ingest
// takes the BLE controller (LSB-first) order, so reverse — address_uint64()/
// address_str() then produce exactly the historical esp32 values.
uint8_t mac_lsb_first[6];
for (uint8_t i = 0; i < 6; i++)
mac_lsb_first[i] = scan_result.bda[5 - i];
this->from_scan_result(mac_lsb_first, scan_result.rssi, scan_result.ble_addr_type, scan_result.ble_adv,
scan_result.adv_data_len + scan_result.scan_rsp_len);
}
#endif // USE_ESP32
ESPBTUUID ESPBTUUID::as_128bit() const {
if (this->type_ == Type::UUID128)
return *this;
uint8_t data[16];
this->to_128bit_(data);
return ESPBTUUID::from_raw(data);
}
bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const {
// Adjacent byte-pair search — identical semantics to esp32_ble::ESPBTUUID::contains.
switch (this->type_) {
case Type::UUID16:
return (this->uuid_.uuid16 >> 8) == data2 && (this->uuid_.uuid16 & 0xFF) == data1;
case Type::UUID32:
for (uint8_t i = 0; i < 3; i++) {
bool a = ((this->uuid_.uuid32 >> i * 8) & 0xFF) == data1;
bool b = ((this->uuid_.uuid32 >> (i + 1) * 8) & 0xFF) == data2;
if (a && b)
return true;
}
return false;
case Type::UUID128:
for (uint8_t i = 0; i < 15; i++) {
if (this->uuid_.uuid128[i] == data1 && this->uuid_.uuid128[i + 1] == data2)
return true;
}
return false;
}
return false;
}
const char *ESPBTUUID::to_str(char *buf) const {
// Identical output format to esp32_ble::ESPBTUUID::to_str.
char *pos = buf;
switch (this->type_) {
case Type::UUID16:
*pos++ = '0';
*pos++ = 'x';
*pos++ = format_hex_pretty_char(this->uuid_.uuid16 >> 12);
*pos++ = format_hex_pretty_char((this->uuid_.uuid16 >> 8) & 0x0F);
*pos++ = format_hex_pretty_char((this->uuid_.uuid16 >> 4) & 0x0F);
*pos++ = format_hex_pretty_char(this->uuid_.uuid16 & 0x0F);
*pos = 0; // NUL-terminate
return buf;
case Type::UUID32:
*pos++ = '0';
*pos++ = 'x';
for (int shift = 28; shift >= 0; shift -= 4)
*pos++ = format_hex_pretty_char((this->uuid_.uuid32 >> shift) & 0x0F);
*pos = 0; // NUL-terminate
return buf;
default:
case Type::UUID128:
// Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
for (int8_t i = 15; i >= 0; i--) {
uint8_t byte = this->uuid_.uuid128[i];
*pos++ = format_hex_pretty_char(byte >> 4);
*pos++ = format_hex_pretty_char(byte & 0x0F);
if (i == 12 || i == 10 || i == 8 || i == 6)
*pos++ = '-';
}
*pos = 0; // NUL-terminate
return buf;
}
}
void ESPBTUUID::to_128bit_(uint8_t out[16]) const {
// Bluetooth Base UUID 00000000-0000-1000-8000-00805F9B34FB (LSB-first), with the 16/32-bit
// value placed at bytes 12..; identical expansion to esp32_ble::ESPBTUUID::as_128bit().
static const uint8_t BASE[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
if (this->type_ == Type::UUID128) {
memcpy(out, this->uuid_.uuid128, 16);
return;
}
memcpy(out, BASE, 16);
const uint32_t value = (this->type_ == Type::UUID32) ? this->uuid_.uuid32 : this->uuid_.uuid16;
const size_t len = (this->type_ == Type::UUID32) ? 4 : 2;
for (size_t i = 0; i < len; i++)
out[12 + i] = (value >> (i * 8)) & 0xFF;
}
bool ESPBTUUID::operator==(const ESPBTUUID &other) const {
if (this->type_ == other.type_) {
switch (this->type_) {
case Type::UUID16:
return this->uuid_.uuid16 == other.uuid_.uuid16;
case Type::UUID32:
return this->uuid_.uuid32 == other.uuid_.uuid32;
case Type::UUID128:
return memcmp(this->uuid_.uuid128, other.uuid_.uuid128, 16) == 0;
}
return false;
}
// Different widths: expand both to the 128-bit Bluetooth Base UUID form and compare, so a
// configured 16/32-bit UUID matches the equivalent 128-bit advertisement (esp32 parity).
uint8_t a[16];
uint8_t b[16];
this->to_128bit_(a);
other.to_128bit_(b);
return memcmp(a, b, 16) == 0;
}
// ---------------------------------------------------------------------------
// ESPBLEiBeacon
// ---------------------------------------------------------------------------
ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(this->beacon_data_)); }
optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) {
// iBeacon manufacturer specific data (after company-ID bytes have been stripped):
// [0x02][0x15][16-byte UUID][2-byte major][2-byte minor][1-byte power] = exactly 23 bytes
// Parity with esp32_ble_tracker: gate on the Apple company ID and length only.
// (Checking the 0x02/0x15 sub-type prefix would be stricter, but is a behavior
// change; it belongs to a follow-up, not this refactor.)
if (!data.uuid.contains(0x4C, 0x00)) // Apple company ID 0x004C
return {};
if (data.data.size() != 23)
return {};
return ESPBLEiBeacon(data.data.data());
}
// ---------------------------------------------------------------------------
// ESPBTDevice
// ---------------------------------------------------------------------------
void ESPBTDevice::from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data,
uint16_t data_len) {
// Ingest is BLE controller order (LSB-first); store in printable (MSB-first)
// order so the raw address() accessor matches the historical esp32 layout.
for (uint8_t i = 0; i < 6; i++)
this->address_[i] = mac[5 - i];
this->address_type_ = addr_type;
this->rssi_ = rssi;
this->name_.clear();
this->service_uuids_.clear();
this->manufacturer_datas_.clear();
this->service_datas_.clear();
this->tx_powers_.clear();
this->appearance_.reset();
this->ad_flag_.reset();
this->parse_adv_(data, data_len);
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "Parse Result:");
const char *address_type;
switch (this->address_type_) {
case BLE_ADDR_TYPE_PUBLIC:
address_type = "PUBLIC";
break;
case BLE_ADDR_TYPE_RANDOM:
address_type = "RANDOM";
break;
case BLE_ADDR_TYPE_RPA_PUBLIC:
address_type = "RPA_PUBLIC";
break;
case BLE_ADDR_TYPE_RPA_RANDOM:
address_type = "RPA_RANDOM";
break;
default:
address_type = "UNKNOWN";
break;
}
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGVV(TAG, " Address: %s (%s)", this->address_str_to(addr_buf), address_type);
ESP_LOGVV(TAG, " RSSI: %d", this->rssi_);
ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str());
for (auto &it : this->tx_powers_) {
ESP_LOGVV(TAG, " TX Power: %d", it);
}
if (this->appearance_.has_value()) {
ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_);
}
if (this->ad_flag_.has_value()) {
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
}
char uuid_buf[UUID_STR_LEN];
for (auto &uuid : this->service_uuids_) {
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_str(uuid_buf));
}
char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)];
for (auto &mfg_data : this->manufacturer_datas_) {
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(mfg_data);
if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_str(uuid_buf));
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else {
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", mfg_data.uuid.to_str(uuid_buf),
format_hex_pretty_to(hex_buf, mfg_data.data.data(), mfg_data.data.size()));
}
}
for (auto &svc_data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");
ESP_LOGVV(TAG, " UUID: %s", svc_data.uuid.to_str(uuid_buf));
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, svc_data.data.data(), svc_data.data.size()));
}
ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty_to(hex_buf, data, data_len));
#endif // ESPHOME_LOG_HAS_VERY_VERBOSE
}
std::string ESPBTDevice::address_str() const {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->address_str_to(buf));
}
const char *ESPBTDevice::address_str_to(char *buf) const {
// address_ is stored in printable (MSB-first) order.
format_mac_addr_upper(this->address_, buf);
return buf;
}
uint64_t ESPBTDevice::address_uint64() const {
// address_ is MSB-first; byte 0 of the result is the LSB (esp32 semantics).
uint64_t addr = 0;
for (int i = 0; i < 6; i++)
addr |= static_cast<uint64_t>(this->address_[i]) << ((5 - i) * 8);
return addr;
}
bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
#ifdef USE_BLE_DEVICE_IRK
// Bluetooth Core 5.x "ah" function: hash = e(IRK, padding | prand)[low 24 bits].
// The resolvable private address is prand (top 3 bytes) | hash (bottom 3 bytes).
// Uses the portable software AES-128 shared with the CCM decryptor, so IRK
// matching behaves identically on every platform (volume is one block per
// advertisement from a matching RPA device — software AES is not a cost).
uint8_t ecb_plaintext[16] = {0};
uint8_t ecb_ciphertext[16];
const uint64_t addr64 = this->address_uint64();
ecb_plaintext[13] = (addr64 >> 40) & 0xff;
ecb_plaintext[14] = (addr64 >> 32) & 0xff;
ecb_plaintext[15] = (addr64 >> 24) & 0xff;
aes128_encrypt_block(irk, ecb_plaintext, ecb_ciphertext);
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
#else
// No sensor configured an irk: in this build; the AES core is compiled out.
(void) irk;
return false;
#endif
}
void ESPBTDevice::parse_adv_(const uint8_t *payload, uint16_t len) {
// BLE AD structure TLV: [length][type][value...]
// length includes the type byte.
uint16_t offset = 0;
while (offset < len) {
uint8_t ad_len = payload[offset++];
if (ad_len == 0)
continue; // possible zero-padded advertisement data (esp32_ble_tracker skips these too)
if (offset + ad_len > len)
break;
uint8_t ad_type = payload[offset];
const uint8_t *ad_data = &payload[offset + 1];
uint8_t ad_data_len = ad_len - 1;
offset += ad_len;
switch (ad_type) {
case 0x01: // Flags
if (ad_data_len >= 1)
this->ad_flag_ = ad_data[0];
break;
case 0x08: // Shortened Local Name
case 0x09: // Complete Local Name
// Keep the longest name seen — a merged adv + scan-response frame may carry both the
// shortened and the complete name, and the shortened form must never replace the
// complete one (same rule as esp32_ble_tracker's parse_adv_).
if (ad_data_len > this->name_.length())
this->name_.assign(reinterpret_cast<const char *>(ad_data), ad_data_len);
break;
case 0x0A: // TX Power Level
if (ad_data_len >= 1)
this->tx_powers_.push_back(static_cast<int8_t>(ad_data[0]));
break;
case 0x19: // Appearance
if (ad_data_len >= 2)
this->appearance_ = static_cast<uint16_t>(ad_data[0]) | (static_cast<uint16_t>(ad_data[1]) << 8);
break;
case 0x02: // Incomplete List of 16-bit Service UUIDs
case 0x03: // Complete List of 16-bit Service UUIDs
for (uint8_t i = 0; (i + 1) < ad_data_len; i += 2) {
uint16_t uuid = (static_cast<uint16_t>(ad_data[i + 1]) << 8) | ad_data[i];
this->service_uuids_.push_back(ESPBTUUID::from_uint16(uuid));
}
break;
case 0x04: // Incomplete List of 32-bit Service UUIDs
case 0x05: // Complete List of 32-bit Service UUIDs
for (uint8_t i = 0; (i + 3) < ad_data_len; i += 4) {
uint32_t uuid = (static_cast<uint32_t>(ad_data[i + 3]) << 24) |
(static_cast<uint32_t>(ad_data[i + 2]) << 16) | (static_cast<uint32_t>(ad_data[i + 1]) << 8) |
ad_data[i];
this->service_uuids_.push_back(ESPBTUUID::from_uint32(uuid));
}
break;
case 0x06: // Incomplete List of 128-bit Service UUIDs
case 0x07: // Complete List of 128-bit Service UUIDs
for (uint8_t i = 0; (i + 15) < ad_data_len; i += 16)
this->service_uuids_.push_back(ESPBTUUID::from_raw(&ad_data[i]));
break;
case 0xFF: // Manufacturer Specific Data
if (ad_data_len >= 2) {
uint16_t company_id = (static_cast<uint16_t>(ad_data[1]) << 8) | ad_data[0];
ServiceData sd;
sd.uuid = ESPBTUUID::from_uint16(company_id);
sd.data.assign(ad_data + 2, ad_data + ad_data_len);
this->manufacturer_datas_.push_back(std::move(sd));
}
break;
case 0x16: // Service Data — 16-bit UUID
if (ad_data_len >= 2) {
uint16_t uuid = (static_cast<uint16_t>(ad_data[1]) << 8) | ad_data[0];
ServiceData sd;
sd.uuid = ESPBTUUID::from_uint16(uuid);
sd.data.assign(ad_data + 2, ad_data + ad_data_len);
this->service_datas_.push_back(std::move(sd));
}
break;
case 0x20: // Service Data — 32-bit UUID
if (ad_data_len >= 4) {
uint32_t uuid = (static_cast<uint32_t>(ad_data[3]) << 24) | (static_cast<uint32_t>(ad_data[2]) << 16) |
(static_cast<uint32_t>(ad_data[1]) << 8) | ad_data[0];
ServiceData sd;
sd.uuid = ESPBTUUID::from_uint32(uuid);
sd.data.assign(ad_data + 4, ad_data + ad_data_len);
this->service_datas_.push_back(std::move(sd));
}
break;
case 0x21: // Service Data — 128-bit UUID
if (ad_data_len >= 16) {
ServiceData sd;
sd.uuid = ESPBTUUID::from_raw(ad_data);
sd.data.assign(ad_data + 16, ad_data + ad_data_len);
this->service_datas_.push_back(std::move(sd));
}
break;
default:
break;
}
}
}
} // namespace esphome::ble_device_base
@@ -0,0 +1,239 @@
// ble_device.h
//
// Platform-neutral BLE advertisement types — the generic base every BLE consumer
// (sensor components, bluetooth_proxy, automation triggers) builds against:
// ESPBTUUID / ServiceData / ESPBLEiBeacon / ESPBTDevice / ESPBTDeviceListener
//
// These types are owned here on EVERY platform, with no chip-SDK types in their
// public surface. Platform trackers produce them:
// - esp32_ble_tracker adapts ESP-IDF scan results into ESPBTDevice and
// re-exports these names (esp32 only) for backward compatibility;
// - the LibreTiny trackers (bk72xx / ln882h) feed from_scan_result() directly.
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <string>
#include <vector>
#if defined(__cpp_lib_span)
#include <span>
#endif
#ifdef USE_ESP32
// Historical esp32_ble API surface (below, under the same define) uses the
// ESP-IDF UUID/address/scan-result types directly; never referenced off-esp32.
#include "esphome/components/esp32_ble/ble_scan_result.h"
#include <esp_bt_defs.h>
#endif
namespace esphome::ble_device_base {
using adv_data_t = std::vector<uint8_t>;
// Bluetooth Core address types (spec values; matches ESP-IDF's esp_ble_addr_type_t).
static constexpr uint8_t BLE_ADDR_TYPE_PUBLIC = 0;
static constexpr uint8_t BLE_ADDR_TYPE_RANDOM = 1;
static constexpr uint8_t BLE_ADDR_TYPE_RPA_PUBLIC = 2;
static constexpr uint8_t BLE_ADDR_TYPE_RPA_RANDOM = 3;
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0"
static constexpr size_t UUID_STR_LEN = 37;
// ---------------------------------------------------------------------------
// ESPBTUUID — 16/32/128-bit Bluetooth UUID value type.
// API-compatible with the historical esp32_ble::ESPBTUUID; the esp_bt_uuid_t
// conversions live in esp32_ble (esp32-only adapters), not here.
// ---------------------------------------------------------------------------
class ESPBTUUID {
public:
ESPBTUUID() = default;
static ESPBTUUID from_uint16(uint16_t uuid);
static ESPBTUUID from_uint32(uint32_t uuid);
/// Construct from raw 16-byte little-endian UUID.
static ESPBTUUID from_raw(const uint8_t *data);
/// Construct from raw 16-byte big-endian UUID (reversed on store).
static ESPBTUUID from_raw_reversed(const uint8_t *data);
/// Parse from text: 4 hex chars (16-bit), 8 hex chars (32-bit), 16 raw bytes,
/// or the 36-char dashed UUID form. Same semantics as esp32_ble historically.
static ESPBTUUID from_raw(const char *data, size_t length);
static ESPBTUUID from_raw(const char *data) { return from_raw(data, strlen(data)); }
static ESPBTUUID from_raw(const std::string &data) { return from_raw(data.c_str(), data.length()); }
static ESPBTUUID from_raw(std::initializer_list<uint8_t> data) {
return from_raw(reinterpret_cast<const char *>(data.begin()), data.size());
}
#ifdef USE_ESP32
/// Source compatibility with the historical esp32_ble API (esp32 builds only).
static ESPBTUUID from_uuid(esp_bt_uuid_t uuid);
esp_bt_uuid_t get_uuid() const;
#endif
/// Expand to the 128-bit Bluetooth Base UUID form.
ESPBTUUID as_128bit() const;
/// True if the UUID value contains the adjacent byte pair (data1, data2).
bool contains(uint8_t data1, uint8_t data2) const;
bool operator==(const ESPBTUUID &other) const;
bool operator!=(const ESPBTUUID &other) const { return !(*this == other); }
/// Write "0xABCD" / "0xABCDEF01" / the dashed 128-bit form into buf
/// (>= UUID_STR_LEN bytes) and return buf.
const char *to_str(char *buf) const;
#if defined(__cpp_lib_span)
const char *to_str(std::span<char, UUID_STR_LEN> output) const { return this->to_str(output.data()); }
#endif
enum class Type : uint8_t { UUID16, UUID32, UUID128 };
Type type() const { return this->type_; }
uint16_t uuid16() const { return this->uuid_.uuid16; }
uint32_t uuid32() const { return this->uuid_.uuid32; }
const uint8_t *uuid128() const { return this->uuid_.uuid128; }
protected:
// Expand to the 128-bit Bluetooth Base UUID byte form (out is 16 bytes, little-endian).
void to_128bit_(uint8_t out[16]) const;
Type type_{Type::UUID16};
union {
uint16_t uuid16;
uint32_t uuid32;
uint8_t uuid128[16];
} uuid_{};
};
// ---------------------------------------------------------------------------
// ServiceData — UUID-tagged advertisement payload (0x16 / 0xFF AD types)
// ---------------------------------------------------------------------------
struct ServiceData {
ESPBTUUID uuid;
adv_data_t data;
};
// ---------------------------------------------------------------------------
// ESPBLEiBeacon
// ---------------------------------------------------------------------------
class ESPBLEiBeacon {
public:
ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); }
explicit ESPBLEiBeacon(const uint8_t *data);
static optional<ESPBLEiBeacon> from_manufacturer_data(const ServiceData &data);
uint16_t get_major() const { return byteswap(this->beacon_data_.major); }
uint16_t get_minor() const { return byteswap(this->beacon_data_.minor); }
int8_t get_signal_power() const { return this->beacon_data_.signal_power; }
ESPBTUUID get_uuid() const { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); }
protected:
struct PACKED BeaconData {
uint8_t sub_type;
uint8_t length;
uint8_t proximity_uuid[16];
uint16_t major;
uint16_t minor;
int8_t signal_power;
} beacon_data_;
};
// ---------------------------------------------------------------------------
// ESPBTDevice — parsed BLE advertisement
// ---------------------------------------------------------------------------
class ESPBTDevice {
public:
/// Populate from a raw scan result delivered by a BLE tracker backend.
/// mac is least-significant octet first (BLE controller convention).
void from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len);
// Alias the core constant so the two cannot drift apart.
static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = esphome::MAC_ADDRESS_PRETTY_BUFFER_SIZE;
/// Return MAC as "XX:XX:XX:XX:XX:XX" string.
std::string address_str() const;
/// Buffer overload: writes "XX:XX:XX:XX:XX:XX\0" into buf (>= 18 bytes), returns buf.
const char *address_str_to(char *buf) const;
#if defined(__cpp_lib_span)
const char *address_str_to(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) const {
return this->address_str_to(buf.data());
}
#endif
/// Return MAC as packed uint64 (byte 0 in LSB — matches esp32's address_uint64).
uint64_t address_uint64() const;
/// Raw MAC bytes in printable (MSB-first) order — matches the historical
/// esp32 layout (ESP-IDF bda order).
const uint8_t *address() const { return address_; }
#ifdef USE_ESP32
// Historical esp32 signature: consumers assign the result to esp_ble_addr_type_t.
esp_ble_addr_type_t get_address_type() const { return static_cast<esp_ble_addr_type_t>(this->address_type_); }
/// Historical esp32 ingest (esp32 builds only): parse an ESP-IDF scan result.
void parse_scan_rst(const esp32_ble::BLEScanResult &scan_result);
// Exposed through a function for use in lambdas
const esp32_ble::BLEScanResult &get_scan_result() const { return *scan_result_; }
#else
uint8_t get_address_type() const { return this->address_type_; }
#endif
int get_rssi() const { return rssi_; }
const std::string &get_name() const { return name_; }
const std::vector<ESPBTUUID> &get_service_uuids() const { return service_uuids_; }
const std::vector<ServiceData> &get_manufacturer_datas() const { return manufacturer_datas_; }
const std::vector<ServiceData> &get_service_datas() const { return service_datas_; }
const std::vector<int8_t> &get_tx_powers() const { return tx_powers_; }
const optional<uint16_t> &get_appearance() const { return appearance_; }
const optional<uint8_t> &get_ad_flag() const { return ad_flag_; }
/// Resolve a Resolvable Private Address against a 16-byte IRK (Bluetooth "ah"
/// function, AES-128). Uses the portable software AES shared with the CCM
/// decryptor; compiled only when a sensor configures irk: (request_irk_support).
bool resolve_irk(const uint8_t *irk) const;
optional<ESPBLEiBeacon> get_ibeacon() const {
for (const auto &it : this->manufacturer_datas_) {
auto res = ESPBLEiBeacon::from_manufacturer_data(it);
if (res.has_value())
return res;
}
return {};
}
protected:
void parse_adv_(const uint8_t *payload, uint16_t len);
uint8_t address_[6]{0};
uint8_t address_type_{0};
int rssi_{0};
std::string name_{};
std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{};
#ifdef USE_ESP32
const esp32_ble::BLEScanResult *scan_result_{nullptr};
#endif
std::vector<int8_t> tx_powers_{};
optional<uint16_t> appearance_{};
optional<uint8_t> ad_flag_{};
};
// ---------------------------------------------------------------------------
// ESPBTDeviceListener — base class for BLE consumers (sensors, proxy, triggers)
// ---------------------------------------------------------------------------
class ESPBTDeviceListener {
public:
virtual ~ESPBTDeviceListener() = default;
/// Called at the end of each scan duration period.
virtual void on_scan_end() {}
virtual bool parse_device(const ESPBTDevice &device) = 0;
};
} // namespace esphome::ble_device_base
@@ -0,0 +1,63 @@
// ble_hub.h
//
// BLEHub — the platform-neutral BLE tracker contract.
//
// Every BLE tracker component (esp32_ble_tracker, bk72xx_ble_tracker,
// ln882h_ble_tracker, future chips) implements this interface; every BLE
// consumer (sensor components, bluetooth_proxy) binds to it — in YAML via
// `cv.use_id(BLEHub)`, which resolves whichever tracker the config declares.
// Adding a new BLE chip therefore requires only a new tracker component that
// implements BLEHub: no consumer, registry, or base changes.
//
// Chip differences are expressed as data (HubCapabilities), never as
// platform conditionals in consumers.
#pragma once
#include "ble_device.h"
#include <cstdint>
#include <functional>
namespace esphome::ble_device_base {
/// Callback for raw advertisements (the bluetooth_proxy path).
/// mac[] is least-significant octet first (BLE controller convention);
/// the hub delivers on the ESPHome main loop.
using RawAdvertisementCallback =
std::function<void(const uint8_t *mac, int rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len)>;
/// What a tracker's controller/SDK can do — consumers branch on data, not #ifdefs.
struct HubCapabilities {
/// Controller can send scan requests (active scanning).
bool active_scan;
/// Controller (or tracker) delivers advertisement + scan response as one merged
/// frame. When false, consumers relying on scan-response fields (e.g. names)
/// may only see them where the receiver merges per address (Home Assistant does).
bool merges_scan_response;
/// GATT client connections are available (today: esp32 only, but a chip SDK
/// gaining GATT support only has to flip this bit).
bool gatt;
};
class BLEHub {
public:
virtual ~BLEHub() = default;
/// Register a parsed-advertisement consumer (BLE sensors, automation triggers).
virtual void register_listener(ESPBTDeviceListener *listener) = 0;
/// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time.
virtual void set_raw_advertisement_callback(RawAdvertisementCallback cb) = 0;
virtual HubCapabilities get_capabilities() const = 0;
/// Adapter MAC in printable (MSB-first) order, out[0] = MSB.
virtual void get_adapter_mac(uint8_t out[6]) = 0;
virtual bool scan_running() = 0;
/// True when the current/configured scan mode is active (scan requests sent).
virtual bool scan_active() = 0;
};
} // namespace esphome::ble_device_base
+10 -38
View File
@@ -2,11 +2,19 @@ from collections.abc import Callable, MutableMapping
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
import logging import logging
import re
from typing import Any from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
# bt_uuid validation lives in the platform-neutral ble_device_base; re-exported
# here for backward compatibility.
from esphome.components.ble_device_base import ( # noqa: F401 # pylint: disable=unused-import
BT_UUID16_FORMAT as bt_uuid16_format,
BT_UUID32_FORMAT as bt_uuid32_format,
BT_UUID128_FORMAT as bt_uuid128_format,
bt_uuid,
)
from esphome.components.const import CONF_USE_PSRAM from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import ( from esphome.components.esp32 import (
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
@@ -28,6 +36,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
from esphome.types import ConfigType from esphome.types import ConfigType
AUTO_LOAD = ["ble_device_base"] # ble_uuid.h builds on the neutral ESPBTUUID
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"
@@ -372,43 +381,6 @@ def _validate_key_sizes(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes)
bt_uuid16_format = "XXXX"
bt_uuid32_format = "XXXXXXXX"
bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
def bt_uuid(value):
in_value = cv.string_strict(value)
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 128 UUID format: '{in_value}'"
)
return value
raise cv.Invalid(
f"Bluetooth UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format"
)
def validate_variant(_): def validate_variant(_):
variant = get_esp32_variant() variant = get_esp32_variant()
if variant in NO_BLUETOOTH_VARIANTS: if variant in NO_BLUETOOTH_VARIANTS:
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "ble_uuid.h"
#include <array> #include <array>
#include <functional> #include <functional>
@@ -15,8 +16,6 @@
namespace esphome::esp32_ble { namespace esphome::esp32_ble {
class ESPBTUUID;
class BLEAdvertising { class BLEAdvertising {
public: public:
BLEAdvertising(uint32_t advertising_cycle_time); BLEAdvertising(uint32_t advertising_cycle_time);
-187
View File
@@ -1,187 +0,0 @@
#include "ble_uuid.h"
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <cstring>
#include <cstdio>
#include <cinttypes>
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::esp32_ble {
static const char *const TAG = "esp32_ble";
ESPBTUUID::ESPBTUUID() : uuid_() {}
ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) {
ESPBTUUID ret;
ret.uuid_.len = ESP_UUID_LEN_16;
ret.uuid_.uuid.uuid16 = uuid;
return ret;
}
ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) {
ESPBTUUID ret;
ret.uuid_.len = ESP_UUID_LEN_32;
ret.uuid_.uuid.uuid32 = uuid;
return ret;
}
ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
ESPBTUUID ret;
ret.uuid_.len = ESP_UUID_LEN_128;
memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128);
return ret;
}
ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
ESPBTUUID ret;
ret.uuid_.len = ESP_UUID_LEN_128;
for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++)
ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i];
return ret;
}
ESPBTUUID ESPBTUUID::from_raw(const char *data, size_t length) {
ESPBTUUID ret;
if (length == 4) {
// 16-bit UUID as 4-character hex string
auto parsed = parse_hex<uint16_t>(data, length);
if (parsed.has_value()) {
ret.uuid_.len = ESP_UUID_LEN_16;
ret.uuid_.uuid.uuid16 = parsed.value();
}
} else if (length == 8) {
// 32-bit UUID as 8-character hex string
auto parsed = parse_hex<uint32_t>(data, length);
if (parsed.has_value()) {
ret.uuid_.len = ESP_UUID_LEN_32;
ret.uuid_.uuid.uuid32 = parsed.value();
}
} else if (length == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
// investigated (lack of time)
ret.uuid_.len = ESP_UUID_LEN_128;
memcpy(ret.uuid_.uuid.uuid128, reinterpret_cast<const uint8_t *>(data), 16);
} else if (length == 36) {
// If the length of the string is 36 bytes then we will assume it is a long hex string in
// UUID format.
ret.uuid_.len = ESP_UUID_LEN_128;
int n = 0;
for (size_t i = 0; i < length; i += 2) {
if (data[i] == '-')
i++;
uint8_t msb = data[i];
uint8_t lsb = data[i + 1];
if (msb > '9')
msb -= 7;
if (lsb > '9')
lsb -= 7;
ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F);
}
} else {
ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data);
}
return ret;
}
ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) {
ESPBTUUID ret;
ret.uuid_.len = uuid.len;
if (uuid.len == ESP_UUID_LEN_16) {
ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
} else if (uuid.len == ESP_UUID_LEN_32) {
ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
} else if (uuid.len == ESP_UUID_LEN_128) {
memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128);
}
return ret;
}
ESPBTUUID ESPBTUUID::as_128bit() const {
if (this->uuid_.len == ESP_UUID_LEN_128) {
return *this;
}
uint8_t data[] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint32_t uuid32;
if (this->uuid_.len == ESP_UUID_LEN_32) {
uuid32 = this->uuid_.uuid.uuid32;
} else {
uuid32 = this->uuid_.uuid.uuid16;
}
for (uint16_t i = 0; i < this->uuid_.len; i++) {
data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
}
return ESPBTUUID::from_raw(data);
}
bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const {
if (this->uuid_.len == ESP_UUID_LEN_16) {
return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1;
} else if (this->uuid_.len == ESP_UUID_LEN_32) {
for (uint8_t i = 0; i < 3; i++) {
bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1;
bool b = ((this->uuid_.uuid.uuid32 >> (i + 1) * 8) & 0xFF) == data2;
if (a && b)
return true;
}
} else {
for (uint8_t i = 0; i < 15; i++) {
if (this->uuid_.uuid.uuid128[i] == data1 && this->uuid_.uuid.uuid128[i + 1] == data2)
return true;
}
}
return false;
}
bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
if (this->uuid_.len == uuid.uuid_.len) {
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16;
case ESP_UUID_LEN_32:
return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32;
case ESP_UUID_LEN_128:
return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0;
default:
return false;
}
}
return this->as_128bit() == uuid.as_128bit();
}
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
const char *ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
char *pos = output.data();
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
*pos++ = '0';
*pos++ = 'x';
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 >> 12);
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 8) & 0x0F);
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
*pos = '\0';
return output.data();
case ESP_UUID_LEN_32:
*pos++ = '0';
*pos++ = 'x';
for (int shift = 28; shift >= 0; shift -= 4) {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
}
*pos = '\0';
return output.data();
default:
case ESP_UUID_LEN_128:
// Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
for (int8_t i = 15; i >= 0; i--) {
uint8_t byte = this->uuid_.uuid.uuid128[i];
*pos++ = format_hex_pretty_char(byte >> 4);
*pos++ = format_hex_pretty_char(byte & 0x0F);
if (i == 12 || i == 10 || i == 8 || i == 6) {
*pos++ = '-';
}
}
*pos = '\0';
return output.data();
}
}
} // namespace esphome::esp32_ble
#endif // USE_ESP32_BLE_UUID
#endif // USE_ESP32
+8 -43
View File
@@ -1,56 +1,21 @@
#pragma once #pragma once
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID #ifdef USE_ESP32_BLE_UUID
#include <initializer_list> // The BLE UUID type is owned by the platform-neutral ble_device_base layer;
#include <span> // this header re-exports it under the historical esp32_ble name (esp32 only).
#include <string> // The full historical API surface — including from_uuid()/get_uuid() with the
#include <esp_bt_defs.h> // ESP-IDF esp_bt_uuid_t type — is preserved on esp32 builds.
#include "esphome/components/ble_device_base/ble_device.h"
namespace esphome::esp32_ble { namespace esphome::esp32_ble {
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0" using ble_device_base::UUID_STR_LEN;
static constexpr size_t UUID_STR_LEN = 37; using ESPBTUUID = ble_device_base::ESPBTUUID;
class ESPBTUUID {
public:
ESPBTUUID();
static ESPBTUUID from_uint16(uint16_t uuid);
static ESPBTUUID from_uint32(uint32_t uuid);
static ESPBTUUID from_raw(const uint8_t *data);
static ESPBTUUID from_raw_reversed(const uint8_t *data);
static ESPBTUUID from_raw(const char *data, size_t length);
static ESPBTUUID from_raw(const char *data) { return from_raw(data, strlen(data)); }
static ESPBTUUID from_raw(const std::string &data) { return from_raw(data.c_str(), data.length()); }
static ESPBTUUID from_raw(std::initializer_list<uint8_t> data) {
return from_raw(reinterpret_cast<const char *>(data.begin()), data.size());
}
static ESPBTUUID from_uuid(esp_bt_uuid_t uuid);
ESPBTUUID as_128bit() const;
bool contains(uint8_t data1, uint8_t data2) const;
bool operator==(const ESPBTUUID &uuid) const;
bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); }
esp_bt_uuid_t get_uuid() const;
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
protected:
esp_bt_uuid_t uuid_;
};
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble
@@ -5,7 +5,7 @@ import logging
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble, ota from esphome.components import ble_device_base, esp32_ble, ota
from esphome.components.esp32 import ( from esphome.components.esp32 import (
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
request_bluetooth, request_bluetooth,
@@ -39,7 +39,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.enum import StrEnum from esphome.enum import StrEnum
from esphome.types import ConfigType from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["ble_device_base", "esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"] CODEOWNERS = ["@bdraco"]
@@ -93,6 +93,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None:
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
ESP32BLETracker = esp32_ble_tracker_ns.class_( ESP32BLETracker = esp32_ble_tracker_ns.class_(
"ESP32BLETracker", "ESP32BLETracker",
ble_device_base.BLEHub,
cg.Component, cg.Component,
cg.Parented.template(esp32_ble.ESP32BLE), cg.Parented.template(esp32_ble.ESP32BLE),
) )
@@ -153,26 +154,11 @@ def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
return config return config
def as_hex(value): # Codegen helpers are owned by ble_device_base; kept under the historical names
return cg.RawExpression(f"0x{value}ULL") # here for the components that import them from this module.
as_hex = ble_device_base.as_hex
as_hex_array = ble_device_base.as_hex_array
def as_hex_array(value): as_reversed_hex_array = ble_device_base.as_reversed_hex_array
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
]
return cg.RawExpression(f"(uint8_t*)(const uint8_t[16]){{{','.join(cpp_array)}}}")
def as_reversed_hex_array(value):
value = value.replace("-", "")
cpp_array = [
f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
]
return cg.RawExpression(
f"(uint8_t*)(const uint8_t[16]){{{','.join(reversed(cpp_array))}}}"
)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
@@ -254,6 +240,10 @@ async def to_code(config):
# Register the loggers this component needs # Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.BLE_SCAN) esp32_ble.register_bt_logger(BTLoggers.BLE_SCAN)
# Behavior parity with the pre-split tracker: IRK resolution is always
# available on esp32 (sensors with irk: worked without opting in).
ble_device_base.request_irk_support()
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -346,6 +336,14 @@ async def to_code(config):
async def _add_ble_features(): async def _add_ble_features():
# Add feature-specific defines based on what's needed # Add feature-specific defines based on what's needed
required_features = _get_required_features() required_features = _get_required_features()
# Sensors registered through the neutral ble_device_base path (BLEHub) need
# the parsed-device pipeline compiled in, exactly like esp32-path listeners.
neutral_listener_count = ble_device_base.get_listener_count()
if neutral_listener_count > 0:
required_features.add(BLEFeatures.ESP_BT_DEVICE)
# StaticVector sizing for the neutral (BLEHub) listener list — same
# pattern as the esp32-path registration counts below.
cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", neutral_listener_count)
if BLEFeatures.ESP_BT_DEVICE in required_features: if BLEFeatures.ESP_BT_DEVICE in required_features:
cg.add_define("USE_ESP32_BLE_DEVICE") cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID") cg.add_define("USE_ESP32_BLE_UUID")
@@ -27,15 +27,6 @@
#include <esp_coexist.h> #include <esp_coexist.h>
#endif #endif
#ifdef USE_ESP32_BLE_DEVICE
#ifdef USE_BLE_TRACKER_PSA_AES
#include <psa/crypto.h>
#else
#define MBEDTLS_AES_ALT
#include <aes_alt.h>
#endif
#endif // USE_ESP32_BLE_DEVICE
// bt_trace.h // bt_trace.h
#undef TAG #undef TAG
@@ -43,9 +34,6 @@ namespace esphome::esp32_ble_tracker {
static const char *const TAG = "esp32_ble_tracker"; static const char *const TAG = "esp32_ble_tracker";
// BLE advertisement max: 31 bytes adv data + 31 bytes scan response
static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62;
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
const char *client_state_to_string(ClientState state) { const char *client_state_to_string(ClientState state) {
@@ -263,6 +251,10 @@ void ESP32BLETracker::start_scan_(bool first) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
#endif
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
for (auto *listener : this->neutral_listeners_)
listener->on_scan_end();
#endif #endif
} }
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
@@ -304,6 +296,21 @@ void ESP32BLETracker::register_client(ESPBTClient *client) {
#endif #endif
} }
void ESP32BLETracker::register_listener(ble_device_base::ESPBTDeviceListener *listener) {
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
// Neutral BLEHub path (migrated sensors): parsed-advertisement consumers only.
this->neutral_listeners_.push_back(listener);
this->parse_advertisements_ = true;
#endif
}
void ESP32BLETracker::get_adapter_mac(uint8_t out[6]) {
get_mac_address_raw(out); // WiFi base MAC, MSB-first
// BT MAC = base MAC + 2 on the last octet only, wrapping without carry —
// exactly ESP-IDF's esp_read_mac(ESP_MAC_BT): mac[5] += MAC_ADDR_UNIVERSE_BT_OFFSET.
out[5] += 2;
}
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) { void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
listener->set_parent(this); listener->set_parent(this);
@@ -315,6 +322,13 @@ void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
void ESP32BLETracker::recalculate_advertisement_parser_types() { void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = false; this->raw_advertisements_ = false;
this->parse_advertisements_ = false; this->parse_advertisements_ = false;
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
// Neutral (BLEHub) listeners are parsed-advertisement consumers and are not in
// listeners_; without this, any later esp32-path registration (e.g. the proxy's
// GATT clients) would recompute the flags and silently drop parsed dispatch.
if (!this->neutral_listeners_.empty())
this->parse_advertisements_ = true;
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) { if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
@@ -434,270 +448,6 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) {
} }
} }
#ifdef USE_ESP32_BLE_DEVICE
ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); }
optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) {
if (!data.uuid.contains(0x4C, 0x00))
return {};
if (data.data.size() != 23)
return {};
return ESPBLEiBeacon(data.data.data());
}
void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
this->scan_result_ = &scan_result;
for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
this->address_[i] = scan_result.bda[i];
this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
this->rssi_ = scan_result.rssi;
// Parse advertisement data directly
uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len;
this->parse_adv_(scan_result.ble_adv, total_len);
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "Parse Result:");
const char *address_type;
switch (this->address_type_) {
case BLE_ADDR_TYPE_PUBLIC:
address_type = "PUBLIC";
break;
case BLE_ADDR_TYPE_RANDOM:
address_type = "RANDOM";
break;
case BLE_ADDR_TYPE_RPA_PUBLIC:
address_type = "RPA_PUBLIC";
break;
case BLE_ADDR_TYPE_RPA_RANDOM:
address_type = "RPA_RANDOM";
break;
default:
address_type = "UNKNOWN";
break;
}
ESP_LOGVV(TAG, " Address: %02X:%02X:%02X:%02X:%02X:%02X (%s)", this->address_[0], this->address_[1],
this->address_[2], this->address_[3], this->address_[4], this->address_[5], address_type);
ESP_LOGVV(TAG, " RSSI: %d", this->rssi_);
ESP_LOGVV(TAG, " Name: '%s'", this->name_.c_str());
for (auto &it : this->tx_powers_) {
ESP_LOGVV(TAG, " TX Power: %d", it);
}
if (this->appearance_.has_value()) {
ESP_LOGVV(TAG, " Appearance: %u", *this->appearance_);
}
if (this->ad_flag_.has_value()) {
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
}
for (auto &uuid : this->service_uuids_) {
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
}
char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)];
for (auto &data : this->manufacturer_datas_) {
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
char uuid_buf[esp32_ble::UUID_STR_LEN];
ibeacon.value().get_uuid().to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else {
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf,
format_hex_pretty_to(hex_buf, data.data.data(), data.data.size()));
}
}
for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size()));
}
ESP_LOGVV(TAG, " Adv data: %s",
format_hex_pretty_to(hex_buf, scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len));
#endif
}
void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
size_t offset = 0;
while (offset + 2 < len) {
const uint8_t field_length = payload[offset++]; // First byte is length of adv record
if (field_length == 0) {
continue; // Possible zero padded advertisement data
}
// Validate field fits in remaining payload
if (offset + field_length > len) {
break;
}
// first byte of adv record is adv record type
const uint8_t record_type = payload[offset++];
const uint8_t *record = &payload[offset];
const uint8_t record_length = field_length - 1;
offset += record_length;
// See also Generic Access Profile Assigned Numbers:
// https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ See also ADVERTISING AND SCAN
// RESPONSE DATA FORMAT: https://www.bluetooth.com/specifications/bluetooth-core-specification/ (vol 3, part C, 11)
// See also Core Specification Supplement: https://www.bluetooth.com/specifications/bluetooth-core-specification/
// (called CSS here)
switch (record_type) {
case ESP_BLE_AD_TYPE_NAME_SHORT:
case ESP_BLE_AD_TYPE_NAME_CMPL: {
// CSS 1.2 LOCAL NAME
// "The Local Name data type shall be the same as, or a shortened version of, the local name assigned to the
// device." CSS 1: Optional in this context; shall not appear more than once in a block.
// SHORTENED LOCAL NAME
// "The Shortened Local Name data type defines a shortened version of the Local Name data type. The Shortened
// Local Name data type shall not be used to advertise a name that is longer than the Local Name data type."
if (record_length > this->name_.length()) {
this->name_ = std::string(reinterpret_cast<const char *>(record), record_length);
}
break;
}
case ESP_BLE_AD_TYPE_TX_PWR: {
// CSS 1.5 TX POWER LEVEL
// "The TX Power Level data type indicates the transmitted power level of the packet containing the data type."
// CSS 1: Optional in this context (may appear more than once in a block).
this->tx_powers_.push_back(*record);
break;
}
case ESP_BLE_AD_TYPE_APPEARANCE: {
// CSS 1.12 APPEARANCE
// "The Appearance data type defines the external appearance of the device."
// See also https://www.bluetooth.com/specifications/gatt/characteristics/
// CSS 1: Optional in this context; shall not appear more than once in a block and shall not appear in both
// the AD and SRD of the same extended advertising interval.
this->appearance_ = *reinterpret_cast<const uint16_t *>(record);
break;
}
case ESP_BLE_AD_TYPE_FLAG: {
// CSS 1.3 FLAGS
// "The Flags data type contains one bit Boolean flags. The Flags data type shall be included when any of the
// Flag bits are non-zero and the advertising packet is connectable, otherwise the Flags data type may be
// omitted."
// CSS 1: Optional in this context; shall not appear more than once in a block.
this->ad_flag_ = *record;
break;
}
// CSS 1.1 SERVICE UUID
// The Service UUID data type is used to include a list of Service or Service Class UUIDs.
// There are six data types defined for the three sizes of Service UUIDs that may be returned:
// CSS 1: Optional in this context (may appear more than once in a block).
case ESP_BLE_AD_TYPE_16SRV_CMPL:
case ESP_BLE_AD_TYPE_16SRV_PART: {
// • 16-bit Bluetooth Service UUIDs
for (uint8_t i = 0; i < record_length / 2; i++) {
this->service_uuids_.push_back(ESPBTUUID::from_uint16(*reinterpret_cast<const uint16_t *>(record + 2 * i)));
}
break;
}
case ESP_BLE_AD_TYPE_32SRV_CMPL:
case ESP_BLE_AD_TYPE_32SRV_PART: {
// • 32-bit Bluetooth Service UUIDs
for (uint8_t i = 0; i < record_length / 4; i++) {
this->service_uuids_.push_back(ESPBTUUID::from_uint32(*reinterpret_cast<const uint32_t *>(record + 4 * i)));
}
break;
}
case ESP_BLE_AD_TYPE_128SRV_CMPL:
case ESP_BLE_AD_TYPE_128SRV_PART: {
// • Global 128-bit Service UUIDs
this->service_uuids_.push_back(ESPBTUUID::from_raw(record));
break;
}
case ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE: {
// CSS 1.4 MANUFACTURER SPECIFIC DATA
// "The Manufacturer Specific data type is used for manufacturer specific data. The first two data octets shall
// contain a company identifier from Assigned Numbers. The interpretation of any other octets within the data
// shall be defined by the manufacturer specified by the company identifier."
// CSS 1: Optional in this context (may appear more than once in a block).
if (record_length < 2) {
ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE");
break;
}
ServiceData data{};
data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast<const uint16_t *>(record));
data.data.assign(record + 2UL, record + record_length);
this->manufacturer_datas_.push_back(data);
break;
}
// CSS 1.11 SERVICE DATA
// "The Service Data data type consists of a service UUID with the data associated with that service."
// CSS 1: Optional in this context (may appear more than once in a block).
case ESP_BLE_AD_TYPE_SERVICE_DATA: {
// «Service Data - 16 bit UUID»
// Size: 2 or more octets
// The first 2 octets contain the 16 bit Service UUID fol- lowed by additional service data
if (record_length < 2) {
ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_SERVICE_DATA");
break;
}
ServiceData data{};
data.uuid = ESPBTUUID::from_uint16(*reinterpret_cast<const uint16_t *>(record));
data.data.assign(record + 2UL, record + record_length);
this->service_datas_.push_back(data);
break;
}
case ESP_BLE_AD_TYPE_32SERVICE_DATA: {
// «Service Data - 32 bit UUID»
// Size: 4 or more octets
// The first 4 octets contain the 32 bit Service UUID fol- lowed by additional service data
if (record_length < 4) {
ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_32SERVICE_DATA");
break;
}
ServiceData data{};
data.uuid = ESPBTUUID::from_uint32(*reinterpret_cast<const uint32_t *>(record));
data.data.assign(record + 4UL, record + record_length);
this->service_datas_.push_back(data);
break;
}
case ESP_BLE_AD_TYPE_128SERVICE_DATA: {
// «Service Data - 128 bit UUID»
// Size: 16 or more octets
// The first 16 octets contain the 128 bit Service UUID followed by additional service data
if (record_length < 16) {
ESP_LOGV(TAG, "Record length too small for ESP_BLE_AD_TYPE_128SERVICE_DATA");
break;
}
ServiceData data{};
data.uuid = ESPBTUUID::from_raw(record);
data.data.assign(record + 16UL, record + record_length);
this->service_datas_.push_back(data);
break;
}
case ESP_BLE_AD_TYPE_INT_RANGE:
// Avoid logging this as it's very verbose
break;
default: {
ESP_LOGV(TAG, "Unhandled type: advType: 0x%02x", record_type);
break;
}
}
}
}
std::string ESPBTDevice::address_str() const {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return this->address_str_to(buf);
}
uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); }
#endif // USE_ESP32_BLE_DEVICE
void ESP32BLETracker::dump_config() { void ESP32BLETracker::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Tracker:"); ESP_LOGCONFIG(TAG, "BLE Tracker:");
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
@@ -759,64 +509,7 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
} }
} }
bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { // resolve_irk() is provided by ble_device_base (portable software AES).
static constexpr size_t AES_BLOCK_SIZE = 16;
static constexpr size_t AES_KEY_BITS = 128;
uint8_t ecb_key[AES_BLOCK_SIZE];
uint8_t ecb_plaintext[AES_BLOCK_SIZE];
uint8_t ecb_ciphertext[AES_BLOCK_SIZE];
uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_);
memcpy(&ecb_key, irk, AES_BLOCK_SIZE);
memset(&ecb_plaintext, 0, AES_BLOCK_SIZE);
ecb_plaintext[13] = (addr64 >> 40) & 0xff;
ecb_plaintext[14] = (addr64 >> 32) & 0xff;
ecb_plaintext[15] = (addr64 >> 24) & 0xff;
#ifdef USE_BLE_TRACKER_PSA_AES
// Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+)
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attributes, AES_KEY_BITS);
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT);
psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING);
mbedtls_svc_key_id_t key_id;
if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) {
return false;
}
size_t output_length;
psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE,
ecb_ciphertext, AES_BLOCK_SIZE, &output_length);
psa_destroy_key(key_id);
if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) {
return false;
}
#else
// Use legacy mbedtls AES API (IDF < 6.0)
mbedtls_aes_context ctx = {0, 0, {0}};
mbedtls_aes_init(&ctx);
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) {
mbedtls_aes_free(&ctx);
return false;
}
if (mbedtls_aes_crypt_ecb(&ctx, ESP_AES_ENCRYPT, ecb_plaintext, ecb_ciphertext) != 0) {
mbedtls_aes_free(&ctx);
return false;
}
mbedtls_aes_free(&ctx);
#endif
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
}
#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32_BLE_DEVICE
@@ -848,6 +541,12 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
found = true; found = true;
} }
#endif #endif
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
for (auto *listener : this->neutral_listeners_) {
if (listener->parse_device(device))
found = true;
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
@@ -876,6 +575,10 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
#endif #endif
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
for (auto *listener : this->neutral_listeners_)
listener->on_scan_end();
#endif
this->set_scanner_state_(ScannerState::IDLE); this->set_scanner_state_(ScannerState::IDLE);
} }
@@ -12,13 +12,6 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_idf_version.h>
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API.
// Use the PSA Crypto API instead.
#define USE_BLE_TRACKER_PSA_AES
#endif
#include <esp_bt_defs.h> #include <esp_bt_defs.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
@@ -26,6 +19,8 @@
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include "esphome/components/ble_device_base/ble_device.h"
#include "esphome/components/ble_device_base/ble_hub.h"
#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/ble_scan_result.h" #include "esphome/components/esp32_ble/ble_scan_result.h"
@@ -38,7 +33,7 @@ namespace esphome::esp32_ble_tracker {
using namespace esp32_ble; using namespace esp32_ble;
using adv_data_t = std::vector<uint8_t>; using adv_data_t = ble_device_base::adv_data_t;
enum AdvertisementParserType { enum AdvertisementParserType {
PARSED_ADVERTISEMENTS, PARSED_ADVERTISEMENTS,
@@ -46,105 +41,27 @@ enum AdvertisementParserType {
}; };
#ifdef USE_ESP32_BLE_UUID #ifdef USE_ESP32_BLE_UUID
struct ServiceData { using ServiceData = ble_device_base::ServiceData;
ESPBTUUID uuid;
adv_data_t data;
};
#endif #endif
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
class ESPBLEiBeacon { // The advertisement device types are owned by the platform-neutral
public: // ble_device_base layer; re-exported here (esp32 only) for backward
ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } // compatibility. ESPBTDevice::parse_scan_rst() (esp32-only) adapts BLEScanResult.
ESPBLEiBeacon(const uint8_t *data); using ESPBLEiBeacon = ble_device_base::ESPBLEiBeacon;
static optional<ESPBLEiBeacon> from_manufacturer_data(const ServiceData &data); using ESPBTDevice = ble_device_base::ESPBTDevice;
uint16_t get_major() { return byteswap(this->beacon_data_.major); }
uint16_t get_minor() { return byteswap(this->beacon_data_.minor); }
int8_t get_signal_power() { return this->beacon_data_.signal_power; }
ESPBTUUID get_uuid() { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); }
protected:
struct {
uint8_t sub_type;
uint8_t length;
uint8_t proximity_uuid[16];
uint16_t major;
uint16_t minor;
int8_t signal_power;
} PACKED beacon_data_;
};
class ESPBTDevice {
public:
void parse_scan_rst(const BLEScanResult &scan_result);
std::string address_str() const;
/// Format MAC address into provided buffer, returns pointer to buffer for convenience
const char *address_str_to(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) const {
format_mac_addr_upper(this->address_, buf.data());
return buf.data();
}
uint64_t address_uint64() const;
const uint8_t *address() const { return address_; }
esp_ble_addr_type_t get_address_type() const { return this->address_type_; }
int get_rssi() const { return rssi_; }
const std::string &get_name() const { return this->name_; }
const std::vector<int8_t> &get_tx_powers() const { return tx_powers_; }
const optional<uint16_t> &get_appearance() const { return appearance_; }
const optional<uint8_t> &get_ad_flag() const { return ad_flag_; }
const std::vector<ESPBTUUID> &get_service_uuids() const { return service_uuids_; }
const std::vector<ServiceData> &get_manufacturer_datas() const { return manufacturer_datas_; }
const std::vector<ServiceData> &get_service_datas() const { return service_datas_; }
// Exposed through a function for use in lambdas
const BLEScanResult &get_scan_result() const { return *scan_result_; }
bool resolve_irk(const uint8_t *irk) const;
optional<ESPBLEiBeacon> get_ibeacon() const {
for (auto &it : this->manufacturer_datas_) {
auto res = ESPBLEiBeacon::from_manufacturer_data(it);
if (res.has_value())
return res;
}
return {};
}
protected:
void parse_adv_(const uint8_t *payload, uint8_t len);
esp_bd_addr_t address_{
0,
};
esp_ble_addr_type_t address_type_{BLE_ADDR_TYPE_PUBLIC};
int rssi_{0};
std::string name_{};
std::vector<int8_t> tx_powers_{};
optional<uint16_t> appearance_{};
optional<uint8_t> ad_flag_{};
std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{};
const BLEScanResult *scan_result_{nullptr};
};
#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32_BLE_DEVICE
class ESP32BLETracker; class ESP32BLETracker;
class ESPBTDeviceListener { // esp32-flavored listener: the neutral parse_device/on_scan_end come from
// ble_device_base; this subclass adds the esp32-only raw-advertisement path
// (BLEScanResult batches) and the tracker back-pointer.
class ESPBTDeviceListener : public ble_device_base::ESPBTDeviceListener {
public: public:
virtual void on_scan_end() {} #ifndef USE_ESP32_BLE_DEVICE
#ifdef USE_ESP32_BLE_DEVICE // Raw-only build: no parsed-device support is compiled in.
virtual bool parse_device(const ESPBTDevice &device) = 0; bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
#endif #endif
virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
virtual AdvertisementParserType get_advertisement_parser_type() { virtual AdvertisementParserType get_advertisement_parser_type() {
@@ -295,6 +212,7 @@ class ESPBTClient : public ESPBTDeviceListener {
}; };
class ESP32BLETracker final : public Component, class ESP32BLETracker final : public Component,
public ble_device_base::BLEHub,
#ifdef USE_OTA_STATE_LISTENER #ifdef USE_OTA_STATE_LISTENER
public ota::OTAGlobalStateListener, public ota::OTAGlobalStateListener,
#endif #endif
@@ -314,10 +232,23 @@ class ESP32BLETracker final : public Component,
void loop() override; void loop() override;
// esp32-flavored path (unmigrated esp32 sensors; sets the tracker back-pointer).
void register_listener(ESPBTDeviceListener *listener); void register_listener(ESPBTDeviceListener *listener);
void register_client(ESPBTClient *client); void register_client(ESPBTClient *client);
void recalculate_advertisement_parser_types(); void recalculate_advertisement_parser_types();
// ---- ble_device_base::BLEHub (the platform-neutral tracker contract) ----
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override;
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback cb) override {
this->raw_advertisement_callback_ = std::move(cb);
}
ble_device_base::HubCapabilities get_capabilities() const override {
return {/* active_scan = */ true, /* merges_scan_response = */ true, /* gatt = */ true};
}
void get_adapter_mac(uint8_t out[6]) override;
bool scan_running() override { return this->scanner_state_ == ScannerState::RUNNING; }
bool scan_active() override { return this->scan_active_; }
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
void print_bt_device_info(const ESPBTDevice &device); void print_bt_device_info(const ESPBTDevice &device);
#endif #endif
@@ -405,6 +336,12 @@ class ESP32BLETracker final : public Component,
StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_; StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
#endif #endif
std::vector<BLEScannerStateListener *> scanner_state_listeners_; std::vector<BLEScannerStateListener *> scanner_state_listeners_;
// Parsed listeners registered through the neutral BLEHub contract (migrated
// sensors); dispatched alongside listeners_.
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
StaticVector<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> neutral_listeners_;
#endif
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{nullptr};
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
/// Vector of addresses that have already been printed in print_bt_device_info /// Vector of addresses that have already been printed in print_bt_device_info
std::vector<uint64_t> already_discovered_; std::vector<uint64_t> already_discovered_;
+2
View File
@@ -44,6 +44,7 @@
#define USE_AREAS #define USE_AREAS
#define USE_BINARY_SENSOR #define USE_BINARY_SENSOR
#define USE_BINARY_SENSOR_FILTER #define USE_BINARY_SENSOR_FILTER
#define USE_BLE_DEVICE_IRK
#define USE_BUTTON #define USE_BUTTON
#define USE_CAMERA #define USE_CAMERA
#define USE_CLIMATE #define USE_CLIMATE
@@ -271,6 +272,7 @@
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT #define USE_ESP32_BLE_SERVER_ON_DISCONNECT
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
#define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2 #define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2
#define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1
#define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1
@@ -0,0 +1,12 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# resolve_irk() is compiled only when a sensor configures irk:
# (request_irk_support() emits USE_BLE_DEVICE_IRK). The unit-test build has
# no sensors, so emit the define here to put the real IRK path under test.
async def to_code_testing(config):
cg.add_define("USE_BLE_DEVICE_IRK")
manifest.to_code = to_code_testing
@@ -0,0 +1,31 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/ble_device_base/ble_device.h"
namespace esphome::ble_device_base::testing {
// from_scan_result() ingests BLE controller order (LSB-first); the public
// accessors must expose the historical esp32 semantics: address() in printable
// (MSB-first) order, address_uint64() with byte 0 in the LSB, address_str()
// printed MSB-first.
namespace {
// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first.
const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa};
} // namespace
TEST(BleDeviceAddress, AccessorsMatchEsp32Semantics) {
ESPBTDevice device;
device.from_scan_result(MAC_LSB_FIRST, -50, BLE_ADDR_TYPE_PUBLIC, nullptr, 0);
const uint8_t *raw = device.address();
EXPECT_EQ(raw[0], 0xaa); // MSB first, like ESP-IDF's bda
EXPECT_EQ(raw[5], 0xff);
EXPECT_EQ(device.address_uint64(), 0xAABBCCDDEEFFULL);
EXPECT_EQ(device.address_str(), "AA:BB:CC:DD:EE:FF");
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,56 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include "esphome/components/ble_device_base/ble_aes_ccm.h"
namespace esphome::ble_device_base::testing {
// Reference vector generated with Python `cryptography` AESCCM(tag_length=4),
// using the same AES-128-CCM parameters BTHome advertisements use: a 16-byte
// key, a 13-byte nonce, a 4-byte authentication tag and no associated data.
namespace {
const uint8_t KEY[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
const uint8_t NONCE[13] = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c};
const uint8_t CIPHERTEXT[7] = {0x68, 0xb4, 0xf6, 0xc5, 0x2b, 0xf8, 0xaf};
const uint8_t TAG[4] = {0x48, 0x4d, 0xaa, 0x56};
const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01};
} // namespace
TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) {
uint8_t out[sizeof(PLAINTEXT)] = {};
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
sizeof(TAG)));
EXPECT_EQ(0, memcmp(out, PLAINTEXT, sizeof(PLAINTEXT)));
}
TEST(BleAesCcm, RejectsTamperedTag) {
uint8_t bad_tag[sizeof(TAG)];
memcpy(bad_tag, TAG, sizeof(TAG));
bad_tag[0] ^= 0x01;
uint8_t out[sizeof(PLAINTEXT)] = {};
EXPECT_FALSE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, bad_tag,
sizeof(bad_tag)));
}
TEST(BleAesCcm, RejectsTamperedCiphertext) {
uint8_t bad_ct[sizeof(CIPHERTEXT)];
memcpy(bad_ct, CIPHERTEXT, sizeof(CIPHERTEXT));
bad_ct[0] ^= 0x01;
uint8_t out[sizeof(PLAINTEXT)] = {};
EXPECT_FALSE(
aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, bad_ct, sizeof(bad_ct), out, TAG, sizeof(TAG)));
}
TEST(BleAesCcm, RejectsWrongKey) {
uint8_t bad_key[sizeof(KEY)];
memcpy(bad_key, KEY, sizeof(KEY));
bad_key[0] ^= 0xFF;
uint8_t out[sizeof(PLAINTEXT)] = {};
EXPECT_FALSE(aes_ccm_auth_decrypt(bad_key, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
sizeof(TAG)));
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,41 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include "esphome/components/ble_device_base/ble_device.h"
namespace esphome::ble_device_base::testing {
// A 16- or 32-bit UUID must compare equal to its 128-bit Bluetooth Base UUID form, matching
// esp32_ble_tracker. The 128-bit raw is the base UUID (LSB-first) with the short value at
// bytes 12.. : here 0x1234 -> bytes [12]=0x34, [13]=0x12.
TEST(BleDeviceUuid, ShortFormMatchesEquivalentLongForm) {
const ESPBTUUID u16 = ESPBTUUID::from_uint16(0x1234);
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00};
const ESPBTUUID u128 = ESPBTUUID::from_raw(raw128);
EXPECT_TRUE(u16 == u128);
EXPECT_TRUE(u128 == u16); // symmetric
}
TEST(BleDeviceUuid, ThirtyTwoBitMatchesEquivalentLongForm) {
const ESPBTUUID u32 = ESPBTUUID::from_uint32(0x1122AAFF);
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
0x00, 0x10, 0x00, 0x00, 0xFF, 0xAA, 0x22, 0x11};
const ESPBTUUID u128 = ESPBTUUID::from_raw(raw128);
EXPECT_TRUE(u32 == u128);
}
TEST(BleDeviceUuid, DifferentUuidsDoNotMatch) {
EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_uint16(0x1235));
const uint8_t raw128[16] = {0xFB, 0x34, 0x9B, 0x5F, 0x80, 0x00, 0x00, 0x80,
0x00, 0x10, 0x00, 0x00, 0x34, 0x12, 0x00, 0x00};
// Same low bytes but a non-base prefix is a genuinely different 128-bit UUID.
uint8_t custom[16];
memcpy(custom, raw128, 16);
custom[0] ^= 0x01;
EXPECT_FALSE(ESPBTUUID::from_uint16(0x1234) == ESPBTUUID::from_raw(custom));
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,48 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/ble_device_base/ble_device.h"
namespace esphome::ble_device_base::testing {
// Reference vector generated with Python `cryptography` AES-128-ECB following
// the RPA resolution procedure (Bluetooth Core, Vol 3 Part H §2.2.2):
// hash = e(IRK, prand), where prand is the top 3 address bytes and the hash
// must equal the low 3 address bytes.
namespace {
const uint8_t IRK[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
// 4A:2B:7C:FB:7B:21 — prand 4A:2B:7C (two MSBs = 01, an RPA), hash FB:7B:21.
const uint8_t RPA_LSB_FIRST[6] = {0x21, 0x7b, 0xfb, 0x7c, 0x2b, 0x4a};
ESPBTDevice make_device(const uint8_t mac_lsb_first[6]) {
ESPBTDevice device;
device.from_scan_result(mac_lsb_first, /*rssi=*/-60, /*addr_type=*/BLE_ADDR_TYPE_RPA_RANDOM, nullptr, 0);
return device;
}
} // namespace
TEST(BleIrk, ResolvesMatchingRpa) {
ESPBTDevice device = make_device(RPA_LSB_FIRST);
EXPECT_TRUE(device.resolve_irk(IRK));
}
TEST(BleIrk, RejectsWrongIrk) {
uint8_t wrong_irk[16];
for (int i = 0; i < 16; i++)
wrong_irk[i] = IRK[i] ^ 0xff;
ESPBTDevice device = make_device(RPA_LSB_FIRST);
EXPECT_FALSE(device.resolve_irk(wrong_irk));
}
TEST(BleIrk, RejectsWrongAddress) {
uint8_t other_mac[6];
for (int i = 0; i < 6; i++)
other_mac[i] = RPA_LSB_FIRST[i];
other_mac[0] ^= 0x01; // corrupt one hash byte
ESPBTDevice device = make_device(other_mac);
EXPECT_FALSE(device.resolve_irk(IRK));
}
} // namespace esphome::ble_device_base::testing