Compare commits

...

66 Commits

Author SHA1 Message Date
kbx81 f8bec0813d fix 2026-03-13 16:48:56 -05:00
kbx81 84762e6ae0 oops 2026-03-13 16:46:13 -05:00
kbx81 2edf313ee3 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-03-13 16:45:23 -05:00
Thomas SAMTER 1eed1adfa0 [pid] Replace std::deque with FixedRingBuffer (#14733)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 11:38:45 -10:00
J. Nick Koston a6c08576be [sensor] Use FixedRingBuffer in SlidingWindowFilter, add window_size limit (#14736) 2026-03-13 10:17:40 -10:00
dependabot[bot] f41aa8b18c Bump ruff from 0.15.5 to 0.15.6 (#14774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 19:35:10 +00:00
Jonathan Swoboda 6700347a48 [wifi] Fix ESP-IDF 6.0 compatibility (#14766)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:47:12 -04:00
Jonathan Swoboda b147830ef9 [core] Fix std::isnan conflict with picolibc on ESP-IDF 6.0 (#14768)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:24:39 -04:00
J. Nick Koston bd844fcd0a [template] Fix misleading 'Text value too long to save' warning (#14753) 2026-03-13 07:37:44 -10:00
J. Nick Koston 8936be628f [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) 2026-03-13 07:37:30 -10:00
J. Nick Koston 5920fa97e4 [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) 2026-03-13 09:20:50 -04:00
Kjell Braden 326769e43c [runtime_image] fix BMP parsing (#14762) 2026-03-13 09:18:42 -04:00
Thomas SAMTER 7524590bcf [const] Add CONF_CLIMATE_ID for climate component sub-entities (#14764) 2026-03-13 09:17:11 -04:00
Michael Kerscher 15ec46abfe [vbus] add DeltaSol CS4 (Citrin Solar 1.3) (#12477) 2026-03-12 22:31:16 -07:00
J. Nick Koston 920af91db6 [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) 2026-03-13 01:37:46 +00:00
J. Nick Koston a744261934 [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 01:12:22 +00:00
J. Nick Koston 59c1368440 [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) 2026-03-12 14:53:46 -10:00
J. Nick Koston 7e8e085a04 [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) 2026-03-12 14:49:07 -10:00
J. Nick Koston 22b25724ae [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) 2026-03-12 14:48:55 -10:00
J. Nick Koston 89719cf4b2 [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) 2026-03-12 14:48:41 -10:00
J. Nick Koston e15b19b223 [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 14:48:29 -10:00
J. Nick Koston 2ca13972b9 [debug] Fix missing reset reason for RP2040/RP2350 (#14740) 2026-03-12 14:48:06 -10:00
J. Nick Koston 7bb4e75459 [rp2040] Use full flash for sketch in testing mode (#14747)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:47:16 -10:00
J. Nick Koston fd8e510745 [light] Fix ambiguous set_effect overload for const char* (#14732) 2026-03-12 18:28:25 -05:00
Brian Kaufman 25c74c8f99 [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) 2026-03-12 13:23:29 -10:00
J. Nick Koston 05d285ba86 [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) 2026-03-12 07:16:53 -10:00
J. Nick Koston 186ca4e458 [uart] Allow hardware UART with single pin on RP2040 (#14725) 2026-03-12 07:16:38 -10:00
J. Nick Koston 618312f0ee [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) 2026-03-12 07:16:23 -10:00
J. Nick Koston 70d188202a [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) 2026-03-12 07:16:08 -10:00
J. Nick Koston 4a21afe7ce [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) 2026-03-12 07:15:48 -10:00
J. Nick Koston fd1d016795 [time] Fix settimeofday() failure on ESP8266 (#14707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 07:15:34 -10:00
J. Nick Koston 03c091adfc [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:15:21 -10:00
J. Nick Koston a3a88acfcf [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) 2026-03-12 07:15:04 -10:00
J. Nick Koston 07f8ae6c82 [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) 2026-03-12 07:14:49 -10:00
Matthias König 25c30ac5bb [mqtt] Fixed permission denied error for client certificates on Windows (#13525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-12 12:00:08 -04:00
guillempages a76767a0ab [runtime_image] Update jpegdec lib version (#14726)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-12 10:15:20 -04:00
Kevin Ahrendt 511d185772 [audio] Bump microOpus to v0.3.5 (#14727) 2026-03-12 08:56:01 -04:00
Brian Kaufman c4c19c8a6c [web_server] use DETAIL_ALL in update_all_json_generator (#14711) 2026-03-11 23:07:26 -10:00
Massimo Antonello fe2d60ccec [one_wire] allow changing address at runtime (#12150) 2026-03-12 01:52:58 -07:00
Keith Burzinski 657890695f [ledc] Fix high-pressure crash & recovery (#14720) 2026-03-12 03:16:02 -05:00
Adam DeMuri 8a5f008aee [modbus] Fix buffer overflow in modbus (#14719)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-11 22:00:26 -10:00
J. Nick Koston f8a22b87b8 [rp2040] Fix crash handler design flaws (#14716) 2026-03-12 18:23:01 +13:00
Keith Burzinski 7f38d95424 [ethernet] ESP32-S3 Ethernet compilation fix (#14717) 2026-03-11 23:48:27 -05:00
Javier Peletier bb7d96b954 [const] Add UNIT_METER_PER_SECOND, UNIT_MILLILITRE, UNIT_POUND to const.py (#14713) 2026-03-11 16:31:17 -10:00
J. Nick Koston 8daa946afa [esp32] Add crash handler to capture and report backtrace across reboots (#14709) 2026-03-12 14:00:20 +13:00
Keith Burzinski ddc40f44fa [ethernet] ESP32-P4 Ethernet compilation fix (#14714)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-11 19:56:25 -05:00
Jonathan Swoboda 409640c0ee [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:30:44 -04:00
Jesse Hills 822c9161c6 Merge branch 'beta' into dev 2026-03-12 09:15:50 +13:00
dependabot[bot] a060f175ad Bump actions/download-artifact from 8.0.0 to 8.0.1 (#14705)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:46 -10:00
dependabot[bot] 73f305ff9c Bump tornado from 6.5.4 to 6.5.5 (#14704)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:19 -10:00
Jesse Hills b6ff7185e7 [ci] Dont run codeowners workflows on release or beta PRs (#14703) 2026-03-12 08:04:07 +13:00
J. Nick Koston 928f6f1866 [ci] Add PR title check for unescaped angle brackets (#14701) 2026-03-12 07:57:43 +13:00
Jesse Hills e7c3277eeb Bump version to 2026.4.0-dev 2026-03-12 07:34:53 +13:00
kbx81 ae9c999052 fix 2026-02-28 23:21:30 -06:00
kbx81 7d2f6fbf55 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-28 23:12:31 -06:00
kbx81 608bef86cc Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-26 23:42:43 -06:00
kbx81 6514dc2fe1 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-26 20:55:50 -06:00
kbx81 240afd23b3 ... 2026-02-26 14:31:17 -06:00
kbx81 156c2a8cb0 optimize 2026-02-26 14:30:31 -06:00
kbx81 908c47bb5e preen, tune 2026-02-25 23:28:44 -06:00
kbx81 6df3a30740 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-25 17:33:27 -06:00
kbx81 0aaf59dbed Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-24 16:51:04 -06:00
kbx81 249c5bb724 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-23 18:01:56 -06:00
kbx81 54ea8dd207 Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy 2026-02-19 18:31:15 -06:00
puddly 4cfb794b62 WIP 2026-02-19 18:22:03 -05:00
kbx81 917af8ff31 [zigbee_proxy] New component 2026-02-19 14:34:29 -06:00
130 changed files with 4500 additions and 308 deletions
+1 -1
View File
@@ -1 +1 @@
e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
+2 -2
View File
@@ -945,13 +945,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -10,6 +10,9 @@ name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches-ignore:
- release
- beta
permissions:
issues: write
@@ -13,6 +13,9 @@ on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
branches-ignore:
- release
- beta
permissions:
pull-requests: write
+12
View File
@@ -65,6 +65,18 @@ jobs:
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}
// Check title starts with [tag] prefix
const bracketPattern = /^\[\w+\]/;
if (!bracketPattern.test(title)) {
+1 -1
View File
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: digests-*
path: /tmp/digests
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.5
rev: v0.15.6
hooks:
# Run the linter.
- id: ruff
+1 -1
View File
@@ -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.0b1
PROJECT_NUMBER = 2026.4.0-dev
# 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
@@ -8,6 +8,13 @@
#endif // CYW43_USES_VSYS_PIN
#include <hardware/adc.h>
// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h),
// but the Arduino framework's config_autogen.h includes a generic board header
// that doesn't define it. Provide the standard value (pin 29) as a fallback.
#ifndef PICO_VSYS_PIN
#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage)
#endif
namespace esphome {
namespace adc {
@@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
// - Save the current effect index.
this->last_effect_index_ = light_state_->get_current_effect_index();
// - Disable any current effect.
light_state_->make_call().set_effect(0).perform();
light_state_->make_call().set_effect(uint32_t{0}).perform();
}
}
enabled_ = enabled;
+33
View File
@@ -69,6 +69,9 @@ service APIConnection {
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc zigbee_proxy_frame(ZigbeeProxyFrame) returns (void) {}
rpc zigbee_proxy_request(ZigbeeProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
@@ -281,6 +284,10 @@ message DeviceInfoResponse {
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
// Indicates if Zigbee proxy support is available and features supported
uint32 zigbee_proxy_feature_flags = 26 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
uint64 zigbee_ieee_address = 27 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
}
message ListEntitiesRequest {
@@ -2669,3 +2676,29 @@ message BluetoothSetConnectionParamsResponse {
uint64 address = 1;
int32 error = 2;
}
// ==================== ZIGBEE ====================
message ZigbeeProxyFrame {
option (id) = 148;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_ZIGBEE_PROXY";
option (no_delay) = true;
bytes data = 1;
}
enum ZigbeeProxyRequestType {
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0;
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1;
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2;
}
message ZigbeeProxyRequest {
option (id) = 149;
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_ZIGBEE_PROXY";
ZigbeeProxyRequestType type = 1;
bytes data = 2;
}
+22
View File
@@ -43,6 +43,9 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_ZIGBEE_PROXY
#include "esphome/components/zigbee_proxy/zigbee_proxy.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
@@ -1317,6 +1320,16 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
}
#endif
#ifdef USE_ZIGBEE_PROXY
void APIConnection::on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) {
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_frame(this, msg);
}
void APIConnection::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_request(this, msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
@@ -1630,6 +1643,11 @@ void APIConnection::complete_authentication_() {
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
}
#endif
#ifdef USE_ZIGBEE_PROXY
if (zigbee_proxy::global_zigbee_proxy != nullptr) {
zigbee_proxy::global_zigbee_proxy->api_connection_authenticated(this);
}
#endif
}
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
@@ -1771,6 +1789,10 @@ bool APIConnection::send_device_info_response_() {
info.port_type = proxy->get_port_type();
}
#endif
#ifdef USE_ZIGBEE_PROXY
resp.zigbee_proxy_feature_flags = zigbee_proxy::global_zigbee_proxy->get_feature_flags();
resp.zigbee_ieee_address = zigbee_proxy::global_zigbee_proxy->get_ieee_address();
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#endif
+18
View File
@@ -14,6 +14,12 @@
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
@@ -174,6 +180,12 @@ class APIConnection final : public APIServerConnectionBase {
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
#ifdef USE_ZIGBEE_PROXY
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) override;
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) override;
void send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { this->send_message(msg); }
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
@@ -235,6 +247,12 @@ class APIConnection final : public APIServerConnectionBase {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
#endif
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
+17 -7
View File
@@ -134,12 +134,16 @@ class APIFrameHelper {
//
// For log messages: Use Nagle to coalesce multiple small log packets into
// fewer larger packets, reducing WiFi overhead. However, we limit batching
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
// shared pbufs, but holding data too long waiting for Nagle's timer causes
// buffer exhaustion and dropped messages.
// to avoid excessive LWIP buffer pressure on memory-constrained devices.
// LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
// holding data too long waiting for Nagle's timer causes buffer exhaustion
// and dropped messages.
//
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
// ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle
// ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
//
// Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
// Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
@@ -150,7 +154,7 @@ class APIFrameHelper {
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
// Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush)
if (this->nodelay_state_ == NODELAY_ON) {
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
@@ -255,10 +259,16 @@ class APIFrameHelper {
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
// (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
static constexpr int8_t NODELAY_ON = -1;
#ifdef USE_ESP8266
static constexpr int8_t LOG_NAGLE_COUNT = 2;
#else
static constexpr int8_t LOG_NAGLE_COUNT = 3;
#endif
int8_t nodelay_state_{NODELAY_ON};
// Internal helper to set TCP_NODELAY socket option
@@ -258,10 +258,13 @@ APIError APINoiseFrameHelper::state_action_() {
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
size_t rx_size = this->rx_buf_.size();
this->prologue_.resize(old_size + 2 + rx_size);
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
}
state_ = State::SERVER_HELLO;
}
+64
View File
@@ -142,6 +142,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_sub_message(25, it);
}
#endif
#ifdef USE_ZIGBEE_PROXY
buffer.encode_uint32(26, this->zigbee_proxy_feature_flags);
#endif
#ifdef USE_ZIGBEE_PROXY
buffer.encode_uint64(27, this->zigbee_ieee_address);
#endif
}
uint32_t DeviceInfoResponse::calculate_size() const {
uint32_t size = 0;
@@ -202,6 +208,12 @@ uint32_t DeviceInfoResponse::calculate_size() const {
for (const auto &it : this->serial_proxies) {
size += ProtoSize::calc_message_force(2, it.calculate_size());
}
#endif
#ifdef USE_ZIGBEE_PROXY
size += ProtoSize::calc_uint32(2, this->zigbee_proxy_feature_flags);
#endif
#ifdef USE_ZIGBEE_PROXY
size += ProtoSize::calc_uint64(2, this->zigbee_ieee_address);
#endif
return size;
}
@@ -3889,5 +3901,57 @@ uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const {
return size;
}
#endif
#ifdef USE_ZIGBEE_PROXY
bool ZigbeeProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
this->data = value.data();
this->data_len = value.size();
break;
}
default:
return false;
}
return true;
}
void ZigbeeProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); }
uint32_t ZigbeeProxyFrame::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_length(1, this->data_len);
return size;
}
bool ZigbeeProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->type = static_cast<enums::ZigbeeProxyRequestType>(value);
break;
default:
return false;
}
return true;
}
bool ZigbeeProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->data = value.data();
this->data_len = value.size();
break;
}
default:
return false;
}
return true;
}
void ZigbeeProxyRequest::encode(ProtoWriteBuffer &buffer) const {
buffer.encode_uint32(1, static_cast<uint32_t>(this->type));
buffer.encode_bytes(2, this->data, this->data_len);
}
uint32_t ZigbeeProxyRequest::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type));
size += ProtoSize::calc_length(1, this->data_len);
return size;
}
#endif
} // namespace esphome::api
+54 -1
View File
@@ -341,6 +341,13 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
};
#endif
#ifdef USE_ZIGBEE_PROXY
enum ZigbeeProxyRequestType : uint32_t {
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0,
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2,
};
#endif
} // namespace enums
@@ -518,7 +525,7 @@ class SerialProxyInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 309;
static constexpr uint16_t ESTIMATED_SIZE = 319;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
@@ -573,6 +580,12 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
#ifdef USE_SERIAL_PROXY
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
#endif
#ifdef USE_ZIGBEE_PROXY
uint32_t zigbee_proxy_feature_flags{0};
#endif
#ifdef USE_ZIGBEE_PROXY
uint64_t zigbee_ieee_address{0};
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint32_t calculate_size() const;
@@ -3285,5 +3298,45 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_ZIGBEE_PROXY
class ZigbeeProxyFrame final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 148;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "zigbee_proxy_frame"; }
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
void encode(ProtoWriteBuffer &buffer) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ZigbeeProxyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 149;
static constexpr uint8_t ESTIMATED_SIZE = 21;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "zigbee_proxy_request"; }
#endif
enums::ZigbeeProxyRequestType type{};
const uint8_t *data{nullptr};
uint16_t data_len{0};
void encode(ProtoWriteBuffer &buffer) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
} // namespace esphome::api
-2
View File
@@ -3,10 +3,8 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLUETOOTH_PROXY
#ifndef USE_API_VARINT64
#define USE_API_VARINT64
#endif
#endif
namespace esphome::api {} // namespace esphome::api
+34 -1
View File
@@ -13,7 +13,7 @@ namespace esphome::api {
static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) {
out.append("'");
if (!ref.empty()) {
out.append(ref.c_str());
out.append(ref.c_str(), ref.size());
}
out.append("'");
}
@@ -806,6 +806,20 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
}
}
#endif
#ifdef USE_ZIGBEE_PROXY
template<> const char *proto_enum_to_string<enums::ZigbeeProxyRequestType>(enums::ZigbeeProxyRequestType value) {
switch (value) {
case enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
return "ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE";
case enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
return "ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
return "ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO";
default:
return "UNKNOWN";
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HelloRequest");
@@ -930,6 +944,12 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
it.dump_to(out);
out.append("\n");
}
#endif
#ifdef USE_ZIGBEE_PROXY
dump_field(out, "zigbee_proxy_feature_flags", this->zigbee_proxy_feature_flags);
#endif
#ifdef USE_ZIGBEE_PROXY
dump_field(out, "zigbee_ieee_address", this->zigbee_ieee_address);
#endif
return out.c_str();
}
@@ -2651,6 +2671,19 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const
return out.c_str();
}
#endif
#ifdef USE_ZIGBEE_PROXY
const char *ZigbeeProxyFrame::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ZigbeeProxyFrame");
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ZigbeeProxyRequest");
dump_field(out, "type", static_cast<enums::ZigbeeProxyRequestType>(this->type));
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
#endif
} // namespace esphome::api
@@ -700,6 +700,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_bluetooth_set_connection_params_request(msg);
break;
}
#endif
#ifdef USE_ZIGBEE_PROXY
case ZigbeeProxyFrame::MESSAGE_TYPE: {
ZigbeeProxyFrame msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_zigbee_proxy_frame"), msg);
#endif
this->on_zigbee_proxy_frame(msg);
break;
}
#endif
#ifdef USE_ZIGBEE_PROXY
case ZigbeeProxyRequest::MESSAGE_TYPE: {
ZigbeeProxyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_zigbee_proxy_request"), msg);
#endif
this->on_zigbee_proxy_request(msg);
break;
}
#endif
default:
break;
+6
View File
@@ -238,6 +238,12 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
#ifdef USE_ZIGBEE_PROXY
virtual void on_zigbee_proxy_frame(const ZigbeeProxyFrame &value){};
#endif
#ifdef USE_ZIGBEE_PROXY
virtual void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
#endif
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
+21
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
from datetime import datetime
import importlib
import logging
from typing import TYPE_CHECKING, Any
import warnings
@@ -18,6 +19,7 @@ import contextlib
from esphome.const import CONF_KEY, CONF_PORT, __version__
from esphome.core import CORE
from esphome.platformio_api import process_stacktrace
from . import CONF_ENCRYPTION
@@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
addresses=addresses, # Pass all addresses for automatic retry
)
dashboard = CORE.dashboard
backtrace_state = False
# Try platform-specific stacktrace handler first, fall back to generic
platform_process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
platform_process_stacktrace = getattr(module, "process_stacktrace")
except (AttributeError, ImportError):
pass
def on_log(msg: SubscribeLogsResponse) -> None:
"""Handle a new log message."""
nonlocal backtrace_state
time_ = datetime.now()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
@@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
)
for parsed_msg in parse_log_message(text, timestamp):
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
for raw_line in text.splitlines():
if platform_process_stacktrace:
backtrace_state = platform_process_stacktrace(
config, raw_line, backtrace_state
)
else:
backtrace_state = process_stacktrace(
config, raw_line, backtrace_state=backtrace_state
)
stop = await async_run(cli, on_log, name=name)
try:
+1 -1
View File
@@ -214,4 +214,4 @@ async def to_code(config):
cg.add_define("USE_AUDIO_MP3_SUPPORT")
if data.opus_support:
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
add_idf_component(name="esphome/micro-opus", ref="0.3.4")
add_idf_component(name="esphome/micro-opus", ref="0.3.5")
@@ -61,7 +61,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
// Defer save to main loop thread to avoid NVS operations from HTTP thread
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
#endif
request->redirect(ESPHOME_F("/?save"));
request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting..."));
}
void CaptivePortal::setup() {
@@ -71,7 +71,7 @@ void CaptivePortal::setup() {
void CaptivePortal::start() {
this->base_->init();
if (!this->initialized_) {
this->base_->add_handler(this);
this->base_->add_handler_without_auth(this);
}
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
+1
View File
@@ -3,6 +3,7 @@
CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_CLIMATE_ID = "climate_id"
BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian"
+61 -3
View File
@@ -1,23 +1,81 @@
#include "debug_component.h"
#ifdef USE_RP2040
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <Arduino.h>
#include <hardware/watchdog.h>
#if defined(PICO_RP2350)
#include <hardware/structs/powman.h>
#else
#include <hardware/structs/vreg_and_chip_reset.h>
#endif
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
namespace esphome {
namespace debug {
static const char *const TAG = "debug";
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
char *buf = buffer.data();
const size_t size = RESET_REASON_BUFFER_SIZE;
size_t pos = 0;
#if defined(PICO_RP2350)
uint32_t chip_reset = powman_hw->chip_reset;
if (chip_reset & 0x04000000) // HAD_GLITCH_DETECT
pos = buf_append_str(buf, size, pos, "Power supply glitch|");
if (chip_reset & 0x00040000) // HAD_RUN_LOW
pos = buf_append_str(buf, size, pos, "RUN pin|");
if (chip_reset & 0x00020000) // HAD_BOR
pos = buf_append_str(buf, size, pos, "Brown-out|");
if (chip_reset & 0x00010000) // HAD_POR
pos = buf_append_str(buf, size, pos, "Power-on reset|");
#else
uint32_t chip_reset = vreg_and_chip_reset_hw->chip_reset;
if (chip_reset & 0x00010000) // HAD_RUN
pos = buf_append_str(buf, size, pos, "RUN pin|");
if (chip_reset & 0x00000100) // HAD_POR
pos = buf_append_str(buf, size, pos, "Power-on reset|");
#endif
if (watchdog_caused_reboot()) {
bool handled = false;
#ifdef USE_RP2040_CRASH_HANDLER
if (rp2040::crash_handler_has_data()) {
pos = buf_append_str(buf, size, pos, "Crash (HardFault)|");
handled = true;
}
#endif
if (!handled) {
if (watchdog_enable_caused_reboot()) {
pos = buf_append_str(buf, size, pos, "Watchdog timeout|");
} else {
pos = buf_append_str(buf, size, pos, "Software reset|");
}
}
}
// Remove trailing '|'
if (pos > 0 && buf[pos - 1] == '|') {
buf[pos - 1] = '\0';
} else if (pos == 0) {
return "Unknown";
}
return buf;
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); }
uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); }
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
uint32_t cpu_freq = rp2040.f_cpu();
uint32_t cpu_freq = ::rp2040.f_cpu();
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
+5
View File
@@ -1442,6 +1442,11 @@ async def to_code(config):
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.add_build_flag("-Wl,-z,noexecstack")
# Arduino already wraps esp_panic_handler for its own backtrace handler,
# so only add our wrap when using ESP-IDF framework to avoid linker conflicts.
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
variant = config[CONF_VARIANT]
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
+6
View File
@@ -1,6 +1,7 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -36,6 +37,11 @@ void arch_restart() {
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
+355
View File
@@ -0,0 +1,355 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "crash_handler.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
#include <esp_attr.h>
#include <esp_private/panic_internal.h>
#include <soc/soc.h>
#if CONFIG_IDF_TARGET_ARCH_XTENSA
#include <esp_cpu_utils.h>
#include <esp_debug_helpers.h>
#include <xtensa_context.h>
#elif CONFIG_IDF_TARGET_ARCH_RISCV
#include <riscv/rvruntime-frames.h>
#endif
static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
static constexpr size_t MAX_BACKTRACE = 16;
// Check if an address looks like code (flash-mapped or IRAM).
// Must be safe to call from panic context (no flash access needed).
static inline bool IRAM_ATTR is_code_addr(uint32_t addr) {
return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH);
}
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Check if a code address is a real return address by verifying the preceding
// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during
// panic) so flash cache is available and both IRAM and IROM are safely readable.
static inline bool is_return_addr(uint32_t addr) {
if (!is_code_addr(addr) || addr < 4)
return false;
// A return address on the stack points to the instruction after a call.
// Check for 4-byte JAL/JALR call instruction before this address.
// Use memcpy for alignment safety — RISC-V C extension means code addresses
// are only 2-byte aligned, so addr-4 may not be 4-byte aligned.
uint32_t inst;
memcpy(&inst, (const void *) (addr - 4), sizeof(inst));
// RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd
uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode
uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7)
// Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7)
if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80)
return true;
// Check for 2-byte compressed c.jalr before this address (C extension).
// c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10
if (addr >= 2) {
uint16_t c_inst = *(uint16_t *) (addr - 2);
if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0)
return true;
}
return false;
}
#endif
// Raw crash data written by the panic handler wrapper.
// Lives in .noinit so it survives software reset but contains garbage after power cycle.
// Validated by magic marker. Static linkage since it's only used within this file.
// Version field is first so future firmware can always identify the struct layout.
// Magic is second to validate the data. Remaining fields can change between versions.
// Version is uint32_t because it would be padded to 4 bytes anyway before the next
// uint32_t field, so we use the full width rather than wasting 3 bytes of padding.
static constexpr uint32_t CRASH_DATA_VERSION = 1;
struct RawCrashData {
uint32_t version;
uint32_t magic;
uint32_t pc;
uint8_t backtrace_count;
uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned)
uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG)
uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic)
uint32_t backtrace[MAX_BACKTRACE];
uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V)
};
static RawCrashData __attribute__((section(".noinit")))
s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Whether crash data was found and validated this boot.
static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
namespace esphome::esp32 {
static const char *const TAG = "esp32.crash";
void crash_handler_read_and_clear() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
// Clamp counts to prevent out-of-bounds reads from corrupt .noinit data
if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE)
s_raw_crash_data.backtrace_count = MAX_BACKTRACE;
if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count)
s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count;
if (s_raw_crash_data.exception > 4) // panic_exception_t max value
s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT
if (s_raw_crash_data.pseudo_excause > 1)
s_raw_crash_data.pseudo_excause = 0;
}
// Clear magic regardless so we don't re-report on next normal reboot
s_raw_crash_data.magic = 0;
}
bool crash_handler_has_data() { return s_crash_data_valid; }
// Look up the exception cause as a human-readable string.
// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays
// not exposed via any public API.
static const char *get_exception_reason() {
#if CONFIG_IDF_TARGET_ARCH_XTENSA
if (s_raw_crash_data.pseudo_excause) {
// SoC-level panic: watchdog, cache error, etc.
// Keep in sync with ESP-IDF's PANIC_RSN_* defines
static const char *const PSEUDO_REASON[] = {
"Unknown reason", // 0
"Unhandled debug exception", // 1
"Double exception", // 2
"Unhandled kernel exception", // 3
"Coprocessor exception", // 4
"Interrupt wdt timeout on CPU0", // 5
"Interrupt wdt timeout on CPU1", // 6
"Cache error", // 7
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0]))
return PSEUDO_REASON[cause];
return PSEUDO_REASON[0];
}
// Real Xtensa exception
static const char *const REASON[] = {
"IllegalInstruction",
"Syscall",
"InstructionFetchError",
"LoadStoreError",
"Level1Interrupt",
"Alloca",
"IntegerDivideByZero",
"PCValue",
"Privileged",
"LoadStoreAlignment",
nullptr,
nullptr,
"InstrPDAddrError",
"LoadStorePIFDataError",
"InstrPIFAddrError",
"LoadStorePIFAddrError",
"InstTLBMiss",
"InstTLBMultiHit",
"InstFetchPrivilege",
nullptr,
"InstrFetchProhibited",
nullptr,
nullptr,
nullptr,
"LoadStoreTLBMiss",
"LoadStoreTLBMultihit",
"LoadStorePrivilege",
nullptr,
"LoadProhibited",
"StoreProhibited",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
return REASON[cause];
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// For SoC-level panics (watchdog, cache error), mcause holds IDF-internal
// interrupt numbers, not standard RISC-V cause codes. The exception type
// field already identifies these, so just return null to use the type name.
if (s_raw_crash_data.pseudo_excause)
return nullptr;
static const char *const REASON[] = {
"Instruction address misaligned",
"Instruction access fault",
"Illegal instruction",
"Breakpoint",
"Load address misaligned",
"Load access fault",
"Store address misaligned",
"Store access fault",
"Environment call from U-mode",
"Environment call from S-mode",
nullptr,
"Environment call from M-mode",
"Instruction page fault",
"Load page fault",
nullptr,
"Store page fault",
};
uint32_t cause = s_raw_crash_data.cause;
if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr)
return REASON[cause];
#endif
return "Unknown";
}
// Exception type names matching panic_exception_t enum
static const char *get_exception_type() {
static const char *const TYPES[] = {
"Debug exception", // PANIC_EXCEPTION_DEBUG
"Interrupt wdt", // PANIC_EXCEPTION_IWDT
"Task wdt", // PANIC_EXCEPTION_TWDT
"Abort", // PANIC_EXCEPTION_ABORT
"Fault", // PANIC_EXCEPTION_FAULT
};
uint8_t exc = s_raw_crash_data.exception;
if (exc < sizeof(TYPES) / sizeof(TYPES[0]))
return TYPES[exc];
return "Unknown";
}
// Intentionally uses separate ESP_LOGE calls per line instead of combining into
// one multi-line log message. This ensures each address appears as its own line
// on the serial console, making it possible to see partial output if the device
// crashes again during boot, and allowing the CLI's process_stacktrace to match
// and decode each address individually.
void crash_handler_log() {
if (!s_crash_data_valid)
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
const char *reason = get_exception_reason();
if (reason != nullptr) {
ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason);
} else {
ESP_LOGE(TAG, " Reason: %s", get_exception_type());
}
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
uint8_t bt_num = 0;
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) {
uint32_t addr = s_raw_crash_data.backtrace[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones.
if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr))
continue;
#endif
#if CONFIG_IDF_TARGET_ARCH_RISCV
const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan";
#else
const char *source = "backtrace";
#endif
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source);
}
// Build addr2line hint with all captured addresses for easy copy-paste
char hint[256];
int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc);
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) {
uint32_t addr = s_raw_crash_data.backtrace[i];
#if CONFIG_IDF_TARGET_ARCH_RISCV
if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr))
continue;
#endif
pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr);
}
ESP_LOGE(TAG, "%s", hint);
}
} // namespace esphome::esp32
// --- Panic handler wrapper ---
// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data
// into NOINIT memory before the normal panic handler runs.
//
extern "C" {
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
// Names are mandated by the --wrap linker mechanism
extern void __real_esp_panic_handler(panic_info_t *info);
void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) {
// Save the faulting PC and exception info
s_raw_crash_data.pc = (uint32_t) info->addr;
s_raw_crash_data.backtrace_count = 0;
s_raw_crash_data.reg_frame_count = 0;
s_raw_crash_data.exception = (uint8_t) info->exception;
s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
// Xtensa: walk the backtrace using the public API
if (info->frame != nullptr) {
auto *xt_frame = (XtExcFrame *) info->frame;
s_raw_crash_data.cause = xt_frame->exccause;
esp_backtrace_frame_t bt_frame = {
.pc = (uint32_t) xt_frame->pc,
.sp = (uint32_t) xt_frame->a1,
.next_pc = (uint32_t) xt_frame->a0,
.exc_frame = xt_frame,
};
uint8_t count = 0;
// First frame PC
uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(first_pc)) {
s_raw_crash_data.backtrace[count++] = first_pc;
}
// Walk remaining frames
while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) {
if (!esp_backtrace_get_next_frame(&bt_frame)) {
break;
}
uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc);
if (is_code_addr(pc)) {
s_raw_crash_data.backtrace[count++] = pc;
}
}
s_raw_crash_data.backtrace_count = count;
}
#elif CONFIG_IDF_TARGET_ARCH_RISCV
// RISC-V: capture MEPC + RA, then scan stack for code addresses
if (info->frame != nullptr) {
auto *rv_frame = (RvExcFrame *) info->frame;
s_raw_crash_data.cause = rv_frame->mcause;
uint8_t count = 0;
// Save MEPC (fault PC) and RA (return address)
if (is_code_addr(rv_frame->mepc)) {
s_raw_crash_data.backtrace[count++] = rv_frame->mepc;
}
if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) {
s_raw_crash_data.backtrace[count++] = rv_frame->ra;
}
// Track how many entries came from registers (MEPC/RA) so we can
// skip return-address validation for them at log time.
s_raw_crash_data.reg_frame_count = count;
// Scan stack for code addresses — captures broadly during panic,
// filtered by is_return_addr() at log time when flash is accessible.
auto *scan_start = (uint32_t *) rv_frame->sp;
for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) {
uint32_t val = scan_start[i];
if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) {
s_raw_crash_data.backtrace[count++] = val;
}
}
s_raw_crash_data.backtrace_count = count;
}
#endif
// Write version and magic last — ensures all data is written before we mark it valid
s_raw_crash_data.version = CRASH_DATA_VERSION;
s_raw_crash_data.magic = CRASH_MAGIC;
// Call the real panic handler (prints to UART, does core dump, reboots, etc.)
__real_esp_panic_handler(info);
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
} // extern "C"
#endif // USE_ESP32_CRASH_HANDLER
#endif // USE_ESP32
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#ifdef USE_ESP32_CRASH_HANDLER
namespace esphome::esp32 {
/// Read crash data from NOINIT memory and clear the magic marker.
void crash_handler_read_and_clear();
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
/// Returns true if crash data was found this boot.
bool crash_handler_has_data();
} // namespace esphome::esp32
#endif // USE_ESP32_CRASH_HANDLER
@@ -27,6 +27,7 @@ static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s
static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum)
static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms
static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s
static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s
static const esp_bt_uuid_t NOTIFY_DESC_UUID = {
.len = ESP_UUID_LEN_16,
.uuid =
@@ -62,6 +63,15 @@ void BLEClientBase::loop() {
// will enable it again when a connection is needed.
else if (this->state() == espbt::ClientState::IDLE) {
this->disable_loop();
} else if (this->state() == espbt::ClientState::DISCONNECTING &&
(millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) {
ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_,
this->address_str_);
// release_services() must be called before set_idle_() — if we entered DISCONNECTING
// via unconditional_disconnect() (which doesn't call release_services()), and ESP-IDF
// never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call.
this->release_services();
this->set_idle_();
}
}
@@ -101,12 +111,16 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
#endif
void BLEClientBase::connect() {
// Prevent duplicate connection attempts
// Prevent duplicate connection attempts or connecting while still disconnecting
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
this->state() == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state()));
return;
} else if (this->state() == espbt::ClientState::DISCONNECTING) {
ESP_LOGW(TAG, "[%d] [%s] Cannot connect, still waiting for CLOSE_EVT to complete disconnect",
this->connection_index_, this->address_str_);
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
this->paired_ = false;
@@ -174,7 +188,7 @@ void BLEClientBase::unconditional_disconnect() {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::DISCONNECTING);
this->set_disconnecting_();
}
}
@@ -220,6 +234,7 @@ void BLEClientBase::log_connection_params_(const char *param_type) {
void BLEClientBase::handle_connection_result_(esp_err_t ret) {
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
// Don't use set_idle_() here — CONNECT_EVT never fired so conn_id_ is still UNSET_CONN_ID.
this->set_state(espbt::ClientState::IDLE);
}
}
@@ -311,15 +326,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
this->set_state(espbt::ClientState::IDLE);
// Connection was never established so CLOSE_EVT may not follow
this->set_idle_();
break;
}
if (this->want_disconnect_) {
// Disconnect was requested after connecting started,
// but before the connection was established. Now that we have
// this->conn_id_ set, we can disconnect it.
// Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_().
this->unconditional_disconnect();
this->conn_id_ = UNSET_CONN_ID;
break;
}
// MTU negotiation already started in ESP_GATTC_CONNECT_EVT
@@ -363,8 +379,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
param->disconnect.reason);
}
// For active disconnects (esp_ble_gattc_close), CLOSE_EVT arrives before
// DISCONNECT_EVT. If CLOSE_EVT already transitioned us to IDLE, don't go
// backwards to DISCONNECTING — the connection is already fully cleaned up.
if (this->state() == espbt::ClientState::IDLE) {
this->log_event_("DISCONNECT_EVT after CLOSE_EVT, already IDLE");
break;
}
// For passive disconnects (remote device disconnected or link lost),
// DISCONNECT_EVT arrives first. Don't transition to IDLE yet — wait for
// CLOSE_EVT to ensure the controller has fully freed resources (L2CAP
// channels, ATT resources, HCI connection handle). Transitioning to IDLE
// here would allow reconnection before cleanup is complete, causing the
// controller to reject the new connection (status=133) or crash with
// ASSERT_PARAM in lld_evt.c.
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->set_disconnecting_();
break;
}
@@ -387,8 +417,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
return false;
this->log_gattc_lifecycle_event_("CLOSE");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
this->set_idle_();
break;
}
case ESP_GATTC_SEARCH_RES_EVT: {
@@ -113,11 +113,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{};
esp_bd_addr_t remote_bda_; // 6 bytes
// Group 5: 2-byte types
// Group 5: 4-byte types
uint32_t disconnecting_started_{0};
// Group 6: 2-byte types
uint16_t conn_id_{UNSET_CONN_ID};
uint16_t mtu_{23};
// Group 6: 1-byte types and small enums
// Group 7: 1-byte types and small enums
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
uint8_t connection_index_;
@@ -137,6 +140,16 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void log_gattc_warning_(const char *operation, esp_err_t err);
void log_connection_params_(const char *param_type);
void handle_connection_result_(esp_err_t ret);
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
void set_idle_() {
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
}
/// Transition to DISCONNECTING and start the safety timeout.
void set_disconnecting_() {
this->disconnecting_started_ = millis();
this->set_state(espbt::ClientState::DISCONNECTING);
}
// Compact error logging helpers to reduce flash usage
void log_error_(const char *message);
void log_error_(const char *message, int code);
+1 -1
View File
@@ -105,7 +105,7 @@ async def to_code(config):
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
+43 -3
View File
@@ -18,6 +18,7 @@
#include <cerrno>
#include <cstdio>
#include <sys/time.h>
namespace esphome {
@@ -238,6 +239,31 @@ void ESPHomeOTAComponent::handle_data_() {
/// and reboots on success.
///
/// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ.
///
/// Socket I/O strategy:
///
/// Before this function, the handshake states use non-blocking I/O:
/// read()/write() return immediately with EWOULDBLOCK if no data
/// loop() retries on next iteration (~16ms), no delay needed
///
/// This function switches to blocking mode with SO_RCVTIMEO/SO_SNDTIMEO:
///
/// Path | Wait mechanism | WDT strategy
/// --------------|------------------------|---------------------------
/// Main read | SO_RCVTIMEO (2s block) | feed_wdt() only, no delay
/// readall_() | SO_RCVTIMEO (2s block) | feed_wdt() + delay(0)
/// writeall_() | SO_SNDTIMEO (2s block) | feed_wdt() + delay(1)
///
/// readall_() uses delay(0) because SO_RCVTIMEO already waited — just yield.
/// writeall_() uses delay(1) because on raw TCP (ESP8266, RP2040) writes
/// never block (tcp_write returns immediately), so delay(1) prevents spinning.
///
/// Platform details:
/// BSD sockets (ESP32): setblocking(true) makes read/write block
/// lwip sockets (LT): setblocking(true) makes read/write block
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
/// socket_delay()/socket_wake() in read();
/// write() always returns immediately
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
bool update_started = false;
size_t total = 0;
@@ -249,6 +275,14 @@ void ESPHomeOTAComponent::handle_data_() {
size_t size_acknowledged = 0;
#endif
// Set socket timeouts and blocking mode (see strategy table above)
struct timeval tv;
tv.tv_sec = 2;
tv.tv_usec = 0;
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
this->client_->setblocking(true);
// Acknowledge auth OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
@@ -299,7 +333,8 @@ void ESPHomeOTAComponent::handle_data_() {
ssize_t read = this->client_->read(buf, requested);
if (read == -1) {
if (this->would_block_(errno)) {
this->yield_and_feed_watchdog_();
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", errno);
@@ -401,7 +436,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
} else {
at += read;
}
this->yield_and_feed_watchdog_();
// read() already waited via SO_RCVTIMEO, just yield without 1ms stall
App.feed_wdt();
delay(0);
}
return true;
@@ -422,10 +459,13 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno);
return false;
}
// EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning
this->yield_and_feed_watchdog_();
} else {
at += written;
// write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT
App.feed_wdt();
}
this->yield_and_feed_watchdog_();
}
return true;
}
@@ -21,22 +21,6 @@
namespace esphome::ethernet {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637
#ifdef USE_ESP32_VARIANT_ESP32P4
#undef ETH_ESP32_EMAC_DEFAULT_CONFIG
#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \
{ \
.smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \
.clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \
.dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \
.emac_dataif_gpio = \
{.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \
.clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \
}
#endif
#endif
static const char *const TAG = "ethernet";
// PHY register size for hex logging
@@ -162,7 +146,7 @@ void EthernetComponent::setup() {
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
@@ -11,10 +11,15 @@
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_mac_esp.h"
#include "esp_netif.h"
#include "esp_mac.h"
#include "esp_idf_version.h"
#if CONFIG_ETH_USE_ESP32_EMAC
extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif
namespace esphome::ethernet {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
@@ -0,0 +1,10 @@
#include "esp_eth_mac_esp.h"
// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers
// which are valid in C but not in C++. This wrapper allows C++ code to get
// the default config without replicating the macro's contents.
#if CONFIG_ETH_USE_ESP32_EMAC
eth_esp32_emac_config_t eth_esp32_emac_default_config(void) {
return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG();
}
#endif
+37
View File
@@ -93,11 +93,31 @@ def _bus_declare_type(value):
raise NotImplementedError
def _rp2040_i2c_controller(pin):
"""Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin.
See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"):
https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
See RP2350 datasheet Table 7 (section 9.4, "Function Select"):
https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
"""
return (pin // 2) % 2
def validate_config(config):
if CORE.is_esp32:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
)(config)
if CORE.is_rp2040:
sda_controller = _rp2040_i2c_controller(config[CONF_SDA])
scl_controller = _rp2040_i2c_controller(config[CONF_SCL])
if sda_controller != scl_controller:
raise cv.Invalid(
f"SDA pin GPIO{config[CONF_SDA]} is on I2C{sda_controller} but "
f"SCL pin GPIO{config[CONF_SCL]} is on I2C{scl_controller}. "
f"Both pins must be on the same I2C controller."
)
return config
@@ -146,6 +166,23 @@ def _final_validate(config):
full_config = fv.full_config.get()[CONF_I2C]
if CORE.using_zephyr and len(full_config) > 1:
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
if CORE.is_rp2040:
if len(full_config) > 2:
raise cv.Invalid(
"The maximum number of I2C interfaces for RP2040/RP2350 is 2"
)
if len(full_config) > 1:
controllers = [
_rp2040_i2c_controller(conf[CONF_SDA]) for conf in full_config
]
if len(set(controllers)) != len(controllers):
raise cv.Invalid(
"Multiple I2C buses are configured to use the same I2C controller. "
"Each bus must use pins on a different controller. "
"The I2C controller is determined by (gpio / 2) % 2: "
"even pin pairs (0-1, 4-5, 8-9, ...) use I2C0, "
"odd pin pairs (2-3, 6-7, 10-11, ...) use I2C1."
)
if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES:
variant = get_esp32_variant()
max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"]
+6 -4
View File
@@ -20,12 +20,14 @@ void ArduinoI2CBus::setup() {
#if defined(USE_ESP8266)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
#elif defined(USE_RP2040)
static bool first = true;
if (first) {
// Select Wire instance based on pin assignment, not definition order.
// I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1
// RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
// RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf
if ((this->sda_pin_ / 2) % 2 == 0) {
wire_ = &Wire;
first = false;
} else {
wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory)
wire_ = &Wire1;
}
#endif
+49 -4
View File
@@ -5,6 +5,10 @@
#include <driver/ledc.h>
#include <cinttypes>
#include <esp_private/periph_ctrl.h>
#if !defined(SOC_LEDC_SUPPORT_FADE_STOP)
#include <hal/ledc_ll.h>
#endif
#define CLOCK_FREQUENCY 80e6f
@@ -16,10 +20,10 @@
static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5;
namespace esphome {
namespace ledc {
namespace esphome::ledc {
static const char *const TAG = "ledc.output";
static bool ledc_peripheral_reset_done = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1;
#if SOC_LEDC_SUPPORT_HS_MODE
@@ -32,6 +36,28 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H
inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; }
#endif
#if !defined(SOC_LEDC_SUPPORT_FADE_STOP)
// Classic ESP32 (currently the only target without SOC_LEDC_SUPPORT_FADE_STOP) can block in
// ledc_ll_set_duty_start() while duty_start is set. We check the same conf1.duty_start bit here
// to defer updates and avoid entering IDF's unbounded wait loop.
//
// This intentionally depends on the classic ESP32 LEDC register layout used by IDF's own LL HAL.
// If another target without SOC_LEDC_SUPPORT_FADE_STOP is introduced, revisit this helper.
static_assert(
#if defined(CONFIG_IDF_TARGET_ESP32)
true,
#else
false,
#endif
"LEDC duty_start pending check assumes classic ESP32 register layout; "
"re-evaluate for this target");
static bool ledc_duty_update_pending(ledc_mode_t speed_mode, ledc_channel_t chan_num) {
auto *hw = LEDC_LL_GET_HW();
return hw->channel_group[speed_mode].channel[chan_num].conf1.duty_start != 0;
}
#endif
float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) {
return static_cast<float>(CLOCK_FREQUENCY) / static_cast<float>(1 << bit_depth);
}
@@ -105,21 +131,40 @@ void LEDCOutput::write_state(float state) {
const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint32_t>(duty_rounded);
if (duty == this->last_duty_) {
return;
}
ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_);
auto speed_mode = get_speed_mode(this->channel_);
auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8);
int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_);
if (duty == max_duty) {
ledc_stop(speed_mode, chan_num, 1);
this->last_duty_ = duty;
} else if (duty == 0) {
ledc_stop(speed_mode, chan_num, 0);
this->last_duty_ = duty;
} else {
#if !defined(SOC_LEDC_SUPPORT_FADE_STOP)
if (ledc_duty_update_pending(speed_mode, chan_num)) {
ESP_LOGV(TAG, "Skipping LEDC duty update on channel %u while previous duty_start is still set", this->channel_);
return;
}
#endif
ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint);
ledc_update_duty(speed_mode, chan_num);
this->last_duty_ = duty;
}
}
void LEDCOutput::setup() {
if (!ledc_peripheral_reset_done) {
ESP_LOGV(TAG, "Resetting LEDC peripheral to clear stale state after reboot");
periph_module_reset(PERIPH_LEDC_MODULE);
ledc_peripheral_reset_done = true;
}
auto speed_mode = get_speed_mode(this->channel_);
auto timer_num = static_cast<ledc_timer_t>((this->channel_ % 8) / 2);
auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8);
@@ -207,12 +252,12 @@ void LEDCOutput::update_frequency(float frequency) {
this->status_clear_error();
// re-apply duty
this->last_duty_ = UINT32_MAX;
this->write_state(this->duty_);
}
uint8_t next_ledc_channel = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace ledc
} // namespace esphome
} // namespace esphome::ledc
#endif
+4 -4
View File
@@ -4,11 +4,11 @@
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
#include <cstdint>
#ifdef USE_ESP32
namespace esphome {
namespace ledc {
namespace esphome::ledc {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern uint8_t next_ledc_channel;
@@ -39,6 +39,7 @@ class LEDCOutput : public output::FloatOutput, public Component {
float phase_angle_{0.0f};
float frequency_{};
float duty_{0.0f};
uint32_t last_duty_{UINT32_MAX};
bool initialized_ = false;
};
@@ -56,7 +57,6 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> {
LEDCOutput *parent_;
};
} // namespace ledc
} // namespace esphome
} // namespace esphome::ledc
#endif
+9 -2
View File
@@ -214,7 +214,14 @@ LightColorValues LightCall::validate_() {
if (this->has_brightness() && this->brightness_ == 0.0f) {
this->state_ = false;
this->set_flag_(FLAG_HAS_STATE);
this->brightness_ = 1.0f;
if (color_mode & ColorCapability::BRIGHTNESS) {
// Reset brightness so the light has nonzero brightness when turned back on.
this->brightness_ = 1.0f;
} else {
// Light doesn't support brightness; clear the flag to avoid a spurious
// "brightness not supported" warning during capability validation.
this->clear_flag_(FLAG_HAS_BRIGHTNESS);
}
}
// Set color brightness to 100% if currently zero and a color is set.
@@ -506,7 +513,7 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
LightCall &LightCall::set_effect(const char *effect, size_t len) {
if (len == 4 && strncasecmp(effect, "none", 4) == 0) {
this->set_effect(0);
this->set_effect(uint32_t{0});
return *this;
}
+2
View File
@@ -130,6 +130,8 @@ class LightCall {
LightCall &set_effect(optional<std::string> effect);
/// Set the effect of the light by its name.
LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); }
/// Set the effect of the light by its name (const char * overload to resolve ambiguity).
LightCall &set_effect(const char *effect) { return this->set_effect(effect, strlen(effect)); }
/// Set the effect of the light by its name and length (zero-copy from API).
LightCall &set_effect(const char *effect, size_t len);
/// Set the effect of the light by its internal index number (only for internal use).
@@ -1,6 +1,7 @@
#ifdef USE_ESP32
#include "logger.h"
#include "esphome/components/esp32/crash_handler.h"
#include <esp_log.h>
#include <driver/uart.h>
@@ -117,6 +118,9 @@ void Logger::pre_setup() {
esp_log_set_vprintf(esp_idf_log_vprintf_);
ESP_LOGI(TAG, "Log initialized");
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
#endif
}
void HOT Logger::write_msg_(const char *msg, uint16_t len) {
@@ -1,6 +1,9 @@
#ifdef USE_RP2040
#include "logger.h"
#include "esphome/core/defines.h"
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#include "esphome/core/log.h"
namespace esphome::logger {
@@ -26,7 +29,9 @@ void Logger::pre_setup() {
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
}
void HOT Logger::write_msg_(const char *msg, uint16_t len) {
+4
View File
@@ -129,6 +129,10 @@ class MDNSComponent final : public Component {
#endif
#ifdef USE_MDNS_STORE_SERVICES
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
#endif
#ifdef USE_RP2040
bool was_connected_{false};
bool initialized_{false};
#endif
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf);
};
+31 -6
View File
@@ -7,7 +7,12 @@
#include "esphome/core/log.h"
#include "mdns_component.h"
// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty.
// Save and restore our definition around the include to avoid a redefinition warning.
#pragma push_macro("IRAM_ATTR")
#undef IRAM_ATTR
#include <ESP8266mDNS.h>
#pragma pop_macro("IRAM_ATTR")
namespace esphome::mdns {
@@ -36,12 +41,32 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
}
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_rp2040);
// Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
// RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart
// mDNS when the network interface reconnects. However, stateUpCB() is stubbed out
// in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used
// schedule_function() which doesn't exist in arduino-pico, and the callback can't
// safely run directly since netif status callbacks fire from IRQ context
// (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets.
//
// Workaround: defer MDNS.begin() and service registration until the network is
// connected (has an IP), then call notifyAPChange() on subsequent reconnects to
// restart mDNS probing and announcing — all from main loop context so it's
// thread-safe.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() {
bool connected = network::is_connected();
if (connected && !this->was_connected_) {
if (!this->initialized_) {
this->setup_buffers_and_register_(register_rp2040);
this->initialized_ = true;
} else {
MDNS.notifyAPChange();
}
}
this->was_connected_ = connected;
if (this->initialized_) {
MDNS.update();
}
});
}
void MDNSComponent::on_shutdown() {
+6 -6
View File
@@ -125,13 +125,17 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
// Byte 0: modbus address (match all)
if (at == 0)
return true;
uint8_t address = raw[0];
uint8_t function_code = raw[1];
// Byte 1: function code
if (at == 1)
return true;
// Byte 2: Size (with modbus rtu function code 4/3)
// See also https://en.wikipedia.org/wiki/Modbus
if (at == 2)
return true;
uint8_t address = raw[0];
uint8_t function_code = raw[1];
uint8_t data_len = raw[2];
uint8_t data_offset = 3;
@@ -146,10 +150,6 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
// chance that this is a complete message ... admittedly there is a small chance is
// isn't but that is quite small given the purpose of the CRC in the first place
// Fewer than 2 bytes can't calc CRC
if (at < 2)
return true;
data_len = at - 2;
data_offset = 1;
+5
View File
@@ -13,6 +13,11 @@ const std::string &OneWireDevice::get_address_name() {
return this->address_name_;
}
void OneWireDevice::set_address(uint64_t address) {
this->address_ = address;
this->address_name_.clear();
}
bool OneWireDevice::send_command_(uint8_t cmd) {
if (!this->bus_->select(this->address_))
return false;
+1 -1
View File
@@ -15,7 +15,7 @@ class OneWireDevice {
public:
/// @brief store the address of the device
/// @param address of the device
void set_address(uint64_t address) { this->address_ = address; }
void set_address(uint64_t address);
void set_index(uint8_t index) { this->index_ = index; }
@@ -105,6 +105,7 @@ OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) {
this->current_address_ = this->start_address_;
this->image_size_ = image_size;
this->bytes_received_ = 0;
this->buffer_len_ = 0;
this->md5_set_ = false;
@@ -140,6 +141,7 @@ OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) {
size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_);
memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer);
this->buffer_len_ += to_buffer;
this->bytes_received_ += to_buffer;
written += to_buffer;
// If buffer is full, write to flash
@@ -252,8 +254,8 @@ OTAResponseTypes ESP8266OTABackend::end() {
}
}
// Calculate actual bytes written
size_t actual_size = this->current_address_ - this->start_address_;
// Calculate actual bytes written (exact uploaded size, excluding flash write padding)
size_t actual_size = this->bytes_received_;
// Check if any data was written
if (actual_size == 0) {
@@ -304,6 +306,7 @@ void ESP8266OTABackend::abort() {
this->buffer_.reset();
this->buffer_len_ = 0;
this->image_size_ = 0;
this->bytes_received_ = 0;
esp8266::preferences_prevent_write(false);
}
@@ -48,6 +48,7 @@ class ESP8266OTABackend final {
uint32_t start_address_{0};
uint32_t current_address_{0};
size_t image_size_{0};
size_t bytes_received_{0};
md5::MD5Digest md5_{};
uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest
+15 -9
View File
@@ -57,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_KD_MULTIPLIER, default=0.0): cv.float_,
cv.Optional(
CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES, default=1
): cv.int_,
): cv.positive_not_null_int,
}
),
cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema(
@@ -68,8 +68,12 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_STARTING_INTEGRAL_TERM, default=0.0): cv.float_,
cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_,
cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_,
cv.Optional(CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1): cv.int_,
cv.Optional(CONF_OUTPUT_AVERAGING_SAMPLES, default=1): cv.int_,
cv.Optional(
CONF_DERIVATIVE_AVERAGING_SAMPLES, default=1
): cv.positive_not_null_int,
cv.Optional(
CONF_OUTPUT_AVERAGING_SAMPLES, default=1
): cv.positive_not_null_int,
}
),
}
@@ -102,13 +106,15 @@ async def to_code(config):
cg.add(var.set_starting_integral_term(params[CONF_STARTING_INTEGRAL_TERM]))
cg.add(var.set_derivative_samples(params[CONF_DERIVATIVE_AVERAGING_SAMPLES]))
cg.add(var.set_output_samples(params[CONF_OUTPUT_AVERAGING_SAMPLES]))
output_samples = params[CONF_OUTPUT_AVERAGING_SAMPLES]
cg.add(var.set_output_samples(output_samples))
if CONF_MIN_INTEGRAL in params:
cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL]))
if CONF_MAX_INTEGRAL in params:
cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL]))
deadband_output_samples = 1
if CONF_DEADBAND_PARAMETERS in config:
params = config[CONF_DEADBAND_PARAMETERS]
cg.add(var.set_threshold_low(params[CONF_THRESHOLD_LOW]))
@@ -116,11 +122,11 @@ async def to_code(config):
cg.add(var.set_kp_multiplier(params[CONF_KP_MULTIPLIER]))
cg.add(var.set_ki_multiplier(params[CONF_KI_MULTIPLIER]))
cg.add(var.set_kd_multiplier(params[CONF_KD_MULTIPLIER]))
cg.add(
var.set_deadband_output_samples(
params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
)
)
deadband_output_samples = params[CONF_DEADBAND_OUTPUT_AVERAGING_SAMPLES]
cg.add(var.set_deadband_output_samples(deadband_output_samples))
# Single shared output buffer sized to max of both modes
cg.add(var.init_output_buffer(max(output_samples, deadband_output_samples)))
cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE]))
+9 -1
View File
@@ -28,7 +28,11 @@ class PIDClimate : public climate::Climate, public Component {
void set_min_integral(float min_integral) { controller_.min_integral_ = min_integral; }
void set_max_integral(float max_integral) { controller_.max_integral_ = max_integral; }
void set_output_samples(int in) { controller_.output_samples_ = in; }
void set_derivative_samples(int in) { controller_.derivative_samples_ = in; }
void set_derivative_samples(int in) {
controller_.derivative_samples_ = in;
if (in > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits)
controller_.derivative_window_.init(in);
}
void set_threshold_low(float in) { controller_.threshold_low_ = in; }
void set_threshold_high(float in) { controller_.threshold_high_ = in; }
@@ -38,6 +42,10 @@ class PIDClimate : public climate::Climate, public Component {
void set_starting_integral_term(float in) { controller_.set_starting_integral_term(in); }
void set_deadband_output_samples(int in) { controller_.deadband_output_samples_ = in; }
void init_output_buffer(int size) {
if (size > 1) // No allocation needed when samples=1 (ring_buffer_average_ short-circuits)
controller_.output_window_.init(size);
}
float get_output_value() const { return output_value_; }
float get_error_value() const { return controller_.error_; }
+15 -17
View File
@@ -21,9 +21,9 @@ float PIDController::update(float setpoint, float process_value) {
// u(t) := p(t) + i(t) + d(t)
float output = proportional_term_ + integral_term_ + derivative_term_;
// smooth/sample the output
// smooth/sample the output using shared buffer with mode-appropriate sample count
int samples = in_deadband() ? deadband_output_samples_ : output_samples_;
return weighted_average_(output_list_, output, samples);
return ring_buffer_average_(output_window_, output, samples);
}
bool PIDController::in_deadband() {
@@ -83,7 +83,7 @@ void PIDController::calculate_derivative_term_(float setpoint) {
previous_setpoint_ = setpoint;
// smooth the derivative samples
derivative = weighted_average_(derivative_list_, derivative, derivative_samples_);
derivative = ring_buffer_average_(derivative_window_, derivative, derivative_samples_);
derivative_term_ = kd_ * derivative;
@@ -93,25 +93,23 @@ void PIDController::calculate_derivative_term_(float setpoint) {
}
}
float PIDController::weighted_average_(std::deque<float> &list, float new_value, int samples) {
// if only 1 sample needed, clear the list and return
if (samples == 1) {
list.clear();
float PIDController::ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples) {
// if only 1 sample needed (or invalid), clear the buffer and return
if (max_samples <= 1) {
buf.clear();
return new_value;
}
// add the new item to the list
list.push_front(new_value);
// Trim oldest entries to make room (handles mode-switching where buffer
// may have more entries than the current mode needs)
while (buf.size() >= static_cast<size_t>(max_samples))
buf.pop();
buf.push(new_value);
// keep only 'samples' readings, by popping off the back of the list
while (samples > 0 && list.size() > static_cast<size_t>(samples))
list.pop_back();
// calculate and return the average of all values in the list
float sum = 0;
for (auto &elem : list)
sum += elem;
return sum / list.size();
for (auto val : buf)
sum += val;
return sum / buf.size();
}
float PIDController::calculate_relative_time_() {
+14 -10
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/hal.h"
#include <deque>
#include "esphome/core/helpers.h"
#include <cmath>
namespace esphome {
@@ -24,10 +25,10 @@ struct PIDController {
/// Differential gain K_d.
float kd_ = 0;
// smooth the derivative value using a weighted average over X samples
int derivative_samples_ = 8;
// smooth the derivative value using an average over X samples
int derivative_samples_ = 1;
/// smooth the output value using a weighted average over X values
/// smooth the output value using an average over X values
int output_samples_ = 1;
float threshold_low_ = 0.0f;
@@ -50,7 +51,10 @@ struct PIDController {
void calculate_proportional_term_();
void calculate_integral_term_();
void calculate_derivative_term_(float setpoint);
float weighted_average_(std::deque<float> &list, float new_value, int samples);
/// Ring buffer smoothing using FixedRingBuffer (single allocation at setup)
float ring_buffer_average_(FixedRingBuffer<float> &buf, float new_value, int max_samples);
float calculate_relative_time_();
/// Error from previous update used for derivative term
@@ -60,12 +64,12 @@ struct PIDController {
float accumulated_integral_ = 0;
uint32_t last_time_ = 0;
// this is a list of derivative values for smoothing.
std::deque<float> derivative_list_;
// Ring buffer for derivative smoothing
FixedRingBuffer<float> derivative_window_;
// this is a list of output values for smoothing.
std::deque<float> output_list_;
// Ring buffer for output smoothing (shared between normal and deadband modes)
FixedRingBuffer<float> output_window_;
}; // Struct PID Controller
}; // Struct PIDController
} // namespace pid
} // namespace esphome
+1 -1
View File
@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.const import CONF_CLIMATE_ID
import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ICON_GAUGE, STATE_CLASS_MEASUREMENT, UNIT_PERCENT
@@ -21,7 +22,6 @@ PID_CLIMATE_SENSOR_TYPES = {
"KD": PIDClimateSensorType.PID_SENSOR_TYPE_KD,
}
CONF_CLIMATE_ID = "climate_id"
CONFIG_SCHEMA = (
sensor.sensor_schema(
PIDClimateSensor,
+7 -1
View File
@@ -203,7 +203,12 @@ async def to_code(config):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
cg.add_platformio_option("board_build.core", "earlephilhower")
cg.add_platformio_option("board_build.filesystem_size", "1m")
# In testing mode, use all flash for sketch to allow linking grouped component tests.
# Real RP2040 hardware uses 1MB filesystem + 1MB sketch, but CI tests may combine
# many components that exceed the 1MB sketch partition.
cg.add_platformio_option(
"board_build.filesystem_size", "0m" if CORE.testing_mode else "1m"
)
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
cg.add_define(
@@ -212,6 +217,7 @@ async def to_code(config):
)
cg.add_define("USE_RP2040_WATCHDOG_TIMEOUT", config[CONF_WATCHDOG_TIMEOUT])
cg.add_define("USE_RP2040_CRASH_HANDLER")
def add_pio_file(component: str, key: str, data: str):
+5 -1
View File
@@ -1,8 +1,10 @@
#ifdef USE_RP2040
#include "core.h"
#include "crash_handler.h"
#include "esphome/core/defines.h"
#ifdef USE_RP2040_CRASH_HANDLER
#include "crash_handler.h"
#endif
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -25,7 +27,9 @@ void arch_restart() {
}
void arch_init() {
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_read_and_clear();
#endif
#if USE_RP2040_WATCHDOG_TIMEOUT > 0
watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false);
#endif
+20 -7
View File
@@ -1,5 +1,8 @@
#ifdef USE_RP2040
#include "esphome/core/defines.h"
#ifdef USE_RP2040_CRASH_HANDLER
#include "crash_handler.h"
#include "esphome/core/log.h"
@@ -13,13 +16,19 @@
static constexpr uint32_t EF_LR = 5;
static constexpr uint32_t EF_PC = 6;
static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
// Version encoded in the magic value: upper 16 bits are sentinel (0xDEAD),
// lower 16 bits are the version number. This avoids using a separate scratch
// register for versioning (we only have 8 total). Future firmware reads the
// sentinel to confirm it's crash data, then the version to know the layout.
static constexpr uint32_t CRASH_MAGIC_SENTINEL = 0xDEAD0000;
static constexpr uint32_t CRASH_DATA_VERSION = 1;
static constexpr uint32_t CRASH_MAGIC_V1 = CRASH_MAGIC_SENTINEL | CRASH_DATA_VERSION;
// We only have 8 scratch registers (32 bytes) that survive watchdog reboot.
// Use them for the most important data, then scan the stack for code addresses.
//
// Scratch register layout:
// [0] = magic (CRASH_MAGIC)
// [0] = versioned magic (upper 16 bits = 0xDEAD sentinel, lower 16 bits = version)
// [1] = PC (program counter at fault)
// [2] = LR (link register from exception frame)
// [3] = SP (stack pointer at fault)
@@ -48,18 +57,21 @@ static const char *const TAG = "rp2040.crash";
// Placed in .noinit so BSS zero-init cannot race with crash_handler_read_and_clear().
// The valid field is explicitly cleared in crash_handler_read_and_clear() instead.
static struct {
static struct CrashData {
bool valid;
uint32_t pc;
uint32_t lr;
uint32_t sp;
uint32_t backtrace[MAX_BACKTRACE];
uint8_t backtrace_count;
} __attribute__((section(".noinit"))) s_crash_data;
} s_crash_data __attribute__((section(".noinit")));
bool crash_handler_has_data() { return s_crash_data.valid; }
void crash_handler_read_and_clear() {
s_crash_data.valid = false;
if (watchdog_hw->scratch[0] == CRASH_MAGIC) {
uint32_t magic = watchdog_hw->scratch[0];
if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) {
s_crash_data.valid = true;
s_crash_data.pc = watchdog_hw->scratch[1];
s_crash_data.lr = watchdog_hw->scratch[2];
@@ -135,7 +147,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame
// by a stacking error or corrupted SP, frame may be invalid. Write a minimal
// crash marker so we at least know a crash occurred.
if (!is_valid_sram_ptr(frame)) {
watchdog_hw->scratch[0] = CRASH_MAGIC;
watchdog_hw->scratch[0] = CRASH_MAGIC_V1;
watchdog_hw->scratch[1] = 0; // PC unknown
watchdog_hw->scratch[2] = 0; // LR unknown
watchdog_hw->scratch[3] = reinterpret_cast<uintptr_t>(frame); // Record the bad SP for diagnosis
@@ -157,7 +169,7 @@ static void __attribute__((used, noreturn)) hard_fault_handler_c(uint32_t *frame
uint32_t pre_fault_sp = reinterpret_cast<uintptr_t>(post_frame);
// Write key registers
watchdog_hw->scratch[0] = CRASH_MAGIC;
watchdog_hw->scratch[0] = CRASH_MAGIC_V1;
watchdog_hw->scratch[1] = frame[EF_PC];
watchdog_hw->scratch[2] = frame[EF_LR];
watchdog_hw->scratch[3] = pre_fault_sp;
@@ -224,4 +236,5 @@ extern "C" void __attribute__((naked, used)) isr_hardfault() {
: "i"(hard_fault_handler_c));
}
#endif // USE_RP2040_CRASH_HANDLER
#endif // USE_RP2040
+7 -1
View File
@@ -2,7 +2,9 @@
#ifdef USE_RP2040
#include <cstdint>
#include "esphome/core/defines.h"
#ifdef USE_RP2040_CRASH_HANDLER
namespace esphome::rp2040 {
@@ -12,6 +14,10 @@ void crash_handler_read_and_clear();
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
/// Returns true if crash data was found this boot.
bool crash_handler_has_data();
} // namespace esphome::rp2040
#endif // USE_RP2040_CRASH_HANDLER
#endif // USE_RP2040
+1 -1
View File
@@ -74,7 +74,7 @@ class JPEGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
class PNGFormat(Format):
@@ -26,6 +26,10 @@ class BmpDecoder : public ImageDecoder {
int HOT decode(uint8_t *buffer, size_t size) override;
bool is_finished() const override {
if (this->bits_per_pixel_ == 0) {
// header not yet received, so dimensions not yet determined
return false;
}
// BMP is finished when we've decoded all pixel data
return this->paint_index_ >= static_cast<size_t>(this->width_ * this->height_);
}
+4 -4
View File
@@ -41,7 +41,7 @@ SelectCall &SelectCall::with_index(size_t index) {
this->operation_ = SELECT_OP_SET;
if (index >= this->parent_->size()) {
ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index);
this->index_ = {}; // Store nullopt for invalid index
this->index_ = nullopt; // Store nullopt for invalid index
} else {
this->index_ = index;
}
@@ -52,7 +52,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) {
const auto &options = this->parent_->traits.get_options();
if (options.empty()) {
ESP_LOGW(TAG, "'%s' - Select has no options", name);
return {};
return nullopt;
}
if (this->operation_ == SELECT_OP_FIRST) {
@@ -67,7 +67,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) {
ESP_LOGD(TAG, "'%s' - Setting", name);
if (!this->index_.has_value()) {
ESP_LOGW(TAG, "'%s' - No option set", name);
return {};
return nullopt;
}
return this->index_;
}
@@ -96,7 +96,7 @@ optional<size_t> SelectCall::calculate_target_index_(const char *name) {
return active_index + 1;
}
return {}; // Can't navigate further without cycling
return nullopt; // Can't navigate further without cycling
}
void SelectCall::perform() {
+17 -17
View File
@@ -403,9 +403,9 @@ async def filter_out_filter_to_code(config, filter_id):
QUANTILE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
cv.Optional(CONF_QUANTILE, default=0.9): cv.zero_to_one_float,
}
),
@@ -427,9 +427,9 @@ async def quantile_filter_to_code(config, filter_id):
MEDIAN_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
}
),
validate_send_first_at,
@@ -449,9 +449,9 @@ async def median_filter_to_code(config, filter_id):
MIN_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
}
),
validate_send_first_at,
@@ -483,9 +483,9 @@ async def min_filter_to_code(config, filter_id):
MAX_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
cv.Optional(CONF_WINDOW_SIZE, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=5): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
}
),
validate_send_first_at,
@@ -509,9 +509,9 @@ async def max_filter_to_code(config, filter_id):
SLIDING_AVERAGE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
cv.Optional(CONF_WINDOW_SIZE, default=15): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
}
),
validate_send_first_at,
@@ -540,8 +540,8 @@ EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float,
cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int,
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int,
cv.Optional(CONF_SEND_EVERY, default=15): cv.int_range(min=1, max=65535),
cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.int_range(min=1, max=65535),
}
),
validate_send_first_at,
+10 -24
View File
@@ -41,26 +41,14 @@ void Filter::initialize(Sensor *parent, Filter *next) {
}
// SlidingWindowFilter
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
: window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
// Allocate ring buffer once at initialization
SlidingWindowFilter::SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at)
: send_every_(send_every), send_at_(send_every - send_first_at) {
this->window_.init(window_size);
}
optional<float> SlidingWindowFilter::new_value(float value) {
// Add value to ring buffer
if (this->window_count_ < this->window_size_) {
// Buffer not yet full - just append
this->window_.push_back(value);
this->window_count_++;
} else {
// Buffer full - overwrite oldest value (ring buffer)
this->window_[this->window_head_] = value;
this->window_head_++;
if (this->window_head_ >= this->window_size_) {
this->window_head_ = 0;
}
}
// Add value to ring buffer (overwrites oldest when full)
this->window_.push_overwrite(value);
// Check if we should send a result
if (++this->send_at_ >= this->send_every_) {
@@ -77,9 +65,8 @@ FixedVector<float> SortedWindowFilter::get_window_values_() {
// Copy window without NaN values using FixedVector (no heap allocation)
// Returns unsorted values - caller will use std::nth_element for partial sorting as needed
FixedVector<float> values;
values.init(this->window_count_);
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
values.init(this->window_.size());
for (float v : this->window_) {
if (!std::isnan(v)) {
values.push_back(v);
}
@@ -150,8 +137,7 @@ float MaxFilter::compute_result() { return this->find_extremum_<std::greater<flo
float SlidingWindowMovingAverageFilter::compute_result() {
float sum = 0;
size_t valid_count = 0;
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
for (float v : this->window_) {
if (!std::isnan(v)) {
sum += v;
valid_count++;
@@ -161,7 +147,7 @@ float SlidingWindowMovingAverageFilter::compute_result() {
}
// ExponentialMovingAverageFilter
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at)
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at)
: alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {}
optional<float> ExponentialMovingAverageFilter::new_value(float value) {
if (!std::isnan(value)) {
@@ -183,7 +169,7 @@ optional<float> ExponentialMovingAverageFilter::new_value(float value) {
}
return {};
}
void ExponentialMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
void ExponentialMovingAverageFilter::set_send_every(uint16_t send_every) { this->send_every_ = send_every; }
void ExponentialMovingAverageFilter::set_alpha(float alpha) { this->alpha_ = alpha; }
// ThrottleAverageFilter
@@ -511,7 +497,7 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
}
// StreamingFilter (base class)
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
: window_size_(window_size), send_first_at_(send_first_at) {}
optional<float> StreamingFilter::new_value(float value) {
+14 -19
View File
@@ -52,7 +52,7 @@ class Filter {
*/
class SlidingWindowFilter : public Filter {
public:
SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
SlidingWindowFilter(uint16_t window_size, uint16_t send_every, uint16_t send_first_at);
optional<float> new_value(float value) final;
@@ -60,14 +60,10 @@ class SlidingWindowFilter : public Filter {
/// Called by new_value() to compute the filtered result from the current window
virtual float compute_result() = 0;
/// Access the sliding window values (ring buffer implementation)
/// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
FixedVector<float> window_;
size_t window_head_{0}; ///< Index where next value will be written
size_t window_count_{0}; ///< Number of valid values in window (0 to window_size_)
size_t window_size_; ///< Maximum window size
size_t send_every_; ///< Send result every N values
size_t send_at_; ///< Counter for send_every
/// Sliding window ring buffer - automatically overwrites oldest values when full
FixedRingBuffer<float> window_;
uint16_t send_every_; ///< Send result every N values
uint16_t send_at_; ///< Counter for send_every
};
/** Base class for Min/Max filters.
@@ -84,8 +80,7 @@ class MinMaxFilter : public SlidingWindowFilter {
template<typename Compare> float find_extremum_() {
float result = NAN;
Compare comp;
for (size_t i = 0; i < this->window_count_; i++) {
float v = this->window_[i];
for (float v : this->window_) {
if (!std::isnan(v)) {
result = std::isnan(result) ? v : (comp(v, result) ? v : result);
}
@@ -239,18 +234,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
*/
class ExponentialMovingAverageFilter : public Filter {
public:
ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at);
ExponentialMovingAverageFilter(float alpha, uint16_t send_every, uint16_t send_first_at);
optional<float> new_value(float value) override;
void set_send_every(size_t send_every);
void set_send_every(uint16_t send_every);
void set_alpha(float alpha);
protected:
float accumulator_{NAN};
float alpha_;
size_t send_every_;
size_t send_at_;
uint16_t send_every_;
uint16_t send_at_;
bool first_value_{true};
};
@@ -570,7 +565,7 @@ class ToNTCTemperatureFilter : public Filter {
*/
class StreamingFilter : public Filter {
public:
StreamingFilter(size_t window_size, size_t send_first_at);
StreamingFilter(uint16_t window_size, uint16_t send_first_at);
optional<float> new_value(float value) final;
@@ -584,9 +579,9 @@ class StreamingFilter : public Filter {
/// Called by new_value() to reset internal state after sending a result
virtual void reset_batch() = 0;
size_t window_size_;
size_t count_{0};
size_t send_first_at_;
uint16_t window_size_;
uint16_t count_{0};
uint16_t send_first_at_;
bool first_send_{true};
};
+10 -1
View File
@@ -14,7 +14,7 @@
#endif
#ifdef USE_LWIP_FAST_SELECT
struct lwip_sock;
#include "esphome/core/lwip_fast_select.h"
#endif
namespace esphome::socket {
@@ -56,6 +56,15 @@ class BSDSocketImpl {
return ::getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING)
// Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock,
// bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting).
if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) {
LwIPLock lock;
if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0))
return 0;
}
#endif
return ::setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) { return ::listen(this->fd_, backlog); }
+2
View File
@@ -51,6 +51,8 @@
#define SO_REUSEADDR 0x0004 /* Allow local address reuse */
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
#define SO_BROADCAST 0x0020 /* permit to send and to receive broadcast messages (see IP_SOF_BROADCAST option) */
#define SO_RCVTIMEO 0x1006 /* receive timeout */
#define SO_SNDTIMEO 0x1005 /* send timeout */
#define SOL_SOCKET 0xfff /* options for socket level */
+108 -13
View File
@@ -5,6 +5,7 @@
#include <cerrno>
#include <cstring>
#include <sys/time.h>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -81,7 +82,9 @@ void socket_delay(uint32_t ms) {
s_socket_woke = false;
return;
}
s_socket_woke = false;
// Don't clear s_socket_woke here — if an IRQ fires between the check above
// and the while loop below, the while condition sees it immediately. Clearing
// here would lose that wake and sleep until the timer fires.
s_delay_expired = false;
// Set a one-shot timer to wake us after the timeout.
// add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error.
@@ -99,6 +102,7 @@ void socket_delay(uint32_t ms) {
// Cancel timer if we woke early (socket data arrived before timeout)
if (!s_delay_expired)
cancel_alarm(alarm);
s_socket_woke = false; // consume the wake for next call
}
// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP
@@ -138,13 +142,46 @@ static const char *const TAG = "socket.lwip";
#define LWIP_LOG(msg, ...)
#endif
// Clear arg, recv, and err callbacks, then abort a connected PCB.
// Only valid for full tcp_pcb (not tcp_pcb_listen).
// Must be called before destroying the object that tcp_arg points to —
// tcp_abort() triggers the err callback synchronously, which would
// otherwise call back into a partially-destroyed object.
// tcp_sent/tcp_poll are not cleared because this implementation
// never registers them.
static void pcb_detach_abort(struct tcp_pcb *pcb) {
tcp_arg(pcb, nullptr);
tcp_recv(pcb, nullptr);
tcp_err(pcb, nullptr);
tcp_abort(pcb);
}
// Clear arg, recv, and err callbacks, then gracefully close a connected PCB.
// Only valid for full tcp_pcb (not tcp_pcb_listen).
// After tcp_close(), the PCB remains alive during the TCP close handshake
// (FIN_WAIT, TIME_WAIT states). Without clearing callbacks first, LWIP
// would call recv/err on a destroyed socket object, corrupting the heap.
// tcp_sent/tcp_poll are not cleared because this implementation
// never registers them.
// Returns ERR_OK on success; on failure the PCB is aborted instead.
static err_t pcb_detach_close(struct tcp_pcb *pcb) {
tcp_arg(pcb, nullptr);
tcp_recv(pcb, nullptr);
tcp_err(pcb, nullptr);
err_t err = tcp_close(pcb);
if (err != ERR_OK) {
tcp_abort(pcb);
}
return err;
}
// ---- LWIPRawCommon methods ----
LWIPRawCommon::~LWIPRawCommon() {
LWIP_LOCK();
if (this->pcb_ != nullptr) {
LWIP_LOG("tcp_abort(%p)", this->pcb_);
tcp_abort(this->pcb_);
pcb_detach_abort(this->pcb_);
this->pcb_ = nullptr;
}
}
@@ -222,15 +259,13 @@ int LWIPRawCommon::close() {
return -1;
}
LWIP_LOG("tcp_close(%p)", this->pcb_);
err_t err = tcp_close(this->pcb_);
err_t err = pcb_detach_close(this->pcb_);
this->pcb_ = nullptr;
if (err != ERR_OK) {
LWIP_LOG(" -> err %d", err);
tcp_abort(this->pcb_);
this->pcb_ = nullptr;
errno = err == ERR_MEM ? ENOMEM : EIO;
return -1;
}
this->pcb_ = nullptr;
return 0;
}
@@ -328,6 +363,18 @@ int LWIPRawCommon::getsockopt(int level, int optname, void *optval, socklen_t *o
*optlen = 4;
return 0;
}
if (level == SOL_SOCKET && optname == SO_RCVTIMEO) {
if (*optlen < sizeof(struct timeval)) {
errno = EINVAL;
return -1;
}
uint32_t ms = this->recv_timeout_cs_ * 10;
auto *tv = reinterpret_cast<struct timeval *>(optval);
tv->tv_sec = ms / 1000;
tv->tv_usec = (ms % 1000) * 1000;
*optlen = sizeof(struct timeval);
return 0;
}
if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
if (*optlen < 4) {
errno = EINVAL;
@@ -357,6 +404,21 @@ int LWIPRawCommon::setsockopt(int level, int optname, const void *optval, sockle
// to prevent warnings
return 0;
}
if (level == SOL_SOCKET && optname == SO_RCVTIMEO) {
if (optlen < sizeof(struct timeval)) {
errno = EINVAL;
return -1;
}
const auto *tv = reinterpret_cast<const struct timeval *>(optval);
uint32_t ms = tv->tv_sec * 1000 + tv->tv_usec / 1000;
uint32_t cs = (ms + 9) / 10; // round up to nearest centisecond
this->recv_timeout_cs_ = cs > 255 ? 255 : static_cast<uint8_t>(cs);
return 0;
}
if (level == SOL_SOCKET && optname == SO_SNDTIMEO) {
// Raw TCP writes are non-blocking (tcp_write), so send timeout is a no-op.
return 0;
}
if (level == IPPROTO_TCP && optname == TCP_NODELAY) {
if (optlen != 4) {
errno = EINVAL;
@@ -487,8 +549,25 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) {
return ERR_OK;
}
ssize_t LWIPRawImpl::read(void *buf, size_t len) {
LWIP_LOCK();
void LWIPRawImpl::wait_for_data_() {
// Wait for data without holding LWIP_LOCK so recv_fn() can run on RP2040
// (needs async_context lock).
//
// Loop until data arrives, connection closes, or the full timeout elapses.
// socket_delay() may return early due to other sockets waking the global
// socket_wake() flag, so we re-enter for the remaining time.
uint32_t timeout_ms = this->recv_timeout_cs_ * 10;
uint32_t start = millis();
while (this->waiting_for_data_()) {
uint32_t elapsed = millis() - start;
if (elapsed >= timeout_ms)
break;
socket_delay(timeout_ms - elapsed);
}
}
ssize_t LWIPRawImpl::read_locked_(void *buf, size_t len) {
// Caller must hold LWIP_LOCK. Copies available data from rx_buf_ into buf.
if (this->pcb_ == nullptr) {
errno = ECONNRESET;
return -1;
@@ -547,11 +626,26 @@ ssize_t LWIPRawImpl::read(void *buf, size_t len) {
return read;
}
ssize_t LWIPRawImpl::read(void *buf, size_t len) {
// See waiting_for_data_() for safety of unlocked reads.
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
this->wait_for_data_();
}
LWIP_LOCK();
return this->read_locked_(buf, len);
}
ssize_t LWIPRawImpl::readv(const struct iovec *iov, int iovcnt) {
// See waiting_for_data_() for safety of unlocked reads.
if (this->recv_timeout_cs_ > 0 && this->waiting_for_data_()) {
this->wait_for_data_();
}
LWIP_LOCK(); // Hold for entire scatter-gather operation
ssize_t ret = 0;
for (int i = 0; i < iovcnt; i++) {
ssize_t err = this->read(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
ssize_t err = this->read_locked_(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
if (err == -1) {
if (ret != 0) {
// if we already read some don't return an error
@@ -673,13 +767,10 @@ ssize_t LWIPRawImpl::writev(const struct iovec *iov, int iovcnt) {
LWIPRawListenImpl::~LWIPRawListenImpl() {
LWIP_LOCK();
// Abort any queued PCBs that were never accepted by the main loop.
// Clear the error callback first — tcp_abort triggers it, and we don't
// want s_queued_err_fn writing to slots during destruction.
for (uint8_t i = 0; i < this->accepted_socket_count_; i++) {
auto &entry = this->accepted_pcbs_[i];
if (entry.pcb != nullptr) {
tcp_err(entry.pcb, nullptr);
tcp_abort(entry.pcb);
pcb_detach_abort(entry.pcb);
entry.pcb = nullptr;
}
if (entry.rx_buf != nullptr) {
@@ -691,6 +782,10 @@ LWIPRawListenImpl::~LWIPRawListenImpl() {
// Listen PCBs must use tcp_close(), not tcp_abort().
// tcp_abandon() asserts pcb->state != LISTEN and would access
// fields that don't exist in the smaller tcp_pcb_listen struct.
// Don't use pcb_detach_close() here — tcp_recv()/tcp_err() also access
// fields that only exist in the full tcp_pcb, not tcp_pcb_listen.
// tcp_close() on a listen PCB is synchronous (frees immediately),
// so there are no async callbacks to worry about.
// Close here and null pcb_ so the base destructor skips tcp_abort.
if (this->pcb_ != nullptr) {
tcp_close(this->pcb_);
+11 -5
View File
@@ -57,6 +57,7 @@ class LWIPRawCommon {
// instead use it for determining whether to call lwip_output
bool nodelay_ = false;
sa_family_t family_ = 0;
uint8_t recv_timeout_cs_ = 0; // SO_RCVTIMEO in centiseconds (0 = no timeout, max 2.55s)
};
/// Connected socket implementation for LWIP raw TCP.
@@ -107,11 +108,8 @@ class LWIPRawImpl : public LWIPRawCommon {
errno = ECONNRESET;
return -1;
}
if (blocking) {
// blocking operation not supported
errno = EINVAL;
return -1;
}
// Raw TCP doesn't use a blocking flag directly. Blocking behavior
// is provided by SO_RCVTIMEO which makes read() wait via socket_delay().
return 0;
}
int loop() { return 0; }
@@ -122,6 +120,14 @@ class LWIPRawImpl : public LWIPRawCommon {
static err_t s_recv_fn(void *arg, struct tcp_pcb *pcb, struct pbuf *pb, err_t err);
protected:
// True when the socket could receive data but none has arrived yet.
// Safe to call without LWIP_LOCK — only null-checks pointers and reads a bool,
// all atomic on ARM/Xtensa. A stale value is harmless: the caller either does
// an unnecessary wait (stale true) or skips it (stale false), and the
// authoritative recheck happens under LWIP_LOCK afterward.
bool waiting_for_data_() const { return this->rx_buf_ == nullptr && !this->rx_closed_ && this->pcb_ != nullptr; }
void wait_for_data_();
ssize_t read_locked_(void *buf, size_t len);
ssize_t internal_write_(const void *buf, size_t len);
int internal_output_();
+10 -1
View File
@@ -10,7 +10,7 @@
#include "headers.h"
#ifdef USE_LWIP_FAST_SELECT
struct lwip_sock;
#include "esphome/core/lwip_fast_select.h"
#endif
namespace esphome::socket {
@@ -52,6 +52,15 @@ class LwIPSocketImpl {
return lwip_getsockopt(this->fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) {
#if defined(USE_LWIP_FAST_SELECT) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING)
// Fast path for TCP_NODELAY: directly set the pcb flag under the TCPIP core lock,
// bypassing lwip_setsockopt overhead (socket lookups, hook, switch cascade, refcounting).
if (level == IPPROTO_TCP && optname == TCP_NODELAY && optlen == sizeof(int) && optval != nullptr) {
LwIPLock lock;
if (esphome_lwip_set_nodelay(this->cached_sock_, *reinterpret_cast<const int *>(optval) != 0))
return 0;
}
#endif
return lwip_setsockopt(this->fd_, level, optname, optval, optlen);
}
int listen(int backlog) { return lwip_listen(this->fd_, backlog); }
@@ -24,23 +24,23 @@ class TemplateTextSaverBase {
template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
public:
bool save(const std::string &value) override {
int diff = value.compare(this->prev_);
if (diff != 0) {
// If string is bigger than the allocation, do not save it.
// We don't need to waste ram setting prev_value either.
int size = value.size();
if (size <= SZ) {
// Make it into a length prefixed thing
unsigned char temp[SZ + 1];
memcpy(temp + 1, value.c_str(), size);
// SZ should be pre checked at the schema level, it can't go past the char range.
temp[0] = ((unsigned char) size);
this->pref_.save(&temp);
this->prev_.assign(value);
return true;
}
if (value == this->prev_) {
return true; // No change, nothing to save
}
return false;
// If string is bigger than the allocation, do not save it.
// We don't need to waste ram setting prev_value either.
int size = value.size();
if (size > SZ) {
return false;
}
// Make it into a length prefixed thing
unsigned char temp[SZ + 1];
memcpy(temp + 1, value.c_str(), size);
// SZ should be pre checked at the schema level, it can't go past the char range.
temp[0] = ((unsigned char) size);
this->pref_.save(&temp);
this->prev_.assign(value);
return true;
}
// Make the preference object. Fill the provided location with the saved data
@@ -26,6 +26,7 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
if (!this->supported_modes_.empty()) {
traits.set_supported_modes(this->supported_modes_);
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_OPERATION_MODE);
}
traits.set_supports_current_temperature(true);
+6 -6
View File
@@ -88,16 +88,16 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
struct timeval timev {
.tv_sec = static_cast<time_t>(epoch), .tv_usec = 0,
};
#ifdef USE_ESP8266
// ESP8266 settimeofday() requires tz to be nullptr
int ret = settimeofday(&timev, nullptr);
#else
struct timezone tz = {0, 0};
int ret = settimeofday(&timev, &tz);
if (ret != 0 && errno == EINVAL) {
// Some ESP8266 frameworks abort when timezone parameter is not NULL
// while ESP32 expects it not to be NULL
ret = settimeofday(&timev, nullptr);
}
#endif
if (ret != 0) {
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
ESP_LOGW(TAG, "settimeofday() failed with code %d", ret);
}
#endif
auto time = this->now();
@@ -105,15 +105,34 @@ void RP2040UartComponent::setup() {
}
}
// Determine which hardware UART to use. A pin that is not specified
// should not prevent hardware UART selection — one-way UART is valid.
// When both pins are configured, both must be HW-capable and agree on UART number.
// When only one pin is configured (nullptr other), use that pin's HW UART.
// If a pin is configured but not HW-capable (inverted/invalid), fall back to SerialPIO.
int8_t hw_uart = -1;
const bool tx_configured = (this->tx_pin_ != nullptr);
const bool rx_configured = (this->rx_pin_ != nullptr);
if (tx_configured && rx_configured) {
// Both pins configured — both must map to the same hardware UART
if (tx_hw != -1 && rx_hw != -1 && tx_hw == rx_hw) {
hw_uart = tx_hw;
}
} else if (tx_configured) {
hw_uart = tx_hw;
} else if (rx_configured) {
hw_uart = rx_hw;
}
#ifdef USE_LOGGER
if (tx_hw == rx_hw && logger::global_logger->get_uart() == tx_hw) {
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", tx_hw);
tx_hw = -1;
rx_hw = -1;
if (hw_uart != -1 && logger::global_logger->get_uart() == hw_uart) {
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", hw_uart);
hw_uart = -1;
}
#endif
if (tx_hw == -1 || rx_hw == -1 || tx_hw != rx_hw) {
if (hw_uart == -1) {
ESP_LOGV(TAG, "Using SerialPIO");
pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin();
pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin();
@@ -127,13 +146,15 @@ void RP2040UartComponent::setup() {
} else {
ESP_LOGV(TAG, "Using Hardware Serial");
SerialUART *serial;
if (tx_hw == 0) {
if (hw_uart == 0) {
serial = &Serial1;
} else {
serial = &Serial2;
}
serial->setTX(this->tx_pin_->get_pin());
serial->setRX(this->rx_pin_->get_pin());
if (this->tx_pin_ != nullptr)
serial->setTX(this->tx_pin_->get_pin());
if (this->rx_pin_ != nullptr)
serial->setRX(this->rx_pin_->get_pin());
serial->setFIFOSize(this->rx_buffer_size_);
serial->begin(this->baud_rate_, config);
this->serial_ = serial;
+1
View File
@@ -19,6 +19,7 @@ CONF_DELTASOL_BS_2009 = "deltasol_bs_2009"
CONF_DELTASOL_BS2 = "deltasol_bs2"
CONF_DELTASOL_C = "deltasol_c"
CONF_DELTASOL_CS2 = "deltasol_cs2"
CONF_DELTASOL_CS4 = "deltasol_cs4"
CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus"
CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
@@ -20,6 +20,7 @@ from .. import (
CONF_DELTASOL_BS_PLUS,
CONF_DELTASOL_C,
CONF_DELTASOL_CS2,
CONF_DELTASOL_CS4,
CONF_DELTASOL_CS_PLUS,
CONF_VBUS_ID,
VBus,
@@ -31,6 +32,7 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component)
DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component)
DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component)
DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component)
DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4BSensor", cg.Component)
DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component)
VBusCustom = vbus_ns.class_("VBusCustomBSensor", cg.Component)
VBusCustomSub = vbus_ns.class_("VBusCustomSubBSensor", cg.Component)
@@ -186,6 +188,28 @@ CONFIG_SCHEMA = cv.typed_schema(
),
}
),
CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaSol_CS4),
cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_PROBLEM,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
),
CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
@@ -350,6 +374,23 @@ async def to_code(config):
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
cg.add(var.set_s4_error_bsensor(sens))
elif config[CONF_MODEL] == CONF_DELTASOL_CS4:
cg.add(var.set_command(0x0100))
cg.add(var.set_source(0x1122))
cg.add(var.set_dest(0x0010))
if CONF_SENSOR1_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR])
cg.add(var.set_s1_error_bsensor(sens))
if CONF_SENSOR2_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR])
cg.add(var.set_s2_error_bsensor(sens))
if CONF_SENSOR3_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR])
cg.add(var.set_s3_error_bsensor(sens))
if CONF_SENSOR4_ERROR in config:
sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR])
cg.add(var.set_s4_error_bsensor(sens))
elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
cg.add(var.set_command(0x0100))
cg.add(var.set_source(0x2211))
@@ -110,6 +110,25 @@ void DeltaSolCS2BSensor::handle_message(std::vector<uint8_t> &message) {
this->s4_error_bsensor_->publish_state(message[18] & 8);
}
void DeltaSolCS4BSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Deltasol CS4:");
LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_);
LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_);
LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_);
LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_);
}
void DeltaSolCS4BSensor::handle_message(std::vector<uint8_t> &message) {
if (this->s1_error_bsensor_ != nullptr)
this->s1_error_bsensor_->publish_state(message[20] & 1);
if (this->s2_error_bsensor_ != nullptr)
this->s2_error_bsensor_->publish_state(message[20] & 2);
if (this->s3_error_bsensor_ != nullptr)
this->s3_error_bsensor_->publish_state(message[20] & 4);
if (this->s4_error_bsensor_ != nullptr)
this->s4_error_bsensor_->publish_state(message[20] & 8);
}
void DeltaSolCSPlusBSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_);
@@ -94,6 +94,23 @@ class DeltaSolCS2BSensor : public VBusListener, public Component {
void handle_message(std::vector<uint8_t> &message) override;
};
class DeltaSolCS4BSensor : public VBusListener, public Component {
public:
void dump_config() override;
void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; }
void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; }
void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; }
void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; }
protected:
binary_sensor::BinarySensor *s1_error_bsensor_{nullptr};
binary_sensor::BinarySensor *s2_error_bsensor_{nullptr};
binary_sensor::BinarySensor *s3_error_bsensor_{nullptr};
binary_sensor::BinarySensor *s4_error_bsensor_{nullptr};
void handle_message(std::vector<uint8_t> &message) override;
};
class DeltaSolCSPlusBSensor : public VBusListener, public Component {
public:
void dump_config() override;
+140 -1
View File
@@ -36,6 +36,7 @@ from .. import (
CONF_DELTASOL_BS_PLUS,
CONF_DELTASOL_C,
CONF_DELTASOL_CS2,
CONF_DELTASOL_CS4,
CONF_DELTASOL_CS_PLUS,
CONF_VBUS_ID,
VBus,
@@ -47,6 +48,7 @@ DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component)
DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component)
DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component)
DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component)
DeltaSol_CS4 = vbus_ns.class_("DeltaSolCS4Sensor", cg.Component)
DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component)
VBusCustom = vbus_ns.class_("VBusCustomSensor", cg.Component)
VBusCustomSub = vbus_ns.class_("VBusCustomSubSensor", cg.Component)
@@ -438,6 +440,99 @@ CONFIG_SCHEMA = cv.typed_schema(
),
}
),
CONF_DELTASOL_CS4: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaSol_CS4),
cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus),
cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE_5): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_PERCENT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema(
unit_of_measurement=UNIT_HOUR,
icon=ICON_TIMER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema(
unit_of_measurement=UNIT_HOUR,
icon=ICON_TIMER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MINUTE,
icon=ICON_TIMER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_VERSION): sensor.sensor_schema(
accuracy_decimals=2,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_FLOW_RATE): sensor.sensor_schema(
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
),
CONF_DELTASOL_CS_PLUS: cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DeltaSol_CS_Plus),
@@ -734,7 +829,51 @@ async def to_code(config):
sens = await sensor.new_sensor(config[CONF_VERSION])
cg.add(var.set_version_sensor(sens))
if config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
elif config[CONF_MODEL] == CONF_DELTASOL_CS4:
cg.add(var.set_command(0x0100))
cg.add(var.set_source(0x1122))
cg.add(var.set_dest(0x0010))
if CONF_TEMPERATURE_1 in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1])
cg.add(var.set_temperature1_sensor(sens))
if CONF_TEMPERATURE_2 in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2])
cg.add(var.set_temperature2_sensor(sens))
if CONF_TEMPERATURE_3 in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3])
cg.add(var.set_temperature3_sensor(sens))
if CONF_TEMPERATURE_4 in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4])
cg.add(var.set_temperature4_sensor(sens))
if CONF_TEMPERATURE_5 in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE_5])
cg.add(var.set_temperature5_sensor(sens))
if CONF_PUMP_SPEED_1 in config:
sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1])
cg.add(var.set_pump_speed1_sensor(sens))
if CONF_PUMP_SPEED_2 in config:
sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2])
cg.add(var.set_pump_speed2_sensor(sens))
if CONF_OPERATING_HOURS_1 in config:
sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1])
cg.add(var.set_operating_hours1_sensor(sens))
if CONF_OPERATING_HOURS_2 in config:
sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2])
cg.add(var.set_operating_hours2_sensor(sens))
if CONF_HEAT_QUANTITY in config:
sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY])
cg.add(var.set_heat_quantity_sensor(sens))
if CONF_TIME in config:
sens = await sensor.new_sensor(config[CONF_TIME])
cg.add(var.set_time_sensor(sens))
if CONF_VERSION in config:
sens = await sensor.new_sensor(config[CONF_VERSION])
cg.add(var.set_version_sensor(sens))
if CONF_FLOW_RATE in config:
sens = await sensor.new_sensor(config[CONF_FLOW_RATE])
cg.add(var.set_flow_rate_sensor(sens))
elif config[CONF_MODEL] == CONF_DELTASOL_CS_PLUS:
cg.add(var.set_command(0x0100))
cg.add(var.set_source(0x2211))
cg.add(var.set_dest(0x0010))
@@ -168,6 +168,52 @@ void DeltaSolCS2Sensor::handle_message(std::vector<uint8_t> &message) {
this->version_sensor_->publish_state(get_u16(message, 28) * 0.01f);
}
void DeltaSolCS4Sensor::dump_config() {
ESP_LOGCONFIG(TAG, "Deltasol CS4:");
LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_);
LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_);
LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_);
LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_);
LOG_SENSOR(" ", "Temperature 5", this->temperature5_sensor_);
LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_);
LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_);
LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_);
LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_);
LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_);
LOG_SENSOR(" ", "System Time", this->time_sensor_);
LOG_SENSOR(" ", "FW Version", this->version_sensor_);
LOG_SENSOR(" ", "Flow Rate", this->flow_rate_sensor_);
}
void DeltaSolCS4Sensor::handle_message(std::vector<uint8_t> &message) {
if (this->temperature1_sensor_ != nullptr)
this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f);
if (this->temperature2_sensor_ != nullptr)
this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f);
if (this->temperature3_sensor_ != nullptr)
this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f);
if (this->temperature4_sensor_ != nullptr)
this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f);
if (this->temperature5_sensor_ != nullptr)
this->temperature5_sensor_->publish_state(get_i16(message, 36) * 0.1f);
if (this->pump_speed1_sensor_ != nullptr)
this->pump_speed1_sensor_->publish_state(message[8]);
if (this->pump_speed2_sensor_ != nullptr)
this->pump_speed2_sensor_->publish_state(message[12]);
if (this->operating_hours1_sensor_ != nullptr)
this->operating_hours1_sensor_->publish_state(get_u16(message, 10));
if (this->operating_hours2_sensor_ != nullptr)
this->operating_hours2_sensor_->publish_state(get_u16(message, 14));
if (this->heat_quantity_sensor_ != nullptr)
this->heat_quantity_sensor_->publish_state((get_u16(message, 30) << 16) + get_u16(message, 28));
if (this->time_sensor_ != nullptr)
this->time_sensor_->publish_state(get_u16(message, 22));
if (this->version_sensor_ != nullptr)
this->version_sensor_->publish_state(get_u16(message, 32) * 0.01f);
if (this->flow_rate_sensor_ != nullptr)
this->flow_rate_sensor_->publish_state(get_u16(message, 38));
}
void DeltaSolCSPlusSensor::dump_config() {
ESP_LOGCONFIG(TAG, "Deltasol CS Plus:");
LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_);
@@ -122,6 +122,41 @@ class DeltaSolCS2Sensor : public VBusListener, public Component {
void handle_message(std::vector<uint8_t> &message) override;
};
class DeltaSolCS4Sensor : public VBusListener, public Component {
public:
void dump_config() override;
void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; }
void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; }
void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; }
void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; }
void set_temperature5_sensor(sensor::Sensor *sensor) { this->temperature5_sensor_ = sensor; }
void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; }
void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; }
void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; }
void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; }
void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; }
void set_time_sensor(sensor::Sensor *sensor) { this->time_sensor_ = sensor; }
void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; }
void set_flow_rate_sensor(sensor::Sensor *sensor) { this->flow_rate_sensor_ = sensor; }
protected:
sensor::Sensor *temperature1_sensor_{nullptr};
sensor::Sensor *temperature2_sensor_{nullptr};
sensor::Sensor *temperature3_sensor_{nullptr};
sensor::Sensor *temperature4_sensor_{nullptr};
sensor::Sensor *temperature5_sensor_{nullptr};
sensor::Sensor *pump_speed1_sensor_{nullptr};
sensor::Sensor *pump_speed2_sensor_{nullptr};
sensor::Sensor *operating_hours1_sensor_{nullptr};
sensor::Sensor *operating_hours2_sensor_{nullptr};
sensor::Sensor *heat_quantity_sensor_{nullptr};
sensor::Sensor *time_sensor_{nullptr};
sensor::Sensor *version_sensor_{nullptr};
sensor::Sensor *flow_rate_sensor_{nullptr};
void handle_message(std::vector<uint8_t> &message) override;
};
class DeltaSolCSPlusSensor : public VBusListener, public Component {
public:
void dump_config() override;
+1 -1
View File
@@ -2181,7 +2181,7 @@ json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *we
}
json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
@@ -11,6 +11,10 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
handler = new internal::AuthMiddlewareHandler(handler, &credentials_);
}
#endif
this->add_handler_without_auth(handler);
}
void WebServerBase::add_handler_without_auth(AsyncWebHandler *handler) {
this->handlers_.push_back(handler);
if (this->server_ != nullptr) {
this->server_->addHandler(handler);
@@ -122,6 +122,14 @@ class WebServerBase {
#endif
void add_handler(AsyncWebHandler *handler);
/**
* WARNING: Registers a handler that bypasses the USE_WEBSERVER_AUTH middleware.
*
* This should only be used for endpoints that are intentionally unauthenticated
* (for example, captive portal or very limited-status endpoints). For normal
* endpoints that should respect web server authentication, use add_handler().
*/
void add_handler_without_auth(AsyncWebHandler *handler);
void set_port(uint16_t port) { port_ = port; }
uint16_t get_port() const { return port_; }
+1 -7
View File
@@ -166,6 +166,7 @@ TTLS_PHASE_2 = {
}
EAP_AUTH_SCHEMA = cv.All(
cv.only_on([Platform.ESP32, Platform.ESP8266]),
cv.Schema(
{
cv.Optional(CONF_IDENTITY): cv.string_strict,
@@ -562,13 +563,6 @@ async def to_code(config):
cg.add_library("ESP8266WiFi", None)
elif CORE.is_rp2040:
cg.add_library("WiFi", None)
# RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart
# mDNS when the network interface reconnects. However, this callback is disabled
# in the arduino-pico framework. As a workaround, we block component setup until
# WiFi is connected via can_proceed(), ensuring mDNS.begin() is called with an
# active connection. This define enables the loop priority sorting infrastructure
# used during the setup blocking phase.
cg.add_define("USE_LOOP_PRIORITY")
if CORE.is_esp32:
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
+1 -15
View File
@@ -6,7 +6,7 @@
#include <type_traits>
#ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_eap_client.h>
#else
#include <esp_wpa2.h>
@@ -2109,20 +2109,6 @@ void WiFiComponent::retry_connect() {
}
}
#ifdef USE_RP2040
// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart
// mDNS when the network interface reconnects. However, this callback is disabled
// in the arduino-pico framework. As a workaround, we block component setup until
// WiFi is connected, ensuring mDNS.begin() is called with an active connection.
bool WiFiComponent::can_proceed() {
if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) {
return true;
}
return this->is_connected_();
}
#endif
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
bool WiFiComponent::is_connected_() const {
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
+1 -5
View File
@@ -18,7 +18,7 @@
#endif
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_eap_client.h>
#else
#include <esp_wpa2.h>
@@ -437,10 +437,6 @@ class WiFiComponent : public Component {
void retry_connect();
#ifdef USE_RP2040
bool can_proceed() override;
#endif
void set_reboot_timeout(uint32_t reboot_timeout);
bool is_connected() const { return this->connected_; }
@@ -17,7 +17,7 @@
#include <memory>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_eap_client.h>
#else
#include <esp_wpa2.h>
@@ -75,7 +75,11 @@ struct IDFWiFiEvent {
#if USE_NETWORK_IPV6
ip_event_got_ip6_t ip_got_ip6;
#endif /* USE_NETWORK_IPV6 */
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
ip_event_assigned_ip_to_client_t ip_assigned_ip_to_client;
#else
ip_event_ap_staipassigned_t ip_ap_staipassigned;
#endif
} data;
};
@@ -116,8 +120,13 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t));
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
memcpy(&event.data.ip_assigned_ip_to_client, event_data, sizeof(ip_event_assigned_ip_to_client_t));
#else
} else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
memcpy(&event.data.ip_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t));
#endif
} else {
// did not match any event, don't send anything
return;
@@ -407,7 +416,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
if (eap_opt.has_value()) {
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
EAPAuth eap = *eap_opt;
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
#else
err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
@@ -419,7 +428,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
int client_cert_len = strlen(eap.client_cert);
int client_key_len = strlen(eap.client_key);
if (ca_cert_len) {
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
#else
err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
@@ -432,7 +441,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// validation is not required as the config tool has already validated it
if (client_cert_len && client_key_len) {
// if we have certs, this must be EAP-TLS
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
(uint8_t *) eap.client_key, client_key_len + 1,
(uint8_t *) eap.password.c_str(), eap.password.length());
@@ -446,7 +455,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
}
} else {
// in the absence of certs, assume this is username/password based
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
#else
err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
@@ -454,7 +463,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
if (err != ESP_OK) {
ESP_LOGV(TAG, "set_username failed %d", err);
}
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
#else
err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
@@ -463,7 +472,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
ESP_LOGV(TAG, "set_password failed %d", err);
}
// set TTLS Phase 2, defaults to MSCHAPV2
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_eap_client_set_ttls_phase2_method(eap.ttls_phase_2);
#else
err = esp_wifi_sta_wpa2_ent_set_ttls_phase2_method(eap.ttls_phase_2);
@@ -472,7 +481,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
ESP_LOGV(TAG, "set_ttls_phase2_method failed %d", err);
}
}
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
err = esp_wifi_sta_enterprise_enable();
#else
err = esp_wifi_sta_wpa2_ent_enable();
@@ -628,14 +637,26 @@ const char *get_disconnect_reason_str(uint8_t reason) {
return "Auth Expired";
case WIFI_REASON_AUTH_LEAVE:
return "Auth Leave";
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
case WIFI_REASON_DISASSOC_DUE_TO_INACTIVITY:
return "Disassociated Due to Inactivity";
#else
case WIFI_REASON_ASSOC_EXPIRE:
return "Association Expired";
#endif
case WIFI_REASON_ASSOC_TOOMANY:
return "Too Many Associations";
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
case WIFI_REASON_CLASS2_FRAME_FROM_NONAUTH_STA:
return "Class 2 Frame from Non-Authenticated STA";
case WIFI_REASON_CLASS3_FRAME_FROM_NONASSOC_STA:
return "Class 3 Frame from Non-Associated STA";
#else
case WIFI_REASON_NOT_AUTHED:
return "Not Authenticated";
case WIFI_REASON_NOT_ASSOCED:
return "Not Associated";
#endif
case WIFI_REASON_ASSOC_LEAVE:
return "Association Leave";
case WIFI_REASON_ASSOC_NOT_AUTHED:
@@ -688,7 +709,7 @@ const char *get_disconnect_reason_str(uint8_t reason) {
return "Association comeback time too long";
case WIFI_REASON_SA_QUERY_TIMEOUT:
return "SA query timeout";
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
return "No AP found with compatible security";
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
@@ -917,8 +938,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf);
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
const auto &it = data->data.ip_assigned_ip_to_client;
#else
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
const auto &it = data->data.ip_ap_staipassigned;
#endif
ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip));
}
}
+102
View File
@@ -0,0 +1,102 @@
import esphome.codegen as cg
from esphome.components import uart, usb_uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
import esphome.final_validate as fv
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["api", "uart"]
CONF_BUFFER_SIZE = "buffer_size"
CONF_INITIAL_TIMEOUT = "initial_timeout"
CONF_MIN_TIMEOUT = "min_timeout"
CONF_MAX_TIMEOUT = "max_timeout"
CONF_USB_UART_ID = "usb_uart_id"
# Default ACK timeout values calibrated for hardware UART (460800 baud, ~2-5 ms round-trip)
_DEFAULT_HW_INITIAL_TIMEOUT = 1600
_DEFAULT_HW_MIN_TIMEOUT = 400
_DEFAULT_HW_MAX_TIMEOUT = 3200
# Optimized ACK timeout values for USB CDC ACM paths (~3-5 ms round-trip with RX callback)
_DEFAULT_USB_INITIAL_TIMEOUT = 30
_DEFAULT_USB_MIN_TIMEOUT = 15
_DEFAULT_USB_MAX_TIMEOUT = 200
zigbee_proxy_ns = cg.esphome_ns.namespace("zigbee_proxy")
ZigbeeProxy = zigbee_proxy_ns.class_("ZigbeeProxy", cg.Component, uart.UARTDevice)
def final_validate(config):
full_config = fv.full_config.get()
if (wifi_conf := full_config.get(CONF_WIFI)) and (
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
):
raise cv.Invalid(
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ZigbeeProxy),
cv.Optional(CONF_BUFFER_SIZE): cv.SplitDefault(
cv.int_range(min=256, max=2048),
esp8266=512,
default=1024,
),
# When usb_uart_id is present the component registers an RX callback
# for zero-wakeup-cycle data delivery and selects USB-optimized ACK
# timeout defaults. Explicit timeout keys always win.
cv.Optional(CONF_USB_UART_ID): cv.use_id(usb_uart.USBUartChannel),
cv.Optional(CONF_INITIAL_TIMEOUT): cv.int_range(min=10, max=10000),
cv.Optional(CONF_MIN_TIMEOUT): cv.int_range(min=10, max=5000),
cv.Optional(CONF_MAX_TIMEOUT): cv.int_range(min=50, max=10000),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA),
)
FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add_define("USE_ZIGBEE_PROXY")
# Set buffer size via define for compile-time allocation
if CONF_BUFFER_SIZE in config:
cg.add_define("ZIGBEE_PROXY_BUFFER_SIZE", config[CONF_BUFFER_SIZE])
# Select timeout defaults based on UART transport type.
# USB CDC ACM with the RX callback has ~3-5 ms round-trip latency; hardware
# UART is similar (~2-5 ms). Different defaults are kept so that future
# non-callback USB paths still get conservative starting values.
is_usb = CONF_USB_UART_ID in config
if is_usb:
cg.add_define("USE_ZIGBEE_PROXY_USB_UART")
usb_ch = await cg.get_variable(config[CONF_USB_UART_ID])
cg.add(var.set_usb_uart_channel(usb_ch))
initial_timeout = config.get(
CONF_INITIAL_TIMEOUT,
_DEFAULT_USB_INITIAL_TIMEOUT if is_usb else _DEFAULT_HW_INITIAL_TIMEOUT,
)
min_timeout = config.get(
CONF_MIN_TIMEOUT,
_DEFAULT_USB_MIN_TIMEOUT if is_usb else _DEFAULT_HW_MIN_TIMEOUT,
)
max_timeout = config.get(
CONF_MAX_TIMEOUT,
_DEFAULT_USB_MAX_TIMEOUT if is_usb else _DEFAULT_HW_MAX_TIMEOUT,
)
cg.add(var.set_initial_timeout(initial_timeout))
cg.add(var.set_min_timeout(min_timeout))
cg.add(var.set_max_timeout(max_timeout))
@@ -0,0 +1,402 @@
#include "zigbee_proxy.h"
#ifdef USE_ZIGBEE_PROXY
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::zigbee_proxy {
static const char *const TAG = "zigbee_proxy";
// CRC-CCITT lookup table for polynomial 0x1021 (x^16 + x^12 + x^5 + 1)
static const uint16_t CRC_TABLE[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD,
0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A,
0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B,
0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861,
0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96,
0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87,
0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A,
0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3,
0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290,
0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E,
0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F,
0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C,
0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83,
0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74,
0x2E93, 0x3EB2, 0x0ED1, 0x1EF0};
uint16_t ZigbeeProxy::calculate_crc_(const uint8_t *data, size_t length, uint16_t init) {
uint16_t crc = init;
for (size_t i = 0; i < length; i++) {
crc = (crc << 8) ^ CRC_TABLE[(crc >> 8) ^ data[i]];
}
return crc;
}
bool ZigbeeProxy::validate_frame_crc_() {
// CRC is calculated over control byte + data
// rx_buffer_[0] contains control byte, rx_buffer_[1..rx_buffer_index_-3] contains data
// rx_buffer_[rx_buffer_index_-2] and rx_buffer_[rx_buffer_index_-1] contain CRC
if (this->rx_buffer_index_ < 3) {
// Frame too short to contain CRC
return false;
}
// Calculate CRC over control + data (exclude CRC bytes)
uint16_t calculated = this->calculate_crc_(this->rx_buffer_.data(), this->rx_buffer_index_ - 2);
// Extract received CRC (big-endian)
uint16_t received = (static_cast<uint16_t>(this->rx_buffer_[this->rx_buffer_index_ - 2]) << 8) |
this->rx_buffer_[this->rx_buffer_index_ - 1];
if (calculated != received) {
ESP_LOGW(TAG, "CRC validation failed: calculated=0x%04X, received=0x%04X", calculated, received);
return false;
}
return true;
}
void ZigbeeProxy::parse_control_byte_(uint8_t control) {
// Decode frame type based on bit patterns:
// DATA: 0xxxxxxx (bit 7 = 0)
// ACK: 10x0xxxx (bits 7-6 = 10, bit 5 = 0)
// NAK: 10x1xxxx (bits 7-6 = 10, bit 5 = 1)
// RST: 11000000 (0xC0)
// RSTACK: 11000001 (0xC1)
// ERROR: 11000010 (0xC2)
AshFrameType frame_type;
if ((control & 0x80) == 0) {
// Bit 7 = 0: DATA frame
frame_type = AshFrameType::DATA;
} else if ((control & 0xC0) == 0x80) {
// Bits 7-6 = 10: ACK or NAK
// ACK format: 100nrPPP (bit 5 = 0)
// NAK format: 101nrPPP (bit 5 = 1)
if ((control & 0x20) == 0) {
frame_type = AshFrameType::ACK;
} else {
frame_type = AshFrameType::NAK;
}
} else {
// Bits 7-6 = 11: control frames (RST, RSTACK, ERROR)
uint8_t control_bits = control & 0x07;
if (control_bits == 0x00) {
frame_type = AshFrameType::RST;
} else if (control_bits == 0x01) {
frame_type = AshFrameType::RSTACK;
} else if (control_bits == 0x02) {
frame_type = AshFrameType::ERROR;
} else {
ESP_LOGW(TAG, "Unknown control frame type: 0x%02X", control);
return;
}
}
// Extract sequence numbers from DATA frame format: 0ffrPPPP
// Bits 6-4 = frmNum, bit 3 = reTx, bits 2-0 = ackNum
uint8_t frame_num = (control >> 4) & 0x07; // Bits 6-4
uint8_t ack_num = control & 0x07; // Bits 2-0
bool retx = (control & 0x08) != 0; // Bit 3 (for DATA frames)
ESP_LOGV(TAG, "Parsed control byte: type=%d, frmNum=%d, ackNum=%d, reTx=%d", static_cast<int>(frame_type), frame_num,
ack_num, retx);
// Handle frame based on type
switch (frame_type) {
case AshFrameType::DATA: {
// Check sequence number
if (frame_num != this->rx_sequence_) {
ESP_LOGW(TAG, "Out of sequence DATA frame: expected %d, got %d", this->rx_sequence_, frame_num);
this->send_nak_frame_(this->rx_sequence_);
return;
}
// Check for ACK in DATA frame (piggybacked ACK) BEFORE processing
// This must happen first because the handler may send new frames
if (this->tx_buffer_pending_ && ack_num == ((this->tx_pending_frame_num_ + 1) & ASH_MAX_SEQUENCE)) {
// ackNum means "I expect frame N next" = "I received up to N-1"
// So if ackNum == pending+1, our pending frame was received
uint32_t rtt = millis() - this->ack_timer_start_;
this->update_adaptive_timeout_(rtt);
this->clear_tx_buffer_();
ESP_LOGV(TAG, "ACK received (piggybacked in DATA), RTT: %u ms", rtt);
}
// Increment RX sequence and send ACK (ack_num = next expected frame)
this->increment_rx_sequence_();
this->send_ack_frame_(this->rx_sequence_);
// Extract payload (skip control byte, exclude CRC)
size_t payload_length = this->rx_buffer_index_ > 3 ? this->rx_buffer_index_ - 3 : 0;
const uint8_t *payload = this->rx_buffer_.data() + 1;
// During boot sequence, route to boot handler
if (this->boot_sequence_active_ && payload_length > 0) {
this->handle_boot_data_frame_(payload, payload_length);
} else if (this->api_connection_ != nullptr && payload_length > 0) {
// Forward EZSP payload to client via client-side ASH DATA frame
this->forward_ncp_data_to_client_(payload, payload_length);
}
break;
}
case AshFrameType::ACK:
// Check if this ACKs our pending frame
// ackNum means "I expect frame N next" = "I received all frames up to N-1"
// So if ackNum == pending+1, our pending frame was acknowledged
if (this->tx_buffer_pending_ && ack_num == ((this->tx_pending_frame_num_ + 1) & ASH_MAX_SEQUENCE)) {
uint32_t rtt = millis() - this->ack_timer_start_;
this->update_adaptive_timeout_(rtt);
this->clear_tx_buffer_();
ESP_LOGV(TAG, "ACK received for frame %d, RTT: %u ms", this->tx_pending_frame_num_, rtt);
}
break;
case AshFrameType::NAK:
ESP_LOGW(TAG, "NAK received for frame %d, retransmitting", ack_num);
if (this->tx_buffer_pending_) {
this->handle_retransmission_();
}
break;
case AshFrameType::RST: {
ESP_LOGW(TAG, "Received RST frame from NCP, sending RSTACK");
// Send RSTACK response
uint8_t rstack_data[] = {0x02, 0x01, 0x00}; // RSTACK with reset code
this->handle_rstack_frame_(rstack_data, sizeof(rstack_data));
break;
}
case AshFrameType::RSTACK:
this->handle_rstack_frame_(this->rx_buffer_.data() + 1, this->rx_buffer_index_ - 3);
break;
case AshFrameType::ERROR:
this->handle_error_frame_(this->rx_buffer_.data() + 1, this->rx_buffer_index_ - 3);
break;
}
}
bool ZigbeeProxy::parse_byte_(uint8_t byte) {
// ASH_CAN (0x1A) resets the parser state - discard any partial frame
static constexpr uint8_t ASH_CAN_BYTE = 0x1A;
if (byte == ASH_CAN_BYTE) {
this->rx_buffer_index_ = 0;
this->escape_next_byte_ = false;
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
return false;
}
switch (this->parsing_state_) {
case ParsingState::WAIT_FLAG_START:
// Handle escape sequences - NCP may send escaped control byte at frame start
if (byte == ASH_ESCAPE_BYTE) {
this->escape_next_byte_ = true;
return false;
}
if (this->escape_next_byte_) {
byte ^= ASH_XOR_BYTE;
this->escape_next_byte_ = false;
// After unescaping, check if it's a CAN byte (0x1A)
if (byte == ASH_CAN_BYTE) {
this->rx_buffer_index_ = 0;
return false;
}
}
if (byte == ASH_FLAG_BYTE) {
// Start of frame with FLAG delimiter
this->rx_buffer_index_ = 0;
this->escape_next_byte_ = false;
this->parsing_state_ = ParsingState::WAIT_CONTROL;
ESP_LOGV(TAG, "Frame start detected (FLAG)");
} else if (this->ash_state_ == AshState::CONNECTED) {
// When connected, NCP often omits leading FLAG on responses
// Any byte could be a control byte:
// - DATA frames: 0x00-0x7F (bit 7 = 0)
// - ACK frames: 0x80-0x9F (bits 7-6 = 10, bit 5 = 0)
// - NAK frames: 0xA0-0xBF (bits 7-6 = 10, bit 5 = 1)
// - RST/RSTACK/ERROR: 0xC0-0xC2 (bits 7-6 = 11)
// Skip reserved bytes that cannot be valid control bytes
if (byte != 0x11 && byte != 0x13) {
this->rx_buffer_index_ = 0;
this->rx_buffer_[this->rx_buffer_index_++] = byte;
this->parsing_state_ = ParsingState::WAIT_DATA;
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
}
} else if ((byte & 0x80) != 0) {
// Before connected, only accept control/management frames (bit 7 set)
// This handles RSTACK (0xC1), ACK (0x8X), NAK (0xAX), ERROR (0xC2)
this->rx_buffer_index_ = 0;
this->rx_buffer_[this->rx_buffer_index_++] = byte;
this->parsing_state_ = ParsingState::WAIT_DATA;
ESP_LOGV(TAG, "Frame start detected (control byte 0x%02X)", byte);
}
break;
case ParsingState::WAIT_CONTROL:
if (byte == ASH_FLAG_BYTE) {
// Empty frame or repeated FLAG
ESP_LOGV(TAG, "Empty frame or repeated FLAG, restarting");
this->rx_buffer_index_ = 0;
return false;
}
if (byte == ASH_ESCAPE_BYTE) {
this->escape_next_byte_ = true;
return false;
}
if (this->escape_next_byte_) {
byte ^= ASH_XOR_BYTE;
this->escape_next_byte_ = false;
}
// Store control byte
this->rx_buffer_[this->rx_buffer_index_++] = byte;
this->parsing_state_ = ParsingState::WAIT_DATA;
break;
case ParsingState::WAIT_DATA:
if (byte == ASH_FLAG_BYTE) {
// End of frame - validate and process
ESP_LOGV(TAG, "Frame complete, %u bytes in buffer", this->rx_buffer_index_);
if (this->validate_frame_crc_()) {
this->parse_control_byte_(this->rx_buffer_[0]);
} else {
// CRC failed - WARN logs byte count only; hex dump at VERBOSE to avoid heap allocation in production
ESP_LOGW(TAG, "CRC failed (%u bytes)", this->rx_buffer_index_);
ESP_LOGV(TAG, "CRC failed frame: %s",
format_hex_pretty(this->rx_buffer_.data(), this->rx_buffer_index_).c_str());
this->send_nak_frame_(this->rx_sequence_);
}
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
return true;
}
if (byte == ASH_ESCAPE_BYTE) {
this->escape_next_byte_ = true;
return false;
}
if (this->escape_next_byte_) {
byte ^= ASH_XOR_BYTE;
this->escape_next_byte_ = false;
}
// Check buffer overflow
if (this->rx_buffer_index_ >= MAX_ASH_FRAME_SIZE) {
ESP_LOGE(TAG, "RX buffer overflow, frame too large");
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
return false;
}
// Store data byte
this->rx_buffer_[this->rx_buffer_index_++] = byte;
break;
default:
this->parsing_state_ = ParsingState::WAIT_FLAG_START;
break;
}
return false;
}
size_t ZigbeeProxy::build_frame_(uint8_t *output, const uint8_t *data, size_t length, AshFrameType type,
uint8_t frame_num, uint8_t ack_num, bool retx) {
size_t pos = 0;
// Start with FLAG
output[pos++] = ASH_FLAG_BYTE;
// Build control byte
uint8_t control = 0;
switch (type) {
case AshFrameType::DATA:
// DATA frame format: 0ffrPPPP
// Bit 7 = 0 (DATA indicator), bits 6-4 = frmNum, bit 3 = reTx, bits 2-0 = ackNum
control = (frame_num << 4) | (retx ? 0x08 : 0x00) | ack_num;
break;
case AshFrameType::ACK:
control = 0x80 | ack_num;
break;
case AshFrameType::NAK:
control = 0xA0 | ack_num;
break;
case AshFrameType::RST:
control = 0xC0;
break;
case AshFrameType::RSTACK:
control = 0xC1;
break;
case AshFrameType::ERROR:
control = 0xC2;
break;
}
// Add control byte with stuffing (reserved: FLAG, ESCAPE, XON, XOFF, SUB, CAN)
if (control == ASH_FLAG_BYTE || control == ASH_ESCAPE_BYTE || control == 0x11 || control == 0x13 || control == 0x18 ||
control == 0x1A) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = control ^ ASH_XOR_BYTE;
} else {
output[pos++] = control;
}
// Add data payload with stuffing
for (size_t i = 0; i < length; i++) {
uint8_t byte = data[i];
if (byte == ASH_FLAG_BYTE || byte == ASH_ESCAPE_BYTE || byte == 0x11 || byte == 0x13 || byte == 0x18 ||
byte == 0x1A) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = byte ^ ASH_XOR_BYTE;
} else {
output[pos++] = byte;
}
}
// Calculate CRC incrementally over control byte then data (avoids a MAX_ASH_FRAME_SIZE stack copy)
uint16_t crc = this->calculate_crc_(&control, 1);
if (length > 0) {
crc = this->calculate_crc_(data, length, crc);
}
// Add CRC with stuffing (big-endian)
uint8_t crc_high = (crc >> 8) & 0xFF;
uint8_t crc_low = crc & 0xFF;
if (crc_high == ASH_FLAG_BYTE || crc_high == ASH_ESCAPE_BYTE || crc_high == 0x11 || crc_high == 0x13 ||
crc_high == 0x18 || crc_high == 0x1A) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = crc_high ^ ASH_XOR_BYTE;
} else {
output[pos++] = crc_high;
}
if (crc_low == ASH_FLAG_BYTE || crc_low == ASH_ESCAPE_BYTE || crc_low == 0x11 || crc_low == 0x13 || crc_low == 0x18 ||
crc_low == 0x1A) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = crc_low ^ ASH_XOR_BYTE;
} else {
output[pos++] = crc_low;
}
// End with FLAG
output[pos++] = ASH_FLAG_BYTE;
return pos;
}
} // namespace esphome::zigbee_proxy
#endif // USE_ZIGBEE_PROXY
@@ -0,0 +1,88 @@
#pragma once
#include <cstdint>
#include <cstddef>
namespace esphome::zigbee_proxy {
// ASH Protocol Constants
static constexpr uint8_t ASH_FLAG_BYTE = 0x7E; // Frame delimiter
static constexpr uint8_t ASH_ESCAPE_BYTE = 0x7D; // Escape/substitution byte
static constexpr uint8_t ASH_XOR_BYTE = 0x20; // XOR mask for escaped bytes
static constexpr uint8_t ASH_SUBSTITUTE_BYTE = 0x18; // Substitution for invalid bytes
// Reserved bytes that must be escaped
static constexpr uint8_t ASH_RESERVED_BYTES[] = {0x7E, 0x7D, 0x11, 0x13, 0x93, 0xA3};
// Buffer size configuration
#ifdef ZIGBEE_PROXY_BUFFER_SIZE
static constexpr size_t MAX_ASH_FRAME_SIZE = ZIGBEE_PROXY_BUFFER_SIZE;
#else
#ifdef USE_ESP8266
static constexpr size_t MAX_ASH_FRAME_SIZE = 512; // Limited RAM on ESP8266
#else
static constexpr size_t MAX_ASH_FRAME_SIZE = 1024; // Full buffer on ESP32/RP2040
#endif
#endif
// Protocol limits
static constexpr uint8_t ASH_MAX_SEQUENCE = 7; // 3-bit sequence number (0-7)
static constexpr uint8_t ASH_TX_WINDOW_SIZE = 1; // Only 1 unacknowledged frame allowed
static constexpr uint8_t ASH_MAX_RETRIES = 5; // Maximum retransmission attempts
static constexpr uint16_t ASH_CRC_INIT = 0xFFFF; // CRC-CCITT initial value
static constexpr uint32_t ASH_RESET_TIMEOUT = 3000; // RST/RSTACK timeout in milliseconds
// IEEE address size
static constexpr size_t ZIGBEE_IEEE_ADDR_SIZE = 8; // 64-bit IEEE address
// ASH Frame Types (encoded in control byte)
// DATA format: 0ffrPPPP - bit 7=0, bits 6-4=frmNum, bit 3=reTx, bits 2-0=ackNum
// ACK/NAK format: 10XnrPPP - bit 5 distinguishes ACK(0) from NAK(1)
enum class AshFrameType : uint8_t {
DATA = 0x00, // Data frame (bit 7 = 0)
ACK = 0x80, // Acknowledge frame (100nrPPP, bit 5 = 0)
NAK = 0xA0, // Negative acknowledge (101nrPPP, bit 5 = 1)
RST = 0xC0, // Reset request (bits 7-6 = 11, bits 2-0 = 000)
RSTACK = 0xC1, // Reset acknowledgment (bits 7-6 = 11, bits 2-0 = 001)
ERROR = 0xC2, // Error indication (bits 7-6 = 11, bits 2-0 = 010)
};
// ASH Connection State
enum class AshState : uint8_t {
DISCONNECTED, // Initial state, no connection
CONNECTING, // Sent RST, waiting for RSTACK
CONNECTED, // Normal operation
FAILED, // Too many errors/timeouts, requires reset
};
// Frame Parsing State Machine
enum class ParsingState : uint8_t {
WAIT_FLAG_START, // Looking for frame start FLAG (0x7E)
WAIT_CONTROL, // Reading control byte
WAIT_DATA, // Reading data payload
WAIT_CRC_HIGH, // Reading CRC high byte
WAIT_CRC_LOW, // Reading CRC low byte
WAIT_FLAG_END, // Expecting end FLAG (0x7E)
};
// Bootloader detection states
enum class BootloaderState : uint8_t {
NORMAL, // Normal operation
DETECTED, // Bootloader mode detected
MENU, // In bootloader menu
};
// EZSP Error Codes (from ERROR frame)
enum class EzspError : uint8_t {
VERSION_NOT_SET = 0x00,
RESET_UNKNOWN = 0x01,
RESET_EXTERNAL = 0x02,
RESET_POWER_ON = 0x03,
RESET_WATCHDOG = 0x04,
RESET_ASSERT = 0x05,
RESET_BOOTLOADER = 0x06,
RESET_SOFTWARE = 0x07,
EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT = 0x51,
};
} // namespace esphome::zigbee_proxy
@@ -0,0 +1,56 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::zigbee_proxy {
// EZSP Protocol Versions
static constexpr uint8_t EZSP_MIN_VERSION = 8; // Minimum supported version
static constexpr uint8_t EZSP_MAX_VERSION = 13; // Maximum version we request
// EZSP Frame Control bits
static constexpr uint8_t EZSP_FRAME_CONTROL_COMMAND = 0x00; // Host to NCP
static constexpr uint8_t EZSP_FRAME_CONTROL_RESPONSE = 0x80; // NCP to Host
static constexpr uint8_t EZSP_FRAME_CONTROL_CALLBACK = 0x90; // Async callback from NCP
// Legacy EZSP frame format (v4-v7): [sequence] [frame_control] [frame_id]
// Extended EZSP frame format (v8+): [sequence] [frame_control_low] [frame_control_high] [frame_id_low] [frame_id_high]
// EZSP Frame IDs - Commands (host to NCP)
static constexpr uint16_t EZSP_VERSION = 0x0000; // Version negotiation
static constexpr uint16_t EZSP_NETWORK_INIT = 0x0017; // Initialize network
static constexpr uint16_t EZSP_NETWORK_STATE = 0x0018; // Get network state
static constexpr uint16_t EZSP_GET_EUI64 = 0x0026; // Get IEEE address
static constexpr uint16_t EZSP_GET_NETWORK_PARAMETERS = 0x0028; // Get network parameters
// EZSP Frame IDs - Callbacks (NCP to host, async)
static constexpr uint16_t EZSP_STACK_STATUS_HANDLER = 0x0019; // Stack status callback
// EZSP Network Status
enum class EzspNetworkStatus : uint8_t {
NO_NETWORK = 0x00,
JOINING_NETWORK = 0x01,
JOINED_NETWORK = 0x02,
JOINED_NETWORK_NO_PARENT = 0x03,
LEAVING_NETWORK = 0x04,
};
// Ember Status codes (subset)
enum class EmberStatus : uint8_t {
SUCCESS = 0x00,
NETWORK_UP = 0x90,
NETWORK_DOWN = 0x91,
NOT_JOINED = 0x93,
};
// Network parameters structure offsets (in getNetworkParameters response)
// Response format: [status] [nodeType] [parameters...]
// Parameters: [extendedPanId (8)] [panId (2)] [radioTxPower] [radioChannel] [joinMethod] ...
static constexpr size_t NETWORK_PARAMS_STATUS_OFFSET = 0;
static constexpr size_t NETWORK_PARAMS_NODE_TYPE_OFFSET = 1;
static constexpr size_t NETWORK_PARAMS_EXT_PAN_ID_OFFSET = 2;
static constexpr size_t NETWORK_PARAMS_PAN_ID_OFFSET = 10;
static constexpr size_t NETWORK_PARAMS_CHANNEL_OFFSET = 13;
} // namespace esphome::zigbee_proxy
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZIGBEE_PROXY
#include "esphome/components/api/api_connection.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/uart/uart.h"
#include "ash_protocol.h"
#include <array>
// Forward-declare USBUartChannel so the set_usb_uart_channel() setter can be declared
// without pulling usb_uart.h into every translation unit that includes this header.
// USE_ZIGBEE_PROXY_USB_UART is defined by the Python to_code() only when usb_uart_id
// is present in the YAML, ensuring the header is actually in the build path.
#ifdef USE_ZIGBEE_PROXY_USB_UART
namespace esphome::usb_uart {
class USBUartChannel;
}
#endif
namespace esphome::zigbee_proxy {
// Timeout configuration structure
struct TimeoutConfig {
uint32_t initial_timeout_ms{1600}; // Initial ACK timeout
uint32_t min_timeout_ms{400}; // Minimum adaptive timeout
uint32_t max_timeout_ms{3200}; // Maximum adaptive timeout
uint32_t current_timeout_ms{1600}; // Current adaptive timeout
};
// Network information structure
struct NetworkInfo {
std::array<uint8_t, ZIGBEE_IEEE_ADDR_SIZE> ieee_address{};
uint16_t pan_id{0};
std::array<uint8_t, 8> extended_pan_id{};
uint8_t channel{0};
bool valid{false};
};
enum ZigbeeProxyFeature : uint32_t {
FEATURE_ZIGBEE_PROXY_ENABLED = 1 << 0,
};
// Boot-time initialization state machine
enum class BootState : uint8_t {
IDLE, // Not initializing
WAIT_RSTACK, // Sent RST, waiting for RSTACK
SEND_VERSION, // Send EZSP version command
WAIT_VERSION, // Waiting for version response
SEND_NETWORK_INIT, // Send networkInit command
WAIT_STACK_STATUS, // Waiting for stackStatusHandler callback
SEND_GET_NETWORK_PARAMS, // Send getNetworkParameters command
WAIT_NETWORK_PARAMS, // Waiting for network parameters response
SEND_FINAL_RST, // Send final RST to reset NCP
WAIT_FINAL_RSTACK, // Waiting for final RSTACK
COMPLETE, // Boot sequence complete
FAILED, // Boot sequence failed
};
class ZigbeeProxy : public uart::UARTDevice, public Component {
public:
ZigbeeProxy();
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override;
bool can_proceed() override;
// API integration
void api_connection_authenticated(api::APIConnection *conn);
void zigbee_proxy_request(api::APIConnection *api_connection, const api::ZigbeeProxyRequest &msg);
void zigbee_proxy_frame(api::APIConnection *api_connection, const api::ZigbeeProxyFrame &msg);
api::APIConnection *get_api_connection() { return this->api_connection_; }
// Feature flags
uint32_t get_feature_flags() const { return ZigbeeProxyFeature::FEATURE_ZIGBEE_PROXY_ENABLED; }
// Network information accessors
const NetworkInfo &get_network_info() const { return this->network_info_; }
uint64_t get_ieee_address() const;
// Frame sending (from API client to NCP)
void send_frame(const uint8_t *data, size_t length);
// Timeout configuration (callable from Python/API)
void set_timeout_config(uint32_t initial_ms, uint32_t min_ms, uint32_t max_ms);
void set_initial_timeout(uint32_t timeout_ms) { this->timeout_config_.initial_timeout_ms = timeout_ms; }
void set_min_timeout(uint32_t timeout_ms) { this->timeout_config_.min_timeout_ms = timeout_ms; }
void set_max_timeout(uint32_t timeout_ms) { this->timeout_config_.max_timeout_ms = timeout_ms; }
#ifdef USE_ZIGBEE_PROXY_USB_UART
/// Called from generated code when usb_uart_id is configured.
/// Registers an RX callback on the channel so incoming bytes are processed
/// immediately in the same USBUartComponent::loop() iteration they arrive,
/// without waiting for the next ZigbeeProxy::loop() call.
void set_usb_uart_channel(usb_uart::USBUartChannel *channel);
#endif
protected:
// ASH Protocol State Machine
void reset_ash_protocol_();
void send_rst_frame_();
void handle_rstack_frame_(const uint8_t *data, size_t length);
void handle_error_frame_(const uint8_t *data, size_t length);
bool send_ack_frame_(uint8_t ack_num);
bool send_nak_frame_(uint8_t ack_num);
bool send_data_frame_(const uint8_t *data, size_t length, bool retransmit = false);
// Frame parsing and building (implemented in ash_protocol.cpp)
bool parse_byte_(uint8_t byte);
void parse_control_byte_(uint8_t control);
bool validate_frame_crc_();
size_t build_frame_(uint8_t *output, const uint8_t *data, size_t length, AshFrameType type, uint8_t frame_num = 0,
uint8_t ack_num = 0, bool retx = false);
uint16_t calculate_crc_(const uint8_t *data, size_t length, uint16_t init = ASH_CRC_INIT);
// Sequence number management
void increment_tx_sequence_() { this->tx_sequence_ = (this->tx_sequence_ + 1) & ASH_MAX_SEQUENCE; }
void increment_rx_sequence_() { this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE; }
// Timeout management
void update_adaptive_timeout_(uint32_t measured_rtt_ms);
void start_ack_timer_() { this->ack_timer_start_ = millis(); }
bool check_ack_timeout_();
// Retransmission
void handle_retransmission_();
void clear_tx_buffer_() {
this->tx_buffer_pending_ = false;
this->tx_retry_count_ = 0;
}
// Boot-time NCP initialization
void start_boot_sequence_();
void advance_boot_state_();
void handle_boot_data_frame_(const uint8_t *data, size_t length);
void send_ezsp_version_();
void send_network_init_();
void send_get_network_params_();
void handle_version_response_(const uint8_t *data, size_t length);
void handle_stack_status_(const uint8_t *data, size_t length);
void handle_network_params_response_(const uint8_t *data, size_t length);
// IEEE address and network info
bool set_ieee_address_(const uint8_t *new_address);
void send_network_info_changed_msg_(api::APIConnection *conn = nullptr);
// WiFi/Zigbee channel conflict detection
void check_wifi_zigbee_conflict_();
// Bootloader detection
void check_bootloader_mode_(const uint8_t *data, size_t length);
// UART processing
void process_uart_();
// Client-side (left) ASH session
void client_parse_byte_(uint8_t byte);
void client_parse_control_byte_(uint8_t control);
bool client_validate_frame_crc_();
void client_send_ack_frame_(uint8_t ack_num);
void client_send_rstack_frame_(uint8_t reset_code);
void client_send_data_frame_(const uint8_t *data, size_t length);
void client_send_error_frame_(uint8_t error_code);
void client_send_raw_frame_(const uint8_t *frame, size_t length);
void client_reset_session_();
// Send raw bytes to API client
void send_to_client_(const uint8_t *data, size_t length);
// Forward NCP frames to client
void forward_ncp_data_to_client_(const uint8_t *payload, size_t length);
void forward_ncp_rstack_to_client_(const uint8_t *data, size_t length);
void forward_ncp_error_to_client_(const uint8_t *data, size_t length);
// Pre-allocated message - always ready to send
api::ZigbeeProxyFrame outgoing_proto_msg_;
// NCP-side (right) ASH buffers
std::array<uint8_t, MAX_ASH_FRAME_SIZE> rx_buffer_;
std::array<uint8_t, MAX_ASH_FRAME_SIZE> tx_buffer_;
std::array<uint8_t, MAX_ASH_FRAME_SIZE> tx_pending_buffer_; // For retransmission
// Client-side (left) ASH buffers
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_rx_buffer_;
std::array<uint8_t, MAX_ASH_FRAME_SIZE> client_tx_buffer_;
// Network information
NetworkInfo network_info_;
// Timeout configuration
TimeoutConfig timeout_config_;
// Pointers (aligned together)
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
// NCP-side (right) 32-bit values
uint32_t setup_time_{0}; // Time when last RST frame was sent
uint32_t boot_start_time_{0}; // Time when the boot sequence began (for overall timeout)
uint32_t ack_timer_start_{0}; // Time when ACK timer started
uint32_t last_rtt_ms_{0}; // Last measured round-trip time
// NCP-side (right) 16-bit values
uint16_t rx_buffer_index_{0}; // Index for populating rx_buffer_
uint16_t tx_pending_length_{0}; // Length of pending TX frame for retransmission
uint16_t calculated_crc_{0}; // CRC calculated during frame reception
// Client-side (left) 16-bit values
uint16_t client_rx_buffer_index_{0};
// NCP-side (right) 8-bit values
uint8_t tx_sequence_{0}; // TX sequence number (0-7)
uint8_t rx_sequence_{0}; // RX sequence number (0-7)
uint8_t tx_retry_count_{0}; // Number of retransmission attempts
uint8_t tx_pending_frame_num_{0}; // Frame number of pending TX frame
uint8_t last_ack_sent_{0}; // Last ACK number sent
// Client-side (left) 8-bit values
uint8_t client_tx_sequence_{0}; // Client-facing TX sequence (proxy → client)
uint8_t client_rx_sequence_{0}; // Client-facing RX sequence (client → proxy)
// NCP-side enums and booleans
AshState ash_state_{AshState::DISCONNECTED};
ParsingState parsing_state_{ParsingState::WAIT_FLAG_START};
BootloaderState bootloader_state_{BootloaderState::NORMAL};
BootState boot_state_{BootState::IDLE};
// Client-side enums and booleans
AshState client_ash_state_{AshState::DISCONNECTED};
ParsingState client_parsing_state_{ParsingState::WAIT_FLAG_START};
uint8_t ezsp_version_{0}; // NCP's EZSP protocol version
uint8_t ezsp_sequence_{0}; // EZSP frame sequence number
uint8_t ezsp_requested_version_{0}; // Version we last requested (for re-negotiation)
bool tx_buffer_pending_{false}; // True if waiting for ACK from NCP
bool escape_next_byte_{false}; // True if next NCP byte should be unescaped
bool client_escape_next_byte_{false}; // True if next client byte should be unescaped
bool network_info_ready_{false}; // True when network info retrieved
bool boot_sequence_active_{false}; // True during boot-time init
};
extern ZigbeeProxy *global_zigbee_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::zigbee_proxy
#endif // USE_ZIGBEE_PROXY
+4 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.3.0b1"
__version__ = "2026.4.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@@ -1235,6 +1235,7 @@ UNIT_LITRE = "L"
UNIT_LUX = "lx"
UNIT_MEGAJOULE = "MJ"
UNIT_METER = "m"
UNIT_METER_PER_SECOND = "m/s"
UNIT_METER_PER_SECOND_SQUARED = "m/s²"
UNIT_MICROAMP = "µA"
UNIT_MICROGRAMS_PER_CUBIC_METER = "µg/m³"
@@ -1244,6 +1245,7 @@ UNIT_MICROSILVERTS_PER_HOUR = "µSv/h"
UNIT_MICROTESLA = "µT"
UNIT_MILLIAMP = "mA"
UNIT_MILLIGRAMS_PER_CUBIC_METER = "mg/m³"
UNIT_MILLILITRE = "mL"
UNIT_MILLIMETER = "mm"
UNIT_MILLISECOND = "ms"
UNIT_MILLISIEMENS_PER_CENTIMETER = "mS/cm"
@@ -1255,6 +1257,7 @@ UNIT_PARTS_PER_MILLION = "ppm"
UNIT_PASCAL = "Pa"
UNIT_PERCENT = "%"
UNIT_PH = "pH"
UNIT_POUND = "lb"
UNIT_PULSES = "pulses"
UNIT_PULSES_PER_MINUTE = "pulses/min"
UNIT_REVOLUTIONS_PER_MINUTE = "RPM"
+3
View File
@@ -589,7 +589,10 @@ async def _add_looping_components() -> None:
async def to_code(config: ConfigType) -> None:
cg.add_global(cg.global_ns.namespace("esphome").using)
# These can be used by user lambdas, put them to default scope
# picolibc (IDF 6.0+) declares isnan in global scope, conflicting with using std::isnan
cg.add_global(cg.RawStatement("#ifndef __PICOLIBC__"))
cg.add_global(cg.RawExpression("using std::isnan"))
cg.add_global(cg.RawStatement("#endif"))
cg.add_global(cg.RawExpression("using std::min"))
cg.add_global(cg.RawExpression("using std::max"))
+4
View File
@@ -138,6 +138,8 @@
#define USE_VALVE
#define USE_WATER_HEATER
#define USE_WATER_HEATER_VISUAL_OVERRIDES
#define USE_ZIGBEE_PROXY
#define USE_ZIGBEE_PROXY_USB_UART
#define USE_ZWAVE_PROXY
// Feature flags which do not work for zephyr
@@ -195,6 +197,7 @@
// ESP32-specific feature flags
#ifdef USE_ESP32
#define USE_ESP32_CRASH_HANDLER
#define USE_MQTT_IDF_ENQUEUE
#define USE_ESPHOME_TASK_LOG_BUFFER
#define USE_OTA_ROLLBACK
@@ -337,6 +340,7 @@
#ifdef USE_RP2040
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0)
#define USE_LOOP_PRIORITY
#define USE_RP2040_CRASH_HANDLER
#define USE_HTTP_REQUEST_RESPONSE
#define USE_I2C
#define USE_LOGGER_USB_CDC
+152 -1
View File
@@ -301,7 +301,7 @@ template<typename T, size_t N> class StaticVector {
/// Not thread-safe. All access (push/pop/iteration) must occur from a single
/// context, or the caller must provide external synchronization.
template<typename T, size_t N> class StaticRingBuffer {
using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>;
using index_type = std::conditional_t<(N <= std::numeric_limits<uint8_t>::max()), uint8_t, uint16_t>;
public:
class Iterator {
@@ -356,6 +356,13 @@ template<typename T, size_t N> class StaticRingBuffer {
index_type size() const { return this->count_; }
bool empty() const { return this->count_ == 0; }
/// Clear all elements (reset to empty)
void clear() {
this->head_ = 0;
this->tail_ = 0;
this->count_ = 0;
}
Iterator begin() { return Iterator(this, 0); }
Iterator end() { return Iterator(this, this->count_); }
ConstIterator begin() const { return ConstIterator(this, 0); }
@@ -368,6 +375,128 @@ template<typename T, size_t N> class StaticRingBuffer {
index_type count_{0};
};
/// Fixed-capacity circular buffer - allocates once at runtime, never reallocates.
/// Runtime-sized equivalent of StaticRingBuffer - use when capacity is only known at initialization.
/// Supports FIFO push/pop and iteration over queued elements.
/// Not thread-safe.
template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()> class FixedRingBuffer {
using index_type = std::conditional_t<
(MAX_CAPACITY <= std::numeric_limits<uint8_t>::max()), uint8_t,
std::conditional_t<(MAX_CAPACITY <= std::numeric_limits<uint16_t>::max()), uint16_t, uint32_t>>;
public:
class Iterator {
public:
Iterator(FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
T &operator*() { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; }
Iterator &operator++() {
++pos_;
return *this;
}
bool operator!=(const Iterator &other) const { return pos_ != other.pos_; }
private:
FixedRingBuffer *buf_;
index_type pos_;
};
class ConstIterator {
public:
ConstIterator(const FixedRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % buf_->capacity_]; }
ConstIterator &operator++() {
++pos_;
return *this;
}
bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; }
private:
const FixedRingBuffer *buf_;
index_type pos_;
};
FixedRingBuffer() = default;
~FixedRingBuffer() {
if constexpr (std::is_trivial<T>::value) {
::operator delete(this->data_);
} else {
delete[] this->data_;
}
}
// Disable copy
FixedRingBuffer(const FixedRingBuffer &) = delete;
FixedRingBuffer &operator=(const FixedRingBuffer &) = delete;
/// Allocate capacity - can only be called once
void init(index_type capacity) {
if constexpr (std::is_trivial<T>::value) {
// Raw allocation without initialization (elements are written before read)
// NOLINTNEXTLINE(bugprone-sizeof-expression)
this->data_ = static_cast<T *>(::operator new(capacity * sizeof(T)));
} else {
this->data_ = new T[capacity];
}
this->capacity_ = capacity;
}
/// Push a value. Returns false if full.
bool push(const T &value) {
if (this->count_ >= this->capacity_)
return false;
this->data_[this->tail_] = value;
this->tail_ = (this->tail_ + 1) % this->capacity_;
++this->count_;
return true;
}
/// Push a value, overwriting the oldest if full.
void push_overwrite(const T &value) {
this->data_[this->tail_] = value;
this->tail_ = (this->tail_ + 1) % this->capacity_;
if (this->count_ >= this->capacity_) {
// Buffer full - advance head to drop oldest, count stays at capacity
this->head_ = this->tail_;
} else {
++this->count_;
}
}
/// Remove the oldest element.
void pop() {
if (this->count_ > 0) {
this->head_ = (this->head_ + 1) % this->capacity_;
--this->count_;
}
}
T &front() { return this->data_[this->head_]; }
const T &front() const { return this->data_[this->head_]; }
index_type size() const { return this->count_; }
bool empty() const { return this->count_ == 0; }
index_type capacity() const { return this->capacity_; }
bool full() const { return this->count_ == this->capacity_; }
/// Clear all elements (reset to empty, keep capacity)
void clear() {
this->head_ = 0;
this->tail_ = 0;
this->count_ = 0;
}
Iterator begin() { return Iterator(this, 0); }
Iterator end() { return Iterator(this, this->count_); }
ConstIterator begin() const { return ConstIterator(this, 0); }
ConstIterator end() const { return ConstIterator(this, this->count_); }
protected:
T *data_{nullptr};
index_type head_{0};
index_type tail_{0};
index_type count_{0};
index_type capacity_{0};
};
/// Fixed-capacity vector - allocates once at runtime, never reallocates
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
/// when size is known at initialization but not at compile time
@@ -942,6 +1071,28 @@ __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).
/// More efficient than buf_append_printf for plain string literals.
/// @param buf Output buffer
/// @param size Total buffer size
/// @param pos Current position in buffer
/// @param str String to append (must not be null)
/// @return New position after appending (capped at size on overflow)
inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str) {
if (pos >= size) {
return size;
}
size_t remaining = size - pos - 1; // reserve space for null terminator
size_t len = strlen(str);
if (len > remaining) {
len = remaining;
}
memcpy(buf + pos, str, len);
pos += len;
buf[pos] = '\0';
return pos;
}
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
/// This avoids multiple heap allocations during string construction.
/// Maximum name length supported is 120 characters for friendly names.

Some files were not shown because too many files have changed in this diff Show More