Package: plapperkasten
plapperkastenManage a headless media device.What?!Turn a Raspberry Pi with only buttons and speakers attached into a jukebox playing local music.In a typical setup you might have a couple of buttons wired to a raspberry pi's GPIO pins, some speakers attached to the audio jack and a RFID reader plugged into one of the USB ports.This is based on the idea of thePhonieBox("original" software, more information onawesomeopensource.com) but written from scratch and not as feature-rich - yet!plapperkastenhas started as a small script to learn the workings of a Raspberry Pi box and to avoid the great but intimidatingly complexRPi-Jukebox-RFID.  As the project has grown more complex and mature I've decided to release it into the wild - so here it is.FeaturesEasy setup: install the python package fromPyPi(withpip install plapperkasten) or configure the whole machine usingplapperkasten-setup. This will guide you from the download of the OS all the way to a functioning device includingSSH, setting up an isolated python environment, configuring sound, users, etc.Nearly everything is a plugin: Theres a plugin for controlling the sound usingALSAand one forPipeWire. Need to output toJACKorPulseAudio?Write your own plugin by lending from the existing plugins.Completely written in Python 3:Does not run as a super user:plapperkastenis geared towards interacting with hardware buttons, sound and the like without needing special privileges. This needs some configuration of your OS butplapperkasten-setupis there to help you - or if you want it your way to give you some hints.SetupPlease seehttps://github.com/randomchars42/plapperkasten-setupfor detailed instructions and an easy installer.DevelopmentStyleDocstrings are formatted according to theGoogle Style Guide.All other formatting tries to adhere toPEP8and is enforced byYAPF.Log entries use lazy evaluation, i.e.,logger.debug('start %s', name), start with a lower-case letter and do not end with a full stop.Raised errors on the other hand use f-strings (if necessary) and contain whole sentences, i.e.ValueError(f'{variable} did not match XXXX.').Linting / CheckingCode should be checked bypylintandmypy.PathsAll path representations should bepathlib.Path-objects instead of strings.LoggingLogging uses a wrapper (plapperkasten.plklogging) around theloggingmodule to cover logging from multiple processes.Import a logger using:fromplapperkasten.plkloggingimportplklogginglogger:plklogging.PlkLogger=plklogging.get_logger(__name__)# your code hereSetup for developmentRequirementsPython >= 3.10as it uses typehinting features only available beginning with 3.10.Semi-optionallibgpiodwithpython3-libgpiodandgpiodmonitor>=1.1.3if you plan to use theinputgpiod-plugin. Alternatively you may implement the functionality usingRPi.GPIOor any library of your choice - but beware: you're gooing to need super user privileges!Recommendationspyenvto setup a python version withput messing up your system.pipenvto create a virtual environment with a defined python version.Example setup using pipenvgitclonegit@github.com:randomchars42/plapperkasten.gitcdplapperkasten# consider adding this to your .bashrc / equivalent for your shell:# `export PIPENV_VENV_IN_PROJECT=1`# this leads to a folder `.venv` being created at the project root# otherwise you might need to tweak the `[tool.mypy]`` path in# `pyproject.toml` (depending on your editor setup).# setup a virtual environment with set python version# set the version to >= 3.10pipenv--python3.10# install development dependenciespipenvinstall--dev# activate venvpipenvshellPlapperkasten logic in a nutshellplapperkastenis a simple platform for plugins that can talk to each other to control a jukebox. There is a plugin recieving input from buttons (src/plapperkasten/plugins/inputgpiod), one recieving input from RFID readers (src/plapperkasten/plugins/inputdevinputevent), one for controlling an MPD client (src/plapperkasten/plugins/mpdclient) and so forth.A good place to get to know the overall logic issrc/plapperkasten/plapperkasten.py. This is the main entry point into the programme.It will in turn:start a process for loggingload the configuration filesgather all plugins fromsrc/plapperkasten/pluginsand~/.config/plapperkasten/plugins(or wherever~/.config/plapperkasten/config.yamlpoints it to)start a process for each plugin (that is not blacklisted in~/.config/plapperkasten/config.yaml)the plugins register for events they might processtriggeron_before_run()for each plugginwait for the plugins to send events to process, translate or re-emit so that other plugins can react to themor wait for all plugins to exit (either by themselves or because of aterminateevent)if ashutdownevent has been emitted,plapperkastenwill try to shutdown the host (here's how to stop it during development)Examplary flow of eventsIn a typical setup you might have a couple of buttons wired to a raspberry pi's GPIO pins, some speakers attached to the audio jack and a RFID reader plugged into one of the USB ports.An RFID token has been recognisedinputdevinputeventlistens to the attached RFID reader and reads a token.inputdevinputeventsends anrawevent with the payload0123456789(the value read from the token) toplapperkastenvia its pipeplapperkastenrecieves the event, looks it up in~/.config/plapperkasten/events.mapand sees it maps to aload_sourceevent with the payloaduse=Mpdclient,key=Music/Folder/Band/Album.plapperkastenemitsload_sourceto those who listenmpdclientlistens toload_sourceand makes mpd or mopidy (whichever you run on the system) add the folderMusic/Folder/Band/Albumto a new playlist and start playing itA button is pressedinputgpiodlistens to the gpio pins and recieves a signal from pin 12.inputgpiodsends an event12_shorttoplapperkastenvia its pipeplapperkastenrecieves the event, looks it up in~/.config/plapperkasten/events.mapand sees it maps to avolume_increaseeventplapperkastenemitsvolume_increaseto those who listenpwwp(short for PipeWire / Wireplumber) listens tovolume_increaseand callswpctl set-volume @DEFAULT_AUDIO_SINK@ 1%+pwwpsends an eventbeeptoplapperkastenplapperkastenknows from its config that it should pass those events through, so it re-emits the event to whom it may concernsoundeffectslistens to thebeepand produces a beepCore filessrc/plapperkasten/settings/config.yamlcontains default settings - user changes should not go here~/.config/plapperkasten/config.yamlmay contain any of the settings fromsrc/plapperkasten/settings/config.yamland overwrite themsrc/plapperkasten/settings/events.mapcould contain default event mappings but is empty - user changes should not go here~/.config/plapperkasten/events.mapmay contain events defined by the user (seesrc/plapperkasten/keymap.pyfor a description of the form)Creating a pluginA good place to start is to copy the "example" plugin and to take off from there. You will find it insrc/plugins/example.Each plugin lives in its own process. This has some severe implications:You need to use the logging functionality provided byplapperkasten.plkloggingsee logging above.The class is initialised in the main process, this is where it might access data from the configuration or other parts of the programme. It may only store immutable information or fresh copies (as returned byplapperkasten.config) or else the multiprocessing hell will be upon you.If you need to do something (like fetching configuration values or registering for events) on initialisation before the plugin's process was started useon_init.If you need to do something as soon as the new process has started useon_before_run.If you need to do something everyXseconds useon_tickand setself._tick_intervaltoXinon_init.If you need to respond to an event, register for it and write anon_EVENTmethod, e.g., placeregister_for('my_event')inon_initand writeon_my_eventto handle the event.If you need total controll - you usually don't - overwriterun, as insrc/plapperkasten/plugins/inputgpiod/inputgpiod.py.If you need to tidy up after you put the code inon_after_run.If you want to preventplapperkastenfrom shutting down after a period of idleness usesend_busy.If you wantplapperkastento think it might restart its "idle" countdown usesend_idle(but the countdown will only commence if all plugins are idle).If you want to send an event toplapperkastenusesend_to_main.Preventing shutdown during developmentTo avoid shutdown during development setdebugin~/.config/plapperkasten/config.yamltotrue.