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()