From 88c2ff3def8b93cd3cd170481eb99449ff71e9c3 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 14 Aug 2025 23:35:33 -0700 Subject: [PATCH] Refactor code structure and clean up unused files --- data/booths/roamer-big.json | 2 +- include/my_board.h | 7 +- include/my_buttons.h | 6 +- platformio.ini | 4 +- src/ATALights.cpp | 10 ++- src/Animations.cpp | 120 ++++++++++++++++++---------------- src/AppUpgrade.cpp | 126 ++++++++++++++++++++++++------------ src/BLE_SP110E.cpp | 6 +- src/BLE_UpdateService.cpp | 78 +++++++++++++--------- src/BleServer.cpp | 11 +++- src/JsonConstrain.cpp | 16 +++-- src/PWM_Output.cpp | 11 +++- src/common/color_tools.cpp | 78 +++++++++++----------- src/global.cpp | 8 ++- src/main.cpp | 5 +- src/my_board.cpp | 45 +++++++++++-- src/my_buttons.cpp | 38 ++++++++--- src/my_tsensor.cpp | 61 +++++++++++++---- src/my_wifi.cpp | 82 +++++++++++++++++------ 19 files changed, 475 insertions(+), 239 deletions(-) diff --git a/data/booths/roamer-big.json b/data/booths/roamer-big.json index 091b2ce..9efe8c4 100644 --- a/data/booths/roamer-big.json +++ b/data/booths/roamer-big.json @@ -104,7 +104,7 @@ "size": 168, "chip": "SK6812", "rgb-order": "rgb", - "shift":-52, + "shift":-5, "offset": 0, "power-div": 0, "i2s-ch": 0, diff --git a/include/my_board.h b/include/my_board.h index 4b092d8..1d02c6b 100644 --- a/include/my_board.h +++ b/include/my_board.h @@ -27,10 +27,11 @@ typedef struct{ }BOARD_PINS; extern BOARD_PINS* thisBoardPins; -#define setStatusPin1(state) digitalWrite(thisBoardPins->stat[0], state); -#define setStatusPin2(state) digitalWrite(thisBoardPins->stat[1], state); +// Safe status pin macros: only write when configured (>=0) +#define setStatusPin1(state) do { if (thisBoardPins && thisBoardPins->stat[0] >= 0) digitalWrite(thisBoardPins->stat[0], state); } while(0) +#define setStatusPin2(state) do { if (thisBoardPins && thisBoardPins->stat[1] >= 0) digitalWrite(thisBoardPins->stat[1], state); } while(0) -void Load_Board_Pins(BOARD_PINS& boardPins, String& path); +bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path); void Init_Board_Basic(BOARD_PINS& boardPins); void updateFanControl(float temperature); void Initialize_Rear_Control(int relayIndex, int buttonIndex, int rampTime, int steps, float min, float max); diff --git a/include/my_buttons.h b/include/my_buttons.h index 972b40c..a63fb39 100644 --- a/include/my_buttons.h +++ b/include/my_buttons.h @@ -4,11 +4,11 @@ #include "OneButton.h" extern OneButton *boardButtons[3]; - -#define Update_Buttons() boardButtons[1]->tick(); boardButtons[2]->tick(); boardButtons[3]->tick(); - void Init_ButtonEvents(int8_t (&pin)[3]); +// Safely tick any initialized buttons (nullptr-aware) +void Update_Buttons(); + void btn1_click(); void btn1_doubleClick(); void btn1_LongPressStart(); diff --git a/platformio.ini b/platformio.ini index 968b6db..904b8cb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,6 +34,6 @@ build_flags = -D CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=1 -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE -D CONFIG_ARDUHAL_LOG_COLORS=1 -upload_port = COM11 +upload_port = COM5 debug_init_break = tbreak setup -monitor_port = COM11 +monitor_port = COM5 diff --git a/src/ATALights.cpp b/src/ATALights.cpp index 6de664d..befd7da 100644 --- a/src/ATALights.cpp +++ b/src/ATALights.cpp @@ -19,7 +19,7 @@ TaskHandle_t Animation_Task_Handle; LEDSTRIP_SETTINGS ledSettings[2]; volatile bool AnimationLooping = false; ANIM_EVENT prevAnimEvent = {0}; -QueueHandle_t animationQueue = xQueueCreate( 1, sizeof( ANIM_EVENT ) ); +QueueHandle_t animationQueue = xQueueCreate( 4, sizeof( ANIM_EVENT ) ); void Lights_Set_Animation(int animIndex, uint8_t red, uint8_t grn, uint8_t blu){ @@ -352,8 +352,12 @@ void Lights_Control_Task(void *parameters){ ESP_LOGD(tag, "New Animation Event: Index: %d", AnimEvent.AnimationIndex); switch (AnimEvent.AnimationIndex) { case -3: // Set Pixel by index - ledSettings[0].leds[AnimEvent.data.data[7]] = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu); - FastLED.show(); + if (AnimEvent.data.data[7] >= 0 && AnimEvent.data.data[7] < ledSettings[0].size) { + ledSettings[0].leds[AnimEvent.data.data[7]] = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu); + FastLED.show(); + } else { + ESP_LOGW(tag, "Pixel index out of range: %d", AnimEvent.data.data[7]); + } break; case -2: // Fill Static Color col = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu); diff --git a/src/Animations.cpp b/src/Animations.cpp index a498bed..159c1d9 100644 --- a/src/Animations.cpp +++ b/src/Animations.cpp @@ -71,7 +71,16 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp xLastWakeTime = xTaskGetTickCount(); // Call animation function - int speedIncrease = callback(); // Call animation function + int speedIncrease = 0; + try { + speedIncrease = callback(); // Call animation function + } catch (const std::exception& e) { + ESP_LOGE("Animation_Loop_Duration", "Callback exception: %s", e.what()); + break; + } catch (...) { + ESP_LOGE("Animation_Loop_Duration", "Callback unknown exception"); + break; + } if(!loop_active_flag) return; @@ -86,7 +95,7 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp TickType_t elapsedTicks = xTaskGetTickCount() - xLastWakeTime; TickType_t delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0; - // Delay and Check for termination request + // Delay and Check for termination request if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } // Check if duration reached and wait for loop_active_flag @@ -94,12 +103,8 @@ void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickTyp TickType_t totalElapsed = xTaskGetTickCount() - startTicks; if (totalElapsed >= durationMs) { - while(loop_active_flag){ - if (ulTaskNotifyTake(pdTRUE, 50)) { - return; - } - } - break; + // Auto-terminate the loop when duration reached + break; } } } @@ -121,7 +126,16 @@ void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t for(;;) { xLastWakeTime = xTaskGetTickCount(); - int speedIncrease = callback(); // Call animation function + int speedIncrease = 0; + try { + speedIncrease = callback(); // Call animation function + } catch (const std::exception& e) { + ESP_LOGE("Animation_Loop_Cycles", "Callback exception: %s", e.what()); + break; + } catch (...) { + ESP_LOGE("Animation_Loop_Cycles", "Callback unknown exception"); + break; + } if(!loop_active_flag) return; @@ -139,14 +153,8 @@ void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t // Delay and Check for termination request if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } - // Check if cycles reached and wait for loop_active_flag + // Check if cycles reached and exit if (loop_cycle_count >= loop_cycles) { - while(loop_active_flag){ - if (ulTaskNotifyTake(pdTRUE, 50)) { - //loop_active_flag = false; - break; - } - } break; } @@ -189,6 +197,7 @@ void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const // Calculate half size for mirroring const int halfSize = size / 2; + if (halfSize <= 0) return; // Create heat array for half the size uint8_t* heat = new (std::nothrow) uint8_t[halfSize]; @@ -211,7 +220,8 @@ void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const // Randomly ignite new sparks at bottom if(random8() < FIRE_SPARKING) { - y = random8(7); + // ensure y is in-bounds for small strips + y = min(random8(7), halfSize - 1); heat[y] = qadd8(heat[y], random8(160, 240)); } @@ -382,11 +392,11 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA // Set fade factor uint8_t fadeFactor = shorterTail ? COMET_FADE_FACTOR2 : COMET_FADE_FACTOR1; - // Initialize comet positions with fixed array + // Initialize comet positions with fixed array, evenly distributed int cometPositions[MAX_COMETS] = {0}; - int spacing = size / totalComets; for (int i = 0; i < totalComets; i++) { - cometPositions[i] = i * spacing; + // Even distribution even when size not divisible by totalComets + cometPositions[i] = (i * size) / totalComets; } // Animation loop @@ -405,7 +415,7 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA } } - // Move and draw comets + // Move and draw comets for (int i = 0; i < totalComets; i++) { if (direction) { cometPositions[i] = (cometPositions[i] + 1) % size; @@ -414,9 +424,11 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA } // Draw comet with solid color - colorPack.col[i % colorPack.size]; + color = colorPack.col[i % colorPack.size]; for (int j = 0; j < cometSize; j++) { - pos = (cometPositions[i] - j + size) % size; + // Tail follows the direction of movement + pos = direction ? (cometPositions[i] - j) : (cometPositions[i] + j); + pos = (pos % size + size) % size; // safe modulus leds[pos] += color; } } @@ -439,7 +451,7 @@ void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PA void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0) { - if (!leds || size <= 0 || totalDurationMs <= 0) return; + if (!leds || size <= 1 || totalDurationMs <= 0) return; const int halfSize = size / 2; const float msPerLed = totalDurationMs / (float)halfSize; @@ -486,42 +498,42 @@ void Anim_ColorBreath(bool volatile& activeFlag, CRGB* leds, int size, const COL unsigned long startTime = millis(); const uint32_t halfTime = timeMs / 2; - uint8_t origBright = FastLED.getBrightness(); + const uint8_t origBright = FastLED.getBrightness(); FastLED.setBrightness(255); - uint8_t brightness = MIN_BRIGHTNESS; + uint8_t breath = MIN_BRIGHTNESS; uint32_t elapsedTime; - CRGB scaledColor, correctedColor; unsigned long currentTime; + CRGB outColor; Animation_Loop(activeFlag, speed, [&]() -> int { - // Calculate elapsed time in current breath cycle + // Elapsed time in the current breath cycle currentTime = millis(); elapsedTime = currentTime - startTime; - - // Calculate brightness using a linear approach + + // Triangle wave brightness: up for half, down for half if (elapsedTime < halfTime) { - brightness = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten + breath = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten } else { - brightness = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim + breath = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim } - - // Scale the color directly - scaledColor = colors.col[colorIndex]; - scaledColor.nscale8(brightness * origBright / 255); - - // Correct the color scale for vision - correctedColor = scaledColor; - correctedColor.nscale8_video(brightness); - + + // Combine breath with original global brightness (rounded) + uint16_t prod = static_cast(breath) * static_cast(origBright); + uint8_t finalBright = static_cast((prod + 127) / 255); + + // Apply perceptual scaling once with combined brightness + outColor = colors.col[colorIndex]; + outColor.nscale8_video(finalBright); + // Fill all LEDs with scaled color - fill_solid(leds, size, correctedColor); + fill_solid(leds, size, outColor); FastLED.show(); - + if (elapsedTime >= timeMs) { colorIndex = (colorIndex + 1) % colors.size; startTime = currentTime; } - + return 0; }); @@ -534,21 +546,17 @@ void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const CRGB color1, color2; - // Create initial gradient - int segmentLength = size / colors.size; - for(int i = 0; i < size; i++) { - // Determine which color segment we're in - int segment = i / segmentLength; + // Create initial gradient: evenly distribute blends across the strip + // For i in [0..size-1], compute position in color space [0..colors.size) + for (int i = 0; i < size; i++) { + uint32_t pos256 = (uint32_t)i * (uint32_t)colors.size * 256u / (uint32_t)size; // 8.8 fixed point + int segment = (int)(pos256 >> 8); // 0..colors.size-1 + uint8_t blendPos = (uint8_t)(pos256 & 0xFF); // 0..255 int nextSegment = (segment + 1) % colors.size; - - // Calculate blend amount within segment - uint8_t blendPos = map(i % segmentLength, 0, segmentLength - 1, 0, 255); - - // Create local copies for blending + + // Local copies for blending color1 = colors.col[segment]; color2 = colors.col[nextSegment]; - - // Set initial gradient leds[i] = blend(color1, color2, blendPos); } diff --git a/src/AppUpgrade.cpp b/src/AppUpgrade.cpp index cef9f2f..991a952 100644 --- a/src/AppUpgrade.cpp +++ b/src/AppUpgrade.cpp @@ -4,8 +4,10 @@ #include #include #include "global.h" -#include "jsonConstrain.h" +#include "JsonConstrain.h" #include "BLE_UpdateService.h" +#include +#include static const char* TAG = "AppUpdater"; TaskHandle_t Update_Task_Handle = NULL; @@ -90,7 +92,8 @@ bool AppUpdater::checkManifest() { // Check if an update is available updateAvailable = false; - if (otaVersion < localVersion) { + // Only mark update available if remote is strictly newer than local + if (otaVersion <= localVersion) { ESP_LOGI(TAG, "No updates available"); return false; }else{ @@ -163,26 +166,47 @@ bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, con //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); - // Single pass: Save file and calculate MD5 - while (totalRead < contentLength) { - size_t available = stream->available(); - if (available) { - size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE))); - - // Write to temp file and update MD5 + if (contentLength > 0) { + // Single pass with known content length + while (totalRead < contentLength) { + size_t available = stream->available(); + if (available) { + size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE))); + + // Write to temp file and update MD5 + if (file.write(downloadBuffer.get(), readLen) != readLen) { + ESP_LOGE(TAG, "Failed to write to temporary file"); + + file.close(); + fileSystem.remove(tempPath.c_str()); + return false; + } + + md5.add(downloadBuffer.get(), readLen); + totalRead += readLen; + updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath); + } + yield(); + } + } else { + // Unknown content length: read until stream ends + for (;;) { + size_t readLen = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE); + if (readLen == 0) { + break; + } if (file.write(downloadBuffer.get(), readLen) != readLen) { ESP_LOGE(TAG, "Failed to write to temporary file"); - file.close(); fileSystem.remove(tempPath.c_str()); return false; } - md5.add(downloadBuffer.get(), readLen); totalRead += readLen; - updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath); + // Progress unknown; emit periodic heartbeats at 0% + updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); + yield(); } - yield(); } file.close(); @@ -286,7 +310,7 @@ bool AppUpdater::updateApp() { // Check available space size_t firmwareSize = http.getSize(); - if (!Update.begin(firmwareSize)) { + if (!Update.begin(firmwareSize > 0 ? firmwareSize : UPDATE_SIZE_UNKNOWN)) { ESP_LOGE(TAG, "Firmware: Not enough space for update"); updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update"); http.end(); @@ -299,31 +323,46 @@ bool AppUpdater::updateApp() { // Download and verify firmware WiFiClient* stream = http.getStreamPtr(); - size_t remaining = firmwareSize; - while (remaining > 0) { - size_t chunk = std::min(remaining, size_t(BUFFER_SIZE)); - size_t read = stream->readBytes(downloadBuffer.get(), chunk); - - // Check for timeout - if (read == 0) { - ESP_LOGE(TAG, "Read timeout"); - - Update.abort(); - http.end(); - return false; + if (firmwareSize > 0) { + size_t remaining = firmwareSize; + while (remaining > 0) { + size_t chunk = std::min(remaining, size_t(BUFFER_SIZE)); + size_t read = stream->readBytes(downloadBuffer.get(), chunk); + + // Check for timeout + if (read == 0) { + ESP_LOGE(TAG, "Read timeout"); + Update.abort(); + http.end(); + return false; + } + + // Update MD5 and write firmware + md5.add(downloadBuffer.get(), read); + if (Update.write(downloadBuffer.get(), read) != read) { + ESP_LOGE(TAG, "Write failed"); + Update.abort(); + http.end(); + return false; + } + + remaining -= read; + updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware"); } - - // Update MD5 and write firmware - md5.add(downloadBuffer.get(), read); - if (Update.write(downloadBuffer.get(), read) != read) { - ESP_LOGE(TAG, "Write failed"); - Update.abort(); - http.end(); - return false; + } else { + // Unknown size: stream until end + for (;;) { + size_t read = stream->readBytes(downloadBuffer.get(), BUFFER_SIZE); + if (read == 0) break; + md5.add(downloadBuffer.get(), read); + if (Update.write(downloadBuffer.get(), read) != read) { + ESP_LOGE(TAG, "Write failed"); + Update.abort(); + http.end(); + return false; + } + updateProgress(UpdateStatus::DOWNLOADING, 0, "firmware"); } - - remaining -= read; - updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware"); } // Verify MD5 @@ -469,6 +508,7 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch const char* msg; bool isComplete = false; + const char* safeMsg = message ? message : ""; switch (newStatus) { case AppUpdater::UpdateStatus::IDLE: snprintf(buffer, sizeof(buffer), "Update idle"); @@ -478,23 +518,23 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch msg = message ? message : ""; break; case AppUpdater::UpdateStatus::DOWNLOADING: - snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", message, percentage); + snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", safeMsg, percentage); msg = buffer; break; case AppUpdater::UpdateStatus::VERIFYING: - snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", message, percentage); + snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", safeMsg, percentage); msg = buffer; break; case AppUpdater::UpdateStatus::FILE_SKIPPED: - snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", message); + snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", safeMsg); msg = buffer; break; case AppUpdater::UpdateStatus::FILE_SAVED: - snprintf(buffer, sizeof(buffer), "%s: File Saved", message); + snprintf(buffer, sizeof(buffer), "%s: File Saved", safeMsg); msg = buffer; break; case AppUpdater::UpdateStatus::MD5_FAILED: - snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", message); + snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", safeMsg); msg = buffer; break; case AppUpdater::UpdateStatus::COMPLETE: @@ -503,7 +543,7 @@ void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const ch isComplete = true; break; case AppUpdater::UpdateStatus::ERROR: - snprintf(buffer, sizeof(buffer), "Error!: %s", message); + snprintf(buffer, sizeof(buffer), "Error!: %s", safeMsg); msg = buffer; break; default: diff --git a/src/BLE_SP110E.cpp b/src/BLE_SP110E.cpp index fba7765..2a44229 100644 --- a/src/BLE_SP110E.cpp +++ b/src/BLE_SP110E.cpp @@ -55,7 +55,11 @@ class SP110ECallbacks : public NimBLECharacteristicCallbacks { std::string rawValue = pCharacteristic->getValue(); const uint8_t* value = reinterpret_cast(rawValue.data()); size_t length = rawValue.length(); - ESP_LOGI(tag, "Data received 0x%02X, 0x%02X, 0x%02X (length %zu):", value[0], value[1], value[2], length); + if (length >= 3) { + ESP_LOGI(tag, "Data received 0x%02X, 0x%02X, 0x%02X (length %zu):", value[0], value[1], value[2], length); + } else { + ESP_LOGI(tag, "Data received (length %zu)", length); + } sendToAllClients(value, length); process_BLE_SP110E_Command(value, length, pCharacteristic); diff --git a/src/BLE_UpdateService.cpp b/src/BLE_UpdateService.cpp index fe0381e..258b305 100644 --- a/src/BLE_UpdateService.cpp +++ b/src/BLE_UpdateService.cpp @@ -35,20 +35,36 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks { ESP_LOGD(tag, "Upgrade Char written with value: %s", value.c_str()); if (value.compare(0, 12, "wifi-connect") == 0) { // Update WiFi credentials - JsonDocument doc; - deserializeJson(doc, value.substr(13)); + // Expecting: "wifi-connect:{\"ssid\":...,\"pass\":...}" + size_t jsonStart = (value.size() > 12 && value[12] == ':') ? 13 : 12; + if (value.size() <= jsonStart) { + ESP_LOGW(tag, "wifi-connect command missing JSON payload"); + return; + } + + DynamicJsonDocument doc(512); + DeserializationError err = deserializeJson(doc, value.substr(jsonStart)); + if (err) { + ESP_LOGW(tag, "JSON parse error for wifi-connect: %s", err.c_str()); + return; + } + JsonObject wifiJson = doc.as(); String ssid = wifiJson["ssid"].as(); String pass = wifiJson["pass"].as(); - ESP_LOGI(tag, "Wifi Credentials: %s, %s", ssid.c_str(), pass.c_str()); - - bool status = StartWifiConnectTask(ssid, pass); - if(status == true){ + if (ssid.length() == 0) { + ESP_LOGW(tag, "wifi-connect missing ssid"); + return; + } + ESP_LOGI(tag, "WiFi connect requested: ssid='%s', pass len=%u", ssid.c_str(), (unsigned)pass.length()); + + bool started = StartWifiConnectTask(ssid, pass); + if (started) { updatePacket.wifiStatus = WIFI_DISCONNECTED; updatePacket.wifiOnline = false; updatePacket.wifiIP[0] = updatePacket.wifiIP[1] = updatePacket.wifiIP[2] = updatePacket.wifiIP[3] = 0; - }else{ - ESP_LOGI(tag, "Failed to start WiFi connection task"); + } else { + ESP_LOGW(tag, "Failed to start WiFi connection task"); } } else if (value.compare("version-check") == 0) { // Check if new version is available @@ -73,22 +89,19 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks { void onRead(NimBLECharacteristic *pCharacteristic) override { updatePacket.wifiOnline = InternetAvailable; - if(WiFi.status() == WL_CONNECTED){ - updatePacket.wifiStatus = WIFI_CONNECTED; - if(updatePacket.wifiIP[0] == 0){ - updatePacket.wifiIP[0] = WiFi.localIP()[0]; - updatePacket.wifiIP[1] = WiFi.localIP()[1]; - updatePacket.wifiIP[2] = WiFi.localIP()[2]; - updatePacket.wifiIP[3] = WiFi.localIP()[3]; - } - }else{ - updatePacket.wifiStatus = WIFI_DISCONNECTED; - if(updatePacket.wifiIP[0] > 0){ - updatePacket.wifiIP[0] = 0; - updatePacket.wifiIP[1] = 0; - updatePacket.wifiIP[2] = 0; - updatePacket.wifiIP[3] = 0; - } + if (WiFi.status() == WL_CONNECTED) { + updatePacket.wifiStatus = WIFI_CONNECTED; + IPAddress ip = WiFi.localIP(); + updatePacket.wifiIP[0] = ip[0]; + updatePacket.wifiIP[1] = ip[1]; + updatePacket.wifiIP[2] = ip[2]; + updatePacket.wifiIP[3] = ip[3]; + } else { + updatePacket.wifiStatus = WIFI_DISCONNECTED; + updatePacket.wifiIP[0] = 0; + updatePacket.wifiIP[1] = 0; + updatePacket.wifiIP[2] = 0; + updatePacket.wifiIP[3] = 0; } //update version @@ -99,19 +112,24 @@ class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks { updatePacket.newVersion[2] = otaVersion.patch(); } - pCharacteristic->setValue(reinterpret_cast(&updatePacket), sizeof(updatePacket)); - ESP_LOGI(tag, "Upgrade Char read"); + // Only populate the control characteristic with the status packet + if (pCharacteristic == pUpgradeCharacteristic1) { + pCharacteristic->setValue(reinterpret_cast(&updatePacket), sizeof(updatePacket)); + ESP_LOGI(tag, "Upgrade status read"); + } } }; void bleUpgrade_send_message(String s){ if(pUpgradeCharacteristic2){ - if (s != nullptr) { - pUpgradeCharacteristic2->setValue(s); + if (s.length() == 0) { + return; + } + // Set value and notify only if there are subscribers to avoid unnecessary work + pUpgradeCharacteristic2->setValue(s.c_str()); + if (pUpgradeCharacteristic2->getSubscribedCount() > 0) { pUpgradeCharacteristic2->notify(); - } else { - ESP_LOGW(tag, "Null string passed to bleUpgrade_send_message"); } } } diff --git a/src/BleServer.cpp b/src/BleServer.cpp index 637b717..fa0c711 100644 --- a/src/BleServer.cpp +++ b/src/BleServer.cpp @@ -13,7 +13,9 @@ class ServerCallbacks : public NimBLEServerCallbacks { // Ensure advertising remains active even after a client connects NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); if (pAdvertising != nullptr) { - pAdvertising->start(); + if (!pAdvertising->isAdvertising()) { + pAdvertising->start(); + } } } @@ -22,7 +24,9 @@ class ServerCallbacks : public NimBLEServerCallbacks { // Restart advertising on disconnect to keep it active always NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); if (pAdvertising != nullptr) { - pAdvertising->start(); + if (!pAdvertising->isAdvertising()) { + pAdvertising->start(); + } } } }; @@ -35,6 +39,7 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) { //NimBLEDevice::init("ATALIGHTS"); NimBLEDevice::init("SP110E-ATA"); //NimBLEDevice::setMTU(247); // Set preferred MTU size (max 247 for BLE) + isInitialized = true; } NimBLEServer *pServer = NimBLEDevice::createServer(); @@ -59,7 +64,7 @@ void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) { // Start BLE advertising NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); if (pAdvertising != nullptr) { - if (!pAdvertising->start()) { + if (!pAdvertising->isAdvertising() && !pAdvertising->start()) { ESP_LOGE(tag, "Failed to start advertising"); return; } diff --git a/src/JsonConstrain.cpp b/src/JsonConstrain.cpp index b8094c2..9b079d9 100644 --- a/src/JsonConstrain.cpp +++ b/src/JsonConstrain.cpp @@ -3,7 +3,10 @@ #include template -void logConstrainedValue(const char* tag, const char* key, T value); +void logConstrainedValue(const char* tag, const char* key, T) { + // Generic fallback when no specialization provided + ESP_LOGD(tag, "Key [%s] value set", key); +} template <> void logConstrainedValue(const char* tag, const char* key, int value) { @@ -16,7 +19,10 @@ void logConstrainedValue(const char* tag, const char* key, float value) { } template -void logClamping(const char* tag, const char* key, T value, T limit, const char* condition); +void logClamping(const char* tag, const char* key, T, T, const char* condition) { + // Generic fallback when no specialization provided + ESP_LOGW(tag, "Key [%s] value too %s. Clamping.", key, condition); +} template <> void logClamping(const char* tag, const char* key, int value, int limit, const char* condition) { @@ -72,12 +78,12 @@ const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, con } String value = jsonObject[key].as(); - if (value.isEmpty()) { + if (value.length() == 0) { ESP_LOGW(tag, "Key [%s] value is empty. Using default value.", key); return strdup(def); } - ESP_LOGD(tag, "Key [%s] value: %s", key, value); + ESP_LOGD(tag, "Key [%s] value: %s", key, value.c_str()); return strdup(value.c_str()); } @@ -93,7 +99,7 @@ String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const String value = jsonObject[key].as(); // Check if the value is empty - if (value.isEmpty()) { + if (value.length() == 0) { ESP_LOGW(tag, "Key [%s] value is empty. Using default value [%s].", key, def.c_str()); return def; } diff --git a/src/PWM_Output.cpp b/src/PWM_Output.cpp index d071bfe..c6ca700 100644 --- a/src/PWM_Output.cpp +++ b/src/PWM_Output.cpp @@ -1,5 +1,6 @@ #include "PWM_Output.h" #include +#include "global.h" const char* tag = "pwmout"; @@ -56,6 +57,11 @@ void PWM_Output::setOutput(float duty){ outDutyVal = static_cast(duty * this->standardFactor); } + // Clamp to valid resolution range [0, 2^res - 1] + int maxVal = static_cast(binaryPow[this->res]); + if (outDutyVal < 0) outDutyVal = 0; + if (outDutyVal > maxVal) outDutyVal = maxVal; + ledcWrite(this->ch, outDutyVal); this->currOutVal = outDutyVal; this->currDuty = duty; @@ -77,8 +83,9 @@ void PWM_Output::setResolution(uint8_t res){ if(this->res < 4) this->res = 4; if(this->res > 16) this->res = 16; - this->standardFactor = binaryPow[res] * 0.01; - this->visionFactor = binaryPow[res] * 0.0001; + // Use the clamped resolution when computing factors + this->standardFactor = binaryPow[this->res] * 0.01f; + this->visionFactor = binaryPow[this->res] * 0.0001f; ESP_LOGD(tag, "factor=%f, vision=%f", this->standardFactor, this->visionFactor); } diff --git a/src/common/color_tools.cpp b/src/common/color_tools.cpp index 3dc53d0..971d976 100644 --- a/src/common/color_tools.cpp +++ b/src/common/color_tools.cpp @@ -3,6 +3,14 @@ #define MAX_HUE 360.0 +// Normalize a hue into [0, MAX_HUE) +static inline float wrapHue(float h) +{ + while (h >= MAX_HUE) h -= MAX_HUE; + while (h < 0.0f) h += MAX_HUE; + return h; +} + /************************* CLASS - HUE PALLET DISPENSER ************************/ @@ -11,59 +19,51 @@ HUE_PALLET_DISPENSER::HUE_PALLET_DISPENSER(void){ } void HUE_PALLET_DISPENSER::Initialize(float hue, float range, int colSteps){ - this->range = range; - this->hueSteps = colSteps; - - this->startHue = hue - this->range / 2; - if(this->startHue < 0.0){ - this->startHue += MAX_HUE; - }else if(this->startHue > MAX_HUE){ - this->startHue -= MAX_HUE; - } + // Clamp and normalize inputs + this->range = constrain(range, 0.0f, (float)MAX_HUE); + this->hueSteps = (colSteps < 0) ? 0 : colSteps; + this->startHue = wrapHue(hue - (this->range * 0.5f)); this->hueIndex = 0; - if(this->hueSteps <= 1){ - this->hueIncrement = 0.0; - this->currHue = hue; - }else{ - this->hueIncrement = this->range / (this->hueSteps - 1); + if (this->hueSteps <= 1) { + this->hueIncrement = 0.0f; + this->currHue = wrapHue(hue); + } else { + this->hueIncrement = (this->range / (this->hueSteps - 1)); + this->currHue = this->startHue; } } int HUE_PALLET_DISPENSER::GetNextPalletHue(void){ - if(this->hueSteps > 1){ - this->currHue = this->startHue + this->hueIndex * this->hueIncrement; - if(this->currHue < 0){ - this->currHue += MAX_HUE; - }else if(this->currHue > MAX_HUE){ - this->currHue -= MAX_HUE; - } - - // TODO Remove later - //rgbpixel_t p = HUEtoRGB(huePallet.currHue); - //Log.traceln(" index: %d, hue= %F, col: %d, %d, %d", huePallet.hueIndex, huePallet.currHue, p.red, p.grn, p.blu); - - this->hueIndex = ++this->hueIndex % this->hueSteps; - return round(this->currHue); - }else{ - return round(this->currHue); + if (this->hueSteps > 1) { + this->currHue = wrapHue(this->startHue + (this->hueIndex * this->hueIncrement)); + this->hueIndex = (this->hueIndex + 1) % this->hueSteps; } + + // Convert to integer hue in [0, 359] + int out = (int)(this->currHue + 0.5f); + if (out >= (int)MAX_HUE) out -= (int)MAX_HUE; + if (out < 0) out = 0; // safety + return out; } float HUE_PALLET_DISPENSER::PeekNextPalletHue(int hueOffset){ - float tempHue = this->startHue + (this->hueIndex + hueOffset) * this->hueIncrement; - - if(tempHue < 0){ - tempHue += MAX_HUE; - }else if(tempHue > MAX_HUE){ - tempHue -= MAX_HUE; - } - + float tempHue = wrapHue(this->startHue + ((this->hueIndex + hueOffset) * this->hueIncrement)); return tempHue; } void HUE_PALLET_DISPENSER::SetHueIndex(int hueIndex){ - this->currHue = hueIndex; + if (this->hueSteps > 0) { + int n = this->hueSteps; + int idx = hueIndex % n; + if (idx < 0) idx += n; + this->hueIndex = idx; + if (this->hueSteps > 1) { + this->currHue = wrapHue(this->startHue + (this->hueIndex * this->hueIncrement)); + } + } else { + this->hueIndex = 0; + } } diff --git a/src/global.cpp b/src/global.cpp index 6e0ed45..7b60b3f 100644 --- a/src/global.cpp +++ b/src/global.cpp @@ -1,5 +1,5 @@ #include "global.h" -#include +#include #include #include #include @@ -257,6 +257,12 @@ void Log_CPU_Load(void) { // Get task stats task_count = uxTaskGetSystemState(task_array, task_count, &total_runtime); + if (total_runtime == 0) { + ESP_LOGW(tag, "Runtime stats not ready yet (total_runtime==0)"); + free(stats); + free(task_array); + return; + } // Find IDLE tasks for (UBaseType_t i = 0; i < task_count; i++) { diff --git a/src/main.cpp b/src/main.cpp index 4b26aef..b06911d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -93,8 +93,7 @@ void setup() { // Serial Port Serial.begin(115200); - while (!Serial) - ; + while (!Serial); // Initialize I2C Port for TSensor, ... Wire.begin(I2C_SDA1_Pin, I2C_SCL1_Pin); @@ -281,7 +280,7 @@ void loop() { static bool ledState = false; // digitalWrite(sys_settings.boardPins.stat[1], ledState = !ledState); - setStatusPin2(ledState = !ledState) + setStatusPin2(ledState = !ledState); } } diff --git a/src/my_board.cpp b/src/my_board.cpp index f0bbcdb..bf7a7fc 100644 --- a/src/my_board.cpp +++ b/src/my_board.cpp @@ -12,13 +12,29 @@ static const char* tag = "board"; BOARD_PINS* thisBoardPins; -void Load_Board_Pins(BOARD_PINS& boardPins, String& path){ +// Basic validator for ESP32-S3 GPIOs; rejects common reserved/USB/strap pins +static bool isValidGpio(int pin) { + if (pin < 0 || pin > 48) return false; + switch (pin) { + case 19: // USB D- + case 20: // USB D+ + case 45: // strapping + case 46: // strapping + return false; + default: + return true; + } +} + +bool Load_Board_Pins(BOARD_PINS& boardPins, const String& path){ + // Default initialize to -1 to avoid stale values on partial loads + memset(&boardPins, -1, sizeof(boardPins)); thisBoardPins = &boardPins; File file = LittleFS.open(path); if (!file) { ESP_LOGE(tag, "Error opening %s...", path.c_str()); - return; + return false; } JsonDocument doc; @@ -27,7 +43,7 @@ void Load_Board_Pins(BOARD_PINS& boardPins, String& path){ if (error) { ESP_LOGE(tag, "%s deserialize error!..", path.c_str()); - return; + return false; } JsonObject boardJson = doc.as(); @@ -60,9 +76,28 @@ void Load_Board_Pins(BOARD_PINS& boardPins, String& path){ boardPins.rf433tx = jsonConstrain(tag, boardJson, "rf433tx", -1, 48, -1); boardPins.rf433rx = jsonConstrain(tag, boardJson, "rf433rx", -1, 48, -1); - // TODO Add hardware version to log + // Validate pins against reserved GPIOs + auto clampPin = [](int v){ return isValidGpio(v) ? v : -1; }; + boardPins.rgb1 = clampPin(boardPins.rgb1); + boardPins.rgb2 = clampPin(boardPins.rgb2); + for (int i=0;i<3;i++) boardPins.btn[i] = clampPin(boardPins.btn[i]); + boardPins.buzzer = clampPin(boardPins.buzzer); + for (int i=0;i<5;i++) boardPins.touch[i] = clampPin(boardPins.touch[i]); + boardPins.shield = clampPin(boardPins.shield); + for (int i=0;i<4;i++) boardPins.relay[i] = clampPin(boardPins.relay[i]); + for (int i=0;i<2;i++) boardPins.stat[i] = clampPin(boardPins.stat[i]); + boardPins.adc1 = clampPin(boardPins.adc1); + boardPins.oled_dc = clampPin(boardPins.oled_dc); + boardPins.oled_rst = clampPin(boardPins.oled_rst); + boardPins.oled_mosi = clampPin(boardPins.oled_mosi); + boardPins.oled_sck = clampPin(boardPins.oled_sck); + boardPins.oled_cs = clampPin(boardPins.oled_cs); + for (int i=0;i<2;i++) boardPins.ext[i] = clampPin(boardPins.ext[i]); + boardPins.rf433tx = clampPin(boardPins.rf433tx); + boardPins.rf433rx = clampPin(boardPins.rf433rx); + ESP_LOGI(tag, "loaded Pins from %s", path.c_str()); - + return true; } diff --git a/src/my_buttons.cpp b/src/my_buttons.cpp index 5de9ae9..a6d9911 100644 --- a/src/my_buttons.cpp +++ b/src/my_buttons.cpp @@ -8,17 +8,29 @@ OneButton *boardButtons[3]; void Init_ButtonEvents(int8_t (&pin)[3]){ - if(pin[0] >= 0) { - boardButtons[0] = new OneButton(pin[0], true, true); - ESP_LOGD(tag, "Button1 Events, pin=%d", pin[0]); + if (pin[0] >= 0) { + if (boardButtons[0] == nullptr) { + boardButtons[0] = new OneButton(pin[0], true, true); + ESP_LOGD(tag, "Button1 Events, pin=%d", pin[0]); + } else { + ESP_LOGD(tag, "Button1 already initialized (pin=%d)", pin[0]); + } } - if(pin[1] >= 0) { - boardButtons[1] = new OneButton(pin[1], true, true); - ESP_LOGD(tag, "Button2 Events, pin=%d", pin[1]); + if (pin[1] >= 0) { + if (boardButtons[1] == nullptr) { + boardButtons[1] = new OneButton(pin[1], true, true); + ESP_LOGD(tag, "Button2 Events, pin=%d", pin[1]); + } else { + ESP_LOGD(tag, "Button2 already initialized (pin=%d)", pin[1]); + } } - if(pin[2] >= 0) { - boardButtons[2] = new OneButton(pin[2], true, false); - ESP_LOGD(tag, "Button3 Events, pin=%d", pin[2]); + if (pin[2] >= 0) { + if (boardButtons[2] == nullptr) { + boardButtons[2] = new OneButton(pin[2], true, false); + ESP_LOGD(tag, "Button3 Events, pin=%d", pin[2]); + } else { + ESP_LOGD(tag, "Button3 already initialized (pin=%d)", pin[2]); + } } /* @@ -62,6 +74,14 @@ void Init_ButtonEvents(int8_t (&pin)[3]){ } +void Update_Buttons() { + for (int i = 0; i < 3; ++i) { + if (boardButtons[i] != nullptr) { + boardButtons[i]->tick(); + } + } +} + void btn1_click() { //IncrementEventIndex(); //Pulse_LED_Status(150); diff --git a/src/my_tsensor.cpp b/src/my_tsensor.cpp index bc2b4fa..2a3ac50 100644 --- a/src/my_tsensor.cpp +++ b/src/my_tsensor.cpp @@ -1,38 +1,77 @@ #include "my_tsensor.h" #include +#include "global.h" static const char* tag = "tsensor"; T_SENSOR tSensorSettings; -TI_TMP102_Compatible *tSensor; +TI_TMP102_Compatible *tSensor = nullptr; /******************* Temperature Control ********************/ void Init_TSensor(uint8_t addr) { - //tSensor = new TI_TMP102_Compatible(72); - tSensor = new TI_TMP102_Compatible(addr); + // Initialize the temperature sensor once with the provided I2C address + if (tSensor == nullptr) { + tSensor = new TI_TMP102_Compatible(addr); + ESP_LOGI(tag, "TSensor initialized at I2C addr 0x%02X", addr); + } else { + ESP_LOGW(tag, "TSensor already initialized; ignoring re-init request (addr 0x%02X)", addr); + } } + static inline float clampf(float v, float lo, float hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; + } + void UpdateFanControl(float temperature, PWM_Output* pwmOut) { + if (pwmOut == nullptr) { + ESP_LOGW(tag, "UpdateFanControl called with null PWM output"); + return; + } + + if (isnan(temperature) || isinf(temperature)) { + ESP_LOGW(tag, "Invalid temperature reading: %f", temperature); + return; + } + static uint8_t FanState = 0; + tSensorSettings.temperature = temperature; // cache last temp float currentDuty = pwmOut->currDuty; float newDuty = currentDuty; + // Sanitize settings locally (do not modify globals) + float sp1 = tSensorSettings.Setpoint1; + float sp2 = tSensorSettings.Setpoint2; + float hyst = tSensorSettings.hyst; + float fp1 = tSensorSettings.fanPower1; + float fp2 = tSensorSettings.fanPower2; + + if (hyst < 0.0f) hyst = 0.0f; + if (sp2 < sp1) { + // Ensure sp2 >= sp1 + float tmp = sp1; sp1 = sp2; sp2 = tmp; + } + const float maxDuty = pwmOut->getMaxDuty(); + fp1 = clampf(fp1, 0.0f, maxDuty); + fp2 = clampf(fp2, 0.0f, maxDuty); + // Fan State Logic - if ((FanState == 2) && (temperature < (tSensorSettings.Setpoint2 - tSensorSettings.hyst))) { - newDuty = tSensorSettings.fanPower1; + if ((FanState == 2) && (temperature < (sp2 - hyst))) { + newDuty = fp1; FanState = 1; //ESP_LOGD(tag, "Dropping down to FanPower1"); } - else if ((FanState == 1) && (temperature < (tSensorSettings.Setpoint1 - tSensorSettings.hyst))) { + else if ((FanState == 1) && (temperature < (sp1 - hyst))) { newDuty = 0; FanState = 0; //ESP_LOGD(tag, "Dropping down to FanPower0"); } - else if ((FanState <= 1) && (temperature > tSensorSettings.Setpoint1)) { - newDuty = tSensorSettings.fanPower1; - if (temperature > tSensorSettings.Setpoint2) { - newDuty = tSensorSettings.fanPower2; + else if ((FanState <= 1) && (temperature > sp1)) { + newDuty = fp1; + if (temperature > sp2) { + newDuty = fp2; FanState = 2; //ESP_LOGD(tag, "Raising up to FanPower2"); } //else { @@ -43,6 +82,6 @@ TI_TMP102_Compatible *tSensor; // Apply new duty cycle if changed if (currentDuty != newDuty) { pwmOut->setOutput(newDuty); - ESP_LOGD(tag, "Board T: %.2f, Fan -> %.2f", temperature, newDuty); + ESP_LOGD(tag, "Board T: %.2f F, Fan -> %.2f (state=%u)", temperature, newDuty, FanState); } } \ No newline at end of file diff --git a/src/my_wifi.cpp b/src/my_wifi.cpp index f3cde55..a47f90c 100644 --- a/src/my_wifi.cpp +++ b/src/my_wifi.cpp @@ -1036,37 +1036,81 @@ bool writeFile(fs::FS &fs, const char *path, const char *message) return false; } - // Open file with error checking - File file = fs.open(path, "w"); - if (!file) + // Normalize and validate path + String finalPath(path); + if (!finalPath.startsWith("/")) { - ESP_LOGE(tag, "Failed to open file: %s", path); + finalPath = String("/") + finalPath; + } + // Prevent directory traversal + if (finalPath.indexOf("..") >= 0) + { + ESP_LOGE(tag, "Rejected unsafe path: %s", finalPath.c_str()); + return false; + } + // Collapse duplicate slashes (optional hardening) + while (finalPath.indexOf("//") >= 0) + { + finalPath.replace("//", "/"); + } + + // Size checks + const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB cap (aligned with readFile) + const size_t totalSize = strlen(message); + if (totalSize > MAX_FILE_SIZE) + { + ESP_LOGE(tag, "Write too large: %u bytes for %s", (unsigned)totalSize, finalPath.c_str()); return false; } - // Write with error handling - try + // Write to a temporary file first for atomicity + String tmpPath = finalPath + ".tmp"; + File tmp = fs.open(tmpPath.c_str(), "w"); + if (!tmp) { - size_t bytesWritten = file.print(message); - if (bytesWritten == 0) + ESP_LOGE(tag, "Failed to open temp file: %s", tmpPath.c_str()); + return false; + } + + // Write in a loop to ensure all bytes are written + size_t written = 0; + const uint8_t *buf = reinterpret_cast(message); + while (written < totalSize) + { + size_t n = tmp.write(buf + written, totalSize - written); + if (n == 0) { - ESP_LOGE(tag, "Failed to write to file: %s", path); - file.close(); + ESP_LOGE(tag, "Write failed to temp file: %s at %u/%u bytes", tmpPath.c_str(), (unsigned)written, (unsigned)totalSize); + tmp.close(); + fs.remove(tmpPath.c_str()); return false; } - - // Ensure all data is written - file.flush(); - file.close(); - ESP_LOGD(tag, "Successfully wrote %u bytes to %s", bytesWritten, path); - return true; + written += n; } - catch (const std::exception &e) + + // Flush and close temp file + tmp.flush(); + tmp.close(); + + // Replace the target file atomically: remove existing then rename + if (fs.exists(finalPath.c_str())) { - ESP_LOGE(tag, "Exception while writing file %s: %s", path, e.what()); - file.close(); + if (!fs.remove(finalPath.c_str())) + { + ESP_LOGE(tag, "Failed to remove existing file: %s", finalPath.c_str()); + fs.remove(tmpPath.c_str()); + return false; + } + } + if (!fs.rename(tmpPath.c_str(), finalPath.c_str())) + { + ESP_LOGE(tag, "Failed to rename %s to %s", tmpPath.c_str(), finalPath.c_str()); + fs.remove(tmpPath.c_str()); return false; } + + ESP_LOGD(tag, "Successfully wrote %u bytes to %s", (unsigned)totalSize, finalPath.c_str()); + return true; } char *readFile(fs::FS &fs, const char *path)