diff --git a/.gitignore b/.gitignore index 2b16c31..3903eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,10 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch -.DS_Store \ No newline at end of file +.DS_Store +.idea/ +CMakeLists.txt +CMakeListsPrivate.txt +cmake-build-az-delivery-devkit-v4/ +cmake-build-debug/ +venv/ \ No newline at end of file diff --git a/html/website.html b/html/website.html index fb10745..793d9d8 100644 --- a/html/website.html +++ b/html/website.html @@ -1,287 +1,538 @@ - + ESPuino-Konfiguration + + + + - - - -
-
-

WLAN-Konfiguration

-
+
+ +
+
+

WLAN-Konfiguration

+
- - -
- Bitte SSID des WLANs eintragen. -
- - - - + + +
+ Bitte SSID des WLANs eintragen. +
+ + + +
- -
-
-
-

RFID-Zuweisungen

-
-
- - - - - - -
- - -
-
-
-
-

RFID-Modifkationen

-
-
- - -
- Bitte eine 12-stellige Zahl eingeben. + +
+
+
+

RFID-Zuweisungen

+
+
+ + + + +
+
+
+
+ Dateien suchen +
+
+
+
+


Der Prozess kann mehrere Minuten dauern...
+
+
- - -
- - - -
-
-
-

MQTT-Konfiguration

-
-
- -
-
- - - - - - + + +
+ + +
+
+
+
+

RFID-Modifkationen

+
+
+ + +
+ Bitte eine 12-stellige Zahl eingeben.
- - - -
-
-
-

FTP-Konfiguration

-
-
+ + +
+ + +
+
+
+
+

MQTT-Konfiguration

+
+
+ + +
+
+ + + + + + +
+ + +
+
+
+
+

FTP-Konfiguration

+
+
- + - -
- - -
-
-
-
-

Allgemeine Konfiguration

-
-
- - - - - - -
-
- - - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
-
-
-

NVS-Importer

-
-
+ +
+ + +
+
+
+
+

Allgemeine Konfiguration

+
+
+ + + + + + +
+
+ + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+
+

NVS-Importer

+
+
-
- -
-
-
- - + } + }; + var myJSON = JSON.stringify(myObj); + socket.send(myJSON); + } + + $(document).ready(function () { + connect(); + renderFileTree(); + + $(function () { + $('[data-toggle="tooltip"]').tooltip(); + }); + }); + + diff --git a/html/websiteMgmt.h b/html/websiteMgmt.h deleted file mode 100644 index f5bc9e7..0000000 --- a/html/websiteMgmt.h +++ /dev/null @@ -1,263 +0,0 @@ -static const char mgtWebsite[] PROGMEM = "\ -\ - \ - ESPuino-Konfiguration\ - \ - \ - \ - \ - \ - \ - \ - \ - \ -
\ -
\ -

WLAN-Konfiguration

\ -
\ -
\ - \ - \ -
\ - Bitte SSID des WLANs eintragen.\ -
\ - \ - \ -
\ - \ - \ -
\ -
\ -
\ -

RFID-Zuweisungen

\ -
\ -
\ - \ - \ - \ - \ - \ - \ -
\ - \ - \ -
\ -
\ -
\ -

RFID-Modifkationen

\ -
\ -
\ - \ - \ -
\ - Bitte eine 12-stellige Zahl eingeben.\ -
\ - \ - \ -
\ - \ - \ -
\ -
\ -
\ -

MQTT-Konfiguration

\ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ - Bitte eine gültige IPv4-Adresse eingeben, z.B. 192.168.2.89.\ -
\ -
\ - \ - \ -
\ -
\ -
\ -

FTP-Konfiguration

\ -
\ -
\ - \ - \ - \ - \ -
\ - \ - \ -
\ -
\ -
\ -

Allgemeine Konfiguration

