[Ai] Improved File upload, some documentation

This commit is contained in:
Stefan Ostermann 2025-05-25 22:38:22 +02:00
parent ec5610c919
commit 1e44745b72
8 changed files with 399 additions and 20 deletions

View File

@ -2,6 +2,7 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"diegoomal.ollama-connection",
"platformio.platformio-ide"
],
"unwantedRecommendations": [

View File

@ -73,6 +73,10 @@ We use three buttons, preferably big and robust. They are grounded if closed, so
Start / Stop -> 17 and GND
Wake up / Next -> 04 and GND
Previous -> 16 and GND
#define BTN_START_STOP 4 // Button on XX and GND
#define BTN_NEXT 17
#define BTN_PREV 16
```
## Battery Voltage

View File

@ -1,5 +1,5 @@
; PlatformIO Project Configuration File
;
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages

View File

@ -87,6 +87,15 @@ uint32_t DirectoryNode::getSecondsPlayed()
void DirectoryNode::buildDirectoryTree(const char *currentPath)
{
// Clear existing data to prevent duplicates when rebuilding
for (DirectoryNode *childNode : subdirectories)
{
delete childNode;
}
subdirectories.clear();
mp3Files.clear();
ids.clear();
File rootDir = SD.open(currentPath);
while (true)
{

View File

@ -37,13 +37,23 @@ const char index_html[] PROGMEM = R"rawliteral(
<button onmouseup="simpleGetCall('stop');" ontouchend="simpleGetCall('stop');">Stop</button>&nbsp;
-->
<p>
<p class="playlist-container">
<h2>🎶 Playlist 🎶</h2>
%DIRECTORY%
</p>
<form method="POST" action="/upload" enctype="multipart/form-data"><input type="file" name="data"/><input type="submit" name="upload" value="Upload" title="Upload File"></form>
<form id="uploadForm" method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="data" id="uploadFile" accept=".mp3,.wav,.flac,.m4a,.ogg"/>
<input type="submit" name="upload" value="Upload" title="Upload Audio File" id="uploadButton"/>
<div id="uploadStatus"></div>
<div id="uploadProgress" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<span id="progressText">0%</span>
</div>
</form>
<h2>Edit RFID Mapping</h2>
<form id="editMappingForm">
<label for="rfid">RFID:</label>
@ -203,6 +213,125 @@ const char index_html[] PROGMEM = R"rawliteral(
};
}
// 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>
</body>
</html>)rawliteral";
</html>)rawliteral";

157
src/css.h
View File

@ -7,6 +7,15 @@ body {
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;
@ -74,4 +83,150 @@ li {
margin: 20px 0; /* Space out elements */
}
)rawliteral";
/* 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";

View File

@ -103,4 +103,4 @@ const long sleepMessageDelay = 1798000;
const long sleepDelay = 1800000;
//const long sleepDelay = 25000;
#endif
#endif

View File

@ -150,27 +150,109 @@ 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;
if (!index) {
logmessage = "Upload Start: " + String(filename);
// open the file on first call and store the file handle in the request object
request->_tempFile = SD.open("/"+filename, FILE_WRITE);
// Validate filename and file extension
if (filename.length() == 0) {
request->send(400, "text/plain", "Invalid filename");
return;
}
// Check if filename has valid extension (mp3, wav, flac, etc.)
String lowerFilename = filename;
lowerFilename.toLowerCase();
if (!lowerFilename.endsWith(".mp3") && !lowerFilename.endsWith(".wav") &&
!lowerFilename.endsWith(".flac") && !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;
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);
// Ensure SD is active
activateSD();
// Check if file already exists and create backup name if needed
String filepath = "/" + filename;
if (SD.exists(filepath)) {
String baseName = filename.substring(0, filename.lastIndexOf('.'));
String extension = filename.substring(filename.lastIndexOf('.'));
int counter = 1;
do {
filepath = "/" + baseName + "_" + String(counter) + extension;
counter++;
} while (SD.exists(filepath) && counter < 100);
if (counter >= 100) {
request->send(409, "text/plain", "Too many files with similar names");
return;
}
Serial.println("File exists, using: " + filepath);
}
// Open the file for writing
request->_tempFile = SD.open(filepath, FILE_WRITE);
if (!request->_tempFile) {
request->send(500, "text/plain", "Failed to create file on SD card");
return;
}
}
if (len) {
// stream the incoming chunk to the opened file
request->_tempFile.write(data, len);
// Check if file handle is valid
if (!request->_tempFile) {
request->send(500, "text/plain", "File handle invalid");
return;
}
//logmessage = "Writing file: " + String(filename) + " index=" + String(index) + " len=" + String(len);
//Serial.println(logmessage);
// Write data and verify bytes written
size_t bytesWritten = request->_tempFile.write(data, len);
if (bytesWritten != len) {
request->_tempFile.close();
request->send(500, "text/plain", "Write error - SD card may be full");
return;
}
// Flush data periodically to ensure it's written
if (index % 8192 == 0) { // Flush every 8KB
request->_tempFile.flush();
}
// Log progress every 100KB
if (index % 102400 == 0) {
logmessage = "Upload progress: " + String(filename) + " - " + humanReadableSize(index + len);
Serial.println(logmessage);
}
}
if (final) {
logmessage = "Upload Complete: " + String(filename) + ",size: " + String(index + len);
// close the file handle as the upload is now done
request->_tempFile.close();
Serial.println(logmessage);
request->redirect("/");
if (request->_tempFile) {
request->_tempFile.flush(); // Ensure all data is written
request->_tempFile.close();
logmessage = "Upload Complete: " + String(filename) + ", size: " + humanReadableSize(index + len);
Serial.println(logmessage);
// Rebuild directory tree to include new file
rootNode.buildDirectoryTree("/");
request->send(200, "text/plain", "Upload successful");
} else {
request->send(500, "text/plain", "Upload failed - file handle was invalid");
}
}
}
@ -957,4 +1039,3 @@ boolean buttonPressed(const uint8_t pin)
return false;
}