Compare commits

..

20 Commits

Author SHA1 Message Date
J. Nick Koston
357ef51beb use simple lookup 2025-06-26 09:22:15 +02:00
Keith Burzinski
2fc8131164 Move consts to cpp file, arrange vars for better memory use 2025-06-26 01:45:51 -05:00
Keith Burzinski
26007175be Merge branch 'dev' into 20250625-ld2410-optimizations 2025-06-26 01:16:12 -05:00
Jesse Hills
09e5aa6011 [script] Add exec bit to run-in-env (#9212) 2025-06-26 00:59:16 -05:00
Jesse Hills
9549304007 [ci] Lint lock.yml (#9214) 2025-06-26 17:44:02 +12:00
Keith Burzinski
f7ac32ceda [ld2450] More optimizing, fix copypasta (#9210) 2025-06-26 00:35:30 -05:00
Jonathan Swoboda
92365f133d [esp32] Improve and simplify IDF component support (#9163) 2025-06-26 17:29:42 +12:00
Keith Burzinski
461e10ca3e inline 2025-06-26 00:27:05 -05:00
Keith Burzinski
205dc9c6f8 Move two_byte_to_int 2025-06-26 00:21:06 -05:00
Jesse Hills
9daa9a6de8 Use shared workflow for locking (#9211) 2025-06-26 16:21:51 +12:00
Keith Burzinski
f5eca9ce8d Even smaller 2025-06-25 23:15:21 -05:00
Keith Burzinski
487dd6c8d1 Fix accesses 2025-06-25 23:00:18 -05:00
J. Nick Koston
23b1e428de Optimize Application class memory layout and reduce loop_interval size (#9208) 2025-06-26 15:35:01 +12:00
Keith Burzinski
67a25e56b0 Fix bluetooth state handling 2025-06-25 21:42:36 -05:00
Keith Burzinski
a27f7cd765 [ld2410] More optimizations 2025-06-25 21:17:59 -05:00
J. Nick Koston
f029f4f20e Fix missing protobuf message dump for batched messages with very verbose logging (#9206) 2025-06-26 13:57:41 +12:00
J. Nick Koston
79e3d2b2d7 Optimize API connection memory with tagged pointers (#9203) 2025-06-26 13:55:12 +12:00
J. Nick Koston
c74e5e0f04 Optimize TemplatableValue memory (#9202) 2025-06-26 13:51:51 +12:00
J. Nick Koston
15ef93ccc9 Optimize API connection loop performance (#9184) 2025-06-26 13:47:41 +12:00
J. Nick Koston
e017250445 Reduce logger CPU usage by disabling loop when buffer is empty (#9160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-26 13:44:07 +12:00
26 changed files with 390 additions and 804 deletions

View File

@@ -1,28 +1,11 @@
---
name: Lock
name: Lock closed issues and PRs
on:
schedule:
- cron: "30 0 * * *"
- cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open
uses: esphome/workflows/.github/workflows/lock.yml@main

View File

@@ -65,6 +65,10 @@ uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_
void APIConnection::start() {
this->last_traffic_ = App.get_loop_component_start_time();
// Set next_ping_retry_ to prevent immediate ping
// This ensures the first ping happens after the keepalive period
this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS;
APIError err = this->helper_->init();
if (err != APIError::OK) {
on_fatal_error();
@@ -159,15 +163,20 @@ void APIConnection::loop() {
on_fatal_error();
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) {
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING");
this->sent_ping_ = this->send_message(PingRequest());
if (!this->sent_ping_) {
// If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
ESP_LOGVV(TAG, "Failed to send ping directly, scheduling at front of batch");
this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE);
this->sent_ping_ = true; // Mark as sent to avoid scheduling multiple pings
this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
this->ping_retries_++;
if (this->ping_retries_ >= MAX_PING_RETRIES) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
} else {
ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
}
}
}
@@ -267,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
// Encode directly into buffer
msg.encode(buffer);
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message for VV debugging
conn->log_send_message_(msg.message_name(), msg.dump());
#endif
// Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode;
@@ -1743,11 +1757,6 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
items.emplace_back(entity, std::move(creator), message_type);
}
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
// Insert at front for high priority messages (no deduplication check)
items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type));
}
bool APIConnection::schedule_batch_() {
if (!this->deferred_batch_.batch_scheduled) {
this->deferred_batch_.batch_scheduled = true;
@@ -1929,12 +1938,6 @@ uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConne
return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
PingRequest req;
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) {
// Use generated ESTIMATED_SIZE constants from each message type
switch (message_type) {

View File

@@ -185,6 +185,7 @@ class APIConnection : public APIServerConnection {
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false;
}
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
@@ -440,16 +441,13 @@ class APIConnection : public APIServerConnection {
// Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type);
// Batch message method for ping requests
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
// Pointers first (4 bytes each, naturally aligned)
std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_;
// 4-byte aligned types
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
int state_subs_at_ = -1;
// Strings (12 bytes each on 32-bit)
@@ -472,7 +470,8 @@ class APIConnection : public APIServerConnection {
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
// 7 bytes used, 1 byte padding
uint8_t ping_retries_{0};
// 8 bytes used, no padding needed
// Larger objects at the end
InitialStateIterator initial_state_iterator_;
@@ -599,8 +598,6 @@ class APIConnection : public APIServerConnection {
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type);
void clear() {
items.clear();
batch_scheduled = false;
@@ -640,12 +637,6 @@ class APIConnection : public APIServerConnection {
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type);
}
// Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type);
return this->schedule_batch_();
}
};
} // namespace api

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService {
template<typename T> bool send_message(const T &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump());
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, T::MESSAGE_TYPE);
}

View File

@@ -526,8 +526,8 @@ void APIServer::on_shutdown() {
for (auto &c : this->clients_) {
if (!c->send_message(DisconnectRequest())) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
// schedule it in the batch so it will be sent with the 5ms timer
c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
}
}
}

View File

@@ -335,6 +335,7 @@ class ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const;
virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
protected:

View File

@@ -4,7 +4,7 @@ import logging
import os
from pathlib import Path
from esphome import git
from esphome import yaml_util
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -23,7 +23,6 @@ from esphome.const import (
CONF_REFRESH,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
CONF_VARIANT,
CONF_VERSION,
KEY_CORE,
@@ -32,14 +31,13 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
TYPE_GIT,
TYPE_LOCAL,
__version__,
)
from esphome.core import CORE, HexInt, TimePeriod
from esphome.cpp_generator import RawExpression
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType
from .boards import BOARDS
from .const import ( # noqa
@@ -49,10 +47,8 @@ from .const import ( # noqa
KEY_EXTRA_BUILD_FILES,
KEY_PATH,
KEY_REF,
KEY_REFRESH,
KEY_REPO,
KEY_SDKCONFIG_OPTIONS,
KEY_SUBMODULES,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32C2,
@@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
def add_idf_component(
*,
name: str,
repo: str,
repo: str = None,
ref: str = None,
path: str = None,
refresh: TimePeriod = None,
@@ -245,30 +241,27 @@ def add_idf_component(
"""Add an esp-idf component to the project."""
if not CORE.using_esp_idf:
raise ValueError("Not an esp-idf project")
if components is None:
components = []
if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]:
if not repo and not ref and not path:
raise ValueError("Requires at least one of repo, ref or path")
if refresh or submodules or components:
_LOGGER.warning(
"The refresh, components and submodules parameters in add_idf_component() are "
"deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report "
"an issue to the external_component author and ask them to update it."
)
if components:
for comp in components:
CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = {
KEY_REPO: repo,
KEY_REF: ref,
KEY_PATH: f"{path}/{comp}" if path else comp,
}
else:
CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = {
KEY_REPO: repo,
KEY_REF: ref,
KEY_PATH: path,
KEY_REFRESH: refresh,
KEY_COMPONENTS: components,
KEY_SUBMODULES: submodules,
}
else:
component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name]
if components is not None:
component_config[KEY_COMPONENTS] = list(
set(component_config[KEY_COMPONENTS] + components)
)
if submodules is not None:
if component_config[KEY_SUBMODULES] is None:
component_config[KEY_SUBMODULES] = submodules
else:
component_config[KEY_SUBMODULES] = list(
set(component_config[KEY_SUBMODULES] + submodules)
)
def add_extra_script(stage: str, filename: str, path: str):
@@ -575,6 +568,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
def _validate_idf_component(config: ConfigType) -> ConfigType:
"""Validate IDF component config and warn about deprecated options."""
if CONF_REFRESH in config:
_LOGGER.warning(
"The 'refresh' option for IDF components is deprecated and has no effect. "
"It will be removed in ESPHome 2026.1. Please remove it from your configuration."
)
return config
ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
@@ -614,15 +618,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH, default="1d"): cv.All(
cv.string, cv.source_refresh
),
}
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(
cv.string, cv.source_refresh
),
}
),
_validate_idf_component,
)
),
}
@@ -814,18 +822,12 @@ async def to_code(config):
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
for component in conf[CONF_COMPONENTS]:
source = component[CONF_SOURCE]
if source[CONF_TYPE] == TYPE_GIT:
add_idf_component(
name=component[CONF_NAME],
repo=source[CONF_URL],
ref=source.get(CONF_REF),
path=component.get(CONF_PATH),
refresh=component[CONF_REFRESH],
)
elif source[CONF_TYPE] == TYPE_LOCAL:
_LOGGER.warning("Local components are not implemented yet.")
add_idf_component(
name=component[CONF_NAME],
repo=component.get(CONF_SOURCE),
ref=component.get(CONF_REF),
path=component.get(CONF_PATH),
)
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
@@ -924,6 +926,26 @@ def _write_sdkconfig():
write_file_if_changed(sdk_path, contents)
def _write_idf_component_yml():
yml_path = Path(CORE.relative_build_path("src/idf_component.yml"))
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
dependencies = {}
for name, component in components.items():
dependency = {}
if component[KEY_REF]:
dependency["version"] = component[KEY_REF]
if component[KEY_REPO]:
dependency["git"] = component[KEY_REPO]
if component[KEY_PATH]:
dependency["path"] = component[KEY_PATH]
dependencies[name] = dependency
contents = yaml_util.dump({"dependencies": dependencies})
else:
contents = ""
write_file_if_changed(yml_path, contents)
# Called by writer.py
def copy_files():
if CORE.using_arduino:
@@ -936,6 +958,7 @@ def copy_files():
)
if CORE.using_esp_idf:
_write_sdkconfig()
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
@@ -952,55 +975,6 @@ def copy_files():
__version__,
)
import shutil
shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True)
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
for name, component in components.items():
repo_dir, _ = git.clone_or_update(
url=component[KEY_REPO],
ref=component[KEY_REF],
refresh=component[KEY_REFRESH],
domain="idf_components",
submodules=component[KEY_SUBMODULES],
)
mkdir_p(CORE.relative_build_path("components"))
component_dir = repo_dir
if component[KEY_PATH] is not None:
component_dir = component_dir / component[KEY_PATH]
if component[KEY_COMPONENTS] == ["*"]:
shutil.copytree(
component_dir,
CORE.relative_build_path("components"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
elif len(component[KEY_COMPONENTS]) > 0:
for comp in component[KEY_COMPONENTS]:
shutil.copytree(
component_dir / comp,
CORE.relative_build_path(f"components/{comp}"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
else:
shutil.copytree(
component_dir,
CORE.relative_build_path(f"components/{name}"),
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git*"),
symlinks=True,
ignore_dangling_symlinks=True,
)
for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items():
if file[KEY_PATH].startswith("http"):
import requests

View File

@@ -10,24 +10,11 @@ GPIOBinarySensor = gpio_ns.class_(
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
)
CONF_USE_INTERRUPT = "use_interrupt"
CONF_INTERRUPT_TYPE = "interrupt_type"
INTERRUPT_TYPES = {
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
"FALLING": gpio_ns.INTERRUPT_FALLING_EDGE,
"ANY": gpio_ns.INTERRUPT_ANY_EDGE,
}
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(GPIOBinarySensor)
.extend(
{
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -40,7 +27,3 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
if config[CONF_USE_INTERRUPT]:
cg.add(var.set_interrupt_type(INTERRUPT_TYPES[config[CONF_INTERRUPT_TYPE]]))

View File

@@ -6,91 +6,17 @@ namespace gpio {
static const char *const TAG = "gpio.binary_sensor";
void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
bool new_state = arg->isr_pin_.digital_read();
if (new_state != arg->last_state_) {
arg->state_ = new_state;
arg->last_state_ = new_state;
arg->changed_ = true;
// Wake up the component from its disabled loop state
if (arg->component_ != nullptr) {
arg->component_->enable_loop_soon_any_context();
}
}
}
void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) {
pin->setup();
this->isr_pin_ = pin->to_isr();
this->component_ = component;
// Read initial state
this->last_state_ = pin->digital_read();
this->state_ = this->last_state_;
// Attach interrupt - from this point on, any changes will be caught by the interrupt
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type);
}
void GPIOBinarySensor::setup() {
if (this->use_interrupt_ && !this->pin_->is_internal()) {
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
this->use_interrupt_ = false;
}
if (this->use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this->interrupt_type_, this);
this->publish_initial_state(this->store_.get_state());
} else {
this->pin_->setup();
this->publish_initial_state(this->pin_->digital_read());
}
this->pin_->setup();
this->publish_initial_state(this->pin_->digital_read());
}
void GPIOBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
const char *mode = this->use_interrupt_ ? "interrupt" : "polling";
ESP_LOGCONFIG(TAG, " Mode: %s", mode);
if (this->use_interrupt_) {
const char *interrupt_type;
switch (this->interrupt_type_) {
case gpio::INTERRUPT_RISING_EDGE:
interrupt_type = "RISING_EDGE";
break;
case gpio::INTERRUPT_FALLING_EDGE:
interrupt_type = "FALLING_EDGE";
break;
case gpio::INTERRUPT_ANY_EDGE:
interrupt_type = "ANY_EDGE";
break;
default:
interrupt_type = "UNKNOWN";
break;
}
ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type);
}
}
void GPIOBinarySensor::loop() {
if (this->use_interrupt_) {
if (this->store_.is_changed()) {
// Clear the flag immediately to minimize the window where we might miss changes
this->store_.clear_changed();
// Read the state and publish it
// Note: If the ISR fires between clear_changed() and get_state(), that's fine -
// we'll process the new change on the next loop iteration
bool state = this->store_.get_state();
this->publish_state(state);
} else {
// No changes, disable the loop until the next interrupt
this->disable_loop();
}
} else {
this->publish_state(this->pin_->digital_read());
}
}
void GPIOBinarySensor::loop() { this->publish_state(this->pin_->digital_read()); }
float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; }

