diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..0c8ea40c2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +/components/controls/buttons_usb_encoder/ @jeripeierSBB +/scripts/installscripts/buster-install-default.sh @jeripeierSBB +/scripts/installscripts/buster-install-default-with-autohotspot.sh @jeripeierSBB diff --git a/.github/ISSUE_TEMPLATE/future3.md b/.github/ISSUE_TEMPLATE/future3.md new file mode 100644 index 000000000..ab5d6bf00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/future3.md @@ -0,0 +1,19 @@ +--- +name: future3 Bug Report +about: Use this template to report bugs for the upcoming version 3 +title: "ISSUE SUMMARY on future3" +labels: future3, bug, needs triage +--- + +### Describe your problem + +Core, Web application ... + +#### What's your hardware set up? + +RPi version, RFID Reader, Audio devices etc. + +#### If possible, try to attach logs from ... (paths from RPi) + + * `~/RPi-Jukebox-RFID/shared/logs` -> General Jukebox logs + * `~/INSTALL-XXXXXXXXX.log` -> The logfile being generated when installing the Jukebox code diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..de5e8ce51 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "monthly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index df6b2837a..1a365a93c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/dockerimage.yml b/.github/workflows/dockerimage.yml index 5e29d663c..6cdce896a 100644 --- a/.github/workflows/dockerimage.yml +++ b/.github/workflows/dockerimage.yml @@ -1,6 +1,12 @@ name: Test Install Scripts on Docker -on: [push] +on: + push: + branches-ignore: + - 'future3/**' + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] jobs: @@ -9,10 +15,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build Docker image and run tests run: | docker build . --file ./ci/Dockerfile.buster.test_install.amd64 --tag rpi-jukebox-rfid-buster:latest docker run --rm -i rpi-jukebox-rfid-buster:latest /code/scripts/installscripts/tests/run_installation_tests.sh docker run --rm -i rpi-jukebox-rfid-buster:latest /code/scripts/installscripts/tests/run_installation_tests2.sh docker run --rm -i rpi-jukebox-rfid-buster:latest /code/scripts/installscripts/tests/run_installation_tests3.sh + - name: Build Docker image and run tests for alternate user hans + run: | + docker build . --file ./ci/Dockerfile.buster.test_install_altuser.amd64 --tag rpi-jukebox-rfid-buster-altuser:latest + docker run --rm -i rpi-jukebox-rfid-buster-altuser:latest /code/scripts/installscripts/tests/run_installation_tests_altuser.sh + docker run --rm -i rpi-jukebox-rfid-buster-altuser:latest /code/scripts/installscripts/tests/run_installation_tests2_altuser.sh + docker run --rm -i rpi-jukebox-rfid-buster-altuser:latest /code/scripts/installscripts/tests/run_installation_tests3_altuser.sh diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5e3184d9b..9462111c7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,6 +1,12 @@ name: Python Checks and Tests -on: [push] +on: + push: + branches-ignore: + - 'future3/**' + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] jobs: build: @@ -9,12 +15,12 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.5, 3.6, 3.7] + python-version: [3.7, 3.8, 3.9, 3.11] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml new file mode 100644 index 000000000..cf5005fd2 --- /dev/null +++ b/.github/workflows/pythonpackage_future3.yml @@ -0,0 +1,58 @@ +name: Python + Docs Checks and Tests + +on: + push: + branches: + - 'future3/**' + paths: + - '**.py' + - '**.py.*' + - 'docs/sphinx/**' + pull_request: + branches: + - 'future3/**' + paths: + - '**.py' + - '**.py.*' + - 'docs/sphinx/**' + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libasound2-dev + python -m pip install --upgrade pip + pip install wheel + pip install spidev + pip install -r requirements.txt + # For operation of the Jukebox, ZMQ must be compiled from sources due to Websocket support + # When just building the docs, the regular ZMQ package is sufficient + pip install -r docs/sphinx/requirements_pyzmq.txt + pip install -r docs/sphinx/requirements.txt + # Also install all optional dependencies + pip install -r src/jukebox/components/rfid/fake_reader_gui/requirements.txt + - name: Lint with flake8 + run: | + pip install flake8 + # Stop the build if linting fails + ./run_flake8.sh + - name: Build the docs + working-directory: ./docs/sphinx + run: | + # Stop the build if documentation cannot be built + # Treat all warnings as errors + sphinx-build -W --keep-going -T -a -E -b html . _build diff --git a/.gitignore b/.gitignore index 61fd2b9b6..04d9c5d32 100755 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ settings/version htdocs/config.php scripts/deviceName.txt scripts/gpio-buttons.py +settings/PhonieboxInstall.conf shared/* playlists/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f5767a92..eb1212ad1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,32 +8,32 @@ # Naming conventions * **Files & folder names** - * all **lower case** - * separate words with **dashes** `-` (less keystrokes, better autocomplete recognition, in HTML links dashes can not be confused) not camel/PascalCaps or underscores - * be **descriptive** in your wording (e.g. `raspberry`, not `juicy-red-thing`) - * move **from general to specific** (e.g. `food-fruit-raspberry`, not `raspberry-food-fruit`) - * unique and clear product IDs (e.g. MAX7219) - * the product ID should be written as is (no lowercase) - * the product ID should come last in a descriptive name (e.g. dot-matrix-module-MAX7219) - * be consistent and look at existing examples before you invent something new + * all **lower case** + * separate words with **dashes** `-` (less keystrokes, better autocomplete recognition, in HTML links dashes can not be confused) not camel/PascalCaps or underscores + * be **descriptive** in your wording (e.g. `raspberry`, not `juicy-red-thing`) + * move **from general to specific** (e.g. `food-fruit-raspberry`, not `raspberry-food-fruit`) + * unique and clear product IDs (e.g. MAX7219) + * the product ID should be written as is (no lowercase) + * the product ID should come last in a descriptive name (e.g. dot-matrix-module-MAX7219) + * be consistent and look at existing examples before you invent something new * **`README.md`** - * written in capital letters, so it's easier to spot - * every new folder of a component deserves a `README.md` file + * written in capital letters, so it's easier to spot + * every new folder of a component deserves a `README.md` file # Structure of files and folders Inside the root folder or the repo, these folders are important: * `scripts` - * this folder should contain **only actively used scripts** (controlling playout, rfid tiggers, etc.) - * some possible services and features might live in the *components* directory (see below) - * if one or more scripts are needed for the activation of a component (like daemons), they should be copied to the `scripts` directory during installation / activation - * WHY? By copying, changes will NOT affect the github repo and make it easier for users to modify their components + * this folder should contain **only actively used scripts** (controlling playout, rfid tiggers, etc.) + * some possible services and features might live in the *components* directory (see below) + * if one or more scripts are needed for the activation of a component (like daemons), they should be copied to the `scripts` directory during installation / activation + * WHY? By copying, changes will NOT affect the github repo and make it easier for users to modify their components * `components` - * contains sub- und subsubfolders for additional features, services, hardware - * **subfolders** are for categories (e.g. displays, soundcards) and are plural, even if there is only one - * **subsubfolders** are specific hardware, services, features, protocols, etc. + * contains sub- und subsubfolders for additional features, services, hardware + * **subfolders** are for categories (e.g. displays, soundcards) and are plural, even if there is only one + * **subsubfolders** are specific hardware, services, features, protocols, etc. # How to contribute @@ -52,7 +52,7 @@ Development is done on the git branch `develop`. How to move to that branch, see * Use the online line install script to get the box installed. * By default this will get you to the `master` branch. You will move to the `develop` branch, do this: -~~~ +~~~bash cd /home/pi/RPi-Jukebox-RFID git checkout develop git fetch origin @@ -62,7 +62,6 @@ git pull The preferred way of code contributions are [pull requests (follow this link for a small howto)](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github). And ideally pull requests using the "running code" on the `develop` branch of your Phoniebox. Alternatively, feel free to post tweaks, suggestions and snippets in the ["issues" section](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). - ## Making Changes * Create a topic branch from where you want to base your work. @@ -91,9 +90,10 @@ The preferred way of code contributions are [pull requests (follow this link for Update: This time without the need to create an extra random.txt file.and uptodate with the master branch. ~~~ + ## Making Trivial Changes -For changes of a trivial nature, it is not always necessary to create a new issue. +For changes of a trivial nature, it is not always necessary to create a new issue. In this case, it is appropriate to start the first line of a commit with one of `(docs)`, `(maint)`, or `(packaging)` instead of a ticket number. @@ -120,17 +120,18 @@ to detect in advance. If the code change results in a test failure, we will make our best effort to correct the error. If a fix cannot be determined and committed within 24 hours -of its discovery, the commit(s) responsible _may_ be reverted, at the -discretion of the committer and Phonie maintainers. -The original contributor will be notified of the revert. +of its discovery, the commit(s) responsible *may* be reverted, at the +discretion of the committer and Phonie maintainers. +The original contributor will be notified of the revert. ### Summary * Changes resulting in test failures will be reverted if they cannot be resolved within one business day. -## Guidelines ## -* Currently Phoniebox runs on Raspian **Buster** and **Stretch**. Therefore all Python code should work with **Python 3.5**. +## Guidelines + +* Currently Phoniebox runs on Raspian **Buster** . Therefore all Python code should work with **Python 3.7**. * For GPIO all code should work with **RPi.GPIO**. gpiozero is currently not intended to use. ## Additional Resources diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 62d9797e9..b1b0ab5f3 --- a/README.md +++ b/README.md @@ -1,50 +1,110 @@ +# Phoniebox: the RPi-Jukebox-RFID + ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/MiczFlor/RPi-Jukebox-RFID/develop) -![Python Tests](https://github.com/MiczFlor/RPi-Jukebox-RFID/workflows/Python%20package/badge.svg) ![Install Script Tests](https://github.com/MiczFlor/RPi-Jukebox-RFID/workflows/Docker%20Test%20Installation/badge.svg) +[![Python Checks and Tests](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/pythonpackage.yml/badge.svg)](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/pythonpackage.yml) [![Test Install Scripts on Docker](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/dockerimage.yml/badge.svg)](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/dockerimage.yml) [![CodeQL](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/codeql-analysis.yml) [![Gitter chat](https://badges.gitter.im/phoniebox/gitter.png)](https://gitter.im/phoniebox) -# Phoniebox: the RPi-Jukebox-RFID - A contactless jukebox for the Raspberry Pi, playing audio files, playlists, podcasts, web streams and spotify triggered by RFID cards. All plug and play via USB, no soldering iron needed. Update: if you must, it now also features a howto for adding GPIO buttons controls. -## The Phoniebox Calendar 2021 is here!!! +**IMPORTANT NOTICE REGARDING SPOTIFY INTEGRATION** + +>Spotify has [disabled access to libspotify on May 16, 2022](https://developer.spotify.com/community/news/2022/04/12/libspotify-sunset/). +This means Phoniebox can not communicate with Spotify via libspotify (and mopidy-spotify) anymore. +The problem is not our code but the cut off by Spotify. + +>We want Phoniebox users to be able to connect their box to their Spotify accounts and play their content. +The possibilities Spotify offers are -- at first glance -- not supporting an integration with Phoniebox. +Third party projects like [librespot-java](https://github.com/librespot-org/librespot-java) enter a grey zone regarding violation of Spotify's *Terms of Services* (ToS). For a potential Spotify re-integration, we are committed to a Spotify ToS compliant way of doing so (both in Version 2 and Version 3). This means looking into the (relatively new) Spotify Playback API, which is going to take an unknown amount of time. + +>This leaves us in a pickle and we are happy to hear from developer talent in the Phoniebox community on how to move forward regarding Phoniebox. +We are also curious to learn about alternative services we can connect with and which you would like to see supported -- or have developed support for already: + + +## The Phoniebox Calendar 2022 is here!!! + +Another bunch of wonderful designs! 2022 is the fourth Phoniebox calendar. If you are interested, you can see the [2019, 2020 and 2021 calendars in the docs folder](https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/docs). Download [the printable PDF of 2022 here](https://mi.cz/static/2022-Phoniebox-Calendar.pdf). + +![The 2022 Phoniebox Calendar](docs/2022-Phoniebox-Calendar.jpg "The 2022 Phoniebox Calendar") + +If you want to be featured on next years calendar, please make sure to add your Phoniebox pics to the [design thread here on github](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/639). + +--- + +## Install latest Phoniebox version + +If you are looking for **the latest stable version**, use the [install script for Raspberry Pi OS](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch). +If you dare to go where only few have gone before, **become an alpha-tester, bug reporter, contributor** for the exciting, totally re-written, up and coming, yet feature incomplete and unstable version 3 of the Phoniebox code base: + +## 🔥 Version 3 is coming ... + +### Newest release: V3.2.0 Beta -> 7. Feb 2022 + +* New GPIO backend based on GPIOZero including more sophisticated controls for inputs and outputs +* Bluetooth Headset support with device buttons +* Equalizer + Mono down mixer +* Localization support for Webapp, current languages supported are English and German (help us translate!) +* Timer support in Webapp and via RFID cards +* Additional system information in Webapp like Battery status, CPU temperature or IP address +* Preperation for Text-to-Speech + Read My IP as a first example +* Bugfixes for Install Script + +Over the last few months, a few Phoniebox fans started to think about a potential future of the Jukebox code. Version 2 is mature +and works well but doesn't scale enough for future development. It's the mix of Shell, Python and PHP. The goal was to tidy up the codebase, focus on a single programming language for the core (Python), establish a solid plugin system and build a responsive web client. [Read on here if you want to learn about more reasons](https://rpi-jukebox-rfid.readthedocs.io/en/latest/). + +### 👋 Looking for adopters, testers and contributors + +If you want to test or help develop this new version called `future3`, let us know what you think about the new architecture, the new web application and help us find bugs (or fix them proactively). -Another bunch of wonderful designs! 2021 is the third Phoniebox calendar. If you are interested, you can see the [2019 and 2020 calendars in the docs folder](https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/docs). Download [the printable PDF of 2021 here](https://mi.cz/static/2021-Phoniebox-Calendar.pdf). +While Version 3 is still under development, it is becoming a lot more stable! Although only some of the features from version 2.x have been ported to version 3 so far. -![The 2021 Phoniebox Calendar](docs/2021-Phoniebox-Calendar.jpg "The 2021 Phoniebox Calendar") +If you seek the adventure, your support will be more then welcome. Before contributing, check out the following references. -The year 2020 also has a clear *:star: community hero :star:*: @s-martin has been doing outstanding work for the Phoniebox community:sparkles:. Thanks to you and may 2021 be a wonderful year for you. +* 🚀 **[Install Jukebox Version 3 Beta](https://rpi-jukebox-rfid.readthedocs.io/en/latest/install.html)** +* 🐛 [Report a bug](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/new?assignees=&labels=future3%2C+bug%2C+needs+triage&template=future3.md&title=ISSUE+SUMMARY+on+future3) +* ☑️ [Feature Status](https://rpi-jukebox-rfid.readthedocs.io/en/latest/featurelist.html) +* 📖 [Documentation](https://rpi-jukebox-rfid.readthedocs.io/en/latest/) +* 👩‍💻 [Development](https://rpi-jukebox-rfid.readthedocs.io/en/latest/development_environment.html) +* 🦄 Code: [Release Branch](https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/main), [Development Branch](https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/future3/develop) + +--- ## Important updates / news * **Discussions forums** we use Github's Discussions feature for a more forum style. Please ask general questions in [Discussions](https://github.com/MiczFlor/RPi-Jukebox-RFID/discussions), bugs and enhancements should still be in [Issues](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). -* **Gitter Community** we got ourselves a gitter community; chat us up at https://gitter.im/phoniebox +* **Phoniebox [future3 Beta](https://rpi-jukebox-rfid.readthedocs.io/en/latest/) released (2022-02)** +* **Gitter Community** we got ourselves a gitter community; chat us up at -* **Phoniebox [2.2](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/4?closed=1) released (2020-11-23)** +* **Phoniebox [2.4](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/7?closed=1) released (2022-12-18)** -The [2.2](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/4?closed=1) release was pushed through the doors with many contributors (some of which in alphabetical order): @andreasbrett @BerniPi @juhrmann @Luegengladiator @MarkusProchaska @MarlonKrug @patrickweigelt @princemaxwell @RalfAlbers @s-martin @themorlan @veloxidSchweiz @xn--nding-jua. [List of all contributors](https://github.com/MiczFlor/RPi-Jukebox-RFID/graphs/contributors) +The [2.4](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/7?closed=1) release was pushed through the doors with many contributors: [List of all contributors](https://github.com/MiczFlor/RPi-Jukebox-RFID/graphs/contributors) -## What's new in version 2.2? +## What's new in version 2.4? + +* 🔥 Finally fixed the annoying `evdev` bug during installation (fixes e.g. #1721, #1653, #1618, #1501 and MANY more) +* Flexible PHP version in install script (makes sure Raspian Bullseye can be used) #1849 +* Publish "card swiped" event via MQTT #1496 +* Right now Spotify integration **still doesn't work** - please follow the discussion at -* :fire: **Fixed location of gpio_settings.ini** for [GPIO control](components/gpio_control/README.md) -* Added support for files with embedded chapters metada (like m4a) enhancement -* Added customizable poweroff command bash enhancement -* Finally fixed resume function... * Lots of fixed bugs and minor improvements... - * Status LED, Rotary Button, Volume Up/Down, custom music directory for +Spotify, Startup sound volume **What's still hot?** + +* [GPIO control](components/gpio_control/README.md) +* Added support for files with embedded chapters metada (like m4a) enhancement +* Added customizable poweroff command bash enhancement +* Support for PC/SC-readers * The constantly improved **one-line install script** handles both **Classic** and **+Spotify** when [setting up your Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command) - * integrated improved [GPIO control](components/gpio_control/README.md) - * integrated selection of RFID readers and uses [multiple readers](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/1012#issue-434052529) simultaneously - * features *non-interactive* installs based on a config file + * integrated improved [GPIO control](components/gpio_control/README.md) + * integrated selection of RFID readers and uses [multiple readers](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/1012#issue-434052529) simultaneously + * features *non-interactive* installs based on a config file * **[WiFi management](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#wifi-settings)** - * RFID cards to **toggle Wifi** (or switch it on/off) - * Read out the Wifi IP address (if you are connecting to a new network and don't know where to point your browser) + * RFID cards to **toggle Wifi** (or switch it on/off) + * Read out the Wifi IP address (if you are connecting to a new network and don't know where to point your browser) * **Hotspot** Phoniebox: [ad-hoc hotspot](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/967) if no known network found (IP: 10.0.0.5 SSID: phoniebox Password: PlayItLoud) -* **Touchscreen** LCD display Player (file: `index-lcd.php`in web app) +* **Touchscreen** LCD display Player (file: `index-lcd.php` in web app) * Integrate your [Phoniebox in your Smart Home](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Smart-Home-remote-control-with-MQTT). * Smoother [Web App running on ajax](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/623). * New [search form for local files](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/710) @@ -58,34 +118,24 @@ The [2.2](https://github.com/MiczFlor/RPi-Jukebox-RFID/milestone/4?closed=1) rel * Support for **[Spotify](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Spotify-FAQ)** and **[Google Play Music](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Enable-Google-Play-Music-GMusic)** integration. * **Podcasts!** More for myself than anybody else, I guess, I added the [podcast feature for Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#podcasts) (2018-05-09) * [Buttons](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons) and [knobs / dials](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Audio-RotaryKnobVolume) to control your **Phoniebox via GPIO**. - + ### Quick install - + [One line install script](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command) for Raspbian `buster` available. -* **MUST READ for users of [Phoniebox +Spotify Edition](docs/SPOTIFY-INTEGRATION.md)** -* This install script combines the two versions *Classic* and *+ Spotify*. +* **MUST READ for users of [Phoniebox +Spotify Edition](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Spotify-FAQ)** +* This install script combines the two versions *Classic* and *+ Spotify*. * *Phoniebox Classic* supports local audio, web radio, podcasts, YouTube (download and convert), GPIO and/or RFID Documentation can be found in the [GitHub wiki for Phoniebox](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki). Please try to add content in the wiki regarding special hardware, software tweaks and the like. -## The 2020 Phoniebox Calendar is out! - -Celebrating all the great designs of 2019, I put together a calendar for 2020, see picture above. If you want to be featured on next years calendar, please make sure to add your Phoniebox pics to the [design thread here on github](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/639). - -The PDF is about 6MB and will print well on A2 paper size, but it should also look good on larger poster sizes. Thanks to all the contributors, designers and makers. Have a good start into 2020 and keep up the good work! -![The 2020 Phoniebox Calendar](docs/2020-Phoniebox-Calendar.jpg "The 2020 Phoniebox Calendar") - -* [Download the 2020 Phoniebox Calendar PDF here](https://drive.google.com/file/d/1krb8G8Td1Vrf3sYWl44nZyuoJ0DIC5vX/view?usp=sharing) -* In case you missed it, [download the 2019 Phoniebox Calendar PDF here](https://drive.google.com/file/d/1NKlertLP0nIKOsHrcqu5pxe6NZU3SfS9/view?usp=sharing) - --- If you like your Phoniebox, consider to [buy me a coffee](https://www.buymeacoffee.com/MiczFlor) -or donate via [PayPal](https://www.paypal.com) to micz.flor@web.de using the *friends* option. +or donate via [PayPal](https://www.paypal.com) to micz.flor@web.de using the *friends* option. --- @@ -99,7 +149,7 @@ or donate via [PayPal](https://www.paypal.com) to micz.flor@web.de using the *fr | --- | --- | --- | |
Installation und Hardware
|
Web App and Audio / Spotify
|
The finished Phoniebox in action
| -A new video screencast about +A new video screencast about **What makes this Phoniebox easy to install and use:** @@ -126,13 +176,24 @@ The **web app** runs on any device and is mobile optimised. It provides: | | | | | | | | --- | --- | --- | --- | --- | --- | -| ![Caption](docs/img/gallery/Steph-20171215_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/Elsa-20171210_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/Geliras-20171228-Jukebox-01-h90.jpg "Caption") | ![Caption](docs/img/gallery/UlliH-20171210_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/KingKahn-20180101-Jukebox-01-h90.jpg "Caption") | ![Caption](docs/img/gallery/hailogugo-20171222-h90-01.jpg "Caption") | +| ![Caption](docs/img/gallery/Steph-20171215_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/Elsa-20171210_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/Geliras-20171228-Jukebox-01-h90.jpg "Caption") | ![Caption](docs/img/gallery/UlliH-20171210_h90-01.jpg "Caption") | ![Caption](docs/img/gallery/KingKahn-20180101-Jukebox-01-h90.jpg "Caption") | ![Caption](docs/img/gallery/hailogugo-20171222-h90-01.jpg "Caption") | **See more innovation, upcycling and creativity in the [Phoniebox Gallery](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/GALLERY) or visit and share the project's homepage at [phoniebox.de](http://phoniebox.de/). There is also an [english Phoniebox page](http://phoniebox.de/index.php?l=en).** +## Sustainability + +You might be surprised how easy and affordable you can get a RaspberryPi or an "appropriate" housing for your Phoniebox **second hand**. Think about the planet before you buy a new one. +Creating a Phoniebox may be sustainable for the following reasons: + +* You **buy things second hand** or **do upcycling on unused objects** and reduce newly produced products +* You built your Phoniebox yourself, so **maintaining and repairing is not a problem** (Additionally **a great community helps** you) +* Since the Phoniebox uses Linux as a base system it's **very unlikely that you run out of system and security updates** - so it can run and run and run... +* **RFID cards or tags can be reused - no need to buy new plastic elements** for changing the music or story linked to a card +* **Build it together with your kids** to show them that building things on their own is possible and in cooperation with others makes life easier and fun at the same time + ## Installation -* Installation instructions for Raspbian (https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch). +* Installation instructions for Raspbian (). * You can also use the [headless installation over ssh](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#ssh-install) straight from a fresh SD card. * For a quick install procedure, take a look at the [bash one line install script for stretch and buster](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command). This should get you started quickly. * If you choose the step by step installation, you need to walk through the configuration steps for [Stretch](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/CONFIGURE-stretch). @@ -161,16 +222,17 @@ Read the [CONTRIBUTING.md](CONTRIBUTING.md) file for [more infos on how to contr ## Reporting bugs -To make maintenance easier for everyone, please run the following script -and post the results when reporting a bug. +To make maintenance easier for everyone, please run the following script and post the results when reporting a bug. (Note: the results contain some personal information like IP or SSID. You might want to erase some of it before sharing with the bug report.) -~~~ + +~~~bash /home/pi/RPi-Jukebox-RFID/scripts/helperscripts/Analytics_AfterInstallScript.sh ~~~ + Just copy this line and paste it into your terminal on the pi. -If you find something that doesn't work. And you tried and tried again, but it still doesn't work, please report your issue in the ["issues" section](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). Make sure to include information about the system and hardware you are using, like: +If you find something that doesn't work. And you tried and tried again, but it still doesn't work, please report your issue in the ["issues" section](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). Make sure to include information about the system and hardware you are using, like: *Raspberry ZERO, OS Jessie, Card reader lists as (insert here) when running scripts/RegisterDevice.py, installed Phoniebox version 0.9.3 (or: using latest master branch).* @@ -227,12 +289,12 @@ Here is a list of equipment needed. You can find a lot second hand online (save ### RFID Reader and cards / fobs * RFID Card Reader (USB): [Neuftech USB RFID Reader ID](https://amzn.to/2RrqScm) using 125 kHz - make sure to buy compatible cards, RFID stickers or key fobs working with the same frequency as the reader. **Important notice:** the hardware of the reader that I had linked here for a long times seems to have changed and suddenly created problems with the Phoniebox installation. The reader listed now has worked and was recommended by two Phoniebox makers (2018 Oct 4). I can not guarantee that this will not change and invite you to give [RFID Reader feedback in this thread](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/231). - * RFID cards: [125 KHz EM4100](https://amzn.to/37pjy9q) make sure the frequency matches the RFID card reader !!! - * RFID fobs / key rings: [EM4100 RFID-Transponder-Schlüsselring, 125 KHz](https://amzn.to/3hsuvLO) make sure the frequency matches the RFID card reader !!! + * RFID cards: [125 KHz EM4100](https://amzn.to/37pjy9q) make sure the frequency matches the RFID card reader !!! + * RFID fobs / key rings: [EM4100 RFID-Transponder-Schlüsselring, 125 KHz](https://amzn.to/3hsuvLO) make sure the frequency matches the RFID card reader !!! + +* RFID Kit RC522: [RC522 Reader, Chip, Card for Raspberry Pi 13.56MHz] () + * RFID sticker / tags: [MIFARE RFID NFC Tags](https://amzn.to/30GfLDg) untested by me personally, but reported to work with work with RC522 and PN532 card readers. -* RFID Kit RC522: [RC522 Reader, Chip, Card for Raspberry Pi 13.56MHz] (https://amzn.to/2C7YZCZ) - * RFID sticker / tags: [MIFARE RFID NFC Tags](https://amzn.to/30GfLDg) untested by me personally, but reported to work with work with RC522 and PN532 card readers. - ### Speakers / amps * [USB Stereo Speaker Set (6 Watt, 3,5mm jack, USB-powered) black](http://amzn.to/2kXrard) | This USB powered speaker set sounds good for its size, is good value for money and keeps this RPi project clean and without the need of a soldering iron :) @@ -243,17 +305,17 @@ Here is a list of equipment needed. You can find a lot second hand online (save * [USB Interface for Arcade buttons](https://amzn.to/3nRAtIS) if you insist on not soldering hardware. (23rd Nov 2020: GPIO control script not yet part of the repo) * Arcade Buttons / Sensors (one of these might suit you) - * [Arcade Buttons / Schalter in various colours](https://amzn.to/2QMxe9r) if you want buttons for the GPIO control. - * [Arcade Buttons wit LED and custom icons](https://amzn.to/2MWQ6hq) as used by [@splitti](https://splittscheid.de/selfmade-phoniebox/#3C). - * [Set: Arcade Buttons / Tasten / Schalter ](https://amzn.to/2T81JTZ) GPIO Extension Board Starter Kit including cables and breadboard. - * [Touch Sensor / Kapazitive Touch Tasten ](https://amzn.to/2Vc4ntx) these are not buttons to press but to touch as GPIO controls. + * [Arcade Buttons / Schalter in various colours](https://amzn.to/2QMxe9r) if you want buttons for the GPIO control. + * [Arcade Buttons wit LED and custom icons](https://amzn.to/2MWQ6hq) as used by [@splitti](https://splittscheid.de/selfmade-phoniebox/#3C). + * [Set: Arcade Buttons / Tasten / Schalter](https://amzn.to/2T81JTZ) GPIO Extension Board Starter Kit including cables and breadboard. + * [Touch Sensor / Kapazitive Touch Tasten](https://amzn.to/2Vc4ntx) these are not buttons to press but to touch as GPIO controls. ### Special hardware -These are links to additional items, which will add an individual flavour to your Phoniebox setup. Consult the issue threads to see if your idea has been realised already. +These are links to additional items, which will add an individual flavour to your Phoniebox setup. Consult the issue threads to see if your idea has been realised already. -* [Ground Loop Isolator / Entstörfilter Audio](https://amzn.to/2Kseo0L) this seems to [get rid off crackles in the audio out (a typical RPi problem)](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/341) +* [Ground Loop Isolator / Entstörfilter Audio](https://amzn.to/2Kseo0L) this seems to [get rid off crackles in the audio out (a typical RPi problem)](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/341) * [Mechanical audio switch](https://amzn.to/35oOSCS) if you want to connect differen audio devices, this is the easiest way (in connection with the *Ground Loop Isolator* you will get good results) -* [Rotary Encoder / Drehregler / Dial](https://amzn.to/2J34guF) for volume control. Read here for more information on how to [integrate the rotary dial](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/267) +* [Rotary Encoder / Drehregler / Dial](https://amzn.to/2J34guF) for volume control. Read here for more information on how to [integrate the rotary dial](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/267) * [HiFiBerry DAC+ Soundcard](https://amzn.to/2J36cU9) Read here for more information on how to [HifiBerry Soundcard integration](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#hifiberry-dac-soundcard-details) * [HDMI zu HDMI + Optisches SPDIF mit 3,5-mm-Stereo-HDMI Audio-Extractor | HDMI zu SPDIF Konverter](https://amzn.to/2N8KP8C) If you plan to use video, this might be the better solution than a USB soundcard or the hifiberry. If takes up some space, but will work with the HDMI audio out and split the signal to deliver audio through 3.5mm jack. diff --git a/ci/Dockerfile.buster.test_install_altuser.amd64 b/ci/Dockerfile.buster.test_install_altuser.amd64 new file mode 100644 index 000000000..9def1fcbf --- /dev/null +++ b/ci/Dockerfile.buster.test_install_altuser.amd64 @@ -0,0 +1,33 @@ +FROM debian:buster + +COPY . /code +WORKDIR /code + +RUN groupadd --gid 1000 wurst ;\ + useradd -u 1000 -g 1000 -G sudo -d /home/hans -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' hans ;\ + chown -R 1000:1000 /code /home/hans ;\ + chmod +x /code/scripts/installscripts/buster-install-default.sh ;\ + chmod +x /code/scripts/installscripts/tests/run_installation_tests_altuser.sh ;\ + chmod +x /code/scripts/installscripts/tests/run_installation_tests2_altuser.sh ;\ + chmod +x /code/scripts/installscripts/tests/run_installation_tests3_altuser.sh + +RUN export DEBIAN_FRONTEND=noninteractive ;\ + apt-get update ;\ + apt-get -y install curl gnupg sudo nano systemd apt-utils;\ + echo 'deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi' > /etc/apt/sources.list.d/raspi.list ;\ + echo 'deb http://archive.raspberrypi.org/debian/ buster main' >> /etc/apt/sources.list.d/raspi.list ;\ + curl http://raspbian.raspberrypi.org/raspbian.public.key | apt-key add - ;\ + curl http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - ;\ + echo 'hans ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/hans ;\ + apt-get clean ;\ + rm -rf /var/cache/apt/* /var/lib/apt/lists/* + +RUN export DEBIAN_FRONTEND=noninteractive ;\ + apt-get update ;\ + apt-get -y dist-upgrade --auto-remove --purge ;\ + apt-get -y install wget build-essential git iw locales wpasupplicant;\ + apt-get clean ;\ + touch /boot/cmdlinetxt ;\ + rm -rf /var/cache/apt/* /var/lib/apt/lists/* + +USER hans diff --git a/ci/README.md b/ci/README.md index 2cabfd10b..9bb4e874f 100644 --- a/ci/README.md +++ b/ci/README.md @@ -11,7 +11,7 @@ This is a work in progress so expect things to fail or being flaky. * Flash its sd card with **raspbian buster lite** * use raspi-config to resize the filesystem to the whole sd card (menu: 7 -> A1) * install some tools and reboot: -``` +```bash sudo apt-get update sudo apt-get -y dist-upgrade sudo apt-get -y install docker.io git @@ -20,25 +20,25 @@ This is a work in progress so expect things to fail or being flaky. ``` * login to your RPi * clone this repo and cd into its local clone: -``` +```bash git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git cd /home/pi/RPi-Jukebox-RFID/ ``` * build the docker image: * **on normal PCs:** - ``` + ```bash docker build -t rpi-jukebox-rfid-stretch:latest -f ci/Dockerfile.stretch.amd64 . docker build -t rpi-jukebox-rfid-buster:latest -f ci/Dockerfile.buster.amd64 . ``` * **on a raspberry pi:** - ``` + ```bash docker build -t rpi-jukebox-rfid-stretch:latest -f ci/Dockerfile.stretch.armv7 . docker build -t rpi-jukebox-rfid-buster:latest -f ci/Dockerfile.buster.armv7 . ``` * get something to drink or eat * run the freshly built docker image and start testing. For example: - ``` + ```bash docker run --rm -ti rpi-jukebox-rfid-buster:latest /bin/bash cd /home/pi/ cp /code/scripts/installscripts/buster-install-default.sh /home/pi/ @@ -48,14 +48,13 @@ This is a work in progress so expect things to fail or being flaky. NOTE: Get familiar with docker and its flags - `--rm` for example will remove the container after you log out of it and all changes will be lost. - ### mount hosts code as volume The created image now contains all the code in the directory `/code` - if you do not want to rebuild the image after each code-change you can 'mount' the RPi's code version into the container: -``` +```bash git clone https://github.com/MiczFlor/RPi-Jukebox-RFID.git cd /home/pi/RPi-Jukebox-RFID/ docker build -t rpi-jukebox-rfid-buster:latest -f ci/Dockerfile . diff --git a/components/audio/PirateAudioHAT/README.md b/components/audio/PirateAudioHAT/README.md index 80975ef8d..44b308f14 100644 --- a/components/audio/PirateAudioHAT/README.md +++ b/components/audio/PirateAudioHAT/README.md @@ -26,7 +26,8 @@ NOTE: changes to the installation should find their way into the script `setup_p `gpio=25=op,dh` `dtoverlay=hifiberry-dac` 5. Add settings to /etc/asound.conf (create it, if it does not exist yet) - ``` + + ```bash pcm.hifiberry { type softvol slave.pcm "plughw:CARD=sndrpihifiberry,DEV=0" @@ -42,8 +43,10 @@ NOTE: changes to the installation should find their way into the script `setup_p card 1 } ``` + 6. Add the following section to /etc/mpd.conf - ``` + + ```bash audio_output { enabled "yes" type "alsa" @@ -55,6 +58,7 @@ NOTE: changes to the installation should find their way into the script `setup_p dop "no" } ``` + 7. Set mixer_control name in /etc/mpd.conf `mixer_control "Master"` 8. Enable SPI @@ -64,7 +68,8 @@ NOTE: changes to the installation should find their way into the script `setup_p 10. Install Mopidy plugins `sudo pip3 install Mopidy-PiDi pidi-display-pil pidi-display-st7789 mopidy-raspberry-gpio` 11. Add the following sections to /etc/mopidy/mopidy.conf: - ``` + + ```bash [raspberry-gpio] enabled = true bcm5 = play_pause,active_low,150 @@ -76,6 +81,7 @@ NOTE: changes to the installation should find their way into the script `setup_p enabled = true display = st7789 ``` + 12. Enable access for modipy user `sudo usermod -a -G spi,i2c,gpio,video mopidy` 13. Reboot Raspberry Pi diff --git a/components/bluetooth-sink-switch/README.md b/components/bluetooth-sink-switch/README.md new file mode 100644 index 000000000..16fabde10 --- /dev/null +++ b/components/bluetooth-sink-switch/README.md @@ -0,0 +1,283 @@ +# Neatly switch between soundcard and bluetooth audio output + +**Wouldn't it be nice to have regular speakers and Bluetooth headphones on your Phoniebox and choose the desired output on a spur of the moment?** + +This component provides a mechanism to toggle between both audio sinks through all the usual user interfaces (i.e. GPIO, RFID Card Swipe, Web Interface). The current status is reflected in the Web Interface and through an optional LED. + +**Convinced? So, what is the vision?** + +When a user powers on their Bluetooth headphones, they connect automatically to the Phoniebox. At the switch of the button (or card swipe, etc) the (already running) audio playback is transferred from the speakers to the headphones. This happens almost seamlessly. Parents feel an instant wave of relief at not having to listen to the 500th iteration of this month favourite song. The small user feels instantly proud at having working headphones much like the mom/dad always uses while doing home-office online meetings. If no Bluetooth headphones are connected, the audio sink toggle request defaults to speakers. An LED indicates the currently active audio sink. + +If the feature [bluetooth-buttons](../controls/buttons-bluetooth-headphone) is enabled the audio stream is automatically switched over to bluetooth on connect and back to speakers on disconnect. + +If no bluetooth device is connected, the output defaults back to speakers. After boot-up the output is always speakers to make sure start-up sound are audiable and to avoid confusion. + +![Web Interface Add-on in settings.php](webif.png "Web Interface Add-on in settings.php") + +**Limitations** + +This feature only works for the *Classic* Edition. Why? It relies on the mpd multiple output channels feature to switch between outputs. This is no available in mopidy, which is used in the Spotify Edition. + +### Installation + +This looks lengthy, but I the major deal is setting up your audio output devices. I have been rather explicit to avoid confusion. + +#### Step 1) Setting up asound.conf + +You need to set up both audio sinks and make sure they work. This is pretty much a prerequisite for everything that follows. + +Follow the instructions for your soundcard. Configure `/etc/asound.conf`correctly. And make sure it works! + +Then follow the instructions on the [Wiki on how to connect the bluetooth device](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Bluetooth). We diverge where we set up two audio sinks instead of one: Just **add** the `pcm.btspeaker` section described in the wiki to `/etc/asound.conf` (choose a name to your liking). Do **not** touch the mpd.conf yet! + +The new entry should end up looking like this: + +~~~bash +pcm.btheadphone { + type plug + slave.pcm { + type bluealsa + service "org.bluealsa" + device "C4:FB:20:63:A7:F2" + profile "a2dp" + delay -20000 + } + hint { + show on + description "Gesas Headphones" + } +} +~~~ + +In case of doubt, reboot. + +Test the new audio device (mine is called `pcm.btheadphone`). Also test the soundcard (here `pcm.hifiberry` for the regular speakers). + +~~~bash +$ aplay -D btheadphone /usr/share/sounds/alsa/Front_Center.wav +$ aplay -D hifiberry /usr/share/sounds/alsa/Front_Center.wav +~~~ + +#### Step 2) Setting up mpd.conf + +You need to set up two audio_output sections. **The order is important**: the first entry must relate to the soundcard setup, the second entry must relate to the bluetooth setup. Give meaningful names, as they will show up in the Web Interface. + +~~~bash +# The first entry should match your soundcard. If you have a working setup, there is most likly no need to change it! +# As an exanple, here is my configuration for my HifiBerry MiniAmp. +audio_output { + type "alsa" + name "HifiBerry Speakers" # This name will show up in the Web Interface Status + device "hifiberry" # This is the pcm.hifiberry device from the asound.conf. + # If you did not specify a name, but use the 'default', delete this entry + auto_resample "no" # Depends on your asound.conf. In doubt delete this line + mixer_control "Master" # This is the iFace name, you will recognize from your Phoniebox installation +} + +# The second entry belongs to the bluetooth device +audio_output { + type "alsa" + name "Gesas Headphones" # This name will show up in the Web Interface Status + device "btheadphone" # This is the pcm.btheadphone device from the asound.conf + mixer_type "software" + auto_resample "no" + auto_format "no" + enabled "no" # Default is disabled, keepin the soundcard as primary output +} +~~~ + +Restarting the mpd.service or else reboot. + +Check the setup: + +~~~sh +$ mpc outputs +Output 1 (HifiBerry Speakers) is disabled +Output 2 (Gesas Headphones) is enabled +~~~ + +You may switch with `$ mpc enable only 1` and `$ mpc enable only 2`. Play some music and use these commands to check you mpd configuration. You should be able to switch the audio output between the two devices. + +#### Step 3) Run the installer + +This sets up the appropriate user rights and registers the component with global settings etc. + +~~~sh +$ cd components/bluetooth-sink-switch +$ ./install-bt-sink-switch.sh +~~~ + +#### Step 4) Fine-tuning + +**Status LED** + +An optional status LED will be turned on if the audio sink is set to bluetooth. If a toggle command is issued, but no bluetooth device is connected, the LED will blink three times. Looks very neat, if you have a button with integrated LED. Add these lines to your `RPi-Jukebox-RFID/settings/gpio_settings.ini`, to use GPIO 13 as LED signal. It is `led_pin` the BCM number of the GPIO pin (i.e. 'led_pin = 13' means GPIO13) and defaults to None. Create the file, if it does not exist. + +**Important note**: Correct capitalization of [BluetoothToggleLed] is important! + +~~~bash +[BluetoothToggleLed] +enabled: True +led_pin: 13 +~~~ + +**GPIO control** + +If you want to toggle from a GPIO button (e.g. on GPIO5), add these lines to your `RPi-Jukebox-RFID/settings/gpio_settings.ini`. Create it, if it does not exist. + +~~~bash +[BluetoothToggle] +enabled: True +Type: Button +Pin: 5 +pull_up: True +hold_time: 0.3 +functionCall: functionCallBluetoothToggle +~~~ + +**RFID Card** + +If you want to toggle by RFID card swipe, set the card id in `rfid_trigger_play.conf`, e.g.: + +~~~bash +### Toggle between speakers and bluetooth headphones +CMDBLUETOOTHTOGGLE="1364237231134" +~~~ + +**Volume attenuation** + +Speakers and Headphones can have very different maximum volume levels. This sometimes leads to very strong volume level changes when switching between speakers and headphones. Restricting the maximum volume with the Phoniebox-integrated max-volume setting does no yield the desired effect, as this is a single setting and does not differentiate between different audio sinks. + +The solution is adding a `softvol` component to the /etc/asound.conf. You may already have one set up, if your soundcard does not have a hardware volume control. Then it is easy! The `softvol` copmonent adds a systemwide ALSA-based volume control for a hardware soundcard. You will need to give it a name, that does **not** exist! Check with `$ amixer scontrols` first, which names are already taken. Here, I have choosen *Master*. This will work even if your soundcard has a hardware volume control. + +The `softvol` component has a feature called *max_db* to limit the maximum volume, which we are going to utilize here. With that we are limiting the maximum volume of the speakers systemwide and independent of MPD or other Phoniebox settings. + +~~~bash +# Add the sofvol section +pcm.hifiberry { + type softvol + slave.pcm "plughw:0,0" # Your audio output. Here: audio stream goes direclty to the soundcard 0 + control.name "Master" # Unique, new iFace name not already used in the system + control.card 0 # Be sure to also adjust the index to your soundcard number + # i.e. if your soundcard is plughw:1,0 this should also be 1 + max_dB -20.0 # This limits the maximum speaker volume. + # Play around with this number until your are satisfied with the + # volume change effect when switching between headphones and speakers + hint { + show on + description "HifiBerry Speakers" + } +} + +# Setup the default device +pcm.!default { + type plug + slave.pcm "hifiberry" +} + +ctl.!default { + type hw card 0 +} +~~~ + +**Attention:** This changes the iFace name! The new iFace name is now *Master*. You will need to adjust your mpd.conf in two places + +- Change the mixer control to the new iFace name: `mixer_control "Master"` +- Ensure that it is there is no entry mixer_type or that it is `mixer_type "hardware"` + +See example [mpd.conf](#step-2-setting-up-mpdconf) above! + +Also change the Phoniebox setting with + +~~~sh +$ echo "Master" > RPi-Jukebox-RFID/settings/Audio_iFace_Name +~~~ + +and reboot! + +Test the setup with running speaker test in one console + +~~~bash +$ speaker-test -D hifiberry +~~~ + +and changing the default volume control in another console + +~~~bash +$ alsamixer +~~~ + +If you are experimenting with a softvol and want to get rid of it again - that is not an easy task. Most promising approach is to insert the SD-Card into a different Linux machine delete the file `/var/lib/alsa/asound.state`. This must be done from a different computer, as this file gets written during shutdown. More infos about the softvol may be found [here](https://alsa.opensrc.org/Softvol) + +## Troubleshooting + +Troubleshooting comes in three major sub-tasks: + +- Step 1) Ensure that your asound.conf and mpd.conf configurations are working correclty by performing the checks described in the respective sections +- Step 2) Check that the script for stream toggling works. Call the script manually from console with debug output: `$ ./RPi-Jukebox-RFID/components/bluetooth-sink-switch/bt-sink-switch.py toggle debug`. And analyze the output +- Step 3) Check the actual user interface that you are going to use (e.g. GPIO buttons) + +## Some background + + + + + + + +## Example config + +For reference, this is my /etc/asound.conf in full (it also sets up an equalizer). The corresponding [mpd.conf](#step-2-setting-up-mpdconf) excerpt is the one given above. + +~~~bash +pcm.btheadphone { + type plug + slave.pcm { + type bluealsa + service "org.bluealsa" + device "00:1B:66:A1:56:8D" + profile "a2dp" + delay -20000 + } + hint { + show on + description "Gesas Headphones" + } +} + +pcm.hifiberry { + type softvol + slave.pcm equal + control.name "Master" + control.card 0 + max_dB -20.0 + hint { + show on + description "HifiBerry Speakers" + } +} + +pcm.!default { + type asym + playback.pcm "hifiberry" +} + + +ctl.!default { + type hw card 0 +} + +ctl.equal { + type equal; +} + +pcm.equalcore { + type equal; + slave.pcm "plughw:0,0"; +} + +pcm.equal { + type plug; + slave.pcm equalcore; +} +~~~ diff --git a/components/bluetooth-sink-switch/bt-sink-switch.py b/components/bluetooth-sink-switch/bt-sink-switch.py new file mode 100755 index 000000000..a107703a7 --- /dev/null +++ b/components/bluetooth-sink-switch/bt-sink-switch.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Provides bt_switch (see below) as function and callable script + +If called as script, the configuration of led_pin reflecting audio sink status is read from ../../settings/gpio_settings.ini' +See function get_led_pin_configuration for details. If no configuration file is found led_pin is None + +Usage: +$ bt-sink-switch cmd [debug] + cmd = toggle|speakers|headphones : select audio target + debug : enable debug logging +""" + +import sys +import re +import subprocess +import logging +import os +import configparser + + +# Create logger +logger = logging.getLogger('bt-sink-switch.py') +logger.setLevel(logging.DEBUG) +# Create console handler and set default level +logconsole = logging.StreamHandler() +logconsole.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S')) +logconsole.setLevel(logging.INFO) +logger.addHandler(logconsole) + + +def bt_usage(sname): + """Print usage, if module is called as script""" + print("Usage") + print(" ./" + sname + " toggle | speakers | headphones [debug]") + + +def bt_check_mpc_err() -> None: + """Error check on mpd output stream and attempt to recover previous state""" + logger.debug("bt_check_mpc_err()") + mpcproc = subprocess.run("mpc status", shell=True, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(mpcproc.stdout) + # grep for this expression: 'ERROR: Failed to open audio output' + mpcerr = re.search(b"ERROR:.*output", mpcproc.stdout) + if mpcerr is not None: + mpcplay = subprocess.run("mpc play", shell=True, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(mpcplay) + + +def bt_switch(cmd, led_pin=None): + """ + Set/Toggle between regular speakers and headphone output. If no bluetooth device is connected, always defaults to mpc output 1 + + To be precise: toggle between mpc output 1 and mpc output 2. + + So, set up your /etc/mpd.conf correctly: first audio_output section should be speakers, second audio_output section should be headphones + To set up bluetooth headphones, follow the wiki + Short guide to connect bluetooth (without audio setup) + sudo bluetoothctl + power on + agent on + scan on -> shows list of Bluetooth devices in reach + pair C4:FB:20:63:A7:F2 -> pairing happens + trust C4:FB:20:63:A7:F2 -> trust you device + connect C4:FB:20:63:A7:F2 -> connect + scan off + exit + Next time headphones are switched on, they should connect automatically + + Requires + sudo apt install bluetooth + + Attention + The user to runs this script (precisly who runs bluetoothctl) needs proper access rights. Otherwise bluetoothctl will always return "no default controller found" + The superuser and users of group "bluetooth" have these. You can check the policy here + /etc/dbus-1/system.d/bluetooth.conf + Best to check first if the user which later runs this script can execute bluetoothctl and get meaningful results + sudo -u www-data bluetoothctl show + E.g. if you want to do bluetooth manipulation from the web interface, you will most likely need to add www-data to the group bluetooth + if you want to test this script from the command line, you will most likely need to add user pi (or whoever you are) to the group bluetooth or run it as superuser + sudo usermod -G bluetooth -a www-data + Don't forget to reboot for group changes to take effect here + + LED support + If LED number (GPIO number, BCM) is provided, a LED is switched to reflect output sink status + off = speakers, on = headphones + LED blinks if no bluetooth device is connected and bluetooth sink is requested, before script default to output 1 + + A note for developers: This script is not persistent and only gets called (from various sources) when the output sink is changed/toggled and exits. + This is done to make is callable from button press (gpio button handler), rfid card number, web interface + The LED state however should be persistent. With GPIOZero, the LED state gets reset at the end of the script. For that reason GPIO state is manipulated through shell commands + + Parameters + ---------- + :param cmd: string is "toggle" | "speakers" | "headphones" + :param led_pin: integer with GPIO pin number of LED to reflect output status. If None, LED support is disabled (and no GPIO pin is blocked) + """ + # Check for valid command + if cmd != "toggle" and cmd != "speakers" and cmd != "headphones": + logger.error("Invalid command. Doing nothing.") + return + + # Rudimentary check if LED pin request is valid GPIO pin number + if led_pin is not None: + if led_pin < 2 or led_pin > 27: + led_pin = None + logger.error("Invalid led_pin. Ignoring led_pin = " + str(led_pin)) + + if led_pin is not None: + # Set-up GPIO LED pin if not already configured. If it already exists, sanity check direction of pin before use + try: + with open("/sys/class/gpio/gpio" + str(led_pin) + "/direction") as f: + if f.readline(3) != "out": + logger.error("LED pin already in use with direction 'in'. Ignoring led_pin = " + str(led_pin)) + led_pin = None + except FileNotFoundError: + # GPIO direction file does not exist -> pin is not configured. Set it up (sleep is necessary!) + proc = subprocess.run("echo " + str(led_pin) + " > /sys/class/gpio/export; \ + sleep 0.1; \ + echo out > /sys/class/gpio/gpio" + str(led_pin) + "/direction", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(proc.stdout) + + # Figure out if output 1 (speakers) is enabled + isSpeakerOn_console = subprocess.run("mpc outputs", shell=True, check=False, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + logger.debug(isSpeakerOn_console.stdout) + isSpeakerOn = re.search(b"^Output 1.*enabled", isSpeakerOn_console.stdout) + + # Figure out if a bluetooth device is connected (any device will do). Assume here that only speakers/headsets will be connected + # -> No need for user to adapt MAC address + # -> will actually support multiple speakers/headsets paired to the phoniebox + # Alternative: Check for specific bluetooth device only with "bluetoothctl info MACADDRESS" + isBtConnected_console = subprocess.run("bluetoothctl info", shell=True, check=False, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + logger.debug(isBtConnected_console.stdout) + isBtConnected = re.search(b"Connected:\s+yes", isBtConnected_console.stdout) + + if (cmd == "toggle" and isSpeakerOn) or (cmd == "headphones"): + # Only switch to BT headphones if they are actually connected + if isBtConnected: + print("Switched audio sink to \"Output 2\"") + # With mpc enable only 2, output 1 gets disabled before output 2 gets enabled causing a stream output fail + # This order avoids the issue + proc = subprocess.run("mpc enable 2; sleep 0.1; mpc disable 1", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(proc.stdout) + # Yet, in some cases, a stream error still occurs: check and recover + bt_check_mpc_err() + if led_pin is not None: + proc = subprocess.run("echo 1 > /sys/class/gpio/gpio" + str(led_pin) + "/value", shell=True, + check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(b'LED on: ' + proc.stdout) + return + else: + print("No bluetooth device connected. Defaulting to \"Output 1\".") + if led_pin: + sleeptime = 0.25 + for i in range(0, 3): + subprocess.run("echo 1 > /sys/class/gpio/gpio" + str(led_pin) + "/value; sleep " + str( + sleeptime) + "; echo 0 > /sys/class/gpio/gpio" + str(led_pin) + "/value; sleep " + str( + sleeptime), shell=True, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + # Default: Switch to Speakers + print("Switched audio sink to \"Output 1\"") + # mpc only 1 always enables 1 first, avoiding any intermediate state with no valid output stream + proc = subprocess.run("mpc enable only 1", shell=True, check=False, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + logger.debug(proc.stdout) + # Yet, in some cases, a stream error still occurs: check and recover + bt_check_mpc_err() + if led_pin: + proc = subprocess.run("echo 0 > /sys/class/gpio/gpio" + str(led_pin) + "/value", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(b'LED off: ' + proc.stdout) + + +def get_led_pin_config(cfg_file): + """Read the led pin for reflecting current sink status from cfg_file which is a Python configparser file + + cfg_file is relative to this script's location or an absolute path + + The file must contain the entry + + [BluetoothToggleLed] + enabled = True + led_pin = 6 + + where + - led_pin is the BCM number of the GPIO pin (i.e. 'led_pin = 6' means GPIO6) and defaults to None + - enabled can be used to temporarily disable the LED + + Note: Capitalization of [BluetoothToggleLed] is important!""" + + # Make sure to locate cfg_file relative to this script's location independent of working directory + if not os.path.isabs(cfg_file): + cfg_file = os.path.dirname(os.path.realpath(__file__)) + '/' + cfg_file + logger.debug(f"Reading config file: '{cfg_file}'") + cfg = configparser.ConfigParser() + cfg_file_success = cfg.read(cfg_file) + if not cfg_file_success: + logger.debug(f"Could not read '{cfg_file}'. Continue with default values (i.e. led off).") + + section_name = 'BluetoothToggleLed' + led_pin = None + if section_name in cfg: + if cfg[section_name].getboolean('enabled', fallback=False): + led_pin = cfg[section_name].getint('led_pin', fallback=None) + if not led_pin: + logger.warning(f"Could not find 'led_pin' or could not read integer value") + elif not 1 <= led_pin <= 27: + logger.warning(f"Ignoring out of range pin number: {led_pin}.") + led_pin = None + else: + logger.debug(f"No section {section_name} found. Defaulting to led_pin = None") + + logger.debug(f"Using LED pin = {led_pin}") + return led_pin + + +if __name__ == "__main__": + if len(sys.argv) == 3: + logconsole.setLevel(logging.DEBUG) + + if 2 <= len(sys.argv) <= 3: + cfg_led_pin = get_led_pin_config('../../settings/gpio_settings.ini') + bt_switch(sys.argv[1], cfg_led_pin) + else: + bt_usage(sys.argv[0]) diff --git a/components/bluetooth-sink-switch/install-bt-sink-switch.sh b/components/bluetooth-sink-switch/install-bt-sink-switch.sh new file mode 100755 index 000000000..f09df2263 --- /dev/null +++ b/components/bluetooth-sink-switch/install-bt-sink-switch.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Check that script is called from source directory +FILE=bt-sink-switch.py +if [ ! -f "$FILE" ]; then + echo -e "Error: Install script must be started from source directoy of bt-headphones!" + exit -1 +fi + +# Only works for the Classic edition +EDITIONFILE=../../settings/edition +if [ ! -f "$EDITIONFILE" ]; then + echo -e "Error: Cannot find file '$EDITIONFILE' to check Phonibox edition" + exit -1 + +fi + +EDITION=`cat $EDITIONFILE` +if [ "${EDITION}" != "classic" ]; then + echo -e "Error: Sorry, this feature is only working with the classic edition, but got '${EDITION}'" + exit -1 +fi + +USER=`whoami` +SCRPATH=`pwd` + +# Ensure script is executable for everyone +sudo chmod ugo+rx $FILE + +# Make sure required packages are installed +echo -e "\nChecking bluetooth packages" +sudo apt install bluetooth -y + +# Add users to bluetooth, to make bluetooth control available through web interface +echo -e "\nSetting up user rights" +sudo usermod -G bluetooth -a www-data +sudo usermod -G bluetooth -a ${USER} + +# Add www-data to allow gpio control (for LED support) +sudo usermod -G gpio -a www-data + +# Let global controls know this feature is enabled +echo -e "\nLet global controls know this feature is enabled" +CONFFILE=../../settings/bluetooth-sink-switch +echo "enabled" > ${CONFFILE} +chmod ugo+rw ${CONFFILE} + +# Restart web service to take notice of new user rights +sudo systemctl restart lighttpd.service + +# Final notes +echo -e "\n\n\nFINAL NOTE:\nPlease check README.md for configuration of optional LED and GPIO toggle button." diff --git a/components/bluetooth-sink-switch/webif.png b/components/bluetooth-sink-switch/webif.png new file mode 100644 index 000000000..adf386ba0 Binary files /dev/null and b/components/bluetooth-sink-switch/webif.png differ diff --git a/components/controls/buttons-bluetooth-headphone/README.md b/components/controls/buttons-bluetooth-headphone/README.md new file mode 100644 index 000000000..8d9397df0 --- /dev/null +++ b/components/controls/buttons-bluetooth-headphone/README.md @@ -0,0 +1,88 @@ +## Control Phoniebox via Buttons from Bluetooth Headset + +Many bluetooth headsets or bluetooth speaker sets have buttons for controlling the music stream. **Let's make use of them!** +This component provides support for controlling your Phoniebox through these buttons on your bluetooth headset (or speaker set). + +### Installation + +1. Make sure your bluetooth headset is connected to the Phoniebox. Follow the instructions in the [Wiki](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Bluetooth). +2. Execute `$ ./install-bt-buttons.sh. It will ask you to identify your headset and set up appropriate user rights, and registers the script as a service. It should work immediatly. In case of doubt, reboot. + - If later changing the headset, re-run `$ ./register-device.py`. Reboot or restart the service with `sudo systemctl restart phoniebox-bt-buttons.service` + +### Supported Buttons + +Out-of-the box support is included for the following buttons + +- Play/Pause +- Previous Track +- Next Track + +Key codes are standarized and so it should also work with your headphones. If you want to add more keys or assign a different behaviour see [Troubleshooting](#troubleshooting) + +*Note:* Volume up/down is inherently supported by the bluetooth protocol. There is no need to handle these by this script. + +### On Connect / On Disconnect + +If the feature [bluetooth-sink-switch](../../bluetooth-sink-switch) is enabled, the script automatically switches the audio stream to headphones / regular speakers on bluetooth connect / disconnect respectivly. Playback state (play/pause) is retained. + +*Note:* On-connect actions may take up to 4 seconds - please be patient (bluetooth connection is only checked every two seconds, bluetooth stream needs to be buffered, etc...) + +You can **customize** the behaviour by editing the functions + +- `bt_on_connect(mpd_support=0)` +- `bt_on_disconnect(mpd_support=0)` + +where `mpd_support` indicates wether the bt-sink-switch-feature is enabled. + +### Troubleshooting + +This feature has been tested with PowerLocus Buddy and Sennheiser Momentum M2 AEBT headphones. + +#### Preparation + +- Stop the service `$ sudo systemctl stop phoniebox-bt-buttons.service` +- Start the script in a command line with debug option `$ ./bt-buttons.py debug` + +#### Check that correct bluetooth device is found + +- Run the [preparatory steps](#preparation) +- Check headset is connected and listed as input event device with `$ cat /proc/bus/input/devices`. Note the device name. +- In the script's debug output you should see something like this. Here the MAC address is the device name + +~~~bash +30.12.2020 21:44:41 - bt-buttons.py - DEBUG: bt_get_device_name() -> C4:FB:20:63:A7:F2 +30.12.2020 21:45:05 - bt-buttons.py - DEBUG: bt_open_device(C4:FB:20:63:A7:F2): Device 'C4:FB:20:63:A7:F2' search success +30.12.2020 21:45:05 - bt-buttons.py - DEBUG: device /dev/input/event1, name "C4:FB:20:63:A7:F2", phys "" +~~~ + +- If you see discrepancies, re-run `$ ./register-device.py`(see above) + +#### Add key codes / change actions + +- Run the [preparatory steps](#preparation) +- Press the buttons on the headset and check for these debug outputs. Note down the keycode. The **163** is the keycode, you are looking for. Go through all the buttons. Also try short/long press. On my headphones, they result in different keycodes + +~~~bash +30.12.2020 21:45:59 - bt-buttons.py - DEBUG: key event at 1609361159.529679, 163 (KEY_NEXTSONG), down +~~~ + +- Go into the source code and adjust these lines for desired behaviour + +~~~python + if event.code == bt_keycode_play: + proc = subprocess.run(f"{path}/../../../scripts/playout_controls.sh -c=playerpause", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + elif event.code == bt_keycode_pause: + proc = subprocess.run(f"{path}/../../../scripts/playout_controls.sh -c=playerpause", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + +~~~ + +#### Still having trouble? + +Check the basics: test the event input. Make sure the headphones are connected beforehand. Replace event*X* with the event number obtained from `$ cat /proc/bus/input/devices`. + +```$ cat /dev/input/eventX``` + +Press some buttons on the headset. Not all buttons will be forwarded, e.g. vol up/down may also be handled only in the headset. +Try also long/short press. The output will look wired. Don't worry - the important thing is that you are seeing something on the console. Now go back to [Troubleshooting](#troubleshooting). diff --git a/components/controls/buttons-bluetooth-headphone/bt-buttons-register-device.py b/components/controls/buttons-bluetooth-headphone/bt-buttons-register-device.py new file mode 100755 index 000000000..73c82244a --- /dev/null +++ b/components/controls/buttons-bluetooth-headphone/bt-buttons-register-device.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +""" +Presents available input devices to user for selection of bluetooth device +""" + +import evdev as ev +import os.path + +# Filename for storing the device name, relative to this script's location +filename_device_selection = '../../../settings/bluetooth-input-device-name.txt' + + +def bt_register_device(filename) -> str: + """Presents available input devices to user for selection of bluetooth device + + Selected device name is stored in 'filename' + + :param filename: Filename for storing the device name, relative to this script's location + :return str: Selected device name + """ + sq = input("Ensure bluetooth devices is turned on and connected. Ready? [Y/n] ") + if sq != "Y" and sq != "y" and sq != "": + print("Exiting ...") + return '' + + all_devices = [ev.InputDevice(path) for path in ev.list_devices()] + if len(all_devices) == 0: + print("#" * 60) + print("# NO INPUT DEVICES FOUND!") + print("#" * 60) + print("Exiting ...") + return '' + + for idx in range(len(all_devices)): + print(f"{str(idx)}: {all_devices[idx].name}") + devid = int(input("Device number: ")) + + filename_abs = os.path.dirname(os.path.realpath(__file__)) + '/' + filename + with open(filename_abs, 'w') as f: + f.write(all_devices[devid].name) + + return all_devices[devid].name + + +if __name__ == '__main__': + bt_register_device(filename_device_selection) diff --git a/components/controls/buttons-bluetooth-headphone/bt-buttons.py b/components/controls/buttons-bluetooth-headphone/bt-buttons.py new file mode 100755 index 000000000..d079ae510 --- /dev/null +++ b/components/controls/buttons-bluetooth-headphone/bt-buttons.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +Enable Bluetooth Headphone/Speaker Buttons for Music Control + +Script will listen to headphone button press events and call appropriate Phoniebox control function +If no headset is connected, it will endlessly check headset connection status every 2 seconds. + +Should be run as service. For debug can be run directly from console with additional debug output: + $ ./bt-buttons.py debug + +- Run install-bt-buttons.sh to configure user rights, service etc (will also run bt-buttons-register-device.py) +- Run bt-buttons-register-device.py with headphones connected to select bluetooth input device + +This script has been tested with the following headsets: PowerLocus Buddy, Sennheiser Momentum M2 AEBT +""" +import evdev as ev +import logging +import subprocess +import time +import sys +import os.path + + +# Filename with stored device name, relative to this script's location +filename_device_selection = '../../../settings/bluetooth-input-device-name.txt' +# Filename to read bt-sink-switch / mpd support from +# See components/bluetooth-audio-toggle for more information +filename_mpd_switch_feature = '../../../settings/bluetooth-sink-switch' + + +# Button key codes +bt_keycode_play = 200 +bt_keycode_pause = 201 +bt_keycode_next = 163 +bt_keycode_prev = 165 + + +# Create logger +logger = logging.getLogger('bt-buttons.py') +logger.setLevel(logging.DEBUG) +# Create console handler and set level to debug +logconsole = logging.StreamHandler() +logconsole.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S')) +logconsole.setLevel(logging.DEBUG) +logger.addHandler(logconsole) + + +def bt_on_disconnect(mpd_support=0) -> None: + """Executed on Bluetooth device disconnect + + Default: Switch output device to speakers + Disconnecting the Bluetooth device during playback causes an error with mpd + as it suddenly has no more output stream. Error state is checked and previous state recovery is attempted to + provide smooth transistion to speakers automatically by bt-sink-switch.py + :param mpd_support: Indicates if bluetooth sink switch feature using mpd is enabled + """ + logger.info("on disconnect") + if mpd_support: + pctproc = subprocess.run(f"{os.path.dirname(os.path.realpath(__file__))}/../../../scripts/playout_controls.sh -c=bluetoothtoggle -v=speakers", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(pctproc.stdout) + + +def bt_on_connect(mpd_support=0) -> None: + """Executed on Bluetooth device connect + + Default: Switch output device to Bluetooth device + Note: During bootup, if bluetooth device gets connected before the service for this script is started, this function + will still be executed + :param mpd_support: Indicates if bluetooth sink switch feature using mpd is enabled + """ + logger.info("on connect") + if mpd_support: + pctproc = subprocess.run(f"{os.path.dirname(os.path.realpath(__file__))}/../../../scripts/playout_controls.sh -c=bluetoothtoggle -v=headphones", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(pctproc.stdout) + + +def bt_get_device_name(filename) -> str: + """Gets the bluetooth device name from config file""" + logger.debug(f"bt_get_device_name looking for '{filename}'") + try: + with open(filename) as f: + devname: str = f.readline().strip() + except Exception as e: + logger.critical("#" * 60) + logger.critical(f"Error opening file '{filename}'. Please run registerBluetoothInput.py first!") + logger.critical(f"Exception: {e.__class__.__name__}") + logger.critical("#" * 60) + raise e + logger.debug(f"bt_get_device_name() -> {devname}") + return devname + + +def bt_get_mpd_support(filename) -> int: + """Checks if bluetooth switch feature is enabled""" + logger.debug(f"bt_get_mpd_support looking for '{filename}'") + try: + with open(filename) as f: + mpdsupport = f.readline().strip().lower() + except PermissionError: + mpdsupport = '' + except FileNotFoundError: + mpdsupport = '' + logger.debug(f"file read out '{mpdsupport}'") + if mpdsupport == 'enabled': + logger.debug("bt_get_mpd_support result is ON") + return 1 + logger.debug("bt_get_mpd_support result is OFF") + return 0 + + +def bt_open_device(name) -> ev.InputDevice: + """Tries to open bluetooth device, raises error if not available to be handled up-level""" + all_devices = [ev.InputDevice(path) for path in ev.list_devices()] + for dev in all_devices: + if dev.name == name: + logger.debug(f"bt_open_device({name}): Device '{name}' search success") + break + else: + # No device found, don't log to prevent log file spamming + # logger.error(f"bt_open_device({name}): Device '{name}' not found") + raise FileNotFoundError + return dev + + +def bt_key_handler(name, mpd_support=0) -> None: + """Actual key handler, once bluetooth device is connected""" + # Try to open the event device, will exit with exception on fail + dev = bt_open_device(name) + logger.debug(dev) + bt_on_connect(mpd_support) + path = os.path.dirname(os.path.realpath(__file__)) + # Infinite loop reading the events. Will fail, if event device gets disconnected + for event in dev.read_loop(): + if event.type == ev.ecodes.EV_KEY: + # Report the button event + logger.debug(ev.categorize(event)) + # Only act on button press, not button release + if event.value == 1: + if event.code == bt_keycode_play: + proc = subprocess.run(f"{path}/../../../scripts/playout_controls.sh -c=playerpause", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + elif event.code == bt_keycode_pause: + proc = subprocess.run(f"{path}/../../../scripts/playout_controls.sh -c=playerpause", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + elif event.code == bt_keycode_next: + proc = subprocess.run(f"{path}/../../../scripts/playout_controls.sh -c=playernext", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + elif event.code == bt_keycode_prev: + proc = subprocess.run(f"{path}/../../../scripts/playout_controls.sh -c=playerprev", shell=True, check=False, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + logger.debug(proc.stdout) + if proc.returncode != 0: + logger.error("#" * 60) + logger.error(f"In subprocess execution (retcode = {str(proc.returncode())})") + logger.error(proc.stdout) + logger.error("#" * 60) + + +def bt_loop(filename_device_selection, filename_mpd_switch_feature, sleeptime=2) -> None: + """Main loop for watching bluetooth device to connect, then call bt_key_handler + + Constantly checks for bluetooth device to connect by calling bt_key_handler() + On bluetooth device connect bt_on_connect will be executed + On bluetooth device disconnect bt_on_disconnect will be executed + + :param filename_mpd_switch_feature: Filename with stored device name, relative to this script's location + :param filename_device_selection: Filename with bluetooth sink switch configuration, relative to this script's location + :param sleeptime: Time to sleep between bluetooth device connection checks + :return: + """ + path = os.path.dirname(os.path.realpath(__file__)) + filename = path + '/' + filename_device_selection + name = bt_get_device_name(filename) + filename = path + '/' + filename_mpd_switch_feature + mpd_support = bt_get_mpd_support(filename) + logger.debug('Waiting for first connect of Bluetooth device') + while True: + try: + bt_key_handler(name, mpd_support) + except FileNotFoundError: + # This error occurs, if opening the bluetooth input device fails + time.sleep(sleeptime) + except OSError: + # This error occurs, when the already opened bluetooth device suddenly gets disconnected + bt_on_disconnect(mpd_support) + time.sleep(sleeptime) + + +if __name__ == '__main__': + if len(sys.argv) == 2: + logconsole.setLevel(logging.DEBUG) + bt_loop(filename_device_selection, filename_mpd_switch_feature, sleeptime=2) diff --git a/components/controls/buttons-bluetooth-headphone/install-bt-buttons.sh b/components/controls/buttons-bluetooth-headphone/install-bt-buttons.sh new file mode 100755 index 000000000..9c98fa2a7 --- /dev/null +++ b/components/controls/buttons-bluetooth-headphone/install-bt-buttons.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +FILE=bt-buttons.py +REGFILE=bt-buttons-register-device.py + +# Check that script is called from source directory +if [ ! -f "$FILE" ]; then + echo -e "Error: Install script must be started from source directoy of bt-headphones!" + exit -1 +fi + +USER=`whoami` +SCRPATH=`pwd` + +chmod ugo+rx ${FILE} +chmod ugo+rx ${REGFILE} + +# Configuring service file +echo -e "\nConfiguring service" +SERVICESAMPLE=../../../misc/sampleconfigs/phoniebox-bt-buttons.service.sample +sed "s@WorkingDirectory.*@WorkingDirectory=${SCRPATH}@g" ${SERVICESAMPLE} > phoniebox-bt-buttons.service.configured +sed -i "s@ExecStart.*@ExecStart=${SCRPATH}/${FILE}@g" phoniebox-bt-buttons.service.configured + +# Install service and enable it +SSRC=phoniebox-bt-buttons.service.configured +SDST=/etc/systemd/system/phoniebox-bt-buttons.service +echo -e "\nInstalling service" +sudo mv -f ${SSRC} ${SDST} +sudo chown root:root ${SDST} +sudo chmod 644 ${SDST} +sudo systemctl enable phoniebox-bt-buttons.service + + +# Call the register-device script +# Do this last, so that user can re-run abortive device registration without having to run the installer again +echo -e "\n\n*******************************************************************" +echo -e "Will try to register bluetooth input device. If this fails, you can re-start the device registration by calling \n $ ./${REGFILE}" +echo -e "Don't forget to reboot or restart the phoniebox-bt-buttons.service afterwards!" +echo -e "*******************************************************************\n\n" +./${REGFILE} + +# Start the service for immediate use +sudo systemctl start phoniebox-bt-buttons.service + +# Final notes +echo -e "\n\nIMPORTANT NOTE:\nThis feature MAY require a certain amount of customization to some headsets. Check out the README.md for details." +echo -e "\n\nEverything is set up now and should work now. In case of doubt, reboot!\n\n" diff --git a/components/controls/buttons_usb_encoder/buttons_usb_encoder.py b/components/controls/buttons_usb_encoder/buttons_usb_encoder.py index 7bf293919..e5b56acfb 100644 --- a/components/controls/buttons_usb_encoder/buttons_usb_encoder.py +++ b/components/controls/buttons_usb_encoder/buttons_usb_encoder.py @@ -2,17 +2,20 @@ import sys -sys.path.append(".") +sys.path.append(".") # This command should be before imports of components import logging from evdev import categorize, ecodes, KeyEvent -import components.gpio_control.function_calls from io_buttons_usb_encoder import button_map, current_device +from components.gpio_control.function_calls import phoniebox_function_calls + +sys.path.append(".") logger = logging.getLogger(__name__) try: button_map = button_map() + function_calls = phoniebox_function_calls() for event in current_device().read_loop(): if event.type == ecodes.EV_KEY: keyevent = categorize(event) @@ -23,7 +26,7 @@ try: function_name = button_map[button_string] try: - getattr(components.gpio_control.function_calls, function_name)() + getattr(function_calls, function_name)() except: logger.warning( "Function " + function_name + " not found in function_calls.py (mapped from button: " + button_string + ")") diff --git a/components/controls/buttons_usb_encoder/map_buttons_usb_encoder.py b/components/controls/buttons_usb_encoder/map_buttons_usb_encoder.py index fa4a22a5f..40b094135 100644 --- a/components/controls/buttons_usb_encoder/map_buttons_usb_encoder.py +++ b/components/controls/buttons_usb_encoder/map_buttons_usb_encoder.py @@ -2,16 +2,19 @@ import sys -sys.path.append(".") +sys.path.append(".") # This command should be before imports of components from evdev import categorize, ecodes, KeyEvent from io_buttons_usb_encoder import current_device, write_button_map import components.gpio_control.function_calls +sys.path.append(".") + + try: functions = list( filter(lambda function_name: function_name.startswith("functionCall"), - dir(components.gpio_control.function_calls))) + dir(components.gpio_control.function_calls.phoniebox_function_calls))) button_map = {} print("") diff --git a/components/displays/HD44780-i2c/README.md b/components/displays/HD44780-i2c/README.md index 2c61903c9..17b6a719a 100755 --- a/components/displays/HD44780-i2c/README.md +++ b/components/displays/HD44780-i2c/README.md @@ -4,89 +4,90 @@ *([Permission to use this code for the Phoniebox project](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/859#discussion_r408667007) on April 15th 2020 by author [Denis Pleic](https://github.com/DenisFromHR). Original [code on https://gist.github.com/DenisFromHR/cc863375a6e19dce359d](https://gist.github.com/DenisFromHR/cc863375a6e19dce359d))* The following files allow using LCD displays based on HD44780 connected via i2c bus for this project. The following displays have been used for testing: - -- 2x16 display -- 4x20 display (recommended as more information can be displayed) - -Various informations such as artist, album, track_number, track_title, track_time and many more can be displayed see main script for more display options. - -The required files are: - -- components/displays/HD44780-i2c/i2c_lcd.py -- components/displays/HD44780-i2c/i2c_lcd_driver.py -- components/displays/HD44780-i2c/i2c-lcd.service.default.sample -- components/displays/HD44780-i2c/README.md - - -The first file is the main LCD script that makes use of I2C_LCD_driver.py. - -The second file is the library needed to drive the LCD via i2c, originates from DenisFromHR (Denis Pleic) see http://www.circuitbasics.com/raspberry-pi-i2c-lcd-set-up-and-programming - -The third is used as sample service file that runs the i2c_lcd.py main script at boot-up if the service is properly installed (install description can be found below.). - -The fourth file is this file which describes the features, usage and installation of the code. - -### Installation - + +- 2x16 display +- 4x20 display (recommended as more information can be displayed) + +Various informations such as artist, album, track_number, track_title, track_time and many more can be displayed see main script for more display options. + +The required files are: + +- components/displays/HD44780-i2c/i2c_lcd.py +- components/displays/HD44780-i2c/i2c_lcd_driver.py +- components/displays/HD44780-i2c/i2c-lcd.service.default.sample +- components/displays/HD44780-i2c/README.md + +The first file is the main LCD script that makes use of I2C_LCD_driver.py. + +The second file is the library needed to drive the LCD via i2c, originates from DenisFromHR (Denis Pleic) see + +The third is used as sample service file that runs the i2c_lcd.py main script at boot-up if the service is properly installed (install description can be found below.). + +The fourth file is this file which describes the features, usage and installation of the code. + +### Installation + * You need to install additional python libraries. Run the following two command in the command line: - -`sudo apt-get install i2c-tools python-smbus python3-numpy python-mpdclient python-mpd` -`pip install smbus numpy python-mpd2` +`sudo apt-get install i2c-tools python-smbus python3-numpy python-mpdclient python-mpd` + +`pip install smbus numpy python-mpd2` + +* You need to know which I2C bus your Raspberry Pi has available on GPIOs: -* You need to know which I2C bus your Raspberry Pi has available on GPIOs: +`ls /dev/i2c-*` -`ls /dev/i2c-*` +It'll output "/dev/i2c-x", where x is your bus number. Note this bus number as you will need it in step 6. -It'll output "/dev/i2c-x", where x is your bus number. Note this bus number as you will need it in step 6. -* Now detect the adapter by using the i2cdetect command, inserting your bus number: +* Now detect the adapter by using the i2cdetect command, inserting your bus number: -`sudo i2cdetect -y bus_number` +`sudo i2cdetect -y bus_number` -The I2C address of my LCD is 27. Take note of this number, it will be need in step 6. +The I2C address of my LCD is 27. Take note of this number, it will be need in step 6. -* if i2cdetect is not found install i2c-tools +* if i2cdetect is not found install i2c-tools -`sudo apt-get update` +`sudo apt-get update` -`sudo apt-get install i2c-tools` +`sudo apt-get install i2c-tools` -* Next we need to install SMBUS, which gives the Python library we’re going to use access to the I2C bus on the Pi. At the command prompt, enter +* Next we need to install SMBUS, which gives the Python library we’re going to use access to the I2C bus on the Pi. At the command prompt, enter -`sudo apt-get install python-smbus` +`sudo apt-get install python-smbus` -* Modify "i2c_lcd_driver.py" line 19 which reads "I2CBUS = 1" and adapt it to your bus number (see step 2.) Furthermore modify line 22 which reads "ADDRESS = 0x27" and adapt it to your I2C address (see step 3.) +* Modify "i2c_lcd_driver.py" line 19 which reads "I2CBUS = 1" and adapt it to your bus number (see step 2.) Furthermore modify line 22 which reads "ADDRESS = 0x27" and adapt it to your I2C address (see step 3.) * Modify "i2c_lcd.py" to adapt it yo your specific display e.g. 2x16 or 4x20 (default). The lines 15-19 look like the following: - + +```bash +################# CHANGE YOUR SETTINGS HERE!!! ########################################### +## Display settings ## +n_cols = 20 # EDIT!!! <-- number of cols your display has ## +n_rows = 4 # EDIT!!! <-- number of rows your display has ## +val_delay = 0.4 # EDIT!!! <-- speed of the scolling text ## ``` -################# CHANGE YOUR SETTINGS HERE!!! ########################################### -## Display settings ## -n_cols = 20 # EDIT!!! <-- number of cols your display has ## -n_rows = 4 # EDIT!!! <-- number of rows your display has ## -val_delay = 0.4 # EDIT!!! <-- speed of the scolling text ## -``` -Check if "n_cols" and "n_rows" need to be changed and modify them if necessary. The "val_delay" constant leave for the time being. Lower values will speed up things but will make the text less visible/readable. - -* next install and start "i2c-lcd.service" -`sudo cp /home/pi/RPi-Jukebox-RFID/components/displays/HD44780-i2c/i2c-lcd.service.default.sample /etc/systemd/system/i2c-lcd.service` +Check if "n_cols" and "n_rows" need to be changed and modify them if necessary. The "val_delay" constant leave for the time being. Lower values will speed up things but will make the text less visible/readable. + +* next install and start "i2c-lcd.service" -* register service by running, it will thereby start on the next boot-up +`sudo cp /home/pi/RPi-Jukebox-RFID/components/displays/HD44780-i2c/i2c-lcd.service.default.sample /etc/systemd/system/i2c-lcd.service` -`sudo systemctl enable i2c-lcd` +* register service by running, it will thereby start on the next boot-up -* Reboot and enjoy! +`sudo systemctl enable i2c-lcd` + +* Reboot and enjoy! --- -For test purposes you can use the following command to start and stop the service without rebooting -to start the service instantly run +For test purposes you can use the following command to start and stop the service without rebooting +to start the service instantly run + +`sudo systemctl start i2c-lcd` -`sudo systemctl start i2c-lcd` +to stop the service instantly run -to stop the service instantly run +`sudo systemctl stop i2c-lcd` -`sudo systemctl stop i2c-lcd` - -Best regards, -Simon \ No newline at end of file +Best regards, +Simon diff --git a/components/displays/HD44780-i2c/i2c_lcd.py b/components/displays/HD44780-i2c/i2c_lcd.py index 1adbcd866..9cdd0aeed 100755 --- a/components/displays/HD44780-i2c/i2c_lcd.py +++ b/components/displays/HD44780-i2c/i2c_lcd.py @@ -333,7 +333,7 @@ def sec_to_min_and_sec(seconds): album = album.replace("\n", "").replace("ä", "\341").replace("ö", "\357").replace("ü", "\365").replace("ß", "\342").replace("Ä", "\341").replace("Ö", "\357").replace("Ü", "\365") # weitere codes siehe https://www.mikrocontroller.net/topic/293125 # except KeyError: # album = "" # - ## read in artist info + ## read in artist info try: # artist = current_song_infos['artist'] # artist = artist.replace("\n", "").replace("ä", "\341").replace("ö", "\357").replace("ü", "\365").replace("ß", "\342").replace("Ä", "\341").replace("Ö", "\357").replace("Ü", "\365") # weitere codes siehe https://www.mikrocontroller.net/topic/293125 # @@ -342,7 +342,7 @@ def sec_to_min_and_sec(seconds): artist = current_song_infos['name'] # artist = artist.replace("\n", "").replace("ä", "\341").replace("ö", "\357").replace("ü", "\365").replace("ß", "\342").replace("Ä", "\341").replace("Ö", "\357").replace("Ü", "\365") # weitere codes siehe https://www.mikrocontroller.net/topic/293125 # except KeyError: # - artist = "" # + artist = "" # if (client.mpd_version) >= "0.20": try: # elapsed = status['elapsed'].split(".")[0] # @@ -351,8 +351,7 @@ def sec_to_min_and_sec(seconds): except KeyError: # track_time = "" # else: # - track_time = subprocess.check_output('mpc | head -n2 | tail -n1 | sed "s/ \+/ /g" | cut -d" " -f3', shell=True) - track_time = track_time.replace("\n", "") # + track_time = subprocess.check_output('mpc | head -n2 | tail -n1 | sed "s/ \+/ /g" | cut -d" " -f3', universal_newlines=True, shell=True) ########################################################################################### ############# RESET GLOBAL COUNTER, IF TITLE CHANGED ############################ @@ -410,7 +409,7 @@ def sec_to_min_and_sec(seconds): if i_counter >= 65000: # i_counter = 1000 # <-- not 0, cause the display could be off # ###################################################################################### - + ####################### REMIND STUFF FOR NEXT CYCLE ################################# last_state = state # last_title = title # diff --git a/components/displays/dot-matrix-module-MAX7219/README.md b/components/displays/dot-matrix-module-MAX7219/README.md index c52bf2bad..766addcf6 100644 --- a/components/displays/dot-matrix-module-MAX7219/README.md +++ b/components/displays/dot-matrix-module-MAX7219/README.md @@ -1,26 +1,26 @@ -# MAX7219 Dot Matrix Display - -## Items needed - -* [NodeMCU ESP8266, CPU/WLAN](https://amzn.to/2urDAky) -* [MAX7219 dot matrix module microcontroller module 4 in one display](https://amzn.to/2Sa5Scx) - -## Configuration - -In the [display.ino](display.ino#L48-L50) there is following configuration part: - - const char* ssid = "foo"; - const char* password = "foo"; - const char* host = "192.168.42.42"; - -`ssid` is your local WiFi network. -`password` is the password for your WiFi network. +# MAX7219 Dot Matrix Display + +## Items needed + +* [NodeMCU ESP8266, CPU/WLAN](https://amzn.to/2urDAky) +* [MAX7219 dot matrix module microcontroller module 4 in one display](https://amzn.to/2Sa5Scx) + +## Configuration + +In the [display.ino](display.ino#L48-L50) there is following configuration part: + + const char* ssid = "foo"; + const char* password = "foo"; + const char* host = "192.168.42.42"; + +`ssid` is your local WiFi network. +`password` is the password for your WiFi network. `host` is the **static** IP of your Phoniebox. - -For flashing the ESP, you can use the [Arduino IDE](https://en.wikipedia.org/wiki/Arduino_IDE). But there are a few more other possibilities to do this. - -## Pics - -![still](still.jpg) -![ticker](ticker.gif) + +For flashing the ESP, you can use the [Arduino IDE](https://en.wikipedia.org/wiki/Arduino_IDE). But there are a few more other possibilities to do this. + +## Pics + +![still](still.jpg) +![ticker](ticker.gif) diff --git a/components/gpio_control/GPIODevices/VolumeControl.py b/components/gpio_control/GPIODevices/VolumeControl.py deleted file mode 100644 index af40b0395..000000000 --- a/components/gpio_control/GPIODevices/VolumeControl.py +++ /dev/null @@ -1,27 +0,0 @@ -from GPIODevices import TwoButtonControl, RotaryEncoder -from gpio_control import logger, getFunctionCall - - -class VolumeControl: - def __new__(self, config): - if config.get('Type') == 'TwoButtonControl': - logger.info('VolumeControl as TwoButtonControl') - return TwoButtonControl( - config.getint('pinUp'), - config.getint('pinDown'), - getFunctionCall(config.get('functionCallUp')), - getFunctionCall(config.get('functionCallDown')), - functionCallTwoBtns=getFunctionCall(config.get('functionCallTwoButtons')), - pull_up=config.getboolean('pull_up', fallback=True), - hold_repeat=config.getboolean('hold_repeat', fallback=True), - hold_time=config.getfloat('hold_time', fallback=0.3), - name='VolumeControl' - ) - elif config.get('Type') == 'RotaryEncoder': - return RotaryEncoder( - config.getint('pinUp'), - config.getint('pinDown'), - getFunctionCall(config.get('functionCallUp')), - getFunctionCall(config.get('functionCallDown')), - config.getfloat('timeBase', fallback=0.1), - name='RotaryVolumeControl') diff --git a/components/gpio_control/GPIODevices/__init__.py b/components/gpio_control/GPIODevices/__init__.py index 445ac80c7..5eaad3009 100644 --- a/components/gpio_control/GPIODevices/__init__.py +++ b/components/gpio_control/GPIODevices/__init__.py @@ -3,5 +3,4 @@ from .shutdown_button import ShutdownButton from .simple_button import SimpleButton from .two_button_control import TwoButtonControl -from .VolumeControl import VolumeControl from .led import * diff --git a/components/gpio_control/GPIODevices/led.py b/components/gpio_control/GPIODevices/led.py index d2d5dbab3..aff6dbc45 100644 --- a/components/gpio_control/GPIODevices/led.py +++ b/components/gpio_control/GPIODevices/led.py @@ -1,7 +1,7 @@ import logging import time +from os import system -import mpd from RPi import GPIO GPIO.setmode(GPIO.BCM) @@ -29,28 +29,16 @@ def status(self): return GPIO.input(self.pin) -class MPDStatusLED(LED): - logger = logging.getLogger("MPDStatusLED") +class StatusLED(LED): + logger = logging.getLogger("StatusLED") - def __init__(self, pin, host='localhost', port=6600, name='MPDStatusLED'): - super(MPDStatusLED, self).__init__(pin, initial_value=False, name=name) - self.mpc = mpd.MPDClient() - self.host = host - self.port = port - self.logger.info('Waiting for MPD Connection on {}:{}'.format( - self.host, self.port)) - while not self.has_mpd_connection(): - self.logger.debug('No MPD Connection yet established') + def __init__(self, pin, name='StatusLED'): + super(StatusLED, self).__init__(pin, initial_value=False, name=name) + self.logger.info('Waiting for phoniebox-startup-scripts service to be active') + systemctlCmd = 'systemctl is-active --quiet phoniebox-startup-scripts.service' + while system(systemctlCmd) != 0: + self.logger.debug('phoniebox-startup-scripts service not yet active') time.sleep(1) - self.logger.info('Connection to MPD server on host {}:{} established'.format(self.host, self.port)) + self.logger.info('phoniebox-startup-scripts service active') self.on() - def has_mpd_connection(self): - self.mpc.disconnect() - try: - self.mpc.connect(self.host, self.port) - self.mpc.ping() - self.mpc.disconnect() - return True - except ConnectionError: - return False diff --git a/components/gpio_control/GPIODevices/shutdown_button.py b/components/gpio_control/GPIODevices/shutdown_button.py index 40129f325..30ba26b6f 100644 --- a/components/gpio_control/GPIODevices/shutdown_button.py +++ b/components/gpio_control/GPIODevices/shutdown_button.py @@ -1,21 +1,25 @@ -import math import time from RPi import GPIO import logging -from .simple_button import SimpleButton +try: + from simple_button import SimpleButton, print_edge_key, print_pull_up_down +except ImportError: + from .simple_button import SimpleButton, print_edge_key, print_pull_up_down logger = logging.getLogger(__name__) class ShutdownButton(SimpleButton): - def __init__(self, pin, action=lambda *args: None, name=None, bouncetime=500, edge=GPIO.FALLING, - hold_time=.1, led_pin=None, time_pressed=2, hold_repeat=False, pull_up_down=GPIO.PUD_UP,iteration_time=.2): + def __init__(self, pin, action=lambda *args: None, name=None, bouncetime=500, antibouncehack=False, edge='falling', + led_pin=None, hold_time=3.0, pull_up_down='pull_up', iteration_time=.2): self.led_pin = led_pin - self.time_pressed = time_pressed self.iteration_time = iteration_time - super(ShutdownButton, self).__init__(pin=pin, action=action, name=name, bouncetime=bouncetime, edge=edge, - hold_time=hold_time, hold_repeat=hold_repeat, pull_up_down=pull_up_down, + if self.led_pin is not None: + GPIO.setup(self.led_pin, GPIO.OUT) + super(ShutdownButton, self).__init__(pin=pin, action=action, name=name, bouncetime=bouncetime, + antibouncehack=antibouncehack, edge=edge, hold_time=hold_time, + pull_up_down=pull_up_down ) pass @@ -30,26 +34,29 @@ def set_led(self, status): logger.debug('cannot set LED to {}: no LED pin defined'.format(status)) def callbackFunctionHandler(self, *args): - status = False - n_checks = math.ceil(self.time_pressed / self.iteration_time) - logger.debug('ShutdownButton pressed, ensuring long press for {} seconds, checking each {}s: {}'.format( - self.time_pressed, self.iteration_time, n_checks + logger.debug('ShutdownButton pressed, ensuring long press for {} seconds, checking each {}s'.format( + self.hold_time, self.iteration_time )) - for x in range(n_checks): - self.set_led(x & 1) - time.sleep(.2) - status = not self.is_pressed - if status: + t_passed = 0 + led_state = True + while t_passed < self.hold_time: + self.set_led(led_state) + time.sleep(self.iteration_time) + t_passed += self.iteration_time + led_state = not led_state + if not self.is_pressed: break - self.set_led(status) - if not status: + if t_passed >= self.hold_time: # trippel off period to indicate command accepted - time.sleep(.6) self.set_led(GPIO.HIGH) + time.sleep(.6) # leave it on for the moment, it will be off when the system is down self.when_pressed(*args) + else: + # switch off LED if pressing was cancelled early (during flashing) + self.set_led(GPIO.LOW) def __repr__(self): - return ''.format( - self.name, self.pin, self.hold_repeat, self.hold_time + return ''.format( + self.name, self.pin, self.hold_time, self.iteration_time, self.led_pin, print_edge_key(self.edge), self.bouncetime,self.antibouncehack, print_pull_up_down(self.pull_up_down) ) diff --git a/components/gpio_control/GPIODevices/simple_button.py b/components/gpio_control/GPIODevices/simple_button.py index 2e2c8202f..69b6e3407 100644 --- a/components/gpio_control/GPIODevices/simple_button.py +++ b/components/gpio_control/GPIODevices/simple_button.py @@ -6,34 +6,44 @@ logger = logging.getLogger(__name__) +map_edge_parse = {'falling':GPIO.FALLING, 'rising':GPIO.RISING, 'both':GPIO.BOTH} +map_pull_parse = {'pull_up':GPIO.PUD_UP, 'pull_down':GPIO.PUD_DOWN, 'pull_off':GPIO.PUD_OFF} +map_edge_print = {GPIO.FALLING: 'falling', GPIO.RISING: 'rising', GPIO.BOTH: 'both'} +map_pull_print = {GPIO.PUD_UP:'pull_up', GPIO.PUD_DOWN: 'pull_down', GPIO.PUD_OFF: 'pull_off'} def parse_edge_key(edge): if edge in [GPIO.FALLING, GPIO.RISING, GPIO.BOTH]: - edge - elif edge.lower() == 'falling': - edge = GPIO.FALLING - elif edge.lower() == 'raising': - edge = GPIO.RISING - elif edge.lower() == 'both': - edge = GPIO.BOTH - else: + return edge + try: + result = map_edge_parse[edge.lower()] + except KeyError: + result = edge raise KeyError('Unknown Edge type {edge}'.format(edge=edge)) - return edge - + return result def parse_pull_up_down(pull_up_down): if pull_up_down in [GPIO.PUD_UP, GPIO.PUD_DOWN, GPIO.PUD_OFF]: - pull_up_down - elif pull_up_down.lower() == 'pull_up': - pull_up_down = GPIO.PUD_UP - elif pull_up_down.lower() == 'pull_down': - pull_up_down = GPIO.PUD_DOWN - elif pull_up_down.lower() == 'pull_off': - pull_up_down = GPIO.PUD_OFF - else: + return pull_up_down + try: + result = map_pull_parse[pull_up_down] + except KeyError: + result = pull_up_down raise KeyError('Unknown Pull Up/Down type {pull_up_down}'.format(pull_up_down=pull_up_down)) - return pull_up_down - + return result + +def print_edge_key(edge): + try: + result = map_edge_print[edge] + except KeyError: + result = edge + return result + +def print_pull_up_down(pull_up_down): + try: + result = map_pull_print[pull_up_down] + except KeyError: + result = pull_up_down + return result # This function takes a holding time (fractional seconds), a channel, a GPIO state and an action reference (function). # It checks if the GPIO is in the state since the function was called. If the state @@ -43,6 +53,7 @@ def checkGpioStaysInState(holdingTime, gpioChannel, gpioHoldingState): startTime = time.perf_counter() # Continously check if time is not over while True: + time.sleep(0.1) currentState = GPIO.input(gpioChannel) if holdingTime < (time.perf_counter() - startTime): break @@ -57,19 +68,21 @@ def checkGpioStaysInState(holdingTime, gpioChannel, gpioHoldingState): class SimpleButton: - def __init__(self, pin, action=lambda *args: None, name=None, bouncetime=500, edge=GPIO.FALLING, - hold_time=.1, hold_repeat=False, pull_up_down=GPIO.PUD_UP): + def __init__(self, pin, action=lambda *args: None, action2=lambda *args: None, name=None, + bouncetime=500, antibouncehack=False, edge='falling', hold_time=.3, hold_mode=None, pull_up_down='pull_up'): self.edge = parse_edge_key(edge) self.hold_time = hold_time - self.hold_repeat = hold_repeat + self.hold_mode = hold_mode self.pull_up = True self.pull_up_down = parse_pull_up_down(pull_up_down) self.pin = pin self.name = name self.bouncetime = bouncetime + self.antibouncehack = antibouncehack GPIO.setup(self.pin, GPIO.IN, pull_up_down=self.pull_up_down) self._action = action + self._action2 = action2 GPIO.add_event_detect(self.pin, edge=self.edge, callback=self.callbackFunctionHandler, bouncetime=self.bouncetime) self.callback_with_pin_argument = False @@ -80,37 +93,71 @@ def callbackFunctionHandler(self, *args): args = args[1:] logger.debug('args after: {}'.format(args)) - if self.hold_repeat: - return self.holdAndRepeatHandler(*args) - logger.info('{}: executre callback'.format(self.name)) - return self.when_pressed(*args) + if self.antibouncehack: + time.sleep(0.1) + inval = GPIO.input(self.pin) + if inval != GPIO.LOW: + return None + + if self.hold_mode in ('Repeat', 'Postpone', 'SecondFunc', 'SecondFuncRepeat'): + return self.longPressHandler(*args) + else: + logger.info('{}: execute callback'.format(self.name)) + return self.when_pressed(*args) @property def when_pressed(self): logger.info('{}: action'.format(self.name)) return self._action + @property + def when_held(self): + logger.info('{}: action2'.format(self.name)) + return self._action2 + @when_pressed.setter def when_pressed(self, func): logger.info('{}: set when_pressed') self._action = func GPIO.remove_event_detect(self.pin) - self._action = func logger.info('add new action') GPIO.add_event_detect(self.pin, edge=self.edge, callback=self.callbackFunctionHandler, bouncetime=self.bouncetime) def set_callbackFunction(self, callbackFunction): self.when_pressed = callbackFunction - def holdAndRepeatHandler(self, *args): - logger.info('{}: holdAndRepeatHandler'.format(self.name)) - # Rise volume as requested - self.when_pressed(*args) - # Detect holding of button - while checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + def longPressHandler(self, *args): + logger.info('{}: longPressHandler, mode: {}'.format(self.name, self.hold_mode)) + # instant action (except Postpone mode) + if self.hold_mode != "Postpone": self.when_pressed(*args) - + + # action(s) after hold_time + if self.hold_mode == "Repeat": + # Repeated call of main action (multiple times if button is held long enough) + while checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + self.when_pressed(*args) + + elif self.hold_mode == "Postpone": + # Postponed call of main action (once) + if checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + self.when_pressed(*args) + while checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + pass + + elif self.hold_mode == "SecondFunc": + # Call of secondary action (once) + if checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + self.when_held(*args) + while checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + pass + + elif self.hold_mode == "SecondFuncRepeat": + # Repeated call of secondary action (multiple times if button is held long enough) + while checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + self.when_held(*args) + def __del__(self): logger.debug('remove event detection') GPIO.remove_event_detect(self.pin) @@ -122,8 +169,8 @@ def is_pressed(self): return GPIO.input(self.pin) def __repr__(self): - return ''.format( - self.name, self.pin, self.hold_repeat, self.hold_time + return ''.format( + self.name, self.pin, print_edge_key(self.edge), self.hold_mode, self.hold_time, self.bouncetime,self.antibouncehack,print_pull_up_down(self.pull_up_down) ) @@ -131,5 +178,5 @@ def __repr__(self): print('please enter pin no to test') pin = int(input()) func = lambda *args: print('FunctionCall with {}'.format(args)) - btn = SimpleButton(pin=pin, action=func, hold_repeat=True) + btn = SimpleButton(pin=pin, action=func, hold_mode='Repeat') pause() diff --git a/components/gpio_control/GPIODevices/two_button_control.py b/components/gpio_control/GPIODevices/two_button_control.py index ef861e275..d4ceb0a11 100644 --- a/components/gpio_control/GPIODevices/two_button_control.py +++ b/components/gpio_control/GPIODevices/two_button_control.py @@ -1,7 +1,7 @@ try: - from simple_button import SimpleButton + from simple_button import SimpleButton, print_edge_key, print_pull_up_down except ImportError: - from .simple_button import SimpleButton + from .simple_button import SimpleButton, print_edge_key, print_pull_up_down from RPi import GPIO import logging logger = logging.getLogger(__name__) @@ -11,17 +11,17 @@ def functionCallTwoButtons(btn1, btn2, functionCall1, functionCall2, functionCallBothPressed=None): def functionCallTwoButtons(*args): - btn1_pin=btn1.pin - btn2_pin=btn2.pin - pressed_button=None - if len(args) > 0 and args[0] in (btn1_pin,btn2_pin): + btn1_pin = btn1.pin + btn2_pin = btn2.pin + pressed_button = None + if len(args) > 0 and args[0] in (btn1_pin, btn2_pin): logger.debug('Remove pin argument by TwoButtonCallbackFunctionHandler - args before: {}'.format(args)) - pressed_button=args[0] + pressed_button = args[0] args = args[1:] logger.debug('args after: {}'.format(args)) btn1_pressed = btn1.is_pressed btn2_pressed = btn2.is_pressed - logger.info('Btn1 {}, Btn2 {}-args:{}'.format(btn1_pressed,btn2_pressed,args)) + logger.info('Btn1 {}, Btn2 {}-args:{}'.format(btn1_pressed, btn2_pressed, args)) if btn1_pressed and btn2_pressed: logger.debug("Both buttons was pressed") if functionCallBothPressed is not None: @@ -59,32 +59,43 @@ def __init__(self, functionCallBtn1, functionCallBtn2, functionCallTwoBtns=None, - pull_up=True, - hold_repeat=True, + pull_up_down='pull_up', + hold_mode=None, hold_time=0.3, + bouncetime=500, + antibouncehack=False, + edge='falling', name='TwoButtonControl'): + self.bcmPin1 = bcmPin1 + self.bcmPin2 = bcmPin2 self.functionCallBtn1 = functionCallBtn1 self.functionCallBtn2 = functionCallBtn2 self.functionCallTwoBtns = functionCallTwoBtns - self.bcmPin1 = bcmPin1 - self.bcmPin2 = bcmPin2 + self.pull_up_down=pull_up_down + self.hold_mode=hold_mode + self.hold_time=hold_time + self.bouncetime=bouncetime + self.antibouncehack=antibouncehack + self.edge=edge self.btn1 = SimpleButton( pin=bcmPin1, - action=lambda *args: None, - name=name + 'Btn2', - bouncetime=500, - edge=GPIO.FALLING, + name=name + 'Btn1', + bouncetime=bouncetime, + antibouncehack=antibouncehack, + edge=edge, hold_time=hold_time, - hold_repeat=hold_repeat) + hold_mode=hold_mode, + pull_up_down=pull_up_down) self.btn1.callback_with_pin_argument = True self.btn2 = SimpleButton(pin=bcmPin2, - action=lambda *args: None, - hold_time=hold_time, - hold_repeat=hold_repeat, name=name + 'Btn2', - bouncetime=500, - edge=GPIO.FALLING) + bouncetime=bouncetime, + antibouncehack=antibouncehack, + edge=edge, + hold_time=hold_time, + hold_mode=hold_mode, + pull_up_down=pull_up_down) self.btn2.callback_with_pin_argument = True generatedTwoButtonFunctionCall = functionCallTwoButtons(self.btn1, self.btn2, @@ -100,11 +111,11 @@ def __init__(self, def __repr__(self): two_btns_action = self.functionCallTwoBtns is not None - return ''.format( - name=self.name, - bcmPin1=self.bcmPin1, - bcmPin2=self.bcmPin2, - two_btns_action=two_btns_action + return ''.format( + self.name, self.bcmPin1, self.bcmPin2, two_btns_action, + self.hold_mode, self.hold_time, print_edge_key(self.edge), + self.bouncetime, self.antibouncehack, + print_pull_up_down(self.pull_up_down) ) diff --git a/components/gpio_control/README.md b/components/gpio_control/README.md index 195447e63..0c92ca6f5 100644 --- a/components/gpio_control/README.md +++ b/components/gpio_control/README.md @@ -4,26 +4,289 @@ This service enables the control of different GPIO input & output devices for co It uses to a configuration file to configure the active devices. ## How to create and run the service? + * The service can be activated during installation with the installscript. -* If the service was not activated during installation, you can alternatively use `sudo install.sh` in this folder. +* If the service was not activated during installation, you can alternatively use `sudo install.sh` in this folder (`components/gpio_control`). ## How to edit configuration files? -The configuration file is located here: `~/RPi-Jukebox-RFID/settings/gpio_settings.ini` + +The configuration file is located here: `~/RPi-Jukebox-RFID/settings/gpio_settings.ini` Editing the configuration file and restarting the service with `sudo systemctl restart phoniebox-gpio-control` will activate the new settings. -In the following the different devices are described. +In the following the different devices are described. Each device can have actions which correspond to function calls. Up to now the following input devices are implemented: -* **Button**: - A simple button which has a hold and repeat functionality as well as a delayed action. - It can be configured using the keywords: Pin (**use GPIO number here**), hold_time, functionCall + +* **Button**: + A simple button with optional long-press actions like hold and repeat functionality or delayed action. + Its main parameters are: `Pin` (use GPIO number here) and `functionCall`. For additional options, see [extended documentation below](#doc_button). + +* **ShutdownButton**: + A specialized implementation for a shutdown button with integrated (but optional) LED support. It initializes a shutdown if the button is pressed more than `time_pressed` seconds and a (optional) LED on GPIO `led_pin` is flashing until that time is reached. For additional information, see [extended documentation below](#doc_sdbutton). * **RotaryEncoder**: - Control of a rotary encoder, for example KY040, see also in - [Wiki](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Audio-RotaryKnobVolume) - it can be configured using pinA (**use GPIO number here**), pinB (**use GPIO number here**), functionCallIncr, functionCallDecr, timeBase=0.1 + Control of a rotary encoder, for example KY040, see also in [Wiki](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Audio-RotaryKnobVolume). + It can be configured using `pinUp` and `PiNDown` (use GPIO numbers here), `functionCallUp`, `functionCallDown`, and `timeBase` see [extended documentation below](#doc_rotary). * **TwoButtonControl**: - This Device uses two Buttons and implements a third action if both buttons are pressed together. + This Device uses two Buttons and implements a third action if both buttons are pressed together. See [extended documentation below](#doc_twobutton). + +* **StatusLED**: + A LED which will light up once the Phoniebox has fully booted up and is ready to be used. For additional information, see [extended documentation below](#doc_sled). + +Each section needs to be activated by setting `enabled: True`. Many example files are located in `~/RPi-Jukebox-RFID/components/gpio_control/example_configs/`. + +# Extended documentation + +This section provides some extended documentation and guideline. Especially some exemplary configurations are introduced showing how these controls can be set up in the configuration file `~/RPi-Jukebox-RFID/settings/gpio_settings.ini`. + +## Button + +At the most basic level, a button can be created using an `ini` entry like this: + +```bash +[PlayPause] +enabled: True +Type: Button +Pin: 27 +functionCall: functionCallPlayerPause +``` + +* **enabled**: This needs to be `True` for the button to work. +* **Pin**: GPIO number +* **functionCall**: The function that you want to be called on a button press. See [function documentation below](#doc_funcs). + +However, a button has more parameters than these. In the following comprehensive list you can also find the default values which are used automatically if you leave out these settings: + +* **hold_mode**: Specifies what shall happen if the button is held pressed for longer than `hold_time`: + * `None` (Default): Nothing special will happen. + * `Repeat`: The configured `functionCall` is repeated after each `hold_time` interval. + * `Postpone`: The function will not be called before `hold_time`, i.e. the button needs to be pressed this long to activate the function + * `SecondFunc`: Holding the button for at least `hold_time` will additionally execute the function `functionCall2`. + * `SecondFuncRepeat`: Like SecondFunc, but `functionCall2` is repeated after each `hold_time` interval. + + In every `hold_mode` except `Postpone`, the main action `functionCall` gets executed instantly. + + Holding the button even longer than `hold_time` will cause no further action unless you are in the `Repeat` or `SecondFuncRepeat` mode. + +* **hold_time**: Reference time for this buttons `hold_mode` feature in seconds. Default is `0.3`. This setting is ignored if `hold_mode` is unset or `None` +* **functionCall2**: Secondary function; default is `None`. This setting is ignored unless `hold_mode` is set to `SecondFunc` or `SecondFuncRepeat`. +* **pull_up_down**: Configures the internal Pull up/down resistors. Valid settings: + * `pull_up` (Default). Internal pull-up resistors are activated. Use this if you attached a button to `GND` to the GPIO pin without any external pull-up resistor. + * `pull_down`. Use this if you need the internal pull-down resistor activated. + * `pull_off`. Use this to deactivate internal pull-up/pulldown resistors. This is useful if your wiring includes your own (external) pull up / down resistors. +* **edge**: Configures the events in which the GPIO library shall trigger the callback function. Valid settings: + * `falling` (Default). Triggers if the GPIO voltage goes down. + * `rising`. Trigegrs only if the GPIO voltage goes up. + * `both`. Triggers in both cases. +* **bouncetime**: This is a setting of the GPIO library to limit bouncing effects during button usage. Default is `500` ms. +* **antibouncehack**: Despite the integrated bounce reduction of the GPIO library some users may notice false triggers of their buttons (e.g. unrequested / double actions when releasing the button. If you encounter such problems, try setting this setting to `True` to activate an additional countermeasure. + +Note: If you prefer, you may also use `Type: SimpleButton` instead of `Type: Button` - this makes no difference. + +## ShutdownButton + +An extended ShutdownButton can be created using an `ini` entry like these: + +```bash +[Shutdown_without_LED] +enabled: True +Type: ShutdownButton +Pin: 3 + +[Shutdown_with_LED] +enabled: True +Type: ShutdownButton +Pin: 3 +led_pin: 17 +``` + +* **enabled**: This needs to be `True` for the extended shutdown button to work. +* **Pin**: GPIO number of the button +* **led_pin**: GPIO number of the LED (Default is `None`). Note that you should not attach LEDs to GPIO ports without a matching resistor in line. + +Again, there are more parameters than these. In the following comprehensive list you can also find the default values which are used automatically if you leave out these settings: + +* **hold_time**: This parameter controls how many seconds (default: `3.0`) the button has to be hold until shutdown will be initiated. +* **iteration_time**: This parameter determines the flashing speed of the LED indicator. Default value is `0.2` seconds. +* **functionCall**: While the default action is `functionCallShutdown`, you might use this button type even with other functions than system shutdown (again, see [function documentation below](#doc_funcs) for a list of available functions). + +Furthermore, the following settings can be used as described for the [regular buttons](#doc_button): **pull_up_down**, **edge**, **bouncetime**, **antibouncehack** + +Note that using a ShutdownButton without a LED can also be implemented with a normal button like this: + +```bash +[Shutdown] +enabled: True +Type: Button +Pin: 3 +hold_mode: Postpone +hold_time: 3.0 +functionCall: functionCallShutdown +``` + +## TwoButtonControl + +A TwoButtonControl can be created using an `ini` entry like this: + +```bash +[PrevNextStop] +enabled: True +Type: TwoButtonControl +Pin1: 24 +Pin2: 25 +functionCall1: functionCallPlayerNext +functionCall2: functionCallPlayerPrev +functionCallTwoButtons: functionCallPlayerStop +``` + +In this example, you can navigate to the previous or, respectively next track by pushing the respective button. If you push both buttons simultaneously, the player stops. + +It is possible to combine the TwoButtonControl with the Repeat mode, e.g. to increment the volume further while the button keeps getting held: + +```bash +[VolumeControl] +enabled: True +Type: TwoButtonControl +Pin1: 5 +Pin2: 6 +hold_mode: Repeat +hold_time: 0.3 +functionCall1: functionCallVolU +functionCall2: functionCallVolD +functionCallTwoButtons: functionCallVol0 +``` + +In this example, the volume will be in-/decreased step-wise using intervals of 0.3 seconds while the respective button is held. If both buttons are pushed simultaneously, the player is muted (volume 0). In this example, Pin1 is used for increasing the volume, while Pin2 decreases it. + +Furthermore, the following settings can be used as described for the [regular buttons](#doc_button): **pull_up_down**, **edge**, **bouncetime**, **antibouncehack** + +## RotaryEncoder + +A RotaryEncoder can be created using an `ini` entry like this: + +```bash +[VolumeControl] +enabled: True +Type: RotaryEncoder +Pin1: 7 +Pin2: 8 +timeBase: 0.02 +functionCall1: functionCallVolU +functionCall2: functionCallVolD +``` + +Pin1 and FunctionCall1 correspond to rotary direction "up", while Pin2 and FunctionCall2 correspond to "down". +Note that the old configuration entries PinUp/PinDown and functionCallUp/functionCallDown are deprecated and might stop working in future. + +## StatusLED + +A StatusLED can be created using an `ini` entry like this: + +```bash +[StatusLED] +enabled: True +Type: StatusLED +Pin: 14 +``` + +* **Pin**: GPIO number of the LED (mandatory option). Note that you should not attach LEDs to GPIO ports without a matching resistor in line. + +Note: If you prefer, you may also use `Type: MPDStatusLED` instead of `Type: StatusLED` - this makes no difference. + +## Further examples + +By tapping the potential of the features presented above, you can create buttons like this: + +### Play random tracks or folders + +If you have buttons to navigate to the next/previous track it might be a good idea to define that holding these buttons for a certain time (e.g. 2 seconds) will activate a random (surpise!) track or even folder/card. This might look like this + +```bash +[NextOrRand] +enabled: True +Type: Button +Pin: 24 +pull_up_down: pull_up +hold_time: 2.0 +hold_mode: SecondFunc +functionCall: functionCallPlayerNext +functionCall2: functionCallPlayerRandomTrack + +[PrevOrRand] +enabled: True +Type: Button +Pin: 25 +pull_up_down: pull_up +hold_time: 2.0 +hold_mode: SecondFunc +functionCall: functionCallPlayerPrev +functionCall2: functionCallPlayerRandomFolder +``` + +### Short and long jumps + +If you are using two buttons to jump backwards or forwards within the current track, you can use the repeated hold action to allow larger jumps: + +```bash +[SkipForward] +enabled: True +Type: Button +Pin: 24 +pull_up_down: pull_up +hold_time: 5.0 +hold_mode: SecondFuncRepeat +functionCall: functionCallPlayerSeekFwd +functionCall2: functionCallPlayerSeekFarFwd +``` + +In this example, a short press initiates a short jump forward by 10 seconds (functionCallPlayerSeekFwd) while holding the button will cause further, longer jumps. In this case it will cause a jump of 1 minute forward (functionCallPlayerSeekFarFwd) every 5 seconds. If you wish, you can adjust these values in `components/gpio_control/function_calls.py`. +For jumping backwards, this can be done equivalently (see [function list below](#doc_funcs)). + +## Functions + +The available functions are defined/implemented in `components/gpio_control/function_calls.py`: + +* **functionCallShutdown**: System shutdown +* **functionCallVolU**: Volume up +* **functionCallVolD**: Volume down +* **functionCallVol0**: Mute +* **functionCallPlayerNext**: Next track +* **functionCallPlayerPrev**: Previous track +* **functionCallPlayerPauseForce**: Pause (forced) +* **functionCallPlayerPause**: Pause +* **functionCallRecordStart**: Start recording +* **functionCallRecordStop**: Stop recording +* **functionCallRecordPlayLatest**: Play latest recording +* **functionCallToggleWifi**: Toggle WIFI +* **functionCallPlayerStop**: Stop Player +* **functionCallPlayerSeekFwd**: Seek 10 seconds forward +* **functionCallPlayerSeekBack**: Seek 10 seconds backward +* **functionCallPlayerSeekFarFwd**: Seek 1 minute forward +* **functionCallPlayerSeekFarBack**: Seek 1 minute backward +* **functionCallPlayerRandomTrack**: Jumps to random track (within current playlist) +* **functionCallPlayerRandomCard**: Activate a random card +* **functionCallPlayerRandomFolder**: Play a random folder + +## Troubleshooting + +If you encounter bouncing effects with your buttons like unrequested/double actions after releasing a button, you can try to set `antibouncehack` to True: + +```bash +[NextSong] +enabled: True +Type: Button +Pin: 26 +functionCall: functionCallPlayerNext +antibouncehack: True +``` + +Instead of adding this to each button, you can also define it as default for all elements, by inserting the statement into the `Default` section which can be found at the beginning of the `~/RPi-Jukebox-RFID/settings/gpio_settings.ini` file: + +```bash +[DEFAULT] +enabled: True +antibouncehack: True +``` diff --git a/components/gpio_control/config_compatibility.py b/components/gpio_control/config_compatibility.py new file mode 100644 index 000000000..62922c299 --- /dev/null +++ b/components/gpio_control/config_compatibility.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import configparser +import os +from shutil import copyfile + +def Ini_CheckAndUpgrade(config): + has_changed = False + for section in config.sections(): + # enable: True --> enabled: True + # enable: False --> enabled: False + if config.has_option(section, 'enable'): + v = config.getboolean(section, 'enable', fallback=False) + config.remove_option(section, 'enable') + has_changed = True + if not config.has_option(section, 'enabled'): + config.set(section, 'enabled', 'True' if v else 'False') + # pull_up: True --> pull_up_down: pull_up + # pull_up: False --> pull_up_down: pull_off + if config.has_option(section, 'pull_up'): + v = config.getboolean(section, 'pull_up', fallback=True) + config.remove_option(section, 'pull_up') + has_changed = True + if not config.has_option(section, 'pull_up_down'): + config.set(section, 'pull_up_down', 'pull_up' if v else 'pull_off') + # hold_repeat: True --> hold_mode: Repeat + # hold_repeat: False --> hold_mode: None + if config.has_option(section, 'hold_repeat'): + v = config.getboolean(section, 'hold_repeat', fallback=False) + config.remove_option(section, 'hold_repeat') + has_changed = True + if not config.has_option(section, 'hold_mode'): + config.set(section, 'hold_mode', 'Repeat' if v else 'None') + # time_pressed --> hold_time + if config.has_option(section, 'time_pressed'): + v = config.getfloat(section, 'time_pressed') + config.remove_option(section, 'time_pressed') + has_changed = True + if not config.has_option(section, 'hold_time'): + config.set(section, 'hold_time', str(v)) + #PinUp: --> Pin1 + #PinDown: --> Pin2 + if config.has_option(section, 'PinUp'): + v = config.getint(section, 'PinUp') + config.remove_option(section, 'PinUp') + has_changed = True + if not config.has_option(section, 'Pin1'): + config.set(section, 'Pin1', str(v)) + if config.has_option(section, 'PinDown'): + v = config.getint(section, 'PinDown') + config.remove_option(section, 'PinDown') + has_changed = True + if not config.has_option(section, 'Pin2'): + config.set(section, 'Pin2', str(v)) + # functionCallUp --> functionCall1 + # functionCallDown --> functionCall2 + if config.has_option(section, 'functionCallUp'): + v = config.get(section, 'functionCallUp') + config.remove_option(section, 'functionCallUp') + has_changed = True + if not config.has_option(section, 'functionCall1'): + config.set(section, 'functionCall1', v) + if config.has_option(section, 'functionCallDown'): + v = config.get(section, 'functionCallDown') + config.remove_option(section, 'functionCallDown') + has_changed = True + if not config.has_option(section, 'functionCall2'): + config.set(section, 'functionCall2', v) + + return has_changed + + +def ConfigCompatibilityChecks(config, config_path): + # Check for deprecated settings in gpio_settings.ini + if not Ini_CheckAndUpgrade(config): + return + + # If we reach here, gpio_settings.ini needed some patching... + + # Try creating a backup of the previous ini file + backup_path = config_path+'.bak' + if os.path.isfile(backup_path): + return + copyfile(config_path, backup_path) + + # Save fixed gpio_settings.ini + with open(config_path, 'w') as inifile: + config.write(inifile) diff --git a/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini b/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini index b17d48f9d..31d2df340 100755 --- a/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini +++ b/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini @@ -22,13 +22,12 @@ functionCallUp: functionCallPlayerNext enabled: True Type: Button Pin: 17 -hold_time: 1 pull_up_down: pull_down edge: raising functionCall: functionCallShutdown [PlayPause] -enable: True +enabled: True Type: Button Pin: 25 pull_up_down: pull_up diff --git a/components/gpio_control/example_configs/gpio_settings.ini b/components/gpio_control/example_configs/gpio_settings.ini index cc226084e..b05004c7a 100755 --- a/components/gpio_control/example_configs/gpio_settings.ini +++ b/components/gpio_control/example_configs/gpio_settings.ini @@ -1,16 +1,18 @@ [DEFAULT] enabled: True +antibouncehack: False + [VolumeControl] enabled: True Type: TwoButtonControl ;or RotaryEncoder -PinUp: 5 -PinDown: 6 -pull_up: True +Pin1: 5 +Pin2: 6 +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat timeBase: 0.1 ;only for RotaryEncoder -functionCallDown: functionCallVolD -functionCallUp: functionCallVolU +functionCall1: functionCallVolU +functionCall2: functionCallVolD functionCallTwoButtons: functionCallVol0 ;only for TwoButtonControl [PrevNextControl] @@ -21,95 +23,99 @@ Pin2: 23 functionCall1: functionCallPlayerPrev functionCall2: functionCallPlayerNext functionCallTwoButtons: None -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: False +hold_mode: None [PlayPause] enabled: True Type: Button Pin: 27 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerPause [Shutdown] enabled: False Type: Button Pin: 3 -pull_up: True +pull_up_down: pull_up +hold_mode: Postpone hold_time: 2 functionCall: functionCallShutdown +[PauseShutdown] +enabled: False +Type: Button +Pin: 3 +pull_up_down: pull_up +hold_time: 2.0 +hold_mode: SecondFunc +functionCall: functionCallPlayerPause +functionCall2: functionCallShutdown + [Volume0] enabled: False Type: Button Pin: 17 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallVol0 [VolumeUp] enabled: False Type: Button Pin: 16 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat functionCall: functionCallVolU [VolumeDown] enabled: False Type: Button Pin: 19 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat functionCall: functionCallVolD [NextSong] enabled: False Type: Button Pin: 26 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerNext [PrevSong] enabled: False Type: Button Pin: 20 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerPrev [FastForward] enabled: false Type: Button Pin: 7 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerSeekFwd [Rewind] enabled: false Type: Button Pin: 8 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerSeekBack [Halt] enabled: False Type: Button Pin: 25 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerPauseForce [RFIDDevice] -enable: True +enabled: True Type: Button Pin: 21 -pull_up: True +pull_up_down: pull_up functionCall: functionCallPlayerStop diff --git a/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini b/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini index 206fc4b0d..303d0888d 100644 --- a/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini +++ b/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini @@ -7,9 +7,7 @@ enabled: True Type: RotaryEncoder PinUp: 17 PinDown: 22 -pull_up: True -hold_time: 0.3 -hold_repeat: True +pull_up_down: pull_up timeBase: 0.2 ; only for rotary encoder functionCallDown: functionCallVolD @@ -27,27 +25,26 @@ Pin2: 12 functionCall1: functionCallPlayerPrev functionCall2: functionCallPlayerNext functionCallTwoButtons: functionCallPlayerStop -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: False +hold_mode: None [Shutdown] enabled: False Type: Button Pin: 3 -hold_time: 2 functionCall: functionCallShutdown [PlayPause] -enable: True +enabled: True Type: Button Pin: 27 -pull_up: True +pull_up_down: pull_up functionCall: functionCallPlayerPause [StatusLED] -enable: True +enabled: True Type: MPDStatusLED Pin: 16 diff --git a/components/gpio_control/example_configs/gpio_settings_status_led.ini b/components/gpio_control/example_configs/gpio_settings_status_led.ini new file mode 100644 index 000000000..68e6461b2 --- /dev/null +++ b/components/gpio_control/example_configs/gpio_settings_status_led.ini @@ -0,0 +1,7 @@ +[DEFAULT] +enabled: True + +[StatusLED] +enabled: True +Type: StatusLED +Pin: 14 diff --git a/components/gpio_control/example_configs/gpio_settings_test.ini b/components/gpio_control/example_configs/gpio_settings_test.ini index a1ab63273..79e365f61 100644 --- a/components/gpio_control/example_configs/gpio_settings_test.ini +++ b/components/gpio_control/example_configs/gpio_settings_test.ini @@ -28,9 +28,9 @@ Pin2: 3 functionCall1: functionCallPlayerPrev functionCall2: functionCallPlayerNext functionCallTwoButtons: None -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: False +hold_mode: None [Shutdown] @@ -38,45 +38,48 @@ enabled: True Type: ShutdownButton Pin: 3 hold_time: 2 +iteration_time: 0.2 +; led_pin: , if LED used functionCall: functionCallShutdown [Volume0] enabled: False Type: Button Pin: 13 -pull_up: True +pull_up_down: pull_up functionCall: functionCallVol0 [VolumeUp] enabled: False Type: Button Pin: 16 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat functionCall: functionCallVolU [VolumeDown] enabled: False Type: Button Pin: 19 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat + [NextSong] enabled: False Type: Button Pin: 26 -pull_up: True +pull_up_down: pull_up [PrevSong] enabled: False Type: Button Pin: 20 -pull_up: True +pull_up_down: pull_up [Halt] enabled: False Type: Button Pin: 21 -pull_up: True +pull_up_down: pull_up diff --git a/components/gpio_control/function_calls.py b/components/gpio_control/function_calls.py index b87f96b8b..5f6deb1ff 100644 --- a/components/gpio_control/function_calls.py +++ b/components/gpio_control/function_calls.py @@ -4,84 +4,90 @@ import os import pathlib -logger = logging.getLogger(__name__) -playout_control_relative_path = "../../scripts/playout_controls.sh" -function_calls_absolute_path = str(pathlib.Path(__file__).parent.absolute()) -playout_control = os.path.abspath(os.path.join(function_calls_absolute_path, playout_control_relative_path)) +class phoniebox_function_calls: + def __init__(self): + self.logger = logging.getLogger(__name__) -def functionCallShutdown(*args): - function_call("{command} -c=shutdown".format(command=playout_control), shell=True) + playout_control_relative_path = "../../scripts/playout_controls.sh" + function_calls_absolute_path = str(pathlib.Path(__file__).parent.absolute()) + self.playout_control = os.path.abspath(os.path.join(function_calls_absolute_path, playout_control_relative_path)) + def functionCallShutdown(self, *args): + function_call("{command} -c=shutdown".format(command=self.playout_control), shell=True) -def functionCallVolU(steps=None): - if steps is None: - function_call("{command} -c=volumeup".format(command=playout_control), shell=True) - else: - function_call("{command} -c=volumeup -v={steps}".format(steps=steps, - command=playout_control), - shell=True) - - -def functionCallVolD(steps=None): - if steps is None: - function_call("{command} -c=volumedown".format(command=playout_control), shell=True) - else: - function_call("{command} -c=volumedown -v={steps}".format(steps=steps, - command=playout_control), - shell=True) - - -def functionCallVol0(*args): - function_call("{command} -c=mute".format(command=playout_control), shell=True) + def functionCallVolU(self, steps=None): + if steps is None: + function_call("{command} -c=volumeup".format(command=self.playout_control), shell=True) + else: + function_call("{command} -c=volumeup -v={steps}".format(steps=steps, + command=self.playout_control), + shell=True) + def functionCallVolD(self, steps=None): + if steps is None: + function_call("{command} -c=volumedown".format(command=self.playout_control), shell=True) + else: + function_call("{command} -c=volumedown -v={steps}".format(steps=steps, + command=self.playout_control), + shell=True) -def functionCallPlayerNext(*args): - function_call("{command} -c=playernext".format(command=playout_control), shell=True) + def functionCallVol0(self, *args): + function_call("{command} -c=mute".format(command=self.playout_control), shell=True) + def functionCallPlayerNext(self, *args): + function_call("{command} -c=playernext".format(command=self.playout_control), shell=True) -def functionCallPlayerPrev(*args): - function_call("{command} -c=playerprev".format(command=playout_control), shell=True) + def functionCallPlayerPrev(self, *args): + function_call("{command} -c=playerprev".format(command=self.playout_control), shell=True) + def functionCallPlayerPauseForce(self, *args): + function_call("{command} -c=playerpauseforce".format(command=self.playout_control), shell=True) -def functionCallPlayerPauseForce(*args): - function_call("{command} -c=playerpauseforce".format(command=playout_control), shell=True) + def functionCallPlayerPause(self, *args): + function_call("{command} -c=playerpause".format(command=self.playout_control), shell=True) + def functionCallRecordStart(self, *args): + function_call("{command} -c=recordstart".format(command=self.playout_control), shell=True) -def functionCallPlayerPause(*args): - function_call("{command} -c=playerpause".format(command=playout_control), shell=True) + def functionCallRecordStop(self, *args): + function_call("{command} -c=recordstop".format(command=self.playout_control), shell=True) + def functionCallRecordPlayLatest(self, *args): + function_call("{command} -c=recordplaylatest".format(command=self.playout_control), shell=True) -def functionCallRecordStart(*args): - function_call("{command} -c=recordstart".format(command=playout_control), shell=True) - - -def functionCallRecordStop(*args): - function_call("{command} -c=recordstop".format(command=playout_control), shell=True) - - -def functionCallRecordPlayLatest(*args): - function_call("{command} -c=recordplaylatest".format(command=playout_control), shell=True) + def functionCallToggleWifi(self, *args): + function_call("{command} -c=togglewifi".format(command=self.playout_control), shell=True) + def functionCallPlayerStop(self, *args): + function_call("{command} -c=playerstop".format(command=self.playout_control), + shell=True) -def functionCallToggleWifi(*args): - function_call("{command} -c=togglewifi".format(command=playout_control), shell=True) + def functionCallPlayerSeekFwd(self, *args): + function_call("{command} -c=playerseek -v=+10".format(command=self.playout_control), shell=True) + def functionCallPlayerSeekBack(self, *args): + function_call("{command} -c=playerseek -v=-10".format(command=self.playout_control), shell=True) -def functionCallPlayerStop(*args): - function_call("{command} -c=playerstop".format(command=playout_control), - shell=True) + def functionCallPlayerSeekFarFwd(self, *args): + function_call("{command} -c=playerseek -v=+60".format(command=self.playout_control), shell=True) + def functionCallPlayerSeekFarBack(self, *args): + function_call("{command} -c=playerseek -v=-60".format(command=self.playout_control), shell=True) -def functionCallPlayerSeekFwd(*args): - function_call("{command} -c=playerseek -v=+10".format(command=playout_control), shell=True) + def functionCallPlayerRandomTrack(self, *args): + function_call("{command} -c=randomtrack".format(command=self.playout_control), shell=True) + def functionCallPlayerRandomCard(self, *args): + function_call("{command} -c=randomcard".format(command=self.playout_control), shell=True) -def functionCallPlayerSeekBack(*args): - function_call("{command} -c=playerseek -v=-10".format(command=playout_control), shell=True) + def functionCallPlayerRandomFolder(self, *args): + function_call("{command} -c=randomfolder".format(command=self.playout_control), shell=True) + def functionCallBluetoothToggle(self, *args): + function_call("{command} -c=bluetoothtoggle -v=toggle".format(command=self.playout_control), shell=True) -def getFunctionCall(functionName): - logger.error('Get FunctionCall: {} {}'.format(functionName, functionName in locals())) - getattr(sys.modules[__name__], str) - return locals().get(functionName, None) + def getFunctionCall(self, functionName): + self.logger.error('Get FunctionCall: {} {}'.format(functionName, functionName in locals())) + getattr(sys.modules[__name__], str) + return locals().get(functionName, None) diff --git a/components/gpio_control/gpio_control.py b/components/gpio_control/gpio_control.py index 3e6134495..44ad68df1 100755 --- a/components/gpio_control/gpio_control.py +++ b/components/gpio_control/gpio_control.py @@ -6,115 +6,124 @@ from GPIODevices import * import function_calls from signal import pause - from RPi import GPIO - -# from GPIODevices.VolumeControl import VolumeControl -# from GPIODevices.led import LED, MPDStatusLED - -GPIO.setmode(GPIO.BCM) - -logger = logging.getLogger(__name__) - - -def getFunctionCall(function_name): - try: - if function_name != 'None': - return getattr(function_calls, function_name) - except AttributeError: - logger.error('Could not find FunctionCall {function_name}'.format(function_name=function_name)) - return lambda *args: None - - -def generate_device(config, deviceName): - print(deviceName) - device_type = config.get('Type') - if deviceName.lower() == 'VolumeControl'.lower(): - return VolumeControl(config) - elif device_type == 'TwoButtonControl': - logger.info('adding TwoButtonControl') - return TwoButtonControl( - config.getint('Pin1'), - config.getint('Pin2'), - getFunctionCall(config.get('functionCall1')), - getFunctionCall(config.get('functionCall2')), - functionCallTwoBtns=getFunctionCall(config.get('functionCallTwoButtons')), - pull_up=config.getboolean('pull_up', fallback=True), - hold_repeat=config.getboolean('hold_repeat', False), - hold_time=config.getfloat('hold_time', fallback=0.3), - name=deviceName) - elif device_type in ('Button', 'SimpleButton'): - return SimpleButton(config.getint('Pin'), - action=getFunctionCall(config.get('functionCall')), - name=deviceName, - bouncetime=config.getint('bouncetime', fallback=500), - edge=config.get('edge', fallback='FALLING'), - hold_repeat=config.getboolean('hold_repeat', False), - hold_time=config.getfloat('hold_time', fallback=0.3), - pull_up_down=config.get('pull_up_down', fallback=GPIO.PUD_UP)) - elif device_type == 'LED': - return LED(config.getint('Pin'), - name=deviceName, - initial_value=config.getboolean('initial_value', fallback=True)) - elif device_type == 'MPDStatusLED': - return MPDStatusLED(config.getint('Pin'), - host=config.get('host', fallback='localhost'), - port=config.getint('port', fallback=6600), - name=deviceName - ) - elif device_type == 'RotaryEncoder': - return RotaryEncoder(config.getint('pinUp'), - config.getint('pinDown'), - getFunctionCall(config.get('functionCallUp')), - getFunctionCall(config.get('functionCallDown')), - config.getfloat('timeBase', fallback=0.1), +from config_compatibility import ConfigCompatibilityChecks + + +class gpio_control(): + + def __init__(self, function_calls): + self.devices = [] + self.function_calls = function_calls + + GPIO.setmode(GPIO.BCM) + + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(__name__) + self.logger.setLevel('INFO') + self.logger.info('GPIO Started') + + def getFunctionCall(self, function_name): + try: + if function_name != 'None': + return getattr(self.function_calls, function_name) + except AttributeError: + self.logger.error('Could not find FunctionCall {function_name}'.format(function_name=function_name)) + return lambda *args: None + + def generate_device(self, config, deviceName): + print(deviceName) + device_type = config.get('Type') + if device_type == 'TwoButtonControl': + self.logger.info('adding TwoButtonControl') + return TwoButtonControl( + config.getint('Pin1'), + config.getint('Pin2'), + self.getFunctionCall(config.get('functionCall1')), + self.getFunctionCall(config.get('functionCall2')), + functionCallTwoBtns=self.getFunctionCall(config.get('functionCallTwoButtons')), + pull_up_down=config.get('pull_up_down', fallback='pull_up'), + hold_mode=config.get('hold_mode', fallback=None), + hold_time=config.getfloat('hold_time', fallback=0.3), + bouncetime=config.getint('bouncetime', fallback=500), + edge=config.get('edge', fallback='falling'), + antibouncehack=config.getboolean('antibouncehack', fallback=False), name=deviceName) - elif device_type == 'ShutdownButton': - return ShutdownButton(pin=config.getint('Pin'), - action=getFunctionCall(config.get('functionCall',fallback='functionCallShutdown')), - name=deviceName, - bouncetime=config.getint('bouncetime', fallback=500), - edge=config.get('edge', fallback='FALLING'), - hold_repeat=config.getboolean('hold_repeat', False), - hold_time=config.getfloat('hold_time', fallback=0.3), - pull_up_down=config.get('pull_up_down', fallback=GPIO.PUD_UP)) - logger.warning('cannot find {}'.format(deviceName)) - return None - - -def get_all_devices(config): - devices = [] - logger.info(config.sections()) - for section in config.sections(): - if config.getboolean(section, 'enabled', fallback=False): - logger.info('adding GPIO-Device, {}'.format(section)) - device = generate_device(config[section], section) - if device is not None: - devices.append(device) + elif device_type in ('Button', 'SimpleButton'): + return SimpleButton(config.getint('Pin'), + action=self.getFunctionCall(config.get('functionCall')), + action2=self.getFunctionCall(config.get('functionCall2', fallback='None')), + name=deviceName, + bouncetime=config.getint('bouncetime', fallback=500), + antibouncehack=config.getboolean('antibouncehack', fallback=False), + edge=config.get('edge', fallback='falling'), + hold_mode=config.get('hold_mode', fallback=None), + hold_time=config.getfloat('hold_time', fallback=0.3), + pull_up_down=config.get('pull_up_down', fallback='pull_up')) + elif device_type == 'LED': + return LED(config.getint('Pin'), + name=deviceName, + initial_value=config.getboolean('initial_value', fallback=True)) + elif device_type in ('StatusLED', 'MPDStatusLED'): + return StatusLED(config.getint('Pin'), name=deviceName) + elif device_type == 'RotaryEncoder': + return RotaryEncoder(config.getint('Pin1'), + config.getint('Pin2'), + self.getFunctionCall(config.get('functionCall1')), + self.getFunctionCall(config.get('functionCall2')), + config.getfloat('timeBase', fallback=0.1), + name=deviceName) + elif device_type == 'ShutdownButton': + return ShutdownButton(pin=config.getint('Pin'), + action=self.getFunctionCall(config.get('functionCall', fallback='functionCallShutdown')), + name=deviceName, + bouncetime=config.getint('bouncetime', fallback=500), + antibouncehack=config.getboolean('antibouncehack', fallback=False), + edge=config.get('edge', fallback='falling'), + hold_time=config.getfloat('hold_time', fallback=3.0), + iteration_time=config.getfloat('iteration_time', fallback=0.2), + led_pin=config.getint('led_pin', fallback=None), + pull_up_down=config.get('pull_up_down', fallback='pull_up')) + self.logger.warning('cannot find {}'.format(deviceName)) + return None + + def get_all_devices(self, config): + self.logger.info(config.sections()) + for section in config.sections(): + if config.getboolean(section, 'enabled', fallback=False): + self.logger.info('adding GPIO-Device, {}'.format(section)) + device = self.generate_device(config[section], section) + if device is not None: + self.devices.append(device) + else: + self.logger.warning('Could not add Device {} with {}'.format(section, config.items(section))) else: - logger.warning('Could not add Device {} with {}'.format(section, config.items(section))) - else: - logger.info('Device {} not enabled'.format(section)) - for dev in devices: - print(dev) - return devices + self.logger.info('Device {} not enabled'.format(section)) + return self.devices + def print_all_devices(self): + for dev in self.devices: + print(dev) -if __name__ == "__main__": + def gpio_loop(self): + self.logger.info('Ready for taking actions') + try: + pause() + except KeyboardInterrupt: + pass + self.logger.info('Exiting GPIO Control') - logging.basicConfig(level='INFO') - logger = logging.getLogger() - logger.setLevel('INFO') - config = configparser.ConfigParser(inline_comment_prefixes=";") +if __name__ == "__main__": + config = configparser.ConfigParser(inline_comment_prefixes=";", delimiters=(':', '=')) config_path = os.path.expanduser('/home/pi/RPi-Jukebox-RFID/settings/gpio_settings.ini') config.read(config_path) + + ConfigCompatibilityChecks(config, config_path) + + phoniebox_function_calls = function_calls.phoniebox_function_calls() + gpio_controler = gpio_control(phoniebox_function_calls) - devices = get_all_devices(config) - print(devices) - logger.info('Ready for taking actions') - try: - pause() - except KeyboardInterrupt: - pass - logger.info('Exiting GPIO Control') + devices = gpio_controler.get_all_devices(config) + gpio_controler.print_all_devices() + gpio_controler.gpio_loop() diff --git a/components/gpio_control/test/gpio_settings_test.ini b/components/gpio_control/test/gpio_settings_test.ini index a1ab63273..a0e318244 100644 --- a/components/gpio_control/test/gpio_settings_test.ini +++ b/components/gpio_control/test/gpio_settings_test.ini @@ -5,14 +5,12 @@ enabled: True ; RotaryEncoder ; TwoButtonControl ; SimpleButton = Button +; StatusLED [VolumeControl] enabled: True Type: RotaryEncoder PinUp: 16 PinDown: 19 -pull_up: True -hold_time: 0.3 -hold_repeat: True timeBase: 0.1 ; timeBase only for rotary encoder functionCallDown: functionCallVolD @@ -28,9 +26,9 @@ Pin2: 3 functionCall1: functionCallPlayerPrev functionCall2: functionCallPlayerNext functionCallTwoButtons: None -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: False +hold_mode: None [Shutdown] @@ -38,45 +36,53 @@ enabled: True Type: ShutdownButton Pin: 3 hold_time: 2 +iteration_time: 0.2 +; led_pin: , if LED used functionCall: functionCallShutdown [Volume0] enabled: False Type: Button Pin: 13 -pull_up: True +pull_up_down: pull_up functionCall: functionCallVol0 [VolumeUp] enabled: False Type: Button Pin: 16 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat functionCall: functionCallVolU [VolumeDown] enabled: False Type: Button Pin: 19 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat + [NextSong] enabled: False Type: Button Pin: 26 -pull_up: True +pull_up_down: pull_up [PrevSong] enabled: False Type: Button Pin: 20 -pull_up: True +pull_up_down: pull_up [Halt] enabled: False Type: Button Pin: 21 -pull_up: True +pull_up_down: pull_up + +[StatusLED] +enabled: True +Type: StatusLED +Pin: 14 diff --git a/components/gpio_control/test/test_SimpleButton.py b/components/gpio_control/test/test_SimpleButton.py index 87a3afe6f..7f118ebb4 100644 --- a/components/gpio_control/test/test_SimpleButton.py +++ b/components/gpio_control/test/test_SimpleButton.py @@ -53,7 +53,7 @@ def test_hold(self, simple_button): GPIO.LOW = 0 GPIO.input.side_effect = [False, False, False, True] simple_button.hold_time = 0 - simple_button.hold_repeat = True + simple_button.hold_mode = 'Repeat' calls = mockedAction.call_count simple_button.callbackFunctionHandler(simple_button.pin) assert mockedAction.call_count - calls == 4 diff --git a/components/gpio_control/test/test_TwoButtonControl.py b/components/gpio_control/test/test_TwoButtonControl.py index 619ea4dd2..2741430d1 100644 --- a/components/gpio_control/test/test_TwoButtonControl.py +++ b/components/gpio_control/test/test_TwoButtonControl.py @@ -92,8 +92,8 @@ def two_button_controller(): functionCallBtn1=mockedFunction1, functionCallBtn2=mockedFunction2, functionCallTwoBtns=mockedFunction3, - pull_up=True, - hold_repeat=False, + pull_up_down='pull_up', + hold_mode=None, hold_time=0.3, name='TwoButtonControl') @@ -105,8 +105,8 @@ def test_init(self): functionCallBtn1=mockedFunction1, functionCallBtn2=mockedFunction2, functionCallTwoBtns=mockedFunction3, - pull_up=True, - hold_repeat=False, + pull_up_down='pull_up', + hold_mode=None, hold_time=0.3, name='TwoButtonControl') @@ -152,5 +152,5 @@ def test_btn1_and_btn2_pressed(self, two_button_controller): assert mockedFunction3.call_count == 2 def test_repr(self, two_button_controller): - expected = "" + expected = "" assert repr(two_button_controller) == expected diff --git a/components/gpio_control/test/test_gpio_control.py b/components/gpio_control/test/test_gpio_control.py index 87a7c8714..aca6e8b3a 100644 --- a/components/gpio_control/test/test_gpio_control.py +++ b/components/gpio_control/test/test_gpio_control.py @@ -2,7 +2,8 @@ import logging from mock import patch, MagicMock -from components.gpio_control.gpio_control import get_all_devices +from gpio_control import gpio_control +import function_calls # def test_functionCallTwoButtonsOnlyBtn2Pressed(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, # functionCallBothPressedMock): @@ -29,6 +30,10 @@ def testMain(): config = configparser.ConfigParser() config.read('./gpio_settings_test.ini') - devices = get_all_devices(config) - print(devices) + + phoniebox_function_calls = function_calls.phoniebox_function_calls() + gpio_controler = gpio_control(phoniebox_function_calls) + + devices = gpio_controler.get_all_devices(config) + gpio_controler.print_all_devices() pass diff --git a/components/rfid-reader/PN532/README.md b/components/rfid-reader/PN532/README.md index ab8a2fead..6cf747c29 100644 --- a/components/rfid-reader/PN532/README.md +++ b/components/rfid-reader/PN532/README.md @@ -8,7 +8,6 @@ Similar shields/breakout boards, based on the same chip might work, but have not It has been tested with the I2C interface. Using SPI might work as well, but it has not been tested. - 1. Connect the PN532 RFID reader to the GPIO pins | PN532 | Raspberry Pi | Raspi Pins | @@ -33,7 +32,6 @@ It has been tested with the I2C interface. Using SPI might work as well, but it - check `sudo i2cdetect -y 1` - output should look like this: - 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- diff --git a/components/rfid-reader/RC522/README.md b/components/rfid-reader/RC522/README.md index a8111a806..156ce5a09 100644 --- a/components/rfid-reader/RC522/README.md +++ b/components/rfid-reader/RC522/README.md @@ -13,3 +13,5 @@ 4. Restart the phoniebox-rfid-reader service: - `sudo systemctl restart phoniebox-rfid-reader.service` + +Be aware that unlike a few other installations with this card reader the phoniebox requires the IRQ pin to be connected (on the raspberry pi and zero normaly to GPIO 24 or PIN 18). diff --git a/components/rfid-reader/RC522/requirements.txt b/components/rfid-reader/RC522/requirements.txt index 775bee697..6643d6123 100644 --- a/components/rfid-reader/RC522/requirements.txt +++ b/components/rfid-reader/RC522/requirements.txt @@ -2,4 +2,4 @@ # You need to install these with `sudo python3 -m pip install --upgrade --force-reinstall -q -r requirements.txt` # pi-rc522 use latest version from Github -git+git://github.com/ondryaso/pi-rc522.git#egg=pi-rc522 +git+https://github.com/ondryaso/pi-rc522.git#egg=pi-rc522 diff --git a/components/smart-home-automation/MQTT-protocol/README.md b/components/smart-home-automation/MQTT-protocol/README.md index a71a595ec..da00394b7 100644 --- a/components/smart-home-automation/MQTT-protocol/README.md +++ b/components/smart-home-automation/MQTT-protocol/README.md @@ -6,16 +6,17 @@ This module will integrate Phoniebox into a Smart Home environment and make it r # Use Cases * let your Smart Home control Phoniebox based on time schedules - * disable wifi in the evening when Phoniebox is used as a sleeping device - * shutdown at night when it's finally bedtime - * lower the volume in the mornings (to keep you asleep) -* control Phoniebox via Voice Assistants like [Snips](https://snips.ai) (which also uses MQTT!), Google Home, Amazon Echo,... + * disable wifi in the evening when Phoniebox is used as a sleeping device + * shutdown at night when it's finally bedtime + * lower the volume in the mornings (to keep you asleep) +* control Phoniebox via Voice Assistants like [Rhasspy](https://github.com/rhasspy/rhasspy) (which also uses MQTT!), Google Home, Amazon Echo,... * let Phoniebox play an informational note to your kids that the weather outside is great and they should consider going outside (if your Smart Home has weather-based sensors) * run statistics on when and how your kid uses Phoniebox - * arrange terms with your kid how long the Phoniebox can be used (e.g. max. 2h per day) - * monitor if your kid complies with those terms or enforce them if need be + * arrange terms with your kid how long the Phoniebox can be used (e.g. max. 2h per day) + * monitor if your kid complies with those terms or enforce them if need be # How it works + Phoniebox' MQTT client connects to the MQTT server that is defined in the `SETTINGS` section of the script itself. It is able to connect by authenticating... 1) with username and password @@ -26,127 +27,146 @@ Please check your MQTT server configuration regarding which authentication metho Phoniebox' MQTT client will do the following things: 1. at startup send state and version info about Phoniebox to - - `phoniebox/version` (e.g. 1.2-rc1) - - `phoniebox/edition` (e.g. classic) - - `phoniebox/state` (online) - - `phoniebox/disk_total` (disk size in Gigabytes) - - `phoniebox/disk_avail` (available disk size in Gigabytes) + * `phoniebox/version` (e.g. 1.2-rc1) + * `phoniebox/edition` (e.g. classic) + * `phoniebox/state` (online) + * `phoniebox/disk_total` (disk size in Gigabytes) + * `phoniebox/disk_avail` (available disk size in Gigabytes) 2. at shutdown send state info to - - `phoniebox/state` (offline) + * `phoniebox/state` (offline) 3. periodically send *all* attributes to `phoniebox/attribute/$attributeName` (this interval can be defined through `refreshIntervalPlaying` and `refreshIntervalIdle` in the `SETTINGS` section) -4. listen for attribute requests on `phoniebox/get/$attribute` -5. listen for commands on `phoniebox/cmd/$command` (if a command needs a parameter it has to be provided via payload) +4. send specific events to `phoniebox/event/$eventName` right away +5. listen for attribute requests on `phoniebox/get/$attribute` +6. listen for commands on `phoniebox/cmd/$command` (if a command needs a parameter it has to be provided via payload) + +## Topic: phoniebox/event/$event + +This is a read-only topic. The following events trigger immediate messages through this topic: + +* card_swiped + +Use these topics to get notified of time-critical events right away rather than having to wait for the periodic send of all attributes or requesting an attribute through the get topic. Currently the only event triggering a message is the "card_swiped" event with the obvious use-case of letting your smart home react upon swiped cards. These cards needn't be configured in Phoniebox as the MQTT daemon will just relay any swiped card to the MQTT server. So it's possible to have a "dim lights" card that will not trigger any Phoniebox action but is picked up by your smart home to dim the lights. ## Topic: phoniebox/get/$attribute -MQTT clients can (additionally to the periodic updates) request an attribute of Phoniebox. Sending an empty payload to `phoniebox/get/volume` will trigger Phoniebox' MQTT client to fetch the current volume from MPD and send the result to `phoniebox/attribute/volume`. + +MQTT clients can (additionally to the periodic updates) request an attribute of Phoniebox. Sending an empty payload to `phoniebox/get/volume` will trigger Phoniebox' MQTT client to fetch the current volume from MPD and send the result to `phoniebox/attribute/volume`. ### Possible attributes -- volume -- mute -- repeat -- random -- state -- file -- artist -- albumartist -- title -- album -- track -- elapsed -- duration -- trackdate -- last_card -- maxvolume -- volstep -- idletime -- rfid (status of rfid service [true/false]) -- gpio (status of gpio service [true/false]) -- remaining_stopafter [minutes left until "stop" is triggered] -- remaining_shutdownafter [minutes left until shutdown] -- remaining_idle [minutes left for the idle shutdown timer] -- throttling -- temperature + +* volume +* mute +* repeat +* repeat_mode [off / single / playlist] +* random +* state +* file +* artist +* albumartist +* title +* album +* track +* elapsed +* duration +* trackdate +* last_card +* maxvolume +* volstep +* idletime +* rfid (status of rfid service [true/false]) +* gpio (status of gpio service [true/false]) +* remaining_stopafter [minutes left until "stop" is triggered] +* remaining_shutdownafter [minutes left until shutdown] +* remaining_idle [minutes left for the idle shutdown timer] +* throttling +* temperature ### Help + Sending empty payload to `phoniebox/get/help` will be responded by a list of all possible attributes to `phoniebox/available_attributes` ## Topic: phoniebox/cmd/$command + MQTT clients can send commands to Phoniebox. Sending an empty payload to `phoniebox/cmd/volumeup` will trigger Phoniebox' MQTT client to execute that command. If the command needs a parameter it has to be provided in the payload (e.g. for `setmaxvolume` a payload with the maximum volume is required). ### Possible commands -- volumeup -- volumedown -- mute -- playerplay -- playerpause -- playernext -- playerprev -- playerstop -- playerrewind -- playershuffle -- playerreplay -- scan -- shutdown -- shutdownsilent -- reboot -- disablewifi + +* volumeup +* volumedown +* mute +* playerplay +* playerpause +* playernext +* playerprev +* playerstop +* playerrewind +* playershuffle +* playerreplay +* scan +* shutdown +* shutdownsilent +* reboot +* disablewifi ### Possible commands (that require a parameter!) -- setvolume [0-100] -- setvolstep [0-100] -- setmaxvolume [0-100] -- setidletime [in minutes] -- playerseek [e.g. +20 for 20sec ahead or -12 for 12sec back] -- shutdownafter [in minutes; 0 = remove timer] -- playerstopafter [in minutes] -- playerrepeat [off / single / playlist] -- rfid [start / stop] -- gpio [start / stop] -- swipecard [card ID] -- playfolder [folder name (not path)] -- playfolderrecursive [folder name (not path)] + +* setvolume [0-100] +* setvolstep [0-100] +* setmaxvolume [0-100] +* setidletime [in minutes] +* playerseek [e.g. +20 for 20sec ahead or -12 for 12sec back] +* shutdownafter [in minutes; 0 = remove timer] +* playerstopafter [in minutes] +* playerrepeat [off / single / playlist] +* rfid [start / stop] +* gpio [start / stop] +* swipecard [card ID] +* playfolder [folder name (not path)] +* playfolderrecursive [folder name (not path)] ### Help + Sending empty payload to `phoniebox/cmd/help` will be responded by a list of all possible commands to `phoniebox/available_commands` and `phoniebox/available_commands_with_params` # Installation Install missing python packages for MQTT: -~~~ -sudo pip3 install paho-mqtt +~~~bash +sudo python3 -m pip install --upgrade --force-reinstall -q -r requirements.txt ~~~ All relevant files can be found in the folder: -~~~ +~~~bash components/smart-home-automation/MQTT-protocol/ ~~~ ## Auto-Starting the daemon at bootup * The daemon is run by executing the script `daemon_mqtt_client.py` which will run in an endless loop. -* There's a sample service file (`phoniebox-mqtt-client.service.stretch-default.sample`) that can be used to register the daemon to be run at bootup. +* There's a sample service file (`phoniebox-mqtt-client.service.stretch-default.sample`) that can be used to register the daemon to be run at bootup. * It is currently not integrated into the one-line-install script so please run the following commands to do it manually. First step: copy files to destination locations: -~~~ +~~~bash # First copy the daemon script and service config file to the correct directory: sudo cp /home/pi/RPi-Jukebox-RFID/components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py /home/pi/RPi-Jukebox-RFID/scripts/ sudo cp /home/pi/RPi-Jukebox-RFID/components/smart-home-automation/MQTT-protocol/phoniebox-mqtt-client.service.stretch-default.sample /etc/systemd/system/phoniebox-mqtt-client.service ~~~ -Now edit the file `/home/pi/RPi-Jukebox-RFID/scripts/daemon_mqtt_client.py` to match your requirements. -Now continue and activate the service. +Now edit the `SETTINGS` section in `/home/pi/RPi-Jukebox-RFID/scripts/daemon_mqtt_client.py` to match your environment (MQTT connection details etc.). Now continue and activate the service. -~~~ +~~~bash # Now systemd has to be notified that there's a new service file: sudo systemctl daemon-reload + # Now enable the service file, to start it on reboot: sudo systemctl enable phoniebox-mqtt-client + # And now you can reboot to have your daemon running or start it manually: sudo systemctl start phoniebox-mqtt-client + # To see if the reader process is running use the following command: sudo systemctl status phoniebox-mqtt-client ~~~ diff --git a/components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py b/components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py index 4ccb639fa..b99b89eab 100644 --- a/components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py +++ b/components/smart-home-automation/MQTT-protocol/daemon_mqtt_client.py @@ -1,31 +1,41 @@ #!/usr/bin/env python3 +import datetime +import os +import re +import ssl +import subprocess +import time +from threading import * + +import inotify.adapters import paho.mqtt.client as mqtt -import os, subprocess, re, ssl, time, datetime - # ---------------------------------------------------------- # Prerequisites # ---------------------------------------------------------- -# pip3 install paho-mqtt +# pip3 install paho-mqtt inotify # ---------------------------------------------------------- # SETTINGS # ---------------------------------------------------------- -DEBUG = False -mqttBaseTopic = "phoniebox" # MQTT base topic -mqttClientId = "phoniebox" # MQTT client ID -mqttHostname = "openHAB" # MQTT server hostname -mqttPort = 8883 # MQTT server port (typically 1883 for unencrypted, 8883 for encrypted) -mqttUsername = "" # username for user/pass based authentication -mqttPassword = "" # password for user/pass based authentication -mqttCA = "/home/pi/MQTT/mqtt-ca.crt" # path to server certificate for certificate-based authentication -mqttCert = "/home/pi/MQTT/mqtt-client-phoniebox.crt" # path to client certificate for certificate-based authentication -mqttKey = "/home/pi/MQTT/mqtt-client-phoniebox.key" # path to client keyfile for certificate-based authentication -mqttConnectionTimeout = 60 # in seconds; timeout for MQTT connection -refreshIntervalPlaying = 5 # in seconds; how often should the status be sent to MQTT (while playing) -refreshIntervalIdle = 30 # in seconds; how often should the status be sent to MQTT (when NOT playing) +config = { + "DEBUG": False, + "mqttBaseTopic": "phoniebox", # MQTT base topic + "mqttClientId": "phoniebox", # MQTT client ID + "mqttHostname": "openHAB", # MQTT server hostname + "mqttPort": 8883, # MQTT server port (typically 1883 for unencrypted, 8883 for encrypted) + "mqttUsername": "", # username for user/pass based authentication + "mqttPassword": "", # password for user/pass based authentication + "mqttCA": "/home/pi/MQTT/mqtt-ca.crt", # path to server certificate for certificate-based authentication + "mqttCert": "/home/pi/MQTT/mqtt-client-phoniebox.crt", # path to client certificate for certificate-based authentication + "mqttKey": "/home/pi/MQTT/mqtt-client-phoniebox.key", # path to client keyfile for certificate-based authentication + "mqttConnectionTimeout": 60, # in seconds; timeout for MQTT connection + "refreshIntervalPlaying": 5, # in seconds; how often should the status be sent to MQTT (while playing) + "refreshIntervalIdle": 30, # in seconds; how often should the status be sent to MQTT (when NOT playing) +} + # ---------------------------------------------------------- # DO NOT CHANGE BELOW @@ -35,12 +45,97 @@ path = os.path.dirname(os.path.realpath(__file__)) # internal refresh interval -refreshInterval = refreshIntervalPlaying +refreshInterval = config.get("refreshIntervalPlaying") # list of available commands and attributes -arAvailableCommands = ['volumeup', 'volumedown', 'mute', 'playerplay', 'playerpause', 'playernext', 'playerprev', 'playerstop', 'playerrewind', 'playershuffle', 'playerreplay', 'scan', 'shutdown', 'shutdownsilent', 'reboot', 'disablewifi'] -arAvailableCommandsWithParam = ['setvolume', 'setvolstep', 'setmaxvolume', 'setidletime', 'playerseek', 'shutdownafter', 'playerstopafter', 'playerrepeat', 'rfid', 'gpio', 'swipecard', 'playfolder', 'playfolderrecursive'] -arAvailableAttributes = ['volume', 'mute', 'repeat', 'random', 'state', 'file', 'artist', 'albumartist', 'title', 'album', 'track', 'elapsed', 'duration', 'trackdate', 'last_card', 'maxvolume', 'volstep', 'idletime', 'rfid', 'gpio', 'remaining_stopafter', 'remaining_shutdownafter', 'remaining_idle', 'throttling', 'temperature'] +arAvailableCommands = [ + "volumeup", + "volumedown", + "mute", + "playerplay", + "playerpause", + "playernext", + "playerprev", + "playerstop", + "playerrewind", + "playershuffle", + "playerreplay", + "scan", + "shutdown", + "shutdownsilent", + "reboot", + "disablewifi", +] +arAvailableCommandsWithParam = [ + "setvolume", + "setvolstep", + "setmaxvolume", + "setidletime", + "playerseek", + "shutdownafter", + "shutdownvolumereduction", + "playerstopafter", + "playerrepeat", + "rfid", + "gpio", + "swipecard", + "playfolder", + "playfolderrecursive", +] +arAvailableAttributes = [ + "volume", + "mute", + "repeat", + "repeat_mode", + "random", + "state", + "file", + "artist", + "albumartist", + "title", + "album", + "track", + "elapsed", + "duration", + "trackdate", + "last_card", + "maxvolume", + "volstep", + "idletime", + "rfid", + "gpio", + "remaining_stopafter", + "remaining_shutdownafter", + "remaining_shutdownvolumereduction", + "remaining_idle", + "throttling", + "temperature", +] + + +def watchForNewCard(): + i = inotify.adapters.Inotify() + i.add_watch(path + "/../settings/Latest_RFID") + + # wait for inotify events + for event in i.event_gen(yield_nones=False): + if event is not None: + # fetch event attributes + (e_header, e_type_names, e_path, e_filename) = event + + # file was closed and written => a new card was swiped + if "IN_CLOSE_WRITE" in e_type_names: + # fetch card ID + cardid = readfile(path + "/../settings/Latest_RFID") + + # publish event "card_swiped" + client.publish( + config.get("mqttBaseTopic") + "/event/card_swiped", payload=cardid + ) + print(" --> Publishing event card_swiped = " + cardid) + + # process all attributes + processGet("all") def on_connect(client, userdata, flags, rc): @@ -55,11 +150,33 @@ def on_connect(client, userdata, flags, rc): disk_total, disk_avail = disk_stats() # publish general server info - client.publish(mqttBaseTopic + "/state", payload="online", qos=1, retain=True) - client.publish(mqttBaseTopic + "/version", payload=version, qos=1, retain=True) - client.publish(mqttBaseTopic + "/edition", payload=edition, qos=1, retain=True) - client.publish(mqttBaseTopic + "/disk_total", payload=disk_total, qos=1, retain=True) - client.publish(mqttBaseTopic + "/disk_avail", payload=disk_avail, qos=1, retain=True) + client.publish( + config.get("mqttBaseTopic") + "/state", payload="online", qos=1, retain=True + ) + client.publish( + config.get("mqttBaseTopic") + "/version", + payload=version, + qos=1, + retain=True, + ) + client.publish( + config.get("mqttBaseTopic") + "/edition", + payload=edition, + qos=1, + retain=True, + ) + client.publish( + config.get("mqttBaseTopic") + "/disk_total", + payload=disk_total, + qos=1, + retain=True, + ) + client.publish( + config.get("mqttBaseTopic") + "/disk_avail", + payload=disk_avail, + qos=1, + retain=True, + ) else: print("Connection could NOT be established. Return-Code:", rc) @@ -80,7 +197,9 @@ def on_message(client, userdata, message): print(" - topic =", message.topic) print(" - value =", message.payload.decode("utf-8")) - regex_extract = re.search(mqttBaseTopic + '\/(.*)\/(.*)', message.topic) + regex_extract = re.search( + config.get("mqttBaseTopic") + "\/(.*)\/(.*)", message.topic + ) message_topic = regex_extract.group(1).lower() message_subtopic = regex_extract.group(2).lower() message_payload = message.payload.decode("utf-8") @@ -97,16 +216,28 @@ def processCmd(command, parameter): if command == "help": availableCommands = ", ".join(arAvailableCommands) availableCommandsWithParam = ", ".join(arAvailableCommandsWithParam) - client.publish(mqttBaseTopic + "/available_commands", payload=availableCommands) - client.publish(mqttBaseTopic + "/available_commands_with_params", payload=availableCommandsWithParam) + client.publish( + config.get("mqttBaseTopic") + "/available_commands", + payload=availableCommands, + ) + client.publish( + config.get("mqttBaseTopic") + "/available_commands_with_params", + payload=availableCommandsWithParam, + ) print(" --> Publishing response available_commands =", availableCommands) - print(" --> Publishing response available_commands_with_params =", availableCommandsWithParam) + print( + " --> Publishing response available_commands_with_params =", + availableCommandsWithParam, + ) # toggle RFID reader daemon elif command == "rfid": parameter = parameter.lower() if parameter == "start" or parameter == "stop": - subprocess.call(["sudo /bin/systemctl " + parameter + " phoniebox-rfid-reader.service"], shell=True) + subprocess.call( + ["sudo /bin/systemctl " + parameter + " phoniebox-rfid-reader.service"], + shell=True, + ) else: print(" --> Expecting parameter start or stop") @@ -114,7 +245,14 @@ def processCmd(command, parameter): elif command == "gpio": parameter = parameter.lower() if parameter == "start" or parameter == "stop": - subprocess.call(["sudo /bin/systemctl " + parameter + " phoniebox-gpio-control.service"], shell=True) + subprocess.call( + [ + "sudo /bin/systemctl " + + parameter + + " phoniebox-gpio-control.service" + ], + shell=True, + ) else: print(" --> Expecting parameter start or stop") @@ -126,12 +264,17 @@ def processCmd(command, parameter): # play folder elif command == "playfolder": print(" --> Playing folder", parameter) - subprocess.call([path + "/rfid_trigger_play.sh -d='" + parameter + "'"], shell=True) + subprocess.call( + [path + "/rfid_trigger_play.sh -d='" + parameter + "'"], shell=True + ) # play folder (recursive) elif command == "playfolderrecursive": print(" --> Playing folder " + parameter + " (recursive)") - subprocess.call([path + "/rfid_trigger_play.sh -d='" + parameter + "' -v=recursive"], shell=True) + subprocess.call( + [path + "/rfid_trigger_play.sh -d='" + parameter + "' -v=recursive"], + shell=True, + ) # all the other known commands w/o param elif command in arAvailableCommands: @@ -140,8 +283,17 @@ def processCmd(command, parameter): # all the other known commands /w param elif command in arAvailableCommandsWithParam: - print(" --> Sending command " + command + " and value " + parameter + " to playout_controls.sh") - subprocess.call([path + "/playout_controls.sh -c=" + command + " -v=" + parameter], shell=True) + print( + " --> Sending command " + + command + + " and value " + + parameter + + " to playout_controls.sh" + ) + subprocess.call( + [path + "/playout_controls.sh -c=" + command + " -v=" + parameter], + shell=True, + ) # we don't know this command else: @@ -149,7 +301,7 @@ def processCmd(command, parameter): return # this was a known command => refresh all attributes as they might have changed - client.publish(mqttBaseTopic + "/get/all", payload="") + client.publish(config.get("mqttBaseTopic") + "/get/all", payload="") def processGet(attribute): @@ -158,18 +310,29 @@ def processGet(attribute): # respond with all attributes if attribute == "all": for attribute in mpd_status: - client.publish(mqttBaseTopic + "/attribute/" + attribute, payload=mpd_status[attribute]) - print(" --> Publishing response " + attribute + " = " + mpd_status[attribute]) + client.publish( + config.get("mqttBaseTopic") + "/attribute/" + attribute, + payload=mpd_status[attribute], + ) + print( + " --> Publishing response " + attribute + " = " + mpd_status[attribute] + ) # list all possible attributes elif attribute == "help": availableAttributes = ", ".join(arAvailableAttributes) - client.publish(mqttBaseTopic + "/available_attributes", payload=availableAttributes) + client.publish( + config.get("mqttBaseTopic") + "/available_attributes", + payload=availableAttributes, + ) print(" --> Publishing response", availableAttributes) # all the other known attributes elif attribute in mpd_status: - client.publish(mqttBaseTopic + "/attribute/" + attribute, payload=mpd_status[attribute]) + client.publish( + config.get("mqttBaseTopic") + "/attribute/" + attribute, + payload=mpd_status[attribute], + ) print(" --> Publishing response " + attribute + " = " + mpd_status[attribute]) # we don't know this attribute @@ -178,10 +341,10 @@ def processGet(attribute): def disk_stats(): - statvfs = os.statvfs('/home/pi') - size_total = statvfs.f_frsize * statvfs.f_blocks # total + statvfs = os.statvfs("/home/pi") + size_total = statvfs.f_frsize * statvfs.f_blocks # total # size_avail = statvfs.f_frsize * statvfs.f_bfree # actual free - size_avail = statvfs.f_frsize * statvfs.f_bavail # free for non-root + size_avail = statvfs.f_frsize * statvfs.f_bavail # free for non-root return round(size_total / 1073741824, 1), round(size_avail / 1073741824, 1) @@ -194,22 +357,28 @@ def readfile(filepath): def isServiceRunning(svc): - cmd = ['/bin/systemctl', 'status', svc] - status = subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip() - if re.search('\n.*Active:.*running.*\n', status): + cmd = ["/bin/systemctl", "status", svc] + status = subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode("utf-8").rstrip() + if re.search("\n.*Active:.*running.*\n", status): return "true" else: return "false" def linux_job_remaining(job_name): - cmd = ['sudo', 'atq', '-q', job_name] - dtQueue = subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip() - - regex = re.search('(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', dtQueue) + cmd = ["sudo", "atq", "-q", job_name] + dtQueue = ( + subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode("utf-8").rstrip() + ) + + regex = re.search( + "(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)", dtQueue + ) if regex: dtNow = datetime.datetime.now() - dtQueue = datetime.datetime.strptime(dtNow.strftime("%d.%m.%Y") + " " + regex.group(5), "%d.%m.%Y %H:%M:%S") + dtQueue = datetime.datetime.strptime( + dtNow.strftime("%d.%m.%Y") + " " + regex.group(5), "%d.%m.%Y %H:%M:%S" + ) # subtract 1 day if queued for the next day if dtNow > dtQueue: @@ -221,42 +390,52 @@ def linux_job_remaining(job_name): def getOsThrottling(): - codes = { - 0: "under-voltage detected", - 1: "arm frequency capped", - 2: "currently throttled", - 3: "soft temperature limit active", - 16: "under-voltage has occurred", - 17: "arm frequency capped has occurred", - 18: "throttling has occurred", - 19: "soft temperature limit has occurred" - } - - p = subprocess.Popen(['vcgencmd', 'get_throttled'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - throttling, err = p.communicate() - codeHex = throttling.rstrip().split("0x")[1] - - # code is zero => no issue - if codeHex == "0": - return "OK" - - # analyse returned code - result = [] - codeBinary = "" - for fourbits in codeHex: - codeBinary = codeBinary + bin(int(fourbits, 16))[2:].zfill(4) - codeBinary = codeBinary[::-1] - for bitNumber in range(len(codeBinary)): - if codeBinary[bitNumber] == "1": - result.append(codes[bitNumber]) - return "WARNING: " + ", ".join(result) + codes = { + 0: "under-voltage detected", + 1: "arm frequency capped", + 2: "currently throttled", + 3: "soft temperature limit active", + 16: "under-voltage has occurred", + 17: "arm frequency capped has occurred", + 18: "throttling has occurred", + 19: "soft temperature limit has occurred", + } + + p = subprocess.Popen( + ["vcgencmd", "get_throttled"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + throttling, err = p.communicate() + codeHex = throttling.rstrip().split("0x")[1] + + # code is zero => no issue + if codeHex == "0": + return "OK" + + # analyse returned code + result = [] + codeBinary = "" + for fourbits in codeHex: + codeBinary = codeBinary + bin(int(fourbits, 16))[2:].zfill(4) + codeBinary = codeBinary[::-1] + for bitNumber in range(len(codeBinary)): + if codeBinary[bitNumber] == "1": + result.append(codes[bitNumber]) + return "WARNING: " + ", ".join(result) def getOsTemperature(): - p = subprocess.Popen(['vcgencmd', 'measure_temp'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - temperature, err = p.communicate() - temperature = temperature.rstrip().split("=")[1] - return temperature + p = subprocess.Popen( + ["vcgencmd", "measure_temp"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + temperature, err = p.communicate() + temperature = temperature.rstrip().split("=")[1] + return temperature def normalizeTrueFalse(s): @@ -273,6 +452,37 @@ def regex(needle, hay, exception="-"): else: return exception +def getDuration(status): + """ Find the duration of the track in the output from mpd status""" + + # try to get the duration value + duration = regex("\nduration: (.*)\n", status) + + if duration == "-": + # if the duration attribute is missing try to get the time + # this attribute value is split into two parts by ":" + # first is the elapsed time and the second part is the duration + duration = regex("\ntime: .*:(.*)\n", status, "0") + + return int(float(duration)) + +REPEAT_MODE_OFF = "off" +REPEAT_MODE_SINGLE = "single" +REPEAT_MODE_PLAYLIST = "playlist" + + +def get_repeat_mode(repeat, status): + """ Returns the repeat mode that is selected in mpd """ + + if repeat == "false": + return REPEAT_MODE_OFF + + single = regex("\nsingle: (.*)\n", status) + if single == "0": + return REPEAT_MODE_PLAYLIST + + return REPEAT_MODE_SINGLE + def fetchData(): # use global refreshInterval as this function is run as a thread through the paho-mqtt loop @@ -281,15 +491,18 @@ def fetchData(): result = {} # fetch status from MPD - cmd = ['nc', '-w', '1', 'localhost', '6600'] - input = 'status\ncurrentsong\nclose'.encode('utf-8') - status = subprocess.run(cmd, stdout=subprocess.PIPE, input=input).stdout.decode('utf-8') + cmd = ["nc", "-w", "1", "localhost", "6600"] + input = "status\ncurrentsong\nclose".encode("utf-8") + status = subprocess.run(cmd, stdout=subprocess.PIPE, input=input).stdout.decode( + "utf-8" + ) # interpret status - result["state"] = regex('\nstate: (.*)\n', status).lower() - result["volume"] = regex('\nvolume: (.*)\n', status) - result["repeat"] = normalizeTrueFalse(regex('\nrepeat: (.*)\n', status)) - result["random"] = normalizeTrueFalse(regex('\nrandom: (.*)\n', status)) + result["state"] = regex("\nstate: (.*)\n", status).lower() + result["volume"] = regex("\nvolume: (.*)\n", status) + result["repeat"] = normalizeTrueFalse(regex("\nrepeat: (.*)\n", status)) + result["repeat_mode"] = get_repeat_mode(result["repeat"], status) + result["random"] = normalizeTrueFalse(regex("\nrandom: (.*)\n", status)) # interpret mute state based on volume if result["volume"] == "0": @@ -300,31 +513,53 @@ def fetchData(): # interpret metadata when in play/pause mode if result["state"] != "stop": - result["file"] = regex('\nfile: (.*)\n', status) - result["artist"] = regex('\nArtist: (.*)\n', status) - result["albumartist"] = regex('\nAlbumArtist: (.*)\n', status) - result["title"] = regex('\nTitle: (.*)\n', status) - result["album"] = regex('\nAlbum: (.*)\n', status) - result["track"] = regex('\nTrack: (.*)\n', status, "0") - result["trackdate"] = regex('\nDate: (.*)\n', status) + result["file"] = regex("\nfile: (.*)\n", status) + result["artist"] = regex("\nArtist: (.*)\n", status) + result["albumartist"] = regex("\nAlbumArtist: (.*)\n", status) + result["title"] = regex("\nTitle: (.*)\n", status) + result["album"] = regex("\nAlbum: (.*)\n", status) + result["track"] = regex("\nTrack: (.*)\n", status, "0") + result["trackdate"] = regex("\nDate: (.*)\n", status) if result["title"] == "-": result["title"] = result["file"] - elapsed = int(float(regex('\nelapsed: (.*)\n', status, "0"))) + elapsed = int(float(regex("\nelapsed: (.*)\n", status, "0"))) hours, remainder = divmod(elapsed, 3600) minutes, seconds = divmod(remainder, 60) - result["elapsed"] = '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)) + result["elapsed"] = "{:02}:{:02}:{:02}".format( + int(hours), int(minutes), int(seconds) + ) - duration = int(float(regex('\nduration: (.*)\n', status, "0"))) + duration = getDuration(status) hours, remainder = divmod(duration, 3600) minutes, seconds = divmod(remainder, 60) - result["duration"] = '{:02}:{:02}:{:02}'.format(int(hours), int(minutes), int(seconds)) + result["duration"] = "{:02}:{:02}:{:02}".format( + int(hours), int(minutes), int(seconds) + ) # fetch some more data from global.conf (via playout_controls.sh) - result["maxvolume"] = subprocess.run([path + "/playout_controls.sh", "-c=getmaxvolume"], stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip() - result["volstep"] = subprocess.run([path + "/playout_controls.sh", "-c=getvolstep"], stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip() - result["idletime"] = subprocess.run([path + "/playout_controls.sh", "-c=getidletime"], stdout=subprocess.PIPE).stdout.decode('utf-8').rstrip() + result["maxvolume"] = ( + subprocess.run( + [path + "/playout_controls.sh", "-c=getmaxvolume"], stdout=subprocess.PIPE + ) + .stdout.decode("utf-8") + .rstrip() + ) + result["volstep"] = ( + subprocess.run( + [path + "/playout_controls.sh", "-c=getvolstep"], stdout=subprocess.PIPE + ) + .stdout.decode("utf-8") + .rstrip() + ) + result["idletime"] = ( + subprocess.run( + [path + "/playout_controls.sh", "-c=getidletime"], stdout=subprocess.PIPE + ) + .stdout.decode("utf-8") + .rstrip() + ) # fetch last card result["last_card"] = readfile(path + "/../settings/Latest_RFID") @@ -336,6 +571,7 @@ def fetchData(): # fetch linux jobs result["remaining_stopafter"] = str(linux_job_remaining("s")) result["remaining_shutdownafter"] = str(linux_job_remaining("t")) + result["remaining_shutdownvolumereduction"] = str(linux_job_remaining("q")) result["remaining_idle"] = str(linux_job_remaining("i")) # fetch OS information @@ -344,47 +580,69 @@ def fetchData(): # modify refresh rate depending on play state if result["state"] == "play": - refreshInterval = refreshIntervalPlaying + refreshInterval = config.get("refreshIntervalPlaying") else: - refreshInterval = refreshIntervalIdle + refreshInterval = config.get("refreshIntervalIdle") return result # create client instance -client = mqtt.Client(mqttClientId) +client = mqtt.Client(config.get("mqttClientId")) # configure authentication -if mqttUsername != "" and mqttPassword != "": - client.username_pw_set(username=mqttUsername, password=mqttPassword) - -if mqttCert != "" and mqttKey != "": - if mqttCA != "": - client.tls_set(ca_certs=mqttCA, certfile=mqttCert, keyfile=mqttKey) +if config.get("mqttUsername") and config.get("mqttPassword"): + client.username_pw_set( + username=config.get("mqttUsername"), password=config.get("mqttPassword") + ) + +if config.get("mqttCert") and config.get("mqttKey"): + if config.get("mqttCA"): + client.tls_set( + ca_certs=config.get("mqttCA"), + certfile=config.get("mqttCert"), + keyfile=config.get("mqttKey"), + ) else: - client.tls_set(certfile=mqttCert, keyfile=mqttKey) -elif mqttCA != "": - client.tls_set(ca_certs=mqttCA) + client.tls_set(certfile=config.get("mqttCert"), keyfile=config.get("mqttKey")) +elif config.get("mqttCA"): + client.tls_set(ca_certs=config.get("mqttCA")) # attach event handlers client.on_connect = on_connect client.on_disconnect = on_disconnect client.on_message = on_message -if DEBUG is True: +if config.get("DEBUG") is True: client.on_log = on_log # define last will -client.will_set(mqttBaseTopic + "/state", payload="offline", qos=1, retain=True) +client.will_set( + config.get("mqttBaseTopic") + "/state", payload="offline", qos=1, retain=True +) # connect to MQTT server -print("Connecting to " + mqttHostname + " on port " + str(mqttPort)) -client.connect(mqttHostname, mqttPort, mqttConnectionTimeout) +print( + "Connecting to " + + config.get("mqttHostname") + + " on port " + + str(config.get("mqttPort")) +) +client.connect( + config.get("mqttHostname"), + config.get("mqttPort"), + config.get("mqttConnectionTimeout"), +) # subscribe to topics -print("Subscribing to " + mqttBaseTopic + "/cmd/#") -client.subscribe(mqttBaseTopic + "/cmd/#") -print("Subscribing to " + mqttBaseTopic + "/get/#") -client.subscribe(mqttBaseTopic + "/get/#") +print("Subscribing to " + config.get("mqttBaseTopic") + "/cmd/#") +client.subscribe(config.get("mqttBaseTopic") + "/cmd/#") +print("Subscribing to " + config.get("mqttBaseTopic") + "/get/#") +client.subscribe(config.get("mqttBaseTopic") + "/get/#") + +# register thread for watchForNewCard +tWatchForNewCard = Thread(target=watchForNewCard) +tWatchForNewCard.setDaemon(True) +tWatchForNewCard.start() # start endless loop client.loop_start() diff --git a/components/smart-home-automation/MQTT-protocol/requirements.txt b/components/smart-home-automation/MQTT-protocol/requirements.txt new file mode 100644 index 000000000..b474338b6 --- /dev/null +++ b/components/smart-home-automation/MQTT-protocol/requirements.txt @@ -0,0 +1,5 @@ +# MQTT daemon related requirements +# you need to install these with `sudo python3 -m pip install --upgrade --force-reinstall -q -r requirements.txt` + +paho-mqtt +inotify diff --git a/composer.json b/composer.json index 87c8028a3..1fe5789bf 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "require-dev": { - "phpunit/phpunit": "^8", + "phpunit/phpunit": "^9", "php-mock/php-mock-phpunit": "^2.5" } } diff --git a/composer.lock b/composer.lock index 6ea7069b9..fbcfe6e49 100644 --- a/composer.lock +++ b/composer.lock @@ -4,156 +4,37 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ff4fb5fbea0b5d58eceb058fe5439afb", + "content-hash": "6d659550344086ef6e414765a7a1a227", "packages": [], "packages-dev": [ - { - "name": "badoo/soft-mocks", - "version": "2.0.6", - "source": { - "type": "git", - "url": "https://github.com/badoo/soft-mocks.git", - "reference": "1cc854697c5e2569282f9db1d29cd0766d889989" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/badoo/soft-mocks/zipball/1cc854697c5e2569282f9db1d29cd0766d889989", - "reference": "1cc854697c5e2569282f9db1d29cd0766d889989", - "shasum": "" - }, - "require": { - "nikic/php-parser": "3.0.6", - "php": ">=5.5" - }, - "require-dev": { - "phpunit/phpunit": "^6.5.5", - "vaimo/composer-patches": "3.23.1" - }, - "type": "library", - "extra": { - "patches": { - "phpunit/phpunit": [ - { - "label": "phpunit run file", - "source": "patches/phpunit4.x/phpunit_phpunit.patch", - "version": "^4.8.0", - "level": "1" - }, - { - "label": "phpunit run file", - "source": "patches/phpunit5.x/phpunit_phpunit.patch", - "version": "^5.7.0", - "level": "1" - }, - { - "label": "phpunit run file", - "source": "patches/phpunit6.x/phpunit_phpunit.patch", - "version": "^6.5.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks #1", - "source": "patches/phpunit4.x/phpunit_add_ability_to_set_custom_filename_rewrite_callbacks_1.patch", - "version": "^4.8.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks #1", - "source": "patches/phpunit5.x/phpunit_add_ability_to_set_custom_filename_rewrite_callbacks_1.patch", - "version": "^5.7.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks #1", - "source": "patches/phpunit6.x/phpunit_add_ability_to_set_custom_filename_rewrite_callbacks_1.patch", - "version": "^6.5.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks #2", - "source": "patches/phpunit4.x/phpunit_add_ability_to_set_custom_filename_rewrite_callbacks_2.patch", - "version": "^4.8.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks #2", - "source": "patches/phpunit5.x/phpunit_add_ability_to_set_custom_filename_rewrite_callbacks_2.patch", - "version": "^5.7.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks #2", - "source": "patches/phpunit6.x/phpunit_add_ability_to_set_custom_filename_rewrite_callbacks_2.patch", - "version": "^6.5.0", - "level": "1" - } - ], - "phpunit/php-code-coverage": [ - { - "label": "Add ability to set custom filename rewrite callbacks", - "source": "patches/phpunit4.x/php-code-coverage_add_ability_to_set_custom_filename_rewrite_callbacks.patch", - "version": "^2.1.0", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks", - "source": "patches/phpunit5.x/php-code-coverage_add_ability_to_set_custom_filename_rewrite_callbacks.patch", - "version": "^4.0.4", - "level": "1" - }, - { - "label": "Add ability to set custom filename rewrite callbacks", - "source": "patches/phpunit6.x/php-code-coverage_add_ability_to_set_custom_filename_rewrite_callbacks.patch", - "version": "^5.3.0", - "level": "1" - } - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Badoo Development" - } - ], - "description": "The idea behind \"Soft Mocks\" - as opposed to \"hardcore\" mocks that work on the level of the PHP interpreter (runkit and uopz) - is to rewrite class code on the spot so that it can be inserted in any place. It works by rewriting code on the fly during file inclusion instead of using extensions like runkit or uopz.", - "time": "2019-05-29T13:16:17+00:00" - }, { "name": "doctrine/instantiator", - "version": "1.3.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", + "doctrine/coding-standard": "^9", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" @@ -167,7 +48,7 @@ { "name": "Marco Pivetta", "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "homepage": "https://ocramius.github.io/" } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", @@ -176,154 +57,60 @@ "constructor", "instantiate" ], - "time": "2019-10-21T16:45:58+00:00" - }, - { - "name": "hamcrest/hamcrest-php", - "version": "v2.0.0", - "source": { - "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/776503d3a8e85d4f9a1148614f95b7a608b046ad", - "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad", - "shasum": "" - }, - "require": { - "php": "^5.3|^7.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" - }, - "require-dev": { - "phpunit/php-file-iterator": "1.3.3", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "hamcrest" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "description": "This is the PHP port of Hamcrest Matchers", - "keywords": [ - "test" - ], - "time": "2016-01-20T08:20:44+00:00" - }, - { - "name": "mockery/mockery", - "version": "1.2.4", - "source": { - "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "b3453f75fd23d9fd41685f2148f4abeacabc6405" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/b3453f75fd23d9fd41685f2148f4abeacabc6405", - "reference": "b3453f75fd23d9fd41685f2148f4abeacabc6405", - "shasum": "" - }, - "require": { - "hamcrest/hamcrest-php": "~2.0", - "lib-pcre": ">=7.0", - "php": ">=5.6.0" - }, - "require-dev": { - "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0" + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-0": { - "Mockery": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "funding": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" }, { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" } ], - "description": "Mockery is a simple yet flexible PHP mock object framework", - "homepage": "https://github.com/mockery/mockery", - "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" - ], - "time": "2019-09-30T08:30:27+00:00" + "time": "2022-03-03T08:28:38+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.9.3", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", - "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -337,28 +124,39 @@ "object", "object graph" ], - "time": "2019-08-09T12:45:53+00:00" + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2022-03-03T13:19:32+00:00" }, { "name": "nikic/php-parser", - "version": "v3.0.6", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "0808939f81c1347a3c8a82a5925385a08074b0f1" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0808939f81c1347a3c8a82a5925385a08074b0f1", - "reference": "0808939f81c1347a3c8a82a5925385a08074b0f1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.5" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0|~5.0" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -366,7 +164,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "4.9-dev" } }, "autoload": { @@ -388,32 +186,37 @@ "parser", "php" ], - "time": "2017-06-28T20:53:48+00:00" + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" + }, + "time": "2022-09-04T07:30:47+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.3", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", - "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "phar-io/version": "^2.0", - "php": "^5.6 || ^7.0" + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -443,24 +246,28 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2018-07-08T19:23:20+00:00" + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", - "version": "2.0.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", - "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -490,37 +297,45 @@ } ], "description": "Library for handling version information and constraints", - "time": "2018-07-08T19:19:57+00:00" + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" }, { "name": "php-mock/php-mock", - "version": "2.1.2", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock.git", - "reference": "35379d7b382b787215617f124662d9ead72c15e3" + "reference": "9a55bd8ba40e6da2e97a866121d2c69dedd4952b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock/zipball/35379d7b382b787215617f124662d9ead72c15e3", - "reference": "35379d7b382b787215617f124662d9ead72c15e3", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/9a55bd8ba40e6da2e97a866121d2c69dedd4952b", + "reference": "9a55bd8ba40e6da2e97a866121d2c69dedd4952b", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1" + "php": "^5.6 || ^7.0 || ^8.0", + "phpunit/php-text-template": "^1 || ^2" }, "replace": { "malkusch/php-mock": "*" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0" + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.5" }, "suggest": { "php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock." }, "type": "library", "autoload": { + "files": [ + "autoload.php" + ], "psr-4": { "phpmock\\": [ "classes/", @@ -551,29 +366,39 @@ "test", "test double" ], - "time": "2019-06-05T20:10:01+00:00" + "support": { + "issues": "https://github.com/php-mock/php-mock/issues", + "source": "https://github.com/php-mock/php-mock/tree/2.3.1" + }, + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2022-02-07T18:57:52+00:00" }, { "name": "php-mock/php-mock-integration", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock-integration.git", - "reference": "5a0d7d7755f823bc2a230cfa45058b40f9013bc4" + "reference": "003d585841e435958a02e9b986953907b8b7609b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/5a0d7d7755f823bc2a230cfa45058b40f9013bc4", - "reference": "5a0d7d7755f823bc2a230cfa45058b40f9013bc4", + "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/003d585841e435958a02e9b986953907b8b7609b", + "reference": "003d585841e435958a02e9b986953907b8b7609b", "shasum": "" }, "require": { "php": ">=5.6", - "php-mock/php-mock": "^2", - "phpunit/php-text-template": "^1" + "php-mock/php-mock": "^2.2", + "phpunit/php-text-template": "^1 || ^2" }, "require-dev": { - "phpunit/phpunit": "^4|^5" + "phpunit/phpunit": "^5.7.27 || ^6 || ^7 || ^8 || ^9" }, "type": "library", "autoload": { @@ -604,26 +429,33 @@ "test", "test double" ], - "time": "2017-02-17T21:31:34+00:00" + "support": { + "issues": "https://github.com/php-mock/php-mock-integration/issues", + "source": "https://github.com/php-mock/php-mock-integration/tree/2.1.0" + }, + "time": "2020-02-08T14:40:25+00:00" }, { "name": "php-mock/php-mock-phpunit", - "version": "2.5.0", + "version": "2.6.1", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock-phpunit.git", - "reference": "7df4bd123ce196bbba19f142c4906c20be8ec546" + "reference": "b9ba2db21e7e1c7deba98bc86dcfc6425fb4647d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/7df4bd123ce196bbba19f142c4906c20be8ec546", - "reference": "7df4bd123ce196bbba19f142c4906c20be8ec546", + "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/b9ba2db21e7e1c7deba98bc86dcfc6425fb4647d", + "reference": "b9ba2db21e7e1c7deba98bc86dcfc6425fb4647d", "shasum": "" }, "require": { "php": ">=7", - "php-mock/php-mock-integration": "^2", - "phpunit/phpunit": "^6 || ^7 || ^8" + "php-mock/php-mock-integration": "^2.1", + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9" + }, + "require-dev": { + "phpspec/prophecy": "^1.10.3" }, "type": "library", "autoload": { @@ -658,257 +490,183 @@ "test", "test double" ], - "time": "2019-10-05T21:44:04+00:00" - }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", - "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", - "shasum": "" - }, - "require": { - "php": ">=7.1" + "support": { + "issues": "https://github.com/php-mock/php-mock-phpunit/issues", + "source": "https://github.com/php-mock/php-mock-phpunit/tree/2.6.1" }, - "require-dev": { - "phpunit/phpunit": "~6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "url": "https://github.com/michalbundyra", + "type": "github" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2018-08-07T13:53:10+00:00" + "time": "2022-09-07T20:40:07+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.2", + "name": "phpunit/php-code-coverage", + "version": "9.2.18", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e", - "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/12fddc491826940cf9b7e88ad9664cf51f0f6d0a", + "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a", "shasum": "" }, "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", - "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", - "webmozart/assert": "^1.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.14", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" }, "require-dev": { - "doctrine/instantiator": "^1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.x-dev" + "dev-master": "9.2-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2019-09-12T14:27:41+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", - "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", - "shasum": "" - }, - "require": { - "php": "^7.1", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "^7.1", - "mockery/mockery": "~1", - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" ], - "authors": [ + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.18" + }, + "funding": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2019-08-22T18:11:29+00:00" + "time": "2022-10-27T13:35:33+00:00" }, { - "name": "phpspec/prophecy", - "version": "1.9.0", + "name": "phpunit/php-file-iterator", + "version": "3.0.6", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203", - "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" + "php": ">=7.3" }, "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" + "filesystem", + "iterator" ], - "time": "2019-10-03T11:07:50+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "7.0.8", + "name": "phpunit/php-invoker", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa0d179a13284c7420fc281fc32750e6cc7c9e2f", - "reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": "^7.2", - "phpunit/php-file-iterator": "^2.0.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.1", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^4.2.2", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1.3" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^8.2.2" + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" }, "suggest": { - "ext-xdebug": "^2.7.2" + "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "7.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -927,39 +685,47 @@ "role": "lead" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ - "coverage", - "testing", - "xunit" + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2019-09-17T06:24:36+00:00" + "time": "2020-09-28T05:58:55+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "2.0.2", + "name": "phpunit/php-text-template", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "050bedf145a257b1ff02746c31894800e5122946" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", - "reference": "050bedf145a257b1ff02746c31894800e5122946", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -978,32 +744,49 @@ "role": "lead" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "filesystem", - "iterator" + "template" ], - "time": "2018-09-13T20:33:42+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" }, { - "name": "phpunit/php-text-template", - "version": "1.2.1", + "name": "phpunit/php-timer", + "version": "5.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -1020,40 +803,83 @@ "role": "lead" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "template" + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2015-06-21T13:50:34+00:00" + "time": "2020-10-26T13:16:10+00:00" }, { - "name": "phpunit/php-timer", - "version": "2.1.2", + "name": "phpunit/phpunit", + "version": "9.5.26", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2", + "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2", "shasum": "" }, "require": { - "php": "^7.1" + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" }, - "require-dev": { - "phpunit/phpunit": "^7.0" + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" }, + "bin": [ + "phpunit" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "9.5-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -1069,38 +895,57 @@ "role": "lead" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", "keywords": [ - "timer" + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } ], - "time": "2019-06-07T04:22:29+00:00" + "time": "2022-10-28T06:00:21+00:00" }, { - "name": "phpunit/php-token-stream", - "version": "3.1.1", + "name": "sebastian/cli-parser", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": "^7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1112,75 +957,51 @@ "license": [ "BSD-3-Clause" ], - "authors": [ + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2019-09-17T06:23:10+00:00" + "time": "2020-09-28T06:08:49+00:00" }, { - "name": "phpunit/phpunit", - "version": "8.4.3", + "name": "sebastian/code-unit", + "version": "1.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "67f9e35bffc0dd52d55d565ddbe4230454fd6a4e" + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/67f9e35bffc0dd52d55d565ddbe4230454fd6a4e", - "reference": "67f9e35bffc0dd52d55d565ddbe4230454fd6a4e", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2.0", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.9.1", - "phar-io/manifest": "^1.0.3", - "phar-io/version": "^2.0.1", - "php": "^7.2", - "phpspec/prophecy": "^1.8.1", - "phpunit/php-code-coverage": "^7.0.7", - "phpunit/php-file-iterator": "^2.0.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1.2", - "sebastian/comparator": "^3.0.2", - "sebastian/diff": "^3.0.2", - "sebastian/environment": "^4.2.2", - "sebastian/exporter": "^3.1.1", - "sebastian/global-state": "^3.0.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^2.0.1", - "sebastian/type": "^1.1.3", - "sebastian/version": "^2.0.1" + "php": ">=7.3" }, "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0.0" + "phpunit/phpunit": "^9.3" }, - "bin": [ - "phpunit" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "8.4-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1199,39 +1020,44 @@ "role": "lead" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2019-11-06T09:42:23+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1251,34 +1077,44 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" }, { "name": "sebastian/comparator", - "version": "3.0.2", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", - "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { - "php": "^7.1", - "sebastian/diff": "^3.0", - "sebastian/exporter": "^3.1" + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^7.1" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1291,6 +1127,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1302,10 +1142,6 @@ { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" } ], "description": "Provides the functionality to compare PHP values for equality", @@ -1315,33 +1151,100 @@ "compare", "equality" ], - "time": "2018-07-12T15:12:46+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" }, { "name": "sebastian/diff", - "version": "3.0.2", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1354,13 +1257,13 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], "description": "Diff implementation", @@ -1371,27 +1274,37 @@ "unidiff", "unified diff" ], - "time": "2019-02-04T06:01:07+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" }, { "name": "sebastian/environment", - "version": "4.2.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^7.5" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-posix": "*" @@ -1399,7 +1312,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.2-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -1424,34 +1337,44 @@ "environment", "hhvm" ], - "time": "2019-05-05T09:05:15+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-04-03T09:37:03+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.2", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/recursion-context": "^3.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1486,35 +1409,45 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], - "time": "2019-09-14T09:02:43+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.0", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", - "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", "shasum": "" }, "require": { - "php": "^7.2", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-uopz": "*" @@ -1522,7 +1455,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1545,34 +1478,101 @@ "keywords": [ "global state" ], - "time": "2019-02-01T05:30:01+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" }, { "name": "sebastian/object-enumerator", - "version": "3.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1592,32 +1592,42 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-08-03T12:35:26+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "1.1.1", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1637,32 +1647,42 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2017-03-29T09:07:27+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/recursion-context", - "version": "3.0.0", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1675,14 +1695,14 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, { "name": "Adam Harvey", "email": "aharvey@php.net" @@ -1690,29 +1710,42 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-03T06:23:57+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" }, { "name": "sebastian/resource-operations", - "version": "2.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", - "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1732,32 +1765,42 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2018-10-04T04:07:39+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" }, { "name": "sebastian/type", - "version": "1.1.3", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", - "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { - "php": "^7.2" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^8.2" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -1778,29 +1821,39 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "time": "2019-07-02T08:10:15+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", - "version": "2.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1821,85 +1874,37 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.12.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.12-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, + "funding": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { "name": "theseer/tokenizer", - "version": "1.1.3", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -1919,57 +1924,17 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-06-13T22:48:21+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0", - "symfony/polyfill-ctype": "^1.8" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "url": "https://github.com/theseer", + "type": "github" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2019-08-24T08:43:50+00:00" + "time": "2021-07-28T10:34:58+00:00" } ], "aliases": [], @@ -1978,5 +1943,6 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.3.0" } diff --git a/docs/2022-Phoniebox-Calendar.jpg b/docs/2022-Phoniebox-Calendar.jpg new file mode 100644 index 000000000..a7541ed35 Binary files /dev/null and b/docs/2022-Phoniebox-Calendar.jpg differ diff --git a/docs/FIRMWARE_UPDATE.md b/docs/FIRMWARE_UPDATE.md index ac88c3fae..d3ad0f879 100644 --- a/docs/FIRMWARE_UPDATE.md +++ b/docs/FIRMWARE_UPDATE.md @@ -3,26 +3,37 @@ **This has not been tested yet**: The analogue audio out quality of the RPi3 is horrible. Learn more about the [impact of the firmware update in the raspberry forum](https://www.raspberrypi.org/forums/viewtopic.php?f=29&t=167934). In the same forum you can find information on the [firmaware update effect on the analogue audio out](https://www.raspberrypi.org/forums/viewtopic.php?f=29&t=136445). If you were to try to update the firmware, now would be the right moment. I tried the firmware update successfully, but haven't yet built a Phoniebox on top of it. So if you are new to the RPi, skip the following lines and go to the next chapter, configuring your keyboard. Open a terminal window and update the firmware typing: -~~~~ + +~~~~bash sudo rpi-update ~~~~ + Then reboot the machine: -~~~~ + +~~~~bash sudo reboot ~~~~ + Now open the terminal window again. The howto suggests to switch off the HDMI audio out: -~~~~ + +~~~~bash amixer cset numid=3 1 ~~~~ + Open the config file with: -~~~~ + +~~~~bash sudo nano /boot/config.txt ~~~~ + and add the following line: -~~~~ + +~~~~bash audio_pwm_mode=2 ~~~~ + Better safe than sorry, reboot the machine once more: -~~~~ + +~~~~bash sudo reboot ~~~~ diff --git a/docs/GPIO-BUTTONS.md b/docs/GPIO-BUTTONS.md index 87a8f7c75..134cb8df2 100644 --- a/docs/GPIO-BUTTONS.md +++ b/docs/GPIO-BUTTONS.md @@ -1,3 +1,6 @@ +Deprecated, please see [wiki](/wiki) for the latest version + +--------------------------------------------- # Control Jukebox with buttons / GPIO @@ -8,7 +11,7 @@ **Add buttons to your jukebox to control volume, skip tracks and more.** Before we start: -One of the plus points about this projects, at least in my mind, +One of the plus points about this projects, at least in my mind, was the fact that you don't need a soldering iron to build it. Everything is USB, plug and play, thank you, boot and go. @@ -21,7 +24,7 @@ confirmation that this works :) Enough said, here we go. ---- +--------------------------------------------- ## Pin numbering on the RPi @@ -72,22 +75,22 @@ There are a number of different ways to connect a button. The easiest one is wel We need to run [GPIO Zero](https://gpiozero.readthedocs.io/en/stable/), a simple interface to GPIO devices with Raspberry Pi. GPIO Zero is installed by default in Raspbian Jessie. To install see the [installing](https://gpiozero.readthedocs.io/en/stable/installing.html) chapter on their site. Better safe than sorry, so lets install the packages on our machine: -~~~ -$ sudo apt-get install python3-gpiozero python-gpiozero +~~~bash +sudo apt-get install python3-gpiozero python-gpiozero ~~~ **Note**: No harm done to install both, python3 and python2. This needs trimming later on. Make a copy of the [python script for the GPIO buttons](../misc/sampleconfigs/gpio-buttons.py.sample) into the scripts folder. This way you are free to make changes to the script without changing your github repo. -~~~ -$ sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/gpio-buttons.py.sample /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py +~~~bash +sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/gpio-buttons.py.sample /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py ~~~ And change the copy to be executable -~~~ -$ sudo chmod +x /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py +~~~bash +sudo chmod +x /home/pi/RPi-Jukebox-RFID/scripts/gpio-buttons.py ~~~ **Note**: work in progress: the [python script for the GPIO buttons](../misc/sampleconfigs/gpio-buttons.py.sample) will be explained when I get to it. diff --git a/docs/INSTALL-COMPLETE-GUIDE.md b/docs/INSTALL-COMPLETE-GUIDE.md index 026680f47..696d2d813 100644 --- a/docs/INSTALL-COMPLETE-GUIDE.md +++ b/docs/INSTALL-COMPLETE-GUIDE.md @@ -1,117 +1,173 @@ # How to set up a Phoniebox from scratch -## What you need +- [How to set up a Phoniebox from scratch](#how-to-set-up-a-phoniebox-from-scratch) + - [1. What you need](#1-what-you-need) + - [2. Install Raspberry Pi OS](#2-install-raspberry-pi-os) + - [3. Initial Boot](#3-initial-boot) + - [4. Prepare hardware](#4-prepare-hardware) + - [5. Audio](#5-audio) + - [5a. On-board headphone](#5a-on-board-headphone) + - [5b. USB sound card](#5b-usb-sound-card) + - [6. Install Phoniebox software](#6-install-phoniebox-software) + - [7. Verify Phoniebox setup](#7-verify-phoniebox-setup) + +--- + +## 1. What you need All parts marked with a star (*) are optional but improve the overall experience. All linked components are examples but have proven to work together. You are free to choose different equipment. 1. [Micro SD Card](https://amzn.to/3do7KJr) (e.g. 32 GB) 1. Raspberry Pi - * [Model 4 B](https://amzn.to/2M0xtfJ) - * [Model 3 B+](https://amzn.to/2NGL7Fa) - * (Model 1, 2, 3 and Zero are possible, but they are slow...) + - [Model 3 B+](https://amzn.to/2NGL7Fa) - recommended + - [Model 4 B](https://amzn.to/2M0xtfJ) - could be a little overhead + - (Model 1, 2, 3 and Zero are possible, but they are slower...) 1. [USB RFID Reader](https://amzn.to/3s47Iun) 1. [RFID Chips](https://amzn.to/3k78F2j) or [RFID Cards](https://amzn.to/3dplljG) 1. [Speakers with 3.5mm jack](https://amzn.to/3dnhmnV) To improve the sound, we recommend: -* [Ground Loop Isolator](https://amzn.to/37nyZjK) * +- [Ground Loop Isolator](https://amzn.to/37nyZjK) * Alternatively you can use an external sound card, but sometimes that doesn't seem to improve much: -* [USB Sound Card](https://amzn.to/3djaKqC) * - [Alternative](https://amzn.to/3u8guth) - -You'll need a few other things for a one time set up only. +- [USB Sound Card](https://amzn.to/3djaKqC) * - [Alternative](https://amzn.to/3u8guth) -1. Second computer (Linux, Mac or Windows) -1. USB Mouse and USB Keyboard -1. Micro SD Card Reader -1. Screen with HDMI connection +--- -Alternative: -* If you are able to connect your Raspberry Pi via a wired network interface (LAN), you can set it up via terminal (SSH) only. -* It's also possible to set up a Pi with [WiFi and ssh](https://raspberrypi.stackexchange.com/questions/10251/prepare-sd-card-for-wifi-on-headless-pi/57023#57023). +## 2. Install Raspberry Pi OS -## Install Raspberry Pi OS on a Micro SD card - -Before you can install the Phoniebox software, you need to prepare your Raspberry Pi and install +Before you can install the Phoniebox software, you need to prepare your Raspberry Pi and install 1. Connect your Micro SD card (through a card reader) to your computer 1. [Download](https://www.raspberrypi.org/software/) the [Raspberry Pi Imager](https://www.raspberrypi.org/blog/raspberry-pi-imager-imaging-utility/) and open it -1. Select **Raspberry Pi OS** as the operating system (Recommended) -1. Select your Micro SD card (card will be formatted) +1. Select **Raspberry Pi OS** as the operating system +1. Select your Micro SD card (your card will be formatted) 1. Click `Write` -1. Wait for the imaging process to be finished (it'll take a few minutes) and eject your SD card +1. Wait for the imaging process to be finished (it'll take a few minutes) + +--- + +## 3. Initial Boot + +You will need a terminal, like PuTTY for Windows or the Terminal for Mac to proceed with the next steps. + +1. Open a terminal of your choice +1. Insert your card again if it has been ejected automatically +1. Navigate to your SC card e.g., `cd /Volumes/boot` for Mac or `D:` for Windows +1. Enable SSH by adding a simple file + + ```bash + $ touch ssh + ``` + +1. Set up your Wifi connection + - Mac + + ```bash + $ nano wpa_supplicant.conf + ``` -## Initial boot - Set up Wifi connection + - Windows -1. Connect a USB mouse, a keyboard and a screen through HDMI -1. Insert the Micro SD card + ```bash + D:\> notepad wpa_supplicant.conf + ``` + +1. Insert the following content, update your country, Wifi credentials and save the file. + + ```bash + country=DE + ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev + update_config=1 + + network={ + ssid="network-name" + psk="network-password" + } + ``` + +1. Eject your SD card and insert it into your Raspberry Pi 1. Start your Raspberry Pi by attaching a power supply -1. Walk through the "Welcome to Raspberry Pi" wizard (the content of this wizard might have changed after this tutorial was created!) - 1. Set your locale - 1. Change your password - 1. Proceed with with screen settings - 1. Select your Wifi Network (You are being asked in the Phoniebox install routine whether you want to set up your Wifi again, we will skip this step then) - 1. Software Update (optional, recommended, takes a while) - 1. Reboot -1. Let's enable a few more settings - 1. Launch `Raspberry Pi Configuration` from the `Preferences` menu - 1. A window opens with the `System` tab selected - 1. Select `To CLI` for `Boot` option - 1. Select `Login as user 'pi'` for `Auto login` option - 1. Select `Wait for network` for `Network at Boot` option (optional, required for Spotify+ version) - 1. Navigate to the `Interfaces` tab - 1. Select `Enabled` next to `SSH` - 1. Click `OK` -1. Set up a static IP (optional, recommended) - 1. Right click on the Wifi sympbol in the upper right corner of your application bar and choose `Wifi & Wired Network Settings` - 1. Configure `interface` and `wlan0` - 1. Check `Disabled IPv6` unless you want to provide a static IPv6 address - 1. Fill out `IPv4` and `Router` (Gateway) options (keep `DNS Servers` and `DNS Search` empty) - 1. Click `Apply` and `Close` -1. Optional: If you like, you can **turn off Bluetooth** to reduce energy consumption (unless you want to use any Bluetooth devices with your Phoniebox) -1. Shutdown your Raspberry Pi (`Application > Logout > Shutdown`) - -## Install Phoniebox software - -If you want to install the **Spotify+ version**, [read this first](https://github.com/MiczFlor/RPi-Jukebox-RFID/blob/develop/docs/SPOTIFY-INTEGRATION.md). - -1. While shut down, disconnect mouse, keyboard and HDMI -1. Connect RFID Reader -1. Conntect USB Sound Card, plug in the 3.5" speakers with the Ground Loop Isolator in between. If you have chosen the example speakers from above, you can power them either through the Raspberry Pi or through an external power source. -1. Boot your Raspberry Pi -1. Open a terminal in your second computer and login via SSH using the `pi` user and your static IP address. If you see a question about authentication and fingerprint, type `yes` and hit `enter` +1. Login into your Raspberry Pi, username is `pi` and password is `raspberry`. If `raspberrypi.local` does not work, find out your Raspberry Pi's IP address from your router. + + ```bash + $ ssh pi@raspberrypi.local ``` - ssh pi@192.168.1.123 + +1. Update the Pi's software. This may take a bit + + ```bash + $ sudo apt update && sudo apt full-upgrade ``` -### Using on-board headphone-jack for audio +1. Reboot with `sudo reboot` +1. Login again with SSH and open the Raspberry Pi config -Note: Installing with an external monitor (HDMI) can create a problem if you use the mini-jack audio out. The problem is that if you plug in a HDMI monitor an additional sound output is added and the index changes. This bothering behavior was introduced, when Raspberry Pi separated headphones jack and HDMI into two different devices in May 2020. + ```bash + $ sudo raspi-config + ``` -See: [Troubleshooting: headphone audio unavailable after unplugging HDMI](https://github.com/MiczFlor/RPi-Jukebox-RFID/discussions/1300) +1. Update the following settings -If this problem occurs, follow the steps in the next section (configure USB sound card). + ```bash + 1 System Options + S5 Boot / Auto Login -> B2 Console Autologin + S6 Network at Boot -> Yes + ``` -### Configure USB sound card (if you are using one) +1. Close the settings panel with `` +1. Shutdown your Raspberry Pi with `sudo shutdown` +--- -1. Configure your **USB sound card**. Check if your sound card has been detected +## 4. Prepare hardware + +1. Connect the RFID Reader +1. Conntect USB Sound Card if available +1. Plug in the 3.5" speakers with the Ground Loop Isolator in between. If you have chosen the example speakers from above, you can power them either through the Raspberry Pi or through an external power source. +1. Boot your Raspberry Pi +1. Open a terminal in your second computer and login via SSH using the `pi` user and default password `raspberry`. If you see a question about authentication and fingerprint, type `yes` and hit `enter` + + ```bash + ssh pi@raspberrypi.local ``` - cat /proc/asound/modules - // returns +--- + +## 5. Audio + +### 5a. On-board headphone + +Installing with an external monitor (HDMI) can create a problem if you use the mini-jack audio out. The problem is that if you plug in a HDMI monitor an additional sound output is added and the index changes. This bothering behavior was introduced, when Raspberry Pi separated headphones jack and HDMI into two different devices in May 2020. +Also see [Troubleshooting: headphone audio unavailable after unplugging HDMI](https://github.com/MiczFlor/RPi-Jukebox-RFID/discussions/1300) - 0 snd_bcm2835 - 1 snd_usb_audio +### 5b. USB sound card + +1. Open the Raspberry Pi config + + ```bash + $ sudo raspi-config ``` -1. To update the sound card priority order, edit the following file + +1. Update the following settings + + ```bash + 1 System Options + S2 Audio -> 1 USB Audio ``` - sudo nano /etc/modprobe.d/alsa-base.conf + +1. Close the settings panel with `` +1. Make your soundcard the primary sound device. To update the sound card priority order, edit the following file: + + ```bash + $ sudo nano /usr/share/alsa/alsa.conf ``` + 1. Find the following variables and change their value from `0` to `1` - ``` + + ```bash defaults.ctl.card 0 defaults.pcm.card 0 @@ -120,23 +176,29 @@ If this problem occurs, follow the steps in the next section (configure USB soun defaults.ctl.card 1 defaults.pcm.card 1 ``` + 1. Reboot 1. Test your audio! Check if you hear white noise in stereo when running the following command from your connected speakers. If not, refer to this [resource](https://learn.adafruit.com/usb-audio-cards-with-a-raspberry-pi/instructions) to troubleshoot. - ``` + + ```bash speaker-test -c2 ``` -### Phoniebox Install Script +--- + +## 6. Install Phoniebox software + +If you want to install the **Spotify+ version**, [read this first](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Spotify-FAQ). Run the following command in your SSH terminal and follow the instructions -``` +```bash cd; rm buster-install-*; wget https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID/master/scripts/installscripts/buster-install-default.sh; chmod +x buster-install-default.sh; ./buster-install-default.sh ``` 1. `Yes` to `Continue interactive installation` 1. `No` to the `Wifi Setting step` - it's already set! -1. `Master` to `CONFIGURE AUDIO INTERFACE (iFace)` +1. `Speaker` to `CONFIGURE AUDIO INTERFACE (iFace)` 1. Setup Spotify (optional) 1. You need to generate your personal Spotify client ID and secret 1. Visit the [Mopidy Spotify Authentication Page](https://mopidy.com/ext/spotify/#authentication) @@ -148,18 +210,21 @@ cd; rm buster-install-*; wget https://raw.githubusercontent.com/MiczFlor/RPi-Juk 1. `Yes` to `FOLDER CONTAINING AUDIO FILES` 1. Optional: In this scenario, we do not install GPIO buttons, so feel free to choose `No` 1. `Yes` to `Do you want to start the installation?` +1. ... Wait a bit for the installation to happen ... 1. `Yes` to `Have you connected your RFID reader?` 1. `1` to select `1. USB-Reader` 1. Choose the `#` that resonates with your RFID reader, in our case `HXGCoLtd Keyboard` 1. `Yes` to `Would you like to reboot now?` -## Verify Phoniebox installation +--- + +## 7. Verify Phoniebox setup -1. Open a browser in your computer and navigate to your static IP: `http://192.168.1.123` +1. Open a browser in your computer and navigate to your Raspberry Pi: `http://raspberrypi.local` 1. You should see the Phoniebox UI 1. In your navigation, choose `Card ID` 1. Swipe one card near your RFID reader. If `Last used Chip ID` is automatically updated (you might hear a beep) and shows a number, your reader works 1. Verify Spotify (optional) 1. Click `Spotify+` in the menu 1. Mopidy opens, a second web player which was also installed - 1. You should be able to search and play Spotify content here \ No newline at end of file + 1. You should be able to search and play Spotify content here diff --git a/docs/SPOTIFY-INTEGRATION.md b/docs/SPOTIFY-INTEGRATION.md deleted file mode 100644 index 3b5419281..000000000 --- a/docs/SPOTIFY-INTEGRATION.md +++ /dev/null @@ -1,420 +0,0 @@ - -# Spotify support for Phoniebox (this guide is for all who want to manually update to +Spotify Edition) - -**MUST READ** - -If you want to integrate Spotify: - -* You **must have** a Spotify Premium subscription. Phoniebox will not work with Spotify Free, just Spotify Premium. -* You need a non-Facebook Spotify username and password. - * If you created your account through Facebook you'll need to create a "device password" to be able to use Phoniebox. Go to http://www.spotify.com/account/set-device-password/, login with your Facebook account, and follow the instructions. However, sometimes that process can fail for users with Facebook logins, in which case you can create an app-specific password on Facebook by going to facebook.com > Settings > Security > App passwords > Generate app passwords, and generate one to use with Phoniebox. -* MP3 (local music) handling has completely changed for the +Spotify Edition. The tracks need to be indexed by Mopidy & Mopidy-Spotify (this is the part of Phoniebox, which gives you spotify support) and the created M3U files do have another structure than normal M3U files. - * You have to scan you library once (if you hadn't yet) and everytime you upload new local tracks to your Phoniebox. - * You can scan local files under "Folders & Files" (Top Navigation). - * For the future we try to integrate an automation for that. - -## Bug reports and testers - -**Testers needed for the Spotify integration** Please read [more in this thread](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/18#issuecomment-430140524). - -If you open an issue on github, please provide as much informations as you can. What is you Edition (Classic or +Spotify)? Which version so you have? What is the problem and what did you try to solve this? And so on.. - -** END of MUST READ :) ** - -## About this document - -You best start will be to start a FRESH INSTALLATION, please read [more here](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/INSTALL-stretch#one-line-install-command). - -The one-line-install should detect an existing installation and ask you if you want to keep the content. That should work, no guarantee :) a backup won't hurt anyone. - -The below documentation might be out of date. The one-line-install script will be more up to date. But the below still exists, because it has more content about how the integration works - and that might help you to understand a problem if and when it occurs. Please add, edit and comment to this document while testing the code. - -## Installing Buster on your Pi - -1. Install Buster on SD Card. -2. Remove card and insert again. - -Setting up the Phoniebox via a SSH connection saves the need for a monitor and a mouse. The following worked on Raspian Buster. - -* Flash your SD card with the Raspian image -* Eject the card and insert it again. This should get the boot partition mounted. -* In the boot partition, create a new empty file called ssh. This will enable the SSH server later. -* Create another file in the same place called wpa_supplicant.conf. Set the file content according to the following example: -~~~ -ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev -network={ - ssid="YOUR_NETWORK_NAME" - psk="YOUR_PASSWORD" - key_mgmt=WPA-PSK -} -~~~ -Note: This works for WPA-secured wifi networks, which should be the vast majority. - -* Save the file -* Unmount and eject the card, insert it into the Raspy, boot. -* Find out the IP address of the raspberry. Most Wifi routers have a user interface that lists all devices in the network with the IP address they got assigned. -* Connect via ssh with username pi and password raspberry. -* Jump back to the top of this document to walk through the other steps of the installation. - -## If you have a USB Sound Card: Correct the Sort Order - -~~~ -cat /proc/asound/modules -~~~ -You get: -~~~ -0 snd_bcm2835 -1 snd_usb_audio -~~~ -Change sort order: -~~~ -sudo nano /etc/modprobe.d/alsa-base.conf -~~~ -If file is empty, add the following lines: -~~~ -options snd_usb_audio index=0 -options snd_bcm2835 index=1 -options snd slots=snd-usb-audio,snd-bcm2835 -~~~ -After reboot you get: -~~~ -cat /proc/asound/modules - -0 snd_usb_audio -1 snd_bcm2835 -~~~ -## If you need Root-User, get your system prepared -~~~ -sudo passwd root -sudo nano /etc/ssh/sshd_config -~~~ -Search for PermitRootLogin and change - -~~~ -#PermitRootLogin prohibit-password -~~~ -to -~~~ -PermitRootLogin yes -~~~ - -## Install MOPIDY -Pin major version of Mopidy to 3 -~~~ -echo -e "Package: mopidy\nPin: version 3.*\nPin-Priority: 1001" | sudo tee /etc/apt/preferences.d/mopidy -~~~ -Add the archive’s GPG key: -~~~ -wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - -~~~ -Add the APT repo to your package sources: -~~~ -sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list -~~~ -Install Mopidy and all dependencies: -~~~ -sudo apt-get update -sudo apt-get upgrade -sudo apt-get --yes --allow-downgrades --allow-remove-essential --allow-change-held-packages install mopidy mopidy-mpd mopidy-local mopidy-spotify -~~~ -Finally, you need to set a couple of config values, and then you’re ready to run Mopidy. Alternatively you may want to have Mopidy run as a system service, automatically starting at boot. - -To install one of the listed packages, e.g. mopidy-spotify, simply run the following: -~~~ -sudo apt-get --yes --allow-downgrades --allow-remove-essential --allow-change-held-packages install libspotify12 python3-cffi python3-ply python3-pycparser python3-spotify -~~~ - -## Mopidy as service... - -On modern systems using systemd you can enable the Mopidy service by running: -~~~ -sudo systemctl enable mopidy -~~~ -This will make Mopidy start when the system boots. - -## Install MOPIDY-IRIS Web Interface -~~~ -sudo python3 -m pip install Mopidy-Iris -~~~ - -Set the rights -~~~ -sudo nano /etc/sudoers -~~~ -Add this line to the end -~~~ -mopidy ALL=NOPASSWD: /usr/local/lib/python2.7/dist-packages/mopidy_iris/system.sh -~~~ - -You have to reboot now. -~~~ -sudo reboot -~~~ - -## Configure Mopidy... -~~~ -sudo nano /etc/mopidy/mopidy.conf -~~~ -This file should look like this (you first have to get client-id and client-secret here: https://www.mopidy.com/authenticate/ ) -This must be done manually. Put your username, password, client_id, client_secret into the spotify section. -The audio section has to be tested, because i don't know if "output = alsasink" works for everyone. "mixer_volume" is the start volume of phoniebox! attention: if you leave this blank, volume will be 100% after reboot!! -~~~ -[core] -cache_dir = /var/cache/mopidy -config_dir = /etc/mopidy -data_dir = /var/lib/mopidy - -[logging] -config_file = /etc/mopidy/logging.conf -debug_file = /var/log/mopidy/mopidy-debug.log - -[local] -enabled = true -media_dir = /home/pi/RPi-Jukebox-RFID/shared/audiofolders -excluded_file_extensions = - .conf - .jpg - .txt - placeholder - -[file] -#enabled = true -metadata_timeout = 1 - -[m3u] -playlists_dir = /home/pi/RPi-Jukebox-RFID/playlists -default_encoding = UTF-8 -default_extension = .m3u - -[audio] -output = alsasink -mixer_volume = 30 - -[mpd] -hostname = 0.0.0.0 - -[http] -hostname = 0.0.0.0 - -[iris] -country = DE -locale = de_DE - -[spotify] -enabled = true -username = %spotify_username% -password = %spotify_password% -client_id = %spotify_client_id% -client_secret = %spotify_client_secret% -#bitrate = 160 -#volume_normalization = true -#private_session = false -#timeout = 10 -#allow_cache = true -#allow_network = true -#allow_playlists = true -#search_album_count = 20 -#search_artist_count = 10 -#search_track_count = 50 -#toplist_countries = - -~~~ -Then edit this file: -~~~ -sudo nano ~/.config/mopidy/mopidy.conf -~~~ -Like this: -~~~ -# For further information about options in this file see: -# http://docs.mopidy.com/ -# -# The initial commented out values reflect the defaults as of: -# Mopidy 2.2.0 -# Mopidy-File 2.2.0 -# Mopidy-HTTP 2.2.0 -# Mopidy-Iris 3.27.1 -# Mopidy-Local 2.2.0 -# Mopidy-Local-Images 1.0.0 -# Mopidy-M3U 2.2.0 -# Mopidy-MPD 2.2.0 -# Mopidy-SoftwareMixer 2.2.0 -# Mopidy-Spotify 3.1.0 -# Mopidy-Stream 2.2.0 -# -# Available options and defaults might have changed since then, -# run `mopidy config` to see the current effective config and -# `mopidy --version` to check the current version. - -[core] -cache_dir = $XDG_CACHE_DIR/mopidy -config_dir = $XDG_CONFIG_DIR/mopidy -data_dir = $XDG_DATA_DIR/mopidy -max_tracklist_length = 10000 -restore_state = false - -[logging] -#color = true -#console_format = %(levelname)-8s %(message)s -#debug_format = %(levelname)-8s %(asctime)s [%(process)d:%(threadName)s] %(name)s\n %(message)s -#debug_file = mopidy.log -#config_file = - -[audio] -#mixer = software -mixer_volume = 30 -output = alsasink -#buffer_time = - -[proxy] -#scheme = -#hostname = -#port = -#username = -#password = - -[local-images] -#enabled = true -#library = json -#base_uri = /images/ -#image_dir = -#album_art_files = -# *.jpg -# *.jpeg -# *.png - -[iris] -#enabled = true -country = DE -locale = de_DE -#spotify_authorization_url = https://jamesbarnsley.co.nz/iris/auth_spotify.php -#lastfm_authorization_url = https://jamesbarnsley.co.nz/iris/auth_lastfm.php -#genius_authorization_url = https://jamesbarnsley.co.nz/iris/auth_genius.php -#snapcast_enabled = false -#snapcast_host = localhost -#snapcast_port = 1705 - -[mpd] -#enabled = true -hostname = 0.0.0.0 -#port = 6600 -#password = -#max_connections = 20 -#connection_timeout = 60 -#zeroconf = Mopidy MPD server on $hostname -#command_blacklist = -# listall -# listallinfo -#default_playlist_scheme = m3u - -[http] -#enabled = true -hostname = 0.0.0.0 -#port = 6680 -#static_dir = -#zeroconf = Mopidy HTTP server on $hostname -#allowed_origins = - -[stream] -#enabled = true -#protocols = -# http -# https -# mms -# rtmp -# rtmps -# rtsp -#metadata_blacklist = -#timeout = 5000 - -[m3u] -#enabled = true -#base_dir = $XDG_MUSIC_DIR -default_encoding = UTF-8 -default_extension = .m3u -playlists_dir = /home/pi/RPi-Jukebox-RFID/playlists - -[softwaremixer] -#enabled = true - -[file] -#enabled = true -#media_dirs = -# $XDG_MUSIC_DIR|Music -# ~/|Home -#excluded_file_extensions = -# .jpg -# .jpeg -#show_dotfiles = false -#follow_symlinks = false -metadata_timeout = 1 - -[local] -enabled = true -#library = json -media_dir = /home/pi/RPi-Jukebox-RFID/shared/audiofolders -#scan_timeout = 1000 -#scan_flush_threshold = 100 -#scan_follow_symlinks = false -excluded_file_extensions = -# .directory -# .html -# .jpeg - .jpg -# .log -# .nfo -# .png - .txt - .conf - placeholder - -[spotify] -enabled = true -username = %spotify_username% -password = %spotify_password% -client_id = %spotify_client_id% -client_secret = %spotify_client_secret% -#bitrate = 160 -#volume_normalization = true -#private_session = false -#timeout = 10 -#allow_cache = true -#allow_network = true -#allow_playlists = true -#search_album_count = 20 -#search_artist_count = 10 -#search_track_count = 50 -#toplist_countries = -~~~ - -## Install Phoniebox (if not done yet) - if you want to UPGRADE to spotify only, skip this step -~~~ -cd; rm stretch-install-*; wget https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID/master/scripts/installscripts/stretch-install-spotify.sh; chmod +x stretch-install-spotify.sh; ./stretch-install-spotify.sh -~~~ -## Change Playlists_Folders_Path to: -~~~ -/home/pi/RPi-Jukebox-RFID/playlists -~~~ -## You have to disable MPD because we use mopidy instead and MPD is included there. - -If you don't disable MPD here, mopidy will not run!!! -~~~ -sudo systemctl disable mpd -~~~ -## Charset problems in display? -If you have problems with UTF-8 and ANSI, try to start raspi-config and change localisation to UTF-8. -I don't know if it works, for me it does after several tries. - -## How to use Spotify? - -When you right-click an album, a track or a playlist in MOPIDY IRIS or Spotify Client (share button), you get a Spotify URI. -This Spotify URI must be used when registering a card with the following syntax: -~~~ -Tracks: spotify:track:###################### -Albums: spotify:album:###################### -Playlists: spotify:user:username:playlist:###################### -(e.g. spotify:user:spotify:playlist:37i9dQZF1DWUVpAXiEPK8P or -spotify:user:tomorrowlandofficial:playlist:0yS25E7g9xQZ1Dst5SqUZn) - -Podcast: spotify:show:###################### (This has not been tested yet!) -~~~ -The information will be stored in a spotify.txt in an audiofolder. diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 1ba535af2..f7e173255 100755 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -25,7 +25,7 @@ We introduce Phoniebox Editions. To distinguish them, we call them "Phoniebox Cl **Please use our [spotify thread](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/18) to post improvements regarding this feature.** -~~~ +~~~bash cd /home/pi/RPi-Jukebox-RFID git checkout master git fetch origin @@ -106,11 +106,11 @@ sudo reboot # Upgrade from Version 1.1.1 to 1.1.7 -Not much has changed in the core of this version. There is the new feature: Integrating **Spotify** to your Phoniebox. Currently this is *only* a [HOWTO document](docs/SPOTIFY-INTEGRATION.md) which needs improvement and your input. I invite everybody to use our [spotify thread](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/18) to post improvements regarding this feature. You might also want to [improve the documentation on *Spotify integration*](docs/SPOTIFY-INTEGRATION.md) and create pull requests so I can merge this with the core. +Not much has changed in the core of this version. There is the new feature: Integrating **Spotify** to your Phoniebox. Currently this is *only* a [HOWTO document](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Spotify-FAQ) which needs improvement and your input. I invite everybody to use our [spotify thread](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/18) to post improvements regarding this feature. You might also want to [improve the documentation on *Spotify integration*](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Spotify-FAQ) and create pull requests so I can merge this with the core. Upgrading is therefore fairly simple. The following will overwrite any local changes to your code but NOT to your configruation files and systemd services, GPIO and the like. Only core code: -~~~ +~~~bash cd /home/pi/RPi-Jukebox-RFID git checkout master git fetch origin @@ -120,12 +120,12 @@ git pull # Upgrade from Version 1.1.1 to 1.1.6 -A few important bug fixes. And a new design. +A few important bug fixes. And a new design. And the option to decide what the 'second swipe' of a card does (see settings in the web app). The following should get you all you need, without running the install script if you -only want to upgrade. +only want to upgrade. -~~~ +~~~bash cd /home/pi/RPi-Jukebox-RFID git checkout master git fetch origin @@ -146,7 +146,8 @@ sudo systemctl enable rfid-reader # Upgrade from Version 1.0.0 to 1.1.1 This upgrade brings the web app UI for file management, recursive folder management, wifi switch off and more. The latest [one-line Phoniebox install script](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/CONFIGURE-stretch#oneLineInstall) contains all the necessary steps, but will treat your upgrade like a new install. Manual upgrade: -~~~ + +~~~bash cd cd /home/pi/RPi-Jukebox-RFID git fetch @@ -168,6 +169,7 @@ sudo service php7.0-fpm restart As of version 1.0 there is a much simpler install procedure: copy and paste one line into your terminal and hit *enter*. Find out more about the [one-line Phoniebox install script](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/CONFIGURE-stretch#oneLineInstall). # Upgrade from 0.9.5 to 0.9.7 + * Adding a *Settings* page in the web app to control features like 'idle shutdown' and 'max volume' and toggle systemd services * Documentation / troubleshooting / tricks: how to install via ssh, improve on board audio quality and the like * Adding auto shutdown when idle for longer than x minutes (see [manual](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#settings) for details) @@ -175,7 +177,8 @@ As of version 1.0 there is a much simpler install procedure: copy and paste one * Fixing bug: settings volume for stereo audio iFace * Fixing bug: bash code compatible with all shells * Web app enhancements (audio level, display 'playing now') -~~~ + +~~~bash # services to launch after boot using systmed sudo cp /home/pi/RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample /etc/systemd/system/phoniebox-idle-watchdog.service sudo chown root:root /etc/systemd/system/phoniebox-idle-watchdog.service @@ -188,11 +191,12 @@ sudo systemctl start phoniebox-idle-watchdog.service ~~~ # Upgrade from 0.9.4 to 0.9.5 + * Configuration of RFID card control in extra file `settings/rfid_trigger_play.conf` * Playout control config now uses `settings` folder to store iFace value (e.g. PCM) and percentage of relative volume change * both bash scripts `scripts/rfid_trigger_play.sh` and `scripts/playout_controls.sh` are not created from `.sample` versions anymore, because the config has been moved to external files. -~~~ +~~~bash # make backups of the current scripts mv /home/pi/RPi-Jukebox-RFID/scripts/rfid_trigger_play.sh /home/pi/RPi-Jukebox-RFID/scripts/rfid_trigger_play.sh.backup.0.9.4 mv /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh.backup.0.9.4 @@ -210,9 +214,11 @@ sudo chmod 775 /home/pi/RPi-Jukebox-RFID/settings/rfid_trigger_play.conf ~~~ # Upgrade to 0.9.4 + * The following script refers to the OS version 'Stretch' in some places but this should also work for 'Jessie'. * OS 'Stretch' and 'Jessie' require different `lighttpd.conf` parameters. Samples can be found in `misc/sampleconfigs` -~~~ + +~~~bash # make backups of the current scripts cp /home/pi/RPi-Jukebox-RFID/scripts/rfid_trigger_play.sh /home/pi/RPi-Jukebox-RFID/scripts/rfid_trigger_play.sh.backup.0.9.3 cp /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh.backup.0.9.3 diff --git a/docs/WLAN-ACCESS-POINT.md b/docs/WLAN-ACCESS-POINT.md index 32049c809..8558f6f42 100644 --- a/docs/WLAN-ACCESS-POINT.md +++ b/docs/WLAN-ACCESS-POINT.md @@ -14,21 +14,26 @@ supports this mode. RPi3 does. Install two packages we need later. Later meaning: when we might not have Internet anymore. Because the wlan0 interface will be set to a static IP to create the access point. -``` + +```bash sudo apt-get update sudo apt-get install dnsmasq hostapd ``` + ## Configure the network Using jessie, dhcpd is activated by default. This dhcp daemon is assigning IP addresses to devices which want to connect to the Phoniebox. Set the IP address for the wlan card by opening: -``` + +```bash sudo nano /etc/network/interfaces ``` + Replace the existing content with the following lines: -``` + +```bash # Localhost auto lo iface lo inet loopback @@ -43,16 +48,21 @@ iface wlan0 inet static address 192.168.1.1 netmask 255.255.255.0 ``` + Now, the wlan is set to the IP address `192.168.1.1`. We add one line to the dhcpd config file: -``` + +```bash sudo nano /etc/dhcpcd.conf ``` + Append the line: -``` + +```bash denyinterfaces wlan0 ``` + Now we reboot and afterwards you should be connected to your RPi directly, not via ssh. Because if your RPi relied on a WiFi connection to the Internet, this will be cut off. Remember: we need the wlan0 interface to hook up other devices to a WiFi network the @@ -62,7 +72,7 @@ Let's check if all interfaces are up and running. We only really need the wlan0 but if eth0 is also up and is connected to the Internet, your Phoniebox will be online and all devices connected to it. Type in the command line: -``` +```bash ip a ``` @@ -73,17 +83,19 @@ monitor. Reboot. -``` +```bash sudo reboot ``` ## dhcp server configuration -``` +```bash sudo nano /etc/dnsmasq.conf ``` + The following lines are the minimal configuration required. -``` + +```bash # interface which is active interface=wlan0 @@ -99,31 +111,42 @@ dhcp-option=option:dns-server,192.168.1.1 Check the configuration before you start the dhcp server and cache. -``` + +```bash dnsmasq --test -C /etc/dnsmasq.conf ``` + This should return 'OK'. Now start `dnsmasq`: -``` + +```bash sudo systemctl restart dnsmasq ``` + Check if it is up and running: -``` + +```bash sudo systemctl status dnsmasq ``` + Now install dnsmasq to start after boot: -``` + +```bash sudo systemctl enable dnsmasq ``` ## configure hostapd + To assign ssid and password, we need to configure `hostapd`. -``` + +```bash sudo nano /etc/hostapd/hostapd.conf ``` + Replace the content of this file (if it already exists) with the following content. -``` + +```bash # interface and driver interface=wlan0 #driver=nl80211 @@ -144,24 +167,31 @@ wpa_key_mgmt=WPA-PSK rsn_pairwise=CCMP wpa_passphrase=Pl4yM3N0w ``` + The network will be listed as `Phoniebox` and the password to connect to the network is `Pl4yM3N0w` (as in 'play me now' with a number four and a number three and a zero). If you want a different ssid and/or password, edit the lines above. This file contains a password in raw text, so make sure only root can read it. -``` + +```bash sudo chmod 600 /etc/hostapd/hostapd.conf ``` -Check if this setup is correct. Open + +Check if this setup is correct. Open the wlan host in debug mode and read through the results. -``` + +```bash sudo hostapd -dd /etc/hostapd/hostapd.conf ``` + Scroll up to see if you can find these two lines anywhere: -``` + +```bash wlan0: interface state COUNTRY_UPDATE->ENABLED wlan0: AP-ENABLED ``` + If yes, you can also try to hook up a device with the network already. See if you can find `Phoniebox` as a WiFi network. @@ -169,26 +199,33 @@ See if you can find `Phoniebox` as a WiFi network. If that works, all is well. Stop the `hostapd` daemon with `Ctrl&C`. Before we can start `hostapd` on boot, we have to add a few lines -in the config file to specify +in the config file to specify the location of the config file. -``` + +```bash sudo nano /etc/default/hostapd ``` + Add these lines: -``` + +```bash RUN_DAEMON=yes DAEMON_CONF="/etc/hostapd/hostapd.conf" ``` + And start `hostapd` with the following commands: -``` + +```bash sudo systemctl start hostapd sudo systemctl enable hostapd ``` + Check if the daemon is up and running: -``` + +```bash sudo systemctl status hostapd ``` + This concludes what we need to connect to the Phoniebox directly via WiFi. If you plan to connect the `eth0` via a cable with the Internet, you need to learn about firewall configurations. Google how to do this (I hope to replace this last paragraph with a nicer explanation and a link later, when I find the time. Apologies.) - diff --git a/htdocs/_assets/bootstrap-3/js/collapse.js b/htdocs/_assets/bootstrap-3/js/collapse.js index 12038693d..386aa7af3 100755 --- a/htdocs/_assets/bootstrap-3/js/collapse.js +++ b/htdocs/_assets/bootstrap-3/js/collapse.js @@ -160,7 +160,7 @@ var target = $trigger.attr('data-target') || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - return $(target) + return $.find(target) } diff --git a/htdocs/ajax.getAudioSink.php b/htdocs/ajax.getAudioSink.php new file mode 100755 index 000000000..6e5cd2734 --- /dev/null +++ b/htdocs/ajax.getAudioSink.php @@ -0,0 +1,11 @@ +enabled", $btOutputs); + $btOutputs = str_replace("disabled", "disabled", $btOutputs); + print "$btOutputs"; + +?> diff --git a/htdocs/ajax.getBluetoothStatus.php b/htdocs/ajax.getBluetoothStatus.php new file mode 100755 index 000000000..b1f5d3ec5 --- /dev/null +++ b/htdocs/ajax.getBluetoothStatus.php @@ -0,0 +1,28 @@ + diff --git a/htdocs/func.php b/htdocs/func.php index 88ac8cb6a..5c053669d 100755 --- a/htdocs/func.php +++ b/htdocs/func.php @@ -296,7 +296,7 @@ function index_folders_print($item, $key) } } $playlist = $contentTree[$key]['path_rel']; - $id = str_replace(",", "", $contentTree[$key]['id']); + $id = $contentTree[$key]['id']; /**/ //print "
\nkey:".$key." id:".$contentTree[$key]['id']." path_rel:".$contentTree[$key]['path_rel']; print_r($contentTree); print "
"; //??? //print "
\nfiles:"; print_r($files); print "
"; //??? diff --git a/htdocs/inc.bluetooth.php b/htdocs/inc.bluetooth.php new file mode 100755 index 000000000..7d7a2cbf1 --- /dev/null +++ b/htdocs/inc.bluetooth.php @@ -0,0 +1,67 @@ + + + + +
+
+
+

+ +

+ "; + print "
"; + print "
"; + if (strpos($btswitch, "Default") === false) { + print "
"; + } else { + print "
"; + } + print "Message: $btswitch
"; + print "
"; + print "
"; + } + ?> +
+ + +
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ diff --git a/htdocs/inc.header.php b/htdocs/inc.header.php index cdf1972e7..2b101cf69 100755 --- a/htdocs/inc.header.php +++ b/htdocs/inc.header.php @@ -168,6 +168,7 @@ function fileGetContentOrDefault($filename, $defaultValue) $edition = $globalConf['EDITION']; $maxvolumevalue = $globalConf['AUDIOVOLMAXLIMIT']; $startupvolumevalue = $globalConf['AUDIOVOLSTARTUP']; +$bootvolumevalue = $globalConf['AUDIOVOLBOOT']; $volstepvalue = $globalConf['AUDIOVOLCHANGESTEP']; $idletimevalue = $globalConf['IDLETIMESHUTDOWN']; $conf['settings_lang'] = $globalConf['LANG']; @@ -196,6 +197,7 @@ function fileGetContentOrDefault($filename, $defaultValue) 'volume', 'maxvolume', 'startupvolume', + 'bootvolume', 'volstep', 'shutdown', 'reboot', @@ -298,6 +300,7 @@ function fileGetContentOrDefault($filename, $defaultValue) 'volume' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setvolume -v=%s", // change volume 'maxvolume' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setmaxvolume -v=%s", // change max volume 'startupvolume' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setstartupvolume -v=%s", // change startup volume + 'bootvolume' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setbootvolume -v=%s", // change boot volume 'volstep' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=setvolstep -v=%s", // change volume step 'mute' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=mute", // volume mute (toggle) 'volumeup' => "/usr/bin/sudo ".$conf['scripts_abs']."/playout_controls.sh -c=volumeup", // volume up diff --git a/htdocs/inc.setBootVolume.php b/htdocs/inc.setBootVolume.php new file mode 100644 index 000000000..7328cb92a --- /dev/null +++ b/htdocs/inc.setBootVolume.php @@ -0,0 +1,59 @@ + + + +
+
+
+

+
'> +
+ + + '/> + +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ diff --git a/htdocs/inc.setStartupVolume.php b/htdocs/inc.setStartupVolume.php index 502b03630..0fb92f054 100755 --- a/htdocs/inc.setStartupVolume.php +++ b/htdocs/inc.setStartupVolume.php @@ -3,8 +3,6 @@ --> diff --git a/htdocs/inc.viewFolderTree.php b/htdocs/inc.viewFolderTree.php index 6810d29da..d46ace944 100755 --- a/htdocs/inc.viewFolderTree.php +++ b/htdocs/inc.viewFolderTree.php @@ -116,18 +116,19 @@ // this is a new and easier way for loading spotify informations! $uri = file_get_contents($subfolder."/spotify.txt"); $url = "https://open.spotify.com/oembed/?url=".trim($uri)."&format=json"; + $headers = stream_context_create(array('http'=>array('method'=>'GET', 'header'=>'user-agent:Phoniebox'))); if (!file_exists($coverfile)) { - $str = file_get_contents($url); + $str = file_get_contents($url, false, $headers); $json = json_decode($str, true); $cover = $json['thumbnail_url']; - $coverdl = file_get_contents($cover); + $coverdl = file_get_contents($cover, false, $headers); file_put_contents($coverfile, $coverdl); } if (!file_exists($titlefile)) { - $str = file_get_contents($url); + $str = file_get_contents($url, false, $headers); $json = json_decode($str, true); $title = $json['title']; @@ -140,13 +141,8 @@ // chop off the $Audio_Folders_Path in the beginning //$temp['path_rel'] = substr($folder."/".$value, strlen($Audio_Folders_Path) + 1, strlen($folder."/".$value)); $temp['path_rel'] = substr($subfolder, strlen($Audio_Folders_Path) + 1, strlen($subfolder)); - // some special version with no slashes or whitespaces for IDs on the panel collapse - $temp['id'] = preg_replace('/\//', '---', $temp['path_rel']); - $temp['id'] = preg_replace('/\ /', '-_-', $temp['id']); - $temp['id'] = preg_replace('/\[/', '_-', $temp['id']); - $temp['id'] = preg_replace('/\]/', '-_', $temp['id']); - $temp['id'] = preg_replace('/&/', 'and', $temp['id']); - $temp['id'] = "ID".preg_replace('/\:/', '-+-', $temp['id']); + // IDs on the panel collapse + $temp['id'] = "ID".$idcounter++; // count the level depth in the tree by counting the slashes in the path $temp['level'] = substr_count($temp['path_rel'], '/'); // information about the content diff --git a/htdocs/index.php b/htdocs/index.php index f45ded1e0..d714567ad 100755 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -124,16 +124,12 @@ $audiofolders = array_filter(glob($Audio_Folders_Path.'/*'), 'is_dir'); usort($audiofolders, 'strcasecmp'); -// counter for ID of each folder +// counter for ID of each folder, increased when used (within inc.viewFolderTree.php) $idcounter = 0; // go through all folders foreach($audiofolders as $audiofolder) { - // increase ID counter - $idcounter++; - include('inc.viewFolderTree.php'); - } ?> diff --git a/htdocs/lang/lang-de-DE.php b/htdocs/lang/lang-de-DE.php old mode 100755 new mode 100644 index 676620772..7487c4db7 --- a/htdocs/lang/lang-de-DE.php +++ b/htdocs/lang/lang-de-DE.php @@ -69,6 +69,7 @@ $lang['globalLanguageSettings'] = "Spracheinstellungen"; $lang['globalPriority'] = "Priorität"; $lang['globalEmail'] = "Email address"; +$lang['globalAudioSink'] = "Audio Ausgabegeräte"; // Player title HTML $lang['playerSeekBack'] = "Rückwärts spulen"; @@ -209,6 +210,7 @@ $lang['settingsVolChangePercent'] = "Lautst. Änderung"; $lang['settingsMaxVol'] = "Max. Lautstärke"; $lang['settingsStartupVol'] = "Start-Lautstärke"; +$lang['settingsBootVol'] = "Lautst. nach Boot"; $lang['settingsWifiRestart'] = "Die Änderungen an der WiFi-Verbindung erfordern einen Neustart, um wirksam zu werden"; $lang['settingsWifiSsidPlaceholder'] = "z.B. PhonieHomie"; $lang['settingsWifiSsidHelp'] = "Der Name, unter dem dein WiFi als 'verfügbares Netzwerk' angezeigt wird"; @@ -239,6 +241,7 @@ $lang['settingsWlanSendON'] = "Ja, E-Mail senden."; $lang['settingsWlanSendOFF'] = "Nein, E-Mail nicht senden."; +$lang['settingsVolumeManager'] = "Auswahl Volume Manager"; $lang['settingsWlanReadNav'] = "Wlan IP vorlesen"; $lang['settingsWlanReadInfo'] = "Wlan IP bei jedem Systemstart vorlesen? (nützlich wenn du deine Phoniebox in ein neues Wlan-Netzwerk mit dynamischer IP verbindest)"; @@ -259,6 +262,8 @@ $lang['infoDebugLogTail'] = "DEBUG Logdatei: Letzte 40 Zeilen"; $lang['infoDebugLogClear'] = "Lösche Inhalt von debug.log"; $lang['infoDebugLogSettings'] = "Debug Log Einstellungen"; +$lang['infoAudioActive'] = "Aktive Audioausgänge"; +$lang['infoBluetoothStatus'] = "Bluetooth Status"; /* * Ordnerverwaltung und Dateien hochladen diff --git a/htdocs/lang/lang-en-UK.php b/htdocs/lang/lang-en-UK.php index 99b3b520e..9610adaa2 100755 --- a/htdocs/lang/lang-en-UK.php +++ b/htdocs/lang/lang-en-UK.php @@ -69,6 +69,7 @@ $lang['globalLanguageSettings'] = "Language Settings"; $lang['globalPriority'] = "Priority"; $lang['globalEmail'] = "Email address"; +$lang['globalAudioSink'] = "Audio Devices"; // Player title HTML $lang['playerSeekBack'] = "seek back"; @@ -216,6 +217,7 @@ $lang['settingsVolChangePercent'] = "Vol. Change %"; $lang['settingsMaxVol'] = "Maximum Volume"; $lang['settingsStartupVol'] = "Startup Volume"; +$lang['settingsBootVol'] = "Volume after reboot"; $lang['settingsWifiRestart'] = "The changes applied to your WiFi connection require a restart to take effect."; $lang['settingsWifiSsidPlaceholder'] = "e.g.: PhonieHomie"; $lang['settingsWifiSsidHelp'] = "The name under which your WiFi shows up as 'available network'"; @@ -240,7 +242,7 @@ $lang['settingsShowCoverOFF'] = "Don't show cover"; $lang['settingsMessageLangfileNewItems'] = "There are new language items in the original lang-en-UK.php file. Your language file has been updated and now contains these (in English). You might want to update your language file and commit your changes to the Phoniebox code :)"; $lang['settingsWlanSendNav'] = "Mail Wlan IP"; -$lang['settingsWlanSendInfo'] = "Send Wlan IP over email on boot? (useful if you hook your Phoniebox into a new Wlan networt with dynamic IP)"; +$lang['settingsWlanSendInfo'] = "Send Wlan IP over email on boot? (useful if you hook your Phoniebox into a new Wlan network with dynamic IP)"; $lang['settingsWlanSendQuest'] = "Send Wlan IP?"; $lang['settingsWlanSendEmail'] = "email addr."; $lang['settingsWlanSendON'] = "Yes, send email."; @@ -249,7 +251,7 @@ $lang['settingsVolumeManager'] = "Select volume manager"; $lang['settingsWlanReadNav'] = "Read Wlan IP"; -$lang['settingsWlanReadInfo'] = "Read IP address of wlan (wifi) each time after booting? (useful if you hook your Phoniebox into a new wlan networt with dynamic IP)"; +$lang['settingsWlanReadInfo'] = "Read IP address of wlan (wifi) each time after booting? (useful if you hook your Phoniebox into a new wlan network with dynamic IP)"; $lang['settingsWlanReadQuest'] = "Read wlan IP?"; $lang['settingsWlanReadON'] = "Yes, read wlan IP."; $lang['settingsWlanReadOFF'] = "No, do not read wlan IP."; @@ -267,6 +269,8 @@ $lang['infoDebugLogTail'] = "DEBUG log file: Last 40 lines"; $lang['infoDebugLogClear'] = "Clear all content from debug.log"; $lang['infoDebugLogSettings'] = "Debug Log Settings"; +$lang['infoAudioActive'] = "Enabled Audio Devices"; +$lang['infoBluetoothStatus'] = "Bluetooth Status"; /* * Folder Management and File Upload diff --git a/htdocs/lang/lang-fr-FR.php b/htdocs/lang/lang-fr-FR.php new file mode 100755 index 000000000..73780f2d2 --- /dev/null +++ b/htdocs/lang/lang-fr-FR.php @@ -0,0 +1,315 @@ +(Requires Javascript in the browser to be enabled.)"; +$lang['cardEditMessageDefault'] = "Les cartes utilisées par le systeme sont listées ici accueil."; +$lang['cardRegisterMessageSwipeNew'] = "Passer une autre carte si vous souhaitez en enregistrer une autre."; +$lang['cardEditMessageInputNew'] = "Type another card ID pick one from the list on the home page."; +$lang['cardRegisterErrorTooMuch'] = "

This is too much! Please select only one audiofolder. Make up your mind.

"; +$lang['cardRegisterErrorStreamAndAudio'] = "

This is too much! Either specify a stream or select an audio folder or system command. Make up your mind.

"; +$lang['cardRegisterErrorStreamOrAudio'] = "

Seems you haven't selected anything! Add an URL and stream type, select a folder or a system command. Or 'Cancel' to go back to the home page.

"; +$lang['cardRegisterErrorExistingAndNew'] = "

This is too much! Either choose an existing folder or create a new one.

"; +$lang['cardRegisterErrorExistingFolder'] = "

A folder named with the same name already exists! Chose a different one.

"; +$lang['cardRegisterErrorSuggestFolder'] = "A folder name for the stream needs to be created. Below in the form I made a suggestion."; +$lang['cardRegisterErrorConvertSpotifyURL'] = "URL Spotify incorrecte, elle a été convertie au bon format"; +$lang['cardRegisterStream2Card'] = "Le stream est lié à la carte."; +$lang['cardRegisterFolder2Card'] = "Le dossier audio est désormais lié à une carte."; +$lang['cardRegisterDownloadingYT'] = "

Piste YouTube en cours de téléchargement. Cela peut prendre plusieurs minutes. Log dans le fichier \"youtube-dl.log\".

"; +$lang['cardRegisterSwipeUpdates'] = "Mise à jour automatique lors du passage d'une carte."; +$lang['cardRegisterManualLinks'] = "

Vous pouvez lier manuellement une carte à un dossier. Explication dans la documentation connection à phoniebox et enregistrer une carte.

"; +$lang['cardRegisterTriggerSuccess'] = "La carte est désormais lié à une commande :"; + +/* +* Card edit form +*/ +$lang['cardFormFolderLegend'] = "Lier la carte RFID à:"; +$lang['cardFormFolderLabel'] = "Lier une carte à un dossier"; +$lang['cardFormFolderSelectDefault'] = "Aucun (faites défiler pour choisir un dossier)"; +$lang['cardFormFolderHelp'] = "Containing local files or add YouTube content (specify below)."; +$lang['cardFormNewFolderLabel'] = "... ou liez un nouveau dossier"; +$lang['cardFormNewFolderHelp'] = "Always use a new folder for streams (see below) and optionally for YouTube."; +$lang['cardFormNewFolderPlaceholder'] = "ex 'Nom de l'artiste/Album'"; +$lang['cardFormTriggerLegend'] = "Trigger system command"; +$lang['cardFormTriggerLabel'] = "... or link to a system command"; +$lang['cardFormTriggerHelp'] = "Select system commands (like 'pause', 'volume up', 'shutdown') from the list of available commands. If a RFID card is already linked to a function, the ID is shown in the pulldown menu."; +$lang['cardFormTriggerSelectDefault'] = "Select command to link"; + +$lang['cardFormStreamLegend'] = "Link Stream"; +$lang['cardFormStreamLabel'] = "Stream URL (always requires new folder above)"; +$lang['cardFormStreamPlaceholderClassic'] = "http(...).mp3 / .m3u / .ogg / .rss / .xml / ..."; +$lang['cardFormStreamPlaceholderPlusSpotify'] = "spotify:album/artist/playlist/track:### / Stream/Podcast like http....mp3 .xml .rss .ogg"; +$lang['cardFormStreamHelp'] = "Add the URL for spotify, podcast, web radio, stream or other online media"; +$lang['cardFormStreamTypeSelectDefault'] = "Select type"; +$lang['cardFormStreamTypeHelp'] = "Select the type you are adding"; + +$lang['cardFormYTLegend'] = "Download YouTube"; +$lang['cardFormYTLabel'] = "YouTube URL (single clip or playlist)"; +$lang['cardFormYTPlaceholder'] = "e.g. https://www.youtube.com/watch?v=7GI0VdPehQI"; +$lang['cardFormYTSelectDefault'] = "Pull down to select a folder or create a new one below"; +$lang['cardFormYTHelp'] = "Full YouTube-URL of clip or playlist. Will be downloaded in the folder specified above or the new one if specified."; +$lang['cardFormRemoveCard'] = "Remove Card ID"; + +// Export Card IDs as .csv file +$lang['cardExportAnchorLink'] = "Export all RFID links (audio playout and commands)"; +$lang['cardExportButtonLink'] = "Create .csv file of available RFID links"; + +// Import Card IDs as .csv file +$lang['cardImportAnchorLink'] = "Import RFID links from .csv file"; +$lang['cardImportFileLabel'] = "Select .csv file to create RFID links"; +$lang['cardImportFileSuccessUpload'] = "Successful upload of file: "; +$lang['cardImportFileErrorUpload'] = "

There was an error uploading the file, please try again!

"; +$lang['cardImportFileErrorFiletype'] = "

Wrong file type! The file must be a .csv file.

"; +$lang['cardImportFormOverwriteLabel'] = "Select import action"; +$lang['cardImportFormOverwriteHelp'] = "Specify what to do with the uploaded RFID links."; +$lang['cardImportFormOverwriteAll'] = "Overwrite both: audio AND commands"; +$lang['cardImportFormOverwriteAudio'] = "Overwrite ONLY audio triggers"; +$lang['cardImportFormOverwriteCommands'] = "Overwrite ONLY system commands"; +$lang['cardImportFileOverwriteMessageCommands'] = "

System commands were overwritten with uploaded RFID IDs.

"; +$lang['cardImportFileOverwriteMessageAudio'] = "

Links to audio playlists etc. were overwritten with uploaded RFID IDs.

"; +$lang['cardImportFormDeleteLabel'] = "Delete or keep other RFID links?"; +$lang['cardImportFormDeleteNone'] = "Keep all existing: audio AND commands"; +$lang['cardImportFormDeleteAll'] = "Delete both: audio AND commands"; +$lang['cardImportFormDeleteAudio'] = "Delete ONLY audio triggers"; +$lang['cardImportFormDeleteCommands'] = "Delete ONLY system commands"; +$lang['cardImportFormDeleteHelp'] = "Which of the existing RFID links should be kept, which deleted?."; +$lang['cardImportFileDeleteMessageCommands'] = "

System commands deleted.

"; +$lang['cardImportFileDeleteMessageAudio'] = "

Audio links deleted.

"; + +/* +* Track edit form +*/ +$lang['trackEditTitle'] = "Gestion des pistes"; +$lang['trackEditInformation'] = "Information sur la piste"; +$lang['trackEditMove'] = "Déplacer une piste"; +$lang['trackEditMoveSelectLabel'] = "Selectionner un dossier"; +$lang['trackEditMoveSelectDefault'] = "Ne pas déplacer le fichier"; +$lang['trackEditDelete'] = "Supprimer la piste"; +$lang['trackEditDeleteLabel'] = "Etes vous sûr de vouloir supprimer ?"; +$lang['trackEditDeleteHelp'] = "Aucun retour arrière pour la suppression d'une piste. Etes vous sûr ?"; +$lang['trackEditDeleteNo'] = "Non, ne pas supprimer cette piste"; +$lang['trackEditDeleteYes'] = "Oui, supprimer cette piste"; + +/* +* Settings +*/ +$lang['settingsPlayoutBehaviourCard'] = "Paramètrage du lecteur RFID"; +$lang['settingsPlayoutBehaviourCardLabel'] = "Passer ou poser la carte ?"; +$lang['settingsPlayoutBehaviourCardSwipe'] = "Passer une carte lance le lecteur."; +$lang['settingsPlayoutBehaviourCardPlace'] = "Poser la carte pour lancer la lecture, l'enlever pour stopper."; +$lang['settingsPlayoutBehaviourCardHelp'] = "Si vous choisissez 'poser la carte', Cela affecte le deuxième passage."; + +$lang['settingsVolChangePercent'] = "Changement du volume %"; +$lang['settingsMaxVol'] = "Volume maximum"; +$lang['settingsStartupVol'] = "Volume au démarrage"; +$lang['settingsBootVol'] = "Volume après reboot"; +$lang['settingsWifiRestart'] = "Les changements sur la configuration Wifi nécessitent un redémarrage."; +$lang['settingsWifiSsidPlaceholder'] = "ex: PhonieHomie"; +$lang['settingsWifiSsidHelp'] = "Le nom de votre réseau Wifi 'mon super réseau'"; +$lang['settingsWifiPassHelp'] = "Mot de passe Wifi (8 caractères minimum)"; +$lang['settingsWifiPrioHelp'] = "Priorité du Wifi (0-100). Si plusieurs Wifi sont détéctés la box se connectera à celui qui a la priorité la plus haute."; +$lang['settingsSecondSwipe'] = "Deuxième passage de carte"; +$lang['settingsSecondSwipeInfo'] = "Que se passe t'il lors du deuuxième passage d'une même carte ? Lecture / Pause ?"; +$lang['settingsSecondSwipeRestart'] = "Reprendre la playlist au début"; +$lang['settingsSecondSwipeSkipnext'] = "Piste suivante"; +$lang['settingsSecondSwipePause'] = "Pause / Lecture"; +$lang['settingsSecondSwipePlay'] = "Reprendre"; +$lang['settingsSecondSwipeNoAudioPlay'] = "Ignorer les déclenchements automatiques, uniquement pour les commandes systeme"; +$lang['settingsSecondSwipePauseInfo'] = "Ignorer le scan des nouvelle carte pour :"; +$lang['second'] = "seconde"; +$lang['seconds'] = "secondes"; +$lang['settingsSecondSwipePauseControlsInfo'] = "Certain type de carte (ex augmentation et diminution du volume, piste suivante et précedente, reculer / avancer) ne devraient pas avoir de délai :"; +$lang['settingsSecondSwipePauseControlsOn'] = "Utiliser la carte immédiatement"; +$lang['settingsSecondSwipePauseControlsOff'] = "Utiliser la carte après un délai (secondes)"; +$lang['settingsWebInterface'] = "Interface web"; +$lang['settingsCoverInfo'] = "Voulez vous afficher les couvertures de vos playlist et titres ?"; +$lang['settingsShowCoverON'] = "Afficher la couverture"; +$lang['settingsShowCoverOFF'] = "Ne pas afficher la couverture"; +$lang['settingsMessageLangfileNewItems'] = "Il y a des nouveautés pour le fichier de langue lang-fr-FR.php. Envoyer vos modiffications sur Github :)"; +$lang['settingsWlanSendNav'] = "Envoyer l'IP par mail"; +$lang['settingsWlanSendInfo'] = "Envoyer l'IP par mail après reboot ? (pratique si vous êtes en IP dynamique et qe vous souhaitez vous connecter)"; +$lang['settingsWlanSendQuest'] = "Envoyer l'IP par mail ?"; +$lang['settingsWlanSendEmail'] = "email"; +$lang['settingsWlanSendON'] = "Oui, envoyer par mail"; +$lang['settingsWlanSendOFF'] = "Non, ne pas envoyer par mail"; + +$lang['settingsVolumeManager'] = "Sélectionner le gestionnaire de volume"; + +$lang['settingsWlanReadNav'] = "Lecture de l'IP"; +$lang['settingsWlanReadInfo'] = "Récupérer l'IP (wifi) après chaque reboot ? (pratique si vous êtes en IP dynamique et qe vous souhaitez vous connecter)"; +$lang['settingsWlanReadQuest'] = "Lire mon IP réseau ?"; +$lang['settingsWlanReadON'] = "Oui, lire mon IP."; +$lang['settingsWlanReadOFF'] = "Non, ne pas lire mon IP."; + +/* +* System info +*/ +$lang['infoOsDistrib'] = "Distribtion Linux"; +$lang['infoOsCodename'] = "Version de l'OS"; +$lang['infoOsTemperature'] = "Température"; +$lang['infoOsThrottle'] = "Throttling"; +$lang['infoStorageUsed'] = "Utilisation disque"; +$lang['infoMopidyStatus'] = "Etat du serveur Mopidy"; +$lang['infoMPDStatus'] = "Etat du serveur MPD"; +$lang['infoDebugLogTail'] = "Fichier debug: Les 40 dernières lignes"; +$lang['infoDebugLogClear'] = "Effacer le contenu du fichier debug.log"; +$lang['infoDebugLogSettings'] = "Paramètres de debug"; +$lang['infoAudioActive'] = "Périphériques audio activés"; +$lang['infoBluetoothStatus'] = "Etat du Bluetooth"; + +/* +* Folder Management and File Upload +*/ +$lang['manageFilesFoldersTitle'] = "Dossier & Fichier"; +$lang['manageFilesFoldersUploadFilesLabel'] = "Sélection du fichier depuis votre PC"; +$lang['manageFilesFoldersUploadLegend'] = "Uploader un fichier"; +$lang['manageFilesFoldersUploadLabel'] = "Sélectionner un dossier ou en créer un nouveau"; +$lang['manageFilesFoldersUploadFolderHelp'] = "Si vous selectionnez un dossier ET que vous en créez un, le nouveau dossier sera inclus dans celui selectionné."; +$lang['manageFilesFoldersNewFolderTitle'] = "Créer un nouveau dossier"; +$lang['manageFilesFoldersNewFolderPositionLegend'] = "Position du dossier"; +$lang['manageFilesFoldersNewFolderPositionDefault'] = "Le nouveau dossier peut être à la racine ou inclus dans un dossier existant"; +$lang['manageFilesFoldersErrorNewFolderName'] = "

Nom du dossier invalide.

"; +$lang['manageFilesFoldersErrorNewFolder'] = "

Aucun dossier selectionné ou aucun dossier valide renseigné.

"; +$lang['manageFilesFoldersErrorNoNewFolder'] = "

Aucun dossier selectionné ou aucun dossier valide renseigné.

"; +$lang['manageFilesFoldersErrorNewFolderExists'] = "

Le dossier existe deja.

"; +$lang['manageFilesFoldersErrorNewFolderNotParent'] = "

Le dossier parent est absent.

"; +$lang['manageFilesFoldersSuccessNewFolder'] = "Création du nouveau dossier ok: "; +$lang['manageFilesFoldersSelectDefault'] = "Faites défiler pour choisir un dossier existant ou créez en un nouveau"; + +$lang['manageFilesFoldersRenewDB'] = "Renouvellement de la base"; +$lang['manageFilesFoldersLocalScan'] = "Scanner la bibliothèque"; +$lang['manageFilesFoldersRenewDBinfo'] = "Il est conseillé de scanner votre librairie après chaque ajout de fichier ou modification de dossier. Seules les nouvelles musiques ou les modifications seront scannées. Modipy sera stoppé lors du scan et relancé automatiquement à la fin du scan."; + +/* +* File search +*/ +$lang['searchTitle'] = "Recherche de fichiers audio"; +$lang['searchExample'] = "ex Stromae"; +$lang['searchSend'] = "Rechercher"; +$lang['searchResult'] = "Resultats:"; + +/* +* Filter +*/ +$lang['filterall'] = "Tout afficher"; +$lang['filterfile'] = "Fichiers"; +$lang['filterlivestream'] = "Livestream"; +$lang['filterpodcast'] = "Podcast"; +$lang['filterspotify'] = "Spotify"; +$lang['filteryoutube'] = "YouTube"; +?> diff --git a/htdocs/lang/lang-nl-NL.php b/htdocs/lang/lang-nl-NL.php index 1fb654509..83b480cf6 100755 --- a/htdocs/lang/lang-nl-NL.php +++ b/htdocs/lang/lang-nl-NL.php @@ -66,6 +66,7 @@ $lang['globalLoop'] = "Loop"; $lang['globalLang'] = "Taal"; $lang['globalLanguageSettings'] = "Taalinstellingen"; +$lang['globalAudioSink'] = "Audio Devices"; $lang['playerFilePlayed'] = "is gespeeld"; $lang['playerFileAdded'] = "is toegevoegd aan de playlist"; @@ -190,6 +191,8 @@ $lang['infoStorageUsed'] = "Opslag gebruik"; $lang['infoMopidyStatus'] = "Mopidy Server Status"; $lang['infoMPDStatus'] = "MPD Server Status"; +$lang['infoAudioActive'] = "Enabled Audio Devices"; +$lang['infoBluetoothStatus'] = "Bluetooth Status"; /* * Folder Management and File Upload diff --git a/htdocs/settings.php b/htdocs/settings.php index f53be68a2..09e2ae280 100755 --- a/htdocs/settings.php +++ b/htdocs/settings.php @@ -135,6 +135,7 @@ include("inc.setMaxVolume.php"); include("inc.setVolumeStep.php"); include("inc.setStartupVolume.php"); +include("inc.setBootVolume.php"); ?>
@@ -142,6 +143,17 @@ + + + +
diff --git a/htdocs/trackEdit.php b/htdocs/trackEdit.php index 19784f6d6..19be521cd 100755 --- a/htdocs/trackEdit.php +++ b/htdocs/trackEdit.php @@ -247,7 +247,7 @@ * read metadata */ $fileName = Files::buildPath($post['folder'], $post['filename']); -$exec = "mid3v2 -l '" .$fileName ."'" ; +$exec = "mid3v2 -l '" .escapeshellarg($fileName) ."'" ; $res = shell_exec($exec); $lines = explode(PHP_EOL, $res); foreach($lines as $line) { diff --git a/misc/sampleconfigs/gpio_settings.ini.sample b/misc/sampleconfigs/gpio_settings.ini.sample index 92839ebd9..e14444991 100644 --- a/misc/sampleconfigs/gpio_settings.ini.sample +++ b/misc/sampleconfigs/gpio_settings.ini.sample @@ -1,92 +1,121 @@ [DEFAULT] enabled: True +antibouncehack: False + [VolumeControl] -enabled: True +enabled: False Type: TwoButtonControl ;or RotaryEncoder -PinUp: 5 -PinDown: 6 -pull_up: True +Pin1: 5 +Pin2: 6 +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat timeBase: 0.1 ;only for RotaryEncoder -functionCallDown: functionCallVolD -functionCallUp: functionCallVolU +functionCall1: functionCallVolU +functionCall2: functionCallVolD functionCallTwoButtons: functionCallVol0 ;only for TwoButtonControl [PrevNextControl] -enabled: True +enabled: False Type: TwoButtonControl Pin1: 22 Pin2: 23 functionCall1: functionCallPlayerPrev functionCall2: functionCallPlayerNext functionCallTwoButtons: None -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: False +hold_mode: None [PlayPause] -enabled: True +enabled: False Type: Button Pin: 27 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerPause [Shutdown] enabled: False Type: Button Pin: 3 -pull_up: True +pull_up_down: pull_up +hold_mode: Postpone hold_time: 2 functionCall: functionCallShutdown +[PauseShutdown] +enabled: False +Type: Button +Pin: 3 +pull_up_down: pull_up +hold_time: 2.0 +hold_mode: SecondFunc +functionCall: functionCallPlayerPause +functionCall2: functionCallShutdown + [Volume0] enabled: False Type: Button Pin: 17 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallVol0 [VolumeUp] enabled: False Type: Button Pin: 16 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat functionCall: functionCallVolU [VolumeDown] enabled: False Type: Button Pin: 19 -pull_up: True +pull_up_down: pull_up hold_time: 0.3 -hold_repeat: True +hold_mode: Repeat functionCall: functionCallVolD [NextSong] enabled: False Type: Button Pin: 26 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerNext [PrevSong] enabled: False Type: Button Pin: 20 -pull_up: True -hold_time: 0.3 +pull_up_down: pull_up functionCall: functionCallPlayerPrev +[FastForward] +enabled: False +Type: Button +Pin: 7 +pull_up_down: pull_up +functionCall: functionCallPlayerSeekFwd + +[Rewind] +enabled: False +Type: Button +Pin: 8 +pull_up_down: pull_up +functionCall: functionCallPlayerSeekBack + [Halt] enabled: False Type: Button -Pin: 21 -pull_up: True -hold_time: 0.3 +Pin: 25 +pull_up_down: pull_up functionCall: functionCallPlayerPauseForce + +[RFIDDevice] +enabled: False +Type: Button +Pin: 21 +pull_up_down: pull_up +functionCall: functionCallPlayerStop diff --git a/misc/sampleconfigs/phoniebox-bt-buttons.service.sample b/misc/sampleconfigs/phoniebox-bt-buttons.service.sample new file mode 100644 index 000000000..3c8c9231d --- /dev/null +++ b/misc/sampleconfigs/phoniebox-bt-buttons.service.sample @@ -0,0 +1,13 @@ +[Unit] +Description=Phoniebox Bluetooth Buttons Service +After=mpd.service + +[Service] +User=pi +Group=pi +Restart=always +WorkingDirectory=/home/pi/RPi-Jukebox-RFID/components/controls/buttons-bluetooth-headphone +ExecStart=/home/pi/RPi-Jukebox-RFID/components/controls/buttons-bluetooth-headphone/bt-buttons.py + +[Install] +WantedBy=multi-user.target diff --git a/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample b/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample index 80dee4a60..745544634 100755 --- a/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample +++ b/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample @@ -8,6 +8,7 @@ Group=pi Restart=always WorkingDirectory=/home/pi/RPi-Jukebox-RFID ExecStart=/home/pi/RPi-Jukebox-RFID/scripts/daemon_rfid_reader.py +Nice=15 [Install] WantedBy=multi-user.target diff --git a/requirements.txt b/requirements.txt index dfd2c27b3..21e820309 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,8 @@ #### ESSENTIAL LIBRARIES FOR MAIN FUNCTIONALITY #### # related libraries. -evdev==0.7.0 -git+git://github.com/lthiery/SPI-Py.git#egg=spi-py +evdev +git+https://github.com/lthiery/SPI-Py.git#egg=spi-py youtube_dl pyserial RPi.GPIO diff --git a/scripts/Reader.py.experimental b/scripts/Reader.py.experimental index 1db4af167..b34479565 100755 --- a/scripts/Reader.py.experimental +++ b/scripts/Reader.py.experimental @@ -7,19 +7,11 @@ import os.path import sys -import serial -import string + import RPi.GPIO as GPIO import logging from evdev import InputDevice, categorize, ecodes, list_devices -# Workaround: when using RC522 reader with pirc522 pkg the py532lib pkg may not be installed and vice-versa -try: - import pirc522 - from py532lib.i2c import * - from py532lib.mifare import * -except ImportError: - pass logger = logging.getLogger(__name__) @@ -59,6 +51,7 @@ class UsbReader(object): class Mfrc522Reader(object): def __init__(self): + import pirc522 self.device = pirc522.RFID() def readCard(self): @@ -83,47 +76,86 @@ class Mfrc522Reader(object): class Rdm6300Reader: - def __init__(self): + def __init__(self, param=None): + import serial device = '/dev/ttyS0' baudrate = 9600 ser_timeout = 0.1 self.last_card_id = '' try: self.rfid_serial = serial.Serial(device, baudrate, timeout=ser_timeout) + self.serial_SerialException = serial.SerialException except serial.SerialException as e: logger.error(e) exit(1) + self.number_format = '' + if param is not None: + nf = param.get("numberformat") + if nf is not None: + self.number_format = nf + + def convert_to_weigand26_when_checksum_ok(self, raw_card_id): + weigand26 = [] + xor = 0 + for i in range(0, len(raw_card_id) >> 1): + val = int(raw_card_id[i * 2:i * 2 + 2], 16) + if (i < 5): + xor = xor ^ val + weigand26.append(val) + else: + chk = val + if (chk == val): + return weigand26 + else: + return None + def readCard(self): - byte_card_id = b'' + byte_card_id = bytearray() try: while True: try: - read_byte = self.rfid_serial.read() - - if read_byte == b'\x02': # start byte - while read_byte != b'\x03': # end bye - read_byte = self.rfid_serial.read() - byte_card_id += read_byte - - card_id = byte_card_id.decode('utf-8') - byte_card_id = '' - card_id = ''.join(x for x in card_id if x in string.printable) - - # Only return UUIDs with correct length - if len(card_id) == 12 and card_id != self.last_card_id: - self.last_card_id = card_id - self.rfid_serial.reset_input_buffer() - return self.last_card_id - - else: # wrong UUID length or already send that UUID last time - self.rfid_serial.reset_input_buffer() + wait_for_start_byte = True + while True: + read_byte = self.rfid_serial.read() + + if (wait_for_start_byte): + if read_byte == b'\x02': + wait_for_start_byte = False + else: + if read_byte != b'\x03': # could get stuck here, check len? check timeout by len == 0?? + byte_card_id.extend(read_byte) + else: + break + + raw_card_id = byte_card_id.decode('ascii') + byte_card_id.clear() + self.rfid_serial.reset_input_buffer() + + if len(raw_card_id) == 12: + w26 = self.convert_to_weigand26_when_checksum_ok(raw_card_id) + if (w26 is not None): + # print ("factory code is ignored" ,w26[0]) + + if self.number_format == 'card_id_dec': + # this will return a 10 Digit card ID e.g. 0006762840 + card_id = '{0:010d}'.format((w26[1] << 24) + (w26[2] << 16) + (w26[3] << 8) + w26[4]) + elif self.number_format == 'card_id_float': + # this will return card ID as fraction e.g. 103,12632 + card_id = '{0:d},{1:05d}'.format(((w26[1] << 8) + w26[2]), ((w26[3] << 8) + w26[4])) + else: + # this will return the raw (original) card ID e.g. 070067315809 + card_id = raw_card_id + + if card_id != self.last_card_id: # does this still makes sense here? + self.last_card_id = card_id # Means 2nd swipe will not be possible with RDM6300 + return self.last_card_id # intentionaly? Good reason for this? except ValueError as ve: - logger.errror(ve) + logger.error(ve) - except serial.SerialException as se: + except self.serial_SerialException as se: logger.error(se) def cleanup(self): @@ -132,6 +164,9 @@ class Rdm6300Reader: class Pn532Reader: def __init__(self): + from py532lib.i2c import Pn532_i2c + from py532lib.mifare import Mifare + from py532lib.mifare import MIFARE_WAIT_FOR_ENTRY pn532 = Pn532_i2c() self.device = Mifare() self.device.SAMconfigure() @@ -152,11 +187,13 @@ class Reader(object): sys.exit('Please run RegisterDevice.py first') else: with open(path + '/deviceName.txt', 'r') as f: - device_name = f.read() + device_name = f.read().rstrip().split(';', 1)[0] if device_name == 'MFRC522': self.reader = Mfrc522Reader() elif device_name == 'RDM6300': + # The Rdm6300Reader supports 2 Additional Number Formats which can bee choosen by an optional parameter dictionary: + # {'numberformat':'card_id_float'} or {'numberformat':'card_id_dec'} self.reader = Rdm6300Reader() elif device_name == 'PN532': self.reader = Pn532Reader() diff --git a/scripts/Reader.py.experimental.Multi b/scripts/Reader.py.experimental.Multi index 326a5bd86..a4626eb5f 100644 --- a/scripts/Reader.py.experimental.Multi +++ b/scripts/Reader.py.experimental.Multi @@ -27,6 +27,7 @@ except ImportError: logger = logging.getLogger(__name__) + class EDevices(Enum): MFRC522 = 0 RDM6300 = 1 diff --git a/scripts/Reader.py.pcsc b/scripts/Reader.py.pcsc new file mode 100644 index 000000000..dac10de7c --- /dev/null +++ b/scripts/Reader.py.pcsc @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +# Reader.py supporting PC/SC-readers +# Uses the first available reader +# Requirements +# apt install pcscd python3-pyscard + +from smartcard.scard import * +from smartcard.util import toHexString +from smartcard.util import * + + +class Reader: + + reader = None + + def __init__(self): + self.reader = self + + def readCard(self): + + response = [] + + try: + hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) + if hresult != SCARD_S_SUCCESS: + raise error( + 'Failed to establish context: ' + \ + SCardGetErrorMessage(hresult)) + + try: + hresult, readers = SCardListReaders(hcontext, []) + if hresult != SCARD_S_SUCCESS: + raise error( + 'Failed to list readers: ' + \ + SCardGetErrorMessage(hresult)) + + readerstates = [] + for i in range(len(readers)): + readerstates += [(readers[i], SCARD_STATE_UNAWARE)] + + hresult, newstates = SCardGetStatusChange(hcontext, 0, readerstates) + + hresult, newstates = SCardGetStatusChange( + hcontext, + INFINITE, + newstates) + + reader = readers[0] + + hresult, hcard, dwActiveProtocol = SCardConnect( + hcontext, + reader, + SCARD_SHARE_SHARED, + SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1) + + hresult, response = SCardTransmit(hcard, dwActiveProtocol, [0xFF, 0xCA, 0x00, 0x00, 0x00]) + + finally: + hresult = SCardReleaseContext(hcontext) + if hresult != SCARD_S_SUCCESS: + raise error( + 'Failed to release context: ' + \ + SCardGetErrorMessage(hresult)) + + return(toHexString(response, PACK)) + + except error as e: + print(e) diff --git a/scripts/daemon_rfid_reader.py b/scripts/daemon_rfid_reader.py index 872f59986..cc643f26d 100755 --- a/scripts/daemon_rfid_reader.py +++ b/scripts/daemon_rfid_reader.py @@ -36,6 +36,10 @@ previous_id = "" previous_time = time.time() +# get swipe or place configuration value +sop = open('../settings/Swipe_or_Place', 'r') +swipe_or_place = sop.read().strip() + # create array for control card ids cards = [] @@ -75,8 +79,10 @@ def handler(signum, frame): while True: # slow down the card reading while loop time.sleep(0.2) - # enable the signal alarm (if no card is present for 1 second) - signal.alarm(1) + + if swipe_or_place == "PLACENOTSWIPE": + # enable the signal alarm (if no card is present for 1 second) + signal.alarm(1) # reading the card id # NOTE: it's been reported that KKMOON Reader might need the following line altered. diff --git a/scripts/helperscripts/README.md b/scripts/helperscripts/README.md new file mode 100644 index 000000000..be4914a6e --- /dev/null +++ b/scripts/helperscripts/README.md @@ -0,0 +1,70 @@ +# helper scripts + +## Analytics_AfterInstallScript.sh + +Checks the system and all conf files to give some feedback in the case of problems. +Might be outdated... + +## AssignIDs4Shortcuts.php + +This script is used by the web app. +This script is called from the command line. +It reads a CSV file and creates shortcuts for audiofolders from the file. +It also creates a modified version of the file rfid_trigger_play.sh which controls the playout. +As a source it uses rfid_trigger_play.sh.sample + +## CreateCsvFromShortcuts.php + +This script is used by the web app. +This script is called from the command line. +It will read all shortcut files and create a CSV file with matching pairs +of RFID and audio folder name. +The created CSV file starts with the line +"id","value" + +## CreatePodcastsKidsDeutsch.sh + +Creates sample folders with files and streams +inside the $AUDIOFOLDERSPATH directory + +## CreateSampleAudiofoldersStreams.sh + +Creates sample folders with files and streams +inside the $AUDIOFOLDERSPATH directory + +## DeleteAllConfig.sh + +This script will delete all config files +including mpd.conf and the like. + +## DeleteSampleAudiofoldersStreams.sh + +Deletes sample folders with files and streams +inside the $AUDIOFOLDERSPATH directory + +## autohotspot + +Changed to autohotspot service + +## cli-player.py + +Command line player to play folders on the Phoniebox. + +A command line replacement some functionality of the phoniebox-web-ui, which challenges the raspberry pi zero. +Using this small script significantly reduces resource usage on the system. + +## cli_ReadWifiIp.php + +Reads out the IP of the Phoniebox in English language on boot. + +## organizeFiles.py + +A small script for conveniently organizing audio folders, +linking them to RFID cards, finding audio folders that are currently +not bound to any RFID card, and fixing broken links. + +A command line replacement some functionality of the phoniebox-web-ui, which challenges the raspberry pi zero. +Using this small script significantly reduces resource usage on the system. + + + diff --git a/scripts/helperscripts/cli-player.py b/scripts/helperscripts/cli-player.py new file mode 100644 index 000000000..8f73b2c30 --- /dev/null +++ b/scripts/helperscripts/cli-player.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +import os +import subprocess +from organizeFiles import readFolders + + +if __name__ == "__main__": + baseDir = "/home/pi/RPi-Jukebox-RFID" + shortcutsDir = os.path.join(baseDir, "shared", "shortcuts") + audioDir = os.path.join(baseDir, "shared", "audiofolders") + scriptsDir = os.path.join(baseDir, "scripts") + + print("=== audio folders:") + audioFolders = {} + folders = readFolders(audioDir=audioDir) + lc2 = 0 + for d2, hasFolderConf2 in sorted(folders.items()): + print(str(lc2) + ": " + d2) + audioFolders[lc2] = d2 + lc2 = lc2 + 1 + + while True: + i = input("select folder / enter mpc command: ") + if i == "quit" or i == "exit": + break + if not i.isnumeric(): + if len(i.strip()) != 0: + print("mpc " + i) + subprocess.call(["mpc"] + i.split(" ")) + continue + inum = int(i) + if inum not in audioFolders: + print("invalid option") + continue + selectedFolder = audioFolders[inum] + print(" playing " + selectedFolder) + subprocess.check_output([scriptsDir + '/rfid_trigger_play.sh', '--dir=' + selectedFolder], shell=False) + + print("bye.") diff --git a/scripts/helperscripts/organizeFiles.py b/scripts/helperscripts/organizeFiles.py new file mode 100644 index 000000000..596ee4839 --- /dev/null +++ b/scripts/helperscripts/organizeFiles.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +import sys +import os +import argparse + + +musicConf = """CURRENTFILENAME="filename" +ELAPSED="0" +PLAYSTATUS="Stopped" +RESUME="OFF" +SHUFFLE="OFF" +LOOP="OFF" +SINGLE="OFF" +""" + +audiobookConf = """CURRENTFILENAME="filename" +ELAPSED="0" +PLAYSTATUS="Stopped" +RESUME="ON" +SHUFFLE="OFF" +LOOP="OFF" +SINGLE="OFF" +""" + + +def readShortcuts(shortcutsDir): + result = {} + for f in os.listdir(shortcutsDir): + absf = os.path.join(shortcutsDir, f) + if os.path.isfile(absf): + val = [] + with open(absf, "r") as fobj: + for line in fobj: + if len(line.strip()) != 0: + val.append(line.rstrip()) + result[f] = val + return result + + +def readFolders(audioDir, relpath=None, isFirst=True): + result = {} + relpath = "" if relpath is None else relpath + hasAudioFiles = False + for f in os.listdir(audioDir): + absf = os.path.join(audioDir, f) + if os.path.isfile(absf): + if not isFirst: + hasAudioFiles = True + elif os.path.isdir(absf): + childResult = readFolders(audioDir=absf, relpath=os.path.join(relpath, f), isFirst=False) + for k, v in childResult.items(): + assert(k not in result) + result[k] = v + if hasAudioFiles: + result[relpath] = os.path.exists(os.path.join(audioDir, "folder.conf")) + return result + + +def _deleteBrokenSymlink(shortcutsDir, cardid, d): + i = input("\ndelete broken symlink [" + cardid + " --> " + str(d) + "]? [y/N]") + if i == "y": + print("deleting symlink.") + os.remove(os.path.join(shortcutsDir, cardid)) + else: + print("keeping broken symlink.") + + +def fixBrokenShortcuts(shortcutsDir, shortcuts, audioFolders): + for cardid, dirs in shortcuts.items(): + if len(dirs) == 0 and cardid != "placeholder": + _deleteBrokenSymlink(shortcutsDir=shortcutsDir, cardid=cardid, d=None) + for d in dirs: + if d not in audioFolders and d != cardid: + _deleteBrokenSymlink(shortcutsDir=shortcutsDir, cardid=cardid, d=d) + + +def _writeFolderConf(audioDir, d, content): + with open(os.path.join(audioDir, d, "folder.conf"), "w") as f: + f.write(content) + + +def _askFolderType(audioDir, d): + i = input("\ntype of " + d + " ? [m]usic/[a]udiobook/[I]gnore: ") + if i == "m": + _writeFolderConf(audioDir=audioDir, d=d, content=musicConf) + elif i == "a": + _writeFolderConf(audioDir=audioDir, d=d, content=audiobookConf) + else: + print("ignoring folder.") + + +def linkLooseFolders(shortcutsDir, audioDir, shortcuts, audioFolders, latestRFIDFile): + allShortcutsDirs = [] + looseFolders = {} + + print("\n\n=== linking loose folders") + for cardid, dirs in shortcuts.items(): + allShortcutsDirs.extend(dirs) + lc2 = 0 + for d2, hasFolderConf2 in sorted(audioFolders.items()): + if d2 not in allShortcutsDirs: + looseFolders[lc2] = d2 + lc2 = lc2 + 1 + + while len(looseFolders) != 0: + print("\n== loose folders:") + for lc, d in looseFolders.items(): + print(str(lc) + ": " + d) + selectedOption = input("\nplease select folder: ") + if len(selectedOption.strip()) == 0: + print("cancel.") + break + if not selectedOption.isnumeric(): + print("invalid input.") + continue + selectedOptionInt = int(selectedOption) + if selectedOptionInt < 0 or selectedOptionInt not in looseFolders: + print("invalid input.") + continue + + with open(latestRFIDFile, "r") as rf: + latestRFID = rf.read().strip() + + d = looseFolders[selectedOptionInt] + cardid = input("\ncardid for \"" + d + "\" [" + latestRFID + "] (enter \"c\" to cancel): ") + if cardid == "c": + print("ok, ignoring this folder.") + else: + if len(cardid) == 0: + cardid = latestRFID + doit = True + if cardid in shortcuts: + doit = False + yn = input("WARNING: cardid already assigned to " + str(shortcuts[cardid]) + ". Override? [y/N] ") + if yn == "y": + doit = True + + if doit: + if not audioFolders[d]: + _askFolderType(audioDir=audioDir, d=d) + with open(os.path.join(shortcutsDir, cardid), "w") as f: + f.write(d) + looseFolders.pop(selectedOptionInt, None) + else: + print("skipping.") + print("done.") + + +def fixFoldersWithoutFolderConf(audioDir, audioFolders): + print("\n\n=== Fixing folders with missing folder.conf ...") + for d, hasFolderConf in audioFolders.items(): + if not hasFolderConf: + _askFolderType(audioDir=audioDir, d=d) + print("=== done.") + + +def findDuplicateShortcuts(shortcuts): + print("\n\n=== Checking folders with multiple shortcuts ...") + linkedFolders = {} + for cardid, dirs in shortcuts.items(): + for d in dirs: + if d not in linkedFolders: + linkedFolders[d] = [] + linkedFolders[d].append(cardid) + for d, cardids in linkedFolders.items(): + if len(cardids) > 1: + print("WARNING: multiple shortcuts for folder [" + d + "]: " + str(cardids)) + print("=== done.") + + +if __name__ == "__main__": + baseDir = "/home/pi/RPi-Jukebox-RFID" + latestRFIDFile = os.path.join(baseDir, "settings", "Latest_RFID") + shortcutsDir = os.path.join(baseDir, "shared", "shortcuts") + audioDir = os.path.join(baseDir, "shared", "audiofolders") + + parser = argparse.ArgumentParser() + parser.add_argument("--baseDir", help="directory containing the phoniebox code; defaults to " + baseDir) + parser.add_argument("--latestRFIDFile", help="file storing the latest RFID card id; defaults to " + latestRFIDFile) + parser.add_argument("--shortcutsDir", help="directory containing the RFID card id shortcuts; defaults to " + shortcutsDir) + parser.add_argument("--audioDir", help="directory containing the audio files; defaults to " + audioDir) + + parser.add_argument("--printShortcuts", help="print list of available shortcuts", action="store_true") + parser.add_argument("--linkLooseFolders", help="iterate through list of folders that are currently unbound to any card id and ask user whether to link them", action="store_true") + parser.add_argument("--fixBrokenShortcuts", help="find and delete dangling shortcuts ", action="store_true") + parser.add_argument("--findDuplicateShortcuts", help="find and delete duplicate shortcuts ", action="store_true") + parser.add_argument("--fixFoldersWithoutFolderConf", help="ask user whether folders without a folder.conf file should be either treated as a music album or an audio book", action="store_true") + args = parser.parse_args() + + if args.baseDir: + baseDir = args.baseDir + if args.latestRFIDFile: + latestRFIDFile = args.latestRFIDFile + if args.shortcutsDir: + shortcutsDir = args.shortcutsDir + if args.audioDir: + audioDir = args.audioDir + + shortcuts = readShortcuts(shortcutsDir=shortcutsDir) + audioFolders = readFolders(audioDir=audioDir) + + if args.printShortcuts: + print("===== shortcuts =====") + shortcutslist = [] + for cardid, thefolders in sorted(shortcuts.items()): + for f in thefolders: + shortcutslist.append([cardid, f]) + for e in sorted(shortcutslist, key=lambda x: x[1]): + print("\"" + e[1] + "\";\t\"" + e[0] + "\"") + print("==================================") + + if args.linkLooseFolders: + linkLooseFolders(shortcutsDir=shortcutsDir, audioDir=audioDir, shortcuts=shortcuts, audioFolders=audioFolders, latestRFIDFile=latestRFIDFile) + if args.fixBrokenShortcuts: + fixBrokenShortcuts(shortcutsDir=shortcutsDir, shortcuts=shortcuts, audioFolders=audioFolders) + if args.findDuplicateShortcuts: + shortcuts2 = readShortcuts(shortcutsDir=shortcutsDir) + findDuplicateShortcuts(shortcuts=shortcuts2) + if args.fixFoldersWithoutFolderConf: + audioFolders2 = readFolders(audioDir=audioDir) + fixFoldersWithoutFolderConf(audioDir=audioDir, audioFolders=audioFolders2) diff --git a/scripts/idle-watchdog.sh b/scripts/idle-watchdog.sh index c4e721474..2183ebf0b 100755 --- a/scripts/idle-watchdog.sh +++ b/scripts/idle-watchdog.sh @@ -34,7 +34,8 @@ do fi # If box is playing and volume is greater 0, remove idle shutdown. Skip this if "at"-queue is already empty - if [ "$(echo "$PLAYERSTATUS" | grep -c "\[playing\]")" == "1" ] && [ $VOLPERCENT -ne "0" ] && [ -n "$(sudo atq -q i)" ]; + # If volume is controlled by amixer, VOLPERCENT is empty. Ignore that. + if [ "$(echo "$PLAYERSTATUS" | grep -c "\[playing\]")" == "1" ] && [[ -z $VOLPERCENT || $VOLPERCENT -ne "0" ]] && [ -n "$(sudo atq -q i)" ]; then for i in `sudo atq -q i | awk '{print $1}'`;do sudo atrm $i;done fi diff --git a/scripts/inc.writeGlobalConfig.sh b/scripts/inc.writeGlobalConfig.sh index 85aa4c224..856c54fb4 100755 --- a/scripts/inc.writeGlobalConfig.sh +++ b/scripts/inc.writeGlobalConfig.sh @@ -119,6 +119,16 @@ fi # 2. then|or read value from file AUDIOIFACENAME=`cat $PATHDATA/../settings/Audio_iFace_Name` +############################################## +# Audio_iFace_Active +# 1. create a default if file does not exist +if [ ! -f $PATHDATA/../settings/Audio_iFace_Active ]; then + echo "0" > $PATHDATA/../settings/Audio_iFace_Active + chmod 777 $PATHDATA/../settings/Audio_iFace_Active +fi +# 2. then|or read value from file +AUDIOIFACEACTIVE=`cat $PATHDATA/../settings/Audio_iFace_Active` + ############################################## # Volume_Manager (mpd or amixer) # 1. create a default if file does not exist @@ -163,12 +173,22 @@ AUDIOVOLMINLIMIT=`cat $PATHDATA/../settings/Min_Volume_Limit` # Startup_Volume # 1. create a default if file does not exist if [ ! -f $PATHDATA/../settings/Startup_Volume ]; then - echo "30" > $PATHDATA/../settings/Startup_Volume + echo "0" > $PATHDATA/../settings/Startup_Volume chmod 777 $PATHDATA/../settings/Startup_Volume fi # 2. then|or read value from file AUDIOVOLSTARTUP=`cat $PATHDATA/../settings/Startup_Volume` +############################################## +# Volume_Boot - after reboot +# 1. create a default if file does not exist +if [ ! -f $PATHDATA/../settings/Volume_Boot ]; then + echo "30" > $PATHDATA/../settings/Volume_Boot + chmod 777 $PATHDATA/../settings/Volume_Boot +fi +# 2. then|or read value from file +AUDIOVOLBOOT=`cat $PATHDATA/../settings/Volume_Boot` + ############################################## # Change_Volume_Idle # Change volume during idle (or only change it during Play and in the WebApp) @@ -313,11 +333,13 @@ CMDSEEKBACK=`grep 'CMDSEEKBACK' $PATHDATA/../settings/rfid_trigger_play.conf|tai # SECONDSWIPEPAUSE # SECONDSWIPEPAUSECONTROLS # AUDIOIFACENAME +# AUDIOIFACEACTIVE # VOLUMEMANAGER # AUDIOVOLCHANGESTEP # AUDIOVOLMAXLIMIT # AUDIOVOLMINLIMIT # AUDIOVOLSTARTUP +# AUDIOVOLBOOT # VOLCHANGEIDLE # IDLETIMESHUTDOWN # POWEROFFCMD @@ -348,11 +370,13 @@ echo "SECONDSWIPE=\"${SECONDSWIPE}\"" >> "${PATHDATA}/../settings/global.conf" echo "SECONDSWIPEPAUSE=\"${SECONDSWIPEPAUSE}\"" >> "${PATHDATA}/../settings/global.conf" echo "SECONDSWIPEPAUSECONTROLS=\"${SECONDSWIPEPAUSECONTROLS}\"" >> "${PATHDATA}/../settings/global.conf" echo "AUDIOIFACENAME=\"${AUDIOIFACENAME}\"" >> "${PATHDATA}/../settings/global.conf" +echo "AUDIOIFACEACTIVE=\"${AUDIOIFACEACTIVE}\"" >> "${PATHDATA}/../settings/global.conf" echo "VOLUMEMANAGER=\"${VOLUMEMANAGER}\"" >> "${PATHDATA}/../settings/global.conf" echo "AUDIOVOLCHANGESTEP=\"${AUDIOVOLCHANGESTEP}\"" >> "${PATHDATA}/../settings/global.conf" echo "AUDIOVOLMAXLIMIT=\"${AUDIOVOLMAXLIMIT}\"" >> "${PATHDATA}/../settings/global.conf" echo "AUDIOVOLMINLIMIT=\"${AUDIOVOLMINLIMIT}\"" >> "${PATHDATA}/../settings/global.conf" echo "AUDIOVOLSTARTUP=\"${AUDIOVOLSTARTUP}\"" >> "${PATHDATA}/../settings/global.conf" +echo "AUDIOVOLBOOT=\"${AUDIOVOLBOOT}\"" >> "${PATHDATA}/../settings/global.conf" echo "VOLCHANGEIDLE=\"${VOLCHANGEIDLE}\"" >> "${PATHDATA}/../settings/global.conf" echo "IDLETIMESHUTDOWN=\"${IDLETIMESHUTDOWN}\"" >> "${PATHDATA}/../settings/global.conf" echo "POWEROFFCMD=\"${POWEROFFCMD}\"" >> "${PATHDATA}/../settings/global.conf" diff --git a/scripts/installscripts/buster-install-default-with-autohotspot.sh b/scripts/installscripts/buster-install-default-with-autohotspot.sh old mode 100755 new mode 100644 index c7e748ab1..2517681fe --- a/scripts/installscripts/buster-install-default-with-autohotspot.sh +++ b/scripts/installscripts/buster-install-default-with-autohotspot.sh @@ -4,7 +4,7 @@ # # NOTE: Running automated install (without interaction): # Each install creates a file called PhonieboxInstall.conf -# in the folder /home/pi/ +# in you $HOME directory # You can install the Phoniebox using such a config file # which means you don't need to run the interactive install: # @@ -12,7 +12,7 @@ # https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/scripts/installscripts # (note: currently only works for buster and newer OS) # 2. make the file executable: chmod +x -# 3. place the PhonieboxInstall.conf in the folder /home/pi/ +# 3. place the PhonieboxInstall.conf in the folder $HOME # 4. run the installscript with option -a like this: # buster-install-default.sh -a @@ -28,7 +28,7 @@ DATETIME=$(date +"%Y%m%d_%H%M%S") SCRIPTNAME="$(basename $0)" JOB="${SCRIPTNAME}" -HOME_DIR="/home/pi" +HOME_DIR=$(echo $HOME) JUKEBOX_HOME_DIR="${HOME_DIR}/RPi-Jukebox-RFID" LOGDIR="${HOME_DIR}"/phoniebox_logs @@ -104,9 +104,9 @@ will guide you through the configuration. If you want to run the AUTOMATED INSTALL (non-interactive) from an existing configuration file, do the following: 1. exit this install script (press n) -2. place your PhonieboxInstall.conf in the folder /home/pi/ +2. place your PhonieboxInstall.conf in the folder ${HOME_DIR} 3. run the installscript with option -a. For example like this: - ./home/pi/buster-install-default.sh -a + .${HOME_DIR}/buster-install-default.sh -a " read -rp "Continue interactive installation? [Y/n] " response case "$response" in @@ -241,7 +241,7 @@ check_existing() { echo "Everything else will remain in a folder called 'BACKUP'. " - ### + ### # See if we find the PhonieboxInstall.conf file # We need to do this first, because if we re-use the .conf file, we need to append # the variables regarding the found content to the also found configuration file. @@ -545,7 +545,7 @@ config_audio_folder() { ##################################################### # Folder path for audio files - # default: /home/pi/RPi-Jukebox-RFID/shared/audiofolders + # default: $HOME/RPi-Jukebox-RFID/shared/audiofolders clear @@ -683,14 +683,17 @@ samba_config() { sudo chmod 644 "${smb_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${smb_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${smb_conf}" # Samba: create user 'pi' with password 'raspberry' + # ToDo: use current user with a default password (echo "raspberry"; echo "raspberry") | sudo smbpasswd -s -a pi } web_server_config() { local lighthttpd_conf="/etc/lighttpd/lighttpd.conf" local fastcgi_php_conf="/etc/lighttpd/conf-available/15-fastcgi-php.conf" - local php_ini="/etc/php/7.3/cgi/php.ini" + local php_ini="/etc/php/$(ls -1 /etc/php)/cgi/php.ini" local sudoers="/etc/sudoers" echo "Configuring web server..." @@ -699,6 +702,8 @@ web_server_config() { sudo cp "${jukebox_dir}"/misc/sampleconfigs/lighttpd.conf.buster-default.sample "${lighthttpd_conf}" sudo chown root:root "${lighthttpd_conf}" sudo chmod 644 "${lighthttpd_conf}" + # double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${lighthttpd_conf}" # Web server PHP7 fastcgi conf # -rw-r--r-- 1 root root 398 Apr 30 09:35 /etc/lighttpd/conf-available/15-fastcgi-php.conf @@ -793,7 +798,7 @@ install_main() { ${apt_get} ${allow_downgrades} install raspberrypi-kernel-headers fi - ${apt_get} ${allow_downgrades} install samba samba-common-bin gcc lighttpd php7.3-common php7.3-cgi php7.3 at mpd mpc mpg123 git ffmpeg resolvconf spi-tools + ${apt_get} ${allow_downgrades} install samba samba-common-bin gcc lighttpd php-common php-cgi php at mpd mpc mpg123 git ffmpeg resolvconf spi-tools netcat alsa-utils lsof procps # restore backup of /etc/resolv.conf in case installation of resolvconf cleared it sudo cp /etc/resolv.conf.orig /etc/resolv.conf @@ -828,7 +833,7 @@ install_main() { # keep major verson 3 of mopidy echo -e "Package: mopidy\nPin: version 3.*\nPin-Priority: 1001" | sudo tee /etc/apt/preferences.d/mopidy - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - + sudo wget -q -O /usr/local/share/keyrings/mopidy-archive-keyring.gpg https://apt.mopidy.com/mopidy.gpg sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list ${apt_get} update ${apt_get} upgrade @@ -839,14 +844,6 @@ install_main() { sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements-spotify.txt fi - local raw_github="https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID" - # I comment the following lines out for now. I think they come from splitti when he applied a hotfix in Feb 2020? - # Back then the master install script needed develop branch files. I think this is from that time...? - #sudo rm "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample - #wget -P "${jukebox_dir}"/misc/sampleconfigs/ "${raw_github}"/develop/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample - #sudo rm "${jukebox_dir}"/scripts/RegisterDevice.py - #wget -P "${jukebox_dir}"/scripts/ "${raw_github}"/develop/scripts/RegisterDevice.py - # Install more required packages echo "Installing additional Python packages..." sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements.txt @@ -878,16 +875,14 @@ install_main() { # create config file for web app from sample sudo cp "${jukebox_dir}"/htdocs/config.php.sample "${jukebox_dir}"/htdocs/config.php + # double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${jukebox_dir}"/htdocs/config.php # Starting web server and php7 sudo lighttpd-enable-mod fastcgi sudo lighttpd-enable-mod fastcgi-php sudo service lighttpd force-reload - # create copy of GPIO script - sudo cp "${jukebox_dir}"/misc/sampleconfigs/gpio-buttons.py.sample "${jukebox_dir}"/scripts/gpio-buttons.py - sudo chmod +x "${jukebox_dir}"/scripts/gpio-buttons.py - # make sure bash scripts have the right settings sudo chown pi:www-data "${jukebox_dir}"/scripts/*.sh sudo chmod +x "${jukebox_dir}"/scripts/*.sh @@ -913,13 +908,30 @@ install_main() { sudo rm "${systemd_dir}"/phoniebox-gpio-buttons.service echo "### Done with erasing old daemons. Stop ignoring errors!" # 2. install new ones - this is version > 1.1.8-beta - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample "${systemd_dir}"/phoniebox-rfid-reader.service + RFID_READER_SERVICE="${systemd_dir}/phoniebox-rfid-reader.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample "${RFID_READER_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${RFID_READER_SERVICE}" + #startup sound now part of phoniebox-startup-scripts #sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-sound.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-scripts.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-buttons.service.stretch-default.sample "${systemd_dir}"/phoniebox-gpio-buttons.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample "${systemd_dir}"/phoniebox-idle-watchdog.service - [[ "${GPIOconfig}" == "YES" ]] && sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-control.service.sample "${systemd_dir}"/phoniebox-gpio-control.service + STARTUP_SCRIPT_SERVICE="${systemd_dir}/phoniebox-startup-scripts.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample "${STARTUP_SCRIPT_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${STARTUP_SCRIPT_SERVICE}" + + IDLE_WATCHDOG_SERVICE="${systemd_dir}/phoniebox-idle-watchdog.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample "${IDLE_WATCHDOG_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${IDLE_WATCHDOG_SERVICE}" + + if [[ "${GPIOconfig}" == "YES" ]]; then + GPIO_CONTROL_SERVICE="${systemd_dir}/phoniebox-gpio-control.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-control.service.sample "${GPIO_CONTROL_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${GPIO_CONTROL_SERVICE}" + fi + sudo chown root:root "${systemd_dir}"/phoniebox-*.service sudo chmod 644 "${systemd_dir}"/phoniebox-*.service # enable the services needed @@ -928,9 +940,6 @@ install_main() { #startup sound is part of phoniebox-startup-scripts now #sudo systemctl enable phoniebox-startup-sound sudo systemctl enable phoniebox-startup-scripts - sudo systemctl enable phoniebox-gpio-buttons - sudo systemctl enable phoniebox-rotary-encoder.service - # copy mp3s for startup and shutdown sound to the right folder cp "${jukebox_dir}"/misc/sampleconfigs/startupsound.mp3.sample "${jukebox_dir}"/shared/startupsound.mp3 cp "${jukebox_dir}"/misc/sampleconfigs/shutdownsound.mp3.sample "${jukebox_dir}"/shared/shutdownsound.mp3 @@ -956,21 +965,25 @@ install_main() { sudo sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${etc_mopidy_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${etc_mopidy_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${etc_mopidy_conf}" + sed -i 's/%spotify_username%/'"$SPOTIuser"'/' "${mopidy_conf}" sed -i 's/%spotify_password%/'"$SPOTIpass"'/' "${mopidy_conf}" sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' "${mopidy_conf}" sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${mopidy_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mopidy_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${mopidy_conf}" fi # GPIO-Control if [[ "${GPIOconfig}" == "YES" ]]; then sudo python3 -m pip install --upgrade --force-reinstall -q -r "${jukebox_dir}"/requirements-GPIO.txt sudo systemctl enable phoniebox-gpio-control.service - if [[ ! -f ~/.config/phoniebox/gpio_settings.ini ]]; then - mkdir -p ~/.config/phoniebox - cp "${jukebox_dir}"/components/gpio_control/example_configs/gpio_settings.ini ~/.config/phoniebox/gpio_settings.ini + if [[ ! -f "${jukebox_dir}"/settings/gpio_settings.ini ]]; then + cp "${jukebox_dir}"/misc/sampleconfigs/gpio_settings.ini.sample "${jukebox_dir}"/settings/gpio_settings.ini fi fi @@ -985,6 +998,8 @@ install_main() { sudo sed -i 's/%AUDIOiFace%/'"$AUDIOiFace"'/' "${mpd_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mpd_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${mpd_conf}" sudo chown mpd:audio "${mpd_conf}" sudo chmod 640 "${mpd_conf}" fi @@ -1104,7 +1119,10 @@ existing_assets() { # make buttons_usb_encoder.py ready to be use from phoniebox-buttons-usb-encoder service sudo chmod +x "${jukebox_dir}"/components/controls/buttons_usb_encoder/buttons_usb_encoder.py # make sure service is still enabled by registering again - sudo cp -v "${jukebox_dir}"/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample /etc/systemd/system/phoniebox-buttons-usb-encoder.service + USB_BUTTONS_SERVICE="/etc/systemd/system/phoniebox-buttons-usb-encoder.service" + sudo cp -v "${jukebox_dir}"/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample "${USB_BUTTONS_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${USB_BUTTONS_SERVICE}" sudo systemctl start phoniebox-buttons-usb-encoder.service sudo systemctl enable phoniebox-buttons-usb-encoder.service fi @@ -1402,7 +1420,7 @@ main() { else echo "Skipping USB device setup..." echo "For manual registration of a USB card reader type:" - echo "python3 /home/pi/RPi-Jukebox-RFID/scripts/RegisterDevice.py" + echo "python3 ${HOME_DIR}/RPi-Jukebox-RFID/scripts/RegisterDevice.py" echo " " echo "Reboot is required to activate all settings!" fi diff --git a/scripts/installscripts/buster-install-default.sh b/scripts/installscripts/buster-install-default.sh old mode 100755 new mode 100644 index 7bedc3122..f3cbcabf1 --- a/scripts/installscripts/buster-install-default.sh +++ b/scripts/installscripts/buster-install-default.sh @@ -4,7 +4,7 @@ # # NOTE: Running automated install (without interaction): # Each install creates a file called PhonieboxInstall.conf -# in the folder /home/pi/ +# in you $HOME directory # You can install the Phoniebox using such a config file # which means you don't need to run the interactive install: # @@ -12,7 +12,7 @@ # https://github.com/MiczFlor/RPi-Jukebox-RFID/tree/develop/scripts/installscripts # (note: currently only works for buster and newer OS) # 2. make the file executable: chmod +x -# 3. place the PhonieboxInstall.conf in the folder /home/pi/ +# 3. place the PhonieboxInstall.conf in the folder $HOME # 4. run the installscript with option -a like this: # buster-install-default.sh -a @@ -28,7 +28,7 @@ DATETIME=$(date +"%Y%m%d_%H%M%S") SCRIPTNAME="$(basename $0)" JOB="${SCRIPTNAME}" -HOME_DIR="/home/pi" +HOME_DIR=$(echo $HOME) JUKEBOX_HOME_DIR="${HOME_DIR}/RPi-Jukebox-RFID" LOGDIR="${HOME_DIR}"/phoniebox_logs @@ -104,9 +104,9 @@ will guide you through the configuration. If you want to run the AUTOMATED INSTALL (non-interactive) from an existing configuration file, do the following: 1. exit this install script (press n) -2. place your PhonieboxInstall.conf in the folder /home/pi/ +2. place your PhonieboxInstall.conf in the folder ${HOME_DIR} 3. run the installscript with option -a. For example like this: - ./home/pi/buster-install-default.sh -a + .${HOME_DIR}/buster-install-default.sh -a " read -rp "Continue interactive installation? [Y/n] " response case "$response" in @@ -545,7 +545,7 @@ config_audio_folder() { ##################################################### # Folder path for audio files - # default: /home/pi/RPi-Jukebox-RFID/shared/audiofolders + # default: $HOME/RPi-Jukebox-RFID/shared/audiofolders clear @@ -683,14 +683,17 @@ samba_config() { sudo chmod 644 "${smb_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${smb_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${smb_conf}" # Samba: create user 'pi' with password 'raspberry' + # ToDo: use current user with a default password (echo "raspberry"; echo "raspberry") | sudo smbpasswd -s -a pi } web_server_config() { local lighthttpd_conf="/etc/lighttpd/lighttpd.conf" local fastcgi_php_conf="/etc/lighttpd/conf-available/15-fastcgi-php.conf" - local php_ini="/etc/php/7.3/cgi/php.ini" + local php_ini="/etc/php/$(ls -1 /etc/php)/cgi/php.ini" local sudoers="/etc/sudoers" echo "Configuring web server..." @@ -699,6 +702,8 @@ web_server_config() { sudo cp "${jukebox_dir}"/misc/sampleconfigs/lighttpd.conf.buster-default.sample "${lighthttpd_conf}" sudo chown root:root "${lighthttpd_conf}" sudo chmod 644 "${lighthttpd_conf}" + # double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${lighthttpd_conf}" # Web server PHP7 fastcgi conf # -rw-r--r-- 1 root root 398 Apr 30 09:35 /etc/lighttpd/conf-available/15-fastcgi-php.conf @@ -781,7 +786,8 @@ install_main() { # Install required packages ${apt_get} ${allow_downgrades} install apt-transport-https - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - + sudo mkdir -p /usr/local/share/keyrings + sudo wget -q -O /usr/local/share/keyrings/mopidy-archive-keyring.gpg https://apt.mopidy.com/mopidy.gpg sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list ${apt_get} update @@ -793,7 +799,7 @@ install_main() { ${apt_get} ${allow_downgrades} install raspberrypi-kernel-headers fi - ${apt_get} ${allow_downgrades} install samba samba-common-bin gcc lighttpd php7.3-common php7.3-cgi php7.3 at mpd mpc mpg123 git ffmpeg resolvconf spi-tools netcat alsa-tools + ${apt_get} ${allow_downgrades} install samba samba-common-bin gcc lighttpd php-common php-cgi php at mpd mpc mpg123 git ffmpeg resolvconf spi-tools netcat alsa-utils lsof procps # restore backup of /etc/resolv.conf in case installation of resolvconf cleared it sudo cp /etc/resolv.conf.orig /etc/resolv.conf @@ -801,8 +807,8 @@ install_main() { # prepare python3 ${apt_get} ${allow_downgrades} install python3 python3-dev python3-pip python3-mutagen python3-gpiozero python3-spidev - # use python3.7 as default - sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1 + # use python3 as default + sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1 # Get github code cd "${HOME_DIR}" || exit @@ -828,7 +834,7 @@ install_main() { # keep major verson 3 of mopidy echo -e "Package: mopidy\nPin: version 3.*\nPin-Priority: 1001" | sudo tee /etc/apt/preferences.d/mopidy - wget -q -O - https://apt.mopidy.com/mopidy.gpg | sudo apt-key add - + sudo wget -q -O /usr/local/share/keyrings/mopidy-archive-keyring.gpg https://apt.mopidy.com/mopidy.gpg sudo wget -q -O /etc/apt/sources.list.d/mopidy.list https://apt.mopidy.com/buster.list ${apt_get} update ${apt_get} upgrade @@ -870,6 +876,8 @@ install_main() { # create config file for web app from sample sudo cp "${jukebox_dir}"/htdocs/config.php.sample "${jukebox_dir}"/htdocs/config.php + # double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${jukebox_dir}"/htdocs/config.php # Starting web server and php7 sudo lighttpd-enable-mod fastcgi @@ -901,12 +909,30 @@ install_main() { sudo rm "${systemd_dir}"/phoniebox-gpio-buttons.service echo "### Done with erasing old daemons. Stop ignoring errors!" # 2. install new ones - this is version > 1.1.8-beta - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample "${systemd_dir}"/phoniebox-rfid-reader.service + RFID_READER_SERVICE="${systemd_dir}/phoniebox-rfid-reader.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-rfid-reader.service.stretch-default.sample "${RFID_READER_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${RFID_READER_SERVICE}" + #startup sound now part of phoniebox-startup-scripts #sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-sound.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-sound.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample "${systemd_dir}"/phoniebox-startup-scripts.service - sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample "${systemd_dir}"/phoniebox-idle-watchdog.service - [[ "${GPIOconfig}" == "YES" ]] && sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-control.service.sample "${systemd_dir}"/phoniebox-gpio-control.service + STARTUP_SCRIPT_SERVICE="${systemd_dir}/phoniebox-startup-scripts.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-startup-scripts.service.stretch-default.sample "${STARTUP_SCRIPT_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${STARTUP_SCRIPT_SERVICE}" + + IDLE_WATCHDOG_SERVICE="${systemd_dir}/phoniebox-idle-watchdog.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-idle-watchdog.service.sample "${IDLE_WATCHDOG_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${IDLE_WATCHDOG_SERVICE}" + + if [[ "${GPIOconfig}" == "YES" ]]; then + GPIO_CONTROL_SERVICE="${systemd_dir}/phoniebox-gpio-control.service" + sudo cp "${jukebox_dir}"/misc/sampleconfigs/phoniebox-gpio-control.service.sample "${GPIO_CONTROL_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${GPIO_CONTROL_SERVICE}" + fi + sudo chown root:root "${systemd_dir}"/phoniebox-*.service sudo chmod 644 "${systemd_dir}"/phoniebox-*.service # enable the services needed @@ -940,12 +966,17 @@ install_main() { sudo sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${etc_mopidy_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${etc_mopidy_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${etc_mopidy_conf}" + sed -i 's/%spotify_username%/'"$SPOTIuser"'/' "${mopidy_conf}" sed -i 's/%spotify_password%/'"$SPOTIpass"'/' "${mopidy_conf}" sed -i 's/%spotify_client_id%/'"$SPOTIclientid"'/' "${mopidy_conf}" sed -i 's/%spotify_client_secret%/'"$SPOTIclientsecret"'/' "${mopidy_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mopidy_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${mopidy_conf}" fi # GPIO-Control @@ -968,6 +999,8 @@ install_main() { sudo sed -i 's/%AUDIOiFace%/'"$AUDIOiFace"'/' "${mpd_conf}" # for $DIRaudioFolders using | as alternate regex delimiter because of the folder path slash sudo sed -i 's|%DIRaudioFolders%|'"$DIRaudioFolders"'|' "${mpd_conf}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${mpd_conf}" sudo chown mpd:audio "${mpd_conf}" sudo chmod 640 "${mpd_conf}" fi @@ -1087,7 +1120,10 @@ existing_assets() { # make buttons_usb_encoder.py ready to be use from phoniebox-buttons-usb-encoder service sudo chmod +x "${jukebox_dir}"/components/controls/buttons_usb_encoder/buttons_usb_encoder.py # make sure service is still enabled by registering again - sudo cp -v "${jukebox_dir}"/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample /etc/systemd/system/phoniebox-buttons-usb-encoder.service + USB_BUTTONS_SERVICE="/etc/systemd/system/phoniebox-buttons-usb-encoder.service" + sudo cp -v "${jukebox_dir}"/components/controls/buttons_usb_encoder/phoniebox-buttons-usb-encoder.service.sample "${USB_BUTTONS_SERVICE}" + # Replace homedir; double quotes for variable expansion + sudo sed -i "s%/home/pi%${HOME_DIR}%g" "${USB_BUTTONS_SERVICE}" sudo systemctl start phoniebox-buttons-usb-encoder.service sudo systemctl enable phoniebox-buttons-usb-encoder.service fi @@ -1312,7 +1348,7 @@ main() { else echo "Skipping USB device setup..." echo "For manual registration of a USB card reader type:" - echo "python3 /home/pi/RPi-Jukebox-RFID/scripts/RegisterDevice.py" + echo "python3 ${HOME_DIR}/RPi-Jukebox-RFID/scripts/RegisterDevice.py" echo " " echo "Reboot is required to activate all settings!" fi diff --git a/scripts/installscripts/tests/run_installation_tests2_altuser.sh b/scripts/installscripts/tests/run_installation_tests2_altuser.sh new file mode 100644 index 000000000..03bce2af7 --- /dev/null +++ b/scripts/installscripts/tests/run_installation_tests2_altuser.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: Test installation with script using a RC522 reader + +# Print current path +echo $PWD + +# Preparations +# Skip interactive Samba WINS config dialog +echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections +# No interactive frontend +export DEBIAN_FRONTEND=noninteractive + +# Run installation (in interactive mode) +# y confirm interactive +# n dont configure wifi +# y PCM as iface +# n no spotify +# y configure mpd +# y audio default location +# y use gpio +# y RFID registration +# 2 use RC522 reader +# yes, reader is connected +# n No reboot + +# TODO check, how this behaves on branches other than develop +GIT_BRANCH=develop bash ./scripts/installscripts/buster-install-default.sh <<< $'y\nn\n\ny\n\nn\n\ny\n\ny\n\ny\n\ny\ny\n2\ny\nn\n' + +# Test installation +./scripts/installscripts/tests/test_installation_altuser.sh diff --git a/scripts/installscripts/tests/run_installation_tests3_altuser.sh b/scripts/installscripts/tests/run_installation_tests3_altuser.sh new file mode 100644 index 000000000..2db9b991f --- /dev/null +++ b/scripts/installscripts/tests/run_installation_tests3_altuser.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: Test installation with script using a configuration with mopidy + +# Print current path +echo $PWD + +# Preparations +# Skip interactive Samba WINS config dialog +echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections +# No interactive frontend +export DEBIAN_FRONTEND=noninteractive + +# Run installation (in interactive mode) +# y confirm interactive +# n dont configure wifi +# y Headphone as iface +# y spotify with myuser, mypassword, myclient_id, myclient_secret +# y configure mpd +# y audio default location +# y config gpio +# n no RFID registration +# n No reboot + +# TODO check, how this behaves on branches other than develop +GIT_BRANCH=develop bash ./scripts/installscripts/buster-install-default.sh <<< $'y\nn\n\ny\n\ny\nmyuser\nmypassword\nmyclient_id\nmyclient_secret\n\ny\n\ny\n\ny\n\ny\nn\nn\n' + +# Test installation +./scripts/installscripts/tests/test_installation_altuser.sh diff --git a/scripts/installscripts/tests/run_installation_tests_altuser.sh b/scripts/installscripts/tests/run_installation_tests_altuser.sh new file mode 100644 index 000000000..d06aac742 --- /dev/null +++ b/scripts/installscripts/tests/run_installation_tests_altuser.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: Test installation with script using a simple configuration + +# Print current path +echo $PWD + +# Preparations +# Skip interactive Samba WINS config dialog +echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections +# No interactive frontend +export DEBIAN_FRONTEND=noninteractive + +# Run installation (in interactive mode) +# y confirm interactive +# n dont configure wifi +# y Headphone as iface +# n no spotify +# y configure mpd +# y audio default location +# y config gpio +# n no RFID registration +# n No reboot + +# TODO check, how this behaves on branches other than develop +GIT_BRANCH=develop bash ./scripts/installscripts/buster-install-default.sh <<< $'y\nn\n\ny\n\nn\n\ny\n\ny\n\ny\n\ny\nn\nn\n' + +# Test installation +./scripts/installscripts/tests/test_installation_altuser.sh diff --git a/scripts/installscripts/tests/test_installation.sh b/scripts/installscripts/tests/test_installation.sh index 2b48b21e2..dd8e5a9ce 100755 --- a/scripts/installscripts/tests/test_installation.sh +++ b/scripts/installscripts/tests/test_installation.sh @@ -155,7 +155,7 @@ verify_apt_packages(){ local packages="libspotify-dev samba samba-common-bin gcc lighttpd php7.3-common php7.3-cgi php7.3 at mpd mpc mpg123 git ffmpeg resolvconf spi-tools python3 python3-dev python3-pip python3-mutagen python3-gpiozero -python3-spidev netcat alsa-tools" +python3-spidev netcat alsa-utils" # TODO apt-transport-https checking only on RPi is currently a workaround local packages_raspberrypi="apt-transport-https raspberrypi-kernel-headers" local packages_spotify="mopidy mopidy-mpd mopidy-local mopidy-spotify libspotify12 diff --git a/scripts/installscripts/tests/test_installation_altuser.sh b/scripts/installscripts/tests/test_installation_altuser.sh new file mode 100755 index 000000000..de61a2f0b --- /dev/null +++ b/scripts/installscripts/tests/test_installation_altuser.sh @@ -0,0 +1,349 @@ +#!/usr/bin/env bash + +# Test to verify that the installation script works as expected. +# This script needs to be adapted, if new packages, etc are added to the install script + +# The absolute path to the folder which contains this script +PATHDATA="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +HOME_DIR="/home/hans" + +tests=0 +failed_tests=0 + +# Tool functions + +check_chmod_chown() { + local mod_expected=$1 + local user_expected=$2 + local group_expected=$3 + local dir=$4 + local files=$5 + + for file in ${files}; + do + mod_actual=$(stat --format '%a' "${dir}/${file}") + user_actual=$(stat -c '%U' "${dir}/${file}") + group_actual=$(stat -c '%G' "${dir}/${file}") + test ! "${mod_expected}" -eq "${mod_actual}" && echo " ERROR: ${file} actual mod (${mod_actual}) differs from expected (${mod_expected})!" + test ! "${user_expected}" == "${user_actual}" && echo " ERROR: ${file} actual owner (${user_actual}) differs from expected (${user_expected})!" + test ! "${group_expected}" == "${group_actual}" && echo " ERROR: ${file} actual group (${group_actual}) differs from expected (${group_expected})!" + done +} + +check_file_contains_string() { + local string="$1" + local file="$2" + + # sudo is required for checking /etc/mopidy/mopidy.conf + if [[ ! $(sudo grep -iw "${string}" "${file}") ]]; then + echo " ERROR: '${string}' not found in ${file}" + ((failed_tests++)) + fi + ((tests++)) +} + +check_service_state() { + local service="$1" + local desired_state="$2" + + local actual_state=$(systemctl show -p ActiveState --value "${service}") + if [[ ! "${actual_state}" == "${desired_state}" ]]; then + echo " ERROR: service ${service} is not ${desired_state} (state: ${actual_state})." + ((failed_tests++)) + fi + ((tests++)) +} + +check_service_enablement() { + local service="$1" + local desired_enablement="$2" + + local actual_enablement=$(systemctl is-enabled "${service}") + if [[ ! "${actual_enablement}" == "${desired_enablement}" ]]; then + echo " ERROR: service ${service} is not ${desired_enablement} (state: ${actual_enablement})." + ((failed_tests++)) + fi + ((tests++)) +} + +check_variable() { + local variable=${1} + # check if variable exist and if it's empty + test -z "${!variable+x}" && echo "ERROR: \$${variable} is missing!" && fail=true && return + test "${!variable}" == "" && echo "ERROR: \$${variable} is empty!" && fail=true +} + +# Verify functions + +verify_conf_file() { + local install_conf="${HOME_DIR}/PhonieboxInstall.conf" + printf "\nTESTING PhonieboxInstall.conf file...\n\n" + # check that PhonieboxInstall.conf exists and is not empty + + # check if config file exists + if [[ -f "${install_conf}" ]]; then + # Source config file + source "${install_conf}" + cat "${install_conf}" + echo "" + else + echo "ERROR: ${install_conf} does not exist!" + exit 1 + fi + + fail=false + if [[ -z "${WIFIconfig+x}" ]]; then + echo " ERROR: \$WIFIconfig is missing or not set!" && fail=true + else + echo "\$WIFIconfig is set to '$WIFIconfig'" + if [[ "$WIFIconfig" == "YES" ]]; then + check_variable "WIFIcountryCode" + check_variable "WIFIssid" + check_variable "WIFIpass" + check_variable "WIFIip" + check_variable "WIFIipRouter" + fi + fi + check_variable "EXISTINGuse" + check_variable "AUDIOiFace" + + if [[ -z "${SPOTinstall+x}" ]]; then + echo " ERROR: \$SPOTinstall is missing or not set!" && fail=true + else + echo "\$SPOTinstall is set to '$SPOTinstall'" + if [ "$SPOTinstall" == "YES" ]; then + check_variable "SPOTIuser" + check_variable "SPOTIpass" + check_variable "SPOTIclientid" + check_variable "SPOTIclientsecret" + fi + fi + check_variable "MPDconfig" + check_variable "DIRaudioFolders" + + if [ "${fail}" == "true" ]; then + exit 1 + fi + + echo "" +} + +verify_wifi_settings() { + local dhcpcd_conf="/etc/dhcpcd.conf" + local wpa_supplicant_conf="/etc/wpa_supplicant/wpa_supplicant.conf" + printf "\nTESTING WiFi settings...\n" + + # check conf files + check_file_contains_string "static ip_address=${WIFIip}/24" "${dhcpcd_conf}" + check_file_contains_string "static routers=${WIFIipRouter}" "${dhcpcd_conf}" + check_file_contains_string "static domain_name_servers=8.8.8.8 ${WIFIipRouter}" "${dhcpcd_conf}" + + check_file_contains_string "country=${WIFIcountryCode}" "${wpa_supplicant_conf}" + check_file_contains_string "ssid=\"${WIFIssid}\"" "${wpa_supplicant_conf}" + check_file_contains_string "psk=\"${WIFIpass}\"" "${wpa_supplicant_conf}" + + # check owner and permissions + check_chmod_chown 664 root netdev "/etc" "dhcpcd.conf" + check_chmod_chown 664 root netdev "/etc/wpa_supplicant" "wpa_supplicant.conf" + + # check that dhcpcd service is enabled and started + check_service_state dhcpcd active + check_service_enablement dhcpcd enabled +} + +verify_apt_packages(){ + local packages="libspotify-dev samba +samba-common-bin gcc lighttpd php7.3-common php7.3-cgi php7.3 at mpd mpc mpg123 git ffmpeg +resolvconf spi-tools python3 python3-dev python3-pip python3-mutagen python3-gpiozero +python3-spidev netcat alsa-utils" + # TODO apt-transport-https checking only on RPi is currently a workaround + local packages_raspberrypi="apt-transport-https raspberrypi-kernel-headers" + local packages_spotify="mopidy mopidy-mpd mopidy-local mopidy-spotify libspotify12 +python3-cffi python3-ply python3-pycparser python3-spotify" + + printf "\nTESTING installed packages...\n\n" + + # also check for spotify packages if it has been installed + if [[ "${SPOTinstall}" == "YES" ]]; then + packages="${packages} ${packages_spotify}" + fi + + # check for raspberry pi packages only on raspberry pi's but not on test docker containers running on x86_64 machines + if [[ $(uname -m) =~ ^armv.+$ ]]; then + packages="${packages} ${packages_raspberrypi}" + fi + + for package in ${packages} + do + if [[ $(apt -qq list "${package}" 2>/dev/null | grep 'installed') ]]; then + echo " ${package} is installed" + else + echo " ERROR: ${package} is not installed" + ((failed_tests++)) + fi + ((tests++)) + done +} + +verify_pip_packages() { + local modules="evdev spi-py youtube_dl pyserial RPi.GPIO" + local modules_spotify="Mopidy-Iris" + local modules_pn532="py532lib" + local modules_rc522="pi-rc522" + local deviceName="${JUKEBOX_HOME_DIR}"/scripts/deviceName.txt + + printf "\nTESTING installed pip modules...\n\n" + + # also check for spotify pip modules if it has been installed + if [[ "${SPOTinstall}" == "YES" ]]; then + modules="${modules} ${modules_spotify}" + fi + + if [[ -f "${deviceName}" ]]; then + # RC522 reader is used + if grep -Fxq "${deviceName}" MFRC522 + then + modules="${modules} ${modules_rc522}" + fi + + # PN532 reader is used + if grep -Fxq "${deviceName}" PN532 + then + modules="${modules} ${modules_pn532}" + fi + fi + + for module in ${modules} + do + if [[ $(pip3 show "${module}") ]]; then + echo " ${module} is installed" + else + echo " ERROR: pip module ${module} is not installed" + ((failed_tests++)) + fi + ((tests++)) + done +} + +verify_samba_config() { + printf "\nTESTING samba config...\n\n" + check_chmod_chown 644 root root "/etc/samba" "smb.conf" + + check_file_contains_string "path=${DIRaudioFolders}" "/etc/samba/smb.conf" +} + +verify_webserver_config() { + printf "\nTESTING webserver config...\n\n" + check_chmod_chown 644 root root "/etc/lighttpd" "lighttpd.conf" + check_chmod_chown 644 root root "/etc/lighttpd/conf-available" "15-fastcgi-php.conf" + check_chmod_chown 644 root root "/etc/php/7.3/cgi" "php.ini" + check_chmod_chown 440 root root "/etc" "sudoers" + + # Bonus TODO: check that fastcgi and fastcgi-php mods are enabled +} + +verify_systemd_services() { + printf "\nTESTING systemd services...\n\n" + # check that services exist + check_chmod_chown 644 root root "/etc/systemd/system" "phoniebox-rfid-reader.service phoniebox-startup-scripts.service phoniebox-gpio-control.service phoniebox-idle-watchdog.service" + + # check that phoniebox services are enabled + check_service_enablement phoniebox-idle-watchdog enabled + check_service_enablement phoniebox-rfid-reader enabled + check_service_enablement phoniebox-startup-scripts enabled + check_service_enablement phoniebox-gpio-control enabled +} + +verify_spotify_config() { + local etc_mopidy_conf="/etc/mopidy/mopidy.conf" + local mopidy_conf="${HOME_DIR}/.config/mopidy/mopidy.conf" + + printf "\nTESTING spotify config...\n\n" + + check_file_contains_string "username = ${SPOTIuser}" "${etc_mopidy_conf}" + check_file_contains_string "password = ${SPOTIpass}" "${etc_mopidy_conf}" + check_file_contains_string "client_id = ${SPOTIclientid}" "${etc_mopidy_conf}" + check_file_contains_string "client_secret = ${SPOTIclientsecret}" "${etc_mopidy_conf}" + check_file_contains_string "media_dir = ${DIRaudioFolders}" "${etc_mopidy_conf}" + + check_file_contains_string "username = ${SPOTIuser}" "${mopidy_conf}" + check_file_contains_string "password = ${SPOTIpass}" "${mopidy_conf}" + check_file_contains_string "client_id = ${SPOTIclientid}" "${mopidy_conf}" + check_file_contains_string "client_secret = ${SPOTIclientsecret}" "${mopidy_conf}" + check_file_contains_string "media_dir = ${DIRaudioFolders}" "${mopidy_conf}" + + # check that mopidy service is enabled + check_service_enablement mopidy enabled + # check that mpd service is disabled + check_service_enablement mpd disabled +} + +verify_mpd_config() { + local mpd_conf="/etc/mpd.conf" + + printf "\nTESTING mpd config...\n\n" + + check_file_contains_string "^[[:blank:]]\+mixer_control[[:blank:]]\+\"${AUDIOiFace}\"" "${mpd_conf}" + check_file_contains_string "^music_directory[[:blank:]]\+\"${DIRaudioFolders}\"" "${mpd_conf}" + + check_chmod_chown 640 mpd audio "/etc" "mpd.conf" + + # check that mpd service is enabled, when Spotify support is not installed + if [[ "${SPOTinstall}" == "NO" ]]; then + check_service_enablement mpd enabled + fi +} + +verify_folder_access() { + local jukebox_dir="${HOME_DIR}/RPi-Jukebox-RFID" + printf "\nTESTING folder access...\n\n" + + # check owner and permissions + check_chmod_chown 775 hans www-data "${jukebox_dir}" "playlists shared htdocs settings" + # ${DIRaudioFolders} => "testing" "audiofolders" + check_chmod_chown 775 hans www-data "${DIRaudioFolders}/.." "audiofolders" + + #find .sh and .py scripts that are NOT executable + local count=$(find . -maxdepth 1 -type f \( -name "*.sh" -o -name "*.py" \) ! -executable | wc -l) + if [[ "${count}" -gt 0 ]]; then + echo " ERROR: found ${count} '*.sh' and/or '*.py' files that are NOT executable:" + find . -maxdepth 1 -type f \( -name "*.sh" -o -name "*.py" \) ! -executable + ((failed_tests++)) + fi + ((tests++)) +} + +main() { + printf "\nTesting installation:\n" + verify_conf_file + if [[ "$WIFIconfig" == "YES" ]]; then + verify_wifi_settings + fi + verify_apt_packages + verify_pip_packages + verify_samba_config + verify_webserver_config + verify_systemd_services + if [[ "${SPOTinstall}" == "YES" ]]; then + verify_spotify_config + fi + verify_mpd_config + verify_folder_access +} + +start=$(date +%s) +main +end=$(date +%s) + +runtime=$((end-start)) +((h=${runtime}/3600)) +((m=($runtime%3600)/60)) +((s=$runtime%60)) + +if [[ "${failed_tests}" -gt 0 ]]; then + echo "${failed_tests} Test(s) failed (of ${tests} tests) (in ${h}h ${m}m ${s}s)." + exit 1 +else + echo "${tests} tests done in ${h}h ${m}m ${s}s." +fi + diff --git a/scripts/playlist_recursive_by_folder.php b/scripts/playlist_recursive_by_folder.php index 8d62de3ad..e8784ab46 100755 --- a/scripts/playlist_recursive_by_folder.php +++ b/scripts/playlist_recursive_by_folder.php @@ -115,9 +115,16 @@ * Read podcast URL and extract audio links from enclosure tag */ $podcast = trim(file_get_contents($folder."/podcast.txt")); - //wget -q -O - "http://www.kakadu.de/podcast-kakadu.2730.de.podcast.xml" | sed -n 's/.*enclosure.*url="\([^"]*\)".*/\1/p' - //wget -q -O - "https://www1.wdr.de/mediathek/audio/hoerspiel-speicher/wdr_hoerspielspeicher150.podcast" | sed -n 's/.*enclosure.*url="\([^"]*\)".*/\1/p' - $exec = 'wget -q -O - \''.$podcast.'\' | sed -n \'s/.*enclosure.*url="\([^"]*\)".*/\1/p\''; + //wget -q -O - "http://www.kakadu.de/podcast-kakadu.2730.de.podcast.xml" | tr '\n' ' ' | sed -e 's/\/>/\/>\n&/g' | sed -n 's/.*enclosure.*url="\([^"]*\)".*/\1/p' + //wget -q -O - "https://www1.wdr.de/mediathek/audio/hoerspiel-speicher/wdr_hoerspielspeicher150.podcast" | tr '\n' ' ' | sed -e 's/\/>/\/>\n&/g' | sed -n 's/.*enclosure.*url="\([^"]*\)".*/\1/p' + //wget -q -O - "https://www.swr.de/~podcast/swr2/leben-und-gesellschaft/podcast-sprechen-wir-ueber-mord-100.xml" | tr '\n' ' ' | sed -e 's/\/>/\/>\n&/g' | sed -n 's/.*enclosure.*url="\([^"]*\)".*/\1/p' + // includes fix if enclosure tags are divided in multiple lines + // 1. "wget" the URL + // 2. remove all the line breaks + // 3. add breaks at the end of every tag that, + // 4. sed (which works line by line) has the proper environment + $exec = 'wget -q -O - \''.$podcast.'\' | tr \'\n\' \' \' | sed -e \'s/\/>/\/>\n&/g\' | sed -n \'s/.*enclosure.*url="\([^"]*\)".*/\1/p\''; + /* * get all the playlist enclosure URLs in a multiline string */ diff --git a/scripts/playout_controls.sh b/scripts/playout_controls.sh index 11517a065..0bc6a3a67 100755 --- a/scripts/playout_controls.sh +++ b/scripts/playout_controls.sh @@ -31,6 +31,9 @@ NOW=`date +%Y-%m-%d.%H:%M:%S` # setstartupvolume # getstartupvolume # setvolumetostartup +# setbootvolume +# getbootvolume +# setvolumetobootvolume # volumeup # volumedown # getchapters @@ -68,6 +71,8 @@ NOW=`date +%Y-%m-%d.%H:%M:%S` # recordstop # recordplaylatest # readwifiipoverspeaker +# bluetoothtoggle +# switchaudioiface # The absolute path to the folder which contains all the scripts. # Unless you are working with symlinks, leave the following line untouched. @@ -95,6 +100,9 @@ fi # it will be created or deleted by this script VOLFILE=${PATHDATA}/../settings/Audio_Volume_Level +# path to file storing the current audio iFace name +IFACEFILE=${PATHDATA}/../settings/Audio_iFace_Name + ############################################################# # Get args from command line (see Usage above) @@ -159,7 +167,7 @@ then dbg "chapters for extension enabled: $CHAPTER_SUPPORT_FOR_EXTENSION" - if [ "$(printf "${CURRENT_SONG_DURATION}\n${CHAPTERMINDURATION}\n" | sort -g | head -1)" == "${CHAPTERMINDURATION}" ]; then + if [ "$(printf "${CURRENT_SONG_DURATION}\n${CHAPTERMINDURATION}\n" | sort -g | head -n1)" == "${CHAPTERMINDURATION}" ]; then CHAPTER_SUPPORT_FOR_DURATION="1" else CHAPTER_SUPPORT_FOR_DURATION="0" @@ -588,6 +596,39 @@ case $COMMAND in echo -e setvol ${AUDIOVOLSTARTUP}\\nclose | nc -w 1 localhost 6600 fi + fi + ;; + setbootvolume) + if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi + # if value is greater than wanted maxvolume, set value to maxvolume + if [ ${VALUE} -gt $AUDIOVOLMAXLIMIT ]; + then + VALUE=$AUDIOVOLMAXLIMIT; + fi + # write new value to file + echo "$VALUE" > ${PATHDATA}/../settings/Volume_Boot + # create global config file because individual setting got changed + . ${PATHDATA}/inc.writeGlobalConfig.sh + ;; + getbootvolume) + if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi + echo ${AUDIOVOLBOOT} + ;; + setvolumetobootvolume) + if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi + # check if startup-volume is disabled + if [ "${AUDIOVOLBOOT}" == 0 ]; then + exit 1 + else + # set volume level in percent + if [ "${VOLUMEMANAGER}" == "amixer" ]; then + # volume handling alternative with amixer not mpd (2020-06-12 related to ticket #973) + amixer sset \'$AUDIOIFACENAME\' ${AUDIOVOLBOOT}% + else + # manage volume with mpd + echo -e setvol ${AUDIOVOLBOOT}\\nclose | nc -w 1 localhost 6600 + fi + fi ;; playerstop) @@ -703,7 +744,13 @@ case $COMMAND in then /bin/sleep $VALUE fi - mpc pause + PLAYSTATE=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=state: ).*') + # Only pause when currently playing + # Otherwise mpc might go from "stopped" back to "pause", causing inconsitency as there is nothing to "resume" + if [ "$PLAYSTATE" == "play" ] + then + mpc pause + fi ;; playerplay) # play / resume current track @@ -907,7 +954,7 @@ case $COMMAND in ;; playlistappend) if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi - mpc add "${VALUE}" + mpc add file://"${VALUE}" # Unmute if muted if [ -f $VOLFILE ]; then # $VOLFILE DOES exist == audio off @@ -928,7 +975,7 @@ case $COMMAND in playsinglefile) if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND} value:${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi mpc clear - mpc add "${VALUE}" + mpc add file://"${VALUE}" mpc repeat off mpc single on # Unmute if muted @@ -983,6 +1030,39 @@ case $COMMAND in rfkill unblock wifi fi ;; + randomcard) + #activate a random card + NUM_CARDS=$(find $AUDIO_FOLDERS_PATH/../shortcuts/ -maxdepth 1 -name '[0-9]*' | wc -l) + dbg "NUM_CARDS: $NUM_CARDS" + if (($NUM_CARDS>0)) + then + RANDOMCARDID=$(ls -d $AUDIO_FOLDERS_PATH/../shortcuts/[0-9]* | shuf -n 1 | xargs basename) + dbg "playing random card $RANDOMCARDID" + ${PATHDATA}/rfid_trigger_play.sh --cardid=$RANDOMCARDID + fi + ;; + randomfolder) + #play a random folder + NUM_FOLDERS=$(find $AUDIO_FOLDERS_PATH -mindepth 1 -maxdepth 1 -type d -printf '1' | wc -c) + dbg "NUM_FOLDERS: $NUM_FOLDERS" + if (($NUM_FOLDERS>0)) + then + RANDOMFOLDER=$(ls -d $AUDIO_FOLDERS_PATH/*/ | shuf -n 1 | xargs -d '\n' basename) + dbg "playing random folder \"$RANDOMFOLDER\"" + ${PATHDATA}/rfid_trigger_play.sh --dir="$RANDOMFOLDER" + fi + ;; + randomtrack) + #jump to a random track from the current playlist (without activating shuffle, i.e. maintaining track order) + NUM_TRACKS=$(echo -e "status\nclose" | nc -w 1 localhost 6600 | grep -o -P '(?<=playlistlength: ).*') + dbg "NUM_TRACKS: $NUM_TRACKS" + if(($NUM_TRACKS > 0)) + then + RANDOMTRACK=$(((RANDOM%${NUM_TRACKS})+1)) + dbg "playing random track $RANDOMTRACK" + mpc play ${RANDOMTRACK} + fi + ;; recordstart) #mkdir $AUDIOFOLDERSPATH/Recordings #kill the potential current playback @@ -1012,7 +1092,7 @@ case $COMMAND in # delete $VOLFILE rm -f $VOLFILE fi - aplay `ls $AUDIOFOLDERSPATH/Recordings/*.wav -1t|head -1` + aplay `ls $AUDIOFOLDERSPATH/Recordings/*.wav -1t|head -n1` ;; readwifiipoverspeaker) # will read out the IP address over the Pi's speaker. @@ -1022,6 +1102,40 @@ case $COMMAND in sudo rm WifiIp.mp3 /usr/bin/php /home/pi/RPi-Jukebox-RFID/scripts/helperscripts/cli_ReadWifiIp.php ;; + bluetoothtoggle) + if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo " ${COMMAND}" >> ${PATHDATA}/../logs/debug.log; fi + $PATHDATA/../components/bluetooth-sink-switch/bt-sink-switch.py $VALUE + ;; + switchaudioiface) + # will switch between primary/secondary audio iFace (e.g. speaker/headphones), if exist + dbg " ${COMMAND}" + if [ "${VOLUMEMANAGER}" == "amixer" ]; then + NEXTAUDIOIFACE=$(((${AUDIOIFACEACTIVE}+1) % 2)) + if [ -f ${IFACEFILE}_${NEXTAUDIOIFACE} ]; then + NEXTAUDIOIFACENAME=`<${IFACEFILE}_${NEXTAUDIOIFACE}` + if [ -f ${VOLFILE}_${NEXTAUDIOIFACE} ]; then + NEXTAUDIOIFACEVOL=`<${VOLFILE}_${NEXTAUDIOIFACE}` + else + NEXTAUDIOIFACEVOL=${AUDIOVOLMAXLIMIT} + fi + # store current volume + amixer sget \'${AUDIOIFACENAME}\' | grep -Po -m 1 '(?<=\[)[^]]*(?=%])' > ${VOLFILE}_${AUDIOIFACEACTIVE} + # unmute next audio iFace + amixer sset \'${NEXTAUDIOIFACENAME}\' ${NEXTAUDIOIFACEVOL}% + # mute current audio iFace + amixer sset \'${AUDIOIFACENAME}\' 0% + # store new active audio iFace + cp ${IFACEFILE}_${NEXTAUDIOIFACE} ${IFACEFILE} + echo "${NEXTAUDIOIFACE}" > ${PATHDATA}/../settings/Audio_iFace_Active + # create global config file because individual setting got changed (time consuming) + . ${PATHDATA}/inc.writeGlobalConfig.sh + else + dbg "Cannot switch audio iFace. ${IFACEFILE}_${NEXTAUDIOIFACE} does not exist." + fi + else + dbg "Command requires \"amixer\" as volume manager." + fi + ;; *) echo Unknown COMMAND $COMMAND VALUE $VALUE if [ "${DEBUG_playout_controls_sh}" == "TRUE" ]; then echo "Unknown COMMAND ${COMMAND} VALUE ${VALUE}" >> ${PATHDATA}/../logs/debug.log; fi diff --git a/scripts/rfid_trigger_play.sh b/scripts/rfid_trigger_play.sh index e96902eb7..b1bc08ede 100755 --- a/scripts/rfid_trigger_play.sh +++ b/scripts/rfid_trigger_play.sh @@ -160,6 +160,10 @@ if [ "$CARDID" ]; then # decrease volume by x% set in Audio_Volume_Change_Step $PATHDATA/playout_controls.sh -c=volumedown ;; + $CMDSWITCHAUDIOIFACE) + # switch between primary/secondary audio iFaces + $PATHDATA/playout_controls.sh -c=switchaudioiface + ;; $CMDSTOP) # kill all running audio players $PATHDATA/playout_controls.sh -c=playerstop @@ -184,6 +188,18 @@ if [ "$CARDID" ]; then sudo $PATHDATA/playout_controls.sh -c=playerprev #/usr/bin/sudo /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=playerprev ;; + $CMDRANDCARD) + # activate a random card + $PATHDATA/playout_controls.sh -c=randomcard + ;; + $CMDRANDFOLD) + # play a random folder + $PATHDATA/playout_controls.sh -c=randomfolder + ;; + $CMDRANDTRACK) + # jump to a random track in playlist (no shuffle mode required) + $PATHDATA/playout_controls.sh -c=randomtrack + ;; $CMDREWIND) # play the first track in playlist sudo $PATHDATA/playout_controls.sh -c=playerrewind @@ -287,6 +303,9 @@ if [ "$CARDID" ]; then $CMDREADWIFIIP) $PATHDATA/playout_controls.sh -c=readwifiipoverspeaker ;; + $CMDBLUETOOTHTOGGLE) + $PATHDATA/playout_controls.sh -c=bluetoothtoggle -v=toggle + ;; *) # We checked if the card was a special command, seems it wasn't. diff --git a/scripts/startup-scripts.sh b/scripts/startup-scripts.sh index 503c165cf..94b1c6a66 100755 --- a/scripts/startup-scripts.sh +++ b/scripts/startup-scripts.sh @@ -34,12 +34,21 @@ STATUS=0 while [ "$STATUS" != "ACTIVE" ]; do STATUS=$(echo -e status\\nclose | nc -w 1 localhost 6600 | grep 'OK MPD'| sed 's/^.*$/ACTIVE/'); done #################################### -# check if and set volume on startup +# Set volume levels on boot +# Both can be set in the Web UI under settings +# Confusion explained: +# 1. Set the volume to the global boot level. +# Sometimes the Pi sets volume to 0. +# The first command makes sure there is *some* level, not 0. +# 2. Then set the volume to the startup volume. +# If the kids crank up the volume at night, +# after a reboot, the box will be back to this level. +/home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=setvolumetobootvolume /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=setvolumetostartup #################### # play startup sound -mpgvolume=$((32768*${AUDIOVOLSTARTUP}/100)) +mpgvolume=$((32768*${AUDIOVOLBOOT}/100)) echo "${mpgvolume} is the mpg123 startup volume" /usr/bin/mpg123 -f -${mpgvolume} /home/pi/RPi-Jukebox-RFID/shared/startupsound.mp3 @@ -52,3 +61,15 @@ mpc rescan if [ "${READWLANIPYN}" == "ON" ]; then /home/pi/RPi-Jukebox-RFID/scripts/playout_controls.sh -c=readwifiipoverspeaker fi + +####################### +# Default audio output to speakers (instead of bluetooth device) irrespective of setting at shutdown +if [ -f $PATHDATA/../settings/bluetooth-sink-switch ]; then + BTSINKSWITCH=`cat $PATHDATA/../settings/bluetooth-sink-switch` + if [ "${BTSINKSWITCH}" == "enabled" ]; then + $PATHDATA/../components/bluetooth-sink-switch/bt-sink-switch.py speakers + fi +fi + + + diff --git a/settings/rfid_trigger_play.conf.sample b/settings/rfid_trigger_play.conf.sample index c983823d8..9b99fa7a2 100755 --- a/settings/rfid_trigger_play.conf.sample +++ b/settings/rfid_trigger_play.conf.sample @@ -29,6 +29,8 @@ CMDVOLUP="%CMDVOLUP%" ### Volume down by one step CMDVOLDOWN="%CMDVOLDOWN%" +### Switch between primary/secondary audio iFace +CMDSWITCHAUDIOIFACE="%CMDSWITCHAUDIOIFACE%" ### Stop player CMDSTOP="%CMDSTOP%" ### Mute player @@ -37,6 +39,12 @@ CMDMUTE="%CMDMUTE%" CMDNEXT="%CMDNEXT%" ### Skip previous track CMDPREV="%CMDPREV%" +### Activate random card +CMDRANDCARD="%CMDRANDCARD%" +### Play random folder +CMDRANDFOLD="%CMDRANDFOLD%" +### Jump to random track +CMDRANDTRACK="%CMDRANDTRACK%" ### Restart the playlist CMDREWIND="%CMDREWIND%" ### Seek ahead 15 sec. @@ -47,6 +55,8 @@ CMDSEEKBACK="%CMDSEEKBACK%" CMDPAUSE="%CMDPAUSE%" ### Resume audio playout CMDPLAY="%CMDPLAY%" +### Toggle between speakers and bluetooth headphones +CMDBLUETOOTHTOGGLE="%CMDBLUETOOTHTOGGLE%" ## System ### Shutdown diff --git a/settings/version-number b/settings/version-number old mode 100755 new mode 100644 index be1c36e58..6b4950e3d --- a/settings/version-number +++ b/settings/version-number @@ -1 +1 @@ -2.3rc1 +2.4