Untitled

mail@pastecode.io avatar
unknown
plain_text
a month ago
70 kB
1
Indexable
Never
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
Leave a Comment