fix(box): no reboot when switching radio stations
Switching station without pressing Stop rebooted: a StreamBuffer allows a single sender, but the previous station's HTTP task lingered (blocked in recv, or in a full-buffer send) past stop()'s wait, so starting the next station made two senders -> FreeRTOS assert -> reboot. Fixes: close the live HTTP socket from stop() (mutex-guarded) so a blocked recv returns at once; short net/decode timeouts; a per-play generation guard so an old task never touches the buffer; and play() waits for the old session to be fully gone before starting.
This commit is contained in:
+57
-16
@@ -34,6 +34,7 @@
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/idf_additions.h"
|
||||
#include "freertos/stream_buffer.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_codec_dev.h"
|
||||
@@ -48,8 +49,8 @@ static const char *TAG = "radio";
|
||||
|
||||
// --- Tuning ------------------------------------------------------------------
|
||||
#define NET_STREAM_BUF_BYTES (32 * 1024) // ~32 KB of buffered compressed MP3
|
||||
#define NET_CHUNK_TIMEOUT_MS 2000 // max wait pushing a network chunk
|
||||
#define DEC_READ_TIMEOUT_MS 2000 // max wait pulling MP3 from buffer
|
||||
#define NET_CHUNK_TIMEOUT_MS 200 // short: re-check s_stop fast even when the buffer is full
|
||||
#define DEC_READ_TIMEOUT_MS 300 // short: the decode task notices a stop promptly
|
||||
#define DEC_READ_CHUNK 4096 // bytes pulled per decode iteration
|
||||
#define DEC_OUT_CAP_INIT 8192 // initial PCM output buffer
|
||||
#define HTTP_RX_BUFFER 2048 // esp_http_client receive buffer
|
||||
@@ -59,11 +60,14 @@ static volatile bool s_stop; // request both tasks to unwind
|
||||
static volatile bool s_http_running; // HTTP task alive
|
||||
static volatile bool s_dec_running; // decode task alive
|
||||
static volatile bool s_playing; // public "is playing" state
|
||||
static volatile uint32_t s_gen; // bumped per play(); old-session tasks bail on mismatch
|
||||
|
||||
static StreamBufferHandle_t s_net_buf; // compressed MP3 bytes: HTTP -> decoder
|
||||
static TaskHandle_t s_http_task;
|
||||
static TaskHandle_t s_dec_task;
|
||||
static char s_url[512];
|
||||
static esp_http_client_handle_t s_client; // live client; closed by stop() to unblock recv
|
||||
static SemaphoreHandle_t s_client_mux; // guards s_client across tasks
|
||||
|
||||
// Register the MP3 frame decoder + simple-decoder container parsers once.
|
||||
static void register_decoders_once(void)
|
||||
@@ -88,12 +92,13 @@ static void register_decoders_once(void)
|
||||
static esp_err_t http_event(esp_http_client_event_t *evt)
|
||||
{
|
||||
if (evt->event_id == HTTP_EVENT_ON_DATA && evt->data_len > 0) {
|
||||
static bool first = false;
|
||||
if (!first) { first = true; lisael_udp_send("RADIO: first net data, len=%d", evt->data_len); }
|
||||
const uint8_t *p = (const uint8_t *)evt->data;
|
||||
size_t remaining = (size_t)evt->data_len;
|
||||
while (remaining > 0) {
|
||||
if (s_stop) {
|
||||
// Abort if a stop was requested OR a newer session has taken over
|
||||
// (gen mismatch) — never let an old task become a 2nd StreamBuffer
|
||||
// sender, which trips a FreeRTOS assert (-> reboot).
|
||||
if (s_stop || (uint32_t)(intptr_t)evt->user_data != s_gen) {
|
||||
return ESP_FAIL; // abort perform() -> HTTP task returns
|
||||
}
|
||||
size_t sent = xStreamBufferSend(s_net_buf, p, remaining,
|
||||
@@ -101,8 +106,7 @@ static esp_err_t http_event(esp_http_client_event_t *evt)
|
||||
p += sent;
|
||||
remaining -= sent;
|
||||
// If sent < remaining we timed out with the buffer full (decoder
|
||||
// stalled / paused) — loop again, re-checking s_stop. No spin: the
|
||||
// send call itself blocks for up to NET_CHUNK_TIMEOUT_MS.
|
||||
// stalled / paused) — loop again, re-checking the guards. No spin.
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
@@ -110,13 +114,13 @@ static esp_err_t http_event(esp_http_client_event_t *evt)
|
||||
|
||||
static void http_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
s_http_running = true;
|
||||
ESP_LOGI(TAG, "http: GET %s", s_url);
|
||||
|
||||
esp_http_client_config_t cfg = {
|
||||
.url = s_url,
|
||||
.event_handler = http_event,
|
||||
.user_data = arg, // this session's generation (see http_event)
|
||||
.timeout_ms = 15000,
|
||||
.buffer_size = HTTP_RX_BUFFER,
|
||||
// Follow 3xx automatically: RadioKing /play/<slug> endpoints redirect to
|
||||
@@ -131,14 +135,23 @@ static void http_task(void *arg)
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||
if (client) {
|
||||
lisael_udp_send("RADIO: http perform start");
|
||||
// Publish the handle so stop() can close the socket and unblock a recv
|
||||
// that would otherwise sit until the socket timeout.
|
||||
xSemaphoreTake(s_client_mux, portMAX_DELAY);
|
||||
s_client = client;
|
||||
xSemaphoreGive(s_client_mux);
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
int status = esp_http_client_get_status_code(client);
|
||||
lisael_udp_send("RADIO: http ended err=%s status=%d",
|
||||
esp_err_to_name(err), status);
|
||||
if (err != ESP_OK && !s_stop) {
|
||||
ESP_LOGW(TAG, "http perform ended: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Reclaim the handle under the lock before freeing it, so stop() never
|
||||
// calls close() on a handle we are cleaning up.
|
||||
xSemaphoreTake(s_client_mux, portMAX_DELAY);
|
||||
s_client = NULL;
|
||||
xSemaphoreGive(s_client_mux);
|
||||
esp_http_client_cleanup(client);
|
||||
} else {
|
||||
lisael_udp_send("RADIO: http client init FAILED");
|
||||
@@ -158,7 +171,7 @@ static void http_task(void *arg)
|
||||
// ----------------------------------------------------------------------------
|
||||
static void decode_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
uint32_t mygen = (uint32_t)(intptr_t)arg;
|
||||
s_dec_running = true;
|
||||
register_decoders_once();
|
||||
|
||||
@@ -190,7 +203,7 @@ static void decode_task(void *arg)
|
||||
|
||||
ESP_LOGI(TAG, "decode task started");
|
||||
|
||||
while (!s_stop) {
|
||||
while (!s_stop && mygen == s_gen) {
|
||||
// Block until at least 1 byte is available (or timeout). When the HTTP
|
||||
// task has exited and drained the buffer this returns 0 -> we leave.
|
||||
size_t n = xStreamBufferReceive(s_net_buf, inbuf, DEC_READ_CHUNK,
|
||||
@@ -209,7 +222,7 @@ static void decode_task(void *arg)
|
||||
.len = (uint32_t)n,
|
||||
.eos = false,
|
||||
};
|
||||
while (raw.len > 0 && !s_stop) {
|
||||
while (raw.len > 0 && !s_stop && mygen == s_gen) {
|
||||
esp_audio_simple_dec_out_t frame = { .buffer = out, .len = out_cap };
|
||||
esp_audio_err_t e = esp_audio_simple_dec_process(dec, &raw, &frame);
|
||||
if (e == ESP_AUDIO_ERR_BUFF_NOT_ENOUGH) {
|
||||
@@ -301,6 +314,23 @@ esp_err_t lisael_radio_play(const char *url)
|
||||
lisael_radio_stop();
|
||||
lisael_audio_stop_all();
|
||||
|
||||
// Wait until the previous session's tasks are COMPLETELY gone before reusing
|
||||
// the shared StreamBuffer/codec. Otherwise an old task could still be a
|
||||
// sender/receiver on the buffer when the new one starts -> two senders ->
|
||||
// FreeRTOS assert -> reboot (the exact "switch station without Stop" crash).
|
||||
// The short NET/DEC timeouts make this quick.
|
||||
for (int i = 0; i < 200 && (s_http_running || s_dec_running); i++) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
if (s_http_running || s_dec_running) {
|
||||
ESP_LOGW(TAG, "previous radio session still winding down — not starting");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (!s_client_mux) {
|
||||
s_client_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
// Lazily create the network->decoder StreamBuffer IN PSRAM: it is ~32 KB
|
||||
// and internal RAM is nearly exhausted (xStreamBufferCreate would fail with
|
||||
// NO_MEM, which is exactly why the radio never started).
|
||||
@@ -319,6 +349,7 @@ esp_err_t lisael_radio_play(const char *url)
|
||||
strlcpy(s_url, url, sizeof(s_url));
|
||||
s_stop = false;
|
||||
s_playing = true;
|
||||
uint32_t gen = ++s_gen; // this session's id; old tasks bail on mismatch
|
||||
lisael_audio_set_source(LISAEL_AUDIO_RADIO);
|
||||
|
||||
// Priority 4 == LVGL task priority. We MUST NOT run above the UI task or a
|
||||
@@ -327,10 +358,10 @@ esp_err_t lisael_radio_play(const char *url)
|
||||
// responsive without preempting LVGL's own time slice. pin to no core so
|
||||
// the scheduler can balance with Wi-Fi/LVGL.
|
||||
BaseType_t ok1 = xTaskCreatePinnedToCoreWithCaps(decode_task, "radio_dec", 8192,
|
||||
NULL, 4, &s_dec_task,
|
||||
(void *)(intptr_t)gen, 4, &s_dec_task,
|
||||
tskNO_AFFINITY, MALLOC_CAP_SPIRAM);
|
||||
BaseType_t ok2 = xTaskCreatePinnedToCoreWithCaps(http_task, "radio_http", 8192,
|
||||
NULL, 4, &s_http_task,
|
||||
(void *)(intptr_t)gen, 4, &s_http_task,
|
||||
tskNO_AFFINITY, MALLOC_CAP_SPIRAM);
|
||||
if (ok1 != pdPASS || ok2 != pdPASS) {
|
||||
ESP_LOGE(TAG, "task create failed");
|
||||
@@ -351,6 +382,16 @@ void lisael_radio_stop(void)
|
||||
}
|
||||
|
||||
s_stop = true;
|
||||
// Close the live HTTP socket so a perform() blocked in recv() returns at
|
||||
// once (otherwise it lingers until the socket timeout, and a fast station
|
||||
// switch would start a 2nd session on the shared buffer -> assert/reboot).
|
||||
if (s_client_mux) {
|
||||
xSemaphoreTake(s_client_mux, portMAX_DELAY);
|
||||
if (s_client) {
|
||||
esp_http_client_close(s_client);
|
||||
}
|
||||
xSemaphoreGive(s_client_mux);
|
||||
}
|
||||
// Nudge the decode task in case it is blocked on an empty StreamBuffer.
|
||||
if (s_net_buf) {
|
||||
xStreamBufferReset(s_net_buf);
|
||||
|
||||
Reference in New Issue
Block a user