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