code.py
1 # SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 """ 6 Class based state machine implementation 7 8 Adafruit invests time and resources providing this open source code. 9 Please support Adafruit and open source hardware by purchasing 10 products from Adafruit! 11 12 Written by Dave Astels for Adafruit Industries 13 Copyright (c) 2018 Adafruit Industries 14 Licensed under the MIT license. 15 16 All text above must be included in any redistribution. 17 """ 18 19 # pylint: disable=global-statement,stop-iteration-return,no-self-use,useless-super-delegation 20 21 import time 22 import random 23 import board 24 import digitalio 25 import busio 26 import adafruit_ds3231 27 import audioio 28 import audiocore 29 import pwmio 30 from rainbowio import colorwheel 31 from adafruit_motor import servo 32 import neopixel 33 from adafruit_debouncer import Debouncer 34 35 # Set to false to disable testing/tracing code 36 TESTING = True 37 38 # Implementation dependant things to tweak 39 NUM_PIXELS = 8 # number of neopixels in the striup 40 DROP_THROTTLE = -0.2 # servo throttle during ball drop 41 DROP_DURATION = 10.0 # how many seconds the ball takes to drop 42 RAISE_THROTTLE = 0.3 # servo throttle while raising the ball 43 FIREWORKS_DURATION = 60.0 # how many second the fireworks last 44 45 # Pins 46 NEOPIXEL_PIN = board.D5 47 POWER_PIN = board.D10 48 SWITCH_PIN = board.D9 49 SERVO_PIN = board.A1 50 51 ################################################################################ 52 # Setup hardware 53 54 # Power to the speaker and neopixels must be enabled using this pin 55 56 enable = digitalio.DigitalInOut(POWER_PIN) 57 enable.direction = digitalio.Direction.OUTPUT 58 enable.value = True 59 60 i2c = busio.I2C(board.SCL, board.SDA) 61 rtc = adafruit_ds3231.DS3231(i2c) 62 63 audio = audioio.AudioOut(board.A0) 64 65 strip = neopixel.NeoPixel(NEOPIXEL_PIN, NUM_PIXELS, brightness=1, auto_write=False) 66 strip.fill(0) # NeoPixels off ASAP on startup 67 strip.show() 68 69 switch_io = digitalio.DigitalInOut(SWITCH_PIN) 70 switch_io.direction = digitalio.Direction.INPUT 71 switch_io.pull = digitalio.Pull.UP 72 switch = Debouncer(switch_io) 73 74 # create a PWMOut object on Pin A2. 75 pwm = pwmio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50) 76 77 # Create a servo object, my_servo. 78 servo = servo.ContinuousServo(pwm) 79 servo.throttle = 0.0 80 81 # Set the time for testing 82 # Once finished testing, the time can be set using the REPL using similar code 83 if TESTING: 84 # year, mon, date, hour, min, sec, wday, yday, isdst 85 t = time.struct_time((2018, 12, 31, 23, 58, 55, 1, -1, -1)) 86 # you must set year, mon, date, hour, min, sec and weekday 87 # yearday is not supported, isdst can be set but we don't do anything with it at this time 88 print("Setting time to:", t) 89 rtc.datetime = t 90 print() 91 92 ################################################################################ 93 # Global Variables 94 95 pixel_count = min([NUM_PIXELS // 2, 20]) 96 97 ################################################################################ 98 # Support functions 99 100 def log(s): 101 """Print the argument if testing/tracing is enabled.""" 102 if TESTING: 103 print(s) 104 105 # Random color 106 107 def random_color_byte(): 108 """ Return one of 32 evenly spaced byte values. 109 This provides random colors that are fairly distinctive.""" 110 return random.randrange(0, 256, 16) 111 112 def random_color(): 113 """Return a random color""" 114 red = random_color_byte() 115 green = random_color_byte() 116 blue = random_color_byte() 117 return (red, green, blue) 118 119 # Color cycling. 120 121 def cycle_sequence(seq): 122 while True: 123 for elem in seq: 124 yield elem 125 126 def rainbow_lamp(seq): 127 g = cycle_sequence(seq) 128 while True: 129 strip.fill(colorwheel(next(g))) 130 strip.show() 131 yield 132 133 # Fireworks effects 134 135 def burst(machine, time_now): 136 """Show a burst of color on all pixels, fading in, holding briefly, 137 then fading out. Each call to this does one step in that 138 process. Return True once the sequence is finished.""" 139 if machine.burst_count == 0: 140 strip.brightness = 0.0 141 strip.fill(machine.firework_color) 142 elif machine.burst_count == 22: 143 machine.firework_step_time = time_now + 0.3 144 return True 145 if time_now < machine.firework_step_time: 146 return False 147 elif machine.burst_count < 11: 148 strip.brightness = machine.burst_count / 10.0 149 machine.firework_step_time = time_now + 0.08 150 elif machine.burst_count == 11: 151 machine.firework_step_time = time_now + 0.3 152 elif machine.burst_count > 11: 153 strip.brightness = 1.0 - ((machine.burst_count - 11) / 10.0) 154 machine.firework_step_time = time_now + 0.08 155 strip.show() 156 machine.burst_count += 1 157 return False 158 159 def shower(machine, time_now): 160 """Show a shower of sparks effect. 161 Each call to this does one step in the process. Return True once the 162 sequence is finished.""" 163 if machine.shower_count == 0: # Initialize on the first step 164 strip.fill(0) 165 strip.brightness = 1.0 166 machine.pixels = [None] * pixel_count 167 machine.pixel_index = 0 168 if time_now < machine.firework_step_time: 169 return False 170 if machine.shower_count == NUM_PIXELS: 171 strip.fill(0) 172 strip.show() 173 return True 174 if machine.pixels[machine.pixel_index]: 175 strip[machine.pixels[machine.pixel_index]] = 0 176 random_pixel = random.randrange(NUM_PIXELS) 177 machine.pixels[machine.pixel_index] = random_pixel 178 strip[random_pixel] = machine.firework_color 179 strip.show() 180 machine.pixel_index = (machine.pixel_index + 1) % pixel_count 181 machine.shower_count += 1 182 machine.firework_step_time = time_now + 0.1 183 return False 184 185 def start_playing(fname): 186 sound_file = open(fname, 'rb') 187 wav = audiocore.WaveFile(sound_file) 188 audio.play(wav, loop=False) 189 190 def stop_playing(): 191 if audio.playing: 192 audio.stop() 193 194 195 ################################################################################ 196 # State Machine 197 198 class StateMachine(object): 199 200 def __init__(self): 201 self.state = None 202 self.states = {} 203 self.firework_color = 0 204 self.firework_step_time = 0 205 self.burst_count = 0 206 self.shower_count = 0 207 self.firework_stop_time = 0 208 self.paused_state = None 209 self.pixels = [] 210 self.pixel_index = 0 211 212 def add_state(self, state): 213 self.states[state.name] = state 214 215 def go_to_state(self, state_name): 216 if self.state: 217 log('Exiting %s' % (self.state.name)) 218 self.state.exit(self) 219 self.state = self.states[state_name] 220 log('Entering %s' % (self.state.name)) 221 self.state.enter(self) 222 223 def update(self): 224 if self.state: 225 log('Updating %s' % (self.state.name)) 226 self.state.update(self) 227 228 # When pausing, don't exit the state 229 def pause(self): 230 self.state = self.states['paused'] 231 log('Pausing') 232 self.state.enter(self) 233 234 # When resuming, don't re-enter the state 235 def resume_state(self, state_name): 236 if self.state: 237 log('Exiting %s' % (self.state.name)) 238 self.state.exit(self) 239 self.state = self.states[state_name] 240 log('Resuming %s' % (self.state.name)) 241 242 def reset_fireworks(self): 243 """As indicated, reset the fireworks system's variables.""" 244 self.firework_color = random_color() 245 self.burst_count = 0 246 self.shower_count = 0 247 self.firework_step_time = time.monotonic() + 0.05 248 strip.fill(0) 249 strip.show() 250 251 252 253 254 ################################################################################ 255 # States 256 257 258 # Abstract parent state class. 259 260 class State(object): 261 262 def __init__(self): 263 pass 264 265 @property 266 def name(self): 267 return '' 268 269 def enter(self, machine): 270 pass 271 272 def exit(self, machine): 273 pass 274 275 def update(self, machine): 276 if switch.fell: 277 machine.paused_state = machine.state.name 278 machine.pause() 279 return False 280 return True 281 282 283 # Wait for 10 seconds to midnight or the witch to be pressed, 284 # then drop the ball. 285 286 class WaitingState(State): 287 288 def __init__(self): 289 super().__init__() 290 291 @property 292 def name(self): 293 return 'waiting' 294 295 def enter(self, machine): 296 State.enter(self, machine) 297 298 def exit(self, machine): 299 State.exit(self, machine) 300 301 def almost_NY(self): 302 now = rtc.datetime 303 return (now.tm_mday == 31 and 304 now.tm_mon == 12 and 305 now.tm_hour == 23 and 306 now.tm_min == 59 and 307 now.tm_sec == 50) 308 309 def update(self, machine): 310 # No super call to check for switch press to pause 311 # switch press here drops the ball 312 if switch.fell or self.almost_NY(): 313 machine.go_to_state('dropping') 314 315 # Drop the ball, playing the countdown and showing 316 # a rainbow effect. 317 318 class DroppingState(State): 319 320 def __init__(self): 321 super().__init__() 322 self.rainbow = None 323 self.rainbow_time = 0 324 self.drop_finish_time = 0 325 326 @property 327 def name(self): 328 return 'dropping' 329 330 def enter(self, machine): 331 State.enter(self, machine) 332 now = time.monotonic() 333 start_playing('./countdown.wav') 334 servo.throttle = DROP_THROTTLE 335 self.rainbow = rainbow_lamp(range(0, 256, 2)) 336 self.rainbow_time = now + 0.1 337 self.drop_finish_time = now + DROP_DURATION 338 339 def exit(self, machine): 340 State.exit(self, machine) 341 servo.throttle = 0.0 342 stop_playing() 343 machine.reset_fireworks() 344 machine.firework_stop_time = time.monotonic() + FIREWORKS_DURATION 345 346 def update(self, machine): 347 if State.update(self, machine): 348 now = time.monotonic() 349 if now >= self.drop_finish_time: 350 machine.go_to_state('burst') 351 if now >= self.rainbow_time: 352 next(self.rainbow) 353 self.rainbow_time = now + 0.1 354 355 356 # Show a fireworks explosion: a burst of color. Then switch to a shower of sparks. 357 358 class BurstState(State): 359 360 def __init__(self): 361 super().__init__() 362 363 @property 364 def name(self): 365 return 'burst' 366 367 def enter(self, machine): 368 State.enter(self, machine) 369 370 def exit(self, machine): 371 State.exit(self, machine) 372 machine.shower_count = 0 373 374 def update(self, machine): 375 if State.update(self, machine): 376 if burst(machine, time.monotonic()): 377 machine.go_to_state('shower') 378 379 380 # Show a shower of sparks following an explosion 381 382 class ShowerState(State): 383 384 def __init__(self): 385 super().__init__() 386 387 @property 388 def name(self): 389 return 'shower' 390 391 def enter(self, machine): 392 State.enter(self, machine) 393 394 def exit(self, machine): 395 State.exit(self, machine) 396 machine.reset_fireworks() 397 398 def update(self, machine): 399 if State.update(self, machine): 400 if shower(machine, time.monotonic()): 401 if time.monotonic() >= machine.firework_stop_time: 402 machine.go_to_state('idle') 403 else: 404 machine.go_to_state('burst') 405 406 407 # Do nothing, wait to be reset 408 409 class IdleState(State): 410 411 def __init__(self): 412 super().__init__() 413 414 @property 415 def name(self): 416 return 'idle' 417 418 def enter(self, machine): 419 State.enter(self, machine) 420 421 def exit(self, machine): 422 State.exit(self, machine) 423 424 def update(self, machine): 425 State.update(self, machine) 426 427 428 # Reset the LEDs and audio, start the servo raising the ball 429 # When the switch is released, stop the ball and move to waiting 430 431 class RaisingState(State): 432 433 def __init__(self): 434 super().__init__() 435 436 @property 437 def name(self): 438 return 'raising' 439 440 def enter(self, machine): 441 State.enter(self, machine) 442 strip.fill(0) 443 strip.brightness = 1.0 444 strip.show() 445 if audio.playing: 446 audio.stop() 447 servo.throttle = RAISE_THROTTLE 448 449 def exit(self, machine): 450 State.exit(self, machine) 451 servo.throttle = 0.0 452 453 def update(self, machine): 454 if State.update(self, machine): 455 if switch.rose: 456 machine.go_to_state('waiting') 457 458 459 # Pause, resuming whem the switch is pressed again. 460 # Reset if the switch has been held for a second. 461 462 class PausedState(State): 463 464 def __init__(self): 465 super().__init__() 466 self.switch_pressed_at = 0 467 self.paused_servo = 0 468 469 @property 470 def name(self): 471 return 'paused' 472 473 def enter(self, machine): 474 State.enter(self, machine) 475 self.switch_pressed_at = time.monotonic() 476 if audio.playing: 477 audio.pause() 478 self.paused_servo = servo.throttle 479 servo.throttle = 0.0 480 481 def exit(self, machine): 482 State.exit(self, machine) 483 484 def update(self, machine): 485 if switch.fell: 486 if audio.paused: 487 audio.resume() 488 servo.throttle = self.paused_servo 489 self.paused_servo = 0.0 490 machine.resume_state(machine.paused_state) 491 elif not switch.value: 492 if time.monotonic() - self.switch_pressed_at > 1.0: 493 machine.go_to_state('raising') 494 495 ################################################################################ 496 # Create the state machine 497 498 pretty_state_machine = StateMachine() 499 pretty_state_machine.add_state(WaitingState()) 500 pretty_state_machine.add_state(DroppingState()) 501 pretty_state_machine.add_state(BurstState()) 502 pretty_state_machine.add_state(ShowerState()) 503 pretty_state_machine.add_state(IdleState()) 504 pretty_state_machine.add_state(RaisingState()) 505 pretty_state_machine.add_state(PausedState()) 506 507 pretty_state_machine.go_to_state('waiting') 508 509 while True: 510 switch.update() 511 pretty_state_machine.update()