mirror of
https://github.com/esphome/esphome.git
synced 2026-07-11 01:15:33 +00:00
Merge branch 'dev' into partition-table-ota
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
dc8ad5472d9fb44ce1ca29a0601afd65705642799a2819704dfc8459fbaf9815
|
||||
c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"--privileged",
|
||||
"-e",
|
||||
"GIT_EDITOR=code --wait"
|
||||
// uncomment and edit the path in order to pass though local USB serial to the conatiner
|
||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
||||
// , "--device=/dev/ttyACM0"
|
||||
],
|
||||
"appPort": 6052,
|
||||
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
|
||||
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.10
|
||||
rev: v0.15.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -58,6 +58,7 @@ repos:
|
||||
entry: python3 script/run-in-env.py pylint
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^esphome/.+\.py$
|
||||
- id: clang-tidy-hash
|
||||
name: Update clang-tidy hash
|
||||
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||
|
||||
+72
-6
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._config_dir = Path(CORE.config_dir).resolve()
|
||||
self._config_path = Path(CORE.config_path).resolve()
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
@@ -258,21 +258,36 @@ class ConfigBundleCreator:
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
We track files by wrapping _load_yaml_internal. The config has already
|
||||
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
|
||||
re-load just to discover the file list.
|
||||
Deliberately uses a fresh re-parse and force-loads every deferred
|
||||
``IncludeFile`` to include *all* potentially-reachable includes,
|
||||
even branches not selected by the local substitutions. Bundles are
|
||||
meant to be compiled on another system where command-line
|
||||
substitution overrides may choose a different branch — e.g.
|
||||
``!include network/${eth_model}/config.yaml`` must ship every
|
||||
candidate so the remote build can pick any one.
|
||||
|
||||
Entries with unresolved substitution variables in the filename
|
||||
path are skipped with a warning (they cannot be resolved without
|
||||
the substitution pass).
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
# Must be a fresh parse: IncludeFile.load() caches its result in
|
||||
# _content, and we discover files by listening for loader calls. On
|
||||
# an already-parsed tree the cache is populated, .load() returns
|
||||
# without calling the loader, the listener never fires, and the
|
||||
# referenced files would be silently dropped from the bundle.
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
yaml_util.load_yaml(self._config_path)
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
else:
|
||||
_force_load_include_files(data)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
@@ -608,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
|
||||
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
|
||||
resolved during the substitution pass. During bundle discovery we need
|
||||
the referenced files to actually load so the ``track_yaml_loads``
|
||||
listener fires for them.
|
||||
|
||||
``IncludeFile`` instances with unresolved substitution variables in the
|
||||
filename cannot be loaded — we skip and warn about those.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, yaml_util.IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
_LOGGER.warning(
|
||||
"Bundle: cannot resolve !include %s (referenced from %s) "
|
||||
"with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Bundle: failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
_force_load_include_files(loaded, _seen)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
_force_load_include_files(value, _seen)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
_force_load_include_files(item, _seen)
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace anova {
|
||||
|
||||
@@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
|
||||
}
|
||||
case READ_TARGET_TEMPERATURE:
|
||||
case SET_TARGET_TEMPERATURE: {
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->target_temp_ = ftoc(this->target_temp_);
|
||||
this->has_target_temp_ = true;
|
||||
break;
|
||||
}
|
||||
case READ_CURRENT_TEMPERATURE: {
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->current_temp_ = ftoc(this->current_temp_);
|
||||
this->has_current_temp_ = true;
|
||||
|
||||
@@ -291,12 +291,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_CONNECTIONS,
|
||||
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
|
||||
esp32=8, # 520KB RAM available
|
||||
esp32=5, # 520KB RAM available
|
||||
rp2040=4, # 264KB RAM but LWIP constraints
|
||||
bk72xx=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
bk72xx=5, # Moderate RAM
|
||||
rtl87xx=5, # Moderate RAM
|
||||
host=8, # Abundant resources
|
||||
ln882x=8, # Moderate RAM
|
||||
ln882x=5, # Moderate RAM
|
||||
): cv.int_range(min=1, max=20),
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
@@ -336,8 +336,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
if CONF_LISTEN_BACKLOG in config:
|
||||
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||
if CONF_MAX_CONNECTIONS in config:
|
||||
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
|
||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
|
||||
|
||||
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
|
||||
|
||||
@@ -118,7 +118,7 @@ void APIServer::loop() {
|
||||
this->accept_new_connections_();
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
if (this->api_connection_count_ == 0) {
|
||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time)
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
@@ -135,15 +135,15 @@ void APIServer::loop() {
|
||||
// Check network connectivity once for all clients
|
||||
if (!network::is_connected()) {
|
||||
// Network is down - disconnect all clients
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->on_fatal_error();
|
||||
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
|
||||
}
|
||||
// Continue to process and clean up the clients below
|
||||
}
|
||||
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
uint8_t client_index = 0;
|
||||
while (client_index < this->api_connection_count_) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
@@ -161,7 +161,7 @@ void APIServer::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
void APIServer::remove_client_(uint8_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -179,14 +179,17 @@ void APIServer::remove_client_(size_t client_index) {
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
|
||||
// [api_connection_count_, N) remain nullptr.
|
||||
const uint8_t last_index = this->api_connection_count_ - 1;
|
||||
if (client_index < last_index) {
|
||||
std::swap(this->clients_[client_index], this->clients_[last_index]);
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
this->clients_[last_index].reset();
|
||||
this->api_connection_count_--;
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -210,8 +213,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
@@ -220,11 +223,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
this->clients_[this->api_connection_count_++].reset(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -237,7 +240,7 @@ void APIServer::dump_config() {
|
||||
" Address: %s:%u\n"
|
||||
" Listen backlog: %u\n"
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
@@ -255,7 +258,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) { \
|
||||
for (auto &c : this->active_clients()) { \
|
||||
if (c->flags_.state_subscription) \
|
||||
c->send_##entity_name##_state(obj); \
|
||||
} \
|
||||
@@ -337,7 +340,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
|
||||
void APIServer::on_event(event::Event *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.state_subscription)
|
||||
c->send_event(obj);
|
||||
}
|
||||
@@ -349,7 +352,7 @@ void APIServer::on_event(event::Event *obj) {
|
||||
void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.state_subscription)
|
||||
c->send_update_state(obj);
|
||||
}
|
||||
@@ -360,7 +363,7 @@ void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
// We could add code to manage a second subscription type, but, since this message type is
|
||||
// very infrequent and small, we simply send it to all clients
|
||||
for (auto &c : this->clients_)
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_message(msg);
|
||||
}
|
||||
#endif
|
||||
@@ -375,7 +378,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
resp.key = key;
|
||||
resp.timings = timings;
|
||||
|
||||
for (auto &c : this->clients_)
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
#endif
|
||||
@@ -392,7 +395,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->send_homeassistant_action(call);
|
||||
}
|
||||
}
|
||||
@@ -532,7 +535,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req);
|
||||
}
|
||||
@@ -583,7 +586,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
|
||||
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void APIServer::request_time() {
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
if (!client->flags_.remove && client->is_authenticated()) {
|
||||
client->send_time_request();
|
||||
return; // Only request from one client to avoid clock conflicts
|
||||
@@ -593,8 +596,8 @@ void APIServer::request_time() {
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected_with_state_subscription() const {
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
|
||||
if (this->clients_[i]->flags_.state_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -609,7 +612,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
@@ -618,7 +621,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
@@ -635,7 +638,7 @@ void APIServer::on_shutdown() {
|
||||
this->batch_delay_ = 5;
|
||||
|
||||
// Send disconnect requests to all connected clients
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
if (!c->send_message(req)) {
|
||||
// If we can't send the disconnect request directly (tx_buffer full),
|
||||
@@ -653,7 +656,7 @@ bool APIServer::teardown() {
|
||||
this->loop();
|
||||
|
||||
// Return true only when all clients have been torn down
|
||||
return this->clients_.empty();
|
||||
return this->api_connection_count_ == 0;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
@@ -63,7 +65,6 @@ class APIServer final : public Component,
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
||||
|
||||
// Get reference to shared buffer for API connections
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
@@ -186,9 +187,26 @@ class APIServer final : public Component,
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
bool is_connected() const { return !this->clients_.empty(); }
|
||||
bool is_connected() const { return this->api_connection_count_ != 0; }
|
||||
bool is_connected_with_state_subscription() const;
|
||||
|
||||
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
|
||||
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
|
||||
// APIConnection but cannot reset/move the slot and break the count invariant.
|
||||
using APIConnectionPtr = std::unique_ptr<APIConnection>;
|
||||
class ActiveClientsView {
|
||||
const APIConnectionPtr *begin_;
|
||||
const APIConnectionPtr *end_;
|
||||
|
||||
public:
|
||||
ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {}
|
||||
const APIConnectionPtr *begin() const { return this->begin_; }
|
||||
const APIConnectionPtr *end() const { return this->end_; }
|
||||
};
|
||||
ActiveClientsView active_clients() const {
|
||||
return {this->clients_.data(), this->clients_.data() + this->api_connection_count_};
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
const char *entity_id; // Pointer to flash (internal) or heap (external)
|
||||
@@ -234,8 +252,8 @@ class APIServer final : public Component,
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
|
||||
void __attribute__((noinline)) remove_client_(uint8_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
@@ -273,8 +291,9 @@ class APIServer final : public Component,
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
// Shared proto write buffer for all connections.
|
||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
||||
@@ -309,10 +328,10 @@ class APIServer final : public Component,
|
||||
uint16_t port_{6053};
|
||||
uint16_t batch_delay_{100};
|
||||
// Connection limits - these defaults will be overridden by config values
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
||||
uint8_t listen_backlog_{4};
|
||||
uint8_t max_connections_{8};
|
||||
bool shutting_down_ = false;
|
||||
uint8_t api_connection_count_{0};
|
||||
// 7 bytes used, 1 byte padding
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr
|
||||
CODEOWNERS = ["@trvrnrth"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensor", "text_sensor"]
|
||||
CONFLICTS_WITH = ["bme68x_bsec2"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BME680_BSEC_ID = "bme680_bsec_id"
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
CONFLICTS_WITH = ["bme680_bsec"]
|
||||
|
||||
DOMAIN = "bme68x_bsec2"
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
|
||||
|
||||
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
|
||||
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
|
||||
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
|
||||
size_t pos = buf_append_str(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, ESPHOME_VERSION);
|
||||
|
||||
this->free_heap_ = get_free_heap_();
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
|
||||
@@ -224,17 +224,21 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
const char *model = ESPHOME_VARIANT;
|
||||
|
||||
// Build features string
|
||||
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
|
||||
pos = buf_append_str(buf, size, pos, "|Chip: ");
|
||||
pos = buf_append_str(buf, size, pos, model);
|
||||
pos = buf_append_str(buf, size, pos, " Features:");
|
||||
bool first_feature = true;
|
||||
for (const auto &feature : CHIP_FEATURES) {
|
||||
if (info.features & feature.bit) {
|
||||
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
|
||||
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
|
||||
pos = buf_append_str(buf, size, pos, feature.name);
|
||||
first_feature = false;
|
||||
info.features &= ~feature.bit;
|
||||
}
|
||||
}
|
||||
if (info.features != 0) {
|
||||
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
|
||||
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
|
||||
pos = buf_append_printf(buf, size, pos, "Other:0x%" PRIx32, info.features);
|
||||
}
|
||||
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
|
||||
|
||||
@@ -267,17 +271,20 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
// Framework detection
|
||||
#ifdef USE_ARDUINO
|
||||
ESP_LOGD(TAG, " Framework: Arduino");
|
||||
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
|
||||
pos = buf_append_str(buf, size, pos, "|Framework: Arduino");
|
||||
#else
|
||||
ESP_LOGD(TAG, " Framework: ESP-IDF");
|
||||
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
|
||||
pos = buf_append_str(buf, size, pos, "|Framework: ESP-IDF");
|
||||
#endif
|
||||
|
||||
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
|
||||
pos = buf_append_str(buf, size, pos, "|ESP-IDF: ");
|
||||
pos = buf_append_str(buf, size, pos, esp_get_idf_version());
|
||||
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
|
||||
mac[4], mac[5]);
|
||||
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
|
||||
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
|
||||
pos = buf_append_str(buf, size, pos, "|Reset: ");
|
||||
pos = buf_append_str(buf, size, pos, reset_reason);
|
||||
pos = buf_append_str(buf, size, pos, "|Wakeup: ");
|
||||
pos = buf_append_str(buf, size, pos, wakeup_cause);
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
|
||||
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
|
||||
|
||||
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
|
||||
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
|
||||
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
|
||||
pos = buf_append_str(buf, size, pos, "|Version: ");
|
||||
pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10);
|
||||
pos = buf_append_str(buf, size, pos, "|Reset Reason: ");
|
||||
pos = buf_append_str(buf, size, pos, reset_reason);
|
||||
pos = buf_append_str(buf, size, pos, "|Chip Name: ");
|
||||
pos = buf_append_str(buf, size, pos, lt_cpu_get_model_name());
|
||||
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
|
||||
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
|
||||
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
|
||||
|
||||
@@ -162,14 +162,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
const char *supply_status =
|
||||
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
|
||||
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
|
||||
pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status);
|
||||
pos = buf_append_str(buf, size, pos, "|Main supply status: ");
|
||||
pos = buf_append_str(buf, size, pos, supply_status);
|
||||
|
||||
// Regulator stage 0
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
|
||||
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
||||
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
|
||||
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: ");
|
||||
pos = buf_append_str(buf, size, pos, reg0_type);
|
||||
pos = buf_append_str(buf, size, pos, ", ");
|
||||
pos = buf_append_str(buf, size, pos, reg0_voltage);
|
||||
#ifdef USE_NRF52_REG0_VOUT
|
||||
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
|
||||
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
|
||||
@@ -177,13 +181,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
#endif
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Regulator stage 0: disabled");
|
||||
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
|
||||
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: disabled");
|
||||
}
|
||||
|
||||
// Regulator stage 1
|
||||
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
|
||||
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
|
||||
pos = buf_append_str(buf, size, pos, "|Regulator stage 1: ");
|
||||
pos = buf_append_str(buf, size, pos, reg1_type);
|
||||
|
||||
// USB power state
|
||||
const char *usb_state;
|
||||
@@ -197,7 +202,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
usb_state = "disconnected";
|
||||
}
|
||||
ESP_LOGD(TAG, "USB power state: %s", usb_state);
|
||||
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
|
||||
pos = buf_append_str(buf, size, pos, "|USB power state: ");
|
||||
pos = buf_append_str(buf, size, pos, usb_state);
|
||||
|
||||
// Power-fail comparator
|
||||
bool enabled;
|
||||
@@ -302,14 +308,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
|
||||
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
|
||||
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
|
||||
pos = buf_append_str(buf, size, pos, pof_voltage);
|
||||
pos = buf_append_str(buf, size, pos, ", VDDH: ");
|
||||
pos = buf_append_str(buf, size, pos, vddh_voltage);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
|
||||
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
|
||||
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
|
||||
pos = buf_append_str(buf, size, pos, pof_voltage);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Power-fail comparator: disabled");
|
||||
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
|
||||
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: disabled");
|
||||
}
|
||||
|
||||
auto package = [](uint32_t value) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "epaper_spi_ssd1683.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
static constexpr const char *const TAG = "epaper_spi.mono";
|
||||
|
||||
void EPaperSSD1683::refresh_screen(bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh screen");
|
||||
this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01});
|
||||
// On partial update, set red RAM to inverse to remove BW ghosting
|
||||
this->cmd_data(0x21, {partial ? (uint8_t) 0x80 : (uint8_t) 0x40, (uint8_t) 0x00});
|
||||
// Set full update to 0xD7 for fast update, 0xF7 for normal
|
||||
// Fast update flashes less and draws sooner but is in busy state for the same amount of time
|
||||
// Manufacturer recommends not using fast update all the time, TODO expose this to the user
|
||||
this->cmd_data(0x22, {partial ? (uint8_t) 0xFC : (uint8_t) 0xF7});
|
||||
this->command(0x20);
|
||||
}
|
||||
|
||||
// Puts the display into deep sleep mode 1, only way to get out is to reset the display
|
||||
// Mode 1 retains RAM while sleeping, necessary for future partial and window updates
|
||||
void EPaperSSD1683::deep_sleep() {
|
||||
if (this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep mode 1");
|
||||
this->cmd_data(0x10, {0x01}); // deep sleep, retain RAM
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Deep sleep mode 2");
|
||||
this->cmd_data(0x10, {0x03}); // deep sleep, lose RAM
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperSSD1683::set_window() {
|
||||
// if not using partial update, the display will go into deep sleep mode 2, so must rewrite entire
|
||||
// buffer since the display RAM will not retain contents
|
||||
if (!this->is_using_partial_update_()) {
|
||||
this->x_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_low_ = 0;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
// round x-coordinates to byte boundaries
|
||||
this->x_low_ /= 8;
|
||||
this->x_high_ += 7;
|
||||
this->x_high_ /= 8;
|
||||
|
||||
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_high_ - 1)});
|
||||
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
|
||||
(uint8_t) ((this->y_high_ - 1) / 256)});
|
||||
this->cmd_data(0x4E, {(uint8_t) this->x_low_});
|
||||
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
|
||||
}
|
||||
|
||||
bool HOT EPaperSSD1683::transfer_data() {
|
||||
auto start_time = millis();
|
||||
if (this->current_data_index_ == 0) {
|
||||
if (this->send_red_) {
|
||||
// round to byte boundaries
|
||||
this->set_window();
|
||||
}
|
||||
// for monochrome, we need to send red on every refresh to prevent dirty pixels
|
||||
// when doing a partial refresh
|
||||
this->command(this->send_red_ ? 0x26 : 0x24);
|
||||
this->current_data_index_ = this->y_low_; // actually current line
|
||||
}
|
||||
size_t row_length = this->x_high_ - this->x_low_;
|
||||
FixedVector<uint8_t> bytes_to_send{};
|
||||
bytes_to_send.init(row_length);
|
||||
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->buffer_[data_idx++];
|
||||
}
|
||||
++this->current_data_index_;
|
||||
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this->disable();
|
||||
this->current_data_index_ = 0;
|
||||
if (this->send_red_) {
|
||||
this->send_red_ = false;
|
||||
return false;
|
||||
}
|
||||
this->send_red_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi_mono.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
/**
|
||||
* A class for Solomon SSD1683 epaper displays.
|
||||
*/
|
||||
class EPaperSSD1683 : public EPaperMono {
|
||||
public:
|
||||
EPaperSSD1683(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperMono(name, width, height, init_sequence, init_sequence_length) {}
|
||||
|
||||
protected:
|
||||
void refresh_screen(bool partial) override;
|
||||
void deep_sleep() override;
|
||||
void set_window() override;
|
||||
bool transfer_data() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,27 @@
|
||||
from esphome.const import CONF_DATA_RATE
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class SSD1683(EpaperModel):
|
||||
def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults):
|
||||
defaults[CONF_DATA_RATE] = data_rate
|
||||
super().__init__(name, class_name, **defaults)
|
||||
|
||||
# fmt: off
|
||||
def get_init_sequence(self, config: dict):
|
||||
_width, height = self.get_dimensions(config)
|
||||
return (
|
||||
(0x01, (height - 1) % 256, (height - 1) // 256, 0x00), # Set column gate limit
|
||||
(0x18, 0x80), # Select internal Temp sensor
|
||||
(0x11, 0x03), # Set transform
|
||||
)
|
||||
|
||||
|
||||
ssd1683 = SSD1683("ssd1683")
|
||||
|
||||
goodisplay_gdey042t81 = ssd1683.extend(
|
||||
"goodisplay-gdey042t81-4.2",
|
||||
width=400,
|
||||
height=300,
|
||||
)
|
||||
@@ -1222,7 +1222,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
|
||||
*ESP32_CHIP_REVISIONS
|
||||
*ESP32_CHIP_REVISIONS, string=True
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
|
||||
@@ -172,10 +172,16 @@ def validate_gpio_pin(pin):
|
||||
exc,
|
||||
)
|
||||
else:
|
||||
# Throw an exception if used for a pin that would not have resulted
|
||||
# in a validation error anyway!
|
||||
# `ignore_pin_validation_error` only suppresses an error raised by the
|
||||
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
|
||||
# numbers). If that didn't raise, the option is a no-op -- warn so the
|
||||
# user can clean it up, but don't block the build.
|
||||
if ignore_pin_validation_warning:
|
||||
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
|
||||
_LOGGER.warning(
|
||||
"GPIO%d has no validation errors to ignore; "
|
||||
"remove `ignore_pin_validation_error: true` from this pin.",
|
||||
pin[CONF_NUMBER],
|
||||
)
|
||||
|
||||
return pin
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_USE_PSRAM
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||
from esphome.components.esp32.const import VARIANT_ESP32C2
|
||||
import esphome.config_validation as cv
|
||||
@@ -342,6 +343,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
|
||||
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
|
||||
),
|
||||
cv.Optional(CONF_USE_PSRAM): cv.All(
|
||||
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -598,6 +602,22 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||
|
||||
# When PSRAM and BT are used together, Bluedroid should prefer SPIRAM for
|
||||
# heap allocations and use dynamic (heap-based) environment memory tables
|
||||
# instead of large static DRAM arrays. This frees ~40 kB of internal RAM.
|
||||
# Reference: Espressif ADF Design Considerations
|
||||
# https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/
|
||||
# design-guide/design-considerations.html
|
||||
if config.get(CONF_USE_PSRAM, False):
|
||||
cg.add_define("USE_ESP32_BLE_PSRAM")
|
||||
# CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST is only available on ESP32
|
||||
# (BTDM dual-mode controller). BLE-only SoCs (C3, S3, C2, H2) do not
|
||||
# expose this Kconfig symbol; applying it there would cause a build error.
|
||||
if get_esp32_variant() == const.VARIANT_ESP32:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST", True)
|
||||
# CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY applies to all Bluedroid-enabled variants.
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY", True)
|
||||
|
||||
# Register the core BLE loggers that are always needed
|
||||
register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)
|
||||
|
||||
|
||||
@@ -667,6 +667,9 @@ void ESP32BLE::dump_config() {
|
||||
" MAC address: %s\n"
|
||||
" IO Capability: %s",
|
||||
mac_s, io_capability_s);
|
||||
#ifdef USE_ESP32_BLE_PSRAM
|
||||
ESP_LOGCONFIG(TAG, " PSRAM BLE allocation: enabled");
|
||||
#endif
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
|
||||
const char *auth_req_mode_s = "<default>";
|
||||
|
||||
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
|
||||
int reset_pin_{-1};
|
||||
int phy_addr_spi_{-1};
|
||||
int clock_speed_;
|
||||
spi_host_device_t interface_{SPI3_HOST};
|
||||
spi_host_device_t interface_{SPI2_HOST};
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -60,20 +60,35 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
|
||||
"""Check if pin is shared exclusively with deep_sleep (wakeup pin)."""
|
||||
pin_key = (CORE.target_platform, CORE.target_platform, pin_num)
|
||||
pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, [])
|
||||
if len(pin_users) != 2:
|
||||
return False
|
||||
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
# Check for ESP8266 GPIO16 interrupt limitation
|
||||
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
|
||||
# the Arduino attachInterrupt() function. This is the only known GPIO pin
|
||||
# across all supported platforms that has this limitation, so we handle it
|
||||
# here instead of in the platform-specific code.
|
||||
def _final_validate(config):
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
|
||||
if not use_interrupt:
|
||||
return config
|
||||
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
|
||||
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
|
||||
# attachment — only internal/native GPIO pins do.
|
||||
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Pin is not an internal GPIO, "
|
||||
"falling back to polling mode.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
|
||||
if CORE.is_esp8266 and pin_num == 16:
|
||||
_LOGGER.warning(
|
||||
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
|
||||
"Falling back to polling mode (same as in ESPHome <2025.7). "
|
||||
@@ -81,22 +96,45 @@ async def to_code(config):
|
||||
"performance with interrupts.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
use_interrupt = False
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
# Check if pin is shared with other components (allow_other_uses)
|
||||
# When a pin is shared, interrupts can interfere with other components
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
|
||||
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
|
||||
"The sensor will use polling mode for compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
config[CONF_PIN][CONF_NUMBER],
|
||||
)
|
||||
use_interrupt = False
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
|
||||
# Exception: deep_sleep wakeup pins are compatible with interrupts when
|
||||
# the pin is only shared between this sensor and deep_sleep (count == 2).
|
||||
if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
if not _pin_shared_only_with_deep_sleep(pin_num):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared "
|
||||
"with other components. The sensor will use polling mode for "
|
||||
"compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
pin_num,
|
||||
)
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, "
|
||||
"keeping interrupts enabled.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
pin_num,
|
||||
)
|
||||
|
||||
if use_interrupt:
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
if config[CONF_USE_INTERRUPT]:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
else:
|
||||
# Only generate call when disabling interrupts (default is true)
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
cg.add(var.set_use_interrupt(False))
|
||||
|
||||
@@ -46,11 +46,6 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::setup() {
|
||||
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
|
||||
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
|
||||
this->store_.use_interrupt_ = false;
|
||||
}
|
||||
|
||||
if (this->store_.use_interrupt_) {
|
||||
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
|
||||
this->store_.setup(internal_pin, this);
|
||||
|
||||
@@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() {
|
||||
}
|
||||
|
||||
std::string HttpContainer::get_response_header(const std::string &header_name) {
|
||||
auto lower = str_lower_case(header_name);
|
||||
auto lower = str_lower_case(header_name); // NOLINT
|
||||
for (const auto &entry : this->response_headers_) {
|
||||
if (entry.name == lower) {
|
||||
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -400,7 +401,7 @@ class HttpRequestComponent : public Component {
|
||||
std::vector<std::string> lower;
|
||||
lower.reserve(collect_headers.size());
|
||||
for (const auto &h : collect_headers) {
|
||||
lower.push_back(str_lower_case(h));
|
||||
lower.push_back(str_lower_case(h)); // NOLINT
|
||||
}
|
||||
return this->perform(url, method, body, request_headers, lower);
|
||||
}
|
||||
@@ -415,7 +416,7 @@ class HttpRequestComponent : public Component {
|
||||
std::vector<std::string> lower;
|
||||
lower.reserve(collect_headers.size());
|
||||
for (const auto &h : collect_headers) {
|
||||
lower.push_back(str_lower_case(h));
|
||||
lower.push_back(str_lower_case(h)); // NOLINT
|
||||
}
|
||||
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
container->response_headers_.clear();
|
||||
auto header_count = container->client_.headers();
|
||||
for (int i = 0; i < header_count; i++) {
|
||||
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
|
||||
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT
|
||||
if (should_collect_header(lower_case_collect_headers, header_name)) {
|
||||
std::string header_value = container->client_.header(i).c_str();
|
||||
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
|
||||
|
||||
@@ -115,7 +115,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
|
||||
container->content_length = container->response_body_.size();
|
||||
for (auto header : response.headers) {
|
||||
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
|
||||
auto lower_name = str_lower_case(header.first);
|
||||
auto lower_name = str_lower_case(header.first); // NOLINT
|
||||
if (should_collect_header(lower_case_collect_headers, lower_name)) {
|
||||
container->response_headers_.push_back({lower_name, header.second});
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
|
||||
switch (evt->event_id) {
|
||||
case HTTP_EVENT_ON_HEADER: {
|
||||
const std::string header_name = str_lower_case(evt->header_key);
|
||||
const std::string header_name = str_lower_case(evt->header_key); // NOLINT
|
||||
if (should_collect_header(user_data->lower_case_collect_headers, header_name)) {
|
||||
const std::string header_value = evt->header_value;
|
||||
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace ili9xxx {
|
||||
|
||||
|
||||
@@ -229,6 +229,10 @@ void ILI9XXXDisplay::update() {
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::display_() {
|
||||
// buffer may be null if allocation failed
|
||||
if (this->buffer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// check if something was displayed
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
|
||||
return;
|
||||
|
||||
@@ -28,7 +28,6 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -676,12 +675,16 @@ def _final_validate(config):
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
fv = full_config.get()
|
||||
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
|
||||
config = config.copy()
|
||||
for c in config:
|
||||
if not c.get(CONF_BYTE_ORDER):
|
||||
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
config = config.copy()
|
||||
for c in config:
|
||||
if byte_order := c.get(CONF_BYTE_ORDER):
|
||||
if byte_order == "BIG_ENDIAN":
|
||||
_LOGGER.warning(
|
||||
"The image '%s' is configured with big-endian byte order, little-endian is expected",
|
||||
c.get(CONF_FILE),
|
||||
)
|
||||
else:
|
||||
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
return config
|
||||
|
||||
|
||||
@@ -753,7 +756,7 @@ async def write_image(config, all_frames=False):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
encoder.end_image()
|
||||
encoder.end_image()
|
||||
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
|
||||
@@ -766,32 +766,38 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
|
||||
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
|
||||
|
||||
void LD2412Component::set_basic_config() {
|
||||
uint8_t min_gate = 1;
|
||||
uint8_t max_gate = TOTAL_GATES;
|
||||
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
|
||||
uint8_t out_pin_level = 0x01;
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() ||
|
||||
!this->timeout_number_->has_state()) {
|
||||
return;
|
||||
if (this->min_distance_gate_number_ != nullptr) {
|
||||
if (!this->min_distance_gate_number_->has_state())
|
||||
return;
|
||||
min_gate = static_cast<int>(this->min_distance_gate_number_->state);
|
||||
}
|
||||
if (this->max_distance_gate_number_ != nullptr) {
|
||||
if (!this->max_distance_gate_number_->has_state())
|
||||
return;
|
||||
max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1;
|
||||
}
|
||||
if (this->timeout_number_ != nullptr) {
|
||||
if (!this->timeout_number_->has_state())
|
||||
return;
|
||||
timeout = static_cast<int>(this->timeout_number_->state);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (!this->out_pin_level_select_->has_state()) {
|
||||
return;
|
||||
if (this->out_pin_level_select_ != nullptr) {
|
||||
if (!this->out_pin_level_select_->has_state())
|
||||
return;
|
||||
out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t value[5] = {
|
||||
#ifdef USE_NUMBER
|
||||
lowbyte(static_cast<int>(this->min_distance_gate_number_->state)),
|
||||
lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1),
|
||||
lowbyte(static_cast<int>(this->timeout_number_->state)),
|
||||
highbyte(static_cast<int>(this->timeout_number_->state)),
|
||||
#else
|
||||
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
|
||||
#else
|
||||
0x01, // Default value if not using select
|
||||
#endif
|
||||
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
|
||||
};
|
||||
this->set_config_mode_(true);
|
||||
this->send_command_(CMD_BASIC_CONF, value, sizeof(value));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -24,6 +25,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.storage_json import StorageJSON
|
||||
|
||||
from . import gpio # noqa
|
||||
@@ -465,6 +467,11 @@ async def component_to_code(config):
|
||||
# it for project source files only. GCC uses the last -O flag.
|
||||
build_src_flags += " -Os"
|
||||
cg.add_platformio_option("build_src_flags", build_src_flags)
|
||||
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
|
||||
# On other families, patch_linker.py routes .sram.text into the right
|
||||
# RAM-executable output section and prints a post-link placement summary.
|
||||
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
|
||||
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
|
||||
# dummy version code
|
||||
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
|
||||
# decrease web server stack size (16k words -> 4k words)
|
||||
@@ -549,3 +556,13 @@ async def component_to_code(config):
|
||||
_configure_lwip(config)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files() -> None:
|
||||
script_dir = Path(__file__).parent
|
||||
patch_linker_file = script_dir / "patch_linker.py.script"
|
||||
copy_file_if_changed(
|
||||
patch_linker_file,
|
||||
CORE.relative_build_path("patch_linker.py"),
|
||||
)
|
||||
|
||||
@@ -79,6 +79,11 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
'''
|
||||
|
||||
BASE_CODE_BOARDS = '''
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family
|
||||
# section routed into RAM-executable memory (see esphome/core/hal.h).
|
||||
#
|
||||
# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK
|
||||
# masks FIQ+IRQ around flash writes). On the remaining families:
|
||||
# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it.
|
||||
# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it.
|
||||
# - LN882H: stock linker has no glob for ".sram.text", so we inject
|
||||
# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH).
|
||||
#
|
||||
# All families also get a post-link summary showing where IRAM_ATTR landed.
|
||||
|
||||
|
||||
_MARKER = "/* esphome .sram.text */"
|
||||
# Strong assignments (not PROVIDE) so the symbols are always emitted in the
|
||||
# ELF; PROVIDE symbols with no references can be garbage-collected.
|
||||
_KEEP_LINE = (
|
||||
" __esphome_sram_text_start = .; "
|
||||
"KEEP(*(.sram.text*)) "
|
||||
"__esphome_sram_text_end = .; "
|
||||
+ _MARKER + "\n"
|
||||
)
|
||||
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
|
||||
|
||||
|
||||
def _detect(env):
|
||||
prefix = "USE_LIBRETINY_VARIANT_"
|
||||
# CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds
|
||||
# the raw "-DNAME" strings. PlatformIO populates both, but the exact order
|
||||
# vs. extra_scripts varies, so check both to be robust.
|
||||
for token in env.get("CPPDEFINES", []):
|
||||
if isinstance(token, (list, tuple)):
|
||||
token = token[0]
|
||||
if isinstance(token, str) and token.startswith(prefix):
|
||||
return token[len(prefix):]
|
||||
for flag in env.get("BUILD_FLAGS", []):
|
||||
if isinstance(flag, str) and "-D" + prefix in flag:
|
||||
name = flag.split("-D", 1)[1].split("=", 1)[0].strip()
|
||||
if name.startswith(prefix):
|
||||
return name[len(prefix):]
|
||||
return None
|
||||
|
||||
|
||||
KNOWN_VARIANTS = frozenset({
|
||||
"LN882H",
|
||||
"RTL8710B",
|
||||
"RTL8720C",
|
||||
})
|
||||
|
||||
|
||||
def _inject_keep(host_section):
|
||||
"""Return a patcher that injects _KEEP_LINE at the top of `host_section`."""
|
||||
def patch(content):
|
||||
if _MARKER in content:
|
||||
return content
|
||||
return host_section.sub(r"\1" + _KEEP_LINE, content, count=1)
|
||||
return patch
|
||||
|
||||
|
||||
# Variants not listed here intentionally have no .ld patcher:
|
||||
# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker
|
||||
# already routes into .ram_image2.text (> BD_RAM).
|
||||
# - RTL8720C: stock linker already consumes *(.sram.text*).
|
||||
# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op.
|
||||
_PATCHERS_BY_VARIANT = {
|
||||
"LN882H": (_inject_keep(_LN_COPY),),
|
||||
}
|
||||
|
||||
|
||||
def _patchers_for(variant):
|
||||
return _PATCHERS_BY_VARIANT.get(variant, ())
|
||||
|
||||
|
||||
def _pre_link(target, source, env):
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")]
|
||||
patched = 0
|
||||
for name in ld_files:
|
||||
path = os.path.join(build_dir, name)
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
original = fh.read()
|
||||
if _MARKER in original:
|
||||
patched += 1
|
||||
continue
|
||||
content = original
|
||||
for fn in _patchers:
|
||||
content = fn(content)
|
||||
if content != original:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
print("ESPHome: patched {} for IRAM_ATTR placement".format(name))
|
||||
patched += 1
|
||||
if not patched:
|
||||
raise RuntimeError(
|
||||
"ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the "
|
||||
"regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format(
|
||||
build_dir
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Substrings matched against demangled names as a fallback on RTL8720C,
|
||||
# where we cannot inject __esphome_sram_text_start/end markers.
|
||||
_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe",
|
||||
"enable_loop_soon_any_context")
|
||||
|
||||
|
||||
def _post_link(target, source, env):
|
||||
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
|
||||
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
|
||||
if not os.path.isfile(elf):
|
||||
return
|
||||
nm = env.subst("$NM")
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[nm, "--defined-only", "--demangle", elf], text=True
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc))
|
||||
return
|
||||
start = end = None
|
||||
fallback = []
|
||||
for line in out.splitlines():
|
||||
parts = line.split(maxsplit=2)
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
addr_str, _kind, name = parts
|
||||
if name == "__esphome_sram_text_start":
|
||||
start = int(addr_str, 16)
|
||||
elif name == "__esphome_sram_text_end":
|
||||
end = int(addr_str, 16)
|
||||
elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS):
|
||||
fallback.append(int(addr_str, 16))
|
||||
print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant))
|
||||
if start is not None and end is not None:
|
||||
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
|
||||
elif fallback:
|
||||
lo, hi = min(fallback), max(fallback)
|
||||
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
|
||||
else:
|
||||
print(" no IRAM_ATTR symbols found")
|
||||
|
||||
|
||||
if (_variant := _detect(env)) is None:
|
||||
raise RuntimeError(
|
||||
"ESPHome: could not determine LibreTiny variant from build flags. "
|
||||
"patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR "
|
||||
"into SRAM; without it, ISR handlers would silently end up in flash."
|
||||
)
|
||||
if _variant not in KNOWN_VARIANTS:
|
||||
raise RuntimeError(
|
||||
"ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not "
|
||||
"know how to route IRAM_ATTR into SRAM for this family. Update "
|
||||
"patch_linker.py.script before shipping firmware.".format(_variant)
|
||||
)
|
||||
|
||||
if _patchers := _patchers_for(_variant):
|
||||
# LibreTiny writes the processed .ld templates into $BUILD_DIR during its
|
||||
# own builder setup, which may run after this script. Register the patch
|
||||
# as a pre-link action so it executes once the linker scripts exist.
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link)
|
||||
|
||||
# Post-link summary for every family that reaches this script.
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)
|
||||
@@ -222,7 +222,7 @@ class LightCall {
|
||||
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
|
||||
|
||||
// Helper to set flag - defaults to true for common case
|
||||
void set_flag_(FieldFlags flag, bool value = true) {
|
||||
void set_flag_(FieldFlags flag, bool value = true) ESPHOME_ALWAYS_INLINE {
|
||||
if (value) {
|
||||
this->flags_ |= flag;
|
||||
} else {
|
||||
@@ -231,7 +231,7 @@ class LightCall {
|
||||
}
|
||||
|
||||
// Helper to clear flag - reduces code size for common case
|
||||
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
|
||||
void clear_flag_(FieldFlags flag) ESPHOME_ALWAYS_INLINE { this->flags_ &= ~flag; }
|
||||
|
||||
// Helper to log unsupported feature and clear flag - reduces code duplication
|
||||
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -35,9 +35,11 @@ LockStateForwarder = lock_ns.class_("LockStateForwarder")
|
||||
LockState = lock_ns.enum("LockState")
|
||||
|
||||
LOCK_STATES = {
|
||||
"OPEN": LockState.LOCK_STATE_OPEN,
|
||||
"LOCKED": LockState.LOCK_STATE_LOCKED,
|
||||
"UNLOCKED": LockState.LOCK_STATE_UNLOCKED,
|
||||
"JAMMED": LockState.LOCK_STATE_JAMMED,
|
||||
"OPENING": LockState.LOCK_STATE_OPENING,
|
||||
"LOCKING": LockState.LOCK_STATE_LOCKING,
|
||||
"UNLOCKING": LockState.LOCK_STATE_UNLOCKING,
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ namespace esphome::lock {
|
||||
|
||||
static const char *const TAG = "lock";
|
||||
|
||||
// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING
|
||||
// Lock state strings indexed by LockState enum.
|
||||
// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range
|
||||
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING");
|
||||
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING", "OPENING",
|
||||
"OPEN");
|
||||
|
||||
const LogString *lock_state_to_string(LockState state) {
|
||||
return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0);
|
||||
@@ -74,12 +75,16 @@ LockCall &LockCall::set_state(optional<LockState> state) {
|
||||
return *this;
|
||||
}
|
||||
LockCall &LockCall::set_state(const char *state) {
|
||||
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
|
||||
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPEN")) == 0) {
|
||||
this->set_state(LOCK_STATE_OPEN);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
|
||||
this->set_state(LOCK_STATE_LOCKED);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) {
|
||||
this->set_state(LOCK_STATE_UNLOCKED);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) {
|
||||
this->set_state(LOCK_STATE_JAMMED);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPENING")) == 0) {
|
||||
this->set_state(LOCK_STATE_OPENING);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) {
|
||||
this->set_state(LOCK_STATE_LOCKING);
|
||||
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) {
|
||||
|
||||
@@ -26,7 +26,9 @@ enum LockState : uint8_t {
|
||||
LOCK_STATE_UNLOCKED = 2,
|
||||
LOCK_STATE_JAMMED = 3,
|
||||
LOCK_STATE_LOCKING = 4,
|
||||
LOCK_STATE_UNLOCKING = 5
|
||||
LOCK_STATE_UNLOCKING = 5,
|
||||
LOCK_STATE_OPENING = 6,
|
||||
LOCK_STATE_OPEN = 7,
|
||||
};
|
||||
const LogString *lock_state_to_string(LockState state);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
|
||||
uint8_t buffer[num_bytes];
|
||||
|
||||
// Wait until data available
|
||||
constexpr uint32_t max_wait_ms = 25;
|
||||
const uint32_t now = millis();
|
||||
while (true) {
|
||||
std::bitset<8> status = this->reg(LTR390_MAIN_STATUS).get();
|
||||
@@ -52,12 +53,12 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
|
||||
if (available)
|
||||
break;
|
||||
|
||||
if (millis() - now > 100) {
|
||||
if (millis() - now > max_wait_ms) {
|
||||
ESP_LOGW(TAG, "Sensor didn't return any data, aborting");
|
||||
return {};
|
||||
}
|
||||
ESP_LOGD(TAG, "Waiting for data");
|
||||
delay(2);
|
||||
ESP_LOGV(TAG, "Waiting for data");
|
||||
delay(1);
|
||||
}
|
||||
|
||||
if (!this->read_bytes(MODEADDRESSES[mode], buffer, num_bytes)) {
|
||||
|
||||
@@ -89,10 +89,12 @@
|
||||
id: hello_world_label_
|
||||
text: "Hello World!"
|
||||
align: center
|
||||
- obj:
|
||||
- container:
|
||||
id: hello_world_qrcode_
|
||||
outline_width: 0
|
||||
border_width: 0
|
||||
height: 100
|
||||
width: 100
|
||||
hidden: !lambda |-
|
||||
return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400;
|
||||
widgets:
|
||||
|
||||
@@ -642,26 +642,28 @@ void LvglComponent::write_random_() {
|
||||
int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
|
||||
if (iterations <= 0)
|
||||
iterations = 1;
|
||||
int16_t width = lv_display_get_horizontal_resolution(this->disp_);
|
||||
int16_t height = lv_display_get_vertical_resolution(this->disp_);
|
||||
while (iterations-- != 0) {
|
||||
int32_t col = random_uint32() % this->width_;
|
||||
int32_t col = random_uint32() % width;
|
||||
col = col / this->draw_rounding * this->draw_rounding;
|
||||
int32_t row = random_uint32() % this->height_;
|
||||
int32_t row = random_uint32() % height;
|
||||
row = row / this->draw_rounding * this->draw_rounding;
|
||||
// size will be between 8 and 32, and a multiple of draw_rounding
|
||||
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
|
||||
lv_area_t area{col, row, col + size - 1, row + size - 1};
|
||||
lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
|
||||
// clip to display bounds just in case
|
||||
if (area.x2 >= this->width_)
|
||||
area.x2 = this->width_ - 1;
|
||||
if (area.y2 >= this->height_)
|
||||
area.y2 = this->height_ - 1;
|
||||
if (area.x2 >= width)
|
||||
area.x2 = width - 1;
|
||||
if (area.y2 >= height)
|
||||
area.y2 = height - 1;
|
||||
|
||||
// line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
|
||||
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
|
||||
for (size_t i = 0; i != line_len; i++) {
|
||||
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
|
||||
reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
|
||||
}
|
||||
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
|
||||
this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,16 +76,23 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
|
||||
// Shortcut / overload, so that the source of an image can easily be updated
|
||||
// from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#if LV_USE_IMAGE
|
||||
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#endif // LV_USE_IMAGE
|
||||
|
||||
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
|
||||
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
|
||||
}
|
||||
inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) {
|
||||
::lv_style_set_bg_image_src(style, image->get_lv_image_dsc());
|
||||
}
|
||||
inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) {
|
||||
::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc());
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -77,8 +77,11 @@ class ArcType(NumberType):
|
||||
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
|
||||
prop = str(prop)
|
||||
if prop.endswith("_angle"):
|
||||
prop = "bg_" + prop
|
||||
await w.set_property(prop, config, processor=validator)
|
||||
await w.set_property(
|
||||
"bg_" + prop, await validator.process(config.get(prop))
|
||||
)
|
||||
else:
|
||||
await w.set_property(prop, config, processor=validator)
|
||||
if CONF_ADJUSTABLE in config:
|
||||
if not config[CONF_ADJUSTABLE]:
|
||||
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)
|
||||
|
||||
@@ -52,19 +52,23 @@ class KeyboardType(WidgetType):
|
||||
if mode := config.get(CONF_MODE):
|
||||
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
|
||||
if textarea := config.get(CONF_TEXTAREA):
|
||||
# If a textarea is configured, it must be generated before the keyboard can attach it.
|
||||
# If not yet configured, defer the attachment code.
|
||||
if not is_widget_completed(textarea):
|
||||
# Can only happen for an initial config, where the keyboard is configured before the
|
||||
# textarea, so it's ok to always emit into the global context
|
||||
async def add_textarea():
|
||||
async with LvContext():
|
||||
await w.set_property(
|
||||
CONF_TEXTAREA,
|
||||
(await get_widgets(config, CONF_TEXTAREA))[0].obj,
|
||||
)
|
||||
|
||||
async def add_textarea():
|
||||
async with LvContext():
|
||||
await w.set_property(
|
||||
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
|
||||
)
|
||||
|
||||
if is_widget_completed(textarea):
|
||||
await add_textarea()
|
||||
else:
|
||||
CORE.add_job(add_textarea)
|
||||
else:
|
||||
# Handles updates in automations, and properly ordered initial config. Code is generated
|
||||
# into the enclosing context (main or lambda)
|
||||
await w.set_property(
|
||||
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
|
||||
)
|
||||
|
||||
|
||||
keyboard_spec = KeyboardType()
|
||||
|
||||
@@ -454,12 +454,12 @@ async def to_code(config):
|
||||
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
|
||||
|
||||
esp32.add_idf_component(name="esphome/esp-micro-speech-features", ref="1.2.3")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
cg.add_build_flag("-DESP_NN")
|
||||
|
||||
cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0")
|
||||
|
||||
if vad_model := config.get(CONF_VAD):
|
||||
cg.add_define("USE_MICRO_WAKE_WORD_VAD")
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
|
||||
# Standalone display
|
||||
# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html
|
||||
DriverChip(
|
||||
"SEEED-RETERMINAL-D1001",
|
||||
height=1280,
|
||||
width=800,
|
||||
hsync_back_porch=20,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=40,
|
||||
vsync_back_porch=12,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
|
||||
reset_pin={"xl9535": None, "number": 2},
|
||||
initsequence=(
|
||||
(0xE0, 0x00),
|
||||
(0xE1, 0x93),
|
||||
(0xE2, 0x65),
|
||||
(0xE3, 0xF8),
|
||||
(0x80, 0x01),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.config_validation import UNDEFINED
|
||||
|
||||
# fmt: off
|
||||
sunton = DriverChip(
|
||||
"ESP32-8048S070",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
pclk_frequency="12.5MHz",
|
||||
de_pin=41,
|
||||
hsync_pin=39,
|
||||
vsync_pin=40,
|
||||
pclk_pin=42,
|
||||
hsync_pulse_width=30,
|
||||
hsync_back_porch=16,
|
||||
hsync_front_porch=210,
|
||||
vsync_pulse_width=13,
|
||||
vsync_back_porch=10,
|
||||
vsync_front_porch=22,
|
||||
data_pins={
|
||||
"red": [14, 21, 47, 48, 45],
|
||||
"green": [9, 46, 3, 8, 16, 1],
|
||||
"blue": [15, 7, 6, 5, 4],
|
||||
},
|
||||
)
|
||||
|
||||
sunton.extend(
|
||||
"ESP32-8048S050",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
pclk_frequency="16MHz",
|
||||
de_pin=40,
|
||||
hsync_pin=39,
|
||||
vsync_pin=41,
|
||||
pclk_pin=42,
|
||||
hsync_back_porch=8,
|
||||
hsync_front_porch=8,
|
||||
hsync_pulse_width=4,
|
||||
vsync_back_porch=8,
|
||||
vsync_front_porch=8,
|
||||
vsync_pulse_width=4,
|
||||
data_pins={
|
||||
"red": [45, 48, 47, 21, 14],
|
||||
"green": [5, 6, 7, 15, 16, 4],
|
||||
"blue": [8, 3, 46, 9, 1],
|
||||
},
|
||||
)
|
||||
@@ -195,7 +195,7 @@ def model_schema(config):
|
||||
"big_endian", "little_endian", lower=True
|
||||
),
|
||||
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
|
||||
*pixel_modes, lower=True
|
||||
),
|
||||
@@ -297,9 +297,9 @@ def _final_validate(config):
|
||||
|
||||
buffer_size = color_depth // 8 * width * height // frac
|
||||
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||
fraction = min(20000.0, buffer_size // 4) / buffer_size
|
||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
(x for x in range(2, 8) if fraction >= 1 / x), 8
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -546,13 +546,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
}
|
||||
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||
// the display height,
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
|
||||
this->start_line_ += this->get_height_internal() / FRACTION) {
|
||||
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
|
||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto lap = millis();
|
||||
#endif
|
||||
this->end_line_ =
|
||||
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
|
||||
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
@@ -574,12 +573,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
// Some chips require that the drawing window be aligned on certain boundaries
|
||||
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
|
||||
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
|
||||
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
|
||||
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
|
||||
this->y_low_ - this->start_line_,
|
||||
round_buffer(this->get_width_internal()) - w - this->x_low_);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->get_width_internal();
|
||||
this->y_low_ = this->get_height_internal();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .ili import ILI9341, ILI9342, ST7789V
|
||||
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
|
||||
|
||||
from .ili import GC9A01A, ILI9341, ILI9342, ST7789V
|
||||
|
||||
ILI9341.extend(
|
||||
# ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller
|
||||
@@ -43,3 +45,10 @@ ILI9342.extend(
|
||||
(0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction
|
||||
)
|
||||
)
|
||||
|
||||
GC9A01A.extend(
|
||||
"ESP32-2424S012",
|
||||
invert_colors=True,
|
||||
cs_pin=10,
|
||||
dc_pin={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True},
|
||||
)
|
||||
|
||||
@@ -555,7 +555,7 @@ ST7789V = DriverChip(
|
||||
),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
GC9A01A = DriverChip(
|
||||
"GC9A01A",
|
||||
mirror_x=True,
|
||||
width=240,
|
||||
|
||||
@@ -5,6 +5,29 @@ namespace esphome::modbus::helpers {
|
||||
|
||||
static const char *const TAG = "modbus_helpers";
|
||||
|
||||
static size_t required_payload_size(SensorValueType sensor_value_type) {
|
||||
switch (sensor_value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::S_WORD:
|
||||
return 2;
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::FP32:
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
return 4;
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
return 8;
|
||||
case SensorValueType::RAW:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
|
||||
switch (value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
@@ -47,93 +70,70 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
|
||||
uint32_t bitmask) {
|
||||
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
|
||||
|
||||
if (offset > data.size()) {
|
||||
ESP_LOGE(TAG, "not enough data for value");
|
||||
// Validate offset against the buffer for all types, including RAW/unsupported, so
|
||||
// a malformed or misconfigured frame still produces an error log.
|
||||
if (static_cast<size_t>(offset) > data.size()) {
|
||||
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
|
||||
static_cast<unsigned int>(offset), data.size());
|
||||
return value;
|
||||
}
|
||||
|
||||
const size_t required_size = required_payload_size(sensor_value_type);
|
||||
if (required_size == 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (data.size() - offset < required_size) {
|
||||
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
|
||||
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
|
||||
required_size);
|
||||
return value;
|
||||
}
|
||||
|
||||
size_t size = data.size() - offset;
|
||||
bool error = false;
|
||||
switch (sensor_value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
if (size >= 2) {
|
||||
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset),
|
||||
bitmask); // default is 0xFFFF ;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
|
||||
break;
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::FP32:
|
||||
if (size >= 4) {
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
break;
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::FP32_R:
|
||||
if (size >= 4) {
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
break;
|
||||
case SensorValueType::S_WORD:
|
||||
if (size >= 2) {
|
||||
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
|
||||
bitmask); // default is 0xFFFF ;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), bitmask); // default is 0xFFFF ;
|
||||
break;
|
||||
case SensorValueType::S_DWORD:
|
||||
if (size >= 4) {
|
||||
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
|
||||
break;
|
||||
case SensorValueType::S_DWORD_R: {
|
||||
if (size >= 4) {
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
// Currently the high word is at the low position
|
||||
// the sign bit is therefore at low before the switch
|
||||
uint32_t sign_bit = (value & 0x8000) << 16;
|
||||
value = mask_and_shift_by_rightbit(
|
||||
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
// Currently the high word is at the low position
|
||||
// the sign bit is therefore at low before the switch
|
||||
uint32_t sign_bit = (value & 0x8000) << 16;
|
||||
value = mask_and_shift_by_rightbit(
|
||||
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
|
||||
} break;
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
// Ignore bitmask for QWORD
|
||||
if (size >= 8) {
|
||||
value = get_data<uint64_t>(data, offset);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint64_t>(data, offset);
|
||||
break;
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R: {
|
||||
// Ignore bitmask for QWORD
|
||||
if (size >= 8) {
|
||||
uint64_t tmp = get_data<uint64_t>(data, offset);
|
||||
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
uint64_t tmp = get_data<uint64_t>(data, offset);
|
||||
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
|
||||
} break;
|
||||
case SensorValueType::RAW:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (error)
|
||||
ESP_LOGE(TAG, "not enough data for value");
|
||||
return value;
|
||||
}
|
||||
} // namespace esphome::modbus::helpers
|
||||
|
||||
@@ -8,8 +8,11 @@ from typing import Any
|
||||
from esphome import git, yaml_util
|
||||
from esphome.components.substitutions import (
|
||||
ContextVars,
|
||||
ErrList,
|
||||
push_context,
|
||||
raise_first_undefined,
|
||||
resolve_include,
|
||||
resolve_substitutions_block,
|
||||
substitute,
|
||||
)
|
||||
from esphome.components.substitutions.jinja import has_jinja
|
||||
@@ -359,12 +362,19 @@ def _substitute_package_definition(
|
||||
if isinstance(package_config, str) or (
|
||||
isinstance(package_config, dict) and is_remote_package(package_config)
|
||||
):
|
||||
# Collect undefined-variable errors (rather than raising strict) so the
|
||||
# path walked through a remote-package dict is preserved and the user
|
||||
# sees which field (url / path / ref / ...) referenced the undefined
|
||||
# variable.
|
||||
errors: ErrList = []
|
||||
package_config = substitute(
|
||||
item=package_config,
|
||||
path=[],
|
||||
parent_context=context_vars or ContextVars(),
|
||||
strict_undefined=False,
|
||||
errors=errors,
|
||||
)
|
||||
raise_first_undefined(errors, package_config, "package definition")
|
||||
return package_config
|
||||
|
||||
|
||||
@@ -516,7 +526,12 @@ def do_packages_pass(
|
||||
if CONF_PACKAGES not in config:
|
||||
return config
|
||||
|
||||
substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {}))
|
||||
with cv.prepend_path(CONF_SUBSTITUTIONS):
|
||||
substitutions = UserDict(
|
||||
resolve_substitutions_block(
|
||||
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
|
||||
)
|
||||
)
|
||||
processor = _PackageProcessor(
|
||||
substitutions, command_line_substitutions, skip_update
|
||||
)
|
||||
|
||||
@@ -24,6 +24,8 @@ static const uint8_t QMC5883L_REGISTER_CONTROL_1 = 0x09;
|
||||
static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A;
|
||||
static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B;
|
||||
|
||||
void IRAM_ATTR QMC5883LComponent::gpio_intr(QMC5883LComponent *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void QMC5883LComponent::setup() {
|
||||
// Soft Reset
|
||||
if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) {
|
||||
@@ -35,6 +37,12 @@ void QMC5883LComponent::setup() {
|
||||
|
||||
if (this->drdy_pin_) {
|
||||
this->drdy_pin_->setup();
|
||||
if (this->drdy_pin_->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(this->drdy_pin_)
|
||||
->attach_interrupt(&QMC5883LComponent::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
this->drdy_use_isr_ = true;
|
||||
this->stop_poller();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t control_1 = 0;
|
||||
@@ -65,8 +73,8 @@ void QMC5883LComponent::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->get_update_interval() < App.get_loop_interval()) {
|
||||
high_freq_.start();
|
||||
if (!this->drdy_use_isr_ && this->get_update_interval() < App.get_loop_interval()) {
|
||||
this->high_freq_.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,23 +92,39 @@ void QMC5883LComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Heading", this->heading_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_PIN(" DRDY Pin: ", this->drdy_pin_);
|
||||
if (this->drdy_pin_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " DRDY mode: %s",
|
||||
this->drdy_use_isr_ ? LOG_STR_LITERAL("interrupt") : LOG_STR_LITERAL("polling"));
|
||||
}
|
||||
}
|
||||
|
||||
void QMC5883LComponent::update() {
|
||||
i2c::ErrorCode err;
|
||||
uint8_t status = false;
|
||||
|
||||
// If DRDY pin is configured and the data is not ready return.
|
||||
// If DRDY is on an external expander we keep the polling path and early-return
|
||||
// if data is not ready yet. Internal DRDY pins take the ISR path via loop().
|
||||
if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
this->read_sensor_();
|
||||
}
|
||||
|
||||
void QMC5883LComponent::loop() {
|
||||
this->disable_loop();
|
||||
if (!this->drdy_use_isr_ || !this->drdy_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
this->read_sensor_();
|
||||
}
|
||||
|
||||
void QMC5883LComponent::read_sensor_() {
|
||||
i2c::ErrorCode err;
|
||||
uint8_t status = false;
|
||||
|
||||
// Status byte gets cleared when data is read, so we have to read this first.
|
||||
// If status and two axes are desired, it's possible to save one byte of traffic by enabling
|
||||
// ROL_PNT in setup and reading 7 bytes starting at the status register.
|
||||
// If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
|
||||
// But simply not reading status saves you 4 bytes always and is much simpler.
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
char buf[32];
|
||||
@@ -165,7 +189,7 @@ void QMC5883LComponent::update() {
|
||||
temp = int16_t(raw_temp) * 0.01f;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
|
||||
ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
|
||||
temp, status);
|
||||
|
||||
if (this->x_sensor_ != nullptr)
|
||||
|
||||
@@ -32,6 +32,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; }
|
||||
void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; }
|
||||
@@ -44,6 +45,9 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
|
||||
void read_sensor_();
|
||||
|
||||
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
|
||||
QMC5883LRange range_{QMC5883L_RANGE_200_UT};
|
||||
QMC5883LOversampling oversampling_{QMC5883L_SAMPLING_512};
|
||||
@@ -53,6 +57,7 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
sensor::Sensor *heading_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
GPIOPin *drdy_pin_{nullptr};
|
||||
bool drdy_use_isr_{false};
|
||||
enum ErrorCode {
|
||||
NONE = 0,
|
||||
COMMUNICATION_FAILED,
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -294,57 +294,59 @@ void Rtttl::play(std::string rtttl) {
|
||||
}
|
||||
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
|
||||
|
||||
// Get default duration
|
||||
this->position_ = this->rtttl_.find("d=", this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing 'd='");
|
||||
return;
|
||||
}
|
||||
this->position_ += 2;
|
||||
num = this->get_integer_();
|
||||
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
|
||||
this->default_note_denominator_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default duration: %d", num);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get default octave
|
||||
this->position_ = this->rtttl_.find("o=", this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing 'o=");
|
||||
return;
|
||||
}
|
||||
this->position_ += 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
|
||||
this->default_octave_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default octave: %d", num);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get BPM
|
||||
this->position_ = this->rtttl_.find("b=", this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing b=");
|
||||
return;
|
||||
}
|
||||
this->position_ += 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
|
||||
bpm = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid BPM: %d", num);
|
||||
return;
|
||||
}
|
||||
|
||||
this->position_ = this->rtttl_.find(':', this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
size_t name_end_position = this->position_;
|
||||
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
|
||||
if (control_end == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing second ':'");
|
||||
return;
|
||||
}
|
||||
this->position_++;
|
||||
|
||||
// Get default duration
|
||||
size_t pos = this->rtttl_.find("d=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
num = this->get_integer_();
|
||||
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
|
||||
this->default_note_denominator_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default duration: %d", num);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get default octave
|
||||
pos = this->rtttl_.find("o=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
|
||||
this->default_octave_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default octave: %d", num);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get BPM
|
||||
pos = this->rtttl_.find("b=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
|
||||
bpm = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid BPM: %d", num);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->position_ = control_end + 1;
|
||||
|
||||
// BPM usually expresses the number of quarter notes per minute
|
||||
this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)
|
||||
|
||||
@@ -127,9 +127,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
this->buffer_[pos + 0] = mapped_color.r;
|
||||
this->buffer_[pos + 0] = mapped_color.b;
|
||||
this->buffer_[pos + 1] = mapped_color.g;
|
||||
this->buffer_[pos + 2] = mapped_color.b;
|
||||
this->buffer_[pos + 2] = mapped_color.r;
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
}
|
||||
|
||||
@@ -32,40 +32,101 @@ void RuntimeStatsCollector::log_stats_() {
|
||||
" Period stats (last %" PRIu32 "ms): %zu active components",
|
||||
this->log_interval_, count);
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
// Sum component time so we can derive main-loop overhead
|
||||
// (active loop time minus time attributable to component loop()s).
|
||||
// Period sum iterates the active-in-period subset; total sum must iterate
|
||||
// all components since total_active_time_us_ includes iterations where
|
||||
// currently-idle components previously ran.
|
||||
uint64_t period_component_sum_us = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
period_component_sum_us += sorted[i]->runtime_stats_.period_time_us;
|
||||
}
|
||||
uint64_t total_component_sum_us = 0;
|
||||
for (auto *component : components) {
|
||||
total_component_sum_us += component->runtime_stats_.total_time_us;
|
||||
}
|
||||
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(sorted, sorted + count, compare_period_time);
|
||||
if (count > 0) {
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(sorted, sorted + count, compare_period_time);
|
||||
|
||||
// Log top components by period runtime
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
|
||||
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
|
||||
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
|
||||
// Log top components by period runtime
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
|
||||
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
|
||||
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Main-loop overhead for the period: active wall time minus component time.
|
||||
// active = sum of per-iteration loop time excluding yield/sleep.
|
||||
if (this->period_active_count_ > 0) {
|
||||
uint64_t active = this->period_active_time_us_;
|
||||
uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0;
|
||||
// Use double for µs→ms conversion so multi-day uptimes (where total
|
||||
// microsecond counters exceed float's ~7-digit mantissa) keep resolution.
|
||||
ESP_LOGI(TAG,
|
||||
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
|
||||
"overhead_total=%.1fms",
|
||||
this->period_active_count_,
|
||||
static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0,
|
||||
static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
|
||||
static_cast<double>(overhead) / 1000.0);
|
||||
uint64_t before = this->period_before_time_us_;
|
||||
uint64_t tail = this->period_tail_time_us_;
|
||||
uint64_t accounted = before + tail;
|
||||
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
|
||||
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
|
||||
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
|
||||
static_cast<double>(inter) / 1000.0);
|
||||
}
|
||||
|
||||
// Log total stats since boot (only for active components - idle ones haven't changed)
|
||||
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
|
||||
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
if (count > 0) {
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
|
||||
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
|
||||
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
|
||||
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
|
||||
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->total_active_count_ > 0) {
|
||||
uint64_t active = this->total_active_time_us_;
|
||||
uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0;
|
||||
ESP_LOGI(TAG,
|
||||
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
|
||||
"overhead_total=%.1fms",
|
||||
this->total_active_count_,
|
||||
static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0,
|
||||
static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
|
||||
static_cast<double>(overhead) / 1000.0);
|
||||
uint64_t before = this->total_before_time_us_;
|
||||
uint64_t tail = this->total_tail_time_us_;
|
||||
uint64_t accounted = before + tail;
|
||||
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
|
||||
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
|
||||
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
|
||||
static_cast<double>(inter) / 1000.0);
|
||||
}
|
||||
|
||||
// Reset period stats
|
||||
for (auto *component : components) {
|
||||
component->runtime_stats_.reset_period();
|
||||
}
|
||||
this->period_active_count_ = 0;
|
||||
this->period_active_time_us_ = 0;
|
||||
this->period_active_max_us_ = 0;
|
||||
this->period_before_time_us_ = 0;
|
||||
this->period_tail_time_us_ = 0;
|
||||
}
|
||||
|
||||
bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) {
|
||||
@@ -76,11 +137,12 @@ bool RuntimeStatsCollector::compare_total_time(Component *a, Component *b) {
|
||||
return a->runtime_stats_.total_time_us > b->runtime_stats_.total_time_us;
|
||||
}
|
||||
|
||||
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
|
||||
if ((int32_t) (current_time - this->next_log_time_) >= 0) {
|
||||
this->log_stats_();
|
||||
this->next_log_time_ = current_time + this->log_interval_;
|
||||
}
|
||||
// Slow path for process_pending_stats — gate already checked by the inline
|
||||
// wrapper in runtime_stats.h. Out-of-line keeps the log_stats_ machinery out
|
||||
// of Application::loop().
|
||||
void RuntimeStatsCollector::process_pending_stats_slow_(uint32_t current_time) {
|
||||
this->log_stats_();
|
||||
this->next_log_time_ = current_time + this->log_interval_;
|
||||
}
|
||||
|
||||
} // namespace runtime_stats
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -26,10 +27,46 @@ class RuntimeStatsCollector {
|
||||
}
|
||||
uint32_t get_log_interval() const { return this->log_interval_; }
|
||||
|
||||
// Process any pending stats printing (should be called after component loop)
|
||||
void process_pending_stats(uint32_t current_time);
|
||||
// Process any pending stats printing. Called on every Application::loop()
|
||||
// tick, so the common "not yet time to log" path must be cheap — inline
|
||||
// the gate check and keep the actual logging work out-of-line.
|
||||
void ESPHOME_ALWAYS_INLINE process_pending_stats(uint32_t current_time) {
|
||||
if ((int32_t) (current_time - this->next_log_time_) >= 0) [[unlikely]] {
|
||||
this->process_pending_stats_slow_(current_time);
|
||||
}
|
||||
}
|
||||
|
||||
// Record the wall time of one main loop iteration excluding the yield/sleep.
|
||||
// Called once per loop from Application::loop().
|
||||
// active_us = total time between loop start and just before yield.
|
||||
// before_us = time spent in Phase A (scheduler tick) excluding time
|
||||
// already attributed to per-component stats.
|
||||
// tail_us = time spent in after_component_phase_ + the trailing record/stats
|
||||
// prefix. Only meaningful on component-phase ticks; reported
|
||||
// as 0 on Phase A-only ticks (no component phase ran, so any
|
||||
// overhead between Phase A and stats belongs to "residual").
|
||||
// Residual overhead at log time = active − Σ(component) − before − tail,
|
||||
// which captures per-iteration inter-component bookkeeping (set_current_component,
|
||||
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
|
||||
// the for-loop itself).
|
||||
void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) {
|
||||
this->period_active_count_++;
|
||||
this->period_active_time_us_ += active_us;
|
||||
if (active_us > this->period_active_max_us_)
|
||||
this->period_active_max_us_ = active_us;
|
||||
this->total_active_count_++;
|
||||
this->total_active_time_us_ += active_us;
|
||||
if (active_us > this->total_active_max_us_)
|
||||
this->total_active_max_us_ = active_us;
|
||||
|
||||
this->period_before_time_us_ += before_us;
|
||||
this->total_before_time_us_ += before_us;
|
||||
this->period_tail_time_us_ += tail_us;
|
||||
this->total_tail_time_us_ += tail_us;
|
||||
}
|
||||
|
||||
protected:
|
||||
void process_pending_stats_slow_(uint32_t current_time);
|
||||
void log_stats_();
|
||||
// Static comparators — member functions have friend access, lambdas do not
|
||||
static bool compare_period_time(Component *a, Component *b);
|
||||
@@ -37,6 +74,22 @@ class RuntimeStatsCollector {
|
||||
|
||||
uint32_t log_interval_;
|
||||
uint32_t next_log_time_{0};
|
||||
|
||||
// Main loop active-time stats (wall time per iteration, excluding yield/sleep).
|
||||
// Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in
|
||||
// a few weeks of uptime, which is well within ESPHome device lifetimes.
|
||||
uint64_t period_active_count_{0};
|
||||
uint64_t period_active_time_us_{0};
|
||||
uint32_t period_active_max_us_{0};
|
||||
uint64_t total_active_count_{0};
|
||||
uint64_t total_active_time_us_{0};
|
||||
uint32_t total_active_max_us_{0};
|
||||
|
||||
// Split of overhead sections — accumulated per iteration.
|
||||
uint64_t period_before_time_us_{0};
|
||||
uint64_t total_before_time_us_{0};
|
||||
uint64_t period_tail_time_us_{0};
|
||||
uint64_t total_tail_time_us_{0};
|
||||
};
|
||||
|
||||
} // namespace runtime_stats
|
||||
|
||||
@@ -118,6 +118,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
@@ -275,6 +276,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityFilter", ValueListFilter
|
||||
)
|
||||
ThrottleWithPriorityNanFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityNanFilter", Filter
|
||||
)
|
||||
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
|
||||
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
|
||||
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
|
||||
@@ -290,6 +294,7 @@ SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
|
||||
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
|
||||
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
|
||||
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
|
||||
RoundSignificantDigitsFilter = sensor_ns.class_("RoundSignificantDigitsFilter", Filter)
|
||||
|
||||
validate_unit_of_measurement = cv.All(
|
||||
cv.string_strict,
|
||||
@@ -656,9 +661,18 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
THROTTLE_WITH_PRIORITY_SCHEMA,
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
if not isinstance(config[CONF_VALUE], list):
|
||||
config[CONF_VALUE] = [config[CONF_VALUE]]
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in config[CONF_VALUE]]
|
||||
values = config[CONF_VALUE]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
# Specialize the common "NaN-only" case (the schema default when the user
|
||||
# omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the
|
||||
# generic ValueListFilter path requires. Behavior is identical: NaN sensor
|
||||
# readings always bypass the throttle.
|
||||
if values and all(isinstance(v, float) and math.isnan(v) for v in values):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
|
||||
)
|
||||
@@ -888,6 +902,18 @@ async def round_multiple_filter_to_code(config, filter_id):
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"round_to_significant_digits",
|
||||
RoundSignificantDigitsFilter,
|
||||
cv.int_range(min=1, max=6),
|
||||
)
|
||||
async def round_significant_digits_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
cg.TemplateArguments(config),
|
||||
)
|
||||
|
||||
|
||||
async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
@@ -269,6 +269,18 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
|
||||
return {};
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityNanFilter
|
||||
ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs)
|
||||
: min_time_between_inputs_(min_time_between_inputs) {}
|
||||
optional<float> ThrottleWithPriorityNanFilter::new_value(float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// DeltaFilter
|
||||
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
|
||||
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
|
||||
|
||||
@@ -399,6 +399,19 @@ template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N>
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Specialization of ThrottleWithPriorityFilter for the common "prioritize NaN"
|
||||
/// case: skips the TemplatableFn<float> array + lambda and inlines the check.
|
||||
class ThrottleWithPriorityNanFilter : public Filter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
// Base class for timeout filters - contains common loop logic
|
||||
class TimeoutFilterBase : public Filter, public Component {
|
||||
public:
|
||||
@@ -591,6 +604,19 @@ class RoundMultipleFilter : public Filter {
|
||||
float multiple_;
|
||||
};
|
||||
|
||||
template<uint8_t Digits> class RoundSignificantDigitsFilter : public Filter {
|
||||
public:
|
||||
optional<float> new_value(float value) override {
|
||||
if (std::isfinite(value)) {
|
||||
if (value == 0.0f)
|
||||
return 0.0f;
|
||||
float factor = pow10_int(Digits - 1 - ilog10(value));
|
||||
return roundf(value * factor) / factor;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
class ToNTCResistanceFilter : public Filter {
|
||||
public:
|
||||
ToNTCResistanceFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {}
|
||||
|
||||
@@ -14,38 +14,34 @@ BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
|
||||
if (!monitor_loop || this->fd_ < 0)
|
||||
return;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
|
||||
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
|
||||
this->loop_monitored_ = App.register_socket(this->cached_sock_);
|
||||
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
|
||||
#else
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
#endif
|
||||
}
|
||||
|
||||
BSDSocketImpl::~BSDSocketImpl() {
|
||||
if (!this->closed_) {
|
||||
this->close();
|
||||
}
|
||||
}
|
||||
BSDSocketImpl::~BSDSocketImpl() { this->close(); }
|
||||
|
||||
int BSDSocketImpl::close() {
|
||||
if (!this->closed_) {
|
||||
// Unregister before closing to avoid dangling pointer in monitored set
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket(this->cached_sock_);
|
||||
this->cached_sock_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
if (this->fd_ < 0) {
|
||||
// Already closed, or never opened.
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Null the cached lwip_sock pointer before closing. The underlying lwip slot can be
|
||||
// recycled for a new connection as soon as ::close() returns, so anything that might
|
||||
// dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would otherwise
|
||||
// touch an unrelated socket's pcb. No per-socket callback unhook is needed —
|
||||
// all LwIP sockets share the same static event_callback.
|
||||
this->cached_sock_ = nullptr;
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(this->fd_);
|
||||
this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible.
|
||||
return ret;
|
||||
}
|
||||
|
||||
int BSDSocketImpl::setblocking(bool blocking) {
|
||||
|
||||
@@ -119,12 +119,21 @@ class BSDSocketImpl {
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
protected:
|
||||
// fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This
|
||||
// replaces a separate closed_ flag: close() sets fd_ = -1 after ::close(), and the
|
||||
// destructor / double-close path just check fd_ < 0.
|
||||
int fd_{-1};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
|
||||
#endif
|
||||
bool closed_{false};
|
||||
// Cached lwip_sock pointer used for direct rcvevent reads in ready() on the
|
||||
// fast-select path. Replaces loop_monitored_: null means this socket is not being
|
||||
// monitored for read events — either monitoring was not requested, the fd was
|
||||
// invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event
|
||||
// callback was hooked and notifications are flowing. close() nulls this to prevent
|
||||
// use-after-free via a recycled lwip slot.
|
||||
struct lwip_sock *cached_sock_{nullptr};
|
||||
#else
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
@@ -14,38 +14,34 @@ LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
|
||||
if (!monitor_loop || this->fd_ < 0)
|
||||
return;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
|
||||
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
|
||||
this->loop_monitored_ = App.register_socket(this->cached_sock_);
|
||||
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
|
||||
#else
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
#endif
|
||||
}
|
||||
|
||||
LwIPSocketImpl::~LwIPSocketImpl() {
|
||||
if (!this->closed_) {
|
||||
this->close();
|
||||
}
|
||||
}
|
||||
LwIPSocketImpl::~LwIPSocketImpl() { this->close(); }
|
||||
|
||||
int LwIPSocketImpl::close() {
|
||||
if (!this->closed_) {
|
||||
// Unregister before closing to avoid dangling pointer in monitored set
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket(this->cached_sock_);
|
||||
this->cached_sock_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
if (this->fd_ < 0) {
|
||||
// Already closed, or never opened.
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Null the cached lwip_sock pointer before closing. The underlying lwip slot can be
|
||||
// recycled for a new connection as soon as lwip_close() returns, so anything that
|
||||
// might dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would
|
||||
// otherwise touch an unrelated socket's pcb. No per-socket callback unhook is needed —
|
||||
// all LwIP sockets share the same static event_callback.
|
||||
this->cached_sock_ = nullptr;
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible.
|
||||
return ret;
|
||||
}
|
||||
|
||||
int LwIPSocketImpl::setblocking(bool blocking) {
|
||||
|
||||
@@ -85,12 +85,21 @@ class LwIPSocketImpl {
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
protected:
|
||||
// fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This
|
||||
// replaces a separate closed_ flag: close() sets fd_ = -1 after lwip_close(), and the
|
||||
// destructor / double-close path just check fd_ < 0.
|
||||
int fd_{-1};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
|
||||
#endif
|
||||
bool closed_{false};
|
||||
// Cached lwip_sock pointer used for direct rcvevent reads in ready() on the
|
||||
// fast-select path. Replaces loop_monitored_: null means this socket is not being
|
||||
// monitored for read events — either monitoring was not requested, the fd was
|
||||
// invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event
|
||||
// callback was hooked and notifications are flowing. close() nulls this to prevent
|
||||
// use-after-free via a recycled lwip slot.
|
||||
struct lwip_sock *cached_sock_{nullptr};
|
||||
#else
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
@@ -42,8 +42,23 @@ using ListenSocket = LWIPRawListenImpl;
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read.
|
||||
inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) {
|
||||
return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock));
|
||||
/// cached_sock == nullptr means the socket is not monitored (monitor_loop was false, fd
|
||||
/// was invalid, or esphome_lwip_get_sock() failed) — in that case return true so the
|
||||
/// caller attempts the read and handles blocking itself.
|
||||
inline bool socket_ready(struct lwip_sock *cached_sock) {
|
||||
return cached_sock == nullptr || esphome_lwip_socket_has_data(cached_sock);
|
||||
}
|
||||
|
||||
/// Resolve an fd to its lwip_sock and install the netconn event-callback hook so the
|
||||
/// main loop is woken by FreeRTOS task notifications when data arrives. Shared between
|
||||
/// BSD and LwIP socket impls on the fast-select path. Returns the cached lwip_sock
|
||||
/// pointer (or nullptr if the fd does not map to a valid lwip_sock).
|
||||
inline struct lwip_sock *hook_fd_for_fast_select(int fd) {
|
||||
struct lwip_sock *sock = esphome_lwip_get_sock(fd);
|
||||
if (sock != nullptr) {
|
||||
esphome_lwip_hook_socket(sock);
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
#elif defined(USE_HOST)
|
||||
/// Shared ready() helper for fd-based socket implementations.
|
||||
@@ -69,7 +84,7 @@ bool socket_ready_fd(int fd, bool loop_monitored);
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
inline bool Socket::ready() const {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
return socket_ready(this->cached_sock_, this->loop_monitored_);
|
||||
return socket_ready(this->cached_sock_);
|
||||
#else
|
||||
return socket_ready_fd(this->fd_, this->loop_monitored_);
|
||||
#endif
|
||||
|
||||
@@ -30,6 +30,56 @@ ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]]
|
||||
jinja = Jinja()
|
||||
|
||||
|
||||
def raise_first_undefined(
|
||||
errors: ErrList,
|
||||
source: Any,
|
||||
context_label: str,
|
||||
) -> None:
|
||||
"""If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable.
|
||||
|
||||
The raised error names the missing variable, the path walked into *source*
|
||||
(for nested dicts, e.g. ``url`` or ``ref``), and the YAML source location
|
||||
when *source* carries one. Only the first error is surfaced; the user will
|
||||
re-run after fixing it and any remaining undefined variables will be
|
||||
reported then.
|
||||
|
||||
``context_label`` is the noun describing where the undefined variable
|
||||
appeared (e.g. ``"package definition"``).
|
||||
"""
|
||||
if not errors:
|
||||
return
|
||||
err, err_path, err_value = errors[0]
|
||||
if len(errors) > 1:
|
||||
# Log any further undefined variables so debug-level output covers
|
||||
# the full set, even though only the first is surfaced to the user.
|
||||
extras = ", ".join(
|
||||
f"{e.message} at '{'->'.join(str(p) for p in p_path)}'"
|
||||
for e, p_path, _ in errors[1:]
|
||||
)
|
||||
_LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras)
|
||||
# Prefer the location of the offending scalar (e.g. the `url:` value) over
|
||||
# the enclosing package-definition dict so the message points at the exact
|
||||
# line/column that carries the undefined variable.
|
||||
location_node = (
|
||||
err_value
|
||||
if isinstance(err_value, ESPHomeDataBase) and err_value.esp_range is not None
|
||||
else source
|
||||
)
|
||||
location = ""
|
||||
if (
|
||||
isinstance(location_node, ESPHomeDataBase)
|
||||
and location_node.esp_range is not None
|
||||
):
|
||||
mark = location_node.esp_range.start_mark
|
||||
# DocumentLocation.line/column are 0-based (from the YAML Mark). Render
|
||||
# as 1-based to match config.line_info() and editor line numbering.
|
||||
location = f" (in {mark.document} {mark.line + 1}:{mark.column + 1})"
|
||||
field = f" at '{'->'.join(str(p) for p in err_path)}'" if err_path else ""
|
||||
raise cv.Invalid(
|
||||
f"Undefined variable in {context_label}{field}: {err.message}{location}"
|
||||
)
|
||||
|
||||
|
||||
def validate_substitution_key(value: Any) -> str:
|
||||
"""Validate and normalize a substitution key, stripping a leading ``$`` if present."""
|
||||
value = cv.string(value)
|
||||
@@ -414,6 +464,34 @@ def _warn_unresolved_variables(errors: ErrList) -> None:
|
||||
)
|
||||
|
||||
|
||||
def resolve_substitutions_block(
|
||||
substitutions: Any,
|
||||
command_line_substitutions: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape.
|
||||
|
||||
The caller is responsible for wrapping the call in
|
||||
``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting.
|
||||
``command_line_substitutions`` seeds the filename context so
|
||||
``substitutions: !include ${var}.yaml`` can reference CLI-provided vars.
|
||||
"""
|
||||
if isinstance(substitutions, IncludeFile):
|
||||
# Single-shot resolution — matches ``_walk_packages`` for the
|
||||
# ``packages: !include`` entry point. Chained includes (an include that
|
||||
# itself loads another ``!include`` at the top level) are not supported.
|
||||
substitutions, _ = resolve_include(
|
||||
substitutions,
|
||||
[],
|
||||
ContextVars(command_line_substitutions or {}),
|
||||
strict_undefined=False,
|
||||
)
|
||||
if not isinstance(substitutions, dict):
|
||||
raise cv.Invalid(
|
||||
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
|
||||
)
|
||||
return substitutions
|
||||
|
||||
|
||||
def do_substitution_pass(
|
||||
config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None
|
||||
) -> OrderedDict:
|
||||
@@ -429,10 +507,9 @@ def do_substitution_pass(
|
||||
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
|
||||
substitutions = config.pop(CONF_SUBSTITUTIONS, {})
|
||||
with cv.prepend_path(CONF_SUBSTITUTIONS):
|
||||
if not isinstance(substitutions, dict):
|
||||
raise cv.Invalid(
|
||||
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
|
||||
)
|
||||
substitutions = resolve_substitutions_block(
|
||||
substitutions, command_line_substitutions
|
||||
)
|
||||
substitutions = merge_dicts_ordered(
|
||||
substitutions, command_line_substitutions or {}
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import text
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INITIAL_VALUE,
|
||||
CONF_LAMBDA,
|
||||
CONF_MAX_LENGTH,
|
||||
@@ -12,6 +13,7 @@ from esphome.const import (
|
||||
CONF_RESTORE_VALUE,
|
||||
CONF_SET_ACTION,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
from .. import template_ns
|
||||
|
||||
@@ -84,8 +86,15 @@ async def to_code(config):
|
||||
if initial_value_config := config.get(CONF_INITIAL_VALUE):
|
||||
cg.add(var.set_initial_value(initial_value_config))
|
||||
if config[CONF_RESTORE_VALUE]:
|
||||
args = cg.TemplateArguments(config[CONF_MAX_LENGTH])
|
||||
saver = TextSaverTemplate.template(args).new()
|
||||
saver_id = ID(
|
||||
f"{config[CONF_ID].id}_value_saver",
|
||||
is_declaration=True,
|
||||
type=TextSaverBase,
|
||||
)
|
||||
saver_type = TextSaverTemplate.template(
|
||||
cg.TemplateArguments(config[CONF_MAX_LENGTH])
|
||||
)
|
||||
saver = cg.Pvariable(saver_id, saver_type.new())
|
||||
cg.add(var.set_value_saver(saver))
|
||||
|
||||
if CONF_SET_ACTION in config:
|
||||
|
||||
@@ -308,6 +308,7 @@ bool CompactString::operator==(const StringRef &other) const {
|
||||
/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │
|
||||
/// └──────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
|
||||
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
|
||||
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
|
||||
if (phase == WiFiRetryPhase::INITIAL_CONNECT)
|
||||
@@ -326,6 +327,7 @@ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
|
||||
return LOG_STR("RESTARTING");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
|
||||
|
||||
bool WiFiComponent::went_through_explicit_hidden_phase_() const {
|
||||
// If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||
|
||||
@@ -137,7 +137,7 @@ async def to_code(config):
|
||||
# the '+1' modifier is relative to the device's own address that will
|
||||
# be automatically added to the provided list.
|
||||
cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}")
|
||||
cg.add_library("droscy/esp_wireguard", "0.4.4")
|
||||
cg.add_library("droscy/esp_wireguard", "0.4.5")
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -101,8 +101,10 @@ void ZWaveProxy::loop() {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void ZWaveProxy::process_uart_() {
|
||||
while (this->available()) {
|
||||
void ZWaveProxy::process_uart_slow_() {
|
||||
// Caller (inline process_uart_) has already confirmed available() > 0, so use do/while to
|
||||
// drain bytes — available() is still checked at the tail, but not redundantly on entry.
|
||||
do {
|
||||
uint8_t byte;
|
||||
if (!this->read_byte(&byte)) {
|
||||
this->status_set_warning(LOG_STR("UART read failed"));
|
||||
@@ -137,7 +139,7 @@ void ZWaveProxy::process_uart_() {
|
||||
this->api_connection_->send_message(this->outgoing_proto_msg_);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (this->available());
|
||||
}
|
||||
|
||||
void ZWaveProxy::dump_config() {
|
||||
@@ -414,7 +416,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ZWaveProxy::response_handler_() {
|
||||
bool ZWaveProxy::response_handler_slow_() {
|
||||
switch (this->parsing_state_) {
|
||||
case ZWAVE_PARSING_STATE_SEND_ACK:
|
||||
this->last_response_ = ZWAVE_FRAME_TYPE_ACK;
|
||||
|
||||
@@ -38,6 +38,13 @@ enum ZWaveParsingState : uint8_t {
|
||||
ZWAVE_PARSING_STATE_READ_BL_MENU,
|
||||
};
|
||||
|
||||
// response_handler_()'s inline fast-path relies on SEND_ACK/CAN/NAK being contiguous in this
|
||||
// enum so a single range check (state - SEND_ACK < 3) is equivalent to three equality checks.
|
||||
static_assert(ZWAVE_PARSING_STATE_SEND_CAN == ZWAVE_PARSING_STATE_SEND_ACK + 1,
|
||||
"SEND_CAN must immediately follow SEND_ACK for response_handler_ fast-path");
|
||||
static_assert(ZWAVE_PARSING_STATE_SEND_NAK == ZWAVE_PARSING_STATE_SEND_ACK + 2,
|
||||
"SEND_NAK must immediately follow SEND_CAN for response_handler_ fast-path");
|
||||
|
||||
enum ZWaveProxyFeature : uint32_t {
|
||||
FEATURE_ZWAVE_PROXY_ENABLED = 1 << 0,
|
||||
};
|
||||
@@ -72,8 +79,31 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
||||
void send_simple_command_(uint8_t command_id);
|
||||
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
|
||||
void parse_start_(uint8_t byte);
|
||||
bool response_handler_();
|
||||
void process_uart_(); // Process all available UART data
|
||||
// Inline fast-path: most calls happen with parsing_state_ outside the SEND_* range, so skip the
|
||||
// out-of-line call entirely in the hot path (e.g. every loop() tick) and only pay for the real
|
||||
// work when a response is actually pending. ESPHOME_ALWAYS_INLINE is required because with -Os
|
||||
// gcc otherwise clones the wrapper into a shared $isra$ outline and keeps the call8.
|
||||
ESPHOME_ALWAYS_INLINE bool response_handler_() {
|
||||
if (this->parsing_state_ < ZWAVE_PARSING_STATE_SEND_ACK || this->parsing_state_ > ZWAVE_PARSING_STATE_SEND_NAK) {
|
||||
return false;
|
||||
}
|
||||
return this->response_handler_slow_();
|
||||
}
|
||||
bool response_handler_slow_();
|
||||
// Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most backends).
|
||||
// On an idle loop tick we want to skip the call to process_uart_ entirely. When bytes are
|
||||
// pending we fall into the slow path, which drains the UART with a do/while so available() is
|
||||
// only checked once per byte — no redundant re-check on entry.
|
||||
ESPHOME_ALWAYS_INLINE void process_uart_() {
|
||||
if (!this->available()) {
|
||||
return;
|
||||
}
|
||||
this->process_uart_slow_();
|
||||
}
|
||||
// Precondition: caller must guarantee available() > 0 before invoking (see inline
|
||||
// process_uart_ above). The slow path uses do/while and would otherwise set a spurious UART
|
||||
// warning on entry if called with no bytes pending.
|
||||
void process_uart_slow_();
|
||||
|
||||
// Pre-allocated message - always ready to send
|
||||
api::ZWaveProxyFrame outgoing_proto_msg_;
|
||||
|
||||
@@ -943,7 +943,26 @@ def time_period_in_minutes_(value):
|
||||
def update_interval(value):
|
||||
if value == "never":
|
||||
return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
|
||||
return positive_time_period_milliseconds(value)
|
||||
result = positive_time_period_milliseconds(value)
|
||||
# 0ms was historically (mis)used as a pseudo-loop() mechanism for
|
||||
# PollingComponents. Under the hood it calls set_interval(0), which
|
||||
# causes Scheduler::call() to spin (WDT reset in the field). Coerce
|
||||
# to 1ms so existing configs keep working at ~1kHz instead of
|
||||
# spinning. Don't hard-fail so configs don't break on upgrade;
|
||||
# authors should migrate to HighFrequencyLoopRequester (C++) for
|
||||
# true run-every-loop behaviour.
|
||||
if result.total_milliseconds == 0:
|
||||
_LOGGER.warning(
|
||||
"update_interval of 0ms is not supported - coercing to 1ms. "
|
||||
"A literal 0ms schedule would spin the main loop (the scheduled "
|
||||
"item would always be due, so the scheduler would never yield "
|
||||
"back) and trigger a watchdog reset. Set update_interval to a "
|
||||
"non-zero value such as 1ms or higher. (Custom C++ components "
|
||||
"that need true run-every-loop behaviour should override loop() "
|
||||
"with HighFrequencyLoopRequester instead.)"
|
||||
)
|
||||
return TimePeriodMilliseconds(milliseconds=1)
|
||||
return result
|
||||
|
||||
|
||||
time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// --- String helpers ---
|
||||
|
||||
std::string str_truncate(const std::string &str, size_t length) {
|
||||
return str.length() > length ? str.substr(0, length) : str;
|
||||
}
|
||||
|
||||
std::string str_until(const char *str, char ch) {
|
||||
const char *pos = strchr(str, ch);
|
||||
return pos == nullptr ? std::string(str) : std::string(str, pos - str);
|
||||
}
|
||||
std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); }
|
||||
|
||||
// wrapper around std::transform to run safely on functions from the ctype.h header
|
||||
// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes
|
||||
template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.length());
|
||||
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); });
|
||||
return result;
|
||||
}
|
||||
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
|
||||
|
||||
std::string str_upper_case(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.length());
|
||||
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return std::toupper(ch); });
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string str_snake_case(const std::string &str) {
|
||||
std::string result = str;
|
||||
for (char &c : result) {
|
||||
c = to_snake_case_char(c);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string str_sanitize(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.size());
|
||||
str_sanitize_to(&result[0], str.size() + 1, str.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string str_snprintf(const char *fmt, size_t len, ...) {
|
||||
std::string str;
|
||||
va_list args;
|
||||
|
||||
str.resize(len);
|
||||
va_start(args, len);
|
||||
size_t out_length = vsnprintf(&str[0], len + 1, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (out_length < len)
|
||||
str.resize(out_length);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string str_sprintf(const char *fmt, ...) {
|
||||
std::string str;
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
size_t length = vsnprintf(nullptr, 0, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
str.resize(length);
|
||||
va_start(args, fmt);
|
||||
vsnprintf(&str[0], length + 1, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
// --- Value formatting helpers ---
|
||||
|
||||
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
value_accuracy_to_buf(buf, value, accuracy_decimals);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// --- Base64 helpers ---
|
||||
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Encode 3 input bytes to 4 base64 characters, append 'count' to ret.
|
||||
static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) {
|
||||
char char_array_4[4];
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
|
||||
}
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
|
||||
|
||||
std::string base64_encode(const uint8_t *buf, size_t buf_len) {
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
char char_array_3[3];
|
||||
|
||||
while (buf_len--) {
|
||||
char_array_3[i++] = *(buf++);
|
||||
if (i == 3) {
|
||||
base64_encode_triple(char_array_3, 4, ret);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
|
||||
base64_encode_triple(char_array_3, i + 1, ret);
|
||||
|
||||
while ((i++ < 3))
|
||||
ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
|
||||
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
|
||||
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
|
||||
std::vector<uint8_t> ret(max_len);
|
||||
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
|
||||
ret.resize(actual_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// --- Hex/binary formatting helpers ---
|
||||
|
||||
std::string format_mac_address_pretty(const uint8_t *mac) {
|
||||
char buf[18];
|
||||
format_mac_addr_upper(mac, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string format_hex(const uint8_t *data, size_t length) {
|
||||
std::string ret;
|
||||
ret.resize(length * 2);
|
||||
format_hex_to(&ret[0], length * 2 + 1, data, length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
|
||||
// Shared implementation for uint8_t and string hex pretty formatting
|
||||
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
size_t hex_len = separator ? (length * 3 - 1) : (length * 2);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
return format_hex_pretty_uint8(data, length, separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
size_t hex_len = separator ? (length * 5 - 1) : (length * 4);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
||||
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
|
||||
}
|
||||
|
||||
std::string format_bin(const uint8_t *data, size_t length) {
|
||||
std::string result;
|
||||
result.resize(length * 8);
|
||||
format_bin_to(&result[0], length * 8 + 1, data, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- MAC address helpers ---
|
||||
|
||||
std::string get_mac_address() {
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
char buf[13];
|
||||
format_mac_addr_lower_no_sep(mac, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string get_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(get_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
/// @file alloc_helpers.h
|
||||
/// @brief Heap-allocating helper functions.
|
||||
///
|
||||
/// These functions return std::string and allocate heap memory on every call.
|
||||
/// On long-running embedded devices, repeated heap allocations fragment memory
|
||||
/// over time, eventually causing crashes even with free memory available.
|
||||
///
|
||||
/// Prefer the stack-based alternatives documented on each function instead.
|
||||
/// New code should avoid using these functions.
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// --- String helpers (allocating) ---
|
||||
|
||||
/// Truncate a string to a specific length.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_truncate(const std::string &str, size_t length);
|
||||
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end
|
||||
/// (requires str to be null-terminated).
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_until(const char *str, char ch);
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_until(const std::string &str, char ch);
|
||||
|
||||
/// Convert the string to lower case.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_lower_case(const std::string &str);
|
||||
|
||||
/// Convert the string to upper case.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_upper_case(const std::string &str);
|
||||
|
||||
/// Convert the string to snake case (lowercase with underscores).
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_snake_case(const std::string &str);
|
||||
|
||||
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
|
||||
/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead.
|
||||
std::string str_sanitize(const std::string &str);
|
||||
|
||||
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
|
||||
|
||||
/// sprintf-like function returning std::string.
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
|
||||
|
||||
// --- Hex/binary formatting helpers (allocating) ---
|
||||
|
||||
/// Format the six-byte array \p mac into a MAC address string.
|
||||
/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead.
|
||||
std::string format_mac_address_pretty(const uint8_t mac[6]);
|
||||
|
||||
/// Format the byte array \p data of length \p len in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
std::string format_hex(const uint8_t *data, size_t length);
|
||||
|
||||
/// Format the vector \p data in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
std::string format_hex(const std::vector<uint8_t> &data);
|
||||
|
||||
/// Format a byte array in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a 16-bit word array in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a byte vector in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a 16-bit word vector in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a string's bytes in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format the byte array \p data of length \p len in binary.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
std::string format_bin(const uint8_t *data, size_t length);
|
||||
|
||||
// --- Value formatting helpers (allocating) ---
|
||||
|
||||
/// Format a float value with accuracy decimals to a string.
|
||||
/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.
|
||||
__attribute__((deprecated("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.")))
|
||||
std::string
|
||||
value_accuracy_to_string(float value, int8_t accuracy_decimals);
|
||||
|
||||
// --- Base64 helpers (allocating) ---
|
||||
|
||||
/// Encode a byte buffer to base64 string.
|
||||
/// @warning Allocates heap memory.
|
||||
std::string base64_encode(const uint8_t *buf, size_t buf_len);
|
||||
/// Encode a byte vector to base64 string.
|
||||
/// @warning Allocates heap memory.
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf);
|
||||
|
||||
/// Decode a base64 string to a byte vector.
|
||||
/// @warning Allocates heap memory. Use base64_decode(data, len, buf, buf_len) with a pre-allocated buffer instead.
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
|
||||
|
||||
// --- MAC address helpers (allocating) ---
|
||||
|
||||
/// Get the device MAC address as a string, in lowercase hex notation.
|
||||
/// @warning Allocates heap memory. Use get_mac_address_into_buffer() instead.
|
||||
std::string get_mac_address();
|
||||
|
||||
/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
|
||||
/// @warning Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead.
|
||||
std::string get_mac_address_pretty();
|
||||
|
||||
} // namespace esphome
|
||||
@@ -93,8 +93,11 @@ void Application::setup() {
|
||||
do {
|
||||
uint32_t now = millis();
|
||||
|
||||
// Process pending loop enables to handle GPIO interrupts during setup
|
||||
this->before_loop_tasks_(now);
|
||||
// Service scheduler and process pending loop enables to handle GPIO
|
||||
// interrupts during setup. During setup we always run the component
|
||||
// phase (no loop_interval_ gate), so call both helpers unconditionally.
|
||||
this->scheduler_tick_(now);
|
||||
this->before_component_phase_();
|
||||
|
||||
for (uint32_t j = 0; j <= i; j++) {
|
||||
// Update loop_component_start_time_ right before calling each component
|
||||
@@ -103,7 +106,7 @@ void Application::setup() {
|
||||
this->feed_wdt();
|
||||
}
|
||||
|
||||
this->after_loop_tasks_();
|
||||
this->after_component_phase_();
|
||||
yield();
|
||||
} while (!component->can_proceed() && !component->is_failed());
|
||||
}
|
||||
@@ -211,11 +214,16 @@ void Application::process_dump_config_() {
|
||||
|
||||
void Application::feed_wdt() {
|
||||
// Cold entry: callers without a millis() timestamp in hand. Fetches the
|
||||
// time and takes the same rate-limit path as feed_wdt_with_time().
|
||||
// time and takes the same rate-limit paths as feed_wdt_with_time().
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
|
||||
this->feed_wdt_slow_(now);
|
||||
}
|
||||
#ifdef USE_STATUS_LED
|
||||
if (now - this->last_status_led_service_ > STATUS_LED_DISPATCH_INTERVAL_MS) {
|
||||
this->service_status_led_slow_(now);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
@@ -223,27 +231,36 @@ void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
|
||||
arch_feed_wdt();
|
||||
this->last_wdt_feed_ = time;
|
||||
#ifdef USE_STATUS_LED
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
auto *sl = status_led::global_status_led;
|
||||
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
|
||||
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
|
||||
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
|
||||
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
|
||||
// its setup() has already run. Re-dispatch only if an error or warning bit has been
|
||||
// set since; otherwise skip entirely.
|
||||
if ((this->app_state_ & STATUS_LED_MASK) == 0)
|
||||
return;
|
||||
sl->enable_loop();
|
||||
} else if (sl_state != COMPONENT_STATE_LOOP) {
|
||||
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
|
||||
return;
|
||||
}
|
||||
sl->loop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
void HOT Application::service_status_led_slow_(uint32_t time) {
|
||||
// Callers (feed_wdt(), feed_wdt_with_time()) have already confirmed the
|
||||
// STATUS_LED_DISPATCH_INTERVAL_MS rate limit was exceeded. Rate-limited
|
||||
// separately from arch_feed_wdt() so the LED blink pattern stays readable
|
||||
// (status_led error blink period is 250 ms) while HAL watchdog pokes can
|
||||
// still run at the much coarser WDT_FEED_INTERVAL_MS cadence.
|
||||
this->last_status_led_service_ = time;
|
||||
if (status_led::global_status_led == nullptr)
|
||||
return;
|
||||
auto *sl = status_led::global_status_led;
|
||||
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
|
||||
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
|
||||
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
|
||||
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
|
||||
// its setup() has already run. Re-dispatch only if an error or warning bit has been
|
||||
// set since; otherwise skip entirely.
|
||||
if ((this->app_state_ & STATUS_LED_MASK) == 0)
|
||||
return;
|
||||
sl->enable_loop();
|
||||
} else if (sl_state != COMPONENT_STATE_LOOP) {
|
||||
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
|
||||
return;
|
||||
}
|
||||
sl->loop();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Application::any_component_has_status_flag_(uint8_t flag) const {
|
||||
// Walk all components (not just looping ones) so non-looping components'
|
||||
// status bits are respected. Only called from the slow-path clear helpers
|
||||
@@ -495,32 +512,7 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
bool Application::register_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
if (sock == nullptr)
|
||||
return false;
|
||||
esphome_lwip_hook_socket(sock);
|
||||
this->monitored_sockets_.push_back(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::unregister_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
|
||||
if (this->monitored_sockets_[i] != sock)
|
||||
continue;
|
||||
|
||||
// Swap with last element and pop - O(1) removal since order doesn't matter.
|
||||
// No need to unhook the netconn callback — all LwIP sockets share the same
|
||||
// static event_callback, and the socket will be closed by the caller.
|
||||
if (i < this->monitored_sockets_.size() - 1)
|
||||
this->monitored_sockets_[i] = this->monitored_sockets_.back();
|
||||
this->monitored_sockets_.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#elif defined(USE_HOST)
|
||||
#ifdef USE_HOST
|
||||
bool Application::register_socket_fd(int fd) {
|
||||
// WARNING: This function is NOT thread-safe and must only be called from the main loop
|
||||
// It modifies socket_fds_ and related variables without locking
|
||||
|
||||
+200
-85
@@ -229,23 +229,50 @@ class Application {
|
||||
|
||||
void schedule_dump_config() { this->dump_config_at_ = 0; }
|
||||
|
||||
/// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the
|
||||
/// rate of HAL pokes low while still being small enough that any plausible
|
||||
/// watchdog timeout (seconds) has orders of magnitude of safety margin.
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3;
|
||||
/// Minimum interval between real arch_feed_wdt() calls. Sized so the outer
|
||||
/// feed in Application::loop() is effectively rate-limited across both the
|
||||
/// normal ~62 Hz cadence and worst-case wake-storm scenarios (e.g. external
|
||||
/// stacks like OpenThread posting frequent wake notifications). Component
|
||||
/// loops and scheduler items still feed after every op, so any op exceeding
|
||||
/// this threshold triggers a real feed naturally.
|
||||
/// Safety margins vs. platform watchdog timeouts:
|
||||
/// - ESP32 task WDT default (5 s): ~16x
|
||||
/// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change
|
||||
/// must keep comfortable margin here
|
||||
/// - ESP8266 HW WDT (~6 s): ~20x
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300;
|
||||
|
||||
/// Feed the task watchdog. Cold entry — callers without a millis()
|
||||
/// timestamp in hand. Out of line to keep call sites tiny.
|
||||
void feed_wdt();
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
/// Dispatch interval for the status LED update. Deliberately shorter than
|
||||
/// WDT_FEED_INTERVAL_MS because the status LED error blink has a 250 ms
|
||||
/// period (status_led.cpp:ERROR_PERIOD_MS) and a 150 ms on-window; the
|
||||
/// dispatch cadence must be short enough to render that blink without
|
||||
/// aliasing. Sampling every 100 ms yields an on/off observation inside
|
||||
/// every error period with headroom for the 250 ms warning on-window.
|
||||
static constexpr uint32_t STATUS_LED_DISPATCH_INTERVAL_MS = 100;
|
||||
#endif
|
||||
|
||||
/// Feed the task watchdog, hot entry. Callers that already have a
|
||||
/// millis() timestamp pay only a load + sub + branch on the common
|
||||
/// (no-op) path. The actual arch feed + status LED update live in
|
||||
/// feed_wdt_slow_.
|
||||
/// (no-op) path. The actual arch feed lives in feed_wdt_slow_.
|
||||
/// When USE_STATUS_LED is compiled in, also gates a separate (shorter)
|
||||
/// interval for dispatching status_led so the LED blink pattern stays
|
||||
/// readable even though arch_feed_wdt pokes are now rate-limited at
|
||||
/// WDT_FEED_INTERVAL_MS. The two rate limits are independent so raising
|
||||
/// WDT_FEED_INTERVAL_MS does not distort the LED cadence.
|
||||
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
|
||||
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
|
||||
this->feed_wdt_slow_(time);
|
||||
}
|
||||
#ifdef USE_STATUS_LED
|
||||
if (static_cast<uint32_t>(time - this->last_status_led_service_) > STATUS_LED_DISPATCH_INTERVAL_MS) [[unlikely]] {
|
||||
this->service_status_led_slow_(time);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void reboot();
|
||||
@@ -318,16 +345,13 @@ class Application {
|
||||
|
||||
Scheduler scheduler;
|
||||
|
||||
/// Register/unregister a socket to be monitored for read events.
|
||||
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
/// Fast select path: hooks netconn callback and registers for monitoring.
|
||||
/// @return true if registration was successful, false if sock is null
|
||||
bool register_socket(struct lwip_sock *sock);
|
||||
void unregister_socket(struct lwip_sock *sock);
|
||||
#elif defined(USE_HOST)
|
||||
/// Fallback select() path: monitors file descriptors.
|
||||
#ifdef USE_HOST
|
||||
/// Register/unregister a socket file descriptor with the host select() fallback loop.
|
||||
/// USE_LWIP_FAST_SELECT builds do not use this API — sockets hook the lwIP netconn
|
||||
/// event_callback directly (see socket.h hook_fd_for_fast_select) and rely on FreeRTOS
|
||||
/// task notifications for wake-up.
|
||||
/// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error.
|
||||
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
|
||||
/// @return true if registration was successful, false if fd exceeds limits
|
||||
bool register_socket_fd(int fd);
|
||||
void unregister_socket_fd(int fd);
|
||||
@@ -337,9 +361,12 @@ class Application {
|
||||
/// @see esphome::wake_loop_threadsafe() in wake.h for platform details.
|
||||
void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); }
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// Wake from ISR (ESP32 only).
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
/// Wake from ISR (ESP32 and LibreTiny).
|
||||
static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); }
|
||||
#elif defined(USE_ESP8266)
|
||||
/// Wake from ISR (ESP8266). No task_woken arg — no FreeRTOS. Caller must be IRAM_ATTR.
|
||||
static void IRAM_ATTR ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); }
|
||||
#endif
|
||||
|
||||
/// Wake from any context (ISR, thread, callback).
|
||||
@@ -399,19 +426,30 @@ class Application {
|
||||
void enable_component_loop_(Component *component);
|
||||
void enable_pending_loops_();
|
||||
void activate_looping_component_(uint16_t index);
|
||||
inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
|
||||
inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; }
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now);
|
||||
inline void ESPHOME_ALWAYS_INLINE before_component_phase_();
|
||||
inline void ESPHOME_ALWAYS_INLINE after_component_phase_() { this->in_loop_ = false; }
|
||||
|
||||
/// Process dump_config output one component per loop iteration.
|
||||
/// Extracted from loop() to keep cold startup/reconnect logging out of the hot path.
|
||||
/// Caller must ensure dump_config_at_ < components_.size().
|
||||
void __attribute__((noinline)) process_dump_config_();
|
||||
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates
|
||||
/// last_wdt_feed_, and re-dispatches the status LED. Out of line so the
|
||||
/// inline wrapper stays tiny.
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt() and updates
|
||||
/// last_wdt_feed_. Out of line so the inline wrapper stays tiny. Does NOT
|
||||
/// touch status_led — that's gated separately via service_status_led_slow_
|
||||
/// because the two rate limits have very different safe ranges (~ seconds
|
||||
/// for WDT, < 250 ms for LED blink rendering).
|
||||
void feed_wdt_slow_(uint32_t time);
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
/// Slow path for the status_led dispatch rate limit. Runs the status_led
|
||||
/// component's loop() based on its state (LOOP / LOOP_DONE with status
|
||||
/// bits set), and updates last_status_led_service_. Out of line to keep
|
||||
/// the feed_wdt_with_time hot path a couple of load+branch sequences.
|
||||
void service_status_led_slow_(uint32_t time);
|
||||
#endif
|
||||
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
#ifdef USE_HOST
|
||||
// select() fallback path is too complex to inline (host platform)
|
||||
@@ -448,9 +486,7 @@ class Application {
|
||||
// and active_end_ is incremented
|
||||
// - This eliminates branch mispredictions from flag checking in the hot loop
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
|
||||
#elif defined(USE_HOST)
|
||||
#ifdef USE_HOST
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
@@ -465,6 +501,10 @@ class Application {
|
||||
uint32_t last_loop_{0};
|
||||
uint32_t loop_component_start_time_{0};
|
||||
uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path
|
||||
#ifdef USE_STATUS_LED
|
||||
// millis() of most recent status_led dispatch; rate-limits independently of last_wdt_feed_
|
||||
uint32_t last_status_led_service_{0};
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
@@ -543,19 +583,25 @@ inline void Application::drain_wake_notifications_() {
|
||||
}
|
||||
#endif // USE_HOST
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
|
||||
// Phase A: drain wake notifications and run the scheduler. Invoked on every
|
||||
// Application::loop() tick regardless of whether a component phase runs, so
|
||||
// scheduler items fire at their requested cadence even when the caller has
|
||||
// raised loop_interval_ for power savings (see Application::loop()).
|
||||
// Returns the timestamp of the last scheduler item that ran (or `now`
|
||||
// unchanged if none ran), so the caller's WDT feed stays monotonic with the
|
||||
// per-item feeds inside scheduler.call() without an extra millis().
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) {
|
||||
#ifdef USE_HOST
|
||||
// Drain wake notifications first to clear socket for next wake
|
||||
this->drain_wake_notifications_();
|
||||
#endif
|
||||
return this->scheduler.call(now);
|
||||
}
|
||||
|
||||
// Process scheduled tasks. Scheduler::call now feeds the watchdog itself
|
||||
// after each scheduled item that actually runs, so we no longer need an
|
||||
// unconditional feed here — when Scheduler::call has no work to do, the
|
||||
// only elapsed time is a sleep wake + a few instructions, and when it does
|
||||
// have work, it fed the wdt as it went.
|
||||
this->scheduler.call(loop_start_time);
|
||||
|
||||
// Phase B entry: only invoked when a component loop phase is about to run.
|
||||
// Processes pending enable_loop requests from ISRs and marks in_loop_ so
|
||||
// reentrant modifications during component.loop() are safe.
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() {
|
||||
// Process any pending enable_loop requests from ISRs
|
||||
// This must be done before marking in_loop_ = true to avoid race conditions
|
||||
if (this->has_pending_enable_loop_requests_) {
|
||||
@@ -575,51 +621,130 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_
|
||||
}
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
// Get the initial loop time at the start
|
||||
uint32_t last_op_end_time = millis();
|
||||
|
||||
this->before_loop_tasks_(last_op_end_time);
|
||||
|
||||
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
|
||||
this->current_loop_index_++) {
|
||||
Component *component = this->looping_components_[this->current_loop_index_];
|
||||
|
||||
// Update the cached time before each component runs
|
||||
this->loop_component_start_time_ = last_op_end_time;
|
||||
|
||||
{
|
||||
this->set_current_component(component);
|
||||
WarnIfComponentBlockingGuard guard{component, last_op_end_time};
|
||||
component->loop();
|
||||
// Use the finish method to get the current time as the end time
|
||||
last_op_end_time = guard.finish();
|
||||
}
|
||||
this->feed_wdt_with_time(last_op_end_time);
|
||||
}
|
||||
|
||||
this->after_loop_tasks_();
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Capture the start of the active (non-sleeping) portion of this iteration.
|
||||
// Used to derive main-loop overhead = active time − Σ(component time) −
|
||||
// before/tail splits recorded below.
|
||||
uint32_t loop_active_start_us = micros();
|
||||
// Snapshot the cumulative component-recorded time so we can subtract the
|
||||
// slice that the scheduler spends inside its own WarnIfComponentBlockingGuard
|
||||
// (scheduler.cpp) — that time is already counted in per-component stats,
|
||||
// so charging it again to "before" would double-count.
|
||||
uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us;
|
||||
#endif
|
||||
// Phase A: always service the scheduler. Decouples scheduler cadence from
|
||||
// loop_interval_ so raised intervals (for power savings) don't drag scheduled
|
||||
// items forward. A tick that only runs the scheduler is cheap.
|
||||
// scheduler_tick_ returns the timestamp of the last scheduler item that ran
|
||||
// (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now`
|
||||
// so the gate check and WDT feed both reflect actual elapsed time after
|
||||
// scheduler dispatch, without an extra millis() call.
|
||||
uint32_t now = this->scheduler_tick_(millis());
|
||||
// Guarantee one WDT feed per tick even when the scheduler had nothing to
|
||||
// dispatch and the component phase is gated out — covers configs with no
|
||||
// looping components and no scheduler work (setup() has its own
|
||||
// per-component feed_wdt calls, so only do this here, not in scheduler_tick_).
|
||||
this->feed_wdt_with_time(now);
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Process any pending runtime stats printing after all components have run
|
||||
// This ensures stats printing doesn't affect component timing measurements
|
||||
uint32_t loop_before_end_us = micros();
|
||||
uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap;
|
||||
// Only meaningful when do_component_phase is true; initialized to 0 so the
|
||||
// tail bucket receives 0 on Phase A-only ticks (no component tail happened,
|
||||
// the gate-check / stats-prefix overhead belongs to "residual", not "tail").
|
||||
uint32_t loop_tail_start_us = 0;
|
||||
#endif
|
||||
|
||||
// Gate the component phase on loop_interval_, an active high-frequency
|
||||
// request, or an explicit wake from a background producer. A scheduler-only
|
||||
// wake (e.g. set_interval firing under a raised loop_interval_) leaves the
|
||||
// component phase gated; an external producer that called wake_loop_*
|
||||
// (MQTT RX, USB RX, BLE event, etc.) needs the component phase to actually
|
||||
// run so its component's loop() can drain the queued work — that is the
|
||||
// long-standing semantic of wake_loop_threadsafe(), and the wake_request
|
||||
// flag preserves it. wake_request_take() exchange-clears the flag; wakes
|
||||
// that arrive during Phase B re-set it and run Phase B again on the next
|
||||
// iteration.
|
||||
const bool high_frequency = HighFrequencyLoopRequester::is_high_frequency();
|
||||
const uint32_t elapsed = now - this->last_loop_;
|
||||
const bool woke = esphome::wake_request_take();
|
||||
const bool do_component_phase = high_frequency || woke || (elapsed >= this->loop_interval_);
|
||||
|
||||
if (do_component_phase) {
|
||||
this->before_component_phase_();
|
||||
|
||||
uint32_t last_op_end_time = now;
|
||||
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
|
||||
this->current_loop_index_++) {
|
||||
Component *component = this->looping_components_[this->current_loop_index_];
|
||||
|
||||
// Update the cached time before each component runs
|
||||
this->loop_component_start_time_ = last_op_end_time;
|
||||
|
||||
{
|
||||
this->set_current_component(component);
|
||||
WarnIfComponentBlockingGuard guard{component, last_op_end_time};
|
||||
component->loop();
|
||||
// Use the finish method to get the current time as the end time
|
||||
last_op_end_time = guard.finish();
|
||||
}
|
||||
this->feed_wdt_with_time(last_op_end_time);
|
||||
}
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
loop_tail_start_us = micros();
|
||||
#endif
|
||||
this->last_loop_ = last_op_end_time;
|
||||
now = last_op_end_time;
|
||||
this->after_component_phase_();
|
||||
}
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Record per-tick timing on every loop, not just component-phase ticks.
|
||||
// record_loop_active is a small accumulator; process_pending_stats is an
|
||||
// inline gate check that early-outs unless now >= next_log_time_.
|
||||
if (global_runtime_stats != nullptr) {
|
||||
global_runtime_stats->process_pending_stats(last_op_end_time);
|
||||
uint32_t loop_now_us = micros();
|
||||
// Subtract scheduled-component time from the "before" bucket so it is
|
||||
// not double-counted (it is already attributed to per-component stats).
|
||||
uint32_t loop_before_wall_us = loop_before_end_us - loop_active_start_us;
|
||||
uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us
|
||||
? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us)
|
||||
: 0;
|
||||
// tail_us is only defined when Phase B ran; 0 on Phase A-only ticks so the
|
||||
// stats bucket keeps its "component-phase trailing overhead" meaning.
|
||||
uint32_t loop_tail_us = do_component_phase ? (loop_now_us - loop_tail_start_us) : 0;
|
||||
global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, loop_tail_us);
|
||||
global_runtime_stats->process_pending_stats(now);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Use the last component's end time instead of calling millis() again
|
||||
// Compute sleep: bounded by time-until-next-component-phase and the
|
||||
// scheduler's next deadline. When a scheduler timer fires it re-enters
|
||||
// loop(), Phase A services it, and the component phase stays gated by
|
||||
// loop_interval_. When a background producer calls wake_loop_threadsafe()
|
||||
// it sets the wake_request flag and wakes select() / the task notification;
|
||||
// the gate above sees the flag and runs Phase B too so the producer's
|
||||
// component can drain its queued work without waiting up to loop_interval_.
|
||||
//
|
||||
// Re-read HighFrequencyLoopRequester::is_high_frequency() here instead of
|
||||
// reusing the cached `high_frequency` captured above: a component calling
|
||||
// HighFrequencyLoopRequester::start() from within its loop() would
|
||||
// otherwise sit under the stale value and sleep for up to loop_interval_
|
||||
// before the request took effect. That was fine pre-decoupling (the old
|
||||
// main loop also called the function fresh at the sleep point) but now
|
||||
// matters much more — loop_interval_ is a power-saving knob documented
|
||||
// to accept multi-second values, so the stale path could add seconds of
|
||||
// latency on an HF request. The call is a trivial atomic read.
|
||||
uint32_t delay_time = 0;
|
||||
auto elapsed = last_op_end_time - this->last_loop_;
|
||||
if (elapsed < this->loop_interval_ && !HighFrequencyLoopRequester::is_high_frequency()) {
|
||||
delay_time = this->loop_interval_ - elapsed;
|
||||
uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time);
|
||||
// next_schedule is max 0.5*delay_time
|
||||
// otherwise interval=0 schedules result in constant looping with almost no sleep
|
||||
next_schedule = std::max(next_schedule, delay_time / 2);
|
||||
delay_time = std::min(next_schedule, delay_time);
|
||||
if (!HighFrequencyLoopRequester::is_high_frequency()) {
|
||||
const uint32_t elapsed_since_phase = now - this->last_loop_;
|
||||
const uint32_t until_phase =
|
||||
(elapsed_since_phase >= this->loop_interval_) ? 0 : (this->loop_interval_ - elapsed_since_phase);
|
||||
const uint32_t until_sched = this->scheduler.next_schedule_in(now).value_or(until_phase);
|
||||
delay_time = std::min(until_phase, until_sched);
|
||||
}
|
||||
this->yield_with_select_(delay_time);
|
||||
this->last_loop_ = last_op_end_time;
|
||||
|
||||
if (this->dump_config_at_ < this->components_.size()) {
|
||||
this->process_dump_config_();
|
||||
@@ -630,26 +755,16 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
#ifndef USE_HOST
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers.
|
||||
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
|
||||
// Fast path (ESP32/LibreTiny): FreeRTOS task notifications posted by the lwip
|
||||
// event_callback wrapper (see lwip_fast_select.c) are the single source of truth for
|
||||
// socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification
|
||||
// that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns
|
||||
// immediately) or wakes a blocked Take directly. Additional wake sources:
|
||||
// wake_loop_threadsafe() from background tasks, and the delay_ms timeout.
|
||||
if (delay_ms == 0) [[unlikely]] {
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any socket already has pending data before sleeping.
|
||||
// If a socket still has unread data (rcvevent > 0) but the task notification was already
|
||||
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
|
||||
// This scan preserves select() semantics: return immediately when any fd is ready.
|
||||
for (struct lwip_sock *sock : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(sock)) {
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep with instant wake via FreeRTOS task notification.
|
||||
// Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout.
|
||||
#endif
|
||||
esphome::internal::wakeable_delay(delay_ms);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
|
||||
} else {
|
||||
// For delays with arguments, capture by value to preserve argument values
|
||||
// Arguments must be copied because original references may be invalid after delay
|
||||
auto f = [this, x...]() { this->play_next_(x...); };
|
||||
// `mutable` is required so captured copies of non-const reference args (e.g. std::string&)
|
||||
// are passed as non-const lvalues to play_next_(const Ts&...) where Ts may be `T&`
|
||||
auto f = [this, x...]() mutable { this->play_next_(x...); };
|
||||
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, Scheduler::NameType::NUMERIC_ID_INTERNAL,
|
||||
nullptr, static_cast<uint32_t>(InternalSchedulerID::DELAY_ACTION),
|
||||
this->delay_.value(x...), std::move(f),
|
||||
|
||||
@@ -506,6 +506,10 @@ void PollingComponent::stop_poller() {
|
||||
|
||||
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
uint64_t ComponentRuntimeStats::global_recorded_us = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif
|
||||
|
||||
void __attribute__((noinline, cold))
|
||||
WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) {
|
||||
bool should_warn;
|
||||
|
||||
@@ -116,6 +116,13 @@ struct ComponentRuntimeStats {
|
||||
uint64_t total_time_us{0};
|
||||
uint32_t total_max_time_us{0};
|
||||
|
||||
// Cumulative sum of every record_time() duration since boot, across all
|
||||
// components. Used by Application::loop() to snapshot time spent inside
|
||||
// WarnIfComponentBlockingGuard (including guards constructed by the
|
||||
// scheduler at scheduler.cpp) so main-loop overhead accounting can
|
||||
// subtract scheduled-callback time from the before_loop_tasks_ wall time.
|
||||
static uint64_t global_recorded_us; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void record_time(uint32_t duration_us) {
|
||||
this->period_count++;
|
||||
this->period_time_us += duration_us;
|
||||
@@ -125,6 +132,7 @@ struct ComponentRuntimeStats {
|
||||
this->total_time_us += duration_us;
|
||||
if (duration_us > this->total_max_time_us)
|
||||
this->total_max_time_us = duration_us;
|
||||
global_recorded_us += duration_us;
|
||||
}
|
||||
void reset_period() {
|
||||
this->period_count = 0;
|
||||
@@ -593,7 +601,7 @@ class Component {
|
||||
*/
|
||||
class PollingComponent : public Component {
|
||||
public:
|
||||
PollingComponent() : PollingComponent(0) {}
|
||||
PollingComponent() : PollingComponent(SCHEDULER_DONT_RUN) {}
|
||||
|
||||
/** Initialize this polling component with the given update interval in ms.
|
||||
*
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#define USE_ENTITY_DEVICE_CLASS
|
||||
#define USE_ENTITY_ICON
|
||||
#define USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
#define USE_ESP32_BLE_PSRAM
|
||||
#define USE_ESP32_CAMERA_JPEG_CONVERSION
|
||||
#define USE_ESP32_HOSTED
|
||||
#define USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
@@ -177,6 +178,7 @@
|
||||
#define USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#define API_MAX_SEND_QUEUE 8
|
||||
#define MAX_API_CONNECTIONS 6
|
||||
#define USE_MD5
|
||||
#define USE_SHA256
|
||||
#define USE_MQTT
|
||||
|
||||
@@ -21,6 +21,35 @@
|
||||
#define IRAM_ATTR __attribute__((noinline, long_call, section(".time_critical")))
|
||||
#define PROGMEM
|
||||
|
||||
#elif defined(USE_LIBRETINY)
|
||||
|
||||
// IRAM_ATTR places a function in executable RAM so it is callable from an
|
||||
// ISR even while flash is busy (XIP stall, OTA, logger flash write).
|
||||
// Each family uses a section its stock linker already routes to RAM:
|
||||
// RTL8710B → .image2.ram.text, RTL8720C → .sram.text. LN882H is the
|
||||
// exception: its stock linker has no matching glob, so patch_linker.py
|
||||
// injects KEEP(*(.sram.text*)) into .flash_copysection at pre-link.
|
||||
//
|
||||
// BK72xx (all variants) are left as a no-op: their SDK wraps flash
|
||||
// operations in GLOBAL_INT_DISABLE() which masks FIQ + IRQ at the CPU for
|
||||
// the duration of every write, so no ISR fires while flash is stalled and
|
||||
// the race IRAM_ATTR guards against cannot occur. The trade-off is that
|
||||
// interrupts are delayed (not dropped) by up to ~20 ms during a sector
|
||||
// erase, but that is an SDK-level choice and cannot be changed from this
|
||||
// layer.
|
||||
#if defined(USE_BK72XX)
|
||||
#define IRAM_ATTR
|
||||
#elif defined(USE_LIBRETINY_VARIANT_RTL8710B)
|
||||
// Stock linker consumes *(.image2.ram.text*) into .ram_image2.text (> BD_RAM).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".image2.ram.text")))
|
||||
#else
|
||||
// RTL8720C: stock linker consumes *(.sram.text*) into .ram.code_text.
|
||||
// LN882H: patch_linker.py.script injects *(.sram.text*) into
|
||||
// .flash_copysection (> RAM0 AT> FLASH).
|
||||
#define IRAM_ATTR __attribute__((noinline, section(".sram.text")))
|
||||
#endif
|
||||
#define PROGMEM
|
||||
|
||||
#else
|
||||
|
||||
#define IRAM_ATTR
|
||||
@@ -28,8 +57,51 @@
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
// Declared in the Beken FreeRTOS port (portmacro.h) and built in ARM mode so
|
||||
// it is callable from Thumb code via interworking. The MRS CPSR instruction
|
||||
// is ARM-only and user code here may be built in Thumb, so in_isr_context()
|
||||
// defers to this port helper on BK72xx instead of reading CPSR inline.
|
||||
extern "C" uint32_t platform_is_in_interrupt_context(void);
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
/// always_inline so callers placed in IRAM keep the detection in IRAM.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() {
|
||||
#if defined(USE_ESP32)
|
||||
return xPortInIsrContext() != 0;
|
||||
#elif defined(USE_ESP8266)
|
||||
// ESP8266 has no reliable single-register ISR detection: PS.INTLEVEL is
|
||||
// non-zero both in a real ISR and when user code masks interrupts. The
|
||||
// ESP8266 wake path is context-agnostic (wake_loop_impl uses esp_schedule
|
||||
// which is ISR-safe) so this helper is unused on this platform.
|
||||
return false;
|
||||
#elif defined(USE_RP2040)
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
#elif defined(USE_BK72XX)
|
||||
// BK72xx is ARM968E-S (ARM9); see extern declaration above.
|
||||
return platform_is_in_interrupt_context() != 0;
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// Cortex-M (AmebaZ, AmebaZ2, LN882H). IPSR is the active exception number;
|
||||
// non-zero means we're in a handler.
|
||||
uint32_t ipsr;
|
||||
__asm__ volatile("mrs %0, ipsr" : "=r"(ipsr));
|
||||
return ipsr != 0;
|
||||
#else
|
||||
// Host and any future platform without an ISR concept.
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void yield();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
|
||||
+27
-179
@@ -221,31 +221,7 @@ bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffi
|
||||
return strncasecmp(str + str_len - suffix_len, suffix, suffix_len) == 0;
|
||||
}
|
||||
|
||||
std::string str_truncate(const std::string &str, size_t length) {
|
||||
return str.length() > length ? str.substr(0, length) : str;
|
||||
}
|
||||
std::string str_until(const char *str, char ch) {
|
||||
const char *pos = strchr(str, ch);
|
||||
return pos == nullptr ? std::string(str) : std::string(str, pos - str);
|
||||
}
|
||||
std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); }
|
||||
// wrapper around std::transform to run safely on functions from the ctype.h header
|
||||
// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes
|
||||
template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.length());
|
||||
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); });
|
||||
return result;
|
||||
}
|
||||
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
|
||||
std::string str_upper_case(const std::string &str) { return str_ctype_transform<std::toupper>(str); }
|
||||
std::string str_snake_case(const std::string &str) {
|
||||
std::string result = str;
|
||||
for (char &c : result) {
|
||||
c = to_snake_case_char(c);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp
|
||||
char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) {
|
||||
if (buffer_size == 0) {
|
||||
return buffer;
|
||||
@@ -258,41 +234,7 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string str_sanitize(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.size());
|
||||
str_sanitize_to(&result[0], str.size() + 1, str.c_str());
|
||||
return result;
|
||||
}
|
||||
std::string str_snprintf(const char *fmt, size_t len, ...) {
|
||||
std::string str;
|
||||
va_list args;
|
||||
|
||||
str.resize(len);
|
||||
va_start(args, len);
|
||||
size_t out_length = vsnprintf(&str[0], len + 1, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (out_length < len)
|
||||
str.resize(out_length);
|
||||
|
||||
return str;
|
||||
}
|
||||
std::string str_sprintf(const char *fmt, ...) {
|
||||
std::string str;
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
size_t length = vsnprintf(nullptr, 0, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
str.resize(length);
|
||||
va_start(args, fmt);
|
||||
vsnprintf(&str[0], length + 1, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
return str;
|
||||
}
|
||||
// str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp
|
||||
|
||||
// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term)
|
||||
static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128;
|
||||
@@ -341,11 +283,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
return chars;
|
||||
}
|
||||
|
||||
std::string format_mac_address_pretty(const uint8_t *mac) {
|
||||
char buf[18];
|
||||
format_mac_addr_upper(mac, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
// format_mac_address_pretty moved to alloc_helpers.cpp
|
||||
|
||||
// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase.
|
||||
// When separator is set, it is written unconditionally after each byte and the last
|
||||
@@ -398,13 +336,7 @@ char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_
|
||||
return format_hex_internal(buffer, buffer_size, data, length, 0, 'a');
|
||||
}
|
||||
|
||||
std::string format_hex(const uint8_t *data, size_t length) {
|
||||
std::string ret;
|
||||
ret.resize(length * 2);
|
||||
format_hex_to(&ret[0], length * 2 + 1, data, length);
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
// format_hex (std::string returning overloads) moved to alloc_helpers.cpp
|
||||
|
||||
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) {
|
||||
return format_hex_internal(buffer, buffer_size, data, length, separator, 'A');
|
||||
@@ -441,43 +373,7 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *dat
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Shared implementation for uint8_t and string hex formatting
|
||||
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
size_t hex_len = separator ? (length * 3 - 1) : (length * 2);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
return format_hex_pretty_uint8(data, length, separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
size_t hex_len = separator ? (length * 5 - 1) : (length * 4);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
||||
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
|
||||
}
|
||||
// format_hex_pretty (all std::string returning overloads) moved to alloc_helpers.cpp
|
||||
|
||||
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
|
||||
if (buffer_size == 0) {
|
||||
@@ -500,12 +396,7 @@ char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string format_bin(const uint8_t *data, size_t length) {
|
||||
std::string result;
|
||||
result.resize(length * 8);
|
||||
format_bin_to(&result[0], length * 8 + 1, data, length);
|
||||
return result;
|
||||
}
|
||||
// format_bin moved to alloc_helpers.cpp
|
||||
|
||||
ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
|
||||
if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0)
|
||||
@@ -522,6 +413,23 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
|
||||
return PARSE_NONE;
|
||||
}
|
||||
|
||||
int8_t ilog10(float value) {
|
||||
float abs_val = fabsf(value);
|
||||
int8_t exp = 0;
|
||||
if (abs_val >= 10.0f) {
|
||||
while (abs_val >= 10.0f) {
|
||||
abs_val /= 10.0f;
|
||||
exp++;
|
||||
}
|
||||
} else if (abs_val < 1.0f) {
|
||||
while (abs_val < 1.0f) {
|
||||
abs_val *= 10.0f;
|
||||
exp--;
|
||||
}
|
||||
}
|
||||
return exp;
|
||||
}
|
||||
|
||||
static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) {
|
||||
if (accuracy_decimals < 0) {
|
||||
float divisor;
|
||||
@@ -537,11 +445,7 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de
|
||||
}
|
||||
}
|
||||
|
||||
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
value_accuracy_to_buf(buf, value, accuracy_decimals);
|
||||
return std::string(buf);
|
||||
}
|
||||
// value_accuracy_to_string moved to alloc_helpers.cpp
|
||||
|
||||
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) {
|
||||
normalize_accuracy_decimals(value, accuracy_decimals);
|
||||
@@ -606,45 +510,7 @@ static inline uint8_t base64_find_char(char c) {
|
||||
// Check if character is valid base64 or base64url
|
||||
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/') || (c == '-') || (c == '_')); }
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
|
||||
|
||||
// Encode 3 input bytes to 4 base64 characters, append 'count' to ret.
|
||||
static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) {
|
||||
char char_array_4[4];
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
|
||||
}
|
||||
|
||||
std::string base64_encode(const uint8_t *buf, size_t buf_len) {
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
char char_array_3[3];
|
||||
|
||||
while (buf_len--) {
|
||||
char_array_3[i++] = *(buf++);
|
||||
if (i == 3) {
|
||||
base64_encode_triple(char_array_3, 4, ret);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
|
||||
base64_encode_triple(char_array_3, i + 1, ret);
|
||||
|
||||
while ((i++ < 3))
|
||||
ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
// base64_encode (both overloads) moved to alloc_helpers.cpp
|
||||
|
||||
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) {
|
||||
return base64_decode(reinterpret_cast<const uint8_t *>(encoded_string.data()), encoded_string.size(), buf, buf_len);
|
||||
@@ -705,14 +571,7 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
|
||||
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
|
||||
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
|
||||
std::vector<uint8_t> ret(max_len);
|
||||
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
|
||||
ret.resize(actual_len);
|
||||
return ret;
|
||||
}
|
||||
// base64_decode (vector-returning overload) moved to alloc_helpers.cpp
|
||||
|
||||
/// Decode base64/base64url string directly into vector of little-endian int32 values
|
||||
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
|
||||
@@ -851,18 +710,7 @@ void HighFrequencyLoopRequester::stop() {
|
||||
this->started_ = false;
|
||||
}
|
||||
|
||||
std::string get_mac_address() {
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
char buf[13];
|
||||
format_mac_addr_lower_no_sep(mac, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string get_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(get_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
// get_mac_address, get_mac_address_pretty moved to alloc_helpers.cpp
|
||||
|
||||
void get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_BUFFER_SIZE> buf) {
|
||||
uint8_t mac[6];
|
||||
|
||||
+60
-222
@@ -21,6 +21,12 @@
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
// Backward compatibility re-export of heap-allocating helpers.
|
||||
// These functions have moved to alloc_helpers.h. External components should
|
||||
// update their includes to use #include "esphome/core/alloc_helpers.h" directly.
|
||||
// This re-export will be removed in 2026.11.0.
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <Esp.h>
|
||||
#include <pgmspace.h>
|
||||
@@ -734,6 +740,11 @@ template<size_t STACK_SIZE, typename T = uint8_t> class SmallBufferWithHeapFallb
|
||||
/// @name Mathematics
|
||||
///@{
|
||||
|
||||
/// Compute floor(log10(fabs(value))) using iterative comparison.
|
||||
/// Avoids pulling in __ieee754_logf/log10f (~1KB flash).
|
||||
/// Only valid for finite, non-zero values.
|
||||
int8_t ilog10(float value);
|
||||
|
||||
/// Compute 10^exp using iterative multiplication/division.
|
||||
/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. // NOLINT
|
||||
/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. // NOLINT
|
||||
@@ -979,27 +990,13 @@ inline bool str_endswith_ignore_case(const std::string &str, const char *suffix)
|
||||
return str_endswith_ignore_case(str.c_str(), str.size(), suffix, strlen(suffix));
|
||||
}
|
||||
|
||||
/// Truncate a string to a specific length.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_truncate(const std::string &str, size_t length);
|
||||
// str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0
|
||||
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end
|
||||
/// (requires str to be null-terminated).
|
||||
std::string str_until(const char *str, char ch);
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end.
|
||||
std::string str_until(const std::string &str, char ch);
|
||||
|
||||
/// Convert the string to lower case.
|
||||
std::string str_lower_case(const std::string &str);
|
||||
/// Convert the string to upper case.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_upper_case(const std::string &str);
|
||||
// str_until, str_lower_case, str_upper_case moved to alloc_helpers.h - remove this comment before 2026.11.0
|
||||
|
||||
/// Convert a single char to snake_case: lowercase and space to underscore.
|
||||
constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; }
|
||||
/// Convert the string to snake case (lowercase with underscores).
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_snake_case(const std::string &str);
|
||||
// str_snake_case moved to alloc_helpers.h - remove this comment before 2026.11.0
|
||||
|
||||
/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore.
|
||||
constexpr char to_sanitized_char(char c) {
|
||||
@@ -1022,9 +1019,7 @@ template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *s
|
||||
return str_sanitize_to(buffer, N, str);
|
||||
}
|
||||
|
||||
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
|
||||
/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead.
|
||||
std::string str_sanitize(const std::string &str);
|
||||
// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0
|
||||
|
||||
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
|
||||
/// This computes object_id hashes directly from names without creating an intermediate buffer.
|
||||
@@ -1040,13 +1035,7 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
|
||||
|
||||
/// sprintf-like function returning std::string.
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
|
||||
// str_snprintf, str_sprintf moved to alloc_helpers.h - remove this comment before 2026.11.0
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
|
||||
@@ -1095,7 +1084,33 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf,
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Safely append a string to buffer without format parsing, returning new position (capped at size).
|
||||
#ifdef USE_ESP8266
|
||||
/// Safely append a PROGMEM string to buffer, returning new position (capped at size).
|
||||
/// ESP8266 internal implementation — prefer the `buf_append_str` macro which wraps
|
||||
/// literals with `PSTR()` automatically so they stay in flash instead of eating RAM.
|
||||
/// @param buf Output buffer
|
||||
/// @param size Total buffer size
|
||||
/// @param pos Current position in buffer
|
||||
/// @param str PROGMEM-resident string to append (must not be null)
|
||||
/// @return New position after appending; returns `size` if `pos >= size`, otherwise
|
||||
/// returns at most `size - 1` because one byte is reserved for the null terminator
|
||||
inline size_t buf_append_str_p(char *buf, size_t size, size_t pos, PGM_P str) {
|
||||
if (pos >= size) {
|
||||
return size;
|
||||
}
|
||||
size_t remaining = size - pos - 1; // reserve space for null terminator
|
||||
size_t len = strnlen_P(str, remaining);
|
||||
memcpy_P(buf + pos, str, len);
|
||||
pos += len;
|
||||
buf[pos] = '\0';
|
||||
return pos;
|
||||
}
|
||||
/// Safely append a string to buffer, returning new position (capped at size).
|
||||
/// More efficient than buf_append_printf for plain string literals.
|
||||
/// On ESP8266 the literal is wrapped with PSTR() so it stays in flash.
|
||||
#define buf_append_str(buf, size, pos, str) buf_append_str_p(buf, size, pos, PSTR(str))
|
||||
#else
|
||||
/// Safely append a string to buffer, returning new position (capped at size).
|
||||
/// More efficient than buf_append_printf for plain string literals.
|
||||
/// @param buf Output buffer
|
||||
/// @param size Total buffer size
|
||||
@@ -1107,15 +1122,16 @@ inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str
|
||||
return size;
|
||||
}
|
||||
size_t remaining = size - pos - 1; // reserve space for null terminator
|
||||
size_t len = strlen(str);
|
||||
if (len > remaining) {
|
||||
len = remaining;
|
||||
size_t len = 0;
|
||||
while (len < remaining && str[len] != '\0') {
|
||||
len++;
|
||||
}
|
||||
memcpy(buf + pos, str, len);
|
||||
pos += len;
|
||||
buf[pos] = '\0';
|
||||
return pos;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
|
||||
/// This avoids multiple heap allocations during string construction.
|
||||
@@ -1441,189 +1457,26 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) {
|
||||
format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE);
|
||||
}
|
||||
|
||||
/// Format the six-byte array \p mac into a MAC address.
|
||||
/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
std::string format_mac_address_pretty(const uint8_t mac[6]);
|
||||
/// Format the byte array \p data of length \p len in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
std::string format_hex(const uint8_t *data, size_t length);
|
||||
/// Format the vector \p data in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
std::string format_hex(const std::vector<uint8_t> &data);
|
||||
// format_mac_address_pretty, format_hex (all overloads) moved to alloc_helpers.h
|
||||
// Remove this comment and the template overloads below before 2026.11.0
|
||||
|
||||
/// Format an unsigned integer in lowercased hex, starting with the most significant byte.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) {
|
||||
val = convert_big_endian(val);
|
||||
return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
}
|
||||
/// Format the std::array \p data in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) {
|
||||
return format_hex(data.data(), data.size());
|
||||
}
|
||||
|
||||
/** Format a byte array in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Converts binary data to a hexadecimal string representation with customizable formatting.
|
||||
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
||||
* Optionally includes the total byte count in parentheses at the end.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
|
||||
*
|
||||
* @note Returns empty string if data is nullptr or length is 0.
|
||||
* @note The length will only be appended if show_length is true AND the length is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* uint8_t data[] = {0xA1, 0xB2, 0xC3};
|
||||
* format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts)
|
||||
* uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
|
||||
* format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)"
|
||||
* format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)"
|
||||
* format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
// format_hex_pretty (all overloads) moved to alloc_helpers.h
|
||||
// Remove this comment and the template overload below before 2026.11.0
|
||||
|
||||
/** Format a 16-bit word array in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Pointer to the 16-bit word array to format.
|
||||
* @param length Number of 16-bit words in the array.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||
* @return Formatted hex string with 4-digit hex values per word.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the length is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* uint16_t data[] = {0xA1B2, 0xC3D4};
|
||||
* format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
|
||||
* uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
|
||||
* format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a byte vector in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
||||
* uppercase hex value with customizable separator.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Vector of bytes to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string representation of the vector contents.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
* format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
|
||||
* std::vector<uint8_t> data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
|
||||
* format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)"
|
||||
* format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
||||
* as a 4-digit uppercase hex value in big-endian order.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data Vector of 16-bit words to format.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||
* @return Formatted hex string representation of the vector contents.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* std::vector<uint16_t> data = {0x1234, 0x5678};
|
||||
* format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
|
||||
* std::vector<uint16_t> data2 = {0x1234, 0x5678, 0x9ABC};
|
||||
* format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a string's bytes in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Treats each character in the string as a byte and formats it in hex.
|
||||
* Useful for debugging binary data stored in std::string containers.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @param data String whose bytes should be formatted as hex.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string representation of the string's byte contents.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the string length is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43
|
||||
* format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts)
|
||||
* std::string data2 = "ABCDE";
|
||||
* format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format an unsigned integer in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Converts the integer to big-endian byte order and formats each byte as hex.
|
||||
* The most significant byte appears first in the output string.
|
||||
*
|
||||
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
* Causes heap fragmentation on long-running devices.
|
||||
*
|
||||
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
||||
* @param val The unsigned integer value to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string with most significant byte first.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* uint32_t value = 0x12345678;
|
||||
* format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts)
|
||||
* uint64_t value2 = 0x123456789ABCDEF0;
|
||||
* format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
|
||||
* format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
|
||||
* format_hex_pretty<uint16_t>(0x1234); // Returns "12.34"
|
||||
* @endcode
|
||||
*/
|
||||
/// Format an unsigned integer in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
|
||||
std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
|
||||
val = convert_big_endian(val);
|
||||
@@ -1683,13 +1536,10 @@ inline char *format_bin_to(char (&buffer)[N], T val) {
|
||||
return format_bin_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
|
||||
}
|
||||
|
||||
/// Format the byte array \p data of length \p len in binary.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
std::string format_bin(const uint8_t *data, size_t length);
|
||||
// format_bin moved to alloc_helpers.h - remove this comment and template overload before 2026.11.0
|
||||
|
||||
/// Format an unsigned integer in binary, starting with the most significant byte.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
|
||||
val = convert_big_endian(val);
|
||||
return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
@@ -1705,9 +1555,7 @@ enum ParseOnOffState : uint8_t {
|
||||
/// Parse a string that contains either on, off or toggle.
|
||||
ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
|
||||
|
||||
/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.
|
||||
ESPDEPRECATED("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.", "2026.1.0")
|
||||
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);
|
||||
// value_accuracy_to_string moved to alloc_helpers.h - remove this comment before 2026.11.0
|
||||
|
||||
/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null)
|
||||
static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64;
|
||||
@@ -1721,10 +1569,8 @@ size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> bu
|
||||
/// Derive accuracy in decimals from an increment step.
|
||||
int8_t step_to_accuracy_decimals(float step);
|
||||
|
||||
std::string base64_encode(const uint8_t *buf, size_t buf_len);
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf);
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
|
||||
// base64_encode (both overloads), base64_decode (vector overload) moved to alloc_helpers.h
|
||||
// Remove this comment before 2026.11.0
|
||||
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
|
||||
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
|
||||
|
||||
@@ -2160,15 +2006,7 @@ class HighFrequencyLoopRequester {
|
||||
/// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
|
||||
void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter)
|
||||
|
||||
/// Get the device MAC address as a string, in lowercase hex notation.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
/// Use get_mac_address_into_buffer() instead.
|
||||
std::string get_mac_address();
|
||||
|
||||
/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
/// Use get_mac_address_pretty_into_buffer() instead.
|
||||
std::string get_mac_address_pretty();
|
||||
// get_mac_address, get_mac_address_pretty moved to alloc_helpers.h - remove this comment before 2026.11.0
|
||||
|
||||
/// Get the device MAC address into the given buffer, in lowercase hex notation.
|
||||
/// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null
|
||||
|
||||
@@ -20,7 +20,8 @@ extern "C" {
|
||||
extern TaskHandle_t esphome_main_task_handle;
|
||||
|
||||
/// Wake the main loop task from another FreeRTOS task. NOT ISR-safe.
|
||||
static inline void esphome_main_task_notify() {
|
||||
/// always_inline so callers placed in IRAM do not reference a flash-resident copy.
|
||||
__attribute__((always_inline)) static inline void esphome_main_task_notify() {
|
||||
TaskHandle_t task = esphome_main_task_handle;
|
||||
if (task != NULL) {
|
||||
xTaskNotifyGive(task);
|
||||
@@ -28,26 +29,14 @@ static inline void esphome_main_task_notify() {
|
||||
}
|
||||
|
||||
/// Wake the main loop task from an ISR. ISR-safe.
|
||||
static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) {
|
||||
__attribute__((always_inline)) static inline void esphome_main_task_notify_from_isr(
|
||||
BaseType_t *px_higher_priority_task_woken) {
|
||||
TaskHandle_t task = esphome_main_task_handle;
|
||||
if (task != NULL) {
|
||||
vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext).
|
||||
static inline void esphome_main_task_notify_any_context() {
|
||||
if (xPortInIsrContext()) {
|
||||
int px_higher_priority_task_woken = 0;
|
||||
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
|
||||
portYIELD_FROM_ISR(px_higher_priority_task_woken);
|
||||
} else {
|
||||
esphome_main_task_notify();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -144,6 +144,19 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
return;
|
||||
}
|
||||
|
||||
// An interval of 0 means "fire every tick forever," which is misuse: the
|
||||
// item would always be due, causing Scheduler::call() to spin and starve
|
||||
// the main loop (WDT reset in the field). Coerce to 1ms so existing code
|
||||
// using update_interval=0ms as a pseudo-loop() continues to work at ~1kHz,
|
||||
// and warn so authors can migrate to HighFrequencyLoopRequester which is
|
||||
// the intended mechanism for running fast in the main loop. Zero-delay
|
||||
// timeouts (defer) remain legitimate one-shots and are not affected.
|
||||
if (type == SchedulerItem::INTERVAL && delay == 0) [[unlikely]] {
|
||||
ESP_LOGE(TAG, "[%s] set_interval(0) would spin main loop - coercing to 1ms (use HighFrequencyLoopRequester)",
|
||||
component ? LOG_STR_ARG(component->get_component_log_str()) : LOG_STR_LITERAL("?"));
|
||||
delay = 1;
|
||||
}
|
||||
|
||||
// Take lock early to protect scheduler_item_pool_ access and retry-cancelled check
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
@@ -520,7 +533,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) {
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
void HOT Scheduler::call(uint32_t now) {
|
||||
uint32_t HOT Scheduler::call(uint32_t now) {
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
this->process_defer_queue_(now);
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
@@ -690,6 +703,9 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
this->debug_verify_no_leak_();
|
||||
}
|
||||
#endif
|
||||
// execute_item_() advances `now` as items fire; return it so the caller
|
||||
// stays monotonic with last_wdt_feed_.
|
||||
return now;
|
||||
}
|
||||
void HOT Scheduler::process_to_add_slow_path_() {
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
@@ -129,7 +129,8 @@ class Scheduler {
|
||||
|
||||
// Execute all scheduled items that are ready
|
||||
// @param now Fresh timestamp from millis() - must not be stale/cached
|
||||
void call(uint32_t now);
|
||||
// @return Timestamp of the last item that ran, or `now` unchanged if none ran.
|
||||
uint32_t call(uint32_t now);
|
||||
|
||||
// Move items from to_add_ into the main heap.
|
||||
// IMPORTANT: This method should only be called from the main thread (loop task).
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/version.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_MQTT
|
||||
#include "esphome/components/mqtt/mqtt_client.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
bool api_is_connected() {
|
||||
#ifdef USE_API
|
||||
if (api::global_api_server != nullptr) {
|
||||
return api::global_api_server->is_connected();
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mqtt_is_connected() {
|
||||
#ifdef USE_MQTT
|
||||
if (mqtt::global_mqtt_client != nullptr) {
|
||||
|
||||
+20
-2
@@ -1,10 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Return whether the node has at least one client connected to the native API
|
||||
bool api_is_connected();
|
||||
/// Return whether the node has at least one client connected to the native API.
|
||||
///
|
||||
/// Inline so that hot-path callers (e.g. component loop() ticks that check connectivity every
|
||||
/// iteration) can skip the call8/return pair. With USE_API disabled this trivially returns false
|
||||
/// and collapses at compile time.
|
||||
#ifdef USE_API
|
||||
ESPHOME_ALWAYS_INLINE inline bool api_is_connected() {
|
||||
return api::global_api_server != nullptr && api::global_api_server->is_connected();
|
||||
}
|
||||
#else
|
||||
ESPHOME_ALWAYS_INLINE inline bool api_is_connected() { return false; }
|
||||
#endif
|
||||
|
||||
/// Return whether the node has an active connection to an MQTT broker
|
||||
bool mqtt_is_connected();
|
||||
|
||||
+19
-3
@@ -12,12 +12,25 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// === ESP32 — IRAM_ATTR entry points ===
|
||||
#ifdef USE_ESP32
|
||||
// === Wake-requested flag storage ===
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::atomic<uint8_t> g_wake_requested{0};
|
||||
#else
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
volatile uint8_t g_wake_requested = 0;
|
||||
#endif
|
||||
|
||||
// === ESP32 / LibreTiny — IRAM_ATTR entry points ===
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) {
|
||||
// ISR-safe: set flag before notify so the wake is visible on the next gate
|
||||
// check. wake_request_set() is just an aligned 8-bit store / atomic store
|
||||
// and is safe from IRAM.
|
||||
wake_request_set();
|
||||
esphome_main_task_notify_from_isr(px_higher_priority_task_woken);
|
||||
}
|
||||
void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); }
|
||||
void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); }
|
||||
#endif
|
||||
|
||||
// === ESP8266 / RP2040 ===
|
||||
@@ -72,6 +85,9 @@ void wakeable_delay(uint32_t ms) {
|
||||
// === Host (UDP loopback socket) ===
|
||||
#ifdef USE_HOST
|
||||
void wake_loop_threadsafe() {
|
||||
// Set flag before sending so the consumer's gate check on the next loop()
|
||||
// entry observes the wake regardless of select() scheduling.
|
||||
wake_request_set();
|
||||
if (App.wake_socket_fd_ >= 0) {
|
||||
const char dummy = 1;
|
||||
::send(App.wake_socket_fd_, &dummy, 1, 0);
|
||||
|
||||
+72
-10
@@ -7,6 +7,10 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
#include <atomic>
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#include "esphome/core/main_task.h"
|
||||
#endif
|
||||
@@ -25,22 +29,71 @@ namespace esphome {
|
||||
extern volatile bool g_main_loop_woke;
|
||||
#endif
|
||||
|
||||
// === wake_request flag — signals Application::loop() that a producer queued
|
||||
// work for some component's loop() to drain (MQTT RX, USB RX, BLE event, etc.)
|
||||
// and the component phase should run this tick instead of being held off by
|
||||
// the loop_interval_ gate. Set by every wake_loop_* entry point; consumed
|
||||
// (via exchange-and-clear) at the gate in Application::loop(). ===
|
||||
//
|
||||
// std::atomic<uint8_t> rather than std::atomic<bool> because GCC on Xtensa
|
||||
// generates an indirect function call for atomic<bool> ops instead of inlining
|
||||
// them — same workaround applied in scheduler.h for the SchedulerItem::remove
|
||||
// flag. On non-atomic platforms a volatile uint8_t suffices: 8-bit aligned
|
||||
// loads/stores are atomic on every supported MCU, and the platform signal
|
||||
// that follows wake_request_set() (FreeRTOS task-notify, esp_schedule, socket
|
||||
// send) provides the cross-thread/cross-core memory barrier.
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern std::atomic<uint8_t> g_wake_requested;
|
||||
|
||||
__attribute__((always_inline)) inline void wake_request_set() { g_wake_requested.store(1, std::memory_order_release); }
|
||||
__attribute__((always_inline)) inline bool wake_request_take() {
|
||||
return g_wake_requested.exchange(0, std::memory_order_acquire) != 0;
|
||||
}
|
||||
#else
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern volatile uint8_t g_wake_requested;
|
||||
|
||||
__attribute__((always_inline)) inline void wake_request_set() { g_wake_requested = 1; }
|
||||
__attribute__((always_inline)) inline bool wake_request_take() {
|
||||
uint8_t v = g_wake_requested;
|
||||
g_wake_requested = 0;
|
||||
return v != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// === ESP32 / LibreTiny (FreeRTOS) ===
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// IRAM_ATTR entry point — defined in wake.cpp.
|
||||
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
|
||||
/// IRAM_ATTR entry point — defined in wake.cpp.
|
||||
void wake_loop_any_context();
|
||||
/// Wake the main loop from any context (ISR or task).
|
||||
/// always_inline so callers placed in IRAM keep the whole wake path in IRAM.
|
||||
__attribute__((always_inline)) inline void wake_main_task_any_context() {
|
||||
// Set the wake-requested flag BEFORE the task notification so the consumer
|
||||
// (Application::loop() gate) is guaranteed to see it on its next gate check.
|
||||
wake_request_set();
|
||||
if (in_isr_context()) {
|
||||
BaseType_t px_higher_priority_task_woken = pdFALSE;
|
||||
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
|
||||
#ifdef portYIELD_FROM_ISR
|
||||
portYIELD_FROM_ISR(px_higher_priority_task_woken);
|
||||
#else
|
||||
/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not
|
||||
/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake
|
||||
/// is not possible. xTaskNotifyGive is used as the best available option.
|
||||
inline void wake_loop_any_context() { esphome_main_task_notify(); }
|
||||
// ARM9 FreeRTOS port (BK72xx) does not define portYIELD_FROM_ISR; the IRQ
|
||||
// exit sequence performs the context switch if one was requested.
|
||||
(void) px_higher_priority_task_woken;
|
||||
#endif
|
||||
} else {
|
||||
esphome_main_task_notify();
|
||||
}
|
||||
}
|
||||
|
||||
inline void wake_loop_threadsafe() { esphome_main_task_notify(); }
|
||||
/// IRAM_ATTR entry points — defined in wake.cpp.
|
||||
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
|
||||
void wake_loop_any_context();
|
||||
|
||||
inline void wake_loop_threadsafe() {
|
||||
wake_request_set();
|
||||
esphome_main_task_notify();
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
inline void wakeable_delay(uint32_t ms) {
|
||||
@@ -57,6 +110,9 @@ inline void wakeable_delay(uint32_t ms) {
|
||||
|
||||
/// Inline implementation — IRAM callers inline this directly.
|
||||
inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() {
|
||||
// Set the wake-requested flag BEFORE esp_schedule so the consumer is
|
||||
// guaranteed to see it on its next gate check.
|
||||
wake_request_set();
|
||||
g_main_loop_woke = true;
|
||||
esp_schedule();
|
||||
}
|
||||
@@ -67,6 +123,9 @@ void wake_loop_any_context();
|
||||
/// Non-ISR: always inline.
|
||||
inline void wake_loop_threadsafe() { wake_loop_impl(); }
|
||||
|
||||
/// ISR-safe: no task_woken arg because ESP8266 has no FreeRTOS. Caller must be IRAM_ATTR.
|
||||
inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { wake_loop_impl(); }
|
||||
|
||||
namespace internal {
|
||||
inline void wakeable_delay(uint32_t ms) {
|
||||
if (ms == 0) {
|
||||
@@ -85,6 +144,9 @@ inline void wakeable_delay(uint32_t ms) {
|
||||
#elif defined(USE_RP2040)
|
||||
|
||||
inline void wake_loop_any_context() {
|
||||
// Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed
|
||||
// to see it on its next gate check.
|
||||
wake_request_set();
|
||||
g_main_loop_woke = true;
|
||||
__sev();
|
||||
}
|
||||
|
||||
+33
-17
@@ -606,33 +606,43 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
|
||||
if isinstance(rhs, MockObj) and rhs.is_new_expr:
|
||||
# For 'new' allocations, use placement new into static storage
|
||||
# to avoid heap fragmentation on embedded devices.
|
||||
the_type = id_.type
|
||||
#
|
||||
# Storage must be sized and aligned for the actual instantiated class,
|
||||
# which may be a subclass of id_.type (e.g. `cv.declare_id(BaseClass)`
|
||||
# combined with `SubClass.new()` — used by ili9xxx, waveshare_epaper,
|
||||
# etc. to select a model-specific constructor). Using id_.type would
|
||||
# run the base-class default constructor instead, silently losing any
|
||||
# subclass initialization. Template args live on the CallExpression
|
||||
# and are re-emitted below.
|
||||
call_expr = rhs.base
|
||||
assert isinstance(call_expr, CallExpression), (
|
||||
f"Expected CallExpression for placement new, got {type(call_expr)}"
|
||||
)
|
||||
actual_type = rhs.new_type if rhs.new_type is not None else id_.type
|
||||
if call_expr.template_args is not None:
|
||||
actual_type = f"{actual_type}{call_expr.template_args}"
|
||||
pointer_type = id_.type
|
||||
# Extract component namespace from type for memory analysis attribution
|
||||
component_ns = _extract_component_ns(str(the_type))
|
||||
component_ns = _extract_component_ns(str(actual_type))
|
||||
storage_name = f"{component_ns}__{id_.id}__pstorage"
|
||||
|
||||
# Declare aligned byte array for the object storage
|
||||
CORE.add_global(
|
||||
RawStatement(
|
||||
f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];"
|
||||
f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];"
|
||||
)
|
||||
)
|
||||
# Pointer declaration uses id_.type to preserve the declared base-class
|
||||
# pointer type for downstream callers (polymorphism through base ptr).
|
||||
CORE.add_global(
|
||||
AssignmentExpression(
|
||||
f"static {the_type}",
|
||||
f"static {pointer_type}",
|
||||
"*const ",
|
||||
id_,
|
||||
MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"),
|
||||
MockObj(f"reinterpret_cast<{pointer_type} *>({storage_name})"),
|
||||
)
|
||||
)
|
||||
# Extract args from the CallExpression and rebuild as placement new.
|
||||
# Template args are already encoded in the_type (e.g. GlobalsComponent<int>),
|
||||
# so we only pass the constructor args, not template_args.
|
||||
call_expr = rhs.base
|
||||
assert isinstance(call_expr, CallExpression), (
|
||||
f"Expected CallExpression for placement new, got {type(call_expr)}"
|
||||
)
|
||||
placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args)
|
||||
placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args)
|
||||
CORE.add(ExpressionStatement(placement_new))
|
||||
else:
|
||||
decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
|
||||
@@ -869,12 +879,16 @@ class MockObj(Expression):
|
||||
Mostly consists of magic methods that allow ESPHome's codegen syntax.
|
||||
"""
|
||||
|
||||
__slots__ = ("base", "op", "is_new_expr")
|
||||
__slots__ = ("base", "op", "is_new_expr", "new_type")
|
||||
|
||||
def __init__(self, base, op=".", is_new_expr=False) -> None:
|
||||
def __init__(self, base, op=".", is_new_expr=False, new_type=None) -> None:
|
||||
self.base = base
|
||||
self.op = op
|
||||
self.is_new_expr = is_new_expr
|
||||
# For `is_new_expr=True` objects, `new_type` holds the class name being
|
||||
# constructed (e.g. "ili9xxx::ILI9XXXST7789V"). Needed by Pvariable so
|
||||
# placement new uses the actual subclass rather than id_.type.
|
||||
self.new_type = new_type
|
||||
|
||||
def __getattr__(self, attr: str) -> "MockObj":
|
||||
# prevent python dunder methods being replaced by mock objects
|
||||
@@ -889,7 +903,9 @@ class MockObj(Expression):
|
||||
|
||||
def __call__(self, *args: SafeExpType) -> "MockObj":
|
||||
call = CallExpression(self.base, *args)
|
||||
return MockObj(call, self.op, is_new_expr=self.is_new_expr)
|
||||
return MockObj(
|
||||
call, self.op, is_new_expr=self.is_new_expr, new_type=self.new_type
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.base)
|
||||
@@ -903,7 +919,7 @@ class MockObj(Expression):
|
||||
|
||||
@property
|
||||
def new(self) -> "MockObj":
|
||||
return MockObj(f"new {self.base}", "->", is_new_expr=True)
|
||||
return MockObj(f"new {self.base}", "->", is_new_expr=True, new_type=self.base)
|
||||
|
||||
def template(self, *args: SafeExpType) -> "MockObj":
|
||||
"""Apply template parameters to this object."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user