/ firmware / src / networking / update.cpp
update.cpp
  1  #include "update.h"
  2  #include <storage.h>
  3  #include <led.h>
  4  
  5  #include <Arduino.h>
  6  #include <Update.h>
  7  #include <SD.h>
  8  #include <WiFiClientSecure.h>
  9  #include <HTTPClient.h>
 10  #include <HTTPUpdate.h>
 11  
 12  static void log_progress(size_t current, size_t total) {
 13    if (total == 0) return;
 14    Serial.printf("[update] %u%%\r", (unsigned)(current * 100 / total));
 15  }
 16  
 17  bool networking::update::applyFromSD(const char *path) {
 18    if (!hardware::storage::ensureSD()) {
 19      Serial.println(F("[update] SD card not available"));
 20      return false;
 21    }
 22  
 23    if (!SD.exists(path)) return false;
 24  
 25    File bin = SD.open(path, FILE_READ);
 26    if (!bin || bin.isDirectory()) {
 27      Serial.println(F("[update] cannot open update file"));
 28      if (bin) bin.close();
 29      return false;
 30    }
 31  
 32    size_t size = bin.size();
 33    if (size == 0) {
 34      Serial.println(F("[update] update file is empty"));
 35      bin.close();
 36      return false;
 37    }
 38  
 39    Serial.printf("[update] found %s (%u bytes)\n", path, (unsigned)size);
 40    LED.set(colors::Magenta);
 41  
 42    String md5_path = String(path) + ".md5";
 43    if (SD.exists(md5_path)) {
 44      File md5_file = SD.open(md5_path, FILE_READ);
 45      if (md5_file) {
 46        String md5 = md5_file.readStringUntil('\n');
 47        md5.trim();
 48        md5_file.close();
 49        if (md5.length() == 32) {
 50          Update.setMD5(md5.c_str());
 51          Serial.printf("[update] MD5: %s\n", md5.c_str());
 52        }
 53      }
 54    }
 55  
 56    Update.onProgress(log_progress);
 57  
 58    if (!Update.begin(size, U_FLASH)) {
 59      Serial.printf("[update] begin failed: %s\n", Update.errorString());
 60      bin.close();
 61      return false;
 62    }
 63  
 64    size_t written = Update.writeStream(bin);
 65    bin.close();
 66  
 67    if (written != size) {
 68      Serial.printf("[update] wrote %u/%u bytes\n", (unsigned)written, (unsigned)size);
 69      Update.abort();
 70      return false;
 71    }
 72  
 73    if (!Update.end()) {
 74      Serial.printf("[update] verify failed: %s\n", Update.errorString());
 75      return false;
 76    }
 77  
 78    Serial.println(F("[update] SD update successful, removing file"));
 79    SD.remove(path);
 80    if (SD.exists(md5_path)) SD.remove(md5_path);
 81    return true;
 82  }
 83  
 84  bool networking::update::applyFromURL(const char *url, const char *cert_pem) {
 85    if (!url || url[0] == '\0') {
 86      Serial.println(F("[update] no URL provided"));
 87      return false;
 88    }
 89  
 90    Serial.printf("[update] fetching %s\n", url);
 91    LED.set(colors::Magenta);
 92  
 93    WiFiClientSecure client;
 94    if (cert_pem) {
 95      client.setCACert(cert_pem);
 96    } else {
 97      client.setInsecure();
 98    }
 99  
100    httpUpdate.rebootOnUpdate(false);
101    httpUpdate.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
102  
103    httpUpdate.onStart([]() {
104      Serial.println(F("[update] download started"));
105    });
106    httpUpdate.onEnd([]() {
107      Serial.println(F("[update] download complete"));
108    });
109    httpUpdate.onProgress([](int current, int total) {
110      if (total > 0)
111        Serial.printf("[update] %d%%\r", current * 100 / total);
112    });
113    httpUpdate.onError([](int error) {
114      Serial.printf("[update] HTTP error: %d\n", error);
115    });
116  
117    t_httpUpdate_return result = httpUpdate.update(client, url);
118  
119    switch (result) {
120      case HTTP_UPDATE_OK:
121        Serial.println(F("[update] HTTPS update successful"));
122        return true;
123      case HTTP_UPDATE_NO_UPDATES:
124        Serial.println(F("[update] server reports no update available"));
125        return false;
126      case HTTP_UPDATE_FAILED:
127      default:
128        Serial.printf("[update] failed: %s\n",
129                      httpUpdate.getLastErrorString().c_str());
130        return false;
131    }
132  }
133  
134  bool networking::update::canRollback() {
135    return Update.canRollBack();
136  }
137  
138  bool networking::update::rollback() {
139    if (!Update.canRollBack()) {
140      Serial.println(F("[update] no rollback partition available"));
141      return false;
142    }
143    Serial.println(F("[update] rolling back to previous firmware"));
144    return Update.rollBack();
145  }
146  
147  void networking::update::checkSDOnBoot() {
148    if (networking::update::applyFromSD()) {
149      Serial.println(F("[update] rebooting into new firmware..."));
150      delay(500);
151      ESP.restart();
152    }
153  }
154  
155  // ─────────────────────────────────────────────────────────────────────────────
156  //  Tests
157  // ─────────────────────────────────────────────────────────────────────────────
158  #ifdef PIO_UNIT_TESTING
159  
160  #include "update.h"
161  #include <testing/utils.h>
162  
163  namespace networking::update { void test(void); }
164  
165  static void test_update_sd_path_config(void) {
166    THEN("the SD update path is configured");
167    TEST_ASSERT_NOT_NULL(config::ota::SD_PATH);
168    TEST_ASSERT_NOT_EMPTY_MESSAGE(config::ota::SD_PATH,
169      "device: config::ota::SD_PATH must not be empty");
170  
171    TEST_PRINTF("SD update file: %s", config::ota::SD_PATH);
172  }
173  
174  static void test_update_no_update_file(void) {
175    WHEN("applyFromSD is called with no update file");
176    THEN("it returns false");
177  
178    if (!hardware::storage::ensureSD()) {
179      TEST_IGNORE_MESSAGE("skipped — no SD card");
180      return;
181    }
182  
183    if (SD.exists(config::ota::SD_PATH)) {
184      TEST_IGNORE_MESSAGE("skipped — update.bin exists on SD (would flash!)");
185      return;
186    }
187  
188    TEST_ASSERT_FALSE_MESSAGE(networking::update::applyFromSD(),
189      "device: should return false when no update file");
190  }
191  
192  static void test_update_rollback_status(void) {
193    WHEN("rollback availability is checked");
194  
195    bool can = networking::update::canRollback();
196    TEST_PRINTF("rollback available: %s", can ? "yes" : "no");
197  }
198  
199  void networking::update::test(void) {
200    MODULE("Update");
201    RUN_TEST(test_update_sd_path_config);
202    RUN_TEST(test_update_no_update_file);
203    RUN_TEST(test_update_rollback_status);
204  }
205  
206  #endif