You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
863 lines
34 KiB
863 lines
34 KiB
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
#include <nvsDump.h>
|
|
#include <esp_task_wdt.h>
|
|
#include "freertos/ringbuf.h"
|
|
#include "ESPAsyncWebServer.h"
|
|
#include "ArduinoJson.h"
|
|
#include "settings.h"
|
|
#include "AudioPlayer.h"
|
|
#include "Battery.h"
|
|
#include "Cmd.h"
|
|
#include "Common.h"
|
|
#include "Ftp.h"
|
|
#include "Led.h"
|
|
#include "Log.h"
|
|
#include "MemX.h"
|
|
#include "Mqtt.h"
|
|
#include "Rfid.h"
|
|
#include "SdCard.h"
|
|
#include "System.h"
|
|
#include "Web.h"
|
|
#include "Wlan.h"
|
|
|
|
#if (LANGUAGE == 1)
|
|
#include "HTMLaccesspoint_DE.h"
|
|
#include "HTMLmanagement_DE.h"
|
|
#endif
|
|
|
|
#if (LANGUAGE == 2)
|
|
#include "HTMLaccesspoint_EN.h"
|
|
#include "HTMLmanagement_EN.h"
|
|
#endif
|
|
|
|
|
|
typedef struct {
|
|
char nvsKey[13];
|
|
char nvsEntry[275];
|
|
} nvs_t;
|
|
|
|
const char mqttTab[] PROGMEM = "<a class=\"nav-item nav-link\" id=\"nav-mqtt-tab\" data-toggle=\"tab\" href=\"#nav-mqtt\" role=\"tab\" aria-controls=\"nav-mqtt\" aria-selected=\"false\"><i class=\"fas fa-network-wired\"></i> MQTT</a>";
|
|
const char ftpTab[] PROGMEM = "<a class=\"nav-item nav-link\" id=\"nav-ftp-tab\" data-toggle=\"tab\" href=\"#nav-ftp\" role=\"tab\" aria-controls=\"nav-ftp\" aria-selected=\"false\"><i class=\"fas fa-folder\"></i> FTP</a>";
|
|
|
|
AsyncWebServer wServer(80);
|
|
AsyncWebSocket ws("/ws");
|
|
AsyncEventSource events("/events");
|
|
|
|
static bool webserverStarted = false;
|
|
|
|
static RingbufHandle_t explorerFileUploadRingBuffer;
|
|
static QueueHandle_t explorerFileUploadStatusQueue;
|
|
static TaskHandle_t fileStorageTaskHandle;
|
|
|
|
static void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
|
static void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
|
static void explorerHandleFileStorageTask(void *parameter);
|
|
static void explorerHandleListRequest(AsyncWebServerRequest *request);
|
|
static void explorerHandleDeleteRequest(AsyncWebServerRequest *request);
|
|
static void explorerHandleCreateRequest(AsyncWebServerRequest *request);
|
|
static void explorerHandleRenameRequest(AsyncWebServerRequest *request);
|
|
static void explorerHandleAudioRequest(AsyncWebServerRequest *request);
|
|
|
|
static bool Web_DumpNvsToSd(const char *_namespace, const char *_destFile);
|
|
|
|
static void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
|
|
static String templateProcessor(const String &templ);
|
|
static void webserverStart(void);
|
|
|
|
void Web_Init(void) {
|
|
wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
request->send_P(200, "text/html", accesspoint_HTML);
|
|
});
|
|
|
|
wServer.on("/init", HTTP_POST, [](AsyncWebServerRequest *request) {
|
|
if (request->hasParam("ssid", true) && request->hasParam("pwd", true) && request->hasParam("hostname", true)) {
|
|
Serial.println(request->getParam("ssid", true)->value());
|
|
Serial.println(request->getParam("pwd", true)->value());
|
|
Serial.println(request->getParam("hostname", true)->value());
|
|
gPrefsSettings.putString("SSID", request->getParam("ssid", true)->value());
|
|
gPrefsSettings.putString("Password", request->getParam("pwd", true)->value());
|
|
gPrefsSettings.putString("Hostname", request->getParam("hostname", true)->value());
|
|
}
|
|
request->send_P(200, "text/html", accesspoint_HTML);
|
|
});
|
|
|
|
wServer.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
#if (LANGUAGE == 1)
|
|
request->send(200, "text/html", "ESPuino wird neu gestartet...");
|
|
#else
|
|
request->send(200, "text/html", "ESPuino is being restarted...");
|
|
#endif
|
|
Serial.flush();
|
|
ESP.restart();
|
|
});
|
|
|
|
wServer.on("/shutdown", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
#if (LANGUAGE == 1)
|
|
request->send(200, "text/html", "ESPuino wird ausgeschaltet...");
|
|
#else
|
|
request->send(200, "text/html", "ESPuino is being shutdown...");
|
|
#endif
|
|
System_RequestSleep();
|
|
});
|
|
|
|
// allow cors for local debug
|
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
|
wServer.begin();
|
|
Log_Println((char *) FPSTR(httpReady), LOGLEVEL_NOTICE);
|
|
}
|
|
|
|
void Web_Cyclic(void) {
|
|
webserverStart();
|
|
ws.cleanupClients();
|
|
}
|
|
|
|
void notFound(AsyncWebServerRequest *request) {
|
|
request->send(404, "text/plain", "Not found");
|
|
}
|
|
|
|
void webserverStart(void) {
|
|
if (Wlan_IsConnected() && !webserverStarted) {
|
|
// attach AsyncWebSocket for Mgmt-Interface
|
|
ws.onEvent(onWebsocketEvent);
|
|
wServer.addHandler(&ws);
|
|
|
|
// attach AsyncEventSource
|
|
wServer.addHandler(&events);
|
|
|
|
// Default
|
|
wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
request->send_P(200, "text/html", management_HTML, templateProcessor);
|
|
});
|
|
|
|
// Log
|
|
wServer.on("/log", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
request->send(200, "text/plain", Log_GetRingBuffer());
|
|
});
|
|
|
|
// NVS-backup-upload
|
|
wServer.on(
|
|
"/upload", HTTP_POST, [](AsyncWebServerRequest *request) {
|
|
request->send_P(200, "text/html", backupRecoveryWebsite);
|
|
},
|
|
handleUpload);
|
|
|
|
// ESP-restart
|
|
wServer.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
request->send_P(200, "text/html", restartWebsite);
|
|
Serial.flush();
|
|
ESP.restart();
|
|
});
|
|
|
|
// ESP-shutdown
|
|
wServer.on("/shutdown", HTTP_GET, [](AsyncWebServerRequest *request) {
|
|
request->send_P(200, "text/html", shutdownWebsite);
|
|
System_RequestSleep();
|
|
});
|
|
|
|
// Fileexplorer (realtime)
|
|
wServer.on("/explorer", HTTP_GET, explorerHandleListRequest);
|
|
|
|
wServer.on(
|
|
"/explorer", HTTP_POST, [](AsyncWebServerRequest *request) {
|
|
request->send(200);
|
|
},
|
|
explorerHandleFileUpload);
|
|
|
|
wServer.on("/explorer", HTTP_DELETE, explorerHandleDeleteRequest);
|
|
|
|
wServer.on("/explorer", HTTP_PUT, explorerHandleCreateRequest);
|
|
|
|
wServer.on("/explorer", HTTP_PATCH, explorerHandleRenameRequest);
|
|
|
|
wServer.on("/exploreraudio", HTTP_POST, explorerHandleAudioRequest);
|
|
|
|
wServer.onNotFound(notFound);
|
|
|
|
// allow cors for local debug
|
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
|
wServer.begin();
|
|
webserverStarted = true;
|
|
}
|
|
}
|
|
|
|
// Used for substitution of some variables/templates of html-files. Is called by webserver's template-engine
|
|
String templateProcessor(const String &templ) {
|
|
if (templ == "FTP_USER") {
|
|
return gPrefsSettings.getString("ftpuser", "-1");
|
|
} else if (templ == "FTP_PWD") {
|
|
return gPrefsSettings.getString("ftppassword", "-1");
|
|
} else if (templ == "FTP_USER_LENGTH") {
|
|
return String(ftpUserLength - 1);
|
|
} else if (templ == "FTP_PWD_LENGTH") {
|
|
return String(ftpPasswordLength - 1);
|
|
} else if (templ == "SHOW_FTP_TAB") { // Only show FTP-tab if FTP-support was compiled
|
|
#ifdef FTP_ENABLE
|
|
return (String) FPSTR(ftpTab);
|
|
#else
|
|
return String();
|
|
#endif
|
|
} else if (templ == "INIT_LED_BRIGHTNESS") {
|
|
return String(gPrefsSettings.getUChar("iLedBrightness", 0));
|
|
} else if (templ == "NIGHT_LED_BRIGHTNESS") {
|
|
return String(gPrefsSettings.getUChar("nLedBrightness", 0));
|
|
} else if (templ == "MAX_INACTIVITY") {
|
|
return String(gPrefsSettings.getUInt("mInactiviyT", 0));
|
|
} else if (templ == "INIT_VOLUME") {
|
|
return String(gPrefsSettings.getUInt("initVolume", 0));
|
|
} else if (templ == "CURRENT_VOLUME") {
|
|
return String(AudioPlayer_GetCurrentVolume());
|
|
} else if (templ == "MAX_VOLUME_SPEAKER") {
|
|
return String(gPrefsSettings.getUInt("maxVolumeSp", 0));
|
|
} else if (templ == "MAX_VOLUME_HEADPHONE") {
|
|
return String(gPrefsSettings.getUInt("maxVolumeHp", 0));
|
|
} else if (templ == "WARNING_LOW_VOLTAGE") {
|
|
return String(gPrefsSettings.getFloat("wLowVoltage", warningLowVoltage));
|
|
} else if (templ == "VOLTAGE_INDICATOR_LOW") {
|
|
return String(gPrefsSettings.getFloat("vIndicatorLow", voltageIndicatorLow));
|
|
} else if (templ == "VOLTAGE_INDICATOR_HIGH") {
|
|
return String(gPrefsSettings.getFloat("vIndicatorHigh", voltageIndicatorHigh));
|
|
} else if (templ == "VOLTAGE_CHECK_INTERVAL") {
|
|
return String(gPrefsSettings.getUInt("vCheckIntv", voltageCheckInterval));
|
|
} else if (templ == "MQTT_SERVER") {
|
|
return gPrefsSettings.getString("mqttServer", "-1");
|
|
} else if (templ == "SHOW_MQTT_TAB") { // Only show MQTT-tab if MQTT-support was compiled
|
|
#ifdef MQTT_ENABLE
|
|
return (String) FPSTR(mqttTab);
|
|
#else
|
|
return String();
|
|
#endif
|
|
} else if (templ == "MQTT_ENABLE") {
|
|
if (Mqtt_IsEnabled()) {
|
|
return String("checked=\"checked\"");
|
|
} else {
|
|
return String();
|
|
}
|
|
} else if (templ == "MQTT_USER") {
|
|
return gPrefsSettings.getString("mqttUser", "-1");
|
|
} else if (templ == "MQTT_PWD") {
|
|
return gPrefsSettings.getString("mqttPassword", "-1");
|
|
} else if (templ == "MQTT_USER_LENGTH") {
|
|
return String(mqttUserLength - 1);
|
|
} else if (templ == "MQTT_PWD_LENGTH") {
|
|
return String(mqttPasswordLength - 1);
|
|
} else if (templ == "MQTT_SERVER_LENGTH") {
|
|
return String(mqttServerLength - 1);
|
|
} else if (templ == "MQTT_PORT") {
|
|
return String(gMqttPort);
|
|
} else if (templ == "IPv4") {
|
|
IPAddress myIP = WiFi.localIP();
|
|
snprintf(Log_Buffer, Log_BufferLength, "%d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]);
|
|
return String(Log_Buffer);
|
|
} else if (templ == "RFID_TAG_ID") {
|
|
return String(gCurrentRfidTagId);
|
|
} else if (templ == "HOSTNAME") {
|
|
return gPrefsSettings.getString("Hostname", "-1");
|
|
}
|
|
|
|
return String();
|
|
}
|
|
|
|
// Takes inputs from webgui, parses JSON and saves values in NVS
|
|
// If operation was successful (NVS-write is verified) true is returned
|
|
bool processJsonRequest(char *_serialJson) {
|
|
StaticJsonDocument<1000> doc;
|
|
DeserializationError error = deserializeJson(doc, _serialJson);
|
|
JsonObject object = doc.as<JsonObject>();
|
|
|
|
if (error) {
|
|
#if (LANGUAGE == 1)
|
|
Serial.print(F("deserializeJson() fehlgeschlagen: "));
|
|
#else
|
|
Serial.print(F("deserializeJson() failed: "));
|
|
#endif
|
|
Serial.println(error.c_str());
|
|
return false;
|
|
}
|
|
|
|
if (doc.containsKey("general")) {
|
|
uint8_t iVol = doc["general"]["iVol"].as<uint8_t>();
|
|
uint8_t mVolSpeaker = doc["general"]["mVolSpeaker"].as<uint8_t>();
|
|
uint8_t mVolHeadphone = doc["general"]["mVolHeadphone"].as<uint8_t>();
|
|
uint8_t iBright = doc["general"]["iBright"].as<uint8_t>();
|
|
uint8_t nBright = doc["general"]["nBright"].as<uint8_t>();
|
|
uint8_t iTime = doc["general"]["iTime"].as<uint8_t>();
|
|
float vWarning = doc["general"]["vWarning"].as<float>();
|
|
float vIndLow = doc["general"]["vIndLow"].as<float>();
|
|
float vIndHi = doc["general"]["vIndHi"].as<float>();
|
|
uint8_t vInt = doc["general"]["vInt"].as<uint8_t>();
|
|
|
|
gPrefsSettings.putUInt("initVolume", iVol);
|
|
gPrefsSettings.putUInt("maxVolumeSp", mVolSpeaker);
|
|
gPrefsSettings.putUInt("maxVolumeHp", mVolHeadphone);
|
|
gPrefsSettings.putUChar("iLedBrightness", iBright);
|
|
gPrefsSettings.putUChar("nLedBrightness", nBright);
|
|
gPrefsSettings.putUInt("mInactiviyT", iTime);
|
|
gPrefsSettings.putFloat("wLowVoltage", vWarning);
|
|
gPrefsSettings.putFloat("vIndicatorLow", vIndLow);
|
|
gPrefsSettings.putFloat("vIndicatorHigh", vIndHi);
|
|
gPrefsSettings.putUInt("vCheckIntv", vInt);
|
|
|
|
// Check if settings were written successfully
|
|
if (gPrefsSettings.getUInt("initVolume", 0) != iVol ||
|
|
gPrefsSettings.getUInt("maxVolumeSp", 0) != mVolSpeaker ||
|
|
gPrefsSettings.getUInt("maxVolumeHp", 0) != mVolHeadphone ||
|
|
gPrefsSettings.getUChar("iLedBrightness", 0) != iBright ||
|
|
gPrefsSettings.getUChar("nLedBrightness", 0) != nBright ||
|
|
gPrefsSettings.getUInt("mInactiviyT", 0) != iTime ||
|
|
gPrefsSettings.getFloat("wLowVoltage", 999.99) != vWarning ||
|
|
gPrefsSettings.getFloat("vIndicatorLow", 999.99) != vIndLow ||
|
|
gPrefsSettings.getFloat("vIndicatorHigh", 999.99) != vIndHi ||
|
|
gPrefsSettings.getUInt("vCheckIntv", 17777) != vInt) {
|
|
return false;
|
|
}
|
|
} else if (doc.containsKey("ftp")) {
|
|
const char *_ftpUser = doc["ftp"]["ftpUser"];
|
|
const char *_ftpPwd = doc["ftp"]["ftpPwd"];
|
|
|
|
gPrefsSettings.putString("ftpuser", (String)_ftpUser);
|
|
gPrefsSettings.putString("ftppassword", (String)_ftpPwd);
|
|
|
|
if (!(String(_ftpUser).equals(gPrefsSettings.getString("ftpuser", "-1")) ||
|
|
String(_ftpPwd).equals(gPrefsSettings.getString("ftppassword", "-1")))) {
|
|
return false;
|
|
}
|
|
} else if (doc.containsKey("mqtt")) {
|
|
uint8_t _mqttEnable = doc["mqtt"]["mqttEnable"].as<uint8_t>();
|
|
const char *_mqttServer = object["mqtt"]["mqttServer"];
|
|
gPrefsSettings.putUChar("enableMQTT", _mqttEnable);
|
|
gPrefsSettings.putString("mqttServer", (String)_mqttServer);
|
|
const char *_mqttUser = doc["mqtt"]["mqttUser"];
|
|
const char *_mqttPwd = doc["mqtt"]["mqttPwd"];
|
|
uint16_t _mqttPort = doc["mqtt"]["mqttPort"].as<uint16_t>();
|
|
|
|
gPrefsSettings.putUChar("enableMQTT", _mqttEnable);
|
|
gPrefsSettings.putString("mqttServer", (String)_mqttServer);
|
|
gPrefsSettings.putString("mqttServer", (String)_mqttServer);
|
|
gPrefsSettings.putString("mqttUser", (String)_mqttUser);
|
|
gPrefsSettings.putString("mqttPassword", (String)_mqttPwd);
|
|
gPrefsSettings.putUInt("mqttPort", _mqttPort);
|
|
|
|
if ((gPrefsSettings.getUChar("enableMQTT", 99) != _mqttEnable) ||
|
|
(!String(_mqttServer).equals(gPrefsSettings.getString("mqttServer", "-1")))) {
|
|
return false;
|
|
}
|
|
} else if (doc.containsKey("rfidMod")) {
|
|
const char *_rfidIdModId = object["rfidMod"]["rfidIdMod"];
|
|
uint8_t _modId = object["rfidMod"]["modId"];
|
|
char rfidString[12];
|
|
if (_modId <= 0) {
|
|
gPrefsRfid.remove(_rfidIdModId);
|
|
} else {
|
|
snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s0%s0%s%u%s0", stringDelimiter, stringDelimiter, stringDelimiter, _modId, stringDelimiter);
|
|
gPrefsRfid.putString(_rfidIdModId, rfidString);
|
|
|
|
String s = gPrefsRfid.getString(_rfidIdModId, "-1");
|
|
if (s.compareTo(rfidString)) {
|
|
return false;
|
|
}
|
|
}
|
|
Web_DumpNvsToSd("rfidTags", (const char*) FPSTR(backupFile)); // Store backup-file every time when a new rfid-tag is programmed
|
|
} else if (doc.containsKey("rfidAssign")) {
|
|
const char *_rfidIdAssinId = object["rfidAssign"]["rfidIdMusic"];
|
|
char _fileOrUrlAscii[MAX_FILEPATH_LENTGH];
|
|
convertUtf8ToAscii(object["rfidAssign"]["fileOrUrl"], _fileOrUrlAscii);
|
|
uint8_t _playMode = object["rfidAssign"]["playMode"];
|
|
char rfidString[275];
|
|
snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s%s%s0%s%u%s0", stringDelimiter, _fileOrUrlAscii, stringDelimiter, stringDelimiter, _playMode, stringDelimiter);
|
|
gPrefsRfid.putString(_rfidIdAssinId, rfidString);
|
|
Serial.println(_rfidIdAssinId);
|
|
Serial.println(rfidString);
|
|
|
|
String s = gPrefsRfid.getString(_rfidIdAssinId, "-1");
|
|
if (s.compareTo(rfidString)) {
|
|
return false;
|
|
}
|
|
Web_DumpNvsToSd("rfidTags", (const char*) FPSTR(backupFile)); // Store backup-file every time when a new rfid-tag is programmed
|
|
} else if (doc.containsKey("wifiConfig")) {
|
|
const char *_ssid = object["wifiConfig"]["ssid"];
|
|
const char *_pwd = object["wifiConfig"]["pwd"];
|
|
const char *_hostname = object["wifiConfig"]["hostname"];
|
|
|
|
gPrefsSettings.putString("SSID", _ssid);
|
|
gPrefsSettings.putString("Password", _pwd);
|
|
gPrefsSettings.putString("Hostname", (String)_hostname);
|
|
|
|
String sSsid = gPrefsSettings.getString("SSID", "-1");
|
|
String sPwd = gPrefsSettings.getString("Password", "-1");
|
|
String sHostname = gPrefsSettings.getString("Hostname", "-1");
|
|
|
|
if (sSsid.compareTo(_ssid) || sPwd.compareTo(_pwd)) {
|
|
return false;
|
|
}
|
|
}
|
|
else if (doc.containsKey("ping")) {
|
|
Web_SendWebsocketData(0, 20);
|
|
return false;
|
|
} else if (doc.containsKey("controls")) {
|
|
if (object["controls"].containsKey("set_volume")) {
|
|
uint8_t new_vol = doc["controls"]["set_volume"].as<uint8_t>();
|
|
AudioPlayer_VolumeToQueueSender(new_vol, true);
|
|
} if (object["controls"].containsKey("action")) {
|
|
uint8_t cmd = doc["controls"]["action"].as<uint8_t>();
|
|
Cmd_Action(cmd);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Sends JSON-answers via websocket
|
|
void Web_SendWebsocketData(uint32_t client, uint8_t code) {
|
|
char *jBuf;
|
|
jBuf = (char *)x_calloc(255, sizeof(char));
|
|
|
|
const size_t CAPACITY = JSON_OBJECT_SIZE(1) + 20;
|
|
StaticJsonDocument<CAPACITY> doc;
|
|
JsonObject object = doc.to<JsonObject>();
|
|
|
|
if (code == 1) {
|
|
object["status"] = "ok";
|
|
} else if (code == 2) {
|
|
object["status"] = "error";
|
|
} else if (code == 10) {
|
|
object["rfidId"] = gCurrentRfidTagId;
|
|
} else if (code == 20) {
|
|
object["pong"] = "pong";
|
|
}
|
|
|
|
serializeJson(doc, jBuf, 255);
|
|
|
|
if (client == 0) {
|
|
ws.printfAll(jBuf);
|
|
} else {
|
|
ws.printf(client, jBuf);
|
|
}
|
|
free(jBuf);
|
|
}
|
|
|
|
// Processes websocket-requests
|
|
void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
|
|
if (type == WS_EVT_CONNECT) {
|
|
//client connected
|
|
Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
|
|
//client->printf("Hello Client %u :)", client->id());
|
|
client->ping();
|
|
} else if (type == WS_EVT_DISCONNECT) {
|
|
//client disconnected
|
|
Serial.printf("ws[%s][%u] disconnect\n", server->url(), client->id());
|
|
} else if (type == WS_EVT_ERROR) {
|
|
//error was received from the other end
|
|
Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
|
|
} else if (type == WS_EVT_PONG) {
|
|
//pong message was received (in response to a ping request maybe)
|
|
Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
|
|
} else if (type == WS_EVT_DATA) {
|
|
//data packet
|
|
AwsFrameInfo *info = (AwsFrameInfo *)arg;
|
|
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);
|
|
|
|
if (processJsonRequest((char *)data)) {
|
|
Web_SendWebsocketData(client->id(), 1);
|
|
}
|
|
|
|
if (info->opcode == WS_TEXT) {
|
|
data[len] = 0;
|
|
Serial.printf("%s\n", (char *)data);
|
|
} else {
|
|
for (size_t i = 0; i < info->len; i++) {
|
|
Serial.printf("%02x ", data[i]);
|
|
}
|
|
Serial.printf("\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handles file upload request from the explorer
|
|
// requires a GET parameter path, as directory path to the file
|
|
void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
|
|
|
System_UpdateActivityTimer();
|
|
|
|
// New File
|
|
if (!index) {
|
|
String utf8FilePath;
|
|
static char filePath[MAX_FILEPATH_LENTGH];
|
|
if (request->hasParam("path")) {
|
|
AsyncWebParameter *param = request->getParam("path");
|
|
utf8FilePath = param->value() + "/" + filename;
|
|
} else {
|
|
utf8FilePath = "/" + filename;
|
|
}
|
|
|
|
convertUtf8ToAscii(utf8FilePath, filePath);
|
|
|
|
snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *)FPSTR (writingFile), utf8FilePath.c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_INFO);
|
|
|
|
// Create Ringbuffer for upload
|
|
if (explorerFileUploadRingBuffer == NULL) {
|
|
explorerFileUploadRingBuffer = xRingbufferCreate(4096, RINGBUF_TYPE_BYTEBUF);
|
|
}
|
|
|
|
// Create Queue for receiving a signal from the store task as synchronisation
|
|
if (explorerFileUploadStatusQueue == NULL) {
|
|
explorerFileUploadStatusQueue = xQueueCreate(1, sizeof(uint8_t));
|
|
}
|
|
|
|
// Create Task for handling the storage of the data
|
|
xTaskCreate(
|
|
explorerHandleFileStorageTask, /* Function to implement the task */
|
|
"fileStorageTask", /* Name of the task */
|
|
4000, /* Stack size in words */
|
|
filePath, /* Task input parameter */
|
|
2 | portPRIVILEGE_BIT, /* Priority of the task */
|
|
&fileStorageTaskHandle /* Task handle. */
|
|
);
|
|
}
|
|
|
|
if (len) {
|
|
// stream the incoming chunk to the ringbuffer
|
|
xRingbufferSend(explorerFileUploadRingBuffer, data, len, portTICK_PERIOD_MS * 1000);
|
|
}
|
|
|
|
if (final) {
|
|
// notify storage task that last data was stored on the ring buffer
|
|
xTaskNotify(fileStorageTaskHandle, 1u, eNoAction);
|
|
// watit until the storage task is sending the signal to finish
|
|
uint8_t signal;
|
|
xQueueReceive(explorerFileUploadStatusQueue, &signal, portMAX_DELAY);
|
|
|
|
// delete task
|
|
vTaskDelete(fileStorageTaskHandle);
|
|
}
|
|
}
|
|
|
|
void explorerHandleFileStorageTask(void *parameter) {
|
|
|
|
File uploadFile;
|
|
size_t item_size;
|
|
uint8_t *item;
|
|
uint8_t value = 0;
|
|
|
|
BaseType_t uploadFileNotification;
|
|
uint32_t uploadFileNotificationValue;
|
|
|
|
uploadFile = gFSystem.open((char *)parameter, "w");
|
|
|
|
for (;;) {
|
|
esp_task_wdt_reset();
|
|
|
|
item = (uint8_t *)xRingbufferReceive(explorerFileUploadRingBuffer, &item_size, portTICK_PERIOD_MS * 100);
|
|
if (item != NULL) {
|
|
uploadFile.write(item, item_size);
|
|
vRingbufferReturnItem(explorerFileUploadRingBuffer, (void *)item);
|
|
} else {
|
|
// No data in the buffer, check if all data arrived for the file
|
|
uploadFileNotification = xTaskNotifyWait(0, 0, &uploadFileNotificationValue, 0);
|
|
if (uploadFileNotification == pdPASS) {
|
|
uploadFile.close();
|
|
// done exit loop to terminate
|
|
break;
|
|
}
|
|
vTaskDelay(portTICK_PERIOD_MS * 100);
|
|
}
|
|
}
|
|
// send signal to upload function to terminate
|
|
xQueueSend(explorerFileUploadStatusQueue, &value, 0);
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
// Sends a list of the content of a directory as JSON file
|
|
// requires a GET parameter path for the directory
|
|
void explorerHandleListRequest(AsyncWebServerRequest *request) {
|
|
DynamicJsonDocument jsonBuffer(16384);
|
|
//StaticJsonDocument<4096> jsonBuffer;
|
|
String serializedJsonString;
|
|
AsyncWebParameter *param;
|
|
char filePath[MAX_FILEPATH_LENTGH];
|
|
JsonArray obj = jsonBuffer.createNestedArray();
|
|
File root;
|
|
if (request->hasParam("path")) {
|
|
param = request->getParam("path");
|
|
convertUtf8ToAscii(param->value(), filePath);
|
|
root = gFSystem.open(filePath);
|
|
} else {
|
|
root = gFSystem.open("/");
|
|
}
|
|
|
|
if (!root) {
|
|
snprintf(Log_Buffer, Log_BufferLength, (char *) FPSTR(failedToOpenDirectory));
|
|
Log_Println(Log_Buffer, LOGLEVEL_DEBUG);
|
|
return;
|
|
}
|
|
|
|
if (!root.isDirectory()) {
|
|
snprintf(Log_Buffer, Log_BufferLength, (char *) FPSTR(notADirectory));
|
|
Log_Println(Log_Buffer, LOGLEVEL_DEBUG);
|
|
return;
|
|
}
|
|
|
|
File file = root.openNextFile();
|
|
|
|
while (file) {
|
|
// ignore hidden folders, e.g. MacOS spotlight files
|
|
if (!startsWith(file.name(), (char *)"/.")) {
|
|
JsonObject entry = obj.createNestedObject();
|
|
convertAsciiToUtf8(file.name(), filePath);
|
|
std::string path = filePath;
|
|
std::string fileName = path.substr(path.find_last_of("/") + 1);
|
|
|
|
entry["name"] = fileName;
|
|
entry["dir"].set(file.isDirectory());
|
|
}
|
|
file = root.openNextFile();
|
|
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
serializeJson(obj, serializedJsonString);
|
|
request->send(200, "application/json; charset=utf-8", serializedJsonString);
|
|
}
|
|
|
|
bool explorerDeleteDirectory(File dir) {
|
|
|
|
File file = dir.openNextFile();
|
|
while (file) {
|
|
|
|
if (file.isDirectory()) {
|
|
explorerDeleteDirectory(file);
|
|
} else {
|
|
gFSystem.remove(file.name());
|
|
}
|
|
|
|
file = dir.openNextFile();
|
|
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
return gFSystem.rmdir(dir.name());
|
|
}
|
|
|
|
// Handles delete request of a file or directory
|
|
// requires a GET parameter path to the file or directory
|
|
void explorerHandleDeleteRequest(AsyncWebServerRequest *request) {
|
|
File file;
|
|
AsyncWebParameter *param;
|
|
char filePath[MAX_FILEPATH_LENTGH];
|
|
if (request->hasParam("path")) {
|
|
param = request->getParam("path");
|
|
convertUtf8ToAscii(param->value(), filePath);
|
|
if (gFSystem.exists(filePath)) {
|
|
file = gFSystem.open(filePath);
|
|
if (file.isDirectory()) {
|
|
if (explorerDeleteDirectory(file)) {
|
|
snprintf(Log_Buffer, Log_BufferLength, "DELETE: %s deleted", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_INFO);
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "DELETE: Cannot delete %s", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
}
|
|
} else {
|
|
if (gFSystem.remove(filePath)) {
|
|
snprintf(Log_Buffer, Log_BufferLength, "DELETE: %s deleted", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_INFO);
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "DELETE: Cannot delete %s", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
}
|
|
}
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "DELETE: Path %s does not exist", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
}
|
|
} else {
|
|
Log_Println("DELETE: No path variable set", LOGLEVEL_ERROR);
|
|
}
|
|
request->send(200);
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
// Handles create request of a directory
|
|
// requires a GET parameter path to the new directory
|
|
void explorerHandleCreateRequest(AsyncWebServerRequest *request) {
|
|
AsyncWebParameter *param;
|
|
char filePath[MAX_FILEPATH_LENTGH];
|
|
if (request->hasParam("path")) {
|
|
param = request->getParam("path");
|
|
convertUtf8ToAscii(param->value(), filePath);
|
|
if (gFSystem.mkdir(filePath)) {
|
|
snprintf(Log_Buffer, Log_BufferLength, "CREATE: %s created", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_INFO);
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "CREATE: Cannot create %s", param->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
}
|
|
} else {
|
|
Log_Println("CREATE: No path variable set", LOGLEVEL_ERROR);
|
|
}
|
|
request->send(200);
|
|
}
|
|
|
|
// Handles rename request of a file or directory
|
|
// requires a GET parameter srcpath to the old file or directory name
|
|
// requires a GET parameter dstpath to the new file or directory name
|
|
void explorerHandleRenameRequest(AsyncWebServerRequest *request) {
|
|
AsyncWebParameter *srcPath;
|
|
AsyncWebParameter *dstPath;
|
|
char srcFullFilePath[MAX_FILEPATH_LENTGH];
|
|
char dstFullFilePath[MAX_FILEPATH_LENTGH];
|
|
if (request->hasParam("srcpath") && request->hasParam("dstpath")) {
|
|
srcPath = request->getParam("srcpath");
|
|
dstPath = request->getParam("dstpath");
|
|
convertUtf8ToAscii(srcPath->value(), srcFullFilePath);
|
|
convertUtf8ToAscii(dstPath->value(), dstFullFilePath);
|
|
if (gFSystem.exists(srcFullFilePath)) {
|
|
if (gFSystem.rename(srcFullFilePath, dstFullFilePath)) {
|
|
snprintf(Log_Buffer, Log_BufferLength, "RENAME: %s renamed to %s", srcPath->value().c_str(), dstPath->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_INFO);
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "RENAME: Cannot rename %s", srcPath->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
}
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "RENAME: Path %s does not exist", srcPath->value().c_str());
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
}
|
|
} else {
|
|
Log_Println("RENAME: No path variable set", LOGLEVEL_ERROR);
|
|
}
|
|
|
|
request->send(200);
|
|
}
|
|
|
|
// Handles audio play requests
|
|
// requires a GET parameter path to the audio file or directory
|
|
// requires a GET parameter playmode
|
|
void explorerHandleAudioRequest(AsyncWebServerRequest *request) {
|
|
AsyncWebParameter *param;
|
|
String playModeString;
|
|
uint32_t playMode;
|
|
char filePath[MAX_FILEPATH_LENTGH];
|
|
if (request->hasParam("path") && request->hasParam("playmode")) {
|
|
param = request->getParam("path");
|
|
convertUtf8ToAscii(param->value(), filePath);
|
|
param = request->getParam("playmode");
|
|
playModeString = param->value();
|
|
|
|
playMode = atoi(playModeString.c_str());
|
|
AudioPlayer_TrackQueueDispatcher(filePath, 0, playMode, 0);
|
|
} else {
|
|
Log_Println("AUDIO: No path variable set", LOGLEVEL_ERROR);
|
|
}
|
|
|
|
request->send(200);
|
|
}
|
|
|
|
// Handles uploaded backup-file and writes valid entries into NVS
|
|
void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
|
Led_SetPause(true); // Workaround to prevent exceptions due to Neopixel-signalisation while NVS-write
|
|
char ebuf[290];
|
|
uint16_t j = 0;
|
|
char *token;
|
|
uint8_t count = 0;
|
|
nvs_t nvsEntry[1];
|
|
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (data[i] != '\n') {
|
|
ebuf[j++] = data[i];
|
|
} else {
|
|
ebuf[j] = '\0';
|
|
j = 0;
|
|
token = strtok(ebuf, stringOuterDelimiter);
|
|
while (token != NULL) {
|
|
if (!count) {
|
|
count++;
|
|
memcpy(nvsEntry[0].nvsKey, token, strlen(token));
|
|
nvsEntry[0].nvsKey[strlen(token)] = '\0';
|
|
} else if (count == 1) {
|
|
count = 0;
|
|
memcpy(nvsEntry[0].nvsEntry, token, strlen(token));
|
|
nvsEntry[0].nvsEntry[strlen(token)] = '\0';
|
|
}
|
|
token = strtok(NULL, stringOuterDelimiter);
|
|
}
|
|
if (isNumber(nvsEntry[0].nvsKey) && nvsEntry[0].nvsEntry[0] == '#') {
|
|
snprintf(Log_Buffer, Log_BufferLength, "%s: %s => %s", (char *) FPSTR(writeEntryToNvs), nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry);
|
|
Log_Println(Log_Buffer, LOGLEVEL_NOTICE);
|
|
gPrefsRfid.putString(nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry);
|
|
}
|
|
}
|
|
}
|
|
Led_SetPause(false);
|
|
}
|
|
|
|
// Dumps all RFID-entries from NVS into a file on SD-card
|
|
bool Web_DumpNvsToSd(const char *_namespace, const char *_destFile) {
|
|
Led_SetPause(true); // Workaround to prevent exceptions due to Neopixel-signalisation while NVS-write
|
|
esp_partition_iterator_t pi; // Iterator for find
|
|
const esp_partition_t *nvs; // Pointer to partition struct
|
|
esp_err_t result = ESP_OK;
|
|
const char *partname = "nvs";
|
|
uint8_t pagenr = 0; // Page number in NVS
|
|
uint8_t i; // Index in Entry 0..125
|
|
uint8_t bm; // Bitmap for an entry
|
|
uint32_t offset = 0; // Offset in nvs partition
|
|
uint8_t namespace_ID; // Namespace ID found
|
|
|
|
pi = esp_partition_find(ESP_PARTITION_TYPE_DATA, // Get partition iterator for
|
|
ESP_PARTITION_SUBTYPE_ANY, // this partition
|
|
partname);
|
|
if (pi) {
|
|
nvs = esp_partition_get(pi); // Get partition struct
|
|
esp_partition_iterator_release(pi); // Release the iterator
|
|
dbgprint("Partition %s found, %d bytes", partname, nvs->size);
|
|
} else {
|
|
snprintf(Log_Buffer, Log_BufferLength, "Partition %s not found!", partname);
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
return NULL;
|
|
}
|
|
namespace_ID = FindNsID(nvs, _namespace); // Find ID of our namespace in NVS
|
|
File backupFile = gFSystem.open(_destFile, FILE_WRITE);
|
|
if (!backupFile) {
|
|
return false;
|
|
}
|
|
while (offset < nvs->size) {
|
|
result = esp_partition_read(nvs, offset, // Read 1 page in nvs partition
|
|
&buf,
|
|
sizeof(nvs_page));
|
|
if (result != ESP_OK) {
|
|
snprintf(Log_Buffer, Log_BufferLength, "Error reading NVS!");
|
|
Log_Println(Log_Buffer, LOGLEVEL_ERROR);
|
|
return false;
|
|
}
|
|
|
|
i = 0;
|
|
|
|
while (i < 126) {
|
|
bm = (buf.Bitmap[i / 4] >> ((i % 4) * 2)) & 0x03; // Get bitmap for this entry
|
|
if (bm == 2) {
|
|
if ((namespace_ID == 0xFF) || // Show all if ID = 0xFF
|
|
(buf.Entry[i].Ns == namespace_ID)) { // otherwise just my namespace
|
|
if (isNumber(buf.Entry[i].Key)) {
|
|
String s = gPrefsRfid.getString((const char *)buf.Entry[i].Key);
|
|
backupFile.printf("%s%s%s%s\n", stringOuterDelimiter, buf.Entry[i].Key, stringOuterDelimiter, s.c_str());
|
|
}
|
|
}
|
|
i += buf.Entry[i].Span; // Next entry
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
offset += sizeof(nvs_page); // Prepare to read next page in nvs
|
|
pagenr++;
|
|
}
|
|
|
|
backupFile.close();
|
|
Led_SetPause(false);
|
|
|
|
return true;
|
|
}
|