Memory Optimizations

This commit is contained in:
2025-07-06 22:25:49 +02:00
parent fe04474ed8
commit 129fa8e624
13 changed files with 1426 additions and 543 deletions

View File

@@ -96,6 +96,11 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath)
mp3Files.clear();
ids.clear();
// Reserve memory to reduce heap fragmentation (optimization 3)
subdirectories.reserve(8); // Reserve space for 8 subdirectories
mp3Files.reserve(16); // Reserve space for 16 MP3 files
ids.reserve(16); // Reserve space for 16 IDs
File rootDir = SD.open(currentPath);
while (true)
{

View File

@@ -63,275 +63,6 @@ const char index_html[] PROGMEM = R"rawliteral(
<button type="button" onclick="editMapping()">Update Mapping</button>
</form>
<script>
setInterval(getState, 4000);
setInterval(updateProgress, 500); // Update progress every second
// Get the <li> elements
var liElements = document.querySelectorAll('ul li');
var lastChange = 0;
var lastStateUpdateTime = Date.now();
var songStartTime = 0;
var currentSongLength = 0;
var isPlaying = false;
var userIsInteracting = false; // Flag to track user interaction with the slider
// Add click event listener to each <li> element
liElements.forEach(function(li) {
li.addEventListener('click', function() {
//var liText = this.innerText;
var id = this.dataset.id;
playSongById(id);
});
});
function simpleGetCall(endpoint) {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/" + endpoint, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
getState(); // Fetch the latest state right after the button action
}
};
xhr.send();
}
function postValue(endpoint,value) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/" + endpoint, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.send("value="+encodeURIComponent(value));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
getState(); // Fetch the latest state right after the button action
}
};
}
function getState() {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var state = JSON.parse(xhr.response);
isPlaying = state['playing'];
if (isPlaying) {
songStartTime = Date.now() - state['time'] * 1000;
currentSongLength = state['length'] * 1000;
}
lastStateUpdateTime = Date.now();
displayState(state);
}
}
xhr.open("GET","/state", true);
xhr.send();
}
function updateProgress() {
if (isPlaying && !userIsInteracting) { // Check if user is not interacting
var elapsedTime = Date.now() - songStartTime;
if (elapsedTime >= currentSongLength) {
elapsedTime = currentSongLength;
isPlaying = false; // Stop updating if the song has ended
}
var progressElement = document.getElementById('progressSlider');
progressElement.value = elapsedTime / 1000; // Convert to seconds
document.getElementById("progressLabel").innerHTML = Math.floor(elapsedTime / 1000);
}
}
function displayState(state) {
document.getElementById("state").innerHTML = state['title'];
document.getElementById("progressLabel").innerHTML = state['time'];
document.getElementById("voltage").innerHTML = state['voltage']+' mV';
document.getElementById("heap").innerHTML = state['heap']+' bytes free heap';
document.getElementById("uid").innerHTML = 'Last NFC ID: '+state['uid'];
var elements = document.getElementsByClassName('play-button');
var btn = elements[0];
if (state['playing']) {
btn.classList.add('paused');
} else {
btn.classList.remove('paused');
}
if (Date.now()-lastChange>1200) {
var progress = document.getElementById('progressSlider');
progress.value = state['time'];
progress.max = state['length'];
var volume = document.getElementById('volumeSlider');
volume.value = state['volume'];
}
updateProgress();
}
function playSongById(id) {
var url = "/playbyid";
var params = "id="+id;
var http = new XMLHttpRequest();
http.open("GET", url+"?"+params, true);
http.onreadystatechange = function()
{
if(http.readyState == 4 && http.status == 200) {
getState();
}
}
http.send(null);
}
function playNamedSong(song) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/playnamed");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//application/x-www-form-urlencoded
var body = song;
xhr.send("title="+encodeURIComponent(body));
}
function editMapping() {
var rfid = document.getElementById('rfid').value;
var song = document.getElementById('song').value;
var xhr = new XMLHttpRequest();
xhr.open("POST", "/edit_mapping", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.send("rfid=" + encodeURIComponent(rfid) + "&song=" + encodeURIComponent(song));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
alert("Mapping updated successfully!");
}
};
}
// Validate file before upload
function validateFile(file) {
var maxSize = 50 * 1024 * 1024; // 50MB limit
var allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/flac', 'audio/mp4', 'audio/ogg'];
var allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.ogg'];
if (file.size > maxSize) {
return 'File too large. Maximum size is 50MB.';
}
var fileName = file.name.toLowerCase();
var hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension) {
return 'Invalid file type. Only audio files (.mp3, .wav, .flac, .m4a, .ogg) are allowed.';
}
return null; // No error
}
// Handle form submission with AJAX to show upload status
document.getElementById('uploadForm').addEventListener('submit', function(event) {
event.preventDefault(); // Prevent the default form submit
var fileInput = document.getElementById('uploadFile');
var file = fileInput.files[0];
if (!file) {
alert('Please select a file to upload.');
return;
}
var validationError = validateFile(file);
if (validationError) {
alert(validationError);
return;
}
var form = event.target;
var formData = new FormData(form);
var uploadButton = document.getElementById('uploadButton');
var uploadStatus = document.getElementById('uploadStatus');
var uploadProgress = document.getElementById('uploadProgress');
var progressFill = document.getElementById('progressFill');
var progressText = document.getElementById('progressText');
// Disable upload button and show progress
uploadButton.disabled = true;
uploadButton.value = 'Uploading...';
uploadProgress.style.display = 'block';
uploadStatus.innerHTML = 'Preparing upload...';
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);
xhr.upload.onloadstart = function() {
uploadStatus.innerHTML = 'Upload started...';
};
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percentComplete = Math.round((event.loaded / event.total) * 100);
progressFill.style.width = percentComplete + '%';
progressText.innerHTML = percentComplete + '%';
uploadStatus.innerHTML = 'Uploading: ' + percentComplete + '% (' +
Math.round(event.loaded / 1024) + 'KB / ' +
Math.round(event.total / 1024) + 'KB)';
}
};
xhr.upload.onerror = function() {
uploadStatus.innerHTML = 'Upload failed due to network error.';
resetUploadForm();
};
xhr.upload.onabort = function() {
uploadStatus.innerHTML = 'Upload was cancelled.';
resetUploadForm();
};
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // Request is done
if (xhr.status >= 200 && xhr.status < 300) { // Success status code range
uploadStatus.innerHTML = 'Upload completed successfully!';
progressFill.style.width = '100%';
progressText.innerHTML = '100%';
setTimeout(function() {
alert('File uploaded successfully!');
location.reload(); // Reload to get updated playlist
}, 1000);
} else {
var errorMsg = xhr.responseText || 'Unknown error occurred';
uploadStatus.innerHTML = 'Upload failed: ' + errorMsg;
alert('Upload failed: ' + errorMsg);
resetUploadForm();
}
}
};
xhr.send(formData); // Send the form data using XMLHttpRequest
});
function resetUploadForm() {
var uploadButton = document.getElementById('uploadButton');
var uploadProgress = document.getElementById('uploadProgress');
var progressFill = document.getElementById('progressFill');
var progressText = document.getElementById('progressText');
uploadButton.disabled = false;
uploadButton.value = 'Upload';
uploadProgress.style.display = 'none';
progressFill.style.width = '0%';
progressText.innerHTML = '0%';
// Clear file input
document.getElementById('uploadFile').value = '';
}
</script>
<script src="/script.js"></script>
</body>
</html>)rawliteral";