\ -
\ -
\ - \ - \ - \ - \ -
\ -
\ - \ - \ - \ - \ -
\ -
\ - \ - \ -
\ - \ - \ -
\ - \ -
\ - \ -\ -"; \ No newline at end of file diff --git a/processHtml.py b/processHtml.py index 1e9dd5f..471c6c3 100644 --- a/processHtml.py +++ b/processHtml.py @@ -1,16 +1,20 @@ #!/usr/bin/python +import re content = '' content2 = '' contentEN = '' content2EN = '' +# TODO: Add a JS Minifier python lib with open('html/website.html', 'r') as r: - data = r.read().replace('\n', '\\\n') + data = r.read() + data = data.replace('\n', '\\\n') data = data.replace('\"', '\\"') data = data.replace('\\d', '\\\d') data = data.replace('\\.', '\\\.') data = data.replace('\\^', '\\\\^') + data = data.replace('%;', '%%;') content += data with open('src/websiteMgmt.h', 'w') as w: diff --git a/src/main.cpp b/src/main.cpp index e01a2a7..a771481 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -67,12 +67,16 @@ #define LOGLEVEL_DEBUG 4 // almost everything // 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 uint8_t serialLoglength = 200; 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) #define SPISD_CS 15 #ifndef SINGLE_SPI_ENABLE @@ -131,8 +135,8 @@ float voltageIndicatorHigh = 4.2; // Upper range for Neopixel- #ifdef MEASURE_BATTERY_VOLTAGE #define VOLTAGE_READ_PIN 33 // Pin to monitor battery-voltage. Change to 35 if you're using Lolin D32 or Lolin D32 pro - uint16_t r1 = 391; // First resistor of voltage-divider (kOhms) (measure exact value with multimeter!) - uint8_t r2 = 128; // Second resistor of voltage-divider (kOhms) (measure exact value with multimeter!) + uint16_t r1 = 389; // First resistor of voltage-divider (kOhms) (measure exact value with multimeter!) + uint8_t r2 = 129; // Second resistor of voltage-divider (kOhms) (measure exact value with multimeter!) // Internal values float refVoltage = 3.3; // Operation-voltage of ESP32; don't change! @@ -523,6 +527,209 @@ void IRAM_ATTR onTimer() { } #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(); +} + + +bool fileExists(fs::FS &fs, const char *file) { + return fs.exists(file); +} + +/** + * 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(F(( " {\n \"id\" : \""))); + file.print(filename); + file.print(F("\",\n \"parent\" : \"")); + file.print(parent); + file.print(F("\",\n \"type\": \"")); + file.print(type); + file.print(F("\",\n \"text\" : \"")); + file.print(filename); + file.print(F("\"\n }")); + // i/o is timing critical keep all stuff running + esp_task_wdt_reset(); + yield(); + 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 + */ +char fileNameBuf[255]; + +void parseSDFileList(fs::FS &fs, const char * dirname, const char * parent, uint8_t levels) { + esp_task_wdt_reset(); + + yield(); + 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()){ + + esp_task_wdt_reset(); + if (pathValid(fileNameBuf)){ + sendWebsocketData(0, 31); + 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" ); + } + } + vTaskDelay(portTICK_PERIOD_MS*50); + file = root.openNextFile(); + // i/o is timing critical keep all stuff running + esp_task_wdt_reset(); + } + +} + +/** + * Public function for creating file index json on SD-Card. + * It notifies the user client via websockets when the indexing + * is done. + */ +void createJSONFileList() { + createFile(SD, DIRECTORY_INDEX_FILE, "[\n"); + parseSDFileList(SD, "/", NULL, FS_DEPTH); + appendToFile(SD, DIRECTORY_INDEX_FILE, "]"); + isFirstJSONtNode = true; + sendWebsocketData(0,30); +} + + +void fileHandlingTask(void *arguments) { + createJSONFileList(); + esp_task_wdt_reset(); + vTaskDelete( NULL ); +} + // Measures voltage of a battery as per interval or after bootup (after allowing a few seconds to settle down) #ifdef MEASURE_BATTERY_VOLTAGE float measureBatteryVoltage(void) { @@ -2792,6 +2999,8 @@ void accessPointStart(const char *SSID, IPAddress ip, IPAddress netmask) { ESP.restart(); }); + // allow cors for local debug + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); wServer.begin(); loggerNl((char *) FPSTR(httpReady), LOGLEVEL_NOTICE); accessPointStarted = true; @@ -3093,6 +3302,19 @@ bool processJsonRequest(char *_serialJson) { } } else if (doc.containsKey("ping")) { sendWebsocketData(0, 20); + return false; + } else if (doc.containsKey("refreshFileList")) { + + //TODO: we need a semaphore or mutex here to prevent + // a call when the task is still running + xTaskCreate( + fileHandlingTask, /* Task function. */ + "TaskTwo", /* String with name of task. */ + 10000, /* Stack size in bytes. */ + NULL, /* Parameter passed as input of the task */ + 1, /* Priority of the task. */ + NULL); /* Task handle. */ + } return true; @@ -3113,8 +3335,13 @@ void sendWebsocketData(uint32_t client, uint8_t code) { object["rfidId"] = currentRfidTagId; } else if (code == 20) { object["pong"] = "pong"; + } else if (code == 30){ + object["refreshFileList"] = "ready"; + }else if (code == 31){ + object["indexingState"] = fileNameBuf; } - char jBuf[50]; + + char jBuf[255]; serializeJson(doc, jBuf, sizeof(jBuf) / sizeof(jBuf[0])); if (client == 0) { @@ -3147,14 +3374,10 @@ void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsE 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 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)) { - returnCode = 1; - } else { - returnCode = 0; + sendWebsocketData(client->id(), 1); } - sendWebsocketData(client->id(), 1); if (info->opcode == WS_TEXT) { data[len] = 0; @@ -3251,7 +3474,14 @@ void webserverStart(void) { ESP.restart(); }); + wServer.on("/files", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(SD, "/files.json", "application/json"); + }); + wServer.onNotFound(notFound); + + // allow cors for local debug + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); wServer.begin(); webserverStarted = true; } @@ -3721,6 +3951,13 @@ void setup() { lastTimeActiveTimestamp = millis(); // initial set after boot + /** + * Create empty Index json file when no file exists. + */ + if(!fileExists(SD,DIRECTORY_INDEX_FILE)){ + createFile(SD,DIRECTORY_INDEX_FILE,"[]"); + ESP.restart(); + } bootComplete = true; Serial.print(F("Free heap: ")); @@ -3762,6 +3999,7 @@ void loop() { #ifdef PLAY_LAST_RFID_AFTER_REBOOT recoverLastRfidPlayed(); #endif + ws.cleanupClients(); } diff --git a/src/websiteMgmt.h b/src/websiteMgmt.h index b89d00b..fe6ffef 100644 --- a/src/websiteMgmt.h +++ b/src/websiteMgmt.h @@ -1,287 +1,538 @@ static const char mgtWebsite[] PROGMEM = "\ \ - \ +\ ESPuino-Konfiguration\ \ \ \ + \ + \ + \ \ + \ \ \ - \ - \ - \ -
\ -
\ -

