Browse Source

Merge branch 'master' into feature/breakoutpcb

master
biologist79 5 years ago
committed by GitHub
parent
commit
9e3f2faa99
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 54
      Hardware-Plaforms/Wemos Lolin32/README.md
  3. BIN
      PCBs/Wemos Lolin32/Gerber/gerber.zip
  4. BIN
      PCBs/Wemos Lolin32/KiCad/Kicad-files.zip
  5. BIN
      PCBs/Wemos Lolin32/Pictures/3d-Model downside.jpg
  6. BIN
      PCBs/Wemos Lolin32/Pictures/3d-Model upside.jpg
  7. BIN
      PCBs/Wemos Lolin32/Pictures/Tonuino-Lolin32-Schematics.pdf
  8. 54
      PCBs/Wemos Lolin32/README.md
  9. 40
      README.md
  10. 380
      html/website.html
  11. 263
      html/websiteMgmt.h
  12. BIN
      pictures/Mgmt-GUI1.jpg
  13. BIN
      pictures/Mgmt-GUI2.jpg
  14. BIN
      pictures/Mgmt-GUI3.jpg
  15. BIN
      pictures/Mgmt-GUI4.jpg
  16. BIN
      pictures/Mgmt-GUI5.jpg
  17. BIN
      pictures/Mgmt-GUI6.jpg
  18. BIN
      pictures/Mgmt-GUI_connection_broken.jpg
  19. BIN
      pictures/Mgmt_GUI_action_ok.jpg
  20. 6
      platformio.ini
  21. 6
      processHtml.py
  22. 373
      src/main.cpp
  23. 153
      src/settings.h
  24. 380
      src/websiteMgmt.h

1
.gitignore

@ -9,4 +9,3 @@ CMakeLists.txt
CMakeListsPrivate.txt
cmake-build-az-delivery-devkit-v4/
cmake-build-debug/
venv/

54
Hardware-Plaforms/Wemos Lolin32/README.md

