diff --git a/.github/workflows/codeql-analysis_v3.yml b/.github/workflows/codeql-analysis_v3.yml index 574f2b2bd..211baae86 100644 --- a/.github/workflows/codeql-analysis_v3.yml +++ b/.github/workflows/codeql-analysis_v3.yml @@ -37,24 +37,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - # Install necessary packages - sudo apt-get update - sudo apt-get install libasound2-dev pulseaudio - python3 -m venv .venv - source ".venv/bin/activate" - - python -m pip install --upgrade pip - pip install -r requirements.txt - # Set the `CODEQL_EXTRACTOR_PYTHON_ANALYSIS_VERSION` environment variable to the Python executable - # that includes the dependencies - echo "CODEQL_EXTRACTOR_PYTHON_ANALYSIS_VERSION=$(which python)" >> $GITHUB_ENV - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 diff --git a/documentation/developers/rfid/mfrc522_spi.md b/documentation/developers/rfid/mfrc522_spi.md index be3abeaa8..f14812ed4 100644 --- a/documentation/developers/rfid/mfrc522_spi.md +++ b/documentation/developers/rfid/mfrc522_spi.md @@ -38,7 +38,7 @@ Mandatory IRQ pin. This can be any GPIO pin. Reset pin for hardware reset. This is an optional pin. If not used, -- hardware reset will only be performed by power-on-reset. This has been tested on works fine. +- hardware reset will only be performed by power-on-reset. This has been tested and works fine. - you **must** tie the reset pin of the MFRC522 board **high**! ### mode_legacy *(default=false)* @@ -77,8 +77,6 @@ MISO. MFRC522 boards can be picked up from many places for little money. -Good quality ones can be found e.g. here - ### Cards/Tags Cards or tags must support 13.56 MHz. Currently, only cards/tags of the type "NXP Mifare Classic 1k(S50)", "NXP Mifare Classic 4k(S70)" and "NXP Mifare Ultralight (C)" can be used. Type "NXP Mifare NTAG2xx" or others will not work! diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index dfc880754..e7f3b060c 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -130,7 +130,7 @@ validate_url() { download_from_url() { local url=$1 local output_filename=$2 - wget --quiet ${url} -O ${output_filename} || exit_on_error "Download failed" + wget ${url} -O ${output_filename} || exit_on_error "Download failed" return $? } @@ -399,7 +399,7 @@ verify_optional_service_enablement() { # 1 : textfile to read get_args_from_file() { local package_file="$1" - sed 's/.*#egg=//g' ${package_file} | sed -E 's/(#|=|>|<).*//g' | xargs echo + sed 's/.*#egg=//g' ${package_file} | sed -E 's/(#|=|>|<|;).*//g' | xargs echo } # Check if all passed packages are installed. Fail on first missing. @@ -439,3 +439,22 @@ verify_pip_modules() { done log " CHECK" } + +# Check if all passed modules are not installed. Fail on first found. +verify_pip_modules_not() { + local modules="$@" + log " Verify modules are not 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 installed" + fi + done + log " CHECK" +} diff --git a/installation/routines/setup_jukebox_core.sh b/installation/routines/setup_jukebox_core.sh index 2e4fbad39..f2c0e55ad 100644 --- a/installation/routines/setup_jukebox_core.sh +++ b/installation/routines/setup_jukebox_core.sh @@ -21,16 +21,40 @@ _jukebox_core_install_os_dependencies() { --allow-change-held-packages } +_jukebox_core_build_and_install_lgpio() { + local tmp_path="${HOME_PATH}/tmp" + local lg_filename="lg" + local lg_zip_filename="${lg_filename}.zip" + + sudo apt-get -y install swig unzip python3-dev python3-setuptools + mkdir -p "${tmp_path}" && cd "${tmp_path}" || exit_on_error + download_from_url "http://abyz.me.uk/lg/${lg_zip_filename}" "${lg_zip_filename}" + unzip ${lg_zip_filename} || exit_on_error + cd "${lg_filename}" || exit_on_error + make && sudo make install + cd "${INSTALLATION_PATH}" && sudo rm -rf "${tmp_path}" +} + _jukebox_core_install_python_requirements() { print_lc " Install Python requirements" - cd "${INSTALLATION_PATH}" || exit_on_error + cd "${INSTALLATION_PATH}" || exit_on_error python3 -m venv $VIRTUAL_ENV source "$VIRTUAL_ENV/bin/activate" pip install --upgrade pip - pip install --no-cache-dir -r "${INSTALLATION_PATH}/requirements.txt" + # Remove excluded libs, if installed - see https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/2470 + pip uninstall -y -r "${INSTALLATION_PATH}"/requirements-excluded.txt + + # prepare lgpio build for bullseye as the binaries are broken + local pip_install_options="" + if [ "$(is_debian_version_at_least 12)" = false ]; then + _jukebox_core_build_and_install_lgpio + pip_install_options="--no-binary=lgpio" + fi + + pip install --no-cache-dir -r "${INSTALLATION_PATH}/requirements.txt" ${pip_install_options} } _jukebox_core_configure_pulseaudio() { @@ -120,6 +144,9 @@ _jukebox_core_check() { local pip_modules=$(get_args_from_file "${INSTALLATION_PATH}/requirements.txt") verify_pip_modules pyzmq $pip_modules + local pip_modules_excluded=$(get_args_from_file "${INSTALLATION_PATH}/requirements-excluded.txt") + verify_pip_modules_not $pip_modules_excluded + log " Verify ZMQ version '${JUKEBOX_ZMQ_VERSION}'" local zmq_version=$(python -c 'import zmq; print(f"{zmq.zmq_version()}")') if [[ "${zmq_version}" != "${JUKEBOX_ZMQ_VERSION}" ]]; then diff --git a/packages-core.txt b/packages-core.txt index b2f6779a2..aa5d944c4 100644 --- a/packages-core.txt +++ b/packages-core.txt @@ -15,3 +15,4 @@ python3 python3-venv python3-dev rsync +wget diff --git a/requirements-excluded.txt b/requirements-excluded.txt new file mode 100644 index 000000000..66dcf834f --- /dev/null +++ b/requirements-excluded.txt @@ -0,0 +1,9 @@ +# Remove excluded libs, if installed +# Libraries which must be excluded. These must be removed with +# You need to uninstall these with `python -m pip uninstall -y -r requirements.txt` + +# RPi.GPIO uses direct /sys/class/gpio/ access, which is removed since kernel 6.6 (bookworm) +# see also +# - https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/2470 +# - https://github.com/MiczFlor/RPi-Jukebox-RFID/discussions/2295 +RPi.GPIO diff --git a/requirements.txt b/requirements.txt index 8ddfc881a..8fea43269 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,8 @@ requests tornado # RPi's GPIO packages: -RPi.GPIO +# use shim to keep current RPi.GPIO behavior also under Bookworm - see https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/2313 +rpi-lgpio gpiozero # PyZMQ is a special case: diff --git a/src/jukebox/components/hostif/linux/__init__.py b/src/jukebox/components/hostif/linux/__init__.py index 6a6590ad6..a26152eae 100644 --- a/src/jukebox/components/hostif/linux/__init__.py +++ b/src/jukebox/components/hostif/linux/__init__.py @@ -10,27 +10,12 @@ import jukebox.publishing import jukebox.speaking_text from jukebox.multitimer import GenericEndlessTimerClass -import socket logger = logging.getLogger('jb.host.lnx') cfg = jukebox.cfghandler.get_handler('jukebox') # Get the main Thread Publisher publisher = jukebox.publishing.get_publisher() -# This is a slightly dirty way of checking if we are on an RPi -# JukeBox installs the dependency RPI which has no meaning on other machines -# If it does not exist all is clear -# It could still be installed, which results in a RuntimeError when loaded on a PC -try: - import RPi.GPIO as gpio # noqa: F401 - - IS_RPI = True -except ModuleNotFoundError: - IS_RPI = False -except RuntimeError as e: - logger.info(f"You don't seem to be on a PI, because loading 'RPi.GPIO' failed: {e.__class__.__name__}: {e}") - IS_RPI = False - # In debug mode, shutdown and reboot command are not actually executed IS_DEBUG = False try: @@ -302,73 +287,60 @@ def start_autohotspot(): return 'not-installed' -@plugin.initialize -def initialize(): - wlan_power = cfg.setndefault('host', 'wlan_power', 'disable_power_down', value=True) - card = cfg.setndefault('host', 'wlan_power', 'card', value='wlan0') - if wlan_power: - wlan_disable_power_down(card) +# --------------------------------------------------------------------------- +# RPi-only stuff +# --------------------------------------------------------------------------- +THROTTLE_CODES = { + 0x1: "under-voltage detected", + 0x2: "ARM frequency capped", + 0x4: "currently throttled", + 0x8: "soft temperature limit active", + 0x10000: "under-voltage has occurred", + 0x20000: "ARM frequency capped has occurred", + 0x40000: "throttling has occurred", + 0x80000: "soft temperature limit has occurred" +} -@plugin.finalize -def finalize(): - global timer_temperature - enabled = cfg.setndefault('host', 'publish_temperature', 'enabled', value=True) - wait_time = cfg.setndefault('host', 'publish_temperature', 'timer_interval_sec', value=5) - timer_temperature = GenericEndlessTimerClass('host.timer.cputemp', wait_time, publish_cpu_temperature) - timer_temperature.__doc__ = "Endless timer for publishing CPU temperature" - # Note: Since timer_temperature is an instance of a class from a different module, - # auto-registration would register it with that module. Manually set package to this plugin module - plugin.register(timer_temperature, name='timer_temperature', package=plugin.loaded_as(__name__)) - if enabled: - publish_cpu_temperature() - timer_temperature.start() - +def command_exists(command): + ret = shutil.which(command) + return ret is not None -@plugin.atexit -def atexit(**ignored_kwargs): - global timer_temperature - timer_temperature.cancel() - return timer_temperature.timer_thread +@plugin.register +def hdmi_power_down(): + """Power down HDMI circuits to save power if no display is connected -# --------------------------------------------------------------------------- -# RPi-only stuff -# --------------------------------------------------------------------------- -if IS_RPI: # noqa: C901 - - THROTTLE_CODES = { - 0x1: "under-voltage detected", - 0x2: "ARM frequency capped", - 0x4: "currently throttled", - 0x8: "soft temperature limit active", - 0x10000: "under-voltage has occurred", - 0x20000: "ARM frequency capped has occurred", - 0x40000: "throttling has occurred", - 0x80000: "soft temperature limit has occurred" - } - - @plugin.register - def hdmi_power_down(): - """Power down HDMI circuits to save power if no display is connected - - This must be done after every reboot""" + This must be done after every reboot""" + success = False + commandname = "tvservice" + if command_exists(commandname): logger.info('Power down HDMI circuits') - ret = subprocess.run(['sudo', '/usr/bin/tvservice', '-o'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + ret = subprocess.run(['sudo', commandname, '-o'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) if ret.returncode != 0: logger.error(f"{ret.stdout}") + else: + success = True + else: + logger.info('Power down HDMI not available on this system') - def filter_throttle_codes(code): - for error, msg in THROTTLE_CODES.items(): - if code & error > 0: - yield msg + return success - @plugin.register - def get_throttled(): + +def filter_throttle_codes(code): + for error, msg in THROTTLE_CODES.items(): + if code & error > 0: + yield msg + + +@plugin.register +def get_throttled(): + commandname = "vcgencmd" + if command_exists(commandname): # https://www.raspberrypi.org/documentation/computers/os.html#get_throttled - ret = subprocess.run(['sudo', 'vcgencmd', 'get_throttled'], - stdout=subprocess.PIPE, check=False) + ret = subprocess.run(['sudo', commandname, 'get_throttled'], + stdout=subprocess.PIPE, check=False) if ret.returncode != 0: status_string = f"Error in subprocess with code: {ret.returncode}" logger.error(status_string) @@ -384,11 +356,44 @@ def get_throttled(): else: # Decode the bit array after we have handled all the possible exceptions status_string = "Warning: " + ', '.join(filter_throttle_codes(status_code)) + else: + logger.info('Throttled state not available on this system') + status_string = "Not available" + + return status_string + + +# --------------------------------------------------------------------------- +# Init +# --------------------------------------------------------------------------- +@plugin.initialize +def initialize(): + wlan_power = cfg.setndefault('host', 'wlan_power', 'disable_power_down', value=True) + card = cfg.setndefault('host', 'wlan_power', 'card', value='wlan0') + if wlan_power: + wlan_disable_power_down(card) + hdmi_off = cfg.setndefault('host', 'rpi', 'hdmi_power_down', value=False) + if hdmi_off: + hdmi_power_down() + + +@plugin.finalize +def finalize(): + global timer_temperature + enabled = cfg.setndefault('host', 'publish_temperature', 'enabled', value=True) + wait_time = cfg.setndefault('host', 'publish_temperature', 'timer_interval_sec', value=5) + timer_temperature = GenericEndlessTimerClass('host.timer.cputemp', wait_time, publish_cpu_temperature) + timer_temperature.__doc__ = "Endless timer for publishing CPU temperature" + # Note: Since timer_temperature is an instance of a class from a different module, + # auto-registration would register it with that module. Manually set package to this plugin module + plugin.register(timer_temperature, name='timer_temperature', package=plugin.loaded_as(__name__)) + if enabled: + publish_cpu_temperature() + timer_temperature.start() - return status_string - @plugin.initialize - def rpi_initialize(): - hdmi_off = cfg.setndefault('host', 'rpi', 'hdmi_power_down', value=False) - if hdmi_off: - hdmi_power_down() +@plugin.atexit +def atexit(**ignored_kwargs): + global timer_temperature + timer_temperature.cancel() + return timer_temperature.timer_thread diff --git a/src/jukebox/components/rfid/hardware/rc522_spi/rc522_spi.py b/src/jukebox/components/rfid/hardware/rc522_spi/rc522_spi.py index d9853ec3d..bb995b084 100644 --- a/src/jukebox/components/rfid/hardware/rc522_spi/rc522_spi.py +++ b/src/jukebox/components/rfid/hardware/rc522_spi/rc522_spi.py @@ -1,7 +1,6 @@ # Standard imports from python packages import logging -import RPi.GPIO as GPIO import pirc522 import jukebox.cfghandler @@ -87,7 +86,7 @@ def __init__(self, reader_cfg_key): pin_rst=pin_rst, pin_irq=pin_irq, antenna_gain=antenna_gain, - pin_mode=GPIO.BCM) + pin_mode='BCM') def cleanup(self): self.device.cleanup() diff --git a/src/jukebox/components/rfid/hardware/rc522_spi/requirements.txt b/src/jukebox/components/rfid/hardware/rc522_spi/requirements.txt index 56b9ca4dc..e6508b4f8 100644 --- a/src/jukebox/components/rfid/hardware/rc522_spi/requirements.txt +++ b/src/jukebox/components/rfid/hardware/rc522_spi/requirements.txt @@ -1,6 +1,4 @@ # RC522 related requirements # You need to install these with `python -m pip install --upgrade --force-reinstall -q -r requirements.txt` -pi-rc522==2.3.0 - - +git+https://github.com/hoffie/pi-rc522-gpiozero@1dc878c