From ab7eae3d94ec15037517c597fc65097719224074 Mon Sep 17 00:00:00 2001 From: cjkas Date: Tue, 20 Oct 2026 20:43:00 +0200 Subject: [PATCH 1/2] fix: download the per-chip LittleFS image in online updates Releases publish SomfyController.littlefs..bin, but the updater asked for SomfyController.littlefs.bin. The download 404'd and the device rebooted into the new firmware with the old web UI. Also: hasFS now matches per-chip images, C6 picks its own firmware file, and recoverFilesystem() points at cjkas instead of rstrouse. Co-Authored-By: Claude Opus 5.5 --- data-src/index.js | 2 +- src/GitOTA.cpp | 30 ++++++++++++------------------ src/GitOTA.h | 1 - src/Utils.cpp | 8 ++++++++ src/Utils.h | 6 ++++++ test/test_utils/test_utils.cpp | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/data-src/index.js b/data-src/index.js index 6dd60fd..2055e5e 100644 --- a/data-src/index.js +++ b/data-src/index.js @@ -4849,7 +4849,7 @@ class Firmware { let div = this.createFileUploader('/updateApplication'); general.reloadApp = true; let inst = div.querySelector('div[id=divInstText]'); - inst.innerHTML = '
Select a binary file [SomfyController.littlefs.bin] containing the littlefs data for the application then press the Upload File button.
'; + inst.innerHTML = '
Select a binary file [SomfyController.littlefs.<chip>.bin, e.g. SomfyController.littlefs.esp32.bin] containing the littlefs data for the application then press the Upload File button.
'; if (this.isMobile()) { inst.innerHTML += `
WARNING
`; inst.innerHTML += '
This browser does not support automatic backups. It is highly recommended that you back up your configuration using the backup button before proceeding.
'; diff --git a/src/GitOTA.cpp b/src/GitOTA.cpp index 9b13d9c..9391624 100644 --- a/src/GitOTA.cpp +++ b/src/GitOTA.cpp @@ -43,7 +43,7 @@ void GitRelease::setReleaseProperty(const char *key, const char *val) { void GitRelease::setAssetProperty(const char *key, const char *val) { if(strcmp(key, "name") == 0) { //Serial.println(val); - if(strstr(val, "littlefs.bin")) this->hasFS = true; + if(isFsAsset(val)) this->hasFS = true; else if(strstr(val, "ino.esp32.bin")) { if(strlen(this->hwVersions)) strcat(this->hwVersions, ","); strcat(this->hwVersions, "32"); @@ -353,22 +353,16 @@ void GitUpdater::emitDownloadProgress(uint8_t num, size_t total, size_t loaded, sockEmit.loop(); webServer.loop(); } -void GitUpdater::setFirmwareFile() { +// The chip part of this board's release asset names. +static const char *chipName() { esp_chip_info_t ci; esp_chip_info(&ci); switch(ci.model) { - case esp_chip_model_t::CHIP_ESP32S3: - strcpy(this->currentFile, "SomfyController.ino.esp32s3.bin"); - break; - case esp_chip_model_t::CHIP_ESP32S2: - strcpy(this->currentFile, "SomfyController.ino.esp32s2.bin"); - break; - case esp_chip_model_t::CHIP_ESP32C3: - strcpy(this->currentFile, "SomfyController.ino.esp32c3.bin"); - break; - default: - strcpy(this->currentFile, "SomfyController.ino.esp32.bin"); - break; + case esp_chip_model_t::CHIP_ESP32S3: return "esp32s3"; + case esp_chip_model_t::CHIP_ESP32S2: return "esp32s2"; + case esp_chip_model_t::CHIP_ESP32C3: return "esp32c3"; + case esp_chip_model_t::CHIP_ESP32C6: return "esp32c6"; + default: return "esp32"; } } @@ -379,14 +373,14 @@ bool GitUpdater::beginUpdate(const char *version) { strcpy(this->targetRelease, version); this->emitUpdateCheck(); - this->setFirmwareFile(); + releaseAssetName(this->currentFile, sizeof(this->currentFile), "ino", chipName()); this->partition = U_FLASH; this->lockFS = this->cancelled = false; this->error = 0; this->error = this->downloadFile(); if(this->error == 0 && !this->cancelled) { somfy.commit(); - strcpy(this->currentFile, "SomfyController.littlefs.bin"); + releaseAssetName(this->currentFile, sizeof(this->currentFile), "littlefs", chipName()); this->partition = U_SPIFFS; this->lockFS = true; this->error = this->downloadFile(); @@ -405,8 +399,8 @@ bool GitUpdater::beginUpdate(const char *version) { return true; } bool GitUpdater::recoverFilesystem() { - sprintf(this->baseUrl, "https://github.com/rstrouse/ESPSomfy-RTS/releases/download/%s/", settings.fwVersion.name); - strcpy(this->currentFile, "SomfyController.littlefs.bin"); + sprintf(this->baseUrl, "https://github.com/cjkas/ESPSomfy-RTS/releases/download/%s/", settings.fwVersion.name); + releaseAssetName(this->currentFile, sizeof(this->currentFile), "littlefs", chipName()); this->status = GIT_UPDATING; this->partition = U_SPIFFS; this->lockFS = true; diff --git a/src/GitOTA.h b/src/GitOTA.h index 6996f83..7a02112 100644 --- a/src/GitOTA.h +++ b/src/GitOTA.h @@ -55,7 +55,6 @@ class GitUpdater { bool beginUpdate(const char *release); bool endUpdate(); int8_t downloadFile(); - void setFirmwareFile(); void setCurrentRelease(GitRepo &repo); void loop(); diff --git a/src/Utils.cpp b/src/Utils.cpp index 75c77f0..2651f2a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -150,6 +150,14 @@ bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain) { bool otaProven(bool networkUp, unsigned long uptimeMs, unsigned long minUptimeMs) { return networkUp && uptimeMs >= minUptimeMs; } +bool releaseAssetName(char *buf, size_t size, const char *kind, const char *chip) { + int n = snprintf(buf, size, "SomfyController.%s.%s.bin", kind, chip); + return n >= 0 && (size_t)n < size; +} +bool isFsAsset(const char *name) { + size_t len = strlen(name); + return strstr(name, ".littlefs.") && len >= 4 && strcmp(name + len - 4, ".bin") == 0; +} const char *wifiAuthModeName(uint8_t authMode) { switch(authMode) { case 0: return "OPEN"; diff --git a/src/Utils.h b/src/Utils.h index 27c9ba9..723d9d5 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -86,6 +86,12 @@ heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalM // update has proven itself: it has run for minUptimeMs and the network is // up. Until then a reset rolls back to the previous firmware. bool otaProven(bool networkUp, unsigned long uptimeMs, unsigned long minUptimeMs); +// A release asset's name: "SomfyController...bin", kind "ino" for +// the firmware or "littlefs" for the web UI image, chip e.g. "esp32s3". False +// when it doesn't fit in size. +bool releaseAssetName(char *buf, size_t size, const char *kind, const char *chip); +// Whether a release asset is a LittleFS image, for any chip. +bool isFsAsset(const char *name); // How well loop() keeps up: the passes it made per second and its longest // pass, over the window since the last memStatus broadcast. Only the loop // task calls record(), rate(), maxUs() and publish(). publish() closes the diff --git a/test/test_utils/test_utils.cpp b/test/test_utils/test_utils.cpp index b17d0e0..2e5db6e 100644 --- a/test/test_utils/test_utils.cpp +++ b/test/test_utils/test_utils.cpp @@ -272,6 +272,34 @@ static void test_otaProven_not_without_network() { TEST_ASSERT_FALSE(otaProven(false, 3600000, 60000)); } +/********************************************************************* + * releaseAssetName / isFsAsset + ********************************************************************/ +static void test_releaseAssetName_matches_the_release_workflow() { + // .github/workflows/build.yaml fwname and fsname. + char buf[64]; + TEST_ASSERT_TRUE(releaseAssetName(buf, sizeof(buf), "ino", "esp32")); + TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32.bin", buf); + TEST_ASSERT_TRUE(releaseAssetName(buf, sizeof(buf), "littlefs", "esp32s3")); + TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32s3.bin", buf); +} +static void test_releaseAssetName_too_small_buffer() { + char buf[16]; + TEST_ASSERT_FALSE(releaseAssetName(buf, sizeof(buf), "littlefs", "esp32c6")); + TEST_ASSERT_EQUAL(sizeof(buf) - 1, strlen(buf)); +} +static void test_isFsAsset_per_chip_images() { + TEST_ASSERT_TRUE(isFsAsset("SomfyController.littlefs.esp32.bin")); + TEST_ASSERT_TRUE(isFsAsset("SomfyController.littlefs.esp32c6.bin")); + TEST_ASSERT_TRUE(isFsAsset("SomfyController.littlefs.bin")); +} +static void test_isFsAsset_not_the_other_assets() { + TEST_ASSERT_FALSE(isFsAsset("SomfyController.ino.esp32.bin")); + TEST_ASSERT_FALSE(isFsAsset("SomfyController.onboard.esp32.bin.zip")); + TEST_ASSERT_FALSE(isFsAsset("SomfyController.littlefs.bin.sha256")); + TEST_ASSERT_FALSE(isFsAsset("")); +} + /********************************************************************* * wifiAuthModeName ********************************************************************/ @@ -402,6 +430,10 @@ int main(int argc, char **argv) { RUN_TEST(test_otaProven_after_the_minimum_uptime_with_network); RUN_TEST(test_otaProven_not_before_the_minimum_uptime); RUN_TEST(test_otaProven_not_without_network); + RUN_TEST(test_releaseAssetName_matches_the_release_workflow); + RUN_TEST(test_releaseAssetName_too_small_buffer); + RUN_TEST(test_isFsAsset_per_chip_images); + RUN_TEST(test_isFsAsset_not_the_other_assets); RUN_TEST(test_wifiAuthModeName_matches_the_idf_enum); RUN_TEST(test_wifiAuthModeName_unknown_past_the_shared_values); RUN_TEST(test_heapEmitTarget_one_client_always_gets_it); From 8196bd20e6fe355a1b24ed737c55d55e9b3b04ec Mon Sep 17 00:00:00 2001 From: cjkas Date: Tue, 20 Oct 2026 21:34:16 +0200 Subject: [PATCH 2/2] refactor: name release assets from the board's chip model Review follow-up for #72. - releaseAssetName() takes ConfigSettings::chipModel ("", "s3", "c3", "c6"), so GitOTA no longer keeps its own chip switch. C3 keeps its name for when the build comes back. - hasFS is set only when this board's own LittleFS image is in the release. isFsAsset() is gone; it also matched the old unsuffixed littlefs.bin, which the updater no longer asks for. - releaseAssetName() returns void: no caller checked the result and every name fits currentFile. - Both manual-upload hints name this board's file. - KNOWN_ISSUES: the "Main" install can't work, the repo holds no .bin files. Co-Authored-By: Claude Opus 5.5 --- data-src/index.js | 6 ++++-- docs/KNOWN_ISSUES.md | 7 +++++++ src/GitOTA.cpp | 24 ++++++------------------ src/Utils.cpp | 9 ++------- src/Utils.h | 10 ++++------ test/test_utils/test_utils.cpp | 34 ++++++++++++---------------------- 6 files changed, 35 insertions(+), 55 deletions(-) diff --git a/data-src/index.js b/data-src/index.js index 2055e5e..06a09ec 100644 --- a/data-src/index.js +++ b/data-src/index.js @@ -4834,7 +4834,8 @@ class Firmware { updateFirmware() { let div = this.createFileUploader('/updateFirmware'); let inst = div.querySelector('div[id=divInstText]'); - let html = '
Select a binary file [SomfyController.ino.esp32.bin] containing the device firmware then press the Upload File button.
'; + let chip = document.getElementById('divContainer').getAttribute('data-chipmodel') || ''; + let html = `
Select a binary file [SomfyController.ino.esp32${chip}.bin] containing the device firmware then press the Upload File button.
`; if (this.isMobile()) { html += `
WARNING
`; html += '
This browser does not support automatic backups. It is highly recommended that you back up your configuration using the backup button before proceeding.
'; @@ -4849,7 +4850,8 @@ class Firmware { let div = this.createFileUploader('/updateApplication'); general.reloadApp = true; let inst = div.querySelector('div[id=divInstText]'); - inst.innerHTML = '
Select a binary file [SomfyController.littlefs.<chip>.bin, e.g. SomfyController.littlefs.esp32.bin] containing the littlefs data for the application then press the Upload File button.
'; + let chip = document.getElementById('divContainer').getAttribute('data-chipmodel') || ''; + inst.innerHTML = `
Select a binary file [SomfyController.littlefs.esp32${chip}.bin] containing the littlefs data for the application then press the Upload File button.
`; if (this.isMobile()) { inst.innerHTML += `
WARNING
`; inst.innerHTML += '
This browser does not support automatic backups. It is highly recommended that you back up your configuration using the backup button before proceeding.
'; diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index 402fe61..ed15935 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -267,3 +267,10 @@ On 2026-10-19 the log shows `web GET /controller from 192.168.1.197 total took 2 - **Fixed: a command for an unknown shade left no trace.** `handleShadeCommand`, `handleTiltCommand` and `handleGroupCommand` in [src/entryPoints/Web.cpp](../src/entryPoints/Web.cpp) now log a `not found` warning. On 2026-10-18 Home Assistant sent 10 commands to a device that had no shades yet, and the log showed only the response times. - **Fixed: `last ping 0ms ago` meant the client never pinged.** The disconnect line in [src/outputs/SocketEmitter.cpp](../src/outputs/SocketEmitter.cpp) now says `never pinged`. Browsers don't ping, so they always showed it. - **Open: `.gz does not exist` errors are noise.** `serveStatic` tries `.gz` first. For files stored without one (`appversion`, `icon.png`), LittleFS logs an `E`-level `does not exist, no permits for creation` line on every request. The fix would be for `minify.py` to gzip the plain files too. `setTryGzipFirst(false)` doesn't help: the error comes from `exists()` probing the missing variant, so it would only move the noise to the gzip-only files. + +## Installing "Main" from the update dialog always fails + +**Status:** open. Found 2026-10-20 while reviewing PR #72. +**Location:** `GitUpdater::beginUpdate` in [src/GitOTA.cpp](../src/GitOTA.cpp). + +The update dialog offers a "Main" entry next to the releases. For it, the updater downloads from `https://raw.githubusercontent.com/cjkas/ESPSomfy-RTS/master/`, but the repository holds no `.bin` files, so the firmware download 404s. Nothing is written and the device keeps running, so it does no harm, but the option can't work. Either drop the entry or point it at a build that publishes per-chip images. diff --git a/src/GitOTA.cpp b/src/GitOTA.cpp index 9391624..2c09b9f 100644 --- a/src/GitOTA.cpp +++ b/src/GitOTA.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include "esp_log.h" #include "ConfigSettings.h" #include "GitOTA.h" @@ -43,7 +42,9 @@ void GitRelease::setReleaseProperty(const char *key, const char *val) { void GitRelease::setAssetProperty(const char *key, const char *val) { if(strcmp(key, "name") == 0) { //Serial.println(val); - if(isFsAsset(val)) this->hasFS = true; + char fsFile[64]; + releaseAssetName(fsFile, sizeof(fsFile), "littlefs", settings.chipModel); + if(strcmp(val, fsFile) == 0) this->hasFS = true; else if(strstr(val, "ino.esp32.bin")) { if(strlen(this->hwVersions)) strcat(this->hwVersions, ","); strcat(this->hwVersions, "32"); @@ -353,19 +354,6 @@ void GitUpdater::emitDownloadProgress(uint8_t num, size_t total, size_t loaded, sockEmit.loop(); webServer.loop(); } -// The chip part of this board's release asset names. -static const char *chipName() { - esp_chip_info_t ci; - esp_chip_info(&ci); - switch(ci.model) { - case esp_chip_model_t::CHIP_ESP32S3: return "esp32s3"; - case esp_chip_model_t::CHIP_ESP32S2: return "esp32s2"; - case esp_chip_model_t::CHIP_ESP32C3: return "esp32c3"; - case esp_chip_model_t::CHIP_ESP32C6: return "esp32c6"; - default: return "esp32"; - } -} - bool GitUpdater::beginUpdate(const char *version) { ESP_LOGI(TAG, "Begin update called..."); if(strcmp(version, "Main") == 0) strcpy(this->baseUrl, "https://raw.githubusercontent.com/cjkas/ESPSomfy-RTS/master/"); @@ -373,14 +361,14 @@ bool GitUpdater::beginUpdate(const char *version) { strcpy(this->targetRelease, version); this->emitUpdateCheck(); - releaseAssetName(this->currentFile, sizeof(this->currentFile), "ino", chipName()); + releaseAssetName(this->currentFile, sizeof(this->currentFile), "ino", settings.chipModel); this->partition = U_FLASH; this->lockFS = this->cancelled = false; this->error = 0; this->error = this->downloadFile(); if(this->error == 0 && !this->cancelled) { somfy.commit(); - releaseAssetName(this->currentFile, sizeof(this->currentFile), "littlefs", chipName()); + releaseAssetName(this->currentFile, sizeof(this->currentFile), "littlefs", settings.chipModel); this->partition = U_SPIFFS; this->lockFS = true; this->error = this->downloadFile(); @@ -400,7 +388,7 @@ bool GitUpdater::beginUpdate(const char *version) { } bool GitUpdater::recoverFilesystem() { sprintf(this->baseUrl, "https://github.com/cjkas/ESPSomfy-RTS/releases/download/%s/", settings.fwVersion.name); - releaseAssetName(this->currentFile, sizeof(this->currentFile), "littlefs", chipName()); + releaseAssetName(this->currentFile, sizeof(this->currentFile), "littlefs", settings.chipModel); this->status = GIT_UPDATING; this->partition = U_SPIFFS; this->lockFS = true; diff --git a/src/Utils.cpp b/src/Utils.cpp index 2651f2a..0fc4a0a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -150,13 +150,8 @@ bool worthRoaming(bool connected, int currentRssi, int apRssi, int minGain) { bool otaProven(bool networkUp, unsigned long uptimeMs, unsigned long minUptimeMs) { return networkUp && uptimeMs >= minUptimeMs; } -bool releaseAssetName(char *buf, size_t size, const char *kind, const char *chip) { - int n = snprintf(buf, size, "SomfyController.%s.%s.bin", kind, chip); - return n >= 0 && (size_t)n < size; -} -bool isFsAsset(const char *name) { - size_t len = strlen(name); - return strstr(name, ".littlefs.") && len >= 4 && strcmp(name + len - 4, ".bin") == 0; +void releaseAssetName(char *buf, size_t size, const char *kind, const char *chipModel) { + snprintf(buf, size, "SomfyController.%s.esp32%s.bin", kind, chipModel); } const char *wifiAuthModeName(uint8_t authMode) { switch(authMode) { diff --git a/src/Utils.h b/src/Utils.h index 723d9d5..7dfbb31 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -86,12 +86,10 @@ heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalM // update has proven itself: it has run for minUptimeMs and the network is // up. Until then a reset rolls back to the previous firmware. bool otaProven(bool networkUp, unsigned long uptimeMs, unsigned long minUptimeMs); -// A release asset's name: "SomfyController...bin", kind "ino" for -// the firmware or "littlefs" for the web UI image, chip e.g. "esp32s3". False -// when it doesn't fit in size. -bool releaseAssetName(char *buf, size_t size, const char *kind, const char *chip); -// Whether a release asset is a LittleFS image, for any chip. -bool isFsAsset(const char *name); +// A release asset's name: "SomfyController..esp32.bin", kind +// "ino" for the firmware or "littlefs" for the web UI image, chipModel as in +// ConfigSettings ("" for the ESP32, "s3", "c3", "c6"). +void releaseAssetName(char *buf, size_t size, const char *kind, const char *chipModel); // How well loop() keeps up: the passes it made per second and its longest // pass, over the window since the last memStatus broadcast. Only the loop // task calls record(), rate(), maxUs() and publish(). publish() closes the diff --git a/test/test_utils/test_utils.cpp b/test/test_utils/test_utils.cpp index 2e5db6e..e549f73 100644 --- a/test/test_utils/test_utils.cpp +++ b/test/test_utils/test_utils.cpp @@ -273,31 +273,23 @@ static void test_otaProven_not_without_network() { } /********************************************************************* - * releaseAssetName / isFsAsset + * releaseAssetName ********************************************************************/ static void test_releaseAssetName_matches_the_release_workflow() { - // .github/workflows/build.yaml fwname and fsname. + // .github/workflows/build.yaml fwname and fsname, from ConfigSettings::chipModel. char buf[64]; - TEST_ASSERT_TRUE(releaseAssetName(buf, sizeof(buf), "ino", "esp32")); + releaseAssetName(buf, sizeof(buf), "ino", ""); TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32.bin", buf); - TEST_ASSERT_TRUE(releaseAssetName(buf, sizeof(buf), "littlefs", "esp32s3")); + releaseAssetName(buf, sizeof(buf), "littlefs", "s3"); TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32s3.bin", buf); + releaseAssetName(buf, sizeof(buf), "littlefs", "c6"); + TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32c6.bin", buf); } -static void test_releaseAssetName_too_small_buffer() { - char buf[16]; - TEST_ASSERT_FALSE(releaseAssetName(buf, sizeof(buf), "littlefs", "esp32c6")); - TEST_ASSERT_EQUAL(sizeof(buf) - 1, strlen(buf)); -} -static void test_isFsAsset_per_chip_images() { - TEST_ASSERT_TRUE(isFsAsset("SomfyController.littlefs.esp32.bin")); - TEST_ASSERT_TRUE(isFsAsset("SomfyController.littlefs.esp32c6.bin")); - TEST_ASSERT_TRUE(isFsAsset("SomfyController.littlefs.bin")); -} -static void test_isFsAsset_not_the_other_assets() { - TEST_ASSERT_FALSE(isFsAsset("SomfyController.ino.esp32.bin")); - TEST_ASSERT_FALSE(isFsAsset("SomfyController.onboard.esp32.bin.zip")); - TEST_ASSERT_FALSE(isFsAsset("SomfyController.littlefs.bin.sha256")); - TEST_ASSERT_FALSE(isFsAsset("")); +static void test_releaseAssetName_keeps_c3() { + // C3 is out of the IDF 5.5 builds for now (ADR 0003) but is coming back. + char buf[64]; + releaseAssetName(buf, sizeof(buf), "ino", "c3"); + TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32c3.bin", buf); } /********************************************************************* @@ -431,9 +423,7 @@ int main(int argc, char **argv) { RUN_TEST(test_otaProven_not_before_the_minimum_uptime); RUN_TEST(test_otaProven_not_without_network); RUN_TEST(test_releaseAssetName_matches_the_release_workflow); - RUN_TEST(test_releaseAssetName_too_small_buffer); - RUN_TEST(test_isFsAsset_per_chip_images); - RUN_TEST(test_isFsAsset_not_the_other_assets); + RUN_TEST(test_releaseAssetName_keeps_c3); RUN_TEST(test_wifiAuthModeName_matches_the_idf_enum); RUN_TEST(test_wifiAuthModeName_unknown_past_the_shared_values); RUN_TEST(test_heapEmitTarget_one_client_always_gets_it);