diff --git a/.DS_Store b/.DS_Store index 5172429..c5e3576 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8281e64..0f0d740 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "platformio.platformio-ide" - ] -} \ No newline at end of file + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6e77734 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/Users/torsten/.platformio/penv/bin/python" +} \ No newline at end of file diff --git a/README.md b/README.md index 7a8ae96..8afcced 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Optionally, GPIO 17 can be used to drive a NPN-transistor (BC337-40) that pulls * make decision, if MQTT should be enabled (enableMqtt) * if yes, set the IP of the MQTT-server accordingly and check the MQTT-topics (states and commands) * in setup() RFID-cards can be statically linked to an action/file. Everything is stored in uC's NVS. -* if Neopixel enabled: set NUM_LEDS to the LED-number of your Neopixel-ring. +* if Neopixel enabled: set NUM_LEDS to the LED-number of your Neopixel-ring and define the Neopixel-type using `#define CHIPSET` * please note: by using audiobook-mode any playlist-savings will be overwritten with every start unless the RFID-cards in setup() are commented out. Main way to link RFID to an action will be a webservice (still under development) * compile and upload the sketch diff --git a/html/tonuino_logo.png b/html/tonuino_logo.png new file mode 100644 index 0000000..67231f6 Binary files /dev/null and b/html/tonuino_logo.png differ diff --git a/html/transformHtmlToCharArray.pl b/html/transformHtmlToCharArray.pl new file mode 100644 index 0000000..8dca048 --- /dev/null +++ b/html/transformHtmlToCharArray.pl @@ -0,0 +1,20 @@ +#!/usr/bin/perl + +use strict; + +open(my $input, "<", "website.html") + or die "Can't open < website.html: $!"; + +open(my $output, ">", "../src/websiteMgmt.h") + or die "Can't open < websiteMgmt.h: $!"; + +print $output "static const char mgtWebsite[] PROGMEM = \""; +while (my $row = <$input>) { + $row =~ s/\"/\\"/g; + $row =~ s/\n/\\/g; + print $output "$row\n"; +} +print $output "\";"; + +close($input); +close($output); \ No newline at end of file diff --git a/html/website.html b/html/website.html new file mode 100644 index 0000000..9d7c904 --- /dev/null +++ b/html/website.html @@ -0,0 +1,300 @@ + + + + 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

