fix: download the per-chip LittleFS image in online updates

Releases publish SomfyController.littlefs.<chip>.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 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-20 20:43:00 +02:00
parent a1622e652d
commit ab7eae3d94
6 changed files with 59 additions and 20 deletions

View file

@ -4849,7 +4849,7 @@ class Firmware {
let div = this.createFileUploader('/updateApplication');
general.reloadApp = true;
let inst = div.querySelector('div[id=divInstText]');
inst.innerHTML = '<div style="font-size:14px;">Select a binary file [SomfyController.littlefs.bin] containing the littlefs data for the application then press the Upload File button.</div>';
inst.innerHTML = '<div style="font-size:14px;">Select a binary file [SomfyController.littlefs.&lt;chip&gt;.bin, e.g. SomfyController.littlefs.esp32.bin] containing the littlefs data for the application then press the Upload File button.</div>';
if (this.isMobile()) {
inst.innerHTML += `<div style="width:100%;color:red;text-align:center;font-weight:bold;"><span style="margin-top:7px;width:100%;background:yellow;padding:3px;display:inline-block;border-radius:5px;background:white;">WARNING<span></div>`;
inst.innerHTML += '<hr/><div style="font-size:14px;margin-bottom:10px;">This browser does not support automatic backups. It is highly recommended that you back up your configuration using the backup button before proceeding.</div>';

View file

@ -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;

View file

@ -55,7 +55,6 @@ class GitUpdater {
bool beginUpdate(const char *release);
bool endUpdate();
int8_t downloadFile();
void setFirmwareFile();
void setCurrentRelease(GitRepo &repo);
void loop();

View file

@ -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";

View file

@ -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.<kind>.<chip>.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

View file

@ -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);