diff --git a/CLAUDE.md b/CLAUDE.md index c1c6102..e3baee3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,12 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4 - Download speed ~1.4 Mbit/s over TLS (fine for ~250 kbit/s; tune buffer sizes / per-chunk overhead). - Not yet tested: HE-AAC variant (140k), other stations, fMP4/ADTS-only playlists, discontinuities (#EXT-X-DISCONTINUITY), network loss and recovery, long runs. - Audio starts only after PTP lock (~20 s after boot): intended, TX needs PTP. - - [ ] cspot (Spotify Connect) + - [ ] cspot (Spotify Connect): login, audio, pause/skip/seek and app volume work (7.5b). Open: + - OTA during an active Spotify session made the upload crawl (~10 kB/s) and ended in a reset; worked around by pausing Spotify / suspending HLS for the upload (aes67_ota_on_update). Root cause not found (suspect: flash writes stall PSRAM-heavy tasks). Watch for similar stalls. + - First connects sometimes fail ("Can't connect to spotify servers"), a retry works. + - Internal heap drops from ~408 KB to ~232 KB with a session; check what can move to PSRAM. + - source.mode changes only start/stop Spotify at boot; in hls mode a running session's audio is dropped (it races through the queue). + - Mute at 0 % volume not yet confirmed. - [ ] failover (auto mode) + /api/player - [ ] 8. Mono sum, gain, polish. diff --git a/components/aes67_ota/aes67_ota.c b/components/aes67_ota/aes67_ota.c index 81d6403..beaee87 100644 --- a/components/aes67_ota/aes67_ota.c +++ b/components/aes67_ota/aes67_ota.c @@ -23,6 +23,26 @@ static const char *TAG = "ota"; +#define MAX_UPDATE_CBS 4 +static aes67_ota_update_cb_t s_update_cbs[MAX_UPDATE_CBS]; +static int s_update_n; + +esp_err_t aes67_ota_on_update(aes67_ota_update_cb_t cb) +{ + if (s_update_n >= MAX_UPDATE_CBS) { + return ESP_ERR_NO_MEM; + } + s_update_cbs[s_update_n++] = cb; + return ESP_OK; +} + +static void notify_update(bool starting) +{ + for (int i = 0; i < s_update_n; i++) { + s_update_cbs[i](starting); + } +} + static bool is_pending(void) { esp_ota_img_states_t st; @@ -114,6 +134,7 @@ static esp_err_t ota_post(httpd_req_t *req) esp_ota_handle_t ota = 0; size_t total = 0, fill = 0; const char *err_msg = NULL; + bool notified = false; int64_t t0 = esp_timer_get_time(); while (total < req->content_len) { @@ -137,6 +158,8 @@ static esp_err_t ota_post(httpd_req_t *req) } const esp_app_desc_t *d = (const esp_app_desc_t *)(buf + DESC_OFFSET); ESP_LOGI(TAG, "image %.32s %.32s", d->project_name, d->version); + notify_update(true); // project quiets streaming/decoding before the flash writes + notified = true; if (esp_ota_begin(dst, OTA_WITH_SEQUENTIAL_WRITES, &ota) != ESP_OK) { err_msg = "ota begin failed"; break; @@ -154,15 +177,20 @@ static esp_err_t ota_post(httpd_req_t *req) if (ota) { esp_ota_abort(ota); } + if (notified) { + notify_update(false); + } // Rejected before the whole body was read: close instead of draining it. httpd_resp_set_hdr(req, "Connection", "close"); return reject(req, err_msg); } esp_err_t err = esp_ota_end(ota); // verifies the image (checksum/hash, chip) if (err != ESP_OK) { + notify_update(false); return reject(req, err == ESP_ERR_OTA_VALIDATE_FAILED ? "image verification failed" : "ota end failed"); } if (esp_ota_set_boot_partition(dst) != ESP_OK) { + notify_update(false); return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "could not set boot partition"); } ESP_LOGW(TAG, "installed to %s in %.1f s, rebooting", dst->label, diff --git a/components/aes67_ota/include/aes67_ota.h b/components/aes67_ota/include/aes67_ota.h index 3c134aa..a33e476 100644 --- a/components/aes67_ota/include/aes67_ota.h +++ b/components/aes67_ota/include/aes67_ota.h @@ -2,8 +2,16 @@ // Core component: must not depend on main/ (project code). #pragma once +#include + #include "esp_err.h" // Registers /api/ota routes. If the running image is pending verification, starts the // self-test: IP address + own web server answering within 60 s, else roll back. esp_err_t aes67_ota_init(void); + +// Called with true right before an accepted upload starts writing flash, and with false if that +// upload then fails (on success the device reboots). Lets the project quiet heavy work (streaming, +// decoding) during the flash writes. Up to 4 callbacks. +typedef void (*aes67_ota_update_cb_t)(bool starting); +esp_err_t aes67_ota_on_update(aes67_ota_update_cb_t cb); diff --git a/components/spotify/include/spotify.h b/components/spotify/include/spotify.h index 5fde0e5..cf76032 100644 --- a/components/spotify/include/spotify.h +++ b/components/spotify/include/spotify.h @@ -1,6 +1,7 @@ // Spotify Connect (cspot). Project code; GPL-3.0 through cspot. #pragma once +#include #include #include @@ -24,6 +25,8 @@ typedef void (*spotify_event_cb_t)(spotify_event_t ev, int value); // Registers the zeroconf endpoints (/spotify_info) and, when source.mode is spotify/auto and the // client credentials are set, advertises the device and starts the session task. esp_err_t spotify_init(spotify_pcm_cb_t pcm, spotify_event_cb_t event); +// Pause/resume the running session (the Spotify app follows). No-op without a session. +void spotify_pause(bool pause); // Current state for status.spotify_state. const char *spotify_state(void); diff --git a/components/spotify/spotify.cpp b/components/spotify/spotify.cpp index 291d777..d1dc881 100644 --- a/components/spotify/spotify.cpp +++ b/components/spotify/spotify.cpp @@ -30,9 +30,19 @@ static std::shared_ptr s_blob; static std::atomic s_got_blob{false}; static std::atomic s_state{"disabled"}; static std::atomic s_pcm_bytes{0}; +static std::shared_ptr s_handler; // running session, else null static spotify_pcm_cb_t s_pcm_cb; static spotify_event_cb_t s_event_cb; +extern "C" void spotify_pause(bool pause) +{ + auto h = s_handler; + if (h) { + h->setPause(pause); + ESP_LOGI(TAG, "%s", pause ? "paused" : "resumed"); + } +} + const char *spotify_state(void) { return s_state.load(); @@ -225,6 +235,7 @@ static void session(void) } ESP_LOGI(TAG, "event %s", event_name(ev->eventType)); }); + s_handler = handler; s_state = "connected"; ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str()); while (true) { @@ -246,6 +257,7 @@ static void spotify_task(void *arg) s_state = "error"; ESP_LOGE(TAG, "session ended: %s", e.what()); } + s_handler.reset(); vTaskDelay(pdMS_TO_TICKS(2000)); } } diff --git a/main/hls.c b/main/hls.c index 24825ed..5a8a3b9 100644 --- a/main/hls.c +++ b/main/hls.c @@ -36,6 +36,12 @@ static media_pl_t *s_pl; // parsed media playlist (PSRAM) static char s_url[URL_MAX]; // configured URL static char s_media_url[URL_MAX]; static uint8_t *s_seg; // current segment (PSRAM) +static volatile bool s_suspended; + +void hls_suspend(bool suspend) +{ + s_suspended = suspend; +} // hls_url when source.mode needs HLS, else "". static void wanted_url(char *out, size_t n) @@ -50,6 +56,9 @@ static void wanted_url(char *out, size_t n) static bool still_wanted(void) { + if (s_suspended) { + return false; + } char u[URL_MAX]; wanted_url(u, sizeof(u)); return strcmp(u, s_url) == 0; @@ -237,7 +246,7 @@ static void hls_task(void *arg) { while (1) { wanted_url(s_url, sizeof(s_url)); - if (!s_url[0]) { + if (!s_url[0] || s_suspended) { vTaskDelay(pdMS_TO_TICKS(1000)); continue; } diff --git a/main/hls.h b/main/hls.h index 0c791c4..ed55de5 100644 --- a/main/hls.h +++ b/main/hls.h @@ -11,3 +11,5 @@ typedef bool (*hls_sink_t)(const uint8_t *data, size_t len, bool segment_start); esp_err_t hls_start(hls_sink_t sink); +// Stop fetching (current download is abandoned) until resumed. +void hls_suspend(bool suspend); diff --git a/main/player.c b/main/player.c index 3646529..e11b1cf 100644 --- a/main/player.c +++ b/main/player.c @@ -4,6 +4,7 @@ #include #include "aes67_cfg.h" +#include "aes67_ota.h" #include "aes67_tx.h" #include "aes67_web.h" #include "audio_out.h" @@ -156,6 +157,25 @@ static void spotify_event(spotify_event_t ev, int value) } } +// Firmware upload: flash writes stall everything running from PSRAM; a Spotify session +// streaming at the same time made uploads crawl and end in a reset. Quiet the sources meanwhile. +static void on_ota(bool starting) +{ + static bool paused_spotify; + if (starting) { + paused_spotify = s_src == PLAYER_SRC_SPOTIFY && !s_paused; + spotify_pause(true); + hls_suspend(true); + ESP_LOGI(TAG, "firmware update: sources paused"); + } else { + hls_suspend(false); + if (paused_spotify) { + spotify_pause(false); + } + ESP_LOGI(TAG, "firmware update failed: sources resumed"); + } +} + static void player_status(cJSON *st) { const char *state = s_src == PLAYER_SRC_TONE ? "playing" : s_src == PLAYER_SRC_OFF ? "idle" : @@ -186,6 +206,7 @@ esp_err_t player_init(void) return ESP_ERR_NO_MEM; } spotify_init(spotify_pcm, spotify_event); + aes67_ota_on_update(on_ota); err = hls_start(decoder_feed); if (err != ESP_OK) { return err;