From 8069179297671a6343f92eb448c29535616eb447 Mon Sep 17 00:00:00 2001 From: Alvin Schiller <103769832+AlvinSchiller@users.noreply.github.com> Date: Fri, 26 Apr 2024 08:43:50 +0200 Subject: [PATCH] Fix button function call (#2355) * fix: handle args for rotaryEncoder don't pass args to callback if 'None'. reorder args for callback to pass functionCallArgs first, if definied. added functionCallArgs to rotaryEncoder definition, to support other functionCalls then volume. * doc: update docs. removed duplicate / obsolete examples Merged wiki page "Audio-RotaryKnobVolume" * fix: fix tests * Apply suggestions from code review Co-authored-by: s-martin * Update codeblock type Co-authored-by: s-martin --------- Co-authored-by: s-martin --- README.md | 2 +- components/gpio_control/README.md | 76 ++++++++++++++++--- .../gpio_setting_rotary_vol_prevnext.ini | 36 --------- .../example_configs/gpio_settings.ini | 18 ++++- .../gpio_settings_rotary_and_led.ini | 49 ------------ .../gpio_settings_status_led.ini | 7 -- components/gpio_control/function_calls.py | 18 ++--- components/gpio_control/gpio_control.py | 9 ++- .../gpio_control/test/test_gpio_control.py | 49 +++++++++--- misc/sampleconfigs/gpio_settings.ini.sample | 18 ++++- 10 files changed, 148 insertions(+), 134 deletions(-) delete mode 100755 components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini delete mode 100644 components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini delete mode 100644 components/gpio_control/example_configs/gpio_settings_status_led.ini diff --git a/README.md b/README.md index 3b2266d84..ebaada654 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Check out the following references. * [PN532](components/rfid-reader/PN532/README.md) * PC/SC * also [multiple readers](https://github.com/MiczFlor/RPi-Jukebox-RFID/pull/1012#issue-434052529) simultaneously -* [**GPIO** control](components/gpio_control/README.md) for [buttons](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons), [knobs / dials](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Audio-RotaryKnobVolume) and much more to control your Phoniebox via GPIO. +* [**GPIO** control](components/gpio_control/README.md) for [buttons](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Using-GPIO-hardware-buttons) and much more to control your Phoniebox via GPIO. * Control via smooth [**Web App**](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#webapp) running on ajax from your phone, tablet or PC. You can play, upload, move files, assign new RFID cards, control playout, settings, etc. * Support for files with embedded chapters metadata (like m4a) * Customizable poweroff command diff --git a/components/gpio_control/README.md b/components/gpio_control/README.md index 8661e8d27..bae060400 100644 --- a/components/gpio_control/README.md +++ b/components/gpio_control/README.md @@ -24,20 +24,18 @@ Up to now the following input devices are implemented: * **ShutdownButton**: A specialized implementation for a shutdown button with integrated (but optional) LED support. It initializes a shutdown if the button is pressed more than `time_pressed` seconds and a (optional) LED on GPIO `led_pin` is flashing until that time is reached. For additional information, see [extended documentation below](#shutdownbutton). -* **RotaryEncoder**: - Control of a rotary encoder, for example KY040, see also in [Wiki](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/Audio-RotaryKnobVolume). - It can be configured using `pinUp` and `PiNDown` (use GPIO numbers here), `functionCallUp`, `functionCallDown`, and `timeBase` see [extended documentation below](#rotaryencoder). - * **TwoButtonControl**: This Device uses two Buttons and implements a third action if both buttons are pressed together. See [extended documentation below](#twobuttoncontrol). +* **RotaryEncoder**: + Control of a rotary encoder, for example KY040. + It can be configured using `Pin1` and `Pin2` (use GPIO numbers here), `functionCall1`, `functionCall2` see [extended documentation below](#rotaryencoder). + * **StatusLED**: A LED which will light up once the Phoniebox has fully booted up and is ready to be used. For additional information, see [extended documentation below](#statusled). Each section needs to be activated by setting `enabled: True`. -Many example files are located in `~/RPi-Jukebox-RFID/components/gpio_control/example_configs/`. - ## Extended documentation This section provides some extended documentation and guideline. Especially some exemplary configurations are introduced showing how these controls can be set up in the configuration file `~/RPi-Jukebox-RFID/settings/gpio_settings.ini`. @@ -172,19 +170,75 @@ Furthermore, the following settings can be used as described for the [regular bu A RotaryEncoder can be created using an `ini` entry like this: ```bash -[VolumeControl] +[RotaryVolumeControl] enabled: True Type: RotaryEncoder -Pin1: 7 -Pin2: 8 -timeBase: 0.02 +Pin1: 22 +Pin2: 23 +timeBase: 0.1 functionCall1: functionCallVolU functionCall2: functionCallVolD ``` -Pin1 and FunctionCall1 correspond to rotary direction "up", while Pin2 and FunctionCall2 correspond to "down". +* **enabled**: This needs to be `True` for the rotary encoder to work. +* **Pin1**: GPIO number corresponding to rotary direction "clockwise" ('CLK') +* **Pin2**: GPIO number corresponding to rotary direction "counter clockwise" ('DT') +* **functionCall1**: function called for every rotation step corresponding to rotary direction "clockwise". See below for passed arguments. See [function documentation below](#functions). +* **functionCall2**: function called for every rotation step corresponding to rotary direction "counter clockwise". See below for passed arguments. See [function documentation below](#functions). +* **timeBase**: Factor used for calculating the rotation value base on rotation speed, defaults to `0.1`. Use `0` for deactivating rotation speed influence. +Example: + * a single rotation step leads to the value 1 passed to the function. + * steady rotation of two to or more steps, leads to the value 1 for the first call and the value 2 for all further calls. + * speeding up rotation of two to or more steps, leads to the value 1 for the first call, the value 2 for the second, the value 3 for the third and so on. +* **functionCall1Args**: Arguments for `functionCall1`, defaults to `None`. If defined takes precedence over rotation value. Arguments are ignored, if `functionCall1` does not take any. +* **functionCall2Args**: Arguments for `functionCall2`, defaults to `None`. If defined takes precedence over rotation value. Arguments are ignored, if `functionCall1` does not take any. + +To also use the push button of the encoder just a button definition: +```bash +[Mute] +enabled: True +Type: Button +Pin: 27 +functionCall: functionCallVol0 +``` + Note that the old configuration entries PinUp/PinDown and functionCallUp/functionCallDown are deprecated and might stop working in future. + +```bash +[RotarySeekingControl] +enabled: True +Type: RotaryEncoder +Pin1: 22 +Pin2: 23 +timeBase: 0.1 +functionCall1: functionCallPlayerSeekFwd +functionCall1Args: 5 +functionCall2: functionCallPlayerSeekBack +functionCall2Args: 5 +``` + +In this example, the encoder will be used to seek for- and backwards by 5 seconds on every rotation step. The rotation value will **NOT** be used in this case as the function args are defined! + + +#### Circuit diagram +```text + .---------------. .---------------. + | | | | + | CLK |----------------------| GPIO 22 | + | | | | + | DT |----------------------| GPIO 23 | + | | | | + | SW |----------------------| GPIO 27 | + | | | | + | + |----------------------| 3.3V | + | | | | + | GND |----------------------| GND | + | | | | + '---------------' '---------------' + KY-040 Raspberry +``` + ### StatusLED A StatusLED can be created using an `ini` entry like this: diff --git a/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini b/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini deleted file mode 100755 index 3e829d55e..000000000 --- a/components/gpio_control/example_configs/gpio_setting_rotary_vol_prevnext.ini +++ /dev/null @@ -1,36 +0,0 @@ -[DEFAULT] -enabled: True - -[VolumeControl] -enabled: True -Type: RotaryEncoder -PinUp: 7 -PinDown: 8 -timeBase: 0.02 -functionCallDown: functionCallVolD -functionCallUp: functionCallVolU - -[PrevNextControl] -enabled: True -Type: RotaryEncoder -PinUp: 16 -PinDown: 20 -timeBase: 0.02 -functionCallDown: functionCallPlayerPrev -functionCallUp: functionCallPlayerNext - -[Shutdown] -enabled: True -Type: Button -Pin: 17 -pull_up_down: pull_down -edge: raising -functionCall: functionCallShutdown - -[PlayPause] -enabled: True -Type: Button -Pin: 25 -pull_up_down: pull_up -edge: falling -functionCall: functionCallPlayerPause diff --git a/components/gpio_control/example_configs/gpio_settings.ini b/components/gpio_control/example_configs/gpio_settings.ini index 4f84ba01b..a386d6ec6 100755 --- a/components/gpio_control/example_configs/gpio_settings.ini +++ b/components/gpio_control/example_configs/gpio_settings.ini @@ -4,20 +4,28 @@ antibouncehack: False [VolumeControl] enabled: False -Type: TwoButtonControl ;or RotaryEncoder +Type: TwoButtonControl Pin1: 5 Pin2: 6 pull_up_down: pull_up hold_time: 0.3 hold_mode: Repeat -timeBase: 0.1 ;only for RotaryEncoder functionCall1: functionCallVolU functionCall2: functionCallVolD -functionCallTwoButtons: functionCallVol0 ;only for TwoButtonControl +functionCallTwoButtons: functionCallVol0 ;functionCall1Args: 1 ;functionCall2Args: 1 ;functionCallTwoButtonsArgs: x +[RotaryVolumeControl] +enabled: False +Type: RotaryEncoder +Pin1: 22 +Pin2: 23 +timeBase: 0.1 +functionCall1: functionCallVolU +functionCall2: functionCallVolD + [PrevNextControl] enabled: False Type: TwoButtonControl @@ -143,3 +151,7 @@ pull_up_down: pull_up functionCall: functionCallTriggerPlayFolder functionCallArgs: someRelativeFolderName +[StatusLED] +enabled: False +Type: StatusLED +Pin: 14 diff --git a/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini b/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini deleted file mode 100644 index f636adbcf..000000000 --- a/components/gpio_control/example_configs/gpio_settings_rotary_and_led.ini +++ /dev/null @@ -1,49 +0,0 @@ -[DEFAULT] -enabled: True - -[VolumeControl] -enabled: True -Type: RotaryEncoder -PinUp: 17 -PinDown: 22 -pull_up_down: pull_up -timeBase: 0.2 -; only for rotary encoder -functionCallDown: functionCallVolD -functionCallUp: functionCallVolU -functionCallTwoButtons: functionCallStop -; only for TwoButtonControl -functionCallButton: functionCallPlayerPause -; only for RotaryEncoderClickable - -[PrevNextControl] -enabled: True -Type: TwoButtonControl -Pin1: 6 -Pin2: 12 -functionCall1: functionCallPlayerPrev -functionCall2: functionCallPlayerNext -functionCallTwoButtons: functionCallPlayerStop -pull_up_down: pull_up -hold_time: 0.3 -hold_mode: None - - -[Shutdown] -enabled: False -Type: Button -Pin: 3 -functionCall: functionCallShutdown - - -[PlayPause] -enabled: True -Type: Button -Pin: 27 -pull_up_down: pull_up -functionCall: functionCallPlayerPause - -[StatusLED] -enabled: True -Type: MPDStatusLED -Pin: 16 diff --git a/components/gpio_control/example_configs/gpio_settings_status_led.ini b/components/gpio_control/example_configs/gpio_settings_status_led.ini deleted file mode 100644 index 68e6461b2..000000000 --- a/components/gpio_control/example_configs/gpio_settings_status_led.ini +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -enabled: True - -[StatusLED] -enabled: True -Type: StatusLED -Pin: 14 diff --git a/components/gpio_control/function_calls.py b/components/gpio_control/function_calls.py index e293282c0..c3eda5913 100644 --- a/components/gpio_control/function_calls.py +++ b/components/gpio_control/function_calls.py @@ -18,7 +18,7 @@ def __init__(self): def functionCallShutdown(self, *args): function_call("{command} -c=shutdown".format(command=self.playout_control), shell=True) - def functionCallVolU(self, steps=None): + def functionCallVolU(self, steps=None, *args): if steps is None: function_call("{command} -c=volumeup".format(command=self.playout_control), shell=True) else: @@ -26,7 +26,7 @@ def functionCallVolU(self, steps=None): command=self.playout_control), shell=True) - def functionCallVolD(self, steps=None): + def functionCallVolD(self, steps=None, *args): if steps is None: function_call("{command} -c=volumedown".format(command=self.playout_control), shell=True) else: @@ -65,22 +65,22 @@ def functionCallPlayerStop(self, *args): function_call("{command} -c=playerstop".format(command=self.playout_control), shell=True) - def functionCallPlayerSeekFwd(self, seconds=None): + def functionCallPlayerSeekFwd(self, seconds=None, *args): if seconds is None: seconds = 10 function_call("{command} -c=playerseek -v=+{value}".format(command=self.playout_control, value=seconds), shell=True) - def functionCallPlayerSeekBack(self, seconds=None): + def functionCallPlayerSeekBack(self, seconds=None, *args): if seconds is None: seconds = 10 function_call("{command} -c=playerseek -v=-{value}".format(command=self.playout_control, value=seconds), shell=True) - def functionCallPlayerSeekFarFwd(self, seconds=None): + def functionCallPlayerSeekFarFwd(self, seconds=None, *args): if seconds is None: seconds = 60 function_call("{command} -c=playerseek -v=+{value}".format(command=self.playout_control, value=seconds), shell=True) - def functionCallPlayerSeekFarBack(self, seconds=None): + def functionCallPlayerSeekFarBack(self, seconds=None, *args): if seconds is None: seconds = 60 function_call("{command} -c=playerseek -v=-{value}".format(command=self.playout_control, value=seconds), shell=True) @@ -94,15 +94,15 @@ def functionCallPlayerRandomCard(self, *args): def functionCallPlayerRandomFolder(self, *args): function_call("{command} -c=randomfolder".format(command=self.playout_control), shell=True) - def functionCallBluetoothToggle(self, mode=None): + def functionCallBluetoothToggle(self, mode=None, *args): if mode is None: mode = 'toggle' function_call("{command} -c=bluetoothtoggle -v={value}".format(command=self.playout_control, value=mode), shell=True) - def functionCallTriggerPlayCardId(self, cardid): + def functionCallTriggerPlayCardId(self, cardid, *args): function_call("{command} --cardid={value}".format(command=self.rfid_trigger, value = cardid), shell=True) - def functionCallTriggerPlayFolder(self, folder): + def functionCallTriggerPlayFolder(self, folder, *args): function_call("{command} --dir={value}".format(command=self.rfid_trigger, value = folder), shell=True) def getFunctionCall(self, functionName): diff --git a/components/gpio_control/gpio_control.py b/components/gpio_control/gpio_control.py index 45977b89b..3e91df31b 100755 --- a/components/gpio_control/gpio_control.py +++ b/components/gpio_control/gpio_control.py @@ -32,7 +32,10 @@ def getFunctionCall(self, function_name, function_args): try: if function_name is not None and function_name != 'None': functionCall = getattr(self.function_calls, function_name) - return (lambda *args: functionCall(*args, function_args)) + if function_args is not None and function_args != 'None': + return (lambda *args: functionCall(function_args, *args)) + else: + return (lambda *args: functionCall(*args)) except AttributeError: self.logger.error('Could not find FunctionCall {function_name}'.format(function_name=function_name)) return lambda *args: None @@ -77,8 +80,8 @@ def generate_device(self, config, deviceName): elif device_type == 'RotaryEncoder': return RotaryEncoder(config.getint('Pin1'), config.getint('Pin2'), - self.getFunctionCall(config.get('functionCall1'), None), - self.getFunctionCall(config.get('functionCall2'), None), + self.getFunctionCall(config.get('functionCall1'), config.get('functionCall1Args', fallback=None)), + self.getFunctionCall(config.get('functionCall2'), config.get('functionCall2Args', fallback=None)), config.getfloat('timeBase', fallback=0.1), name=deviceName) elif device_type == 'ShutdownButton': diff --git a/components/gpio_control/test/test_gpio_control.py b/components/gpio_control/test/test_gpio_control.py index 4c139b67e..26d6885fa 100644 --- a/components/gpio_control/test/test_gpio_control.py +++ b/components/gpio_control/test/test_gpio_control.py @@ -16,11 +16,11 @@ def gpio_control_class(): class MockFunctionCalls: - def funcTestWithoutParameter(self, *args): - return "funcTestWithoutParameter" + def funcTestWithoutParameter(*args): + return "funcTestWithoutParameter()" - def funcTestWithParameter(self, param1): - return f"funcTestWithParameter({param1})" + def funcTestWithParameter(param1, *args): + return f"funcTestWithParameter({str(param1)})" _gpio_control_class = GPIOControl(MockFunctionCalls) # function_calls will be mocked return _gpio_control_class @@ -226,19 +226,44 @@ def test_generateDevice_TwoButtonControl(self, gpio_control_class): def test_getFunctionCall_None_None(self, gpio_control_class): result = gpio_control_class.getFunctionCall(None, None) - assert result(()) is None + assert result() is None result = gpio_control_class.getFunctionCall('None', None) - assert result(()) is None + assert result() is None result = gpio_control_class.getFunctionCall("nonExisting", None) - assert result(()) is None + assert result() is None - def test_getFunctionCall_withoutParam(self, gpio_control_class): + def test_getFunctionCall_funcWithoutArgs(self, gpio_control_class): result = gpio_control_class.getFunctionCall("funcTestWithoutParameter", None) - assert result(()) == "funcTestWithoutParameter" + assert result() == "funcTestWithoutParameter()" - def test_getFunctionCall_withParam(self, gpio_control_class): - result = gpio_control_class.getFunctionCall("funcTestWithParameter", "param1") - assert result(()) == "funcTestWithParameter(param1)" + def test_getFunctionCall_funcWithoutArgs_withParam(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithoutParameter", None) + assert result("param") == "funcTestWithoutParameter()" + + def test_getFunctionCall_funcWithoutArgs_withFuncArgs(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithoutParameter", "funcArgs") + assert result() == "funcTestWithoutParameter()" + + def test_getFunctionCallfuncWithoutArgs_withFuncArgsAndParam(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithoutParameter", "funcArgs") + assert result("param") == "funcTestWithoutParameter()" + + def test_getFunctionCall_funcWithArgs_withoutFuncArgsAndParam(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithParameter", None) + with pytest.raises(TypeError): + result() + + def test_getFunctionCall_funcWithArgs_withParam(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithParameter", None) + assert result("param") == "funcTestWithParameter(param)" + + def test_getFunctionCall_funcWithArgs_withFuncArgs(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithParameter", "funcArgs") + assert result() == "funcTestWithParameter(funcArgs)" + + def test_getFunctionCall_funcWithArgs_withFuncArgsAndParam(self, gpio_control_class): + result = gpio_control_class.getFunctionCall("funcTestWithParameter", "funcArgs") + assert result("param") == "funcTestWithParameter(funcArgs)" # --------------- diff --git a/misc/sampleconfigs/gpio_settings.ini.sample b/misc/sampleconfigs/gpio_settings.ini.sample index 4f84ba01b..a386d6ec6 100644 --- a/misc/sampleconfigs/gpio_settings.ini.sample +++ b/misc/sampleconfigs/gpio_settings.ini.sample @@ -4,20 +4,28 @@ antibouncehack: False [VolumeControl] enabled: False -Type: TwoButtonControl ;or RotaryEncoder +Type: TwoButtonControl Pin1: 5 Pin2: 6 pull_up_down: pull_up hold_time: 0.3 hold_mode: Repeat -timeBase: 0.1 ;only for RotaryEncoder functionCall1: functionCallVolU functionCall2: functionCallVolD -functionCallTwoButtons: functionCallVol0 ;only for TwoButtonControl +functionCallTwoButtons: functionCallVol0 ;functionCall1Args: 1 ;functionCall2Args: 1 ;functionCallTwoButtonsArgs: x +[RotaryVolumeControl] +enabled: False +Type: RotaryEncoder +Pin1: 22 +Pin2: 23 +timeBase: 0.1 +functionCall1: functionCallVolU +functionCall2: functionCallVolD + [PrevNextControl] enabled: False Type: TwoButtonControl @@ -143,3 +151,7 @@ pull_up_down: pull_up functionCall: functionCallTriggerPlayFolder functionCallArgs: someRelativeFolderName +[StatusLED] +enabled: False +Type: StatusLED +Pin: 14