diff --git a/.gitignore b/.gitignore index 61441f2..24e6ac3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ CMakeLists.txt CMakeListsPrivate.txt cmake-build-az-delivery-devkit-v4/ cmake-build-debug/ -venv/ diff --git a/Hardware-Plaforms/Wemos Lolin32/README.md b/Hardware-Plaforms/Wemos Lolin32/README.md new file mode 100644 index 0000000..b914a09 --- /dev/null +++ b/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. diff --git a/PCBs/Wemos Lolin32/Gerber/gerber.zip b/PCBs/Wemos Lolin32/Gerber/gerber.zip new file mode 100644 index 0000000..66b7de8 Binary files /dev/null and b/PCBs/Wemos Lolin32/Gerber/gerber.zip differ diff --git a/PCBs/Wemos Lolin32/KiCad/Kicad-files.zip b/PCBs/Wemos Lolin32/KiCad/Kicad-files.zip new file mode 100644 index 0000000..9bec5f1 Binary files /dev/null and b/PCBs/Wemos Lolin32/KiCad/Kicad-files.zip differ diff --git a/PCBs/Wemos Lolin32/Pictures/3d-Model downside.jpg b/PCBs/Wemos Lolin32/Pictures/3d-Model downside.jpg new file mode 100644 index 0000000..7181ccd Binary files /dev/null and b/PCBs/Wemos Lolin32/Pictures/3d-Model downside.jpg differ diff --git a/PCBs/Wemos Lolin32/Pictures/3d-Model upside.jpg b/PCBs/Wemos Lolin32/Pictures/3d-Model upside.jpg new file mode 100644 index 0000000..bce0208 Binary files /dev/null and b/PCBs/Wemos Lolin32/Pictures/3d-Model upside.jpg differ diff --git a/PCBs/Wemos Lolin32/Pictures/Tonuino-Lolin32-Schematics.pdf b/PCBs/Wemos Lolin32/Pictures/Tonuino-Lolin32-Schematics.pdf new file mode 100644 index 0000000..5c1001d Binary files /dev/null and b/PCBs/Wemos Lolin32/Pictures/Tonuino-Lolin32-Schematics.pdf differ diff --git a/PCBs/Wemos Lolin32/README.md b/PCBs/Wemos Lolin32/README.md new file mode 100644 index 0000000..b914a09 --- /dev/null +++ b/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. diff --git a/README.md b/README.md index 4598f18..a420b00 100644 --- a/README.md +++ b/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.
+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.
-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.
+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.
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: +Webgui #6: + + Webgui: websocket broken: Webgui: action ok: -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. diff --git a/html/website.html b/html/website.html index fb10745..5fd60a9 100644 --- a/html/website.html +++ b/html/website.html @@ -1,287 +1,538 @@ - + ESPuino-Konfiguration + + + + - - - -
-
-

WLAN-Konfiguration

-
+
+ +
+
+

WLAN-Konfiguration

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

RFID-Zuweisungen

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

RFID-Modifkationen

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

RFID-Zuweisungen

+
+
+ + + + +
+
+
+
+ Dateiliste aktualisieren +
+
+
+
+


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

MQTT-Konfiguration

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

RFID-Modifkationen

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

FTP-Konfiguration

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

MQTT-Konfiguration

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

FTP-Konfiguration

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

Allgemeine Konfiguration

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

NVS-Importer

-
-
+ +
+ + +
+
+
+
+

Allgemeine Konfiguration

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

NVS-Importer

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

WLAN-Konfiguration

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

RFID-Zuweisungen

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

RFID-Modifkationen

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

MQTT-Konfiguration

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

FTP-Konfiguration

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

Allgemeine Konfiguration

