diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..6647c181 --- /dev/null +++ b/.clang-format @@ -0,0 +1,41 @@ +--- +BasedOnStyle: WebKit +AlignArrayOfStructures: Right +AlignConsecutiveBitFields: + Enabled: true +AlignConsecutiveMacros: + Enabled: true +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: InlineOnly +BreakBeforeBraces: Attach +Cpp11BracedListStyle: true +FixNamespaceComments: true +IncludeBlocks: Regroup +IncludeCategories: + - Regex: Arduino.h + Priority: -1 + SortPriority: -2 + CaseSensitive: true + - Regex: settings.h + Priority: -1 + SortPriority: -1 + CaseSensitive: true + - Regex: ^" + Priority: 1 + SortPriority: 0 + CaseSensitive: false + - Regex: .* + Priority: 4 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: (_test)?$ +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +InsertBraces: true +InsertNewlineAtEOF: true +NamespaceIndentation: None +PointerAlignment: Right +SpaceAfterCStyleCast: true +Standard: Auto +TabWidth: 4 +UseTab: Always diff --git a/.editorconfig b/.editorconfig index c6992c98..a67ad0c4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true [*] # Loosely following the kernel coding style: https://www.kernel.org/doc/html/v4.10/process/coding-style.html charset = utf-8 end_of_line = lf -indent_size = 8 +indent_size = 4 indent_style = tab trim_trailing_whitespace = true insert_final_newline = true @@ -22,6 +22,9 @@ indent_size = unset indent_style = unset insert_final_newline = false +[*.md] +max_line_length = 100 + [platformio.ini] indent_size = 4 indent_style = space diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..ee0e974f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# Code re-formatting with clang +d1f82a7c93f160f9197c8b4e14e5a984ac6f9449 + +# Code re-formatting with editorconf +3a562b67e3ca0f98ed128650440323c3b2d5fc78 diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 00000000..c78c54fd --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +[blame] + ignoreRevsFile = .git-blame-ignore-revs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 9e21fbff..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Build all boards -on: - push: - branches: - - master - -jobs: - main: - name: Build all boards - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Setup Python - uses: actions/setup-python@master - with: - python-version: '3.x' - - name: Install Platform IO - run: | - python -m pip install --upgrade pip - pip3 install -U platformio - - name: Build - run: platformio run -e esp32-a1s -e lolin32 -e lolin_d32 -e lolin_d32_pro -e lolin_d32_pro_sdmmc_pe -e nodemcu-32s -e az-delivery-devkit-v4 -e ttgo_t8 -e complete diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml new file mode 100644 index 00000000..5079a26e --- /dev/null +++ b/.github/workflows/clang-format-check.yml @@ -0,0 +1,15 @@ +name: clang-format-check +run-name: Clang-format Check +on: [workflow_dispatch, push, pull_request] +jobs: + formatting-check: + name: Formatting Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run clang-format style check for C/C++/Protobuf programs. + uses: jidicula/clang-format-action@v4.11.0 + with: + clang-format-version: '16' + check-path: 'src' + fallback-style: 'LLVM' # optional \ No newline at end of file diff --git a/.github/workflows/test-builds.yml b/.github/workflows/test-builds.yml new file mode 100644 index 00000000..c297f61a --- /dev/null +++ b/.github/workflows/test-builds.yml @@ -0,0 +1,58 @@ +name: build-all +run-name: Build all boards +on: + workflow_dispatch: + push: + pull_request: + paths: + - 'src/*' + - 'html/*' + - 'test/*' + - '**.ini' + - '**.py' + - '**.csv' + +jobs: + test_builds: + name: Run all Tests + runs-on: ubuntu-latest + strategy: + matrix: + variant: + - esp32-a1s + - lolin32 + - lolin_d32 + - lolin_d32_pro + - lolin_d32_pro_sdmmc_pe + - nodemcu-32s + - az-delivery-devkit-v4 + - ttgo_t8 + - complete + + steps: + - uses: actions/checkout@v3 + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v3 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + + - name: Setup Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: '3.9' + - name: Install PlatformIO Core + run: | + pip install -U setuptools platformio + pio upgrade --dev + pio pkg update --global + - name: Build ${{ matrix.variant }} + run: platformio run -e ${{ matrix.variant }} diff --git a/.gitignore b/.gitignore index 8add9a2e..cbbd8e27 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,12 @@ venv/ .vscode/ipch .DS_Store .idea/ -CMakeLists.txt CMakeListsPrivate.txt cmake-build-az-delivery-devkit-v4/ cmake-build-debug/ platformio-override.ini +sdkconfig.* +!sdkconfig.defaults src/settings-override.h src/settings-custom-override.h +node_modules/ diff --git a/.vscode/settings.json b/.vscode/settings.json index cad7657d..b3d7f118 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "cmake.configureOnOpen": false -} \ No newline at end of file + "cmake.configureOnOpen": false, + "gitlens.advanced.blame.customArguments": [ + "--ignore-revs-file", ".git-blame-ignore-revs" + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..19a9fbd1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16.0) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ESPuino) diff --git a/README.md b/README.md index 264b5bd5..794b3d64 100644 --- a/README.md +++ b/README.md @@ -1,264 +1,518 @@ -# ESPuino - rfid-based musiccontroller based on ESP32 with I2S-DAC-support +# ESPuino - RFID-controlled Audio Player based on ESP32 with I2S-DAC Support + +![build workflow](https://github.com/biologist79/ESPuino/actions/workflows/test-builds.yml/badge.svg) ## Forum -* EN: I've set up a primarily German-speaking community with much documentation. Also an international corner for non-German-speakers is available at https://forum.espuino.de. Github-Login can be used there but it's not mandatory. -* DE: Ich habe ein primär deutschsprachiges Forum aufgesetzt, welches ich mit reichlich Doku versehen habe. Würde mich freuen, euch dort zu sehen: https://forum.espuino.de. Ihr könnt euch dort mit eurem Github-Login einloggen, jedoch auch "normal" anmelden. Dokumenation findet ihr insbesondere hier: https://forum.espuino.de/c/dokumentation/anleitungen/10 -## Build status -![build workflow](https://github.com/biologist79/ESPuino/actions/workflows/build.yml/badge.svg) - -## Changelog -Last three events: -* 08.01.2023: New feature: `PAUSE_ON_MIN_VOLUME`. Playback is paused automatically if volume reaches minVolume (usually 0). -* 18.10.2022: New playmode: pick random subdirectory of a given directory and play it's content alphabetic ordered -* 02.10.2022: ESPuino is now able to stream audio to external BT-devices. This is currently in testing. Big thanks to @tueddy for providing this feature! - -## Known bugs -* For ESPuinos making use of SPI to connect SD, there's an unsolved problem that occasionally leads to incomplete file-transfers via webtransfer or FTP-transger. Solution: use SD_MMC instead (by the way: it's faster and needs one GPIO less). + +- EN: I've set up a primarily German-speaking community with much documentation. Also an + international corner for non-German-speakers is available at . + GitHub login can be used for signing in there (optional). +- DE: Ich habe ein primär deutschsprachiges Forum aufgesetzt, welches ich mit reichlich Doku + versehen habe. Würde mich freuen, euch dort zu sehen: . Ihr könnt euch + dort mit eurem Github-Login einloggen, jedoch auch "normal" anmelden. Dokumentation findet ihr + insbesondere hier: . + +## News + +> :warning: By the end of october 2023, ESPuino switched framework from ESP32-Arduino1 to ESP32-Arduino2. +This brought lots of improvements but as it turned out, due to memory restrictions this version +no longer runs safely on ESP32 without PSRAM. So please make sure to use an ESP32-WROVER! + +## Current development + +There is a [development branch (dev)](https://github.com/biologist79/ESPuino/tree/dev) that contains +new features, that will be introduced and tested there first until they become part of the master +branch. Feel free to try but be advised this could be unstable. + ## ESPuino - what's that? -The basic idea of ESPuino is to use RFID-tags 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 stuff from SD or webradio. Place another RFID-object on it and anything else is played. Simple as that. - -This project is based on the popular microcontroller [ESP32 by Espressif](https://www.espressif.com/en/products/hardware/esp32/overview). Why? It's powerful and having WiFi-support out-of-the-box enables further features like an integrated webserver, smarthome-integration via MQTT, webradio and FTP-server. And nonetheless Bluetooth, too! However, my primary focus was to port the project to a modular base: mp3-decoding is done in software and the digital music-output is done via the popular [I2S-protocol](https://en.wikipedia.org/wiki/I%C2%B2S). So we need a [DAC](https://en.wikipedia.org/wiki/Digital-to-analog_converter) to make an analog signal of it: I did all my tests with [MAX98357A](https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp/pinouts), [UDA1334](https://www.adafruit.com/product/3678), [MS6324](https://forum.espuino.de/t/kopfhoererplatine-basierend-auf-ms6324-und-tda1308/1099/) and [PCM5102a](https://github.com/biologist79/ESPuino/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TDA1308). General advice: ESPuino makes use of library [ESP32-audioI2S](https://github.com/schreibfaul1/ESP32-audioI2S/); so everything that's supposed to work with this library should work with ESPuino, too (but maybe not right out of the box). Especially this is true for [ES8388](https://github.com/schreibfaul1/ESP32-audioI2S/blob/master/examples/ESP32_ES8388/ESP32_ES8388.ino). - -## Hardware-setup -You can start on a breadboard with jumper wires but I strongly recommend to start right away with a PCB like [ESPuino-mini](https://forum.espuino.de/t/espuino-minid32-pro-lolin-d32-d32-pro-mit-sd-mmc-und-port-expander-smd/866). Several PCBs are available. Please click on the links below for more informations and pictures. -* [Lolin D32 / Lolin D32 pro + PN5180/RC522 + port-expander (SMD)](https://forum.espuino.de/t/espuino-minid32-pro-lolin-d32-d32-pro-mit-sd-mmc-und-port-expander-smd/866) -* [Lolin32 + SD_MMC + PN5180/RC522 (THT)](https://forum.espuino.de/t/lolin32-mit-sd-sd-mmc-und-pn5180-als-rfid-leser/77/) -* [NodeMCU ESP32 + SD_MMC + PN5180/RC522 (THT)](https://forum.espuino.de/t/az-delivery-esp32-nodemcu-devkit-c-mit-sd-mmc-und-pn5180-als-rfid-leser/634) -* And some more... please have a look [here](https://forum.espuino.de/c/hardware/pcbs/11). -* All of these platforms are compatible with [headphone-pcb #1](https://forum.espuino.de/t/kopfhoererplatine-basierend-auf-ms6324-und-tda1308/1099/) and [headphone-pcb #2](https://forum.espuino.de/t/kopfhoererplatine-basierend-auf-uda1334-pj306b/705) and [headphone-pcb #3](https://github.com/biologist79/ESPuino/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TDA1308). - -## Getting Started -* [Much much documentation in german language](https://forum.espuino.de/c/dokumentation/anleitungen/10). -* You need to install Microsoft's [Visual Studio Code](https://code.visualstudio.com/). -* Install [Platformio Plugin](https://platformio.org/install/ide?install=vscode) into [Visual Studio Code](https://code.visualstudio.com/) and make sure to have a look at the [documentation](https://docs.platformio.org/en/latest/integration/ide/pioide.html). Step-by-step-manual is available [here](https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/.) -* Install [Git](https://git-scm.com/downloads) and make a copy ("clone") my repository to your local computer using `git clone https://github.com/biologist79/ESPuino.git`. Using git you can keep your local repository easily up to date without doing copy'n'paste. To keep it up to date run `git pull origin master`. Further infos [here](https://stackoverflow.com/questions/1443210/updating-a-local-repository-with-changes-from-a-github-repository) and [here](https://forum.espuino.de/t/espuino-in-platformio-anlegen-und-mit-git-aktuell-halten/891). -* (Optional) Install [Gitlens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) as plugin (to have advanced Git-support). -* Now, that the git-repository is saved locally, import this folder into Platformio as a project. -* Select the [desired environment](https://forum.espuino.de/t/projekt-und-profilwechsel-in-visual-studio-code/768) (e.g. lolin_d32_pro_sdmmc_pe). -* Edit `src/settings.h` according your needs. -* Edit board-specific (`HAL`) config-file (e.g. `settings-lolin32.h` for Lolin32 or `settings-lolin_d32_pro_sdmmc_pe.h` for Lolin D32/D32 pro). If you're running a board that is not listed there: start with `settings-custom.h` and change it according your needs. -* Connect your develboard via USB, click the alien-head to the left, choose the project-task that matches your desired HAL and run `Upload and Monitor`. All libraries necessary should be fetched in background now followed by code-compilation. After that, your ESP32 is flashed with the firmware. Depending on your develboard it might me necessary to push a button in order to allow ESP32 to enter flashmode (not necessary für Lolin32, D32 und D32 pro). -* Now have a look at the serial-output at the bottom of Visual Studio Code's window. At the first run there might appear a few error-messages (related to missing entries in NVS). Don't worry, this is just normal. However, make sure SD is running as this is mandatory! -* If everything ran fine, at the first run, ESPuino should open an access-point with the name "ESPuino". Join this WiFi with your computer (or mobile) and enter `http://192.168.4.1` to your webbrowser. Enter WiFi-credentials and the hostname. After saving the configuraton, restart ESPuino. Hint: I tried to connect this access-point via Android mobile. Basically that's no problem, but as my mobile detected there'd be no internet-connection, it kept LTE-connection open and prevented me from connecting to `http://192.168.4.1`. So if in doubts better use a computer. -* After reboot ESPuino tries to join your WiFi (with the credentials previously entered). If that was successful, an IP is shown in the serial-console. You can call ESPuino's GUI using a webbrowser via this IP; make sure to allow Javascript. If mDNS-feature is active in `src/settings.h`, you can use the hostname configured extended by .local instead the IP. So if you configured `espuino` as hostname, you can use `http://espuino.local` for webgui and FTP. -* Via FTP and webGUI you can upload data (but don't expect it to be super fast). -* FTP needs to be activated after boot if you need it! Don't forget to assign action `ENABLE_FTP_SERVER` in `settings.h` to be able to activate it. Neopixel flashes green (1x) if enabling was successful. It'll be disabled automatically after next reboot. Means: you have to enable it every time you need it (if reboot was in between). Sounds annoying and maybe it is, but's running this way in order to have more heap-memory available (for webstream) if FTP isn't necessary. -* Via webbrowser you can configure various settings and pair RFID-tags with actions. If MQTT/FTP-support was not compiled, their config-tabs won't appear. + +The basic idea of ESPuino is to use RFID tags to control an audio player. Even for kids this concept +is simple: place a RFID-tagged object (card, toy character, etc.) on top of a box and the music +starts to play stuff from SD card or webradio. Place a different RFID tag on it and something else +is played. Simple as that. + +This project is based on the popular microcontroller [ESP32 by +Espressif](https://www.espressif.com/en/products/hardware/esp32/overview). Why? It's powerful and +having WiFi support out-of-the-box enables further features like an integrated webserver, +smarthome-integration via MQTT, webradio and FTP server. And even Bluetooth, too! However, my +primary focus was to port the project to a modular base: MP3-decoding is done in software and the +digital music output is done via the popular [I2S protocol](https://en.wikipedia.org/wiki/I%C2%B2S). +So we need a [DAC](https://en.wikipedia.org/wiki/Digital-to-analog_converter) to make an analog +signal of it: I did all my tests with +[MAX98357A](https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp/pinouts), +[UDA1334](https://www.adafruit.com/product/3678), +[MS6324](https://forum.espuino.de/t/kopfhoererplatine-basierend-auf-ms6324-und-tda1308/1099/) and +[PCM5102a](https://github.com/biologist79/ESPuino/tree/master/PCBs/Headphone%20with%20PCM5102a%20and%20TDA1308). +General advice: ESPuino makes use of library +[ESP32-audioI2S](https://github.com/schreibfaul1/ESP32-audioI2S/); so everything that's supposed to +work with this library should work with ESPuino, too (but maybe not right out-of-the-box). +Especially this is true for +[ES8388](https://github.com/schreibfaul1/ESP32-audioI2S/blob/master/examples/ESP32_ES8388/ESP32_ES8388.ino). + +## Hardware setup + +You could start on a breadboard with jumper wires but I _strongly_ recommend to start right away +with a PCB that was especially developed for ESPuino. There are several available, but +[ESPuino-mini 4L (SMD)](https://forum.espuino.de/t/espuino-mini-4layer/1661) can be considered as +being the latest generation. Furthermore you need a ESP32-develboard like (or another one that's +pin compatible): + +- [D32 pro LiFePO4](https://forum.espuino.de/t/esp32-develboard-d32-pro-lifepo4/1109) +- [E32 LiPo](https://forum.espuino.de/t/esp32-develboard-e32-lipo/1135) +- [Wemos Lolin D32 pro](https://www.wemos.cc/en/latest/d32/d32_pro.html) + +> :warning: **Due to memory restrictions meanwhile it's mandatory to use ESP32 with +PSRAM.** This being said you need to make sure that your develboard carries an ESP32-WROVER. +And you should make sure that 16 MB flash memory is available (both is true for all +develboards named above). + +Optionally a [headphone-pcb](https://forum.espuino.de/t/kopfhoererplatine-basierend-auf-ms6324-und-tda1308/1099/) +can be attached to [ESPuino-mini 4L (SMD)](https://forum.espuino.de/t/espuino-mini-4layer/1661). + +However, feel free to develop PCBs yourself. But again, be advised your ESP32 needs PSRAM in order to +run ESPuino properly. + +## Getting started + +- [Much more documentation in german + language](https://forum.espuino.de/c/dokumentation/anleitungen/10). +- You need to install Microsoft's [Visual Studio Code](https://code.visualstudio.com/). +- Install [PlatformIO Plugin](https://platformio.org/install/ide?install=vscode) into [Visual Studio + Code](https://code.visualstudio.com/) and make sure to have a look at the + [documentation](https://docs.platformio.org/en/latest/integration/ide/pioide.html). + Step-by-step-manual is available + [here](https://randomnerdtutorials.com/vs-code-platformio-ide-esp32-esp8266-arduino/.) +- Install [Git](https://git-scm.com/downloads) and make a copy ("clone") of my repository to your local + computer using `git clone https://github.com/biologist79/ESPuino.git`. Using Git you can keep your + local repository easily up to date without doing copy'n'paste. To keep it up to date run `git pull + origin master`. Further infos + [here](https://stackoverflow.com/questions/1443210/updating-a-local-repository-with-changes-from-a-github-repository) + and + [here](https://forum.espuino.de/t/espuino-in-platformio-anlegen-und-mit-git-aktuell-halten/891). +- (Optional) Install [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) + as plugin (to have advanced Git features in VSCode). +- Now, that the Git repository is saved locally, import this folder into Platformio as a project. +- Select the [desired + environment](https://forum.espuino.de/t/projekt-und-profilwechsel-in-visual-studio-code/768) (e.g. + lolin_d32_pro_sdmmc_pe). +- Edit `src/settings.h` according your needs. +- Edit board-specific (`HAL`) config-file (e.g. `settings-lolin_d32_pro_sdmmc_pe.h` for Lolin + D32/D32 pro). If you're running a board that is not listed there: start with `settings-custom.h` + and change it according your needs. +- Connect your develboard via USB, click the alien-head icon in the left sidebar, choose the project + task that matches your desired HAL and run `Upload and Monitor`. All libraries necessary are + fetched automatically and compilation of the code gets started. After that, your ESP32 is flashed + with the firmware. Depending on your develboard it might be necessary to push a button in order to + allow ESP32 to enter flash mode (not necessary für Lolin32, D32 und D32 pro). +- Now have a look at the serial output at the bottom of Visual Studio Code's window. At the first + run there might appear a few error messages (related to missing entries in NVS). Don't worry, this + is just normal. However, make sure the SD card is detected as this is mandatory! +- If everything ran fine, at the first run, ESPuino should open an access-point and ESPuino offers a + captive portal that is shown on your computer. If that's not the case, join a WiFi called + "ESPuino" and enter `http://192.168.4.1` to your webbrowser. Enter WiFi credentials and the + hostname there (or in the captive portal). After saving the configuration, restart ESPuino. +- After reboot ESPuino tries to join your WiFi (with the credentials previously entered). If that + was successful, an IP is shown in the serial console. You can access ESPuino's GUI using a + webbrowser via this IP; make sure to allow Javascript. If the mDNS feature is active in + `src/settings.h`, you can use the hostname configured extended by .local instead the IP. So if you + configured `espuino` as hostname, you can use `http://espuino.local` for web GUI and FTP. +- Via FTP and web GUI you can upload data (expect a throughput like 320 kB/s up to 700 kB/s). +- FTP needs to be activated after boot if you need it! Don't forget to assign action + `ENABLE_FTP_SERVER` in `settings.h` to be able to activate it. Neopixel flashes green (1x) if + enabling was successful. It'll be disabled automatically after next reboot. Means: you have to + enable it every time you need it (if reboot was in between). Sounds annoying and maybe it is, + but's running this way in order to have more heap-memory available (for webstream) if FTP isn't + needed. +- Via webbrowser you can configure various settings and pair RFID tags with actions. If + MQTT/FTP-support was not compiled, their config tabs won't appear. ## SD-card: SPI or SD-MMC (1 bit)-mode? -Having SD working is mandatory, ESPuino doesn't start without working SD! However, there are two modes available to connect SD-cards: SPI and SDMMC (1 bit). Advice: don't use SPI as there's a bug that often leading to broken files due to interrupted file-transfers. Beside of that, SDMMC is still twice as fast as SPI and needs one GPIO less. + +Having the SD card working is mandatory, ESPuino doesn't start without working SD card (at least +unless `NO_SDCARD` hasn't been enabled previously). However, there are two modes available to +interface SD cards: SPI and SDMMC (1 bit). Be advised that SDMMC is twice as fast as SPI and +needs one GPIO less. So basically it's a no-brainer. ## Which RFID-reader: RC522 or PN5180? -RC522 is so to say the ESPuino-standard. It's cheap and works, but RFID-tag has to be placed near the reader. PN5180 instead has better RFID range/sensitivity and can read ISO-15693 / iCode SLIX2-tags aka 'Tonies' (you need a password to read Tonies), too. You can also wake up ESPuino with the a ISO-14443 card (after flashing PN5180 with a new firmware). This feature is called LPCD. Disadvantages PN5180: it's more expensive and needs more GPIOs (6/7 instead of 4). In my opinion it's worth it! Refer PN5180's wire-section below for further informations. Hint: if using 3.3V only make sure to connect these 3.3V to PN5180's 5V AND 3.3V. Sounds weird but it's necessary. -## 3.3 or 5V? -ESP32 runs at 3.3 V only. But what about the periphery? -* 3.3V! Because: if you plan to use battery-mode with LiPo/LiFePO4, there's no 5 V available (unless USB is connected or you make use of a boost-converter). -That's why my focus is on 3.3 V only. If you want to use 5 V instead - do so, but be advised it might not be compatible with battery-mode. -* MAX98357a: provides more power at 5 V but also runs at 3.3 V. Anyway: it's still loud enough (in my opinion). -* Neopixel: specification says it needs 5 V but runs at 3.3 V as well. -* RC522: needs 3.3 V (don't ever power with 5 V!) -* PN5180: at 3.3 V make sure to connect both 5 V and 3.3 V-pins to 3.3 V. If 5 V is available all the time: connect 5 V to 5 V and 3.3 V to 3.3 V. -* SD: needs 3.3 V but if voltage-regulator is onboard, it can be connected to 5 V as well -* Rotary encoder: 3.3 V (don't power with 5 V! Encoder doesn't care if connected to 3.3 or 5 V, but GPIOs of ESP32 do!) +RC522 is so to say the ESPuino standard. It's cheap and works, but RFID tags have to be placed close +to the reader. PN5180 instead has better RFID range/sensitivity and can read ISO-15693 / iCode +SLIX2-tags aka 'Tonies' (you need a password to read Tonies), too. You can also wake up ESPuino with +the a ISO-14443 card (after flashing PN5180 with a new firmware). This feature is called LPCD. +Disadvantages PN5180: it's more expensive and needs more GPIOs (6/7 instead of 4). In my opinion +it's worth it! Refer to PN5180's wiring section below for further information. Hint: if using 3.3 V +only make sure to connect these 3.3 V to PN5180's 5 V AND 3.3 V. Sounds weird but it's necessary. -## WiFi -WiFi is mandatory for webgui, FTP, MQTT and webradio. However, WiFi can be temporarily or permanently disabled (and ESPuino remembers this state after the next restart. There are two ways to (re-)enable/disable WiFi: -* Use a special [modification-card](https://forum.espuino.de/t/was-sind-modifikationskarten/37) that can be configured via webgui. -* Assign action `CMD_TOGGLE_WIFI_STATUS` to a button (or multi-button). This toggles the current WiFi-status. +## 3.3 V or 5 V? -## Bluetooth -ESPuino can be used as bluetooth-sink (a2dp-sink). In this mode you can stream (e.g. from a mobile device) via bluetooth audio to your espuino. This mode can be enabled/disabled via a RFID-modification-card or by assigning action `CMD_TOGGLE_BLUETOOTH_MODE` to a button (or multi-button). Applying this will restart ESPuino immediately. Activated bluetooth is indicated by four slow rotating *blue* LEDs. Please note: due to memory-restrictions it's not possible to run bluetooth in parallel with WiFi. This finally means that you cannot access webgui while this mode is enabled. +ESP32 runs at 3.3 V only. But what about the periphery? -ESPuino can also be used to stream audio (a2dp-source) to a bluetooth headset or external bluetooth speakers. This mode can be enabled/disabled via a RFID-modification-card or by assigning action `CMD_TOGGLE_BLUETOOTH_SOURCE_MODE` to a button (or multi-button). Applying this will restart ESPuino immediately. Activated bluetooth is indicated by four slow rotating *blue-violet* LEDs. After the bluetooth headset is connected LEDs turn to blue. Please note: due to memory-restrictions it's not possible to run bluetooth in parallel with WiFi. This finally means that you cannot stream webradio via bluetooth or access webgui while this mode is enabled. +- 3.3 V! Because: if you plan to use battery mode with LiPo/LiFePO4, there's no 5 V available (unless + USB is connected or you make use of a boost converter). That's why my focus is on 3.3 V only. If + you want to use 5 V instead - do so, but be advised it might not be compatible with battery mode. +- MAX98357A: provides more power at 5 V but also runs at 3.3 V. Anyway: it's still loud enough (in + my opinion). +- Neopixel: specification says it needs 5 V but runs at 3.3 V as well. +- RC522: needs 3.3 V (don't ever power with 5 V!) +- PN5180: at 3.3 V make sure to connect both 5 V and 3.3 V pins to 3.3 V. If 5 V is available all + the time: connect 5 V to 5 V and 3.3 V to 3.3 V. +- SD card: needs 3.3 V but if voltage regulator is onboard, it can be connected to 5 V as well +- Rotary encoder: 3.3 V (don't power with 5 V! Encoder doesn't care if connected to 3.3 or 5 V, but + GPIOs of ESP32 do!) -## Port-expander -There might be situations where you run out of GPIOs. To address this, port-expander [PCA9555](https://www.nxp.com/docs/en/data-sheet/PCA9555.pdf) can be used to extend number of input-channels (output-mode is only supported in special cases). This port-expander provides 2 ports with 8 channels each - so 16 channels in total. To activate PCA9555 you need to enable `PORT_EXPANDER_ENABLE`. Like GPIOs in your develboard-specific settings-file, you can assign numbers. Range is 100->115 where 100: port 0 channel 0 -> 107: port 0 channel 7; 108: port 1 channel 0 -> 115: port 1 channel 7. Via `expanderI2cAddress` port-expander's I2C-address can be changed. It's `0x20` if all A0, A1, A2 are wired to GND.
+## WiFi -## After ESPuino is connected to your WiFi -After making ESPuino part of your LAN/WiFi, the 'regular' webgui is available at the IP assigned by your router (or the configured hostname). Using this GUI you can: -* configure WiFi -* make bindings between RFID-tag, file/directory/URL and playMode -* make bindings between RFID-tag and a modification-type -* configure MQTT (if enabled) -* configure FTP (if enabled) -* configure initial volume, maximum volume (speaker / headphone), brightness of Neopixel (nightmode / default) and inactivity-time -* view logs / status / current track -* control player -* upload audiofiles (called webtransfer) -* do OTA-updates (ESP32s with 16 MB of flash-memory only) -* import + delete NVS-RFID-assigments -* restart + shutdown ESPuino +WiFi is mandatory for web GUI, FTP, MQTT and webradio. However, WiFi can be temporarily or +permanently disabled (and ESPuino remembers this state after the next restart). There are two ways +to (re-)enable/disable WiFi: -Webgui #1: - +- Use a special [modification card](https://forum.espuino.de/t/was-sind-modifikationskarten/37) that + can be configured via web GUI. +- Assign action `CMD_TOGGLE_WIFI_STATUS` to a button (or multi-button). This toggles the current + WiFi status. -Webgui #2: - +## Bluetooth + +> :warning: **Due to memory restrictions it's not possible to run Bluetooth in + parallel with WiFi.** This means that you cannot stream webradio via Bluetooth + or access the web GUI while this mode is enabled. -Webgui #3: - +### ESPuino as A2DP sink (stream to ESPuino) -Webgui #4: - +ESPuino can be used as Bluetooth sink (A2DP sink). In this mode you can stream audio (e.g. from a +mobile device) via Bluetooth to your ESPuino. This mode can be enabled/disabled via a RFID +modification card or by assigning action `CMD_TOGGLE_BLUETOOTH_MODE` to a button (or multi-button). +Applying this will restart ESPuino immediately. Activated Bluetooth is indicated by four +slow rotating _blue-violet_ LEDs. After the Bluetooth device is paired the LEDs turn to blue. -Webgui #5: - +### ESPuino as A2DP source (stream from ESPuino) -Webgui #6: - +ESPuino can also be used to stream audio (A2DP source) to a Bluetooth headset or external Bluetooth +speakers. This mode can be enabled/disabled via a RFID modification card or by assigning action +`CMD_TOGGLE_BLUETOOTH_SOURCE_MODE` to a button (or multi-button). Applying this will restart ESPuino +immediately. Activated Bluetooth is indicated by four slow rotating _blue-violet_ LEDs. After the +Bluetooth headset is connected LEDs turn to blue. -Webgui #7: - +## Port expander -Webgui: websocket broken: - +There might be situations where you run out of GPIOs. To address this, a port expander +[PCA9555](https://www.nxp.com/docs/en/data-sheet/PCA9555.pdf) can be used to extend the number of +input channels (output mode is only supported in special cases). PCA9555 provides 2 ports with 8 +channels each - so 16 channels in total. To activate PCA9555 you need to enable +`PORT_EXPANDER_ENABLE`. Like GPIOs in your develboard-specific settings-file, you can assign +numbers. Range is `100` (port 0 channel 0) -> `115` (port 1 channel 7). Via `expanderI2cAddress` the +port expander's I2C-address can be changed. It's `0x20` if pins `A0`, `A1`, `A2` are wired to GND. -Webgui: action ok: - +## After ESPuino is connected to your WiFi -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 is filled out automatically by selecting a file/directory in the filebrowser. +After making ESPuino part of your LAN/WiFi, the 'regular' web GUI is available at the IP assigned by +your router (or the configured hostname). Using this GUI you can: + +- configure WiFi +- make bindings between RFID tag, file/directory/URL and playback mode +- make bindings between RFID tag and a modification type +- configure MQTT (if enabled) +- configure FTP (if enabled) +- configure initial volume, maximum volume (speaker / headphone), brightness of Neopixel (night mode + / default) and inactivity time +- configure voltage levels for battery mode +- view logs / status / current track +- control player +- run modifications (like modification card) +- upload audiofiles (called web transfer) +- do OTA updates (ESP32 with 16 MB of flash memory only) +- import + delete NVS-RFID-assigments +- restart + shutdown ESPuino + +> :information_source: As you apply a RFID tag to the RFID reader, the corresponding ID is pushed to + the GUI automatically. So there's no need to enter such IDs manually (unless you want to). The + file path is filled out automatically by selecting a file/directory in the file browser. + + + + + + + + + + ## Interacting with ESPuino -### Playmodes -It's not just simply playing music; different playmodes are supported: -* `Single track` => plays one track one time -* `Single track (loop)` => plays one track forever -* `Single track of a directory (random). Followed by sleep` => picks and plays one single track out of a directory and falls asleep subsequently. Neopixel gets dimmed. -* `Audiobook`=> single file or playlist/folder; last play-position (file and playlist) is saved (when pushing pause or moving to another track) and re-used next time -* `Audiobook (loop)` => same as audiobook but loops forever -* `Folder/playlist (alph. sorted)` => plays all tracks in alph. order from a folder one time -* `Folder/playlist (random order)` => plays all tracks in random order from a folder one time -* `Folder/playlist (alph. sorted)` => plays all tracks in alph. order from a folder forever -* `Folder/playlist (random order)` => plays all tracks in random order from a folder forever -* `All tracks of a random subdirectory (sorted alph.)` => plays of tracks in alph. order of a randomly picked subdirectory of a given directory -* `Webradio` => always only one "track": plays a webstream -* `List (files from SD and/or webstreams) from local .m3u-File` => can be one or more files / webradio-stations with local .m3u as sourcefile - -### Modification RFID-tags -There are special RFID-tags, that don't start music by themself but can modify things. If applied a second time, it's previous action/modification will be reversed. Please note: all sleep-modes do dimming (Neopixel) automatically because it's supposed to be used in the evening when going to bed. Well, at least that's my children's indication :-) So first make sure to start the music then use a modification-card in order to apply your desired modification: -* lock/unlock all buttons -* sleep after 5/30/60/120 minutes -* sleep after end of current track -* sleep after end of playlist -* sleep after five tracks -* dimm neopixel -* current track in loop-mode (is "stronger" than playlist-loop but doesn't overwrite it!) -* playlist in loop-mode -* track und playlist loop-mode can both be activated at the same time, but unless track-loop isn't deactivated, playlist-loop won't be effective -* Toggle WiFi (enable/disable) => disabling WiFi while webstream is active will stop a running webstream instantly! -* Toggle Bluetooth sink (enable/disable) => restarts ESPuino immediately. In this mode you can stream to your ESPuino via BT. -* Toggle Bluetooth source (enable/disable) => restarts ESPuino immediately. In this mode your ESPuino can stream via BT to an external device. - -### Neopixel-ring (optional) -Indicates different things. Don't forget configuration of number of LEDs via #define NUM_LEDS -* While booting: every second LED (rotating orange) -* Unable to mount SD: LEDs flashing red (will remain forever unless SD-card is available or `SHUTDOWN_IF_SD_BOOT_FAILS` is active) -* IDLE: four LEDs slow rotating (white if WiFi connected; green if WiFi disabled or ESPuino is about to connect to WiFi) -* BLUETOOTH: four LEDs slow rotating coloured blue -* ERROR: all LEDs flashing red (1x) if an action was not accepted -* OK: all LEDs flashing green (1x) if an action was accepted -* BUSY: violet; four fast rotating LEDs when generating a playlist. Duration depends on the number of files in your playlist. -* track-progress: rainbow; number of LEDs relative to play-progress -* playlist-progress: blue; appears only shortly in playlist-mode with the beginning every new track; number of LEDs relative to progress -* webstream: two slow rotating LEDs that change their colours rainbow-wise as the stream proceeds -* volume: green => red-gradient; number of LEDs relative from current to max volume -* switching off: red-circle that grows until long-press-time is reached -* buttons locked: track-progress-LEDs coloured red -* paused: track-progress-LEDs coloured orange -* rewind: if single-track-loop is activated a LED-rewind is performed when restarting the given track -* (Optional) Undervoltage: flashes three times red if battery-voltage is too low. This voltage-level can be configured via GUI. -* (Optional) Short press of rotary encoder's button provides battery-voltage visualisation via Neopixel. Upper und lower voltage cut-offs can be adjusted via GUI. So for example if lower voltage is set to 3.2 V and upper voltage to 4.2 V, 50% of the LEDs indicate a voltage of 3.7 V. - -Please note: some Neopixels use a reversed addressing which leads to the 'problem', that all effects are shown -counter clockwise. If you want to change that behaviour, just enable `NEOPIXEL_REVERSE_ROTATION`. + +### Playback modes + +It's not just simply playing music; different playback modes are supported: + +- `Single track` => plays one track one time +- `Single track (loop)` => plays one track forever +- `Single track of a directory (random). Followed by sleep` => picks and plays one single track out + of a directory and falls asleep subsequently. Neopixel gets dimmed. +- `Audiobook`=> single file or playlist/folder; last play position (file and playlist) is saved + (when pushing pause or moving to another track) and reused next time +- `Audiobook (loop)` => same as audiobook but loops forever +- `Folder/playlist (alph. sorted)` => plays all tracks in alph. order from a folder one time +- `Folder/playlist (random order)` => plays all tracks in random order from a folder one time +- `Folder/playlist (alph. sorted)` => plays all tracks in alph. order from a folder forever +- `Folder/playlist (random order)` => plays all tracks in random order from a folder forever +- `All tracks of a random subdirectory (sorted alph.)` => plays of tracks in alph. order of a + randomly picked subdirectory of a given directory +- `All tracks of a random subdirectory (random order)` => plays all tracks in random order of a + randomly picked subdirectory of a given directory +- `Webradio` => always only one "track": plays a webstream +- `List (files from SD and/or webstreams) from local .m3u-File` => can be one or more files / + webradio stations with local .m3u as sourcefile + +### Modification RFID tags + +There are special RFID tags, that don't start music by themselves but can modify things. If applied +a second time, it's previous action/modification will be reversed. + +So first make sure to start the music then use a modification card in order to apply your desired +modification: + +- Lock/unlock all buttons +- Sleep after 5/30/60/120 minutes +- Sleep after end of current track +- Sleep after end of playlist +- Sleep after five tracks +- Dim Neopixel +- Loop track +- Loop playlist +- Toggle WiFi (enable/disable) => disabling WiFi while webstream is active will stop a running + webstream instantly! +- Toggle Bluetooth sink (enable/disable) => restarts ESPuino immediately. In this mode you can + stream to your ESPuino via BT. +- Toggle Bluetooth source (enable/disable) => restarts ESPuino immediately. In this mode your + ESPuino can stream via BT to an external device. +- Toggle through the different modes (Normal => BT-Sink => BT-Source => Normal) +- Speech output of IP-address or current time + +> :information_source: All sleep modes do dimming (Neopixel) automatically because it's supposed to + be used in the evening when going to bed. Well, at least that's my children's indication :-) +> +> :information_source: Track and playlist loop mode can both be activated at the same time, but + unless track loop isn't deactivated, playlist loop won't be effective + +### Neopixel LEDs (optional) + +Indicates different things. Don't forget configuration of number of LEDs via `#define NUM_LEDS`. +Most designs use a Neopixel ring, but a linear strip is also possible. + +> :information_source: Some Neopixels use a reversed addressing which leads to the 'problem', that + all effects are shown counter clockwise. If you want to change that behaviour, just enable + `NEOPIXEL_REVERSE_ROTATION`. + +#### Boot + +- While booting: every second LED (rotating orange) +- Unable to mount SD: LEDs flashing red (will remain forever unless SD card is available or + `SHUTDOWN_IF_SD_BOOT_FAILS` is active) + +#### Status + +- **Idle**: four LEDs slowly rotating (white if WiFi connected; green if WiFi disabled or ESPuino is + about to connect to WiFi) +- **Bluetooth**: four LEDs slow rotating coloured blue +- **Error**: all LEDs flashing red (1x) if an action was not accepted +- **OK**: all LEDs flashing green (1x) if an action was accepted +- **Power Off**: red-circle that grows until long-press-time is reached +- **Buttons Locked**: track-progress-LEDs coloured red + +#### Playback + +- **Busy**: violet; four fast rotating LEDs when generating a playlist. Duration depends on the + number of files in your playlist. +- **Track Progress**: rainbow; number of LEDs relative to play-progress +- **Playlist Progress**: blue; appears only shortly in playlist-mode with the beginning every new + track; number of LEDs relative to progress +- **Webstream**: two slow rotating LEDs that change their colours rainbow-wise as the stream + proceeds +- **Volume**: green => red-gradient; number of LEDs relative from current to max volume +- **Paused**: track-progress-LEDs coloured orange +- **Rewind**: if single-track-loop is activated a LED-rewind is performed when restarting the given + track + +#### Battery Status (optional) + +- **Undervoltage**: flashes three times red if battery-voltage is too low. This voltage-level can be + configured via GUI. +- Short press of rotary encoder's button provides battery-voltage visualisation via Neopixel. Upper + und lower voltage cut-offs can be adjusted via GUI. So for example if lower voltage is set to 3.2 + V and upper voltage to 4.2 V, 50% of the LEDs indicate a voltage of 3.7 V. ### Buttons -Important: this section describes my default-design: 3 buttons + rotary-encoder. Feel free to change button-number (up to 5) and button-actions according your needs in `settings.h` and your develboard-specific config-file (e.g. `settings-lolin32.h`). At maximum you can activate five buttons + rotary-encoder. -Minimum duration for long press (to distinguish vom short press) in ms is defined by `intervalToLongPress`. All actions available are listed in `src/values.h`. If using GPIO >= 34 make sure to add a external pullup-resistor (10 k). -* previous (short): previous track / beginning of the first track if pressed while first track is playing -* previous (long): first track of playlist -* next (short): next track of playlist -* next (long): last track of playlist -* pause/play (short/long): pause/play -* rotary encoder (turning): vol +/- -* rotary encoder (button long): switch off (only when on) -* rotary encoder (button short): switch on (when switched off) -* rotary encoder (button short): show battery-voltage via Neopixel (when switched on and `MEASURE_BATTERY_VOLTAGE` is active) -* previous (long; keep pressed) + next (short) + release (both): toggle WiFi enabled/disabled - -### Music-play -* Music starts to play right after a valid RFID-tag was applied. -* If `PLAY_LAST_RFID_AFTER_REBOOT` is active, ESPuino will remember the last RFID applied => music-autoplay. -* If a folder should be played that contains many mp3s, the playlist-generation can take a few seconds. -* For all playmodes that are not single tracks or webradio a filecache is available to speed up playlist-generation. The cache is generated as you apply the corresponding RFID-tag for the first time. Use `CACHED_PLAYLIST_ENABLE` to enable it - I really recommend to use it. -* A file's name including path isn't allowed exceed 255 characters. -* While playlist is generated Neopixel indicates BUSY-mode. -* After last track was played, Neopixel indicates IDLE-mode. - -### Audiobook-mode -This mode is different from the others because the last playposition is saved. Playposition is saved when... -* next track starts. -* first/previous/last track requested by button. -* pause was pressed. -* track is over. -* playlist is over (playposition is set back to the first track and file-position 0). -* As per default last playposition is not saved when applying a new RFID-tag. You can enable this using `SAVE_PLAYPOS_WHEN_RFID_CHANGE`. -* As per default last playposition is not saved when doing shutdown. You can enable this using `SAVE_PLAYPOS_BEFORE_SHUTDOWN`. + +> :warning: This section describes my default-design: 3 buttons + rotary-encoder. Feel free to + change number of buttons (up to 5) and button-actions according your needs in `settings.h` and + your develboard-specific config-file (e.g. `settings-lolin32.h`). At maximum you can activate five + buttons + rotary-encoder. Minimum duration for long press (to distinguish vom short press) in ms + is defined by `intervalToLongPress`. All actions available are listed in `src/values.h`. If using + GPIO \>= 34 make sure to add a external pullup-resistor (10 k). + +- **Previous** (short): previous track / beginning of the first track if pressed while first track + is playing +- **Previous** (long): first track of playlist +- **Next** (short): next track of playlist +- **Next** (long): last track of playlist +- **Pause/Play** (short/long): pause/play +- **Rotary Encoder** (turning): vol +/- +- **Rotary Encoder** (button long): switch off (only when on) +- **Rotary Encoder** (button short): switch on (when switched off) +- **Rotary Encoder** (button short): show battery-voltage via Neopixel (when switched on and + `MEASURE_BATTERY_VOLTAGE` is active) +- **Previous** (long; keep pressed) + **Next** (short) + release (both): toggle WiFi + enabled/disabled + +### Music playback + +- Music starts to play right away after a valid RFID tag was applied (if it's known to ESPuino). +- If `PLAY_LAST_RFID_AFTER_REBOOT` is active, ESPuino will remember the last RFID applied => + music-autoplay. +- If a folder should be played that contains many MP3s, the playlist generation can take a few + seconds. +- A file's name including path isn't allowed to exceed 255 characters. +- While the playlist is generated Neopixel indicates BUSY-mode. +- After the last track was played, Neopixel indicates IDLE-mode. + +### Audiobook mode + +This mode is different from the others because the last playback position is saved, when... + +- next track starts. +- first/previous/last track requested by button. +- pause was pressed. +- track is over. +- playlist is over (position is reset to the first track and file position 0). +- As per default last playback position is not saved when applying a new RFID tag. You can enable + this using `SAVE_PLAYPOS_WHEN_RFID_CHANGE`. +- As per default last playback position is not saved when doing shutdown. You can enable this using + `SAVE_PLAYPOS_BEFORE_SHUTDOWN`. ### FTP (optional) -* FTP needs to be activated after boot! Don't forget to assign action `ENABLE_FTP_SERVER` in `settings.h` or use a modification-card to to activate it! Neopixel flashes green (1x) if enabling was successful. It'll be disabled automatically after next reboot. Means: you have to enable it every time you need it (if reboot was in between). Sounds annoying and maybe it is, but's running this way in order to save heap-memory when FTP isn't needed. -* Why FTP? Well: in order to avoid exposing µSD-card or disassembling ESPuino all the time for adding new music, it's possible to transfer music to the µSD-card using FTP. Another possibility is to do via webGUI (webtransfer). -* Default-user and password are set to `esp32` / `esp32` but can be changed via GUI. -* Make sure to set the max. number of parallel connections to ONE in your FTP-client and the charset to CP437. CP437 is important if you want to use german umlauts (öäüß). -* Secured FTP is not available. So make sure to disable SSL/TLS. -* Software: my recommendation is [Filezilla](https://filezilla-project.org/) as it's free and available for multiple platforms. -* Don't expect a super fast data-transfer; it's around 185 kB/s (SPI-mode) and 310-360 kB/s (MMC-mode). -* Please note: if music is played in parallel, this rate decrases dramatically! So better stop playback when doing file-transfers. + +- FTP needs to be activated after boot! Don't forget to assign action `ENABLE_FTP_SERVER` in + `settings.h` or use a modification card to activate it! Neopixel flashes green (1x) if enabling + was successful. It'll be disabled automatically after next reboot. Means: you have to enable it + every time you need it (if reboot was in between). Sounds annoying and maybe it is, but it's + running this way in order to save heap memory when FTP isn't needed. +- Why FTP? Well: in order to avoid exposing the SD card or disassembling ESPuino all the time for + adding new music, it's possible to transfer music to the SD card using FTP. Another possibility + is to do via web GUI (webtransfer). +- Default user and password are set to `esp32` / `esp32` but can be changed via GUI. +- Secured FTP is not available. So make sure to disable SSL/TLS. +- Software: my recommendation is [Filezilla](https://filezilla-project.org/) as it's free and + available for multiple platforms. +- Please note: if music is played in parallel, this rate decreases dramatically! So better stop + playback when doing file transfers. ### Energy saving -As already described in the modify-section, there are different sleepmodes available. Additionaly µC will be put to deepsleep after 10 minutes of inactivity (configurable my maxInactivityTime) unless ESPuino doesn't play music, has a FTP-client connected and any input via buttons. Every button-interaction resets the counter. -### MQTT (optional) -Everything that can be controlled via RFID-tags and buttons, can also be controlled via MQTT (excepting toggling WiFi-status as this doesn't make sense). All manual interactions (buttons, RFID-tags) are also sent to MQTT in parallel, so everything is always in-sync (unless Wifi/MQTT-connection is broken). In my home-setup I'm using [openHAB](https://www.openhab.org/) to "encapsulate" MQTT into a nice GUI, that's accessible via APP + web. I [described](https://github.com/biologist79/ESPuino/tree/master/openHAB) a sample-config for openHAB2. However, meanwhile openHAB3 is available and all the stuff described can also be configured via GUI. Be advised that openHAB is pretty complex and you have to spend some time to get familiar with it. +As already described in the modifications section, there are different sleep modes available. +Additionally the ESP32 controller will be put to deep sleep after 10 minutes of inactivity +(configurable via `maxInactivityTime`) unless ESPuino doesn't play music, has a FTP client connected +and any input via buttons. Every button interaction resets the counter. ### Backups -As all assignments between RFID-IDs and actions (playmode, file to play...) is saved in ESP's NVS, the problem is that it's all gone when the ESP is broken. So that's where a backup comes into play. So every time you change or add a new assignment between a RFID-tag and an action via GUI, a backup-file is saved on the µSD-card. The file's name can be changed via `backupFile`. So better don't delete it! Using the webgui you can use the upload-form to import such a file. - -## Smarthome (optional) -As already described, MQTT is supported. In order to use it it's necessary to run a MQTT-broker; [Mosquitto](https://mosquitto.org/) for instance. After connecting to it, ESPuino subscribes to all command-topics. State-topics are used to push states to the broker in order to inform others if anything changed (change of volume, new playlist, new track... name it). Others, like openHAB, subscribe to state-topics end send commands via command-topics. So it's not just limited to openHAB. It's just necessary to use a platform, that supports MQTT. For further informations (and pictures) refer the [subfolder](https://github.com/biologist79/ESPuino/tree/master/openHAB). - -## MQTT-topics and their ranges -Feel free to use your own smarthome-environments (instead of openHAB). The MQTT-topics available are described as follows. Please note: if you want to send a command to ESPuino, you have to use a cmnd-topic whereas ESPuino pushes its states back via state-topics. So guess you want to change the volume to 8 you have to send this number via topic-variable `topicLoudnessCmnd`. Immediately after doing to, ESPuino sends a conformation of this command using `topicLoudnessState`. To get hands on MQTT I recommend this [one](https://www.hivemq.com/mqtt-essentials/) as introducton (covers more than you need for ESPuino). - -| topic-variable | range | meaning | -| ----------------------- | --------------- | ------------------------------------------------------------------------------ | -| topicSleepCmnd | 0 or OFF | Power off ESPuino immediately | -| topicSleepState | ON or OFF | Sends ESPuino's last state | -| topicRfidCmnd | 12 digits | Set number of RFID-tag which 'emulates' an RFID-tag (e.g. `123789456089`) | -| topicRfidState | 12 digits | ID of current RFID-tag (if not a modification-card) | -| topicTrackState | String | Sends current track number, total number of tracks and full path of curren track. E.g. "(2/10) /mp3/kinderlieder/Ri ra rutsch.mp3" | -| topicTrackControlCmnd | 1 -> 7 | `1`=stop; `2`=unused!; `3`=play/pause; `4`=next; `5`=prev; `6`=first; `7`=last | +As all assignments between RFID IDs and actions (playback mode, file to play, ...) is saved in ESP's +NVS, the problem is that it's all gone when the ESP is broken. So that's where a backup comes in +handy. Every time you change or add a new assignment between a RFID tag and an action via GUI, a +backup file is saved on the SD card. The file's name can be changed via `backupFile`. So better +don't delete it! Using the web GUI you can use the upload form to import such a file. + +### Smarthome/MQTT (optional) + +Everything that can be controlled via RFID tags and buttons, can also be controlled via MQTT +(excepting toggling WiFi status as this doesn't make sense). All manual interactions (buttons, RFID +tags) are also sent to MQTT in parallel, so everything is always in sync (unless +Wifi/MQTT-connection is broken). + +In order to use it it's necessary to run a MQTT broker; [Mosquitto](https://mosquitto.org/) for +instance. After connecting to it, ESPuino subscribes to all command-topics. State-topics are used to +push states to the broker in order to inform others if anything changed (change of volume, new +playlist, new track, you name it). + +In my home setup I'm using [openHAB](https://www.openhab.org/) to "encapsulate" MQTT into a nice +GUI, that's accessible via app + web. For further information (and pictures) refer to the [openHAB +directory](https://github.com/biologist79/ESPuino/tree/master/openHAB). + +> :information_source: I [described](https://github.com/biologist79/ESPuino/tree/master/openHAB) a + sample config for openHAB2. However, meanwhile openHAB3 is available and all the stuff described + can also be configured via GUI. Be advised that openHAB is pretty complex and you have to spend + some time to get familiar with it. + +#### MQTT topics + +Feel free to use your own smarthome environments (instead of openHAB). The MQTT topics available are +described as follows. + +> :information_source: If you want to send a command to ESPuino, you have to use a cmnd-topic + whereas ESPuino pushes its states back via state-topics. So guess you want to change the volume to + 8 you have to send this number via topic-variable `topicLoudnessCmnd`. Immediately after doing so, + ESPuino sends a conformation of this command using `topicLoudnessState`. To get hands on MQTT I + recommend this [one](https://www.hivemq.com/mqtt-essentials/) as introduction (covers more than + you need for ESPuino). + +| topic-variable | range | meaning | +| ----------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| topicSleepCmnd | 0 or OFF | Power off ESPuino immediately | +| topicSleepState | ON or OFF | Sends ESPuino's last state | +| topicRfidCmnd | 12 digits | Set number of RFID tag which 'emulates' an RFID tag (e.g. `123789456089`) | +| topicRfidState | 12 digits | ID of current RFID tag (if not a modification card) | +| topicTrackState | String | Sends current track number, total number of tracks and full path of curren track. E.g. "(2/10) /mp3/kinderlieder/Ri ra rutsch.mp3" | +| topicTrackControlCmnd | 1 -> 7 | `1`=stop; `2`=unused!; `3`=play/pause; `4`=next; `5`=prev; `6`=first; `7`=last | | topicCoverChangedState | | Indicated that the cover image has potentially changed. For performance reasons the application should load the image only if it's visible to the user | -| topicLoudnessCmnd | 0 -> 21 | Set loudness (depends on minVolume / maxVolume) | -| topicLoudnessState | 0 -> 21 | Sends loudness (depends on minVolume / maxVolume | -| topicSleepTimerCmnd | EOP | Power off after end to playlist | -| | EOT | Power off after end of track | -| | EO5T | Power off after end of five tracks | -| | 1 -> 2^32 | Duration in minutes to power off | -| | 0 | Deactivate timer (if active) | -| topicSleepTimerState | various | Sends active timer (`EOP`, `EOT`, `EO5T`, `0`, ...) | -| topicState | Online, Offline | `Online` when powering on, `Offline` when powering off | -| topicCurrentIPv4IP | IPv4-string | Sends ESPuino's IP-address (e.g. `192.168.2.78`) | -| topicLockControlsCmnd | ON, OFF | Set if controls (buttons, rotary encoder) should be locked | -| topicLockControlsState | ON, OFF | Sends if controls (buttons, rotary encoder) are locked | -| topicPlaymodeState | 0 - 10 | Sends current playmode (single track, audiobook...; see playmodes) | -| topicRepeatModeCmnd | 0 - 3 | Set repeat-mode: `0`=no; `1`=track; `2`=playlist; `3`=both | -| topicRepeatModeState | 0 - 3 | Sends repeat-mode | -| topicLedBrightnessCmnd | 0 - 255 | Set brightness of Neopixel | -| topicLedBrightnessState | 0 - 255 | Sends brightness of Neopixel | -| topicBatteryVoltage | float | Voltage (e.g. 3.81) | -| topicBatterySOC | float | Current battery charge in percent (e.g. 83.0) | -| topicWiFiRssiState | int | Numeric WiFi signal-strength (dBm) | -| topicSRevisionState | String | Software-revision | +| topicLoudnessCmnd | 0 -> 21 | Set loudness (depends on minVolume / maxVolume) | +| topicLoudnessState | 0 -> 21 | Sends loudness (depends on minVolume / maxVolume | +| topicSleepTimerCmnd | EOP | Power off after end to playlist | +| | EOT | Power off after end of track | +| | EO5T | Power off after end of five tracks | +| | 1 -> 2^32 | Duration in minutes to power off | +| | 0 | Deactivate timer (if active) | +| topicSleepTimerState | various | Sends active timer (`EOP`, `EOT`, `EO5T`, `0`, ...) | +| topicState | Online, Offline | `Online` when powering on, `Offline` when powering off | +| topicCurrentIPv4IP | IPv4-string | Sends ESPuino's IP-address (e.g. `192.168.2.78`) | +| topicLockControlsCmnd | ON, OFF | Set if controls (buttons, rotary encoder) should be locked | +| topicLockControlsState | ON, OFF | Sends if controls (buttons, rotary encoder) are locked | +| topicPlaymodeState | 0 - 10 | Sends current playback mode (single track, audiobook...; see [playback modes](#playback-modes)) | +| topicRepeatModeCmnd | 0 - 3 | Set repeat-mode: `0`=no; `1`=track; `2`=playlist; `3`=both | +| topicRepeatModeState | 0 - 3 | Sends repeat-mode | +| topicLedBrightnessCmnd | 0 - 255 | Set brightness of Neopixel | +| topicLedBrightnessState | 0 - 255 | Sends brightness of Neopixel | +| topicBatteryVoltage | float | Voltage (e.g. 3.81) | +| topicBatterySOC | float | Current battery charge in percent (e.g. 83.0) | +| topicWiFiRssiState | int | Numeric WiFi signal-strength (dBm) | +| topicSRevisionState | String | Software-revision | + +## Development and Contributions + +### Code Formatting + +Automatic code formatting via `clang-format` is used. The configuration/rules can be found in +`.clang-format`. If you use Visual Studio Code as IDE, the support for this is automatically +available through the C++ extension. + +When editing source code, use Ctrl+Shift+I to run the auto-formatting on the file or Ctrl+K Ctrl+F +for the selected code. + +See the [documentation](https://code.visualstudio.com/docs/cpp/cpp-ide#_code-formatting) for more +options like run auto-formatting on saving or editing the file. + +The CI (via "GitHub Actions") checks changes when they are pushed in order to keep the source code +clean and provide feedback to the developers and maintainers. + +To keep the output of `git blame` clean despite the massive changes when introducing new formatting +rules, we have a `.git-blame-ignore-revs` that lists the commits to ignore. In VSCode this should be +used automatically if you use the "GitLens" extension. Otherwise make sure you apply the config +fragment for git by running `git config --local include.path ../.gitconfig` once in your local +clone. diff --git a/changelog.md b/changelog.md index 419ba1ee..b2165758 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,135 @@ -## New (modules) +# Changelog + +## DEV-branch + +* 30.11.2023: Fix a nullptr access after trying to replay an invalid filename (#271), thanks to Olaf! +* 29.11.2023: Updated audio library to play more MP3s, faster track change & delivery of the cover image +* 25.11.2023: Save some cpu time in audio task by only updating the playtime statistics every 250ms +* 22.11.2023: Web-UI: Search for files feature #268 +* 21.11.2023: New command CMD_TOGGLE_MODE to switch Normal => BT-Sink => BT-Source +* 21.11.2023: Bugfix: Some commands e.g. Play/Pause did not work in BT-Source mode +* 21.11.2023: Faster pairing animation in BT-Source mode to better distinguish between the two BT modes +* 19.11.2023: Give audiotask a higher task priority, fixes crackling sound +* 19.11.2023: bugfix_same_rfid_twice init #262, see Github comments +* 17.11.2023: Show track progress, current playtime & duration in web-ui (#267) +* 16.11.2023: Fix delay with getLocalTime() +* 14.11.2023: Multi Wlan improvements (#266), thanks to @laszloh ! +* 13.11.2023: Update third party libraries +* 08.11.2023: Fix HTML syntax errors, found with static code analysis tool "HtmlHint" +* 08.11.2023: Better logging for boards without PSRAM, fewer logs when compiling with NO_SDCARD +* 07.11.2023: Set timezone after startup, thanks to @Joe91 ! + + +## Version 2 (07.11.2023) + +* 04.11.2023: LPCD: wakeup check for ISO-14443 cards also with IRQ connected to port-expander +* 04.11.2023: Bugfix for showing wrong loglevel +* 03.11.2023: Web-ui: Fix overlapping info/log after pressing refresh button +* 01.11.2023: Fix folder upload with special chars & playtime formatting +* 31.10.2023: Code Formatting via clang-format #264, thanks to @laszloh & @fschrempf !! +* 27.10.2023: PlatformIO package 6.4.0, Arduino version stays at 2.0.11 +* 26.10.2023: LPCD, RFID_IRQ on port-expander: fix compiler warning +* 26.10.2023: portTICK_RATE_MS is deprecated, use portTICK_PERIOD_MS instead () +* 26.10.2023: Cppcheck: Fix some warnings/hints +* 22.10.2023: Bugfix PortExpander: beginTransmission()/endTransmission() must be balanced +* 21.10.2023: Enhanced logging: Show Loglevel +* 14.10.2023: New define NO_SDCARD, enable to start without any SD card, e.g. for a webplayer only. +* 05.10.2023: Enable "Arduino as component" by default +* 02.10.2023: Optimize Arduino as component (Disable BLE, BT-HFP & SPI ethernet) +* 25.09.2023: Handle unicode characters in NVS backup, write UTF-8 BOM in backup.txt +* 23.09.2023: Fix some log messages, update FTP & A2DP libraries +* 22.09.2023: Bugfix_same_rfid_twice init (#262) - Thanks to @Joe91 !! +* 06.09.2023: Arduino as component #261 - Thanks to @Joe91 !! +* 01.09.2023: Regression: start an initial WiFi scan on startup, fix redirect to captive portal +* 31.08.2023: Show nvs rfid assignments in web-ui & allow to delete single tags +* 31.08.2023: invoke "/rfid" endpoint (#260) +* 22.08.2023: Unify endpoints for accesspoint.html/management.html (#258) +* 15.08.2023: Web-ui: Change /restart endpoint to POST, refresh button for info & log modal +* 12.08.2023: Web-ui improvements +* 08.08.2023: check & avoid excessive "PING" calls from web-ui +* 08.08.2023: Configurable volumecurve #256, thanks to @Niko & @Wolle +* 08.08.2023: Refactor "/info" endpoint to make frontend/backend more independant #257 +* 07.08.2023: LOG message for starting mDNS service +* 04.08.2023: Support for .oga audio files (Ogg Vorbis Audio container) +* 04.08.2023: Web-UI: Replace the template processor (#253) +* 02.08.2023: Bugfix M3U-playlist + PREVIOUSTRACK, thanks to @Niko! +* 31.07.2023: Restart system: avoid peripheral power down while restarting +* 31.07.2023: increase stacksize for audio-task (audio-streams with SSL) +* 22.07.2023: New command CMD_TELL_CURRENT_TIME for announcing the current time +* 21.07.2023: Show overall playtime total in web-ui +* 21.07.2023: Refactor shutdown procedure, add new command CMD_RESTARTSYSTEM +* 20.07.2023: Allow to configure the timezone in settings.h +* 19.07.2023: Show received timestamp of NTP request in log +* 11.07.2023: Fix regression, SD-card was not working in SPI mode +* 09.07.2023: New ftp server by peterus #254. Thanks to @Joe91 for contribution !! +* 08.07.2023: Allow to compile without SD-card (webplayer only) +* 04.07.2023: fix volume jumping at startup when initVolume <> factory default (3u) +* 29.06.2023: Update PN5180 library to fix compilation with DCORE_DEBUG_LEVEL=5 +* 25.06.2023: Some spelling corrections, thanks to @SciLor ! +* 23.06.2023: CMD_TELL_IP_ADDRESS: IP as text (replace thousand separator with word "point") +* 23.06.2023: web-upload improvement (#249). Thanks to @Joe91 !! +* 23.06.2023: Refactor web: Move dependant html-code from web.cpp to management.html (#250) +* 20.06.2023: PlatformIO package 6.3.2 +* 20.06.2023: Move ScanWiFiOnStart to global WiFi configuration (#248) +* 18.06.2023: Stricter hostname validation (#246). Thanks to @SZenglein !! +* 18.06.2023: Some Web-UI improvements (#247) +* 17.06.2023: CMD_TOGGLE_WIFI_STATUS: Escape from BT-mode, WiFi cannot coexist with BT and can cause a crash +* 13.06.2023: Bluetooth configuration tab in web-interface (#244) +* 13.06.2023: Introduce new playmode "RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM" +* 07.06.2023: Use bssid to connect to best WIFI and added option to always perform a WIFI-scan #241 +* 06.06.2023: List available WiFi's in accesspoint view +* 06.06.2023: Remove support for Arduino 1.0.6 & playlist cache +* 05.06.2023: FastLED 3.6.0, PlatformIO package 6.3.1 +* 01.06.2023: Improve neopixel display in Bluetooth speaker mode (BT-Sink) (#239) +* 01.06.2023: Reduce floating the log with redundant RSSI-messages: Log RSSI value only if it has changed by > 3 dBm +* 31.05.2023: Wifi: Log BSSID in scan and connection success, Wifi fixes (#240) +* 26.05.2023: Fix compiler warnings if MQTT_ENABLE is not defined +* 26.05.2023: Error in log: `[E][Preferences.cpp:96]` remove(): nvs_erase_key fail +* 23.05.2023: Migration from single-wifi setup: Initialize "use_static_ip" +* 22.05.2023: DEV branch has stricter compiler rules, fix some of (mostly harmless) warnings +* 22.05.2023: BTSource: fix Stack canary watchpoint triggered (BtAppT) +* 19.05.2023: Show Arduino version at startup & in web-info +* 19.05.2023: Add support for saving multiple wifi networks (#221) +* 09.05.2023: Bluetooth.cpp: Fix logging crash in sink mode (#238) +* 06.05.2023: README.md: Improve formatting, style, language and content +* 03.05.2023: Add support for control LEDs to be connected at the end of the NeoPixel Ring (#198) +* 27.04.2023: Remove usage of pgmspace.h header +* 27.04.2023: Move message formatting into LogMessages +* 26.04.2023: Configure button with active HIGH +* 20.04.2023: Speedup playlist generation & file listing by factor > 20 +* 14.04.2023: Removing HAL for ESPmuse (stale development) +* 12.04.2023: Restrict value ranges to guard array access +* 11.04.2023: Merge pull request #226 from fschrempf/bugfix_rfid_assign +* 08.04.2023: Enforcing uniform codestyle +* 08.04.2023: Allow to escape from bluetooth mode with an unknown card, switch back to normal mode +* 03.04.2023: Merge pull request #223 from laszloh/bugfix/snprintf_warning, fix for snprintf trunctation warnings +* 03.04.2023: Add missing translations +* 31.03.2023: build.yml: Trigger workflow for all branches +* 27.03.2023: Merge pull request #219 from laszloh/feature/modern_cpp +* 25.03.2023: Transition to c++17 +* 24.03.2023: Lost feature „PLAY_LAST_RFID_AFTER_REBOOT“ +* 23.03.2023: Change volume animation to triggered mode +* 22.03.2023: Merge pull request #215 from fschrempf/execute-commands +* 22.03.2023: Change rotary encoder to delta mode +* 22.03.2023: Fix errorous indicator in case no audio is playing +* 22.03.2023: Fix for stuck LED-Shutdown animation +* 21.03.2023: Pause some tasks for speedup & smoother HTTP upload +* 21.03.2023: Feature: command-execution on control-site of webserver +* 21.03.2023: Merge pull request #210 from fschrempf/led_rework_cleaned_frieder +* 21.03.2023: Set default environment to arduino2 on dev-branch +* 18.03.2023: Bugfix: Corrupted files after HTTP-upload +* 18.03.2023: Major rework of LED animation task +* 13.03.2023: Fix for playing files via Web-UI for Arduino >=2.0.5 +* 13.03.2023: Fix captive portal, Add fallback text AP setup +* 10.03.2023: Merge pull request #204 from laszloh/bugfix/heap_handling +* 10.03.2023: Always free playlist memory when SdCard_ReturnPlaylist is called +* 10.03.2023: Fix heap corruption due to insufficent memory allocation +* 10.03.2023: Harmonize return value of SdCard_ReturnPlaylist +* 10.03.2023: Correct the name of the "sort" function to randomize +* 10.03.2023: Create DEV(eloper) branch + +## Version 1 + * 22.04.2021: Introduced refactoring-branch * xx.05.2021: Fixing/stabilizing code * 08.06.2021: Added global support for PA/HP-enable @@ -22,6 +153,7 @@ * 08.01.2023: New feature: `PAUSE_ON_MIN_VOLUME`. Playback is paused automatically if volume reaches minVolume (usually 0). ## Old (monolithic main.cpp) + * 11.07.2020: Added support for reversed Neopixel addressing. * 09.10.2020: mqttUser / mqttPassword can now be configured via webgui. * 16.10.2020: Added English as supported lanuage. @@ -40,7 +172,6 @@ * 18.12.2020: Added SD-MMC 1 Bit-mode (`SD_MMC_1BIT_MODE`). This mode needs one GPIO less and provides almost doubled speed (compared to SPI) for FTP-transfers (thanks @tueddy for contribution!) * 18.12.2020: Added support for RFID-reader PN5180 (`RFID_READER_TYPE_PN5180`). PN5180 has better RFID-range/sensitivity and can read ISO-15693 / iCode SLIX2-tags aka 'Tonies' (thanks @tueddy for contribution!) * 20.12.2020: Due to memory-issues with webstreams, FTP needs to be activated by pressing pause+next-button now -
More to come... * 23.12.2020: User-config is now split into general part (settings.h) and develboard-specific part (e.g. settings-lolin32.h) * 13.01.2021: Added fileexlorer to webgui (thanks @grch87 for contribution!). Now files and directories can be renamed, uploaded and deleted via webgui. * 17.01.2021: Added directive `STATIC_IP_ENABLE`: (optional) static IPv4-configuration diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 00000000..7a46da17 --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,9 @@ +dependencies: + idf: + component_hash: null + source: + type: idf + version: 4.4.4 +manifest_hash: dcf4d39b94252de130019eadceb989d72b0dbc26b552cfdcbb50f6da531d2b92 +target: esp32 +version: 1.0.0 diff --git a/gitVersion.py b/gitVersion.py index 76af4c3c..92d93579 100644 --- a/gitVersion.py +++ b/gitVersion.py @@ -21,7 +21,8 @@ TEMPLATE = """ #ifndef __GIT_REVISION_H__ #define __GIT_REVISION_H__ - constexpr const char gitRevision[] PROGMEM = "Git-revision: {git_revision}"; + constexpr const char gitRevision[] = "Git-revision: {git_revision}"; + constexpr const char gitRevShort[] = "\\"{git_revision}\\""; #endif """ diff --git a/html/accesspoint.html b/html/accesspoint.html new file mode 100644 index 00000000..53abc00c --- /dev/null +++ b/html/accesspoint.html @@ -0,0 +1,214 @@ + + + + ESPuino + + + + + + + + +
+

WiFi-configuration

+
+ :
+
+ :
+
+ :
+

+ + +

+ + + + + + +
+
+

Ready to go?

+ +
+ + + + \ No newline at end of file diff --git a/html/accesspoint_DE.html b/html/accesspoint_DE.html deleted file mode 100644 index 26d2ba3e..00000000 --- a/html/accesspoint_DE.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - WLAN-Einrichtung - - - - -
-

WLAN-Einrichtung

-
-
-
-
-
-

- -
-
-

Fertig?

- -
- - \ No newline at end of file diff --git a/html/accesspoint_EN.html b/html/accesspoint_EN.html deleted file mode 100644 index c031253b..00000000 --- a/html/accesspoint_EN.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - WiFi-configuration - - - - -
-

WiFi-configuration

-
-
-
-
-
-

- -
-
-

Ready to go?

- -
- - \ No newline at end of file diff --git a/html/js/i18next.min.js b/html/js/i18next.min.js new file mode 100644 index 00000000..2ee9cfd7 --- /dev/null +++ b/html/js/i18next.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).i18next=t()}(this,(function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(t){var n=function(t,n){if("object"!==e(t)||null===t)return t;var r=t[Symbol.toPrimitive];if(void 0!==r){var o=r.call(t,n||"default");if("object"!==e(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===n?String:Number)(t)}(t,"string");return"symbol"===e(n)?n:String(n)}function r(e,t){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:{};t(this,e),this.init(n,r)}return o(e,[{key:"init",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.prefix=t.prefix||"i18next:",this.logger=e||g,this.options=t,this.debug=t.debug}},{key:"setDebug",value:function(e){this.debug=e}},{key:"log",value:function(){for(var e=arguments.length,t=new Array(e),n=0;n1?t-1:0),r=1;r-1?e.replace(/###/g,"."):e}function o(){return!e||"string"==typeof e}for(var i="string"!=typeof t?[].concat(t):t.split(".");i.length>1;){if(o())return{};var a=r(i.shift());!e[a]&&n&&(e[a]=new n),e=Object.prototype.hasOwnProperty.call(e,a)?e[a]:{}}return o()?{}:{obj:e,k:r(i.shift())}}function O(e,t,n){var r=b(e,t,Object);r.obj[r.k]=n}function k(e,t){var n=b(e,t),r=n.obj,o=n.k;if(r)return r[o]}function w(e,t,n){var r=k(e,n);return void 0!==r?r:k(t,n)}function x(e,t,n){for(var r in t)"__proto__"!==r&&"constructor"!==r&&(r in e?"string"==typeof e[r]||e[r]instanceof String||"string"==typeof t[r]||t[r]instanceof String?n&&(e[r]=t[r]):x(e[r],t[r],n):e[r]=t[r]);return e}function S(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}var j={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};function P(e){return"string"==typeof e?e.replace(/[&<>"'\/]/g,(function(e){return j[e]})):e}var L="undefined"!=typeof window&&window.navigator&&void 0===window.navigator.userAgentData&&window.navigator.userAgent&&window.navigator.userAgent.indexOf("MSIE")>-1,R=[" ",",","?","!",";"];function N(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function C(e){for(var t=1;t2&&void 0!==arguments[2]?arguments[2]:".";if(e){if(e[t])return e[t];for(var r=t.split(n),o=e,i=0;ii+a;)a++,u=o[s=r.slice(i,i+a).join(n)];if(void 0===u)return;if(null===u)return null;if(t.endsWith(s)){if("string"==typeof u)return u;if(s&&"string"==typeof u[s])return u[s]}var c=r.slice(i+a).join(n);return c?D(u,c,n):void 0}o=o[r[i]]}return o}}var F=function(e){s(r,e);var n=E(r);function r(e){var o,a=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{ns:["translation"],defaultNS:"translation"};return t(this,r),o=n.call(this),L&&d.call(i(o)),o.data=e||{},o.options=a,void 0===o.options.keySeparator&&(o.options.keySeparator="."),void 0===o.options.ignoreJSONStructure&&(o.options.ignoreJSONStructure=!0),o}return o(r,[{key:"addNamespaces",value:function(e){this.options.ns.indexOf(e)<0&&this.options.ns.push(e)}},{key:"removeNamespaces",value:function(e){var t=this.options.ns.indexOf(e);t>-1&&this.options.ns.splice(t,1)}},{key:"getResource",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=void 0!==r.keySeparator?r.keySeparator:this.options.keySeparator,i=void 0!==r.ignoreJSONStructure?r.ignoreJSONStructure:this.options.ignoreJSONStructure,a=[e,t];n&&"string"!=typeof n&&(a=a.concat(n)),n&&"string"==typeof n&&(a=a.concat(o?n.split(o):n)),e.indexOf(".")>-1&&(a=e.split("."));var s=k(this.data,a);return s||!i||"string"!=typeof n?s:D(this.data&&this.data[e]&&this.data[e][t],n,o)}},{key:"addResource",value:function(e,t,n,r){var o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{silent:!1},i=this.options.keySeparator;void 0===i&&(i=".");var a=[e,t];n&&(a=a.concat(i?n.split(i):n)),e.indexOf(".")>-1&&(r=t,t=(a=e.split("."))[1]),this.addNamespaces(t),O(this.data,a,r),o.silent||this.emit("added",e,t,n,r)}},{key:"addResources",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{silent:!1};for(var o in n)"string"!=typeof n[o]&&"[object Array]"!==Object.prototype.toString.apply(n[o])||this.addResource(e,t,o,n[o],{silent:!0});r.silent||this.emit("added",e,t,n)}},{key:"addResourceBundle",value:function(e,t,n,r,o){var i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{silent:!1},a=[e,t];e.indexOf(".")>-1&&(r=n,n=t,t=(a=e.split("."))[1]),this.addNamespaces(t);var s=k(this.data,a)||{};r?x(s,n,o):s=C(C({},s),n),O(this.data,a,s),i.silent||this.emit("added",e,t,n)}},{key:"removeResourceBundle",value:function(e,t){this.hasResourceBundle(e,t)&&delete this.data[e][t],this.removeNamespaces(t),this.emit("removed",e,t)}},{key:"hasResourceBundle",value:function(e,t){return void 0!==this.getResource(e,t)}},{key:"getResourceBundle",value:function(e,t){return t||(t=this.options.defaultNS),"v1"===this.options.compatibilityAPI?C(C({},{}),this.getResource(e,t)):this.getResource(e,t)}},{key:"getDataByLanguage",value:function(e){return this.data[e]}},{key:"hasLanguageSomeTranslations",value:function(e){var t=this.getDataByLanguage(e);return!!(t&&Object.keys(t)||[]).find((function(e){return t[e]&&Object.keys(t[e]).length>0}))}},{key:"toJSON",value:function(){return this.data}}]),r}(d),I={processors:{},addPostProcessor:function(e){this.processors[e.name]=e},handle:function(e,t,n,r,o){var i=this;return e.forEach((function(e){i.processors[e]&&(t=i.processors[e].process(t,n,r,o))})),t}};function A(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function T(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{};return t(this,a),n=r.call(this),L&&d.call(i(n)),m(["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat","utils"],e,i(n)),n.options=o,void 0===n.options.keySeparator&&(n.options.keySeparator="."),n.logger=h.create("translator"),n}return o(a,[{key:"changeLanguage",value:function(e){e&&(this.language=e)}},{key:"exists",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}};if(null==e)return!1;var n=this.resolve(e,t);return n&&void 0!==n.res}},{key:"extractFromKey",value:function(e,t){var n=void 0!==t.nsSeparator?t.nsSeparator:this.options.nsSeparator;void 0===n&&(n=":");var r=void 0!==t.keySeparator?t.keySeparator:this.options.keySeparator,o=t.ns||this.options.defaultNS||[],i=n&&e.indexOf(n)>-1,a=!(this.options.userDefinedKeySeparator||t.keySeparator||this.options.userDefinedNsSeparator||t.nsSeparator||function(e,t,n){t=t||"",n=n||"";var r=R.filter((function(e){return t.indexOf(e)<0&&n.indexOf(e)<0}));if(0===r.length)return!0;var o=new RegExp("(".concat(r.map((function(e){return"?"===e?"\\?":e})).join("|"),")")),i=!o.test(e);if(!i){var a=e.indexOf(n);a>0&&!o.test(e.substring(0,a))&&(i=!0)}return i}(e,n,r));if(i&&!a){var s=e.match(this.interpolator.nestingRegexp);if(s&&s.length>0)return{key:e,namespaces:o};var u=e.split(n);(n!==r||n===r&&this.options.ns.indexOf(u[0])>-1)&&(o=u.shift()),e=u.join(r)}return"string"==typeof o&&(o=[o]),{key:e,namespaces:o}}},{key:"translate",value:function(t,n,r){var o=this;if("object"!==e(n)&&this.options.overloadTranslationOptionHandler&&(n=this.options.overloadTranslationOptionHandler(arguments)),n||(n={}),null==t)return"";Array.isArray(t)||(t=[String(t)]);var i=void 0!==n.returnDetails?n.returnDetails:this.options.returnDetails,s=void 0!==n.keySeparator?n.keySeparator:this.options.keySeparator,u=this.extractFromKey(t[t.length-1],n),c=u.key,l=u.namespaces,f=l[l.length-1],p=n.lng||this.language,g=n.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if(p&&"cimode"===p.toLowerCase()){if(g){var h=n.nsSeparator||this.options.nsSeparator;return i?(d.res="".concat(f).concat(h).concat(c),d):"".concat(f).concat(h).concat(c)}return i?(d.res=c,d):c}var d=this.resolve(t,n),v=d&&d.res,y=d&&d.usedKey||c,m=d&&d.exactUsedKey||c,b=Object.prototype.toString.apply(v),O=["[object Number]","[object Function]","[object RegExp]"],k=void 0!==n.joinArrays?n.joinArrays:this.options.joinArrays,w=!this.i18nFormat||this.i18nFormat.handleAsObject,x="string"!=typeof v&&"boolean"!=typeof v&&"number"!=typeof v;if(w&&v&&x&&O.indexOf(b)<0&&("string"!=typeof k||"[object Array]"!==b)){if(!n.returnObjects&&!this.options.returnObjects){this.options.returnedObjectHandler||this.logger.warn("accessing an object - but returnObjects options is not enabled!");var S=this.options.returnedObjectHandler?this.options.returnedObjectHandler(y,v,T(T({},n),{},{ns:l})):"key '".concat(c," (").concat(this.language,")' returned an object instead of string.");return i?(d.res=S,d):S}if(s){var j="[object Array]"===b,P=j?[]:{},L=j?m:y;for(var R in v)if(Object.prototype.hasOwnProperty.call(v,R)){var N="".concat(L).concat(s).concat(R);P[R]=this.translate(N,T(T({},n),{joinArrays:!1,ns:l})),P[R]===N&&(P[R]=v[R])}v=P}}else if(w&&"string"==typeof k&&"[object Array]"===b)(v=v.join(k))&&(v=this.extendTranslation(v,t,n,r));else{var C=!1,E=!1,D=void 0!==n.count&&"string"!=typeof n.count,F=a.hasDefaultValue(n),I=D?this.pluralResolver.getSuffix(p,n.count,n):"",A=n["defaultValue".concat(I)]||n.defaultValue;!this.isValidLookup(v)&&F&&(C=!0,v=A),this.isValidLookup(v)||(E=!0,v=c);var V=n.missingKeyNoValueFallbackToKey||this.options.missingKeyNoValueFallbackToKey,U=V&&E?void 0:v,K=F&&A!==v&&this.options.updateMissing;if(E||C||K){if(this.logger.log(K?"updateKey":"missingKey",p,f,c,K?A:v),s){var B=this.resolve(c,T(T({},n),{},{keySeparator:!1}));B&&B.res&&this.logger.warn("Seems the loaded translations were in flat JSON format instead of nested. Either set keySeparator: false on init or make sure your translations are published in nested format.")}var M=[],H=this.languageUtils.getFallbackCodes(this.options.fallbackLng,n.lng||this.language);if("fallback"===this.options.saveMissingTo&&H&&H[0])for(var z=0;z1&&void 0!==arguments[1]?arguments[1]:{};return"string"==typeof e&&(e=[e]),e.forEach((function(e){if(!a.isValidLookup(t)){var u=a.extractFromKey(e,s),c=u.key;n=c;var l=u.namespaces;a.options.fallbackNS&&(l=l.concat(a.options.fallbackNS));var f=void 0!==s.count&&"string"!=typeof s.count,p=f&&!s.ordinal&&0===s.count&&a.pluralResolver.shouldUseIntlApi(),g=void 0!==s.context&&("string"==typeof s.context||"number"==typeof s.context)&&""!==s.context,h=s.lngs?s.lngs:a.languageUtils.toResolveHierarchy(s.lng||a.language,s.fallbackLng);l.forEach((function(e){a.isValidLookup(t)||(i=e,!U["".concat(h[0],"-").concat(e)]&&a.utils&&a.utils.hasLoadedNamespace&&!a.utils.hasLoadedNamespace(i)&&(U["".concat(h[0],"-").concat(e)]=!0,a.logger.warn('key "'.concat(n,'" for languages "').concat(h.join(", "),'" won\'t get resolved as namespace "').concat(i,'" was not yet loaded'),"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!")),h.forEach((function(n){if(!a.isValidLookup(t)){o=n;var i,u=[c];if(a.i18nFormat&&a.i18nFormat.addLookupKeys)a.i18nFormat.addLookupKeys(u,c,n,e,s);else{var l;f&&(l=a.pluralResolver.getSuffix(n,s.count,s));var h="".concat(a.options.pluralSeparator,"zero");if(f&&(u.push(c+l),p&&u.push(c+h)),g){var d="".concat(c).concat(a.options.contextSeparator).concat(s.context);u.push(d),f&&(u.push(d+l),p&&u.push(d+h))}}for(;i=u.pop();)a.isValidLookup(t)||(r=i,t=a.getResource(n,e,i,s))}})))}))}})),{res:t,usedKey:n,exactUsedKey:r,usedLng:o,usedNS:i}}},{key:"isValidLookup",value:function(e){return!(void 0===e||!this.options.returnNull&&null===e||!this.options.returnEmptyString&&""===e)}},{key:"getResource",value:function(e,t,n){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return this.i18nFormat&&this.i18nFormat.getResource?this.i18nFormat.getResource(e,t,n,r):this.resourceStore.getResource(e,t,n,r)}}],[{key:"hasDefaultValue",value:function(e){for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t)&&"defaultValue"===t.substring(0,"defaultValue".length)&&void 0!==e[t])return!0;return!1}}]),a}(d);function B(e){return e.charAt(0).toUpperCase()+e.slice(1)}var M=function(){function e(n){t(this,e),this.options=n,this.supportedLngs=this.options.supportedLngs||!1,this.logger=h.create("languageUtils")}return o(e,[{key:"getScriptPartFromCode",value:function(e){if(!e||e.indexOf("-")<0)return null;var t=e.split("-");return 2===t.length?null:(t.pop(),"x"===t[t.length-1].toLowerCase()?null:this.formatLanguageCode(t.join("-")))}},{key:"getLanguagePartFromCode",value:function(e){if(!e||e.indexOf("-")<0)return e;var t=e.split("-");return this.formatLanguageCode(t[0])}},{key:"formatLanguageCode",value:function(e){if("string"==typeof e&&e.indexOf("-")>-1){var t=["hans","hant","latn","cyrl","cans","mong","arab"],n=e.split("-");return this.options.lowerCaseLng?n=n.map((function(e){return e.toLowerCase()})):2===n.length?(n[0]=n[0].toLowerCase(),n[1]=n[1].toUpperCase(),t.indexOf(n[1].toLowerCase())>-1&&(n[1]=B(n[1].toLowerCase()))):3===n.length&&(n[0]=n[0].toLowerCase(),2===n[1].length&&(n[1]=n[1].toUpperCase()),"sgn"!==n[0]&&2===n[2].length&&(n[2]=n[2].toUpperCase()),t.indexOf(n[1].toLowerCase())>-1&&(n[1]=B(n[1].toLowerCase())),t.indexOf(n[2].toLowerCase())>-1&&(n[2]=B(n[2].toLowerCase()))),n.join("-")}return this.options.cleanCode||this.options.lowerCaseLng?e.toLowerCase():e}},{key:"isSupportedCode",value:function(e){return("languageOnly"===this.options.load||this.options.nonExplicitSupportedLngs)&&(e=this.getLanguagePartFromCode(e)),!this.supportedLngs||!this.supportedLngs.length||this.supportedLngs.indexOf(e)>-1}},{key:"getBestMatchFromCodes",value:function(e){var t,n=this;return e?(e.forEach((function(e){if(!t){var r=n.formatLanguageCode(e);n.options.supportedLngs&&!n.isSupportedCode(r)||(t=r)}})),!t&&this.options.supportedLngs&&e.forEach((function(e){if(!t){var r=n.getLanguagePartFromCode(e);if(n.isSupportedCode(r))return t=r;t=n.options.supportedLngs.find((function(e){if(0===e.indexOf(r))return e}))}})),t||(t=this.getFallbackCodes(this.options.fallbackLng)[0]),t):null}},{key:"getFallbackCodes",value:function(e,t){if(!e)return[];if("function"==typeof e&&(e=e(t)),"string"==typeof e&&(e=[e]),"[object Array]"===Object.prototype.toString.apply(e))return e;if(!t)return e.default||[];var n=e[t];return n||(n=e[this.getScriptPartFromCode(t)]),n||(n=e[this.formatLanguageCode(t)]),n||(n=e[this.getLanguagePartFromCode(t)]),n||(n=e.default),n||[]}},{key:"toResolveHierarchy",value:function(e,t){var n=this,r=this.getFallbackCodes(t||this.options.fallbackLng||[],e),o=[],i=function(e){e&&(n.isSupportedCode(e)?o.push(e):n.logger.warn("rejecting language code not found in supportedLngs: ".concat(e)))};return"string"==typeof e&&e.indexOf("-")>-1?("languageOnly"!==this.options.load&&i(this.formatLanguageCode(e)),"languageOnly"!==this.options.load&&"currentOnly"!==this.options.load&&i(this.getScriptPartFromCode(e)),"currentOnly"!==this.options.load&&i(this.getLanguagePartFromCode(e))):"string"==typeof e&&i(this.formatLanguageCode(e)),r.forEach((function(e){o.indexOf(e)<0&&i(n.formatLanguageCode(e))})),o}}]),e}(),H=[{lngs:["ach","ak","am","arn","br","fil","gun","ln","mfe","mg","mi","oc","pt","pt-BR","tg","tl","ti","tr","uz","wa"],nr:[1,2],fc:1},{lngs:["af","an","ast","az","bg","bn","ca","da","de","dev","el","en","eo","es","et","eu","fi","fo","fur","fy","gl","gu","ha","hi","hu","hy","ia","it","kk","kn","ku","lb","mai","ml","mn","mr","nah","nap","nb","ne","nl","nn","no","nso","pa","pap","pms","ps","pt-PT","rm","sco","se","si","so","son","sq","sv","sw","ta","te","tk","ur","yo"],nr:[1,2],fc:2},{lngs:["ay","bo","cgg","fa","ht","id","ja","jbo","ka","km","ko","ky","lo","ms","sah","su","th","tt","ug","vi","wo","zh"],nr:[1],fc:3},{lngs:["be","bs","cnr","dz","hr","ru","sr","uk"],nr:[1,2,5],fc:4},{lngs:["ar"],nr:[0,1,2,3,11,100],fc:5},{lngs:["cs","sk"],nr:[1,2,5],fc:6},{lngs:["csb","pl"],nr:[1,2,5],fc:7},{lngs:["cy"],nr:[1,2,3,8],fc:8},{lngs:["fr"],nr:[1,2],fc:9},{lngs:["ga"],nr:[1,2,3,7,11],fc:10},{lngs:["gd"],nr:[1,2,3,20],fc:11},{lngs:["is"],nr:[1,2],fc:12},{lngs:["jv"],nr:[0,1],fc:13},{lngs:["kw"],nr:[1,2,3,4],fc:14},{lngs:["lt"],nr:[1,2,10],fc:15},{lngs:["lv"],nr:[1,2,0],fc:16},{lngs:["mk"],nr:[1,2],fc:17},{lngs:["mnk"],nr:[0,1,2],fc:18},{lngs:["mt"],nr:[1,2,11,20],fc:19},{lngs:["or"],nr:[2,1],fc:2},{lngs:["ro"],nr:[1,2,20],fc:20},{lngs:["sl"],nr:[5,1,2,3],fc:21},{lngs:["he","iw"],nr:[1,2,20,21],fc:22}],z={1:function(e){return Number(e>1)},2:function(e){return Number(1!=e)},3:function(e){return 0},4:function(e){return Number(e%10==1&&e%100!=11?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2)},5:function(e){return Number(0==e?0:1==e?1:2==e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5)},6:function(e){return Number(1==e?0:e>=2&&e<=4?1:2)},7:function(e){return Number(1==e?0:e%10>=2&&e%10<=4&&(e%100<10||e%100>=20)?1:2)},8:function(e){return Number(1==e?0:2==e?1:8!=e&&11!=e?2:3)},9:function(e){return Number(e>=2)},10:function(e){return Number(1==e?0:2==e?1:e<7?2:e<11?3:4)},11:function(e){return Number(1==e||11==e?0:2==e||12==e?1:e>2&&e<20?2:3)},12:function(e){return Number(e%10!=1||e%100==11)},13:function(e){return Number(0!==e)},14:function(e){return Number(1==e?0:2==e?1:3==e?2:3)},15:function(e){return Number(e%10==1&&e%100!=11?0:e%10>=2&&(e%100<10||e%100>=20)?1:2)},16:function(e){return Number(e%10==1&&e%100!=11?0:0!==e?1:2)},17:function(e){return Number(1==e||e%10==1&&e%100!=11?0:1)},18:function(e){return Number(0==e?0:1==e?1:2)},19:function(e){return Number(1==e?0:0==e||e%100>1&&e%100<11?1:e%100>10&&e%100<20?2:3)},20:function(e){return Number(1==e?0:0==e||e%100>0&&e%100<20?1:2)},21:function(e){return Number(e%100==1?1:e%100==2?2:e%100==3||e%100==4?3:0)},22:function(e){return Number(1==e?0:2==e?1:(e<0||e>10)&&e%10==0?2:3)}},J=["v1","v2","v3"],_={zero:0,one:1,two:2,few:3,many:4,other:5};function q(){var e={};return H.forEach((function(t){t.lngs.forEach((function(n){e[n]={numbers:t.nr,plurals:z[t.fc]}}))})),e}var $=function(){function e(n){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t(this,e),this.languageUtils=n,this.options=r,this.logger=h.create("pluralResolver"),this.options.compatibilityJSON&&"v4"!==this.options.compatibilityJSON||"undefined"!=typeof Intl&&Intl.PluralRules||(this.options.compatibilityJSON="v3",this.logger.error("Your environment seems not to be Intl API compatible, use an Intl.PluralRules polyfill. Will fallback to the compatibilityJSON v3 format handling.")),this.rules=q()}return o(e,[{key:"addRule",value:function(e,t){this.rules[e]=t}},{key:"getRule",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(this.shouldUseIntlApi())try{return new Intl.PluralRules(e,{type:t.ordinal?"ordinal":"cardinal"})}catch(e){return}return this.rules[e]||this.rules[this.languageUtils.getLanguagePartFromCode(e)]}},{key:"needsPlural",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=this.getRule(e,t);return this.shouldUseIntlApi()?n&&n.resolvedOptions().pluralCategories.length>1:n&&n.numbers.length>1}},{key:"getPluralFormsOfKey",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this.getSuffixes(e,n).map((function(e){return"".concat(t).concat(e)}))}},{key:"getSuffixes",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=this.getRule(e,n);return r?this.shouldUseIntlApi()?r.resolvedOptions().pluralCategories.sort((function(e,t){return _[e]-_[t]})).map((function(e){return"".concat(t.options.prepend).concat(e)})):r.numbers.map((function(r){return t.getSuffix(e,r,n)})):[]}},{key:"getSuffix",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=this.getRule(e,n);return r?this.shouldUseIntlApi()?"".concat(this.options.prepend).concat(r.select(t)):this.getSuffixRetroCompatible(r,t):(this.logger.warn("no plural rule found for: ".concat(e)),"")}},{key:"getSuffixRetroCompatible",value:function(e,t){var n=this,r=e.noAbs?e.plurals(t):e.plurals(Math.abs(t)),o=e.numbers[r];this.options.simplifyPluralSuffix&&2===e.numbers.length&&1===e.numbers[0]&&(2===o?o="plural":1===o&&(o=""));var i=function(){return n.options.prepend&&o.toString()?n.options.prepend+o.toString():o.toString()};return"v1"===this.options.compatibilityJSON?1===o?"":"number"==typeof o?"_plural_".concat(o.toString()):i():"v2"===this.options.compatibilityJSON||this.options.simplifyPluralSuffix&&2===e.numbers.length&&1===e.numbers[0]?i():this.options.prepend&&r.toString()?this.options.prepend+r.toString():r.toString()}},{key:"shouldUseIntlApi",value:function(){return!J.includes(this.options.compatibilityJSON)}}]),e}();function W(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function Y(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};t(this,e),this.logger=h.create("interpolator"),this.options=n,this.format=n.interpolation&&n.interpolation.format||function(e){return e},this.init(n)}return o(e,[{key:"init",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};e.interpolation||(e.interpolation={escapeValue:!0});var t=e.interpolation;this.escape=void 0!==t.escape?t.escape:P,this.escapeValue=void 0===t.escapeValue||t.escapeValue,this.useRawValueToEscape=void 0!==t.useRawValueToEscape&&t.useRawValueToEscape,this.prefix=t.prefix?S(t.prefix):t.prefixEscaped||"{{",this.suffix=t.suffix?S(t.suffix):t.suffixEscaped||"}}",this.formatSeparator=t.formatSeparator?t.formatSeparator:t.formatSeparator||",",this.unescapePrefix=t.unescapeSuffix?"":t.unescapePrefix||"-",this.unescapeSuffix=this.unescapePrefix?"":t.unescapeSuffix||"",this.nestingPrefix=t.nestingPrefix?S(t.nestingPrefix):t.nestingPrefixEscaped||S("$t("),this.nestingSuffix=t.nestingSuffix?S(t.nestingSuffix):t.nestingSuffixEscaped||S(")"),this.nestingOptionsSeparator=t.nestingOptionsSeparator?t.nestingOptionsSeparator:t.nestingOptionsSeparator||",",this.maxReplaces=t.maxReplaces?t.maxReplaces:1e3,this.alwaysFormat=void 0!==t.alwaysFormat&&t.alwaysFormat,this.resetRegExp()}},{key:"reset",value:function(){this.options&&this.init(this.options)}},{key:"resetRegExp",value:function(){var e="".concat(this.prefix,"(.+?)").concat(this.suffix);this.regexp=new RegExp(e,"g");var t="".concat(this.prefix).concat(this.unescapePrefix,"(.+?)").concat(this.unescapeSuffix).concat(this.suffix);this.regexpUnescape=new RegExp(t,"g");var n="".concat(this.nestingPrefix,"(.+?)").concat(this.nestingSuffix);this.nestingRegexp=new RegExp(n,"g")}},{key:"interpolate",value:function(e,t,n,r){var o,i,a,s=this,u=this.options&&this.options.interpolation&&this.options.interpolation.defaultVariables||{};function c(e){return e.replace(/\$/g,"$$$$")}var l=function(e){if(e.indexOf(s.formatSeparator)<0){var o=w(t,u,e);return s.alwaysFormat?s.format(o,void 0,n,Y(Y(Y({},r),t),{},{interpolationkey:e})):o}var i=e.split(s.formatSeparator),a=i.shift().trim(),c=i.join(s.formatSeparator).trim();return s.format(w(t,u,a),c,n,Y(Y(Y({},r),t),{},{interpolationkey:a}))};this.resetRegExp();var f=r&&r.missingInterpolationHandler||this.options.missingInterpolationHandler,p=r&&r.interpolation&&void 0!==r.interpolation.skipOnVariables?r.interpolation.skipOnVariables:this.options.interpolation.skipOnVariables;return[{regex:this.regexpUnescape,safeValue:function(e){return c(e)}},{regex:this.regexp,safeValue:function(e){return s.escapeValue?c(s.escape(e)):c(e)}}].forEach((function(t){for(a=0;o=t.regex.exec(e);){var n=o[1].trim();if(void 0===(i=l(n)))if("function"==typeof f){var u=f(e,o,r);i="string"==typeof u?u:""}else if(r&&r.hasOwnProperty(n))i="";else{if(p){i=o[0];continue}s.logger.warn("missed to pass in variable ".concat(n," for interpolating ").concat(e)),i=""}else"string"==typeof i||s.useRawValueToEscape||(i=y(i));var c=t.safeValue(i);if(e=e.replace(o[0],c),p?(t.regex.lastIndex+=i.length,t.regex.lastIndex-=o[0].length):t.regex.lastIndex=0,++a>=s.maxReplaces)break}})),e}},{key:"nest",value:function(e,t){var n,r,o,i=this,a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};function s(e,t){var n=this.nestingOptionsSeparator;if(e.indexOf(n)<0)return e;var r=e.split(new RegExp("".concat(n,"[ ]*{"))),i="{".concat(r[1]);e=r[0];var a=(i=this.interpolate(i,o)).match(/'/g),s=i.match(/"/g);(a&&a.length%2==0&&!s||s.length%2!=0)&&(i=i.replace(/'/g,'"'));try{o=JSON.parse(i),t&&(o=Y(Y({},t),o))}catch(t){return this.logger.warn("failed parsing options string in nesting for key ".concat(e),t),"".concat(e).concat(n).concat(i)}return delete o.defaultValue,e}for(;n=this.nestingRegexp.exec(e);){var u=[];(o=(o=Y({},a)).replace&&"string"!=typeof o.replace?o.replace:o).applyPostProcessor=!1,delete o.defaultValue;var c=!1;if(-1!==n[0].indexOf(this.formatSeparator)&&!/{.*}/.test(n[1])){var l=n[1].split(this.formatSeparator).map((function(e){return e.trim()}));n[1]=l.shift(),u=l,c=!0}if((r=t(s.call(this,n[1].trim(),o),o))&&n[0]===e&&"string"!=typeof r)return r;"string"!=typeof r&&(r=y(r)),r||(this.logger.warn("missed to resolve ".concat(n[1]," for nesting ").concat(e)),r=""),c&&(r=u.reduce((function(e,t){return i.format(e,t,a.lng,Y(Y({},a),{},{interpolationkey:n[1].trim()}))}),r.trim())),e=e.replace(n[0],r),this.regexp.lastIndex=0}return e}}]),e}();function Q(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:{};t(this,e),this.logger=h.create("formatter"),this.options=n,this.formats={number:te((function(e,t){var n=new Intl.NumberFormat(e,t);return function(e){return n.format(e)}})),currency:te((function(e,t){var n=new Intl.NumberFormat(e,ee(ee({},t),{},{style:"currency"}));return function(e){return n.format(e)}})),datetime:te((function(e,t){var n=new Intl.DateTimeFormat(e,ee({},t));return function(e){return n.format(e)}})),relativetime:te((function(e,t){var n=new Intl.RelativeTimeFormat(e,ee({},t));return function(e){return n.format(e,t.range||"day")}})),list:te((function(e,t){var n=new Intl.ListFormat(e,ee({},t));return function(e){return n.format(e)}}))},this.init(n)}return o(e,[{key:"init",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}},n=t.interpolation;this.formatSeparator=n.formatSeparator?n.formatSeparator:n.formatSeparator||","}},{key:"add",value:function(e,t){this.formats[e.toLowerCase().trim()]=t}},{key:"addCached",value:function(e,t){this.formats[e.toLowerCase().trim()]=te(t)}},{key:"format",value:function(e,t,n,r){var o=this;return t.split(this.formatSeparator).reduce((function(e,t){var i=function(e){var t=e.toLowerCase().trim(),n={};if(e.indexOf("(")>-1){var r=e.split("(");t=r[0].toLowerCase().trim();var o=r[1].substring(0,r[1].length-1);if("currency"===t&&o.indexOf(":")<0)n.currency||(n.currency=o.trim());else if("relativetime"===t&&o.indexOf(":")<0)n.range||(n.range=o.trim());else{o.split(";").forEach((function(e){if(e){var t=X(e.split(":")),r=t[0],o=t.slice(1).join(":").trim().replace(/^'+|'+$/g,"");n[r.trim()]||(n[r.trim()]=o),"false"===o&&(n[r.trim()]=!1),"true"===o&&(n[r.trim()]=!0),isNaN(o)||(n[r.trim()]=parseInt(o,10))}}))}}return{formatName:t,formatOptions:n}}(t),a=i.formatName,s=i.formatOptions;if(o.formats[a]){var u=e;try{var c=r&&r.formatParams&&r.formatParams[r.interpolationkey]||{},l=c.locale||c.lng||r.locale||r.lng||n;u=o.formats[a](e,l,ee(ee(ee({},s),r),c))}catch(e){o.logger.warn(e)}return u}return o.logger.warn("there was no format function for ".concat(a)),e}),e)}}]),e}();function re(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function oe(e){for(var t=1;t3&&void 0!==arguments[3]?arguments[3]:{};return t(this,r),s=n.call(this),L&&d.call(i(s)),s.backend=e,s.store=o,s.services=a,s.languageUtils=a.languageUtils,s.options=u,s.logger=h.create("backendConnector"),s.waitingReads=[],s.maxParallelReads=u.maxParallelReads||10,s.readingCalls=0,s.maxRetries=u.maxRetries>=0?u.maxRetries:5,s.retryTimeout=u.retryTimeout>=1?u.retryTimeout:350,s.state={},s.queue=[],s.backend&&s.backend.init&&s.backend.init(a,u.backend,u),s}return o(r,[{key:"queueLoad",value:function(e,t,n,r){var o=this,i={},a={},s={},u={};return e.forEach((function(e){var r=!0;t.forEach((function(t){var s="".concat(e,"|").concat(t);!n.reload&&o.store.hasResourceBundle(e,t)?o.state[s]=2:o.state[s]<0||(1===o.state[s]?void 0===a[s]&&(a[s]=!0):(o.state[s]=1,r=!1,void 0===a[s]&&(a[s]=!0),void 0===i[s]&&(i[s]=!0),void 0===u[t]&&(u[t]=!0)))})),r||(s[e]=!0)})),(Object.keys(i).length||Object.keys(a).length)&&this.queue.push({pending:a,pendingCount:Object.keys(a).length,loaded:{},errors:[],callback:r}),{toLoad:Object.keys(i),pending:Object.keys(a),toLoadLanguages:Object.keys(s),toLoadNamespaces:Object.keys(u)}}},{key:"loaded",value:function(e,t,n){var r=e.split("|"),o=r[0],i=r[1];t&&this.emit("failedLoading",o,i,t),n&&this.store.addResourceBundle(o,i,n),this.state[e]=t?-1:2;var a={};this.queue.forEach((function(n){var r,s,u,c,l,f;r=n.loaded,s=i,c=b(r,[o],Object),l=c.obj,f=c.k,l[f]=l[f]||[],u&&(l[f]=l[f].concat(s)),u||l[f].push(s),function(e,t){void 0!==e.pending[t]&&(delete e.pending[t],e.pendingCount--)}(n,e),t&&n.errors.push(t),0!==n.pendingCount||n.done||(Object.keys(n.loaded).forEach((function(e){a[e]||(a[e]={});var t=n.loaded[e];t.length&&t.forEach((function(t){void 0===a[e][t]&&(a[e][t]=!0)}))})),n.done=!0,n.errors.length?n.callback(n.errors):n.callback())})),this.emit("loaded",a),this.queue=this.queue.filter((function(e){return!e.done}))}},{key:"read",value:function(e,t,n){var r=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:this.retryTimeout,a=arguments.length>5?arguments[5]:void 0;if(!e.length)return a(null,{});if(this.readingCalls>=this.maxParallelReads)this.waitingReads.push({lng:e,ns:t,fcName:n,tried:o,wait:i,callback:a});else{this.readingCalls++;var s=function(s,u){if(r.readingCalls--,r.waitingReads.length>0){var c=r.waitingReads.shift();r.read(c.lng,c.ns,c.fcName,c.tried,c.wait,c.callback)}s&&u&&o2&&void 0!==arguments[2]?arguments[2]:{},o=arguments.length>3?arguments[3]:void 0;if(!this.backend)return this.logger.warn("No backend was added via i18next.use. Will not load resources."),o&&o();"string"==typeof e&&(e=this.languageUtils.toResolveHierarchy(e)),"string"==typeof t&&(t=[t]);var i=this.queueLoad(e,t,r,o);if(!i.toLoad.length)return i.pending.length||o(),null;i.toLoad.forEach((function(e){n.loadOne(e)}))}},{key:"load",value:function(e,t,n){this.prepareLoading(e,t,{},n)}},{key:"reload",value:function(e,t,n){this.prepareLoading(e,t,{reload:!0},n)}},{key:"loadOne",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",r=e.split("|"),o=r[0],i=r[1];this.read(o,i,"read",void 0,void 0,(function(r,a){r&&t.logger.warn("".concat(n,"loading namespace ").concat(i," for language ").concat(o," failed"),r),!r&&a&&t.logger.log("".concat(n,"loaded namespace ").concat(i," for language ").concat(o),a),t.loaded(e,r,a)}))}},{key:"saveMissing",value:function(e,t,n,r,o){var i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{},a=arguments.length>6&&void 0!==arguments[6]?arguments[6]:function(){};if(this.services.utils&&this.services.utils.hasLoadedNamespace&&!this.services.utils.hasLoadedNamespace(t))this.logger.warn('did not save key "'.concat(n,'" as the namespace "').concat(t,'" was not yet loaded'),"This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!");else if(null!=n&&""!==n){if(this.backend&&this.backend.create){var s=oe(oe({},i),{},{isUpdate:o}),u=this.backend.create.bind(this.backend);if(u.length<6)try{var c;(c=5===u.length?u(e,t,n,r,s):u(e,t,n,r))&&"function"==typeof c.then?c.then((function(e){return a(null,e)})).catch(a):a(null,c)}catch(e){a(e)}else u(e,t,n,r,a,s)}e&&e[0]&&this.store.addResource(e[0],t,n,r)}}}]),r}(d);function se(){return{debug:!1,initImmediate:!0,ns:["translation"],defaultNS:["translation"],fallbackLng:["dev"],fallbackNS:!1,supportedLngs:!1,nonExplicitSupportedLngs:!1,load:"all",preload:!1,simplifyPluralSuffix:!0,keySeparator:".",nsSeparator:":",pluralSeparator:"_",contextSeparator:"_",partialBundledLanguages:!1,saveMissing:!1,updateMissing:!1,saveMissingTo:"fallback",saveMissingPlurals:!0,missingKeyHandler:!1,missingInterpolationHandler:!1,postProcess:!1,postProcessPassResolved:!1,returnNull:!0,returnEmptyString:!0,returnObjects:!1,joinArrays:!1,returnedObjectHandler:!1,parseMissingKeyHandler:!1,appendNamespaceToMissingKey:!1,appendNamespaceToCIMode:!1,overloadTranslationOptionHandler:function(t){var n={};if("object"===e(t[1])&&(n=t[1]),"string"==typeof t[1]&&(n.defaultValue=t[1]),"string"==typeof t[2]&&(n.tDescription=t[2]),"object"===e(t[2])||"object"===e(t[3])){var r=t[3]||t[2];Object.keys(r).forEach((function(e){n[e]=r[e]}))}return n},interpolation:{escapeValue:!0,format:function(e,t,n,r){return e},prefix:"{{",suffix:"}}",formatSeparator:",",unescapePrefix:"-",nestingPrefix:"$t(",nestingSuffix:")",nestingOptionsSeparator:",",maxReplaces:1e3,skipOnVariables:!0}}}function ue(e){return"string"==typeof e.ns&&(e.ns=[e.ns]),"string"==typeof e.fallbackLng&&(e.fallbackLng=[e.fallbackLng]),"string"==typeof e.fallbackNS&&(e.fallbackNS=[e.fallbackNS]),e.supportedLngs&&e.supportedLngs.indexOf("cimode")<0&&(e.supportedLngs=e.supportedLngs.concat(["cimode"])),e}function ce(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function le(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1?arguments[1]:void 0;if(t(this,a),e=r.call(this),L&&d.call(i(e)),e.options=ue(n),e.services={},e.logger=h,e.modules={external:[]},ge(i(e)),o&&!e.isInitialized&&!n.isClone){if(!e.options.initImmediate)return e.init(n,o),u(e,i(e));setTimeout((function(){e.init(n,o)}),0)}return e}return o(a,[{key:"init",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1?arguments[1]:void 0;"function"==typeof t&&(n=t,t={}),!t.defaultNS&&!1!==t.defaultNS&&t.ns&&("string"==typeof t.ns?t.defaultNS=t.ns:t.ns.indexOf("translation")<0&&(t.defaultNS=t.ns[0]));var r=se();function o(e){return e?"function"==typeof e?new e:e:null}if(this.options=le(le(le({},r),this.options),ue(t)),"v1"!==this.options.compatibilityAPI&&(this.options.interpolation=le(le({},r.interpolation),this.options.interpolation)),void 0!==t.keySeparator&&(this.options.userDefinedKeySeparator=t.keySeparator),void 0!==t.nsSeparator&&(this.options.userDefinedNsSeparator=t.nsSeparator),!this.options.isClone){var i;this.modules.logger?h.init(o(this.modules.logger),this.options):h.init(null,this.options),this.modules.formatter?i=this.modules.formatter:"undefined"!=typeof Intl&&(i=ne);var a=new M(this.options);this.store=new F(this.options.resources,this.options);var s=this.services;s.logger=h,s.resourceStore=this.store,s.languageUtils=a,s.pluralResolver=new $(a,{prepend:this.options.pluralSeparator,compatibilityJSON:this.options.compatibilityJSON,simplifyPluralSuffix:this.options.simplifyPluralSuffix}),!i||this.options.interpolation.format&&this.options.interpolation.format!==r.interpolation.format||(s.formatter=o(i),s.formatter.init(s,this.options),this.options.interpolation.format=s.formatter.format.bind(s.formatter)),s.interpolator=new G(this.options),s.utils={hasLoadedNamespace:this.hasLoadedNamespace.bind(this)},s.backendConnector=new ae(o(this.modules.backend),s.resourceStore,s,this.options),s.backendConnector.on("*",(function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o1?n-1:0),o=1;o0&&"dev"!==u[0]&&(this.options.lng=u[0])}this.services.languageDetector||this.options.lng||this.logger.warn("init: no languageDetector is used and no lng is defined");var c=["getResource","hasResourceBundle","getResourceBundle","getDataByLanguage"];c.forEach((function(t){e[t]=function(){var n;return(n=e.store)[t].apply(n,arguments)}}));var l=["addResource","addResources","addResourceBundle","removeResourceBundle"];l.forEach((function(t){e[t]=function(){var n;return(n=e.store)[t].apply(n,arguments),e}}));var f=v(),p=function(){var t=function(t,r){e.isInitialized&&!e.initializedStoreOnce&&e.logger.warn("init: i18next is already initialized. You should call init just once!"),e.isInitialized=!0,e.options.isClone||e.logger.log("initialized",e.options),e.emit("initialized",e.options),f.resolve(r),n(t,r)};if(e.languages&&"v1"!==e.options.compatibilityAPI&&!e.isInitialized)return t(null,e.t.bind(e));e.changeLanguage(e.options.lng,t)};return this.options.resources||!this.options.initImmediate?p():setTimeout(p,0),f}},{key:"loadResources",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:pe,r=n,o="string"==typeof e?e:this.language;if("function"==typeof e&&(r=e),!this.options.resources||this.options.partialBundledLanguages){if(o&&"cimode"===o.toLowerCase())return r();var i=[],a=function(e){e&&t.services.languageUtils.toResolveHierarchy(e).forEach((function(e){i.indexOf(e)<0&&i.push(e)}))};if(o)a(o);else{var s=this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);s.forEach((function(e){return a(e)}))}this.options.preload&&this.options.preload.forEach((function(e){return a(e)})),this.services.backendConnector.load(i,this.options.ns,(function(e){e||t.resolvedLanguage||!t.language||t.setResolvedLanguage(t.language),r(e)}))}else r(null)}},{key:"reloadResources",value:function(e,t,n){var r=v();return e||(e=this.languages),t||(t=this.options.ns),n||(n=pe),this.services.backendConnector.reload(e,t,(function(e){r.resolve(),n(e)})),r}},{key:"use",value:function(e){if(!e)throw new Error("You are passing an undefined module! Please check the object you are passing to i18next.use()");if(!e.type)throw new Error("You are passing a wrong module! Please check the object you are passing to i18next.use()");return"backend"===e.type&&(this.modules.backend=e),("logger"===e.type||e.log&&e.warn&&e.error)&&(this.modules.logger=e),"languageDetector"===e.type&&(this.modules.languageDetector=e),"i18nFormat"===e.type&&(this.modules.i18nFormat=e),"postProcessor"===e.type&&I.addPostProcessor(e),"formatter"===e.type&&(this.modules.formatter=e),"3rdParty"===e.type&&this.modules.external.push(e),this}},{key:"setResolvedLanguage",value:function(e){if(e&&this.languages&&!(["cimode","dev"].indexOf(e)>-1))for(var t=0;t-1)&&this.store.hasLanguageSomeTranslations(n)){this.resolvedLanguage=n;break}}}},{key:"changeLanguage",value:function(e,t){var n=this;this.isLanguageChangingTo=e;var r=v();this.emit("languageChanging",e);var o=function(e){n.language=e,n.languages=n.services.languageUtils.toResolveHierarchy(e),n.resolvedLanguage=void 0,n.setResolvedLanguage(e)},i=function(i){e||i||!n.services.languageDetector||(i=[]);var a="string"==typeof i?i:n.services.languageUtils.getBestMatchFromCodes(i);a&&(n.language||o(a),n.translator.language||n.translator.changeLanguage(a),n.services.languageDetector&&n.services.languageDetector.cacheUserLanguage&&n.services.languageDetector.cacheUserLanguage(a)),n.loadResources(a,(function(e){!function(e,i){i?(o(i),n.translator.changeLanguage(i),n.isLanguageChangingTo=void 0,n.emit("languageChanged",i),n.logger.log("languageChanged",i)):n.isLanguageChangingTo=void 0,r.resolve((function(){return n.t.apply(n,arguments)})),t&&t(e,(function(){return n.t.apply(n,arguments)}))}(e,a)}))};return e||!this.services.languageDetector||this.services.languageDetector.async?!e&&this.services.languageDetector&&this.services.languageDetector.async?0===this.services.languageDetector.detect.length?this.services.languageDetector.detect().then(i):this.services.languageDetector.detect(i):i(e):i(this.services.languageDetector.detect()),r}},{key:"getFixedT",value:function(t,n,r){var o=this,i=function t(n,i){var a;if("object"!==e(i)){for(var s=arguments.length,u=new Array(s>2?s-2:0),c=2;c1&&void 0!==arguments[1]?arguments[1]:{};if(!this.isInitialized)return this.logger.warn("hasLoadedNamespace: i18next was not initialized",this.languages),!1;if(!this.languages||!this.languages.length)return this.logger.warn("hasLoadedNamespace: i18n.languages were undefined or empty",this.languages),!1;var r=this.resolvedLanguage||this.languages[0],o=!!this.options&&this.options.fallbackLng,i=this.languages[this.languages.length-1];if("cimode"===r.toLowerCase())return!0;var a=function(e,n){var r=t.services.backendConnector.state["".concat(e,"|").concat(n)];return-1===r||2===r};if(n.precheck){var s=n.precheck(this,a);if(void 0!==s)return s}return!!this.hasResourceBundle(r,e)||(!(this.services.backendConnector.backend&&(!this.options.resources||this.options.partialBundledLanguages))||!(!a(r,e)||o&&!a(i,e)))}},{key:"loadNamespaces",value:function(e,t){var n=this,r=v();return this.options.ns?("string"==typeof e&&(e=[e]),e.forEach((function(e){n.options.ns.indexOf(e)<0&&n.options.ns.push(e)})),this.loadResources((function(e){r.resolve(),t&&t(e)})),r):(t&&t(),Promise.resolve())}},{key:"loadLanguages",value:function(e,t){var n=v();"string"==typeof e&&(e=[e]);var r=this.options.preload||[],o=e.filter((function(e){return r.indexOf(e)<0}));return o.length?(this.options.preload=r.concat(o),this.loadResources((function(e){n.resolve(),t&&t(e)})),n):(t&&t(),Promise.resolve())}},{key:"dir",value:function(e){if(e||(e=this.resolvedLanguage||(this.languages&&this.languages.length>0?this.languages[0]:this.language)),!e)return"rtl";var t=this.services&&this.services.languageUtils||new M(se());return["ar","shu","sqr","ssh","xaa","yhd","yud","aao","abh","abv","acm","acq","acw","acx","acy","adf","ads","aeb","aec","afb","ajp","apc","apd","arb","arq","ars","ary","arz","auz","avl","ayh","ayl","ayn","ayp","bbz","pga","he","iw","ps","pbt","pbu","pst","prp","prd","ug","ur","ydd","yds","yih","ji","yi","hbo","men","xmn","fa","jpr","peo","pes","prs","dv","sam","ckb"].indexOf(t.getLanguagePartFromCode(e))>-1||e.toLowerCase().indexOf("-arab")>1?"rtl":"ltr"}},{key:"cloneInstance",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:pe,r=le(le(le({},this.options),t),{isClone:!0}),o=new a(r);void 0===t.debug&&void 0===t.prefix||(o.logger=o.logger.clone(t));var i=["store","services","language"];return i.forEach((function(t){o[t]=e[t]})),o.services=le({},this.services),o.services.utils={hasLoadedNamespace:o.hasLoadedNamespace.bind(o)},o.translator=new K(o.services,o.options),o.translator.on("*",(function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0;return new he(e,t)}));var de=he.createInstance();return de.createInstance=he.createInstance,de})); diff --git a/html/js/i18nextHttpBackend.min.js b/html/js/i18nextHttpBackend.min.js new file mode 100644 index 00000000..3100ac9d --- /dev/null +++ b/html/js/i18nextHttpBackend.min.js @@ -0,0 +1 @@ +!function(e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).i18nextHttpBackend=e()}(function(){return function o(i,r,a){function s(t,e){if(!r[t]){if(!i[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(u)return u(t,!0);throw(e=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",e}n=r[t]={exports:{}},i[t][0].call(n.exports,function(e){return s(i[t][1][e]||e)},n,n.exports,o,i,r,a)}return r[t].exports}for(var u="function"==typeof require&&require,e=0;e"),u=t.innerHTML.indexOf("")+11;l>-1&&u>6&&(t.innerHTML=[t.innerHTML.substring(0,l),t.innerHTML.slice(u)].join("")),t.innerHTML=["",e.t(n,c(r,t.innerHTML)),"",t.innerHTML].join("")}else if("append"===i){var f=t.innerHTML.indexOf(""),s=t.innerHTML.indexOf("")+11;f>-1&&s>6&&(t.innerHTML=[t.innerHTML.substring(0,f),t.innerHTML.slice(s)].join("")),t.innerHTML=[t.innerHTML,"",e.t(n,c(r,t.innerHTML),"")].join("")}else if(0===i.indexOf("data-")){var a=i.substr("data-".length),p=e.t(n,c(r,t.getAttribute(a)));t.setAttribute(a,p),t.setAttribute(i,p)}else t.setAttribute(i,e.t(n,c(r,t.getAttribute(i))))}function i(e){return JSON.parse(e.replace(/:\s*"([^"]*)"/g,function(e,t){return': "'+t.replace(/:/g,"@colon@")+'"'}).replace(/:\s*'([^']*)'/g,function(e,t){return': "'+t.replace(/:/g,"@colon@")+'"'}).replace(/(['"])?([a-z0-9A-Z_]+)(['"])?\s*:/g,'"$2": ').replace(/@colon@/g,":"))}function o(e,r){var o=e.getAttribute(u.selectorAttr);if(o){var l=e,c=e.getAttribute(u.targetAttr);if(null!=c&&(l=e.querySelector(c)||e),r||!0!==u.useOptionsAttr||(r=i(e.getAttribute(u.optionsAttr)||"{}")),r=r||{},o.indexOf(";")>=0)for(var f=o.split(";"),s=0,a=f.length;s-1;c--)o(l[c],t);o(i,t)}}var u=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};u=t({},r,u);var c=function(e,n){return u.parseDefaultValueFromContent?t({},e,{defaultValue:n}):e};return l}var r={selectorAttr:"data-i18n",targetAttr:"i18n-target",optionsAttr:"i18n-options",useOptionsAttr:!1,parseDefaultValueFromContent:!0,document:document};return{init:n}}); \ No newline at end of file diff --git a/html/locales/de.json b/html/locales/de.json new file mode 100644 index 00000000..3867e834 --- /dev/null +++ b/html/locales/de.json @@ -0,0 +1,323 @@ +{ + "submit": "Absenden", + "restart": "Neustart", + "reset": "Reset", + "execute": "Ausführen", + "title": "ESPuino-Konfiguration", + "shutdown": "Ausschalten", + "log": "Log", + "info": "Info", + "delete": "Löschen", + "cancel": "Abbrechen", + "refresh": "Aktualisieren", + "restartinfo": "ESPuino wird jetzt neu gestartet...\n\nDie Seite lädt sich nach dem Neustart automatisch neu.", + "shutdowninfo": "ESPuino wird jetzt ausgeschaltet...\n\nDie Seite lädt sich nach dem Wiedereinschalten automatisch neu.", + "toast": { + "rfidDetect": "RFID Tag mit {{rfidId}} erkannt.", + "success": "Aktion erfolgreich ausgeführt.", + "conlost": "Die Verbindung zum ESPuino ist unterbrochen! Bitte Seite neu laden." + }, + "nav": { + "control": "Steuerung", + "rfid": "RFID", + "wifi": "WLAN", + "mqtt": "MQTT", + "ftp": "FTP", + "bluetooth": "Bluetooth", + "general": "Allgemein", + "tools": "Tools", + "forum": "Forum" + }, + "wifi": { + "title": "WLAN-Einstellungen", + "networks": "Netzwerke", + "savedNetworks": "Gespeicherte Netzwerke", + "ssid": { + "title": "WLAN-Name (SSID)", + "placeholder": "SSID", + "validation": "Bitte SSID des WLANs eintragen" + }, + "password": { + "title": "Passwort", + "placeholder": "Passwort", + "validation": "Bitte Passwort vom WLANs eintragen" + }, + "hostname": { + "title": "ESPuino-Name (Hostname)", + "placeholder": "espuino", + "validation": "Trage einen validen Hostnamen ein" + }, + "static": { + "addr": "Statische IP-Adresse", + "enabled": "Statische IP-Konfiguration", + "gateway": "Gateway für statische IP-Konfiguration", + "subnet": "Subnetzmaske für statische IP-Konfiguration", + "dns1": " DNS 1 für statische IP-Konfiguration", + "dns2": "DNS 2 für statische IP-Konfiguration" + }, + "scan": { + "enabled": "Start mit bestem WLAN" + }, + "delete": { + "title": "Netzwerk entfernen", + "prompt": "Gespeichertes Netzwerk \"{{ssid}}\" wirklich entfernen?" + }, + "restartPrompt": "Fertig?" + }, + "control": { + "title": "Steuerung", + "first": "Erster", + "prev": "Vorheriger", + "playpause": "Play / Pause", + "forward": "Nächster", + "last": "Letzter", + "volume": "Lautstärke", + "voldown": "Leiser", + "volup": "Lauter", + "current": "Aktueller Titel", + "command": "Modifikation Ausführen" + }, + "files": { + "title": "Dateien", + "loading": "Wird geladen...", + "context": { + "newFolder" : "Neuer Ordner", + "play": "Abspielen", + "refresh": "Aktualisieren", + "delete": "Löschen", + "rename": "Umbenennen", + "download": "Download" + }, + "files": { + "title": "Dateien", + "desc": "Eine oder mehrere Datei(en) hochladen" + }, + "directory": { + "title": "Ordner", + "desc": "Einen Ordner samt Inhalt (und allen Unterordnern) hochladen" + }, + "search": { + "placeholder": "Dateien suchen.." + }, + "upload": { + "title": "Upload", + "desc": "Upload starten", + "selectFolder": "Wähle den Ordner für den Upload!", + "selectFile": "Wähle eine Datei zum Hochladen!", + "success": "Upload erfolgreich ({{elapsed}}, {{speed}} KB/s)", + "error": "Upload fehlgeschlagen", + "timeCalc": "Verbleibende Zeit wird berechnet..", + "minutes_one": "Minute", + "minutes_other": "Minuten", + "seconds": "Sekunden", + "fewSec": "wenige", + "progress": "{{percent}}% ({{speed}} KB/s), {{remaining.value}} {{remaining.unit}} verbleibend.." + }, + "rfid": { + "title": "RFID-Zuweisungen", + "idNumber": "RFID-Chip-Nummer (12-stellig)", + "music": "Musik", + "modification": "Modifikation", + "savedassignments": "Gespeicherte RFID-Zuweisungen", + "fileurl": { + "title": "Datei, Verzeichnis oder URL (^ und # als Zeichen nicht erlaubt)", + "placeholder": "z.B. /mp3/Hoerspiele/Yakari/Yakari_und_seine_Freunde.mp3" + }, + "playmode": { + "title": "Abspielmodus", + "placeholder":"Modus auswählen", + "mode": { + "1":"Einzelner Titel", + "2":"Einzelner Titel (Endlosschleife)", + "12":"Einzelner Titel eines Verzeichnis (zufällig). Danach schlafen.", + "3":"Hörbuch", + "4":"Hörbuch (Endlosschleife)", + "5":"Alle Titel eines Verzeichnis (sortiert)", + "6":"Alle Titel eines Verzeichnis (zufällig)", + "7":"Alle Titel eines Verzeichnis (sortiert, Endlosschleife)", + "9":"Alle Titel eines Verzeichnis (zufällig, Endlosschleife)", + "13":"Alle Titel aus einem zufälligen Unterverzeichnis (sortiert)", + "14":"Alle Titel aus einem zufälligen Unterverzeichnis (zufällig)", + "8":"Webradio", + "11":"Liste (Dateien von SD und/oder Webstreams) aus lokaler .m3u-Datei" + }, + "error": "Kein gültiger Abspielmodus" + }, + "mod": { + "title": "Modifikation", + "placeholder": "Modifikation auswählen", + "cmd": { + "100": "Tastensperre", + "179": "Schlafe sofort", + "101": "Schlafen nach 15 Minuten", + "102": "Schlafen nach 30 Minuten", + "103": "Schlafen nach 1 Stunde", + "104": "Schlafen nach 2 Stunden", + "105":"Schlafen nach Ende des Titels", + "106":"Schlafen nach Ende der Playlist", + "107":"Schlafen nach fünf Titeln", + "110":"Wiederhole Playlist (endlos)", + "111":"Wiederhole Titel (endlos)", + "120":"Dimme LEDs (Nachtmodus)", + "130":"Aktiviere/deaktiviere WLAN", + "140":"Bluetooth-Lautsprecher aktivieren/deaktivieren", + "141":"Bluetooth-Kopfhörer aktivieren/deaktivieren", + "142":"Wechsle Modus (Normal => BT-Lautsprecher => BT-Kopfhörer)", + "150":"Aktiviere FTP", + "151":"IP-Adresse ansagen", + "152":"Uhrzeit ansagen", + "0":"Lösche Zuordnung", + "170":"Play/Pause", + "171":"Vorheriger Titel", + "172":"Nächster Titel", + "173":"Erster Titel", + "174":"Letzter Titel", + "180":"Springe vorwärts (n Sekunden)", + "181":"Springe rückwärts (n Sekunden)" + } + } + } + }, + "mqtt": { + "title": "MQTT-Einstellungen", + "enable": "MQTT aktivieren", + "clientId": { + "title": "MQTT-ClientId", + "placeholder": "z.B. ESPuino", + "validation": "" + }, + "server": { + "title": "MQTT-Server", + "placeholder": "f.e. 192.168.2.89", + "validation": "" + }, + "user": { + "title": "MQTT-Benutzername (optional)", + "placeholder": "Benutzername", + "validation": "" + }, + "pwd": { + "title": "MQTT-Passwort (optional)", + "placeholder": "Passwort", + "validation": "" + }, + "port": { + "title": "MQTT-Port", + "placeholder": "z.B.. 1883", + "validation": "" + } + }, + "ftp": { + "title": "FTP-Einstellungen", + "user": { + "title": "FTP-Benutzername", + "placeholder": "Benutzername" + }, + "pwd": { + "title": "FTP-Passwort", + "placeholder": "Passwort" + }, + "start": { + "title": "FTP-Server starten", + "desc": "Aktiviert den FTP-Server bis zum Neustart des Geräts.", + "button": "FTP-Server starten" + } + }, + "bt": { + "sink": { + "title": "ESPuino als Bluetooth-Lautsprecher", + "desc": "ESPuino wird als Bluetooth-Lautsprecher gestartet. Nach dem Wechsel in diesen Modus wird die Web-Schnittstelle nicht mehr zur Verfügung stehen, bis das System im normalen Modus neu gestartet wird.", + "button": "Als Bluetooth-Lautsprecher starten" + }, + "source": { + "configtitle": "Bluetooth-Kopfhörer Einstellungen", + "title": "Mit Bluetooth-Kopfhörer verbinden", + "desc": "Das Gerät verbindet sich mit dem angegebenen Bluetooth-Kopfhörer. Nach dem Wechsel in diesen Modus wird die Web-Schnittstelle nicht mehr zur Verfügung stehen, bis das System im normalen Modus neu gestartet wird.", + "button": "Im Kopfhörer-Modus starten" + }, + "device": { + "title": "Bluetooth-Gerät (Kopfhörer)", + "placeholder": "z.B. My POGS Wireless Headphone" + }, + "pincode": { + "title": "Pairing PIN-Code", + "placeholder": "z.B. 0000" + } + }, + "general": { + "volume": { + "title": "Lautstärke", + "restart": "Nach dem Einschalten", + "speakerMax": "Maximal (Lautsprecher)", + "headphoneMax": "Maximal (Kopfhörer)" + }, + "neopixel": { + "title": "Neopixel (Helligkeit)", + "restart": "Nach dem Einschalten", + "nightmode": "Im Nachtmodus" + }, + "sleep": { + "title": "Deep Sleep", + "incativity": "Inaktivität nach (in Minuten)" + }, + "battery": { + "title": "Batterie", + "desc": "Status über Neopixel anzeigen", + "lowWarning": "Unter dieser Spannung wird eine Warnung angezeigt", + "lowCritical": "Eine LED leuchtet bei dieser Spannung", + "high": "Alle LEDs leuchten bei dieser Spannung", + "measureInterval": "Zeitabstand der Messung (in Minuten)" + } + }, + "tools": { + "nvserase": { + "title": "NVS RFID-Zuweisungen löschen", + "desc": "Über den Importer werden lediglich neue Einträge importiert, jedoch keine bestehenden Einträge aktiv gelöscht. Im Falle einer doppelten Zuweisung wird ein Eintrag allenfalls überschrieben. Mit dieser Funktion können alle bestehenden NVS-RFID-Zuweisungen gelöscht werden, so dass der ESPuino im Anschluss keinerlei Karten mehr kennt. Wird im Anschluss der Importer gestartet, befinden sich im Speicher des ESPuino anschließend exakt nur solche Zuweisungen, die Teil der Backup-Datei sind. Weitere Infos gibt es hier.", + "button": "Zuweisungen löschen", + "prompt": "Alle RFID-Zuweisungen wirklich löschen?" + }, + "nvsdelete": { + "title": "NVS RFID-Zuweisung entfernen", + "prompt": "Gespeicherte Zuweisung \"{{rfid}}\" wirklich entfernen?" + }, + "nvsimport": { + "title": "NVS RFID-Importer", + "desc": "Hier kann eine Backup-Datei hochgeladen werden, um NVS-RFID-Zuweisungen zu importieren." + }, + "fwupdate": { + "title": "Firmware-Update", + "desc": "Hier kann ein Firmware-Update durchgeführt werden." + } + }, + "forum": { + "title": "Forum", + "desc": "Du hast Probleme mit ESPuino oder bist an einem Erfahrungsaustausch interessiert?
Dann schaue doch mal im ESPuino-Forum vorbei! Insbesondere gibt es dort auch einen
Bereich, in dem reichlich Dokumentation hinterlegt ist. Wir freuen uns auf deinen Besuch!" + }, + "datetime": { + "day": "Tag", + "days": "Tage", + "hour": "Stunde", + "hours": "Stunden", + "minute": "Minute", + "minutes": "Minuten", + "second": "Sekunde", + "seconds": "Sekunden" + }, + "systeminfo": { + "title": "Information", + "softwareversion": "ESPuino {{softwareversion}}", + "gitversion": "ESPuino {{gitversion}}", + "arduinoversion": "Arduino Version: {{arduinoversion}} (ESP-IDF {{idfversion}})", + "hardware": "Hardware: {{hwmodel}}, Revision {{hwrevision}}, CPU: {{hwfreq}} MHZ", + "freeheap": "Freier Heap: {{freeheap}} Bytes", + "largestfreeblock": "Größter freier Heap-Block: {{largestfreeblock}} Bytes", + "freepsram": "Freier PS-RAM: {{freepsram}} Bytes", + "currentip": "Aktuelle IP-Adresse: {{currentip}}", + "rssi": "WLAN Signalstärke: {{rssi}} dBm", + "audiotimetotal": "Audio-Gesamtspielzeit seit {{firststart}}: {{audiotimetotal}}", + "audiotimesincestart": "Spielzeit seit letztem Start: {{audiotimesincestart}}", + "currvoltage": "Aktuelle Batteriespannung: {{currvoltage}} V", + "chargelevel": "Aktuelle Batterieladung: {{chargelevel}} %", + "hallsensor": "HallEffectSensor NullFieldValue: {{hsnullfieldvalue}}, actual: {{hsactual}}, diff:{{hsdiff}}, LastWaitFor_State:{{hslastwaitforstate}} (waited:{{hswaited}} ms)" + } +} \ No newline at end of file diff --git a/html/locales/en.json b/html/locales/en.json new file mode 100644 index 00000000..5d52fef0 --- /dev/null +++ b/html/locales/en.json @@ -0,0 +1,323 @@ +{ + "submit": "Submit", + "restart": "Restart", + "reset": "Reset", + "execute": "Execute", + "title": "ESPuino-Settings", + "shutdown": "Shutdown", + "log": "Log", + "info": "Info", + "delete": "Delete", + "cancel": "Cancel", + "refresh": "Refresh", + "restartinfo": "ESPuino is being restarted...\n\nPage reloads automatically after restart finished.", + "shutdowninfo": "ESPuino is now switched off...\n\nPage reloads automatically when the ESPuino is switched on again..", + "toast": { + "rfidDetect": "RFID-tag {{rfidId}} detected.", + "success": "Action performed successfully.", + "conlost": "Connection to ESPuino broken! Please reload website." + }, + "nav": { + "control": "Control", + "rfid": "RFID", + "wifi": "WiFi", + "mqtt": "MQTT", + "ftp": "FTP", + "bluetooth": "Bluetooth", + "general": "General", + "tools": "Tools", + "forum": "Forum" + }, + "wifi": { + "title": "WiFi-configuration", + "networks": "Networks", + "savedNetworks": "Saved Networks", + "ssid": { + "title": "WiFi-name (SSID)", + "placeholder": "SSID", + "validation": "Enter WiFi's SSID" + }, + "password": { + "title": "Password", + "placeholder": "Password", + "validation": "Enter the WiFi's password" + }, + "hostname": { + "title": "ESPuino's name (hostname)", + "placeholder": "espuino", + "validation": "Enter a valid hostname" + }, + "static": { + "addr": "Static IP-Address", + "enabled": "Static IP-Configuration", + "gateway": "Gateway for static IP-Configuration", + "subnet": "Subnet-mask for static IP-Configuration", + "dns1": " DNS 1 for static IP-Configuration", + "dns2": "DNS 2 for static IP-Configuration" + }, + "scan": { + "enabled": "Start with strongest WiFi" + }, + "delete": { + "title": "Delete WiFi", + "prompt": "Delete saved WiFi \"{{ssid}}\" ?" + }, + "restartPrompt": "Ready to go?" + }, + "control": { + "title": "Control", + "first": "First", + "prev": "Previous", + "playpause": "Play / Pause", + "forward": "Next", + "last": "Last", + "volume": "Volume", + "voldown": "Volume Down", + "volup": "Volume Up", + "current": "Current track", + "command": "Execute Modification" + }, + "files": { + "title": "Files", + "loading": "Please wait...", + "context": { + "newFolder": "New Folder", + "play": "Play", + "refresh": "Refresh", + "delete": "Delete", + "rename": "Rename", + "download": "Download" + }, + "files": { + "title": "Files", + "desc": "Upload one ore more files" + }, + "directory": { + "title": "Directory", + "desc": "Upload directory with all files and subdirectories" + }, + "search": { + "placeholder": "Search files.." + }, + "upload": { + "title": "Upload", + "desc": "Start Upload", + "selectFolder": "Please select the upload location!", + "selectFile": "Please select files to upload!", + "success": "Upload successful ({{elapsed}}, {{speed}} KB/s)", + "error": "Upload error", + "timeCalc": "Remaining time is being calculated..", + "minutes_one": "minute", + "minutes_other": "minutes", + "seconds": "seconds", + "fewSec": "few", + "progress": "{{percent}}% ({{speed}} KB/s), {{remaining.value}} {{remaining.unit}} remaining.." + }, + "rfid": { + "title": "RFID-Assignments", + "idNumber": "RFID-number (12 digits)", + "music": "Music", + "modification": "Modification", + "savedassignments": "Saved RFID-assignments", + "fileurl": { + "title": "File, directory or URL (^ and # aren't allowed as chars)", + "placeholder": "f.e. /mp3/Audiobook/Yakari/Yakari_and_Friends.mp3" + }, + "playmode": { + "title": "Playmode", + "placeholder":"Select mode", + "mode": { + "1":"Single track", + "2":"Single track (loop)", + "12":"Single track of a directory (random). Followed by sleep.", + "3":"Audiobook", + "4":"Audiobook (loop)", + "5":"All tracks of a directory (sorted alph.)", + "6":"All tracks of a directory (random)", + "7":"All tracks of a directory (sorted alph., loop)", + "9":"All tracks of a directory (random, loop)", + "13":"All tracks of a random subdirectory (sorted alph.)", + "14":"All tracks of a random subdirectory (random)", + "8":"Webradio", + "11":"List (files from SD and/or webstreams) from local .m3u-File" + }, + "error": "Invalid playmode" + }, + "mod": { + "title": "Mod", + "placeholder": "Select modification", + "cmd": { + "100": "Keylock", + "179": "Sleep immediately", + "101": "Sleep after 15 minutes", + "102":"Sleep after 30 minutes", + "103":"Sleep after 1 hour", + "104":"Sleep after 2 hours", + "105":"Sleep after end of track", + "106":"Sleep after end of playlist", + "107":"Sleep after end of five tracks", + "110":"Loop playlist", + "111":"Loop track", + "120":"Dimm LEDs (nightmode)", + "130":"Toggle WiFi", + "140":"Toggle Bluetooth Speaker", + "141":"Toggle Bluetooth Headphones", + "142":"Toggle Mode (Normal => BT-Speaker => Bluetooth Headphone)", + "150":"Enable FTP", + "151":"Announce IP-Address", + "152":"Announce current time", + "0":"Remove assignment", + "170":"Toggle Play/Pause", + "171":"Previous track", + "172":"Next track", + "173":"First track", + "174":"Last track", + "180":"Seek forwards (n seconds)", + "181":"Seek backwards (n seconds)" + } + } + } + }, + "mqtt": { + "title": "MQTT-settings", + "enable": "Enable MQTT", + "clientId": { + "title": "MQTT-ClientId", + "placeholder": "f.e. ESPuino", + "validation": "" + }, + "server": { + "title": "MQTT-server", + "placeholder": "f.e. 192.168.2.89", + "validation": "" + }, + "user": { + "title": "MQTT-username (optional)", + "placeholder": "username", + "validation": "" + }, + "pwd": { + "title": "MQTT-password (optional)", + "placeholder": "password", + "validation": "" + }, + "port": { + "title": "MQTT-port", + "placeholder": "f.e. 1883", + "validation": "" + } + }, + "ftp": { + "title": "FTP-settings", + "user": { + "title": "FTP-Username", + "placeholder": "username" + }, + "pwd": { + "title": "FTP-password", + "placeholder": "password" + }, + "start": { + "title": "Start FTP-server", + "desc": "Enables FTP-server until device is restarted.", + "button": "Start FTP-server" + } + }, + "bt": { + "sink": { + "title": "ESPuino as Bluetooth speaker", + "desc": "ESPuino is started as a Bluetooth speaker. After switching to this mode, the web interface will no longer be available until the system is restarted in normal mode.", + "button": "Start as Bluetooth speaker" + }, + "source": { + "configtitle": "Bluetooth headphone settings", + "title": "Connect with Bluetooth headphone", + "desc": "The device connects to the specified Bluetooth headset. After switching to this mode, the web interface will no longer be available until the system is restarted in normal mode.", + "button": "Start in headphone mode" + }, + "device": { + "title": "Bluetooth device (headphone)", + "placeholder": "e.g. My POGS Wireless Headphone" + }, + "pincode": { + "title": "Pairing PIN-Code", + "placeholder": "e.g. 0000" + } + }, + "general": { + "volume": { + "title": "Volume", + "restart": "After restart", + "speakerMax": "Max. volume (speaker)", + "headphoneMax": "Max. volume (headphones)" + }, + "neopixel": { + "title": "Neopixel (brightness)", + "restart": "After restart", + "nightmode": "For nightmode" + }, + "sleep": { + "title": "Deep Sleep", + "incativity": "After n minutes inactivity" + }, + "battery": { + "title": "Battery", + "desc": "Show voltage-status via Neopixel", + "lowWarning": "Show warning below this threshold", + "lowCritical": "Lowest voltage, that is indicated by one LED", + "high": "Voltage, that is indicated by all LEDs", + "measureInterval": "Interval between measurements (in minutes)" + } + }, + "tools": { + "nvserase": { + "title": "Erase NVS RFID-assignments", + "desc": "Via importer new entries will only be inserted but old ones won't be erased. Only in case an old assignment to the same entry already exists, it will be overwritten. With this function all existing rfid-assignments will be erased. Further infos in German language.", + "button": "Delete assignments", + "prompt": "Erase all saved NVS RFID-assignments?" + }, + "nvsdelete": { + "title": "Remove NVS RFID-assignment", + "prompt": "Remove assignment \"{{rfid}}\" ?" + }, + "nvsimport": { + "title": "NVS RFID-Importer", + "desc": "Backupfile can be uploaded right here in order to import NVS-RFID-assignments." + }, + "fwupdate": { + "title": "Firmware-Update", + "desc": "Firmware can be updated right here." + } + }, + "forum": { + "title": "Forum", + "desc": "Having problems or aim to discuss about ESPuino?
Join us at ESPuino-Forum! Especially there's a lot of (german)
documentation online!" + }, + "datetime": { + "day": "day", + "days": "days", + "hour": "hour", + "hours": "hours", + "minute": "minute", + "minutes": "minutes", + "second": "second", + "seconds": "seconds" + }, + "systeminfo": { + "title": "Information", + "softwareversion": "ESPuino {{softwareversion}}", + "gitversion": "ESPuino {{gitversion}}", + "arduinoversion": "Arduino Version: {{arduinoversion}} (ESP-IDF {{idfversion}})", + "hardware": "Hardware: {{hwmodel}}, Revision {{hwrevision}}, CPU: {{hwfreq}} MHZ", + "freeheap": "Free heap: {{freeheap}} Bytes", + "largestfreeblock": "Largest free heap-block: {{largestfreeblock}} Bytes", + "freepsram": "Free PS-RAM: {{freepsram}} Bytes", + "currentip": "Current IP-Address: {{currentip}}", + "rssi": "WiFi signal strength: {{rssi}} dBm", + "audiotimetotal": "Total audio playtime since {{firststart}}: {{audiotimetotal}}", + "audiotimesincestart": "Playtime since last start: {{audiotimesincestart}}", + "currvoltage": "Current battery voltage: {{currvoltage}} V", + "chargelevel": "Current charge level: {{chargelevel}} %", + "hallsensor": "HallEffectSensor NullFieldValue: {{hsnullfieldvalue}}, actual: {{hsactual}}, diff:{{hsdiff}}, LastWaitFor_State:{{hslastwaitforstate}} (waited:{{hswaited}} ms)" + } +} \ No newline at end of file diff --git a/html/management.html b/html/management.html new file mode 100644 index 00000000..611a51f1 --- /dev/null +++ b/html/management.html @@ -0,0 +1,2232 @@ + + + + ESPuino-Settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/html/management_DE.html b/html/management_DE.html deleted file mode 100644 index 21a1ecd9..00000000 --- a/html/management_DE.html +++ /dev/null @@ -1,1334 +0,0 @@ - - - - ESPuino-Konfiguration - - - - - - - - - - - - - - - - - - - -
- -
- - - - \ No newline at end of file diff --git a/html/management_EN.html b/html/management_EN.html deleted file mode 100644 index 49dd37b1..00000000 --- a/html/management_EN.html +++ /dev/null @@ -1,1333 +0,0 @@ - - - - ESPuino-Settings - - - - - - - - - - - - - - - - - - - -
- -
- - - - \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index dcf7d938..0b1c0b9a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,181 +15,138 @@ default_envs = lolin_d32_pro_sdmmc_pe [env] board_build.flash_mode = qio board_build.bootloader = dio -platform = espressif32@<=3.5.0 -;platform = espressif32@<=5.2.0 +board_build.partitions = custom_4mb_noota.csv +platform = espressif32@^6.4.0 ;platform = espressif32 ;platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream -framework = arduino +;framework = arduino +framework = arduino, espidf monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +;monitor_port = /dev/cu.SLAB_USBtoUART +;upload_port = /dev/cu.SLAB_USBtoUART +;upload_speed = 115200 extra_scripts = pre:gitVersion.py + pre:updateSdkConfig.py pre:processHtml.py lib_deps = - https://github.com/SZenglein/ESP32-audioI2S.git#4186228 - https://github.com/madhephaestus/ESP32Encoder.git#22992b3 + https://github.com/schreibfaul1/ESP32-audioI2S.git#4311587 + https://github.com/madhephaestus/ESP32Encoder.git#9979722 https://github.com/knolleary/pubsubclient.git#2d228f2 - https://github.com/biologist79/ESP32FTPServer - https://github.com/FastLED/FastLED.git#3.5.0 + https://github.com/peterus/ESP-FTP-Server-Lib#554959f + https://github.com/tueddy/FastLED.git#3.6.0_IRAM ;save some IRAM to compile with all features (https://github.com/FastLED/FastLED.git@3.6.0) https://github.com/me-no-dev/ESPAsyncWebServer.git#1d46269 https://github.com/me-no-dev/AsyncTCP.git#ca8ac5f - https://github.com/bblanchon/ArduinoJson.git#7abf875 - https://github.com/pschatzmann/ESP32-A2DP.git#e5474e5 + https://github.com/bblanchon/ArduinoJson.git#7517ecb + https://github.com/pschatzmann/ESP32-A2DP.git#96d37bc https://github.com/Arduino-IRremote/Arduino-IRremote.git#ed94895 https://github.com/kkloesener/MFRC522_I2C.git#121a27e https://github.com/miguelbalboa/rfid.git#ba72b92 https://github.com/tuniii/LogRingBuffer.git#89d7d3e - https://github.com/tueddy/PN5180-Library.git#01b3e48 + https://github.com/tueddy/PN5180-Library.git#4a1f139 https://github.com/SZenglein/Arduino-MAX17055_Driver#4a0783e -platform_packages = - platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#1.0.6 - ;platformio/framework-arduinoespressif32 @ https://github.com/tuniii/arduino-esp32-v1.0.6-wt#v1.0.6-patched - ;platformio/tool-esptoolpy @ ~1.30100 - ;framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#2.0.1-RC1 +build_flags = + -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 + -DCONFIG_ASYNC_TCP_USE_WDT=1 +; -DCORE_DEBUG_LEVEL=6 + -std=c++17 + -std=gnu++17 + -Wall + -Wextra + -Wunreachable-code + +build_unflags = + -std=gnu++11 + -Werror=all [env:esp32-a1s] board = esp-wrover-kit lib_deps = ${env.lib_deps} https://github.com/kkloesener/AC101.git -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -build_flags = -DHAL=2 +build_flags = ${env.build_flags} + -DHAL=2 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DLOG_BUFFER_SIZE=10240 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 board_upload.maximum_size = 16777216 board_upload.flash_size = 16MB -monitor_filters = esp32_exception_decoder [env:lolin32] ;https://docs.platformio.org/en/latest/boards/espressif32/lolin32.html board = lolin32 -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -build_flags = -DHAL=1 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 -;upload_port = /dev/cu.SLAB_USBtoUART -;monitor_port = /dev/cu.SLAB_USBtoUART -monitor_filters = esp32_exception_decoder +build_flags = ${env.build_flags} + -DHAL=1 [env:lolin_d32] ;https://docs.platformio.org/en/latest/boards/espressif32/lolin_d32.html board = lolin_d32 -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -build_flags = -DHAL=3 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 -;upload_port = /dev/cu.wchusbserial1410 -;monitor_port = /dev/cu.wchusbserial1410 -monitor_filters = esp32_exception_decoder +build_flags = ${env.build_flags} + -DHAL=3 [env:lolin_d32_sdmmc_pe] ;https://docs.platformio.org/en/latest/boards/espressif32/lolin_d32.html board = lolin_d32 -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -build_flags = -DHAL=9 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 -;upload_port = /dev/cu.wchusbserial1410 -;monitor_port = /dev/cu.wchusbserial1410 -monitor_filters = esp32_exception_decoder +build_flags = ${env.build_flags} + -DHAL=9 [env:lolin_d32_pro] ;https://docs.platformio.org/en/latest/boards/espressif32/lolin_d32_pro.html board = lolin_d32_pro -;board_build.partitions = huge_app.csv -;board_build.partitions = custom_4mb_noota.csv board_build.partitions = custom_16mb_ota.csv -;upload_port = /dev/cu.wchusbserial1410 -;monitor_port = /dev/cu.wchusbserial1410 -monitor_filters = esp32_exception_decoder -build_flags = -DHAL=4 +build_flags = ${env.build_flags} + -DHAL=4 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DLOG_BUFFER_SIZE=10240 -DBOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 board_upload.maximum_size = 16777216 board_upload.flash_size = 16MB [env:lolin_d32_pro_sdmmc_pe] ;https://docs.platformio.org/en/latest/boards/espressif32/lolin_d32_pro.html board = lolin_d32_pro -;board_build.partitions = huge_app.csv -;board_build.partitions = custom_4mb_noota.csv board_build.partitions = custom_16mb_ota.csv -;upload_port = /dev/cu.wchusbserial1410 -;monitor_port = /dev/cu.wchusbserial1410 -monitor_filters = esp32_exception_decoder -build_flags = -DHAL=7 +build_flags = ${env.build_flags} + -DHAL=7 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DLOG_BUFFER_SIZE=10240 -DBOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT - ;-DCORE_DEBUG_LEVEL=5 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 board_upload.maximum_size = 16777216 board_upload.flash_size = 16MB [env:nodemcu-32s] ;https://docs.platformio.org/en/latest/boards/espressif32/nodemcu-32s.html board = nodemcu-32s -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -;upload_port = /dev/cu.SLAB_USBtoUART -;monitor_port = /dev/cu.SLAB_USBtoUART -monitor_filters = esp32_exception_decoder [env:az-delivery-devkit-v4] ;https://docs.platformio.org/en/latest/boards/espressif32/az-delivery-devkit-v4.html board = az-delivery-devkit-v4 -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -;upload_port = /dev/cu.SLAB_USBtoUART -;monitor_port = /dev/cu.SLAB_USBtoUART -monitor_filters = esp32_exception_decoder -build_flags = -DHAL=8 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 +build_flags = ${env.build_flags} + -DHAL=8 [env:ttgo_t8] ;https://docs.platformio.org/en/latest/boards/espressif32/esp-wrover-kit.html board = esp-wrover-kit -;board_build.partitions = huge_app.csv -board_build.partitions = custom_4mb_noota.csv -;upload_port = /dev/cu.SLAB_USBtoUART -;monitor_port = /dev/cu.SLAB_USBtoUART -monitor_filters = esp32_exception_decoder -build_flags = -DHAL=5 +build_flags = ${env.build_flags} + -DHAL=5 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DLOG_BUFFER_SIZE=10240 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 [env:complete] ;https://docs.platformio.org/en/latest/boards/espressif32/esp-wrover-kit.html board = esp-wrover-kit -;board_build.partitions = huge_app.csv board_build.partitions = custom_16mb_ota.csv -;upload_port = /dev/tty.wchusbserial1410 -;monitor_port = /dev/tty.wchusbserial1410 -monitor_filters = esp32_exception_decoder -build_flags = -DHAL=6 +build_flags = ${env.build_flags} + -DHAL=6 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DLOG_BUFFER_SIZE=10240 -DBOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 board_upload.maximum_size = 16777216 board_upload.flash_size = 16MB @@ -198,37 +155,30 @@ board_upload.flash_size = 16MB ;https://docs.platformio.org/en/latest/boards/espressif32/esp-wrover-kit.html board = esp-wrover-kit board_build.partitions = custom_8mb_ota.csv -build_flags = -DHAL=99 +build_flags = ${env.build_flags} + -DHAL=99 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue -DLOG_BUFFER_SIZE=10240 -DBOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT ; 8MB is fine board_upload.maximum_size = 8388608 board_upload.flash_size = 8MB -monitor_filters = esp32_exception_decoder ;;; Change upload/monitor-port of your board regarding your operating-system and develboard! ;MAC: /dev/cu.SLAB_USBtoUART / /dev/cu.wchusbserial1420 / /dev/cu.wchusbserial1410 ;WINDOWS: COM3 ;LINUX: /dev/ttyUSB0 (if it's the first USB-device attached, otherwise increase number) -[env:museluxe] -board = esp-wrover-kit -board_build.partitions = custom_4mb_noota.csv -build_flags = -DHAL=10 - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue - -DLOG_BUFFER_SIZE=10240 - -DCONFIG_ASYNC_TCP_RUNNING_CORE=1 - -DCONFIG_ASYNC_TCP_USE_WDT=1 -monitor_filters = esp32_exception_decoder -;[env:esp32-s3-devkitc-1] -;platform = espressif32 -;board = esp32-s3-devkitc-1 +[env:esp32-s3-devkitc-1] +;https://docs.platformio.org/en/latest/boards/espressif32/esp32-s3-devkitc-1.html +board = esp32-s3-devkitc-1 +platform = espressif32 ; change microcontroller -;board_build.mcu = esp32s3 +board_build.mcu = esp32s3 ; change MCU frequency -;board_build.f_cpu = 240000000L +build_flags = ${env.build_flags} + -DHAL=99 + -DLOG_BUFFER_SIZE=10240 diff --git a/processHtml.py b/processHtml.py index 2e30abd5..0adc27c6 100644 --- a/processHtml.py +++ b/processHtml.py @@ -1,32 +1,50 @@ # -*- coding: utf-8 -*- """ -Use this script for creating PROGMEM header files from html files. +Use this script for creating binary header files from html files. """ from pathlib import Path - +import os +import mimetypes +import gzip Import("env") # pylint: disable=undefined-variable +try: + from flask_minify.parsers import Parser +except ImportError: + print("Trying to Install required module: flask_minify\nIf this failes, please execute \"pip install flask_minify\" manually.") + env.Execute("$PYTHONEXE -m pip install flask_minify") + +from flask_minify.parsers import Parser +import json + OUTPUT_DIR = ( Path(env.subst("$BUILD_DIR")) / "generated" ) # pylint: disable=undefined-variable HTML_DIR = Path("html").absolute() -HTML_FILES = [ - Path("management_DE.html"), - Path("management_EN.html"), - Path("accesspoint_DE.html"), - Path("accesspoint_EN.html"), +# List of files, which will only be minifed but not compressed (f.e. html files with templates) +WWW_FILES = [] +# list of all files, which shall be compressed before embedding +# files with ".json" ending will be minifed before compression, ".js" will not be changed! +BINARY_FILES =[ + Path("management.html"), + Path("accesspoint.html"), + Path("js/i18next.min.js"), + Path("js/i18nextHttpBackend.min.js"), + Path("js/loc_i18next.min.js"), + Path("locales/de.json"), + Path("locales/en.json") ] class HtmlHeaderProcessor: - """Create c code PROGMEM header files from HTML files""" + """Create c code binary header files from HTML files""" @staticmethod def _escape_html(data): """Escape HTML characters for usage in C""" - data = data.replace("\n", "\\\n") + data = data.replace("\n", "\\n") data = data.replace('"', r"\"") data = data.replace(r"\d", r"\\d") data = data.replace(r"\.", r"\\.") @@ -36,23 +54,87 @@ def _escape_html(data): @classmethod def _process_header_file(cls, html_path, header_path): - with html_path.open("r") as html_file: - content = cls._escape_html(html_file.read()) + parser = Parser({}, True) + parser.update_runtime_options(True, True, True) + with html_path.open(mode="r", encoding="utf-8") as html_file: + content = html_file.read() + content = parser.minify(content, "html") # minify content as html + content = cls._escape_html(content) - with header_path.open("w") as header_file: + with header_path.open(mode="w", encoding="utf-8") as header_file: header_file.write( - f"static const char {html_path.name.split('_')[0]}_HTML[] PROGMEM = \"" + f"static const char {html_path.name.split('.')[0]}_HTML[] = \"" ) header_file.write(content) - header_file.write('";') + header_file.write('";\n') + header_file.close() + + @classmethod + def _process_binary_file(cls, binary_path, header_path, info): + # minify json files explicitly + if binary_path.suffix == "json": + with binary_path.open(mode="r", encoding="utf-8") as f: + jsonObj = json.load(f) + content = json.dumps(jsonObj, separators=(',', ':')) + # use everything else as is + else: + with binary_path.open(mode="r", encoding="utf-8") as f: + content = f.read() + + # compress content + data = gzip.compress(content.encode()) + + with header_path.open(mode="a", encoding="utf-8") as header_file: + varName = binary_path.name.split('.')[0] + header_file.write( + f"static const uint8_t {varName}_BIN[] = {{\n " + ) + size = 0 + for d in data: + # write out the compressed byte stream as a hex array and create a newline after every 20th entry + header_file.write("0x{:02X},".format(d)) + size = size + 1 + if not (size % 20): + header_file.write("\n ") + header_file.write("\n};\n\n") + # populate dict with our information + info["size"] = size + info["variable"] = f"{varName}_BIN" + return info @classmethod def process(cls): print("GENERATING HTML HEADER FILES") - for html_file in HTML_FILES: + for html_file in WWW_FILES: header_file = f"HTML{html_file.stem}.h" print(f" {HTML_DIR / html_file} -> {OUTPUT_DIR / header_file}") cls._process_header_file(HTML_DIR / html_file, OUTPUT_DIR / header_file) + binary_header = OUTPUT_DIR / "HTMLbinary.h" + if binary_header.exists(): + os.remove(binary_header) # remove file if it exists, since we are appending to it + + fileList = [] # dict holding the array of metadata for all processed binary files + for binary_file in BINARY_FILES: + filePath = HTML_DIR / binary_file + print(f" {filePath} -> {binary_header}") + + info = dict() # the dict entry for this file + info["uri"] = "/" + filePath.relative_to(HTML_DIR).as_posix() + info["mimeType"] = mimetypes.types_map[filePath.suffix] + info = cls._process_binary_file(HTML_DIR / binary_file, binary_header, info) + fileList.append(info) + + with binary_header.open(mode="a", encoding="utf-8") as f: + f.write("""using RouteRegistrationHandler = std::function; + +class WWWData { + public: + static void registerRoutes(RouteRegistrationHandler handler) { +""") + for info in fileList: + # write the fileList entries to the binary file. These will be the paramenter with which the handler is called to register the endpoint on the webserver + f.write(f' handler("{info["uri"]}", "{info["mimeType"]}", {info["variable"]}, {info["size"]});\n') + f.write(" }\n};\n") HtmlHeaderProcessor().process() diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 00000000..dd04cb6e --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,104 @@ +CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y +CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_AUTOSTART_ARDUINO=y +CONFIG_ARDUHAL_ESP_LOG=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT=y +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y +CONFIG_COMPILER_DISABLE_GCC8_WARNINGS=y +CONFIG_BT_ENABLED=y +CONFIG_BTDM_CTRL_MODE_BTDM=y +CONFIG_BTDM_SCAN_DUPL_CACHE_SIZE=20 +CONFIG_BT_BTC_TASK_STACK_SIZE=8192 +CONFIG_BT_BTU_TASK_STACK_SIZE=8192 +CONFIG_BT_CLASSIC_ENABLED=y +CONFIG_BT_A2DP_ENABLE=y +CONFIG_BT_SPP_ENABLED=y +# CONFIG_BT_HFP_ENABLE is not set +CONFIG_BT_STACK_NO_LOG=y +# CONFIG_BT_BLE_ENABLED is not set +CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY=y +# CONFIG_BLE_MESH is not set +# CONFIG_SPI_MASTER_ISR_IN_IRAM is not set +# CONFIG_SPI_SLAVE_ISR_IN_IRAM is not set +CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC=y +CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST=y +CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID=y +CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESP32_SPIRAM_SUPPORT=y +CONFIG_SPIRAM_SPEED_80M=y +# CONFIG_SPIRAM_BOOT_INIT is not set +CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=4096 +CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=0 +CONFIG_SPIRAM_OCCUPY_HSPI_HOST=y +CONFIG_ESP32_ULP_COPROC_ENABLED=y +CONFIG_ESP32_XTAL_FREQ_AUTO=y +# CONFIG_ETH_USE_SPI_ETHERNET is not set +CONFIG_ETH_SPI_ETHERNET_DM9051=y +CONFIG_ETH_SPI_ETHERNET_W5500=y +CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y +CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 +CONFIG_HTTPD_WS_SUPPORT=y +CONFIG_ESP_HTTPS_SERVER_ENABLE=y +CONFIG_ESP_IPC_TASK_STACK_SIZE=1024 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2048 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=4096 +CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 +CONFIG_ESP_TASK_WDT_PANIC=y +# CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 is not set +CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096 +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=8 +CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM=8 +CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM=16 +CONFIG_ESP32_WIFI_CSI_ENABLED=y +# CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE is not set +CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y +CONFIG_FATFS_CODEPAGE_850=y +CONFIG_FATFS_LFN_STACK=y +CONFIG_FATFS_API_ENCODING_UTF_8=y +# CONFIG_FMB_CONTROLLER_SLAVE_ID_SUPPORT is not set +CONFIG_FMB_TIMER_PORT_ENABLED=y +CONFIG_FREERTOS_HZ=1000 +# CONFIG_FREERTOS_ASSERT_ON_UNTESTED_FUNCTION is not set +CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1024 +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH=y +CONFIG_FREERTOS_FPU_IN_ISR=y +CONFIG_HEAP_POISONING_LIGHT=y +CONFIG_LOG_DEFAULT_LEVEL_ERROR=y +# CONFIG_LOG_COLORS is not set +CONFIG_LWIP_MAX_SOCKETS=16 +CONFIG_LWIP_ETHARP_TRUST_IP_MAC=y +# CONFIG_LWIP_DHCP_DOES_ARP_CHECK is not set +CONFIG_LWIP_DHCP_RESTORE_LAST_IP=y +CONFIG_LWIP_DHCP_OPTIONS_LEN=128 +CONFIG_LWIP_IPV6_AUTOCONFIG=y +CONFIG_LWIP_TCP_SYNMAXRTX=6 +CONFIG_LWIP_TCP_MSS=1436 +CONFIG_LWIP_TCP_RTO_TIME=3000 +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=2560 +CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0=y +CONFIG_LWIP_SNTP_MAX_SERVERS=3 +CONFIG_LWIP_DHCP_GET_NTP_SRV=y +CONFIG_LWIP_SNTP_UPDATE_DELAY=10800000 +# CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN is not set +CONFIG_MBEDTLS_PSK_MODES=y +CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y +CONFIG_MBEDTLS_SSL_PROTO_DTLS=y +CONFIG_MBEDTLS_CAMELLIA_C=y +CONFIG_OPENSSL_ASSERT_DO_NOTHING=y +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=2048 +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=10 +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=2 +CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=4096 +CONFIG_WIFI_PROV_BLE_BONDING=y +CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION=y +# CONFIG_FMB_COMM_MODE_TCP_EN is not set diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 59db61e5..8642407e 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -1,9 +1,11 @@ #include -#include -#include #include "settings.h" -#include "Audio.h" + #include "AudioPlayer.h" + +#include "Audio.h" +#include "Bluetooth.h" +#include "Cmd.h" #include "Common.h" #include "Led.h" #include "Log.h" @@ -15,17 +17,20 @@ #include "RotaryEncoder.h" #include "SdCard.h" #include "System.h" -#include "Wlan.h" #include "Web.h" -#include "Bluetooth.h" -#include "Cmd.h" +#include "Wlan.h" +#include "main.h" -#define AUDIOPLAYER_VOLUME_MAX 21u -#define AUDIOPLAYER_VOLUME_MIN 0u +#include +#include + +#define AUDIOPLAYER_VOLUME_MAX 21u +#define AUDIOPLAYER_VOLUME_MIN 0u #define AUDIOPLAYER_VOLUME_INIT 3u playProps gPlayProperties; -//uint32_t cnt123 = 0; +TaskHandle_t AudioTaskHandle; +// uint32_t cnt123 = 0; // Volume static uint8_t AudioPlayer_CurrentVolume = AUDIOPLAYER_VOLUME_INIT; @@ -34,43 +39,53 @@ static uint8_t AudioPlayer_MaxVolumeSpeaker = AUDIOPLAYER_VOLUME_MAX; static uint8_t AudioPlayer_MinVolume = AUDIOPLAYER_VOLUME_MIN; static uint8_t AudioPlayer_InitVolume = AUDIOPLAYER_VOLUME_INIT; +// current playtime +uint32_t AudioPlayer_CurrentTime; +uint32_t AudioPlayer_FileDuration; + +// Playtime stats +time_t playTimeSecTotal = 0; +time_t playTimeSecSinceStart = 0; + #ifdef HEADPHONE_ADJUST_ENABLE - static bool AudioPlayer_HeadphoneLastDetectionState; - static uint32_t AudioPlayer_HeadphoneLastDetectionTimestamp = 0u; - static uint8_t AudioPlayer_MaxVolumeHeadphone = 11u; // Maximum volume that can be adjusted in headphone-mode (default; can be changed later via GUI) +static bool AudioPlayer_HeadphoneLastDetectionState; +static uint32_t AudioPlayer_HeadphoneLastDetectionTimestamp = 0u; +static uint8_t AudioPlayer_MaxVolumeHeadphone = 11u; // Maximum volume that can be adjusted in headphone-mode (default; can be changed later via GUI) #endif static void AudioPlayer_Task(void *parameter); static void AudioPlayer_HeadphoneVolumeManager(void); static char **AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); static int AudioPlayer_ArrSortHelper(const void *a, const void *b); -static void AudioPlayer_SortPlaylist(const char **arr, int n); -static void AudioPlayer_SortPlaylist(char *str[], const uint32_t count); +static void AudioPlayer_SortPlaylist(char **arr, int n); +static void AudioPlayer_RandomizePlaylist(char **str, const uint32_t count); static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks); static void AudioPlayer_ClearCover(void); void AudioPlayer_Init(void) { - #ifndef USE_LAST_VOLUME_AFTER_REBOOT - // Get initial volume from NVS - uint32_t nvsInitialVolume = gPrefsSettings.getUInt("initVolume", 0); - #else - // Get volume used at last shutdown - uint32_t nvsInitialVolume = gPrefsSettings.getUInt("previousVolume", 999); - if (nvsInitialVolume == 999) { - gPrefsSettings.putUInt("previousVolume", AudioPlayer_GetInitVolume()); - nvsInitialVolume = AudioPlayer_GetInitVolume(); - } else { - Log_Println((char *) FPSTR(rememberLastVolume), LOGLEVEL_ERROR); - } - #endif + // load playtime total from NVS + playTimeSecTotal = gPrefsSettings.getULong("playTimeTotal", 0); + +#ifndef USE_LAST_VOLUME_AFTER_REBOOT + // Get initial volume from NVS + uint32_t nvsInitialVolume = gPrefsSettings.getUInt("initVolume", 0); +#else + // Get volume used at last shutdown + uint32_t nvsInitialVolume = gPrefsSettings.getUInt("previousVolume", 999); + if (nvsInitialVolume == 999) { + gPrefsSettings.putUInt("previousVolume", AudioPlayer_GetInitVolume()); + nvsInitialVolume = AudioPlayer_GetInitVolume(); + } else { + Log_Println(rememberLastVolume, LOGLEVEL_ERROR); + } +#endif if (nvsInitialVolume) { AudioPlayer_SetInitVolume(nvsInitialVolume); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredInitialLoudnessFromNvs), nvsInitialVolume); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, restoredInitialLoudnessFromNvs, nvsInitialVolume); } else { gPrefsSettings.putUInt("initVolume", AudioPlayer_GetInitVolume()); - Log_Println((char *) FPSTR(wroteInitialLoudnessToNvs), LOGLEVEL_ERROR); + Log_Println(wroteInitialLoudnessToNvs, LOGLEVEL_ERROR); } // Get maximum volume for speaker from NVS @@ -78,63 +93,84 @@ void AudioPlayer_Init(void) { if (nvsMaxVolumeSpeaker) { AudioPlayer_SetMaxVolumeSpeaker(nvsMaxVolumeSpeaker); AudioPlayer_SetMaxVolume(nvsMaxVolumeSpeaker); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredMaxLoudnessForSpeakerFromNvs), nvsMaxVolumeSpeaker); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, restoredMaxLoudnessForSpeakerFromNvs, nvsMaxVolumeSpeaker); } else { gPrefsSettings.putUInt("maxVolumeSp", nvsMaxVolumeSpeaker); - Log_Println((char *) FPSTR(wroteMaxLoudnessForSpeakerToNvs), LOGLEVEL_ERROR); + Log_Println(wroteMaxLoudnessForSpeakerToNvs, LOGLEVEL_ERROR); } - #ifdef HEADPHONE_ADJUST_ENABLE - #if (HP_DETECT >= 0 && HP_DETECT <= MAX_GPIO) - pinMode(HP_DETECT, INPUT); - #endif - AudioPlayer_HeadphoneLastDetectionState = Audio_Detect_Mode_HP(Port_Read(HP_DETECT)); - - // Get maximum volume for headphone from NVS - uint32_t nvsAudioPlayer_MaxVolumeHeadphone = gPrefsSettings.getUInt("maxVolumeHp", 0); - if (nvsAudioPlayer_MaxVolumeHeadphone) { - AudioPlayer_MaxVolumeHeadphone = nvsAudioPlayer_MaxVolumeHeadphone; - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredMaxLoudnessForHeadphoneFromNvs), nvsAudioPlayer_MaxVolumeHeadphone); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putUInt("maxVolumeHp", nvsAudioPlayer_MaxVolumeHeadphone); - Log_Println((char *) FPSTR(wroteMaxLoudnessForHeadphoneToNvs), LOGLEVEL_ERROR); - } +#ifdef HEADPHONE_ADJUST_ENABLE + #if (HP_DETECT >= 0 && HP_DETECT <= MAX_GPIO) + pinMode(HP_DETECT, INPUT); #endif + AudioPlayer_HeadphoneLastDetectionState = Audio_Detect_Mode_HP(Port_Read(HP_DETECT)); + + // Get maximum volume for headphone from NVS + uint32_t nvsAudioPlayer_MaxVolumeHeadphone = gPrefsSettings.getUInt("maxVolumeHp", 0); + if (nvsAudioPlayer_MaxVolumeHeadphone) { + AudioPlayer_MaxVolumeHeadphone = nvsAudioPlayer_MaxVolumeHeadphone; + Log_Printf(LOGLEVEL_INFO, restoredMaxLoudnessForHeadphoneFromNvs, nvsAudioPlayer_MaxVolumeHeadphone); + } else { + gPrefsSettings.putUInt("maxVolumeHp", nvsAudioPlayer_MaxVolumeHeadphone); + Log_Println(wroteMaxLoudnessForHeadphoneToNvs, LOGLEVEL_ERROR); + } +#endif // Adjust volume depending on headphone is connected and volume-adjustment is enabled AudioPlayer_SetupVolumeAndAmps(); // clear title and cover image - gPlayProperties.title = NULL; + gPlayProperties.title[0] = '\0'; gPlayProperties.coverFilePos = 0; // Don't start audio-task in BT-speaker mode! if ((System_GetOperationMode() == OPMODE_NORMAL) || (System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE)) { xTaskCreatePinnedToCore( - AudioPlayer_Task, /* Function to implement the task */ - "mp3play", /* Name of the task */ - 5500, /* Stack size in words */ - NULL, /* Task input parameter */ + AudioPlayer_Task, /* Function to implement the task */ + "mp3play", /* Name of the task */ + 6000, /* Stack size in words */ + NULL, /* Task input parameter */ 2 | portPRIVILEGE_BIT, /* Priority of the task */ - NULL, /* Task handle. */ - 1 /* Core where the task should run */ + &AudioTaskHandle, /* Task handle. */ + 1 /* Core where the task should run */ ); } } +void AudioPlayer_Exit(void) { + Log_Println("shutdown audioplayer..", LOGLEVEL_NOTICE); + // save playtime total to NVS + playTimeSecTotal += playTimeSecSinceStart; + gPrefsSettings.putULong("playTimeTotal", playTimeSecTotal); +// Make sure last playposition for audiobook is saved when playback is active while shutdown was initiated +#ifdef SAVE_PLAYPOS_BEFORE_SHUTDOWN + if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); + while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS + vTaskDelay(portTICK_PERIOD_MS * 100u); + } + } +#endif +} + +static uint32_t lastPlayingTimestamp = 0; + void AudioPlayer_Cyclic(void) { AudioPlayer_HeadphoneVolumeManager(); + if ((millis() - lastPlayingTimestamp >= 1000) && gPlayProperties.playMode != NO_PLAYLIST && gPlayProperties.playMode != BUSY && !gPlayProperties.pausePlay) { + // audio is playing, update the playtime since start + lastPlayingTimestamp = millis(); + playTimeSecSinceStart += 1; + } } // Wrapper-function to reverse detection of connected headphones. // Normally headphones are supposed to be plugged in if a given GPIO/channel is LOW/false. bool Audio_Detect_Mode_HP(bool _state) { - #ifndef DETECT_HP_ON_HIGH - return _state; - #else - return !_state; - #endif +#ifndef DETECT_HP_ON_HIGH + return _state; +#else + return !_state; +#endif } uint8_t AudioPlayer_GetCurrentVolume(void) { @@ -177,117 +213,123 @@ void AudioPlayer_SetInitVolume(uint8_t value) { AudioPlayer_InitVolume = value; } -void Audio_setTitle(const char *format, ...) -{ - // Allocates space for title of current track only once and keeps char* in order to avoid heap-fragmentation. - static char* _title = NULL; - if (_title == NULL) { - _title = (char *) x_malloc(sizeof(char) * 255); - gPlayProperties.title = _title; - } +time_t AudioPlayer_GetPlayTimeAllTime(void) { + return (playTimeSecTotal + playTimeSecSinceStart) * 1000; +} + +time_t AudioPlayer_GetPlayTimeSinceStart(void) { + return (playTimeSecSinceStart * 1000); +} + +uint32_t AudioPlayer_GetCurrentTime(void) { + return AudioPlayer_CurrentTime; +} +uint32_t AudioPlayer_GetFileDuration(void) { + return AudioPlayer_FileDuration; +} + +void Audio_setTitle(const char *format, ...) { char buf[256]; va_list args; va_start(args, format); - vsnprintf(buf, sizeof(buf)/sizeof(buf[0]), format, args); + vsnprintf(buf, sizeof(buf) / sizeof(buf[0]), format, args); va_end(args); convertAsciiToUtf8(buf, gPlayProperties.title); // notify web ui and mqtt Web_SendWebsocketData(0, 30); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicTrackState), gPlayProperties.title, false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicTrackState, gPlayProperties.title, false); +#endif } // Set maxVolume depending on headphone-adjustment is enabled and headphone is/is not connected // Enable/disable PA/HP-amps initially void AudioPlayer_SetupVolumeAndAmps(void) { - #ifdef PLAY_MONO_SPEAKER - gPlayProperties.currentPlayMono = true; - gPlayProperties.newPlayMono = true; - #else - gPlayProperties.currentPlayMono = false; - gPlayProperties.newPlayMono = false; +#ifdef PLAY_MONO_SPEAKER + gPlayProperties.currentPlayMono = true; + gPlayProperties.newPlayMono = true; +#else + gPlayProperties.currentPlayMono = false; + gPlayProperties.newPlayMono = false; +#endif + +#ifndef HEADPHONE_ADJUST_ENABLE + AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeSpeaker; + // If automatic HP-detection is not used, we enabled both (PA / HP) if defined + #ifdef GPIO_PA_EN + Port_Write(GPIO_PA_EN, true, true); + #endif + #ifdef GPIO_HP_EN + Port_Write(GPIO_HP_EN, true, true); #endif +#else + if (Audio_Detect_Mode_HP(Port_Read(HP_DETECT))) { + AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeSpeaker; // 1 if headphone is not connected + #ifdef GPIO_PA_EN + Port_Write(GPIO_PA_EN, true, true); + #endif + #ifdef GPIO_HP_EN + Port_Write(GPIO_HP_EN, false, true); + #endif + } else { + AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeHeadphone; // 0 if headphone is connected (put to GND) + gPlayProperties.newPlayMono = false; // always stereo for headphones! - #ifndef HEADPHONE_ADJUST_ENABLE - AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeSpeaker; - // If automatic HP-detection is not used, we enabled both (PA / HP) if defined - #ifdef GPIO_PA_EN - Port_Write(GPIO_PA_EN, true, true); - #endif - #ifdef GPIO_HP_EN - Port_Write(GPIO_HP_EN, true, true); - #endif - #else - if (Audio_Detect_Mode_HP(Port_Read(HP_DETECT))) { - AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeSpeaker; // 1 if headphone is not connected - #ifdef GPIO_PA_EN - Port_Write(GPIO_PA_EN, true, true); - #endif - #ifdef GPIO_HP_EN - Port_Write(GPIO_HP_EN, false, true); - #endif - } else { - AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeHeadphone; // 0 if headphone is connected (put to GND) - gPlayProperties.newPlayMono = false; // always stereo for headphones! - - #ifdef GPIO_PA_EN - Port_Write(GPIO_PA_EN, false, true); - #endif - #ifdef GPIO_HP_EN - Port_Write(GPIO_HP_EN, true, true); - #endif - } - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(maxVolumeSet), AudioPlayer_MaxVolume); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - return; + #ifdef GPIO_PA_EN + Port_Write(GPIO_PA_EN, false, true); + #endif + #ifdef GPIO_HP_EN + Port_Write(GPIO_HP_EN, true, true); #endif + } + Log_Printf(LOGLEVEL_INFO, maxVolumeSet, AudioPlayer_MaxVolume); + return; +#endif } void AudioPlayer_HeadphoneVolumeManager(void) { - #ifdef HEADPHONE_ADJUST_ENABLE - bool currentHeadPhoneDetectionState = Audio_Detect_Mode_HP(Port_Read(HP_DETECT)); - - if (AudioPlayer_HeadphoneLastDetectionState != currentHeadPhoneDetectionState && (millis() - AudioPlayer_HeadphoneLastDetectionTimestamp >= headphoneLastDetectionDebounce)) { - if (currentHeadPhoneDetectionState) { - AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeSpeaker; - #ifdef PLAY_MONO_SPEAKER - gPlayProperties.newPlayMono = true; - #else - gPlayProperties.newPlayMono = false; - #endif - - #ifdef GPIO_PA_EN - Port_Write(GPIO_PA_EN, true, false); - #endif - #ifdef GPIO_HP_EN - Port_Write(GPIO_HP_EN, false, false); - #endif - } else { - AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeHeadphone; - gPlayProperties.newPlayMono = false; // Always stereo for headphones - if (AudioPlayer_GetCurrentVolume() > AudioPlayer_MaxVolume) { - AudioPlayer_VolumeToQueueSender(AudioPlayer_MaxVolume, true); // Lower volume for headphone if headphone's maxvolume is exceeded by volume set in speaker-mode - } +#ifdef HEADPHONE_ADJUST_ENABLE + bool currentHeadPhoneDetectionState = Audio_Detect_Mode_HP(Port_Read(HP_DETECT)); + + if (AudioPlayer_HeadphoneLastDetectionState != currentHeadPhoneDetectionState && (millis() - AudioPlayer_HeadphoneLastDetectionTimestamp >= headphoneLastDetectionDebounce)) { + if (currentHeadPhoneDetectionState) { + AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeSpeaker; + #ifdef PLAY_MONO_SPEAKER + gPlayProperties.newPlayMono = true; + #else + gPlayProperties.newPlayMono = false; + #endif - #ifdef GPIO_PA_EN - Port_Write(GPIO_PA_EN, false, false); - #endif - #ifdef GPIO_HP_EN - Port_Write(GPIO_HP_EN, true, false); - #endif + #ifdef GPIO_PA_EN + Port_Write(GPIO_PA_EN, true, false); + #endif + #ifdef GPIO_HP_EN + Port_Write(GPIO_HP_EN, false, false); + #endif + } else { + AudioPlayer_MaxVolume = AudioPlayer_MaxVolumeHeadphone; + gPlayProperties.newPlayMono = false; // Always stereo for headphones + if (AudioPlayer_GetCurrentVolume() > AudioPlayer_MaxVolume) { + AudioPlayer_VolumeToQueueSender(AudioPlayer_MaxVolume, true); // Lower volume for headphone if headphone's maxvolume is exceeded by volume set in speaker-mode } - AudioPlayer_HeadphoneLastDetectionState = currentHeadPhoneDetectionState; - AudioPlayer_HeadphoneLastDetectionTimestamp = millis(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(maxVolumeSet), AudioPlayer_MaxVolume); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } + + #ifdef GPIO_PA_EN + Port_Write(GPIO_PA_EN, false, false); + #endif + #ifdef GPIO_HP_EN + Port_Write(GPIO_HP_EN, true, false); #endif + } + AudioPlayer_HeadphoneLastDetectionState = currentHeadPhoneDetectionState; + AudioPlayer_HeadphoneLastDetectionTimestamp = millis(); + Log_Printf(LOGLEVEL_INFO, maxVolumeSet, AudioPlayer_MaxVolume); + } +#endif } -class AudioCustom: public Audio { +class AudioCustom : public Audio { public: void *operator new(size_t size) { return psramFound() ? ps_malloc(size) : malloc(size); @@ -296,20 +338,21 @@ class AudioCustom: public Audio { // Function to play music as task void AudioPlayer_Task(void *parameter) { - #ifdef BOARD_HAS_PSRAM - AudioCustom *audio = new AudioCustom(); - #else - static Audio audioAsStatic; // Don't use heap as it's needed for other stuff :-) - Audio *audio = &audioAsStatic; - #endif +#ifdef BOARD_HAS_PSRAM + AudioCustom *audio = new AudioCustom(); +#else + static Audio audioAsStatic; // Don't use heap as it's needed for other stuff :-) + Audio *audio = &audioAsStatic; +#endif - #ifdef I2S_COMM_FMT_LSB_ENABLE - audio->setI2SCommFMT_LSB(true); - #endif +#ifdef I2S_COMM_FMT_LSB_ENABLE + audio->setI2SCommFMT_LSB(true); +#endif uint8_t settleCount = 0; + AudioPlayer_CurrentVolume = AudioPlayer_GetInitVolume(); audio->setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); - audio->setVolume(AudioPlayer_GetInitVolume()); + audio->setVolume(AudioPlayer_CurrentVolume, VOLUMECURVE); audio->forceMono(gPlayProperties.currentPlayMono); if (gPlayProperties.currentPlayMono) { audio->setTone(3, 0, 0); @@ -319,27 +362,43 @@ void AudioPlayer_Task(void *parameter) { static BaseType_t trackQStatus; static uint8_t trackCommand = NO_ACTION; bool audioReturnCode; + AudioPlayer_CurrentTime = 0; + AudioPlayer_FileDuration = 0; + static uint32_t AudioPlayer_LastPlaytimeStatsTimestamp = 0u; for (;;) { /* if (cnt123++ % 100 == 0) { - snprintf(Log_Buffer, Log_BufferLength, "%u", uxTaskGetStackHighWaterMark(NULL)); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + Log_Printf(LOGLEVEL_DEBUG, "%u", uxTaskGetStackHighWaterMark(NULL)); } */ if (xQueueReceive(gVolumeQueue, ¤tVolume, 0) == pdPASS) { - snprintf(Log_Buffer, Log_BufferLength, "%s: %d", (char *) FPSTR(newLoudnessReceivedQueue), currentVolume); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - audio->setVolume(currentVolume); + Log_Printf(LOGLEVEL_INFO, newLoudnessReceivedQueue, currentVolume); + audio->setVolume(currentVolume, VOLUMECURVE); Web_SendWebsocketData(0, 50); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLoudnessState), currentVolume, false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicLoudnessState, currentVolume, false); +#endif } if (xQueueReceive(gTrackControlQueue, &trackCommand, 0) == pdPASS) { - snprintf(Log_Buffer, Log_BufferLength, "%s: %d", (char *) FPSTR(newCntrlReceivedQueue), trackCommand); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, newCntrlReceivedQueue, trackCommand); + } + + // Update playtime stats every 250 ms + if ((millis() - AudioPlayer_LastPlaytimeStatsTimestamp) > 250) { + AudioPlayer_LastPlaytimeStatsTimestamp = millis(); + // Update current playtime and duration + AudioPlayer_CurrentTime = audio->getAudioCurrentTime(); + AudioPlayer_FileDuration = audio->getAudioFileDuration(); + // Calculate relative position in file (for trackprogress neopixel & web-ui) + if (!gPlayProperties.playlistFinished && !gPlayProperties.isWebstream) { + if (!gPlayProperties.pausePlay && (gPlayProperties.seekmode != SEEK_POS_PERCENT) && (audio->getFileSize() > 0)) { // To progress necessary when paused + gPlayProperties.currentRelPos = ((double) (audio->getFilePos() - audio->inBufferFilled()) / (double) audio->getFileSize()) * 100; + } + } else { + gPlayProperties.currentRelPos = 0; + } } trackQStatus = xQueueReceive(gTrackQueue, &gPlayProperties.playlist, 0); @@ -349,20 +408,13 @@ void AudioPlayer_Task(void *parameter) { gPlayProperties.pausePlay = false; } audio->stopSong(); - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "%s mit %d Titel(n)", (char *) FPSTR(newPlaylistReceived), gPlayProperties.numberOfTracks); - #else - snprintf(Log_Buffer, Log_BufferLength, "%s with %d track(s)", (char *) FPSTR(newPlaylistReceived), gPlayProperties.numberOfTracks); - #endif - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, newPlaylistReceived, gPlayProperties.numberOfTracks); + Log_Printf(LOGLEVEL_DEBUG, "Free heap: %u", ESP.getFreeHeap()); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) F("Free heap: "), ESP.getFreeHeap()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicPlaymodeState), gPlayProperties.playMode, false); - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicPlaymodeState, gPlayProperties.playMode, false); + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); +#endif // If we're in audiobook-mode and apply a modification-card, we don't // want to save lastPlayPosition for the mod-card but for the card that holds the playlist @@ -377,27 +429,28 @@ void AudioPlayer_Task(void *parameter) { continue; } if (gPlayProperties.saveLastPlayPosition) { // Don't save for AUDIOBOOK_LOOP because not necessary - if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.numberOfTracks) - { + if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.numberOfTracks) { // Only save if there's another track, otherwise it will be saved at end of playlist anyway AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks); } } if (gPlayProperties.sleepAfterCurrentTrack) { // Go to sleep if "sleep after track" was requested + gPlayProperties.playlistFinished = true; + gPlayProperties.playMode = NO_PLAYLIST; System_RequestSleep(); break; } if (!gPlayProperties.repeatCurrentTrack) { // If endless-loop requested, track-number will not be incremented gPlayProperties.currentTrackNumber++; } else { - Log_Println((char *) FPSTR(repeatTrackDueToPlaymode), LOGLEVEL_INFO); + Log_Println(repeatTrackDueToPlaymode, LOGLEVEL_INFO); Led_Indicate(LedIndicatorType::Rewind); } } if (gPlayProperties.playlistFinished && trackCommand != NO_ACTION) { - if (gPlayProperties.playMode != BUSY ) { // Prevents from staying in mode BUSY forever when error occured (e.g. directory empty that should be played) - Log_Println((char *) FPSTR(noPlaymodeChangeIfIdle), LOGLEVEL_NOTICE); + if (gPlayProperties.playMode != BUSY) { // Prevents from staying in mode BUSY forever when error occured (e.g. directory empty that should be played) + Log_Println(noPlaymodeChangeIfIdle, LOGLEVEL_NOTICE); trackCommand = NO_ACTION; System_IndicateError(); continue; @@ -409,21 +462,24 @@ void AudioPlayer_Task(void *parameter) { case STOP: audio->stopSong(); trackCommand = NO_ACTION; - Log_Println((char *) FPSTR(cmndStop), LOGLEVEL_INFO); + Log_Println(cmndStop, LOGLEVEL_INFO); gPlayProperties.pausePlay = true; gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; - Audio_setTitle((char *)FPSTR (noPlaylist)); + Audio_setTitle(noPlaylist); AudioPlayer_ClearCover(); continue; case PAUSEPLAY: trackCommand = NO_ACTION; audio->pauseResume(); - Log_Println((char *) FPSTR(cmndPause), LOGLEVEL_INFO); + if (gPlayProperties.pausePlay) { + Log_Println(cmndResumeFromPause, LOGLEVEL_INFO); + } else { + Log_Println(cmndPause, LOGLEVEL_INFO); + } if (gPlayProperties.saveLastPlayPosition && !gPlayProperties.pausePlay) { - snprintf(Log_Buffer, Log_BufferLength, "%s: %u (%u)", (char *) FPSTR(trackPausedAtPos), audio->getFilePos(), audio->getFilePos() - audio->inBufferFilled()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, trackPausedAtPos, audio->getFilePos(), audio->getFilePos() - audio->inBufferFilled()); AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); } gPlayProperties.pausePlay = !gPlayProperties.pausePlay; @@ -438,9 +494,9 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.repeatCurrentTrack) { // End loop if button was pressed gPlayProperties.repeatCurrentTrack = false; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); +#endif } // Allow next track if current track played in playlist isn't the last track. // Exception: loop-playlist is active. In this case playback restarts at the first track of the playlist. @@ -452,14 +508,14 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.saveLastPlayPosition) { AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - Log_Println((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } - Log_Println((char *) FPSTR(cmndNextTrack), LOGLEVEL_INFO); + Log_Println(cmndNextTrack, LOGLEVEL_INFO); if (!gPlayProperties.playlistFinished) { audio->stopSong(); } } else { - Log_Println((char *) FPSTR(lastTrackAlreadyActive), LOGLEVEL_NOTICE); + Log_Println(lastTrackAlreadyActive, LOGLEVEL_NOTICE); System_IndicateError(); continue; } @@ -473,51 +529,59 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.repeatCurrentTrack) { // End loop if button was pressed gPlayProperties.repeatCurrentTrack = false; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); +#endif } - if (gPlayProperties.currentTrackNumber > 0 || gPlayProperties.repeatPlaylist) { - if (audio->getAudioCurrentTime() < 5) { // play previous track when current track time is small, else play current track again - if (gPlayProperties.currentTrackNumber == 0 && gPlayProperties.repeatPlaylist) { - gPlayProperties.currentTrackNumber = gPlayProperties.numberOfTracks - 1; // Go back to last track in loop-mode when first track is played - } else { - gPlayProperties.currentTrackNumber--; - } + if (gPlayProperties.playMode == WEBSTREAM) { + Log_Println(trackChangeWebstream, LOGLEVEL_INFO); + System_IndicateError(); + continue; + } else if (gPlayProperties.playMode == LOCAL_M3U) { + Log_Println(cmndPrevTrack, LOGLEVEL_INFO); + if (gPlayProperties.currentTrackNumber > 0) { + gPlayProperties.currentTrackNumber--; + } else { + System_IndicateError(); + continue; } + } else { + if (gPlayProperties.currentTrackNumber > 0 || gPlayProperties.repeatPlaylist) { + if (audio->getAudioCurrentTime() < 5) { // play previous track when current track time is small, else play current track again + if (gPlayProperties.currentTrackNumber == 0 && gPlayProperties.repeatPlaylist) { + gPlayProperties.currentTrackNumber = gPlayProperties.numberOfTracks - 1; // Go back to last track in loop-mode when first track is played + } else { + gPlayProperties.currentTrackNumber--; + } + } - if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - Log_Println((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); - } + if (gPlayProperties.saveLastPlayPosition) { + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + Log_Println(trackStartAudiobook, LOGLEVEL_INFO); + } - Log_Println((char *) FPSTR(cmndPrevTrack), LOGLEVEL_INFO); - if (!gPlayProperties.playlistFinished) { + Log_Println(cmndPrevTrack, LOGLEVEL_INFO); + if (!gPlayProperties.playlistFinished) { + audio->stopSong(); + } + } else { + if (gPlayProperties.saveLastPlayPosition) { + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + } audio->stopSong(); - } - } else { - if (gPlayProperties.playMode == WEBSTREAM) { - Log_Println((char *) FPSTR(trackChangeWebstream), LOGLEVEL_INFO); - System_IndicateError(); - continue; - } - if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - } - audio->stopSong(); - Led_Indicate(LedIndicatorType::Rewind); - audioReturnCode = audio->connecttoFS(gFSystem, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); - // consider track as finished, when audio lib call was not successful - if (!audioReturnCode) { - System_IndicateError(); - gPlayProperties.trackFinished = true; + Led_Indicate(LedIndicatorType::Rewind); + audioReturnCode = audio->connecttoFS(gFSystem, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + // consider track as finished, when audio lib call was not successful + if (!audioReturnCode) { + System_IndicateError(); + gPlayProperties.trackFinished = true; + continue; + } + Log_Println(trackStart, LOGLEVEL_INFO); continue; } - Log_Println((char *) FPSTR(trackStart), LOGLEVEL_INFO); - continue; } break; - case FIRSTTRACK: trackCommand = NO_ACTION; if (gPlayProperties.pausePlay) { @@ -527,9 +591,9 @@ void AudioPlayer_Task(void *parameter) { gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - Log_Println((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } - Log_Println((char *) FPSTR(cmndFirstTrack), LOGLEVEL_INFO); + Log_Println(cmndFirstTrack, LOGLEVEL_INFO); if (!gPlayProperties.playlistFinished) { audio->stopSong(); } @@ -545,14 +609,14 @@ void AudioPlayer_Task(void *parameter) { gPlayProperties.currentTrackNumber = gPlayProperties.numberOfTracks - 1; if (gPlayProperties.saveLastPlayPosition) { AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - Log_Println((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } - Log_Println((char *) FPSTR(cmndLastTrack), LOGLEVEL_INFO); + Log_Println(cmndLastTrack, LOGLEVEL_INFO); if (!gPlayProperties.playlistFinished) { audio->stopSong(); } } else { - Log_Println((char *) FPSTR(lastTrackAlreadyActive), LOGLEVEL_NOTICE); + Log_Println(lastTrackAlreadyActive, LOGLEVEL_NOTICE); System_IndicateError(); continue; } @@ -563,7 +627,7 @@ void AudioPlayer_Task(void *parameter) { default: trackCommand = NO_ACTION; - Log_Println((char *) FPSTR(cmndDoesNotExist), LOGLEVEL_NOTICE); + Log_Println(cmndDoesNotExist, LOGLEVEL_NOTICE); System_IndicateError(); continue; } @@ -579,7 +643,7 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.currentTrackNumber >= gPlayProperties.numberOfTracks) { // Check if last element of playlist is already reached - Log_Println((char *) FPSTR(endOfPlaylistReached), LOGLEVEL_NOTICE); + Log_Println(endOfPlaylistReached, LOGLEVEL_NOTICE); if (!gPlayProperties.repeatPlaylist) { if (gPlayProperties.saveLastPlayPosition) { // Set back to first track @@ -587,11 +651,11 @@ void AudioPlayer_Task(void *parameter) { } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; - Audio_setTitle((char *)FPSTR (noPlaylist)); + Audio_setTitle(noPlaylist); AudioPlayer_ClearCover(); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicPlaymodeState), gPlayProperties.playMode, false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicPlaymodeState, gPlayProperties.playMode, false); +#endif gPlayProperties.currentTrackNumber = 0; gPlayProperties.numberOfTracks = 0; if (gPlayProperties.sleepAfterPlaylist) { @@ -605,7 +669,7 @@ void AudioPlayer_Task(void *parameter) { System_RequestSleep(); continue; } // Repeat playlist; set current track number back to 0 - Log_Println((char *) FPSTR(repeatPlaylistDueToPlaymode), LOGLEVEL_NOTICE); + Log_Println(repeatPlaylistDueToPlaymode, LOGLEVEL_NOTICE); gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + 0), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); @@ -624,11 +688,11 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.playMode == WEBSTREAM || (gPlayProperties.playMode == LOCAL_M3U && gPlayProperties.isWebstream)) { // Webstream audioReturnCode = audio->connecttohost(*(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); gPlayProperties.playlistFinished = false; + gTriedToConnectToHost = true; } else if (gPlayProperties.playMode != WEBSTREAM && !gPlayProperties.isWebstream) { // Files from SD if (!gFSystem.exists(*(gPlayProperties.playlist + gPlayProperties.currentTrackNumber))) { // Check first if file/folder exists - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(dirOrFileDoesNotExist), *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); gPlayProperties.trackFinished = true; continue; } else { @@ -648,29 +712,23 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.startAtFilePos > 0) { audio->setFilePos(gPlayProperties.startAtFilePos); gPlayProperties.startAtFilePos = 0; - snprintf(Log_Buffer, Log_BufferLength, "%s %u", (char *) FPSTR(trackStartatPos), audio->getFilePos()); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, trackStartatPos, audio->getFilePos()); } if (gPlayProperties.isWebstream) { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): Webradio", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks); + Audio_setTitle("(%u/%u): Webradio", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks); } else { Audio_setTitle("Webradio"); } } else { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); } else { Audio_setTitle("%s", *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); } } AudioPlayer_ClearCover(); - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "'%s' wird abgespielt (%d von %d)", *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); - #else - snprintf(Log_Buffer, Log_BufferLength, "'%s' is being played (%d of %d)", *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); - #endif - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); gPlayProperties.playlistFinished = false; } } @@ -679,23 +737,20 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.seekmode != SEEK_NORMAL) { if (gPlayProperties.seekmode == SEEK_FORWARDS) { if (audio->setTimeOffset(jumpOffset)) { - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "%d Sekunden nach vorne gesprungen", jumpOffset); - #else - snprintf(Log_Buffer, Log_BufferLength, "Jumped %d seconds forwards", jumpOffset); - #endif - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, secondsJumpForward, jumpOffset); } else { System_IndicateError(); } } else if (gPlayProperties.seekmode == SEEK_BACKWARDS) { if (audio->setTimeOffset(-(jumpOffset))) { - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "%d Sekunden zurueck gesprungen", jumpOffset); - #else - snprintf(Log_Buffer, Log_BufferLength, "Jumped %d seconds backwards", jumpOffset); - #endif - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, secondsJumpBackward, jumpOffset); + } else { + System_IndicateError(); + } + } else if ((gPlayProperties.seekmode == SEEK_POS_PERCENT) && (gPlayProperties.currentRelPos > 0) && (gPlayProperties.currentRelPos < 100)) { + uint32_t newFilePos = uint32_t((double) (gPlayProperties.currentRelPos / 100) * audio->getFileSize()); + if (audio->setFilePos(newFilePos)) { + Log_Printf(LOGLEVEL_NOTICE, JumpToPosition, newFilePos, audio->getFileSize()); } else { System_IndicateError(); } @@ -704,16 +759,40 @@ void AudioPlayer_Task(void *parameter) { } // Handle IP-announcement - if (gPlayProperties.tellIpAddress) { - gPlayProperties.tellIpAddress = false; - char ipBuf[16]; - Wlan_GetIpAddress().toCharArray(ipBuf, sizeof(ipBuf)); + if (gPlayProperties.tellMode == TTS_IP_ADDRESS) { + gPlayProperties.tellMode = TTS_NONE; + String ipText = Wlan_GetIpAddress(); + bool speechOk; +#if (LANGUAGE == DE) + ipText.replace(".", "Punkt"); // make IP as text (replace thousand separator) + speechOk = audio->connecttospeech(ipText.c_str(), "de"); +#else + ipText.replace(".", "point"); + speechOk = audio->connecttospeech(ipText.c_str(), "en"); +#endif + if (!speechOk) { + System_IndicateError(); + } + } + + // Handle time-announcement + if (gPlayProperties.tellMode == TTS_CURRENT_TIME) { + gPlayProperties.tellMode = TTS_NONE; + struct tm timeinfo; + getLocalTime(&timeinfo); + static char timeStringBuff[64]; bool speechOk; - #if (LANGUAGE == DE) - speechOk = audio->connecttospeech(ipBuf, "de"); - #else - speechOk = audio->connecttospeech(ipBuf, "en"); - #endif +#if (LANGUAGE == DE) + snprintf(timeStringBuff, sizeof(timeStringBuff), "Es ist %02d:%02d Uhr", timeinfo.tm_hour, timeinfo.tm_min); + speechOk = audio->connecttospeech(timeStringBuff, "de"); +#else + if (timeinfo.tm_hour > 12) { + snprintf(timeStringBuff, sizeof(timeStringBuff), "It is %02d:%02d PM", timeinfo.tm_hour - 12, timeinfo.tm_min); + } else { + snprintf(timeStringBuff, sizeof(timeStringBuff), "It is %02d:%02d AM", timeinfo.tm_hour, timeinfo.tm_min); + } + speechOk = audio->connecttospeech(timeStringBuff, "en"); +#endif if (!speechOk) { System_IndicateError(); } @@ -723,7 +802,7 @@ void AudioPlayer_Task(void *parameter) { if (!gPlayProperties.currentSpeechActive && gPlayProperties.lastSpeechActive) { gPlayProperties.lastSpeechActive = false; if (gPlayProperties.playMode != NO_PLAYLIST) { - xQueueSend(gRfidCardQueue, gPlayProperties.playRfidTag, 0); // Re-inject previous RFID-ID in order to continue playback + xQueueSend(gRfidCardQueue, gPlayProperties.playRfidTag, 0); // Re-inject previous RFID-ID in order to continue playback } } @@ -740,17 +819,6 @@ void AudioPlayer_Task(void *parameter) { } } - // Calculate relative position in file (for neopixel) for SD-card-mode - if (!gPlayProperties.playlistFinished && !gPlayProperties.isWebstream) { - if (millis() % 20 == 0) { // Keep it simple - if (!gPlayProperties.pausePlay) { // To progress necessary when paused - gPlayProperties.currentRelPos = ((double)(audio->getFilePos() - audio->inBufferFilled()) / (double)audio->getFileSize()) * 100; - } - } - } else { - gPlayProperties.currentRelPos = 0; - } - audio->loop(); if (gPlayProperties.playlistFinished || gPlayProperties.pausePlay) { if (!gPlayProperties.currentSpeechActive) { @@ -777,7 +845,19 @@ void AudioPlayer_Task(void *parameter) { } else { vTaskDelay(portTICK_PERIOD_MS * 1); } - //esp_task_wdt_reset(); // Don't forget to feed the dog! + // esp_task_wdt_reset(); // Don't forget to feed the dog! + +#ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE + static uint8_t resetOnNextIdle = false; + if (gPlayProperties.playlistFinished || gPlayProperties.playMode == NO_PLAYLIST) { + if (resetOnNextIdle) { + Rfid_ResetOldRfid(); + resetOnNextIdle = false; + } + } else { + resetOnNextIdle = true; + } +#endif } vTaskDelete(NULL); } @@ -790,7 +870,7 @@ uint8_t AudioPlayer_GetRepeatMode(void) { return PLAYLIST; } else if (!gPlayProperties.repeatPlaylist && gPlayProperties.repeatCurrentTrack) { return TRACK; - } else{ + } else { return NO_REPEAT; } } @@ -801,11 +881,12 @@ void AudioPlayer_VolumeToQueueSender(const int32_t _newVolume, bool reAdjustRota uint32_t _volume; int32_t _volumeBuf = AudioPlayer_GetCurrentVolume(); + Led_Indicate(LedIndicatorType::VolumeChange); if (_newVolume < AudioPlayer_GetMinVolume()) { - Log_Println((char *) FPSTR(minLoudnessReached), LOGLEVEL_INFO); + Log_Println(minLoudnessReached, LOGLEVEL_INFO); return; } else if (_newVolume > AudioPlayer_GetMaxVolume()) { - Log_Println((char *) FPSTR(maxLoudnessReached), LOGLEVEL_INFO); + Log_Println(maxLoudnessReached, LOGLEVEL_INFO); return; } else { _volume = _newVolume; @@ -818,55 +899,55 @@ void AudioPlayer_VolumeToQueueSender(const int32_t _newVolume, bool reAdjustRota } } - // Pauses playback if playback is active and volume is changes from minVolume+1 to minVolume (usually 0) void AudioPlayer_PauseOnMinVolume(const uint8_t oldVolume, const uint8_t newVolume) { - #ifdef PAUSE_ON_MIN_VOLUME - if (gPlayProperties.playMode == BUSY || gPlayProperties.playMode == NO_PLAYLIST) { - return; - } +#ifdef PAUSE_ON_MIN_VOLUME + if (gPlayProperties.playMode == BUSY || gPlayProperties.playMode == NO_PLAYLIST) { + return; + } - if (!gPlayProperties.pausePlay ) { // Volume changes from 1 to 0 - if (oldVolume == AudioPlayer_GetMinVolume()+1 && newVolume == AudioPlayer_GetMinVolume()) { - Cmd_Action(CMD_PLAYPAUSE); - } + if (!gPlayProperties.pausePlay) { // Volume changes from 1 to 0 + if (oldVolume == AudioPlayer_GetMinVolume() + 1 && newVolume == AudioPlayer_GetMinVolume()) { + Cmd_Action(CMD_PLAYPAUSE); } - if (gPlayProperties.pausePlay) { // Volume changes from 0 to 1 - if (oldVolume == AudioPlayer_GetMinVolume() && newVolume > AudioPlayer_GetMinVolume()) { - Cmd_Action(CMD_PLAYPAUSE); - } + } + if (gPlayProperties.pausePlay) { // Volume changes from 0 to 1 + if (oldVolume == AudioPlayer_GetMinVolume() && newVolume > AudioPlayer_GetMinVolume()) { + Cmd_Action(CMD_PLAYPAUSE); } - #endif + } +#endif } - // Receives de-serialized RFID-data (from NVS) and dispatches playlists for the given // playmode to the track-queue. void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _lastPlayPos, const uint32_t _playMode, const uint16_t _trackLastPlayed) { - // Make sure last playposition for audiobook is saved when new RFID-tag is applied - #ifdef SAVE_PLAYPOS_WHEN_RFID_CHANGE - if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS - vTaskDelay(portTICK_RATE_MS * 100u); - } +// Make sure last playposition for audiobook is saved when new RFID-tag is applied +#ifdef SAVE_PLAYPOS_WHEN_RFID_CHANGE + if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); + while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS + vTaskDelay(portTICK_PERIOD_MS * 100u); } - #endif - char *filename; - filename = (char *) x_malloc(sizeof(char) * 255); + } +#endif + char filename[255]; + + size_t sizeCpy = strnlen(_itemToPlay, sizeof(filename) - 1); // get the len of the play item (to a max of 254 chars) + memcpy(filename, _itemToPlay, sizeCpy); + filename[sizeCpy] = '\0'; // terminate the string - strncpy(filename, _itemToPlay, 255); gPlayProperties.startAtFilePos = _lastPlayPos; gPlayProperties.currentTrackNumber = _trackLastPlayed; char **musicFiles; if (_playMode != WEBSTREAM) { - if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY) { - filename = SdCard_pickRandomSubdirectory(filename); // *filename (input): target-directory // *filename (output): random subdirectory - if (filename == NULL) { // If error occured while extracting random subdirectory + if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY || _playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM) { + const char *tmp = SdCard_pickRandomSubdirectory(filename); // *filename (input): target-directory // *filename (output): random subdirectory + if (tmp == NULL) { // If error occured while extracting random subdirectory musicFiles = NULL; } else { - musicFiles = SdCard_ReturnPlaylist(filename, _playMode); // Provide random subdirectory in order to enter regular playlist-generation + musicFiles = SdCard_ReturnPlaylist(filename, _playMode); // Provide random subdirectory in order to enter regular playlist-generation } } else { musicFiles = SdCard_ReturnPlaylist(filename, _playMode); @@ -877,7 +958,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l // Catch if error occured (e.g. file not found) if (musicFiles == NULL) { - Log_Println((char *) FPSTR(errorOccured), LOGLEVEL_ERROR); + Log_Println(errorOccured, LOGLEVEL_ERROR); System_IndicateError(); if (gPlayProperties.playMode != NO_PLAYLIST) { AudioPlayer_TrackControlToQueueSender(STOP); @@ -887,17 +968,16 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.playMode = BUSY; // Show @Neopixel, if uC is busy with creating playlist if (!strcmp(*(musicFiles - 1), "0")) { - Log_Println((char *) FPSTR(noMp3FilesInDir), LOGLEVEL_NOTICE); + Log_Println(noMp3FilesInDir, LOGLEVEL_NOTICE); System_IndicateError(); if (!gPlayProperties.pausePlay) { AudioPlayer_TrackControlToQueueSender(STOP); while (!gPlayProperties.pausePlay) { - vTaskDelay(portTICK_RATE_MS * 10u); + vTaskDelay(portTICK_PERIOD_MS * 10u); } } gPlayProperties.playMode = NO_PLAYLIST; - free(filename); return; } @@ -911,14 +991,14 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.saveLastPlayPosition = false; gPlayProperties.playUntilTrackNumber = 0; - #ifdef PLAY_LAST_RFID_AFTER_REBOOT - // Store last RFID-tag to NVS - gPrefsSettings.putString("lastRfid", gCurrentRfidTagId); - #endif +#ifdef PLAY_LAST_RFID_AFTER_REBOOT + // Store last RFID-tag to NVS + gPrefsSettings.putString("lastRfid", gCurrentRfidTagId); +#endif switch (gPlayProperties.playMode) { case SINGLE_TRACK: { - Log_Println((char *) FPSTR(modeSingleTrack), LOGLEVEL_NOTICE); + Log_Println(modeSingleTrack, LOGLEVEL_NOTICE); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } @@ -926,7 +1006,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l case SINGLE_TRACK_LOOP: { gPlayProperties.repeatCurrentTrack = true; gPlayProperties.repeatPlaylist = true; - Log_Println((char *) FPSTR(modeSingleTrackLoop), LOGLEVEL_NOTICE); + Log_Println(modeSingleTrackLoop, LOGLEVEL_NOTICE); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } @@ -936,16 +1016,16 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.playUntilTrackNumber = 0; gPlayProperties.numberOfTracks = 1; // Limit number to 1 even there are more entries in the playlist Led_ResetToNightBrightness(); - Log_Println((char *) FPSTR(modeSingleTrackRandom), LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + Log_Println(modeSingleTrackRandom, LOGLEVEL_NOTICE); + AudioPlayer_RandomizePlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } case AUDIOBOOK: { // Tracks need to be alph. sorted! gPlayProperties.saveLastPlayPosition = true; - Log_Println((char *) FPSTR(modeSingleAudiobook), LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist((const char **)musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + Log_Println(modeSingleAudiobook, LOGLEVEL_NOTICE); + AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } @@ -953,50 +1033,50 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l case AUDIOBOOK_LOOP: { // Tracks need to be alph. sorted! gPlayProperties.repeatPlaylist = true; gPlayProperties.saveLastPlayPosition = true; - Log_Println((char *) FPSTR(modeSingleAudiobookLoop), LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist((const char **)musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + Log_Println(modeSingleAudiobookLoop, LOGLEVEL_NOTICE); + AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } case ALL_TRACKS_OF_DIR_SORTED: case RANDOM_SUBDIRECTORY_OF_DIRECTORY: { - snprintf(Log_Buffer, Log_BufferLength, "%s '%s' ", (char *) FPSTR(modeAllTrackAlphSorted), filename); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist((const char **)musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + Log_Printf(LOGLEVEL_NOTICE, modeAllTrackAlphSorted, filename); + AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } - case ALL_TRACKS_OF_DIR_RANDOM: { - Log_Println((char *) FPSTR(modeAllTrackRandom), LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + case ALL_TRACKS_OF_DIR_RANDOM: + case RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM: { + Log_Printf(LOGLEVEL_NOTICE, modeAllTrackRandom, filename); + AudioPlayer_RandomizePlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } case ALL_TRACKS_OF_DIR_SORTED_LOOP: { gPlayProperties.repeatPlaylist = true; - Log_Println((char *) FPSTR(modeAllTrackAlphSortedLoop), LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist((const char **)musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + Log_Println(modeAllTrackAlphSortedLoop, LOGLEVEL_NOTICE); + AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } case ALL_TRACKS_OF_DIR_RANDOM_LOOP: { gPlayProperties.repeatPlaylist = true; - Log_Println((char *) FPSTR(modeAllTrackRandomLoop), LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(musicFiles, strtoul(*(musicFiles - 1), NULL, 10)); + Log_Println(modeAllTrackRandomLoop, LOGLEVEL_NOTICE); + AudioPlayer_RandomizePlaylist(musicFiles, gPlayProperties.numberOfTracks); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } case WEBSTREAM: { // This is always just one "track" - Log_Println((char *) FPSTR(modeWebstream), LOGLEVEL_NOTICE); + Log_Println(modeWebstream, LOGLEVEL_NOTICE); if (Wlan_IsConnected()) { xQueueSend(gTrackQueue, &(musicFiles), 0); } else { - Log_Println((char *) FPSTR(webstreamNotAvailable), LOGLEVEL_ERROR); + Log_Println(webstreamNotAvailable, LOGLEVEL_ERROR); System_IndicateError(); gPlayProperties.playMode = NO_PLAYLIST; } @@ -1004,22 +1084,26 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } case LOCAL_M3U: { // Can be one or multiple SD-files or webradio-stations; or a mix of both - Log_Println((char *) FPSTR(modeWebstreamM3u), LOGLEVEL_NOTICE); + Log_Println(modeWebstreamM3u, LOGLEVEL_NOTICE); xQueueSend(gTrackQueue, &(musicFiles), 0); break; } default: - Log_Println((char *) FPSTR(modeDoesNotExist), LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, modeInvalid, gPlayProperties.playMode); gPlayProperties.playMode = NO_PLAYLIST; System_IndicateError(); } - free(filename); } /* Wraps putString for writing settings into NVS for RFID-cards. Returns number of characters written. */ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks) { + if (_playMode == NO_PLAYLIST) { + // writing back to NVS with NO_PLAYLIST seems to be a bug - Todo: Find the cause here + Log_Printf(LOGLEVEL_ERROR, modeInvalid, _playMode); + return 0; + } Led_SetPause(true); // Workaround to prevent exceptions due to Neopixel-signalisation while NVS-write char prefBuf[275]; char trackBuf[255]; @@ -1028,7 +1112,7 @@ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_tra // If it's a directory we just want to play/save basename(path) if (_numberOfTracks > 1) { const char s = '/'; - char *last = strrchr(_track, s); + const char *last = strrchr(_track, s); char *first = strchr(_track, s); unsigned long substr = last - first + 1; if (substr <= sizeof(trackBuf) / sizeof(trackBuf[0])) { @@ -1039,12 +1123,7 @@ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_tra } snprintf(prefBuf, sizeof(prefBuf) / sizeof(prefBuf[0]), "%s%s%s%u%s%d%s%u", stringDelimiter, trackBuf, stringDelimiter, _playPosition, stringDelimiter, _playMode, stringDelimiter, _trackLastPlayed); - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "Schreibe '%s' in NVS für RFID-Card-ID %s mit Abspielmodus %d und letzter Track %u\n", prefBuf, _rfidCardId, _playMode, _trackLastPlayed); - #else - snprintf(Log_Buffer, Log_BufferLength, "Write '%s' to NVS for RFID-Card-ID %s with playmode %d and last track %u\n", prefBuf, _rfidCardId, _playMode, _trackLastPlayed); - #endif - Log_Print(Log_Buffer, LOGLEVEL_INFO, true); + Log_Printf(LOGLEVEL_INFO, wroteLastTrackToNvs, prefBuf, _rfidCardId, _playMode, _trackLastPlayed); Log_Println(prefBuf, LOGLEVEL_INFO); Led_SetPause(false); return gPrefsRfid.putString(_rfidCardId, prefBuf); @@ -1061,21 +1140,15 @@ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_tra // Adds webstream to playlist; same like SdCard_ReturnPlaylist() but always only one entry char **AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { - char *webUrl = x_strdup(_webUrl); - static char **url; - - if (url != NULL) { - --url; - freeMultiCharArray(url, strtoul(*url, NULL, 10)); - } + static char number[] = "1"; + static char *url[2] = {number, nullptr}; - url = (char **)x_malloc(sizeof(char *) * 2); + free(url[1]); - url[0] = x_strdup("1"); // Number of files is always 1 in url-mode - url[1] = x_strdup(webUrl); + number[0] = '1'; + url[1] = x_strdup(_webUrl); - free(webUrl); - return ++url; + return &(url[1]); } // Adds new control-command to control-queue @@ -1084,7 +1157,7 @@ void AudioPlayer_TrackControlToQueueSender(const uint8_t trackCommand) { } // Knuth-Fisher-Yates-algorithm to randomize playlist -void AudioPlayer_SortPlaylist(char *str[], const uint32_t count) { +void AudioPlayer_RandomizePlaylist(char **str, const uint32_t count) { if (!count) { return; } @@ -1104,11 +1177,11 @@ void AudioPlayer_SortPlaylist(char *str[], const uint32_t count) { // Helper to sort playlist alphabetically static int AudioPlayer_ArrSortHelper(const void *a, const void *b) { - return strcmp(*(const char **)a, *(const char **)b); + return strcmp(*(const char **) a, *(const char **) b); } // Sort playlist alphabetically -void AudioPlayer_SortPlaylist(const char **arr, int n) { +void AudioPlayer_SortPlaylist(char **arr, int n) { qsort(arr, n, sizeof(const char *), AudioPlayer_ArrSortHelper); } @@ -1117,42 +1190,38 @@ void AudioPlayer_ClearCover(void) { gPlayProperties.coverFilePos = 0; // websocket and mqtt notify cover image has changed Web_SendWebsocketData(0, 40); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicCoverChangedState), "", false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicCoverChangedState, "", false); +#endif } // Some mp3-lib-stuff (slightly changed from default) void audio_info(const char *info) { - snprintf(Log_Buffer, Log_BufferLength, "info : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, "info : %s", info); } -void audio_id3data(const char *info) { //id3 metadata - snprintf(Log_Buffer, Log_BufferLength, "id3data : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +void audio_id3data(const char *info) { // id3 metadata + Log_Printf(LOGLEVEL_INFO, "id3data : %s", info); // get title - if (startsWith((char *)info, "Title:")) { + if (startsWith((char *) info, "Title:")) { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks, info + 6); + Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks, info + 6); } else { Audio_setTitle("%s", info + 6); } } } -void audio_eof_mp3(const char *info) { //end of file - snprintf(Log_Buffer, Log_BufferLength, "eof_mp3 : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +void audio_eof_mp3(const char *info) { // end of file + Log_Printf(LOGLEVEL_INFO, "eof_mp3 : %s", info); gPlayProperties.trackFinished = true; } void audio_showstation(const char *info) { - snprintf(Log_Buffer, Log_BufferLength, "station : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, "station : %s", info); if (strcmp(info, "")) { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks, info); + Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks, info); } else { Audio_setTitle("%s", info); } @@ -1160,11 +1229,10 @@ void audio_showstation(const char *info) { } void audio_showstreamtitle(const char *info) { - snprintf(Log_Buffer, Log_BufferLength, "streamtitle : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, "streamtitle : %s", info); if (strcmp(info, "")) { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks, info); + Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks, info); } else { Audio_setTitle("%s", info); } @@ -1172,43 +1240,38 @@ void audio_showstreamtitle(const char *info) { } void audio_bitrate(const char *info) { - snprintf(Log_Buffer, Log_BufferLength, "bitrate : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, "bitrate : %s", info); } -void audio_commercial(const char *info) { //duration in sec - snprintf(Log_Buffer, Log_BufferLength, "commercial : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +void audio_commercial(const char *info) { // duration in sec + Log_Printf(LOGLEVEL_INFO, "commercial : %s", info); } -void audio_icyurl(const char *info) { //homepage - snprintf(Log_Buffer, Log_BufferLength, "icyurl : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +void audio_icyurl(const char *info) { // homepage + Log_Printf(LOGLEVEL_INFO, "icyurl : %s", info); } -void audio_lasthost(const char *info) { //stream URL played - snprintf(Log_Buffer, Log_BufferLength, "lasthost : %s", info); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +void audio_lasthost(const char *info) { // stream URL played + Log_Printf(LOGLEVEL_INFO, "lasthost : %s", info); } // id3 tag: save cover image -void audio_id3image(File& file, const size_t pos, const size_t size) { +void audio_id3image(File &file, const size_t pos, const size_t size) { // save cover image position and size for later use gPlayProperties.coverFilePos = pos; gPlayProperties.coverFileSize = size; // websocket and mqtt notify cover image has changed Web_SendWebsocketData(0, 40); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicCoverChangedState), "", false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicCoverChangedState, "", false); +#endif } -void audio_eof_speech(const char *info){ +void audio_eof_speech(const char *info) { gPlayProperties.currentSpeechActive = false; } - // process audio sample extern (for bluetooth source) -void audio_process_i2s(uint32_t* sample, bool *continueI2S){ +void audio_process_i2s(uint32_t *sample, bool *continueI2S) { *continueI2S = !Bluetooth_Source_SendAudioData(sample); } diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index a61ba5b5..88bf2f71 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -1,38 +1,39 @@ #pragma once typedef struct { // Bit field - uint8_t playMode: 4; // playMode - char **playlist; // playlist - char *title; // current title - bool repeatCurrentTrack: 1; // If current track should be looped - bool repeatPlaylist: 1; // If whole playlist should be looped - uint16_t currentTrackNumber: 9; // Current tracknumber - uint16_t numberOfTracks: 9; // Number of tracks in playlist - unsigned long startAtFilePos; // Offset to start play (in bytes) - double currentRelPos; // Current relative playPosition (in %) - bool sleepAfterCurrentTrack: 1; // If uC should go to sleep after current track - bool sleepAfterPlaylist: 1; // If uC should go to sleep after whole playlist - bool sleepAfter5Tracks: 1; // If uC should go to sleep after 5 tracks - bool saveLastPlayPosition: 1; // If playposition/current track should be saved (for AUDIOBOOK) - char playRfidTag[13]; // ID of RFID-tag that started playlist - bool pausePlay: 1; // If pause is active - bool trackFinished: 1; // If current track is finished - bool playlistFinished: 1; // If whole playlist is finished - uint8_t playUntilTrackNumber: 6; // Number of tracks to play after which uC goes to sleep - uint8_t seekmode: 2; // If seekmode is active and if yes: forward or backwards? - bool newPlayMono: 1; // true if mono; false if stereo (helper) - bool currentPlayMono: 1; // true if mono; false if stereo - bool isWebstream: 1; // Indicates if track currenty played is a webstream - bool tellIpAddress: 1; // If true current IP-address is spoken - bool currentSpeechActive: 1; // If speech-play is active - bool lastSpeechActive: 1; // If speech-play was active - size_t coverFilePos; // current cover file position - size_t coverFileSize; // current cover file size + uint8_t playMode : 4; // playMode + char **playlist; // playlist + char title[255]; // current title + bool repeatCurrentTrack : 1; // If current track should be looped + bool repeatPlaylist : 1; // If whole playlist should be looped + uint16_t currentTrackNumber : 9; // Current tracknumber + uint16_t numberOfTracks : 9; // Number of tracks in playlist + unsigned long startAtFilePos; // Offset to start play (in bytes) + double currentRelPos; // Current relative playPosition (in %) + bool sleepAfterCurrentTrack : 1; // If uC should go to sleep after current track + bool sleepAfterPlaylist : 1; // If uC should go to sleep after whole playlist + bool sleepAfter5Tracks : 1; // If uC should go to sleep after 5 tracks + bool saveLastPlayPosition : 1; // If playposition/current track should be saved (for AUDIOBOOK) + char playRfidTag[13]; // ID of RFID-tag that started playlist + bool pausePlay : 1; // If pause is active + bool trackFinished : 1; // If current track is finished + bool playlistFinished : 1; // If whole playlist is finished + uint8_t playUntilTrackNumber : 6; // Number of tracks to play after which uC goes to sleep + uint8_t seekmode : 2; // If seekmode is active and if yes: forward or backwards? + bool newPlayMono : 1; // true if mono; false if stereo (helper) + bool currentPlayMono : 1; // true if mono; false if stereo + bool isWebstream : 1; // Indicates if track currenty played is a webstream + uint8_t tellMode : 2; // Tell mode for text to speech announcments + bool currentSpeechActive : 1; // If speech-play is active + bool lastSpeechActive : 1; // If speech-play was active + size_t coverFilePos; // current cover file position + size_t coverFileSize; // current cover file size } playProps; extern playProps gPlayProperties; void AudioPlayer_Init(void); +void AudioPlayer_Exit(void); void AudioPlayer_Cyclic(void); uint8_t AudioPlayer_GetRepeatMode(void); void AudioPlayer_VolumeToQueueSender(const int32_t _newVolume, bool reAdjustRotary); @@ -53,3 +54,7 @@ void AudioPlayer_SetInitVolume(uint8_t value); void AudioPlayer_SetupVolumeAndAmps(void); bool Audio_Detect_Mode_HP(bool _state); void Audio_setTitle(const char *format, ...); +time_t AudioPlayer_GetPlayTimeSinceStart(void); +time_t AudioPlayer_GetPlayTimeAllTime(void); +uint32_t AudioPlayer_GetCurrentTime(void); +uint32_t AudioPlayer_GetFileDuration(void); diff --git a/src/Battery.cpp b/src/Battery.cpp index 78b3ee33..1e1e4109 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -1,67 +1,68 @@ #include #include "settings.h" -#include "Log.h" + #include "Battery.h" -#include "Mqtt.h" + #include "Led.h" -#include "System.h" +#include "Log.h" +#include "Mqtt.h" #include "Power.h" #include "Rfid.h" +#include "System.h" #ifdef BATTERY_MEASURE_ENABLE - uint8_t batteryCheckInterval = s_batteryCheckInterval; - - void Battery_Init(void) { - uint32_t vInterval = gPrefsSettings.getUInt("vCheckIntv", 17777); - if (vInterval != 17777) { - batteryCheckInterval = vInterval; - snprintf(Log_Buffer, Log_BufferLength, "%s: %u Minuten", (char *)FPSTR(batteryCheckIntervalFromNVS), vInterval); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putUInt("vCheckIntv", batteryCheckInterval); - } - Battery_InitInner(); +uint8_t batteryCheckInterval = s_batteryCheckInterval; - #ifdef SHUTDOWN_ON_BAT_CRITICAL - if (Battery_IsCritical()) { - Battery_LogStatus(); +void Battery_Init(void) { + uint32_t vInterval = gPrefsSettings.getUInt("vCheckIntv", 17777); + if (vInterval != 17777) { + batteryCheckInterval = vInterval; + Log_Printf(LOGLEVEL_INFO, batteryCheckIntervalFromNVS, vInterval); + } else { + gPrefsSettings.putUInt("vCheckIntv", batteryCheckInterval); + } - Log_Println((char *)FPSTR(batteryCriticalMsg), LOGLEVEL_NOTICE); + Battery_InitInner(); - // Power down and enter deepsleep - Power_PeripheralOff(); - delay(200); - esp_deep_sleep_start(); - } - #endif + #ifdef SHUTDOWN_ON_BAT_CRITICAL + if (Battery_IsCritical()) { + Battery_LogStatus(); + Log_Println(batteryCriticalMsg, LOGLEVEL_NOTICE); + // Power down and enter deepsleep + delay(200); + esp_deep_sleep_start(); } + #endif +} - // Measures battery as per interval or after bootup (after allowing a few seconds to settle down) - void Battery_Cyclic(void) { - static uint32_t lastBatteryCheckTimestamp = 0; - if ((millis() - lastBatteryCheckTimestamp >= batteryCheckInterval * 60000) || (!lastBatteryCheckTimestamp && millis() >= 10000)) { - Battery_CyclicInner(); - Battery_PublishMQTT(); - Battery_LogStatus(); +// Measures battery as per interval or after bootup (after allowing a few seconds to settle down) +void Battery_Cyclic(void) { + static uint32_t lastBatteryCheckTimestamp = 0; + if ((millis() - lastBatteryCheckTimestamp >= batteryCheckInterval * 60000) || (!lastBatteryCheckTimestamp && millis() >= 10000)) { + Battery_CyclicInner(); + Battery_PublishMQTT(); + Battery_LogStatus(); - if (Battery_IsLow()) { - Log_Println((char *)FPSTR(batteryLowMsg), LOGLEVEL_ERROR); - Led_Indicate(LedIndicatorType::VoltageWarning); - } - - #ifdef SHUTDOWN_ON_BAT_CRITICAL - if (Battery_IsCritical()) { - Log_Println((char *)FPSTR(batteryCriticalMsg), LOGLEVEL_ERROR); - System_RequestSleep(); - } - #endif + if (Battery_IsLow()) { + Log_Println(batteryLowMsg, LOGLEVEL_ERROR); + Led_Indicate(LedIndicatorType::VoltageWarning); + } - lastBatteryCheckTimestamp = millis(); + #ifdef SHUTDOWN_ON_BAT_CRITICAL + if (Battery_IsCritical()) { + Log_Println(batteryCriticalMsg, LOGLEVEL_ERROR); + System_RequestSleep(); } + #endif + + lastBatteryCheckTimestamp = millis(); } +} #else // Battery Measure disabled, add dummy methods - void Battery_Cyclic(void) {} - void Battery_Init(void) {} +void Battery_Cyclic(void) { +} +void Battery_Init(void) { +} #endif diff --git a/src/Battery.h b/src/Battery.h index 9dac1c2e..78e13270 100644 --- a/src/Battery.h +++ b/src/Battery.h @@ -3,11 +3,11 @@ extern uint8_t batteryCheckInterval; #if defined(MEASURE_BATTERY_VOLTAGE) - extern float voltageIndicatorCritical; - extern float warningLowVoltage; - extern float warningCriticalVoltage; - extern float voltageIndicatorLow; - extern float voltageIndicatorHigh; +extern float voltageIndicatorCritical; +extern float warningLowVoltage; +extern float warningCriticalVoltage; +extern float voltageIndicatorLow; +extern float voltageIndicatorHigh; #endif void Battery_Init(void); @@ -21,7 +21,6 @@ bool Battery_IsCritical(void); void Battery_PublishMQTT(void); void Battery_LogStatus(void); - // Implementation specific tasks void Battery_CyclicInner(void); void Battery_InitInner(void); diff --git a/src/BatteryMax17055.cpp b/src/BatteryMax17055.cpp index e58be197..ab31e51b 100644 --- a/src/BatteryMax17055.cpp +++ b/src/BatteryMax17055.cpp @@ -1,7 +1,8 @@ #include -#include "Battery.h" #include "settings.h" +#include "Battery.h" + #ifdef MEASURE_BATTERY_MAX17055 #include "Log.h" #include "Mqtt.h" @@ -10,175 +11,146 @@ #include #include - float batteryLow = s_batteryLow; - float batteryCritical = s_batteryCritical; - uint16_t cycles = 0; - - MAX17055 sensor; - - extern TwoWire i2cBusTwo; - - void Battery_InitInner() { - bool por = false; - sensor.init(s_batteryCapacity, s_emptyVoltage, s_recoveryVoltage, s_batteryChemistry, s_vCharge, s_resistSensor, por, &i2cBusTwo, &delay); - cycles = gPrefsSettings.getUShort("MAX17055_cycles", 0x0000); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f", (char *)"Cycles saved in NVS:", cycles / 100.0); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - // if power was lost, restore model params - if (por) { - // TODO i18n necessary? - Log_Println("Battery detected power loss - loading fuel gauge parameters.", LOGLEVEL_NOTICE); - uint16_t rComp0 = gPrefsSettings.getUShort("rComp0", 0xFFFF); - uint16_t tempCo = gPrefsSettings.getUShort("tempCo", 0xFFFF); - uint16_t fullCapRep = gPrefsSettings.getUShort("fullCapRep", 0xFFFF); - uint16_t fullCapNom = gPrefsSettings.getUShort("fullCapNom", 0xFFFF); - - Log_Println("Loaded MAX17055 battery model parameters from NVS:", LOGLEVEL_DEBUG); - snprintf(Log_Buffer, Log_BufferLength, "%s: 0x%.4x", (char *)"rComp0", rComp0); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - snprintf(Log_Buffer, Log_BufferLength, "%s: 0x%.4x", (char *)"tempCo", tempCo); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - snprintf(Log_Buffer, Log_BufferLength, "%s: 0x%.4x", (char *)"fullCapRep", fullCapRep); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - snprintf(Log_Buffer, Log_BufferLength, "%s: 0x%.4x", (char *)"fullCapNom", fullCapNom); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - if ((rComp0 & tempCo & fullCapRep & fullCapNom) != 0xFFFF) { - Log_Println("Successfully loaded fuel gauge parameters.", LOGLEVEL_NOTICE); - sensor.restoreLearnedParameters(rComp0, tempCo, fullCapRep, cycles, fullCapNom); - } else { - Log_Println("Failed loading fuel gauge parameters.", LOGLEVEL_NOTICE); - } - } else { - Log_Println("Battery continuing normal operation", LOGLEVEL_DEBUG); - } - - Log_Println("MAX17055 init done. Battery configured with the following settings:", LOGLEVEL_DEBUG); - float val = sensor.getDesignCapacity(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f mAh", (char *)"Design Capacity", val); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - val = sensor.getReportedCapacity(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f mAh", (char *)"Reported Capacity", val); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - val = sensor.getEmptyVoltage() / 100.0; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)"Empty Voltage", val); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - uint16_t modelCfg = sensor.getModelCfg(); - snprintf(Log_Buffer, Log_BufferLength, "%s: 0x%.4x", (char *)"ModelCfg Value", modelCfg); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - uint16_t cycles = sensor.getCycles(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f", (char *)"Cycles", cycles / 100.0); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - float vBatteryLow = gPrefsSettings.getFloat("batteryLow", 999.99); - if (vBatteryLow <= 999) { - batteryLow = vBatteryLow; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f %%", (char *)FPSTR(batteryLowFromNVS), batteryLow); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putFloat("batteryLow", batteryLow); - } - - float vBatteryCritical = gPrefsSettings.getFloat("batteryCritical", 999.99); - if (vBatteryCritical <= 999) { - batteryCritical = vBatteryCritical; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f %%", (char *)FPSTR(batteryCriticalFromNVS), batteryCritical); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +float batteryLow = s_batteryLow; +float batteryCritical = s_batteryCritical; +uint16_t cycles = 0; + +MAX17055 sensor; + +extern TwoWire i2cBusTwo; + +void Battery_InitInner() { + bool por = false; + sensor.init(s_batteryCapacity, s_emptyVoltage, s_recoveryVoltage, s_batteryChemistry, s_vCharge, s_resistSensor, por, &i2cBusTwo, &delay); + cycles = gPrefsSettings.getUShort("MAX17055_cycles", 0x0000); + Log_Printf(LOGLEVEL_DEBUG, "Cycles saved in NVS: %.2f", cycles / 100.0); + + // if power was lost, restore model params + if (por) { + // TODO i18n necessary? + Log_Println("Battery detected power loss - loading fuel gauge parameters.", LOGLEVEL_NOTICE); + uint16_t rComp0 = gPrefsSettings.getUShort("rComp0", 0xFFFF); + uint16_t tempCo = gPrefsSettings.getUShort("tempCo", 0xFFFF); + uint16_t fullCapRep = gPrefsSettings.getUShort("fullCapRep", 0xFFFF); + uint16_t fullCapNom = gPrefsSettings.getUShort("fullCapNom", 0xFFFF); + + Log_Println("Loaded MAX17055 battery model parameters from NVS:", LOGLEVEL_DEBUG); + Log_Printf(LOGLEVEL_DEBUG, "rComp0: 0x%.4x", rComp0); + Log_Printf(LOGLEVEL_DEBUG, "tempCo: 0x%.4x", tempCo); + Log_Printf(LOGLEVEL_DEBUG, "fullCapRep: 0x%.4x", fullCapRep); + Log_Printf(LOGLEVEL_DEBUG, "fullCapNom: 0x%.4x", fullCapNom); + + if ((rComp0 & tempCo & fullCapRep & fullCapNom) != 0xFFFF) { + Log_Println("Successfully loaded fuel gauge parameters.", LOGLEVEL_NOTICE); + sensor.restoreLearnedParameters(rComp0, tempCo, fullCapRep, cycles, fullCapNom); } else { - gPrefsSettings.putFloat("batteryCritical", batteryCritical); + Log_Println("Failed loading fuel gauge parameters.", LOGLEVEL_NOTICE); } + } else { + Log_Println("Battery continuing normal operation", LOGLEVEL_DEBUG); } - void Battery_CyclicInner() { - // It is recommended to save the learned capacity parameters every time bit 6 of the Cycles register toggles - uint16_t sensorCycles = sensor.getCycles(); - // sensorCycles = 0xFFFF likely means read error - if (sensor.getPresent() && sensorCycles != 0xFFFF && uint16_t(cycles + 0x0040) <= sensorCycles) { - Log_Println("Battery Cycle passed 64%, store MAX17055 learned parameters", LOGLEVEL_DEBUG); - uint16_t rComp0; - uint16_t tempCo; - uint16_t fullCapRep; - uint16_t fullCapNom; - sensor.getLearnedParameters(rComp0, tempCo, fullCapRep, sensorCycles, fullCapNom); - gPrefsSettings.putUShort("rComp0", rComp0); - gPrefsSettings.putUShort("tempCo", tempCo); - gPrefsSettings.putUShort("fullCapRep", fullCapRep); - gPrefsSettings.putUShort("MAX17055_cycles", sensorCycles); - gPrefsSettings.putUShort("fullCapNom", fullCapNom); - cycles = sensorCycles; - } + Log_Println("MAX17055 init done. Battery configured with the following settings:", LOGLEVEL_DEBUG); + float val = sensor.getDesignCapacity(); + Log_Printf(LOGLEVEL_DEBUG, "Design Capacity: %.2f mAh", val); + val = sensor.getReportedCapacity(); + Log_Printf(LOGLEVEL_DEBUG, "Reported Capacity: %.2f mAh", val); + val = sensor.getEmptyVoltage() / 100.0; + Log_Printf(LOGLEVEL_DEBUG, "Empty Voltage: %.2f V", val); + uint16_t modelCfg = sensor.getModelCfg(); + Log_Printf(LOGLEVEL_DEBUG, "ModelCfg Value: 0x%.4x", modelCfg); + uint16_t cycles = sensor.getCycles(); + Log_Printf(LOGLEVEL_DEBUG, "Cycles: %.2f", cycles / 100.0); + + float vBatteryLow = gPrefsSettings.getFloat("batteryLow", 999.99); + if (vBatteryLow <= 999) { + batteryLow = vBatteryLow; + Log_Printf(LOGLEVEL_INFO, batteryLowFromNVS, batteryLow); + } else { + gPrefsSettings.putFloat("batteryLow", batteryLow); } - float Battery_GetVoltage(void) { - return sensor.getInstantaneousVoltage(); + float vBatteryCritical = gPrefsSettings.getFloat("batteryCritical", 999.99); + if (vBatteryCritical <= 999) { + batteryCritical = vBatteryCritical; + Log_Printf(LOGLEVEL_INFO, batteryCriticalFromNVS, batteryCritical); + } else { + gPrefsSettings.putFloat("batteryCritical", batteryCritical); } - - void Battery_PublishMQTT() { - #ifdef MQTT_ENABLE - float voltage = Battery_GetVoltage(); - char vstr[6]; - snprintf(vstr, 6, "%.2f", voltage); - publishMqtt((char *)FPSTR(topicBatteryVoltage), vstr, false); - - float soc = Battery_EstimateLevel() * 100; - snprintf(vstr, 6, "%.2f", soc); - publishMqtt((char *)FPSTR(topicBatterySOC), vstr, false); - #endif +} + +void Battery_CyclicInner() { + // It is recommended to save the learned capacity parameters every time bit 6 of the Cycles register toggles + uint16_t sensorCycles = sensor.getCycles(); + // sensorCycles = 0xFFFF likely means read error + if (sensor.getPresent() && sensorCycles != 0xFFFF && uint16_t(cycles + 0x0040) <= sensorCycles) { + Log_Println("Battery Cycle passed 64%, store MAX17055 learned parameters", LOGLEVEL_DEBUG); + uint16_t rComp0; + uint16_t tempCo; + uint16_t fullCapRep; + uint16_t fullCapNom; + sensor.getLearnedParameters(rComp0, tempCo, fullCapRep, sensorCycles, fullCapNom); + gPrefsSettings.putUShort("rComp0", rComp0); + gPrefsSettings.putUShort("tempCo", tempCo); + gPrefsSettings.putUShort("fullCapRep", fullCapRep); + gPrefsSettings.putUShort("MAX17055_cycles", sensorCycles); + gPrefsSettings.putUShort("fullCapNom", fullCapNom); + cycles = sensorCycles; } - - void Battery_LogStatus(void) { - float voltage = Battery_GetVoltage(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)FPSTR(currentVoltageMsg), voltage); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - - float soc = Battery_EstimateLevel() * 100; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f %%", (char *)FPSTR(currentChargeMsg), soc); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - - float avgCurr = sensor.getAverageCurrent(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f mA", (char *)FPSTR(batteryCurrentMsg), avgCurr); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - - float temperature = sensor.getTemperature(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f °C", (char *)FPSTR(batteryTempMsg), temperature); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - - // pretty useless because of low resolution - // float maxCurrent = sensor.getMaxCurrent(); - // snprintf(Log_Buffer, Log_BufferLength, "%s: %.4f mA", "Max current to battery since last check", maxCurrent); - // Log_Println(Log_Buffer, LOGLEVEL_INFO); - // float minCurrent = sensor.getMinCurrent(); - // snprintf(Log_Buffer, Log_BufferLength, "%s: %.4f mA", "Min current to battery since last check", minCurrent); - // Log_Println(Log_Buffer, LOGLEVEL_INFO); - // sensor.resetMaxMinCurrent(); - - float cycles = sensor.getCycles() / 100.0; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f", (char *)FPSTR(batteryCyclesMsg), cycles); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +} + +float Battery_GetVoltage(void) { + return sensor.getInstantaneousVoltage(); +} + +void Battery_PublishMQTT() { + #ifdef MQTT_ENABLE + float voltage = Battery_GetVoltage(); + char vstr[6]; + snprintf(vstr, 6, "%.2f", voltage); + publishMqtt(topicBatteryVoltage, vstr, false); + + float soc = Battery_EstimateLevel() * 100; + snprintf(vstr, 6, "%.2f", soc); + publishMqtt(topicBatterySOC, vstr, false); + #endif +} + +void Battery_LogStatus(void) { + Log_Printf(LOGLEVEL_INFO, currentVoltageMsg, Battery_GetVoltage()); + Log_Printf(LOGLEVEL_INFO, currentChargeMsg, Battery_EstimateLevel() * 100); + Log_Printf(LOGLEVEL_INFO, batteryCurrentMsg, sensor.getAverageCurrent()); + Log_Printf(LOGLEVEL_INFO, batteryTempMsg, sensor.getTemperature()); + + // pretty useless because of low resolution + // Log_Printf(LOGLEVEL_INFO, "Max current to battery since last check: %.4f mA", sensor.getMaxCurrent()); + // Log_Printf(LOGLEVEL_INFO, "Min current to battery since last check: %.4f mA", sensor.getMinCurrent()); + // sensor.resetMaxMinCurrent(); + + Log_Printf(LOGLEVEL_INFO, batteryCyclesMsg, sensor.getCycles() / 100.0); +} + +float Battery_EstimateLevel(void) { + return sensor.getSOC() / 100; +} + +bool Battery_IsLow(void) { + float soc = sensor.getSOC(); + if (soc > 100.0) { + Log_Println("Battery percentage reading invalid, try again.", LOGLEVEL_DEBUG); + soc = sensor.getSOC(); } - float Battery_EstimateLevel(void) { - return sensor.getSOC() / 100; - } + return soc < batteryLow; +} - bool Battery_IsLow(void) { - float soc = sensor.getSOC(); - if (soc > 100.0) { - Log_Println("Battery percentage reading invalid, try again.", LOGLEVEL_DEBUG); - soc = sensor.getSOC(); - } - - return soc < batteryLow; +bool Battery_IsCritical(void) { + float soc = sensor.getSOC(); + if (soc > 100.0) { + Log_Println("Battery percentage reading invalid, try again.", LOGLEVEL_DEBUG); + soc = sensor.getSOC(); } - bool Battery_IsCritical(void) { - float soc = sensor.getSOC(); - if (soc > 100.0) { - Log_Println("Battery percentage reading invalid, try again.", LOGLEVEL_DEBUG); - soc = sensor.getSOC(); - } - - return soc < batteryCritical; - } + return soc < batteryCritical; +} #endif diff --git a/src/BatteryMeasureVoltage.cpp b/src/BatteryMeasureVoltage.cpp index 66cb4955..e14e867e 100644 --- a/src/BatteryMeasureVoltage.cpp +++ b/src/BatteryMeasureVoltage.cpp @@ -1,134 +1,137 @@ #include #include "settings.h" -#include "Log.h" + #include "Battery.h" -#include "Mqtt.h" #include "Led.h" +#include "Log.h" +#include "Mqtt.h" #include "System.h" // Only enable measurements if valid GPIO is used #if defined(MEASURE_BATTERY_VOLTAGE) && (VOLTAGE_READ_PIN >= 0 && VOLTAGE_READ_PIN <= 39) - constexpr uint16_t maxAnalogValue = 4095u; // Highest value given by analogRead(); don't change! - - float warningLowVoltage = s_warningLowVoltage; - float warningCriticalVoltage = s_warningCriticalVoltage; - float voltageIndicatorLow = s_voltageIndicatorLow; - float voltageIndicatorHigh = s_voltageIndicatorHigh; - - void Battery_InitInner() { - // Get voltages from NVS for Neopixel - float vLowIndicator = gPrefsSettings.getFloat("vIndicatorLow", 999.99); - if (vLowIndicator <= 999) { - voltageIndicatorLow = vLowIndicator; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)FPSTR(voltageIndicatorLowFromNVS), vLowIndicator); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { // preseed if not set - gPrefsSettings.putFloat("vIndicatorLow", voltageIndicatorLow); - } - - float vHighIndicator = gPrefsSettings.getFloat("vIndicatorHigh", 999.99); - if (vHighIndicator <= 999) { - voltageIndicatorHigh = vHighIndicator; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)FPSTR(voltageIndicatorHighFromNVS), vHighIndicator); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putFloat("vIndicatorHigh", voltageIndicatorHigh); - } - - float vLowWarning = gPrefsSettings.getFloat("wLowVoltage", 999.99); - if (vLowWarning <= 999) { - warningLowVoltage = vLowWarning; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)FPSTR(warningLowVoltageFromNVS), vLowWarning); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putFloat("wLowVoltage", warningLowVoltage); - } - - float vCriticalWarning = gPrefsSettings.getFloat("wCritVoltage", 999.99); - if (vCriticalWarning <= 999) { - warningCriticalVoltage = vCriticalWarning; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)FPSTR(warningCriticalVoltageFromNVS), vCriticalWarning); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putFloat("wCritVoltage", warningCriticalVoltage); - } +constexpr uint16_t maxAnalogValue = 4095u; // Highest value given by analogRead(); don't change! + +float warningLowVoltage = s_warningLowVoltage; +float warningCriticalVoltage = s_warningCriticalVoltage; +float voltageIndicatorLow = s_voltageIndicatorLow; +float voltageIndicatorHigh = s_voltageIndicatorHigh; + +void Battery_InitInner() { + // Get voltages from NVS for Neopixel + float vLowIndicator = gPrefsSettings.getFloat("vIndicatorLow", 999.99); + if (vLowIndicator <= 999) { + voltageIndicatorLow = vLowIndicator; + Log_Printf(LOGLEVEL_INFO, voltageIndicatorLowFromNVS, vLowIndicator); + } else { // preseed if not set + gPrefsSettings.putFloat("vIndicatorLow", voltageIndicatorLow); } - - void Battery_CyclicInner() { - // no special cyclic task necessary for voltage measure + float vHighIndicator = gPrefsSettings.getFloat("vIndicatorHigh", 999.99); + if (vHighIndicator <= 999) { + voltageIndicatorHigh = vHighIndicator; + Log_Printf(LOGLEVEL_INFO, voltageIndicatorHighFromNVS, vHighIndicator); + } else { + gPrefsSettings.putFloat("vIndicatorHigh", voltageIndicatorHigh); } - // The average of several analog reads will be taken to reduce the noise (Note: One analog read takes ~10µs) - float Battery_GetVoltage(void) { - float factor = 1 / ((float) rdiv2 / (rdiv2 + rdiv1)); - float averagedAnalogValue = 0; - uint8_t i; - for (i = 0; i <= 19; i++) { - averagedAnalogValue += (float)analogRead(VOLTAGE_READ_PIN); - } - averagedAnalogValue /= 20.0; - return (averagedAnalogValue / maxAnalogValue) * referenceVoltage * factor + offsetVoltage; + float vLowWarning = gPrefsSettings.getFloat("wLowVoltage", 999.99); + if (vLowWarning <= 999) { + warningLowVoltage = vLowWarning; + Log_Printf(LOGLEVEL_INFO, warningLowVoltageFromNVS, vLowWarning); + } else { + gPrefsSettings.putFloat("wLowVoltage", warningLowVoltage); } - void Battery_PublishMQTT(){ - #ifdef MQTT_ENABLE - float voltage = Battery_GetVoltage(); - char vstr[6]; - snprintf(vstr, 6, "%.2f", voltage); - publishMqtt((char *)FPSTR(topicBatteryVoltage), vstr, false); - - float soc = Battery_EstimateLevel() * 100; - snprintf(vstr, 6, "%.2f", soc); - publishMqtt((char *)FPSTR(topicBatterySOC), vstr, false); - #endif + float vCriticalWarning = gPrefsSettings.getFloat("wCritVoltage", 999.99); + if (vCriticalWarning <= 999) { + warningCriticalVoltage = vCriticalWarning; + Log_Printf(LOGLEVEL_INFO, warningCriticalVoltageFromNVS, vCriticalWarning); + } else { + gPrefsSettings.putFloat("wCritVoltage", warningCriticalVoltage); } - - void Battery_LogStatus(void){ - float voltage = Battery_GetVoltage(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f V", (char *)FPSTR(currentVoltageMsg), voltage); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - - float soc = Battery_EstimateLevel() * 100; - snprintf(Log_Buffer, Log_BufferLength, "%s: %.2f %%", (char *)FPSTR(currentChargeMsg), soc); - Log_Println(Log_Buffer, LOGLEVEL_INFO); +} + +void Battery_CyclicInner() { + // no special cyclic task necessary for voltage measure +} + +// The average of several analog reads will be taken to reduce the noise (Note: One analog read takes ~10µs) +float Battery_GetVoltage(void) { + float factor = 1 / ((float) rdiv2 / (rdiv2 + rdiv1)); + float averagedAnalogValue = 0; + uint8_t i; + for (i = 0; i <= 19; i++) { + averagedAnalogValue += (float) analogRead(VOLTAGE_READ_PIN); } + averagedAnalogValue /= 20.0; + return (averagedAnalogValue / maxAnalogValue) * referenceVoltage * factor + offsetVoltage; +} - float Battery_EstimateLevel(void) { - float currentVoltage = Battery_GetVoltage(); - float vDiffIndicatorRange = voltageIndicatorHigh - voltageIndicatorLow; - float vDiffCurrent = currentVoltage - voltageIndicatorLow; - float estimatedLevel = vDiffCurrent / vDiffIndicatorRange; - if (estimatedLevel < 0) { // Don't return value < 0.0 - return 0.0F; - } - return (estimatedLevel > 1) ? 1.0F : estimatedLevel; // Don't return value > 1.0 +void Battery_PublishMQTT() { + #ifdef MQTT_ENABLE + float voltage = Battery_GetVoltage(); + char vstr[6]; + snprintf(vstr, 6, "%.2f", voltage); + publishMqtt(topicBatteryVoltage, vstr, false); + + float soc = Battery_EstimateLevel() * 100; + snprintf(vstr, 6, "%.2f", soc); + publishMqtt(topicBatterySOC, vstr, false); + #endif +} + +void Battery_LogStatus(void) { + Log_Printf(LOGLEVEL_INFO, currentVoltageMsg, Battery_GetVoltage()); + Log_Printf(LOGLEVEL_INFO, currentChargeMsg, Battery_EstimateLevel() * 100); +} + +float Battery_EstimateLevel(void) { + float currentVoltage = Battery_GetVoltage(); + float vDiffIndicatorRange = voltageIndicatorHigh - voltageIndicatorLow; + float vDiffCurrent = currentVoltage - voltageIndicatorLow; + float estimatedLevel = vDiffCurrent / vDiffIndicatorRange; + if (estimatedLevel < 0) { // Don't return value < 0.0 + return 0.0F; } + return (estimatedLevel > 1) ? 1.0F : estimatedLevel; // Don't return value > 1.0 +} - bool Battery_IsLow(void) { - return Battery_GetVoltage() < warningLowVoltage; - } +bool Battery_IsLow(void) { + return Battery_GetVoltage() < warningLowVoltage; +} - bool Battery_IsCritical(void) { - return Battery_GetVoltage() < warningCriticalVoltage; - } +bool Battery_IsCritical(void) { + return Battery_GetVoltage() < warningCriticalVoltage; +} #else #ifdef MEASURE_BATTERY_VOLTAGE - // add some dummy impls to make CI happy - - float warningLowVoltage = 0.4f; - float warningCriticalVoltage = 0.1f; - float voltageIndicatorLow = 3.0f; - float voltageIndicatorHigh = 4.2f; - - void Battery_InitInner(void){} - void Battery_CyclicInner(void){} - float Battery_GetVoltage(void){return 4.2;} - void Battery_PublishMQTT(void){} - void Battery_LogStatus(void){} - float Battery_EstimateLevel(void) {return 42.0;} - bool Battery_IsLow(void) {return false;} - bool Battery_IsCritical(void) {return false;} +// add some dummy impls to make CI happy + +float warningLowVoltage = 0.4f; +float warningCriticalVoltage = 0.1f; +float voltageIndicatorLow = 3.0f; +float voltageIndicatorHigh = 4.2f; + +void Battery_InitInner(void) { +} +void Battery_CyclicInner(void) { +} +float Battery_GetVoltage(void) { + return 4.2; +} +void Battery_PublishMQTT(void) { +} +void Battery_LogStatus(void) { +} +float Battery_EstimateLevel(void) { + return 42.0; +} +bool Battery_IsLow(void) { + return false; +} +bool Battery_IsCritical(void) { + return false; +} #endif #endif diff --git a/src/Bluetooth.cpp b/src/Bluetooth.cpp index 7995e0d6..f45bdba2 100644 --- a/src/Bluetooth.cpp +++ b/src/Bluetooth.cpp @@ -1,11 +1,15 @@ #include -#include #include "settings.h" + #include "Bluetooth.h" + +#include "Common.h" #include "Log.h" #include "RotaryEncoder.h" #include "System.h" +#include + #ifdef BLUETOOTH_ENABLE #include "esp_bt.h" #include "BluetoothA2DPSink.h" @@ -16,283 +20,313 @@ #define BLUETOOTHPLAYER_VOLUME_MAX 21u #define BLUETOOTHPLAYER_VOLUME_MIN 0u - BluetoothA2DPSink *a2dp_sink; - BluetoothA2DPSource *a2dp_source; - RingbufHandle_t audioSourceRingBuffer; +BluetoothA2DPSink *a2dp_sink; +BluetoothA2DPSource *a2dp_source; +RingbufHandle_t audioSourceRingBuffer; +String btDeviceName; #endif - #ifdef BLUETOOTH_ENABLE - // for esp_a2d_connection_state_t see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_a2dp.html#_CPPv426esp_a2d_connection_state_t - void connection_state_changed(esp_a2d_connection_state_t state, void *ptr){ - if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth sink => connection state: %s", a2dp_sink->to_str(state)); - } else { - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth source => connection state: %s", a2dp_source->to_str(state)); - } - Log_Println(Log_Buffer, LOGLEVEL_INFO); +const char *getType() { + if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { + return "sink"; + } else { + return "source"; } +} #endif +#ifdef BLUETOOTH_ENABLE +// for esp_a2d_connection_state_t see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_a2dp.html#_CPPv426esp_a2d_connection_state_t +void connection_state_changed(esp_a2d_connection_state_t state, void *ptr) { + Log_Printf(LOGLEVEL_INFO, "Bluetooth %s => connection state: %s", getType(), ((BluetoothA2DPCommon *) ptr)->to_str(state)); + if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { + // for Neopixel (indicator LEDs) use the webstream mode + gPlayProperties.isWebstream = false; + gPlayProperties.pausePlay = false; + gPlayProperties.playlistFinished = true; + } +} +#endif #ifdef BLUETOOTH_ENABLE // for esp_a2d_audio_state_t see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_a2dp.html#_CPPv421esp_a2d_audio_state_t void audio_state_changed(esp_a2d_audio_state_t state, void *ptr) { + Log_Printf(LOGLEVEL_INFO, "Bluetooth %s => audio state: %s", getType(), ((BluetoothA2DPCommon *) ptr)->to_str(state)); if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth sink => audio state: %s", a2dp_sink->to_str(state)); - } else { - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth source => audio state: %s", a2dp_source->to_str(state)); - } - Log_Println(Log_Buffer, LOGLEVEL_INFO); + // set play/pause status + gPlayProperties.pausePlay = (state != ESP_A2D_AUDIO_STATE_STARTED); + // for Neopixel (indicator LEDs) use the webstream mode + gPlayProperties.playlistFinished = false; + gPlayProperties.isWebstream = true; + } } #endif #ifdef BLUETOOTH_ENABLE - // handle Bluetooth AVRC metadata - // https://docs.espressif.com/projects/esp-idf/en/release-v3.2/api-reference/bluetooth/esp_avrc.html - void avrc_metadata_callback(uint8_t id, const uint8_t *text) { - if (strlen((char *)text) == 0) - return; - switch (id) { - case ESP_AVRC_MD_ATTR_TITLE: - // title - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC Title: %s", text); - break; - case ESP_AVRC_MD_ATTR_ARTIST: - // artists - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC Artist: %s", text); - break; - case ESP_AVRC_MD_ATTR_ALBUM: - // album - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC Album: %s", text); - break; - case ESP_AVRC_MD_ATTR_TRACK_NUM: - // current track number - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC Track-No: %s", text); - break; - case ESP_AVRC_MD_ATTR_NUM_TRACKS: - // number of tracks in playlist - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC Number of tracks: %s", text); - break; - case ESP_AVRC_MD_ATTR_GENRE: - // genre - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC Genre: %s", text); - break; - default: - // unknown/unsupported metadata - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => AVRC metadata rsp: attribute id 0x%x, %s", id, text); - break; - } - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); +// handle Bluetooth AVRC metadata +// https://docs.espressif.com/projects/esp-idf/en/release-v3.2/api-reference/bluetooth/esp_avrc.html +void avrc_metadata_callback(uint8_t id, const uint8_t *text) { + if (strlen((char *) text) == 0) { + return; } + switch (id) { + case ESP_AVRC_MD_ATTR_TITLE: + // title + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC Title: %s", text); + break; + case ESP_AVRC_MD_ATTR_ARTIST: + // artists + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC Artist: %s", text); + break; + case ESP_AVRC_MD_ATTR_ALBUM: + // album + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC Album: %s", text); + break; + case ESP_AVRC_MD_ATTR_TRACK_NUM: + // current track number + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC Track-No: %s", text); + break; + case ESP_AVRC_MD_ATTR_NUM_TRACKS: + // number of tracks in playlist + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC Number of tracks: %s", text); + break; + case ESP_AVRC_MD_ATTR_GENRE: + // genre + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC Genre: %s", text); + break; + default: + // unknown/unsupported metadata + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => AVRC metadata rsp: attribute id 0x%x, %s", id, text); + break; + } +} #endif #ifdef BLUETOOTH_ENABLE - // feed the A2DP source with audio data - int32_t get_data_channels(Frame *frame, int32_t channel_len) { - if (channel_len < 0 || frame == NULL) - return 0; - // Receive data from ring buffer - size_t len{}; - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - vRingbufferGetInfo(audioSourceRingBuffer, nullptr, nullptr, nullptr, nullptr, &len); - #else - vRingbufferGetInfo(audioSourceRingBuffer, nullptr, nullptr, nullptr, &len); - #endif - if (len < (channel_len * 4)) { - // Serial.println("Bluetooth source => not enough data"); - return 0; - }; - size_t sampleSize = 0; - uint8_t* sampleBuff; - sampleBuff = (uint8_t *)xRingbufferReceiveUpTo(audioSourceRingBuffer, &sampleSize, (portTickType)portMAX_DELAY, channel_len * 4); - if (sampleBuff != NULL) { - // fill the channel data - for (int sample = 0; sample < (channel_len); ++sample) { - frame[sample].channel1 = (sampleBuff[sample * 4 + 3] << 8) | sampleBuff[sample * 4 + 2]; - frame[sample].channel2 = (sampleBuff[sample * 4 + 1] << 8) | sampleBuff[sample * 4]; - }; - vRingbufferReturnItem(audioSourceRingBuffer, (void *)sampleBuff); +// feed the A2DP source with audio data +int32_t get_data_channels(Frame *frame, int32_t channel_len) { + if (channel_len < 0 || frame == NULL) { + return 0; + } + // Receive data from ring buffer + size_t len {}; + vRingbufferGetInfo(audioSourceRingBuffer, nullptr, nullptr, nullptr, nullptr, &len); + if (len < (channel_len * 4)) { + // Serial.println("Bluetooth source => not enough data"); + return 0; + }; + size_t sampleSize = 0; + uint8_t *sampleBuff; + sampleBuff = (uint8_t *) xRingbufferReceiveUpTo(audioSourceRingBuffer, &sampleSize, (TickType_t) portMAX_DELAY, channel_len * 4); + if (sampleBuff != NULL) { + // fill the channel data + for (int sample = 0; sample < (channel_len); ++sample) { + frame[sample].channel1 = (sampleBuff[sample * 4 + 3] << 8) | sampleBuff[sample * 4 + 2]; + frame[sample].channel2 = (sampleBuff[sample * 4 + 1] << 8) | sampleBuff[sample * 4]; }; - return channel_len; + vRingbufferReturnItem(audioSourceRingBuffer, (void *) sampleBuff); }; + return channel_len; +}; #endif #ifdef BLUETOOTH_ENABLE - // callback which is notified on update Receiver RSSI - void rssi(esp_bt_gap_cb_param_t::read_rssi_delta_param &rssiParam){ - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth => RSSI value: %d", rssiParam.rssi_delta); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - } +// callback which is notified on update Receiver RSSI +void rssi(esp_bt_gap_cb_param_t::read_rssi_delta_param &rssiParam) { + Log_Printf(LOGLEVEL_DEBUG, "Bluetooth => RSSI value: %d", rssiParam.rssi_delta); +} #endif +#ifdef BLUETOOTH_ENABLE +// Callback notifying BT-source devices available. +// Return true to connect, false will continue scanning +bool scan_bluetooth_device_callback(const char *ssid, esp_bd_addr_t address, int rssi) { + Log_Printf(LOGLEVEL_INFO, "Bluetooth source => Device found: %s", ssid); + + if (btDeviceName == "") { + // no device name given, connect to first device found + return true; + } else { + // connect if device name (partially) matching, todo: compare case insensitive here? + return startsWith(ssid, btDeviceName.c_str()); + } +} +#endif void Bluetooth_VolumeChanged(int _newVolume) { - #ifdef BLUETOOTH_ENABLE - snprintf(Log_Buffer, Log_BufferLength, "%s %d !", (char *) FPSTR("Bluetooth => volume changed: "), _newVolume); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - uint8_t _volume; - if (_newVolume < BLUETOOTHPLAYER_VOLUME_MIN) { - return; - } else if (_newVolume > BLUETOOTHPLAYER_VOLUME_MAX) { - return; - } else { - // map bluetooth volume (0..127) to ESPuino volume (0..21) to - _volume = map(_newVolume, 0, 0x7F, BLUETOOTHPLAYER_VOLUME_MIN, BLUETOOTHPLAYER_VOLUME_MAX); - AudioPlayer_SetCurrentVolume(_volume); - // update rotary encoder - RotaryEncoder_Readjust(); - } - #endif +#ifdef BLUETOOTH_ENABLE + if ((_newVolume < 0) || (_newVolume > 0x7F)) { + return; + } + // map bluetooth volume (0..127) to ESPuino volume (0..21) to + uint8_t _volume; + _volume = map(_newVolume, 0, 0x7F, BLUETOOTHPLAYER_VOLUME_MIN, BLUETOOTHPLAYER_VOLUME_MAX); + if (AudioPlayer_GetCurrentVolume() != _volume) { + Log_Printf(LOGLEVEL_INFO, "Bluetooth => volume changed: %d !", _volume); + AudioPlayer_VolumeToQueueSender(_volume, true); + } +#endif } void Bluetooth_Init(void) { - #ifdef BLUETOOTH_ENABLE - if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { - // bluetooth in sink mode (player acts as a BT-Speaker) - a2dp_sink = new BluetoothA2DPSink(); - i2s_pin_config_t pin_config = { - .bck_io_num = I2S_BCLK, - .ws_io_num = I2S_LRC, - .data_out_num = I2S_DOUT, - .data_in_num = I2S_PIN_NO_CHANGE}; - a2dp_sink->set_pin_config(pin_config); - a2dp_sink->activate_pin_code(false); - #ifdef PLAY_MONO_SPEAKER - a2dp_sink->set_mono_downmix(true); - #endif - a2dp_sink->set_auto_reconnect(true); - a2dp_sink->set_rssi_active(true); - a2dp_sink->set_rssi_callback(rssi); - // start bluetooth sink - a2dp_sink->start((char *)FPSTR(nameBluetoothSinkDevice)); - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth sink started, Device: %s", (char *)FPSTR(nameBluetoothSinkDevice)); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - // connect events after startup - a2dp_sink->set_on_connection_state_changed(connection_state_changed); - a2dp_sink->set_on_audio_state_changed(audio_state_changed); - a2dp_sink->set_avrc_metadata_callback(avrc_metadata_callback); - a2dp_sink->set_on_volumechange(Bluetooth_VolumeChanged); - } else if (System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) { - // create audio source ringbuffer on demand - audioSourceRingBuffer = xRingbufferCreate(8192, RINGBUF_TYPE_BYTEBUF); - if (audioSourceRingBuffer == NULL) - Log_Println("cannot create audioSourceRingBuffer!", LOGLEVEL_ERROR); - // setup BT source - a2dp_source = new BluetoothA2DPSource(); +#ifdef BLUETOOTH_ENABLE + if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { + // bluetooth in sink mode (player acts as a BT-Speaker) + a2dp_sink = new BluetoothA2DPSink(); + i2s_pin_config_t pin_config = { + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) + .mck_io_num = 0, + #endif + .bck_io_num = I2S_BCLK, + .ws_io_num = I2S_LRC, + .data_out_num = I2S_DOUT, + .data_in_num = I2S_PIN_NO_CHANGE + }; + a2dp_sink->set_pin_config(pin_config); + a2dp_sink->activate_pin_code(false); + #ifdef PLAY_MONO_SPEAKER + a2dp_sink->set_mono_downmix(true); + #endif + a2dp_sink->set_auto_reconnect(true); + a2dp_sink->set_rssi_active(true); + a2dp_sink->set_rssi_callback(rssi); + // start bluetooth sink + a2dp_sink->start(nameBluetoothSinkDevice); + Log_Printf(LOGLEVEL_INFO, "Bluetooth sink started, Device: %s", nameBluetoothSinkDevice); + // connect events after startup + a2dp_sink->set_on_connection_state_changed(connection_state_changed, a2dp_sink); + a2dp_sink->set_on_audio_state_changed(audio_state_changed, a2dp_sink); + a2dp_sink->set_avrc_metadata_callback(avrc_metadata_callback); + a2dp_sink->set_on_volumechange(Bluetooth_VolumeChanged); + } else if (System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) { + // create audio source ringbuffer on demand + audioSourceRingBuffer = xRingbufferCreate(8192, RINGBUF_TYPE_BYTEBUF); + if (audioSourceRingBuffer == NULL) { + Log_Println("cannot create audioSourceRingBuffer!", LOGLEVEL_ERROR); + } + // setup BT source + a2dp_source = new BluetoothA2DPSource(); - //a2dp_source->set_auto_reconnect(false); // auto reconnect - //a2dp_source->set_task_core(1); // task core - //a2dp_source->set_nvs_init(true); // erase/initialize NVS - //a2dp_source->set_ssp_enabled(true); // enable secure simple pairing + // a2dp_source->set_auto_reconnect(false); // auto reconnect + // a2dp_source->set_task_core(1); // task core + // a2dp_source->set_nvs_init(true); // erase/initialize NVS + // a2dp_source->set_ssp_enabled(true); // enable secure simple pairing - // start bluetooth source - String btDeviceName = gPrefsSettings.getString("btDeviceName", nameBluetoothSourceDevice); - a2dp_source->start(btDeviceName.c_str(), get_data_channels); - snprintf(Log_Buffer, Log_BufferLength, "Bluetooth source started, connect to device: '%s'", (char *)FPSTR(btDeviceName.c_str())); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - // connect events after startup - a2dp_source->set_on_connection_state_changed(connection_state_changed); - a2dp_source->set_on_audio_state_changed(audio_state_changed); - // max headphone volume (0..255): volume is controlled by audio class - a2dp_source->set_volume(127); - } else { - esp_bt_mem_release(ESP_BT_MODE_BTDM); + // pairing pin-code, see https://forum.espuino.de/t/neues-feature-bluetooth-kopfhoerer/1293/30 + String btPinCode = gPrefsSettings.getString("btPinCode", ""); + if (btPinCode != "") { + a2dp_source->set_ssp_enabled(true); + a2dp_source->set_pin_code(btPinCode.c_str()); } - #endif + // start bluetooth source + a2dp_source->set_ssid_callback(scan_bluetooth_device_callback); + a2dp_source->start(get_data_channels); + // get device name + btDeviceName = ""; + if (gPrefsSettings.isKey("btDeviceName")) { + btDeviceName = gPrefsSettings.getString("btDeviceName", ""); + } + Log_Printf(LOGLEVEL_INFO, "Bluetooth source started, connect to device: '%s'", (btDeviceName == "") ? "connect to first device found" : btDeviceName.c_str()); + // connect events after startup + a2dp_source->set_on_connection_state_changed(connection_state_changed, a2dp_source); + a2dp_source->set_on_audio_state_changed(audio_state_changed, a2dp_source); + // max headphone volume (0..255): volume is controlled by audio class + a2dp_source->set_volume(127); + } else { + esp_bt_mem_release(ESP_BT_MODE_BTDM); + } +#endif } void Bluetooth_Cyclic(void) { - #ifdef BLUETOOTH_ENABLE - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { - esp_a2d_audio_state_t state = a2dp_sink->get_audio_state(); - // Reset Sleep Timer when audio is playing - if (state == ESP_A2D_AUDIO_STATE_STARTED) { - System_UpdateActivityTimer(); - } +#ifdef BLUETOOTH_ENABLE + if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { + esp_a2d_audio_state_t state = a2dp_sink->get_audio_state(); + // Reset Sleep Timer when audio is playing + if (state == ESP_A2D_AUDIO_STATE_STARTED) { + System_UpdateActivityTimer(); } - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) && (a2dp_source)) { - esp_a2d_audio_state_t state = a2dp_source->get_audio_state(); - // Reset Sleep Timer when audio is playing - if (state == ESP_A2D_AUDIO_STATE_STARTED) { - System_UpdateActivityTimer(); - } + } + if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) && (a2dp_source)) { + esp_a2d_audio_state_t state = a2dp_source->get_audio_state(); + // Reset Sleep Timer when audio is playing + if (state == ESP_A2D_AUDIO_STATE_STARTED) { + System_UpdateActivityTimer(); } - #endif + } +#endif } void Bluetooth_PlayPauseTrack(void) { - #ifdef BLUETOOTH_ENABLE - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { - esp_a2d_audio_state_t state = a2dp_sink->get_audio_state(); - if (state == ESP_A2D_AUDIO_STATE_STARTED) { - a2dp_sink->play(); - } else { - a2dp_sink->pause(); - } +#ifdef BLUETOOTH_ENABLE + if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { + esp_a2d_audio_state_t state = a2dp_sink->get_audio_state(); + if (state == ESP_A2D_AUDIO_STATE_STARTED) { + a2dp_sink->play(); + } else { + a2dp_sink->pause(); } - #endif + } +#endif } void Bluetooth_NextTrack(void) { - #ifdef BLUETOOTH_ENABLE - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { - a2dp_sink->next(); - } - #endif +#ifdef BLUETOOTH_ENABLE + if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { + a2dp_sink->next(); + } +#endif } void Bluetooth_PreviousTrack(void) { - #ifdef BLUETOOTH_ENABLE - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { - a2dp_sink->previous(); - } - #endif +#ifdef BLUETOOTH_ENABLE + if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink)) { + a2dp_sink->previous(); + } +#endif } // set volume from ESPuino to phone needs at least Arduino ESP version 2.0.0 void Bluetooth_SetVolume(const int32_t _newVolume, bool reAdjustRotary) { - #ifdef BLUETOOTH_ENABLE - if (!a2dp_sink) - return; - uint8_t _volume; - if (_newVolume < BLUETOOTHPLAYER_VOLUME_MIN) { - return; - } else if (_newVolume > BLUETOOTHPLAYER_VOLUME_MAX) { - return; - } else { - // map ESPuino min/max volume (0..21) to bluetooth volume (0..127) - _volume = map(_newVolume, BLUETOOTHPLAYER_VOLUME_MIN, BLUETOOTHPLAYER_VOLUME_MAX, 0, 0x7F); - a2dp_sink->set_volume(_volume); - if (reAdjustRotary) { - RotaryEncoder_Readjust(); - } +#ifdef BLUETOOTH_ENABLE + if (!a2dp_sink) { + return; + } + uint8_t _volume; + if (_newVolume < int32_t(BLUETOOTHPLAYER_VOLUME_MIN)) { + return; + } else if (_newVolume > BLUETOOTHPLAYER_VOLUME_MAX) { + return; + } else { + // map ESPuino min/max volume (0..21) to bluetooth volume (0..127) + _volume = map(_newVolume, BLUETOOTHPLAYER_VOLUME_MIN, BLUETOOTHPLAYER_VOLUME_MAX, 0, 0x7F); + a2dp_sink->set_volume(_volume); + if (reAdjustRotary) { + RotaryEncoder_Readjust(); } - #endif + } +#endif } -bool Bluetooth_Source_SendAudioData(uint32_t* sample) { - #ifdef BLUETOOTH_ENABLE - // send audio data to ringbuffer - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) && (a2dp_source) && a2dp_source->is_connected()) { - return (pdTRUE == xRingbufferSend(audioSourceRingBuffer, sample, sizeof(uint32_t), (portTickType)portMAX_DELAY)); - } else { - return false; - } - #else +bool Bluetooth_Source_SendAudioData(uint32_t *sample) { +#ifdef BLUETOOTH_ENABLE + // send audio data to ringbuffer + if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) && (a2dp_source) && a2dp_source->is_connected()) { + return (pdTRUE == xRingbufferSend(audioSourceRingBuffer, sample, sizeof(uint32_t), (TickType_t) portMAX_DELAY)); + } else { return false; - #endif + } +#else + return false; +#endif } -bool Bluetooth_Source_Connected() { - #ifdef BLUETOOTH_ENABLE - // send audio data to ringbuffer - if ((System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) && (a2dp_source) && a2dp_source->is_connected()) { - return true; - } else { - return false; - } - #else - return false; - #endif +bool Bluetooth_Device_Connected() { +#ifdef BLUETOOTH_ENABLE + // send audio data to ringbuffer + return (((System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) && (a2dp_sink) && a2dp_sink->is_connected()) || ((System_GetOperationMode() == OPMODE_BLUETOOTH_SOURCE) && (a2dp_source) && a2dp_source->is_connected())); +#else + return false; +#endif } diff --git a/src/Bluetooth.h b/src/Bluetooth.h index 64c88ce1..c6d6d55b 100644 --- a/src/Bluetooth.h +++ b/src/Bluetooth.h @@ -1,6 +1,5 @@ #pragma once - void Bluetooth_Init(void); void Bluetooth_Cyclic(void); @@ -14,5 +13,5 @@ void Bluetooth_PreviousTrack(void); // Support for AVRC Commands starting from ESP32 Release 2.0.0 void Bluetooth_SetVolume(const int32_t _newVolume, bool reAdjustRotary); -bool Bluetooth_Source_SendAudioData(uint32_t* sample); -bool Bluetooth_Source_Connected(); +bool Bluetooth_Source_SendAudioData(uint32_t *sample); +bool Bluetooth_Device_Connected(); diff --git a/src/Button.cpp b/src/Button.cpp index 9970f89f..7680a991 100644 --- a/src/Button.cpp +++ b/src/Button.cpp @@ -1,8 +1,10 @@ #include #include "settings.h" -#include "Log.h" + #include "Button.h" + #include "Cmd.h" +#include "Log.h" #include "Port.h" #include "System.h" @@ -42,12 +44,12 @@ bool gButtonInitComplete = false; #define EXPANDER_5_ENABLE #endif -t_button gButtons[7]; // next + prev + pplay + rotEnc + button4 + button5 + dummy-button +t_button gButtons[7]; // next + prev + pplay + rotEnc + button4 + button5 + dummy-button uint8_t gShutdownButton = 99; // Helper used for Neopixel: stores button-number of shutdown-button uint16_t gLongPressTime = 0; #ifdef PORT_EXPANDER_ENABLE - extern bool Port_AllowReadFromPortExpander; +extern bool Port_AllowReadFromPortExpander; #endif static volatile SemaphoreHandle_t Button_TimerSemaphore; @@ -57,65 +59,88 @@ static void IRAM_ATTR onTimer(); static void Button_DoButtonActions(void); void Button_Init() { - #if (WAKEUP_BUTTON >= 0 && WAKEUP_BUTTON <= MAX_GPIO) - if (ESP_ERR_INVALID_ARG == esp_sleep_enable_ext0_wakeup((gpio_num_t)WAKEUP_BUTTON, 0)) { - snprintf(Log_Buffer, Log_BufferLength, "%s (GPIO: %u)", (char *) FPSTR(wrongWakeUpGpio), WAKEUP_BUTTON); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - } - #endif +#if (WAKEUP_BUTTON >= 0 && WAKEUP_BUTTON <= MAX_GPIO) + if (ESP_ERR_INVALID_ARG == esp_sleep_enable_ext0_wakeup((gpio_num_t) WAKEUP_BUTTON, 0)) { + Log_Printf(LOGLEVEL_ERROR, wrongWakeUpGpio, WAKEUP_BUTTON); + } +#endif - #ifdef NEOPIXEL_ENABLE // Try to find button that is used for shutdown via longpress-action (only necessary for Neopixel) - #if defined(BUTTON_0_ENABLE) || defined(EXPANDER_0_ENABLE) - #if (BUTTON_0_LONG == CMD_SLEEPMODE) - gShutdownButton = 0; - #endif +#ifdef NEOPIXEL_ENABLE // Try to find button that is used for shutdown via longpress-action (only necessary for Neopixel) + #if defined(BUTTON_0_ENABLE) || defined(EXPANDER_0_ENABLE) + #if (BUTTON_0_LONG == CMD_SLEEPMODE) + gShutdownButton = 0; #endif - #if defined(BUTTON_1_ENABLE) || defined(EXPANDER_1_ENABLE) - #if (BUTTON_1_LONG == CMD_SLEEPMODE) - gShutdownButton = 1; - #endif + #endif + #if defined(BUTTON_1_ENABLE) || defined(EXPANDER_1_ENABLE) + #if (BUTTON_1_LONG == CMD_SLEEPMODE) + gShutdownButton = 1; #endif - #if defined(BUTTON_2_ENABLE) || defined(EXPANDER_2_ENABLE) - #if (BUTTON_2_LONG == CMD_SLEEPMODE) - gShutdownButton = 2; - #endif + #endif + #if defined(BUTTON_2_ENABLE) || defined(EXPANDER_2_ENABLE) + #if (BUTTON_2_LONG == CMD_SLEEPMODE) + gShutdownButton = 2; #endif - #if defined(BUTTON_3_ENABLE) || defined(EXPANDER_3_ENABLE) - #if (BUTTON_3_LONG == CMD_SLEEPMODE) - gShutdownButton = 3; - #endif + #endif + #if defined(BUTTON_3_ENABLE) || defined(EXPANDER_3_ENABLE) + #if (BUTTON_3_LONG == CMD_SLEEPMODE) + gShutdownButton = 3; #endif - #if defined(BUTTON_4_ENABLE) || defined(EXPANDER_4_ENABLE) - #if (BUTTON_4_LONG == CMD_SLEEPMODE) - gShutdownButton = 4; - #endif + #endif + #if defined(BUTTON_4_ENABLE) || defined(EXPANDER_4_ENABLE) + #if (BUTTON_4_LONG == CMD_SLEEPMODE) + gShutdownButton = 4; #endif - #if defined(BUTTON_5_ENABLE) || defined(EXPANDER_5_ENABLE) - #if (BUTTON_5_LONG == CMD_SLEEPMODE) - gShutdownButton = 5; - #endif + #endif + #if defined(BUTTON_5_ENABLE) || defined(EXPANDER_5_ENABLE) + #if (BUTTON_5_LONG == CMD_SLEEPMODE) + gShutdownButton = 5; #endif #endif +#endif - // Activate internal pullups for all enabled buttons connected to GPIOs - #ifdef BUTTON_0_ENABLE +// Activate internal pullups for all enabled buttons connected to GPIOs +#ifdef BUTTON_0_ENABLE + if (BUTTON_0_ACTIVE_STATE) { + pinMode(NEXT_BUTTON, INPUT); + } else { pinMode(NEXT_BUTTON, INPUT_PULLUP); - #endif - #ifdef BUTTON_1_ENABLE + } +#endif +#ifdef BUTTON_1_ENABLE + if (BUTTON_1_ACTIVE_STATE) { + pinMode(PREVIOUS_BUTTON, INPUT); + } else { pinMode(PREVIOUS_BUTTON, INPUT_PULLUP); - #endif - #ifdef BUTTON_2_ENABLE + } +#endif +#ifdef BUTTON_2_ENABLE + if (BUTTON_2_ACTIVE_STATE) { + pinMode(PAUSEPLAY_BUTTON, INPUT); + } else { pinMode(PAUSEPLAY_BUTTON, INPUT_PULLUP); - #endif - #ifdef BUTTON_3_ENABLE + } +#endif +#ifdef BUTTON_3_ENABLE + if (BUTTON_3_ACTIVE_STATE) { + pinMode(ROTARYENCODER_BUTTON, INPUT); + } else { pinMode(ROTARYENCODER_BUTTON, INPUT_PULLUP); - #endif - #ifdef BUTTON_4_ENABLE + } +#endif +#ifdef BUTTON_4_ENABLE + if (BUTTON_4_ACTIVE_STATE) { + pinMode(BUTTON_4, INPUT); + } else { pinMode(BUTTON_4, INPUT_PULLUP); - #endif - #ifdef BUTTON_5_ENABLE + } +#endif +#ifdef BUTTON_5_ENABLE + if (BUTTON_5_ACTIVE_STATE) { + pinMode(BUTTON_5, INPUT); + } else { pinMode(BUTTON_5, INPUT_PULLUP); - #endif + } +#endif // Create 1000Hz-HW-Timer (currently only used for buttons) Button_TimerSemaphore = xSemaphoreCreateBinary(); @@ -129,34 +154,34 @@ void Button_Init() { void Button_Cyclic() { if (xSemaphoreTake(Button_TimerSemaphore, 0) == pdTRUE) { unsigned long currentTimestamp = millis(); - #ifdef PORT_EXPANDER_ENABLE - Port_Cyclic(); - #endif +#ifdef PORT_EXPANDER_ENABLE + Port_Cyclic(); +#endif if (System_AreControlsLocked()) { return; } - // Buttons can be mixed between GPIO and port-expander. - // But at the same time only one of them can be for example NEXT_BUTTON - #if defined(BUTTON_0_ENABLE) || defined(EXPANDER_0_ENABLE) - gButtons[0].currentState = Port_Read(NEXT_BUTTON); - #endif - #if defined(BUTTON_1_ENABLE) || defined(EXPANDER_1_ENABLE) - gButtons[1].currentState = Port_Read(PREVIOUS_BUTTON); - #endif - #if defined(BUTTON_2_ENABLE) || defined(EXPANDER_2_ENABLE) - gButtons[2].currentState = Port_Read(PAUSEPLAY_BUTTON); - #endif - #if defined(BUTTON_3_ENABLE) || defined(EXPANDER_3_ENABLE) - gButtons[3].currentState = Port_Read(ROTARYENCODER_BUTTON); - #endif - #if defined(BUTTON_4_ENABLE) || defined(EXPANDER_4_ENABLE) - gButtons[4].currentState = Port_Read(BUTTON_4); - #endif - #if defined(BUTTON_5_ENABLE) || defined(EXPANDER_5_ENABLE) - gButtons[5].currentState = Port_Read(BUTTON_5); - #endif +// Buttons can be mixed between GPIO and port-expander. +// But at the same time only one of them can be for example NEXT_BUTTON +#if defined(BUTTON_0_ENABLE) || defined(EXPANDER_0_ENABLE) + gButtons[0].currentState = Port_Read(NEXT_BUTTON) ^ BUTTON_0_ACTIVE_STATE; +#endif +#if defined(BUTTON_1_ENABLE) || defined(EXPANDER_1_ENABLE) + gButtons[1].currentState = Port_Read(PREVIOUS_BUTTON) ^ BUTTON_1_ACTIVE_STATE; +#endif +#if defined(BUTTON_2_ENABLE) || defined(EXPANDER_2_ENABLE) + gButtons[2].currentState = Port_Read(PAUSEPLAY_BUTTON) ^ BUTTON_2_ACTIVE_STATE; +#endif +#if defined(BUTTON_3_ENABLE) || defined(EXPANDER_3_ENABLE) + gButtons[3].currentState = Port_Read(ROTARYENCODER_BUTTON) ^ BUTTON_3_ACTIVE_STATE; +#endif +#if defined(BUTTON_4_ENABLE) || defined(EXPANDER_4_ENABLE) + gButtons[4].currentState = Port_Read(BUTTON_4) ^ BUTTON_4_ACTIVE_STATE; +#endif +#if defined(BUTTON_5_ENABLE) || defined(EXPANDER_5_ENABLE) + gButtons[5].currentState = Port_Read(BUTTON_5) ^ BUTTON_5_ACTIVE_STATE; +#endif // Iterate over all buttons in struct-array for (uint8_t i = 0; i < sizeof(gButtons) / sizeof(gButtons[0]); i++) { diff --git a/src/Button.h b/src/Button.h index 19cfa869..931f202d 100644 --- a/src/Button.h +++ b/src/Button.h @@ -1,10 +1,10 @@ #pragma once typedef struct { - bool lastState : 1; + bool lastState : 1; bool currentState : 1; - bool isPressed : 1; - bool isReleased : 1; + bool isPressed : 1; + bool isReleased : 1; unsigned long lastPressedTimestamp; unsigned long lastReleasedTimestamp; unsigned long firstPressedTimestamp; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..483bc0cf --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,6 @@ +# This file was automatically generated for projects +# without default 'CMakeLists.txt' file. + +FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) + +idf_component_register(SRCS ${app_sources}) diff --git a/src/Cmd.cpp b/src/Cmd.cpp index d06e1869..5e274599 100644 --- a/src/Cmd.cpp +++ b/src/Cmd.cpp @@ -1,6 +1,8 @@ #include #include "settings.h" + #include "Cmd.h" + #include "AudioPlayer.h" #include "Battery.h" #include "Bluetooth.h" @@ -16,15 +18,15 @@ void Cmd_Action(const uint16_t mod) { case CMD_LOCK_BUTTONS_MOD: { // Locks/unlocks all buttons System_ToggleLockControls(); if (System_AreControlsLocked()) { - Log_Println((char *) FPSTR(modificatorAllButtonsLocked), LOGLEVEL_NOTICE); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLockControlsState), "ON", false); - #endif + Log_Println(modificatorAllButtonsLocked, LOGLEVEL_NOTICE); +#ifdef MQTT_ENABLE + publishMqtt(topicLockControlsState, "ON", false); +#endif } else { - Log_Println((char *) FPSTR(modificatorAllButtonsUnlocked), LOGLEVEL_NOTICE); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLockControlsState), "OFF", false); - #endif + Log_Println(modificatorAllButtonsUnlocked, LOGLEVEL_NOTICE); +#ifdef MQTT_ENABLE + publishMqtt(topicLockControlsState, "OFF", false); +#endif } System_IndicateOk(); break; @@ -34,7 +36,7 @@ void Cmd_Action(const uint16_t mod) { System_SetSleepTimer(15u); gPlayProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active - gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active gPlayProperties.playUntilTrackNumber = 0; System_IndicateOk(); break; @@ -44,7 +46,7 @@ void Cmd_Action(const uint16_t mod) { System_SetSleepTimer(30u); gPlayProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active - gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active gPlayProperties.playUntilTrackNumber = 0; System_IndicateOk(); break; @@ -54,7 +56,7 @@ void Cmd_Action(const uint16_t mod) { System_SetSleepTimer(60u); gPlayProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active - gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active gPlayProperties.playUntilTrackNumber = 0; System_IndicateOk(); break; @@ -64,7 +66,7 @@ void Cmd_Action(const uint16_t mod) { System_SetSleepTimer(120u); gPlayProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active - gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + gPlayProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active gPlayProperties.playUntilTrackNumber = 0; System_IndicateOk(); break; @@ -72,7 +74,7 @@ void Cmd_Action(const uint16_t mod) { case CMD_SLEEP_AFTER_END_OF_TRACK: { // Puts uC to sleep after end of current track if (gPlayProperties.playMode == NO_PLAYLIST) { - Log_Println((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + Log_Println(modificatorNotallowedWhenIdle, LOGLEVEL_NOTICE); System_IndicateError(); return; } @@ -82,63 +84,63 @@ void Cmd_Action(const uint16_t mod) { if (gPlayProperties.sleepAfterCurrentTrack) { gPlayProperties.sleepAfterCurrentTrack = false; - Log_Println((char *) FPSTR(modificatorSleepAtEOTd), LOGLEVEL_NOTICE); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "0", false); - #endif + Log_Println(modificatorSleepAtEOTd, LOGLEVEL_NOTICE); +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "0", false); +#endif Led_ResetToInitialBrightness(); } else { System_DisableSleepTimer(); gPlayProperties.sleepAfterCurrentTrack = true; - Log_Println((char *) FPSTR(modificatorSleepAtEOT), LOGLEVEL_NOTICE); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "EOT", false); - #endif + Log_Println(modificatorSleepAtEOT, LOGLEVEL_NOTICE); +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "EOT", false); +#endif Led_ResetToNightBrightness(); } - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); +#endif System_IndicateOk(); break; } case CMD_SLEEP_AFTER_END_OF_PLAYLIST: { // Puts uC to sleep after end of whole playlist (can take a while :->) if (gPlayProperties.playMode == NO_PLAYLIST) { - Log_Println((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + Log_Println(modificatorNotallowedWhenIdle, LOGLEVEL_NOTICE); System_IndicateError(); return; } if (gPlayProperties.sleepAfterPlaylist) { System_DisableSleepTimer(); gPlayProperties.sleepAfterPlaylist = false; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "0", false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "0", false); +#endif Led_ResetToInitialBrightness(); - Log_Println((char *) FPSTR(modificatorSleepAtEOPd), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepAtEOPd, LOGLEVEL_NOTICE); } else { gPlayProperties.sleepAfterPlaylist = true; Led_ResetToNightBrightness(); - Log_Println((char *) FPSTR(modificatorSleepAtEOP), LOGLEVEL_NOTICE); - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "EOP", false); - #endif + Log_Println(modificatorSleepAtEOP, LOGLEVEL_NOTICE); +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "EOP", false); +#endif } gPlayProperties.sleepAfterCurrentTrack = false; gPlayProperties.playUntilTrackNumber = 0; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); +#endif System_IndicateOk(); break; } case CMD_SLEEP_AFTER_5_TRACKS: { if (gPlayProperties.playMode == NO_PLAYLIST) { - Log_Println((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + Log_Println(modificatorNotallowedWhenIdle, LOGLEVEL_NOTICE); System_IndicateError(); return; } @@ -150,49 +152,49 @@ void Cmd_Action(const uint16_t mod) { if (gPlayProperties.sleepAfter5Tracks) { gPlayProperties.sleepAfter5Tracks = false; gPlayProperties.playUntilTrackNumber = 0; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "0", false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "0", false); +#endif Led_ResetToInitialBrightness(); - Log_Println((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepd, LOGLEVEL_NOTICE); } else { gPlayProperties.sleepAfter5Tracks = true; if (gPlayProperties.currentTrackNumber + 5 > gPlayProperties.numberOfTracks) { // If currentTrack + 5 exceeds number of tracks in playlist, sleep after end of playlist gPlayProperties.sleepAfterPlaylist = true; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "EOP", false); - #endif - } else { +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "EOP", false); +#endif + } else { gPlayProperties.playUntilTrackNumber = gPlayProperties.currentTrackNumber + 5; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), "EO5T", false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, "EO5T", false); +#endif } Led_ResetToNightBrightness(); - Log_Println((char *) FPSTR(sleepTimerEO5), LOGLEVEL_NOTICE); + Log_Println(sleepTimerEO5, LOGLEVEL_NOTICE); } - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); +#endif System_IndicateOk(); break; } case CMD_REPEAT_PLAYLIST: { if (gPlayProperties.playMode == NO_PLAYLIST) { - Log_Println((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + Log_Println(modificatorNotallowedWhenIdle, LOGLEVEL_NOTICE); System_IndicateError(); } else { if (gPlayProperties.repeatPlaylist) { - Log_Println((char *) FPSTR(modificatorPlaylistLoopDeactive), LOGLEVEL_NOTICE); + Log_Println(modificatorPlaylistLoopDeactive, LOGLEVEL_NOTICE); } else { - Log_Println((char *) FPSTR(modificatorPlaylistLoopActive), LOGLEVEL_NOTICE); + Log_Println(modificatorPlaylistLoopActive, LOGLEVEL_NOTICE); } gPlayProperties.repeatPlaylist = !gPlayProperties.repeatPlaylist; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); +#endif System_IndicateOk(); } break; @@ -200,84 +202,97 @@ void Cmd_Action(const uint16_t mod) { case CMD_REPEAT_TRACK: { // Introduces looping for track-mode if (gPlayProperties.playMode == NO_PLAYLIST) { - Log_Println((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + Log_Println(modificatorNotallowedWhenIdle, LOGLEVEL_NOTICE); System_IndicateError(); } else { if (gPlayProperties.repeatCurrentTrack) { - Log_Println((char *) FPSTR(modificatorTrackDeactive), LOGLEVEL_NOTICE); + Log_Println(modificatorTrackDeactive, LOGLEVEL_NOTICE); } else { - Log_Println((char *) FPSTR(modificatorTrackActive), LOGLEVEL_NOTICE); + Log_Println(modificatorTrackActive, LOGLEVEL_NOTICE); } gPlayProperties.repeatCurrentTrack = !gPlayProperties.repeatCurrentTrack; - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); +#endif System_IndicateOk(); } break; } case CMD_DIMM_LEDS_NIGHTMODE: { - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - #endif - Log_Println((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); +#ifdef MQTT_ENABLE + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); +#endif + Log_Println(ledsDimmedToNightmode, LOGLEVEL_INFO); Led_ResetToNightBrightness(); System_IndicateOk(); break; } case CMD_TOGGLE_WIFI_STATUS: { + System_SetOperationMode(OPMODE_NORMAL); // escape from BT-mode, WiFi cannot coexist with BT and can cause a crash Wlan_ToggleEnable(); System_IndicateOk(); break; } - #ifdef BLUETOOTH_ENABLE - case CMD_TOGGLE_BLUETOOTH_SINK_MODE: { - if (System_GetOperationModeFromNvs() == OPMODE_NORMAL) { - System_IndicateOk(); - System_SetOperationMode(OPMODE_BLUETOOTH_SINK); - } else if (System_GetOperationModeFromNvs() == OPMODE_BLUETOOTH_SINK) { - System_IndicateOk(); - System_SetOperationMode(OPMODE_NORMAL); - } else { - System_IndicateError(); - } - break; +#ifdef BLUETOOTH_ENABLE + case CMD_TOGGLE_BLUETOOTH_SINK_MODE: { + if (System_GetOperationModeFromNvs() == OPMODE_NORMAL) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_BLUETOOTH_SINK); + } else if (System_GetOperationModeFromNvs() == OPMODE_BLUETOOTH_SINK) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_NORMAL); + } else { + System_IndicateError(); } - case CMD_TOGGLE_BLUETOOTH_SOURCE_MODE: { - if (System_GetOperationModeFromNvs() == OPMODE_NORMAL) { - System_IndicateOk(); - System_SetOperationMode(OPMODE_BLUETOOTH_SOURCE); - } else if (System_GetOperationModeFromNvs() == OPMODE_BLUETOOTH_SOURCE) { - System_IndicateOk(); - System_SetOperationMode(OPMODE_NORMAL); - } else { - System_IndicateError(); - } - break; + break; + } + case CMD_TOGGLE_BLUETOOTH_SOURCE_MODE: { + if (System_GetOperationModeFromNvs() == OPMODE_NORMAL) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_BLUETOOTH_SOURCE); + } else if (System_GetOperationModeFromNvs() == OPMODE_BLUETOOTH_SOURCE) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_NORMAL); + } else { + System_IndicateError(); } - #endif + break; + } + case CMD_TOGGLE_MODE: { + if (System_GetOperationModeFromNvs() == OPMODE_NORMAL) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_BLUETOOTH_SINK); + } else if (System_GetOperationModeFromNvs() == OPMODE_BLUETOOTH_SINK) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_BLUETOOTH_SOURCE); + } else if (System_GetOperationModeFromNvs() == OPMODE_BLUETOOTH_SOURCE) { + System_IndicateOk(); + System_SetOperationMode(OPMODE_NORMAL); + } else { + System_IndicateError(); + } + break; + } +#endif - #ifdef FTP_ENABLE - case CMD_ENABLE_FTP_SERVER: { - if (millis() <= 30000) { // Only allow to enable FTP within the first 30s after start (to prevent children it mess it up) - Ftp_EnableServer(); - } else { - Log_Println((char *) FPSTR(ftpEnableTooLate), LOGLEVEL_ERROR); - System_IndicateError(); - } - break; +#ifdef FTP_ENABLE + case CMD_ENABLE_FTP_SERVER: { + if (millis() <= 30000) { // Only allow to enable FTP within the first 30s after start (to prevent children it mess it up) + Ftp_EnableServer(); + } else { + Log_Println(ftpEnableTooLate, LOGLEVEL_ERROR); + System_IndicateError(); } - #endif + break; + } +#endif case CMD_TELL_IP_ADDRESS: { if (Wlan_IsConnected()) { - if (!gPlayProperties.pausePlay) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - } - gPlayProperties.tellIpAddress = true; + gPlayProperties.tellMode = TTS_IP_ADDRESS; gPlayProperties.currentSpeechActive = true; gPlayProperties.lastSpeechActive = true; System_IndicateOk(); @@ -288,8 +303,21 @@ void Cmd_Action(const uint16_t mod) { break; } + case CMD_TELL_CURRENT_TIME: { + if (Wlan_IsConnected()) { + gPlayProperties.tellMode = TTS_CURRENT_TIME; + gPlayProperties.currentSpeechActive = true; + gPlayProperties.lastSpeechActive = true; + System_IndicateOk(); + } else { + Log_Println(unableToTellTime, LOGLEVEL_ERROR); + System_IndicateError(); + } + break; + } + case CMD_PLAYPAUSE: { - if (OPMODE_NORMAL == System_GetOperationMode()) { + if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) { AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); } else { Bluetooth_PlayPauseTrack(); @@ -298,7 +326,7 @@ void Cmd_Action(const uint16_t mod) { } case CMD_PREVTRACK: { - if (OPMODE_NORMAL == System_GetOperationMode()) { + if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) { AudioPlayer_TrackControlToQueueSender(PREVIOUSTRACK); } else { Bluetooth_PreviousTrack(); @@ -307,7 +335,7 @@ void Cmd_Action(const uint16_t mod) { } case CMD_NEXTTRACK: { - if (OPMODE_NORMAL == System_GetOperationMode()) { + if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) { AudioPlayer_TrackControlToQueueSender(NEXTTRACK); } else { Bluetooth_NextTrack(); @@ -331,7 +359,7 @@ void Cmd_Action(const uint16_t mod) { } case CMD_VOLUMEUP: { - if (OPMODE_NORMAL == System_GetOperationMode()) { + if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) { AudioPlayer_VolumeToQueueSender(AudioPlayer_GetCurrentVolume() + 1, true); } else { Bluetooth_SetVolume(AudioPlayer_GetCurrentVolume() + 1, true); @@ -339,21 +367,21 @@ void Cmd_Action(const uint16_t mod) { break; } - case CMD_VOLUMEDOWN:{ - if (OPMODE_NORMAL == System_GetOperationMode()) { + case CMD_VOLUMEDOWN: { + if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) { AudioPlayer_VolumeToQueueSender(AudioPlayer_GetCurrentVolume() - 1, true); } else { Bluetooth_SetVolume(AudioPlayer_GetCurrentVolume() - 1, true); } - break; + break; } case CMD_MEASUREBATTERY: { - #ifdef BATTERY_MEASURE_ENABLE - Battery_LogStatus(); - Battery_PublishMQTT(); - Led_Indicate(LedIndicatorType::Voltage); - #endif +#ifdef BATTERY_MEASURE_ENABLE + Battery_LogStatus(); + Battery_PublishMQTT(); + Led_Indicate(LedIndicatorType::Voltage); +#endif break; } @@ -362,6 +390,11 @@ void Cmd_Action(const uint16_t mod) { break; } + case CMD_RESTARTSYSTEM: { + System_Restart(); + break; + } + case CMD_SEEK_FORWARDS: { gPlayProperties.seekmode = SEEK_FORWARDS; break; @@ -377,16 +410,15 @@ void Cmd_Action(const uint16_t mod) { break; } - #ifdef ENABLE_ESPUINO_DEBUG - case PRINT_TASK_STATS: { - System_esp_print_tasks(); - break; - } - #endif +#ifdef ENABLE_ESPUINO_DEBUG + case PRINT_TASK_STATS: { + System_esp_print_tasks(); + break; + } +#endif default: { - snprintf(Log_Buffer, Log_BufferLength, "%s %d !", (char *) FPSTR(modificatorDoesNotExist), mod); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, modificatorDoesNotExist, mod); System_IndicateError(); } } diff --git a/src/Common.h b/src/Common.h index b33fedf1..15881097 100644 --- a/src/Common.h +++ b/src/Common.h @@ -3,12 +3,11 @@ // FilePathLength #define MAX_FILEPATH_LENTGH 256 -constexpr char stringDelimiter[] = "#"; // Character used to encapsulate data in linear NVS-strings (don't change) +constexpr char stringDelimiter[] = "#"; // Character used to encapsulate data in linear NVS-strings (don't change) constexpr char stringOuterDelimiter[] = "^"; // Character used to encapsulate encapsulated data along with RFID-ID in backup-file -inline bool isNumber(const char *str) -{ - byte i = 0; +inline bool isNumber(const char *str) { + int i = 0; while (*(str + i) != '\0') { if (!isdigit(*(str + i++))) { @@ -18,7 +17,7 @@ inline bool isNumber(const char *str) if (i > 0) { return true; - } else{ + } else { return false; } } @@ -48,51 +47,10 @@ inline bool endsWith(const char *str, const char *suf) { return b == suf && *a == *b; } -inline void convertUtf8ToAscii(String utf8String, char *asciiString) { - - int k = 0; - bool f_C3_seen = false; - - for (int i = 0; i < utf8String.length() && k < MAX_FILEPATH_LENTGH - 1; i++) { - - if (utf8String[i] == 195) { // C3 - f_C3_seen = true; - continue; - } else { - if (f_C3_seen == true) { - f_C3_seen = false; - switch (utf8String[i]) { - case 0x84: - asciiString[k++] = 0x8e; - break; // Ä - case 0xa4: - asciiString[k++] = 0x84; - break; // ä - case 0x9c: - asciiString[k++] = 0x9a; - break; // Ü - case 0xbc: - asciiString[k++] = 0x81; - break; // ü - case 0x96: - asciiString[k++] = 0x99; - break; // Ö - case 0xb6: - asciiString[k++] = 0x94; - break; // ö - case 0x9f: - asciiString[k++] = 0xe1; - break; // ß - default: - asciiString[k++] = 0xdb; // Unknown... - } - } else { - asciiString[k++] = utf8String[i]; - } - } - } - - asciiString[k] = 0; +inline void convertFilenameToAscii(String utf8String, char *asciiString) { + // Arduino >= 2.0.5 filenames are already unicode, copy to result here without UTF-8 decoding + strncpy(asciiString, (char *) utf8String.c_str(), utf8String.length() / sizeof(asciiString[0])); + asciiString[utf8String.length()] = 0; } inline void convertAsciiToUtf8(String asciiString, char *utf8String) { @@ -140,10 +98,10 @@ inline void convertAsciiToUtf8(String asciiString, char *utf8String) { // Release previously allocated memory inline void freeMultiCharArray(char **arr, const uint32_t cnt) { - for (uint32_t i = 0; i <= cnt; i++) { - /*snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(freePtr), *(arr+i)); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG);*/ + for (uint32_t i = 0; i < cnt; i++) { + // Log_Printf(LOGLEVEL_DEBUG, freePtr, *(arr+i), arr); free(*(arr + i)); } + free(arr); *arr = NULL; } diff --git a/src/Ftp.cpp b/src/Ftp.cpp index 9c2dd629..e7c88c97 100644 --- a/src/Ftp.cpp +++ b/src/Ftp.cpp @@ -1,24 +1,27 @@ #include -#include #include "settings.h" + #include "Ftp.h" + #include "Log.h" #include "MemX.h" #include "SdCard.h" #include "System.h" #include "Wlan.h" +#include + #ifdef FTP_ENABLE - #include "ESP32FtpServer.h" + #include "ESP-FTP-Server-Lib.h" #endif // FTP -String Ftp_User = "esp32"; // FTP-user (default; can be changed later via GUI) -String Ftp_Password = "esp32"; // FTP-password (default; can be changed later via GUI) +String Ftp_User = "esp32"; // FTP-user (default; can be changed later via GUI) +String Ftp_Password = "esp32"; // FTP-password (default; can be changed later via GUI) // FTP #ifdef FTP_ENABLE -FtpServer *ftpSrv; // Heap-alloction takes place later (when needed) +FTPServer *ftpSrv; // Heap-alloction takes place later (when needed) bool ftpEnableLastStatus = false; bool ftpEnableCurrentStatus = false; #endif @@ -30,75 +33,69 @@ void Ftp_Init(void) { // Get FTP-user from NVS String nvsFtpUser = gPrefsSettings.getString("ftpuser", "-1"); if (!nvsFtpUser.compareTo("-1")) { - gPrefsSettings.putString("ftpuser", (String)Ftp_User); - Log_Println((char *) FPSTR(wroteFtpUserToNvs), LOGLEVEL_ERROR); + gPrefsSettings.putString("ftpuser", (String) Ftp_User); + Log_Println(wroteFtpUserToNvs, LOGLEVEL_ERROR); } else { Ftp_User = nvsFtpUser; - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredFtpUserFromNvs), nvsFtpUser.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, restoredFtpUserFromNvs, nvsFtpUser.c_str()); } // Get FTP-password from NVS String nvsFtpPassword = gPrefsSettings.getString("ftppassword", "-1"); if (!nvsFtpPassword.compareTo("-1")) { - gPrefsSettings.putString("ftppassword", (String)Ftp_Password); - Log_Println((char *) FPSTR(wroteFtpPwdToNvs), LOGLEVEL_ERROR); + gPrefsSettings.putString("ftppassword", (String) Ftp_Password); + Log_Println(wroteFtpPwdToNvs, LOGLEVEL_ERROR); } else { Ftp_Password = nvsFtpPassword; - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredFtpPwdFromNvs), nvsFtpPassword.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, restoredFtpPwdFromNvs, nvsFtpPassword.c_str()); } } void Ftp_Cyclic(void) { - #ifdef FTP_ENABLE - ftpManager(); +#ifdef FTP_ENABLE + ftpManager(); - if (WL_CONNECTED == WiFi.status()) { - if (ftpEnableLastStatus && ftpEnableCurrentStatus) { - ftpSrv->handleFTP(); - } + if (WL_CONNECTED == WiFi.status()) { + if (ftpEnableLastStatus && ftpEnableCurrentStatus) { + ftpSrv->handle(); } + } - if (ftpEnableLastStatus && ftpEnableCurrentStatus) { - if (ftpSrv->isConnected()) { - System_UpdateActivityTimer(); // Re-adjust timer while client is connected to avoid ESP falling asleep - } + if (ftpEnableLastStatus && ftpEnableCurrentStatus) { + if (ftpSrv->countConnections() > 0) { + System_UpdateActivityTimer(); // Re-adjust timer while client is connected to avoid ESP falling asleep } - #endif + } +#endif } void Ftp_EnableServer(void) { - #ifdef FTP_ENABLE - if (Wlan_IsConnected() && !ftpEnableLastStatus && !ftpEnableCurrentStatus) { - ftpEnableLastStatus = true; - #else - if (Wlan_IsConnected()) { - #endif +#ifdef FTP_ENABLE + if (Wlan_IsConnected() && !ftpEnableLastStatus && !ftpEnableCurrentStatus) { + ftpEnableLastStatus = true; +#else + if (Wlan_IsConnected()) { +#endif - System_IndicateOk(); + System_IndicateOk(); } else { - Log_Println((char *) FPSTR(unableToStartFtpServer), LOGLEVEL_ERROR); + Log_Println(unableToStartFtpServer, LOGLEVEL_ERROR); System_IndicateError(); } } // Creates FTP-instance only when requested void ftpManager(void) { - #ifdef FTP_ENABLE - if (ftpEnableLastStatus && !ftpEnableCurrentStatus) { - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(freeHeapWithoutFtp), ESP.getFreeHeap()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - ftpEnableCurrentStatus = true; - ftpSrv = new FtpServer(); - ftpSrv->begin(gFSystem, Ftp_User, Ftp_Password); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(freeHeapWithFtp), ESP.getFreeHeap()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - #if (LANGUAGE == DE) - Log_Println((char *) F("FTP-Server gestartet"), LOGLEVEL_NOTICE); - #else - Log_Println((char *) F("FTP-Server started"), LOGLEVEL_NOTICE); - #endif - } - #endif +#ifdef FTP_ENABLE + if (ftpEnableLastStatus && !ftpEnableCurrentStatus) { + Log_Printf(LOGLEVEL_DEBUG, freeHeapWithoutFtp, ESP.getFreeHeap()); + ftpEnableCurrentStatus = true; + ftpSrv = new FTPServer(); + ftpSrv->addUser(Ftp_User, Ftp_Password); + ftpSrv->addFilesystem("SD-Card", &gFSystem); + ftpSrv->begin(); + Log_Printf(LOGLEVEL_DEBUG, freeHeapWithFtp, ESP.getFreeHeap()); + Log_Println(ftpServerStarted, LOGLEVEL_NOTICE); + } +#endif } diff --git a/src/Ftp.h b/src/Ftp.h index fc43124a..dd13cb90 100644 --- a/src/Ftp.h +++ b/src/Ftp.h @@ -1,7 +1,7 @@ #pragma once -constexpr uint8_t ftpUserLength = 10u; // Length will be published n-1 as maxlength to GUI -constexpr uint8_t ftpPasswordLength = 15u; // Length will be published n-1 as maxlength to GUI +constexpr uint8_t ftpUserLength = 10u; // Length will be published n-1 as maxlength to GUI +constexpr uint8_t ftpPasswordLength = 15u; // Length will be published n-1 as maxlength to GUI void Ftp_Init(void); void Ftp_Cyclic(void); diff --git a/src/HallEffectSensor.cpp b/src/HallEffectSensor.cpp index b970c788..6244b51a 100644 --- a/src/HallEffectSensor.cpp +++ b/src/HallEffectSensor.cpp @@ -1,160 +1,160 @@ // NIKO-AT 04.12.2022 Project ESPuino new feature: https://forum.espuino.de/t/magnetische-hockey-tags/1449/35 #include -#include "System.h" -#include "HallEffectSensor.h" #include "settings.h" + +#include "HallEffectSensor.h" + #include "Log.h" +#include "System.h" #ifdef HALLEFFECT_SENSOR_ENABLE -// debugging -#define HallEffectDebug_DISABLED // remove _DISABLED - -// gPrefs VarNames -#define NULLFIELDVALUENAME "HES_NullValue" + // debugging + #define HallEffectDebug_DISABLED // remove _DISABLED + // gPrefs VarNames + #define NULLFIELDVALUENAME "HES_NullValue" HallEffectSensor gHallEffectSensor; - -HallEffectSensor::HallEffectSensor(){ - pinMode(HallEffectSensor_PIN,INPUT); +HallEffectSensor::HallEffectSensor() { + pinMode(HallEffectSensor_PIN, INPUT); }; // private -void HallEffectSensor::LoadNullFieldValueFromNVS(){ - nullFieldValue = gPrefsSettings.getUShort(NULLFIELDVALUENAME, 0); - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("HallEffectSensor> NullFieldValue from NVS:%d")), nullFieldValue); - Log_Println(Log_Buffer, LOGLEVEL_INFO); -} - -bool HallEffectSensor::SaveToNullFieldValueNVS( uint16_t val){ - uint16_t diff = abs(val-nullFieldValue); - bool res = (gPrefsSettings.putUShort(NULLFIELDVALUENAME, val) == 2); - char resMsg[10] = "ERROR"; - if (res){ - strcpy(resMsg,"OK"); - nullFieldValue= val; - } - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("HallEffectSensor> Save NullFieldValue in NVS> Result:%s> NullValue:%d, diff:%d")), resMsg, val, diff); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - return res; +void HallEffectSensor::LoadNullFieldValueFromNVS() { + nullFieldValue = gPrefsSettings.getUShort(NULLFIELDVALUENAME, 0); + Log_Printf(LOGLEVEL_INFO, "HallEffectSensor> NullFieldValue from NVS:%d", nullFieldValue); +} + +bool HallEffectSensor::SaveToNullFieldValueNVS(uint16_t val) { + uint16_t diff = abs(val - nullFieldValue); + bool res = (gPrefsSettings.putUShort(NULLFIELDVALUENAME, val) == 2); + const char *resMsg = "ERROR"; + if (res) { + resMsg = "OK"; + nullFieldValue = val; + } + Log_Printf(LOGLEVEL_INFO, "HallEffectSensor> Save NullFieldValue in NVS> Result:%s> NullValue:%d, diff:%d", resMsg, val, diff); + return res; } -void HallEffectSensor::StartDebugging(){ - maxVal = 0; - minVal = 9999; - diffVal = 0; - lastCalibrationSubCallMS = 0; - calibrationFctCalls = 0; +void HallEffectSensor::StartDebugging() { + maxVal = 0; + minVal = 9999; + diffVal = 0; + lastCalibrationSubCallMS = 0; + calibrationFctCalls = 0; }; // public -void HallEffectSensor::init(){ - LoadNullFieldValueFromNVS(); - #ifdef HallEffectDebug - gHallEffectSensor.calibration(); - #else - int sva=gHallEffectSensor.readSensorValueAverage(true); - // check calibration - int diff = abs(sva-nullFieldValue); - if ((nullFieldValue<1) || ((diff>5)&&(diff=HallEffectMinDiffValue) { - diff = sva-nullFieldValue; - byte nr = 1; - if (diff>0) nr=2; - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("HallEffectSensor> No AutoCalibration> TAG side:%d detected")), nr); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("HallEffectSensor> No AutoCalibration> Difference too small> diff:%d")), diff); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - #endif +void HallEffectSensor::init() { + LoadNullFieldValueFromNVS(); + #ifdef HallEffectDebug + gHallEffectSensor.calibration(); + #else + int sva = gHallEffectSensor.readSensorValueAverage(true); + // check calibration + int diff = abs(sva - nullFieldValue); + if ((nullFieldValue < 1) || ((diff > 5) && (diff < HallEffectMaxDiffValueReCalibrate))) { + SaveToNullFieldValueNVS(sva); + } else if (diff >= HallEffectMinDiffValue) { + diff = sva - nullFieldValue; + byte nr = 1; + if (diff > 0) { + nr = 2; + } + Log_Printf(LOGLEVEL_INFO, "HallEffectSensor> No AutoCalibration> TAG side:%d detected", nr); + } else { + Log_Printf(LOGLEVEL_INFO, "HallEffectSensor> No AutoCalibration> Difference too small> diff:%d", diff); + } + #endif } -void HallEffectSensor::cyclic(){ - #ifdef HallEffectDebug - if ((calibrationFctCalls < maxCalibrationFctCalls) && (millis() > (lastCalibrationSubCallMS + calibrationFctCallsPauseMS))){ - readSensorValueAverage(false); - calibrationFctCalls++; - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("%d. calibration-loop of HallEffectSensor (make sure NO TAG is/was near reader!) ...")), calibrationFctCalls); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - lastCalibrationSubCallMS = millis(); - } - if (calibrationFctCalls == maxCalibrationFctCalls){ // Log results - uint16_t sva = (maxVal+minVal)/2; - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("Finished calibration of HallEffectSensor> HES_Uav:%d (min:%d, max:%d, (diff/2):%d, probes:%d)")), sva, minVal, maxVal, diffVal/2, calibrationProbes); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - calibrationFctCalls++; // stop logging results - #ifdef HallEffectDebug - calibration(); - delay(100); - #endif - } - #endif +void HallEffectSensor::cyclic() { + #ifdef HallEffectDebug + if ((calibrationFctCalls < maxCalibrationFctCalls) && (millis() > (lastCalibrationSubCallMS + calibrationFctCallsPauseMS))) { + readSensorValueAverage(false); + calibrationFctCalls++; + Log_Printf(LOGLEVEL_INFO, "%d. calibration-loop of HallEffectSensor (make sure NO TAG is/was near reader!) ...", calibrationFctCalls); + lastCalibrationSubCallMS = millis(); + } + if (calibrationFctCalls == maxCalibrationFctCalls) { // Log results + uint16_t sva = (maxVal + minVal) / 2; + Log_Printf(LOGLEVEL_INFO, "Finished calibration of HallEffectSensor> HES_Uav:%d (min:%d, max:%d, (diff/2):%d, probes:%d)", sva, minVal, maxVal, diffVal / 2, calibrationProbes); + calibrationFctCalls++; // stop logging results + #ifdef HallEffectDebug + calibration(); + delay(100); + #endif + } + #endif }; -uint16_t HallEffectSensor::readSensorValueAverage(bool doLog){ - // read analog value HallEffectAverageProbes times and return average - int sum = 0; - int cnt = 0; - for (int i=0; imaxVal) maxVal = av; - if (av value:%d")), av); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - return av; +uint16_t HallEffectSensor::readSensorValueAverage(bool doLog) { + // read analog value HallEffectAverageProbes times and return average + int sum = 0; + int cnt = 0; + for (int i = 0; i < HallEffectAverageProbesMS * 40; i++) { + uint16_t v = analogRead(HallEffectSensor_PIN); + sum += v; + cnt++; + delayMicroseconds(25); + } + uint16_t av = sum / cnt; + // update statistic + if (av > maxVal) { + maxVal = av; + } + if (av < minVal) { + minVal = av; + } + diffVal = maxVal - minVal; + calibrationProbes++; + if (doLog) { + Log_Printf(LOGLEVEL_INFO, "Read HallEffectSensor> value:%d", av); + } + return av; }; -bool HallEffectSensor::saveActualFieldValue2NVS(){ - uint16_t sva=gHallEffectSensor.readSensorValueAverage(true); - bool res= SaveToNullFieldValueNVS(sva); - LoadNullFieldValueFromNVS(); - return res; +bool HallEffectSensor::saveActualFieldValue2NVS() { + uint16_t sva = gHallEffectSensor.readSensorValueAverage(true); + bool res = SaveToNullFieldValueNVS(sva); + LoadNullFieldValueFromNVS(); + return res; }; -uint8_t HallEffectSensor::waitForState( uint16_t waitMS){ - // wait for magnetic north or south - lastWaitForState=0; - unsigned long int startms = millis();; - uint16_t sav; - do { - sav = readSensorValueAverage(true); - if (sav < nullFieldValue-HallEffectMinDiffValue) - lastWaitForState = 1; - else if (sav > nullFieldValue+HallEffectMinDiffValue) - lastWaitForState = 2; - else - delay(50); - } while ( (lastWaitForState==0) && (millis() < (startms + waitMS))); - lastWaitForStateMS = millis()-startms; - snprintf(Log_Buffer, Log_BufferLength, "HallEffectSensor waitForState> fieldValue:%d => state:%d, (duration:%d ms)", sav, lastWaitForState, lastWaitForStateMS); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - return lastWaitForState; +uint8_t HallEffectSensor::waitForState(uint16_t waitMS) { + // wait for magnetic north or south + lastWaitForState = 0; + unsigned long int startms = millis(); + ; + uint16_t sav; + do { + sav = readSensorValueAverage(true); + if (sav < nullFieldValue - HallEffectMinDiffValue) { + lastWaitForState = 1; + } else if (sav > nullFieldValue + HallEffectMinDiffValue) { + lastWaitForState = 2; + } else { + delay(50); + } + } while ((lastWaitForState == 0) && (millis() < (startms + waitMS))); + lastWaitForStateMS = millis() - startms; + Log_Printf(LOGLEVEL_INFO, "HallEffectSensor waitForState> fieldValue:%d => state:%d, (duration:%d ms)", sav, lastWaitForState, lastWaitForStateMS); + return lastWaitForState; }; -uint16_t HallEffectSensor::NullFieldValue(){ - return nullFieldValue; +uint16_t HallEffectSensor::NullFieldValue() { + return nullFieldValue; } -int HallEffectSensor::LastWaitForStateMS(){ - return lastWaitForStateMS; +int HallEffectSensor::LastWaitForStateMS() { + return lastWaitForStateMS; } -int8_t HallEffectSensor::LastWaitForState(){ - return lastWaitForState; +int8_t HallEffectSensor::LastWaitForState() { + return lastWaitForState; } #endif diff --git a/src/HallEffectSensor.h b/src/HallEffectSensor.h index fe15c3ec..bd5da2ca 100644 --- a/src/HallEffectSensor.h +++ b/src/HallEffectSensor.h @@ -1,38 +1,38 @@ #pragma once -#define HallEffectMinDiffValue 100 // I had a minimum change of 250, so reliable (factor 2.5) sensitive detection | Delta analog read max. +- 10 -#define HallEffectMaxDiffValueReCalibrate 50 // Auto ReCalibration after start, when no HockeyDualfunctionTag is near reader -#define HallEffectAverageProbesMS 40 // average wird HallEffectAverageProbesMS milliseconds ermittelt -#define HallEffectWaitMS 750 // max. definable Value: 65.535 (uint16_t) affects responsetime, when normal Tag (no HockeyMagneticTag) is used, 1000 ms is a good value for me +#define HallEffectMinDiffValue 100 // I had a minimum change of 250, so reliable (factor 2.5) sensitive detection | Delta analog read max. +- 10 +#define HallEffectMaxDiffValueReCalibrate 50 // Auto ReCalibration after start, when no HockeyDualfunctionTag is near reader +#define HallEffectAverageProbesMS 40 // average wird HallEffectAverageProbesMS milliseconds ermittelt +#define HallEffectWaitMS 750 // max. definable Value: 65.535 (uint16_t) affects responsetime, when normal Tag (no HockeyMagneticTag) is used, 1000 ms is a good value for me class HallEffectSensor { - public: - HallEffectSensor(); - void init(); - void cyclic(); - bool saveActualFieldValue2NVS(); - uint16_t NullFieldValue(); - int LastWaitForStateMS(); - int8_t LastWaitForState(); - uint16_t readSensorValueAverage(bool doLog); - uint8_t waitForState( uint16_t waitMS); // 0=no magneticTag, 1=negativ magField, 2=positive magField - private: - // calibration - static const uint16_t calibrationFctCallsPauseMS = 0; - static const uint16_t maxCalibrationFctCalls = 1; - unsigned long int lastCalibrationSubCallMS = 0; - int maxVal = 0; - int minVal = 9999; - int diffVal = 0; - int calibrationFctCalls = maxCalibrationFctCalls+1; - int calibrationProbes = 0; - // vars - int lastWaitForStateMS = -1; - int8_t lastWaitForState = -1; - uint16_t nullFieldValue; - bool SaveToNullFieldValueNVS( uint16_t val); - void LoadNullFieldValueFromNVS(); - void StartDebugging(); // debug only +public: + HallEffectSensor(); + void init(); + void cyclic(); + bool saveActualFieldValue2NVS(); + uint16_t NullFieldValue(); + int LastWaitForStateMS(); + int8_t LastWaitForState(); + uint16_t readSensorValueAverage(bool doLog); + uint8_t waitForState(uint16_t waitMS); // 0=no magneticTag, 1=negativ magField, 2=positive magField +private: + // calibration + static const uint16_t calibrationFctCallsPauseMS = 0; + static const uint16_t maxCalibrationFctCalls = 1; + unsigned long int lastCalibrationSubCallMS = 0; + int maxVal = 0; + int minVal = 9999; + int diffVal = 0; + int calibrationFctCalls = maxCalibrationFctCalls + 1; + int calibrationProbes = 0; + // vars + int lastWaitForStateMS = -1; + int8_t lastWaitForState = -1; + uint16_t nullFieldValue; + bool SaveToNullFieldValueNVS(uint16_t val); + void LoadNullFieldValueFromNVS(); + void StartDebugging(); // debug only }; extern HallEffectSensor gHallEffectSensor; diff --git a/src/IrReceiver.cpp b/src/IrReceiver.cpp index b5a2e0bb..2e50b1c3 100644 --- a/src/IrReceiver.cpp +++ b/src/IrReceiver.cpp @@ -1,8 +1,11 @@ #include #include "settings.h" + #include "IrReceiver.h" + #include "AudioPlayer.h" #include "Cmd.h" +#include "Log.h" #include "Queues.h" #include "System.h" @@ -12,127 +15,127 @@ // HW-Timer #ifdef IR_CONTROL_ENABLE - uint32_t IrReceiver_LastRcCmdTimestamp = 0u; +uint32_t IrReceiver_LastRcCmdTimestamp = 0u; #endif void IrReceiver_Init() { - #ifdef IR_CONTROL_ENABLE - IrReceiver.begin(IRLED_PIN); - #endif +#ifdef IR_CONTROL_ENABLE + IrReceiver.begin(IRLED_PIN); +#endif } void IrReceiver_Cyclic() { - #ifdef IR_CONTROL_ENABLE - static uint8_t lastVolume = 0; +#ifdef IR_CONTROL_ENABLE + static uint8_t lastVolume = 0; - if (IrReceiver.decode()){ + if (IrReceiver.decode()) { - // Print a short summary of received data - IrReceiver.printIRResultShort(&Serial); - Serial.println(); - IrReceiver.resume(); // Enable receiving of the next value - bool rcActionOk = false; - if (millis() - IrReceiver_LastRcCmdTimestamp >= IR_DEBOUNCE) { - rcActionOk = true; // not used for volume up/down - IrReceiver_LastRcCmdTimestamp = millis(); - } + // Print a short summary of received data + IrReceiver.printIRResultShort(&Serial); + Serial.println(); + IrReceiver.resume(); // Enable receiving of the next value + bool rcActionOk = false; + if (millis() - IrReceiver_LastRcCmdTimestamp >= IR_DEBOUNCE) { + rcActionOk = true; // not used for volume up/down + IrReceiver_LastRcCmdTimestamp = millis(); + } - switch (IrReceiver.decodedIRData.command) { - case RC_PLAY: { - if (rcActionOk) { - Cmd_Action(CMD_PLAYPAUSE); - Log_Println((char *) F("RC: Play"), LOGLEVEL_NOTICE); - } - break; + switch (IrReceiver.decodedIRData.command) { + case RC_PLAY: { + if (rcActionOk) { + Cmd_Action(CMD_PLAYPAUSE); + Log_Println("RC: Play", LOGLEVEL_NOTICE); } - case RC_PAUSE: { - if (rcActionOk) { - Cmd_Action(CMD_PLAYPAUSE); - Log_Println((char *) F("RC: Pause"), LOGLEVEL_NOTICE); - } - break; - } - case RC_NEXT: { - if (rcActionOk) { - Cmd_Action(CMD_NEXTTRACK); - Log_Println((char *) F("RC: Next"), LOGLEVEL_NOTICE); - } - break; - } - case RC_PREVIOUS: { - if (rcActionOk) { - Cmd_Action(CMD_PREVTRACK); - Log_Println((char *) F("RC: Previous"), LOGLEVEL_NOTICE); - } - break; + break; + } + case RC_PAUSE: { + if (rcActionOk) { + Cmd_Action(CMD_PLAYPAUSE); + Log_Println("RC: Pause", LOGLEVEL_NOTICE); } - case RC_FIRST: { - if (rcActionOk) { - Cmd_Action(CMD_FIRSTTRACK); - Log_Println((char *) F("RC: First"), LOGLEVEL_NOTICE); - } - break; + break; + } + case RC_NEXT: { + if (rcActionOk) { + Cmd_Action(CMD_NEXTTRACK); + Log_Println("RC: Next", LOGLEVEL_NOTICE); } - case RC_LAST: { - if (rcActionOk) { - Cmd_Action(CMD_LASTTRACK); - Log_Println((char *) F("RC: Last"), LOGLEVEL_NOTICE); - } - break; + break; + } + case RC_PREVIOUS: { + if (rcActionOk) { + Cmd_Action(CMD_PREVTRACK); + Log_Println("RC: Previous", LOGLEVEL_NOTICE); } - case RC_MUTE: { - if (rcActionOk) { - if (AudioPlayer_GetCurrentVolume() > 0) { - lastVolume = AudioPlayer_GetCurrentVolume(); - AudioPlayer_SetCurrentVolume(0u); - } else { - AudioPlayer_SetCurrentVolume(lastVolume); // Remember last volume if mute is pressed again - } - - uint8_t currentVolume = AudioPlayer_GetCurrentVolume(); - xQueueSend(gVolumeQueue, ¤tVolume, 0); - Log_Println((char *) F("RC: Mute"), LOGLEVEL_NOTICE); - } - break; + break; + } + case RC_FIRST: { + if (rcActionOk) { + Cmd_Action(CMD_FIRSTTRACK); + Log_Println("RC: First", LOGLEVEL_NOTICE); } - case RC_BLUETOOTH: { - if (rcActionOk) { - Cmd_Action(CMD_TOGGLE_BLUETOOTH_SINK_MODE); - Log_Println((char *) F("RC: Bluetooth sink"), LOGLEVEL_NOTICE); - } - break; + break; + } + case RC_LAST: { + if (rcActionOk) { + Cmd_Action(CMD_LASTTRACK); + Log_Println("RC: Last", LOGLEVEL_NOTICE); } - // +++ todo: bluetooth source mode +++ - case RC_FTP: { - if (rcActionOk) { - Cmd_Action(CMD_ENABLE_FTP_SERVER); - Log_Println((char *) F("RC: FTP"), LOGLEVEL_NOTICE); + break; + } + case RC_MUTE: { + if (rcActionOk) { + if (AudioPlayer_GetCurrentVolume() > 0) { + lastVolume = AudioPlayer_GetCurrentVolume(); + AudioPlayer_SetCurrentVolume(0u); + } else { + AudioPlayer_SetCurrentVolume(lastVolume); // Remember last volume if mute is pressed again } - break; + + uint8_t currentVolume = AudioPlayer_GetCurrentVolume(); + xQueueSend(gVolumeQueue, ¤tVolume, 0); + Log_Println("RC: Mute", LOGLEVEL_NOTICE); } - case RC_SHUTDOWN: { - if (rcActionOk) { - System_RequestSleep(); - Log_Println((char *) F("RC: Shutdown"), LOGLEVEL_NOTICE); - } - break; + break; + } + case RC_BLUETOOTH: { + if (rcActionOk) { + Cmd_Action(CMD_TOGGLE_BLUETOOTH_SINK_MODE); + Log_Println("RC: Bluetooth sink", LOGLEVEL_NOTICE); } - case RC_VOL_DOWN: { - Cmd_Action(CMD_VOLUMEDOWN); - Log_Println((char *) F("RC: Volume down"), LOGLEVEL_NOTICE); - break; + break; + } + // +++ todo: bluetooth source mode +++ + case RC_FTP: { + if (rcActionOk) { + Cmd_Action(CMD_ENABLE_FTP_SERVER); + Log_Println("RC: FTP", LOGLEVEL_NOTICE); } - case RC_VOL_UP: { - Cmd_Action(CMD_VOLUMEUP); - Log_Println((char *) F("RC: Volume up"), LOGLEVEL_NOTICE); - break; + break; + } + case RC_SHUTDOWN: { + if (rcActionOk) { + System_RequestSleep(); + Log_Println("RC: Shutdown", LOGLEVEL_NOTICE); } - default: { - if (rcActionOk) { - Log_Println((char *) F("RC: unknown"), LOGLEVEL_NOTICE); - } + break; + } + case RC_VOL_DOWN: { + Cmd_Action(CMD_VOLUMEDOWN); + Log_Println("RC: Volume down", LOGLEVEL_NOTICE); + break; + } + case RC_VOL_UP: { + Cmd_Action(CMD_VOLUMEUP); + Log_Println("RC: Volume up", LOGLEVEL_NOTICE); + break; + } + default: { + if (rcActionOk) { + Log_Println("RC: unknown", LOGLEVEL_NOTICE); } } } - #endif + } +#endif } diff --git a/src/Led.cpp b/src/Led.cpp index 993b663f..78c1ecc4 100644 --- a/src/Led.cpp +++ b/src/Led.cpp @@ -1,762 +1,1123 @@ #include -#include -#include #include "settings.h" + +#include "Led.h" + #include "AudioPlayer.h" #include "Battery.h" +#include "Bluetooth.h" #include "Button.h" -#include "Led.h" #include "Log.h" +#include "Port.h" #include "System.h" #include "Wlan.h" -#include "Bluetooth.h" -#include "Port.h" + +#include +#include #ifdef NEOPIXEL_ENABLE #include - #define LED_INITIAL_BRIGHTNESS 16u + #define LED_INITIAL_BRIGHTNESS 16u #define LED_INITIAL_NIGHT_BRIGHTNESS 2u - #define LED_INDICATOR_SET(indicator) ((Led_Indicators) |= (1u << ((uint8_t)indicator))) - #define LED_INDICATOR_IS_SET(indicator) (((Led_Indicators) & (1u << ((uint8_t)indicator))) > 0u) - #define LED_INDICATOR_CLEAR(indicator) ((Led_Indicators) &= ~(1u << ((uint8_t)indicator))) + #define LED_INDICATOR_SET(indicator) ((Led_Indicators) |= (1u << ((uint8_t) indicator))) + #define LED_INDICATOR_IS_SET(indicator) (((Led_Indicators) & (1u << ((uint8_t) indicator))) > 0u) + #define LED_INDICATOR_CLEAR(indicator) ((Led_Indicators) &= ~(1u << ((uint8_t) indicator))) #ifndef LED_OFFSET #define LED_OFFSET 0 - #elif LED_OFFSET < 0 || LED_OFFSET >= NUM_LEDS - #error LED_OFFSET must be between 0 and NUM_LEDS-1 + #elif LED_OFFSET < 0 || LED_OFFSET >= NUM_INDICATOR_LEDS + #error LED_OFFSET must be between 0 and NUM_INDICATOR_LEDS-1 #endif // Time in milliseconds the volume indicator is visible - #define LED_VOLUME_INDICATOR_RETURN_DELAY 1000U - #define LED_VOLUME_INDICATOR_NUM_CYCLES (LED_VOLUME_INDICATOR_RETURN_DELAY / 20) + #define LED_VOLUME_INDICATOR_RETURN_DELAY 1000U + #define LED_VOLUME_INDICATOR_NUM_CYCLES (LED_VOLUME_INDICATOR_RETURN_DELAY / 20) + +extern t_button gButtons[7]; // next + prev + pplay + rotEnc + button4 + button5 + dummy-button +extern uint8_t gShutdownButton; + +static uint32_t Led_Indicators = 0u; - extern t_button gButtons[7]; // next + prev + pplay + rotEnc + button4 + button5 + dummy-button - extern uint8_t gShutdownButton; +static bool Led_Pause = false; // Used to pause Neopixel-signalisation (while NVS-writes as this leads to exceptions; don't know why) - static uint32_t Led_Indicators = 0u; +static uint8_t Led_InitialBrightness = LED_INITIAL_BRIGHTNESS; +static uint8_t Led_Brightness = LED_INITIAL_BRIGHTNESS; +static uint8_t Led_NightBrightness = LED_INITIAL_NIGHT_BRIGHTNESS; +constexpr uint8_t Led_IdleDotDistance = NUM_INDICATOR_LEDS / NUM_LEDS_IDLE_DOTS; - static bool Led_Pause = false; // Used to pause Neopixel-signalisation (while NVS-writes as this leads to exceptions; don't know why) +static CRGBArray leds; +static CRGBSet indicator(leds(0, NUM_INDICATOR_LEDS - 1)); +static CRGBSet controlLeds(leds(NUM_INDICATOR_LEDS, NUM_INDICATOR_LEDS + NUM_CONTROL_LEDS - 1)); - static uint8_t Led_InitialBrightness = LED_INITIAL_BRIGHTNESS; - static uint8_t Led_Brightness = LED_INITIAL_BRIGHTNESS; - static uint8_t Led_NightBrightness = LED_INITIAL_NIGHT_BRIGHTNESS; +TaskHandle_t Led_TaskHandle; +static void Led_Task(void *parameter); +static uint8_t Led_Address(uint8_t number); - static void Led_Task(void *parameter); - static uint8_t Led_Address(uint8_t number); +// animation-functions prototypes +AnimationReturnType Animation_PlaylistProgress(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_BatteryMeasurement(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Volume(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Progress(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Boot(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Shutdown(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Error(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Ok(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_VoltageWarning(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Webstream(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Rewind(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Idle(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Busy(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Pause(const bool startNewAnimation, CRGBSet &leds); +AnimationReturnType Animation_Speech(const bool startNewAnimation, CRGBSet &leds); #endif void Led_Init(void) { - #ifdef NEOPIXEL_ENABLE - // Get some stuff from NVS... - // Get initial LED-brightness from NVS - uint8_t nvsILedBrightness = gPrefsSettings.getUChar("iLedBrightness", 0); - if (nvsILedBrightness) { - Led_InitialBrightness = nvsILedBrightness; - Led_Brightness = nvsILedBrightness; - snprintf(Log_Buffer, Log_BufferLength, "%s: %d", (char *) FPSTR(initialBrightnessfromNvs), nvsILedBrightness); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putUChar("iLedBrightness", Led_InitialBrightness); - Log_Println((char *) FPSTR(wroteInitialBrightnessToNvs), LOGLEVEL_ERROR); - } +#ifdef NEOPIXEL_ENABLE + // Get some stuff from NVS... + // Get initial LED-brightness from NVS + uint8_t nvsILedBrightness = gPrefsSettings.getUChar("iLedBrightness", 0); + if (nvsILedBrightness) { + Led_InitialBrightness = nvsILedBrightness; + Led_Brightness = nvsILedBrightness; + Log_Printf(LOGLEVEL_INFO, initialBrightnessfromNvs, nvsILedBrightness); + } else { + gPrefsSettings.putUChar("iLedBrightness", Led_InitialBrightness); + Log_Println(wroteInitialBrightnessToNvs, LOGLEVEL_ERROR); + } - // Get night LED-brightness from NVS - uint8_t nvsNLedBrightness = gPrefsSettings.getUChar("nLedBrightness", 255); - if (nvsNLedBrightness != 255) { - Led_NightBrightness = nvsNLedBrightness; - snprintf(Log_Buffer, Log_BufferLength, "%s: %d", (char *) FPSTR(restoredInitialBrightnessForNmFromNvs), nvsNLedBrightness); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - gPrefsSettings.putUChar("nLedBrightness", Led_NightBrightness); - Log_Println((char *) FPSTR(wroteNmBrightnessToNvs), LOGLEVEL_ERROR); - } - - xTaskCreatePinnedToCore( - Led_Task, /* Function to implement the task */ - "Led_Task", /* Name of the task */ - 1512, /* Stack size in words */ - NULL, /* Task input parameter */ - 1, /* Priority of the task */ - NULL, /* Task handle. */ - 1 /* Core where the task should run */ - ); - #endif + // Get night LED-brightness from NVS + uint8_t nvsNLedBrightness = gPrefsSettings.getUChar("nLedBrightness", 255); + if (nvsNLedBrightness != 255) { + Led_NightBrightness = nvsNLedBrightness; + Log_Printf(LOGLEVEL_INFO, restoredInitialBrightnessForNmFromNvs, nvsNLedBrightness); + } else { + gPrefsSettings.putUChar("nLedBrightness", Led_NightBrightness); + Log_Println(wroteNmBrightnessToNvs, LOGLEVEL_ERROR); + } + + xTaskCreatePinnedToCore( + Led_Task, /* Function to implement the task */ + "Led_Task", /* Name of the task */ + 1512, /* Stack size in words */ + NULL, /* Task input parameter */ + 1, /* Priority of the task */ + &Led_TaskHandle, /* Task handle. */ + 1 /* Core where the task should run */ + ); +#endif } void Led_Exit(void) { - #ifdef NEOPIXEL_ENABLE - FastLED.clear(); - FastLED.show(); - #endif +#ifdef NEOPIXEL_ENABLE + Log_Println("shutdown LED..", LOGLEVEL_NOTICE); + if (Led_TaskHandle) { + vTaskDelete(Led_TaskHandle); + Led_TaskHandle = NULL; + } + // Turn off LEDs in order to avoid LEDs still glowing when ESP32 is in deepsleep + FastLED.clear(true); +#endif } void Led_Indicate(LedIndicatorType value) { - #ifdef NEOPIXEL_ENABLE - LED_INDICATOR_SET(value); - #endif +#ifdef NEOPIXEL_ENABLE + LED_INDICATOR_SET(value); +#endif } void Led_SetPause(boolean value) { - #ifdef NEOPIXEL_ENABLE - Led_Pause = value; - #endif +#ifdef NEOPIXEL_ENABLE + Led_Pause = value; +#endif } // Used to reset brightness to initial value after prevously active sleepmode was left void Led_ResetToInitialBrightness(void) { - #ifdef NEOPIXEL_ENABLE - if (Led_Brightness == Led_NightBrightness || Led_Brightness == 0) { // Only reset to initial value if brightness wasn't intentionally changed (or was zero) - Led_Brightness = Led_InitialBrightness; - Log_Println((char *) FPSTR(ledsDimmedToInitialValue), LOGLEVEL_INFO); - } - #endif - #ifdef BUTTONS_LED - Port_Write(BUTTONS_LED, HIGH, false); - #endif +#ifdef NEOPIXEL_ENABLE + if (Led_Brightness == Led_NightBrightness || Led_Brightness == 0) { // Only reset to initial value if brightness wasn't intentionally changed (or was zero) + Led_Brightness = Led_InitialBrightness; + Log_Println(ledsDimmedToInitialValue, LOGLEVEL_INFO); + } +#endif +#ifdef BUTTONS_LED + Port_Write(BUTTONS_LED, HIGH, false); +#endif } void Led_ResetToNightBrightness(void) { - #ifdef NEOPIXEL_ENABLE - Led_Brightness = Led_NightBrightness; - Log_Println((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); - #endif - #ifdef BUTTONS_LED - Port_Write(BUTTONS_LED, LOW, false); - #endif +#ifdef NEOPIXEL_ENABLE + Led_Brightness = Led_NightBrightness; + Log_Println(ledsDimmedToNightmode, LOGLEVEL_INFO); +#endif +#ifdef BUTTONS_LED + Port_Write(BUTTONS_LED, LOW, false); +#endif } uint8_t Led_GetBrightness(void) { - #ifdef NEOPIXEL_ENABLE - return Led_Brightness; - #else - return 0u; - #endif +#ifdef NEOPIXEL_ENABLE + return Led_Brightness; +#else + return 0u; +#endif } void Led_SetBrightness(uint8_t value) { - #ifdef NEOPIXEL_ENABLE - Led_Brightness = value; - #ifdef BUTTONS_LED - Port_Write(BUTTONS_LED, value <= Led_NightBrightness ? LOW : HIGH, false); - #endif +#ifdef NEOPIXEL_ENABLE + Led_Brightness = value; + #ifdef BUTTONS_LED + Port_Write(BUTTONS_LED, value <= Led_NightBrightness ? LOW : HIGH, false); #endif +#endif } // Calculates physical address for a virtual LED address. This handles reversing the rotation direction of the ring and shifting the starting LED +#ifdef NEOPIXEL_ENABLE uint8_t Led_Address(uint8_t number) { #ifdef NEOPIXEL_REVERSE_ROTATION #if LED_OFFSET > 0 - return number <= LED_OFFSET - 1 ? LED_OFFSET - 1 - number : NUM_LEDS + LED_OFFSET - 1 - number; + return number <= LED_OFFSET - 1 ? LED_OFFSET - 1 - number : NUM_INDICATOR_LEDS + LED_OFFSET - 1 - number; #else - return NUM_LEDS - 1 - number; + return NUM_INDICATOR_LEDS - 1 - number; #endif #else #if LED_OFFSET > 0 - return number >= NUM_LEDS - LED_OFFSET ? number + LED_OFFSET - NUM_LEDS : number + LED_OFFSET; + return number >= NUM_INDICATOR_LEDS - LED_OFFSET ? number + LED_OFFSET - NUM_INDICATOR_LEDS : number + LED_OFFSET; #else - return number; + return number; #endif #endif } +#endif -void Led_SetButtonLedsEnabled(boolean value) { - #ifdef BUTTONS_LED - Port_Write(BUTTONS_LED, value ? HIGH : LOW, false); +#ifdef NEOPIXEL_ENABLE +void Led_DrawControls() { + #if NUM_CONTROL_LEDS > 0 + static CRGB::HTMLColorCode controlLedColors[NUM_CONTROL_LEDS] = CONTROL_LEDS_COLORS; + for (uint8_t controlLed = 0; controlLed < NUM_CONTROL_LEDS; controlLed++) { + controlLeds[controlLed] = controlLedColors[controlLed]; + } #endif } +#endif -bool Led_NeedsProgressRedraw(bool lastPlayState, bool lastLockState, - bool requestProgressRedraw) { -#ifdef BATTERY_MEASURE_ENABLE - if (gPlayProperties.pausePlay != lastPlayState || - System_AreControlsLocked() != lastLockState || - requestProgressRedraw || - LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning) || - LED_INDICATOR_IS_SET(LedIndicatorType::Voltage) || - !gButtons[gShutdownButton].currentState || - System_IsSleepRequested()) { -#else - if (gPlayProperties.pausePlay != lastPlayState || - System_AreControlsLocked() != lastLockState || - requestProgressRedraw || - !gButtons[gShutdownButton].currentState || - System_IsSleepRequested()) { +void Led_SetButtonLedsEnabled(boolean value) { +#ifdef BUTTONS_LED + Port_Write(BUTTONS_LED, value ? HIGH : LOW, false); #endif - return true; +} + +#ifdef NEOPIXEL_ENABLE +CRGB Led_DimColor(CRGB color, uint8_t brightness) { + const uint8_t factor = uint16_t(brightness * __UINT8_MAX__) / DIMMABLE_STATES; + return color.nscale8(factor); +} +CRGB::HTMLColorCode Led_GetIdleColor() { + CRGB::HTMLColorCode idleColor = CRGB::Black; + if ((OPMODE_BLUETOOTH_SINK == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) { + if (Bluetooth_Device_Connected()) { + idleColor = CRGB::Blue; + } else { + idleColor = CRGB::BlueViolet; + } + } else { + if (Wlan_ConnectionTryInProgress()) { + idleColor = CRGB::Orange; + } else { + idleColor = CRGB::Green; + if (Wlan_IsConnected()) { + idleColor = CRGB::White; + } + } } + return idleColor; +} +void Led_DrawIdleDots(CRGBSet &leds, uint8_t offset, CRGB::HTMLColorCode color) { + for (uint8_t i = 0; i < NUM_LEDS_IDLE_DOTS; i++) { + leds[(Led_Address(offset) + i * Led_IdleDotDistance) % leds.size()] = color; + } +} + +bool CheckForPowerButtonAnimation() { + if (gShutdownButton < (sizeof(gButtons) / sizeof(gButtons[0])) - 1) { // Only show animation, if CMD_SLEEPMODE was assigned to BUTTON_n_LONG + button is pressed + if (gButtons[gShutdownButton].isPressed && (millis() - gButtons[gShutdownButton].firstPressedTimestamp >= 150) && gButtonInitComplete) { + return true; + } + } return false; } +#endif +#ifdef NEOPIXEL_ENABLE static void Led_Task(void *parameter) { - #ifdef NEOPIXEL_ENABLE - static uint8_t hlastVolume = AudioPlayer_GetCurrentVolume(); - static double lastPos = gPlayProperties.currentRelPos; - static bool lastPlayState = false; - static bool lastLockState = false; - static bool requestClearLeds = false; - static bool requestProgressRedraw = false; - static bool showEvenError = false; - static bool turnedOffLeds = false; - static bool singleLedStatus = false; - static uint8_t ledPosWebstream = 0; - static uint8_t ledSwitchInterval = 5; // time in secs (webstream-only) - static uint8_t webstreamColor = 0; - static unsigned long lastSwitchTimestamp = 0; - static bool redrawProgress = false; - static uint8_t lastLedBrightness = Led_Brightness; - static CRGB::HTMLColorCode idleColor; - static CRGB::HTMLColorCode speechColor = CRGB::Yellow; - static CRGB::HTMLColorCode generalColor; - static CRGB leds[NUM_LEDS]; - FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalSMD5050); - FastLED.setBrightness(Led_Brightness); - - for (;;) { - if (Led_Pause) { // Workaround to prevent exceptions while NVS-writes take place - vTaskDelay(portTICK_RATE_MS * 10); - continue; - } - if (System_IsSleepRequested()) { // If deepsleep is planned, turn off LEDs first in order to avoid LEDs still glowing when ESP32 is in deepsleep - if (!turnedOffLeds) { - FastLED.clear(true); - turnedOffLeds = true; - } + static uint8_t lastLedBrightness = Led_Brightness; + FastLED.addLeds(leds, leds.size()).setCorrection(TypicalSMD5050); + FastLED.setBrightness(Led_Brightness); + FastLED.setDither(DISABLE_DITHER); + + LedAnimationType activeAnimation = LedAnimationType::NoNewAnimation; + LedAnimationType nextAnimation = LedAnimationType::NoNewAnimation; + bool animationActive = false; + int32_t animationTimer = 0; + + for (;;) { + // special handling + if (Led_Pause) { // Workaround to prevent exceptions while NVS-writes take place + vTaskDelay(portTICK_PERIOD_MS * 10); + continue; + } + + Led_DrawControls(); + + uint32_t taskDelay = 20; + bool startNewAnimation = false; - vTaskDelay(portTICK_RATE_MS * 10); - continue; + // check indications and set led-mode + // this mode will then be animated if the priority and the current animation state fit + if (!LED_INDICATOR_IS_SET(LedIndicatorType::BootComplete)) { + nextAnimation = LedAnimationType::Boot; + } else if (CheckForPowerButtonAnimation()) { + nextAnimation = LedAnimationType::Shutdown; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::Error)) { + LED_INDICATOR_CLEAR(LedIndicatorType::Error); + nextAnimation = LedAnimationType::Error; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::Ok)) { + LED_INDICATOR_CLEAR(LedIndicatorType::Ok); + nextAnimation = LedAnimationType::Ok; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning)) { + LED_INDICATOR_CLEAR(LedIndicatorType::VoltageWarning); + nextAnimation = LedAnimationType::VoltageWarning; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::Voltage)) { + nextAnimation = LedAnimationType::BatteryMeasurement; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::VolumeChange)) { + nextAnimation = LedAnimationType::Volume; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::Rewind)) { + LED_INDICATOR_CLEAR(LedIndicatorType::Rewind); + nextAnimation = LedAnimationType::Rewind; + } else if (LED_INDICATOR_IS_SET(LedIndicatorType::PlaylistProgress)) { + nextAnimation = LedAnimationType::Playlist; + } else if (gPlayProperties.currentSpeechActive) { + nextAnimation = LedAnimationType::Speech; + } else if (gPlayProperties.playlistFinished) { + nextAnimation = LedAnimationType::Idle; + } else if (gPlayProperties.pausePlay && !gPlayProperties.isWebstream) { + nextAnimation = LedAnimationType::Pause; + } else if (gPlayProperties.isWebstream) { // also animate pause in the webstream animation + nextAnimation = LedAnimationType::Webstream; + } else if ((gPlayProperties.playMode != BUSY) && (gPlayProperties.playMode != NO_PLAYLIST)) { + nextAnimation = LedAnimationType::Progress; + } else if (gPlayProperties.playMode == NO_PLAYLIST) { + nextAnimation = LedAnimationType::Idle; + } else if (gPlayProperties.playMode == BUSY) { + nextAnimation = LedAnimationType::Busy; + } else { + nextAnimation = LedAnimationType::NoNewAnimation; // should not happen + } + + // check for instant transition if the requested animation has a higher priority then the current one + if (nextAnimation < activeAnimation) { + animationActive = false; // abort current animation + animationTimer = 0; + } + // transition to new animation + if ((!animationActive) && (animationTimer <= 0)) { + activeAnimation = nextAnimation; // set new animation + startNewAnimation = true; + } + + // apply brightness-changes + if (lastLedBrightness != Led_Brightness) { + FastLED.setBrightness(Led_Brightness); + lastLedBrightness = Led_Brightness; + } + + // when there is no delay anymore we have to animate something + if (animationTimer <= 0) { + AnimationReturnType ret; + // animate the current animation + switch (activeAnimation) { + case LedAnimationType::Boot: + ret = Animation_Boot(startNewAnimation, indicator); + break; + + case LedAnimationType::Shutdown: + ret = Animation_Shutdown(startNewAnimation, indicator); + break; + + case LedAnimationType::Error: + ret = Animation_Error(startNewAnimation, indicator); + break; + + case LedAnimationType::Ok: + ret = Animation_Ok(startNewAnimation, indicator); + break; + + case LedAnimationType::Volume: + ret = Animation_Volume(startNewAnimation, indicator); + break; + + case LedAnimationType::VoltageWarning: + ret = Animation_VoltageWarning(startNewAnimation, indicator); + break; + + case LedAnimationType::BatteryMeasurement: + ret = Animation_BatteryMeasurement(startNewAnimation, indicator); + break; + + case LedAnimationType::Rewind: + ret = Animation_Rewind(startNewAnimation, indicator); + break; + + case LedAnimationType::Playlist: + ret = Animation_PlaylistProgress(startNewAnimation, indicator); + break; + + case LedAnimationType::Idle: + ret = Animation_Idle(startNewAnimation, indicator); + break; + + case LedAnimationType::Busy: + ret = Animation_Busy(startNewAnimation, indicator); + break; + + case LedAnimationType::Speech: + ret = Animation_Speech(startNewAnimation, indicator); + break; + + case LedAnimationType::Pause: + ret = Animation_Pause(startNewAnimation, indicator); + break; + + case LedAnimationType::Progress: + ret = Animation_Progress(startNewAnimation, indicator); + break; + + case LedAnimationType::Webstream: + ret = Animation_Webstream(startNewAnimation, indicator); + break; + + default: + indicator = CRGB::Black; + FastLED.show(); + ret.animationActive = false; + ret.animationDelay = 50; + break; } - // Multi-LED: rotates orange unless boot isn't complete - // Single-LED: blinking orange - if (!LED_INDICATOR_IS_SET(LedIndicatorType::BootComplete)) { - FastLED.clear(); - for (uint8_t led = 0; led < NUM_LEDS; led++) { - if (showEvenError) { - if (Led_Address(led) % 2 == 0) { - if (millis() <= 10000) { - leds[Led_Address(led)] = CRGB::Orange; - } else { - leds[Led_Address(led)] = CRGB::Red; - } - } - } else { - if (millis() >= 10000) { // Flashes red after 10s (will remain forever if SD cannot be mounted) - leds[Led_Address(led)] = CRGB::Red; - } else { - if (Led_Address(led) % 2 == 1) { - leds[Led_Address(led)] = CRGB::Orange; - } - } - } - } + // apply delay and state from animation + animationActive = ret.animationActive; + animationTimer = ret.animationDelay; + if (ret.animationRefresh) { FastLED.show(); - showEvenError = !showEvenError; - vTaskDelay(portTICK_RATE_MS * 500); - continue; } + } - if (lastLedBrightness != Led_Brightness) { - FastLED.setBrightness(Led_Brightness); - lastLedBrightness = Led_Brightness; - } + // get the time to wait and delay the task + if ((animationTimer > 0) && (animationTimer < taskDelay)) { + taskDelay = animationTimer; + } + animationTimer -= taskDelay; + vTaskDelay(portTICK_PERIOD_MS * taskDelay); + } + vTaskDelete(NULL); +} +#endif - // Multi-LED: growing red as long button for sleepmode is pressed. - // Single-LED: red when pressed and flashing red when long interval-duration is reached - if (gShutdownButton < (sizeof(gButtons) / sizeof(gButtons[0])) - 1) { // Only show animation, if CMD_SLEEPMODE was assigned to BUTTON_n_LONG + button is pressed - //snprintf(Log_Buffer, Log_BufferLength, "%u", uxTaskGetStackHighWaterMark(NULL)); - //Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - if (!gButtons[gShutdownButton].currentState && (millis() - gButtons[gShutdownButton].firstPressedTimestamp >= 150) && gButtonInitComplete) { - if (NUM_LEDS == 1) { - FastLED.clear(); - if (millis() - gButtons[gShutdownButton].firstPressedTimestamp <= intervalToLongPress) { - leds[0] = CRGB::Red; - FastLED.show(); - } else { - if (singleLedStatus) { - leds[0] = CRGB::Red; - } else { - leds[0] = CRGB::Black; - } - FastLED.show(); - singleLedStatus = !singleLedStatus; - vTaskDelay(portTICK_RATE_MS * 50); - } - } else { - if (millis() - gButtons[gShutdownButton].firstPressedTimestamp >= intervalToLongPress) { - vTaskDelay(portTICK_RATE_MS * 50); - continue; - } - FastLED.clear(); - for (uint8_t led = 0; led < NUM_LEDS; led++) { - leds[Led_Address(led)] = CRGB::Red; - if (gButtons[gShutdownButton].currentState) { - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 5); - break; - } - FastLED.show(); - vTaskDelay(intervalToLongPress / NUM_LEDS * portTICK_RATE_MS); - } - } - } - } else { - gShutdownButton = (sizeof(gButtons) / sizeof(gButtons[0])) - 1; // If CMD_SLEEPMODE was not assigned to an enabled button, dummy-button is used - if (!gButtons[gShutdownButton].currentState) { - gButtons[gShutdownButton].currentState = true; - } +#ifdef NEOPIXEL_ENABLE +// --------------------------------------------------------------------- +// --------------- ANIMATION-METHODS --------------------- +// --------------------------------------------------------------------- +// * each function has to be non-blocking. +// * all states are kept in this function. +// * all funcitons return the desired delay and if they are still active +// * the function will be called the next time when the returned delay has +// passed +// * the optional Start Flag signals that the animation is started new + +// -------------------------------- +// BOOT-UP Animation +// -------------------------------- +AnimationReturnType Animation_Boot(const bool startNewAnimation, CRGBSet &leds) { + (void) startNewAnimation; // start is not used + // static vars + static bool showEvenError = false; + + // 10 s without success? + if (millis() > 10000) { + leds = CRGB::Red; + if (showEvenError) { + // and then draw in the black dots + for (uint8_t i = 0; i < leds.size(); i += 2) { + leds[i] = CRGB::Black; } + } + } else { + leds = CRGB::Black; + const uint8_t startLed = (showEvenError) ? 1 : 0; + for (uint8_t i = startLed; i < leds.size(); i += 2) { + leds[i] = CRGB::Orange; + } + } + showEvenError = !showEvenError; - // Multi-LED: all leds flash red 1x - // Single-LED: led flashes red 5x - if (LED_INDICATOR_IS_SET(LedIndicatorType::Error)) { // If error occured (e.g. RFID-modification not accepted) - LED_INDICATOR_CLEAR(LedIndicatorType::Error); - requestProgressRedraw = true; - FastLED.clear(); - - if (NUM_LEDS == 1) { - for (uint8_t cnt = 0; cnt < 5; cnt++) { - FastLED.clear(); - if (singleLedStatus) { - leds[0] = CRGB::Red; - } else { - leds[0] = CRGB::Black; - } - FastLED.show(); - singleLedStatus = !singleLedStatus; - vTaskDelay(portTICK_RATE_MS * 100); - } - } else { - for (uint8_t led = 0; led < NUM_LEDS; led++) { - leds[Led_Address(led)] = CRGB::Red; - } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 200); + return AnimationReturnType(false, 500, true); // always wait 500 ms +} + +// -------------------------------- +// Shutdown Animation +// -------------------------------- +AnimationReturnType Animation_Shutdown(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = true; + int32_t animationDelay = 0; + // static vars + static bool singleLedStatus = false; + static uint32_t animationIndex = 0; + if (startNewAnimation) { + animationIndex = 0; + } + + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds = CRGB::Black; + if (millis() - gButtons[gShutdownButton].firstPressedTimestamp <= intervalToLongPress) { + leds[0] = CRGB::Red; + animationDelay = 5; + } else { + if (singleLedStatus) { + leds[0] = CRGB::Red; + } + singleLedStatus = !singleLedStatus; + animationDelay = 50; + } + animationActive = false; + } else { + if ((millis() - gButtons[gShutdownButton].firstPressedTimestamp >= intervalToLongPress) && (animationIndex >= leds.size())) { + animationDelay = 50; + if (!gButtons[gShutdownButton].isPressed) { + // increase animation index to bail out, if we had a kombi-button + animationIndex++; + if (animationIndex >= leds.size() + 3) { + animationActive = false; // this is approx. 150ms after the button is released } } - - // Multi-LED: all leds flash green 1x - // Single-LED: led flashes green 5x - if (LED_INDICATOR_IS_SET(LedIndicatorType::Ok)) { // If action was accepted - LED_INDICATOR_CLEAR(LedIndicatorType::Ok); - requestProgressRedraw = true; - FastLED.clear(); - - if (NUM_LEDS == 1) { - for (uint8_t cnt = 0; cnt < 5; cnt++) { - FastLED.clear(); - if (singleLedStatus) { - leds[0] = CRGB::Green; - } else { - leds[0] = CRGB::Black; - } - FastLED.show(); - singleLedStatus = !singleLedStatus; - vTaskDelay(portTICK_RATE_MS * 100); - } + } else { + if (animationIndex == 0) { + leds = CRGB::Black; + } + if (animationIndex < leds.size()) { + leds[Led_Address(animationIndex)] = CRGB::Red; + if (gButtons[gShutdownButton].currentState) { + animationDelay = 5; + animationActive = false; } else { - for (uint8_t led = 0; led < NUM_LEDS; led++) { - leds[Led_Address(led)] = CRGB::Green; - } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 400); + animationDelay = intervalToLongPress / leds.size(); } + animationIndex++; } + } + } + return AnimationReturnType(animationActive, animationDelay, true); +} - #ifdef BATTERY_MEASURE_ENABLE - // Single + Multiple LEDs: flashes red three times if battery-voltage is low - if (LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning)) { - LED_INDICATOR_CLEAR(LedIndicatorType::VoltageWarning); - requestProgressRedraw = true; - for (uint8_t i = 0; i < 3; i++) { - FastLED.clear(); +// -------------------------------- +// Error Animation +// -------------------------------- +AnimationReturnType Animation_Error(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = true; + uint16_t animationDelay = 0; + // static vars + static bool singleLedStatus = false; + static uint16_t animationIndex = 0; + if (startNewAnimation) { + animationIndex = 0; + } - for (uint8_t led = 0; led < NUM_LEDS; led++) { - leds[Led_Address(led)] = CRGB::Red; - } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 200); - FastLED.clear(); + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds = CRGB::Black; + if (singleLedStatus) { + leds[0] = CRGB::Red; + } + singleLedStatus = !singleLedStatus; - for (uint8_t led = 0; led < NUM_LEDS; led++) { - leds[Led_Address(led)] = CRGB::Black; - } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 200); - } - } + if (animationIndex < 5) { + animationIndex++; + animationDelay = 100; + } else { + animationActive = false; + } + } else { + leds = CRGB::Red; + if (animationIndex > 0) { + animationActive = false; + } else { + animationIndex++; + animationDelay = 200; + } + } + return AnimationReturnType(animationActive, animationDelay, true); +} +// -------------------------------- +// OK Animation +// -------------------------------- +AnimationReturnType Animation_Ok(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = true; + uint16_t animationDelay = 0; + // static vars + static bool singleLedStatus = false; + static uint16_t animationIndex = 0; + if (startNewAnimation) { + animationIndex = 0; + } - // Single-LED: indicates voltage coloured between gradient green (high) => red (low) - // Multi-LED: number of LEDs indicates voltage-level with having green >= 60% ; orange < 60% + >= 30% ; red < 30% - if (LED_INDICATOR_IS_SET(LedIndicatorType::Voltage)) { - LED_INDICATOR_CLEAR(LedIndicatorType::Voltage); - float batteryLevel = Battery_EstimateLevel(); + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds = CRGB::Black; + if (singleLedStatus) { + leds[0] = CRGB::Green; + } + singleLedStatus = !singleLedStatus; - if (batteryLevel < 0) { // If voltage is too low or no battery is connected - LED_INDICATOR_SET(LedIndicatorType::Error); - break; - } else { - FastLED.clear(); - if (NUM_LEDS == 1) { - if (batteryLevel < 0.3) { - leds[0] = CRGB::Red; - } else if (batteryLevel < 0.6) { - leds[0] = CRGB::Orange; - } else { - leds[0] = CRGB::Green; - } - FastLED.show(); - } else { - uint8_t numLedsToLight = batteryLevel * NUM_LEDS; - if (numLedsToLight > NUM_LEDS) { // Can happen e.g. if no battery is connected - numLedsToLight = NUM_LEDS; - } - for (uint8_t led = 0; led < numLedsToLight; led++) { - if (batteryLevel < 0.3) { - leds[Led_Address(led)] = CRGB::Red; - } else if (batteryLevel < 0.6) { - leds[Led_Address(led)] = CRGB::Orange; - } else { - leds[Led_Address(led)] = CRGB::Green; - } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 20); - } - FastLED.show(); - } + if (animationIndex < 5) { + animationIndex++; + animationDelay = 100; + } else { + animationActive = false; + } + } else { + leds = CRGB::Green; + if (animationIndex > 0) { + animationActive = false; + } else { + animationIndex++; + animationDelay = 400; + } + } + return AnimationReturnType(animationActive, animationDelay, true); +} - for (uint8_t i = 0; i <= 100; i++) { - if (hlastVolume != AudioPlayer_GetCurrentVolume() || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - break; - } +// -------------------------------- +// VoltageWarning Animation +// -------------------------------- +// Single + Multiple LEDs: flashes red three times if battery-voltage is low +AnimationReturnType Animation_VoltageWarning(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = true; + uint16_t animationDelay = 0; + // static vars + static uint16_t animationIndex = 0; - vTaskDelay(portTICK_RATE_MS * 20); - } - } - } - #endif + if (startNewAnimation) { + animationIndex = 0; + } - /* - * - Single-LED: led indicates loudness between green (low) => red (high) - * - Multiple-LEDs: number of LEDs indicate loudness; gradient is shown between - * green (low) => red (high) - */ - if (hlastVolume != AudioPlayer_GetCurrentVolume()) { // If volume has been changed - uint8_t timeout = LED_VOLUME_INDICATOR_NUM_CYCLES; - // wait for further volume changes within next 20ms for 50 cycles = 1s - while (timeout--) { - uint8_t numLedsToLight = map(AudioPlayer_GetCurrentVolume(), 0, - AudioPlayer_GetMaxVolume(), 0, - NUM_LEDS); - - // Reset timeout when volume has changed - if (hlastVolume != AudioPlayer_GetCurrentVolume()) - timeout = LED_VOLUME_INDICATOR_NUM_CYCLES; - - hlastVolume = AudioPlayer_GetCurrentVolume(); - FastLED.clear(); - - if (NUM_LEDS == 1) { - uint8_t hue = 85 - (90 * - ((double)AudioPlayer_GetCurrentVolume() / - (double)AudioPlayer_GetMaxVolumeSpeaker())); - leds[0].setHue(hue); + leds = CRGB::Black; + if (animationIndex % 2 == 0) { + leds = CRGB::Red; + } + + if (animationIndex < 6) { + animationIndex++; + animationDelay = 200; + } else { + animationActive = false; + } + return AnimationReturnType(animationActive, animationDelay, true); +} + +// -------------------------------- +// Webstream Animation +// -------------------------------- +// Animates the progress and Pause of a Webstream +AnimationReturnType Animation_Webstream(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = false; + bool refresh = false; + int32_t animationDelay = 0; + // static vars + static uint8_t ledPosWebstream = 0; + static uint8_t webstreamColor = 0; + static uint16_t timerProgress = 0; + + // pause-animation + if (gPlayProperties.pausePlay) { + leds = CRGB::Black; + CRGB::HTMLColorCode generalColor = CRGB::Orange; + if (OPMODE_BLUETOOTH_SINK == System_GetOperationMode()) { + generalColor = CRGB::Blue; + } + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds[0] = generalColor; + } else { + leds[Led_Address(ledPosWebstream)] = generalColor; + leds[(Led_Address(ledPosWebstream) + leds.size() / 2) % leds.size()] = generalColor; + } + animationDelay = 10; + timerProgress = 0; // directly show new animation after pause + refresh = true; + } else { + if (startNewAnimation || timerProgress == 0) { + leds = CRGB::Black; + timerProgress = 100; + if (ledPosWebstream + 1 < leds.size()) { + ledPosWebstream++; + } else { + ledPosWebstream = 0; + } + if (System_AreControlsLocked()) { + leds[Led_Address(ledPosWebstream)] = CRGB::Red; + if constexpr (NUM_INDICATOR_LEDS > 1) { + leds[(Led_Address(ledPosWebstream) + leds.size() / 2) % leds.size()] = CRGB::Red; + } + } else { + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds[0].setHue(webstreamColor++); } else { - /* - * (Inverse) color-gradient from green (85) back to (still) - * red (250) using unsigned-cast. - */ - for (int led = 0; led < numLedsToLight; led++) { - uint8_t hue = (-86.0f) / (NUM_LEDS-1) * led + 85.0f; - leds[Led_Address(led)].setHue(hue); + if (OPMODE_BLUETOOTH_SINK == System_GetOperationMode()) { + leds[Led_Address(ledPosWebstream)] = CRGB::Blue; + leds[(Led_Address(ledPosWebstream) + leds.size() / 2) % leds.size()] = CRGB::Blue; + } else { + leds[Led_Address(ledPosWebstream)].setHue(webstreamColor); + leds[(Led_Address(ledPosWebstream) + leds.size() / 2) % leds.size()].setHue(webstreamColor++); } } + } + refresh = true; + } + timerProgress--; + animationDelay = 45; + if (timerProgress > 0) { + animationActive = true; + } + } + return AnimationReturnType(animationActive, animationDelay, refresh); +} - FastLED.show(); +// -------------------------------- +// Rewind Animation +// -------------------------------- +AnimationReturnType Animation_Rewind(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = false; + bool refresh = false; + int32_t animationDelay = 0; + // static vars + static uint16_t animationIndex = 0; + if (startNewAnimation) { + animationIndex = 0; + } - // abort volume change indication if necessary - if (LED_INDICATOR_IS_SET(LedIndicatorType::Error) || - LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || - !gButtons[gShutdownButton].currentState || - System_IsSleepRequested()) { - break; - } + if constexpr (NUM_INDICATOR_LEDS >= 4) { + animationActive = true; - vTaskDelay(portTICK_RATE_MS * 20); - } + if (animationIndex < (leds.size())) { + leds[Led_Address(leds.size() - 1 - animationIndex)] = CRGB::Black; + refresh = true; + animationDelay = 30; + animationIndex++; + } else { + animationActive = false; + } + } + return AnimationReturnType(animationActive, animationDelay, refresh); +} - requestProgressRedraw = true; +// -------------------------------- +// Idle Animation +// -------------------------------- +AnimationReturnType Animation_Idle(const bool startNewAnimation, CRGBSet &leds) { + // return values + int32_t animationDelay = 0; + bool animationActive = true; + bool refresh = true; + // static vars + static int16_t ledIndex = 0; + // this can be removed to always continue on the last position in idle + if (startNewAnimation) { + ledIndex = 0; + } + + if (ledIndex < leds.size()) { + CRGB::HTMLColorCode idleColor = Led_GetIdleColor(); + leds = CRGB::Black; + Led_DrawIdleDots(leds, ledIndex, idleColor); + if (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode()) { + // animate a bit faster in BT-Source to distinguish between the bluetooth modes + animationDelay = 30 * 10; + } else { + animationDelay = 50 * 10; } + ledIndex++; + } else { + animationActive = false; + refresh = false; + ledIndex = 0; + } + return AnimationReturnType(animationActive, animationDelay, refresh); +} - // < 4 LEDs: doesn't make sense at all - // >= 4 LEDs: collapsing ring (blue => black) - if (LED_INDICATOR_IS_SET(LedIndicatorType::Rewind)) { - LED_INDICATOR_CLEAR(LedIndicatorType::Rewind); - if (NUM_LEDS >= 4) { - for (uint8_t i = NUM_LEDS - 1; i > 0; i--) { - leds[Led_Address(i)] = CRGB::Black; - FastLED.show(); - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - break; - } else { - vTaskDelay(portTICK_RATE_MS * 30); - } - } - } +// -------------------------------- +// Busy Animation +// -------------------------------- +AnimationReturnType Animation_Busy(const bool startNewAnimation, CRGBSet &leds) { + // return values + bool animationActive = true; + int32_t animationDelay = 0; + // static vars + static bool singleLedStatus = false; + static uint16_t animationIndex = 0; + if (startNewAnimation) { + animationIndex = 0; + } + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds = CRGB::Black; + singleLedStatus = !singleLedStatus; + if (singleLedStatus) { + leds[0] = CRGB::BlueViolet; } + animationDelay = 100; + animationActive = false; + } else { + if (animationIndex < leds.size()) { + leds = CRGB::Black; + CRGB::HTMLColorCode idleColor = Led_GetIdleColor(); + Led_DrawIdleDots(leds, animationIndex, idleColor); + animationDelay = 50; + animationIndex++; + } else { + animationActive = false; + } + } + return AnimationReturnType(animationActive, animationDelay, true); +} - // < 4 LEDs: doesn't make sense at all - // >= 4 LEDs: growing ring (black => blue); relative number of LEDs indicate playlist-progress - if (LED_INDICATOR_IS_SET(LedIndicatorType::PlaylistProgress)) { - LED_INDICATOR_CLEAR(LedIndicatorType::PlaylistProgress); - if (NUM_LEDS >= 4) { - if (gPlayProperties.numberOfTracks > 1 && gPlayProperties.currentTrackNumber < gPlayProperties.numberOfTracks) { - uint8_t numLedsToLight = map(gPlayProperties.currentTrackNumber, 0, gPlayProperties.numberOfTracks - 1, 0, NUM_LEDS); - FastLED.clear(); - for (uint8_t i = 0; i < numLedsToLight; i++) { - leds[Led_Address(i)] = CRGB::Blue; - FastLED.show(); - #ifdef BATTERY_MEASURE_ENABLE - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning) || LED_INDICATOR_IS_SET(LedIndicatorType::Voltage) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #else - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #endif - break; - } else { - vTaskDelay(portTICK_RATE_MS * 30); - } - } +// -------------------------------- +// Pause Animation +// -------------------------------- +// Animates the pause if no Webstream is active +AnimationReturnType Animation_Pause(const bool startNewAnimation, CRGBSet &leds) { + (void) startNewAnimation; // start is not used - for (uint8_t i = 0; i <= 100; i++) { - #ifdef BATTERY_MEASURE_ENABLE - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning) || LED_INDICATOR_IS_SET(LedIndicatorType::Voltage) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #else - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #endif - break; - } else { - vTaskDelay(portTICK_RATE_MS * 15); - } - } + leds = CRGB::Black; + CRGB::HTMLColorCode generalColor = CRGB::Orange; + if (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode()) { + generalColor = CRGB::Blue; + } - for (uint8_t i = numLedsToLight; i > 0; i--) { - leds[Led_Address(i) - 1] = CRGB::Black; - FastLED.show(); - #ifdef BATTERY_MEASURE_ENABLE - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning) || LED_INDICATOR_IS_SET(LedIndicatorType::Voltage) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #else - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #endif - break; - } - else { - vTaskDelay(portTICK_RATE_MS * 30); - } - } + uint8_t pauseOffset = 0; + if (OFFSET_PAUSE_LEDS) { + pauseOffset = ((leds.size() / NUM_LEDS_IDLE_DOTS) / 2) - 1; + } + Led_DrawIdleDots(leds, pauseOffset, generalColor); + + return AnimationReturnType(false, 10, true); +} + +// -------------------------------- +// Speech Animation +// -------------------------------- +// only draw yellow pause-dots +AnimationReturnType Animation_Speech(const bool startNewAnimation, CRGBSet &leds) { + (void) startNewAnimation; // start is not used + + leds = CRGB::Black; + uint8_t pauseOffset = 0; + if (OFFSET_PAUSE_LEDS) { + pauseOffset = ((leds.size() / NUM_LEDS_IDLE_DOTS) / 2) - 1; + } + Led_DrawIdleDots(leds, pauseOffset, CRGB::Yellow); + + return AnimationReturnType(false, 10, true); +} + +// -------------------------------- +// Progress in Track Animation +// -------------------------------- +AnimationReturnType Animation_Progress(const bool startNewAnimation, CRGBSet &leds) { + // return values + int32_t animationDelay = 0; + // static values + static double lastPos = 0.0f; + + if (gPlayProperties.currentRelPos != lastPos || startNewAnimation) { + lastPos = gPlayProperties.currentRelPos; + leds = CRGB::Black; + if constexpr (NUM_INDICATOR_LEDS == 1) { + leds[0].setHue((uint8_t) (85 - ((double) 90 / 100) * gPlayProperties.currentRelPos)); + } else { + const uint32_t ledValue = std::clamp(map(gPlayProperties.currentRelPos, 0, 98, 0, leds.size() * DIMMABLE_STATES), 0, leds.size() * DIMMABLE_STATES); + const uint8_t fullLeds = ledValue / DIMMABLE_STATES; + const uint8_t lastLed = ledValue % DIMMABLE_STATES; + for (uint8_t led = 0; led < fullLeds; led++) { + if (System_AreControlsLocked()) { + leds[Led_Address(led)] = CRGB::Red; + } else if (!gPlayProperties.pausePlay) { // Hue-rainbow + leds[Led_Address(led)].setHue((uint8_t) (((float) PROGRESS_HUE_END - (float) PROGRESS_HUE_START) / (leds.size() - 1) * led + PROGRESS_HUE_START)); + } + } + if (lastLed > 0) { + if (System_AreControlsLocked()) { + leds[Led_Address(fullLeds)] = CRGB::Red; + } else { + leds[Led_Address(fullLeds)].setHue((uint8_t) (((float) PROGRESS_HUE_END - (float) PROGRESS_HUE_START) / (leds.size() - 1) * fullLeds + PROGRESS_HUE_START)); } + leds[Led_Address(fullLeds)] = Led_DimColor(leds[Led_Address(fullLeds)], lastLed); } } + animationDelay = 10; + } + return AnimationReturnType(false, animationDelay, true); +} - // Skip playmodes if shutdown-button is pressed as this leads to ugly indications - if (!gButtons[gShutdownButton].currentState && gShutdownButton != 99) { - vTaskDelay(portTICK_RATE_MS * 20); - continue; +// -------------------------------- +// Volume-Change Animation +// -------------------------------- +// - Single-LED: led indicates loudness between green (low) => red (high) +// - Multiple-LEDs: number of LEDs indicate loudness; gradient is shown between +// green (low) => red (high) +AnimationReturnType Animation_Volume(const bool startNewAnimation, CRGBSet &leds) { + // return-values + int32_t animationDelay = 0; + bool animationActive = true; + // static values + static uint16_t cyclesWaited = 0; + + // wait for further volume changes within next 20ms for 50 cycles = 1s + const uint32_t ledValue = std::clamp(map(AudioPlayer_GetCurrentVolume(), 0, AudioPlayer_GetMaxVolume(), 0, leds.size() * DIMMABLE_STATES), 0, leds.size() * DIMMABLE_STATES); + const uint8_t fullLeds = ledValue / DIMMABLE_STATES; + const uint8_t lastLed = ledValue % DIMMABLE_STATES; + + leds = CRGB::Black; + + if constexpr (NUM_INDICATOR_LEDS == 1) { + const uint8_t hue = 85 - (90 * ((double) AudioPlayer_GetCurrentVolume() / (double) AudioPlayer_GetMaxVolumeSpeaker())); + leds[0].setHue(hue); + } else { + /* + * (Inverse) color-gradient from green (85) back to (still) + * red (250) using unsigned-cast. + */ + for (int led = 0; led < fullLeds; led++) { + const uint8_t hue = (-86.0f) / (leds.size() - 1) * led + 85.0f; + leds[Led_Address(led)].setHue(hue); + } + if (lastLed > 0) { + const uint8_t hue = (-86.0f) / (leds.size() - 1) * fullLeds + 85.0f; + leds[Led_Address(fullLeds)].setHue(hue); + leds[Led_Address(fullLeds)] = Led_DimColor(leds[Led_Address(fullLeds)], lastLed); } + } - switch (gPlayProperties.playMode) { - case NO_PLAYLIST: // If no playlist is active (idle) - if (hlastVolume == AudioPlayer_GetCurrentVolume() && lastLedBrightness == Led_Brightness) { - for (uint8_t i = 0; i < NUM_LEDS; i++) { - if (hlastVolume != AudioPlayer_GetCurrentVolume()) { - // skip idle pattern if volume has changed - break; - } - if (OPMODE_BLUETOOTH_SINK == System_GetOperationMode()) { - idleColor = CRGB::Blue; - } else - if (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode()) { - if (Bluetooth_Source_Connected()) { - idleColor = CRGB::Blue; - } else { - idleColor = CRGB::BlueViolet; - } - } else { - if (Wlan_ConnectionTryInProgress()) { - idleColor = CRGB::Orange; - } else { - if (Wlan_IsConnected() && gPlayProperties.currentSpeechActive) { - idleColor = speechColor; - } else { - if (Wlan_IsConnected()) { - idleColor = CRGB::White; - } else { - idleColor = CRGB::Green; - } - } - } + // reset animation if volume changes + if (LED_INDICATOR_IS_SET(LedIndicatorType::VolumeChange) || startNewAnimation) { + LED_INDICATOR_CLEAR(LedIndicatorType::VolumeChange); + cyclesWaited = 0; + } + + if (cyclesWaited < LED_VOLUME_INDICATOR_NUM_CYCLES) { + animationDelay = 20; + cyclesWaited++; + } else { + animationActive = false; + cyclesWaited = 0; + } + return AnimationReturnType(animationActive, animationDelay, true); +} + +// -------------------------------- +// PLAYLIST-PROGRESS Animation +// -------------------------------- +AnimationReturnType Animation_PlaylistProgress(const bool startNewAnimation, CRGBSet &leds) { + // return-values + static bool animationActive = false; // signals if the animation is currently active + int32_t animationDelay = 0; + // static variables for animation + static LedPlaylistProgressStates animationState = LedPlaylistProgressStates::Done; // Statemachine-variable of this animation + static uint32_t animationCounter = 0; // counter-variable to loop through leds or to wait + static uint32_t staticLastBarLenghtPlaylist = 0; // variable to remember the last length of the progress-bar (for connecting animations) + static uint32_t staticLastTrack = 0; // variable to remember the last track (for connecting animations) + + if constexpr (NUM_INDICATOR_LEDS >= 4) { + if (gPlayProperties.numberOfTracks > 1 && gPlayProperties.currentTrackNumber < gPlayProperties.numberOfTracks) { + const uint32_t ledValue = std::clamp(map(gPlayProperties.currentTrackNumber, 0, gPlayProperties.numberOfTracks - 1, 0, leds.size() * DIMMABLE_STATES), 0, leds.size() * DIMMABLE_STATES); + const uint8_t fullLeds = ledValue / DIMMABLE_STATES; + const uint8_t lastLed = ledValue % DIMMABLE_STATES; + + if (LED_INDICATOR_IS_SET(LedIndicatorType::PlaylistProgress)) { + LED_INDICATOR_CLEAR(LedIndicatorType::PlaylistProgress); + // check if we need to animate a transition between from an already running animation + // only animate diff, if triggered again + if (!startNewAnimation) { + // forward progress + if (staticLastTrack < gPlayProperties.currentTrackNumber) { + if (animationState > LedPlaylistProgressStates::FillBar) { + animationState = LedPlaylistProgressStates::FillBar; + animationCounter = staticLastBarLenghtPlaylist; } - FastLED.clear(); - if (Led_Address(i) == 0) { // White if Wifi is enabled and blue if not - leds[0] = idleColor; - leds[NUM_LEDS / 4] = idleColor; - leds[NUM_LEDS / 2] = idleColor; - leds[NUM_LEDS / 4 * 3] = idleColor; + // backwards progress + } else if (staticLastTrack > gPlayProperties.currentTrackNumber) { + if (staticLastBarLenghtPlaylist < fullLeds) { + animationState = LedPlaylistProgressStates::FillBar; + animationCounter = staticLastBarLenghtPlaylist; } else { - leds[Led_Address(i) % NUM_LEDS] = idleColor; - leds[(Led_Address(i) + NUM_LEDS / 4) % NUM_LEDS] = idleColor; - leds[(Led_Address(i) + NUM_LEDS / 2) % NUM_LEDS] = idleColor; - leds[(Led_Address(i) + NUM_LEDS / 4 * 3) % NUM_LEDS] = idleColor; - } - FastLED.show(); - for (uint8_t i = 0; i <= 50; i++) { - #ifdef BATTERY_MEASURE_ENABLE - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || LED_INDICATOR_IS_SET(LedIndicatorType::VoltageWarning) || LED_INDICATOR_IS_SET(LedIndicatorType::Voltage) || gPlayProperties.playMode != NO_PLAYLIST || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #else - if (hlastVolume != AudioPlayer_GetCurrentVolume() || lastLedBrightness != Led_Brightness || LED_INDICATOR_IS_SET(LedIndicatorType::Error) || LED_INDICATOR_IS_SET(LedIndicatorType::Ok) || gPlayProperties.playMode != NO_PLAYLIST || !gButtons[gShutdownButton].currentState || System_IsSleepRequested()) { - #endif - break; - } else { - vTaskDelay(portTICK_RATE_MS * 10); - } + animationState = LedPlaylistProgressStates::EmptyBarToTarget; + animationCounter = staticLastBarLenghtPlaylist; } } } - break; - - case BUSY: // If uC is busy (parsing SD-card) - requestProgressRedraw = true; - requestClearLeds = true; - if (NUM_LEDS == 1) { - FastLED.clear(); - singleLedStatus = !singleLedStatus; - if (singleLedStatus) { - leds[0] = CRGB::BlueViolet; + staticLastTrack = gPlayProperties.currentTrackNumber; + } + + if (startNewAnimation) { + animationActive = true; + animationCounter = 0; + animationState = LedPlaylistProgressStates::FillBar; + } + + animationDelay = 30; + uint8_t barLength = 0; + switch (animationState) { + case LedPlaylistProgressStates::FillBar: + barLength = animationCounter; + if (animationCounter >= fullLeds) { + animationState = LedPlaylistProgressStates::Wait; + animationCounter = 0; } else { - leds[0] = CRGB::Black; + animationCounter++; } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 100); - } else { - for (uint8_t i = 0; i < NUM_LEDS; i++) { - FastLED.clear(); - if (Led_Address(i) == 0) { - leds[0] = CRGB::BlueViolet; - leds[NUM_LEDS / 4] = CRGB::BlueViolet; - leds[NUM_LEDS / 2] = CRGB::BlueViolet; - leds[NUM_LEDS / 4 * 3] = CRGB::BlueViolet; - } else { - leds[Led_Address(i) % NUM_LEDS] = CRGB::BlueViolet; - leds[(Led_Address(i) + NUM_LEDS / 4) % NUM_LEDS] = CRGB::BlueViolet; - leds[(Led_Address(i) + NUM_LEDS / 2) % NUM_LEDS] = CRGB::BlueViolet; - leds[(Led_Address(i) + NUM_LEDS / 4 * 3) % NUM_LEDS] = CRGB::BlueViolet; - } - FastLED.show(); - if (gPlayProperties.playMode != BUSY) { - break; - } - vTaskDelay(portTICK_RATE_MS * 50); + break; + case LedPlaylistProgressStates::Wait: + // wait + animationCounter++; + if (animationCounter >= 50) { + animationState = LedPlaylistProgressStates::EmptyBar; + animationCounter = fullLeds; } - } - break; - - default: // If playlist is active (doesn't matter which type) - if (!gPlayProperties.playlistFinished) { - if (Led_NeedsProgressRedraw(lastPlayState, lastLockState, - requestProgressRedraw)) { - lastPlayState = gPlayProperties.pausePlay; - lastLockState = System_AreControlsLocked(); - requestProgressRedraw = false; - redrawProgress = true; + barLength = fullLeds; + break; + case LedPlaylistProgressStates::EmptyBar: + // negative + barLength = animationCounter; + if (animationCounter == 0) { + animationState = LedPlaylistProgressStates::Done; + } else { + animationCounter--; } - - if (requestClearLeds) { - FastLED.clear(); - FastLED.show(); - requestClearLeds = false; + break; + case LedPlaylistProgressStates::EmptyBarToTarget: + // move back to target and wait there + barLength = animationCounter; + if (animationCounter <= fullLeds) { + animationState = LedPlaylistProgressStates::Wait; + animationCounter = 0; + } else { + animationCounter--; } + break; + default: + // done + animationActive = false; + break; + } - // Single-LED: led indicates between gradient green (beginning) => red (end) - // Multiple-LED: growing number of leds indicate between gradient green (beginning) => red (end) - if (!gPlayProperties.isWebstream) { - if (gPlayProperties.currentRelPos != lastPos || redrawProgress) { - redrawProgress = false; - lastPos = gPlayProperties.currentRelPos; - FastLED.clear(); - if (NUM_LEDS == 1) { - leds[0].setHue((uint8_t)(85 - ((double)90 / 100) * gPlayProperties.currentRelPos)); - } else { - uint8_t numLedsToLight = map(gPlayProperties.currentRelPos, 0, 98, 0, NUM_LEDS); - for (uint8_t led = 0; led < numLedsToLight; led++) { - if (System_AreControlsLocked()) { - leds[Led_Address(led)] = CRGB::Red; - } else if (!gPlayProperties.pausePlay) { // Hue-rainbow - leds[Led_Address(led)].setHue((uint8_t)(((float)PROGRESS_HUE_END - (float)PROGRESS_HUE_START) / (NUM_LEDS-1) * led + PROGRESS_HUE_START)); - } - } - } - if (gPlayProperties.pausePlay) { - generalColor = CRGB::Orange; - if (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode()) { - generalColor = CRGB::Blue; - } else - if (gPlayProperties.currentSpeechActive) { - generalColor = speechColor; - } - - leds[Led_Address(0)] = generalColor; - if (NUM_LEDS > 1) { - leds[(Led_Address(NUM_LEDS / 4)) % NUM_LEDS] = generalColor; - leds[(Led_Address(NUM_LEDS / 2)) % NUM_LEDS] = generalColor; - leds[(Led_Address(NUM_LEDS / 4 * 3)) % NUM_LEDS] = generalColor; - } - break; - } - } - } - else { // ... but do things a little bit different for Webstream as there's no progress available - if (lastSwitchTimestamp == 0 || (millis() - lastSwitchTimestamp >= ledSwitchInterval * 1000) || redrawProgress) { - redrawProgress = false; - lastSwitchTimestamp = millis(); - FastLED.clear(); - if (ledPosWebstream + 1 < NUM_LEDS) { - ledPosWebstream++; - } else { - ledPosWebstream = 0; - } - if (System_AreControlsLocked()) { - leds[Led_Address(ledPosWebstream)] = CRGB::Red; - if (NUM_LEDS > 1) { - leds[(Led_Address(ledPosWebstream) + NUM_LEDS / 2) % NUM_LEDS] = CRGB::Red; - } - } else if (!gPlayProperties.pausePlay) { - if (NUM_LEDS == 1) { - leds[0].setHue(webstreamColor++); - } else { - leds[Led_Address(ledPosWebstream)].setHue(webstreamColor); - leds[(Led_Address(ledPosWebstream) + NUM_LEDS / 2) % NUM_LEDS].setHue(webstreamColor++); - } - } else if (gPlayProperties.pausePlay) { - generalColor = CRGB::Orange; - if (gPlayProperties.currentSpeechActive) { - generalColor = speechColor; - } - if (NUM_LEDS == 1) { - leds[0] = generalColor; - } else { - leds[Led_Address(ledPosWebstream)] = generalColor; - leds[(Led_Address(ledPosWebstream) + NUM_LEDS / 2) % NUM_LEDS] = generalColor; - } - } - } - } - FastLED.show(); - vTaskDelay(portTICK_RATE_MS * 5); - } + // draw bar + leds = CRGB::Black; + for (uint8_t i = 0; i < barLength; i++) { + leds[Led_Address(i)] = CRGB::Blue; + } + if (barLength == fullLeds && lastLed > 0) { + leds[Led_Address(barLength)] = Led_DimColor(CRGB::Blue, lastLed); } - vTaskDelay(portTICK_RATE_MS * 1); - //esp_task_wdt_reset(); + staticLastBarLenghtPlaylist = barLength; } - vTaskDelete(NULL); + } else { + // nothing to show. Just clear indicator + LED_INDICATOR_CLEAR(LedIndicatorType::PlaylistProgress); + } + + return AnimationReturnType(animationActive, animationDelay, true); +} + +// -------------------------------- +// BATTERY_MEASUREMENT Animation +// -------------------------------- +// Single-LED: indicates voltage coloured between gradient green (high) => red (low) +// Multi-LED: number of LEDs indicates voltage-level with having green >= 60% ; orange < 60% + >= 30% ; red < 30% +AnimationReturnType Animation_BatteryMeasurement(const bool startNewAnimation, CRGBSet &leds) { + // return-values + static bool animationActive = false; + bool refresh = false; + int32_t animationDelay = 0; + // static variables for animation + static float staticBatteryLevel = 0.0f; // variable to store the measured battery-voltage + static uint32_t filledLedCount = 0; // loop variable to animate led-bar + + LED_INDICATOR_CLEAR(LedIndicatorType::Voltage); + + if (startNewAnimation) { + #ifdef MEASURE_BATTERY_VOLTAGE + float batteryLevel = Battery_EstimateLevel(); + #else + float batteryLevel = 1.0f; #endif + if (batteryLevel < 0.0f) { // If voltage is too low or no battery is connected + LED_INDICATOR_SET(LedIndicatorType::Error); + return AnimationReturnType(); // abort to indicate error + } + staticBatteryLevel = batteryLevel; + filledLedCount = 0; + animationActive = true; + leds = CRGB::Black; + refresh = true; + } + if constexpr (NUM_INDICATOR_LEDS == 1) { + if (staticBatteryLevel < 0.3) { + leds[0] = CRGB::Red; + } else if (staticBatteryLevel < 0.6) { + leds[0] = CRGB::Orange; + } else { + leds[0] = CRGB::Green; + } + refresh = true; + + animationDelay = 20 * 100; + animationActive = false; + } else { + uint8_t numLedsToLight = std::clamp(staticBatteryLevel * leds.size(), 0, leds.size()); + + // fill all needed LEDs + if (filledLedCount < numLedsToLight) { + if (staticBatteryLevel < 0.3) { + leds[Led_Address(filledLedCount)] = CRGB::Red; + } else if (staticBatteryLevel < 0.6) { + leds[Led_Address(filledLedCount)] = CRGB::Orange; + } else { + leds[Led_Address(filledLedCount)] = CRGB::Green; + } + refresh = true; + + filledLedCount++; + animationDelay = 20; + } else { // Wait a little + animationDelay = 20 * 100; + animationActive = false; + } + } + return AnimationReturnType(animationActive, animationDelay, refresh); +} +#endif + +void Led_TaskPause(void) { +#ifdef NEOPIXEL_ENABLE + vTaskSuspend(Led_TaskHandle); + FastLED.clear(true); +#endif +} + +void Led_TaskResume(void) { +#ifdef NEOPIXEL_ENABLE + vTaskResume(Led_TaskHandle); +#endif } diff --git a/src/Led.h b/src/Led.h index e70d6868..e36ef20a 100644 --- a/src/Led.h +++ b/src/Led.h @@ -1,16 +1,63 @@ #pragma once -typedef enum class LedIndicator -{ +typedef enum class LedIndicator { BootComplete = 0, Error, Ok, PlaylistProgress, Rewind, Voltage, - VoltageWarning + VoltageWarning, + VolumeChange } LedIndicatorType; +// ordered by priority +enum class LedAnimationType { + Boot = 0, + Shutdown, + Error, + Ok, + VoltageWarning, + Volume, + BatteryMeasurement, + Rewind, + Playlist, + Speech, + Pause, + Progress, + Webstream, + Idle, + Busy, + NoNewAnimation +}; + +enum class LedPlaylistProgressStates { + FillBar = 0, + Wait, + EmptyBar, + EmptyBarToTarget, + Done +}; + +struct AnimationReturnType { + bool animationActive; + int32_t animationDelay; + bool animationRefresh; + + void clear() { + animationActive = false; + animationDelay = 0; + } + AnimationReturnType() + : animationActive(false) + , animationDelay(0) + , animationRefresh(false) { } + AnimationReturnType(bool active, int32_t delay, bool refresh = false) + : animationActive(active) + , animationDelay(delay) + , animationRefresh(refresh) { } +}; + void Led_Init(void); void Led_Exit(void); void Led_Indicate(LedIndicatorType value); @@ -19,7 +66,5 @@ void Led_ResetToInitialBrightness(void); void Led_ResetToNightBrightness(void); uint8_t Led_GetBrightness(void); void Led_SetBrightness(uint8_t value); -uint8_t AudioPlayer_GetInitVolume(void); -void AudioPlayer_SetInitVolume(uint8_t value); -uint8_t AudioPlayer_GetInitVolume(void); -void AudioPlayer_SetInitVolume(uint8_t value); +void Led_TaskPause(void); +void Led_TaskResume(void); diff --git a/src/Log.cpp b/src/Log.cpp index 1fcc8c46..fa679be4 100644 --- a/src/Log.cpp +++ b/src/Log.cpp @@ -1,19 +1,31 @@ #include #include "settings.h" + #include "Log.h" -#include "MemX.h" -#include "LogRingBuffer.h" -// Serial-logging buffer -uint8_t Log_BufferLength = 200; -char *Log_Buffer; +#include "LogRingBuffer.h" +#include "MemX.h" static LogRingBuffer *Log_RingBuffer = NULL; -void Log_Init(void){ +void Log_Init(void) { Serial.begin(115200); Log_RingBuffer = new LogRingBuffer(); - Log_Buffer = (char *) x_calloc(Log_BufferLength, sizeof(char)); // Buffer for all log-messages +} + +String getLoglevel(const uint8_t logLevel) { + switch (logLevel) { + case LOGLEVEL_ERROR: + return "E"; + case LOGLEVEL_NOTICE: + return "N"; + case LOGLEVEL_INFO: + return "I"; + case LOGLEVEL_DEBUG: + return "D"; + default: + return " "; + } } /* Wrapper-function for serial-logging (with newline) @@ -24,11 +36,10 @@ void Log_Init(void){ void Log_Println(const char *_logBuffer, const uint8_t _minLogLevel) { if (SERIAL_LOGLEVEL >= _minLogLevel) { uint32_t ctime = millis(); - Serial.printf("[ %u ] ", ctime); + const String sLogLevel = getLoglevel(_minLogLevel); + Serial.printf("%s [%u] ", sLogLevel.c_str(), ctime); Serial.println(_logBuffer); - Log_RingBuffer->print("[ "); - Log_RingBuffer->print(ctime); - Log_RingBuffer->print(" ] "); + Log_RingBuffer->printf("%s [%u] ", sLogLevel.c_str(), ctime); Log_RingBuffer->println(_logBuffer); } } @@ -38,19 +49,39 @@ void Log_Print(const char *_logBuffer, const uint8_t _minLogLevel, bool printTim if (SERIAL_LOGLEVEL >= _minLogLevel) { if (printTimestamp) { uint32_t ctime = millis(); - Serial.printf("[ %u ] ", ctime); + const String sLogLevel = getLoglevel(_minLogLevel); + Serial.printf("%s [%u] ", sLogLevel.c_str(), ctime); Serial.print(_logBuffer); - Log_RingBuffer->print("[ "); - Log_RingBuffer->print(ctime); - Log_RingBuffer->print(" ] "); + Log_RingBuffer->printf("%s [%u] ", sLogLevel.c_str(), ctime); } else { Serial.print(_logBuffer); - } Log_RingBuffer->print(_logBuffer); } } +int Log_Printf(const uint8_t _minLogLevel, const char *format, ...) { + char loc_buf[201]; // Allow a maximum buffer of 200 characters in a single log message + + int len; + va_list arg; + va_start(arg, format); + + // use the local buffer and trunctate string if it's larger + len = vsnprintf(loc_buf, sizeof(loc_buf), format, arg); + + Log_Print(loc_buf, _minLogLevel, true); + if (len > sizeof(loc_buf) - 1) { + // long string was trunctated + Log_Print("...", _minLogLevel, false); + } + Log_Print("\n", _minLogLevel, false); + + va_end(arg); + + return std::min(len, sizeof(loc_buf) - 1); +} + String Log_GetRingBuffer(void) { return Log_RingBuffer->get(); } diff --git a/src/Log.h b/src/Log.h index 5715256b..fab09937 100644 --- a/src/Log.h +++ b/src/Log.h @@ -2,13 +2,10 @@ #include "logmessages.h" // Loglevels available (don't change!) -#define LOGLEVEL_ERROR 1 // only errors +#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 - -extern uint8_t Log_BufferLength; -extern char *Log_Buffer; // Buffer for all log-messages +#define LOGLEVEL_INFO 3 // infos + errors + important messages +#define LOGLEVEL_DEBUG 4 // almost everything /* Wrapper-function for serial-logging (with newline) _logBuffer: char* to log @@ -20,5 +17,8 @@ void Log_Println(const char *_logBuffer, const uint8_t _minLogLevel); /* Wrapper-function for serial-logging (without newline) */ void Log_Print(const char *_logBuffer, const uint8_t _minLogLevel, bool printTimestamp); +/* Wrapper-function for printf serial-logging (with newline) */ +int Log_Printf(const uint8_t _minLogLevel, const char *format, ...); + void Log_Init(void); String Log_GetRingBuffer(void); diff --git a/src/LogMessages_DE.cpp b/src/LogMessages_DE.cpp index 56b32d35..0e69fd97 100644 --- a/src/LogMessages_DE.cpp +++ b/src/LogMessages_DE.cpp @@ -4,233 +4,249 @@ #if (LANGUAGE == DE) #include "Log.h" - const char tryConnectMqttS[] PROGMEM = "Versuche Verbindung zu MQTT-Broker aufzubauen"; - const char mqttOk[] PROGMEM = "MQTT-Session aufgebaut."; - const char sleepTimerEOP[] PROGMEM = "Sleep-Timer: Nach dem letzten Track der Playlist."; - const char sleepTimerEOT[] PROGMEM = "Sleep-Timer: Nach dem Ende des laufenden Tracks."; - const char sleepTimerStop[] PROGMEM = "Sleep-Timer wurde deaktiviert."; - const char sleepTimerEO5[] PROGMEM = "Sleep Timer: Nach Ende des Titels oder, wenn früher, Ende der Playlist"; - const char sleepTimerAlreadyStopped[] PROGMEM = "Sleep-Timer ist bereits deaktiviert."; - const char sleepTimerSetTo[] PROGMEM = "Sleep-Timer gesetzt auf"; - const char allowButtons[] PROGMEM = "Alle Tasten werden freigegeben."; - const char lockButtons[] PROGMEM = "Alle Tasten werden gesperrt."; - const char noPlaylistNotAllowedMqtt[] PROGMEM = "Playmode kann nicht auf 'Keine Playlist' gesetzt werden via MQTT."; - const char playmodeChangedMQtt[] PROGMEM = "Playmode per MQTT angepasst."; - const char noPlaymodeChangeIfIdle[] PROGMEM = "Playmode kann nicht verändert werden, wenn keine Playlist aktiv ist."; - const char noValidTopic[] PROGMEM = "Kein gültiges Topic"; - const char freePtr[] PROGMEM = "Ptr-Freigabe"; - const char freeMemory[] PROGMEM = "Freier Speicher"; - const char writeEntryToNvs[] PROGMEM = "Schreibe Eintrag in NVS"; - const char freeMemoryAfterFree[] PROGMEM = "Freier Speicher nach Aufräumen"; - const char releaseMemoryOfOldPlaylist[] PROGMEM = "Gebe Speicher der alten Playlist frei."; - const char dirOrFileDoesNotExist[] PROGMEM = "Datei oder Verzeichnis existiert nicht "; - const char unableToAllocateMemForPlaylist[] PROGMEM = "Speicher für Playlist konnte nicht allokiert werden!"; - const char unableToAllocateMem[] PROGMEM = "Speicher konnte nicht allokiert werden!"; - const char fileModeDetected[] PROGMEM = "Dateimodus erkannt."; - const char nameOfFileFound[] PROGMEM = "Gefundenes File"; - const char reallocCalled[] PROGMEM = "Speicher reallokiert."; - const char unableToAllocateMemForLinearPlaylist[] PROGMEM = "Speicher für lineare Playlist konnte nicht allokiert werden!"; - const char numberOfValidFiles[] PROGMEM = "Anzahl gültiger Files/Webstreams"; - const char newLoudnessReceivedQueue[] PROGMEM = "Neue Lautstärke empfangen via Queue"; - const char newCntrlReceivedQueue[] PROGMEM = "Kontroll-Kommando empfangen via Queue"; - const char newPlaylistReceived[] PROGMEM = "Neue Playlist empfangen"; - const char repeatTrackDueToPlaymode[] PROGMEM = "Wiederhole Titel aufgrund von Playmode."; - const char repeatPlaylistDueToPlaymode[] PROGMEM = "Wiederhole Playlist aufgrund von Playmode."; - const char cmndStop[] PROGMEM = "Kommando: Stop"; - const char cmndPause[] PROGMEM = "Kommando: Pause"; - const char cmndNextTrack[] PROGMEM = "Kommando: Nächster Titel"; - const char cmndPrevTrack[] PROGMEM = "Kommando: Vorheriger Titel"; - const char cmndFirstTrack[] PROGMEM = "Kommando: Erster Titel von Playlist"; - const char cmndLastTrack[] PROGMEM = "Kommando: Letzter Titel von Playlist"; - const char cmndDoesNotExist[] PROGMEM = "Dieses Kommando existiert nicht."; - const char lastTrackAlreadyActive[] PROGMEM = "Es wird bereits der letzte Track gespielt."; - const char trackStartAudiobook[] PROGMEM = "Titel wird im Hörspielmodus von vorne gespielt."; - const char trackStart[] PROGMEM = "Titel wird von vorne gespielt."; - const char trackChangeWebstream[] PROGMEM = "Im Webradio-Modus kann nicht an den Anfang gesprungen werden."; - const char endOfPlaylistReached[] PROGMEM = "Ende der Playlist erreicht."; - const char trackStartatPos[] PROGMEM = "Titel wird abgespielt ab Position"; - const char waitingForTaskQueues[] PROGMEM = "Task Queue für RFID existiert noch nicht, warte..."; - const char rfidScannerReady[] PROGMEM = "RFID-Tags koennen jetzt gescannt werden..."; - const char rfidTagDetected[] PROGMEM = "RFID-Karte erkannt: "; - const char rfid15693TagDetected[] PROGMEM = "RFID-Karte (ISO-15693) erkannt: "; - const char rfidTagReceived[] PROGMEM = "RFID-Karte empfangen"; - const char dontAccepctSameRfid[] PROGMEM = "Aktuelle RFID-Karte erneut aufgelegt - abgelehnt!"; - const char rfidTagUnknownInNvs[] PROGMEM = "RFID-Karte ist im NVS nicht hinterlegt."; - const char goToSleepDueToIdle[] PROGMEM = "Gehe in Deep Sleep wegen Inaktivität..."; - const char goToSleepDueToTimer[] PROGMEM = "Gehe in Deep Sleep wegen Sleep Timer..."; - const char goToSleepNow[] PROGMEM = "Gehe jetzt in Deep Sleep!"; - const char maxLoudnessReached[] PROGMEM = "Maximale Lautstärke bereits erreicht!"; - const char minLoudnessReached[] PROGMEM = "Minimale Lautstärke bereits erreicht!"; - const char errorOccured[] PROGMEM = "Fehler aufgetreten!"; - const char noMp3FilesInDir[] PROGMEM = "Verzeichnis beinhaltet keine mp3-Files."; - const char modeSingleTrack[] PROGMEM = "Modus: Einzelner Track"; - const char modeSingleTrackLoop[] PROGMEM = "Modus: Einzelner Track in Endlosschleife"; - const char modeSingleTrackRandom[] PROGMEM = "Modus: Einzelner Track eines Ordners zufällig"; - const char modeSingleAudiobook[] PROGMEM = "Modus: Hoerspiel"; - const char modeSingleAudiobookLoop[] PROGMEM = "Modus: Hoerspiel in Endlosschleife"; - const char modeAllTrackAlphSorted[] PROGMEM = "Modus: Spiele alle Tracks (alphabetisch sortiert) des Ordners"; - const char modeAllTrackRandom[] PROGMEM = "Modus: Alle Tracks eines Ordners zufällig"; - const char modeAllTrackAlphSortedLoop[] PROGMEM = "Modus: Alle Tracks eines Ordners sortiert (alphabetisch) in Endlosschleife"; - const char modeAllTrackRandomLoop[] PROGMEM = "Modus: Alle Tracks eines Ordners zufällig in Endlosschleife"; - const char modeWebstream[] PROGMEM = "Modus: Webstream"; - const char modeWebstreamM3u[] PROGMEM = "Modus: Webstream (lokale .m3u-Datei)"; - const char webstreamNotAvailable[] PROGMEM = "Aktuell kein Webstream möglich, da keine WLAN-Verbindung vorhanden!"; - const char modeDoesNotExist[] PROGMEM = "Abspielmodus existiert nicht!"; - const char modeRepeatNone[] PROGMEM = "Repeatmodus: Kein Repeat"; - const char modeRepeatTrack[] PROGMEM = "Repeatmodus: Aktueller Titel"; - const char modeRepeatPlaylist[] PROGMEM = "Repeatmodus: Gesamte Playlist"; - const char modeRepeatTracknPlaylist[] PROGMEM = "Repeatmodus: Track und Playlist"; - const char modificatorAllButtonsLocked[] PROGMEM = "Modifikator: Alle Tasten werden per RFID gesperrt."; - const char modificatorAllButtonsUnlocked[] PROGMEM = "Modifikator: Alle Tasten werden per RFID freigegeben."; - const char modificatorSleepd[] PROGMEM = "Modifikator: Sleep-Timer wieder deaktiviert."; - const char modificatorSleepTimer15[] PROGMEM = "Modifikator: Sleep-Timer per RFID aktiviert (15 Minuten)."; - const char modificatorSleepTimer30[] PROGMEM = "Modifikator: Sleep-Timer per RFID aktiviert (30 Minuten)."; - const char modificatorSleepTimer60[] PROGMEM = "Modifikator: Sleep-Timer per RFID aktiviert (60 Minuten)."; - const char modificatorSleepTimer120[] PROGMEM = "Modifikator: Sleep-Timer per RFID aktiviert (2 Stunden)."; - const char ledsDimmedToNightmode[] PROGMEM = "LEDs wurden auf Nachtmodus gedimmt."; - const char ledsDimmedToInitialValue[] PROGMEM = "LEDs wurden auf initiale Helligkeit gedimmt."; - const char modificatorNotallowedWhenIdle[] PROGMEM = "Modifikator kann bei nicht aktivierter Playlist nicht angewendet werden."; - const char modificatorSleepAtEOT[] PROGMEM = "Modifikator: Sleep-Timer am Ende des Titels aktiviert."; - const char modificatorSleepAtEOTd[] PROGMEM = "Modifikator: Sleep-Timer am Ende des Titels deaktiviert."; - const char modificatorSleepAtEOP[] PROGMEM = "Modifikator: Sleep-Timer am Ende der Playlist aktiviert."; - const char modificatorSleepAtEOPd[] PROGMEM = "Modifikator: Sleep-Timer am Ende der Playlist deaktiviert."; - const char modificatorAllTrackAlphSortedLoop[] PROGMEM = "Modifikator: Alle Titel (alphabetisch sortiert) in Endlosschleife."; - const char modificatorAllTrackRandomLoop[] PROGMEM = "Modifikator: Alle Titel (zufällige Reihenfolge) in Endlosschleife."; - const char modificatorCurTrackLoop[] PROGMEM = "Modifikator: Aktueller Titel in Endlosschleife."; - const char modificatorCurAudiobookLoop[] PROGMEM = "Modifikator: Aktuelles Hörspiel in Endlosschleife."; - const char modificatorPlaylistLoopActive[] PROGMEM = "Modifikator: Alle Titel in Endlosschleife aktiviert."; - const char modificatorPlaylistLoopDeactive[] PROGMEM = "Modifikator: Alle Titel in Endlosschleife deaktiviert."; - const char modificatorTrackActive[] PROGMEM = "Modifikator: Titel in Endlosschleife aktiviert."; - const char modificatorTrackDeactive[] PROGMEM = "Modifikator: Titel in Endlosschleife deaktiviert."; - const char modificatorNotAllowed[] PROGMEM = "Modifikator konnte nicht angewendet werden."; - const char modificatorLoopRev[] PROGMEM = "Modifikator: Endlosschleife beendet."; - const char modificatorDoesNotExist[] PROGMEM = "Ein Karten-Modifikator existiert nicht vom Typ"; - const char errorOccuredNvs[] PROGMEM = "Es ist ein Fehler aufgetreten beim Lesen aus dem NVS!"; - const char statementsReceivedByServer[] PROGMEM = "Vom Server wurde Folgendes empfangen"; - const char savedSsidInNvs[] PROGMEM = "Speichere SSID in NVS"; - const char savedWifiPwdInNvs[] PROGMEM = "Speichere WLAN-Password in NVS"; - const char apReady[] PROGMEM = "Access-Point geöffnet"; - const char httpReady[] PROGMEM = "HTTP-Server gestartet."; - const char unableToMountSd[] PROGMEM = "SD-Karte konnte nicht gemountet werden."; - const char unableToCreateVolQ[] PROGMEM = "Konnte Volume-Queue nicht anlegen."; - const char unableToCreateRfidQ[] PROGMEM = "Konnte RFID-Queue nicht anlegen."; - const char unableToCreateMgmtQ[] PROGMEM = "Konnte Play-Management-Queue nicht anlegen."; - const char unableToCreatePlayQ[] PROGMEM = "Konnte Track-Queue nicht anlegen.."; - const char initialBrightnessfromNvs[] PROGMEM = "Initiale LED-Helligkeit wurde aus NVS geladen"; - const char wroteInitialBrightnessToNvs[] PROGMEM = "Initiale LED-Helligkeit wurde ins NVS geschrieben."; - const char restoredInitialBrightnessForNmFromNvs[] PROGMEM = "LED-Helligkeit für Nachtmodus wurde aus NVS geladen"; - const char wroteNmBrightnessToNvs[] PROGMEM = "LED-Helligkeit für Nachtmodus wurde ins NVS geschrieben."; - const char wroteFtpUserToNvs[] PROGMEM = "FTP-User wurde ins NVS geschrieben."; - const char restoredFtpUserFromNvs[] PROGMEM = "FTP-User wurde aus NVS geladen"; - const char wroteFtpPwdToNvs[] PROGMEM = "FTP-Passwort wurde ins NVS geschrieben."; - const char restoredFtpPwdFromNvs[] PROGMEM = "FTP-Passwort wurde aus NVS geladen"; - const char restoredMaxInactivityFromNvs[] PROGMEM = "Maximale Inaktivitätszeit wurde aus NVS geladen"; - const char wroteMaxInactivityToNvs[] PROGMEM = "Maximale Inaktivitätszeit wurde ins NVS geschrieben."; - const char restoredInitialLoudnessFromNvs[] PROGMEM = "Initiale Lautstärke wurde aus NVS geladen"; - const char wroteInitialLoudnessToNvs[] PROGMEM = "Initiale Lautstärke wurde ins NVS geschrieben."; - const char restoredMaxLoudnessForSpeakerFromNvs[] PROGMEM = "Maximale Lautstärke für Lautsprecher wurde aus NVS geladen"; - const char restoredMaxLoudnessForHeadphoneFromNvs[] PROGMEM = "Maximale Lautstärke für Kopfhörer wurde aus NVS geladen"; - const char wroteMaxLoudnessForSpeakerToNvs[] PROGMEM = "Maximale Lautstärke für Lautsprecher wurde ins NVS geschrieben."; - const char wroteMaxLoudnessForHeadphoneToNvs[] PROGMEM = "Maximale Lautstärke für Kopfhörer wurde ins NVS geschrieben."; - const char maxVolumeSet[] PROGMEM = "Maximale Lautstärke wurde gesetzt auf"; - const char wroteMqttFlagToNvs[] PROGMEM = "MQTT-Flag wurde ins NVS geschrieben."; - const char restoredMqttActiveFromNvs[] PROGMEM = "MQTT-Flag (aktiviert) wurde aus NVS geladen"; - const char restoredMqttDeactiveFromNvs[] PROGMEM = "MQTT-Flag (deaktiviert) wurde aus NVS geladen"; - const char wroteMqttClientIdToNvs[] PROGMEM = "MQTT-ClientId wurde ins NVS geschrieben."; - const char restoredMqttClientIdFromNvs[] PROGMEM = "MQTT-ClientId wurde aus NVS geladen"; - const char wroteMqttServerToNvs[] PROGMEM = "MQTT-Server wurde ins NVS geschrieben."; - const char restoredMqttServerFromNvs[] PROGMEM = "MQTT-Server wurde aus NVS geladen"; - const char wroteMqttUserToNvs[] PROGMEM = "MQTT-User wurde ins NVS geschrieben."; - const char restoredMqttUserFromNvs[] PROGMEM = "MQTT-User wurde aus NVS geladen"; - const char wroteMqttPwdToNvs[] PROGMEM = "MQTT-Passwort wurde ins NVS geschrieben."; - const char restoredMqttPwdFromNvs[] PROGMEM = "MQTT-Passwort wurde aus NVS geladen"; - const char restoredMqttPortFromNvs[] PROGMEM = "MQTT-Port wurde aus NVS geladen"; - const char mqttWithPwd[] PROGMEM = "Verbinde zu MQTT-Server mit User und Passwort"; - const char mqttWithoutPwd[] PROGMEM = "Verbinde zu MQTT-Server ohne User und Passwort"; - const char ssidNotFoundInNvs[] PROGMEM = "SSID wurde im NVS nicht gefunden."; - const char wifiPwdNotFoundInNvs[] PROGMEM = "WLAN-Passwort wurde im NVS nicht gefunden."; - const char wifiStaticIpConfigNotFoundInNvs[] PROGMEM = "Statische WLAN-IP-Konfiguration wurde im NVS nicht gefunden."; - const char wifiHostnameNotSet[] PROGMEM = "Keine Hostname-Konfiguration im NVS gefunden."; - const char mqttConnFailed[] PROGMEM = "Verbindung fehlgeschlagen, versuche in Kürze erneut"; - const char restoredHostnameFromNvs[] PROGMEM = "Hostname aus NVS geladen"; - const char currentVoltageMsg[] PROGMEM = "Aktuelle Batteriespannung"; - const char currentChargeMsg[] PROGMEM = "Aktuelle Batterieladung"; - const char batteryCurrentMsg[] PROGMEM = "Stromverbrauch (Batterie)"; - const char batteryTempMsg[] PROGMEM = "Temperatur der Batterie"; - const char batteryCyclesMsg[] PROGMEM = "Gesehene Batteriezyklen"; - const char batteryLowMsg[] PROGMEM = "Batterieladung niedrig"; - const char batteryCriticalMsg[] PROGMEM = "Batterieladung kritisch. Gehe in Deepsleep..."; - const char sdBootFailedDeepsleep[] PROGMEM = "Bootgang wegen SD fehlgeschlagen. Gehe in Deepsleep..."; - const char wifiEnabledAfterRestart[] PROGMEM = "WLAN wird aktiviert."; - const char wifiDisabledAfterRestart[] PROGMEM = "WLAN wird deaktiviert."; - const char voltageIndicatorLowFromNVS[] PROGMEM = "Unterer Spannungslevel (Batterie) fuer Neopixel-Anzeige aus NVS geladen"; - const char voltageIndicatorHighFromNVS[] PROGMEM = "Oberer Spannungslevel (Batterie) fuer Neopixel-Anzeige aus NVS geladen"; - const char batteryCheckIntervalFromNVS[] PROGMEM = "Zyklus für Batteriemessung fuer Neopixel-Anzeige aus NVS geladen"; - const char warningLowVoltageFromNVS[] PROGMEM = "Spannungslevel (Batterie) fuer Niedrig-Warnung via Neopixel aus NVS geladen"; - const char warningCriticalVoltageFromNVS[] PROGMEM = "Spannungslevel (Batterie) fuer Kritisch-Warnung via Neopixel aus NVS geladen"; - const char batteryLowFromNVS[] PROGMEM = "Batterieladestand fuer Niedrig-Warnung via Neopixel aus NVS geladen"; - const char batteryCriticalFromNVS[] PROGMEM = "Batterieladestand fuer Kritisch-Warnung via Neopixel aus NVS geladen"; - const char unableToRestoreLastRfidFromNVS[] PROGMEM = "Letzte RFID konnte nicht aus NVS geladen werden"; - const char restoredLastRfidFromNVS[] PROGMEM = "Letzte RFID wurde aus NVS geladen"; - const char failedOpenFileForWrite[] PROGMEM = "Öffnen der Datei für den Schreibvorgang fehlgeschlagen"; - const char fileWritten[] PROGMEM = "Datei geschrieben"; - const char writeFailed[] PROGMEM = "Schreibvorgang fehlgeschlagen"; - const char writingFile[] PROGMEM = "Schreibe Datei"; - const char failedToOpenFileForAppending[] PROGMEM = "Öffnen der Datei zum Schreiben der JSON-Datei fehlgeschlagen"; - const char listingDirectory[] PROGMEM = "Verzeichnisinhalt anzeigen"; - const char failedToOpenDirectory[] PROGMEM = "Öffnen des Verzeichnisses fehlgeschlagen"; - const char notADirectory[] PROGMEM = "Kein Verzeichnis"; - const char sdMountedMmc1BitMode[] PROGMEM = "Versuche SD-Karte wird im SD_MMC-Modus (1 Bit) zu mounten..."; - const char sdMountedSpiMode[] PROGMEM = "Versuche SD-Karte wird im SPI-Modus zu mounten..."; - const char backupRecoveryWebsite[] PROGMEM = "

Das Backup-File wird eingespielt...
Zur letzten Seite zurückkehren.

"; - const char restartWebsite[] PROGMEM = "

Der ESPuino wird neu gestartet...
Zur letzten Seite zurückkehren.

"; - const char shutdownWebsite[] PROGMEM = "

Der ESPuino wird ausgeschaltet...

"; - const char mqttMsgReceived[] PROGMEM = "MQTT-Nachricht empfangen"; - const char trackPausedAtPos[] PROGMEM = "Titel pausiert bei Position"; - const char freeHeapWithoutFtp[] PROGMEM = "Freier Heap-Speicher vor FTP-Instanzierung"; - const char freeHeapWithFtp[] PROGMEM = "Freier Heap-Speicher nach FTP-Instanzierung"; - const char freeHeapAfterSetup[] PROGMEM = "Freier Heap-Speicher nach Setup-Routine"; - const char tryStaticIpConfig[] PROGMEM = "Statische IP-Konfiguration wird durchgeführt..."; - const char staticIPConfigFailed[] PROGMEM = "Statische IP-Konfiguration fehlgeschlagen"; - const char wakeUpRfidNoIso14443[] PROGMEM = "ESP32 wurde vom Kartenleser aus dem Deepsleep aufgeweckt. Allerdings wurde keine ISO-14443-Karte gefunden. Gehe zurück in den Deepsleep..."; - const char lowPowerCardSuccess[] PROGMEM = "Kartenerkennung via 'low power' erfolgreich durchgeführt"; - const char rememberLastVolume[] PROGMEM = "Lautstärke vor dem letzten Shutdown wird wiederhergestellt. Dies überschreibt die Einstellung der initialen Lautstärke aus der GUI."; - const char unableToStartFtpServer[] PROGMEM = "Der FTP-Server konnte nicht gestartet werden. Entweder weil er ist bereits gestartet oder kein WLAN verfügbar ist."; - const char unableToTellIpAddress[] PROGMEM = "IP-Adresse kann nicht angesagt werden, da keine WLAN-Verbindung besteht."; - const char newPlayModeStereo[] PROGMEM = "Neuer Modus: stereo"; - const char newPlayModeMono[] PROGMEM = "Neuer Modus: mono"; - const char portExpanderFound[] PROGMEM = "Port-expander gefunden"; - const char portExpanderNotFound[] PROGMEM = "Port-expander nicht gefunden"; - const char portExpanderInterruptEnabled[] PROGMEM = "Interrupt für Port-Expander aktiviert"; - const char warningRefactoring[] PROGMEM = "!!!!WICHTIG!!!! Beachte bitte https://forum.espuino.de/t/wechsel-zum-refactoring-branch-was-ist-zu-beachten/510 !!!!WICHTIG!!!!"; - const char playlistGenModeUncached[] PROGMEM = "Playlist-Generierung: uncached"; - const char playlistGenModeCached[] PROGMEM = "Playlist-Generierung: cached"; - const char playlistCacheFoundBut0[] PROGMEM = "Playlist-Cache-File gefunden, jedoch 0 Bytes groß"; - const char bootLoopDetected[] PROGMEM = "Bootschleife erkannt! Letzte RFID wird nicht aufgerufen."; - const char noBootLoopDetected[] PROGMEM = "Keine Bootschleife erkannt. Wunderbar :-)"; - const char importCountNokNvs[] PROGMEM = "Anzahl der ungültigen Import-Einträge"; - const char errorReadingTmpfile[] PROGMEM = "Beim Lesen der temporären Importdatei ist ein Fehler aufgetreten!"; - const char errorWritingTmpfile[] PROGMEM = "Beim Schreiben der temporären Importdatei ist ein Fehler aufgetreten!"; - const char eraseRfidNvsWeb[] PROGMEM = "

Die NVS-RFID-Zuweisungen werden gelöscht...
Zur letzten Seite zurückkehren.

"; - const char eraseRfidNvs[] PROGMEM = "NVS-RFID-Zuweisungen werden gelöscht..."; - const char erasePlaylistCachefile[] PROGMEM = "Playlist-Cachefile gelöscht"; - const char fwStart[] PROGMEM = "Starte Firmware-update via OTA..."; - const char fwEnd[] PROGMEM = "Firmware-update beendet"; - const char otaNotSupported[] PROGMEM = "Firmware-update wird von diesem ESPuino nicht unterstuetzt!"; - const char otaNotSupportedWebsite[] PROGMEM = "

Firmware-update wird von diesem ESPuino nicht unterstuetzt!
Zur letzten Seite zurückkehren.

"; - const char noPlaylist[] PROGMEM = "Keine Playlist aktiv."; - const char rfidTagRemoved[] PROGMEM = "RFID-Karte wurde entfernt"; - const char rfidTagReapplied[] PROGMEM = "RFID-Karte erneut aufgelegt"; - const char ftpEnableTooLate[] PROGMEM = "FTP kann nur innerhalb der ersten 30s aktiviert werden. Kinderschutz :-)"; - const char syncingViaNtp[] PROGMEM = "Synchronisiere Uhrzeit via NTP..."; - const char sdInfo[] PROGMEM = "SD-Kartengröße / freier Speicherplatz"; - const char paOn[] PROGMEM = "Lautsprecher eingeschaltet"; - const char paOff[] PROGMEM = "Lautsprecher ausgeschaltet"; - const char hpOn[] PROGMEM = "Kopfhörer eingeschaltet"; - const char hpOff[] PROGMEM = "Kopfhörer ausgeschaltet"; - const char webTxCanceled[] PROGMEM = "Der Webtransfer wurde aufgrund von Inaktivität beendet."; - const char cantConnectToWifi[] PROGMEM = "Verbindung zum WLAN nicht möglich. Nächster Versuch..."; - const char tryToPickRandomDir[] PROGMEM = "Versuche ein zufälliges Unterzeichnis zu finden aus"; - const char pickedRandomDir[] PROGMEM = "Zufällig ausgewähltes Unterverzeichnis"; - const char wrongWakeUpGpio[] PROGMEM = "Der gewählte GPIO ist nicht vom Typ RTC und unterstützt daher das Aufwecken des ESP32 nicht!"; +const char tryConnectMqttS[] = "Versuche Verbindung zu MQTT-Broker aufzubauen: %s"; +const char mqttOk[] = "MQTT-Session aufgebaut."; +const char sleepTimerEOP[] = "Sleep-Timer: Nach dem letzten Track der Playlist."; +const char sleepTimerEOT[] = "Sleep-Timer: Nach dem Ende des laufenden Tracks."; +const char sleepTimerStop[] = "Sleep-Timer wurde deaktiviert."; +const char sleepTimerEO5[] = "Sleep Timer: Nach Ende des Titels oder, wenn früher, Ende der Playlist"; +const char sleepTimerAlreadyStopped[] = "Sleep-Timer ist bereits deaktiviert."; +const char sleepTimerSetTo[] = "Sleep-Timer gesetzt auf %u Minute(n)"; +const char allowButtons[] = "Alle Tasten werden freigegeben."; +const char lockButtons[] = "Alle Tasten werden gesperrt."; +const char noPlaylistNotAllowedMqtt[] = "Playmode kann nicht auf 'Keine Playlist' gesetzt werden via MQTT."; +const char playmodeChangedMQtt[] = "Playmode per MQTT angepasst."; +const char noPlaymodeChangeIfIdle[] = "Playmode kann nicht verändert werden, wenn keine Playlist aktiv ist."; +const char noValidTopic[] = "Kein gültiges Topic: %s"; +const char freePtr[] = "Ptr-Freigabe: %s (0x%04x)"; +const char freeMemory[] = "Freier Speicher: %u Bytes"; +const char writeEntryToNvs[] = "[%u] Schreibe Eintrag in NVS: %s => %s"; +const char freeMemoryAfterFree[] = "Freier Speicher nach Aufräumen: %u Bytes"; +const char releaseMemoryOfOldPlaylist[] = "Gebe Speicher der alten Playlist frei (Freier Speicher: %u Bytes)"; +const char dirOrFileDoesNotExist[] = "Datei oder Verzeichnis existiert nicht: %s"; +const char unableToAllocateMemForPlaylist[] = "Speicher für Playlist konnte nicht allokiert werden!"; +const char unableToAllocateMem[] = "Speicher konnte nicht allokiert werden!"; +const char fileModeDetected[] = "Dateimodus erkannt."; +const char nameOfFileFound[] = "Gefundenes File: %s"; +const char reallocCalled[] = "Speicher reallokiert."; +const char unableToAllocateMemForLinearPlaylist[] = "Speicher für lineare Playlist konnte nicht allokiert werden!"; +const char numberOfValidFiles[] = "Anzahl gültiger Files/Webstreams: %u"; +const char newLoudnessReceivedQueue[] = "Neue Lautstärke empfangen via Queue: %u"; +const char newCntrlReceivedQueue[] = "Kontroll-Kommando empfangen via Queue: %u"; +const char newPlaylistReceived[] = "Neue Playlist mit %d Titel(n) empfangen"; +const char repeatTrackDueToPlaymode[] = "Wiederhole Titel aufgrund von Playmode."; +const char repeatPlaylistDueToPlaymode[] = "Wiederhole Playlist aufgrund von Playmode."; +const char cmndStop[] = "Kommando: Stop"; +const char cmndPause[] = "Kommando: Pause"; +const char cmndResumeFromPause[] = "Kommando: Fortsetzen"; +const char cmndNextTrack[] = "Kommando: Nächster Titel"; +const char cmndPrevTrack[] = "Kommando: Vorheriger Titel"; +const char cmndFirstTrack[] = "Kommando: Erster Titel von Playlist"; +const char cmndLastTrack[] = "Kommando: Letzter Titel von Playlist"; +const char cmndDoesNotExist[] = "Dieses Kommando existiert nicht."; +const char lastTrackAlreadyActive[] = "Es wird bereits der letzte Track gespielt."; +const char trackStartAudiobook[] = "Titel wird im Hörspielmodus von vorne gespielt."; +const char trackStart[] = "Titel wird von vorne gespielt."; +const char trackChangeWebstream[] = "Im Webradio-Modus kann nicht an den Anfang gesprungen werden."; +const char endOfPlaylistReached[] = "Ende der Playlist erreicht."; +const char trackStartatPos[] = "Titel wird abgespielt ab Position %u"; +const char waitingForTaskQueues[] = "Task Queue für RFID existiert noch nicht, warte..."; +const char rfidScannerReady[] = "RFID-Tags koennen jetzt gescannt werden..."; +const char rfidTagDetected[] = "RFID-Karte erkannt: %s"; +const char rfid15693TagDetected[] = "RFID-Karte (ISO-15693) erkannt: "; +const char rfidTagReceived[] = "RFID-Karte empfangen"; +const char dontAccepctSameRfid[] = "Aktuelle RFID-Karte erneut aufgelegt - abgelehnt! (%s)"; +const char rfidTagUnknownInNvs[] = "RFID-Karte ist im NVS nicht hinterlegt."; +const char goToSleepDueToIdle[] = "Gehe in Deep Sleep wegen Inaktivität..."; +const char goToSleepDueToTimer[] = "Gehe in Deep Sleep wegen Sleep Timer..."; +const char goToSleepNow[] = "Gehe jetzt in Deep Sleep!"; +const char maxLoudnessReached[] = "Maximale Lautstärke bereits erreicht!"; +const char minLoudnessReached[] = "Minimale Lautstärke bereits erreicht!"; +const char errorOccured[] = "Fehler aufgetreten!"; +const char noMp3FilesInDir[] = "Verzeichnis beinhaltet keine mp3-Files."; +const char modeSingleTrack[] = "Modus: Einzelner Track"; +const char modeSingleTrackLoop[] = "Modus: Einzelner Track in Endlosschleife"; +const char modeSingleTrackRandom[] = "Modus: Einzelner Track eines Ordners zufällig"; +const char modeSingleAudiobook[] = "Modus: Hoerspiel"; +const char modeSingleAudiobookLoop[] = "Modus: Hoerspiel in Endlosschleife"; +const char modeAllTrackAlphSorted[] = "Modus: Spiele alle Tracks (alphabetisch sortiert) des Ordners '%s'"; +const char modeAllTrackRandom[] = "Modus: Spiele alle Tracks (zufällig sortiert) des Ordners '%s'"; +const char modeAllTrackAlphSortedLoop[] = "Modus: Alle Tracks eines Ordners sortiert (alphabetisch) in Endlosschleife"; +const char modeAllTrackRandomLoop[] = "Modus: Alle Tracks eines Ordners zufällig in Endlosschleife"; +const char modeWebstream[] = "Modus: Webstream"; +const char modeWebstreamM3u[] = "Modus: Webstream (lokale .m3u-Datei)"; +const char webstreamNotAvailable[] = "Aktuell kein Webstream möglich, da keine WLAN-Verbindung vorhanden!"; +const char modeInvalid[] = "Ungültiger Abspielmodus %d!"; +const char modeRepeatNone[] = "Repeatmodus: Kein Repeat"; +const char modeRepeatTrack[] = "Repeatmodus: Aktueller Titel"; +const char modeRepeatPlaylist[] = "Repeatmodus: Gesamte Playlist"; +const char modeRepeatTracknPlaylist[] = "Repeatmodus: Track und Playlist"; +const char modificatorAllButtonsLocked[] = "Modifikator: Alle Tasten werden per RFID gesperrt."; +const char modificatorAllButtonsUnlocked[] = "Modifikator: Alle Tasten werden per RFID freigegeben."; +const char modificatorSleepd[] = "Modifikator: Sleep-Timer wieder deaktiviert."; +const char modificatorSleepTimer15[] = "Modifikator: Sleep-Timer per RFID aktiviert (15 Minuten)."; +const char modificatorSleepTimer30[] = "Modifikator: Sleep-Timer per RFID aktiviert (30 Minuten)."; +const char modificatorSleepTimer60[] = "Modifikator: Sleep-Timer per RFID aktiviert (60 Minuten)."; +const char modificatorSleepTimer120[] = "Modifikator: Sleep-Timer per RFID aktiviert (2 Stunden)."; +const char ledsDimmedToNightmode[] = "LEDs wurden auf Nachtmodus gedimmt."; +const char ledsDimmedToInitialValue[] = "LEDs wurden auf initiale Helligkeit gedimmt."; +const char modificatorNotallowedWhenIdle[] = "Modifikator kann bei nicht aktivierter Playlist nicht angewendet werden."; +const char modificatorSleepAtEOT[] = "Modifikator: Sleep-Timer am Ende des Titels aktiviert."; +const char modificatorSleepAtEOTd[] = "Modifikator: Sleep-Timer am Ende des Titels deaktiviert."; +const char modificatorSleepAtEOP[] = "Modifikator: Sleep-Timer am Ende der Playlist aktiviert."; +const char modificatorSleepAtEOPd[] = "Modifikator: Sleep-Timer am Ende der Playlist deaktiviert."; +const char modificatorAllTrackAlphSortedLoop[] = "Modifikator: Alle Titel (alphabetisch sortiert) in Endlosschleife."; +const char modificatorAllTrackRandomLoop[] = "Modifikator: Alle Titel (zufällige Reihenfolge) in Endlosschleife."; +const char modificatorCurTrackLoop[] = "Modifikator: Aktueller Titel in Endlosschleife."; +const char modificatorCurAudiobookLoop[] = "Modifikator: Aktuelles Hörspiel in Endlosschleife."; +const char modificatorPlaylistLoopActive[] = "Modifikator: Alle Titel in Endlosschleife aktiviert."; +const char modificatorPlaylistLoopDeactive[] = "Modifikator: Alle Titel in Endlosschleife deaktiviert."; +const char modificatorTrackActive[] = "Modifikator: Titel in Endlosschleife aktiviert."; +const char modificatorTrackDeactive[] = "Modifikator: Titel in Endlosschleife deaktiviert."; +const char modificatorNotAllowed[] = "Modifikator konnte nicht angewendet werden."; +const char modificatorLoopRev[] = "Modifikator: Endlosschleife beendet."; +const char modificatorDoesNotExist[] = "Ein Karten-Modifikator existiert nicht vom Typ %d!"; +const char errorOccuredNvs[] = "Es ist ein Fehler aufgetreten beim Lesen aus dem NVS!"; +const char statementsReceivedByServer[] = "Vom Server wurde Folgendes empfangen"; +const char apReady[] = "Access-Point geöffnet"; +const char httpReady[] = "HTTP-Server gestartet."; +const char unableToMountSd[] = "SD-Karte konnte nicht gemountet werden."; +const char unableToCreateVolQ[] = "Konnte Volume-Queue nicht anlegen."; +const char unableToCreateRfidQ[] = "Konnte RFID-Queue nicht anlegen."; +const char unableToCreateMgmtQ[] = "Konnte Play-Management-Queue nicht anlegen."; +const char unableToCreatePlayQ[] = "Konnte Track-Queue nicht anlegen.."; +const char initialBrightnessfromNvs[] = "Initiale LED-Helligkeit wurde aus NVS geladen: %u"; +const char wroteInitialBrightnessToNvs[] = "Initiale LED-Helligkeit wurde ins NVS geschrieben."; +const char restoredInitialBrightnessForNmFromNvs[] = "LED-Helligkeit für Nachtmodus wurde aus NVS geladen: %u"; +const char wroteNmBrightnessToNvs[] = "LED-Helligkeit für Nachtmodus wurde ins NVS geschrieben."; +const char wroteFtpUserToNvs[] = "FTP-User wurde ins NVS geschrieben."; +const char restoredFtpUserFromNvs[] = "FTP-User wurde aus NVS geladen: %s"; +const char wroteFtpPwdToNvs[] = "FTP-Passwort wurde ins NVS geschrieben."; +const char restoredFtpPwdFromNvs[] = "FTP-Passwort wurde aus NVS geladen: %s"; +const char restoredMaxInactivityFromNvs[] = "Maximale Inaktivitätszeit wurde aus NVS geladen: %u Minuten"; +const char wroteMaxInactivityToNvs[] = "Maximale Inaktivitätszeit wurde ins NVS geschrieben."; +const char restoredInitialLoudnessFromNvs[] = "Initiale Lautstärke wurde aus NVS geladen: %u"; +const char wroteInitialLoudnessToNvs[] = "Initiale Lautstärke wurde ins NVS geschrieben."; +const char restoredMaxLoudnessForSpeakerFromNvs[] = "Maximale Lautstärke für Lautsprecher wurde aus NVS geladen: %u"; +const char restoredMaxLoudnessForHeadphoneFromNvs[] = "Maximale Lautstärke für Kopfhörer wurde aus NVS geladen: %u"; +const char wroteMaxLoudnessForSpeakerToNvs[] = "Maximale Lautstärke für Lautsprecher wurde ins NVS geschrieben."; +const char wroteMaxLoudnessForHeadphoneToNvs[] = "Maximale Lautstärke für Kopfhörer wurde ins NVS geschrieben."; +const char maxVolumeSet[] = "Maximale Lautstärke wurde gesetzt auf: %u"; +const char wroteMqttFlagToNvs[] = "MQTT-Flag wurde ins NVS geschrieben."; +const char restoredMqttActiveFromNvs[] = "MQTT-Flag (aktiviert) wurde aus NVS geladen: %u"; +const char restoredMqttDeactiveFromNvs[] = "MQTT-Flag (deaktiviert) wurde aus NVS geladen: %u"; +const char wroteMqttClientIdToNvs[] = "MQTT-ClientId wurde ins NVS geschrieben."; +const char restoredMqttClientIdFromNvs[] = "MQTT-ClientId wurde aus NVS geladen: %s"; +const char wroteMqttServerToNvs[] = "MQTT-Server wurde ins NVS geschrieben."; +const char restoredMqttServerFromNvs[] = "MQTT-Server wurde aus NVS geladen: %s"; +const char wroteMqttUserToNvs[] = "MQTT-User wurde ins NVS geschrieben."; +const char restoredMqttUserFromNvs[] = "MQTT-User wurde aus NVS geladen: %s"; +const char wroteMqttPwdToNvs[] = "MQTT-Passwort wurde ins NVS geschrieben."; +const char restoredMqttPwdFromNvs[] = "MQTT-Passwort wurde aus NVS geladen: %s"; +const char restoredMqttPortFromNvs[] = "MQTT-Port wurde aus NVS geladen: %u"; +const char mqttWithPwd[] = "Verbinde zu MQTT-Server mit User und Passwort"; +const char mqttWithoutPwd[] = "Verbinde zu MQTT-Server ohne User und Passwort"; +const char ssidNotFoundInNvs[] = "SSID wurde im NVS nicht gefunden."; +const char wifiStaticIpConfigNotFoundInNvs[] = "Statische WLAN-IP-Konfiguration wurde im NVS nicht gefunden."; +const char wifiHostnameNotSet[] = "Keine Hostname-Konfiguration im NVS gefunden."; +const char mqttConnFailed[] = "Verbindung fehlgeschlagen, versuche in Kürze erneut: rc=%i (%d / %d)"; +const char restoredHostnameFromNvs[] = "Hostname aus NVS geladen: %s"; +const char currentVoltageMsg[] = "Aktuelle Batteriespannung: %.2f V"; +const char currentChargeMsg[] = "Aktuelle Batterieladung: %.2f %%"; +const char batteryCurrentMsg[] = "Stromverbrauch (Batterie): %.2f mA"; +const char batteryTempMsg[] = "Temperatur der Batterie: %.2f°C"; +const char batteryCyclesMsg[] = "Gesehene Batteriezyklen: %.2f"; +const char batteryLowMsg[] = "Batterieladung niedrig"; +const char batteryCriticalMsg[] = "Batterieladung kritisch. Gehe in Deepsleep..."; +const char sdBootFailedDeepsleep[] = "Bootgang wegen SD fehlgeschlagen. Gehe in Deepsleep..."; +const char wifiEnabledMsg[] = "WLAN wird aktiviert."; +const char wifiDisabledMsg[] = "WLAN wird deaktiviert."; +const char voltageIndicatorLowFromNVS[] = "Unterer Spannungslevel (Batterie) fuer Neopixel-Anzeige aus NVS geladen: %.2fV"; +const char voltageIndicatorHighFromNVS[] = "Oberer Spannungslevel (Batterie) fuer Neopixel-Anzeige aus NVS geladen: %.2fV"; +const char batteryCheckIntervalFromNVS[] = "Zyklus für Batteriemessung fuer Neopixel-Anzeige aus NVS geladen: %u Minuten"; +const char warningLowVoltageFromNVS[] = "Spannungslevel (Batterie) fuer Niedrig-Warnung via Neopixel aus NVS geladen: %.2fV"; +const char warningCriticalVoltageFromNVS[] = "Spannungslevel (Batterie) fuer Kritisch-Warnung via Neopixel aus NVS geladen: %.2fV"; +const char batteryLowFromNVS[] = "Batterieladestand fuer Niedrig-Warnung via Neopixel aus NVS geladen: %.2f %%"; +const char batteryCriticalFromNVS[] = "Batterieladestand fuer Kritisch-Warnung via Neopixel aus NVS geladen: %.2f %%"; +const char unableToRestoreLastRfidFromNVS[] = "Letzte RFID konnte nicht aus NVS geladen werden"; +const char restoredLastRfidFromNVS[] = "Letzte RFID wurde aus NVS geladen: %s"; +const char failedOpenFileForWrite[] = "Öffnen der Datei für den Schreibvorgang fehlgeschlagen"; +const char fileWritten[] = "Datei geschrieben: %s => %zu bytes in %lu ms (%lu kiB/s)"; +const char writeFailed[] = "Schreibvorgang fehlgeschlagen"; +const char writingFile[] = "Schreibe Datei: %s"; +const char failedToOpenFileForAppending[] = "Öffnen der Datei zum Schreiben der JSON-Datei fehlgeschlagen"; +const char listingDirectory[] = "Verzeichnisinhalt anzeigen"; +const char failedToOpenDirectory[] = "Öffnen des Verzeichnisses fehlgeschlagen"; +const char notADirectory[] = "Kein Verzeichnis"; +const char sdMountedMmc1BitMode[] = "Versuche SD-Karte im SD_MMC-Modus (1 Bit) zu mounten..."; +const char sdMountedSpiMode[] = "Versuche SD-Karte im SPI-Modus zu mounten..."; +const char restartWebsite[] = "

Der ESPuino wird neu gestartet...
Zur letzten Seite zurückkehren.

"; +const char shutdownWebsite[] = "Der ESPuino wird ausgeschaltet..."; +const char mqttMsgReceived[] = "MQTT-Nachricht empfangen: [Topic: %s] [Command: %s]"; +const char trackPausedAtPos[] = "Titel pausiert bei Position: %u (%u)"; +const char freeHeapWithoutFtp[] = "Freier Heap-Speicher vor FTP-Instanzierung: %u"; +const char freeHeapWithFtp[] = "Freier Heap-Speicher nach FTP-Instanzierung: %u"; +const char ftpServerStarted[] = "FTP-Server gestartet"; +const char freeHeapAfterSetup[] = "Freier Heap-Speicher nach Setup-Routine"; +const char tryStaticIpConfig[] = "Statische IP-Konfiguration wird durchgeführt..."; +const char staticIPConfigFailed[] = "Statische IP-Konfiguration fehlgeschlagen"; +const char wakeUpRfidNoIso14443[] = "ESP32 wurde vom Kartenleser aus dem Deepsleep aufgeweckt. Allerdings wurde keine ISO-14443-Karte gefunden. Gehe zurück in den Deepsleep..."; +const char lowPowerCardSuccess[] = "Kartenerkennung via 'low power' erfolgreich durchgeführt"; +const char rememberLastVolume[] = "Lautstärke vor dem letzten Shutdown wird wiederhergestellt. Dies überschreibt die Einstellung der initialen Lautstärke aus der GUI."; +const char unableToStartFtpServer[] = "Der FTP-Server konnte nicht gestartet werden. Entweder weil er ist bereits gestartet oder kein WLAN verfügbar ist."; +const char unableToTellIpAddress[] = "IP-Adresse kann nicht angesagt werden, da keine WLAN-Verbindung besteht."; +const char unableToTellTime[] = "Uhrzeit kann nicht angesagt werden, da keine WLAN-Verbindung besteht."; +const char newPlayModeStereo[] = "Neuer Modus: stereo"; +const char newPlayModeMono[] = "Neuer Modus: mono"; +const char portExpanderFound[] = "Port-expander gefunden"; +const char portExpanderNotFound[] = "Port-expander nicht gefunden"; +const char portExpanderInterruptEnabled[] = "Interrupt für Port-Expander aktiviert"; +const char playlistGen[] = "Playlist-Generierung"; +const char bootLoopDetected[] = "Bootschleife erkannt! Letzte RFID wird nicht aufgerufen."; +const char noBootLoopDetected[] = "Keine Bootschleife erkannt. Wunderbar :-)"; +const char importCountNokNvs[] = "Anzahl der ungültigen Import-Einträge: %u"; +const char errorReadingTmpfile[] = "Beim Lesen der temporären Importdatei ist ein Fehler aufgetreten!"; +const char errorWritingTmpfile[] = "Beim Schreiben der temporären Importdatei ist ein Fehler aufgetreten!"; +const char eraseRfidNvs[] = "NVS-RFID-Zuweisungen werden gelöscht..."; +const char fwStart[] = "Starte Firmware-update via OTA..."; +const char fwEnd[] = "Firmware-update beendet"; +const char otaNotSupported[] = "Firmware-update wird von diesem ESPuino nicht unterstuetzt!"; +const char otaNotSupportedWebsite[] = "

Firmware-update wird von diesem ESPuino nicht unterstuetzt!
Zur letzten Seite zurückkehren.

"; +const char noPlaylist[] = "Keine Playlist aktiv."; +const char rfidTagRemoved[] = "RFID-Karte wurde entfernt"; +const char rfidTagReapplied[] = "RFID-Karte erneut aufgelegt"; +const char ftpEnableTooLate[] = "FTP kann nur innerhalb der ersten 30s aktiviert werden. Kinderschutz :-)"; +const char dateTimeRTC[] = "Datum/Uhrzeit (Interne RTC): %02d.%02d.%4d, %02d:%02d:%02d"; +const char syncingViaNtp[] = "Synchronisiere Uhrzeit via NTP..."; +const char ntpGotTime[] = "Datum/Uhrzeit empfangen von NTP-Server: %02d.%02d.%4d, %02d:%02d:%02d"; +const char ntpFailed[] = "NTP: Datum/Uhrzeit (noch) nicht verfügbar"; +const char sdInfo[] = "SD-Kartengröße / freier Speicherplatz: %llu MB / %llu MB"; +const char paOn[] = "Lautsprecher eingeschaltet"; +const char paOff[] = "Lautsprecher ausgeschaltet"; +const char hpOn[] = "Kopfhörer eingeschaltet"; +const char hpOff[] = "Kopfhörer ausgeschaltet"; +const char webTxCanceled[] = "Der Webtransfer wurde aufgrund von Inaktivität beendet."; +const char tryToPickRandomDir[] = "Versuche ein zufälliges Unterverzeichnis zu finden aus: %s"; +const char pickedRandomDir[] = "Zufällig ausgewähltes Unterverzeichnis: %s"; +const char wrongWakeUpGpio[] = "Der gewählte GPIO ist nicht vom Typ RTC und unterstützt daher das Aufwecken des ESP32 nicht! (GPIO: %u)"; +const char currentlyPlaying[] = "'%s' wird abgespielt (%d von %d)"; +const char secondsJumpForward[] = "%d Sekunden nach vorne gesprungen"; +const char secondsJumpBackward[] = "%d Sekunden zurück gesprungen"; +const char JumpToPosition[] = "Sprung zu Position %u/%u"; +const char wroteLastTrackToNvs[] = "Schreibe '%s' in NVS für RFID-Card-ID %s mit Abspielmodus %d und letzter Track %u"; +const char wifiConnectionInProgress[] = "Versuche mit WLAN '%s' zu verbinden..."; +const char wifiConnectionSuccess[] = "Verbunden mit WLAN '%s' (Signalstärke: %d dBm, Kanal: %d, MAC-Adresse: %s)"; +const char wifiCurrentIp[] = "Aktuelle IP: %s"; +const char jsonErrorMsg[] = "deserializeJson() fehlgeschlagen: %s"; +const char wifiDeleteNetwork[] = "Lösche gespeichertes WLAN %s"; +const char wifiNetworkLoaded[] = "SSID %d von NVS geladen: %s"; +const char wifiTooManyNetworks[] = "Anzahl der WLAN-Netze in NVS ist %d, aber es sind maximal %d erlaubt."; +const char wifiAddTooManyNetworks[] = "Kein Platz, weiteres WLAN zu speichern!"; +const char wifiAddNetwork[] = "Füge WLAN hinzu: %s"; +const char wifiUpdateNetwork[] = "Ändere Passwort für WLAN %s"; +const char wifiScanResult[] = "WLAN '%s'gefunden (Signalstärke: %d dBm, Kanal: %d, MAC-Adresse: %s)"; +const char cantConnectToWifi[] = "WLAN-Verbindung fehlgeschlagen."; +const char wifiSetLastSSID[] = "Schreibe letzte erfolgreiche SSID in NVS für WLAN Schnellstart: %s"; +const char mDNSStarted[] = "mDNS gestartet: http://%s.local"; +const char mDNSFailed[] = "mDNS Start fehlgeschlagen, Hostname: %s"; #endif diff --git a/src/LogMessages_EN.cpp b/src/LogMessages_EN.cpp index eb38c51d..3a6a08af 100644 --- a/src/LogMessages_EN.cpp +++ b/src/LogMessages_EN.cpp @@ -4,233 +4,249 @@ #if (LANGUAGE == EN) #include "Log.h" - const char tryConnectMqttS[] PROGMEM = "Trying to connect to MQTT-broker"; - const char mqttOk[] PROGMEM = "MQTT-connection established."; - const char sleepTimerEOP[] PROGMEM = "Sleep-timer: after last track of playlist."; - const char sleepTimerEOT[] PROGMEM = "Sleep-timer: after end of current track."; - const char sleepTimerStop[] PROGMEM = "Sleep-timer has been disabled."; - const char sleepTimerEO5[] PROGMEM = "Sleep-timer: after five track or end of playlist - whatever is reached first"; - const char sleepTimerAlreadyStopped[] PROGMEM = "sleep-timer is already disabled."; - const char sleepTimerSetTo[] PROGMEM = "sleep-timer adjusted to"; - const char allowButtons[] PROGMEM = "Unlocking all keys."; - const char lockButtons[] PROGMEM = "Locking all keys."; - const char noPlaylistNotAllowedMqtt[] PROGMEM = "Playmode cannot be adjusted to 'no playlist' via MQTT."; - const char playmodeChangedMQtt[] PROGMEM = "Playlist adjusted via MQTT."; - const char noPlaymodeChangeIfIdle[] PROGMEM = "Playlist cannot be adjusted while no playlist is active."; - const char noValidTopic[] PROGMEM = "No valid MQTT-topic"; - const char freePtr[] PROGMEM = "Releasing Pointer"; - const char freeMemory[] PROGMEM = "Free memory"; - const char writeEntryToNvs[] PROGMEM = "Storing data to NVS"; - const char freeMemoryAfterFree[] PROGMEM = "Free memory after cleaning"; - const char releaseMemoryOfOldPlaylist[] PROGMEM = "Releasing memory of old playlist."; - const char dirOrFileDoesNotExist[] PROGMEM = "File of directory does not exist"; - const char unableToAllocateMemForPlaylist[] PROGMEM = "Unable to allocate memory for playlist!"; - const char unableToAllocateMem[] PROGMEM = "Unable to allocate memory!"; - const char fileModeDetected[] PROGMEM = "File-mode detected."; - const char nameOfFileFound[] PROGMEM = "File found"; - const char reallocCalled[] PROGMEM = "Reallocated memory."; - const char unableToAllocateMemForLinearPlaylist[] PROGMEM = "Unable to allocate memory for linear playlist!"; - const char numberOfValidFiles[] PROGMEM = "Number of valid files/webstreams"; - const char newLoudnessReceivedQueue[] PROGMEM = "New volume received via queue"; - const char newCntrlReceivedQueue[] PROGMEM = "Control-command received via queue"; - const char newPlaylistReceived[] PROGMEM = "New playlist received"; - const char repeatTrackDueToPlaymode[] PROGMEM = "Repeating track due to playmode configured."; - const char repeatPlaylistDueToPlaymode[] PROGMEM = "Repeating playlist due to playmode configured."; - const char cmndStop[] PROGMEM = "Command: stop"; - const char cmndPause[] PROGMEM = "Command: pause"; - const char cmndNextTrack[] PROGMEM = "Command: next track"; - const char cmndPrevTrack[] PROGMEM = "Command: previous track"; - const char cmndFirstTrack[] PROGMEM = "Command: first track of playlist"; - const char cmndLastTrack[] PROGMEM = "Command: last track of playlist"; - const char cmndDoesNotExist[] PROGMEM = "Command requested does not exist."; - const char lastTrackAlreadyActive[] PROGMEM = "Already playing last track."; - const char trackStartAudiobook[] PROGMEM = "Starting track in playmode from the very beginning."; - const char trackStart[] PROGMEM = "Starting track from the very beginning."; - const char trackChangeWebstream[] PROGMEM = "Playing from the very beginning is not possible while webradio-mode is active."; - const char endOfPlaylistReached[] PROGMEM = "Reached end of playlist."; - const char trackStartatPos[] PROGMEM = "Starting track at position"; - const char waitingForTaskQueues[] PROGMEM = "Task Queue for RFID does not exist yet, waiting..."; - const char rfidScannerReady[] PROGMEM = "RFID-tags can now be applied..."; - const char rfidTagDetected[] PROGMEM = "RFID-tag detected: "; - const char rfid15693TagDetected[] PROGMEM = "RFID-ta (ISO-15693) detected: "; - const char rfidTagReceived[] PROGMEM = "RFID-tag received"; - const char dontAccepctSameRfid[] PROGMEM = "Reapplied same rfid-tag - rejected!"; - const char rfidTagUnknownInNvs[] PROGMEM = "RFID-tag is unkown to NVS."; - const char goToSleepDueToIdle[] PROGMEM = "Going to deepsleep due to inactivity-timer..."; - const char goToSleepDueToTimer[] PROGMEM = "Going to deepsleep due to sleep timer..."; - const char goToSleepNow[] PROGMEM = "Going to deepsleep now!"; - const char maxLoudnessReached[] PROGMEM = "Already reached max volume!"; - const char minLoudnessReached[] PROGMEM = "Already reached min volume!"; - const char errorOccured[] PROGMEM = "Error occured!"; - const char noMp3FilesInDir[] PROGMEM = "Directory does not contain mp3-files."; - const char modeSingleTrack[] PROGMEM = "Mode: Single track"; - const char modeSingleTrackLoop[] PROGMEM = "Mode: single track as infinite loop"; - const char modeSingleTrackRandom[] PROGMEM = "Mode: single track (random) of directory"; - const char modeSingleAudiobook[] PROGMEM = "Mode: audiobook"; - const char modeSingleAudiobookLoop[] PROGMEM = "Mode: audiobook as infinite loop"; - const char modeAllTrackAlphSorted[] PROGMEM = "Mode: all tracks (in alph. order) of directory"; - const char modeAllTrackRandom[] PROGMEM = "Mode: all tracks (in random. order) of directory"; - const char modeAllTrackAlphSortedLoop[] PROGMEM = "Mode: all tracks (in alph. order) of directory as infinite loop"; - const char modeAllTrackRandomLoop[] PROGMEM = "Mode: all tracks (in random order) of directory as infinite loop"; - const char modeWebstream[] PROGMEM = "Mode: webstream"; - const char modeWebstreamM3u[] PROGMEM = "Mode: Webstream (local .m3u-file)"; - const char webstreamNotAvailable[] PROGMEM = "Unable to access webstream as no wifi-connection is available!"; - const char modeDoesNotExist[] PROGMEM = "Playmode does not exist!"; - const char modeRepeatNone[] PROGMEM = "Repeatmode: no repeat"; - const char modeRepeatTrack[] PROGMEM = "Repeatmode: current track"; - const char modeRepeatPlaylist[] PROGMEM = "Repeatmode: whole playlist"; - const char modeRepeatTracknPlaylist[] PROGMEM = "Repeatmode: track and playlist"; - const char modificatorAllButtonsLocked[] PROGMEM = "Modificator: locking all keys via RFID-tag."; - const char modificatorAllButtonsUnlocked[] PROGMEM = "Modificator: unlocking all keys via RFID-tag."; - const char modificatorSleepd[] PROGMEM = "Modificator: sleep-Timer deactivated."; - const char modificatorSleepTimer15[] PROGMEM = "Modificator: sleep-Timer enabled via RFID (15 minutes)."; - const char modificatorSleepTimer30[] PROGMEM = "Modificator: sleep-Timer enabled via RFID (30 minutes)."; - const char modificatorSleepTimer60[] PROGMEM = "Modificator: sleep-Timer enabled via RFID (60 minutes)."; - const char modificatorSleepTimer120[] PROGMEM = "Modificator: sleep-Timer enabled via RFID (2 hours)."; - const char ledsDimmedToNightmode[] PROGMEM = "Dimmed LEDs to nightmode."; - const char ledsDimmedToInitialValue[] PROGMEM = "Dimmed LEDs to initial value."; - const char modificatorNotallowedWhenIdle[] PROGMEM = "Modificator cannot be applied while playlist is inactive."; - const char modificatorSleepAtEOT[] PROGMEM = "Modificator: adjusted sleep-timer to after end of current track."; - const char modificatorSleepAtEOTd[] PROGMEM = "Modificator: disabled sleep-timer after end of current track."; - const char modificatorSleepAtEOP[] PROGMEM = "Modificator: adjusted sleep-timer to after end of playlist."; - const char modificatorSleepAtEOPd[] PROGMEM = "Modificator: disabled sleep-timer after end of playlist."; - const char modificatorAllTrackAlphSortedLoop[] PROGMEM = "Modificator: adjusted to all tracks (in alph. order) as infinite loop."; - const char modificatorAllTrackRandomLoop[] PROGMEM = "Modificator: adjusted to all tracks (in random order) as infinite loop."; - const char modificatorCurTrackLoop[] PROGMEM = "Modificator: adjusted to current track as infinite loop."; - const char modificatorCurAudiobookLoop[] PROGMEM = "Modificator: adjusted to current audiobook as infinite loop."; - const char modificatorPlaylistLoopActive[] PROGMEM = "Modificator: adjusted to all tracks as infinite loop."; - const char modificatorPlaylistLoopDeactive[] PROGMEM = "Modificator: disabled all tracks as infinite loop."; - const char modificatorTrackActive[] PROGMEM = "Modificator: adjusted to current track as infinite loop."; - const char modificatorTrackDeactive[] PROGMEM = "Modificator: disabled current track as infinite loop."; - const char modificatorNotAllowed[] PROGMEM = "Unable to apply modificator."; - const char modificatorLoopRev[] PROGMEM = "Modificator: infinite loop ended."; - const char modificatorDoesNotExist[] PROGMEM = "This type of card-modificator does not exist"; - const char errorOccuredNvs[] PROGMEM = "Error occured while reading from NVS!"; - const char statementsReceivedByServer[] PROGMEM = "Data received from server"; - const char savedSsidInNvs[] PROGMEM = "Storing SSID to NVS"; - const char savedWifiPwdInNvs[] PROGMEM = "Storing wifi-password to NVS"; - const char apReady[] PROGMEM = "Started wifi-access-point"; - const char httpReady[] PROGMEM = "Started HTTP-server."; - const char unableToMountSd[] PROGMEM = "Unable to mount sd-card."; - const char unableToCreateVolQ[] PROGMEM = "Unable to create volume-queue."; - const char unableToCreateRfidQ[] PROGMEM = "Unable to create RFID-queue."; - const char unableToCreateMgmtQ[] PROGMEM = "Unable to play-management-queue."; - const char unableToCreatePlayQ[] PROGMEM = "Unable to create track-queue.."; - const char initialBrightnessfromNvs[] PROGMEM = "Restoring initial LED-brightness from NVS"; - const char wroteInitialBrightnessToNvs[] PROGMEM = "Storing initial LED-brightness to NVS."; - const char restoredInitialBrightnessForNmFromNvs[] PROGMEM = "Restored LED-brightness for nightmode from NVS"; - const char wroteNmBrightnessToNvs[] PROGMEM = "Stored LED-brightness for nightmode to NVS."; - const char wroteFtpUserToNvs[] PROGMEM = "Stored FTP-user to NVS."; - const char restoredFtpUserFromNvs[] PROGMEM = "Restored FTP-user from NVS"; - const char wroteFtpPwdToNvs[] PROGMEM = "Stored FTP-password to NVS."; - const char restoredFtpPwdFromNvs[] PROGMEM = "Restored FTP-password from NVS"; - const char restoredMaxInactivityFromNvs[] PROGMEM = "Restored maximum inactivity-time from NVS."; - const char wroteMaxInactivityToNvs[] PROGMEM = "Stored maximum inactivity-time to NVS."; - const char restoredInitialLoudnessFromNvs[] PROGMEM = "Restored initial volume from NVS"; - const char wroteInitialLoudnessToNvs[] PROGMEM = "Stored initial volume to NVS."; - const char restoredMaxLoudnessForSpeakerFromNvs[] PROGMEM = "Restored maximum volume for speaker from NVS"; - const char restoredMaxLoudnessForHeadphoneFromNvs[] PROGMEM = "Restored maximum volume for headphone from NVS"; - const char wroteMaxLoudnessForSpeakerToNvs[] PROGMEM = "Wrote maximum volume for speaker to NVS."; - const char wroteMaxLoudnessForHeadphoneToNvs[] PROGMEM = "Wrote maximum volume for headphone to NVS."; - const char maxVolumeSet[] PROGMEM = "Maximum volume set to"; - const char wroteMqttFlagToNvs[] PROGMEM = "Stored MQTT-flag to NVS."; - const char restoredMqttActiveFromNvs[] PROGMEM = "Restored MQTT-flag (enabled) from NVS"; - const char restoredMqttDeactiveFromNvs[] PROGMEM = "Restored MQTT-flag (disabled) from NVS"; - const char wroteMqttClientIdToNvs[] PROGMEM = "Stored MQTT-clientid to NVS."; - const char restoredMqttClientIdFromNvs[] PROGMEM = "Restored MQTT-clientid from NVS"; - const char wroteMqttServerToNvs[] PROGMEM = "Stored MQTT-server to NVS."; - const char restoredMqttServerFromNvs[] PROGMEM = "Restored MQTT-Server from NVS"; - const char wroteMqttUserToNvs[] PROGMEM = "Stored MQTT-user to NVS."; - const char restoredMqttUserFromNvs[] PROGMEM = "Restored MQTT-user from NVS"; - const char wroteMqttPwdToNvs[] PROGMEM = "Stored MQTT-password to NVS."; - const char restoredMqttPwdFromNvs[] PROGMEM = "Restored MQTT-password from NVS"; - const char restoredMqttPortFromNvs[] PROGMEM = "Restored MQTT-port from NVS"; - const char mqttWithPwd[] PROGMEM = "Try to connect to MQTT-server with user und password"; - const char mqttWithoutPwd[] PROGMEM = "Try to connect to MQTT-server without user und password"; - const char ssidNotFoundInNvs[] PROGMEM = "Unable to find SSID to NVS."; - const char wifiPwdNotFoundInNvs[] PROGMEM = "Unable to find wifi-password to NVS."; - const char wifiStaticIpConfigNotFoundInNvs[] PROGMEM = "Unable to find wifi-ip-configuration to NVS."; - const char wifiHostnameNotSet[] PROGMEM = "Unable to find hostname-configuration to NVS."; - const char mqttConnFailed[] PROGMEM = "Unable to establish mqtt-connection, trying again..."; - const char restoredHostnameFromNvs[] PROGMEM = "Restored hostname from NVS"; - const char currentVoltageMsg[] PROGMEM = "Current battery-voltage"; - const char currentChargeMsg[] PROGMEM = "Current battery charge"; - const char batteryCurrentMsg[] PROGMEM = "Power usage (Battery)"; - const char batteryTempMsg[] PROGMEM = "Battery temperature"; - const char batteryCyclesMsg[] PROGMEM = "Seen battery cycles"; - const char batteryLowMsg[] PROGMEM = "Battery charge low"; - const char batteryCriticalMsg[] PROGMEM = "Battery charge critical. Will go to deepsleep..."; - const char sdBootFailedDeepsleep[] PROGMEM = "Failed to boot due to SD. Will go to deepsleep..."; - const char wifiEnabledAfterRestart[] PROGMEM = "WiFi will be enabled."; - const char wifiDisabledAfterRestart[] PROGMEM = "WiFi will be disabled ."; - const char voltageIndicatorLowFromNVS[] PROGMEM = "Restored lower voltage-level for Neopixel-display from NVS"; - const char voltageIndicatorHighFromNVS[] PROGMEM = "Restored upper voltage-level for Neopixel-display from NVS"; - const char batteryCheckIntervalFromNVS[] PROGMEM = "Restored interval of battery-measurement or Neopixel-display from NVS"; - const char warningLowVoltageFromNVS[] PROGMEM = "Restored low battery-voltage-level for warning via Neopixel from NVS"; - const char warningCriticalVoltageFromNVS[] PROGMEM = "Restored critical battery-voltage-level for warning via Neopixel from NVS"; - const char batteryLowFromNVS[] PROGMEM = "Restored low battery level from NVS"; - const char batteryCriticalFromNVS[] PROGMEM = "Restored critical battery level from NVS"; - const char unableToRestoreLastRfidFromNVS[] PROGMEM = "Unable to restore last RFID from NVS"; - const char restoredLastRfidFromNVS[] PROGMEM = "Restored last RFID from NVS"; - const char failedOpenFileForWrite[] PROGMEM = "Failed to open file for writing"; - const char fileWritten[] PROGMEM = "File written"; - const char writeFailed[] PROGMEM = "Write failed"; - const char writingFile[] PROGMEM = "Writing file"; - const char failedToOpenFileForAppending[] PROGMEM = "Failed to open file for appending"; - const char listingDirectory[] PROGMEM = "Listing directory"; - const char failedToOpenDirectory[] PROGMEM = "Failed to open directory"; - const char notADirectory[] PROGMEM = "Not a directory"; - const char sdMountedMmc1BitMode[] PROGMEM = "SD card mounted in SPI-mode configured..."; - const char sdMountedSpiMode[] PROGMEM = "Mounting SD card in SPI-mode..."; - const char backupRecoveryWebsite[] PROGMEM = "

Backup-file is being applied...
Back to last page.

"; - const char restartWebsite[] PROGMEM = "

ESPuino is being restarted...
Back to last page.

"; - const char shutdownWebsite[] PROGMEM = "

Der ESPuino is being shutdown...

"; - const char mqttMsgReceived[] PROGMEM = "MQTT-message received"; - const char trackPausedAtPos[] PROGMEM = "Track paused at position"; - const char freeHeapWithoutFtp[] PROGMEM = "Free heap before FTP-allocation"; - const char freeHeapWithFtp[] PROGMEM = "Free heap after FTP-allocation"; - const char freeHeapAfterSetup[] PROGMEM = "Free heap after setup"; - const char tryStaticIpConfig[] PROGMEM = "Performing IP-configuration..."; - const char staticIPConfigFailed[] PROGMEM = "IP-configuration failed"; - const char wakeUpRfidNoIso14443[] PROGMEM = "Wakeup caused by low power card-detection. RF-field changed but no ISO-14443 card on reader was found. So I'll return back to sleep now..."; - const char lowPowerCardSuccess[] PROGMEM = "Switch to low power card-detection: success"; - const char rememberLastVolume[] PROGMEM = "Restored volume used before last shutdown. This overwrites the initial volume configured via webgui."; - const char unableToStartFtpServer[] PROGMEM = "FTP-server cannot be started. This is because FTP-service is already active or because WiFi is unavailable."; - const char unableToTellIpAddress[] PROGMEM = "IP-address can't be announced as there's no WiFi-connection available."; - const char newPlayModeStereo[] PROGMEM = "New mode: stereo"; - const char newPlayModeMono[] PROGMEM = "New mode: mono"; - const char portExpanderFound[] PROGMEM = "Port-expander found"; - const char portExpanderNotFound[] PROGMEM = "Unable to detect port-expander"; - const char portExpanderInterruptEnabled[] PROGMEM = "Enabled interrupt-handling for port-expander"; - const char warningRefactoring[] PROGMEM = "!!!!IMPORTANT!!!! Please review https://forum.espuino.de/t/wechsel-zum-refactoring-branch-was-ist-zu-beachten/510 !!!!IMPORTANT!!!!"; - const char playlistGenModeUncached[] PROGMEM = "Playlist-generation: uncached"; - const char playlistGenModeCached[] PROGMEM = "Playlist-generation: cached"; - const char playlistCacheFoundBut0[] PROGMEM = "Playlist-cache-file found but 0 bytes"; - const char bootLoopDetected[] PROGMEM = "Bootloop detected! Last RFID won't be restored."; - const char noBootLoopDetected[] PROGMEM = "No bootloop detected. Great :-)"; - const char importCountNokNvs[] PROGMEM = "Number of invalid import-entries"; - const char errorReadingTmpfile[] PROGMEM = "Error occured while reading from import-tmpfile"; - const char errorWritingTmpfile[] PROGMEM = "Error occured while writing to import-tmpfile"; - const char eraseRfidNvsWeb[] PROGMEM = "

NVS-RFID-assignments are being deleted...
Back to last page.

"; - const char eraseRfidNvs[] PROGMEM = "NVS-RFID-assignments are being deleted..."; - const char erasePlaylistCachefile[] PROGMEM = "Playlist-cachefile deleted"; - const char fwStart[] PROGMEM = "Starting firmware-update via OTA..."; - const char fwEnd[] PROGMEM = "Firmware-update finished"; - const char otaNotSupported[] PROGMEM = "Firmware-update isn't supported by this ESPuino!"; - const char otaNotSupportedWebsite[] PROGMEM = "

FFirmware-update isn't supported by this ESPuino!
Back to last page.

"; - const char noPlaylist[] PROGMEM = "No active playlist."; - const char rfidTagRemoved[] PROGMEM = "RFID-tag removed"; - const char rfidTagReapplied[] PROGMEM = "RFID-tag reapplied"; - const char ftpEnableTooLate[] PROGMEM = "FTP can only be enabled within the first 30s after ESPuino-start. Child-protection :-)"; - const char syncingViaNtp[] PROGMEM = "Getting current time via NTP..."; - const char sdInfo[] PROGMEM = "SD card size / free space"; - const char paOn[] PROGMEM = "Loudspeaker on"; - const char paOff[] PROGMEM = "Loudspeaker off"; - const char hpOn[] PROGMEM = "Headphones on"; - const char hpOff[] PROGMEM = "Headphones off"; - const char webTxCanceled[] PROGMEM = "Webtransfer canceled due to inactivity-timer."; - const char cantConnectToWifi[] PROGMEM = "Can't connect to wifi. Trying again..."; - const char tryToPickRandomDir[] PROGMEM = "Try to pick randon subdirectory out of"; - const char pickedRandomDir[] PROGMEM = "Randomly picked subdirectory"; - const char wrongWakeUpGpio[] PROGMEM = "GPIO choosen as wakeup-pin isn't of type RTC and that reason for can't wake up ESP32!"; +const char tryConnectMqttS[] = "Trying to connect to MQTT-broker: %s"; +const char mqttOk[] = "MQTT-connection established."; +const char sleepTimerEOP[] = "Sleep-timer: after last track of playlist."; +const char sleepTimerEOT[] = "Sleep-timer: after end of current track."; +const char sleepTimerStop[] = "Sleep-timer has been disabled."; +const char sleepTimerEO5[] = "Sleep-timer: after five track or end of playlist - whatever is reached first"; +const char sleepTimerAlreadyStopped[] = "sleep-timer is already disabled."; +const char sleepTimerSetTo[] = "sleep-timer adjusted to %u minute(s)"; +const char allowButtons[] = "Unlocking all keys."; +const char lockButtons[] = "Locking all keys."; +const char noPlaylistNotAllowedMqtt[] = "Playmode cannot be adjusted to 'no playlist' via MQTT."; +const char playmodeChangedMQtt[] = "Playlist adjusted via MQTT."; +const char noPlaymodeChangeIfIdle[] = "Playlist cannot be adjusted while no playlist is active."; +const char noValidTopic[] = "No valid MQTT-topic: %s"; +const char freePtr[] = "Releasing Pointer: %s (0x%04x)"; +const char freeMemory[] = "Free memory: %u Bytes"; +const char writeEntryToNvs[] = "[%u] Storing data to NVS: %s => %s"; +const char freeMemoryAfterFree[] = "Free memory after cleaning: %u Bytes"; +const char releaseMemoryOfOldPlaylist[] = "Releasing memory of old playlist (Free memory: %u Bytes)."; +const char dirOrFileDoesNotExist[] = "File of directory does not exist: %s"; +const char unableToAllocateMemForPlaylist[] = "Unable to allocate memory for playlist!"; +const char unableToAllocateMem[] = "Unable to allocate memory!"; +const char fileModeDetected[] = "File-mode detected."; +const char nameOfFileFound[] = "File found: %s"; +const char reallocCalled[] = "Reallocated memory."; +const char unableToAllocateMemForLinearPlaylist[] = "Unable to allocate memory for linear playlist!"; +const char numberOfValidFiles[] = "Number of valid files/webstreams: %u"; +const char newLoudnessReceivedQueue[] = "New volume received via queue: %u"; +const char newCntrlReceivedQueue[] = "Control-command received via queue: %u"; +const char newPlaylistReceived[] = "New playlist received with %d track(s)"; +const char repeatTrackDueToPlaymode[] = "Repeating track due to playmode configured."; +const char repeatPlaylistDueToPlaymode[] = "Repeating playlist due to playmode configured."; +const char cmndStop[] = "Command: stop"; +const char cmndPause[] = "Command: pause"; +const char cmndResumeFromPause[] = "Command: resume"; +const char cmndNextTrack[] = "Command: next track"; +const char cmndPrevTrack[] = "Command: previous track"; +const char cmndFirstTrack[] = "Command: first track of playlist"; +const char cmndLastTrack[] = "Command: last track of playlist"; +const char cmndDoesNotExist[] = "Command requested does not exist."; +const char lastTrackAlreadyActive[] = "Already playing last track."; +const char trackStartAudiobook[] = "Starting track in playmode from the very beginning."; +const char trackStart[] = "Starting track from the very beginning."; +const char trackChangeWebstream[] = "Playing from the very beginning is not possible while webradio-mode is active."; +const char endOfPlaylistReached[] = "Reached end of playlist."; +const char trackStartatPos[] = "Starting track at position %u"; +const char waitingForTaskQueues[] = "Task Queue for RFID does not exist yet, waiting..."; +const char rfidScannerReady[] = "RFID-tags can now be applied..."; +const char rfidTagDetected[] = "RFID-tag detected: %s"; +const char rfid15693TagDetected[] = "RFID-ta (ISO-15693) detected: "; +const char rfidTagReceived[] = "RFID-tag received"; +const char dontAccepctSameRfid[] = "Reapplied same rfid-tag - rejected! (%s)"; +const char rfidTagUnknownInNvs[] = "RFID-tag is unkown to NVS."; +const char goToSleepDueToIdle[] = "Going to deepsleep due to inactivity-timer..."; +const char goToSleepDueToTimer[] = "Going to deepsleep due to sleep timer..."; +const char goToSleepNow[] = "Going to deepsleep now!"; +const char maxLoudnessReached[] = "Already reached max volume!"; +const char minLoudnessReached[] = "Already reached min volume!"; +const char errorOccured[] = "Error occured!"; +const char noMp3FilesInDir[] = "Directory does not contain mp3-files."; +const char modeSingleTrack[] = "Mode: Single track"; +const char modeSingleTrackLoop[] = "Mode: single track as infinite loop"; +const char modeSingleTrackRandom[] = "Mode: single track (random) of directory"; +const char modeSingleAudiobook[] = "Mode: audiobook"; +const char modeSingleAudiobookLoop[] = "Mode: audiobook as infinite loop"; +const char modeAllTrackAlphSorted[] = "Mode: all tracks (in alph. order) of directory '%s'"; +const char modeAllTrackRandom[] = "Mode: all tracks (in random. order) of directory '%s"; +const char modeAllTrackAlphSortedLoop[] = "Mode: all tracks (in alph. order) of directory as infinite loop"; +const char modeAllTrackRandomLoop[] = "Mode: all tracks (in random order) of directory as infinite loop"; +const char modeWebstream[] = "Mode: webstream"; +const char modeWebstreamM3u[] = "Mode: Webstream (local .m3u-file)"; +const char webstreamNotAvailable[] = "Unable to access webstream as no wifi-connection is available!"; +const char modeDoesNotExist[] = "Playmode does not exist!"; +const char modeRepeatNone[] = "Repeatmode: no repeat"; +const char modeRepeatTrack[] = "Repeatmode: current track"; +const char modeRepeatPlaylist[] = "Repeatmode: whole playlist"; +const char modeRepeatTracknPlaylist[] = "Repeatmode: track and playlist"; +const char modificatorAllButtonsLocked[] = "Modificator: locking all keys via RFID-tag."; +const char modificatorAllButtonsUnlocked[] = "Modificator: unlocking all keys via RFID-tag."; +const char modificatorSleepd[] = "Modificator: sleep-Timer deactivated."; +const char modificatorSleepTimer15[] = "Modificator: sleep-Timer enabled via RFID (15 minutes)."; +const char modificatorSleepTimer30[] = "Modificator: sleep-Timer enabled via RFID (30 minutes)."; +const char modificatorSleepTimer60[] = "Modificator: sleep-Timer enabled via RFID (60 minutes)."; +const char modificatorSleepTimer120[] = "Modificator: sleep-Timer enabled via RFID (2 hours)."; +const char ledsDimmedToNightmode[] = "Dimmed LEDs to nightmode."; +const char ledsDimmedToInitialValue[] = "Dimmed LEDs to initial value."; +const char modificatorNotallowedWhenIdle[] = "Modificator cannot be applied while playlist is inactive."; +const char modificatorSleepAtEOT[] = "Modificator: adjusted sleep-timer to after end of current track."; +const char modificatorSleepAtEOTd[] = "Modificator: disabled sleep-timer after end of current track."; +const char modificatorSleepAtEOP[] = "Modificator: adjusted sleep-timer to after end of playlist."; +const char modificatorSleepAtEOPd[] = "Modificator: disabled sleep-timer after end of playlist."; +const char modificatorAllTrackAlphSortedLoop[] = "Modificator: adjusted to all tracks (in alph. order) as infinite loop."; +const char modificatorAllTrackRandomLoop[] = "Modificator: adjusted to all tracks (in random order) as infinite loop."; +const char modificatorCurTrackLoop[] = "Modificator: adjusted to current track as infinite loop."; +const char modificatorCurAudiobookLoop[] = "Modificator: adjusted to current audiobook as infinite loop."; +const char modificatorPlaylistLoopActive[] = "Modificator: adjusted to all tracks as infinite loop."; +const char modificatorPlaylistLoopDeactive[] = "Modificator: disabled all tracks as infinite loop."; +const char modificatorTrackActive[] = "Modificator: adjusted to current track as infinite loop."; +const char modificatorTrackDeactive[] = "Modificator: disabled current track as infinite loop."; +const char modificatorNotAllowed[] = "Unable to apply modificator."; +const char modificatorLoopRev[] = "Modificator: infinite loop ended."; +const char modificatorDoesNotExist[] = "This type of card-modificator does not exist %d!"; +const char errorOccuredNvs[] = "Error occured while reading from NVS!"; +const char statementsReceivedByServer[] = "Data received from server"; +const char apReady[] = "Started wifi-access-point"; +const char httpReady[] = "Started HTTP-server."; +const char unableToMountSd[] = "Unable to mount sd-card."; +const char unableToCreateVolQ[] = "Unable to create volume-queue."; +const char unableToCreateRfidQ[] = "Unable to create RFID-queue."; +const char unableToCreateMgmtQ[] = "Unable to play-management-queue."; +const char unableToCreatePlayQ[] = "Unable to create track-queue.."; +const char initialBrightnessfromNvs[] = "Restoring initial LED-brightness from NVS: %u"; +const char wroteInitialBrightnessToNvs[] = "Storing initial LED-brightness to NVS."; +const char restoredInitialBrightnessForNmFromNvs[] = "Restored LED-brightness for nightmode from NVS: %u"; +const char wroteNmBrightnessToNvs[] = "Stored LED-brightness for nightmode to NVS."; +const char wroteFtpUserToNvs[] = "Stored FTP-user to NVS."; +const char restoredFtpUserFromNvs[] = "Restored FTP-user from NVS: %s"; +const char wroteFtpPwdToNvs[] = "Stored FTP-password to NVS."; +const char restoredFtpPwdFromNvs[] = "Restored FTP-password from NVS: %s"; +const char restoredMaxInactivityFromNvs[] = "Restored maximum inactivity-time from NVS: %u minutes"; +const char wroteMaxInactivityToNvs[] = "Stored maximum inactivity-time to NVS."; +const char restoredInitialLoudnessFromNvs[] = "Restored initial volume from NVS: %u"; +const char wroteInitialLoudnessToNvs[] = "Stored initial volume to NVS."; +const char restoredMaxLoudnessForSpeakerFromNvs[] = "Restored maximum volume for speaker from NVS: %u"; +const char restoredMaxLoudnessForHeadphoneFromNvs[] = "Restored maximum volume for headphone from NVS: %u"; +const char wroteMaxLoudnessForSpeakerToNvs[] = "Wrote maximum volume for speaker to NVS."; +const char wroteMaxLoudnessForHeadphoneToNvs[] = "Wrote maximum volume for headphone to NVS."; +const char maxVolumeSet[] = "Maximum volume set to: %u"; +const char wroteMqttFlagToNvs[] = "Stored MQTT-flag to NVS."; +const char restoredMqttActiveFromNvs[] = "Restored MQTT-flag (enabled) from NVS: %u"; +const char restoredMqttDeactiveFromNvs[] = "Restored MQTT-flag (disabled) from NVS: %u"; +const char wroteMqttClientIdToNvs[] = "Stored MQTT-clientid to NVS."; +const char restoredMqttClientIdFromNvs[] = "Restored MQTT-clientid from NVS: %s"; +const char wroteMqttServerToNvs[] = "Stored MQTT-server to NVS."; +const char restoredMqttServerFromNvs[] = "Restored MQTT-Server from NVS: %s"; +const char wroteMqttUserToNvs[] = "Stored MQTT-user to NVS."; +const char restoredMqttUserFromNvs[] = "Restored MQTT-user from NVS: %s"; +const char wroteMqttPwdToNvs[] = "Stored MQTT-password to NVS."; +const char restoredMqttPwdFromNvs[] = "Restored MQTT-password from NVS: %s"; +const char restoredMqttPortFromNvs[] = "Restored MQTT-port from NVS: %u"; +const char mqttWithPwd[] = "Try to connect to MQTT-server with user und password"; +const char mqttWithoutPwd[] = "Try to connect to MQTT-server without user und password"; +const char ssidNotFoundInNvs[] = "Unable to find SSID to NVS."; +const char wifiStaticIpConfigNotFoundInNvs[] = "Unable to find wifi-ip-configuration to NVS."; +const char wifiHostnameNotSet[] = "Unable to find hostname-configuration to NVS."; +const char mqttConnFailed[] = "Unable to establish mqtt-connection, trying again: rc=%i (%d / %d)"; +const char restoredHostnameFromNvs[] = "Restored hostname from NVS: %s"; +const char currentVoltageMsg[] = "Current battery-voltage: %.2f V"; +const char currentChargeMsg[] = "Current battery charge: %.2f %%"; +const char batteryCurrentMsg[] = "Power usage (Battery): %.2f mA"; +const char batteryTempMsg[] = "Battery temperature: %.2f °C"; +const char batteryCyclesMsg[] = "Seen battery cycles: %.2f"; +const char batteryLowMsg[] = "Battery charge low"; +const char batteryCriticalMsg[] = "Battery charge critical. Will go to deepsleep..."; +const char sdBootFailedDeepsleep[] = "Failed to boot due to SD. Will go to deepsleep..."; +const char wifiEnabledMsg[] = "WiFi will be enabled."; +const char wifiDisabledMsg[] = "WiFi will be disabled ."; +const char voltageIndicatorLowFromNVS[] = "Restored lower voltage-level for Neopixel-display from NVS: %.2fV"; +const char voltageIndicatorHighFromNVS[] = "Restored upper voltage-level for Neopixel-display from NVS: %.2fV"; +const char batteryCheckIntervalFromNVS[] = "Restored interval of battery-measurement or Neopixel-display from NVS: %u minutes"; +const char warningLowVoltageFromNVS[] = "Restored low battery-voltage-level for warning via Neopixel from NVS: %.2fV"; +const char warningCriticalVoltageFromNVS[] = "Restored critical battery-voltage-level for warning via Neopixel from NVS: %.2fV"; +const char batteryLowFromNVS[] = "Restored low battery level from NVS: %.2f %%"; +const char batteryCriticalFromNVS[] = "Restored critical battery level from NVS: %.2f %%"; +const char unableToRestoreLastRfidFromNVS[] = "Unable to restore last RFID from NVS"; +const char restoredLastRfidFromNVS[] = "Restored last RFID from NVS: %s"; +const char failedOpenFileForWrite[] = "Failed to open file for writing"; +const char fileWritten[] = "File written: %s => %zu bytes in %lu ms (%lu kiB/s)"; +const char writeFailed[] = "Write failed"; +const char writingFile[] = "Writing file: %s"; +const char failedToOpenFileForAppending[] = "Failed to open file for appending"; +const char listingDirectory[] = "Listing directory"; +const char failedToOpenDirectory[] = "Failed to open directory"; +const char notADirectory[] = "Not a directory"; +const char sdMountedMmc1BitMode[] = "SD card mounted in SD-MMC 1-Bit mode..."; +const char sdMountedSpiMode[] = "Mounting SD card in SPI-mode..."; +const char restartWebsite[] = "

ESPuino is being restarted...
Back to last page.

"; +const char shutdownWebsite[] = "Der ESPuino is being shutdown..."; +const char mqttMsgReceived[] = "MQTT-message received: [Topic: %s] [Command: %s]"; +const char trackPausedAtPos[] = "Track paused at position: %u (%u)"; +const char freeHeapWithoutFtp[] = "Free heap before FTP-allocation: %u"; +const char freeHeapWithFtp[] = "Free heap after FTP-allocation: %u"; +const char freeHeapAfterSetup[] = "Free heap after setup"; +const char ftpServerStarted[] = "FTP-Server started"; +const char tryStaticIpConfig[] = "Performing IP-configuration..."; +const char staticIPConfigFailed[] = "IP-configuration failed"; +const char wakeUpRfidNoIso14443[] = "Wakeup caused by low power card-detection. RF-field changed but no ISO-14443 card on reader was found. So I'll return back to sleep now..."; +const char lowPowerCardSuccess[] = "Switch to low power card-detection: success"; +const char rememberLastVolume[] = "Restored volume used before last shutdown. This overwrites the initial volume configured via webgui."; +const char unableToStartFtpServer[] = "FTP-server cannot be started. This is because FTP-service is already active or because WiFi is unavailable."; +const char unableToTellIpAddress[] = "IP-address can't be announced as there's no WiFi-connection available."; +const char unableToTellTime[] = "Time can't be announced as there's no WiFi-connection available."; +const char newPlayModeStereo[] = "New mode: stereo"; +const char newPlayModeMono[] = "New mode: mono"; +const char portExpanderFound[] = "Port-expander found"; +const char portExpanderNotFound[] = "Unable to detect port-expander"; +const char portExpanderInterruptEnabled[] = "Enabled interrupt-handling for port-expander"; +const char playlistGen[] = "Playlist-generation"; +const char bootLoopDetected[] = "Bootloop detected! Last RFID won't be restored."; +const char noBootLoopDetected[] = "No bootloop detected. Great :-)"; +const char importCountNokNvs[] = "Number of invalid import-entries: %u"; +const char errorReadingTmpfile[] = "Error occured while reading from import-tmpfile"; +const char errorWritingTmpfile[] = "Error occured while writing to import-tmpfile"; +const char eraseRfidNvs[] = "NVS-RFID-assignments are being deleted..."; +const char fwStart[] = "Starting firmware-update via OTA..."; +const char fwEnd[] = "Firmware-update finished"; +const char otaNotSupported[] = "Firmware-update isn't supported by this ESPuino!"; +const char otaNotSupportedWebsite[] = "

FFirmware-update isn't supported by this ESPuino!
Back to last page.

"; +const char noPlaylist[] = "No active playlist."; +const char rfidTagRemoved[] = "RFID-tag removed"; +const char rfidTagReapplied[] = "RFID-tag reapplied"; +const char ftpEnableTooLate[] = "FTP can only be enabled within the first 30s after ESPuino-start. Child-protection :-)"; +const char dateTimeRTC[] = "Date/time (Internal RTC)): %02d.%02d.%4d, %02d:%02d:%02d"; +const char syncingViaNtp[] = "Getting current time via NTP..."; +const char ntpGotTime[] = "Received date/time from NTP server: %02d.%02d.%4d, %02d:%02d:%02d"; +const char ntpFailed[] = "Failed to obtain NTP time"; +const char sdInfo[] = "SD card size / free space: %llu MB / %llu MB"; +const char paOn[] = "Loudspeaker on"; +const char paOff[] = "Loudspeaker off"; +const char hpOn[] = "Headphones on"; +const char hpOff[] = "Headphones off"; +const char webTxCanceled[] = "Webtransfer canceled due to inactivity-timer."; +const char tryToPickRandomDir[] = "Try to pick randon subdirectory out of: %s"; +const char pickedRandomDir[] = "Randomly picked subdirectory: %s"; +const char wrongWakeUpGpio[] = "GPIO choosen as wakeup-pin isn't of type RTC and that reason for can't wake up ESP32! (GPIO: %u)"; +const char currentlyPlaying[] = "'%s' is being played (%d of %d)"; +const char secondsJumpForward[] = "Jumped %d seconds forwards"; +const char secondsJumpBackward[] = "Jumped %d seconds backward"; +const char JumpToPosition[] = "Jumped to position %u/%u"; +const char wroteLastTrackToNvs[] = "Write '%s' to NVS for RFID-Card-ID %s with playmode %d and last track %u"; +const char wifiConnectionInProgress[] = "Try to connect to WiFi with SSID '%s'..."; +const char wifiConnectionSuccess[] = "Connected with WiFi '%s' (signal strength: %d dBm, channel: %d, BSSID: %s)"; +const char wifiCurrentIp[] = "Current IP: %s"; +const char jsonErrorMsg[] = "deserializeJson() failed: %s"; +const char wifiDeleteNetwork[] = "Deleting saved WiFi %s"; +const char wifiNetworkLoaded[] = "Loaded SSID %d from NVS: %s"; +const char wifiTooManyNetworks[] = "Number of networks in NVS is %d, but that's more than the allowed %d"; +const char wifiAddTooManyNetworks[] = "No space left to add another WiFi network!"; +const char wifiAddNetwork[] = "Add WiFi network %s"; +const char wifiUpdateNetwork[] = "Update WiFi network %s"; +const char wifiScanResult[] = "Found WiFi with SSID %s and signal strength %d dBm on channel %d, BSSID %s."; +const char cantConnectToWifi[] = "Failed to connect to WiFi."; +const char wifiSetLastSSID[] = "Write last successful SSID to NVS for WiFi fast-path: %s"; +const char mDNSStarted[] = "mDNS started: http://%s.local"; +const char mDNSFailed[] = "mDNS failure, hostname: %s"; #endif diff --git a/src/MemX.cpp b/src/MemX.cpp index edad521f..91c1c4b9 100644 --- a/src/MemX.cpp +++ b/src/MemX.cpp @@ -1,13 +1,14 @@ #include + #include "MemX.h" // Wraps strdup(). Without PSRAM, strdup is called => so heap is used. // With PSRAM being available, the same is done what strdup() does, but with allocation on PSRAM. -char * x_strdup(const char *_str) { +char *x_strdup(const char *_str) { if (!psramInit()) { return strdup(_str); } else { - char *dst = (char *) ps_malloc(strlen (_str) + 1); + char *dst = (char *) ps_malloc(strlen(_str) + 1); if (dst == NULL) { return NULL; } @@ -17,7 +18,7 @@ char * x_strdup(const char *_str) { } // Wraps ps_malloc() and malloc(). Selection depends on whether PSRAM is available or not. -char * x_malloc(uint32_t _allocSize) { +char *x_malloc(uint32_t _allocSize) { if (psramInit()) { return (char *) ps_malloc(_allocSize); } else { @@ -25,9 +26,8 @@ char * x_malloc(uint32_t _allocSize) { } } - // Wraps ps_calloc() and calloc(). Selection depends on whether PSRAM is available or not. -char * x_calloc(uint32_t _allocSize, uint32_t _unitSize) { +char *x_calloc(uint32_t _allocSize, uint32_t _unitSize) { if (psramInit()) { return (char *) ps_calloc(_allocSize, _unitSize); } else { diff --git a/src/Mqtt.cpp b/src/Mqtt.cpp index 1ca74974..17161ac3 100644 --- a/src/Mqtt.cpp +++ b/src/Mqtt.cpp @@ -1,16 +1,19 @@ #include -#include #include "settings.h" + #include "Mqtt.h" + #include "AudioPlayer.h" #include "Led.h" #include "Log.h" #include "MemX.h" -#include "System.h" #include "Queues.h" +#include "System.h" #include "Wlan.h" #include "revision.h" +#include + #ifdef MQTT_ENABLE #define MQTT_SOCKET_TIMEOUT 1 // https://github.com/knolleary/pubsubclient/issues/403 #include @@ -18,122 +21,120 @@ // MQTT-helper #ifdef MQTT_ENABLE - static WiFiClient Mqtt_WifiClient; - static PubSubClient Mqtt_PubSubClient(Mqtt_WifiClient); - // Please note: all of them are defaults that can be changed later via GUI - String gMqttClientId = DEVICE_HOSTNAME; // ClientId for the MQTT-server, must be server wide unique (if not found in NVS this one will be taken) - String gMqttServer = "192.168.2.43"; // IP-address of MQTT-server (if not found in NVS this one will be taken) - String gMqttUser = "mqtt-user"; // MQTT-user - String gMqttPassword = "mqtt-password"; // MQTT-password - uint16_t gMqttPort = 1883; // MQTT-Port +static WiFiClient Mqtt_WifiClient; +static PubSubClient Mqtt_PubSubClient(Mqtt_WifiClient); +// Please note: all of them are defaults that can be changed later via GUI +String gMqttClientId = DEVICE_HOSTNAME; // ClientId for the MQTT-server, must be server wide unique (if not found in NVS this one will be taken) +String gMqttServer = "192.168.2.43"; // IP-address of MQTT-server (if not found in NVS this one will be taken) +String gMqttUser = "mqtt-user"; // MQTT-user +String gMqttPassword = "mqtt-password"; // MQTT-password +uint16_t gMqttPort = 1883; // MQTT-Port #endif // MQTT static bool Mqtt_Enabled = true; +#ifdef MQTT_ENABLE static void Mqtt_ClientCallback(const char *topic, const byte *payload, uint32_t length); static bool Mqtt_Reconnect(void); static void Mqtt_PostWiFiRssi(void); +#endif void Mqtt_Init() { - #ifdef MQTT_ENABLE - // Get MQTT-enable from NVS - uint8_t nvsEnableMqtt = gPrefsSettings.getUChar("enableMQTT", 99); - switch (nvsEnableMqtt) { - case 99: - gPrefsSettings.putUChar("enableMQTT", Mqtt_Enabled); - Log_Println((char *) FPSTR(wroteMqttFlagToNvs), LOGLEVEL_ERROR); - break; - case 1: - Mqtt_Enabled = nvsEnableMqtt; - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredMqttActiveFromNvs), nvsEnableMqtt); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - break; - case 0: - Mqtt_Enabled = nvsEnableMqtt; - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredMqttDeactiveFromNvs), nvsEnableMqtt); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - break; - } - - // Get MQTT-clientid from NVS - String nvsMqttClientId = gPrefsSettings.getString("mqttClientId", "-1"); - if (!nvsMqttClientId.compareTo("-1")) { - gPrefsSettings.putString("mqttClientId", gMqttClientId); - Log_Println((char *) FPSTR(wroteMqttClientIdToNvs), LOGLEVEL_ERROR); - } else { - gMqttClientId = nvsMqttClientId; - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredMqttClientIdFromNvs), nvsMqttClientId.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - - // Get MQTT-server from NVS - String nvsMqttServer = gPrefsSettings.getString("mqttServer", "-1"); - if (!nvsMqttServer.compareTo("-1")) { - gPrefsSettings.putString("mqttServer", gMqttServer); - Log_Println((char *) FPSTR(wroteMqttServerToNvs), LOGLEVEL_ERROR); - } else { - gMqttServer = nvsMqttServer; - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredMqttServerFromNvs), nvsMqttServer.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - - // Get MQTT-user from NVS - String nvsMqttUser = gPrefsSettings.getString("mqttUser", "-1"); - if (!nvsMqttUser.compareTo("-1")) { - gPrefsSettings.putString("mqttUser", (String)gMqttUser); - Log_Println((char *) FPSTR(wroteMqttUserToNvs), LOGLEVEL_ERROR); - } else { - gMqttUser = nvsMqttUser; - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredMqttUserFromNvs), nvsMqttUser.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - - // Get MQTT-password from NVS - String nvsMqttPassword = gPrefsSettings.getString("mqttPassword", "-1"); - if (!nvsMqttPassword.compareTo("-1")) { - gPrefsSettings.putString("mqttPassword", (String)gMqttPassword); - Log_Println((char *) FPSTR(wroteMqttPwdToNvs), LOGLEVEL_ERROR); - } else { - gMqttPassword = nvsMqttPassword; - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredMqttPwdFromNvs), nvsMqttPassword.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - - // Get MQTT-password from NVS - uint32_t nvsMqttPort = gPrefsSettings.getUInt("mqttPort", 99999); - if (nvsMqttPort == 99999) { - gPrefsSettings.putUInt("mqttPort", gMqttPort); - } else { - gMqttPort = nvsMqttPort; - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredMqttPortFromNvs), gMqttPort); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } - - // Only enable MQTT if requested - if (Mqtt_Enabled) { - Mqtt_PubSubClient.setServer(gMqttServer.c_str(), gMqttPort); - Mqtt_PubSubClient.setCallback(Mqtt_ClientCallback); - } - #endif +#ifdef MQTT_ENABLE + // Get MQTT-enable from NVS + uint8_t nvsEnableMqtt = gPrefsSettings.getUChar("enableMQTT", 99); + switch (nvsEnableMqtt) { + case 99: + gPrefsSettings.putUChar("enableMQTT", Mqtt_Enabled); + Log_Println(wroteMqttFlagToNvs, LOGLEVEL_ERROR); + break; + case 1: + Mqtt_Enabled = nvsEnableMqtt; + Log_Printf(LOGLEVEL_INFO, restoredMqttActiveFromNvs, nvsEnableMqtt); + break; + case 0: + Mqtt_Enabled = nvsEnableMqtt; + Log_Printf(LOGLEVEL_INFO, restoredMqttDeactiveFromNvs, nvsEnableMqtt); + break; + } + + // Get MQTT-clientid from NVS + String nvsMqttClientId = gPrefsSettings.getString("mqttClientId", "-1"); + if (!nvsMqttClientId.compareTo("-1")) { + gPrefsSettings.putString("mqttClientId", gMqttClientId); + Log_Println(wroteMqttClientIdToNvs, LOGLEVEL_ERROR); + } else { + gMqttClientId = nvsMqttClientId; + Log_Printf(LOGLEVEL_INFO, restoredMqttClientIdFromNvs, nvsMqttClientId.c_str()); + } + + // Get MQTT-server from NVS + String nvsMqttServer = gPrefsSettings.getString("mqttServer", "-1"); + if (!nvsMqttServer.compareTo("-1")) { + gPrefsSettings.putString("mqttServer", gMqttServer); + Log_Println(wroteMqttServerToNvs, LOGLEVEL_ERROR); + } else { + gMqttServer = nvsMqttServer; + Log_Printf(LOGLEVEL_INFO, restoredMqttServerFromNvs, nvsMqttServer.c_str()); + } + + // Get MQTT-user from NVS + String nvsMqttUser = gPrefsSettings.getString("mqttUser", "-1"); + if (!nvsMqttUser.compareTo("-1")) { + gPrefsSettings.putString("mqttUser", (String) gMqttUser); + Log_Println(wroteMqttUserToNvs, LOGLEVEL_ERROR); + } else { + gMqttUser = nvsMqttUser; + Log_Printf(LOGLEVEL_INFO, restoredMqttUserFromNvs, nvsMqttUser.c_str()); + } + + // Get MQTT-password from NVS + String nvsMqttPassword = gPrefsSettings.getString("mqttPassword", "-1"); + if (!nvsMqttPassword.compareTo("-1")) { + gPrefsSettings.putString("mqttPassword", (String) gMqttPassword); + Log_Println(wroteMqttPwdToNvs, LOGLEVEL_ERROR); + } else { + gMqttPassword = nvsMqttPassword; + Log_Printf(LOGLEVEL_INFO, restoredMqttPwdFromNvs, nvsMqttPassword.c_str()); + } + + // Get MQTT-password from NVS + uint32_t nvsMqttPort = gPrefsSettings.getUInt("mqttPort", 99999); + if (nvsMqttPort == 99999) { + gPrefsSettings.putUInt("mqttPort", gMqttPort); + } else { + gMqttPort = nvsMqttPort; + Log_Printf(LOGLEVEL_INFO, restoredMqttPortFromNvs, gMqttPort); + } + + // Only enable MQTT if requested + if (Mqtt_Enabled) { + Mqtt_PubSubClient.setServer(gMqttServer.c_str(), gMqttPort); + Mqtt_PubSubClient.setCallback(Mqtt_ClientCallback); + } +#else + Mqtt_Enabled = false; +#endif } void Mqtt_Cyclic(void) { - #ifdef MQTT_ENABLE - if (Mqtt_Enabled && Wlan_IsConnected()) { - Mqtt_Reconnect(); - Mqtt_PubSubClient.loop(); - Mqtt_PostWiFiRssi(); - } - #endif +#ifdef MQTT_ENABLE + if (Mqtt_Enabled && Wlan_IsConnected()) { + Mqtt_Reconnect(); + Mqtt_PubSubClient.loop(); + Mqtt_PostWiFiRssi(); + } +#endif } void Mqtt_Exit(void) { - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicState), "Offline", false); - publishMqtt((char *) FPSTR(topicTrackState), "---", false); - Mqtt_PubSubClient.disconnect(); - #endif +#ifdef MQTT_ENABLE + Log_Println("shutdown MQTT..", LOGLEVEL_NOTICE); + publishMqtt(topicState, "Offline", false); + publishMqtt(topicTrackState, "---", false); + Mqtt_PubSubClient.disconnect(); +#endif } bool Mqtt_IsEnabled(void) { @@ -142,324 +143,319 @@ bool Mqtt_IsEnabled(void) { /* Wrapper-functions for MQTT-publish */ bool publishMqtt(const char *topic, const char *payload, bool retained) { - #ifdef MQTT_ENABLE - if (strcmp(topic, "") != 0) { - if (Mqtt_PubSubClient.connected()) { - Mqtt_PubSubClient.publish(topic, payload, retained); - //delay(100); - return true; - } +#ifdef MQTT_ENABLE + if (strcmp(topic, "") != 0) { + if (Mqtt_PubSubClient.connected()) { + Mqtt_PubSubClient.publish(topic, payload, retained); + // delay(100); + return true; } - #endif + } +#endif return false; } bool publishMqtt(const char *topic, int32_t payload, bool retained) { - #ifdef MQTT_ENABLE - char buf[11]; - snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%d", payload); - return publishMqtt(topic, buf, retained); - #else - return false; - #endif +#ifdef MQTT_ENABLE + char buf[11]; + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%d", payload); + return publishMqtt(topic, buf, retained); +#else + return false; +#endif } bool publishMqtt(const char *topic, unsigned long payload, bool retained) { - #ifdef MQTT_ENABLE - char buf[11]; - snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%lu", payload); - return publishMqtt(topic, buf, retained); - #else - return false; - #endif +#ifdef MQTT_ENABLE + char buf[11]; + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%lu", payload); + return publishMqtt(topic, buf, retained); +#else + return false; +#endif } bool publishMqtt(const char *topic, uint32_t payload, bool retained) { - #ifdef MQTT_ENABLE - char buf[11]; - snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%u", payload); - return publishMqtt(topic, buf, retained); - #else - return false; - #endif +#ifdef MQTT_ENABLE + char buf[11]; + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%u", payload); + return publishMqtt(topic, buf, retained); +#else + return false; +#endif } // Cyclic posting of WiFi-signal-strength void Mqtt_PostWiFiRssi(void) { - #ifdef MQTT_ENABLE - static uint32_t lastMqttRssiTimestamp = 0; +#ifdef MQTT_ENABLE + static uint32_t lastMqttRssiTimestamp = 0; - if (!lastMqttRssiTimestamp || (millis() - lastMqttRssiTimestamp >= 60000)) { - lastMqttRssiTimestamp = millis(); - publishMqtt((char *) FPSTR(topicWiFiRssiState), Wlan_GetRssi(), false); - } - #endif + if (!lastMqttRssiTimestamp || (millis() - lastMqttRssiTimestamp >= 60000)) { + lastMqttRssiTimestamp = millis(); + publishMqtt(topicWiFiRssiState, Wlan_GetRssi(), false); + } +#endif } /* Connects/reconnects to MQTT-Broker unless connection is not already available. Manages MQTT-subscriptions. */ bool Mqtt_Reconnect() { - #ifdef MQTT_ENABLE - static uint32_t mqttLastRetryTimestamp = 0u; - uint8_t connect = false; - uint8_t i = 0; +#ifdef MQTT_ENABLE + static uint32_t mqttLastRetryTimestamp = 0u; + uint8_t connect = false; + uint8_t i = 0; - if (!mqttLastRetryTimestamp || millis() - mqttLastRetryTimestamp >= mqttRetryInterval * 1000) { - mqttLastRetryTimestamp = millis(); - } else { - return false; - } + if (!mqttLastRetryTimestamp || millis() - mqttLastRetryTimestamp >= mqttRetryInterval * 1000) { + mqttLastRetryTimestamp = millis(); + } else { + return false; + } - while (!Mqtt_PubSubClient.connected() && i < mqttMaxRetriesPerInterval) { - i++; - snprintf(Log_Buffer, Log_BufferLength, "%s %s", (char *) FPSTR(tryConnectMqttS), gMqttServer.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + while (!Mqtt_PubSubClient.connected() && i < mqttMaxRetriesPerInterval) { + i++; + Log_Printf(LOGLEVEL_NOTICE, tryConnectMqttS, gMqttServer.c_str()); - // Try to connect to MQTT-server. If username AND password are set, they'll be used - if ((gMqttUser.length() < 1u) || (gMqttPassword.length()) < 1u) { - Log_Println((char *) FPSTR(mqttWithoutPwd), LOGLEVEL_NOTICE); - if (Mqtt_PubSubClient.connect(gMqttClientId.c_str())) { - connect = true; - } - } else { - Log_Println((char *) FPSTR(mqttWithPwd), LOGLEVEL_NOTICE); - if (Mqtt_PubSubClient.connect(gMqttClientId.c_str(), gMqttUser.c_str(), gMqttPassword.c_str(), (char *) FPSTR(topicState), 0, false, "Offline")) { - connect = true; - } + // Try to connect to MQTT-server. If username AND password are set, they'll be used + if ((gMqttUser.length() < 1u) || (gMqttPassword.length()) < 1u) { + Log_Println(mqttWithoutPwd, LOGLEVEL_NOTICE); + if (Mqtt_PubSubClient.connect(gMqttClientId.c_str())) { + connect = true; } - if (connect) { - Log_Println((char *) FPSTR(mqttOk), LOGLEVEL_NOTICE); + } else { + Log_Println(mqttWithPwd, LOGLEVEL_NOTICE); + if (Mqtt_PubSubClient.connect(gMqttClientId.c_str(), gMqttUser.c_str(), gMqttPassword.c_str(), topicState, 0, false, "Offline")) { + connect = true; + } + } + if (connect) { + Log_Println(mqttOk, LOGLEVEL_NOTICE); - // Deepsleep-subscription - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicSleepCmnd)); + // Deepsleep-subscription + Mqtt_PubSubClient.subscribe(topicSleepCmnd); - // RFID-Tag-ID-subscription - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicRfidCmnd)); + // RFID-Tag-ID-subscription + Mqtt_PubSubClient.subscribe(topicRfidCmnd); - // Loudness-subscription - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicLoudnessCmnd)); + // Loudness-subscription + Mqtt_PubSubClient.subscribe(topicLoudnessCmnd); - // Sleep-Timer-subscription - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicSleepTimerCmnd)); + // Sleep-Timer-subscription + Mqtt_PubSubClient.subscribe(topicSleepTimerCmnd); - // Next/previous/stop/play-track-subscription - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicTrackControlCmnd)); + // Next/previous/stop/play-track-subscription + Mqtt_PubSubClient.subscribe(topicTrackControlCmnd); - // Lock controls - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicLockControlsCmnd)); + // Lock controls + Mqtt_PubSubClient.subscribe(topicLockControlsCmnd); - // Current repeat-Mode - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicRepeatModeCmnd)); + // Current repeat-Mode + Mqtt_PubSubClient.subscribe(topicRepeatModeCmnd); - // LED-brightness - Mqtt_PubSubClient.subscribe((char *) FPSTR(topicLedBrightnessCmnd)); + // LED-brightness + Mqtt_PubSubClient.subscribe(topicLedBrightnessCmnd); - // Publish current state - publishMqtt((char *) FPSTR(topicState), "Online", false); - publishMqtt((char *) FPSTR(topicTrackState), gPlayProperties.title, false); - publishMqtt((char *) FPSTR(topicCoverChangedState), "", false); - publishMqtt((char *) FPSTR(topicLoudnessState), AudioPlayer_GetCurrentVolume(), false); - publishMqtt((char *) FPSTR(topicSleepTimerState), System_GetSleepTimerTimeStamp(), false); - publishMqtt((char *) FPSTR(topicLockControlsState), System_AreControlsLocked(), false); - publishMqtt((char *) FPSTR(topicPlaymodeState), gPlayProperties.playMode, false); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - publishMqtt((char *) FPSTR(topicCurrentIPv4IP), Wlan_GetIpAddress().c_str(), false); - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); + // Publish current state + publishMqtt(topicState, "Online", false); + publishMqtt(topicTrackState, gPlayProperties.title, false); + publishMqtt(topicCoverChangedState, "", false); + publishMqtt(topicLoudnessState, AudioPlayer_GetCurrentVolume(), false); + publishMqtt(topicSleepTimerState, System_GetSleepTimerTimeStamp(), false); + publishMqtt(topicLockControlsState, System_AreControlsLocked(), false); + publishMqtt(topicPlaymodeState, gPlayProperties.playMode, false); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); + publishMqtt(topicCurrentIPv4IP, Wlan_GetIpAddress().c_str(), false); + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); - char revBuf[12]; - strncpy(revBuf, softwareRevision+19, sizeof(revBuf)-1); - revBuf[sizeof(revBuf)-1] = '\0'; - publishMqtt((char *) FPSTR(topicSRevisionState), revBuf, false); + char revBuf[12]; + strncpy(revBuf, softwareRevision + 19, sizeof(revBuf) - 1); + revBuf[sizeof(revBuf) - 1] = '\0'; + publishMqtt(topicSRevisionState, revBuf, false); - return Mqtt_PubSubClient.connected(); - } else { - snprintf(Log_Buffer, Log_BufferLength, "%s: rc=%i (%d / %d)", (char *) FPSTR(mqttConnFailed), Mqtt_PubSubClient.state(), i, mqttMaxRetriesPerInterval); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - } + return Mqtt_PubSubClient.connected(); + } else { + Log_Printf(LOGLEVEL_ERROR, mqttConnFailed, Mqtt_PubSubClient.state(), i, mqttMaxRetriesPerInterval); } - return false; - #else - return false; - #endif + } + return false; +#else + return false; +#endif } // Is called if there's a new MQTT-message for us void Mqtt_ClientCallback(const char *topic, const byte *payload, uint32_t length) { - #ifdef MQTT_ENABLE - char *receivedString = (char*)x_calloc(length + 1u, sizeof(char)); - memcpy(receivedString, (char *) payload, length); - char *mqttTopic = x_strdup(topic); - - snprintf(Log_Buffer, Log_BufferLength, "%s: [Topic: %s] [Command: %s]", (char *) FPSTR(mqttMsgReceived), mqttTopic, receivedString); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - - // Go to sleep? - if (strcmp_P(topic, topicSleepCmnd) == 0) { - if ((strcmp(receivedString, "OFF") == 0) || (strcmp(receivedString, "0") == 0)) { - System_RequestSleep(); - } - } - // New track to play? Take RFID-ID as input - else if (strcmp_P(topic, topicRfidCmnd) == 0) { - char *_rfidId = x_strdup(receivedString); - xQueueSend(gRfidCardQueue, _rfidId, 0); +#ifdef MQTT_ENABLE + char *receivedString = (char *) x_calloc(length + 1u, sizeof(char)); + memcpy(receivedString, (char *) payload, length); + + Log_Printf(LOGLEVEL_INFO, mqttMsgReceived, topic, receivedString); + + // Go to sleep? + if (strcmp_P(topic, topicSleepCmnd) == 0) { + if ((strcmp(receivedString, "OFF") == 0) || (strcmp(receivedString, "0") == 0)) { + System_RequestSleep(); } - // Loudness to change? - else if (strcmp_P(topic, topicLoudnessCmnd) == 0) { - unsigned long vol = strtoul(receivedString, NULL, 10); - AudioPlayer_VolumeToQueueSender(vol, true); + } + // New track to play? Take RFID-ID as input + else if (strcmp_P(topic, topicRfidCmnd) == 0) { + xQueueSend(gRfidCardQueue, receivedString, 0); + } + // Loudness to change? + else if (strcmp_P(topic, topicLoudnessCmnd) == 0) { + unsigned long vol = strtoul(receivedString, NULL, 10); + AudioPlayer_VolumeToQueueSender(vol, true); + } + // Modify sleep-timer? + else if (strcmp_P(topic, topicSleepTimerCmnd) == 0) { + if (gPlayProperties.playMode == NO_PLAYLIST) { // Don't allow sleep-modications if no playlist is active + Log_Println(modificatorNotallowedWhenIdle, LOGLEVEL_INFO); + publishMqtt(topicSleepState, 0, false); + System_IndicateError(); + free(receivedString); + return; } - // Modify sleep-timer? - else if (strcmp_P(topic, topicSleepTimerCmnd) == 0) { - if (gPlayProperties.playMode == NO_PLAYLIST) { // Don't allow sleep-modications if no playlist is active - Log_Println((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_INFO); - publishMqtt((char *) FPSTR(topicSleepState), 0, false); - System_IndicateError(); - return; - } - if (strcmp(receivedString, "EOP") == 0) { - gPlayProperties.sleepAfterPlaylist = true; - Log_Println((char *) FPSTR(sleepTimerEOP), LOGLEVEL_NOTICE); - publishMqtt((char *) FPSTR(topicSleepTimerState), "EOP", false); - Led_ResetToNightBrightness(); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - System_IndicateOk(); - return; - } else if (strcmp(receivedString, "EOT") == 0) { - gPlayProperties.sleepAfterCurrentTrack = true; - Log_Println((char *) FPSTR(sleepTimerEOT), LOGLEVEL_NOTICE); - publishMqtt((char *) FPSTR(topicSleepTimerState), "EOT", false); - Led_ResetToNightBrightness(); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - System_IndicateOk(); - return; - } else if (strcmp(receivedString, "EO5T") == 0) { - if ((gPlayProperties.numberOfTracks - 1) >= (gPlayProperties.currentTrackNumber + 5)) { - gPlayProperties.playUntilTrackNumber = gPlayProperties.currentTrackNumber + 5; - } else { - gPlayProperties.sleepAfterPlaylist = true; // If +5 tracks is > than active playlist, take end of current playlist - } - Log_Println((char *) FPSTR(sleepTimerEO5), LOGLEVEL_NOTICE); - publishMqtt((char *) FPSTR(topicSleepTimerState), "EO5T", false); - Led_ResetToNightBrightness(); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - System_IndicateOk(); - return; - } else if (strcmp(receivedString, "0") == 0) { // Disable sleep after it was active previously - if (System_IsSleepTimerEnabled()) { - System_DisableSleepTimer(); - Log_Println((char *) FPSTR(sleepTimerStop), LOGLEVEL_NOTICE); - System_IndicateOk(); - publishMqtt((char *) FPSTR(topicSleepState), 0, false); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - gPlayProperties.sleepAfterPlaylist = false; - gPlayProperties.sleepAfterCurrentTrack = false; - gPlayProperties.playUntilTrackNumber = 0; - return; - } else { - Log_Println((char *) FPSTR(sleepTimerAlreadyStopped), LOGLEVEL_INFO); - System_IndicateError(); - return; - } + if (strcmp(receivedString, "EOP") == 0) { + gPlayProperties.sleepAfterPlaylist = true; + Log_Println(sleepTimerEOP, LOGLEVEL_NOTICE); + publishMqtt(topicSleepTimerState, "EOP", false); + Led_ResetToNightBrightness(); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); + System_IndicateOk(); + free(receivedString); + return; + } else if (strcmp(receivedString, "EOT") == 0) { + gPlayProperties.sleepAfterCurrentTrack = true; + Log_Println(sleepTimerEOT, LOGLEVEL_NOTICE); + publishMqtt(topicSleepTimerState, "EOT", false); + Led_ResetToNightBrightness(); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); + System_IndicateOk(); + free(receivedString); + return; + } else if (strcmp(receivedString, "EO5T") == 0) { + if ((gPlayProperties.numberOfTracks - 1) >= (gPlayProperties.currentTrackNumber + 5)) { + gPlayProperties.playUntilTrackNumber = gPlayProperties.currentTrackNumber + 5; + } else { + gPlayProperties.sleepAfterPlaylist = true; // If +5 tracks is > than active playlist, take end of current playlist } - System_SetSleepTimer((uint8_t)strtoul(receivedString, NULL, 10)); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u Minute(n)", (char *) FPSTR(sleepTimerSetTo), System_GetSleepTimer()); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Println(sleepTimerEO5, LOGLEVEL_NOTICE); + publishMqtt(topicSleepTimerState, "EO5T", false); + Led_ResetToNightBrightness(); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); System_IndicateOk(); - - gPlayProperties.sleepAfterPlaylist = false; - gPlayProperties.sleepAfterCurrentTrack = false; - } - // Track-control (pause/play, stop, first, last, next, previous) - else if (strcmp_P(topic, topicTrackControlCmnd) == 0) { - uint8_t controlCommand = strtoul(receivedString, NULL, 10); - AudioPlayer_TrackControlToQueueSender(controlCommand); - } - - // Check if controls should be locked - else if (strcmp_P(topic, topicLockControlsCmnd) == 0) { - if (strcmp(receivedString, "OFF") == 0) { - System_SetLockControls(false); - Log_Println((char *) FPSTR(allowButtons), LOGLEVEL_NOTICE); - publishMqtt((char *) FPSTR(topicLockControlsState), "OFF", false); - System_IndicateOk(); - } else if (strcmp(receivedString, "ON") == 0) { - System_SetLockControls(true); - Log_Println((char *) FPSTR(lockButtons), LOGLEVEL_NOTICE); - publishMqtt((char *) FPSTR(topicLockControlsState), "ON", false); + free(receivedString); + return; + } else if (strcmp(receivedString, "0") == 0) { // Disable sleep after it was active previously + if (System_IsSleepTimerEnabled()) { + System_DisableSleepTimer(); + Log_Println(sleepTimerStop, LOGLEVEL_NOTICE); System_IndicateOk(); + publishMqtt(topicSleepState, 0, false); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); + gPlayProperties.sleepAfterPlaylist = false; + gPlayProperties.sleepAfterCurrentTrack = false; + gPlayProperties.playUntilTrackNumber = 0; + } else { + Log_Println(sleepTimerAlreadyStopped, LOGLEVEL_INFO); + System_IndicateError(); } + free(receivedString); + return; } - - // Check if playmode should be adjusted - else if (strcmp_P(topic, topicRepeatModeCmnd) == 0) { - uint8_t repeatMode = strtoul(receivedString, NULL, 10); - snprintf(Log_Buffer, Log_BufferLength, "Repeat: %d", repeatMode); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - if (gPlayProperties.playMode != NO_PLAYLIST) { - if (gPlayProperties.playMode == NO_PLAYLIST) { - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - Log_Println((char *) FPSTR(noPlaylistNotAllowedMqtt), LOGLEVEL_ERROR); - System_IndicateError(); - } else { - switch (repeatMode) { - case NO_REPEAT: - gPlayProperties.repeatCurrentTrack = false; - gPlayProperties.repeatPlaylist = false; - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - Log_Println((char *) FPSTR(modeRepeatNone), LOGLEVEL_INFO); - System_IndicateOk(); - break; - - case TRACK: - gPlayProperties.repeatCurrentTrack = true; - gPlayProperties.repeatPlaylist = false; - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - Log_Println((char *) FPSTR(modeRepeatTrack), LOGLEVEL_INFO); - System_IndicateOk(); - break; - - case PLAYLIST: - gPlayProperties.repeatCurrentTrack = false; - gPlayProperties.repeatPlaylist = true; - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - Log_Println((char *) FPSTR(modeRepeatPlaylist), LOGLEVEL_INFO); - System_IndicateOk(); - break; - - case TRACK_N_PLAYLIST: - gPlayProperties.repeatCurrentTrack = true; - gPlayProperties.repeatPlaylist = true; - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - Log_Println((char *) FPSTR(modeRepeatTracknPlaylist), LOGLEVEL_INFO); - System_IndicateOk(); - break; - - default: - System_IndicateError(); - publishMqtt((char *) FPSTR(topicRepeatModeState), AudioPlayer_GetRepeatMode(), false); - break; - } + System_SetSleepTimer((uint8_t) strtoul(receivedString, NULL, 10)); + Log_Printf(LOGLEVEL_NOTICE, sleepTimerSetTo, System_GetSleepTimer()); + System_IndicateOk(); + + gPlayProperties.sleepAfterPlaylist = false; + gPlayProperties.sleepAfterCurrentTrack = false; + } + // Track-control (pause/play, stop, first, last, next, previous) + else if (strcmp_P(topic, topicTrackControlCmnd) == 0) { + uint8_t controlCommand = strtoul(receivedString, NULL, 10); + AudioPlayer_TrackControlToQueueSender(controlCommand); + } + + // Check if controls should be locked + else if (strcmp_P(topic, topicLockControlsCmnd) == 0) { + if (strcmp(receivedString, "OFF") == 0) { + System_SetLockControls(false); + Log_Println(allowButtons, LOGLEVEL_NOTICE); + publishMqtt(topicLockControlsState, "OFF", false); + System_IndicateOk(); + } else if (strcmp(receivedString, "ON") == 0) { + System_SetLockControls(true); + Log_Println(lockButtons, LOGLEVEL_NOTICE); + publishMqtt(topicLockControlsState, "ON", false); + System_IndicateOk(); + } + } + + // Check if playmode should be adjusted + else if (strcmp_P(topic, topicRepeatModeCmnd) == 0) { + uint8_t repeatMode = strtoul(receivedString, NULL, 10); + Log_Printf(LOGLEVEL_NOTICE, "Repeat: %d", repeatMode); + if (gPlayProperties.playMode != NO_PLAYLIST) { + if (gPlayProperties.playMode == NO_PLAYLIST) { + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); + Log_Println(noPlaylistNotAllowedMqtt, LOGLEVEL_ERROR); + System_IndicateError(); + } else { + switch (repeatMode) { + case NO_REPEAT: + gPlayProperties.repeatCurrentTrack = false; + gPlayProperties.repeatPlaylist = false; + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); + Log_Println(modeRepeatNone, LOGLEVEL_INFO); + System_IndicateOk(); + break; + + case TRACK: + gPlayProperties.repeatCurrentTrack = true; + gPlayProperties.repeatPlaylist = false; + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); + Log_Println(modeRepeatTrack, LOGLEVEL_INFO); + System_IndicateOk(); + break; + + case PLAYLIST: + gPlayProperties.repeatCurrentTrack = false; + gPlayProperties.repeatPlaylist = true; + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); + Log_Println(modeRepeatPlaylist, LOGLEVEL_INFO); + System_IndicateOk(); + break; + + case TRACK_N_PLAYLIST: + gPlayProperties.repeatCurrentTrack = true; + gPlayProperties.repeatPlaylist = true; + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); + Log_Println(modeRepeatTracknPlaylist, LOGLEVEL_INFO); + System_IndicateOk(); + break; + + default: + System_IndicateError(); + publishMqtt(topicRepeatModeState, AudioPlayer_GetRepeatMode(), false); + break; } } } + } - // Check if LEDs should be dimmed - else if (strcmp_P(topic, topicLedBrightnessCmnd) == 0) { - Led_SetBrightness(strtoul(receivedString, NULL, 10)); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - } + // Check if LEDs should be dimmed + else if (strcmp_P(topic, topicLedBrightnessCmnd) == 0) { + Led_SetBrightness(strtoul(receivedString, NULL, 10)); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); + } - // Requested something that isn't specified? - else { - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(noValidTopic), topic); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - System_IndicateError(); - } + // Requested something that isn't specified? + else { + Log_Printf(LOGLEVEL_ERROR, noValidTopic, topic); + System_IndicateError(); + } - free(receivedString); - free(mqttTopic); - #endif + free(receivedString); +#endif } diff --git a/src/Mqtt.h b/src/Mqtt.h index 1fd64aab..f7554884 100644 --- a/src/Mqtt.h +++ b/src/Mqtt.h @@ -1,7 +1,7 @@ #pragma once #ifdef MQTT_ENABLE - #define MQTT_SOCKET_TIMEOUT 1 // https://github.com/knolleary/pubsubclient/issues/403 + #define MQTT_SOCKET_TIMEOUT 1 // https://github.com/knolleary/pubsubclient/issues/403 #include #endif // MQTT-configuration diff --git a/src/Port.cpp b/src/Port.cpp index d86ae859..2536bdf7 100644 --- a/src/Port.cpp +++ b/src/Port.cpp @@ -1,9 +1,12 @@ #include -#include #include "settings.h" + #include "Port.h" + #include "Log.h" +#include + // Infos: // PCA9555 has 16 channels that are subdivided into 2 ports with 8 channels each. // Every channels is represented by a bit. @@ -14,41 +17,41 @@ // 115 => port 1 channel/bit 7 #ifdef PORT_EXPANDER_ENABLE - extern TwoWire i2cBusTwo; +extern TwoWire i2cBusTwo; - uint8_t Port_ExpanderPortsInputChannelStatus[2]; - static uint8_t Port_ExpanderPortsOutputChannelStatus[2] = {255, 255}; // Stores current configuration of output-channels locally - void Port_ExpanderHandler(void); - uint8_t Port_ChannelToBit(const uint8_t _channel); - void Port_WriteInitMaskForOutputChannels(void); - void Port_Test(void); +uint8_t Port_ExpanderPortsInputChannelStatus[2]; +static uint8_t Port_ExpanderPortsOutputChannelStatus[2] = {255, 255}; // Stores current configuration of output-channels locally +void Port_ExpanderHandler(void); +uint8_t Port_ChannelToBit(const uint8_t _channel); +void Port_WriteInitMaskForOutputChannels(void); +void Port_Test(void); #if (PE_INTERRUPT_PIN >= 0 && PE_INTERRUPT_PIN <= MAX_GPIO) #define PE_INTERRUPT_PIN_ENABLE - void IRAM_ATTR PORT_ExpanderISR(void); - bool Port_AllowReadFromPortExpander = false; - bool Port_AllowInitReadFromPortExpander = true; +void IRAM_ATTR PORT_ExpanderISR(void); +bool Port_AllowReadFromPortExpander = false; +bool Port_AllowInitReadFromPortExpander = true; #endif #endif void Port_Init(void) { - #ifdef PORT_EXPANDER_ENABLE - Port_Test(); - Port_WriteInitMaskForOutputChannels(); - Port_ExpanderHandler(); - #endif +#ifdef PORT_EXPANDER_ENABLE + Port_Test(); + Port_WriteInitMaskForOutputChannels(); + Port_ExpanderHandler(); +#endif - #ifdef PE_INTERRUPT_PIN_ENABLE - pinMode(PE_INTERRUPT_PIN, INPUT_PULLUP); - attachInterrupt(PE_INTERRUPT_PIN, PORT_ExpanderISR, FALLING); - Log_Println(portExpanderInterruptEnabled, LOGLEVEL_NOTICE); - #endif +#ifdef PE_INTERRUPT_PIN_ENABLE + pinMode(PE_INTERRUPT_PIN, INPUT_PULLUP); + attachInterrupt(PE_INTERRUPT_PIN, PORT_ExpanderISR, FALLING); + Log_Println(portExpanderInterruptEnabled, LOGLEVEL_NOTICE); +#endif } void Port_Cyclic(void) { - #ifdef PORT_EXPANDER_ENABLE - Port_ExpanderHandler(); - #endif +#ifdef PORT_EXPANDER_ENABLE + Port_ExpanderHandler(); +#endif } // Wrapper: reads from GPIOs (via digitalRead()) or from port-expander (if enabled) @@ -58,14 +61,14 @@ bool Port_Read(const uint8_t _channel) { case 0 ... MAX_GPIO: // GPIO return digitalRead(_channel); - #ifdef PORT_EXPANDER_ENABLE - case 100 ... 107: // Port-expander (port 0) - return (Port_ExpanderPortsInputChannelStatus[0] & (1 << (_channel - 100))); // Remove offset 100 (return false if pressed) +#ifdef PORT_EXPANDER_ENABLE + case 100 ... 107: // Port-expander (port 0) + return (Port_ExpanderPortsInputChannelStatus[0] & (1 << (_channel - 100))); // Remove offset 100 (return false if pressed) - case 108 ... 115: // Port-expander (port 1) - return (Port_ExpanderPortsInputChannelStatus[1] & (1 << (_channel - 108))); // Remove offset 100 + 8 (return false if pressed) + case 108 ... 115: // Port-expander (port 1) + return (Port_ExpanderPortsInputChannelStatus[1] & (1 << (_channel - 108))); // Remove offset 100 + 8 (return false if pressed) - #endif +#endif default: // Everything else (doesn't make sense at all) isn't supposed to be pressed return true; @@ -75,29 +78,29 @@ bool Port_Read(const uint8_t _channel) { // Configures OUTPUT-mode for GPIOs (non port-expander) // Output-mode for port-channels is done via Port_WriteInitMaskForOutputChannels() void Port_Write(const uint8_t _channel, const bool _newState, const bool _initGpio) { - #ifdef GPIO_PA_EN - if (_channel == GPIO_PA_EN) { - if (_newState) { - Log_Println((char *) FPSTR(paOn), LOGLEVEL_NOTICE); - } else { - Log_Println((char *) FPSTR(paOff), LOGLEVEL_NOTICE); - } +#ifdef GPIO_PA_EN + if (_channel == GPIO_PA_EN) { + if (_newState) { + Log_Println(paOn, LOGLEVEL_NOTICE); + } else { + Log_Println(paOff, LOGLEVEL_NOTICE); } - #endif + } +#endif - #ifdef GPIO_HP_EN - if (_channel == GPIO_HP_EN) { - if (_newState) { - Log_Println((char *) FPSTR(hpOn), LOGLEVEL_NOTICE); - } else { - Log_Println((char *) FPSTR(hpOff), LOGLEVEL_NOTICE); - } +#ifdef GPIO_HP_EN + if (_channel == GPIO_HP_EN) { + if (_newState) { + Log_Println(hpOn, LOGLEVEL_NOTICE); + } else { + Log_Println(hpOff, LOGLEVEL_NOTICE); } - #endif + } +#endif // Make init only for GPIO but not for PE (because PE is already done earlier) if (_initGpio) { - if (_channel >= 0 && _channel <= MAX_GPIO) { + if (_channel <= MAX_GPIO) { pinMode(_channel, OUTPUT); } } @@ -108,31 +111,31 @@ void Port_Write(const uint8_t _channel, const bool _newState, const bool _initGp break; } - #ifdef PORT_EXPANDER_ENABLE - case 100 ... 115: { - uint8_t portOffset = 0; - if (_channel >= 108 && _channel <= 115) { - portOffset = 1; - } - - uint8_t oldPortBitmask = Port_ExpanderPortsOutputChannelStatus[portOffset]; - uint8_t newPortBitmask; - - i2cBusTwo.beginTransmission(expanderI2cAddress); - i2cBusTwo.write(0x02); // Pointer to output configuration-register - if (_newState) { - newPortBitmask = (oldPortBitmask | (1 << Port_ChannelToBit(_channel))); - Port_ExpanderPortsOutputChannelStatus[portOffset] = newPortBitmask; // Write back new status - } else { - newPortBitmask = (oldPortBitmask & ~(1 << Port_ChannelToBit(_channel))); - Port_ExpanderPortsOutputChannelStatus[portOffset] = newPortBitmask; // Write back new status - } - i2cBusTwo.write(Port_ExpanderPortsOutputChannelStatus[0]); - i2cBusTwo.write(Port_ExpanderPortsOutputChannelStatus[1]); - i2cBusTwo.endTransmission(); - break; +#ifdef PORT_EXPANDER_ENABLE + case 100 ... 115: { + uint8_t portOffset = 0; + if (_channel >= 108 && _channel <= 115) { + portOffset = 1; } - #endif + + uint8_t oldPortBitmask = Port_ExpanderPortsOutputChannelStatus[portOffset]; + uint8_t newPortBitmask; + + i2cBusTwo.beginTransmission(expanderI2cAddress); + i2cBusTwo.write(0x02); // Pointer to output configuration-register + if (_newState) { + newPortBitmask = (oldPortBitmask | (1 << Port_ChannelToBit(_channel))); + Port_ExpanderPortsOutputChannelStatus[portOffset] = newPortBitmask; // Write back new status + } else { + newPortBitmask = (oldPortBitmask & ~(1 << Port_ChannelToBit(_channel))); + Port_ExpanderPortsOutputChannelStatus[portOffset] = newPortBitmask; // Write back new status + } + i2cBusTwo.write(Port_ExpanderPortsOutputChannelStatus[0]); + i2cBusTwo.write(Port_ExpanderPortsOutputChannelStatus[1]); + i2cBusTwo.endTransmission(); + break; + } +#endif default: { break; @@ -141,252 +144,256 @@ void Port_Write(const uint8_t _channel, const bool _newState, const bool _initGp } #ifdef PORT_EXPANDER_ENABLE - // Translates digitalWrite-style "GPIO" to bit - uint8_t Port_ChannelToBit(const uint8_t _channel) { - switch (_channel) { - case 100: - case 108: - return 0; - break; - case 101: - case 109: - return 1; - break; - case 102: - case 110: - return 2; - break; - case 103: - case 111: - return 3; - break; - case 104: - case 112: - return 4; - break; - case 105: - case 113: - return 5; - break; - case 106: - case 114: - return 6; - break; - case 107: - case 115: - return 7; - break; - - default: - return 255; // not valid! - } - } +// Translates digitalWrite-style "GPIO" to bit +uint8_t Port_ChannelToBit(const uint8_t _channel) { + switch (_channel) { + case 100: + case 108: + return 0; + break; + case 101: + case 109: + return 1; + break; + case 102: + case 110: + return 2; + break; + case 103: + case 111: + return 3; + break; + case 104: + case 112: + return 4; + break; + case 105: + case 113: + return 5; + break; + case 106: + case 114: + return 6; + break; + case 107: + case 115: + return 7; + break; - // Writes initial port-configuration (I/O) for port-expander PCA9555 - // If no output-channel is necessary, nothing has to be configured as all channels are in input-mode as per default (255) - // So every bit representing an output-channel needs to be set to 0. - void Port_WriteInitMaskForOutputChannels(void) { - const uint8_t portBaseValueBitMask = 255; - const uint8_t portsToWrite = 2; - uint8_t OutputBitMaskInOutAsPerPort[portsToWrite] = { portBaseValueBitMask, portBaseValueBitMask }; // 255 => all channels set to input; [0]: port0, [1]: port1 - uint8_t OutputBitMaskLowHighAsPerPort[portsToWrite] = { 0x00, 0x00}; // Bit configured as 0 for an output-channels means: logic LOW - - #ifdef GPIO_PA_EN // Set as output to enable/disable amp for loudspeaker - if (GPIO_PA_EN >= 100 && GPIO_PA_EN <= 107) { - // Bits of channels to be configured as input are 1 by default. - // So in order to change I/O-direction to output we need to set those bits to 0. - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); - } else if (GPIO_PA_EN >= 108 && GPIO_PA_EN <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); - } - #endif + default: + return 255; // not valid! + } +} - #ifdef GPIO_HP_EN // Set as output to enable/disable amp for headphones - if (GPIO_HP_EN >= 100 && GPIO_HP_EN <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); - } else if (GPIO_HP_EN >= 108 && GPIO_HP_EN <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); - } - #endif +// Writes initial port-configuration (I/O) for port-expander PCA9555 +// If no output-channel is necessary, nothing has to be configured as all channels are in input-mode as per default (255) +// So every bit representing an output-channel needs to be set to 0. +void Port_WriteInitMaskForOutputChannels(void) { + const uint8_t portBaseValueBitMask = 255; + const uint8_t portsToWrite = 2; + uint8_t OutputBitMaskInOutAsPerPort[portsToWrite] = {portBaseValueBitMask, portBaseValueBitMask}; // 255 => all channels set to input; [0]: port0, [1]: port1 + uint8_t OutputBitMaskLowHighAsPerPort[portsToWrite] = {0x00, 0x00}; // Bit configured as 0 for an output-channels means: logic LOW + + #ifdef GPIO_PA_EN // Set as output to enable/disable amp for loudspeaker + if (GPIO_PA_EN >= 100 && GPIO_PA_EN <= 107) { + // Bits of channels to be configured as input are 1 by default. + // So in order to change I/O-direction to output we need to set those bits to 0. + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); + } else if (GPIO_PA_EN >= 108 && GPIO_PA_EN <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); + } + #endif - #ifdef POWER // Set as output to trigger mosfet/power-pin for powering peripherals. Hint: logic is inverted if INVERT_POWER is enabled. - if (POWER >= 100 && POWER <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(POWER)); - } else if (POWER >= 108 && POWER <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(POWER)); - } - #endif + #ifdef GPIO_HP_EN // Set as output to enable/disable amp for headphones + if (GPIO_HP_EN >= 100 && GPIO_HP_EN <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); + } else if (GPIO_HP_EN >= 108 && GPIO_HP_EN <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); + } + #endif - #ifdef BUTTONS_LED - if (BUTTONS_LED >= 100 && BUTTONS_LED <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); - } else if (BUTTONS_LED >= 108 && BUTTONS_LED <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); - } - #endif + #ifdef POWER // Set as output to trigger mosfet/power-pin for powering peripherals. Hint: logic is inverted if INVERT_POWER is enabled. + if (POWER >= 100 && POWER <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(POWER)); + } else if (POWER >= 108 && POWER <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(POWER)); + } + #endif - // Only change port-config if necessary (at least bitmask changed from base-default for one port) - if ((OutputBitMaskInOutAsPerPort[0] != portBaseValueBitMask) || (OutputBitMaskInOutAsPerPort[1] != portBaseValueBitMask)) { - i2cBusTwo.beginTransmission(expanderI2cAddress); - i2cBusTwo.write(0x06); // Pointer to configuration of input/output - for (uint8_t i=0; i= 100 && BUTTONS_LED <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); + } else if (BUTTONS_LED >= 108 && BUTTONS_LED <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); + } + #endif - // Write low/high-config to all output-channels. Channels that are configured as input are silently/automatically ignored by PCA9555 - i2cBusTwo.beginTransmission(expanderI2cAddress); - i2cBusTwo.write(0x02); // Pointer to configuration of output-channels (high/low) - i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[0]); // port0 - i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[1]); // port1 - i2cBusTwo.endTransmission(); + // Only change port-config if necessary (at least bitmask changed from base-default for one port) + if ((OutputBitMaskInOutAsPerPort[0] != portBaseValueBitMask) || (OutputBitMaskInOutAsPerPort[1] != portBaseValueBitMask)) { + i2cBusTwo.beginTransmission(expanderI2cAddress); + i2cBusTwo.write(0x06); // Pointer to configuration of input/output + for (uint8_t i = 0; i < portsToWrite; i++) { + i2cBusTwo.write(OutputBitMaskInOutAsPerPort[i]); + // Serial.printf("Register %u - Mask: %u\n", 0x06+i, OutputBitMaskInOutAsPerPort[i]); } + i2cBusTwo.endTransmission(); + + // Write low/high-config to all output-channels. Channels that are configured as input are silently/automatically ignored by PCA9555 + i2cBusTwo.beginTransmission(expanderI2cAddress); + i2cBusTwo.write(0x02); // Pointer to configuration of output-channels (high/low) + i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[0]); // port0 + i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[1]); // port1 + i2cBusTwo.endTransmission(); } +} - // Some channels are configured as output before shutdown in order to avoid unwanted interrupts while ESP32 sleeps - void Port_MakeSomeChannelsOutputForShutdown(void) { - const uint8_t portBaseValueBitMask = 255; - const uint8_t portsToWrite = 2; - uint8_t OutputBitMaskInOutAsPerPort[portsToWrite] = { portBaseValueBitMask, portBaseValueBitMask }; // 255 => all channels set to input; [0]: port0, [1]: port1 - uint8_t OutputBitMaskLowHighAsPerPort[portsToWrite] = { 0x00, 0x00}; - - #ifdef HP_DETECT // https://forum.espuino.de/t/lolin-d32-pro-mit-sd-mmc-pn5180-max-fuenf-buttons-und-port-expander-smd/638/33 - if (HP_DETECT >= 100 && HP_DETECT <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(HP_DETECT)); - } else if (HP_DETECT >= 108 && HP_DETECT <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(HP_DETECT)); - } - #endif +// Some channels are configured as output before shutdown in order to avoid unwanted interrupts while ESP32 sleeps +void Port_MakeSomeChannelsOutputForShutdown(void) { + const uint8_t portBaseValueBitMask = 255; + const uint8_t portsToWrite = 2; + uint8_t OutputBitMaskInOutAsPerPort[portsToWrite] = {portBaseValueBitMask, portBaseValueBitMask}; // 255 => all channels set to input; [0]: port0, [1]: port1 + uint8_t OutputBitMaskLowHighAsPerPort[portsToWrite] = {0x00, 0x00}; + + #ifdef HP_DETECT // https://forum.espuino.de/t/lolin-d32-pro-mit-sd-mmc-pn5180-max-fuenf-buttons-und-port-expander-smd/638/33 + if (HP_DETECT >= 100 && HP_DETECT <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(HP_DETECT)); + } else if (HP_DETECT >= 108 && HP_DETECT <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(HP_DETECT)); + } + #endif - // There's no possibility to get current I/O-status from PCA9555. So we just re-set it again for OUTPUT-pins. - #ifdef GPIO_PA_EN - if (GPIO_PA_EN >= 100 && GPIO_PA_EN <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); - } else if (GPIO_PA_EN >= 108 && GPIO_PA_EN <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); - } - #endif + // There's no possibility to get current I/O-status from PCA9555. So we just re-set it again for OUTPUT-pins. + #ifdef GPIO_PA_EN + if (GPIO_PA_EN >= 100 && GPIO_PA_EN <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); + } else if (GPIO_PA_EN >= 108 && GPIO_PA_EN <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_PA_EN)); + } + #endif - #ifdef GPIO_HP_EN - if (GPIO_HP_EN >= 100 && GPIO_HP_EN <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); - } else if (GPIO_HP_EN >= 108 && GPIO_HP_EN <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); - } - #endif + #ifdef GPIO_HP_EN + if (GPIO_HP_EN >= 100 && GPIO_HP_EN <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); + } else if (GPIO_HP_EN >= 108 && GPIO_HP_EN <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(GPIO_HP_EN)); + } + #endif - #ifdef POWER // Set as output to trigger mosfet/power-pin for powering peripherals. Hint: logic is inverted if INVERT_POWER is enabled. - if (POWER >= 100 && POWER <= 107) { - #ifdef INVERT_POWER - OutputBitMaskLowHighAsPerPort[0] |= (1 << Port_ChannelToBit(POWER)); - #else - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(POWER)); - #endif - } else if (POWER >= 108 && POWER <= 115) { - #ifdef INVERT_POWER - OutputBitMaskLowHighAsPerPort[1] |= (1 << Port_ChannelToBit(POWER)); - #else - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(POWER)); - #endif - } + #ifdef POWER // Set as output to trigger mosfet/power-pin for powering peripherals. Hint: logic is inverted if INVERT_POWER is enabled. + if (POWER >= 100 && POWER <= 107) { + #ifdef INVERT_POWER + OutputBitMaskLowHighAsPerPort[0] |= (1 << Port_ChannelToBit(POWER)); + #else + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(POWER)); #endif - - #ifdef BUTTONS_LED - if (BUTTONS_LED >= 100 && BUTTONS_LED <= 107) { - OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); - } else if (BUTTONS_LED >= 108 && BUTTONS_LED <= 115) { - OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); - } + } else if (POWER >= 108 && POWER <= 115) { + #ifdef INVERT_POWER + OutputBitMaskLowHighAsPerPort[1] |= (1 << Port_ChannelToBit(POWER)); + #else + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(POWER)); #endif + } + #endif - // Only change port-config if necessary (at least bitmask changed from base-default for one port) - if ((OutputBitMaskInOutAsPerPort[0] != portBaseValueBitMask) || (OutputBitMaskInOutAsPerPort[1] != portBaseValueBitMask)) { - i2cBusTwo.beginTransmission(expanderI2cAddress); - i2cBusTwo.write(0x06); // Pointer to configuration of input/output - for (uint8_t i=0; i= 100 && BUTTONS_LED <= 107) { + OutputBitMaskInOutAsPerPort[0] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); + } else if (BUTTONS_LED >= 108 && BUTTONS_LED <= 115) { + OutputBitMaskInOutAsPerPort[1] &= ~(1 << Port_ChannelToBit(BUTTONS_LED)); + } + #endif - // Write low/high-config to all output-channels. Channels that are configured as input are silently/automatically ignored by PCA9555 - i2cBusTwo.beginTransmission(expanderI2cAddress); - i2cBusTwo.write(0x02); // Pointer to configuration of output-channels (high/low) - i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[0]); // port0 - i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[1]); // port1 - i2cBusTwo.endTransmission(); + // Only change port-config if necessary (at least bitmask changed from base-default for one port) + if ((OutputBitMaskInOutAsPerPort[0] != portBaseValueBitMask) || (OutputBitMaskInOutAsPerPort[1] != portBaseValueBitMask)) { + i2cBusTwo.beginTransmission(expanderI2cAddress); + i2cBusTwo.write(0x06); // Pointer to configuration of input/output + for (uint8_t i = 0; i < portsToWrite; i++) { + i2cBusTwo.write(OutputBitMaskInOutAsPerPort[i]); } + i2cBusTwo.endTransmission(); + + // Write low/high-config to all output-channels. Channels that are configured as input are silently/automatically ignored by PCA9555 + i2cBusTwo.beginTransmission(expanderI2cAddress); + i2cBusTwo.write(0x02); // Pointer to configuration of output-channels (high/low) + i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[0]); // port0 + i2cBusTwo.write(OutputBitMaskLowHighAsPerPort[1]); // port1 + i2cBusTwo.endTransmission(); } +} - // Reads input-registers from port-expander and writes output into global cache-array - // Datasheet: https://www.nxp.com/docs/en/data-sheet/PCA9555.pdf - void Port_ExpanderHandler(void) { - static bool verifyChangeOfInputRegister[2] = {false, false}; // Used to debounce once in case of register-change - static uint8_t inputRegisterBuffer[2]; - - // If interrupt-handling is active, only read port-expander's registers if interrupt was fired - #ifdef PE_INTERRUPT_PIN_ENABLE - if (!Port_AllowReadFromPortExpander && !Port_AllowInitReadFromPortExpander && !verifyChangeOfInputRegister[0] && !verifyChangeOfInputRegister[1]) { - return; - } else if (Port_AllowInitReadFromPortExpander) { - Port_AllowInitReadFromPortExpander = false; - } else if (Port_AllowReadFromPortExpander || Port_AllowInitReadFromPortExpander) { - Port_AllowReadFromPortExpander = false; - } - #endif +// Reads input-registers from port-expander and writes output into global cache-array +// Datasheet: https://www.nxp.com/docs/en/data-sheet/PCA9555.pdf +void Port_ExpanderHandler(void) { + static bool verifyChangeOfInputRegister[2] = {false, false}; // Used to debounce once in case of register-change + static uint8_t inputRegisterBuffer[2]; + // If interrupt-handling is active, only read port-expander's registers if interrupt was fired + #ifdef PE_INTERRUPT_PIN_ENABLE + if (!Port_AllowReadFromPortExpander && !Port_AllowInitReadFromPortExpander && !verifyChangeOfInputRegister[0] && !verifyChangeOfInputRegister[1]) { + return; + } else if (Port_AllowInitReadFromPortExpander) { + Port_AllowInitReadFromPortExpander = false; + } else if (Port_AllowReadFromPortExpander || Port_AllowInitReadFromPortExpander) { + Port_AllowReadFromPortExpander = false; + } + #endif + + for (uint8_t i = 0; i < 2; i++) { i2cBusTwo.beginTransmission(expanderI2cAddress); - for (uint8_t i = 0; i < 2; i++) { - i2cBusTwo.write(0x00 + i); // Pointer to input-register... - i2cBusTwo.endTransmission(); - i2cBusTwo.requestFrom(expanderI2cAddress, 1u); // ...and read its byte - - if (i2cBusTwo.available()) { - inputRegisterBuffer[i] = i2cBusTwo.read(); // Cache current readout - // Check if input-register changed. If so, don't use the value immediately - // but wait another cycle instead (=> rudimentary debounce). - // Added because there've been "ghost"-events occasionally with Arduino2 (https://forum.espuino.de/t/aktueller-stand-esp32-arduino-2/1389/55) - if (inputRegisterBuffer[i] != Port_ExpanderPortsInputChannelStatus[i] && millis() >= 10000 && !verifyChangeOfInputRegister[i]) { - verifyChangeOfInputRegister[i] = true; - //Serial.println("Verify set!"); - } else { - verifyChangeOfInputRegister[i] = false; - Port_ExpanderPortsInputChannelStatus[i] = inputRegisterBuffer[i]; - } - //Serial.printf("%u Debug: PE-Port: %u Status: %u\n", millis(), i, Port_ExpanderPortsInputChannelStatus[i]); + i2cBusTwo.write(0x00 + i); // Pointer to input-register... + uint8_t error = i2cBusTwo.endTransmission(); + if (error != 0) { + Log_Printf(LOGLEVEL_ERROR, "Error in endTransmission(): %d", error); + return; + } + i2cBusTwo.requestFrom(expanderI2cAddress, 1u); // ...and read its byte + + if (i2cBusTwo.available()) { + inputRegisterBuffer[i] = i2cBusTwo.read(); // Cache current readout + // Check if input-register changed. If so, don't use the value immediately + // but wait another cycle instead (=> rudimentary debounce). + // Added because there've been "ghost"-events occasionally with Arduino2 (https://forum.espuino.de/t/aktueller-stand-esp32-arduino-2/1389/55) + if (inputRegisterBuffer[i] != Port_ExpanderPortsInputChannelStatus[i] && millis() >= 10000 && !verifyChangeOfInputRegister[i]) { + verifyChangeOfInputRegister[i] = true; + // Serial.println("Verify set!"); + } else { + verifyChangeOfInputRegister[i] = false; + Port_ExpanderPortsInputChannelStatus[i] = inputRegisterBuffer[i]; } + // Serial.printf("%u Debug: PE-Port: %u Status: %u\n", millis(), i, Port_ExpanderPortsInputChannelStatus[i]); } } +} - // Make sure ports are read finally at shutdown in order to clear any active IRQs that could cause re-wakeup immediately - void Port_Exit(void) { - Port_MakeSomeChannelsOutputForShutdown(); +// Make sure ports are read finally at shutdown in order to clear any active IRQs that could cause re-wakeup immediately +void Port_Exit(void) { + Port_MakeSomeChannelsOutputForShutdown(); + for (uint8_t i = 0; i < 2; i++) { i2cBusTwo.beginTransmission(expanderI2cAddress); - for (uint8_t i = 0; i < 2; i++) { - i2cBusTwo.write(0x00 + i); // Pointer to input-register... - i2cBusTwo.endTransmission(); - i2cBusTwo.requestFrom(expanderI2cAddress, 1u); // ...and read its byte + i2cBusTwo.write(0x00 + i); // Pointer to input-register... + i2cBusTwo.endTransmission(); + i2cBusTwo.requestFrom(expanderI2cAddress, 1u); // ...and read its byte - if (i2cBusTwo.available()) { - Port_ExpanderPortsInputChannelStatus[i] = i2cBusTwo.read(); - } + if (i2cBusTwo.available()) { + Port_ExpanderPortsInputChannelStatus[i] = i2cBusTwo.read(); } } +} - // Tests if port-expander can be detected at address configured - void Port_Test(void) { - i2cBusTwo.beginTransmission(expanderI2cAddress); - i2cBusTwo.write(0x02); - if (!i2cBusTwo.endTransmission()) { - Log_Println(portExpanderFound, LOGLEVEL_NOTICE); - } else { - Log_Println(portExpanderNotFound, LOGLEVEL_ERROR); - } +// Tests if port-expander can be detected at address configured +void Port_Test(void) { + i2cBusTwo.beginTransmission(expanderI2cAddress); + i2cBusTwo.write(0x02); + if (!i2cBusTwo.endTransmission()) { + Log_Println(portExpanderFound, LOGLEVEL_NOTICE); + } else { + Log_Println(portExpanderNotFound, LOGLEVEL_ERROR); } +} #ifdef PE_INTERRUPT_PIN_ENABLE - void IRAM_ATTR PORT_ExpanderISR(void) { - Port_AllowReadFromPortExpander = true; - } +void IRAM_ATTR PORT_ExpanderISR(void) { + Port_AllowReadFromPortExpander = true; +} #endif #endif diff --git a/src/Power.cpp b/src/Power.cpp index 1b54a7e9..c3590183 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -1,29 +1,29 @@ #include #include "settings.h" -#include "Power.h" -#include "Port.h" +#include "Power.h" +#include "Port.h" void Power_Init(void) { - #if (POWER >= 0 && POWER <= MAX_GPIO) - pinMode(POWER, OUTPUT); // Only necessary for GPIO. For port-expander it's done (previously) via Port_init() - #endif +#if (POWER >= 0 && POWER <= MAX_GPIO) + pinMode(POWER, OUTPUT); // Only necessary for GPIO. For port-expander it's done (previously) via Port_init() +#endif } // Switch on peripherals. Please note: meaning of POWER_ON is HIGH per default. But is LOW in case of INVERT_POWER is enabled. void Power_PeripheralOn(void) { Port_Write(POWER, POWER_ON, false); - #ifdef BUTTONS_LED - Port_Write(BUTTONS_LED, HIGH, false); - #endif - delay(50); // Give peripherals some time to settle down +#ifdef BUTTONS_LED + Port_Write(BUTTONS_LED, HIGH, false); +#endif + delay(50); // Give peripherals some time to settle down } // Switch off peripherals. Please note: meaning of POWER_OFF is LOW per default. But is HIGH in case of INVERT_POWER is enabled. void Power_PeripheralOff(void) { Port_Write(POWER, POWER_OFF, false); - #ifdef BUTTONS_LED - Port_Write(BUTTONS_LED, LOW, false); - #endif +#ifdef BUTTONS_LED + Port_Write(BUTTONS_LED, LOW, false); +#endif } diff --git a/src/Power.h b/src/Power.h index b00010e9..06053d38 100644 --- a/src/Power.h +++ b/src/Power.h @@ -1,10 +1,10 @@ #pragma once #ifdef INVERT_POWER - #define POWER_ON LOW + #define POWER_ON LOW #define POWER_OFF HIGH #else - #define POWER_ON HIGH + #define POWER_ON HIGH #define POWER_OFF LOW #endif diff --git a/src/Queues.cpp b/src/Queues.cpp index 4096f7bf..85991199 100644 --- a/src/Queues.cpp +++ b/src/Queues.cpp @@ -1,5 +1,6 @@ #include #include "settings.h" + #include "Log.h" #include "Rfid.h" @@ -12,22 +13,22 @@ void Queues_Init(void) { // Create queues gVolumeQueue = xQueueCreate(1, sizeof(int)); if (gVolumeQueue == NULL) { - Log_Println((char *) FPSTR(unableToCreateVolQ), LOGLEVEL_ERROR); + Log_Println(unableToCreateVolQ, LOGLEVEL_ERROR); } gRfidCardQueue = xQueueCreate(1, cardIdStringSize); if (gRfidCardQueue == NULL) { - Log_Println((char *) FPSTR(unableToCreateRfidQ), LOGLEVEL_ERROR); + Log_Println(unableToCreateRfidQ, LOGLEVEL_ERROR); } gTrackControlQueue = xQueueCreate(1, sizeof(uint8_t)); if (gTrackControlQueue == NULL) { - Log_Println((char *) FPSTR(unableToCreateMgmtQ), LOGLEVEL_ERROR); + Log_Println(unableToCreateMgmtQ, LOGLEVEL_ERROR); } char **playlistArray; gTrackQueue = xQueueCreate(1, sizeof(playlistArray)); if (gTrackQueue == NULL) { - Log_Println((char *) FPSTR(unableToCreatePlayQ), LOGLEVEL_ERROR); + Log_Println(unableToCreatePlayQ, LOGLEVEL_ERROR); } } diff --git a/src/Rfid.h b/src/Rfid.h index 4c34cc49..f43bc56c 100644 --- a/src/Rfid.h +++ b/src/Rfid.h @@ -6,17 +6,19 @@ constexpr uint8_t cardIdStringSize = (cardIdSize * 3u) + 1u; extern char gCurrentRfidTagId[cardIdStringSize]; #ifndef PAUSE_WHEN_RFID_REMOVED - #ifdef DONT_ACCEPT_SAME_RFID_TWICE // ignore feature silently if PAUSE_WHEN_RFID_REMOVED is active + #ifdef DONT_ACCEPT_SAME_RFID_TWICE // ignore feature silently if PAUSE_WHEN_RFID_REMOVED is active #define DONT_ACCEPT_SAME_RFID_TWICE_ENABLE #endif #endif #ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE - extern char gOldRfidTagId[cardIdStringSize]; +void Rfid_ResetOldRfid(void); #endif void Rfid_Init(void); void Rfid_Cyclic(void); void Rfid_Exit(void); +void Rfid_TaskPause(void); +void Rfid_TaskResume(void); void Rfid_WakeupCheck(void); void Rfid_PreferenceLookupHandler(void); diff --git a/src/RfidCommon.cpp b/src/RfidCommon.cpp index 02f81963..3ab279a3 100644 --- a/src/RfidCommon.cpp +++ b/src/RfidCommon.cpp @@ -1,6 +1,6 @@ #include #include "settings.h" -#include "Rfid.h" + #include "AudioPlayer.h" #include "Cmd.h" #include "Common.h" @@ -8,93 +8,119 @@ #include "MemX.h" #include "Mqtt.h" #include "Queues.h" +#include "Rfid.h" #include "System.h" #include "Web.h" unsigned long Rfid_LastRfidCheckTimestamp = 0; char gCurrentRfidTagId[cardIdStringSize] = ""; // No crap here as otherwise it could be shown in GUI #ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE - char gOldRfidTagId[cardIdStringSize] = "X"; // Init with crap +char gOldRfidTagId[cardIdStringSize] = "X"; // Init with crap +#endif + +// check if we have RFID-reader enabled +#if defined(RFID_READER_TYPE_MFRC522_SPI) || defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_PN5180) + #define RFID_READER_ENABLED 1 #endif // Tries to lookup RFID-tag-string in NVS and extracts parameter from it if found void Rfid_PreferenceLookupHandler(void) { - #if defined (RFID_READER_TYPE_MFRC522_SPI) || defined (RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_PN5180) - BaseType_t rfidStatus; - char rfidTagId[cardIdStringSize]; - char _file[255]; - uint32_t _lastPlayPos = 0; - uint16_t _trackLastPlayed = 0; - uint32_t _playMode = 1; - - rfidStatus = xQueueReceive(gRfidCardQueue, &rfidTagId, 0); - if (rfidStatus == pdPASS) { - System_UpdateActivityTimer(); - strncpy(gCurrentRfidTagId, rfidTagId, cardIdStringSize-1); - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(rfidTagReceived), gCurrentRfidTagId); - Web_SendWebsocketData(0, 10); // Push new rfidTagId to all websocket-clients - Log_Println(Log_Buffer, LOGLEVEL_INFO); +#if defined(RFID_READER_ENABLED) + BaseType_t rfidStatus; + char rfidTagId[cardIdStringSize]; + char _file[255]; + uint32_t _lastPlayPos = 0; + uint16_t _trackLastPlayed = 0; + uint32_t _playMode = 1; - String s = gPrefsRfid.getString(gCurrentRfidTagId, "-1"); // Try to lookup rfidId in NVS - if (!s.compareTo("-1")) { - Log_Println((char *) FPSTR(rfidTagUnknownInNvs), LOGLEVEL_ERROR); - System_IndicateError(); - #ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE - strncpy(gOldRfidTagId, gCurrentRfidTagId, cardIdStringSize-1); // Even if not found in NVS: accept it as card last applied - #endif - return; - } + rfidStatus = xQueueReceive(gRfidCardQueue, &rfidTagId, 0); + if (rfidStatus == pdPASS) { + System_UpdateActivityTimer(); + strncpy(gCurrentRfidTagId, rfidTagId, cardIdStringSize - 1); + Log_Printf(LOGLEVEL_INFO, "%s: %s", rfidTagReceived, gCurrentRfidTagId); + Web_SendWebsocketData(0, 10); // Push new rfidTagId to all websocket-clients + String s = "-1"; + if (gPrefsRfid.isKey(gCurrentRfidTagId)) { + s = gPrefsRfid.getString(gCurrentRfidTagId, "-1"); // Try to lookup rfidId in NVS + } + if (!s.compareTo("-1")) { + Log_Println(rfidTagUnknownInNvs, LOGLEVEL_ERROR); + System_IndicateError(); + // allow to escape from bluetooth mode with an unknown card, switch back to normal mode + System_SetOperationMode(OPMODE_NORMAL); + return; + } - char *token; - uint8_t i = 1; - token = strtok((char *)s.c_str(), stringDelimiter); - while (token != NULL) { // Try to extract data from string after lookup - if (i == 1) { - strncpy(_file, token, sizeof(_file) / sizeof(_file[0])); - } else if (i == 2) { - _lastPlayPos = strtoul(token, NULL, 10); - } else if (i == 3) { - _playMode = strtoul(token, NULL, 10); - } else if (i == 4) { - _trackLastPlayed = strtoul(token, NULL, 10); - } - i++; - token = strtok(NULL, stringDelimiter); + char *token; + uint8_t i = 1; + token = strtok((char *) s.c_str(), stringDelimiter); + while (token != NULL) { // Try to extract data from string after lookup + if (i == 1) { + strncpy(_file, token, sizeof(_file) / sizeof(_file[0])); + } else if (i == 2) { + _lastPlayPos = strtoul(token, NULL, 10); + } else if (i == 3) { + _playMode = strtoul(token, NULL, 10); + } else if (i == 4) { + _trackLastPlayed = strtoul(token, NULL, 10); } + i++; + token = strtok(NULL, stringDelimiter); + } - if (i != 5) { - Log_Println((char *) FPSTR(errorOccuredNvs), LOGLEVEL_ERROR); - System_IndicateError(); + if (i != 5) { + Log_Println(errorOccuredNvs, LOGLEVEL_ERROR); + System_IndicateError(); + } else { + // Only pass file to queue if strtok revealed 3 items + if (_playMode >= 100) { + // Modification-cards can change some settings (e.g. introducing track-looping or sleep after track/playlist). + Cmd_Action(_playMode); } else { - // Only pass file to queue if strtok revealed 3 items - if (_playMode >= 100) { - // Modification-cards can change some settings (e.g. introducing track-looping or sleep after track/playlist). - Cmd_Action(_playMode); + #ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE + if (strncmp(gCurrentRfidTagId, gOldRfidTagId, 12) == 0) { + Log_Printf(LOGLEVEL_ERROR, dontAccepctSameRfid, gCurrentRfidTagId); + // System_IndicateError(); // Enable to have shown error @neopixel every time + return; } else { - #ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE - if (strncmp(gCurrentRfidTagId, gOldRfidTagId, 12) == 0) { - snprintf(Log_Buffer, Log_BufferLength, "%s (%s)", (char *) FPSTR(dontAccepctSameRfid), gCurrentRfidTagId); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - //System_IndicateError(); // Enable to have shown error @neopixel every time - return; - } else { - strncpy(gOldRfidTagId, gCurrentRfidTagId, 12); - } - #endif - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicRfidState), gCurrentRfidTagId, false); - #endif - - #ifdef BLUETOOTH_ENABLE - // if music rfid was read, go back to normal mode - if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { - System_SetOperationMode(OPMODE_NORMAL); - } - #endif + strncpy(gOldRfidTagId, gCurrentRfidTagId, 12); + } + #endif + #ifdef MQTT_ENABLE + publishMqtt(topicRfidState, gCurrentRfidTagId, false); + #endif - AudioPlayer_TrackQueueDispatcher(_file, _lastPlayPos, _playMode, _trackLastPlayed); + #ifdef BLUETOOTH_ENABLE + // if music rfid was read, go back to normal mode + if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { + System_SetOperationMode(OPMODE_NORMAL); } + #endif + + AudioPlayer_TrackQueueDispatcher(_file, _lastPlayPos, _playMode, _trackLastPlayed); } } - #endif + } +#endif +} + +#ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE +void Rfid_ResetOldRfid() { + strncpy(gOldRfidTagId, "X", cardIdStringSize - 1); +} +#endif + +#if defined(RFID_READER_ENABLED) +extern TaskHandle_t rfidTaskHandle; +#endif + +void Rfid_TaskPause(void) { +#if defined(RFID_READER_ENABLED) + vTaskSuspend(rfidTaskHandle); +#endif +} +void Rfid_TaskResume(void) { +#if defined(RFID_READER_ENABLED) + vTaskResume(rfidTaskHandle); +#endif } diff --git a/src/RfidMfrc522.cpp b/src/RfidMfrc522.cpp index ed2add4e..6e560aab 100644 --- a/src/RfidMfrc522.cpp +++ b/src/RfidMfrc522.cpp @@ -1,13 +1,15 @@ #include #include "settings.h" -#include "Rfid.h" + +#include "AudioPlayer.h" +#include "HallEffectSensor.h" #include "Log.h" #include "MemX.h" #include "Queues.h" +#include "Rfid.h" #include "System.h" + #include -#include "AudioPlayer.h" -#include "HallEffectSensor.h" #if defined RFID_READER_TYPE_MFRC522_SPI || defined RFID_READER_TYPE_MFRC522_I2C #ifdef RFID_READER_TYPE_MFRC522_SPI @@ -20,175 +22,176 @@ #include #endif - extern unsigned long Rfid_LastRfidCheckTimestamp; - static void Rfid_Task(void *parameter); +extern unsigned long Rfid_LastRfidCheckTimestamp; +TaskHandle_t rfidTaskHandle; +static void Rfid_Task(void *parameter); #ifdef RFID_READER_TYPE_MFRC522_I2C - extern TwoWire i2cBusTwo; - static MFRC522_I2C mfrc522(MFRC522_ADDR, MFRC522_RST_PIN, &i2cBusTwo); +extern TwoWire i2cBusTwo; +static MFRC522_I2C mfrc522(MFRC522_ADDR, MFRC522_RST_PIN, &i2cBusTwo); #endif #ifdef RFID_READER_TYPE_MFRC522_SPI - static MFRC522 mfrc522(RFID_CS, RST_PIN); +static MFRC522 mfrc522(RFID_CS, RST_PIN); #endif - void Rfid_Init(void) { - #ifdef RFID_READER_TYPE_MFRC522_SPI - SPI.begin(RFID_SCK, RFID_MISO, RFID_MOSI, RFID_CS); - SPI.setFrequency(1000000); - #endif +void Rfid_Init(void) { + #ifdef RFID_READER_TYPE_MFRC522_SPI + SPI.begin(RFID_SCK, RFID_MISO, RFID_MOSI, RFID_CS); + SPI.setFrequency(1000000); + #endif - // Init RC522 Card-Reader - #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_MFRC522_SPI) - mfrc522.PCD_Init(); - mfrc522.PCD_SetAntennaGain(rfidGain); - delay(50); - Log_Println((char *) FPSTR(rfidScannerReady), LOGLEVEL_DEBUG); - - xTaskCreatePinnedToCore( - Rfid_Task, /* Function to implement the task */ - "rfid", /* Name of the task */ - 2048, /* Stack size in words */ - NULL, /* Task input parameter */ - 2 | portPRIVILEGE_BIT, /* Priority of the task */ - NULL, /* Task handle. */ - 1 /* Core where the task should run */ - ); - #endif - } + // Init RC522 Card-Reader + #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_MFRC522_SPI) + mfrc522.PCD_Init(); + mfrc522.PCD_SetAntennaGain(rfidGain); + delay(50); + Log_Println(rfidScannerReady, LOGLEVEL_DEBUG); + + xTaskCreatePinnedToCore( + Rfid_Task, /* Function to implement the task */ + "rfid", /* Name of the task */ + 2048, /* Stack size in words */ + NULL, /* Task input parameter */ + 2 | portPRIVILEGE_BIT, /* Priority of the task */ + &rfidTaskHandle, /* Task handle. */ + 1 /* Core where the task should run */ + ); + #endif +} - void Rfid_Task(void *parameter) { - uint8_t control = 0x00; +void Rfid_Task(void *parameter) { + #ifdef PAUSE_WHEN_RFID_REMOVED + uint8_t control = 0x00; + #endif - for (;;) { - if (RFID_SCAN_INTERVAL/2 >= 20) { - vTaskDelay(portTICK_RATE_MS * (RFID_SCAN_INTERVAL/2)); - } else { - vTaskDelay(portTICK_RATE_MS * 20); + for (;;) { + if (RFID_SCAN_INTERVAL / 2 >= 20) { + vTaskDelay(portTICK_PERIOD_MS * (RFID_SCAN_INTERVAL / 2)); + } else { + vTaskDelay(portTICK_PERIOD_MS * 20); + } + byte cardId[cardIdSize]; + String cardIdString; + #ifdef PAUSE_WHEN_RFID_REMOVED + byte lastValidcardId[cardIdSize]; + bool sameCardReapplied = false; + #endif + if ((millis() - Rfid_LastRfidCheckTimestamp) >= RFID_SCAN_INTERVAL) { + // Log_Printf(LOGLEVEL_DEBUG, "%u", uxTaskGetStackHighWaterMark(NULL)); + + Rfid_LastRfidCheckTimestamp = millis(); + // Reset the loop if no new card is present on the sensor/reader. This saves the entire process when idle. + + if (!mfrc522.PICC_IsNewCardPresent()) { + continue; } - byte cardId[cardIdSize]; - String cardIdString; - #ifdef PAUSE_WHEN_RFID_REMOVED - byte lastValidcardId[cardIdSize]; - bool cardAppliedCurrentRun = false; - bool sameCardReapplied = false; - #endif - if ((millis() - Rfid_LastRfidCheckTimestamp) >= RFID_SCAN_INTERVAL) { - //snprintf(Log_Buffer, Log_BufferLength, "%u", uxTaskGetStackHighWaterMark(NULL)); - //Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - Rfid_LastRfidCheckTimestamp = millis(); - // Reset the loop if no new card is present on the sensor/reader. This saves the entire process when idle. - - if (!mfrc522.PICC_IsNewCardPresent()) { - continue; - } - // Select one of the cards - if (!mfrc522.PICC_ReadCardSerial()) { - continue; - } - #ifdef PAUSE_WHEN_RFID_REMOVED - cardAppliedCurrentRun = true; - #endif - - #ifndef PAUSE_WHEN_RFID_REMOVED - mfrc522.PICC_HaltA(); - mfrc522.PCD_StopCrypto1(); - #endif - - memcpy(cardId, mfrc522.uid.uidByte, cardIdSize); - #ifdef HALLEFFECT_SENSOR_ENABLE - cardId[cardIdSize-1] = cardId[cardIdSize-1] + gHallEffectSensor.waitForState(HallEffectWaitMS); - #endif - - #ifdef PAUSE_WHEN_RFID_REMOVED - if (memcmp((const void *)lastValidcardId, (const void *)cardId, sizeof(cardId)) == 0) { - sameCardReapplied = true; - } - #endif + // Select one of the cards + if (!mfrc522.PICC_ReadCardSerial()) { + continue; + } - Log_Print((char *) FPSTR(rfidTagDetected), LOGLEVEL_NOTICE, true); - for (uint8_t i=0u; i < cardIdSize; i++) { - snprintf(Log_Buffer, Log_BufferLength, "%02x%s", cardId[i], (i < cardIdSize - 1u) ? "-" : "\n"); - Log_Print(Log_Buffer, LOGLEVEL_NOTICE, false); - } + #ifndef PAUSE_WHEN_RFID_REMOVED + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); + #endif + + memcpy(cardId, mfrc522.uid.uidByte, cardIdSize); + + #ifdef HALLEFFECT_SENSOR_ENABLE + cardId[cardIdSize - 1] = cardId[cardIdSize - 1] + gHallEffectSensor.waitForState(HallEffectWaitMS); + #endif + + #ifdef PAUSE_WHEN_RFID_REMOVED + if (memcmp((const void *) lastValidcardId, (const void *) cardId, sizeof(cardId)) == 0) { + sameCardReapplied = true; + } + #endif + + String hexString; + for (uint8_t i = 0u; i < cardIdSize; i++) { + char str[4]; + snprintf(str, sizeof(str), "%02x%c", cardId[i], (i < cardIdSize - 1u) ? '-' : ' '); + hexString += str; + } + Log_Printf(LOGLEVEL_NOTICE, rfidTagDetected, hexString.c_str()); + + for (uint8_t i = 0u; i < cardIdSize; i++) { + char num[4]; + snprintf(num, sizeof(num), "%03d", cardId[i]); + cardIdString += num; + } - for (uint8_t i=0u; i < cardIdSize; i++) { - char num[4]; - snprintf(num, sizeof(num), "%03d", cardId[i]); - cardIdString += num; + #ifdef PAUSE_WHEN_RFID_REMOVED + #ifdef ACCEPT_SAME_RFID_AFTER_TRACK_END + if (!sameCardReapplied || gPlayProperties.trackFinished || gPlayProperties.playlistFinished) { // Don't allow to send card to queue if it's the same card again if track or playlist is unfnished + #else + if (!sameCardReapplied) { // Don't allow to send card to queue if it's the same card again... + #endif + xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); + } else { + // If pause-button was pressed while card was not applied, playback could be active. If so: don't pause when card is reapplied again as the desired functionality would be reversed in this case. + if (gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); // ... play/pause instead (but not for BT) } + } + memcpy(lastValidcardId, mfrc522.uid.uidByte, cardIdSize); + #else + xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); // If PAUSE_WHEN_RFID_REMOVED isn't active, every card-apply leads to new playlist-generation + #endif - #ifdef PAUSE_WHEN_RFID_REMOVED - #ifdef ACCEPT_SAME_RFID_AFTER_TRACK_END - if (!sameCardReapplied || gPlayProperties.trackFinished || gPlayProperties.playlistFinished) { // Don't allow to send card to queue if it's the same card again if track or playlist is unfnished - #else - if (!sameCardReapplied){ // Don't allow to send card to queue if it's the same card again... - #endif - } else { - // If pause-button was pressed while card was not applied, playback could be active. If so: don't pause when card is reapplied again as the desired functionality would be reversed in this case. - if (gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); // ... play/pause instead (but not for BT) - } - } - memcpy(lastValidcardId, mfrc522.uid.uidByte, cardIdSize); - #else - xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); // If PAUSE_WHEN_RFID_REMOVED isn't active, every card-apply leads to new playlist-generation - #endif - - #ifdef PAUSE_WHEN_RFID_REMOVED - // https://github.com/miguelbalboa/rfid/issues/188; voodoo! :-) - while (true) { - if (RFID_SCAN_INTERVAL/2 >= 20) { - vTaskDelay(portTICK_RATE_MS * (RFID_SCAN_INTERVAL/2)); - } else { - vTaskDelay(portTICK_RATE_MS * 20); - } - control=0; - for (uint8_t i=0u; i<3; i++) { - if (!mfrc522.PICC_IsNewCardPresent()) { - if (mfrc522.PICC_ReadCardSerial()) { - control |= 0x16; - } - if (mfrc522.PICC_ReadCardSerial()) { - control |= 0x16; - } - control += 0x1; - } - control += 0x4; + #ifdef PAUSE_WHEN_RFID_REMOVED + // https://github.com/miguelbalboa/rfid/issues/188; voodoo! :-) + while (true) { + if (RFID_SCAN_INTERVAL / 2 >= 20) { + vTaskDelay(portTICK_PERIOD_MS * (RFID_SCAN_INTERVAL / 2)); + } else { + vTaskDelay(portTICK_PERIOD_MS * 20); + } + control = 0; + for (uint8_t i = 0u; i < 3; i++) { + if (!mfrc522.PICC_IsNewCardPresent()) { + if (mfrc522.PICC_ReadCardSerial()) { + control |= 0x16; } - - if (control == 13 || control == 14) { - //card is still there - } else { - break; + if (mfrc522.PICC_ReadCardSerial()) { + control |= 0x16; } + control += 0x1; } + control += 0x4; + } - Log_Println((char *) FPSTR(rfidTagRemoved), LOGLEVEL_NOTICE); - if (!gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - Log_Println((char *) FPSTR(rfidTagReapplied), LOGLEVEL_NOTICE); - } - mfrc522.PICC_HaltA(); - mfrc522.PCD_StopCrypto1(); - cardAppliedCurrentRun = false; - #endif + if (control == 13 || control == 14) { + // card is still there + } else { + break; + } + } + + Log_Println(rfidTagRemoved, LOGLEVEL_NOTICE); + if (!gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); + Log_Println(rfidTagReapplied, LOGLEVEL_NOTICE); } + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); + #endif } } +} - void Rfid_Cyclic(void) { - // Not necessary as cyclic stuff performed by task Rfid_Task() - } +void Rfid_Cyclic(void) { + // Not necessary as cyclic stuff performed by task Rfid_Task() +} - void Rfid_Exit(void) { - #ifndef RFID_READER_TYPE_MFRC522_I2C - mfrc522.PCD_SoftPowerDown(); - #endif - } +void Rfid_Exit(void) { + #ifndef RFID_READER_TYPE_MFRC522_I2C + mfrc522.PCD_SoftPowerDown(); + #endif +} - void Rfid_WakeupCheck(void) { - } +void Rfid_WakeupCheck(void) { +} #endif diff --git a/src/RfidPn5180.cpp b/src/RfidPn5180.cpp index 521953ce..a5df7932 100644 --- a/src/RfidPn5180.cpp +++ b/src/RfidPn5180.cpp @@ -1,16 +1,19 @@ #include -#include -#include #include "settings.h" -#include "Rfid.h" + +#include "AudioPlayer.h" +#include "HallEffectSensor.h" #include "Log.h" #include "MemX.h" +#include "Port.h" #include "Queues.h" +#include "Rfid.h" #include "System.h" -#include "Port.h" + +#include +#include #include -#include "AudioPlayer.h" -#include "HallEffectSensor.h" +#include #ifdef RFID_READER_TYPE_PN5180 #include @@ -20,365 +23,399 @@ #define RFID_PN5180_STATE_INIT 0u -#define RFID_PN5180_NFC14443_STATE_RESET 1u +#define RFID_PN5180_NFC14443_STATE_RESET 1u #define RFID_PN5180_NFC14443_STATE_READCARD 2u -#define RFID_PN5180_NFC14443_STATE_ACTIVE 99u +#define RFID_PN5180_NFC14443_STATE_ACTIVE 99u -#define RFID_PN5180_NFC15693_STATE_RESET 3u -#define RFID_PN5180_NFC15693_STATE_SETUPRF 4u -#define RFID_PN5180_NFC15693_STATE_DISABLEPRIVACYMODE 5u -#define RFID_PN5180_NFC15693_STATE_GETINVENTORY 6u -#define RFID_PN5180_NFC15693_STATE_ACTIVE 100u +#define RFID_PN5180_NFC15693_STATE_RESET 3u +#define RFID_PN5180_NFC15693_STATE_SETUPRF 4u +#define RFID_PN5180_NFC15693_STATE_GETINVENTORY 5u +#define RFID_PN5180_NFC15693_STATE_DISABLEPRIVACYMODE 6u +#define RFID_PN5180_NFC15693_STATE_GETINVENTORY_PRIVACY 7u +#define RFID_PN5180_NFC15693_STATE_ACTIVE 100u extern unsigned long Rfid_LastRfidCheckTimestamp; +#if (defined(PORT_EXPANDER_ENABLE) && (RFID_IRQ > 99)) +extern TwoWire i2cBusTwo; +#endif + #ifdef RFID_READER_TYPE_PN5180 - static void Rfid_Task(void *parameter); - static TaskHandle_t rfidTaskHandle; +static void Rfid_Task(void *parameter); +TaskHandle_t rfidTaskHandle; #ifdef PN5180_ENABLE_LPCD - void Rfid_EnableLpcd(void); - bool enabledLpcdShutdown = false; // Indicates if LPCD should be activated as part of the shutdown-process +void Rfid_EnableLpcd(void); +bool enabledLpcdShutdown = false; // Indicates if LPCD should be activated as part of the shutdown-process - void Rfid_SetLpcdShutdownStatus(bool lpcdStatus) { - enabledLpcdShutdown = lpcdStatus; - } +void Rfid_SetLpcdShutdownStatus(bool lpcdStatus) { + enabledLpcdShutdown = lpcdStatus; +} - bool Rfid_GetLpcdShutdownStatus(void) { - return enabledLpcdShutdown; - } +bool Rfid_GetLpcdShutdownStatus(void) { + return enabledLpcdShutdown; +} #endif - void Rfid_Init(void) { - #ifdef PN5180_ENABLE_LPCD - // Check if wakeup-reason was card-detection (PN5180 only) - // This only works if RFID.IRQ is connected to a GPIO and not to a port-expander - esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); - if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT1) { - Rfid_WakeupCheck(); - } - // disable pin hold from deep sleep - gpio_deep_sleep_hold_dis(); - gpio_hold_dis(gpio_num_t(RFID_CS)); // NSS - gpio_hold_dis(gpio_num_t(RFID_RST)); // RST - #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) - pinMode(RFID_IRQ, INPUT); // Not necessary for port-expander as for pca9555 all pins are configured as input per default - #endif - #endif - - xTaskCreatePinnedToCore( - Rfid_Task, /* Function to implement the task */ - "rfid", /* Name of the task */ - 2048, /* Stack size in words */ - NULL, /* Task input parameter */ - 2 | portPRIVILEGE_BIT, /* Priority of the task */ - &rfidTaskHandle, /* Task handle. */ - 0 /* Core where the task should run */ - ); +void Rfid_Init(void) { + #ifdef PN5180_ENABLE_LPCD + // Check if wakeup-reason was card-detection (PN5180 only) + // This only works if RFID.IRQ is connected to a GPIO and not to a port-expander + esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); + if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT1) { + Rfid_WakeupCheck(); } - void Rfid_Cyclic(void) { - // Not necessary as cyclic stuff performed by task Rfid_Task() + // wakeup-check if IRQ is connected to port-expander, signal arrives as pushbutton + #if (defined(PORT_EXPANDER_ENABLE) && (RFID_IRQ > 99)) + if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) { + // read IRQ state from port-expander + i2cBusTwo.begin(ext_IIC_DATA, ext_IIC_CLK); + delay(50); + Port_Init(); + uint8_t irqState = Port_Read(RFID_IRQ); + if (irqState == LOW) { + Log_Println("Wakeup caused by low power card-detection on port-expander", LOGLEVEL_NOTICE); + Rfid_WakeupCheck(); + } } + #endif - void Rfid_Task(void *parameter) { - static PN5180ISO14443 nfc14443(RFID_CS, RFID_BUSY, RFID_RST); - static PN5180ISO15693 nfc15693(RFID_CS, RFID_BUSY, RFID_RST); - uint32_t lastTimeDetected14443 = 0; - uint32_t lastTimeDetected15693 = 0; - #ifdef PAUSE_WHEN_RFID_REMOVED - byte lastValidcardId[cardIdSize]; - bool cardAppliedCurrentRun = false; - bool cardAppliedLastRun = false; + // disable pin hold from deep sleep + gpio_deep_sleep_hold_dis(); + gpio_hold_dis(gpio_num_t(RFID_CS)); // NSS + gpio_hold_dis(gpio_num_t(RFID_RST)); // RST + #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) + pinMode(RFID_IRQ, INPUT); // Not necessary for port-expander as for pca9555 all pins are configured as input per default #endif - uint8_t stateMachine = RFID_PN5180_STATE_INIT; - static byte cardId[cardIdSize], lastCardId[cardIdSize]; - uint8_t uid[10]; - bool showDisablePrivacyNotification = true; - - // wait until queues are created - while (gRfidCardQueue == NULL) { - Log_Println((char *) FPSTR(waitingForTaskQueues), LOGLEVEL_DEBUG); - vTaskDelay(50); + #endif + + xTaskCreatePinnedToCore( + Rfid_Task, /* Function to implement the task */ + "rfid", /* Name of the task */ + 2048, /* Stack size in words */ + NULL, /* Task input parameter */ + 2 | portPRIVILEGE_BIT, /* Priority of the task */ + &rfidTaskHandle, /* Task handle. */ + 0 /* Core where the task should run */ + ); +} + +void Rfid_Cyclic(void) { + // Not necessary as cyclic stuff performed by task Rfid_Task() +} + +void Rfid_Task(void *parameter) { + static PN5180ISO14443 nfc14443(RFID_CS, RFID_BUSY, RFID_RST); + static PN5180ISO15693 nfc15693(RFID_CS, RFID_BUSY, RFID_RST); + uint32_t lastTimeDetected14443 = 0; + uint32_t lastTimeDetected15693 = 0; + #ifdef PAUSE_WHEN_RFID_REMOVED + byte lastValidcardId[cardIdSize]; + bool cardAppliedCurrentRun = false; + bool cardAppliedLastRun = false; + #endif + uint8_t stateMachine = RFID_PN5180_STATE_INIT; + static byte cardId[cardIdSize], lastCardId[cardIdSize]; + uint8_t uid[10]; + bool showDisablePrivacyNotification = true; + + // wait until queues are created + while (gRfidCardQueue == NULL) { + Log_Println(waitingForTaskQueues, LOGLEVEL_DEBUG); + vTaskDelay(50); + } + + for (;;) { + vTaskDelay(portTICK_PERIOD_MS * 10u); + #ifdef PN5180_ENABLE_LPCD + if (Rfid_GetLpcdShutdownStatus()) { + Rfid_EnableLpcd(); + Rfid_SetLpcdShutdownStatus(false); // give feedback that execution is complete + while (true) { + vTaskDelay(portTICK_PERIOD_MS * 100u); // there's no way back if shutdown was initiated + } } + #endif + String cardIdString; + bool cardReceived = false; + #ifdef PAUSE_WHEN_RFID_REMOVED + bool sameCardReapplied = false; + #endif - for (;;) { - vTaskDelay(portTICK_RATE_MS * 10u); - #ifdef PN5180_ENABLE_LPCD - if (Rfid_GetLpcdShutdownStatus()) { - Rfid_EnableLpcd(); - Rfid_SetLpcdShutdownStatus(false); // give feedback that execution is complete - while (true) { - vTaskDelay(portTICK_RATE_MS * 100u); // there's no way back if shutdown was initiated - } - } - #endif - String cardIdString; - bool cardReceived = false; - #ifdef PAUSE_WHEN_RFID_REMOVED - bool sameCardReapplied = false; - #endif - - if (RFID_PN5180_STATE_INIT == stateMachine) { - nfc14443.begin(); - nfc14443.reset(); - // show PN5180 reader version - uint8_t firmwareVersion[2]; - nfc14443.readEEprom(FIRMWARE_VERSION, firmwareVersion, sizeof(firmwareVersion)); - snprintf(Log_Buffer, Log_BufferLength, "%s%d.%d", (char *) F("PN5180 firmware version="), firmwareVersion[1], firmwareVersion[0]); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - // activate RF field - delay(4u); - Log_Println((char *) FPSTR(rfidScannerReady), LOGLEVEL_DEBUG); + if (RFID_PN5180_STATE_INIT == stateMachine) { + nfc14443.begin(); + nfc14443.reset(); + // show PN5180 reader version + uint8_t firmwareVersion[2]; + nfc14443.readEEprom(FIRMWARE_VERSION, firmwareVersion, sizeof(firmwareVersion)); + Log_Printf(LOGLEVEL_DEBUG, "PN5180 firmware version=%d.%d", firmwareVersion[1], firmwareVersion[0]); + + // activate RF field + delay(4u); + Log_Println(rfidScannerReady, LOGLEVEL_DEBUG); // 1. check for an ISO-14443 card - } else if (RFID_PN5180_NFC14443_STATE_RESET == stateMachine) { - nfc14443.reset(); - //snprintf(Log_Buffer, Log_BufferLength, "%u", uxTaskGetStackHighWaterMark(NULL)); - //Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - } else if (RFID_PN5180_NFC14443_STATE_READCARD == stateMachine) { - - if (nfc14443.readCardSerial(uid) >= 4) { - cardReceived = true; - stateMachine = RFID_PN5180_NFC14443_STATE_ACTIVE; - lastTimeDetected14443 = millis(); - #ifdef PAUSE_WHEN_RFID_REMOVED - cardAppliedCurrentRun = true; - #endif - } else { - // Reset to dummy-value if no card is there - // Necessary to differentiate between "card is still applied" and "card is re-applied again after removal" - // lastTimeDetected14443 is used to prevent "new card detection with old card" with single events where no card was detected - if (!lastTimeDetected14443 || (millis() - lastTimeDetected14443 >= 1000)) { - lastTimeDetected14443 = 0; - #ifdef PAUSE_WHEN_RFID_REMOVED - cardAppliedCurrentRun = false; - #endif - for (uint8_t i=0; i= 4) { + cardReceived = true; + stateMachine = RFID_PN5180_NFC14443_STATE_ACTIVE; + lastTimeDetected14443 = millis(); + #ifdef PAUSE_WHEN_RFID_REMOVED + cardAppliedCurrentRun = true; + #endif + } else { + // Reset to dummy-value if no card is there + // Necessary to differentiate between "card is still applied" and "card is re-applied again after removal" + // lastTimeDetected14443 is used to prevent "new card detection with old card" with single events where no card was detected + if (!lastTimeDetected14443 || (millis() - lastTimeDetected14443 >= 1000)) { + lastTimeDetected14443 = 0; + #ifdef PAUSE_WHEN_RFID_REMOVED + cardAppliedCurrentRun = false; + #endif + for (uint8_t i = 0; i < cardIdSize; i++) { + lastCardId[i] = 0; } + } else { + stateMachine = RFID_PN5180_NFC14443_STATE_ACTIVE; // Still consider first event as "active" } + } // 2. check for an ISO-15693 card - } else if (RFID_PN5180_NFC15693_STATE_RESET == stateMachine) { - nfc15693.reset(); - } else if (RFID_PN5180_NFC15693_STATE_SETUPRF == stateMachine) { - nfc15693.setupRF(); - } else if (RFID_PN5180_NFC15693_STATE_DISABLEPRIVACYMODE == stateMachine) { - // check for ICODE-SLIX2 password protected tag - // put your privacy password here, e.g.: - // https://de.ifixit.com/Antworten/Ansehen/513422/nfc+Chips+f%C3%BCr+tonies+kaufen - uint8_t password[] = {0x01, 0x02, 0x03, 0x04}; - ISO15693ErrorCode myrc = nfc15693.disablePrivacyMode(password); - if (ISO15693_EC_OK == myrc) { - if (showDisablePrivacyNotification) { - showDisablePrivacyNotification = false; - Log_Println((char *) F("disabling privacy-mode successful"), LOGLEVEL_NOTICE); - } - } - } else if (RFID_PN5180_NFC15693_STATE_GETINVENTORY == stateMachine) { - // try to read ISO15693 inventory - ISO15693ErrorCode rc = nfc15693.getInventory(uid); - if (rc == ISO15693_EC_OK) { - cardReceived = true; - stateMachine = RFID_PN5180_NFC15693_STATE_ACTIVE; - lastTimeDetected15693 = millis(); - #ifdef PAUSE_WHEN_RFID_REMOVED - cardAppliedCurrentRun = true; - #endif + } else if (RFID_PN5180_NFC15693_STATE_RESET == stateMachine) { + nfc15693.reset(); + } else if (RFID_PN5180_NFC15693_STATE_SETUPRF == stateMachine) { + nfc15693.setupRF(); + } else if (RFID_PN5180_NFC15693_STATE_DISABLEPRIVACYMODE == stateMachine) { + // check for ICODE-SLIX2 password protected tag + // put your privacy password here, e.g.: + // https://de.ifixit.com/Antworten/Ansehen/513422/nfc+Chips+f%C3%BCr+tonies+kaufen + // + // default factory password for ICODE-SLIX2 is {0x0F, 0x0F, 0x0F, 0x0F} + // + uint8_t password[] = {0x0F, 0x0F, 0x0F, 0x0F}; + ISO15693ErrorCode myrc = nfc15693.disablePrivacyMode(password); + if (ISO15693_EC_OK == myrc) { + if (showDisablePrivacyNotification) { + showDisablePrivacyNotification = false; + Log_Println("disabling privacy-mode successful", LOGLEVEL_NOTICE); } else { - // lastTimeDetected15693 is used to prevent "new card detection with old card" with single events where no card was detected - if (!lastTimeDetected15693 || (millis() - lastTimeDetected15693 >= 400)) { - lastTimeDetected15693 = 0; - #ifdef PAUSE_WHEN_RFID_REMOVED - cardAppliedCurrentRun = false; - #endif - for (uint8_t i=0; i pause - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - Log_Println((char *) FPSTR(rfidTagRemoved), LOGLEVEL_NOTICE); - } - cardAppliedLastRun = cardAppliedCurrentRun; - #endif - - // send card to queue - if (cardReceived) { - memcpy(cardId, uid, cardIdSize); - - // check for different card id - if (memcmp((const void *)cardId, (const void *)lastCardId, sizeof(cardId)) == 0) { - // reset state machine - if (RFID_PN5180_NFC14443_STATE_ACTIVE == stateMachine) { - stateMachine = RFID_PN5180_NFC14443_STATE_RESET; - continue; - } else if (RFID_PN5180_NFC15693_STATE_ACTIVE == stateMachine) { - stateMachine = RFID_PN5180_NFC15693_STATE_RESET; - continue; + } else if ((RFID_PN5180_NFC15693_STATE_GETINVENTORY == stateMachine) || (RFID_PN5180_NFC15693_STATE_GETINVENTORY_PRIVACY == stateMachine)) { + // try to read ISO15693 inventory + ISO15693ErrorCode rc = nfc15693.getInventory(uid); + if (rc == ISO15693_EC_OK) { + cardReceived = true; + stateMachine = RFID_PN5180_NFC15693_STATE_ACTIVE; + lastTimeDetected15693 = millis(); + #ifdef PAUSE_WHEN_RFID_REMOVED + cardAppliedCurrentRun = true; + #endif + } else { + // lastTimeDetected15693 is used to prevent "new card detection with old card" with single events where no card was detected + if (!lastTimeDetected15693 || (millis() - lastTimeDetected15693 >= 400)) { + lastTimeDetected15693 = 0; + #ifdef PAUSE_WHEN_RFID_REMOVED + cardAppliedCurrentRun = false; + #endif + for (uint8_t i = 0; i < cardIdSize; i++) { + lastCardId[i] = 0; } + } else { + stateMachine = RFID_PN5180_NFC15693_STATE_ACTIVE; } + } + } - memcpy(lastCardId, cardId, cardIdSize); - showDisablePrivacyNotification = true; - #ifdef HALLEFFECT_SENSOR_ENABLE - cardId[cardIdSize-1] = cardId[cardIdSize-1] + gHallEffectSensor.waitForState(HallEffectWaitMS); - #endif - - #ifdef PAUSE_WHEN_RFID_REMOVED - if (memcmp((const void *)lastValidcardId, (const void *)cardId, sizeof(cardId)) == 0) { - sameCardReapplied = true; - } - #endif + #ifdef PAUSE_WHEN_RFID_REMOVED + if (!cardAppliedCurrentRun && cardAppliedLastRun && !gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { // Card removed => pause + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); + Log_Println(rfidTagRemoved, LOGLEVEL_NOTICE); + } + cardAppliedLastRun = cardAppliedCurrentRun; + #endif - Log_Print((char *) FPSTR(rfidTagDetected), LOGLEVEL_NOTICE, true); - snprintf(Log_Buffer, Log_BufferLength, "(%s) ID: ", (RFID_PN5180_NFC14443_STATE_ACTIVE == stateMachine) ? "ISO-14443" : "ISO-15693"); - Log_Print(Log_Buffer, LOGLEVEL_NOTICE, false); + // send card to queue + if (cardReceived) { + memcpy(cardId, uid, cardIdSize); - for (uint8_t i = 0u; i < cardIdSize; i++) { - snprintf(Log_Buffer, Log_BufferLength, "%02x%s", cardId[i], (i < cardIdSize - 1u) ? "-" : "\n"); - Log_Print(Log_Buffer, LOGLEVEL_NOTICE, false); + // check for different card id + if (memcmp((const void *) cardId, (const void *) lastCardId, sizeof(cardId)) == 0) { + // reset state machine + if (RFID_PN5180_NFC14443_STATE_ACTIVE == stateMachine) { + stateMachine = RFID_PN5180_NFC14443_STATE_RESET; + continue; + } else if (RFID_PN5180_NFC15693_STATE_ACTIVE == stateMachine) { + stateMachine = RFID_PN5180_NFC15693_STATE_RESET; + continue; } + } - for (uint8_t i = 0u; i < cardIdSize; i++) { - char num[4]; - snprintf(num, sizeof(num), "%03d", cardId[i]); - cardIdString += num; - } + memcpy(lastCardId, cardId, cardIdSize); + showDisablePrivacyNotification = true; - #ifdef PAUSE_WHEN_RFID_REMOVED - #ifdef ACCEPT_SAME_RFID_AFTER_TRACK_END - if (!sameCardReapplied || gPlayProperties.trackFinished || gPlayProperties.playlistFinished) { // Don't allow to send card to queue if it's the same card again if track or playlist is unfnished - #else - if (!sameCardReapplied){ // Don't allow to send card to queue if it's the same card again... - #endif - xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); - } else { - // If pause-button was pressed while card was not applied, playback could be active. If so: don't pause when card is reapplied again as the desired functionality would be reversed in this case. - if (gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); // ... play/pause instead - Log_Println((char *) FPSTR(rfidTagReapplied), LOGLEVEL_NOTICE); - } - } - memcpy(lastValidcardId, uid, cardIdSize); - #else - xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); // If PAUSE_WHEN_RFID_REMOVED isn't active, every card-apply leads to new playlist-generation - #endif + #ifdef HALLEFFECT_SENSOR_ENABLE + cardId[cardIdSize - 1] = cardId[cardIdSize - 1] + gHallEffectSensor.waitForState(HallEffectWaitMS); + #endif + + #ifdef PAUSE_WHEN_RFID_REMOVED + if (memcmp((const void *) lastValidcardId, (const void *) cardId, sizeof(cardId)) == 0) { + sameCardReapplied = true; } + #endif - if (RFID_PN5180_NFC14443_STATE_ACTIVE == stateMachine) { // If 14443 is active, bypass 15693 as next check (performance) - stateMachine = RFID_PN5180_NFC14443_STATE_RESET; - } else if (RFID_PN5180_NFC15693_STATE_ACTIVE == stateMachine) { // If 15693 is active, bypass 14443 as next check (performance) - stateMachine = RFID_PN5180_NFC15693_STATE_RESET; + String hexString; + for (uint8_t i = 0u; i < cardIdSize; i++) { + char str[4]; + snprintf(str, sizeof(str), "%02x%c", cardId[i], (i < cardIdSize - 1u) ? '-' : ' '); + hexString += str; + } + Log_Printf(LOGLEVEL_NOTICE, rfidTagDetected, hexString.c_str()); + Log_Printf(LOGLEVEL_NOTICE, "Card type: %s", (RFID_PN5180_NFC14443_STATE_ACTIVE == stateMachine) ? "ISO-14443" : "ISO-15693"); + + for (uint8_t i = 0u; i < cardIdSize; i++) { + char num[4]; + snprintf(num, sizeof(num), "%03d", cardId[i]); + cardIdString += num; + } + + #ifdef PAUSE_WHEN_RFID_REMOVED + #ifdef ACCEPT_SAME_RFID_AFTER_TRACK_END + if (!sameCardReapplied || gPlayProperties.trackFinished || gPlayProperties.playlistFinished) { // Don't allow to send card to queue if it's the same card again if track or playlist is unfnished + #else + if (!sameCardReapplied) { // Don't allow to send card to queue if it's the same card again... + #endif + xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); } else { - stateMachine++; - if (stateMachine > RFID_PN5180_NFC15693_STATE_GETINVENTORY) { - stateMachine = RFID_PN5180_NFC14443_STATE_RESET; + // If pause-button was pressed while card was not applied, playback could be active. If so: don't pause when card is reapplied again as the desired functionality would be reversed in this case. + if (gPlayProperties.pausePlay && System_GetOperationMode() != OPMODE_BLUETOOTH_SINK) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); // ... play/pause instead + Log_Println(rfidTagReapplied, LOGLEVEL_NOTICE); } } + memcpy(lastValidcardId, uid, cardIdSize); + #else + xQueueSend(gRfidCardQueue, cardIdString.c_str(), 0); // If PAUSE_WHEN_RFID_REMOVED isn't active, every card-apply leads to new playlist-generation + #endif } - } - void Rfid_Exit(void) { - #ifdef PN5180_ENABLE_LPCD - Rfid_SetLpcdShutdownStatus(true); - while (Rfid_GetLpcdShutdownStatus()) { // Make sure init of LPCD is complete! - vTaskDelay(portTICK_RATE_MS * 10u); + if (RFID_PN5180_NFC14443_STATE_ACTIVE == stateMachine) { // If 14443 is active, bypass 15693 as next check (performance) + stateMachine = RFID_PN5180_NFC14443_STATE_RESET; + } else if (RFID_PN5180_NFC15693_STATE_ACTIVE == stateMachine) { // If 15693 is active, bypass 14443 as next check (performance) + stateMachine = RFID_PN5180_NFC15693_STATE_RESET; + } else { + stateMachine++; + if (stateMachine > RFID_PN5180_NFC15693_STATE_GETINVENTORY_PRIVACY) { + stateMachine = RFID_PN5180_NFC14443_STATE_RESET; } - #endif - vTaskDelete(rfidTaskHandle); + } } +} - // Handles activation of LPCD (while shutdown is in progress) - void Rfid_EnableLpcd(void) { - // goto low power card detection mode - #ifdef PN5180_ENABLE_LPCD - static PN5180 nfc(RFID_CS, RFID_BUSY, RFID_RST); - nfc.begin(); - nfc.reset(); - // show PN5180 reader version - uint8_t firmwareVersion[2]; - nfc.readEEprom(FIRMWARE_VERSION, firmwareVersion, sizeof(firmwareVersion)); - snprintf(Log_Buffer, Log_BufferLength, "%s%d.%d", (char *) F("PN5180 firmware version="), firmwareVersion[1], firmwareVersion[0]); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - // check firmware version: PN5180 firmware < 4.0 has several bugs preventing the LPCD mode - // you can flash latest firmware with this project: https://github.com/abidxraihan/PN5180_Updater_ESP32 - if (firmwareVersion[1] < 4) { - Log_Println((char *) F("This PN5180 firmware does not work with LPCD! use firmware >= 4.0"), LOGLEVEL_ERROR); - return; - } - Log_Println((char *) F("prepare low power card detection..."), LOGLEVEL_NOTICE); - uint8_t irqConfig = 0b0000000; // Set IRQ active low + clear IRQ-register - nfc.writeEEprom(IRQ_PIN_CONFIG, &irqConfig, 1); - /*nfc.readEEprom(IRQ_PIN_CONFIG, &irqConfig, 1); - Serial.print(F("IRQ_PIN_CONFIG=0x")); - Serial.println(irqConfig, HEX);*/ - nfc.prepareLPCD(); - Log_Println((char *) F("PN5180 IRQ PIN: "), LOGLEVEL_NOTICE); - Serial.println(Port_Read(RFID_IRQ)); - // turn on LPCD - uint16_t wakeupCounterInMs = 0x3FF; // must be in the range of 0x0 - 0xA82. max wake-up time is 2960 ms. - if (nfc.switchToLPCD(wakeupCounterInMs)) { - Log_Println((char *) F("switch to low power card detection: success"), LOGLEVEL_NOTICE); - // configure wakeup pin for deep-sleep wake-up, use ext1 - #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) - esp_sleep_enable_ext1_wakeup((1ULL << (RFID_IRQ)), ESP_EXT1_WAKEUP_ALL_LOW); - #endif - // freeze pin states in deep sleep - gpio_hold_en(gpio_num_t(RFID_CS)); // CS/NSS - gpio_hold_en(gpio_num_t(RFID_RST)); // RST - gpio_deep_sleep_hold_en(); - } else { - Log_Println((char *) F("switchToLPCD failed"), LOGLEVEL_ERROR); - } +void Rfid_Exit(void) { + #ifdef PN5180_ENABLE_LPCD + Rfid_SetLpcdShutdownStatus(true); + while (Rfid_GetLpcdShutdownStatus()) { // Make sure init of LPCD is complete! + vTaskDelay(portTICK_PERIOD_MS * 10u); + } + #endif + vTaskDelete(rfidTaskHandle); +} + +// Handles activation of LPCD (while shutdown is in progress) +void Rfid_EnableLpcd(void) { + // goto low power card detection mode + #ifdef PN5180_ENABLE_LPCD + static PN5180 nfc(RFID_CS, RFID_BUSY, RFID_RST); + nfc.begin(); + nfc.reset(); + // show PN5180 reader version + uint8_t firmwareVersion[2]; + nfc.readEEprom(FIRMWARE_VERSION, firmwareVersion, sizeof(firmwareVersion)); + Log_Printf(LOGLEVEL_DEBUG, "PN5180 firmware version=%d.%d", firmwareVersion[1], firmwareVersion[0]); + + // check firmware version: PN5180 firmware < 4.0 has several bugs preventing the LPCD mode + // you can flash latest firmware with this project: https://github.com/abidxraihan/PN5180_Updater_ESP32 + if (firmwareVersion[1] < 4) { + Log_Println("This PN5180 firmware does not work with LPCD! use firmware >= 4.0", LOGLEVEL_ERROR); + return; + } + Log_Println("prepare low power card detection...", LOGLEVEL_NOTICE); + uint8_t irqConfig = 0b0000000; // Set IRQ active low + clear IRQ-register + nfc.writeEEprom(IRQ_PIN_CONFIG, &irqConfig, 1); + /* + nfc.readEEprom(IRQ_PIN_CONFIG, &irqConfig, 1); + Log_Printf("IRQ_PIN_CONFIG=0x%02X", irqConfig) + */ + nfc.prepareLPCD(); + Log_Printf(LOGLEVEL_DEBUG, "PN5180 IRQ PIN (%d) state: %d", RFID_IRQ, Port_Read(RFID_IRQ)); + // turn on LPCD + uint16_t wakeupCounterInMs = 0x3FF; // must be in the range of 0x0 - 0xA82. max wake-up time is 2960 ms. + if (nfc.switchToLPCD(wakeupCounterInMs)) { + Log_Println("switch to low power card detection: success", LOGLEVEL_NOTICE); + // configure wakeup pin for deep-sleep wake-up, use ext1. For a real GPIO only, not PE + #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) + if (ESP_ERR_INVALID_ARG == esp_sleep_enable_ext1_wakeup((1ULL << (RFID_IRQ)), ESP_EXT1_WAKEUP_ALL_LOW)) { + Log_Printf(LOGLEVEL_ERROR, wrongWakeUpGpio, RFID_IRQ); + } #endif + // freeze pin states in deep sleep + gpio_hold_en(gpio_num_t(RFID_CS)); // CS/NSS + gpio_hold_en(gpio_num_t(RFID_RST)); // RST + gpio_deep_sleep_hold_en(); + } else { + Log_Println("switchToLPCD failed", LOGLEVEL_ERROR); } + #endif +} - // wake up from LPCD, check card is present. This works only for ISO-14443 compatible cards - void Rfid_WakeupCheck(void) { - #ifdef PN5180_ENABLE_LPCD - // disable pin hold from deep sleep - gpio_deep_sleep_hold_dis(); - gpio_hold_dis(gpio_num_t(RFID_CS)); // NSS - gpio_hold_dis(gpio_num_t(RFID_RST)); // RST - #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) - pinMode(RFID_IRQ, INPUT); - #endif - static PN5180ISO14443 nfc14443(RFID_CS, RFID_BUSY, RFID_RST); - nfc14443.begin(); - nfc14443.reset(); - // enable RF field - nfc14443.setupRF(); - if (!nfc14443.isCardPresent()) { - nfc14443.reset(); - uint16_t wakeupCounterInMs = 0x3FF; // needs to be in the range of 0x0 - 0xA82. max wake-up time is 2960 ms. - if (nfc14443.switchToLPCD(wakeupCounterInMs)) { - Log_Println((char *) FPSTR(lowPowerCardSuccess), LOGLEVEL_INFO); - // configure wakeup pin for deep-sleep wake-up, use ext1 - esp_sleep_enable_ext1_wakeup((1ULL << (RFID_IRQ)), ESP_EXT1_WAKEUP_ALL_LOW); - // freeze pin states in deep sleep - gpio_hold_en(gpio_num_t(RFID_CS)); // CS/NSS - gpio_hold_en(gpio_num_t(RFID_RST)); // RST - gpio_deep_sleep_hold_en(); - Log_Println((char *) FPSTR(wakeUpRfidNoIso14443), LOGLEVEL_ERROR); - esp_deep_sleep_start(); - } else { - Log_Println((char *) F("switchToLPCD failed"), LOGLEVEL_ERROR); - } - } - nfc14443.end(); +// wake up from LPCD, check card is present. This works only for ISO-14443 compatible cards +void Rfid_WakeupCheck(void) { + #ifdef PN5180_ENABLE_LPCD + // disable pin hold from deep sleep + gpio_deep_sleep_hold_dis(); + gpio_hold_dis(gpio_num_t(RFID_CS)); // NSS + gpio_hold_dis(gpio_num_t(RFID_RST)); // RST + #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) + pinMode(RFID_IRQ, INPUT); + #endif + static PN5180ISO14443 nfc14443(RFID_CS, RFID_BUSY, RFID_RST); + nfc14443.begin(); + nfc14443.reset(); + // enable RF field + nfc14443.setupRF(); + if (!nfc14443.isCardPresent()) { + nfc14443.reset(); + uint16_t wakeupCounterInMs = 0x3FF; // needs to be in the range of 0x0 - 0xA82. max wake-up time is 2960 ms. + if (nfc14443.switchToLPCD(wakeupCounterInMs)) { + Log_Println(lowPowerCardSuccess, LOGLEVEL_INFO); + // configure wakeup pin for deep-sleep wake-up, use ext1 + #if (RFID_IRQ >= 0 && RFID_IRQ <= MAX_GPIO) + // configure wakeup pin for deep-sleep wake-up, use ext1. For a real GPIO only, not PE + esp_sleep_enable_ext1_wakeup((1ULL << (RFID_IRQ)), ESP_EXT1_WAKEUP_ALL_LOW); #endif + #if (defined(PORT_EXPANDER_ENABLE) && (RFID_IRQ > 99)) + // reset IRQ state on port-expander + Port_Exit(); + #endif + // freeze pin states in deep sleep + gpio_hold_en(gpio_num_t(RFID_CS)); // CS/NSS + gpio_hold_en(gpio_num_t(RFID_RST)); // RST + gpio_deep_sleep_hold_en(); + Log_Println(wakeUpRfidNoIso14443, LOGLEVEL_ERROR); + esp_deep_sleep_start(); + } else { + Log_Println("switchToLPCD failed", LOGLEVEL_ERROR); + } } + nfc14443.end(); + #endif +} #endif diff --git a/src/RotaryEncoder.cpp b/src/RotaryEncoder.cpp index 46f1b7b6..7cec3a61 100644 --- a/src/RotaryEncoder.cpp +++ b/src/RotaryEncoder.cpp @@ -1,76 +1,59 @@ #include #include "settings.h" + #include "RotaryEncoder.h" + #include "AudioPlayer.h" #include "Log.h" #include "System.h" #ifdef USEROTARY_ENABLE -#include + #include #endif // Rotary encoder-configuration #ifdef USEROTARY_ENABLE - ESP32Encoder encoder; - // Rotary encoder-helper - int32_t lastEncoderValue; - int32_t currentEncoderValue; - int32_t lastVolume = -1; // Don't change -1 as initial-value! +ESP32Encoder encoder; +// Rotary encoder-helper +int32_t lastEncoderValue; +int32_t currentEncoderValue; +int32_t lastVolume = -1; // Don't change -1 as initial-value! #endif void RotaryEncoder_Init(void) { - // Init rotary encoder - #ifdef USEROTARY_ENABLE - #ifndef REVERSE_ROTARY - encoder.attachHalfQuad(ROTARYENCODER_CLK, ROTARYENCODER_DT); - #else - encoder.attachHalfQuad(ROTARYENCODER_DT, ROTARYENCODER_CLK); - #endif - encoder.clearCount(); - encoder.setCount(AudioPlayer_GetInitVolume() * 2); // Ganzes Raster ist immer +2, daher initiale Lautstärke mit 2 multiplizieren +// Init rotary encoder +#ifdef USEROTARY_ENABLE + #ifndef REVERSE_ROTARY + encoder.attachHalfQuad(ROTARYENCODER_CLK, ROTARYENCODER_DT); + #else + encoder.attachHalfQuad(ROTARYENCODER_DT, ROTARYENCODER_CLK); #endif + encoder.clearCount(); +#endif } void RotaryEncoder_Readjust(void) { - #ifdef USEROTARY_ENABLE - encoder.clearCount(); - encoder.setCount(AudioPlayer_GetCurrentVolume() * 2); - #endif } // Handles volume directed by rotary encoder void RotaryEncoder_Cyclic(void) { - #ifdef USEROTARY_ENABLE -#ifdef INCLUDE_ROTARY_IN_CONTROLS_LOCK - if (System_AreControlsLocked()) { - encoder.clearCount(); - encoder.setCount(AudioPlayer_GetCurrentVolume() * 2); - return; - } -#endif - - currentEncoderValue = encoder.getCount(); - // Only if initial run or value has changed. And only after "full step" of rotary encoder - if (((lastEncoderValue != currentEncoderValue) || lastVolume == -1) && (currentEncoderValue % 2 == 0)) { - System_UpdateActivityTimer(); // Set inactivity back if rotary encoder was used - if ((AudioPlayer_GetMaxVolume() * 2) < currentEncoderValue) { - encoder.clearCount(); - encoder.setCount(AudioPlayer_GetMaxVolume() * 2); - Log_Println((char *) FPSTR(maxLoudnessReached), LOGLEVEL_INFO); - currentEncoderValue = encoder.getCount(); - } else if (currentEncoderValue < AudioPlayer_GetMinVolume()) { - encoder.clearCount(); - encoder.setCount(AudioPlayer_GetMinVolume()); - Log_Println((char *) FPSTR(minLoudnessReached), LOGLEVEL_INFO); - currentEncoderValue = encoder.getCount(); - } - lastEncoderValue = currentEncoderValue; - AudioPlayer_SetCurrentVolume(lastEncoderValue / 2u); - if (AudioPlayer_GetCurrentVolume() != lastVolume) { - AudioPlayer_PauseOnMinVolume(lastVolume, AudioPlayer_GetCurrentVolume()); - lastVolume = AudioPlayer_GetCurrentVolume(); - AudioPlayer_VolumeToQueueSender(AudioPlayer_GetCurrentVolume(), false); - } - } +#ifdef USEROTARY_ENABLE + #ifdef INCLUDE_ROTARY_IN_CONTROLS_LOCK + if (System_AreControlsLocked()) { + encoder.clearCount(); + return; + } #endif + + int32_t encoderValue = encoder.getCount(); + // Only if initial run or value has changed. And only after "full step" of rotary encoder + if ((encoderValue != 0) && (encoderValue % 2 == 0)) { + System_UpdateActivityTimer(); // Set inactivity back if rotary encoder was used + // just reset the encoder here, so we get a new delta next time + encoder.clearCount(); + auto currentVol = AudioPlayer_GetCurrentVolume(); + AudioPlayer_VolumeToQueueSender(currentVol + (encoderValue / 2), false); + return; + } +#endif } diff --git a/src/SdCard.cpp b/src/SdCard.cpp index fd43542f..e6b1e1f3 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -1,6 +1,8 @@ #include #include "settings.h" + #include "SdCard.h" + #include "Common.h" #include "Led.h" #include "Log.h" @@ -8,164 +10,184 @@ #include "System.h" #ifdef SD_MMC_1BIT_MODE - fs::FS gFSystem = (fs::FS)SD_MMC; +fs::FS gFSystem = (fs::FS) SD_MMC; #else - SPIClass spiSD(HSPI); - fs::FS gFSystem = (fs::FS)SD; +SPIClass spiSD(HSPI); +fs::FS gFSystem = (fs::FS) SD; #endif void SdCard_Init(void) { - #ifndef SINGLE_SPI_ENABLE - #ifdef SD_MMC_1BIT_MODE - pinMode(2, INPUT_PULLUP); - while (!SD_MMC.begin("/sdcard", true)) { - #else - pinMode(SPISD_CS, OUTPUT); - digitalWrite(SPISD_CS, HIGH); - spiSD.begin(SPISD_SCK, SPISD_MISO, SPISD_MOSI, SPISD_CS); - spiSD.setFrequency(1000000); - while (!SD.begin(SPISD_CS, spiSD)) { - #endif +#ifdef NO_SDCARD + // Initialize without any SD card, e.g. for webplayer only + Log_Println("Init without SD card ", LOGLEVEL_NOTICE); + return +#endif + +#ifndef SINGLE_SPI_ENABLE + #ifdef SD_MMC_1BIT_MODE + pinMode(2, INPUT_PULLUP); + while (!SD_MMC.begin("/sdcard", true)) { #else - #ifdef SD_MMC_1BIT_MODE - pinMode(2, INPUT_PULLUP); - while (!SD_MMC.begin("/sdcard", true)) { - #else - while (!SD.begin(SPISD_CS)) { - #endif + pinMode(SPISD_CS, OUTPUT); + digitalWrite(SPISD_CS, HIGH); + spiSD.begin(SPISD_SCK, SPISD_MISO, SPISD_MOSI, SPISD_CS); + spiSD.setFrequency(1000000); + while (!SD.begin(SPISD_CS, spiSD)) { #endif - Log_Println((char *) FPSTR(unableToMountSd), LOGLEVEL_ERROR); - delay(500); - #ifdef SHUTDOWN_IF_SD_BOOT_FAILS - if (millis() >= deepsleepTimeAfterBootFails * 1000) { - Log_Println((char *) FPSTR(sdBootFailedDeepsleep), LOGLEVEL_ERROR); - esp_deep_sleep_start(); - } +#else + #ifdef SD_MMC_1BIT_MODE + pinMode(2, INPUT_PULLUP); + while (!SD_MMC.begin("/sdcard", true)) { + #else + while (!SD.begin(SPISD_CS)) { #endif - } +#endif + Log_Println(unableToMountSd, LOGLEVEL_ERROR); + delay(500); +#ifdef SHUTDOWN_IF_SD_BOOT_FAILS + if (millis() >= deepsleepTimeAfterBootFails * 1000) { + Log_Println(sdBootFailedDeepsleep, LOGLEVEL_ERROR); + esp_deep_sleep_start(); + } +#endif + } } void SdCard_Exit(void) { - // SD card goto idle mode - #ifdef SD_MMC_1BIT_MODE - SD_MMC.end(); - #endif +// SD card goto idle mode +#ifdef SINGLE_SPI_ENABLE + Log_Println("shutdown SD card (SPI)..", LOGLEVEL_NOTICE); + SD.end(); +#endif +#ifdef SD_MMC_1BIT_MODE + Log_Println("shutdown SD card (SD_MMC)..", LOGLEVEL_NOTICE); + SD_MMC.end(); +#endif } sdcard_type_t SdCard_GetType(void) { sdcard_type_t cardType; - #ifdef SD_MMC_1BIT_MODE - Log_Println((char *) FPSTR(sdMountedMmc1BitMode), LOGLEVEL_NOTICE); - cardType = SD_MMC.cardType(); - #else - Log_Println((char *) FPSTR(sdMountedSpiMode), LOGLEVEL_NOTICE); - cardType = SD.cardType(); - #endif - return cardType; +#ifdef SD_MMC_1BIT_MODE + Log_Println(sdMountedMmc1BitMode, LOGLEVEL_NOTICE); + cardType = SD_MMC.cardType(); +#else + Log_Println(sdMountedSpiMode, LOGLEVEL_NOTICE); + cardType = SD.cardType(); +#endif + return cardType; } uint64_t SdCard_GetSize() { - #ifdef SD_MMC_1BIT_MODE - return SD_MMC.cardSize(); - #else - return SD.cardSize(); - #endif +#ifdef SD_MMC_1BIT_MODE + return SD_MMC.cardSize(); +#else + return SD.cardSize(); +#endif } uint64_t SdCard_GetFreeSize() { - #ifdef SD_MMC_1BIT_MODE - return SD_MMC.cardSize() - SD_MMC.usedBytes(); - #else - return SD.cardSize() - SD.usedBytes(); - #endif +#ifdef SD_MMC_1BIT_MODE + return SD_MMC.cardSize() - SD_MMC.usedBytes(); +#else + return SD.cardSize() - SD.usedBytes(); +#endif } void SdCard_PrintInfo() { // show SD card type sdcard_type_t cardType = SdCard_GetType(); - Log_Print((char *) F("SD card type: "), LOGLEVEL_DEBUG, true ); - if (cardType == CARD_MMC) { - Log_Println((char *) F("MMC"), LOGLEVEL_DEBUG); - } else if (cardType == CARD_SD) { - Log_Println((char *) F("SDSC"), LOGLEVEL_DEBUG); - } else if (cardType == CARD_SDHC) { - Log_Println((char *) F("SDHC"), LOGLEVEL_DEBUG); - } else { - Log_Println((char *) F("UNKNOWN"), LOGLEVEL_DEBUG); + const char *type = "UNKNOWN"; + switch (cardType) { + case CARD_MMC: + type = "MMC"; + break; + + case CARD_SD: + type = "SDSC"; + break; + + case CARD_SDHC: + type = "SDHC"; + break; + + default: + break; } + Log_Printf(LOGLEVEL_DEBUG, "SD card type: %s", type); // show SD card size / free space uint64_t cardSize = SdCard_GetSize() / (1024 * 1024); - uint64_t freeSize = SdCard_GetFreeSize() / (1024 * 1024);; - snprintf(Log_Buffer, Log_BufferLength, "%s: %llu MB / %llu MB", (char *) FPSTR(sdInfo), cardSize, freeSize); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + uint64_t freeSize = SdCard_GetFreeSize() / (1024 * 1024); + ; + Log_Printf(LOGLEVEL_NOTICE, sdInfo, cardSize, freeSize); } - // Check if file-type is correct bool fileValid(const char *_fileItem) { + // make file extension to lowercase (compare case insenstive) + char *lFileItem; + lFileItem = x_strdup(_fileItem); + if (lFileItem == NULL) { + return false; + } + lFileItem = strlwr(lFileItem); const char ch = '/'; char *subst; - subst = strrchr(_fileItem, ch); // Don't use files that start with . - - return (!startsWith(subst, (char *) "/.")) && ( - endsWith(_fileItem, ".mp3") || endsWith(_fileItem, ".MP3") || - endsWith(_fileItem, ".aac") || endsWith(_fileItem, ".AAC") || - endsWith(_fileItem, ".m3u") || endsWith(_fileItem, ".M3U") || - endsWith(_fileItem, ".m4a") || endsWith(_fileItem, ".M4A") || - endsWith(_fileItem, ".wav") || endsWith(_fileItem, ".WAV") || - endsWith(_fileItem, ".flac") || endsWith(_fileItem, ".FLAC") || - endsWith(_fileItem, ".asx") || endsWith(_fileItem, ".ASX")); + subst = strrchr(lFileItem, ch); // Don't use files that start with . + bool isValid = (!startsWith(subst, (char *) "/.")) && ( + // audio file formats + endsWith(lFileItem, ".mp3") || endsWith(lFileItem, ".aac") || endsWith(lFileItem, ".m4a") || endsWith(lFileItem, ".wav") || endsWith(lFileItem, ".flac") || endsWith(lFileItem, ".ogg") || endsWith(lFileItem, ".oga") || endsWith(lFileItem, ".opus") || + // playlist file formats + endsWith(lFileItem, ".m3u") || endsWith(lFileItem, ".m3u8") || endsWith(lFileItem, ".pls") || endsWith(lFileItem, ".asx")); + free(lFileItem); + return isValid; } - // Takes a directory as input and returns a random subdirectory from it char *SdCard_pickRandomSubdirectory(char *_directory) { + uint32_t listStartTimestamp = millis(); + // Look if file/folder requested really exists. If not => break. File directory = gFSystem.open(_directory); if (!directory) { - Log_Println((char *) FPSTR(dirOrFileDoesNotExist), LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, _directory); return NULL; } - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(tryToPickRandomDir), _directory); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, tryToPickRandomDir, _directory); static uint8_t allocCount = 1; - uint16_t allocSize = psramInit() ? 65535 : 1024; // There's enough PSRAM. So we don't have to care... + uint16_t allocSize = psramInit() ? 65535 : 1024; // There's enough PSRAM. So we don't have to care... uint16_t directoryCount = 0; - char *buffer = _directory; // input char* is reused as it's content no longer needed + char *buffer = _directory; // input char* is reused as it's content no longer needed char *subdirectoryList = (char *) x_calloc(allocSize, sizeof(char)); if (subdirectoryList == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForLinearPlaylist), LOGLEVEL_ERROR); + Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); System_IndicateError(); return NULL; } // Create linear list of subdirectories with #-delimiters while (true) { - File fileItem = directory.openNextFile(); - if (!fileItem) { + bool isDir = false; + String MyfileName = directory.getNextFileName(&isDir); + if (MyfileName == "") { break; } - if (!fileItem.isDirectory()) { + if (!isDir) { continue; } else { - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - strncpy(buffer, (char *) fileItem.path(), 255); - #else - strncpy(buffer, (char *) fileItem.name(), 255); - #endif - - /*snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(nameOfFileFound), buffer); - Log_Println(Log_Buffer, LOGLEVEL_INFO);*/ + strncpy(buffer, MyfileName.c_str(), 255); + // Log_Printf(LOGLEVEL_INFO, nameOfFileFound, buffer); if ((strlen(subdirectoryList) + strlen(buffer) + 2) >= allocCount * allocSize) { - subdirectoryList = (char *) realloc(subdirectoryList, ++allocCount * allocSize); - Log_Println((char *) FPSTR(reallocCalled), LOGLEVEL_DEBUG); - if (subdirectoryList == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForLinearPlaylist), LOGLEVEL_ERROR); + char *tmp = (char *) realloc(subdirectoryList, ++allocCount * allocSize); + Log_Println(reallocCalled, LOGLEVEL_DEBUG); + if (tmp == NULL) { + Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); System_IndicateError(); + free(subdirectoryList); return NULL; } + subdirectoryList = tmp; } strcat(subdirectoryList, stringDelimiter); strcat(subdirectoryList, buffer); @@ -179,120 +201,70 @@ char *SdCard_pickRandomSubdirectory(char *_directory) { return NULL; } - uint16_t randomNumber = random(directoryCount) + 1; // Create random-number with max = subdirectory-count + uint16_t randomNumber = random(directoryCount) + 1; // Create random-number with max = subdirectory-count uint16_t delimiterFoundCount = 0; - uint32_t a=0; - uint8_t b=0; + uint32_t a = 0; + uint8_t b = 0; // Walk through subdirectory-array and extract randomized subdirectory while (subdirectoryList[a] != '\0') { if (subdirectoryList[a] == '#') { delimiterFoundCount++; } else { - if (delimiterFoundCount == randomNumber) { // Pick subdirectory of linear char* according to random number + if (delimiterFoundCount == randomNumber) { // Pick subdirectory of linear char* according to random number buffer[b++] = subdirectoryList[a]; } } - if (delimiterFoundCount > randomNumber || (b == 254)) { // It's over when next delimiter is found or buffer is full + if (delimiterFoundCount > randomNumber || (b == 254)) { // It's over when next delimiter is found or buffer is full buffer[b] = '\0'; free(subdirectoryList); - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(pickedRandomDir), _directory); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - return buffer; // Full path of random subdirectory + Log_Printf(LOGLEVEL_NOTICE, pickedRandomDir, _directory); + return buffer; // Full path of random subdirectory } a++; } free(subdirectoryList); + Log_Printf(LOGLEVEL_DEBUG, "pick random directory from SD-card finished: %lu ms", (millis() - listStartTimestamp)); return NULL; } - /* Puts SD-file(s) or directory into a playlist First element of array always contains the number of payload-items. */ char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { static char **files; - char *serializedPlaylist; - char fileNameBuf[255]; - char cacheFileNameBuf[275]; - bool readFromCacheFile = false; - bool enablePlaylistCaching = false; + char *serializedPlaylist = NULL; bool enablePlaylistFromM3u = false; + // uint32_t listStartTimestamp = millis(); + + if (files != NULL) { // If **ptr already exists, de-allocate its memory + Log_Printf(LOGLEVEL_DEBUG, releaseMemoryOfOldPlaylist, ESP.getFreeHeap()); + freeMultiCharArray(files, strtoul(files[0], NULL, 10) + 1); + files = nullptr; + Log_Printf(LOGLEVEL_DEBUG, freeMemoryAfterFree, ESP.getFreeHeap()); + } // Look if file/folder requested really exists. If not => break. File fileOrDirectory = gFSystem.open(fileName); if (!fileOrDirectory) { - Log_Println((char *) FPSTR(dirOrFileDoesNotExist), LOGLEVEL_ERROR); - return NULL; + Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, fileName); + return nullptr; } - // Create linear playlist of caching-file - #ifdef CACHED_PLAYLIST_ENABLE - strncpy(cacheFileNameBuf, fileName, sizeof(cacheFileNameBuf)); - strcat(cacheFileNameBuf, "/"); - strcat(cacheFileNameBuf, (const char*) FPSTR(playlistCacheFile)); // Build absolute path of cacheFile - - // Decide if to use cacheFile. It needs to exist first... - if (gFSystem.exists(cacheFileNameBuf)) { // Check if cacheFile (already) exists - readFromCacheFile = true; - } - - // ...and playmode has to be != random/single (as random along with caching doesn't make sense at all) - if (_playMode == SINGLE_TRACK || - _playMode == SINGLE_TRACK_LOOP) { - readFromCacheFile = false; - } else { - enablePlaylistCaching = true; - } - - // Read linear playlist (csv with #-delimiter) from cachefile (faster!) - if (readFromCacheFile) { - File cacheFile = gFSystem.open(cacheFileNameBuf); - if (cacheFile) { - uint32_t cacheFileSize = cacheFile.size(); - - if (!(cacheFileSize >= 1)) { // Make sure it's greater than 0 bytes - Log_Println((char *) FPSTR(playlistCacheFoundBut0), LOGLEVEL_ERROR); - readFromCacheFile = false; - } else { - Log_Println((char *) FPSTR(playlistGenModeCached), LOGLEVEL_NOTICE); - serializedPlaylist = (char *) x_calloc(cacheFileSize+10, sizeof(char)); - - char buf; - uint32_t fPos = 0; - while (cacheFile.available() > 0) { - buf = cacheFile.read(); - serializedPlaylist[fPos++] = buf; - } - } - cacheFile.close(); - } - } - #endif - - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(freeMemory), ESP.getFreeHeap()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - - if (files != NULL) { // If **ptr already exists, de-allocate its memory - Log_Println((char *) FPSTR(releaseMemoryOfOldPlaylist), LOGLEVEL_DEBUG); - --files; - freeMultiCharArray(files, strtoul(*files, NULL, 10)); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(freeMemoryAfterFree), ESP.getFreeHeap()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - } + Log_Printf(LOGLEVEL_DEBUG, freeMemory, ESP.getFreeHeap()); // Parse m3u-playlist and create linear-playlist out of it if (_playMode == LOCAL_M3U) { - if (fileOrDirectory && !fileOrDirectory.isDirectory() && fileOrDirectory.size() >= 0) { + if (fileOrDirectory && !fileOrDirectory.isDirectory() && fileOrDirectory.size() > 0) { enablePlaylistFromM3u = true; uint16_t allocCount = 1; - uint16_t allocSize = psramInit() ? 65535 : 1024; // There's enough PSRAM. So we don't have to care... + uint16_t allocSize = psramInit() ? 65535 : 1024; // There's enough PSRAM. So we don't have to care... serializedPlaylist = (char *) x_calloc(allocSize, sizeof(char)); if (serializedPlaylist == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForLinearPlaylist), LOGLEVEL_ERROR); + Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); System_IndicateError(); - return files; + return nullptr; } char buf; char lastBuf = '#'; @@ -301,59 +273,61 @@ char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { serializedPlaylist[0] = '#'; while (fileOrDirectory.available() > 0) { buf = fileOrDirectory.read(); - if (fPos+1 >= allocCount * allocSize) { - serializedPlaylist = (char *) realloc(serializedPlaylist, ++allocCount * allocSize); - Log_Println((char *) FPSTR(reallocCalled), LOGLEVEL_DEBUG); - if (serializedPlaylist == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForLinearPlaylist), LOGLEVEL_ERROR); + if (buf == '#') { + // skip M3U comment lines starting with # + fileOrDirectory.readStringUntil('\n'); + continue; + } + if (fPos + 1 >= allocCount * allocSize) { + char *tmp = (char *) realloc(serializedPlaylist, ++allocCount * allocSize); + Log_Println(reallocCalled, LOGLEVEL_DEBUG); + if (tmp == NULL) { + Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); System_IndicateError(); free(serializedPlaylist); - return files; + return nullptr; } + serializedPlaylist = tmp; } if (buf != '\n' && buf != '\r') { serializedPlaylist[fPos++] = buf; lastBuf = buf; } else { - if (lastBuf != '#') { // Strip empty lines from m3u + if (lastBuf != '#') { // Strip empty lines from m3u serializedPlaylist[fPos++] = '#'; lastBuf = '#'; } } } - if (serializedPlaylist[fPos-1] == '#') { // Remove trailing delimiter if set - serializedPlaylist[fPos-1] = '\0'; + if (serializedPlaylist[fPos - 1] == '#') { // Remove trailing delimiter if set + serializedPlaylist[fPos - 1] = '\0'; } } else { - return files; + return nullptr; } } - // Don't read from cachefile or m3u-file. Means: read filenames from SD and make playlist of it - if (!readFromCacheFile && !enablePlaylistFromM3u) { - Log_Println((char *) FPSTR(playlistGenModeUncached), LOGLEVEL_NOTICE); + // Don't read from m3u-file. Means: read filenames from SD and make playlist of it + if (!enablePlaylistFromM3u) { + Log_Println(playlistGen, LOGLEVEL_NOTICE); + char fileNameBuf[255]; // File-mode if (!fileOrDirectory.isDirectory()) { files = (char **) x_malloc(sizeof(char *) * 2); - if (files == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForPlaylist), LOGLEVEL_ERROR); + if (files == nullptr) { + Log_Println(unableToAllocateMemForPlaylist, LOGLEVEL_ERROR); System_IndicateError(); - return NULL; + return nullptr; } - Log_Println((char *) FPSTR(fileModeDetected), LOGLEVEL_INFO); - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - strncpy(fileNameBuf, (char *) fileOrDirectory.path(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); - #else - strncpy(fileNameBuf, (char *) fileOrDirectory.name(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); - #endif + Log_Println(fileModeDetected, LOGLEVEL_INFO); + strncpy(fileNameBuf, fileOrDirectory.path(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); if (fileValid(fileNameBuf)) { - files = (char **) x_malloc(sizeof(char *) * 2); files[1] = x_strdup(fileNameBuf); } files[0] = x_strdup("1"); // Number of files is always 1 in file-mode - return ++files; + return &(files[1]); } // Directory-mode (linear-playlist) @@ -364,51 +338,35 @@ char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { } serializedPlaylist = (char *) x_calloc(allocSize, sizeof(char)); - File cacheFile; - if (enablePlaylistCaching) { - cacheFile = gFSystem.open(cacheFileNameBuf, FILE_WRITE); - } - while (true) { - File fileItem = fileOrDirectory.openNextFile(); - if (!fileItem) { + bool isDir = false; + String MyfileName = fileOrDirectory.getNextFileName(&isDir); + if (MyfileName == "") { break; } - if (fileItem.isDirectory()) { + if (isDir) { continue; } else { - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - strncpy(fileNameBuf, (char *) fileItem.path(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); - #else - strncpy(fileNameBuf, (char *) fileItem.name(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); - #endif - - // Don't support filenames that start with "." and only allow .mp3 + strncpy(fileNameBuf, MyfileName.c_str(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); + // Don't support filenames that start with "." and only allow .mp3 and other supported audio file formats if (fileValid(fileNameBuf)) { - /*snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(nameOfFileFound), fileNameBuf); - Log_Println(Log_Buffer, LOGLEVEL_INFO);*/ + // Log_Printf(LOGLEVEL_INFO, "%s: %s", nameOfFileFound), fileNameBuf); if ((strlen(serializedPlaylist) + strlen(fileNameBuf) + 2) >= allocCount * allocSize) { - serializedPlaylist = (char *) realloc(serializedPlaylist, ++allocCount * allocSize); - Log_Println((char *) FPSTR(reallocCalled), LOGLEVEL_DEBUG); - if (serializedPlaylist == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForLinearPlaylist), LOGLEVEL_ERROR); + char *tmp = (char *) realloc(serializedPlaylist, ++allocCount * allocSize); + Log_Println(reallocCalled, LOGLEVEL_DEBUG); + if (tmp == nullptr) { + Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); System_IndicateError(); - return files; + free(serializedPlaylist); + return nullptr; } + serializedPlaylist = tmp; } strcat(serializedPlaylist, stringDelimiter); strcat(serializedPlaylist, fileNameBuf); - if (cacheFile && enablePlaylistCaching) { - cacheFile.print(stringDelimiter); - cacheFile.print(fileNameBuf); // Write linear playlist to cacheFile - } } } } - - if (cacheFile && enablePlaylistCaching) { - cacheFile.close(); - } } // Get number of elements out of serialized playlist @@ -420,13 +378,12 @@ char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { } // Alloc only necessary number of playlist-pointers - files = (char **) x_malloc(sizeof(char *) * cnt + 1); - - if (files == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForPlaylist), LOGLEVEL_ERROR); + files = (char **) x_malloc(sizeof(char *) * (cnt + 1)); + if (files == nullptr) { + Log_Println(unableToAllocateMemForPlaylist, LOGLEVEL_ERROR); System_IndicateError(); free(serializedPlaylist); - return NULL; + return nullptr; } // Extract elements out of serialized playlist and copy to playlist @@ -442,14 +399,16 @@ char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { files[0] = (char *) x_malloc(sizeof(char) * 5); - if (files[0] == NULL) { - Log_Println((char *) FPSTR(unableToAllocateMemForPlaylist), LOGLEVEL_ERROR); + if (files[0] == nullptr) { + Log_Println(unableToAllocateMemForPlaylist, LOGLEVEL_ERROR); System_IndicateError(); - return NULL; + freeMultiCharArray(files, cnt + 1); + files = nullptr; + return nullptr; } - sprintf(files[0], "%u", cnt); - snprintf(Log_Buffer, Log_BufferLength, "%s: %d", (char *) FPSTR(numberOfValidFiles), cnt); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + snprintf(files[0], 5, "%u", cnt); + Log_Printf(LOGLEVEL_NOTICE, numberOfValidFiles, cnt); + // Log_Printf(LOGLEVEL_DEBUG, "build playlist from SD-card finished: %lu ms", (millis() - listStartTimestamp)); - return ++files; // return ptr+1 (starting at 1st payload-item); ptr+0 contains number of items + return &(files[1]); // return ptr+1 (starting at 1st payload-item); ptr+0 contains number of items } diff --git a/src/System.cpp b/src/System.cpp index d396a229..a37b6711 100644 --- a/src/System.cpp +++ b/src/System.cpp @@ -1,32 +1,34 @@ #include #include "settings.h" + #include "System.h" + +#include "Audio.h" #include "AudioPlayer.h" -#include "Rfid.h" #include "Led.h" #include "Log.h" #include "Mqtt.h" -#include "SdCard.h" #include "Port.h" +#include "Power.h" +#include "Rfid.h" +#include "SdCard.h" +#include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "esp_system.h" -#include "Audio.h" -#include "Power.h" -constexpr const char prefsRfidNamespace[] PROGMEM = "rfidTags"; // Namespace used to save IDs of rfid-tags -constexpr const char prefsSettingsNamespace[] PROGMEM = "settings"; // Namespace used for generic settings +constexpr const char prefsRfidNamespace[] = "rfidTags"; // Namespace used to save IDs of rfid-tags +constexpr const char prefsSettingsNamespace[] = "settings"; // Namespace used for generic settings Preferences gPrefsRfid; Preferences gPrefsSettings; -unsigned long System_LastTimeActiveTimestamp = 0u; // Timestamp of last user-interaction +unsigned long System_LastTimeActiveTimestamp = 0u; // Timestamp of last user-interaction unsigned long System_SleepTimerStartTimestamp = 0u; // Flag if sleep-timer is active -bool System_GoToSleep = false; // Flag for turning uC immediately into deepsleep -bool System_Sleeping = false; // Flag for turning into deepsleep is in progress -bool System_LockControls = false; // Flag if buttons and rotary encoder is locked -uint8_t System_MaxInactivityTime = 10u; // Time in minutes, after uC is put to deep sleep because of inactivity (and modified later via GUI) -uint8_t System_SleepTimer = 30u; // Sleep timer in minutes that can be optionally used (and modified later via MQTT or RFID) +bool System_GoToSleep = false; // Flag for turning uC immediately into deepsleep +bool System_Sleeping = false; // Flag for turning into deepsleep is in progress +bool System_LockControls = false; // Flag if buttons and rotary encoder is locked +uint8_t System_MaxInactivityTime = 10u; // Time in minutes, after uC is put to deep sleep because of inactivity (and modified later via GUI) +uint8_t System_SleepTimer = 30u; // Sleep timer in minutes that can be optionally used (and modified later via MQTT or RFID) // Operation Mode volatile uint8_t System_OperationMode; @@ -37,18 +39,17 @@ void System_DeepSleepManager(void); void System_Init(void) { srand(esp_random()); - gPrefsRfid.begin((char *) FPSTR(prefsRfidNamespace)); - gPrefsSettings.begin((char *) FPSTR(prefsSettingsNamespace)); + gPrefsRfid.begin(prefsRfidNamespace); + gPrefsSettings.begin(prefsSettingsNamespace); // Get maximum inactivity-time from NVS uint32_t nvsMInactivityTime = gPrefsSettings.getUInt("mInactiviyT", 0); if (nvsMInactivityTime) { System_MaxInactivityTime = nvsMInactivityTime; - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(restoredMaxInactivityFromNvs), nvsMInactivityTime); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, restoredMaxInactivityFromNvs, nvsMInactivityTime); } else { gPrefsSettings.putUInt("mInactiviyT", System_MaxInactivityTime); - Log_Println((char *) FPSTR(wroteMaxInactivityToNvs), LOGLEVEL_ERROR); + Log_Println(wroteMaxInactivityToNvs, LOGLEVEL_ERROR); } System_OperationMode = gPrefsSettings.getUChar("operationMode", OPMODE_NORMAL); @@ -67,10 +68,6 @@ void System_RequestSleep(void) { System_GoToSleep = true; } -bool System_IsSleepRequested(void) { - return System_GoToSleep; -} - bool System_SetSleepTimer(uint8_t minutes) { bool sleepTimerEnabled = false; @@ -78,7 +75,7 @@ bool System_SetSleepTimer(uint8_t minutes) { System_SleepTimerStartTimestamp = 0u; System_SleepTimer = 0u; Led_ResetToInitialBrightness(); - Log_Println((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepd, LOGLEVEL_NOTICE); } else { System_SleepTimerStartTimestamp = millis(); System_SleepTimer = minutes; @@ -86,20 +83,20 @@ bool System_SetSleepTimer(uint8_t minutes) { Led_ResetToNightBrightness(); if (minutes == 15) { - Log_Println((char *)FPSTR(modificatorSleepTimer15), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepTimer15, LOGLEVEL_NOTICE); } else if (minutes == 30) { - Log_Println((char *)FPSTR(modificatorSleepTimer30), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepTimer30, LOGLEVEL_NOTICE); } else if (minutes == 60) { - Log_Println((char *)FPSTR(modificatorSleepTimer60), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepTimer60, LOGLEVEL_NOTICE); } else { - Log_Println((char *)FPSTR(modificatorSleepTimer120), LOGLEVEL_NOTICE); + Log_Println(modificatorSleepTimer120, LOGLEVEL_NOTICE); } } - #ifdef MQTT_ENABLE - publishMqtt((char *) FPSTR(topicSleepTimerState), System_GetSleepTimer(), false); - publishMqtt((char *) FPSTR(topicLedBrightnessState), Led_GetBrightness(), false); - #endif +#ifdef MQTT_ENABLE + publishMqtt(topicSleepTimerState, System_GetSleepTimer(), false); + publishMqtt(topicLedBrightnessState, Led_GetBrightness(), false); +#endif return sleepTimerEnabled; } @@ -168,16 +165,49 @@ uint8_t System_GetOperationModeFromNvs(void) { void System_SleepHandler(void) { unsigned long m = millis(); if (m >= System_LastTimeActiveTimestamp && (m - System_LastTimeActiveTimestamp >= (System_MaxInactivityTime * 1000u * 60u))) { - Log_Println((char *) FPSTR(goToSleepDueToIdle), LOGLEVEL_INFO); + Log_Println(goToSleepDueToIdle, LOGLEVEL_INFO); System_RequestSleep(); } else if (System_SleepTimerStartTimestamp > 00) { if (m - System_SleepTimerStartTimestamp >= (System_SleepTimer * 1000u * 60u)) { - Log_Println((char *) FPSTR(goToSleepDueToTimer), LOGLEVEL_INFO); + Log_Println(goToSleepDueToTimer, LOGLEVEL_INFO); System_RequestSleep(); } } } +// prepare power down +void System_PreparePowerDown(void) { + + AudioPlayer_Exit(); +// Disable amps in order to avoid ugly noises when powering off +#ifdef GPIO_PA_EN + Log_Println("shutdown amplifier..", LOGLEVEL_NOTICE); + Port_Write(GPIO_PA_EN, false, false); +#endif +#ifdef GPIO_HP_EN + Log_Println("shutdown headphone..", LOGLEVEL_NOTICE); + Port_Write(GPIO_HP_EN, false, false); +#endif + + Mqtt_Exit(); + Led_Exit(); + +#ifdef USE_LAST_VOLUME_AFTER_REBOOT + gPrefsSettings.putUInt("previousVolume", AudioPlayer_GetCurrentVolume()); +#endif + SdCard_Exit(); + + Serial.flush(); +} + +void System_Restart(void) { + // prepare power down (shutdown common modules) + System_PreparePowerDown(); + // restart the ESP-32 + Log_Println("restarting..", LOGLEVEL_NOTICE); + ESP.restart(); +} + // Puts uC to deep-sleep if flag is set void System_DeepSleepManager(void) { if (System_GoToSleep) { @@ -186,61 +216,26 @@ void System_DeepSleepManager(void) { } System_Sleeping = true; - Log_Println((char *) FPSTR(goToSleepNow), LOGLEVEL_NOTICE); - - // Make sure last playposition for audiobook is saved when playback is active while shutdown was initiated - #ifdef SAVE_PLAYPOS_BEFORE_SHUTDOWN - if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS - vTaskDelay(portTICK_RATE_MS * 100u); - } - } - #endif - - // Disable amps in order to avoid ugly noises when powering off - #ifdef GPIO_PA_EN - Port_Write(GPIO_PA_EN, false, false); - #endif - #ifdef GPIO_HP_EN - Port_Write(GPIO_HP_EN, false, false); - #endif - - Mqtt_Exit(); - Led_Exit(); - - #ifdef USE_LAST_VOLUME_AFTER_REBOOT - gPrefsSettings.putUInt("previousVolume", AudioPlayer_GetCurrentVolume()); - #endif - SdCard_Exit(); - - Serial.flush(); + Log_Println(goToSleepNow, LOGLEVEL_NOTICE); + // prepare power down (shutdown common modules) + System_PreparePowerDown(); // switch off power Power_PeripheralOff(); + // time to settle down.. delay(200); - #if defined (RFID_READER_TYPE_MFRC522_SPI) || defined (RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_PN5180) - Rfid_Exit(); - #endif - #ifdef PORT_EXPANDER_ENABLE - Port_Exit(); - #endif - Log_Println((char *) F("deep-sleep, good night......."), LOGLEVEL_NOTICE); +// .. for LPCD +#if defined(RFID_READER_TYPE_MFRC522_SPI) || defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_PN5180) + Rfid_Exit(); +#endif +#ifdef PORT_EXPANDER_ENABLE + Port_Exit(); +#endif + // goto sleep now + Log_Println("deep-sleep, good night.......", LOGLEVEL_NOTICE); esp_deep_sleep_start(); } } -// Shows warning after upgrade -void System_ShowUpgradeWarning(void) { - uint32_t nvsShowUpgradeWarningCount = gPrefsSettings.getUInt("wcountrefact", 0); - if (!nvsShowUpgradeWarningCount) { - gPrefsSettings.putUInt("wcountrefact", 1); - Log_Println((char *) FPSTR(warningRefactoring), LOGLEVEL_ERROR); - } else if (nvsShowUpgradeWarningCount < 5) { - gPrefsSettings.putUInt("wcountrefact", ++nvsShowUpgradeWarningCount); - Log_Println((char *) FPSTR(warningRefactoring), LOGLEVEL_ERROR); - } -} - // Print the wake-up reason why ESP32 is awake now void System_ShowWakeUpReason() { esp_sleep_wakeup_cause_t wakeup_reason; @@ -248,32 +243,31 @@ void System_ShowWakeUpReason() { switch (wakeup_reason) { case ESP_SLEEP_WAKEUP_EXT0: - Log_Println((char *) F("Wakeup caused by push-button"), LOGLEVEL_NOTICE); + Log_Println("Wakeup caused by push-button", LOGLEVEL_NOTICE); break; case ESP_SLEEP_WAKEUP_EXT1: - Log_Println((char *) F("Wakeup caused by low power card-detection"), LOGLEVEL_NOTICE); + Log_Println("Wakeup caused by low power card-detection", LOGLEVEL_NOTICE); break; case ESP_SLEEP_WAKEUP_TIMER: - Log_Println((char *) F("Wakeup caused by timer"), LOGLEVEL_NOTICE); + Log_Println("Wakeup caused by timer", LOGLEVEL_NOTICE); break; case ESP_SLEEP_WAKEUP_TOUCHPAD: - Log_Println((char *) F("Wakeup caused by touchpad"), LOGLEVEL_NOTICE); + Log_Println("Wakeup caused by touchpad", LOGLEVEL_NOTICE); break; case ESP_SLEEP_WAKEUP_ULP: - Log_Println((char *) F("Wakeup caused by ULP-program"), LOGLEVEL_NOTICE); + Log_Println("Wakeup caused by ULP-program", LOGLEVEL_NOTICE); break; default: - snprintf(Log_Buffer, Log_BufferLength, "Wakeup was not caused by deepsleep: %d", wakeup_reason); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, "Wakeup was not caused by deepsleep: %d", wakeup_reason); break; } } #ifdef ENABLE_ESPUINO_DEBUG - void System_esp_print_tasks(void) { - char *pbuffer = (char *)calloc(2048, 1); - vTaskGetRunTimeStats(pbuffer); - Serial.printf("=====\n%s\n=====", pbuffer); - free(pbuffer); - } +void System_esp_print_tasks(void) { + char *pbuffer = (char *) calloc(2048, 1); + vTaskGetRunTimeStats(pbuffer); + Serial.printf("=====\n%s\n=====", pbuffer); + free(pbuffer); +} #endif diff --git a/src/System.h b/src/System.h index cd79e710..25acb102 100644 --- a/src/System.h +++ b/src/System.h @@ -3,12 +3,13 @@ extern Preferences gPrefsRfid; extern Preferences gPrefsSettings; +extern TaskHandle_t AudioTaskHandle; void System_Init(void); void System_Cyclic(void); void System_UpdateActivityTimer(void); void System_RequestSleep(void); -bool System_IsSleepRequested(void); +void System_Restart(void); bool System_SetSleepTimer(uint8_t minutes); void System_DisableSleepTimer(); bool System_IsSleepTimerEnabled(void); @@ -23,6 +24,5 @@ void System_IndicateOk(void); void System_SetOperationMode(uint8_t opMode); uint8_t System_GetOperationMode(void); uint8_t System_GetOperationModeFromNvs(void); -void System_ShowUpgradeWarning(void); void System_esp_print_tasks(void); void System_ShowWakeUpReason(); diff --git a/src/Web.cpp b/src/Web.cpp index 8a36a533..76b3dc3b 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -1,19 +1,18 @@ #include -#include -#include -#include -#include -#include "soc/timer_group_struct.h" -#include "soc/timer_group_reg.h" -#include "freertos/ringbuf.h" -#include "ESPAsyncWebServer.h" -#include "ArduinoJson.h" #include "settings.h" + +#include "Web.h" + +#include "ArduinoJson.h" +#include "AsyncJson.h" #include "AudioPlayer.h" #include "Battery.h" #include "Cmd.h" #include "Common.h" +#include "ESPAsyncWebServer.h" #include "Ftp.h" +#include "HTMLbinary.h" +#include "HallEffectSensor.h" #include "Led.h" #include "Log.h" #include "MemX.h" @@ -21,38 +20,36 @@ #include "Rfid.h" #include "SdCard.h" #include "System.h" -#include "Web.h" #include "Wlan.h" +#include "freertos/ringbuf.h" #include "revision.h" -#include "Rfid.h" -#include "HallEffectSensor.h" - -#if (LANGUAGE == DE) - #include "HTMLaccesspoint_DE.h" - #include "HTMLmanagement_DE.h" -#endif - -#if (LANGUAGE == EN) - #include "HTMLaccesspoint_EN.h" - #include "HTMLmanagement_EN.h" -#endif +#include "soc/timer_group_reg.h" +#include "soc/timer_group_struct.h" +#include +#include +#include +#include typedef struct { char nvsKey[13]; char nvsEntry[275]; } nvs_t; -const char mqttTab[] PROGMEM = " MQTT"; -const char ftpTab[] PROGMEM = " FTP"; - AsyncWebServer wServer(80); AsyncWebSocket ws("/ws"); AsyncEventSource events("/events"); static bool webserverStarted = false; +static const uint32_t chunk_size = 16384; // bigger chunks increase write-performance to SD-Card +static const uint32_t nr_of_buffers = 2; // at least two buffers. No speed improvement yet with more than two. + +uint8_t buffer[nr_of_buffers][chunk_size]; +volatile uint32_t size_in_buffer[nr_of_buffers]; +volatile bool buffer_full[nr_of_buffers]; +uint32_t index_buffer_write = 0; +uint32_t index_buffer_read = 0; -static RingbufHandle_t explorerFileUploadRingBuffer; static QueueHandle_t explorerFileUploadStatusQueue; static TaskHandle_t fileStorageTaskHandle; @@ -66,66 +63,223 @@ static void explorerHandleDeleteRequest(AsyncWebServerRequest *request); static void explorerHandleCreateRequest(AsyncWebServerRequest *request); static void explorerHandleRenameRequest(AsyncWebServerRequest *request); static void explorerHandleAudioRequest(AsyncWebServerRequest *request); +static void handleGetSavedSSIDs(AsyncWebServerRequest *request); +static void handlePostSavedSSIDs(AsyncWebServerRequest *request, JsonVariant &json); +static void handleDeleteSavedSSIDs(AsyncWebServerRequest *request); +static void handleGetActiveSSID(AsyncWebServerRequest *request); +static void handleGetWiFiConfig(AsyncWebServerRequest *request); +static void handlePostWiFiConfig(AsyncWebServerRequest *request, JsonVariant &json); static void handleCoverImageRequest(AsyncWebServerRequest *request); - -static bool Web_DumpNvsToSd(const char *_namespace, const char *_destFile); +static void handleWiFiScanRequest(AsyncWebServerRequest *request); +static void handleGetRFIDRequest(AsyncWebServerRequest *request); +static void handlePostRFIDRequest(AsyncWebServerRequest *request, JsonVariant &json); +static void handleDeleteRFIDRequest(AsyncWebServerRequest *request); +static void handleGetInfo(AsyncWebServerRequest *request); +static void handleGetSettings(AsyncWebServerRequest *request); +static void handlePostSettings(AsyncWebServerRequest *request, JsonVariant &json); static void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); -static String templateProcessor(const String &templ); +static void settingsToJSON(JsonObject obj, const String section); +static bool JSONToSettings(JsonObject obj); static void webserverStart(void); -void Web_DeleteCachefile(const char *fileOrDirectory); // If PSRAM is available use it allocate memory for JSON-objects struct SpiRamAllocator { - void* allocate(size_t size) { + void *allocate(size_t size) { return ps_malloc(size); - } - void deallocate(void* pointer) { + void deallocate(void *pointer) { free(pointer); } }; using SpiRamJsonDocument = BasicJsonDocument; -void Web_Init(void) { - wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send_P(200, "text/html", accesspoint_HTML); - }); +static void serveProgmemFiles(const String &uri, const String &contentType, const uint8_t *content, size_t len) { + wServer.on(uri.c_str(), HTTP_GET, [contentType, content, len](AsyncWebServerRequest *request) { + AsyncWebServerResponse *response; - wServer.on("/init", HTTP_POST, [](AsyncWebServerRequest *request) { - if (request->hasParam("ssid", true) && request->hasParam("pwd", true) && request->hasParam("hostname", true)) { - gPrefsSettings.putString("SSID", request->getParam("ssid", true)->value()); - gPrefsSettings.putString("Password", request->getParam("pwd", true)->value()); - gPrefsSettings.putString("Hostname", request->getParam("hostname", true)->value()); + // const bool etag = request->hasHeader("if-None-Match") && request->getHeader("if-None-Match")->value().equals(gitRevShort); + const bool etag = false; + if (etag) { + response = request->beginResponse(304); + } else { + response = request->beginResponse_P(200, contentType, content, len); + response->addHeader("Content-Encoding", "gzip"); } - request->send_P(200, "text/html", accesspoint_HTML); + // response->addHeader("Cache-Control", "public, max-age=31536000, immutable"); + // response->addHeader("ETag", gitRevShort); // use git revision as digest + request->send(response); }); +} - wServer.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { - #if (LANGUAGE == DE) - request->send(200, "text/html", (char *) F("ESPuino wird neu gestartet...")); - #else - request->send(200, "text/html", (char *) F("ESPuino is being restarted...")); - #endif - Serial.flush(); - ESP.restart(); - }); +class OneParamRewrite : public AsyncWebRewrite { +protected: + String _urlPrefix; + int _paramIndex; + String _paramsBackup; - wServer.on("/shutdown", HTTP_GET, [](AsyncWebServerRequest *request) { - #if (LANGUAGE == DE) - request->send(200, "text/html", (char *) F("ESPuino wird ausgeschaltet...")); - #else - request->send(200, "text/html", (char *) F("ESPuino is being shutdown...")); - #endif - System_RequestSleep(); - }); +public: + OneParamRewrite(const char *from, const char *to) + : AsyncWebRewrite(from, to) { + + _paramIndex = _from.indexOf('{'); + + if (_paramIndex >= 0 && _from.endsWith("}")) { + _urlPrefix = _from.substring(0, _paramIndex); + int index = _params.indexOf('{'); + if (index >= 0) { + _params = _params.substring(0, index); + } + } else { + _urlPrefix = _from; + } + _paramsBackup = _params; + } + + bool match(AsyncWebServerRequest *request) override { + if (request->url().startsWith(_urlPrefix)) { + if (_paramIndex >= 0) { + _params = _paramsBackup + request->url().substring(_paramIndex); + } else { + _params = _paramsBackup; + } + return true; + + } else { + return false; + } + } +}; + +// List all key in NVS for a given namespace +// callback function is called for every key with userdefined data object +bool listNVSKeys(const char *_namespace, void *data, bool (*callback)(const char *key, void *data)) { + Led_SetPause(true); // Workaround to prevent exceptions due to Neopixel-signalisation while NVS-write + esp_partition_iterator_t pi; // Iterator for find + const esp_partition_t *nvs; // Pointer to partition struct + esp_err_t result = ESP_OK; + const char *partname = "nvs"; + uint8_t pagenr = 0; // Page number in NVS + uint8_t i; // Index in Entry 0..125 + uint8_t bm; // Bitmap for an entry + uint32_t offset = 0; // Offset in nvs partition + uint8_t namespace_ID; // Namespace ID found + + pi = esp_partition_find(ESP_PARTITION_TYPE_DATA, // Get partition iterator for + ESP_PARTITION_SUBTYPE_ANY, // this partition + partname); + if (pi) { + nvs = esp_partition_get(pi); // Get partition struct + esp_partition_iterator_release(pi); // Release the iterator + Log_Printf(LOGLEVEL_DEBUG, "Partition %s found, %d bytes", partname, nvs->size); + } else { + Log_Printf(LOGLEVEL_ERROR, "Partition %s not found!", partname); + return false; + } + namespace_ID = FindNsID(nvs, _namespace); // Find ID of our namespace in NVS + while (offset < nvs->size) { + result = esp_partition_read(nvs, offset, // Read 1 page in nvs partition + &buf, + sizeof(nvs_page)); + if (result != ESP_OK) { + Log_Println("Error reading NVS!", LOGLEVEL_ERROR); + return false; + } + + i = 0; + + while (i < 126) { + bm = (buf.Bitmap[i / 4] >> ((i % 4) * 2)) & 0x03; // Get bitmap for this entry + if (bm == 2) { + if ((namespace_ID == 0xFF) || // Show all if ID = 0xFF + (buf.Entry[i].Ns == namespace_ID)) { // otherwise just my namespace + if (isNumber(buf.Entry[i].Key)) { + if (!callback(buf.Entry[i].Key, data)) { + return false; + } + } + } + i += buf.Entry[i].Span; // Next entry + } else { + i++; + } + } + offset += sizeof(nvs_page); // Prepare to read next page in nvs + pagenr++; + } + Led_SetPause(false); - // allow cors for local debug (https://github.com/me-no-dev/ESPAsyncWebServer/issues/1080) - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization"); - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true"); - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); - wServer.begin(); - Log_Println((char *) FPSTR(httpReady), LOGLEVEL_NOTICE); + return true; +} + +// callback for writing a NVS entry to file +bool DumpNvsToSdCallback(const char *key, void *data) { + String s = gPrefsRfid.getString(key); + File *file = (File *) data; + file->printf("%s%s%s%s\n", stringOuterDelimiter, key, stringOuterDelimiter, s.c_str()); + return true; +} + +// Dumps all RFID-entries from NVS into a file on SD-card +bool Web_DumpNvsToSd(const char *_namespace, const char *_destFile) { + File file = gFSystem.open(_destFile, FILE_WRITE); + if (!file) { + return false; + } + // write UTF-8 BOM + file.write(0xEF); + file.write(0xBB); + file.write(0xBF); + // list all NVS keys + bool success = listNVSKeys(_namespace, &file, DumpNvsToSdCallback); + file.close(); + return success; +} + +// First request will return 0 results unless you start scan from somewhere else (loop/setup) +// Do not request more often than 3-5 seconds +static void handleWiFiScanRequest(AsyncWebServerRequest *request) { + String json = "["; + int n = WiFi.scanComplete(); + if (n == -2) { + // -2 if scan not triggered + WiFi.scanNetworks(true, false, true, 120); + } else if (n) { + for (int i = 0; i < n; ++i) { + if (i > 9) { + break; + } + // calculate RSSI as quality in percent + int quality; + if (WiFi.RSSI(i) <= -100) { + quality = 0; + } else if (WiFi.RSSI(i) >= -50) { + quality = 100; + } else { + quality = 2 * (WiFi.RSSI(i) + 100); + } + if (i) { + json += ","; + } + json += "{"; + json += "\"ssid\":\"" + WiFi.SSID(i) + "\""; + json += ",\"bssid\":\"" + WiFi.BSSIDstr(i) + "\""; + json += ",\"rssi\":" + String(WiFi.RSSI(i)); + json += ",\"channel\":" + String(WiFi.channel(i)); + json += ",\"secure\":" + String(WiFi.encryptionType(i)); + json += ",\"quality\":" + String(quality); // WiFi strength in percent + json += ",\"wico\":\"w" + String(int(round(map(quality, 0, 100, 1, 4)))) + "\""; // WiFi strength icon ("w1"-"w4") + json += ",\"pico\":\"" + String((WIFI_AUTH_OPEN == WiFi.encryptionType(i)) ? "" : "pw") + "\""; // auth icon ("p1" for secured) + json += "}"; + } + WiFi.scanDelete(); + if (WiFi.scanComplete() == -2) { + WiFi.scanNetworks(true, false, true, 120); + } + } + json += "]"; + request->send(200, "application/json", json); + json = String(); } void Web_Cyclic(void) { @@ -133,12 +287,17 @@ void Web_Cyclic(void) { ws.cleanupClients(); } +// handle not found void notFound(AsyncWebServerRequest *request) { - request->send(404, "text/plain", "Not found"); + Log_Printf(LOGLEVEL_ERROR, "%s not found, redirect to startpage", request->url().c_str()); + String html = "Ooups - page \"" + request->url() + "\" not found (404)"; + html += ""; + // for captive portal, send statuscode 200 & auto redirect to startpage + request->send(200, "text/html", html); } void webserverStart(void) { - if (Wlan_IsConnected() && !webserverStarted) { + if (!webserverStarted && (Wlan_IsConnected() || (WiFi.getMode() == WIFI_AP))) { // attach AsyncWebSocket for Mgmt-Interface ws.onEvent(onWebsocketEvent); wServer.addHandler(&ws); @@ -148,89 +307,81 @@ void webserverStart(void) { // Default wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - if (gFSystem.exists("/.html/index.htm")) { - // serve webpage from SD card - request->send(gFSystem, "/.html/index.htm", String(), false, templateProcessor); + AsyncWebServerResponse *response; + + // const bool etag = request->hasHeader("if-None-Match") && request->getHeader("if-None-Match")->value().equals(gitRevShort); + const bool etag = false; + if (etag) { + response = request->beginResponse(304); } else { - // serve webpage from PROGMEM - request->send_P(200, "text/html", management_HTML, templateProcessor); + if (WiFi.getMode() == WIFI_STA) { + // serve management.html in station-mode +#ifdef NO_SDCARD + response = request->beginResponse_P(200, "text/html", (const uint8_t *) management_BIN, sizeof(management_BIN)); + response->addHeader("Content-Encoding", "gzip"); +#else + if (gFSystem.exists("/.html/index.htm")) { + response = request->beginResponse(gFSystem, "/.html/index.htm", "text/html", false); + } else { + response = request->beginResponse_P(200, "text/html", (const uint8_t *) management_BIN, sizeof(management_BIN)); + response->addHeader("Content-Encoding", "gzip"); + } +#endif + } else { + // serve accesspoint.html in AP-mode + response = request->beginResponse_P(200, "text/html", (const uint8_t *) accesspoint_BIN, sizeof(accesspoint_BIN)); + response->addHeader("Content-Encoding", "gzip"); + } } + // response->addHeader("Cache-Control", "public, max-age=31536000, immutable"); + // response->addHeader("ETag", gitRevShort); // use git revision as digest + request->send(response); }); + + WWWData::registerRoutes(serveProgmemFiles); + // Log wServer.on("/log", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain; charset=utf-8", Log_GetRingBuffer()); + System_UpdateActivityTimer(); }); - // software/wifi/heap/psram-info - wServer.on( - "/info", HTTP_GET, [](AsyncWebServerRequest *request) { - String info = "ESPuino " + (String) softwareRevision; - info += "\nESPuino " + (String) gitRevision; - info += "\nESP-IDF version: " + String(ESP.getSdkVersion()); - #if (LANGUAGE == DE) - info += "\nFreier Heap: " + String(ESP.getFreeHeap()) + " Bytes"; - info += "\nGroesster freier Heap-Block: " + String((uint32_t)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)) + " Bytes"; - info += "\nFreier PSRAM: "; - info += (!psramInit()) ? "nicht verfuegbar" : String(ESP.getFreePsram()); - if (Wlan_IsConnected()) { - IPAddress myIP = WiFi.localIP(); - info += "\nAktuelle IP: " + String(myIP[0]) + '.' + String(myIP[1]) + '.' + String(myIP[2]) + '.' + String(myIP[3]); - info += "\nWLAN-Signalstaerke: " + String((int8_t)Wlan_GetRssi()) + " dBm"; - } - #else - info += "\nFree heap: " + String(ESP.getFreeHeap()) + " bytes"; - info += "\nLargest free heap-block: " + String((uint32_t)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)) + " bytes"; - info += "\nFree PSRAM: "; - info += (!psramInit()) ? "not available" : String(ESP.getFreePsram()); - if (Wlan_IsConnected()) { - IPAddress myIP = WiFi.localIP(); - info += "\nCurrent IP: " + String(myIP[0]) + '.' + String(myIP[1]) + '.' + String(myIP[2]) + '.' + String(myIP[3]); - info += "\nWiFi signal-strength: " + String((int8_t)Wlan_GetRssi()) + " dBm"; - } - #endif - #ifdef BATTERY_MEASURE_ENABLE - snprintf(Log_Buffer, Log_BufferLength, "\n%s: %.2f V", (char *) FPSTR(currentVoltageMsg), Battery_GetVoltage()); - info += (String) Log_Buffer; - snprintf(Log_Buffer, Log_BufferLength, "\n%s: %.2f %%", (char *)FPSTR(currentChargeMsg), Battery_EstimateLevel() * 100); - info += (String) Log_Buffer; - #endif - #ifdef HALLEFFECT_SENSOR_ENABLE - uint16_t sva = gHallEffectSensor.readSensorValueAverage(true); - int diff = sva-gHallEffectSensor.NullFieldValue(); - snprintf(Log_Buffer, Log_BufferLength, (char *) FPSTR(F("\nHallEffectSensor NullFieldValue:%d, actual:%d, diff:%d, LastWaitFor_State:%d (waited:%d ms)")), gHallEffectSensor.NullFieldValue(), sva, diff, gHallEffectSensor.LastWaitForState(), gHallEffectSensor.LastWaitForStateMS()); - info += (String) Log_Buffer; - #endif - request->send_P(200, "text/plain", info.c_str()); - }); + // info + wServer.on("/info", HTTP_GET, handleGetInfo); // NVS-backup-upload wServer.on( "/upload", HTTP_POST, [](AsyncWebServerRequest *request) { - request->send_P(200, "text/html", backupRecoveryWebsite); + request->send(200); }, handleUpload); // OTA-upload wServer.on( "/update", HTTP_POST, [](AsyncWebServerRequest *request) { - #ifdef BOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT - request->send(200, "text/html", restartWebsite); }, - #else - request->send(200, "text/html", otaNotSupportedWebsite); }, - #endif +#ifdef BOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT + if (Update.hasError()) { + request->send(500, "text/plain", Update.errorString()); + } else { + request->send(200, "text/html", restartWebsite); + } +#else + request->send(500, "text/html", otaNotSupportedWebsite); +#endif + }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - #ifndef BOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT - Log_Println((char *) FPSTR(otaNotSupported), LOGLEVEL_NOTICE); - return; - #endif +#ifndef BOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT + Log_Println(otaNotSupported, LOGLEVEL_NOTICE); + return; +#endif if (!index) { - if (!gPlayProperties.pausePlay) { // Pause playback as it sounds ugly when OTA starts - Cmd_Action(CMD_PLAYPAUSE); // Pause first to possibly to save last playposition (if necessary) - Cmd_Action(CMD_STOP); - } + // pause some tasks to get more free CPU time for the upload + vTaskSuspend(AudioTaskHandle); + Led_TaskPause(); + Rfid_TaskPause(); Update.begin(); - Log_Println((char *) FPSTR(fwStart), LOGLEVEL_NOTICE); + Log_Println(fwStart, LOGLEVEL_NOTICE); } Update.write(data, len); @@ -238,33 +389,74 @@ void webserverStart(void) { if (final) { Update.end(true); - Log_Println((char *) FPSTR(fwEnd), LOGLEVEL_NOTICE); + // resume the paused tasks + Led_TaskResume(); + vTaskResume(AudioTaskHandle); + Rfid_TaskResume(); + Log_Println(fwEnd, LOGLEVEL_NOTICE); + if (Update.hasError()) { + Log_Println(Update.errorString(), LOGLEVEL_ERROR); + } Serial.flush(); - ESP.restart(); + // ESP.restart(); // restart is done via webpage javascript } - }); + }); // ESP-restart - wServer.on("/restart", HTTP_GET, [](AsyncWebServerRequest *request) { + wServer.on("/restart", HTTP_POST, [](AsyncWebServerRequest *request) { request->send_P(200, "text/html", restartWebsite); - Serial.flush(); - ESP.restart(); + System_Restart(); }); // ESP-shutdown - wServer.on("/shutdown", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send_P(200, "text/html", shutdownWebsite); + wServer.on("/shutdown", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", shutdownWebsite); System_RequestSleep(); }); - // ESP-shutdown - wServer.on("/rfidnvserase", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send_P(200, "text/html", eraseRfidNvsWeb); - Log_Println((char *) FPSTR(eraseRfidNvs), LOGLEVEL_NOTICE); - gPrefsRfid.clear(); - Web_DumpNvsToSd("rfidTags", (const char*) FPSTR(backupFile)); +#ifdef CONFIG_FREERTOS_USE_TRACE_FACILITY + // runtime task statistics + wServer.on("/stats", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncResponseStream *response = request->beginResponseStream("text/html"); + response->println(" ESPuino runtime stats"); + response->println(""); // refresh page every 2 seconds + response->println("Tasklist:
");
+			// show tasklist
+			char *pbuffer = (char *) calloc(2048, 1);
+			vTaskList(pbuffer);
+			response->println(pbuffer);
+			response->println("


Runtime statistics:
");
+			// show vTaskGetRunTimeStats()
+			vTaskGetRunTimeStats(pbuffer);
+			response->println(pbuffer);
+			response->println("
"); + free(pbuffer); + // send the response last + request->send(response); + }); +#endif + + // erase all RFID-assignments from NVS + wServer.on("/rfidnvserase", HTTP_POST, [](AsyncWebServerRequest *request) { + Log_Println(eraseRfidNvs, LOGLEVEL_NOTICE); + // make a backup first + Web_DumpNvsToSd("rfidTags", backupFile); + if (gPrefsRfid.clear()) { + request->send(200); + } else { + request->send(500); + } + System_UpdateActivityTimer(); }); + // RFID + wServer.on("/rfid", HTTP_GET, handleGetRFIDRequest); + wServer.addHandler(new AsyncCallbackJsonWebHandler("/rfid", handlePostRFIDRequest)); + wServer.addRewrite(new OneParamRewrite("/rfid/{id}", "/rfid?id={id}")); + wServer.on("/rfid", HTTP_DELETE, handleDeleteRFIDRequest); + + // WiFi scan + wServer.on("/wifiscan", HTTP_GET, handleWiFiScanRequest); // Fileexplorer (realtime) wServer.on("/explorer", HTTP_GET, explorerHandleListRequest); @@ -285,12 +477,23 @@ void webserverStart(void) { wServer.on("/exploreraudio", HTTP_POST, explorerHandleAudioRequest); + wServer.on("/savedSSIDs", HTTP_GET, handleGetSavedSSIDs); + wServer.addHandler(new AsyncCallbackJsonWebHandler("/savedSSIDs", handlePostSavedSSIDs)); + + wServer.addRewrite(new OneParamRewrite("/savedSSIDs/{ssid}", "/savedSSIDs?ssid={ssid}")); + wServer.on("/savedSSIDs", HTTP_DELETE, handleDeleteSavedSSIDs); + wServer.on("/activeSSID", HTTP_GET, handleGetActiveSSID); + + wServer.on("/wificonfig", HTTP_GET, handleGetWiFiConfig); + wServer.addHandler(new AsyncCallbackJsonWebHandler("/wificonfig", handlePostWiFiConfig)); + // current cover image wServer.on("/cover", HTTP_GET, handleCoverImageRequest); // ESPuino logo wServer.on("/logo", HTTP_GET, [](AsyncWebServerRequest *request) { - Log_Println((char *) F("logo request"), LOGLEVEL_DEBUG); +#ifndef NO_SDCARD + Log_Println("logo request", LOGLEVEL_DEBUG); if (gFSystem.exists("/.html/logo.png")) { request->send(gFSystem, "/.html/logo.png", "image/png"); return; @@ -299,205 +502,98 @@ void webserverStart(void) { request->send(gFSystem, "/.html/logo.svg", "image/svg+xml"); return; }; - request->redirect("https://www.espuino.de/espuino/Espuino32.png"); +#endif + request->redirect("https://www.espuino.de/Espuino.webp"); }); // ESPuino favicon - wServer.on("/favicon", HTTP_GET, [](AsyncWebServerRequest *request) { - Log_Println((char *) F("favicon request"), LOGLEVEL_DEBUG); + wServer.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request) { +#ifndef NO_SDCARD if (gFSystem.exists("/.html/favicon.ico")) { request->send(gFSystem, "/.html/favicon.png", "image/x-icon"); return; }; +#endif request->redirect("https://espuino.de/espuino/favicon.ico"); }); - // Init HallEffectSensor Value - #ifdef HALLEFFECT_SENSOR_ENABLE - wServer.on("/inithalleffectsensor", HTTP_GET, [](AsyncWebServerRequest *request) { - bool bres = gHallEffectSensor.saveActualFieldValue2NVS(); - snprintf(Log_Buffer, Log_BufferLength,(char *) FPSTR(F("WebRequest>HallEffectSensor FieldValue: %d => NVS, Status: %s")), gHallEffectSensor.NullFieldValue(), bres ? "OK" : "ERROR"); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - request->send(200, "text/html", Log_Buffer); - }); - #endif + // ESPuino settings + wServer.on("/settings", HTTP_GET, handleGetSettings); + wServer.addHandler(new AsyncCallbackJsonWebHandler("/settings", handlePostSettings)); + // Init HallEffectSensor Value +#ifdef HALLEFFECT_SENSOR_ENABLE + wServer.on("/inithalleffectsensor", HTTP_GET, [](AsyncWebServerRequest *request) { + bool bres = gHallEffectSensor.saveActualFieldValue2NVS(); + char buffer[128]; + snprintf(buffer, sizeof(buffer), "WebRequest>HallEffectSensor FieldValue: %d => NVS, Status: %s", gHallEffectSensor.NullFieldValue(), bres ? "OK" : "ERROR"); + Log_Println(buffer, LOGLEVEL_INFO); + request->send(200, "text/html", buffer); + }); +#endif wServer.onNotFound(notFound); - // allow cors for local debug + // allow cors for local debug (https://github.com/me-no-dev/ESPAsyncWebServer/issues/1080) + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); wServer.begin(); webserverStarted = true; + Log_Println(httpReady, LOGLEVEL_NOTICE); + // start a first WiFi scan (to get a WiFi list more quickly in webview) + WiFi.scanNetworks(true, false, true, 120); } } -// Used for substitution of some variables/templates of html-files. Is called by webserver's template-engine -String templateProcessor(const String &templ) { - if (templ == "FTP_USER") { - return gPrefsSettings.getString("ftpuser", "-1"); - } else if (templ == "FTP_PWD") { - return gPrefsSettings.getString("ftppassword", "-1"); - } else if (templ == "FTP_USER_LENGTH") { - return String(ftpUserLength - 1); - } else if (templ == "FTP_PWD_LENGTH") { - return String(ftpPasswordLength - 1); - } else if (templ == "SHOW_FTP_TAB") { // Only show FTP-tab if FTP-support was compiled - #ifdef FTP_ENABLE - return (String) FPSTR(ftpTab); - #else - return String(); - #endif - } else if (templ == "INIT_LED_BRIGHTNESS") { - return String(gPrefsSettings.getUChar("iLedBrightness", 0)); - } else if (templ == "NIGHT_LED_BRIGHTNESS") { - return String(gPrefsSettings.getUChar("nLedBrightness", 0)); - } else if (templ == "MAX_INACTIVITY") { - return String(gPrefsSettings.getUInt("mInactiviyT", 0)); - } else if (templ == "INIT_VOLUME") { - return String(gPrefsSettings.getUInt("initVolume", 0)); - } else if (templ == "CURRENT_VOLUME") { - return String(AudioPlayer_GetCurrentVolume()); - } else if (templ == "MAX_VOLUME_SPEAKER") { - return String(gPrefsSettings.getUInt("maxVolumeSp", 0)); - } else if (templ == "MAX_VOLUME_HEADPHONE") { - return String(gPrefsSettings.getUInt("maxVolumeHp", 0)); -#ifdef BATTERY_MEASURE_ENABLE - #ifdef MEASURE_BATTERY_VOLTAGE - } else if (templ == "WARNING_LOW_VOLTAGE") { - return String(gPrefsSettings.getFloat("wLowVoltage", warningLowVoltage)); - } else if (templ == "VOLTAGE_INDICATOR_LOW") { - return String(gPrefsSettings.getFloat("vIndicatorLow", voltageIndicatorLow)); - } else if (templ == "VOLTAGE_INDICATOR_HIGH") { - return String(gPrefsSettings.getFloat("vIndicatorHigh", voltageIndicatorHigh)); - #endif - #ifdef MEASURE_BATTERY_OTHER // placeholder - } else if (templ == "todo") { - return "todo"; - #endif - } else if (templ == "BATTERY_CHECK_INTERVAL") { - return String(gPrefsSettings.getUInt("vCheckIntv", batteryCheckInterval)); -#else - // TODO: hide battery config -#endif - } else if (templ == "MQTT_CLIENTID") { - return gPrefsSettings.getString("mqttClientId", "-1"); - } else if (templ == "MQTT_SERVER") { - return gPrefsSettings.getString("mqttServer", "-1"); - } else if (templ == "SHOW_MQTT_TAB") { // Only show MQTT-tab if MQTT-support was compiled - #ifdef MQTT_ENABLE - return (String) FPSTR(mqttTab); - #else - return String(); - #endif - } else if (templ == "MQTT_ENABLE") { - if (Mqtt_IsEnabled()) { - return String("checked=\"checked\""); - } else { - return String(); - } - } else if (templ == "MQTT_USER") { - return gPrefsSettings.getString("mqttUser", "-1"); - } else if (templ == "MQTT_PWD") { - return gPrefsSettings.getString("mqttPassword", "-1"); - } else if (templ == "MQTT_USER_LENGTH") { - return String(mqttUserLength - 1); - } else if (templ == "MQTT_PWD_LENGTH") { - return String(mqttPasswordLength - 1); - } else if (templ == "MQTT_CLIENTID_LENGTH") { - return String(mqttClientIdLength - 1); - } else if (templ == "MQTT_SERVER_LENGTH") { - return String(mqttServerLength - 1); - } else if (templ == "MQTT_PORT") { -#ifdef MQTT_ENABLE - return String(gMqttPort); -#endif - } else if (templ == "BT_SOURCE_NAME") { - return gPrefsSettings.getString("btDeviceName", ""); - } else if (templ == "IPv4") { - IPAddress myIP = WiFi.localIP(); - snprintf(Log_Buffer, Log_BufferLength, "%d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); - return String(Log_Buffer); - } else if (templ == "RFID_TAG_ID") { - return String(gCurrentRfidTagId); - } else if (templ == "HOSTNAME") { - return gPrefsSettings.getString("Hostname", "-1"); - } - - return String(); -} +unsigned long lastPongTimestamp; -// Takes inputs from webgui, parses JSON and saves values in NVS -// If operation was successful (NVS-write is verified) true is returned -bool processJsonRequest(char *_serialJson) { - if (!_serialJson) { +// process JSON to settings +bool JSONToSettings(JsonObject doc) { + if (!doc) { + Log_Println("JSONToSettings: doc unassigned", LOGLEVEL_DEBUG); return false; } - #ifdef BOARD_HAS_PSRAM - SpiRamJsonDocument doc(1000); - #else - StaticJsonDocument<1000> doc; - #endif - - DeserializationError error = deserializeJson(doc, _serialJson); - - if (error) { - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "%s", (char *) F("deserializeJson() fehlgeschlagen: ")); - #else - nprintf(Log_Buffer, Log_BufferLength, "%s", (char *) F("deserializeJson() failed: ")); - #endif - Log_Print(Log_Buffer, LOGLEVEL_ERROR, true); - snprintf(Log_Buffer, Log_BufferLength, "%s\n", error.c_str()); - Log_Print(Log_Buffer, LOGLEVEL_ERROR, true); - return false; + if (doc.containsKey("general")) { + // general settings + if (gPrefsSettings.putUInt("initVolume", doc["general"]["initVolume"].as()) == 0 || gPrefsSettings.putUInt("maxVolumeSp", doc["general"]["maxVolumeSp"].as()) == 0 || gPrefsSettings.putUInt("maxVolumeHp", doc["general"]["maxVolumeHp"].as()) == 0 || gPrefsSettings.putUInt("mInactiviyT", doc["general"]["sleepInactivity"].as()) == 0) { + Log_Println("Failed to save general settings", LOGLEVEL_ERROR); + return false; + } } - - JsonObject object = doc.as(); - - if (doc.containsKey("general")) { - uint8_t iVol = doc["general"]["iVol"].as(); - uint8_t mVolSpeaker = doc["general"]["mVolSpeaker"].as(); - uint8_t mVolHeadphone = doc["general"]["mVolHeadphone"].as(); - uint8_t iBright = doc["general"]["iBright"].as(); - uint8_t nBright = doc["general"]["nBright"].as(); - uint8_t iTime = doc["general"]["iTime"].as(); - float vWarning = doc["general"]["vWarning"].as(); - float vIndLow = doc["general"]["vIndLow"].as(); - float vIndHi = doc["general"]["vIndHi"].as(); - uint8_t vInt = doc["general"]["vInt"].as(); - - gPrefsSettings.putUInt("initVolume", iVol); - gPrefsSettings.putUInt("maxVolumeSp", mVolSpeaker); - gPrefsSettings.putUInt("maxVolumeHp", mVolHeadphone); - gPrefsSettings.putUChar("iLedBrightness", iBright); - gPrefsSettings.putUChar("nLedBrightness", nBright); - gPrefsSettings.putUInt("mInactiviyT", iTime); - gPrefsSettings.putFloat("wLowVoltage", vWarning); - gPrefsSettings.putFloat("vIndicatorLow", vIndLow); - gPrefsSettings.putFloat("vIndicatorHigh", vIndHi); - gPrefsSettings.putUInt("vCheckIntv", vInt); - - // Check if settings were written successfully - if (gPrefsSettings.getUInt("initVolume", 0) != iVol || - gPrefsSettings.getUInt("maxVolumeSp", 0) != mVolSpeaker || - gPrefsSettings.getUInt("maxVolumeHp", 0) != mVolHeadphone || - gPrefsSettings.getUChar("iLedBrightness", 0) != iBright || - gPrefsSettings.getUChar("nLedBrightness", 0) != nBright || - gPrefsSettings.getUInt("mInactiviyT", 0) != iTime || - gPrefsSettings.getFloat("wLowVoltage", 999.99) != vWarning || - gPrefsSettings.getFloat("vIndicatorLow", 999.99) != vIndLow || - gPrefsSettings.getFloat("vIndicatorHigh", 999.99) != vIndHi || - gPrefsSettings.getUInt("vCheckIntv", 17777) != vInt) { - return false; + if (doc.containsKey("wifi")) { + // WiFi settings + String hostName = doc["wifi"]["hostname"]; + if (!Wlan_ValidateHostname(hostName)) { + Log_Println("Invalid hostname", LOGLEVEL_ERROR); + return false; + } + if (((!Wlan_SetHostname(hostName)) || gPrefsSettings.putBool("ScanWiFiOnStart", doc["wifi"]["scanOnStart"].as()) == 0)) { + Log_Println("Failed to save wifi settings", LOGLEVEL_ERROR); + return false; + } + } + if (doc.containsKey("led")) { + // Neopixel settings + if (gPrefsSettings.putUChar("iLedBrightness", doc["led"]["initBrightness"].as()) == 0 || gPrefsSettings.putUChar("nLedBrightness", doc["led"]["nightBrightness"].as()) == 0) { + Log_Println("Failed to save LED settings", LOGLEVEL_ERROR); + return false; + } + } + if (doc.containsKey("battery")) { + // Battery settings + if (gPrefsSettings.putFloat("wLowVoltage", doc["battery"]["warnLowVoltage"].as()) == 0 || gPrefsSettings.putFloat("vIndicatorLow", doc["battery"]["indicatorLow"].as()) == 0 || gPrefsSettings.putFloat("vIndicatorHigh", doc["battery"]["indicatorHi"].as()) == 0 || gPrefsSettings.putUInt("vCheckIntv", doc["battery"]["voltageCheckInterval"].as()) == 0) { + Log_Println("Failed to save battery settings", LOGLEVEL_ERROR); + return false; } Battery_Init(); - } else if (doc.containsKey("ftp")) { - const char *_ftpUser = doc["ftp"]["ftpUser"]; - const char *_ftpPwd = doc["ftp"]["ftpPwd"]; - - gPrefsSettings.putString("ftpuser", (String)_ftpUser); - gPrefsSettings.putString("ftppassword", (String)_ftpPwd); + } + if (doc.containsKey("ftp")) { + const char *_ftpUser = doc["ftp"]["username"]; + const char *_ftpPwd = doc["ftp"]["password"]; - if (!(String(_ftpUser).equals(gPrefsSettings.getString("ftpuser", "-1")) || - String(_ftpPwd).equals(gPrefsSettings.getString("ftppassword", "-1")))) { + gPrefsSettings.putString("ftpuser", (String) _ftpUser); + gPrefsSettings.putString("ftppassword", (String) _ftpPwd); + // Check if settings were written successfully + if (!(String(_ftpUser).equals(gPrefsSettings.getString("ftpuser", "-1")) || String(_ftpPwd).equals(gPrefsSettings.getString("ftppassword", "-1")))) { + Log_Println("Failed to save ftp settings", LOGLEVEL_ERROR); return false; } } else if (doc.containsKey("ftpStatus")) { @@ -505,32 +601,45 @@ bool processJsonRequest(char *_serialJson) { if (_ftpStart == 1) { // ifdef FTP_ENABLE is checked in Ftp_EnableServer() Ftp_EnableServer(); } - } else if (doc.containsKey("mqtt")) { - uint8_t _mqttEnable = doc["mqtt"]["mqttEnable"].as(); - const char *_mqttClientId = object["mqtt"]["mqttClientId"]; - const char *_mqttServer = object["mqtt"]["mqttServer"]; - const char *_mqttUser = doc["mqtt"]["mqttUser"]; - const char *_mqttPwd = doc["mqtt"]["mqttPwd"]; - uint16_t _mqttPort = doc["mqtt"]["mqttPort"].as(); + } + if (doc.containsKey("mqtt")) { + uint8_t _mqttEnable = doc["mqtt"]["enable"].as(); + const char *_mqttClientId = doc["mqtt"]["clientID"]; + const char *_mqttServer = doc["mqtt"]["server"]; + const char *_mqttUser = doc["mqtt"]["username"]; + const char *_mqttPwd = doc["mqtt"]["password"]; + uint16_t _mqttPort = doc["mqtt"]["port"].as(); gPrefsSettings.putUChar("enableMQTT", _mqttEnable); - gPrefsSettings.putString("mqttClientId", (String)_mqttClientId); - gPrefsSettings.putString("mqttServer", (String)_mqttServer); - gPrefsSettings.putString("mqttUser", (String)_mqttUser); - gPrefsSettings.putString("mqttPassword", (String)_mqttPwd); + gPrefsSettings.putString("mqttClientId", (String) _mqttClientId); + gPrefsSettings.putString("mqttServer", (String) _mqttServer); + gPrefsSettings.putString("mqttUser", (String) _mqttUser); + gPrefsSettings.putString("mqttPassword", (String) _mqttPwd); gPrefsSettings.putUInt("mqttPort", _mqttPort); - if ((gPrefsSettings.getUChar("enableMQTT", 99) != _mqttEnable) || - (!String(_mqttServer).equals(gPrefsSettings.getString("mqttServer", "-1")))) { + if ((gPrefsSettings.getUChar("enableMQTT", 99) != _mqttEnable) || (!String(_mqttServer).equals(gPrefsSettings.getString("mqttServer", "-1")))) { + Log_Println("Failed to save mqtt settings", LOGLEVEL_ERROR); + return false; + } + } + if (doc.containsKey("bluetooth")) { + // bluetooth settings + const char *_btDeviceName = doc["bluetooth"]["deviceName"]; + gPrefsSettings.putString("btDeviceName", (String) _btDeviceName); + const char *btPinCode = doc["bluetooth"]["pinCode"]; + gPrefsSettings.putString("btPinCode", (String) btPinCode); + // Check if settings were written successfully + if (gPrefsSettings.getString("btDeviceName", "") != _btDeviceName || gPrefsSettings.getString("btPinCode", "") != btPinCode) { + Log_Println("Failed to save bluetooth settings", LOGLEVEL_ERROR); return false; } } else if (doc.containsKey("rfidMod")) { - const char *_rfidIdModId = object["rfidMod"]["rfidIdMod"]; - uint8_t _modId = object["rfidMod"]["modId"]; - char rfidString[12]; + const char *_rfidIdModId = doc["rfidMod"]["rfidIdMod"]; + uint8_t _modId = doc["rfidMod"]["modId"]; if (_modId <= 0) { gPrefsRfid.remove(_rfidIdModId); } else { + char rfidString[12]; snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s0%s0%s%u%s0", stringDelimiter, stringDelimiter, stringDelimiter, _modId, stringDelimiter); gPrefsRfid.putString(_rfidIdModId, rfidString); @@ -539,49 +648,41 @@ bool processJsonRequest(char *_serialJson) { return false; } } - Web_DumpNvsToSd("rfidTags", (const char*) FPSTR(backupFile)); // Store backup-file every time when a new rfid-tag is programmed + Web_DumpNvsToSd("rfidTags", backupFile); // Store backup-file every time when a new rfid-tag is programmed } else if (doc.containsKey("rfidAssign")) { - const char *_rfidIdAssinId = object["rfidAssign"]["rfidIdMusic"]; + const char *_rfidIdAssinId = doc["rfidAssign"]["rfidIdMusic"]; char _fileOrUrlAscii[MAX_FILEPATH_LENTGH]; - convertUtf8ToAscii(object["rfidAssign"]["fileOrUrl"], _fileOrUrlAscii); - uint8_t _playMode = object["rfidAssign"]["playMode"]; + convertFilenameToAscii(doc["rfidAssign"]["fileOrUrl"], _fileOrUrlAscii); + uint8_t _playMode = doc["rfidAssign"]["playMode"]; + if (_playMode <= 0) { + Log_Println("rfidAssign: Invalid playmode", LOGLEVEL_ERROR); + return false; + } char rfidString[275]; snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s%s%s0%s%u%s0", stringDelimiter, _fileOrUrlAscii, stringDelimiter, stringDelimiter, _playMode, stringDelimiter); gPrefsRfid.putString(_rfidIdAssinId, rfidString); - #ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE - strncpy(gOldRfidTagId, "X", cardIdStringSize-1); // Set old rfid-id to crap in order to allow to re-apply a new assigned rfid-tag exactly once - #endif +#ifdef DONT_ACCEPT_SAME_RFID_TWICE_ENABLE + Rfid_ResetOldRfid(); // Set old rfid-id to crap in order to allow to re-apply a new assigned rfid-tag exactly once +#endif String s = gPrefsRfid.getString(_rfidIdAssinId, "-1"); if (s.compareTo(rfidString)) { return false; } - Web_DumpNvsToSd("rfidTags", (const char*) FPSTR(backupFile)); // Store backup-file every time when a new rfid-tag is programmed - } else if (doc.containsKey("wifiConfig")) { - const char *_ssid = object["wifiConfig"]["ssid"]; - const char *_pwd = object["wifiConfig"]["pwd"]; - const char *_hostname = object["wifiConfig"]["hostname"]; - - gPrefsSettings.putString("SSID", _ssid); - gPrefsSettings.putString("Password", _pwd); - gPrefsSettings.putString("Hostname", (String)_hostname); - - String sSsid = gPrefsSettings.getString("SSID", "-1"); - String sPwd = gPrefsSettings.getString("Password", "-1"); - String sHostname = gPrefsSettings.getString("Hostname", "-1"); - - if (sSsid.compareTo(_ssid) || sPwd.compareTo(_pwd)) { - return false; + Web_DumpNvsToSd("rfidTags", backupFile); // Store backup-file every time when a new rfid-tag is programmed + } else if (doc.containsKey("ping")) { + if ((millis() - lastPongTimestamp) > 1000u) { + // send pong (keep-alive heartbeat), check for excessive calls + lastPongTimestamp = millis(); + Web_SendWebsocketData(0, 20); } - } - else if (doc.containsKey("ping")) { - Web_SendWebsocketData(0, 20); return false; } else if (doc.containsKey("controls")) { - if (object["controls"].containsKey("set_volume")) { + if (doc["controls"].containsKey("set_volume")) { uint8_t new_vol = doc["controls"]["set_volume"].as(); AudioPlayer_VolumeToQueueSender(new_vol, true); - } if (object["controls"].containsKey("action")) { + } + if (doc["controls"].containsKey("action")) { uint8_t cmd = doc["controls"]["action"].as(); Cmd_Action(cmd); } @@ -591,19 +692,282 @@ bool processJsonRequest(char *_serialJson) { Web_SendWebsocketData(0, 40); } else if (doc.containsKey("volume")) { Web_SendWebsocketData(0, 50); + } else if (doc.containsKey("settings")) { + Web_SendWebsocketData(0, 60); + } else if (doc.containsKey("ssids")) { + Web_SendWebsocketData(0, 70); + } else if (doc.containsKey("trackProgress")) { + if (doc["trackProgress"].containsKey("posPercent")) { + gPlayProperties.seekmode = SEEK_POS_PERCENT; + gPlayProperties.currentRelPos = doc["trackProgress"]["posPercent"].as(); + } + Web_SendWebsocketData(0, 80); } return true; } +// process settings to JSON object +static void settingsToJSON(JsonObject obj, const String section) { + if ((section == "") || (section == "current")) { + // current values + JsonObject curObj = obj.createNestedObject("current"); + curObj["volume"].set(AudioPlayer_GetCurrentVolume()); + curObj["rfidTagId"] = String(gCurrentRfidTagId); + } + if ((section == "") || (section == "general")) { + // general settings + JsonObject generalObj = obj.createNestedObject("general"); + generalObj["initVolume"].set(gPrefsSettings.getUInt("initVolume", 0)); + generalObj["maxVolumeSp"].set(gPrefsSettings.getUInt("maxVolumeSp", 0)); + generalObj["maxVolumeHp"].set(gPrefsSettings.getUInt("maxVolumeHp", 0)); + generalObj["sleepInactivity"].set(gPrefsSettings.getUInt("mInactiviyT", 0)); + } + if ((section == "") || (section == "wifi")) { + // WiFi settings + JsonObject wifiObj = obj.createNestedObject("wifi"); + wifiObj["hostname"] = Wlan_GetHostname(); + wifiObj["scanOnStart"].set(gPrefsSettings.getBool("ScanWiFiOnStart", false)); + } + if (section == "ssids") { + // saved SSID's + JsonObject ssidsObj = obj.createNestedObject("ssids"); + JsonArray ssidArr = ssidsObj.createNestedArray("savedSSIDs"); + Wlan_GetSavedNetworks([ssidArr](const WiFiSettings &network) { + ssidArr.add(network.ssid); + }); + + // active SSID + if (Wlan_IsConnected()) { + ssidsObj["active"] = Wlan_GetCurrentSSID(); + } + } +#ifdef NEOPIXEL_ENABLE + if ((section == "") || (section == "led")) { + // LED settings + JsonObject ledObj = obj.createNestedObject("led"); + ledObj["initBrightness"].set(gPrefsSettings.getUChar("iLedBrightness", 0)); + ledObj["nightBrightness"].set(gPrefsSettings.getUChar("nLedBrightness", 0)); + } +#endif +#ifdef MEASURE_BATTERY_VOLTAGE + if ((section == "") || (section == "battery")) { + // battery settings + JsonObject batteryObj = obj.createNestedObject("battery"); + batteryObj["warnLowVoltage"].set(gPrefsSettings.getFloat("wLowVoltage", s_warningLowVoltage)); + batteryObj["indicatorLow"].set(gPrefsSettings.getFloat("vIndicatorLow", s_voltageIndicatorLow)); + batteryObj["indicatorHi"].set(gPrefsSettings.getFloat("vIndicatorHigh", s_voltageIndicatorHigh)); + batteryObj["voltageCheckInterval"].set(gPrefsSettings.getUInt("vCheckIntv", s_batteryCheckInterval)); + } +#endif + if (section == "defaults") { + // default factory settings + JsonObject defaultsObj = obj.createNestedObject("defaults"); + defaultsObj["initVolume"].set(3u); // AUDIOPLAYER_VOLUME_INIT + defaultsObj["maxVolumeSp"].set(21u); // AUDIOPLAYER_VOLUME_MAX + defaultsObj["maxVolumeHp"].set(18u); // gPrefsSettings.getUInt("maxVolumeHp", 0)); + defaultsObj["sleepInactivity"].set(10u); // System_MaxInactivityTime +#ifdef NEOPIXEL_ENABLE + defaultsObj["initBrightness"].set(16u); // LED_INITIAL_BRIGHTNESS + defaultsObj["nightBrightness"].set(2u); // LED_INITIAL_NIGHT_BRIGHTNESS +#endif +#ifdef MEASURE_BATTERY_VOLTAGE + defaultsObj["warnLowVoltage"].set(s_warningLowVoltage); + defaultsObj["indicatorLow"].set(s_voltageIndicatorLow); + defaultsObj["indicatorHi"].set(s_voltageIndicatorHigh); + defaultsObj["voltageCheckInterval"].set(s_batteryCheckInterval); +#endif + } +// FTP +#ifdef FTP_ENABLE + if ((section == "") || (section == "ftp")) { + JsonObject ftpObj = obj.createNestedObject("ftp"); + ftpObj["username"] = gPrefsSettings.getString("ftpuser", "-1"); + ftpObj["password"] = gPrefsSettings.getString("ftppassword", "-1"); + ftpObj["maxUserLength"].set(ftpUserLength - 1); + ftpObj["maxPwdLength"].set(ftpUserLength - 1); + } +#endif +// MQTT +#ifdef MQTT_ENABLE + if ((section == "") || (section == "mqtt")) { + JsonObject mqttObj = obj.createNestedObject("mqtt"); + mqttObj["enable"].set(Mqtt_IsEnabled()); + mqttObj["clientID"] = gPrefsSettings.getString("mqttClientId", "-1"); + mqttObj["server"] = gPrefsSettings.getString("mqttServer", "-1"); + mqttObj["port"].set(gPrefsSettings.getUInt("mqttPort", 0)); + mqttObj["username"] = gPrefsSettings.getString("mqttUser", "-1"); + mqttObj["password"] = gPrefsSettings.getString("mqttPassword", "-1"); + mqttObj["maxUserLength"].set(mqttUserLength - 1); + mqttObj["maxPwdLength"].set(mqttPasswordLength - 1); + mqttObj["maxClientIdLength"].set(mqttClientIdLength - 1); + mqttObj["maxServerLength"].set(mqttServerLength - 1); + } +#endif +// Bluetooth +#ifdef BLUETOOTH_ENABLE + if ((section == "") || (section == "bluetooth")) { + JsonObject btObj = obj.createNestedObject("bluetooth"); + if (gPrefsSettings.isKey("btDeviceName")) { + btObj["deviceName"] = gPrefsSettings.getString("btDeviceName", ""); + } else { + btObj["deviceName"] = ""; + } + if (gPrefsSettings.isKey("btPinCode")) { + btObj["pinCode"] = gPrefsSettings.getString("btPinCode", ""); + } else { + btObj["pinCode"] = ""; + } + } +#endif +} + +// handle get info +void handleGetInfo(AsyncWebServerRequest *request) { + + // param to get a single info section + String section = ""; + if (request->hasParam("section")) { + section = request->getParam("section")->value(); + } +#ifdef BOARD_HAS_PSRAM + SpiRamJsonDocument doc(512); +#else + StaticJsonDocument<512> doc; +#endif + JsonObject infoObj = doc.createNestedObject("info"); + // software + if ((section == "") || (section == "software")) { + JsonObject softwareObj = infoObj.createNestedObject("software"); + softwareObj["version"] = (String) softwareRevision; + softwareObj["git"] = (String) gitRevision; + softwareObj["arduino"] = String(ESP_ARDUINO_VERSION_MAJOR) + "." + String(ESP_ARDUINO_VERSION_MINOR) + "." + String(ESP_ARDUINO_VERSION_PATCH); + softwareObj["idf"] = String(ESP.getSdkVersion()); + } + // hardware + if ((section == "") || (section == "hardware")) { + JsonObject hardwareObj = infoObj.createNestedObject("hardware"); + hardwareObj["model"] = String(ESP.getChipModel()); + hardwareObj["revision"] = ESP.getChipRevision(); + hardwareObj["freq"] = ESP.getCpuFreqMHz(); + } + // memory + if ((section == "") || (section == "memory")) { + JsonObject memoryObj = infoObj.createNestedObject("memory"); + memoryObj["freeHeap"] = ESP.getFreeHeap(); + memoryObj["largestFreeBlock"] = (uint32_t) heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); + if (psramFound()) { + memoryObj["freePSRam"] = ESP.getFreePsram(); + } + } + // wifi + if ((section == "") || (section == "wifi")) { + JsonObject wifiObj = infoObj.createNestedObject("wifi"); + wifiObj["ip"] = Wlan_GetIpAddress(); + wifiObj["rssi"] = (int8_t) Wlan_GetRssi(); + } + // audio + if ((section == "") || (section == "audio")) { + JsonObject audioObj = infoObj.createNestedObject("audio"); + audioObj["playtimeTotal"] = AudioPlayer_GetPlayTimeAllTime(); + audioObj["playtimeSinceStart"] = AudioPlayer_GetPlayTimeSinceStart(); + audioObj["firstStart"] = gPrefsSettings.getULong("firstStart", 0); + } +#ifdef BATTERY_MEASURE_ENABLE + // battery + if ((section == "") || (section == "battery")) { + JsonObject batteryObj = infoObj.createNestedObject("battery"); + batteryObj["currVoltage"] = Battery_GetVoltage(); + batteryObj["chargeLevel"] = Battery_EstimateLevel() * 100; + } +#endif +#ifdef HALLEFFECT_SENSOR_ENABLE + if ((section == "") || (section == "hallsensor")) { + // hallsensor + JsonObject hallObj = infoObj.createNestedObject("hallsensor"); + uint16_t sva = gHallEffectSensor.readSensorValueAverage(true); + int diff = sva - gHallEffectSensor.NullFieldValue(); + + hallObj["nullFieldValue"] = gHallEffectSensor.NullFieldValue(); + hallObj["actual"] = sva; + hallObj["diff"] = diff; + hallObj["lastWaitState"] = gHallEffectSensor.LastWaitForState(); + hallObj["lastWaitMS"] = gHallEffectSensor.LastWaitForStateMS(); + } +#endif + + String serializedJsonString; + serializeJson(infoObj, serializedJsonString); + request->send(200, "application/json; charset=utf-8", serializedJsonString); + System_UpdateActivityTimer(); +} + +// handle get settings +void handleGetSettings(AsyncWebServerRequest *request) { + + // param to get a single settings section + String section = ""; + if (request->hasParam("section")) { + section = request->getParam("section")->value(); + } +#ifdef BOARD_HAS_PSRAM + SpiRamJsonDocument doc(8192); +#else + StaticJsonDocument<8192> doc; +#endif + JsonObject settingsObj = doc.createNestedObject("settings"); + settingsToJSON(settingsObj, section); + String serializedJsonString; + serializeJson(settingsObj, serializedJsonString); + request->send(200, "application/json; charset=utf-8", serializedJsonString); +} + +// handle post settings +void handlePostSettings(AsyncWebServerRequest *request, JsonVariant &json) { + const JsonObject &jsonObj = json.as(); + bool succ = JSONToSettings(jsonObj); + if (succ) { + request->send(200); + } else { + request->send(500, "text/plain; charset=utf-8", "error saving settings"); + } +} + +// Takes inputs from webgui, parses JSON and saves values in NVS +// If operation was successful (NVS-write is verified) true is returned +bool processJsonRequest(char *_serialJson) { + if (!_serialJson) { + return false; + } +#ifdef BOARD_HAS_PSRAM + SpiRamJsonDocument doc(1000); +#else + StaticJsonDocument<1000> doc; +#endif + + DeserializationError error = deserializeJson(doc, _serialJson); + + if (error) { + Log_Printf(LOGLEVEL_ERROR, jsonErrorMsg, error.c_str()); + return false; + } + + JsonObject obj = doc.as(); + return JSONToSettings(obj); +} + // Sends JSON-answers via websocket void Web_SendWebsocketData(uint32_t client, uint8_t code) { - if (!webserverStarted) + if (!webserverStarted) { + // webserver not yet started return; - char *jBuf = (char *) x_calloc(255, sizeof(char)); - - const size_t CAPACITY = JSON_OBJECT_SIZE(1) + 200; - StaticJsonDocument doc; + } + if (ws.count() == 0) { + // we do not have any webclient connected + return; + } + char *jBuf = (char *) x_calloc(1024, sizeof(char)); + StaticJsonDocument<1024> doc; JsonObject object = doc.to(); if (code == 1) { @@ -616,7 +980,7 @@ void Web_SendWebsocketData(uint32_t client, uint8_t code) { object["pong"] = "pong"; object["rssi"] = Wlan_GetRssi(); // todo: battery percent + loading status +++ - //object["battery"] = Battery_GetVoltage(); + // object["battery"] = Battery_GetVoltage(); } else if (code == 30) { JsonObject entry = object.createNestedObject("trackinfo"); entry["pausePlay"] = gPlayProperties.pausePlay; @@ -624,13 +988,26 @@ void Web_SendWebsocketData(uint32_t client, uint8_t code) { entry["numberOfTracks"] = gPlayProperties.numberOfTracks; entry["volume"] = AudioPlayer_GetCurrentVolume(); entry["name"] = gPlayProperties.title; + entry["posPercent"] = gPlayProperties.currentRelPos; + entry["playMode"] = gPlayProperties.playMode; } else if (code == 40) { object["coverimg"] = "coverimg"; } else if (code == 50) { object["volume"] = AudioPlayer_GetCurrentVolume(); + } else if (code == 60) { + JsonObject entry = object.createNestedObject("settings"); + settingsToJSON(entry, ""); + } else if (code == 70) { + JsonObject entry = object.createNestedObject("settings"); + settingsToJSON(entry, "ssids"); + } else if (code == 80) { + JsonObject entry = object.createNestedObject("trackProgress"); + entry["posPercent"] = gPlayProperties.currentRelPos; + entry["time"] = AudioPlayer_GetCurrentTime(); + entry["duration"] = AudioPlayer_GetFileDuration(); }; - serializeJson(doc, jBuf, 255); + serializeJson(doc, jBuf, 1024); if (client == 0) { ws.printfAll(jBuf); @@ -640,67 +1017,64 @@ void Web_SendWebsocketData(uint32_t client, uint8_t code) { free(jBuf); } - // Processes websocket-requests void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { if (type == WS_EVT_CONNECT) { - //client connected - snprintf(Log_Buffer, Log_BufferLength, "ws[%s][%u] connect", server->url(), client->id()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - //client->printf("Hello Client %u :)", client->id()); - //client->ping(); + // client connected + Log_Printf(LOGLEVEL_DEBUG, "ws[%s][%u] connect", server->url(), client->id()); + // client->printf("Hello Client %u :)", client->id()); + // client->ping(); } else if (type == WS_EVT_DISCONNECT) { - //client disconnected - snprintf(Log_Buffer, Log_BufferLength, "ws[%s][%u] disconnect", server->url(), client->id()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + // client disconnected + Log_Printf(LOGLEVEL_DEBUG, "ws[%s][%u] disconnect", server->url(), client->id()); } else if (type == WS_EVT_ERROR) { - //error was received from the other end - snprintf(Log_Buffer, Log_BufferLength, "ws[%s][%u] error(%u): %s", server->url(), client->id(), *((uint16_t *)arg), (char *)data); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + // error was received from the other end + Log_Printf(LOGLEVEL_DEBUG, "ws[%s][%u] error(%u): %s", server->url(), client->id(), *((uint16_t *) arg), (char *) data); } else if (type == WS_EVT_PONG) { - //pong message was received (in response to a ping request maybe) - snprintf(Log_Buffer, Log_BufferLength, "ws[%s][%u] pong[%u]: %s", server->url(), client->id(), len, (len) ? (char *)data : ""); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + // pong message was received (in response to a ping request maybe) + Log_Printf(LOGLEVEL_DEBUG, "ws[%s][%u] pong[%u]: %s", server->url(), client->id(), len, (len) ? (char *) data : ""); } else if (type == WS_EVT_DATA) { - //data packet - AwsFrameInfo *info = (AwsFrameInfo *)arg; + // data packet + const AwsFrameInfo *info = (AwsFrameInfo *) arg; if (info && info->final && info->index == 0 && info->len == len && client && len > 0) { - //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); + // the whole message is in a single frame and we got all of it's data + // Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT) ? "text" : "binary", info->len); - if (processJsonRequest((char *)data)) { - if (data && (strncmp((char *)data, "getTrack", 8))) { // Don't send back ok-feedback if track's name is requested in background + if (processJsonRequest((char *) data)) { + if (data && (strncmp((char *) data, "track", 5))) { // Don't send back ok-feedback if track's name is requested in background Web_SendWebsocketData(client->id(), 1); } } if (info->opcode == WS_TEXT) { data[len] = 0; - //Serial.printf("%s\n", (char *)data); + // Serial.printf("%s\n", (char *)data); } else { for (size_t i = 0; i < info->len; i++) { Serial.printf("%02x ", data[i]); } - //Serial.printf("\n"); + // Serial.printf("\n"); } } } } -void explorerCreateParentDirectories(const char* filePath) { +void explorerCreateParentDirectories(const char *filePath) { char tmpPath[MAX_FILEPATH_LENTGH]; char *rest; rest = strchr(filePath, '/'); while (rest) { - if (rest-filePath != 0){ - memcpy(tmpPath, filePath, rest-filePath); - tmpPath[rest-filePath] = '\0'; - printf("creating dir \"%s\"\n", tmpPath); - gFSystem.mkdir(tmpPath); + if (rest - filePath != 0) { + memcpy(tmpPath, filePath, rest - filePath); + tmpPath[rest - filePath] = '\0'; + if (!gFSystem.exists(tmpPath)) { + Log_Printf(LOGLEVEL_DEBUG, "creating dir \"%s\"\n", tmpPath); + gFSystem.mkdir(tmpPath); + } } - rest = strchr(rest+1, '/'); + rest = strchr(rest + 1, '/'); } } @@ -712,148 +1086,179 @@ void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, s // New File if (!index) { + String utf8Folder = "/"; String utf8FilePath; static char filePath[MAX_FILEPATH_LENTGH]; if (request->hasParam("path")) { - AsyncWebParameter *param = request->getParam("path"); - utf8FilePath = param->value() + "/" + filename; - } else { - utf8FilePath = "/" + filename; + const AsyncWebParameter *param = request->getParam("path"); + utf8Folder = param->value() + "/"; } + utf8FilePath = utf8Folder + filename; - convertUtf8ToAscii(utf8FilePath, filePath); + convertFilenameToAscii(utf8FilePath, filePath); - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *)FPSTR (writingFile), utf8FilePath.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - Web_DeleteCachefile(utf8FilePath.c_str()); + Log_Printf(LOGLEVEL_INFO, writingFile, utf8FilePath.c_str()); // Create Parent directories explorerCreateParentDirectories(filePath); - // Create Ringbuffer for upload - if (explorerFileUploadRingBuffer == NULL) { - explorerFileUploadRingBuffer = xRingbufferCreate(8192, RINGBUF_TYPE_BYTEBUF); - } - // Create Queue for receiving a signal from the store task as synchronisation if (explorerFileUploadStatusQueue == NULL) { explorerFileUploadStatusQueue = xQueueCreate(1, sizeof(uint8_t)); } + // reset buffers + index_buffer_write = 0; + index_buffer_read = 0; + for (uint32_t i = 0; i < nr_of_buffers; i++) { + size_in_buffer[i] = 0; + buffer_full[i] = false; + } + // Create Task for handling the storage of the data xTaskCreatePinnedToCore( explorerHandleFileStorageTask, /* Function to implement the task */ - "fileStorageTask", /* Name of the task */ - 4000, /* Stack size in words */ - filePath, /* Task input parameter */ - 2 | portPRIVILEGE_BIT, /* Priority of the task */ - &fileStorageTaskHandle, /* Task handle. */ - 1 /* Core where the task should run */ + "fileStorageTask", /* Name of the task */ + 4000, /* Stack size in words */ + filePath, /* Task input parameter */ + 2 | portPRIVILEGE_BIT, /* Priority of the task */ + &fileStorageTaskHandle, /* Task handle. */ + 1 /* Core where the task should run */ ); } if (len) { - // stream the incoming chunk to the ringbuffer - xRingbufferSend(explorerFileUploadRingBuffer, data, len, portTICK_PERIOD_MS * 1000); + // wait till buffer is ready + while (buffer_full[index_buffer_write]) { + vTaskDelay(2u); + } + + size_t len_to_write = len; + size_t space_left = chunk_size - size_in_buffer[index_buffer_write]; + if (space_left < len_to_write) { + len_to_write = space_left; + } + // write content to buffer + memcpy(buffer[index_buffer_write] + size_in_buffer[index_buffer_write], data, len_to_write); + size_in_buffer[index_buffer_write] += len_to_write; + + // check if buffer is filled. If full, signal that ready and change buffers + if (size_in_buffer[index_buffer_write] == chunk_size) { + // signal, that buffer is ready. Increment index + buffer_full[index_buffer_write] = true; + index_buffer_write = (index_buffer_write + 1) % nr_of_buffers; + + // if still content left, put it into next buffer + if (len_to_write < len) { + // wait till new buffer is ready + while (buffer_full[index_buffer_write]) { + vTaskDelay(2u); + } + size_t len_left_to_write = len - len_to_write; + memcpy(buffer[index_buffer_write], data + len_to_write, len_left_to_write); + size_in_buffer[index_buffer_write] = len_left_to_write; + } + } } if (final) { + // if file not completely done yet, signal that buffer is filled + if (size_in_buffer[index_buffer_write] > 0) { + buffer_full[index_buffer_write] = true; + } // notify storage task that last data was stored on the ring buffer xTaskNotify(fileStorageTaskHandle, 1u, eNoAction); // watit until the storage task is sending the signal to finish uint8_t signal; xQueueReceive(explorerFileUploadStatusQueue, &signal, portMAX_DELAY); - - // delete task - vTaskDelete(fileStorageTaskHandle); } } // feed the watchdog timer without delay void feedTheDog(void) { - #ifdef SD_MMC_1BIT_MODE - // feed dog 0 - TIMERG0.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable - TIMERG0.wdt_feed=1; // feed dog - TIMERG0.wdt_wprotect=0; // write protect - // feed dog 1 - TIMERG1.wdt_wprotect=TIMG_WDT_WKEY_VALUE; // write enable - TIMERG1.wdt_feed=1; // feed dog - TIMERG1.wdt_wprotect=0; // write protect - #else +#if defined(SD_MMC_1BIT_MODE) && defined(CONFIG_IDF_TARGET_ESP32) + // feed dog 0 + TIMERG0.wdt_wprotect = TIMG_WDT_WKEY_VALUE; // write enable + TIMERG0.wdt_feed = 1; // feed dog + TIMERG0.wdt_wprotect = 0; // write protect + // feed dog 1 + TIMERG1.wdt_wprotect = TIMG_WDT_WKEY_VALUE; // write enable + TIMERG1.wdt_feed = 1; // feed dog + TIMERG1.wdt_wprotect = 0; // write protect +#else // Without delay upload-feature is broken for SD via SPI (for whatever reason...) - vTaskDelay(portTICK_PERIOD_MS * 11); - #endif + vTaskDelay(portTICK_PERIOD_MS * 11); +#endif } void explorerHandleFileStorageTask(void *parameter) { File uploadFile; - size_t item_size; size_t bytesOk = 0; size_t bytesNok = 0; uint32_t chunkCount = 0; uint32_t transferStartTimestamp = millis(); - uint8_t *item; uint8_t value = 0; uint32_t lastUpdateTimestamp = millis(); - uint32_t maxUploadDelay = 20; // After this delay (in seconds) task will be deleted as transfer is considered to be finally broken + uint32_t maxUploadDelay = 20; // After this delay (in seconds) task will be deleted as transfer is considered to be finally broken BaseType_t uploadFileNotification; uint32_t uploadFileNotificationValue; - uploadFile = gFSystem.open((char *)parameter, "w"); - size_t maxItems = xRingbufferGetMaxItemSize(explorerFileUploadRingBuffer); + uploadFile = gFSystem.open((char *) parameter, "w"); + uploadFile.setBufferSize(chunk_size); + + // pause some tasks to get more free CPU time for the upload + vTaskSuspend(AudioTaskHandle); + Led_TaskPause(); + Rfid_TaskPause(); + for (;;) { - // check buffer is filled with enough data - size_t itemsFree = xRingbufferGetCurFreeSize(explorerFileUploadRingBuffer); - item_size = maxItems - itemsFree; - if (item_size < (maxItems / 2)) { - // not enough data in the buffer, check if all data arrived for the file - uploadFileNotification = xTaskNotifyWait(0, 0, &uploadFileNotificationValue, 0); - if (uploadFileNotification == pdPASS) { - item = (uint8_t *)xRingbufferReceive(explorerFileUploadRingBuffer, &item_size, portTICK_PERIOD_MS * 100); - if (item != NULL) { - chunkCount++; - if (!uploadFile.write(item, item_size)) { - bytesNok += item_size; - } else { - bytesOk += item_size; - } - vRingbufferReturnItem(explorerFileUploadRingBuffer, (void *)item); - vTaskDelay(portTICK_PERIOD_MS * 20); + // check buffer is full with enough data or all data already sent + uploadFileNotification = xTaskNotifyWait(0, 0, &uploadFileNotificationValue, 0); + if ((buffer_full[index_buffer_read]) || (uploadFileNotification == pdPASS)) { + + while (buffer_full[index_buffer_read]) { + chunkCount++; + size_t item_size = size_in_buffer[index_buffer_read]; + if (!uploadFile.write(buffer[index_buffer_read], item_size)) { + bytesNok += item_size; + feedTheDog(); + } else { + bytesOk += item_size; } + // update handling of buffers + size_in_buffer[index_buffer_read] = 0; + buffer_full[index_buffer_read] = 0; + index_buffer_read = (index_buffer_read + 1) % nr_of_buffers; + // update timestamp + lastUpdateTimestamp = millis(); + } + + if (uploadFileNotification == pdPASS) { uploadFile.close(); - snprintf(Log_Buffer, Log_BufferLength, "%s: %s => %zu bytes in %lu ms (%lu kiB/s)", (char *)FPSTR (fileWritten), (char *)parameter, bytesNok+bytesOk, (millis() - transferStartTimestamp), (bytesNok+bytesOk)/(millis() - transferStartTimestamp)); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - snprintf(Log_Buffer, Log_BufferLength, "Bytes [ok] %zu / [not ok] %zu, Chunks: %zu\n", bytesOk, bytesNok, chunkCount); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + Log_Printf(LOGLEVEL_INFO, fileWritten, (char *) parameter, bytesNok + bytesOk, (millis() - transferStartTimestamp), (bytesNok + bytesOk) / (millis() - transferStartTimestamp)); + Log_Printf(LOGLEVEL_DEBUG, "Bytes [ok] %zu / [not ok] %zu, Chunks: %zu\n", bytesOk, bytesNok, chunkCount); // done exit loop to terminate break; } - + } else { if (lastUpdateTimestamp + maxUploadDelay * 1000 < millis()) { - snprintf(Log_Buffer, Log_BufferLength, (char *) FPSTR(webTxCanceled)); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Println(webTxCanceled, LOGLEVEL_ERROR); + // resume the paused tasks + Led_TaskResume(); + vTaskResume(AudioTaskHandle); + Rfid_TaskResume(); + // just delete task without signaling (abort) vTaskDelete(NULL); return; } - vTaskDelay(portTICK_PERIOD_MS * 5); + vTaskDelay(portTICK_PERIOD_MS * 2); continue; } - - item = (uint8_t *)xRingbufferReceive(explorerFileUploadRingBuffer, &item_size, portTICK_PERIOD_MS * 100); - if (item != NULL) { - chunkCount++; - if (!uploadFile.write(item, item_size)) { - bytesNok += item_size; - feedTheDog(); - } else { - bytesOk += item_size; - } - vRingbufferReturnItem(explorerFileUploadRingBuffer, (void *)item); - feedTheDog(); - lastUpdateTimestamp = millis(); - } } + // resume the paused tasks + Led_TaskResume(); + vTaskResume(AudioTaskHandle); + Rfid_TaskResume(); // send signal to upload function to terminate xQueueSend(explorerFileUploadStatusQueue, &value, 0); vTaskDelete(NULL); @@ -862,78 +1267,59 @@ void explorerHandleFileStorageTask(void *parameter) { // Sends a list of the content of a directory as JSON file // requires a GET parameter path for the directory void explorerHandleListRequest(AsyncWebServerRequest *request) { - uint32_t listStartTimestamp = millis(); - //DynamicJsonDocument jsonBuffer(8192); - #ifdef BOARD_HAS_PSRAM - SpiRamJsonDocument jsonBuffer(65636); - #else - StaticJsonDocument<8192> jsonBuffer; - #endif +#ifdef NO_SDCARD + request->send(200, "application/json; charset=utf-8", "[]"); // maybe better to send 404 here? + return; +#endif +#ifdef BOARD_HAS_PSRAM + SpiRamJsonDocument jsonBuffer(65636); +#else + StaticJsonDocument<8192> jsonBuffer; +#endif String serializedJsonString; - AsyncWebParameter *param; char filePath[MAX_FILEPATH_LENTGH]; JsonArray obj = jsonBuffer.createNestedArray(); File root; if (request->hasParam("path")) { + AsyncWebParameter *param; param = request->getParam("path"); - convertUtf8ToAscii(param->value(), filePath); + convertFilenameToAscii(param->value(), filePath); root = gFSystem.open(filePath); } else { root = gFSystem.open("/"); } if (!root) { - snprintf(Log_Buffer, Log_BufferLength, (char *) FPSTR(failedToOpenDirectory)); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + Log_Println(failedToOpenDirectory, LOGLEVEL_DEBUG); return; } if (!root.isDirectory()) { - snprintf(Log_Buffer, Log_BufferLength, (char *) FPSTR(notADirectory)); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + Log_Println(notADirectory, LOGLEVEL_DEBUG); return; } - File file = root.openNextFile(); - - while (file) { + bool isDir = false; + String MyfileName = root.getNextFileName(&isDir); + while (MyfileName != "") { // ignore hidden folders, e.g. MacOS spotlight files - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - if (!startsWith( file.path() , (char *)"/.")) { - #else - if (!startsWith( file.name() , (char *)"/.")) { - #endif + if (!startsWith(MyfileName.c_str(), (char *) "/.")) { JsonObject entry = obj.createNestedObject(); - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - convertAsciiToUtf8(file.path(), filePath); - #else - convertAsciiToUtf8(file.name(), filePath); - #endif + convertAsciiToUtf8(MyfileName.c_str(), filePath); std::string path = filePath; std::string fileName = path.substr(path.find_last_of("/") + 1); - entry["name"] = fileName; - entry["dir"].set(file.isDirectory()); - } - file.close(); - file = root.openNextFile(); - - - if (!gPlayProperties.pausePlay) { - // time critical, avoid delay with many files on SD-card! - feedTheDog(); - } else { - // If playback is active this can (at least sometimes) prevent scattering - vTaskDelay(portTICK_PERIOD_MS * 5); + if (isDir) { + entry["dir"].set(true); + } } + MyfileName = root.getNextFileName(&isDir); } root.close(); serializeJson(obj, serializedJsonString); - snprintf(Log_Buffer, Log_BufferLength, "build filelist finished: %lu ms", (millis() - listStartTimestamp)); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - request->send(200, (char *) F("application/json; charset=utf-8"), serializedJsonString); + request->send(200, "application/json; charset=utf-8", serializedJsonString); } bool explorerDeleteDirectory(File dir) { @@ -944,11 +1330,7 @@ bool explorerDeleteDirectory(File dir) { if (file.isDirectory()) { explorerDeleteDirectory(file); } else { - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - gFSystem.remove(file.path()); - #else - gFSystem.remove(file.name()); - #endif + gFSystem.remove(file.path()); } file = dir.openNextFile(); @@ -956,29 +1338,7 @@ bool explorerDeleteDirectory(File dir) { esp_task_wdt_reset(); } - #if ESP_ARDUINO_VERSION_MAJOR >= 2 - return gFSystem.rmdir(dir.path()); - #else - return gFSystem.rmdir(dir.name()); - #endif -} - -// Handles delete-requests for cachefiles. -// This is necessary to avoid outdated cachefiles if content of a directory changes (create, rename, delete). -void Web_DeleteCachefile(const char *fileOrDirectory) { - char cacheFile[MAX_FILEPATH_LENTGH]; - const char s = '/'; - char *last = strrchr(fileOrDirectory, s); - char *first = strchr(fileOrDirectory, s); - unsigned long substr = last - first + 1; - snprintf(cacheFile, substr+1, "%s", fileOrDirectory); - strcat(cacheFile, playlistCacheFile); - if (gFSystem.exists(cacheFile)) { - if (gFSystem.remove(cacheFile)) { - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(erasePlaylistCachefile), cacheFile); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - } - } + return gFSystem.rmdir(dir.path()); } // Handles download request of a file @@ -989,24 +1349,22 @@ void explorerHandleDownloadRequest(AsyncWebServerRequest *request) { char filePath[MAX_FILEPATH_LENTGH]; // check has path param if (!request->hasParam("path")) { - Log_Println((char *) F("DOWNLOAD: No path variable set"), LOGLEVEL_ERROR); + Log_Println("DOWNLOAD: No path variable set", LOGLEVEL_ERROR); request->send(404); return; } // check file exists on SD card param = request->getParam("path"); - convertUtf8ToAscii(param->value(), filePath); + convertFilenameToAscii(param->value(), filePath); if (!gFSystem.exists(filePath)) { - snprintf(Log_Buffer, Log_BufferLength, "DOWNLOAD: File not found on SD card: %s", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "DOWNLOAD: File not found on SD card: %s", param->value().c_str()); request->send(404); return; } // check is file and not a directory file = gFSystem.open(filePath); if (file.isDirectory()) { - snprintf(Log_Buffer, Log_BufferLength, "DOWNLOAD: Cannot download a directory %s", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "DOWNLOAD: Cannot download a directory %s", param->value().c_str()); request->send(404); file.close(); return; @@ -1019,12 +1377,12 @@ void explorerHandleDownloadRequest(AsyncWebServerRequest *request) { }; fileBlk *fileObj = new fileBlk; fileObj->dataFile = file; - request->_tempObject = (void*)fileObj; + request->_tempObject = (void *) fileObj; AsyncWebServerResponse *response = request->beginResponse(dataType, fileObj->dataFile.size(), [request](uint8_t *buffer, size_t maxlen, size_t index) -> size_t { - fileBlk *fileObj = (fileBlk*)request->_tempObject; + fileBlk *fileObj = (fileBlk *) request->_tempObject; size_t thisSize = fileObj->dataFile.read(buffer, maxlen); - if((index + thisSize) >= fileObj->dataFile.size()){ + if ((index + thisSize) >= fileObj->dataFile.size()) { fileObj->dataFile.close(); request->_tempObject = NULL; delete fileObj; @@ -1040,37 +1398,34 @@ void explorerHandleDownloadRequest(AsyncWebServerRequest *request) { // requires a GET parameter path to the file or directory void explorerHandleDeleteRequest(AsyncWebServerRequest *request) { File file; - AsyncWebParameter *param; char filePath[MAX_FILEPATH_LENTGH]; if (request->hasParam("path")) { + AsyncWebParameter *param; param = request->getParam("path"); - convertUtf8ToAscii(param->value(), filePath); + convertFilenameToAscii(param->value(), filePath); if (gFSystem.exists(filePath)) { + // stop playback, file to delete might be in use + Cmd_Action(CMD_STOP); file = gFSystem.open(filePath); if (file.isDirectory()) { if (explorerDeleteDirectory(file)) { - snprintf(Log_Buffer, Log_BufferLength, "DELETE: %s deleted", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, "DELETE: %s deleted", param->value().c_str()); } else { - snprintf(Log_Buffer, Log_BufferLength, "DELETE: Cannot delete %s", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "DELETE: Cannot delete %s", param->value().c_str()); } } else { + const String cPath = filePath; if (gFSystem.remove(filePath)) { - snprintf(Log_Buffer, Log_BufferLength, "DELETE: %s deleted", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - Web_DeleteCachefile(filePath); + Log_Printf(LOGLEVEL_INFO, "DELETE: %s deleted", param->value().c_str()); } else { - snprintf(Log_Buffer, Log_BufferLength, "DELETE: Cannot delete %s", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "DELETE: Cannot delete %s", param->value().c_str()); } } } else { - snprintf(Log_Buffer, Log_BufferLength, "DELETE: Path %s does not exist", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "DELETE: Path %s does not exist", param->value().c_str()); } } else { - Log_Println((char *) F("DELETE: No path variable set"), LOGLEVEL_ERROR); + Log_Println("DELETE: No path variable set", LOGLEVEL_ERROR); } request->send(200); esp_task_wdt_reset(); @@ -1079,20 +1434,18 @@ void explorerHandleDeleteRequest(AsyncWebServerRequest *request) { // Handles create request of a directory // requires a GET parameter path to the new directory void explorerHandleCreateRequest(AsyncWebServerRequest *request) { - AsyncWebParameter *param; - char filePath[MAX_FILEPATH_LENTGH]; if (request->hasParam("path")) { + AsyncWebParameter *param; + char filePath[MAX_FILEPATH_LENTGH]; param = request->getParam("path"); - convertUtf8ToAscii(param->value(), filePath); + convertFilenameToAscii(param->value(), filePath); if (gFSystem.mkdir(filePath)) { - snprintf(Log_Buffer, Log_BufferLength, "CREATE: %s created", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); + Log_Printf(LOGLEVEL_INFO, "CREATE: %s created", param->value().c_str()); } else { - snprintf(Log_Buffer, Log_BufferLength, "CREATE: Cannot create %s", param->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "CREATE: Cannot create %s", param->value().c_str()); } } else { - Log_Println((char *) F("CREATE: No path variable set"), LOGLEVEL_ERROR); + Log_Println("CREATE: No path variable set", LOGLEVEL_ERROR); } request->send(200); } @@ -1101,30 +1454,26 @@ void explorerHandleCreateRequest(AsyncWebServerRequest *request) { // requires a GET parameter srcpath to the old file or directory name // requires a GET parameter dstpath to the new file or directory name void explorerHandleRenameRequest(AsyncWebServerRequest *request) { - AsyncWebParameter *srcPath; - AsyncWebParameter *dstPath; - char srcFullFilePath[MAX_FILEPATH_LENTGH]; - char dstFullFilePath[MAX_FILEPATH_LENTGH]; if (request->hasParam("srcpath") && request->hasParam("dstpath")) { + AsyncWebParameter *srcPath; + AsyncWebParameter *dstPath; + char srcFullFilePath[MAX_FILEPATH_LENTGH]; + char dstFullFilePath[MAX_FILEPATH_LENTGH]; srcPath = request->getParam("srcpath"); dstPath = request->getParam("dstpath"); - convertUtf8ToAscii(srcPath->value(), srcFullFilePath); - convertUtf8ToAscii(dstPath->value(), dstFullFilePath); + convertFilenameToAscii(srcPath->value(), srcFullFilePath); + convertFilenameToAscii(dstPath->value(), dstFullFilePath); if (gFSystem.exists(srcFullFilePath)) { if (gFSystem.rename(srcFullFilePath, dstFullFilePath)) { - snprintf(Log_Buffer, Log_BufferLength, "RENAME: %s renamed to %s", srcPath->value().c_str(), dstPath->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - Web_DeleteCachefile(dstFullFilePath); + Log_Printf(LOGLEVEL_INFO, "RENAME: %s renamed to %s", srcPath->value().c_str(), dstPath->value().c_str()); } else { - snprintf(Log_Buffer, Log_BufferLength, "RENAME: Cannot rename %s", srcPath->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "RENAME: Cannot rename %s", srcPath->value().c_str()); } } else { - snprintf(Log_Buffer, Log_BufferLength, "RENAME: Path %s does not exist", srcPath->value().c_str()); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + Log_Printf(LOGLEVEL_ERROR, "RENAME: Path %s does not exist", srcPath->value().c_str()); } } else { - Log_Println((char *) F("RENAME: No path variable set"), LOGLEVEL_ERROR); + Log_Println("RENAME: No path variable set", LOGLEVEL_ERROR); } request->send(200); @@ -1137,29 +1486,286 @@ void explorerHandleAudioRequest(AsyncWebServerRequest *request) { AsyncWebParameter *param; String playModeString; uint32_t playMode; - char filePath[MAX_FILEPATH_LENTGH]; if (request->hasParam("path") && request->hasParam("playmode")) { param = request->getParam("path"); - convertUtf8ToAscii(param->value(), filePath); + char filePath[MAX_FILEPATH_LENTGH]; + convertFilenameToAscii(param->value(), filePath); param = request->getParam("playmode"); playModeString = param->value(); playMode = atoi(playModeString.c_str()); AudioPlayer_TrackQueueDispatcher(filePath, 0, playMode, 0); } else { - Log_Println((char *) F("AUDIO: No path variable set"), LOGLEVEL_ERROR); + Log_Println("AUDIO: No path variable set", LOGLEVEL_ERROR); } request->send(200); } +void handleGetSavedSSIDs(AsyncWebServerRequest *request) { + AsyncJsonResponse *response = new AsyncJsonResponse(true); + JsonArray json_ssids = response->getRoot(); + Wlan_GetSavedNetworks([json_ssids](const WiFiSettings &network) { + json_ssids.add(network.ssid); + }); + + response->setLength(); + request->send(response); +} + +void handlePostSavedSSIDs(AsyncWebServerRequest *request, JsonVariant &json) { + const JsonObject &jsonObj = json.as(); + + struct WiFiSettings networkSettings; + + // TODO: we truncate ssid and password, which is better than not checking at all, but still silently failing + strncpy(networkSettings.ssid, (const char *) jsonObj["ssid"], 32); + networkSettings.ssid[32] = '\0'; + strncpy(networkSettings.password, (const char *) jsonObj["pwd"], 64); + networkSettings.password[64] = '\0'; + + networkSettings.use_static_ip = (bool) jsonObj["static"]; + + if (jsonObj.containsKey("static_addr")) { + networkSettings.static_addr = (uint32_t) IPAddress().fromString((const char *) jsonObj["static_addr"]); + } + if (jsonObj.containsKey("static_gateway")) { + networkSettings.static_gateway = (uint32_t) IPAddress().fromString((const char *) jsonObj["static_gateway"]); + } + if (jsonObj.containsKey("static_subnet")) { + networkSettings.static_subnet = (uint32_t) IPAddress().fromString((const char *) jsonObj["static_subnet"]); + } + if (jsonObj.containsKey("static_dns1")) { + networkSettings.static_dns1 = (uint32_t) IPAddress().fromString((const char *) jsonObj["static_dns1"]); + } + if (jsonObj.containsKey("static_dns2")) { + networkSettings.static_dns2 = (uint32_t) IPAddress().fromString((const char *) jsonObj["static_dns2"]); + } + + bool succ = Wlan_AddNetworkSettings(networkSettings); + + if (succ) { + request->send(200, "text/plain; charset=utf-8", networkSettings.ssid); + } else { + request->send(500, "text/plain; charset=utf-8", "error adding network"); + } +} + +void handleDeleteSavedSSIDs(AsyncWebServerRequest *request) { + const AsyncWebParameter *p = request->getParam("ssid"); + const String ssid = p->value(); + + bool succ = Wlan_DeleteNetwork(ssid); + + if (succ) { + request->send(200, "text/plain; charset=utf-8", ssid); + } else { + request->send(500, "text/plain; charset=utf-8", "error deleting network"); + } +} + +void handleGetActiveSSID(AsyncWebServerRequest *request) { + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject obj = response->getRoot(); + + if (Wlan_IsConnected()) { + String active = Wlan_GetCurrentSSID(); + obj["active"] = active; + } + + response->setLength(); + request->send(response); +} + +void handleGetWiFiConfig(AsyncWebServerRequest *request) { + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject obj = response->getRoot(); + bool scanWifiOnStart = gPrefsSettings.getBool("ScanWiFiOnStart", false); + + obj["hostname"] = Wlan_GetHostname(); + obj["scanOnStart"].set(scanWifiOnStart); + + response->setLength(); + request->send(response); +} + +void handlePostWiFiConfig(AsyncWebServerRequest *request, JsonVariant &json) { + const JsonObject &jsonObj = json.as(); + + // always perform perform a WiFi scan on startup? + bool alwaysScan = jsonObj["scanOnStart"]; + gPrefsSettings.putBool("ScanWiFiOnStart", alwaysScan); + + // hostname + String strHostname = jsonObj["hostname"]; + if (!Wlan_ValidateHostname(strHostname)) { + Log_Println("hostname validation failed", LOGLEVEL_ERROR); + request->send(400, "text/plain; charset=utf-8", "hostname validation failed"); + return; + } + + bool succ = Wlan_SetHostname(strHostname); + if (succ) { + Log_Println("WiFi configuration saved.", LOGLEVEL_NOTICE); + request->send(200, "text/plain; charset=utf-8", strHostname); + } else { + Log_Println("error setting hostname", LOGLEVEL_ERROR); + request->send(500, "text/plain; charset=utf-8", "error setting hostname"); + } +} + +static bool tagIdToJSON(const String tagId, JsonObject entry) { + String s = gPrefsRfid.getString(tagId.c_str(), "-1"); // Try to lookup rfidId in NVS + if (!s.compareTo("-1")) { + return false; + } + char _file[255]; + uint32_t _lastPlayPos = 0; + uint16_t _trackLastPlayed = 0; + uint32_t _mode = 1; + char *token; + uint8_t i = 1; + token = strtok((char *) s.c_str(), stringDelimiter); + while (token != NULL) { // Try to extract data from string after lookup + if (i == 1) { + strncpy(_file, token, sizeof(_file) / sizeof(_file[0])); + } else if (i == 2) { + _lastPlayPos = strtoul(token, NULL, 10); + } else if (i == 3) { + _mode = strtoul(token, NULL, 10); + } else if (i == 4) { + _trackLastPlayed = strtoul(token, NULL, 10); + } + i++; + token = strtok(NULL, stringDelimiter); + } + entry["id"] = tagId; + if (_mode >= 100) { + entry["modId"] = _mode; + } else { + entry["fileOrUrl"] = _file; + entry["playMode"] = _mode; + entry["lastPlayPos"] = _lastPlayPos; + entry["trackLastPlayed"] = _trackLastPlayed; + } + return true; +} + +// callback for writing a NVS entry to JSON +bool DumpNvsToJSONCallback(const char *key, void *data) { + JsonArray *myArr = (JsonArray *) data; + JsonObject obj = myArr->createNestedObject(); + return tagIdToJSON(key, obj); +} + +static void handleGetRFIDRequest(AsyncWebServerRequest *request) { + String tagId = ""; + if (request->hasParam("id")) { + tagId = request->getParam("id")->value(); + } + if (tagId == "") { + // return all RFID assignments + AsyncJsonResponse *response = new AsyncJsonResponse(true, 8192); + JsonArray Arr = response->getRoot(); + if (listNVSKeys("rfidTags", &Arr, DumpNvsToJSONCallback)) { + response->setLength(); + request->send(response); + } else { + request->send(500, "error reading entries from NVS"); + } + } else { + if (gPrefsRfid.isKey(tagId.c_str())) { + // return single RFID assignment + AsyncJsonResponse *response = new AsyncJsonResponse(false); + JsonObject obj = response->getRoot(); + tagIdToJSON(tagId, obj); + response->setLength(); + request->send(response); + } else { + // RFID assignment not found + request->send(404); + } + } +} + +static void handlePostRFIDRequest(AsyncWebServerRequest *request, JsonVariant &json) { + const JsonObject &jsonObj = json.as(); + + String tagId = jsonObj["id"]; + if (tagId.isEmpty()) { + Log_Println("/rfid (POST): Missing tag id", LOGLEVEL_ERROR); + request->send(500, "text/plain; charset=utf-8", "/rfid (POST): Missing tag id"); + return; + } + String fileOrUrl = jsonObj["fileOrUrl"]; + if (fileOrUrl.isEmpty()) { + fileOrUrl = "0"; + } + char _fileOrUrlAscii[MAX_FILEPATH_LENTGH]; + convertFilenameToAscii(fileOrUrl, _fileOrUrlAscii); + uint8_t _playModeOrModId; + if (jsonObj.containsKey("modId")) { + _playModeOrModId = jsonObj["modId"]; + } else { + _playModeOrModId = jsonObj["playMode"]; + } + if (_playModeOrModId <= 0) { + Log_Println("/rfid (POST): Invalid playMode or modId", LOGLEVEL_ERROR); + request->send(500, "text/plain; charset=utf-8", "/rfid (POST): Invalid playMode or modId"); + return; + } + char rfidString[275]; + snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s%s%s0%s%u%s0", stringDelimiter, _fileOrUrlAscii, stringDelimiter, stringDelimiter, _playModeOrModId, stringDelimiter); + gPrefsRfid.putString(tagId.c_str(), rfidString); + + String s = gPrefsRfid.getString(tagId.c_str(), "-1"); + if (s.compareTo(rfidString)) { + request->send(500, "text/plain; charset=utf-8", "/rfid (POST): cannot save assignment to NVS"); + return; + } + Web_DumpNvsToSd("rfidTags", backupFile); // Store backup-file every time when a new rfid-tag is programmed + // return the new/modified RFID assignment + AsyncJsonResponse *response = new AsyncJsonResponse(false); + JsonObject obj = response->getRoot(); + tagIdToJSON(tagId, obj); + response->setLength(); + request->send(response); +} + +static void handleDeleteRFIDRequest(AsyncWebServerRequest *request) { + String tagId = ""; + if (request->hasParam("id")) { + tagId = request->getParam("id")->value(); + } + if (tagId.isEmpty()) { + Log_Println("/rfid (DELETE): Missing tag id", LOGLEVEL_ERROR); + request->send(500, "text/plain; charset=utf-8", "/rfid (DELETE): Missing tag id"); + return; + } + if (gPrefsRfid.isKey(tagId.c_str())) { + if (tagId.equals(gCurrentRfidTagId)) { + // stop playback, tag to delete is in use + Cmd_Action(CMD_STOP); + } + if (gPrefsRfid.remove(tagId.c_str())) { + Log_Printf(LOGLEVEL_INFO, "/rfid (DELETE): tag %s removed successfuly", tagId); + request->send(200, "text/plain; charset=utf-8", tagId + " removed successfuly"); + } else { + Log_Println("/rfid (DELETE):error removing tag from NVS", LOGLEVEL_ERROR); + request->send(500, "text/plain; charset=utf-8", "error removing tag from NVS"); + } + } else { + Log_Printf(LOGLEVEL_DEBUG, "/rfid (DELETE): tag %s not exists", tagId); + request->send(404, "text/plain; charset=utf-8", "error removing tag from NVS: Tag not exists"); + } +} + // Takes stream from file-upload and writes payload into a temporary sd-file. void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { static File tmpFile; static size_t fileIndex = 0; static char tmpFileName[13]; esp_task_wdt_reset(); - if (!index) { snprintf(tmpFileName, 13, "/_%lu", millis()); tmpFile = gFSystem.open(tmpFileName, FILE_WRITE); @@ -1168,14 +1774,17 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, } if (!tmpFile) { - Log_Println((char *) FPSTR(errorWritingTmpfile), LOGLEVEL_ERROR); + Log_Println(errorWritingTmpfile, LOGLEVEL_ERROR); return; } - for (size_t i = 0; i < len; i++) { - tmpFile.printf("%c", data[i]); + size_t wrote = tmpFile.write(data, len); + if (wrote != len) { + // we did not write all bytes --> fail + Log_Printf(LOGLEVEL_ERROR, "Error writing %s. Expected %u, wrote %u (error: %u)!", tmpFile.path(), len, wrote, tmpFile.getWriteError()); + return; } - fileIndex += len; + fileIndex += wrote; if (final) { tmpFile.close(); @@ -1193,17 +1802,27 @@ void Web_DumpSdToNvs(const char *_filename) { uint16_t importCount = 0; uint16_t invalidCount = 0; nvs_t nvsEntry[1]; - char buf; File tmpFile = gFSystem.open(_filename); - if (!tmpFile) { - Log_Println((char *) FPSTR(errorReadingTmpfile), LOGLEVEL_ERROR); + if (!tmpFile || (tmpFile.available() < 3)) { + Log_Println(errorReadingTmpfile, LOGLEVEL_ERROR); return; } Led_SetPause(true); + // try to read UTF-8 BOM marker + bool isUtf8 = (tmpFile.read() == 0xEF) && (tmpFile.read() == 0xBB) && (tmpFile.read() == 0xBF); + if (!isUtf8) { + // no BOM found, reset to start of file + tmpFile.seek(0); + } + while (tmpFile.available() > 0) { - buf = tmpFile.read(); + if (j >= sizeof(ebuf)) { + Log_Println(errorReadingTmpfile, LOGLEVEL_ERROR); + return; + } + char buf = tmpFile.read(); if (buf != '\n') { ebuf[j++] = buf; } else { @@ -1217,14 +1836,17 @@ void Web_DumpSdToNvs(const char *_filename) { nvsEntry[0].nvsKey[strlen(token)] = '\0'; } else { count = false; - memcpy(nvsEntry[0].nvsEntry, token, strlen(token)); - nvsEntry[0].nvsEntry[strlen(token)] = '\0'; + if (isUtf8) { + memcpy(nvsEntry[0].nvsEntry, token, strlen(token)); + nvsEntry[0].nvsEntry[strlen(token)] = '\0'; + } else { + convertAsciiToUtf8(String(token), nvsEntry[0].nvsEntry); + } } token = strtok(NULL, stringOuterDelimiter); } if (isNumber(nvsEntry[0].nvsKey) && nvsEntry[0].nvsEntry[0] == '#') { - snprintf(Log_Buffer, Log_BufferLength, "[%u] %s: %s => %s", ++importCount, (char *) FPSTR(writeEntryToNvs), nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, writeEntryToNvs, ++importCount, nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry); gPrefsRfid.putString(nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry); } else { invalidCount++; @@ -1233,80 +1855,11 @@ void Web_DumpSdToNvs(const char *_filename) { } Led_SetPause(false); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(importCountNokNvs), invalidCount); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, importCountNokNvs, invalidCount); tmpFile.close(); gFSystem.remove(_filename); } -// Dumps all RFID-entries from NVS into a file on SD-card -bool Web_DumpNvsToSd(const char *_namespace, const char *_destFile) { - Led_SetPause(true); // Workaround to prevent exceptions due to Neopixel-signalisation while NVS-write - esp_partition_iterator_t pi; // Iterator for find - const esp_partition_t *nvs; // Pointer to partition struct - esp_err_t result = ESP_OK; - const char *partname = "nvs"; - uint8_t pagenr = 0; // Page number in NVS - uint8_t i; // Index in Entry 0..125 - uint8_t bm; // Bitmap for an entry - uint32_t offset = 0; // Offset in nvs partition - uint8_t namespace_ID; // Namespace ID found - - pi = esp_partition_find(ESP_PARTITION_TYPE_DATA, // Get partition iterator for - ESP_PARTITION_SUBTYPE_ANY, // this partition - partname); - if (pi) { - nvs = esp_partition_get(pi); // Get partition struct - esp_partition_iterator_release(pi); // Release the iterator - dbgprint("Partition %s found, %d bytes", partname, nvs->size); - } else { - snprintf(Log_Buffer, Log_BufferLength, "Partition %s not found!", partname); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - return NULL; - } - namespace_ID = FindNsID(nvs, _namespace); // Find ID of our namespace in NVS - File backupFile = gFSystem.open(_destFile, FILE_WRITE); - if (!backupFile) { - return false; - } - while (offset < nvs->size) { - result = esp_partition_read(nvs, offset, // Read 1 page in nvs partition - &buf, - sizeof(nvs_page)); - if (result != ESP_OK) { - snprintf(Log_Buffer, Log_BufferLength, (char *) F("Error reading NVS!")); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - return false; - } - - i = 0; - - while (i < 126) { - bm = (buf.Bitmap[i / 4] >> ((i % 4) * 2)) & 0x03; // Get bitmap for this entry - if (bm == 2) { - if ((namespace_ID == 0xFF) || // Show all if ID = 0xFF - (buf.Entry[i].Ns == namespace_ID)) { // otherwise just my namespace - if (isNumber(buf.Entry[i].Key)) { - String s = gPrefsRfid.getString((const char *)buf.Entry[i].Key); - backupFile.printf("%s%s%s%s\n", stringOuterDelimiter, buf.Entry[i].Key, stringOuterDelimiter, s.c_str()); - } - } - i += buf.Entry[i].Span; // Next entry - } else { - i++; - } - } - offset += sizeof(nvs_page); // Prepare to read next page in nvs - pagenr++; - } - - backupFile.close(); - Led_SetPause(false); - - return true; -} - - // handle album cover image request static void handleCoverImageRequest(AsyncWebServerRequest *request) { @@ -1315,14 +1868,14 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) { // request->send(200, "image/svg+xml", ""); if (gPlayProperties.playMode == WEBSTREAM) { // no cover -> send placeholder icon for webstream (fa-soundcloud) - snprintf(Log_Buffer, Log_BufferLength, (char *) F("no cover image for webstream")); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - request->send(200, "image/svg+xml", FPSTR("")); + Log_Println("no cover image for webstream", LOGLEVEL_NOTICE); + request->send(200, "image/svg+xml", ""); } else { // no cover -> send placeholder icon for playing music from SD-card (fa-music) - snprintf(Log_Buffer, Log_BufferLength, (char *) F("no cover image for SD-card audio")); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - request->send(200, "image/svg+xml", FPSTR("")); + if (gPlayProperties.playMode != NO_PLAYLIST) { + Log_Println("no cover image for SD-card audio", LOGLEVEL_DEBUG); + } + request->send(200, "image/svg+xml", ""); } return; } @@ -1341,8 +1894,7 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) { break; } } - snprintf(Log_Buffer, Log_BufferLength, "serve cover image (%s): %s", (char *) mimeType, coverFileName); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, "serve cover image (%s): %s", mimeType, coverFileName); // skip image type (1 Byte) coverFile.read(); @@ -1358,18 +1910,16 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) { } int imageSize = gPlayProperties.coverFileSize; - AsyncWebServerResponse *response = request->beginChunkedResponse(mimeType, [coverFile,imageSize](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { - - if (maxLen > 1024) { - maxLen = 1024; - } + AsyncWebServerResponse *response = request->beginChunkedResponse(mimeType, [coverFile, imageSize](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + // some kind of webserver bug with actual size available, reduce the len + maxLen = maxLen >> 1; File file = coverFile; // local copy of file pointer size_t leftToWrite = imageSize - index; - if(! leftToWrite) { - return 0;//end of transfer + if (!leftToWrite) { + return 0; // end of transfer } - size_t willWrite = (leftToWrite > maxLen)?maxLen:leftToWrite; + size_t willWrite = (leftToWrite > maxLen) ? maxLen : leftToWrite; file.read(buffer, willWrite); index += willWrite; return willWrite; diff --git a/src/Web.h b/src/Web.h index 1c0529dc..c4ad9f7f 100644 --- a/src/Web.h +++ b/src/Web.h @@ -1,5 +1,4 @@ #pragma once -void Web_Init(void); void Web_Cyclic(void); void Web_SendWebsocketData(uint32_t client, uint8_t code); diff --git a/src/Wlan.cpp b/src/Wlan.cpp index 6c8cdb5b..76cfbe7e 100644 --- a/src/Wlan.cpp +++ b/src/Wlan.cpp @@ -1,178 +1,553 @@ #include -#include -#include #include "settings.h" + +#include "Wlan.h" + #include "AudioPlayer.h" -#include "RotaryEncoder.h" #include "Log.h" +#include "MemX.h" +#include "RotaryEncoder.h" #include "System.h" #include "Web.h" -#include "MemX.h" +#include "esp_sntp.h" +#include "main.h" -// HELPER // -unsigned long wifiCheckLastTimestamp = 0; -bool wifiEnabled; // Current status if wifi is enabled -uint32_t wifiStatusToggledTimestamp = 0; -bool wifiNeedsRestart = false; -uint8_t wifiConnectIteration = 0; -bool wifiConnectionTryInProgress = false; -bool wifiInit = true; -uint32_t lastPrintRssiTimestamp = 0; +#include +#include +#include + +#define WIFI_STATE_INIT 0u +#define WIFI_STATE_CONNECT_LAST 1u +#define WIFI_STATE_SCAN_CONN 2u +#define WIFI_STATE_CONN_SUCCESS 3u +#define WIFI_STATE_CONNECTED 4u +#define WIFI_STATE_DISCONNECTED 5u +#define WIFI_STATE_CONN_FAILED 6u +#define WIFI_STATE_AP 7u +#define WIFI_STATE_END 8u + +uint8_t wifiState = WIFI_STATE_INIT; + +#define RECONNECT_INTERVAL 600000 // AP-WiFi -IPAddress apIP(192, 168, 4, 1); // Access-point's static IP +IPAddress apIP(192, 168, 4, 1); // Access-point's static IP IPAddress apNetmask(255, 255, 255, 0); // Access-point's netmask -bool accessPointStarted = false; -void accessPointStart(const char *SSID, IPAddress ip, IPAddress netmask); +bool wifiEnabled; // Current status if wifi is enabled + +void accessPointStart(const char *SSID, const char *password, IPAddress ip, IPAddress netmask); bool getWifiEnableStatusFromNVS(void); void writeWifiStatusToNVS(bool wifiStatus); -bool Wlan_IsConnected(void); -int8_t Wlan_GetRssi(void); -// WiFi-credentials + hostname -String hostname; -char *_ssid; -char *_pwd; +void handleWifiStateInit(); + +// state for connection attempt +static uint8_t scanIndex = 0; +static uint8_t connectionAttemptCounter = 0; +static unsigned long connectStartTimestamp = 0; +static uint32_t connectionFailedTimestamp = 0; + +// state for persistent settings +static const char *nvsWiFiNetworksKey = "SAVED_WIFIS"; +static constexpr size_t maxSavedNetworks = 10; +static size_t numKnownNetworks = 0; +static WiFiSettings knownNetworks[maxSavedNetworks]; +static String hostname; + +// state for AP +DNSServer *dnsServer; +constexpr uint8_t DNS_PORT = 53; void Wlan_Init(void) { wifiEnabled = getWifiEnableStatusFromNVS(); + + hostname = gPrefsSettings.getString("Hostname"); + // Get (optional) hostname-configuration from NVS + if (hostname) { + Log_Printf(LOGLEVEL_INFO, restoredHostnameFromNvs, hostname.c_str()); + } else { + Log_Println(wifiHostnameNotSet, LOGLEVEL_INFO); + } + + // load array of up to maxSavedNetworks from NVS + numKnownNetworks = gPrefsSettings.getBytes(nvsWiFiNetworksKey, knownNetworks, maxSavedNetworks * sizeof(WiFiSettings)) / sizeof(WiFiSettings); + for (int i = 0; i < numKnownNetworks; i++) { + knownNetworks[i].ssid[32] = '\0'; + knownNetworks[i].password[64] = '\0'; + Log_Printf(LOGLEVEL_NOTICE, wifiNetworkLoaded, i, knownNetworks[i].ssid); + } + + // ******************* MIGRATION ******************* + // migration from single-wifi setup. Delete some time in the future + if (gPrefsSettings.isKey("SSID") && gPrefsSettings.isKey("Password")) { + String strSSID = gPrefsSettings.getString("SSID", ""); + String strPassword = gPrefsSettings.getString("Password", ""); + Log_Println("migrating from old wifi NVS settings!", LOGLEVEL_NOTICE); + gPrefsSettings.putString("LAST_SSID", strSSID); + + struct WiFiSettings networkSettings; + + strncpy(networkSettings.ssid, strSSID.c_str(), 32); + networkSettings.ssid[32] = '\0'; + strncpy(networkSettings.password, strPassword.c_str(), 64); + networkSettings.password[64] = '\0'; + networkSettings.use_static_ip = false; + +#ifdef STATIC_IP_ENABLE + networkSettings.static_addr = (uint32_t) IPAddress(LOCAL_IP); + networkSettings.static_gateway = (uint32_t) IPAddress(GATEWAY_IP); + networkSettings.static_subnet = (uint32_t) IPAddress(SUBNET_IP); + networkSettings.static_dns1 = (uint32_t) IPAddress(DNS_IP); + networkSettings.use_static_ip = true; +#endif + + Wlan_AddNetworkSettings(networkSettings); + // clean up old values from nvs + gPrefsSettings.remove("SSID"); + gPrefsSettings.remove("Password"); + } + + // ******************* MIGRATION ******************* + + if (OPMODE_NORMAL != System_GetOperationMode()) { + wifiState = WIFI_STATE_END; + return; + } + + // The use of dynamic allocation is recommended to save memory and reduce resources usage. + // However, the dynamic performs slightly slower than the static allocation. + // Use static allocation if you want to have more performance and if your application is multi-tasking. + // Arduino 2.0.x only, comment to use dynamic buffers. + + // for Arduino 2.0.9 this does not seem to bring any advantage just more memory use, so leave it outcommented + // WiFi.useStaticBuffers(true); + + wifiState = WIFI_STATE_INIT; + handleWifiStateInit(); } -void Wlan_Cyclic(void) { - // If wifi would not be activated, return instantly +void connectToKnownNetwork(WiFiSettings settings, byte *bssid = nullptr) { + // set hostname on connect, because when resetting wifi config elsewhere it could be reset + if (hostname.compareTo("-1")) { + WiFi.setHostname(hostname.c_str()); + } + + if (settings.use_static_ip) { + Log_Println(tryStaticIpConfig, LOGLEVEL_NOTICE); + if (!WiFi.config( + IPAddress(settings.static_addr), + IPAddress(settings.static_gateway), + IPAddress(settings.static_subnet), + IPAddress(settings.static_dns1), + IPAddress(settings.static_dns2))) { + Log_Println(staticIPConfigFailed, LOGLEVEL_ERROR); + } + } + + Log_Printf(LOGLEVEL_NOTICE, wifiConnectionInProgress, settings.ssid); + + WiFi.begin(settings.ssid, settings.password, 0, bssid); +} + +void handleWifiStateInit() { if (!wifiEnabled) { + wifiState = WIFI_STATE_END; return; } - if (wifiInit || wifiNeedsRestart) { - wifiConnectionTryInProgress = true; - bool wifiAccessIncomplete = false; - // Get credentials from NVS - String strSSID = gPrefsSettings.getString("SSID", "-1"); - if (!strSSID.compareTo("-1")) { - Log_Println((char *) FPSTR(ssidNotFoundInNvs), LOGLEVEL_ERROR); - wifiAccessIncomplete = true; - } - String strPassword = gPrefsSettings.getString("Password", "-1"); - if (!strPassword.compareTo("-1")) { - Log_Println((char *) FPSTR(wifiPwdNotFoundInNvs), LOGLEVEL_ERROR); - wifiAccessIncomplete = true; + WiFi.mode(WIFI_STA); + + scanIndex = 0; + connectionAttemptCounter = 0; + connectStartTimestamp = 0; + connectionFailedTimestamp = 0; + bool scanWiFiOnStart = gPrefsSettings.getBool("ScanWiFiOnStart", false); + if (scanWiFiOnStart) { + // perform a scan to find the strongest network with same ssid (e.g. for mesh/repeater networks) + WiFi.scanNetworks(true, false, true, 120); + wifiState = WIFI_STATE_SCAN_CONN; + } else { + // quick connect without additional scan + wifiState = WIFI_STATE_CONNECT_LAST; + } +} + +void handleWifiStateConnectLast() { + if (WiFi.status() == WL_CONNECTED) { + wifiState = WIFI_STATE_CONN_SUCCESS; + return; + } + + if (connectStartTimestamp > 0 && millis() - connectStartTimestamp < 3000) { + return; + } + + WiFi.disconnect(true, true); + WiFi.mode(WIFI_STA); + + // for speed, try to connect to last ssid first + String lastSSID = gPrefsSettings.getString("LAST_SSID"); + + std::optional lastSettings = std::nullopt; + + if (lastSSID) { + for (int i = 0; i < numKnownNetworks; i++) { + if (strncmp(knownNetworks[i].ssid, lastSSID.c_str(), 32) == 0) { + lastSettings = knownNetworks[i]; + break; + } } + } + + if (!lastSettings || connectionAttemptCounter > 1) { + // you can tweak passive/active mode and time per channel + // routers send a beacon msg every 100ms and passive mode with 120ms works well and is fastest here + WiFi.scanNetworks(true, false, true, 120); + + connectionAttemptCounter = 0; + connectStartTimestamp = 0; + wifiState = WIFI_STATE_SCAN_CONN; + return; + } + + connectStartTimestamp = millis(); + connectToKnownNetwork(lastSettings.value()); + connectionAttemptCounter++; +} - if (wifiAccessIncomplete) { - accessPointStart((char *) FPSTR(accessPointNetworkSSID), apIP, apNetmask); - wifiInit = false; - wifiConnectionTryInProgress = false; +void handleWifiStateScanConnect() { + // wait for scan results and handle them + + if (WiFi.status() == WL_CONNECTED) { + WiFi.scanDelete(); + wifiState = WIFI_STATE_CONN_SUCCESS; + return; + } + + int wifiScanCompleteResult = WiFi.scanComplete(); + + switch (wifiScanCompleteResult) { + case -1: + // scan not fin + return; + case -2: + // scan not triggered + wifiState = WIFI_STATE_CONN_FAILED; return; + case 0: + wifiState = WIFI_STATE_CONN_FAILED; + return; + } + + if (connectStartTimestamp == 0) { + for (int i = 0; i < wifiScanCompleteResult; ++i) { + Log_Printf(LOGLEVEL_NOTICE, wifiScanResult, WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.channel(i), WiFi.BSSIDstr(i).c_str()); } - _ssid = x_strdup(strSSID.c_str()); - _pwd = x_strdup(strPassword.c_str()); - - // The use of dynamic allocation is recommended to save memory and reduce resources usage. - // However, the dynamic performs slightly slower than the static allocation. - // Use static allocation if you want to have more performance and if your application is multi-tasking. - // Arduino 2.0.x only, outcomment to use static buffers. - //WiFi.useStaticBuffers(true); - - // set to station mode - WiFi.mode(WIFI_STA); - // Get (optional) hostname-configuration from NVS - hostname = gPrefsSettings.getString("Hostname", "-1"); - if (hostname.compareTo("-1")) { - //WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); - WiFi.setHostname(hostname.c_str()); - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredHostnameFromNvs), hostname.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } else { - Log_Println((char *) FPSTR(wifiHostnameNotSet), LOGLEVEL_INFO); + } else { + if (millis() - connectStartTimestamp < 5000) { + return; } + } - // Add configuration of static IP (if requested) - #ifdef STATIC_IP_ENABLE - snprintf(Log_Buffer, Log_BufferLength, "%s", (char *) FPSTR(tryStaticIpConfig)); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - if (!WiFi.config(IPAddress(LOCAL_IP), IPAddress(GATEWAY_IP), IPAddress(SUBNET_IP), IPAddress(DNS_IP))) { - snprintf(Log_Buffer, Log_BufferLength, "%s", (char *) FPSTR(staticIPConfigFailed)); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); + WiFi.disconnect(true, true); + WiFi.mode(WIFI_STA); + // fortunately, scan results are already sorted by best signal + for (int i = scanIndex; i < wifiScanCompleteResult; i++) { + // try to connect to wifi network with index i + String issid = WiFi.SSID(i); + byte *bssid = WiFi.BSSID(i); + // check if ssid name matches any saved ssid + for (int j = 0; j < numKnownNetworks; j++) { + if (strncmp(issid.c_str(), knownNetworks[j].ssid, 32) == 0) { + connectToKnownNetwork(knownNetworks[j], bssid); + + connectStartTimestamp = millis(); + + // prepare for next iteration + if (connectionAttemptCounter > 0) { + scanIndex = i + 1; + connectionAttemptCounter = 0; + } else { + scanIndex = i; + connectionAttemptCounter++; + } + + return; } - #endif - - // Try to join local WiFi. If not successful, an access-point is opened. - WiFi.begin(_ssid, _pwd); - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "Versuche mit WLAN '%s' zu verbinden...", _ssid); - #else - snprintf(Log_Buffer, Log_BufferLength, "Try to connect to WiFi with SSID '%s'...", _ssid); - #endif - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - wifiCheckLastTimestamp = millis(); - wifiConnectIteration = 1; // 1: First try; 2: Retry; 0: inactive - wifiInit = false; - wifiNeedsRestart = false; - } - - if (wifiConnectIteration > 0 && (millis() - wifiCheckLastTimestamp >= 500)) { - if (WiFi.status() != WL_CONNECTED && wifiConnectIteration == 1 && (millis() - wifiCheckLastTimestamp >= 4500)) { - snprintf(Log_Buffer, Log_BufferLength, "%s", (char *) FPSTR(cantConnectToWifi)); - Log_Println(Log_Buffer, LOGLEVEL_ERROR); - WiFi.disconnect(true, true); - WiFi.begin(_ssid, _pwd); // ESP32-workaround (otherwise WiFi-connection fails every 2nd time) - wifiConnectIteration = 2; - return; } + } + + WiFi.scanDelete(); + wifiState = WIFI_STATE_CONN_FAILED; +} + +// Callback function (get's called when time adjusts via NTP) +void ntpTimeAvailable(struct timeval *t) { + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + Log_Println(ntpFailed, LOGLEVEL_NOTICE); + return; + } + static char timeStringBuff[255]; + snprintf(timeStringBuff, sizeof(timeStringBuff), ntpGotTime, timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + Log_Println(timeStringBuff, LOGLEVEL_NOTICE); + // set ESPuino's very first start date + if (!gPrefsSettings.isKey("firstStart")) { + gPrefsSettings.putULong("firstStart", t->tv_sec); + } +} - if (WiFi.status() != WL_CONNECTED && wifiConnectIteration == 2 && (millis() - wifiCheckLastTimestamp >= 9000)) { - wifiConnectIteration = 0; - accessPointStart((char *) FPSTR(accessPointNetworkSSID), apIP, apNetmask); - wifiConnectionTryInProgress = false; - free(_ssid); - free(_pwd); +static bool initialStart = true; +// executed once after successfully connecting +void handleWifiStateConnectionSuccess() { + initialStart = false; + IPAddress myIP = WiFi.localIP(); + String mySSID = Wlan_GetCurrentSSID(); + + Log_Printf(LOGLEVEL_NOTICE, wifiConnectionSuccess, mySSID.c_str(), WiFi.RSSI(), WiFi.channel(), WiFi.BSSIDstr().c_str()); + Log_Printf(LOGLEVEL_NOTICE, wifiCurrentIp, myIP.toString().c_str()); + + if (!gPrefsSettings.getString("LAST_SSID").equals(mySSID)) { + Log_Printf(LOGLEVEL_INFO, wifiSetLastSSID, mySSID.c_str()); + gPrefsSettings.putString("LAST_SSID", mySSID); + } + + // get current time and date + Log_Println(syncingViaNtp, LOGLEVEL_NOTICE); + // Updates system time immediately upon receiving a response from the SNTP server + sntp_set_sync_mode(SNTP_SYNC_MODE_IMMED); + // set notification call-back function + sntp_set_time_sync_notification_cb(ntpTimeAvailable); + // start NTP request with timezone + configTzTime(timeZone, "de.pool.ntp.org", "0.pool.ntp.org", "ptbtime1.ptb.de"); +#ifdef MDNS_ENABLE + // zero conf, make device available as .local + if (MDNS.begin(hostname.c_str())) { + MDNS.addService("http", "tcp", 80); + Log_Printf(LOGLEVEL_NOTICE, mDNSStarted, hostname.c_str()); + } else { + Log_Printf(LOGLEVEL_ERROR, mDNSFailed, hostname.c_str()); + } +#endif + delete dnsServer; + dnsServer = nullptr; + +#ifdef PLAY_LAST_RFID_AFTER_REBOOT + if (gPlayLastRfIdWhenWiFiConnected && gTriedToConnectToHost) { + gPlayLastRfIdWhenWiFiConnected = false; + recoverLastRfidPlayedFromNvs(true); + } +#endif + + wifiState = WIFI_STATE_CONNECTED; +} + +static uint32_t lastPrintRssiTimestamp = 0; +static int8_t lastRssiValue = 0; +void handleWifiStateConnected() { + switch (WiFi.status()) { + case WL_CONNECTED: + break; + case WL_NO_SSID_AVAIL: + // is set if reconnect failed and network is not found + wifiState = WIFI_STATE_DISCONNECTED; + return; + case WL_DISCONNECTED: + // is set if reconnect failed for other reason + wifiState = WIFI_STATE_DISCONNECTED; return; + default: + break; + } + + if (millis() - lastPrintRssiTimestamp >= 60000) { + lastPrintRssiTimestamp = millis(); + // show RSSI value only if it has changed by > 3 dBm + if (abs(lastRssiValue - Wlan_GetRssi()) > 3) { + Log_Printf(LOGLEVEL_DEBUG, "RSSI: %d dBm", Wlan_GetRssi()); + lastRssiValue = Wlan_GetRssi(); } + } +} - if (WiFi.status() == WL_CONNECTED) { - wifiConnectionTryInProgress = false; - wifiConnectIteration = 0; - IPAddress myIP = WiFi.localIP(); - #if (LANGUAGE == DE) - snprintf(Log_Buffer, Log_BufferLength, "Aktuelle IP: %d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); - #else - snprintf(Log_Buffer, Log_BufferLength, "Current IP: %d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); - #endif - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - // get current time and date - Log_Println((char *) FPSTR(syncingViaNtp), LOGLEVEL_NOTICE); - // timezone: Berlin - long gmtOffset_sec = 3600; - int daylightOffset_sec = 3600; - configTime(gmtOffset_sec, daylightOffset_sec, "de.pool.ntp.org", "0.pool.ntp.org", "ptbtime1.ptb.de"); - #ifdef MDNS_ENABLE - // zero conf, make device available as .local - if (MDNS.begin(hostname.c_str())) { - MDNS.addService("http", "tcp", 80); - } - #endif - free(_ssid); - free(_pwd); +static uint32_t wifiAPStartedTimestamp = 0; +void handleWifiStateConnectionFailed() { + // good candidate for a user setting + static constexpr uint32_t wifiReconnectTimeout = 600000; + + if (connectionFailedTimestamp == 0) { + Log_Println(cantConnectToWifi, LOGLEVEL_INFO); + connectionFailedTimestamp = millis(); + } + + if (initialStart) { + initialStart = false; + accessPointStart(accessPointNetworkSSID, accessPointNetworkPassword, apIP, apNetmask); + wifiAPStartedTimestamp = millis(); + wifiState = WIFI_STATE_AP; + return; + } + + // every 600s, try connecting again + if (millis() - connectionFailedTimestamp > wifiReconnectTimeout) { + wifiState = WIFI_STATE_INIT; + return; + } +} + +void handleWifiStateAP() { + // good candidate for a user setting + static constexpr uint32_t closeWifiAPTimeout = 300000; + + // close the AP after the desired time has passed; set to 0 to keep on forever + if (closeWifiAPTimeout != 0 && millis() - wifiAPStartedTimestamp > closeWifiAPTimeout) { + WiFi.mode(WIFI_OFF); + wifiState = WIFI_STATE_DISCONNECTED; + return; + } + + dnsServer->processNextRequest(); +} + +void Wlan_Cyclic(void) { + switch (wifiState) { + case WIFI_STATE_INIT: + handleWifiStateInit(); + return; + case WIFI_STATE_CONNECT_LAST: + handleWifiStateConnectLast(); + return; + case WIFI_STATE_SCAN_CONN: + handleWifiStateScanConnect(); + return; + case WIFI_STATE_CONN_SUCCESS: + handleWifiStateConnectionSuccess(); + return; + case WIFI_STATE_CONNECTED: + handleWifiStateConnected(); + return; + case WIFI_STATE_DISCONNECTED: + wifiState = WIFI_STATE_INIT; + return; + case WIFI_STATE_CONN_FAILED: + handleWifiStateConnectionFailed(); + return; + case WIFI_STATE_AP: + handleWifiStateAP(); + return; + case WIFI_STATE_END: + WiFi.disconnect(true, true); + WiFi.mode(WIFI_OFF); + return; + } +} + +bool Wlan_ValidateHostname(String newHostname) { + size_t len = newHostname.length(); + const char *hostname = newHostname.c_str(); + + // validation: first char alphanumerical, then alphanumerical or '-', last char alphanumerical + // These rules are mainly for mDNS purposes, a "pretty" hostname could have far fewer restrictions + bool validated = true; + if (len < 2 || len > 32) { + validated = false; + } + + if (!isAlphaNumeric(hostname[0]) || !isAlphaNumeric(hostname[len - 1])) { + validated = false; + } + + for (int i = 0; i < len; i++) { + if (!isAlphaNumeric(hostname[i]) && hostname[i] != '-') { + validated = false; + break; } } - if (Wlan_IsConnected()) { - if (millis() - lastPrintRssiTimestamp >= 60000) { - lastPrintRssiTimestamp = millis(); - snprintf(Log_Buffer, Log_BufferLength, "RSSI: %d dBm", Wlan_GetRssi()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + return validated; +} + +bool Wlan_SetHostname(String newHostname) { + // hostname should just be applied after reboot + gPrefsSettings.putString("Hostname", newHostname); + // check if hostname is written + return (gPrefsSettings.getString("Hostname", "-1") == newHostname); +} + +bool Wlan_AddNetworkSettings(WiFiSettings settings) { + settings.ssid[32] = '\0'; + settings.password[64] = '\0'; + + for (uint8_t i = 0; i < numKnownNetworks; i++) { + if (strncmp(settings.ssid, knownNetworks[i].ssid, 32) == 0) { + Log_Printf(LOGLEVEL_NOTICE, wifiUpdateNetwork, settings.ssid); + knownNetworks[i] = settings; + gPrefsSettings.putBytes(nvsWiFiNetworksKey, knownNetworks, numKnownNetworks * sizeof(WiFiSettings)); + return true; } } + + if (numKnownNetworks >= maxSavedNetworks) { + Log_Println(wifiAddTooManyNetworks, LOGLEVEL_ERROR); + return false; + } + + Log_Printf(LOGLEVEL_NOTICE, wifiAddNetwork, settings.ssid); + + knownNetworks[numKnownNetworks] = settings; + numKnownNetworks += 1; + + gPrefsSettings.putBytes(nvsWiFiNetworksKey, knownNetworks, numKnownNetworks * sizeof(WiFiSettings)); + + return true; } -bool Wlan_ConnectionTryInProgress(void) { - return wifiConnectionTryInProgress; +uint8_t Wlan_NumSavedNetworks() { + return numKnownNetworks; } -void Wlan_ToggleEnable(void) { - writeWifiStatusToNVS(!getWifiEnableStatusFromNVS()); +void Wlan_GetSavedNetworks(std::function handler) { + for (uint8_t i = 0; i < numKnownNetworks; i++) { + handler(knownNetworks[i]); + } +} + +const String Wlan_GetCurrentSSID() { + return WiFi.SSID(); +} + +const String Wlan_GetHostname() { + return gPrefsSettings.getString("Hostname", "ESPuino"); +} + +bool Wlan_DeleteNetwork(String ssid) { + Log_Printf(LOGLEVEL_NOTICE, wifiDeleteNetwork, ssid.c_str()); + + for (uint8_t i = 0; i < numKnownNetworks; i++) { + if (strncmp(ssid.c_str(), knownNetworks[i].ssid, 32) == 0) { + + // delete and move all following elements to the left + std::copy(&knownNetworks[i + 1], &knownNetworks[numKnownNetworks], &knownNetworks[i]); + numKnownNetworks--; + + size_t new_length = numKnownNetworks * sizeof(WiFiSettings); + + if (new_length == 0) { + gPrefsSettings.remove(nvsWiFiNetworksKey); + } else { + gPrefsSettings.putBytes(nvsWiFiNetworksKey, knownNetworks, new_length); + } + + return true; + } + } + // ssid not found + return false; +} + +bool Wlan_ConnectionTryInProgress(void) { + return wifiState == WIFI_STATE_SCAN_CONN; } String Wlan_GetIpAddress(void) { @@ -184,19 +559,21 @@ int8_t Wlan_GetRssi(void) { } // Initialize soft access-point -void accessPointStart(const char *SSID, IPAddress ip, IPAddress netmask) { +void accessPointStart(const char *SSID, const char *password, IPAddress ip, IPAddress netmask) { WiFi.mode(WIFI_AP); WiFi.softAPConfig(ip, ip, netmask); - WiFi.softAP(SSID); + WiFi.softAP(SSID, (password != NULL && password[0] != '\0') ? password : NULL); delay(500); - Log_Println((char *) FPSTR(apReady), LOGLEVEL_NOTICE); - snprintf(Log_Buffer, Log_BufferLength, "IP-Adresse: %d.%d.%d.%d", apIP[0], apIP[1], apIP[2], apIP[3]); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Println(apReady, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, "IP-Adresse: %s", apIP.toString().c_str()); - Web_Init(); + if (!dnsServer) { + dnsServer = new DNSServer(); + } - accessPointStarted = true; + dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + dnsServer->start(DNS_PORT, "*", ip); } // Reads stored WiFi-status from NVS @@ -212,27 +589,29 @@ bool getWifiEnableStatusFromNVS(void) { return wifiStatus; } +void Wlan_ToggleEnable(void) { + writeWifiStatusToNVS(!wifiEnabled); +} + // Writes to NVS whether WiFi should be activated void writeWifiStatusToNVS(bool wifiStatus) { - if (!wifiStatus) { - if (gPrefsSettings.putUInt("enableWifi", 0)) { // disable - Log_Println((char *) FPSTR(wifiDisabledAfterRestart), LOGLEVEL_NOTICE); - if (gPlayProperties.isWebstream) { - AudioPlayer_TrackControlToQueueSender(STOP); - } - delay(300); - WiFi.mode(WIFI_OFF); - wifiEnabled = false; - } + wifiEnabled = wifiStatus; + + gPrefsSettings.putUInt("enableWifi", wifiEnabled ? 1 : 0); + + if (wifiEnabled) { + Log_Println(wifiEnabledMsg, LOGLEVEL_NOTICE); } else { - if (gPrefsSettings.putUInt("enableWifi", 1)) { // enable - Log_Println((char *) FPSTR(wifiEnabledAfterRestart), LOGLEVEL_NOTICE); - wifiNeedsRestart = true; - wifiEnabled = true; + Log_Println(wifiDisabledMsg, LOGLEVEL_NOTICE); + if (gPlayProperties.isWebstream) { + AudioPlayer_TrackControlToQueueSender(STOP); } } + + // go to init state again to handle new 'wifiEnabled' + wifiState = WIFI_STATE_INIT; } bool Wlan_IsConnected(void) { - return (WiFi.status() == WL_CONNECTED); + return (wifiState == WIFI_STATE_CONNECTED); } diff --git a/src/Wlan.h b/src/Wlan.h index a39d4899..9c38c49e 100644 --- a/src/Wlan.h +++ b/src/Wlan.h @@ -1,9 +1,31 @@ #pragma once +#include + +// be very careful changing this struct, as it is used for NVS storage and will corrupt existing entries +struct WiFiSettings { + char ssid[33]; + char password[65]; + bool use_static_ip; + uint32_t static_addr; + uint32_t static_gateway; + uint32_t static_subnet; + uint32_t static_dns1; + uint32_t static_dns2; +}; + void Wlan_Init(void); void Wlan_Cyclic(void); +bool Wlan_AddNetworkSettings(WiFiSettings); +uint8_t Wlan_NumSavedNetworks(); +void Wlan_GetSavedNetworks(std::function); +const String Wlan_GetCurrentSSID(); +const String Wlan_GetHostname(); +bool Wlan_DeleteNetwork(String); +bool Wlan_ValidateHostname(String); +bool Wlan_SetHostname(String); bool Wlan_IsConnected(void); -boolean Wlan_ToggleEnable(void); +void Wlan_ToggleEnable(void); String Wlan_GetIpAddress(void); int8_t Wlan_GetRssi(void); bool Wlan_ConnectionTryInProgress(void); diff --git a/src/logmessages.h b/src/logmessages.h index d6a54e5e..d92ae944 100644 --- a/src/logmessages.h +++ b/src/logmessages.h @@ -34,6 +34,7 @@ extern const char repeatTrackDueToPlaymode[]; extern const char repeatPlaylistDueToPlaymode[]; extern const char cmndStop[]; extern const char cmndPause[]; +extern const char cmndResumeFromPause[]; extern const char cmndNextTrack[]; extern const char cmndPrevTrack[]; extern const char cmndFirstTrack[]; @@ -71,7 +72,7 @@ extern const char modeAllTrackRandomLoop[]; extern const char modeWebstream[]; extern const char modeWebstreamM3u[]; extern const char webstreamNotAvailable[]; -extern const char modeDoesNotExist[]; +extern const char modeInvalid[]; extern const char modeRepeatNone[]; extern const char modeRepeatTrack[]; extern const char modeRepeatPlaylist[]; @@ -157,8 +158,8 @@ extern const char batteryTempMsg[]; extern const char batteryCyclesMsg[]; extern const char batteryLowMsg[]; extern const char batteryCriticalMsg[]; -extern const char wifiEnabledAfterRestart[]; -extern const char wifiDisabledAfterRestart[]; +extern const char wifiEnabledMsg[]; +extern const char wifiDisabledMsg[]; extern const char voltageIndicatorLowFromNVS[]; extern const char voltageIndicatorHighFromNVS[]; extern const char batteryCheckIntervalFromNVS[]; @@ -178,13 +179,13 @@ extern const char failedToOpenDirectory[]; extern const char notADirectory[]; extern const char sdMountedMmc1BitMode[]; extern const char sdMountedSpiMode[]; -extern const char backupRecoveryWebsite[]; extern const char restartWebsite[]; extern const char shutdownWebsite[]; extern const char mqttMsgReceived[]; extern const char trackPausedAtPos[]; extern const char freeHeapWithoutFtp[]; extern const char freeHeapWithFtp[]; +extern const char ftpServerStarted[]; extern const char freeHeapAfterSetup[]; extern const char tryStaticIpConfig[]; extern const char staticIPConfigFailed[]; @@ -198,17 +199,13 @@ extern const char portExpanderFound[]; extern const char portExpanderNotFound[]; extern const char portExpanderInterruptEnabled[]; extern const char warningRefactoring[]; -extern const char playlistGenModeUncached[]; -extern const char playlistGenModeCached[]; -extern const char playlistCacheFoundBut0[]; +extern const char playlistGen[]; extern const char bootLoopDetected[]; extern const char noBootLoopDetected[]; extern const char importCountNokNvs[]; extern const char errorReadingTmpfile[]; extern const char errorWritingTmpfile[]; -extern const char eraseRfidNvsWeb[]; extern const char eraseRfidNvs[]; -extern const char erasePlaylistCachefile[]; extern const char fwStart[]; extern const char fwEnd[]; extern const char otaNotSupported[]; @@ -217,15 +214,38 @@ extern const char noPlaylist[]; extern const char rfidTagRemoved[]; extern const char rfidTagReapplied[]; extern const char unableToTellIpAddress[]; +extern const char unableToTellTime[]; extern const char ftpEnableTooLate[]; +extern const char dateTimeRTC[]; extern const char syncingViaNtp[]; +extern const char ntpGotTime[]; +extern const char ntpFailed[]; extern const char sdInfo[]; extern const char paOn[]; extern const char paOff[]; extern const char hpOn[]; extern const char hpOff[]; extern const char webTxCanceled[]; -extern const char cantConnectToWifi[]; extern const char tryToPickRandomDir[]; extern const char pickedRandomDir[]; extern const char wrongWakeUpGpio[]; +extern const char currentlyPlaying[]; +extern const char secondsJumpForward[]; +extern const char secondsJumpBackward[]; +extern const char JumpToPosition[]; +extern const char wroteLastTrackToNvs[]; +extern const char wifiConnectionInProgress[]; +extern const char wifiConnectionSuccess[]; +extern const char wifiCurrentIp[]; +extern const char jsonErrorMsg[]; +extern const char wifiDeleteNetwork[]; +extern const char wifiNetworkLoaded[]; +extern const char wifiTooManyNetworks[]; +extern const char wifiAddTooManyNetworks[]; +extern const char wifiAddNetwork[]; +extern const char wifiUpdateNetwork[]; +extern const char wifiScanResult[]; +extern const char cantConnectToWifi[]; +extern const char wifiSetLastSSID[]; +extern const char mDNSStarted[]; +extern const char mDNSFailed[]; diff --git a/src/main.cpp b/src/main.cpp index eae11d89..b52d7fe8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,9 @@ // !!! MAKE SURE TO EDIT settings.h !!! #include -#include #include "settings.h" // Contains all user-relevant settings (general) +#include "main.h" + #include "AudioPlayer.h" #include "Battery.h" #include "Bluetooth.h" @@ -10,12 +11,14 @@ #include "Cmd.h" #include "Common.h" #include "Ftp.h" +#include "HallEffectSensor.h" #include "IrReceiver.h" #include "Led.h" #include "Log.h" -#include "Mqtt.h" #include "MemX.h" +#include "Mqtt.h" #include "Port.h" +#include "Power.h" #include "Queues.h" #include "Rfid.h" #include "RotaryEncoder.h" @@ -24,9 +27,22 @@ #include "Web.h" #include "Wlan.h" #include "revision.h" -#include "Power.h" -#include "HallEffectSensor.h" +#include + +bool gPlayLastRfIdWhenWiFiConnected = false; +bool gTriedToConnectToHost = false; + +static constexpr const char *logo = R"literal( + _____ ____ ____ _ +| ____| / ___| | _ \ _ _ (_) _ __ ___ +| _| \__ \ | |_) | | | | | | | | '_ \ / _ \ +| |___ ___) | | __/ | |_| | | | | | | | | (_) | +|_____| |____/ |_| \__,_| |_| |_| |_| \___/ + Rfid-controlled musicplayer + + +)literal"; // avoid PSRAM check while wake-up from deepsleep bool testSPIRAM(void) { @@ -34,102 +50,100 @@ bool testSPIRAM(void) { } #ifdef PLAY_LAST_RFID_AFTER_REBOOT - bool recoverLastRfid = true; - bool recoverBootCount = true; - bool resetBootCount = false; - uint32_t bootCount = 0; +bool recoverLastRfid = true; +bool recoverBootCount = true; +bool resetBootCount = false; +uint32_t bootCount = 0; #endif //////////// #if (HAL == 2) #include "AC101.h" - static TwoWire i2cBusOne = TwoWire(0); - static AC101 ac(&i2cBusOne); +static TwoWire i2cBusOne = TwoWire(0); +static AC101 ac(&i2cBusOne); #endif // I2C #ifdef I2C_2_ENABLE - TwoWire i2cBusTwo = TwoWire(1); +TwoWire i2cBusTwo = TwoWire(1); #endif #ifdef PLAY_LAST_RFID_AFTER_REBOOT - // If a problem occurs, remembering last rfid can lead into a boot loop that's hard to escape of. - // That reason for a mechanism is necessary to prevent this. - // At start of a boot, bootCount is incremented by one and after 30s decremented because - // uptime of 30s is considered as "successful boot". - void recoverBootCountFromNvs(void) { - if (recoverBootCount) { - recoverBootCount = false; - resetBootCount = true; - bootCount = gPrefsSettings.getUInt("bootCount", 999); - - if (bootCount == 999) { // first init - bootCount = 1; - gPrefsSettings.putUInt("bootCount", bootCount); - } else if (bootCount >= 3) { // considered being a bootloop => don't recover last rfid! - bootCount = 1; - gPrefsSettings.putUInt("bootCount", bootCount); - gPrefsSettings.putString("lastRfid", "-1"); // reset last rfid - Log_Println((char *) FPSTR(bootLoopDetected), LOGLEVEL_ERROR); - recoverLastRfid = false; - } else { // normal operation - gPrefsSettings.putUInt("bootCount", ++bootCount); - } - } - - if (resetBootCount && millis() >= 30000) { // reset bootcount - resetBootCount = false; - bootCount = 0; +// If a problem occurs, remembering last rfid can lead into a boot loop that's hard to escape of. +// That reason for a mechanism is necessary to prevent this. +// At start of a boot, bootCount is incremented by one and after 30s decremented because +// uptime of 30s is considered as "successful boot". +void recoverBootCountFromNvs(void) { + if (recoverBootCount) { + recoverBootCount = false; + resetBootCount = true; + bootCount = gPrefsSettings.getUInt("bootCount", 999); + + if (bootCount == 999) { // first init + bootCount = 1; + gPrefsSettings.putUInt("bootCount", bootCount); + } else if (bootCount >= 3) { // considered being a bootloop => don't recover last rfid! + bootCount = 1; gPrefsSettings.putUInt("bootCount", bootCount); - Log_Println((char *) FPSTR(noBootLoopDetected), LOGLEVEL_INFO); + gPrefsSettings.putString("lastRfid", "-1"); // reset last rfid + Log_Println(bootLoopDetected, LOGLEVEL_ERROR); + recoverLastRfid = false; + } else { // normal operation + gPrefsSettings.putUInt("bootCount", ++bootCount); } } - // Get last RFID-tag applied from NVS - void recoverLastRfidPlayedFromNvs(void) { - if (recoverLastRfid) { - if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { // Don't recover if BT-mode is desired - recoverLastRfid = false; - return; - } + if (resetBootCount && millis() >= 30000) { // reset bootcount + resetBootCount = false; + bootCount = 0; + gPrefsSettings.putUInt("bootCount", bootCount); + Log_Println(noBootLoopDetected, LOGLEVEL_INFO); + } +} + +// Get last RFID-tag applied from NVS +void recoverLastRfidPlayedFromNvs(bool force) { + if (recoverLastRfid || force) { + if (System_GetOperationMode() == OPMODE_BLUETOOTH_SINK) { // Don't recover if BT-mode is desired recoverLastRfid = false; - String lastRfidPlayed = gPrefsSettings.getString("lastRfid", "-1"); - if (!lastRfidPlayed.compareTo("-1")) { - Log_Println((char *) FPSTR(unableToRestoreLastRfidFromNVS), LOGLEVEL_INFO); - } else { - char *lastRfid = x_strdup(lastRfidPlayed.c_str()); - xQueueSend(gRfidCardQueue, lastRfid, 0); - snprintf(Log_Buffer, Log_BufferLength, "%s: %s", (char *) FPSTR(restoredLastRfidFromNVS), lastRfidPlayed.c_str()); - Log_Println(Log_Buffer, LOGLEVEL_INFO); - } + return; + } + recoverLastRfid = false; + String lastRfidPlayed = gPrefsSettings.getString("lastRfid", "-1"); + if (!lastRfidPlayed.compareTo("-1")) { + Log_Println(unableToRestoreLastRfidFromNVS, LOGLEVEL_INFO); + } else { + xQueueSend(gRfidCardQueue, lastRfidPlayed.c_str(), 0); + gPlayLastRfIdWhenWiFiConnected = !force; + Log_Printf(LOGLEVEL_INFO, restoredLastRfidFromNVS, lastRfidPlayed.c_str()); } } +} #endif - void setup() { Log_Init(); Queues_Init(); // Make sure all wakeups can be enabled *before* initializing RFID, which can enter sleep immediately - Button_Init(); // To preseed internal button-storage with values - #ifdef PN5180_ENABLE_LPCD - Rfid_Init(); - #endif + Button_Init(); // To preseed internal button-storage with values +#ifdef PN5180_ENABLE_LPCD + Rfid_Init(); +#endif System_Init(); - // Init 2nd i2c-bus if RC522 is used with i2c or if port-expander is enabled - #ifdef I2C_2_ENABLE - i2cBusTwo.begin(ext_IIC_DATA, ext_IIC_CLK); - delay(50); - Log_Println((char *) FPSTR(rfidScannerReady), LOGLEVEL_DEBUG); - #endif +// Init 2nd i2c-bus if RC522 is used with i2c or if port-expander is enabled +#ifdef I2C_2_ENABLE + i2cBusTwo.begin(ext_IIC_DATA, ext_IIC_CLK); + delay(50); + Log_Println(rfidScannerReady, LOGLEVEL_DEBUG); +#endif - #ifdef HALLEFFECT_SENSOR_ENABLE - gHallEffectSensor.init(); - #endif +#ifdef HALLEFFECT_SENSOR_ENABLE + gHallEffectSensor.init(); +#endif // Needs i2c first if port-expander is used Port_Init(); @@ -150,40 +164,32 @@ void setup() { Led_Init(); - // Only used for ESP32-A1S-Audiokit - #if (HAL == 2) - i2cBusOne.begin(IIC_DATA, IIC_CLK, 40000); +// Only used for ESP32-A1S-Audiokit +#if (HAL == 2) + i2cBusOne.begin(IIC_DATA, IIC_CLK, 40000); - while (not ac.begin()) { - Log_Println((char *) F("AC101 Failed!"), LOGLEVEL_ERROR); - delay(1000); - } - Log_Println((char *) F("AC101 via I2C - OK!"), LOGLEVEL_NOTICE); + while (not ac.begin()) { + Log_Println("AC101 Failed!", LOGLEVEL_ERROR); + delay(1000); + } + Log_Println("AC101 via I2C - OK!", LOGLEVEL_NOTICE); - pinMode(22, OUTPUT); - digitalWrite(22, HIGH); - ac.SetVolumeHeadphone(80); - #endif + pinMode(22, OUTPUT); + digitalWrite(22, HIGH); + ac.SetVolumeHeadphone(80); +#endif // Needs power first SdCard_Init(); // welcome message - Serial.println(F("")); - Serial.println(F(" _____ ____ ____ _ ")); - Serial.println(F(" | ____| / ___| | _ \\ _ _ (_) _ __ ___ ")); - Serial.println(F(" | _| \\__ \\ | |_) | | | | | | | | '_ \\ / _ \\")); - Serial.println(F(" | |___ ___) | | __/ | |_| | | | | | | | | (_) |")); - Serial.println(F(" |_____| |____/ |_| \\__,_| |_| |_| |_| \\___/ ")); - Serial.print(F(" Rfid-controlled musicplayer\n\n")); + Serial.print(logo); // Software-version - snprintf(Log_Buffer, Log_BufferLength, "%s", softwareRevision); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - snprintf(Log_Buffer, Log_BufferLength, "%s", gitRevision); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); - snprintf(Log_Buffer, Log_BufferLength, "ESP-IDF version: %s", (char *) (ESP.getSdkVersion())); - Log_Println(Log_Buffer, LOGLEVEL_NOTICE); + Log_Println(softwareRevision, LOGLEVEL_NOTICE); + Log_Println(gitRevision, LOGLEVEL_NOTICE); + Log_Printf(LOGLEVEL_NOTICE, "Arduino version: %d.%d.%d", ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH); + Log_Printf(LOGLEVEL_NOTICE, "ESP-IDF version: %s", ESP.getSdkVersion()); // print wake-up reason System_ShowWakeUpReason(); @@ -192,11 +198,11 @@ void setup() { Ftp_Init(); Mqtt_Init(); - #ifndef PN5180_ENABLE_LPCD - #if defined (RFID_READER_TYPE_MFRC522_SPI) || defined (RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_PN5180) - Rfid_Init(); - #endif +#ifndef PN5180_ENABLE_LPCD + #if defined(RFID_READER_TYPE_MFRC522_SPI) || defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_PN5180) + Rfid_Init(); #endif +#endif RotaryEncoder_Init(); Wlan_Init(); Bluetooth_Init(); @@ -209,17 +215,28 @@ void setup() { System_UpdateActivityTimer(); // initial set after boot Led_Indicate(LedIndicatorType::BootComplete); - snprintf(Log_Buffer, Log_BufferLength, "%s: %u", (char *) FPSTR(freeHeapAfterSetup), ESP.getFreeHeap()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - snprintf(Log_Buffer, Log_BufferLength, "PSRAM: %u bytes", ESP.getPsramSize()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); - snprintf(Log_Buffer, Log_BufferLength, "Flash-size: %u bytes", ESP.getFlashChipSize()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + Log_Printf(LOGLEVEL_DEBUG, "%s: %u", freeHeapAfterSetup, ESP.getFreeHeap()); + if (psramFound()) { + Log_Printf(LOGLEVEL_DEBUG, "PSRAM: %u bytes", ESP.getPsramSize()); + } else { + Log_Println("PSRAM: --", LOGLEVEL_DEBUG); + } + Log_Printf(LOGLEVEL_DEBUG, "Flash-size: %u bytes", ESP.getFlashChipSize()); + + // setup timezone & show internal RTC date/time if available + setenv("TZ", timeZone, 1); + tzset(); + struct tm timeinfo; + if (getLocalTime(&timeinfo, 5)) { + static char timeStringBuff[255]; + snprintf(timeStringBuff, sizeof(timeStringBuff), dateTimeRTC, timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); + Log_Println(timeStringBuff, LOGLEVEL_DEBUG); + } + if (Wlan_IsConnected()) { - snprintf(Log_Buffer, Log_BufferLength, "RSSI: %d dBm", Wlan_GetRssi()); - Log_Println(Log_Buffer, LOGLEVEL_DEBUG); + Log_Printf(LOGLEVEL_DEBUG, "RSSI: %d dBm", Wlan_GetRssi()); } - System_ShowUpgradeWarning(); + #ifdef CONTROLS_LOCKED_BY_DEFAULT System_SetLockControls(true); #endif @@ -229,8 +246,7 @@ void loop() { if (OPMODE_BLUETOOTH_SINK == System_GetOperationMode()) { // bluetooth speaker mode Bluetooth_Cyclic(); - } else - if (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode()) { + } else if (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode()) { // bluetooth headset mode Bluetooth_Cyclic(); RotaryEncoder_Cyclic(); @@ -245,21 +261,20 @@ void loop() { AudioPlayer_Cyclic(); Battery_Cyclic(); - //Port_Cyclic(); // called by button (controlled via hw-timer) + // Port_Cyclic(); // called by button (controlled via hw-timer) Button_Cyclic(); System_Cyclic(); Rfid_PreferenceLookupHandler(); - #ifdef PLAY_LAST_RFID_AFTER_REBOOT - recoverBootCountFromNvs(); - recoverLastRfidPlayedFromNvs(); - #endif +#ifdef PLAY_LAST_RFID_AFTER_REBOOT + recoverBootCountFromNvs(); + recoverLastRfidPlayedFromNvs(); +#endif IrReceiver_Cyclic(); - vTaskDelay(portTICK_RATE_MS * 5u); + vTaskDelay(portTICK_PERIOD_MS * 5u); - #ifdef HALLEFFECT_SENSOR_ENABLE - gHallEffectSensor.cyclic(); - #endif - +#ifdef HALLEFFECT_SENSOR_ENABLE + gHallEffectSensor.cyclic(); +#endif } diff --git a/src/main.h b/src/main.h new file mode 100644 index 00000000..08092fb1 --- /dev/null +++ b/src/main.h @@ -0,0 +1,8 @@ +#pragma once + +extern bool gPlayLastRfIdWhenWiFiConnected; +extern bool gTriedToConnectToHost; + +#ifdef PLAY_LAST_RFID_AFTER_REBOOT +extern void recoverLastRfidPlayedFromNvs(bool force = false); +#endif diff --git a/src/revision.h b/src/revision.h index 429d4376..e1f4d56e 100644 --- a/src/revision.h +++ b/src/revision.h @@ -1,5 +1,4 @@ #pragma once #include "gitrevision.h" - -constexpr const char softwareRevision[] PROGMEM = "Software-revision: 20230122-1-szenglein"; +constexpr const char softwareRevision[] = "Software-revision: 20231202-0-szenglein"; diff --git a/src/settings-azdelivery_sdmmc.h b/src/settings-azdelivery_sdmmc.h index 1cd08ff1..181678c2 100644 --- a/src/settings-azdelivery_sdmmc.h +++ b/src/settings-azdelivery_sdmmc.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_CUSTOM_H__ #define __ESPUINO_SETTINGS_CUSTOM_H__ #include "Arduino.h" diff --git a/src/settings-complete.h b/src/settings-complete.h index daf6d84e..9a43db4f 100644 --- a/src/settings-complete.h +++ b/src/settings-complete.h @@ -1,137 +1,141 @@ -#include "Arduino.h" - -//######################### INFOS #################################### -/* This is a config-file for ESPuino complete with port-expander PCA9555. - PCB: tba - Infos: Uses ESP32-WROVER, port-expander PCA9555, amp + headphone-amp integrated - Caveats: None - Status: Test in progress... -*/ - -//################## GPIO-configuration ############################## -#ifdef SD_MMC_1BIT_MODE - // uSD-card-reader (via SD-MMC 1Bit) - // - // SD_MMC uses fixed pins - // (MOSI) 15 CMD - // (SCK) 14 SCK - // (MISO) 2 D0 -#else - // SPI-SD IS NOT SUPPORTED BY THIS PCB - DON'T USE INTERNAL SD-READER! - #define SPISD_CS 99 // GPIO for chip select (SD) - #ifndef SINGLE_SPI_ENABLE - #define SPISD_MOSI 99 // GPIO for master out slave in (SD) => not necessary for single-SPI - #define SPISD_MISO 99 // GPIO for master in slave ou (SD) => not necessary for single-SPI - #define SPISD_SCK 99 // GPIO for clock-signal (SD) => not necessary for single-SPI - #endif -#endif - -// RFID (via SPI) -#define RST_PIN 99 // Not necessary for RC522 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) - -#ifdef RFID_READER_TYPE_PN5180 - #define RFID_BUSY 32 // PN5180 BUSY PIN - #define RFID_RST 22 // PN5180 RESET PIN - #define RFID_IRQ 99 // PN5180 IRQ PIN (only needed for low power card detection) -#endif -// I2S (DAC) -#define I2S_DOUT 25 // Digital out (I2S) -#define I2S_BCLK 27 // BCLK (I2S) -#define I2S_LRC 26 // LRC (I2S) - -// Rotary encoder -#ifdef USEROTARY_ENABLE - //#define REVERSE_ROTARY // To reverse encoder's direction; switching CLK / DT in hardware does the same - #define ROTARYENCODER_CLK 35 // rotary encoder's CLK - #define ROTARYENCODER_DT 34 // rotary encoder's DT -#endif - -// Amp enable (optional) -#define GPIO_PA_EN 112 // To enable amp for loudspeaker (GPIO or port-channel) -#define GPIO_HP_EN 113 // To enable amp for headphones (GPIO or port-channel) - -// Control-buttons (set to 99 to DISABLE; 0->39 for GPIO; 100->115 for port-expander) -#define NEXT_BUTTON 102 // Button 0: GPIO to detect next -#define PREVIOUS_BUTTON 103 // Button 1: GPIO to detect previous -#define PAUSEPLAY_BUTTON 100 // Button 2: GPIO to detect pause/play -#define ROTARYENCODER_BUTTON 105 // rotary encoder's button -#define BUTTON_4 104 // Button 4: unnamed optional button -#define BUTTON_5 101 // Button 5: unnamed optional button - -//#define BUTTONS_LED 114 // Powers the LEDs of the buttons. Make sure the current consumed by the LEDs can be handled by the used GPIO - -// Channels of port-expander can be read cyclic or interrupt-driven. It's strongly recommended to use the interrupt-way! -// Infos: https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306 -#ifdef PORT_EXPANDER_ENABLE - #define PE_INTERRUPT_PIN 4 // GPIO that is used to receive interrupts from port-expander -#endif - -// I2C-configuration (necessary for RC522 [only via i2c - not spi!] or port-expander) -#ifdef I2C_2_ENABLE - #define ext_IIC_CLK 33 // i2c-SCL (clock) - #define ext_IIC_DATA 5 // i2c-SDA (data) -#endif - -// Wake-up button => this also is the interrupt-pin if port-expander is enabled! -// Please note: only RTC-GPIOs (0, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 39, 99) can be used! Set to 99 to DISABLE. -// Please note #2: this button can be used as interrupt-pin for port-expander. If so, all pins connected to port-expander can wake up ESPuino. -#define WAKEUP_BUTTON ROTARYENCODER_BUTTON // Defines the button that is used to wake up ESPuino from deepsleep. - -// (optional) Power-control -#define POWER 13 // GPIO used to drive transistor-circuit, that switches off peripheral devices while ESP32-deepsleep -#ifdef POWER - //#define INVERT_POWER // If enabled, use inverted logic for POWER circuit, that means peripherals are turned off by writing HIGH -#endif - -// (optional) Neopixel -#define LED_PIN 12 // GPIO for Neopixel-signaling - -// (optinal) Headphone-detection -#ifdef HEADPHONE_ADJUST_ENABLE - #define DETECT_HP_ON_HIGH // Per default headphones are supposed to be connected if HT_DETECT is LOW. DETECT_HP_ON_HIGH will change this behaviour to HIGH. - #define HP_DETECT 107 // 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 39 // GPIO used to monitor battery-voltage. - constexpr float referenceVoltage = 3.3; // Reference-voltage - constexpr float offsetVoltage = 0.55; // If voltage measured by ESP isn't 100% accurate, you can add a correction-value here -#endif - -// (optional) For measuring battery-voltage a voltage-divider is necessary. Their values need to be configured here. -#ifdef MEASURE_BATTERY_VOLTAGE - constexpr uint16_t rdiv1 = 100; // Rdiv1 of voltage-divider (kOhms) - constexpr uint16_t rdiv2 = 33; // Rdiv2 of voltage-divider (kOhms) => used to measure voltage via ADC! -#endif - -// (optional) hallsensor. Make sure the GPIO defined doesn't overlap with existing configuration. Please note: only user-support is provided for this feature. -#ifdef HALLEFFECT_SENSOR_ENABLE - #define HallEffectSensor_PIN 34 // GPIO that is used for hallsensor (ADC); user-support: https://forum.espuino.de/t/magnetische-hockey-tags/1449/35 -#endif - -// (Optional) remote control via infrared -#ifdef IR_CONTROL_ENABLE - #define IRLED_PIN 0 // GPIO where IR-receiver is connected (only tested with VS1838B) - #define IR_DEBOUNCE 200 // Interval in ms to wait at least for next signal (not used for actions volume up/down) - - // Actions available. Use your own remote control and have a look at the console for "Command=0x??". E.g. "Protocol=NEC Address=0x17F Command=0x68 Repeat gap=39750us" - // Make sure to define a hex-code not more than once as this will lead to a compile-error - // https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265 - #define RC_PLAY 0x68 // command for play - #define RC_PAUSE 0x67 // command for pause - #define RC_NEXT 0x6b // command for next track of playlist - #define RC_PREVIOUS 0x6a // command for previous track of playlist - #define RC_FIRST 0x6c // command for first track of playlist - #define RC_LAST 0x6d // command for last track of playlist - #define RC_VOL_UP 0x1a // Command for volume up (one step) - #define RC_VOL_DOWN 0x1b // Command for volume down (one step) - #define RC_MUTE 0x1c // Command to mute ESPuino - #define RC_SHUTDOWN 0x2a // Command for deepsleep - #define RC_BLUETOOTH 0x72 // Command to enable/disable bluetooth - #define RC_FTP 0x65 // Command to enable FTP-server +// clang-format off + +#ifndef __ESPUINO_COMPLETE + #define __ESPUINO_COMPLETE + #include "Arduino.h" + + //######################### INFOS #################################### + /* This is a config-file for ESPuino complete with ESP32-WROVER-E. + PCB: tba + Infos: Uses ESP32-WROVER, boost-buck-converter, port-expander, 2x MAX98357a, MS6324 + TDA1308 (headphone), TP5000 (LiPo + LiFePO4-support) + Caveats: Don't forget to verify polarity of your battery-connector. It needs to fit the polarity printed on the PCB! + Settings: Make sure to enable at least PORT_EXPANDER_ENABLE. PLAY_MONO_SPEAKER should be disabled. + Status: Test in progress... + */ + + //################## GPIO-configuration ############################## + #ifdef SD_MMC_1BIT_MODE + // uSD-card-reader (via SD-MMC 1Bit) + // + // SD_MMC uses fixed pins + // (MOSI) 15 CMD + // (SCK) 14 SCK + // (MISO) 2 D0 + #else + // SPI-SD IS NOT SUPPORTED BY THIS PCB + #define SPISD_CS 99 // GPIO for chip select (SD) + #ifndef SINGLE_SPI_ENABLE + #define SPISD_MOSI 99 // GPIO for master out slave in (SD) => not necessary for single-SPI + #define SPISD_MISO 99 // GPIO for master in slave ou (SD) => not necessary for single-SPI + #define SPISD_SCK 99 // GPIO for clock-signal (SD) => not necessary for single-SPI + #endif + #endif + + // RFID (via SPI) + #define RST_PIN 99 // Not necessary for RC522 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) + + // RFID (PN5180 only; not necessary for RC522) + #ifdef RFID_READER_TYPE_PN5180 + #define RFID_BUSY 33 // PN5180 BUSY PIN + #define RFID_RST 22 // PN5180 RESET PIN + #define RFID_IRQ 32 // PN5180 IRQ PIN (only needed for low power card detection) + #endif + + // I2S (DAC) + #define I2S_DOUT 25 // Digital out (I2S) + #define I2S_BCLK 27 // BCLK (I2S) + #define I2S_LRC 26 // LRC (I2S) + + // Rotary encoder + #ifdef USEROTARY_ENABLE + //#define REVERSE_ROTARY // To reverse encoder's direction; switching CLK / DT in hardware does the same + #define ROTARYENCODER_CLK 34 // rotary encoder's CLK + #define ROTARYENCODER_DT 39 // rotary encoder's DT + #endif + + // Amp enable + #define GPIO_PA_EN 113 // To enable amp for loudspeaker (GPIO or port-channel) + //#define GPIO_HP_EN 112 // To enable amp for headphones (GPIO or port-channel) + + // Control-buttons + #define NEXT_BUTTON 102 // Button 0: GPIO to detect next + #define PREVIOUS_BUTTON 100 // Button 1: GPIO to detect previous + #define PAUSEPLAY_BUTTON 101 // Button 2: GPIO to detect pause/play + #define ROTARYENCODER_BUTTON 105 // rotary encoder's button + #define BUTTON_4 103 // Button 4: unnamed optional button + #define BUTTON_5 104 // Button 5: unnamed optional button + + //#define BUTTONS_LED 109 // Powers the LEDs of the buttons. (ext.7) + + // Channels of port-expander can be read cyclic or interrupt-driven. It's strongly recommended to use the interrupt-way! + // Infos: https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306 + #ifdef PORT_EXPANDER_ENABLE + #define PE_INTERRUPT_PIN 36 // GPIO that is used to receive interrupts from port-expander + #endif + + // I2C-configuration (necessary at least for port-expander - don't change!) + #ifdef I2C_2_ENABLE + #define ext_IIC_CLK 4 // i2c-SCL (clock) + #define ext_IIC_DATA 13 // i2c-SDA (data) + #endif + + // Wake-up button => this also is the interrupt-pin if port-expander is enabled! + // Please note: only RTC-GPIOs (0, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 39, 99) can be used! Set to 99 to DISABLE. + // Please note #2: this button can be used as interrupt-pin for port-expander. If so, all pins connected to port-expander can wake up ESPuino. + #define WAKEUP_BUTTON 36 // Defines the button that is used to wake up ESPuino from deepsleep. + + // (optional) Power-control + #define POWER 114 // GPIO used to drive transistor-circuit, that switches off peripheral devices while ESP32-deepsleep + #ifdef POWER + #define INVERT_POWER // If enabled, use inverted logic for POWER circuit, that means peripherals are turned off by writing HIGH + #endif + + // (optional) Neopixel + #define LED_PIN 12 // GPIO for Neopixel-signaling + + // Headphone-detection + #ifdef HEADPHONE_ADJUST_ENABLE + //#define DETECT_HP_ON_HIGH // Per default headphones are supposed to be connected if HT_DETECT is LOW. DETECT_HP_ON_HIGH will change this behaviour to HIGH. + #define HP_DETECT 108 // 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 35 // GPIO used to monitor battery-voltage. + constexpr float referenceVoltage = 3.345; // Reference-voltage (provided by dc-dc-converter) + constexpr float offsetVoltage = 0.25; // If voltage measured by ESP isn't 100% accurate, you can add a correction-value here + constexpr uint16_t rdiv1 = 300; // Rdiv1 of voltage-divider (kOhms) + constexpr uint16_t rdiv2 = 300; // Rdiv2 of voltage-divider (kOhms) => used to measure voltage via ADC! + #endif + + // (optional) hallsensor. Make sure the GPIO defined doesn't overlap with existing configuration. Please note: only user-support is provided for this feature. + #ifdef HALLEFFECT_SENSOR_ENABLE + #define HallEffectSensor_PIN 32 // GPIO that is used for hallsensor (ADC); user-support: https://forum.espuino.de/t/magnetische-hockey-tags/1449/35 + #endif + + // (Optional) remote control via infrared + #ifdef IR_CONTROL_ENABLE + #define IRLED_PIN 5 // GPIO where IR-receiver is connected (only tested with VS1838B) + #define IR_DEBOUNCE 200 // Interval in ms to wait at least for next signal (not used for actions volume up/down) + + // Actions available. Use your own remote control and have a look at the console for "Command=0x??". E.g. "Protocol=NEC Address=0x17F Command=0x68 Repeat gap=39750us" + // Make sure to define a hex-code not more than once as this will lead to a compile-error + // https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265 + #define RC_PLAY 0x68 // command for play + #define RC_PAUSE 0x67 // command for pause + #define RC_NEXT 0x6b // command for next track of playlist + #define RC_PREVIOUS 0x6a // command for previous track of playlist + #define RC_FIRST 0x6c // command for first track of playlist + #define RC_LAST 0x6d // command for last track of playlist + #define RC_VOL_UP 0x1a // Command for volume up (one step) + #define RC_VOL_DOWN 0x1b // Command for volume down (one step) + #define RC_MUTE 0x1c // Command to mute ESPuino + #define RC_SHUTDOWN 0x2a // Command for deepsleep + #define RC_BLUETOOTH 0x72 // Command to enable/disable bluetooth + #define RC_FTP 0x65 // Command to enable FTP-server + #endif #endif diff --git a/src/settings-custom.h b/src/settings-custom.h index a9cc565e..c1703e7b 100644 --- a/src/settings-custom.h +++ b/src/settings-custom.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_CUSTOM_H__ #define __ESPUINO_SETTINGS_CUSTOM_H__ #include "Arduino.h" diff --git a/src/settings-espa1s.h b/src/settings-espa1s.h index 55a23759..6e3f0edc 100644 --- a/src/settings-espa1s.h +++ b/src/settings-espa1s.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_ESPA1S_H__ #define __ESPUINO_SETTINGS_ESPA1S_H__ #include "Arduino.h" diff --git a/src/settings-lolin32.h b/src/settings-lolin32.h index f3c243ab..a0bb668f 100644 --- a/src/settings-lolin32.h +++ b/src/settings-lolin32.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_LOLIN32_H__ #define __ESPUINO_SETTINGS_LOLIN32_H__ #include "Arduino.h" diff --git a/src/settings-lolin_d32.h b/src/settings-lolin_d32.h index 4dcd6245..e108ba14 100644 --- a/src/settings-lolin_d32.h +++ b/src/settings-lolin_d32.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_LOLIN_D32_H__ #define __ESPUINO_SETTINGS_LOLIN_D32_H__ #include "Arduino.h" diff --git a/src/settings-lolin_d32_pro.h b/src/settings-lolin_d32_pro.h index b2df515e..e9d12f3e 100644 --- a/src/settings-lolin_d32_pro.h +++ b/src/settings-lolin_d32_pro.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_LOLIN_D32_PRO_H__ #define __ESPUINO_SETTINGS_LOLIN_D32_PRO_H__ #include "Arduino.h" diff --git a/src/settings-lolin_d32_pro_sdmmc_pe.h b/src/settings-lolin_d32_pro_sdmmc_pe.h index 9a1721de..ba5cd46f 100644 --- a/src/settings-lolin_d32_pro_sdmmc_pe.h +++ b/src/settings-lolin_d32_pro_sdmmc_pe.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_LOLIN_D32_PRO_H__ #define __ESPUINO_SETTINGS_LOLIN_D32_PRO_H__ #include "Arduino.h" diff --git a/src/settings-lolin_d32_sdmmc_pe.h b/src/settings-lolin_d32_sdmmc_pe.h index ff5b1eb5..5f22e57e 100644 --- a/src/settings-lolin_d32_sdmmc_pe.h +++ b/src/settings-lolin_d32_sdmmc_pe.h @@ -1,3 +1,5 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_LOLIN_D32_H__ #define __ESPUINO_SETTINGS_LOLIN_D32_H__ #include "Arduino.h" diff --git a/src/settings-muse_proto.h b/src/settings-muse_proto.h deleted file mode 100644 index 9f66933e..00000000 --- a/src/settings-muse_proto.h +++ /dev/null @@ -1,159 +0,0 @@ -#ifndef __ESPUINO_SETTINGS_MUSEPROTO_H__ -#define __ESPUINO_SETTINGS_MUSEPROTO_H__ - #include "Arduino.h" - - //######################### INFOS #################################### - /* - * Make sure to read instructions: - * https://forum.espuino.de/t/anleitung-raspiaudio-muse-proto-anfaenger-espuino/1598 - * - * Support for this device is limited, and only available by the community in the - * forum. (See thread above) - * - * This is a device-specific settings file for the RASPIAUDIO Muse Proto - * offered by https://raspiaudio.com/produit/muse-proto and available at - * multiple sources. It uses - * - * * On-Board Neopixel (1 Pixel) - * * PN532 NFC Reader, using I2C, GPIOs 12 (CLK) and 32 (DATA) - * * KY-040 Rotary Encoder with on-board pull ups on GPIOs 36, 39 (Printed VP/VN) - * * Three buttons without pull ups on GPIOs 18, 19, 0 - * * Batteries measured using the on-board battery controller - * * Using the on-board 3W output for externals speakers - * - */ - - //################## GPIO-configuration ############################## - // Please note: GPIOs 34, 35, 36, 39 are input-only and don't have pullup-resistors. - // So if connecting a button to these, make sure to add a 10k-pullup-resistor for each button. - // Further infos: https://randomnerdtutorials.com/esp32-pinout-reference-gpios/ - #ifdef SD_MMC_1BIT_MODE - // uSD-card-reader (via SD-MMC 1Bit) - // - // SD_MMC uses fixed pins - // (MOSI) 15 CMD - // (SCK) 14 SCK - // (MISO) 2 D0 - #else - // 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 - #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 99 // GPIO for chip select (RFID) - #define RFID_MOSI 99 // GPIO for master out slave in (RFID) - #define RFID_MISO 99 // GPIO for master in slave out (RFID) - #define RFID_SCK 99 // GPIO for clock-signal (RFID) - - // RFID (via I2C) - #if defined(RFID_READER_TYPE_MFRC522_I2C) - #define MFRC522_RST_PIN 4 // needed for initialisation -> MTDI on JTAG header - #endif - - #ifdef RFID_READER_TYPE_PN5180 - #define RFID_BUSY 16 // PN5180 BUSY PIN - #define RFID_RST 22 // PN5180 RESET PIN - #define RFID_IRQ 39 // PN5180 IRQ PIN (only needed for low power card detection) - #endif - - // I2S (DAC) - #define I2S_DOUT 26 // Digital out (I2S) - #define I2S_BCLK 5 // BCLK (I2S) - #define I2S_LRC 25 // LRC (I2S) - - // Rotary encoder - #ifdef USEROTARY_ENABLE - //#define REVERSE_ROTARY // To reverse encoder's direction; switching CLK / DT in hardware does the same - #define ROTARYENCODER_CLK 36 // SENSOR-VP - #define ROTARYENCODER_DT 39 // SENSOR_VN - #endif - - // Amp enable (optional) - #define GPIO_PA_EN 21 // To enable amp for loudspeaker (GPIO or port-channel) - - // Control-buttons (set to 99 to DISABLE; 0->39 for GPIO; 100->115 for port-expander) - #define NEXT_BUTTON 18 // Button 0: GPIO to detect next - #define PREVIOUS_BUTTON 19 // Button 1: GPIO to detect previous (Important: as of 19.11.2020 changed from 33 to 2; make sure to change in SD-MMC-mode) - #define PAUSEPLAY_BUTTON 0 // Button 2: GPIO to detect pause/play - #define ROTARYENCODER_BUTTON 99 // (set to 99 to disable; 0->39 for GPIO; 100->115 for port-expander) - #define BUTTON_4 99 // Button 4: unnamed optional button - #define BUTTON_5 99 // Button 5: unnamed optional button - - // Channels of port-expander can be read cyclic or interrupt-driven. It's strongly recommended to use the interrupt-way! - // Infos: https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306 - #ifdef PORT_EXPANDER_ENABLE - #define PE_INTERRUPT_PIN 99 // GPIO that is used to receive interrupts from port-expander - #endif - - // I2C-configuration (necessary for RC522 or RC532 [only via i2c - not spi!] or port-expander) - #ifdef I2C_2_ENABLE - #define ext_IIC_CLK 12 // i2c-SCL (clock) - #define ext_IIC_DATA 32 // i2c-SDA (data) - #endif - - // Wake-up button => this also is the interrupt-pin if port-expander is enabled! - // Please note: only RTC-GPIOs (0, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 39, 99) can be used! Set to 99 to DISABLE. - // Please note #2: this button can be used as interrupt-pin for port-expander. If so, all pins connected to port-expander can wake up ESPuino. - #define WAKEUP_BUTTON PAUSEPLAY_BUTTON // Defines the button that is used to wake up ESPuino from deepsleep. - - // (optional) Power-control - #define POWER 17 // GPIO used to drive transistor-circuit, that switches off peripheral devices while ESP32-deepsleep - #ifdef POWER - //#define INVERT_POWER // If enabled, use inverted logic for POWER circuit, that means peripherals are turned off by writing HIGH - #endif - - // (optional) Neopixel - #define LED_PIN 22 // GPIO for Neopixel-signaling - - // (optinal) Headphone-detection - #ifdef HEADPHONE_ADJUST_ENABLE - //#define DETECT_HP_ON_HIGH // Per default headphones are supposed to be connected if HT_DETECT is LOW. DETECT_HP_ON_HIGH will change this behaviour to HIGH. - #define HP_DETECT 27 // 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! - constexpr float referenceVoltage = 3.35; // Voltage between 3.3V and GND-pin at the develboard in battery-mode (disconnect USB!) - constexpr float offsetVoltage = 0.1; // If voltage measured by ESP isn't 100% accurate, you can add an correction-value here - #endif - - // (optional) For measuring battery-voltage a voltage-divider is necessary. Their values need to be configured here. - #ifdef MEASURE_BATTERY_VOLTAGE - constexpr uint16_t rdiv1 = 129; // Rdiv1 of voltage-divider (kOhms) (measure exact value with multimeter!) - constexpr uint16_t rdiv2 = 129; // Rdiv2 of voltage-divider (kOhms) (measure exact value with multimeter!) => used to measure voltage via ADC! - #endif - - // (optional) hallsensor. Make sure the GPIO defined doesn't overlap with existing configuration. Please note: only user-support is provided for this feature. - #ifdef HALLEFFECT_SENSOR_ENABLE - #define HallEffectSensor_PIN 34 // GPIO that is used for hallsensor (ADC); user-support: https://forum.espuino.de/t/magnetische-hockey-tags/1449/35 - #endif - - // (Optional) remote control via infrared - #ifdef IR_CONTROL_ENABLE - #define IRLED_PIN 22 // GPIO where IR-receiver is connected (only tested with VS1838B) - #define IR_DEBOUNCE 200 // Interval in ms to wait at least for next signal (not used for actions volume up/down) - - // Actions available. Use your own remote control and have a look at the console for "Command=0x??". E.g. "Protocol=NEC Address=0x17F Command=0x68 Repeat gap=39750us" - // Make sure to define a hex-code not more than once as this will lead to a compile-error - // https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265 - #define RC_PLAY 0x68 // command for play - #define RC_PAUSE 0x67 // command for pause - #define RC_NEXT 0x6b // command for next track of playlist - #define RC_PREVIOUS 0x6a // command for previous track of playlist - #define RC_FIRST 0x6c // command for first track of playlist - #define RC_LAST 0x6d // command for last track of playlist - #define RC_VOL_UP 0x1a // Command for volume up (one step) - #define RC_VOL_DOWN 0x1b // Command for volume down (one step) - #define RC_MUTE 0x1c // Command to mute ESPuino - #define RC_SHUTDOWN 0x2a // Command for deepsleep - #define RC_BLUETOOTH 0x72 // Command to enable/disable bluetooth - #define RC_FTP 0x65 // Command to enable FTP-server - #endif -#endif diff --git a/src/settings-override.h.sample b/src/settings-override.h.sample index a3cc559d..78e511f7 100644 --- a/src/settings-override.h.sample +++ b/src/settings-override.h.sample @@ -1,290 +1,322 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_OVERRIDE_H__ #define __ESPUINO_SETTINGS_OVERRIDE_H__ #include "Arduino.h" #include "values.h" //######################### INFOS #################################### - // This is the general configfile for ESPuino-configuration. - - //################## HARDWARE-PLATFORM ############################### - /* Make sure to also edit the configfile, that is specific for your platform. - If in doubts (your develboard is not listed) use HAL 1 - 1: Wemos Lolin32 => settings-lolin32.h - 2: ESP32-A1S Audiokit => settings-espa1s.h - 3: Wemos Lolin D32 => settings-lolin_D32.h - 4: Wemos Lolin D32 pro => settings-lolin_D32_pro.h - 5: Lilygo T8 (V1.7) => settings-ttgo_t8.h - 6: ESPuino complete => settings-complete.h - 7: Lolin D32 pro SDMMC Port-Expander => settings-lolin_d32_pro_sdmmc_pe.h - 8: AZDelivery ESP32 NodeMCU => settings-azdelivery_sdmmc.h - 9: Lolin D32 SDMMC Port-Expander => settings-lolin_d32_sdmmc_pe.h - 10: RASPIAUDIO Muse Proto => settings-muse_proto.h - 99: custom => settings-custom.h - more to come... - */ - #ifndef HAL // Will be set by platformio.ini. If using Arduino-IDE you have to set HAL according your needs! - #define HAL 1 // HAL 1 = LoLin32, 2 = ESP32-A1S-AudioKit, 3 = Lolin D32, 4 = Lolin D32 pro; ... 99 = custom - #endif - - - //########################## MODULES ################################# - //#define PORT_EXPANDER_ENABLE // When enabled, buttons can be connected via port-expander PCA9555 (https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306) - //#define I2S_COMM_FMT_LSB_ENABLE // Enables FMT instead of MSB for I2S-communication-format. Used e.g. by PT2811. Don't enable for MAX98357a, AC101 or PCM5102A) - #define MDNS_ENABLE // When enabled, you don't have to handle with ESPuino's IP-address. If hostname is set to "ESPuino", you can reach it via ESPuino.local - //#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd - #define FTP_ENABLE // Enables FTP-server; DON'T FORGET TO ACTIVATE AFTER BOOT BY PRESSING PAUSE + NEXT-BUTTONS (IN PARALLEL)! - #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 DE // DE = deutsch; EN = english - //#define STATIC_IP_ENABLE // Enables static IP-configuration (change static ip-section accordingly) - #define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone) and to enable stereo (if PLAY_MONO_SPEAKER is set) - #define PLAY_MONO_SPEAKER // If only one speaker is used enabling mono should make sense. Please note: headphones is always stereo (if HEADPHONE_ADJUST_ENABLE is active) - #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 MEASURE_BATTERY_MAX17055 // Enables battery-measurement via external fuel gauge (MAX17055) - //#define SHUTDOWN_ON_BAT_CRITICAL // Whether to turn off on critical battery-level (only used if MEASURE_BATTERY_XXX is active) - //#define PLAY_LAST_RFID_AFTER_REBOOT // When restarting ESPuino, the last RFID that was active before, is recalled and played - //#define USE_LAST_VOLUME_AFTER_REBOOT // Remembers the volume used at last shutdown after reboot - #define USEROTARY_ENABLE // If rotary-encoder is used (don't forget to review WAKEUP_BUTTON if you disable this feature!) - #define BLUETOOTH_ENABLE // If enabled and bluetooth-mode is active, you can stream to your ESPuino via bluetooth (a2dp-sink). - //#define IR_CONTROL_ENABLE // Enables remote control (https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265) - #define CACHED_PLAYLIST_ENABLE // Enables playlist-caching (infos: https://forum.espuino.de/t/neues-feature-cached-playlist/515) - //#define PAUSE_WHEN_RFID_REMOVED // Playback starts when card is applied and pauses automatically, when card is removed (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) - //#define DONT_ACCEPT_SAME_RFID_TWICE // RFID-reader doesn't accept the same RFID-tag twice in a row (unless it's a modification-card or RFID-tag is unknown in NVS). Flag will be ignored silently if PAUSE_WHEN_RFID_REMOVED is active. (https://forum.espuino.de/t/neues-feature-dont-accept-same-rfid-twice/1247) - //#define SAVE_PLAYPOS_BEFORE_SHUTDOWN // When playback is active and mode audiobook was selected, last play-position is saved automatically when shutdown is initiated - //#define SAVE_PLAYPOS_WHEN_RFID_CHANGE // When playback is active and mode audiobook was selected, last play-position is saved automatically for old playlist when new RFID-tag is applied - - - //################## select SD card mode ############################# - #define SD_MMC_1BIT_MODE // run SD card in SD-MMC 1Bit mode (using GPIOs 15 + 14 + 2 is mandatory!) - //#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!) - - - //################## select RFID reader ############################## - #define RFID_READER_TYPE_MFRC522_SPI // use MFRC522 via SPI - //#define RFID_READER_TYPE_MFRC522_I2C // use MFRC522 via I2C - //#define RFID_READER_TYPE_PN5180 // use PN5180 via SPI - - #ifdef RFID_READER_TYPE_MFRC522_I2C - #define MFRC522_ADDR 0x28 // default I2C-address of MFRC522 - #endif - - #ifdef RFID_READER_TYPE_PN5180 - //#define PN5180_ENABLE_LPCD // Wakes up ESPuino if RFID-tag was applied while deepsleep is active. Only ISO-14443-tags are supported for wakeup! - #endif - - #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_MFRC522_SPI) - constexpr uint8_t rfidGain = 0x07 << 4; // Sensitivity of RC522. For possible values see reference: https://forum.espuino.de/uploads/default/original/1X/9de5f8d35cbc123c1378cad1beceb3f51035cec0.png - #endif - - - //############# Port-expander-configuration ###################### - #ifdef PORT_EXPANDER_ENABLE - constexpr uint8_t expanderI2cAddress = 0x20; // I2C-address of PCA9555 (0x20 is true if PCA's pins A0+A1+A2 are pulled to GND) - #endif - - //################## BUTTON-Layout ################################## - /* German documentation: https://forum.espuino.de/t/das-dynamische-button-layout/224 - Please note the following numbers as you need to know them in order to define actions for buttons. - Even if you don't use all of them, their numbers won't change - 0: NEXT_BUTTON - 1: PREVIOUS_BUTTON - 2: PAUSEPLAY_BUTTON - 3: ROTARYENCODER_BUTTON - 4: BUTTON_4 - 5: BUTTON_5 - - Don't forget to enable/configure those buttons you want to use in your develboard-specific config (e.g. settings-custom.h) - - Single-buttons [can be long or short] (examples): - BUTTON_0_SHORT => Button 0 (NEXT_BUTTON) pressed shortly - BUTTON_3_SHORT => Button 3 (ROTARYENCODER_BUTTON) pressed shortly - BUTTON_4_LONG => Button 4 (BUTTON_4) pressed long - - Multi-buttons [short only] (examples): - BUTTON_MULTI_01 => Buttons 0+1 (NEXT_BUTTON + PREVIOUS_BUTTON) pressed in parallel - BUTTON_MULTI_12 => Buttons 1+2 (PREV_BUTTON + PAUSEPLAY_BUTTON) pressed in parallel - - Actions: - To all of those buttons, an action can be assigned freely. - Please have a look at values.h to look up actions available (>=100 can be used) - If you don't want to assign an action or you don't use a given button: CMD_NOTHING has to be set - */ - // *****BUTTON***** *****ACTION***** - #define BUTTON_0_SHORT CMD_NEXTTRACK - #define BUTTON_1_SHORT CMD_PREVTRACK - #define BUTTON_2_SHORT CMD_PLAYPAUSE - #define BUTTON_3_SHORT CMD_MEASUREBATTERY - #define BUTTON_4_SHORT CMD_SEEK_BACKWARDS - #define BUTTON_5_SHORT CMD_SEEK_FORWARDS - - #define BUTTON_0_LONG CMD_LASTTRACK - #define BUTTON_1_LONG CMD_FIRSTTRACK - #define BUTTON_2_LONG CMD_PLAYPAUSE - #define BUTTON_3_LONG CMD_SLEEPMODE - #define BUTTON_4_LONG CMD_NOTHING - #define BUTTON_5_LONG CMD_NOTHING - - #define BUTTON_MULTI_01 CMD_NOTHING //CMD_TOGGLE_WIFI_STATUS (disabled now to prevent children from unwanted WiFi-disable) - #define BUTTON_MULTI_02 CMD_ENABLE_FTP_SERVER - #define BUTTON_MULTI_03 CMD_NOTHING - #define BUTTON_MULTI_04 CMD_NOTHING - #define BUTTON_MULTI_05 CMD_NOTHING - #define BUTTON_MULTI_12 CMD_TELL_IP_ADDRESS - #define BUTTON_MULTI_13 CMD_NOTHING - #define BUTTON_MULTI_14 CMD_NOTHING - #define BUTTON_MULTI_15 CMD_NOTHING - #define BUTTON_MULTI_23 CMD_NOTHING - #define BUTTON_MULTI_24 CMD_NOTHING - #define BUTTON_MULTI_25 CMD_NOTHING - #define BUTTON_MULTI_34 CMD_NOTHING - #define BUTTON_MULTI_35 CMD_NOTHING - #define BUTTON_MULTI_45 CMD_NOTHING - - //#################### Various settings ############################## - - // Serial-logging-configuration - #define SERIAL_LOGLEVEL LOGLEVEL_DEBUG // Current loglevel for serial console - - // Static ip-configuration - #ifdef STATIC_IP_ENABLE - #define LOCAL_IP 192,168,2,100 // ESPuino's IP - #define GATEWAY_IP 192,168,2,1 // IP of the gateway/router - #define SUBNET_IP 255,255,255,0 // Netmask of your network (/24 => 255.255.255.0) - #define DNS_IP 192,168,2,1 // DNS-server of your network; in private networks it's usually the gatewy's IP - #endif - - // Buttons (better leave unchanged if in doubts :-)) - constexpr uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons - constexpr uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of previous/next-button - - // RFID-RC522 - #define RFID_SCAN_INTERVAL 100 // Interval-time in ms (how often is RFID read?) - - // Automatic restart - #ifdef SHUTDOWN_IF_SD_BOOT_FAILS - constexpr 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 - - // ESPuino will create a WiFi if joing existing WiFi was not possible. Name can be configured here. - constexpr const char accessPointNetworkSSID[] PROGMEM = "ESPuino"; // Access-point's SSID - - // Bluetooth - constexpr const char nameBluetoothSinkDevice[] PROGMEM = "ESPuino"; // Name of your ESPuino as Bluetooth-device - constexpr const char nameBluetoothSourceDevice[] PROGMEM = "My POGS Wireless Headphone"; // Name of Bluetooth-device to connect to (BT-Headset name) (https://forum.espuino.de/t/neues-feature-bluetooth-kopfhoerer/1293/) - - // Where to store the backup-file for NVS-records - constexpr const char backupFile[] PROGMEM = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done - constexpr const char playlistCacheFile[] PROGMEM = "playlistcache.csv"; // Filename that is used for caching playlists - - //#################### Settings for optional Modules############################## - // (optinal) Neopixel - #ifdef NEOPIXEL_ENABLE - #define NUM_LEDS 24 // number of LEDs - #define CHIPSET WS2812B // type of Neopixel - #define COLOR_ORDER GRB - #endif - - #if defined(MEASURE_BATTERY_VOLTAGE) || defined(MEASURE_BATTERY_MAX17055) - #define BATTERY_MEASURE_ENABLE // Don't change. Set automatically if any method of battery monitoring is selected. - constexpr uint8_t s_batteryCheckInterval = 10; // How often battery is measured (in minutes) (can be changed via GUI!) - #endif - - #ifdef MEASURE_BATTERY_VOLTAGE - // (optional) Default-voltages for battery-monitoring via Neopixel; can be changed later via WebGUI - constexpr float s_warningLowVoltage = 3.4; // If battery-voltage is <= this value, a cyclic warning will be indicated by Neopixel (can be changed via GUI!) - constexpr float s_warningCriticalVoltage = 3.1; // If battery-voltage is <= this value, assume battery near-empty. Set to 0V to disable. - constexpr float s_voltageIndicatorLow = 3.0; // Lower range for Neopixel-voltage-indication (0 leds) (can be changed via GUI!) - constexpr float s_voltageIndicatorHigh = 4.2; // Upper range for Neopixel-voltage-indication (all leds) (can be changed via GUI!) - #endif - - #ifdef MEASURE_BATTERY_MAX17055 - constexpr float s_batteryLow = 15.0; // low percentage - constexpr float s_batteryCritical = 5.0; // critical percentage - - constexpr uint16_t s_batteryCapacity = 6000; // design cap of battery (mAh) - constexpr uint16_t s_emptyVoltage = 300; // empty voltage in 10mV - constexpr uint16_t s_recoveryVoltage = 360; // recovery voltage in 10mV - constexpr uint8_t s_batteryChemistry = 0x60; // 0 = Li-Ion, 0x20 = NCR, 0x60 = LiFePO4 - constexpr float s_resistSensor = 0.01; // current sense resistor, currently non-default values might lead to problems - constexpr bool s_vCharge = false; // true if charge voltage is greater than 4.275V - #endif - - // enable I2C if necessary - #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(PORT_EXPANDER_ENABLE) || defined(MEASURE_BATTERY_MAX17055) - #define I2C_2_ENABLE - #endif - - // (optinal) Headphone-detection (leave unchanged if in doubts...) - #ifdef HEADPHONE_ADJUST_ENABLE - constexpr uint16_t headphoneLastDetectionDebounce = 1000; // Debounce-interval in ms when plugging in headphone - #endif - - // Seekmode-configuration - constexpr uint8_t jumpOffset = 30; // Offset in seconds to jump for commands CMD_SEEK_FORWARDS / CMD_SEEK_BACKWARDS - - // (optional) Topics for MQTT - #ifdef MQTT_ENABLE - constexpr uint16_t mqttRetryInterval = 60; // Try to reconnect to MQTT-server every (n) seconds if connection is broken - constexpr uint8_t mqttMaxRetriesPerInterval = 1; // Number of retries per time-interval (mqttRetryInterval). mqttRetryInterval 60 / mqttMaxRetriesPerInterval 1 => once every 60s - #define DEVICE_HOSTNAME "ESP32-ESPuino" // Name that is used for MQTT - constexpr const char topicSleepCmnd[] PROGMEM = "Cmnd/ESPuino/Sleep"; - constexpr const char topicSleepState[] PROGMEM = "State/ESPuino/Sleep"; - constexpr const char topicRfidCmnd[] PROGMEM = "Cmnd/ESPuino/Rfid"; - constexpr const char topicRfidState[] PROGMEM = "State/ESPuino/Rfid"; - constexpr const char topicTrackState[] PROGMEM = "State/ESPuino/Track"; - constexpr const char topicTrackControlCmnd[] PROGMEM = "Cmnd/ESPuino/TrackControl"; - constexpr const char topicCoverChangedState[] PROGMEM = "State/ESPuino/CoverChanged"; - constexpr const char topicLoudnessCmnd[] PROGMEM = "Cmnd/ESPuino/Loudness"; - constexpr const char topicLoudnessState[] PROGMEM = "State/ESPuino/Loudness"; - constexpr const char topicSleepTimerCmnd[] PROGMEM = "Cmnd/ESPuino/SleepTimer"; - constexpr const char topicSleepTimerState[] PROGMEM = "State/ESPuino/SleepTimer"; - constexpr const char topicState[] PROGMEM = "State/ESPuino/State"; - constexpr const char topicCurrentIPv4IP[] PROGMEM = "State/ESPuino/IPv4"; - constexpr const char topicLockControlsCmnd[] PROGMEM ="Cmnd/ESPuino/LockControls"; - constexpr const char topicLockControlsState[] PROGMEM ="State/ESPuino/LockControls"; - constexpr const char topicPlaymodeState[] PROGMEM = "State/ESPuino/Playmode"; - constexpr const char topicRepeatModeCmnd[] PROGMEM = "Cmnd/ESPuino/RepeatMode"; - constexpr const char topicRepeatModeState[] PROGMEM = "State/ESPuino/RepeatMode"; - constexpr const char topicLedBrightnessCmnd[] PROGMEM = "Cmnd/ESPuino/LedBrightness"; - constexpr const char topicLedBrightnessState[] PROGMEM = "State/ESPuino/LedBrightness"; - constexpr const char topicWiFiRssiState[] PROGMEM = "State/ESPuino/WifiRssi"; - constexpr const char topicSRevisionState[] PROGMEM = "State/ESPuino/SoftwareRevision"; - #ifdef BATTERY_MEASURE_ENABLE - constexpr const char topicBatteryVoltage[] PROGMEM = "State/ESPuino/Voltage"; - constexpr const char topicBatterySOC[] PROGMEM = "State/ESPuino/Battery"; - #endif - #endif - - // !!! MAKE SURE TO EDIT PLATFORM SPECIFIC settings-****.h !!! - #if (HAL == 1) - #include "settings-lolin32.h" // Contains all user-relevant settings for Wemos Lolin32 - #elif (HAL == 2) - #include "settings-espa1s.h" // Contains all user-relevant settings for ESP32-A1S Audiokit - #elif (HAL == 3) - #include "settings-lolin_d32.h" // Contains all user-relevant settings for Wemos Lolin D32 - #elif (HAL == 4) - #include "settings-lolin_d32_pro.h" // Contains all user-relevant settings for Wemos Lolin D32 pro - #elif (HAL == 5) - #include "settings-ttgo_t8.h" // Contains all user-relevant settings for Lilygo TTGO T8 1.7 - #elif (HAL == 6) - #include "settings-complete.h" // Contains all user-relevant settings for ESPuino complete - #elif (HAL == 7) - #include "settings-lolin_d32_pro_sdmmc_pe.h" // Pre-configured settings for ESPuino Lolin D32 pro with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) - #elif (HAL == 8) - #include "settings-azdelivery_sdmmc.h" // Pre-configured settings for AZ Delivery ESP32 NodeMCU / Devkit C (https://forum.espuino.de/t/az-delivery-esp32-nodemcu-devkit-c-mit-sd-mmc-und-pn5180-als-rfid-leser/634) - #elif (HAL == 9) - #include "settings-lolin_d32_sdmmc_pe.h" // Pre-configured settings for Lolin D32 (non-pro) with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32-pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) - #elif (HAL == 10) - #include "settings-muse_proto.h" // Pre-configured settings for Raspiaudio ESPMuse Proto Board with I2C RFID Reader (https://raspiaudio.com/produit/muse-proto) - #elif (HAL == 99) - #include "settings-custom.h" // Contains all user-relevant settings custom-board - #endif - - //#define ENABLE_ESPUINO_DEBUG // Needs modification of platformio.ini (https://forum.espuino.de/t/rfid-mit-oder-ohne-task/353/21); better don't enable unless you know what you're doing :-) - #endif //settings_override + // This is the general configfile for ESPuino-configuration. + + //################## HARDWARE-PLATFORM ############################### + /* Make sure to also edit the configfile, that is specific for your platform. + If in doubts (your develboard is not listed) use HAL 1 + 1: Wemos Lolin32 => settings-lolin32.h + 2: ESP32-A1S Audiokit => settings-espa1s.h + 3: Wemos Lolin D32 => settings-lolin_D32.h + 4: Wemos Lolin D32 pro => settings-lolin_D32_pro.h + 5: Lilygo T8 (V1.7) => settings-ttgo_t8.h + 6: ESPuino complete => settings-complete.h + 7: Lolin D32 pro SDMMC Port-Expander => settings-lolin_d32_pro_sdmmc_pe.h + 8: AZDelivery ESP32 NodeMCU => settings-azdelivery_sdmmc.h + 9: Lolin D32 SDMMC Port-Expander => settings-lolin_d32_sdmmc_pe.h + 99: custom => settings-custom.h + more to come... + */ + #ifndef HAL // Will be set by platformio.ini. If using Arduino-IDE you have to set HAL according your needs! + #define HAL 1 // HAL 1 = LoLin32, 2 = ESP32-A1S-AudioKit, 3 = Lolin D32, 4 = Lolin D32 pro; ... 99 = custom + #endif + + + //########################## MODULES ################################# + //#define PORT_EXPANDER_ENABLE // When enabled, buttons can be connected via port-expander PCA9555 (https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306) + //#define I2S_COMM_FMT_LSB_ENABLE // Enables FMT instead of MSB for I2S-communication-format. Used e.g. by PT2811. Don't enable for MAX98357a, AC101 or PCM5102A) + #define MDNS_ENABLE // When enabled, you don't have to handle with ESPuino's IP-address. If hostname is set to "ESPuino", you can reach it via ESPuino.local + //#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd + #define FTP_ENABLE // Enables FTP-server; DON'T FORGET TO ACTIVATE AFTER BOOT BY PRESSING PAUSE + NEXT-BUTTONS (IN PARALLEL)! + #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 DE // DE = deutsch; EN = english + //#define STATIC_IP_ENABLE // DEPRECATED: Enables static IP-configuration (change static ip-section accordingly) + #define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone) and to enable stereo (if PLAY_MONO_SPEAKER is set) + //#define PLAY_MONO_SPEAKER // If only one speaker is used enabling mono should make sense. Please note: headphones is always stereo (if HEADPHONE_ADJUST_ENABLE is active) + #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 MEASURE_BATTERY_MAX17055 // Enables battery-measurement via external fuel gauge (MAX17055) + //#define SHUTDOWN_ON_BAT_CRITICAL // Whether to turn off on critical battery-level (only used if MEASURE_BATTERY_XXX is active) + //#define PLAY_LAST_RFID_AFTER_REBOOT // When restarting ESPuino, the last RFID that was active before, is recalled and played + //#define USE_LAST_VOLUME_AFTER_REBOOT // Remembers the volume used at last shutdown after reboot + #define USEROTARY_ENABLE // If rotary-encoder is used (don't forget to review WAKEUP_BUTTON if you disable this feature!) + #define BLUETOOTH_ENABLE // If enabled and bluetooth-mode is active, you can stream to your ESPuino or to a headset via bluetooth (a2dp-sink & a2dp-source). Note: This feature consumes a lot of resources and the available flash/ram might not be sufficient. + //#define IR_CONTROL_ENABLE // Enables remote control (https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265) + //#define PAUSE_WHEN_RFID_REMOVED // Playback starts when card is applied and pauses automatically, when card is removed (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) + //#define PAUSE_ON_MIN_VOLUME // When playback is active and volume is changed to zero, playback is paused automatically. Playback is continued if volume reaches 1. (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) + //#define DONT_ACCEPT_SAME_RFID_TWICE // RFID-reader doesn't accept the same RFID-tag twice in a row (unless it's a modification-card or RFID-tag is unknown in NVS). Flag will be ignored silently if PAUSE_WHEN_RFID_REMOVED is active. (https://forum.espuino.de/t/neues-feature-dont-accept-same-rfid-twice/1247) + //#define SAVE_PLAYPOS_BEFORE_SHUTDOWN // When playback is active and mode audiobook was selected, last play-position is saved automatically when shutdown is initiated + //#define SAVE_PLAYPOS_WHEN_RFID_CHANGE // When playback is active and mode audiobook was selected, last play-position is saved automatically for old playlist when new RFID-tag is applied + //#define HALLEFFECT_SENSOR_ENABLE // Support for hallsensor. For fine-tuning please adjust HallEffectSensor.h Please note: only user-support provided (https://forum.espuino.de/t/magnetische-hockey-tags/1449/35) + #define VOLUMECURVE 0 // 0=square, 1=logarithmic (1 is more flatten at lower volume) + + //################## set PAUSE_WHEN_RFID_REMOVED behaviour ############################# + #ifdef PAUSE_WHEN_RFID_REMOVED + #define ACCEPT_SAME_RFID_AFTER_TRACK_END // Accepts same RFID after playback has ended (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541/2) + #endif + + //################## select SD card mode ############################# + #define SD_MMC_1BIT_MODE // run SD card in SD-MMC 1Bit mode (using GPIOs 15 + 14 + 2 is mandatory!) + //#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!) + //#define NO_SDCARD // enable to start without any SD card, e.g. for a webplayer only. SD card Settings above will be ignored + + + //################## select RFID reader ############################## + #define RFID_READER_TYPE_MFRC522_SPI // use MFRC522 via SPI + //#define RFID_READER_TYPE_MFRC522_I2C // use MFRC522 via I2C + //#define RFID_READER_TYPE_PN5180 // use PN5180 via SPI + + #ifdef RFID_READER_TYPE_MFRC522_I2C + #define MFRC522_ADDR 0x28 // default I2C-address of MFRC522 + #endif + + #ifdef RFID_READER_TYPE_PN5180 + //#define PN5180_ENABLE_LPCD // Wakes up ESPuino if RFID-tag was applied while deepsleep is active. Only ISO-14443-tags are supported for wakeup! + #endif + + #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_MFRC522_SPI) + constexpr uint8_t rfidGain = 0x07 << 4; // Sensitivity of RC522. For possible values see reference: https://forum.espuino.de/uploads/default/original/1X/9de5f8d35cbc123c1378cad1beceb3f51035cec0.png + #endif + + + //############# Port-expander-configuration ###################### + #ifdef PORT_EXPANDER_ENABLE + constexpr uint8_t expanderI2cAddress = 0x20; // I2C-address of PCA9555 (0x20 is true if PCA's pins A0+A1+A2 are pulled to GND) + #endif + + //################## BUTTON-Layout ################################## + /* German documentation: https://forum.espuino.de/t/das-dynamische-button-layout/224 + Please note the following numbers as you need to know them in order to define actions for buttons. + Even if you don't use all of them, their numbers won't change + 0: NEXT_BUTTON + 1: PREVIOUS_BUTTON + 2: PAUSEPLAY_BUTTON + 3: ROTARYENCODER_BUTTON + 4: BUTTON_4 + 5: BUTTON_5 + + Don't forget to enable/configure those buttons you want to use in your develboard-specific config (e.g. settings-custom.h) + + Single-buttons [can be long or short] (examples): + BUTTON_0_SHORT => Button 0 (NEXT_BUTTON) pressed shortly + BUTTON_3_SHORT => Button 3 (ROTARYENCODER_BUTTON) pressed shortly + BUTTON_4_LONG => Button 4 (BUTTON_4) pressed long + + Multi-buttons [short only] (examples): + BUTTON_MULTI_01 => Buttons 0+1 (NEXT_BUTTON + PREVIOUS_BUTTON) pressed in parallel + BUTTON_MULTI_12 => Buttons 1+2 (PREV_BUTTON + PAUSEPLAY_BUTTON) pressed in parallel + + Actions: + To all of those buttons, an action can be assigned freely. + Please have a look at values.h to look up actions available (>=100 can be used) + If you don't want to assign an action or you don't use a given button: CMD_NOTHING has to be set + */ + // *****BUTTON***** *****ACTION***** + #define BUTTON_0_SHORT CMD_NEXTTRACK + #define BUTTON_1_SHORT CMD_PREVTRACK + #define BUTTON_2_SHORT CMD_PLAYPAUSE + #define BUTTON_3_SHORT CMD_MEASUREBATTERY + #define BUTTON_4_SHORT CMD_SEEK_BACKWARDS + #define BUTTON_5_SHORT CMD_SEEK_FORWARDS + + #define BUTTON_0_LONG CMD_LASTTRACK + #define BUTTON_1_LONG CMD_FIRSTTRACK + #define BUTTON_2_LONG CMD_PLAYPAUSE + #define BUTTON_3_LONG CMD_SLEEPMODE + #define BUTTON_4_LONG CMD_VOLUMEUP + #define BUTTON_5_LONG CMD_VOLUMEDOWN + + #define BUTTON_MULTI_01 CMD_NOTHING //CMD_TOGGLE_WIFI_STATUS (disabled now to prevent children from unwanted WiFi-disable) + #define BUTTON_MULTI_02 CMD_ENABLE_FTP_SERVER + #define BUTTON_MULTI_03 CMD_NOTHING + #define BUTTON_MULTI_04 CMD_NOTHING + #define BUTTON_MULTI_05 CMD_NOTHING + #define BUTTON_MULTI_12 CMD_TELL_IP_ADDRESS + #define BUTTON_MULTI_13 CMD_NOTHING + #define BUTTON_MULTI_14 CMD_NOTHING + #define BUTTON_MULTI_15 CMD_NOTHING + #define BUTTON_MULTI_23 CMD_NOTHING + #define BUTTON_MULTI_24 CMD_NOTHING + #define BUTTON_MULTI_25 CMD_NOTHING + #define BUTTON_MULTI_34 CMD_NOTHING + #define BUTTON_MULTI_35 CMD_NOTHING + #define BUTTON_MULTI_45 CMD_NOTHING + + //#################### Various settings ############################## + + // Serial-logging-configuration + #define SERIAL_LOGLEVEL LOGLEVEL_DEBUG // Current loglevel for serial console + + // DEPRECATED: This is now done using dynamic network configuration. + // If left, it is used for the automatic migration exactly once + // Static ip-configuration + #ifdef STATIC_IP_ENABLE + #define LOCAL_IP 192,168,2,100 // ESPuino's IP + #define GATEWAY_IP 192,168,2,1 // IP of the gateway/router + #define SUBNET_IP 255,255,255,0 // Netmask of your network (/24 => 255.255.255.0) + #define DNS_IP 192,168,2,1 // DNS-server of your network; in private networks it's usually the gatewy's IP + #endif + + // Buttons (better leave unchanged if in doubts :-)) + constexpr uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons + constexpr uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of buttons + + // Buttons active state: Default 0 for active LOW, 1 for active HIGH e.g. for TTP223 Capacitive Touch Switch Button (FinnBox) + #define BUTTON_0_ACTIVE_STATE 0 + #define BUTTON_1_ACTIVE_STATE 0 + #define BUTTON_2_ACTIVE_STATE 0 + #define BUTTON_3_ACTIVE_STATE 0 + #define BUTTON_4_ACTIVE_STATE 0 + #define BUTTON_5_ACTIVE_STATE 0 + + //#define CONTROLS_LOCKED_BY_DEFAULT // If set the controls are locked at boot + #define INCLUDE_ROTARY_IN_CONTROLS_LOCK // If set the rotary encoder is locked if controls are locked + + // RFID-RC522 + #define RFID_SCAN_INTERVAL 100 // Interval-time in ms (how often is RFID read?) + + // Automatic restart + #ifdef SHUTDOWN_IF_SD_BOOT_FAILS + constexpr 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 + + // timezone + // see list of valid timezones: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv + // example for Europe/Berlin: "CET-1CEST,M3.5.0,M10.5.0/3" + // example for America/Toronto: "EST5EDT,M3.2.0,M11.1.0" + constexpr const char timeZone[] = "CET-1CEST,M3.5.0,M10.5.0/3"; // Europe/Berlin + + // ESPuino will create a WiFi if joing existing WiFi was not possible. Name and password can be configured here. + constexpr const char accessPointNetworkSSID[] = "ESPuino"; // Access-point's SSID + constexpr const char accessPointNetworkPassword[] = ""; // Access-point's Password, at least 8 characters! Set to an empty string to spawn an open WiFi. + + // Bluetooth + constexpr const char nameBluetoothSinkDevice[] = "ESPuino"; // Name of your ESPuino as Bluetooth-device + + // Where to store the backup-file for NVS-records + constexpr const char backupFile[] = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done + + //#################### Settings for optional Modules############################## + // (optinal) Neopixel + #ifdef NEOPIXEL_ENABLE + #define NUM_INDICATOR_LEDS 24 // number of Neopixel LEDs (formerly NUM_LEDS) + #define NUM_CONTROL_LEDS 0 // optional control leds (https://forum.espuino.de/t/statische-ws2812-leds/1703) + #define CONTROL_LEDS_COLORS {} // Colors for the control LEDs. Make sure it lists at least NUM_CONTROL_LEDS colors, e.g. for three control LEDs define: CONTROL_LEDS_COLORS {CRGB::Yellow, CRGB::Blue, 0xFFFFFF} (predefined colors: http://fastled.io/docs/3.1/struct_c_r_g_b.html) + #define CHIPSET WS2812B // type of Neopixel + #define COLOR_ORDER GRB + #define NUM_LEDS_IDLE_DOTS 4 // count of LEDs, which are shown when Idle + #define OFFSET_PAUSE_LEDS false // if true the pause-leds are centered in the mid of the LED-Strip + #define PROGRESS_HUE_START 85 // Start and end hue of mulitple-LED progress indicator. Hue ranges from basically 0 - 255, but you can also set numbers outside this range to get the desired effect (e.g. 85-215 will go from green to purple via blue, 341-215 start and end at exactly the same color but go from green to purple via yellow and red) + #define PROGRESS_HUE_END -1 + #define DIMMABLE_STATES 50 // Number of dimmed values between two full LEDs (https://forum.espuino.de/t/led-verbesserungen-rework/1739) + //#define LED_OFFSET 0 // shifts the starting LED in the original direction of the neopixel ring + #endif + + #if defined(MEASURE_BATTERY_VOLTAGE) || defined(MEASURE_BATTERY_MAX17055) + #define BATTERY_MEASURE_ENABLE // Don't change. Set automatically if any method of battery monitoring is selected. + constexpr uint8_t s_batteryCheckInterval = 10; // How often battery is measured (in minutes) (can be changed via GUI!) + #endif + + #ifdef MEASURE_BATTERY_VOLTAGE + // (optional) Default-voltages for battery-monitoring via Neopixel; can be changed later via WebGUI + constexpr float s_warningLowVoltage = 3.4; // If battery-voltage is <= this value, a cyclic warning will be indicated by Neopixel (can be changed via GUI!) + constexpr float s_warningCriticalVoltage = 3.1; // If battery-voltage is <= this value, assume battery near-empty. Set to 0V to disable. + constexpr float s_voltageIndicatorLow = 3.0; // Lower range for Neopixel-voltage-indication (0 leds) (can be changed via GUI!) + constexpr float s_voltageIndicatorHigh = 4.2; // Upper range for Neopixel-voltage-indication (all leds) (can be changed via GUI!) + #endif + + #ifdef MEASURE_BATTERY_MAX17055 + constexpr float s_batteryLow = 15.0; // low percentage + constexpr float s_batteryCritical = 5.0; // critical percentage + + constexpr uint16_t s_batteryCapacity = 6000; // design cap of battery (mAh) + constexpr uint16_t s_emptyVoltage = 300; // empty voltage in 10mV + constexpr uint16_t s_recoveryVoltage = 360; // recovery voltage in 10mV + constexpr uint8_t s_batteryChemistry = 0x60; // 0 = Li-Ion, 0x20 = NCR, 0x60 = LiFePO4 + constexpr float s_resistSensor = 0.01; // current sense resistor, currently non-default values might lead to problems + constexpr bool s_vCharge = false; // true if charge voltage is greater than 4.275V + #endif + + // enable I2C if necessary + #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(PORT_EXPANDER_ENABLE) || defined(MEASURE_BATTERY_MAX17055) + #define I2C_2_ENABLE + #endif + + // (optinal) Headphone-detection (leave unchanged if in doubts...) + #ifdef HEADPHONE_ADJUST_ENABLE + constexpr uint16_t headphoneLastDetectionDebounce = 1000; // Debounce-interval in ms when plugging in headphone + #endif + + // Seekmode-configuration + constexpr uint8_t jumpOffset = 30; // Offset in seconds to jump for commands CMD_SEEK_FORWARDS / CMD_SEEK_BACKWARDS + + // (optional) Topics for MQTT + #ifdef MQTT_ENABLE + constexpr uint16_t mqttRetryInterval = 60; // Try to reconnect to MQTT-server every (n) seconds if connection is broken + constexpr uint8_t mqttMaxRetriesPerInterval = 1; // Number of retries per time-interval (mqttRetryInterval). mqttRetryInterval 60 / mqttMaxRetriesPerInterval 1 => once every 60s + #define DEVICE_HOSTNAME "ESP32-ESPuino" // Name that is used for MQTT + constexpr const char topicSleepCmnd[] = "Cmnd/ESPuino/Sleep"; + constexpr const char topicSleepState[] = "State/ESPuino/Sleep"; + constexpr const char topicRfidCmnd[] = "Cmnd/ESPuino/Rfid"; + constexpr const char topicRfidState[] = "State/ESPuino/Rfid"; + constexpr const char topicTrackState[] = "State/ESPuino/Track"; + constexpr const char topicTrackControlCmnd[] = "Cmnd/ESPuino/TrackControl"; + constexpr const char topicCoverChangedState[] = "State/ESPuino/CoverChanged"; + constexpr const char topicLoudnessCmnd[] = "Cmnd/ESPuino/Loudness"; + constexpr const char topicLoudnessState[] = "State/ESPuino/Loudness"; + constexpr const char topicSleepTimerCmnd[] = "Cmnd/ESPuino/SleepTimer"; + constexpr const char topicSleepTimerState[] = "State/ESPuino/SleepTimer"; + constexpr const char topicState[] = "State/ESPuino/State"; + constexpr const char topicCurrentIPv4IP[] = "State/ESPuino/IPv4"; + constexpr const char topicLockControlsCmnd[] ="Cmnd/ESPuino/LockControls"; + constexpr const char topicLockControlsState[] ="State/ESPuino/LockControls"; + constexpr const char topicPlaymodeState[] = "State/ESPuino/Playmode"; + constexpr const char topicRepeatModeCmnd[] = "Cmnd/ESPuino/RepeatMode"; + constexpr const char topicRepeatModeState[] = "State/ESPuino/RepeatMode"; + constexpr const char topicLedBrightnessCmnd[] = "Cmnd/ESPuino/LedBrightness"; + constexpr const char topicLedBrightnessState[] = "State/ESPuino/LedBrightness"; + constexpr const char topicWiFiRssiState[] = "State/ESPuino/WifiRssi"; + constexpr const char topicSRevisionState[] = "State/ESPuino/SoftwareRevision"; + #ifdef BATTERY_MEASURE_ENABLE + constexpr const char topicBatteryVoltage[] = "State/ESPuino/Voltage"; + constexpr const char topicBatterySOC[] = "State/ESPuino/Battery"; + #endif + #endif + + // !!! MAKE SURE TO EDIT PLATFORM SPECIFIC settings-****.h !!! + #if (HAL == 1) + #include "settings-lolin32.h" // Contains all user-relevant settings for Wemos Lolin32 + #elif (HAL == 2) + #include "settings-espa1s.h" // Contains all user-relevant settings for ESP32-A1S Audiokit + #elif (HAL == 3) + #include "settings-lolin_d32.h" // Contains all user-relevant settings for Wemos Lolin D32 + #elif (HAL == 4) + #include "settings-lolin_d32_pro.h" // Contains all user-relevant settings for Wemos Lolin D32 pro + #elif (HAL == 5) + #include "settings-ttgo_t8.h" // Contains all user-relevant settings for Lilygo TTGO T8 1.7 + #elif (HAL == 6) + #include "settings-complete.h" // Contains all user-relevant settings for ESPuino complete + #elif (HAL == 7) + #include "settings-lolin_d32_pro_sdmmc_pe.h" // Pre-configured settings for ESPuino Lolin D32 pro with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) + #elif (HAL == 8) + #include "settings-azdelivery_sdmmc.h" // Pre-configured settings for AZ Delivery ESP32 NodeMCU / Devkit C (https://forum.espuino.de/t/az-delivery-esp32-nodemcu-devkit-c-mit-sd-mmc-und-pn5180-als-rfid-leser/634) + #elif (HAL == 9) + #include "settings-lolin_d32_sdmmc_pe.h" // Pre-configured settings for Lolin D32 (non-pro) with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32-pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) + #elif (HAL == 99) + #include "settings-custom.h" // Contains all user-relevant settings custom-board + #endif + + //#define ENABLE_ESPUINO_DEBUG // Needs modification of platformio.ini (https://forum.espuino.de/t/rfid-mit-oder-ohne-task/353/21); better don't enable unless you know what you're doing :-) + #endif //settings_override #endif diff --git a/src/settings-ttgo_t8.h b/src/settings-ttgo_t8.h index c3d7feb3..2b385222 100644 --- a/src/settings-ttgo_t8.h +++ b/src/settings-ttgo_t8.h @@ -1,3 +1,5 @@ +// clang-format off + #include "Arduino.h" //######################### INFOS #################################### diff --git a/src/settings.h b/src/settings.h index 725fcbae..7089879a 100644 --- a/src/settings.h +++ b/src/settings.h @@ -1,304 +1,325 @@ +// clang-format off + #ifndef __ESPUINO_SETTINGS_H__ - #define __ESPUINO_SETTINGS_H__ + #define __ESPUINO_SETTINGS_H__ #include "Arduino.h" #include "values.h" #if __has_include("settings-override.h") - #include "settings-override.h" + #include "settings-override.h" #else - //######################### INFOS #################################### - // This is the general configfile for ESPuino-configuration. - - //################## HARDWARE-PLATFORM ############################### - /* Make sure to also edit the configfile, that is specific for your platform. - If in doubts (your develboard is not listed) use HAL 1 - 1: Wemos Lolin32 => settings-lolin32.h - 2: ESP32-A1S Audiokit => settings-espa1s.h - 3: Wemos Lolin D32 => settings-lolin_D32.h - 4: Wemos Lolin D32 pro => settings-lolin_D32_pro.h - 5: Lilygo T8 (V1.7) => settings-ttgo_t8.h - 6: ESPuino complete => settings-complete.h - 7: Lolin D32 pro SDMMC Port-Expander => settings-lolin_d32_pro_sdmmc_pe.h - 8: AZDelivery ESP32 NodeMCU => settings-azdelivery_sdmmc.h - 9: Lolin D32 SDMMC Port-Expander => settings-lolin_d32_sdmmc_pe.h - 10: RASPIAUDIO Muse Proto => settings-muse_proto.h - 99: custom => settings-custom.h - more to come... - */ - #ifndef HAL // Will be set by platformio.ini. If using Arduini-IDE you have to set HAL according your needs! - #define HAL 99 // HAL 1 = LoLin32, 2 = ESP32-A1S-AudioKit, 3 = Lolin D32, 4 = Lolin D32 pro; ... 99 = custom - #endif - - - //########################## MODULES ################################# - //#define PORT_EXPANDER_ENABLE // When enabled, buttons can be connected via port-expander PCA9555 (https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306) - //#define I2S_COMM_FMT_LSB_ENABLE // Enables FMT instead of MSB for I2S-communication-format. Used e.g. by PT2811. Don't enable for MAX98357a, AC101 or PCM5102A) - #define MDNS_ENABLE // When enabled, you don't have to handle with ESPuino's IP-address. If hostname is set to "ESPuino", you can reach it via ESPuino.local - //#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd - #define FTP_ENABLE // Enables FTP-server; DON'T FORGET TO ACTIVATE AFTER BOOT BY PRESSING PAUSE + NEXT-BUTTONS (IN PARALLEL)! - #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 DE // DE = deutsch; EN = english - //#define STATIC_IP_ENABLE // Enables static IP-configuration (change static ip-section accordingly) - #define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone) and to enable stereo (if PLAY_MONO_SPEAKER is set) - #define PLAY_MONO_SPEAKER // If only one speaker is used enabling mono should make sense. Please note: headphones is always stereo (if HEADPHONE_ADJUST_ENABLE is active) - #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 MEASURE_BATTERY_MAX17055 // Enables battery-measurement via external fuel gauge (MAX17055) - #define SHUTDOWN_ON_BAT_CRITICAL // Whether to turn off on critical battery-level (only used if MEASURE_BATTERY_XXX is active) - #define PLAY_LAST_RFID_AFTER_REBOOT // When restarting ESPuino, the last RFID that was active before, is recalled and played - #define USE_LAST_VOLUME_AFTER_REBOOT // Remembers the volume used at last shutdown after reboot - #define USEROTARY_ENABLE // If rotary-encoder is used (don't forget to review WAKEUP_BUTTON if you disable this feature!) - #define BLUETOOTH_ENABLE // If enabled and bluetooth-mode is active, you can stream to your ESPuino via bluetooth (a2dp-sink). - //#define IR_CONTROL_ENABLE // Enables remote control (https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265) - #define CACHED_PLAYLIST_ENABLE // Enables playlist-caching (infos: https://forum.espuino.de/t/neues-feature-cached-playlist/515) - //#define PAUSE_WHEN_RFID_REMOVED // Playback starts when card is applied and pauses automatically, when card is removed (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) - #define SAVE_PLAYPOS_BEFORE_SHUTDOWN // When playback is active and mode audiobook was selected, last play-position is saved automatically when shutdown is initiated - #define SAVE_PLAYPOS_WHEN_RFID_CHANGE // When playback is active and mode audiobook was selected, last play-position is saved automatically for old playlist when new RFID-tag is applied - //#define DONT_ACCEPT_SAME_RFID_TWICE // RFID-reader doesn't accept the same RFID-tag twice in a row (unless it's a modification-card or RFID-tag is unknown in NVS). Flag will be ignored silently if PAUSE_WHEN_RFID_REMOVED is active. (https://forum.espuino.de/t/neues-feature-dont-accept-same-rfid-twice/1247) - #define PAUSE_ON_MIN_VOLUME // When playback is active and volume is changed to zero, playback is paused automatically. Playback is continued if volume reaches 1. (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) - //#define HALLEFFECT_SENSOR_ENABLE // Support for hallsensor. For fine-tuning please adjust HallEffectSensor.h Please note: only user-support provided (https://forum.espuino.de/t/magnetische-hockey-tags/1449/35) - - //################## set PAUSE_WHEN_RFID_REMOVED behaviour ############################# - #ifdef PAUSE_WHEN_RFID_REMOVED - #define ACCEPT_SAME_RFID_AFTER_TRACK_END // Accepts same RFID after playback has ended (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541/2) - #endif - - //################## select SD card mode ############################# - #define SD_MMC_1BIT_MODE // run SD card in SD-MMC 1Bit mode (using GPIOs 15 + 14 + 2 is mandatory!) - //#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!) - - - //################## select RFID reader ############################## - //#define RFID_READER_TYPE_MFRC522_SPI // use MFRC522 via SPI - //#define RFID_READER_TYPE_MFRC522_I2C // use MFRC522 via I2C - #define RFID_READER_TYPE_PN5180 // use PN5180 via SPI - - #ifdef RFID_READER_TYPE_MFRC522_I2C - #define MFRC522_ADDR 0x28 // default I2C-address of MFRC522 - #endif - - #ifdef RFID_READER_TYPE_PN5180 - #define PN5180_ENABLE_LPCD // Wakes up ESPuino if RFID-tag was applied while deepsleep is active. Only ISO-14443-tags are supported for wakeup! - #endif - - #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_MFRC522_SPI) - constexpr uint8_t rfidGain = 0x07 << 4; // Sensitivity of RC522. For possible values see reference: https://forum.espuino.de/uploads/default/original/1X/9de5f8d35cbc123c1378cad1beceb3f51035cec0.png - #endif - - - //############# Port-expander-configuration ###################### - #ifdef PORT_EXPANDER_ENABLE - constexpr uint8_t expanderI2cAddress = 0x20; // I2C-address of PCA9555 (0x20 is true if PCA's pins A0+A1+A2 are pulled to GND) - #endif - - //################## BUTTON-Layout ################################## - /* German documentation: https://forum.espuino.de/t/das-dynamische-button-layout/224 - Please note the following numbers as you need to know them in order to define actions for buttons. - Even if you don't use all of them, their numbers won't change - 0: NEXT_BUTTON - 1: PREVIOUS_BUTTON - 2: PAUSEPLAY_BUTTON - 3: ROTARYENCODER_BUTTON - 4: BUTTON_4 - 5: BUTTON_5 - - Don't forget to enable/configure those buttons you want to use in your develboard-specific config (e.g. settings-custom.h) - - Single-buttons [can be long or short] (examples): - BUTTON_0_SHORT => Button 0 (NEXT_BUTTON) pressed shortly - BUTTON_3_SHORT => Button 3 (ROTARYENCODER_BUTTON) pressed shortly - BUTTON_4_LONG => Button 4 (BUTTON_4) pressed long - - Multi-buttons [short only] (examples): - BUTTON_MULTI_01 => Buttons 0+1 (NEXT_BUTTON + PREVIOUS_BUTTON) pressed in parallel - BUTTON_MULTI_12 => Buttons 1+2 (PREV_BUTTON + PAUSEPLAY_BUTTON) pressed in parallel - - Actions: - To all of those buttons, an action can be assigned freely. - Please have a look at values.h to look up actions available (>=100 can be used) - If you don't want to assign an action or you don't use a given button: CMD_NOTHING has to be set - */ - // *****BUTTON***** *****ACTION***** - #define BUTTON_0_SHORT CMD_PREVTRACK - #define BUTTON_1_SHORT CMD_NEXTTRACK - #define BUTTON_2_SHORT CMD_PLAYPAUSE - #define BUTTON_3_SHORT CMD_PLAYPAUSE - #define BUTTON_4_SHORT CMD_NOTHING - #define BUTTON_5_SHORT CMD_NOTHING - - #define BUTTON_0_LONG CMD_SEEK_BACKWARDS - #define BUTTON_1_LONG CMD_SEEK_FORWARDS - #define BUTTON_2_LONG CMD_PLAYPAUSE - #define BUTTON_3_LONG CMD_SLEEPMODE - #define BUTTON_4_LONG CMD_VOLUMEUP - #define BUTTON_5_LONG CMD_VOLUMEDOWN - - #define BUTTON_MULTI_01 CMD_MEASUREBATTERY //CMD_TOGGLE_WIFI_STATUS (disabled now to prevent children from unwanted WiFi-disable) - #define BUTTON_MULTI_02 CMD_NOTHING // CMD_ENABLE_FTP_SERVER - #define BUTTON_MULTI_03 CMD_NOTHING - #define BUTTON_MULTI_04 CMD_NOTHING - #define BUTTON_MULTI_05 CMD_NOTHING - #define BUTTON_MULTI_12 CMD_TELL_IP_ADDRESS - #define BUTTON_MULTI_13 CMD_NOTHING // CMD_TOGGLE_BLUETOOTH_MODE - #define BUTTON_MULTI_14 CMD_NOTHING - #define BUTTON_MULTI_15 CMD_NOTHING - #define BUTTON_MULTI_23 CMD_NOTHING // CMD_TOGGLE_WIFI_STATUS - #define BUTTON_MULTI_24 CMD_NOTHING - #define BUTTON_MULTI_25 CMD_NOTHING - #define BUTTON_MULTI_34 CMD_NOTHING - #define BUTTON_MULTI_35 CMD_NOTHING - #define BUTTON_MULTI_45 CMD_NOTHING - - //#################### Various settings ############################## - - // Serial-logging-configuration - #define SERIAL_LOGLEVEL LOGLEVEL_DEBUG // Current loglevel for serial console - - // Static ip-configuration - #ifdef STATIC_IP_ENABLE - #define LOCAL_IP 192,168,2,100 // ESPuino's IP - #define GATEWAY_IP 192,168,2,1 // IP of the gateway/router - #define SUBNET_IP 255,255,255,0 // Netmask of your network (/24 => 255.255.255.0) - #define DNS_IP 192,168,2,1 // DNS-server of your network; in private networks it's usually the gatewy's IP - #endif - - // Buttons (better leave unchanged if in doubts :-)) - constexpr uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons - constexpr uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of buttons - - //#define CONTROLS_LOCKED_BY_DEFAULT // If set the controls are locked at boot - #define INCLUDE_ROTARY_IN_CONTROLS_LOCK // If set the rotary encoder is locked if controls are locked - - // RFID-RC522 - #define RFID_SCAN_INTERVAL 100 // Interval-time in ms (how often is RFID read?) - - // Automatic restart - #ifdef SHUTDOWN_IF_SD_BOOT_FAILS - constexpr 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 - - // ESPuino will create a WiFi if joing existing WiFi was not possible. Name can be configured here. - constexpr const char accessPointNetworkSSID[] PROGMEM = "ESPuino"; // Access-point's SSID - - // Bluetooth - constexpr const char nameBluetoothSinkDevice[] PROGMEM = "ESPuino"; // Name of your ESPuino as Bluetooth-device - constexpr const char nameBluetoothSourceDevice[] PROGMEM = "My POGS Wireless Headphone"; // Name of Bluetooth-device to connect to (BT-Headset name) (https://forum.espuino.de/t/neues-feature-bluetooth-kopfhoerer/1293/) - - // Where to store the backup-file for NVS-records - constexpr const char backupFile[] PROGMEM = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done - constexpr const char playlistCacheFile[] PROGMEM = "playlistcache.csv"; // Filename that is used for caching playlists - - //#################### Settings for optional Modules############################## - // (optinal) Neopixel - #ifdef NEOPIXEL_ENABLE - #define NUM_LEDS 24 // number of LEDs - #define CHIPSET WS2812B // type of Neopixel - #define COLOR_ORDER GRB - #define PROGRESS_HUE_START 85 // Start and end hue of mulitple-LED progress indicator. Hue ranges from basically 0 - 255, but you can also set numbers outside this range to get the desired effect (e.g. 85-215 will go from green to purple via blue, 341-215 start and end at exactly the same color but go from green to purple via yellow and red) - #define PROGRESS_HUE_END -1 - //#define LED_OFFSET 0 // shifts the starting LED in the original direction of the neopixel ring - #endif - - #if defined(MEASURE_BATTERY_VOLTAGE) || defined(MEASURE_BATTERY_MAX17055) - #define BATTERY_MEASURE_ENABLE // Don't change. Set automatically if any method of battery monitoring is selected. - constexpr uint8_t s_batteryCheckInterval = 10; // How often battery is measured (in minutes) (can be changed via GUI!) - #endif - - #ifdef MEASURE_BATTERY_VOLTAGE - // (optional) Default-voltages for battery-monitoring via Neopixel; can be changed later via WebGUI - constexpr float s_warningLowVoltage = 3.4; // If battery-voltage is <= this value, a cyclic warning will be indicated by Neopixel (can be changed via GUI!) - constexpr float s_warningCriticalVoltage = 3.1; // If battery-voltage is <= this value, assume battery near-empty. Set to 0V to disable. - constexpr float s_voltageIndicatorLow = 3.0; // Lower range for Neopixel-voltage-indication (0 leds) (can be changed via GUI!) - constexpr float s_voltageIndicatorHigh = 4.2; // Upper range for Neopixel-voltage-indication (all leds) (can be changed via GUI!) - #endif - - #ifdef MEASURE_BATTERY_MAX17055 - constexpr float s_batteryLow = 15.0; // low percentage - constexpr float s_batteryCritical = 0.99; // critical percentage - - constexpr uint16_t s_batteryCapacity = 6000; // design cap of battery (mAh) - constexpr uint16_t s_emptyVoltage = 300; // empty voltage in 10mV - constexpr uint16_t s_recoveryVoltage = 348; // recovery voltage in 10mV - constexpr uint8_t s_batteryChemistry = 0x60; // 0 = Li-Ion, 0x20 = NCR, 0x60 = LiFePO4 - constexpr float s_resistSensor = 0.01; // current sense resistor, currently non-default values might lead to problems - constexpr bool s_vCharge = false; // true if charge voltage is greater than 4.275V - #endif - - // enable I2C if necessary - #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(PORT_EXPANDER_ENABLE) || defined(MEASURE_BATTERY_MAX17055) - #define I2C_2_ENABLE - #endif - - // (optinal) Headphone-detection (leave unchanged if in doubts...) - #ifdef HEADPHONE_ADJUST_ENABLE - constexpr uint16_t headphoneLastDetectionDebounce = 1000; // Debounce-interval in ms when plugging in headphone - #endif - - // Seekmode-configuration - constexpr uint8_t jumpOffset = 30; // Offset in seconds to jump for commands CMD_SEEK_FORWARDS / CMD_SEEK_BACKWARDS - - // (optional) Topics for MQTT - #ifdef MQTT_ENABLE - constexpr uint16_t mqttRetryInterval = 60; // Try to reconnect to MQTT-server every (n) seconds if connection is broken - constexpr uint8_t mqttMaxRetriesPerInterval = 1; // Number of retries per time-interval (mqttRetryInterval). mqttRetryInterval 60 / mqttMaxRetriesPerInterval 1 => once every 60s - #define DEVICE_HOSTNAME "ESP32-ESPuino" // Name that is used for MQTT - constexpr const char topicSleepCmnd[] PROGMEM = "Cmnd/ESPuino/Sleep"; - constexpr const char topicSleepState[] PROGMEM = "State/ESPuino/Sleep"; - constexpr const char topicRfidCmnd[] PROGMEM = "Cmnd/ESPuino/Rfid"; - constexpr const char topicRfidState[] PROGMEM = "State/ESPuino/Rfid"; - constexpr const char topicTrackState[] PROGMEM = "State/ESPuino/Track"; - constexpr const char topicTrackControlCmnd[] PROGMEM = "Cmnd/ESPuino/TrackControl"; - constexpr const char topicCoverChangedState[] PROGMEM = "State/ESPuino/CoverChanged"; - constexpr const char topicLoudnessCmnd[] PROGMEM = "Cmnd/ESPuino/Loudness"; - constexpr const char topicLoudnessState[] PROGMEM = "State/ESPuino/Loudness"; - constexpr const char topicSleepTimerCmnd[] PROGMEM = "Cmnd/ESPuino/SleepTimer"; - constexpr const char topicSleepTimerState[] PROGMEM = "State/ESPuino/SleepTimer"; - constexpr const char topicState[] PROGMEM = "State/ESPuino/State"; - constexpr const char topicCurrentIPv4IP[] PROGMEM = "State/ESPuino/IPv4"; - constexpr const char topicLockControlsCmnd[] PROGMEM ="Cmnd/ESPuino/LockControls"; - constexpr const char topicLockControlsState[] PROGMEM ="State/ESPuino/LockControls"; - constexpr const char topicPlaymodeState[] PROGMEM = "State/ESPuino/Playmode"; - constexpr const char topicRepeatModeCmnd[] PROGMEM = "Cmnd/ESPuino/RepeatMode"; - constexpr const char topicRepeatModeState[] PROGMEM = "State/ESPuino/RepeatMode"; - constexpr const char topicLedBrightnessCmnd[] PROGMEM = "Cmnd/ESPuino/LedBrightness"; - constexpr const char topicLedBrightnessState[] PROGMEM = "State/ESPuino/LedBrightness"; - constexpr const char topicWiFiRssiState[] PROGMEM = "State/ESPuino/WifiRssi"; - constexpr const char topicSRevisionState[] PROGMEM = "State/ESPuino/SoftwareRevision"; - #ifdef BATTERY_MEASURE_ENABLE - constexpr const char topicBatteryVoltage[] PROGMEM = "State/ESPuino/Voltage"; - constexpr const char topicBatterySOC[] PROGMEM = "State/ESPuino/Battery"; - #endif - #endif - - // !!! MAKE SURE TO EDIT PLATFORM SPECIFIC settings-****.h !!! - #if (HAL == 1) - #include "settings-lolin32.h" // Contains all user-relevant settings for Wemos Lolin32 - #elif (HAL == 2) - #include "settings-espa1s.h" // Contains all user-relevant settings for ESP32-A1S Audiokit - #elif (HAL == 3) - #include "settings-lolin_d32.h" // Contains all user-relevant settings for Wemos Lolin D32 - #elif (HAL == 4) - #include "settings-lolin_d32_pro.h" // Contains all user-relevant settings for Wemos Lolin D32 pro - #elif (HAL == 5) - #include "settings-ttgo_t8.h" // Contains all user-relevant settings for Lilygo TTGO T8 1.7 - #elif (HAL == 6) - #include "settings-complete.h" // Contains all user-relevant settings for ESPuino complete - #elif (HAL == 7) - #include "settings-lolin_d32_pro_sdmmc_pe.h" // Pre-configured settings for ESPuino Lolin D32 pro with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) - #elif (HAL == 8) - #include "settings-azdelivery_sdmmc.h" // Pre-configured settings for AZ Delivery ESP32 NodeMCU / Devkit C (https://forum.espuino.de/t/az-delivery-esp32-nodemcu-devkit-c-mit-sd-mmc-und-pn5180-als-rfid-leser/634) - #elif (HAL == 9) - #include "settings-lolin_d32_sdmmc_pe.h" // Pre-configured settings for Lolin D32 (non-pro) with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32-pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) - #elif (HAL == 10) - #include "settings-muse_proto.h" // Pre-configured settings for Raspiaudio ESPMuse Proto Board with I2C RFID Reader (https://raspiaudio.com/produit/muse-proto) - #elif (HAL == 99) - #include "settings-custom.h" // Contains all user-relevant settings custom-board - #endif - - //#define ENABLE_ESPUINO_DEBUG // Needs modification of platformio.ini (https://forum.espuino.de/t/rfid-mit-oder-ohne-task/353/21); better don't enable unless you know what you're doing :-) - #endif //settings_override + //######################### INFOS #################################### + // This is the general configfile for ESPuino-configuration. + + //################## HARDWARE-PLATFORM ############################### + /* Make sure to also edit the configfile, that is specific for your platform. + If in doubts (your develboard is not listed) use HAL 1 + 1: Wemos Lolin32 => settings-lolin32.h + 2: ESP32-A1S Audiokit => settings-espa1s.h + 3: Wemos Lolin D32 => settings-lolin_D32.h + 4: Wemos Lolin D32 pro => settings-lolin_D32_pro.h + 5: Lilygo T8 (V1.7) => settings-ttgo_t8.h + 6: ESPuino complete => settings-complete.h + 7: Lolin D32 pro SDMMC Port-Expander => settings-lolin_d32_pro_sdmmc_pe.h + 8: AZDelivery ESP32 NodeMCU => settings-azdelivery_sdmmc.h + 9: Lolin D32 SDMMC Port-Expander => settings-lolin_d32_sdmmc_pe.h + 99: custom => settings-custom.h + more to come... + */ + #ifndef HAL // Will be set by platformio.ini. If using Arduino-IDE you have to set HAL according your needs! + #define HAL 99 // HAL 1 = LoLin32, 2 = ESP32-A1S-AudioKit, 3 = Lolin D32, 4 = Lolin D32 pro; ... 99 = custom + #endif + + + //########################## MODULES ################################# + //#define PORT_EXPANDER_ENABLE // When enabled, buttons can be connected via port-expander PCA9555 (https://forum.espuino.de/t/einsatz-des-port-expanders-pca9555/306) + //#define I2S_COMM_FMT_LSB_ENABLE // Enables FMT instead of MSB for I2S-communication-format. Used e.g. by PT2811. Don't enable for MAX98357a, AC101 or PCM5102A) + #define MDNS_ENABLE // When enabled, you don't have to handle with ESPuino's IP-address. If hostname is set to "ESPuino", you can reach it via ESPuino.local + //#define MQTT_ENABLE // Make sure to configure mqtt-server and (optionally) username+pwd + #define FTP_ENABLE // Enables FTP-server; DON'T FORGET TO ACTIVATE AFTER BOOT BY PRESSING PAUSE + NEXT-BUTTONS (IN PARALLEL)! + #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 DE // DE = deutsch; EN = english + //#define STATIC_IP_ENABLE // DEPRECATED: Enables static IP-configuration (change static ip-section accordingly) + #define HEADPHONE_ADJUST_ENABLE // Used to adjust (lower) volume for optional headphone-pcb (refer maxVolumeSpeaker / maxVolumeHeadphone) and to enable stereo (if PLAY_MONO_SPEAKER is set) + #define PLAY_MONO_SPEAKER // If only one speaker is used enabling mono should make sense. Please note: headphones is always stereo (if HEADPHONE_ADJUST_ENABLE is active) + #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 MEASURE_BATTERY_MAX17055 // Enables battery-measurement via external fuel gauge (MAX17055) + #define SHUTDOWN_ON_BAT_CRITICAL // Whether to turn off on critical battery-level (only used if MEASURE_BATTERY_XXX is active) + //#define PLAY_LAST_RFID_AFTER_REBOOT // When restarting ESPuino, the last RFID that was active before, is recalled and played + #define USE_LAST_VOLUME_AFTER_REBOOT // Remembers the volume used at last shutdown after reboot + #define USEROTARY_ENABLE // If rotary-encoder is used (don't forget to review WAKEUP_BUTTON if you disable this feature!) + #define BLUETOOTH_ENABLE // If enabled and bluetooth-mode is active, you can stream to your ESPuino or to a headset via bluetooth (a2dp-sink & a2dp-source). Note: This feature consumes a lot of resources and the available flash/ram might not be sufficient. + //#define IR_CONTROL_ENABLE // Enables remote control (https://forum.espuino.de/t/neues-feature-fernsteuerung-per-infrarot-fernbedienung/265) + //#define PAUSE_WHEN_RFID_REMOVED // Playback starts when card is applied and pauses automatically, when card is removed (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) + //#define PAUSE_ON_MIN_VOLUME // When playback is active and volume is changed to zero, playback is paused automatically. Playback is continued if volume reaches 1. (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541) + #define DONT_ACCEPT_SAME_RFID_TWICE // RFID-reader doesn't accept the same RFID-tag twice in a row (unless it's a modification-card or RFID-tag is unknown in NVS). Flag will be ignored silently if PAUSE_WHEN_RFID_REMOVED is active. (https://forum.espuino.de/t/neues-feature-dont-accept-same-rfid-twice/1247) + #define SAVE_PLAYPOS_BEFORE_SHUTDOWN // When playback is active and mode audiobook was selected, last play-position is saved automatically when shutdown is initiated + #define SAVE_PLAYPOS_WHEN_RFID_CHANGE // When playback is active and mode audiobook was selected, last play-position is saved automatically for old playlist when new RFID-tag is applied + //#define HALLEFFECT_SENSOR_ENABLE // Support for hallsensor. For fine-tuning please adjust HallEffectSensor.h Please note: only user-support provided (https://forum.espuino.de/t/magnetische-hockey-tags/1449/35) + #define VOLUMECURVE 1 // 0=square, 1=logarithmic (1 is more flatten at lower volume) + + //################## set PAUSE_WHEN_RFID_REMOVED behaviour ############################# + #ifdef PAUSE_WHEN_RFID_REMOVED + #define ACCEPT_SAME_RFID_AFTER_TRACK_END // Accepts same RFID after playback has ended (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541/2) + #endif + + //################## select SD card mode ############################# + #define SD_MMC_1BIT_MODE // run SD card in SD-MMC 1Bit mode (using GPIOs 15 + 14 + 2 is mandatory!) + //#define SINGLE_SPI_ENABLE // If only one SPI-instance should be used instead of two (not yet working!) + //#define NO_SDCARD // enable to start without any SD card, e.g. for a webplayer only. SD card Settings above will be ignored + + + //################## select RFID reader ############################## + //#define RFID_READER_TYPE_MFRC522_SPI // use MFRC522 via SPI + //#define RFID_READER_TYPE_MFRC522_I2C // use MFRC522 via I2C + #define RFID_READER_TYPE_PN5180 // use PN5180 via SPI + + #ifdef RFID_READER_TYPE_MFRC522_I2C + #define MFRC522_ADDR 0x28 // default I2C-address of MFRC522 + #endif + + + #ifdef RFID_READER_TYPE_PN5180 + #define PN5180_ENABLE_LPCD // Wakes up ESPuino if RFID-tag was applied while deepsleep is active. Only ISO-14443-tags are supported for wakeup! + #endif + + #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(RFID_READER_TYPE_MFRC522_SPI) + constexpr uint8_t rfidGain = 0x07 << 4; // Sensitivity of RC522. For possible values see reference: https://forum.espuino.de/uploads/default/original/1X/9de5f8d35cbc123c1378cad1beceb3f51035cec0.png + #endif + + + //############# Port-expander-configuration ###################### + #ifdef PORT_EXPANDER_ENABLE + constexpr uint8_t expanderI2cAddress = 0x20; // I2C-address of PCA9555 (0x20 is true if PCA's pins A0+A1+A2 are pulled to GND) + #endif + + //################## BUTTON-Layout ################################## + /* German documentation: https://forum.espuino.de/t/das-dynamische-button-layout/224 + Please note the following numbers as you need to know them in order to define actions for buttons. + Even if you don't use all of them, their numbers won't change + 0: NEXT_BUTTON + 1: PREVIOUS_BUTTON + 2: PAUSEPLAY_BUTTON + 3: ROTARYENCODER_BUTTON + 4: BUTTON_4 + 5: BUTTON_5 + + Don't forget to enable/configure those buttons you want to use in your develboard-specific config (e.g. settings-custom.h) + + Single-buttons [can be long or short] (examples): + BUTTON_0_SHORT => Button 0 (NEXT_BUTTON) pressed shortly + BUTTON_3_SHORT => Button 3 (ROTARYENCODER_BUTTON) pressed shortly + BUTTON_4_LONG => Button 4 (BUTTON_4) pressed long + + Multi-buttons [short only] (examples): + BUTTON_MULTI_01 => Buttons 0+1 (NEXT_BUTTON + PREVIOUS_BUTTON) pressed in parallel + BUTTON_MULTI_12 => Buttons 1+2 (PREV_BUTTON + PAUSEPLAY_BUTTON) pressed in parallel + + Actions: + To all of those buttons, an action can be assigned freely. + Please have a look at values.h to look up actions available (>=100 can be used) + If you don't want to assign an action or you don't use a given button: CMD_NOTHING has to be set + */ + // *****BUTTON***** *****ACTION***** + #define BUTTON_0_SHORT CMD_PREVTRACK + #define BUTTON_1_SHORT CMD_NEXTTRACK + #define BUTTON_2_SHORT CMD_PLAYPAUSE + #define BUTTON_3_SHORT CMD_PLAYPAUSE + #define BUTTON_4_SHORT CMD_SEEK_BACKWARDS + #define BUTTON_5_SHORT CMD_SEEK_FORWARDS + + #define BUTTON_0_LONG CMD_SEEK_BACKWARDS + #define BUTTON_1_LONG CMD_SEEK_FORWARDS + #define BUTTON_2_LONG CMD_PLAYPAUSE + #define BUTTON_3_LONG CMD_SLEEPMODE + #define BUTTON_4_LONG CMD_VOLUMEUP + #define BUTTON_5_LONG CMD_VOLUMEDOWN + + #define BUTTON_MULTI_01 CMD_MEASUREBATTERY //CMD_TOGGLE_WIFI_STATUS (disabled now to prevent children from unwanted WiFi-disable) + #define BUTTON_MULTI_02 CMD_NOTHING // CMD_ENABLE_FTP_SERVER + #define BUTTON_MULTI_03 CMD_NOTHING + #define BUTTON_MULTI_04 CMD_NOTHING + #define BUTTON_MULTI_05 CMD_NOTHING + #define BUTTON_MULTI_12 CMD_TELL_IP_ADDRESS + #define BUTTON_MULTI_13 CMD_NOTHING + #define BUTTON_MULTI_14 CMD_NOTHING + #define BUTTON_MULTI_15 CMD_NOTHING + #define BUTTON_MULTI_23 CMD_NOTHING + #define BUTTON_MULTI_24 CMD_NOTHING + #define BUTTON_MULTI_25 CMD_NOTHING + #define BUTTON_MULTI_34 CMD_NOTHING + #define BUTTON_MULTI_35 CMD_NOTHING + #define BUTTON_MULTI_45 CMD_NOTHING + + //#################### Various settings ############################## + + // Serial-logging-configuration + #define SERIAL_LOGLEVEL LOGLEVEL_DEBUG // Current loglevel for serial console + + // DEPRECATED: This is now done using dynamic network configuration. + // If left, it is used for the automatic migration exactly once + // Static ip-configuration + #ifdef STATIC_IP_ENABLE + #define LOCAL_IP 192,168,2,100 // ESPuino's IP + #define GATEWAY_IP 192,168,2,1 // IP of the gateway/router + #define SUBNET_IP 255,255,255,0 // Netmask of your network (/24 => 255.255.255.0) + #define DNS_IP 192,168,2,1 // DNS-server of your network; in private networks it's usually the gatewy's IP + #endif + + // Buttons (better leave unchanged if in doubts :-)) + constexpr uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons + constexpr uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of buttons + + // Buttons active state: Default 0 for active LOW, 1 for active HIGH e.g. for TTP223 Capacitive Touch Switch Button (FinnBox) + #define BUTTON_0_ACTIVE_STATE 0 + #define BUTTON_1_ACTIVE_STATE 0 + #define BUTTON_2_ACTIVE_STATE 0 + #define BUTTON_3_ACTIVE_STATE 0 + #define BUTTON_4_ACTIVE_STATE 0 + #define BUTTON_5_ACTIVE_STATE 0 + + //#define CONTROLS_LOCKED_BY_DEFAULT // If set the controls are locked at boot + #define INCLUDE_ROTARY_IN_CONTROLS_LOCK // If set the rotary encoder is locked if controls are locked + + // RFID-RC522 + #define RFID_SCAN_INTERVAL 100 // Interval-time in ms (how often is RFID read?) + + // Automatic restart + #ifdef SHUTDOWN_IF_SD_BOOT_FAILS + constexpr 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 + + // timezone + // see list of valid timezones: https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv + // example for Europe/Berlin: "CET-1CEST,M3.5.0,M10.5.0/3" + // example for America/Toronto: "EST5EDT,M3.2.0,M11.1.0" + constexpr const char timeZone[] = "CET-1CEST,M3.5.0,M10.5.0/3"; // Europe/Berlin + + // ESPuino will create a WiFi if joing existing WiFi was not possible. Name and password can be configured here. + constexpr const char accessPointNetworkSSID[] = "ESPuino"; // Access-point's SSID + constexpr const char accessPointNetworkPassword[] = ""; // Access-point's Password, at least 8 characters! Set to an empty string to spawn an open WiFi. + + // Bluetooth + constexpr const char nameBluetoothSinkDevice[] = "ESPuino"; // Name of your ESPuino as Bluetooth-device + + // Where to store the backup-file for NVS-records + constexpr const char backupFile[] = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done + + //#################### Settings for optional Modules############################## + // (optinal) Neopixel + #ifdef NEOPIXEL_ENABLE + #define NUM_INDICATOR_LEDS 24 // number of Neopixel LEDs (formerly NUM_LEDS) + #define NUM_CONTROL_LEDS 0 // optional control leds (https://forum.espuino.de/t/statische-ws2812-leds/1703) + #define CONTROL_LEDS_COLORS {} // Colors for the control LEDs. Make sure it lists at least NUM_CONTROL_LEDS colors, e.g. for three control LEDs define: CONTROL_LEDS_COLORS {CRGB::Yellow, CRGB::Blue, 0xFFFFFF} (predefined colors: http://fastled.io/docs/3.1/struct_c_r_g_b.html) + #define CHIPSET WS2812B // type of Neopixel + #define COLOR_ORDER GRB + #define NUM_LEDS_IDLE_DOTS 4 // count of LEDs, which are shown when Idle + #define OFFSET_PAUSE_LEDS false // if true the pause-leds are centered in the mid of the LED-Strip + #define PROGRESS_HUE_START 85 // Start and end hue of mulitple-LED progress indicator. Hue ranges from basically 0 - 255, but you can also set numbers outside this range to get the desired effect (e.g. 85-215 will go from green to purple via blue, 341-215 start and end at exactly the same color but go from green to purple via yellow and red) + #define PROGRESS_HUE_END -1 + #define DIMMABLE_STATES 50 // Number of dimmed values between two full LEDs (https://forum.espuino.de/t/led-verbesserungen-rework/1739) + //#define LED_OFFSET 0 // shifts the starting LED in the original direction of the neopixel ring + #endif + + #if defined(MEASURE_BATTERY_VOLTAGE) || defined(MEASURE_BATTERY_MAX17055) + #define BATTERY_MEASURE_ENABLE // Don't change. Set automatically if any method of battery monitoring is selected. + constexpr uint8_t s_batteryCheckInterval = 10; // How often battery is measured (in minutes) (can be changed via GUI!) + #endif + + #ifdef MEASURE_BATTERY_VOLTAGE + // (optional) Default-voltages for battery-monitoring via Neopixel; can be changed later via WebGUI + constexpr float s_warningLowVoltage = 3.4; // If battery-voltage is <= this value, a cyclic warning will be indicated by Neopixel (can be changed via GUI!) + constexpr float s_warningCriticalVoltage = 3.1; // If battery-voltage is <= this value, assume battery near-empty. Set to 0V to disable. + constexpr float s_voltageIndicatorLow = 3.0; // Lower range for Neopixel-voltage-indication (0 leds) (can be changed via GUI!) + constexpr float s_voltageIndicatorHigh = 4.2; // Upper range for Neopixel-voltage-indication (all leds) (can be changed via GUI!) + #endif + + #ifdef MEASURE_BATTERY_MAX17055 + constexpr float s_batteryLow = 15.0; // low percentage + constexpr float s_batteryCritical = 0.99; // critical percentage + + constexpr uint16_t s_batteryCapacity = 6000; // design cap of battery (mAh) + constexpr uint16_t s_emptyVoltage = 300; // empty voltage in 10mV + constexpr uint16_t s_recoveryVoltage = 348; // recovery voltage in 10mV + constexpr uint8_t s_batteryChemistry = 0x60; // 0 = Li-Ion, 0x20 = NCR, 0x60 = LiFePO4 + constexpr float s_resistSensor = 0.01; // current sense resistor, currently non-default values might lead to problems + constexpr bool s_vCharge = false; // true if charge voltage is greater than 4.275V + #endif + + // enable I2C if necessary + #if defined(RFID_READER_TYPE_MFRC522_I2C) || defined(PORT_EXPANDER_ENABLE) || defined(MEASURE_BATTERY_MAX17055) + #define I2C_2_ENABLE + #endif + + // (optinal) Headphone-detection (leave unchanged if in doubts...) + #ifdef HEADPHONE_ADJUST_ENABLE + constexpr uint16_t headphoneLastDetectionDebounce = 1000; // Debounce-interval in ms when plugging in headphone + #endif + + // Seekmode-configuration + constexpr uint8_t jumpOffset = 30; // Offset in seconds to jump for commands CMD_SEEK_FORWARDS / CMD_SEEK_BACKWARDS + + // (optional) Topics for MQTT + #ifdef MQTT_ENABLE + constexpr uint16_t mqttRetryInterval = 60; // Try to reconnect to MQTT-server every (n) seconds if connection is broken + constexpr uint8_t mqttMaxRetriesPerInterval = 1; // Number of retries per time-interval (mqttRetryInterval). mqttRetryInterval 60 / mqttMaxRetriesPerInterval 1 => once every 60s + #define DEVICE_HOSTNAME "ESP32-ESPuino" // Name that is used for MQTT + constexpr const char topicSleepCmnd[] = "Cmnd/ESPuino/Sleep"; + constexpr const char topicSleepState[] = "State/ESPuino/Sleep"; + constexpr const char topicRfidCmnd[] = "Cmnd/ESPuino/Rfid"; + constexpr const char topicRfidState[] = "State/ESPuino/Rfid"; + constexpr const char topicTrackState[] = "State/ESPuino/Track"; + constexpr const char topicTrackControlCmnd[] = "Cmnd/ESPuino/TrackControl"; + constexpr const char topicCoverChangedState[] = "State/ESPuino/CoverChanged"; + constexpr const char topicLoudnessCmnd[] = "Cmnd/ESPuino/Loudness"; + constexpr const char topicLoudnessState[] = "State/ESPuino/Loudness"; + constexpr const char topicSleepTimerCmnd[] = "Cmnd/ESPuino/SleepTimer"; + constexpr const char topicSleepTimerState[] = "State/ESPuino/SleepTimer"; + constexpr const char topicState[] = "State/ESPuino/State"; + constexpr const char topicCurrentIPv4IP[] = "State/ESPuino/IPv4"; + constexpr const char topicLockControlsCmnd[] ="Cmnd/ESPuino/LockControls"; + constexpr const char topicLockControlsState[] ="State/ESPuino/LockControls"; + constexpr const char topicPlaymodeState[] = "State/ESPuino/Playmode"; + constexpr const char topicRepeatModeCmnd[] = "Cmnd/ESPuino/RepeatMode"; + constexpr const char topicRepeatModeState[] = "State/ESPuino/RepeatMode"; + constexpr const char topicLedBrightnessCmnd[] = "Cmnd/ESPuino/LedBrightness"; + constexpr const char topicLedBrightnessState[] = "State/ESPuino/LedBrightness"; + constexpr const char topicWiFiRssiState[] = "State/ESPuino/WifiRssi"; + constexpr const char topicSRevisionState[] = "State/ESPuino/SoftwareRevision"; + #ifdef BATTERY_MEASURE_ENABLE + constexpr const char topicBatteryVoltage[] = "State/ESPuino/Voltage"; + constexpr const char topicBatterySOC[] = "State/ESPuino/Battery"; + #endif + #endif + + // !!! MAKE SURE TO EDIT PLATFORM SPECIFIC settings-****.h !!! + #if (HAL == 1) + #include "settings-lolin32.h" // Contains all user-relevant settings for Wemos Lolin32 + #elif (HAL == 2) + #include "settings-espa1s.h" // Contains all user-relevant settings for ESP32-A1S Audiokit + #elif (HAL == 3) + #include "settings-lolin_d32.h" // Contains all user-relevant settings for Wemos Lolin D32 + #elif (HAL == 4) + #include "settings-lolin_d32_pro.h" // Contains all user-relevant settings for Wemos Lolin D32 pro + #elif (HAL == 5) + #include "settings-ttgo_t8.h" // Contains all user-relevant settings for Lilygo TTGO T8 1.7 + #elif (HAL == 6) + #include "settings-complete.h" // Contains all user-relevant settings for ESPuino complete + #elif (HAL == 7) + #include "settings-lolin_d32_pro_sdmmc_pe.h" // Pre-configured settings for ESPuino Lolin D32 pro with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) + #elif (HAL == 8) + #include "settings-azdelivery_sdmmc.h" // Pre-configured settings for AZ Delivery ESP32 NodeMCU / Devkit C (https://forum.espuino.de/t/az-delivery-esp32-nodemcu-devkit-c-mit-sd-mmc-und-pn5180-als-rfid-leser/634) + #elif (HAL == 9) + #include "settings-lolin_d32_sdmmc_pe.h" // Pre-configured settings for Lolin D32 (non-pro) with SDMMC + port-expander (https://forum.espuino.de/t/espuino-minid32-pro-lolin-d32-pro-mit-sd-mmc-und-port-expander-smd/866) + #elif (HAL == 99) + #include "settings-custom.h" // Contains all user-relevant settings custom-board + #endif + + //#define ENABLE_ESPUINO_DEBUG // Needs modification of platformio.ini (https://forum.espuino.de/t/rfid-mit-oder-ohne-task/353/21); better don't enable unless you know what you're doing :-) + #endif //settings_override #endif diff --git a/src/values.h b/src/values.h index c5b653f4..abe09db0 100644 --- a/src/values.h +++ b/src/values.h @@ -1,84 +1,93 @@ #pragma once // Operation Mode -#define OPMODE_NORMAL 0 // Normal mode -#define OPMODE_BLUETOOTH_SINK 1 // Bluetooth sink mode. Player acts as as bluetooth speaker. WiFi is deactivated. Music from SD and webstreams can't be played. -#define OPMODE_BLUETOOTH_SOURCE 2 // Bluetooth sourcemode. Player sennds audio to bluetooth speaker/headset. WiFi is deactivated. Music from SD and webstreams can't be played. +#define OPMODE_NORMAL 0 // Normal mode +#define OPMODE_BLUETOOTH_SINK 1 // Bluetooth sink mode. Player acts as as bluetooth speaker. WiFi is deactivated. Music from SD and webstreams can't be played. +#define OPMODE_BLUETOOTH_SOURCE 2 // Bluetooth sourcemode. Player sennds audio to bluetooth speaker/headset. WiFi is deactivated. Music from SD and webstreams can't be played. // Track-Control -#define NO_ACTION 0 // Dummy to unset track-control-command -#define STOP 1 // Stop play -#define PLAY 2 // Start play (currently not used) -#define PAUSEPLAY 3 // Pause/play -#define NEXTTRACK 4 // Next track of playlist -#define PREVIOUSTRACK 5 // Previous track of playlist -#define FIRSTTRACK 6 // First track of playlist -#define LASTTRACK 7 // Last track of playlist +#define NO_ACTION 0 // Dummy to unset track-control-command +#define STOP 1 // Stop play +#define PLAY 2 // Start play (currently not used) +#define PAUSEPLAY 3 // Pause/play +#define NEXTTRACK 4 // Next track of playlist +#define PREVIOUSTRACK 5 // Previous track of playlist +#define FIRSTTRACK 6 // First track of playlist +#define LASTTRACK 7 // Last track of playlist // Playmodes -#define NO_PLAYLIST 0 // If no playlist is active -#define SINGLE_TRACK 1 // Play a single track -#define SINGLE_TRACK_LOOP 2 // Play a single track in infinite-loop -#define SINGLE_TRACK_OF_DIR_RANDOM 12 // Play a single track of a directory and fall asleep subsequently -#define AUDIOBOOK 3 // Single track, can save last play-position -#define AUDIOBOOK_LOOP 4 // Single track as infinite-loop, can save last play-position -#define ALL_TRACKS_OF_DIR_SORTED 5 // Play all files of a directory (alph. sorted) -#define ALL_TRACKS_OF_DIR_RANDOM 6 // Play all files of a directory (randomized) -#define ALL_TRACKS_OF_DIR_SORTED_LOOP 7 // Play all files of a directory (alph. sorted) in infinite-loop -#define ALL_TRACKS_OF_DIR_RANDOM_LOOP 9 // Play all files of a directory (randomized) in infinite-loop -#define RANDOM_SUBDIRECTORY_OF_DIRECTORY 13 // Picks a random subdirectory from a given directory and do ALL_TRACKS_OF_DIR_SORTED -#define WEBSTREAM 8 // Play webradio-stream -#define LOCAL_M3U 11 // Plays items (webstream or files) with addresses/paths from a local m3u-file -#define BUSY 10 // Used if playlist is created - +#define NO_PLAYLIST 0 // If no playlist is active +#define SINGLE_TRACK 1 // Play a single track +#define SINGLE_TRACK_LOOP 2 // Play a single track in infinite-loop +#define SINGLE_TRACK_OF_DIR_RANDOM 12 // Play a single track of a directory and fall asleep subsequently +#define AUDIOBOOK 3 // Single track, can save last play-position +#define AUDIOBOOK_LOOP 4 // Single track as infinite-loop, can save last play-position +#define ALL_TRACKS_OF_DIR_SORTED 5 // Play all files of a directory (alph. sorted) +#define ALL_TRACKS_OF_DIR_RANDOM 6 // Play all files of a directory (randomized) +#define ALL_TRACKS_OF_DIR_SORTED_LOOP 7 // Play all files of a directory (alph. sorted) in infinite-loop +#define ALL_TRACKS_OF_DIR_RANDOM_LOOP 9 // Play all files of a directory (randomized) in infinite-loop +#define RANDOM_SUBDIRECTORY_OF_DIRECTORY 13 // Picks a random subdirectory from a given directory and do ALL_TRACKS_OF_DIR_SORTED +#define RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM 14 // Picks a random subdirectory from a given directory and do ALL_TRACKS_OF_DIR_RANDOM +#define WEBSTREAM 8 // Play webradio-stream +#define LOCAL_M3U 11 // Plays items (webstream or files) with addresses/paths from a local m3u-file +#define BUSY 10 // Used if playlist is created // RFID-modifcation-types -#define CMD_NOTHING 0 // Do Nothing -#define CMD_LOCK_BUTTONS_MOD 100 // Locks all buttons and rotary encoder -#define CMD_SLEEP_TIMER_MOD_15 101 // Puts uC into deepsleep after 15 minutes + LED-DIMM -#define CMD_SLEEP_TIMER_MOD_30 102 // Puts uC into deepsleep after 30 minutes + LED-DIMM -#define CMD_SLEEP_TIMER_MOD_60 103 // Puts uC into deepsleep after 60 minutes + LED-DIMM -#define CMD_SLEEP_TIMER_MOD_120 104 // Puts uC into deepsleep after 120 minutes + LED-DIMM -#define CMD_SLEEP_AFTER_END_OF_TRACK 105 // Puts uC into deepsleep after track is finished + LED-DIMM -#define CMD_SLEEP_AFTER_END_OF_PLAYLIST 106 // Puts uC into deepsleep after playlist is finished + LED-DIMM -#define CMD_SLEEP_AFTER_5_TRACKS 107 // Puts uC into deepsleep after five tracks + LED-DIMM -#define CMD_REPEAT_PLAYLIST 110 // Changes active playmode to endless-loop (for a playlist) -#define CMD_REPEAT_TRACK 111 // Changes active playmode to endless-loop (for a single track) -#define CMD_DIMM_LEDS_NIGHTMODE 120 // Changes LED-brightness -#define CMD_TOGGLE_WIFI_STATUS 130 // Toggles WiFi-status -#define CMD_TOGGLE_BLUETOOTH_SINK_MODE 140 // Toggles Normal/Bluetooth sink Mode -#define CMD_TOGGLE_BLUETOOTH_SOURCE_MODE 141 // Toggles Normal/Bluetooth source Mode -#define CMD_ENABLE_FTP_SERVER 150 // Enables FTP-server -#define CMD_TELL_IP_ADDRESS 151 // Command: ESPuino announces its IP-address via speech +#define CMD_NOTHING 0 // Do Nothing +#define CMD_LOCK_BUTTONS_MOD 100 // Locks all buttons and rotary encoder +#define CMD_SLEEP_TIMER_MOD_15 101 // Puts uC into deepsleep after 15 minutes + LED-DIMM +#define CMD_SLEEP_TIMER_MOD_30 102 // Puts uC into deepsleep after 30 minutes + LED-DIMM +#define CMD_SLEEP_TIMER_MOD_60 103 // Puts uC into deepsleep after 60 minutes + LED-DIMM +#define CMD_SLEEP_TIMER_MOD_120 104 // Puts uC into deepsleep after 120 minutes + LED-DIMM +#define CMD_SLEEP_AFTER_END_OF_TRACK 105 // Puts uC into deepsleep after track is finished + LED-DIMM +#define CMD_SLEEP_AFTER_END_OF_PLAYLIST 106 // Puts uC into deepsleep after playlist is finished + LED-DIMM +#define CMD_SLEEP_AFTER_5_TRACKS 107 // Puts uC into deepsleep after five tracks + LED-DIMM +#define CMD_REPEAT_PLAYLIST 110 // Changes active playmode to endless-loop (for a playlist) +#define CMD_REPEAT_TRACK 111 // Changes active playmode to endless-loop (for a single track) +#define CMD_DIMM_LEDS_NIGHTMODE 120 // Changes LED-brightness +#define CMD_TOGGLE_WIFI_STATUS 130 // Toggles WiFi-status +#define CMD_TOGGLE_BLUETOOTH_SINK_MODE 140 // Toggles Normal/Bluetooth sink Mode +#define CMD_TOGGLE_BLUETOOTH_SOURCE_MODE 141 // Toggles Normal/Bluetooth source Mode +#define CMD_TOGGLE_MODE 142 // Toggles Normal => Bluetooth sink => Bluetooth source => Normal Mode +#define CMD_ENABLE_FTP_SERVER 150 // Enables FTP-server +#define CMD_TELL_IP_ADDRESS 151 // Command: ESPuino announces its IP-address via speech +#define CMD_TELL_CURRENT_TIME 152 // Command: ESPuino announces current time via speech -#define CMD_PLAYPAUSE 170 // Command: play/pause -#define CMD_PREVTRACK 171 // Command: previous track -#define CMD_NEXTTRACK 172 // Command: next track -#define CMD_FIRSTTRACK 173 // Command: first track -#define CMD_LASTTRACK 174 // Command: last track -#define CMD_VOLUMEINIT 175 // Command: set volume to initial value -#define CMD_VOLUMEUP 176 // Command: increase volume by 1 -#define CMD_VOLUMEDOWN 177 // Command: lower volume by 1 -#define CMD_MEASUREBATTERY 178 // Command: Measure battery-voltage -#define CMD_SLEEPMODE 179 // Command: Go to deepsleep -#define CMD_SEEK_FORWARDS 180 // Command: jump forwards (time period to jump (in seconds) is configured via settings.h: jumpOffset) -#define CMD_SEEK_BACKWARDS 181 // Command: jump backwards (time period to jump (in seconds) is configured via settings.h: jumpOffset) -#define CMD_STOP 182 // Command: stops playback +#define CMD_PLAYPAUSE 170 // Command: play/pause +#define CMD_PREVTRACK 171 // Command: previous track +#define CMD_NEXTTRACK 172 // Command: next track +#define CMD_FIRSTTRACK 173 // Command: first track +#define CMD_LASTTRACK 174 // Command: last track +#define CMD_VOLUMEINIT 175 // Command: set volume to initial value +#define CMD_VOLUMEUP 176 // Command: increase volume by 1 +#define CMD_VOLUMEDOWN 177 // Command: lower volume by 1 +#define CMD_MEASUREBATTERY 178 // Command: Measure battery-voltage +#define CMD_SLEEPMODE 179 // Command: Go to deepsleep +#define CMD_SEEK_FORWARDS 180 // Command: jump forwards (time period to jump (in seconds) is configured via settings.h: jumpOffset) +#define CMD_SEEK_BACKWARDS 181 // Command: jump backwards (time period to jump (in seconds) is configured via settings.h: jumpOffset) +#define CMD_STOP 182 // Command: stops playback +#define CMD_RESTARTSYSTEM 183 // Command: restart System // Repeat-Modes -#define NO_REPEAT 0 // No repeat -#define TRACK 1 // Repeat current track (infinite loop) -#define PLAYLIST 2 // Repeat whole playlist (infinite loop) -#define TRACK_N_PLAYLIST 3 // Repeat both (infinite loop) +#define NO_REPEAT 0 // No repeat +#define TRACK 1 // Repeat current track (infinite loop) +#define PLAYLIST 2 // Repeat whole playlist (infinite loop) +#define TRACK_N_PLAYLIST 3 // Repeat both (infinite loop) // Seek-modes -#define SEEK_NORMAL 0 // Normal play -#define SEEK_FORWARDS 1 // Seek forwards -#define SEEK_BACKWARDS 2 // Seek backwards +#define SEEK_NORMAL 0 // Normal play +#define SEEK_FORWARDS 1 // Seek forwards +#define SEEK_BACKWARDS 2 // Seek backwards +#define SEEK_POS_PERCENT 3 // Seek to position (0-100) + +// TTS +#define TTS_NONE 0 // Do nothng (IDLE) +#define TTS_IP_ADDRESS 1 // Tell IP-address +#define TTS_CURRENT_TIME 2 // Tell current time // supported languages -#define DE 1 -#define EN 2 +#define DE 1 +#define EN 2 // Debug -#define PRINT_TASK_STATS 900 // Prints task stats (only debugging; needs modification of platformio.ini (https://forum.espuino.de/t/rfid-mit-oder-ohne-task/353/21)) +#define PRINT_TASK_STATS 900 // Prints task stats for debugging, needs CONFIG_FREERTOS_USE_TRACE_FACILITY=y and CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y in sdkconfig.defaults diff --git a/test/webpage/package-lock.json b/test/webpage/package-lock.json new file mode 100644 index 00000000..88786348 --- /dev/null +++ b/test/webpage/package-lock.json @@ -0,0 +1,1019 @@ +{ + "name": "express-test", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "express-test", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.1", + "express": "^4.18.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + }, + "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + } + } +} diff --git a/test/webpage/package.json b/test/webpage/package.json new file mode 100644 index 00000000..064ddcfd --- /dev/null +++ b/test/webpage/package.json @@ -0,0 +1,15 @@ +{ + "name": "express-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.1", + "express": "^4.18.2" + } +} diff --git a/test/webpage/server.js b/test/webpage/server.js new file mode 100644 index 00000000..21d29765 --- /dev/null +++ b/test/webpage/server.js @@ -0,0 +1,15 @@ +const express = require("express"); +const app = express(); + +app.use(express.static("../../html")); + +app.get("/", (req, res)=>{ + res.redirect("/management.html"); +}); + +const server = app.listen(8081, ()=>{ + const host = server.address().address; + const port = server.address().port; + + console.log("Test server listening at http://%s:%s", host, port); +}) \ No newline at end of file diff --git a/updateSdkConfig.py b/updateSdkConfig.py new file mode 100644 index 00000000..074510e4 --- /dev/null +++ b/updateSdkConfig.py @@ -0,0 +1,34 @@ +Import("env") +from pathlib import Path +import json + +default_file = "sdkconfig.defaults" +build_file = ".pio/lastBuild.json" + +# Get last modified timestamp of default file +default_path = Path(default_file) +if default_path.is_file(): + default_time = default_path.stat().st_mtime +else: + default_time = 0 + +# Check if default file has been modified since last build +build_path = Path(build_file) +if build_path.is_file(): + with open(build_file, "r") as f: + build_data = json.load(f) + last_timestamp = build_data.get("last_timestamp", 0) +else: + last_timestamp = 0 + build_data = {} # default value for build_data + +if default_time > last_timestamp: + # Delete all sdkconfig files except for sdkconfig.default + for file_path in Path(".").glob("sdkconfig.*"): + if file_path.name != default_file: + file_path.unlink() + + # Write last known timestamp to lastBuild.json + build_data["last_timestamp"] = default_time # update last_timestamp + with open(build_file, "w") as f: + json.dump(build_data, f) \ No newline at end of file