[ai] memory optimizations

This commit is contained in:
Stefan Ostermann 2025-11-03 22:49:35 +01:00
parent b97eb79b91
commit c14624ef92
5 changed files with 186 additions and 119 deletions

View File

@ -1,6 +1,10 @@
#include "DirectoryNode.h"
#include "globals.h"
#include <algorithm>
#include <cstring> // strlen, strlcpy, strlcat
#include <strings.h> // 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<String> 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());
}
@ -227,7 +279,10 @@ void DirectoryNode::printDirectoryTree(int level) const
{
Serial.print(F(" "));
}
Serial.println(buildFullPath(mp3File));
// Use buffer for building path
buildFullPath(mp3File, buffer, buffer_size);
Serial.println(buffer);
}
for (DirectoryNode *childNode : subdirectories)
@ -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 &currentGloba
{
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;
@ -484,8 +547,10 @@ DirectoryNode *DirectoryNode::findPreviousMP3Globally(const String &currentGloba
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 &currentGlobal)
{
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 &currentGlobal)
// 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("<li data-id=\""));
out.print(ids[i]);
out.print(F("\">"));
out.print(buildFullPath(mp3Files[i]));
buildFullPath(mp3Files[i], buffer, buffer_size);
out.print(buffer);
out.println(F("</li>"));
#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);
}

View File

@ -18,11 +18,17 @@ private:
std::vector<DirectoryNode*> subdirectories;
std::vector<String> mp3Files;
std::vector<uint16_t> 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<DirectoryNode*>& getSubdirectories() const;
const std::vector<String>& getMP3Files() const;
const String& getDirPath() const;
String getFullPathByIndex(size_t index) const;
size_t getNumOfFiles() const;

View File

@ -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;
/*

View File

@ -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("</td></tr>"));
// Yield occasionally if async server is buffering
out.flush();
yield();
}
out.print(F("</table>"));
@ -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
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
@ -1321,7 +1315,7 @@ void init_webserver() {
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);

View File

@ -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.