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)) 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 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)) 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 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 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 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) 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) 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) 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 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 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) 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 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 def on(self, volume=1): self.volume = volume 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 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) 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) def close(self): self._pwm_buzzer.close() 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) 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) 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) 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) 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) 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) 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) def close(self): super().close() for led in self._leds: led.close() self._leds = None RGBLED.colour = RGBLED.color 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) 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() 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() 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) 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) 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 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 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) 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) 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) 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) def stop(self): """ Stops the robot. """ self._left.stop() self._right.stop() 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 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)) def min(self): """ Set the servo to its minimum position. """ self.value = 0 def mid(self): """ Set the servo to its mid-point position. """ self.value = 0.5 def max(self): """ Set the servo to its maximum position. """ self.value = 1 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() class DigitalInputDevice(InputDevice, PinMixin): """ Represents a generic input device with digital functionality e.g. buttons that can be either active or inactive. :param int pin: The pin that the device is connected to. :param bool pull_up: If :data:`True`, the device will be pulled up to HIGH. If :data:`False` (the default), the device will be pulled down to LOW. :param bool active_state: If :data:`True` (the default), 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. :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. The default is :data:`None`. """ def __init__(self, pin, pull_up=False, active_state=None, bounce_time=None): super().__init__(active_state) self._pin_num = pin self._pin = Pin( pin, mode=Pin.IN, pull=Pin.PULL_UP if pull_up else Pin.PULL_DOWN) self._bounce_time = bounce_time if active_state is None: self._active_state = False if pull_up else True else: self._active_state = active_state self._state = self._pin.value() self._when_activated = None self._when_deactivated = None # setup interupt self._pin.irq(self._pin_change, Pin.IRQ_RISING | Pin.IRQ_FALLING) def _state_to_value(self, state): return int(bool(state) == self._active_state) def _read(self): return self._state_to_value(self._state) def _pin_change(self, p): # turn off the interupt p.irq(handler=None) last_state = p.value() if self._bounce_time is not None: # wait for stability stop = ticks_ms() + (self._bounce_time * 1000) while ticks_ms() < stop: # keep checking, reset the stop if the value changes if p.value() != last_state: stop = ticks_ms() + self._bounce_time last_state = p.value() # re-enable the interupt p.irq(self._pin_change, Pin.IRQ_RISING | Pin.IRQ_FALLING) # did the value actually change? if self._state != last_state: # set the state self._state = self._pin.value() # manage call backs callback_to_run = None if self.value and self._when_activated is not None: callback_to_run = self._when_activated elif not self.value and self._when_deactivated is not None: callback_to_run = self._when_deactivated if callback_to_run is not None: def schedule_callback(callback): callback() try: schedule(schedule_callback, callback_to_run) except RuntimeError as e: if str(e) == "schedule queue full": raise EventFailedScheduleQueueFull( "{} - {} not run due to the micropython schedule being full".format( str(self), callback_to_run.__name__)) else: raise e @property def is_active(self): """ Returns :data:`True` if the device is active. """ return bool(self.value) @property def is_inactive(self): """ Returns :data:`True` if the device is inactive. """ return not bool(self.value) @property def when_activated(self): """ Returns a :samp:`callback` that will be called when the device is activated. """ return self._when_activated @when_activated.setter def when_activated(self, value): self._when_activated = value @property def when_deactivated(self): """ Returns a :samp:`callback` that will be called when the device is deactivated. """ return self._when_deactivated @when_deactivated.setter def when_deactivated(self, value): self._when_deactivated = value def close(self): """ Closes the device and releases any resources. Once closed, the device can no longer be used. """ self._pin.irq(handler=None) self._pin = None 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 class Button(Switch): """ Represents a push button, which can be either pressed or released. :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. """ pass 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 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 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 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