diff --git a/components/__init__.py b/components/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/components/gpio_control/GPIODevices/__init__.py b/components/gpio_control/GPIODevices/__init__.py new file mode 100644 index 000000000..a53d7ef65 --- /dev/null +++ b/components/gpio_control/GPIODevices/__init__.py @@ -0,0 +1,5 @@ +from .rotary_encoder import RotaryEncoder +from .two_button_control import TwoButtonControl +from .shutdown_button import ShutdownButton +from .simple_button import SimpleButton +from .two_button_control import TwoButtonControl diff --git a/components/gpio_control/GPIODevices/rotary_encoder.py b/components/gpio_control/GPIODevices/rotary_encoder.py new file mode 100755 index 000000000..63ced802e --- /dev/null +++ b/components/gpio_control/GPIODevices/rotary_encoder.py @@ -0,0 +1,148 @@ +#!/usr/bin/python3 +# rotary volume knob +# these files belong all together: +# RPi-Jukebox-RFID/scripts/rotary-encoder.py +# RPi-Jukebox-RFID/scripts/rotary_encoder.py +# RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-rotary-encoder.service.stretch-default.sample +# See wiki for more info: https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki + +import RPi.GPIO as GPIO +from timeit import default_timer as timer +import ctypes +import logging +from signal import pause + +logger = logging.getLogger(__name__) + +c_uint8 = ctypes.c_uint8 + + +class Flags_bits(ctypes.LittleEndianStructure): + _fields_ = [ + ("A", c_uint8, 1), # asByte & 1 + ("B", c_uint8, 1), # asByte & 2 + ] + + +class Flags(ctypes.Union): + _anonymous_ = ("bit",) + _fields_ = [ + ("bit", Flags_bits), + ("asByte", c_uint8) + ] + + +class RotaryEncoder: + # select Enocder state bits + KeyIncr = 0b00000010 + KeyDecr = 0b00000001 + + tblEncoder = [ + 0b00000011, 0b00000111, 0b00010011, 0b00000011, + 0b00001011, 0b00000111, 0b00000011, 0b00000011, + 0b00001011, 0b00000111, 0b00001111, 0b00000011, + 0b00001011, 0b00000011, 0b00001111, 0b00000001, + 0b00010111, 0b00000011, 0b00010011, 0b00000011, + 0b00010111, 0b00011011, 0b00010011, 0b00000011, + 0b00010111, 0b00011011, 0b00000011, 0b00000010] + + def __init__(self, pinA, pinB, functionCallIncr=None, functionCallDecr=None, timeBase=0.1, + name='RotaryEncoder'): + logger.debug('Initialize {name} RotaryEncoder({arg_Apin}, {arg_Bpin})'.format( + arg_Apin=pinA, + arg_Bpin=pinB, + name=name if name is not None else '' + )) + self.name = name + # persist values + self.pinA = pinA + self.pinB = pinB + self.functionCallbackIncr = functionCallIncr + self.functionCallbackDecr = functionCallDecr + self.timeBase = timeBase + + self.encoderState = Flags() # stores the encoder state machine state + self.startTime = timer() + + # setup pins + GPIO.setup(self.pinA, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(self.pinB, GPIO.IN, pull_up_down=GPIO.PUD_UP) + self._is_active = False + self.start() + + def __repr__(self): + repr_str = '<{class_name}{object_name} on pin_a {pin_a},' + \ + ' pin_b {pin_b},timBase {time_base} is_active={is_active}%s>' + return repr_str.format( + class_name=self.__class__.__name__, + object_name=':{}'.format(self.name) if self.name is not None else '', + pin_a=self.pinA, + pin_b=self.pinB, + time_base=self.timeBase, + is_active=self.is_active) + + def start(self): + logger.debug('Start Event Detection on {} and {}'.format(self.pinA, self.pinB)) + self._is_active = True + GPIO.add_event_detect(self.pinA, GPIO.BOTH, callback=self._Callback) + GPIO.add_event_detect(self.pinB, GPIO.BOTH, callback=self._Callback) + + def stop(self): + logger.debug('Stop Event Detection on {} and {}'.format(self.pinA, self.pinB)) + GPIO.remove_event_detect(self.pinA) + GPIO.remove_event_detect(self.pinB) + self._is_active = False + + def __del__(self): + if self.is_active: + self.stop() + + @property + def is_active(self): + return self._is_active + + def _StepSize(self): + end = timer() + duration = end - self.startTime + self.startTime = end + return int(self.timeBase / duration) + 1 + + def _Callback(self, pin): + logger.debug('EventDetection Called') + # construct new state machine input from encoder state and old state + statusA = GPIO.input(self.pinA) + statusB = GPIO.input(self.pinB) + + self.encoderState.A = statusA + self.encoderState.B = statusB + logger.debug('new encoderState: "{}" -> {}, {},{}'.format( + self.encoderState.asByte, + self.tblEncoder[self.encoderState.asByte],statusA,statusB + )) + current_state = self.encoderState.asByte + self.encoderState.asByte = self.tblEncoder[current_state] + + if self.KeyIncr == self.encoderState.asByte: + steps = self._StepSize() + logger.info('{name}: Calling functionIncr {steps}'.format( + name=self.name,steps=steps)) + self.functionCallbackIncr(steps) + elif self.KeyDecr == self.encoderState.asByte: + steps = self._StepSize() + logger.info('{name}: Calling functionDecr {steps}'.format( + name=self.name, steps=steps)) + self.functionCallbackDecr(steps) + else: + logger.debug('Ignoring encoderState: "{}"'.format(self.encoderState.asByte)) + +if __name__ == "__main__": + logging.basicConfig(level='INFO') + GPIO.setmode(GPIO.BCM) + pin1 = int(input('please enter first pin')) + pin2 = int(input('please enter second pin')) + func1 = lambda *args: print('Function Incr executed with {}'.format(args)) + func2 = lambda *args: print('Function Decr executed with {}'.format(args)) + rotarty_encoder = RotaryEncoder(pin1, pin2, func1, func2) + + print('running') + pause() diff --git a/components/gpio_control/GPIODevices/shutdown_button.py b/components/gpio_control/GPIODevices/shutdown_button.py new file mode 100644 index 000000000..5acb437b3 --- /dev/null +++ b/components/gpio_control/GPIODevices/shutdown_button.py @@ -0,0 +1,48 @@ +import math +import time +from RPi import GPIO +import logging +from .simple_button import SimpleButton + + +logger = logging.getLogger(__name__) +class ShutdownButton(SimpleButton): + + def __init__(self, pin, action=lambda *args: None, name=None, bouncetime=500, edge=GPIO.FALLING, + hold_time=.1, led_pin=None, time_pressed=2): + self.led_pin = led_pin + self.time_pressed = 2 + self.iteration_time = .2 + super(ShutdownButton, self).__init__(pin=pin, action=action, name=name, bouncetime=bouncetime, edge=edge, + hold_time=hold_time, hold_repeat=False) + + + # function to provide user feedback (= flashing led) while the shutdown button is pressed + # do not directly call shutdown, in case it was hit accedently + # shutdown is only issued when the button remains pressed for all interations of the for loop + def set_led(self, status): + if self.led_pin is not None: + logger.debug('set LED on pin {} to {}'.format(self.led_pin, status)) + GPIO.output(self.led_pin, status) + else: + logger.debug('cannot set LED to {}: no LED pin defined'.format(status)) + + def callbackFunctionHandler(self, *args): + status = False + n_checks = math.ceil(self.time_pressed/self.iteration_time) + logger.debug('ShutdownButton pressed, ensuring long press for {} seconds, checking each {}s: {}'.format( + self.time_pressed, self.iteration_time, n_checks + )) + for x in range(n_checks): + self.set_led(x & 1) + time.sleep(.2) + status = not self.is_pressed + if status: + break + self.set_led(status) + if not status: + # trippel off period to indicate command accepted + time.sleep(.6) + self.set_led(GPIO.HIGH) + # leave it on for the moment, it will be off when the system is down + self.when_pressed(*args) diff --git a/components/gpio_control/GPIODevices/simple_button.py b/components/gpio_control/GPIODevices/simple_button.py new file mode 100644 index 000000000..d13871033 --- /dev/null +++ b/components/gpio_control/GPIODevices/simple_button.py @@ -0,0 +1,117 @@ +import time +from signal import pause +import logging +import RPi.GPIO as GPIO +GPIO.setmode(GPIO.BCM) + +logger = logging.getLogger(__name__) + +def parse_edge_key(edge): + if edge in [GPIO.FALLING, GPIO.RISING, GPIO.BOTH]: + edge + elif edge.lower() == 'falling': + edge = GPIO.FALLING + elif edge.lower() == 'raising': + edge = GPIO.RISING + elif edge.lower() == 'both': + edge = GPIO.BOTH + else: + raise KeyError('Unknown Edge type {edge}'.format(edge=edge)) + return edge + + + +# This function takes a holding time (fractional seconds), a channel, a GPIO state and an action reference (function). +# It checks if the GPIO is in the state since the function was called. If the state +# changes it return False. If the time is over the function returns True. +def checkGpioStaysInState(holdingTime, gpioChannel, gpioHoldingState): + # Get a reference start time (https://docs.python.org/3/library/time.html#time.perf_counter) + startTime = time.perf_counter() + # Continously check if time is not over + while True: + currentState = GPIO.input(gpioChannel) + if holdingTime < (time.perf_counter() - startTime): + break + # Return if state does not match holding state + if (gpioHoldingState != currentState): + return False + # Else: Wait + + if (gpioHoldingState != currentState): + return False + return True + + +class SimpleButton: + def __init__(self, pin, action=lambda *args: None, name=None, bouncetime=500, edge=GPIO.FALLING, + hold_time=.1, hold_repeat=False): + self.edge = parse_edge_key(edge) + self.hold_time = hold_time + self.hold_repeat = hold_repeat + self.pull_up = True + + self.pin = pin + self.name = name + self.bouncetime = bouncetime + GPIO.setup(self.pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) + self._action = action + GPIO.add_event_detect(self.pin, edge=self.edge, callback=self.callbackFunctionHandler, + bouncetime=self.bouncetime) + + def callbackFunctionHandler(self, *args): + if self.hold_repeat: + return self.holdAndRepeatHandler(*args) + logger.info('{}: executre callback'.format(self.name)) + return self.when_pressed(*args) + + + @property + def when_pressed(self): + logger.info('{}: action'.format(self.name)) + return self._action + + @when_pressed.setter + def when_pressed(self, func): + logger.info('{}: set when_pressed') + self._action = func + + GPIO.remove_event_detect(self.pin) + self._action = func + logger.info('add new action') + GPIO.add_event_detect(self.pin, edge=self.edge, callback=self.callbackFunctionHandler, bouncetime=self.bouncetime) + + + + def set_callbackFunction(self, callbackFunction): + self.when_pressed = callbackFunction + + def holdAndRepeatHandler(self, *args): + logger.info('{}: holdAndRepeatHandler'.format(self.name)) + # Rise volume as requested + self.when_pressed(*args) + # Detect holding of button + while checkGpioStaysInState(self.hold_time, self.pin, GPIO.LOW): + self.when_pressed(*args) + + def __del__(self): + logger.debug('remove event detection') + GPIO.remove_event_detect(self.pin) + + @property + def is_pressed(self): + if self.pull_up: + return not GPIO.input(self.pin) + return GPIO.input(self.pin) + + def __repr__(self): + return ''.format( + self.name, self.pin, self.hold_repeat, self.hold_time + ) + + +if __name__ == "__main__": + print('please enter pin no to test') + pin = int(input()) + func = lambda *args: print('FunctionCall with {}'.format(args)) + btn = SimpleButton(pin=pin, action=func, hold_repeat=True) + pause() diff --git a/components/gpio_control/GPIODevices/two_button_control.py b/components/gpio_control/GPIODevices/two_button_control.py new file mode 100644 index 000000000..159359d47 --- /dev/null +++ b/components/gpio_control/GPIODevices/two_button_control.py @@ -0,0 +1,104 @@ +try: + from simple_button import SimpleButton +except ImportError: + from .simple_button import SimpleButton +from RPi import GPIO +import logging +logger = logging.getLogger(__name__) + +GPIO.setmode(GPIO.BCM) + + +def functionCallTwoButtons(btn1, btn2, functionCall1, functionCall2, functionCallBothPressed=None): + def functionCallTwoButtons(*args): + btn1_pressed = btn1.is_pressed + btn2_pressed = btn2.is_pressed + logger.debug('Btn1 {}, Btn2 {}'.format(btn1_pressed,btn2_pressed)) + if btn1_pressed and btn2_pressed: + logger.debug("Both buttons was pressed") + if functionCallBothPressed is not None: + logger.debug("Both Btns are pressed, action: functionCallBothPressed") + logger.info('functionCallBoth') + return functionCallBothPressed(*args) + logger.debug('No two button pressed action defined') + elif btn1_pressed: + logger.debug("Btn1 is pressed, secondary Btn not pressed, action: functionCall1") + logger.info('functionCall1') + return functionCall1(*args) + elif btn2_pressed: + logger.debug("Btn2 is pressed, action: functionCall2") + logger.info('functionCall2') + return functionCall2(*args) + else: + logger.debug("No Button Pressed: no action") + return None + + return functionCallTwoButtons + + +class TwoButtonControl: + def __init__(self, + bcmPin1, + bcmPin2, + functionCallBtn1, + functionCallBtn2, + functionCallTwoBtns=None, + pull_up=True, + hold_repeat=True, + hold_time=0.3, + name='TwoButtonControl'): + self.functionCallBtn1 = functionCallBtn1 + self.functionCallBtn2 = functionCallBtn2 + self.functionCallTwoBtns = functionCallTwoBtns + self.bcmPin1 = bcmPin1 + self.bcmPin2 = bcmPin2 + self.btn1 = SimpleButton( + pin=bcmPin1, + action=lambda *args: None, + name=name+'Btn2', + bouncetime=500, + edge=GPIO.FALLING, + hold_time=hold_time, + hold_repeat=hold_repeat) + + self.btn2 = SimpleButton(pin=bcmPin2, + action=lambda *args: None, + hold_time=hold_time, + hold_repeat=hold_repeat, + name=name+'Btn2', + bouncetime=500, + edge=GPIO.FALLING) + generatedTwoButtonFunctionCall = functionCallTwoButtons(self.btn1, + self.btn2, + self.functionCallBtn1, + self.functionCallBtn2, + self.functionCallTwoBtns + ) + self.action = generatedTwoButtonFunctionCall + logger.info('adding new action') + self.btn1.when_pressed = generatedTwoButtonFunctionCall + self.btn2.when_pressed = generatedTwoButtonFunctionCall + self.name = name + + def __repr__(self): + two_btns_action = self.functionCallTwoBtns is not None + return ''.format( + name=self.name, + bcmPin1=self.bcmPin1, + bcmPin2=self.bcmPin2, + two_btns_action=two_btns_action + ) + + +if __name__ == "__main__": + logging.basicConfig(level='INFO') + pin1 = int(input('please enter first pin')) + pin2 = int(input('please enter second pin')) + func1 = lambda *args: print('Function Btn1 executed with {}'.format(args)) + func2 = lambda *args: print('Function Btn2 executed with {}'.format(args)) + func3 = lambda *args: print('Function BothBtns executed with {}'.format(args)) + two_btn_control = TwoButtonControl(pin1,pin2,func1,func2,func3) + + print('running') + while True: + pass diff --git a/components/gpio_control/README.md b/components/gpio_control/README.md new file mode 100644 index 000000000..543374970 --- /dev/null +++ b/components/gpio_control/README.md @@ -0,0 +1,42 @@ +# GPIO CONTROL + +This service enables the control of different GPIO input & output devices for controlling the Phoniebox. +It uses to a configuration file to configure the active devices. + +In the following the different devices are described. +Each device can have actions which correspond to function calls. +Up to now the following input devices are implemented: +* **Button**: + A simple button which has a hold and repeat functionality as well as a delayed action. + It can be configured using the keywords: Pin, hold_time, functionCall + +* **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 pinA, pinB, functionCallIncr, functionCallDecr, timeBase=0.1 + +* **TwoButtonControl**: + This Device uses two Buttons and implements a thrid action if both buttons are pressed together. + + + +## How to create and run the service? +* The configuration file needs to be placed in ~/.config/phoniebox/gpio_settings.ini +* The gpio_control.py needs to be started as a service. TODO + +Editing the configuration file and restarting the service will activate the new settings + +how to create a config file +what options can be used for what config vars +how to create tests and run them +milestones you would see for this component +often those who start something new, know best what would be nice to have and in what order. +Because while they build it, they are cutting corners :) +what is it that you think would be nice to have? + +## How to edit configuration files? + +### Which options do I have +The following +## Could this later be switched on and off in the web app? (see milestones below) +A nice diff --git a/components/gpio_control/__init__.py b/components/gpio_control/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/components/gpio_control/example_configs/gpio_settings.ini b/components/gpio_control/example_configs/gpio_settings.ini new file mode 100644 index 000000000..a42926215 --- /dev/null +++ b/components/gpio_control/example_configs/gpio_settings.ini @@ -0,0 +1,75 @@ +[DEFAULT] +enabled: True +[VolumeControl] +enabled: False +Type: RotaryEncoderClickable +PinUp: 16 +PinDown: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +timeBase: 0.1 # only for rotary encoder +functionCallDown: functionCallVolD +functionCallUp: functionCallVolU +functionCallTwoButtons: functionCallVol0 #only for TwoButtonControl +functionCallButton: functionCallPlayerPause # only for RotaryEncoderClickable + +[PrevNextControl] +Type: TwoButtonControl +Pin1: 4 +Pin2: 3 +functionCall1: functionCallPlayerPrev +functionCall2: functionCallPlayerNext +functionCallTwoButtons: None +pull_up: True +hold_time: 0.3 +hold_repeat: False + + +[Shutdown] +enabled: False +Type: Button +Pin: 3 +hold_time: 2 +functionCall: functionCallShutdown + +[Volume0] +enabled: False +Type: Button +Pin: 13 +pull_up: True +functionCall: functionCallVol0 + +[VolumeUp] +enabled: False +Type: Button +Pin: 16 +pull_up: True +hold_time: 0.3 +hold_repeat: True +functionCall: functionCallVolU + +[VolumeDown] +enabled: False +Type: Button +Pin: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +[NextSong] +enabled: False +Type: Button +Pin: 26 +pull_up: True + +[PrevSong] +enabled: False +Type: Button +Pin: 20 +pull_up: True + +[Halt] +enabled: False +Type: Button +Pin: 21 +pull_up: True diff --git a/components/gpio_control/example_configs/gpio_settings_test.ini b/components/gpio_control/example_configs/gpio_settings_test.ini new file mode 100644 index 000000000..91659e03a --- /dev/null +++ b/components/gpio_control/example_configs/gpio_settings_test.ini @@ -0,0 +1,82 @@ +[DEFAULT] +enabled: True + +#The following Types exist: +# RotaryEncoder +# TwoButtonControl +# SimpleButton = Button +[VolumeControl] +enabled: True +Type: RotaryEncoder +PinUp: 16 +PinDown: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +timeBase: 0.1 +#timeBase only for rotary encoder +functionCallDown: functionCallVolD +functionCallUp: functionCallVolU +functionCallTwoButtons: functionCallVol0 +# functionCallTwoButtons only for TwoButtonControl +#functionCallButton: functionCallPlayerPause ; only for RotaryEncoderClickable + +[PrevNextControl] +Type: TwoButtonControl +Pin1: 4 +Pin2: 3 +functionCall1: functionCallPlayerPrev +functionCall2: functionCallPlayerNext +functionCallTwoButtons: None +pull_up: True +hold_time: 0.3 +hold_repeat: False + + +[Shutdown] +enabled: True +Type: Button +Pin: 3 +hold_time: 2 +functionCall: functionCallShutdown + +[Volume0] +enabled: False +Type: Button +Pin: 13 +pull_up: True +functionCall: functionCallVol0 + +[VolumeUp] +enabled: False +Type: Button +Pin: 16 +pull_up: True +hold_time: 0.3 +hold_repeat: True +functionCall: functionCallVolU + +[VolumeDown] +enabled: False +Type: Button +Pin: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +[NextSong] +enabled: False +Type: Button +Pin: 26 +pull_up: True + +[PrevSong] +enabled: False +Type: Button +Pin: 20 +pull_up: True + +[Halt] +enabled: False +Type: Button +Pin: 21 +pull_up: True diff --git a/components/gpio_control/example_configs/phoniebox_gpio_control.service b/components/gpio_control/example_configs/phoniebox_gpio_control.service new file mode 100644 index 000000000..b468e6056 --- /dev/null +++ b/components/gpio_control/example_configs/phoniebox_gpio_control.service @@ -0,0 +1,18 @@ +[Unit] +Description=Phoniebox GPIO Control Service +After=network.target iptables.service firewalld.service + +[Service] +Type=simple +User=pi +Group=pi +WorkingDirectory=/home/pi/RPi-Jukebox-RFID/components/gpio_control/ +ExecStart=/home/pi/RPi-Jukebox-RFID/components/gpio_control/gpio_control.py +SyslogIdentifier=PhonieboxGPIOControl +StandardOutput=syslog +StandardError=syslog +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/components/gpio_control/function_calls.py b/components/gpio_control/function_calls.py new file mode 100644 index 000000000..6bf5c29b8 --- /dev/null +++ b/components/gpio_control/function_calls.py @@ -0,0 +1,72 @@ +import logging +import sys +from subprocess import Popen as function_call + +logger = logging.getLogger(__name__) + +playout_control = "../../scripts/playout_controls.sh" + +def functionCallShutdown(*args): + function_call("{command} -c=shutdown".format(command=playout_control), shell=True) + + +def functionCallVolU(steps=None): + if steps is None: + function_call("{command} -c=volumeup".format(command=playout_control), shell=True) + else: + function_call("{command} -c=volumeup -v={steps}".format(steps=steps, + command=playout_control), + shell=True) + + +def functionCallVolD(steps=None): + if steps is None: + function_call("{command} -c=volumedown".format(command=playout_control), shell=True) + else: + function_call("{command} -c=volumedown -v={steps}".format(steps=steps, + command=playout_control), + shell=True) + + +def functionCallVol0(*args): + function_call("{command} -c=mute".format(command=playout_control), shell=True) + + +def functionCallPlayerNext(*args): + function_call("{command} -c=playernext".format(command=playout_control), shell=True) + + +def functionCallPlayerPrev(*args): + function_call("{command} -c=playerprev".format(command=playout_control), shell=True) + + +def functionCallPlayerPauseForce(*args): + function_call("{command} -c=playerpauseforce".format(command=playout_control), shell=True) + + +def functionCallPlayerPause(*args): + function_call("{command} -c=playerpause".format(command=playout_control), shell=True) + + +def functionCallRecordStart(*args): + function_call("{command} -c=recordstart".format(command=playout_control), shell=True) + + +def functionCallRecordStop(*args): + function_call("{command} -c=recordstop".format(command=playout_control), shell=True) + + +def functionCallRecordPlayLatest(*args): + function_call("{command} -c=recordplaylatest".format(command=playout_control), shell=True) + + +def functionCallToggleWifi(*args): + function_call("{command} -c=togglewifi".format(command=playout_control), shell=True) + + + +def getFunctionCall(functionName): + logger.error('Get FunctionCall: {} {}'.format(functionName,functionName in locals())) + getattr(sys.modules[__name__], str) + return locals().get(functionName, None) + diff --git a/components/gpio_control/gpio_control.py b/components/gpio_control/gpio_control.py new file mode 100755 index 000000000..94c8b65eb --- /dev/null +++ b/components/gpio_control/gpio_control.py @@ -0,0 +1,114 @@ +#! /usr/bin/python3 +import configparser +import os +import logging + +from GPIODevices import * +import function_calls +from signal import pause + +from RPi import GPIO + +GPIO.setmode(GPIO.BCM) + +logger = logging.getLogger(__name__) + +def getFunctionCall(function_name): + try: + if function_name != 'None': + return getattr(function_calls, function_name) + except AttributeError: + logger.error('Could not find FunctionCall {function_name}'.format(function_name=function_name)) + return lambda *args: None + + +class VolumeControl: + def __new__(self, config): + if config.get('Type') == 'TwoButtonControl': + logger.info('VolumeControl as TwoButtonControl') + return TwoButtonControl( + config.getint('pinUp'), + config.getint('pinDown'), + getFunctionCall(config.get('functionCallUp')), + getFunctionCall(config.get('functionCallDown')), + functionCallTwoBtns=getFunctionCall(config.get('functionCallTwoButtons')), + pull_up=config.getboolean('pull_up', fallback=True), + hold_repeat=config.getboolean('hold_repeat', fallback=True), + hold_time=config.getfloat('hold_time', fallback=0.3), + name='VolumeControl' + ) + elif config.get('Type') == 'RotaryEncoder': + return RotaryEncoder( + config.getint('pinUp'), + config.getint('pinDown'), + getFunctionCall(config.get('functionCallUp')), + getFunctionCall(config.get('functionCallDown')), + config.getfloat('timeBase',fallback=0.1), + name='RotaryVolumeControl') + + +def generate_device(config, deviceName): + print(deviceName) + device_type = config.get('Type') + if deviceName.lower() == 'VolumeControl'.lower(): + return VolumeControl(config) + elif device_type == 'TwoButtonControl': + logger.info('adding TwoButtonControl') + return TwoButtonControl( + config.getint('Pin1'), + config.getint('Pin2'), + getFunctionCall(config.get('functionCall1')), + getFunctionCall(config.get('functionCall2')), + functionCallTwoBtns=getFunctionCall(config.get('functionCallTwoButtons')), + pull_up=config.getboolean('pull_up',fallback=True), + hold_repeat=config.getboolean('hold_repeat', False), + hold_time=config.getfloat('hold_time',fallback=0.3), + name=deviceName) + elif device_type in ('Button', 'SimpleButton'): + return SimpleButton(config.getint('Pin'), + action=getFunctionCall(config.get('functionCall')), + name=deviceName, + bouncetime=config.getint('bouncetime', fallback=500), + edge=config.get('edge',fallback='FALLING'), + hold_repeat=config.getboolean('hold_repeat', False), + hold_time=config.getfloat('hold_time',fallback=0.3)) + logger.warning('cannot find {}'.format(deviceName)) + return None + + +def get_all_devices(config): + devices = [] + logger.info(config.sections()) + for section in config.sections(): + if config.getboolean(section, 'enabled', fallback=False): + logger.info('adding GPIO-Device, {}'.format(section)) + device = generate_device(config[section], section) + if device is not None: + devices.append(device) + else: + logger.warning('Could not add Device {} with {}'.format(section, config.items(section))) + else: + logger.info('Device {} not enabled'.format(section)) + for dev in devices: + print(dev) + return devices + + +if __name__ == "__main__": + + logging.basicConfig(level='INFO') + logger = logging.getLogger() + logger.setLevel('INFO') + + config = configparser.ConfigParser() + config_path = os.path.expanduser('~/.config/phoniebox/gpio_settings.ini') + config.read(config_path) + + devices = get_all_devices(config) + print(devices) + logger.info('Ready for taking actions') + try: + pause() + except KeyboardInterrupt: + pass + logger.info('Exiting GPIO Control') diff --git a/components/gpio_control/install.sh b/components/gpio_control/install.sh new file mode 100755 index 000000000..7553e9785 --- /dev/null +++ b/components/gpio_control/install.sh @@ -0,0 +1,57 @@ + +echo 'Installing GPIO_Control service' +echo + +FILE=$HOME/.config/phoniebox/gpio_settings.ini +if test -f "$FILE"; then + echo "$FILE exist" + echo "Script will not install a configuration" +else + unset options i + while IFS= read -r -d $'\0' f; do + options[i++]="$f" + done < <(find ./example_configs/ -maxdepth 1 -type f -name "*.ini" -print0 ) + + + echo 'Please choose a default configuration' + select opt in "${options[@]}" "Stop the script"; do + case $opt in + *.ini) + echo "Configuration file $opt selected" + echo "Copy file to $FILE" + echo cp -v $opt $FILE + cp -v $opt $FILE + break + ;; + "Stop the script") + echo "You chose to stop" + break + ;; + *) + echo "This is not a number" + ;; + esac + done + +fi +echo +echo 'Installing GPIO_Control service, this will require to enter your password up to 3 times to enable the service' +read -p "Press enter to continue " -n 1 -r +SERVICE_FILE=/etc/systemd/system/phoniebox_gpio_control.service +if test -f "$SERVICE_FILE"; then + echo "$SERVICE_FILE exists."; + read -p "Press enter to continue " -n 1 -r; + #echo "systemctl daemon-reload" + #systemctl daemon-reload +else + sudo cp -v ./example_configs/phoniebox_gpio_control.service /etc/systemd/system/ + echo "systemctl start phoniebox_gpio_control.service" + systemctl start phoniebox_gpio_control.service + echo "systemctl enable phoniebox_gpio_control.service" + systemctl enable phoniebox_gpio_control.service +fi +systemctl status phoniebox_gpio_control.service + + + + diff --git a/components/gpio_control/test/__init__.py b/components/gpio_control/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/components/gpio_control/test/conftest.py b/components/gpio_control/test/conftest.py new file mode 100644 index 000000000..94c1cc67d --- /dev/null +++ b/components/gpio_control/test/conftest.py @@ -0,0 +1,16 @@ +from mock import MagicMock, patch + + +MockRPi = MagicMock() +modules = { + "RPi": MockRPi, + "RPi.GPIO": MockRPi.GPIO, +} + +MockRPi.GPIO.RISING = 31 +MockRPi.GPIO.FALLING = 32 +MockRPi.GPIO.BOTH = 33 +MockRPi.GPIO.HIGH = 1 +MockRPi.GPIO.LOW = 0 +patcher = patch.dict("sys.modules", modules) +patcher.start() diff --git a/components/gpio_control/test/gpio_settings_test.ini b/components/gpio_control/test/gpio_settings_test.ini new file mode 100644 index 000000000..91659e03a --- /dev/null +++ b/components/gpio_control/test/gpio_settings_test.ini @@ -0,0 +1,82 @@ +[DEFAULT] +enabled: True + +#The following Types exist: +# RotaryEncoder +# TwoButtonControl +# SimpleButton = Button +[VolumeControl] +enabled: True +Type: RotaryEncoder +PinUp: 16 +PinDown: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +timeBase: 0.1 +#timeBase only for rotary encoder +functionCallDown: functionCallVolD +functionCallUp: functionCallVolU +functionCallTwoButtons: functionCallVol0 +# functionCallTwoButtons only for TwoButtonControl +#functionCallButton: functionCallPlayerPause ; only for RotaryEncoderClickable + +[PrevNextControl] +Type: TwoButtonControl +Pin1: 4 +Pin2: 3 +functionCall1: functionCallPlayerPrev +functionCall2: functionCallPlayerNext +functionCallTwoButtons: None +pull_up: True +hold_time: 0.3 +hold_repeat: False + + +[Shutdown] +enabled: True +Type: Button +Pin: 3 +hold_time: 2 +functionCall: functionCallShutdown + +[Volume0] +enabled: False +Type: Button +Pin: 13 +pull_up: True +functionCall: functionCallVol0 + +[VolumeUp] +enabled: False +Type: Button +Pin: 16 +pull_up: True +hold_time: 0.3 +hold_repeat: True +functionCall: functionCallVolU + +[VolumeDown] +enabled: False +Type: Button +Pin: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +[NextSong] +enabled: False +Type: Button +Pin: 26 +pull_up: True + +[PrevSong] +enabled: False +Type: Button +Pin: 20 +pull_up: True + +[Halt] +enabled: False +Type: Button +Pin: 21 +pull_up: True diff --git a/components/gpio_control/test/test_RotaryEncoder.py b/components/gpio_control/test/test_RotaryEncoder.py new file mode 100644 index 000000000..bae83d264 --- /dev/null +++ b/components/gpio_control/test/test_RotaryEncoder.py @@ -0,0 +1,82 @@ +import pytest +from mock import MagicMock + +from ..GPIODevices.rotary_encoder import RotaryEncoder +from RPi import GPIO + + +pinA = 1 +pinB = 2 + +mockCallDecr = MagicMock() +mockCallIncr = MagicMock() + + +@pytest.fixture +def functionCallIncr(): + return mockCallIncr + + +@pytest.fixture +def functionCallDecr(): + return mockCallDecr + + +@pytest.fixture +def rotaryEncoder(functionCallIncr, functionCallDecr): + mockCallDecr.reset_mock() + mockCallIncr.reset_mock() + return RotaryEncoder(pinA, pinB, + functionCallIncr=functionCallIncr, + functionCallDecr=functionCallDecr, + timeBase=0.1, + name='MockedGPIOInteraction') + + +# +# @patch("RPi", autospec=True) +# @patch("RPi.GPIO", autospec=True) +class TestRotaryEncoder: + + def test_init(self, functionCallIncr, functionCallDecr): + RotaryEncoder(pinA, pinB, functionCallIncr=functionCallIncr, functionCallDecr=functionCallDecr, timeBase=0.1, + name=None) + + def test_repr(self, rotaryEncoder): + expected = "" + assert repr(rotaryEncoder) == expected + + def test_start_stop(self, rotaryEncoder): + calls = GPIO.add_event_detect.call_count + assert rotaryEncoder.is_active is True + GPIO.remove_event_detect.assert_not_called() + rotaryEncoder.stop() + assert GPIO.remove_event_detect.call_count == 2 + assert rotaryEncoder.is_active is False + + def test_Callback_Decr(self, rotaryEncoder): + values = [{pinA: False, pinB: True}, {pinA: True, pinB: True}, {pinA: True, pinB: False}, + {pinA: False, pinB: False}] + for i in range(6): + def side_effect(arg): + return values[i % len(values)][arg] + + GPIO.input.side_effect = side_effect + rotaryEncoder._Callback(1) + mockCallDecr.assert_called_once() + mockCallIncr.assert_not_called() + + def test_Callback_Incr(self, rotaryEncoder): + values = [{pinA: False, pinB: True}, + {pinA: False, pinB: False}, + {pinA: True, pinB: False}, + {pinA: True, pinB: True}] + for i in range(7): + def side_effect(arg): + return values[i % len(values)][arg] + + GPIO.input.side_effect = side_effect + rotaryEncoder._Callback(1) + print(mockCallDecr.call_count, mockCallIncr.call_count) + mockCallDecr.assert_not_called() + mockCallIncr.assert_called_once() diff --git a/components/gpio_control/test/test_SimpleButton.py b/components/gpio_control/test/test_SimpleButton.py new file mode 100644 index 000000000..7e6aefa04 --- /dev/null +++ b/components/gpio_control/test/test_SimpleButton.py @@ -0,0 +1,43 @@ +from mock import patch, MagicMock +import pytest + +import RPi.GPIO as GPIO +from ..GPIODevices.simple_button import SimpleButton + +pin = 1 +mockedAction = MagicMock() + + +@pytest.fixture +def simple_button(): + return SimpleButton(pin, action=mockedAction, name='TestButton', + bouncetime=500, edge=GPIO.FALLING) + + +class TestButton: + mockedFunction = MagicMock() + + def test_init(self): + SimpleButton(pin, action=self.mockedFunction, name='TestButton', + bouncetime=500, edge=GPIO.FALLING) + + def test_callback(self, simple_button): + simple_button.callbackFunctionHandler()(simple_button.pin) + mockedAction.asser_called_once() + + def test_change_when_pressed(self, simple_button): + mockedAction.asser_called_once() + newMockedAction = MagicMock() + simple_button.when_pressed = newMockedAction + simple_button.callbackFunctionHandler()(simple_button.pin) + newMockedAction.asser_called_once() + mockedAction.asser_called_once() + + def test_hold(self, simple_button): + GPIO.LOW = 0 + GPIO.input.side_effect = [False, False, False, True] + simple_button.hold_time = 0 + simple_button.hold_repeat = True + calls = mockedAction.call_count + simple_button.callbackFunctionHandler(simple_button.pin) + assert mockedAction.call_count - calls == 4 diff --git a/components/gpio_control/test/test_TwoButtonControl.py b/components/gpio_control/test/test_TwoButtonControl.py new file mode 100644 index 000000000..e45230584 --- /dev/null +++ b/components/gpio_control/test/test_TwoButtonControl.py @@ -0,0 +1,155 @@ +import mock +import pytest +from mock import MagicMock + +from ..GPIODevices import two_button_control +from ..GPIODevices.two_button_control import functionCallTwoButtons, TwoButtonControl + + +@pytest.fixture +def btn1Mock(): + return mock.MagicMock() + + +@pytest.fixture +def btn2Mock(): + return mock.MagicMock() + + +@pytest.fixture +def functionCall1Mock(): + return mock.MagicMock() + + +@pytest.fixture +def functionCall2Mock(): + return mock.MagicMock() + + +@pytest.fixture +def functionCallBothPressedMock(): + return mock.MagicMock() + + +def test_functionCallTwoButtonsOnlyBtn1Pressed(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock, + functionCallBothPressedMock): + btn1Mock.is_pressed = True + btn2Mock.is_pressed = False + func = functionCallTwoButtons(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock, + functionCallBothPressed=functionCallBothPressedMock) + func() + functionCall1Mock.assert_called_once_with() + functionCall2Mock.assert_not_called() + functionCallBothPressedMock.assert_not_called() + + +def test_functionCallTwoButtonsBothBtnsPressedFunctionCallBothPressedExists(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock, + functionCallBothPressedMock): + btn1Mock.is_pressed = True + btn2Mock.is_pressed = True + func = functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, + functionCallBothPressed=functionCallBothPressedMock) + func() + functionCall1Mock.assert_not_called() + functionCall2Mock.assert_not_called() + functionCallBothPressedMock.assert_called_once_with() + + +def test_functionCallTwoButtonsBothBtnsPressedFunctionCallBothPressedIsNone(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock): + btn1Mock.is_pressed = True + btn2Mock.is_pressed = True + func = functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, + functionCallBothPressed=None) + func() + functionCall1Mock.assert_not_called() + functionCall2Mock.assert_not_called() + + +mockedFunction1 = MagicMock() +mockedFunction2 = MagicMock() +mockedFunction3 = MagicMock() + + +@pytest.fixture +def two_button_controller(): + mockedFunction1.reset_mock() + mockedFunction2.reset_mock() + mockedFunction3.reset_mock() + return TwoButtonControl(bcmPin1=1, + bcmPin2=2, + functionCallBtn1=mockedFunction1, + functionCallBtn2=mockedFunction2, + functionCallTwoBtns=mockedFunction3, + pull_up=True, + hold_repeat=False, + hold_time=0.3, + name='TwoButtonControl') + + +class TestTwoButtonControl: + def test_init(self): + TwoButtonControl(bcmPin1=1, + bcmPin2=2, + functionCallBtn1=mockedFunction1, + functionCallBtn2=mockedFunction2, + functionCallTwoBtns=mockedFunction3, + pull_up=True, + hold_repeat=False, + hold_time=0.3, + name='TwoButtonControl') + + def test_btn1_pressed(self, two_button_controller): + pinA = two_button_controller.bcmPin1 + pinB = two_button_controller.bcmPin2 + def func(pin): + values = {pinA: False, pinB: True} + if pin in values: + return values[pin] + else: + print('Cannot find pin {} in values: {}'.format(pin,values)) + return None + two_button_control.GPIO.input.side_effect = func + two_button_controller.action() + mockedFunction1.assert_called_once() + mockedFunction2.assert_not_called() + mockedFunction3.assert_not_called() + two_button_controller.action() + assert mockedFunction1.call_count == 2 + + def test_btn2_pressed(self, two_button_controller): + pinA = two_button_controller.bcmPin1 + pinB = two_button_controller.bcmPin2 + two_button_control.GPIO.input.side_effect = lambda pin: {pinA: True, pinB: False}[pin] + two_button_controller.action() + mockedFunction1.assert_not_called() + mockedFunction2.assert_called_once() + mockedFunction3.assert_not_called() + two_button_controller.action() + assert mockedFunction2.call_count == 2 + + def test_btn1_and_btn2_pressed(self, two_button_controller): + pinA = two_button_controller.bcmPin1 + pinB = two_button_controller.bcmPin2 + two_button_control.GPIO.input.side_effect = lambda pin: {pinA: False, pinB: False}[pin] + two_button_controller.action() + mockedFunction1.assert_not_called() + mockedFunction2.assert_not_called() + mockedFunction3.assert_called_once() + two_button_controller.action() + assert mockedFunction3.call_count == 2 + + def test_repr(self, two_button_controller): + expected = "" + assert repr(two_button_controller) == expected diff --git a/components/gpio_control/test/test_gpio_control.py b/components/gpio_control/test/test_gpio_control.py new file mode 100644 index 000000000..359d660b0 --- /dev/null +++ b/components/gpio_control/test/test_gpio_control.py @@ -0,0 +1,35 @@ +import configparser +import logging + +from mock import patch, MagicMock +from components.gpio_control.gpio_control import get_all_devices + +# def test_functionCallTwoButtonsOnlyBtn2Pressed(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, +# functionCallBothPressedMock): +# btn1Mock.is_pressed = False +# btn2Mock.is_pressed = True +# func = functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, +# functionCallBothPressed=functionCallBothPressedMock) +# func() +# functionCall1Mock.assert_not_called() +# functionCall2Mock.assert_called_once_with() +# functionCallBothPressedMock.assert_not_called() + +mockedFunction1 = MagicMock() +mockedFunction2 = MagicMock() +mockedFunction3 = MagicMock() + +mockedFunction1.side_effect = lambda *args: print('MockFunction1 called') +mockedFunction2.side_effect = lambda *args: print('MockFunction2 called') +mockedFunction3.side_effect = lambda *args: print('MockFunction3 called') + +logging.basicConfig(level='DEBUG') + + +def testMain(): + config = configparser.ConfigParser() + config.read('./gpio_settings_test.ini') + devices = get_all_devices(config) + print(devices) + pass + diff --git a/components/gpio_control/test/test_shutdown_button.py b/components/gpio_control/test/test_shutdown_button.py new file mode 100644 index 000000000..b76be7ffa --- /dev/null +++ b/components/gpio_control/test/test_shutdown_button.py @@ -0,0 +1,43 @@ +import pytest + +from mock import Mock, patch +import mock + +from ..GPIODevices.shutdown_button import ShutdownButton, GPIO + +mock_time = Mock() + +mocked_function = Mock() + + +@pytest.fixture +def shutdown_button(): + mocked_function.reset_mock() + return ShutdownButton(pin=1, action=mocked_function) + + +class TestShutdownButton(): + def test_init(self): + ShutdownButton(pin=1) + + @patch('time.sleep', mock_time) + def test_action(self, shutdown_button): + for i in range(9): + GPIO.input.reset_mock() + GPIO.input.side_effect = i*[0]+[1] + shutdown_button.callbackFunctionHandler() + assert GPIO.input.call_count == i+1 + mocked_function.assert_not_called() + + + @patch('time.sleep', mock_time) + def test_action2(self, shutdown_button): + GPIO.input.side_effect = lambda *args: 1 + shutdown_button.callbackFunctionHandler() + mocked_function.assert_not_called() + + @patch('time.sleep', mock_time) + def test_action3(self, shutdown_button): + GPIO.input.side_effect = lambda *args: 0 + shutdown_button.callbackFunctionHandler() + mocked_function.assert_called_once() diff --git a/requirements.txt b/requirements.txt index 167bfb7fb..08da60dea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,17 +22,17 @@ pi-rc522 # We'll use pytest to run our tests; this isn't really necessary to run the code, but it is to run # the tests. With this here, you can run the tests with `py.test` from the base directory. -# pytest +pytest # Makes it so that pytest can handle the code structure we use, with src/main/python, and src/test. -# pytest-pythonpath +pytest-pythonpath # Allows generation of coverage reports with pytest. -# pytest-cov +pytest-cov # Allows marking tests as flaky, to be rerun if they fail # flaky # Allows codecov to generate coverage reports -# coverage +coverage # codecov diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/rotary-encoder.py b/scripts/rotary-encoder.py deleted file mode 100755 index 9765da7d7..000000000 --- a/scripts/rotary-encoder.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/python3 -# rotary volume and track knob -# This script is compatible with any I2S DAC e.g. from Hifiberry, Justboom, ES9023, PCM5102A -# Please combine with corresponding gpio button script, which handles the button functionality of the encoder -# RPi-Jukebox-RFID/misc/sampleconfigs/gpio-buttons.py.rotaryencoder.sample - -# these files belong all together: -# RPi-Jukebox-RFID/scripts/rotary-encoder.py -# RPi-Jukebox-RFID/scripts/rotary_encoder_base.py -# RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-rotary-encoder.service.stretch-default.sample -# RPi-Jukebox-RFID/misc/sampleconfigs/gpio-buttons.py.rotaryencoder.sample -# See wiki for more info: https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki - -# -# circuit diagram for one of two possible encoders (volume), use GPIOs from code below for the tracks -# (capacitors are optionally) -# KY-040 is just one example, typically the pins are named A and B instead of Clock and Data -# -# .---------------. .---------------. -# | | | | -# | B / DT |------o---------------| GPIO 5 | -# | | | | | -# | A / CLK |------)----o----------| GPIO 6 | -# | | | | | | -# | SW |------)----)----------| GPIO 3 | -# | | | | | | -# | + |------)----)----------| 3.3V | -# | | | | | | -# | GND |------)----)----------| GND | -# | | | | | | -# '---------------' | | '---------------' -# KY-040 | | Raspberry -# | | -# --- --- -# 100nF --- --- 100nF -# | | -# | | -# | | -# === === -# GND GND -# - -import RPi.GPIO as GPIO -from rotary_encoder_base import RotaryEncoder as enc -import sys -from signal import pause -from subprocess import check_call - - -def rotaryChangeCWVol(steps): - check_call("./scripts/playout_controls.sh -c=volumeup -v="+str(steps), shell=True) - - -def rotaryChangeCCWVol(steps): - check_call("./scripts/playout_controls.sh -c=volumedown -v="+str(steps), shell=True) - - -def rotaryChangeCWTrack(steps): - check_call("./scripts/playout_controls.sh -c=playernext", shell=True) - - -def rotaryChangeCCWTrack(steps): - check_call("./scripts/playout_controls.sh -c=playerprev", shell=True) - - -APinVol = 6 -BPinVol = 5 - -APinTrack = 23 -BPinTrack = 22 - -GPIO.setmode(GPIO.BCM) - -if __name__ == "__main__": - - try: - encVol = enc(APinVol, BPinVol, rotaryChangeCWVol, rotaryChangeCCWVol, 0.2) - encTrack = enc(APinTrack, BPinTrack, rotaryChangeCWTrack, rotaryChangeCCWTrack, 0.05) - - encVol.start() - encTrack.start() - pause() - except KeyboardInterrupt: - encVol.stop() - encTrack.stop() - GPIO.cleanup() - print("\nExiting rotary encoder decoder\n") - # exit the application - - sys.exit(0) diff --git a/scripts/rotary_encoder_base.py b/scripts/rotary_encoder_base.py deleted file mode 100755 index 6fc8f512d..000000000 --- a/scripts/rotary_encoder_base.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/python3 -# rotary volume knob -# these files belong all together: -# RPi-Jukebox-RFID/scripts/rotary-encoder.py -# RPi-Jukebox-RFID/scripts/rotary_encoder_base.py -# RPi-Jukebox-RFID/misc/sampleconfigs/phoniebox-rotary-encoder.service.stretch-default.sample -# See wiki for more info: https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki - -import RPi.GPIO as GPIO -from timeit import default_timer as timer -import ctypes - -c_uint8 = ctypes.c_uint8 - - -class Flags_bits(ctypes.LittleEndianStructure): - _fields_ = [ - ("A", c_uint8, 1), # asByte & 1 - ("B", c_uint8, 1), # asByte & 2 - ] - - -class Flags(ctypes.Union): - _anonymous_ = ("bit",) - _fields_ = [ - ("bit", Flags_bits), - ("asByte", c_uint8) - ] - - -class RotaryEncoder: - - # select Enocder state bits - KeyIncr = 0b00000010 - KeyDecr = 0b00000001 - - tblEncoder = [ - 0b00000011, 0b00000111, 0b00010011, 0b00000011, - 0b00001011, 0b00000111, 0b00000011, 0b00000011, - 0b00001011, 0b00000111, 0b00001111, 0b00000011, - 0b00001011, 0b00000011, 0b00001111, 0b00000001, - 0b00010111, 0b00000011, 0b00010011, 0b00000011, - 0b00010111, 0b00011011, 0b00010011, 0b00000011, - 0b00010111, 0b00011011, 0b00000011, 0b00000010] - - def __init__(self, arg_Apin, arg_Bpin, arg_rotaryCallbackCW=None, arg_rotaryCallbackCCW=None, arg_TimeBase=0.1): - # persist values - self.Apin = arg_Apin - self.Bpin = arg_Bpin - self.rotaryCallbackCW = arg_rotaryCallbackCW - self.rotaryCallbackCCW = arg_rotaryCallbackCCW - self.TimeBase = arg_TimeBase - - self.EncoderState = Flags() # stores the encoder state machine state - self.StartTime = timer() - - # setup pins - GPIO.setup(self.Apin, GPIO.IN, pull_up_down=GPIO.PUD_UP) - GPIO.setup(self.Bpin, GPIO.IN, pull_up_down=GPIO.PUD_UP) - - def start(self): - GPIO.add_event_detect(self.Apin, GPIO.BOTH, callback=self._Callback) - GPIO.add_event_detect(self.Bpin, GPIO.BOTH, callback=self._Callback) - - def stop(self): - GPIO.remove_event_detect(self.Apin) - GPIO.remove_event_detect(self.Bpin) - - def _StepSize(self): - end = timer() - duration = end - self.StartTime - self.StartTime = end - return int(self.TimeBase/duration) + 1 - - def _Callback(self, pin): - # construct new state machine input from encoder state and old state - self.EncoderState.A = GPIO.input(self.Apin) - self.EncoderState.B = GPIO.input(self.Bpin) - self.EncoderState.asByte = self.tblEncoder[self.EncoderState.asByte] - - if self.KeyIncr == self.EncoderState.asByte: - self.rotaryCallbackCW(self._StepSize()) - - elif self.KeyDecr == self.EncoderState.asByte: - self.rotaryCallbackCCW(self._StepSize()) diff --git a/scripts/test/mockedGPIO.py b/scripts/test/mockedGPIO.py new file mode 100644 index 000000000..e4ff6a153 --- /dev/null +++ b/scripts/test/mockedGPIO.py @@ -0,0 +1,17 @@ +from mock import MagicMock, patch + +MockRPi = MagicMock() +modules = { + "RPi": MockRPi, + "RPi.GPIO": MockRPi.GPIO, +} + +MockRPi.GPIO.RISING = 31 +MockRPi.GPIO.FALLING = 32 +MockRPi.GPIO.BOTH = 33 +MockRPi.GPIO.HIGH = 1 +MockRPi.GPIO.LOW = 0 +patcher = patch.dict("sys.modules", modules) +patcher.start() +import RPi.GPIO +GPIO = RPi.GPIO diff --git a/scripts/test/test_TwoButtonControl.py b/scripts/test/test_TwoButtonControl.py new file mode 100644 index 000000000..ed2caa514 --- /dev/null +++ b/scripts/test/test_TwoButtonControl.py @@ -0,0 +1,155 @@ +import mock +import pytest +from mock import MagicMock +from test.mockedGPIO import GPIO + + +import TwoButtonControl + +@pytest.fixture +def btn1Mock(): + return mock.MagicMock() + + +@pytest.fixture +def btn2Mock(): + return mock.MagicMock() + + +@pytest.fixture +def functionCall1Mock(): + return mock.MagicMock() + + +@pytest.fixture +def functionCall2Mock(): + return mock.MagicMock() + + +@pytest.fixture +def functionCallBothPressedMock(): + return mock.MagicMock() + + +def test_functionCallTwoButtonsOnlyBtn1Pressed(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock, + functionCallBothPressedMock): + btn1Mock.is_pressed = True + btn2Mock.is_pressed = False + func = TwoButtonControl.functionCallTwoButtons(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock, + functionCallBothPressed=functionCallBothPressedMock) + func() + functionCall1Mock.assert_called_once_with() + functionCall2Mock.assert_not_called() + functionCallBothPressedMock.assert_not_called() + + +def test_functionCallTwoButtonsBothBtnsPressedFunctionCallBothPressedExists(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock, + functionCallBothPressedMock): + btn1Mock.is_pressed = True + btn2Mock.is_pressed = True + func = TwoButtonControl.functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, + functionCallBothPressed=functionCallBothPressedMock) + func() + functionCall1Mock.assert_not_called() + functionCall2Mock.assert_not_called() + functionCallBothPressedMock.assert_called_once_with() + + +def test_functionCallTwoButtonsBothBtnsPressedFunctionCallBothPressedIsNone(btn1Mock, + btn2Mock, + functionCall1Mock, + functionCall2Mock): + btn1Mock.is_pressed = True + btn2Mock.is_pressed = True + func = TwoButtonControl.functionCallTwoButtons(btn1Mock, btn2Mock, functionCall1Mock, functionCall2Mock, + functionCallBothPressed=None) + func() + functionCall1Mock.assert_not_called() + functionCall2Mock.assert_not_called() + + +mockedFunction1 = MagicMock() +mockedFunction2 = MagicMock() +mockedFunction3 = MagicMock() + + +@pytest.fixture +def two_button_control(): + mockedFunction1.reset_mock() + mockedFunction2.reset_mock() + mockedFunction3.reset_mock() + return TwoButtonControl.TwoButtonControl(bcmPin1=1, + bcmPin2=2, + functionCallBtn1=mockedFunction1, + functionCallBtn2=mockedFunction2, + functionCallTwoBtns=mockedFunction3, + pull_up=True, + hold_repeat=False, + hold_time=0.3, + name='TwoButtonControl') + + +class TestTwoButtonControl: + def test_init(self): + TwoButtonControl.TwoButtonControl(bcmPin1=1, + bcmPin2=2, + functionCallBtn1=mockedFunction1, + functionCallBtn2=mockedFunction2, + functionCallTwoBtns=mockedFunction3, + pull_up=True, + hold_repeat=False, + hold_time=0.3, + name='TwoButtonControl') + + def test_btn1_pressed(self, two_button_control): + pinA = two_button_control.bcmPin1 + pinB = two_button_control.bcmPin2 + def func(pin): + values = {pinA: False, pinB: True} + if pin in values: + return values[pin] + else: + print('Cannot find pin {} in values: {}'.format(pin,values)) + return None + TwoButtonControl.GPIO.input.side_effect = func + two_button_control.action() + mockedFunction1.assert_called_once() + mockedFunction2.assert_not_called() + mockedFunction3.assert_not_called() + two_button_control.action() + assert mockedFunction1.call_count == 2 + + def test_btn2_pressed(self, two_button_control): + pinA = two_button_control.bcmPin1 + pinB = two_button_control.bcmPin2 + TwoButtonControl.GPIO.input.side_effect = lambda pin: {pinA: True, pinB: False}[pin] + two_button_control.action() + mockedFunction1.assert_not_called() + mockedFunction2.assert_called_once() + mockedFunction3.assert_not_called() + two_button_control.action() + assert mockedFunction2.call_count == 2 + + def test_btn1_and_btn2_pressed(self, two_button_control): + pinA = two_button_control.bcmPin1 + pinB = two_button_control.bcmPin2 + TwoButtonControl.GPIO.input.side_effect = lambda pin: {pinA: False, pinB: False}[pin] + two_button_control.action() + mockedFunction1.assert_not_called() + mockedFunction2.assert_not_called() + mockedFunction3.assert_called_once() + two_button_control.action() + assert mockedFunction3.call_count == 2 + + def test_repr(self, two_button_control): + expected = "" + assert repr(two_button_control) == expected diff --git a/settings/gpio_settings.ini b/settings/gpio_settings.ini new file mode 100644 index 000000000..a42926215 --- /dev/null +++ b/settings/gpio_settings.ini @@ -0,0 +1,75 @@ +[DEFAULT] +enabled: True +[VolumeControl] +enabled: False +Type: RotaryEncoderClickable +PinUp: 16 +PinDown: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +timeBase: 0.1 # only for rotary encoder +functionCallDown: functionCallVolD +functionCallUp: functionCallVolU +functionCallTwoButtons: functionCallVol0 #only for TwoButtonControl +functionCallButton: functionCallPlayerPause # only for RotaryEncoderClickable + +[PrevNextControl] +Type: TwoButtonControl +Pin1: 4 +Pin2: 3 +functionCall1: functionCallPlayerPrev +functionCall2: functionCallPlayerNext +functionCallTwoButtons: None +pull_up: True +hold_time: 0.3 +hold_repeat: False + + +[Shutdown] +enabled: False +Type: Button +Pin: 3 +hold_time: 2 +functionCall: functionCallShutdown + +[Volume0] +enabled: False +Type: Button +Pin: 13 +pull_up: True +functionCall: functionCallVol0 + +[VolumeUp] +enabled: False +Type: Button +Pin: 16 +pull_up: True +hold_time: 0.3 +hold_repeat: True +functionCall: functionCallVolU + +[VolumeDown] +enabled: False +Type: Button +Pin: 19 +pull_up: True +hold_time: 0.3 +hold_repeat: True +[NextSong] +enabled: False +Type: Button +Pin: 26 +pull_up: True + +[PrevSong] +enabled: False +Type: Button +Pin: 20 +pull_up: True + +[Halt] +enabled: False +Type: Button +Pin: 21 +pull_up: True