"""General-purpose I/O abstraction module.
Provide a convergence layer API to abstract from several different
GPIO implementing driver modules possibly installed on the target
system.
"""
__author__ = "Oliver Maye"
__version__ = "0.1"
__all__ = ["GPIO"]
import logging
from threading import Thread
import warnings
from .interruptable import Interruptable
from .module import Module
from .systypes import ErrorCode
[docs]class GPIO( Module, Interruptable ):
"""General-purpose I/O abstraction class.
Provide access to and control over the underlying GPIO hardware. For
that, an implementing driver module is used. Currently, RPi.GPIO,
gpiozero and periphery are supported. As a convergence layer, this
class is to hide specifics and level syntactic requirements of the
implementing package.
"""
_IMPLPAK_NONE = 0
_IMPLPAK_RPIGPIO = 1
_IMPLPAK_GPIOZERO = 2
_IMPLPAK_PERIPHERY = 3
_IMPLPAK_SIM = 4
_POLL_TIMEOUT = 1
PINNUMBERING_BCM = "BCM" # Pin naming by GPIOx number
PINNUMBERING_BOARD = "BOARD" # Pin naming by number on header
DIRECTION_IN = 1
DIRECTION_OUT = 2
LEVEL_LOW = 0
LEVEL_HIGH = 1
PULL_DEFAULT = 0 # Don't touch, leave resistance as is
PULL_NONE = 1 # Disable resistance
PULL_UP = 2
PULL_DOWN = 3
TRIGGER_EDGE_RISING = 1
TRIGGER_EDGE_FALLING = 2
TRIGGER_EDGE_ANY = 3
TRIGGER_LEVEL_HIGH = 4
TRIGGER_LEVEL_LOW = 5
BOUNCE_NONE = 0 # Disable de-bouncing.
BOUNCE_DEFAULT = 200 # Default de-bounce interval in ms.
EVENT_DEFAULT = "gpioFired" # Specific event fired on interrupt.
def __init__(self):
"""Initialize the instance with defaults.
As part of the construction, the underlying implementation is
determined. So, at this time, one of the supported gpio packages
will be accessed.
Still, note that just after construction, the instance is not
operable, yet. Call open() to configure it and set it into a
functional state.
"""
self._factory = None
self.pin = None
self._dictDirection = {}
self._dictLevel = {}
self._dictPull = {}
self._dictTrigger = {}
self._designator = None
self._direction = GPIO.DIRECTION_OUT
self._trigger = GPIO.TRIGGER_EDGE_RISING
self._bounce = GPIO.BOUNCE_NONE
self._fIntEnabled = False
Interruptable.__init__(self)
self._implpak = self._detectProvider()
self._worker = None
self._workerDone = False
# Figure out, which of the supported driver packages is installed.
# Also, do the implementation-specific initialization, e.g. of
# dictionaries.
# Supported packages are (by priority):
# - RPi.GPIO
# - gpiozero
# - periphery
# :return: One of the _IMPLPAK_xxx constants to indicate the
# implementation package.
# :rtype: int
# :raise: warning in case that none of the supported packages could
# be found.
def _detectProvider(self):
ret = GPIO._IMPLPAK_NONE
# Check for RPi.GPIO
if ret == GPIO._IMPLPAK_NONE:
try:
import RPi.GPIO as gpioFactory
self._factory = gpioFactory
self._dictNumScheme = {
GPIO.PINNUMBERING_BCM: gpioFactory.BCM,
GPIO.PINNUMBERING_BOARD: gpioFactory.BOARD,
}
self._dictDirection = {
GPIO.DIRECTION_IN: gpioFactory.IN,
GPIO.DIRECTION_OUT: gpioFactory.OUT,
}
self._dictLevel = {
GPIO.LEVEL_LOW: gpioFactory.LOW,
GPIO.LEVEL_HIGH: gpioFactory.HIGH,
}
self._dictPull = {
GPIO.PULL_DEFAULT: gpioFactory.PUD_OFF,
GPIO.PULL_NONE: gpioFactory.PUD_OFF,
GPIO.PULL_DOWN: gpioFactory.PUD_DOWN,
GPIO.PULL_UP: gpioFactory.PUD_UP,
}
self._dictTrigger = {
GPIO.TRIGGER_EDGE_RISING: gpioFactory.RISING,
GPIO.TRIGGER_EDGE_FALLING: gpioFactory.FALLING,
GPIO.TRIGGER_EDGE_ANY: gpioFactory.BOTH,
}
ret = GPIO._IMPLPAK_RPIGPIO
except ModuleNotFoundError:
pass # Suppress the exception, use return, instead.
# Check for gpiozero
if ret == GPIO._IMPLPAK_NONE:
try:
from gpiozero import DigitalInputDevice, DigitalOutputDevice
self._inFactory = DigitalInputDevice
self._outFactory = DigitalOutputDevice
self._dictLevel = {GPIO.LEVEL_LOW: False, GPIO.LEVEL_HIGH: True}
self._dictPull = {
GPIO.PULL_DEFAULT: None,
GPIO.PULL_NONE: None,
GPIO.PULL_DOWN: False,
GPIO.PULL_UP: True,
}
ret = GPIO._IMPLPAK_GPIOZERO
except ModuleNotFoundError:
pass # Suppress the exception, use return, instead.
# Check for periphery
if ret == GPIO._IMPLPAK_NONE:
try:
from periphery import GPIO as gpioFactory
self._factory = gpioFactory
self._dictDirection = {
GPIO.DIRECTION_IN: "in",
GPIO.DIRECTION_OUT: "out",
}
self._dictLevel = {GPIO.LEVEL_LOW: False, GPIO.LEVEL_HIGH: True}
self._dictLevel2Dir = {GPIO.LEVEL_LOW: "low", GPIO.LEVEL_HIGH: "high"}
self._dictPull = {
GPIO.PULL_DEFAULT: "default",
GPIO.PULL_NONE: "disable",
GPIO.PULL_DOWN: "pull_down",
GPIO.PULL_UP: "pull_up",
}
self._dictTrigger = {
GPIO.TRIGGER_EDGE_RISING: "rising",
GPIO.TRIGGER_EDGE_FALLING: "falling",
GPIO.TRIGGER_EDGE_ANY: "both",
}
ret = GPIO._IMPLPAK_PERIPHERY
except ModuleNotFoundError:
pass # Suppress the exception, use return, instead.
# Failure
if ret == GPIO._IMPLPAK_NONE:
warnings.warn(
"Cannot find GPIO factory lib. Using SIM. Consider installing RPi.GPIO, gpiozero or periphery!"
)
self._dictLevel = {
GPIO.LEVEL_LOW: GPIO.LEVEL_LOW,
GPIO.LEVEL_HIGH: GPIO.LEVEL_HIGH,
}
ret = GPIO._IMPLPAK_SIM
return ret
# Interrupt handling routine called by the underlying implementation
# upon a gpio interrupt occurrence. Determine the source (pin) of
# this interrupt and inform registrants by firing an event.
#
# :param handin: Parameter as provided by the underlying implementation
# :type handin: implementation-specific
def _callback(self, handin):
if self._implpak == GPIO._IMPLPAK_GPIOZERO:
argDes = handin.pin.number
else:
argDes = handin
super()._fire(GPIO.EVENT_DEFAULT, argDes)
return None
# Thread working loop to poll for the pin state triggering an
# interrupt. This is necessary in case interrupts are not natively
# supported by the underlying implementation, such as for the
# periphery package.
def _workerLoop(self):
logging.debug("gpio <%d> starts working loop.", self._designator)
self._workerDone = False
lastTime = 0
while not self._workerDone:
value = self.pin.poll(GPIO._POLL_TIMEOUT)
if value:
evt = self.pin.read_event()
if (evt.timestamp - lastTime) > self._bounce * 1000000:
lastTime = evt.timestamp
logging.debug("gpio <%d> consumed event %s.", self._designator, evt)
self._callback(self._designator)
logging.debug("gpio <%d> terminates working loop.", self._designator)
# Stop the worker thread, if appropriate.
def _stopWorker(self):
if self._worker:
if self._worker.is_alive():
self._workerDone = True
self._worker.join()
self._worker = None
[docs] @classmethod
def Params_init(cls, paramDict):
"""Initialize parameters with their defaults.
The given dictionary should not be None, on entry.
Options not present in the dictionary will be added and set to
their defaults on return.
The following options are supported.
================== ============================================== =========================
Key Range Default
================== ============================================== =========================
gpio.pinNumbering GPIO.PINNUMBERING_[BCM | BOARD] GPIO.PINNUMBERING_BCM
gpio.pinDesignator pin name or number (e.g. 17 or "GPIO17") None
gpio.direction GPIO.DIRECTION_[IN | OUT] GPIO.DIRECTION_OUT
gpio.level GPIO.LEVEL_[LOW | HIGH] GPIO.LEVEL_LOW
gpio.pull GPIO.PULL_[DEFAULT | NONE | UP | DOWN] GPIO.PULL_DEFAULT (NONE)
gpio.trigger GPIO.TRIGGER_EDGE_[RISING | FALLING | ANY] GPIO.TRIGGER_EDGE_RISING
gpio.bounce integer number, delay in milliseconds [ms] GPIO.BOUNCE_DEFAULT
gpio.feedback Arbitrary. Passed on to the interrupt handler. None
gpio.handler Handling routine reference. None
================== ============================================== =========================
:param dict(str, object) paramDict: Configuration parameters as obtained from :meth:`Params_init`, possibly.
:return: none
:rtype: None
"""
if not ("gpio.pinNumbering" in paramDict):
paramDict["gpio.pinNumbering"] = GPIO.PINNUMBERING_BCM
if not ("gpio.direction" in paramDict):
paramDict["gpio.direction"] = GPIO.DIRECTION_OUT
if not ("gpio.level" in paramDict):
paramDict["gpio.level"] = GPIO.LEVEL_LOW
if not ("gpio.pull" in paramDict):
paramDict["gpio.pull"] = GPIO.PULL_DEFAULT
if not ("gpio.trigger" in paramDict):
paramDict["gpio.trigger"] = GPIO.TRIGGER_EDGE_RISING
if not ("gpio.bounce" in paramDict):
paramDict["gpio.bounce"] = GPIO.BOUNCE_DEFAULT
if not ("gpio.feedback" in paramDict):
paramDict["gpio.feedback"] = None
if not ("gpio.handler" in paramDict):
paramDict["gpio.handler"] = None
return None
[docs] def open(self, paramDict):
"""Opens the instance and sets it in a usable state.
Allocate necessary hardware resources and configure
user-adjustable parameters to meaningful defaults.
This function must be called prior to any further usage of the
instance. Involving it in the system ramp-up procedure could be
a good choice. After usage of this instance is finished, the
application should call :meth:`close`.
:param dict(str, object) paramDict: Configuration parameters as obtained from :meth:`Params_init`, possibly.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
ret = ErrorCode.errOk
# Retrieve defaults
defaults = {}
self.Params_init(defaults)
handler = None
# Scan parameters
self._designator = paramDict.get("gpio.pinDesignator", None)
if self._designator is None:
ret = ErrorCode.errInvalidParameter
numScheme = paramDict.get("gpio.pinNumbering", defaults["gpio.pinNumbering"])
self._direction = paramDict.get("gpio.direction", defaults["gpio.direction"])
level = paramDict.get("gpio.level", defaults["gpio.level"])
if self._direction == GPIO.DIRECTION_IN:
pull = paramDict.get("gpio.pull", defaults["gpio.pull"])
self._trigger = paramDict.get("gpio.trigger", defaults["gpio.trigger"])
self._bounce = paramDict.get("gpio.bounce", defaults["gpio.bounce"])
feedback = paramDict.get("gpio.feedback", defaults["gpio.feedback"])
handler = paramDict.get("gpio.handler", defaults["gpio.handler"])
if ret == ErrorCode.errOk:
if self._implpak == GPIO._IMPLPAK_RPIGPIO:
self._factory.setmode(self._dictNumScheme[numScheme])
if self._direction == GPIO.DIRECTION_OUT:
self._factory.setup(
self._designator,
self._factory.OUT,
initial=self._dictLevel[level],
)
else:
self._factory.setup(
self._designator,
self._factory.IN,
pull_up_down=self._dictPull[pull],
)
elif self._implpak == GPIO._IMPLPAK_GPIOZERO:
if numScheme == GPIO.PINNUMBERING_BOARD:
self._designator = "BOARD" + str(self._designator)
if self._direction == GPIO.DIRECTION_OUT:
self.pin = self._outFactory(
self._designator, initial_value=self._dictLevel[level]
)
else:
if pull == GPIO.PULL_NONE:
actState = (self._trigger == GPIO.TRIGGER_EDGE_RISING) or (
self._trigger == GPIO.TRIGGER_LEVEL_HIGH
)
else:
actState = None
if self._bounce > 0:
self.pin = self._inFactory(
self._designator,
pull_up=self._dictPull[pull],
active_state=actState,
bounce_time=self._bounce,
)
else:
self.pin = self._inFactory(
self._designator,
pull_up=self._dictPull[pull],
active_state=actState,
)
elif self._implpak == GPIO._IMPLPAK_PERIPHERY:
if numScheme == GPIO.PINNUMBERING_BCM:
if self._direction == GPIO.DIRECTION_OUT:
self.pin = self._factory(
"/dev/gpiochip0",
self._designator,
self._dictLevel2Dir[level],
)
else:
self.pin = self._factory(
"/dev/gpiochip0",
self._designator,
self._dictDirection[GPIO.DIRECTION_IN],
bias=self._dictPull[pull],
)
else:
ret = ErrorCode.errNotSupported
elif self._implpak == GPIO._IMPLPAK_SIM:
self._level = level
else:
ret = ErrorCode.errNotImplemented
if ret == ErrorCode.errOk:
if handler:
ret = self.registerInterruptHandler(
GPIO.EVENT_DEFAULT, feedback, handler
)
return ret
[docs] def close(self):
"""Closes this instance and releases associated hardware resources.
This is the counterpart of :meth:`open`. Upon return, further
usage of this instance is prohibited and may lead to unexpected
results. The instance can be re-activated by calling :meth:`open`,
again.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
ret = self.registerInterruptHandler(None)
if self._implpak == GPIO._IMPLPAK_RPIGPIO:
if not self._designator is None:
self._factory.cleanup(self._designator)
elif self._implpak == GPIO._IMPLPAK_GPIOZERO:
self.pin.close()
elif self._implpak == GPIO._IMPLPAK_PERIPHERY:
self._stopWorker()
self.pin.close()
elif self._implpak == GPIO._IMPLPAK_SIM:
pass
else:
ret = ErrorCode.errNotImplemented
self.pin = None
return ret
[docs] def setRunLevel(self, level):
"""Select the power-saving operation mode.
Switches the instance to one of the power-saving modes or
recovers from these modes. Situation-aware deployment of these
modes can greatly reduce the system's total power consumption.
:param RunLevel level: The level to switch to.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
del level
return ErrorCode.errNotImplemented
[docs] def enableInterrupt(self):
"""Enables the gpio interrupt for that pin.
If the pin is configured for input, enables the interrupt for
that pin. Depending on the trigger configured during :meth:`open`,
an event will be fired the next time when the condition is
satisfied.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
ret = ErrorCode.errOk
if self._fIntEnabled:
ret = ErrorCode.errOk
else:
if self._implpak == GPIO._IMPLPAK_RPIGPIO:
if self._bounce > 0:
self._factory.add_event_detect(
self._designator,
self._dictTrigger[self._trigger],
callback=self._callback,
bouncetime=self._bounce,
)
else:
self._factory.add_event_detect(
self._designator,
self._dictTrigger[self._trigger],
callback=self._callback,
)
self._fIntEnabled = True
elif self._implpak == GPIO._IMPLPAK_GPIOZERO:
self.pin.when_activated = self._callback
if self._trigger == GPIO.TRIGGER_EDGE_ANY:
self.pin.when_deactivated = self._callback
self._fIntEnabled = True
elif self._implpak == GPIO._IMPLPAK_PERIPHERY:
self.pin.edge = self._dictTrigger[self._trigger]
self._stopWorker()
self._worker = Thread(target=self._workerLoop, name="GPIO worker")
self._worker.start()
self._fIntEnabled = True
else:
ret = ErrorCode.errNotImplemented
return ret
[docs] def disableInterrupt(self):
"""Disables the gpio interrupt for that pin.
Immediately disables the interrupt for that pin. It will not
_fire an event anymore, unless :meth:`enableInterrupt` is called
anew.
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
ret = ErrorCode.errOk
if self._fIntEnabled:
if self._implpak == GPIO._IMPLPAK_RPIGPIO:
self._factory.remove_event_detect(self._designator)
self._fIntEnabled = False
elif self._implpak == GPIO._IMPLPAK_GPIOZERO:
from gpiozero import CallbackSetToNone
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=CallbackSetToNone)
self.pin.when_activated = None
self.pin.when_deactivated = None
self._fIntEnabled = False
elif self._implpak == GPIO._IMPLPAK_PERIPHERY:
self._stopWorker()
self.pin.edge = "none"
self._fIntEnabled = False
else:
ret = ErrorCode.errNotImplemented
else:
ret = ErrorCode.errOk
return ret
[docs] def get(self):
"""Retrieve the pin level.
Gives the pin level, independent of whether the pin direction
is set to input or output.
:return: GPIO.LEVEL_HIGH, if the pin is at high level. Otherwise, GPIO.LEVEL_LOW.
:rtype: int
"""
level = GPIO.LEVEL_LOW
if self._implpak == GPIO._IMPLPAK_RPIGPIO:
status = self._factory.input(self._designator)
elif self._implpak == GPIO._IMPLPAK_GPIOZERO:
status = self.pin.value
elif self._implpak == GPIO._IMPLPAK_PERIPHERY:
status = self.pin.read()
elif self._implpak == GPIO._IMPLPAK_SIM:
status = self._level
else:
status = 0
if status == self._dictLevel[GPIO.LEVEL_HIGH]:
level = GPIO.LEVEL_HIGH
else:
level = GPIO.LEVEL_LOW
return level
[docs] def set(self, newLevel):
"""Sets the pin to the given level.
Outputs the given level at this pin. Does not work, if this pin
is set to input direction.
:param int newLevel: The new level to set this pin to. Must be one of GPIO.LEVEL_[HIGH | LOW].
:return: An error code indicating either success or the reason of failure.
:rtype: ErrorCode
"""
ret = ErrorCode.errOk
if self._implpak == GPIO._IMPLPAK_RPIGPIO:
self._factory.output(self._designator, self._dictLevel[newLevel])
elif self._implpak == GPIO._IMPLPAK_GPIOZERO:
self.pin.value = self._dictLevel[newLevel]
elif self._implpak == GPIO._IMPLPAK_PERIPHERY:
self.pin.write(self._dictLevel[newLevel])
elif self._implpak == GPIO._IMPLPAK_SIM:
if newLevel == GPIO.LEVEL_HIGH:
self._level = GPIO.LEVEL_HIGH
else:
self._level = GPIO.LEVEL_LOW
else:
ret = ErrorCode.errNotImplemented
return ret