code.py
  1  # SPDX-FileCopyrightText: 2020 Jeff Epler for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  import random
  6  import time
  7  
  8  import adafruit_imageload.bmp
  9  import audioio
 10  import audiomp3
 11  import board
 12  import displayio
 13  import digitalio
 14  import framebufferio
 15  import rgbmatrix
 16  
 17  displayio.release_displays()
 18  
 19  matrix = rgbmatrix.RGBMatrix(
 20      width=64, height=32, bit_depth=4,
 21      rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
 22      addr_pins=[board.A5, board.A4, board.A3, board.A2],
 23      clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
 24  display = framebufferio.FramebufferDisplay(matrix, auto_refresh=False)
 25  
 26  # Each wheel can be in one of three states:
 27  STOPPED, RUNNING, BRAKING = range(3)
 28  
 29  # Return a duplicate of the input list in a random (shuffled) order.
 30  def shuffled(seq):
 31      return sorted(seq, key=lambda _: random.random())
 32  
 33  # The Wheel class manages the state of one wheel. "pos" is a position in
 34  # floating point coordinates, with one 1 pixel being 1 position.
 35  # The wheel also has a velocity (in positions
 36  # per tick) and a state (one of the above constants)
 37  class Wheel(displayio.TileGrid):
 38      def __init__(self, bitmap, palette):
 39          # Portions of up to 3 tiles are visible.
 40          super().__init__(bitmap=bitmap, pixel_shader=palette,
 41                           width=1, height=3, tile_width=20, tile_height=24)
 42          self.order = shuffled(range(20))
 43          self.state = STOPPED
 44          self.pos = 0
 45          self.vel = 0
 46          self.termvel = 2
 47          self.y = 0
 48          self.x = 0
 49          self.stop_time = time.monotonic_ns()
 50          self.step()
 51  
 52      def step(self):
 53          # Update each wheel for one time step
 54          if self.state == RUNNING:
 55              # Slowly lose speed when running, but go at least terminal velocity
 56              self.vel = max(self.vel * .99, self.termvel)
 57              if time.monotonic_ns() > self.stop_time:
 58                  self.state = BRAKING
 59          elif self.state == BRAKING:
 60              # More quickly lose speed when baking, down to speed 0.4
 61              self.vel = max(self.vel * .85, 0.4)
 62  
 63          # Advance the wheel according to the velocity, and wrap it around
 64          # after 24*20 positions
 65          self.pos = (self.pos + self.vel) % (20*24)
 66  
 67          # Compute the rounded Y coordinate
 68          yy = round(self.pos)
 69          # Compute the offset of the tile (tiles are 24 pixels tall)
 70          yyy = yy % 24
 71          # Find out which tile is the top tile
 72          off = yy // 24
 73  
 74          # If we're braking and a tile is close to midscreen,
 75          # then stop and make sure that tile is exactly centered
 76          if self.state == BRAKING and self.vel <= 2 and yyy < 8:
 77              self.pos = off * 24
 78              self.vel = 0
 79              yyy = 0
 80              self.state = STOPPED
 81  
 82          # Move the displayed tiles to the correct height and make sure the
 83          # correct tiles are displayed.
 84          self.y = yyy - 20
 85          for i in range(3):
 86              self[i] = self.order[(19 - i + off) % 20]
 87  
 88      # Set the wheel running again, using a slight bit of randomness.
 89      # The 'i' value makes sure the first wheel brakes first, the second
 90      # brakes second, and the third brakes third.
 91      def kick(self, i):
 92          self.state = RUNNING
 93          self.vel = random.uniform(8, 10)
 94          self.termvel = random.uniform(1.8, 4.2)
 95          self.stop_time = time.monotonic_ns() + 3000000000 + i * 350000000
 96  
 97  
 98  # This bitmap contains the emoji we're going to use. It is assumed
 99  # to contain 20 icons, each 20x24 pixels. This fits nicely on the 64x32
100  # RGB matrix display.
101  the_bitmap, the_palette = adafruit_imageload.load(
102      "/emoji.bmp",
103      bitmap=displayio.Bitmap,
104      palette=displayio.Palette)
105  
106  # Our fruit machine has 3 wheels, let's create them with a correct horizontal
107  # (x) offset and arbitrary vertical (y) offset.
108  g = displayio.Group()
109  wheels = []
110  for idx in range(3):
111      wheel = Wheel(the_bitmap, the_palette)
112      wheel.x = idx * 22
113      wheel.y = -20
114      g.append(wheel)
115      wheels.append(wheel)
116  display.show(g)
117  
118  # We want a digital input to trigger the fruit machine
119  button = digitalio.DigitalInOut(board.A1)
120  button.switch_to_input(pull=digitalio.Pull.UP)
121  
122  # Enable the speaker
123  enable = digitalio.DigitalInOut(board.D4)
124  enable.switch_to_output(True)
125  
126  mp3file = open("/triangles-loop.mp3", "rb")
127  sample = audiomp3.MP3Decoder(mp3file)
128  
129  # Play the sample (just loop it for now)
130  speaker = audioio.AudioOut(board.A0)
131  speaker.play(sample, loop=True)
132  
133  # Here's the main loop
134  while True:
135      # Refresh the dislpay (doing this manually ensures the wheels move
136      # together, not at different times)
137      display.refresh(minimum_frames_per_second=0, target_frames_per_second=60)
138  
139      all_stopped = all(si.state == STOPPED for si in wheels)
140      if all_stopped:
141          # Once everything comes to a stop, wait until the lever is pulled and
142          # start everything over again.  Maybe you want to check if the
143          # combination is a "winner" and add a light show or something.
144  
145          while button.value:
146              pass
147          for idx, si in enumerate(wheels):
148              si.kick(idx)
149  
150      # Otherwise, let the wheels keep spinning...
151      for idx, si in enumerate(wheels):
152          si.step()