235
src/css.h
View File

@@ -1,232 +1,3 @@
const char css[] PROGMEM = R"rawliteral(
body {
font-family: Arial, sans-serif;
margin: 0 auto;
padding: 20px;
text-align: center; /* Center align elements */
background-color: #f4f4f4; /* Light background */
}
.playlist-container {
max-height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-top: 20px;
text-align: left; /* Align playlist text to the left */
}
li {
cursor: pointer;
list-style-type: none;
}
.play-button, .next-button, .prev-button {
border: 0;
background: transparent;
cursor: pointer;
transition: background-color 0.3s ease; /* Smooth transition for hover */
}
.play-button {
box-sizing: border-box;
margin: 5% auto;
height: 50px; /* Consistent size for play button */
border-color: transparent transparent transparent #007bff;
border-style: solid;
border-width: 25px 0 25px 40px; /* Adjusted size */
}
.play-button.paused {
border-style: double;
border-width: 25px 0 25px 40px; /* Same size for pause button */
height: 50px; /* Consistent height */
}
.play-button:hover {
border-color: transparent transparent transparent #0056b3; /* Darker blue on hover */
}
.next-button, .prev-button {
padding: 0;
margin: 10px;
border-color: transparent #007bff transparent #007bff;
border-style: solid;
}
.next-button {
border-width: 15px 0 15px 25px;
box-shadow: 8px 0 0 0 #007bff;
}
.next-button:hover {
border-color: transparent #0056b3 transparent #0056b3;
box-shadow: 8px 0 0 0 #0056b3;
}
.prev-button {
border-width: 15px 25px 15px 0;
box-shadow: -8px 0 0 0 #007bff;
}
.prev-button:hover {
border-color: transparent #0056b3 transparent #0056b3;
box-shadow: -8px 0 0 0 #0056b3;
}
.slider {
width: 90%; /* Make slider wider for easier interaction */
margin: 10px auto; /* Center align the slider */
}
.slidecontainer {
margin: 20px 0; /* Space out elements */
}
/* Upload progress bar styles */
.progress-bar {
width: 100%;
height: 20px;
background-color: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
width: 0%;
transition: width 0.3s ease;
border-radius: 10px;
}
#uploadProgress {
margin: 15px 0;
text-align: center;
}
#progressText {
font-weight: bold;
color: #007bff;
margin-left: 10px;
}
#uploadStatus {
margin: 10px 0;
padding: 8px;
border-radius: 4px;
font-weight: bold;
}
#uploadStatus:not(:empty) {
background-color: #e7f3ff;
border: 1px solid #007bff;
color: #0056b3;
}
/* Form styling improvements */
#uploadForm {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin: 20px 0;
}
#uploadButton {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-left: 10px;
transition: background-color 0.3s ease;
}
#uploadButton:hover:not(:disabled) {
background-color: #0056b3;
}
#uploadButton:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
#uploadFile {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
margin-right: 10px;
}
/* RFID mapping form styling */
#editMappingForm {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin: 20px 0;
text-align: left;
}
#editMappingForm label {
display: block;
margin: 10px 0 5px 0;
font-weight: bold;
}
#editMappingForm input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
box-sizing: border-box;
}
#editMappingForm button {
background-color: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
transition: background-color 0.3s ease;
}
#editMappingForm button:hover {
background-color: #218838;
}
/* Responsive design improvements */
@media (max-width: 600px) {
body {
padding: 10px;
}
.slider {
width: 95%;
}
#uploadForm, #editMappingForm {
padding: 15px;
}
#uploadButton, #editMappingForm button {
width: 100%;
margin: 10px 0;
}
#uploadFile {
width: 100%;
margin: 10px 0;
}
}
)rawliteral";
// CSS content moved to SD card at /system/style.css
// This saves approximately 4KB of flash memory
const char css[] PROGMEM = "";

