From c14624ef920cdc32b6c68f6aa9c2c163b64021de Mon Sep 17 00:00:00 2001 From: Stefan Ostermann Date: Mon, 3 Nov 2025 22:49:35 +0100 Subject: [PATCH] [ai] memory optimizations --- src/DirectoryNode.cpp | 134 ++++++++++++++++++++++++++--------- src/DirectoryNode.h | 7 +- src/globals.h | 2 +- src/main.cpp | 160 ++++++++++++++++++++---------------------- src/main.h | 2 +- 5 files changed, 186 insertions(+), 119 deletions(-) diff --git a/src/DirectoryNode.cpp b/src/DirectoryNode.cpp index fc00f3c..8d0ad32 100644 --- a/src/DirectoryNode.cpp +++ b/src/DirectoryNode.cpp @@ -1,6 +1,10 @@ #include "DirectoryNode.h" #include "globals.h" #include +#include // strlen, strlcpy, strlcat +#include // strcasecmp + +char DirectoryNode::buffer[DirectoryNode::buffer_size]; DirectoryNode::DirectoryNode(const String &nodeName) : name(nodeName), currentPlaying("") @@ -42,14 +46,7 @@ const String &DirectoryNode::getDirPath() const return dirPath; } -String DirectoryNode::getFullPathByIndex(size_t index) const -{ - if (index < mp3Files.size()) - { - return buildFullPath(mp3Files[index]); - } - return String(); -} + String DirectoryNode::buildFullPath(const String &fileName) const { @@ -65,11 +62,49 @@ String DirectoryNode::buildFullPath(const String &fileName) const return p; } +bool DirectoryNode::comparePathWithString(const char* path, const String& target) const +{ + // Convert target to char* for comparison + const char* targetStr = target.c_str(); + + // Case-insensitive string comparison + return strcasecmp(path, targetStr) == 0; +} + +void DirectoryNode::buildFullPath(const String &fileName, char* out, size_t n) const +{ + if (n == 0) return; + out[0] = '\0'; + + if (dirPath == "/") + { + strlcat(out, "/", n); + } + else + { + strlcpy(out, dirPath.c_str(), n); + strlcat(out, "/", n); + } + + strlcat(out, fileName.c_str(), n); +} + void DirectoryNode::setCurrentPlaying(const String &mp3File) { bool isAbs = (mp3File.length() > 0) && (mp3File.charAt(0) == '/'); const String &fileName = isAbs ? mp3File.substring(mp3File.lastIndexOf('/') + 1) : mp3File; - currentPlaying = isAbs ? mp3File : buildFullPath(fileName); + + if (isAbs) + { + currentPlaying = mp3File; // Already absolute path + } + else + { + // Use buffer for building relative path + buildFullPath(fileName, buffer, buffer_size); + currentPlaying = String(buffer); // Convert back to String for assignment + } + for (size_t i = 0; i < mp3Files.size(); i++) { if (mp3Files[i] == fileName && ids.size() > i) @@ -128,9 +163,6 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath) subdirectories.clear(); mp3Files.clear(); ids.clear(); - subdirectories.shrink_to_fit(); - mp3Files.shrink_to_fit(); - ids.shrink_to_fit(); // Set directory path for this node (normalize: keep "/" or remove trailing slash) String path = String(currentPath); @@ -145,6 +177,12 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath) std::vector fileNames; File rootDir = SD.open(currentPath); + if (!rootDir) + { + Serial.print(F("buildDirectoryTree: failed to open path: ")); + Serial.println(currentPath); + return; + } while (true) { File entry = rootDir.openNextFile(); @@ -153,7 +191,7 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath) break; } - if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir)) + if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir) != 0) { dirNames.emplace_back(entry.name()); } @@ -195,12 +233,26 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath) for (const String &dirName : dirNames) { DirectoryNode *newNode = new DirectoryNode(dirName); + if (!newNode) + { + Serial.println(F("buildDirectoryTree: OOM creating DirectoryNode")); + continue; + } subdirectories.push_back(newNode); - String childPath = String(currentPath); - if (!childPath.endsWith("/")) + String childPath; + childPath.reserve(dirPath.length() + 1 + dirName.length()); + if (dirPath == "/") + { + childPath = "/"; + childPath += dirName; + } + else + { + childPath = dirPath; childPath += "/"; - childPath += dirName; + childPath += dirName; + } newNode->buildDirectoryTree(childPath.c_str()); } @@ -220,16 +272,19 @@ void DirectoryNode::printDirectoryTree(int level) const Serial.print(F(" ")); } Serial.println(name); - + for (const String &mp3File : mp3Files) { for (int i = 0; i <= level; i++) { Serial.print(F(" ")); } - Serial.println(buildFullPath(mp3File)); + + // Use buffer for building path + buildFullPath(mp3File, buffer, buffer_size); + Serial.println(buffer); } - + for (DirectoryNode *childNode : subdirectories) { childNode->printDirectoryTree(level + 1); @@ -274,7 +329,8 @@ DirectoryNode *DirectoryNode::advanceToMP3(const uint16_t id) if (id == ids[i]) { // Found the current MP3 file - currentPlaying = buildFullPath(mp3Files[i]); + buildFullPath(mp3Files[i], buffer, buffer_size); + currentPlaying = String(buffer); // Convert back to String for assignment currentPlayingId = id; return this; } @@ -329,7 +385,9 @@ DirectoryNode *DirectoryNode::advanceToMP3(const String &songName) { if (isAbsolutePath) { - if (buildFullPath(mp3Files[i]).equalsIgnoreCase(songName)) + // Use static buffer for path building and comparison + buildFullPath(mp3Files[i], buffer, buffer_size); + if (comparePathWithString(buffer, songName)) { setCurrentPlaying(mp3Files[i]); return this; @@ -337,7 +395,9 @@ DirectoryNode *DirectoryNode::advanceToMP3(const String &songName) } else { - String f = mp3Files[i]; + // Use static buffer for case conversion and comparison + buildFullPath(mp3Files[i], buffer, buffer_size); + String f = String(buffer); f.toLowerCase(); if (f.endsWith(lowTarget)) { @@ -431,7 +491,8 @@ DirectoryNode *DirectoryNode::goToPreviousMP3(uint32_t thresholdSeconds) int currentIndex = -1; for (size_t i = 0; i < mp3Files.size(); i++) { - if (currentPlaying == buildFullPath(mp3Files[i])) + buildFullPath(mp3Files[i], buffer, buffer_size); + if (comparePathWithString(buffer, currentPlaying)) { currentIndex = i; break; @@ -442,7 +503,7 @@ DirectoryNode *DirectoryNode::goToPreviousMP3(uint32_t thresholdSeconds) if (currentIndex > 0) { Serial.print(F("goToPreviousMP3: Moving to previous song in same directory: ")); - Serial.println(buildFullPath(mp3Files[currentIndex - 1])); + Serial.println(mp3Files[currentIndex - 1]); setCurrentPlaying(mp3Files[currentIndex - 1]); return this; } @@ -471,7 +532,9 @@ DirectoryNode *DirectoryNode::findPreviousMP3Globally(const String ¤tGloba { DirectoryNode *node = allMP3s[i].first; int fileIndex = allMP3s[i].second; - if (node->buildFullPath(node->mp3Files[fileIndex]) == currentGlobal) + + node->buildFullPath(node->mp3Files[fileIndex], buffer, buffer_size); + if (comparePathWithString(buffer, currentGlobal)) { currentGlobalIndex = i; break; @@ -483,10 +546,12 @@ DirectoryNode *DirectoryNode::findPreviousMP3Globally(const String ¤tGloba { DirectoryNode *prevNode = allMP3s[currentGlobalIndex - 1].first; int prevFileIndex = allMP3s[currentGlobalIndex - 1].second; - + + prevNode->buildFullPath(prevNode->mp3Files[prevFileIndex], buffer, buffer_size); + Serial.print(F("findPreviousMP3Globally: Moving to previous song globally: ")); - Serial.println(prevNode->buildFullPath(prevNode->mp3Files[prevFileIndex])); - + Serial.println(buffer); + prevNode->setCurrentPlaying(prevNode->mp3Files[prevFileIndex]); return prevNode; } @@ -525,7 +590,8 @@ DirectoryNode *DirectoryNode::advanceToNextMP3(const String ¤tGlobal) { for (size_t i = 0; i < mp3Files.size(); i++) { - if (currentGlobal == buildFullPath(mp3Files[i])) + buildFullPath(mp3Files[i], buffer, buffer_size); + if (currentGlobal == String(buffer)) { // Found the current playing MP3 file if (i < mp3Files.size() - 1) @@ -555,7 +621,8 @@ DirectoryNode *DirectoryNode::advanceToNextMP3(const String ¤tGlobal) // Have each subdirectory advance its song for (size_t i = 0; i < subdir->mp3Files.size(); i++) { - if (currentGlobal == subdir->buildFullPath(subdir->mp3Files[i])) + subdir->buildFullPath(subdir->mp3Files[i], buffer, buffer_size); + if (currentGlobal == String(buffer)) { // Found the current playing MP3 file if (i < subdir->mp3Files.size() - 1) @@ -603,10 +670,11 @@ void DirectoryNode::streamDirectoryHTML(Print &out) const { out.print(F("
  • ")); - out.print(buildFullPath(mp3Files[i])); + buildFullPath(mp3Files[i], buffer, buffer_size); + out.print(buffer); out.println(F("
  • ")); #ifdef DEBUG - Serial.printf("stream song: %s\n", buildFullPath(mp3Files[i]).c_str()); + Serial.printf("stream song: %s\n", buffer); #endif // Yield every few items to allow the async web server to send buffered data if (i % 5 == 4) { @@ -614,6 +682,8 @@ void DirectoryNode::streamDirectoryHTML(Print &out) const { } } + out.flush(); + for (DirectoryNode* child : subdirectories) { child->streamDirectoryHTML(out); } diff --git a/src/DirectoryNode.h b/src/DirectoryNode.h index 626b7b2..8dc0d4e 100644 --- a/src/DirectoryNode.h +++ b/src/DirectoryNode.h @@ -18,11 +18,17 @@ private: std::vector subdirectories; std::vector mp3Files; std::vector ids; + static const size_t path_size = 256; String currentPlaying; uint16_t currentPlayingId = 0; uint16_t secondsPlayed = 0; + static const size_t buffer_size = path_size; + static char buffer[buffer_size]; String buildFullPath(const String& fileName) const; + void buildFullPath(const String &fileName, char* buffer, size_t bufferSize) const; + bool comparePathWithString(const char* path, const String& target) const; + public: @@ -36,7 +42,6 @@ public: const std::vector& getSubdirectories() const; const std::vector& getMP3Files() const; const String& getDirPath() const; - String getFullPathByIndex(size_t index) const; size_t getNumOfFiles() const; diff --git a/src/globals.h b/src/globals.h index efd97cb..4a182da 100644 --- a/src/globals.h +++ b/src/globals.h @@ -31,7 +31,7 @@ static const char* hdr_connection_key = "Connection"; static const char* hdr_connection_val = "close"; -const size_t buffer_size = 256; +const size_t buffer_size = 80; /* diff --git a/src/main.cpp b/src/main.cpp index a292727..f8333ce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,39 @@ // webrequest_blockings is a simple watchdog counter that tracks how long at least one HTTP request has been “active” (not yet disconnected) according to the AsyncWebServer. int webrequest_blockings = 0; +// Prebuilt system paths to avoid repeated String allocations +static String SYS_PREFIX; +static String PATH_INDEX, PATH_INDEX_GZ, PATH_STYLE, PATH_STYLE_GZ, PATH_SCRIPT, PATH_SCRIPT_GZ; +static String PATH_MAPPING, PATH_PROGRESS, PATH_CONFIG, PATH_SLEEP, PATH_STARTUP; + +static inline void buildSystemPathsOnce() { + if (!SYS_PREFIX.isEmpty()) return; + + SYS_PREFIX = "/"; + SYS_PREFIX += sys_dir; + SYS_PREFIX += "/"; + + auto make = [](const char* name) { + String s; + s = SYS_PREFIX; + s += name; + return s; + }; + + PATH_INDEX = make(index_file); + PATH_STYLE = make(style_file); + PATH_SCRIPT = make(script_file); + PATH_MAPPING = make(mapping_file); + PATH_PROGRESS= make(progress_file); + PATH_CONFIG = make(config_file); + PATH_SLEEP = make(sleep_sound); + PATH_STARTUP = make(startup_sound); + + PATH_INDEX_GZ = PATH_INDEX; PATH_INDEX_GZ += F(".gz"); + PATH_STYLE_GZ = PATH_STYLE; PATH_STYLE_GZ += F(".gz"); + PATH_SCRIPT_GZ = PATH_SCRIPT; PATH_SCRIPT_GZ += F(".gz"); +} + void activateSD() { @@ -118,27 +151,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, filepath += filename; if (SD.exists(filepath)) { - String baseName = filename.substring(0, filename.lastIndexOf('.')); - String extension = filename.substring(filename.lastIndexOf('.')); - int counter = 1; - filepath.reserve(1 + baseName.length() + 1 + 10 + extension.length()); - do - { - filepath = "/"; - filepath += baseName; - filepath += "_"; - filepath += counter; - filepath += extension; - counter++; - } while (SD.exists(filepath) && counter < 100); - - if (counter >= 100) - { - request->send(409, txt_plain, F("Too many files w sim names")); - return; - } - Serial.print(F("File exists, using: ")); - Serial.println(filepath); + request->send(500, txt_plain, F("File already exists.")); } // Open the file for writing (guard with simple mutex) @@ -266,9 +279,6 @@ uint32_t getBatteryVoltageMv() { uint32_t voltage = analogReadMilliVolts(BAT_VOLTAGE_PIN); voltage *= 2; //*2 because of the voltage divider. - // Serial.print("Battery Voltage: "); - // Serial.println(voltage); - // Serial.println(" mV"); return voltage; } @@ -735,7 +745,7 @@ void editMapping(AsyncWebServerRequest *request) } rfid_map[rfid] = MappingEntry(song, mode); - saveMappingToFile(getSysDir(mapping_file)); + saveMappingToFile(PATH_MAPPING); request->send_P(200, txt_plain, PSTR("Mapping updated")); } else @@ -901,6 +911,7 @@ static void streamMappingHTML(Print &out) out.print(pair.second.mode); out.print(F("")); // Yield occasionally if async server is buffering + out.flush(); yield(); } out.print(F("")); @@ -975,7 +986,7 @@ void togglePlayPause() { if (currentNode != NULL) { - writeSongProgress(getSysDir(progress_file).c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); + writeSongProgress(PATH_PROGRESS.c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); audio.pauseResume(); } else @@ -1247,37 +1258,22 @@ static void serveStaticFile(AsyncWebServerRequest *request, } void init_webserver() { + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - static String htmlPath = ""; - static String htmlPathGz = ""; - if (htmlPath.isEmpty()) { - htmlPath = getSysDir(index_file); - htmlPathGz = htmlPath + F(".gz"); - } - serveStaticFile(request, htmlPath, htmlPathGz, txt_html_charset, hdr_cache_control_val, F("ERROR: /system/index.html(.gz) not found!"), true); + serveStaticFile(request, PATH_INDEX, PATH_INDEX_GZ, txt_html_charset, hdr_cache_control_val, F("ERROR: /system/index.html(.gz) not found!"), true); + + }); server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) { - static String cssPath = ""; - static String cssPathGz = ""; - if (cssPath.isEmpty()) { - cssPath = getSysDir(style_file); - cssPathGz = cssPath + F(".gz"); - } - serveStaticFile(request, cssPath, cssPathGz, "text/css", "public, max-age=300", F("ERROR: /system/style.css(.gz) not found!"), true); + serveStaticFile(request, PATH_STYLE, PATH_STYLE_GZ, "text/css", "public, max-age=300", F("ERROR: /system/style.css(.gz) not found!"), true); }); server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request) { - static String jsPath = ""; - static String jsPathGz = ""; - if (jsPath.isEmpty()) { - jsPath = getSysDir(script_file); - jsPathGz = jsPath + F(".gz"); - } - serveStaticFile(request, jsPath, jsPathGz, "application/javascript", "public, max-age=300", F("ERROR: /system/script.js(.gz) not found!"), true); + serveStaticFile(request, PATH_SCRIPT, PATH_SCRIPT_GZ, "application/javascript", "public, max-age=300", F("ERROR: /system/script.js(.gz) not found!"), true); }); // Dynamic endpoints to avoid template processing heap spikes @@ -1287,9 +1283,7 @@ void init_webserver() { request->onDisconnect([](){ webreq_exit(); }); // Stream the response directly from the directory tree to avoid large temporary Strings AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset, buffer_size); -#ifdef DEBUG - Serial.printf("Serving /directory heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles()); -#endif + Serial.printf("Serving /directory heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles()); stream->addHeader(hdr_cache_control_key, hdr_cache_control_val); stream->addHeader(hdr_connection_key, hdr_connection_val); // Generate HTML directly into the stream under lock @@ -1303,7 +1297,7 @@ void init_webserver() { request->onDisconnect([](){ webreq_exit();}); // Stream mapping to avoid building a large HTML String AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset, buffer_size); -#ifdef DEBUG +#ifdef DEBUG Serial.printf("Serving /mapping heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); #endif stream->addHeader(hdr_cache_control_key, hdr_cache_control_val); @@ -1317,11 +1311,11 @@ void init_webserver() { { webreq_enter(); request->onDisconnect([](){ webreq_exit(); }); -#ifdef DEBUG +#ifdef DEBUG Serial.printf("Serving /state heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); #endif // Stream JSON directly to avoid DynamicJsonDocument/String allocations - AsyncResponseStream* stream = request->beginResponseStream(F("application/json; charset=UTF-8"), 256); + AsyncResponseStream* stream = request->beginResponseStream(F("application/json; charset=UTF-8"), buffer_size); stream->addHeader(hdr_cache_control_key, hdr_cache_control_val); stream->addHeader(hdr_connection_key, hdr_connection_val); streamStateJSON(*stream); @@ -1416,6 +1410,7 @@ void setup() Serial.print(F("Initializing SD card...")); activateSD(); Serial.println(F("SD initialization done.")); + buildSystemPathsOnce(); // Seed RNG for shuffle mode #if defined(ESP32) @@ -1433,10 +1428,11 @@ void setup() rootNode.buildDirectoryTree("/"); rootNode.printDirectoryTree(); + Serial.printf("Heap after dir tree: %u\n", (unsigned)xPortGetFreeHeapSize()); - readDataFromFile(getSysDir(mapping_file)); + readDataFromFile(PATH_MAPPING); - String progressPath = getSysDir(progress_file); + String progressPath = PATH_PROGRESS; continuePlaying = config.startAtStoredProgress && readSongProgress(progressPath.c_str()); @@ -1471,8 +1467,8 @@ void setup() audio.setVolume(config.initialVolume); // Use config value volume = config.initialVolume; // Update global volume variable - // Optimize audio buffer size to save memory (ESP32-audioI2S optimization) - audio.setBufferSize(8192); // Reduced from default large buffer (saves 40-600KB!) + // Optimize audio buffer size to save heap (lower = less RAM, but risk of underflow on high bitrates) + audio.setBufferSize(8000); Serial.println(F("Audio init")); @@ -1499,8 +1495,11 @@ void setup() if (wifiManager.autoConnect("HannaBox")) { + Serial.printf("Heap before init_webserver: %u\n", (unsigned)xPortGetFreeHeapSize()); init_webserver(); + Serial.printf("Heap before server.begin: %u\n", (unsigned)xPortGetFreeHeapSize()); server.begin(); + Serial.printf("Heap after server.begin: %u\n", (unsigned)xPortGetFreeHeapSize()); Serial.println(F("Wifi init")); } else @@ -1514,7 +1513,7 @@ void setup() xTaskCreatePinnedToCore( loop2, /* Function to implement the task */ "RFIDTask", /* Name of the task */ - 4096, /* Stack size in words - reduced from 10000 to 4096 (optimization 2) */ + 2048, /* Stack size in words - reduced from 4096 to 2048 to free heap */ NULL, /* Task input parameter */ 0, /* Priority of the task */ &RfidTask, /* Task handle. */ @@ -1577,25 +1576,15 @@ void volume_action(AsyncWebServerRequest *request) request->send_P(200, txt_plain, PSTR("ok")); } -const String getSysDir(const String filename) -{ - String st_sys_str(96); - st_sys_str.clear(); - st_sys_str.concat("/"); - st_sys_str.concat(sys_dir); - st_sys_str.concat("/"); - st_sys_str.concat(filename); - return st_sys_str; -} void loop() { if (webreq_cnt > 0 && webrequest_blockings > MAX_WEBREQUEST_BLOCKINGS) { - Serial.println(F("excessive webrequest blocking - suppress reset")); - // Avoid resetting server mid-response to prevent mixing headers/body or truncation - server.reset(); - init_webserver(); - webreq_cnt = 0; + if (!server_reset_pending) { + Serial.println(F("excessive webrequest blocking - scheduling server reset")); + server_reset_pending = true; + } + // reset the counter to avoid repeated scheduling while still busy webrequest_blockings = 0; } @@ -1638,7 +1627,7 @@ void loop() else if (!startupSoundPlayed) { startupSoundPlayed = true; - playSongByPath(getSysDir(startup_sound)); + playSongByPath(PATH_STARTUP); } // send device to sleep: @@ -1650,18 +1639,12 @@ void loop() prepareSleepMode = true; if (currentNode != nullptr) { - static String progressPath = ""; - if (progressPath.isEmpty()) { - progressPath = getSysDir(progress_file); - } - - deactivateRFID(); activateSD(); - writeSongProgress(progressPath.c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); + writeSongProgress(PATH_PROGRESS.c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); } - playSongByPath(getSysDir(sleep_sound)); + playSongByPath(PATH_SLEEP); } if (now - lastInteraction > config.sleepDelay) @@ -1708,7 +1691,7 @@ void loop() } audio.setVolume(vol); volume = vol; - playSongByPath(getSysDir(startup_sound)); + playSongByPath(PATH_STARTUP); } } } @@ -1743,7 +1726,7 @@ void loop() } audio.setVolume(vol); volume = vol; - playSongByPath(getSysDir(startup_sound)); + playSongByPath(PATH_STARTUP); } } } @@ -1793,6 +1776,15 @@ void loop() webrequest_blockings = 0; } + // Perform deferred server reset when no active requests to avoid AsyncTCP stack corruption + if (server_reset_pending && webreq_cnt == 0) { + Serial.println(F("performing deferred server reset")); + server.reset(); + init_webserver(); + server.begin(); + server_reset_pending = false; + } + loopCounter++; vTaskDelay(1); diff --git a/src/main.h b/src/main.h index 40a1b07..1c92d80 100644 --- a/src/main.h +++ b/src/main.h @@ -88,6 +88,7 @@ bool RFIDActive = false; volatile uint32_t webreq_cnt = 0; static inline void webreq_enter() { __sync_add_and_fetch(&webreq_cnt, 1); } static inline void webreq_exit() { __sync_sub_and_fetch(&webreq_cnt, 1); } +volatile bool server_reset_pending = false; uint16_t voltage_threshold_counter = 0; @@ -111,7 +112,6 @@ void init_webserver(); boolean buttonPressed(const uint8_t pin); -const String getSysDir(const String filename); /** * Helper routine to dump a byte array as hex values to Serial.