View File

@@ -2,51 +2,14 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace gpio {
// Store class for ISR data (no vtables, ISR-safe)
class GPIOBinarySensorStore {
public:
void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component);
static void gpio_intr(GPIOBinarySensorStore *arg);
bool get_state() const {
// No lock needed: state_ is atomically updated by ISR
// Volatile ensures we read the latest value
return this->state_;
}
bool is_changed() const {
// Simple read of volatile bool - no clearing here
return this->changed_;
}
void clear_changed() {
// Separate method to clear the flag
this->changed_ = false;
}
protected:
ISRInternalGPIOPin isr_pin_;
volatile bool state_{false};
volatile bool last_state_{false};
volatile bool changed_{false};
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
};
class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
public:
// No destructor needed: ESPHome components are created at boot and live forever.
// Interrupts are only detached on reboot when memory is cleared anyway.
void set_pin(GPIOPin *pin) { pin_ = pin; }
void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; }
void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Setup pin
@@ -59,9 +22,6 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
protected:
GPIOPin *pin_;
bool use_interrupt_{true};
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
GPIOBinarySensorStore store_;
};
} // namespace gpio

View File

@@ -17,8 +17,106 @@ namespace esphome {
namespace ld2410 {
static const char *const TAG = "ld2410";
static const char *const NO_MAC = "08:05:04:03:02:01";
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
LD2410Component::LD2410Component() {}
// Memory-efficient lookup tables
namespace {
struct StringToUint8 {
const char *str;
uint8_t value;
};
struct Uint8ToString {
uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr StringToUint8 DISTANCE_RESOLUTIONS[] = {
{"0.2m", DISTANCE_RESOLUTION_0_2},
{"0.75m", DISTANCE_RESOLUTION_0_75},
};
constexpr Uint8ToString DISTANCE_RESOLUTIONS_REV[] = {
{DISTANCE_RESOLUTION_0_2, "0.2m"},
{DISTANCE_RESOLUTION_0_75, "0.75m"},
};
constexpr StringToUint8 LIGHT_FUNCTIONS[] = {
{"off", LIGHT_FUNCTION_OFF},
{"below", LIGHT_FUNCTION_BELOW},
{"above", LIGHT_FUNCTION_ABOVE},
};
constexpr Uint8ToString LIGHT_FUNCTIONS_REV[] = {
{LIGHT_FUNCTION_OFF, "off"},
{LIGHT_FUNCTION_BELOW, "below"},
{LIGHT_FUNCTION_ABOVE, "above"},
};
constexpr StringToUint8 OUT_PIN_LEVELS[] = {
{"low", OUT_PIN_LEVEL_LOW},
{"high", OUT_PIN_LEVEL_HIGH},
};
constexpr Uint8ToString OUT_PIN_LEVELS_REV[] = {
{OUT_PIN_LEVEL_LOW, "low"},
{OUT_PIN_LEVEL_HIGH, "high"},
};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) {
if (str == entry.str)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
} // namespace
// Commands
static const uint8_t CMD_ENABLE_CONF = 0xFF;
static const uint8_t CMD_DISABLE_CONF = 0xFE;
static const uint8_t CMD_ENABLE_ENG = 0x62;
static const uint8_t CMD_DISABLE_ENG = 0x63;
static const uint8_t CMD_MAXDIST_DURATION = 0x60;
static const uint8_t CMD_QUERY = 0x61;
static const uint8_t CMD_GATE_SENS = 0x64;
static const uint8_t CMD_VERSION = 0xA0;
static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB;
static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA;
static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE;
static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD;
static const uint8_t CMD_SET_BAUD_RATE = 0xA1;
static const uint8_t CMD_BT_PASSWORD = 0xA9;
static const uint8_t CMD_MAC = 0xA5;
static const uint8_t CMD_RESET = 0xA2;
static const uint8_t CMD_RESTART = 0xA3;
static const uint8_t CMD_BLUETOOTH = 0xA4;
// Commands values
static const uint8_t CMD_MAX_MOVE_VALUE = 0x00;
static const uint8_t CMD_MAX_STILL_VALUE = 0x01;
static const uint8_t CMD_DURATION_VALUE = 0x02;
// Command Header & Footer
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
// Data Header & Footer
static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1};
static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5};
void LD2410Component::dump_config() {
ESP_LOGCONFIG(TAG, "LD2410:");
@@ -78,7 +176,7 @@ void LD2410Component::dump_config() {
" Throttle: %ums\n"
" MAC address: %s\n"
" Firmware version: %s",
this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str()));
this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
}
void LD2410Component::setup() {
@@ -200,7 +298,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
*/
#ifdef USE_SENSOR
if (this->moving_target_distance_sensor_ != nullptr) {
int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]);
int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]);
if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance)
this->moving_target_distance_sensor_->publish_state(new_moving_target_distance);
}
@@ -210,7 +308,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
this->moving_target_energy_sensor_->publish_state(new_moving_target_energy);
}
if (this->still_target_distance_sensor_ != nullptr) {
int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]);
int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]);
if (this->still_target_distance_sensor_->get_state() != new_still_target_distance)
this->still_target_distance_sensor_->publish_state(new_still_target_distance);
}
@@ -220,7 +318,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
this->still_target_energy_sensor_->publish_state(new_still_target_energy);
}
if (this->detection_distance_sensor_ != nullptr) {
int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]);
int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]);
if (this->detection_distance_sensor_->get_state() != new_detect_distance)
this->detection_distance_sensor_->publish_state(new_detect_distance);
}
@@ -282,25 +380,6 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
#endif
}
const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X";
std::string format_version(uint8_t *buffer) {
std::string::size_type version_size = 256;
std::string version;
do {
version.resize(version_size + 1);
version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17],
buffer[16], buffer[15], buffer[14]);
} while (version_size + 1 > version.size());
version.resize(version_size);
return version;
}
const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X";
const std::string UNKNOWN_MAC("unknown");
const std::string NO_MAC("08:05:04:03:02:01");
#ifdef USE_NUMBER
std::function<void(void)> set_number_value(number::Number *n, float value) {
float normalized_value = value * 1.0;
@@ -326,7 +405,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
ESP_LOGE(TAG, "Invalid status");
return true;
}
if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) {
if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) {
ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]);
return true;
}
@@ -347,8 +426,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
#endif
break;
case lowbyte(CMD_VERSION):
this->version_ = format_version(buffer);
ESP_LOGV(TAG, "Firmware version: %s", const_cast<char *>(this->version_.c_str()));
this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(this->version_);
@@ -357,8 +436,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
break;
case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): {
std::string distance_resolution =
DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11]));
ESP_LOGV(TAG, "Distance resolution: %s", const_cast<char *>(distance_resolution.c_str()));
find_str(DISTANCE_RESOLUTIONS_REV, ld2410::two_byte_to_int(buffer[10], buffer[11]));
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str());
#ifdef USE_SELECT
if (this->distance_resolution_select_ != nullptr &&
this->distance_resolution_select_->state != distance_resolution) {
@@ -367,9 +446,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
#endif
} break;
case lowbyte(CMD_QUERY_LIGHT_CONTROL): {
this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]);
this->light_function_ = find_str(LIGHT_FUNCTIONS_REV, buffer[10]);
this->light_threshold_ = buffer[11] * 1.0;
this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]);
this->out_pin_level_ = find_str(OUT_PIN_LEVELS_REV, buffer[12]);
ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str()));
ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_);
ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str()));
@@ -402,7 +481,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC);
this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
}
#endif
break;
@@ -448,7 +527,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
/*
None Duration: 33~34th bytes
*/
updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33])));
updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33])));
for (auto &update : updates) {
update();
}
@@ -505,14 +584,14 @@ void LD2410Component::set_bluetooth(bool enable) {
void LD2410Component::set_distance_resolution(const std::string &state) {
this->set_config_mode_(true);
uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00};
uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS, state), 0x00};
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
void LD2410Component::set_baud_rate(const std::string &state) {
this->set_config_mode_(true);
uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00};
uint8_t cmd_value[2] = {find_uint8(BAUD_RATES, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -646,9 +725,9 @@ void LD2410Component::set_light_out_control() {
return;
}
this->set_config_mode_(true);
uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_);
uint8_t light_function = find_uint8(LIGHT_FUNCTIONS, this->light_function_);
uint8_t light_threshold = static_cast<uint8_t>(this->light_threshold_);
uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_);
uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS, this->out_pin_level_);
uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00};
this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4);
delay(50); // NOLINT