\ -
\ -
\ - \ - \ - \ - \ -
\ -
\ - \ - \ - \ - \ -
\ -
\ - \ - \ -
\ - \ - \ -
\ - \ -
\ - \ -\ -"; \ No newline at end of file diff --git a/pictures/Mgmt-GUI1.jpg b/pictures/Mgmt-GUI1.jpg index 5f87a3f..483906f 100644 Binary files a/pictures/Mgmt-GUI1.jpg and b/pictures/Mgmt-GUI1.jpg differ diff --git a/pictures/Mgmt-GUI2.jpg b/pictures/Mgmt-GUI2.jpg index 50a8da3..ee3cb6a 100644 Binary files a/pictures/Mgmt-GUI2.jpg and b/pictures/Mgmt-GUI2.jpg differ diff --git a/pictures/Mgmt-GUI3.jpg b/pictures/Mgmt-GUI3.jpg index b365774..56b4d07 100644 Binary files a/pictures/Mgmt-GUI3.jpg and b/pictures/Mgmt-GUI3.jpg differ diff --git a/pictures/Mgmt-GUI4.jpg b/pictures/Mgmt-GUI4.jpg index bdb2732..8fda942 100644 Binary files a/pictures/Mgmt-GUI4.jpg and b/pictures/Mgmt-GUI4.jpg differ diff --git a/pictures/Mgmt-GUI5.jpg b/pictures/Mgmt-GUI5.jpg index 40c2b2d..e21e08f 100644 Binary files a/pictures/Mgmt-GUI5.jpg and b/pictures/Mgmt-GUI5.jpg differ diff --git a/pictures/Mgmt-GUI6.jpg b/pictures/Mgmt-GUI6.jpg new file mode 100644 index 0000000..863ba15 Binary files /dev/null and b/pictures/Mgmt-GUI6.jpg differ diff --git a/pictures/Mgmt-GUI_connection_broken.jpg b/pictures/Mgmt-GUI_connection_broken.jpg index bd26541..75c7101 100644 Binary files a/pictures/Mgmt-GUI_connection_broken.jpg and b/pictures/Mgmt-GUI_connection_broken.jpg differ diff --git a/pictures/Mgmt_GUI_action_ok.jpg b/pictures/Mgmt_GUI_action_ok.jpg index 3b5cc21..e4e3e0c 100644 Binary files a/pictures/Mgmt_GUI_action_ok.jpg and b/pictures/Mgmt_GUI_action_ok.jpg differ diff --git a/platformio.ini b/platformio.ini index d665438..b8911b9 100644 --- a/platformio.ini +++ b/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 \ No newline at end of file +extra_scripts = + pre:processHtml.py \ No newline at end of file diff --git a/processHtml.py b/processHtml.py index 1e9dd5f..471c6c3 100644 --- a/processHtml.py +++ b/processHtml.py @@ -1,16 +1,20 @@ #!/usr/bin/python +import re content = '' content2 = '' contentEN = '' content2EN = '' +# TODO: Add a JS Minifier python lib with open('html/website.html', 'r') as r: - data = r.read().replace('\n', '\\\n') + data = r.read() + data = data.replace('\n', '\\\n') data = data.replace('\"', '\\"') data = data.replace('\\d', '\\\d') data = data.replace('\\.', '\\\.') data = data.replace('\\^', '\\\\^') + data = data.replace('%;', '%%;') content += data with open('src/websiteMgmt.h', 'w') as w: diff --git a/src/main.cpp b/src/main.cpp index e01a2a7..7a43c10 100644 --- a/src/main.cpp +++ b/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 #include "Arduino.h" #include @@ -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); } - 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(); } diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..c5f852c --- /dev/null +++ b/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 diff --git a/src/websiteMgmt.h b/src/websiteMgmt.h index b89d00b..2c11616 100644 --- a/src/websiteMgmt.h +++ b/src/websiteMgmt.h @@ -1,287 +1,538 @@ static const char mgtWebsite[] PROGMEM = "\ \ - \ +\ ESPuino-Konfiguration\ \ \ \ + \ + \ + \ \ + \ \ \ - \ - \ - \ -
\ -
\ -

WLAN-Konfiguration

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

WLAN-Konfiguration

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

RFID-Zuweisungen

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

RFID-Modifkationen

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

RFID-Zuweisungen

\ +
\ +
\ + \ + \ + \ + \ +
\ +
\ +
\ +
\ + Dateiliste aktualisieren\ +
\ +
\ +
\ +
\ +


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

MQTT-Konfiguration

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

RFID-Modifkationen

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

FTP-Konfiguration

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

MQTT-Konfiguration

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

FTP-Konfiguration

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

Allgemeine Konfiguration

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

NVS-Importer

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

Allgemeine Konfiguration

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

NVS-Importer

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