Skip to content

Commit ce45e6c

Browse files
feat(ota): manual update check + robust release parse, friendlier status (1.0.7)
Config-page "Check for updates now" button posts /api/ota/check, which sets ota_check_now; otaAutoService() on Core 0 honors it immediately, bypassing the 6-hour timer (consumed only once the device is idle so it never interrupts a pass/stream/upload). Buffer the GitHub release JSON with getString() before parsing instead of deserializing straight off the TLS stream. A mid-transfer stall -- seen on the check that runs right after an STM flash -- was surfacing as a bare "bad API response"; the body is now read whole within the timeout, and any real parse failure reports the ArduinoJson error detail (and is logged with the body length) so it can't be mistaken for "up to date". Reword the result: both MCUs current -> "Up to date (ESP x, STM y)"; something newer -> "Update available: ESP ... | STM ...". Bump FW_VERSION 1.0.6 -> 1.0.7. Verified on hardware: repeated manual checks report "Up to date (ESP 1.0.7, STM 1.0.6)" with no bad-response flicker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 86e9121 commit ce45e6c

4 files changed

Lines changed: 59 additions & 15 deletions

File tree

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/platformio.ini‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ build_flags =
6363
-DARDUINO_USB_CDC_ON_BOOT=1
6464
; ESP32 firmware version: reported in the heartbeat and used by the OTA
6565
; flow to decide whether this board needs updating. Bump on every release.
66-
'-DFW_VERSION="1.0.6"'
66+
'-DFW_VERSION="1.0.7"'
6767
; Bootstrap token for register_device/capture transport auth. Sourced from
6868
; the environment so the secret is never committed: set MSC_BOOTSTRAP_TOKEN
6969
; before building (PowerShell: $env:MSC_BOOTSTRAP_TOKEN="..."; bash:

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/src/config_portal.h‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ button.warn{background:#3a2326;color:#f87171;border:1px solid #5b2a2e}
129129
<div class=row><span class=k>Boot count</span><span class=v id=bootCount>&mdash;</span></div>
130130
<div class=row><span class=k>Firmware</span><span class=v id=fwVersion>&mdash;</span></div>
131131
<div class=row><span class=k>Update status</span><span class=v id=otaStatus>&mdash;</span></div>
132+
<button onclick=checkUpdate()>Check for updates now</button>
132133
</div>
133134
134135
<div class=toast id=toast></div>
@@ -186,6 +187,10 @@ async function clearWifi(){
186187
await post('/api/clear',{});
187188
document.body.innerHTML='<h1>Cleared. Rebooting&hellip;</h1><p class=sub>Join the MiniSpeedCam WiFi to reconfigure.</p>';
188189
}
190+
async function checkUpdate(){
191+
await post('/api/ota/check',{});
192+
toast('Checking GitHub for updates...');
193+
}
189194
function syncStream(on){
190195
if(on===streamShown)return;
191196
const img=$('streamImg');
@@ -401,6 +406,16 @@ static esp_err_t portalStreamPost(httpd_req_t* req) {
401406
return httpd_resp_sendstr(req, "{\"ok\":true}");
402407
}
403408

409+
// POST /api/ota/check -> request an immediate GitHub update check. Only sets the
410+
// flag; taskCore0's otaAutoService() runs the check (and applies any newer image)
411+
// the next time it's idle, bypassing the periodic timer. The result appears in
412+
// the "Update status" line the page already polls.
413+
static esp_err_t portalOtaCheckPost(httpd_req_t* req) {
414+
ota_check_now = true;
415+
httpd_resp_set_type(req, "application/json");
416+
return httpd_resp_sendstr(req, "{\"ok\":true}");
417+
}
418+
404419
static httpd_handle_t config_httpd = NULL;
405420

406421
/**
@@ -417,7 +432,7 @@ void load_config_portal(void) {
417432
// /api/state polls; the old separate :81 stream server is gone (its sockets
418433
// freed), so this single server can afford more open sockets.
419434
config.max_open_sockets = 7;
420-
config.max_uri_handlers = 8;
435+
config.max_uri_handlers = 10;
421436
config.lru_purge_enable = true;
422437
config.stack_size = 8192; // headroom for ArduinoJson + String building
423438

@@ -432,12 +447,14 @@ void load_config_portal(void) {
432447
httpd_uri_t u_wifi = { .uri = "/api/wifi", .method = HTTP_POST, .handler = portalWifiPost, .user_ctx = NULL };
433448
httpd_uri_t u_clear = { .uri = "/api/clear", .method = HTTP_POST, .handler = portalClearPost, .user_ctx = NULL };
434449
httpd_uri_t u_stream = { .uri = "/api/stream", .method = HTTP_POST, .handler = portalStreamPost, .user_ctx = NULL };
450+
httpd_uri_t u_otachk = { .uri = "/api/ota/check",.method = HTTP_POST, .handler = portalOtaCheckPost, .user_ctx = NULL };
435451
httpd_register_uri_handler(config_httpd, &u_root);
436452
httpd_register_uri_handler(config_httpd, &u_state);
437453
httpd_register_uri_handler(config_httpd, &u_set);
438454
httpd_register_uri_handler(config_httpd, &u_wifi);
439455
httpd_register_uri_handler(config_httpd, &u_clear);
440456
httpd_register_uri_handler(config_httpd, &u_stream);
457+
httpd_register_uri_handler(config_httpd, &u_otachk);
441458

442459
// The aiming MJPEG stream shares this same server (GET /stream), shown inline
443460
// in a small window on the config page. See camera_stream.cpp.

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/src/ota.h‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,21 @@ bool otaCheckGithub() {
122122
filter["tag_name"] = true;
123123
filter["assets"][0]["name"] = true;
124124
filter["assets"][0]["browser_download_url"] = true;
125-
JsonDocument doc;
126-
DeserializationError err = deserializeJson(doc, https.getStream(), DeserializationOption::Filter(filter));
125+
// Buffer the body before parsing. Reading the (large) release JSON straight
126+
// off the TLS stream is fragile -- a mid-transfer stall (seen on the check
127+
// that runs right after an STM flash) surfaces as "bad API response"
128+
// (IncompleteInput). getString() reads the whole body within the timeout so
129+
// the parser sees it intact; the error detail makes any recurrence concrete
130+
// (and is NOT an "up to date" signal).
131+
String body = https.getString();
127132
https.end();
128-
if (err) { otaSetStatus("check: bad API response"); return false; }
133+
JsonDocument doc;
134+
DeserializationError err = deserializeJson(doc, body, DeserializationOption::Filter(filter));
135+
if (err) {
136+
Serial.printf("[OTA] api parse failed: %s (body=%u B)\n", err.c_str(), (unsigned)body.length());
137+
otaSetStatus("check: bad API response (%s)", err.c_str());
138+
return false;
139+
}
129140

130141
tag = (const char*)(doc["tag_name"] | "");
131142
for (JsonObject a : doc["assets"].as<JsonArray>()) {
@@ -171,12 +182,18 @@ bool otaCheckGithub() {
171182
// a valid health signal for the post-update watchdog (otaHealthService()).
172183
g_cloud_ok = true;
173184

174-
char espinfo[44], stminfo[44];
175-
if (ota_esp_available) snprintf(espinfo, sizeof espinfo, "%s->%s", FW_VERSION, ota_esp_version.c_str());
176-
else snprintf(espinfo, sizeof espinfo, "%s ok", FW_VERSION);
177-
if (ota_stm_available) snprintf(stminfo, sizeof stminfo, "%s->%s", stm_fw_version.c_str(), ota_stm_version.c_str());
178-
else snprintf(stminfo, sizeof stminfo, "%s ok", stm_fw_version.c_str());
179-
otaSetStatus("ESP %s | STM %s", espinfo, stminfo);
185+
if (!ota_esp_available && !ota_stm_available) {
186+
// Both current -- the common, friendly "nothing to do" result.
187+
otaSetStatus("Up to date (ESP %s, STM %s)", FW_VERSION, stm_fw_version.c_str());
188+
} else {
189+
// Something newer is available (and about to be applied below) -- show what.
190+
char espinfo[44], stminfo[44];
191+
if (ota_esp_available) snprintf(espinfo, sizeof espinfo, "%s->%s", FW_VERSION, ota_esp_version.c_str());
192+
else snprintf(espinfo, sizeof espinfo, "%s ok", FW_VERSION);
193+
if (ota_stm_available) snprintf(stminfo, sizeof stminfo, "%s->%s", stm_fw_version.c_str(), ota_stm_version.c_str());
194+
else snprintf(stminfo, sizeof stminfo, "%s ok", stm_fw_version.c_str());
195+
otaSetStatus("Update available: ESP %s | STM %s", espinfo, stminfo);
196+
}
180197
return true;
181198
}
182199

@@ -398,15 +415,21 @@ void otaAutoService() {
398415

399416
if (stm_flash_busy) return;
400417
if (WiFi.getMode() != WIFI_STA || WiFi.status() != WL_CONNECTED) return;
401-
if ((long)(millis() - nextCheck) < 0) return;
402418

403-
// Only act when idle. Don't advance the timer if busy, so we retry promptly
404-
// once the current pass/stream/upload finishes (passes last only seconds).
419+
// A manual "check now" (config-page button) bypasses the interval timer; the
420+
// periodic check otherwise waits for nextCheck to elapse.
421+
bool manual = ota_check_now;
422+
if (!manual && (long)(millis() - nextCheck) < 0) return;
423+
424+
// Only act when idle. Don't advance the timer or consume the manual request if
425+
// busy, so it retries promptly once the current pass/stream/upload finishes
426+
// (passes last only seconds).
405427
bool idle = !g_run_active && !stream_active &&
406428
(uploadQueue == nullptr || uxQueueMessagesWaiting(uploadQueue) == 0);
407429
if (!idle) return;
408430

409-
nextCheck = millis() + OTA_CHECK_INTERVAL_MS; // schedule the next check regardless of outcome
431+
ota_check_now = false; // consume the manual request (if any)
432+
nextCheck = millis() + OTA_CHECK_INTERVAL_MS; // schedule the next periodic check regardless of outcome
410433

411434
if (!otaCheckGithub()) return;
412435

‎version/r1.1/firmware/platformio/esp32_firmware_platformio/src/variables.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ const char* server_github_latest = "https://api.github.com/repos/" GITHUB_OWNER
189189
// URLs/MD5s and the *_available flags are touched only on Core 0, so they need
190190
// no guarding.
191191
char ota_status[96] = "idle";
192+
// Manual "check for updates now" request from the config-page button. Set by the
193+
// POST /api/ota/check handler (httpd task) and consumed by otaAutoService() on
194+
// Core 0, which then runs a check immediately instead of waiting for the timer.
195+
volatile bool ota_check_now = false;
192196
String ota_esp_version, ota_esp_url, ota_esp_md5;
193197
String ota_stm_version, ota_stm_url, ota_stm_md5;
194198
bool ota_esp_available = false;

0 commit comments

Comments
 (0)