# coding: utf-8
import atexit
from abc import ABCMeta, abstractmethod
from .util import StrKeyDict
HIGH = 'HIGH'
LOW = 'LOW'
# TODO: 4 states implementation: IN, OUT, ANALOG, PWM
IN = 'IN'
OUT = 'OUT'
ANALOG = 'ANALOG'
PWM = 'PWM'
class WrongPinMode(Exception):
value = 'Operation not supported in current mode.'
class ModeNotSuported(Exception):
value = 'Mode not suported by Pin or Board.'
class ArgumentOutOfRange(Exception):
value = 'Argument not in the range 0.0 to 1.0'
[docs]class Board(object):
"""Abstract class defining common interface for all boards.
Instance attributes of interest to end-users:
``«board».pins``
A ``dict`` with physical pin locations as keys and ``Pin`` instances
as values.
``«board».cleanup()``
This should be called to release the pins for other applications on
some boards. It is called automatically when the script finishes.
Implementations of ``Board`` subclasses should:
* Call ``super(«BoardSubclass», self).__init__()`` and
``self._add_pins(«pins»)`` in their ``__init__`` method.
* Implement ``_set_pin_mode()`` and ``_set_pin_state()``.
* Override ``cleanup()``, if the board needs it.
"""
__metaclass__ = ABCMeta
def __init__(self):
"""Registers ``self.cleanup`` for calling at script exit.
This ``__init__`` method should be called by the ``__init__``
of all ``Board`` subclasses using ``super(MyBoard, self).__init__()``.
The ``__init__`` of board subclasses should also call
``self._add_pins(pins)`` with an iterable of ``Pin`` instances.
"""
atexit.register(self.cleanup)
[docs] def filter_pins(self, *pin_types):
"""Get a list of pins that are instances of the given pin_types
See the ``digital_pins`` property for an example of use.
Arguments:
``pin_types``: an iterable of types (usually, ``Pin`` subclasses)
"""
filtered = []
for pin_type in pin_types:
sub = [x for x in self.pins.values() if isinstance(x, pin_type)]
filtered += sub
return filtered
[docs] def select_pins(self, locations):
"""Get list of pins from iterable of locations"""
locations = list(locations)
return [self.pins[location] for location in locations]
@property
def digital_pins(self):
"""[property] Get list of digital pins"""
return self.filter_pins(DigitalPin)
[docs] def cleanup(self):
"""Releases pins for use by other applications.
Overriding this stub may or may not be needed in specific drivers.
For example, scripts running on boards using standard ``sysfs``
GPIO access should ``unexport`` the pins before exiting.
"""
pass
######################################################################
# the following methods are of interest only to implementers of
# drivers, i.e. concrete Board subclasses
[docs] def _add_pins(self, pins):
"""Populate ``board.pins`` mapping from ``Pin`` instances.
The ``__init__`` method of concrete ``Board`` subclasses should
call this method to populate the board instance ``pins`` mapping.
Arguments:
``pins``: an iterable of ``Pin`` instances
"""
self.pins = StrKeyDict()
for pin in pins:
self.pins[pin.location] = pin
@abstractmethod
[docs] def _set_pin_mode(self, pin, mode):
"""Abstract method to be implemented by each ``Board`` subclass.
The ``«pin».mode(…)`` property calls this method because
the procedure to set pin mode changes from board to board.
"""
@abstractmethod
[docs] def _set_pin_state(self, pin, state):
"""Abstract method to be implemented by each ``Board`` subclass
The ``«pin».__change_state(…)`` method calls this method because
the procedure to set pin state changes from board to board.
"""
@abstractmethod
[docs] def _get_pin_state(self, pin):
"""Abstract method to be implemented by each ``Board`` subclass
"""
class PwmOutputCapable(object):
"""Mixin interface for boards that support PwmOutputPin
Concrete ``PwmOutputCapable`` subclasses should implement
``_get_pin_value`` to write the PWM signal of analog pins.
"""
__metaclass__ = ABCMeta
@abstractmethod
def _set_pwm_mode(self, pin):
"""Abstract method to be implemented by each ``Board`` subclass."""
@abstractmethod
def _set_pwm_frequency(self, pin, value):
"""Abstract method to be implemented by each ``Board`` subclass.
The ``«PwmPin».frequency(…)`` method calls this method because
the procedure to set the PWM's frequency changes from board to board.
"""
@abstractmethod
def _set_pwm_duty_cycle(self, pin, value):
"""Abstract method to be implemented by each ``Board`` subclass.
The ``«PwmPin».value(…)`` method calls this method because
the procedure to set PWM's duty cycle changes from board to board.
"""
def _get_pwm_duty_cycle(self, pin):
"""
This method should be overwritten if the ``Board`` subclass
has this feature.
"""
if hasattr(pin, '_duty_cycle'):
return pin._duty_cycle
return 0.0
def _get_pwm_frequency(self, pin):
"""
This method should be overwritten if the ``Board`` subclass
has this feature.
"""
if hasattr(pin, '_frequency'):
return pin._frequency
return 0.0
[docs]class Pin(object):
"""Abstract class defining common interface for all pins."""
__metaclass__ = ABCMeta
suported_modes = []
def __init__(self, board, location, gpio_id=None):
"""Initialize ``Pin`` instance with
Arguments:
``board``
The board to which the pin is attached.
``location``
Physical location of pin; ``int`` and ``str`` are
acceptable.
``gpio_id``
Logical name of GPIO pin (e.g. ``sysfs`` file name).
"""
self.board = board
self.location = location
if gpio_id is not None:
self.gpio_id = gpio_id
self._mode = None
def __repr__(self):
cls_name = self.__class__.__name__
location = self.location
if hasattr(self, 'gpio_id'):
gpio_id = 'gpio%s' % self.gpio_id
else:
gpio_id = ''
return '<{cls_name} {gpio_id}@{location}>'.format(**locals())
@property
def mode(self):
"""[property] Get/set pin mode to ``pingo.IN``, ``pingo.OUT``
``pingo.ANALOG`` or ``pingo.PWM``"""
return self._mode
@mode.setter
def mode(self, value):
if value not in self.suported_modes:
raise ModeNotSuported()
if value in [IN, OUT]:
self.board._set_pin_mode(self, value)
elif value == ANALOG:
self.board._set_analog_mode(self, value)
elif value == PWM:
self.board._set_pwm_mode(self, value)
self._mode = value
[docs]class DigitalPin(Pin):
"""Defines common interface for all digital pins.
Implementers of board drivers do not need to subclass this class
because pins delegate all board-dependent behavior to the board.
"""
suported_modes = [IN, OUT]
def __init__(self, board, location, gpio_id=None):
Pin.__init__(self, board, location, gpio_id)
self._state = None
@property
def state(self):
"""[property] Get/set pin state to ``pingo.HIGH`` or ``pingo.LOW``"""
if self.mode not in [IN, OUT]:
raise WrongPinMode()
if self.mode == IN:
self._state = self.board._get_pin_state(self)
return self._state
@state.setter
def state(self, value):
if self.mode != OUT:
raise WrongPinMode()
self.board._set_pin_state(self, value)
self._state = value
[docs] def low(self):
"""Set voltage of pin to ``pingo.LOW`` (GND)."""
self.state = LOW
lo = low # shortcut for interactive use
[docs] def high(self):
"""Set state of the pin to ``pingo.HIGH`` (Vcc)."""
self.state = HIGH
hi = high # shortcut for interactive use
def toggle(self):
self.state = HIGH if self.state == LOW else LOW
class PwmPin(DigitalPin):
suported_modes = [IN, OUT, PWM]
def __init__(self, board, location, gpio_id=None, frequency=None):
DigitalPin.__init__(self, board, location, gpio_id)
self._frequency = frequency
self._duty_cycle = None
# TUDO:
# Write a decorator to test mode == 'MODE'
@property
def value(self):
if self.mode != PWM:
raise WrongPinMode()
return self.board._get_pwm_duty_cycle(self)
@value.setter
def value(self, value):
if self.mode != PWM:
raise WrongPinMode()
if not 0.0 <= value <= 100.0:
raise ArgumentOutOfRange()
self.board._set_pwm_duty_cycle(self, value)
self._duty_cycle = value
@property
def frequency(self):
if self.mode != PWM:
raise WrongPinMode()
return self.board._get_pwm_frequency(self)
@frequency.setter
def frequency(self, new_frequency):
if self.mode != PWM:
raise WrongPinMode()
if new_frequency <= 0.0:
raise ArgumentOutOfRange()
self.board._set_pwm_frequency(self, new_frequency)
self._frequency = new_frequency
[docs]class AnalogPin(Pin):
"""Defines common interface for all analog pins.
Implementers of board drivers do not need to subclass this class
because pins delegate all board-dependent behavior to the board.
This pin type supports read operations only.
"""
suported_modes = [IN, ANALOG]
[docs] def __init__(self, board, location, resolution, gpio_id=None):
"""
:param board: the board to which this ping belongs
:param location: the physical location of the pin on the board
:param resolution: resolution of the AD converter in bits
:param gpio_id: the logical id for GPIO access, if applicable
"""
Pin.__init__(self, board, location, gpio_id)
self.bits = resolution
self._mode = None
@property
def value(self):
"""[property] Pin value as integer from 0 to 2 ** resolution - 1"""
return self.board._get_pin_value(self)
[docs] def ratio(self, from_min=0, from_max=None, to_min=0.0, to_max=1.0):
""" Pin value as a float, by default from 0.0 to 1.0.
The ``from...`` and ``to...`` parameters work like in the Arduino map_
function, converting values from an expected input range to a desired
output range.
.. _map: http://arduino.cc/en/reference/map
"""
if from_max is None:
from_max = 2 ** self.bits - 1
_value = self.value
return (float(_value - from_min) * (to_max - to_min) /
(from_max - from_min) + to_min)
@property
def percent(self):
"""[property] Pin value as float from 0.0 to 100.0"""
return self.ratio(to_max=100.0)
class GroundPin(Pin):
def __repr__(self):
return '<%s>' % self.__class__.__name__
class VccPin(Pin):
def __init__(self, board, location, voltage):
Pin.__init__(self, board, location)
self.voltage = voltage # e.g. 3.3, 5.0
def __repr__(self):
return '<%s %0.1fV>' % (self.__class__.__name__,
self.voltage)