View File

@@ -26,33 +26,11 @@
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include <map>
namespace esphome {
namespace ld2410 {
#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1)
// Commands
static const uint8_t CMD_ENABLE_CONF = 0x00FF;
static const uint8_t CMD_DISABLE_CONF = 0x00FE;
static const uint8_t CMD_ENABLE_ENG = 0x0062;
static const uint8_t CMD_DISABLE_ENG = 0x0063;
static const uint8_t CMD_MAXDIST_DURATION = 0x0060;
static const uint8_t CMD_QUERY = 0x0061;
static const uint8_t CMD_GATE_SENS = 0x0064;
static const uint8_t CMD_VERSION = 0x00A0;
static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB;
static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA;
static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE;
static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD;
static const uint8_t CMD_SET_BAUD_RATE = 0x00A1;
static const uint8_t CMD_BT_PASSWORD = 0x00A9;
static const uint8_t CMD_MAC = 0x00A5;
static const uint8_t CMD_RESET = 0x00A2;
static const uint8_t CMD_RESTART = 0x00A3;
static const uint8_t CMD_BLUETOOTH = 0x00A4;
enum BaudRateStructure : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
@@ -61,58 +39,25 @@ enum BaudRateStructure : uint8_t {
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
BAUD_RATE_460800 = 8,
};
static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}};
enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 };
static const std::map<std::string, uint8_t> DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2},
{"0.75m", DISTANCE_RESOLUTION_0_75}};
static const std::map<uint8_t, std::string> DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"},
{DISTANCE_RESOLUTION_0_75, "0.75m"}};
enum DistanceResolutionStructure : uint8_t {
DISTANCE_RESOLUTION_0_2 = 0x01,
DISTANCE_RESOLUTION_0_75 = 0x00,
};
enum LightFunctionStructure : uint8_t {
LIGHT_FUNCTION_OFF = 0x00,
LIGHT_FUNCTION_BELOW = 0x01,
LIGHT_FUNCTION_ABOVE = 0x02
LIGHT_FUNCTION_ABOVE = 0x02,
};
static const std::map<std::string, uint8_t> LIGHT_FUNCTION_ENUM_TO_INT{
{"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}};
static const std::map<uint8_t, std::string> LIGHT_FUNCTION_INT_TO_ENUM{
{LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}};
enum OutPinLevelStructure : uint8_t {
OUT_PIN_LEVEL_LOW = 0x00,
OUT_PIN_LEVEL_HIGH = 0x01,
};
enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 };
static const std::map<std::string, uint8_t> OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW},
{"high", OUT_PIN_LEVEL_HIGH}};
static const std::map<uint8_t, std::string> OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"},
{OUT_PIN_LEVEL_HIGH, "high"}};
// Commands values
static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000;
static const uint8_t CMD_MAX_STILL_VALUE = 0x0001;
static const uint8_t CMD_DURATION_VALUE = 0x0002;
// Command Header & Footer
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
// Data Header & Footer
static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1};
static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5};
/*
Data Type: 6th byte
Target states: 9th byte
Moving target distance: 10~11th bytes
Moving target energy: 12th byte
Still target distance: 13~14th bytes
Still target energy: 15th byte
Detect distance: 16~17th bytes
*/
enum PeriodicDataStructure : uint8_t {
DATA_TYPES = 6,
TARGET_STATES = 8,
@@ -129,11 +74,20 @@ enum PeriodicDataStructure : uint8_t {
LIGHT_SENSOR = 37,
OUT_PIN_SENSOR = 38,
};
enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 };
enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 };
enum PeriodicDataValue : uint8_t {
HEAD = 0xAA,
END = 0x55,
CHECK = 0x00,
};
enum AckDataStructure : uint8_t {
COMMAND = 6,
COMMAND_STATUS = 7,
};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
// char cmd[2] = {enable ? 0xFF : 0xFE, 0x00};
class LD2410Component : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
SUB_SENSOR(moving_target_distance)
@@ -176,7 +130,6 @@ class LD2410Component : public Component, public uart::UARTDevice {
#endif
public:
LD2410Component();
void setup() override;
void dump_config() override;
void loop() override;
@@ -202,7 +155,6 @@ class LD2410Component : public Component, public uart::UARTDevice {
void factory_reset();
protected:
int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len);
void set_config_mode_(bool enable);
void handle_periodic_data_(uint8_t *buffer, int len);
@@ -215,14 +167,14 @@ class LD2410Component : public Component, public uart::UARTDevice {
void get_light_control_();
void restart_();
int32_t last_periodic_millis_ = millis();
int32_t last_engineering_mode_change_millis_ = millis();
int32_t last_periodic_millis_ = 0;
int32_t last_engineering_mode_change_millis_ = 0;
uint16_t throttle_;
float light_threshold_ = -1;
std::string version_;
std::string mac_;
std::string out_pin_level_;
std::string light_function_;
float light_threshold_ = -1;
#ifdef USE_NUMBER
std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(9);
std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(9);

View File

@@ -17,8 +17,9 @@ namespace esphome {
namespace ld2450 {
static const char *const TAG = "ld2450";
static const char *const NO_MAC("08:05:04:03:02:01");
static const char *const UNKNOWN_MAC("unknown");
static const char *const NO_MAC = "08:05:04:03:02:01";
static const char *const UNKNOWN_MAC = "unknown";
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
// LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0x00FF;
@@ -98,13 +99,6 @@ static inline std::string get_direction(int16_t speed) {
return STATIONARY;
}
static inline std::string format_version(uint8_t *buffer) {
return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
buffer[14]);
}
LD2450Component::LD2450Component() {}
void LD2450Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
#ifdef USE_NUMBER
@@ -189,7 +183,7 @@ void LD2450Component::dump_config() {
" Throttle: %ums\n"
" MAC Address: %s\n"
" Firmware version: %s",
this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str()));
this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str());
}
void LD2450Component::loop() {
@@ -596,7 +590,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif
break;
case lowbyte(CMD_VERSION):
this->version_ = ld2450::format_version(buffer);
this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
@@ -617,7 +611,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC);
this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
}
#endif
break;

