[esp32_ble_server] Keep constant set_value payloads in shared flash tables (#20314)

This commit is contained in:
J. Nick Koston
2026-10-11 15:12:09 -10:00
committed by GitHub
parent 993fd8d7a2
commit 5d64b65d72
10 changed files with 204 additions and 36 deletions
+53 -15
View File
@@ -1,4 +1,6 @@
import encodings
import struct
from typing import Any
from esphome import automation
import esphome.codegen as cg
@@ -25,8 +27,10 @@ from esphome.const import (
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
import esphome.final_validate as fv
from esphome.schema_extractors import SCHEMA_EXTRACT
from esphome.types import ConfigType, TemplateArgsType
AUTO_LOAD = ["esp32_ble", "bytebuffer"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
@@ -111,8 +115,6 @@ BLECharacteristicNotifyAction = esp32_ble_server_automations_ns.class_(
)
bytebuffer_ns = cg.esphome_ns.namespace("bytebuffer")
Endianness_ns = bytebuffer_ns.namespace("Endian")
ByteBuffer_ns = bytebuffer_ns.namespace("ByteBuffer")
ByteBuffer = bytebuffer_ns.class_("ByteBuffer")
PROPERTY_MAP = {
@@ -141,6 +143,15 @@ class ValueType:
return value
def float32(value: Any) -> float:
value = cv.float_(value)
try:
struct.pack("<f", value)
except OverflowError as e:
raise cv.Invalid(f"{value} is out of range for a float") from e
return value
VALUE_TYPES = {
type_name: ValueType(type_name, validator, length)
for type_name, validator, length in (
@@ -152,7 +163,7 @@ VALUE_TYPES = {
("int16_t", cv.int_range(-32768, 32767), 2),
("int32_t", cv.int_range(-2147483648, 2147483647), 4),
("int64_t", cv.int_range(-9223372036854775808, 9223372036854775807), 8),
("float", cv.float_, 4),
("float", float32, 4),
("double", cv.float_, 8),
("string", cv.string_strict, None), # Length is variable
)
@@ -495,19 +506,48 @@ def parse_uuid(uuid):
return ESPBTUUID_ns.from_raw(uuid)
_STRUCT_FORMATS = {
"uint8_t": "B",
"uint16_t": "H",
"uint32_t": "I",
"uint64_t": "Q",
"int8_t": "b",
"int16_t": "h",
"int32_t": "i",
"int64_t": "q",
"float": "f",
"double": "d",
}
def value_bytes(value_config: ConfigType) -> list[int]:
"""The bytes of a constant value, as ByteBuffer::wrap packed them at runtime."""
value = value_config[CONF_DATA]
if isinstance(value, str):
return list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
return value
order = ">" if value_config[CONF_ENDIANNESS] == "BIG" else "<"
return list(struct.pack(order + _STRUCT_FORMATS[value_config[CONF_TYPE]], value))
async def parse_value(value_config, args):
value = value_config[CONF_DATA]
if isinstance(value, cv.Lambda):
return await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
# An initializer list calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value_bytes(value_config))
if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
# Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
# This calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
async def set_value_buffer(
var: MockObj, value_config: ConfigType, args: TemplateArgsType
) -> None:
value = value_config[CONF_DATA]
if not isinstance(value, cv.Lambda):
value = value_bytes(value_config)
await automation.templatable_bytes(
value, args, var.set_buffer_template, var.set_buffer_static, "ble_server_value"
)
def calculate_num_handles(service_config):
@@ -662,8 +702,7 @@ async def to_code(config):
async def ble_server_characteristic_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
await set_value_buffer(var, config[CONF_VALUE], args)
cg.add_define("USE_ESP32_BLE_SERVER_SET_VALUE_ACTION")
return var
@@ -682,8 +721,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a
async def ble_server_descriptor_set_value(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
value = await parse_value(config[CONF_VALUE], args)
cg.add(var.set_buffer(value))
await set_value_buffer(var, config[CONF_VALUE], args)
cg.add_define("USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION")
return var
@@ -30,6 +30,14 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const uint8_t *data, size_t length) {
if (length == 0) { // An empty constant is a nullptr table
this->value_.clear();
return;
}
this->value_.assign(data, data + length);
}
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
@@ -30,6 +30,7 @@ class BLECharacteristic final {
~BLECharacteristic();
void set_value(ByteBuffer buffer);
void set_value(const uint8_t *data, size_t length);
void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(const std::string &buffer);
@@ -49,17 +49,18 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING;
}
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value(buffer.data(), buffer.size()); }
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value(data.begin(), data.size()); }
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
void BLEDescriptor::set_value(const uint8_t *data, size_t length) {
if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return;
}
this->value_.attr_len = length;
memcpy(this->value_.attr_value, data, length);
if (length != 0) // An empty constant is a nullptr table
memcpy(this->value_.attr_value, data, length);
}
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
@@ -26,6 +26,7 @@ class BLEDescriptor {
void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(const uint8_t *data, size_t length);
void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
@@ -42,8 +43,6 @@ class BLEDescriptor {
}
protected:
void set_value_impl_(const uint8_t *data, size_t length);
BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_;
uint16_t handle_{0xFFFF};
@@ -16,6 +16,16 @@ namespace esphome::esp32_ble_server::esp32_ble_server_automations {
using namespace esp32_ble;
/// Sets a characteristic or descriptor value from a constant flash table or a lambda's vector.
// Not TemplatableBytes::visit, which would copy the lambda's vector instead of moving it into the value.
template<typename T, typename V, typename... Xs> void apply_value(T *target, const V &value, Xs &&...x) {
if (value.is_static()) {
target->set_value(value.data(), value.size());
} else {
target->set_value(value.value(x...));
}
}
class BLETriggers {
public:
#ifdef USE_ESP32_BLE_SERVER_CHARACTERISTIC_ON_WRITE
@@ -67,25 +77,23 @@ class BLECharacteristicSetValueActionManager {
template<typename... Ts> class BLECharacteristicSetValueAction final : public Action<Ts...> {
public:
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
TEMPLATABLE_BYTES(buffer)
void play(const Ts &...x) override {
// If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
return;
// Set initial value
this->parent_->set_value(this->buffer_.value(x...));
apply_value(this->parent_, this->buffer_, x...);
// Set the listener for read events
// ``mutable`` keeps by-copy captures non-const for triggers passing args by reference
// (e.g. climate on_control's ClimateCall&). See #17142.
this->parent_->on_read([this, x...](uint16_t id) mutable {
// Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...));
apply_value(this->parent_, this->buffer_, x...);
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, [this, x...]() mutable { this->parent_->set_value(this->buffer_.value(x...)); });
this->parent_, [this, x...]() mutable { apply_value(this->parent_, this->buffer_, x...); });
}
protected:
@@ -115,10 +123,8 @@ template<typename... Ts> class BLECharacteristicNotifyAction final : public Acti
template<typename... Ts> class BLEDescriptorSetValueAction final : public Action<Ts...> {
public:
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(std::initializer_list<uint8_t> buffer) { this->buffer_ = std::vector<uint8_t>(buffer); }
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); }
TEMPLATABLE_BYTES(buffer)
void play(const Ts &...x) override { apply_value(this->parent_, this->buffer_, x...); }
protected:
BLEDescriptor *parent_;
@@ -0,0 +1,37 @@
esphome:
name: test
esp32:
variant: esp32
esp32_ble_server:
id: ble_server
services:
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
characteristics:
- id: test_characteristic
uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
read: true
value: [1, 2, 3, 4]
descriptors:
- id: test_descriptor
uuid: 0x4414
value: "Initial"
- uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc99a
write: true
on_write:
- ble_server.characteristic.set_value:
id: test_characteristic
value:
data: 4660
type: uint16_t
endianness: BIG
- ble_server.descriptor.set_value:
id: test_descriptor
value:
data: 4660
type: uint16_t
endianness: BIG
- ble_server.characteristic.set_value:
id: test_characteristic
value: !lambda return x;
@@ -9,10 +9,16 @@ import pytest
from esphome.components.esp32_ble_server import (
CCCD_DESCRIPTOR_UUID,
CONF_ENDIANNESS,
CONF_STRING_ENCODING,
CUD_DESCRIPTOR_UUID,
DEVICE_INFORMATION_SERVICE_UUID,
float32,
uuid_is,
value_bytes,
)
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_TYPE
@pytest.mark.parametrize(
@@ -73,3 +79,69 @@ def test_advertising_required(
main_cpp = generate_main(component_config_path(config_file))
assert f"set_advertising_required({str(required).lower()})" in main_cpp
# The same values and bytes as tests/components/bytebuffer/test_wrap_values.cpp, which pins
# what ByteBuffer::wrap produced from the expressions this component used to emit.
@pytest.mark.parametrize(
("data", "type_", "endianness", "expected"),
[
(18, "uint8_t", "LITTLE", [0x12]),
(18, "uint16_t", "LITTLE", [0x12, 0x00]),
(4660, "uint16_t", "BIG", [0x12, 0x34]),
(305419896, "uint32_t", "LITTLE", [0x78, 0x56, 0x34, 0x12]),
(
1311768467294899695,
"uint64_t",
"BIG",
[0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF],
),
(-5, "int8_t", "LITTLE", [0xFB]),
(-2, "int16_t", "BIG", [0xFF, 0xFE]),
(-2, "int32_t", "LITTLE", [0xFE, 0xFF, 0xFF, 0xFF]),
(-2, "int64_t", "LITTLE", [0xFE] + [0xFF] * 7),
(123.1, "float", "BIG", [0x42, 0xF6, 0x33, 0x33]),
(0.1, "float", "LITTLE", [0xCD, 0xCC, 0xCC, 0x3D]),
(0.1, "double", "LITTLE", [0x9A, 0x99, 0x99, 0x99, 0x99, 0x99, 0xB9, 0x3F]),
(2.5, "double", "BIG", [0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
],
)
def test_value_bytes_match_bytebuffer(
data: float, type_: str, endianness: str, expected: list[int]
) -> None:
config = {
CONF_DATA: data,
CONF_TYPE: type_,
CONF_ENDIANNESS: endianness,
CONF_STRING_ENCODING: "utf_8",
}
assert value_bytes(config) == expected
def test_value_bytes_strings_and_lists() -> None:
assert value_bytes({CONF_DATA: "hé", CONF_STRING_ENCODING: "utf_8"}) == [
0x68,
0xC3,
0xA9,
]
assert value_bytes({CONF_DATA: [1, 2], CONF_STRING_ENCODING: "utf_8"}) == [1, 2]
def test_set_value_constants_share_a_flash_table(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("set_value.yaml"))
assert main_cpp.count("ble_server_value[] PROGMEM = {0x12, 0x34};") == 1
assert main_cpp.count("set_buffer_static(ble_server_value, 2);") == 2
assert "set_buffer_template(" in main_cpp
assert "ByteBuffer::wrap" not in main_cpp
def test_float32_rejects_out_of_range() -> None:
for value in (1e40, -1e40):
with pytest.raises(cv.Invalid, match="out of range for a float"):
float32(value)
assert float32(3.4028235e38) == 3.4028235e38
assert float32(float("inf")) == float("inf")
@@ -8,8 +8,7 @@
namespace esphome::bytebuffer {
// The expressions esp32_ble_server emits for typed constant values, and the bytes they produce.
// Doubles are written as float literals, so they carry float precision.
// Typed values as ByteBuffer::wrap packs them; esp32_ble_server's value_bytes() is tested against the same bytes.
TEST(ByteBufferWrapValues, MatchesGeneratedConstants) {
EXPECT_EQ(ByteBuffer::wrap(uint8_t(18), LITTLE).get_data(), (std::vector<uint8_t>{0x12}));
EXPECT_EQ(ByteBuffer::wrap(uint16_t(18), LITTLE).get_data(), (std::vector<uint8_t>{0x12, 0x00}));
@@ -24,9 +23,9 @@ TEST(ByteBufferWrapValues, MatchesGeneratedConstants) {
(std::vector<uint8_t>{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}));
EXPECT_EQ(ByteBuffer::wrap(float(123.1f), BIG).get_data(), (std::vector<uint8_t>{0x42, 0xf6, 0x33, 0x33}));
EXPECT_EQ(ByteBuffer::wrap(float(0.1f), LITTLE).get_data(), (std::vector<uint8_t>{0xcd, 0xcc, 0xcc, 0x3d}));
EXPECT_EQ(ByteBuffer::wrap(double(0.1f), LITTLE).get_data(),
(std::vector<uint8_t>{0x00, 0x00, 0x00, 0xa0, 0x99, 0x99, 0xb9, 0x3f}));
EXPECT_EQ(ByteBuffer::wrap(double(2.5f), BIG).get_data(),
EXPECT_EQ(ByteBuffer::wrap(double(0.1), LITTLE).get_data(),
(std::vector<uint8_t>{0x9a, 0x99, 0x99, 0x99, 0x99, 0x99, 0xb9, 0x3f}));
EXPECT_EQ(ByteBuffer::wrap(double(2.5), BIG).get_data(),
(std::vector<uint8_t>{0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}));
}
@@ -110,6 +110,13 @@ esp32_ble_server:
- ble_server.descriptor.set_value:
id: test_change_descriptor
value: "Changed"
# Empty constants
- ble_server.characteristic.set_value:
id: test_change_characteristic
value: ""
- ble_server.descriptor.set_value:
id: test_change_descriptor
value: ""
# Lambdas that read the trigger argument
- ble_server.characteristic.set_value:
id: test_change_characteristic