diff --git a/NY_Ball_Drop/Auld_Lang_Syne.wav b/NY_Ball_Drop/Auld_Lang_Syne.wav new file mode 100755 index 000000000..9ac98a645 Binary files /dev/null and b/NY_Ball_Drop/Auld_Lang_Syne.wav differ diff --git a/NY_Ball_Drop/code.py b/NY_Ball_Drop/code.py new file mode 100644 index 000000000..49675cae0 --- /dev/null +++ b/NY_Ball_Drop/code.py @@ -0,0 +1,371 @@ +""" +New Year's Eve ball drop robot friend. + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +# pylint: disable=global-statement + +import time +import random +import board +from digitalio import DigitalInOut, Direction, Pull +import busio +import adafruit_ds3231 +import audioio +import pulseio +from adafruit_motor import servo +import neopixel +from debouncer import Debouncer + +# Set to false to disable testing/tracing code +TESTING = True + +# Implementation dependant things to tweak +NUM_PIXELS = 78 # number of neopixels in the striup +DROP_THROTTLE = -0.03 # servo throttle during ball drop +DROP_DURATION = 10.0 # how many seconds the ball takes to drop +RAISE_THROTTLE = 0.1 # servo throttle while raising the ball +FIREWORKS_DURATION = 30.0 # how many second the fireworks last + +# Pins +NEOPIXEL_PIN = board.D5 +POWER_PIN = board.D10 +SWITCH_PIN = board.D9 +SERVO_PIN = board.A1 + +# States +WAITING_STATE = 0 +PAUSED_STATE = 1 +DROPPING_STATE = 2 +BURST_STATE = 3 +SHOWER_STATE = 4 +RAISING_STATE = 5 +IDLE_STATE = 6 +RESET_STATE = 7 + + +################################################################################ +# Setup hardware + +# Power to the speaker and neopixels must be enabled using this pin + +enable = DigitalInOut(POWER_PIN) +enable.direction = Direction.OUTPUT +enable.value = True + +i2c = busio.I2C(board.SCL, board.SDA) +rtc = adafruit_ds3231.DS3231(i2c) + +audio = audioio.AudioOut(board.A0) + +strip = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False) +strip.fill(0) # NeoPixels off ASAP on startup +strip.show() + +switch = Debouncer(SWITCH_PIN, Pull.UP, 0.01) + +# create a PWMOut object on Pin A2. +pwm = pulseio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50) + +# Create a servo object, my_servo. +servo = servo.ContinuousServo(pwm) +servo.throttle = 0.0 + +# Set the time for testing +# Once finished testing, the time can be set using the REPL using similar code +if TESTING: + # year, mon, date, hour, min, sec, wday, yday, isdst + t = time.struct_time((2018, 12, 31, 23, 58, 55, 1, -1, -1)) + # you must set year, mon, date, hour, min, sec and weekday + # yearday is not supported, isdst can be set but we don't do anything with it at this time + print("Setting time to:", t) + rtc.datetime = t + print() + +################################################################################ +# Variables + +firework_color = 0 +firework_step_time = 0 +burst_count = 0 +shower_count = 0 +firework_stop_time = 0 +pixel_count = min([NUM_PIXELS // 2, 20]) +pixels = [] +pixel_index = 0 + + +################################################################################ +# Support functions + +def log(s): + """Print the argument if testing/tracing is enabled.""" + if TESTING: + print(s) + +# Random color + +def random_color_byte(): + """ Return one of 32 evenly spaced byte values. + This provides random colors that are fairly distinctive.""" + return random.randrange(0, 256, 16) + +def random_color(): + """Return a random color""" + red = random_color_byte() + green = random_color_byte() + blue = random_color_byte() + return (red, green, blue) + +# Color cycling. See https://learn.adafruit.com/hacking-ikea-lamps-with-circuit-playground-express/lamp-it-up + +def wheel(pos): + # Input a value 0 to 255 to get a color value. + # The colours are a transition r - g - b - back to r. + if pos < 0 or pos > 255: + return 0, 0, 0 + if pos < 85: + return int(255 - pos*3), int(pos*3), 0 + if pos < 170: + pos -= 85 + return 0, int(255 - pos*3), int(pos*3) + pos -= 170 + return int(pos * 3), 0, int(255 - (pos*3)) + +def cycle_sequence(seq): + while True: + for elem in seq: + yield elem + +def rainbow_lamp(seq): + g = cycle_sequence(seq) + while True: + strip.fill(wheel(next(g))) + strip.show() + yield + +# Fireworks effects + +def reset_fireworks(time_now): + """As indicated, reset the fireworks system's variables.""" + global firework_color, burst_count, shower_count, firework_step_time + firework_color = random_color() + burst_count = 0 + shower_count = 0 + strip.fill(0) + strip.show() + firework_step_time = time_now + 0.05 + + +def burst(time_now): + """Show a burst of color on all pixels, fading in, holding briefly, + then fading out. Each call to this does one step in that + process. Return True once the sequence is finished.""" + global firework_step_time, burst_count, shower_count + log("burst %d" % (burst_count)) + if burst_count == 0: + strip.brightness = 0.0 + strip.fill(firework_color) + elif burst_count == 22: + shower_count = 0 + firework_step_time = time_now + 0.3 + return True + if time_now < firework_step_time: + return False + elif burst_count < 11: + strip.brightness = burst_count / 10.0 + firework_step_time = time_now + 0.08 + elif burst_count == 11: + firework_step_time = time_now + 0.3 + elif burst_count > 11: + strip.brightness = 1.0 - ((burst_count - 11) / 10.0) + firework_step_time = time_now + 0.08 + strip.show() + burst_count += 1 + return False + +def shower(time_now): + """Show a shower of sparks effect. + Each call to this does one step in the process. Return True once the + sequence is finished.""" + global firework_step_time, pixels, pixel_index, shower_count + log("Shower %d" % (shower_count)) + if shower_count == 0: # Initialize on the first step + strip.fill(0) + strip.brightness = 1.0 + pixels = [None] * pixel_count + pixel_index = 0 + if time_now < firework_step_time: + return False + if shower_count == NUM_PIXELS: + strip.fill(0) + strip.show() + return True + if pixels[pixel_index]: + strip[pixels[pixel_index]] = 0 + random_pixel = random.randrange(NUM_PIXELS) + pixels[pixel_index] = random_pixel + strip[random_pixel] = firework_color + strip.show() + pixel_index = (pixel_index + 1) % pixel_count + shower_count += 1 + firework_step_time = time_now + 0.1 + return False + +def start_playing(fname): + sound_file = open(fname, 'rb') + wav = audioio.WaveFile(sound_file) + audio.play(wav, loop=False) + +def stop_playing(): + if audio.playing: + audio.stop() + + +state = WAITING_STATE +paused_state = None +paused_servo = 0.0 +switch_pressed_at = 0 +drop_finish_time = 0 + +while True: + test_trigger = False + now = time.monotonic() + t = rtc.datetime + switch.update() + fell = switch.fell # reading fell/rose will reset it, so we grab it here + + # The machine sits in paused state until the switch is pressed again in + # which case the machine goes back to the state it was in when paused (and + # resumes the audio and servo as it was) or the switch is held for a second + # in which case it goes to the reset state. + if state == PAUSED_STATE: + log("Paused") + if fell: + if audio.paused: + audio.resume() + servo.throttle = paused_servo + paused_servo = 0.0 + state = paused_state + elif not switch.value: + if now - switch_pressed_at > 1.0: + state = RESET_STATE + continue + + # There is a special check here for a switch press in any state other than + # waiting. If there is a press, the current state, audio, and servo values + # are saved and paused state is entered + if fell and state != WAITING_STATE: + switch_pressed_at = now + paused_state = state + if audio.playing: + audio.pause() + paused_servo = servo.throttle + servo.throttle = 0.0 + state = PAUSED_STATE + continue + + # Waiting state handles waiting until 23:59 on NYE or until the switch is + # pressed. When either happens, the song is played and the servo starts + # droppping the ball. As well, a rainbow effect is started on the LEDs and + # the machine moves to dropping state. + if state == WAITING_STATE: + log("Waiting") + if fell: + while not switch.rose: + switch.update() + test_trigger = True + + if test_trigger or (t.tm_mday == 31 and + t.tm_mon == 12 and + t.tm_hour == 23 and + t.tm_min == 59 and + t.tm_sec == 50): + test_trigger = False + # Play the song + start_playing('./countdown.wav') + + # Drop the ball + servo.throttle = DROP_THROTTLE + + # color show in the ball + rainbow = rainbow_lamp(range(0, 256, 2)) + log("1 minute to midnight") + rainbow_time = now + 0.1 + + drop_finish_time = now + DROP_DURATION + state = DROPPING_STATE + + # In dropping the ball is dropping, colors are cycling, and the song is + # playing. After the machine has been in this state long enough (set by + # DROP_DURATION) it cleans up and switches to fireworks mode (burst to be + # exact). + elif state == DROPPING_STATE: + log("Dropping") + if now >= drop_finish_time: + log("***Midnight") + servo.throttle = 0.0 + stop_playing() + start_playing('./Auld_Lang_Syne.wav') + reset_fireworks(now) + firework_stop_time = now + FIREWORKS_DURATION + state = BURST_STATE + continue + if now >= rainbow_time: + next(rainbow) + rainbow_time = now + 0.1 + + # This state shows a burst of light (vi the burst function. It stays in + # this mode until burst is finished, indicated by burst returning + # True. When that happens the machine moves to the shower state. + elif state == BURST_STATE: + log("Burst") + if burst(now): + state = SHOWER_STATE + shower_count = 0 + + # This state shows a shower-of-sparks effect until the shower function + # returns True. If it's time to stop the fireworks effects the machine + # moves to the idle state. Otherwise if loops back to the burst state. + elif state == SHOWER_STATE: + log("Shower") + if shower(now): + if now >= firework_stop_time: + state = IDLE_STATE + else: + state = BURST_STATE + reset_fireworks(now) + + # The idle state currently just jumps into the waiting state. + elif state == IDLE_STATE: + log("Idle") + state = WAITING_STATE + + # This state resets the LEDs and audio, starts the servo raising the ball + # and moves to the raising state. + elif state == RESET_STATE: + log("Reset") + strip.fill(0) + strip.brightness = 1.0 + strip.show() + if audio.playing: + audio.stop() + servo.throttle = RAISE_THROTTLE + state = RAISING_STATE + + # This state simply waits until the switch is released at which time it + # stops the servo and moves to the waiting state. + elif state == RAISING_STATE: + log("Raise") + if switch.rose: + servo.throttle = 0.0 + state = WAITING_STATE diff --git a/NY_Ball_Drop/countdown.wav b/NY_Ball_Drop/countdown.wav new file mode 100644 index 000000000..5a5cac285 Binary files /dev/null and b/NY_Ball_Drop/countdown.wav differ diff --git a/NY_Ball_Drop/debouncer.py b/NY_Ball_Drop/debouncer.py new file mode 100644 index 000000000..ffa989bfc --- /dev/null +++ b/NY_Ball_Drop/debouncer.py @@ -0,0 +1,92 @@ +""" +GPIO Pin Debouncer + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +import time +import digitalio + +class Debouncer(object): + """Debounce an input pin""" + + DEBOUNCED_STATE = 0x01 + UNSTABLE_STATE = 0x02 + CHANGED_STATE = 0x04 + + + def __init__(self, pin, mode=None, interval=0.010): + """Make am instance. + :param int pin: the pin (from board) to debounce + :param int mode: digitalio.Pull.UP or .DOWN (default is no pull up/down) + :param int interval: bounce threshold in seconds (default is 0.010, i.e. 10 milliseconds) + """ + self.state = 0x00 + self.pin = digitalio.DigitalInOut(pin) + self.pin.direction = digitalio.Direction.INPUT + if mode != None: + self.pin.pull = mode + if self.pin.value: + self.__set_state(Debouncer.DEBOUNCED_STATE | Debouncer.UNSTABLE_STATE) + self.previous_time = 0 + if interval is None: + self.interval = 0.010 + else: + self.interval = interval + + + def __set_state(self, bits): + self.state |= bits + + + def __unset_state(self, bits): + self.state &= ~bits + + + def __toggle_state(self, bits): + self.state ^= bits + + + def __get_state(self, bits): + return (self.state & bits) != 0 + + + def update(self): + """Update the debouncer state. Must be called before using any of the properties below""" + self.__unset_state(Debouncer.CHANGED_STATE) + current_state = self.pin.value + if current_state != self.__get_state(Debouncer.UNSTABLE_STATE): + self.previous_time = time.monotonic() + self.__toggle_state(Debouncer.UNSTABLE_STATE) + else: + if time.monotonic() - self.previous_time >= self.interval: + if current_state != self.__get_state(Debouncer.DEBOUNCED_STATE): + self.previous_time = time.monotonic() + self.__toggle_state(Debouncer.DEBOUNCED_STATE) + self.__set_state(Debouncer.CHANGED_STATE) + + + @property + def value(self): + """Return the current debounced value of the input.""" + return self.__get_state(Debouncer.DEBOUNCED_STATE) + + + @property + def rose(self): + """Return whether the debounced input went from low to high at the most recent update.""" + return self.__get_state(self.DEBOUNCED_STATE) and self.__get_state(self.CHANGED_STATE) + + + @property + def fell(self): + """Return whether the debounced input went from high to low at the most recent update.""" + return (not self.__get_state(self.DEBOUNCED_STATE)) and self.__get_state(self.CHANGED_STATE) \ No newline at end of file