Browse Source

feat (Filebrowser): added file browser to RFID-assignment

* added a couple of JavaScript libraries
* added functions to main.cpp for file handling
* added websocket handller for index file notifications
* added webserver route for delivering file index
* refactored html notifications, introduced toastr
master
Mario Lukas 5 years ago
parent
commit
b099448f03
  1. 228
      html/website.html
  2. 12
      platformio.ini
  3. 221
      src/main.cpp
  4. 145
      src/websiteMgmt.h

228
html/website.html

@ -5,9 +5,26 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://ts-cs.de/tonuino/css/bootstrap.min.css"> <link rel="stylesheet" href="https://ts-cs.de/tonuino/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/themes/default/style.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" />
<script src="https://ts-cs.de/tonuino/js/jquery.min.js"></script> <script src="https://ts-cs.de/tonuino/js/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.0/jquery-ui.min.js"></script>
<script src="https://ts-cs.de/tonuino/js/popper.min.js"></script> <script src="https://ts-cs.de/tonuino/js/popper.min.js"></script>
<script src="https://ts-cs.de/tonuino/js/bootstrap.min.js"></script> <script src="https://ts-cs.de/tonuino/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/jstree.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
<style type="text/css">
.filetree {
border: 1px solid black;
margin: 0em 0em 1em 0em;
height: 200px;
overflow-y: scroll;
}
.fa-sync:hover{
color: #666666;
}
</style>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-sm bg-primary navbar-dark"> <nav class="navbar navbar-expand-sm bg-primary navbar-dark">
@ -75,17 +92,23 @@
<input type="text" class="form-control" id="rfidIdMusic" maxlength="12" pattern="[0-9]{12}" placeholder="%RFID_TAG_ID%" name="rfidIdMusic" required> <input type="text" class="form-control" id="rfidIdMusic" maxlength="12" pattern="[0-9]{12}" placeholder="%RFID_TAG_ID%" name="rfidIdMusic" required>
<label for="fileOrUrl">Datei, Verzeichnis oder URL (^ und # als Zeichen nicht erlaubt)</label> <label for="fileOrUrl">Datei, Verzeichnis oder URL (^ und # als Zeichen nicht erlaubt)</label>
<input type="text" class="form-control" id="fileOrUrl" maxlength="255" placeholder="z.B. /mp3/Hoerspiele/Yakari/Yakari_und_seine_Freunde.mp3" pattern="^[^\^#]+$" name="fileOrUrl" required> <input type="text" class="form-control" id="fileOrUrl" maxlength="255" placeholder="z.B. /mp3/Hoerspiele/Yakari/Yakari_und_seine_Freunde.mp3" pattern="^[^\^#]+$" name="fileOrUrl" required>
<div id="filebrowser">
<div class="filetree demo" id="filetree"></div>
<div style="width:100%; margin-botton:1em; text-align: right; font-size: 0.8em;">
<span id="refreshAction" data-toggle="tooltip" data-placement="top" title="Datei Liste aktualisieren."><i class="fas fa-sync fa-1x"></i></span>
</div>
</div>
<label for="playMode">Abspielmodus</label> <label for="playMode">Abspielmodus</label>
<select class="form-control" id="playMode" name="playMode"> <select class="form-control" id="playMode" name="playMode">
<option value="1">Einzelner Titel</option>
<option value="2">Einzelner Titel (Endlosschleife)</option>
<option value="3">Hörbuch</option>
<option value="4">Hörbuch (Endlosschleife)</option>
<option value="5">Alle Titel eines Verzeichnis (sortiert)</option>
<option value="6">Alle Titel eines Verzeichnis (zufällig)</option>
<option value="7">Alle Titel eines Verzeichnis (sortiert, Endlosschleife)</option>
<option value="9">Alle Titel eines Verzeichnis (zufällig, Endlosschleife)</option>
<option value="8">Webradio</option>
<option class="option-file" value="1">Einzelner Titel</option>
<option class="option-file" value="2">Einzelner Titel (Endlosschleife)</option>
<option class="option-folder" value="3">Hörbuch</option>
<option class="option-folder" value="4">Hörbuch (Endlosschleife)</option>
<option class="option-folder" value="5">Alle Titel eines Verzeichnis (sortiert)</option>
<option class="option-folder" value="6">Alle Titel eines Verzeichnis (zufällig)</option>
<option class="option-folder" value="7">Alle Titel eines Verzeichnis (sortiert, Endlosschleife)</option>
<option class="option-folder" value="9">Alle Titel eines Verzeichnis (zufällig, Endlosschleife)</option>
<option class="option-stream" value="8">Webradio</option>
</select> </select>
</div> </div>
<button type="reset" class="btn btn-secondary">Reset</button> <button type="reset" class="btn btn-secondary">Reset</button>
@ -212,40 +235,140 @@
</form> </form>
<div class="messages col-md-6 my-2"></div> <div class="messages col-md-6 my-2"></div>
</div> </div>
<script>
<script type="text/javascript">
var DEBUG = true;
var lastIdclicked = ''; var lastIdclicked = '';
var errorBox = '<div class="alert alert-danger alert-dismissible fade show" role="alert">Es ist ein Fehler aufgetreten!<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button></div>';
var okBox = '<div class="alert alert-success alert-dismissible fade show" role="alert">Aktion erfolgreich ausgeführt.<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button></div>';
var socket = new WebSocket("ws://%IPv4%/ws");
var host = $(location).attr('hostname');
function connect() {
socket = new WebSocket("ws://%IPv4%/ws");
if(DEBUG){
host = "192.168.178.112";
} }
function ping() {
var myObj = {
"ping": {
ping: 'ping'
}
toastr.options = {
"closeButton": false,
"debug": false,
"newestOnTop": false,
"progressBar": false,
"positionClass": "toast-top-right",
"preventDuplicates": false,
"onclick": null,
"showDuration": "300",
"hideDuration": "1000",
"timeOut": "5000",
"extendedTimeOut": "1000",
"showEasing": "swing",
"hideEasing": "linear",
"showMethod": "fadeIn",
"hideMethod": "fadeOut"
}; };
var myJSON = JSON.stringify(myObj);
socket.send(myJSON);
tm = setTimeout(function () {
alert("Die Verbindung zum Tonuino ist unterbrochen!\\nBitte Seite neu laden.");
}, 5000);
function postRendering(event, data){
Object.keys(data.instance._model.data).forEach(function(key,index) {
var cur = data.instance.get_node(data.instance._model.data[key]);
var lastFolder = cur['id'].split('/').filter(function(el) {
return el.trim().length > 0;
}).pop();
if ((/\.(mp3|MP3|ogg|wav|WAV|OGG|wma|WMA|acc|ACC|flac|FLAC)$/i).test(lastFolder)){
data.instance.set_type(data.instance._model.data[key], 'audio');
} else {
if (data.instance._model.data[key]['type'] == "file"){
data.instance.disable_node(data.instance._model.data[key]);
}
}
data.instance.rename_node(data.instance._model.data[key], lastFolder);
});
} }
function pong() {
clearTimeout(tm);
function renderFileTree(){
var filesURI = $(location).attr('protocol') + host + "/files";
if(DEBUG) {
filesURI = "http://"+ host +"/files";
}
$('#filetree').jstree({
'core' : {
'check_callback': true,
'data' : {
url: filesURI,
data: function(data) {
if(data['state']['failed'] || !data['state']['loaded']){
toastr.warning("Verzeichnise konnten nicht geladen werden.")
}
console.log(data['state']);
} }
}
},
'types' : {
'folder' : {
'icon' : "fa fa-folder"
},
'file' : {
'icon': "fa fa-file"
},
'audio' : {
'icon' : "fa fa-file-audio"
},
'default' : {
'icon' : "fa fa-folder"
}
},
'plugins' : [ "themes", "types" ]
}).bind('loaded.jstree', function (event, data) {
postRendering(event, data);
}).bind('refresh.jstree',function (event, data) {
postRendering(event, data);
});
}
$('#filetree').on('select_node.jstree', function (e, data) {
$('input[name=fileOrUrl]').val(data.node.id);
if(data.node.type == "folder"){
console.log(data.node.type)
$('.option-folder').show();
$('.option-file').hide();
// preselect first folder action in list
$('#playMode option').removeAttr('selected').filter('[value=3]').attr('selected', true);
}
if (data.node.type == "audio"){
$('.option-file').show();
$('.option-folder').hide();
$('#playMode option').removeAttr('selected').filter('[value=1]').attr('selected', true);
}
});
$('#refreshAction').on("click", function() {
refreshFileList();
$("#refreshAction i").addClass("fa-spin");
});
$('#playMode').on("change", function () {
if (this.value == 8){
$('#filebrowser').slideUp();
} else {
$('#filebrowser').slideDown();
}
});
var socket = undefined;
function connect() {
socket = new WebSocket("ws://" + host + "/ws");
socket.onopen = function () { socket.onopen = function () {
setInterval(ping, 15000); setInterval(ping, 15000);
}; };
socket.onclose = function(e) { socket.onclose = function(e) {
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason); console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
//toastr.error('Die Websocket Verbindung wurde unterbrochen.');
setTimeout(function() { setTimeout(function() {
connect(); connect();
}, 5000); }, 5000);
@ -253,6 +376,7 @@
socket.onerror = function(err) { socket.onerror = function(err) {
console.error('Socket encountered error: ', err.message, 'Closing socket'); console.error('Socket encountered error: ', err.message, 'Closing socket');
toastr.error('Es gab einen Fehler bei der Websocket Verbindung');
socket.close(); socket.close();
}; };
@ -262,21 +386,54 @@
if (socketMsg.rfidId != null) { if (socketMsg.rfidId != null) {
document.getElementById('rfidIdMod').value = socketMsg.rfidId; document.getElementById('rfidIdMod').value = socketMsg.rfidId;
document.getElementById('rfidIdMusic').value = socketMsg.rfidId; document.getElementById('rfidIdMusic').value = socketMsg.rfidId;
$("#rfidIdMusic").fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500);
$("#rfidIdMod").fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500);
toastr.info("RFID Tag mit "+ socketMsg.rfidId + " erkannt." );
$("#rfidIdMusic").effect("highlight", {color:"#abf5af"}, 3000);
$("#rfidIdMod").effect("highlight", {color:"#abf5af"}, 3000);
} if (socketMsg.status != null) { } if (socketMsg.status != null) {
if (socketMsg.status == 'ok') { if (socketMsg.status == 'ok') {
$("#" + lastIdclicked).find('.messages').html(okBox);
toastr.success("Aktion erfolgreich ausgeführt." );
} else { } else {
$("#" + lastIdclicked).find('.messages').html(errorBox);
toastr.error("Es ist ein Fehler aufgetreten." );
} }
} if (socketMsg.pong != null) { } if (socketMsg.pong != null) {
if (socketMsg.pong == 'pong') { if (socketMsg.pong == 'pong') {
pong(); pong();
} }
} if ("refreshFileList" in socketMsg){
toastr.info("Die Datei Liste wurde neu erzeugt!");
$("#refreshAction i").removeClass("fa-spin");
$('#filetree').jstree(true).refresh();
} }
}; };
}
function ping() {
var myObj = {
"ping": {
ping: 'ping'
}
};
var myJSON = JSON.stringify(myObj);
socket.send(myJSON);
tm = setTimeout(function () {
toastr.warning('Die Verbindung zum Tonuino ist unterbrochen! Bitte Seite neu laden.');
}, 5000);
}
function pong() {
clearTimeout(tm);
}
function refreshFileList(clickedId){
lastIdclicked = clickedId;
var myObj = {
"refreshFileList": true
};
var myJSON = JSON.stringify(myObj);
socket.send(myJSON);
};
function genSettings(clickedId) { function genSettings(clickedId) {
lastIdclicked = clickedId; lastIdclicked = clickedId;
@ -374,6 +531,15 @@
var myJSON = JSON.stringify(myObj); var myJSON = JSON.stringify(myObj);
socket.send(myJSON); socket.send(myJSON);
} }
$(document).ready(function(){
connect();
renderFileTree();
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
});
</script> </script>
</body> </body>
</html> </html>

