diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..d057a8b57 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = test/* diff --git a/.dockerignore b/.dockerignore index 55fe62a70..a587de477 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ # Application docker docs -installation shared # webapp diff --git a/.flake8 b/.flake8 index d11edbe8a..0c2cd276e 100644 --- a/.flake8 +++ b/.flake8 @@ -15,7 +15,7 @@ per-file-ignores = count = True max-complexity = 12 statistics = True -filename = *.py,*.py.* +filename = *.py extend-exclude = # Ignore all scratch development directories scratch*, diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md index 1be942778..508cf50b9 100644 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -50,7 +50,7 @@ Otherwise the output of `cat /etc/os-release` i.e. `master` the following command will help with that -`cd /home/pi/RPi-Jukebox-RFID/ && git status | head -2` +`cd ~/RPi-Jukebox-RFID/ && git status | head -2` --> ### Installscript diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index 48e27b15d..f2174d0c0 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -6,13 +6,11 @@ on: - 'future3/**' paths: - '**.py' - - '**.py.*' pull_request: branches: - 'future3/**' paths: - '**.py' - - '**.py.*' jobs: build: @@ -21,7 +19,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v3 @@ -41,8 +39,27 @@ jobs: pip3 install -r src/jukebox/components/rfid/hardware/pn532_i2c_py532/requirements.txt pip3 install -r src/jukebox/components/rfid/hardware/rdm6300_serial/requirements.txt pip3 install -r src/jukebox/components/rfid/hardware/rc522_spi/requirements.txt + - name: Run pytest with coverage + run: | + ./run_pytest.sh --cov --cov-report xml --cov-config=.coveragerc + - name: Report to Coveralls (parallel) + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + file: coverage.xml + format: cobertura + parallel: true - name: Lint with flake8 run: | pip3 install flake8 # Stop the build if linting fails ./run_flake8.sh + + finish: + needs: build + runs-on: ubuntu-latest + steps: + - name: Close parallel build + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true diff --git a/.github/workflows/test_docker_debian_codename_sub_v3.yml b/.github/workflows/test_docker_debian_codename_sub_v3.yml new file mode 100644 index 000000000..dde60cb2a --- /dev/null +++ b/.github/workflows/test_docker_debian_codename_sub_v3.yml @@ -0,0 +1,186 @@ +name: Subworkflow Test Install Scripts Debian V3 + +on: + workflow_call: + inputs: + debian_codename: + required: true + type: string + platform: + required: true + type: string + docker_image_name: + required: false + type: string + default: rpi-jukebox-rfid-v3 + cache_scope: + required: false + type: string + default: ${{ github.ref }}-test-debian-v3 + local_registry_port: + required: false + type: number + default: 5000 + runs_on: + required: false + type: string + default: ubuntu-latest + +env: + TEST_USER_NAME: testuser + TEST_USER_GROUP: testusergroup + +# let only one instance run the test so cache is not corrupted. +# cancel already running instances as only the last run will be relevant +concurrency: + group: ${{ inputs.cache_scope }}-${{ inputs.debian_codename }}-${{ inputs.platform }} + cancel-in-progress: true + +jobs: + + # Build container for test execution + build: + runs-on: ${{ inputs.runs_on }} + + outputs: + cache_key: ${{ steps.vars.outputs.cache_key }} + image_file_name: ${{ steps.vars.outputs.image_file_name }} + image_tag_name: ${{ steps.vars.outputs.image_tag_name }} + + # create local docker registry to use locally build images + services: + registry: + image: registry:2 + ports: + - ${{ inputs.local_registry_port }}:5000 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + with: + # network=host driver-opt needed to push to local registry + driver-opts: network=host + + - name: Set Output pre-vars + id: pre-vars + env: + DEBIAN_CODENAME: ${{ inputs.debian_codename }} + DOCKER_IMAGE_NAME: ${{ inputs.docker_image_name }} + CACHE_SCOPE: ${{ inputs.cache_scope }} + PLATFORM: ${{ inputs.platform }} + run: | + PLATFORM=${PLATFORM////_} + echo "image_tag_name=${{ env.DOCKER_IMAGE_NAME }}:${{ env.DEBIAN_CODENAME }}-${PLATFORM}-test" >> $GITHUB_OUTPUT + echo "image_file_name=${{ env.DOCKER_IMAGE_NAME }}-${{ env.DEBIAN_CODENAME }}-${PLATFORM}.tar" >> $GITHUB_OUTPUT + echo "cache_scope=${{ env.CACHE_SCOPE }}-${{ env.DEBIAN_CODENAME }}-${PLATFORM}" >> $GITHUB_OUTPUT + + - name: Set Output vars + id: vars + env: + LOCAL_REGISTRY_PORT: ${{ inputs.local_registry_port }} + run: | + echo "image_tag_name=${{ steps.pre-vars.outputs.image_tag_name }}" >> $GITHUB_OUTPUT + echo "image_tag_name_local_base=localhost:${{ env.LOCAL_REGISTRY_PORT }}/${{ steps.pre-vars.outputs.image_tag_name }}-base" >> $GITHUB_OUTPUT + echo "image_file_name=${{ steps.pre-vars.outputs.image_file_name }}" >> $GITHUB_OUTPUT + echo "image_file_path=./${{ steps.pre-vars.outputs.image_file_name }}" >> $GITHUB_OUTPUT + echo "cache_scope=${{ steps.pre-vars.outputs.cache_scope }}" >> $GITHUB_OUTPUT + echo "cache_key=${{ steps.pre-vars.outputs.cache_scope }}-${{ github.sha }}#${{ github.run_attempt }}" >> $GITHUB_OUTPUT + + # Build base image for debian version name. Layers will be cached and image pushes to local registry + - name: Build Image - Base + uses: docker/build-push-action@v5 + with: + context: . + load: false + push: true + file: ./ci/ci-debian.Dockerfile + target: test-code + platforms: ${{ inputs.platform }} + tags: ${{ steps.vars.outputs.image_tag_name_local_base }} + cache-from: type=gha,scope=${{ steps.vars.outputs.cache_scope }} + cache-to: type=gha,mode=max,scope=${{ steps.vars.outputs.cache_scope }} + build-args: | + DEBIAN_CODENAME=${{ inputs.debian_codename }} + USER_NAME=${{ env.TEST_USER_NAME }} + USER_GROUP=${{ env.TEST_USER_GROUP }} + GIT_BRANCH=${{ github.head_ref || github.ref_name }} + GIT_USER=${{ github.event.pull_request.head.user.login || github.repository_owner }} + + # Build new image with updates packages based on base image. Layers will NOT be chached. Result is written to file. + - name: Build Image - Update + uses: docker/build-push-action@v5 + with: + context: . + load: false + push: false + file: ./ci/ci-debian.Dockerfile + target: test-update + platforms: ${{ inputs.platform }} + tags: ${{ steps.vars.outputs.image_tag_name }} + cache-from: type=gha,scope=${{ steps.vars.outputs.cache_scope }} + # DON'T use 'cache-to' here as the layer is then cached and this build would be useless + outputs: type=docker,dest=${{ steps.vars.outputs.image_file_path }} + build-args: | + BASE_TEST_IMAGE=${{ steps.vars.outputs.image_tag_name_local_base }} + + - name: Artifact Upload Docker Image + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.vars.outputs.image_file_name }} + path: ${{ steps.vars.outputs.image_file_path }} + retention-days: 1 + + + # Run tests with build image + test: + needs: [build] + runs-on: ${{ inputs.runs_on }} + + strategy: + fail-fast: false + matrix: + test_script: ['run_install_common.sh', 'run_install_faststartup.sh', 'run_install_webapp_local.sh', 'run_install_webapp_download.sh', 'run_install_libzmq_local.sh'] + + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.0.0 + + - name: Artifact Download Docker Image + uses: actions/download-artifact@v3 + with: + name: ${{ needs.build.outputs.image_file_name }} + + - name: Load Docker Image + run: | + docker load --input ${{ needs.build.outputs.image_file_name }} + + # Run test + - name: Run Test ${{ inputs.debian_codename }}-${{ env.TEST_USER_NAME }}-${{ matrix.test_script }} + uses: tj-actions/docker-run@v2 + with: + image: ${{ needs.build.outputs.image_tag_name }} + options: --platform ${{ inputs.platform }} --user ${{ env.TEST_USER_NAME }} --init + name: ${{ matrix.test_script }} + args: | + ./${{ matrix.test_script }} + + # cleanup after test execution + cleanup: + # run only if tests didn't fail: keep the artifact to make job reruns possible + if: ${{ !failure() }} + needs: [build, test] + runs-on: ${{ inputs.runs_on }} + + steps: + - name: Artifact Delete Docker Image + uses: geekyeggo/delete-artifact@v2 + with: + name: ${{ needs.build.outputs.image_file_name }} diff --git a/.github/workflows/test_docker_debian_v3.yml b/.github/workflows/test_docker_debian_v3.yml new file mode 100644 index 000000000..6f90048ec --- /dev/null +++ b/.github/workflows/test_docker_debian_v3.yml @@ -0,0 +1,33 @@ +name: Test Install Scripts Debian v3 + +on: + schedule: + # run at 17:00 every sunday + - cron: '0 17 * * 0' + push: + pull_request: + # The branches below must be a subset of the branches above + branches: [ future3/develop ] + +# let only one instance run the test so cache is not corrupted. +# cancel already running instances as only the last run will be relevant +concurrency: + group: ${{ github.ref }}-test-debian-v3 + cancel-in-progress: true + +jobs: + + # Build container and run tests. Duplication of job intended for better visualization. + run_bookworm_armv7: + name: 'bookworm armv7' + uses: ./.github/workflows/test_docker_debian_codename_sub_v3.yml + with: + debian_codename: 'bookworm' + platform: linux/arm/v7 + + run_bullseye_armv7: + name: 'bullseye armv7' + uses: ./.github/workflows/test_docker_debian_codename_sub_v3.yml + with: + debian_codename: 'bullseye' + platform: linux/arm/v7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3bc03705..dbf12f84d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ For bug fixes and improvements just open an issue or PR as described below. If y * By default this will get you to the `future3/main` branch. You will move to the `future3/develop` branch, do this: ~~~bash -cd /home/pi/RPi-Jukebox-RFID +cd ~/RPi-Jukebox-RFID git checkout future3/develop git fetch origin git reset --hard origin/future3/develop @@ -122,7 +122,7 @@ If you touched *any* Python file (even if only for fixing spelling errors), run It contains out setup file. ~~~bash -cd /home/pi/RPi-Jukebox-RFID +cd ~/RPi-Jukebox-RFID ./run_flake8.sh ~~~ @@ -135,7 +135,7 @@ Tests are very few at the moment, but it cannot hurt to run them. If you have te them. ~~~bash -cd /home/pi/RPi-Jukebox-RFID/ +cd ~/RPi-Jukebox-RFID/ ./run_pytest.sh ~~~ diff --git a/README.md b/README.md index b95b0e8c3..5824bebbb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # RFID Jukebox Version 3 (aka future3) +![GitHub last commit (branch)](https://img.shields.io/github/last-commit/MiczFlor/RPi-Jukebox-RFID/future3/develop) + +[![Test Install Scripts Debian v3](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/test_docker_debian_v3.yml/badge.svg?branch=future3%2Fdevelop)](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/test_docker_debian_v3.yml) [![Python + Docs Checks and Tests](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/pythonpackage_future3.yml/badge.svg?branch=future3%2Fdevelop)](https://github.com/MiczFlor/RPi-Jukebox-RFID/actions/workflows/pythonpackage_future3.yml) [![Coverage Status](https://coveralls.io/repos/github/MiczFlor/RPi-Jukebox-RFID/badge.svg?branch=future3/develop)](https://coveralls.io/github/MiczFlor/RPi-Jukebox-RFID?branch=future3/develop) + +[![Matrix chat](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#phoniebox_community:gitter.im) + ## What is this? The exiting, new **Version 3** of the RPi Jukebox RFID. A complete re-write of the Jukebox. @@ -9,12 +15,8 @@ project check out the [documentation of Version 2]( /boot/cmdline.txt + +RUN echo "--- install packages (1) ---" \ + && apt-get update \ + && apt-get -y install \ + apt-utils \ + curl \ + gnupg \ + && echo "--- add sources ---" \ + && curl -fsSL http://raspbian.raspberrypi.org/raspbian.public.key | gpg --dearmor > /usr/share/keyrings/raspberrypi-raspbian-keyring.gpg \ + && curl -fsSL http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor > /usr/share/keyrings/raspberrypi-archive-debian-keyring.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/raspberrypi-raspbian-keyring.gpg] http://raspbian.raspberrypi.org/raspbian/ ${DEBIAN_CODENAME} main contrib non-free rpi" > /etc/apt/sources.list.d/raspi.list \ + && echo "deb [signed-by=/usr/share/keyrings/raspberrypi-archive-debian-keyring.gpg] http://archive.raspberrypi.org/debian/ ${DEBIAN_CODENAME} main" >> /etc/apt/sources.list.d/raspi.list \ + && echo "--- install packages (2) ---" \ + && apt-get update \ + && apt-get -y upgrade \ + && apt-get -y install \ + build-essential \ + iproute2 \ + openssh-client \ + sudo \ + systemd \ + wireless-tools \ + wget \ + wpasupplicant \ + && rm -rf /var/lib/apt/lists/* + +# Set NonInteractive for sudo usage in container. 'sudo' package needed +RUN echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections +# ------ + +# Base Target for setting up a test user. user can be selected with the docker '--user YYY' option +FROM base as test-user +ARG USER_NAME=pi +ARG USER_GROUP=$USER_NAME +ARG USER_ID=1000 + +ENV TEST_USER_GROUP=test +RUN groupadd --gid 1002 $TEST_USER_GROUP + +RUN groupadd --gid 1000 $USER_GROUP \ + && useradd -u $USER_ID -g $USER_GROUP -G sudo,$TEST_USER_GROUP -d /home/$USER_NAME -m -s /bin/bash -p '$1$iV7TOwOe$6ojkJQXyEA9bHd/SqNLNj0' $USER_NAME \ + && echo "$USER_NAME ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USER_NAME + +ENV XDG_RUNTIME_DIR=/run/user/$USER_ID DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$USER_ID/bus +# ------ + +# Target for adding envs and scripts from the repo to test installation +FROM test-user as test-code +ARG GIT_BRANCH +ARG GIT_USER + +ENV GIT_BRANCH=$GIT_BRANCH GIT_USER=$GIT_USER + +COPY --chown=root:$TEST_USER_GROUP --chmod=770 packages-core.txt ./ + +RUN echo "--- install internal packages ---" \ + && apt-get update \ + && sed 's/#.*//g' packages-core.txt | xargs apt-get -y install \ + && rm -rf /var/lib/apt/lists/* + +ENV INSTALL_SCRIPT_PATH=/code + +WORKDIR ${INSTALL_SCRIPT_PATH} +COPY --chown=root:$TEST_USER_GROUP --chmod=770 installation/install-jukebox.sh ./ + +WORKDIR ${INSTALL_SCRIPT_PATH}/tests +COPY --chown=root:$TEST_USER_GROUP --chmod=770 ci/installation/*.sh ./ +# ------ + + +# Target for applying latest updates (should not be cached!) +FROM $BASE_TEST_IMAGE as test-update +RUN apt-get update \ + && apt-get -y upgrade \ + && rm -rf /var/lib/apt/lists/* +# ------ diff --git a/ci/installation/run_install_common.sh b/ci/installation/run_install_common.sh new file mode 100644 index 000000000..102c71aa4 --- /dev/null +++ b/ci/installation/run_install_common.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: +# Test for a common installation path. Including autohotspot + +SOURCE="${BASH_SOURCE[0]}" +SCRIPT_DIR="$(dirname "$SOURCE")" +LOCAL_INSTALL_SCRIPT_PATH="${INSTALL_SCRIPT_PATH:-${SCRIPT_DIR}/../../installation}" +LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" + +export ENABLE_WEBAPP_PROD_DOWNLOAD=true +# Run installation (in interactive mode) +# y - start setup +# n - use static ip +# n - deactivate ipv6 +# y - setup autohotspot +# n - use custom password +# n - deactivate bluetooth +# n - disable on-chip audio +# - - mpd overwrite config (only with existing installation) +# n - setup rfid reader +# y - setup samba +# y - setup webapp +# n - setup kiosk mode +# - - install node (forced WebApp Download) +# n - reboot + +"${LOCAL_INSTALL_SCRIPT_PATH}/install-jukebox.sh" <<< 'y +n +n +y +n +n +n +n +y +y +n +n +' diff --git a/ci/installation/run_install_faststartup.sh b/ci/installation/run_install_faststartup.sh new file mode 100644 index 000000000..46cda25ec --- /dev/null +++ b/ci/installation/run_install_faststartup.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: +# Test for disabling features (suggestions for faststartup). Skips installing all additionals. + +SOURCE="${BASH_SOURCE[0]}" +SCRIPT_DIR="$(dirname "$SOURCE")" +LOCAL_INSTALL_SCRIPT_PATH="${INSTALL_SCRIPT_PATH:-${SCRIPT_DIR}/../../installation}" +LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" + +# Run installation (in interactive mode) +# y - start setup +# y - use static ip +# y - deactivate ipv6 +# n - setup autohotspot +# y - deactivate bluetooth +# y - disable on-chip audio +# - - mpd overwrite config (only with existing installation) +# n - setup rfid reader +# n - setup samba +# n - setup webapp +# - - setup kiosk mode (only with webapp = y) +# - - install node (only with webapp = y) +# n - reboot + +"${LOCAL_INSTALL_SCRIPT_PATH}/install-jukebox.sh" <<< 'y +y +y +n +y +y +n +n +n +n +' diff --git a/ci/installation/run_install_libzmq_local.sh b/ci/installation/run_install_libzmq_local.sh new file mode 100644 index 000000000..20f246ff8 --- /dev/null +++ b/ci/installation/run_install_libzmq_local.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: +# Test for the libzmq local build (no precompiled download) + +SOURCE="${BASH_SOURCE[0]}" +SCRIPT_DIR="$(dirname "$SOURCE")" +LOCAL_INSTALL_SCRIPT_PATH="${INSTALL_SCRIPT_PATH:-${SCRIPT_DIR}/../../installation}" +LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" + +export BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE=true +# Run installation (in interactive mode) +# y - start setup +# n - use static ip +# n - deactivate ipv6 +# n - setup autohotspot +# n - deactivate bluetooth +# n - disable on-chip audio +# - - mpd overwrite config (only with existing installation) +# n - setup rfid reader +# n - setup samba +# n - setup webapp +# - - setup kiosk mode (only with webapp = y) +# - - install node (only with webapp = y) +# n - reboot + +"${LOCAL_INSTALL_SCRIPT_PATH}/install-jukebox.sh" <<< 'y +n +n +n +n +n +n +n +n +n +' diff --git a/ci/installation/run_install_webapp_download.sh b/ci/installation/run_install_webapp_download.sh new file mode 100644 index 000000000..69496e8e4 --- /dev/null +++ b/ci/installation/run_install_webapp_download.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: +# Test for the WebApp (download) and dependent features path. + +SOURCE="${BASH_SOURCE[0]}" +SCRIPT_DIR="$(dirname "$SOURCE")" +LOCAL_INSTALL_SCRIPT_PATH="${INSTALL_SCRIPT_PATH:-${SCRIPT_DIR}/../../installation}" +LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" + +export ENABLE_WEBAPP_PROD_DOWNLOAD=true +# Run installation (in interactive mode) +# y - start setup +# n - use static ip +# n - deactivate ipv6 +# n - setup autohotspot +# n - deactivate bluetooth +# n - disable on-chip audio +# - - mpd overwrite config (only with existing installation) +# n - setup rfid reader +# n - setup samba +# y - setup webapp +# y - setup kiosk mode +# - - install node (forced webapp download) +# n - reboot + +"${LOCAL_INSTALL_SCRIPT_PATH}/install-jukebox.sh" <<< 'y +n +n +n +n +n +n +n +y +y +n +' diff --git a/ci/installation/run_install_webapp_local.sh b/ci/installation/run_install_webapp_local.sh new file mode 100644 index 000000000..d4f122fd5 --- /dev/null +++ b/ci/installation/run_install_webapp_local.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Install Phoniebox and test it +# Used e.g. for tests on Docker + +# Objective: +# Test for the WebApp (build locally) and dependent features path. + +SOURCE="${BASH_SOURCE[0]}" +SCRIPT_DIR="$(dirname "$SOURCE")" +LOCAL_INSTALL_SCRIPT_PATH="${INSTALL_SCRIPT_PATH:-${SCRIPT_DIR}/../../installation}" +LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" + +export ENABLE_WEBAPP_PROD_DOWNLOAD=false +# Run installation (in interactive mode) +# y - start setup +# n - use static ip +# n - deactivate ipv6 +# n - setup autohotspot +# n - deactivate bluetooth +# n - disable on-chip audio +# - - mpd overwrite config (only with existing installation) +# n - setup rfid reader +# n - setup samba +# y - setup webapp +# y - setup kiosk mode +# y - install node +# n - reboot + +"${LOCAL_INSTALL_SCRIPT_PATH}/install-jukebox.sh" <<< 'y +n +n +n +n +n +n +n +y +y +y +n +' diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b2216894e..1679bbeb5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -40,6 +40,7 @@ services: restart: unless-stopped tty: true volumes: + - ../src/jukebox:/root/RPi-Jukebox-RFID/src/jukebox - ../shared:/root/RPi-Jukebox-RFID/shared - ./config/docker.pulse.mpd.conf:/root/.config/mpd/mpd.conf command: python run_jukebox.py diff --git a/docker/jukebox.Dockerfile b/docker/jukebox.Dockerfile index 2eb025592..253c476c7 100644 --- a/docker/jukebox.Dockerfile +++ b/docker/jukebox.Dockerfile @@ -1,7 +1,5 @@ FROM debian:bullseye-slim -# Prepare Raspberry Pi like environment - # These are only dependencies that are required to get as close to the # Raspberry Pi environment as possible. RUN apt-get update && apt-get install -y \ @@ -36,9 +34,19 @@ USER ${USER} WORKDIR ${HOME} COPY --chown=${USER}:${USER} . ${INSTALLATION_PATH}/ -USER root -RUN pip install --no-cache-dir --upgrade -r ${INSTALLATION_PATH}/requirements.txt -RUN pip install pyzmq +RUN pip install --no-cache-dir -r ${INSTALLATION_PATH}/requirements.txt + +ENV ZMQ_TMP_DIR libzmq +ENV ZMQ_VERSION 4.3.5 +ENV ZMQ_PREFIX /usr/local + +RUN [ "$(uname -m)" = "aarch64" ] && ARCH="arm64" || ARCH="$(uname -m)"; \ + wget https://github.com/pabera/libzmq/releases/download/v${ZMQ_VERSION}/libzmq5-${ARCH}-${ZMQ_VERSION}.tar.gz -O libzmq.tar.gz; \ + tar -xzf libzmq.tar.gz -C ${ZMQ_PREFIX}; \ + rm -f libzmq.tar.gz; + +RUN export ZMQ_PREFIX=${PREFIX} && export ZMQ_DRAFT_API=1 +RUN pip install -v --no-binary pyzmq --pre pyzmq EXPOSE 5555 5556 diff --git a/documentation/README.md b/documentation/README.md index 9705e54d2..71ef9010f 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -6,16 +6,18 @@ The exciting, new Version 3 of the RPi Jukebox RFID. A complete rewrite of the J > This documentation applies to the Version 3 which is developed in the branches `future3/main` and `future3/develop`. Currently the default Version is 2.x To find out more about the RPi Jukebox RFID -project check out the [documentation of Version 2](https://github.com/MiczFlor/RPi-Jukebox-RFID) or [www.phoniebox.de](https://www.phoniebox.de/). +project check out the [documentation of Version 2](https://github.com/MiczFlor/RPi-Jukebox-RFID) or [www.phoniebox.de](https://phoniebox.de/). ## Quickstart -* [Installing Phoniebox future3](./content/userguide/installation.md) -* [Update](./content/userguide/update.md) -* [Feature Status](./content/developers/status.md) -* [Known Issues](./content/developers/known-issues.md) -* [User Guide](./content/userguide/) -* [Developer Reference](./content/developers) +* For Builders: Building a Phoniebox + * [Installing Phoniebox future3](./builders/installation.md) + * [Builder Guides](./builders/README.md) + * [Update](./builders/update.md) +* For Developers: Add features or fix bugs + * [Developer Guides](./developers/README.md) + * [Feature Status](./developers/status.md) + * [Known Issues](./developers/known-issues.md) ## future3 diff --git a/documentation/content/userguide/README.md b/documentation/builders/README.md similarity index 89% rename from documentation/content/userguide/README.md rename to documentation/builders/README.md index 445ba9421..3d70bca15 100644 --- a/documentation/content/userguide/README.md +++ b/documentation/builders/README.md @@ -1,4 +1,4 @@ -# User Guide +# Builder Guides ## Getting started @@ -21,4 +21,4 @@ * [System](./system.md) * [Feature Status](../developers/status.md) * [Known Issues](../developers/known-issues.md) -* [Developer Reference](../developers) +* [Developer Reference](../developers/README.md) diff --git a/documentation/content/userguide/audio.md b/documentation/builders/audio.md similarity index 88% rename from documentation/content/userguide/audio.md rename to documentation/builders/audio.md index d4b06b481..4fd82e457 100644 --- a/documentation/content/userguide/audio.md +++ b/documentation/builders/audio.md @@ -10,17 +10,17 @@ Stream transfer happens on user input or automatically on the connection of an a This is mainly targeted at Bluetooth Headsets/Speakers. Audio outputs run via PulseAudio and the basic configuration should be easy. -There is a [configuration tool](../developers/coreapps.md#run_configure_audio.py), +There is a [configuration tool](../developers/coreapps.md#Audio), to setup the configuration for the Jukebox Core App. -To set up the audio +### To set up the audio 1. Follow the setup steps according to your sound card 2. Check that the sound output works [as described below](audio.md#checking-system-sound-output) -3. Run the the tool [run_configure_audio](../developers/coreapps.md#run_configure_audio.py) +3. Run the [audio configuration tool](../developers/coreapps.md#Audio) 4. [Fine-tune audio parameters](audio.md#additional-options) -## Checking system sound output +#### Checking system sound output Run the following steps in a console: @@ -31,7 +31,7 @@ $ pactl list sinks short 1 bluez_sink.C4_FB_20_63_CO_FE.a2dp_sink module-bluez5-device.c s16le 2ch 44100Hz # Set the default sink (this will be reset at reboot) -$ pactl set-default-sink sink_name +$ pactl set-default-sink # Check default sink is correctly set $ pactl info @@ -50,10 +50,10 @@ You can also try different PulseAudio sinks without setting the default sink. In volume level for this sink: ```bash -$ paplay -d sink_name /usr/share/sounds/alsa/Front_Center.wav +$ paplay -d /usr/share/sounds/alsa/Front_Center.wav ``` -# Bluetooth +## Bluetooth Bluetooth setup consists of three steps @@ -99,7 +99,7 @@ Rerun the config tool to register the Bluetooth device with the Jukebox core app For other audio configuration options, please look at the `jukebox.yaml` for now. -Directly edit `jukebox.yaml` following the steps: [Best practice procedure](configuraton.md#best-practice-procedure). +Directly edit `jukebox.yaml` following the steps: [Best practice procedure](configuration.md#best-practice-procedure). ## Developer Information @@ -134,8 +134,4 @@ You are, of course, free to modify the PulseAudio configuration to your needs. R 1. [PulseAudio Documentation](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User) 2. [PulseAudio Examples](https://wiki.archlinux.org/title/PulseAudio/Examples) -In this case, run the configuration tool with below parameter to avoid touching the PulseAudio configuration file. - -```bash -$ ./run_configure_audio.py --ro_pulse -``` +In this case, run the [audio configuration tool](../developers/coreapps.md#Audio) with the parameter `--ro_pulse` to avoid touching the PulseAudio configuration file. diff --git a/documentation/content/userguide/autohotspot.md b/documentation/builders/autohotspot.md similarity index 99% rename from documentation/content/userguide/autohotspot.md rename to documentation/builders/autohotspot.md index 7f064cb53..69e6f4d6a 100644 --- a/documentation/content/userguide/autohotspot.md +++ b/documentation/builders/autohotspot.md @@ -28,7 +28,7 @@ After connecting to the `Phoniebox_Hotspot` you are able to connect via ssh to your Jukebox ``` bash -ssh pi@10.0.0.5 +ssh @10.0.0.5 ``` ## Changing basic configuration of the hotspot diff --git a/documentation/content/userguide/bluetooth-audio-buttons.md b/documentation/builders/bluetooth-audio-buttons.md similarity index 100% rename from documentation/content/userguide/bluetooth-audio-buttons.md rename to documentation/builders/bluetooth-audio-buttons.md diff --git a/documentation/content/userguide/card-database.md b/documentation/builders/card-database.md similarity index 97% rename from documentation/content/userguide/card-database.md rename to documentation/builders/card-database.md index 66d4e3925..67dbf5dae 100644 --- a/documentation/content/userguide/card-database.md +++ b/documentation/builders/card-database.md @@ -4,7 +4,7 @@ In the card database, an RPC command is assigned to every card. This RPC command is called every time when the card is swiped (or placed) on the reader. Every RPC callable function can be called. See -[RPC Commands](userguide/rpc_commands.md) for an introduction. +[RPC Commands](rpc-commands.md) for an introduction. The card database is stored in `shared\settings\cards.yaml`. Here are some examples for RPC command assignments to cards \'0001\' to \'0003\' diff --git a/documentation/content/userguide/concepts.md b/documentation/builders/concepts.md similarity index 77% rename from documentation/content/userguide/concepts.md rename to documentation/builders/concepts.md index 70f285973..37c13db89 100644 --- a/documentation/content/userguide/concepts.md +++ b/documentation/builders/concepts.md @@ -16,12 +16,10 @@ The Remote Procedure Call (RPC) server allows remotely triggering actions (e.g., Why should you care? Because we use the same protocol when triggering actions from other inputs like a card swipe, a GPIO button press, etc. How that works is described in [RPC Commands](rpc-commands.md). -You will find a full list of RPC callable functions in [RPC Command Reference](rpc-command-reference.md) and aliases for convenience in [RPC Command Alias Reference](rpc-command-alias-reference.md). - -We also have a tool to send RPC commands to the running Jukebox application: [run_rpc_tool.py](../../../src/jukebox/run_rpc_tool.py). +We also have a [tool to send RPC commands](../developers/coreapps.md#RPC) to the running Jukebox application. ## Publishing Message Queue The Publishing Message Queue is the complementary part to the RPC where the core application publishes its status and status updates. As a user, you need not worry about it. -If you want to interact with the Jukebox from your own application, this is where you get the current state from. Details about the protocol can be found here (TBD). A sniffer tool exists which listens and prints the incoming status messages: [run_publicity_sniffer.py](../../../src/jukebox/run_rpc_tool.py). +If you want to interact with the Jukebox from your own application, this is where you get the current state from. Details about the protocol can be found here (TBD). A [sniffer tool](../developers/coreapps.md#Publicity-Sniffer) exists which listens and prints the incoming status messages. diff --git a/documentation/content/userguide/configuration.md b/documentation/builders/configuration.md similarity index 72% rename from documentation/content/userguide/configuration.md rename to documentation/builders/configuration.md index 2aaa3184b..f09e8098d 100644 --- a/documentation/content/userguide/configuration.md +++ b/documentation/builders/configuration.md @@ -1,16 +1,16 @@ # Jukebox Configuration -The Jukebox configuration is managed by set of files located in `../shared/settings`. +The Jukebox configuration is managed by a set of files located in `shared/settings`. Some configuration changes can be made through the WebUI and take immediate effect. The majority of configuration options is only available by editing the config files - *when the service is not running!* Don't fear (overly), they contain commentaries. -For several aspects we have :ref:`developer/coreapps:Configuration Tools` and detailed guides: +For several aspects we have [configuration tools](../developers/coreapps.md#configuration-tools) and detailed guides: * [Audio Configuration](./audio.md#audio-configuration) -* [RFID Reader Configuration](./rfid/basics.md#reader-configuration) +* [RFID Reader Configuration](../developers/rfid/basics.md#reader-configuration) Even after running the tools, certain aspects can only be changed by modifying the configuration files directly. @@ -24,7 +24,8 @@ $ systemctl --user stop jukebox-daemon $ nano ./shared/settings/jukebox.yaml # Start Jukebox in console and check the log output (optional) -$ ./src/jukebox/run_jukebox.py +$ cd src/jukebox +$ ./run_jukebox.py # and if OK, press Ctrl-C and restart the service # Restart the service @@ -36,5 +37,6 @@ This could be useful if you want your Jukebox to only allow a lower volume when at night time when there is time to go to bed :-) ```bash -$./run_jukebox.py --conf ../path/to/custom/config.yaml +$ cd src/jukebox +$ ./run_jukebox.py --conf path/to/custom/config.yaml ``` diff --git a/documentation/content/userguide/installation.md b/documentation/builders/installation.md similarity index 84% rename from documentation/content/userguide/installation.md rename to documentation/builders/installation.md index 109423faf..756e2a03c 100644 --- a/documentation/content/userguide/installation.md +++ b/documentation/builders/installation.md @@ -3,7 +3,7 @@ ## Install Raspberry Pi OS Lite > [!IMPORTANT] -> Currently, the installation does only work on Raspberry Pi's with ARMv7 and ARMv8 architecture, so 2, 3 and 4! Pi 1 and Zero's are currently unstable and will require a bit more work! +> Currently, the installation does only work on Raspberry Pi's with ARMv7 and ARMv8 architecture, so 2, 3 and 4! Pi 1 and Zero's are currently unstable and will require a bit more work! Pi 4 and 5 are an excess ;-) Before you can install the Phoniebox software, you need to prepare your Raspberry Pi. @@ -16,7 +16,7 @@ Before you can install the Phoniebox software, you need to prepare your Raspberr * Click `Edit Settings` * Switch to the `General` tab * Provide a hostname. (When on Mac, you will be able to use it to connect via SSH) - * Username currently MUST be `pi`. Other usernames are currently not supported. + * Username * Password * Wifi * Set locale settings @@ -72,7 +72,7 @@ You will need a terminal, like PuTTY for Windows or the Terminal app for Mac to 7. Eject your SD card and insert it into your Raspberry Pi. 8. Start your Raspberry Pi by attaching a power supply. -9. Login into your Raspberry Pi, username is `pi` and password is `raspberry`. +9. Login into your Raspberry Pi If `raspberrypi.local` does not work, find out your Raspberry Pi's IP address from your router. @@ -85,22 +85,24 @@ Run the following command in your SSH terminal and follow the instructions cd; bash <(wget -qO- https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID/future3/main/installation/install-jukebox.sh) ``` -This will get the latest stable release from the branch future3/main. +This will get the latest **stable release** from the branch *future3/main*. + To install directly from a specific branch and/or a different repository specify the variables like this: ```bash - cd; GIT_USER='MiczFlor' GIT_BRANCH='future3/develop' bash <(wget -qO- https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID/future3/develop/installation/install-jukebox.sh) ``` This will switch directly to the specified feature branch during installation. > [!NOTE] -> For all branches *except* the current Release, you will need to build the Web App locally on the Pi. This is not part of the installation process due to memory limitation issues. See [Steps to install](../developers/development-environment.md#steps-to-install) +> For all branches *except* the current Release future3/main, you will need to build the Web App locally on the Pi. This is not part of the installation process due to memory limitation issues. See [Developer steps to install](../developers/development-environment.md#steps-to-install) If you suspect an error you can monitor the installation-process with ```bash cd; tail -f INSTALL-.log ``` + +After successful installation, continue with [configuring your Phoniebox](configuration.md). diff --git a/documentation/content/userguide/mock_gpio.png b/documentation/builders/mock_gpio.png similarity index 100% rename from documentation/content/userguide/mock_gpio.png rename to documentation/builders/mock_gpio.png diff --git a/documentation/content/userguide/rfid.md b/documentation/builders/rfid.md similarity index 100% rename from documentation/content/userguide/rfid.md rename to documentation/builders/rfid.md diff --git a/documentation/builders/rpc-commands.md b/documentation/builders/rpc-commands.md new file mode 100644 index 000000000..f98da8cb4 --- /dev/null +++ b/documentation/builders/rpc-commands.md @@ -0,0 +1,112 @@ +# RPC Commands + + +We use the RPC commands when triggering actions from different inputs like a card swipe, +a GPIO button press, etc. Triggering an action is equal to sending an RPC function call. +In many places the command to send when an input is triggered is configurable in a YAML-file. + +## Basics + +Consequently, you need to know how to specify the RPC command in the YAML file. +Here is the essence of what you need to know: + +An RPC command consists of up to three parts + + #. the function to execute (e.g. play_folder, change_volume) + #. the positional arguments (optional) + #. the keyword arguments (optional) + +The function specification consists of two (e.g., ``host.shutdown``) or three terms (e.g., ``volume.ctrl.change_volume``). +In configuration files, this will look like this: + +.. code-block:: yaml + + package: host + plugin: shutdown + +Or like this for a three part function with the argument set to ``5``: + +.. code-block:: yaml + + package: volume + plugin: ctrl + method: change_volume + args: [5] + +The keyword ``method`` is optional. If needs to be used depends on the function you want to call. + +## Aliases + + +Not so complicated, right? It will get even easier. For common commands we have defined aliases. An alias simply maps +to a pre-defined RPC command, e.g. ``play_card`` maps to ``player.ctrl.play_card``. + +Instead of + +.. code-block:: yaml + + package: player + plugin: ctrl + method: play_card + args: [path/to/folder] + +you can simply specify instead : + +.. code-block:: yaml + + alias: play_card + args: [path/to/folder] + +Using in alias is optional. But if the keyword is present in the configuration it takes precedence over an explicit +specified RPC command. + +## Arguments + +Arguments can be specified in similar fashion to Python function arguments: as positional arguments and / or +keyword arguments. Let's check out play_card, which is defined as: + +.. py:function:: play_card(...) -> player.ctrl.play_card(folder: str, recursive: bool = False) + :noindex: + + :param folder: Folder path relative to music library path + :param recursive: Add folder recursively + +This means it takes two arguments: + + * folder of type string + * recursive of type bool + +In the following examples, we will always use the alias for smaller configuration text. All three examples +do exactly the same, but use different ways of specifying the command. + +.. code-block:: yaml + + alias: play_card + args: [path/to/folder, True] + +.. code-block:: yaml + + alias: play_card + args: [path/to/folder] + kwargs: + recursive: True + +.. code-block:: yaml + + alias: play_card + kwargs: + folder: path/to/folder + recursive: True + + +.. important:: *args* must be a **list** of arguments to be passed! Even if only a single argument is passed. + So, use *args: [value]*. We try catch mis-uses but that might not always work. + + +You will find some more examples the configuration of the [Card Database](card-database.md) + +## For developers + +To send RPC commands for testing and debugging purpose you can use the [CLI Tool](../developers/coreapps.md#RPC). +Also here is a ready-to-use decoding functions which decodes an RPC command (with or without alias) +from a YAML entry:func:`jukebox.utils.decode_rpc_command`. diff --git a/documentation/content/userguide/system.md b/documentation/builders/system.md similarity index 89% rename from documentation/content/userguide/system.md rename to documentation/builders/system.md index 5b1ce5c0c..0e2c19485 100644 --- a/documentation/content/userguide/system.md +++ b/documentation/builders/system.md @@ -10,9 +10,8 @@ The system consists of 4. [Web UI](system.md#web-ui) which is served through an Nginx web server 5. A set of [Configuration Tools](../developers/coreapps.md#configuration-tools) and a set of [Developer Tools](../developers/coreapps.md#developer-tools) -.. note:: The default install puts everything into the folder `/home/pi/RPi-Jukebox-RFID`. - Another folder might work, but is certainly not tested. Things are installed for the default user `pi`. Again, - another user might work, but is not tested. +.. note:: The default install puts everything into the users home folder `~/RPi-Jukebox-RFID`. + Another folder might work, but is certainly not tested. ## Music Player Daemon (MPD) @@ -71,7 +70,7 @@ Service control and service configuration file location is identical to MPD. ## Jukebox Core Service -The :ref:`developer/coreapps:Jukebox Core` runs as a *user-local* service with the name `jukebox-daemon`. +The [Jukebox Core Service](../developers/coreapps.md#Jukebox-Core) runs as a *user-local* service with the name `jukebox-daemon`. Similar to MPD, it's important that it does run as system-wide service to be able to interact with PulseAudio. The service can be controlled with the `systemctl`-command by adding the parameter `--user` @@ -102,7 +101,7 @@ Starting and stopping the service can be useful for debugging or configuration c The Web UI is served using nginx. Nginx runs as a system service. The home directory is localed at ``` -/home/pi/RPi-Jukebox-RFID/src/webapp/build +./src/webapp/build ``` The Nginx configuration is located at diff --git a/documentation/content/userguide/troubleshooting.md b/documentation/builders/troubleshooting.md similarity index 86% rename from documentation/content/userguide/troubleshooting.md rename to documentation/builders/troubleshooting.md index a0618ab5d..a83384ec5 100644 --- a/documentation/content/userguide/troubleshooting.md +++ b/documentation/builders/troubleshooting.md @@ -15,8 +15,8 @@ Debugging your setup runs in several steps ## The short answer ```bash -../shared/logs/app.log : Complete Debug Messages -../shared/logs/errors.log: Only Errors and Warnings +shared/logs/app.log : Complete Debug Messages +shared/logs/errors.log: Only Errors and Warnings ``` These files always contain the messages of the current run only. @@ -33,9 +33,9 @@ http://ip.of.your.box/logs ## The long answer: A few more details -If started without parameters, the Jukebox checks for the existence of `../shared/settings/logger.yaml` +If started without parameters, the Jukebox checks for the existence of `shared/settings/logger.yaml` and if present, uses that configuration for logging. This file is created by the installation process. -The default configuration file is also provided in `../resources/default-settings/logger.default.yaml`. +The default configuration file is also provided in `resources/default-settings/logger.default.yaml`. We use Python's logging module to provide the debug messages which is configured through this file. **We are still in the Pre-Release phase which means full debug logging is enabled by default.** @@ -47,8 +47,8 @@ The default logging config does 2 things: 1. It writes 2 log files: ```bash -../shared/logs/app.log : Complete Debug Messages -../shared/logs/errors.log : Only Errors and Warnings +shared/logs/app.log : Complete Debug Messages +shared/logs/errors.log : Only Errors and Warnings ``` 2. Prints logging messages to the console. If run as a service, only error messages are emitted to console to avoid spamming the system log files. @@ -63,11 +63,12 @@ on the console log. $ systemctl --user stop jukebox-daemon # Start the Jukebox in debug mode: +$ cd src/jukebox + # with default logger: $ ./run_jukebox.py - # or with custom logger configuration: -$ ./run_jukebox.py --logger ../path/to/logger.yaml +$ ./run_jukebox.py --logger path/to/custom/logger.yaml ``` ### Fallback configuration @@ -77,6 +78,7 @@ Attention: This only emits messages to the console and does not write to the log This is more a fallback features: ``` bash +$ cd src/jukebox $ ./run_jukebox.py -vv ``` diff --git a/documentation/content/userguide/update.md b/documentation/builders/update.md similarity index 89% rename from documentation/content/userguide/update.md rename to documentation/builders/update.md index e09137ca3..f6dab2c91 100644 --- a/documentation/content/userguide/update.md +++ b/documentation/builders/update.md @@ -2,6 +2,12 @@ ## Updating your Jukebox Version 3 +### Update from v3.2.1 and prior + +As there are some significant changes in the installation, a new setup on a fresh image is required. + +### General + Things on Version 3 are moving fast and you may want to keep up with recent changes. Since we are in Alpha Release stage, a fair number of fixes are expected to be committed in the near future. diff --git a/documentation/content/calendars/2019-Phoniebox-Calendar.jpg b/documentation/calendars/2019-Phoniebox-Calendar.jpg similarity index 100% rename from documentation/content/calendars/2019-Phoniebox-Calendar.jpg rename to documentation/calendars/2019-Phoniebox-Calendar.jpg diff --git a/documentation/content/calendars/2020-Phoniebox-Calendar.jpg b/documentation/calendars/2020-Phoniebox-Calendar.jpg similarity index 100% rename from documentation/content/calendars/2020-Phoniebox-Calendar.jpg rename to documentation/calendars/2020-Phoniebox-Calendar.jpg diff --git a/documentation/content/calendars/2021-Phoniebox-Calendar.jpg b/documentation/calendars/2021-Phoniebox-Calendar.jpg similarity index 100% rename from documentation/content/calendars/2021-Phoniebox-Calendar.jpg rename to documentation/calendars/2021-Phoniebox-Calendar.jpg diff --git a/documentation/content/developers/development-environment.md b/documentation/content/developers/development-environment.md deleted file mode 100644 index 62eaeaec6..000000000 --- a/documentation/content/developers/development-environment.md +++ /dev/null @@ -1,72 +0,0 @@ -# Development Environment - -You have 3 development options: - -## Directly on Raspberry Pi - -The full setup is running on the RPi and you access files via SSH. -Pretty easy to set up as you simply do a normal install and switch to -the `future3/develop` branch. - -### Steps to install - -We recommend to use at least a Pi 3 or Pi Zero 2 for development. This -hardware won\'t be needed in production, but it can be slow while -developing. - -1. Install the latest Pi OS on a SD card. -2. Boot up your Raspberry Pi. -3. [Install](../userguide/installation.md) the Jukebox software as if you were building a - Phoniebox. You can install from your own fork and feature branch if - you wish which can be changed later as well. The original repository - will be set as `upstream`. -4. Once the installation has successfully ran, reboot your Pi. -5. Due to some resource constraints, the Webapp does not build the - latest changes and instead consumes the latest official release. To - change that, you need to install NodeJS and build the Webapp - locally. -6. Install NodeJS using the existing installer - - ``` bash - cd ~/RPi-Jukebox-RFID/installation/routines; \ - source setup_jukebox_webapp.sh; \ - _jukebox_webapp_install_node - ``` - -7. To free up RAM, reboot your Pi. -8. Build the Webapp using the existing build command. If the build - fails, you might have forgotten to reboot. - - ``` bash - cd ~/RPi-Jukebox-RFID/src/webapp; \ - ./run_rebuild.sh -u - ``` - -9. The Webapp should now be updated. -10. To continuously update Webapp, pull the latest changes from your - repository and rerun the command above. - -## Locally on any Linux machine - -The jukebox also runs on any Linux machine. The Raspberry Pi specific -stuff will not work of course. That is no issue depending our your -development area. USB RFID Readers, however, will work. You will have -to install and configure [MPD (Music Player -Daemon)](https://www.musicpd.org/). - -In addition to the `requirements.txt`, you will this -dependency. On the Raspberry PI, the latest stable release of ZMQ does -not support WebSockets. We need to compile the latest version from -Github, which is taken care of by the installation script. For regular -machines, the normal package can be installed: - -``` bash -pip install pyzmq -``` - -You will have to start Jukebox core application and the WebUI -separately. The MPD usually runs as a service. - -## Using Docker container - -There is a complete [Docker setup](./docker.md). diff --git a/documentation/content/developers/known-issues.md b/documentation/content/developers/known-issues.md deleted file mode 100644 index 598ecd791..000000000 --- a/documentation/content/developers/known-issues.md +++ /dev/null @@ -1,13 +0,0 @@ -# Known Issues - -## Browsers - -The Web UI will **not** work with Firefox, due to an issue with websockets and pyzmq. Please use a different -browser for now. - -## Configuration - -In `jukebox.yaml` (and all other config files): do not use relative paths with `~/some/dir`. -Always use entire explicit path, e.g. `/home/pi/some/dir`. - -**Sole** exception is in `playermpd.mpd_conf`. diff --git a/documentation/content/developers/rfid/template_reader.md b/documentation/content/developers/rfid/template_reader.md deleted file mode 100644 index 48275d619..000000000 --- a/documentation/content/developers/rfid/template_reader.md +++ /dev/null @@ -1,2 +0,0 @@ - -For documentation see [src/jukebox/components/rfid/hardware/template_new_reader/README.md](../../../../src/jukebox/components/rfid/hardware/template_new_reader/README.md). diff --git a/documentation/content/developers/README.md b/documentation/developers/README.md similarity index 78% rename from documentation/content/developers/README.md rename to documentation/developers/README.md index a2cb94c42..6c973d6ba 100644 --- a/documentation/content/developers/README.md +++ b/documentation/developers/README.md @@ -1,9 +1,9 @@ -# Developers +# Developer Guides ## Getting started * [Development Environment](./development-environment.md) -* [Setting up Docker](./docker.md) +* [Python Development Notes](pyhton.md) ## Reference diff --git a/documentation/content/developers/coreapps.md b/documentation/developers/coreapps.md similarity index 58% rename from documentation/content/developers/coreapps.md rename to documentation/developers/coreapps.md index 757af2ae4..3e4fc7b1f 100644 --- a/documentation/content/developers/coreapps.md +++ b/documentation/developers/coreapps.md @@ -4,53 +4,59 @@ The Jukebox\'s core apps are located in `src/jukebox`. Run the following command to learn more about each app and its parameters: ``` bash -$ ./run_app_name.py -h +$ cd src/jukebox +$ ./ -h ``` + ## Jukebox Core -### `run_jukebox.py` +**Scriptname:** [run_jukebox.py](../../src/jukebox/run_jukebox.py) This is the main app and starts the Jukebox Core. -Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart the service. For example after a configuration change. Not all configuration changes can be applied on-the-fly. See [Jukebox Configuration](../userguide/configuration.md#jukebox-configuration). +Usually this runs as a service, which is started automatically after boot-up. At times, it may be necessary to restart the service. For example after a configuration change. Not all configuration changes can be applied on-the-fly. See [Jukebox Configuration](../builders/configuration.md#jukebox-configuration). + +For debugging, it is usually desirable to run the Jukebox directly from the console rather than as service. This gives direct logging info in the console and allows changing command line parameters. See [Troubleshooting](../builders/troubleshooting.md). -For debugging, it is usually desirable to run the Jukebox directly from the console rather than as service. This gives direct logging info in the console and allows changing command line parameters. See [Troubleshooting](../userguide/troubleshooting.md). ## Configuration Tools Before running the configuration tools, stop the Jukebox Core service. -See [Best practice procedure](../userguide/configuration.md#best-practice-procedure). +See [Best practice procedure](../builders/configuration.md#best-practice-procedure). -### `run_configure_audio.py` +### Audio -Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. +**Scriptname:** [run_configure_audio.py](../../src/jukebox/run_configure_audio.py) -Will also setup equalizer and mono down mixer in the pulseaudio config file. +Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. -Run this once after installation. Can be re-run at any time to change the settings. For more information see [Audio Configuration](../userguide/audio.md). +Will also setup equalizer and mono down mixer in the pulseaudio config file. Run this once after installation. Can be re-run at any time to change the settings. For more information see [Audio Configuration](../builders/audio.md). -### `run_register_rfid_reader.py` +### RFID Reader -Setup tool to configure the RFID Readers. +**Scriptname:** [run_register_rfid_reader.py](../../src/jukebox/run_register_rfid_reader.py) + +Setup tool to configure the RFID Readers. Run this once to register and configure the RFID readers with the Jukebox. Can be re-run at any time to change the settings. For more information see [RFID Readers](./rfid/README.md). > [!NOTE] > This tool will always write a new configurations file. Thus, overwrite the old one (after checking with the user). Any manual modifications to the settings will have to be re-applied + ## Developer Tools -### `run_rpc_tool.py` +### RPC -Command Line Interface to the Jukebox RPC Server +**Scriptname:** [run_rpc_tool.py](../../src/jukebox/run_rpc_tool.py) -A command line tool for sending RPC commands to the running jukebox app. This uses the same interface as the WebUI. Can be used for additional control or for debugging. +Command Line Interface to the Jukebox RPC Server. -The tool features auto-completion and command history. +A command line tool for sending RPC commands to the running jukebox app. This uses the same interface as the WebUI. Can be used for additional control or for debugging.The tool features auto-completion and command history. The list of available commands is fetched from the running Jukebox service. -The list of available commands is fetched from the running Jukebox service. +### Publicity Sniffer -### `run_publicity_sniffer.py` +**Scriptname:** [run_publicity_sniffer.py](../../src/jukebox/run_publicity_sniffer.py) A command line tool that monitors all messages being sent out from the Jukebox via the publishing interface. Received messages are printed in the console. Mainly used for debugging. diff --git a/documentation/content/developers/developer-issues.md b/documentation/developers/developer-issues.md similarity index 81% rename from documentation/content/developers/developer-issues.md rename to documentation/developers/developer-issues.md index 4b1bd4794..c8dd3b6c3 100644 --- a/documentation/content/developers/developer-issues.md +++ b/documentation/developers/developer-issues.md @@ -46,9 +46,22 @@ Alternatively, use the provided script, which sets the variable for you (provided your swap size is large enough): ``` bash +$ cd src/webapp $ ./run_rebuild.sh ``` +**Changing Swap Size** + +This will set the swapsize to 1024 MB (and will deactivate swapfactor). Change accordingly if you have a SD Card with small capacity. + +``` +sudo dphys-swapfile swapoff +sudo sed -i "s|.*CONF_SWAPSIZE=.*|CONF_SWAPSIZE=1024|g" /etc/dphys-swapfile +sudo sed -i "s|^\s*CONF_SWAPFACTOR=|#CONF_SWAPFACTOR=|g" /etc/dphys-swapfile +sudo dphys-swapfile setup +sudo dphys-swapfile swapon +``` + ### Process exited too early // kill -9 ``` {.bash emphasize-lines="8,9"} diff --git a/documentation/developers/development-environment.md b/documentation/developers/development-environment.md new file mode 100644 index 000000000..9abefbee0 --- /dev/null +++ b/documentation/developers/development-environment.md @@ -0,0 +1,57 @@ +# Development Environment + + + +You have 3 development options. Each option has its pros and cons. To interact with GPIO or other hardware, it's required to develop directly on a Raspberry Pi. For general development of Python code (Jukebox) or JavaScript (Webapp), we recommend Docker. Developing on your local machine (Linux, Mac, Windows) works as well and requires all dependencies to be installed locally. + +* [Develop in Docker](#develop-in-docker) +* [Develop on Raspberry Pi](#develop-on-raspberry-pi) +* [Develop on local machine](#develop-on-local-machine) + +## Develop in Docker + +There is a complete [Docker setup](./docker.md). + +## Develop on Raspberry Pi + +The full setup is running on the RPi and you access files via SSH. Pretty easy to set up as you simply do a normal install and switch to the `future3/develop` branch. + +### Steps to install + +We recommend to use at least a Pi 3 or Pi Zero 2 for development. This hardware won\'t be needed in production, but it can be slow while developing. + +1. Install the latest Pi OS on a SD card. +2. Boot up your Raspberry Pi. +3. [Install](../builders/installation.md) the Jukebox software as if you were building a Phoniebox. You can install from your own fork and feature branch you wish which can be changed later as well. The original repository will be set as `upstream`. +4. Once the installation has successfully ran, reboot your Pi. +5. Due to some resource constraints, the Webapp does not build the latest changes and instead consumes the latest official release. To change that, you need to install NodeJS and build the Webapp locally. +6. Install NodeJS using the existing installer + + ``` bash + cd ~/RPi-Jukebox-RFID/installation/routines; \ + source setup_jukebox_webapp.sh; \ + _jukebox_webapp_install_node + ``` + +7. To free up RAM, reboot your Pi. +8. Build the Webapp using the existing build command. If the build fails, you might have forgotten to reboot. + + ``` bash + cd ~/RPi-Jukebox-RFID/src/webapp; \ + ./run_rebuild.sh -u + ``` + +9. The Webapp should now be updated. +10. To continuously update Webapp, pull the latest changes from your repository and rerun the command above. + +## Develop on local machine + +The jukebox also runs on any Linux machine. The Raspberry Pi specific stuff will not work of course. That is no issue depending our your development area. USB RFID Readers, however, will work. You will have to install and configure [MPD (Music Player Daemon)](https://www.musicpd.org/). + +In addition to the `requirements.txt`, you will this dependency. On the Raspberry PI, the latest stable release of ZMQ does not support WebSockets. We need to compile the latest version from Github, which is taken care of by the installation script. For regular machines, the normal package can be installed: + +``` bash +pip install pyzmq +``` + +You will have to start Jukebox core application and the WebUI separately. The MPD usually runs as a service. diff --git a/documentation/content/developers/docker.md b/documentation/developers/docker.md similarity index 88% rename from documentation/content/developers/docker.md rename to documentation/developers/docker.md index 0def11610..39518e248 100644 --- a/documentation/content/developers/docker.md +++ b/documentation/developers/docker.md @@ -14,16 +14,8 @@ need to adapt some of those commands to your needs. ## Prerequisites -1. Install required software - * Linux - * [Docker](https://docs.docker.com/engine/install/debian/) - * [Compose](https://docs.docker.com/compose/install/) - * Mac - * [Docker & Compose (Mac)](https://docs.docker.com/docker-for-mac/install/) - * [pulseaudio (Docker)](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html) - * Windows - * [Docker & Compose (Windows)](https://docs.docker.com/docker-for-windows/install/) - * [pulseaudio (Windows)](https://www.freedesktop.org/wiki/Software/PulseAudio/Ports/Windows/Support/) +1. Install required software: Docker, Compose and pulseaudio + * Check installations guide for [Mac](#mac), [Windows](#windows) or [Linux](#linux) 2. Pull the Jukebox repository: @@ -41,7 +33,7 @@ need to adapt some of those commands to your needs. * Override/Merge the values from the following [Override file](https://github.com/MiczFlor/RPi-Jukebox-RFID/blob/future3/develop/docker/config/jukebox.overrides.yaml) in your `jukebox.yaml`. * **\[Currently required\]** Update all relative paths (`../..`) in to `/home/pi/RPi-Jukebox-RFID`. -4. Change directory into the `./RPi-Jukebox-RFID/shared/audiofolders` +4. Change directory into the `./shared/audiofolders` and copy a set of MP3 files into this folder (for more fun when testing). @@ -52,47 +44,10 @@ practice to isolate different components in different Docker images. They can be run individually or in combination. To do that, we use `docker-compose`. -### Linux - -Make sure you don\'t use `sudo` to run your `docker-compose`. Check out -Docker\'s [post-installation guide](https://docs.docker.com/engine/install/linux-postinstall/) for more information. - -```bash -// Build Images -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml build - -// Run Docker Environment -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml up - -// Shuts down Docker containers and Docker network -$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml down -``` - -Note: if you have `mpd` running on your system, you need to stop it -using: - -``` bash -$ sudo systemctl stop mpd.socket -$ sudo mpd --kill -``` - -Otherwise you might get the error message: - -``` bash -$ docker-compose -f docker-compose.yml -f docker-compose.linux.yml up -Starting mpd ... -Starting mpd ... error -(...) -Error starting userland proxy: listen tcp4 0.0.0.0:6600: bind: address already in use -``` - -Read these threads for details: [thread 1](https://unix.stackexchange.com/questions/456909/socket-already-in-use-but-is-not-listed-mpd) and [thread 2](https://stackoverflow.com/questions/5106674/error-address-already-in-use-while-binding-socket-with-address-but-the-port-num/5106755#5106755) - ### Mac -Remember, pulseaudio is a prerequisite. [Follow these -instructions](https://stackoverflow.com/a/50939994/1062438) for Mac -hosts. +1. [Install Docker & Compose (Mac)](https://docs.docker.com/docker-for-mac/install/) +2. [Install pulseaudio](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712) (Other references: [[1]](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html), [[2]](https://stackoverflow.com/a/50939994/1062438)) ``` bash // Build Images @@ -107,31 +62,32 @@ $ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml d ### Windows -1. Download - [pulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio/Ports/Windows/Support/) +1. Install [Docker & Compose (Windows)](https://docs.docker.com/docker-for-windows/install/) + +2. Download [pulseaudio](https://www.freedesktop.org/wiki/Software/PulseAudio/Ports/Windows/Support/) -2. Uncompress somewhere in your user folder +3. Uncompress somewhere in your user folder -3. Edit `$INSTALL_DIR/etc/pulse/default.pa` +4. Edit `$INSTALL_DIR/etc/pulse/default.pa` -4. Add the following line +5. Add the following line ``` bash load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1 ``` -5. Edit `$INSTALL_DIR/etc/pulse//etc/pulse/daemon.conf`, find the +6. Edit `$INSTALL_DIR/etc/pulse//etc/pulse/daemon.conf`, find the following line and change it to: ``` bash exit-idle-time = -1 ``` -6. Execute `$INSTALL_DIR/bin/pulseaudio.exe` +7. Execute `$INSTALL_DIR/bin/pulseaudio.exe` -7. Make sure Docker is running (e.g. start Docker Desktop) +8. Make sure Docker is running (e.g. start Docker Desktop) -8. Run `docker-compose` +9. Run `docker-compose` ``` bash // Build Images @@ -144,6 +100,46 @@ $ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.mac.yml d $ docker-compose -f docker/docker-compose.yml down ``` +### Linux + +1. Install Docker & Compose + * [Docker](https://docs.docker.com/engine/install/debian/) + * [Compose](https://docs.docker.com/compose/install/) +2. Make sure you don\'t use `sudo` to run your `docker-compose`. Check out +Docker\'s [post-installation guide](https://docs.docker.com/engine/install/linux-postinstall/) for more information. + +```bash +// Build Images +$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml build + +// Run Docker Environment +$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml up + +// Shuts down Docker containers and Docker network +$ docker-compose -f docker/docker-compose.yml -f docker/docker-compose.linux.yml down +``` + +Note: if you have `mpd` running on your system, you need to stop it +using: + +``` bash +$ sudo systemctl stop mpd.socket +$ sudo mpd --kill +``` + +Otherwise you might get the error message: + +``` bash +$ docker-compose -f docker-compose.yml -f docker-compose.linux.yml up +Starting mpd ... +Starting mpd ... error +(...) +Error starting userland proxy: listen tcp4 0.0.0.0:6600: bind: address already in use +``` + +Read these threads for details: [thread 1](https://unix.stackexchange.com/questions/456909/socket-already-in-use-but-is-not-listed-mpd) and [thread 2](https://stackoverflow.com/questions/5106674/error-address-already-in-use-while-binding-socket-with-address-but-the-port-num/5106755#5106755) + + ## Test & Develop The Dockerfile is defined to start all Phoniebox related services. diff --git a/documentation/developers/known-issues.md b/documentation/developers/known-issues.md new file mode 100644 index 000000000..1460c1cb4 --- /dev/null +++ b/documentation/developers/known-issues.md @@ -0,0 +1,24 @@ +# Known Issues + +## Installing `libzmq` in Docker fails + +To speed up the Docker build process, we are distributing pre-build versions of libzmq with drafts flag at the latest version. In case the download fails because the respective architecture build does not exist, you can build the version yourself. + +Add `build-essential` to be installed additionally with `apt-get`. Additionally, replace the command to download the pre-built library with the following command. + +``` +# Compile ZMQ +RUN cd ${HOME} && mkdir ${ZMQ_TMP_DIR} && cd ${ZMQ_TMP_DIR}; \ + wget https://github.com/zeromq/libzmq/releases/download/v${ZMQ_VERSION}/zeromq-${ZMQ_VERSION}.tar.gz -O libzmq.tar.gz; \ + tar -xzf libzmq.tar.gz; \ + rm -f libzmq.tar.gz; \ + zeromq-${ZMQ_VERSION}/configure --prefix=${ZMQ_PREFIX} --enable-drafts; \ + make && make install +``` + +## Configuration + +In `jukebox.yaml` (and all other config files): +Always use relative path from settingsfile `../../`, but do not use relative paths with `~/`. + +**Sole** exception is in `playermpd.mpd_conf`. diff --git a/documentation/developers/libzmq.md b/documentation/developers/libzmq.md new file mode 100644 index 000000000..3407d971e --- /dev/null +++ b/documentation/developers/libzmq.md @@ -0,0 +1,68 @@ +# `libzmq` for Raspberry Pi + +## `libzmp` Releases + +The Jukebox requires `libzmq` to work properly. We provide downloadable builds to speed up the installation process of the Phoniebox. + +* https://github.com/pabera/libzmq/releases + +> [!NOTE] +> We can't use stable builds that are distributed by [zeromq](https://github.com/zeromq/libzmq/releases) directly because the Jukebox requires draft builds to support WebSockets. These [draft builds](https://pyzmq.readthedocs.io/en/latest/howto/draft.html) are not officially provided through zeromq for Raspberry Pi architecture (e.g. `armv6` or `armv7`). + +## Building `libzmp` + +If you need to update the `libzmq` version in the future, follow these steps. + +### Install Cross-Compilation Environment + +First, you need to install Dockcross. Dockcross provides Docker images for cross-compilation. + +1. **Pull the Dockcross Image**: For Raspberry Pi B, 4 or Zero 2 we need `linux-armv7`, for older models `linux-armv6`. The following example shows how to compile for `armv7` (32 bit, `arm32v7`). If you want to compile for another target, change the commands accordingly. For Docker Development environments, other targets like `arm64` or `x86_64` become relevant. + +```bash +docker pull dockcross/linux-armv7 +``` + +3. **Create a Dockcross Script**: After pulling the image, you create a Dockcross script which will be used to run the cross-compilation tools in the Docker container. + +```bash +docker run --rm dockcross/linux-armv7 > ./dockcross-linux-armv7 +chmod +x ./dockcross-linux-armv7 +``` + +This command creates a script named `dockcross-linux-armv7` in your current directory. + +### Cross-Compiling libzmq + +With Dockcross installed, you can now modify your `libzmq` compilation process to use the Dockcross environment. + +1. **Download `libzmq` Source**: Similar to your original process, download the `libzmq` source code: + +```bash +ZMQ_VERSION=4.3.5 +wget https://github.com/zeromq/libzmq/releases/download/v${ZMQ_VERSION}/zeromq-${ZMQ_VERSION}.tar.gz -O libzmq.tar.gz +tar -xzf libzmq.tar.gz +``` + +2. **Cross-Compilation using Dockcross**: + +Modify your build process to run inside the Dockcross environment: + +```bash +./dockcross-linux-armv7 bash -c '\ +cd zeromq-${ZMQ_VERSION} && \ +./configure --prefix=/usr/local --enable-drafts && \ +make -j$(nproc) && \ +make install DESTDIR=$(pwd)/../installed' +``` + +> [!NOTE] +> In the script above, you need to update ${ZMQ_VERSION} to the actual value as the script does not jave access to your host machine ENV variables. + +This command configures and builds `libzmq` inside the Docker container. The `DESTDIR` variable is used to specify where to install the files inside the container. + +3. **Compress the Compiled Binaries**: After compilation, the binaries are located in the `installed` directory inside your `zeromq-${ZMQ_VERSION}` directory. + +``` +tar -czvf libzmq5-armv7-${ZMQ_VERSION}.tar.gz -C installed/usr/local --exclude='.' include lib +``` diff --git a/documentation/developers/pyhton.md b/documentation/developers/pyhton.md new file mode 100644 index 000000000..31659ef3c --- /dev/null +++ b/documentation/developers/pyhton.md @@ -0,0 +1,18 @@ +# Python Development Notes + +## Prerequisites + +> [!NOTE] +> All Python scripts must be run within a [virtual environment](https://docs.python.org/3/library/venv.html) (`.venv`). All Python plugins are installed encapsulated within this environment. + +Before you can run Python code, you need to enable the virtual environment. On the Raspberry Pi, it's located in the project root `~/RPi-Jukebox-RFID/.venv`. Depending on your setup, the absolute path can vary. + +``` +$ source ~/RPi-Jukebox-RFID/.venv/bin/activate +``` + +If the virtual environment has been activated correctly, your terminal will now show a prefix (`.venv`). If you want to leave the venv again execute deactivate. + +``` +$ deactivate +``` diff --git a/documentation/content/developers/rfid/README.md b/documentation/developers/rfid/README.md similarity index 100% rename from documentation/content/developers/rfid/README.md rename to documentation/developers/rfid/README.md diff --git a/documentation/content/developers/rfid/basics.md b/documentation/developers/rfid/basics.md similarity index 85% rename from documentation/content/developers/rfid/basics.md rename to documentation/developers/rfid/basics.md index 5cd1d1e9f..7d557ed06 100644 --- a/documentation/content/developers/rfid/basics.md +++ b/documentation/developers/rfid/basics.md @@ -6,7 +6,7 @@ Cards placed on the reader trigger an action. An action may be any callable plugin function through the RPC with any arguments. Typically, this would be "play some folder", but can also be "activate shutdown timer", or "increase volume". This is configured in the -[Card Database](../../userguide/card-database.md). +[Card Database](../../builders/card-database.md). You may configure a single or even multiple parallel readers (of different or identical types). @@ -35,8 +35,7 @@ Readers operate on one of two different frequencies: 125kHz or 13.56 MHz. Make s ## Reader Configuration During the installation process, you can already configure a RFID -reader. To manually configure RFID reader(s), -[please run the tool](../coreapps.md#run_register_rfid_reader.py), (`src/jukebox/run_register_rfid_reader.py`). +reader. To manually configure RFID reader(s) run the [RFID reader configuration tool](../coreapps.md#RFID-Reader). It will generate a reader configuration file at `shared/settings/rfid.yaml`. You can re-run the tool to change the @@ -67,7 +66,7 @@ Indicates the Python package used for this reader. Filled by the RFID configurat #### config: -Filled by the [RFID configuration tool](../coreapps.md#run_register_rfid_reader.py) (`src/jukebox/run_register_rfid_reader.py`) based on default values and user input. After running the tool, you may manually change some settings here, as not everything can be configured through the tool. Note that re-running the tool will completely rewrite the configuration file. +Filled by the [RFID reader configuration tool](../coreapps.md#RFID-Reader) based on default values and user input. After running the tool, you may manually change some settings here, as not everything can be configured through the tool. Note that re-running the tool will completely rewrite the configuration file. #### same_id_delay: float \| integer @@ -79,7 +78,7 @@ For place-capable RFID readers enable dual action mode: a start action (e.g. pla #### card_removal_action: Dictionary -Executes the given function on card removal. Only relevant if place_not_swipe is true. The action is identical for all cards read on that reader. The removal-action can be set to ignored on a card-by-card basis. More on card action configurations in [RPC Commands](../userguide/rpc-commands.md). +Executes the given function on card removal. Only relevant if place_not_swipe is true. The action is identical for all cards read on that reader. The removal-action can be set to ignored on a card-by-card basis. More on card action configurations in [RPC Commands](../../builders/rpc-commands.md). > [!NOTE] > Developer's note: The reason for a unique removal action for all cards is that card triggering and card removal are happening in two separate threads. Removal needs to be in a time-out thread. Thus, we would need to transport information from one thread to another. This can be done of course but is not implemented (yet). Ignoring card removal is much easier and works for now. diff --git a/documentation/content/developers/rfid/genericusb.md b/documentation/developers/rfid/genericusb.md similarity index 77% rename from documentation/content/developers/rfid/genericusb.md rename to documentation/developers/rfid/genericusb.md index a9a6565b5..544f0245c 100644 --- a/documentation/content/developers/rfid/genericusb.md +++ b/documentation/developers/rfid/genericusb.md @@ -4,7 +4,7 @@ This module covers all types of USB-based RFID input readers. If you plan to connect multiple USB-based RFID readers to the Jukebox, make -sure to connect all of them before running the registration tool [run_register_rfid_reader.py](../coreapps.md). +sure to connect all of them before running the [RFID reader configuration tool](../coreapps.md#RFID-Reader). > [!NOTE] > The user needs to be part of the group \'input\' for evdev to work. This should usually be the case. However, a user can be added with: diff --git a/documentation/content/developers/rfid/mfrc522_spi.md b/documentation/developers/rfid/mfrc522_spi.md similarity index 96% rename from documentation/content/developers/rfid/mfrc522_spi.md rename to documentation/developers/rfid/mfrc522_spi.md index f7710b2b1..5dc46aab6 100644 --- a/documentation/content/developers/rfid/mfrc522_spi.md +++ b/documentation/developers/rfid/mfrc522_spi.md @@ -6,7 +6,7 @@ RC522 RFID reader via SPI connection. ## Installation -Run the [run_register_rfid_reader.py](../coreapps.md#run_register_rfid_reader.py) tool for guided +Run the [RFID reader configuration tool](../coreapps.md#RFID-Reader) for guided installation. ## Options diff --git a/documentation/content/developers/rfid/mock_reader.md b/documentation/developers/rfid/mock_reader.md similarity index 89% rename from documentation/content/developers/rfid/mock_reader.md rename to documentation/developers/rfid/mock_reader.md index da90d5405..5daee0ab5 100644 --- a/documentation/content/developers/rfid/mock_reader.md +++ b/documentation/developers/rfid/mock_reader.md @@ -6,7 +6,7 @@ machine - probably in a Python virtual environment. **place-capable**: yes -If you [mock the GPIO pins](../userguide/gpio.md), this GUI will show the GPIO devices. +If you [mock the GPIO pins](../../../src/jukebox/components/gpio/gpioz/README.rst#use-mock-pins), this GUI will show the GPIO devices. ![image](mock_reader.png) diff --git a/documentation/content/developers/rfid/mock_reader.png b/documentation/developers/rfid/mock_reader.png similarity index 100% rename from documentation/content/developers/rfid/mock_reader.png rename to documentation/developers/rfid/mock_reader.png diff --git a/documentation/content/developers/rfid/pn532_i2c.md b/documentation/developers/rfid/pn532_i2c.md similarity index 100% rename from documentation/content/developers/rfid/pn532_i2c.md rename to documentation/developers/rfid/pn532_i2c.md diff --git a/documentation/content/developers/rfid/rdm6300.md b/documentation/developers/rfid/rdm6300.md similarity index 100% rename from documentation/content/developers/rfid/rdm6300.md rename to documentation/developers/rfid/rdm6300.md diff --git a/documentation/developers/rfid/template_reader.md b/documentation/developers/rfid/template_reader.md new file mode 100644 index 000000000..6a7a8b0c3 --- /dev/null +++ b/documentation/developers/rfid/template_reader.md @@ -0,0 +1,42 @@ + +# Template Reader + +*Template for creating and integrating a new RFID Reader* + +> [!NOTE] +> For developers only + +This template provides the skeleton API for a new Reader. If you follow +the conventions outlined below, your new reader will be picked up +automatically There is no extra need to register the reader module with +the Phoniebox. Just re-run [RFID reader configuration tool](../coreapps.md#RFID-Reader). + +Follow the instructions in [template_new_reader.py](../../../src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py) + +Also have a look at the other reader subpackages to see how stuff works +with an example + +## File structure + +Your new reader is a python subpackage with these three mandatory files + +``` bash +components/rfid/hardware/awesome_reader/ + +- awesome_reader.py <-- The actual reader module + +- description.py <-- A description module w/o dependencies. Do not change the filename! + +- README.rst <-- The Readme +``` + +The module documentation must go into a separate file, called README.ME. + +## Conventions + +- Single reader per directory / subpackage +- reader module directory name and reader module file name must be + identical +- Obviously awesome_reader will be replaced with something more + descriptive. The naming scheme for the subpackage is + - \\_\\_\ + - e.g. generic_usb/generic_usb.py + - e.g. pn532_spi/pn532_spi.py + - ... diff --git a/documentation/content/developers/status.md b/documentation/developers/status.md similarity index 98% rename from documentation/content/developers/status.md rename to documentation/developers/status.md index 3cc8d7f47..eac874608 100644 --- a/documentation/content/developers/status.md +++ b/documentation/developers/status.md @@ -183,7 +183,7 @@ Topics marked _in progress_ are already in the process of implementation by comm ### GPIO -- [x] All done! Read the docs at [GPIO Recipes](#userguide/gpioz:GPIO Recipes)! +- [x] All done! Read the docs at [GPIO Recipes](#builders/gpioz:GPIO Recipes)! - [ ] USB Buttons: It's a different category as it works similar to the RFID cards (in progress) ### WLAN @@ -227,6 +227,7 @@ Topics marked _in progress_ are already in the process of implementation by comm - [x] Enable/Disable Auto-Hotspot - [x] `run_npm_build` script - [x] Must consider `export NODE_OPTIONS=--max-old-space-size=512` +- [ ] Upload audio files via WebUI https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/2138 ## Installation Procedure diff --git a/installation/README.md b/installation/README.md index aff262bd8..178f1b7be 100644 --- a/installation/README.md +++ b/installation/README.md @@ -3,18 +3,15 @@ ## Logging - Bash Script output rules ```bash -Output to both console and logfile: "$ command | tee /dev/fd/3" -Output to console only "$ command 1>&3" -Output to logfile only: "$ command" -No output to both console and logfile: "$ command > /dev/null" +run_and_print_lc "Run a command and log its output to both console and logfile" +print_lc "This message will be logged to both console and logfile" +print_c "This message will only be logged to the console" +log "This message will only be logged to the logfile" +clear_c "Clears the console screen" ``` [Learn more about bash script outputs](https://stackoverflow.com/questions/18460186/writing-outputs-to-log-file-and-console) -## Quick Installation +## Installation -Note: Replace the branch in this command to be the one you like to install depending on your needs. Release branch is preset. - -```bash -cd; bash <(wget -qO- https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID/future3/main/installation/install-jukebox.sh) -``` +[Install Phoniebox software](../documentation/builders/installation.md#install-phoniebox-software) diff --git a/installation/includes/00_constants.sh b/installation/includes/00_constants.sh index e19dd1327..380e1de2e 100644 --- a/installation/includes/00_constants.sh +++ b/installation/includes/00_constants.sh @@ -1,7 +1,11 @@ RPI_BOOT_CONFIG_FILE="/boot/config.txt" +RPI_BOOT_CMDLINE_FILE="/boot/cmdline.txt" SHARED_PATH="${INSTALLATION_PATH}/shared" SETTINGS_PATH="${SHARED_PATH}/settings" SYSTEMD_USR_PATH="/usr/lib/systemd/user" +VIRTUAL_ENV="${INSTALLATION_PATH}/.venv" +# Do not change this directory! It must match MPDs expectation where to find the user configuration +MPD_CONF_PATH="${HOME}/.config/mpd/mpd.conf" # The default upstream user, release branch, and develop branch # These are used to prepare the repo for developers diff --git a/installation/includes/01_default_config.sh b/installation/includes/01_default_config.sh index eb6e45a91..fa1bafb61 100644 --- a/installation/includes/01_default_config.sh +++ b/installation/includes/01_default_config.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE=false +BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE=${BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE:-"false"} ENABLE_STATIC_IP=true DISABLE_IPv6=true ENABLE_AUTOHOTSPOT=false @@ -11,7 +11,9 @@ DISABLE_SSH_QOS=true DISABLE_BOOT_SCREEN=true DISABLE_BOOT_LOGS_PRINT=true SETUP_MPD=true +ENABLE_MPD_OVERWRITE_INSTALL=true UPDATE_RASPI_OS=${UPDATE_RASPI_OS:-"true"} +ENABLE_RFID_READER=true ENABLE_SAMBA=true ENABLE_WEBAPP=true ENABLE_KIOSK_MODE=false diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index 9ad8e71b8..7520241c6 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Helpers +### Helpers # $1->start, $2->end calc_runtime_and_print() { @@ -9,7 +9,7 @@ calc_runtime_and_print() { ((m=(${runtime}%3600)/60)) ((s=${runtime}%60)) - echo "Done in ${h}h ${m}m ${s}s." + echo "Done in ${h}h ${m}m ${s}s" } run_with_timer() { @@ -17,42 +17,279 @@ run_with_timer() { $1; # Executes the function passed as an argument - calc_runtime_and_print time_start $(date +%s) | tee /dev/fd/3 - echo "--------------------------------------" + run_and_print_lc calc_runtime_and_print time_start $(date +%s) } -_download_file_from_google_drive() { - GD_SHARING_ID=${1} - TAR_FILENAME=${2} - wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?export=download&confirm=$(wget --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?export=download&id=${GD_SHARING_ID}' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=${GD_SHARING_ID}" -O ${TAR_FILENAME} && rm -rf /tmp/cookies.txt - echo "Downloaded from Google Drive ID ${GD_SHARING_ID} into ${TAR_FILENAME}" +run_with_log_frame() { + local time_start=$(date +%s); + local description="$2" + log "\n\n" + log "#########################################################" + print_lc "${description}" + + $1; # Executes the function passed as an argument + + local done_in=$(calc_runtime_and_print time_start $(date +%s)) + log "\n${done_in} - ${description}" + log "#########################################################" } -get_onboard_audio() { - if grep -q -E "^dtparam=([^,]*,)*audio=(on|true|yes|1).*" ${RPI_BOOT_CONFIG_FILE} - then - echo 1 +get_architecture() { + local arch="" + if [ "$(uname -m)" = "armv7l" ]; then + arch="armv7" + elif [ "$(uname -m)" = "armv6l" ]; then + arch="armv6" + elif [ "$(uname -m)" = "aarch64" ]; then + arch="arm64" else - echo 0 + arch="$(uname -m)" fi -} -check_os_type() { - # Check if current distro is a 32 bit version - # Support for 64 bit Distros has not been checked (or precisely: is known not to work) - # All RaspianOS versions report as machine "armv6l" or "armv7l", if 32 bit (even the ARMv8 cores!) + echo $arch +} - local os_type - os_type=$(uname -m) +get_version_string() { + local python_file="$1" + local version_major + local version_minor + local version_patch - echo -e "\nChecking OS type '$os_type'" | tee /dev/fd/3 + version_major=$(grep 'VERSION_MAJOR\s*=\s*[0-9]*' "${python_file}" | awk -F= '{print $2}' | tr -d ' ') + version_minor=$(grep 'VERSION_MINOR\s*=\s*[0-9]*' "${python_file}" | awk -F= '{print $2}' | tr -d ' ') + version_patch=$(grep 'VERSION_PATCH\s*=\s*[0-9]*' "${python_file}" | awk -F= '{print $2}' | tr -d ' ') - if [[ $os_type == "armv7l" || $os_type == "armv6l" ]]; then - echo -e " ... OK!\n" | tee /dev/fd/3 + if [ -n "$version_major" ] && [ -n "$version_minor" ] && [ -n "$version_patch" ]; then + local version_string="${version_major}.${version_minor}.${version_patch}" + echo ${version_string} else - echo "ERROR: Only 32 bit operating systems supported. Please use a 32bit version of RaspianOS!" | tee /dev/fd/3 - echo "You can fix this problem for 64bit kernels: https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/2041" | tee /dev/fd/3 - exit 1 + exit_on_error "ERROR: Unable to extract version information from ${python_file}" fi +} + +### Verify helpers +print_verify_installation() { + log "\n + ------------------------------------------------------- + Check installation +" +} + +# Check if the file(s) exists +verify_files_exists() { + local files="$@" + log " Verify '${files}' exists" + + if [[ -z "${files}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + for file in $files + do + test ! -f ${file} && exit_on_error "ERROR: '${file}' does not exists or is not a file!" + done + log " CHECK" +} + +# Check if the dir(s) exists +verify_dirs_exists() { + local dirs="$@" + log " Verify '${dirs}' exists" + + if [[ -z "${dirs}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + for dir in $dirs + do + test ! -d ${dir} && exit_on_error "ERROR: '${dir}' does not exists or is not a dir!" + done + log " CHECK" +} + +# Check if the file(s) has/have the expected owner and modifications +verify_files_chmod_chown() { + local mod_expected=$1 + local user_expected=$2 + local group_expected=$3 + local files="${@:4}" + log " Verify '${mod_expected}' '${user_expected}:${group_expected}' is set for '${files}'" + + if [[ -z "${mod_expected}" || -z "${user_expected}" || -z "${group_expected}" || -z "${files}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + for file in $files + do + test ! -f ${file} && exit_on_error "ERROR: '${file}' does not exists or is not a file!" + + mod_actual=$(stat --format '%a' "${file}") + user_actual=$(stat -c '%U' "${file}") + group_actual=$(stat -c '%G' "${file}") + test ! "${mod_expected}" -eq "${mod_actual}" && exit_on_error "ERROR: '${file}' actual mod '${mod_actual}' differs from expected '${mod_expected}'!" + test ! "${user_expected}" == "${user_actual}" && exit_on_error "ERROR: '${file}' actual owner '${user_actual}' differs from expected '${user_expected}'!" + test ! "${group_expected}" == "${group_actual}" && exit_on_error "ERROR: '${file}' actual group '${group_actual}' differs from expected '${group_expected}'!" + done + log " CHECK" +} + +# Check if the dir(s) has/have the expected owner and modifications +verify_dirs_chmod_chown() { + local mod_expected=$1 + local user_expected=$2 + local group_expected=$3 + local dirs="${@:4}" + log " Verify '${mod_expected}' '${user_expected}:${group_expected}' is set for '${dirs}'" + + if [[ -z "${mod_expected}" || -z "${user_expected}" || -z "${group_expected}" || -z "${dirs}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + for dir in $dirs + do + test ! -d ${dir} && exit_on_error "ERROR: '${dir}' does not exists or is not a dir!" + + mod_actual=$(stat --format '%a' "${dir}") + user_actual=$(stat -c '%U' "${dir}") + group_actual=$(stat -c '%G' "${dir}") + test ! "${mod_expected}" -eq "${mod_actual}" && exit_on_error "ERROR: '${dir}' actual mod '${mod_actual}' differs from expected '${mod_expected}'!" + test ! "${user_expected}" == "${user_actual}" && exit_on_error "ERROR: '${dir}' actual owner '${user_actual}' differs from expected '${user_expected}'!" + test ! "${group_expected}" == "${group_actual}" && exit_on_error "ERROR: '${dir}' actual group '${group_actual}' differs from expected '${group_expected}'!" + done + log " CHECK" +} + +verify_file_contains_string() { + local string="$1" + local file="$2" + log " Verify '${string}' found in '${file}'" + + if [[ -z "${string}" || -z "${file}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + if [[ ! $(grep -iw "${string}" "${file}") ]]; then + exit_on_error "ERROR: '${string}' not found in '${file}'" + fi + log " CHECK" +} + +verify_file_contains_string_once() { + local string="$1" + local file="$2" + log " Verify '${string}' found in '${file}'" + + if [[ -z "${string}" || -z "${file}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + local file_contains_string_count=$(grep -oiw "${string}" "${file}" | wc -l) + if [ "$file_contains_string_count" -lt 1 ]; then + exit_on_error "ERROR: '${string}' not found in '${file}'" + elif [ "$file_contains_string_count" -gt 1 ]; then + exit_on_error "ERROR: '${string}' found more than once in '${file}'" + fi + log " CHECK" +} + +verify_service_state() { + local service="$1" + local desired_state="$2" + local option="${3:+$3 }" # optional, dont't quote in next call! + log " Verify service '${option}${service}' is '${desired_state}'" + + if [[ -z "${service}" || -z "${desired_state}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + local actual_state=$(systemctl is-active ${option}${service}) + if [[ ! "${actual_state}" == "${desired_state}" ]]; then + exit_on_error "ERROR: service '${option}${service}' is not '${desired_state}' (state: '${actual_state}')." + fi + log " CHECK" +} + +verify_service_enablement() { + local service="$1" + local desired_enablement="$2" + local option="${3:+$3 }" # optional, dont't quote in next call! + log " Verify service ${option}${service} is ${desired_enablement}" + + if [[ -z "${service}" || -z "${desired_enablement}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + local actual_enablement=$(systemctl is-enabled ${option}${service}) + if [[ ! "${actual_enablement}" == "${desired_enablement}" ]]; then + exit_on_error "ERROR: service ${option}${service} is not ${desired_enablement} (state: ${actual_enablement})." + fi + log " CHECK" +} + +verify_optional_service_enablement() { + local service="$1" + local desired_enablement="$2" + local option="${3:+$3 }" # optional, dont't quote in next call! + log " Verify service ${option}${service} is ${desired_enablement}" + + if [[ -z "${service}" || -z "${desired_enablement}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + local actual_enablement=$(systemctl is-enabled ${option}${service}) 2>/dev/null + if [[ -z "${actual_enablement}" ]]; then + log " INFO: optional service ${option}${service} is not installed." + elif [[ "${actual_enablement}" == "static" ]]; then + log " INFO: optional service ${option}${service} is set static." + elif [[ ! "${actual_enablement}" == "${desired_enablement}" ]]; then + exit_on_error "ERROR: service ${option}${service} is not ${desired_enablement} (state: ${actual_enablement})." + fi + log " CHECK" +} + +# Reads a textfile and returns all lines as args. +# Does filter out comments, egg-prefixes and version suffixes +# Arguments: +# 1 : textfile to read +get_args_from_file() { + local package_file="$1" + sed 's/.*#egg=//g' ${package_file} | sed -E 's/(#|=|>|<).*//g' | xargs echo +} + +# Check if all passed packages are installed. Fail on first missing. +verify_apt_packages() { + local packages="$@" + log " Verify packages are installed: '${packages}'" + + if [[ -z "${packages}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + local apt_list_installed=$(apt -qq list --installed 2>/dev/null) + for package in ${packages} + do + if [[ ! $(echo "${apt_list_installed}" | grep -i "^${package}/.*installed") ]]; then + exit_on_error "ERROR: ${package} is not installed" + fi + done + log " CHECK" +} + +# Check if all passed modules are installed. Fail on first missing. +verify_pip_modules() { + local modules="$@" + log " Verify modules are installed: '${modules}'" + + if [[ -z "${modules}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + local pip_list_installed=$(pip list 2>/dev/null) + for module in ${modules} + do + if [[ ! $(echo "${pip_list_installed}" | grep -i "^${module} ") ]]; then + exit_on_error "ERROR: ${module} is not installed" + fi + done + log " CHECK" } diff --git a/installation/includes/03_welcome.sh b/installation/includes/03_welcome.sh index 7aeaa56ab..5b3ee84be 100644 --- a/installation/includes/03_welcome.sh +++ b/installation/includes/03_welcome.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash welcome() { - clear 1>&3 - echo "######################################################### + clear_c + print_c "######################################################### # # # ___ __ ______ _ __________ ____ __ _ _ # # / _ \/ // / __ \/ |/ / _/ __/( _ \ / \( \/ ) # @@ -29,16 +29,16 @@ in a separate SSH session: cd; tail -f ${INSTALLATION_LOGFILE} Let's set up your Phoniebox. -Do you want to start the installation? [Y/n]" 1>&3 +Do you want to start the installation? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) exit ;; *) - echo "Starting installation + print_c "Starting installation --------------------- -" 1>&3 +" ;; esac } diff --git a/installation/includes/04_cleanup.sh b/installation/includes/04_cleanup.sh index fd714132f..d6e39266c 100644 --- a/installation/includes/04_cleanup.sh +++ b/installation/includes/04_cleanup.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash -cleanup() { - sudo rm -rf /var/lib/apt/lists/* +_run_cleanup() { + sudo rm -rf /var/lib/apt/lists/* +} - echo "DONE: cleanup" +cleanup() { + run_with_log_frame _run_cleanup "Cleanup" } diff --git a/installation/includes/05_finish.sh b/installation/includes/05_finish.sh index 50c2f6d9d..22ba6ae80 100644 --- a/installation/includes/05_finish.sh +++ b/installation/includes/05_finish.sh @@ -2,7 +2,7 @@ finish() { local local_hostname=$(hostname) - echo -e "####################### FINISHED ######################## + print_lc "####################### FINISHED ######################## Installation complete! @@ -14,41 +14,20 @@ Your SSH connection will disconnect. After the reboot, you can access the WebApp in your browser at http://${local_hostname}.local or http://${CURRENT_IP_ADDRESS} Don't forget to upload files. - -Do you want to reboot now? [Y/n]" 1>&3 +" +print_c "Do you want to reboot now? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) - echo "Reboot aborted" | tee /dev/fd/3 - echo "DONE: finish" + print_lc "Reboot aborted" + log "DONE: finish" exit ;; *) - echo "Rebooting ..." | tee /dev/fd/3 - echo "DONE: finish" + print_lc "Rebooting ..." + log "DONE: finish" sudo reboot ;; esac } - -# Generic emergency error handler that exits the script immediately -# Print additional custom message if passed as first argument -# Examples: -# cd some-dir || exit_on_error -# cd some-dir || exit_on_error "During installation of some" -exit_on_error () { - - echo -e "\n****************************************" | tee /dev/fd/3 - echo "ERROR OCCURRED! -A non-recoverable error occurred. -Check install log for details:" | tee /dev/fd/3 - echo "$INSTALLATION_LOGFILE" | tee /dev/fd/3 - echo "****************************************" | tee /dev/fd/3 - if [[ -n $1 ]]; then - echo "$1" | tee /dev/fd/3 - echo "****************************************" | tee /dev/fd/3 - fi - echo "Abort!" - exit 1 -} diff --git a/installation/install-jukebox.sh b/installation/install-jukebox.sh index 44b03fca0..84827f99d 100755 --- a/installation/install-jukebox.sh +++ b/installation/install-jukebox.sh @@ -20,78 +20,137 @@ GIT_URL="https://github.com/${GIT_USER}/${GIT_REPO_NAME}" echo GIT_BRANCH $GIT_BRANCH echo GIT_URL $GIT_URL -CURRENT_USER="${SUDO_USER:-$USER}" +CURRENT_USER="${SUDO_USER:-$(whoami)}" +CURRENT_USER_GROUP=$(id -gn "$CURRENT_USER") HOME_PATH=$(getent passwd "$CURRENT_USER" | cut -d: -f6) -echo "Current User: $CURRENT_USER" -echo "User home dir: $HOME_PATH" INSTALLATION_PATH="${HOME_PATH}/${GIT_REPO_NAME}" INSTALL_ID=$(date +%s) +INSTALLATION_LOGFILE="${HOME_PATH}/INSTALL-${INSTALL_ID}.log" + +# Manipulate file descriptor for logging +_setup_logging(){ + if [ "$CI_RUNNING" == "true" ]; then + exec 3>&1 2>&1 + else + exec 3>&1 1>>"${INSTALLATION_LOGFILE}" 2>&1 || { echo "ERROR: Cannot create log file."; exit 1; } + fi + echo "Log start: ${INSTALL_ID}" +} + +# Function to log to both console and logfile +print_lc() { + local message="$1" + echo -e "$message" | tee /dev/fd/3 +} + +# Function to log to logfile only +log() { + local message="$1" + echo -e "$message" +} -checkPrerequisite() { - #currently the user 'pi' is mandatory - #https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/1785 - if [ "${CURRENT_USER}" != "pi" ]; then - echo - echo "ERROR: User must be 'pi'!" - echo " Other usernames are currently not supported." - echo " Please check the wiki for further information" - exit 2 +# Function to run a command where the output will be logged to both console and logfile +run_and_print_lc() { + "$@" | tee /dev/fd/3 +} + +# Function to log to console only +print_c() { + local message="$1" + echo -e "$message" 1>&3 +} + +# Function to clear console screen +clear_c() { + clear 1>&3 +} + +# Generic emergency error handler that exits the script immediately +# Print additional custom message if passed as first argument +# Examples: +# a command || exit_on_error +# a command || exit_on_error "Execution of command failed" +exit_on_error () { + print_lc "\n****************************************" + print_lc "ERROR OCCURRED! +A non-recoverable error occurred. +Check install log for details:" + print_lc "$INSTALLATION_LOGFILE" + print_lc "****************************************" + if [[ -n $1 ]]; then + print_lc "$1" + print_lc "****************************************" fi + log "Abort!" + exit 1 +} - if [ "${HOME_PATH}" != "/home/pi" ]; then - echo - echo "ERROR: HomeDir must be '/home/pi'!" - echo " Other usernames are currently not supported." - echo " Please check the wiki for further information" - exit 2 +# Check if current distro is a 32 bit version +# Support for 64 bit Distros has not been checked (or precisely: is known not to work) +# All RaspianOS versions report as machine "armv6l" or "armv7l", if 32 bit (even the ARMv8 cores!) +_check_os_type() { + local os_type=$(uname -m) + + print_lc "\nChecking OS type '$os_type'" + + if [[ $os_type == "armv7l" || $os_type == "armv6l" ]]; then + print_lc " ... OK!\n" + else + print_lc "ERROR: Only 32 bit operating systems supported. Please use a 32bit version of RaspianOS!" + print_lc "You can fix this problem for 64bit kernels: https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/2041" + exit 1 fi } -download_jukebox_source() { +_download_jukebox_source() { + log "#########################################################" + print_c "Downloading Phoniebox software from Github ..." + print_lc "Download Source: ${GIT_URL}/${GIT_BRANCH}" + + cd "${HOME_PATH}" || exit_on_error "ERROR: Changing to home dir failed." wget -qO- "${GIT_URL}/tarball/${GIT_BRANCH}" | tar xz # Use case insensitive search/sed because user names in Git Hub are case insensitive - GIT_REPO_DOWNLOAD=$(find . -maxdepth 1 -type d -iname "${GIT_USER}-${GIT_REPO_NAME}-*") - echo "GIT REPO DOWNLOAD = $GIT_REPO_DOWNLOAD" - GIT_HASH=$(echo "$GIT_REPO_DOWNLOAD" | sed -rn "s/.*${GIT_USER}-${GIT_REPO_NAME}-([0-9a-fA-F]+)/\1/ip") + local git_repo_download=$(find . -maxdepth 1 -type d -iname "${GIT_USER}-${GIT_REPO_NAME}-*") + log "GIT REPO DOWNLOAD = $git_repo_download" + GIT_HASH=$(echo "$git_repo_download" | sed -rn "s/.*${GIT_USER}-${GIT_REPO_NAME}-([0-9a-fA-F]+)/\1/ip") # Save the git hash for this particular download for later git repo initialization - echo "GIT HASH = $GIT_HASH" - if [[ -z ${GIT_REPO_DOWNLOAD} ]]; then - echo "ERROR in finding git download. Panic." - exit 1 + log "GIT HASH = $GIT_HASH" + if [[ -z "${git_repo_download}" ]]; then + exit_on_error "ERROR: Couldn't find git download." fi - if [[ -z ${GIT_HASH} ]]; then - echo "ERROR in determining git hash from download. Panic." - exit 1 + if [[ -z "${GIT_HASH}" ]]; then + exit_on_error "ERROR: Couldn't determine git hash from download." fi - mv "$GIT_REPO_DOWNLOAD" "$GIT_REPO_NAME" - unset GIT_REPO_DOWNLOAD + mv "$git_repo_download" "$GIT_REPO_NAME" + log "\nDONE: Downloading Phoniebox software from Github" + log "#########################################################" } +_load_sources() { + # Load / Source dependencies + for i in "${INSTALLATION_PATH}"/installation/includes/*; do + source "$i" + done -### CHECK PREREQUISITE -checkPrerequisite - -### RUN INSTALLATION -INSTALLATION_LOGFILE="${HOME_PATH}/INSTALL-${INSTALL_ID}.log" -exec 3>&1 1>>"${INSTALLATION_LOGFILE}" 2>&1 || { echo "Cannot create log file. Panic."; exit 1; } -echo "Log start: ${INSTALL_ID}" + for j in "${INSTALLATION_PATH}"/installation/routines/*; do + source "$j" + done +} -clear 1>&3 -echo "Downloading Phoniebox software from Github ..." 1>&3 -echo "Download Source: ${GIT_URL}/${GIT_BRANCH}" | tee /dev/fd/3 +### SETUP LOGGING +_setup_logging -download_jukebox_source -cd "${INSTALLATION_PATH}" || { echo "ERROR in changing to install dir. Panic."; exit 1; } +### CHECK PREREQUISITE +_check_os_type -# Load / Source dependencies -for i in "${INSTALLATION_PATH}"/installation/includes/*; do - source "$i" -done +### RUN INSTALLATION +log "Current User: $CURRENT_USER" +log "User home dir: $HOME_PATH" -for j in "${INSTALLATION_PATH}"/installation/routines/*; do - source "$j" -done +_download_jukebox_source +cd "${INSTALLATION_PATH}" || exit_on_error "ERROR: Changing to install dir failed." +_load_sources welcome run_with_timer install diff --git a/installation/routines/customize_options.sh b/installation/routines/customize_options.sh index d5ac7c547..4007b88f5 100644 --- a/installation/routines/customize_options.sh +++ b/installation/routines/customize_options.sh @@ -8,15 +8,15 @@ _option_static_ip() { CURRENT_GATEWAY=$(echo "${CURRENT_ROUTE}" | awk '{ print $3; exit }') CURRENT_INTERFACE=$(echo "${CURRENT_ROUTE}" | awk '{ print $5; exit }') CURRENT_IP_ADDRESS=$(echo "${CURRENT_ROUTE}" | awk '{ print $7; exit }') - clear 1>&3 - echo "----------------------- STATIC IP ----------------------- + clear_c + print_c "----------------------- STATIC IP ----------------------- Setting a static IP will save a lot of start up time. The static adress will be '${CURRENT_IP_ADDRESS}' from interface '${CURRENT_INTERFACE}' with the gateway '${CURRENT_GATEWAY}'. -Set a static IP? [Y/n]" 1>&3 +Set a static IP? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) @@ -25,18 +25,18 @@ Set a static IP? [Y/n]" 1>&3 *) ;; esac - echo "ENABLE_STATIC_IP=${ENABLE_STATIC_IP}" + log "ENABLE_STATIC_IP=${ENABLE_STATIC_IP}" } _option_ipv6() { # DISABLE_IPv6 - clear 1>&3 - echo "------------------------- IP V6 ------------------------- + clear_c + print_c "------------------------- IP V6 ------------------------- IPv6 is only needed if you intend to use it. Otherwise it can be disabled. -Do you want to disable IPv6? [Y/n]" 1>&3 +Do you want to disable IPv6? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) @@ -45,19 +45,19 @@ Do you want to disable IPv6? [Y/n]" 1>&3 *) ;; esac - echo "DISABLE_IPv6=${DISABLE_IPv6}" + log "DISABLE_IPv6=${DISABLE_IPv6}" } _option_autohotspot() { # ENABLE_AUTOHOTSPOT - clear 1>&3 - echo "---------------------- AUTOHOTSPOT ---------------------- + clear_c + print_c "---------------------- AUTOHOTSPOT ---------------------- When enabled, this service spins up a WiFi hotspot when the Phoniebox is unable to connect to a known WiFi. This way you can still access it. -Do you want to enable an Autohotpot? [y/N]" 1>&3 +Do you want to enable an Autohotpot? [y/N]" read -r response case "$response" in [yY][eE][sS]|[yY]) @@ -68,13 +68,13 @@ Do you want to enable an Autohotpot? [y/N]" 1>&3 esac if [ "$ENABLE_AUTOHOTSPOT" = true ]; then - echo "Do you want to set a custom Password? (default: ${AUTOHOTSPOT_PASSWORD}) [y/N] " 1>&3 + print_c "Do you want to set a custom Password? (default: ${AUTOHOTSPOT_PASSWORD}) [y/N] " read -r response_pw_q case "$response_pw_q" in [yY][eE][sS]|[yY]) while [ $(echo ${response_pw}|wc -m) -lt 8 ] do - echo "Please type the new password (at least 8 character)." 1>&3 + print_c "Please type the new password (at least 8 character)." read -r response_pw done AUTOHOTSPOT_PASSWORD="${response_pw}" @@ -84,29 +84,27 @@ Do you want to enable an Autohotpot? [y/N]" 1>&3 esac if [ "$ENABLE_STATIC_IP" = true ]; then - echo "Wifi hotspot cannot be enabled with static IP. Disabling static IP configuration." 1>&3 - echo "--------------------- - " 1>&3 + print_c "Wifi hotspot cannot be enabled with static IP. Disabling static IP configuration." ENABLE_STATIC_IP=false - echo "ENABLE_STATIC_IP=${ENABLE_STATIC_IP}" + log "ENABLE_STATIC_IP=${ENABLE_STATIC_IP}" fi fi - echo "ENABLE_AUTOHOTSPOT=${ENABLE_AUTOHOTSPOT}" + log "ENABLE_AUTOHOTSPOT=${ENABLE_AUTOHOTSPOT}" if [ "$ENABLE_AUTOHOTSPOT" = true ]; then - echo "AUTOHOTSPOT_PASSWORD=${AUTOHOTSPOT_PASSWORD}" + log "AUTOHOTSPOT_PASSWORD=${AUTOHOTSPOT_PASSWORD}" fi } _option_bluetooth() { # DISABLE_BLUETOOTH - clear 1>&3 - echo "----------------------- BLUETOOTH ----------------------- + clear_c + print_c "----------------------- BLUETOOTH ----------------------- Turning off Bluetooth will save energy and start up time, if you do not plan to use it. -Do you want to disable Bluetooth? [Y/n]" 1>&3 +Do you want to disable Bluetooth? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) @@ -115,41 +113,88 @@ Do you want to disable Bluetooth? [Y/n]" 1>&3 *) ;; esac - echo "DISABLE_BLUETOOTH=${DISABLE_BLUETOOTH}" + log "DISABLE_BLUETOOTH=${DISABLE_BLUETOOTH}" +} + +_option_mpd() { + clear_c + if [[ "$SETUP_MPD" == true ]]; then + if [[ -f "${MPD_CONF_PATH}" || -f "${SYSTEMD_USR_PATH}/mpd.service" ]]; then + print_c "-------------------------- MPD -------------------------- + +It seems there is a MPD already installed. +Note: It is important that MPD runs as a user service! +Would you like to overwrite your configuration? [Y/n]" + read -r response + case "$response" in + [nN][oO]|[nN]) + ENABLE_MPD_OVERWRITE_INSTALL=false + ;; + *) + ;; + esac + fi + fi + + log "SETUP_MPD=${SETUP_MPD}" + if [ "$SETUP_MPD" == true ]; then + log "ENABLE_MPD_OVERWRITE_INSTALL=${ENABLE_MPD_OVERWRITE_INSTALL}" + fi +} + +_option_rfid_reader() { + # ENABLE_RFID_READER + clear_c + print_c "---------------------- RFID READER ---------------------- + +Phoniebox can be controlled with rfid cards/tags, if you +have a rfid reader connected. +Choose yes to setup a reader. You get prompted for +the type selection and configuration later on. + +Do you want to setup a rfid reader? [Y/n]" + read -r response + case "$response" in + [nN][oO]|[nN]) + ENABLE_RFID_READER=false + ;; + *) + ;; + esac + log "ENABLE_RFID_READER=${ENABLE_RFID_READER}" } _option_samba() { # ENABLE_SAMBA - clear 1>&3 - echo "------------------------- SAMBA ------------------------- + clear_c + print_c "------------------------- SAMBA ------------------------- Samba is required to conveniently copy files to your Phoniebox via a network share. If you don't need it, feel free to skip the installation. If you are unsure, stick to YES! -Do you want to install Samba? [Y/n]" 1>&3 +Do you want to install Samba? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) ENABLE_SAMBA=false - ENABLE_KIOSK_MODE=false ;; *) ;; esac - echo "ENABLE_SAMBA=${ENABLE_SAMBA}" + log "ENABLE_SAMBA=${ENABLE_SAMBA}" } _option_webapp() { # ENABLE_WEBAPP - clear 1>&3 - echo "------------------------ WEBAPP ------------------------- + clear_c + print_c "------------------------ WEBAPP ------------------------- This is only required if you want to use a graphical interface to manage your Phoniebox! -Would you like to install the web application? [Y/n]" 1>&3 +Would you like to install the web application? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) @@ -159,20 +204,20 @@ Would you like to install the web application? [Y/n]" 1>&3 *) ;; esac - echo "ENABLE_WEBAPP=${ENABLE_WEBAPP}" + log "ENABLE_WEBAPP=${ENABLE_WEBAPP}" } _option_kiosk_mode() { # ENABLE_KIOSK_MODE - clear 1>&3 - echo "----------------------- KIOSK MODE ---------------------- + clear_c + print_c "----------------------- KIOSK MODE ---------------------- If you have a screen attached to your RPi, this will launch the web application right after boot. It will only install the necessary xserver dependencies and not the entire RPi desktop environment. -Would you like to enable the Kiosk Mode? [y/N]" 1>&3 +Would you like to enable the Kiosk Mode? [y/N]" read -r response case "$response" in [yY][eE][sS]|[yY]) @@ -181,18 +226,18 @@ Would you like to enable the Kiosk Mode? [y/N]" 1>&3 *) ;; esac - echo "ENABLE_KIOSK_MODE=${ENABLE_KIOSK_MODE}" + log "ENABLE_KIOSK_MODE=${ENABLE_KIOSK_MODE}" } _options_update_raspi_os() { # UPDATE_RASPI_OS - clear 1>&3 - echo "----------------------- UPDATE OS ----------------------- + clear_c + print_c "----------------------- UPDATE OS ----------------------- This shall be done eventually, but increases the installation time a lot. -Would you like to update the operating system? [Y/n]" 1>&3 +Would you like to update the operating system? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) @@ -201,14 +246,14 @@ Would you like to update the operating system? [Y/n]" 1>&3 *) ;; esac - echo "UPDATE_RASPI_OS=${UPDATE_RASPI_OS}" + log "UPDATE_RASPI_OS=${UPDATE_RASPI_OS}" } _option_disable_onboard_audio() { # Disable BCM on-chip audio (typically Headphones) # not needed when external sound card is sued - clear 1>&3 - echo "--------------------- ON-CHIP AUDIO --------------------- + clear_c + print_c "--------------------- ON-CHIP AUDIO --------------------- If you are using an external sound card (e.g. USB, HifiBerry, PirateAudio, etc), we recommend to disable @@ -222,7 +267,7 @@ We will do our best not to mess anything up. However, a backup copy will be written to ${DISABLE_ONBOARD_AUDIO_BACKUP} ) -Disable Pi's on-chip audio (headphone / jack output)? [y/N]" 1>&3 +Disable Pi's on-chip audio (headphone / jack output)? [y/N]" read -r response case "$response" in [yY][eE][sS]|[yY]) @@ -231,52 +276,59 @@ Disable Pi's on-chip audio (headphone / jack output)? [y/N]" 1>&3 *) ;; esac - echo "DISABLE_ONBOARD_AUDIO=${DISABLE_ONBOARD_AUDIO}" + log "DISABLE_ONBOARD_AUDIO=${DISABLE_ONBOARD_AUDIO}" } _option_webapp_devel_build() { # Let's detect if we are on the official release branch - if [[ "$GIT_BRANCH" != "${GIT_BRANCH_RELEASE}" || "$GIT_USER" != "$GIT_UPSTREAM_USER" ]]; then + if [[ "$GIT_BRANCH" != "${GIT_BRANCH_RELEASE}" || "$GIT_USER" != "$GIT_UPSTREAM_USER" || "$CI_RUNNING" == "true" ]]; then ENABLE_INSTALL_NODE=true # Unless ENABLE_WEBAPP_PROD_DOWNLOAD is forced to true by user override, do not download a potentially stale build - if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" = "release-only" ]]; then + if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" == "release-only" ]]; then ENABLE_WEBAPP_PROD_DOWNLOAD=false fi - if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" = false ]]; then - clear 1>&3 - echo "--------------------- WEBAPP NODE --------------------- + if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" == false ]]; then + clear_c + print_c "--------------------- WEBAPP NODE --------------------- You are installing from a non-release branch. This means, you will need to build the web app locally. For that you'll need Node. -Do you want to install Node? [Y/n]" 1>&3 +Do you want to install Node? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) ENABLE_INSTALL_NODE=false + ENABLE_WEBAPP_PROD_DOWNLOAD=true ;; *) ;; esac # This message will be displayed at the end of the installation process - FIN_MESSAGE="$FIN_MESSAGE\nATTENTION: You need to build the web app locally with - $ cd ~/RPi-Jukebox-RFID/src/webapp && ./run_rebuild.sh -u - This must be done after reboot, due to memory restrictions. - Read the documentation regarding local Web App builds!" + local tmp_fin_message="ATTENTION: You need to build the web app locally with + $ cd ~/RPi-Jukebox-RFID/src/webapp && ./run_rebuild.sh -u + This must be done after reboot, due to memory restrictions. + Read the documentation regarding local Web App builds!" + FIN_MESSAGE="${FIN_MESSAGE:+$FIN_MESSAGE\n}${tmp_fin_message}" fi fi -} -customize_options() { - echo "Customize Options starts" + log "ENABLE_INSTALL_NODE=${ENABLE_INSTALL_NODE}" + if [ "$ENABLE_INSTALL_NODE" != true ]; then + log "ENABLE_WEBAPP_PROD_DOWNLOAD=${ENABLE_WEBAPP_PROD_DOWNLOAD}" + fi +} +_run_customize_options() { _option_ipv6 _option_static_ip _option_autohotspot _option_bluetooth _option_disable_onboard_audio + _option_mpd + _option_rfid_reader _option_samba _option_webapp if [[ $ENABLE_WEBAPP == true ]] ; then @@ -286,6 +338,8 @@ customize_options() { # Bullseye is currently under active development and should be updated in any case. # Hence, removing the step below as it becomse mandatory # _options_update_raspi_os +} - echo "Customize Options ends" +customize_options() { + run_with_log_frame _run_customize_options "Customize Options" } diff --git a/installation/routines/install.sh b/installation/routines/install.sh index ca25a17a3..62d602f17 100644 --- a/installation/routines/install.sh +++ b/installation/routines/install.sh @@ -1,19 +1,18 @@ install() { - check_os_type - clear 1>&3 + clear_c customize_options - clear 1>&3 + clear_c set_raspi_config - if [ "$DISABLE_SSH_QOS" = true ] ; then set_ssh_qos; fi; - if [ "$UPDATE_RASPI_OS" = true ] ; then update_raspi_os; fi; + set_ssh_qos + update_raspi_os init_git_repo_from_tardir setup_jukebox_core - if [ "$SETUP_MPD" = true ] ; then setup_mpd; fi; - if [ "$ENABLE_SAMBA" = true ] ; then setup_samba; fi; - if [ "$ENABLE_WEBAPP" = true ] ; then setup_jukebox_webapp; fi; - if [ "$ENABLE_KIOSK_MODE" = true ] ; then setup_kiosk_mode; fi; + setup_mpd + setup_samba + setup_jukebox_webapp + setup_kiosk_mode setup_rfid_reader optimize_boot_time - if [ "$ENABLE_AUTOHOTSPOT" = true ] ; then setup_autohotspot; fi; + setup_autohotspot cleanup } diff --git a/installation/routines/optimize_boot_time.sh b/installation/routines/optimize_boot_time.sh index 2fea66a86..bb6f71902 100644 --- a/installation/routines/optimize_boot_time.sh +++ b/installation/routines/optimize_boot_time.sh @@ -2,18 +2,24 @@ # Reference: https://panther.software/configuration-code/raspberry-pi-3-4-faster-boot-time-in-few-easy-steps/ +OPTIMIZE_DHCP_CONF="/etc/dhcpcd.conf" +OPTIMIZE_BOOT_CMDLINE_OPTIONS="consoleblank=1 logo.nologo quiet loglevel=0 plymouth.enable=0 vt.global_cursor_default=0 plymouth.ignore-serial-consoles splash fastboot noatime nodiratime noram" +OPTIMIZE_DHCP_CONF_HEADER="## Jukebox DHCP Config" +OPTIMIZE_IPV6_CONF_HEADER="## Jukebox IPV6 Config" +OPTIMIZE_BOOT_CONF_HEADER="## Jukebox Boot Config" + _optimize_disable_irrelevant_services() { - echo " * Disable keyboard-setup.service" + log " Disable keyboard-setup.service" sudo systemctl disable keyboard-setup.service - echo " * Disable triggerhappy.service" + log " Disable triggerhappy.service" sudo systemctl disable triggerhappy.service sudo systemctl disable triggerhappy.socket - echo " * Disable raspi-config.service" + log " Disable raspi-config.service" sudo systemctl disable raspi-config.service - echo " * Disable apt-daily.service & apt-daily-upgrade.service" + log " Disable apt-daily.service & apt-daily-upgrade.service" sudo systemctl disable apt-daily.service sudo systemctl disable apt-daily-upgrade.service sudo systemctl disable apt-daily.timer @@ -23,30 +29,28 @@ _optimize_disable_irrelevant_services() { # TODO: If false, actually make sure bluetooth is enabled _optimize_handle_bluetooth() { if [ "$DISABLE_BLUETOOTH" = true ] ; then - echo " * Disable hciuart.service and bluetooth" + print_lc " Disable bluetooth" sudo systemctl disable hciuart.service sudo systemctl disable bluetooth.service fi } # TODO: Allow options to enable/disable wifi, Dynamic/Static IP etc. -_optimize_handle_network_connection() { +_optimize_static_ip() { # Static IP Address and DHCP optimizations - local DHCP_CONF="/etc/dhcpcd.conf" - if [ "$ENABLE_STATIC_IP" = true ] ; then - echo " * Set static IP address" | tee /dev/fd/3 - if grep -q "## Jukebox DHCP Config" "$DHCP_CONF"; then - echo " Skipping. Already set up!" | tee /dev/fd/3 + print_lc " Set static IP address" + if grep -q "${OPTIMIZE_DHCP_CONF_HEADER}" "$OPTIMIZE_DHCP_CONF"; then + log " Skipping. Already set up!" else # DHCP has not been configured - echo " * ${CURRENT_INTERFACE} is the default network interface" | tee /dev/fd/3 - echo " * ${CURRENT_GATEWAY} is the Router Gateway address" | tee /dev/fd/3 - echo " * Using ${CURRENT_IP_ADDRESS} as the static IP for now" | tee /dev/fd/3 + log " ${CURRENT_INTERFACE} is the default network interface" + log " ${CURRENT_GATEWAY} is the Router Gateway address" + log " Using ${CURRENT_IP_ADDRESS} as the static IP for now" - sudo tee -a $DHCP_CONF <<-EOF + sudo tee -a $OPTIMIZE_DHCP_CONF <<-EOF -## Jukebox DHCP Config +${OPTIMIZE_DHCP_CONF_HEADER} interface ${CURRENT_INTERFACE} static ip_address=${CURRENT_IP_ADDRESS}/24 static routers=${CURRENT_GATEWAY} @@ -55,59 +59,114 @@ static domain_name_servers=${CURRENT_GATEWAY} EOF fi - else - echo " * Skipped static IP address" fi } # TODO: Allow both Enable and Disable _optimize_ipv6_arp() { if [ "$DISABLE_IPv6" = true ] ; then - echo " * Disabling IPV6 and ARP" - sudo tee -a $DHCP_CONF <<-EOF + print_lc " Disabling IPV6" + if grep -q "${OPTIMIZE_IPV6_CONF_HEADER}" "$OPTIMIZE_DHCP_CONF"; then + log " Skipping. Already set up!" + else + sudo tee -a $OPTIMIZE_DHCP_CONF <<-EOF -## Jukebox boot speed-up settings +${OPTIMIZE_IPV6_CONF_HEADER} noarp ipv4only noipv6 EOF - + fi fi } # TODO: Allow both Enable and Disable _optimize_handle_boot_screen() { if [ "$DISABLE_BOOT_SCREEN" = true ] ; then - echo " * Disable RPi rainbow screen" - BOOT_CONFIG='/boot/config.txt' - sudo tee -a $BOOT_CONFIG <<-EOF + log " Disable RPi rainbow screen" + if grep -q "${OPTIMIZE_BOOT_CONF_HEADER}" "$RPI_BOOT_CONFIG_FILE"; then + log " Skipping. Already set up!" + else + sudo tee -a $RPI_BOOT_CONFIG_FILE <<-EOF -## Jukebox Settings +${OPTIMIZE_BOOT_CONF_HEADER} disable_splash=1 EOF + fi fi } # TODO: Allow both Enable and Disable _optimize_handle_boot_logs() { if [ "$DISABLE_BOOT_LOGS_PRINT" = true ] ; then - echo " * Disable boot logs" - BOOT_CMDLINE='/boot/cmdline.txt' - sudo sed -i "$ s/$/ consoleblank=1 logo.nologo quiet loglevel=0 plymouth.enable=0 vt.global_cursor_default=0 plymouth.ignore-serial-consoles splash fastboot noatime nodiratime noram/" $BOOT_CMDLINE + log " Disable boot logs" + + if [ ! -s "${RPI_BOOT_CMDLINE_FILE}" ];then + sudo tee "${RPI_BOOT_CMDLINE_FILE}" <<-EOF +${OPTIMIZE_BOOT_CMDLINE_OPTIONS} +EOF + else + for option in $OPTIMIZE_BOOT_CMDLINE_OPTIONS + do + if ! grep -qiw "$option" "${RPI_BOOT_CMDLINE_FILE}" ; then + sudo sed -i "s/$/ $option/" "${RPI_BOOT_CMDLINE_FILE}" + fi + done + fi fi } -optimize_boot_time() { - echo "Optimize boot time" | tee /dev/fd/3 - _optimize_disable_irrelevant_services - _optimize_handle_bluetooth - _optimize_handle_network_connection - _optimize_ipv6_arp - _optimize_handle_boot_screen - _optimize_handle_boot_logs +_optimize_check() { + print_verify_installation + + verify_optional_service_enablement keyboard-setup.service disabled + verify_optional_service_enablement triggerhappy.service disabled + verify_optional_service_enablement triggerhappy.socket disabled + verify_optional_service_enablement raspi-config.service disabled + verify_optional_service_enablement apt-daily.service disabled + verify_optional_service_enablement apt-daily-upgrade.service disabled + verify_optional_service_enablement apt-daily.timer disabled + verify_optional_service_enablement apt-daily-upgrade.timer disabled - echo "DONE: optimize_boot_time" + if [ "$DISABLE_BLUETOOTH" = true ] ; then + verify_optional_service_enablement hciuart.service disabled + verify_optional_service_enablement bluetooth.service disabled + fi + + if [ "$ENABLE_STATIC_IP" = true ] ; then + verify_file_contains_string_once "${OPTIMIZE_DHCP_CONF_HEADER}" "${OPTIMIZE_DHCP_CONF}" + verify_file_contains_string "${CURRENT_INTERFACE}" "${OPTIMIZE_DHCP_CONF}" + verify_file_contains_string "${CURRENT_IP_ADDRESS}" "${OPTIMIZE_DHCP_CONF}" + verify_file_contains_string "${CURRENT_GATEWAY}" "${OPTIMIZE_DHCP_CONF}" + fi + if [ "$DISABLE_IPv6" = true ] ; then + verify_file_contains_string_once "${OPTIMIZE_IPV6_CONF_HEADER}" "${OPTIMIZE_DHCP_CONF}" + fi + if [ "$DISABLE_BOOT_SCREEN" = true ] ; then + verify_file_contains_string_once "${OPTIMIZE_BOOT_CONF_HEADER}" "${RPI_BOOT_CONFIG_FILE}" + fi + + if [ "$DISABLE_BOOT_LOGS_PRINT" = true ] ; then + for option in $OPTIMIZE_BOOT_CMDLINE_OPTIONS + do + verify_file_contains_string_once $option "${RPI_BOOT_CMDLINE_FILE}" + done + fi +} + +_run_optimize_boot_time() { + _optimize_disable_irrelevant_services + _optimize_handle_bluetooth + _optimize_static_ip + _optimize_ipv6_arp + _optimize_handle_boot_screen + _optimize_handle_boot_logs + _optimize_check +} + +optimize_boot_time() { + run_with_log_frame _run_optimize_boot_time "Optimize boot time" } diff --git a/installation/routines/set_raspi_config.sh b/installation/routines/set_raspi_config.sh index 1b1c3ee55..7f39a0ba5 100644 --- a/installation/routines/set_raspi_config.sh +++ b/installation/routines/set_raspi_config.sh @@ -1,33 +1,33 @@ #!/usr/bin/env bash - -set_raspi_config() { - echo "Set default raspi-config" | tee /dev/fd/3 +_run_set_raspi_config() { # Source: https://raspberrypi.stackexchange.com/a/66939 # Autologin - echo " * Enable Autologin for user" + log " Enable Autologin for user" sudo raspi-config nonint do_boot_behaviour B2 # Wait for network at boot - # echo " * Enable 'Wait for network at boot'" + # log " Enable 'Wait for network at boot'" # sudo raspi-config nonint do_boot_wait 1 # power management of wifi: switch off to avoid disconnecting - echo " * Disable Wifi power management to avoid disconnecting" + log " Disable Wifi power management to avoid disconnecting" sudo iwconfig wlan0 power off # On-board audio - if [[ $(get_onboard_audio) -eq 1 ]]; then - DISABLE_ONBOARD_AUDIO=${DISABLE_ONBOARD_AUDIO:-false} - if [[ $DISABLE_ONBOARD_AUDIO = true ]]; then - echo " * Disable on-chip BCM audio" - echo "Backup ${RPI_BOOT_CONFIG_FILE} --> ${DISABLE_ONBOARD_AUDIO_BACKUP}" + if [ "$DISABLE_ONBOARD_AUDIO" == true ]; then + log " Disable on-chip BCM audio" + if grep -q -E "^dtparam=([^,]*,)*audio=(on|true|yes|1).*" "${RPI_BOOT_CONFIG_FILE}" ; then + log " Backup ${RPI_BOOT_CONFIG_FILE} --> ${DISABLE_ONBOARD_AUDIO_BACKUP}" sudo cp "${RPI_BOOT_CONFIG_FILE}" "${DISABLE_ONBOARD_AUDIO_BACKUP}" sudo sed -i "s/^\(dtparam=\([^,]*,\)*\)audio=\(on\|true\|yes\|1\)\(.*\)/\1audio=off\4/g" "${RPI_BOOT_CONFIG_FILE}" + else + log " On board audio seems to be off already. Not touching ${RPI_BOOT_CONFIG_FILE}" fi - else - echo "On board audio seems to be off already. Not touching ${RPI_BOOT_CONFIG_FILE}" fi +} +set_raspi_config() { + run_with_log_frame _run_set_raspi_config "Set default raspi-config" } diff --git a/installation/routines/set_ssh_qos.sh b/installation/routines/set_ssh_qos.sh index ad67ddc41..76242f712 100644 --- a/installation/routines/set_ssh_qos.sh +++ b/installation/routines/set_ssh_qos.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash set_ssh_qos() { - # The latest version of SSH installed on the Raspberry Pi 3 uses QoS headers, which disagrees with some - # routers and other hardware. This causes immense delays when remotely accessing the RPi over ssh. - echo " * Set SSH QoS to best effort" - echo -e "IPQoS 0x00 0x00\n" | sudo tee -a /etc/ssh/sshd_config - echo -e "IPQoS 0x00 0x00\n" | sudo tee -a /etc/ssh/ssh_config + if [ "$DISABLE_SSH_QOS" == true ] ; then + # The latest version of SSH installed on the Raspberry Pi 3 uses QoS headers, which disagrees with some + # routers and other hardware. This causes immense delays when remotely accessing the RPi over ssh. + log " Set SSH QoS to best effort" + echo -e "IPQoS 0x00 0x00\n" | sudo tee -a /etc/ssh/sshd_config + echo -e "IPQoS 0x00 0x00\n" | sudo tee -a /etc/ssh/ssh_config + fi } diff --git a/installation/routines/setup_autohotspot.sh b/installation/routines/setup_autohotspot.sh index 214a90e0f..a083b3fcf 100644 --- a/installation/routines/setup_autohotspot.sh +++ b/installation/routines/setup_autohotspot.sh @@ -1,14 +1,34 @@ #!/usr/bin/env bash +# inspired by +# https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection + + +AUTOHOTSPOT_HOSTAPD_CONF_FILE="/etc/hostapd/hostapd.conf" +AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE="/etc/default/hostapd" +AUTOHOTSPOT_DNSMASQ_CONF_FILE="/etc/dnsmasq.conf" +AUTOHOTSPOT_DHCPD_CONF_FILE="/etc/dhcpcd.conf" + +AUTOHOTSPOT_TARGET_PATH="/usr/bin/autohotspot" + _get_interface() { # interfaces may vary WIFI_INTERFACE=$(iw dev | grep "Interface"| awk '{ print $2 }') - WIFI_REGION=$(iw reg get | grep country | awk '{ print $2}' | cut -d: -f1) -} + WIFI_REGION=$(iw reg get | grep country | head -n 1 | awk '{ print $2}' | cut -d: -f1) + # fix for CI runs on docker + if [ "${CI_RUNNING}" == "true" ]; then + if [ -z "${WIFI_INTERFACE}" ]; then + WIFI_INTERFACE="CI TEST INTERFACE" + fi + if [ -z "${WIFI_REGION}" ]; then + WIFI_REGION="CI TEST REGION" + fi + fi +} _install_packages() { - sudo apt-get -y install hostapd dnsmasq + sudo apt-get -y install hostapd dnsmasq iw # disable services. We want to start them manually sudo systemctl unmask hostapd @@ -17,18 +37,18 @@ _install_packages() { } _configure_hostapd() { - HOSTAPD_CUSTOM_FILE="${INSTALLATION_PATH}"/resources/autohotspot/hostapd.conf - HOSTAPD_CONF_FILE="/etc/hostapd/hostapd.conf" + local HOSTAPD_CUSTOM_FILE="${INSTALLATION_PATH}"/resources/autohotspot/hostapd.conf + sed -i "s/WIFI_INTERFACE/${WIFI_INTERFACE}/g" "${HOSTAPD_CUSTOM_FILE}" sed -i "s/AUTOHOTSPOT_PASSWORD/${AUTOHOTSPOT_PASSWORD}/g" "${HOSTAPD_CUSTOM_FILE}" sed -i "s/WIFI_REGION/${WIFI_REGION}/g" "${HOSTAPD_CUSTOM_FILE}" - sudo cp "${HOSTAPD_CUSTOM_FILE}" "${HOSTAPD_CONF_FILE}" + sudo cp "${HOSTAPD_CUSTOM_FILE}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" - sudo sed -i "s@^#DAEMON_CONF=.*@DAEMON_CONF=\"${HOSTAPD_CONF_FILE}\"@g" /etc/default/hostapd + sudo sed -i "s@^#DAEMON_CONF=.*@DAEMON_CONF=\"${AUTOHOTSPOT_HOSTAPD_CONF_FILE}\"@g" "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" } _configure_dnsmasq() { - sudo tee -a /etc/dnsmasq.conf <<-EOF + sudo tee -a "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" <<-EOF #AutoHotspot Config #stop DNSmasq from using resolv.conf no-resolv @@ -42,33 +62,58 @@ EOF _other_configuration() { sudo mv /etc/network/interfaces /etc/network/interfaces.bak sudo touch /etc/network/interfaces - echo nohook wpa_supplicant | sudo tee -a /etc/dhcpcd.conf + echo nohook wpa_supplicant | sudo tee -a "${AUTOHOTSPOT_DHCPD_CONF_FILE}" } _install_service_and_timer() { sudo cp "${INSTALLATION_PATH}"/resources/autohotspot/autohotspot.service /etc/systemd/system/autohotspot.service sudo systemctl enable autohotspot.service - sudo cp "${INSTALLATION_PATH}"/resources/autohotspot/autohotspot.timer /etc/cron.d/autohotspot + + local cron_autohotspot_file="/etc/cron.d/autohotspot" + sudo cp "${INSTALLATION_PATH}"/resources/autohotspot/autohotspot.timer "${cron_autohotspot_file}" + sudo sed -i "s|%%USER%%|${CURRENT_USER}|g" "${cron_autohotspot_file}" } _install_autohotspot_script() { - TARGET_PATH="/usr/bin/autohotspot" - sudo cp "${INSTALLATION_PATH}"/resources/autohotspot/autohotspot "${TARGET_PATH}" - sudo chmod +x "${TARGET_PATH}" + sudo cp "${INSTALLATION_PATH}"/resources/autohotspot/autohotspot "${AUTOHOTSPOT_TARGET_PATH}" + sudo chmod +x "${AUTOHOTSPOT_TARGET_PATH}" } -setup_autohotspot() { - echo "Install AutoHotspot functionality" | tee /dev/fd/3 - # inspired by - # https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection - _get_interface +_autohotspot_check() { + print_verify_installation + + verify_apt_packages hostapd dnsmasq iw + + verify_service_enablement hostapd.service disabled + verify_service_enablement dnsmasq.service disabled + verify_service_enablement autohotspot.service enabled + + verify_files_exists "/etc/cron.d/autohotspot" + verify_files_exists "${AUTOHOTSPOT_TARGET_PATH}" + + verify_file_contains_string "${WIFI_INTERFACE}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "${AUTOHOTSPOT_PASSWORD}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "${WIFI_REGION}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + + verify_file_contains_string "${WIFI_INTERFACE}" "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + verify_file_contains_string "nohook wpa_supplicant" "${AUTOHOTSPOT_DHCPD_CONF_FILE}" +} + +_run_setup_autohotspot() { _install_packages + _get_interface _configure_hostapd _configure_dnsmasq _other_configuration _install_autohotspot_script _install_service_and_timer + _autohotspot_check +} - echo "DONE: setup_autohotspot" +setup_autohotspot() { + if [ "$ENABLE_AUTOHOTSPOT" == true ] ; then + run_with_log_frame _run_setup_autohotspot "Install AutoHotspot" + fi } diff --git a/installation/routines/setup_git.sh b/installation/routines/setup_git.sh index 740d43ae3..c04ff7acf 100644 --- a/installation/routines/setup_git.sh +++ b/installation/routines/setup_git.sh @@ -2,7 +2,7 @@ GIT_ABORT_MSG="Aborting dir to git repo conversion. Your directory content is untouched, you simply cannot use git for updating / developing" _git_install_os_dependencies() { - echo "Install Git dependencies" + log " Install Git dependencies" sudo apt-get -y update; sudo apt-get -y install \ git \ --no-install-recommends \ @@ -12,9 +12,9 @@ _git_install_os_dependencies() { } _git_convert_tardir_git_repo() { - echo "****************************************************" - echo "*** Converting tar-ball download into git repository" - echo "****************************************************" + log "**************************************************** +*** Converting tar-ball download into git repository +****************************************************" # Just in case, the git version is not new enough, we split up git init -b "${GIT_BRANCH}" into: git -c init.defaultBranch=main init @@ -30,19 +30,19 @@ _git_convert_tardir_git_repo() { # We simply get everything from the beginning of future 3 development but excluding Version 2.X if [[ $GIT_USE_SSH == true ]]; then git remote add origin "git@github.com:${GIT_USER}/${GIT_REPO_NAME}.git" - echo "*** Git fetch (SSH) *******************************" + log "\n*** Git fetch (SSH) *******************************" # Prevent: The authenticity of host 'github.com (140.82.121.4)' can't be established. # Do only for this one command, so we do not disable the checks forever if ! git -c core.sshCommand='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' fetch origin "${GIT_BRANCH}" --set-upstream --shallow-since=2021-04-21 --tags; then - echo "" - echo "*** NOTICE *****************************************" - echo "* Error in getting Git Repository using SSH! USING FALLBACK HTTPS." - echo "* Note: This is only relevant for developers!" - echo "* Did you forget to upload the ssh key for this machine to GitHub?" - echo "* Defaulting to HTTPS protocol. You can change back to SSH later with" - echo "* git remote set-url origin git@github.com:${GIT_USER}/${GIT_REPO_NAME}.git" - echo "* git remote set-url upstream git@github.com:${GIT_UPSTREAM_USER}/${GIT_REPO_NAME}.git" + log "\n*** NOTICE ***************************************** +* Error in getting Git Repository using SSH! USING FALLBACK HTTPS. +* Note: This is only relevant for developers! +* Did you forget to upload the ssh key for this machine to GitHub? +* Defaulting to HTTPS protocol. You can change back to SSH later with +* git remote set-url origin git@github.com:${GIT_USER}/${GIT_REPO_NAME}.git +* git remote set-url upstream git@github.com:${GIT_UPSTREAM_USER}/${GIT_REPO_NAME}.git\n" + git remote remove origin GIT_USE_SSH=false else @@ -58,30 +58,31 @@ _git_convert_tardir_git_repo() { if [[ "$GIT_USER" != "$GIT_UPSTREAM_USER" ]]; then git remote add upstream "https://github.com/${GIT_UPSTREAM_USER}/${GIT_REPO_NAME}.git" fi - echo "*** Git fetch (HTTPS) *****************************" + log "\n*** Git fetch (HTTPS) *****************************" if ! git fetch origin --set-upstream --shallow-since=2021-04-21 --tags "${GIT_BRANCH}"; then - echo "Error: Could not fetch repository!" - echo -e "$GIT_ABORT_MSG" + log "Error: Could not fetch repository!" + log "$GIT_ABORT_MSG" return fi fi HASH_BRANCH=$(git rev-parse FETCH_HEAD) || { echo -e "$GIT_ABORT_MSG"; return; } - echo "*** FETCH_HEAD ($GIT_BRANCH) = $HASH_BRANCH" + + log "\n*** FETCH_HEAD ($GIT_BRANCH) = $HASH_BRANCH" git add . # Checkout the exact hash that we have downloaded as tarball - echo "*** Git checkout commit" + log "*** Git checkout commit" git -c advice.detachedHead=false checkout "$GIT_HASH" || { echo -e "$GIT_ABORT_MSG"; return; } HASH_HEAD=$(git rev-parse HEAD) || { echo -e "$GIT_ABORT_MSG"; return; } - echo "*** REQUESTED COMMIT = $HASH_HEAD" + log "*** REQUESTED COMMIT = $HASH_HEAD" # Let's move onto the relevant branch, WITHOUT touching the current checked-out commit # Since we have fetched with --set-upstream above this initializes the tracking branch - echo "*** Git initialize branch" + log "*** Git initialize branch" git checkout -b "$GIT_BRANCH" if [[ "$GIT_USER" != "$GIT_UPSTREAM_USER" ]]; then - echo "*** Get upstream release tags" + log "*** Get upstream release tags" # Always get the upstream release branch to get all release tags # in case they have not been copied to user repository git fetch upstream --shallow-since=2021-04-21 --tags "${GIT_BRANCH_RELEASE}" @@ -97,7 +98,7 @@ _git_convert_tardir_git_repo() { if [[ $GIT_BRANCH != "${GIT_BRANCH_RELEASE}" ]]; then OUTPUT=$(git fetch origin --shallow-since=2021-04-21 --tags "${GIT_BRANCH_RELEASE}" 2>&1) if [[ $? -ne 128 ]]; then - echo "*** Preparing ${GIT_BRANCH_RELEASE} in background" + log "*** Preparing ${GIT_BRANCH_RELEASE} in background" echo -e "$OUTPUT" fi unset OUTPUT @@ -105,7 +106,7 @@ _git_convert_tardir_git_repo() { if [[ $GIT_BRANCH != "${GIT_BRANCH_DEVELOP}" ]]; then OUTPUT=$(git fetch origin --shallow-since=2021-04-21 --tags "${GIT_BRANCH_DEVELOP}" 2>&1) if [[ $? -ne 128 ]]; then - echo "*** Preparing ${GIT_BRANCH_DEVELOP} in background" + log "*** Preparing ${GIT_BRANCH_DEVELOP} in background" echo -e "$OUTPUT" fi unset OUTPUT @@ -113,23 +114,24 @@ _git_convert_tardir_git_repo() { # Provide some status outputs to the user if [[ "${HASH_BRANCH}" != "${HASH_HEAD}" ]]; then - echo "*** IMPORTANT NOTICE *******************************" - echo "* Your requested branch has moved on while you were installing." - echo "* Don't worry! We will stay within the the exact download version!" - echo "* But we set up the git repo to be ready for updating." - echo "* To start updating (observe updating guidelines!), do:" - echo "* $ git pull origin $GIT_BRANCH" + log "\n*** IMPORTANT NOTICE ******************************* +* Your requested branch has moved on while you were installing. +* Don't worry! We will stay within the exact download version! +* But we set up the git repo to be ready for updating. +* To start updating (observe updating guidelines!), do: +* $ git pull origin $GIT_BRANCH\n" + fi - echo "*** Git remotes ************************************" + log "*** Git remotes ************************************" git remote -v - echo "*** Git status *************************************" + log "*** Git status *************************************" git status -sb - echo "*** Git log ****************************************" + log "*** Git log ****************************************" git log --oneline "HEAD^..origin/$GIT_BRANCH" - echo "*** Git describe ***********************************" + log "*** Git describe ***********************************" git describe --tag --dirty='-dirty' - echo "****************************************************" + log "****************************************************" cp -f .githooks/* .git/hooks @@ -137,12 +139,20 @@ _git_convert_tardir_git_repo() { unset HASH_BRANCH } -init_git_repo_from_tardir() { - echo "Install Git & init repository" | tee /dev/fd/3 +_git_repo_check() { + print_verify_installation - cd "${INSTALLATION_PATH}" || exit_on_error - _git_install_os_dependencies - _git_convert_tardir_git_repo + verify_apt_packages git + verify_dirs_chmod_chown 755 "${CURRENT_USER}" "${CURRENT_USER_GROUP}" "${INSTALLATION_PATH}/.git" +} + +_run_init_git_repo_from_tardir() { + cd "${INSTALLATION_PATH}" || exit_on_error + _git_install_os_dependencies + _git_convert_tardir_git_repo + _git_repo_check +} - echo "DONE: init_git_repo_from_tardir" +init_git_repo_from_tardir() { + run_with_log_frame _run_init_git_repo_from_tardir "Install Git & init repository" } diff --git a/installation/routines/setup_jukebox_core.sh b/installation/routines/setup_jukebox_core.sh index e6dfa6d8f..a7d0f29b6 100644 --- a/installation/routines/setup_jukebox_core.sh +++ b/installation/routines/setup_jukebox_core.sh @@ -1,32 +1,28 @@ #!/usr/bin/env bash # Constants -GD_ID_COMPILED_LIBZMQ_ARMV7="1KP6BqLF-i2dCUsHhOUpOwwuOmKsB5GKY" # ARMv7: https://drive.google.com/file/d/1KP6BqLF-i2dCUsHhOUpOwwuOmKsB5GKY/view?usp=sharing -GD_ID_COMPILED_LIBZMQ_ARMV6="1iygOm-G1cg_3YERuVRT6FhGBE34ZkwgV" # ARMv6: https://drive.google.com/file/d/1iygOm-G1cg_3YERuVRT6FhGBE34ZkwgV/view?usp=sharing -GD_ID_COMPILED_PYZMQ_ARMV7="" -GD_ID_COMPILED_PYZMQ_ARMV6="1lDsV_pVcXbg6YReHb9AldMkyRZCpc6-n" # https://drive.google.com/file/d/1lDsV_pVcXbg6YReHb9AldMkyRZCpc6-n/view?usp=sharing +JUKEBOX_ZMQ_TMP_DIR="${HOME_PATH}/libzmq" +JUKEBOX_ZMQ_PREFIX="/usr/local" +JUKEBOX_ZMQ_VERSION="4.3.5" -ZMQ_TMP_DIR="libzmq" -ZMQ_PREFIX="/usr/local" +JUKEBOX_PULSE_CONFIG="${HOME_PATH}"/.config/pulse/default.pa +JUKEBOX_SERVICE_NAME="${SYSTEMD_USR_PATH}/jukebox-daemon.service" _show_slow_hardware_message() { -echo " -------------------------------------------------------------------- + print_c " -------------------------------------------------------------------- | Your hardware is a little slower so this step will take a while. | | Go watch a movie but don't let your computer go to sleep for the | | SSH connection to remain intact. | - --------------------------------------------------------------------" 1>&3 + --------------------------------------------------------------------" } # Functions _jukebox_core_install_os_dependencies() { - echo " Install Jukebox OS dependencies" + print_lc " Install Jukebox OS dependencies" + + local apt_packages=$(get_args_from_file "${INSTALLATION_PATH}/packages-core.txt") sudo apt-get -y update && sudo apt-get -y install \ - at \ - alsa-utils \ - python3 python3-venv python3-dev \ - espeak ffmpeg mpg123 \ - pulseaudio pulseaudio-module-bluetooth pulseaudio-utils caps \ - libasound2-dev \ + $apt_packages \ --no-install-recommends \ --allow-downgrades \ --allow-remove-essential \ @@ -34,11 +30,10 @@ _jukebox_core_install_os_dependencies() { } _jukebox_core_install_python_requirements() { - echo " Install Python requirements" + print_lc " Install Python requirements" cd "${INSTALLATION_PATH}" || exit_on_error - VIRTUAL_ENV="${INSTALLATION_PATH}/.venv" python3 -m venv $VIRTUAL_ENV source "$VIRTUAL_ENV/bin/activate" @@ -47,36 +42,36 @@ _jukebox_core_install_python_requirements() { } _jukebox_core_configure_pulseaudio() { - echo "Copy PulseAudio configuration" - mkdir -p ~/.config/pulse - cp -f "${INSTALLATION_PATH}/resources/default-settings/pulseaudio.default.pa" ~/.config/pulse/default.pa + print_lc " Copy PulseAudio configuration" + mkdir -p $(dirname "$JUKEBOX_PULSE_CONFIG") + cp -f "${INSTALLATION_PATH}/resources/default-settings/pulseaudio.default.pa" "${JUKEBOX_PULSE_CONFIG}" } _jukebox_core_build_libzmq_with_drafts() { - LIBSODIUM_VERSION="1.0.18" - ZMQ_VERSION="4.3.4" - - { cd "${HOME_PATH}" && mkdir "${ZMQ_TMP_DIR}" && cd "${ZMQ_TMP_DIR}"; } || exit_on_error - wget --quiet https://github.com/jedisct1/libsodium/releases/download/${LIBSODIUM_VERSION}-RELEASE/libsodium-${LIBSODIUM_VERSION}.tar.gz - tar -zxvf libsodium-${LIBSODIUM_VERSION}.tar.gz - cd libsodium-${LIBSODIUM_VERSION} || exit_on_error - ./configure - make && make install - - cd "${HOME}/${ZMQ_TMP_DIR}" || exit_on_error - wget https://github.com/zeromq/libzmq/releases/download/v${ZMQ_VERSION}/zeromq-${ZMQ_VERSION}.tar.gz -O libzmq.tar.gz - tar -xzf libzmq.tar.gz - zeromq-${ZMQ_VERSION}/configure --prefix=${ZMQ_PREFIX} --enable-drafts - make && make install + print_lc " Building libzmq v${JUKEBOX_ZMQ_VERSION} with drafts support" + local zmq_filename="zeromq-${JUKEBOX_ZMQ_VERSION}" + local zmq_tar_filename="${zmq_filename}.tar.gz" + local cpu_count=${CPU_COUNT:-$(python3 -c "import os; print(os.cpu_count())")} + + cd "${JUKEBOX_ZMQ_TMP_DIR}" || exit_on_error + wget --quiet https://github.com/zeromq/libzmq/releases/download/v${JUKEBOX_ZMQ_VERSION}/${zmq_tar_filename} + tar -xzf ${zmq_tar_filename} + rm -f ${zmq_tar_filename} + cd ${zmq_filename} || exit_on_error + ./configure --prefix=${JUKEBOX_ZMQ_PREFIX} --enable-drafts --disable-Werror + make -j${cpu_count} && sudo make install } _jukebox_core_download_prebuilt_libzmq_with_drafts() { - local ZMQ_TAR_FILENAME="libzmq.tar.gz" - - _download_file_from_google_drive "${LIBZMQ_GD_DOWNLOAD_ID}" "${ZMQ_TAR_FILENAME}" - tar -xzf ${ZMQ_TAR_FILENAME} - rm -f ${ZMQ_TAR_FILENAME} - sudo rsync -a ./* ${ZMQ_PREFIX}/ + log " Download pre-compiled libzmq with drafts support" + local zmq_tar_filename="libzmq.tar.gz" + ARCH=$(get_architecture) + + cd "${JUKEBOX_ZMQ_TMP_DIR}" || exit_on_error + wget --quiet https://github.com/pabera/libzmq/releases/download/v${JUKEBOX_ZMQ_VERSION}/libzmq5-${ARCH}-${JUKEBOX_ZMQ_VERSION}.tar.gz -O ${zmq_tar_filename} + tar -xzf ${zmq_tar_filename} + rm -f ${zmq_tar_filename} + sudo rsync -a ./* ${JUKEBOX_ZMQ_PREFIX}/ } _jukebox_core_build_and_install_pyzmq() { @@ -87,62 +82,79 @@ _jukebox_core_build_and_install_pyzmq() { # Sources: # https://pyzmq.readthedocs.io/en/latest/howto/draft.html # https://github.com/MonsieurV/ZeroMQ-RPi/blob/master/README.md - echo " Build and install pyzmq with WebSockets Support" + # https://github.com/zeromq/pyzmq/issues/1523#issuecomment-1593120264 + print_lc " Install pyzmq with libzmq-drafts to support WebSockets" if ! pip list | grep -F pyzmq >> /dev/null; then - # Download pre-compiled libzmq from Google Drive because RPi has trouble compiling it - echo " Download pre-compiled libzmq from Google Drive because RPi has trouble compiling it" - { cd "${HOME_PATH}" && mkdir "${ZMQ_TMP_DIR}" && cd "${ZMQ_TMP_DIR}"; } || exit_on_error - - # ARMv7 as default - LIBZMQ_GD_DOWNLOAD_ID=${GD_ID_COMPILED_LIBZMQ_ARMV7} if [[ $(uname -m) == "armv6l" ]]; then - # ARMv6 as fallback - LIBZMQ_GD_DOWNLOAD_ID=${GD_ID_COMPILED_LIBZMQ_ARMV6} _show_slow_hardware_message fi + mkdir -p "${JUKEBOX_ZMQ_TMP_DIR}" || exit_on_error if [ "$BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE" = true ] ; then _jukebox_core_build_libzmq_with_drafts else _jukebox_core_download_prebuilt_libzmq_with_drafts fi - ZMQ_PREFIX="${ZMQ_PREFIX}" ZMQ_DRAFT_API=1 \ - pip install --no-cache-dir --no-binary "pyzmq" --pre pyzmq + ZMQ_PREFIX="${JUKEBOX_ZMQ_PREFIX}" ZMQ_DRAFT_API=1 \ + pip install -v --no-binary pyzmq --pre pyzmq else - echo " Skipping. pyzmq already installed" + print_lc " Skipping. pyzmq already installed" fi } _jukebox_core_install_settings() { - echo " Register Jukebox settings" + print_lc " Register Jukebox settings" cp -f "${INSTALLATION_PATH}/resources/default-settings/jukebox.default.yaml" "${SETTINGS_PATH}/jukebox.yaml" cp -f "${INSTALLATION_PATH}/resources/default-settings/logger.default.yaml" "${SETTINGS_PATH}/logger.yaml" } _jukebox_core_register_as_service() { - echo " Register Jukebox Core user service" + print_lc " Register Jukebox Core user service" - local jukebox_service="${SYSTEMD_USR_PATH}/jukebox-daemon.service" - sudo cp -f "${INSTALLATION_PATH}/resources/default-services/jukebox-daemon.service" "${jukebox_service}" - sudo sed -i "s|%%INSTALLATION_PATH%%|${INSTALLATION_PATH}|g" "${jukebox_service}" - sudo chmod 644 "${jukebox_service}" + sudo cp -f "${INSTALLATION_PATH}/resources/default-services/jukebox-daemon.service" "${JUKEBOX_SERVICE_NAME}" + sudo sed -i "s|%%INSTALLATION_PATH%%|${INSTALLATION_PATH}|g" "${JUKEBOX_SERVICE_NAME}" + sudo chmod 644 "${JUKEBOX_SERVICE_NAME}" systemctl --user daemon-reload systemctl --user enable jukebox-daemon.service } -setup_jukebox_core() { - echo "Install Jukebox Core" | tee /dev/fd/3 +_jukebox_core_check() { + print_verify_installation + + local apt_packages=$(get_args_from_file "${INSTALLATION_PATH}/packages-core.txt") + verify_apt_packages $apt_packages + + verify_dirs_exists "${VIRTUAL_ENV}" + + local pip_modules=$(get_args_from_file "${INSTALLATION_PATH}/requirements.txt") + verify_pip_modules pyzmq $pip_modules + + verify_files_chmod_chown 644 "${CURRENT_USER}" "${CURRENT_USER_GROUP}" "${JUKEBOX_PULSE_CONFIG}" - _jukebox_core_install_os_dependencies - _jukebox_core_install_python_requirements - _jukebox_core_configure_pulseaudio - _jukebox_core_build_and_install_pyzmq - _jukebox_core_install_settings - _jukebox_core_register_as_service + verify_files_chmod_chown 644 "${CURRENT_USER}" "${CURRENT_USER_GROUP}" "${SETTINGS_PATH}/jukebox.yaml" + verify_files_chmod_chown 644 "${CURRENT_USER}" "${CURRENT_USER_GROUP}" "${SETTINGS_PATH}/logger.yaml" - echo "DONE: setup_jukebox_core" + verify_files_chmod_chown 644 root root "${SYSTEMD_USR_PATH}/jukebox-daemon.service" + + verify_file_contains_string "${INSTALLATION_PATH}" "${JUKEBOX_SERVICE_NAME}" + + verify_service_enablement jukebox-daemon.service enabled --user +} + +_run_setup_jukebox_core() { + _jukebox_core_install_os_dependencies + _jukebox_core_install_python_requirements + _jukebox_core_build_and_install_pyzmq + _jukebox_core_configure_pulseaudio + _jukebox_core_install_settings + _jukebox_core_register_as_service + _jukebox_core_check +} + +setup_jukebox_core() { + run_with_log_frame _run_setup_jukebox_core "Install Jukebox Core" } diff --git a/installation/routines/setup_jukebox_webapp.sh b/installation/routines/setup_jukebox_webapp.sh index a0e195622..f7407f96c 100644 --- a/installation/routines/setup_jukebox_webapp.sh +++ b/installation/routines/setup_jukebox_webapp.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Constants -GD_ID_COMPILED_WEBAPP="1EE_1MdneGtKL5V7GyYZC0nb6ODQWTsPb" # https://drive.google.com/file/d/1EE_1MdneGtKL5V7GyYZC0nb6ODQWTsPb/view?usp=sharing +WEBAPP_NGINX_SITE_DEFAULT_CONF="/etc/nginx/sites-available/default" # For ARMv7+ NODE_MAJOR=20 @@ -15,13 +15,13 @@ _jukebox_webapp_install_node() { sudo apt-get -y update if which node > /dev/null; then - echo " Found existing NodeJS. Hence, updating NodeJS" | tee /dev/fd/3 + print_lc " Found existing NodeJS. Hence, updating NodeJS" sudo npm cache clean -f sudo npm install --silent -g n sudo n --quiet latest sudo npm update --silent -g else - echo " Install NodeJS" | tee /dev/fd/3 + print_lc " Install NodeJS" # Zero and older versions of Pi with ARMv6 only # support experimental NodeJS @@ -39,14 +39,13 @@ _jukebox_webapp_install_node() { sudo apt-get update sudo apt-get install -y nodejs fi - fi } # TODO: Avoid building the app locally # Instead implement a Github Action that prebuilds on commititung a git tag _jukebox_webapp_build() { - echo " Building web application" + print_lc " Building web application" cd "${INSTALLATION_PATH}/src/webapp" || exit_on_error npm ci --prefer-offline --no-audit --production rm -rf build @@ -55,45 +54,68 @@ _jukebox_webapp_build() { } _jukebox_webapp_download() { - echo " Downloading web application" | tee /dev/fd/3 + print_lc " Downloading web application" + local JUKEBOX_VERSION=$(get_version_string "${INSTALLATION_PATH}/src/jukebox/jukebox/version.py") local TAR_FILENAME="webapp-build.tar.gz" + local DOWNLOAD_URL="https://github.com/MiczFlor/RPi-Jukebox-RFID/releases/download/v${JUKEBOX_VERSION}/webapp-v${JUKEBOX_VERSION}.tar.gz" + log " DOWNLOAD_URL: ${DOWNLOAD_URL}" + cd "${INSTALLATION_PATH}/src/webapp" || exit_on_error - _download_file_from_google_drive ${GD_ID_COMPILED_WEBAPP} ${TAR_FILENAME} + # URL must be set to default repo as installation can be run from different repos as well where releases may not exist + wget --quiet ${DOWNLOAD_URL} -O ${TAR_FILENAME} tar -xzf ${TAR_FILENAME} rm -f ${TAR_FILENAME} cd "${INSTALLATION_PATH}" || exit_on_error } _jukebox_webapp_register_as_system_service_with_nginx() { - echo " Install and configure nginx" | tee /dev/fd/3 + print_lc " Install and configure nginx" sudo apt-get -qq -y update sudo apt-get -y purge apache2 sudo apt-get -y install nginx - sudo service nginx start - - sudo mv -f /etc/nginx/sites-available/default /etc/nginx/sites-available/default.orig - sudo cp -f "${INSTALLATION_PATH}/resources/default-settings/nginx.default" /etc/nginx/sites-available/default + sudo mv -f "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" "${WEBAPP_NGINX_SITE_DEFAULT_CONF}.orig" + sudo cp -f "${INSTALLATION_PATH}/resources/default-settings/nginx.default" "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" + sudo sed -i "s|%%INSTALLATION_PATH%%|${INSTALLATION_PATH}|g" "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" # make sure nginx can access the home directory of the user - sudo chmod o+x /home/pi + sudo chmod o+x "${HOME_PATH}" - sudo service nginx restart + sudo systemctl restart nginx.service } -setup_jukebox_webapp() { - echo "Install web application" | tee /dev/fd/3 +_jukebox_webapp_check() { + print_verify_installation - if [[ $ENABLE_WEBAPP_PROD_DOWNLOAD == true || $ENABLE_WEBAPP_PROD_DOWNLOAD == release-only ]] ; then - _jukebox_webapp_download - fi - if [[ $ENABLE_INSTALL_NODE == true ]] ; then - _jukebox_webapp_install_node - # Local Web App build during installation does not work at the moment - # Needs to be done after reboot! There will be a message at the end of the installation process - # _jukebox_webapp_build - fi - _jukebox_webapp_register_as_system_service_with_nginx + if [[ $ENABLE_WEBAPP_PROD_DOWNLOAD == true || $ENABLE_WEBAPP_PROD_DOWNLOAD == release-only ]] ; then + verify_dirs_exists "${INSTALLATION_PATH}/src/webapp/build" + fi + if [[ $ENABLE_INSTALL_NODE == true ]] ; then + verify_apt_packages nodejs + fi + + verify_apt_packages nginx + verify_files_exists "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" - echo "DONE: setup_jukebox_webapp" + verify_service_enablement nginx.service enabled +} + +_run_setup_jukebox_webapp() { + if [[ $ENABLE_WEBAPP_PROD_DOWNLOAD == true || $ENABLE_WEBAPP_PROD_DOWNLOAD == release-only ]] ; then + _jukebox_webapp_download + fi + if [[ $ENABLE_INSTALL_NODE == true ]] ; then + _jukebox_webapp_install_node + # Local Web App build during installation does not work at the moment + # Needs to be done after reboot! There will be a message at the end of the installation process + # _jukebox_webapp_build + fi + _jukebox_webapp_register_as_system_service_with_nginx + _jukebox_webapp_check +} + +setup_jukebox_webapp() { + if [ "$ENABLE_WEBAPP" == true ] ; then + run_with_log_frame _run_setup_jukebox_webapp "Install web application" + fi } diff --git a/installation/routines/setup_kiosk_mode.sh b/installation/routines/setup_kiosk_mode.sh index b2857b624..b6e543768 100644 --- a/installation/routines/setup_kiosk_mode.sh +++ b/installation/routines/setup_kiosk_mode.sh @@ -1,6 +1,13 @@ #!/usr/bin/env bash +KIOSK_MODE_CONF_HEADER="## Jukebox Kiosk Mode" +KIOSK_MODE_XINITRC='/etc/xdg/openbox/autostart' +KIOSK_MODE_BASHRC="${HOME_PATH}/.bashrc" +KIOSK_MODE_CHROMIUM_CUSTOM_DISABLE_UPDATE_CHECK='/etc/chromium-browser/customizations/01-disable-update-check' +KIOSK_MODE_CHROMIUM_FLAG_UPDATE_INTERVAL='--check-for-update-interval=31536000' + _kiosk_mode_install_os_dependencies() { + print_lc " Install Kiosk Mode dependencies" # Resource: # https://blog.r0b.io/post/minimal-rpi-kiosk/ sudo apt-get -qq -y install --no-install-recommends \ @@ -12,19 +19,20 @@ _kiosk_mode_install_os_dependencies() { } _kiosk_mode_set_autostart() { + print_lc " Configure Kiosk Mode" local _DISPLAY='$DISPLAY' local _XDG_VTNR='$XDG_VTNR' - cat << EOF >> /home/pi/.bashrc -## Jukebox kiosk autostart + tee -a "${KIOSK_MODE_BASHRC}" <<-EOF + +${KIOSK_MODE_CONF_HEADER} [[ -z $_DISPLAY && $_XDG_VTNR -eq 1 ]] && startx -- -nocursor EOF - local XINITRC='/etc/xdg/openbox/autostart' - cat << EOF | sudo tee -a $XINITRC + sudo tee -a "${KIOSK_MODE_XINITRC}" <<-EOF -## Jukebox Kiosk Mode +${KIOSK_MODE_CONF_HEADER} # Disable any form of screen saver / screen blanking / power management xset s off xset s noblank @@ -46,16 +54,43 @@ EOF _kiosk_mode_update_settings() { # Resource: https://github.com/Thyraz/Sonos-Kids-Controller/blob/d1f061f4662c54ae9b8dc8b545f9c3ba39f670eb/README.md#kiosk-mode-installation - sudo touch /etc/chromium-browser/customizations/01-disable-update-check;echo CHROMIUM_FLAGS=\"\$\{CHROMIUM_FLAGS\} --check-for-update-interval=31536000\" | sudo tee /etc/chromium-browser/customizations/01-disable-update-check + sudo mkdir -p $(dirname "${KIOSK_MODE_CHROMIUM_CUSTOM_DISABLE_UPDATE_CHECK}") + sudo rm -f "${KIOSK_MODE_CHROMIUM_CUSTOM_DISABLE_UPDATE_CHECK}" + sudo tee -a "${KIOSK_MODE_CHROMIUM_CUSTOM_DISABLE_UPDATE_CHECK}" <<-EOF +${KIOSK_MODE_CONF_HEADER} +CHROMIUM_FLAGS=\"\$\{CHROMIUM_FLAGS\} --check-for-update-interval=31536000\" +EOF +} + +_kiosk_mode_check() { + print_verify_installation + + verify_apt_packages xserver-xorg \ + x11-xserver-utils \ + xinit \ + openbox \ + chromium-browser + verify_files_exists "${KIOSK_MODE_BASHRC}" + verify_file_contains_string "${KIOSK_MODE_CONF_HEADER}" "${KIOSK_MODE_BASHRC}" + + verify_files_exists "${KIOSK_MODE_XINITRC}" + verify_file_contains_string "${KIOSK_MODE_CONF_HEADER}" "${KIOSK_MODE_XINITRC}" + + verify_files_exists "${KIOSK_MODE_CHROMIUM_CUSTOM_DISABLE_UPDATE_CHECK}" + verify_file_contains_string "${KIOSK_MODE_CONF_HEADER}" "${KIOSK_MODE_CHROMIUM_CUSTOM_DISABLE_UPDATE_CHECK}" } -setup_kiosk_mode() { - echo "Setup Kiosk Mode" | tee /dev/fd/3 +_run_setup_kiosk_mode() { + _kiosk_mode_install_os_dependencies + _kiosk_mode_set_autostart + _kiosk_mode_update_settings + _kiosk_mode_check +} - _kiosk_mode_install_os_dependencies - _kiosk_mode_set_autostart - _kiosk_mode_update_settings - echo "DONE: setup_kiosk_mode" +setup_kiosk_mode() { + if [ "$ENABLE_KIOSK_MODE" == true ] ; then + run_with_log_frame _run_setup_kiosk_mode "Setup Kiosk Mode" + fi } diff --git a/installation/routines/setup_mpd.sh b/installation/routines/setup_mpd.sh index 53b9ac01b..6a95a95e5 100644 --- a/installation/routines/setup_mpd.sh +++ b/installation/routines/setup_mpd.sh @@ -3,14 +3,12 @@ AUDIOFOLDERS_PATH="${SHARED_PATH}/audiofolders" PLAYLISTS_PATH="${SHARED_PATH}/playlists" -# Do not change this directory! It must match MPDs expectation where to find the user configuration -MPD_CONF_PATH="$HOME/.config/mpd/mpd.conf" - _mpd_install_os_dependencies() { + log " Install MPD OS dependencies" sudo apt-get -y update - echo "Install MPD OS dependencies" - echo "Note: Installing MPD will cause a message: 'Job failed. See journalctl -xe for details'" - echo "It can be ignored! It's an artefact of the MPD installation - nothing we can do about it." + + log "Note: Installing MPD might cause a message: 'Job failed. See journalctl -xe for details' +It can be ignored! It's an artefact of the MPD installation - nothing we can do about it." sudo apt-get -y install \ mpd mpc \ --no-install-recommends \ @@ -20,8 +18,15 @@ _mpd_install_os_dependencies() { } _mpd_configure() { + print_lc " Configure MPD as user local service" + + # Make sure system-wide mpd is disabled + sudo systemctl stop mpd.socket + sudo systemctl stop mpd.service + sudo systemctl disable mpd.socket + sudo systemctl disable mpd.service # MPD will be setup as user process (rather than a system-wide process) - mkdir -p ~/.config/mpd + mkdir -p $(dirname "$MPD_CONF_PATH") cp -f "${INSTALLATION_PATH}/resources/default-settings/mpd.default.conf" "${MPD_CONF_PATH}" @@ -29,48 +34,38 @@ _mpd_configure() { sed -i 's|%%JUKEBOX_AUDIOFOLDERS_PATH%%|'"$AUDIOFOLDERS_PATH"'|' "${MPD_CONF_PATH}" sed -i 's|%%JUKEBOX_PLAYLISTS_PATH%%|'"$PLAYLISTS_PATH"'|' "${MPD_CONF_PATH}" + # Prepare user-service MPD to be started at next boot + systemctl --user daemon-reload + systemctl --user enable mpd.socket + systemctl --user enable mpd.service } -setup_mpd() { - echo "Install MPD" | tee /dev/fd/3 +_mpd_check() { + print_verify_installation - local MPD_EXECUTE_INSTALL=true + verify_apt_packages mpd mpc - if [[ -f ${MPD_CONF_PATH} || -f ${SYSTEMD_USR_PATH}/mpd.service ]]; then - echo "It seems there is a MPD already installed. -Note: It is important that MPD runs as a user service! -Would you like to overwrite your configuration? [Y/n]" 1>&3 - read -r response - case "$response" in - [nN][oO]|[nN]) - MPD_EXECUTE_INSTALL=false - ;; - *) - ;; - esac - fi + verify_files_chmod_chown 755 "${CURRENT_USER}" "${CURRENT_USER_GROUP}" "${MPD_CONF_PATH}" - echo "MPD_EXECUTE_INSTALL=${MPD_EXECUTE_INSTALL}" + verify_file_contains_string "${AUDIOFOLDERS_PATH}" "${MPD_CONF_PATH}" + verify_file_contains_string "${PLAYLISTS_PATH}" "${MPD_CONF_PATH}" - if [[ $MPD_EXECUTE_INSTALL == true ]] ; then + verify_service_enablement mpd.socket disabled + verify_service_enablement mpd.service disabled - # Install/update only if enabled: do not stuff up any existing configuration - _mpd_install_os_dependencies + verify_service_enablement mpd.socket enabled --user + verify_service_enablement mpd.service enabled --user +} - # Make sure system-wide mpd is disabled - echo "Configure MPD as user local service" | tee /dev/fd/3 - sudo systemctl stop mpd.socket - sudo systemctl stop mpd - sudo systemctl disable mpd.socket - sudo systemctl disable mpd +_run_setup_mpd() { + _mpd_install_os_dependencies _mpd_configure - # Prepare user-service MPD to be started at next boot - systemctl --user daemon-reload - systemctl --user enable mpd.socket - systemctl --user enable mpd - # Start MPD now, but not the socket: MPD is already started and we expect a reboot anyway - systemctl --user start mpd - fi - - echo "DONE: setup_mpd" + _mpd_check +} + +setup_mpd() { + # Install/update only if enabled: do not stuff up any existing configuration + if [[ "$SETUP_MPD" == true && $ENABLE_MPD_OVERWRITE_INSTALL == true ]] ; then + run_with_log_frame _run_setup_mpd "Install MPD" + fi } diff --git a/installation/routines/setup_rfid_reader.sh b/installation/routines/setup_rfid_reader.sh index 4ae693076..3003d79a4 100644 --- a/installation/routines/setup_rfid_reader.sh +++ b/installation/routines/setup_rfid_reader.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash -setup_rfid_reader() { - echo "Install RFID Reader" | tee /dev/fd/3 - - python "${INSTALLATION_PATH}/src/jukebox/run_register_rfid_reader.py" | tee /dev/fd/3 +_run_setup_rfid_reader() { + run_and_print_lc python "${INSTALLATION_PATH}/src/jukebox/run_register_rfid_reader.py" +} - echo "DONE: setup_rfid_reader" +setup_rfid_reader() { + if [ "$ENABLE_RFID_READER" == true ] ; then + run_with_log_frame _run_setup_rfid_reader "Install RFID Reader" + fi } diff --git a/installation/routines/setup_samba.sh b/installation/routines/setup_samba.sh index 0914439b7..c1875113e 100644 --- a/installation/routines/setup_samba.sh +++ b/installation/routines/setup_samba.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash +SMB_CONF="/etc/samba/smb.conf" +SMB_CONF_HEADER="## Jukebox Samba Config" + _samba_install_os_dependencies() { - echo "Install Samba Core dependencies" + log " Install Samba Core dependencies" sudo apt-get -qq -y update; sudo apt-get -qq -y install \ samba samba-common-bin \ --no-install-recommends \ @@ -11,25 +14,24 @@ _samba_install_os_dependencies() { } _samba_set_user() { - local SMB_CONF="/etc/samba/smb.conf" - local SMB_USER="pi" + print_lc " Configure Samba" local SMB_PASSWD="raspberry" # Samba has not been configured - if grep -q "## Jukebox Samba Config" "$SMB_CONF"; then - echo " Skipping. Already set up!" | tee /dev/fd/3 + if grep -q "$SMB_CONF_HEADER" "$SMB_CONF"; then + print_lc " Skipping. Already set up!" else # Create Samba user - (echo "${SMB_PASSWD}"; echo "${SMB_PASSWD}") | sudo smbpasswd -s -a $SMB_USER + (echo "${SMB_PASSWD}"; echo "${SMB_PASSWD}") | sudo smbpasswd -s -a "${CURRENT_USER}" sudo chown root:root $SMB_CONF sudo chmod 777 $SMB_CONF # Create Samba Mount Points sudo cat << EOF >> $SMB_CONF -## Jukebox Samba Config +${SMB_CONF_HEADER} [phoniebox] - comment= Pi Jukebox + comment=Pi Jukebox path=${SHARED_PATH} browseable=Yes writeable=Yes @@ -43,13 +45,31 @@ EOF fi } -setup_samba() { - echo "Install Samba and configure user" | tee /dev/fd/3 +_samba_check() { + print_verify_installation + + verify_apt_packages samba samba-common-bin - # Skip interactive Samba WINS config dialog - echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections - _samba_install_os_dependencies - _samba_set_user + verify_files_chmod_chown 644 root root "${SMB_CONF}" - echo "DONE: setup_samba" + verify_file_contains_string "${SMB_CONF_HEADER}" "${SMB_CONF}" + verify_file_contains_string "${SHARED_PATH}" "${SMB_CONF}" + + if ! (sudo pdbedit -L | grep -qw "^${CURRENT_USER}") ; then + exit_on_error "ERROR: samba user not found" + fi +} + +_run_setup_samba() { + # Skip interactive Samba WINS config dialog + echo "samba-common samba-common/dhcp boolean false" | sudo debconf-set-selections + _samba_install_os_dependencies + _samba_set_user + _samba_check +} + +setup_samba() { + if [ "$ENABLE_SAMBA" == true ] ; then + run_with_log_frame _run_setup_samba "Install Samba" + fi } diff --git a/installation/routines/update_raspi_os.sh b/installation/routines/update_raspi_os.sh index b7c356454..f38e975ed 100644 --- a/installation/routines/update_raspi_os.sh +++ b/installation/routines/update_raspi_os.sh @@ -1,9 +1,14 @@ #!/usr/bin/env bash -update_raspi_os() { - echo "Updating Raspberry Pi OS" | tee /dev/fd/3 - - sudo apt-get -qq -y update; sudo apt-get -qq -y full-upgrade; sudo apt-get -qq -y autoremove +_run_update_raspi_os() { + sudo apt-get -qq -y update && sudo apt-get -qq -y full-upgrade || exit_on_error "Failed to Update Raspberry Pi OS" + if [ "$CI_RUNNING" != "true" ]; then + sudo apt-get -qq -y autoremove + fi +} - echo "DONE: update_raspi_os" +update_raspi_os() { + if [ "$UPDATE_RASPI_OS" == true ] ; then + run_with_log_frame _run_update_raspi_os "Updating Raspberry Pi OS" + fi } diff --git a/packages-core.txt b/packages-core.txt new file mode 100644 index 000000000..b2f6779a2 --- /dev/null +++ b/packages-core.txt @@ -0,0 +1,17 @@ +# Define packages for apt-get. These can be installed with +# 'sed 's/#.*//g' packages.txt | xargs sudo apt-get install' + +at +alsa-utils +caps +espeak +ffmpeg +libasound2-dev +mpg123 +pulseaudio +pulseaudio-module-bluetooth +pulseaudio-utils +python3 +python3-venv +python3-dev +rsync diff --git a/requirements.txt b/requirements.txt index 97e47fe24..a04c8c80b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ wheel evdev pyalsaaudio pulsectl -python_mpd2 +python-mpd2 ruamel.yaml # For playlistgenerator requests @@ -30,8 +30,8 @@ gpiozero # On regular Linux PCs, Websocket is enabled in the Python package # pyzmq - # Code quality flake8>=4.0.0 pytest +pytest-cov mock diff --git a/resources/autohotspot/autohotspot.timer b/resources/autohotspot/autohotspot.timer index a35f15757..eb2acaebe 100644 --- a/resources/autohotspot/autohotspot.timer +++ b/resources/autohotspot/autohotspot.timer @@ -1,3 +1,3 @@ # cron timer for autohotspot -*/5 * * * * pi sudo /usr/bin/autohotspot 2>&1 | logger -t autohotspot -@reboot pi sudo /usr/bin/autohotspot 2>&1 | logger -t autohotspot +*/5 * * * * %%USER%% sudo /usr/bin/autohotspot 2>&1 | logger -t autohotspot +@reboot %%USER%% sudo /usr/bin/autohotspot 2>&1 | logger -t autohotspot diff --git a/resources/default-settings/jukebox.default.yaml b/resources/default-settings/jukebox.default.yaml index 7120a95b4..5d0a7a62d 100644 --- a/resources/default-settings/jukebox.default.yaml +++ b/resources/default-settings/jukebox.default.yaml @@ -1,5 +1,5 @@ # IMPORTANT: -# Do not use paths with '~/some/dir' - always use '/home/pi/some/dir' +# Always use relative path from settingsfile `../../`, but do not use relative paths with `~/`. # Sole (!) exception is in playermpd.mpd_conf system: box_name: Jukebox @@ -30,7 +30,7 @@ pulse: toggle_on_connect: true # Limit maximum volume range to XX % - can be changed through the UI (for temporary use) soft_max_volume: 70 - # Run the tool run_configure_audio.py to configure the pulseaudio sinks + # Run the audio configuration tool to configure the pulseaudio sinks # # After startup, the audio output defaults to primary # Any Bluetooth device should be the secondary (as it may not always be available directly after boot) diff --git a/resources/default-settings/nginx.default b/resources/default-settings/nginx.default index b949beb26..d664f4cdd 100644 --- a/resources/default-settings/nginx.default +++ b/resources/default-settings/nginx.default @@ -2,7 +2,7 @@ server { listen 80 default_server; listen [::]:80 default_server; - root /home/pi/RPi-Jukebox-RFID/src/webapp/build; + root %%INSTALLATION_PATH%%/src/webapp/build; index index.html index.htm; @@ -21,7 +21,7 @@ server { } location /logs { - root /home/pi/RPi-Jukebox-RFID/shared; + root %%INSTALLATION_PATH%%/shared; autoindex on; autoindex_exact_size off; @@ -31,14 +31,14 @@ server { } location @buildwebui { - root /home/pi/RPi-Jukebox-RFID/resources/html; + root %%INSTALLATION_PATH%%/resources/html; try_files /runbuildui.html =404; internal; } error_page 404 = /404.html; location /404.html { - root /home/pi/RPi-Jukebox-RFID/resources/html; + root %%INSTALLATION_PATH%%/resources/html; internal; } } diff --git a/run_pytest.sh b/run_pytest.sh index a3cbd8df6..766f05182 100755 --- a/run_pytest.sh +++ b/run_pytest.sh @@ -10,4 +10,4 @@ SCRIPT_DIR="$(dirname "$SOURCE")" cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) # Run pytest -pytest -c pytest.ini +pytest -c pytest.ini $@ diff --git a/src/cli_client/pbc.c b/src/cli_client/pbc.c index 8dcc462cb..6653ade86 100644 --- a/src/cli_client/pbc.c +++ b/src/cli_client/pbc.c @@ -47,6 +47,7 @@ int g_verbose = 0; typedef struct { char object [MAX_STRLEN]; + char package [MAX_STRLEN]; char method [MAX_STRLEN]; char params [MAX_PARAMS][MAX_STRLEN]; int num_params; @@ -126,7 +127,7 @@ void * connect_and_send_request(t_request * tr) } else sprintf(kwargs, "\"kwargs\":{},"); - snprintf(json_request,MAX_REQEST_STRLEN,"{\"plugin\": \"%s\", \"method\": \"%s\", %s\"id\":%d}",tr->object,tr->method,kwargs,123); + snprintf(json_request,MAX_REQEST_STRLEN,"{\"package\": \"%s\", \"plugin\": \"%s\", \"method\": \"%s\", %s\"id\":%d}",tr->package,tr->object,tr->method,kwargs,123); json_len = strlen(json_request); if (g_verbose) printf("Sending Request (%ld Bytes):\n%s\n",json_len,json_request); @@ -146,7 +147,7 @@ int check_and_map_parameters_to_json(char * arg, t_request * tr) { name = strtok(arg, ":"); value = strtok(NULL, ":"); - fmt = (isdigit(*value)) ? "\"%s\":%s" : "\"%s\":\"%s\""; + fmt = (isdigit(*value)||*value=='-') ? "\"%s\":%s" : "\"%s\":\"%s\""; snprintf (tr->params[tr->num_params++],MAX_STRLEN, fmt,name,value); ret = 1; } @@ -156,9 +157,10 @@ int check_and_map_parameters_to_json(char * arg, t_request * tr) void usage(void) { - fprintf(stderr,"\npbc -> PhonieBox Command line interface\nusage: pbc -o object -m method param_name:value\n\n"); + fprintf(stderr,"\npbc -> PhonieBox Command line interface\nusage: pbc -p package -o plugin -m method param_name:value\n\n"); fprintf(stderr," -h this screen\n"); - fprintf(stderr," -o, --object object\n"); + fprintf(stderr," -p, --package package\n"); + fprintf(stderr," -o, --object plugin\n"); fprintf(stderr," -m, --method method\n"); fprintf(stderr," -a, --address default=tcp://localhost:5555\n"); fprintf(stderr," -v verbose\n"); @@ -184,6 +186,7 @@ int HandleOptions(int argc,char *argv[], t_request * tr) /* These options don't set a flag. We distinguish them by their indices. */ {"help", no_argument, 0, 'h'}, + {"package", required_argument, 0, 'p'}, {"object", required_argument, 0, 'o'}, {"method", required_argument, 0, 'm'}, {"address", required_argument, 0, 'a'}, @@ -208,7 +211,9 @@ int HandleOptions(int argc,char *argv[], t_request * tr) usage(); puts ("option -a\n"); break; - + case 'p': + strncpy (tr->package,optarg,MAX_STRLEN); + break; case 'o': strncpy (tr->object,optarg,MAX_STRLEN); break; diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py new file mode 100644 index 000000000..3975fdb67 --- /dev/null +++ b/src/jukebox/components/playermpd/__init__.py @@ -0,0 +1,663 @@ +# -*- coding: utf-8 -*- +""" +Package for interfacing with the MPD Music Player Daemon + +Status information in three topics +1) Player Status: published only on change + This is a subset of the MPD status (and not the full MPD status) ?? + - folder + - song + - volume (volume is published only via player status, and not separatly to avoid too many Threads) + - ... +2) Elapsed time: published every 250 ms, unless constant + - elapsed +3) Folder Config: published only on change + This belongs to the folder being played + Publish: + - random, resume, single, loop + On save store this information: + Contains the information for resume functionality of each folder + - random, resume, single, loop + - if resume: + - current song, elapsed + - what is PLAYSTATUS for? + When to save + - on stop + Angstsave: + - on pause (only if box get turned off without proper shutdown - else stop gets implicitly called) + - on status change of random, resume, single, loop (for resume omit current status if currently playing- this has now meaning) + Load checks: + - if resume, but no song, elapsed -> log error and start from the beginning + +Status storing: + - Folder config for each folder (see above) + - Information to restart last folder playback, which is: + - last_folder -> folder_on_close + - song, elapsed + - random, resume, single, loop + - if resume is enabled, after start we need to set last_played_folder, such that card swipe is detected as second swipe?! + on the other hand: if resume is enabled, this is also saved to folder.config -> and that is checked by play card + +Internal status + - last played folder: Needed to detect second swipe + + +Saving {'player_status': {'last_played_folder': 'TraumfaengerStarkeLieder', 'CURRENTSONGPOS': '0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3'}, +'audio_folder_status': +{'TraumfaengerStarkeLieder': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'stop', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}, +'Giraffenaffen': {'ELAPSED': '1.0', 'CURRENTFILENAME': 'TraumfaengerStarkeLieder/01.mp3', 'CURRENTSONGPOS': '0', 'PLAYSTATUS': 'play', 'RESUME': 'OFF', 'SHUFFLE': 'OFF', 'LOOP': 'OFF', 'SINGLE': 'OFF'}}} + +References: +https://github.com/Mic92/python-mpd2 +https://python-mpd2.readthedocs.io/en/latest/topics/commands.html +https://mpd.readthedocs.io/en/latest/protocol.html + +sudo -u mpd speaker-test -t wav -c 2 +""" # noqa: E501 +# Warum ist "Second Swipe" im Player und nicht im RFID Reader? +# Second swipe ist abhängig vom Player State - nicht vom RFID state. +# Beispiel: RFID triggered Folder1, Webapp triggered Folder2, RFID Folder1: Dann muss das 2. Mal Folder1 auch als "first swipe" +# gewertet werden. Wenn der RFID das basierend auf IDs macht, kann der nicht unterscheiden und glaubt es ist 2. Swipe. +# Beispiel 2: Jemand hat RFID Reader (oder 1x RFID und 1x Barcode Scanner oder so) angeschlossen. Liest zuerst Karte mit +# Reader 1 und dann mit Reader 2: Reader 2 weiß nicht, was bei Reader 1 passiert ist und denkt es ist 1. swipe. +# Beispiel 3: RFID trigered Folder1, Playlist läuft durch und hat schon gestoppt, dann wird die Karte wieder vorgehalten. +# Dann muss das als 1. Swipe gewertet werden +# Beispiel 4: RFID triggered "Folder1", dann wird Karte "Volume Up" aufgelegt, dann wieder Karte "Folder1": Auch das ist +# aus Sicht ders Playbacks 2nd Swipe +# 2nd Swipe ist keine im Reader festgelegte Funktion extra fur den Player. +# +# In der aktuellen Implementierung weiß der Player (der second "swipe" dekodiert) überhaupt nichts vom RFID. +# Im Prinzip gibt es zwei "Play" Funktionen: (1) play always from start und (2) play with toggle action. +# Die Webapp ruft immer (1) auf und die RFID immer (2). Jetzt kann man sogar für einige Karten sagen +# immer (1) - also kein Second Swipe und für andere (2). +# Sollte der Reader das Swcond swipe dekodieren, muss aber der Reader den Status des Player kennen. +# Das ist allerdings ein Problem. In Version 2 ist das nicht aufgefallen, +# weil alles uber File I/Os lief - Thread safe ist das nicht! +# +# Beispiel: Second swipe bei anderen Funktionen, hier: WiFi on/off. +# Was die Karte Action tut ist ein Toggle. Der Toggle hängt vom Wifi State ab, den der RFID Kartenleser nicht kennt. +# Den kann der Leser auch nicht tracken. Der State kann ja auch über die WebApp oder Kommandozeile geändert werden. +# Toggle (und 2nd Swipe generell) ist immer vom Status des Zielsystems abhängig und kann damit nur vom Zielsystem geändert +# werden. Bei Wifi also braucht man 3 Funktionen: on / off / toggle. Toggle ist dann first swipe / second swipe + +import os +import mpd +import threading +import logging +import time +import functools +import components.player +import jukebox.cfghandler +import jukebox.utils as utils +import jukebox.plugs as plugs +import jukebox.multitimer as multitimer +import jukebox.publishing as publishing +import jukebox.playlistgenerator as playlistgenerator +import misc + +from jukebox.NvManager import nv_manager +from .playcontentcallback import PlayContentCallbacks, PlayCardState + +logger = logging.getLogger('jb.PlayerMPD') +cfg = jukebox.cfghandler.get_handler('jukebox') + + +class MpdLock: + def __init__(self, client: mpd.MPDClient, host: str, port: int): + self._lock = threading.RLock() + self.client = client + self.host = host + self.port = port + + def _try_connect(self): + try: + self.client.connect(self.host, self.port) + except mpd.base.ConnectionError: + pass + + def __enter__(self): + self._lock.acquire() + self._try_connect() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._lock.release() + + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + locked = self._lock.acquire(blocking, timeout) + if locked: + self._try_connect() + return locked + + def release(self): + self._lock.release() + + def locked(self): + return self._lock.locked() + + +class PlayerMPD: + """Interface to MPD Music Player Daemon""" + + def __init__(self): + self.nvm = nv_manager() + self.mpd_host = cfg.getn('playermpd', 'host') + self.music_player_status = self.nvm.load(cfg.getn('playermpd', 'status_file')) + + self.second_swipe_action_dict = {'toggle': self.toggle, + 'play': self.play, + 'skip': self.next, + 'rewind': self.rewind, + 'replay': self.replay, + 'replay_if_stopped': self.replay_if_stopped} + self.second_swipe_action = None + self.decode_2nd_swipe_option() + + self.mpd_client = mpd.MPDClient() + # The timeout refer to the low-level socket time-out + # If these are too short and the response is not fast enough (due to the PI being busy), + # the current MPC command times out. Leave these at blocking calls, since we do not react on a timed out socket + # in any relevant matter anyway + self.mpd_client.timeout = None # network timeout in seconds (floats allowed), default: None + self.mpd_client.idletimeout = None # timeout for fetching the result of the idle command + self.connect() + logger.info(f"Connected to MPD Version: {self.mpd_client.mpd_version}") + + self.current_folder_status = {} + if not self.music_player_status: + self.music_player_status['player_status'] = {} + self.music_player_status['audio_folder_status'] = {} + self.music_player_status.save_to_json() + self.current_folder_status = {} + self.music_player_status['player_status']['last_played_folder'] = '' + else: + last_played_folder = self.music_player_status['player_status'].get('last_played_folder') + if last_played_folder: + # current_folder_status is a dict, but last_played_folder a str + self.current_folder_status = self.music_player_status['audio_folder_status'][last_played_folder] + # Restore the playlist status in mpd + # But what about playback position? + self.mpd_client.clear() + # This could fail and cause load fail of entire package: + # self.mpd_client.add(last_played_folder) + logger.info(f"Last Played Folder: {last_played_folder}") + + # Clear last folder played, as we actually did not play any folder yet + # Needed for second swipe detection + # TODO: This will loose the last_played_folder information is the box is started and closed with playing anything... + # Change this to last_played_folder and shutdown_state (for restoring) + self.music_player_status['player_status']['last_played_folder'] = '' + + self.old_song = None + self.mpd_status = {} + self.mpd_status_poll_interval = 0.25 + self.mpd_lock = MpdLock(self.mpd_client, self.mpd_host, 6600) + self.status_is_closing = False + # self.status_thread = threading.Timer(self.mpd_status_poll_interval, self._mpd_status_poll).start() + + self.status_thread = multitimer.GenericEndlessTimerClass('mpd.timer_status', + self.mpd_status_poll_interval, self._mpd_status_poll) + self.status_thread.start() + + def exit(self): + logger.debug("Exit routine of playermpd started") + self.status_is_closing = True + self.status_thread.cancel() + self.mpd_client.disconnect() + self.nvm.save_all() + return self.status_thread.timer_thread + + def connect(self): + self.mpd_client.connect(self.mpd_host, 6600) + + def decode_2nd_swipe_option(self): + cfg_2nd_swipe_action = cfg.setndefault('playermpd', 'second_swipe_action', 'alias', value='none').lower() + if cfg_2nd_swipe_action not in [*self.second_swipe_action_dict.keys(), 'none', 'custom']: + logger.error(f"Config mpd.second_swipe_action must be one of " + f"{[*self.second_swipe_action_dict.keys(), 'none', 'custom']}. Ignore setting.") + if cfg_2nd_swipe_action in self.second_swipe_action_dict.keys(): + self.second_swipe_action = self.second_swipe_action_dict[cfg_2nd_swipe_action] + if cfg_2nd_swipe_action == 'custom': + custom_action = utils.decode_rpc_call(cfg.getn('playermpd', 'second_swipe_action', default=None)) + self.second_swipe_action = functools.partial(plugs.call_ignore_errors, + custom_action['package'], + custom_action['plugin'], + custom_action['method'], + custom_action['args'], + custom_action['kwargs']) + + def mpd_retry_with_mutex(self, mpd_cmd, *args): + """ + This method adds thread saftey for acceses to mpd via a mutex lock, + it shall be used for each access to mpd to ensure thread safety + In case of a communication error the connection will be reestablished and the pending command will be repeated 2 times + + I think this should be refactored to a decorator + """ + with self.mpd_lock: + try: + value = mpd_cmd(*args) + except Exception as e: + logger.error(f"{e.__class__.__qualname__}: {e}") + value = None + return value + + def _mpd_status_poll(self): + """ + this method polls the status from mpd and stores the important inforamtion in the music_player_status, + it will repeat itself in the intervall specified by self.mpd_status_poll_interval + """ + self.mpd_status.update(self.mpd_retry_with_mutex(self.mpd_client.status)) + self.mpd_status.update(self.mpd_retry_with_mutex(self.mpd_client.currentsong)) + + if self.mpd_status.get('elapsed') is not None: + self.current_folder_status["ELAPSED"] = self.mpd_status['elapsed'] + self.music_player_status['player_status']["CURRENTSONGPOS"] = self.mpd_status['song'] + self.music_player_status['player_status']["CURRENTFILENAME"] = self.mpd_status['file'] + + if self.mpd_status.get('file') is not None: + self.current_folder_status["CURRENTFILENAME"] = self.mpd_status['file'] + self.current_folder_status["CURRENTSONGPOS"] = self.mpd_status['song'] + self.current_folder_status["ELAPSED"] = self.mpd_status.get('elapsed', '0.0') + self.current_folder_status["PLAYSTATUS"] = self.mpd_status['state'] + self.current_folder_status["RESUME"] = "OFF" + self.current_folder_status["SHUFFLE"] = "OFF" + self.current_folder_status["LOOP"] = "OFF" + self.current_folder_status["SINGLE"] = "OFF" + + # Delete the volume key to avoid confusion + # Volume is published via the 'volume' component! + try: + del self.mpd_status['volume'] + except KeyError: + pass + publishing.get_publisher().send('playerstatus', self.mpd_status) + + @plugs.tag + def get_player_type_and_version(self): + with self.mpd_lock: + value = self.mpd_client.mpd_version() + return value + + @plugs.tag + def update(self): + with self.mpd_lock: + state = self.mpd_client.update() + return state + + @plugs.tag + def update_wait(self): + state = self.update() + self._db_wait_for_update(state) + return state + + @plugs.tag + def play(self): + with self.mpd_lock: + self.mpd_client.play() + + @plugs.tag + def stop(self): + with self.mpd_lock: + self.mpd_client.stop() + + @plugs.tag + def pause(self, state: int = 1): + """Enforce pause to state (1: pause, 0: resume) + + This is what you want as card removal action: pause the playback, so it can be resumed when card is placed + on the reader again. What happens on re-placement depends on configured second swipe option + """ + with self.mpd_lock: + self.mpd_client.pause(state) + + @plugs.tag + def prev(self): + logger.debug("Prev") + with self.mpd_lock: + self.mpd_client.previous() + + @plugs.tag + def next(self): + """Play next track in current playlist""" + logger.debug("Next") + with self.mpd_lock: + self.mpd_client.next() + + @plugs.tag + def seek(self, new_time): + with self.mpd_lock: + self.mpd_client.seekcur(new_time) + + @plugs.tag + def shuffle(self, random): + # As long as we don't work with waiting lists (aka playlist), this implementation is ok! + self.mpd_retry_with_mutex(self.mpd_client.random, 1 if random else 0) + + @plugs.tag + def rewind(self): + """ + Re-start current playlist from first track + + Note: Will not re-read folder config, but leave settings untouched""" + logger.debug("Rewind") + with self.mpd_lock: + self.mpd_client.play(1) + + @plugs.tag + def replay(self): + """ + Re-start playing the last-played folder + + Will reset settings to folder config""" + logger.debug("Replay") + with self.mpd_lock: + self.play_folder(self.music_player_status['player_status']['last_played_folder']) + + @plugs.tag + def toggle(self): + """Toggle pause state, i.e. do a pause / resume depending on current state""" + logger.debug("Toggle") + with self.mpd_lock: + self.mpd_client.pause() + + @plugs.tag + def replay_if_stopped(self): + """ + Re-start playing the last-played folder unless playlist is still playing + + .. note:: To me this seems much like the behaviour of play, + but we keep it as it is specifically implemented in box 2.X""" + with self.mpd_lock: + if self.mpd_status['state'] == 'stop': + self.play_folder(self.music_player_status['player_status']['last_played_folder']) + + @plugs.tag + def repeatmode(self, mode): + if mode == 'repeat': + repeat = 1 + single = 0 + elif mode == 'single': + repeat = 1 + single = 1 + else: + repeat = 0 + single = 0 + + with self.mpd_lock: + self.mpd_client.repeat(repeat) + self.mpd_client.single(single) + + @plugs.tag + def get_current_song(self, param): + return self.mpd_status + + @plugs.tag + def map_filename_to_playlist_pos(self, filename): + # self.mpd_client.playlistfind() + raise NotImplementedError + + @plugs.tag + def remove(self): + raise NotImplementedError + + @plugs.tag + def move(self): + # song_id = param.get("song_id") + # step = param.get("step") + # MPDClient.playlistmove(name, from, to) + # MPDClient.swapid(song1, song2) + raise NotImplementedError + + @plugs.tag + def play_single(self, song_url): + with self.mpd_lock: + self.mpd_client.clear() + self.mpd_client.addid(song_url) + self.mpd_client.play() + + @plugs.tag + def resume(self): + with self.mpd_lock: + songpos = self.current_folder_status["CURRENTSONGPOS"] + elapsed = self.current_folder_status["ELAPSED"] + self.mpd_client.seek(songpos, elapsed) + self.mpd_client.play() + + @plugs.tag + def play_card(self, folder: str, recursive: bool = False): + """ + Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content + + Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action + accordingly. + + :param folder: Folder path relative to music library path + :param recursive: Add folder recursively + """ + # Developers notes: + # + # * 2nd swipe trigger may also happen, if playlist has already stopped playing + # --> Generally, treat as first swipe + # * 2nd swipe of same Card ID may also happen if a different song has been played in between from WebUI + # --> Treat as first swipe + # * With place-not-swipe: Card is placed on reader until playlist expieres. Music stop. Card is removed and + # placed again on the reader: Should be like first swipe + # * TODO: last_played_folder is restored after box start, so first swipe of last played card may look like + # second swipe + # + logger.debug(f"last_played_folder = {self.music_player_status['player_status']['last_played_folder']}") + with self.mpd_lock: + is_second_swipe = self.music_player_status['player_status']['last_played_folder'] == folder + if self.second_swipe_action is not None and is_second_swipe: + logger.debug('Calling second swipe action') + + # run callbacks before second_swipe_action is invoked + play_card_callbacks.run_callbacks(folder, PlayCardState.secondSwipe) + + self.second_swipe_action() + else: + logger.debug('Calling first swipe action') + + # run callbacks before play_folder is invoked + play_card_callbacks.run_callbacks(folder, PlayCardState.firstSwipe) + + self.play_folder(folder, recursive) + + @plugs.tag + def get_folder_content(self, folder: str): + """ + Get the folder content as content list with meta-information. Depth is always 1. + + Call repeatedly to descend in hierarchy + + :param folder: Folder path relative to music library path + """ + plc = playlistgenerator.PlaylistCollector(components.player.get_music_library_path()) + plc.get_directory_content(folder) + return plc.playlist + + @plugs.tag + def play_folder(self, folder: str, recursive: bool = False) -> None: + """ + Playback a music folder. + + Folder content is added to the playlist as described by :mod:`jukebox.playlistgenerator`. + The playlist is cleared first. + + :param folder: Folder path relative to music library path + :param recursive: Add folder recursively + """ + # TODO: This changes the current state -> Need to save last state + with self.mpd_lock: + logger.info(f"Play folder: '{folder}'") + self.mpd_client.clear() + + plc = playlistgenerator.PlaylistCollector(components.player.get_music_library_path()) + plc.parse(folder, recursive) + uri = '--unset--' + try: + for uri in plc: + self.mpd_client.addid(uri) + except mpd.base.CommandError as e: + logger.error(f"{e.__class__.__qualname__}: {e} at uri {uri}") + except Exception as e: + logger.error(f"{e.__class__.__qualname__}: {e} at uri {uri}") + + self.music_player_status['player_status']['last_played_folder'] = folder + + self.current_folder_status = self.music_player_status['audio_folder_status'].get(folder) + if self.current_folder_status is None: + self.current_folder_status = self.music_player_status['audio_folder_status'][folder] = {} + + self.mpd_client.play() + + @plugs.tag + def play_album(self, albumartist: str, album: str): + """ + Playback a album found in MPD database. + + All album songs are added to the playlist + The playlist is cleared first. + + :param albumartist: Artist of the Album provided by MPD database + :param album: Album name provided by MPD database + """ + with self.mpd_lock: + logger.info(f"Play album: '{album}' by '{albumartist}") + self.mpd_client.clear() + self.mpd_retry_with_mutex(self.mpd_client.findadd, 'albumartist', albumartist, 'album', album) + self.mpd_client.play() + + @plugs.tag + def queue_load(self, folder): + # There was something playing before -> stop and save state + # Clear the queue + # Check / Create the playlist + # - not needed if same folder is played again? Buf what if files have been added a mpc update has been run? + # - and this a re-trigger to start the new playlist + # If we must update the playlists everytime anyway why write them to file and not just keep them in the queue? + # Load the playlist + # Get folder config and apply settings + pass + + @plugs.tag + def playerstatus(self): + return self.mpd_status + + @plugs.tag + def playlistinfo(self): + with self.mpd_lock: + value = self.mpd_client.playlistinfo() + return value + + # Attention: MPD.listal will consume a lot of memory with large libs.. should be refactored at some point + @plugs.tag + def list_all_dirs(self): + with self.mpd_lock: + result = self.mpd_client.listall() + # list = [entry for entry in list if 'directory' in entry] + return result + + @plugs.tag + def list_albums(self): + with self.mpd_lock: + albums = self.mpd_retry_with_mutex(self.mpd_client.list, 'album', 'group', 'albumartist') + + return albums + + @plugs.tag + def list_song_by_artist_and_album(self, albumartist, album): + with self.mpd_lock: + albums = self.mpd_retry_with_mutex(self.mpd_client.find, 'albumartist', albumartist, 'album', album) + + return albums + + @plugs.tag + def get_song_by_url(self, song_url): + # MPD can play absolute paths but can find songs in its database only by relative path + # In certain situations, `song_url` can be an absolute path. Then, it will be trimed to be relative + _music_library_path_absolute = os.path.expanduser(components.player.get_music_library_path()) + song_url = song_url.replace(f'{_music_library_path_absolute}/', '') + + with self.mpd_lock: + song = self.mpd_retry_with_mutex(self.mpd_client.find, 'file', song_url) + + return song + + def get_volume(self): + """ + Get the current volume + + For volume control do not use directly, but use through the plugin 'volume', + as the user may have configured a volume control manager other than MPD""" + with self.mpd_lock: + volume = self.mpd_client.status().get('volume') + return int(volume) + + def set_volume(self, volume): + """ + Set the volume + + For volume control do not use directly, but use through the plugin 'volume', + as the user may have configured a volume control manager other than MPD""" + with self.mpd_lock: + self.mpd_client.setvol(volume) + return self.get_volume() + + def _db_wait_for_update(self, update_id: int): + logger.debug("Waiting for update to finish") + while self._db_is_updating(update_id): + # a little throttling + time.sleep(0.1) + + def _db_is_updating(self, update_id: int): + with self.mpd_lock: + _status = self.mpd_client.status() + _cur_update_id = _status.get('updating_db') + if _cur_update_id is not None and int(_cur_update_id) <= int(update_id): + return True + else: + return False + + +# --------------------------------------------------------------------------- +# Plugin Initializer / Finalizer +# --------------------------------------------------------------------------- + +player_ctrl: PlayerMPD +#: Callback handler instance for play_card events. +#: - is executed when play_card function is called +#: States: +#: - See :class:`PlayCardState` +#: See :class:`PlayContentCallbacks` +play_card_callbacks: PlayContentCallbacks[PlayCardState] + + +@plugs.initialize +def initialize(): + global player_ctrl + player_ctrl = PlayerMPD() + plugs.register(player_ctrl, name='ctrl') + + global play_card_callbacks + play_card_callbacks = PlayContentCallbacks[PlayCardState]('play_card_callbacks', logger, context=player_ctrl.mpd_lock) + + # Update mpc library + library_update = cfg.setndefault('playermpd', 'library', 'update_on_startup', value=True) + if library_update: + player_ctrl.update() + + # Check user rights on music library + library_check_user_rights = cfg.setndefault('playermpd', 'library', 'check_user_rights', value=True) + if library_check_user_rights is True: + music_library_path = components.player.get_music_library_path() + if music_library_path is not None: + logger.info(f"Change user rights for {music_library_path}") + misc.recursive_chmod(music_library_path, mode_files=0o666, mode_dirs=0o777) + + +@plugs.atexit +def atexit(**ignored_kwargs): + global player_ctrl + return player_ctrl.exit() diff --git a/src/jukebox/components/rfid/configure/__init__.py b/src/jukebox/components/rfid/configure/__init__.py index 7f9e232f2..0a8ff7aca 100755 --- a/src/jukebox/components/rfid/configure/__init__.py +++ b/src/jukebox/components/rfid/configure/__init__.py @@ -10,6 +10,8 @@ logger = logging.getLogger() +NO_RFID_READER = 'No RFID Reader' + def reader_install_dependencies(reader_path: str, dependency_install: str) -> None: """ @@ -80,6 +82,40 @@ def reader_load_module(reader_name): return reader_module +def _get_reader_descriptions(reader_dirs: list[str]) -> dict[str, tuple[str, str]]: + # Try to load the description modules from all valid directories (as this has no dependencies) + # If unavailable, use placeholder description + reader_descriptions = {} + for reader_type in reader_dirs: + reader_description_module_name = '' + reader_description = '' + if reader_type == NO_RFID_READER: + # Add Option to not add a RFid Reader + reader_description_module_name = reader_type + reader_description = reader_type + else: + reader_description_module_name = f"{reader_type + '/' + reader_type + '.py'}" + try: + reader_description_module = (importlib.import_module('components.rfid.hardware.' + reader_type + + '.description', 'pkg.subpkg')) + reader_description = reader_description_module.DESCRIPTION + except ModuleNotFoundError: + # The developer for this reader simply omitted to provide a description module + # Or there is no valid module in this directory, despite correct naming scheme. + # But this we will only find out later, because we want to be as lenient as possible + # and don't already load and check reader modules the user is + # not selecting (and thus no interested in) + logger.warning(f"No module 'description.py' available for reader subpackage '{reader_type}'") + reader_description = '(No description provided!)' + except AttributeError: + # The module loaded ok, but has no identifier 'DESCRIPTION' + logger.warning(f"Module 'description.py' of reader subpackage '{reader_type}' is missing 'DESCRIPTION'. " + f"Spelling error?") + reader_description = '(No description provided!)' + reader_descriptions[reader_type] = (reader_description, reader_description_module_name) + return reader_descriptions + + def query_user_for_reader(dependency_install='query') -> dict: """ Ask the user to select a RFID reader and prompt for the reader's configuration @@ -115,39 +151,20 @@ def query_user_for_reader(dependency_install='query') -> dict: package_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/../hardware') logger.debug(f"Package location: {package_dir}") # For known included readers, specify manual order - included_readers = ['generic_usb', 'rdm6300_serial', 'rc522_spi', 'pn532_i2c_py532', 'fake_reader_gui'] + included_readers = [NO_RFID_READER, 'generic_usb', 'rdm6300_serial', 'rc522_spi', 'pn532_i2c_py532', 'fake_reader_gui'] # Get all local directories (i.e subpackages) that conform to naming/structuring convention (except known readers) # Naming convention: modname/modname.py - reader_dirs = [x for x in os.listdir(package_dir) + additional_readers = [x for x in os.listdir(package_dir) if (os.path.isdir(package_dir + '/' + x) and os.path.exists(package_dir + '/' + x + '/' + x + '.py') and os.path.isfile(package_dir + '/' + x + '/' + x + '.py') and not x.endswith('template_new_reader') and x not in included_readers)] - reader_dirs = [*included_readers, *sorted(reader_dirs, key=lambda x: x.casefold())] + reader_dirs = [*included_readers, *sorted(additional_readers, key=lambda x: x.casefold())] + logger.debug(f"reader_dirs = {reader_dirs}") - # Try to load the description modules from all valid directories (as this has no dependencies) - # If unavailable, use placeholder description - reader_description_modules = [] - reader_descriptions = [] - for reader_type in reader_dirs: - try: - reader_description_modules.append(importlib.import_module('components.rfid.hardware.' + reader_type - + '.description', 'pkg.subpkg')) - reader_descriptions.append(reader_description_modules[-1].DESCRIPTION) - except ModuleNotFoundError: - # The developer for this reader simply omitted to provide a description module - # Or there is no valid module in this directory, despite correct naming scheme. But this we will only find out - # later, because we want to be as lenient as possible and don't already load and check reader modules the user is - # not selecting (and thus no interested in) - logger.warning(f"No module 'description.py' available for reader subpackage '{reader_type}'") - reader_descriptions.append('(No description provided!)') - except AttributeError: - # The module loaded ok, but has no identifier 'DESCRIPTION' - logger.warning(f"Module 'description.py' of reader subpackage '{reader_type}' is missing 'DESCRIPTION'. " - f"Spelling error?") - reader_descriptions.append('(No description provided!)') + reader_descriptions = _get_reader_descriptions(reader_dirs) # Prepare the configuration collector with the base values config_dict = {'rfid': {'readers': {}}} @@ -157,14 +174,21 @@ def query_user_for_reader(dependency_install='query') -> dict: while True: # List all modules and query user print("Choose Reader Module from list:\n") - for idx, (des, mod) in enumerate(zip(reader_descriptions, reader_dirs)): + for idx, (des, mod) in enumerate(reader_descriptions.values()): print(f" {Colors.lightgreen}{idx:2d}{Colors.reset}: {Colors.lightcyan}{Colors.bold}{des:40s}{Colors.reset} " - f"(Module: {mod + '/' + mod + '.py'})") + f"(Module: {mod})") print("") reader_id = pyil.input_int("Reader module number?", min=0, max=len(reader_descriptions) - 1, prompt_color=Colors.lightgreen, prompt_hint=True) + # The (short) name of the selected reader module, which is identical to the directory name - reader_select_name.append(reader_dirs[reader_id]) + reader_selected = list(reader_descriptions.keys())[reader_id] + print(f"Reader selected: '{reader_selected}'") + if reader_selected == NO_RFID_READER: + logger.debug(f"Entry '{NO_RFID_READER}' selected. skip") + break + + reader_select_name.append(reader_selected) # If this reader has not been selected before, auto install dependencies if reader_select_name[-1] not in reader_select_name[:-1]: diff --git a/src/jukebox/components/rfid/hardware/fake_reader_gui/README.md b/src/jukebox/components/rfid/hardware/fake_reader_gui/README.md index 9ba73355a..6144ac1f5 100644 --- a/src/jukebox/components/rfid/hardware/fake_reader_gui/README.md +++ b/src/jukebox/components/rfid/hardware/fake_reader_gui/README.md @@ -1,2 +1,2 @@ -For documentation see [documentation/content/developers/rfid/mock_reader.md](../../../../../../documentation/content/developers/rfid/mock_reader.md). +For documentation see [documentation/developers/rfid/mock_reader.md](../../../../../../documentation/developers/rfid/mock_reader.md). diff --git a/src/jukebox/components/rfid/hardware/fake_reader_gui/requirements.txt b/src/jukebox/components/rfid/hardware/fake_reader_gui/requirements.txt index 931c9db0f..937256e86 100644 --- a/src/jukebox/components/rfid/hardware/fake_reader_gui/requirements.txt +++ b/src/jukebox/components/rfid/hardware/fake_reader_gui/requirements.txt @@ -1,5 +1,6 @@ # This GUI-based mock reader also requires: tkinter # tkinter is a standard Python package and needs not be installed separately # It is available on most Unix systems (but not on headless Raspbian RPi where running a GUI is difficult anyway) +# You need to install these with `python -m pip install --upgrade --force-reinstall -q -r requirements.txt` ttkthemes diff --git a/src/jukebox/components/rfid/hardware/generic_usb/README.md b/src/jukebox/components/rfid/hardware/generic_usb/README.md index e82e9005b..cf61d5822 100644 --- a/src/jukebox/components/rfid/hardware/generic_usb/README.md +++ b/src/jukebox/components/rfid/hardware/generic_usb/README.md @@ -1,2 +1,2 @@ -For documentation see [documentation/content/developers/rfid/genericusb.md](../../../../../../documentation/content/developers/rfid/genericusb.md). +For documentation see [documentation/developers/rfid/genericusb.md](../../../../../../documentation/developers/rfid/genericusb.md). diff --git a/src/jukebox/components/rfid/hardware/pn532_i2c_py532/README.md b/src/jukebox/components/rfid/hardware/pn532_i2c_py532/README.md index d92a17c37..6a342e1d6 100644 --- a/src/jukebox/components/rfid/hardware/pn532_i2c_py532/README.md +++ b/src/jukebox/components/rfid/hardware/pn532_i2c_py532/README.md @@ -1,2 +1,2 @@ -For documentation see [documentation/content/developers/rfid/pn532_i2c.md](../../../../../../documentation/content/developers/rfid/pn532_i2c.md). +For documentation see [documentation/developers/rfid/pn532_i2c.md](../../../../../../documentation/developers/rfid/pn532_i2c.md). diff --git a/src/jukebox/components/rfid/hardware/pn532_i2c_py532/requirements.txt b/src/jukebox/components/rfid/hardware/pn532_i2c_py532/requirements.txt index 9b156854b..f7fe9563f 100644 --- a/src/jukebox/components/rfid/hardware/pn532_i2c_py532/requirements.txt +++ b/src/jukebox/components/rfid/hardware/pn532_i2c_py532/requirements.txt @@ -1,3 +1,4 @@ # PN532 related requirements # You need to install these with `python -m pip install --upgrade --force-reinstall -q -r requirements.txt` + py532lib diff --git a/src/jukebox/components/rfid/hardware/rc522_spi/README.md b/src/jukebox/components/rfid/hardware/rc522_spi/README.md index a78a7a890..8fcbb38a8 100644 --- a/src/jukebox/components/rfid/hardware/rc522_spi/README.md +++ b/src/jukebox/components/rfid/hardware/rc522_spi/README.md @@ -1,2 +1,2 @@ -For documentation see [documentation/content/developers/rfid/mfrc522_spi.md](../../../../../../documentation/content/developers/rfid/mfrc522_spi.md). +For documentation see [documentation/developers/rfid/mfrc522_spi.md](../../../../../../documentation/developers/rfid/mfrc522_spi.md). diff --git a/src/jukebox/components/rfid/hardware/rdm6300_serial/README.md b/src/jukebox/components/rfid/hardware/rdm6300_serial/README.md index 895c24bbf..9f0f39fad 100644 --- a/src/jukebox/components/rfid/hardware/rdm6300_serial/README.md +++ b/src/jukebox/components/rfid/hardware/rdm6300_serial/README.md @@ -1,2 +1,2 @@ -For documentation see [documentation/content/developers/rfid/rdm6300.md](../../../../../../documentation/content/developers/rfid/rdm6300.md). +For documentation see [documentation/developers/rfid/rdm6300.md](../../../../../../documentation/developers/rfid/rdm6300.md). diff --git a/src/jukebox/components/rfid/hardware/rdm6300_serial/requirements.txt b/src/jukebox/components/rfid/hardware/rdm6300_serial/requirements.txt index f6c1a1f57..df92852d9 100644 --- a/src/jukebox/components/rfid/hardware/rdm6300_serial/requirements.txt +++ b/src/jukebox/components/rfid/hardware/rdm6300_serial/requirements.txt @@ -1 +1,4 @@ +# RDM6300 related requirements +# You need to install these with `python -m pip install --upgrade --force-reinstall -q -r requirements.txt` + pyserial diff --git a/src/jukebox/components/rfid/hardware/template_new_reader/README.md b/src/jukebox/components/rfid/hardware/template_new_reader/README.md index 77f3008c2..2ff93198c 100644 --- a/src/jukebox/components/rfid/hardware/template_new_reader/README.md +++ b/src/jukebox/components/rfid/hardware/template_new_reader/README.md @@ -1,41 +1,2 @@ -# Template Reader -*Template for creating and integrating a new RFID Reader* - -> [!NOTE] -> For developers only - -This template provides the skeleton API for a new Reader. If you follow -the conventions outlined below, your new reader will be picked up -automatically There is no extra need to register the reader module with -the Phoniebox. Just re-run `the reader config tool `. - -Follow the instructions in [template_new_reader.py] - -Also have a look at the other reader subpackages to see how stuff works -with an example - -## File structure - -Your new reader is a python subpackage with these three mandatory files - -``` bash -components/rfid/hardware/awesome_reader/ - +- awesome_reader.py <-- The actual reader module - +- description.py <-- A description module w/o dependencies. Do not change the filename! - +- README.rst <-- The Readme -``` - -The module documentation must go into a separate file, called README.ME. - -## Conventions - -- Single reader per directory / subpackage -- reader module directory name and reader module file name must be - identical -- Obviously awesome_reader will be replaced with something more - descriptive. The naming scheme for the subpackage is - - \\_\\_\ - - e.g. generic_usb/generic_usb.py - - e.g. pn532_spi/pn532_spi.py - - ... +For documentation see [documentation/developers/rfid/template_reader.md](../../../../../../documentation/developers/rfid/template_reader.md). diff --git a/src/jukebox/components/rfid/reader/__init__.py b/src/jukebox/components/rfid/reader/__init__.py index 9245a127a..db0ccb1da 100644 --- a/src/jukebox/components/rfid/reader/__init__.py +++ b/src/jukebox/components/rfid/reader/__init__.py @@ -239,14 +239,22 @@ def run(self): # noqa: C901 @plugs.finalize def finalize(): - jukebox.cfghandler.load_yaml(cfg_rfid, cfg_main.getn('rfid', 'reader_config')) - - # Load all the required modules - # Start a ReaderRunner-Thread for each Reader - for reader_cfg_key in cfg_rfid['rfid']['readers'].keys(): - _READERS[reader_cfg_key] = ReaderRunner(reader_cfg_key) - for reader_cfg_key in cfg_rfid['rfid']['readers'].keys(): - _READERS[reader_cfg_key].start() + try: + reader_config_file = cfg_main.getn('rfid', 'reader_config') + jukebox.cfghandler.load_yaml(cfg_rfid, reader_config_file) + except FileNotFoundError: + cfg_rfid.config_dict({'rfid': {'readers': {}}}) + log.warning(f"rfid reader database file not found. Creating empty database: '{reader_config_file}'") + # Save the empty rfid reader database, to make sure we can create the file and have access to it + cfg_rfid.save(only_if_changed=False) + + if 'rfid' in cfg_rfid and 'readers' in cfg_rfid['rfid']: + # Load all the required modules + # Start a ReaderRunner-Thread for each Reader + for reader_cfg_key in cfg_rfid['rfid']['readers'].keys(): + _READERS[reader_cfg_key] = ReaderRunner(reader_cfg_key) + for reader_cfg_key in cfg_rfid['rfid']['readers'].keys(): + _READERS[reader_cfg_key].start() @plugs.atexit diff --git a/src/jukebox/jukebox/version.py b/src/jukebox/jukebox/version.py index ff86e01fc..03fc34a66 100644 --- a/src/jukebox/jukebox/version.py +++ b/src/jukebox/jukebox/version.py @@ -1,7 +1,7 @@ VERSION_MAJOR = 3 -VERSION_MINOR = 2 -VERSION_PATCH = 1 +VERSION_MINOR = 4 +VERSION_PATCH = 0 VERSION_EXTRA = "" __version__ = '%i.%i.%i' % (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) diff --git a/src/webapp/src/components/Library/lists/index.js b/src/webapp/src/components/Library/lists/index.js index a153619e6..22970d9a9 100644 --- a/src/webapp/src/components/Library/lists/index.js +++ b/src/webapp/src/components/Library/lists/index.js @@ -28,7 +28,7 @@ const LibraryLists = () => { const [cardId] = useState(searchParams.get('cardId')); const [musicFilter, setMusicFilter] = useState(''); - const handleMusicFilder = (event) => { + const handleMusicFolder = (event) => { setMusicFilter(event.target.value); }; @@ -49,7 +49,7 @@ const LibraryLists = () => { {isSelecting && }