@ -0,0 +1,54 @@
# Tonuino-PCB based on Wemos' Lolin32
## Introduction
After I've been asked many times to provide a PCB, I finally did so :-) It makes use of Wemos' Lolin32 which is the predecessor of Lolin D32. D32's advantage over Lolin32 is especially, that a voltage-divider for measuring battery's voltage is already integrated (fixed-wired to GPIO 35). However, as I wasn't aware of that when buying Lolin32 and because of now, that multiple Lolin32 are here on my desk, my reasonable intention was to use them. So things would have been a bit easier with D32 but in the end it works the same way with Lolin32.
## Features
* Fits Wemos Lolin32 (not Lolin D32, Lolin D32 pro or Lolin 32 lite!)
* Outer diameter: 56 x 93mm
* JST-PH 2.0-connectors for buttons, rotary encoder, Neopixel, RFID and battery (not 2.54mm pitch!)
* 2.54mm-connectors for MAX98354a and uSD-card-reader
* Mosfet-circuit that switches off MAX98357a, Neopixel, headphone-pcb and uSD-card-reader automatically when deepsleep is active
* All peripherals are solely driven at 3.3V! Keep this especially in mind when choosing uSD-reader. If in doubts use one without voltage-regulator (link below).
* If [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) is used, MAX98357a is automatically muted when there's a headphone plugged in and vice versa.
* If `HEADPHONE_ADJUST_ENABLE` is enabled and a headphone is plugged in, an alternative maximum volume is activated. I added this feature because [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) makes use of an amp that (probably) "allows" children to damage ears. This maximum volume can be set and re-adjusted via webgui.
## Prerequisites
* If no [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) is connected, make sure `HEADPHONE_ADJUST_ENABLE` is not active.
* I used 390/130 kOhms-resistors as voltage-divider. However, make sure to use a multimeter to determine their exact values in order to achieve a better battery-measurement. They can be configured in `settings.h` as `rdiv1` und `rdiv2`. Hint: for Lolin D32's battery-measurement 100k+100k were used. However, I decided to change the ratio from 50/50% to 25/75% to have a "better" signal. 100/100 might be work as well; didn't test it.
## Things to mention
* RFID: In order to avoid buying a 6pin-JST-PH-connector I used 2x3pin. This is because I already had ten of them (see link below). Accidently, on my PCB-layout, I switched the direction of one of these two connectors. However, it's just an visual issue.
* In contrast to Lolin D32, Lolin32 doesn't feature an integrated voltage-divider. That's why on the lower left there's a JST-PH2.0-connector to connect the LiPo-battery. Make sure to connect + to the left und GND to the right. From there you need to solder two short wires (5cm or so) onto the pcb with a JST-PH2.0-connector attached on the other side. This one needs to be plugged into Lolin32. Please note: Lolin's JST-PH2.0-connector needs (+) left side and GND right side. Don't be confused if black/red-colouring of the JST-wires used seems "weird" (black => (+); red => GND).
* Better don't solder Lolin32 directly to the PCB. I recommend to make use of female connectors instead (link below).
## Hardware-setup
The heart of my project is an ESP32 on a [Wemos Lolin32 development-board](https://www.ebay.de/itm/4MB-Flash-WEMOS-Lolin32-V1-0-0-WIFI-Bluetooth-Card-Based-ESP-32-ESP-WROOM-32/162716855489). Make sure to install the drivers for the USB/Serial-chip (CP2102 e.g.).
* [MAX98357A (like Adafruit's)](https://de.aliexpress.com/item/32999952454.html)
* [uSD-card-reader 3.3V only](https://www.ebay.de/itm/Micro-SPI-Kartenleser-Card-Reader-2GB-SD-8GB-SDHC-Card-3-3V-ESP8266-Arduino-NEU/333796577968)
* [RFID-reader](https://www.amazon.de/AZDelivery-Reader-Arduino-Raspberry-gratis/dp/B074S8MRQ7)
* [RFID-tags](https://www.amazon.de/AZDelivery-Keycard-56MHz-Schlüsselkarte-Karte/dp/B07TVJPTM7)
* [Neopixel-ring](https://de.aliexpress.com/item/32673883645.html)
* [Rotary Encoder](https://de.aliexpress.com/item/33041814942.html)
* [Buttons](https://de.aliexpress.com/item/32896285438.html)
* [Speaker](https://www.visaton.de/de/produkte/chassiszubehoer/breitband-systeme/fr-7-4-ohm)
* uSD-card: doesn't have to be a super-fast one; uC is limiting the throughput. Tested 32GB without any problems.
* [JSP PH-2.0-connectors](https://de.aliexpress.com/item/32968344273.html)
* [Female connector](https://de.aliexpress.com/item/32724478308.html)
* [(optional) IDC-connector female 6pin for headphone-pcb](https://de.aliexpress.com/item/33029492417.html)
* [(optional) IDC-connector male 6pin for headphone-pcb](https://de.aliexpress.com/item/1005001400147026.html)
## Parts
* 1x IRF530NPbF (N-channel MOSFET)
* 1x NDP6020P (P-channel MOSFET)
* 1x 1k resistor
* 1x 10k resistor
* 2x 100k resistor
* 1x 130k resistor (can be replaced by 100k)
* 1x 390k resistor (can be replaced by 100k)
## Where to order?
I ordered my PCBs at [jlcpcb](https://jlcpcb.com/). You have to order at least 5 pcs, which is only at 2$ + shipping. It took two weeks to arrive. If you want to have a look at the PCBs first (without having KiCad installed), visit [Gerberlook](https://www.gerblook.org/) and upload `gerber.zip` from the Gerberfiles-folder.
## Do I need to install KiCad?
Unless you don't want to change anything: no! All you need to provide are the gerberfiles (`gerber.zip`) to your manufactur (e.g. [jlcpcb](https://jlcpcb.com/)). However, all Kicad-files used are provided as well.

BIN
PCBs/Wemos Lolin32/Gerber/gerber.zip

BIN
PCBs/Wemos Lolin32/KiCad/Kicad-files.zip

BIN
PCBs/Wemos Lolin32/Pictures/3d-Model downside.jpg

After

Width: 1438  |  Height: 799  |  Size: 53 KiB

BIN
PCBs/Wemos Lolin32/Pictures/3d-Model upside.jpg

After

Width: 1438  |  Height: 799  |  Size: 65 KiB

BIN
PCBs/Wemos Lolin32/Pictures/Tonuino-Lolin32-Schematics.pdf

54
PCBs/Wemos Lolin32/README.md

@ -0,0 +1,54 @@
# Tonuino-PCB based on Wemos' Lolin32
## Introduction
After I've been asked many times to provide a PCB, I finally did so :-) It makes use of Wemos' Lolin32 which is the predecessor of Lolin D32. D32's advantage over Lolin32 is especially, that a voltage-divider for measuring battery's voltage is already integrated (fixed-wired to GPIO 35). However, as I wasn't aware of that when buying Lolin32 and because of now, that multiple Lolin32 are here on my desk, my reasonable intention was to use them. So things would have been a bit easier with D32 but in the end it works the same way with Lolin32.
## Features
* Fits Wemos Lolin32 (not Lolin D32, Lolin D32 pro or Lolin 32 lite!)
* Outer diameter: 56 x 93mm
* JST-PH 2.0-connectors for buttons, rotary encoder, Neopixel, RFID and battery (not 2.54mm pitch!)
* 2.54mm-connectors for MAX98354a and uSD-card-reader
* Mosfet-circuit that switches off MAX98357a, Neopixel, headphone-pcb and uSD-card-reader automatically when deepsleep is active
* All peripherals are solely driven at 3.3V! Keep this especially in mind when choosing uSD-reader. If in doubts use one without voltage-regulator (link below).
* If [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) is used, MAX98357a is automatically muted when there's a headphone plugged in and vice versa.
* If `HEADPHONE_ADJUST_ENABLE` is enabled and a headphone is plugged in, an alternative maximum volume is activated. I added this feature because [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) makes use of an amp that (probably) "allows" children to damage ears. This maximum volume can be set and re-adjusted via webgui.
## Prerequisites
* If no [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) is connected, make sure `HEADPHONE_ADJUST_ENABLE` is not active.
* I used 390/130 kOhms-resistors as voltage-divider. However, make sure to use a multimeter to determine their exact values in order to achieve a better battery-measurement. They can be configured in `settings.h` as `rdiv1` und `rdiv2`. Hint: for Lolin D32's battery-measurement 100k+100k were used. However, I decided to change the ratio from 50/50% to 25/75% to have a "better" signal. 100/100 might be work as well; didn't test it.
## Things to mention
* RFID: In order to avoid buying a 6pin-JST-PH-connector I used 2x3pin. This is because I already had ten of them (see link below). Accidently, on my PCB-layout, I switched the direction of one of these two connectors. However, it's just an visual issue.
* In contrast to Lolin D32, Lolin32 doesn't feature an integrated voltage-divider. That's why on the lower left there's a JST-PH2.0-connector to connect the LiPo-battery. Make sure to connect + to the left und GND to the right. From there you need to solder two short wires (5cm or so) onto the pcb with a JST-PH2.0-connector attached on the other side. This one needs to be plugged into Lolin32. Please note: Lolin's JST-PH2.0-connector needs (+) left side and GND right side. Don't be confused if black/red-colouring of the JST-wires used seems "weird" (black => (+); red => GND).
* Better don't solder Lolin32 directly to the PCB. I recommend to make use of female connectors instead (link below).
## Hardware-setup
The heart of my project is an ESP32 on a [Wemos Lolin32 development-board](https://www.ebay.de/itm/4MB-Flash-WEMOS-Lolin32-V1-0-0-WIFI-Bluetooth-Card-Based-ESP-32-ESP-WROOM-32/162716855489). Make sure to install the drivers for the USB/Serial-chip (CP2102 e.g.).
* [MAX98357A (like Adafruit's)](https://de.aliexpress.com/item/32999952454.html)
* [uSD-card-reader 3.3V only](https://www.ebay.de/itm/Micro-SPI-Kartenleser-Card-Reader-2GB-SD-8GB-SDHC-Card-3-3V-ESP8266-Arduino-NEU/333796577968)
* [RFID-reader](https://www.amazon.de/AZDelivery-Reader-Arduino-Raspberry-gratis/dp/B074S8MRQ7)
* [RFID-tags](https://www.amazon.de/AZDelivery-Keycard-56MHz-Schlüsselkarte-Karte/dp/B07TVJPTM7)
* [Neopixel-ring](https://de.aliexpress.com/item/32673883645.html)
* [Rotary Encoder](https://de.aliexpress.com/item/33041814942.html)
* [Buttons](https://de.aliexpress.com/item/32896285438.html)
* [Speaker](https://www.visaton.de/de/produkte/chassiszubehoer/breitband-systeme/fr-7-4-ohm)
* uSD-card: doesn't have to be a super-fast one; uC is limiting the throughput. Tested 32GB without any problems.
* [JSP PH-2.0-connectors](https://de.aliexpress.com/item/32968344273.html)
* [Female connector](https://de.aliexpress.com/item/32724478308.html)
* [(optional) IDC-connector female 6pin for headphone-pcb](https://de.aliexpress.com/item/33029492417.html)
* [(optional) IDC-connector male 6pin for headphone-pcb](https://de.aliexpress.com/item/1005001400147026.html)
## Parts
* 1x IRF530NPbF (N-channel MOSFET)
* 1x NDP6020P (P-channel MOSFET)
* 1x 1k resistor
* 1x 10k resistor
* 2x 100k resistor
* 1x 130k resistor (can be replaced by 100k)
* 1x 390k resistor (can be replaced by 100k)
## Where to order?
I ordered my PCBs at [jlcpcb](https://jlcpcb.com/). You have to order at least 5 pcs, which is only at 2$ + shipping. It took two weeks to arrive. If you want to have a look at the PCBs first (without having KiCad installed), visit [Gerberlook](https://www.gerblook.org/) and upload `gerber.zip` from the Gerberfiles-folder.
## Do I need to install KiCad?
Unless you don't want to change anything: no! All you need to provide are the gerberfiles (`gerber.zip`) to your manufactur (e.g. [jlcpcb](https://jlcpcb.com/)). However, all Kicad-files used are provided as well.

40
README.md

@ -1,7 +1,7 @@
# Tonuino based on ESP32 with I2S-DAC-support
## NEWS
Currently I'm working on a new Tonuino that is completely based on 3.3V and makes use of an (optional) [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308). As uC-develboard a Lolin32 is used and it's (optionally) battery-powered. So stay tuned...
Finally, the long announced Tonuino-PCB for Wemos' Lolin32 is [there](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Wemos%20Lolin32). It can (optionally) be used alongside with a [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308). As uC-develboard a Lolin32 is used and it's (optionally) battery-powered. Peripherals (Neopixel, RFID, headphone-pcb and MAX98357a) are driven at 3.3V solely.
## History
[...]
@ -13,7 +13,10 @@ Currently I'm working on a new Tonuino that is completely based on 3.3V and make
* 20.11.2020: Added directive `MEASURE_BATTERY_VOLTAGE`: monitoring battery's voltage is now supported.
* 25.11.2020: WiFi can npw be activated/deactivated instantly by pressing two buttons.
* 28.11.2020: Battery's voltage can now be visualized by Neopixel by short-press of rotary encoder's burtton.
* 28.11.2020. Added directive `PLAY_LAST_RFID_AFTER_REBOOT`: Tonuino will recall the last RFID played after reboot.
* 28.11.2020: Added directive `PLAY_LAST_RFID_AFTER_REBOOT`: Tonuino will recall the last RFID played after reboot.
* 05.12.2020: Added filebrowser to webgui (thanks @mariolukas for contribution!)
* 05.12.2020: Moved all user-relevant settings to src/settings.h
* 06.12.2020: Added PCB for Wemos Lolin32
More to come...
## Disclaimer
@ -29,14 +32,14 @@ The core of my implementation is based on the popular [ESP32 by Espressif](https
The basic idea of Tonuino (and my fork, respectively) is to provide a way, to use the Arduino-platform for a music-control-concept that supports locally stored music-files instead of being fully cloud-dependend. This basically means that RFID-tags are used to direct a music-player. Even for kids this concept is simple: place an RFID-object (card, character) on top of a box and the music starts to play. Place another RFID-object on it and anything else is played. Simple as that.
## Hardware-setup
The heart of my project is an ESP32 on a [Wemos Lolin32 development-board](https://www.ebay.de/itm/4MB-Flash-WEMOS-Lolin32-V1-0-0-WIFI-Bluetooth-Card-Based-ESP-32-ESP-WROOM-32/162716855489). If ordered in China (Aliexpress, eBay e.g.) it's pretty cheap (around 4€) but even in Europe it's only around 8€. Make sure to install the drivers for the USB/Serial-chip (CP2102 e.g.). If unsure have a look at eBay or Aliexpress for "Lolin 32".
* [MAX98357A (like Adafruit's)](https://www.ebay.de/itm/MAX98357-Amplifier-Breakout-Interface-I2S-Class-D-Module-For-ESP32-Raspberry-Pi/174319322988)
The heart of my project is an ESP32 on a [Wemos Lolin32 development-board](https://www.ebay.de/itm/4MB-Flash-WEMOS-Lolin32-V1-0-0-WIFI-Bluetooth-Card-Based-ESP-32-ESP-WROOM-32/162716855489). If ordered in China (Aliexpress, eBay e.g.) it's pretty cheap (around 4€) but even in Europe it's only around 8€. Make sure to install the drivers for the USB/Serial-chip (CP2102 e.g.).
* [MAX98357A (like Adafruit's)](https://de.aliexpress.com/item/32999952454.html)
* [uSD-card-reader 3.3V + 5V](https://www.amazon.de/AZDelivery-Reader-Speicher-Memory-Arduino/dp/B077MB17JB)
* [uSD-card-reader 3.3V only](https://www.ebay.de/itm/Micro-SPI-Kartenleser-Card-Reader-2GB-SD-8GB-SDHC-Card-3-3V-ESP8266-Arduino-NEU/333796577968)
* [RFID-reader](https://www.amazon.de/AZDelivery-Reader-Arduino-Raspberry-gratis/dp/B074S8MRQ7)
* [RFID-tags](https://www.amazon.de/AZDelivery-Keycard-56MHz-Schlüsselkarte-Karte/dp/B07TVJPTM7)
* [Neopixel-ring](https://www.ebay.de/itm/LED-Ring-24-x-5050-RGB-LEDs-WS2812-integrierter-Treiber-NeoPixel-kompatibel/282280571841)
* [Rotary Encoder](https://www.amazon.de/gp/product/B07T3672VK)
* [Neopixel-ring](https://de.aliexpress.com/item/32673883645.html)
* [Rotary Encoder](https://de.aliexpress.com/item/33041814942.html)
* [Buttons](https://de.aliexpress.com/item/32896285438.html)
* [Speaker](https://www.visaton.de/de/produkte/chassiszubehoer/breitband-systeme/fr-7-4-ohm)
* uSD-card: doesn't have to be a super-fast one; uC is limiting the throughput. Tested 32GB without any problems.
@ -45,11 +48,13 @@ Most of them can be ordered cheaper directly in China. It's just a give an short
## Getting Started
I recommend Microsoft's [Visual Studio Code](https://code.visualstudio.com/) or [Atom](https://atom.io/) alongside with [Platformio Plugin](https://platformio.org/install/ide?install=vscode). Since my project on Github contains [platformio.ini](platformio.ini), libraries used should be fetched automatically. Please note: if you use another ESP32-develboard (different to Lolin32) you might have to change "env:" in platformio.ini to the corresponding value. Documentation can be found [here](https://docs.platformio.org/en/latest/projectconf.html). After that it might be necessary to adjust the names of the GPIO-pins in the upper #define-section of my code.
In the upper section of main.cpp you can specify the modules that should be compiled into the code.
In src/settings.h you have to specify the modules that should be compiled into the code and all the user-relevant config-parameters as well.
Please note: if MQTT is enabled it's still possible to deactivate it via webgui.
## Wiring (2 SPI-instances)
A lot of wiring is necessary to get ESP32-Tonuino working. After my first experiments I soldered the stuff on a board in order to avoid wild-west-cabling. Especially for the interconnect between uC and uSD-card-reader make sure to use short wires (like 10cm or so)! Important: you can easily connect another I2S-DACs by just connecting them in parallel to the I2S-pins (DIN, BCLK, LRC). This is true for example if you plan to integrate a [line/headphone-pcb](https://www.adafruit.com/product/3678). In general, this runs fine. But unfortunately especially this board lacks of a headphone jack, that takes note if a plug is inserted or not. Best way is to use a [headphone jack](https://www.conrad.de/de/p/cliff-fcr1295-klinken-steckverbinder-3-5-mm-buchse-einbau-horizontal-polzahl-3-stereo-schwarz-1-st-705830.html) that has a pin that is pulled to GND, if there's no plug and vice versa. Using for example a MOSFET-circuit, this GND-signal can be inverted in a way, that MAX98357.SD is pulled down to GND if there's a plug. Doing that will turn off the speaker immediately if there's a plug and vice versa. Have a look at the PCB-folder in order to view the detailed solution. Here's an example for such a [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) that makes use of GND.
A lot of wiring is necessary to get ESP32-Tonuino working. After my first experiments I soldered the stuff on a board in order to avoid wild-west-cabling. Especially for the interconnect between uC and uSD-card-reader make sure to use short wires (like 10cm or so)! Important: you can easily connect another I2S-DACs by just connecting them in parallel to the I2S-pins (DIN, BCLK, LRC). This is true for example if you plan to integrate a [line/headphone-pcb](https://www.adafruit.com/product/3678). In general, this runs fine. But unfortunately especially this board lacks of a headphone jack, that takes note if a plug is inserted or not. Best way is to use a [headphone jack](https://www.conrad.de/de/p/cliff-fcr1295-klinken-steckverbinder-3-5-mm-buchse-einbau-horizontal-polzahl-3-stereo-schwarz-1-st-705830.html) that has a pin that is pulled to GND, if there's no plug and vice versa. Using for example a MOSFET-circuit, this GND-signal can be inverted in a way, that MAX98357.SD is pulled down to GND if there's a plug. Doing that will turn off the speaker immediately if there's a plug and vice versa. Have a look at the PCB-folder in order to view the detailed solution. Here's an example for such a [headphone-pcb](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TMD1308) that makes use of GND.<br />
Have a look at my PCB in the subfolder Hardware-Platforms/Wemos Lolin32. Probably this makes things easier.
| ESP32 (GPIO) | Hardware | Pin | Comment |
| ------------- | --------------------- | ------ | ------------------------------------------------------------ |
@ -91,7 +96,7 @@ A lot of wiring is necessary to get ESP32-Tonuino working. After my first experi
Optionally, GPIO 17 can be used to drive a NPN-transistor (BC337-40) that pulls a p-channel MOSFET (IRF9520) to GND in order to switch on/off 5V-current. Transistor-circuit is described [here](https://dl6gl.de/schalten-mit-transistoren.html): Just have a look at Abb. 4. Values of the resistors I used: R1: 10k, R2: omitted(!), R4: 10k, R5: 4,7k. <br />
I also tested this successfully for a 3.3V-setup with IRF530NPBF (N-channel MOSFET) and NDP6020P (P-channel MOSFET). Resistor-values: R1: 100k, R2: omitted(!), R4: 100k, R5: 4,7k. A 3.3V-setup is helpful if you want to battery-power your Tonuino and 5V is not available in battery-mode. For example this is the case when using Wemos Lolin32 with only having LiPo connected. <br />
This also works for a 3.3V-setup with IRF530NPBF (N-channel MOSFET) and NDP6020P (P-channel MOSFET). Resistor-values: R1: 100k, R2: omitted(!), R4: 100k, R5: 1k. A 3.3V-setup is helpful if you want to battery-power your Tonuino and 5V is not available in battery-mode. For example this is the case when using Wemos Lolin32 with only having LiPo connected. Please refer the schematics for my [Lolin32-PCB](https://github.com/biologist79/Tonuino-ESP32-I2S/blob/master/PCBs/Wemos%20Lolin32/Pictures/Tonuino-Lolin32-Schematics.pdf) for further informations.<br />
Advice: When powering a SD-card-reader solely with 3.3V, make sure to use one WITHOUT a voltage regulator. Or at least one with a pin dedicated for 3.3V (bypassing voltage regulator). This is because if 3.3V go through the voltage regulator a small voltage-drop will be introduced, which may lead to SD-malfunction as the resulting voltage is a bit too low. Vice versa if you want to connect your reader solely to 5V, make sure to have one WITH a voltage regulator :-).
## Wiring (1 SPI-instance) [EXPERIMENTAL, maybe not working!]
@ -138,12 +143,13 @@ When using a develboard with for example SD-card-reader already integrated (Loli
[Here](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/Hardware-Plaforms/ESP32-A1S-Audiokit) I described a solution for a board with many GPIOs used internally and a very limited number of GPIOs exposed. That's why I had to use different SPI-GPIOs for RFID as well. Please note I used a slightly modified [RFID-lib](https://github.com/biologist79/Tonuino-ESP32-I2S/tree/master/Hardware-Plaforms/ESP32-A1S-Audiokit/lib/MFRC522) there.
## Prerequisites / tipps
* Open settings.h
* choose if optional modules (MQTT, FTP, Neopixel) should be compiled/enabled
* for debugging-purposes serialDebug can be set to ERROR, NOTICE, INFO or DEBUG.
* if MQTT=yes, set the IP or hostname of the MQTT-server accordingly and check the MQTT-topics (states and commands)
* if Neopixel enabled: set NUM_LEDS to the LED-number of your Neopixel-ring and define the Neopixel-type using `#define CHIPSET`
* For debugging-purposes serialDebug can be set to ERROR, NOTICE, INFO or DEBUG.
* If MQTT=yes, set the IP or hostname of the MQTT-server accordingly and check the MQTT-topics (states and commands)
* If Neopixel enabled: set NUM_LEDS to the LED-number of your Neopixel-ring and define the Neopixel-type using `#define CHIPSET`
* If you're using Arduino-IDE please make sure to change ESP32's partition-layout to `No OTA (2MB APP/2MB Spiffs)` as otherwise the sketch won't fit into the flash-memory.
* Please keep in mind that working SD is mandatory. Unless `SD_NOT_MANDATORY_ENABLE` is not set, Tonuino will never fully start up if SD is not working. Only use `SD_NOT_MANDATORY_ENABLE` for debugging as for normal operational mode, not having SD working doesn't make sense. Even if only webradio-mode is intended, SD would be used to backup RFID-tag-learnings (/backup.txt).
* Please keep in mind that working SD is mandatory. Unless `SD_NOT_MANDATORY_ENABLE` is not set, Tonuino will never fully start up if SD is not working. Only use `SD_NOT_MANDATORY_ENABLE` for debugging as for normal operational mode, not having SD working doesn't make sense. Even if only webradio-mode is intended, SD would be used to backup RFID-tag-learnings (/backup.txt) or JSON-file-index (files.json).
* If you want to monitor battery's voltage, make sure to enable `MEASURE_BATTERY_VOLTAGE`. Use a voltage-divider as voltage of a LiPo is way too high for ESP32 (only 3.3V supported!). For my tests I connected VBat with a serial connection of 130k + 390k resistors (VBat(+)--130k--X--390k--VBat(-)). X is the measure-point where to connect the GPIO to. If using Lolin D32 or Lolin D32 pro, make sure to adjust both values to 100k each and change GPIO to 35 as this is already integrated and wired fixed.
Please note: via GUI upper and lower voltage cut-offs for visualisation of battery-voltage (Neopixel) is available. Additional GUI-configurable values are interval (in minutes) for checking battery voltage and the cut off-voltage below whose a warning is shown via Neopixel.
* If you're using a headphone-pcb with a [headphone jack](https://www.conrad.de/de/p/cliff-fcr1295-klinken-steckverbinder-3-5-mm-buchse-einbau-horizontal-polzahl-3-stereo-schwarz-1-st-705830.html) that has a pin to indicate if there's a plug, you can use this signal along with the feature `HEADPHONE_ADJUST_ENABLE` to limit the maximum headphone-voltage automatically. As per default you have to invert this signal (with a P-channel MOSFET) and connect it to GPIO22.
@ -184,13 +190,17 @@ Webgui #4:
Webgui #5:
<img src="https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/pictures/Mgmt-GUI5.jpg" width="300">
Webgui #6:
<img src="https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/pictures/Mgmt-GUI5.jpg" width="300">
Webgui: websocket broken:
<img src="https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/pictures/Mgmt-GUI_connection_broken.jpg" width="300">
Webgui: action ok:
<img src="https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/pictures/Mgmt_GUI_action_ok.jpg" width="300">
Please note: as you apply a RFID-tag to the RFID-reader, the corresponding ID is pushed to the GUI (and flashes a few times; so you can't miss it). So there's no need to enter such IDs manually (unless you want to).
Please note: as you apply a RFID-tag to the RFID-reader, the corresponding ID is pushed to the GUI. So there's no need to enter such IDs manually (unless you want to). Filepath can be filled out by selecting a file/directory in the tree.
IMPORTANT: Every time you add, delete or rename stuff on the SD-card, it's necessary to re-index. Simply click on the refresh-button below the filetree and wait until it's done.
## Interacting with Tonuino
### Playmodes
@ -277,7 +287,7 @@ After having Tonuino running on your ESP32 in your local WiFi, the webinterface-
* General-configuration (volume (speaker + headphone), neopixel-brightness (night-mode + initial), sleep after inactivity)
### FTP (optional)
In order to avoid exposing uSD-card or disassembling the Tonuino all the time for adding new music, it's possible to transfer music onto the uSD-card using FTP. Please make sure to set the max. number of parallel connections to ONE in your FTP-client. My recommendation is [Filezilla](https://filezilla-project.org/). But don't expect fast data-transfer. Initially it was around 145 kB/s but after modifying ftp-server-lib (changing from 4 kB static-buffer to 16 kB heap-buffer) I saw rates improving a bit. Please note: if music is played in parallel, this rate decrases dramatically! So better stop playback then doing a FTP-transfer. However, playback sounds normal if a FTP-upload is performed in parallel. Default-user and password are set via `ftpUser` and `ftpPassword` but can be changed later via GUI.
In order to avoid exposing uSD-card or disassembling the Tonuino all the time for adding new music, it's possible to transfer music onto the uSD-card using FTP. Please make sure to set the max. number of parallel connections to ONE in your FTP-client. My recommendation is [Filezilla](https://filezilla-project.org/). But don't expect fast data-transfer. Initially it was around 145 kB/s but after modifying ftp-server-lib (changing from 4 kB static-buffer to 16 kB heap-buffer) I saw rates improving to around 185 kB/s. Please note: if music is played in parallel, this rate decrases dramatically! So better stop playback when doing a FTP-transfer. However, playback sounds normal if a FTP-upload is performed in parallel. Default-user and password are set to `esp32` / `esp32` but can be changed later via GUI.
### Files / ID3-tags (IMPORTANT!)
Make sure to not use filenames that contain German 'Umlaute'. I've been told this is also true for mp3's ID3-tags.

380
html/website.html

@ -5,17 +5,81 @@
<meta charset="utf-8">
<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://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://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/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;
height: 200px;
margin: 0em 0em 1em 0em;
overflow-y: scroll;
}
.fa-sync:hover {
color: #666666;
}
.clickForRefresh {
text-align: center;
color: gray;
cursor: pointer;
margin-top: 5em;
}
.clickForRefresh:hover {
color: darkgray;
}
.filetree-container {
position: relative;
}
.indexing-progress {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
opacity: 0.7;
display: none;
}
.refreshAction{
text-align: right;
font-size: 0.8em;
}
.refreshAction:hover{
cursor: pointer;
color: darkgray;
}
.overlay {
z-index: 9;
opacity: 0.8;
background: #1a1919;
height: 200px;
display: none;
width: 100%;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-sm bg-primary navbar-dark">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand">
<img src="https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/html/tonuino_logo.png" width="30" height="30" class="d-inline-block align-top" alt="" />
<img src="https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/html/tonuino_logo.png"
width="30" height="30" class="d-inline-block align-top" alt=""/>
Tonuino
</a>
<div class="collapse navbar-collapse" id="collapsibleNavbar">
@ -60,7 +124,8 @@
<label for="pwd">Passwort:</label>
<input type="password" class="form-control" id="pwd" placeholder="Passwort" name="pwd" required>
<label for="hostname">Tonuino-Name (Hostname):</label>
<input type="text" class="form-control" id="hostname" placeholder="tonuino" name="hostname" value="%HOSTNAME%" pattern="^[^-\.]{2,32}" required>
<input type="text" class="form-control" id="hostname" placeholder="tonuino" name="hostname"
value="%HOSTNAME%" pattern="^[^-\.]{2,32}" required>
</div>
<button type="reset" class="btn btn-secondary">Reset</button>
<button type="submit" class="btn btn-primary">Absenden</button>
@ -72,20 +137,35 @@
<form action="#rfidMusicTags" method="POST" onsubmit="rfidAssign('rfidMusicTags'); return false">
<div class="form-group col-md-6">
<label for="rfidIdMusic">RFID-Chip-Nummer (12-stellig)</label>
<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>
<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 class="filetree-container">
<div id="filebrowser">
<div class="filetree demo" id="filetree"></div>
<div class="refreshAction">
<span id="refreshAction" data-toggle="tooltip" data-placement="top" title="Datei Liste aktualisieren."><i class="fas fa-sync fa-1x"></i> Dateiliste aktualisieren</span>
</div>
</div>
<div id="indexing-progress" class="indexing-progress overlay">
<div style="text-align: center; color:white; margin-top:2em;">
<div><i class="fas fa-sync fa-spin fa-2x"></i> <br><br> Der Prozess kann mehrere Minuten dauern...</div>
<div id="currentProcessedFile"></div>
</div>
</div>
</div>
<label for="playMode">Abspielmodus</label>
<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-file-and-folder" value="3">Hörbuch</option>
<option class="option-file-and-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>
</div>
<button type="reset" class="btn btn-secondary">Reset</button>
@ -98,7 +178,8 @@
<form class="needs-validation" action="#rfidModTags" method="POST" onsubmit="rfidMods('rfidModTags'); return false">
<div class="form-group col-md-6">
<label for="rfidIdMod">RFID-Chip-Nummer (12-stellig)</label>
<input type="text" class="form-control" id="rfidIdMod" maxlength="12" pattern="[0-9]{12}" placeholder="%RFID_TAG_ID%" name="rfidIdMod" required>
<input type="text" class="form-control" id="rfidIdMod" maxlength="12" pattern="[0-9]{12}"
placeholder="%RFID_TAG_ID%" name="rfidIdMod" required>
<div class="invalid-feedback">
Bitte eine 12-stellige Zahl eingeben.
</div>
@ -125,7 +206,8 @@
</div>
<div class="container my-5" id="mqttConfig">
<h2>MQTT-Konfiguration</h2>
<form class="needs-validation" action="#mqttConfig" method="POST" onsubmit="mqttSettings('mqttConfig'); return false">
<form class="needs-validation" action="#mqttConfig" method="POST"
onsubmit="mqttSettings('mqttConfig'); return false">
<div class="form-check col-md-6">
<input class="form-check-input" type="checkbox" value="1" id="mqttEnable" name="mqttEnable" %MQTT_ENABLE%>
<label class="form-check-label" for="mqttEnable">
@ -134,11 +216,14 @@
</div>
<div class="form-group my-2 col-md-6">
<label for="mqttServer">MQTT-Server</label>
<input type="text" class="form-control" id="mqttServer" minlength="7" maxlength="%MQTT_SERVER_LENGTH%" placeholder="z.B. 192.168.2.89" name="mqttServer" value="%MQTT_SERVER%">
<input type="text" class="form-control" id="mqttServer" minlength="7" maxlength="%MQTT_SERVER_LENGTH%"
placeholder="z.B. 192.168.2.89" name="mqttServer" value="%MQTT_SERVER%">
<label for="mqttUser">MQTT-Benutzername (optional):</label>
<input type="text" class="form-control" id="mqttUser" maxlength="%MQTT_USER_LENGTH%" placeholder="Benutzername" name="mqttUser" value="%MQTT_USER%">
<input type="text" class="form-control" id="mqttUser" maxlength="%MQTT_USER_LENGTH%"
placeholder="Benutzername" name="mqttUser" value="%MQTT_USER%">
<label for="mqttPwd">Passwort (optional):</label>
<input type="password" class="form-control" id="mqttPwd" maxlength="%MQTT_PWD_LENGTH%" placeholder="Passwort" name="mqttPwd" value="%MQTT_PWD%">
<input type="password" class="form-control" id="mqttPwd" maxlength="%MQTT_PWD_LENGTH%"
placeholder="Passwort" name="mqttPwd" value="%MQTT_PWD%">
</div>
<button type="reset" class="btn btn-secondary">Reset</button>
<button type="submit" class="btn btn-primary">Absenden</button>
@ -150,9 +235,11 @@
<form action="#ftpConfig" method="POST" onsubmit="ftpSettings('ftpConfig'); return false">
<div class="form-group col-md-6">
<label for="ftpUser">FTP-Benutzername:</label>
<input type="text" class="form-control" id="ftpUser" maxlength="%FTP_USER_LENGTH%" placeholder="Benutzername" name="ftpUser" value="%FTP_USER%" required>
<input type="text" class="form-control" id="ftpUser" maxlength="%FTP_USER_LENGTH%"
placeholder="Benutzername" name="ftpUser" value="%FTP_USER%" required>
<label for="pwd">Passwort:</label>
<input type="password" class="form-control" id="ftpPwd" maxlength="%FTP_PWD_LENGTH%" placeholder="Passwort" name="ftpPwd" value="%FTP_PWD%" required>
<input type="password" class="form-control" id="ftpPwd" maxlength="%FTP_PWD_LENGTH%" placeholder="Passwort"
name="ftpPwd" value="%FTP_PWD%" required>
</div>
<button type="reset" class="btn btn-secondary">Reset</button>
<button type="submit" class="btn btn-primary">Absenden</button>
@ -164,37 +251,50 @@
<form action="#generalConfig" method="POST" onsubmit="genSettings('generalConfig'); return false">
<div class="form-group col-md-6">
<label for="initialVolume">Lautstärke nach dem Einschalten</label>
<input type="number" min="1" max="21" class="form-control" id="initialVolume" name="initialVolume" value="%INIT_VOLUME%" required>
<input type="number" min="1" max="21" class="form-control" id="initialVolume" name="initialVolume"
value="%INIT_VOLUME%" required>
<label for="maxVolumeSpeaker">Maximale Lautstärke (Lautsprecher)</label>
<input type="number" min="1" max="21" class="form-control" id="maxVolumeSpeaker" name="maxVolumeSpeaker" value="%MAX_VOLUME_SPEAKER%" required>
<input type="number" min="1" max="21" class="form-control" id="maxVolumeSpeaker" name="maxVolumeSpeaker"
value="%MAX_VOLUME_SPEAKER%" required>
<label for="maxVolumeHeadphone">Maximale Lautstärke (Kopfhörer)</label>
<input type="number" min="1" max="21" class="form-control" id="maxVolumeHeadphone" name="maxVolumeHeadphone" value="%MAX_VOLUME_HEADPHONE%" required>
<input type="number" min="1" max="21" class="form-control" id="maxVolumeHeadphone" name="maxVolumeHeadphone"
value="%MAX_VOLUME_HEADPHONE%" required>
</div>
<div class="form-group col-md-6">
<label for="initBrightness">Neopixel-Helligkeit nach dem Einschalten</label>
<input type="number" min="0" max="255" class="form-control" id="initBrightness" name="initBrightness" value="%INIT_LED_BRIGHTNESS%" required>
<input type="number" min="0" max="255" class="form-control" id="initBrightness" name="initBrightness"
value="%INIT_LED_BRIGHTNESS%" required>
<label for="nightBrightness">Neopixel-Helligkeit im Nachtmodus</label>
<input type="number" min="0" max="255" class="form-control" id="nightBrightness" name="nightBrightness" value="%NIGHT_LED_BRIGHTNESS%" required>
<input type="number" min="0" max="255" class="form-control" id="nightBrightness" name="nightBrightness"
value="%NIGHT_LED_BRIGHTNESS%" required>
</div>
<div class="form-group col-md-6">
<label for="inactivityTime">Deep-Sleep nach Inaktivität (Minuten)</label>
<input type="number" min="1" max="1440" class="form-control" id="inactivityTime" name="inactivityTime" value="%MAX_INACTIVITY%" required>
<input type="number" min="1" max="1440" class="form-control" id="inactivityTime" name="inactivityTime"
value="%MAX_INACTIVITY%" required>
</div>
<div class="form-group col-md-6">
<label for="warningLowVoltage">(Optional) Spannungsgrenze (Batterie), ab der Warnung auf Neopixel angezeigt wird (z.B. 3.4)</label>
<input type="text" class="form-control" id="warningLowVoltage" name="warningLowVoltage" value="%WARNING_LOW_VOLTAGE%" pattern="^\d{1,2}(\.\d{1,3})?" required>
<label for="warningLowVoltage">(Optional) Spannungsgrenze (Batterie), ab der Warnung auf Neopixel angezeigt
wird (z.B. 3.4)</label>
<input type="text" class="form-control" id="warningLowVoltage" name="warningLowVoltage"
value="%WARNING_LOW_VOLTAGE%" pattern="^\d{1,2}(\.\d{1,3})?" required>
</div>
<div class="form-group col-md-6">
<label for="voltageIndicatorLow">(Optional) Unterer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B. 3.1)</label>
<input type="text" class="form-control" id="voltageIndicatorLow" name="voltageIndicatorLow" value="%VOLTAGE_INDICATOR_LOW%" pattern="^\d{1,2}(\.\d{1,3})?" required>
<label for="voltageIndicatorLow">(Optional) Unterer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B.
3.1)</label>
<input type="text" class="form-control" id="voltageIndicatorLow" name="voltageIndicatorLow"
value="%VOLTAGE_INDICATOR_LOW%" pattern="^\d{1,2}(\.\d{1,3})?" required>
</div>
<div class="form-group col-md-6">
<label for="voltageIndicatorHigh">(Optional) Oberer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B. 4.2)</label>
<input type="text" class="form-control" id="voltageIndicatorHigh" name="voltageIndicatorHigh" value="%VOLTAGE_INDICATOR_HIGH%" pattern="^\d{1,2}(\.\d{1,3})?" required>
<label for="voltageIndicatorHigh">(Optional) Oberer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B.
4.2)</label>
<input type="text" class="form-control" id="voltageIndicatorHigh" name="voltageIndicatorHigh"
value="%VOLTAGE_INDICATOR_HIGH%" pattern="^\d{1,2}(\.\d{1,3})?" required>
</div>
<div class="form-group col-md-6">
<label for="voltageCheckInterval">(Optional) Häufigkeit der Spannungsmessung (Batterie) in Minuten</label>
<input type="number" min="1" max="180" class="form-control" id="voltageCheckInterval" name="voltageCheckInterval" value="%VOLTAGE_CHECK_INTERVAL%" required>
<input type="number" min="1" max="180" class="form-control" id="voltageCheckInterval"
name="voltageCheckInterval" value="%VOLTAGE_CHECK_INTERVAL%" required>
</div>
<button type="reset" class="btn btn-secondary">Reset</button>
<button type="submit" class="btn btn-primary">Absenden</button>
@ -212,40 +312,152 @@
</form>
<div class="messages col-md-6 my-2"></div>
</div>
<script>
<script type="text/javascript">
var DEBUG = false;
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 host = $(location).attr('hostname');
if (DEBUG) {
host = "192.168.178.112";
}
var socket = new WebSocket("ws://%IPv4%/ws");
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 connect() {
socket = new WebSocket("ws://%IPv4%/ws");
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 ping() {
var myObj = {
"ping": {
ping: 'ping'
function renderFileTree() {
var filesURI = "/files";
if (DEBUG) {
filesURI = "http://" + host + "/files";
}
};
var myJSON = JSON.stringify(myObj);
socket.send(myJSON);
tm = setTimeout(function () {
alert("Die Verbindung zum Tonuino ist unterbrochen!\\nBitte Seite neu laden.");
}, 5000);
$('#filetree').jstree({
'core': {
'check_callback': true,
'data': {
url: filesURI,
error: function (XMLHttpRequest, textStatus, errorThrown) {
$('#j1_loading').hide();
$("#refreshAction").hide();
$('#filetree').html("<div class='clickForRefresh' ><i class='fas fa-sync fa-1x'><span id='#clickForRefresh' > Dateien suchen.</span></div>");
$('#filetree').on("click", function () {
refreshFileList();
});
toastr.error("Die Verzeichnis-Liste konnte nicht geladen werden.");
}
},
function pong() {
clearTimeout(tm);
},
'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);
if ((data.instance._model.data['#']['children'].length == 0)) {
toastr.info("Der SD-Karten-Index muss erzeugt werden.");
}
}).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") {
$('.option-folder').show();
$('.option-file').hide();
$('#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();
$("#indexing-progress").show();
$("#refreshAction").hide();
});
$('#playMode').on("change", function () {
if (this.value == 8) {
$('#filebrowser').slideUp();
} else {
$('#filebrowser').slideDown();
}
});
function showFileIndexingState() {
$("#indexing-progress").show();
$("#refreshAction").hide();
}
function hideFileIndexingState() {
$("#indexing-progress").hide();
$("#refreshAction").show();
}
var socket = undefined;
var tm;
function connect() {
socket = new WebSocket("ws://" + host + "/ws");
socket.onopen = function () {
setInterval(ping, 15000);
};
socket.onclose = function (e) {
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
socket = null;
setTimeout(function () {
connect();
}, 5000);
@ -253,7 +465,6 @@
socket.onerror = function (err) {
console.error('Socket encountered error: ', err.message, 'Closing socket');
socket.close();
};
socket.onmessage = function(event) {
@ -262,20 +473,60 @@
if (socketMsg.rfidId != null) {
document.getElementById('rfidIdMod').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." );
} if (socketMsg.status != null) {
if (socketMsg.status == 'ok') {
$("#" + lastIdclicked).find('.messages').html(okBox);
} else {
$("#" + lastIdclicked).find('.messages').html(errorBox);
$("#rfidIdMusic").effect("highlight", {color:"#abf5af"}, 3000);
$("#rfidIdMod").effect("highlight", {color:"#abf5af"}, 3000);
} if ("status" in socketMsg) {
if (socketMsg.status == "ok") {
toastr.success("Aktion erfolgreich ausgeführt." );
}
} if (socketMsg.pong != null) {
} if ("pong" in socketMsg) {
if (socketMsg.pong == 'pong') {
pong();
}
} if ("refreshFileList" in socketMsg) {
hideFileIndexingState();
toastr.info("Die Dateiliste wurde neu erzeugt!");
$('#filetree').jstree(true).refresh();
}
if ("indexingState" in socketMsg) {
if(socketMsg.indexingState != null) {
$("#currentProcessedFile").text(socketMsg.indexingState);
console.log(socketMsg.indexingState);
}
}
};
}
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);
$("#refreshAction").hide();
socket.send(myJSON);
showFileIndexingState();
};
function genSettings(clickedId) {
@ -374,6 +625,15 @@
var myJSON = JSON.stringify(myObj);
socket.send(myJSON);
}
$(document).ready(function () {
connect();
renderFileTree();
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
});
</script>
</body>
</html>

263
html/websiteMgmt.h

@ -1,263 +0,0 @@
static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<html lang=\"de\">\
<head>\
<title>ESPuino-Konfiguration</title>\
<meta charset=\"utf-8\">\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
<link rel=\"stylesheet\" href=\"https://ts-cs.de/css/bootstrap.min.css\">\
<script src=\"https://ts-cs.de/js/jquery.min.js\"></script>\
<script src=\"https://ts-cs.de/js/popper.min.js\"></script>\
<script src=\"https://ts-cs.de/js/bootstrap.min.js\"></script>\
</head>\
<body>\
<nav class=\"navbar navbar-expand-sm bg-primary navbar-dark\">\
<button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\
<span class=\"navbar-toggler-icon\"></span>\
</button>\
<a class=\"navbar-brand\">\
<img src=\"./tonuino_logo.png\" width=\"30\" height=\"30\" class=\"d-inline-block align-top\" alt=\"\" />\
Tonuino\
</a>\
<div class=\"collapse navbar-collapse\" id=\"collapsibleNavbar\">\
<ul class=\"navbar-nav mr-auto\">\
<li class=\"nav-item\">\
<a class=\"nav-link\" href=\"#wifiConfig\">WLAN</a>\
</li>\
<li class=\"nav-item\">\
<a class=\"nav-link\" href=\"#rfidMusicTags\">RFID-Zuweisungen</a>\
</li>\
<li class=\"nav-item\">\
<a class=\"nav-link\" href=\"#rfidModTags\">RFID-Modifikationen</a>\
</li>\
<li class=\"nav-item\">\
<a class=\"nav-link\" href=\"#mqttConfig\">MQTT</a>\
</li>\
<li class=\"nav-item\">\
<a class=\"nav-link\" href=\"#ftpConfig\">FTP</a>\
</li>\
<li class=\"nav-item\">\
<a class=\"nav-link\" href=\"#generalConfig\">Allgemein</a>\
</li>\
</ul>\
</div>\
</nav>\
<br />\
<div class=\"container\" id=\"wifiConfig\">\
<h2>WLAN-Konfiguration</h2>\
<form action=\"#wifiConfig\" method=\"GET\">\
<div class=\"form-group col-md-6\">\
<label for=\"SSID\">WLAN-Name (SSID):</label>\
<input type=\"text\" class=\"form-control\" id=\"SSID\" placeholder=\"SSID\" name=\"SSID\" required>\
<div class=\"invalid-feedback\">\
Bitte SSID des WLANs eintragen.\
</div>\
<label for=\"pwd\">Passwort:</label>\
<input type=\"password\" class=\"form-control\" id=\"pwd\" placeholder=\"Passwort\" name=\"pwd\" required>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\" onclick=\"wifiConfig()\">Absenden</button>\
</form>\
</div>\
<div class=\"container my-5\" id=\"rfidMusicTags\">\
<h2>RFID-Zuweisungen</h2>\
<form action=\"#rfidMusicTags\" method=\"GET\">\
<div class=\"form-group col-md-6\">\
<label for=\"rfidIdMusic\">RFID-Chip-Nummer</label>\
<input type=\"text\" class=\"form-control\" id=\"rfidIdMusic\" maxlength=\"12\" pattern=\"[0-9]{12}\" placeholder=\"012345678912\" name=\"rfidIdMusic\" required>\
<label for=\"fileOrUrl\">Datei, Verzeichnis oder URL (URL nur für Webradio)</label>\
<input type=\"text\" class=\"form-control\" id=\"fileOrUrl\" maxlength=\"255\" placeholder=\"z.B. /mp3/Hoerspiele/Yakari/Yakari_und_seine_Freunde.mp3\" name=\"fileOrUrl\" required>\
<label for=\"playMode\">Abspielmodus</label>\
<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=\"8\">Alle Titel eines Verzeichnis (zufällig, Endlosschleife)</option>\
<option value=\"9\">Webradio</option>\
</select>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\" onclick=\"rfidAssign()\">Absenden</button>\
</form>\
</div>\
<div class=\"container my-5\" id=\"rfidModTags\">\
<h2>RFID-Modifkationen</h2>\
<form class=\"needs-validation\" action=\"#rfidModTags\" method=\"GET\" novalidate>\
<div class=\"form-group col-md-6\">\
<label for=\"rfidIdMod\">RFID-Chip-Nummer</label>\
<input type=\"text\" class=\"form-control\" id=\"rfidIdMod\" maxlength=\"12\" pattern=\"[0-9]{12}\" placeholder=\"012345678912\" name=\"rfidIdMod\" required>\
<div class=\"invalid-feedback\">\
Bitte eine 12-stellige Zahl eingeben.\
</div>\
<label for=\"modId\">Abspielmodus</label>\
<select class=\"form-control\" id=\"modId\" name=\"modId\">\
<option value=\"100\">Tastensperre</option>\
<option value=\"101\">Schlafen nach 15 Minuten</option>\
<option value=\"102\">Schlafen nach 30 Minuten</option>\
<option value=\"103\">Schlafen nach 1 Stunde</option>\
<option value=\"104\">Schlafen nach 2 Stunden</option>\
<option value=\"105\">Schlafen nach Ende des Titels</option>\
<option value=\"106\">Schlafen nach Ende der Playlist</option>\
<option value=\"110\">Wiederhole Playlist (endlos)</option>\
<option value=\"111\">Wiederhole Titel (endlos)</option>\
<option value=\"112\">Dimme LEDs (Nachtmodus)</option>\
</select>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\" onclick=\"rfidMods()\">Absenden</button>\
</form>\
</div>\
<div class=\"container my-5\" id=\"mqttConfig\">\
<h2>MQTT-Konfiguration</h2>\
<form class=\"needs-validation\" action=\"#mqttConfig\" method=\"GET\" novalidate>\
<div class=\"form-check col-md-6\">\
<input class=\"form-check-input\" type=\"checkbox\" value=\"1\" id=\"mqttEnable\" name=\"mqttEnable\" %MQTT_ENABLE%>\
<label class=\"form-check-label\" for=\"mqttEnable\">\
MQTT aktivieren\
</label>\
</div>\
<div class=\"form-group my-2 col-md-6\">\
<label for=\"mqttServer\">MQTT-Server (IP-Adresse)</label>\
<input type=\"text\" class=\"form-control\" id=\"mqttServer\" pattern=\"^((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$\" minlength=\"7\" maxlength=\"15\" placeholder=\"z.B. 192.168.2.89\" name=\"mqttServer\" value=\"%MQTT_SERVER%\">\
<div class=\"invalid-feedback\">\
Bitte eine gültige IPv4-Adresse eingeben, z.B. 192.168.2.89.\
</div>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\" onclick=\"mqttSettings()\">Absenden</button>\
</form>\
</div>\
<div class=\"container\" id=\"ftpConfig\">\
<h2>FTP-Konfiguration</h2>\
<form action=\"#ftpConfig\" method=\"GET\">\
<div class=\"form-group col-md-6\">\
<label for=\"ftpUser\">FTP-Benutzername:</label>\
<input type=\"text\" class=\"form-control\" id=\"ftpUser\" maxlength=\"32\" placeholder=\"Benutzername\" name=\"ftpUser\" value=\"%FTP_USER%\" required>\
<label for=\"pwd\">Passwort:</label>\
<input type=\"password\" class=\"form-control\" id=\"ftpPwd\" maxlength=\"32\" placeholder=\"Passwort\" name=\"ftpPwd\" value=\"%FTP_PWD%\" required>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\" onClick=\"ftpSettings()\">Absenden</button>\
</form>\
</div>\
<div class=\"container my-5\" id=\"generalConfig\">\
<h2>Allgemeine Konfiguration</h2>\
<form action=\"#generalConfig\" method=\"GET\">\
<div class=\"form-group col-md-6\">\
<label for=\"initialVolume\">Lautstärke nach dem Einschalten</label>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"initialVolume\" name=\"initialVolume\" value=\"%INIT_VOLUME%\" required>\
<label for=\"maxVolume\">Maximale Lautstärke</label>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"maxVolume\" name=\"maxVolume\" value=\"%MAX_VOLUME%\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"initBrightness\">Neopixel-Helligkeit nach dem Einschalten</label>\
<input type=\"number\" min=\"0\" max=\"255\" class=\"form-control\" id=\"initBrightness\" name=\"initBrightness\" value=\"%INIT_LED_BRIGHTBESS%\" required>\
<label for=\"nightBrightness\">Neopixel-Helligkeit im Nachtmodus</label>\
<input type=\"number\" min=\"0\" max=\"255\" class=\"form-control\" id=\"nightBrightness\" name=\"nightBrightness\" value=\"%NIGHT_LED_BRIGHTBESS%\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"inactivityTime\">Deep-Sleep nach Inaktivität (Minuten)</label>\
<input type=\"number\" min=\"1\" max=\"1440\" class=\"form-control\" id=\"inactivityTime\" name=\"inactivityTime\" value=\"%MAX_INACTIVITY%\" required>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\" onClick=\"genSettings()\">Absenden</button>\
</form>\
<script>\
(function() {\
'use strict';\
window.addEventListener('load', function() {\
// Fetch all the forms we want to apply custom Bootstrap validation styles to\
var forms = document.getElementsByClassName('needs-validation');\
// Loop over them and prevent submission\
var validation = Array.prototype.filter.call(forms, function(form) {\
form.addEventListener('submit', function(event) {\
if (form.checkValidity() === false) {\
event.preventDefault();\
event.stopPropagation();\
}\
form.classList.add('was-validated');\
}, false);\
});\
}, false);\
});\
\
let socket = new WebSocket(\"ws://%IPv4%:81/ws\");\
\
function genSettings() {\
\
var myObj = {\
\"general\": {\
iVol: document.getElementById('initialVolume').value,\
mVol: document.getElementById('maxVolume').value,\
iBright: document.getElementById('initBrightness').value,\
nBright: document.getElementById('nightBrightness').value,\
iTime: document.getElementById('inactivityTime').value\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
\
function ftpSettings() {\
var myObj = {\
\"ftp\": {\
ftpUser: document.getElementById('ftpUser').value,\
ftpPwd: document.getElementById('ftpPwd').value\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
\
function mqttSettings() {\
var myObj = {\
\"mqtt\": {\
enable: document.getElementById('mqttEnable').value,\
server: document.getElementById('mqttServer').value\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
\
function rfidMods() {\
var myObj = {\
\"rfidMod\": {\
rfidId: document.getElementById('rfidIdMod').value,\
modId: document.getElementById('modId').value\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
\
function rfidAssign() {\
var myObj = {\
\"rfidAssign\": {\
rfidId: document.getElementById('rfidIdMusic').value,\
fileOrUrl: document.getElementById('fileOrUrl').value,\
playmode: document.getElementById('playMode').value\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
\
function wifiConfig() {\
var myObj = {\
\"wifiConfig\": {\
ssid: document.getElementById('SSID').value,\
pwd: document.getElementById('pwd').value\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
</script>\
</div>\
</body>\
</html>\
";

BIN
pictures/Mgmt-GUI1.jpg

Before

Width: 1080  |  Height: 1856  |  Size: 336 KiB

After

Width: 1080  |  Height: 1208  |  Size: 197 KiB

BIN
pictures/Mgmt-GUI2.jpg

Before

Width: 1080  |  Height: 1223  |  Size: 188 KiB

After

Width: 883  |  Height: 1280  |  Size: 190 KiB

BIN
pictures/Mgmt-GUI3.jpg

Before

Width: 1080  |  Height: 1644  |  Size: 240 KiB

After

Width: 825  |  Height: 1280  |  Size: 132 KiB

BIN
pictures/Mgmt-GUI4.jpg

Before

Width: 1080  |  Height: 1471  |  Size: 245 KiB

After

Width: 921  |  Height: 1280  |  Size: 132 KiB

BIN
pictures/Mgmt-GUI5.jpg

Before

Width: 1080  |  Height: 602  |  Size: 81 KiB

After

Width: 851  |  Height: 1280  |  Size: 156 KiB

BIN
pictures/Mgmt-GUI6.jpg

After

Width: 1080  |  Height: 1857  |  Size: 313 KiB

BIN
pictures/Mgmt-GUI_connection_broken.jpg

Before

Width: 1080  |  Height: 1853  |  Size: 242 KiB

After

Width: 1080  |  Height: 1206  |  Size: 236 KiB

BIN
pictures/Mgmt_GUI_action_ok.jpg

Before

Width: 1080  |  Height: 1843  |  Size: 268 KiB

After

Width: 1032  |  Height: 1280  |  Size: 137 KiB

6
platformio.ini

@ -18,6 +18,8 @@ monitor_speed = 115200
board_build.partitions = no_ota.csv
;board_build.partitions = min_spiffs.csv
upload_port = /dev/cu.SLAB_USBtoUART
monitor_port = /dev/cu.SLAB_USBtoUART
lib_deps =
https://github.com/schreibfaul1/ESP32-audioI2S.git
@ -33,5 +35,5 @@ lib_deps =
; https://github.com/pschatzmann/ESP32-A2DP.git
; Don't forget to run this script if you changed the html-files provided in any way
;extra_scripts =
; pre:processHtml.py
extra_scripts =
pre:processHtml.py

6
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:

373
src/main.cpp

@ -1,19 +1,6 @@
// Define modules to compile:
#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd
#define FTP_ENABLE // Enables FTP-server
#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 LANGUAGE 1 // 1 = deutsch; 2 = english
#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 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 PLAY_LAST_RFID_AFTER_REBOOT // When restarting Tonuino, the last RFID that was active before, is recalled and played
//#define SD_NOT_MANDATORY_ENABLE // Only for debugging-purposes: Tonuino will also start without mounted SD-card anyway (will only try once to mount it). Will overwrite SHUTDOWN_IF_SD_BOOT_FAILS!
//#define BLUETOOTH_ENABLE // Doesn't work currently (so don't enable) as there's not enough DRAM available
// !!! MAKE SURE TO EDIT settings.h !!!
#include "settings.h" // Contains all user-relevant settings
#include <ESP32Encoder.h>
#include "Arduino.h"
#include <WiFi.h>
@ -60,45 +47,11 @@
// https://docs.aws.amazon.com/de_de/freertos-kernel/latest/dg/queue-management.html
// https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html#how-do-i-declare-a-global-flash-string-and-use-it
// Loglevels
#define LOGLEVEL_ERROR 1 // only errors
#define LOGLEVEL_NOTICE 2 // errors + important messages
#define LOGLEVEL_INFO 3 // infos + errors + important messages
#define LOGLEVEL_DEBUG 4 // almost everything
// Serial-logging-configuration
const uint8_t serialDebug = LOGLEVEL_INFO; // Current loglevel for serial console
// Serial-logging buffer
uint8_t serialLoglength = 200;
char *logBuf = (char*) calloc(serialLoglength, sizeof(char)); // Buffer for all log-messages
// GPIOs (uSD card-reader)
#define SPISD_CS 15
#ifndef SINGLE_SPI_ENABLE
#define SPISD_MOSI 13
#define SPISD_MISO 16 // 12 doesn't work with some devel-boards
#define SPISD_SCK 14
#endif
// GPIOs (RFID-readercurrentRfidTagId)
#define RST_PIN 99 // Not necessary but has to be set anyway; so let's use a dummy-number
#define RFID_CS 21
#define RFID_MOSI 23
#define RFID_MISO 19
#define RFID_SCK 18
// GPIOs (DAC)
#define I2S_DOUT 25
#define I2S_BCLK 27
#define I2S_LRC 26
// GPIO to detect if headphone was plugged in (pulled to GND)
#ifdef HEADPHONE_ADJUST_ENABLE
#define HP_DETECT 22 // Detects if there's a plug in the headphone jack or not
uint16_t headphoneLastDetectionDebounce = 1000; // Debounce-interval in ms when plugging in headphone
// Internal values
bool headphoneLastDetectionState;
uint32_t headphoneLastDetectionTimestamp = 0;
#endif
@ -107,34 +60,7 @@ char *logBuf = (char*) calloc(serialLoglength, sizeof(char)); // Buffer for all
BluetoothA2DPSink a2dp_sink;
#endif
// GPIO used to trigger transistor-circuit / RFID-reader
#define POWER 17
// GPIOs (Rotary encoder)
#define DREHENCODER_CLK 34 // If you want to reverse encoder's direction, just switch GPIOs of CLK with DT
#define DREHENCODER_DT 35 // Info: Lolin D32 / Lolin D32 pro 35 are using 35 for battery-voltage-monitoring!
#define DREHENCODER_BUTTON 32 // Button is used to switch Tonuino on and off
// GPIOs (Control-buttons)
#define PAUSEPLAY_BUTTON 5
#define NEXT_BUTTON 4
#define PREVIOUS_BUTTON 2 // Please note: as of 19.11.2020 changed from 33 to 2
// GPIOs (LEDs)
#define LED_PIN 12 // Pin where Neopixel is connected to
// (optional) Default-voltages for battery-monitoring
float warningLowVoltage = 3.4; // If battery-voltage is >= this value, a cyclic warning will be indicated by Neopixel (can be changed via GUI!)
uint8_t voltageCheckInterval = 10; // How of battery-voltage is measured (in minutes) (can be changed via GUI!)
float voltageIndicatorLow = 3.0; // Lower range for Neopixel-voltage-indication (0 leds) (can be changed via GUI!)
float voltageIndicatorHigh = 4.2; // Upper range for Neopixel-voltage-indication (all leds) (can be changed via GUI!)
#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!)
// Internal values
float refVoltage = 3.3; // Operation-voltage of ESP32; don't change!
uint16_t maxAnalogValue = 4095; // Highest value given by analogRead(); don't change!
uint32_t lastVoltageCheckTimestamp = 0;
@ -143,13 +69,6 @@ float voltageIndicatorHigh = 4.2; // Upper range for Neopixel-
#endif
#endif
// Neopixel-configuration
#ifdef NEOPIXEL_ENABLE
#define NUM_LEDS 24 // number of LEDs
#define CHIPSET WS2812B // type of Neopixel
#define COLOR_ORDER GRB
#endif
#ifdef PLAY_LAST_RFID_AFTER_REBOOT
bool recoverLastRfid = true;
#endif
@ -228,18 +147,13 @@ uint8_t initialLedBrightness = 16; // Initial brightness of
uint8_t ledBrightness = initialLedBrightness;
uint8_t nightLedBrightness = 2; // Brightness of Neopixel in nightmode
// Automatic restart
#ifdef SHUTDOWN_IF_SD_BOOT_FAILS
uint32_t deepsleepTimeAfterBootFails = 20; // Automatic restart takes place if boot was not successful after this period (in seconds)
#endif
// MQTT
bool enableMqtt = true;
#ifdef MQTT_ENABLE
uint8_t mqttFailCount = 3; // Number of times mqtt-reconnect is allowed to fail. If >= mqttFailCount to further reconnects take place
uint8_t const stillOnlineInterval = 60; // Interval 'I'm still alive' is sent via MQTT (in seconds)
#endif
// RFID
#define RFID_SCAN_INTERVAL 300 // in ms
uint8_t const cardIdSize = 4; // RFID
// Volume
uint8_t maxVolume = 21; // Current maximum volume that can be adjusted
@ -250,7 +164,7 @@ uint8_t initVolume = 3; // 0...21 (If not found
uint8_t maxVolumeHeadphone = 11; // Maximum volume that can be adjusted in headphone-mode (default; can be changed later via GUI)
#endif
// Sleep
uint8_t maxInactivityTime = 10; // Time in minutes, after uC is put to deep sleep because of inactivity
uint8_t maxInactivityTime = 10; // Time in minutes, after uC is put to deep sleep because of inactivity (and modified later via GUI)
uint8_t sleepTimer = 30; // Sleep timer in minutes that can be optionally used (and modified later via MQTT or RFID)
// FTP
uint8_t ftpUserLength = 10; // Length will be published n-1 as maxlength to GUI
@ -258,14 +172,6 @@ uint8_t ftpPasswordLength = 15; // Length will be publis
char *ftpUser = strndup((char*) "esp32", ftpUserLength); // FTP-user (default; can be changed later via GUI)
char *ftpPassword = strndup((char*) "esp32", ftpPasswordLength); // FTP-password (default; can be changed later via GUI)
// Button-configuration (change according your needs)
uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons
uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of previous/next-button
// Where to store the backup-file
#ifndef SD_NOT_MANDATORY_ENABLE
static const char backupFile[] PROGMEM = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done
#endif
// Don't change anything here unless you know what you're doing
// HELPER //
@ -304,7 +210,6 @@ uint8_t currentVolume = initVolume;
////////////
// AP-WiFi
static const char accessPointNetworkSSID[] PROGMEM = "Tonuino"; // Access-point's SSID
IPAddress apIP(192, 168, 4, 1); // Access-point's static IP
IPAddress apNetmask(255, 255, 255, 0); // Access-point's netmask
bool accessPointStarted = false;
@ -321,28 +226,6 @@ char *mqtt_server = strndup((char*) "192.168.2.43", mqttServerLength); // I
char *mqttUser = strndup((char*) "mqtt-user", mqttUserLength); // MQTT-user
char *mqttPassword = strndup((char*) "mqtt-password", mqttPasswordLength); // MQTT-password*/
#ifdef MQTT_ENABLE
#define DEVICE_HOSTNAME "ESP32-Tonuino" // Name that that is used for MQTT
static const char topicSleepCmnd[] PROGMEM = "Cmnd/Tonuino/Sleep";
static const char topicSleepState[] PROGMEM = "State/Tonuino/Sleep";
static const char topicTrackCmnd[] PROGMEM = "Cmnd/Tonuino/Track";
static const char topicTrackState[] PROGMEM = "State/Tonuino/Track";
static const char topicTrackControlCmnd[] PROGMEM = "Cmnd/Tonuino/TrackControl";
static const char topicLoudnessCmnd[] PROGMEM = "Cmnd/Tonuino/Loudness";
static const char topicLoudnessState[] PROGMEM = "State/Tonuino/Loudness";
static const char topicSleepTimerCmnd[] PROGMEM = "Cmnd/Tonuino/SleepTimer";
static const char topicSleepTimerState[] PROGMEM = "State/Tonuino/SleepTimer";
static const char topicState[] PROGMEM = "State/Tonuino/State";
static const char topicCurrentIPv4IP[] PROGMEM = "State/Tonuino/IPv4";
static const char topicLockControlsCmnd[] PROGMEM ="Cmnd/Tonuino/LockControls";
static const char topicLockControlsState[] PROGMEM ="State/Tonuino/LockControls";
static const char topicPlaymodeState[] PROGMEM = "State/Tonuino/Playmode";
static const char topicRepeatModeCmnd[] PROGMEM = "Cmnd/Tonuino/RepeatMode";
static const char topicRepeatModeState[] PROGMEM = "State/Tonuino/RepeatMode";
static const char topicLedBrightnessCmnd[] PROGMEM = "Cmnd/Tonuino/LedBrightness";
static const char topicLedBrightnessState[] PROGMEM = "State/Tonuino/LedBrightness";
static const char topicBatteryVoltage[] PROGMEM = "State/Tonuino/Voltage";
#endif
char stringDelimiter[] = "#"; // Character used to encapsulate data in linear NVS-strings (don't change)
char stringOuterDelimiter[] = "^"; // Character used to encapsulate encapsulated data along with RFID-ID in backup-file
@ -523,10 +406,213 @@ 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) {
float factor = 1 / ((float) r1/(r1+r2));
float factor = 1 / ((float) rdiv2/(rdiv2+rdiv1));
return ((float) analogRead(VOLTAGE_READ_PIN) / maxAnalogValue) * refVoltage * factor;
}
@ -2792,6 +2878,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 +3181,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 +3214,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 +3253,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);
}
if (info->opcode == WS_TEXT) {
data[len] = 0;
@ -3251,7 +3353,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 +3830,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 +3878,7 @@ void loop() {
#ifdef PLAY_LAST_RFID_AFTER_REBOOT
recoverLastRfidPlayed();
#endif
ws.cleanupClients();
}

153
src/settings.h

@ -0,0 +1,153 @@
#include "Arduino.h"
//########################## MODULES #################################
#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd
#define FTP_ENABLE // Enables FTP-server
#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 LANGUAGE 1 // 1 = deutsch; 2 = english
//#define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone)
#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 PLAY_LAST_RFID_AFTER_REBOOT // When restarting Tonuino, the last RFID that was active before, is recalled and played
//#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!)
//#define SD_NOT_MANDATORY_ENABLE // Only for debugging-purposes: Tonuino will also start without mounted SD-card anyway (will only try once to mount it). Will overwrite SHUTDOWN_IF_SD_BOOT_FAILS!
//#define BLUETOOTH_ENABLE // Doesn't work currently (so don't enable) as there's not enough DRAM available
//################## GPIO-configuration ##############################
// uSD-card-reader (via SPI)
#define SPISD_CS 15 // GPIO for chip select (SD)
#ifndef SINGLE_SPI_ENABLE
#define SPISD_MOSI 13 // GPIO for master out slave in (SD) => not necessary for single-SPI
#define SPISD_MISO 16 // GPIO for master in slave ou (SD) => not necessary for single-SPI
#define SPISD_SCK 14 // GPIO for clock-signal (SD) => not necessary for single-SPI
#endif
// RFID (via SPI)
#define RST_PIN 99 // Not necessary but has to be set anyway; so let's use a dummy-number
#define RFID_CS 21 // GPIO for chip select (RFID)
#define RFID_MOSI 23 // GPIO for master out slave in (RFID)
#define RFID_MISO 19 // GPIO for master in slave out (RFID)
#define RFID_SCK 18 // GPIO for clock-signal (RFID)
// I2S (DAC)
#define I2S_DOUT 25 // Digital out (I2S)
#define I2S_BCLK 27 // BCLK (I2S)
#define I2S_LRC 26 // LRC (I2S)
// Rotary encoder
#define DREHENCODER_CLK 34 // If you want to reverse encoder's direction, just switch GPIOs of CLK with DT (in software or hardware)
#define DREHENCODER_DT 35 // Info: Lolin D32 / Lolin D32 pro 35 are using 35 for battery-voltage-monitoring!
#define DREHENCODER_BUTTON 32 // Button is used to switch Tonuino on and off
// Control-buttons
#define PAUSEPLAY_BUTTON 5 // GPIO to detect pause/play
#define NEXT_BUTTON 4 // GPIO to detect next
#define PREVIOUS_BUTTON 2 // GPIO to detect previous (Important: as of 19.11.2020 changed from 33 to 2)
// (optional) Power-control
#define POWER 17 // GPIO used to drive transistor-circuit, that switches off peripheral devices while ESP32-deepsleep
// (optional) Neopixel
#define LED_PIN 12 // GPIO for Neopixel-signaling
// (optinal) Headphone-detection
#ifdef HEADPHONE_ADJUST_ENABLE
#define HP_DETECT 22 // GPIO that detects, if there's a plug in the headphone jack or not
#endif
// (optional) Monitoring of battery-voltage via ADC
#ifdef MEASURE_BATTERY_VOLTAGE
#define VOLTAGE_READ_PIN 33 // GPIO used to monitor battery-voltage. Change to 35 if you're using Lolin D32 or Lolin D32 pro as it's hard-wired there!
#endif
//#################### Various settings ##############################
// Loglevels available (don't change!)
#define LOGLEVEL_ERROR 1 // only errors
#define LOGLEVEL_NOTICE 2 // errors + important messages
#define LOGLEVEL_INFO 3 // infos + errors + important messages
#define LOGLEVEL_DEBUG 4 // almost everything
// Serial-logging-configuration
const uint8_t serialDebug = LOGLEVEL_DEBUG; // Current loglevel for serial console
// Buttons (better leave unchanged if in doubts :-))
uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons
uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of previous/next-button
// RFID
#define RFID_SCAN_INTERVAL 300 // Interval-time in ms (how often is RFID read?)
// Automatic restart
#ifdef SHUTDOWN_IF_SD_BOOT_FAILS
uint32_t deepsleepTimeAfterBootFails = 20; // Automatic restart takes place if boot was not successful after this period (in seconds)
#endif
// FTP
// Nothing to be configured here...
// Default user/password is esp32/esp32 but can be changed via webgui
// Tonuino will create a WiFi if joing existing WiFi was not possible. Name can be configured here.
static const char accessPointNetworkSSID[] PROGMEM = "Tonuino"; // Access-point's SSID
// Where to store the backup-file for NVS-records
#ifndef SD_NOT_MANDATORY_ENABLE
static const char backupFile[] PROGMEM = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done
#endif
// (webgui) File Browser
uint8_t FS_DEPTH = 5; // Max. recursion-depth of file tree
const char *DIRECTORY_INDEX_FILE = "/files.json"; // Filename of files.json index file
// (optinal) Neopixel
#ifdef NEOPIXEL_ENABLE
#define NUM_LEDS 24 // number of LEDs
#define CHIPSET WS2812B // type of Neopixel
#define COLOR_ORDER GRB
#endif
// (optional) Default-voltages for battery-monitoring via Neopixel
float warningLowVoltage = 3.4; // If battery-voltage is >= this value, a cyclic warning will be indicated by Neopixel (can be changed via GUI!)
uint8_t voltageCheckInterval = 10; // How of battery-voltage is measured (in minutes) (can be changed via GUI!)
float voltageIndicatorLow = 3.0; // Lower range for Neopixel-voltage-indication (0 leds) (can be changed via GUI!)
float voltageIndicatorHigh = 4.2; // Upper range for Neopixel-voltage-indication (all leds) (can be changed via GUI!)
// (optinal) For measuring battery-voltage a voltage-divider is necessary. Their values need to be configured here.
#ifdef MEASURE_BATTERY_VOLTAGE
uint8_t rdiv1 = 129; // Rdiv1 of voltage-divider (kOhms) (measure exact value with multimeter!)
uint16_t rdiv2 = 389; // Rdiv2 of voltage-divider (kOhms) (measure exact value with multimeter!) => used to measure voltage via ADC!
#endif
// (optinal) Headphone-detection (leave unchanged if in doubts...)
#ifdef HEADPHONE_ADJUST_ENABLE
uint16_t headphoneLastDetectionDebounce = 1000; // Debounce-interval in ms when plugging in headphone
#endif
// (optional) Topics for MQTT
#ifdef MQTT_ENABLE
#define DEVICE_HOSTNAME "ESP32-Tonuino-Leonie" // Name that that is used for MQTT
static const char topicSleepCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/Sleep";
static const char topicSleepState[] PROGMEM = "State/Tonuino-Leonie/Sleep";
static const char topicTrackCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/Track";
static const char topicTrackState[] PROGMEM = "State/Tonuino-Leonie/Track";
static const char topicTrackControlCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/TrackControl";
static const char topicLoudnessCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/Loudness";
static const char topicLoudnessState[] PROGMEM = "State/Tonuino-Leonie/Loudness";
static const char topicSleepTimerCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/SleepTimer";
static const char topicSleepTimerState[] PROGMEM = "State/Tonuino-Leonie/SleepTimer";
static const char topicState[] PROGMEM = "State/Tonuino-Leonie/State";
static const char topicCurrentIPv4IP[] PROGMEM = "State/Tonuino-Leonie/IPv4";
static const char topicLockControlsCmnd[] PROGMEM ="Cmnd/Tonuino-Leonie/LockControls";
static const char topicLockControlsState[] PROGMEM ="State/Tonuino-Leonie/LockControls";
static const char topicPlaymodeState[] PROGMEM = "State/Tonuino-Leonie/Playmode";
static const char topicRepeatModeCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/RepeatMode";
static const char topicRepeatModeState[] PROGMEM = "State/Tonuino-Leonie/RepeatMode";
static const char topicLedBrightnessCmnd[] PROGMEM = "Cmnd/Tonuino-Leonie/LedBrightness";
static const char topicLedBrightnessState[] PROGMEM = "State/Tonuino-Leonie/LedBrightness";
static const char topicBatteryVoltage[] PROGMEM = "State/Tonuino-Leonie/Voltage";
#endif

380
src/websiteMgmt.h

@ -5,17 +5,81 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<meta charset=\"utf-8\">\
<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://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://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/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;\
height: 200px;\
margin: 0em 0em 1em 0em;\
overflow-y: scroll;\
}\
\
.fa-sync:hover {\
color: #666666;\
}\
\
.clickForRefresh {\
text-align: center;\
color: gray;\
cursor: pointer;\
margin-top: 5em;\
}\
\
.clickForRefresh:hover {\
color: darkgray;\
}\
\
.filetree-container {\
position: relative;\
}\
\
.indexing-progress {\
width: 100%%;\
height: 100%%;\
position: absolute;\
top: 0;\
left: 0;\
opacity: 0.7;\
display: none;\
}\
\
.refreshAction{\
text-align: right;\
font-size: 0.8em;\
}\
\
.refreshAction:hover{\
cursor: pointer;\
color: darkgray;\
}\
\
.overlay {\
z-index: 9;\
opacity: 0.8;\
background: #1a1919;\
height: 200px;\
display: none;\
width: 100%%;\
}\
</style>\
</head>\
<body>\
<nav class=\"navbar navbar-expand-sm bg-primary navbar-dark\">\
<button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\
<button class=\"navbar-toggler\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\"\
aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\
<span class=\"navbar-toggler-icon\"></span>\
</button>\
<a class=\"navbar-brand\">\
<img src=\"https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/html/tonuino_logo.png\" width=\"30\" height=\"30\" class=\"d-inline-block align-top\" alt=\"\" />\
<img src=\"https://raw.githubusercontent.com/biologist79/Tonuino-ESP32-I2S/master/html/tonuino_logo.png\"\
width=\"30\" height=\"30\" class=\"d-inline-block align-top\" alt=\"\"/>\
Tonuino\
</a>\
<div class=\"collapse navbar-collapse\" id=\"collapsibleNavbar\">\
@ -60,7 +124,8 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<label for=\"pwd\">Passwort:</label>\
<input type=\"password\" class=\"form-control\" id=\"pwd\" placeholder=\"Passwort\" name=\"pwd\" required>\
<label for=\"hostname\">Tonuino-Name (Hostname):</label>\
<input type=\"text\" class=\"form-control\" id=\"hostname\" placeholder=\"tonuino\" name=\"hostname\" value=\"%HOSTNAME%\" pattern=\"^[^-\\.]{2,32}\" required>\
<input type=\"text\" class=\"form-control\" id=\"hostname\" placeholder=\"tonuino\" name=\"hostname\"\
value=\"%HOSTNAME%\" pattern=\"^[^-\\.]{2,32}\" required>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\">Absenden</button>\
@ -72,20 +137,35 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<form action=\"#rfidMusicTags\" method=\"POST\" onsubmit=\"rfidAssign('rfidMusicTags'); return false\">\
<div class=\"form-group col-md-6\">\
<label for=\"rfidIdMusic\">RFID-Chip-Nummer (12-stellig)</label>\
<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>\
<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 class=\"filetree-container\">\
<div id=\"filebrowser\">\
<div class=\"filetree demo\" id=\"filetree\"></div>\
<div class=\"refreshAction\">\
<span id=\"refreshAction\" data-toggle=\"tooltip\" data-placement=\"top\" title=\"Datei Liste aktualisieren.\"><i class=\"fas fa-sync fa-1x\"></i> Dateiliste aktualisieren</span>\
</div>\
</div>\
<div id=\"indexing-progress\" class=\"indexing-progress overlay\">\
<div style=\"text-align: center; color:white; margin-top:2em;\">\
<div><i class=\"fas fa-sync fa-spin fa-2x\"></i> <br><br> Der Prozess kann mehrere Minuten dauern...</div>\
<div id=\"currentProcessedFile\"></div>\
</div>\
</div>\
</div>\
<label for=\"playMode\">Abspielmodus</label>\
<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-file-and-folder\" value=\"3\">Hörbuch</option>\
<option class=\"option-file-and-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>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
@ -98,7 +178,8 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<form class=\"needs-validation\" action=\"#rfidModTags\" method=\"POST\" onsubmit=\"rfidMods('rfidModTags'); return false\">\
<div class=\"form-group col-md-6\">\
<label for=\"rfidIdMod\">RFID-Chip-Nummer (12-stellig)</label>\
<input type=\"text\" class=\"form-control\" id=\"rfidIdMod\" maxlength=\"12\" pattern=\"[0-9]{12}\" placeholder=\"%RFID_TAG_ID%\" name=\"rfidIdMod\" required>\
<input type=\"text\" class=\"form-control\" id=\"rfidIdMod\" maxlength=\"12\" pattern=\"[0-9]{12}\"\
placeholder=\"%RFID_TAG_ID%\" name=\"rfidIdMod\" required>\
<div class=\"invalid-feedback\">\
Bitte eine 12-stellige Zahl eingeben.\
</div>\
@ -125,7 +206,8 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
</div>\
<div class=\"container my-5\" id=\"mqttConfig\">\
<h2>MQTT-Konfiguration</h2>\
<form class=\"needs-validation\" action=\"#mqttConfig\" method=\"POST\" onsubmit=\"mqttSettings('mqttConfig'); return false\">\
<form class=\"needs-validation\" action=\"#mqttConfig\" method=\"POST\"\
onsubmit=\"mqttSettings('mqttConfig'); return false\">\
<div class=\"form-check col-md-6\">\
<input class=\"form-check-input\" type=\"checkbox\" value=\"1\" id=\"mqttEnable\" name=\"mqttEnable\" %MQTT_ENABLE%>\
<label class=\"form-check-label\" for=\"mqttEnable\">\
@ -134,11 +216,14 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
</div>\
<div class=\"form-group my-2 col-md-6\">\
<label for=\"mqttServer\">MQTT-Server</label>\
<input type=\"text\" class=\"form-control\" id=\"mqttServer\" minlength=\"7\" maxlength=\"%MQTT_SERVER_LENGTH%\" placeholder=\"z.B. 192.168.2.89\" name=\"mqttServer\" value=\"%MQTT_SERVER%\">\
<input type=\"text\" class=\"form-control\" id=\"mqttServer\" minlength=\"7\" maxlength=\"%MQTT_SERVER_LENGTH%\"\
placeholder=\"z.B. 192.168.2.89\" name=\"mqttServer\" value=\"%MQTT_SERVER%\">\
<label for=\"mqttUser\">MQTT-Benutzername (optional):</label>\
<input type=\"text\" class=\"form-control\" id=\"mqttUser\" maxlength=\"%MQTT_USER_LENGTH%\" placeholder=\"Benutzername\" name=\"mqttUser\" value=\"%MQTT_USER%\">\
<input type=\"text\" class=\"form-control\" id=\"mqttUser\" maxlength=\"%MQTT_USER_LENGTH%\"\
placeholder=\"Benutzername\" name=\"mqttUser\" value=\"%MQTT_USER%\">\
<label for=\"mqttPwd\">Passwort (optional):</label>\
<input type=\"password\" class=\"form-control\" id=\"mqttPwd\" maxlength=\"%MQTT_PWD_LENGTH%\" placeholder=\"Passwort\" name=\"mqttPwd\" value=\"%MQTT_PWD%\">\
<input type=\"password\" class=\"form-control\" id=\"mqttPwd\" maxlength=\"%MQTT_PWD_LENGTH%\"\
placeholder=\"Passwort\" name=\"mqttPwd\" value=\"%MQTT_PWD%\">\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\">Absenden</button>\
@ -150,9 +235,11 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<form action=\"#ftpConfig\" method=\"POST\" onsubmit=\"ftpSettings('ftpConfig'); return false\">\
<div class=\"form-group col-md-6\">\
<label for=\"ftpUser\">FTP-Benutzername:</label>\
<input type=\"text\" class=\"form-control\" id=\"ftpUser\" maxlength=\"%FTP_USER_LENGTH%\" placeholder=\"Benutzername\" name=\"ftpUser\" value=\"%FTP_USER%\" required>\
<input type=\"text\" class=\"form-control\" id=\"ftpUser\" maxlength=\"%FTP_USER_LENGTH%\"\
placeholder=\"Benutzername\" name=\"ftpUser\" value=\"%FTP_USER%\" required>\
<label for=\"pwd\">Passwort:</label>\
<input type=\"password\" class=\"form-control\" id=\"ftpPwd\" maxlength=\"%FTP_PWD_LENGTH%\" placeholder=\"Passwort\" name=\"ftpPwd\" value=\"%FTP_PWD%\" required>\
<input type=\"password\" class=\"form-control\" id=\"ftpPwd\" maxlength=\"%FTP_PWD_LENGTH%\" placeholder=\"Passwort\"\
name=\"ftpPwd\" value=\"%FTP_PWD%\" required>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\">Absenden</button>\
@ -164,37 +251,50 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
<form action=\"#generalConfig\" method=\"POST\" onsubmit=\"genSettings('generalConfig'); return false\">\
<div class=\"form-group col-md-6\">\
<label for=\"initialVolume\">Lautstärke nach dem Einschalten</label>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"initialVolume\" name=\"initialVolume\" value=\"%INIT_VOLUME%\" required>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"initialVolume\" name=\"initialVolume\"\
value=\"%INIT_VOLUME%\" required>\
<label for=\"maxVolumeSpeaker\">Maximale Lautstärke (Lautsprecher)</label>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"maxVolumeSpeaker\" name=\"maxVolumeSpeaker\" value=\"%MAX_VOLUME_SPEAKER%\" required>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"maxVolumeSpeaker\" name=\"maxVolumeSpeaker\"\
value=\"%MAX_VOLUME_SPEAKER%\" required>\
<label for=\"maxVolumeHeadphone\">Maximale Lautstärke (Kopfhörer)</label>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"maxVolumeHeadphone\" name=\"maxVolumeHeadphone\" value=\"%MAX_VOLUME_HEADPHONE%\" required>\
<input type=\"number\" min=\"1\" max=\"21\" class=\"form-control\" id=\"maxVolumeHeadphone\" name=\"maxVolumeHeadphone\"\
value=\"%MAX_VOLUME_HEADPHONE%\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"initBrightness\">Neopixel-Helligkeit nach dem Einschalten</label>\
<input type=\"number\" min=\"0\" max=\"255\" class=\"form-control\" id=\"initBrightness\" name=\"initBrightness\" value=\"%INIT_LED_BRIGHTNESS%\" required>\
<input type=\"number\" min=\"0\" max=\"255\" class=\"form-control\" id=\"initBrightness\" name=\"initBrightness\"\
value=\"%INIT_LED_BRIGHTNESS%\" required>\
<label for=\"nightBrightness\">Neopixel-Helligkeit im Nachtmodus</label>\
<input type=\"number\" min=\"0\" max=\"255\" class=\"form-control\" id=\"nightBrightness\" name=\"nightBrightness\" value=\"%NIGHT_LED_BRIGHTNESS%\" required>\
<input type=\"number\" min=\"0\" max=\"255\" class=\"form-control\" id=\"nightBrightness\" name=\"nightBrightness\"\
value=\"%NIGHT_LED_BRIGHTNESS%\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"inactivityTime\">Deep-Sleep nach Inaktivität (Minuten)</label>\
<input type=\"number\" min=\"1\" max=\"1440\" class=\"form-control\" id=\"inactivityTime\" name=\"inactivityTime\" value=\"%MAX_INACTIVITY%\" required>\
<input type=\"number\" min=\"1\" max=\"1440\" class=\"form-control\" id=\"inactivityTime\" name=\"inactivityTime\"\
value=\"%MAX_INACTIVITY%\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"warningLowVoltage\">(Optional) Spannungsgrenze (Batterie), ab der Warnung auf Neopixel angezeigt wird (z.B. 3.4)</label>\
<input type=\"text\" class=\"form-control\" id=\"warningLowVoltage\" name=\"warningLowVoltage\" value=\"%WARNING_LOW_VOLTAGE%\" pattern=\"^\\d{1,2}(\\.\\d{1,3})?\" required>\
<label for=\"warningLowVoltage\">(Optional) Spannungsgrenze (Batterie), ab der Warnung auf Neopixel angezeigt\
wird (z.B. 3.4)</label>\
<input type=\"text\" class=\"form-control\" id=\"warningLowVoltage\" name=\"warningLowVoltage\"\
value=\"%WARNING_LOW_VOLTAGE%\" pattern=\"^\\d{1,2}(\\.\\d{1,3})?\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"voltageIndicatorLow\">(Optional) Unterer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B. 3.1)</label>\
<input type=\"text\" class=\"form-control\" id=\"voltageIndicatorLow\" name=\"voltageIndicatorLow\" value=\"%VOLTAGE_INDICATOR_LOW%\" pattern=\"^\\d{1,2}(\\.\\d{1,3})?\" required>\
<label for=\"voltageIndicatorLow\">(Optional) Unterer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B.\
3.1)</label>\
<input type=\"text\" class=\"form-control\" id=\"voltageIndicatorLow\" name=\"voltageIndicatorLow\"\
value=\"%VOLTAGE_INDICATOR_LOW%\" pattern=\"^\\d{1,2}(\\.\\d{1,3})?\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"voltageIndicatorHigh\">(Optional) Oberer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B. 4.2)</label>\
<input type=\"text\" class=\"form-control\" id=\"voltageIndicatorHigh\" name=\"voltageIndicatorHigh\" value=\"%VOLTAGE_INDICATOR_HIGH%\" pattern=\"^\\d{1,2}(\\.\\d{1,3})?\" required>\
<label for=\"voltageIndicatorHigh\">(Optional) Oberer Akkuspannungslevel (Batterie) für Neopixel-Anzeige (z.B.\
4.2)</label>\
<input type=\"text\" class=\"form-control\" id=\"voltageIndicatorHigh\" name=\"voltageIndicatorHigh\"\
value=\"%VOLTAGE_INDICATOR_HIGH%\" pattern=\"^\\d{1,2}(\\.\\d{1,3})?\" required>\
</div>\
<div class=\"form-group col-md-6\">\
<label for=\"voltageCheckInterval\">(Optional) Häufigkeit der Spannungsmessung (Batterie) in Minuten</label>\
<input type=\"number\" min=\"1\" max=\"180\" class=\"form-control\" id=\"voltageCheckInterval\" name=\"voltageCheckInterval\" value=\"%VOLTAGE_CHECK_INTERVAL%\" required>\
<input type=\"number\" min=\"1\" max=\"180\" class=\"form-control\" id=\"voltageCheckInterval\"\
name=\"voltageCheckInterval\" value=\"%VOLTAGE_CHECK_INTERVAL%\" required>\
</div>\
<button type=\"reset\" class=\"btn btn-secondary\">Reset</button>\
<button type=\"submit\" class=\"btn btn-primary\">Absenden</button>\
@ -212,33 +312,144 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
</form>\
<div class=\"messages col-md-6 my-2\"></div>\
</div>\
<script>\
<script type=\"text/javascript\">\
var DEBUG = false;\
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 host = $(location).attr('hostname');\
\
if (DEBUG) {\
host = \"192.168.178.112\";\
}\
\
var socket = new WebSocket(\"ws://%IPv4%/ws\");\
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 connect() {\
socket = new WebSocket(\"ws://%IPv4%/ws\");\
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 ping() {\
var myObj = {\
\"ping\": {\
ping: 'ping'\
function renderFileTree() {\
\
var filesURI = \"/files\";\
if (DEBUG) {\
filesURI = \"http://\" + host + \"/files\";\
}\
};\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
tm = setTimeout(function () {\
alert(\"Die Verbindung zum Tonuino ist unterbrochen!\\nBitte Seite neu laden.\");\
}, 5000);\
$('#filetree').jstree({\
'core': {\
'check_callback': true,\
'data': {\
url: filesURI,\
error: function (XMLHttpRequest, textStatus, errorThrown) {\
$('#j1_loading').hide();\
$(\"#refreshAction\").hide();\
$('#filetree').html(\"<div class='clickForRefresh' ><i class='fas fa-sync fa-1x'><span id='#clickForRefresh' > Dateien suchen.</span></div>\");\
$('#filetree').on(\"click\", function () {\
refreshFileList();\
});\
toastr.error(\"Die Verzeichnis-Liste konnte nicht geladen werden.\");\
}\
},\
\
function pong() {\
clearTimeout(tm);\
},\
'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);\
if ((data.instance._model.data['#']['children'].length == 0)) {\
toastr.info(\"Der SD-Karten-Index muss erzeugt werden.\");\
}\
}).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\") {\
$('.option-folder').show();\
$('.option-file').hide();\
$('#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();\
$(\"#indexing-progress\").show();\
$(\"#refreshAction\").hide();\
});\
\
$('#playMode').on(\"change\", function () {\
if (this.value == 8) {\
$('#filebrowser').slideUp();\
} else {\
$('#filebrowser').slideDown();\
}\
});\
\
function showFileIndexingState() {\
$(\"#indexing-progress\").show();\
$(\"#refreshAction\").hide();\
}\
\
function hideFileIndexingState() {\
$(\"#indexing-progress\").hide();\
$(\"#refreshAction\").show();\
}\
\
var socket = undefined;\
var tm;\
\
function connect() {\
socket = new WebSocket(\"ws://\" + host + \"/ws\");\
\
socket.onopen = function () {\
setInterval(ping, 15000);\
@ -246,6 +457,7 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
\
socket.onclose = function (e) {\
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);\
socket = null;\
setTimeout(function () {\
connect();\
}, 5000);\
@ -253,7 +465,6 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
\
socket.onerror = function (err) {\
console.error('Socket encountered error: ', err.message, 'Closing socket');\
socket.close();\
};\
\
socket.onmessage = function(event) {\
@ -262,20 +473,60 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
if (socketMsg.rfidId != null) {\
document.getElementById('rfidIdMod').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.\" );\
\
} if (socketMsg.status != null) {\
if (socketMsg.status == 'ok') {\
$(\"#\" + lastIdclicked).find('.messages').html(okBox);\
} else {\
$(\"#\" + lastIdclicked).find('.messages').html(errorBox);\
$(\"#rfidIdMusic\").effect(\"highlight\", {color:\"#abf5af\"}, 3000);\
$(\"#rfidIdMod\").effect(\"highlight\", {color:\"#abf5af\"}, 3000);\
\
} if (\"status\" in socketMsg) {\
if (socketMsg.status == \"ok\") {\
toastr.success(\"Aktion erfolgreich ausgeführt.\" );\
}\
} if (socketMsg.pong != null) {\
} if (\"pong\" in socketMsg) {\
if (socketMsg.pong == 'pong') {\
pong();\
}\
} if (\"refreshFileList\" in socketMsg) {\
hideFileIndexingState();\
toastr.info(\"Die Dateiliste wurde neu erzeugt!\");\
$('#filetree').jstree(true).refresh();\
\
}\
if (\"indexingState\" in socketMsg) {\
if(socketMsg.indexingState != null) {\
$(\"#currentProcessedFile\").text(socketMsg.indexingState);\
console.log(socketMsg.indexingState);\
}\
}\
};\
}\
\
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);\
$(\"#refreshAction\").hide();\
socket.send(myJSON);\
showFileIndexingState();\
};\
\
function genSettings(clickedId) {\
@ -374,6 +625,15 @@ static const char mgtWebsite[] PROGMEM = "<!DOCTYPE html>\
var myJSON = JSON.stringify(myObj);\
socket.send(myJSON);\
}\
\
$(document).ready(function () {\
connect();\
renderFileTree();\
\
$(function () {\
$('[data-toggle=\"tooltip\"]').tooltip();\
});\
});\
</script>\
</body>\
</html>\
Loading…
Cancel
Save