+
+
+ + + + +
+
+ + + + +
+
+ + +
+ + +
+
+
+ + + diff --git a/html/websiteMgmt.h b/html/websiteMgmt.h new file mode 100644 index 0000000..f5bc9e7 --- /dev/null +++ b/html/websiteMgmt.h @@ -0,0 +1,263 @@ +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/platformio.ini b/platformio.ini index ed68f0c..97ef427 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,6 +13,7 @@ platform = espressif32 board = nodemcu-32s framework = arduino monitor_speed = 115200 +board_build.partitions = no_ota.csv lib_deps = https://github.com/schreibfaul1/ESP32-audioI2S.git @@ -20,6 +21,10 @@ lib_deps = https://github.com/knolleary/pubsubclient.git https://github.com/biologist79/ESP32FTPServer https://github.com/FastLED/FastLED.git - https://github.com/me-no-dev/ESPAsyncWebServer.git https://github.com/biologist79/rfid.git - ;https://github.com/bblanchon/ArduinoJson.git + ESP Async WebServer + https://github.com/me-no-dev/AsyncTCP + https://github.com/bblanchon/ArduinoJson.git + +extra_scripts = + pre:processHtml.py \ No newline at end of file diff --git a/processHtml.py b/processHtml.py new file mode 100644 index 0000000..76f1db4 --- /dev/null +++ b/processHtml.py @@ -0,0 +1,18 @@ +#!/usr/bin/python + +content = '' + +with open('html/website.html', 'r') as r: + data = r.read().replace('\n', '\\\n') + #content += '"' + #data = r.read().replace('\n', '"\n') + data = data.replace('\"', '\\"') + content += data + +with open('src/websiteMgmt.h', 'w') as w: + w.write("static const char mgtWebsite[] PROGMEM = \"") + w.write(content) + w.write("\";") + +r.close() +w.close() \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index ec61615..c553532 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,8 +24,11 @@ #include #endif #include "logmessages.h" +#include "websiteMgmt.h" +#include #include #include "website.h" +#include @@ -162,7 +165,7 @@ uint8_t const cardIdSize = 4; // RFID // Volume uint8_t maxVolume = 21; // Maximum volume that can be adjusted uint8_t minVolume = 0; // Lowest volume that can be adjusted -uint8_t initVolume = 3; // 0...21 +uint8_t initVolume = 3; // 0...21 (If not found in NVS, this one will be taken) // Sleep uint8_t maxInactivityTime = 10; // Time in minutes, after uC is put to deep sleep because of inactivity uint8_t sleepTimer = 30; // Sleep timer in minutes that can be optionally used (and modified later via MQTT or RFID) @@ -213,7 +216,7 @@ char ftpUser[10] = "esp32"; // FTP-user char ftpPassword[15] = "esp32"; // FTP-password // MQTT-configuration -char mqtt_server[16] = "192.168.2.43"; // IP-address of MQTT-server +char mqtt_server[16] = "192.168.2.43"; // IP-address of MQTT-server (if not found in NVS this one will be taken) #ifdef MQTT_ENABLE #define DEVICE_HOSTNAME "ESP32-Tonuino" // Name that that is used for MQTT static const char topicSleepCmnd[] PROGMEM = "Cmnd/Tonuino/Sleep"; @@ -245,6 +248,8 @@ void notFound(AsyncWebServerRequest *request) { request->send(404, "text/plain", "Not found"); } AsyncWebServer wServer(81); +AsyncWebSocket ws("/ws"); +AsyncEventSource events("/events"); // Audio/mp3 SPIClass spiSD(HSPI); @@ -330,6 +335,7 @@ void sortPlaylist(const char** arr, int n); bool startsWith(const char *str, const char *pre); void trackControlToQueueSender(const uint8_t trackCommand); void rfidPreferenceLookupHandler (void); +void sendWebsocketData(uint32_t client, uint8_t code); void trackQueueDispatcher(const char *_sdFile, const uint32_t _lastPlayPos, const uint32_t _playMode, const uint16_t _trackLastPlayed); void volumeHandler(const int32_t _minVolume, const int32_t _maxVolume); void volumeToQueueSender(const int32_t _newVolume); @@ -2312,6 +2318,7 @@ void rfidPreferenceLookupHandler (void) { lastTimeActiveTimestamp = millis(); snprintf(logBuf, sizeof(logBuf)/sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(rfidTagReceived), rfidTagId); currentRfidTagId = strdup(rfidTagId); + sendWebsocketData(0, 10); // Push new rfidTagId to all websocket-clients loggerNl(logBuf, LOGLEVEL_INFO); String s = prefsRfid.getString(rfidTagId, "-1"); // Try to lookup rfidId in NVS @@ -2443,12 +2450,13 @@ wl_status_t wifiManager(void) { WiFi.begin(_ssid, _pwd); uint8_t tryCount=0; - while (WiFi.status() != WL_CONNECTED && tryCount <= 4) { + while (WiFi.status() != WL_CONNECTED && tryCount <= 6) { delay(500); Serial.print(F(".")); + Serial.print(WiFi.status()); tryCount++; wifiCheckLastTimestamp = millis(); - if (tryCount >= 4 && WiFi.status() == WL_CONNECT_FAILED) { + if (tryCount >= 2 && WiFi.status() == WL_CONNECT_FAILED) { WiFi.begin(_ssid, _pwd); // ESP32-workaround } } @@ -2469,6 +2477,236 @@ wl_status_t wifiManager(void) { } +// Used for substitution of some variables/templates of html-file +String templateProcessor(const String& templ) { + if(templ == "FTP_USER") { + return prefsSettings.getString("ftpuser", "-1"); + } else if (templ == "FTP_PWD") { + return prefsSettings.getString("ftppassword", "-1"); + } else if (templ == "INIT_LED_BRIGHTBESS") { + return String(prefsSettings.getUChar("iLedBrightness", 0)); + } else if (templ == "NIGHT_LED_BRIGHTBESS") { + return String(prefsSettings.getUChar("nLedBrightness", 0)); + } else if (templ == "MAX_INACTIVITY") { + return String(prefsSettings.getUInt("mInactiviyT", 0)); + } else if (templ == "INIT_VOLUME") { + return String(prefsSettings.getUInt("initVolume", 0)); + } else if (templ == "MAX_VOLUME") { + return String(prefsSettings.getUInt("maxVolume", 0)); + } else if (templ == "MQTT_SERVER") { + return prefsSettings.getString("mqttServer", "-1"); + } else if (templ == "MQTT_ENABLE") { + if (enableMqtt) { + return String("checked=\"checked\""); + } else { + return String(); + } + } else if (templ == "IPv4") { + myIP = WiFi.localIP(); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); + return String(logBuf); + } else if (templ == "RFID_TAG_ID") { + return String(currentRfidTagId); + } + + 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(); + + if (error) { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(error.c_str()); + return false; + } + + if (doc.containsKey("general")) { + uint8_t iVol = doc["general"]["iVol"].as(); + uint8_t mVol = doc["general"]["mVol"].as(); + uint8_t iBright = doc["general"]["iBright"].as(); + uint8_t nBright = doc["general"]["nBright"].as(); + uint8_t iTime = doc["general"]["iTime"].as(); + + prefsSettings.putUInt("initVolume", iVol); + prefsSettings.putUInt("maxVolume", mVol); + prefsSettings.putUChar("iLedBrightness", iBright); + prefsSettings.putUChar("nLedBrightness", nBright); + prefsSettings.putUInt("mInactiviyT", iTime); + + // Check if settings were written successfully + if (prefsSettings.getUInt("initVolume", 0) != iVol || + prefsSettings.getUInt("maxVolume", 0) != mVol || + prefsSettings.getUChar("iLedBrightness", 0) != iBright || + prefsSettings.getUChar("nLedBrightness", 0) != nBright || + prefsSettings.getUInt("mInactiviyT", 0) != iTime) { + Serial.println("net gut!"); + return false; + } + + } else if (doc.containsKey("ftp")) { + const char *_ftpUser = doc["ftp"]["ftpUser"]; + const char *_ftpPwd = doc["ftp"]["ftpPwd"]; + + prefsSettings.putString("ftpuser", (String) _ftpUser); + prefsSettings.putString("ftppassword", (String) _ftpPwd); + + if(!(String(_ftpUser).equals(prefsSettings.getString("ftpuser", "-1")) || + String(_ftpPwd).equals(prefsSettings.getString("ftppassword", "-1")))) { + Serial.println("net gut2!"); + return false; + } + + } else if (doc.containsKey("mqtt")) { + uint8_t _mqttEnable = doc["mqtt"]["mqttEnable"].as(); + const char *_mqttServer = object["mqtt"]["mqttServer"]; + prefsSettings.putUChar("enableMQTT", _mqttEnable); + prefsSettings.putString("mqttServer", (String) _mqttServer); + + if ((prefsSettings.getUChar("enableMQTT", 99) != _mqttEnable) || + (!String(_mqttServer).equals(prefsSettings.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]; + snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s0%s0%s%u%s0", stringDelimiter, stringDelimiter, stringDelimiter, _modId, stringDelimiter); + prefsRfid.putString(_rfidIdModId, rfidString); + + String s = prefsRfid.getString(_rfidIdModId, "-1"); + if (s.compareTo(rfidString)) { + return false; + } + + } else if (doc.containsKey("rfidAssign")) { + const char *_rfidIdModId = object["rfidAssign"]["rfidIdMusic"]; + const char *_fileOrUrl = object["rfidAssign"]["fileOrUrl"]; + uint8_t _playMode = object["rfidAssign"]["playMode"]; + char rfidString[275]; + snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s%s%s0%s%u%s0", stringDelimiter, _fileOrUrl, stringDelimiter, stringDelimiter, _playMode, stringDelimiter); + prefsRfid.putString(_rfidIdModId, rfidString); + + String s = prefsRfid.getString(_rfidIdModId, "-1"); + if (s.compareTo(rfidString)) { + return false; + } + + } else if (doc.containsKey("wifiConfig")) { + const char *_ssid = object["wifiConfig"]["ssid"]; + const char *_pwd = object["wifiConfig"]["pwd"]; + + prefsSettings.putString("SSID", _ssid); + prefsSettings.putString("Password", _pwd); + + String sSsid = prefsSettings.getString("SSID", "-1"); + String sPwd = prefsSettings.getString("Password", "-1"); + + if (sSsid.compareTo(_ssid) || sPwd.compareTo(_pwd)) { + return false; + } + } + + return true; +} + + +// Sends JSON-answers via websocket +void sendWebsocketData(uint32_t client, uint8_t code) { + const size_t CAPACITY = JSON_OBJECT_SIZE(1) + 20; + StaticJsonDocument doc; + JsonObject object = doc.to(); + + if (code == 1) { + object["status"] = "ok"; + } else if (code == 2) { + object["status"] = "error"; + } else if (code == 10) { + object["rfidId"] = currentRfidTagId; + } + char jBuf[50]; + serializeJson(doc, jBuf, sizeof(jBuf) / sizeof(jBuf[0])); + + if (client == 0) { + ws.printfAll(jBuf); + } else { + ws.printf(client, 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: %u\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); + uint8_t returnCode; + + if (processJsonRequest((char*)data)) { + returnCode = 1; + } else { + returnCode = 0; + } + sendWebsocketData(client->id(), 1); + //ws.printf(client->id(), doc); + + 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"); + } + + if (info->opcode == WS_TEXT) { + //client->text("I got your text message"); + } else { + //client->binary("I got your binary message"); + } + } else { + if (info->message_opcode == WS_TEXT) { + data[len] = 0; + } + + if ((info->index + len) == info->len) { + if (info->final) { + if (info->message_opcode == WS_TEXT) { + //client->text("I got your text message"); + } else { + //client->binary("I got your binary message"); + } + } + } + } + } +} + + void setup() { Serial.begin(115200); srand(esp_random()); @@ -2556,6 +2794,7 @@ void setup() { uint8_t nvsILedBrightness = prefsSettings.getUChar("iLedBrightness", 0); if (nvsILedBrightness) { initialLedBrightness = nvsILedBrightness; + ledBrightness = nvsILedBrightness; snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %d", (char *) FPSTR(initialBrightnessfromNvs), nvsILedBrightness); loggerNl(logBuf, LOGLEVEL_INFO); } else { @@ -2633,11 +2872,12 @@ void setup() { uint8_t nvsEnableMqtt = prefsSettings.getUChar("enableMQTT", 99); switch (nvsEnableMqtt) { case 99: - prefsSettings.putUChar("enableMQTT", 0); + prefsSettings.putUChar("enableMQTT", enableMqtt); loggerNl((char *) FPSTR(wroteMqttFlagToNvs), LOGLEVEL_ERROR); break; case 1: - prefsSettings.putUChar("enableMQTT", enableMqtt); + //prefsSettings.putUChar("enableMQTT", enableMqtt); + enableMqtt = nvsEnableMqtt; snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(loadedMqttActiveFromNvs), nvsEnableMqtt); loggerNl(logBuf, LOGLEVEL_INFO); break; @@ -2725,8 +2965,12 @@ void setup() { lastTimeActiveTimestamp = millis(); // initial set after boot if (wifiManager() == WL_CONNECTED) { + // Websocket + ws.onEvent(onWebsocketEvent); + wServer.addHandler(&ws); + wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, "text/plain", "Hello, world"); + request->send_P(200, "text/html", mgtWebsite, templateProcessor); }); // Send a GET request to /get?message= diff --git a/src/websiteMgmt.h b/src/websiteMgmt.h new file mode 100644 index 0000000..a1fff48 --- /dev/null +++ b/src/websiteMgmt.h @@ -0,0 +1,301 @@ +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/svg/README.md b/svg/README.md index 494620d..1076dd8 100644 --- a/svg/README.md +++ b/svg/README.md @@ -1,2 +1,2 @@ -holes_n_ring.svg: Used to CNC a ring and the holes into a wood-enclosure
-inner ring.svg: Used to cnc a ring that the speaker is screwed to. The ring is glued to the enclosure. \ No newline at end of file +`holes_n_ring.svg`: Used to CNC a ring and the holes into a wood-enclosure
+`inner ring.svg`: Used to cnc a ring that the speaker is screwed to. The ring is glued to the enclosure. \ No newline at end of file