12
platformio.ini

@ -8,16 +8,16 @@
; Please visit documentation for the other options and examples ; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html ; https://docs.platformio.org/page/projectconf.html
;[env:nodemcu-32s]
[env:lolin32]
[env:az-delivery-devkit-v4]
platform = espressif32 platform = espressif32
;board = nodemcu-32s
board = lolin32
board = az-delivery-devkit-v4
framework = arduino framework = arduino
monitor_speed = 115200 monitor_speed = 115200
board_build.partitions = no_ota.csv board_build.partitions = no_ota.csv
;board_build.partitions = min_spiffs.csv
upload_port = /dev/cu.SLAB_USBtoUART
monitor_port = /dev/cu.SLAB_USBtoUART
build_flags = -O2
build_unflags = -Os
lib_deps = lib_deps =
https://github.com/schreibfaul1/ESP32-audioI2S.git https://github.com/schreibfaul1/ESP32-audioI2S.git

221
src/main.cpp

@ -1,13 +1,13 @@
// Define modules to compile: // Define modules to compile:
#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd
//#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd
#define FTP_ENABLE // Enables FTP-server #define FTP_ENABLE // Enables FTP-server
#define NEOPIXEL_ENABLE // Don't forget configuration of NUM_LEDS if enabled #define NEOPIXEL_ENABLE // Don't forget configuration of NUM_LEDS if enabled
#define NEOPIXEL_REVERSE_ROTATION // Some Neopixels are adressed/soldered counter-clockwise. This can be configured here. #define NEOPIXEL_REVERSE_ROTATION // Some Neopixels are adressed/soldered counter-clockwise. This can be configured here.
#define LANGUAGE 1 // 1 = deutsch; 2 = english #define LANGUAGE 1 // 1 = deutsch; 2 = english
#define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone)
//#define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone)
//#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!) //#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!)
#define SHUTDOWN_IF_SD_BOOT_FAILS // Will put ESP to deepsleep if boot fails due to SD. Really recommend this if there's in battery-mode no other way to restart ESP! Interval adjustable via deepsleepTimeAfterBootFails. #define SHUTDOWN_IF_SD_BOOT_FAILS // Will put ESP to deepsleep if boot fails due to SD. Really recommend this if there's in battery-mode no other way to restart ESP! Interval adjustable via deepsleepTimeAfterBootFails.
#define MEASURE_BATTERY_VOLTAGE // Enables battery-measurement via GPIO (ADC) and voltage-divider
//#define MEASURE_BATTERY_VOLTAGE // Enables battery-measurement via GPIO (ADC) and voltage-divider
//#define PLAY_LAST_RFID_AFTER_REBOOT // When restarting Tonuino, the last RFID that was active before, is recalled and played //#define PLAY_LAST_RFID_AFTER_REBOOT // When restarting Tonuino, the last RFID that was active before, is recalled and played
@ -67,12 +67,16 @@
#define LOGLEVEL_DEBUG 4 // almost everything #define LOGLEVEL_DEBUG 4 // almost everything
// Serial-logging-configuration // Serial-logging-configuration
const uint8_t serialDebug = LOGLEVEL_INFO; // Current loglevel for serial console
const uint8_t serialDebug = LOGLEVEL_DEBUG; // Current loglevel for serial console
// Serial-logging buffer // Serial-logging buffer
uint8_t serialLoglength = 200; uint8_t serialLoglength = 200;
char *logBuf = (char*) calloc(serialLoglength, sizeof(char)); // Buffer for all log-messages char *logBuf = (char*) calloc(serialLoglength, sizeof(char)); // Buffer for all log-messages
// File Browser
uint8_t FS_DEPTH = 3; // max recursion depth of file tree
const char * DIRECTORY_INDEX_FILE = "/files.json"; // filename of files.json index file
// GPIOs (uSD card-reader) // GPIOs (uSD card-reader)
#define SPISD_CS 15 #define SPISD_CS 15
#ifndef SINGLE_SPI_ENABLE #ifndef SINGLE_SPI_ENABLE
@ -523,6 +527,195 @@ void IRAM_ATTR onTimer() {
} }
#endif #endif
/**
* Creates a new file on the SD Card.
* @param fs
* @param path
* @param message
*/
void createFile(fs::FS &fs, const char * path, const char * message){
snprintf(logBuf, serialLoglength, "Writing file: %s\n", path);
loggerNl(logBuf, LOGLEVEL_DEBUG);
File file = fs.open(path, FILE_WRITE);
if(!file){
snprintf(logBuf, serialLoglength, "Failed to open file for writing");
loggerNl(logBuf, LOGLEVEL_ERROR);
return;
}
if(file.print(message)){
snprintf(logBuf, serialLoglength, "File written");
loggerNl(logBuf, LOGLEVEL_DEBUG);
} else {
Serial.println("Write failed");
snprintf(logBuf, serialLoglength, "Write failed");
loggerNl(logBuf, LOGLEVEL_ERROR);
}
file.close();
}
/**
* Appends raw input to a file
* @param fs
* @param path
* @param text
*/
void appendToFile(fs::FS &fs, const char *path, const char *text){
File file = fs.open(path, FILE_APPEND);
esp_task_wdt_reset();
file.print(text);
file.close();
}
// indicates if the given node is first node of file
bool isFirstJSONtNode = true;
/**
* Helper function for writing file index to json file.
* This function appends a new json node for files/directories to
* a given file.
* @param fs
* @param path
* @param filename
* @param parent
* @param type
*/
void appendNodeToJSONFile(fs::FS &fs, const char * path, const char *filename, const char *parent, const char *type ) {
// Serial.printf("Appending to file: %s\n", path);
snprintf(logBuf, serialLoglength, "Listing directory: %s\n", filename);
loggerNl(logBuf, LOGLEVEL_DEBUG);
File file = fs.open(path, FILE_APPEND);
// i/o is timing critical keep all stuff running
esp_task_wdt_reset();
if(!file){
snprintf(logBuf, serialLoglength, "Failed to open file for appending");
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
if (!isFirstJSONtNode){
file.print(",");
}
//TODO: write a minified json, without all those whitespaces
// it is just easier to debug when json is in a nice format
// anyway ugly but works and is stable
file.print( " {\n \"id\" : \"");
file.print(filename);
file.print("\",\n \"parent\" : \"");
file.print(parent);
file.print("\",\n \"type\": \"");
file.print(type);
file.print("\",\n \"text\" : \"");
file.print(filename);
file.print("\"\n }");
// i/o is timing critical keep all stuff running
esp_task_wdt_reset();
file.close();
if(isFirstJSONtNode){
isFirstJSONtNode = false;
}
}
/**
* Checks if a path is valid. (e.g. hidden path is not valid)
* @param _fileItem
* @return
*/
bool pathValid(const char *_fileItem) {
const char ch = '/';
char *subst;
subst = strrchr(_fileItem, ch); // Don't use files that start with .
return (!startsWith(subst, (char *) "/."));
}
/**
* SD-Card index parser. Parses the SD Card directories
* by a given file path depth recursive and appends the
* found files and directories to files.json file.
* @param fs
* @param dirname
* @param parent
* @param levels
*/
void parseSDFileList(fs::FS &fs, const char * dirname, const char * parent, uint8_t levels){
char fileNameBuf[255];
// i/o is timing critical keep all stuff running
esp_task_wdt_reset();
File root = fs.open(dirname);
if(!root){
snprintf(logBuf, serialLoglength, "Failed to open directory");
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
if(!root.isDirectory()){
snprintf(logBuf, serialLoglength, "Not a directory");
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
File file = root.openNextFile();
while(file){
esp_task_wdt_reset();
const char *parent;
if (strcmp(root.name(), "/") == 0 || root.name() == 0){
parent = "#\0";
} else {
parent = root.name();
}
if (file.name() == 0 ){
continue;
}
strncpy(fileNameBuf, (char *) file.name(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0]));
// we have a folder
if(file.isDirectory()){
if (pathValid(fileNameBuf)){
appendNodeToJSONFile(SD, DIRECTORY_INDEX_FILE, fileNameBuf, parent, "folder" );
// check for next subfolder
if(levels){
parseSDFileList(fs, fileNameBuf, root.name(), levels -1);
}
}
// we have a file
} else {
if (fileValid(fileNameBuf)){
appendNodeToJSONFile(SD, DIRECTORY_INDEX_FILE, fileNameBuf, parent, "file" );
}
}
file = root.openNextFile();
// i/o is timing critical keep all stuff running
esp_task_wdt_reset();
}
}
// TODO: maybe this is not save with asyncWebserver
bool indexingIsRunning = false;
/**
* Public function for creating file index json on SD-Card.
* It notifies the user client via websockets when the indexing
* is done.
*/
void createJSONFileList(){
if(!indexingIsRunning){
indexingIsRunning = true;
createFile(SD, DIRECTORY_INDEX_FILE, "[\n");
parseSDFileList(SD, "/", NULL, FS_DEPTH);
appendToFile(SD, DIRECTORY_INDEX_FILE, "]");
isFirstJSONtNode = true;
sendWebsocketData(0, 30);
indexingIsRunning = false;
}
}
// Measures voltage of a battery as per interval or after bootup (after allowing a few seconds to settle down) // Measures voltage of a battery as per interval or after bootup (after allowing a few seconds to settle down)
#ifdef MEASURE_BATTERY_VOLTAGE #ifdef MEASURE_BATTERY_VOLTAGE
float measureBatteryVoltage(void) { float measureBatteryVoltage(void) {
@ -2792,6 +2985,8 @@ void accessPointStart(const char *SSID, IPAddress ip, IPAddress netmask) {
ESP.restart(); ESP.restart();
}); });
// allow cors for local debug
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
wServer.begin(); wServer.begin();
loggerNl((char *) FPSTR(httpReady), LOGLEVEL_NOTICE); loggerNl((char *) FPSTR(httpReady), LOGLEVEL_NOTICE);
accessPointStarted = true; accessPointStarted = true;
@ -3093,6 +3288,9 @@ bool processJsonRequest(char *_serialJson) {
} }
} else if (doc.containsKey("ping")) { } else if (doc.containsKey("ping")) {
sendWebsocketData(0, 20); sendWebsocketData(0, 20);
return false;
} else if (doc.containsKey("refreshFileList")) {
createJSONFileList();
} }
return true; return true;
@ -3113,6 +3311,8 @@ void sendWebsocketData(uint32_t client, uint8_t code) {
object["rfidId"] = currentRfidTagId; object["rfidId"] = currentRfidTagId;
} else if (code == 20) { } else if (code == 20) {
object["pong"] = "pong"; object["pong"] = "pong";
} else if (code == 30){
object["refreshFileList"] = "ready";
} }
char jBuf[50]; char jBuf[50];
serializeJson(doc, jBuf, sizeof(jBuf) / sizeof(jBuf[0])); serializeJson(doc, jBuf, sizeof(jBuf) / sizeof(jBuf[0]));
@ -3147,14 +3347,10 @@ void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsE
if (info->final && info->index == 0 && info->len == len) { if (info->final && info->index == 0 && info->len == len) {
//the whole message is in a single frame and we got all of it's data //the whole message is in a single frame and we got all of it's data
Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len);
uint8_t returnCode;
if (processJsonRequest((char*)data)) { if (processJsonRequest((char*)data)) {
returnCode = 1;
} else {
returnCode = 0;
}
sendWebsocketData(client->id(), 1); sendWebsocketData(client->id(), 1);
}
if (info->opcode == WS_TEXT) { if (info->opcode == WS_TEXT) {
data[len] = 0; data[len] = 0;
@ -3251,7 +3447,14 @@ void webserverStart(void) {
ESP.restart(); ESP.restart();
}); });
wServer.on("/files", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(SD, "/files.json", "application/json");
});
wServer.onNotFound(notFound); wServer.onNotFound(notFound);
// allow cors for local debug
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
wServer.begin(); wServer.begin();
webserverStarted = true; webserverStarted = true;
} }

