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