mirror of
https://github.com/esphome/esphome.git
synced 2026-02-13 21:17:35 -07:00
Compare commits
11 Commits
bump_espid
...
2026.2.0b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa4047089 | ||
|
|
a8a324cbfb | ||
|
|
f6aeef2e68 | ||
|
|
297dfb0db4 | ||
|
|
c08356b0c1 | ||
|
|
e9bf9bc691 | ||
|
|
ead7937dbf | ||
|
|
844210519a | ||
|
|
7c70b2e04e | ||
|
|
e000858d77 | ||
|
|
97d6f394de |
@@ -1 +1 @@
|
||||
9c4da80001128f05bb3bb3668a18609eea86e0b56bb04a22e66c7ec6609f92ff
|
||||
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.3.0-dev
|
||||
PROJECT_NUMBER = 2026.2.0b2
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b
|
||||
ARG BUILD_TYPE
|
||||
FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*"
|
||||
RUN git config --system --add safe.directory "*" \
|
||||
&& git config --system advice.detachedHead false
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
|
||||
@@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
|
||||
|
||||
/// Run through handshake messages (if in that phase)
|
||||
APIError APINoiseFrameHelper::loop() {
|
||||
// During handshake phase, process as many actions as possible until we can't progress
|
||||
// socket_->ready() stays true until next main loop, but state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read
|
||||
while (state_ != State::DATA && this->socket_->ready()) {
|
||||
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
|
||||
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
|
||||
// that must follow reads, deadlocking the handshake. state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read.
|
||||
bool socket_ready = this->socket_->ready();
|
||||
while (state_ != State::DATA && socket_ready) {
|
||||
APIError err = state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
|
||||
@@ -117,37 +117,7 @@ void APIServer::setup() {
|
||||
void APIServer::loop() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
if (this->socket_ && this->socket_->ready()) {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
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);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
this->accept_new_connections_();
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
@@ -178,46 +148,88 @@ void APIServer::loop() {
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
}
|
||||
// Handle disconnection promptly - close socket to free LWIP PCB
|
||||
// resources and prevent retransmit crashes on ESP8266.
|
||||
if (client->flags_.remove) {
|
||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||
this->remove_client_(client_index);
|
||||
} else {
|
||||
client_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// 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());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
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);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// 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());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,11 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
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);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
|
||||
@@ -613,13 +613,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
|
||||
|
||||
|
||||
def _format_framework_espidf_version(
|
||||
ver: cv.Version, release: str | None = None
|
||||
) -> str:
|
||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
||||
# a PIO platformio/framework-espidf value
|
||||
if release is None:
|
||||
release = ESP_IDF_RELEASE_LOOKUP.get(ver)
|
||||
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
|
||||
ext = "tar.xz"
|
||||
else:
|
||||
@@ -696,9 +692,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"latest": cv.Version(5, 5, 2),
|
||||
"dev": cv.Version(5, 5, 2),
|
||||
}
|
||||
ESP_IDF_RELEASE_LOOKUP: dict[cv.Version, str] = {
|
||||
cv.Version(5, 5, 2): "260206",
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
@@ -762,7 +755,7 @@ def _check_versions(config):
|
||||
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
|
||||
value[CONF_SOURCE] = value.get(
|
||||
CONF_SOURCE,
|
||||
_format_framework_espidf_version(version, value.get(CONF_RELEASE)),
|
||||
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
|
||||
)
|
||||
if _is_framework_url(value[CONF_SOURCE]):
|
||||
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
|
||||
@@ -1474,7 +1467,7 @@ async def to_code(config):
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[_format_framework_espidf_version(idf_ver)],
|
||||
[_format_framework_espidf_version(idf_ver, None)],
|
||||
)
|
||||
# Use stub package to skip downloading precompiled libs
|
||||
stubs_dir = CORE.relative_build_path("arduino_libs_stub")
|
||||
@@ -1518,14 +1511,6 @@ async def to_code(config):
|
||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||
)
|
||||
|
||||
# ESP32-P4: The ESP-IDF 5.5.2.260206 release changed the default of
|
||||
# ESP32P4_SELECTS_REV_LESS_V3 from y to n. PlatformIO always uses
|
||||
# sections.ld.in (for rev <3) rather than sections.rev3.ld.in (for rev >=3),
|
||||
# causing a linker script mismatch with the generated memory.ld.
|
||||
# Restore the previous default until PlatformIO handles this properly.
|
||||
if variant == VARIANT_ESP32P4:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", True)
|
||||
|
||||
# Set minimum chip revision for ESP32 variant
|
||||
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
|
||||
# and for PSRAM users saves significant IRAM by keeping C library functions in ROM.
|
||||
|
||||
@@ -403,21 +403,18 @@ def final_validation(config):
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
|
||||
|
||||
# Check if BLE Server is needed
|
||||
has_ble_server = "esp32_ble_server" in full_config
|
||||
|
||||
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
|
||||
has_ble_client = (
|
||||
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
|
||||
)
|
||||
|
||||
# Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a
|
||||
# GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED
|
||||
# guard, causing a compile error when both are disabled.
|
||||
# Additionally, when GATT Client is enabled, GATT Server must also be enabled
|
||||
# as an internal dependency in the Bluedroid stack.
|
||||
# ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
|
||||
# This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
|
||||
# See: https://github.com/espressif/esp-idf/issues/17724
|
||||
# TODO: Revert to conditional once the gatt_main.c bug is fixed upstream:
|
||||
# has_ble_server = "esp32_ble_server" in full_config
|
||||
# add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
|
||||
|
||||
# Handle max_connections: check for deprecated location in esp32_ble_tracker
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
from esphome.components.mipi import (
|
||||
ETMOD,
|
||||
FRMCTR2,
|
||||
GMCTRN1,
|
||||
GMCTRP1,
|
||||
IFCTR,
|
||||
MODE_RGB,
|
||||
PWCTR1,
|
||||
PWCTR3,
|
||||
PWCTR4,
|
||||
PWCTR5,
|
||||
PWSET,
|
||||
DriverChip,
|
||||
)
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .amoled import CO5300
|
||||
@@ -142,16 +129,6 @@ DriverChip(
|
||||
),
|
||||
),
|
||||
)
|
||||
ST7789P = DriverChip(
|
||||
"ST7789P",
|
||||
# Max supported dimensions
|
||||
width=240,
|
||||
height=320,
|
||||
# SPI: RGB layout
|
||||
color_order=MODE_RGB,
|
||||
invert_colors=True,
|
||||
draw_rounding=1,
|
||||
)
|
||||
|
||||
ILI9488_A.extend(
|
||||
"PICO-RESTOUCH-LCD-3.5",
|
||||
@@ -185,61 +162,3 @@ AXS15231.extend(
|
||||
cs_pin=9,
|
||||
reset_pin=21,
|
||||
)
|
||||
|
||||
# Waveshare 1.83-v2
|
||||
#
|
||||
# Do not use on 1.83-v1: Vendor warning on different chip!
|
||||
ST7789P.extend(
|
||||
"WAVESHARE-1.83-V2",
|
||||
# Panel size smaller than ST7789 max allowed
|
||||
width=240,
|
||||
height=284,
|
||||
# Vendor specific init derived from vendor sample code
|
||||
# "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp"
|
||||
# Compatible MIT license, see esphome/LICENSE file.
|
||||
initsequence=(
|
||||
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
|
||||
(ETMOD, 0x35),
|
||||
(0xBB, 0x19),
|
||||
(PWCTR1, 0x2C),
|
||||
(PWCTR3, 0x01),
|
||||
(PWCTR4, 0x12),
|
||||
(PWCTR5, 0x20),
|
||||
(IFCTR, 0x0F),
|
||||
(PWSET, 0xA4, 0xA1),
|
||||
(
|
||||
GMCTRP1,
|
||||
0xD0,
|
||||
0x04,
|
||||
0x0D,
|
||||
0x11,
|
||||
0x13,
|
||||
0x2B,
|
||||
0x3F,
|
||||
0x54,
|
||||
0x4C,
|
||||
0x18,
|
||||
0x0D,
|
||||
0x0B,
|
||||
0x1F,
|
||||
0x23,
|
||||
),
|
||||
(
|
||||
GMCTRN1,
|
||||
0xD0,
|
||||
0x04,
|
||||
0x0C,
|
||||
0x11,
|
||||
0x13,
|
||||
0x2C,
|
||||
0x3F,
|
||||
0x44,
|
||||
0x51,
|
||||
0x2F,
|
||||
0x1F,
|
||||
0x1F,
|
||||
0x20,
|
||||
0x23,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -38,8 +38,7 @@ void PulseMeterSensor::setup() {
|
||||
}
|
||||
|
||||
void PulseMeterSensor::loop() {
|
||||
// Reset the count in get before we pass it back to the ISR as set
|
||||
this->get_->count_ = 0;
|
||||
State state;
|
||||
|
||||
{
|
||||
// Lock the interrupt so the interrupt code doesn't interfere with itself
|
||||
@@ -58,31 +57,35 @@ void PulseMeterSensor::loop() {
|
||||
}
|
||||
this->last_pin_val_ = current;
|
||||
|
||||
// Swap out set and get to get the latest state from the ISR
|
||||
std::swap(this->set_, this->get_);
|
||||
// Get the latest state from the ISR and reset the count in the ISR
|
||||
state.last_detected_edge_us_ = this->state_.last_detected_edge_us_;
|
||||
state.last_rising_edge_us_ = this->state_.last_rising_edge_us_;
|
||||
state.count_ = this->state_.count_;
|
||||
this->state_.count_ = 0;
|
||||
}
|
||||
|
||||
const uint32_t now = micros();
|
||||
|
||||
// If an edge was peeked, repay the debt
|
||||
if (this->peeked_edge_ && this->get_->count_ > 0) {
|
||||
if (this->peeked_edge_ && state.count_ > 0) {
|
||||
this->peeked_edge_ = false;
|
||||
this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
state.count_--;
|
||||
}
|
||||
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early
|
||||
if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ &&
|
||||
now - this->get_->last_rising_edge_us_ >= this->filter_us_) {
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early.
|
||||
// Wait for the debt to be repaid before counting another unprocessed edge early.
|
||||
if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ &&
|
||||
now - state.last_rising_edge_us_ >= this->filter_us_) {
|
||||
this->peeked_edge_ = true;
|
||||
this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_;
|
||||
this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
state.last_detected_edge_us_ = state.last_rising_edge_us_;
|
||||
state.count_++;
|
||||
}
|
||||
|
||||
// Check if we detected a pulse this loop
|
||||
if (this->get_->count_ > 0) {
|
||||
if (state.count_ > 0) {
|
||||
// Keep a running total of pulses if a total sensor is configured
|
||||
if (this->total_sensor_ != nullptr) {
|
||||
this->total_pulses_ += this->get_->count_;
|
||||
this->total_pulses_ += state.count_;
|
||||
const uint32_t total = this->total_pulses_;
|
||||
this->total_sensor_->publish_state(total);
|
||||
}
|
||||
@@ -94,15 +97,15 @@ void PulseMeterSensor::loop() {
|
||||
this->meter_state_ = MeterState::RUNNING;
|
||||
} break;
|
||||
case MeterState::RUNNING: {
|
||||
uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(this->get_->count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us,
|
||||
this->get_->count_, pulse_width_us);
|
||||
uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(state.count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_,
|
||||
pulse_width_us);
|
||||
this->publish_state((60.0f * 1000000.0f) / pulse_width_us);
|
||||
} break;
|
||||
}
|
||||
|
||||
this->last_processed_edge_us_ = this->get_->last_detected_edge_us_;
|
||||
this->last_processed_edge_us_ = state.last_detected_edge_us_;
|
||||
}
|
||||
// No detected edges this loop
|
||||
else {
|
||||
@@ -141,14 +144,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) {
|
||||
// This is an interrupt handler - we can't call any virtual method from this method
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
auto &state = sensor->edge_state_;
|
||||
auto &set = *sensor->set_;
|
||||
auto &edge_state = sensor->edge_state_;
|
||||
auto &state = sensor->state_;
|
||||
|
||||
if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
state.last_sent_edge_us_ = now;
|
||||
set.last_detected_edge_us_ = now;
|
||||
set.last_rising_edge_us_ = now;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
edge_state.last_sent_edge_us_ = now;
|
||||
state.last_detected_edge_us_ = now;
|
||||
state.last_rising_edge_us_ = now;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// This ISR is bound to rising edges, so the pin is high
|
||||
@@ -160,26 +163,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) {
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
const bool pin_val = sensor->isr_pin_.digital_read();
|
||||
auto &state = sensor->pulse_state_;
|
||||
auto &set = *sensor->set_;
|
||||
auto &pulse_state = sensor->pulse_state_;
|
||||
auto &state = sensor->state_;
|
||||
|
||||
// Filter length has passed since the last interrupt
|
||||
const bool length = now - state.last_intr_ >= sensor->filter_us_;
|
||||
const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_;
|
||||
|
||||
if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
state.latched_ = false;
|
||||
} else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
state.latched_ = true;
|
||||
set.last_detected_edge_us_ = state.last_intr_;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
pulse_state.latched_ = false;
|
||||
} else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
pulse_state.latched_ = true;
|
||||
state.last_detected_edge_us_ = pulse_state.last_intr_;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// Due to order of operations this includes
|
||||
// length && latched && rising (just reset from a long low edge)
|
||||
// !latched && (rising || high) (noise on the line resetting the potential rising edge)
|
||||
set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_;
|
||||
state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_;
|
||||
|
||||
state.last_intr_ = now;
|
||||
pulse_state.last_intr_ = now;
|
||||
sensor->last_pin_val_ = pin_val;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,17 +46,16 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
|
||||
uint32_t total_pulses_ = 0;
|
||||
uint32_t last_processed_edge_us_ = 0;
|
||||
|
||||
// This struct (and the two pointers) are used to pass data between the ISR and loop.
|
||||
// These two pointers are exchanged each loop.
|
||||
// Use these to send data from the ISR to the loop not the other way around (except for resetting the values).
|
||||
// This struct and variable are used to pass data between the ISR and loop.
|
||||
// The data from state_ is read and then count_ in state_ is reset in each loop.
|
||||
// This must be done while guarded by an InterruptLock. Use this variable to send data
|
||||
// from the ISR to the loop not the other way around (except for resetting count_).
|
||||
struct State {
|
||||
uint32_t last_detected_edge_us_ = 0;
|
||||
uint32_t last_rising_edge_us_ = 0;
|
||||
uint32_t count_ = 0;
|
||||
};
|
||||
State state_[2];
|
||||
volatile State *set_ = state_;
|
||||
volatile State *get_ = state_ + 1;
|
||||
volatile State state_{};
|
||||
|
||||
// Only use the following variables in the ISR or while guarded by an InterruptLock
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
|
||||
@@ -90,7 +90,6 @@ void IDFUARTComponent::setup() {
|
||||
return;
|
||||
}
|
||||
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
|
||||
this->lock_ = xSemaphoreCreateMutex();
|
||||
|
||||
#if (SOC_UART_LP_NUM >= 1)
|
||||
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
|
||||
@@ -102,11 +101,7 @@ void IDFUARTComponent::setup() {
|
||||
this->rx_buffer_size_ = fifo_len * 2;
|
||||
}
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
|
||||
this->load_settings(false);
|
||||
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
@@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
constexpr int event_queue_size = 20;
|
||||
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
|
||||
#else
|
||||
constexpr int event_queue_size = 0;
|
||||
QueueHandle_t *event_queue_ptr = nullptr;
|
||||
#endif
|
||||
err = uart_driver_install(this->uart_num_, // UART number
|
||||
this->rx_buffer_size_, // RX ring buffer size
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
20, // event queue size/depth
|
||||
&this->uart_event_queue_, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
event_queue_size, // event queue size/depth
|
||||
event_queue_ptr, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||
@@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
|
||||
}
|
||||
|
||||
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
|
||||
xSemaphoreGive(this->lock_);
|
||||
if (write_len != (int32_t) len) {
|
||||
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
|
||||
this->mark_failed();
|
||||
@@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
if (!this->check_read_timeout_())
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
*data = this->peek_byte_;
|
||||
} else {
|
||||
@@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
this->peek_byte_ = *data;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(this->lock_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
int32_t read_len = 0;
|
||||
if (!this->check_read_timeout_(len))
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
length_to_read--;
|
||||
*data = this->peek_byte_;
|
||||
@@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
}
|
||||
if (length_to_read > 0)
|
||||
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
xSemaphoreGive(this->lock_);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||
@@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() {
|
||||
size_t available = 0;
|
||||
esp_err_t err;
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
err = uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
xSemaphoreGive(this->lock_);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
|
||||
@@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() {
|
||||
|
||||
void IDFUARTComponent::flush() {
|
||||
ESP_LOGVV(TAG, " Flushing");
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::check_logger_conflict() {}
|
||||
@@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() {
|
||||
ESP_LOGV(TAG, "RX event task started");
|
||||
}
|
||||
|
||||
// FreeRTOS task that relays UART ISR events to the main loop.
|
||||
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
|
||||
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
|
||||
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
|
||||
// is done by the main loop. This task only reads from the event queue and
|
||||
// calls App.wake_loop_threadsafe().
|
||||
void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
auto *self = static_cast<IDFUARTComponent *>(param);
|
||||
uart_event_t event;
|
||||
@@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
|
||||
case UART_FIFO_OVF:
|
||||
case UART_BUFFER_FULL:
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
|
||||
uart_flush_input(self->uart_num_);
|
||||
// Don't call uart_flush_input() here — this task does not own the read side.
|
||||
// ESP-IDF examples flush on overflow because the same task handles both events
|
||||
// and reads, so flush and read are serialized. Here, reads happen on the main
|
||||
// loop, so flushing from this task races with read_array() and can destroy data
|
||||
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
|
||||
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
|
||||
// into the ring buffer and re-enables RX interrupts once space is freed.
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
/// ESP-IDF UART driver wrapper.
|
||||
///
|
||||
/// Thread safety: All public methods must only be called from the main loop.
|
||||
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
|
||||
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
|
||||
/// (when enabled) must not call any of these methods — it communicates with the
|
||||
/// main loop exclusively via App.wake_loop_threadsafe().
|
||||
class IDFUARTComponent : public UARTComponent, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
void flush() override;
|
||||
|
||||
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Load the UART with the current settings.
|
||||
@@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
protected:
|
||||
void check_logger_conflict() override;
|
||||
uart_port_t uart_num_;
|
||||
QueueHandle_t uart_event_queue_;
|
||||
uart_config_t get_config_();
|
||||
SemaphoreHandle_t lock_;
|
||||
|
||||
bool has_peek_{false};
|
||||
uint8_t peek_byte_;
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// RX notification support
|
||||
// RX notification support — runs on a separate FreeRTOS task.
|
||||
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
|
||||
// event queue and call App.wake_loop_threadsafe().
|
||||
void start_rx_event_task_();
|
||||
static void rx_event_task_func(void *param);
|
||||
|
||||
QueueHandle_t uart_event_queue_;
|
||||
TaskHandle_t rx_event_task_handle_{nullptr};
|
||||
#endif // USE_UART_WAKE_LOOP_ON_RX
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.3.0-dev"
|
||||
__version__ = "2026.2.0b2"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -136,7 +136,7 @@ extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260206/esp-idf-v5.5.2.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
lib_deps =
|
||||
@@ -171,7 +171,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2.260206/esp-idf-v5.5.2.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
framework = espidf
|
||||
lib_deps =
|
||||
|
||||
@@ -369,7 +369,7 @@ def get_logger_tags():
|
||||
"api.service",
|
||||
]
|
||||
for file in CORE_COMPONENTS_PATH.rglob("*.cpp"):
|
||||
data = file.read_text()
|
||||
data = file.read_text(encoding="utf-8")
|
||||
match = pattern.search(data)
|
||||
if match:
|
||||
tags.append(match.group(1))
|
||||
|
||||
@@ -3,15 +3,9 @@ display:
|
||||
spi_16: true
|
||||
pixel_mode: 18bit
|
||||
model: ili9488
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: ${dc_pin}
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: ${cs_pin}
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: ${reset_pin}
|
||||
dc_pin: ${dc_pin}
|
||||
cs_pin: ${cs_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
data_rate: 20MHz
|
||||
invert_colors: true
|
||||
show_test_card: true
|
||||
@@ -30,15 +24,3 @@ display:
|
||||
height: 200
|
||||
enable_pin: ${enable_pin}
|
||||
bus_mode: single
|
||||
|
||||
- platform: mipi_spi
|
||||
model: WAVESHARE-1.83-V2
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: ${dc_pin}
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: ${cs_pin}
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: ${reset_pin}
|
||||
|
||||
@@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions(
|
||||
# The chime_sensor has chime: true, so opening it while disarmed
|
||||
# should trigger on_chime callback
|
||||
|
||||
# Set up future for the on_ready from opening the chime sensor
|
||||
# (alarm becomes "not ready" when chime sensor opens).
|
||||
# We must wait for this BEFORE creating the close future, otherwise
|
||||
# the open event's log can arrive late and resolve the close future,
|
||||
# causing the test to proceed before the chime close is processed.
|
||||
ready_after_chime_open: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.append(ready_after_chime_open)
|
||||
|
||||
# We're currently DISARMED - open the chime sensor
|
||||
client.switch_command(chime_switch_info.key, True)
|
||||
|
||||
@@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions(
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Close the chime sensor and wait for alarm to become ready again
|
||||
# We need to wait for this transition before testing door sensor,
|
||||
# otherwise there's a race where the door sensor state change could
|
||||
# arrive before the chime sensor state change, leaving the alarm in
|
||||
# a continuous "not ready" state with no on_ready callback fired.
|
||||
# Wait for the on_ready from the chime sensor opening
|
||||
try:
|
||||
await asyncio.wait_for(ready_after_chime_open, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when chime sensor opened. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Now create the future for the close event and close the sensor.
|
||||
# Since we waited for the open event above, the close event's
|
||||
# on_ready log cannot be confused with the open event's.
|
||||
ready_after_chime_close: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.append(ready_after_chime_close)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user