145
src/websiteMgmt.h

@ -5,9 +5,26 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<meta charset=\"utf-8\">\ <meta charset=\"utf-8\">\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
<link rel=\"stylesheet\" href=\"https://ts-cs.de/tonuino/css/bootstrap.min.css\">\ <link rel=\"stylesheet\" href=\"https://ts-cs.de/tonuino/css/bootstrap.min.css\">\
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/themes/default/style.min.css\" />\
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css\"/>\
<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css\" />\
<script src=\"https://ts-cs.de/tonuino/js/jquery.min.js\"></script>\ <script src=\"https://ts-cs.de/tonuino/js/jquery.min.js\"></script>\
<script src=\"https://code.jquery.com/ui/1.12.0/jquery-ui.min.js\"></script>\
<script src=\"https://ts-cs.de/tonuino/js/popper.min.js\"></script>\ <script src=\"https://ts-cs.de/tonuino/js/popper.min.js\"></script>\
<script src=\"https://ts-cs.de/tonuino/js/bootstrap.min.js\"></script>\ <script src=\"https://ts-cs.de/tonuino/js/bootstrap.min.js\"></script>\
<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/jstree.min.js\"></script>\
<script src=\"https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js\"></script>\
<style type=\"text/css\">\
.filetree {\
border: 1px solid black;\
margin: 0em 0em 1em 0em;\
height: 200px;\
overflow-y: scroll;\
}\
.fa-sync:hover{\
color: #666666;\
}\
</style>\
</head>\ </head>\
<body>\ <body>\
<nav class=\"navbar navbar-expand-sm bg-primary navbar-dark\">\ <nav class=\"navbar navbar-expand-sm bg-primary navbar-dark\">\
@ -75,6 +92,12 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<input type=\"text\" class=\"form-control\" id=\"rfidIdMusic\" maxlength=\"12\" pattern=\"[0-9]{12}\" placeholder=\"%RFID_TAG_ID%\" name=\"rfidIdMusic\" required>\ <input type=\"text\" class=\"form-control\" id=\"rfidIdMusic\" maxlength=\"12\" pattern=\"[0-9]{12}\" placeholder=\"%RFID_TAG_ID%\" name=\"rfidIdMusic\" required>\
<label for=\"fileOrUrl\">Datei, Verzeichnis oder URL (^ und # als Zeichen nicht erlaubt)</label>\ <label for=\"fileOrUrl\">Datei, Verzeichnis oder URL (^ und # als Zeichen nicht erlaubt)</label>\
<input type=\"text\" class=\"form-control\" id=\"fileOrUrl\" maxlength=\"255\" placeholder=\"z.B. /mp3/Hoerspiele/Yakari/Yakari_und_seine_Freunde.mp3\" pattern=\"^[^\\^#]+$\" name=\"fileOrUrl\" required>\ <input type=\"text\" class=\"form-control\" id=\"fileOrUrl\" maxlength=\"255\" placeholder=\"z.B. /mp3/Hoerspiele/Yakari/Yakari_und_seine_Freunde.mp3\" pattern=\"^[^\\^#]+$\" name=\"fileOrUrl\" required>\
<div id=\"filebrowser\">\
<div class=\"filetree demo\" id=\"filetree\"></div>\
<div style=\"width:100%; margin-botton:1em; text-align: right; font-size: 0.8em;\">\
<span id=\"refreshAction\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Datei Liste aktualisieren.\"><i class=\"fas fa-sync fa-1x\"></i></span>\
</div>\
</div>\
<label for=\"playMode\">Abspielmodus</label>\ <label for=\"playMode\">Abspielmodus</label>\
<select class=\"form-control\" id=\"playMode\" name=\"playMode\">\ <select class=\"form-control\" id=\"playMode\" name=\"playMode\">\
<option value=\"1\">Einzelner Titel</option>\ <option value=\"1\">Einzelner Titel</option>\
@ -212,10 +235,99 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
</form>\ </form>\
<div class=\"messages col-md-6 my-2\"></div>\ <div class=\"messages col-md-6 my-2\"></div>\
</div>\ </div>\
<script>\
<script type=\"text/javascript\">\
var lastIdclicked = '';\ var lastIdclicked = '';\
var errorBox = '<div class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">Es ist ein Fehler aufgetreten!<button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button></div>';\
var okBox = '<div class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">Aktion erfolgreich ausgeführt.<button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button></div>';\
\
$(function () {\
$('[data-toggle=\"tooltip\"]').tooltip();\
});\
\
toastr.options = {\
\"closeButton\": false,\
\"debug\": false,\
\"newestOnTop\": false,\
\"progressBar\": false,\
\"positionClass\": \"toast-top-right\",\
\"preventDuplicates\": false,\
\"onclick\": null,\
\"showDuration\": \"300\",\
\"hideDuration\": \"1000\",\
\"timeOut\": \"5000\",\
\"extendedTimeOut\": \"1000\",\
\"showEasing\": \"swing\",\
\"hideEasing\": \"linear\",\
\"showMethod\": \"fadeIn\",\
\"hideMethod\": \"fadeOut\"\
};\
\
function postRendering(event, data){\
Object.keys(data.instance._model.data).forEach(function(key,index) {\
\
var cur = data.instance.get_node(data.instance._model.data[key]);\
var lastFolder = cur['id'].split('/').filter(function(el) {\
return el.trim().length > 0;\
}).pop();\
if ((/\\.(mp3|MP3|ogg|wav|WAV|OGG|wma|WMA|acc|ACC|flac|FLAC)$/i).test(lastFolder)){\
data.instance.set_type(data.instance._model.data[key], 'audio');\
} else {\
if (data.instance._model.data[key]['type'] == \"file\"){\
data.instance.disable_node(data.instance._model.data[key]);\
}\
}\
data.instance.rename_node(data.instance._model.data[key], lastFolder);\
});\
}\
\
function renderFileTree(){\
$('#filetree').jstree({\
'core' : {\
'check_callback': true,\
'data' : {\
url: \"/files\",\
data : function (node) {\
console.log(node);\
}\
}\
},\
'types' : {\
'folder' : {\
'icon' : \"fa fa-folder\"\
},\
'file' : {\
'icon': \"fa fa-file\"\
},\
'audio' : {\
'icon' : \"fa fa-file-audio\"\
},\
'default' : {\
'icon' : \"fa fa-folder\"\
}\
},\
'plugins' : [ \"themes\", \"types\" ]\
}).bind('loaded.jstree', function (event, data) {\
postRendering(event, data);\
}).bind('refresh.jstree',function (event, data) {\
postRendering(event, data);\
});\
}\
renderFileTree();\
\
$('#filetree').on('select_node.jstree', function (e, data) {\
$('input[name=fileOrUrl]').val(data.node.id);\
});\
\
$('#refreshAction').on(\"click\", function() {\
refreshFileList();\
$(\"#refreshAction i\").addClass(\"fa-spin\");\
});\
\
$('#playMode').on(\"change\", function () {\
if (this.value == 8){\
$('#filebrowser').slideUp();\
} else {\
$('#filebrowser').slideDown();\
}\
});\
\ \
var socket = new WebSocket(\"ws://%IPv4%/ws\");\ var socket = new WebSocket(\"ws://%IPv4%/ws\");\
\ \
@ -232,7 +344,7 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
var myJSON = JSON.stringify(myObj);\ var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\ socket.send(myJSON);\
tm = setTimeout(function () {\ tm = setTimeout(function () {\
alert(\"Die Verbindung zum Tonuino ist unterbrochen!\\nBitte Seite neu laden.\");\
toastr.warning('Die Verbindung zum Tonuino ist unterbrochen! Bitte Seite neu laden.');\
}, 5000);\ }, 5000);\
}\ }\
\ \
@ -246,6 +358,7 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
\ \
socket.onclose = function(e) {\ socket.onclose = function(e) {\
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);\ console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);\
//toastr.error('Die Websocket Verbindung wurde unterbrochen.');\
setTimeout(function() {\ setTimeout(function() {\
connect();\ connect();\
}, 5000);\ }, 5000);\
@ -253,6 +366,7 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
\ \
socket.onerror = function(err) {\ socket.onerror = function(err) {\
console.error('Socket encountered error: ', err.message, 'Closing socket');\ console.error('Socket encountered error: ', err.message, 'Closing socket');\
toastr.error('Es gab einen Fehler bei der Websocket Verbindung');\
socket.close();\ socket.close();\
};\ };\
\ \
@ -262,21 +376,36 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
if (socketMsg.rfidId != null) {\ if (socketMsg.rfidId != null) {\
document.getElementById('rfidIdMod').value = socketMsg.rfidId;\ document.getElementById('rfidIdMod').value = socketMsg.rfidId;\
document.getElementById('rfidIdMusic').value = socketMsg.rfidId;\ document.getElementById('rfidIdMusic').value = socketMsg.rfidId;\
$(\"#rfidIdMusic\").fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500);\
$(\"#rfidIdMod\").fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500).fadeOut(500).fadeIn(500);\
toastr.info(\"RFID Tag mit \"+ socketMsg.rfidId + \" erkannt.\" );\
\
$(\"#rfidIdMusic\").effect(\"highlight\", {color:\"#abf5af\"}, 3000);\
$(\"#rfidIdMod\").effect(\"highlight\", {color:\"#abf5af\"}, 3000);\
\ \
} if (socketMsg.status != null) {\ } if (socketMsg.status != null) {\
if (socketMsg.status == 'ok') {\ if (socketMsg.status == 'ok') {\
$(\"#\" + lastIdclicked).find('.messages').html(okBox);\
toastr.success(\"Aktion erfolgreich ausgeführt.\" );\
} else {\ } else {\
$(\"#\" + lastIdclicked).find('.messages').html(errorBox);\
toastr.error(\"Es ist ein Fehler aufgetreten.\" );\
}\ }\
} if (socketMsg.pong != null) {\ } if (socketMsg.pong != null) {\
if (socketMsg.pong == 'pong') {\ if (socketMsg.pong == 'pong') {\
pong();\ pong();\
}\ }\
} if (\"refreshFileList\" in socketMsg){\
toastr.info(\"Die Datei Liste wurde neu erzeugt!\");\
$(\"#refreshAction i\").removeClass(\"fa-spin\");\
$('#filetree').jstree(true).refresh();\
}\ }\
};\ };\
\
function refreshFileList(clickedId){\
lastIdclicked = clickedId;\
var myObj = {\
\"refreshFileList\": true\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
};\
\ \
function genSettings(clickedId) {\ function genSettings(clickedId) {\
lastIdclicked = clickedId;\ lastIdclicked = clickedId;\

Loading…
Cancel
Save