WLAN-Konfiguration

\ -
\ +
\ +\ +
\ +
\ +

WLAN-Konfiguration

\ + \
\ - \ - \ -
\ - Bitte SSID des WLANs eintragen.\ -
\ - \ - \ - \ - \ + \ + \ +
\ + Bitte SSID des WLANs eintragen.\ +
\ + \ + \ + \ + \
\ \ \ - \ -
\ -
\ -
\ -

RFID-Zuweisungen

\ -
\ -
\ - \ - \ - \ - \ - \ - \ -
\ - \ - \ -
\ -
\ -
\ -
\ -

RFID-Modifkationen

\ -
\ -
\ - \ - \ -
\ - Bitte eine 12-stellige Zahl eingeben.\ + \ +
\ +
\ +
\ +

RFID-Zuweisungen

\ +
\ +
\ + \ + \ + \ + \ +
\ +
\ +
\ +
\ + Dateien suchen\ +
\ +
\ +
\ +
\ +


Der Prozess kann mehrere Minuten dauern...
\ +
\ +
\
\ - \ - \ -
\ - \ - \ - \ -
\ -
\ -
\ -

MQTT-Konfiguration

\ -
\ -
\ - \ - \
\ -
\ - \ - \ - \ - \ - \ - \ + \ + \ +
\ + \ + \ +
\ +
\ +
\ +
\ +

RFID-Modifkationen

\ +
\ +
\ + \ + \ +
\ + Bitte eine 12-stellige Zahl eingeben.\
\ - \ - \ - \ -
\ -
\ -
\ -

FTP-Konfiguration

\ -
\ -
\ + \ + \ +
\ + \ + \ +
\ +
\ +
\ +
\ +

MQTT-Konfiguration

\ +
\ +
\ + \ + \ +
\ +
\ + \ + \ + \ + \ + \ + \ +
\ + \ + \ +
\ +
\ +
\ +
\ +

FTP-Konfiguration

\ +
\ +
\ \ - \ + \ \ - \ -
\ - \ - \ -
\ -
\ -
\ -
\ -

Allgemeine Konfiguration

\ -
\ -
\ - \ - \ - \ - \ - \ - \ -
\ -
\ - \ - \ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ -
\ - \ - \ -
\ - \ - \ -
\ -
\ -
\ -
\ -

NVS-Importer

\ -
\ -
\ + \ +
\ + \ + \ +
\ +
\ +
\ +
\ +

Allgemeine Konfiguration

\ +
\ +
\ + \ + \ + \ + \ + \ + \ +
\ +
\ + \ + \ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ +
\ + \ + \ +
\ + \ + \ +
\ +
\ +
\ +
\ +

NVS-Importer

\ +
\ +
\ \ \ -
\ - \ -
\ -
\ -
\ - \ - \ + }\ + };\ + var myJSON = JSON.stringify(myObj);\ + socket.send(myJSON);\ + }\ +\ + $(document).ready(function () {\ + connect();\ + renderFileTree();\ +\ + $(function () {\ + $('[data-toggle=\"tooltip\"]').tooltip();\ + });\ + });\ +\ +\ \ "; \ No newline at end of file