From c4ee966d92614caa2e5bca552d52333dd3ddaac9 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Wed, 3 Jan 2024 23:37:49 +0100 Subject: [PATCH 01/24] add workflow files to paths to trigger run (#2202) --- .github/workflows/test_docker_debian_v3.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_docker_debian_v3.yml b/.github/workflows/test_docker_debian_v3.yml index f333d15eb..06718afba 100644 --- a/.github/workflows/test_docker_debian_v3.yml +++ b/.github/workflows/test_docker_debian_v3.yml @@ -8,6 +8,7 @@ on: branches: - 'future3/**' paths: + - '.github/workflows/test_docker_debian*.yml' - 'installation/**' - 'ci/**' - 'resources/**' @@ -20,6 +21,7 @@ on: - future3/develop - future3/main paths: + - '.github/workflows/test_docker_debian*.yml' - 'installation/**' - 'ci/**' - 'resources/**' From 03e64ff8680cd214f61fd56a2b6049c82b1b2d36 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:35:04 +0100 Subject: [PATCH 02/24] New card actions: play, pause, prev, next, toggle, repeat, shuffle (#2179) * Adding additional player controls to assign to cards * streamline some RPC command namings across jukebox and webapp * Fix typos * Fix Repeat toggles * Fix toggle_shuffle * Fix typo in command * Fix another typo * Fix wrong parameter for shuffle * Update German translation for single-repeat * Simplify Shuffle option * Simplify Repeat option * Streamline Player functions with new set_ methods in jukebox * Abstract OptionsSelector * Refactor some code * Fix indentation * Rename set_ methods * Undo some doc changes --- documentation/developers/docker.md | 15 ++++- documentation/developers/status.md | 7 +- src/jukebox/components/playermpd/__init__.py | 55 +++++++++++++-- src/jukebox/components/rpc_command_alias.py | 18 +++++ src/webapp/public/locales/de/translation.json | 44 +++++++++--- src/webapp/public/locales/en/translation.json | 45 ++++++++++--- src/webapp/src/commands/index.js | 13 +++- .../Cards/controls/actions/audio/index.js | 34 +++++++++- .../actions/audio/slider-change-volume.js | 4 +- .../controls/actions/synchronisation/index.js | 11 ++- .../rfidcards/change-on-rfid-scan-options.js | 67 ------------------- .../Cards/controls/options-selector.js | 61 +++++++++++++++++ src/webapp/src/components/Player/controls.js | 34 +++++----- src/webapp/src/components/Player/cover.js | 6 +- src/webapp/src/components/Player/index.js | 2 +- src/webapp/src/components/Player/seekbar.js | 2 +- src/webapp/src/config.js | 9 ++- src/webapp/src/i18n.js | 1 + 18 files changed, 297 insertions(+), 131 deletions(-) delete mode 100644 src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js create mode 100644 src/webapp/src/components/Cards/controls/options-selector.js diff --git a/documentation/developers/docker.md b/documentation/developers/docker.md index d7242750e..80651ce84 100644 --- a/documentation/developers/docker.md +++ b/documentation/developers/docker.md @@ -47,7 +47,20 @@ They can be run individually or in combination. To do that, we use ### Mac 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)) +2. Install pulseaudio + 1. Use Homebrew to install + ``` + $ brew install pulseaudio + ``` + 2. Enable pulseaudio network capabilities. In an editor, open `/opt/homebrew/Cellar/pulseaudio/16.1/etc/pulse/default.pa` (you might need to adapt this path to your own system settings). Uncomment the following line. + ``` + load-module module-native-protocol-tcp + ``` + 3. Restart the pulseaudio service + ``` + $ brew services restart pulseaudio + ``` + 4. If you have trouble with your audio, try these resources to troubleshoot: [[1]](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712), [[2]](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html), [[3]](https://stackoverflow.com/a/50939994/1062438) > [!NOTE] > In order for Pulseaudio to work properly with Docker on your Mac, you need to start Pulseaudio in a specific way. Otherwise MPD will throw an exception. See [Pulseaudio issues on Mac](#pulseaudio-issue-on-mac) for more info. diff --git a/documentation/developers/status.md b/documentation/developers/status.md index 2fb92897f..48c2b6c3b 100644 --- a/documentation/developers/status.md +++ b/documentation/developers/status.md @@ -107,10 +107,9 @@ Topics marked _in progress_ are already in the process of implementation by comm - [ ] Folder configuration (_in progress_) - [ ] [Reference](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#manage-playout-behaviour) - [ ] Resume: Save and restore position (how interact with shuffle?) - - [ ] Single: Enable mpc single - - [ ] Shuffle: Enable mpc random (not shuffle) - - Rename to random, as this is mpc random - - [ ] Loop: Loop playlist + - [ ] Repeat Playlist + - [ ] Repeat Song + - [ ] Shuffle ### MPD Player diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index e854ee5ee..c77c21051 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -335,11 +335,6 @@ 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): """ @@ -363,7 +358,6 @@ def replay(self): @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() @@ -378,8 +372,27 @@ def replay_if_stopped(self): if self.mpd_status['state'] == 'stop': self.play_folder(self.music_player_status['player_status']['last_played_folder']) + # Shuffle + 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 repeatmode(self, mode): + def shuffle(self, option='toggle'): + if option == 'toggle': + if self.mpd_status['random'] == '0': + self._shuffle(1) + else: + self._shuffle(0) + elif option == 'enable': + self._shuffle(1) + elif option == 'disable': + self._shuffle(0) + else: + logger.error(f"'{option}' does not exist for 'shuffle'") + + # Repeat + def _repeatmode(self, mode): if mode == 'repeat': repeat = 1 single = 0 @@ -394,6 +407,34 @@ def repeatmode(self, mode): self.mpd_client.repeat(repeat) self.mpd_client.single(single) + @plugs.tag + def repeat(self, option='toggle'): + if option == 'toggle': + if self.mpd_status['repeat'] == '0': + self._repeatmode('repeat') + elif self.mpd_status['repeat'] == '1' and self.mpd_status['single'] == '0': + self._repeatmode('single') + else: + self._repeatmode(None) + elif option == 'toggle_repeat': + if self.mpd_status['repeat'] == '0': + self._repeatmode('repeat') + else: + self._repeatmode(None) + elif option == 'toggle_repeat_single': + if self.mpd_status['single'] == '0': + self._repeatmode('single') + else: + self._repeatmode(None) + elif option == 'enable_repeat': + self._repeatmode('repeat') + elif option == 'enable_repeat_single': + self._repeatmode('single') + elif option == 'disable': + self._repeatmode(None) + else: + logger.error(f"'{option}' does not exist for 'repeat'") + @plugs.tag def get_current_song(self, param): return self.mpd_status diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index bb484891a..e56727ff4 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -36,6 +36,12 @@ 'package': 'player', 'plugin': 'ctrl', 'method': 'play_folder'}, + 'play': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'play', + 'note': 'Play the currently selected song', + 'ignore_card_removal_action': True}, 'pause': { 'package': 'player', 'plugin': 'ctrl', @@ -57,6 +63,18 @@ 'plugin': 'ctrl', 'method': 'toggle', 'ignore_card_removal_action': True}, + 'shuffle': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'shuffle', + 'note': 'Shuffle', + 'ignore_card_removal_action': True}, + 'repeat': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'repeat', + 'note': 'Repeat', + 'ignore_card_removal_action': True}, # VOLUME 'set_volume': { diff --git a/src/webapp/public/locales/de/translation.json b/src/webapp/public/locales/de/translation.json index 85d39b879..f3bd89782 100644 --- a/src/webapp/public/locales/de/translation.json +++ b/src/webapp/public/locales/de/translation.json @@ -23,7 +23,14 @@ "timer_fade_volume": "Fade volume", "toggle_output": "Audio-Ausgang umschalten", "sync_rfidcards_all": "Alle Audiodateien und Karteneinträge synchronisieren", - "sync_rfidcards_change_on_rfid_scan": "Aktivierung ändern für 'on RFID scan' " + "sync_rfidcards_change_on_rfid_scan": "Aktivierung ändern für 'on RFID scan'", + "next_song": "Nächster Song", + "pause": "Pause", + "play": "Abspielen", + "prev_song": "Vorheriger Song", + "shuffle": "Zufallswiedergabe", + "repeat": "Wiedergabe wiederholen", + "toggle": "Abspielen/Pause umschalten" } }, "controls-selector": { @@ -56,8 +63,25 @@ "no-music-selected": "Es ist keine Musik ausgewählt.", "loading-song-error": "Während des Ladens des Songs ist ein Fehler aufgetreten." }, - "volume": { - "title": "Lautstärke Stufen" + "audio": { + "repeat": { + "description": "Wähle den zu setzenden Status.", + "label-toggle": "Umschalten - Schaltet durch 1) Wiedergabeliste Wiederholen, 2) Song Wiederholen, 3) Wiederholen Deaktiveren", + "label-toggle-repeat": "Wiedergabeliste Wiederholen umschalten", + "label-toggle-repeat-single": "Song Wiederholen umschalten", + "label-enable-repeat": "Wiedergabeliste Wiederholen aktivieren", + "label-enable-repeat-single": "Song Wiederholen aktivieren", + "label-disable": "Wiederholen deaktivieren" + }, + "shuffle": { + "description": "Wähle den zu setzenden Status.", + "label-toggle": "Umschalten", + "label-enable": "Aktivieren", + "label-disable": "Deaktivieren" + }, + "volume": { + "title": "Lautstärke Stufen" + } }, "timers": { "description": "Wähle die Anzahl der Minuten nachdem die Aktion ausgeführt werden soll." @@ -138,17 +162,17 @@ "player": { "controls": { "shuffle": { - "activate": "Shuffle aktivieren", - "deactivate": "Shuffle deaktivieren" + "enable": "Zufallswiedergabe aktivieren", + "disable": "Zufallswiedergabe deaktivieren" }, - "skip": "Zurück", + "prev_song": "Zurück", "play": "Abspielen", "pause": "Pause", - "next": "Weiter", + "next_song": "Weiter", "repeat": { - "activate": "Wiederholen aktivieren", - "activate-single": "1 Wiederholen aktivieren", - "deactivate": "Wiederholen deaktivieren" + "enable": "Wiedergabeliste Wiederholen aktivieren", + "enable-single": "Song Wiederholen aktivieren", + "disable": "Wiederholen deaktivieren" } }, "cover": { diff --git a/src/webapp/public/locales/en/translation.json b/src/webapp/public/locales/en/translation.json index d03941eb6..348d3771d 100644 --- a/src/webapp/public/locales/en/translation.json +++ b/src/webapp/public/locales/en/translation.json @@ -23,7 +23,14 @@ "timer_fade_volume": "Fade volume", "toggle_output": "Toggle audio output", "sync_rfidcards_all": "Sync all audiofiles and card entries", - "sync_rfidcards_change_on_rfid_scan": "Change activation of 'on RFID scan'" + "sync_rfidcards_change_on_rfid_scan": "Change activation of 'on RFID scan'", + "next_song": "Next song", + "pause": "Pause", + "play": "Play", + "prev_song": "Previous song", + "shuffle": "Shuffle", + "repeat": "Repeat", + "toggle": "Toggle Play/Pause" } }, "controls-selector": { @@ -56,8 +63,25 @@ "no-music-selected": "No music selected", "loading-song-error": "An error occurred while loading song." }, - "volume": { - "title": "Volume steps" + "audio": { + "repeat": { + "description": "Choose the state to set.", + "label-toggle": "Toggle - Loops through 1) Repeat playlist, 2) Repeat song, 3) Disable repeat", + "label-toggle-repeat": "Toggle Repeat Playlist", + "label-toggle-repeat-single": "Toggle Repeat Song", + "label-enable-repeat": "Enable Repeat Playlist", + "label-enable-repeat-single": "Enable Repeat Song", + "label-disable": "Disable" + }, + "shuffle": { + "description": "Choose the state to set.", + "label-toggle": "Toggle", + "label-enable": "Enable", + "label-disable": "Disable" + }, + "volume": { + "title": "Volume steps" + } }, "timers": { "description": "Choose the amount of minutes you want the action to be performed." @@ -138,17 +162,17 @@ "player": { "controls": { "shuffle": { - "activate": "Activate shuffle", - "deactivate": "Deactivate shuffle" + "enable": "Enable shuffle", + "disable": "Disable shuffle" }, - "skip": "Skip previous track", + "prev_song": "Skip to previous track", "play": "Play", "pause": "Pause", - "next": "Skip next track", + "next_song": "Skip to next track", "repeat": { - "activate": "Activate repeat", - "activate-single": "Activate single repeat", - "deactivate": "Deactivate repeat" + "enable": "Enable Playlist repeat", + "enable-single": "Enable Song repeat", + "disable": "Disable repeat" } }, "cover": { @@ -207,6 +231,7 @@ }, "secondswipe": { "title": "Second Swipe", + "description": "Second action after the same card swiped again", "restart": "Restart playlist", "toggle": "Toggle pause / play", "skip": "Skip to next track", diff --git a/src/webapp/src/commands/index.js b/src/webapp/src/commands/index.js index 2352e46b7..8c844d8da 100644 --- a/src/webapp/src/commands/index.js +++ b/src/webapp/src/commands/index.js @@ -82,25 +82,32 @@ const commands = { plugin: 'ctrl', method: 'pause', }, - previous: { + prev_song: { _package: 'player', plugin: 'ctrl', method: 'prev', }, - next: { + next_song: { _package: 'player', plugin: 'ctrl', method: 'next', }, + toggle: { + _package: 'player', + plugin: 'ctrl', + method: 'toggle', + }, shuffle: { _package: 'player', plugin: 'ctrl', method: 'shuffle', + argKeys: ['option'], }, repeat: { _package: 'player', plugin: 'ctrl', - method: 'repeatmode', + method: 'repeat', + argKeys: ['option'], }, seek: { _package: 'player', diff --git a/src/webapp/src/components/Cards/controls/actions/audio/index.js b/src/webapp/src/components/Cards/controls/actions/audio/index.js index 1d3ddc465..aba96cdc2 100644 --- a/src/webapp/src/components/Cards/controls/actions/audio/index.js +++ b/src/webapp/src/components/Cards/controls/actions/audio/index.js @@ -2,10 +2,11 @@ import React from 'react'; import CommandSelector from '../../command-selector'; import SliderChangeVolume from './slider-change-volume'; +import OptionsSelector from '../../options-selector'; import { getActionAndCommand } from '../../../utils'; -const SelectVolume = ({ +const SelectAudioVolume = ({ actionData, handleActionDataChange, }) => { @@ -23,8 +24,37 @@ const SelectVolume = ({ handleActionDataChange={handleActionDataChange} /> } + {command === 'shuffle' && + + } + {command === 'repeat' && + + } ); }; -export default SelectVolume; \ No newline at end of file +export default SelectAudioVolume; \ No newline at end of file diff --git a/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js b/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js index 2e70b489a..64f5edada 100644 --- a/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js +++ b/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js @@ -37,12 +37,12 @@ const SliderChangeVolume = ({ - {t('cards.controls.actions.volume.title')} + {t('cards.controls.actions.audio.volume.title')} {command === 'sync_rfidcards_change_on_rfid_scan' && - } diff --git a/src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js b/src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js deleted file mode 100644 index 6d2b2fde6..000000000 --- a/src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - Grid, - FormControl, - FormControlLabel, - Radio, - RadioGroup, - Typography, -} from '@mui/material'; - -import { - getActionAndCommand, - getArgsValues, -} from '../../../../utils'; - - -const ChangeOnRfidScan = ({ - actionData, - handleActionDataChange, -}) => { - const { t } = useTranslation(); - - const { action, command } = getActionAndCommand(actionData); - const [option] = getArgsValues(actionData); - - const onChange = (event, option) => { - handleActionDataChange(action, command, { option }) - }; - - return ( - - - - {t('cards.controls.actions.synchronisation.rfidcards.description')} - - - - } - label={t('cards.controls.actions.synchronisation.rfidcards.label-toggle')} - value="toggle" - /> - } - label={t('cards.controls.actions.synchronisation.rfidcards.label-enable')} - value="enable" - /> - } - label={t('cards.controls.actions.synchronisation.rfidcards.label-disable')} - value="disable" - /> - - - - - ); -}; - -export default ChangeOnRfidScan; diff --git a/src/webapp/src/components/Cards/controls/options-selector.js b/src/webapp/src/components/Cards/controls/options-selector.js new file mode 100644 index 000000000..7a2fd1b22 --- /dev/null +++ b/src/webapp/src/components/Cards/controls/options-selector.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + Grid, + FormControl, + FormControlLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; + +import { + getActionAndCommand, + getArgsValues, +} from '../utils.js'; + +const OptionsSelector = ({ + actionType, + actionData, + handleActionDataChange, + optionLabel, + options, +}) => { + const { t } = useTranslation(); + const { action, command } = getActionAndCommand(actionData); + const [option] = getArgsValues(actionData); + + const onChange = (event, option) => { + handleActionDataChange(action, command, { option }) + }; + + return ( + + + + {t(optionLabel)} + + + + {options.map(({ labelKey, value }) => ( + } + label={t(labelKey)} + value={value} + /> + ))} + + + + + ); +}; + +export default OptionsSelector; diff --git a/src/webapp/src/components/Player/controls.js b/src/webapp/src/components/Player/controls.js index dc433cef8..48a587a32 100644 --- a/src/webapp/src/components/Player/controls.js +++ b/src/webapp/src/components/Player/controls.js @@ -32,15 +32,11 @@ const Controls = () => { } = state; const toggleShuffle = () => { - request('shuffle', { random: !isShuffle }); + request('shuffle', { option: 'toggle' }); } const toggleRepeat = () => { - let mode = null; - if (!isRepeat && !isSingle) mode = 'repeat'; - if (isRepeat && !isSingle) mode = 'single'; - - request('repeat', { mode }); + request('repeat', { option: 'toggle' }); } useEffect(() => { @@ -58,14 +54,14 @@ const Controls = () => { const labelShuffle = () => ( isShuffle - ? t('player.controls.shuffle.deactivate') - : t('player.controls.shuffle.activate') + ? t('player.controls.shuffle.disable') + : t('player.controls.shuffle.enable') ); const labelRepeat = () => { - if (!isRepeat) return t('player.controls.repeat.activate'); - if (isRepeat && !isSingle) return t('player.controls.repeat.activate-single'); - if (isRepeat && isSingle) return t('player.controls.repeat.deactivate'); + if (!isRepeat) return t('player.controls.repeat.enable'); + if (isRepeat && !isSingle) return t('player.controls.repeat.enable-single'); + if (isRepeat && isSingle) return t('player.controls.repeat.disable'); }; return ( @@ -89,14 +85,14 @@ const Controls = () => { - {/* Skip previous track */} + {/* Skip to previous song */} request('previous')} + onClick={e => request('prev_song')} size="large" sx={iconStyles} - title={t('player.controls.skip')} + title={t('player.controls.prev_song')} > @@ -127,14 +123,14 @@ const Controls = () => { } - {/* Skip next track */} + {/* Skip to next song */} request('next')} + onClick={e => request('next_song')} size="large" sx={iconStyles} - title={t('player.controls.next')} + title={t('player.controls.next_song')} > diff --git a/src/webapp/src/components/Player/cover.js b/src/webapp/src/components/Player/cover.js index d20133acb..a744b7b5f 100644 --- a/src/webapp/src/components/Player/cover.js +++ b/src/webapp/src/components/Player/cover.js @@ -33,7 +33,11 @@ const Cover = ({ coverImage }) => { {t('player.cover.title')}} {!coverImage && { if (result) { setCoverImage(`/cover-cache/${result}`); setBackgroundImage([ - 'linear-gradient(to bottom, rgba(18, 18, 18, 0.7), rgba(18, 18, 18, 1))', + 'linear-gradient(to bottom, rgba(18, 18, 18, 0.5), rgba(18, 18, 18, 1))', `url(/cover-cache/${result})` ].join(',')); }; diff --git a/src/webapp/src/components/Player/seekbar.js b/src/webapp/src/components/Player/seekbar.js index aab3682ee..b138bf827 100644 --- a/src/webapp/src/components/Player/seekbar.js +++ b/src/webapp/src/components/Player/seekbar.js @@ -69,7 +69,7 @@ const SeekBar = () => { direction="row" justifyContent="space-between" sx={ { - marginTop: '-20px', + marginTop: '-10px', }} > diff --git a/src/webapp/src/config.js b/src/webapp/src/config.js index 35b5a980e..482db205e 100644 --- a/src/webapp/src/config.js +++ b/src/webapp/src/config.js @@ -42,7 +42,14 @@ const JUKEBOX_ACTIONS_MAP = { audio: { commands: { change_volume: {}, - toggle_output: {} + toggle_output: {}, + play: {}, + pause: {}, + toggle: {}, + next_song: {}, + prev_song: {}, + shuffle: {}, + repeat: {}, }, }, diff --git a/src/webapp/src/i18n.js b/src/webapp/src/i18n.js index b2a752fdd..d374abae3 100644 --- a/src/webapp/src/i18n.js +++ b/src/webapp/src/i18n.js @@ -18,6 +18,7 @@ i18n // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, + // lng: 'en', fallbackLng: 'en', interpolation: { escapeValue: false, // not needed for react as it escapes by default From 6e9e4f7ae1db3a83ba9e147e320f1aaa6499de70 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Mon, 15 Jan 2024 07:35:36 +0100 Subject: [PATCH 03/24] Add swap file adjustment for webapp build (#2204) * adjust swapfile if memory is too low * add option for webapp build * update ci tests * Update run_install_webapp_local.sh * update ENABLE_WEBAPP_BUILD handling * updated logging * add build webapp test workflow * fix path * fix actions * fix: add shell * fix trigger path * adjust node mem calculation leave enough memory for the system. increase swap with min size * use shell script for builds * add env CI * use new action for release build * adjusted min sizes * remove obsolete code * update workflow name * move update dependencies entirely to rebuild script. update docs * move existing build folder to backup. update docs * increase swap with lower step size. minor fixes * refactor vars and logging. added verbose param. * update logging * fix indentation * update logging. moved NODEMEM check * refactor webapp build during installation removed unneccessay vars (only use ENABLE_WEBAPP_PROD_DOWNLOAD). trimmed webapp build option. updated messages. harmonized var access and queries * Update docs * Update messages * harmonize webapp wording * fix flake8 * fix flake8 * update node installation. update node version for armv6. use recommended setup from nodesource. removed update version on new installation run. * update docs * ci add platform linux/armv6 (deactivated) * reverted to https://deb.nodesource.com/ installation * update docs * align fin message * Update docs. Remove full URL references to branch * ignore stderr if node is not installed before * ignore stderr if node is not installed before * bugfix. removed additional char * add npm project config better network handling especially for armv6l devices * use performance optimized install command update warning for armv6l devices * Update docs Update installation steps. split for version. add tip from installation- Add webapp doc for developers. update docs and link to new webapp doc. eliminate duplications. merged developer-issues into new webapp doc * also raise fetch-retry-mintimeout * update docs harmonize wording for webapp. add build output. change ARMv6 installation note. * add note for webserver restart * update installation docs. add bullet list * change npm config values * show "slow hardware message" at installation start * update armv6l warning for webapp rebuild * make script return value more clear * Make local webapp build an optional step due to unpredictable npm connection errors. Show webapp fin message only if webapp build fails * use prebuilt webapp bundle also for develop * update docs * Update webapp.md * Update customize_options.sh (typos) * Update installation.md * Update system.md * Update installation.md * Update webapp.md * Update customize_options.sh * change npm config values * update docs * update welcome to match changes in wiki * update rebuild message * Add doc links * update ignorefiles for build.bak * Update doc and link for constributors * Update CONTRIBUTING.md --------- Co-authored-by: pabera <1260686+pabera@users.noreply.github.com> --- .dockerignore | 1 + .githooks/post-merge | 6 +- .github/actions/build-webapp/action.yml | 25 +++ .../bundle_webapp_and_release_v3.yml | 28 +-- .github/workflows/test_build_webapp_v3.yml | 33 ++++ .../test_docker_debian_codename_sub_v3.yml | 9 +- .github/workflows/test_docker_debian_v3.yml | 16 ++ CONTRIBUTING.md | 37 ++-- ci/installation/run_install_common.sh | 2 +- ci/installation/run_install_faststartup.sh | 2 +- ci/installation/run_install_libzmq_local.sh | 2 +- .../run_install_webapp_download.sh | 4 +- ci/installation/run_install_webapp_local.sh | 4 +- documentation/builders/autohotspot.md | 8 +- documentation/builders/concepts.md | 2 +- documentation/builders/installation.md | 38 +++- documentation/builders/system.md | 6 +- documentation/builders/update.md | 2 +- documentation/developers/README.md | 1 + documentation/developers/developer-issues.md | 86 -------- .../developers/development-environment.md | 32 +-- documentation/developers/docker.md | 4 +- documentation/developers/status.md | 2 +- documentation/developers/webapp.md | 149 ++++++++++++++ installation/includes/00_constants.sh | 3 + installation/includes/01_default_config.sh | 4 +- installation/includes/02_helpers.sh | 11 ++ installation/includes/03_welcome.sh | 14 +- installation/includes/05_finish.sh | 2 +- installation/routines/customize_options.sh | 46 ++--- installation/routines/install.sh | 1 + installation/routines/setup_jukebox_core.sh | 13 -- installation/routines/setup_jukebox_webapp.sh | 143 +++++++++----- resources/default-settings/gpio.example.yaml | 2 +- resources/html/404.html | 2 +- resources/html/runbuildui.html | 8 +- src/jukebox/components/playermpd/__init__.py | 9 +- src/webapp/.gitignore | 3 + src/webapp/.npmrc | 3 + src/webapp/public/index.html | 2 +- src/webapp/run_rebuild.sh | 187 ++++++++++++------ 41 files changed, 585 insertions(+), 367 deletions(-) create mode 100644 .github/actions/build-webapp/action.yml create mode 100644 .github/workflows/test_build_webapp_v3.yml delete mode 100644 documentation/developers/developer-issues.md create mode 100644 documentation/developers/webapp.md create mode 100644 src/webapp/.npmrc diff --git a/.dockerignore b/.dockerignore index a587de477..da30b5a97 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,4 @@ shared src/webapp/node_modules src/webapp/npm-debug.log src/webapp/build +src/webapp/build.bak diff --git a/.githooks/post-merge b/.githooks/post-merge index 3b838c87c..6fc6e4f54 100755 --- a/.githooks/post-merge +++ b/.githooks/post-merge @@ -5,7 +5,7 @@ # TO ACTIVATE: cp .githooks/post-merge .git/hooks/. # # Checks: -# - Changes to web app +# - Changes to Web App # - Changes to web dependency # - Changes to python requirements # @@ -20,7 +20,7 @@ warn_npm_dependency() { echo "************************************************************" echo "ATTENTION: npm dependencies have changed since last pull!" echo "" - echo "To update dependencies and rebuilt WebApp run:" + echo "To update dependencies and rebuilt Web App run:" echo "$ cd src/webapp && ./run_rebuild.sh -u" echo "************************************************************" echo -e "\n" @@ -31,7 +31,7 @@ warn_webapp() { echo "************************************************************" echo "ATTENTION: Web App sources have changed since last pull!" echo "" - echo "To rebuilt the WebApp run:" + echo "To rebuilt the Web App run:" echo "$ cd src/webapp && ./run_rebuild.sh" echo "************************************************************" echo -e "\n" diff --git a/.github/actions/build-webapp/action.yml b/.github/actions/build-webapp/action.yml new file mode 100644 index 000000000..9f192105f --- /dev/null +++ b/.github/actions/build-webapp/action.yml @@ -0,0 +1,25 @@ +name: Build Web App +description: 'Build Web App with Node' +inputs: + webapp-root-path: + description: 'root path of the Web App sources' + required: false + default: './src/webapp' +outputs: + webapp-root-path: + description: 'used root path of the Web App sources' + value: ${{ inputs.webapp-root-path }} + +runs: + using: "composite" + steps: + - name: Setup Node.js 20.x + uses: actions/setup-node@v3 + with: + node-version: 20.x + - name: run build + working-directory: ${{ inputs.webapp-root-path }} + shell: bash + env: + CI: false + run: ./run_rebuild.sh -u \ No newline at end of file diff --git a/.github/workflows/bundle_webapp_and_release_v3.yml b/.github/workflows/bundle_webapp_and_release_v3.yml index 548f2b47a..13bffe472 100644 --- a/.github/workflows/bundle_webapp_and_release_v3.yml +++ b/.github/workflows/bundle_webapp_and_release_v3.yml @@ -1,4 +1,4 @@ -name: Bundle Webapp and Release +name: Bundle Web App and Release on: push: @@ -18,7 +18,7 @@ jobs: check_abort: ${{ steps.vars.outputs.check_abort }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set Output vars id: vars @@ -72,9 +72,6 @@ jobs: if: ${{ needs.check.outputs.check_abort == 'false' }} runs-on: ubuntu-latest - env: - WEBAPP_ROOT_PATH: ./src/webapp - outputs: tag_name: ${{ needs.check.outputs.tag_name }} release_type: ${{ needs.check.outputs.release_type }} @@ -83,7 +80,7 @@ jobs: webapp_bundle_name_latest: ${{ steps.vars.outputs.webapp_bundle_name_latest }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set Output vars id: vars @@ -94,21 +91,12 @@ jobs: echo "webapp_bundle_name=webapp-build-${COMMIT_SHA:0:10}.tar.gz" >> $GITHUB_OUTPUT echo "webapp_bundle_name_latest=webapp-build-latest.tar.gz" >> $GITHUB_OUTPUT - - name: Setup Node.js 20.x - uses: actions/setup-node@v3 - with: - node-version: 20.x - - name: npm install - working-directory: ${{ env.WEBAPP_ROOT_PATH }} - run: npm install - - name: npm build - working-directory: ${{ env.WEBAPP_ROOT_PATH }} - env: - CI: false - run: npm run build + - name: Build Web App + id: build-webapp + uses: ./.github/actions/build-webapp - name: Create Bundle - working-directory: ${{ env.WEBAPP_ROOT_PATH }} + working-directory: ${{ steps.build-webapp.outputs.webapp-root-path }} run: | tar -czvf ${{ steps.vars.outputs.webapp_bundle_name }} build @@ -116,7 +104,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: ${{ steps.vars.outputs.webapp_bundle_name }} - path: ${{ env.WEBAPP_ROOT_PATH }}/${{ steps.vars.outputs.webapp_bundle_name }} + path: ${{ steps.build-webapp.outputs.webapp-root-path }}/${{ steps.vars.outputs.webapp_bundle_name }} retention-days: 5 release: diff --git a/.github/workflows/test_build_webapp_v3.yml b/.github/workflows/test_build_webapp_v3.yml new file mode 100644 index 000000000..426d01f04 --- /dev/null +++ b/.github/workflows/test_build_webapp_v3.yml @@ -0,0 +1,33 @@ +name: Test Build Web App v3 + +on: + schedule: + # run at 18:00 every sunday + - cron: '0 18 * * 0' + push: + branches: + - 'future3/**' + paths: + - '.github/workflows/test_build_webapp_v3.yml' + - '.github/actions/build-webapp/**' + - 'src/webapp/**' + pull_request: + # The branches below must be a subset of the branches above + branches: + - future3/develop + - future3/main + paths: + - '.github/workflows/test_build_webapp_v3.yml' + - '.github/actions/build-webapp/**' + - 'src/webapp/**' + +jobs: + + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build Web App + uses: ./.github/actions/build-webapp \ No newline at end of file diff --git a/.github/workflows/test_docker_debian_codename_sub_v3.yml b/.github/workflows/test_docker_debian_codename_sub_v3.yml index dde60cb2a..a9ec217dc 100644 --- a/.github/workflows/test_docker_debian_codename_sub_v3.yml +++ b/.github/workflows/test_docker_debian_codename_sub_v3.yml @@ -1,4 +1,4 @@ -name: Subworkflow Test Install Scripts Debian V3 +name: Subworkflow Test Install Scripts Debian v3 on: workflow_call: @@ -46,6 +46,7 @@ jobs: 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 }} + docker_run_options: ${{ steps.vars.outputs.docker_run_options }} # create local docker registry to use locally build images services: @@ -83,6 +84,7 @@ jobs: id: vars env: LOCAL_REGISTRY_PORT: ${{ inputs.local_registry_port }} + PLATFORM: ${{ inputs.platform }} 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 @@ -90,6 +92,9 @@ jobs: 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 + if [ "${{ env.PLATFORM }}" == "linux/arm/v6" ] ; then + echo "docker_run_options=-e QEMU_CPU=arm1176" >> $GITHUB_OUTPUT + fi # Build base image for debian version name. Layers will be cached and image pushes to local registry - name: Build Image - Base @@ -167,7 +172,7 @@ jobs: uses: tj-actions/docker-run@v2 with: image: ${{ needs.build.outputs.image_tag_name }} - options: --platform ${{ inputs.platform }} --user ${{ env.TEST_USER_NAME }} --init + options: ${{ needs.build.outputs.docker_run_options }} --platform ${{ inputs.platform }} --user ${{ env.TEST_USER_NAME }} --init name: ${{ matrix.test_script }} args: | ./${{ matrix.test_script }} diff --git a/.github/workflows/test_docker_debian_v3.yml b/.github/workflows/test_docker_debian_v3.yml index 06718afba..673745db9 100644 --- a/.github/workflows/test_docker_debian_v3.yml +++ b/.github/workflows/test_docker_debian_v3.yml @@ -45,9 +45,25 @@ jobs: debian_codename: 'bookworm' platform: linux/arm/v7 + # # can be activate on test branches, currently failing + # run_bookworm_armv6: + # name: 'bookworm armv6' + # uses: ./.github/workflows/test_docker_debian_codename_sub_v3.yml + # with: + # debian_codename: 'bookworm' + # platform: linux/arm/v6 + run_bullseye_armv7: name: 'bullseye armv7' uses: ./.github/workflows/test_docker_debian_codename_sub_v3.yml with: debian_codename: 'bullseye' platform: linux/arm/v7 + + # # can be activate on test branches, currently failing + # run_bullseye_armv6: + # name: 'bullseye armv6' + # uses: ./.github/workflows/test_docker_debian_codename_sub_v3.yml + # with: + # debian_codename: 'bullseye' + # platform: linux/arm/v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbf12f84d..f4ac9cd16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,12 +45,17 @@ as local, temporary scratch areas. Contributors have played a bigger role over time to keep Phoniebox on the edge of innovation :) -We want to keep it as easy as possible to contribute changes that get things working in your environment. -There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. +Our goal is to make it simple for you to contribute changes that improve functionality in your specific environment. +To achieve this, we have a set of guidelines that we kindly request contributors to adhere to. +These guidelines help us maintain a streamlined process and stay on top of incoming contributions. -Development for Version 3 is done on the git branch `future3/develop`. How to move to that branch, see below. +To report bug fixes and improvements, please follow the steps outlined below: +1. For bug fixes and minor improvements, simply open a new issue or pull request (PR). +2. If you intend to port a feature from Version 2.x to future3 or wish to implement a new feature, we recommend reaching out to us beforehand. + - In such cases, please create an issue outlining your plans and intentions. + - We will ensure that there are no ongoing efforts on the same topic. -For bug fixes and improvements just open an issue or PR as described below. If you plan to port a feature from Version 2.X or implement a new feature, it is advisable to contact us first. In this case, also open an issue describing what you are planning to do. We will just check that nobody else is already on the subject. We are looking forward to your work. Check the current [feature list](https://rpi-jukebox-rfid.readthedocs.io/en/latest/featurelist.html) for available features and work in progress. +We eagerly await your contributions! You can review the current [feature list](documentation/developers/status.md) to check for available features and ongoing work. ## Getting Started @@ -60,31 +65,21 @@ For bug fixes and improvements just open an issue or PR as described below. If y Version 2 will continue to live for quite a while. * Clearly describe the issue including steps to reproduce when it is a bug * Make sure you fill in the earliest version that you know has the issue -* By default this will get you to the `future3/main` branch. You will move to the `future3/develop` branch, do this: - -~~~bash -cd ~/RPi-Jukebox-RFID -git checkout future3/develop -git fetch origin -git reset --hard origin/future3/develop -git pull -~~~ The preferred way of code contributions are [pull requests (follow this link for a small howto)](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github). -And, ideally pull requests use the "running code" on the `future3/develop` branch of your Phoniebox. +And ideally pull requests use the "running code" of your Phoniebox. Alternatively, feel free to post tweaks, suggestions and snippets in the ["issues" section](https://github.com/MiczFlor/RPi-Jukebox-RFID/issues). ## Making Changes +* Create a fork of this repository * Create a topic branch from where you want to base your work. - * This is usually the master branch or the develop branch. - * Only target release branches if you are certain your fix must be on that + * This is usually the `future3/develop` branch. + * Only target the `future3/main` branch if you are certain your fix must be on that branch. - * To quickly create a topic branch based on master, run `git checkout -b - fix/master/my_contribution master`. Please avoid working directly on the - `master` branch. * Make commits of logical and atomic units. * Check for unnecessary whitespace with `git diff --check` before committing. +* See also the [documentation for developers](documentation/developers/README.md) ## Making Trivial Changes @@ -168,8 +163,8 @@ The original contributor will be notified of the revert. ## Guidelines -* Phoniebox runs on Raspian **Buster**. Therefore, all Python code should work at least with **Python 3.7**. -* For GPIO all code should work with **RPi.GPIO**. gpiozero is currently not intended to use. +* Phoniebox runs on Raspberry Pi OS. +* Minimum python version is currently **Python 3.9**. ## Additional Resources diff --git a/ci/installation/run_install_common.sh b/ci/installation/run_install_common.sh index 3d4c59778..b8c641580 100644 --- a/ci/installation/run_install_common.sh +++ b/ci/installation/run_install_common.sh @@ -24,7 +24,7 @@ export ENABLE_WEBAPP_PROD_DOWNLOAD=true # n - setup rfid reader # y - setup samba # y - setup webapp -# - - install node (forced WebApp Download) +# - - build webapp (skipped due to forced webapp Download) # n - setup kiosk mode # n - reboot diff --git a/ci/installation/run_install_faststartup.sh b/ci/installation/run_install_faststartup.sh index 249d78ffc..134aeca71 100644 --- a/ci/installation/run_install_faststartup.sh +++ b/ci/installation/run_install_faststartup.sh @@ -22,7 +22,7 @@ LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" # n - setup rfid reader # n - setup samba # n - setup webapp -# - - install node (only with webapp = y) +# - - build webapp (only with webapp = y) # - - setup kiosk mode (only with webapp = y) # n - reboot diff --git a/ci/installation/run_install_libzmq_local.sh b/ci/installation/run_install_libzmq_local.sh index aa3726b2f..335cb24a1 100644 --- a/ci/installation/run_install_libzmq_local.sh +++ b/ci/installation/run_install_libzmq_local.sh @@ -23,7 +23,7 @@ export BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE=true # n - setup rfid reader # n - setup samba # n - setup webapp -# - - install node (only with webapp = y) +# - - build webapp (only with webapp = y) # - - setup kiosk mode (only with webapp = y) # n - reboot diff --git a/ci/installation/run_install_webapp_download.sh b/ci/installation/run_install_webapp_download.sh index 698e057b9..ded27ec54 100644 --- a/ci/installation/run_install_webapp_download.sh +++ b/ci/installation/run_install_webapp_download.sh @@ -4,7 +4,7 @@ # Used e.g. for tests on Docker # Objective: -# Test for the WebApp (download) and dependent features path. +# Test for the Web App (download) and dependent features path. SOURCE="${BASH_SOURCE[0]}" SCRIPT_DIR="$(dirname "$SOURCE")" @@ -22,7 +22,7 @@ LOCAL_INSTALL_SCRIPT_PATH="${LOCAL_INSTALL_SCRIPT_PATH%/}" # n - setup rfid reader # n - setup samba # y - setup webapp -# n - install node +# n - build webapp # y - setup kiosk mode # n - reboot diff --git a/ci/installation/run_install_webapp_local.sh b/ci/installation/run_install_webapp_local.sh index 917f985af..7af16df3a 100644 --- a/ci/installation/run_install_webapp_local.sh +++ b/ci/installation/run_install_webapp_local.sh @@ -4,7 +4,7 @@ # Used e.g. for tests on Docker # Objective: -# Test for the WebApp (build locally) and dependent features path. +# Test for the Web App (build locally) and dependent features path. SOURCE="${BASH_SOURCE[0]}" SCRIPT_DIR="$(dirname "$SOURCE")" @@ -23,7 +23,7 @@ export ENABLE_WEBAPP_PROD_DOWNLOAD=false # n - setup rfid reader # n - setup samba # y - setup webapp -# y - install node +# y - build webapp # y - setup kiosk mode # n - reboot diff --git a/documentation/builders/autohotspot.md b/documentation/builders/autohotspot.md index 69e6f4d6a..ecf996f81 100644 --- a/documentation/builders/autohotspot.md +++ b/documentation/builders/autohotspot.md @@ -2,7 +2,7 @@ The Auto-Hotspot function allows the Jukebox to switch between its connection between a known WiFi and an automatically generated hotspot -so that you can still access via SSH or Webapp. +so that you can still access via SSH or Web App. > [!IMPORTANT] > Please configure the WiFi connection to your home access point before enabling these feature! @@ -17,10 +17,10 @@ hotspot named `Phoniebox_Hotspot`. You will be able to connect to this hotspot using the given password in the installation or the default password: `PlayItLoud!` -### Webapp +### Web App After connecting to the `Phoniebox_Hotspot` you are able to connect to -the webapp accessing the website [10.0.0.5](http://10.0.0.5/). +the Web App accessing the website [10.0.0.5](http://10.0.0.5/). ### ssh @@ -69,7 +69,7 @@ ieee80211d=1 ## Disabling automatism -Auto-Hotspot can be enabled or disabled using the Webapp. +Auto-Hotspot can be enabled or disabled using the Web App. > [!IMPORTANT] > Disabling or enabling will keep the last state. diff --git a/documentation/builders/concepts.md b/documentation/builders/concepts.md index 37c13db89..0a4305cfe 100644 --- a/documentation/builders/concepts.md +++ b/documentation/builders/concepts.md @@ -12,7 +12,7 @@ The core app is centered around a plugin concept. This serves three purposes: ## Remote Procedure Call Server (RPC) -The Remote Procedure Call (RPC) server allows remotely triggering actions (e.g., from the Webapp) within the Jukebox core application. Only Python functions registered by the plugin interface can be called. This simplifies external APIs and lets us focus on the relevant user functions. +The Remote Procedure Call (RPC) server allows remotely triggering actions (e.g., from the Web App) within the Jukebox core application. Only Python functions registered by the plugin interface can be called. This simplifies external APIs and lets us focus on the relevant user functions. 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). diff --git a/documentation/builders/installation.md b/documentation/builders/installation.md index 945368480..7ce0e59c0 100644 --- a/documentation/builders/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! Pi 4 and 5 are an excess ;-) +> All Raspberry Pi models are supported. For sufficient performance, **we recommend Pi 2, 3 or Zero 2** (`ARMv7` models). Because Pi 1 or Zero 1 (`ARMv6` models) have limited resources, they are slower (during installation and start up procedure) and might 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. @@ -79,30 +79,48 @@ You will need a terminal, like PuTTY for Windows or the Terminal app for Mac to ## Install Phoniebox software -Run the following command in your SSH terminal and follow the instructions +Choose a version, run the corresponding install command in your SSH terminal and follow the instructions. +* [Stable Release](#stable-release) +* [Pre-Release](#pre-release) +* [Development](#development) + +After a successful installation, [configure your Phoniebox](configuration.md). + +> [!TIP] +> Depending on your hardware, this installation might last around 60 minutes (usually it's faster, 20-30 min). It updates OS packages, installs Phoniebox dependencies and applies settings. Be patient and don't let your computer go to sleep. It might disconnect your SSH connection causing the interruption of the installation process. Consider starting the installation in a terminal multiplexer like 'screen' or 'tmux' to avoid this. + +### Stable Release +This will install the latest **stable release** from the *future3/main* branch. ```bash 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*. +### Pre-Release +This will install the latest **pre-release** from the *future3/develop* branch. -To install directly from a specific branch and/or a different repository -specify the variables like this: +```bash +cd; GIT_BRANCH='future3/develop' bash <(wget -qO- https://raw.githubusercontent.com/MiczFlor/RPi-Jukebox-RFID/future3/develop/installation/install-jukebox.sh) +``` + +### Development +You can also install a specific branch and/or a fork repository. Update the variables to refer to your desired location. (The URL must not necessarily be updated, unless you have actually updated the file being downloaded.) + +> [!IMPORTANT] +> A fork repository must be named '*RPi-Jukebox-RFID*' like the official repository ```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 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) +> The Installation of the official repository's release branches ([Stable Release](#stable-release) and [Pre-Release](#pre-release)) will deploy a pre-build bundle of the Web App. +> If you install another branch or from a fork repository, the Web App needs to be built locally. This is part of the installation process. See the the developers [Web App](../developers/webapp.md) documentation for further details. -If you suspect an error you can monitor the installation-process with +### Logs +To follow the installation closely, use this command in another terminal. ```bash cd; tail -f INSTALL-.log ``` -After successful installation, continue with [configuring your Phoniebox](configuration.md). diff --git a/documentation/builders/system.md b/documentation/builders/system.md index 2f7df8888..f6eeb7ba1 100644 --- a/documentation/builders/system.md +++ b/documentation/builders/system.md @@ -7,7 +7,7 @@ The system consists of 1. [Music Player Daemon (MPD)](system.md#music-player-daemon-mpd) which we use for all music playback (local, stream, podcast, ...) 2. [PulseAudio](system.md#pulseaudio) for flexible audio output support 3. [Jukebox Core Service](system.md#jukebox-core-service) for controlling MPD and PulseAudio and providing all the features -4. [Web UI](system.md#web-ui) which is served through an Nginx web server +4. [Web App](system.md#web-app-ui) as User Interface (UI) for a web browser 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 users home folder `~/RPi-Jukebox-RFID`. @@ -96,9 +96,9 @@ The `systemd` service file is located at the default location for user services: Starting and stopping the service can be useful for debugging or configuration checks. -## Web UI +## Web App (UI) -The Web UI is served using nginx. Nginx runs as a system service. The home directory is localed at +The [Web App](../developers/webapp.md) is served using nginx. Nginx runs as a system service. The home directory is located at ```text ./src/webapp/build diff --git a/documentation/builders/update.md b/documentation/builders/update.md index f6dab2c91..94baf0a93 100644 --- a/documentation/builders/update.md +++ b/documentation/builders/update.md @@ -29,7 +29,7 @@ $ git pull $ diff shared/settings/jukebox.yaml resources/default-settings/jukebox.default.yaml $ cd src/webapp -$ ./run_rebuild.sh +$ ./run_rebuild.sh -u ``` ## Migration Path from Version 2 diff --git a/documentation/developers/README.md b/documentation/developers/README.md index 64fee44c1..cfa389713 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -8,6 +8,7 @@ ## Reference * [Jukebox Apps](./coreapps.md) +* [Web App](./webapp.md) * [RFID Readers](./rfid) * [Feature Status](./status.md) * [Known Issues](./known-issues.md) diff --git a/documentation/developers/developer-issues.md b/documentation/developers/developer-issues.md deleted file mode 100644 index bcb4d491a..000000000 --- a/documentation/developers/developer-issues.md +++ /dev/null @@ -1,86 +0,0 @@ -# Developer Issues - -## Building the Webapp on the PI - -### JavaScript heap out of memory - -While (re-) building the Web App, you get the following output: - -``` {.bash emphasize-lines="12"} -pi@MusicPi:~/RPi-Jukebox-RFID/src/webapp $ npm run build - -> webapp@0.1.0 build -> react-scripts build - -Creating an optimized production build... - -[...] - -<--- JS stacktrace ---> - -FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory -``` - -#### Reason - -Not enough memory for Node - -#### Solution - -Prior to building set the node memory environment variable. - -1. Make sure the value is less than the total available space on the - system, or you may run into the next issue. (Not always though!) - Check memory availability with `free -mt`. -2. We also experience trouble, when the space is set too small a - value. 512 always works, 256 sometimes does, sometimes does not. - If your free memory is small, consider increasing the swap size of - your system! - -``` bash -export NODE_OPTIONS=--max-old-space-size=512 -npm run build -``` - -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. - -```bash -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"} -pi@MusicPi:~/RPi-Jukebox-RFID/src/webapp $ npm run build - -> webapp@0.1.0 build -> react-scripts build - -... - -The build failed because the process exited too early. -This probably means the system ran out of memory or someone called 'kill -9' on the process. -``` - -#### Reason - -Node tried to allocate more memory than available on the system. - -#### Solution - -Adjust the node memory variable as described in [JavaScript heap out of memory](#javascript-heap-out-of-memory). But make sure to allocate less memory than the available memory. If that is not sufficient, increase the swap file size of your -system and try again. diff --git a/documentation/developers/development-environment.md b/documentation/developers/development-environment.md index 249f6263c..d2d995919 100644 --- a/documentation/developers/development-environment.md +++ b/documentation/developers/development-environment.md @@ -1,6 +1,6 @@ # 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. +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 (Web App), we recommend Docker. Developing on your local machine (Linux, Mac, Windows) works as well and requires all dependencies to be installed locally. - [Development Environment](#development-environment) - [Develop in Docker](#develop-in-docker) @@ -15,35 +15,15 @@ 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. +The full setup is running on the RPi and you access files via SSH. ### 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. +We recommend to use at least a Pi 3 or Pi Zero 2 for development. While this hardware won\'t be needed in production, it comes in helpful 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. +1. Follow the [installation preperation](../builders/installation.md#install-raspberry-pi-os-lite) steps +1. [Install](../builders/installation.md#development) your feature/fork branch of the Jukebox software. The official repository will be set as `upstream`. +1. If neccessary [build the Web App](./webapp.md) locally ## Develop on local machine diff --git a/documentation/developers/docker.md b/documentation/developers/docker.md index 80651ce84..3718373db 100644 --- a/documentation/developers/docker.md +++ b/documentation/developers/docker.md @@ -30,7 +30,7 @@ need to adapt some of those commands to your needs. $ cp ./resources/default-settings/jukebox.default.yaml ./shared/settings/jukebox.yaml ``` - * 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`. + * Override/Merge the values from the following [Override file](../../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 `./shared/audiofolders` @@ -159,7 +159,7 @@ Read these threads for details: [thread 1](https://unix.stackexchange.com/questi The Dockerfile is defined to start all Phoniebox related services. -Open in your browser to see the web application. +Open in your browser to see the Web App. While the `webapp` container does not require a reload while working on it (hot-reload is enabled), you will have to restart your `jukebox` diff --git a/documentation/developers/status.md b/documentation/developers/status.md index 48c2b6c3b..0a40f8125 100644 --- a/documentation/developers/status.md +++ b/documentation/developers/status.md @@ -6,7 +6,7 @@ There are a few things that are specifically not integrated yet: playing streams In the following is the currently implemented feature list in more detail. It also shows some of the shortcomings. However, the list is _not complete in terms of planned features_, but probably _reflects more of where work is currently being put into_. -**For new contributors:** If you want to port a feature from version 2.X or implement a new feature, contact us. Open an issue or join us in the chat room. You may pick topics marked as open below, but also any other topic missing in the list below. As mentioned, that list is not complete in terms of open features. Check the [Contribution guide](https://github.com/MiczFlor/RPi-Jukebox-RFID/blob/future3/main/CONTRIBUTING.md). +**For new contributors:** If you want to port a feature from version 2.X or implement a new feature, contact us. Open an issue or join us in the chat room. You may pick topics marked as open below, but also any other topic missing in the list below. As mentioned, that list is not complete in terms of open features. Check the [Contribution guide](../../CONTRIBUTING.md). Topics marked _in progress_ are already in the process of implementation by community members. diff --git a/documentation/developers/webapp.md b/documentation/developers/webapp.md new file mode 100644 index 000000000..2e8504337 --- /dev/null +++ b/documentation/developers/webapp.md @@ -0,0 +1,149 @@ +# Web App + +The Web App sources are located in `src/webapp`. A pre-build bundle of the Web App is deployed when installing from an official release branch. If you install from a feature branch or a fork repository, the Web App needs to be built locally. This requires Node to be installed and is part of the installation process. + +## Install node manually + +If you installed from an official release branch, Node might not be installed. To install Node for local development, follow the [official setup](https://deb.nodesource.com/). + +``` bash +NODE_MAJOR=20 +sudo apt-get -y update && sudo apt-get -y install ca-certificates curl gnupg +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +sudo apt-get -y update && sudo apt-get -y install nodejs +``` + +## Develop the Web App + +The Web App is a React application based on [Create React App](https://create-react-app.dev/). To start a development server, run the following command: + +``` +cd ~/RPi-Jukebox-RFID/src/webapp +npm install # Just the first time or when dependencies change +npm start +``` + +## Build the Web App + +To build your Web App after its source code has changed (e.g. through a local change or through a pull from the repository), it needs to be rebuilt manually. +Use the provided script to rebuild whenever required. The artifacts can be found in the folder `build`. + +```bash +cd ~/RPi-Jukebox-RFID/src/webapp; \ +./run_rebuild.sh -u +``` + +After a successfull build you might need to restart the web server. + +``` +sudo systemctl restart nginx.service +``` + +## Known Issues while building + +### JavaScript heap out of memory + +While (re-) building the Web App, you get the following output: + +``` {.bash emphasize-lines="12"} +> webapp@0.1.0 build +> react-scripts build + +Creating an optimized production build... + +[...] + +<--- JS stacktrace ---> + +FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory +``` + +#### Reason + +Not enough memory for Node + +#### Solution + +Use the [provided script](#build-the-web-app) to rebuild the Web App. It sets the needed node options and also checks and adjusts the swap size if there is not enough memory available. + +If you need to run the commands manually, make sure to have enough memory available (min. 512 MB). The following commands might help. + +Set the swapsize to 512 MB (and deactivate swapfactor). Adapt accordingly if you have a SD Card with small capacity. +```bash +sudo dphys-swapfile swapoff +sudo sed -i "s|.*CONF_SWAPSIZE=.*|CONF_SWAPSIZE=512|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 +``` + +Set Node's maximum amount of memory. Memory must be available. +``` bash +export NODE_OPTIONS=--max-old-space-size=512 +npm run build +``` + +### Process exited too early // kill -9 + +``` {.bash emphasize-lines="8,9"} +> webapp@0.1.0 build +> react-scripts build + +[...] + +The build failed because the process exited too early. +This probably means the system ran out of memory or someone called 'kill -9' on the process. +``` + +#### Reason + +Node tried to allocate more memory than available on the system. + +#### Solution + +See [JavaScript heap out of memory](#javascript-heap-out-of-memory) + + +### Client network socket disconnected + +``` {.bash emphasize-lines="8,9"} +[...] + +npm ERR! code ECONNRESET +npm ERR! network Client network socket disconnected before secure TLS connection was established +npm ERR! network This is a problem related to network connectivity. +npm ERR! network In most cases you are behind a proxy or have bad network settings. +npm ERR! network +npm ERR! network If you are behind a proxy, please make sure that the +npm ERR! network 'proxy' config is set properly. See: 'npm help config' +``` + +#### Reason + +The network connection is too slow or has issues. +This tends to happen on `armv6l` devices where building takes significantly more time due to limited resources. + +#### Solution + +Try to use an ethernet connection. A reboot and/or running the script multiple times might also help ([Build produces EOF errors](#build-produces-eof-errors) might occur). + +If the error still persists, try to raise the timeout for npm package resolution. + +1. Open the npm config file in an editor +1. Increase the `fetch-retry-*` values by '30000' (30 seconds) and save +1. Retry the build + +### Build produces EOF errors + +#### Reason + +A previous run failed during installation and left a package corrupted. + +#### Solution + +Remove the mode packages and rerun again the script. +``` {.bash emphasize-lines="8,9"} +rm -rf node_modules +``` diff --git a/installation/includes/00_constants.sh b/installation/includes/00_constants.sh index 380e1de2e..89299989c 100644 --- a/installation/includes/00_constants.sh +++ b/installation/includes/00_constants.sh @@ -16,4 +16,7 @@ GIT_BRANCH_DEVELOP=${GIT_BRANCH_DEVELOP:-future3/develop} # This message will be displayed at the end of the installation process # Functions wanting to have something important printed at the end should APPEND to this variable +# example: +# local tmp_fin_message="A Message" +# FIN_MESSAGE="${FIN_MESSAGE:+$FIN_MESSAGE\n}${tmp_fin_message}" FIN_MESSAGE="" diff --git a/installation/includes/01_default_config.sh b/installation/includes/01_default_config.sh index fa1bafb61..ec7b67b66 100644 --- a/installation/includes/01_default_config.sh +++ b/installation/includes/01_default_config.sh @@ -26,8 +26,6 @@ GIT_USE_SSH=${GIT_USE_SSH:-"true"} # For non-production builds, the Wep App must be build locally # Valid values # - release-only: download in release branch only -# - true: force download even in non-release branch, +# - true: force download even in non-release branch # - false: never download ENABLE_WEBAPP_PROD_DOWNLOAD=${ENABLE_WEBAPP_PROD_DOWNLOAD:-"release-only"} -# Install Node during setup for Web App building. This is only needed for development builds -ENABLE_INSTALL_NODE=${ENABLE_INSTALL_NODE:-"false"} diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index 239a18ccf..e9ca7640e 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -2,6 +2,17 @@ ### Helpers +show_slow_hardware_message() { + if [[ $(uname -m) == "armv6l" ]]; then + print_c "-------------------------------------------------------------------- +| Your hardware is a little slower so this will take a while. | +| Go watch a movie but don't let your computer go to sleep for the | +| SSH connection to remain intact. | +-------------------------------------------------------------------- +" + fi +} + # $1->start, $2->end calc_runtime_and_print() { runtime=$(($2-$1)) diff --git a/installation/includes/03_welcome.sh b/installation/includes/03_welcome.sh index 5b3ee84be..62c4910c8 100644 --- a/installation/includes/03_welcome.sh +++ b/installation/includes/03_welcome.sh @@ -16,16 +16,16 @@ You are turning your Raspberry Pi into a Phoniebox. Good choice! Depending on your hardware, this installation might last -around 60 minutes (usually it's faster). It updates OS -packages, installs Phoniebox dependencies and registers -settings. Be patient and don't let your computer go to -sleep. It might disconnect your SSH connection causing -the interruption of the installation process. +around 60 minutes (usually it's faster, 20-30 min). It +updates OS packages, installs Phoniebox dependencies and +applies settings. Be patient and don't let your computer +go to sleep. It might disconnect your SSH connection +causing the interruption of the installation process. Consider starting the installation in a terminal multiplexer like 'screen' or 'tmux' to avoid this. -By the way, you can follow the installation details here -in a separate SSH session: +To follow the installation closely, use this command +in another terminal. cd; tail -f ${INSTALLATION_LOGFILE} Let's set up your Phoniebox. diff --git a/installation/includes/05_finish.sh b/installation/includes/05_finish.sh index 22ba6ae80..c48fc31d2 100644 --- a/installation/includes/05_finish.sh +++ b/installation/includes/05_finish.sh @@ -11,7 +11,7 @@ ${FIN_MESSAGE} In order to start, you need to reboot your Raspberry Pi. Your SSH connection will disconnect. -After the reboot, you can access the WebApp in your browser at +After the reboot, you can access the Web App in your browser at http://${local_hostname}.local or http://${CURRENT_IP_ADDRESS} Don't forget to upload files. " diff --git a/installation/routines/customize_options.sh b/installation/routines/customize_options.sh index 7409a6e07..3e94b07dc 100644 --- a/installation/routines/customize_options.sh +++ b/installation/routines/customize_options.sh @@ -189,12 +189,12 @@ Do you want to install Samba? [Y/n]" _option_webapp() { # ENABLE_WEBAPP clear_c - print_c "------------------------ WEBAPP ------------------------- + print_c "------------------------ WEB APP ------------------------ 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]" +Would you like to install the Web App? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) @@ -213,7 +213,7 @@ _option_kiosk_mode() { print_c "----------------------- KIOSK MODE ---------------------- If you have a screen attached to your RPi, -this will launch the web application right after boot. +this will launch the Web App right after boot. It will only install the necessary xserver dependencies and not the entire RPi desktop environment. @@ -282,48 +282,38 @@ Disable Pi's on-chip audio (headphone / jack output)? [y/N]" _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" || "$CI_RUNNING" == "true" ]]; then - ENABLE_INSTALL_NODE=true + if [[ "$GIT_BRANCH" != "${GIT_BRANCH_RELEASE}" && "$GIT_BRANCH" != "${GIT_BRANCH_DEVELOP}" ]] || [[ "$GIT_USER" != "$GIT_UPSTREAM_USER" ]] || [[ "$CI_RUNNING" == "true" ]] ; then # 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 ENABLE_WEBAPP_PROD_DOWNLOAD=false fi - if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" == false ]]; then + if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" != true && "$ENABLE_WEBAPP_PROD_DOWNLOAD" != "release-only" ]]; then clear_c - print_c "--------------------- WEBAPP NODE --------------------- + print_c "--------------------- WEB APP BUILD --------------------- -You are installing from an unofficial branch. -Therefore a prebuilt web app is not available and -you will have to build it locally. +You are installing from a non-release branch +and/or an unofficial repository. +Therefore a pre-build Web App is not available +and it needs to be built locally. This requires Node to be installed. -You can choose to decline the Node installation and -the lastest prebuilt version from the main repository -will be installed. This can lead to incompatibilities. +If you decline, the lastest pre-build version +from the official repository will be installed. +This can lead to incompatibilities. -Do you want to install Node? [Y/n]" +Do you want to build the Web App? [Y/n]" read -r response case "$response" in [nN][oO]|[nN]) - ENABLE_INSTALL_NODE=false - ENABLE_WEBAPP_PROD_DOWNLOAD=true - ;; + ENABLE_WEBAPP_PROD_DOWNLOAD=true + ;; *) - # This message will be displayed at the end of the installation process - 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}" - ;; + ;; esac fi fi - log "ENABLE_INSTALL_NODE=${ENABLE_INSTALL_NODE}" - if [ "$ENABLE_INSTALL_NODE" != true ]; then - log "ENABLE_WEBAPP_PROD_DOWNLOAD=${ENABLE_WEBAPP_PROD_DOWNLOAD}" - fi + log "ENABLE_WEBAPP_PROD_DOWNLOAD=${ENABLE_WEBAPP_PROD_DOWNLOAD}" } _run_customize_options() { diff --git a/installation/routines/install.sh b/installation/routines/install.sh index 62d602f17..f1f2a5f80 100644 --- a/installation/routines/install.sh +++ b/installation/routines/install.sh @@ -2,6 +2,7 @@ install() { clear_c customize_options clear_c + show_slow_hardware_message set_raspi_config set_ssh_qos update_raspi_os diff --git a/installation/routines/setup_jukebox_core.sh b/installation/routines/setup_jukebox_core.sh index 1c524abb0..d9c06b937 100644 --- a/installation/routines/setup_jukebox_core.sh +++ b/installation/routines/setup_jukebox_core.sh @@ -8,14 +8,6 @@ JUKEBOX_ZMQ_VERSION="4.3.5" JUKEBOX_PULSE_CONFIG="${HOME_PATH}"/.config/pulse/default.pa JUKEBOX_SERVICE_NAME="${SYSTEMD_USR_PATH}/jukebox-daemon.service" -_show_slow_hardware_message() { - 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. | - --------------------------------------------------------------------" -} - # Functions _jukebox_core_install_os_dependencies() { print_lc " Install Jukebox OS dependencies" @@ -86,11 +78,6 @@ _jukebox_core_build_and_install_pyzmq() { print_lc " Install pyzmq with libzmq-drafts to support WebSockets" if ! pip list | grep -F pyzmq >> /dev/null; then - - if [[ $(uname -m) == "armv6l" ]]; then - _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 diff --git a/installation/routines/setup_jukebox_webapp.sh b/installation/routines/setup_jukebox_webapp.sh index 2884f18cb..7fcbce7ff 100644 --- a/installation/routines/setup_jukebox_webapp.sh +++ b/installation/routines/setup_jukebox_webapp.sh @@ -3,58 +3,87 @@ # Constants WEBAPP_NGINX_SITE_DEFAULT_CONF="/etc/nginx/sites-available/default" -# For ARMv7+ +# Node major version used. +# If changed also update in .github\actions\build-webapp\action.yml NODE_MAJOR=20 -# For ARMv6 -# To update version, follow these links -# https://github.com/sdesalas/node-pi-zero -# https://github.com/nodejs/unofficial-builds/ -NODE_SOURCE_EXPERIMENTAL="https://raw.githubusercontent.com/sdesalas/node-pi-zero/master/install-node-v16.3.0.sh" +# Node version for ARMv6 (unofficial builds) +NODE_ARMv6_VERSION=v20.10.0 -_jukebox_webapp_install_node() { - sudo apt-get -y update +OPTIONAL_WEBAPP_BUILD_FAILED=false - if which node > /dev/null; then - 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 +_jukebox_webapp_install_node() { print_lc " Install NodeJS" - # Zero and older versions of Pi with ARMv6 only - # support experimental NodeJS - if [[ $(uname -m) == "armv6l" ]]; then - wget -O - ${NODE_SOURCE_EXPERIMENTAL} | sudo bash - sudo apt-get -qq -y install nodejs - sudo npm install --silent -g npm + local node_version_installed=$(node -v 2>/dev/null) + local arch=$(uname -m) + if [[ "$arch" == "armv6l" ]]; then + if [ "$node_version_installed" == "$NODE_ARMv6_VERSION" ]; then + print_lc " Skipping. NodeJS already installed" + else + # For ARMv6 unofficial build + # https://github.com/nodejs/unofficial-builds/ + local node_tmp_dir="${HOME_PATH}/node" + local node_install_dir=/usr/local/lib/nodejs + local node_filename="node-${NODE_ARMv6_VERSION}-linux-${arch}" + local node_tar_filename="${node_filename}.tar.gz" + node_download_url="https://unofficial-builds.nodejs.org/download/release/${NODE_ARMv6_VERSION}/${node_tar_filename}" + + mkdir -p "${node_tmp_dir}" && cd "${node_tmp_dir}" || exit_on_error + download_from_url ${node_download_url} ${node_tar_filename} + tar -xzf ${node_tar_filename} + rm -rf ${node_tar_filename} + + # see https://github.com/nodejs/help/wiki/Installation + # Remove existing symlinks + sudo unlink /usr/bin/node 2>/dev/null + sudo unlink /usr/bin/npm 2>/dev/null + sudo unlink /usr/bin/npx 2>/dev/null + + # Clear existing nodejs and copy new files + sudo rm -rf "${node_install_dir}" + sudo mv "${node_filename}" "${node_install_dir}" + + sudo ln -s "${node_install_dir}/bin/node" /usr/bin/node + sudo ln -s "${node_install_dir}/bin/npm" /usr/bin/npm + sudo ln -s "${node_install_dir}/bin/npx" /usr/bin/npx + + cd "${HOME_PATH}" || exit_on_error + rm -rf "${node_tmp_dir}" + fi else - # install NodeJS and npm as recommended in - # https://github.com/nodesource/distributions - sudo apt-get install -y ca-certificates curl gnupg - sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list - sudo apt-get update - sudo apt-get install -y nodejs + if [[ "$node_version_installed" == "v${NODE_MAJOR}."* ]]; then + print_lc " Skipping. NodeJS already installed" + else + sudo apt-get -y remove nodejs + # install NodeJS as recommended in + # https://deb.nodesource.com/ + sudo apt-get -y update && sudo apt-get -y install ca-certificates curl gnupg + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list + sudo apt-get -y update && sudo apt-get -y install nodejs + fi fi - fi } -# TODO: Avoid building the app locally -# Instead implement a Github Action that prebuilds on commititung a git tag _jukebox_webapp_build() { - print_lc " Building web application" - cd "${INSTALLATION_PATH}/src/webapp" || exit_on_error - npm ci --prefer-offline --no-audit --production - rm -rf build - # The build wrapper script checks available memory on system and sets Node options accordingly - ./run_rebuild.sh + print_lc " Building Web App" + cd "${INSTALLATION_PATH}/src/webapp" || exit_on_error + if ! ./run_rebuild.sh -u ; then + print_lc " Web App build failed! + Follow instructions shown at the end of installation!" + OPTIONAL_WEBAPP_BUILD_FAILED=true + # This message will be displayed at the end of the installation process + local tmp_fin_message="ATTENTION: The build of the Web App failed during installation. + Please run the build manually with the following command + $ cd ~/RPi-Jukebox-RFID/src/webapp && ./run_rebuild.sh -u + Read the documentation regarding local Web App builds!" + FIN_MESSAGE="${FIN_MESSAGE:+$FIN_MESSAGE\n}${tmp_fin_message}" + fi } _jukebox_webapp_download() { - print_lc " Downloading web application" + print_lc " Downloading Web App" local jukebox_version=$(python "${INSTALLATION_PATH}/src/jukebox/jukebox/version.py") local git_head_hash=$(git -C "${INSTALLATION_PATH}" rev-parse --verify --quiet HEAD) local git_head_hash_short=${git_head_hash:0:10} @@ -67,11 +96,11 @@ _jukebox_webapp_download() { if validate_url ${download_url_commit} ; then log " DOWNLOAD_URL ${download_url_commit}" download_from_url ${download_url_commit} ${tar_filename} - elif [[ $ENABLE_WEBAPP_PROD_DOWNLOAD == true ]] && validate_url ${download_url_latest} ; then + elif [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" == true ]] && validate_url ${download_url_latest} ; then log " DOWNLOAD_URL ${download_url_latest}" download_from_url ${download_url_latest} ${tar_filename} else - exit_on_error "No prebuild webapp bundle found!" + exit_on_error "No prebuild Web App bundle found!" fi tar -xzf ${tar_filename} rm -f ${tar_filename} @@ -80,7 +109,7 @@ _jukebox_webapp_download() { _jukebox_webapp_register_as_system_service_with_nginx() { print_lc " Install and configure nginx" - sudo apt-get -qq -y update + sudo apt-get -y update sudo apt-get -y purge apache2 sudo apt-get -y install nginx @@ -97,11 +126,22 @@ _jukebox_webapp_register_as_system_service_with_nginx() { _jukebox_webapp_check() { print_verify_installation - if [[ $ENABLE_WEBAPP_PROD_DOWNLOAD == true || $ENABLE_WEBAPP_PROD_DOWNLOAD == release-only ]] ; then + 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 + else + local arch=$(uname -m) + if [[ "$arch" == "armv6l" ]]; then + local node_version_installed=$(node -v 2>/dev/null) + log " Verify 'node' is installed" + test ! "${node_version_installed}" == "${NODE_ARMv6_VERSION}" && exit_on_error "ERROR: 'node' not in expected version: '${node_version_installed}' instead of '${NODE_ARMv6_VERSION}'!" + log " CHECK" + else + verify_apt_packages nodejs + fi + + if [[ "$OPTIONAL_WEBAPP_BUILD_FAILED" == false ]]; then + verify_dirs_exists "${INSTALLATION_PATH}/src/webapp/build" + fi fi verify_apt_packages nginx @@ -111,14 +151,11 @@ _jukebox_webapp_check() { } _run_setup_jukebox_webapp() { - if [[ $ENABLE_WEBAPP_PROD_DOWNLOAD == true || $ENABLE_WEBAPP_PROD_DOWNLOAD == release-only ]] ; then + if [[ "$ENABLE_WEBAPP_PROD_DOWNLOAD" == true || "$ENABLE_WEBAPP_PROD_DOWNLOAD" == "release-only" ]] ; then _jukebox_webapp_download - fi - if [[ $ENABLE_INSTALL_NODE == true ]] ; then + else _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 + _jukebox_webapp_build fi _jukebox_webapp_register_as_system_service_with_nginx _jukebox_webapp_check @@ -126,6 +163,6 @@ _run_setup_jukebox_webapp() { setup_jukebox_webapp() { if [ "$ENABLE_WEBAPP" == true ] ; then - run_with_log_frame _run_setup_jukebox_webapp "Install web application" + run_with_log_frame _run_setup_jukebox_webapp "Install Web App" fi } diff --git a/resources/default-settings/gpio.example.yaml b/resources/default-settings/gpio.example.yaml index f19b83c87..59793210f 100644 --- a/resources/default-settings/gpio.example.yaml +++ b/resources/default-settings/gpio.example.yaml @@ -1,6 +1,6 @@ # Provides a few selected examples of GPIO configuration # Check out the documentation for many more configuration recipes -# https://rpi-jukebox-rfid.readthedocs.io/en/latest/index.html +# documentation/builders/gpio.md pin_factory: type: rpigpio.RPiGPIOFactory output_devices: diff --git a/resources/html/404.html b/resources/html/404.html index 1a60d2415..a79decc6c 100755 --- a/resources/html/404.html +++ b/resources/html/404.html @@ -15,7 +15,7 @@

Ups! Requested file not found.

Why not try again from the top-level of

diff --git a/resources/html/runbuildui.html b/resources/html/runbuildui.html index 04f92c704..6ade7bb0a 100755 --- a/resources/html/runbuildui.html +++ b/resources/html/runbuildui.html @@ -12,11 +12,11 @@

Ups! Looks like your Web UI has not been build!

No reason to panic. Please run through the following steps:

    -
  • cd ~/RPi-Jukebox-RFID/src/webapp
  • -
  • ./run_rebuild.sh -u
  • -
  • Reload this page
  • +
  • cd ~/RPi-Jukebox-RFID/src/webapp

  • +
  • ./run_rebuild.sh -u

  • +
  • Reload this page

-

In case of trouble when building the Web UI, consult the documentation! +

In case of trouble when building the Web UI, consult the official documentation! diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index c77c21051..a2dbc914a 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -56,8 +56,9 @@ """ # 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: RFID triggered Folder1, Web App 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. @@ -68,7 +69,7 @@ # # 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 +# Die Web App 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, @@ -76,7 +77,7 @@ # # 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. +# Den kann der Leser auch nicht tracken. Der State kann ja auch über die Web App 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 diff --git a/src/webapp/.gitignore b/src/webapp/.gitignore index 4d29575de..b32ff75cd 100644 --- a/src/webapp/.gitignore +++ b/src/webapp/.gitignore @@ -11,6 +11,9 @@ # production /build +# development +/build.bak + # misc .DS_Store .env.local diff --git a/src/webapp/.npmrc b/src/webapp/.npmrc new file mode 100644 index 000000000..f05c1e85f --- /dev/null +++ b/src/webapp/.npmrc @@ -0,0 +1,3 @@ +fetch-retries=10 +fetch-retry-mintimeout=20000 +fetch-retry-maxtimeout=120000 diff --git a/src/webapp/public/index.html b/src/webapp/public/index.html index 71ea85188..30c055a5e 100644 --- a/src/webapp/public/index.html +++ b/src/webapp/public/index.html @@ -7,7 +7,7 @@ diff --git a/src/webapp/run_rebuild.sh b/src/webapp/run_rebuild.sh index 870813d78..e8e4d06c0 100755 --- a/src/webapp/run_rebuild.sh +++ b/src/webapp/run_rebuild.sh @@ -1,30 +1,35 @@ #!/usr/bin/env bash usage() { - echo -e "\nRebuild the Web App\n" - echo "${BASH_SOURCE[0]} [-u] [-m SIZE]" - echo " -u : Update NPM dependencies before rebuild (only necessary if package.json changed)" - echo " -m SIZE : Set Node memory limit in MB (if omitted limit is deduced automatically)" - echo -e "\n\n" + echo -e "\nRebuild the Web App\n" + echo "${BASH_SOURCE[0]} [-u] [-m SIZE]" + echo " -u : Update NPM dependencies before rebuild (only necessary on first build or if package.json changed" + echo " -m SIZE : Set Node memory limit in MB (if omitted limit is deduced automatically and swap might be adjusted)" + echo " -v : Increase verbosity" + echo -e "\n\n" } UPDATE_DEPENDENCIES=false +VERBOSE=false -while getopts ":uhm:" opt; do - case ${opt} in - u) - UPDATE_DEPENDENCIES=true - ;; - m) - NODEMEM="${OPTARG}" - ;; - h) - usage - ;; - \?) - usage - ;; - esac +while getopts ":uhvm:" opt; do + case ${opt} in + u) + UPDATE_DEPENDENCIES=true + ;; + m) + NODEMEM="${OPTARG}" + ;; + v) + VERBOSE=true + ;; + h) + usage + ;; + \?) + usage + ;; + esac done # Change working directory to location of script @@ -32,44 +37,87 @@ SOURCE=${BASH_SOURCE[0]} SCRIPT_DIR="$(dirname "$SOURCE")" cd "$SCRIPT_DIR" || exit 1 -# Need to check free space and limit Node memory usage -# for PIs with little memory -MemTotal=$(grep MemTotal /proc/meminfo | awk '{print $2}') -MemFree=$(grep MemFree /proc/meminfo | awk '{print $2}') -SwapFree=$(grep SwapFree /proc/meminfo | awk '{print $2}') -TotalFree=$((SwapFree + MemFree)) - -MemTotal=$((MemTotal / 1024)) -MemFree=$((MemFree / 1024)) -SwapFree=$((SwapFree / 1024)) -TotalFree=$((TotalFree / 1024)) - -echo "Total phys memory: ${MemTotal} MB" -echo "Free phys memory : ${MemFree} MB" -echo "Free swap memory : ${SwapFree} MB" -echo "Free total memory: ${TotalFree} MB" - - -if [[ -z $NODEMEM ]]; then - # Keep a buffer of minimum 20 MB - if [[ $TotalFree -gt 1044 ]]; then - NODEMEM=1024 - elif [[ $TotalFree -gt 532 ]]; then - NODEMEM=512 - elif [[ $TotalFree -gt 276 ]]; then - NODEMEM=256 - else - echo "ERROR: Not enough memory available on system. Please increase swap size to give at least 276 MByte free memory." - echo "Current free memory = $TotalFree MB" - echo "Hint: if only a little memory is missing, stopping spocon, mpd, and jukebox-daemon might give you enough space" - exit 1 - fi -fi +change_swap() { + local new_swap_size="$1" + sudo dphys-swapfile swapoff || return 1 + sudo sed -i "s|.*CONF_SWAPSIZE=.*|CONF_SWAPSIZE=${new_swap_size}|g" /etc/dphys-swapfile || return 1 + sudo sed -i "s|^\s*CONF_SWAPFACTOR=|#CONF_SWAPFACTOR=|g" /etc/dphys-swapfile || return 1 + sudo dphys-swapfile setup 1&>/dev/null || return 1 + sudo dphys-swapfile swapon || return 1 +} -if [[ $NODEMEM -gt $TotalFree ]]; then - echo "ERROR: Requested node memory setting is larger than available free memory: $NODEMEM MB > $TotalFree MB" - exit 1 -fi +# Need to check free space and limit Node memory usage for PIs with little memory. +# Adjust swap if needed to have minimum memory available +calc_nodemem() { + echo "calculate usable memory" + # keep a buffer for the kernel etc. + local mem_buffer=256 + + local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}') + local mem_free=$(grep MemFree /proc/meminfo | awk '{print $2}') + local swap_total=$(grep SwapTotal /proc/meminfo | awk '{print $2}') + local swap_free=$(grep SwapFree /proc/meminfo | awk '{print $2}') + local total_free=$((swap_free + mem_free)) + + mem_total=$((mem_total / 1024)) + mem_free=$((mem_free / 1024)) + swap_total=$((swap_total / 1024)) + swap_free=$((swap_free / 1024)) + total_free=$((total_free / 1024)) + + local free_to_use=$((total_free - mem_buffer)) + + if [ "$VERBOSE" == true ]; then + echo " Total phys memory : ${mem_total} MB" + echo " Free phys memory : ${mem_free} MB" + echo " Total swap memory : ${swap_total} MB" + echo " Free swap memory : ${swap_free} MB" + echo " Free total memory : ${total_free} MB" + echo " Keep as buffer : ${mem_buffer} MB" + echo -e " Free usable memory: ${free_to_use} MB\n" + fi + + if [[ -z $NODEMEM ]]; then + # mininum memory used for node + local mem_min=512 + if [[ $free_to_use -gt $mem_min ]]; then + NODEMEM=$free_to_use + else + echo " WARN: Not enough memory left on system for node (usable ${free_to_use} MB, min. ${mem_min} MB)." + echo " Trying to adjust swap size ..." + + local add_swap_size=$((mem_min / 2)) + local new_swap_size=$((swap_total + add_swap_size)) + + # keep a buffer on the filesystem + local filesystem_needed=$((add_swap_size + 512)) + local filesystem_free=$(df -BM -P / | tail -n 1 | awk '{print $4}') + filesystem_free=${filesystem_free//M} + + if [ "$VERBOSE" == true ]; then + echo " New swap size = $new_swap_size MB" + echo " Additional filesystem space needed = $filesystem_needed MB" + echo " Current free filesystem space = $filesystem_free MB" + fi + + if [ "${filesystem_free}" -lt "${filesystem_needed}" ]; then + echo " ERROR: Not enough space available on filesystem for swap (free ${filesystem_free} MB, min. ${filesystem_needed} MB). Abort!" + exit 1 + elif ! change_swap $new_swap_size ; then + echo " ERROR: failed to change swap size. Abort!" + exit 1 + fi + + calc_nodemem || return 1 + fi + + elif [[ $NODEMEM -gt $free_to_use ]]; then + echo " ERROR: Requested node memory setting is larger than usable free memory: ${NODEMEM} MB > ${free_to_use} MB (free ${total_free} MB - buffer ${mem_buffer} MB). Abort!" + exit 1 + fi +} + +calc_nodemem export NODE_OPTIONS=--max-old-space-size=${NODEMEM} @@ -77,16 +125,27 @@ echo "Setting Node Options:" env | grep NODE if [[ $(uname -m) == armv6l ]]; then - echo " You are running on a hardware with less resources. Building - the webapp might fail. If so, try to install the stable - release installation instead." + echo " +----------------------------------------------------------- +| You are running a hardware with limited resources. | +| Building the Web App takes significantly more time. | +| In case it fails, check the documentation | +| to trouble shoot. | +----------------------------------------------------------- +" fi -# In rare cases you will need to update the npm dependencies -# This is the case when the file package.json changed if [[ $UPDATE_DEPENDENCIES == true ]]; then - npm install + npm install --prefer-offline --no-audit fi +build_output_folder="build" # Rebuild Web App -npm run build +rm -rf "${build_output_folder}.bak" +if [ -d "${build_output_folder}" ]; then + mv -f "${build_output_folder}" "${build_output_folder}.bak" +fi +if ! npm run build ; then + echo "ERROR: rebuild of Web App failed!" + exit 1 +fi From 03e619789baaad3e3b0876503605f1e295be0d9e Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:24:29 +0100 Subject: [PATCH 04/24] [New component] HiFiBerry Sound Card & OnOff SHIM (#2169) * Add components folder to installation and docs. First component is hifiberry sound card * Allow for all hifiberry boards * Disabling HDMI Audio * Finalize HiFiBerry doc * Refer to Pi Pinout for convenience * Enable ALSA config as an option as well * Allow script to be run multiple times * Add OnOff SHIM as component * Make script more robust based on PR comments * Update hifiberry soundcard options * Automate soundcard detection for asound.conf * use /boot/config based on debian version * Reorganize a few things * some bugfixes * Final fixes * Optimize case * Update docs * Uninstall option * fix: Remove option was not reachable * fix: enable sudo * fix: make I/O fail silently * feat: Introduce 1-line installation * feat: outsource onboard_sound as its own option * refactor: remove alsa config * fix: update case function * fix: adding some thens * fix: some iterations did not work * fix: adding another sudo * refactor: get_key_by_item_number for associated arrays * refactor: remove last bits of alsa * fix: final touches * fix: add missing removal only option * fix: outsource example_usage for 1-line install * fix: condition for 1-line installation * fix: another fix for if conditions * refactor: move 1-line installation down * gs * another fix * fix: write array check differently * refactor: final touches * feat: enable silent mode for check_existing_hifiberry * fix: reintroduce sudo check * fix: documentation * fix: final touches again * fix: remove is_sudo again * fix: Remove last sudo occurrences * fix: bullet proof * Make bash files executable * fix: Update documentation * fix: 1-line path does not worj * Update documentation/builders/components/soundcards/hifiberry.md Co-authored-by: s-martin * Update documentation/builders/components/soundcards/hifiberry.md Co-authored-by: s-martin * Update documentation/builders/components/soundcards/hifiberry.md Co-authored-by: s-martin * fix: Update regex for commented code * Adding OnOff Shim resource --- documentation/builders/README.md | 6 + .../builders/components/power/onoff-shim.md | 36 ++++++ .../components/soundcards/hifiberry.md | 49 ++++++++ documentation/developers/docker.md | 2 +- installation/components/setup_hifiberry.sh | 119 ++++++++++++++++++ installation/includes/02_helpers.sh | 70 +++++++++-- installation/options/onboard_sound.sh | 27 ++++ 7 files changed, 296 insertions(+), 13 deletions(-) create mode 100644 documentation/builders/components/power/onoff-shim.md create mode 100644 documentation/builders/components/soundcards/hifiberry.md create mode 100755 installation/components/setup_hifiberry.sh create mode 100755 installation/options/onboard_sound.sh diff --git a/documentation/builders/README.md b/documentation/builders/README.md index f9fef397e..8ea5e0648 100644 --- a/documentation/builders/README.md +++ b/documentation/builders/README.md @@ -14,6 +14,12 @@ * [Card Database](./card-database.md) * [Troubleshooting](./troubleshooting.md) +## Components +* [Power](./components/power/) + * [OnOff SHIM for safe power on/off](./components/power/onoff-shim.md) +* [Soundcards](./components/soundcards/) + * [HiFiBerry Boards](./components/soundcards/hifiberry.md) + ## Advanced * [Bluetooth (and audio buttons)](./bluetooth-audio-buttons.md) diff --git a/documentation/builders/components/power/onoff-shim.md b/documentation/builders/components/power/onoff-shim.md new file mode 100644 index 000000000..b83ea6140 --- /dev/null +++ b/documentation/builders/components/power/onoff-shim.md @@ -0,0 +1,36 @@ +# OnOff SHIM by Pimorino + +The OnOff SHIM from Pimorino allows you to savely start and shutdown your Raspberry Pi through a button. While you can switch of your Phoniebox via an RFID Card (through an RPC command), it is difficult to switch it on again without cutting the physical power supply. + +## Installation + +To install the software, open a terminal and type the following command to run the one-line-installer. A reboot will be required once the installation is finished. + +> [!NOTE] +> The installation will ask you a few questions. You can safely answer with the default response. + +``` +curl https://get.pimoroni.com/onoffshim | bash +``` + +* [Source](https://shop.pimoroni.com/products/onoff-shim?variant=41102600138) + +## How to manually wire OnOff SHIM + +The OnOff SHIM comes with a 12-PIN header which needs soldering. If you want to spare some GPIO pins for other purposes, you can individually wire the OnOff SHIM with the Raspberry Pi. Below you can find a table of Pins to be connected. + +| Board pin name | Board pin | Physical RPi pin | RPi pin name | +|----------------|-----------|------------------|--------------| +| 3.3V | 1 | 1, 17 | 3V3 power | +| 5V | 2 | 2 | 5V power | +| 5V | 4 | 4 | 5V power | +| GND | 6 | 6, 9, 20, 25 | Ground | +| GPLCLK0 | 7 | 7 | GPIO4 | +| GPIO17 | 11 | 11 | GPIO17 | + +* More information can be found here: https://pinout.xyz/pinout/onoff_shim + +## Assembly options + +![](https://cdn.review-images.pimoroni.com/upload-b6276a310ccfbeae93a2d13ec19ab83b-1617096824.jpg?width=640) + diff --git a/documentation/builders/components/soundcards/hifiberry.md b/documentation/builders/components/soundcards/hifiberry.md new file mode 100644 index 000000000..1f19fa96d --- /dev/null +++ b/documentation/builders/components/soundcards/hifiberry.md @@ -0,0 +1,49 @@ +# HiFiBerry + +The installation script works for the most common set of HiFiBerry boards but also other "DAC" related sound cards like `I2S PCM5102A DAC`. + +## Automatic setup + +Run the following command to install any HiFiBerry board. Make sure you reboot your device afterwards. + +```bash +cd ~/RPi-Jukebox-RFID/installation/components +./setup_hifiberry.sh +``` + +If you know you HifiBerry Board identifier, you can run the script as a 1-liner as well + +```bash +./setup_hifiberry.sh enable hifiberry-dac +``` + +If you like to disable your HiFiberry Sound card and enable onboard sound, run the following command + + +```bash +./setup_hifiberry.sh disable +``` + +## Additional information + +If you like to understand what's happening under the hood, feel free to check the [install script](../../../../installation/components/setup_hifiberry.sh). + +The setup is based on [HiFiBerry's instructions](https://www.hifiberry.com/docs/software/configuring-linux-3-18-x/). + +## How to manually wire your HiFiBerry board + +Most HiFiBerry boards come with 40-pin header that you can directly attach to your Pi. This idles many GPIO pins that may be required for other inputs to be attached (like GPIO buttons or RFID). You can also connect your HiFiBerry board separately. The following table show cases the pins required. + +* [Raspberry Pi Pinout](https://github.com/raspberrypi/documentation/blob/develop/documentation/asciidoc/computers/os/using-gpio.adoc) + +| Board pin name | Board pin | Physical RPi pin | RPi pin name | +|----------------|-----------|------------------|--------------| +| 3.3V | 1 | 1, 17 | 3V3 power | +| 5V | 2 | 2, 4 | 5V power | +| GND | 6 | 6, 9, 20, 25 | Ground | +| PCM_CLK | 12 | 12 | GPIO18 | +| PCM_FS | 36 | 36 | GPIO19 | +| PCM_DIN | 38 | 38 | GPIO20 | +| PCM_DOUT | 40 | 40 | GPIO21 | + +You can find more information about manually wiring [here](https://forum-raspberrypi.de/forum/thread/44967-kein-ton-ueber-hifiberry-miniamp-am-rpi-4/?postID=401305#post401305). diff --git a/documentation/developers/docker.md b/documentation/developers/docker.md index 3718373db..827178aca 100644 --- a/documentation/developers/docker.md +++ b/documentation/developers/docker.md @@ -15,7 +15,7 @@ need to adapt some of those commands to your needs. ## Prerequisites 1. Install required software: Docker, Compose and pulseaudio - * Check installations guide for [Mac](#mac), [Windows](#windows) or [Linux](#linux) + * Check installation guide for [Mac](#mac), [Windows](#windows) or [Linux](#linux) 2. Pull the Jukebox repository: diff --git a/installation/components/setup_hifiberry.sh b/installation/components/setup_hifiberry.sh new file mode 100755 index 000000000..dc2da8d6e --- /dev/null +++ b/installation/components/setup_hifiberry.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash + +# This script follows the official HiFiBerry documentation +# https://www.hifiberry.com/docs/software/configuring-linux-3-18-x/ + +source ../includes/02_helpers.sh + +script_name=$(basename "$0") +boot_config_path=$(get_boot_config_path) + +declare -A hifiberry_map=( + ["hifiberry-dac"]="DAC (HiFiBerry MiniAmp, I2S PCM5102A DAC)" + ["hifiberry-dacplus"]="HiFiBerry DAC+ Standard/Pro/Amp2" + ["hifiberry-dacplushd"]="HiFiBerry DAC2 HD" + ["hifiberry-dacplusadc"]="HiFiBerry DAC+ ADC" + ["hifiberry-dacplusadcpro"]="HiFiBerry DAC+ ADC Pro" + ["hifiberry-digi"]="HiFiBerry Digi+" + ["hifiberry-digi-pro"]="HiFiBerry Digi+ Pro" + ["hifiberry-amp"]="HiFiBerry Amp+ (not Amp2)" + ["hifiberry-amp3"]="HiFiBerry Amp3" +) + +example_usage() { + for key in "${!hifiberry_map[@]}"; do + description="${hifiberry_map[$key]}" + echo "$key) $description" + done + echo "Example usage: ./${script_name} enable hifiberry-dac" +} + +enable_hifiberry() { + echo "Enabling HiFiBerry board..." + grep -qxF "^dtoverlay=$1" "$boot_config_path" || echo "dtoverlay=$1" | sudo tee -a "$boot_config_path" > /dev/null + ./../options/onboard_sound.sh disable +} + +disable_hifiberry() { + echo "Removing existing HiFiBerry configuration..." + sudo sed -i '/^dtoverlay=hifiberry-/d' "$boot_config_path" + ./../options/onboard_sound.sh enable +} + +check_existing_hifiberry() { + existing_config=$(grep '^dtoverlay=hifiberry-' "$boot_config_path") + if [ ! -z "$existing_config" ]; then + if [ "$1" = "silent" ]; then + disable_hifiberry + return 0 + fi + + echo "Existing HiFiBerry configuration detected: $existing_config" + read -p "Do you want to proceed with a new configuration? This will remove the existing one. (Y/n): " choice + case $choice in + [nN][oO]|[nN]) + echo "Exiting without making changes."; + exit;; + *) + disable_hifiberry; + return 0;; + esac + fi +} + +# 1-line installation +if [ $# -ge 1 ]; then + if [[ "$1" != "enable" && "$1" != "disable" ]] || [[ "$1" == "enable" && -z "$2" ]]; then + echo "Error: Invalid arguments provided. +Usage: ./${script_name} +where can be 'enable' or 'disable'. + +The following board options exist:" + example_usage + exit 1 + fi + + if [ "$1" == "enable" ]; then + if [[ -v hifiberry_map["$2"] ]]; then + check_existing_hifiberry "silent" + enable_hifiberry "$2" + exit 1 + fi + + echo "'$2' is not a valid option. You can choose from:" + example_usage + exit 1 + fi + + disable_hifiberry + exit 1 +fi + +# Guided installation +board_count=${#hifiberry_map[@]} +counter=1 + +echo "Select your HiFiBerry board:" +for key in "${!hifiberry_map[@]}"; do + description="${hifiberry_map[$key]}" + echo "$counter) $description" + ((counter++)) +done +echo "0) Remove existing HiFiBerry configuration" + +read -p "Enter your choice (0-$board_count): " choice +case $choice in + [0]) + disable_hifiberry; + ;; + [1-$board_count]) + selected_board=$(get_key_by_item_number hifiberry_map "$choice") + check_existing_hifiberry + enable_hifiberry "$selected_board"; + ;; + *) + echo "Invalid selection. Exiting."; + exit 1;; +esac + +echo "Configuration complete. Please restart your device." diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index e9ca7640e..f2b60313b 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -13,6 +13,21 @@ show_slow_hardware_message() { fi } +# Get key by item number of associated array +get_key_by_item_number() { + local -n array="$1" + local item_number="$2" + local count=0 + + for key in "${!array[@]}"; do + ((count++)) + if [ "$count" -eq "$item_number" ]; then + echo "$key" + return + fi + done +} + # $1->start, $2->end calc_runtime_and_print() { runtime=$(($2-$1)) @@ -46,18 +61,49 @@ run_with_log_frame() { } 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 - arch="$(uname -m)" - fi - - echo $arch + local arch="" + if [ "$(uname -m)" = "armv7l" ]; then + arch="armv7" + elif [ "$(uname -m)" = "armv6l" ]; then + arch="armv6" + elif [ "$(uname -m)" = "aarch64" ]; then + arch="arm64" + else + arch="$(uname -m)" + fi + + echo $arch +} + +is_raspian() { + if [[ $( . /etc/os-release; printf '%s\n' "$ID"; ) == *"raspbian"* ]]; then + echo true + else + echo false + fi +} + +get_debian_version_number() { + source /etc/os-release + echo "$VERSION_ID" +} + +get_boot_config_path() { + if [ "$(is_raspian)" = true ]; then + local debian_version_number=$(get_debian_version_number) + + # Bullseye and lower + if [ "$debian_version_number" -le 11 ]; then + echo "/boot/config.txt" + # Bookworm and higher + elif [ "$debian_version_number" -ge 12 ]; then + echo "/boot/firmware/config.txt" + else + echo "unknown" + fi + else + echo "unknown" + fi } validate_url() { diff --git a/installation/options/onboard_sound.sh b/installation/options/onboard_sound.sh new file mode 100755 index 000000000..475a9161d --- /dev/null +++ b/installation/options/onboard_sound.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +source ../includes/02_helpers.sh +script_name=$(basename "$0") +boot_config_path=$(get_boot_config_path) + +if [ -z "$1" ] || { [ "$1" != "enable" ] && [ "$1" != "disable" ]; }; then + echo "Error: Invalid or no argument provided. +Usage: ./${script_name} + where can be 'enable' or 'disable'" + exit 1 +fi + +arg="$1" + +if [ "$arg" = "enable" ]; then + echo "Enabling Onboard Sound..." + sudo sed -i "s/^\(dtparam=\([^,]*,\)*\)audio=\(off\|false\|no\|0\)\(.*\)/\1audio=on\4/g" "$boot_config_path" + sudo sed -i '/^dtoverlay=vc4-fkms-v3d/{s/,audio=off//g;}' "$boot_config_path" + sudo sed -i '/^dtoverlay=vc4-kms-v3d/{s/,noaudio//g;}' "$boot_config_path" +elif [ "$arg" = "disable" ]; then + echo "Disabling Onboard Sound..." + sudo sed -i "s/^\(dtparam=\([^,]*,\)*\)audio=\(on\|true\|yes\|1\)\(.*\)/\1audio=off\4/g" "$boot_config_path" + sudo sed -i '/^dtoverlay=vc4-fkms-v3d/{s/,audio=off//g;s/$/,audio=off/g;}' "$boot_config_path" + sudo sed -i '/^dtoverlay=vc4-kms-v3d/{s/,noaudio//g;s/$/,noaudio/g;}' "$boot_config_path" +fi + +# TODO Test From 4f015dde6b348dadf0ebf5dae2de89ba9cf18046 Mon Sep 17 00:00:00 2001 From: s-martin Date: Tue, 16 Jan 2024 09:49:02 +0100 Subject: [PATCH 05/24] Create markdown docs from docstring in py files (#2181) * adding pydoc-markdown yml * add lazydocs * run lazydocs and pydoc-markdown parallel for testing * fix output path * add created docs to source control * fix the path to source code * change filter * revert last commit * use only pydoc-markdown * remove the test files from version control * rename script and add to pre-commit hook * change py file to test pre commit hook * modify py file again * test markdown formatting * updated docstring * use sphinx renderer * update markdown * convert links and formatting to markdown * make link to plugin docs * fix comment * fix wrong docstring * add more fixes to doc * update docstring-md * rename to README.md so github picks it up directly * fix formatting * improve docs * improve docs * fix docs * updte docstream md * fix formatting for md * fix formatting to md and check crossref * fix links * fix formatting for notes * fix links * revert wrong fix * fixed indentation * add generated docstring * Check for docstring in action * Try without request changes * try request changes * remove docstring check from action --- .githooks/pre-commit | 14 +- documentation/developers/README.md | 2 + documentation/developers/docstring/README.md | 5914 +++++++++++++++++ documentation/developers/known-issues.md | 2 + pydoc-markdown.yml | 13 + requirements.txt | 3 + run_docgeneration.sh | 15 + .../batt_mon_i2c_ads1015/__init__.py | 53 +- .../bluetooth_audio_buttons/__init__.py | 6 +- .../controls/common/evdev_listener.py | 6 +- .../components/gpio/gpioz/core/converter.py | 2 - .../gpio/gpioz/core/input_devices.py | 37 +- .../components/gpio/gpioz/core/mock.py | 3 +- .../gpio/gpioz/core/output_devices.py | 3 +- .../components/gpio/gpioz/plugin/__init__.py | 12 +- .../gpio/gpioz/plugin/connectivity.py | 36 +- .../components/hostif/linux/__init__.py | 3 +- src/jukebox/components/jingle/__init__.py | 11 +- src/jukebox/components/playermpd/__init__.py | 5 +- .../playermpd/playcontentcallback.py | 4 +- .../template_new_reader.py | 11 +- .../components/rfid/reader/__init__.py | 6 +- src/jukebox/components/rpc_command_alias.py | 2 +- src/jukebox/components/volume/__init__.py | 83 +- src/jukebox/jukebox/cfghandler.py | 5 +- src/jukebox/jukebox/playlistgenerator.py | 2 - src/jukebox/jukebox/plugs.py | 84 +- src/jukebox/jukebox/publishing/server.py | 100 +- src/jukebox/jukebox/rpc/server.py | 13 +- src/jukebox/jukebox/utils.py | 7 +- src/jukebox/misc/loggingext.py | 56 +- src/jukebox/run_configure_audio.py | 2 +- src/jukebox/run_jukebox.py | 4 +- src/jukebox/run_register_rfid_reader.py | 7 +- src/jukebox/run_rpc_tool.py | 2 +- 35 files changed, 6241 insertions(+), 287 deletions(-) create mode 100644 documentation/developers/docstring/README.md create mode 100644 pydoc-markdown.yml create mode 100755 run_docgeneration.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 95901bcda..b1b0c0348 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -7,7 +7,7 @@ # Checks # - flake8 on staged python files # Note: This only checks the modified files -# - docs build of if any python file or any doc file is staged +# - docs build of if any python file is staged # Note: This builds the entire documentation if a changed file goes into the documentation # # If there are problem with this script, commit may still be done with @@ -28,6 +28,18 @@ fi code=$(( flake8_code )) +doc_code=0 +if [[ -n $PY_FILES ]]; then + echo -e "\n**************************************************************" + echo -e "Modified Python source files. Generation markdown docs from docstring ... \n" + echo -e "**************************************************************\n" + ./run_docgeneration.sh -c + doc_code=$? + echo "pydoc_markdown return code: $doc_code" +fi + +code=$(( flake8_code + doc_code )) + if [[ code -gt 0 ]]; then echo -e "\n**************************************************************" echo -e "ERROR(s) during pre-commit checks. Aborting commit!" diff --git a/documentation/developers/README.md b/documentation/developers/README.md index cfa389713..f6ec65dc4 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -10,6 +10,8 @@ * [Jukebox Apps](./coreapps.md) * [Web App](./webapp.md) * [RFID Readers](./rfid) +* [Docstring API Docs (from py files)](./docstring/README.md) +* [Plugin Reference](./docstring/README.md#jukeboxplugs) * [Feature Status](./status.md) * [Known Issues](./known-issues.md) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md new file mode 100644 index 000000000..dde85b224 --- /dev/null +++ b/documentation/developers/docstring/README.md @@ -0,0 +1,5914 @@ +# None + +## Table of Contents + +* [run\_jukebox](#run_jukebox) +* [\_\_init\_\_](#__init__) +* [run\_register\_rfid\_reader](#run_register_rfid_reader) +* [run\_rpc\_tool](#run_rpc_tool) + * [get\_common\_beginning](#run_rpc_tool.get_common_beginning) + * [runcmd](#run_rpc_tool.runcmd) +* [run\_configure\_audio](#run_configure_audio) +* [run\_publicity\_sniffer](#run_publicity_sniffer) +* [misc](#misc) + * [recursive\_chmod](#misc.recursive_chmod) + * [flatten](#misc.flatten) + * [getattr\_hierarchical](#misc.getattr_hierarchical) +* [misc.inputminus](#misc.inputminus) + * [input\_int](#misc.inputminus.input_int) + * [input\_yesno](#misc.inputminus.input_yesno) +* [misc.loggingext](#misc.loggingext) + * [ColorFilter](#misc.loggingext.ColorFilter) + * [\_\_init\_\_](#misc.loggingext.ColorFilter.__init__) + * [PubStream](#misc.loggingext.PubStream) + * [PubStreamHandler](#misc.loggingext.PubStreamHandler) +* [misc.simplecolors](#misc.simplecolors) + * [Colors](#misc.simplecolors.Colors) + * [resolve](#misc.simplecolors.resolve) + * [print](#misc.simplecolors.print) +* [components](#components) +* [components.playermpd.playcontentcallback](#components.playermpd.playcontentcallback) + * [PlayContentCallbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks) + * [register](#components.playermpd.playcontentcallback.PlayContentCallbacks.register) + * [run\_callbacks](#components.playermpd.playcontentcallback.PlayContentCallbacks.run_callbacks) +* [components.playermpd](#components.playermpd) + * [PlayerMPD](#components.playermpd.PlayerMPD) + * [mpd\_retry\_with\_mutex](#components.playermpd.PlayerMPD.mpd_retry_with_mutex) + * [pause](#components.playermpd.PlayerMPD.pause) + * [next](#components.playermpd.PlayerMPD.next) + * [rewind](#components.playermpd.PlayerMPD.rewind) + * [replay](#components.playermpd.PlayerMPD.replay) + * [toggle](#components.playermpd.PlayerMPD.toggle) + * [replay\_if\_stopped](#components.playermpd.PlayerMPD.replay_if_stopped) + * [play\_card](#components.playermpd.PlayerMPD.play_card) + * [get\_single\_coverart](#components.playermpd.PlayerMPD.get_single_coverart) + * [get\_folder\_content](#components.playermpd.PlayerMPD.get_folder_content) + * [play\_folder](#components.playermpd.PlayerMPD.play_folder) + * [play\_album](#components.playermpd.PlayerMPD.play_album) + * [get\_volume](#components.playermpd.PlayerMPD.get_volume) + * [set\_volume](#components.playermpd.PlayerMPD.set_volume) + * [play\_card\_callbacks](#components.playermpd.play_card_callbacks) +* [components.playermpd.coverart\_cache\_manager](#components.playermpd.coverart_cache_manager) +* [components.rpc\_command\_alias](#components.rpc_command_alias) +* [components.synchronisation.rfidcards](#components.synchronisation.rfidcards) + * [SyncRfidcards](#components.synchronisation.rfidcards.SyncRfidcards) + * [sync\_change\_on\_rfid\_scan](#components.synchronisation.rfidcards.SyncRfidcards.sync_change_on_rfid_scan) + * [sync\_all](#components.synchronisation.rfidcards.SyncRfidcards.sync_all) + * [sync\_card\_database](#components.synchronisation.rfidcards.SyncRfidcards.sync_card_database) + * [sync\_folder](#components.synchronisation.rfidcards.SyncRfidcards.sync_folder) +* [components.synchronisation](#components.synchronisation) +* [components.synchronisation.syncutils](#components.synchronisation.syncutils) +* [components.volume](#components.volume) + * [PulseMonitor](#components.volume.PulseMonitor) + * [SoundCardConnectCallbacks](#components.volume.PulseMonitor.SoundCardConnectCallbacks) + * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) + * [toggle\_on\_connect](#components.volume.PulseMonitor.toggle_on_connect) + * [stop](#components.volume.PulseMonitor.stop) + * [run](#components.volume.PulseMonitor.run) + * [PulseVolumeControl](#components.volume.PulseVolumeControl) + * [OutputChangeCallbackHandler](#components.volume.PulseVolumeControl.OutputChangeCallbackHandler) + * [OutputVolumeCallbackHandler](#components.volume.PulseVolumeControl.OutputVolumeCallbackHandler) + * [toggle\_output](#components.volume.PulseVolumeControl.toggle_output) + * [get\_outputs](#components.volume.PulseVolumeControl.get_outputs) + * [publish\_volume](#components.volume.PulseVolumeControl.publish_volume) + * [publish\_outputs](#components.volume.PulseVolumeControl.publish_outputs) + * [set\_volume](#components.volume.PulseVolumeControl.set_volume) + * [get\_volume](#components.volume.PulseVolumeControl.get_volume) + * [change\_volume](#components.volume.PulseVolumeControl.change_volume) + * [get\_mute](#components.volume.PulseVolumeControl.get_mute) + * [mute](#components.volume.PulseVolumeControl.mute) + * [set\_output](#components.volume.PulseVolumeControl.set_output) + * [set\_soft\_max\_volume](#components.volume.PulseVolumeControl.set_soft_max_volume) + * [get\_soft\_max\_volume](#components.volume.PulseVolumeControl.get_soft_max_volume) + * [card\_list](#components.volume.PulseVolumeControl.card_list) +* [components.rfid](#components.rfid) +* [components.rfid.reader](#components.rfid.reader) + * [RfidCardDetectCallbacks](#components.rfid.reader.RfidCardDetectCallbacks) + * [register](#components.rfid.reader.RfidCardDetectCallbacks.register) + * [run\_callbacks](#components.rfid.reader.RfidCardDetectCallbacks.run_callbacks) + * [rfid\_card\_detect\_callbacks](#components.rfid.reader.rfid_card_detect_callbacks) + * [CardRemovalTimerClass](#components.rfid.reader.CardRemovalTimerClass) + * [\_\_init\_\_](#components.rfid.reader.CardRemovalTimerClass.__init__) +* [components.rfid.configure](#components.rfid.configure) + * [reader\_install\_dependencies](#components.rfid.configure.reader_install_dependencies) + * [reader\_load\_module](#components.rfid.configure.reader_load_module) + * [query\_user\_for\_reader](#components.rfid.configure.query_user_for_reader) + * [write\_config](#components.rfid.configure.write_config) +* [components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui](#components.rfid.hardware.fake_reader_gui.fake_reader_gui) +* [components.rfid.hardware.fake\_reader\_gui.description](#components.rfid.hardware.fake_reader_gui.description) +* [components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon) + * [create\_inputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_inputs) + * [set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.set_state) + * [que\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_state) + * [fix\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.fix_state) + * [pbox\_set\_state](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.pbox_set_state) + * [que\_set\_pbox](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.que_set_pbox) + * [create\_outputs](#components.rfid.hardware.fake_reader_gui.gpioz_gui_addon.create_outputs) +* [components.rfid.hardware.generic\_usb.description](#components.rfid.hardware.generic_usb.description) +* [components.rfid.hardware.generic\_usb.generic\_usb](#components.rfid.hardware.generic_usb.generic_usb) +* [components.rfid.hardware.rc522\_spi.description](#components.rfid.hardware.rc522_spi.description) +* [components.rfid.hardware.rc522\_spi.rc522\_spi](#components.rfid.hardware.rc522_spi.rc522_spi) +* [components.rfid.hardware.pn532\_i2c\_py532.description](#components.rfid.hardware.pn532_i2c_py532.description) +* [components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532](#components.rfid.hardware.pn532_i2c_py532.pn532_i2c_py532) +* [components.rfid.hardware.rdm6300\_serial.rdm6300\_serial](#components.rfid.hardware.rdm6300_serial.rdm6300_serial) + * [decode](#components.rfid.hardware.rdm6300_serial.rdm6300_serial.decode) +* [components.rfid.hardware.rdm6300\_serial.description](#components.rfid.hardware.rdm6300_serial.description) +* [components.rfid.hardware.template\_new\_reader.description](#components.rfid.hardware.template_new_reader.description) +* [components.rfid.hardware.template\_new\_reader.template\_new\_reader](#components.rfid.hardware.template_new_reader.template_new_reader) + * [query\_customization](#components.rfid.hardware.template_new_reader.template_new_reader.query_customization) + * [ReaderClass](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass) + * [\_\_init\_\_](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.__init__) + * [cleanup](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.cleanup) + * [stop](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.stop) + * [read\_card](#components.rfid.hardware.template_new_reader.template_new_reader.ReaderClass.read_card) +* [components.rfid.readerbase](#components.rfid.readerbase) + * [ReaderBaseClass](#components.rfid.readerbase.ReaderBaseClass) +* [components.rfid.cards](#components.rfid.cards) + * [list\_cards](#components.rfid.cards.list_cards) + * [delete\_card](#components.rfid.cards.delete_card) + * [register\_card](#components.rfid.cards.register_card) + * [register\_card\_custom](#components.rfid.cards.register_card_custom) + * [save\_card\_database](#components.rfid.cards.save_card_database) +* [components.rfid.cardutils](#components.rfid.cardutils) + * [decode\_card\_command](#components.rfid.cardutils.decode_card_command) + * [card\_command\_to\_str](#components.rfid.cardutils.card_command_to_str) + * [card\_to\_str](#components.rfid.cardutils.card_to_str) +* [components.publishing](#components.publishing) + * [republish](#components.publishing.republish) +* [components.player](#components.player) + * [MusicLibPath](#components.player.MusicLibPath) + * [get\_music\_library\_path](#components.player.get_music_library_path) +* [components.jingle](#components.jingle) + * [JingleFactory](#components.jingle.JingleFactory) + * [list](#components.jingle.JingleFactory.list) + * [play](#components.jingle.play) + * [play\_startup](#components.jingle.play_startup) + * [play\_shutdown](#components.jingle.play_shutdown) +* [components.jingle.alsawave](#components.jingle.alsawave) + * [AlsaWave](#components.jingle.alsawave.AlsaWave) + * [play](#components.jingle.alsawave.AlsaWave.play) + * [AlsaWaveBuilder](#components.jingle.alsawave.AlsaWaveBuilder) + * [\_\_init\_\_](#components.jingle.alsawave.AlsaWaveBuilder.__init__) +* [components.jingle.jinglemp3](#components.jingle.jinglemp3) + * [JingleMp3Play](#components.jingle.jinglemp3.JingleMp3Play) + * [play](#components.jingle.jinglemp3.JingleMp3Play.play) + * [JingleMp3PlayBuilder](#components.jingle.jinglemp3.JingleMp3PlayBuilder) + * [\_\_init\_\_](#components.jingle.jinglemp3.JingleMp3PlayBuilder.__init__) +* [components.hostif.linux](#components.hostif.linux) + * [shutdown](#components.hostif.linux.shutdown) + * [reboot](#components.hostif.linux.reboot) + * [jukebox\_is\_service](#components.hostif.linux.jukebox_is_service) + * [is\_any\_jukebox\_service\_active](#components.hostif.linux.is_any_jukebox_service_active) + * [restart\_service](#components.hostif.linux.restart_service) + * [get\_disk\_usage](#components.hostif.linux.get_disk_usage) + * [get\_cpu\_temperature](#components.hostif.linux.get_cpu_temperature) + * [get\_ip\_address](#components.hostif.linux.get_ip_address) + * [wlan\_disable\_power\_down](#components.hostif.linux.wlan_disable_power_down) + * [get\_autohotspot\_status](#components.hostif.linux.get_autohotspot_status) + * [stop\_autohotspot](#components.hostif.linux.stop_autohotspot) + * [start\_autohotspot](#components.hostif.linux.start_autohotspot) +* [components.misc](#components.misc) + * [rpc\_cmd\_help](#components.misc.rpc_cmd_help) + * [get\_all\_loaded\_packages](#components.misc.get_all_loaded_packages) + * [get\_all\_failed\_packages](#components.misc.get_all_failed_packages) + * [get\_start\_time](#components.misc.get_start_time) + * [get\_log](#components.misc.get_log) + * [get\_log\_debug](#components.misc.get_log_debug) + * [get\_log\_error](#components.misc.get_log_error) + * [get\_git\_state](#components.misc.get_git_state) + * [empty\_rpc\_call](#components.misc.empty_rpc_call) +* [components.controls](#components.controls) +* [components.controls.bluetooth\_audio\_buttons](#components.controls.bluetooth_audio_buttons) +* [components.controls.common.evdev\_listener](#components.controls.common.evdev_listener) + * [find\_device](#components.controls.common.evdev_listener.find_device) + * [EvDevKeyListener](#components.controls.common.evdev_listener.EvDevKeyListener) + * [\_\_init\_\_](#components.controls.common.evdev_listener.EvDevKeyListener.__init__) + * [run](#components.controls.common.evdev_listener.EvDevKeyListener.run) + * [start](#components.controls.common.evdev_listener.EvDevKeyListener.start) +* [components.battery\_monitor](#components.battery_monitor) +* [components.battery\_monitor.BatteryMonitorBase](#components.battery_monitor.BatteryMonitorBase) + * [pt1\_frac](#components.battery_monitor.BatteryMonitorBase.pt1_frac) + * [BattmonBase](#components.battery_monitor.BatteryMonitorBase.BattmonBase) +* [components.battery\_monitor.batt\_mon\_simulator](#components.battery_monitor.batt_mon_simulator) + * [battmon\_simulator](#components.battery_monitor.batt_mon_simulator.battmon_simulator) +* [components.battery\_monitor.batt\_mon\_i2c\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015) + * [battmon\_ads1015](#components.battery_monitor.batt_mon_i2c_ads1015.battmon_ads1015) +* [components.gpio.gpioz.plugin](#components.gpio.gpioz.plugin) + * [output\_devices](#components.gpio.gpioz.plugin.output_devices) + * [input\_devices](#components.gpio.gpioz.plugin.input_devices) + * [factory](#components.gpio.gpioz.plugin.factory) + * [IS\_ENABLED](#components.gpio.gpioz.plugin.IS_ENABLED) + * [IS\_MOCKED](#components.gpio.gpioz.plugin.IS_MOCKED) + * [CONFIG\_FILE](#components.gpio.gpioz.plugin.CONFIG_FILE) + * [ServiceIsRunningCallbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks) + * [register](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.register) + * [run\_callbacks](#components.gpio.gpioz.plugin.ServiceIsRunningCallbacks.run_callbacks) + * [service\_is\_running\_callbacks](#components.gpio.gpioz.plugin.service_is_running_callbacks) + * [build\_output\_device](#components.gpio.gpioz.plugin.build_output_device) + * [build\_input\_device](#components.gpio.gpioz.plugin.build_input_device) + * [get\_output](#components.gpio.gpioz.plugin.get_output) + * [on](#components.gpio.gpioz.plugin.on) + * [off](#components.gpio.gpioz.plugin.off) + * [set\_value](#components.gpio.gpioz.plugin.set_value) + * [flash](#components.gpio.gpioz.plugin.flash) +* [components.gpio.gpioz.plugin.connectivity](#components.gpio.gpioz.plugin.connectivity) + * [BUZZ\_TONE](#components.gpio.gpioz.plugin.connectivity.BUZZ_TONE) + * [register\_rfid\_callback](#components.gpio.gpioz.plugin.connectivity.register_rfid_callback) + * [register\_status\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_led_callback) + * [register\_status\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_buzzer_callback) + * [register\_status\_tonalbuzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_status_tonalbuzzer_callback) + * [register\_audio\_sink\_change\_callback](#components.gpio.gpioz.plugin.connectivity.register_audio_sink_change_callback) + * [register\_volume\_led\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_led_callback) + * [register\_volume\_buzzer\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_buzzer_callback) + * [register\_volume\_rgbled\_callback](#components.gpio.gpioz.plugin.connectivity.register_volume_rgbled_callback) +* [components.gpio.gpioz.core.converter](#components.gpio.gpioz.core.converter) + * [ColorProperty](#components.gpio.gpioz.core.converter.ColorProperty) + * [VolumeToRGB](#components.gpio.gpioz.core.converter.VolumeToRGB) + * [\_\_call\_\_](#components.gpio.gpioz.core.converter.VolumeToRGB.__call__) + * [luminize](#components.gpio.gpioz.core.converter.VolumeToRGB.luminize) +* [components.gpio.gpioz.core.mock](#components.gpio.gpioz.core.mock) + * [patch\_mock\_outputs\_with\_callback](#components.gpio.gpioz.core.mock.patch_mock_outputs_with_callback) +* [components.gpio.gpioz.core.input\_devices](#components.gpio.gpioz.core.input_devices) + * [NameMixin](#components.gpio.gpioz.core.input_devices.NameMixin) + * [set\_rpc\_actions](#components.gpio.gpioz.core.input_devices.NameMixin.set_rpc_actions) + * [EventProperty](#components.gpio.gpioz.core.input_devices.EventProperty) + * [ButtonBase](#components.gpio.gpioz.core.input_devices.ButtonBase) + * [value](#components.gpio.gpioz.core.input_devices.ButtonBase.value) + * [pin](#components.gpio.gpioz.core.input_devices.ButtonBase.pin) + * [pull\_up](#components.gpio.gpioz.core.input_devices.ButtonBase.pull_up) + * [close](#components.gpio.gpioz.core.input_devices.ButtonBase.close) + * [Button](#components.gpio.gpioz.core.input_devices.Button) + * [on\_press](#components.gpio.gpioz.core.input_devices.Button.on_press) + * [LongPressButton](#components.gpio.gpioz.core.input_devices.LongPressButton) + * [on\_press](#components.gpio.gpioz.core.input_devices.LongPressButton.on_press) + * [ShortLongPressButton](#components.gpio.gpioz.core.input_devices.ShortLongPressButton) + * [RotaryEncoder](#components.gpio.gpioz.core.input_devices.RotaryEncoder) + * [pin\_a](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_a) + * [pin\_b](#components.gpio.gpioz.core.input_devices.RotaryEncoder.pin_b) + * [on\_rotate\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_clockwise) + * [on\_rotate\_counter\_clockwise](#components.gpio.gpioz.core.input_devices.RotaryEncoder.on_rotate_counter_clockwise) + * [close](#components.gpio.gpioz.core.input_devices.RotaryEncoder.close) + * [TwinButton](#components.gpio.gpioz.core.input_devices.TwinButton) + * [StateVar](#components.gpio.gpioz.core.input_devices.TwinButton.StateVar) + * [close](#components.gpio.gpioz.core.input_devices.TwinButton.close) + * [value](#components.gpio.gpioz.core.input_devices.TwinButton.value) + * [is\_active](#components.gpio.gpioz.core.input_devices.TwinButton.is_active) +* [components.gpio.gpioz.core.output\_devices](#components.gpio.gpioz.core.output_devices) + * [LED](#components.gpio.gpioz.core.output_devices.LED) + * [flash](#components.gpio.gpioz.core.output_devices.LED.flash) + * [Buzzer](#components.gpio.gpioz.core.output_devices.Buzzer) + * [flash](#components.gpio.gpioz.core.output_devices.Buzzer.flash) + * [PWMLED](#components.gpio.gpioz.core.output_devices.PWMLED) + * [flash](#components.gpio.gpioz.core.output_devices.PWMLED.flash) + * [RGBLED](#components.gpio.gpioz.core.output_devices.RGBLED) + * [flash](#components.gpio.gpioz.core.output_devices.RGBLED.flash) + * [TonalBuzzer](#components.gpio.gpioz.core.output_devices.TonalBuzzer) + * [flash](#components.gpio.gpioz.core.output_devices.TonalBuzzer.flash) + * [melody](#components.gpio.gpioz.core.output_devices.TonalBuzzer.melody) +* [components.timers](#components.timers) +* [jukebox](#jukebox) +* [jukebox.callingback](#jukebox.callingback) + * [CallbackHandler](#jukebox.callingback.CallbackHandler) + * [register](#jukebox.callingback.CallbackHandler.register) + * [run\_callbacks](#jukebox.callingback.CallbackHandler.run_callbacks) + * [has\_callbacks](#jukebox.callingback.CallbackHandler.has_callbacks) +* [jukebox.version](#jukebox.version) + * [version](#jukebox.version.version) + * [version\_info](#jukebox.version.version_info) +* [jukebox.cfghandler](#jukebox.cfghandler) + * [ConfigHandler](#jukebox.cfghandler.ConfigHandler) + * [loaded\_from](#jukebox.cfghandler.ConfigHandler.loaded_from) + * [get](#jukebox.cfghandler.ConfigHandler.get) + * [setdefault](#jukebox.cfghandler.ConfigHandler.setdefault) + * [getn](#jukebox.cfghandler.ConfigHandler.getn) + * [setn](#jukebox.cfghandler.ConfigHandler.setn) + * [setndefault](#jukebox.cfghandler.ConfigHandler.setndefault) + * [config\_dict](#jukebox.cfghandler.ConfigHandler.config_dict) + * [is\_modified](#jukebox.cfghandler.ConfigHandler.is_modified) + * [clear\_modified](#jukebox.cfghandler.ConfigHandler.clear_modified) + * [save](#jukebox.cfghandler.ConfigHandler.save) + * [load](#jukebox.cfghandler.ConfigHandler.load) + * [get\_handler](#jukebox.cfghandler.get_handler) + * [load\_yaml](#jukebox.cfghandler.load_yaml) + * [write\_yaml](#jukebox.cfghandler.write_yaml) +* [jukebox.playlistgenerator](#jukebox.playlistgenerator) + * [TYPE\_DECODE](#jukebox.playlistgenerator.TYPE_DECODE) + * [PlaylistCollector](#jukebox.playlistgenerator.PlaylistCollector) + * [\_\_init\_\_](#jukebox.playlistgenerator.PlaylistCollector.__init__) + * [set\_exclusion\_endings](#jukebox.playlistgenerator.PlaylistCollector.set_exclusion_endings) + * [get\_directory\_content](#jukebox.playlistgenerator.PlaylistCollector.get_directory_content) + * [parse](#jukebox.playlistgenerator.PlaylistCollector.parse) +* [jukebox.NvManager](#jukebox.NvManager) +* [jukebox.publishing](#jukebox.publishing) + * [get\_publisher](#jukebox.publishing.get_publisher) +* [jukebox.publishing.subscriber](#jukebox.publishing.subscriber) +* [jukebox.publishing.server](#jukebox.publishing.server) + * [PublishServer](#jukebox.publishing.server.PublishServer) + * [run](#jukebox.publishing.server.PublishServer.run) + * [handle\_message](#jukebox.publishing.server.PublishServer.handle_message) + * [handle\_subscription](#jukebox.publishing.server.PublishServer.handle_subscription) + * [Publisher](#jukebox.publishing.server.Publisher) + * [\_\_init\_\_](#jukebox.publishing.server.Publisher.__init__) + * [send](#jukebox.publishing.server.Publisher.send) + * [revoke](#jukebox.publishing.server.Publisher.revoke) + * [resend](#jukebox.publishing.server.Publisher.resend) + * [close\_server](#jukebox.publishing.server.Publisher.close_server) +* [jukebox.daemon](#jukebox.daemon) + * [log\_active\_threads](#jukebox.daemon.log_active_threads) + * [JukeBox](#jukebox.daemon.JukeBox) + * [signal\_handler](#jukebox.daemon.JukeBox.signal_handler) +* [jukebox.plugs](#jukebox.plugs) + * [PluginPackageClass](#jukebox.plugs.PluginPackageClass) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [register](#jukebox.plugs.register) + * [tag](#jukebox.plugs.tag) + * [initialize](#jukebox.plugs.initialize) + * [finalize](#jukebox.plugs.finalize) + * [atexit](#jukebox.plugs.atexit) + * [load](#jukebox.plugs.load) + * [load\_all\_named](#jukebox.plugs.load_all_named) + * [load\_all\_unnamed](#jukebox.plugs.load_all_unnamed) + * [load\_all\_finalize](#jukebox.plugs.load_all_finalize) + * [close\_down](#jukebox.plugs.close_down) + * [call](#jukebox.plugs.call) + * [call\_ignore\_errors](#jukebox.plugs.call_ignore_errors) + * [exists](#jukebox.plugs.exists) + * [get](#jukebox.plugs.get) + * [loaded\_as](#jukebox.plugs.loaded_as) + * [delete](#jukebox.plugs.delete) + * [dump\_plugins](#jukebox.plugs.dump_plugins) + * [summarize](#jukebox.plugs.summarize) + * [generate\_help\_rst](#jukebox.plugs.generate_help_rst) + * [get\_all\_loaded\_packages](#jukebox.plugs.get_all_loaded_packages) + * [get\_all\_failed\_packages](#jukebox.plugs.get_all_failed_packages) +* [jukebox.speaking\_text](#jukebox.speaking_text) +* [jukebox.multitimer](#jukebox.multitimer) + * [MultiTimer](#jukebox.multitimer.MultiTimer) + * [cancel](#jukebox.multitimer.MultiTimer.cancel) + * [GenericTimerClass](#jukebox.multitimer.GenericTimerClass) + * [\_\_init\_\_](#jukebox.multitimer.GenericTimerClass.__init__) + * [start](#jukebox.multitimer.GenericTimerClass.start) + * [cancel](#jukebox.multitimer.GenericTimerClass.cancel) + * [toggle](#jukebox.multitimer.GenericTimerClass.toggle) + * [trigger](#jukebox.multitimer.GenericTimerClass.trigger) + * [is\_alive](#jukebox.multitimer.GenericTimerClass.is_alive) + * [get\_timeout](#jukebox.multitimer.GenericTimerClass.get_timeout) + * [set\_timeout](#jukebox.multitimer.GenericTimerClass.set_timeout) + * [publish](#jukebox.multitimer.GenericTimerClass.publish) + * [get\_state](#jukebox.multitimer.GenericTimerClass.get_state) + * [GenericEndlessTimerClass](#jukebox.multitimer.GenericEndlessTimerClass) + * [GenericMultiTimerClass](#jukebox.multitimer.GenericMultiTimerClass) + * [\_\_init\_\_](#jukebox.multitimer.GenericMultiTimerClass.__init__) + * [start](#jukebox.multitimer.GenericMultiTimerClass.start) +* [jukebox.utils](#jukebox.utils) + * [decode\_rpc\_call](#jukebox.utils.decode_rpc_call) + * [decode\_rpc\_command](#jukebox.utils.decode_rpc_command) + * [decode\_and\_call\_rpc\_command](#jukebox.utils.decode_and_call_rpc_command) + * [bind\_rpc\_command](#jukebox.utils.bind_rpc_command) + * [rpc\_call\_to\_str](#jukebox.utils.rpc_call_to_str) + * [generate\_cmd\_alias\_rst](#jukebox.utils.generate_cmd_alias_rst) + * [generate\_cmd\_alias\_reference](#jukebox.utils.generate_cmd_alias_reference) + * [get\_git\_state](#jukebox.utils.get_git_state) +* [jukebox.rpc](#jukebox.rpc) +* [jukebox.rpc.client](#jukebox.rpc.client) +* [jukebox.rpc.server](#jukebox.rpc.server) + * [RpcServer](#jukebox.rpc.server.RpcServer) + * [\_\_init\_\_](#jukebox.rpc.server.RpcServer.__init__) + * [run](#jukebox.rpc.server.RpcServer.run) + + + +# run\_jukebox + +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](../../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). + + + + +# \_\_init\_\_ + + + +# run\_register\_rfid\_reader + +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 + + + + +# run\_rpc\_tool + +Command Line Interface to the Jukebox RPC Server + +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. + +.. todo: + - kwargs support + + + + +#### get\_common\_beginning + +```python +def get_common_beginning(strings) +``` + +Return the strings that are common to the beginning of each string in the strings list. + + + + +#### runcmd + +```python +def runcmd(cmd) +``` + +Just run a command. + +Right now duplicates more or less main() +:todo remove duplication of code + + + + +# run\_configure\_audio + +Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. + +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#audio-configuration). + + + + +# run\_publicity\_sniffer + +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. + + + + +# misc + + + +#### recursive\_chmod + +```python +def recursive_chmod(path, mode_files, mode_dirs) +``` + +Recursively change folder and file permissions + +mode_files/mode dirs can be given in octal notation e.g. 0o777 +flags from the stats module. + +Reference: https://docs.python.org/3/library/os.html#os.chmod + + + + +#### flatten + +```python +def flatten(iterable) +``` + +Flatten all levels of hierarchy in nested iterables + + + + +#### getattr\_hierarchical + +```python +def getattr_hierarchical(obj: Any, name: str) -> Any +``` + +Like the builtin getattr, but descends though the hierarchy levels + + + + +# misc.inputminus + +Zero 3rd-party dependency module for user prompting + +Yes, there are modules out there to do the same and they have more features. +However, this is low-complexity and has zero dependencies + + + + +#### input\_int + +```python +def input_int(prompt, + blank=None, + min=None, + max=None, + prompt_color=None, + prompt_hint=False) -> int +``` + +Request an integer input from user + +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `min`: Minimum valid integer value (None disables this check) +- `max`: Maximum valid integer value (None disables this check) +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [min...max, default=xx] to end of prompt + +**Returns**: + +integer value read from user input + + + +#### input\_yesno + +```python +def input_yesno(prompt, + blank=None, + prompt_color=None, + prompt_hint=False) -> bool +``` + +Request a yes / no choice from user + +Accepts multiple input for true/false and is case insensitive + +**Arguments**: + +- `prompt`: The prompt to display +- `blank`: Value to return when user just hits enter. Leave at None, if blank is invalid +- `prompt_color`: Color of the prompt. Color will be reset at end of prompt +- `prompt_hint`: Append a 'hint' with [y/n] to end of prompt. Default choice will be capitalized + +**Returns**: + +boolean value read from user input + + + +# misc.loggingext + +## Logger + +We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. + +The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy +level below 'jb'. It will inherit settings from it's parent logger unless otherwise configured in the yaml file. +Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be +created on the spot. + +Example: How to get logger and log away at your heart's content: + + >>> import logging + >>> logger = logging.getLogger('jb.awesome_module') + >>> logger.info('Started general awesomeness aura') + +Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module: + + loggers: + jb: + level: WARNING + handlers: [console, debug_file_handler, error_file_handler] + propagate: no + jb.awesome_module: + level: DEBUG + + +> [!NOTE] +> The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes +> sense). +> There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output + + + + +## ColorFilter Objects + +```python +class ColorFilter(logging.Filter) +``` + +This filter adds colors to the logger + +It adds all colors from simplecolors by using the color name as new keyword, +i.e. use %(colorname)c or {colorname} in the formatter string + +It also adds the keyword {levelnameColored} which is an auto-colored drop-in replacement +for the levelname depending on severity. + +Don't forget to {reset} the color settings at the end of the string. + + + + +#### \_\_init\_\_ + +```python +def __init__(enable=True, color_levelname=True) +``` + +**Arguments**: + +- `enable`: Enable the coloring +- `color_levelname`: Enable auto-coloring when using the levelname keyword + + + +## PubStream Objects + +```python +class PubStream() +``` + +Stream handler wrapper around the publisher for logging.StreamHandler + +Allows logging to send all log information (based on logging configuration) +to the Publisher. + +> [!CAUTION] +> This can lead to recursions! +> Recursions come up when +> * Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, +> which causes a send, ..... +> * Publisher initialization emits logs, which need a Publisher instance to send logs + +> [!IMPORTANT] +> To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the +> functions in the send-function stack! + + + + +## PubStreamHandler Objects + +```python +class PubStreamHandler(logging.StreamHandler) +``` + +Wrapper for logging.StreamHandler with stream = PubStream + +This serves one purpose: In logger.yaml custom handlers +can be configured (which are automatically instantiated). +Using this Handler, we can output to PubStream whithout +support code to instantiate PubStream keeping this file generic + + + + +# misc.simplecolors + +Zero 3rd-party dependency module to add colors to unix terminal output + +Yes, there are modules out there to do the same and they have more features. +However, this is low-complexity and has zero dependencies + + + + +## Colors Objects + +```python +class Colors() +``` + +Container class for all the colors as constants + + + + +#### resolve + +```python +def resolve(color_name: str) +``` + +Resolve a color name into the respective color constant + +**Arguments**: + +- `color_name`: Name of the color + +**Returns**: + +color constant + + + +#### print + +```python +def print(color: Colors, + *values, + sep=' ', + end='\n', + file=sys.stdout, + flush=False) +``` + +Drop-in replacement for print with color choice and auto color reset for convenience + +Use just as a regular print function, but with first parameter as color + + + + +# components + + + +# components.playermpd.playcontentcallback + + + +## PlayContentCallbacks Objects + +```python +class PlayContentCallbacks(Generic[STATE], CallbackHandler) +``` + +Callbacks are executed in various play functions + + + + +#### register + +```python +def register(func: Callable[[str, STATE], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(folder: str, state: STATE) + :noindex: + +**Arguments**: + +- `folder`: relativ path to folder to play +- `state`: indicator of the state inside the calling + + + +#### run\_callbacks + +```python +def run_callbacks(folder: str, state: STATE) +``` + + + + + +# components.playermpd + +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 + + + + +## PlayerMPD Objects + +```python +class PlayerMPD() +``` + +Interface to MPD Music Player Daemon + + + + +#### mpd\_retry\_with\_mutex + +```python +def mpd_retry_with_mutex(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 + + + + +#### pause + +```python +@plugs.tag +def pause(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 + + + + +#### next + +```python +@plugs.tag +def next() +``` + +Play next track in current playlist + + + + +#### rewind + +```python +@plugs.tag +def rewind() +``` + +Re-start current playlist from first track + +Note: Will not re-read folder config, but leave settings untouched + + + + +#### replay + +```python +@plugs.tag +def replay() +``` + +Re-start playing the last-played folder + +Will reset settings to folder config + + + + +#### toggle + +```python +@plugs.tag +def toggle() +``` + +Toggle pause state, i.e. do a pause / resume depending on current state + + + + +#### replay\_if\_stopped + +```python +@plugs.tag +def replay_if_stopped() +``` + +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 + + + + +#### play\_card + +```python +@plugs.tag +def play_card(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. + +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively + + + +#### get\_single\_coverart + +```python +@plugs.tag +def get_single_coverart(song_url) +``` + +Saves the album art image to a cache and returns the filename. + + + + +#### get\_folder\_content + +```python +@plugs.tag +def get_folder_content(folder: str) +``` + +Get the folder content as content list with meta-information. Depth is always 1. + +Call repeatedly to descend in hierarchy + +**Arguments**: + +- `folder`: Folder path relative to music library path + + + +#### play\_folder + +```python +@plugs.tag +def play_folder(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. + +**Arguments**: + +- `folder`: Folder path relative to music library path +- `recursive`: Add folder recursively + + + +#### play\_album + +```python +@plugs.tag +def play_album(albumartist: str, album: str) +``` + +Playback a album found in MPD database. + +All album songs are added to the playlist +The playlist is cleared first. + +**Arguments**: + +- `albumartist`: Artist of the Album provided by MPD database +- `album`: Album name provided by MPD database + + + +#### get\_volume + +```python +def get_volume() +``` + +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 + + + + +#### set\_volume + +```python +def set_volume(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 + + + + +#### play\_card\_callbacks + +Callback handler instance for play_card events. + +- is executed when play_card function is called +States: +- See :class:`PlayCardState` +See :class:`PlayContentCallbacks` + + + + +# components.playermpd.coverart\_cache\_manager + + + +# components.rpc\_command\_alias + +This file provides definitions for RPC command aliases + +See [RPC Commands](../../builders/rpc-commands.md) + + + + +# components.synchronisation.rfidcards + +Handles the synchronisation of RFID cards (audiofolder and card database entries). + +sync-all -> all card entries and audiofolders are synced from remote including deletions +sync-on-scan -> only the entry and audiofolder for the cardId will be synced from remote. + Deletions are only performed on files and subfolder inside the audiofolder. + A deletion of the audiofolder itself on remote side will not be propagated. + +card database: +On synchronisation the remote file will not be synced with the original cards database, but rather a local copy. +If a full sync is performed, the state is written back to the original file. +If a single card sync is performed, only the state of the specific cardId is updated in the original file. +This is done to allow to play audio offline. +Otherwise we would also update other cardIds where the audiofolders have not been synced yet. +The local copy is kept to reduce unnecessary syncing. + + + + +## SyncRfidcards Objects + +```python +class SyncRfidcards() +``` + +Control class for sync RFID cards functionality + + + + +#### sync\_change\_on\_rfid\_scan + +```python +@plugs.tag +def sync_change_on_rfid_scan(option: str = 'toggle') -> None +``` + +Change activation of 'on_rfid_scan_enabled' + +**Arguments**: + +- `option`: Must be one of 'enable', 'disable', 'toggle' + + + +#### sync\_all + +```python +@plugs.tag +def sync_all() -> bool +``` + +Sync all audiofolder and cardids from the remote server. + +Removes local entries not existing at the remote server. + + + + +#### sync\_card\_database + +```python +@plugs.tag +def sync_card_database(card_id: str) -> bool +``` + +Sync the card database from the remote server, if existing. + +If card_id is provided only this entry is updated. + +**Arguments**: + +- `card_id`: The cardid to update + + + +#### sync\_folder + +```python +@plugs.tag +def sync_folder(folder: str) -> bool +``` + +Sync the folder from the remote server, if existing + +**Arguments**: + +- `folder`: Folder path relative to music library path + + + +# components.synchronisation + + + +# components.synchronisation.syncutils + + + +# components.volume + +PulseAudio Volume Control Plugin Package + +## Features + +* Volume Control +* Two outputs +* Watcher thread on volume / output change + +## Publishes + +* volume.level +* volume.sink + +## PulseAudio References + + + +Check fallback device (on device de-connect): + + $ pacmd list-sinks | grep -e 'name:' -e 'index' + + +## Integration + +Pulse Audio runs as a user process. Processes who want to communicate / stream to it +must also run as a user process. + +This means must also run as user process, as described in +[Music Player Daemon](../../builders/system.md#music-player-daemon-mpd). + +## Misc + +PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module +with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration +in ``/usr/pulse/default.pa``. So, we don't need to worry about it. +If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs +from the Jukebox. Remove it from the configuration! + + ### Use hot-plugged devices like Bluetooth or USB automatically (LP: `1702794`) + ### not available on PI? + .ifexists module-switch-on-connect.so + load-module module-switch-on-connect + .endif + +## Why PulseAudio? + +The audio configuration of the system is one of those topics, +which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and +makes our life easier. Besides, it is only option to support Bluetooth at the moment. + +## Callbacks: + +The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): + +1. :func:`add_on_connect_callback` +2. :func:`add_on_output_change_callbacks` +3. :func:`add_on_volume_change_callback` + + + + +## PulseMonitor Objects + +```python +class PulseMonitor(threading.Thread) +``` + +A thread for monitoring and interacting with the Pulse Lib via pulsectrl + +Whenever we want to access pulsectl, we need to exit the event listen loop. +This is handled by the context manager. It stops the event loop and returns +the pulsectl instance to be used (it does no return the monitor thread itself!) + +The context manager also locks the module to ensure proper thread sequencing, +as only a single thread may work with pulsectl at any time. Currently, an RLock is +used, even if it may not be necessary + + + + +## SoundCardConnectCallbacks Objects + +```python +class SoundCardConnectCallbacks(CallbackHandler) +``` + +Callbacks are executed when + +* new sound card gets connected + + + + +#### register + +```python +def register(func: Callable[[str, str], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(card_driver: str, device_name: str) + :noindex: + +**Arguments**: + +- `card_driver`: The PulseAudio card driver module, +e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` +- `device_name`: The sound card device name as reported +in device properties + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + + + + + +#### toggle\_on\_connect + +```python +@property +def toggle_on_connect() +``` + +Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this + +property changes the behavior. + +> [!NOTE] +> A new card is always assumed to be the secondary device from the audio configuration. +> At the moment there is no check it actually is the configured device. This means any new +> device connection will initiate the toggle. This, however, is no real issue as the RPi's audio +> system will be relatively stable once setup + + + + +#### toggle\_on\_connect + +```python +@toggle_on_connect.setter +def toggle_on_connect(state=True) +``` + +Toggle Doc 2 + + + + +#### stop + +```python +def stop() +``` + +Stop the pulse monitor thread + + + + +#### run + +```python +def run() -> None +``` + +Starts the pulse monitor thread + + + + +## PulseVolumeControl Objects + +```python +class PulseVolumeControl() +``` + +Volume control manager for PulseAudio + +When accessing the pulse library, it needs to be put into a special +state. Which is ensured by the context manager + + with pulse_monitor as pulse ... + + +All private functions starting with `_function_name` assume that this is ensured by +the calling function. All user functions acquire proper context! + + + + +## OutputChangeCallbackHandler Objects + +```python +class OutputChangeCallbackHandler(CallbackHandler) +``` + +Callbacks are executed when + +* audio sink is changed + + + + +#### register + +```python +def register(func: Callable[[str, str, int, int], None]) +``` + +Add a new callback function :attr:`func`. + +Parameters always give the valid audio sink. That means, if an error +occurred, all parameters are valid. + +Callback signature is + +.. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) + :noindex: + +**Arguments**: + +- `sink_name`: PulseAudio's sink name +- `alias`: The alias for :attr:`sink_name` +- `sink_index`: The index of the sink in the configuration list +- `error_state`: 1 if there was an attempt to change the output +but an error occurred. Above parameters always give the now valid sink! +If a sink change is successful, it is 0. + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + + + + + +## OutputVolumeCallbackHandler Objects + +```python +class OutputVolumeCallbackHandler(CallbackHandler) +``` + +Callbacks are executed when + +* audio volume level is changed + + + + +#### register + +```python +def register(func: Callable[[int, bool, bool], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(volume: int, is_min: bool, is_max: bool) + :noindex: + +**Arguments**: + +- `volume`: Volume level +- `is_min`: 1, if volume level is minimum, else 0 +- `is_max`: 1, if volume level is maximum, else 0 + + + +#### run\_callbacks + +```python +def run_callbacks(sink_name, alias, sink_index, error_state) +``` + + + + + +#### toggle\_output + +```python +@plugin.tag +def toggle_output() +``` + +Toggle the audio output sink + + + + +#### get\_outputs + +```python +@plugin.tag +def get_outputs() +``` + +Get current output and list of outputs + + + + +#### publish\_volume + +```python +@plugin.tag +def publish_volume() +``` + +Publish (volume, mute) + + + + +#### publish\_outputs + +```python +@plugin.tag +def publish_outputs() +``` + +Publish current output and list of outputs + + + + +#### set\_volume + +```python +@plugin.tag +def set_volume(volume: int) +``` + +Set the volume (0-100) for the currently active output + + + + +#### get\_volume + +```python +@plugin.tag +def get_volume() +``` + +Get the volume + + + + +#### change\_volume + +```python +@plugin.tag +def change_volume(step: int) +``` + +Increase/decrease the volume by step for the currently active output + + + + +#### get\_mute + +```python +@plugin.tag +def get_mute() +``` + +Return mute status for the currently active output + + + + +#### mute + +```python +@plugin.tag +def mute(mute=True) +``` + +Set mute status for the currently active output + + + + +#### set\_output + +```python +@plugin.tag +def set_output(sink_index: int) +``` + +Set the active output (sink_index = 0: primary, 1: secondary) + + + + +#### set\_soft\_max\_volume + +```python +@plugin.tag +def set_soft_max_volume(max_volume: int) +``` + +Limit the maximum volume to max_volume for the currently active output + + + + +#### get\_soft\_max\_volume + +```python +@plugin.tag +def get_soft_max_volume() +``` + +Return the maximum volume limit for the currently active output + + + + +#### card\_list + +```python +def card_list() -> List[pulsectl.PulseCardInfo] +``` + +Return the list of present sound card + + + + +# components.rfid + + + +# components.rfid.reader + + + +## RfidCardDetectCallbacks Objects + +```python +class RfidCardDetectCallbacks(CallbackHandler) +``` + +Callbacks are executed if rfid card is detected + + + + +#### register + +```python +def register(func: Callable[[str, RfidCardDetectState], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(card_id: str, state: int) + :noindex: + +**Arguments**: + +- `card_id`: Card ID +- `state`: See `RfidCardDetectState` + + + +#### run\_callbacks + +```python +def run_callbacks(card_id: str, state: RfidCardDetectState) +``` + + + + + +#### rfid\_card\_detect\_callbacks + +Callback handler instance for rfid_card_detect_callbacks events. + +See [`RfidCardDetectCallbacks`](#components.rfid.reader.RfidCardDetectCallbacks) + + + + +## CardRemovalTimerClass Objects + +```python +class CardRemovalTimerClass(threading.Thread) +``` + +A timer watchdog thread that calls timeout_action on time-out + + + + +#### \_\_init\_\_ + +```python +def __init__(on_timeout_callback, logger: logging.Logger = None) +``` + +**Arguments**: + +- `on_timeout_callback`: The function to execute on time-out + + + +# components.rfid.configure + + + +#### reader\_install\_dependencies + +```python +def reader_install_dependencies(reader_path: str, + dependency_install: str) -> None +``` + +Install dependencies for the selected reader module + +**Arguments**: + +- `reader_path`: Path to the reader module +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + + + +#### reader\_load\_module + +```python +def reader_load_module(reader_name) +``` + +Load the module for the reader_name + +A ModuleNotFoundError is unrecoverable, but we at least want to give some hint how to resolve that to the user +All other errors will NOT be handled. Modules that do not load due to compile errors have other problems + +**Arguments**: + +- `reader_name`: Name of the reader to load the module for + +**Returns**: + +module + + + +#### query\_user\_for\_reader + +```python +def query_user_for_reader(dependency_install='query') -> dict +``` + +Ask the user to select a RFID reader and prompt for the reader's configuration + +This function performs the following steps, to find and present all available readers to the user + +- search for available reader subpackages +- dynamically load the description module for each reader subpackage +- queries user for selection +- if no_dep_install=False, install dependencies as given by requirements.txt and execute setup.inc.sh of subpackage +- dynamically load the actual reader module from the reader subpackage +- if selected reader has customization options query user for that now +- return configuration + +There are checks to make sure we have the right reader modules and they are what we expect. +The are as few requirements towards the reader module as possible and everything else is optional +(see reader_template for these requirements) +However, there is no error handling w.r.t to user input and reader's query_config. Firstly, in this script +we cannot gracefully handle an exception that occurs on reader level, and secondly the exception will simply +exit the script w/o writing the config to file. No harm done. + +This script expects to reside in the directory with all the reader subpackages, i.e it is part of the rfid-reader package. +Otherwise you'll need to adjust sys.path + +**Arguments**: + +- `dependency_install`: how to handle installing of dependencies +'query': query user (default) +'auto': automatically +'no': don't install dependencies + +**Returns**: + +`dict as {section: {parameter: value}}`: nested dict with entire configuration that can be read into ConfigParser + + + +#### write\_config + +```python +def write_config(config_file: str, + config_dict: dict, + force_overwrite=False) -> None +``` + +Write configuration to config_file + +**Arguments**: + +- `config_file`: relative or absolute path to config file +- `config_dict`: nested dict with configuration parameters for ConfigParser consumption +- `force_overwrite`: overwrite existing configuration file without asking + + + +# components.rfid.hardware.fake\_reader\_gui.fake\_reader\_gui + + + +# components.rfid.hardware.fake\_reader\_gui.description + + + +# components.rfid.hardware.fake\_reader\_gui.gpioz\_gui\_addon + +Add GPIO input devices and output devices to the RFID Mock Reader GUI + + + + +#### create\_inputs + +```python +def create_inputs(frame, default_btn_width, default_padx, default_pady) +``` + +Add all input devies to the GUI + +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the buttons to + +**Returns**: + +List of all added GUI buttons + + + +#### set\_state + +```python +def set_state(value, box_state_var) +``` + +Change the value of a checkbox state variable + + + + +#### que\_set\_state + +```python +def que_set_state(value, box_state_var) +``` + +Queue the action to change a checkbox state variable to the TK GUI main thread + + + + +#### fix\_state + +```python +def fix_state(box_state_var) +``` + +Prevent a checkbox state variable to change on checkbox mouse press + + + + +#### pbox\_set\_state + +```python +def pbox_set_state(value, pbox_state_var, label_var) +``` + +Update progress bar state and related state label + + + + +#### que\_set\_pbox + +```python +def que_set_pbox(value, pbox_state_var, label_var) +``` + +Queue the action to change the progress bar state to the TK GUI main thread + + + + +#### create\_outputs + +```python +def create_outputs(frame, default_btn_width, default_padx, default_pady) +``` + +Add all output devices to the GUI + +**Arguments**: + +- `frame`: The TK frame (e.g. LabelFrame) in the main GUI to add the representations to + +**Returns**: + +List of all added GUI objects + + + +# components.rfid.hardware.generic\_usb.description + + + +# components.rfid.hardware.generic\_usb.generic\_usb + + + +# components.rfid.hardware.rc522\_spi.description + + + +# components.rfid.hardware.rc522\_spi.rc522\_spi + + + +# components.rfid.hardware.pn532\_i2c\_py532.description + + + +# components.rfid.hardware.pn532\_i2c\_py532.pn532\_i2c\_py532 + + + +# components.rfid.hardware.rdm6300\_serial.rdm6300\_serial + + + +#### decode + +```python +def decode(raw_card_id: bytearray, number_format: int) -> str +``` + +Decode the RDM6300 data format into actual card ID + + + + +# components.rfid.hardware.rdm6300\_serial.description + + + +# components.rfid.hardware.template\_new\_reader.description + +Provide a short title for this reader. + +This is what that user will see when asked for selecting his RFID reader +So, be precise but readable. Precise means 40 characters or less + + + + +# components.rfid.hardware.template\_new\_reader.template\_new\_reader + + + +#### query\_customization + +```python +def query_customization() -> dict +``` + +Query the user for reader parameter customization + +This function will be called during the configuration/setup phase when the user selects this reader module. +It must return all configuration parameters that are necessary to later use the Reader class. +You can ask the user for selections and choices. And/or provide default values. +If your reader requires absolutely no configuration return {} + + + + +## ReaderClass Objects + +```python +class ReaderClass(ReaderBaseClass) +``` + +The actual reader class that is used to read RFID cards. + +It will be instantiated once and then read_card() is called in an endless loop. + +It will be used in a manner + with Reader(reader_cfg_key) as reader: + for card_id in reader: + ... +which ensures proper resource de-allocation. For this to work derive this class from ReaderBaseClass. +All the required interfaces are implemented there. + +Put your code into these functions (see below for more information) + - `__init__` + - read_card + - cleanup + - stop + + + + +#### \_\_init\_\_ + +```python +def __init__(reader_cfg_key) +``` + +In the constructor, you will get the `reader_cfg_key` with which you can access the configuration data + +As you are dealing directly with potentially user-manipulated config information, it is +advisable to do some sanity checks and give useful error messages. Even if you cannot recover gracefully, +a good error message helps :-) + + + + +#### cleanup + +```python +def cleanup() +``` + +The cleanup function: free and release all resources used by this card reader (if any). + +Put all your cleanup code here, e.g. if you are using the serial bus or GPIO pins. +Will be called implicitly via the __exit__ function +This function must exist! If there is nothing to do, just leave the pass statement in place below + + + + +#### stop + +```python +def stop() +``` + +This function is called to tell the reader to exist it's reading function. + +This function is called before cleanup is called. + +> [!NOTE] +> This is usually called from a different thread than the reader's thread! And this is the reason for the +> two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt +> to read a card. Once called, the function read_card will not be called again. When the reader thread exits +> cleanup is called from the reader thread itself. + + + + +#### read\_card + +```python +def read_card() -> str +``` + +Blocking or non-blocking function that waits for a new card to appear and return the card's UID as string + +This is were your main code goes :-) +This function must return a string with the card id +In case of error, it may return None or an empty string + +The function should break and return with an empty string, once stop() is called + + + + +# components.rfid.readerbase + + + +## ReaderBaseClass Objects + +```python +class ReaderBaseClass(ABC) +``` + +Abstract Base Class for all Reader Classes to ensure common API + +Look at template_new_reader.py for documentation how to integrate a new RFID reader + + + + +# components.rfid.cards + +Handling the RFID card database + +A few considerations: +- Changing the Card DB influences to current state + - rfid.reader: Does not care, as it always freshly looks into the DB when a new card is triggered + - fake_reader_gui: Initializes the Drop-down menu once on start --> Will get out of date! + +Do we need a notifier? Or a callback for modules to get notified? +Do we want to publish the information about a card DB update? +TODO: Add callback for on_database_change + +TODO: check card id type (if int, convert to str) +TODO: check if args is really a list (convert if not?) + + + + +#### list\_cards + +```python +@plugs.register +def list_cards() +``` + +Provide a summarized, decoded list of all card actions + +This is intended as basis for a formatter function + +Format: 'id': {decoded_function_call, ignore_same_id_delay, ignore_card_removal_action, description, from_alias} + + + + +#### delete\_card + +```python +@plugs.register +def delete_card(card_id: str, auto_save: bool = True) +``` + +**Arguments**: + +- `auto_save`: +- `card_id`: + + + +#### register\_card + +```python +@plugs.register +def register_card(card_id: str, + cmd_alias: str, + args: Optional[List] = None, + kwargs: Optional[Dict] = None, + ignore_card_removal_action: Optional[bool] = None, + ignore_same_id_delay: Optional[bool] = None, + overwrite: bool = False, + auto_save: bool = True) +``` + +Register a new card based on quick-selection + +If you are going to call this through the RPC it will get a little verbose + +**Example:** Registering a new card with ID *0009* for increment volume with a custom argument to inc_volume +(*here: 15*) and custom *ignore_same_id_delay value*:: + + plugin.call_ignore_errors('cards', 'register_card', + args=['0009', 'inc_volume'], + kwargs={'args': [15], 'ignore_same_id_delay': True, 'overwrite': True}) + + + + +#### register\_card\_custom + +```python +@plugs.register +def register_card_custom() +``` + +Register a new card with full RPC call specification (Not implemented yet) + + + + +#### save\_card\_database + +```python +@plugs.register +def save_card_database(filename=None, *, only_if_changed=True) +``` + +Store the current card database. If filename is None, it is saved back to the file it was loaded from + + + + +# components.rfid.cardutils + +Common card decoding functions + +TODO: Thread safety when accessing the card DB! + + + + +#### decode\_card\_command + +```python +def decode_card_command(cfg_rpc_cmd: Mapping, logger: logging.Logger = log) +``` + +Extension of utils.decode_action with card-specific parameters + + + + +#### card\_command\_to\_str + +```python +def card_command_to_str(cfg_rpc_cmd: Mapping, long=False) -> List[str] +``` + +Returns a list of strings with [card_action, ignore_same_id_delay, ignore_card_removal_action] + +The last two parameters are only present, if *long* is True and if they are present in the cfg_rpc_cmd + + + + +#### card\_to\_str + +```python +def card_to_str(card_id: str, long=False) -> List[str] +``` + +Returns a list of strings from card entry command in the format of :func:`card_command_to_str` + + + + +# components.publishing + +Plugin interface for Jukebox Publisher + +Thin wrapper around jukebox.publishing to benefit from the plugin loading / exit handling / function handling + +This is the first package to be loaded and the last to be closed: put Hello and Goodbye publish messages here. + + + + +#### republish + +```python +@plugin.register +def republish(topic=None) +``` + +Re-publish the topic tree 'topic' to all subscribers + +**Arguments**: + +- `topic`: Topic tree to republish. None = resend all + + + +# components.player + + + +## MusicLibPath Objects + +```python +class MusicLibPath() +``` + +Extract the music directory from the mpd.conf file + + + + +#### get\_music\_library\_path + +```python +def get_music_library_path() +``` + +Get the music library path + + + + +# components.jingle + +Jingle Playback Factory for extensible run-time support of various file types + + + + +## JingleFactory Objects + +```python +class JingleFactory() +``` + +Jingle Factory + + + + +#### list + +```python +def list() +``` + +List the available volume services + + + + +#### play + +```python +@plugin.register +def play(filename) +``` + +Play the jingle using the configured jingle service + +> [!NOTE] +> This runs in a separate thread. And this may cause troubles +> when changing the volume level before +> and after the sound playback: There is nothing to prevent another +> thread from changing the volume and sink while playback happens +> and afterwards we change the volume back to where it was before! + +There is no way around this dilemma except for not running the jingle as a +separate thread. Currently (as thread) even the RPC is started before the sound +is finished and the volume is reset to normal... + +However: Volume plugin is loaded before jingle and sets the default +volume. No interference here. It can now only happen +if (a) through the RPC or (b) some other plugin the volume is changed. Okay, now +(a) let's hope that there is enough delay in the user requesting a volume change +(b) let's hope no other plugin wants to do that +(c) no bluetooth device connects during this time (and pulseaudio control is set to toggle_on_connect) +and take our changes with the threaded approach. + + + + +#### play\_startup + +```python +@plugin.register +def play_startup() +``` + +Play the startup sound (using jingle.play) + + + + +#### play\_shutdown + +```python +@plugin.register +def play_shutdown() +``` + +Play the shutdown sound (using jingle.play) + + + + +# components.jingle.alsawave + +ALSA wave jingle Service for jingle.JingleFactory + + + + +## AlsaWave Objects + +```python +@plugin.register +class AlsaWave() +``` + +Jingle Service for playing wave files directly from Python through ALSA + + + + +#### play + +```python +@plugin.tag +def play(filename) +``` + +Play the wave file + + + + +## AlsaWaveBuilder Objects + +```python +class AlsaWaveBuilder() +``` + + + +#### \_\_init\_\_ + +```python +def __init__() +``` + +Builder instantiates AlsaWave during init and not during first call because + +we want AlsaWave registers as plugin function in any case if this plugin is loaded +(and not only on first use!) + + + + +# components.jingle.jinglemp3 + +Generic MP3 jingle Service for jingle.JingleFactory + + + + +## JingleMp3Play Objects + +```python +@plugin.register(auto_tag=True) +class JingleMp3Play() +``` + +Jingle Service for playing MP3 files + + + + +#### play + +```python +def play(filename) +``` + +Play the MP3 file + + + + +## JingleMp3PlayBuilder Objects + +```python +class JingleMp3PlayBuilder() +``` + + + +#### \_\_init\_\_ + +```python +def __init__() +``` + +Builder instantiates JingleMp3Play during init and not during first call because + +we want JingleMp3Play registers as plugin function in any case if this plugin is loaded +(and not only on first use!) + + + + +# components.hostif.linux + + + +#### shutdown + +```python +@plugin.register +def shutdown() +``` + +Shutdown the host machine + + + + +#### reboot + +```python +@plugin.register +def reboot() +``` + +Reboot the host machine + + + + +#### jukebox\_is\_service + +```python +@plugin.register +def jukebox_is_service() +``` + +Check if current Jukebox process is running as a service + + + + +#### is\_any\_jukebox\_service\_active + +```python +@plugin.register +def is_any_jukebox_service_active() +``` + +Check if a Jukebox service is running + +> [!NOTE] +> Does not have the be the current app, that is running as a service! + + + + +#### restart\_service + +```python +@plugin.register +def restart_service() +``` + +Restart Jukebox App if running as a service + + + + +#### get\_disk\_usage + +```python +@plugin.register() +def get_disk_usage(path='/') +``` + +Return the disk usage in Megabytes as dictionary for RPC export + + + + +#### get\_cpu\_temperature + +```python +@plugin.register +def get_cpu_temperature() +``` + +Get the CPU temperature with single decimal point + +No error handling: this is expected to take place up-level! + + + + +#### get\_ip\_address + +```python +@plugin.register +def get_ip_address() +``` + +Get the IP address + + + + +#### wlan\_disable\_power\_down + +```python +@plugin.register() +def wlan_disable_power_down(card=None) +``` + +Turn off power management of wlan. Keep RPi reachable via WLAN + +This must be done after every reboot +card=None takes card from configuration file + + + + +#### get\_autohotspot\_status + +```python +@plugin.register +def get_autohotspot_status() +``` + +Get the status of the auto hotspot feature + + + + +#### stop\_autohotspot + +```python +@plugin.register() +def stop_autohotspot() +``` + +Stop auto hotspot functionality + +Basically disabling the cronjob and running the script one last time manually + + + + +#### start\_autohotspot + +```python +@plugin.register() +def start_autohotspot() +``` + +start auto hotspot functionality + +Basically enabling the cronjob and running the script one time manually + + + + +# components.misc + +Miscellaneous function package + + + + +#### rpc\_cmd\_help + +```python +@plugin.register +def rpc_cmd_help() +``` + +Return all commands for RPC + + + + +#### get\_all\_loaded\_packages + +```python +@plugin.register +def get_all_loaded_packages() +``` + +Get all successfully loaded plugins + + + + +#### get\_all\_failed\_packages + +```python +@plugin.register +def get_all_failed_packages() +``` + +Get all plugins with error during load or initialization + + + + +#### get\_start\_time + +```python +@plugin.register +def get_start_time() +``` + +Time when JukeBox has been started + + + + +#### get\_log + +```python +def get_log(handler_name: str) +``` + +Get the log file from the loggers (debug_file_handler, error_file_handler) + + + + +#### get\_log\_debug + +```python +@plugin.register +def get_log_debug() +``` + +Get the log file (from the debug_file_handler) + + + + +#### get\_log\_error + +```python +@plugin.register +def get_log_error() +``` + +Get the log file (from the error_file_handler) + + + + +#### get\_git\_state + +```python +@plugin.register +def get_git_state() +``` + +Return git state information for the current branch + + + + +#### empty\_rpc\_call + +```python +@plugin.register +def empty_rpc_call(msg: str = '') +``` + +This function does nothing. + +The RPC command alias 'none' is mapped to this function. + +This is also used when configuration errors lead to non existing RPC command alias definitions. +When the alias definition is void, we still want to return a valid function to simplify error handling +up the module call stack. + +**Arguments**: + +- `msg`: If present, this message is send to the logger with severity warning + + + +# components.controls + + + +# components.controls.bluetooth\_audio\_buttons + +Plugin to attempt to automatically listen to it's buttons (play, next, ...) + +when a bluetooth sound device (headphone, speakers) connects + +This effectively does: + +* register a callback with components.volume to get notified when a new sound card connects +* if that is a bluetooth device, try opening an input device with similar name using +* button listeners are run each in its own thread + + + + +# components.controls.common.evdev\_listener + +Generalized listener for ``dev/input`` devices + + + + +#### find\_device + +```python +def find_device(device_name: str, + exact_name: bool = True, + mandatory_keys: Optional[Set[int]] = None) -> str +``` + +Find an input device with device_name and mandatory keys. + +**Arguments**: + +- `device_name`: See :func:`_filter_by_device_name` +- `exact_name`: See :func:`_filter_by_device_name` +- `mandatory_keys`: See :func:`_filter_by_mandatory_keys` + +**Raises**: + +- `FileNotFoundError`: if no device is found. +- `AttributeError`: if device does not have the mandatory key +If multiple devices match, the first match is returned + +**Returns**: + +The path to the device + + + +## EvDevKeyListener Objects + +```python +class EvDevKeyListener(threading.Thread) +``` + +Opens and event input device from ``/dev/inputs``, and runs callbacks upon the button presses. + +Input devices could be .e.g. Keyboard, Bluetooth audio buttons, USB buttons + +Runs as a separate thread. When device disconnects or disappears, thread exists. A new thread must be started +when device re-connects. + +Assign callbacks to :attr:`EvDevKeyListener.button_callbacks` + + + + +#### \_\_init\_\_ + +```python +def __init__(device_name_request: str, exact_name: bool, thread_name: str) +``` + +**Arguments**: + +- `device_name_request`: The device name to look for +- `exact_name`: If true, device_name must mach exactly, else a match is returned if device_name is a substring of +the reported device name +- `thread_name`: Name of the listener thread + + + +#### run + +```python +def run() +``` + + + + + +#### start + +```python +def start() -> None +``` + +Start the tread and start listening + + + + +# components.battery\_monitor + + + +# components.battery\_monitor.BatteryMonitorBase + + + +## pt1\_frac Objects + +```python +class pt1_frac() +``` + +fixed point first order filter, fractional format: 2^16,2^16 + + + + +## BattmonBase Objects + +```python +class BattmonBase() +``` + +Battery Monitor base class + + + + +# components.battery\_monitor.batt\_mon\_simulator + + + +## battmon\_simulator Objects + +```python +class battmon_simulator(BatteryMonitorBase.BattmonBase) +``` + +Battery Monitor Simulator + + + + +# components.battery\_monitor.batt\_mon\_i2c\_ads1015 + + + +## battmon\_ads1015 Objects + +```python +class battmon_ads1015(BatteryMonitorBase.BattmonBase) +``` + +Battery Monitor based on a ADS1015 + +> [!CAUTION] +> Lithium and other batteries are dangerous and must be treated with care. +> Rechargeable Lithium Ion batteries are potentially hazardous and can +> present a serious **FIRE HAZARD** if damaged, defective or improperly used. +> Do not use this circuit to a lithium ion battery without expertise and +> training in handling and use of batteries of this type. +> Use appropriate test equipment and safety protocols during development. +> There is no warranty, this may not work as expected or at all! + +This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: + + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === + +Attention: +* the circuit is constantly draining the battery! (leak current up to: 2.1µA) +* the time between sample needs to be a minimum 1sec with this high impedance voltage divider + don't use the continuous conversion method! + + + + +# components.gpio.gpioz.plugin + +The GPIOZ plugin interface build all input and output devices from the configuration file and connects + +the actions and callbacks. It also provides a very restricted, but common API for the output devices to the RPC. +That API is mainly used for testing. All the relevant output state changes are usually made through callbacks directly +using the output device's API. + + + + +#### output\_devices + +List of all created output devices + + + + +#### input\_devices + +List of all created input devices + + + + +#### factory + +The global pin factory used in this module + +Using different pin factories for different devices is not supported + + + + +#### IS\_ENABLED + +Indicates that the GPIOZ module is enabled and loaded w/o errors + + + + +#### IS\_MOCKED + +Indicates that the pin factory is a mock factory + + + + +#### CONFIG\_FILE + +The path of the config file the GPIOZ configuration was loaded from + + + + +## ServiceIsRunningCallbacks Objects + +```python +class ServiceIsRunningCallbacks(CallbackHandler) +``` + +Callbacks are executed when + +* Jukebox app started +* Jukebox shuts down + +This is intended to e.g. signal an LED to change state. +This is integrated into this module because: + +* we need the GPIO to control a LED (it must be available when the status callback comes) +* the plugin callback functions provide all the functionality to control the status of the LED +* which means no need to adapt other modules + + + + +#### register + +```python +def register(func: Callable[[int], None]) +``` + +Add a new callback function :attr:`func`. + +Callback signature is + +.. py:function:: func(status: int) + :noindex: + +**Arguments**: + +- `status`: 1 if app started, 0 if app shuts down + + + +#### run\_callbacks + +```python +def run_callbacks(status: int) +``` + + + + + +#### service\_is\_running\_callbacks + +Callback handler instance for service_is_running_callbacks events. + +See :class:`ServiceIsRunningCallbacks` + + + + +#### build\_output\_device + +```python +def build_output_device(name: str, config: Dict) +``` + +Construct and register a new output device + +In principal all supported GPIOZero output devices can be used. +For all devices a custom functions need to be written to control the state of the outputs + + + + +#### build\_input\_device + +```python +def build_input_device(name: str, config) +``` + +Construct and connect a new input device + +Supported input devices are those from gpio.gpioz.core.input_devices + + + + +#### get\_output + +```python +def get_output(name: str) +``` + +Get the output device instance based on the configured name + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### on + +```python +@plugin.register +def on(name: str) +``` + +Turn an output device on + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### off + +```python +@plugin.register +def off(name: str) +``` + +Turn an output device off + +**Arguments**: + +- `name`: The alias name output device instance + + + +#### set\_value + +```python +@plugin.register +def set_value(name: str, value: Any) +``` + +Set the output device to :attr:`value` + +**Arguments**: + +- `name`: The alias name output device instance +- `value`: Value to set the device to + + + +#### flash + +```python +@plugin.register +def flash(name, + on_time=1, + off_time=1, + n=1, + *, + fade_in_time=0, + fade_out_time=0, + tone=None, + color=(1, 1, 1)) +``` + +Flash (blink or beep) an output device + +This is a generic function for all types of output devices. Parameters not applicable to an +specific output device are silently ignored + +**Arguments**: + +- `name`: The alias name output device instance +- `on_time`: Time in seconds in state ``ON`` +- `off_time`: Time in seconds in state ``OFF`` +- `n`: Number of flash cycles +- `tone`: The tone in to play, e.g. 'A4'. *Only for TonalBuzzer*. +- `color`: The RGB color *only for PWMLED*. +- `fade_in_time`: Time in seconds for transitioning to on. *Only for PWMLED and RGBLED* +- `fade_out_time`: Time in seconds for transitioning to off. *Only for PWMLED and RGBLED* + + + +# components.gpio.gpioz.plugin.connectivity + +Provide connector functions to hook up to some kind of Jukebox functionality and change the output device's state + +accordingly. + +Connector functions can often be used for various output devices. Some connector functions are specific to +an output device type. + + + + +#### BUZZ\_TONE + +The tone to be used as buzz tone when the buzzer is an active buzzer + + + + +#### register\_rfid\_callback + +```python +def register_rfid_callback(device) +``` + +Flash the output device once on successful RFID card detection and thrice if card ID is unknown + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.LED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` +* :class:`components.gpio.gpioz.core.output_devices.Buzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + + +#### register\_status\_led\_callback + +```python +def register_status_led_callback(device) +``` + +Turn LED on when Jukebox App has started + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.LED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + + +#### register\_status\_buzzer\_callback + +```python +def register_status_buzzer_callback(device) +``` + +Buzz once when Jukebox App has started, twice when closing down + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.Buzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + + +#### register\_status\_tonalbuzzer\_callback + +```python +def register_status_tonalbuzzer_callback(device) +``` + +Buzz a multi-note melody when Jukebox App has started and when closing down + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + + +#### register\_audio\_sink\_change\_callback + +```python +def register_audio_sink_change_callback(device) +``` + +Turn LED on if secondary audio output is selected. If audio output change + +fails, blink thrice + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.LED` +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + + +#### register\_volume\_led\_callback + +```python +def register_volume_led_callback(device) +``` + +Have a PWMLED change it's brightness according to current volume. LED flashes when minimum or maximum volume + +is reached. Minimum value is still a very dimly turned on LED (i.e. LED is never off). + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.PWMLED` + + + + +#### register\_volume\_buzzer\_callback + +```python +def register_volume_buzzer_callback(device) +``` + +Sound a buzzer once when minimum or maximum value is reached + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.Buzzer` +* :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + + + + +#### register\_volume\_rgbled\_callback + +```python +def register_volume_rgbled_callback(device) +``` + +Have a :class:`RGBLED` change it's color according to current volume. LED flashes when minimum or maximum volume + +is reached. + +Compatible devices: + +* :class:`components.gpio.gpioz.core.output_devices.RGBLED` + + + + +# components.gpio.gpioz.core.converter + +Provides converter functions/classes for various Jukebox parameters to + +values that can be assigned to GPIO output devices + + + + +## ColorProperty Objects + +```python +class ColorProperty() +``` + +Color descriptor ensuring valid weight ranges + + + + +## VolumeToRGB Objects + +```python +class VolumeToRGB() +``` + +Converts linear volume level to an RGB color value running through the color spectrum + +**Arguments**: + +- `max_input`: Maximum input value of linear input data +- `offset`: Offset in degrees in the color circle. Color circle +traverses blue (0), cyan(60), green (120), yellow(180), red (240), magenta (340) +- `section`: The section of the full color circle to use in degrees +Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 + + conv = VolumeToRGB(100, offset=120, section=180) + (r, g, b) = conv(50) + +The three components of an RGB LEDs do not have the same luminosity. +Weight factors are used to get a balanced color output + + + +#### \_\_call\_\_ + +```python +def __call__(volume) -> Tuple[float, float, float] +``` + +Perform conversion for single volume level + +**Returns**: + +Tuple(red, green, blue) + + + +#### luminize + +```python +def luminize(r, g, b) +``` + +Apply the color weight factors to the input color values + + + + +# components.gpio.gpioz.core.mock + +Changes to the GPIOZero devices for using with the Mock RFID Reader + + + + +#### patch\_mock\_outputs\_with\_callback + +```python +def patch_mock_outputs_with_callback() +``` + +Monkey Patch LED + Buzzer to get a callback when state changes + +This targets to represent the state in the TK GUI. +Other output devices cannot be represented in the GUI and are silently ignored. + +> [!NOTE] +> Only for developing purposes! + + + + +# components.gpio.gpioz.core.input\_devices + +Provides all supported input devices for the GPIOZ plugin. + +Input devices are based on GPIOZero devices. So for certain configuration parameters, you should +their documentation. + +All callback handlers are replaced by GPIOZ callback handlers. These are usually configured +by using the :func:`set_rpc_actions` each input device exhibits. + +For examples how to use the devices from the configuration files, see +[GPIO: Input Devices](../../builders/gpio.md#input-devices). + + + + +## NameMixin Objects + +```python +class NameMixin(ABC) +``` + +Provides name property and RPC decode function + + + + +#### set\_rpc\_actions + +```python +@abstractmethod +def set_rpc_actions(action_config) -> None +``` + +Set all input device callbacks from :attr:`action_config` + +**Arguments**: + +- `action_config`: Dictionary with one +[RPC Commands](../../builders/rpc-commands.md) definition entry for every device callback + + + +## EventProperty Objects + +```python +class EventProperty() +``` + +Event callback property + + + + +## ButtonBase Objects + +```python +class ButtonBase(ABC) +``` + +Common stuff for single button devices + + + + +#### value + +```python +@property +def value() +``` + +Returns 1 if the button is currently pressed, and 0 if it is not. + + + + +#### pin + +```python +@property +def pin() +``` + +Returns the underlying pin class from GPIOZero. + + + + +#### pull\_up + +```python +@property +def pull_up() +``` + +If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. + + + + +#### close + +```python +def close() +``` + +Close the device and release the pin + + + + +## Button Objects + +```python +class Button(NameMixin, ButtonBase) +``` + +A basic Button that runs a single actions on button press + +**Arguments**: + +- `pull_up` (`bool`): If :data:`True`, the device uses an internal pull-up resistor to set the GPIO pin “high” by default. +If :data:`False` the internal pull-down resistor is used. If :data:`None`, the pin will be floating and an external +resistor must be used and the :attr:`active_state` must be set. +- `active_state` (`bool or None`): If :data:`True`, when the hardware pin state is ``HIGH``, the software +pin is ``HIGH``. If :data:`False`, the input polarity is reversed: when +the hardware pin state is ``HIGH``, the software pin state is ``LOW``. +Use this parameter to set the active state of the underlying pin when +configuring it as not pulled (when *pull_up* is :data:`None`). When +*pull_up* is :data:`True` or :data:`False`, the active state is +automatically set to the proper value. +- `bounce_time` (`float or None`): Specifies the length of time (in seconds) that the component will +ignore changes in state after an initial change. This defaults to +:data:`None` which indicates that no bounce compensation will be +performed. +- `hold_repeat` (`bool`): If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else action +is run only once independent of the length of time the button is pressed for. +- `hold_time` (`float`): Time in seconds to wait between invocations of :attr:`on_press`. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file + +.. copied from GPIOZero's documentation: active_state, bounce_time +.. Copyright Ben Nuttall / SPDX-License-Identifier: BSD-3-Clause + + + +#### on\_press + +```python +@property +def on_press() +``` + +The function to run when the device has been pressed + + + + +## LongPressButton Objects + +```python +class LongPressButton(NameMixin, ButtonBase) +``` + +A Button that runs a single actions only when the button is pressed long enough + +**Arguments**: + +- `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `active_state`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `hold_repeat`: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action +is run only once independent of the length of time the button is pressed for. +- `hold_time`: The minimum time, the button must be pressed be running :attr:`on_press` for the first time. +Also the time in seconds to wait between invocations of :attr:`on_press`. + + + +#### on\_press + +```python +@on_press.setter +def on_press(func) +``` + +The function to run when the device has been pressed for longer than :attr:`hold_time` + + + + +## ShortLongPressButton Objects + +```python +class ShortLongPressButton(NameMixin, ButtonBase) +``` + +A single button that runs two different actions depending if the button is pressed for a short or long time. + +The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press +can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. +But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release +event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run +in this case! + +**Arguments**: + +- `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `active_state`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action +- `pin_factory`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) + + + +## RotaryEncoder Objects + +```python +class RotaryEncoder(NameMixin) +``` + +A rotary encoder to run one of two actions depending on the rotation direction. + +**Arguments**: + +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `pin_factory`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) + + + +#### pin\_a + +```python +@property +def pin_a() +``` + +Returns the underlying pin A + + + + +#### pin\_b + +```python +@property +def pin_b() +``` + +Returns the underlying pin B + + + + +#### on\_rotate\_clockwise + +```python +@property +def on_rotate_clockwise() +``` + +The function to run when the encoder is rotated clockwise + + + + +#### on\_rotate\_counter\_clockwise + +```python +@property +def on_rotate_counter_clockwise() +``` + +The function to run when the encoder is rotated counter clockwise + + + + +#### close + +```python +def close() +``` + +Close the device and release the pin + + + + +## TwinButton Objects + +```python +class TwinButton(NameMixin) +``` + +A two-button device which can run up to six different actions, a.k.a the six function beast. + +Per user press "input" of the TwinButton, only a single callback is executed (but this callback +may be executed several times). +The shortest possible time is used to ensure a unique identification to an action can be made. For example a short press +can only be identified, when a button is released before :attr:`hold_time`, i.e. not directly on button press. +But a long press can be identified as soon as :attr:`hold_time` is reached and there is no need to wait for the release +event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run +in this case! + +It is not necessary to configure all actions. + +**Arguments**: + +- `pull_up`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `active_state`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `bounce_time`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `hold_time`: The time in seconds to differentiate if it is a short or long press. If the button is released before +this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the +short press action is ignored. +- `hold_repeat`: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press +action. A long dual press is never repeated independent of this setting +- `pin_factory`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) +- `name`: See [`Button`](#components.gpio.gpioz.core.input_devices.Button) + + + +## StateVar Objects + +```python +class StateVar(Enum) +``` + +State encoding of the Mealy FSM + + + + +#### close + +```python +def close() +``` + +Close the device and release the pins + + + + +#### value + +```python +@property +def value() +``` + +2 bit integer indicating if and which button is currently pressed. Button A is the LSB. + + + + +#### is\_active + +```python +@property +def is_active() +``` + + + + + +# components.gpio.gpioz.core.output\_devices + +Provides all supported output devices for the GPIOZ plugin. + +For each device all constructor parameters can be set via the configuration file. Only exceptions +are the :attr:`name` and :attr:`pin_factory` which are set by internal mechanisms. + +The devices a are a relatively thin wrapper around the GPIOZero devices with the same name. +We add a name property to be used for error log message and similar and a :func:`flash` function +to all devices. This function provides a unified API to all devices. This means it can be called for every device +with parameters for this device and optional parameters from another device. Unused/unsupported parameters +are silently ignored. This is done to reduce the amount of coding required for connectivity functions. + +For examples how to use the devices from the configuration files, see +[GPIO: Output Devices](../../builders/gpio.md#output-devices). + + + + +## LED Objects + +```python +class LED(NameMixin, gpiozero.LED) +``` + +A binary LED + +**Arguments**: + +- `pin`: The GPIO pin which the LED is connected +- `active_high`: If :data:`true` the output pin will have a high logic level when the device is turned on. +- `pin_factory`: The GPIOZero pin factory. This parameter cannot be set through the configuration file +- `name` (`str`): The name of the button for use in error messages. This parameter cannot be set explicitly +through the configuration file + + + +#### flash + +```python +def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) +``` + +Exactly like :func:`blink` but restores the original state after flashing the device + +**Arguments**: + +- `on_time` (`float`): Number of seconds on. Defaults to 1 second. +- `off_time` (`float`): Number of seconds off. Defaults to 1 second. +- `n`: Number of times to blink; :data:`None` means forever. +- `background` (`bool`): If :data:`True` (the default), start a background thread to +continue blinking and return immediately. If :data:`False`, only +return when the blink is finished +- `ignored_kwargs`: Ignore all other keywords so this function can be called with identical +parameters also for all other output devices + + + +## Buzzer Objects + +```python +class Buzzer(NameMixin, gpiozero.Buzzer) +``` + + + +#### flash + +```python +def flash(on_time=1, off_time=1, n=1, *, background=True, **ignored_kwargs) +``` + +Flash the device and restore the previous value afterwards + + + + +## PWMLED Objects + +```python +class PWMLED(NameMixin, gpiozero.PWMLED) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + n=1, + *, + fade_in_time=0, + fade_out_time=0, + background=True, + **ignored_kwargs) +``` + +Flash the LED and restore the previous value afterwards + + + + +## RGBLED Objects + +```python +class RGBLED(NameMixin, gpiozero.RGBLED) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + *, + fade_in_time=0, + fade_out_time=0, + on_color=(1, 1, 1), + off_color=(0, 0, 0), + n=None, + background=True, + **igorned_kwargs) +``` + +Flash the LED with :attr:`on_color` and restore the previous value afterwards + + + + +## TonalBuzzer Objects + +```python +class TonalBuzzer(NameMixin, gpiozero.TonalBuzzer) +``` + + + +#### flash + +```python +def flash(on_time=1, + off_time=1, + n=1, + *, + tone=None, + background=True, + **ignored_kwargs) +``` + +Play the tone :data:`tone` for :attr:`n` times + + + + +#### melody + +```python +def melody(on_time=0.2, + off_time=0.05, + *, + tone: Optional[List[Tone]] = None, + background=True) +``` + +Play a melody from the list of tones in :attr:`tone` + + + + +# components.timers + + + +# jukebox + + + +# jukebox.callingback + +Provides a generic callback handler + + + + +## CallbackHandler Objects + +```python +class CallbackHandler() +``` + +Generic Callback Handler to collect callbacks functions through :func:`register` and execute them + +with :func:`run_callbacks` + +A lock is used to sequence registering of new functions and running callbacks. + +**Arguments**: + +- `name`: A name of this handler for usage in log messages +- `logger`: The logger instance to use for logging +- `context`: A custom context handler to use as lock. If none, a local :class:`threading.Lock()` will be created + + + +#### register + +```python +def register(func: Optional[Callable[..., None]]) +``` + +Register a new function to be executed when the callback event happens + +**Arguments**: + +- `func`: The function to register. If set to :data:`None`, this register request is silently ignored. + + + +#### run\_callbacks + +```python +def run_callbacks(*args, **kwargs) +``` + +Run all registered callbacks. + +*ALL* exceptions from callback functions will be caught and logged only. +Exceptions are not raised upwards! + + + + +#### has\_callbacks + +```python +@property +def has_callbacks() +``` + + + + + +# jukebox.version + + + +#### version + +```python +def version() +``` + +Return the Jukebox version as a string + + + + +#### version\_info + +```python +def version_info() +``` + +Return the Jukebox version as a tuple of three numbers + +If this is a development version, an identifier string will be appended after the third integer. + + + + +# jukebox.cfghandler + +This module handles global and local configuration data + +The concept is that config handler is created and initialized once in the main thread:: + + cfg = get_handler('global') + load_yaml(cfg, 'filename.yaml') + +In all other modules (in potentially different threads) the same handler is obtained and used by:: + + cfg = get_handler('global') + +This eliminates the need to pass an effectively global configuration handler by parameters across the entire design. +Handlers are identified by their name (in the above example *global*) + +The function :func:`get_handler` is the main entry point to obtain a new or existing handler. + + + + +## ConfigHandler Objects + +```python +class ConfigHandler() +``` + +The configuration handler class + +Don't instantiate directly. Always use :func:`get_handler`! + +**Threads:** + +All threads can read and write to the configuration data. +**Proper thread-safeness must be ensured** by the the thread modifying the data by acquiring the lock +Easiest and best way is to use the context handler:: + + with cfg: + cfg['key'] = 66 + cfg.setndefault('hello', value='world') + +For a single function call, this is done implicitly. In this case, there is no need +to explicitly acquire the lock. + +Alternatively, you can lock and release manually by using :func:`acquire` and :func:`release` +But be very sure to release the lock even in cases of errors an exceptions! +Else we have a deadlock. + +Reading may be done without acquiring a lock. But be aware that when reading multiple values without locking, another +thread may intervene and modify some values in between! So, locking is still recommended. + + + + +#### loaded\_from + +```python +@property +def loaded_from() -> Optional[str] +``` + +Property to store filename from which the config was loaded + + + + +#### get + +```python +def get(key, *, default=None) +``` + +Enforce keyword on default to avoid accidental misuse when actually getn is wanted + + + + +#### setdefault + +```python +def setdefault(key, *, value) +``` + +Enforce keyword on default to avoid accidental misuse when actually setndefault is wanted + + + + +#### getn + +```python +def getn(*keys, default=None) +``` + +Get the value at arbitrary hierarchy depth. Return ``default`` if key not present + +The *default* value is returned no matter at which hierarchy level the path aborts. +A hierarchy is considered as any type with a :func:`get` method. + + + + +#### setn + +```python +def setn(*keys, value, hierarchy_type=None) -> None +``` + +Set the ``key: value`` pair at arbitrary hierarchy depth + +All non-existing hierarchy levels are created. + +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + + + +#### setndefault + +```python +def setndefault(*keys, value, hierarchy_type=None) +``` + +Set the ``key: value`` pair at arbitrary hierarchy depth unless the key already exists + +All non-existing hierarchy levels are created. + +**Arguments**: + +- `keys`: Key hierarchy path through the nested levels +- `value`: The default value to set +- `hierarchy_type`: The type for new hierarchy levels. If *None*, the top-level type +is used + +**Returns**: + +The actual value or or the default value if key does not exit + + + +#### config\_dict + +```python +def config_dict(data) +``` + +Initialize configuration data from dict-like data structure + +**Arguments**: + +- `data`: configuration data + + + +#### is\_modified + +```python +def is_modified() -> bool +``` + +Check if the data has changed since the last load/store + +> [!NOTE] +> This relies on the *__str__* representation of the underlying data structure +> In case of ruamel, this ignores comments and only looks at the data + + + + +#### clear\_modified + +```python +def clear_modified() -> None +``` + +Sets the current state as new baseline, clearing the is_modified state + + + + +#### save + +```python +def save(only_if_changed: bool = False) -> None +``` + +Save config back to the file it was loaded from + +If you want to save to a different file, use :func:`write_yaml`. + + + + +#### load + +```python +def load(filename: str) -> None +``` + +Load YAML config file into memory + + + + +#### get\_handler + +```python +def get_handler(name: str) -> ConfigHandler +``` + +Get a configuration data handler with the specified name, creating it + +if it doesn't yet exit. If created, it is always created empty. + +This is the main entry point for obtaining an configuration handler + +**Arguments**: + +- `name`: Name of the config handler + +**Returns**: + +`ConfigHandler`: The configuration data handler for *name* + + + +#### load\_yaml + +```python +def load_yaml(cfg: ConfigHandler, filename: str) -> None +``` + +Load a yaml file into a ConfigHandler + +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to yaml file + +**Returns**: + +None + + + +#### write\_yaml + +```python +def write_yaml(cfg: ConfigHandler, + filename: str, + only_if_changed: bool = False, + *args, + **kwargs) -> None +``` + +Writes ConfigHandler data to yaml file / sys.stdout + +**Arguments**: + +- `cfg`: ConfigHandler instance +- `filename`: filename to output file. If *sys.stdout*, output is written to console +- `only_if_changed`: Write file only, if ConfigHandler.is_modified() +- `args`: passed on to yaml.dump(...) +- `kwargs`: passed on to yaml.dump(...) + +**Returns**: + +None + + + +# jukebox.playlistgenerator + +Playlists are build from directory content in the following way: + +a directory is parsed and files are added to the playlist in the following way + +1. files are added in alphabetic order +2. files ending with ``*livestream.txt`` are unpacked and the containing URL(s) are added verbatim to the playlist +3. files ending with ``*podcast.txt`` are unpacked and the containing Podcast URL(s) are expanded and added to the playlist +4. files ending with ``*.m3u`` are treated as folder playlist. Regular folder processing is suspended and the playlist + is build solely from the ``*.m3u`` content. Only the alphabetically first ``*.m3u`` is processed. URLs are added verbatim + to the playlist except for ``*.xml`` and ``*.podcast`` URLS, which are expanded first + +An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. + + 01-livestream.txt + 02-livestream.txt + music.mp3 + podcast.txt + +All files are treated as music files and are added to the playlist, except those: + + * starting with ``.``, + * not having a file ending, i.e. do not contain a ``.``, + * ending with ``.txt``, + * ending with ``.m3u``, + * ending with one of the excluded file endings in :attr:`PlaylistCollector._exclude_endings` + +In recursive mode, the playlist is generated by concatenating all sub-folder playlists. Sub-folders are parsed +in alphabetic order. Symbolic links are being followed. The above rules are enforced on a per-folder bases. +This means, one ``*.m3u`` file per sub-folder is processed (if present). + +In ``*.txt`` and ``*.m3u`` files, all lines starting with ``#`` are ignored. + + + + +#### TYPE\_DECODE + +Types if file entires in parsed directory + + + + +## PlaylistCollector Objects + +```python +class PlaylistCollector() +``` + +Build a playlist from directory(s) + +This class is intended to be used with an absolute path to the music library:: + + plc = PlaylistCollector('/home/chris/music') + plc.parse('Traumfaenger') + print(f"res = {plc}") + +But it can also be used with relative paths from current working directory:: + + plc = PlaylistCollector('.') + plc.parse('../../../../music/Traumfaenger') + print(f"res = {plc}") + +The file ending exclusion list :attr:`PlaylistCollector._exclude_endings` is a class variable for performance reasons. +If changed it will affect all instances. For modifications always call :func:`set_exclusion_endings`. + + + + +#### \_\_init\_\_ + +```python +def __init__(music_library_base_path='/') +``` + +Initialize the playlist generator with music_library_base_path + +**Arguments**: + +- `music_library_base_path`: Base path the the music library. This is used to locate the file in the disk +but is omitted when generating the playlist entries. I.e. all files in the playlist are relative to this base dir + + + +#### set\_exclusion\_endings + +```python +@classmethod +def set_exclusion_endings(cls, endings: List[str]) +``` + +Set the class-wide file ending exclusion list + +See :attr:`PlaylistCollector._exclude_endings` + + + + +#### get\_directory\_content + +```python +def get_directory_content(path='.') +``` + +Parse the folder ``path`` and create a content list. Depth is always the current level + +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` + +**Returns**: + +[ { type: 'directory', name: 'Simone', path: '/some/path/to/Simone' }, {...} ] +where type is one of :attr:`TYPE_DECODE` + + + +#### parse + +```python +def parse(path='.', recursive=False) +``` + +Parse the folder ``path`` and create a playlist from it's content + +**Arguments**: + +- `path`: Path to folder **relative** to ``music_library_base_path`` +- `recursive`: Parse folder recursivley, or stay in top-level folder + + + +# jukebox.NvManager + + + +# jukebox.publishing + + + +#### get\_publisher + +```python +def get_publisher() +``` + +Return the publisher instance for this thread + +Per thread, only one publisher instance is required to connect to the inproc socket. +A new instance is created if it does not already exist. + +If there is a remote-chance that your function publishing something may be called form +different threads, always make a fresh call to ``get_publisher()`` to get the correct instance for the current thread. + +Example:: + + import jukebox.publishing as publishing + + class MyClass: + def __init__(self): + pass + + def say_hello(name): + publishing.get_publisher().send('hello', f'Hi {name}, howya?') + +To stress what **NOT** to do: don't get a publisher instance in the constructor and save it to ``self._pub``. +If you do and ``say_hello`` gets called from different threads, the publisher of the thread which instantiated the class +will be used. + +If you need your very own private Publisher Instance, you'll need to instantiate it yourself. +But: the use cases are very rare for that. I cannot think of one at the moment. + +**Remember**: Don’t share ZeroMQ sockets between threads. + + + + +# jukebox.publishing.subscriber + + + +# jukebox.publishing.server + +## Publishing Server + +The common publishing server for the entire Jukebox using ZeroMQ + +### Structure + + +-----------------------+ + | functional interface | Publisher + | | - functional interface for single Thread + | PUB | - sends data to publisher (and thus across threads) + +-----------------------+ + | (1) + v + +-----------------------+ + | SUB (bind) | PublishServer + | | - Last Value (LV) Cache + | XPUB (bind) | - Subscriber notification and LV resend + +-----------------------+ - independent thread + | (2) + v + +#### Connection (1): Internal connection + +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) + + Protocol: Multi-part message + + Part 1: Topic (in topic tree format) + E.g. player.status.elapsed + + Part 2: Payload or Message in json serialization + If empty (i.e. ``b''``), it means delete the topic sub-tree from cache. And instruct subscribers to do the same + + Part 3: Command + Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer + and the message is not forwarded to the outside. This third part of the message is never forwarded + +#### Connection (2): External connection + +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` + + Protocol: Multi-part message + + Part 1: Topic (in topic tree format) + E.g. player.status.elapsed + + Part 2: Payload or Message in json serialization + If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) + +### Why? Why? + +Check out the [ZeroMQ Documentation](https://zguide.zeromq.org/docs/chapter5) +for why you need a proxy in a good design. + +For use case, we made a few simplifications + +### Design Rationales + +* "If you need [millions of messages per second](https://zguide.zeromq.org/docs/chapter5/`Pros`-and-Cons-of-Pub-Sub) + sent to thousands of points, + you'll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." +* "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then + the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/`Last`-Value-Caching)" +* "Let's imagine [our feed has an average of 100,000 100-byte messages a + second](https://zguide.zeromq.org/docs/chapter5/`High`-Speed-Subscribers-Black-Box-Pattern) [...]. + While 100K messages a second is easy for a ZeroMQ application, ..." + +**But we have:** + +* few dozen subscribers --> Check! +* limited number of topics --> Check! +* max ~10 messages per second --> Check! +* small common state information --> Check! +* only the server updates the state --> Check! + +This means, we can use less complex patters than used for these high-speed, high code count, high data rate networks :-) + +* XPUB / XSUB to detect new subscriber +* Cache the entire state in the publisher +* Re-send the entire state on-demand (and then even to every subscriber) +* Using the same channel: sends state to every subscriber + +**Reliability considerations** + +* Late joining client (or drop-off and re-join): get full state update +* Server crash etc: No special handling necessary, we are simple + and don't need recovery in this case. Server will publish initial state + after re-start +* Subscriber too slow: Subscribers problem (TODO: Do we need to do anything about it?) + +**Start-up sequence:** + +* Publisher plugin is first plugin to be loaded +* Due to Publisher - PublisherServer structure no further sequencing required + +### Plugin interactions and usage + +RPC can trigger through function call in components/publishing plugin that + +* entire state is re-published (from the cache) +* a specific topic tree is re-published (from the cache) + +Plugins publishing state information should publish initial state at @plugin.finalize + +> [!IMPORTANT] +> Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is +> required per thread. But the publisher instance **must** be thread-local! +> Always go through :func:`publishing.get_publisher()`. + +**Sockets** + +Three sockets are opened: + +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules + that want to know about the current state on event based updates. + +**Further ZeroMQ References:** + +* [Working with Messages](https://zguide.zeromq.org/docs/chapter2/`Working`-with-Messages) +* [Multiple Threads](https://zguide.zeromq.org/docs/chapter2/`Multithreading`-with-ZeroMQ) + + + + +## PublishServer Objects + +```python +class PublishServer(threading.Thread) +``` + +The publish proxy server that collects and caches messages from all internal publishers and + +forwards them to the outside world + +Handles new subscriptions by sending out the entire cached state to **all** subscribers + +The code is structures using a [Reactor Pattern](https://zguide.zeromq.org/docs/chapter5/`Using`-a-Reactor) + + + + +#### run + +```python +def run() +``` + +Thread's activity + + + + +#### handle\_message + +```python +def handle_message(msg) +``` + +Handle incoming messages + + + + +#### handle\_subscription + +```python +def handle_subscription(msg) +``` + +Handle new subscribers + + + + +## Publisher Objects + +```python +class Publisher() +``` + +The publisher that provides the functional interface to the application + +> [!NOTE] +> * An instance must not be shared across threads! +> * One instance per thread is enough + + + + +#### \_\_init\_\_ + +```python +def __init__(check_thread_owner=True) +``` + +**Arguments**: + +- `check_thread_owner`: Check if send() is always called from the correct thread. This is debug feature +and is intended to expose the situation before it leads to real trouble. Leave it on! + + + +#### send + +```python +def send(topic: str, payload) +``` + +Send out a message for topic + + + + +#### revoke + +```python +def revoke(topic: str) +``` + +Revoke a single topic element (not a topic tree!) + + + + +#### resend + +```python +def resend(topic: Optional[str] = None) +``` + +Instructs the PublishServer to resend current status to all subscribers + +Not necessary to call after incremental updates or new subscriptions - that will happen automatically! + + + + +#### close\_server + +```python +def close_server() +``` + +Instructs the PublishServer to close itself down + + + + +# jukebox.daemon + + + +#### log\_active\_threads + +```python +@atexit.register +def log_active_threads() +``` + +This functions is registered with atexit very early, meaning it will be run very late. It is the best guess to + +evaluate which Threads are still running (and probably shouldn't be) + +This function is registered before all the plugins and their dependencies are loaded + + + + +## JukeBox Objects + +```python +class JukeBox() +``` + + + +#### signal\_handler + +```python +def signal_handler(esignal, frame) +``` + +Signal handler for orderly shutdown + +On first Ctrl-C (or SIGTERM) orderly shutdown procedure is embarked upon. It gets allocated a time-out! +On third Ctrl-C (or SIGTERM), this is interrupted and there will be a hard exit! + + + + +# jukebox.plugs + +A plugin package with some special functionality + +Plugins packages are python packages that are dynamically loaded. From these packages only a subset of objects is exposed +through the plugs.call interface. The python packages can use decorators or dynamic function call to register (callable) +objects. + +The python package name may be different from the name the package is registered under in plugs. This allows to load different +python packages for a specific feature based on a configuration file. Note: Python package are still loaded as regular +python packages and can be accessed by normal means + +If you want to provide additional functionality to the same feature (probably even for run-time switching) +you can implement a Factory Pattern using this package. Take a look at volume.py as an example. + +**Example:** Decorate a function for auto-registering under it's own name: + + import jukebox.plugs as plugs + @plugs.register + def func1(param): + pass + +**Example:** Decorate a function for auto-registering under a new name: + + @plugs.register(name='better_name') + def func2(param): + pass + +**Example:** Register a function during run-time under it's own name: + + def func3(param): + pass + plugs.register(func3) + +**Example:** Register a function during run-time under a new name: + + def func4(param): + pass + plugs.register(func4, name='other_name', package='other_package') + +**Example:** Decorate a class for auto registering during initialization, +including all methods (see _register_class for more info): + + @plugs.register(auto_tag=True) + class MyClass1: + pass + +**Example:** Register a class instance, from which only report is a callable method through the plugs interface: + + class MyClass2: + @plugs.tag + def report(self): + pass + myinst2 = MyClass2() + plugin.register(myinst2, name='myinst2') + +Naming convention: + +* package + * Either a python package + * or a plugin package (which is the python package but probably loaded under a different name inside plugs) +* plugin + * An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) + * The string name to above object +* name + * The string name of the plugin object for registration +* method + * In case the object is a class instance a bound method to call from the class instance + * The string name to above object + + + + +## PluginPackageClass Objects + +```python +class PluginPackageClass() +``` + +A local data class for holding all information about a loaded plugin package + + + + +#### register + +```python +@overload +def register(plugin: Callable) -> Callable +``` + +1-level decorator around a function + + + + +#### register + +```python +@overload +def register(plugin: Type) -> Any +``` + +Signature: 1-level decorator around a class + + + + +#### register + +```python +@overload +def register(*, name: str, package: Optional[str] = None) -> Callable +``` + +Signature: 2-level decorator around a function + + + + +#### register + +```python +@overload +def register(*, auto_tag: bool = False, package: Optional[str] = None) -> Type +``` + +Signature: 2-level decorator around a class + + + + +#### register + +```python +@overload +def register(plugin: Callable[..., Any] = None, + *, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False) -> Callable +``` + +Signature: Run-time registration of function / class instance / bound method + + + + +#### register + +```python +def register(plugin: Optional[Callable] = None, + *, + name: Optional[str] = None, + package: Optional[str] = None, + replace: bool = False, + auto_tag: bool = False) -> Callable +``` + +A generic decorator / run-time function to register plugin module callables + +The functions comes in five distinct signatures for 5 use cases: + +1. ``@plugs.register``: decorator for a class w/o any arguments +2. ``@plugs.register``: decorator for a function w/o any arguments +3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments +4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments +5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of + * function + * bound method + * class instance + +For more documentation see the functions +* :func:`_register_obj` +* :func:`_register_class` + +See the examples in Module :mod:`plugs` how to use this decorator / function + +**Arguments**: + +- `plugin`: +- `name`: +- `package`: +- `replace`: +- `auto_tag`: + + + +#### tag + +```python +def tag(func: Callable) -> Callable +``` + +Method decorator for tagging a method as callable through the plugs interface + +Note that the instantiated class must still be registered as plugin object +(either with the class decorator or dynamically) + +**Arguments**: + +- `func`: function to decorate + +**Returns**: + +the function + + + +#### initialize + +```python +def initialize(func: Callable) -> Callable +``` + +Decorator for functions that shall be called by the plugs package directly after the module is loaded + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### finalize + +```python +def finalize(func: Callable) -> Callable +``` + +Decorator for functions that shall be called by the plugs package directly after ALL modules are loaded + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### atexit + +```python +def atexit(func: Callable[[int], Any]) -> Callable[[int], Any] +``` + +Decorator for functions that shall be called by the plugs package directly after at exit of program. + +> [!IMPORTANT] +> There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called +> during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your +> shutdown handler. + +The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) +It is intended for passing down the signal number that initiated the program termination + +**Arguments**: + +- `func`: Function to decorate + +**Returns**: + +The function itself + + + +#### load + +```python +def load(package: str, + load_as: Optional[str] = None, + prefix: Optional[str] = None) +``` + +Loads a python package as plugin package + +Executes a regular python package load. That means a potentially existing `__init__.py` is executed. +Decorator `@register` can by used to register functions / classes / class istances as plugin callable +Decorator `@initializer` can be used to tag functions that shall be called after package loading +Decorator `@finalizer` can be used to tag functions that shall be called after ALL plugin packges have been loaded +Instead of using `@initializer`, you may of course use `__init__.py` + +Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under +which they are loaded as plugin package also. + +**Arguments**: + +- `package`: Python package to load as plugin package +- `load_as`: Plugin package registration name. If None the name is the python's package simple name +- `prefix`: Prefix to python package to create fully qualified name. This is used only to locate the python package +and ignored otherwise. Useful if all the plugin module are in a dedicated folder + + + +#### load\_all\_named + +```python +def load_all_named(packages_named: Mapping[str, str], + prefix: Optional[str] = None, + ignore_errors=False) +``` + +Load all packages in packages_named with mapped names + +**Arguments**: + +- `packages_named`: Dict[load_as, package] + + + +#### load\_all\_unnamed + +```python +def load_all_unnamed(packages_unnamed: Iterable[str], + prefix: Optional[str] = None, + ignore_errors=False) +``` + +Load all packages in packages_unnamed with default names + + + + +#### load\_all\_finalize + +```python +def load_all_finalize(ignore_errors=False) +``` + +Calls all functions registered with @finalize from all loaded modules in the order they were loaded + +This must be executed after the last plugin package is loaded + + + + +#### close\_down + +```python +def close_down(**kwargs) -> Any +``` + +Calls all functions registered with @atexit from all loaded modules in reverse order of module load order + +Modules are processed in reverse order. Several at-exit tagged functions of a single module are processed +in the order of registration. + +Errors raised in functions are suppressed to ensure all plugins are processed + + + + +#### call + +```python +def call(package: str, + plugin: str, + method: Optional[str] = None, + *, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None) -> Any +``` + +Call a function/method from the loaded plugins + +If a plugin is a function or a callable instance of a class, this is equivalent to + +``package.plugin(*args, **kwargs)`` + +If plugin is a class instance from which a method is called, this is equivalent to the followig. +Also remember, that method must have the attribute ``plugin_callable = True`` + +``package.plugin.method(*args, **kwargs)`` + +Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. + +> [!NOTE] +> There is no logger in this function as they all belong up-level where the exceptions are handled. +> If you want logger messages instead of exceptions, use :func:`call_ignore_errors` + +**Arguments**: + +- `package`: Name of the plugin package in which to look for function/class instance +- `plugin`: Function name or instance name of a class +- `method`: Method name when accessing a class instance' method. Leave at *None* if unneeded. +- `as_thread`: Run the callable in separate daemon thread. +There is no return value from the callable in this case! The return value is the thread object. +Also note that Exceptions in the Thread must be handled in the Thread and are not propagated to the main Thread. +All threads are started as daemon threads with terminate upon main program termination. +There is not stop-thread mechanism. This is intended for short lived threads. +- `thread_name`: Name of the thread +- `args`: Arguments passed to callable +- `kwargs`: Keyword arguments passed to callable + +**Returns**: + +The return value from the called function, or, if started as thread the thread object + + + +#### call\_ignore\_errors + +```python +def call_ignore_errors(package: str, + plugin: str, + method: Optional[str] = None, + *, + args=(), + kwargs=None, + as_thread: bool = False, + thread_name: Optional[str] = None) -> Any +``` + +Call a function/method from the loaded plugins ignoring all raised Exceptions. + +Errors get logged. + +See :func:`call` for parameter documentation. + + + + +#### exists + +```python +def exists(package: str, + plugin: Optional[str] = None, + method: Optional[str] = None) -> bool +``` + +Check if an object is registered within the plugs package + + + + +#### get + +```python +def get(package: str, + plugin: Optional[str] = None, + method: Optional[str] = None) -> Any +``` + +Get a plugs-package registered object + +The return object depends on the number of parameters + +* 1 argument: Get the python module reference for the plugs *package* +* 2 arguments: Get the plugin reference for the plugs *package.plugin* +* 3 arguments: Get the plugin reference for the plugs *package.plugin.method* + + + + +#### loaded\_as + +```python +def loaded_as(module_name: str) -> str +``` + +Return the plugin name a python module is loaded as + + + + +#### delete + +```python +def delete(package: str, plugin: Optional[str] = None, ignore_errors=False) +``` + +Delete a plugin object from the registered plugs callables + +> [!NOTE] +> This does not 'unload' the python module. It merely makes it un-callable via plugs! + + + + +#### dump\_plugins + +```python +def dump_plugins(stream) +``` + +Write a human readable summary of all plugin callables to stream + + + + +#### summarize + +```python +def summarize() +``` + +Create a reference summary of all plugin callables in dictionary format + + + + +#### generate\_help\_rst + +```python +def generate_help_rst(stream) +``` + +Write a reference of all plugin callables in Restructured Text format + + + + +#### get\_all\_loaded\_packages + +```python +def get_all_loaded_packages() -> Dict[str, str] +``` + +Report a short summary of all loaded packages + +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` + + + +#### get\_all\_failed\_packages + +```python +def get_all_failed_packages() -> Dict[str, str] +``` + +Report those packages that did not load error free + +> [!NOTE] +> Package could fail to load +> * altogether: these package are not registered +> * partially: during initializer, finalizer functions: The package is loaded, +> but the function did not execute error-free +> +> Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED + +**Returns**: + +Dictionary of the form `{loaded_as: loaded_from, ...}` + + + +# jukebox.speaking\_text + +Text to Speech. Plugin to speak any given text via speaker + + + + +# jukebox.multitimer + +Multitimer Module + + + + +## MultiTimer Objects + +```python +class MultiTimer(threading.Thread) +``` + +Call a function after a specified number of seconds, repeat that iteration times + +May be cancelled during any of the wait times. +Function is called with keyword parameter 'iteration' (which decreases down to 0 for the last iteration) + +If iterations is negative, an endlessly repeating timer is created (which needs to be cancelled with cancel()) + +Initiates start and publishing by calling self.publish_callback + +Note: Inspired by threading.Timer and generally using the same API + + + + +#### cancel + +```python +def cancel() +``` + +Stop the timer if it hasn't finished all iterations yet. + + + + +## GenericTimerClass Objects + +```python +class GenericTimerClass() +``` + +Interface for plugin / RPC accessibility for a single event timer + + + + +#### \_\_init\_\_ + +```python +def __init__(name, wait_seconds: float, function, args=None, kwargs=None) +``` + +**Arguments**: + +- `wait_seconds`: The time in seconds to wait before calling function +- `function`: The function to call with args and kwargs. +- `args`: Parameters for function call +- `kwargs`: Parameters for function call + + + +#### start + +```python +@plugin.tag +def start(wait_seconds=None) +``` + +Start the timer (with default or new parameters) + + + + +#### cancel + +```python +@plugin.tag +def cancel() +``` + +Cancel the timer + + + + +#### toggle + +```python +@plugin.tag +def toggle() +``` + +Toggle the activation of the timer + + + + +#### trigger + +```python +@plugin.tag +def trigger() +``` + +Trigger the next target execution before the time is up + + + + +#### is\_alive + +```python +@plugin.tag +def is_alive() +``` + +Check if timer is active + + + + +#### get\_timeout + +```python +@plugin.tag +def get_timeout() +``` + +Get the configured time-out + +**Returns**: + +The total wait time. (Not the remaining wait time!) + + + +#### set\_timeout + +```python +@plugin.tag +def set_timeout(wait_seconds: float) +``` + +Set a new time-out in seconds. Re-starts the timer if already running! + + + + +#### publish + +```python +@plugin.tag +def publish() +``` + +Publish the current state and config + + + + +#### get\_state + +```python +@plugin.tag +def get_state() +``` + +Get the current state and config as dictionary + + + + +## GenericEndlessTimerClass Objects + +```python +class GenericEndlessTimerClass(GenericTimerClass) +``` + +Interface for plugin / RPC accessibility for an event timer call function endlessly every m seconds + + + + +## GenericMultiTimerClass Objects + +```python +class GenericMultiTimerClass(GenericTimerClass) +``` + +Interface for plugin / RPC accessibility for an event timer that performs an action n times every m seconds + + + + +#### \_\_init\_\_ + +```python +def __init__(name, + iterations: int, + wait_seconds_per_iteration: float, + callee, + args=None, + kwargs=None) +``` + +**Arguments**: + +- `iterations`: Number of times callee is called +- `wait_seconds_per_iteration`: Wait in seconds before each iteration +- `callee`: A builder class that gets instantiated once as callee(*args, iterations=iterations, **kwargs). +Then with every time out iteration __call__(*args, iteration=iteration, **kwargs) is called. +'iteration' is the current iteration count in decreasing order! +- `args`: +- `kwargs`: + + + +#### start + +```python +@plugin.tag +def start(iterations=None, wait_seconds_per_iteration=None) +``` + +Start the timer (with default or new parameters) + + + + +# jukebox.utils + +Common utility functions + + + + +#### decode\_rpc\_call + +```python +def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict] +``` + +Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. + +> [!IMPORTANT] +> Leaves all other parameters in cfg_action untouched or later downstream processing! + +**Arguments**: + +- `cfg_rpc_call`: RPC command as configuration entry + +**Returns**: + +A fully populated deep copy of cfg_rpc_call + + + +#### decode\_rpc\_command + +```python +def decode_rpc_command(cfg_rpc_cmd: Dict, + logger: logging.Logger = log) -> Optional[Dict] +``` + +Decode an RPC Command from a config entry. + +This means + +* Decode RPC command alias (if present) +* Ensure all RPC call parameters have valid default values + +If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call +which emits a misuse warning when called +If an explicitly specified this is not done. However, it is ensured that the returned +dictionary contains all mandatory parameters for an RPC call. RPC call functions have error handling +for non-existing RPC commands and we get a clearer error message. + +**Arguments**: + +- `cfg_rpc_cmd`: RPC command as configuration entry +- `logger`: The logger to use + +**Returns**: + +A decoded, fully populated deep copy of cfg_rpc_cmd + + + +#### decode\_and\_call\_rpc\_command + +```python +def decode_and_call_rpc_command(rpc_cmd: Dict, logger: logging.Logger = log) +``` + +Convenience function combining decode_rpc_command and plugs.call_ignore_errors + + + + +#### bind\_rpc\_command + +```python +def bind_rpc_command(cfg_rpc_cmd: Dict, + dereference=False, + logger: logging.Logger = log) +``` + +Decode an RPC command configuration entry and bind it to a function + +**Arguments**: + +- `dereference`: Dereference even the call to plugs.call(...) + ``. If false, the returned function is ``plugs.call(package, plugin, method, *args, **kwargs)`` with + all checks applied at bind time + ``. If true, the returned function is ``package.plugin.method(*args, **kwargs)`` with + all checks applied at bind time. + +Setting deference to True, circumvents the dynamic nature of the plugins: the function to call + must exist at bind time and cannot change. If False, the function to call must only exist at call time. + This can be important during the initialization where package ordering and initialization means that not all + classes have been instantiated yet. With dereference=True also the plugs thread lock for serialization of calls + is circumvented. Use with care! + +**Returns**: + +Callable function w/o parameters which directly runs the RPC command +using plugs.call_ignore_errors + + + +#### rpc\_call\_to\_str + +```python +def rpc_call_to_str(cfg_rpc_call: Dict, with_args=True) -> str +``` + +Return a readable string of an RPC call config + +**Arguments**: + +- `cfg_rpc_call`: RPC call configuration entry +- `with_args`: Return string shall include the arguments of the function + + + +#### generate\_cmd\_alias\_rst + +```python +def generate_cmd_alias_rst(stream) +``` + +Write a reference of all rpc command aliases in Restructured Text format + + + + +#### generate\_cmd\_alias\_reference + +```python +def generate_cmd_alias_reference(stream) +``` + +Write a reference of all rpc command aliases in text format + + + + +#### get\_git\_state + +```python +def get_git_state() +``` + +Return git state information for the current branch + + + + +# jukebox.rpc + + + +# jukebox.rpc.client + + + +# jukebox.rpc.server + +## Remote Procedure Call Server (RPC) + +Bind to tcp and/or websocket port and translates incoming requests to procedure calls. +Avaiable procedures to call are all functions registered with the plugin package. + +The protocol is loosely based on [jsonrpc](https://www.jsonrpc.org/specification) + +But with different elements directly relating to the plugin concept and Python function argument options + + { + 'package' : str # The plugin package loaded from python module + 'plugin' : str # The plugin object to be accessed from the package + # (i.e. function or class instance) + 'method' : str # (optional) The method of the class instance + 'args' : [ ] # (optional) Positional arguments as list + 'kwargs' : { } # (optional) Keyword arguments as dictionary + 'as_thread': bool # (optional) start call in separate thread + 'id' : Any # (optional) Round-trip id for response (may not be None) + 'tsp' : Any # (optional) measure and return total processing time for + # the call request (may not be None) + } + +**Response** + +A response will ALWAYS be send, independent of presence of 'id'. This is in difference to the +jsonrpc specification. But this is a ZeroMQB REQ/REP pattern requirement! + +If 'id' is omitted, the response will be 'None'! Unless an error occurred, then the error is returned. +The absence of 'id' indicates that the requester is not interested in the response. +If present, 'id' and 'tsp' may not be None. If they are None, there are treated as if non-existing. + +**Sockets** + +Three sockets are opened + +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be + call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though + the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which + button triggers what action) + + + + +## RpcServer Objects + +```python +class RpcServer() +``` + +The RPC Server Class + + + + +#### \_\_init\_\_ + +```python +def __init__(context=None) +``` + +Initialize the connections and bind to the ports + + + + +#### run + +```python +def run() +``` + +The main endless loop waiting for requests and forwarding the + +call request to the plugin module + + diff --git a/documentation/developers/known-issues.md b/documentation/developers/known-issues.md index 817298c60..db3429bcc 100644 --- a/documentation/developers/known-issues.md +++ b/documentation/developers/known-issues.md @@ -16,6 +16,8 @@ RUN cd ${HOME} && mkdir ${ZMQ_TMP_DIR} && cd ${ZMQ_TMP_DIR}; \ make && make install ``` +[libzmq details](./libzmq.md) + ## Configuration In `jukebox.yaml` (and all other config files): diff --git a/pydoc-markdown.yml b/pydoc-markdown.yml new file mode 100644 index 000000000..62519e694 --- /dev/null +++ b/pydoc-markdown.yml @@ -0,0 +1,13 @@ +loaders: +- type: python + search_path: [./src/jukebox] +processors: + - type: filter +# skip_empty_modules: true # Uncommenting this skips also run_jukebox etc. + - type: sphinx + - type: crossref +renderer: + type: markdown + render_toc: true + filename: ./documentation/developers/docstring/README.md + render_page_title: true diff --git a/requirements.txt b/requirements.txt index ea9546315..c172a7636 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,3 +34,6 @@ flake8>=4.0.0 pytest pytest-cov mock + +# API docs generation +pydoc-markdown diff --git a/run_docgeneration.sh b/run_docgeneration.sh new file mode 100755 index 000000000..22ab8bc14 --- /dev/null +++ b/run_docgeneration.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Runner script for pydoc-markdown to ensure +# - independent from working directory + +# Change working directory to location of script +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) + +# Run pydoc-markdown +# make sure, directory exists +mkdir -p ./documentation/developers/docstring +# expects pydoc-markdown.yml at working dir +pydoc-markdown diff --git a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py index 04459c9ea..af193a37f 100644 --- a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py +++ b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py @@ -35,42 +35,39 @@ class battmon_ads1015(BatteryMonitorBase.BattmonBase): - '''Battery Monitor based on a ADS1015 + """Battery Monitor based on a ADS1015 - CAUTION - WARNING - ======================================================================== - Lithium and other batteries are dangerous and must be treated with care. - Rechargeable Lithium Ion batteries are potentially hazardous and can - present a serious FIRE HAZARD if damaged, defective or improperly used. - Do not use this circuit to a lithium ion battery without expertise and - training in handling and use of batteries of this type. - Use appropriate test equipment and safety protocols during development. - - There is no warranty, this may not work as expected or at all! - ========================================================================= + > [!CAUTION] + > Lithium and other batteries are dangerous and must be treated with care. + > Rechargeable Lithium Ion batteries are potentially hazardous and can + > present a serious **FIRE HAZARD** if damaged, defective or improperly used. + > Do not use this circuit to a lithium ion battery without expertise and + > training in handling and use of batteries of this type. + > Use appropriate test equipment and safety protocols during development. + > There is no warranty, this may not work as expected or at all! This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === Attention: - - the circuit is constantly draining the battery! (leak current up to: 2.1µA) - - the time between sample needs to be a minimum 1sec with this high impedance voltage divider + * the circuit is constantly draining the battery! (leak current up to: 2.1µA) + * the time between sample needs to be a minimum 1sec with this high impedance voltage divider don't use the continuous conversion method! - ''' + """ def __init__(self, cfg): super().__init__(cfg, logger) diff --git a/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py b/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py index 999a4f218..4d17f398e 100644 --- a/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py +++ b/src/jukebox/components/controls/bluetooth_audio_buttons/__init__.py @@ -4,9 +4,9 @@ This effectively does: - * register a callback with components.volume to get notified when a new sound card connects - * if that is a bluetooth device, try opening an input device with similar name using - * button listeners are run each in its own thread +* register a callback with components.volume to get notified when a new sound card connects +* if that is a bluetooth device, try opening an input device with similar name using +* button listeners are run each in its own thread """ import logging diff --git a/src/jukebox/components/controls/common/evdev_listener.py b/src/jukebox/components/controls/common/evdev_listener.py index 05b92005c..a4279afda 100644 --- a/src/jukebox/components/controls/common/evdev_listener.py +++ b/src/jukebox/components/controls/common/evdev_listener.py @@ -49,10 +49,8 @@ def _filter_by_device_name(all_devices: List[evdev.InputDevice], def find_device(device_name: str, exact_name: bool = True, mandatory_keys: Optional[Set[int]] = None) -> str: """Find an input device with device_name and mandatory keys. - Raises - - #. FileNotFoundError, if no device is found. - #. AttributeError, if device does not have the mandatory keys + :raise FileNotFoundError: if no device is found. + :raise AttributeError: if device does not have the mandatory key If multiple devices match, the first match is returned diff --git a/src/jukebox/components/gpio/gpioz/core/converter.py b/src/jukebox/components/gpio/gpioz/core/converter.py index ba9581113..849bc8e17 100644 --- a/src/jukebox/components/gpio/gpioz/core/converter.py +++ b/src/jukebox/components/gpio/gpioz/core/converter.py @@ -43,8 +43,6 @@ class VolumeToRGB: Map input :data:`0...100` to color range :data:`green...magenta` and get the color for level 50 - .. code-block:: python - conv = VolumeToRGB(100, offset=120, section=180) (r, g, b) = conv(50) diff --git a/src/jukebox/components/gpio/gpioz/core/input_devices.py b/src/jukebox/components/gpio/gpioz/core/input_devices.py index 5090762f4..bae049cc5 100644 --- a/src/jukebox/components/gpio/gpioz/core/input_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/input_devices.py @@ -9,7 +9,8 @@ All callback handlers are replaced by GPIOZ callback handlers. These are usually configured by using the :func:`set_rpc_actions` each input device exhibits. -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Input devices` +For examples how to use the devices from the configuration files, see +[GPIO: Input Devices](../../builders/gpio.md#input-devices). """ import functools @@ -75,7 +76,7 @@ def set_rpc_actions(self, action_config) -> None: Set all input device callbacks from :attr:`action_config` :param action_config: Dictionary with one - :ref:`RPC Command ` definition entry for every device callback + [RPC Commands](../../builders/rpc-commands.md) definition entry for every device callback """ pass @@ -233,11 +234,11 @@ class LongPressButton(NameMixin, ButtonBase): """ A Button that runs a single actions only when the button is pressed long enough - :param pull_up: See `Button`_ + :param pull_up: See #Button - :param active_state: See `Button`_ + :param active_state: See #Button - :param bounce_time: See `Button`_ + :param bounce_time: See #Button :param hold_repeat: If :data:`True` repeat the :attr:`on_press` every :attr:`hold_time` seconds. Else only action is run only once independent of the length of time the button is pressed for. @@ -291,11 +292,11 @@ class ShortLongPressButton(NameMixin, ButtonBase): event. Furthermore, if there is a long hold, only the long hold action is executed - the short press action is not run in this case! - :param pull_up: See `Button`_ + :param pull_up: See #Button - :param active_state: See `Button`_ + :param active_state: See #Button - :param bounce_time: See `Button`_ + :param bounce_time: See #Button :param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the @@ -304,9 +305,9 @@ class ShortLongPressButton(NameMixin, ButtonBase): :param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press action - :param pin_factory: See `Button`_ + :param pin_factory: See #Button - :param name: See `Button`_ + :param name: See #Button """ def __init__( self, pin=None, *, pull_up=True, active_state=None, bounce_time=None, @@ -370,11 +371,11 @@ class RotaryEncoder(NameMixin): """ A rotary encoder to run one of two actions depending on the rotation direction. - :param bounce_time: See `Button`_ + :param bounce_time: See #Button - :param pin_factory: See `Button`_ + :param pin_factory: See #Button - :param name: See `Button`_ + :param name: See #Button """ def __init__(self, a, b, *, bounce_time=None, pin_factory=None, name=None): super().__init__(name=name) @@ -442,11 +443,11 @@ class TwinButton(NameMixin): It is not necessary to configure all actions. - :param pull_up: See `Button`_ + :param pull_up: See #Button - :param active_state: See `Button`_ + :param active_state: See #Button - :param bounce_time: See `Button`_ + :param bounce_time: See #Button :param hold_time: The time in seconds to differentiate if it is a short or long press. If the button is released before this time, it is a short press. As soon as the button is held for :attr:`hold_time` it is a long press and the @@ -455,9 +456,9 @@ class TwinButton(NameMixin): :param hold_repeat: If :data:`True` repeat the long press action every :attr:`hold_time` seconds after first long press action. A long dual press is never repeated independent of this setting - :param pin_factory: See `Button`_ + :param pin_factory: See #Button - :param name: See `Button`_ + :param name: See #Button """ class StateVar(Enum): diff --git a/src/jukebox/components/gpio/gpioz/core/mock.py b/src/jukebox/components/gpio/gpioz/core/mock.py index bccd5e0e1..ae2e49e15 100644 --- a/src/jukebox/components/gpio/gpioz/core/mock.py +++ b/src/jukebox/components/gpio/gpioz/core/mock.py @@ -19,7 +19,8 @@ def patch_mock_outputs_with_callback(): This targets to represent the state in the TK GUI. Other output devices cannot be represented in the GUI and are silently ignored. - ..note:: Only for developing purposes!""" + > [!NOTE] + > Only for developing purposes!""" gpiozero.LED._write_orig = gpiozero.LED._write gpiozero.LED._write = rewrite gpiozero.LED.on_change_callback = None diff --git a/src/jukebox/components/gpio/gpioz/core/output_devices.py b/src/jukebox/components/gpio/gpioz/core/output_devices.py index 50949f82b..78f1d23da 100644 --- a/src/jukebox/components/gpio/gpioz/core/output_devices.py +++ b/src/jukebox/components/gpio/gpioz/core/output_devices.py @@ -11,7 +11,8 @@ with parameters for this device and optional parameters from another device. Unused/unsupported parameters are silently ignored. This is done to reduce the amount of coding required for connectivity functions. -For examples how to use the devices from the configuration files, see :ref:`userguide/gpioz:Output devices` +For examples how to use the devices from the configuration files, see +[GPIO: Output Devices](../../builders/gpio.md#output-devices). """ from typing import Optional, List diff --git a/src/jukebox/components/gpio/gpioz/plugin/__init__.py b/src/jukebox/components/gpio/gpioz/plugin/__init__.py index 9bc151e55..6fc9ab973 100644 --- a/src/jukebox/components/gpio/gpioz/plugin/__init__.py +++ b/src/jukebox/components/gpio/gpioz/plugin/__init__.py @@ -56,15 +56,15 @@ class ServiceIsRunningCallbacks(CallbackHandler): """ Callbacks are executed when - * Jukebox app started - * Jukebox shuts down + * Jukebox app started + * Jukebox shuts down This is intended to e.g. signal an LED to change state. This is integrated into this module because: - * we need the GPIO to control a LED (it must be available when the status callback comes) - * the plugin callback functions provide all the functionality to control the status of the LED - * which means no need to adapt other modules + * we need the GPIO to control a LED (it must be available when the status callback comes) + * the plugin callback functions provide all the functionality to control the status of the LED + * which means no need to adapt other modules """ def register(self, func: Callable[[int], None]): @@ -76,7 +76,7 @@ def register(self, func: Callable[[int], None]): .. py:function:: func(status: int) :noindex: - :param status: 1 if app started, 0 if app shuts down + :param status: 1 if app started, 0 if app shuts down """ super().register(func) diff --git a/src/jukebox/components/gpio/gpioz/plugin/connectivity.py b/src/jukebox/components/gpio/gpioz/plugin/connectivity.py index 3e5baea2d..abbcb1a32 100644 --- a/src/jukebox/components/gpio/gpioz/plugin/connectivity.py +++ b/src/jukebox/components/gpio/gpioz/plugin/connectivity.py @@ -55,11 +55,11 @@ def register_rfid_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.LED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.Buzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def rfid_callback(card_id: str, state: RfidCardDetectState): @@ -78,9 +78,9 @@ def register_status_led_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.LED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` """ def set_status_led(state): @@ -101,8 +101,8 @@ def register_status_buzzer_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.Buzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def set_status_buzzer(state): @@ -121,7 +121,7 @@ def register_status_tonalbuzzer_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def set_status_buzzer(state): @@ -143,9 +143,9 @@ def register_audio_sink_change_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.LED` - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.LED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` """ def audio_sink_change_callback(alias, sink_name, sink_index, error_state): @@ -167,7 +167,7 @@ def register_volume_led_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.PWMLED` + * :class:`components.gpio.gpioz.core.output_devices.PWMLED` """ def audio_volume_change_callback(volume, is_min, is_max): @@ -191,8 +191,8 @@ def register_volume_buzzer_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.Buzzer` - - :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` + * :class:`components.gpio.gpioz.core.output_devices.Buzzer` + * :class:`components.gpio.gpioz.core.output_devices.TonalBuzzer` """ def set_volume_buzzer(volume, is_min, is_max): @@ -210,7 +210,7 @@ def register_volume_rgbled_callback(device): Compatible devices: - - :class:`components.gpio.gpioz.core.output_devices.RGBLED` + * :class:`components.gpio.gpioz.core.output_devices.RGBLED` """ volume_to_rgb = VolumeToRGB(100, 120, 180) diff --git a/src/jukebox/components/hostif/linux/__init__.py b/src/jukebox/components/hostif/linux/__init__.py index 582074413..32dfded08 100644 --- a/src/jukebox/components/hostif/linux/__init__.py +++ b/src/jukebox/components/hostif/linux/__init__.py @@ -103,7 +103,8 @@ def jukebox_is_service(): def is_any_jukebox_service_active(): """Check if a Jukebox service is running - .. note:: Does not have the be the current app, that is running as a service! + > [!NOTE] + > Does not have the be the current app, that is running as a service! """ ret = subprocess.run(["systemctl", "--user", "show", "jukebox-daemon", "--property", "ActiveState", "--value"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, diff --git a/src/jukebox/components/jingle/__init__.py b/src/jukebox/components/jingle/__init__.py index 43e55cd74..940015dd5 100644 --- a/src/jukebox/components/jingle/__init__.py +++ b/src/jukebox/components/jingle/__init__.py @@ -56,11 +56,12 @@ def initialize(): def play(filename): """Play the jingle using the configured jingle service - Note: This runs in a separate thread. And this may cause troubles - when changing the volume level before - and after the sound playback: There is nothing to prevent another - thread from changing the volume and sink while playback happens - and afterwards we change the volume back to where it was before! + > [!NOTE] + > This runs in a separate thread. And this may cause troubles + > when changing the volume level before + > and after the sound playback: There is nothing to prevent another + > thread from changing the volume and sink while playback happens + > and afterwards we change the volume back to where it was before! There is no way around this dilemma except for not running the jingle as a separate thread. Currently (as thread) even the RPC is started before the sound diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index a2dbc914a..9073a9b4a 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -367,8 +367,9 @@ 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""" + > [!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']) diff --git a/src/jukebox/components/playermpd/playcontentcallback.py b/src/jukebox/components/playermpd/playcontentcallback.py index a60452a23..ce5a1b8fb 100644 --- a/src/jukebox/components/playermpd/playcontentcallback.py +++ b/src/jukebox/components/playermpd/playcontentcallback.py @@ -27,8 +27,8 @@ def register(self, func: Callable[[str, STATE], None]): .. py:function:: func(folder: str, state: STATE) :noindex: - :param folder: relativ path to folder to play - :param state: indicator of the state inside the calling + :param folder: relativ path to folder to play + :param state: indicator of the state inside the calling """ super().register(func) diff --git a/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py b/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py index b33399b0a..a4481efd6 100644 --- a/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py +++ b/src/jukebox/components/rfid/hardware/template_new_reader/template_new_reader.py @@ -48,7 +48,7 @@ class ReaderClass(ReaderBaseClass): All the required interfaces are implemented there. Put your code into these functions (see below for more information) - - __init__ + - `__init__` - read_card - cleanup - stop @@ -101,10 +101,11 @@ def stop(self): This function is called before cleanup is called. - .. note: This is usually called from a different thread than the reader's thread! And this is the reason for the - two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt - to read a card. Once called, the function read_card will not be called again. When the reader thread exits - cleanup is called from the reader thread itself. + > [!NOTE] + > This is usually called from a different thread than the reader's thread! And this is the reason for the + > two-step exit strategy. This function works across threads to indicate to the reader that is should stop attempt + > to read a card. Once called, the function read_card will not be called again. When the reader thread exits + > cleanup is called from the reader thread itself. """ self._keep_running = False diff --git a/src/jukebox/components/rfid/reader/__init__.py b/src/jukebox/components/rfid/reader/__init__.py index db0ccb1da..37d4a363d 100644 --- a/src/jukebox/components/rfid/reader/__init__.py +++ b/src/jukebox/components/rfid/reader/__init__.py @@ -41,8 +41,8 @@ def register(self, func: Callable[[str, RfidCardDetectState], None]): .. py:function:: func(card_id: str, state: int) :noindex: - :param card_id: Card ID - :param state: See :class:`RfidCardDetectState` + :param card_id: Card ID + :param state: See #RfidCardDetectState """ super().register(func) @@ -52,7 +52,7 @@ def run_callbacks(self, card_id: str, state: RfidCardDetectState): #: Callback handler instance for rfid_card_detect_callbacks events. -#: See :class:`RfidCardDetectCallbacks` +#: See #RfidCardDetectCallbacks rfid_card_detect_callbacks: RfidCardDetectCallbacks = RfidCardDetectCallbacks('rfid_card_detect_callbacks', log) diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index e56727ff4..f6e238559 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -1,7 +1,7 @@ """ This file provides definitions for RPC command aliases -See :ref:`userguide/rpc_commands` +See [RPC Commands](../../builders/rpc-commands.md) """ # -------------------------------------------------------------- diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index 6653baa77..ccc4873d7 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -2,33 +2,35 @@ # Copyright (c) See file LICENSE in project root folder """PulseAudio Volume Control Plugin Package -Features +## Features - * Volume Control - * Two outputs - * Watcher thread on volume / output change +* Volume Control +* Two outputs +* Watcher thread on volume / output change -Publishes +## Publishes - * volume.level - * volume.sink +* volume.level +* volume.sink -PulseAudio References +## PulseAudio References -https://brokkr.net/2018/05/24/down-the-drain-the-elusive-default-pulseaudio-sink/ + Check fallback device (on device de-connect): -$ pacmd list-sinks | grep -e 'name:' -e 'index' + $ pacmd list-sinks | grep -e 'name:' -e 'index' -Integration + +## Integration Pulse Audio runs as a user process. Processes who want to communicate / stream to it must also run as a user process. -This means must also run as user process, as described in :ref:`userguide/system:Music Player Daemon (MPD)` +This means must also run as user process, as described in +[Music Player Daemon](../../builders/system.md#music-player-daemon-mpd). -Misc +## Misc PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration @@ -36,27 +38,25 @@ If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs from the Jukebox. Remove it from the configuration! -.. code-block:: text - ### Use hot-plugged devices like Bluetooth or USB automatically (LP: #1702794) ### not available on PI? .ifexists module-switch-on-connect.so load-module module-switch-on-connect .endif -Why PulseAudio? +## Why PulseAudio? The audio configuration of the system is one of those topics, which has a myriad of options and possibilities. Every system is different and PulseAudio unifies these and makes our life easier. Besides, it is only option to support Bluetooth at the moment. -Callbacks: +## Callbacks: The following callbacks are provided. Register callbacks with these adder functions (see their documentation for details): - #. :func:`add_on_connect_callback` - #. :func:`add_on_output_change_callbacks` - #. :func:`add_on_volume_change_callback` +1. :func:`add_on_connect_callback` +2. :func:`add_on_output_change_callbacks` +3. :func:`add_on_volume_change_callback` """ import collections import logging @@ -116,10 +116,10 @@ def register(self, func: Callable[[str, str], None]): .. py:function:: func(card_driver: str, device_name: str) :noindex: - :param card_driver: The PulseAudio card driver module, - e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` - :param device_name: The sound card device name as reported - in device properties + :param card_driver: The PulseAudio card driver module, + e.g. :data:`module-bluez5-device.c` or :data:`module-alsa-card.c` + :param device_name: The sound card device name as reported + in device properties """ super().register(func) @@ -140,7 +140,7 @@ def __init__(self): # For the callback handler: We use the context lock only explicitly for registering new functions # When the callbacks are run, it happens from inside the pulse_monitor which an already acquired lock #: Callback handler instance for on_connect_callbacks events. - #: See :class:`PulseMonitor.SoundCardConnectCallbacks` + #: See #PulseMonitor.SoundCardConnectCallbacks self.on_connect_callbacks: PulseMonitor.SoundCardConnectCallbacks = PulseMonitor.SoundCardConnectCallbacks( 'on_connect_callback', logger, context=self) @@ -149,10 +149,11 @@ def toggle_on_connect(self): """Returns :data:`True` if the sound card shall be changed when a new card connects/disconnects. Setting this property changes the behavior. - .. note:: A new card is always assumed to be the secondary device from the audio configuration. - At the moment there is no check it actually is the configured device. This means any new - device connection will initiate the toggle. This, however, is no real issue as the RPi's audio - system will be relatively stable once setup + > [!NOTE] + > A new card is always assumed to be the secondary device from the audio configuration. + > At the moment there is no check it actually is the configured device. This means any new + > device connection will initiate the toggle. This, however, is no real issue as the RPi's audio + > system will be relatively stable once setup """ return self._toggle_on_connect @@ -282,8 +283,6 @@ class PulseVolumeControl: When accessing the pulse library, it needs to be put into a special state. Which is ensured by the context manager - .. code-block: python - with pulse_monitor as pulse ... @@ -309,12 +308,12 @@ def register(self, func: Callable[[str, str, int, int], None]): .. py:function:: func(sink_name: str, alias: str, sink_index: int, error_state: int) :noindex: - :param sink_name: PulseAudio's sink name - :param alias: The alias for :attr:`sink_name` - :param sink_index: The index of the sink in the configuration list - :param error_state: 1 if there was an attempt to change the output - but an error occurred. Above parameters always give the now valid sink! - If a sink change is successful, it is 0. + :param sink_name: PulseAudio's sink name + :param alias: The alias for :attr:`sink_name` + :param sink_index: The index of the sink in the configuration list + :param error_state: 1 if there was an attempt to change the output + but an error occurred. Above parameters always give the now valid sink! + If a sink change is successful, it is 0. """ super().register(func) @@ -338,9 +337,9 @@ def register(self, func: Callable[[int, bool, bool], None]): .. py:function:: func(volume: int, is_min: bool, is_max: bool) :noindex: - :param volume: Volume level - :param is_min: 1, if volume level is minimum, else 0 - :param is_max: 1, if volume level is maximum, else 0 + :param volume: Volume level + :param is_min: 1, if volume level is minimum, else 0 + :param is_max: 1, if volume level is maximum, else 0 """ super().register(func) @@ -359,12 +358,12 @@ def __init__(self, sink_list: List[PulseAudioSinkClass]): # When the callbacks are run, it happens from inside the pulse_control which an already acquired lock #: Callback handler instance for on_output_change_callbacks events. - #: See :class:`PulseVolumeControl.OutputChangeCallbackHandler` + #: See #PulseVolumeControl.OutputChangeCallbackHandler self.on_output_change_callbacks = PulseVolumeControl.OutputChangeCallbackHandler( 'on_output_change_callbacks', logger, context=pulse_monitor) #: Callback handler instance for on_output_change_callbacks events. - #: See :class:`PulseVolumeControl.OutputVolumeCallbackHandler` + #: See #PulseVolumeControl.OutputVolumeCallbackHandler self.on_volume_change_callbacks = PulseVolumeControl.OutputVolumeCallbackHandler( 'on_volume_change_callbacks', logger, context=pulse_monitor) diff --git a/src/jukebox/jukebox/cfghandler.py b/src/jukebox/jukebox/cfghandler.py index 3482a1b42..8108f1d33 100644 --- a/src/jukebox/jukebox/cfghandler.py +++ b/src/jukebox/jukebox/cfghandler.py @@ -236,8 +236,9 @@ def is_modified(self) -> bool: """ Check if the data has changed since the last load/store - .. note: This relies on the *__str__* representation of the underlying data structure - In case of ruamel, this ignores comments and only looks at the data + > [!NOTE] + > This relies on the *__str__* representation of the underlying data structure + > In case of ruamel, this ignores comments and only looks at the data """ with self._lock: is_modified_value = self._hash != hashlib.md5(self._data.__str__().encode('utf8')).digest() diff --git a/src/jukebox/jukebox/playlistgenerator.py b/src/jukebox/jukebox/playlistgenerator.py index db64d3eff..b9f0223c6 100755 --- a/src/jukebox/jukebox/playlistgenerator.py +++ b/src/jukebox/jukebox/playlistgenerator.py @@ -12,8 +12,6 @@ An directory may contain a mixed set of files and multiple ``*.txt`` files, e.g. -.. code-block:: bash - 01-livestream.txt 02-livestream.txt music.mp3 diff --git a/src/jukebox/jukebox/plugs.py b/src/jukebox/jukebox/plugs.py index afad5da28..5e4a95f21 100644 --- a/src/jukebox/jukebox/plugs.py +++ b/src/jukebox/jukebox/plugs.py @@ -14,39 +14,39 @@ If you want to provide additional functionality to the same feature (probably even for run-time switching) you can implement a Factory Pattern using this package. Take a look at volume.py as an example. -**Example:** Decorate a function for auto-registering under it's own name:: +**Example:** Decorate a function for auto-registering under it's own name: import jukebox.plugs as plugs @plugs.register def func1(param): pass -**Example:** Decorate a function for auto-registering under a new name:: +**Example:** Decorate a function for auto-registering under a new name: @plugs.register(name='better_name') def func2(param): pass -**Example:** Register a function during run-time under it's own name:: +**Example:** Register a function during run-time under it's own name: def func3(param): pass plugs.register(func3) -**Example:** Register a function during run-time under a new name:: +**Example:** Register a function during run-time under a new name: def func4(param): pass plugs.register(func4, name='other_name', package='other_package') **Example:** Decorate a class for auto registering during initialization, -including all methods (see _register_class for more info):: +including all methods (see _register_class for more info): @plugs.register(auto_tag=True) class MyClass1: pass -**Example:** Register a class instance, from which only report is a callable method through the plugs interface:: +**Example:** Register a class instance, from which only report is a callable method through the plugs interface: class MyClass2: @plugs.tag @@ -57,20 +57,17 @@ def report(self): Naming convention: -package - 1. Either a python package - 2. or a plugin package (which is the python package but probably loaded under a different name inside plugs) - -plugin - 1. An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) - 2. The string name to above object - -name - The string name of the plugin object for registration - -method - 1. In case the object is a class instance a bound method to call from the class instance - 2. The string name to above object +* package + * Either a python package + * or a plugin package (which is the python package but probably loaded under a different name inside plugs) +* plugin + * An object from the package that can be accessed through the plugs call function (i.e. a function or a class instance) + * The string name to above object +* name + * The string name of the plugin object for registration +* method + * In case the object is a class instance a bound method to call from the class instance + * The string name to above object """ @@ -405,15 +402,13 @@ def register(plugin: Optional[Callable] = None, *, 3. ``@plugs.register(auto_tag=bool)``: decorator for a class with 1 arguments 4. ``@plugs.register(name=name, package=package)``: decorator for a function with 1 or 2 arguments 5. ``plugs.register(plugin, name=name, package=package)``: run-time registration of - * function * bound method * class instance For more documentation see the functions - - * :func:`_register_obj` - * :func:`_register_class` + * :func:`_register_obj` + * :func:`_register_class` See the examples in Module :mod:`plugs` how to use this decorator / function @@ -504,9 +499,10 @@ def atexit(func: Callable[[int], Any]) -> Callable[[int], Any]: """ Decorator for functions that shall be called by the plugs package directly after at exit of program. - .. important:: There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called - during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your - shutdown handler. + > [!IMPORTANT] + > There is no automatism as in atexit.atexit. The function plugs.shutdown() must be explicitly called + > during the shutdown procedure of your program. This is by design, so you can choose the exact situation in your + > shutdown handler. The atexit-functions are called with a single integer argument, which is passed down from plugin.exit(int) It is intended for passing down the signal number that initiated the program termination @@ -527,11 +523,11 @@ def load(package: str, load_as: Optional[str] = None, prefix: Optional[str] = No """ Loads a python package as plugin package - Executes a regular python package load. That means a potentially existing __init__.py is executed. - Decorator @register can by used to register functions / classes / class istances as plugin callable - Decorator @initializer can be used to tag functions that shall be called after package loading - Decorator @finalizer can be used to tag functions that shall be called after ALL plugin packges have been loaded - Instead of using @initializer, you may of course use __init__.py + Executes a regular python package load. That means a potentially existing `__init__.py` is executed. + Decorator `@register` can by used to register functions / classes / class istances as plugin callable + Decorator `@initializer` can be used to tag functions that shall be called after package loading + Decorator `@finalizer` can be used to tag functions that shall be called after ALL plugin packges have been loaded + Instead of using `@initializer`, you may of course use `__init__.py` Python packages may be loaded under a different plugs package name. Python packages must be unique and the name under which they are loaded as plugin package also. @@ -723,9 +719,9 @@ def call(package: str, plugin: str, method: Optional[str] = None, *, Calls are serialized by a thread lock. The thread lock is shared with call_ignore_errors. - .. note:: - There is no logger in this function as they all belong up-level where the exceptions are handled. - If you want logger messages instead of exceptions, use :func:`call_ignore_errors` + > [!NOTE] + > There is no logger in this function as they all belong up-level where the exceptions are handled. + > If you want logger messages instead of exceptions, use :func:`call_ignore_errors` :param package: Name of the plugin package in which to look for function/class instance :param plugin: Function name or instance name of a class @@ -824,7 +820,9 @@ def loaded_as(module_name: str) -> str: def delete(package: str, plugin: Optional[str] = None, ignore_errors=False): """Delete a plugin object from the registered plugs callables - Note: This does not 'unload' the python module. It merely makes it un-callable via plugs!""" + > [!NOTE] + > This does not 'unload' the python module. It merely makes it un-callable via plugs! + """ with _lock_module: if exists(package, plugin): if plugin is None: @@ -971,13 +969,13 @@ def get_all_loaded_packages() -> Dict[str, str]: def get_all_failed_packages() -> Dict[str, str]: """Report those packages that did not load error free - .. note:: Package could fail to load - - 1. altogether: these package are not registered - 2. partially: during initializer, finalizer functions: The package is loaded, - but the function did not execute error-free - - Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED + > [!NOTE] + > Package could fail to load + > * altogether: these package are not registered + > * partially: during initializer, finalizer functions: The package is loaded, + > but the function did not execute error-free + > + > Partially loaded packages are listed in both _PLUGINS and _PLUGINS_FAILED :return: Dictionary of the form `{loaded_as: loaded_from, ...}`""" with _lock_module: diff --git a/src/jukebox/jukebox/publishing/server.py b/src/jukebox/jukebox/publishing/server.py index 7db5b5846..66729da6e 100644 --- a/src/jukebox/jukebox/publishing/server.py +++ b/src/jukebox/jukebox/publishing/server.py @@ -1,31 +1,28 @@ """ -Publishing Server -******************** +## Publishing Server The common publishing server for the entire Jukebox using ZeroMQ -Structure ----------------- - -.. code-block:: text - - +-----------------------+ - | functional interface | Publisher - | | - functional interface for single Thread - | PUB | - sends data to publisher (and thus across threads) - +-----------------------+ - | (1) - v - +-----------------------+ - | SUB (bind) | PublishServer - | | - Last Value (LV) Cache - | XPUB (bind) | - Subscriber notification and LV resend - +-----------------------+ - independent thread - | (2) - v - -Connection (1): Internal connection - Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) +### Structure + + +-----------------------+ + | functional interface | Publisher + | | - functional interface for single Thread + | PUB | - sends data to publisher (and thus across threads) + +-----------------------+ + | (1) + v + +-----------------------+ + | SUB (bind) | PublishServer + | | - Last Value (LV) Cache + | XPUB (bind) | - Subscriber notification and LV resend + +-----------------------+ - independent thread + | (2) + v + +#### Connection (1): Internal connection + +Internal connection only - do not use (no, not even inside this App for you own plugins - always bind to the PublishServer) Protocol: Multi-part message @@ -39,10 +36,11 @@ Usually empty, i.e. ``b''``. If not empty the message is treated as command for the PublishServer and the message is not forwarded to the outside. This third part of the message is never forwarded -Connection (2): External connection - Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! - Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will - also get you all the branch topics. To get everything, subscribe to ``b''`` +#### Connection (2): External connection + +Upon connection of a new subscriber, the entire current state is resend from cache to ALL subscribers! +Subscribers must subscribe to topics. Topics are treated as topic trees! Subscribing to a root tree will +also get you all the branch topics. To get everything, subscribe to ``b''`` Protocol: Multi-part message @@ -52,24 +50,22 @@ Part 2: Payload or Message in json serialization If empty (i.e. b''), it means the subscriber must delete this key locally (not valid anymore) -Why? Why? -------------- +### Why? Why? -Check out the `ZeroMQ Documentation `_ +Check out the [ZeroMQ Documentation](https://zguide.zeromq.org/docs/chapter5) for why you need a proxy in a good design. For use case, we made a few simplifications -Design Rationales -------------------- +### Design Rationales -* "If you need `millions of messages per second `_ +* "If you need [millions of messages per second](https://zguide.zeromq.org/docs/chapter5/#Pros-and-Cons-of-Pub-Sub) sent to thousands of points, - you’ll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." + you'll appreciate pub-sub a lot more than if you need a few messages a second sent to a handful of recipients." * "lower-volume network with a few dozen subscribers and a limited number of topics, we can use TCP and then - the `XSUB and XPUB `_" -* "Let’s imagine `our feed has an average of 100,000 100-byte messages a second - `_ [...]. + the [XSUB and XPUB](https://zguide.zeromq.org/docs/chapter5/#Last-Value-Caching)" +* "Let's imagine [our feed has an average of 100,000 100-byte messages a + second](https://zguide.zeromq.org/docs/chapter5/#High-Speed-Subscribers-Black-Box-Pattern) [...]. While 100K messages a second is easy for a ZeroMQ application, ..." **But we have:** @@ -100,8 +96,7 @@ * Publisher plugin is first plugin to be loaded * Due to Publisher - PublisherServer structure no further sequencing required -Plugin interactions and usage ------------------------------- +### Plugin interactions and usage RPC can trigger through function call in components/publishing plugin that @@ -110,23 +105,24 @@ Plugins publishing state information should publish initial state at @plugin.finalize -.. important:: Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is - required per thread. But the publisher instance **must** be thread-local! - Always go through :func:`publishing.get_publisher()`. +> [!IMPORTANT] +> Do not direclty instantiate the Publisher in your plugin module. Only one Publisher is +> required per thread. But the publisher instance **must** be thread-local! +> Always go through :func:`publishing.get_publisher()`. **Sockets** Three sockets are opened: -#. TCP (on a configurable port) -#. Websocket (on a configurable port) -#. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://PublisherToProxy`` all topics are published app-internally. This can be used for plugin modules that want to know about the current state on event based updates. **Further ZeroMQ References:** -* `Working with Messages `_ -* `Multiple Threads `_ +* [Working with Messages](https://zguide.zeromq.org/docs/chapter2/#Working-with-Messages) +* [Multiple Threads](https://zguide.zeromq.org/docs/chapter2/#Multithreading-with-ZeroMQ) """ # Developer's notes: @@ -190,7 +186,7 @@ class PublishServer(threading.Thread): Handles new subscriptions by sending out the entire cached state to **all** subscribers - The code is structures using a `Reactor Pattern `_ + The code is structures using a [Reactor Pattern](https://zguide.zeromq.org/docs/chapter5/#Using-a-Reactor) """ def __init__(self, tcp_port, websocket_port): super().__init__(name='PubServer') @@ -271,9 +267,9 @@ class Publisher: """ The publisher that provides the functional interface to the application - .. note:: - * An instance must not be shared across threads! - * One instance per thread is enough + > [!NOTE] + > * An instance must not be shared across threads! + > * One instance per thread is enough """ def __init__(self, check_thread_owner=True): diff --git a/src/jukebox/jukebox/rpc/server.py b/src/jukebox/jukebox/rpc/server.py index 8615bced7..b7e55b243 100644 --- a/src/jukebox/jukebox/rpc/server.py +++ b/src/jukebox/jukebox/rpc/server.py @@ -1,17 +1,14 @@ # -*- coding: utf-8 -*- """ -Remote Procedure Call Server (RPC) -************************************* +## Remote Procedure Call Server (RPC) Bind to tcp and/or websocket port and translates incoming requests to procedure calls. Avaiable procedures to call are all functions registered with the plugin package. -To protocol is loosely based on `jsonrpc `_ +The protocol is loosely based on [jsonrpc](https://www.jsonrpc.org/specification) But with different elements directly relating to the plugin concept and Python function argument options -.. code-block:: yaml - { 'package' : str # The plugin package loaded from python module 'plugin' : str # The plugin object to be accessed from the package @@ -38,9 +35,9 @@ Three sockets are opened -#. TCP (on a configurable port) -#. Websocket (on a configurable port) -#. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be +1. TCP (on a configurable port) +2. Websocket (on a configurable port) +3. Inproc: On ``inproc://JukeBoxRpcServer`` connection from the internal app are accepted. This is indented be call arbitrary RPC functions from plugins that provide an interface to the outside world (e.g. GPIO). By also going though the RPC instead of calling function directly we increase thread-safety and provide easy configurability (e.g. which button triggers what action) diff --git a/src/jukebox/jukebox/utils.py b/src/jukebox/jukebox/utils.py index 9be97b6db..dbd647490 100644 --- a/src/jukebox/jukebox/utils.py +++ b/src/jukebox/jukebox/utils.py @@ -17,7 +17,8 @@ def decode_rpc_call(cfg_rpc_call: Dict) -> Optional[Dict]: """Makes sure that the core rpc call parameters have valid default values in cfg_rpc_call. - .. important: Leaves all other parameters in cfg_action untouched or later downstream processing! + > [!IMPORTANT] + > Leaves all other parameters in cfg_action untouched or later downstream processing! :param cfg_rpc_call: RPC command as configuration entry :return: A fully populated deep copy of cfg_rpc_call @@ -41,8 +42,8 @@ def decode_rpc_command(cfg_rpc_cmd: Dict, logger: logging.Logger = log) -> Optio This means - * Decode RPC command alias (if present) - * Ensure all RPC call parameters have valid default values + * Decode RPC command alias (if present) + * Ensure all RPC call parameters have valid default values If the command alias cannot be decoded correctly, the command is mapped to misc.empty_rpc_call which emits a misuse warning when called diff --git a/src/jukebox/misc/loggingext.py b/src/jukebox/misc/loggingext.py index 9328cfea8..36b040339 100644 --- a/src/jukebox/misc/loggingext.py +++ b/src/jukebox/misc/loggingext.py @@ -1,7 +1,6 @@ """ -############## -Logger -############## +## Logger + We use a hierarchical Logger structure based on pythons logging module. It can be finely configured with a yaml file. The top-level logger is called 'jb' (to make it short). In any module you may simple create a child-logger at any hierarchy @@ -9,25 +8,27 @@ Hierarchy separator is the '.'. If the logger already exits, getLogger will return a reference to the same, else it will be created on the spot. -:Example: How to get logger and log away at your heart's content: +Example: How to get logger and log away at your heart's content: + >>> import logging >>> logger = logging.getLogger('jb.awesome_module') >>> logger.info('Started general awesomeness aura') -Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module:: -`` -loggers: - jb: - level: WARNING - handlers: [console, debug_file_handler, error_file_handler] - propagate: no - jb.awesome_module: - level: DEBUG -`` - -.. note:: -The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes sense) -There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output +Example: YAML snippet, setting WARNING as default level everywhere and DEBUG for jb.awesome_module: + + loggers: + jb: + level: WARNING + handlers: [console, debug_file_handler, error_file_handler] + propagate: no + jb.awesome_module: + level: DEBUG + + +> [!NOTE] +> The name (and hierarchy path) of the logger can be arbitrary and must not necessarily match the module name (still makes +> sense). +> There can be multiple loggers per module, e.g. for special classes, to further control the amount of log output """ import sys import logging @@ -80,21 +81,22 @@ def filter(self, record): class PubStream: - """" + """ Stream handler wrapper around the publisher for logging.StreamHandler Allows logging to send all log information (based on logging configuration) to the Publisher. - ATTENTION: This can lead to recursions! - - Recursions come up when - (a) Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, - which causes a send, ..... - (b) Publisher initialization emits logs, which need a Publisher instance to send logs + > [!CAUTION] + > This can lead to recursions! + > Recursions come up when + > * Publish.send / PublishServer.send also emits logs, which cause a another send, which emits a log, + > which causes a send, ..... + > * Publisher initialization emits logs, which need a Publisher instance to send logs - IMPORTANT: To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the - functions in the send-function stack! + > [!IMPORTANT] + > To avoid endless recursions: The creation of a Publisher MUST NOT generate any log messages! Nor any of the + > functions in the send-function stack! """ def __init__(self): self._topic = 'core.logger' diff --git a/src/jukebox/run_configure_audio.py b/src/jukebox/run_configure_audio.py index 6bb0e70f6..93f0a4c6a 100755 --- a/src/jukebox/run_configure_audio.py +++ b/src/jukebox/run_configure_audio.py @@ -5,7 +5,7 @@ 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 :ref:`userguide/audio:Audio Configuration`. +For more information see [Audio Configuration](../../builders/audio.md#audio-configuration). """ import os import argparse diff --git a/src/jukebox/run_jukebox.py b/src/jukebox/run_jukebox.py index 0735e2b8e..789e57aca 100755 --- a/src/jukebox/run_jukebox.py +++ b/src/jukebox/run_jukebox.py @@ -5,11 +5,11 @@ 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 :ref:`userguide/configuration:Jukebox Configuration`. +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 :ref:`userguide/troubleshooting:Troubleshooting`. +See [Troubleshooting](../../builders/troubleshooting.md). """ import os.path import argparse diff --git a/src/jukebox/run_register_rfid_reader.py b/src/jukebox/run_register_rfid_reader.py index 3aa69735e..18a1614d8 100755 --- a/src/jukebox/run_register_rfid_reader.py +++ b/src/jukebox/run_register_rfid_reader.py @@ -3,10 +3,11 @@ 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 :ref:`rfid/rfid:RFID Readers`. +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 +> [!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 """ import os diff --git a/src/jukebox/run_rpc_tool.py b/src/jukebox/run_rpc_tool.py index 1593d7796..4bd834e12 100755 --- a/src/jukebox/run_rpc_tool.py +++ b/src/jukebox/run_rpc_tool.py @@ -11,7 +11,7 @@ The list of available commands is fetched from the running Jukebox service. .. todo: - - kwargs support + - kwargs support """ From 6f0976443980d787473085fc9d8feedd659280b4 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Thu, 18 Jan 2024 09:19:32 +0100 Subject: [PATCH 06/24] deactivate kioskmode installation option on armv6 devices (#2217) * deactivate kiosk mode in armv6 devices suggestion from Issue 2209 * added "disabling" information to message * change message to be more clear --- installation/routines/customize_options.sh | 38 +++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/installation/routines/customize_options.sh b/installation/routines/customize_options.sh index 3e94b07dc..c903df189 100644 --- a/installation/routines/customize_options.sh +++ b/installation/routines/customize_options.sh @@ -208,25 +208,39 @@ Would you like to install the Web App? [Y/n]" } _option_kiosk_mode() { - # ENABLE_KIOSK_MODE - clear_c - print_c "----------------------- KIOSK MODE ---------------------- + # ENABLE_KIOSK_MODE + clear_c + print_c "----------------------- KIOSK MODE ----------------------" + if [[ $(get_architecture) == "armv6" ]]; then + + print_c " +Due to limited resources the kiosk mode is not supported +on Raspberry Pi 1 or Zero 1 ('ARMv6' models). +Kiosk mode will not be installed. + +Press enter to continue." + read + ENABLE_KIOSK_MODE=false + else + print_c " If you have a screen attached to your RPi, this will launch the Web App 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]" - read -r response - case "$response" in - [yY][eE][sS]|[yY]) - ENABLE_KIOSK_MODE=true - ;; - *) - ;; - esac - log "ENABLE_KIOSK_MODE=${ENABLE_KIOSK_MODE}" + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + ENABLE_KIOSK_MODE=true + ;; + *) + ;; + esac + fi + + log "ENABLE_KIOSK_MODE=${ENABLE_KIOSK_MODE}" } _options_update_raspi_os() { From f6db16098f1f41b18842f5af15b38d2911737e6d Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Wed, 24 Jan 2024 23:12:45 +0100 Subject: [PATCH 07/24] Update docs (#2216) * minor fixes * add "Pre-install preparation / workarounds" section add "Workaround for network related features on bookworm" * add "Workaround for 64-bit Kernels" harmonize and update description / message * minor fix * Update warning block as alerts are not supported inside of "details" * minor fix * fix Raspberry Pi OS name Co-authored-by: s-martin * fix Raspberry Pi OS name * fix typo * added link to installation * updates from reviews * fix file for synchronisation doc. moved to components * add docs for samba * Apply suggestions from code review Co-authored-by: s-martin * restructure builders readme --------- Co-authored-by: s-martin --- documentation/README.md | 3 +- documentation/builders/README.md | 22 ++++++---- .../synchronisation/rfidcards.md} | 10 ++--- documentation/builders/configuration.md | 6 +-- documentation/builders/installation.md | 44 +++++++++++++++++-- documentation/builders/samba.md | 18 ++++++++ documentation/builders/update.md | 3 +- documentation/developers/README.md | 2 +- documentation/developers/docstring/README.md | 2 +- installation/includes/02_helpers.sh | 4 +- installation/install-jukebox.sh | 10 ++--- .../hardware/fake_reader_gui/requirements.txt | 2 +- src/jukebox/components/volume/__init__.py | 4 +- 13 files changed, 93 insertions(+), 37 deletions(-) rename documentation/builders/{rfid.md => components/synchronisation/rfidcards.md} (97%) create mode 100644 documentation/builders/samba.md diff --git a/documentation/README.md b/documentation/README.md index b10941a55..bb11dd6f4 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -42,7 +42,8 @@ project check out the [documentation of Version 2](https://github.com/MiczFlor/R Version 3 has reached a mature state and will soon be the default version. However, some features may still be missing. Please check the [Feature Status](./developers/status.md), if YOUR feature is already implemented. -> ![NOTE] If version 3 has all the features you need, we recommend using Version 3. +> [!NOTE] +> If version 3 has all the features you need, we recommend using Version 3. If there is a feature missing, please open an issue. diff --git a/documentation/builders/README.md b/documentation/builders/README.md index 8ea5e0648..5909596aa 100644 --- a/documentation/builders/README.md +++ b/documentation/builders/README.md @@ -6,24 +6,28 @@ * [Update](./update.md) * [Configuring Phoniebox](./configuration.md) -## Configuration +## Features -* [Audio](./audio.md) -* [RFID](./rfid.md) +* Audio + * [Audio Output](./audio.md) + * [Bluetooth audio buttons](./bluetooth-audio-buttons.md) * [GPIO Recipes](./gpio.md) * [Card Database](./card-database.md) -* [Troubleshooting](./troubleshooting.md) + * [RFID Cards synchronisation](./components/synchronisation/rfidcards.md) +* [Auto Hotspot](./autohotspot.md) +* File Management + * [Network share / Samba](./samba.md) + +## Hardware Components -## Components * [Power](./components/power/) * [OnOff SHIM for safe power on/off](./components/power/onoff-shim.md) * [Soundcards](./components/soundcards/) * [HiFiBerry Boards](./components/soundcards/hifiberry.md) - +* [RFID Readers](./../developers/rfid/README.md) + ## Advanced - -* [Bluetooth (and audio buttons)](./bluetooth-audio-buttons.md) -* [Auto Hotspot](./autohotspot.md) +* [Troubleshooting](./troubleshooting.md) * [Concepts](./concepts.md) * [System](./system.md) * [RPC Commands](./rpc-commands.md) diff --git a/documentation/builders/rfid.md b/documentation/builders/components/synchronisation/rfidcards.md similarity index 97% rename from documentation/builders/rfid.md rename to documentation/builders/components/synchronisation/rfidcards.md index f8826f68f..a4c974b4b 100644 --- a/documentation/builders/rfid.md +++ b/documentation/builders/components/synchronisation/rfidcards.md @@ -1,6 +1,4 @@ -# RFID - -## Syncronisation RFID Cards +# Synchronisation RFID Cards This component handles the synchronisation of RFID cards (audiofolder and card database entries). @@ -15,7 +13,7 @@ RFID card to the command. For the \"RFID scan sync\" feature, activate the option in the configuration or bind a RFID card to the command for dynamic activation or deactivation. -### Synchronisation +## Synchronisation The synchronisation will be FROM a server TO the Phoniebox, overriding existing files. A local configuration will be lost after the @@ -26,7 +24,7 @@ To access the files on the server, 2 modes are supported: SSH or MOUNT. Please make sure you have the correct access rights to the source and use key-based authentication for SSH. -#### RFID scan sync +### RFID scan sync If the feature \"RFID scan sync\" is activated, there will be a check on every RFID scan against the server if a matching card entry and audiofolder is available. If so, changes will be synced. The playback @@ -40,7 +38,7 @@ deleted on remote. This is also true for changed card entries (the old audiofolder / -files will remain). To remove not existing items us a \"sync-all\". -### Configuration +## Configuration Set the corresponding setting in `shared\settings\jukebox.yaml` to activate this feature. diff --git a/documentation/builders/configuration.md b/documentation/builders/configuration.md index dba2da7dc..2e1c4ff23 100644 --- a/documentation/builders/configuration.md +++ b/documentation/builders/configuration.md @@ -7,11 +7,7 @@ The majority of configuration options is only available by editing the config fi *when the service is not running!* Don't fear (overly), they contain commentaries. -For several aspects we have [configuration tools](../developers/coreapps.md#configuration-tools) and detailed guides: - -* [Audio Configuration](./audio.md#audio-configuration) -* [RFID Reader Configuration](../developers/rfid/basics.md#reader-configuration) -* [GPIO Recipes](./gpio.md) +For several aspects we have [configuration tools](../developers/coreapps.md#configuration-tools) and [detailed guides](./README.md#features). Even after running the tools, certain aspects can only be changed by modifying the configuration files directly. diff --git a/documentation/builders/installation.md b/documentation/builders/installation.md index 7ce0e59c0..1d4b18470 100644 --- a/documentation/builders/installation.md +++ b/documentation/builders/installation.md @@ -10,7 +10,9 @@ Before you can install the Phoniebox software, you need to prepare your Raspberr 1. Connect a Micro SD card to your computer (preferable an SD card with high read throughput) 2. Download the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) and run it 3. Click on "Raspberry Pi Device" and select "No filtering" -4. Select **Raspberry Pi OS Lite (32-bit)** (without desktop environment) as the operating system. `future3` does not support 64bit kernels (`aarch64`). +4. As operating system select **Raspberry Pi OS (other)** and then **Raspberry Pi OS Lite (Legacy, 32-bit)** (no desktop environment). *64-bit is currently not supported*. + * Bookworm support is partly broken, see [here](#workaround-for-network-related-features-on-bookworm). + * For Pi 4 and newer also check [this](#workaround-for-64-bit-kernels-pi-4-and-newer). 5. Select your Micro SD card (your card will be formatted) 6. After you click `Next`, a prompt will ask you if you like to customize the OS settings * Click `Edit Settings` @@ -26,12 +28,12 @@ Before you can install the Phoniebox software, you need to prepare your Raspberr 8. Confirm the next warning about erasing the SD card with `Yes` 9. Wait for the imaging process to be finished (it'll take a few minutes) + +### Pre-boot preparation

In case you forgot to customize the OS settings, follow these instructions after RPi OS has been written to the SD card. -### Pre-boot preparation - You will need a terminal, like PuTTY for Windows or the Terminal app for Mac to proceed with the next steps. 1. Open a terminal of your choice. @@ -77,6 +79,42 @@ You will need a terminal, like PuTTY for Windows or the Terminal app for Mac to
+### Pre-install preparation / workarounds + +#### Workaround for network related features on Bookworm +
+With Bookworm the network settings have changed. Now "NetworkManager" is used instead of "dhcpcd". +This breaks breaks network related features like "Static IP", "Wifi Setup" and "Autohotspot". +Before running the installation, the network config has to be changed via raspi-config, to use the "old" dhcpcd network settings. + +:warning: +If the settings are changed, your network will reset and Wifi will not be configured, so you lose ssh access via wireless connection. +So make sure you use a wired connection or perform the following steps in a local terminal with a connected monitor and keyboard. + +Change network config +* run `sudo raspi-config` +* select `6 - Advanced Options` +* select `AA - Network Config` +* select `dhcpcd` + +If you need Wifi, add the information now +* select `1 - System Options` +* select `1 - Wireless LAN` +* enter Wifi information +
+ +#### Workaround for 64-bit Kernels (Pi 4 and newer) +
+ +The installation process checks if a 32-bit OS is running, as 64-bit is currently not supported. +This check also fails if the kernel is running in 64-bit mode. This is the default for Raspberry Pi models 4 and newer. + +To be able to run the installation, you have to switch to the 32-bit mode by modifying the `config.txt` and add/change the line `arm_64bit=0`. +Up to Bullseye, the `config.txt` file is located at `/boot/`. Since Bookworm, the location changed to `/boot/firmware/` ([see here](https://www.raspberrypi.com/documentation/computers/config_txt.html)). + +Reboot before you proceed. +
+ ## Install Phoniebox software Choose a version, run the corresponding install command in your SSH terminal and follow the instructions. diff --git a/documentation/builders/samba.md b/documentation/builders/samba.md new file mode 100644 index 000000000..ac9a93bbc --- /dev/null +++ b/documentation/builders/samba.md @@ -0,0 +1,18 @@ +# Samba + +To conveniently copy files to your Phoniebox via network `samba` can be configured during the installation. The folder `./shared/` will be exposed as network share `phoniebox`, giving you access to the audio and config folders. + +## Connect + +To access the share open your OS network environment and select your Phoniebox device. +Alternatively directly access it via url with the file explorer (e.g. Windows `\\`, MacOS `smb://`). + +See also +* [MacOS](https://support.apple.com/lt-lt/guide/mac-help/mchlp1140/mac) + +## User name / Password + +As login credentials use the same username you used to run the installation with. The password is `raspberry`. +You can change the password anytime using the command `sudo smbpasswd -a ""`. + + diff --git a/documentation/builders/update.md b/documentation/builders/update.md index 94baf0a93..e84655e7c 100644 --- a/documentation/builders/update.md +++ b/documentation/builders/update.md @@ -35,7 +35,8 @@ $ ./run_rebuild.sh -u ## Migration Path from Version 2 There is no update path coming from Version 2.x of the Jukebox. -You need to do a fresh install of Version 3 on a fresh Raspian Bullseye image. +You need to do a fresh install of Version 3 on a fresh Raspberry Pi OS image. +See [Installing Phoniebox future3](./installation.md). > [!IMPORTANT] > Do start with a fresh SD card image! diff --git a/documentation/developers/README.md b/documentation/developers/README.md index f6ec65dc4..5598f34d7 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -9,7 +9,7 @@ * [Jukebox Apps](./coreapps.md) * [Web App](./webapp.md) -* [RFID Readers](./rfid) +* [RFID Readers](./rfid/README.md) * [Docstring API Docs (from py files)](./docstring/README.md) * [Plugin Reference](./docstring/README.md#jukeboxplugs) * [Feature Status](./status.md) diff --git a/documentation/developers/docstring/README.md b/documentation/developers/docstring/README.md index dde85b224..aa7cefecc 100644 --- a/documentation/developers/docstring/README.md +++ b/documentation/developers/docstring/README.md @@ -1245,7 +1245,7 @@ This means must also run as user process, as described in ## Misc PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module -with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration +with name module-switch-on-connect. On Raspberry Pi OS Bullseye, this module is not part of the default configuration in ``/usr/pulse/default.pa``. So, we don't need to worry about it. If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs from the Jukebox. Remove it from the configuration! diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index f2b60313b..9cd51c824 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -75,7 +75,7 @@ get_architecture() { echo $arch } -is_raspian() { +is_raspbian() { if [[ $( . /etc/os-release; printf '%s\n' "$ID"; ) == *"raspbian"* ]]; then echo true else @@ -89,7 +89,7 @@ get_debian_version_number() { } get_boot_config_path() { - if [ "$(is_raspian)" = true ]; then + if [ "$(is_raspbian)" = true ]; then local debian_version_number=$(get_debian_version_number) # Bullseye and lower diff --git a/installation/install-jukebox.sh b/installation/install-jukebox.sh index 2df7ab528..1ec3f9ad5 100755 --- a/installation/install-jukebox.sh +++ b/installation/install-jukebox.sh @@ -86,9 +86,9 @@ Check install log for details:" exit 1 } -# 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 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 Raspberry Pi OS versions report as machine "armv6l" or "armv7l", if 32-bit (even the ARMv8 cores!) _check_os_type() { local os_type=$(uname -m) @@ -97,8 +97,8 @@ _check_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" + print_lc "ERROR: Only 32-bit operating systems are supported. Please use a 32-bit version of Raspberry Pi OS!" + print_lc "For Pi 4 models or newer running a 64-bit kernels, also see this: https://github.com/MiczFlor/RPi-Jukebox-RFID/issues/2041" exit 1 fi } 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 937256e86..b539a2478 100644 --- a/src/jukebox/components/rfid/hardware/fake_reader_gui/requirements.txt +++ b/src/jukebox/components/rfid/hardware/fake_reader_gui/requirements.txt @@ -1,6 +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) +# It is available on most Unix systems (but not on headless Raspberry Pi OS 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/volume/__init__.py b/src/jukebox/components/volume/__init__.py index ccc4873d7..4782581ca 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -33,7 +33,7 @@ ## Misc PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module -with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration +with name module-switch-on-connect. On Raspberry Pi OS Bullseye, this module is not part of the default configuration in ``/usr/pulse/default.pa``. So, we don't need to worry about it. If the module gets loaded it conflicts with the toggle on connect and the selected primary / secondary outputs from the Jukebox. Remove it from the configuration! @@ -635,7 +635,7 @@ def finalize(): global pulse_control # Set default output and start-up volume # Note: PulseAudio may switch the sink automatically to a connecting bluetooth device depending on the loaded module - # with name module-switch-on-connect. On RaspianOS Bullseye, this module is not part of the default configuration. + # with name module-switch-on-connect. On Raspberry Pi OS Bullseye, this module is not part of the default configuration. # So, we shouldn't need to worry about it. Still, set output and startup volume close to each other # to minimize bluetooth connection in between global pulse_control From 1c0afa36d499c91bfca95309a98992c67ef4662c Mon Sep 17 00:00:00 2001 From: s-martin Date: Mon, 29 Jan 2024 08:37:00 +0100 Subject: [PATCH 08/24] fix the messageboxes (#2223) --- documentation/builders/rpc-commands.md | 3 ++- documentation/builders/system.md | 6 ++++-- documentation/builders/troubleshooting.md | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/documentation/builders/rpc-commands.md b/documentation/builders/rpc-commands.md index 181034f4a..a45e0b643 100644 --- a/documentation/builders/rpc-commands.md +++ b/documentation/builders/rpc-commands.md @@ -97,7 +97,8 @@ kwargs: recursive: True ``` -> [!IMPORTANT] *args* must be a **list** of arguments to be passed! Even if only a single argument is passed. +> [!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) diff --git a/documentation/builders/system.md b/documentation/builders/system.md index f6eeb7ba1..ab9311d9f 100644 --- a/documentation/builders/system.md +++ b/documentation/builders/system.md @@ -10,7 +10,8 @@ The system consists of 4. [Web App](system.md#web-app-ui) as User Interface (UI) for a web browser 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 users home folder `~/RPi-Jukebox-RFID`. +> [!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) @@ -34,7 +35,8 @@ $ systemctl --user start mpd $ systemctl --user stop mpd ``` -> [!IMPORTANT] Never start or enable the system-wide MPD service with `sudo systemctl start mpd`! +> [!IMPORTANT] +> Never start or enable the system-wide MPD service with `sudo systemctl start mpd`! To check if MPD is running or has issues, use diff --git a/documentation/builders/troubleshooting.md b/documentation/builders/troubleshooting.md index 2f34af083..ffc5189dc 100644 --- a/documentation/builders/troubleshooting.md +++ b/documentation/builders/troubleshooting.md @@ -29,7 +29,8 @@ The logs are also available via the Web Server: http://ip.of.your.box/logs ``` -> [!IMPORTANT] Always check the time modification date or the beginning of the log file to ensure you are not looking at an old log file! +> [!IMPORTANT] +> Always check the time modification date or the beginning of the log file to ensure you are not looking at an old log file! ## The long answer: A few more details From 0aad2a93e8d04428628ce6a9cc94bbe5a3e6eecc Mon Sep 17 00:00:00 2001 From: SKHSKHilber <47213735+SKHSKHilber@users.noreply.github.com> Date: Tue, 30 Jan 2024 08:36:58 +0100 Subject: [PATCH 09/24] Improve Bluetooth docs (#2174) * Update audio.md * Update audio.md * Update audio.md * Update audio.md --------- Co-authored-by: s-martin Co-authored-by: pabera <1260686+pabera@users.noreply.github.com> --- documentation/builders/audio.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/builders/audio.md b/documentation/builders/audio.md index 54bfe72bf..93c46dd54 100644 --- a/documentation/builders/audio.md +++ b/documentation/builders/audio.md @@ -86,6 +86,7 @@ Pairing successful .... [PowerLocus Buddy]# exit ``` +If `bluetoothctl` has trouble to execute due to permission issue, try `sudo bluetoothctl`. Wait for a few seconds and then with `$ pactl list sinks short`, check wether the Bluetooth device shows up as an output. Its name usually looks like this: `bluez_sink.C4_FB_20_63_CO_FE.a2dp_sink`. From c306719a4b76ad829c79284df144e26ec17852b8 Mon Sep 17 00:00:00 2001 From: Alexander Lochmann Date: Tue, 30 Jan 2024 08:40:48 +0100 Subject: [PATCH 10/24] Playlists, Livestreams, Podcasts (#2200) * first attempt * Introduce harmonize_mpd_url * Revert "first attempt" This reverts commit c99f2ed884fd17987d7eee1c69caf93e212fc0c5. * feat: add docs about using playlists * feat: add docs about using livestreams * feat: add docs about using podcasts * Apply suggestions from code review Co-authored-by: s-martin * fix: diverify examples * fix: better folder display * fix: addressing comments * fix: addressing comments * fix: update wrong links and fix some wording --------- Co-authored-by: pabera <1260686+pabera@users.noreply.github.com> Co-authored-by: s-martin --- documentation/builders/README.md | 7 +- .../webapp/playlists-livestreams-podcasts.md | 142 ++++++++++++++++++ src/jukebox/components/playermpd/__init__.py | 13 +- 3 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 documentation/builders/webapp/playlists-livestreams-podcasts.md diff --git a/documentation/builders/README.md b/documentation/builders/README.md index 5909596aa..6d9e67bff 100644 --- a/documentation/builders/README.md +++ b/documentation/builders/README.md @@ -25,7 +25,12 @@ * [Soundcards](./components/soundcards/) * [HiFiBerry Boards](./components/soundcards/hifiberry.md) * [RFID Readers](./../developers/rfid/README.md) - + +## Web Application + +* Music + * [Playlists, Livestreams and Podcasts](./webapp/playlists-livestreams-podcasts.md) + ## Advanced * [Troubleshooting](./troubleshooting.md) * [Concepts](./concepts.md) diff --git a/documentation/builders/webapp/playlists-livestreams-podcasts.md b/documentation/builders/webapp/playlists-livestreams-podcasts.md new file mode 100644 index 000000000..2b8be79a1 --- /dev/null +++ b/documentation/builders/webapp/playlists-livestreams-podcasts.md @@ -0,0 +1,142 @@ +# Playlists, Livestreams and Podcasts + +By default, the Jukebox represents music based on its metadata like album name, artist or song name. The hierarchy and order of songs is determined by their original definition, e.g. order of songs within an album. If you prefer a specific list of songs to be played, you can use playlists (files ending with `*.m3u`). Jukebox also supports livestreams and podcasts (if connected to the internet) through playlists. + +## Playlists +If you like the Jukebox to play songs in a pre-defined order, you can use .m3u playlists. + +A .m3u playlist is a plain text file that contains a list of file paths or URLs to multimedia files. Each entry in the playlist represents a single song, and they are listed in the order in which they should be played. + +### Structure of a .m3u playlist +A .m3u playlist is a simple text document with each song file listed on a separate line. Each entry is optionally preceded by a comment line that starts with a '#' symbol. The actual file paths or URLs of the media files come after the comment. + +### Creating a .m3u playlist + +1. You can create a .m3u playlist using a plain text editor. +1. Open a new text file +1. [Optional] Start by adding a comment line to provide a description or notes about the playlist. +1. On the following lines, list the file paths or URLs of the media files you want to include in the playlist, one per line. They must refer to true files paths on your Jukebox. They can be relative or absolute paths. +1. Save the file with the .m3u extension, e.g. `my_playlist.m3u`. + +``` +# Absolute +/home//RPi-Jukebox-RFID/shared/audiofolders/Simone Sommerland/Die 30 besten Kindergartenlieder/08 - Pitsch, patsch, Pinguin.mp3 +/home//RPi-Jukebox-RFID/shared/audiofolders/Simone Sommerland/Die 30 besten Spiel- Und Bewegungslieder/12 - Das rote Pferd.mp3 +# Relative +Bibi und Tina/bibi-tina-jetzt-in-echt-kinofilm-soundtrack/bibi-tina-jetzt-in-echt-kinofilm-soundtrack-7-ordinary-girl.mp3 +``` + +### Using .m3u playlists in Jukebox + +The Jukebox Web App handles the playlists in a way that it allows you to browse its content just like other songs. This means, you won't see the m3u playlist itself and instead, the individual items of the playlist. They also become actionable and you can select individual songs from it to play, or play the entire playlist. + +> [!NOTE] +> Files ending with `.m3u` are treated as folder playlist. Regular folder processing is suspended and the playlist is built solely from the `.m3u` content. Only the alphabetically first `.m3u` file is processed, others are ignored. + +Based on the note above, we suggest to use m3u playlists like this, especially if you like to manage multiple playlists. + +1. In the `audiofolders` directory (or any sub-directory), create a new folder. +1. In this new folder, copy your .m3u playlist. Make sure the links to the respective songs are correct. +1. Open the Web App. Under `Library`, select the `Folder` view and browse to the new folder you created. +1. You should now be able to browse and play the content of the playlist. + +#### Example folder structure + +``` +└── audiofolders + ├── wake-up-songs + │ └── playlist.m3u + └── lullabies-sleep-well + └── playlist.m3u +``` + +### Assigning a .m3u playlist to a card + +In the Jukebox Web App, .m3u playlists do not show up as individual files. In order to assign a playlist to a card, do the following: + +1. [Follow the steps above](#using-m3u-playlists-in-jukebox) to add a playlist to your Jukebox (make sure you have created individual folders). +1. Open the `Cards` tab in the Web App and click on the `+` button to add a new card. +1. As a Jukebox action, select "Play music", then select "Select music". +1. In the `Library` view, select the `Folder` view located in the top right corner. +1. Browse to the folder you created (representing your playlist) and click on it. + +You are essentially assigning a folder (just like any other conventional folder) to your card representing the content of your playlist. + +## Livestreams + +In order to play radio livestreams on your Jukebox, you use playlists to register your livestream and make it accessible. + +### Using livestream.txt playlist in Jukebox + +1. [Follow the steps above](#using-m3u-playlists-in-jukebox) to add a playlist to your Jukebox (make sure you have created individual folders). +1. When creating the playlist file, make sure it's called or at least ends with `livestream.txt` instead of `.m3u` (Examples: `awesome-livestream.txt`, `livestream.txt`). +1. Add URLs of your livestreams just like you would add songs in `.m3u` playlists. + +You can now assign livestreams to cards [following the example](#assigning-a-m3u-playlist-to-a-card) of playlists. + +#### Example folder structure and playlist names + +``` +└── audiofolders + ├── wdr-kids + │ └── wdr-kids-livestream.txt + ├── energy + │ └── cool-livestream.txt + └── classic + └── livestream.txt +``` + +#### Example of livestream.txt + +```txt +https://wdr-diemaus-live.icecastssl.wdr.de/wdr/diemaus/live/mp3/128/stream.mp3 +http://channels.webradio.antenne.de/hits-fuer-kids +``` + +## Podcasts + +Just like you add livestreams to the Jukebox, you can also add individual Podcasts or entire Podcast feeds to the Jukebox. + +You have 3 options to play Podcasts + +1. Create a playlist and reference individual direct URLs to Podcast episodes (just like [livestreams](#livestreams)) +1. Provide a Podcast RSS feed +1. Download the MP3 and add them like normal songs to your Jukebox. This also makes them available offline. + +We will explain options 1 and 2 more closely. + +### Using podcast.txt playlist in Jukebox + +1. [Follow the steps above](#using-m3u-playlists-in-jukebox) to add a playlist to your Jukebox (make sure you have created individual folders). +1. When creating the playlist file, make sure it's called or at least ends with `podcasts.txt` instead of `.m3u`. (Examples: `awesome-podcast.txt`, `podcast.txt`). +1. Add links to your individual podcast episodes just like you would with songs in .m3u playlists +1. As an alternative, you can provide a single RSS feed (XML). Jukebox will expand the file and refer to all episodes listed within this file. + +#### Example folder structure and playlist names + +``` +└── audiofolders + ├── die-maus + │ └── die-maus-podcast.txt + ├── miras-welt + │ └── cool-podcast.txt + └── kakadu + └── podcast.txt +``` + +#### Example of podcast.txt for individual episodes + +```txt +https://podcastb11277.podigee.io/94-ich-ware-gerne-beliebt-wie-geht-das +https://podcastb11277.podigee.io/91-wieso-kann-ich-nicht-den-ganzen-tag-fernsehen +``` + +#### Example of podcast.txt for RSS feeds (XML) + +```txt +https://kinder.wdr.de/radio/diemaus/audio/diemaus-60/diemaus-60-106.podcast +``` + +```txt +http://www.kakadu.de/podcast-kakadu.2730.de.podcast.xml +``` diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index 9073a9b4a..65c6e7267 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -280,6 +280,14 @@ def _mpd_status_poll(self): pass publishing.get_publisher().send('playerstatus', self.mpd_status) + # MPD can play absolute paths but can find songs in its database only by relative path + # This function aims to prepare the song_url accordingly + def harmonize_mpd_url(self, song_url): + _music_library_path_absolute = os.path.expanduser(components.player.get_music_library_path()) + song_url = song_url.replace(f'{_music_library_path_absolute}/', '') + + return song_url + @plugs.tag def get_player_type_and_version(self): with self.mpd_lock: @@ -667,10 +675,7 @@ def list_songs_by_artist_and_album(self, albumartist, album): @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}/', '') + song_url = self.harmonize_mpd_url(song_url) with self.mpd_lock: song = self.mpd_retry_with_mutex(self.mpd_client.find, 'file', song_url) From 85587a686f04bbbcf47679a264696d5a71d07e69 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Thu, 1 Feb 2024 23:56:38 +0100 Subject: [PATCH 11/24] Future3 add login motd with note about venv (#2225) * add ssh welcome script * changed message * moved login message setup * fix sudo on copy * update message * fix typo in filename --- documentation/developers/README.md | 2 +- .../developers/{pyhton.md => python.md} | 0 installation/routines/install.sh | 1 + installation/routines/set_raspi_config.sh | 4 ++++ resources/system/99-rpi-jukebox-rfid-welcome | 16 ++++++++++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) rename documentation/developers/{pyhton.md => python.md} (100%) create mode 100644 resources/system/99-rpi-jukebox-rfid-welcome diff --git a/documentation/developers/README.md b/documentation/developers/README.md index 5598f34d7..6addeaff1 100644 --- a/documentation/developers/README.md +++ b/documentation/developers/README.md @@ -3,7 +3,7 @@ ## Getting started * [Development Environment](./development-environment.md) -* [Python Development Notes](pyhton.md) +* [Python Development Notes](python.md) ## Reference diff --git a/documentation/developers/pyhton.md b/documentation/developers/python.md similarity index 100% rename from documentation/developers/pyhton.md rename to documentation/developers/python.md diff --git a/installation/routines/install.sh b/installation/routines/install.sh index f1f2a5f80..66a65203c 100644 --- a/installation/routines/install.sh +++ b/installation/routines/install.sh @@ -15,5 +15,6 @@ install() { setup_rfid_reader optimize_boot_time setup_autohotspot + setup_login_message cleanup } diff --git a/installation/routines/set_raspi_config.sh b/installation/routines/set_raspi_config.sh index 7f39a0ba5..6bfa8e725 100644 --- a/installation/routines/set_raspi_config.sh +++ b/installation/routines/set_raspi_config.sh @@ -31,3 +31,7 @@ _run_set_raspi_config() { set_raspi_config() { run_with_log_frame _run_set_raspi_config "Set default raspi-config" } + +setup_login_message() { + sudo cp -f "${INSTALLATION_PATH}/resources/system/99-rpi-jukebox-rfid-welcome" "/etc/update-motd.d/99-rpi-jukebox-rfid-welcome" +} diff --git a/resources/system/99-rpi-jukebox-rfid-welcome b/resources/system/99-rpi-jukebox-rfid-welcome new file mode 100644 index 000000000..281b1a152 --- /dev/null +++ b/resources/system/99-rpi-jukebox-rfid-welcome @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +echo " +######################################################### + ___ __ ______ _ __________ ____ __ _ _ + / _ \/ // / __ \/ |/ / _/ __/( _ \ / \( \/ ) + / ___/ _ / /_/ / // // _/ ) _ (( O )) ( +/_/ /_//_/\____/_/|_/___/____/ (____/ \__/(_/\_) +future3 + +If you want to run a python script from the project +activate the venv before with `source .venv/bin/activate` +See also https://github.com/MiczFlor/RPi-Jukebox-RFID/ +blob/future3/main/documentation/developers/python.md + +#########################################################" From 2bb93679a68ff06497b492f2e0191bd0ce37b2fb Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Fri, 2 Feb 2024 00:06:52 +0100 Subject: [PATCH 12/24] Minor fixes (#2234) * fix motd * fix error message for generic usb reader --- resources/system/99-rpi-jukebox-rfid-welcome | 2 +- src/jukebox/components/rfid/hardware/generic_usb/generic_usb.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/system/99-rpi-jukebox-rfid-welcome b/resources/system/99-rpi-jukebox-rfid-welcome index 281b1a152..1b9904f96 100644 --- a/resources/system/99-rpi-jukebox-rfid-welcome +++ b/resources/system/99-rpi-jukebox-rfid-welcome @@ -9,7 +9,7 @@ echo " future3 If you want to run a python script from the project -activate the venv before with `source .venv/bin/activate` +activate the venv before with 'source .venv/bin/activate' See also https://github.com/MiczFlor/RPi-Jukebox-RFID/ blob/future3/main/documentation/developers/python.md diff --git a/src/jukebox/components/rfid/hardware/generic_usb/generic_usb.py b/src/jukebox/components/rfid/hardware/generic_usb/generic_usb.py index 287bd6b9e..f6d0a5db5 100755 --- a/src/jukebox/components/rfid/hardware/generic_usb/generic_usb.py +++ b/src/jukebox/components/rfid/hardware/generic_usb/generic_usb.py @@ -82,7 +82,7 @@ def query_customization() -> dict: print(f" {Colors.lightgreen}ID{Colors.reset}: " f"{Colors.lightgreen}isKey{Colors.reset}: {Colors.lightcyan}Name{Colors.reset}") if len(devices) == 0: - logger.error("USB device list is empty. Make sure USB RFID reader is connected. Then re-run register_reader.py") + logger.error("USB device list is empty. Make sure USB RFID reader is connected. Then re-run reader registration") return {'device_name': '__error_empty_device_list__'} for idx, (dev, key) in enumerate(zip(devices, devices_is_key)): print(f" {Colors.lightgreen}{idx:2d}{Colors.reset}:" From e1b46b61d8ba185bd64b3d9422a08e44f11827a3 Mon Sep 17 00:00:00 2001 From: powertomato Date: Sun, 4 Feb 2024 16:52:18 +0100 Subject: [PATCH 13/24] Add NFCPy support (#2190) * Add NFCPy support * Fixed PR comments * Improve documentation, add an example * Fix setup.inc.sh execution rights * Fix PR comments * Fix PR comments * Fix PR comments --- .../developers/rfid/generic_nfcpy.md | 36 +++++ .../rfid/hardware/generic_nfcpy/README.md | 2 + .../hardware/generic_nfcpy/description.py | 3 + .../hardware/generic_nfcpy/generic_nfcpy.py | 128 ++++++++++++++++++ .../hardware/generic_nfcpy/requirements.txt | 4 + .../rfid/hardware/generic_nfcpy/setup.inc.sh | 42 ++++++ 6 files changed, 215 insertions(+) create mode 100644 documentation/developers/rfid/generic_nfcpy.md create mode 100644 src/jukebox/components/rfid/hardware/generic_nfcpy/README.md create mode 100644 src/jukebox/components/rfid/hardware/generic_nfcpy/description.py create mode 100644 src/jukebox/components/rfid/hardware/generic_nfcpy/generic_nfcpy.py create mode 100644 src/jukebox/components/rfid/hardware/generic_nfcpy/requirements.txt create mode 100755 src/jukebox/components/rfid/hardware/generic_nfcpy/setup.inc.sh diff --git a/documentation/developers/rfid/generic_nfcpy.md b/documentation/developers/rfid/generic_nfcpy.md new file mode 100644 index 000000000..bea7b302a --- /dev/null +++ b/documentation/developers/rfid/generic_nfcpy.md @@ -0,0 +1,36 @@ +# Generic NFCPy Reader + +This module is based on the user space NFC reader library [nfcpy](https://nfcpy.readthedocs.io/en/latest/overview.html) ([on github](https://github.com/nfcpy/nfcpy)). +The link above also contains a list of [supported devices](https://nfcpy.readthedocs.io/en/latest/overview.html#supported-devices). + +The goal of this module is to handle USB NFC devices, that don't have a HID-keyboard +driver, and thus cannot be used with the [genericusb](genericusb.md) module. + +> [!NOTE] +> Since nfcpy is a user-space library, it is required to supress the kernel from loading its driver. +> The setup will do this automatically, so make sure the device is connected +> before running the [RFID reader configuration tool](../coreapps.md#RFID-Reader). + +# Configuration + +The installation script will scan for compatible devices and will assist in configuration. +By setting `rfid > readers > generic_nfcpy > config > device_path` in `shared/settings/rfid.yaml` you can override the +device location. By specifying an explicit device location it is possible to use multiple readers compatible with NFCpy. + +Example configuration for a usb-device with vendor ID 072f and product ID 2200: +```yaml +rfid: + readers: + read_00: + module: generic_nfcpy + config: + device_path: usb:072f:2200 + same_id_delay: 1.0 + log_ignored_cards: false + place_not_swipe: + enabled: false + card_removal_action: + alias: pause +``` + +For possible values see the `path` parameter in this [nfcpy documentation](https://nfcpy.readthedocs.io/en/latest/modules/clf.html#nfc.clf.ContactlessFrontend.open) \ No newline at end of file diff --git a/src/jukebox/components/rfid/hardware/generic_nfcpy/README.md b/src/jukebox/components/rfid/hardware/generic_nfcpy/README.md new file mode 100644 index 000000000..f1c92ded0 --- /dev/null +++ b/src/jukebox/components/rfid/hardware/generic_nfcpy/README.md @@ -0,0 +1,2 @@ + +For documentation see [documentation/developers/rfid/generic_nfcpy.md](../../../../../../documentation/developers/rfid/generic_nfcpy.md). diff --git a/src/jukebox/components/rfid/hardware/generic_nfcpy/description.py b/src/jukebox/components/rfid/hardware/generic_nfcpy/description.py new file mode 100644 index 000000000..eea9438eb --- /dev/null +++ b/src/jukebox/components/rfid/hardware/generic_nfcpy/description.py @@ -0,0 +1,3 @@ +""" List of supported devices https://nfcpy.readthedocs.io/en/latest/overview.html""" +# 40 chars: '========================================' +DESCRIPTION = 'Generic NFCPY NFC Reader Module' diff --git a/src/jukebox/components/rfid/hardware/generic_nfcpy/generic_nfcpy.py b/src/jukebox/components/rfid/hardware/generic_nfcpy/generic_nfcpy.py new file mode 100644 index 000000000..4b6ac0051 --- /dev/null +++ b/src/jukebox/components/rfid/hardware/generic_nfcpy/generic_nfcpy.py @@ -0,0 +1,128 @@ +# Standard imports from python packages +import logging + +import nfc +import glob +from nfc.clf import RemoteTarget +import nfc.clf.device + +# Import the ReaderBaseClass for common API. Leave as this line as it is! +from components.rfid import ReaderBaseClass +import jukebox.cfghandler +import misc.inputminus as pyil +from misc.simplecolors import Colors + +# Also import the description string into this module, to make everything available in a single module w/o code duplication +# Leave this line as is! +from .description import DESCRIPTION + +# Create logger. +logger = logging.getLogger('jb.rfid.nfcpy') +# Get the global handler to the RFID config +cfg = jukebox.cfghandler.get_handler('rfid') + + +def query_customization() -> dict: + # filter all log records from nfc.clf + loggerNfcClf = logging.getLogger('nfc.clf') + loggerNfcClf.filter = lambda record: 0 + + devices = [] + clf = nfc.ContactlessFrontend() + + # find usb devices + for vid_pid_pair in nfc.clf.device.usb_device_map.keys(): + device_id = "usb:%04x:%04x" % vid_pid_pair + if clf.open(device_id): + devices.append({'id': device_id, 'vendor': clf.device.vendor_name, 'name': clf.device.product_name}) + clf.close() + + # find tty device + matching_files = glob.glob("/dev/ttyUSB[0-9]*") + matching_files += glob.glob("/dev/ttyAMA[0-9]*") + for file_path in matching_files: + for driver in nfc.clf.device.tty_driver_list: + device_id = f'{file_path}:{driver}' + if clf.open(device_id): + devices.append({'id': device_id, 'vendor': clf.device.vendor_name, 'name': clf.device.product_name}) + clf.close() + + print("\nChoose RFID device from USB device list:\n") + logger.debug(f"USB devices: {[x['name'] for x in devices]}") + if len(devices) == 0: + logger.error("USB device list is empty. Make sure USB RFID reader is connected. Then re-run reader registration") + return {'device_path': None} + + for idx, dev in enumerate(devices): + print(f" {Colors.lightgreen}{idx:2d}{Colors.reset}:" + f"{Colors.lightcyan}{Colors.bold}{dev['vendor']} {dev['name']}{Colors.reset}") + + dev_id = pyil.input_int("Device number?", min=0, max=len(devices) - 1, prompt_color=Colors.lightgreen, prompt_hint=True) + device_path = devices[dev_id]['id'] + + return {'device_path': device_path} + + +class ReaderClass(ReaderBaseClass): + """ + The reader class for nfcpy supported NFC card readers. + """ + def __init__(self, reader_cfg_key): + # Create a per-instance logger, just in case the reader will run multiple times in various threads + self._logger = logging.getLogger(f'jb.rfid.nfcpy({reader_cfg_key})') + # Initialize the super-class. Don't change anything here + super().__init__(reader_cfg_key=reader_cfg_key, description=DESCRIPTION, logger=self._logger) + + # Get the configuration from the rfid.yaml: + # Lock config around the access + with cfg: + # Get a reference to the actual reader-specific config + config = cfg.getn('rfid', 'readers', reader_cfg_key, 'config', default=None) + if config is None: + self._logger.error("Configuration may not be empty!!") + raise KeyError("configuration may not be empty!!") + + device_path = config.setdefault('device_path', None) + self.clf = nfc.ContactlessFrontend() + self.clf.open(device_path) + + self._keep_running = True + + def cleanup(self): + """ + The cleanup function: free and release all resources used by this card reader (if any). + """ + self.clf.close() + + def stop(self): + """ + This function is called to tell the reader to exit its reading function. + """ + self._keep_running = False + + def read_card(self) -> str: + """ + Blocking or non-blocking function that waits for a new card to appear and return the card's UID as string + """ + self._logger.debug("Wait for card") + while self._keep_running: + target = self.clf.sense(RemoteTarget('106A'), + RemoteTarget('106B'), + RemoteTarget('212F'), + interval=0.1, + iterations=1) + if not target: + continue + + tag = nfc.tag.activate(self.clf, target) + if not tag: + continue + + id = '' + for char in tag.identifier: + id += '%02X' % char + + self._logger.debug(f'Found card with ID: "{id}"') + return id + self._logger.debug("NFC read stopped") + return '' diff --git a/src/jukebox/components/rfid/hardware/generic_nfcpy/requirements.txt b/src/jukebox/components/rfid/hardware/generic_nfcpy/requirements.txt new file mode 100644 index 000000000..8fb09c9db --- /dev/null +++ b/src/jukebox/components/rfid/hardware/generic_nfcpy/requirements.txt @@ -0,0 +1,4 @@ +# NFCPy related requirements +# You need to install these with `python -m pip install --upgrade --force-reinstall -q -r requirements.txt` + +nfcpy diff --git a/src/jukebox/components/rfid/hardware/generic_nfcpy/setup.inc.sh b/src/jukebox/components/rfid/hardware/generic_nfcpy/setup.inc.sh new file mode 100755 index 000000000..d831b3de9 --- /dev/null +++ b/src/jukebox/components/rfid/hardware/generic_nfcpy/setup.inc.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +CURRENT_USER="${SUDO_USER:-$(whoami)}" + +modprobe_file="/etc/modprobe.d/disable_driver_jukebox_nfcpy.conf" + +if [ -e "$modprobe_file" ]; then + sudo rm -f "$modprobe_file" +fi +if lsmod | grep "pn533_usb"; then + sudo rmmod pn533_usb 2>/dev/null + sudo sh -c "echo 'install pn533_usb /bin/true' >> $modprobe_file" +fi + +if lsmod | grep "port100"; then + sudo rmmod port100 2>/dev/null + sudo sh -c "echo 'install port100 /bin/true' >> $modprobe_file" +fi + +udev_file="/etc/udev/rules.d/50-usb-nfc-rule.rules" + +usb_devices=$(lsusb | sed -e 's/.*ID \([a-f0-9]\+:[a-f0-9]\+\).*/\1/g') + +valid_device_ids=($(python -c "import nfc.clf.device; [print('%04x:%04x' % x) for x in nfc.clf.device.usb_device_map.keys()]")) + +if [ -e "$udev_file" ]; then + sudo rm -f "$udev_file" +fi +for dev in $usb_devices; do + if echo ${valid_device_ids[@]} | grep -woq $dev; then + #$dev is in valid id array + VID="${dev%%:*}" + PID="${dev#*:}" + sudo sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"$VID\", ATTRS{idProduct}==\"$PID\", GROUP=\"plugdev\"' >> $udev_file" + fi +done +sudo udevadm control --reload-rules +sudo udevadm trigger + +sudo gpasswd -a $CURRENT_USER plugdev +sudo gpasswd -a $CURRENT_USER dialout +sudo gpasswd -a $CURRENT_USER tty \ No newline at end of file From b2a6517b306d5bba9247038be126786afd1aef1a Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:03:07 +0100 Subject: [PATCH 14/24] included venv activation for python scripts (#2233) * add setup script for rfid reader. moved python script * add check for venv activation * fix * fix cd fail behavior (dont start subprocess) * add script for run jukebox for venv. updated docs. * add setup script for audio sink. moved python script * add script for rpc tool. moved python script * add script for sniffer tool. moved python script * fixed parameter passing * fix paths * fix flake8 * fix comments and docs * fixed parameter passing * some fixes * fixed execution rights * some fixes * move scripts to python root again (failing import paths). fixed absolute path in caller scripts * fix relative paths * update notes about config file paths * update caller scripts path handling * update comments * add venv activation to tests * explicit installation of python3-venv not needed * renamed audio setup file. updated docs path. * Update configuration.md * Update coreapps.md * update motd message * fix execution rights for motd file. refactored into own postinstall script * fix execution right for audio setup --------- Co-authored-by: pabera <1260686+pabera@users.noreply.github.com> --- .githooks/post-merge | 1 + .github/workflows/codeql-analysis_v3.yml | 3 ++ .github/workflows/pythonpackage_future3.yml | 4 +- documentation/builders/configuration.md | 20 +++++----- documentation/builders/troubleshooting.md | 9 ++--- documentation/developers/coreapps.md | 37 +++++++++---------- documentation/developers/known-issues.md | 2 +- .../components/setup_configure_audio.sh | 16 ++++++++ installation/components/setup_rfid_reader.sh | 16 ++++++++ installation/routines/install.sh | 2 +- installation/routines/set_raspi_config.sh | 4 -- installation/routines/setup_postinstall.sh | 13 +++++++ installation/routines/setup_rfid_reader.sh | 4 +- .../default-services/jukebox-daemon.service | 4 +- .../default-settings/jukebox.default.yaml | 2 +- .../default-settings/pulseaudio.default.pa | 2 +- resources/system/99-rpi-jukebox-rfid-welcome | 12 ++---- run_docgeneration.sh | 10 +++-- run_flake8.sh | 11 ++++-- run_jukebox.sh | 16 ++++++++ run_pytest.sh | 11 ++++-- src/jukebox/components/volume/__init__.py | 2 +- src/jukebox/run_configure_audio.py | 0 src/jukebox/run_publicity_sniffer.py | 0 src/jukebox/run_register_rfid_reader.py | 3 +- src/jukebox/run_rpc_tool.py | 0 tools/run_publicity_sniffer.sh | 16 ++++++++ tools/run_rpc_tool.sh | 16 ++++++++ 28 files changed, 166 insertions(+), 70 deletions(-) create mode 100755 installation/components/setup_configure_audio.sh create mode 100755 installation/components/setup_rfid_reader.sh create mode 100644 installation/routines/setup_postinstall.sh create mode 100755 run_jukebox.sh mode change 100755 => 100644 src/jukebox/run_configure_audio.py mode change 100755 => 100644 src/jukebox/run_publicity_sniffer.py mode change 100755 => 100644 src/jukebox/run_register_rfid_reader.py mode change 100755 => 100644 src/jukebox/run_rpc_tool.py create mode 100755 tools/run_publicity_sniffer.sh create mode 100755 tools/run_rpc_tool.sh diff --git a/.githooks/post-merge b/.githooks/post-merge index 6fc6e4f54..07b81db41 100755 --- a/.githooks/post-merge +++ b/.githooks/post-merge @@ -43,6 +43,7 @@ warn_python_requirements() { echo "ATTENTION: Python requirements have changed since last pull!" echo "" echo "To update python requirements on the RPi run" + echo "$ source .venv/bin/activate" echo "$ python -m pip install --upgrade -r requirements.txt" echo "************************************************************" echo -e "\n" diff --git a/.github/workflows/codeql-analysis_v3.yml b/.github/workflows/codeql-analysis_v3.yml index 1c1c68ba3..89693df2a 100644 --- a/.github/workflows/codeql-analysis_v3.yml +++ b/.github/workflows/codeql-analysis_v3.yml @@ -45,6 +45,9 @@ jobs: run: | # Install necessary packages 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-PYTHON` environment variable to the Python executable diff --git a/.github/workflows/pythonpackage_future3.yml b/.github/workflows/pythonpackage_future3.yml index f2174d0c0..2236a5a47 100644 --- a/.github/workflows/pythonpackage_future3.yml +++ b/.github/workflows/pythonpackage_future3.yml @@ -31,6 +31,9 @@ jobs: run: | sudo apt-get update sudo apt-get install -y libasound2-dev pulseaudio + python3 -m venv .venv + source ".venv/bin/activate" + python3 -m pip install --upgrade pip pip3 install -r requirements.txt # For operation of the Jukebox, ZMQ must be compiled from sources due to Websocket support @@ -51,7 +54,6 @@ jobs: parallel: true - name: Lint with flake8 run: | - pip3 install flake8 # Stop the build if linting fails ./run_flake8.sh diff --git a/documentation/builders/configuration.md b/documentation/builders/configuration.md index 2e1c4ff23..dd4c1fd53 100644 --- a/documentation/builders/configuration.md +++ b/documentation/builders/configuration.md @@ -1,15 +1,15 @@ # Jukebox Configuration 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. +Some configuration changes can be made through the Web App and take immediate effect. -The majority of configuration options is only available by editing the config files - +The majority of configuration options are 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 [configuration tools](../developers/coreapps.md#configuration-tools) and [detailed guides](./README.md#features). +For several aspects, we have [configuration tools](../developers/coreapps.md#configuration-tools) and [detailed guides](./README.md#features). -Even after running the tools, certain aspects can only be changed by modifying the configuration files directly. +Even after using the tools, certain aspects can only be changed by directly modifying the configuration files. ## Best practice procedure @@ -21,19 +21,17 @@ $ systemctl --user stop jukebox-daemon $ nano ./shared/settings/jukebox.yaml # Start Jukebox in console and check the log output (optional) -$ cd src/jukebox -$ ./run_jukebox.py +$ ./run_jukebox.sh # and if OK, press Ctrl-C and restart the service # Restart the service $ systemctl --user start jukebox-daemon ``` - -To try different configurations, you can start the Jukebox with a custom config file. +To try different configurations, you can start the Jukebox with a custom config file. This could be useful if you want your Jukebox to only allow a lower volume when started -at night time when there is time to go to bed :-) +at nighttime, signaling it's time to go to bed. :-) +The path to the custom config file must be either absolute or relative to the folder `src/jukebox/`. ```bash -$ cd src/jukebox -$ ./run_jukebox.py --conf path/to/custom/config.yaml +$ ./run_jukebox.sh --conf /absolute/path/to/custom/config.yaml ``` diff --git a/documentation/builders/troubleshooting.md b/documentation/builders/troubleshooting.md index ffc5189dc..5b4061aa8 100644 --- a/documentation/builders/troubleshooting.md +++ b/documentation/builders/troubleshooting.md @@ -64,12 +64,10 @@ on the console log. $ systemctl --user stop jukebox-daemon # Start the Jukebox in debug mode: -$ cd src/jukebox - # with default logger: -$ ./run_jukebox.py +$ ./run_jukebox.sh # or with custom logger configuration: -$ ./run_jukebox.py --logger path/to/custom/logger.yaml +$ ./run_jukebox.sh --logger path/to/custom/logger.yaml ``` ### Fallback configuration @@ -79,8 +77,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 +$ ./run_jukebox.sh -vv ``` ### Extreme cases diff --git a/documentation/developers/coreapps.md b/documentation/developers/coreapps.md index f1402683d..80b3c043a 100644 --- a/documentation/developers/coreapps.md +++ b/documentation/developers/coreapps.md @@ -1,7 +1,6 @@ # Jukebox Apps -The Jukebox\'s core apps are located in `src/jukebox`. Run the following -command to learn more about each app and its parameters: +The Jukebox's core apps are located in `src/jukebox`. To learn more about each app and its parameters, run the following command: ``` bash $ cd src/jukebox @@ -10,13 +9,13 @@ $ ./ -h ## Jukebox Core -**Scriptname:** [run_jukebox.py](../../src/jukebox/run_jukebox.py) +**Scriptname:** [run_jukebox.sh](../../run_jukebox.sh) -This is the main app and starts the Jukebox Core. +This is the main app. It 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](../builders/configuration.md#jukebox-configuration). +This runs as a service, which starts 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's best to run Jukebox directly from the console rather than as a service, as this provides direct logging information in the console and allows for changing command line parameters. See [Troubleshooting](../builders/troubleshooting.md). ## Configuration Tools @@ -25,41 +24,39 @@ See [Best practice procedure](../builders/configuration.md#best-practice-procedu ### Audio -**Scriptname:** [run_configure_audio.py](../../src/jukebox/run_configure_audio.py) +**Scriptname:** [setup_configure_audio.sh](../../installation/components/setup_configure_audio.sh) -Setup tool to register the PulseAudio sinks as primary and secondary audio outputs. +A setup tool to register the PulseAudio sinks as primary and secondary audio outputs. -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). +This will also set up an equalizer and mono downmixer in the PulseAudio configuration file. Run this once after installation. It can be re-run at any time to change the settings. For more information see [Audio Configuration](../builders/audio.md). ### RFID Reader -**Scriptname:** [run_register_rfid_reader.py](../../src/jukebox/run_register_rfid_reader.py) +**Scriptname:** [setup_rfid_reader.sh](../../installation/components/setup_rfid_reader.sh) 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). +Run this once to register and configure the RFID readers with Jukebox. It 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 +> This tool will always create a new configuration file, thereby overwriting the old one (after confirming with the user). Any manual modifications to the settings will need to be reapplied. ## Developer Tools ### RPC -**Scriptname:** [run_rpc_tool.py](../../src/jukebox/run_rpc_tool.py) +**Scriptname:** [run_rpc_tool.sh](../../tools/run_rpc_tool.sh) Command Line Interface to the Jukebox RPC Server. -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. Use `./run_rpc_tool.py` to start the tool in interactive mode. +A command-line tool for sending RPC commands to the running Jukebox app, utilizing the same interface as the Web App, provides additional control or debugging capabilities. Start the tool in interactive mode with `./run_rpc_tool.sh`. -The tool features auto-completion and command history. +Features include auto-completion and command history, with available commands fetched from the running Jukebox service. -The list of available commands is fetched from the running Jukebox service. - -The tool can also be used to send commands directly, when passing a `-c` argument, e.g. `./run_rpc_tool.py -c host.shutdown`. +For direct command execution, use the `-c` argument, e.g., `./run_rpc_tool.sh -c host.shutdown`. ### Publicity Sniffer -**Scriptname:** [run_publicity_sniffer.py](../../src/jukebox/run_publicity_sniffer.py) +**Scriptname:** [run_publicity_sniffer.sh](../../tools/run_publicity_sniffer.sh) -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. +This command-line tool monitors all messages sent from Jukebox through the publishing interface, printing received messages in the console. It is primarily used for debugging. diff --git a/documentation/developers/known-issues.md b/documentation/developers/known-issues.md index db3429bcc..74823a16a 100644 --- a/documentation/developers/known-issues.md +++ b/documentation/developers/known-issues.md @@ -21,6 +21,6 @@ RUN cd ${HOME} && mkdir ${ZMQ_TMP_DIR} && cd ${ZMQ_TMP_DIR}; \ ## Configuration In `jukebox.yaml` (and all other config files): -Always use relative path from settingsfile `../../`, but do not use relative paths with `~/`. +Always use relative path from folder `src/jukebox` (`../../`), but do not use relative paths with `~/`. **Sole** exception is in `playermpd.mpd_conf`. diff --git a/installation/components/setup_configure_audio.sh b/installation/components/setup_configure_audio.sh new file mode 100755 index 000000000..dc1313e1c --- /dev/null +++ b/installation/components/setup_configure_audio.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Runner script to ensure +# - correct venv activation +# - independent from working directory + +# Change working directory to project root +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +PROJECT_ROOT="$SCRIPT_DIR"/../.. +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } + +cd src/jukebox || { echo "Could not change directory"; exit 1; } +python run_configure_audio.py $@ diff --git a/installation/components/setup_rfid_reader.sh b/installation/components/setup_rfid_reader.sh new file mode 100755 index 000000000..ebabcf12b --- /dev/null +++ b/installation/components/setup_rfid_reader.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Runner script to ensure +# - correct venv activation +# - independent from working directory + +# Change working directory to project root +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +PROJECT_ROOT="$SCRIPT_DIR"/../.. +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } + +cd src/jukebox || { echo "Could not change directory"; exit 1; } +python run_register_rfid_reader.py $@ diff --git a/installation/routines/install.sh b/installation/routines/install.sh index 66a65203c..4ff3e1ac9 100644 --- a/installation/routines/install.sh +++ b/installation/routines/install.sh @@ -15,6 +15,6 @@ install() { setup_rfid_reader optimize_boot_time setup_autohotspot - setup_login_message + setup_postinstall cleanup } diff --git a/installation/routines/set_raspi_config.sh b/installation/routines/set_raspi_config.sh index 6bfa8e725..7f39a0ba5 100644 --- a/installation/routines/set_raspi_config.sh +++ b/installation/routines/set_raspi_config.sh @@ -31,7 +31,3 @@ _run_set_raspi_config() { set_raspi_config() { run_with_log_frame _run_set_raspi_config "Set default raspi-config" } - -setup_login_message() { - sudo cp -f "${INSTALLATION_PATH}/resources/system/99-rpi-jukebox-rfid-welcome" "/etc/update-motd.d/99-rpi-jukebox-rfid-welcome" -} diff --git a/installation/routines/setup_postinstall.sh b/installation/routines/setup_postinstall.sh new file mode 100644 index 000000000..f448635f4 --- /dev/null +++ b/installation/routines/setup_postinstall.sh @@ -0,0 +1,13 @@ +_setup_login_message() { + local login_message_welcome_file="/etc/update-motd.d/99-rpi-jukebox-rfid-welcome" + sudo cp -f "${INSTALLATION_PATH}/resources/system/99-rpi-jukebox-rfid-welcome" "$login_message_welcome_file" + sudo chmod +x "$login_message_welcome_file" +} + +_run_setup_postinstall() { + _setup_login_message +} + +setup_postinstall() { + run_with_log_frame _run_setup_postinstall "Post install" +} diff --git a/installation/routines/setup_rfid_reader.sh b/installation/routines/setup_rfid_reader.sh index 3003d79a4..c7f3b7120 100644 --- a/installation/routines/setup_rfid_reader.sh +++ b/installation/routines/setup_rfid_reader.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash _run_setup_rfid_reader() { - run_and_print_lc python "${INSTALLATION_PATH}/src/jukebox/run_register_rfid_reader.py" + local script="${INSTALLATION_PATH}"/installation/components/setup_rfid_reader.sh + sudo chmod +x "$script" + run_and_print_lc "$script" } setup_rfid_reader() { diff --git a/resources/default-services/jukebox-daemon.service b/resources/default-services/jukebox-daemon.service index 050897e14..7898088e2 100644 --- a/resources/default-services/jukebox-daemon.service +++ b/resources/default-services/jukebox-daemon.service @@ -10,8 +10,8 @@ After=network.target sound.target mpd.service pulseaudio.service Requires=mpd.service pulseaudio.service [Service] -WorkingDirectory=%%INSTALLATION_PATH%%/src/jukebox -ExecStart=/bin/bash -c 'source %%INSTALLATION_PATH%%/.venv/bin/activate && python run_jukebox.py' +WorkingDirectory=%%INSTALLATION_PATH%% +ExecStart=/bin/bash -c '%%INSTALLATION_PATH%%/run_jukebox.sh' StandardOutput=inherit StandardError=inherit Restart=always diff --git a/resources/default-settings/jukebox.default.yaml b/resources/default-settings/jukebox.default.yaml index 5c37f178a..c087cc024 100644 --- a/resources/default-settings/jukebox.default.yaml +++ b/resources/default-settings/jukebox.default.yaml @@ -1,5 +1,5 @@ # IMPORTANT: -# Always use relative path from settingsfile `../../`, but do not use relative paths with `~/`. +# Always use relative path from folder `src/jukebox` (`../../`), but do not use relative paths with `~/`. # Sole (!) exception is in playermpd.mpd_conf system: box_name: Jukebox diff --git a/resources/default-settings/pulseaudio.default.pa b/resources/default-settings/pulseaudio.default.pa index ee91ff9bf..e4d20ec53 100644 --- a/resources/default-settings/pulseaudio.default.pa +++ b/resources/default-settings/pulseaudio.default.pa @@ -147,5 +147,5 @@ load-module module-filter-apply #set-default-source input ### Configuration by Jukebox's Tool may come below -# Run ./run_configure_audio.py for configuration +# Run ./installation/components/setup_configure_audio.sh for configuration diff --git a/resources/system/99-rpi-jukebox-rfid-welcome b/resources/system/99-rpi-jukebox-rfid-welcome index 1b9904f96..50cc71cc7 100644 --- a/resources/system/99-rpi-jukebox-rfid-welcome +++ b/resources/system/99-rpi-jukebox-rfid-welcome @@ -1,16 +1,12 @@ #!/usr/bin/env bash echo " -######################################################### +################################################## ___ __ ______ _ __________ ____ __ _ _ / _ \/ // / __ \/ |/ / _/ __/( _ \ / \( \/ ) / ___/ _ / /_/ / // // _/ ) _ (( O )) ( /_/ /_//_/\____/_/|_/___/____/ (____/ \__/(_/\_) -future3 -If you want to run a python script from the project -activate the venv before with 'source .venv/bin/activate' -See also https://github.com/MiczFlor/RPi-Jukebox-RFID/ -blob/future3/main/documentation/developers/python.md - -#########################################################" + Welcome to your Phoniebox +################################################## +" diff --git a/run_docgeneration.sh b/run_docgeneration.sh index 22ab8bc14..4fb5f1dc9 100755 --- a/run_docgeneration.sh +++ b/run_docgeneration.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash -# Runner script for pydoc-markdown to ensure +# Runner script to ensure +# - correct venv activation # - independent from working directory -# Change working directory to location of script +# Change working directory to project root SOURCE=${BASH_SOURCE[0]} SCRIPT_DIR="$(dirname "$SOURCE")" -cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) +PROJECT_ROOT="$SCRIPT_DIR" +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } # Run pydoc-markdown # make sure, directory exists diff --git a/run_flake8.sh b/run_flake8.sh index a9ec73285..f6603dc0a 100755 --- a/run_flake8.sh +++ b/run_flake8.sh @@ -1,13 +1,16 @@ #!/usr/bin/env bash -# Runner script for flak8 to ensure -# - correct config file +# Runner script to ensure +# - correct venv activation # - independent from working directory -# Change working directory to location of script +# Change working directory to project root SOURCE=${BASH_SOURCE[0]} SCRIPT_DIR="$(dirname "$SOURCE")" -cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) +PROJECT_ROOT="$SCRIPT_DIR" +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } # Run flake8 flake8 --config .flake8 "$@" diff --git a/run_jukebox.sh b/run_jukebox.sh new file mode 100755 index 000000000..48e8aa7ba --- /dev/null +++ b/run_jukebox.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Runner script to ensure +# - correct venv activation +# - independent from working directory + +# Change working directory to project root +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +PROJECT_ROOT="$SCRIPT_DIR" +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } + +cd src/jukebox || { echo "Could not change directory"; exit 1; } +python run_jukebox.py $@ diff --git a/run_pytest.sh b/run_pytest.sh index 766f05182..c944419ae 100755 --- a/run_pytest.sh +++ b/run_pytest.sh @@ -1,13 +1,16 @@ #!/usr/bin/env bash -# Runner script for pytest to ensure -# - correct config file +# Runner script to ensure +# - correct venv activation # - independent from working directory -# Change working directory to location of script +# Change working directory to project root SOURCE=${BASH_SOURCE[0]} SCRIPT_DIR="$(dirname "$SOURCE")" -cd "$SCRIPT_DIR" || (echo "Could not change to top-level project directory" && exit 1) +PROJECT_ROOT="$SCRIPT_DIR" +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } # Run pytest pytest -c pytest.ini $@ diff --git a/src/jukebox/components/volume/__init__.py b/src/jukebox/components/volume/__init__.py index 4782581ca..9dd827e4a 100644 --- a/src/jukebox/components/volume/__init__.py +++ b/src/jukebox/components/volume/__init__.py @@ -603,7 +603,7 @@ def parse_config() -> List[PulseAudioSinkClass]: logger.error(f"Configured sink '{pulse_sink_name}' not available sinks '{all_sinks}!\n" f"Using default sink '{default_sink_name}' as fallback\n" f"Things like audio sink toggle and volume limit will not work as expected!\n" - f"Please run audio config tool: ./run_configure_audio.py") + f"Please run audio config tool: ./installation/components/setup_configure_audio.sh") sink_list.append(PulseAudioSinkClass(alias, pulse_sink_name, volume_limit)) key = 'secondary' diff --git a/src/jukebox/run_configure_audio.py b/src/jukebox/run_configure_audio.py old mode 100755 new mode 100644 diff --git a/src/jukebox/run_publicity_sniffer.py b/src/jukebox/run_publicity_sniffer.py old mode 100755 new mode 100644 diff --git a/src/jukebox/run_register_rfid_reader.py b/src/jukebox/run_register_rfid_reader.py old mode 100755 new mode 100644 index 18a1614d8..91d363157 --- a/src/jukebox/run_register_rfid_reader.py +++ b/src/jukebox/run_register_rfid_reader.py @@ -33,7 +33,8 @@ def main(): # The default config file relative to this files location and independent of working directory - cfg_file_default = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/../../shared/settings/rfid.yaml') + script_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) + cfg_file_default = os.path.abspath(os.path.join(script_path, '../../shared/settings/rfid.yaml')) parser = argparse.ArgumentParser() parser.add_argument("-f", "--force", diff --git a/src/jukebox/run_rpc_tool.py b/src/jukebox/run_rpc_tool.py old mode 100755 new mode 100644 diff --git a/tools/run_publicity_sniffer.sh b/tools/run_publicity_sniffer.sh new file mode 100755 index 000000000..2ea47b48b --- /dev/null +++ b/tools/run_publicity_sniffer.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Runner script to ensure +# - correct venv activation +# - independent from working directory + +# Change working directory to project root +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +PROJECT_ROOT="$SCRIPT_DIR"/.. +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } + +cd src/jukebox || { echo "Could not change directory"; exit 1; } +python run_publicity_sniffer.py $@ diff --git a/tools/run_rpc_tool.sh b/tools/run_rpc_tool.sh new file mode 100755 index 000000000..dad895c56 --- /dev/null +++ b/tools/run_rpc_tool.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Runner script to ensure +# - correct venv activation +# - independent from working directory + +# Change working directory to project root +SOURCE=${BASH_SOURCE[0]} +SCRIPT_DIR="$(dirname "$SOURCE")" +PROJECT_ROOT="$SCRIPT_DIR"/.. +cd "$PROJECT_ROOT" || { echo "Could not change directory"; exit 1; } + +source .venv/bin/activate || { echo "ERROR: Failed to activate virtual environment for python"; exit 1; } + +cd src/jukebox || { echo "Could not change directory"; exit 1; } +python run_rpc_tool.py $@ From 1e1ae258f0837026c944cb5be93111e894fc2f5e Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Sun, 4 Feb 2024 17:06:16 +0100 Subject: [PATCH 15/24] prevent installer rerun and remove update path (#2235) * fix doc * prevent run with existing installation * update docs for update path * Update documentation/builders/update.md Co-authored-by: s-martin --------- Co-authored-by: s-martin --- .githooks/post-merge | 17 ++++++++++++++++- documentation/builders/gpio.md | 3 +-- documentation/builders/update.md | 30 +++++------------------------- installation/install-jukebox.sh | 15 ++++++++++++++- 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/.githooks/post-merge b/.githooks/post-merge index 07b81db41..59e132b7c 100755 --- a/.githooks/post-merge +++ b/.githooks/post-merge @@ -58,7 +58,17 @@ warn_githooks() { echo "$ cp .githooks/* .git/hooks/." echo "************************************************************" echo -e "\n" +} +warn_installer() { + echo -e "\n" + echo "************************************************************" + echo "ATTENTION: Installer sources have changed since last pull!" + echo "" + echo "Rerun the installer to apply changes" + echo "$ ./installation/install-jukebox.sh" + echo "************************************************************" + echo -e "\n" } # files_changed="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)" @@ -66,6 +76,7 @@ webapp_changed="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD src webapp_dep_changed="$(git diff --name-only --no-commit-id ORIG_HEAD HEAD src/webapp/package.json)" python_req_changed="$(git diff --name-only --no-commit-id ORIG_HEAD HEAD requirements.txt)" githooks_changed="$(git diff --name-only --no-commit-id ORIG_HEAD HEAD .githooks)" +installer_changed="$(git diff --name-only --no-commit-id ORIG_HEAD HEAD installation)" if [[ -n $python_req_changed ]]; then warn_python_requirements @@ -81,5 +92,9 @@ if [[ -n $githooks_changed ]]; then warn_githooks fi +if [[ -n $installer_changed ]]; then + warn_installer +fi + echo -e "\nTo see a summary of what happened since your last pull, do:" -echo -e "git show --oneline -s ORIG_HEAD..HEAD\n" \ No newline at end of file +echo -e "git show --oneline -s ORIG_HEAD..HEAD\n" diff --git a/documentation/builders/gpio.md b/documentation/builders/gpio.md index 37622edc0..9d496c6b7 100644 --- a/documentation/builders/gpio.md +++ b/documentation/builders/gpio.md @@ -2,8 +2,7 @@ ## Enabling GPIO -The GPIO module needs to be enabled in your main configuration file ``shared/settings/jukebox.yaml``. Look for the -this entry and modify it accordingly: +The GPIO module needs to be enabled in your main configuration file ``shared/settings/jukebox.yaml``. Look for this entry and modify it accordingly: ```yml gpioz: diff --git a/documentation/builders/update.md b/documentation/builders/update.md index e84655e7c..cb919492a 100644 --- a/documentation/builders/update.md +++ b/documentation/builders/update.md @@ -2,34 +2,14 @@ ## Updating your Jukebox Version 3 -### Update from v3.2.1 and prior +### Update from v3.5.0 and prior -As there are some significant changes in the installation, a new setup on a fresh image is required. +As there are some significant changes in the Jukebox installation, no updates can be performed with the installer. +Please backup your './shared' folder and changed files and run a new installation on a fresh image. +Restore your old files after the new installation was successful and check if new mandatory settings have been added. -### 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. - -You will need to do three things to update your version from develop (or the next release candidate version) - -1. Pull the newest code base from Github -2. Check for new entries in the configuration -3. Re-build the WebUI - -```bash -# Switch to develop (if desired) -$ git checkout future3/develop - -# Get latest code -$ git pull - -# Check if new (mandatory) options appeared in jukebox.yaml -# with your favourite diff tool and merge them +``` bash $ diff shared/settings/jukebox.yaml resources/default-settings/jukebox.default.yaml - -$ cd src/webapp -$ ./run_rebuild.sh -u ``` ## Migration Path from Version 2 diff --git a/installation/install-jukebox.sh b/installation/install-jukebox.sh index 1ec3f9ad5..1ab96f3a1 100755 --- a/installation/install-jukebox.sh +++ b/installation/install-jukebox.sh @@ -103,6 +103,18 @@ _check_os_type() { fi } +_check_existing_installation() { + if [[ -e "${INSTALLATION_PATH}" ]]; then + print_lc " +############## EXISTING INSTALLATION FOUND ############## +Rerunning the installer over an existing installation is +currently not supported (overwrites settings, etc). +Please backup your 'shared' folder and manually changed +files and run the installation on a fresh image." + exit 1 + fi +} + _download_jukebox_source() { log "#########################################################" print_c "Downloading Phoniebox software from Github ..." @@ -122,7 +134,7 @@ _download_jukebox_source() { if [[ -z "${GIT_HASH}" ]]; then exit_on_error "ERROR: Couldn't determine git hash from download." fi - mv "$git_repo_download" "$GIT_REPO_NAME" + mv "$git_repo_download" "$GIT_REPO_NAME" || exit_on_error "ERROR: Can't overwrite existing installation." log "\nDONE: Downloading Phoniebox software from Github" log "#########################################################" } @@ -143,6 +155,7 @@ _setup_logging ### CHECK PREREQUISITE _check_os_type +_check_existing_installation ### RUN INSTALLATION log "Current User: $CURRENT_USER" From 32d57f5d73e57365859e8545394ae3b002a1d4a4 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 4 Feb 2024 20:07:18 +0100 Subject: [PATCH 16/24] Bump to v3.5.0 --- src/jukebox/jukebox/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jukebox/jukebox/version.py b/src/jukebox/jukebox/version.py index 37382cee7..65ccc87c0 100644 --- a/src/jukebox/jukebox/version.py +++ b/src/jukebox/jukebox/version.py @@ -2,7 +2,7 @@ VERSION_MAJOR = 3 VERSION_MINOR = 5 VERSION_PATCH = 0 -VERSION_EXTRA = "alpha" +VERSION_EXTRA = "" # build a version string in compliance with the SemVer specification # https://semver.org/#semantic-versioning-specification-semver From 2fe705507f4241c50bff882e31b0dff6121094e0 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Sun, 4 Feb 2024 20:37:32 +0100 Subject: [PATCH 17/24] NetworkManager Support (#2218) * reworked autohotspot configuration option * updated autohotspot creation (copied changes from v2) * fix placeholder prefix/suffix (%%) * moved files in subdir for dhcpcd * renamed setup_autohotspot file * added switch for network settings * added autohotspot based on NetworkManager * update function names * fixed duplicate variables * fix var check * handle CI run for autohotspot setup * changed network management check for ci * fixed timer service name * renamed script. delete obsolete file * fix syntax * fix timer service unit definition * move is is_service_enabled to helper class * extracted konstant and fixed formatting * fix indentation * added shebang again * some refactorings in autohotspot script * fix checks * fix for local vars and arrays. refactor nw_profiles. * refactored var value and log output * refactor device and ssid checks * refactore force_hotspot option * refactor is_active_ap check and bugfixes * update log output * made timer reenablement configurable * phoniebox header added * moved check to helper. removed obsolete code * removed ip configuration. changed ip * add note for static ip conf with autohotspot * harmonize logging and opts * changed dhcpcd timer from cron to systemd. add seperate daemon service for wpa-supplicant handling * fix wifi after restart if autohot spot is deactivated * add hostname to default hotspot ssid * updated autohotspot docs * fix static ip for NetworkManager. fix ipv6 disablement * fix boot file paths * some fixes * pull config file backup to helpers * combine service is-enabled calls * update network related docs * Update autohotspot.md Aligned some language and typos * Update installation.md Aligning wording and removing typos --------- Co-authored-by: pabera <1260686+pabera@users.noreply.github.com> --- ci/ci-debian.Dockerfile | 4 +- ci/installation/run_install_common.sh | 2 +- ci/installation/run_install_faststartup.sh | 2 + ci/installation/run_install_libzmq_local.sh | 1 + .../run_install_webapp_download.sh | 2 + ci/installation/run_install_webapp_local.sh | 1 + documentation/builders/autohotspot.md | 114 ++---- documentation/builders/installation.md | 15 +- installation/includes/00_constants.sh | 3 +- installation/includes/01_default_config.sh | 6 +- installation/includes/02_helpers.sh | 93 ++++- installation/routines/customize_options.sh | 124 ++++-- installation/routines/optimize_boot_time.sh | 140 ++++--- installation/routines/set_raspi_config.sh | 12 +- installation/routines/setup_autohotspot.sh | 117 ++---- .../setup_autohotspot_NetworkManager.sh | 98 +++++ .../routines/setup_autohotspot_dhcpcd.sh | 185 +++++++++ .../autohotspot/NetworkManager/autohotspot | 353 ++++++++++++++++++ .../NetworkManager/autohotspot.service | 11 + .../NetworkManager/autohotspot.timer | 10 + resources/autohotspot/autohotspot | 193 ---------- resources/autohotspot/autohotspot.service | 11 - resources/autohotspot/autohotspot.timer | 3 - resources/autohotspot/dhcpcd/autohotspot | 228 +++++++++++ .../dhcpcd/autohotspot-daemon.service | 10 + .../autohotspot/dhcpcd/autohotspot.service | 11 + .../autohotspot/dhcpcd/autohotspot.timer | 10 + resources/autohotspot/dhcpcd/dnsmasq.conf | 7 + resources/autohotspot/dhcpcd/hostapd | 20 + .../autohotspot/{ => dhcpcd}/hostapd.conf | 8 +- src/cli_client/pbc.c | 32 +- .../components/hostif/linux/__init__.py | 52 ++- 32 files changed, 1327 insertions(+), 551 deletions(-) create mode 100644 installation/routines/setup_autohotspot_NetworkManager.sh create mode 100644 installation/routines/setup_autohotspot_dhcpcd.sh create mode 100644 resources/autohotspot/NetworkManager/autohotspot create mode 100644 resources/autohotspot/NetworkManager/autohotspot.service create mode 100644 resources/autohotspot/NetworkManager/autohotspot.timer delete mode 100644 resources/autohotspot/autohotspot delete mode 100644 resources/autohotspot/autohotspot.service delete mode 100644 resources/autohotspot/autohotspot.timer create mode 100644 resources/autohotspot/dhcpcd/autohotspot create mode 100644 resources/autohotspot/dhcpcd/autohotspot-daemon.service create mode 100644 resources/autohotspot/dhcpcd/autohotspot.service create mode 100644 resources/autohotspot/dhcpcd/autohotspot.timer create mode 100644 resources/autohotspot/dhcpcd/dnsmasq.conf create mode 100644 resources/autohotspot/dhcpcd/hostapd rename resources/autohotspot/{ => dhcpcd}/hostapd.conf (66%) diff --git a/ci/ci-debian.Dockerfile b/ci/ci-debian.Dockerfile index 5228d83d5..d4348ecdf 100644 --- a/ci/ci-debian.Dockerfile +++ b/ci/ci-debian.Dockerfile @@ -8,8 +8,8 @@ ENV TERM=xterm DEBIAN_FRONTEND=noninteractive ENV CI_RUNNING=true # create RPi configs to test installation -RUN touch /boot/config.txt -RUN echo "logo.nologo" > /boot/cmdline.txt +RUN mkdir -p /boot && touch /boot/config.txt && echo "logo.nologo" > /boot/cmdline.txt +RUN mkdir -p /boot/firmware && touch /boot/firmware/config.txt && echo "logo.nologo" > /boot/firmware/cmdline.txt RUN echo "--- install packages (1) ---" \ && apt-get update \ diff --git a/ci/installation/run_install_common.sh b/ci/installation/run_install_common.sh index b8c641580..0b73ae791 100644 --- a/ci/installation/run_install_common.sh +++ b/ci/installation/run_install_common.sh @@ -17,7 +17,7 @@ export ENABLE_WEBAPP_PROD_DOWNLOAD=true # n - use static ip # n - deactivate ipv6 # y - setup autohotspot -# n - use custom password +# n - change default configuration # n - deactivate bluetooth # n - disable on-chip audio # - - mpd overwrite config (only with existing installation) diff --git a/ci/installation/run_install_faststartup.sh b/ci/installation/run_install_faststartup.sh index 134aeca71..2a98a1869 100644 --- a/ci/installation/run_install_faststartup.sh +++ b/ci/installation/run_install_faststartup.sh @@ -11,11 +11,13 @@ 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 +# - - change default configuration (only with autohotspot = y) # y - deactivate bluetooth # y - disable on-chip audio # - - mpd overwrite config (only with existing installation) diff --git a/ci/installation/run_install_libzmq_local.sh b/ci/installation/run_install_libzmq_local.sh index 335cb24a1..7ce6e14ac 100644 --- a/ci/installation/run_install_libzmq_local.sh +++ b/ci/installation/run_install_libzmq_local.sh @@ -17,6 +17,7 @@ export BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE=true # n - use static ip # n - deactivate ipv6 # n - setup autohotspot +# - - change default configuration (only with autohotspot = y) # n - deactivate bluetooth # n - disable on-chip audio # - - mpd overwrite config (only with existing installation) diff --git a/ci/installation/run_install_webapp_download.sh b/ci/installation/run_install_webapp_download.sh index ded27ec54..ee72588ef 100644 --- a/ci/installation/run_install_webapp_download.sh +++ b/ci/installation/run_install_webapp_download.sh @@ -11,11 +11,13 @@ 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 # n - use static ip # n - deactivate ipv6 # n - setup autohotspot +# - - change default configuration (only with autohotspot = y) # n - deactivate bluetooth # n - disable on-chip audio # - - mpd overwrite config (only with existing installation) diff --git a/ci/installation/run_install_webapp_local.sh b/ci/installation/run_install_webapp_local.sh index 7af16df3a..bd7ce8def 100644 --- a/ci/installation/run_install_webapp_local.sh +++ b/ci/installation/run_install_webapp_local.sh @@ -17,6 +17,7 @@ export ENABLE_WEBAPP_PROD_DOWNLOAD=false # n - use static ip # n - deactivate ipv6 # n - setup autohotspot +# - - change default configuration (only with autohotspot = y) # n - deactivate bluetooth # n - disable on-chip audio # - - mpd overwrite config (only with existing installation) diff --git a/documentation/builders/autohotspot.md b/documentation/builders/autohotspot.md index ecf996f81..5a62a37ca 100644 --- a/documentation/builders/autohotspot.md +++ b/documentation/builders/autohotspot.md @@ -1,107 +1,63 @@ # Auto-Hotspot -The Auto-Hotspot function allows the Jukebox to switch between its -connection between a known WiFi and an automatically generated hotspot -so that you can still access via SSH or Web App. +The Auto-Hotspot function enables the Jukebox to switch its connection between a known WiFi network and an automatically generated hotspot, allowing access via SSH or Web App. > [!IMPORTANT] -> Please configure the WiFi connection to your home access point before enabling these feature! - -To create a hotspot and allow clients to connect -[hostapd](http://w1.fi/hostapd/) and [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html). +> Please configure the WiFi connection to your home access point before enabling this feature! ## How to connect -When the Jukebox is not able to connect to a known WiFi it will create a -hotspot named `Phoniebox_Hotspot`. You will be able to connect to this -hotspot using the given password in the installation or the default -password: `PlayItLoud!` - -### Web App - -After connecting to the `Phoniebox_Hotspot` you are able to connect to -the Web App accessing the website [10.0.0.5](http://10.0.0.5/). +When the Jukebox cannot connect to a known WiFi, it will automatically create a hotspot. +You can connect to this hotspot using the password set during installation. +Afterwards, you can access the Web App or connect via SSH as before, using the IP from the configuration. -### ssh - -After connecting to the `Phoniebox_Hotspot` you are able to connect via -ssh to your Jukebox - -``` bash -ssh @10.0.0.5 -``` - -## Changing basic configuration of the hotspot - -The whole hotspot configuration can be found at -`/etc/hostapd/hostapd.conf`. - -The following parameters are relevant: - -- `ssid` for the displayed hotspot name -- `wpa_passphrase` for the password of the hotspot -- `country_code` the country you are currently in - -``` bash -$ cat /etc/hostapd/hostapd.conf - -#2.4GHz setup wifi 80211 b,g,n -interface=wlan0 -driver=nl80211 -ssid=Phoniebox_Hotspot -hw_mode=g -channel=8 -wmm_enabled=0 -macaddr_acl=0 -auth_algs=1 -ignore_broadcast_ssid=0 -wpa=2 -wpa_passphrase==PlayItLoud! -wpa_key_mgmt=WPA-PSK -wpa_pairwise=CCMP TKIP -rsn_pairwise=CCMP - -#80211n - Change GB to your WiFi country code -country_code=DE -ieee80211n=1 -ieee80211d=1 +The default configuration is +``` text +* SSID : Phoniebox_Hotspot_ +* Password : PlayItLoud! +* WiFi Country Code : DE +* IP : 10.0.0.1 ``` ## Disabling automatism -Auto-Hotspot can be enabled or disabled using the Web App. +Auto-Hotspot can be enabled or disabled using the Web App or RPC Commands. + +> [!NOTE] +> Disabling the Auto-Hotspot will run the WiFi check again and maintain the last connection state until reboot. > [!IMPORTANT] -> Disabling or enabling will keep the last state. +> If you disable this feature, you will lose access to the Jukebox if you are not near a known WiFi after reboot! ## Troubleshooting -### Phoniebox is not connecting to the known WiFi - -The script will fall back to the hotspot so you still have some type of -connection. - -Check your password in `/etc/wpa_supplicant/wpa_supplicant.conf`. - ### AutoHotspot functionality is not working -You can check the output of the script by running the following script: +Check the `autohotspot.service` status +``` bash +sudo systemctl status autohotspot.service +``` +and logs ``` bash -$ sudo /usr/bin/autohotspot +sudo journalctl -u autohotspot.service -n 50 ``` -### You need to add a new wifi network to the Raspberry Pi +### Jukebox is not connecting to the known WiFi + +The script will fall back to the hotspot, ensuring you still have some type of connection. + +Check your WiFi configuration. -Because it is in Auto-Hotspot mode, you won\'t be able to scan for new -wifi signals. +### You need to add a new WiFi network to the Raspberry Pi -You will need to add a new network to -`/etc/wpa_supplicant/wpa_supplicant.conf` manually. Enter the following -details replacing mySSID and myPassword with your details. If your WiFi -has a hidden SSID then include the line `scan_ssid=1`. +#### Using the command line +Connect to the hotspot and open a terminal. Use the [raspi-config](https://www.raspberrypi.com/documentation/computers/configuration.html#wireless-lan) tool to add the new WiFi. ## Resources -[Raspberry Pi - Auto WiFi Hotspot Switch - Direct -Connection](https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection) +* [Raspberry Connect - Auto WiFi Hotspot Switch](https://www.raspberryconnect.com/projects/65-raspberrypi-hotspot-accesspoints/158-raspberry-pi-auto-wifi-hotspot-switch-direct-connection) +* [Raspberry Pi - Configuring networking](https://www.raspberrypi.com/documentation/computers/configuration.html#using-the-command-line) +* [dhcpcd / wpa_supplicant]() + * [hostapd](http://w1.fi/hostapd/) + * [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) diff --git a/documentation/builders/installation.md b/documentation/builders/installation.md index 1d4b18470..7cd321b7a 100644 --- a/documentation/builders/installation.md +++ b/documentation/builders/installation.md @@ -11,7 +11,6 @@ Before you can install the Phoniebox software, you need to prepare your Raspberr 2. Download the [Raspberry Pi Imager](https://www.raspberrypi.com/software/) and run it 3. Click on "Raspberry Pi Device" and select "No filtering" 4. As operating system select **Raspberry Pi OS (other)** and then **Raspberry Pi OS Lite (Legacy, 32-bit)** (no desktop environment). *64-bit is currently not supported*. - * Bookworm support is partly broken, see [here](#workaround-for-network-related-features-on-bookworm). * For Pi 4 and newer also check [this](#workaround-for-64-bit-kernels-pi-4-and-newer). 5. Select your Micro SD card (your card will be formatted) 6. After you click `Next`, a prompt will ask you if you like to customize the OS settings @@ -81,21 +80,21 @@ You will need a terminal, like PuTTY for Windows or the Terminal app for Mac to ### Pre-install preparation / workarounds -#### Workaround for network related features on Bookworm +#### Network management since Bookworm
-With Bookworm the network settings have changed. Now "NetworkManager" is used instead of "dhcpcd". -This breaks breaks network related features like "Static IP", "Wifi Setup" and "Autohotspot". -Before running the installation, the network config has to be changed via raspi-config, to use the "old" dhcpcd network settings. +With Bookworm, network management has changed. Now, "NetworkManager" is used instead of "dhcpcd". +Both methods are supported during installation, but "NetworkManager" is recommended as it is simpler to set up and use. +For Bullseye, this can also be activated, though it requires a manual process before running the installation. :warning: -If the settings are changed, your network will reset and Wifi will not be configured, so you lose ssh access via wireless connection. -So make sure you use a wired connection or perform the following steps in a local terminal with a connected monitor and keyboard. +If the settings are changed, your network will reset, and WiFi will not be configured, causing you to lose SSH access via wireless connection. +Therefore, make sure you use a wired connection or perform the following steps in a local terminal with a connected monitor and keyboard. Change network config * run `sudo raspi-config` * select `6 - Advanced Options` * select `AA - Network Config` -* select `dhcpcd` +* select `NetworkManager` If you need Wifi, add the information now * select `1 - System Options` diff --git a/installation/includes/00_constants.sh b/installation/includes/00_constants.sh index 89299989c..574febed3 100644 --- a/installation/includes/00_constants.sh +++ b/installation/includes/00_constants.sh @@ -1,7 +1,6 @@ -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_PATH="/etc/systemd/system" 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 diff --git a/installation/includes/01_default_config.sh b/installation/includes/01_default_config.sh index ec7b67b66..a3115fee6 100644 --- a/installation/includes/01_default_config.sh +++ b/installation/includes/01_default_config.sh @@ -4,8 +4,11 @@ BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE=${BUILD_LIBZMQ_WITH_DRAFTS_ON_DEVICE:-"false" ENABLE_STATIC_IP=true DISABLE_IPv6=true ENABLE_AUTOHOTSPOT=false -AUTOHOTSPOT_CHANGE_PASSWORD=false +AUTOHOTSPOT_PROFILE="Phoniebox_Hotspot" +AUTOHOTSPOT_SSID="$AUTOHOTSPOT_PROFILE" AUTOHOTSPOT_PASSWORD="PlayItLoud!" +AUTOHOTSPOT_IP="10.0.0.1" +AUTOHOTSPOT_COUNTRYCODE="DE" DISABLE_BLUETOOTH=true DISABLE_SSH_QOS=true DISABLE_BOOT_SCREEN=true @@ -18,7 +21,6 @@ ENABLE_SAMBA=true ENABLE_WEBAPP=true ENABLE_KIOSK_MODE=false DISABLE_ONBOARD_AUDIO=false -DISABLE_ONBOARD_AUDIO_BACKUP="${RPI_BOOT_CONFIG_FILE}.backup.audio_on_$(date +%d.%m.%y_%H.%M.%S)" # Always try to use GIT with SSH first, and on failure drop down to HTTPS GIT_USE_SSH=${GIT_USE_SSH:-"true"} diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index 9cd51c824..be496b1b9 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -88,16 +88,17 @@ get_debian_version_number() { echo "$VERSION_ID" } -get_boot_config_path() { +_get_boot_file_path() { + local filename="$1" if [ "$(is_raspbian)" = true ]; then local debian_version_number=$(get_debian_version_number) # Bullseye and lower if [ "$debian_version_number" -le 11 ]; then - echo "/boot/config.txt" + echo "/boot/${filename}" # Bookworm and higher elif [ "$debian_version_number" -ge 12 ]; then - echo "/boot/firmware/config.txt" + echo "/boot/firmware/${filename}" else echo "unknown" fi @@ -106,6 +107,14 @@ get_boot_config_path() { fi } +get_boot_config_path() { + echo $(_get_boot_file_path "config.txt") +} + +get_boot_cmdline_path() { + echo $(_get_boot_file_path "cmdline.txt") +} + validate_url() { local url=$1 wget --spider ${url} >/dev/null 2>&1 @@ -119,6 +128,70 @@ download_from_url() { return $? } +get_string_length() { + local string="$1" + # "-n" option is needed otherwise an additional linebreak char is added by echo + echo -n ${string} | wc -m +} + +_get_service_enablement() { + local service="$1" + local option="${2:+$2 }" # optional, dont't quote in 'systemctl' call! + + if [[ -z "${service}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + local actual_enablement=$(systemctl is-enabled ${option}${service} 2>/dev/null) + + echo "$actual_enablement" +} + +is_service_enabled() { + local service="$1" + local option="$2" + local actual_enablement=$(_get_service_enablement $service $option) + + if [[ "$actual_enablement" == "enabled" ]]; then + echo true + else + echo false + fi +} + +is_dhcpcd_enabled() { + echo $(is_service_enabled "dhcpcd.service") +} + +is_NetworkManager_enabled() { + echo $(is_service_enabled "NetworkManager.service") +} + +# create flag file if files does no exist (*.remove) or copy present conf to backup file (*.orig) +# to correctly handling de-/activation of corresponding feature +config_file_backup() { + local config_file="$1" + local config_flag_file="${config_file}.remove" + local config_orig_file="${config_file}.orig" + if [ ! -f "${config_file}" ]; then + sudo touch "${config_flag_file}" + elif [ ! -f "${config_orig_file}" ] && [ ! -f "${config_flag_file}" ]; then + sudo cp "${config_file}" "${config_orig_file}" + fi +} + +# revert config files backed up with `config_file_backup` +config_file_revert() { + local config_file="$1" + local config_flag_file="${config_file}.remove" + local config_orig_file="${config_file}.orig" + if [ -f "${config_flag_file}" ]; then + sudo rm "${config_flag_file}" "${config_file}" + elif [ -f "${config_orig_file}" ]; then + sudo mv "${config_orig_file}" "${config_file}" + fi +} + ### Verify helpers print_verify_installation() { log "\n @@ -220,7 +293,7 @@ verify_file_contains_string() { exit_on_error "ERROR: at least one parameter value is missing!" fi - if [[ ! $(grep -iw "${string}" "${file}") ]]; then + if [[ ! $(sudo grep -iw "${string}" "${file}") ]]; then exit_on_error "ERROR: '${string}' not found in '${file}'" fi log " CHECK" @@ -235,7 +308,7 @@ verify_file_contains_string_once() { exit_on_error "ERROR: at least one parameter value is missing!" fi - local file_contains_string_count=$(grep -oiw "${string}" "${file}" | wc -l) + local file_contains_string_count=$(sudo 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 @@ -247,7 +320,7 @@ verify_file_contains_string_once() { verify_service_state() { local service="$1" local desired_state="$2" - local option="${3:+$3 }" # optional, dont't quote in next call! + 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 @@ -264,14 +337,14 @@ verify_service_state() { verify_service_enablement() { local service="$1" local desired_enablement="$2" - local option="${3:+$3 }" # optional, dont't quote in next call! + local option="$3" 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}) + local actual_enablement=$(_get_service_enablement $service $option) if [[ ! "${actual_enablement}" == "${desired_enablement}" ]]; then exit_on_error "ERROR: service ${option}${service} is not ${desired_enablement} (state: ${actual_enablement})." fi @@ -281,14 +354,14 @@ verify_service_enablement() { verify_optional_service_enablement() { local service="$1" local desired_enablement="$2" - local option="${3:+$3 }" # optional, dont't quote in next call! + local option="$3" 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 + local actual_enablement=$(_get_service_enablement $service $option) if [[ -z "${actual_enablement}" ]]; then log " INFO: optional service ${option}${service} is not installed." elif [[ "${actual_enablement}" == "static" ]]; then diff --git a/installation/routines/customize_options.sh b/installation/routines/customize_options.sh index c903df189..de1dd5a19 100644 --- a/installation/routines/customize_options.sh +++ b/installation/routines/customize_options.sh @@ -49,51 +49,98 @@ Do you want to disable IPv6? [Y/n]" } _option_autohotspot() { - # ENABLE_AUTOHOTSPOT - clear_c - print_c "---------------------- AUTOHOTSPOT ---------------------- + # ENABLE_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]" - read -r response - case "$response" in - [yY][eE][sS]|[yY]) - ENABLE_AUTOHOTSPOT=true - ;; - *) - ;; - esac +Note: +Static IP configuration cannot be enabled with +WiFi hotspot and will be disabled, if selected before. - if [ "$ENABLE_AUTOHOTSPOT" = true ]; then - 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 +Do you want to enable an Autohotspot? [y/N]" + read -r response + case "$response" in [yY][eE][sS]|[yY]) - while [ $(echo ${response_pw}|wc -m) -lt 8 ] - do - print_c "Please type the new password (at least 8 character)." - read -r response_pw - done - AUTOHOTSPOT_PASSWORD="${response_pw}" - ;; + ENABLE_AUTOHOTSPOT=true + ;; *) - ;; - esac + ;; + esac + + if [ "$ENABLE_AUTOHOTSPOT" = true ]; then + #add hostname to default SSID to prevent collision + local local_hostname=$(hostname) + AUTOHOTSPOT_SSID="${AUTOHOTSPOT_SSID}_${local_hostname}" + AUTOHOTSPOT_SSID="${AUTOHOTSPOT_SSID:0:32}" + + local response_autohotspot + while [[ $response_autohotspot != "n" ]] + do + print_c " +--- Current configuration for Autohotpot +SSID : $AUTOHOTSPOT_SSID +Password : $AUTOHOTSPOT_PASSWORD +WiFi Country Code : $AUTOHOTSPOT_COUNTRYCODE +IP : $AUTOHOTSPOT_IP +Do you want to change this values? [y/N]" + read -r response_autohotspot + case "$response_autohotspot" in + [yY][eE][sS]|[yY]) + local response_ssid="" + local response_ssid_length=0 + while [[ $response_ssid_length -lt 1 || $response_ssid_length -gt 32 ]] + do + print_c "Please type the hotspot ssid (must be between 1 and 32 characters long):" + read -r response_ssid + response_ssid_length=$(get_string_length ${response_ssid}) + done + + local response_pw="" + local response_pw_length=0 + while [[ $response_pw_length -lt 8 || $response_pw_length -gt 63 ]] + do + print_c "Please type the new password (must be between 8 and 63 characters long):" + read -r response_pw + response_pw_length=$(get_string_length ${response_pw}) + done + + local response_country_code="" + local response_country_code_length=0 + while [[ $response_country_code_length -ne 2 ]] + do + print_c "Please type the WiFi country code (e.g. DE, GB, CZ or US):" + read -r response_country_code + response_country_code="${response_country_code^^}" # to Uppercase + response_country_code_length=$(get_string_length ${response_country_code}) + done + + AUTOHOTSPOT_SSID="${response_ssid}" + AUTOHOTSPOT_PASSWORD="${response_pw}" + AUTOHOTSPOT_COUNTRYCODE="${response_country_code}" + ;; + *) + response_autohotspot=n + ;; + esac + done - if [ "$ENABLE_STATIC_IP" = true ]; then - print_c "Wifi hotspot cannot be enabled with static IP. Disabling static IP configuration." - ENABLE_STATIC_IP=false - log "ENABLE_STATIC_IP=${ENABLE_STATIC_IP}" - fi - fi + if [ "$ENABLE_STATIC_IP" = true ]; then + ENABLE_STATIC_IP=false + echo "ENABLE_STATIC_IP=${ENABLE_STATIC_IP}" + fi + fi - log "ENABLE_AUTOHOTSPOT=${ENABLE_AUTOHOTSPOT}" - if [ "$ENABLE_AUTOHOTSPOT" = true ]; then - log "AUTOHOTSPOT_PASSWORD=${AUTOHOTSPOT_PASSWORD}" - fi + echo "ENABLE_AUTOHOTSPOT=${ENABLE_AUTOHOTSPOT}" + if [ "$ENABLE_AUTOHOTSPOT" = true ]; then + echo "AUTOHOTSPOT_SSID=${AUTOHOTSPOT_SSID}" + echo "AUTOHOTSPOT_PASSWORD=${AUTOHOTSPOT_PASSWORD}" + echo "AUTOHOTSPOT_COUNTRYCODE=${AUTOHOTSPOT_COUNTRYCODE}" + echo "AUTOHOTSPOT_IP=${AUTOHOTSPOT_IP}" + fi } _option_bluetooth() { @@ -275,11 +322,10 @@ the on-chip audio. It will make the ALSA sound configuration easier. If you are planning to only use Bluetooth speakers, leave the on-chip audio enabled! -(This will touch your boot configuration in -${RPI_BOOT_CONFIG_FILE}. +(This will touch your boot configuration file. We will do our best not to mess anything up. However, -a backup copy will be written to -${DISABLE_ONBOARD_AUDIO_BACKUP} ) +a backup copy will be written. Please check the install +log after for further details.) Disable Pi's on-chip audio (headphone / jack output)? [y/N]" read -r response diff --git a/installation/routines/optimize_boot_time.sh b/installation/routines/optimize_boot_time.sh index bb6f71902..e3761c31d 100644 --- a/installation/routines/optimize_boot_time.sh +++ b/installation/routines/optimize_boot_time.sh @@ -4,8 +4,8 @@ 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_BOOT_CMDLINE_OPTIONS_IPV6="ipv6.disable=1" 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() { @@ -26,6 +26,24 @@ _optimize_disable_irrelevant_services() { sudo systemctl disable apt-daily-upgrade.timer } +_add_options_to_cmdline() { + local options="$1" + + local cmdlineFile=$(get_boot_cmdline_path) + if [ ! -s "${cmdlineFile}" ];then + sudo tee "${cmdlineFile}" <<-EOF +${options} +EOF + else + for option in $options + do + if ! grep -qiw "$option" "${cmdlineFile}" ; then + sudo sed -i "s/$/ $option/" "${cmdlineFile}" + fi + done + fi +} + # TODO: If false, actually make sure bluetooth is enabled _optimize_handle_bluetooth() { if [ "$DISABLE_BLUETOOTH" = true ] ; then @@ -37,58 +55,52 @@ _optimize_handle_bluetooth() { # TODO: Allow options to enable/disable wifi, Dynamic/Static IP etc. _optimize_static_ip() { - # Static IP Address and DHCP optimizations - if [ "$ENABLE_STATIC_IP" = true ] ; then - 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 - 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 $OPTIMIZE_DHCP_CONF <<-EOF + # Static IP Address and DHCP optimizations + if [[ $(is_dhcpcd_enabled) == true ]]; then + if [ "$ENABLE_STATIC_IP" = true ] ; then + 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 + 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 $OPTIMIZE_DHCP_CONF <<-EOF ${OPTIMIZE_DHCP_CONF_HEADER} interface ${CURRENT_INTERFACE} static ip_address=${CURRENT_IP_ADDRESS}/24 static routers=${CURRENT_GATEWAY} static domain_name_servers=${CURRENT_GATEWAY} +noarp EOF + fi + fi fi - fi } # TODO: Allow both Enable and Disable +# Disable ipv6 thoroughly on the system with kernel parameter _optimize_ipv6_arp() { - if [ "$DISABLE_IPv6" = true ] ; then - 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 - -${OPTIMIZE_IPV6_CONF_HEADER} -noarp -ipv4only -noipv6 - -EOF + if [ "$DISABLE_IPv6" = true ] ; then + print_lc " Disabling IPV6" + _add_options_to_cmdline "${OPTIMIZE_BOOT_CMDLINE_OPTIONS_IPV6}" fi - fi } # TODO: Allow both Enable and Disable _optimize_handle_boot_screen() { + local configFile=$(get_boot_config_path) if [ "$DISABLE_BOOT_SCREEN" = true ] ; then log " Disable RPi rainbow screen" - if grep -q "${OPTIMIZE_BOOT_CONF_HEADER}" "$RPI_BOOT_CONFIG_FILE"; then + if grep -q "${OPTIMIZE_BOOT_CONF_HEADER}" "$configFile"; then log " Skipping. Already set up!" else - sudo tee -a $RPI_BOOT_CONFIG_FILE <<-EOF + sudo tee -a $configFile <<-EOF ${OPTIMIZE_BOOT_CONF_HEADER} disable_splash=1 @@ -103,25 +115,40 @@ _optimize_handle_boot_logs() { if [ "$DISABLE_BOOT_LOGS_PRINT" = true ] ; then 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 + _add_options_to_cmdline "${OPTIMIZE_BOOT_CMDLINE_OPTIONS}" fi } +get_nm_active_profile() +{ + local active_profile=$(nmcli -g DEVICE,CONNECTION device status | grep "^${CURRENT_INTERFACE}" | cut -d':' -f2) + echo "$active_profile" +} + +_optimize_static_ip_NetworkManager() { + if [[ $(is_NetworkManager_enabled) == true ]]; then + if [ "$ENABLE_STATIC_IP" = true ] ; then + print_lc " Set static IP address" + 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" + local active_profile=$(get_nm_active_profile) + sudo nmcli connection modify "$active_profile" ipv4.method manual ipv4.address "${CURRENT_IP_ADDRESS}/24" ipv4.gateway "$CURRENT_GATEWAY" ipv4.dns "$CURRENT_GATEWAY" + #else + # for future deactivation + #sudo nmcli connection modify "$active_profile" ipv4.method auto ipv4.address "" ipv4.gateway "" ipv4.dns "" + fi + fi +} + _optimize_check() { print_verify_installation + local cmdlineFile=$(get_boot_cmdline_path) + local configFile=$(get_boot_config_path) + + verify_optional_service_enablement keyboard-setup.service disabled verify_optional_service_enablement triggerhappy.service disabled verify_optional_service_enablement triggerhappy.socket disabled @@ -137,33 +164,44 @@ _optimize_check() { 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}" + if [[ $(is_dhcpcd_enabled) == 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 [[ $(is_NetworkManager_enabled) == true ]]; then + local active_profile=$(get_nm_active_profile) + local active_profile_path="/etc/NetworkManager/system-connections/${active_profile}.nmconnection" + verify_files_exists "${active_profile_path}" + verify_file_contains_string "${CURRENT_IP_ADDRESS}" "${active_profile_path}" + verify_file_contains_string "${CURRENT_GATEWAY}" "${active_profile_path}" + fi fi if [ "$DISABLE_IPv6" = true ] ; then - verify_file_contains_string_once "${OPTIMIZE_IPV6_CONF_HEADER}" "${OPTIMIZE_DHCP_CONF}" + verify_file_contains_string_once "${OPTIMIZE_BOOT_CMDLINE_OPTIONS_IPV6}" "${cmdlineFile}" fi if [ "$DISABLE_BOOT_SCREEN" = true ] ; then - verify_file_contains_string_once "${OPTIMIZE_BOOT_CONF_HEADER}" "${RPI_BOOT_CONFIG_FILE}" + verify_file_contains_string_once "${OPTIMIZE_BOOT_CONF_HEADER}" "${configFile}" 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}" + verify_file_contains_string_once $option "${cmdlineFile}" done fi } _run_optimize_boot_time() { _optimize_disable_irrelevant_services + _optimize_handle_boot_screen + _optimize_handle_boot_logs _optimize_handle_bluetooth _optimize_static_ip + _optimize_static_ip_NetworkManager _optimize_ipv6_arp - _optimize_handle_boot_screen - _optimize_handle_boot_logs _optimize_check } diff --git a/installation/routines/set_raspi_config.sh b/installation/routines/set_raspi_config.sh index 7f39a0ba5..ef4707c91 100644 --- a/installation/routines/set_raspi_config.sh +++ b/installation/routines/set_raspi_config.sh @@ -17,13 +17,15 @@ _run_set_raspi_config() { # On-board audio if [ "$DISABLE_ONBOARD_AUDIO" == true ]; then + local configFile=$(get_boot_config_path) 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}" + if grep -q -E "^dtparam=([^,]*,)*audio=(on|true|yes|1).*" "${configFile}" ; then + local configFile_backup="${configFile}.backup.audio_on_$(date +%d.%m.%y_%H.%M.%S)" + log " Backup ${configFile} --> ${configFile_backup}" + sudo cp "${configFile}" "${configFile_backup}" + sudo sed -i "s/^\(dtparam=\([^,]*,\)*\)audio=\(on\|true\|yes\|1\)\(.*\)/\1audio=off\4/g" "${configFile}" else - log " On board audio seems to be off already. Not touching ${RPI_BOOT_CONFIG_FILE}" + log " On board audio seems to be off already. Not touching ${configFile}" fi fi } diff --git a/installation/routines/setup_autohotspot.sh b/installation/routines/setup_autohotspot.sh index a083b3fcf..47b72757a 100644 --- a/installation/routines/setup_autohotspot.sh +++ b/installation/routines/setup_autohotspot.sh @@ -1,119 +1,46 @@ #!/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_INTERFACES_CONF_FILE="/etc/network/interfaces" AUTOHOTSPOT_TARGET_PATH="/usr/bin/autohotspot" +AUTOHOTSPOT_SERVICE="autohotspot.service" +AUTOHOTSPOT_SERVICE_PATH="${SYSTEMD_PATH}/${AUTOHOTSPOT_SERVICE}" +AUTOHOTSPOT_TIMER="autohotspot.timer" +AUTOHOTSPOT_TIMER_PATH="${SYSTEMD_PATH}/${AUTOHOTSPOT_TIMER}" _get_interface() { # interfaces may vary WIFI_INTERFACE=$(iw dev | grep "Interface"| awk '{ print $2 }') - 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 iw - - # disable services. We want to start them manually - sudo systemctl unmask hostapd - sudo systemctl disable hostapd - sudo systemctl disable dnsmasq -} - -_configure_hostapd() { - 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}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" - - sudo sed -i "s@^#DAEMON_CONF=.*@DAEMON_CONF=\"${AUTOHOTSPOT_HOSTAPD_CONF_FILE}\"@g" "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" -} - -_configure_dnsmasq() { - sudo tee -a "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" <<-EOF -#AutoHotspot Config -#stop DNSmasq from using resolv.conf -no-resolv -#Interface to use -interface=${WIFI_INTERFACE} -bind-interfaces -dhcp-range=10.0.0.50,10.0.0.150,12h -EOF -} - -_other_configuration() { - sudo mv /etc/network/interfaces /etc/network/interfaces.bak - sudo touch /etc/network/interfaces - 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 - 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}" +_get_last_ip_segment() { + local ip="$1" + echo $ip | cut -d'.' -f1-3 } -_install_autohotspot_script() { - sudo cp "${INSTALLATION_PATH}"/resources/autohotspot/autohotspot "${AUTOHOTSPOT_TARGET_PATH}" - sudo chmod +x "${AUTOHOTSPOT_TARGET_PATH}" -} - - -_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 -} setup_autohotspot() { if [ "$ENABLE_AUTOHOTSPOT" == true ] ; then - run_with_log_frame _run_setup_autohotspot "Install AutoHotspot" + local installed=false + if [[ $(is_dhcpcd_enabled) == true || "${CI_RUNNING}" == "true" ]]; then + run_with_log_frame _run_setup_autohotspot_dhcpcd "Install AutoHotspot" + installed=true + fi + + if [[ $(is_NetworkManager_enabled) == true || "${CI_RUNNING}" == "true" ]]; then + run_with_log_frame _run_setup_autohotspot_NetworkManager "Install AutoHotspot" + installed=true + fi + + if [[ "$installed" != true ]]; then + exit_on_error "ERROR: No network service available" + fi fi } diff --git a/installation/routines/setup_autohotspot_NetworkManager.sh b/installation/routines/setup_autohotspot_NetworkManager.sh new file mode 100644 index 000000000..66e777f1b --- /dev/null +++ b/installation/routines/setup_autohotspot_NetworkManager.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +AUTOHOTSPOT_NETWORKMANAGER_RESOURCES_PATH="${INSTALLATION_PATH}/resources/autohotspot/NetworkManager" +AUTOHOTSPOT_NETWORKMANAGER_CONNECTIONS_PATH="/etc/NetworkManager/system-connections" + +_install_packages_NetworkManager() { + sudo apt-get -y install iw +} + +_install_autohotspot_NetworkManager() { + # configure interface conf + config_file_backup "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + sudo rm "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + sudo touch "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + + # create service to trigger hotspot + local ip_without_last_segment=$(_get_last_ip_segment $AUTOHOTSPOT_IP) + sudo cp "${AUTOHOTSPOT_NETWORKMANAGER_RESOURCES_PATH}"/autohotspot "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%WIFI_INTERFACE%%|${WIFI_INTERFACE}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_PROFILE%%|${AUTOHOTSPOT_PROFILE}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_SSID%%|${AUTOHOTSPOT_SSID}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_PASSWORD%%|${AUTOHOTSPOT_PASSWORD}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_IP%%|${AUTOHOTSPOT_IP}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%IP_WITHOUT_LAST_SEGMENT%%|${ip_without_last_segment}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_TIMER_NAME%%|${AUTOHOTSPOT_TIMER}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo chmod +x "${AUTOHOTSPOT_TARGET_PATH}" + + sudo cp "${AUTOHOTSPOT_NETWORKMANAGER_RESOURCES_PATH}"/autohotspot.service "${AUTOHOTSPOT_SERVICE_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_SCRIPT%%|${AUTOHOTSPOT_TARGET_PATH}|g" "${AUTOHOTSPOT_SERVICE_PATH}" + + sudo cp "${AUTOHOTSPOT_NETWORKMANAGER_RESOURCES_PATH}"/autohotspot.timer "${AUTOHOTSPOT_TIMER_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_SERVICE%%|${AUTOHOTSPOT_SERVICE}|g" "${AUTOHOTSPOT_TIMER_PATH}" + + + sudo systemctl unmask "${AUTOHOTSPOT_SERVICE}" + sudo systemctl unmask "${AUTOHOTSPOT_TIMER}" + sudo systemctl disable "${AUTOHOTSPOT_SERVICE}" + sudo systemctl enable "${AUTOHOTSPOT_TIMER}" +} + +_uninstall_autohotspot_NetworkManager() { + # clear autohotspot configurations made from past installation + + # stop services and clear services + if systemctl list-unit-files "${AUTOHOTSPOT_SERVICE}" >/dev/null 2>&1 ; then + sudo systemctl stop "${AUTOHOTSPOT_TIMER}" + sudo systemctl disable "${AUTOHOTSPOT_TIMER}" + sudo systemctl stop "${AUTOHOTSPOT_SERVICE}" + sudo systemctl disable "${AUTOHOTSPOT_SERVICE}" + sudo rm "${AUTOHOTSPOT_SERVICE_PATH}" + sudo rm "${AUTOHOTSPOT_TIMER_PATH}" + fi + + if [ -f "${AUTOHOTSPOT_TARGET_PATH}" ]; then + sudo rm "${AUTOHOTSPOT_TARGET_PATH}" + fi + + sudo rm -f "${AUTOHOTSPOT_NETWORKMANAGER_CONNECTIONS_PATH}/${AUTOHOTSPOT_PROFILE}*" + + # remove config files + config_file_revert "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" +} + +_autohotspot_check_NetworkManager() { + print_verify_installation + + verify_apt_packages iw + + verify_service_enablement "${AUTOHOTSPOT_SERVICE}" disabled + verify_service_enablement "${AUTOHOTSPOT_TIMER}" enabled + + verify_files_exists "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + + local ip_without_last_segment=$(_get_last_ip_segment $AUTOHOTSPOT_IP) + verify_files_exists "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "wdev0='${WIFI_INTERFACE}'" "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "ap_profile_name='${AUTOHOTSPOT_PROFILE}'" "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "ap_ssid='${AUTOHOTSPOT_SSID}'" "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "ap_pw='${AUTOHOTSPOT_PASSWORD}'" "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "ap_ip='${AUTOHOTSPOT_IP}" "${AUTOHOTSPOT_TARGET_PATH}" #intentional "open end" + verify_file_contains_string "ap_gate='${ip_without_last_segment}" "${AUTOHOTSPOT_TARGET_PATH}" #intentional "open end" + verify_file_contains_string "timer_service_name='${AUTOHOTSPOT_TIMER}'" "${AUTOHOTSPOT_TARGET_PATH}" + + verify_files_exists "${AUTOHOTSPOT_SERVICE_PATH}" + verify_file_contains_string "ExecStart=${AUTOHOTSPOT_TARGET_PATH}" "${AUTOHOTSPOT_SERVICE_PATH}" + + verify_files_exists "${AUTOHOTSPOT_TIMER_PATH}" + verify_file_contains_string "Unit=${AUTOHOTSPOT_SERVICE}" "${AUTOHOTSPOT_TIMER_PATH}" +} + +_run_setup_autohotspot_NetworkManager() { + log "Install AutoHotspot NetworkManager" + _install_packages_NetworkManager + _get_interface + _uninstall_autohotspot_NetworkManager + _install_autohotspot_NetworkManager + _autohotspot_check_NetworkManager +} diff --git a/installation/routines/setup_autohotspot_dhcpcd.sh b/installation/routines/setup_autohotspot_dhcpcd.sh new file mode 100644 index 000000000..9ca92016c --- /dev/null +++ b/installation/routines/setup_autohotspot_dhcpcd.sh @@ -0,0 +1,185 @@ +#!/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_DHCPCD_CONF_FILE="/etc/dhcpcd.conf" +AUTOHOTSPOT_DHCPCD_CONF_NOHOOK_WPA_SUPPLICANT="nohook wpa_supplicant" + +AUTOHOTSPOT_SERVICE_DAEMON="autohotspot-daemon.service" +AUTOHOTSPOT_SERVICE_DAEMON_PATH="${SYSTEMD_PATH}/${AUTOHOTSPOT_SERVICE_DAEMON}" + +AUTOHOTSPOT_DHCPCD_RESOURCES_PATH="${INSTALLATION_PATH}/resources/autohotspot/dhcpcd" + +_install_packages_dhcpcd() { + sudo apt-get -y install hostapd dnsmasq iw + + # disable services. We want to start them manually + sudo systemctl unmask hostapd + sudo systemctl disable hostapd + sudo systemctl stop hostapd + sudo systemctl unmask dnsmasq + sudo systemctl disable dnsmasq + sudo systemctl stop dnsmasq +} + +_install_autohotspot_dhcpcd() { + # configure interface conf + config_file_backup "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + sudo rm "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + sudo touch "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + + + # configure DNS + config_file_backup "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + + local ip_without_last_segment=$(_get_last_ip_segment $AUTOHOTSPOT_IP) + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/dnsmasq.conf "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + sudo sed -i "s|%%WIFI_INTERFACE%%|${WIFI_INTERFACE}|g" "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + sudo sed -i "s|%%IP_WITHOUT_LAST_SEGMENT%%|${ip_without_last_segment}|g" "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + + + # configure hostapd conf + config_file_backup "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/hostapd.conf "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + sudo sed -i "s|%%WIFI_INTERFACE%%|${WIFI_INTERFACE}|g" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + sudo sed -i "s|%%AUTOHOTSPOT_SSID%%|${AUTOHOTSPOT_SSID}|g" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + sudo sed -i "s|%%AUTOHOTSPOT_PASSWORD%%|${AUTOHOTSPOT_PASSWORD}|g" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + sudo sed -i "s|%%AUTOHOTSPOT_COUNTRYCODE%%|${AUTOHOTSPOT_COUNTRYCODE}|g" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + + + # configure hostapd daemon + config_file_backup "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/hostapd "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + sudo sed -i "s|%%HOSTAPD_CONF%%|${AUTOHOTSPOT_HOSTAPD_CONF_FILE}|g" "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + + + # configure dhcpcd conf + config_file_backup "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + if [ ! -f "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" ]; then + sudo touch "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + sudo chown root:netdev "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + sudo chmod 664 "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + fi + + if [[ ! $(grep -w "${AUTOHOTSPOT_DHCPCD_CONF_NOHOOK_WPA_SUPPLICANT}" ${AUTOHOTSPOT_DHCPCD_CONF_FILE}) ]]; then + sudo bash -c "echo ${AUTOHOTSPOT_DHCPCD_CONF_NOHOOK_WPA_SUPPLICANT} >> ${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + fi + + # create service to trigger hotspot + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/autohotspot "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%WIFI_INTERFACE%%|${WIFI_INTERFACE}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_IP%%|${AUTOHOTSPOT_IP}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_SERVICE_DAEMON%%|${AUTOHOTSPOT_SERVICE_DAEMON}|g" "${AUTOHOTSPOT_TARGET_PATH}" + sudo chmod +x "${AUTOHOTSPOT_TARGET_PATH}" + + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/autohotspot-daemon.service "${AUTOHOTSPOT_SERVICE_DAEMON_PATH}" + sudo sed -i "s|%%WIFI_INTERFACE%%|${WIFI_INTERFACE}|g" "${AUTOHOTSPOT_SERVICE_DAEMON_PATH}" + + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/autohotspot.service "${AUTOHOTSPOT_SERVICE_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_SCRIPT%%|${AUTOHOTSPOT_TARGET_PATH}|g" "${AUTOHOTSPOT_SERVICE_PATH}" + + sudo cp "${AUTOHOTSPOT_DHCPCD_RESOURCES_PATH}"/autohotspot.timer "${AUTOHOTSPOT_TIMER_PATH}" + sudo sed -i "s|%%AUTOHOTSPOT_SERVICE%%|${AUTOHOTSPOT_SERVICE}|g" "${AUTOHOTSPOT_TIMER_PATH}" + + sudo systemctl enable "${AUTOHOTSPOT_SERVICE_DAEMON}" + sudo systemctl disable "${AUTOHOTSPOT_SERVICE}" + sudo systemctl enable "${AUTOHOTSPOT_TIMER}" +} + + +_uninstall_autohotspot_dhcpcd() { + # clear autohotspot configurations made from past installation + + # remove old crontab entries from previous versions + local cron_autohotspot_file="/etc/cron.d/autohotspot" + if [ -f "${cron_autohotspot_file}" ]; then + sudo rm -f "${cron_autohotspot_file}" + fi + + # stop and clear services + if systemctl list-unit-files "${AUTOHOTSPOT_SERVICE}" >/dev/null 2>&1 ; then + sudo systemctl stop hostapd + sudo systemctl stop dnsmasq + sudo systemctl stop "${AUTOHOTSPOT_TIMER}" + sudo systemctl disable "${AUTOHOTSPOT_TIMER}" + sudo systemctl stop "${AUTOHOTSPOT_SERVICE}" + sudo systemctl disable "${AUTOHOTSPOT_SERVICE}" + sudo systemctl disable "${AUTOHOTSPOT_SERVICE_DAEMON}" + sudo rm "${AUTOHOTSPOT_SERVICE_PATH}" + sudo rm "${AUTOHOTSPOT_TIMER_PATH}" + sudo rm "${AUTOHOTSPOT_SERVICE_DAEMON_PATH}" + fi + + if [ -f "${AUTOHOTSPOT_TARGET_PATH}" ]; then + sudo rm "${AUTOHOTSPOT_TARGET_PATH}" + fi + + # remove config files + config_file_revert "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + config_file_revert "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + config_file_revert "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + config_file_revert "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + config_file_revert "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" +} + + +_autohotspot_check_dhcpcd() { + 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_DAEMON}" enabled + verify_service_enablement "${AUTOHOTSPOT_SERVICE}" disabled + verify_service_enablement "${AUTOHOTSPOT_TIMER}" enabled + + verify_files_exists "${AUTOHOTSPOT_INTERFACES_CONF_FILE}" + + local ip_without_last_segment=$(_get_last_ip_segment $AUTOHOTSPOT_IP) + verify_files_exists "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + verify_file_contains_string "${WIFI_INTERFACE}" "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + verify_file_contains_string "dhcp-range=${ip_without_last_segment}" "${AUTOHOTSPOT_DNSMASQ_CONF_FILE}" + + verify_files_exists "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "interface=${WIFI_INTERFACE}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "ssid=${AUTOHOTSPOT_SSID}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "wpa_passphrase=${AUTOHOTSPOT_PASSWORD}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + verify_file_contains_string "country_code=${AUTOHOTSPOT_COUNTRYCODE}" "${AUTOHOTSPOT_HOSTAPD_CONF_FILE}" + + verify_files_exists "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + verify_file_contains_string "DAEMON_CONF=\"${AUTOHOTSPOT_HOSTAPD_CONF_FILE}\"" "${AUTOHOTSPOT_HOSTAPD_DAEMON_CONF_FILE}" + + verify_files_exists "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + verify_file_contains_string "${AUTOHOTSPOT_DHCPCD_CONF_NOHOOK_WPA_SUPPLICANT}" "${AUTOHOTSPOT_DHCPCD_CONF_FILE}" + + verify_files_exists "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "wifidev=\"${WIFI_INTERFACE}\"" "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "hotspot_ip=${AUTOHOTSPOT_IP}" "${AUTOHOTSPOT_TARGET_PATH}" + verify_file_contains_string "daemon_service=\"${AUTOHOTSPOT_SERVICE_DAEMON}\"" "${AUTOHOTSPOT_TARGET_PATH}" + + + verify_files_exists "${AUTOHOTSPOT_SERVICE_DAEMON_PATH}" + verify_file_contains_string "\-i \"${WIFI_INTERFACE}\"" "${AUTOHOTSPOT_SERVICE_DAEMON_PATH}" + + verify_files_exists "${AUTOHOTSPOT_SERVICE_PATH}" + verify_file_contains_string "ExecStart=${AUTOHOTSPOT_TARGET_PATH}" "${AUTOHOTSPOT_SERVICE_PATH}" + + verify_files_exists "${AUTOHOTSPOT_TIMER_PATH}" + verify_file_contains_string "Unit=${AUTOHOTSPOT_SERVICE}" "${AUTOHOTSPOT_TIMER_PATH}" +} + +_run_setup_autohotspot_dhcpcd() { + log "Install AutoHotspot dhcpcd" + _install_packages_dhcpcd + _get_interface + _uninstall_autohotspot_dhcpcd + _install_autohotspot_dhcpcd + _autohotspot_check_dhcpcd +} diff --git a/resources/autohotspot/NetworkManager/autohotspot b/resources/autohotspot/NetworkManager/autohotspot new file mode 100644 index 000000000..992190376 --- /dev/null +++ b/resources/autohotspot/NetworkManager/autohotspot @@ -0,0 +1,353 @@ +#!/bin/bash +#version 0.8 +#date 12 Dec 2023 +#Copyright Graeme Richards - RaspberryConnect.com +#Released under the GPL3 Licence (https://www.gnu.org/licenses/gpl-3.0.en.html) + +#Script to automatically switch to an Access Point when no Wifi connection is available +#Developed on a Raspberry Pi PiOS Bookworm for use with Network Manager + +#Additions where made for the Phoniebox project +#https://github.com/MiczFlor/RPi-Jukebox-RFID + +#Device Names +wdev0='%%WIFI_INTERFACE%%' #wifi device that AP will work on + +#AP setup +ap_profile_name='%%AUTOHOTSPOT_PROFILE%%' +ap_ssid='%%AUTOHOTSPOT_SSID%%' +ap_pw='%%AUTOHOTSPOT_PASSWORD%%' +ap_ip='%%AUTOHOTSPOT_IP%%/24' +ap_gate='%%IP_WITHOUT_LAST_SEGMENT%%.254' + +timer_service_name='%%AUTOHOTSPOT_TIMER_NAME%%' + +#If wifi is disabled in Network Manager, then enable it automatically. +#if set to 'false' then wifi will stay off and no AccessPoint will be generated or Network Connection will be available. +re_enable_wifi=false + +#If true, check if timer is active. Will have been disabled if arg -a used. +re_enable_timer=false + +#************************************* +#*****No user editable lines below**** + +NO_SSID='NoSSid' + +profiles=() #Currently Available Profiles +active="" #The active connection +active_ap=false #is the active profile an AP y/n +nw_profile=() #saved NW Profiles +ap_profile=() #The saved AP profiles +ssidChk=("$NO_SSID") +force_hotspot=false + +#Function is NM installed and running +check_prerequisite() { + if systemctl -all list-unit-files NetworkManager.service | grep "could not be found" >/dev/null 2>&1 ;then + if systemctl -all list-unit-files dhcpcd.service | grep "(running)" >/dev/null 2>&1 ;then + echo "This script is not compatible with the network setup." + echo "Please use the dhcpcd version" + else + echo "Network Manager is not managing the Wifi on this device" + echo "Unable to continue." + fi + exit 1 + else + local isnm="$( systemctl status NetworkManager | grep '(running)' )" + if echo "$isnm" | grep -v "(running)" ;then >/dev/null 2>&1; #NM not running + echo "Network Manager is required but is not the active system network service" + echo "Unable to continue." + exit 1 + fi + fi +} + +#Function get all wifi profiles +saved_profiles() +{ + ap_profile=() + nw_profile=() + local n="$(nmcli -t -f TYPE,NAME,AUTOCONNECT-PRIORITY con show)" #Capture Output + n="$(awk 1 ORS=':' <(echo "$n"))" #Replaces LF with : Delimeter + local profiles=() + readarray -d ':' -t profiles < <(printf "%s" "$n") #Change to array output + if [ ! -z "$profiles" ]; then + for (( c=0; c<=${#profiles[@]}; c+=3 )) #array of profiles + do + if [ ! -z "${profiles[$c+1]}" ] ; then + local conn="$(nmcli con show "${profiles[$c+1]}" | grep 'wireless.mode')" #show mode infurstructure, AP + local mode=() + readarray -d ':' -t mode < <(printf "%s" "$conn") + local mode2="$(echo "${mode[1]}" | sed 's/[[:blank:]]//g')" + if [ "$mode2" = "ap" ]; then + ap_profile+=("${profiles[$c+1]}") + echo "AP Profile: ${profiles[$c+1]}" + elif [ "$mode2" = "infrastructure" ]; then + nw_profile+=("${profiles[$c+1]}") + echo "NW Profile: ${profiles[$c+1]}" + fi + fi + done + fi +} + +#Function what is the current active wifi +active_wifi() +{ + local act="$(nmcli -t -f TYPE,NAME,DEVICE con show --active | grep "$wdev0")" #List of active devices + act="$(awk 1 ORS=':' <(echo "$act"))" #Replaces LF with : Delimeter + local active_name=() + readarray -d ':' -t active_name < <(printf "%s" "$act") #Change to array output + if [ ! -z "$active_name" ]; then + active="${active_name[1]}" + else + active="" + fi +} + +#Function is the current Connection an AP +is_active_ap() +{ + active_ap=false + if [ ! -z "$active" ] ; then + for i in "${ap_profile[@]}" + do + if [[ $i == "$active" ]]; then + active_ap=true + break + fi + done + fi +} + +#Function IW SSID scan +nearby_ssids_iw() +{ + if [ ${#nw_profile[@]} -eq 0 ]; then #only scan if NW profiles exists# + return + fi + + #Check to see what SSID's and MAC addresses are in range + echo "SSID availability check" + local i=0; j=0 + while [ $i -eq 0 ] + do + local scanreply=$(iw dev "$wdev0" scan ap-force 2>&1) + local ssidreply=$(echo "$scanreply" | egrep "^BSS|SSID:") + if [ $j -ge 5 ]; then + ssidreply="" + i=1 + elif [ -z "$ssidreply" ] ; then + echo "Error scan SSID's, try $j: $scanreply" + j=$((j + 1)) + sleep 2 + else + #success + i=1 + fi + done + + ssidChk=() + for profile in "${nw_profile[@]}" + do + echo "Assessing profile: ${profile}" + local idssid=$(nmcli -t con show "${profile}" | grep "wireless.ssid") + if (echo "$ssidreply" | grep -F -- "${idssid:21}" ) >/dev/null 2>&1 + then + echo "Valid SSID detected, assessing Wifi status" + ssidChk+="${profile}" + fi + done + + if [ "${#ssidChk[@]}" -eq 0 ]; then + echo "No Valid SSID detected" + ssidChk+="$NO_SSID" + fi +} + +check_device() +{ + echo "Device availability check" + local j=0 + while [ true ] #wait for wifi if busy, usb wifi is slower. + do + if [ $j -ge 5 ]; then + echo "No wifi device '$wdev0' connected" + exit 1 + elif (nmcli device show "$wdev0" 2>&1 >/dev/null) ; then + echo "Wifi device '$wdev0' available" + if (rfkill list wifi -rno HARD,SOFT | grep -i "unblocked.*unblocked") >/dev/null 2>&1 ; then + return + else + if [[ $re_enable_wifi = true ]] ; then + nmcli radio wifi on + echo "Wifi has been re-activated" + sleep 10 #delay to allow wifi to initialise + return + else + echo "Wifi is deactivated" + exit 0 + fi + fi + else + j=$((j + 1)) + sleep 2 + fi + done +} + +#Activate AP profile +start_ap() +{ + local ex=0 + for i in "${ap_profile[@]}" + do + if [[ $i == "$ap_profile_name" ]]; then + ex=1 #known saved AP profile is available + break + fi + done + if [ $ex -eq 0 ];then + ap_new_profile #if known AP profile not found, create it + fi + nmcli con up "$ap_profile_name" >/dev/null 2>&1 + sleep 3 #give time for ap to be setup + active_wifi + is_active_ap + if [ "$active_ap" = true ]; then + echo "Access Point started" + local curip="$(nmcli -t con show $active | grep IP4.ADDRESS)" + readarray -d ':' -t ipid < <(printf "%s" "$curip") + local showip="$(echo "${ipid[1]}" | sed 's/[[:blank:]]//g')" + if [ ! -z $showip ]; then + echo "AP on IP Address ${showip::-3}" + fi + else + echo "AP failed to be created." + fi +} + +#Activate NW profile +start_nw() +{ + if [ "$active_ap" = true ]; then + echo "The active profile is $active. Shutting down" + nmcli con down "$active" >/dev/null 2>&1 + fi + + local active_nw="" + for i in "${nw_profile[@]}" + do + echo "Checking: $i" + con="$(nmcli con up $i)" + if ( echo "$con" | grep 'Connection successfully activated' ) >/dev/null 2>&1; then + echo "Connection was good" + active_wifi + active_nw="$active" + elif ( echo "$con" | grep 'Connection activation failed' ) >/dev/null 2>&1; then + echo "Unable to make a connection. Check the password is ok for the ssid ${nw_profile[$c]}" + active_nw="" + else + echo "Unable to confirm the connection status" + active_nw="" + fi + if [ ! -z "$active_nw" ] ;then + echo "A valid connection has been made with $i" + break + fi + done + + if [ -z "$active_nw" ] ;then + echo "A network connection has not been made with any known ssid. Activating access point" + start_ap + fi +} + +#Function Create AP profile +ap_new_profile() +{ + echo "Create a AP profile ${ap_profile_name}" + nmcli device wifi hotspot ifname $wdev0 con-name "$ap_profile_name" ssid "$ap_ssid" band bg password "$ap_pw" >/dev/null 2>&1 + nmcli con mod "$ap_profile_name" ipv4.method shared ipv4.addr "$ap_ip" ipv4.gateway "$ap_gate" >/dev/null 2>&1 + ap_profile+=("$ap_profile_name") +} + +#Main +check_prerequisite +check_device + +while getopts "aht" opt; do + case $opt in + a ) + force_hotspot=true + ;; + t ) + re_enable_timer=true + ;; + h ) + sc="$(basename $0)" + echo -e "\nby default the $sc script will setup a connection to a WiFi network where a profile exists" + echo "otherwise an Access Point called $ap_ssid will be created. Using ip address $ap_ip" + echo "The local wifi signals will be check every 2 minutes. If a known SSID comes into range" + echo "the Access Point will be shutdown and a connection to the Wifi network will be made." + echo "using sudo $sc -a will activate the Access Point regardless of any existing WiFi profile" + echo "and stop the timed checks. Use sudo $sc to return to normal use." + exit + ;; + * ) + echo "option not valid" + exit + ;; + esac +done + +saved_profiles #get list of saved profile +active_wifi +is_active_ap +echo -e "The active profile is $active\n" + +if [ "$force_hotspot" = true ]; then + if [ ! "$active_ap" = true ]; then + systemctl stop "$timer_service_name" + start_ap + elif [ ! "$active" = "$ap_profile_name" ]; then #Other AP is running, swap to this one + nmcli con down "$active" + start_ap + else + echo "Access Point $active is already running" + fi +else + if [ ! -z "$active" ]; then #Active Profile Yes + if [ "$active_ap" = true ]; then #Yes it's an AP profile + nearby_ssids_iw #scan for nearby SSID's + if [ "${ssidChk[0]}" != "$NO_SSID" ]; then #known ssid in range + start_nw + elif [ ! "$active" = "$ap_profile_name" ]; then #Other AP is running, swap to this one + nmcli con down "$active" + start_ap + fi + fi + else #no active profile + nearby_ssids_iw #scan for nearby SSID's + if [ "${ssidChk[0]}" != "$NO_SSID" ]; then #known ssid in range + start_nw + else + start_ap + fi + fi + + if [[ $re_enable_timer = true ]] ; then + #check if timer is active. Will have been disabled if arg -a used. + tup="$(systemctl list-timers | grep '${timer_service_name}')" + if [ -z "$tup" ];then + systemctl start "$timer_service_name" + echo "Reactivated timer" + fi + fi +fi + +active_wifi +is_active_ap +echo -e "\nThe Wifi profile in use is: $active" +echo -e "Is this a local access point? $active_ap\n" diff --git a/resources/autohotspot/NetworkManager/autohotspot.service b/resources/autohotspot/NetworkManager/autohotspot.service new file mode 100644 index 000000000..ff808cd9e --- /dev/null +++ b/resources/autohotspot/NetworkManager/autohotspot.service @@ -0,0 +1,11 @@ +[Unit] +Description=Automatically generates an wifi hotspot when a valid SSID is not in range +After=multi-user.target +Requires=network-online.target + +[Service] +Type=simple +ExecStart=%%AUTOHOTSPOT_SCRIPT%% + +[Install] +WantedBy=multi-user.target diff --git a/resources/autohotspot/NetworkManager/autohotspot.timer b/resources/autohotspot/NetworkManager/autohotspot.timer new file mode 100644 index 000000000..dc7427170 --- /dev/null +++ b/resources/autohotspot/NetworkManager/autohotspot.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Timer to run the %%AUTOHOTSPOT_SERVICE%% every 2 mins + +[Timer] +OnBootSec=0min +OnCalendar=*:0/2 +Unit=%%AUTOHOTSPOT_SERVICE%% + +[Install] +WantedBy=timers.target diff --git a/resources/autohotspot/autohotspot b/resources/autohotspot/autohotspot deleted file mode 100644 index 92cb7eebc..000000000 --- a/resources/autohotspot/autohotspot +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/bash -#version 0.961-N/HS - -#You may share this script on the condition a reference to RaspberryConnect.com -#must be included in copies or derivatives of this script. - -#A script to switch between a wifi network and a non internet routed Hotspot -#Works at startup or with a seperate timer or manually without a reboot -#Other setup required find out more at -#http://www.raspberryconnect.com - -wifidev="wlan0" #device name to use. Default is wlan0. -#use the command: iw dev ,to see wifi interface name - -IFSdef=$IFS -cnt=0 -#These four lines capture the wifi networks the RPi is setup to use -wpassid=$(awk '/ssid="/{ print $0 }' /etc/wpa_supplicant/wpa_supplicant.conf | awk -F'ssid=' '{ print $2 }' | sed 's/\r//g'| awk 'BEGIN{ORS=","} {print}' | sed 's/\"/''/g' | sed 's/,$//') -IFS="," -ssids=($wpassid) -IFS=$IFSdef #reset back to defaults - - -#Note:If you only want to check for certain SSIDs -#Remove the # in in front of ssids=('mySSID1'.... below and put a # infront of all four lines above -# separated by a space, eg ('mySSID1' 'mySSID2') -#ssids=('mySSID1' 'mySSID2' 'mySSID3') - -#Enter the Routers Mac Addresses for hidden SSIDs, seperated by spaces ie -#( '11:22:33:44:55:66' 'aa:bb:cc:dd:ee:ff' ) -mac=() - -ssidsmac=("${ssids[@]}" "${mac[@]}") #combines ssid and MAC for checking - -createAdHocNetwork() -{ - echo "Creating Hotspot" - ip link set dev "$wifidev" down - ip a add 10.0.0.5/24 brd + dev "$wifidev" - ip link set dev "$wifidev" up - dhcpcd -k "$wifidev" >/dev/null 2>&1 - systemctl start dnsmasq - systemctl start hostapd -} - -KillHotspot() -{ - echo "Shutting Down Hotspot" - ip link set dev "$wifidev" down - systemctl stop hostapd - systemctl stop dnsmasq - ip addr flush dev "$wifidev" - ip link set dev "$wifidev" up - dhcpcd -n "$wifidev" >/dev/null 2>&1 -} - -ChkWifiUp() -{ - echo "Checking WiFi connection ok" - sleep 20 #give time for connection to be completed to router - if ! wpa_cli -i "$wifidev" status | grep 'ip_address' >/dev/null 2>&1 - then #Failed to connect to wifi (check your wifi settings, password etc) - echo 'Wifi failed to connect, falling back to Hotspot.' - wpa_cli terminate "$wifidev" >/dev/null 2>&1 - createAdHocNetwork - fi -} - - -chksys() -{ - #After some system updates hostapd gets masked using Raspbian Buster, and above. This checks and fixes - #the issue and also checks dnsmasq is ok so the hotspot can be generated. - #Check Hostapd is unmasked and disabled - if systemctl -all list-unit-files hostapd.service | grep "hostapd.service masked" >/dev/null 2>&1 ;then - systemctl unmask hostapd.service >/dev/null 2>&1 - fi - if systemctl -all list-unit-files hostapd.service | grep "hostapd.service enabled" >/dev/null 2>&1 ;then - systemctl disable hostapd.service >/dev/null 2>&1 - systemctl stop hostapd >/dev/null 2>&1 - fi - #Check dnsmasq is disabled - if systemctl -all list-unit-files dnsmasq.service | grep "dnsmasq.service masked" >/dev/null 2>&1 ;then - systemctl unmask dnsmasq >/dev/null 2>&1 - fi - if systemctl -all list-unit-files dnsmasq.service | grep "dnsmasq.service enabled" >/dev/null 2>&1 ;then - systemctl disable dnsmasq >/dev/null 2>&1 - systemctl stop dnsmasq >/dev/null 2>&1 - fi -} - - -FindSSID() -{ -#Check to see what SSID's and MAC addresses are in range -ssidChk=('NoSSid') -i=0; j=0 -until [ $i -eq 1 ] #wait for wifi if busy, usb wifi is slower. -do - ssidreply=$((iw dev "$wifidev" scan ap-force | egrep "^BSS|SSID:") 2>&1) >/dev/null 2>&1 - #echo "SSid's in range: " $ssidreply - printf '%s\n' "${ssidreply[@]}" - echo "Device Available Check try " $j - if (($j >= 10)); then #if busy 10 times goto hotspot - echo "Device busy or unavailable 10 times, going to Hotspot" - ssidreply="" - i=1 - elif echo "$ssidreply" | grep "No such device (-19)" >/dev/null 2>&1; then - echo "No Device Reported, try " $j - NoDevice - elif echo "$ssidreply" | grep "Network is down (-100)" >/dev/null 2>&1 ; then - echo "Network Not available, trying again" $j - j=$((j + 1)) - sleep 2 - elif echo "$ssidreply" | grep "Read-only file system (-30)" >/dev/null 2>&1 ; then - echo "Temporary Read only file system, trying again" - j=$((j + 1)) - sleep 2 - elif echo "$ssidreply" | grep "Invalid exchange (-52)" >/dev/null 2>&1 ; then - echo "Temporary unavailable, trying again" - j=$((j + 1)) - sleep 2 - elif echo "$ssidreply" | grep -v "resource busy (-16)" >/dev/null 2>&1 ; then - echo "Device Available, checking SSid Results" - i=1 - else #see if device not busy in 2 seconds - echo "Device unavailable checking again, try " $j - j=$((j + 1)) - sleep 2 - fi -done - -for ssid in "${ssidsmac[@]}" -do - if (echo "$ssidreply" | grep -F -- "$ssid") >/dev/null 2>&1 - then - #Valid SSid found, passing to script - echo "Valid SSID Detected, assesing Wifi status" - ssidChk=$ssid - return 0 - else - #No Network found, NoSSid issued" - echo "No SSid found, assessing WiFi status" - ssidChk='NoSSid' - fi -done -} - -NoDevice() -{ - #if no wifi device,ie usb wifi removed, activate wifi so when it is - #reconnected wifi to a router will be available - echo "No wifi device connected" - wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1 - exit 1 -} - -chksys -FindSSID - -#Create Hotspot or connect to valid wifi networks -if [ "$ssidChk" != "NoSSid" ] -then - if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 - then #hotspot running and ssid in range - KillHotspot - echo "Hotspot Deactivated, Bringing Wifi Up" - wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1 - ChkWifiUp - elif { wpa_cli -i "$wifidev" status | grep 'ip_address'; } >/dev/null 2>&1 - then #Already connected - echo "Wifi already connected to a network" - else #ssid exists and no hotspot running connect to wifi network - echo "Connecting to the WiFi Network" - wpa_supplicant -B -i "$wifidev" -c /etc/wpa_supplicant/wpa_supplicant.conf >/dev/null 2>&1 - ChkWifiUp - fi -else #ssid or MAC address not in range - if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 - then - echo "Hostspot already active" - elif { wpa_cli status | grep "$wifidev"; } >/dev/null 2>&1 - then - echo "Cleaning wifi files and Activating Hotspot" - wpa_cli terminate >/dev/null 2>&1 - ip addr flush "$wifidev" - ip link set dev "$wifidev" down - rm -r /var/run/wpa_supplicant >/dev/null 2>&1 - createAdHocNetwork - else #"No SSID, activating Hotspot" - createAdHocNetwork - fi -fi diff --git a/resources/autohotspot/autohotspot.service b/resources/autohotspot/autohotspot.service deleted file mode 100644 index 13d9101b4..000000000 --- a/resources/autohotspot/autohotspot.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=Automatically generates an internet Hotspot when a valid ssid is not in range -After=multi-user.target - -[Service] -Type=oneshot -RemainAfterExit=yes -ExecStart=/usr/bin/autohotspot - -[Install] -WantedBy=multi-user.target diff --git a/resources/autohotspot/autohotspot.timer b/resources/autohotspot/autohotspot.timer deleted file mode 100644 index eb2acaebe..000000000 --- a/resources/autohotspot/autohotspot.timer +++ /dev/null @@ -1,3 +0,0 @@ -# cron timer for 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/autohotspot/dhcpcd/autohotspot b/resources/autohotspot/dhcpcd/autohotspot new file mode 100644 index 000000000..a7388512e --- /dev/null +++ b/resources/autohotspot/dhcpcd/autohotspot @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +#version 0.962-N/HS + +#You may share this script on the condition all references to RaspberryConnect.com +#must be included in copies or derivatives of this script. + +#A script to switch between a wifi network and a non internet routed Hotspot +#Works at startup or with a seperate timer or manually without a reboot +#Other setup required find out more at +#http://www.raspberryconnect.com + +#Additions where made for the Phoniebox project +#https://github.com/MiczFlor/RPi-Jukebox-RFID + +while getopts "a" opt; do + case $opt in + a ) + FORCE_HOTSPOT=true + ;; + * ) + echo "option not valid" + exit + ;; + esac +done + +NO_SSID='NoSSid' +ssidChk="$NO_SSID" + +wifidev="%%WIFI_INTERFACE%%" #device name to use. +#use the command: iw dev ,to see wifi interface name +hotspot_ip=%%AUTOHOTSPOT_IP%% +daemon_service="%%AUTOHOTSPOT_SERVICE_DAEMON%%" + +IFSdef=$IFS +cnt=0 +#These four lines capture the wifi networks the RPi is setup to use +wpassid=$(awk '/ssid="/{ print $0 }' /etc/wpa_supplicant/wpa_supplicant.conf | awk -F'ssid=' '{ print $2 }' | sed 's/\r//g'| awk 'BEGIN{ORS=","} {print}' | sed 's/\"/''/g' | sed 's/,$//') +IFS="," +ssids=($wpassid) +IFS=$IFSdef #reset back to defaults + + +#Note:If you only want to check for certain SSIDs +#Remove the # in in front of ssids=('mySSID1'.... below and put a # infront of all four lines above +# separated by a space, eg ('mySSID1' 'mySSID2') +#ssids=('mySSID1' 'mySSID2' 'mySSID3') + +#Enter the Routers Mac Addresses for hidden SSIDs, seperated by spaces ie +#( '11:22:33:44:55:66' 'aa:bb:cc:dd:ee:ff' ) +mac=() + +ssidsmac=("${ssids[@]}" "${mac[@]}") #combines ssid and MAC for checking + +CreateAdHocNetwork() +{ + echo "Creating Hotspot" + ip link set dev "$wifidev" down + ip a add "$hotspot_ip"/24 brd + dev "$wifidev" + ip link set dev "$wifidev" up + dhcpcd -k "$wifidev" >/dev/null 2>&1 + systemctl start dnsmasq + systemctl start hostapd +} + +KillHotspot() +{ + echo "Shutting Down Hotspot" + ip link set dev "$wifidev" down + systemctl stop hostapd + systemctl stop dnsmasq + ip addr flush dev "$wifidev" + ip link set dev "$wifidev" up + dhcpcd -n "$wifidev" >/dev/null 2>&1 +} + +CheckWifiUp() +{ + echo "Checking WiFi connection ok" + sleep 20 #give time for connection to be completed to router + if ! wpa_cli -i "$wifidev" status | grep 'ip_address' >/dev/null 2>&1 + then #Failed to connect to wifi (check your wifi settings, password etc) + echo 'Wifi failed to connect, falling back to Hotspot.' + wpa_cli terminate "$wifidev" >/dev/null 2>&1 + CreateAdHocNetwork + fi +} + +InitWPA() { + systemctl restart "$daemon_service" +} + +CheckServices() +{ + #After some system updates hostapd gets masked using Raspbian Buster, and above. This checks and fixes + #the issue and also checks dnsmasq is ok so the hotspot can be generated. + #Check Hostapd is unmasked and disabled + if (systemctl -all list-unit-files hostapd.service | grep "hostapd.service masked") >/dev/null 2>&1 ;then + systemctl unmask hostapd.service >/dev/null 2>&1 + fi + if (systemctl -all list-unit-files hostapd.service | grep "hostapd.service enabled") >/dev/null 2>&1 ;then + systemctl disable hostapd.service >/dev/null 2>&1 + systemctl stop hostapd >/dev/null 2>&1 + fi + #Check dnsmasq is disabled + if (systemctl -all list-unit-files dnsmasq.service | grep "dnsmasq.service masked") >/dev/null 2>&1 ;then + systemctl unmask dnsmasq >/dev/null 2>&1 + fi + if (systemctl -all list-unit-files dnsmasq.service | grep "dnsmasq.service enabled") >/dev/null 2>&1 ;then + systemctl disable dnsmasq >/dev/null 2>&1 + systemctl stop dnsmasq >/dev/null 2>&1 + fi +} + +CheckDevice() +{ + echo "Device availability check" + local j=0 + while [ true ] #wait for wifi if busy, usb wifi is slower. + do + + if [ $j -ge 5 ]; then + #if no wifi device,ie usb wifi removed, activate wifi so when it is + #reconnected wifi to a router will be available + echo "No wifi device '$wifidev' connected" + InitWPA + exit 1 + elif (iw dev "$wifidev" info 2>&1 >/dev/null) ; then + echo "Wifi device '$wifidev' available" + if (rfkill list wifi -rno HARD,SOFT | grep -i "unblocked.*unblocked") >/dev/null 2>&1 ; then + local wifidev_up=$(ip link show "$wifidev" up) + if [ -z "$wifidev_up" ]; then + echo "Wifi is down. Setting up" + ip link set dev "$wifidev" up + sleep 2 + fi + return + else + echo "Wifi is deactivated" + exit 0 + fi + else + j=$((j + 1)) + sleep 2 + fi + done +} + +FindSSID() +{ + if [ -n "$FORCE_HOTSPOT" ]; then return; fi + + #Check to see what SSID's and MAC addresses are in range + echo "SSID availability check" + local i=0; j=0 + while [ $i -eq 0 ] + do + scanreply=$(iw dev "$wifidev" scan ap-force 2>&1) + ssidreply=$(echo "$scanreply" | egrep "^BSS|SSID:") + if [ $j -ge 5 ]; then + ssidreply="" + i=1 + elif [ -z "$ssidreply" ] ; then + echo "Error scan SSID's, try $j: $scanreply" + j=$((j + 1)) + sleep 2 + else + #success + i=1 + fi + done + + for ssid in "${ssidsmac[@]}" + do + if (echo "$ssidreply" | grep "$ssid") >/dev/null 2>&1 + then + echo "Valid SSID detected, assessing Wifi status" + ssidChk=$ssid + break + fi + done + + if [ "$ssidChk" == "$NO_SSID" ]; then + echo "No Valid SSID detected" + fi +} + +CheckSSID() +{ + #Create Hotspot or connect to valid wifi networks + if [ "$ssidChk" != "$NO_SSID" ] + then + if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 + then #hotspot running and ssid in range + KillHotspot + echo "Hotspot Deactivated, Bringing Wifi Up" + InitWPA + CheckWifiUp + elif { wpa_cli -i "$wifidev" status | grep 'ip_address'; } >/dev/null 2>&1 + then #Already connected + echo "Wifi already connected to a network" + else #ssid exists and no hotspot running connect to wifi network + echo "Connecting to the WiFi Network" + InitWPA + CheckWifiUp + fi + else #ssid or MAC address not in range + if systemctl status hostapd | grep "(running)" >/dev/null 2>&1 + then + echo "Hostspot already active" + elif { wpa_cli status | grep "$wifidev"; } >/dev/null 2>&1 + then + echo "Cleaning wifi files and Activating Hotspot" + wpa_cli terminate >/dev/null 2>&1 + ip addr flush "$wifidev" + ip link set dev "$wifidev" down + rm -r /var/run/wpa_supplicant >/dev/null 2>&1 + CreateAdHocNetwork + else #"No SSID, activating Hotspot" + CreateAdHocNetwork + fi + fi +} + +CheckServices +CheckDevice +FindSSID +CheckSSID diff --git a/resources/autohotspot/dhcpcd/autohotspot-daemon.service b/resources/autohotspot/dhcpcd/autohotspot-daemon.service new file mode 100644 index 000000000..5906a4840 --- /dev/null +++ b/resources/autohotspot/dhcpcd/autohotspot-daemon.service @@ -0,0 +1,10 @@ +[Unit] +Description=Daemon for regular network connection if no hotspot is created +After=multi-user.target + +[Service] +Type=simple +ExecStart=wpa_supplicant -i "%%WIFI_INTERFACE%%" -c /etc/wpa_supplicant/wpa_supplicant.conf + +[Install] +WantedBy=multi-user.target diff --git a/resources/autohotspot/dhcpcd/autohotspot.service b/resources/autohotspot/dhcpcd/autohotspot.service new file mode 100644 index 000000000..ff808cd9e --- /dev/null +++ b/resources/autohotspot/dhcpcd/autohotspot.service @@ -0,0 +1,11 @@ +[Unit] +Description=Automatically generates an wifi hotspot when a valid SSID is not in range +After=multi-user.target +Requires=network-online.target + +[Service] +Type=simple +ExecStart=%%AUTOHOTSPOT_SCRIPT%% + +[Install] +WantedBy=multi-user.target diff --git a/resources/autohotspot/dhcpcd/autohotspot.timer b/resources/autohotspot/dhcpcd/autohotspot.timer new file mode 100644 index 000000000..dc7427170 --- /dev/null +++ b/resources/autohotspot/dhcpcd/autohotspot.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Timer to run the %%AUTOHOTSPOT_SERVICE%% every 2 mins + +[Timer] +OnBootSec=0min +OnCalendar=*:0/2 +Unit=%%AUTOHOTSPOT_SERVICE%% + +[Install] +WantedBy=timers.target diff --git a/resources/autohotspot/dhcpcd/dnsmasq.conf b/resources/autohotspot/dhcpcd/dnsmasq.conf new file mode 100644 index 000000000..da0ba46ef --- /dev/null +++ b/resources/autohotspot/dhcpcd/dnsmasq.conf @@ -0,0 +1,7 @@ +#AutoHotspot Config +#stop DNSmasq from using resolv.conf +no-resolv +#Interface to use +interface=%%WIFI_INTERFACE%% +bind-interfaces +dhcp-range=%%IP_WITHOUT_LAST_SEGMENT%%.100,%%IP_WITHOUT_LAST_SEGMENT%%.200,12h diff --git a/resources/autohotspot/dhcpcd/hostapd b/resources/autohotspot/dhcpcd/hostapd new file mode 100644 index 000000000..92798c162 --- /dev/null +++ b/resources/autohotspot/dhcpcd/hostapd @@ -0,0 +1,20 @@ +# Defaults for hostapd initscript +# +# See /usr/share/doc/hostapd/README.Debian for information about alternative +# methods of managing hostapd. +# +# Uncomment and set DAEMON_CONF to the absolute path of a hostapd configuration +# file and hostapd will be started during system boot. An example configuration +# file can be found at /usr/share/doc/hostapd/examples/hostapd.conf.gz +# +DAEMON_CONF="%%HOSTAPD_CONF%%" + +# Additional daemon options to be appended to hostapd command:- +# -d show more debug messages (-dd for even more) +# -K include key data in debug messages +# -t include timestamps in some debug messages +# +# Note that -B (daemon mode) and -P (pidfile) options are automatically +# configured by the init.d script and must not be added to DAEMON_OPTS. +# +#DAEMON_OPTS="" diff --git a/resources/autohotspot/hostapd.conf b/resources/autohotspot/dhcpcd/hostapd.conf similarity index 66% rename from resources/autohotspot/hostapd.conf rename to resources/autohotspot/dhcpcd/hostapd.conf index 8860368a4..4edc8f045 100644 --- a/resources/autohotspot/hostapd.conf +++ b/resources/autohotspot/dhcpcd/hostapd.conf @@ -1,7 +1,7 @@ #2.4GHz setup wifi 80211 b,g,n -interface=WIFI_INTERFACE +interface=%%WIFI_INTERFACE%% driver=nl80211 -ssid=Phoniebox_Hotspot +ssid=%%AUTOHOTSPOT_SSID%% hw_mode=g channel=8 wmm_enabled=0 @@ -9,12 +9,12 @@ macaddr_acl=0 auth_algs=1 ignore_broadcast_ssid=0 wpa=2 -wpa_passphrase=AUTOHOTSPOT_PASSWORD +wpa_passphrase=%%AUTOHOTSPOT_PASSWORD%% wpa_key_mgmt=WPA-PSK wpa_pairwise=CCMP TKIP rsn_pairwise=CCMP #80211n - Change GB to your WiFi country code -country_code=WIFI_REGION +country_code=%%AUTOHOTSPOT_COUNTRYCODE%% ieee80211n=1 ieee80211d=1 diff --git a/src/cli_client/pbc.c b/src/cli_client/pbc.c index 0b5cfb762..7f39c493e 100644 --- a/src/cli_client/pbc.c +++ b/src/cli_client/pbc.c @@ -44,7 +44,7 @@ #define MAX_PARAMS 16 int g_verbose = 0; -typedef struct +typedef struct { char object [MAX_STRLEN]; char package [MAX_STRLEN]; @@ -61,44 +61,44 @@ int send_zmq_request_and_wait_response(char * request, int request_len, char * r void *context = zmq_ctx_new (); void *requester = zmq_socket (context, ZMQ_REQ); int linger = 200; - + if (g_verbose) { int major, minor, patch; zmq_version (&major, &minor, &patch); printf ("Current ØMQ version is %d.%d.%d\n", major, minor, patch); } - + zmq_setsockopt(requester,ZMQ_LINGER,&linger,sizeof(linger)); zmq_setsockopt(requester,ZMQ_RCVTIMEO,&linger,sizeof(linger)); zmq_connect (requester, address); if (g_verbose) printf("connected to: %s",address); - + zmq_ret = zmq_send (requester, request, request_len, 0); if (zmq_ret > 0) { zmq_ret = zmq_recv (requester, response, max_response_len, 0); - + if (zmq_ret > 0) { - printf ("Received %s (%d Bytes)\n", response,zmq_ret); + printf ("Received %s (%d Bytes)\n", response,zmq_ret); ret = 0; } else { - printf ("zmq_recv rturned %d \n", zmq_ret); + printf ("zmq_recv rturned %d \n", zmq_ret); } } else { - if (g_verbose) printf ("zmq_send returned %d\n", zmq_ret); + if (g_verbose) printf ("zmq_send returned %d\n", zmq_ret); } zmq_close (requester); - zmq_ctx_destroy (context); + zmq_ctx_destroy (context); return (ret); } @@ -114,7 +114,7 @@ void * connect_and_send_request(t_request * tr) if (tr->num_params > 0) { sprintf(kwargs, "\"kwargs\":{"); - + for (n = 0;n < tr->num_params;) { strcat(kwargs,tr->params[n]); @@ -129,7 +129,7 @@ void * connect_and_send_request(t_request * tr) 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); send_zmq_request_and_wait_response(json_request,json_len,json_response,MAX_REQEST_STRLEN,tr->address); @@ -177,7 +177,7 @@ int HandleOptions(int argc,char *argv[], t_request * tr) { int c; sprintf(tr->address,"tcp://localhost:5555"); - + const struct option long_options[] = { /* These options set a flag. */ @@ -213,7 +213,7 @@ int HandleOptions(int argc,char *argv[], t_request * tr) break; case 'p': strncpy (tr->package,optarg,MAX_STRLEN); - break; + break; case 'o': strncpy (tr->object,optarg,MAX_STRLEN); break; @@ -229,7 +229,7 @@ int HandleOptions(int argc,char *argv[], t_request * tr) case 'a': strncpy (tr->address,optarg,MAX_STRLEN); break; - + default: usage(); abort (); @@ -240,7 +240,7 @@ int HandleOptions(int argc,char *argv[], t_request * tr) if (optind < argc) { while (optind < argc) - { + { check_and_map_parameters_to_json(argv[optind++], tr); } } @@ -256,6 +256,6 @@ int main(int argc,char *argv[]) HandleOptions(argc,argv,&tr); connect_and_send_request(&tr); - + return 0; } diff --git a/src/jukebox/components/hostif/linux/__init__.py b/src/jukebox/components/hostif/linux/__init__.py index 32dfded08..6a6590ad6 100644 --- a/src/jukebox/components/hostif/linux/__init__.py +++ b/src/jukebox/components/hostif/linux/__init__.py @@ -238,7 +238,7 @@ def get_autohotspot_status(): if os.path.isfile("/etc/systemd/system/autohotspot.service"): status = 'inactive' - ret = subprocess.run(['systemctl', 'is-active', 'autohotspot'], + ret = subprocess.run(['systemctl', 'is-active', 'autohotspot.timer'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, stdin=subprocess.DEVNULL) # 0 = active, 3 = inactive @@ -260,22 +260,21 @@ def get_autohotspot_status(): def stop_autohotspot(): """Stop auto hotspot functionality - Basically disabling the cronjob and running the script one last time manually + Stopping and disabling the timer and running the service one last time manually """ if os.path.isfile("/etc/systemd/system/autohotspot.service"): - cron_job = "/etc/cron.d/autohotspot" - subprocess.run(["sudo", "sed", "-i", r"s/^\*.*/#&/", cron_job], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) - subprocess.run(['sudo', '/usr/bin/systemctl', 'stop', 'autohotspot'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) - subprocess.run(['sudo', '/usr/bin/systemctl', 'disable', 'autohotspot'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) - ret = subprocess.run(['sudo', 'autohotspot'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - check=False) - if ret.returncode != 0: - msg = f"Error 'stop_autohotspot': {ret.stdout} (Code: {ret.returncode})" - logger.error(msg) - return {'error': {'code': -1, 'message': msg}} + # Stop timer + subprocess.run(['sudo', '/usr/bin/systemctl', 'stop', 'autohotspot.timer'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + # Prevent start after system restart + subprocess.run(['sudo', '/usr/bin/systemctl', 'disable', 'autohotspot.timer'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + # Prevent start after system restart (should always be disabled, but make sure) + subprocess.run(['sudo', '/usr/bin/systemctl', 'disable', 'autohotspot.service'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + + subprocess.run(['sudo', '/usr/bin/systemctl', 'start', 'autohotspot.service'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) return 'inactive' else: @@ -285,24 +284,17 @@ def stop_autohotspot(): @plugin.register() def start_autohotspot(): - """start auto hotspot functionality + """Start auto hotspot functionality - Basically enabling the cronjob and running the script one time manually + Enabling and starting the timer (timer will start the service) """ if os.path.isfile("/etc/systemd/system/autohotspot.service"): - cron_job = "/etc/cron.d/autohotspot" - subprocess.run(["sudo", "sed", "-i", "-r", r"s/(^#)(\*[0-9]*)/\*/", cron_job], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) - subprocess.run(['sudo', '/usr/bin/systemctl', 'start', 'autohotspot'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) - subprocess.run(['sudo', '/usr/bin/systemctl', 'enable', 'autohotspot'], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) - ret = subprocess.run(['sudo', 'autohotspot'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - check=False) - if ret.returncode != 0: - msg = f"Error 'start_autohotspot': {ret.stdout} (Code: {ret.returncode})" - logger.error(msg) - return {'error': {'code': -1, 'message': msg}} + # Enable start after system restart + subprocess.run(['sudo', '/usr/bin/systemctl', 'enable', 'autohotspot.timer'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) + # Start timer (starts the service immediately) + subprocess.run(['sudo', '/usr/bin/systemctl', 'start', 'autohotspot.timer'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False) return 'active' else: From 20389e3f3a18582bc21d2fb9b48978d8324a56b5 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:11:45 +0100 Subject: [PATCH 18/24] Allow default value for CoverArtCache path (#2237) --- src/jukebox/components/playermpd/__init__.py | 4 +--- .../components/playermpd/coverart_cache_manager.py | 8 ++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index 65c6e7267..49f630224 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -157,9 +157,7 @@ def __init__(self): self.decode_2nd_swipe_option() self.mpd_client = mpd.MPDClient() - - coverart_cache_path = cfg.getn('webapp', 'coverart_cache_path') - self.coverart_cache_manager = CoverartCacheManager(os.path.expanduser(coverart_cache_path)) + self.coverart_cache_manager = CoverartCacheManager() # 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), diff --git a/src/jukebox/components/playermpd/coverart_cache_manager.py b/src/jukebox/components/playermpd/coverart_cache_manager.py index 7883372ba..a7ae12eef 100644 --- a/src/jukebox/components/playermpd/coverart_cache_manager.py +++ b/src/jukebox/components/playermpd/coverart_cache_manager.py @@ -1,9 +1,13 @@ import os +import jukebox.cfghandler + +cfg = jukebox.cfghandler.get_handler('jukebox') class CoverartCacheManager: - def __init__(self, cache_folder_path): - self.cache_folder_path = cache_folder_path + def __init__(self): + coverart_cache_path = cfg.setndefault('webapp', 'coverart_cache_path', value='../../src/webapp/build/cover-cache') + self.cache_folder_path = os.path.expanduser(coverart_cache_path) def find_file_by_hash(self, hash_value): for filename in os.listdir(self.cache_folder_path): From 5c491085d04d12168a26abc1414344639e6c2d67 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Sun, 4 Feb 2024 23:09:36 +0100 Subject: [PATCH 19/24] Bump to v3.6.0-alpha --- src/jukebox/jukebox/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jukebox/jukebox/version.py b/src/jukebox/jukebox/version.py index 65ccc87c0..a7f37c5e6 100644 --- a/src/jukebox/jukebox/version.py +++ b/src/jukebox/jukebox/version.py @@ -1,8 +1,8 @@ VERSION_MAJOR = 3 -VERSION_MINOR = 5 +VERSION_MINOR = 6 VERSION_PATCH = 0 -VERSION_EXTRA = "" +VERSION_EXTRA = "alpha" # build a version string in compliance with the SemVer specification # https://semver.org/#semantic-versioning-specification-semver From b35013c70def28506e7bae3a79fdc0e01b6d656f Mon Sep 17 00:00:00 2001 From: s-martin Date: Mon, 5 Feb 2024 10:54:43 +0100 Subject: [PATCH 20/24] Link to nfcpy in docs (#2238) * Link to nfcpy * update doc for serial devices * Change name Co-authored-by: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> --------- Co-authored-by: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> --- documentation/developers/rfid/README.md | 3 ++- documentation/developers/rfid/generic_nfcpy.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/documentation/developers/rfid/README.md b/documentation/developers/rfid/README.md index c1412a90b..0b2df4db3 100644 --- a/documentation/developers/rfid/README.md +++ b/documentation/developers/rfid/README.md @@ -8,6 +8,7 @@ * [RDM6300 Reader](rdm6300.md) * [MFRC522 SPI Reader](mfrc522_spi.md) * [PN532 I2C Reader](pn532_i2c.md) + * [Generic Readers without HID (NFCpy)](generic_nfcpy.md) * [Mock Reader](mock_reader.md) * [Template Reader](template_reader.md) - \ No newline at end of file + diff --git a/documentation/developers/rfid/generic_nfcpy.md b/documentation/developers/rfid/generic_nfcpy.md index bea7b302a..76de98d88 100644 --- a/documentation/developers/rfid/generic_nfcpy.md +++ b/documentation/developers/rfid/generic_nfcpy.md @@ -4,7 +4,7 @@ This module is based on the user space NFC reader library [nfcpy](https://nfcpy. The link above also contains a list of [supported devices](https://nfcpy.readthedocs.io/en/latest/overview.html#supported-devices). The goal of this module is to handle USB NFC devices, that don't have a HID-keyboard -driver, and thus cannot be used with the [genericusb](genericusb.md) module. +driver, and thus cannot be used with the [genericusb](genericusb.md) module. Also some serial devices are supported. > [!NOTE] > Since nfcpy is a user-space library, it is required to supress the kernel from loading its driver. From 027ec2905e006565fbba2f51f90e2a7d3053838c Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Sun, 11 Feb 2024 22:10:39 +0100 Subject: [PATCH 21/24] remove listen on ipv6 if disabled (#2254) --- installation/includes/02_helpers.sh | 15 +++++++++++++++ installation/routines/setup_jukebox_webapp.sh | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/installation/includes/02_helpers.sh b/installation/includes/02_helpers.sh index be496b1b9..dfad65187 100644 --- a/installation/includes/02_helpers.sh +++ b/installation/includes/02_helpers.sh @@ -299,6 +299,21 @@ verify_file_contains_string() { log " CHECK" } +verify_file_does_not_contain_string() { + local string="$1" + local file="$2" + log " Verify '${string}' not found in '${file}'" + + if [[ -z "${string}" || -z "${file}" ]]; then + exit_on_error "ERROR: at least one parameter value is missing!" + fi + + if grep -iq "${string}" "${file}"; then + exit_on_error "ERROR: '${string}' found in '${file}'" + fi + log " CHECK" +} + verify_file_contains_string_once() { local string="$1" local file="$2" diff --git a/installation/routines/setup_jukebox_webapp.sh b/installation/routines/setup_jukebox_webapp.sh index 7fcbce7ff..f95e547f1 100644 --- a/installation/routines/setup_jukebox_webapp.sh +++ b/installation/routines/setup_jukebox_webapp.sh @@ -117,6 +117,10 @@ _jukebox_webapp_register_as_system_service_with_nginx() { 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}" + if [ "$DISABLE_IPv6" = true ] ; then + sudo sed -i '/listen \[::\]:80/d' "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" + fi + # make sure nginx can access the home directory of the user sudo chmod o+x "${HOME_PATH}" @@ -147,6 +151,10 @@ _jukebox_webapp_check() { verify_apt_packages nginx verify_files_exists "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" + if [ "$DISABLE_IPv6" = true ] ; then + verify_file_does_not_contain_string "listen [::]:80" "${WEBAPP_NGINX_SITE_DEFAULT_CONF}" + fi + verify_service_enablement nginx.service enabled } From 9110c9424ea086aa7289a59ac7d69d0297b3adc0 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:50:56 +0100 Subject: [PATCH 22/24] hotfix v3.5.1 --- src/jukebox/jukebox/version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jukebox/jukebox/version.py b/src/jukebox/jukebox/version.py index a7f37c5e6..dae1bc8c0 100644 --- a/src/jukebox/jukebox/version.py +++ b/src/jukebox/jukebox/version.py @@ -1,8 +1,8 @@ VERSION_MAJOR = 3 -VERSION_MINOR = 6 -VERSION_PATCH = 0 -VERSION_EXTRA = "alpha" +VERSION_MINOR = 5 +VERSION_PATCH = 1 +VERSION_EXTRA = "" # build a version string in compliance with the SemVer specification # https://semver.org/#semantic-versioning-specification-semver From d6b48d8c7a3cb5069e05687a1ac397c96f45d6e4 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Mon, 12 Feb 2024 00:07:24 +0100 Subject: [PATCH 23/24] Bump to v3.6.0-alpha --- src/jukebox/jukebox/version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jukebox/jukebox/version.py b/src/jukebox/jukebox/version.py index dae1bc8c0..a7f37c5e6 100644 --- a/src/jukebox/jukebox/version.py +++ b/src/jukebox/jukebox/version.py @@ -1,8 +1,8 @@ VERSION_MAJOR = 3 -VERSION_MINOR = 5 -VERSION_PATCH = 1 -VERSION_EXTRA = "" +VERSION_MINOR = 6 +VERSION_PATCH = 0 +VERSION_EXTRA = "alpha" # build a version string in compliance with the SemVer specification # https://semver.org/#semantic-versioning-specification-semver From 75743da4ccbd9e7b68f6e410eb89fefdc240736e Mon Sep 17 00:00:00 2001 From: s-martin Date: Wed, 14 Feb 2024 09:25:01 +0100 Subject: [PATCH 24/24] Extract docs for battery monitor (#2257) * extract battmon docs into markdown * Fix typo * add link to ADS1015 --- documentation/builders/README.md | 16 +++++---- .../components/power/batterymonitor.md | 33 +++++++++++++++++++ .../batt_mon_i2c_ads1015/__init__.py | 31 +---------------- 3 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 documentation/builders/components/power/batterymonitor.md diff --git a/documentation/builders/README.md b/documentation/builders/README.md index 6d9e67bff..ed08f5980 100644 --- a/documentation/builders/README.md +++ b/documentation/builders/README.md @@ -9,29 +9,31 @@ ## Features * Audio - * [Audio Output](./audio.md) - * [Bluetooth audio buttons](./bluetooth-audio-buttons.md) + * [Audio Output](./audio.md) + * [Bluetooth audio buttons](./bluetooth-audio-buttons.md) * [GPIO Recipes](./gpio.md) * [Card Database](./card-database.md) - * [RFID Cards synchronisation](./components/synchronisation/rfidcards.md) + * [RFID Cards synchronisation](./components/synchronisation/rfidcards.md) * [Auto Hotspot](./autohotspot.md) * File Management - * [Network share / Samba](./samba.md) + * [Network share / Samba](./samba.md) ## Hardware Components * [Power](./components/power/) - * [OnOff SHIM for safe power on/off](./components/power/onoff-shim.md) + * [OnOff SHIM for safe power on/off](./components/power/onoff-shim.md) + * [Battery Monitor based on a ADS1015](./components/power/batterymonitor.md) * [Soundcards](./components/soundcards/) - * [HiFiBerry Boards](./components/soundcards/hifiberry.md) + * [HiFiBerry Boards](./components/soundcards/hifiberry.md) * [RFID Readers](./../developers/rfid/README.md) ## Web Application * Music - * [Playlists, Livestreams and Podcasts](./webapp/playlists-livestreams-podcasts.md) + * [Playlists, Livestreams and Podcasts](./webapp/playlists-livestreams-podcasts.md) ## Advanced + * [Troubleshooting](./troubleshooting.md) * [Concepts](./concepts.md) * [System](./system.md) diff --git a/documentation/builders/components/power/batterymonitor.md b/documentation/builders/components/power/batterymonitor.md new file mode 100644 index 000000000..d57e14acb --- /dev/null +++ b/documentation/builders/components/power/batterymonitor.md @@ -0,0 +1,33 @@ +# Battery Monitor based on a ADS1015 + +> [!CAUTION] +> Lithium and other batteries are dangerous and must be treated with care. +> Rechargeable Lithium Ion batteries are potentially hazardous and can +> present a serious **FIRE HAZARD** if damaged, defective or improperly used. +> Do not use this circuit to a lithium ion battery without expertise and +> training in handling and use of batteries of this type. +> Use appropriate test equipment and safety protocols during development. +> There is no warranty, this may not work as expected or at all! + +The script in [src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/\_\_init\_\_.py](../../../../src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py) is intended to read out the voltage of a single Cell LiIon Battery using a [CY-ADS1015 Board](https://www.adafruit.com/product/1083): + +```text + 3.3V + + + | + .----o----. + ___ | | SDA + .--------|___|---o----o---------o AIN0 o------ + | 2MΩ | | | | SCL + | .-. | | ADS1015 o------ + --- | | --- | | + Battery - 1.5MΩ| | ---100nF '----o----' + 2.9V-4.2V| '-' | | + | | | | + === === === === +``` + +> [!WARNING] +> +> * the circuit is constantly draining the battery! (leak current up to: 2.1µA) +> * the time between sample needs to be a minimum 1sec with this high impedance voltage divider don't use the continuous conversion method! diff --git a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py index af193a37f..551759c14 100644 --- a/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py +++ b/src/jukebox/components/battery_monitor/batt_mon_i2c_ads1015/__init__.py @@ -37,36 +37,7 @@ class battmon_ads1015(BatteryMonitorBase.BattmonBase): """Battery Monitor based on a ADS1015 - > [!CAUTION] - > Lithium and other batteries are dangerous and must be treated with care. - > Rechargeable Lithium Ion batteries are potentially hazardous and can - > present a serious **FIRE HAZARD** if damaged, defective or improperly used. - > Do not use this circuit to a lithium ion battery without expertise and - > training in handling and use of batteries of this type. - > Use appropriate test equipment and safety protocols during development. - > There is no warranty, this may not work as expected or at all! - - This script is intended to read out the Voltage of a single Cell LiIon Battery using a CY-ADS1015 Board: - - 3.3V - + - | - .----o----. - ___ | | SDA - .--------|___|---o----o---------o AIN0 o------ - | 2MΩ | | | | SCL - | .-. | | ADS1015 o------ - --- | | --- | | - Battery - 1.5MΩ| | ---100nF '----o----' - 2.9V-4.2V| '-' | | - | | | | - === === === === - - Attention: - * the circuit is constantly draining the battery! (leak current up to: 2.1µA) - * the time between sample needs to be a minimum 1sec with this high impedance voltage divider - don't use the continuous conversion method! - + See [Battery Monitor documentation](../../builders/components/power/batterymonitor.md) """ def __init__(self, cfg):