diff --git a/README.md b/README.md index 2d665f0..0d09bb0 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,21 @@ datasheet: https://www.vishay.com/docs/82459/tsop48.pdf - pin 16 - IR RX - pin 17 - IR TX - 22R - IR LED OSRAM SFH4544 - - pin 18 - button - pin 19 - button - pin 20 - button - - pin 22 - Red LED, resistor 75R - - pin 28 - 2xAA 1,5V BAT - Schottky diode - VSYS Pi Pico +- pin 15 - PN2222A NPN transistor - Piezo + +# BLE UART Serial Terminal +Show IR RX recived commands + +### Send IR Commands from terminal + +'led_off' LED OFF +'led_on' LED ON +'bomb' Send Explode command over IR TX + +Any Lasertag command in hex `FFFFFF` or `0xFFFFFF` send this command over IR TX \ No newline at end of file diff --git a/lib/picozero.py b/lib/picozero.py new file mode 100644 index 0000000..5323602 --- /dev/null +++ b/lib/picozero.py @@ -0,0 +1,1969 @@ +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 + diff --git a/main.py b/main.py index 236cc4e..bd30543 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,13 @@ -# OpenLaserTag device -# OLT protocol. -# Tomas Krejci [Njord] - +########################################################### +# # +# OpenLaserTag remote control # +# OLT protocol # +# Tomas Krejci [Njord] tk@tomaskrejci.com # +# # +# version 1.0 # +# 2024-06-22 # +# # +########################################################### import machine, time, re from sys import platform @@ -34,24 +40,19 @@ device_id_str = device_id_str[-4:] # Keep last 8 char device_id = int(device_id_str, 16) # Convert to int from hex in string ### Piezo ### -piezo_pin = machine.Pin(15, machine.Pin.OUT, value=1) -piezo_pin2 = machine.Pin(14, machine.Pin.OUT, value=1) +piezo_pin = machine.Pin(15, machine.Pin.OUT, value=0) async def piezo_beep(sec=0.3): - piezo_pin.value(0) - piezo_pin2.value(0) - await asyncio.sleep(sec) piezo_pin.value(1) - piezo_pin2.value(1) + await asyncio.sleep(sec) + piezo_pin.value(0) async def piezo_short_beep(): - piezo_pin.value(0) - piezo_pin2.value(0) - await asyncio.sleep(0.1) piezo_pin.value(1) - piezo_pin2.value(1) + await asyncio.sleep(0.1) + piezo_pin.value(0) #### Voltage monitor ### @@ -83,8 +84,8 @@ async def battery_monitor(aadc): ) # reference voltage (3.3V) divided by the maximum ADC value (65535) voltage = round(voltage, 2) capacity = voltage_to_capacity(voltage / 2) - logger.info(f"Battery Voltage: {voltage}V, Capacity: {capacity}%") - uart.write(f"BATTERY: Voltage: {voltage}V, Capacity: {capacity}%\n") + logger.info(f"Battery Voltage: {voltage:.2f}V, Capacity: {capacity:.0f}%") + uart.write(f"BATTERY: Voltage: {voltage:.2f}V, Capacity: {capacity:.0f}%\n") await asyncio.sleep_ms(10000) @@ -239,7 +240,8 @@ import logging global logger init_ticks = time.ticks_ms() # change logging level here, logging.ERROR is default, logging.DEBUG for more info, and reset device -log_level = logging.DEBUG +# log_level = logging.DEBUG +log_level = logging.INFO # log_level = logging.ERROR logging.basicConfig(level=log_level, format="%(name)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__)