diff --git a/docs/api.rst b/docs/api.rst index b08953d..782d90c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -101,6 +101,14 @@ Button :inherited-members: :members: +MotionSensor +------------ + +.. autoclass:: MotionSensor + :show-inheritance: + :inherited-members: + :members: + Switch ------ diff --git a/docs/examples/motion_sensor.py b/docs/examples/motion_sensor.py new file mode 100644 index 0000000..fcc231c --- /dev/null +++ b/docs/examples/motion_sensor.py @@ -0,0 +1,15 @@ +from picozero import MotionSensor +from time import sleep + +pir = MotionSensor(2) + +print("PIR Motion Sensor Example") +print("Waiting for motion...") + +while True: + if pir.motion_detected: + print("Motion detected!") + sleep(1) + else: + print("No motion") + sleep(0.5) diff --git a/docs/examples/motion_sensor_callbacks.py b/docs/examples/motion_sensor_callbacks.py new file mode 100644 index 0000000..7deba65 --- /dev/null +++ b/docs/examples/motion_sensor_callbacks.py @@ -0,0 +1,16 @@ +from picozero import MotionSensor, pico_led +from time import sleep + +pir = MotionSensor(2) + +# Set up event callbacks +pir.when_motion = pico_led.on +pir.when_no_motion = pico_led.off + +# Keep the program running +try: + while True: + sleep(1) +except KeyboardInterrupt: + print("\nShutting down...") + pico_led.off() # Make sure LED is off when exiting diff --git a/picozero/__init__.py b/picozero/__init__.py index 794583e..83cb63c 100644 --- a/picozero/__init__.py +++ b/picozero/__init__.py @@ -28,6 +28,7 @@ DigitalInputDevice, Switch, Button, + MotionSensor, AnalogInputDevice, Potentiometer, diff --git a/picozero/picozero.py b/picozero/picozero.py index 22c5541..2836e0b 100644 --- a/picozero/picozero.py +++ b/picozero/picozero.py @@ -6,17 +6,23 @@ # EXCEPTIONS ############################################################################### + class PWMChannelAlreadyInUse(Exception): pass + class EventFailedScheduleQueueFull(Exception): pass + ############################################################################### # SUPPORTING CLASSES ############################################################################### -def clamp(n, low, high): return max(low, min(n, high)) + +def clamp(n, low, high): + return max(low, min(n, high)) + def pinout(output=True): """ @@ -24,7 +30,7 @@ def pinout(output=True): :param bool output: If :data:`True` (the default) the pinout will be "printed". - + """ pins = """ ---usb--- GP0 1 |o o| -1 VBUS @@ -53,6 +59,7 @@ def pinout(output=True): print(pins) return pins + class PinMixin: """ Mixin used by devices that have a single pin number. @@ -68,6 +75,7 @@ def pin(self): def __str__(self): return "{} (pin {})".format(self.__class__.__name__, self._pin_num) + class PinsMixin: """ Mixin used by devices that use multiple pins. @@ -82,10 +90,11 @@ def pins(self): def __str__(self): return "{} (pins - {})".format(self.__class__.__name__, self._pin_nums) - + + class ValueChange: """ - Internal class to control the value of an output device. + Internal class to control the value of an output device. :param OutputDevice output_device: The OutputDevice object you wish to change the value of. @@ -93,63 +102,68 @@ class ValueChange: :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. - + 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) + + 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 @@ -158,7 +172,7 @@ def _get_value(self): # recreate the generator and start again self._gen = self._generator() return next(self._gen) - + def stop(self): """ Stops the ValueChange object running. @@ -166,26 +180,29 @@ def stop(self): self._running = False self._timer.deinit() + ############################################################################### # OUTPUT DEVICES ############################################################################### + class OutputDevice: """ - Base class for output devices. - """ + 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 + 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 @@ -194,7 +211,7 @@ def active_high(self): 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): """ @@ -206,7 +223,7 @@ def value(self): def value(self, value): self._stop_change() self._write(value) - + def on(self, value=1, t=None, wait=False): """ Turns the device on. @@ -215,11 +232,11 @@ def on(self, value=1, t=None, wait=False): 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 + 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 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. @@ -227,14 +244,22 @@ def on(self, value=1, t=None, wait=False): if t is None: self.value = value else: - self._start_change(lambda : iter([(value, t), ]), 1, wait) + self._start_change( + lambda: iter( + [ + (value, t), + ] + ), + 1, + wait, + ) def off(self): """ Turns the device off. """ self.value = 0 - + @property def is_active(self): """ @@ -250,39 +275,39 @@ def toggle(self): 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`, + 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 + 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 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. + 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) - + 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() @@ -294,6 +319,7 @@ def close(self): """ self.value = 0 + class DigitalOutputDevice(OutputDevice, PinMixin): """ Represents a device driven by a digital pin. @@ -310,23 +336,24 @@ class DigitalOutputDevice(OutputDevice, PinMixin): 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 @@ -335,6 +362,7 @@ def close(self): super().close() self._pin = None + class DigitalLED(DigitalOutputDevice): """ Represents a simple LED, which can be switched on and off. @@ -351,10 +379,13 @@ class DigitalLED(DigitalOutputDevice): 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. @@ -371,10 +402,13 @@ class Buzzer(DigitalOutputDevice): 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. @@ -398,42 +432,76 @@ class PWMOutputDevice(OutputDevice, PinMixin): 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"] + + 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): + + 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]) - ) + 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 + 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): """ @@ -447,7 +515,7 @@ def freq(self): Returns the current frequency of the device. """ return self._pwm.freq() - + @freq.setter def freq(self, freq): """ @@ -455,19 +523,28 @@ def freq(self, freq): """ 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): + 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`, + 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 + The number of times to repeat the blink operation. If `None`, the device will continue blinking forever. The default is `None`. :param bool wait: @@ -487,18 +564,18 @@ def blink(self, on_time=1, off_time=None, n=None, wait=False, fade_in_time=0, fa 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) @@ -506,12 +583,12 @@ def blink_generator(): 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) @@ -519,7 +596,7 @@ def blink_generator(): 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. @@ -527,21 +604,29 @@ def pulse(self, fade_in_time=1, fade_out_time=None, n=None, wait=False, fps=25): :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) + 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): """ @@ -551,10 +636,11 @@ def close(self): 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. @@ -578,12 +664,15 @@ class PWMLED(PWMOutputDevice): 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. + the value of the `pwm` parameter. :: @@ -612,15 +701,10 @@ def LED(pin, pwm=True, active_high=True, initial_value=False): :data:`True`, the device will be switched on initially. """ if pwm: - return PWMLED( - pin=pin, - active_high=active_high, - initial_value=initial_value) + 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) + return DigitalLED(pin=pin, active_high=active_high, initial_value=initial_value) + try: pico_led = LED("LED", pwm=False) @@ -628,6 +712,7 @@ def LED(pin, pwm=True, active_high=True, initial_value=False): # 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. @@ -650,13 +735,18 @@ class PWMBuzzer(PWMOutputDevice): :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): + """ + + 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. @@ -666,7 +756,7 @@ class Speaker(OutputDevice, PinMixin): :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. @@ -679,22 +769,109 @@ class Speaker(OutputDevice, PinMixin): 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): - + "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, @@ -702,14 +879,14 @@ def __init__(self, pin, initial_freq=440, initial_volume=0, duty_factor=1023, ac 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 @@ -736,52 +913,61 @@ def volume(self): 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 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) + 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): + 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`, + 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 + The number of times to repeat the beep operation. If `None`, the device will continue beeping forever. The default is `None`. :param bool wait: @@ -800,11 +986,13 @@ def beep(self, on_time=1, off_time=None, n=None, wait=False, fade_in_time=0, fad 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) + 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. + Plays a tune for a given duration. :param int tune: @@ -818,7 +1006,7 @@ def play(self, tune=440, duration=1, volume=1, n=1, wait=True): + 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. @@ -828,7 +1016,7 @@ def play(self, tune=440, duration=1, volume=1, n=1, wait=True): :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. @@ -846,7 +1034,7 @@ def play(self, tune=440, duration=1, volume=1, n=1, wait=True): 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 @@ -856,19 +1044,20 @@ def tune_generator(): 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 @@ -888,7 +1077,7 @@ class RGBLED(OutputDevice, PinsMixin): :type red: int :param red: - The GP pin that controls the red component of the RGB LED. + 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. @@ -903,27 +1092,35 @@ class RGBLED(OutputDevice, PinsMixin): 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 + 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): + + 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)) + 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 + value = (value,) * 3 for led, v in zip(self._leds, value): led.value = v - + @property def value(self): """ @@ -952,10 +1149,10 @@ def is_active(self): 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): """ @@ -1026,24 +1223,32 @@ def invert(self): """ 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. + 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._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): + + 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. + 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. @@ -1059,42 +1264,52 @@ def blink(self, on_times=1, fade_times=0, colors=((1, 0, 0), (0, 1, 0), (0, 0, 1 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) + on_times = (on_times,) * len(colors) if type(fade_times) is not tuple: - fade_times = (fade_times, ) * len(colors) + 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 + (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 + 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): + + 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. @@ -1104,7 +1319,7 @@ def pulse(self, fade_times=1, colors=((0, 0, 0), (1, 0, 0), (0, 0, 0), (0, 1, 0) Number of seconds to spend fading out. Defaults to 1. :type colors: tuple :param colors: - Tuple of colours to pulse between in order. Defaults to red, off, green, off, blue, off. + 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: @@ -1112,8 +1327,15 @@ def pulse(self, fade_times=1, colors=((0, 0, 0), (1, 0, 0), (0, 0, 0), (0, 1, 0) """ 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): + + 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. @@ -1123,7 +1345,7 @@ def cycle(self, fade_times=1, colors=((1, 0, 0), (0, 1, 0), (0, 0, 1)), n=None, Number of seconds to spend fading out. Defaults to 1. :type colors: tuple :param colors: - Tuple of colours to cycle between. Defaults to red, green, blue. + 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. @@ -1136,9 +1358,11 @@ def close(self): 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 @@ -1147,23 +1371,28 @@ class Motor(PinsMixin): :type forward: int :param forward: - The GP pin that controls the "forward" motion of the motor. - + 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. - + 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 + 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) - + 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. @@ -1174,11 +1403,11 @@ def on(self, speed=1, t=None, wait=False): the opposite direction. Defaults to 1. :param float t: - The time in seconds that the motor should run for. If None is + 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 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. @@ -1186,11 +1415,11 @@ def on(self, speed=1, t=None, wait=False): 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() @@ -1224,11 +1453,11 @@ def forward(self, speed=1, t=None, wait=False): 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 + 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 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. @@ -1243,11 +1472,11 @@ def backward(self, speed=1, t=None, wait=False): 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 + 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 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. @@ -1262,9 +1491,11 @@ def close(self): self._forward.close() self._backward.close() + Motor.start = Motor.on Motor.stop = Motor.off + class Robot: """ Represents a generic dual-motor robot / rover / buggy. @@ -1283,18 +1514,19 @@ class Robot: robot.forward() :param tuple left: - A tuple of two pins representing the forward and backward inputs of the + 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 + 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. + 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) @@ -1326,7 +1558,7 @@ def value(self): @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". @@ -1335,19 +1567,19 @@ def forward(self, speed=1, t=None, wait=False): 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 + 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 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". @@ -1356,63 +1588,63 @@ def backward(self, speed=1, t=None, wait=False): 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 + 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 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 + 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 + 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 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 + 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 + 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 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. @@ -1427,9 +1659,11 @@ def close(self): """ self._left.close() self._right.close() - + + Rover = Robot + class Servo(PWMOutputDevice): """ Represents a PWM-controlled servo motor. @@ -1440,7 +1674,7 @@ class Servo(PWMOutputDevice): :type pin: int :param pin: - The pin the servo motor is connected to. + 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 @@ -1461,32 +1695,56 @@ class Servo(PWMOutputDevice): :param int duty_factor: The duty factor of the PWM signal. This is a value between 0 and 65535. - Defaults to 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): + + 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) - + + 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) - + 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)) - + 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. @@ -1499,14 +1757,17 @@ def off(self): """ self.value = None + ############################################################################### # INPUT DEVICES ############################################################################### + class InputDevice: """ Base class for input devices. """ + def __init__(self, active_state=None): self._active_state = active_state @@ -1525,18 +1786,19 @@ def active_state(self): 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` + 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 + Represents a generic input device with digital functionality e.g. buttons that can be either active or inactive. :param int pin: @@ -1555,43 +1817,43 @@ class DigitalInputDevice(InputDevice, PinMixin): 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 + 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) + 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) @@ -1600,36 +1862,38 @@ def _pin_change(self, p): if p.value() != last_state: stop = ticks_ms() + (self._bounce_time * 1000) last_state = p.value() - + # re-enable the interupt p.irq(self._pin_change, Pin.IRQ_RISING | Pin.IRQ_FALLING) - - # did the value actually change? + + # 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__)) + str(self), callback_to_run.__name__ + ) + ) else: raise e @@ -1646,29 +1910,29 @@ 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 @@ -1677,6 +1941,7 @@ def close(self): self._pin.irq(handler=None) self._pin = None + class Switch(DigitalInputDevice): """ Represents a toggle switch, which is either open or closed. @@ -1692,17 +1957,20 @@ class Switch(DigitalInputDevice): 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 + presses from registering as multiple presses. Defaults to 0.02 seconds. """ - def __init__(self, pin, pull_up=True, bounce_time=0.02): + + 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. @@ -1718,51 +1986,84 @@ class Button(Switch): 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 + 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 +Button.when_released = Button.when_deactivated + + +class MotionSensor(DigitalInputDevice): + """ + Represents a PIR (Passive Infrared) motion sensor (e.g. HC-SR501) + + :param int pin: + The pin that the motion sensor 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. + Most PIR sensors work with pull_up=False. + + :param float bounce_time: + The bounce time for the device. If set, the device will ignore + any motion events that happen within the bounce time after a + motion event. This is useful to prevent false triggers. + Defaults to 1 seconds. + """ + + def __init__(self, pin, pull_up=False, bounce_time=1.00): + super().__init__(pin=pin, pull_up=pull_up, bounce_time=bounce_time) + + +MotionSensor.motion_detected = MotionSensor.is_active +# Note: No alias for is_inactive - use 'not pir.motion_detected' for clarity +MotionSensor.when_motion = MotionSensor.when_activated +MotionSensor.when_no_motion = MotionSensor.when_deactivated + class AnalogInputDevice(InputDevice, PinMixin): """ - Represents a generic input device with analogue functionality, e.g. + 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. + 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): """ @@ -1792,6 +2093,7 @@ def voltage(self): def close(self): self._adc = None + class Potentiometer(AnalogInputDevice): """ Represents a potentiometer, which outputs a variable voltage @@ -1801,55 +2103,59 @@ class Potentiometer(AnalogInputDevice): :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. + 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 + 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 + 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. + 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. + 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 + 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 @@ -1858,10 +2164,11 @@ def temp_conversion(voltage): 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) - + self._conversion = conversion + super().__init__(pin, active_state, threshold) + @property def temp(self): """ @@ -1883,11 +2190,13 @@ def conversion(self): @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. @@ -1896,24 +2205,25 @@ class DistanceSensor(PinsMixin): The pin that the ECHO pin is connected to. :param int trigger: - The pin that the TRIG pin is connected to. + 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() @@ -1932,30 +2242,30 @@ def _read(self): 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 + 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 + 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() @@ -1966,4 +2276,3 @@ def max_distance(self): Returns the maximum distance that the sensor will measure in metres. """ return self._max_distance - diff --git a/tests/test_picozero.py b/tests/test_picozero.py index d1cbcd0..e994cd2 100644 --- a/tests/test_picozero.py +++ b/tests/test_picozero.py @@ -2,25 +2,27 @@ from picozero import * from time import ticks_ms + def log_device_values(d, timeout): values = [d.value] - + timeout_ms = ticks_ms() + (timeout * 1000) - + while ticks_ms() < timeout_ms: if values[-1] != d.value: values.append(d.value) - + return values + class MockPin: def __init__(self, initial_state=0, irq_handler=None): self._state = initial_state self._irq_handler = irq_handler - + def read(self): return self._state - + def write(self, state): self._state = state if self._irq_handler is not None: @@ -31,38 +33,41 @@ def value(self, state=None): return self._state else: self.write(state) - + def irq(self, handler, args=None): self._irq_handler = handler + class MockADC: def __init__(self, initial_state=0): self._state = initial_state - + def read(self): return self._state - + def write(self, state): self._state = state - + def read_u16(self): return self._state + class MockEvent: def __init__(self): self._is_set = False - + def set(self): self._is_set = True - + def is_set(self): return self._is_set - + def reset(self): self._is_set = False - + + class Testpicozero(unittest.TestCase): - + def assertInRange(self, value, lower, upper): msg = "Expected %r to be in range {} to {}".format(lower, upper) self.assertTrue(value <= upper, msg) @@ -71,7 +76,7 @@ def assertInRange(self, value, lower, upper): ########################################################################### # SUPPORTING ########################################################################### - + def test_pinout(self): pins = pinout(output=False) self.assertIsNotNone(pins) @@ -82,55 +87,55 @@ def test_pinout(self): def test_digital_output_device_default_values(self): d = DigitalOutputDevice(1) - + self.assertTrue(d.active_high) self.assertEqual(d.value, 0) self.assertFalse(d.is_active) - + d.on() self.assertTrue(d.value) self.assertEqual(d._pin.value(), 1) self.assertTrue(d.is_active) - + d.off() self.assertFalse(d.value) self.assertEqual(d._pin.value(), 0) self.assertFalse(d.is_active) - + d.value = True self.assertEqual(d.value, 1) d.value = False self.assertEqual(d.value, 0) - + d.close() self.assertIsNone(d._pin) def test_digital_output_device_alt_values(self): d = DigitalOutputDevice(1, active_high=False, initial_value=True) - + self.assertFalse(d.active_high) self.assertTrue(d.value) - + d.off() self.assertEqual(d._pin.value(), 1) - + d.on() self.assertEqual(d._pin.value(), 0) - + d.close() - + def test_digital_output_device_blink(self): d = DigitalOutputDevice(1) - + d.blink() values = log_device_values(d, 1.1) - self.assertEqual(values, [1,0]) + self.assertEqual(values, [1, 0]) d.off() self.assertFalse(d.value) - + d.blink(on_time=0.1, off_time=0.1, n=2) values = log_device_values(d, 0.5) - self.assertEqual(values, [1,0,1,0]) + self.assertEqual(values, [1, 0, 1, 0]) self.assertFalse(d.value) d.close() @@ -139,110 +144,161 @@ def test_digital_LED(self): d = DigitalLED(1) self.assertFalse(d.is_lit) d.close() - + def test_pwm_output_device_default_values(self): d = PWMOutputDevice(1) - + self.assertTrue(d.active_high) self.assertEqual(d.value, 0) self.assertFalse(d.is_active) self.assertEqual(d.freq, 100) - + d.on() self.assertTrue(d.value) self.assertTrue(d.is_active) self.assertEqual(d._pwm.duty_u16(), 65535) self.assertTrue(d.is_active) - + d.off() self.assertFalse(d.value) - self.assertEqual(d._pwm.duty_u16(),0) + self.assertEqual(d._pwm.duty_u16(), 0) self.assertFalse(d.is_active) - + d.value = True self.assertEqual(d.value, 1) d.value = False self.assertEqual(d.value, 0) - + d.value = 0.5 self.assertAlmostEqual(d.value, 0.5, places=2) self.assertTrue(d.is_active) - + d.close() self.assertIsNone(d._pwm) - + def test_pwm_output_device_alt_values(self): - d = PWMOutputDevice(1, freq=200, duty_factor=10000, active_high=False, initial_value=True) - + d = PWMOutputDevice( + 1, freq=200, duty_factor=10000, active_high=False, initial_value=True + ) + self.assertFalse(d.active_high) self.assertTrue(d.value) self.assertEqual(d.freq, 200) - + d.off() # prior to micropython v1.20 PWM returned 1 less than the duty_factor # unless the duty was set to the maximum 65535 # self.assertEqual(d._pwm.duty_u16(), 9999) self.assertEqual(d._pwm.duty_u16(), 10000) - self.assertAlmostEqual(d.value, 0, places=2) - + self.assertAlmostEqual(d.value, 0, places=2) + d.on() self.assertEqual(d._pwm.duty_u16(), 0) self.assertEqual(d.value, 1) - + d.off() - + d.close() def test_pwm_output_device_blink(self): d = PWMOutputDevice(1) - + d.blink() values = log_device_values(d, 1.1) - self.assertEqual(values, [1,0]) + self.assertEqual(values, [1, 0]) d.off() self.assertFalse(d.value) - + d.blink(on_time=0.1, off_time=0.1, n=2) values = log_device_values(d, 0.5) - self.assertEqual(values, [1,0,1,0]) + self.assertEqual(values, [1, 0, 1, 0]) self.assertFalse(d.value) d.close() - + def test_pwm_output_device_pulse(self): d = PWMOutputDevice(1) - + d.pulse(n=1) values = log_device_values(d, 2.1) - + expected = [ - 0.0, 0.04, 0.08, 0.12, 0.16, 0.2, 0.24, 0.28, 0.32, 0.36, 0.4, - 0.44, 0.48, 0.52, 0.56, 0.6, 0.64, 0.68, 0.72, 0.76, 0.8, 0.84, - 0.88, 0.92, 0.96, 1.0, 0.96, 0.92, 0.88, 0.84, 0.8, 0.76, - 0.72, 0.68, 0.64, 0.6, 0.56, 0.52, 0.48, 0.44, 0.4, 0.36, 0.32, - 0.28, 0.24, 0.2, 0.16, 0.12, 0.08, 0.04, 0.0] - + 0.0, + 0.04, + 0.08, + 0.12, + 0.16, + 0.2, + 0.24, + 0.28, + 0.32, + 0.36, + 0.4, + 0.44, + 0.48, + 0.52, + 0.56, + 0.6, + 0.64, + 0.68, + 0.72, + 0.76, + 0.8, + 0.84, + 0.88, + 0.92, + 0.96, + 1.0, + 0.96, + 0.92, + 0.88, + 0.84, + 0.8, + 0.76, + 0.72, + 0.68, + 0.64, + 0.6, + 0.56, + 0.52, + 0.48, + 0.44, + 0.4, + 0.36, + 0.32, + 0.28, + 0.24, + 0.2, + 0.16, + 0.12, + 0.08, + 0.04, + 0.0, + ] + if len(values) == len(expected): for i in range(len(values)): self.assertAlmostEqual(values[i], expected[i], places=2) else: self.fail(f"{len(values)} were generated, {len(expected)} were expected.") - + d.pulse(fade_in_time=0.5, fade_out_time=1, n=1, fps=4) values = log_device_values(d, 2.1) - + expected = [0.0, 0.5, 1.0, 0.75, 0.5, 0.25, 0] - + if len(values) == len(expected): for i in range(len(values)): self.assertAlmostEqual(values[i], expected[i], places=2) else: - self.fail(f"{len(values)} values were generated, {len(expected)} were expected.") - + self.fail( + f"{len(values)} values were generated, {len(expected)} were expected." + ) + d.close() - + def test_motor_default_values(self): - d = Motor(1,2) + d = Motor(1, 2) self.assertEqual(d.value, 0) @@ -257,7 +313,7 @@ def test_motor_default_values(self): d.backward() self.assertEqual(d.value, -1) - + d.value = 0.5 self.assertAlmostEqual(d.value, 0.5, places=2) @@ -266,18 +322,18 @@ def test_motor_default_values(self): d.forward(1, t=0.5) values = log_device_values(d, 0.6) - self.assertEqual(values, [1,0]) + self.assertEqual(values, [1, 0]) self.assertEqual(d.value, 0) - + d.backward(1, t=0.5) values = log_device_values(d, 0.6) - self.assertEqual(values, [-1,0]) + self.assertEqual(values, [-1, 0]) self.assertEqual(d.value, 0) d.close() def test_motor_alt_values(self): - d = Motor(1,2,pwm=False) + d = Motor(1, 2, pwm=False) d.value = 0.5 self.assertEqual(d.value, 1) @@ -289,25 +345,25 @@ def test_motor_alt_values(self): self.assertEqual(d.value, 0) d.close() - + def test_robot(self): - d = Robot(left=(1,2), right=(3,4)) - + d = Robot(left=(1, 2), right=(3, 4)) + d.forward() - self.assertEqual(d.value, (1,1)) - + self.assertEqual(d.value, (1, 1)) + d.left() - self.assertEqual(d.value, (-1,1)) - + self.assertEqual(d.value, (-1, 1)) + d.right() - self.assertEqual(d.value, (1,-1)) - + self.assertEqual(d.value, (1, -1)) + d.value = (0.5, -0.5) self.assertAlmostEqual(d.left_motor.value, 0.5, places=2) self.assertAlmostEqual(d.right_motor.value, -0.5, places=2) - + d.stop() - self.assertEqual(d.value, (0,0)) + self.assertEqual(d.value, (0, 0)) d.close() @@ -315,269 +371,291 @@ def test_LED_factory(self): d = LED(1) self.assertIsInstance(d, PWMLED) d.close() - + d = LED(1, pwm=False) self.assertIsInstance(d, DigitalLED) d.close() - + def test_pico_led(self): - + self.assertIsInstance(pico_led, DigitalLED) - + self.assertEqual(pico_led.value, 0) - + pico_led.on() self.assertEqual(pico_led.value, 1) - + pico_led.off() self.assertEqual(pico_led.value, 0) def test_rgb_led_default_values(self): - d = RGBLED(1,2,3) - - self.assertEqual(d.value, (0,0,0)) - + d = RGBLED(1, 2, 3) + + self.assertEqual(d.value, (0, 0, 0)) + d.on() - self.assertEqual(d.value, (1,1,1)) - + self.assertEqual(d.value, (1, 1, 1)) + d.off() - self.assertEqual(d.value, (0,0,0)) - + self.assertEqual(d.value, (0, 0, 0)) + d.value = (0.25, 0.5, 0.75) self.assertAlmostEqual(d.value[0], 0.25, places=2) self.assertAlmostEqual(d.value[1], 0.5, places=2) self.assertAlmostEqual(d.value[2], 0.75, places=2) - + d.red = 200 self.assertAlmostEqual(d.value[0], 0.78, places=2) - + d.green = 100 self.assertAlmostEqual(d.value[1], 0.39, places=2) - + d.blue = 50 self.assertAlmostEqual(d.value[2], 0.20, places=2) - + d.close() - + def test_rgb_led_alt_values(self): - d = RGBLED(1,2,3, initial_value=(1,1,1), pwm=False) - - self.assertEqual(d.value, (1,1,1)) - + d = RGBLED(1, 2, 3, initial_value=(1, 1, 1), pwm=False) + + self.assertEqual(d.value, (1, 1, 1)) + d.on() - self.assertEqual(d.value, (1,1,1)) - + self.assertEqual(d.value, (1, 1, 1)) + d.off() - self.assertEqual(d.value, (0,0,0)) - + self.assertEqual(d.value, (0, 0, 0)) + d.value = (1, 1, 1) - self.assertEqual(d.value, (1,1,1)) - + self.assertEqual(d.value, (1, 1, 1)) + d.value = (0, 0.5, 1) - self.assertEqual(d.value, (0,1,1)) - + self.assertEqual(d.value, (0, 1, 1)) + d.close() def test_servo_default_value(self): d = Servo(1) - + self.assertEqual(d.value, None) - + d.value = 0 self.assertAlmostEqual(d.value, 0, 2) - self.assertInRange(d._pwm.duty_u16(), int((0.001 / 0.02) * 65535) - 1, int((0.001 / 0.02) * 65535) + 1) - + self.assertInRange( + d._pwm.duty_u16(), + int((0.001 / 0.02) * 65535) - 1, + int((0.001 / 0.02) * 65535) + 1, + ) + d.value = 1 self.assertAlmostEqual(d.value, 1, 2) - self.assertInRange(d._pwm.duty_u16(), int((0.002 / 0.02) * 65535) - 1, int((0.002 / 0.02) * 65535) + 1) - + self.assertInRange( + d._pwm.duty_u16(), + int((0.002 / 0.02) * 65535) - 1, + int((0.002 / 0.02) * 65535) + 1, + ) + d.value = None self.assertEqual(d.value, None) self.assertEqual(d._pwm.duty_u16(), 0) - + d.min() self.assertAlmostEqual(d.value, 0, 2) - + d.mid() self.assertAlmostEqual(d.value, 0.5, 2) - + d.max() self.assertAlmostEqual(d.value, 1, 2) - + d.off() self.assertEqual(d._pwm.duty_u16(), 0) - + d.close() - + def test_servo_alt_values(self): - d = Servo(1, initial_value=1, min_pulse_width=0.9/1000, max_pulse_width=2.1/1000, frame_width=19/1000) - + d = Servo( + 1, + initial_value=1, + min_pulse_width=0.9 / 1000, + max_pulse_width=2.1 / 1000, + frame_width=19 / 1000, + ) + self.assertAlmostEqual(d.value, 1, 2) - + d.value = 0 - self.assertInRange(d._pwm.duty_u16(), int((0.0009 / 0.019) * 65535) - 1, int((0.0009 / 0.019) * 65535) + 1) - + self.assertInRange( + d._pwm.duty_u16(), + int((0.0009 / 0.019) * 65535) - 1, + int((0.0009 / 0.019) * 65535) + 1, + ) + d.value = 1 - self.assertInRange(d._pwm.duty_u16(), int((0.0021 / 0.019) * 65535) - 1, int((0.0021 / 0.019) * 65535) + 1) - + self.assertInRange( + d._pwm.duty_u16(), + int((0.0021 / 0.019) * 65535) - 1, + int((0.0021 / 0.019) * 65535) + 1, + ) + d.value = None self.assertEqual(d._pwm.duty_u16(), 0) - + d.close() - + ########################################################################### # INPUT DEVICES ########################################################################### - + def test_digital_input_device_default_values(self): d = DigitalInputDevice(1) - + pin = MockPin(irq_handler=d._pin_change) d._pin = pin - + self.assertTrue(d.active_state) self.assertFalse(d.is_active) self.assertEqual(d.value, 0) - + pin.write(1) - + self.assertTrue(d.is_active) self.assertEqual(d.value, 1) - + pin.write(0) - + self.assertFalse(d.is_active) self.assertEqual(d.value, 0) - + d.close() - + def test_digital_input_device_alt_values(self): d = DigitalInputDevice(1, pull_up=False, active_state=False) - + pin = MockPin(irq_handler=d._pin_change) d._pin = pin - + self.assertFalse(d.active_state) self.assertTrue(d.is_active) self.assertEqual(d.value, 1) - + pin.write(1) - + self.assertFalse(d.is_active) self.assertEqual(d.value, 0) - + pin.write(0) - + self.assertTrue(d.is_active) self.assertEqual(d.value, 1) - + d.close() - + def test_digital_input_device_activated_deactivated(self): d = DigitalInputDevice(1) - + pin = MockPin(irq_handler=d._pin_change) d._pin = pin - + event_activated = MockEvent() event_deactivated = MockEvent() - + d.when_activated = event_activated.set d.when_deactivated = event_deactivated.set - + self.assertFalse(event_activated.is_set()) pin.write(1) self.assertTrue(event_activated.is_set()) - + self.assertFalse(event_deactivated.is_set()) pin.write(0) self.assertTrue(event_deactivated.is_set()) - + d.close() - + def test_adc_input_device_default_values(self): d = AnalogInputDevice(1) - + adc = MockADC() d._adc = adc - + self.assertTrue(d.active_state) self.assertFalse(d.is_active) self.assertEqual(d.value, 0) - + adc.write(65535) self.assertTrue(d.is_active) self.assertEqual(d.value, 1) self.assertEqual(d.voltage, 3.3) - + adc.write(0) self.assertFalse(d.is_active) self.assertEqual(d.value, 0) self.assertEqual(d.voltage, 0) - + # mid point adc.write(32767) self.assertAlmostEqual(d.value, 0.5, places=2) self.assertAlmostEqual(d.voltage, 1.65, places=2) - + d.close() - + def test_adc_input_device_alt_values(self): d = AnalogInputDevice(1, active_state=False, threshold=0.1) - + adc = MockADC() d._adc = adc - + self.assertFalse(d.active_state) self.assertTrue(d.is_active) self.assertEqual(d.value, 1) - + adc.write(65535) self.assertFalse(d.is_active) self.assertEqual(d.value, 0) self.assertEqual(d.voltage, 0) - + adc.write(0) self.assertTrue(d.is_active) self.assertEqual(d.value, 1) self.assertEqual(d.voltage, 3.3) - + d.close() - + def test_adc_input_device_threshold(self): d = AnalogInputDevice(1) - + adc = MockADC() d._adc = adc - + self.assertFalse(d.is_active) - + # mid point adc.write(32767) self.assertFalse(d.is_active) - + # above threshold adc.write(32768) self.assertTrue(d.is_active) - + # below threshold adc.write(32766) self.assertFalse(d.is_active) - + d.threshold = 0.1 - + self.assertTrue(d.is_active) - + adc.write(6553) self.assertFalse(d.is_active) - + d.close() - + def test_temp_sensory(self): - + def temp_conversion(voltage): return voltage + 2 - + t = TemperatureSensor(4, conversion=temp_conversion) adc = MockADC() @@ -585,17 +663,17 @@ def temp_conversion(voltage): adc.write(65535) self.assertEqual(t.temp, 3.3 + 2) - + adc.write(0) self.assertEqual(t.temp, 2) t.close() def test_pico_temp_sensor(self): - + self.assertIsInstance(pico_temp_sensor, TemperatureSensor) self.assertEqual(pico_temp_sensor.pin, 4) self.assertIsNotNone(pico_temp_sensor.temp) -unittest.main() +unittest.main()