from machine import Pin, PWM, Timer, ADC
from micropython import schedule
from time import ticks_ms, ticks_us, sleep
###############################################################################
# EXCEPTIONS
###############################################################################
class PWMChannelAlreadyInUse(Exception):
pass
class EventFailedScheduleQueueFull(Exception):
pass
###############################################################################
# SUPPORTING CLASSES
###############################################################################
def clamp(n, low, high): return max(low, min(n, high))
[docs]def pinout(output=True):
"""
Returns a textual representation of the Raspberry Pi pico pins and functions.
:param bool output:
If :data:`True` (the default) the pinout will be "printed".
"""
pins = """ ---usb---
GP0 1 |o o| -1 VBUS
GP1 2 |o o| -2 VSYS
GND 3 |o o| -3 GND
GP2 4 |o o| -4 3V3_EN
GP3 5 |o o| -5 3V3(OUT)
GP4 6 |o o| -6 ADC_VREF
GP5 7 |o o| -7 GP28 ADC2
GND 8 |o o| -8 GND AGND
GP6 9 |o o| -9 GP27 ADC1
GP7 10 |o o| -10 GP26 ADC0
GP8 11 |o o| -11 RUN
GP9 12 |o o| -12 GP22
GND 13 |o o| -13 GND
GP10 14 |o o| -14 GP21
GP11 15 |o o| -15 GP20
GP12 16 |o o| -16 GP19
GP13 17 |o o| -17 GP18
GND 18 |o o| -18 GND
GP14 19 |o o| -19 GP17
GP15 20 |o o| -20 GP16
---------"""
if output:
print(pins)
return pins
class PinMixin:
"""
Mixin used by devices that have a single pin number.
"""
@property
def pin(self):
"""
Returns the pin number used by the device.
"""
return self._pin_num
def __str__(self):
return "{} (pin {})".format(self.__class__.__name__, self._pin_num)
class PinsMixin:
"""
Mixin used by devices that use multiple pins.
"""
@property
def pins(self):
"""
Returns a tuple of pins used by the device.
"""
return self._pin_nums
def __str__(self):
return "{} (pins - {})".format(self.__class__.__name__, self._pin_nums)
class ValueChange:
"""
Internal class to control the value of an output device.
:param OutputDevice output_device:
The OutputDevice object you wish to change the value of.
:param generator:
A generator function that yields a 2d list of
((value, seconds), *).
The output_device's value will be set for the number of
seconds.
:param int n:
The number of times to repeat the sequence. If None, the
sequence will repeat forever.
:param bool wait:
If True the ValueChange object will block (wait) until
the sequence has completed.
"""
def __init__(self, output_device, generator, n, wait):
self._output_device = output_device
self._generator = generator
self._n = n
self._gen = self._generator()
self._timer = Timer()
self._running = True
self._wait = wait
self._set_value()
def _set_value(self, timer_obj=None):
if self._wait:
# wait for the exection to end
next_seq = self._get_value()
while next_seq is not None:
value, seconds = next_seq
self._output_device._write(value)
sleep(seconds)
next_seq = self._get_value()
else:
# run the timer
next_seq = self._get_value()
if next_seq is not None:
value, seconds = next_seq
self._output_device._write(value)
self._timer.init(period=int(seconds * 1000), mode=Timer.ONE_SHOT, callback=self._set_value)
if next_seq is None:
# the sequence has finished, turn the device off
self._output_device.off()
self._running = False
def _get_value(self):
try:
return next(self._gen)
except StopIteration:
self._n = self._n - 1 if self._n is not None else None
if self._n == 0:
# it's the end, return None
return None
else:
# recreate the generator and start again
self._gen = self._generator()
return next(self._gen)
def stop(self):
"""
Stops the ValueChange object running.
"""
self._running = False
self._timer.deinit()
###############################################################################
# OUTPUT DEVICES
###############################################################################
class OutputDevice:
"""
Base class for output devices.
"""
def __init__(self, active_high=True, initial_value=False):
self.active_high = active_high
if initial_value is not None:
self._write(initial_value)
self._value_changer = None
@property
def active_high(self):
"""
Sets or returns the active_high property. If :data:`True`, the
:meth:`on` method will set the Pin to HIGH. If :data:`False`,
the :meth:`on` method will set the Pin to LOW (the :meth:`off` method
always does the opposite).
"""
return self._active_state
@active_high.setter
def active_high(self, value):
self._active_state = True if value else False
self._inactive_state = False if value else True
@property
def value(self):
"""
Sets or returns a value representing the state of the device: 1 is on, 0 is off.
"""
return self._read()
@value.setter
def value(self, value):
self._stop_change()
self._write(value)
def on(self, value=1, t=None, wait=False):
"""
Turns the device on.
:param float value:
The value to set when turning on. Defaults to 1.
:param float t:
The time in seconds that the device should be on. If None is
specified, the device will stay on. The default is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the device will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
if t is None:
self.value = value
else:
self._start_change(lambda : iter([(value, t), ]), 1, wait)
def off(self):
"""
Turns the device off.
"""
self.value = 0
@property
def is_active(self):
"""
Returns :data:`True` if the device is on.
"""
return bool(self.value)
def toggle(self):
"""
If the device is off, turn it on. If it is on, turn it off.
"""
if self.is_active:
self.off()
else:
self.on()
def blink(self, on_time=1, off_time=None, n=None, wait=False):
"""
Makes the device turn on and off repeatedly.
:param float on_time:
The length of time in seconds that the device will be on. Defaults to 1.
:param float off_time:
The length of time in seconds that the device will be off. If `None`,
it will be the same as ``on_time``. Defaults to `None`.
:param int n:
The number of times to repeat the blink operation. If None is
specified, the device will continue blinking forever. The default
is None.
:param bool wait:
If True, the method will block until the device stops turning on and off.
If False, the method will return and the device will turn on and off in
the background. Defaults to False.
"""
off_time = on_time if off_time is None else off_time
self.off()
# is there anything to change?
if on_time > 0 or off_time > 0:
self._start_change(lambda : iter([(1,on_time), (0,off_time)]), n, wait)
def _start_change(self, generator, n, wait):
self._value_changer = ValueChange(self, generator, n, wait)
def _stop_change(self):
if self._value_changer is not None:
self._value_changer.stop()
self._value_changer = None
def close(self):
"""
Turns the device off.
"""
self.value = 0
[docs]class DigitalOutputDevice(OutputDevice, PinMixin):
"""
Represents a device driven by a digital pin.
:param int pin:
The pin that the device is connected to.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the LED will be off initially. If
:data:`True`, the LED will be switched on initially.
"""
def __init__(self, pin, active_high=True, initial_value=False):
self._pin_num = pin
self._pin = Pin(pin, Pin.OUT)
super().__init__(active_high, initial_value)
def _value_to_state(self, value):
return int(self._active_state if value else self._inactive_state)
def _state_to_value(self, state):
return int(bool(state) == self._active_state)
def _read(self):
return self._state_to_value(self._pin.value())
def _write(self, value):
self._pin.value(self._value_to_state(value))
[docs] def close(self):
"""
Closes the device and turns the device off. Once closed, the device
can no longer be used.
"""
super().close()
self._pin = None
[docs]class DigitalLED(DigitalOutputDevice):
"""
Represents a simple LED, which can be switched on and off.
:param int pin:
The pin that the device is connected to.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the LED will be off initially. If
:data:`True`, the LED will be switched on initially.
"""
pass
DigitalLED.is_lit = DigitalLED.is_active
[docs]class Buzzer(DigitalOutputDevice):
"""
Represents an active or passive buzzer, which can be turned on or off.
:param int pin:
The pin that the device is connected to.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the Buzzer will be off initially. If
:data:`True`, the Buzzer will be switched on initially.
"""
pass
Buzzer.beep = Buzzer.blink
[docs]class PWMOutputDevice(OutputDevice, PinMixin):
"""
Represents a device driven by a PWM pin.
:param int pin:
The pin that the device is connected to.
:param int freq:
The frequency of the PWM signal in hertz. Defaults to 100.
:param int duty_factor:
The duty factor of the PWM signal. This is a value between 0 and 65535.
Defaults to 65535.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the LED will be off initially. If
:data:`True`, the LED will be switched on initially.
"""
PIN_TO_PWM_CHANNEL = ["0A","0B","1A","1B","2A","2B","3A","3B","4A","4B","5A","5B","6A","6B","7A","7B","0A","0B","1A","1B","2A","2B","3A","3B","4A","4B","5A","5B","6A","6B"]
_channels_used = {}
def __init__(self, pin, freq=100, duty_factor=65535, active_high=True, initial_value=False):
self._check_pwm_channel(pin)
self._pin_num = pin
self._duty_factor = duty_factor
self._pwm = PWM(Pin(pin))
self._pwm.freq(freq)
super().__init__(active_high, initial_value)
def _check_pwm_channel(self, pin_num):
channel = PWMOutputDevice.PIN_TO_PWM_CHANNEL[pin_num]
if channel in PWMOutputDevice._channels_used.keys():
raise PWMChannelAlreadyInUse(
"PWM channel {} is already in use by {}. Use a different pin".format(
channel,
str(PWMOutputDevice._channels_used[channel])
)
)
else:
PWMOutputDevice._channels_used[channel] = self
def _state_to_value(self, state):
return (state if self.active_high else self._duty_factor - state) / self._duty_factor
def _value_to_state(self, value):
return int(self._duty_factor * (value if self.active_high else 1 - value))
def _read(self):
return self._state_to_value(self._pwm.duty_u16())
def _write(self, value):
self._pwm.duty_u16(self._value_to_state(value))
@property
def is_active(self):
"""
Returns :data:`True` if the device is on.
"""
return self.value != 0
@property
def freq(self):
"""
Returns the current frequency of the device.
"""
return self._pwm.freq()
@freq.setter
def freq(self, freq):
"""
Sets the frequency of the device.
"""
self._pwm.freq(freq)
[docs] def blink(self, on_time=1, off_time=None, n=None, wait=False, fade_in_time=0, fade_out_time=None, fps=25):
"""
Makes the device turn on and off repeatedly.
:param float on_time:
The length of time in seconds the device will be on. Defaults to 1.
:param float off_time:
The length of time in seconds the device will be off. If `None`,
it will be the same as ``on_time``. Defaults to `None`.
:param int n:
The number of times to repeat the blink operation. If `None`, the
device will continue blinking forever. The default is `None`.
:param bool wait:
If True, the method will block until the LED stops blinking. If False,
the method will return and the LED will blink in the background.
Defaults to False.
:param float fade_in_time:
The length of time in seconds to spend fading in. Defaults to 0.
:param float fade_out_time:
The length of time in seconds to spend fading out. If `None`,
it will be the same as ``fade_in_time``. Defaults to `None`.
:param int fps:
The frames per second that will be used to calculate the number of
steps between off/on states when fading. Defaults to 25.
"""
self.off()
off_time = on_time if off_time is None else off_time
fade_out_time = fade_in_time if fade_out_time is None else fade_out_time
def blink_generator():
if fade_in_time > 0:
for s in [
(i * (1 / fps) / fade_in_time, 1 / fps)
for i in range(int(fps * fade_in_time))
]:
yield s
if on_time > 0:
yield (1, on_time)
if fade_out_time > 0:
for s in [
(1 - (i * (1 / fps) / fade_out_time), 1 / fps)
for i in range(int(fps * fade_out_time))
]:
yield s
if off_time > 0:
yield (0, off_time)
# is there anything to change?
if on_time > 0 or off_time > 0 or fade_in_time > 0 or fade_out_time > 0:
self._start_change(blink_generator, n, wait)
[docs] def pulse(self, fade_in_time=1, fade_out_time=None, n=None, wait=False, fps=25):
"""
Makes the device pulse on and off repeatedly.
:param float fade_in_time:
The length of time in seconds that the device will take to turn on.
Defaults to 1.
:param float fade_out_time:
The length of time in seconds that the device will take to turn off.
Defaults to 1.
:param int fps:
The frames per second that will be used to calculate the number of
steps between off/on states. Defaults to 25.
:param int n:
The number of times to pulse the LED. If None, the LED will pulse
forever. Defaults to None.
:param bool wait:
If True, the method will block until the LED stops pulsing. If False,
the method will return and the LED will pulse in the background.
Defaults to False.
"""
self.blink(on_time=0, off_time=0, fade_in_time=fade_in_time, fade_out_time=fade_out_time, n=n, wait=wait, fps=fps)
[docs] def close(self):
"""
Closes the device and turns the device off. Once closed, the device
can no longer be used.
"""
super().close()
del PWMOutputDevice._channels_used[
PWMOutputDevice.PIN_TO_PWM_CHANNEL[self._pin_num]
]
self._pwm.deinit()
self._pwm = None
[docs]class PWMLED(PWMOutputDevice):
"""
Represents an LED driven by a PWM pin; the brightness of the LED can be changed.
:param int pin:
The pin that the device is connected to.
:param int freq:
The frequency of the PWM signal in hertz. Defaults to 100.
:param int duty_factor:
The duty factor of the PWM signal. This is a value between 0 and 65535.
Defaults to 65535.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the LED will be off initially. If
:data:`True`, the LED will be switched on initially.
"""
PWMLED.brightness = PWMLED.value
[docs]def LED(pin, pwm=True, active_high=True, initial_value=False):
"""
Returns an instance of :class:`DigitalLED` or :class:`PWMLED` depending on
the value of the `pwm` parameter.
::
from picozero import LED
my_pwm_led = LED(1)
my_digital_led = LED(2, pwm=False)
:param int pin:
The pin that the device is connected to.
:param int pin:
If `pwm` is :data:`True` (the default), a :class:`PWMLED` will be
returned. If `pwm` is :data:`False`, a :class:`DigitalLED` will be
returned. A :class:`PWMLED` can control the brightness of the LED but
uses 1 PWM channel.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the device will be off initially. If
:data:`True`, the device will be switched on initially.
"""
if pwm:
return PWMLED(
pin=pin,
active_high=active_high,
initial_value=initial_value)
else:
return DigitalLED(
pin=pin,
active_high=active_high,
initial_value=initial_value)
try:
pico_led = LED("LED", pwm=False)
except TypeError:
# older version of micropython before "LED" was supported
pico_led = LED(25, pwm=False)
[docs]class PWMBuzzer(PWMOutputDevice):
"""
Represents a passive buzzer driven by a PWM pin; the volume of the buzzer can be changed.
:param int pin:
The pin that the buzzer is connected to.
:param int freq:
The frequency of the PWM signal in hertz. Defaults to 440.
:param int duty_factor:
The duty factor of the PWM signal. This is a value between 0 and 65535.
Defaults to 1023.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the buzzer will be off initially. If
:data:`True`, the buzzer will be switched on initially.
"""
def __init__(self, pin, freq=440, duty_factor=1023, active_high=True, initial_value=False):
super().__init__(pin, freq, duty_factor, active_high, initial_value)
PWMBuzzer.volume = PWMBuzzer.value
PWMBuzzer.beep = PWMBuzzer.blink
[docs]class Speaker(OutputDevice, PinMixin):
"""
Represents a speaker driven by a PWM pin.
:param int pin:
The pin that the speaker is connected to.
:param int initial_freq:
The initial frequency of the PWM signal in hertz. Defaults to 440.
:param int initial_volume:
The initial volume of the PWM signal. This is a value between 0 and
1. Defaults to 0.
:param int duty_factor:
The duty factor of the PWM signal. This is a value between 0 and 65535.
Defaults to 1023.
:param bool active_high:
If :data:`True` (the default), the :meth:`on` method will set the Pin
to HIGH. If :data:`False`, the :meth:`on` method will set the Pin to
LOW (the :meth:`off` method always does the opposite).
"""
NOTES = {
'b0': 31, 'c1': 33, 'c#1': 35, 'd1': 37, 'd#1': 39, 'e1': 41, 'f1': 44, 'f#1': 46, 'g1': 49,'g#1': 52, 'a1': 55,
'a#1': 58, 'b1': 62, 'c2': 65, 'c#2': 69, 'd2': 73, 'd#2': 78,
'e2': 82, 'f2': 87, 'f#2': 93, 'g2': 98, 'g#2': 104, 'a2': 110, 'a#2': 117, 'b2': 123,
'c3': 131, 'c#3': 139, 'd3': 147, 'd#3': 156, 'e3': 165, 'f3': 175, 'f#3': 185, 'g3': 196, 'g#3': 208, 'a3': 220, 'a#3': 233, 'b3': 247,
'c4': 262, 'c#4': 277, 'd4': 294, 'd#4': 311, 'e4': 330, 'f4': 349, 'f#4': 370, 'g4': 392, 'g#4': 415, 'a4': 440, 'a#4': 466, 'b4': 494,
'c5': 523, 'c#5': 554, 'd5': 587, 'd#5': 622, 'e5': 659, 'f5': 698, 'f#5': 740, 'g5': 784, 'g#5': 831, 'a5': 880, 'a#5': 932, 'b5': 988,
'c6': 1047, 'c#6': 1109, 'd6': 1175, 'd#6': 1245, 'e6': 1319, 'f6': 1397, 'f#6': 1480, 'g6': 1568, 'g#6': 1661, 'a6': 1760, 'a#6': 1865, 'b6': 1976,
'c7': 2093, 'c#7': 2217, 'd7': 2349, 'd#7': 2489,
'e7': 2637, 'f7': 2794, 'f#7': 2960, 'g7': 3136, 'g#7': 3322, 'a7': 3520, 'a#7': 3729, 'b7': 3951,
'c8': 4186, 'c#8': 4435, 'd8': 4699, 'd#8': 4978
}
def __init__(self, pin, initial_freq=440, initial_volume=0, duty_factor=1023, active_high=True):
self._pin_num = pin
self._pwm_buzzer = PWMBuzzer(
pin,
freq=initial_freq,
duty_factor=duty_factor,
active_high=active_high,
initial_value=None,
)
super().__init__(active_high, None)
self.volume = initial_volume
[docs] def on(self, volume=1):
self.volume = volume
[docs] def off(self):
self.volume = 0
@property
def value(self):
"""
Sets or returns the value of the speaker. The value is a tuple of (freq, volume).
"""
return tuple(self.freq, self.volume)
@value.setter
def value(self, value):
self._stop_change()
self._write(value)
@property
def volume(self):
"""
Sets or returns the volume of the speaker: 1 for maximum volume, 0 for off.
"""
return self._volume
@volume.setter
def volume(self, value):
self._volume = value
self.value = (self.freq, self.volume)
@property
def freq(self):
"""
Sets or returns the current frequency of the speaker.
"""
return self._pwm_buzzer.freq
@freq.setter
def freq(self, freq):
self.value = (freq, self.volume)
def _write(self, value):
# set the frequency
if value[0] is not None:
self._pwm_buzzer.freq = value[0]
# write the volume value
if value[1] is not None:
self._pwm_buzzer.volume = value[1]
def _to_freq(self, freq):
if freq is not None and freq != '' and freq != 0:
if type(freq) is str:
return int(self.NOTES[freq])
elif freq <= 128 and freq > 0: # MIDI
midi_factor = 2**(1/12)
return int(440 * midi_factor ** (freq - 69))
else:
return freq
else:
return None
[docs] def beep(self, on_time=1, off_time=None, n=None, wait=False, fade_in_time=0, fade_out_time=None, fps=25):
"""
Makes the buzzer turn on and off repeatedly.
:param float on_time:
The length of time in seconds that the device will be on. Defaults to 1.
:param float off_time:
The length of time in seconds that the device will be off. If `None`,
it will be the same as ``on_time``. Defaults to `None`.
:param int n:
The number of times to repeat the beep operation. If `None`, the
device will continue beeping forever. The default is `None`.
:param bool wait:
If True, the method will block until the buzzer stops beeping. If False,
the method will return and the buzzer will beep in the background.
Defaults to False.
:param float fade_in_time:
The length of time in seconds to spend fading in. Defaults to 0.
:param float fade_out_time:
The length of time in seconds to spend fading out. If `None`,
it will be the same as ``fade_in_time``. Defaults to `None`.
:param int fps:
The frames per second that will be used to calculate the number of
steps between off/on states when fading. Defaults to 25.
"""
self._pwm_buzzer.blink(on_time, off_time, n, wait, fade_in_time, fade_out_time, fps)
[docs] def play(self, tune=440, duration=1, volume=1, n=1, wait=True):
"""
Plays a tune for a given duration.
:param int tune:
The tune to play can be specified as:
+ a single "note", represented as:
+ a frequency in Hz e.g. `440`
+ a midi note e.g. `60`
+ a note name as a string e.g. `"E4"`
+ a list of notes and duration e.g. `[440, 1]` or `["E4", 2]`
+ a list of two value tuples of (note, duration) e.g. `[(440,1), (60, 2), ("e4", 3)]`
Defaults to `440`.
:param int volume:
The volume of the tune; 1 is maximum volume, 0 is mute. Defaults to 1.
:param float duration:
The duration of each note in seconds. Defaults to 1.
:param int n:
The number of times to play the tune. If None, the tune will play
forever. Defaults to 1.
:param bool wait:
If True, the method will block until the tune has finished. If False,
the method will return and the tune will play in the background.
Defaults to True.
"""
self.off()
# tune isn't a list, so it must be a single frequency or note
if not isinstance(tune, (list, tuple)):
tune = [(tune, duration)]
# if the first element isn't a list, then it must be list of a single note and duration
elif not isinstance(tune[0], (list, tuple)):
tune = [tune]
def tune_generator():
for note in tune:
# note isn't a list or tuple, it must be a single frequency or note
if not isinstance(note, (list, tuple)):
# make it into a tuple
note = (note, duration)
# turn the notes into frequencies
freq = self._to_freq(note[0])
freq_duration = note[1]
freq_volume = volume if freq is not None else 0
# if this is a tune of greater than 1 note, add gaps between notes
if len(tune) == 1:
yield ((freq, freq_volume), freq_duration)
else:
yield ((freq, freq_volume), freq_duration * 0.9)
yield ((freq, 0), freq_duration * 0.1)
self._start_change(tune_generator, n, wait)
[docs] def close(self):
self._pwm_buzzer.close()
[docs]class RGBLED(OutputDevice, PinsMixin):
"""
Extends :class:`OutputDevice` and represents a full colour LED component (composed
of red, green, and blue LEDs).
Connect the common cathode (longest leg) to a ground pin; connect each of
the other legs (representing the red, green, and blue anodes) to any GP
pins. You should use three limiting resistors (one per anode).
The following code will make the LED yellow::
from picozero import RGBLED
rgb = RGBLED(1, 2, 3)
rgb.color = (1, 1, 0)
0–255 colours are also supported::
rgb.color = (255, 255, 0)
:type red: int
:param red:
The GP pin that controls the red component of the RGB LED.
:type green: int
:param green:
The GP pin that controls the green component of the RGB LED.
:type blue: int
:param blue:
The GP pin that controls the blue component of the RGB LED.
:param bool active_high:
Set to :data:`True` (the default) for common cathode RGB LEDs. If you
are using a common anode RGB LED, set this to :data:`False`.
:type initial_value: ~colorzero.Color or tuple
:param initial_value:
The initial color for the RGB LED. Defaults to black ``(0, 0, 0)``.
:param bool pwm:
If :data:`True` (the default), construct :class:`PWMLED` instances for
each component of the RGBLED. If :data:`False`, construct
:class:`DigitalLED` instances.
"""
def __init__(self, red=None, green=None, blue=None, active_high=True,
initial_value=(0, 0, 0), pwm=True):
self._pin_nums = (red, green, blue)
self._leds = ()
self._last = initial_value
LEDClass = PWMLED if pwm else DigitalLED
self._leds = tuple(
LEDClass(pin, active_high=active_high)
for pin in (red, green, blue))
super().__init__(active_high, initial_value)
def _write(self, value):
if type(value) is not tuple:
value = (value, ) * 3
for led, v in zip(self._leds, value):
led.value = v
@property
def value(self):
"""
Represents the colour of the LED as an RGB 3-tuple of ``(red, green,
blue)`` where each value is between 0 and 1 if *pwm* was :data:`True`
when the class was constructed (but only takes values of 0 or 1 otherwise).
For example, red would be ``(1, 0, 0)`` and yellow would be ``(1, 1,
0)``, whereas orange would be ``(1, 0.5, 0)``.
"""
return tuple(led.value for led in self._leds)
@value.setter
def value(self, value):
self._stop_change()
self._write(value)
@property
def is_active(self):
"""
Returns :data:`True` if the LED is currently active (not black) and
:data:`False` otherwise.
"""
return self.value != (0, 0, 0)
is_lit = is_active
def _to_255(self, value):
return round(value * 255)
def _from_255(self, value):
return 0 if value == 0 else value / 255
@property
def color(self):
"""
Represents the colour of the LED as an RGB 3-tuple of ``(red, green,
blue)`` where each value is between 0 and 255 if *pwm* was :data:`True`
when the class was constructed (but only takes values of 0 or 255 otherwise).
For example, red would be ``(255, 0, 0)`` and yellow would be ``(255, 255,
0)``, whereas orange would be ``(255, 127, 0)``.
"""
return tuple(self._to_255(v) for v in self.value)
@color.setter
def color(self, value):
self.value = tuple(self._from_255(v) for v in value)
@property
def red(self):
"""
Represents the red component of the LED as a value between 0 and 255 if *pwm* was :data:`True`
when the class was constructed (but only takes values of 0 or 255 otherwise).
"""
return self._to_255(self.value[0])
@red.setter
def red(self, value):
r, g, b = self.value
self.value = self._from_255(value), g, b
@property
def green(self):
"""
Represents the green component of the LED as a value between 0 and 255 if *pwm* was :data:`True`
when the class was constructed (but only takes values of 0 or 255 otherwise).
"""
return self._to_255(self.value[1])
@green.setter
def green(self, value):
r, g, b = self.value
self.value = r, self._from_255(value), b
@property
def blue(self):
"""
Represents the blue component of the LED as a value between 0 and 255 if *pwm* was :data:`True`
when the class was constructed (but only takes values of 0 or 255 otherwise).
"""
return self._to_255(self.value[2])
@blue.setter
def blue(self, value):
r, g, b = self.value
self.value = r, g, self._from_255(value)
[docs] def on(self):
"""
Turns the LED on. This is equivalent to setting the LED color to white, e.g.
``(1, 1, 1)``.
"""
self.value = (1, 1, 1)
[docs] def invert(self):
"""
Inverts the state of the device. If the device is currently off
(:attr:`value` is ``(0, 0, 0)``), this changes it to "fully" on
(:attr:`value` is ``(1, 1, 1)``). If the device has a specific colour,
this method inverts the colour.
"""
r, g, b = self.value
self.value = (1 - r, 1 - g, 1 - b)
[docs] def toggle(self):
"""
Toggles the state of the device. If the device has a specific colour, then that colour is saved and the device is turned off.
If the device is off, it will be changed to the last colour it had when it was on or, if none, to fully on (:attr:`value` is ``(1, 1, 1)``).
"""
if self.value == (0, 0, 0):
self.value = self._last or (1, 1, 1)
else:
self._last = self.value
self.value = (0, 0, 0)
[docs] def blink(self, on_times=1, fade_times=0, colors=((1, 0, 0), (0, 1, 0), (0, 0, 1)), n=None, wait=False, fps=25):
"""
Makes the device blink between colours repeatedly.
:param float on_times:
Single value or tuple of numbers of seconds to stay on each colour. Defaults to 1 second.
:param float fade_times:
Single value or tuple of times to fade between each colour. Must be 0 if
*pwm* was :data:`False` when the class was constructed.
:type colors: tuple
Tuple of colours to blink between, use ``(0, 0, 0)`` for off.
:param colors:
The colours to blink between. Defaults to red, green, blue.
:type n: int or None
:param n:
Number of times to blink; :data:`None` (the default) means forever.
:param bool wait:
If :data:`False` (the default), use a Timer to manage blinking,
continue blinking, and return immediately. If :data:`False`, only
return when the blinking is finished (warning: the default value of
*n* will result in this method never returning).
"""
self.off()
if type(on_times) is not tuple:
on_times = (on_times, ) * len(colors)
if type(fade_times) is not tuple:
fade_times = (fade_times, ) * len(colors)
# If any value is above zero then treat all as 0-255 values
if any(v > 1 for v in sum(colors, ())):
colors = tuple(tuple(self._from_255(v) for v in t) for t in colors)
def blink_generator():
# Define a linear interpolation between
# off_color and on_color
lerp = lambda t, fade_in, color1, color2: tuple(
(1 - t) * off + t * on
if fade_in else
(1 - t) * on + t * off
for off, on in zip(color2, color1)
)
for c in range(len(colors)):
if on_times[c] > 0:
yield (colors[c], on_times[c])
if fade_times[c] > 0:
for i in range(int(fps * fade_times[c])):
v = lerp(i * (1 / fps) / fade_times[c], True, colors[(c + 1) % len(colors)], colors[c])
t = 1 / fps
yield (v, t)
self._start_change(blink_generator, n, wait)
[docs] def pulse(self, fade_times=1, colors=((0, 0, 0), (1, 0, 0), (0, 0, 0), (0, 1, 0), (0, 0, 0), (0, 0, 1)), n=None, wait=False, fps=25):
"""
Makes the device fade between colours repeatedly.
:param float fade_times:
Single value or tuple of numbers of seconds to spend fading. Defaults to 1.
:param float fade_out_time:
Number of seconds to spend fading out. Defaults to 1.
:type colors: tuple
:param on_color:
Tuple of colours to pulse between in order. Defaults to red, off, green, off, blue, off.
:type off_color: ~colorzero.Color or tuple
:type n: int or None
:param n:
Number of times to pulse; :data:`None` (the default) means forever.
"""
on_times = 0
self.blink(on_times, fade_times, colors, n, wait, fps)
[docs] def cycle(self, fade_times=1, colors=((1, 0, 0), (0, 1, 0), (0, 0, 1)), n=None, wait=False, fps=25):
"""
Makes the device fade in and out repeatedly.
:param float fade_times:
Single value or tuple of numbers of seconds to spend fading between colours. Defaults to 1.
:param float fade_times:
Number of seconds to spend fading out. Defaults to 1.
:type colors: tuple
:param on_color:
Tuple of colours to cycle between. Defaults to red, green, blue.
:type n: int or None
:param n:
Number of times to cycle; :data:`None` (the default) means forever.
"""
on_times = 0
self.blink(on_times, fade_times, colors, n, wait, fps)
[docs] def close(self):
super().close()
for led in self._leds:
led.close()
self._leds = None
RGBLED.colour = RGBLED.color
[docs]class Motor(PinsMixin):
"""
Represents a motor connected to a motor controller that has a two-pin
input. One pin drives the motor "forward", the other drives the motor
"backward".
:type forward: int
:param forward:
The GP pin that controls the "forward" motion of the motor.
:type backward: int
:param backward:
The GP pin that controls the "backward" motion of the motor.
:param bool pwm:
If :data:`True` (the default), PWM pins are used to drive the motor.
When using PWM pins, values between 0 and 1 can be used to set the
speed.
"""
def __init__(self, forward, backward, pwm=True):
self._pin_nums = (forward, backward)
self._forward = PWMOutputDevice(forward) if pwm else DigitalOutputDevice(forward)
self._backward = PWMOutputDevice(backward) if pwm else DigitalOutputDevice(backward)
[docs] def on(self, speed=1, t=None, wait=False):
"""
Turns the motor on and makes it turn.
:param float speed:
The speed as a value between -1 and 1: 1 turns the motor at
full speed in one direction, -1 turns the motor at full speed in
the opposite direction. Defaults to 1.
:param float t:
The time in seconds that the motor should run for. If None is
specified, the motor will stay on. The default is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
if speed > 0:
self._backward.off()
self._forward.on(speed, t, wait)
elif speed < 0:
self._forward.off()
self._backward.on(-speed, t, wait)
else:
self.off()
[docs] def off(self):
"""
Stops the motor turning.
"""
self._backward.off()
self._forward.off()
@property
def value(self):
"""
Sets or returns the motor speed as a value between -1 and 1: -1 is full
speed "backward", 1 is full speed "forward", 0 is stopped.
"""
return self._forward.value + (-self._backward.value)
@value.setter
def value(self, value):
if value != 0:
self.on(value)
else:
self.stop()
[docs] def forward(self, speed=1, t=None, wait=False):
"""
Makes the motor turn "forward".
:param float speed:
The speed as a value between 0 and 1: 1 is full speed, 0 is stop. Defaults to 1.
:param float t:
The time in seconds that the motor should turn for. If None is
specified, the motor will stay on. The default is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
self.on(speed, t, wait)
[docs] def backward(self, speed=1, t=None, wait=False):
"""
Makes the motor turn "backward".
:param float speed:
The speed as a value between 0 and 1: 1 is full speed, 0 is stop. Defaults to 1.
:param float t:
The time in seconds that the motor should turn for. If None is
specified, the motor will stay on. The default is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
self.on(-speed, t, wait)
[docs] def close(self):
"""
Closes the device and releases any resources. Once closed, the device
can no longer be used.
"""
self._forward.close()
self._backward.close()
Motor.start = Motor.on
Motor.stop = Motor.off
[docs]class Robot:
"""
Represents a generic dual-motor robot / rover / buggy.
Alias for :class:`Rover`.
This class is constructed with two tuples representing the forward and
backward pins of the left and right controllers. For example,
if the left motor's controller is connected to pins 12 and 13, while the
right motor's controller is connected to pins 14 and 15, then the following
example will drive the robot forward::
from picozero import Robot
robot = Robot(left=(12, 13), right=(14, 15))
robot.forward()
:param tuple left:
A tuple of two pins representing the forward and backward inputs of the
left motor's controller.
:param tuple right:
A tuple of two pins representing the forward and backward inputs of the
right motor's controller.
:param bool pwm:
If :data:`True` (the default), pwm pins will be used, allowing variable
speed control.
"""
def __init__(self, left, right, pwm=True):
self._left = Motor(left[0], left[1], pwm)
self._right = Motor(right[0], right[1], pwm)
@property
def left_motor(self):
"""
Returns the left :class:`Motor`.
"""
return self._left
@property
def right_motor(self):
"""
Returns the right :class:`Motor`.
"""
return self._right
@property
def value(self):
"""
Represents the motion of the robot as a tuple of (left_motor_speed,
right_motor_speed) with ``(-1, -1)`` representing full speed backwards,
``(1, 1)`` representing full speed forwards, and ``(0, 0)``
representing stopped.
"""
return (self._left.value, self._right.value)
@value.setter
def value(self, value):
self._left.value, self._right.value = value
[docs] def forward(self, speed=1, t=None, wait=False):
"""
Makes the robot move "forward".
:param float speed:
The speed as a value between 0 and 1: 1 is full speed, 0 is stop. Defaults to 1.
:param float t:
The time in seconds that the robot should move for. If None is
specified, the robot will continue to move until stopped. The default
is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
self._left.forward(speed, t, False)
self._right.forward(speed, t, wait)
[docs] def backward(self, speed=1, t=None, wait=False):
"""
Makes the robot move "backward".
:param float speed:
The speed as a value between 0 and 1: 1 is full speed, 0 is stop. Defaults to 1.
:param float t:
The time in seconds that the robot should move for. If None is
specified, the robot will continue to move until stopped. The default
is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
self._left.backward(speed, t, False)
self._right.backward(speed, t, wait)
[docs] def left(self, speed=1, t=None, wait=False):
"""
Makes the robot turn "left" by turning the left motor backward and the
right motor forward.
:param float speed:
The speed as a value between 0 and 1: 1 is full speed, 0 is stop. Defaults to 1.
:param float t:
The time in seconds that the robot should turn for. If None is
specified, the robot will continue to turn until stopped. The default
is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
self._left.backward(speed, t, False)
self._right.forward(speed, t, wait)
[docs] def right(self, speed=1, t=None, wait=False):
"""
Makes the robot turn "right" by turning the left motor forward and the
right motor backward.
:param float speed:
The speed as a value between 0 and 1: 1 is full speed, 0 is stop. Defaults to 1.
:param float t:
The time in seconds that the robot should turn for. If None is
specified, the robot will continue to turn until stopped. The default
is None.
:param bool wait:
If True, the method will block until the time `t` has expired.
If False, the method will return and the motor will turn on in
the background. Defaults to False. Only effective if `t` is not
None.
"""
self._left.forward(speed, t, False)
self._right.backward(speed, t, wait)
[docs] def stop(self):
"""
Stops the robot.
"""
self._left.stop()
self._right.stop()
[docs] def close(self):
"""
Closes the device and releases any resources. Once closed, the device
can no longer be used.
"""
self._left.close()
self._right.close()
Rover = Robot
[docs]class Servo(PWMOutputDevice):
"""
Represents a PWM-controlled servo motor.
Setting the `value` to 0 will move the servo to its minimum position,
1 will move the servo to its maximum position. Setting the `value` to
:data:`None` will turn the servo "off" (i.e. no signal is sent).
:type pin: int
:param pin:
The pin the servo motor is connected to.
:param bool initial_value:
If :data:`0`, the servo will be set to its minimum position. If
:data:`1`, the servo will set to its maximum position. If :data:`None`
(the default), the position of the servo will not change.
:param float min_pulse_width:
The pulse width corresponding to the servo's minimum position. This
defaults to 1ms.
:param float max_pulse_width:
The pulse width corresponding to the servo's maximum position. This
defaults to 2ms.
:param float frame_width:
The length of time between servo control pulses measured in seconds.
This defaults to 20ms which is a common value for servos.
:param int duty_factor:
The duty factor of the PWM signal. This is a value between 0 and 65535.
Defaults to 65535.
"""
def __init__(self, pin, initial_value=None, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000, duty_factor=65535):
self._min_duty = int((min_pulse_width / frame_width) * duty_factor)
self._max_duty = int((max_pulse_width / frame_width) * duty_factor)
super().__init__(pin, freq=int(1 / frame_width), duty_factor=duty_factor, initial_value=initial_value)
def _state_to_value(self, state):
return None if state == 0 else clamp((state - self._min_duty) / (self._max_duty - self._min_duty), 0, 1)
def _value_to_state(self, value):
return 0 if value is None else int(self._min_duty + ((self._max_duty - self._min_duty) * value))
[docs] def min(self):
"""
Set the servo to its minimum position.
"""
self.value = 0
[docs] def mid(self):
"""
Set the servo to its mid-point position.
"""
self.value = 0.5
[docs] def max(self):
"""
Set the servo to its maximum position.
"""
self.value = 1
[docs] def off(self):
"""
Turn the servo "off" by setting the value to `None`.
"""
self.value = None
###############################################################################
# INPUT DEVICES
###############################################################################
class InputDevice:
"""
Base class for input devices.
"""
def __init__(self, active_state=None):
self._active_state = active_state
@property
def active_state(self):
"""
Sets or returns the active state of the device. If :data:`None` (the default),
the device will return the value that the pin is set to. If
:data:`True`, the device will return :data:`True` if the pin is
HIGH. If :data:`False`, the device will return :data:`False` if the
pin is LOW.
"""
return self._active_state
@active_state.setter
def active_state(self, value):
self._active_state = True if value else False
self._inactive_state = False if value else True
@property
def value(self):
"""
Returns the current value of the device. This is either :data:`True`
or :data:`False` depending on the value of :attr:`active_state`.
"""
return self._read()
[docs]class Switch(DigitalInputDevice):
"""
Represents a toggle switch, which is either open or closed.
:param int pin:
The pin that the device is connected to.
:param bool pull_up:
If :data:`True` (the default), the device will be pulled up to
HIGH. If :data:`False`, the device will be pulled down to LOW.
:param float bounce_time:
The bounce time for the device. If set, the device will ignore
any button presses that happen within the bounce time after a
button release. This is useful to prevent accidental button
presses from registering as multiple presses. Defaults to 0.02
seconds.
"""
def __init__(self, pin, pull_up=True, bounce_time=0.02):
super().__init__(pin=pin, pull_up=pull_up, bounce_time=bounce_time)
Switch.is_closed = Switch.is_active
Switch.is_open = Switch.is_inactive
Switch.when_closed = Switch.when_activated
Switch.when_opened = Switch.when_deactivated
Button.is_pressed = Button.is_active
Button.is_released = Button.is_inactive
Button.when_pressed = Button.when_activated
Button.when_released = Button.when_deactivated
class AnalogInputDevice(InputDevice, PinMixin):
"""
Represents a generic input device with analogue functionality, e.g.
a potentiometer.
:param int pin:
The pin that the device is connected to.
:param active_state:
The active state of the device. If :data:`True` (the default),
the :class:`AnalogInputDevice` will assume that the device is
active when the pin is high and above the threshold. If
``active_state`` is ``False``, the device will be active when
the pin is low and below the threshold.
:param float threshold:
The threshold that the device must be above or below to be
considered active. The default is 0.5.
"""
def __init__(self, pin, active_state=True, threshold=0.5):
self._pin_num = pin
super().__init__(active_state)
self._adc = ADC(pin)
self._threshold = float(threshold)
def _state_to_value(self, state):
return (state if self.active_state else 65535 - state) / 65535
def _value_to_state(self, value):
return int(65535 * (value if self.active_state else 1 - value))
def _read(self):
return self._state_to_value(self._adc.read_u16())
@property
def threshold(self):
"""
The threshold that the device must be above or below to be
considered active. The default is 0.5.
"""
return self._threshold
@threshold.setter
def threshold(self, value):
self._threshold = float(value)
@property
def is_active(self):
"""
Returns :data:`True` if the device is active.
"""
return self.value > self.threshold
@property
def voltage(self):
"""
Returns the voltage of the analogue device.
"""
return self.value * 3.3
def close(self):
self._adc = None
[docs]class Potentiometer(AnalogInputDevice):
"""
Represents a potentiometer, which outputs a variable voltage
between 0 and 3.3V.
Alias for :class:`Pot`.
:param int pin:
The pin that the device is connected to.
:param active_state:
The active state of the device. If :data:`True` (the default),
the :class:`AnalogInputDevice` will assume that the device is
active when the pin is high and above the threshold. If
``active_state`` is ``False``, the device will be active when
the pin is low and below the threshold.
:param float threshold:
The threshold that the device must be above or below to be
considered active. The default is 0.5.
"""
pass
Pot = Potentiometer
def pico_temp_conversion(voltage):
# Formula for calculating temp from voltage for the onboard temperature sensor
return 27 - (voltage - 0.706)/0.001721
[docs]class TemperatureSensor(AnalogInputDevice):
"""
Represents a TemperatureSensor, which outputs a variable voltage. The voltage
can be converted to a temperature using a `conversion` function passed as a
parameter.
Alias for :class:`Thermistor` and :class:`TempSensor`.
:param int pin:
The pin that the device is connected to.
:param active_state:
The active state of the device. If :data:`True` (the default),
the :class:`AnalogInputDevice` will assume that the device is
active when the pin is high and above the threshold. If
``active_state`` is ``False``, the device will be active when
the pin is low and below the threshold.
:param float threshold:
The threshold that the device must be above or below to be
considered active. The default is 0.5.
:param float conversion:
A function that takes a voltage and returns a temperature.
e.g. The internal temperature sensor has a voltage range of 0.706V to 0.716V
and would use the follow conversion function::
def temp_conversion(voltage):
return 27 - (voltage - 0.706)/0.001721
temp_sensor = TemperatureSensor(pin, conversion=temp_conversion)
If :data:`None` (the default), the ``temp`` property will return :data:`None`.
"""
def __init__(self, pin, active_state=True, threshold=0.5, conversion=None):
self._conversion = conversion
super().__init__(pin, active_state, threshold)
@property
def temp(self):
"""
Returns the temperature of the device. If the conversion function is not
set, this will return :data:`None`.
"""
if self._conversion is not None:
return self._conversion(self.voltage)
else:
return None
@property
def conversion(self):
"""
Sets or returns the conversion function for the device.
"""
return self._conversion
@conversion.setter
def conversion(self, value):
self._conversion = value
pico_temp_sensor = TemperatureSensor(4, True, 0.5, pico_temp_conversion)
TempSensor = TemperatureSensor
Thermistor = TemperatureSensor
[docs]class DistanceSensor(PinsMixin):
"""
Represents a HC-SR04 ultrasonic distance sensor.
:param int echo:
The pin that the ECHO pin is connected to.
:param int trigger:
The pin that the TRIG pin is connected to.
:param float max_distance:
The :attr:`value` attribute reports a normalized value between 0 (too
close to measure) and 1 (maximum distance). This parameter specifies
the maximum distance expected in meters. This defaults to 1.
"""
def __init__(self, echo, trigger, max_distance=1):
self._pin_nums = (echo, trigger)
self._max_distance = max_distance
self._echo = Pin(echo, mode=Pin.IN, pull=Pin.PULL_DOWN)
self._trigger = Pin(trigger, mode=Pin.OUT, value=0)
def _read(self):
echo_on = None
echo_off = None
timed_out = False
self._trigger.off()
sleep(0.000005)
self._trigger.on()
sleep(0.00001)
self._trigger.off()
# If an echo isn't measured in 100 milliseconds, it should
# be considered out of range. The maximum length of the
# echo is 38 milliseconds but it's not known how long the
# transmission takes after the trigger
stop = ticks_ms() + 100
while echo_off is None and not timed_out:
if self._echo.value() == 1 and echo_on is None:
echo_on = ticks_us()
if echo_on is not None and self._echo.value() == 0:
echo_off = ticks_us()
if ticks_ms() > stop:
timed_out = True
if echo_off is None or timed_out:
return None
else:
distance = ((echo_off - echo_on) * 0.000343) / 2
distance = min(distance, self._max_distance)
return distance
@property
def value(self):
"""
Returns a value between 0, indicating the reflector is either touching
the sensor or is sufficiently near that the sensor can’t tell the
difference, and 1, indicating the reflector is at or beyond the
specified max_distance. A return value of None indicates that the
echo was not received before the timeout.
"""
distance = self.distance
return distance / self._max_distance if distance is not None else None
@property
def distance(self):
"""
Returns the current distance measured by the sensor in meters. Note
that this property will have a value between 0 and max_distance.
"""
return self._read()
@property
def max_distance(self):
"""
Returns the maximum distance that the sensor will measure in metres.
"""
return self._max_distance