OTA: quiet the audio sources during a firmware upload

An upload during an active Spotify session crawled (~10 kB/s, 197 s)
and ended in a reset (seen twice). Workaround:
- aes67_ota_on_update(cb): cb(true) right before an accepted upload
  writes flash, cb(false) if it then fails.
- The player pauses Spotify (spotify_pause -> SpircHandler::setPause, the
  app follows) and suspends HLS fetching (hls_suspend); resumed if the
  upload fails. AES67 TX keeps running (silence).
- Verified: upload during Spotify playback paused the session and
  installed in 25.8 s (normal for 2 MB), clean reboot and self-test.
  Root cause still open (noted in CLAUDE.md with the other cspot items).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 19:02:03 +10:00
parent cd747aec40
commit 8ba4de5eda
8 changed files with 90 additions and 2 deletions
+6 -1
View File
@@ -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). - 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. - 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. - 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 - [ ] failover (auto mode) + /api/player
- [ ] 8. Mono sum, gain, polish. - [ ] 8. Mono sum, gain, polish.
+28
View File
@@ -23,6 +23,26 @@
static const char *TAG = "ota"; 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) static bool is_pending(void)
{ {
esp_ota_img_states_t st; 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; esp_ota_handle_t ota = 0;
size_t total = 0, fill = 0; size_t total = 0, fill = 0;
const char *err_msg = NULL; const char *err_msg = NULL;
bool notified = false;
int64_t t0 = esp_timer_get_time(); int64_t t0 = esp_timer_get_time();
while (total < req->content_len) { 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); 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); 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) { if (esp_ota_begin(dst, OTA_WITH_SEQUENTIAL_WRITES, &ota) != ESP_OK) {
err_msg = "ota begin failed"; err_msg = "ota begin failed";
break; break;
@@ -154,15 +177,20 @@ static esp_err_t ota_post(httpd_req_t *req)
if (ota) { if (ota) {
esp_ota_abort(ota); esp_ota_abort(ota);
} }
if (notified) {
notify_update(false);
}
// Rejected before the whole body was read: close instead of draining it. // Rejected before the whole body was read: close instead of draining it.
httpd_resp_set_hdr(req, "Connection", "close"); httpd_resp_set_hdr(req, "Connection", "close");
return reject(req, err_msg); return reject(req, err_msg);
} }
esp_err_t err = esp_ota_end(ota); // verifies the image (checksum/hash, chip) esp_err_t err = esp_ota_end(ota); // verifies the image (checksum/hash, chip)
if (err != ESP_OK) { if (err != ESP_OK) {
notify_update(false);
return reject(req, err == ESP_ERR_OTA_VALIDATE_FAILED ? "image verification failed" : "ota end failed"); return reject(req, err == ESP_ERR_OTA_VALIDATE_FAILED ? "image verification failed" : "ota end failed");
} }
if (esp_ota_set_boot_partition(dst) != ESP_OK) { 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"); 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, ESP_LOGW(TAG, "installed to %s in %.1f s, rebooting", dst->label,
+8
View File
@@ -2,8 +2,16 @@
// Core component: must not depend on main/ (project code). // Core component: must not depend on main/ (project code).
#pragma once #pragma once
#include <stdbool.h>
#include "esp_err.h" #include "esp_err.h"
// Registers /api/ota routes. If the running image is pending verification, starts the // 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. // self-test: IP address + own web server answering within 60 s, else roll back.
esp_err_t aes67_ota_init(void); 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);
+3
View File
@@ -1,6 +1,7 @@
// Spotify Connect (cspot). Project code; GPL-3.0 through cspot. // Spotify Connect (cspot). Project code; GPL-3.0 through cspot.
#pragma once #pragma once
#include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@@ -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 // 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. // 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); 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. // Current state for status.spotify_state.
const char *spotify_state(void); const char *spotify_state(void);
+12
View File
@@ -30,9 +30,19 @@ static std::shared_ptr<cspot::LoginBlob> s_blob;
static std::atomic<bool> s_got_blob{false}; static std::atomic<bool> s_got_blob{false};
static std::atomic<const char *> s_state{"disabled"}; static std::atomic<const char *> s_state{"disabled"};
static std::atomic<uint64_t> s_pcm_bytes{0}; static std::atomic<uint64_t> s_pcm_bytes{0};
static std::shared_ptr<cspot::SpircHandler> s_handler; // running session, else null
static spotify_pcm_cb_t s_pcm_cb; static spotify_pcm_cb_t s_pcm_cb;
static spotify_event_cb_t s_event_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) const char *spotify_state(void)
{ {
return s_state.load(); return s_state.load();
@@ -225,6 +235,7 @@ static void session(void)
} }
ESP_LOGI(TAG, "event %s", event_name(ev->eventType)); ESP_LOGI(TAG, "event %s", event_name(ev->eventType));
}); });
s_handler = handler;
s_state = "connected"; s_state = "connected";
ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str()); ESP_LOGI(TAG, "connected as %s", ctx->config.username.c_str());
while (true) { while (true) {
@@ -246,6 +257,7 @@ static void spotify_task(void *arg)
s_state = "error"; s_state = "error";
ESP_LOGE(TAG, "session ended: %s", e.what()); ESP_LOGE(TAG, "session ended: %s", e.what());
} }
s_handler.reset();
vTaskDelay(pdMS_TO_TICKS(2000)); vTaskDelay(pdMS_TO_TICKS(2000));
} }
} }
+10 -1
View File
@@ -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_url[URL_MAX]; // configured URL
static char s_media_url[URL_MAX]; static char s_media_url[URL_MAX];
static uint8_t *s_seg; // current segment (PSRAM) 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 "". // hls_url when source.mode needs HLS, else "".
static void wanted_url(char *out, size_t n) 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) static bool still_wanted(void)
{ {
if (s_suspended) {
return false;
}
char u[URL_MAX]; char u[URL_MAX];
wanted_url(u, sizeof(u)); wanted_url(u, sizeof(u));
return strcmp(u, s_url) == 0; return strcmp(u, s_url) == 0;
@@ -237,7 +246,7 @@ static void hls_task(void *arg)
{ {
while (1) { while (1) {
wanted_url(s_url, sizeof(s_url)); wanted_url(s_url, sizeof(s_url));
if (!s_url[0]) { if (!s_url[0] || s_suspended) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
continue; continue;
} }
+2
View File
@@ -11,3 +11,5 @@
typedef bool (*hls_sink_t)(const uint8_t *data, size_t len, bool segment_start); typedef bool (*hls_sink_t)(const uint8_t *data, size_t len, bool segment_start);
esp_err_t hls_start(hls_sink_t sink); esp_err_t hls_start(hls_sink_t sink);
// Stop fetching (current download is abandoned) until resumed.
void hls_suspend(bool suspend);
+21
View File
@@ -4,6 +4,7 @@
#include <string.h> #include <string.h>
#include "aes67_cfg.h" #include "aes67_cfg.h"
#include "aes67_ota.h"
#include "aes67_tx.h" #include "aes67_tx.h"
#include "aes67_web.h" #include "aes67_web.h"
#include "audio_out.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) static void player_status(cJSON *st)
{ {
const char *state = s_src == PLAYER_SRC_TONE ? "playing" : s_src == PLAYER_SRC_OFF ? "idle" : 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; return ESP_ERR_NO_MEM;
} }
spotify_init(spotify_pcm, spotify_event); spotify_init(spotify_pcm, spotify_event);
aes67_ota_on_update(on_ota);
err = hls_start(decoder_feed); err = hls_start(decoder_feed);
if (err != ESP_OK) { if (err != ESP_OK) {
return err; return err;