View File

@@ -141,7 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
#endif
public:
LD2450Component();
void setup() override;
void dump_config() override;
void loop() override;
@@ -197,17 +196,17 @@ class LD2450Component : public Component, public uart::UARTDevice {
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint32_t last_periodic_millis_ = 0;
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0;
uint16_t timeout_ = 5;
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint8_t zone_type_ = 0;
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
std::string version_{};
std::string mac_{};
#ifdef USE_NUMBER

View File

@@ -1,26 +0,0 @@
"""
Runtime statistics component for ESPHome.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
DEPENDENCIES = []
CONF_ENABLED = "enabled"
CONF_LOG_INTERVAL = "log_interval"
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
cv.Optional(
CONF_LOG_INTERVAL, default=60000
): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
"""Generate code for the runtime statistics component."""
cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))

View File

@@ -136,10 +136,6 @@ void Application::loop() {
this->in_loop_ = false;
this->app_state_ = new_app_state;
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
runtime_stats.process_pending_stats(last_op_end_time);
// Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_;
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {

View File

@@ -9,7 +9,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/runtime_stats.h"
#include "esphome/core/scheduler.h"
#ifdef USE_DEVICES
@@ -338,6 +337,9 @@ class Application {
* Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester
* helper in helpers.h
*
* Note: This method is not called by ESPHome core code. It is only used by lambda functions
* in YAML configurations or by external components.
*
* @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds.
*/
void set_loop_interval(uint32_t loop_interval) {
@@ -346,18 +348,6 @@ class Application {
uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); }
/** Enable or disable runtime statistics collection.
*
* @param enable Whether to enable runtime statistics collection.
*/
void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
/** Set the interval at which runtime statistics are logged.
*
* @param interval The interval in milliseconds between logging of runtime statistics.
*/
void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
void schedule_dump_config() { this->dump_config_at_ = 0; }
void feed_wdt(uint32_t time = 0);

View File

@@ -303,9 +303,6 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_;
// Record component runtime stats
runtime_stats.record_component_time(this->component_, blocking_time, curr_time);
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);

View File

@@ -6,7 +6,6 @@
#include <string>
#include "esphome/core/optional.h"
#include "esphome/core/runtime_stats.h"
namespace esphome {

View File

@@ -375,7 +375,7 @@ void ComponentIterator::advance() {
}
if (advance_platform) {
this->state_ = static_cast<IteratorState>(static_cast<uint16_t>(this->state_) + 1);
this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1);
this->at_ = 0;
} else if (success) {
this->at_++;

View File

@@ -93,9 +93,7 @@ class ComponentIterator {
virtual bool on_end();
protected:
// Iterates over all ESPHome entities (sensors, switches, lights, etc.)
// Supports up to 256 entity types and up to 65,535 entities of each type
enum class IteratorState : uint8_t {
enum class IteratorState {
NONE = 0,
BEGIN,
#ifdef USE_BINARY_SENSOR
@@ -169,7 +167,7 @@ class ComponentIterator {
#endif
MAX,
} state_{IteratorState::NONE};
uint16_t at_{0}; // Supports up to 65,535 entities per type
size_t at_{0};
bool include_internal_{false};
};

View File

@@ -1,92 +0,0 @@
#include "esphome/core/runtime_stats.h"
#include "esphome/core/component.h"
#include <algorithm>
namespace esphome {
RuntimeStatsCollector runtime_stats;
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
if (!this->enabled_ || component == nullptr)
return;
// Check if we have cached the name for this component
auto name_it = this->component_names_cache_.find(component);
if (name_it == this->component_names_cache_.end()) {
// First time seeing this component, cache its name
const char *source = component->get_component_source();
this->component_names_cache_[component] = source;
this->component_stats_[source].record_time(duration_ms);
} else {
// Use cached name - no string operations, just map lookup
this->component_stats_[name_it->second].record_time(duration_ms);
}
// If next_log_time_ is 0, initialize it
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
// Don't print stats here anymore - let process_pending_stats handle it
}
void RuntimeStatsCollector::log_stats_() {
ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
ESP_LOGI(RUNTIME_TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
// First collect stats we want to display
std::vector<ComponentStatPair> stats_to_display;
for (const auto &it : this->component_stats_) {
const ComponentRuntimeStats &stats = it.second;
if (stats.get_period_count() > 0) {
ComponentStatPair pair = {it.first, &stats};
stats_to_display.push_back(pair);
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
stats->get_period_time_ms());
}
// Log total stats since boot
ESP_LOGI(RUNTIME_TAG, "Total stats (since boot):");
// Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(),
[](const ComponentStatPair &a, const ComponentStatPair &b) {
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
});
for (const auto &it : stats_to_display) {
const std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
}
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if (!this->enabled_ || this->next_log_time_ == 0)
return;
if (current_time >= this->next_log_time_) {
this->log_stats_();
this->reset_stats_();
this->next_log_time_ = current_time + this->log_interval_;
}
}
} // namespace esphome

View File

@@ -1,121 +0,0 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include <cstdint>
#include <algorithm>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
static const char *const RUNTIME_TAG = "runtime";
class Component; // Forward declaration
class ComponentRuntimeStats {
public:
ComponentRuntimeStats()
: period_count_(0),
total_count_(0),
period_time_ms_(0),
total_time_ms_(0),
period_max_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Period stats (reset each logging interval)
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
}
// Total stats (persistent until reboot)
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
}
protected:
// Period stats (reset each logging interval)
uint32_t period_count_;
uint32_t period_time_ms_;
uint32_t period_max_time_ms_;
// Total stats (persistent until reboot)
uint32_t total_count_;
uint32_t total_time_ms_;
uint32_t total_max_time_ms_;
};
// For sorting components by run time
struct ComponentStatPair {
std::string name;
const ComponentRuntimeStats *stats;
bool operator>(const ComponentStatPair &other) const {
// Sort by period time as that's what we're displaying in the logs
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
}
};
class RuntimeStatsCollector {
public:
RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
uint32_t get_log_interval() const { return this->log_interval_; }
void set_enabled(bool enabled) { this->enabled_ = enabled; }
bool is_enabled() const { return this->enabled_; }
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
protected:
void log_stats_();
void reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
// Back to string keys, but we'll cache the source name per component
std::map<std::string, ComponentRuntimeStats> component_stats_;
std::map<Component *, std::string> component_names_cache_;
uint32_t log_interval_;
uint32_t next_log_time_;
bool enabled_;
};
// Global instance for runtime stats collection
extern RuntimeStatsCollector runtime_stats;
} // namespace esphome

View File

@@ -886,7 +886,7 @@ def build_message_type(
public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
snake_name = camel_to_snake(desc.name)
public_content.append(
f'static constexpr const char *message_name() {{ return "{snake_name}"; }}'
f'const char *message_name() const override {{ return "{snake_name}"; }}'
)
public_content.append("#endif")
@@ -1356,7 +1356,7 @@ def main() -> None:
hpp += " template<typename T>\n"
hpp += " bool send_message(const T &msg) {\n"
hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
hpp += " this->log_send_message_(T::message_name(), msg.dump());\n"
hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n"
hpp += "#endif\n"
hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n"
hpp += " }\n\n"

0
script/run-in-env.py Normal file → Executable file
View File