diff --git a/.gitignore b/.gitignore index 15466ba7c..78f38399a 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ pnpm-lock.yaml interface/.tsbuildinfo test/test_api/package-lock.json .clangd +mklittlefs diff --git a/data/pre_load.json b/data/pre_load.json index cd5fdd2c7..6459dc0a6 100644 --- a/data/pre_load.json +++ b/data/pre_load.json @@ -1,7 +1,6 @@ { "type": "systembackup", - "version": "3.8.2", - "date": "2026-03-29T13:28:15", + "version": "3.9.0", "systembackup": [ { "type": "settings", @@ -9,7 +8,7 @@ "ssid": "", "bssid": "", "password": "", - "hostname": "ems-esp", + "hostname": "ems-esp2", "static_ip_config": false, "bandwidth20": false, "nosleep": true, @@ -19,7 +18,7 @@ "tx_power": 0 }, "AP": { - "provision_mode": 2, + "provision_mode": 1, "ssid": "ems-esp", "password": "ems-esp-neo", "channel": 1, @@ -62,7 +61,7 @@ "send_response": false }, "NTP": { - "enabled": true, + "enabled": false, "server": "time.google.com", "tz_label": "Europe/Amsterdam", "tz_format": "CET-1CEST,M3.5.0,M10.5.0/3" @@ -83,7 +82,7 @@ ] }, "Settings": { - "version": "3.8.2", + "version": "3.9.0", "board_profile": "E32V2_2", "platform": "ESP32", "locale": "en", @@ -132,7 +131,7 @@ "modbus_port": 502, "modbus_max_clients": 10, "modbus_timeout": 300, - "developer_mode": true, + "developer_mode": false, "email_enabled": false, "email_ssl": false, "email_starttls": true, @@ -154,14 +153,6 @@ { "type": "customizations", "Customizations": { - "ts": [ - { - "id": "28_1767_7B13_2502", - "name": "gateway_temperature", - "offset": 0, - "is_system": true - } - ], "as": [ { "gpio": 39, @@ -207,22 +198,14 @@ } }, { - "type": "customSupport", - "Support": { - "html": [ - "This product is installed and managed by:", - "", - "Bosch Installer Example", - "", - "Nefit Road 12", - "1234 AB Amsterdam", - "Phone: +31 123 456 789", - "email: support@boschinstaller.nl", - "", - "For help and questions please contact your installer." - ], - "img_url": "https://emsesp.org/media/images/designer.png" - } + "type": "nvs", + "nvs": [ + { + "type": 1, + "key": "fresh_firmware", + "value": 0 + } + ] } ] } \ No newline at end of file diff --git a/src/ESP32React/UploadFileService.cpp b/src/ESP32React/UploadFileService.cpp index 3d4e99152..4d0209d1f 100644 --- a/src/ESP32React/UploadFileService.cpp +++ b/src/ESP32React/UploadFileService.cpp @@ -4,6 +4,7 @@ #include #include +// #include static String getFilenameExtension(const String & filename) { const auto pos = filename.lastIndexOf('.'); @@ -16,8 +17,8 @@ static String getFilenameExtension(const String & filename) { UploadFileService::UploadFileService(AsyncWebServer * server, SecurityManager * securityManager) : _securityManager(securityManager) , _is_firmware(false) + , _is_filesystem(false) , _md5() { - // upload a file via a form server->on( UPLOAD_FILE_PATH, HTTP_POST, @@ -41,8 +42,14 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri const String extension = getFilenameExtension(filename); const std::size_t filesize = request->contentLength(); - _is_firmware = false; - if ((extension == "bin") && (filesize > 1000000)) { + _is_firmware = false; + _is_filesystem = false; + + if (extension == "bin" && filename.endsWith("littlefs.bin")) { + // LittleFS filesystem image + _is_filesystem = true; + _md5[0] = '\0'; // clear any stale md5 so Update.end() doesn't compare against it + } else if ((extension == "bin") && (filesize > 2000000)) { _is_firmware = true; } else if (extension == "json") { _md5[0] = '\0'; // clear md5 @@ -88,6 +95,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri #endif // it's firmware - initialize the ArduinoOTA updater emsesp::EMSESP::logger().info("Uploading firmware file %s (size: %d KB). Please wait...", filename.c_str(), filesize / 1024); + // turn off UART to prevent interference with the upload emsesp::EMSuart::stop(); @@ -96,28 +104,55 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri Update.setMD5(_md5.data()); _md5.front() = '\0'; } - request->onDisconnect([this] { handleEarlyDisconnect(); }); // success, let's make sure we end the update if the client hangs up + request->onDisconnect([this] { handleDisconnect(); }); // success, let's make sure we end the update if the client hangs up } else { handleError(request, 507); // failed to begin, send an error response Insufficient Storage return; } + } else if (_is_filesystem) { + // LittleFS filesystem image - flash directly to the spiffs/littlefs partition + emsesp::EMSESP::logger().info("Uploading filesystem image %s (size: %u KB). Please wait...", filename.c_str(), static_cast(filesize / 1024)); + emsesp::EMSuart::stop(); + LittleFS.end(); // unmount LittleFS before we overwrite the partition under it + + // request->contentLength() is the multipart HTTP body size, not the file size, + // so it can exceed the partition by a few hundred bytes. Use UPDATE_SIZE_UNKNOWN + // and let the Update library size against the whole partition. + if (Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { + // emsesp::EMSESP::logger().info("Update.begin(U_SPIFFS) ok, partition size %u bytes", static_cast(Update.size())); + request->onDisconnect([this] { handleDisconnect(); }); + } else { + emsesp::EMSESP::logger().err("Update.begin(U_SPIFFS) failed: %s", Update.errorString()); + handleError(request, 507); + return; + } } else { // its a normal file, open a new temp file to write the contents too request->_tempFile = LittleFS.open(TEMP_FILENAME_PATH, "w"); } } - if (!_is_firmware) { - if (len && len != request->_tempFile.write(data, len)) { // stream the incoming chunk to the opened file - handleError(request, 507); // 507-Insufficient Storage - } - } else if (!request->_tempObject) { // if we haven't delt with an error, continue with the firmware update - if (Update.write(data, len) != len) { - handleError(request, 500); // internal error, failed - return; - } - if (final && !Update.end(true)) { - handleError(request, 500); // internal error, failed + if (_is_firmware || _is_filesystem) { + if (!request->_tempObject) { // if we haven't delt with an error, continue with the OTA update + if (Update.write(data, len) != len) { + emsesp::EMSESP::logger().err("Update.write failed at offset %u (chunk %u): %s", + static_cast(Update.progress()), + static_cast(len), + Update.errorString()); + handleError(request, 500); // internal error, failed + return; + } + if (final) { + if (!Update.end(true)) { + emsesp::EMSESP::logger().err("Update.end failed: %s", Update.errorString()); + handleError(request, 500); + return; + } + } + } else { + if (len && len != request->_tempFile.write(data, len)) { // stream the incoming chunk to the opened file + handleError(request, 507); // 507-Insufficient Storage + } } } } @@ -135,11 +170,13 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) { return; } - // check if it was a firmware upgrade - // if no error, send the success response as a JSON - if (_is_firmware && !request->_tempObject) { - // set NVS to tell EMS-ESP this is a new fresh firmware on next restart - emsesp::EMSESP::nvs_.putBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, true); + // check if it was a firmware or filesystem image upgrade + // if no error, send the success response and request a restart + if ((_is_firmware || _is_filesystem) && !request->_tempObject) { + if (_is_firmware) { + // set NVS to tell EMS-ESP this is a new fresh firmware on next restart + emsesp::EMSESP::nvs_.putBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, true); + } AsyncWebServerResponse * response = request->beginResponse(200); request->send(response); @@ -178,15 +215,21 @@ void UploadFileService::handleError(AsyncWebServerRequest * request, int code) { // that is caught by the web code. Unfortunately the http error code is not sent to the client on fast network connections if (code == 406) { request->client()->close(); - _is_firmware = false; + _is_firmware = false; + _is_filesystem = false; Update.abort(); } + + // if we aborted a filesystem upload, remount LittleFS so the device keeps working + if (_is_filesystem) { + LittleFS.begin(); + } } -void UploadFileService::handleEarlyDisconnect() { +void UploadFileService::handleDisconnect() { emsesp::EMSESP::logger().info("Upload finished"); emsesp::EMSESP::system_.uart_init(); // re-enable UART - _is_firmware = false; - Update.abort(); + _is_firmware = false; + _is_filesystem = false; } diff --git a/src/ESP32React/UploadFileService.h b/src/ESP32React/UploadFileService.h index 352342148..4c8a20dbb 100644 --- a/src/ESP32React/UploadFileService.h +++ b/src/ESP32React/UploadFileService.h @@ -22,13 +22,14 @@ class UploadFileService { private: SecurityManager * _securityManager; bool _is_firmware; + bool _is_filesystem; std::array _md5; void handleUpload(AsyncWebServerRequest * request, const String & filename, size_t index, uint8_t * data, size_t len, bool final); void uploadComplete(AsyncWebServerRequest * request); void handleError(AsyncWebServerRequest * request, int code); - void handleEarlyDisconnect(); + void handleDisconnect(); }; #endif \ No newline at end of file diff --git a/src/core/emsesp.cpp b/src/core/emsesp.cpp index 330210591..a550e8307 100644 --- a/src/core/emsesp.cpp +++ b/src/core/emsesp.cpp @@ -1711,6 +1711,11 @@ void EMSESP::start() { bool factory_settings = false; #endif +#if defined(EMSESP_DEBUG) + // LOG_DEBUG("Listing root directory before:"); + // system_.listDir("/", 3); // show the contents of the root directory +#endif + // start NVS storage if (!nvs_.begin("ems-esp", false, "nvs1")) { // try bigger nvs partition on 16M flash first nvs_.begin("ems-esp", false, "nvs"); // fallback to small nvs @@ -1725,6 +1730,11 @@ void EMSESP::start() { // loads core system services settings (mqtt, ap, ntp etc) esp32React.begin(); +#if defined(EMSESP_DEBUG) + // LOG_DEBUG("Listing root directory after:"); + // system_.listDir("/", 3); // show the contents of the root directory +#endif + #ifndef EMSESP_STANDALONE if (factory_settings) { LOG_WARNING("No settings found on filesystem. Using factory settings."); diff --git a/src/core/network.cpp b/src/core/network.cpp index ff9420ae9..bf09d8288 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -412,7 +412,7 @@ void Network::startmDNS() const { MDNS.addService("telnet", "tcp", 23); // add our telnet console MDNS.addServiceTxt("http", "tcp", "address", address_s.c_str()); - emsesp::EMSESP::logger().info("Starting mDNS Responder service"); + emsesp::EMSESP::logger().info("Starting mDNS Responder service for %s", address_s.c_str()); #endif } diff --git a/src/core/system.cpp b/src/core/system.cpp index ad33f5c13..093d73b37 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -732,18 +732,16 @@ void System::start() { last_system_check_ = 0; // force the LED to go from fast flash to pulse uart_init(); // start UART syslog_init(); // start syslog - modbus_init(); // start modbus + modbus_init(); // start modbus } // button single click void System::button_OnClick(PButton & b) { LOG_NOTICE("Button pressed - single click"); -#if defined(EMSESP_TEST) #ifndef EMSESP_STANDALONE // show filesystem - Test::listDir(LittleFS, "/", 3); -#endif + listDir("/", 3); #endif } @@ -3478,4 +3476,39 @@ void System::restore_snapshot_gpios(std::vector & u_gpios, std::vector