[ai] memory optimizations, wifi reset, battery
This commit is contained in:
parent
34c499bd49
commit
9c937dc62d
|
|
@ -234,9 +234,24 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath)
|
||||||
mp3Files.reserve(fileNames.size());
|
mp3Files.reserve(fileNames.size());
|
||||||
ids.reserve(fileNames.size());
|
ids.reserve(fileNames.size());
|
||||||
|
|
||||||
// Create subdirectories in alphabetical order
|
// Add MP3 files in alphabetical order first to free fileNames memory before recursing
|
||||||
for (const String &dirName : dirNames)
|
for (const String &fileName : fileNames)
|
||||||
{
|
{
|
||||||
|
mp3Files.push_back(fileName);
|
||||||
|
ids.push_back(getNextId());
|
||||||
|
}
|
||||||
|
// Free memory used by fileNames vector
|
||||||
|
{
|
||||||
|
std::vector<String> empty;
|
||||||
|
fileNames.swap(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subdirectories in alphabetical order
|
||||||
|
// Use index loop and std::move to free strings in dirNames as we go, reducing stack memory usage during recursion
|
||||||
|
for (size_t i = 0; i < dirNames.size(); ++i)
|
||||||
|
{
|
||||||
|
String dirName = std::move(dirNames[i]); // Move string content out of vector
|
||||||
|
|
||||||
DirectoryNode *newNode = new DirectoryNode(dirName);
|
DirectoryNode *newNode = new DirectoryNode(dirName);
|
||||||
if (!newNode)
|
if (!newNode)
|
||||||
{
|
{
|
||||||
|
|
@ -261,13 +276,6 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath)
|
||||||
|
|
||||||
newNode->buildDirectoryTree(childPath.c_str());
|
newNode->buildDirectoryTree(childPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add MP3 files in alphabetical order (store only filenames; build full paths on demand)
|
|
||||||
for (const String &fileName : fileNames)
|
|
||||||
{
|
|
||||||
mp3Files.push_back(fileName);
|
|
||||||
ids.push_back(getNextId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DirectoryNode::printDirectoryTree(int level) const
|
void DirectoryNode::printDirectoryTree(int level) const
|
||||||
|
|
@ -648,50 +656,3 @@ DirectoryNode *DirectoryNode::advanceToNextMP3(const String ¤tGlobal)
|
||||||
Serial.println(F("no more nodes found"));
|
Serial.println(F("no more nodes found"));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Not used anymore due to new
|
|
||||||
* backpressure-safe, low-heap HTML streaming solution to prevent AsyncTCP cbuf resize OOM during /directory.
|
|
||||||
*
|
|
||||||
* @param out
|
|
||||||
*/
|
|
||||||
void DirectoryNode::streamDirectoryHTML(Print &out) const {
|
|
||||||
#ifdef DEBUG
|
|
||||||
Serial.printf("StreamDirectoryHTML name=%s numOfFiles=%i\n", name, mp3Files.size());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (name == "/") {
|
|
||||||
out.println(F("<ul>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name != "/") {
|
|
||||||
out.print(F("<li data-id=\""));
|
|
||||||
out.print(id);
|
|
||||||
out.print(F("\"><b>"));
|
|
||||||
out.print(name);
|
|
||||||
out.println(F("</b></li>"));
|
|
||||||
yield(); // Yield to allow network stack to send buffered data
|
|
||||||
}
|
|
||||||
|
|
||||||
for (size_t i = 0; i < mp3Files.size(); i++) {
|
|
||||||
out.print(F("<li data-id=\""));
|
|
||||||
out.print(ids[i]);
|
|
||||||
out.print(F("\">"));
|
|
||||||
out.print(mp3Files[i].c_str());
|
|
||||||
out.println(F("</li>"));
|
|
||||||
|
|
||||||
// Yield every few items to allow the async web server to send buffered data
|
|
||||||
if (i % 5 == 4) {
|
|
||||||
yield();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (DirectoryNode* child : subdirectories) {
|
|
||||||
child->streamDirectoryHTML(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name == "/") {
|
|
||||||
out.println(F("</ul>"));
|
|
||||||
yield(); // Final yield before completing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ public:
|
||||||
void buildFlatMP3List(std::vector<std::pair<DirectoryNode*, int>>& allMP3s);
|
void buildFlatMP3List(std::vector<std::pair<DirectoryNode*, int>>& allMP3s);
|
||||||
DirectoryNode* advanceToMP3(const uint16_t id);
|
DirectoryNode* advanceToMP3(const uint16_t id);
|
||||||
void advanceToFirstMP3InThisNode();
|
void advanceToFirstMP3InThisNode();
|
||||||
void streamDirectoryHTML(Print &out) const;
|
|
||||||
DirectoryNode* findFirstDirectoryWithMP3s();
|
DirectoryNode* findFirstDirectoryWithMP3s();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
#ifndef DIRECTORY_WALKER_H
|
||||||
|
#define DIRECTORY_WALKER_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
#include "DirectoryNode.h"
|
||||||
|
|
||||||
|
struct WalkerState {
|
||||||
|
const DirectoryNode* node;
|
||||||
|
uint8_t phase; // 0: Start, 1: Files, 2: Subdirs, 3: End
|
||||||
|
size_t idx; // Index for vectors
|
||||||
|
|
||||||
|
WalkerState(const DirectoryNode* n) : node(n), phase(0), idx(0) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DirectoryWalker {
|
||||||
|
private:
|
||||||
|
std::vector<WalkerState> stack;
|
||||||
|
String pending;
|
||||||
|
size_t pendingOffset;
|
||||||
|
|
||||||
|
void generateNext() {
|
||||||
|
if (stack.empty()) return;
|
||||||
|
|
||||||
|
WalkerState& state = stack.back();
|
||||||
|
const DirectoryNode* node = state.node;
|
||||||
|
|
||||||
|
switch (state.phase) {
|
||||||
|
case 0: // Start
|
||||||
|
if (node->getName() == "/") {
|
||||||
|
pending += F("<ul>\r\n");
|
||||||
|
} else {
|
||||||
|
pending += F("<li data-id=\"");
|
||||||
|
pending += String(node->getId());
|
||||||
|
pending += F("\"><b>");
|
||||||
|
pending += node->getName();
|
||||||
|
pending += F("</b></li>\r\n");
|
||||||
|
}
|
||||||
|
state.phase = 1;
|
||||||
|
state.idx = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1: // Files
|
||||||
|
if (state.idx < node->getMP3Files().size()) {
|
||||||
|
pending += F("<li data-id=\"");
|
||||||
|
pending += String(node->getFileIdAt(state.idx));
|
||||||
|
pending += F("\">");
|
||||||
|
pending += node->getMP3Files()[state.idx];
|
||||||
|
pending += F("</li>\r\n");
|
||||||
|
state.idx++;
|
||||||
|
} else {
|
||||||
|
state.phase = 2;
|
||||||
|
state.idx = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // Subdirs
|
||||||
|
if (state.idx < node->getSubdirectories().size()) {
|
||||||
|
// Push child
|
||||||
|
const DirectoryNode* child = node->getSubdirectories()[state.idx];
|
||||||
|
state.idx++; // Advance index for when we return
|
||||||
|
stack.emplace_back(child);
|
||||||
|
// Next loop will process the child (Phase 0)
|
||||||
|
} else {
|
||||||
|
state.phase = 3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3: // End
|
||||||
|
if (node->getName() == "/") {
|
||||||
|
pending += F("</ul>\r\n");
|
||||||
|
}
|
||||||
|
stack.pop_back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
DirectoryWalker(const DirectoryNode* root) : pendingOffset(0) {
|
||||||
|
if (root) {
|
||||||
|
stack.emplace_back(root);
|
||||||
|
// Reserve some space for pending string to avoid frequent reallocations
|
||||||
|
pending.reserve(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t read(uint8_t* buffer, size_t maxLen) {
|
||||||
|
size_t written = 0;
|
||||||
|
|
||||||
|
while (written < maxLen) {
|
||||||
|
// If pending buffer is empty or fully consumed, generate more
|
||||||
|
if (pending.length() == 0 || pendingOffset >= pending.length()) {
|
||||||
|
pending = ""; // Reset string content (capacity is kept)
|
||||||
|
pendingOffset = 0;
|
||||||
|
|
||||||
|
if (stack.empty()) {
|
||||||
|
break; // Done
|
||||||
|
}
|
||||||
|
generateNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy from pending to output buffer
|
||||||
|
if (pending.length() > pendingOffset) {
|
||||||
|
size_t available = pending.length() - pendingOffset;
|
||||||
|
size_t toCopy = std::min(available, maxLen - written);
|
||||||
|
memcpy(buffer + written, pending.c_str() + pendingOffset, toCopy);
|
||||||
|
written += toCopy;
|
||||||
|
pendingOffset += toCopy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
37
src/main.cpp
37
src/main.cpp
|
|
@ -21,8 +21,10 @@
|
||||||
|
|
||||||
#include "globals.h"
|
#include "globals.h"
|
||||||
#include "DirectoryNode.h"
|
#include "DirectoryNode.h"
|
||||||
|
#include "DirectoryWalker.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
|
||||||
// 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.
|
// 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.
|
||||||
|
|
@ -1306,22 +1308,19 @@ void init_webserver() {
|
||||||
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
webreq_enter();
|
webreq_enter();
|
||||||
|
// Use shared_ptr to manage Walker lifecycle, ensuring it persists as long as the response needs it
|
||||||
|
// and is automatically deleted when the response is finished/destroyed.
|
||||||
|
std::shared_ptr<DirectoryWalker> walker = std::make_shared<DirectoryWalker>(&rootNode);
|
||||||
|
|
||||||
request->onDisconnect([](){ webreq_exit(); });
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
Serial.printf("Serving /directory heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles());
|
Serial.printf("Serving /directory heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles());
|
||||||
#endif
|
#endif
|
||||||
// True chunked response: re-generate output deterministically and skip 'index' bytes each call
|
// True chunked response using stateful walker
|
||||||
AsyncWebServerResponse *response = request->beginChunkedResponse(
|
AsyncWebServerResponse *response = request->beginChunkedResponse(
|
||||||
txt_html_charset,
|
txt_html_charset,
|
||||||
[](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
|
[walker](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
|
||||||
ChunkedSkipBufferPrint sink(buffer, maxLen, index);
|
return walker->read(buffer, maxLen);
|
||||||
// Generate HTML directly into the sink (no large intermediate buffers)
|
|
||||||
rootNode.streamDirectoryHTML(sink);
|
|
||||||
// finished?
|
|
||||||
if (index >= sink.totalProduced()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return sink.bytesWritten();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// Optional headers:
|
// Optional headers:
|
||||||
|
|
@ -1436,6 +1435,14 @@ void init_webserver() {
|
||||||
|
|
||||||
server.on("/move_file", HTTP_GET, handleMoveFile);
|
server.on("/move_file", HTTP_GET, handleMoveFile);
|
||||||
server.on("/delete_file", HTTP_GET, handleDeleteFile);
|
server.on("/delete_file", HTTP_GET, handleDeleteFile);
|
||||||
|
|
||||||
|
server.on("/reset_wifi", HTTP_POST, [](AsyncWebServerRequest *request)
|
||||||
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
|
request->send(200, txt_plain, F("WiFi reset. Device will restart..."));
|
||||||
|
asyncReset = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
|
|
@ -1638,6 +1645,16 @@ void loop()
|
||||||
webrequest_blockings = 0;
|
webrequest_blockings = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (asyncReset)
|
||||||
|
{
|
||||||
|
asyncReset = false;
|
||||||
|
delay(1000);
|
||||||
|
Serial.println(F("Disconnecting WiFi and resetting..."));
|
||||||
|
WiFi.disconnect(true, true);
|
||||||
|
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
if (audio.isRunning())
|
if (audio.isRunning())
|
||||||
{
|
{
|
||||||
if (asyncStop)
|
if (asyncStop)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ bool asyncNext = false;
|
||||||
|
|
||||||
bool asyncPrev = false;
|
bool asyncPrev = false;
|
||||||
|
|
||||||
|
bool asyncReset = false;
|
||||||
|
|
||||||
bool SDActive = false;
|
bool SDActive = false;
|
||||||
|
|
||||||
bool RFIDActive = false;
|
bool RFIDActive = false;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,10 @@
|
||||||
<h1>HannaBox</h1>
|
<h1>HannaBox</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status" id="state">—</div>
|
<div class="header-status-group">
|
||||||
|
<div id="batteryStatus" class="battery-status" title="Battery"></div>
|
||||||
|
<div class="status" id="state">—</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
|
@ -148,6 +151,11 @@
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="deleteFileOnServer()">Delete</button>
|
<button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="deleteFileOnServer()">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<h4>System</h4>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<button class="action-btn" style="background-color: #ef4444;" onclick="resetWifi()">Reset WiFi Settings</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,24 @@ function loadDirectory() {
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetWifi() {
|
||||||
|
if (!confirm('Are you sure you want to reset WiFi settings? The device will restart and create an access point.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/reset_wifi', true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
|
alert('WiFi settings reset. Device is restarting...');
|
||||||
|
} else {
|
||||||
|
alert('Reset failed: ' + (xhr.responseText || 'Unknown error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
function loadMapping() {
|
function loadMapping() {
|
||||||
var el = document.getElementById('mappingList');
|
var el = document.getElementById('mappingList');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
@ -241,6 +259,25 @@ function displayState(state) {
|
||||||
var voltageEl = document.getElementById("voltage");
|
var voltageEl = document.getElementById("voltage");
|
||||||
if (voltageEl) voltageEl.innerHTML = (state['voltage'] || '') + ' mV';
|
if (voltageEl) voltageEl.innerHTML = (state['voltage'] || '') + ' mV';
|
||||||
|
|
||||||
|
// Update header battery indicator
|
||||||
|
var headerBattery = document.getElementById("batteryStatus");
|
||||||
|
if (headerBattery) {
|
||||||
|
var mv = state['voltage'] || 0;
|
||||||
|
if (mv > 0) {
|
||||||
|
// Estimate percentage for single cell LiPo (approx 3.3V - 4.2V)
|
||||||
|
var pct = Math.round((mv - 3300) / (4200 - 3300) * 100);
|
||||||
|
if (pct < 0) pct = 0;
|
||||||
|
if (pct > 100) pct = 100;
|
||||||
|
|
||||||
|
headerBattery.innerHTML =
|
||||||
|
'<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style="opacity:0.7"><path d="M16 4h-1V2a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/></svg>' +
|
||||||
|
'<span>' + pct + '%</span>';
|
||||||
|
headerBattery.title = mv + ' mV';
|
||||||
|
} else {
|
||||||
|
headerBattery.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var heapEl = document.getElementById("heap");
|
var heapEl = document.getElementById("heap");
|
||||||
if (heapEl) heapEl.innerHTML = (state['heap'] || '') + ' bytes free heap';
|
if (heapEl) heapEl.innerHTML = (state['heap'] || '') + ' bytes free heap';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,19 @@ a { color: var(--accent); text-decoration: none; }
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.battery-status {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
/* Status (current song) */
|
/* Status (current song) */
|
||||||
.status {
|
.status {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue