Compare commits

..

9 Commits

Author SHA1 Message Date
J. Nick Koston
fd683a5609 [socket] Implement working ready() for LWIP raw TCP sockets (ESP8266/RP2040)
Previously ready() always returned true on ESP8266/RP2040, causing
every socket to be checked on every loop iteration even when no data
was available.

Override ready() in LWIPRawImpl to check rx_buf_/rx_closed_/pcb_ state,
and in LWIPRawListenImpl to check accepted_socket_count_. This uses
existing fields so no extra memory is needed per socket.

Keep ready() virtual only on the non-select path (ESP8266/RP2040) so
the select()-based path (ESP32) retains the non-virtual optimization
from the previous commit.
2026-02-10 17:37:42 -06:00
J. Nick Koston
4a6eb0b16d [socket] Devirtualize Socket::ready() and get_fd() for hot loop path
Move fd_, closed_, and loop_monitored_ fields from BSD/LWIP socket
implementations to the base Socket class. Since only one socket
implementation is active per build, these can be non-virtual.

Make Socket::ready() and get_fd() non-virtual, eliminating vtable
dispatch on every main loop iteration. Inline is_socket_ready via
friendship for the fast path while keeping the public API with
bounds checking for external callers.

Saves ~316 bytes of flash on ESP32-IDF builds.
2026-02-10 12:30:20 -06:00
J. Nick Koston
2585779f11 [api] Remove duplicate peername storage to save RAM (#13540) 2026-02-11 07:23:16 +13:00
Jonathan Swoboda
b8ec3aab1d [ci] Pin ESP-IDF version for Arduino framework builds (#13909)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 12:16:25 -05:00
Jonathan Swoboda
c4b109eebd [esp32_rmt_led_strip, remote_receiver, pulse_counter] Replace hardcoded clock frequencies with runtime queries (#13908)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 17:09:56 +00:00
Jonathan Swoboda
03b41855f5 [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#13911)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 16:03:26 +00:00
Jonathan Swoboda
13a124c86d [pulse_counter] Migrate from legacy PCNT API to new ESP-IDF 5.x API (#13904)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:10:27 -05:00
Kevin Ahrendt
298efb5340 [resampler] Refactor for stability and to support Sendspin (#12254)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-10 09:56:31 -05:00
J. Nick Koston
d4ccc64dc0 [http_request] Fix IDF chunked response completion detection (#13886) 2026-02-10 08:55:59 -06:00
32 changed files with 537 additions and 347 deletions

View File

@@ -1 +1 @@
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
}
APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect
this->helper_->close();
// requested a disconnect - don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true;
return;
}
@@ -293,7 +293,8 @@ bool APIConnection::send_disconnect_response_() {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response() {
this->helper_->close();
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true;
}
@@ -1469,8 +1470,11 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1489,8 +1493,9 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1838,7 +1843,8 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
this->helper_->close();
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->flags_.remove = true;
}
@@ -1899,6 +1905,10 @@ bool APIConnection::schedule_batch_() {
}
void APIConnection::process_batch_() {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false;
return;
@@ -1923,10 +1933,6 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size;
}
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -1950,20 +1956,7 @@ void APIConnection::process_batch_() {
return;
}
// Multi-message path — heavy stack frame isolated in separate noinline function
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
}
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
const uint8_t frame_overhead = header_padding + footer_size;
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
// Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
@@ -1990,7 +1983,7 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead;
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
// Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible,
@@ -2006,38 +1999,42 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
current_offset = shared_buf.size() + footer_size;
}
if (items_processed > 0) {
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) {
this->deferred_batch_.remove_front(items_processed);
this->schedule_batch_();
return;
}
if (items_processed == 0) {
this->deferred_batch_.clear();
return;
}
// All items processed (or none could be processed)
this->clear_batch_();
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning
this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items
this->schedule_batch_();
} else {
// All items processed
this->clear_batch_();
}
}
// Dispatch message encoding based on message_type
@@ -2204,12 +2201,14 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_client_peername(), LOG_STR_ARG(message));
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -276,8 +276,10 @@ class APIConnection final : public APIServerConnectionBase {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
return this->helper_->get_peername_to(buf);
}
protected:
// Helper function to handle authentication completion
@@ -549,8 +551,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
// Remove processed items from the front
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
@@ -622,8 +624,6 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;

