from vent.io.devices import IODeviceBase
from vent.common.fashion import pigpio_command
import pigpio
[docs]class Pin(IODeviceBase):
""" Base Class wrapping pigpio methods for interacting with GPIO pins on
the raspberry pi. Subclasses include InputPin, OutputPin; along with
any specialized pins or specific devices defined in vent.io.actuators
& vent.io.sensors (note: actuators and sensors do not need to be tied
to a GPIO pin and may instead be interfaced through an ADC or I2C).
This is an abstract base class. The subclasses InputPin and
OutputPin extend Pin into a usable form.
"""
_PIGPIO_MODES = {'INPUT': 0,
'OUTPUT': 1,
'ALT5': 2,
'ALT4': 3,
'ALT0': 4,
'ALT1': 5,
'ALT2': 6,
'ALT3': 7}
[docs] def __init__(self, pin, pig=None):
""" Inherits attributes and methods from IODeviceBase.
Args:
pin (int): The number of the pin to use
pig (PigpioConnection): pigpiod connection to use; if not specified, a new one is established
"""
super().__init__(pig)
self.pin = pin
@property
@pigpio_command
def mode(self) -> str:
""" The currently active pigpio mode of the pin."""
return {value: key for key, value in self._PIGPIO_MODES.items()}[self.pig.get_mode(self.pin)]
@mode.setter
@pigpio_command
def mode(self, mode):
""" Performs validation on requested mode, then sets the mode. Raises runtime error if something goes wrong.
Args:
mode (str): A mode in _PIGPIO_MODES
"""
if mode not in self._PIGPIO_MODES:
raise ValueError("Pin mode must be one of: {}".format([*self._PIGPIO_MODES.keys()]))
result = self.pig.set_mode(self.pin, self._PIGPIO_MODES[mode])
if result != 0:
raise RuntimeError('Failed to write mode {} on pin {}'.format(mode, self.pin))
[docs] def toggle(self):
""" If pin is on, turn it off. If it's off, turn it on. Do not raise a warning when pin is read in this way."""
self.write(not self.read())
[docs] @pigpio_command
def read(self) -> int:
""" Returns the value of the pin: usually 0 or 1 but can be overridden by subclass."""
return self.pig.read(self.pin)
[docs] @pigpio_command
def write(self, value):
""" Sets the value of the Pin. Usually 0 or 1 but behavior differs for some subclasses.
Args:
value: The value to write to the pin. Can be either `1` to turn on the pin or `0` to turn it off.
"""
if value not in (0, 1):
raise ValueError('Cannot write a value other than 0 or 1 to a Pin')
self.pig.write(self.pin, value)
[docs]class PWMOutput(Pin):
""" A pin configured to output a PWM signal. Can be configured to use either a hardware-generated or
software-generated signal. Overrides parent methods read() and write().
"""
_DEFAULT_FREQUENCY = 20000
_DEFAULT_SOFT_FREQ = 2000
_HARDWARE_PWM_PINS = (12, 13, 18, 19)
[docs] def __init__(self, pin, initial_duty=0, frequency=None, pig=None):
""" Inherits attributes from parent Pin, then sets PWM frequency & initial duty (use defaults if None)
Args:
pin (int): The number of the pin to use. Hardware PWM pins are 12, 13, 18, and 19.
initial_duty (float): The initial duty cycle of the pin. Must be between 0 and 1.
frequency (float): The PWM frequency to use.
pig (PigpioConnection): pigpiod connection to use; if not specified, a new one is established
"""
super().__init__(pin=pin, pig=pig)
self._hardware_enabled = True if self.pin in self._HARDWARE_PWM_PINS else False
default_f = self._DEFAULT_FREQUENCY if self._hardware_enabled else self._DEFAULT_SOFT_FREQ
self.__pwm(default_f if frequency is None else frequency, initial_duty)
@property
def hardware_enabled(self):
""" Return true if this is a hardware-enabled PWM pin; False if not. The Raspberry Pi only supports hardware-
generated PWM on pins 12, 13, 18, and 19, so generally `hardware_enabled` will be true if this is one of those,
and false if it is not. However, `hardware_enabled` can also by dynamically set to False if for some reason
pigpio is unable to start a hardware PWM (i.e. if the clock is unavailable or in use or something)
"""
return self._hardware_enabled
@property
@pigpio_command
def frequency(self) -> float:
""" Return the current PWM frequency active on the pin."""
return self.pig.get_PWM_frequency(self.pin)
@frequency.setter
def frequency(self, new_frequency):
""" TODO: extend exception handling/logging to this
Note: pigpio.pi.hardware_PWM() returns 0 if OK and an error code otherwise.
- Tries to write hardware PWM if hardware_enabled
- If that fails, or if not hardware_enabled, tries to write software PWM instead.
Args:
new_frequency (float): A new PWM frequency to use.
"""
self.__pwm(new_frequency, self._duty())
@property
@pigpio_command
def duty(self) -> float:
""" Returns the PWM duty cycle (pulled straight from pigpiod) mapped to the range [0-1] """
return self.pig.get_PWM_dutycycle(self.pin) / self.pig.get_PWM_range(self.pin)
[docs] @pigpio_command
def _duty(self) -> int:
""" Returns the pigpio integer representation of the duty cycle."""
return self.pig.get_PWM_dutycycle(self.pin)
@duty.setter
@pigpio_command
def duty(self, duty_cycle):
""" Sets the duty cycle.
Args:
duty_cycle (float): The PWM duty cycle to set. Must be between 0 and 1 (verified upon calling __pwm()).
"""
self.__pwm(self.frequency, duty_cycle)
[docs] def read(self) -> float:
""" Overridden to return duty cycle instead of reading the value on the pin."""
return self.duty
[docs] def write(self, value):
""" Overridden to write duty cycle.
Args:
value (float): See `PWMOutput.duty`
"""
self.duty = value
def __pwm(self, frequency, duty):
""" Sets a PWM frequency and duty using either hardware or software generated PWM according to the value of
`self.hardware_enabled`. If hardware_enabled, starts a hardware pwm with the requested duty. If not
hardware_enabled, or if there is a problem setting a hardware generated PWM, starts a software PWM.
Args:
frequency (float): A new PWM frequency to use.
duty (float): The PWM duty cycle to set. Must be between 0 and 1.
"""
if not 0 <= duty <= 1:
raise ValueError('Duty cycle must be between 0 and 1 but got {}'.format(duty))
_duty = int(duty * self.pig.get_PWM_range(self.pin))
if self._hardware_enabled:
self.__hardware_pwm(frequency, _duty)
if not self._hardware_enabled:
self.__software_pwm(frequency, _duty)
@pigpio_command
def __hardware_pwm(self, frequency, duty):
""" Used for pins where hardware pwm is available.
-Tries to write a hardware pwm. result == 0 if it succeeds.
-Sets hardware_enabled flag to indicate success or failure
Args:
frequency: A new PWM frequency to use.
duty (int): The PWM duty cycle to set. Must be between 0 and 1.
"""
try:
self.pig.hardware_PWM(self.pin, frequency, duty)
self._hardware_enabled = True
except pigpio.error:
self._hardware_enabled = False
self.__software_pwm(frequency, duty)
@pigpio_command
def __software_pwm(self, frequency, duty):
""" Used for pins where hardware PWM is NOT available.
Args:
frequency: A new PWM frequency to use.
duty (int): A pigpio integer representation of duty cycle
"""
self.pig.set_PWM_dutycycle(self.pin, duty)
realized_frequency = self.pig.set_PWM_frequency(self.pin, frequency)
if frequency != realized_frequency:
raise RuntimeWarning(
'A PWM frequency of {} was requested but the best that could be done was {}'.format(
frequency,
realized_frequency
)
)
self._hardware_enabled = False