diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 6f5011246cb..8c052acc87a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -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 diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 4727877633c..1d793ac6b17 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -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) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index c88360ca786..a427cc4a052 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -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 { 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 || diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index bd12b7d1231..486984a694b 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -218,32 +218,50 @@ std::shared_ptr 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 diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index ad11811a8fa..2a130eae587 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -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(); diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index c0d74cef4a8..ef4cc980f66 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -1,6 +1,11 @@ #include "pulse_counter_sensor.h" #include "esphome/core/log.h" +#ifdef HAS_PCNT +#include +#include +#endif + namespace esphome { namespace pulse_counter { @@ -56,103 +61,107 @@ 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(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 max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_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 diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index f906e9e5cb3..a7913d5d66b 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,14 +6,13 @@ #include -// 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 +#if defined(USE_ESP32) +#include +#ifdef SOC_PCNT_SUPPORTED +#include #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 diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 65be5ee7936..01244635679 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -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) diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 7036862d148..4e4705a8892 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -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])) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index ad61aca0841..74420f906a1 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -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; + 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 { - 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->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 temp_ring_buffer = this->ring_buffer_.lock(); + std::shared_ptr 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 stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::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 stack_allocator(RAMAllocator::ALLOC_EXTERNAL); + stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE); + } else { + RAMAllocator stack_allocator(RAMAllocator::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 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(params); - this_resampler->task_created_ = true; xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING); std::unique_ptr resampler = @@ -269,7 +376,7 @@ void ResamplerSpeaker::resample_task(void *params) { std::shared_ptr 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 diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 810087ab7f4..c1ebd7e7b5b 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -8,14 +8,16 @@ #include "esphome/core/component.h" -#include #include +#include 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}; };