mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 16:04:55 +00:00
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.0b2
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -214,4 +214,4 @@ async def to_code(config):
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
if data.opus_support:
|
||||
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.4")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.5")
|
||||
|
||||
@@ -61,7 +61,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
|
||||
#endif
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting..."));
|
||||
}
|
||||
|
||||
void CaptivePortal::setup() {
|
||||
@@ -71,7 +71,7 @@ void CaptivePortal::setup() {
|
||||
void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler(this);
|
||||
this->base_->add_handler_without_auth(this);
|
||||
}
|
||||
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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).
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -2109,20 +2109,6 @@ void WiFiComponent::retry_connect() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_RP2040
|
||||
// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart
|
||||
// mDNS when the network interface reconnects. However, this callback is disabled
|
||||
// in the arduino-pico framework. As a workaround, we block component setup until
|
||||
// WiFi is connected, ensuring mDNS.begin() is called with an active connection.
|
||||
|
||||
bool WiFiComponent::can_proceed() {
|
||||
if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) {
|
||||
return true;
|
||||
}
|
||||
return this->is_connected_();
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
bool WiFiComponent::is_connected_() const {
|
||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||
|
||||
@@ -437,10 +437,6 @@ class WiFiComponent : public Component {
|
||||
|
||||
void retry_connect();
|
||||
|
||||
#ifdef USE_RP2040
|
||||
bool can_proceed() override;
|
||||
#endif
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
|
||||
bool is_connected() const { return this->connected_; }
|
||||
|
||||
@@ -76,9 +76,7 @@ void Mutex::unlock() { k_mutex_unlock(static_cast<k_mutex *>(this->handle_)); }
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); }
|
||||
IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); }
|
||||
|
||||
// Zephyr doesn't support lwIP core locking, so this is a no-op
|
||||
LwIPLock::LwIPLock() {}
|
||||
LwIPLock::~LwIPLock() {}
|
||||
// Zephyr LwIPLock is defined inline as a no-op in helpers.h
|
||||
|
||||
uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp)
|
||||
bool random_bytes(uint8_t *data, size_t len) {
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.3.0b1"
|
||||
__version__ = "2026.3.0b2"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -195,6 +195,7 @@
|
||||
|
||||
// ESP32-specific feature flags
|
||||
#ifdef USE_ESP32
|
||||
#define USE_ESP32_CRASH_HANDLER
|
||||
#define USE_MQTT_IDF_ENQUEUE
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#define USE_OTA_ROLLBACK
|
||||
@@ -337,6 +338,7 @@
|
||||
#ifdef USE_RP2040
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0)
|
||||
#define USE_LOOP_PRIORITY
|
||||
#define USE_RP2040_CRASH_HANDLER
|
||||
#define USE_HTTP_REQUEST_RESPONSE
|
||||
#define USE_I2C
|
||||
#define USE_LOGGER_USB_CDC
|
||||
|
||||
@@ -942,6 +942,28 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf,
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Safely append a string to buffer without format parsing, returning new position (capped at size).
|
||||
/// More efficient than buf_append_printf for plain string literals.
|
||||
/// @param buf Output buffer
|
||||
/// @param size Total buffer size
|
||||
/// @param pos Current position in buffer
|
||||
/// @param str String to append (must not be null)
|
||||
/// @return New position after appending (capped at size on overflow)
|
||||
inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str) {
|
||||
if (pos >= size) {
|
||||
return size;
|
||||
}
|
||||
size_t remaining = size - pos - 1; // reserve space for null terminator
|
||||
size_t len = strlen(str);
|
||||
if (len > remaining) {
|
||||
len = remaining;
|
||||
}
|
||||
memcpy(buf + pos, str, len);
|
||||
pos += len;
|
||||
buf[pos] = '\0';
|
||||
return pos;
|
||||
}
|
||||
|
||||
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
|
||||
/// This avoids multiple heap allocations during string construction.
|
||||
/// Maximum name length supported is 120 characters for friendly names.
|
||||
@@ -1779,19 +1801,27 @@ class InterruptLock {
|
||||
|
||||
/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
|
||||
*
|
||||
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled.
|
||||
* It ensures thread-safe access to lwIP APIs.
|
||||
* This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled,
|
||||
* and on RP2040 when CYW43 WiFi is active (cyw43_arch_lwip_begin/end).
|
||||
*
|
||||
* @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp
|
||||
* On platforms without lwIP core locking (ESP8266, LibreTiny, Zephyr),
|
||||
* this is a no-op defined inline so the compiler can eliminate all call overhead.
|
||||
*/
|
||||
class LwIPLock {
|
||||
public:
|
||||
LwIPLock();
|
||||
~LwIPLock();
|
||||
|
||||
// Delete copy constructor and copy assignment operator to prevent accidental copying
|
||||
LwIPLock(const LwIPLock &) = delete;
|
||||
LwIPLock &operator=(const LwIPLock &) = delete;
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_RP2040)
|
||||
// Platforms with potential lwIP core locking — out-of-line implementations in helpers.cpp
|
||||
LwIPLock();
|
||||
~LwIPLock();
|
||||
#else
|
||||
// No lwIP core locking — inline no-ops (empty bodies instead of = default
|
||||
// to prevent clang-tidy unused-variable warnings at call sites)
|
||||
LwIPLock() {}
|
||||
~LwIPLock() {}
|
||||
#endif
|
||||
};
|
||||
|
||||
/** Helper class to request `loop()` to be called as fast as possible.
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
// LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc.
|
||||
#include <lwip/api.h>
|
||||
#include <lwip/priv/sockets_priv.h>
|
||||
#include <lwip/tcp.h>
|
||||
// FreeRTOS include paths differ: ESP-IDF uses freertos/ prefix, LibreTiny does not
|
||||
#ifdef USE_ESP32
|
||||
#include <freertos/FreeRTOS.h>
|
||||
@@ -216,6 +217,21 @@ void esphome_lwip_hook_socket(struct lwip_sock *sock) {
|
||||
sock->conn->callback = esphome_socket_event_callback;
|
||||
}
|
||||
|
||||
bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) {
|
||||
if (sock == NULL || sock->conn == NULL)
|
||||
return false;
|
||||
if (NETCONNTYPE_GROUP(sock->conn->type) != NETCONN_TCP)
|
||||
return false;
|
||||
if (sock->conn->pcb.tcp == NULL)
|
||||
return false;
|
||||
if (enable) {
|
||||
tcp_nagle_disable(sock->conn->pcb.tcp);
|
||||
} else {
|
||||
tcp_nagle_enable(sock->conn->pcb.tcp);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wake the main loop from another FreeRTOS task. NOT ISR-safe.
|
||||
void esphome_lwip_wake_main_loop(void) {
|
||||
TaskHandle_t task = s_main_loop_task;
|
||||
|
||||
@@ -66,6 +66,13 @@ void esphome_lwip_wake_main_loop(void);
|
||||
/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed.
|
||||
void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken);
|
||||
|
||||
/// Set or clear TCP_NODELAY on a socket's tcp_pcb directly.
|
||||
/// Must be called with the TCPIP core lock held (LwIPLock in C++).
|
||||
/// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade,
|
||||
/// hooks, refcounting) — just a direct pcb->flags bit set/clear.
|
||||
/// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP.
|
||||
bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable);
|
||||
|
||||
/// Wake the main loop task from any context (ISR, thread, or main loop).
|
||||
/// ESP32-only: uses xPortInIsrContext() to detect ISR context.
|
||||
/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths.
|
||||
|
||||
@@ -4,7 +4,7 @@ dependencies:
|
||||
esphome/esp-audio-libs:
|
||||
version: 2.0.3
|
||||
esphome/micro-opus:
|
||||
version: 0.3.4
|
||||
version: 0.3.5
|
||||
espressif/esp-tflite-micro:
|
||||
version: 1.3.3~1
|
||||
espressif/esp32-camera:
|
||||
@@ -20,7 +20,7 @@ dependencies:
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
espressif/esp_hosted:
|
||||
version: 2.12.0
|
||||
version: 2.12.1
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
zorxx/multipart-parser:
|
||||
|
||||
@@ -2,6 +2,7 @@ import contextlib
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
import tempfile
|
||||
import time
|
||||
@@ -109,14 +110,18 @@ def prepare(
|
||||
CONF_CLIENT_CERTIFICATE_KEY
|
||||
):
|
||||
with (
|
||||
tempfile.NamedTemporaryFile(mode="w+") as cert_file,
|
||||
tempfile.NamedTemporaryFile(mode="w+") as key_file,
|
||||
tempfile.NamedTemporaryFile(mode="w+", delete=False) as cert_file,
|
||||
tempfile.NamedTemporaryFile(mode="w+", delete=False) as key_file,
|
||||
):
|
||||
cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE))
|
||||
cert_file.flush()
|
||||
key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY))
|
||||
key_file.flush()
|
||||
context.load_cert_chain(cert_file.name, key_file.name)
|
||||
try:
|
||||
cert_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE))
|
||||
key_file.write(config[CONF_MQTT].get(CONF_CLIENT_CERTIFICATE_KEY))
|
||||
cert_file.close()
|
||||
key_file.close()
|
||||
context.load_cert_chain(cert_file.name, key_file.name)
|
||||
finally:
|
||||
os.unlink(cert_file.name)
|
||||
os.unlink(key_file.name)
|
||||
client.tls_set_context(context)
|
||||
|
||||
try:
|
||||
|
||||
@@ -340,6 +340,8 @@ STACKTRACE_ESP32_BACKTRACE_RE = re.compile(
|
||||
r"Backtrace:(?:\s*0x[0-9a-fA-F]{8}:0x[0-9a-fA-F]{8})+"
|
||||
)
|
||||
STACKTRACE_ESP32_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
# ESP32 crash handler (stored backtrace from previous boot)
|
||||
STACKTRACE_ESP32_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})")
|
||||
STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
|
||||
|
||||
@@ -371,6 +373,11 @@ def process_stacktrace(config, line, backtrace_state):
|
||||
)
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
# ESP32 crash handler backtrace (from previous boot)
|
||||
match = re.search(STACKTRACE_ESP32_CRASH_BT_RE, line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
# ESP32 single-line backtrace
|
||||
match = re.match(STACKTRACE_ESP32_BACKTRACE_RE, line)
|
||||
if match is not None:
|
||||
|
||||
@@ -4,7 +4,7 @@ PyYAML==6.0.3
|
||||
paho-mqtt==1.6.1
|
||||
colorama==0.4.6
|
||||
icmplib==3.0.4
|
||||
tornado==6.5.4
|
||||
tornado==6.5.5
|
||||
tzlocal==5.3.1 # from time
|
||||
tzdata>=2021.1 # from time
|
||||
pyserial==3.5
|
||||
|
||||
@@ -642,7 +642,7 @@ class StringType(TypeInfo):
|
||||
# For SOURCE_BOTH, check if StringRef is set (sending) or use string (received)
|
||||
return (
|
||||
f"if (!this->{self.field_name}_ref_.empty()) {{"
|
||||
f' out.append("\'").append(this->{self.field_name}_ref_.c_str()).append("\'");'
|
||||
f' out.append("\'").append(this->{self.field_name}_ref_.c_str(), this->{self.field_name}_ref_.size()).append("\'");'
|
||||
f"}} else {{"
|
||||
f' out.append("\'").append(this->{self.field_name}).append("\'");'
|
||||
f"}}"
|
||||
@@ -2705,7 +2705,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("'");
|
||||
}
|
||||
|
||||
11
tests/components/adc/test.rp2040-pico2-ard.yaml
Normal file
11
tests/components/adc/test.rp2040-pico2-ard.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
sensor:
|
||||
- id: my_sensor
|
||||
platform: adc
|
||||
pin: VCC
|
||||
name: ADC Test sensor
|
||||
update_interval: "1:01"
|
||||
unit_of_measurement: "°C"
|
||||
icon: "mdi:water-percent"
|
||||
accuracy_decimals: 5
|
||||
setup_priority: -100
|
||||
force_update: true
|
||||
@@ -2,10 +2,10 @@ ethernet:
|
||||
type: W5500
|
||||
clk_pin: 19
|
||||
mosi_pin: 21
|
||||
miso_pin: 23
|
||||
miso_pin: 17
|
||||
cs_pin: 18
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
reset_pin: 12
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
|
||||
1
tests/components/ethernet/test.esp32-p4-idf.yaml
Normal file
1
tests/components/ethernet/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common-ip101.yaml
|
||||
1
tests/components/ethernet/test.esp32-s3-idf.yaml
Normal file
1
tests/components/ethernet/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common-w5500.yaml
|
||||
@@ -60,6 +60,12 @@ esphome:
|
||||
}
|
||||
}
|
||||
|
||||
# Test set_effect with const char* doesn't cause ambiguous overload (issue #14728)
|
||||
- lambda: |-
|
||||
auto call = id(test_monochromatic_light).turn_on();
|
||||
call.set_effect("None");
|
||||
call.perform();
|
||||
|
||||
- light.toggle: test_binary_light
|
||||
- light.turn_off: test_rgb_light
|
||||
- light.turn_on:
|
||||
|
||||
59
tests/components/modbus/modbus_test.cpp
Normal file
59
tests/components/modbus/modbus_test.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::modbus {
|
||||
|
||||
// Exposes protected methods for testing.
|
||||
class TestModbus : public Modbus {
|
||||
public:
|
||||
bool test_parse_modbus_byte(uint8_t byte) { return this->parse_modbus_byte_(byte); }
|
||||
void test_clear_rx_buffer() { this->rx_buffer_.clear(); }
|
||||
void set_waiting(uint8_t addr) { this->waiting_for_response_ = addr; }
|
||||
};
|
||||
|
||||
class MockDevice : public ModbusDevice {
|
||||
public:
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override { this->data_received = true; }
|
||||
bool data_received{false};
|
||||
};
|
||||
|
||||
TEST(ModbusTest, TwoByteRegressionTest) {
|
||||
TestModbus modbus;
|
||||
modbus.set_role(ModbusRole::CLIENT);
|
||||
// First byte (at=0)
|
||||
EXPECT_TRUE(modbus.test_parse_modbus_byte(0x01));
|
||||
// Second byte (at=1)
|
||||
// This used to reach raw[2] because it skipped the if(at==2) check, causing a
|
||||
// buffer overflow.
|
||||
EXPECT_TRUE(modbus.test_parse_modbus_byte(0x03));
|
||||
}
|
||||
|
||||
TEST(ModbusTest, TestValidFrame) {
|
||||
TestModbus modbus;
|
||||
modbus.set_role(ModbusRole::CLIENT);
|
||||
|
||||
MockDevice device;
|
||||
device.set_parent(&modbus);
|
||||
device.set_address(0x01);
|
||||
modbus.register_device(&device);
|
||||
modbus.set_waiting(0x01);
|
||||
|
||||
// Address 1, Function 3, Length 2, Data 0x1234
|
||||
uint8_t frame_data[] = {0x01, 0x03, 0x02, 0x12, 0x34};
|
||||
uint16_t crc = esphome::crc16(frame_data, sizeof(frame_data));
|
||||
|
||||
std::vector<uint8_t> frame;
|
||||
for (uint8_t b : frame_data)
|
||||
frame.push_back(b);
|
||||
frame.push_back(crc & 0xFF);
|
||||
frame.push_back((crc >> 8) & 0xFF);
|
||||
|
||||
for (size_t i = 0; i < frame.size(); i++) {
|
||||
bool result = modbus.test_parse_modbus_byte(frame[i]);
|
||||
EXPECT_TRUE(result) << "Failed at byte " << i << " (0x" << std::hex << (int) frame[i] << ")";
|
||||
}
|
||||
EXPECT_TRUE(device.data_received);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus
|
||||
6
tests/components/spi/test.rp2040-pico2-ard.yaml
Normal file
6
tests/components/spi/test.rp2040-pico2-ard.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
clk_pin: GPIO2
|
||||
mosi_pin: GPIO3
|
||||
miso_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -23,3 +23,6 @@ uart:
|
||||
baud_rate: 115200
|
||||
debug:
|
||||
debug_prefix: "[UART1] "
|
||||
- id: uart_rx_only
|
||||
rx_pin: 17
|
||||
baud_rate: 1200
|
||||
|
||||
27
tests/integration/fixtures/online_image_bmp.yaml
Normal file
27
tests/integration/fixtures/online_image_bmp.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
esphome:
|
||||
name: online-image-bmp
|
||||
|
||||
host:
|
||||
|
||||
http_request:
|
||||
|
||||
display:
|
||||
|
||||
online_image:
|
||||
- url: http://127.0.0.1:HTTP_PORT/foo.bmp
|
||||
id: myimg
|
||||
format: BMP
|
||||
type: RGB
|
||||
on_download_finished:
|
||||
logger.log:
|
||||
format: "download finished. cache hit: %u"
|
||||
args: [cached]
|
||||
|
||||
api:
|
||||
actions:
|
||||
- action: fetch_image
|
||||
then:
|
||||
- component.update: myimg
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
23
tests/integration/fixtures/template_text_save.yaml
Normal file
23
tests/integration/fixtures/template_text_save.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
esphome:
|
||||
name: host-template-text-save-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
|
||||
logger:
|
||||
|
||||
preferences:
|
||||
flash_write_interval: 0s
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
name: "Test Text Restore"
|
||||
id: test_text_restore
|
||||
optimistic: true
|
||||
min_length: 0
|
||||
max_length: 10
|
||||
mode: text
|
||||
initial_value: "hello"
|
||||
restore_value: true
|
||||
119
tests/integration/test_online_image_bmp.py
Normal file
119
tests/integration/test_online_image_bmp.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# black 8x8 RGB BMP, generated with
|
||||
# from PIL import Image
|
||||
# from io import BytesIO
|
||||
# b = BytesIO()
|
||||
# img = Image.new("RGB", (8, 8))
|
||||
# img.save(b, format="BMP")
|
||||
# b.getvalue()
|
||||
BMP_IMAGE = b"BM\xf6\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x08\x00\x00\x00\x08\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\xc0\x00\x00\x00\xc4\x0e\x00\x00\xc4\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
LEN_BMP_IMAGE = len(BMP_IMAGE)
|
||||
|
||||
|
||||
def handle_http(http_request_future):
|
||||
async def handler(reader, writer):
|
||||
try:
|
||||
async with asyncio.timeout(1.0):
|
||||
data = await reader.readuntil(b"\r\n")
|
||||
|
||||
# ensure our request matches the expectation
|
||||
expected_request = b"GET /foo.bmp HTTP/1.1\r\n"
|
||||
assert data[: len(expected_request)] == expected_request
|
||||
|
||||
# consume rest of request
|
||||
async with asyncio.timeout(1.0):
|
||||
data = await reader.readuntil(b"\r\n\r\n")
|
||||
|
||||
http_request_future.set_result(True)
|
||||
|
||||
http_response = [
|
||||
b"HTTP/1.1 200 OK",
|
||||
b"Content-Length: %d" % LEN_BMP_IMAGE,
|
||||
b"Content-Type: text/plain",
|
||||
b"Connection: close",
|
||||
b"",
|
||||
b"",
|
||||
]
|
||||
writer.write(b"\r\n".join(http_response))
|
||||
await writer.drain()
|
||||
|
||||
writer.write(BMP_IMAGE)
|
||||
|
||||
await writer.drain()
|
||||
except Exception as exc:
|
||||
if not http_request_future.done():
|
||||
http_request_future.set_exception(exc)
|
||||
raise
|
||||
finally:
|
||||
writer.close()
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_online_image_bmp(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Esphome shouldn't block the main loop when a http response is slow"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track http request
|
||||
http_request_future = loop.create_future()
|
||||
download_finished_future = loop.create_future()
|
||||
downloaded_bytes_future = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
|
||||
if match := re.search(r"Image fully downloaded, (\d+) bytes", line):
|
||||
downloaded_bytes_future.set_result(int(match.group(1)))
|
||||
|
||||
if "download finished" in line:
|
||||
download_finished_future.set_result(True)
|
||||
|
||||
server = await asyncio.start_server(
|
||||
handle_http(http_request_future), "127.0.0.1", 0
|
||||
)
|
||||
http_server_port = server.sockets[0].getsockname()[1]
|
||||
|
||||
config = yaml_config.replace("HTTP_PORT", str(http_server_port))
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
server,
|
||||
run_compiled(config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "online-image-bmp"
|
||||
|
||||
# List services to find our test service
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
# Find test service
|
||||
request_service = next((s for s in services if s.name == "fetch_image"), None)
|
||||
|
||||
assert request_service is not None, "fetch_image service not found"
|
||||
|
||||
await client.execute_service(request_service, {})
|
||||
|
||||
async with asyncio.timeout(0.1):
|
||||
await http_request_future
|
||||
|
||||
async with asyncio.timeout(0.5):
|
||||
numbytes = await downloaded_bytes_future
|
||||
assert numbytes == LEN_BMP_IMAGE
|
||||
await download_finished_future
|
||||
131
tests/integration/test_template_text_save.py
Normal file
131
tests/integration/test_template_text_save.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Integration test for template text restore_value persistence.
|
||||
|
||||
Tests that:
|
||||
1. A template text with restore_value saves its value to preferences
|
||||
2. The saved value persists across restarts (binary re-run)
|
||||
3. Setting the same value again does not produce a spurious "too long" warning
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import socket
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import TextInfo, TextState
|
||||
import pytest
|
||||
|
||||
from .conftest import run_binary_and_wait_for_port, wait_and_connect_api_client
|
||||
from .state_utils import InitialStateHelper, require_entity
|
||||
from .types import CompileFunction, ConfigWriter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_template_text_save(
|
||||
yaml_config: str,
|
||||
write_yaml_config: ConfigWriter,
|
||||
compile_esphome: CompileFunction,
|
||||
reserved_tcp_port: tuple[int, socket.socket],
|
||||
) -> None:
|
||||
"""Test template text save/restore persistence and duplicate-save behavior."""
|
||||
port, port_socket = reserved_tcp_port
|
||||
|
||||
# Clean up any stale preference file from previous runs
|
||||
prefs_file = (
|
||||
Path.home() / ".esphome" / "prefs" / "host-template-text-save-test.prefs"
|
||||
)
|
||||
if prefs_file.exists():
|
||||
prefs_file.unlink()
|
||||
|
||||
# Write and compile once
|
||||
config_path = await write_yaml_config(yaml_config)
|
||||
binary_path = await compile_esphome(config_path)
|
||||
|
||||
# Release the reserved port so the binary can bind to it
|
||||
port_socket.close()
|
||||
|
||||
# --- First run: set a value and verify no spurious warnings ---
|
||||
warning_lines: list[str] = []
|
||||
|
||||
def capture_warnings(line: str) -> None:
|
||||
if "too long to save" in line.lower():
|
||||
warning_lines.append(line)
|
||||
|
||||
async with (
|
||||
run_binary_and_wait_for_port(
|
||||
binary_path, "127.0.0.1", port, line_callback=capture_warnings
|
||||
),
|
||||
wait_and_connect_api_client(port=port) as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info.name == "host-template-text-save-test"
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
text_entity = require_entity(
|
||||
entities, "test_text_restore", TextInfo, "Test Text Restore"
|
||||
)
|
||||
|
||||
# Set up state tracking
|
||||
loop = asyncio.get_running_loop()
|
||||
state_futures: dict[int, asyncio.Future[Any]] = {}
|
||||
|
||||
def on_state(state: Any) -> None:
|
||||
if state.key in state_futures and not state_futures[state.key].done():
|
||||
state_futures[state.key].set_result(state)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Verify initial value from config
|
||||
initial = initial_state_helper.initial_states[text_entity.key]
|
||||
assert isinstance(initial, TextState)
|
||||
assert initial.state == "hello"
|
||||
|
||||
async def wait_for_state(key: int, timeout: float = 2.0) -> Any:
|
||||
state_futures[key] = loop.create_future()
|
||||
try:
|
||||
return await asyncio.wait_for(state_futures[key], timeout)
|
||||
finally:
|
||||
state_futures.pop(key, None)
|
||||
|
||||
# Set a new value that fits within max_length
|
||||
client.text_command(key=text_entity.key, state="world")
|
||||
state = await wait_for_state(text_entity.key)
|
||||
assert state.state == "world"
|
||||
|
||||
# Set the same value again - should NOT produce "too long" warning
|
||||
client.text_command(key=text_entity.key, state="world")
|
||||
# Give time for the warning to appear (if any)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# No warnings should have appeared
|
||||
assert warning_lines == [], (
|
||||
f"Unexpected 'too long to save' warning(s): {warning_lines}"
|
||||
)
|
||||
|
||||
# --- Second run: verify the value was restored from preferences ---
|
||||
async with (
|
||||
run_binary_and_wait_for_port(binary_path, "127.0.0.1", port),
|
||||
wait_and_connect_api_client(port=port) as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
text_entity = require_entity(
|
||||
entities, "test_text_restore", TextInfo, "Test Text Restore"
|
||||
)
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(lambda s: None))
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# The value should be "world" - restored from preferences
|
||||
restored = initial_state_helper.initial_states[text_entity.key]
|
||||
assert isinstance(restored, TextState)
|
||||
assert restored.state == "world", (
|
||||
f"Expected restored value 'world', got '{restored.state}'"
|
||||
)
|
||||
|
||||
# Clean up preference file
|
||||
if prefs_file.exists():
|
||||
prefs_file.unlink()
|
||||
@@ -102,7 +102,11 @@ async def test_water_heater_template(
|
||||
f"Expected target temp 60.0, got {initial_state.target_temperature}"
|
||||
)
|
||||
|
||||
# Verify supported features: away mode and on/off (fixture has away + is_on lambdas)
|
||||
# Verify supported features: operation mode, away mode, and on/off
|
||||
assert (
|
||||
test_water_heater.supported_features
|
||||
& WaterHeaterFeature.SUPPORTS_OPERATION_MODE
|
||||
) != 0, "Expected SUPPORTS_OPERATION_MODE in supported_features"
|
||||
assert (
|
||||
test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE
|
||||
) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: componenttestrp2040pico2ard
|
||||
friendly_name: $component_name
|
||||
|
||||
rp2040:
|
||||
board: rpipico2
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
packages:
|
||||
component_under_test: !include
|
||||
file: $component_test_file
|
||||
vars:
|
||||
component_test_file: $component_test_file
|
||||
12
tests/test_build_components/common/spi/rp2040-pico2-ard.yaml
Normal file
12
tests/test_build_components/common/spi/rp2040-pico2-ard.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Common SPI configuration for RP2040 Pico 2 (RP2350) Arduino tests
|
||||
|
||||
substitutions:
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO19
|
||||
miso_pin: GPIO16
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: ${clk_pin}
|
||||
mosi_pin: ${mosi_pin}
|
||||
miso_pin: ${miso_pin}
|
||||
@@ -673,6 +673,34 @@ def test_process_stacktrace_bad_alloc(
|
||||
assert state is False
|
||||
|
||||
|
||||
def test_process_stacktrace_esp32_crash_handler(
|
||||
setup_core: Path, mock_decode_pc: Mock
|
||||
) -> None:
|
||||
"""Test process_stacktrace handles ESP32 crash handler backtrace lines."""
|
||||
config = {"name": "test"}
|
||||
|
||||
# Simulate crash handler log lines as they appear from the API/serial
|
||||
line_pc = "[E][esp32.crash:078]: PC: 0x400D1234 (fault location)"
|
||||
state = platformio_api.process_stacktrace(config, line_pc, False)
|
||||
# PC line is matched by existing STACKTRACE_ESP32_PC_RE
|
||||
mock_decode_pc.assert_called_with(config, "400D1234")
|
||||
assert state is False
|
||||
|
||||
mock_decode_pc.reset_mock()
|
||||
|
||||
line_bt0 = "[E][esp32.crash:080]: BT0: 0x400D5678 (backtrace)"
|
||||
state = platformio_api.process_stacktrace(config, line_bt0, False)
|
||||
mock_decode_pc.assert_called_once_with(config, "400D5678")
|
||||
assert state is False
|
||||
|
||||
mock_decode_pc.reset_mock()
|
||||
|
||||
line_bt1 = "[E][esp32.crash:080]: BT1: 0x42005ABC (backtrace)"
|
||||
state = platformio_api.process_stacktrace(config, line_bt1, False)
|
||||
mock_decode_pc.assert_called_once_with(config, "42005ABC")
|
||||
assert state is False
|
||||
|
||||
|
||||
def test_patch_file_downloader_succeeds_first_try() -> None:
|
||||
"""Test patch_file_downloader succeeds on first attempt."""
|
||||
mock_exception_cls = type("PackageException", (Exception,), {})
|
||||
|
||||
Reference in New Issue
Block a user