mirror of
https://github.com/esphome/esphome.git
synced 2026-06-27 08:03:42 +00:00
Compare commits
76 Commits
20260218-z
...
2026.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
609003c897 | ||
|
|
2c10adba85 | ||
|
|
9e4e2d78dc | ||
|
|
af9366fdd4 | ||
|
|
448402ca2c | ||
|
|
fc67551edc | ||
|
|
98d3dce672 | ||
|
|
4cb93d4df8 | ||
|
|
91e66cfd9d | ||
|
|
6cf32af33f | ||
|
|
6b9be033d6 | ||
|
|
5cc03d9bef | ||
|
|
0fa96b6e1e | ||
|
|
be2e4a5278 | ||
|
|
80bd6489cf | ||
|
|
ccf672d7ee | ||
|
|
6154b673c2 | ||
|
|
3bde7ec978 | ||
|
|
8caa11dcf4 | ||
|
|
1b70df2c1f | ||
|
|
4122fa5ddd | ||
|
|
c5d42b0569 | ||
|
|
37f9541f32 | ||
|
|
8bbfadb59a | ||
|
|
a40d97f346 | ||
|
|
d6c67d5c35 | ||
|
|
0816b27398 | ||
|
|
9133582aa0 | ||
|
|
f36b0fcb61 | ||
|
|
bb0a5dc8a8 | ||
|
|
0c260e483e | ||
|
|
b8ce907976 | ||
|
|
ffce637ea5 | ||
|
|
d6fba39037 | ||
|
|
5d5c2723b2 | ||
|
|
06d1498c47 | ||
|
|
254e1f3abb | ||
|
|
deb6b97eea | ||
|
|
22ea2764d4 | ||
|
|
632dbc8fe8 | ||
|
|
98d9871620 | ||
|
|
a064eceb9b | ||
|
|
49107f2174 | ||
|
|
e9c2659147 | ||
|
|
18b54f075e | ||
|
|
45e40223ac | ||
|
|
1ab1534028 | ||
|
|
039efdb02a | ||
|
|
b0447dc521 | ||
|
|
aacbaab5f8 | ||
|
|
dc5032f72f | ||
|
|
c263c2c382 | ||
|
|
910784ca84 | ||
|
|
0b99e8f08d | ||
|
|
93be539789 | ||
|
|
390bb0451f | ||
|
|
14c3e2d9d9 | ||
|
|
23c7e0f803 | ||
|
|
cb4d1d1b5e | ||
|
|
2ba807efe8 | ||
|
|
c8cf9b74b1 | ||
|
|
33475703da | ||
|
|
1b7d0f9c0b | ||
|
|
1d881ef6f4 | ||
|
|
3a838d897f | ||
|
|
da130c900f | ||
|
|
440734dadf | ||
|
|
df2ddc47ec | ||
|
|
4b1c4ba5c0 | ||
|
|
6002badb3c | ||
|
|
e8f51fec88 | ||
|
|
7cec2d3029 | ||
|
|
2b0c471ed7 | ||
|
|
064bd13ebb | ||
|
|
2627490a11 | ||
|
|
4219d6d367 |
@@ -1 +1 @@
|
||||
e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e
|
||||
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.3.0b1
|
||||
PROJECT_NUMBER = 2026.3.0
|
||||
|
||||
# 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;
|
||||
|
||||
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
|
||||
uint8_t current_sensor_;
|
||||
// The AM43 often gets into a state where it spams loads of battery update
|
||||
// notifications. Here we will limit to no more than every 10s.
|
||||
uint8_t last_battery_update_;
|
||||
uint32_t last_battery_update_;
|
||||
};
|
||||
|
||||
} // namespace am43
|
||||
|
||||
@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
|
||||
|
||||
uint8_t buf[128];
|
||||
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
|
||||
// The ESP's i2c driver has a limited buffer size.
|
||||
// This way of retrieving the data should be wrong according to the datasheet
|
||||
// but it seems to work.
|
||||
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
|
||||
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
|
||||
// and advances its internal pointer after every 4th byte.
|
||||
uint8_t read = std::min(32, fifo_level * 4 - pos);
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
}
|
||||
|
||||
if (millis() - this->gesture_start_ > 500) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -235,6 +241,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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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("'");
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -442,8 +442,12 @@ class ProtoMessage {
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
|
||||
#ifndef USE_HOST
|
||||
protected:
|
||||
#endif
|
||||
// Non-virtual destructor is protected to prevent polymorphic deletion.
|
||||
// On host platform, made public to allow value-initialization of std::array
|
||||
// members (e.g. DeviceInfoResponse::devices) without clang errors.
|
||||
~ProtoMessage() = default;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
|
||||
INT_MASK = 0xF0,
|
||||
THRESH_MASK = 0x0F,
|
||||
R_SPIKE_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xE0,
|
||||
CAP_MASK = 0xF0,
|
||||
LIGHT_MASK = 0xCF,
|
||||
DISTURB_MASK = 0xDF,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) {
|
||||
|
||||
// First, use the peek buffer if available
|
||||
if (this->has_peek_) {
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
|
||||
#endif
|
||||
data[0] = this->peek_buffer_;
|
||||
this->has_peek_ = false;
|
||||
data++;
|
||||
if (--len == 0) { // Decrement len first, then check it...
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
|
||||
#endif
|
||||
return true; // No more to read
|
||||
return true; // No more to read
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,8 +186,8 @@ async def to_code_base(config):
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
"1.3.40408",
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library",
|
||||
None,
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
|
||||
float DallasTemperatureSensor::get_temp_c_() {
|
||||
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
|
||||
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
|
||||
if (this->scratch_pad_[7] == 0) {
|
||||
return NAN;
|
||||
}
|
||||
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
|
||||
}
|
||||
switch (this->resolution_) {
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace debug {
|
||||
|
||||
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
|
||||
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
|
||||
static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128;
|
||||
|
||||
// buf_append_printf is now provided by esphome/core/helpers.h
|
||||
|
||||
@@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent {
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer);
|
||||
uint32_t get_free_heap_();
|
||||
size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos);
|
||||
void update_platform_();
|
||||
|
||||
@@ -98,7 +98,7 @@ static const char *const WAKEUP_CAUSES[] = {
|
||||
"BT",
|
||||
};
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
const char *wake_reason;
|
||||
unsigned reason = esp_sleep_get_wakeup_cause();
|
||||
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
|
||||
@@ -196,9 +196,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
|
||||
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
|
||||
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
char reset_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer));
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer));
|
||||
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
|
||||
@@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
// ESP8266 doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace debug {
|
||||
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return lt_get_reboot_reason_name(lt_get_reboot_reason());
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
#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
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); }
|
||||
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, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
// Zephyr doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ void EE895Component::setup() {
|
||||
this->read(serial_number, 20);
|
||||
|
||||
crc16_check = (serial_number[19] << 8) + serial_number[18];
|
||||
if (crc16_check != calc_crc16_(serial_number, 19)) {
|
||||
if (crc16_check != calc_crc16_(serial_number, 18)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
|
||||
address[2] = addr & 0xFF;
|
||||
address[3] = (reg_cnt >> 8) & 0xFF;
|
||||
address[4] = reg_cnt & 0xFF;
|
||||
crc16 = calc_crc16_(address, 6);
|
||||
crc16 = calc_crc16_(address, 5);
|
||||
address[5] = crc16 & 0xFF;
|
||||
address[6] = (crc16 >> 8) & 0xFF;
|
||||
this->write(address, 7);
|
||||
@@ -95,7 +95,7 @@ float EE895Component::read_float_() {
|
||||
uint8_t i2c_response[8];
|
||||
this->read(i2c_response, 8);
|
||||
crc16_check = (i2c_response[7] << 8) + i2c_response[6];
|
||||
if (crc16_check != calc_crc16_(i2c_response, 7)) {
|
||||
if (crc16_check != calc_crc16_(i2c_response, 6)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
this->status_set_warning();
|
||||
return 0;
|
||||
@@ -107,12 +107,9 @@ float EE895Component::read_float_() {
|
||||
}
|
||||
|
||||
uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
|
||||
uint8_t crc_check_buf[22];
|
||||
for (int i = 0; i < len; i++) {
|
||||
crc_check_buf[i + 1] = buf[i];
|
||||
}
|
||||
crc_check_buf[0] = this->address_;
|
||||
return crc16(crc_check_buf, len);
|
||||
uint8_t addr = this->address_;
|
||||
uint16_t crc = crc16(&addr, 1);
|
||||
return crc16(buf, len, crc);
|
||||
}
|
||||
} // namespace ee895
|
||||
} // namespace esphome
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
esphome/components/esp32/crash_handler.cpp
Normal file
355
esphome/components/esp32/crash_handler.cpp
Normal 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
esphome/components/esp32/crash_handler.h
Normal file
18
esphome/components/esp32/crash_handler.h
Normal 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
|
||||
@@ -575,8 +575,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
|
||||
load_ble_event(event, args...);
|
||||
|
||||
// Push the event to the queue
|
||||
// Push always succeeds: pool is sized to queue capacity (N-1), so if
|
||||
// allocate() returned non-null, the queue is guaranteed to have room.
|
||||
global_ble->ble_events_.push(event);
|
||||
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
// Explicit template instantiations for the friend function
|
||||
|
||||
@@ -221,7 +221,13 @@ class ESP32BLE : public Component {
|
||||
|
||||
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
|
||||
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
||||
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements (one slot distinguishes full from empty).
|
||||
// This guarantees allocate() returns nullptr before push() can fail, which:
|
||||
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
|
||||
// 2. Avoids needing release() on the producer path after a failed push(),
|
||||
// preserving the SPSC contract on the pool's internal free list
|
||||
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_;
|
||||
|
||||
// 4-byte aligned members
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() {
|
||||
for (auto *descriptor : this->descriptors_) {
|
||||
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
vSemaphoreDelete(this->set_value_lock_);
|
||||
}
|
||||
|
||||
BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
|
||||
this->set_value_lock_ = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
|
||||
this->properties_ = (esp_gatt_char_prop_t) 0;
|
||||
|
||||
this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0);
|
||||
@@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
|
||||
|
||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
|
||||
xSemaphoreTake(this->set_value_lock_, 0L);
|
||||
this->value_ = std::move(buffer);
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
}
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
|
||||
|
||||
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
|
||||
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_server {
|
||||
@@ -84,8 +82,6 @@ class BLECharacteristic {
|
||||
|
||||
uint16_t value_read_offset_{0};
|
||||
std::vector<uint8_t> value_;
|
||||
SemaphoreHandle_t set_value_lock_;
|
||||
|
||||
std::vector<BLEDescriptor *> descriptors_;
|
||||
|
||||
struct ClientNotificationEntry {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -22,9 +22,7 @@ void Mutex::unlock() {}
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
|
||||
|
||||
// ESP8266 doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
// ESP8266 LwIPLock is defined inline as a no-op in helpers.h
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
wifi_get_macaddr(STATION_IF, mac);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
|
||||
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
@@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
|
||||
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
|
||||
@@ -163,10 +163,14 @@ class ESPNowComponent : public Component {
|
||||
|
||||
uint8_t own_address_[ESP_NOW_ETH_ALEN]{0};
|
||||
LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{};
|
||||
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{};
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing a pool slot leak.
|
||||
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> receive_packet_pool_{};
|
||||
|
||||
LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{};
|
||||
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{};
|
||||
// Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment.
|
||||
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{};
|
||||
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
|
||||
|
||||
uint8_t wifi_channel_{0};
|
||||
|
||||
@@ -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
|
||||
|
||||
10
esphome/components/ethernet/ethernet_helpers.c
Normal file
10
esphome/components/ethernet/ethernet_helpers.c
Normal file
@@ -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
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
CONF_OUTPUT_ID,
|
||||
CONF_RGB_ORDER,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
fastled_base_ns = cg.esphome_ns.namespace("fastled_base")
|
||||
@@ -41,5 +42,9 @@ async def new_fastled_light(config):
|
||||
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
|
||||
|
||||
cg.add_library("fastled/FastLED", "3.9.16")
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
|
||||
include_builtin_idf_component("esp_lcd")
|
||||
await light.register_light(var, config)
|
||||
return var
|
||||
|
||||
@@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
|
||||
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
|
||||
InterruptLock lock;
|
||||
uint64_t ret = 0;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
for (uint8_t i = 0; i < 64; i++) {
|
||||
ret |= (uint64_t(this->read_bit_()) << i);
|
||||
}
|
||||
return ret;
|
||||
|
||||
@@ -87,19 +87,12 @@ void GreeClimate::transmit_state() {
|
||||
// Calculate the checksum
|
||||
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
|
||||
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
|
||||
} else if (this->model_ == GREE_YAG) {
|
||||
} else {
|
||||
remote_state[7] =
|
||||
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4);
|
||||
} else {
|
||||
remote_state[7] =
|
||||
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
|
||||
((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) &
|
||||
0x0F)
|
||||
<< 4) |
|
||||
(remote_state[7] & 0x0F);
|
||||
}
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
|
||||
@@ -7,50 +7,36 @@ namespace hdc2010 {
|
||||
|
||||
static const char *const TAG = "hdc2010";
|
||||
|
||||
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
|
||||
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
|
||||
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
|
||||
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
|
||||
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
|
||||
static const uint8_t CONFIG = 0x0E;
|
||||
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
|
||||
// Register addresses
|
||||
static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00;
|
||||
static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01;
|
||||
static constexpr uint8_t REG_HUMIDITY_LOW = 0x02;
|
||||
static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03;
|
||||
static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E;
|
||||
static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F;
|
||||
|
||||
// REG_MEASUREMENT_CONF (0x0F) bit masks
|
||||
static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger
|
||||
static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode
|
||||
static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution
|
||||
static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution
|
||||
|
||||
// REG_RESET_DRDY_INT_CONF (0x0E) bit masks
|
||||
static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode
|
||||
|
||||
void HDC2010Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
const uint8_t data[2] = {
|
||||
0b00000000, // resolution 14bit for both humidity and temperature
|
||||
0b00000000 // reserved
|
||||
};
|
||||
|
||||
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
|
||||
ESP_LOGW(TAG, "Initial config instruction error");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
// Set measurement mode to temperature and humidity
|
||||
// Set 14-bit resolution for both sensors and measurement mode to temp + humidity
|
||||
uint8_t config_contents;
|
||||
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode
|
||||
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
|
||||
// Set rate to manual
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x8F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set temperature resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0x3F;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
|
||||
// Set humidity resolution to 14bit
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents &= 0xCF;
|
||||
this->write_bytes(CONFIG, &config_contents, 1);
|
||||
// Set auto measurement rate to manual (on-demand via MEAS_TRIG)
|
||||
this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
|
||||
config_contents &= ~AMM_MASK;
|
||||
this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
|
||||
}
|
||||
|
||||
void HDC2010Component::dump_config() {
|
||||
@@ -67,9 +53,9 @@ void HDC2010Component::dump_config() {
|
||||
void HDC2010Component::update() {
|
||||
// Trigger measurement
|
||||
uint8_t config_contents;
|
||||
this->read_register(CONFIG, &config_contents, 1);
|
||||
config_contents |= 0x01;
|
||||
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
config_contents |= MEAS_TRIG;
|
||||
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
|
||||
|
||||
// 1ms delay after triggering the sample
|
||||
set_timeout(1, [this]() {
|
||||
@@ -90,8 +76,8 @@ void HDC2010Component::update() {
|
||||
float HDC2010Component::read_temp() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
|
||||
this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1);
|
||||
this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t temp = encode_uint16(byte[1], byte[0]);
|
||||
return (float) temp * 0.0025177f - 40.0f;
|
||||
@@ -100,8 +86,8 @@ float HDC2010Component::read_temp() {
|
||||
float HDC2010Component::read_humidity() {
|
||||
uint8_t byte[2];
|
||||
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
|
||||
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
|
||||
this->read_register(REG_HUMIDITY_LOW, &byte[0], 1);
|
||||
this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1);
|
||||
|
||||
uint16_t humidity = encode_uint16(byte[1], byte[0]);
|
||||
return (float) humidity * 0.001525879f;
|
||||
|
||||
@@ -23,6 +23,12 @@ namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.update";
|
||||
|
||||
// Wraps UpdateInfo + error for the task→main-loop handoff.
|
||||
struct TaskResult {
|
||||
update::UpdateInfo info;
|
||||
const LogString *error_str{nullptr};
|
||||
};
|
||||
|
||||
static const size_t MAX_READ_SIZE = 256;
|
||||
static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
|
||||
@@ -77,134 +83,148 @@ void HttpRequestUpdate::update() {
|
||||
void HttpRequestUpdate::update_task(void *params) {
|
||||
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
|
||||
|
||||
// Allocate once — every path below returns via the single defer at the end.
|
||||
// On failure, error_str is set; on success it is nullptr.
|
||||
auto *result = new TaskResult();
|
||||
auto *info = &result->info;
|
||||
|
||||
auto container = this_update->request_parent_->get(this_update->source_url_);
|
||||
|
||||
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
|
||||
UPDATE_RETURN;
|
||||
if (container != nullptr)
|
||||
container->end();
|
||||
result->error_str = LOG_STR("Failed to fetch manifest");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *data = allocator.allocate(container->content_length);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer(
|
||||
[this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
{
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *data = allocator.allocate(container->content_length);
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
|
||||
container->end();
|
||||
result->error_str = LOG_STR("Failed to allocate memory for manifest");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
|
||||
this_update->request_parent_->get_timeout());
|
||||
if (read_result.status != HttpReadStatus::OK) {
|
||||
if (read_result.status == HttpReadStatus::TIMEOUT) {
|
||||
ESP_LOGE(TAG, "Timeout reading manifest");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
|
||||
}
|
||||
this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>();
|
||||
this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>();
|
||||
allocator.deallocate(data, container->content_length);
|
||||
container->end();
|
||||
result->error_str = LOG_STR("Failed to read manifest");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
|
||||
for (auto build : builds_array) {
|
||||
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
|
||||
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
|
||||
info->title = root[ESPHOME_F("name")].as<std::string>();
|
||||
info->latest_version = root[ESPHOME_F("version")].as<std::string>();
|
||||
|
||||
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
|
||||
for (auto build : builds_array) {
|
||||
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
|
||||
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
|
||||
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
|
||||
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
return false;
|
||||
}
|
||||
info->firmware_url = ota[ESPHOME_F("path")].as<std::string>();
|
||||
info->md5 = ota[ESPHOME_F("md5")].as<std::string>();
|
||||
|
||||
if (ota[ESPHOME_F("summary")].is<const char *>())
|
||||
info->summary = ota[ESPHOME_F("summary")].as<std::string>();
|
||||
if (ota[ESPHOME_F("release_url")].is<const char *>())
|
||||
info->release_url = ota[ESPHOME_F("release_url")].as<std::string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>();
|
||||
this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>();
|
||||
|
||||
if (ota[ESPHOME_F("summary")].is<const char *>())
|
||||
this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>();
|
||||
if (ota[ESPHOME_F("release_url")].is<const char *>())
|
||||
this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
// Merge source_url_ and this_update->update_info_.firmware_url
|
||||
if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
|
||||
std::string path = this_update->update_info_.firmware_url;
|
||||
if (path[0] == '/') {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
|
||||
this_update->update_info_.firmware_url = domain + path;
|
||||
} else {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
|
||||
this_update->update_info_.firmware_url = domain + path;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
result->error_str = LOG_STR("Failed to parse manifest JSON");
|
||||
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
// Merge source_url_ and firmware_url
|
||||
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
|
||||
std::string path = info->firmware_url;
|
||||
if (path[0] == '/') {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
|
||||
info->firmware_url = domain + path;
|
||||
} else {
|
||||
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
|
||||
info->firmware_url = domain + path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||
info->current_version = ESPHOME_PROJECT_VERSION;
|
||||
#else
|
||||
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||
info->current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
bool trigger_update_available = false;
|
||||
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
this_update->update_info_.latest_version == this_update->update_info_.current_version) {
|
||||
this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
|
||||
trigger_update_available = true;
|
||||
}
|
||||
this_update->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
}
|
||||
|
||||
// Defer to main loop to ensure thread-safe execution of:
|
||||
// - status_clear_error() performs non-atomic read-modify-write on component_state_
|
||||
// - publish_state() triggers API callbacks that write to the shared protobuf buffer
|
||||
// which can be corrupted if accessed concurrently from task and main loop threads
|
||||
// - update_available trigger to ensure consistent state when the trigger fires
|
||||
this_update->defer([this_update, trigger_update_available]() {
|
||||
this_update->update_info_.has_progress = false;
|
||||
this_update->update_info_.progress = 0.0f;
|
||||
defer:
|
||||
// Release container before vTaskDelete (which doesn't call destructors)
|
||||
container.reset();
|
||||
|
||||
// Defer to the main loop so all update_info_ and state_ writes happen on the
|
||||
// same thread as readers (API, MQTT, web server). This is a single defer for
|
||||
// both success and error paths to avoid multiple std::function instantiations.
|
||||
// Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
|
||||
this_update->defer([this_update, result]() {
|
||||
if (result->error_str != nullptr) {
|
||||
this_update->status_set_error(result->error_str);
|
||||
delete result;
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine new state on main loop (avoids extra lambda captures from task)
|
||||
bool trigger_update_available = false;
|
||||
update::UpdateState new_state;
|
||||
if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) {
|
||||
new_state = update::UPDATE_STATE_NO_UPDATE;
|
||||
} else {
|
||||
new_state = update::UPDATE_STATE_AVAILABLE;
|
||||
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
|
||||
trigger_update_available = true;
|
||||
}
|
||||
}
|
||||
|
||||
this_update->update_info_ = std::move(result->info);
|
||||
this_update->state_ = new_state;
|
||||
delete result; // Safe: moved-from state is valid for destruction
|
||||
|
||||
this_update->status_clear_error();
|
||||
this_update->publish_state();
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -26,9 +26,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
|
||||
|
||||
// LibreTiny doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
// LibreTiny LwIPLock is defined inline as a no-op in helpers.h
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
WiFi.macAddress(mac);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_height();
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() {
|
||||
}
|
||||
|
||||
point = buffer[5] & 0xF;
|
||||
if (point > 2) {
|
||||
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
|
||||
point = 2;
|
||||
}
|
||||
|
||||
if (point == 1) {
|
||||
err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -80,6 +80,7 @@ bool StreamingModel::load_model_() {
|
||||
TfLiteTensor *output = this->interpreter_->output(0);
|
||||
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
|
||||
ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (output->type != kTfLiteUInt8) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() {
|
||||
void MQTTBackendESP32::loop() {
|
||||
// process new events
|
||||
// handle only 1 message per loop iteration
|
||||
if (!mqtt_events_.empty()) {
|
||||
auto &event = mqtt_events_.front();
|
||||
mqtt_event_handler_(event);
|
||||
mqtt_events_.pop();
|
||||
Event *event = this->mqtt_event_queue_.pop();
|
||||
if (event != nullptr) {
|
||||
this->mqtt_event_handler_(*event);
|
||||
this->mqtt_event_pool_.release(event);
|
||||
}
|
||||
|
||||
// Log dropped inbound events (check is cheap - single atomic load in common case)
|
||||
uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count();
|
||||
if (inbound_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped);
|
||||
}
|
||||
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
@@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
|
||||
void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id,
|
||||
void *event_data) {
|
||||
MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args);
|
||||
// queue event to decouple processing
|
||||
// queue event to decouple processing from ESP-IDF MQTT task to main loop
|
||||
if (instance) {
|
||||
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
|
||||
instance->mqtt_events_.emplace(event);
|
||||
auto *event = instance->mqtt_event_pool_.allocate();
|
||||
if (event == nullptr) {
|
||||
// Pool exhausted, drop event (counted via queue's dropped counter)
|
||||
instance->mqtt_event_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
event->populate(*static_cast<esp_mqtt_event_t *>(event_data));
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
instance->mqtt_event_queue_.push(event);
|
||||
|
||||
// Wake main loop immediately to process MQTT event instead of waiting for select() timeout
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
@@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this_mqtt->mqtt_event_pool_.release(elem);
|
||||
this_mqtt->mqtt_outbound_pool_.release(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload,
|
||||
size_t len) {
|
||||
auto *elem = this->mqtt_event_pool_.allocate();
|
||||
auto *elem = this->mqtt_outbound_pool_.allocate();
|
||||
|
||||
if (!elem) {
|
||||
// Queue is full - increment counter but don't log immediately.
|
||||
@@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos,
|
||||
// Use the helper to allocate and copy data
|
||||
if (!elem->set_data(topic, payload, len)) {
|
||||
// Allocation failed, return elem to pool
|
||||
this->mqtt_event_pool_.release(elem);
|
||||
this->mqtt_outbound_pool_.release(elem);
|
||||
// Increment counter without logging to avoid cascade effect during memory pressure
|
||||
this->mqtt_queue_.increment_dropped_count();
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <string>
|
||||
#include <queue>
|
||||
#include <cstring>
|
||||
#include <mqtt_client.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
@@ -18,32 +17,39 @@
|
||||
namespace esphome::mqtt {
|
||||
|
||||
struct Event {
|
||||
esp_mqtt_event_id_t event_id;
|
||||
esp_mqtt_event_id_t event_id{};
|
||||
std::vector<char> data;
|
||||
int total_data_len;
|
||||
int current_data_offset;
|
||||
int total_data_len{0};
|
||||
int current_data_offset{0};
|
||||
std::string topic;
|
||||
int msg_id;
|
||||
bool retain;
|
||||
int qos;
|
||||
bool dup;
|
||||
bool session_present;
|
||||
esp_mqtt_error_codes_t error_handle;
|
||||
int msg_id{0};
|
||||
bool retain{false};
|
||||
int qos{0};
|
||||
bool dup{false};
|
||||
bool session_present{false};
|
||||
esp_mqtt_error_codes_t error_handle{};
|
||||
|
||||
// Construct from esp_mqtt_event_t
|
||||
// Any pointer values that are unsafe to keep are converted to safe copies
|
||||
Event(const esp_mqtt_event_t &event)
|
||||
: event_id(event.event_id),
|
||||
data(event.data, event.data + event.data_len),
|
||||
total_data_len(event.total_data_len),
|
||||
current_data_offset(event.current_data_offset),
|
||||
topic(event.topic, event.topic_len),
|
||||
msg_id(event.msg_id),
|
||||
retain(event.retain),
|
||||
qos(event.qos),
|
||||
dup(event.dup),
|
||||
session_present(event.session_present),
|
||||
error_handle(*event.error_handle) {}
|
||||
// Populate from esp_mqtt_event_t
|
||||
// Copies pointer-based data to owned storage for safe cross-thread transfer
|
||||
void populate(const esp_mqtt_event_t &event) {
|
||||
this->event_id = event.event_id;
|
||||
this->data.assign(event.data, event.data + event.data_len);
|
||||
this->total_data_len = event.total_data_len;
|
||||
this->current_data_offset = event.current_data_offset;
|
||||
this->topic.assign(event.topic, event.topic_len);
|
||||
this->msg_id = event.msg_id;
|
||||
this->retain = event.retain;
|
||||
this->qos = event.qos;
|
||||
this->dup = event.dup;
|
||||
this->session_present = event.session_present;
|
||||
this->error_handle = *event.error_handle;
|
||||
}
|
||||
|
||||
// Release owned resources for pool reuse (keeps allocated capacity for efficiency)
|
||||
void release() {
|
||||
this->data.clear();
|
||||
this->topic.clear();
|
||||
}
|
||||
};
|
||||
|
||||
enum MqttQueueTypeT : uint8_t {
|
||||
@@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
static constexpr size_t TASK_STACK_SIZE = 3072;
|
||||
static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations
|
||||
static constexpr ssize_t TASK_PRIORITY = 5;
|
||||
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
|
||||
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
|
||||
static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker
|
||||
|
||||
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
|
||||
void set_client_id(const char *client_id) final { this->client_id_ = client_id; }
|
||||
@@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
bool skip_cert_cn_check_{false};
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
static void esphome_mqtt_task(void *params);
|
||||
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment.
|
||||
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_;
|
||||
NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_;
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL,
|
||||
@@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
||||
CallbackManager<on_message_callback_t> on_message_;
|
||||
CallbackManager<on_publish_user_callback_t> on_publish_;
|
||||
std::string cached_topic_;
|
||||
std::queue<Event> mqtt_events_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements (one slot distinguishes full from empty).
|
||||
// This guarantees allocate() returns nullptr before push() can fail, which:
|
||||
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
|
||||
// 2. Avoids needing release() on the producer path after a failed push(),
|
||||
// preserving the SPSC contract on the pool's internal free list
|
||||
EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_;
|
||||
LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_;
|
||||
|
||||
#if defined(USE_MQTT_IDF_ENQUEUE)
|
||||
uint32_t last_dropped_log_time_{0};
|
||||
|
||||
@@ -129,7 +129,7 @@ void OnlineImage::update() {
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = ::time(nullptr);
|
||||
this->start_time_ = millis();
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ void OnlineImage::loop() {
|
||||
// Finalize decoding
|
||||
this->end_decode();
|
||||
|
||||
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
|
||||
(uint32_t) (::time(nullptr) - this->start_time_));
|
||||
ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 " ms", this->downloader_->get_bytes_read(),
|
||||
millis() - this->start_time_);
|
||||
|
||||
// Save caching headers
|
||||
this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
|
||||
|
||||
@@ -97,7 +97,7 @@ class OnlineImage : public PollingComponent,
|
||||
*/
|
||||
std::string last_modified_ = "";
|
||||
|
||||
time_t start_time_;
|
||||
uint32_t start_time_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.components.image import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
|
||||
from esphome.core import CORE
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
|
||||
@@ -74,7 +75,14 @@ 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")
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
# JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level
|
||||
# ARDUINO_ESP32S3_DEV define) that require esp-dsp headers.
|
||||
# On Arduino this overwrites the stub; on IDF it adds the component.
|
||||
add_idf_component(name="espressif/esp-dsp", ref="1.7.1")
|
||||
|
||||
|
||||
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_);
|
||||
}
|
||||
|
||||
@@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
|
||||
this->sg_recv_data_state_ = FRAME_DATA_LEN_H;
|
||||
break;
|
||||
case FRAME_DATA_LEN_H:
|
||||
if (value <= 4) {
|
||||
this->sg_data_len_ = value * 256;
|
||||
if (value == 0) {
|
||||
this->sg_frame_buf_[4] = value;
|
||||
this->sg_recv_data_state_ = FRAME_DATA_LEN_L;
|
||||
} else {
|
||||
this->sg_data_len_ = 0;
|
||||
this->sg_recv_data_state_ = FRAME_IDLE;
|
||||
ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value);
|
||||
}
|
||||
break;
|
||||
case FRAME_DATA_LEN_L:
|
||||
this->sg_data_len_ += value;
|
||||
if (this->sg_data_len_ > 32) {
|
||||
this->sg_data_len_ = value;
|
||||
if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) {
|
||||
ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value);
|
||||
this->sg_data_len_ = 0;
|
||||
this->sg_recv_data_state_ = FRAME_IDLE;
|
||||
@@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
|
||||
}
|
||||
break;
|
||||
case FRAME_DATA_BYTES:
|
||||
this->sg_data_len_ -= 1;
|
||||
this->sg_frame_buf_[this->sg_frame_len_++] = value;
|
||||
if (this->sg_data_len_ <= 0) {
|
||||
if (--this->sg_data_len_ == 0) {
|
||||
this->sg_recv_data_state_ = FRAME_DATA_CRC;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -403,10 +403,12 @@ 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_QUANTILE, default=0.9): cv.zero_to_one_float,
|
||||
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.float_range(
|
||||
min=0, min_included=False, max=1
|
||||
),
|
||||
}
|
||||
),
|
||||
validate_send_first_at,
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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,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); }
|
||||
|
||||
@@ -417,7 +417,7 @@ void SpeakerMediaPlayer::loop() {
|
||||
this->media_playlist_.pop_front();
|
||||
}
|
||||
// Only delay starting playback if moving on the next playlist item or repeating the current item
|
||||
timeout_ms = this->announcement_playlist_delay_ms_;
|
||||
timeout_ms = this->media_playlist_delay_ms_;
|
||||
}
|
||||
if (!this->media_playlist_.empty()) {
|
||||
PlaylistItem playlist_item = this->media_playlist_.front();
|
||||
|
||||
@@ -50,8 +50,9 @@ void TC74Component::read_temperature_() {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t temperature_reg;
|
||||
if (this->read_register(TC74_REGISTER_TEMPERATURE, &temperature_reg, 1) != i2c::ERROR_OK) {
|
||||
int8_t temperature_reg;
|
||||
if (this->read_register(TC74_REGISTER_TEMPERATURE, reinterpret_cast<uint8_t *>(&temperature_reg), 1) !=
|
||||
i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -26,16 +26,13 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) {
|
||||
event->data.line_state.dtr = dtr;
|
||||
event->data.line_state.rts = rts;
|
||||
|
||||
if (!this->event_queue_.push(event)) {
|
||||
ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_);
|
||||
// Return event to pool since we couldn't queue it
|
||||
this->event_pool_.release(event);
|
||||
} else {
|
||||
// Wake main loop immediately to process event
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->event_queue_.push(event);
|
||||
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity,
|
||||
@@ -53,16 +50,13 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_
|
||||
event->data.line_coding.parity = parity;
|
||||
event->data.line_coding.data_bits = data_bits;
|
||||
|
||||
if (!this->event_queue_.push(event)) {
|
||||
ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_);
|
||||
// Return event to pool since we couldn't queue it
|
||||
this->event_pool_.release(event);
|
||||
} else {
|
||||
// Wake main loop immediately to process event
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->event_queue_.push(event);
|
||||
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void USBCDCACMInstance::process_events_() {
|
||||
|
||||
@@ -102,7 +102,11 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parented<USBCDCACMC
|
||||
LineStateCallback line_state_callback_{nullptr};
|
||||
|
||||
// Lock-free queue and event pool for cross-task event passing
|
||||
EventPool<CDCEvent, EVENT_QUEUE_SIZE> event_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing both a pool slot leak and an SPSC
|
||||
// violation on the pool's internal free list.
|
||||
EventPool<CDCEvent, EVENT_QUEUE_SIZE - 1> event_pool_;
|
||||
LockFreeQueue<CDCEvent, EVENT_QUEUE_SIZE> event_queue_;
|
||||
};
|
||||
|
||||
|
||||
@@ -144,7 +144,10 @@ class USBClient : public Component {
|
||||
// Lock-free event queue and pool for USB task to main loop communication
|
||||
// Must be public for access from static callbacks
|
||||
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
|
||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing a pool slot leak.
|
||||
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE - 1> event_pool;
|
||||
|
||||
protected:
|
||||
// Process USB events from the queue. Returns true if any work was done.
|
||||
|
||||
@@ -193,7 +193,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *
|
||||
return;
|
||||
}
|
||||
|
||||
// Push to lock-free queue (always succeeds since pool size == queue size)
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
client->event_queue.push(event);
|
||||
|
||||
// Re-enable component loop to process the queued event
|
||||
|
||||
@@ -160,11 +160,9 @@ void USBUartChannel::write_array(const uint8_t *data, size_t len) {
|
||||
size_t chunk_len = std::min(len, UsbOutputChunk::MAX_CHUNK_SIZE);
|
||||
memcpy(chunk->data, data, chunk_len);
|
||||
chunk->length = static_cast<uint8_t>(chunk_len);
|
||||
if (!this->output_queue_.push(chunk)) {
|
||||
this->output_pool_.release(chunk);
|
||||
ESP_LOGE(TAG, "Output queue full - lost %zu bytes", len);
|
||||
break;
|
||||
}
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->output_queue_.push(chunk);
|
||||
data += chunk_len;
|
||||
len -= chunk_len;
|
||||
}
|
||||
@@ -320,7 +318,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
|
||||
chunk->channel = channel;
|
||||
|
||||
// Push to lock-free queue for main loop processing
|
||||
// Push always succeeds because pool size == queue size
|
||||
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
|
||||
// allocate() returned non-null, the queue cannot be full.
|
||||
this->usb_data_queue_.push(chunk);
|
||||
|
||||
// Re-enable component loop to process the queued data
|
||||
|
||||
@@ -158,7 +158,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
|
||||
// Larger structures first (8+ bytes)
|
||||
RingBuffer input_buffer_;
|
||||
LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_;
|
||||
EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
|
||||
// before push() can fail, preventing a pool slot leak.
|
||||
EventPool<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT - 1> output_pool_;
|
||||
std::function<void()> rx_callback_{};
|
||||
CdcEps cdc_dev_{};
|
||||
StringRef debug_prefix_{};
|
||||
@@ -190,7 +193,8 @@ class USBUartComponent : public usb_host::USBClient {
|
||||
// Lock-free data transfer from USB task to main loop
|
||||
static constexpr int USB_DATA_QUEUE_SIZE = 32;
|
||||
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
|
||||
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
|
||||
// Pool sized to queue capacity (SIZE-1) — see USBUartChannel::output_pool_ comment.
|
||||
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE - 1> chunk_pool_;
|
||||
|
||||
protected:
|
||||
std::vector<USBUartChannel *> channels_{};
|
||||
|
||||
@@ -619,6 +619,8 @@ void VoiceAssistant::start_playback_timeout_() {
|
||||
this->cancel_timeout("speaker-timeout");
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED);
|
||||
|
||||
if (this->api_client_ == nullptr)
|
||||
return;
|
||||
api::VoiceAssistantAnnounceFinished msg;
|
||||
msg.success = true;
|
||||
this->api_client_->send_message(msg);
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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]:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user