View File

@@ -47,7 +47,6 @@
#include "globals.h"
#include "WebContent.h"
#include "css.h"
#include "DirectoryNode.h"
#define SOUND_STARTUP "start.mp3"
@@ -94,15 +93,12 @@ bool SDActive = false;
bool RFIDActive = false;
bool webrequestActive = false;
uint16_t voltage_threshold_counter = 0;
size_t free_heap = 0;
/*
std::map<String, String> rfid_map{{"67 152 204 14", "01-The_Box_Tops-The_Letter.mp3"},
{"67 175 148 160", "068-Der_Schatz_im_Bergsee"}};
*/
void activateSD()
{
@@ -149,7 +145,7 @@ String humanReadableSize(const size_t bytes) {
}
void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
String logmessage;
static String logBuffer; // Static to avoid repeated allocations
if (!index) {
// Validate filename and file extension
@@ -158,28 +154,31 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
return;
}
// Check if filename has valid extension (mp3, wav, flac, etc.)
String lowerFilename = filename;
lowerFilename.toLowerCase();
// Use const reference to avoid string copies
const String& lowerFilename = filename;
if (!lowerFilename.endsWith(".mp3") && !lowerFilename.endsWith(".wav") &&
!lowerFilename.endsWith(".flac") && !lowerFilename.endsWith(".m4a") &&
!lowerFilename.endsWith(".ogg")) {
!lowerFilename.endsWith(".m4a") && !lowerFilename.endsWith(".ogg")) {
request->send(400, "text/plain", "Invalid file type. Only audio files are allowed.");
return;
}
// Check available SD card space
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
uint64_t usedBytes = SD.usedBytes() / (1024 * 1024);
uint64_t freeSpace = cardSize - usedBytes;
// More efficient space check using bit shift
uint32_t freeSpace = (SD.cardSize() - SD.usedBytes()) >> 20; // Bit shift instead of division
if (freeSpace < 10) { // Less than 10MB free
request->send(507, "text/plain", "Insufficient storage space");
return;
}
logmessage = "Upload Start: " + String(filename) + " (Free space: " + String(freeSpace) + "MB)";
Serial.println(logmessage);
// Pre-allocate log buffer
logBuffer.reserve(128);
logBuffer = "Upload Start: ";
logBuffer += filename;
logBuffer += " (Free: ";
logBuffer += String(freeSpace);
logBuffer += "MB)";
Serial.println(logBuffer);
logBuffer.clear(); // Free memory immediately
// Ensure SD is active
activateSD();
@@ -199,7 +198,8 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
request->send(409, "text/plain", "Too many files with similar names");
return;
}
Serial.println("File exists, using: " + filepath);
Serial.print("File exists, using: ");
Serial.println(filepath);
}
// Open the file for writing
@@ -208,7 +208,6 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
request->send(500, "text/plain", "Failed to create file on SD card");
return;
}
}
if (len) {
@@ -227,14 +226,16 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
}
// Flush data periodically to ensure it's written
if (index % 8192 == 0) { // Flush every 8KB
if (index % 4096 == 0) { // Flush every 4KB
request->_tempFile.flush();
}
// Log progress every 100KB
if (index % 102400 == 0) {
logmessage = "Upload progress: " + String(filename) + " - " + humanReadableSize(index + len);
Serial.println(logmessage);
// Reduce logging frequency to save memory - log every 200KB instead of 100KB
if (len && (index % 204800 == 0)) {
logBuffer = "Upload: ";
logBuffer += humanReadableSize(index + len);
Serial.println(logBuffer);
logBuffer.clear();
}
}
@@ -243,8 +244,12 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
request->_tempFile.flush(); // Ensure all data is written
request->_tempFile.close();
logmessage = "Upload Complete: " + String(filename) + ", size: " + humanReadableSize(index + len);
Serial.println(logmessage);
logBuffer = "Upload Complete: ";
logBuffer += filename;
logBuffer += ", size: ";
logBuffer += humanReadableSize(index + len);
Serial.println(logBuffer);
logBuffer.clear();
// Rebuild directory tree to include new file
rootNode.buildDirectoryTree("/");
@@ -455,13 +460,16 @@ boolean readSongProgress(const char *filename) {
String getState()
{
DynamicJsonDocument jsonState(1024);
// Use static buffer to avoid repeated allocations
static DynamicJsonDocument jsonState(512);
jsonState.clear(); // Clear previous data
jsonState["playing"] = audio.isRunning();
if (currentNode != nullptr)
jsonState["title"] = *currentNode->getCurrentPlaying();
else
jsonState["title"] = "Angehalten";
jsonState["title"] = "Angehalten"; // Store in flash
jsonState["time"] = audio.getAudioCurrentTime();
jsonState["volume"] = audio.getVolume();
@@ -469,10 +477,11 @@ String getState()
jsonState["voltage"] = lastVoltage;
jsonState["uid"] = lastUid;
jsonState["heap"] = free_heap;
String output;
output.reserve(256); // Pre-allocate string buffer
serializeJson(jsonState, output);
jsonState.clear();
jsonState.garbageCollect();
return output;
}
@@ -531,11 +540,11 @@ std::map<String, String> readDataFromFile(const char *filename) {
String processor(const String &var)
{
if (var == "DIRECTORY")
if (var == "DIRECTORY")
{
return rootNode.getDirectoryStructureHTML();
}
return String();
return String(); // Return empty string instead of creating new String
}
void stop()
@@ -718,6 +727,9 @@ void setup()
*/
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(volume); // 0...21
// Optimize audio buffer size to save memory (ESP32-audioI2S optimization)
audio.setBufferSize(8192); // Reduced from default large buffer (saves 40-600KB!)
Serial.println("Audio initialized.");
@@ -725,10 +737,19 @@ void setup()
free_heap = xPortGetFreeHeapSize();
// first parameter is name of access point, second is the password
AsyncWiFiManager wifiManager(&server, &dns);
wifiManager.setTimeout(180);
// Memory optimizations for WiFiManager
wifiManager.setDebugOutput(false); // Disable debug strings
wifiManager.setMinimumSignalQuality(20); // Reduce AP scan results
wifiManager.setRemoveDuplicateAPs(true); // Remove duplicate APs from memory
// Reduce timeouts to free memory faster
wifiManager.setTimeout(60); // Reduced from 180
wifiManager.setConnectTimeout(15); // Faster connection attempts
wifiManager.setConfigPortalTimeout(60); // Shorter portal timeout
Serial.println("Deactivating Brownout detector...");
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
@@ -737,11 +758,60 @@ void setup()
if (wifiManager.autoConnect("HannaBox"))
{
/*
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send_P(200, "text/html charset=UTF-8", index_html, processor); });
*/
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
deactivateRFID();
activateSD();
String htmlPath = "/system/index.html";
if (SD.exists(htmlPath)) {
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html", false, processor);
response->addHeader("Content-Type", "text/html; charset=UTF-8");
request->send(response);
} else {
// Fallback: serve minimal error if file not found
request->send(200, "text/plain", "ERROR: /system/index.html on SD Card not found!");
}
webrequestActive = false;
});
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send_P(200, "text/css", css); });
{
webrequestActive = true;
deactivateRFID();
activateSD();
String cssPath = "/system/style.css";
if (SD.exists(cssPath)) {
request->send(SD, cssPath, "text/css");
} else {
// Fallback: serve minimal CSS if file not found
request->send(200, "text/css", "body{font-family:Arial;text-align:center;padding:20px;}");
}
webrequestActive = false;
});
server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
deactivateRFID();
activateSD();
String jsPath = "/system/script.js";
if (SD.exists(jsPath)) {
request->send(SD, jsPath, "application/javascript");
} else {
// Fallback: serve minimal JS if file not found
request->send(200, "application/javascript", "console.log('JavaScript file not found on SD card');");
}
webrequestActive = false;
});
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
{
@@ -806,7 +876,7 @@ void setup()
xTaskCreatePinnedToCore(
loop2, /* Function to implement the task */
"RFIDTask", /* Name of the task */
10000, /* Stack size in words */
4096, /* Stack size in words - reduced from 10000 to 4096 (optimization 2) */
NULL, /* Task input parameter */
0, /* Priority of the task */
&RfidTask, /* Task handle. */
@@ -892,7 +962,7 @@ void loop()
start();
}
if (continuePlaying) {
if (continuePlaying && !webrequestActive) {
continuePlaying = false;
startupSoundPlayed = true;
playSongById(currentSongId,currentSongSeconds);
@@ -966,7 +1036,7 @@ void loop()
if (loopCounter % RFID_LOOP_INTERVAL == 0)
if (loopCounter % RFID_LOOP_INTERVAL == 0 && !webrequestActive)
{
deactivateSD();
activateRFID();
@@ -1022,7 +1092,7 @@ void loop2(void *parameter)
Serial.println("loop2 started");
loggingDone = true;
}
vTaskDelay(1);
//vTaskDelay(1);
}
}