View File

@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
if (this->socket_) {
this->socket_->getpeername_to(buf);
} else {
buf[0] = '\0';
}
return buf.data();
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,8 +90,9 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -105,6 +106,8 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -231,8 +234,6 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -192,11 +192,15 @@ void APIServer::loop() {
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());

View File

@@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component

View File

@@ -95,9 +95,9 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
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")

View File

@@ -7,22 +7,25 @@
#include "esphome/core/log.h"
#include <esp_attr.h>
#include <esp_clk_tree.h>
namespace esphome {
namespace esp32_rmt_led_strip {
static const char *const TAG = "esp32_rmt_led_strip";
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
static const uint8_t RMT_CLK_DIV = 1;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
static const uint8_t RMT_CLK_DIV = 2;
#endif
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
// Query the RMT default clock source frequency. This varies by variant:
// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2.
// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within
// the 15-bit rmt_symbol_word_t duration field max of 32767.
static uint32_t rmt_resolution_hz() {
uint32_t freq;
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
rmt_symbol_word_t *symbols, bool *done, void *arg) {
@@ -92,7 +95,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
rmt_tx_channel_config_t channel;
memset(&channel, 0, sizeof(channel));
channel.clk_src = RMT_CLK_SRC_DEFAULT;
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
channel.resolution_hz = rmt_resolution_hz();
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
@@ -137,7 +140,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
float ratio = (float) rmt_resolution_hz() / 1e09f;
// 0-bit
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);

View File

@@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
if CORE.is_esp32:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
include_builtin_idf_component("esp_driver_pcnt")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
* - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
*
* Chunked responses that complete in a reasonable time work correctly on both
* platforms. The limitation below applies only to *streaming* chunked
* responses where data arrives slowly over a long period.
*
* Streaming chunked responses are NOT supported (all platforms):
* The read helpers (http_read_loop_result, http_read_fully) block the main
* event loop until all response data is received. For streaming responses
* where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy),
* this starves the event loop on both ESP-IDF and Arduino. If data arrives
* just often enough to avoid the caller's timeout, the loop runs
* indefinitely. If data stops entirely, ESP-IDF fails with
* -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with
* delay(1) until the caller's timeout fires. Supporting streaming requires
* a non-blocking incremental read pattern that yields back to the event
* loop between chunks. Components that need streaming should use
* esp_http_client directly on a separate FreeRTOS task with
* esp_http_client_is_complete_data_received() for completion detection
* (see audio_reader.cpp for an example).
*
* Chunked transfer encoding - platform differences:
* - ESP-IDF HttpContainer:
* HttpContainerIDF overrides is_read_complete() to call
* esp_http_client_is_complete_data_received(), which is the
* authoritative completion check for both chunked and non-chunked
* transfers. When esp_http_client_read() returns 0 for a completed
* chunked response, read() returns 0 and is_read_complete() returns
* true, so callers get COMPLETE from http_read_loop_result().
*
* - Arduino HttpContainer:
* Chunked responses are decoded internally (see
* HttpContainerArduino::read_chunked_()). When the final chunk arrives,
* is_chunked_ is cleared and content_length is set to bytes_read_.
* Completion is then detected via is_read_complete(), and a subsequent
* read() returns 0 to indicate "all content read" (not
* HTTP_ERROR_CONNECTION_CLOSED).
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
@@ -204,9 +240,13 @@ class HttpContainer : public Parented<HttpRequestComponent> {
size_t get_bytes_read() const { return this->bytes_read_; }
/// Check if all expected content has been read
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
bool is_read_complete() const {
/// Check if all expected content has been read.
/// Base implementation handles non-chunked responses and status-code-based no-body checks.
/// Platform implementations may override for chunked completion detection:
/// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked.
/// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final
/// chunk, after which the base implementation detects completion.
virtual bool is_read_complete() const {
// Per RFC 9112, these responses have no body:
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||

View File

@@ -218,32 +218,50 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
bool HttpContainerIDF::is_read_complete() const {
// Base class handles no-body status codes and non-chunked content_length completion
if (HttpContainer::is_read_complete()) {
return true;
}
// For chunked responses, use the authoritative ESP-IDF completion check
return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
}
// ESP-IDF HTTP read implementation (blocking mode)
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// esp_http_client_read() in blocking mode returns:
// > 0: bytes read
// 0: connection closed (end of stream)
// 0: all chunked data received (is_chunk_complete true) or connection closed
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
// < 0: error
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: all content read (only returned when content_length is known and fully read)
// 0: all content read (for both content_length-based and chunked completion)
// < 0: error/connection closed
//
// Note on chunked transfer encoding:
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
// We handle this by skipping the content_length check when content_length is 0,
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
// by returning 0.
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
// esp_http_client_is_complete_data_received() to distinguish successful completion from
// connection errors. Callers use http_read_loop_result() which checks is_read_complete()
// to return COMPLETE for successful chunked EOF.
//
// Streaming chunked responses are not supported (see http_request.h for details).
// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
// after its internal transport timeout (configured via timeout_ms) expires.
// This is passed through as a negative return value, which callers treat as an error.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content (non-chunked only)
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
if (this->is_read_complete()) {
// Check if we've already read all expected content (non-chunked and no-body only).
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
// returns true as soon as all data arrives from the network, but data may still be in
// the client's internal buffer waiting to be consumed by esp_http_client_read().
if (HttpContainer::is_read_complete()) {
return 0; // All content read successfully
}
@@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return read_len_or_error;
}
// esp_http_client_read() returns 0 in two cases:
// 1. Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// For case 2, 0 indicates that all chunked data has already been delivered
// in previous successful read() calls, so treating this as a closed
// connection does not cause any loss of response data.
// esp_http_client_read() returns 0 when:
// - Known content_length: connection closed before all data received (error)
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
//
// Return 0 in both cases. Callers use http_read_loop_result() which calls
// is_read_complete() to distinguish these:
// - Chunked complete: is_read_complete() returns true (via
// esp_http_client_is_complete_data_received()), caller gets COMPLETE
// - Non-chunked incomplete: is_read_complete() returns false, caller
// eventually gets TIMEOUT (since no more data arrives)
if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED;
return 0;
}
// Negative value - error, return the actual error code for debugging

View File

@@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer {
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
int read(uint8_t *buf, size_t max_len) override;
void end() override;
bool is_read_complete() const override;
/// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt();

View File

@@ -1,6 +1,11 @@
#include "pulse_counter_sensor.h"
#include "esphome/core/log.h"
#ifdef HAS_PCNT
#include <esp_clk_tree.h>
#include <hal/pcnt_ll.h>
#endif
namespace esphome {
namespace pulse_counter {
@@ -56,103 +61,109 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() {
#ifdef HAS_PCNT
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0;
static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0;
this->pin = pin;
this->pin->setup();
this->pcnt_unit = next_pcnt_unit;
this->pcnt_channel = next_pcnt_channel;
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1);
if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) {
next_pcnt_unit = PCNT_UNIT_0;
next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1);
pcnt_unit_config_t unit_config = {
.low_limit = INT16_MIN,
.high_limit = INT16_MAX,
.flags = {.accum_count = true},
};
esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
ESP_LOGCONFIG(TAG,
" PCNT Unit Number: %u\n"
" PCNT Channel Number: %u",
this->pcnt_unit, this->pcnt_channel);
pcnt_chan_config_t chan_config = {
.edge_gpio_num = this->pin->get_pin(),
.level_gpio_num = -1,
};
error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error));
return false;
}
pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS;
pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
switch (this->rising_edge_mode) {
case PULSE_COUNTER_DISABLE:
rising = PCNT_COUNT_DIS;
rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
break;
case PULSE_COUNTER_INCREMENT:
rising = PCNT_COUNT_INC;
rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
break;
case PULSE_COUNTER_DECREMENT:
rising = PCNT_COUNT_DEC;
rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
break;
}
switch (this->falling_edge_mode) {
case PULSE_COUNTER_DISABLE:
falling = PCNT_COUNT_DIS;
falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
break;
case PULSE_COUNTER_INCREMENT:
falling = PCNT_COUNT_INC;
falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
break;
case PULSE_COUNTER_DECREMENT:
falling = PCNT_COUNT_DEC;
falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
break;
}
pcnt_config_t pcnt_config = {
.pulse_gpio_num = this->pin->get_pin(),
.ctrl_gpio_num = PCNT_PIN_NOT_USED,
.lctrl_mode = PCNT_MODE_KEEP,
.hctrl_mode = PCNT_MODE_KEEP,
.pos_mode = rising,
.neg_mode = falling,
.counter_h_lim = 0,
.counter_l_lim = 0,
.unit = this->pcnt_unit,
.channel = this->pcnt_channel,
};
esp_err_t error = pcnt_unit_config(&pcnt_config);
error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error));
return false;
}
if (this->filter_us != 0) {
uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u);
ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val);
error = pcnt_set_filter_value(this->pcnt_unit, filter_val);
uint32_t apb_freq;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq;
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns),
};
error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_filter_enable(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error));
return false;
}
}
error = pcnt_counter_pause(this->pcnt_unit);
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_counter_clear(this->pcnt_unit);
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_counter_resume(this->pcnt_unit);
error = pcnt_unit_enable(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_clear_count(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_start(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
return true;
}
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
pulse_counter_t counter;
pcnt_get_counter_value(this->pcnt_unit, &counter);
pulse_counter_t ret = counter - this->last_value;
this->last_value = counter;
int count;
pcnt_unit_get_count(this->pcnt_unit, &count);
pulse_counter_t ret = count - this->last_value;
this->last_value = count;
return ret;
}
#endif // HAS_PCNT

View File

@@ -6,14 +6,13 @@
#include <cinttypes>
// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h)
// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the
// "driver" IDF component dependency. See:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#include <driver/pcnt.h>
#if defined(USE_ESP32)
#include <soc/soc_caps.h>
#ifdef SOC_PCNT_SUPPORTED
#include <driver/pulse_cnt.h>
#define HAS_PCNT
#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#endif // SOC_PCNT_SUPPORTED
#endif // USE_ESP32
namespace esphome {
namespace pulse_counter {
@@ -24,11 +23,7 @@ enum PulseCounterCountMode {
PULSE_COUNTER_DECREMENT,
};
#ifdef HAS_PCNT
using pulse_counter_t = int16_t;
#else // HAS_PCNT
using pulse_counter_t = int32_t;
#endif // HAS_PCNT
struct PulseCounterStorageBase {
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
@@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase {
bool pulse_counter_setup(InternalGPIOPin *pin) override;
pulse_counter_t read_raw_value() override;
pcnt_unit_t pcnt_unit;
pcnt_channel_t pcnt_channel;
pcnt_unit_handle_t pcnt_unit{nullptr};
pcnt_channel_handle_t pcnt_channel{nullptr};
};
#endif // HAS_PCNT

View File

@@ -129,10 +129,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
use_pcnt = config.get(CONF_USE_PCNT)
if CORE.is_esp32 and use_pcnt:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# Provides driver/pcnt.h header for hardware pulse counter API
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
include_builtin_idf_component("esp_driver_pcnt")
var = await sensor.new_sensor(config, use_pcnt)
await cg.register_component(var, config)

View File

@@ -3,15 +3,11 @@
#ifdef USE_ESP32
#include <driver/gpio.h>
#include <esp_clk_tree.h>
namespace esphome::remote_receiver {
static const char *const TAG = "remote_receiver.esp32";
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
#endif
static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) {
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
@@ -98,7 +94,10 @@ void RemoteReceiverComponent::setup() {
}
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000);
uint32_t rmt_freq;
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED,
&rmt_freq);
uint32_t max_filter_ns = UINT8_MAX * 1000u / (rmt_freq / 1000000);
memset(&this->store_.config, 0, sizeof(this->store_.config));
this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import audio, esp32, speaker
from esphome.components import audio, esp32, socket, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
@@ -34,7 +34,7 @@ def _set_stream_limits(config):
return config
def _validate_audio_compatability(config):
def _validate_audio_compatibility(config):
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
@@ -73,10 +73,13 @@ CONFIG_SCHEMA = cv.All(
)
FINAL_VALIDATE_SCHEMA = _validate_audio_compatability
FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility
async def to_code(config):
# Enable wake_loop_threadsafe for immediate command processing from other tasks
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await speaker.register_speaker(var, config)
@@ -86,12 +89,11 @@ async def to_code(config):
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@@ -4,6 +4,8 @@
#include "esphome/components/audio/audio_resampler.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -17,13 +19,17 @@ static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 20;
static const uint32_t TASK_STACK_SIZE = 3072;
static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
static const char *const TAG = "resampler_speaker";
enum ResamplingEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // stops the resampler task
COMMAND_STOP = (1 << 0), // signals stop request
COMMAND_START = (1 << 1), // signals start request
COMMAND_FINISH = (1 << 2), // signals finish request (graceful stop)
TASK_COMMAND_STOP = (1 << 5), // signals the task to stop
STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11),
STATE_STOPPING = (1 << 12),
@@ -34,9 +40,16 @@ enum ResamplingEventGroupBits : uint32_t {
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
void ResamplerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Resampler Speaker:\n"
" Target Bits Per Sample: %u\n"
" Target Sample Rate: %" PRIu32 " Hz",
this->target_bits_per_sample_, this->target_sample_rate_);
}
void ResamplerSpeaker::setup() {
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
@@ -55,81 +68,155 @@ void ResamplerSpeaker::setup() {
this->audio_output_callback_(new_frames, write_timestamp);
}
});
// Start with loop disabled since no task is running and no commands are pending
this->disable_loop();
}
void ResamplerSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
// Process commands with priority: STOP > FINISH > START
// This ensures stop commands take precedence over conflicting start commands
if (event_group_bits & ResamplingEventGroupBits::COMMAND_STOP) {
if (this->state_ == speaker::STATE_RUNNING || this->state_ == speaker::STATE_STARTING) {
// Clear STOP, START, and FINISH bits - stop takes precedence
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
ResamplingEventGroupBits::COMMAND_START |
ResamplingEventGroupBits::COMMAND_FINISH);
this->waiting_for_output_ = false;
this->enter_stopping_state_();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bits
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
ResamplingEventGroupBits::COMMAND_START |
ResamplingEventGroupBits::COMMAND_FINISH);
}
// Leave bits set if STATE_STOPPING - will be processed once stopped
} else if (event_group_bits & ResamplingEventGroupBits::COMMAND_FINISH) {
if (this->state_ == speaker::STATE_RUNNING) {
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
this->output_speaker_->finish();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bit
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
}
// Leave bit set if transitioning states - will be processed once state allows
} else if (event_group_bits & ResamplingEventGroupBits::COMMAND_START) {
if (this->state_ == speaker::STATE_STOPPED) {
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START);
this->state_ = speaker::STATE_STARTING;
} else if (this->state_ == speaker::STATE_RUNNING) {
// Already running, just clear the command bit
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START);
}
// Leave bit set if transitioning states - will be processed once state allows
}
// Re-read bits after command processing (enter_stopping_state_ may have set task bits)
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting resampler task");
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING);
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers"));
this->status_set_error(LOG_STR("Not enough memory"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
this->state_ = speaker::STATE_STOPPING;
this->enter_stopping_state_();
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream"));
this->status_set_error(LOG_STR("Unsupported stream"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
this->state_ = speaker::STATE_STOPPING;
this->enter_stopping_state_();
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
this->status_set_error(LOG_STR("Resampler task failed"));
this->status_set_error(LOG_STR("Resampler failure"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
this->state_ = speaker::STATE_STOPPING;
this->enter_stopping_state_();
}
if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) {
ESP_LOGD(TAG, "Started resampler task");
ESP_LOGV(TAG, "Started");
this->status_clear_error();
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
}
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) {
ESP_LOGD(TAG, "Stopping resampler task");
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
}
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
if (this->delete_task_() == ESP_OK) {
ESP_LOGD(TAG, "Stopped resampler task");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
}
this->delete_task_();
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
}
switch (this->state_) {
case speaker::STATE_STARTING: {
esp_err_t err = this->start_();
if (err == ESP_OK) {
this->status_clear_error();
this->state_ = speaker::STATE_RUNNING;
} else {
switch (err) {
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start"));
break;
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack"));
default:
this->status_set_error(LOG_STR("Failed to start resampler"));
break;
if (!this->waiting_for_output_) {
esp_err_t err = this->start_();
if (err == ESP_OK) {
this->callback_remainder_ = 0; // reset callback remainder
this->status_clear_error();
this->waiting_for_output_ = true;
this->state_start_ms_ = App.get_loop_component_start_time();
} else {
this->set_start_error_(err);
this->waiting_for_output_ = false;
this->enter_stopping_state_();
}
} else {
if (this->output_speaker_->is_running()) {
this->state_ = speaker::STATE_RUNNING;
this->waiting_for_output_ = false;
} else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) {
// Timed out waiting for the output speaker to start
this->waiting_for_output_ = false;
this->enter_stopping_state_();
}
this->state_ = speaker::STATE_STOPPING;
}
break;
}
case speaker::STATE_RUNNING:
if (this->output_speaker_->is_stopped()) {
this->state_ = speaker::STATE_STOPPING;
this->enter_stopping_state_();
}
break;
case speaker::STATE_STOPPING: {
if ((this->output_speaker_->get_pause_state()) ||
((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS)) {
// If output speaker is paused or stopping timeout exceeded, force stop
this->output_speaker_->stop();
}
if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) {
// Only transition to stopped state once the output speaker and resampler task are fully stopped
this->waiting_for_output_ = false;
this->state_ = speaker::STATE_STOPPED;
}
break;
case speaker::STATE_STOPPING:
this->stop_();
this->state_ = speaker::STATE_STOPPED;
break;
}
case speaker::STATE_STOPPED:
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits == 0) {
// No pending events, disable loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void ResamplerSpeaker::set_start_error_(esp_err_t err) {
switch (err) {
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Task failed to start"));
break;
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Not enough memory"));
break;
default:
this->status_set_error(LOG_STR("Failed to start"));
break;
}
}
@@ -143,16 +230,33 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic
if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) {
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
} else {
if (this->ring_buffer_.use_count() == 1) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer) {
// Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
} else {
// Delay to avoid repeatedly hammering while waiting for the speaker to start
vTaskDelay(ticks_to_wait);
}
}
return bytes_written;
}
void ResamplerSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
this->enable_loop_soon_any_context();
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits & command_bit)) {
xEventGroupSetBits(this->event_group_, command_bit);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
if (wake_loop) {
App.wake_loop_threadsafe();
}
#endif
}
}
void ResamplerSpeaker::start() { this->send_command_(ResamplingEventGroupBits::COMMAND_START, true); }
esp_err_t ResamplerSpeaker::start_() {
this->target_stream_info_ = audio::AudioStreamInfo(
@@ -185,7 +289,7 @@ esp_err_t ResamplerSpeaker::start_task_() {
}
if (this->task_handle_ == nullptr) {
this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this,
this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this,
RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
}
@@ -196,43 +300,47 @@ esp_err_t ResamplerSpeaker::start_task_() {
return ESP_OK;
}
void ResamplerSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; }
void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::COMMAND_STOP); }
void ResamplerSpeaker::stop_() {
void ResamplerSpeaker::enter_stopping_state_() {
this->state_ = speaker::STATE_STOPPING;
this->state_start_ms_ = App.get_loop_component_start_time();
if (this->task_handle_ != nullptr) {
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP);
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP);
}
this->output_speaker_->stop();
}
esp_err_t ResamplerSpeaker::delete_task_() {
if (!this->task_created_) {
void ResamplerSpeaker::delete_task_() {
if (this->task_handle_ != nullptr) {
// Delete the suspended task
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr;
if (this->task_stack_buffer_ != nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
}
this->task_stack_buffer_ = nullptr;
}
return ESP_OK;
}
return ESP_ERR_INVALID_STATE;
if (this->task_stack_buffer_ != nullptr) {
// Deallocate the task stack buffer
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
}
this->task_stack_buffer_ = nullptr;
}
}
void ResamplerSpeaker::finish() { this->output_speaker_->finish(); }
void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); }
bool ResamplerSpeaker::has_buffered_data() const {
bool has_ring_buffer_data = false;
if (this->requires_resampling_() && (this->ring_buffer_.use_count() > 0)) {
has_ring_buffer_data = (this->ring_buffer_.lock()->available() > 0);
if (this->requires_resampling_()) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer) {
has_ring_buffer_data = (temp_ring_buffer->available() > 0);
}
}
return (has_ring_buffer_data || this->output_speaker_->has_buffered_data());
}
@@ -253,9 +361,8 @@ bool ResamplerSpeaker::requires_resampling_() const {
}
void ResamplerSpeaker::resample_task(void *params) {
ResamplerSpeaker *this_resampler = (ResamplerSpeaker *) params;
ResamplerSpeaker *this_resampler = static_cast<ResamplerSpeaker *>(params);
this_resampler->task_created_ = true;
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
std::unique_ptr<audio::AudioResampler> resampler =
@@ -269,7 +376,7 @@ void ResamplerSpeaker::resample_task(void *params) {
std::shared_ptr<RingBuffer> temp_ring_buffer =
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (temp_ring_buffer.use_count() == 0) {
if (!temp_ring_buffer) {
err = ESP_ERR_NO_MEM;
} else {
this_resampler->ring_buffer_ = temp_ring_buffer;
@@ -291,7 +398,7 @@ void ResamplerSpeaker::resample_task(void *params) {
while (err == ESP_OK) {
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
if (event_bits & ResamplingEventGroupBits::COMMAND_STOP) {
if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) {
break;
}
@@ -310,8 +417,8 @@ void ResamplerSpeaker::resample_task(void *params) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
resampler.reset();
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
this_resampler->task_created_ = false;
vTaskDelete(nullptr);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
} // namespace resampler

View File

@@ -8,14 +8,16 @@
#include "esphome/core/component.h"
#include <freertos/event_groups.h>
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
namespace esphome {
namespace resampler {
class ResamplerSpeaker : public Component, public speaker::Speaker {
public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void dump_config() override;
void setup() override;
void loop() override;
@@ -65,13 +67,18 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
/// ESP_ERR_INVALID_STATE if the task wasn't created
esp_err_t start_task_();
/// @brief Stops the output speaker. If the resampling task is running, it sends the stop command.
void stop_();
/// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is
/// running, and stops the output speaker.
void enter_stopping_state_();
/// @brief Deallocates the task stack and resets the pointers.
/// @return ESP_OK if successful
/// ESP_ERR_INVALID_STATE if the task hasn't stopped itself
esp_err_t delete_task_();
/// @brief Sets the appropriate status error based on the start failure reason.
void set_start_error_(esp_err_t err);
/// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers.
void delete_task_();
/// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop.
void send_command_(uint32_t command_bit, bool wake_loop = false);
inline bool requires_resampling_() const;
static void resample_task(void *params);
@@ -83,7 +90,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
speaker::Speaker *output_speaker_{nullptr};
bool task_stack_in_psram_{false};
bool task_created_{false};
bool waiting_for_output_{false};
TaskHandle_t task_handle_{nullptr};
StaticTask_t task_stack_;
@@ -98,6 +105,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
uint32_t target_sample_rate_;
uint32_t buffer_duration_ms_;
uint32_t state_start_ms_{0};
uint64_t callback_remainder_{0};
};

View File

@@ -16,19 +16,13 @@ namespace esphome::socket {
class BSDSocketImpl final : public Socket {
public:
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
BSDSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~BSDSocketImpl() override {
if (!this->closed_) {
@@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
@@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket {
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
};
// Helper to create a socket with optional monitoring

View File

@@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket {
errno = ENOSYS;
return -1;
}
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
int setblocking(bool blocking) final {
if (pcb_ == nullptr) {
errno = ECONNRESET;
@@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
}
bool ready() const override { return this->accepted_socket_count_ > 0; }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
if (pcb_ == nullptr) {
errno = EBADF;

View File

@@ -11,19 +11,13 @@ namespace esphome::socket {
class LwIPSocketImpl final : public Socket {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
LwIPSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~LwIPSocketImpl() override {
if (!this->closed_) {
@@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
@@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket {
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
};
// Helper to create a socket with optional monitoring

View File

@@ -10,6 +10,10 @@ namespace esphome::socket {
Socket::~Socket() {}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
#endif
// Platform-specific inet_ntop wrappers
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value

View File

@@ -63,13 +63,29 @@ class Socket {
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported)
virtual int get_fd() const { return -1; }
/// Get the underlying file descriptor (returns -1 if not supported)
/// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
#endif
/// Check if socket has data ready to read
/// For loop-monitored sockets, checks with the Application's select() results
/// For non-monitored sockets, always returns true (assumes data may be available)
/// For select()-based sockets: non-virtual, checks Application's select() results
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; }
#endif
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
};
/// Create a socket of the given domain, type and protocol.

View File

@@ -430,12 +430,14 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
}
if (this->api_client_ != nullptr) {
char current_peername[socket::SOCKADDR_STR_LEN];
char new_peername[socket::SOCKADDR_STR_LEN];
ESP_LOGE(TAG,
"Multiple API Clients attempting to connect to Voice Assistant\n"
"Current client: %s (%s)\n"
"New client: %s (%s)",
this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(),
client->get_peername());
this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(),
client->get_peername_to(new_peername));
return;
}

View File

@@ -605,15 +605,6 @@ void Application::unregister_socket_fd(int fd) {
}
}
bool Application::is_socket_ready(int fd) const {
// This function is thread-safe for reading the result of select()
// However, it should only be called after select() has been executed in the main loop
// The read_fds_ is only modified by select() in the main loop
if (fd < 0 || fd >= FD_SETSIZE)
return false;
return FD_ISSET(fd, &this->read_fds_);
}
#endif
void Application::yield_with_select_(uint32_t delay_ms) {

View File

@@ -101,6 +101,10 @@
#include "esphome/components/update/update_entity.h"
#endif
namespace esphome::socket {
class Socket;
} // namespace esphome::socket
namespace esphome {
// Teardown timeout constant (in milliseconds)
@@ -491,7 +495,8 @@ class Application {
void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking
/// This function is thread-safe for reading, but should be called after select() has run
bool is_socket_ready(int fd) const;
/// The read_fds_ is only modified by select() in the main loop
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
#ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task
@@ -503,6 +508,15 @@ class Application {
protected:
friend Component;
friend class socket::Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Safe because: fd was validated in register_socket_fd() at registration time,
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
/// FD_ISSET may include its own upper bounds check depending on platform.
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
void register_component_(Component *comp);

View File

@@ -10,7 +10,7 @@ dependencies:
espressif/mdns:
version: 1.9.1
espressif/esp_wifi_remote:
version: 1.2.4
version: 1.3.2
rules:
- if: "target in [esp32h2, esp32p4]"
espressif/eppp_link:
@@ -18,7 +18,7 @@ dependencies:
rules:
- if: "target in [esp32h2, esp32p4]"
espressif/esp_hosted:
version: 2.9.3
version: 2.11.5
rules:
- if: "target in [esp32h2, esp32p4]"
zorxx/multipart-parser:

View File

@@ -136,6 +136,7 @@ extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps =