From 7b9d3b4c331d989bc9bb475173a04e2cfac82348 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 19:59:24 -1000 Subject: [PATCH 1/6] [packet_transport] Store parent pointer to enable inline Callback storage Add a back-pointer to PacketTransport in the Sensor and BinarySensor structs so the state callback lambdas can capture only [&sensor] (one pointer) instead of [this, &sensor] (two pointers). This brings the capture size within the Callback inline threshold (sizeof(void*)), avoiding a heap allocation per callback registration. --- .../packet_transport/packet_transport.cpp | 8 ++++---- .../packet_transport/packet_transport.h | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 964037a02c..3f6e60b86f 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,16 +221,16 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](float x) { - this->updated_ = true; + sensor.sensor->add_on_state_callback([&sensor](float x) { + sensor.parent->updated_ = true; sensor.updated = true; }); } #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { - sensor.sensor->add_on_state_callback([this, &sensor](bool value) { - this->updated_ = true; + sensor.sensor->add_on_state_callback([&sensor](bool value) { + sensor.parent->updated_ = true; sensor.updated = true; }); } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index b3798738e2..08303ef54a 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -37,11 +37,14 @@ struct Provider { #endif }; +class PacketTransport; + #ifdef USE_SENSOR struct Sensor { sensor::Sensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif #ifdef USE_BINARY_SENSOR @@ -49,10 +52,18 @@ struct BinarySensor { binary_sensor::BinarySensor *sensor; const char *id; bool updated; + PacketTransport *parent; }; #endif class PacketTransport : public PollingComponent { +#ifdef USE_SENSOR + friend struct Sensor; +#endif +#ifdef USE_BINARY_SENSOR + friend struct BinarySensor; +#endif + public: void setup() override; void loop() override; @@ -61,7 +72,7 @@ class PacketTransport : public PollingComponent { #ifdef USE_SENSOR void add_sensor(const char *id, sensor::Sensor *sensor) { - Sensor st{sensor, id, true}; + Sensor st{sensor, id, true, this}; this->sensors_.push_back(st); } void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { @@ -71,7 +82,7 @@ class PacketTransport : public PollingComponent { #endif #ifdef USE_BINARY_SENSOR void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { - BinarySensor st{sensor, id, true}; + BinarySensor st{sensor, id, true, this}; this->binary_sensors_.push_back(st); } From 96f4f1537cf01bf1e9b4c4b1fcb5f4f6d73ab517 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:26:42 -1000 Subject: [PATCH 2/6] [packet_transport] Add safety comments for [&sensor] capture lifetime --- esphome/components/packet_transport/packet_transport.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 3f6e60b86f..afe5300dd5 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,6 +221,8 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { + // [&sensor] is safe: sensor refers to a vector element that is never modified after setup(), + // so the reference remains valid for the component's lifetime. sensor.sensor->add_on_state_callback([&sensor](float x) { sensor.parent->updated_ = true; sensor.updated = true; @@ -229,6 +231,8 @@ void PacketTransport::setup() { #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { + // [&sensor] is safe: sensor refers to a vector element that is never modified after setup(), + // so the reference remains valid for the component's lifetime. sensor.sensor->add_on_state_callback([&sensor](bool value) { sensor.parent->updated_ = true; sensor.updated = true; From ad668f6395e124278bd0096011d17d099ba60ce8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:29:02 -1000 Subject: [PATCH 3/6] [packet_transport] Use FixedVector for sensor lists to eliminate realloc machinery The sensor and binary sensor counts are known at config time, so pre-allocate with FixedVector::init() instead of using std::vector which pulls in _M_realloc_insert template instantiation code. --- esphome/components/packet_transport/__init__.py | 10 ++++++++-- .../components/packet_transport/packet_transport.cpp | 4 ++-- esphome/components/packet_transport/packet_transport.h | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 1930e45e85..0b166bb65c 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -177,13 +177,19 @@ async def register_packet_transport(var, config): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) is_provider = False - for sens_conf in config.get(CONF_SENSORS, ()): + sensors = config.get(CONF_SENSORS, ()) + binary_sensors = config.get(CONF_BINARY_SENSORS, ()) + if sensors: + cg.add(var.set_sensor_count(len(sensors))) + if binary_sensors: + cg.add(var.set_binary_sensor_count(len(binary_sensors))) + for sens_conf in sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_sensor(bcst_id, sensor)) - for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + for sens_conf in binary_sensors: is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index afe5300dd5..6c6259d97f 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -221,7 +221,7 @@ void PacketTransport::setup() { } #ifdef USE_SENSOR for (auto &sensor : this->sensors_) { - // [&sensor] is safe: sensor refers to a vector element that is never modified after setup(), + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, // so the reference remains valid for the component's lifetime. sensor.sensor->add_on_state_callback([&sensor](float x) { sensor.parent->updated_ = true; @@ -231,7 +231,7 @@ void PacketTransport::setup() { #endif #ifdef USE_BINARY_SENSOR for (auto &sensor : this->binary_sensors_) { - // [&sensor] is safe: sensor refers to a vector element that is never modified after setup(), + // [&sensor] is safe: sensor refers to a FixedVector element that never reallocates, // so the reference remains valid for the component's lifetime. sensor.sensor->add_on_state_callback([&sensor](bool value) { sensor.parent->updated_ = true; diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index 08303ef54a..c2cf68936d 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -71,6 +72,7 @@ class PacketTransport : public PollingComponent { void dump_config() override; #ifdef USE_SENSOR + void set_sensor_count(size_t count) { this->sensors_.init(count); } void add_sensor(const char *id, sensor::Sensor *sensor) { Sensor st{sensor, id, true, this}; this->sensors_.push_back(st); @@ -81,6 +83,7 @@ class PacketTransport : public PollingComponent { } #endif #ifdef USE_BINARY_SENSOR + void set_binary_sensor_count(size_t count) { this->binary_sensors_.init(count); } void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { BinarySensor st{sensor, id, true, this}; this->binary_sensors_.push_back(st); @@ -152,11 +155,11 @@ class PacketTransport : public PollingComponent { std::vector encryption_key_{}; #ifdef USE_SENSOR - std::vector sensors_{}; + FixedVector sensors_{}; string_map_t> remote_sensors_{}; #endif #ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; + FixedVector binary_sensors_{}; string_map_t> remote_binary_sensors_{}; #endif From c1a843a9af34fc6b97eae4de222a5e116a2c2bc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:30:50 -1000 Subject: [PATCH 4/6] [packet_transport] Remove unnecessary friend declarations The lambdas are defined inside PacketTransport::setup() so they inherit member-function access rights. The friend declarations are not needed for accessing protected members through parent pointer. --- esphome/components/packet_transport/packet_transport.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index c2cf68936d..836775bc85 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -58,13 +58,6 @@ struct BinarySensor { #endif class PacketTransport : public PollingComponent { -#ifdef USE_SENSOR - friend struct Sensor; -#endif -#ifdef USE_BINARY_SENSOR - friend struct BinarySensor; -#endif - public: void setup() override; void loop() override; From 257d804d0657054ce7972e493d93dc36ba40bfef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:34:06 -1000 Subject: [PATCH 5/6] [packet_transport] Use const ref in dump_config loops to avoid copies --- esphome/components/packet_transport/packet_transport.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 6c6259d97f..a2199977aa 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -552,11 +552,11 @@ void PacketTransport::dump_config() { " Ping-pong: %s", this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_)); #ifdef USE_SENSOR - for (auto sensor : this->sensors_) + for (const auto &sensor : this->sensors_) ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); #endif #ifdef USE_BINARY_SENSOR - for (auto sensor : this->binary_sensors_) + for (const auto &sensor : this->binary_sensors_) ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); #endif for (const auto &host : this->providers_) { From 9a3798ad2aa54c466b5e0e48202b85534cfbe4c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 21:46:41 -1000 Subject: [PATCH 6/6] [packet_transport] Fix tests to call set_sensor_count before add_sensor FixedVector requires init() before push_back(). Update all test cases to call set_sensor_count()/set_binary_sensor_count() before adding sensors, matching what the Python codegen does. --- .../packet_transport/binary_sensor/binary_sensor_test.cpp | 4 ++++ tests/components/packet_transport/sensor/sensor_test.cpp | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp index 36af087d2c..5ad25c2d7d 100644 --- a/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp +++ b/tests/components/packet_transport/binary_sensor/binary_sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportBinarySensorTest, AddBinarySensor) { TestablePacketTransport transport; binary_sensor::BinarySensor bs; + transport.set_binary_sensor_count(1); transport.add_binary_sensor("motion", &bs); ASSERT_EQ(transport.binary_sensors_.size(), 1u); EXPECT_STREQ(transport.binary_sensors_[0].id, "motion"); @@ -24,6 +25,7 @@ TEST(PacketTransportBinarySensorTest, UnencryptedBinarySensorRoundTrip) { encoder.init_for_test("sender"); binary_sensor::BinarySensor local_bs; local_bs.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("motion", &local_bs); encoder.send_data_(true); @@ -46,11 +48,13 @@ TEST(PacketTransportBinarySensorTest, MultipleSensorsRoundTrip) { sensor::Sensor s1, s2; s1.state = 10.0f; s2.state = 20.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); binary_sensor::BinarySensor bs1; bs1.state = true; + encoder.set_binary_sensor_count(1); encoder.add_binary_sensor("bs1", &bs1); encoder.send_data_(true); diff --git a/tests/components/packet_transport/sensor/sensor_test.cpp b/tests/components/packet_transport/sensor/sensor_test.cpp index 2f681aee58..5d1cfb4bc2 100644 --- a/tests/components/packet_transport/sensor/sensor_test.cpp +++ b/tests/components/packet_transport/sensor/sensor_test.cpp @@ -5,6 +5,7 @@ namespace esphome::packet_transport::testing { TEST(PacketTransportSensorTest, AddSensor) { TestablePacketTransport transport; sensor::Sensor s; + transport.set_sensor_count(1); transport.add_sensor("temp", &s); ASSERT_EQ(transport.sensors_.size(), 1u); EXPECT_STREQ(transport.sensors_[0].id, "temp"); @@ -26,6 +27,7 @@ TEST(PacketTransportSensorTest, UnencryptedSensorRoundTrip) { encoder.init_for_test("sender"); sensor::Sensor local_sensor; local_sensor.state = 42.5f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -53,6 +55,7 @@ TEST(PacketTransportSensorTest, EncryptedSensorRoundTrip) { encoder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 99.9f; + encoder.set_sensor_count(1); encoder.add_sensor("temp", &local_sensor); encoder.send_data_(true); @@ -77,6 +80,7 @@ TEST(PacketTransportSensorTest, SendDataOnlyUpdated) { sensor::Sensor s1, s2; s1.state = 1.0f; s2.state = 2.0f; + encoder.set_sensor_count(2); encoder.add_sensor("s1", &s1); encoder.add_sensor("s2", &s2); @@ -111,6 +115,7 @@ TEST(PacketTransportSensorTest, PingKeyIncludedInTransmittedPacket) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); // Requester sends a MAGIC_PING that the responder processes @@ -148,6 +153,7 @@ TEST(PacketTransportSensorTest, MissingPingKeyBlocksSensorData) { responder.set_encryption_key(key); sensor::Sensor local_sensor; local_sensor.state = 77.7f; + responder.set_sensor_count(1); responder.add_sensor("temp", &local_sensor); responder.send_data_(true); ASSERT_EQ(responder.sent_packets.size(), 1u);