code.py
  1  # SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  """
  6  CircuitPython random blinkenlights for Little Connection Machine. For
  7  Raspberry Pi Pico RP2040, but could be adapted to other CircuitPython-
  8  capable boards with two or more I2C buses. Requires adafruit_bus_device
  9  and adafruit_is31fl3731 libraries.
 10  
 11  This code plays dirty pool to get fast matrix updates and is NOT good code
 12  to learn from, and might fail to work with future versions of the IS31FL3731
 13  library. But doing things The Polite Way wasn't fast enough. Explained as
 14  we go...
 15  """
 16  
 17  # pylint: disable=import-error
 18  import random
 19  import board
 20  import busio
 21  from adafruit_is31fl3731.matrix import Matrix as Display
 22  
 23  BRIGHTNESS = 40  # CONFIGURABLE: LED brightness, 0 (off) to 255 (max)
 24  PERCENT = 33  # CONFIGURABLE: amount of 'on' LEDs, 0 (none) to 100 (all)
 25  
 26  # This code was originally written for the Raspberry Pi Pico, but should be
 27  # portable to any CircuitPython-capable board WITH TWO OR MORE I2C BUSES.
 28  # IS31FL3731 can have one of four addresses, so to run eight of them we
 29  # need *two* I2C buses, and not all boards can provide that. Here's where
 30  # you'd define the pin numbers for a board...
 31  I2C1_SDA = board.GP18  # First I2C bus
 32  I2C1_SCL = board.GP19
 33  I2C2_SDA = board.GP16  # Second I2C bus
 34  I2C2_SCL = board.GP17
 35  
 36  # pylint: disable=too-few-public-methods
 37  class FakePILImage:
 38      """Minimal class meant to simulate a small subset of a Python PIL image,
 39      so we can pass it to the IS31FL3731 image() function later. THIS IS THE
 40      DIRTY POOL PART OF THE CODE, because CircuitPython doesn't have PIL,
 41      it's too much to handle. That image() function is normally meant for
 42      robust "desktop" Python, using the Blinka package...but it's still
 43      present (but normally goes unused) in CircuitPython. Having worked with
 44      that library source, I know exactly what object members its looking for,
 45      and can fake a minimal set here...BUT THIS MAY BREAK IF THE LIBRARY OR
 46      PIL CHANGES!"""
 47  
 48      def __init__(self):
 49          self.mode = "L"  # Grayscale mode in PIL
 50          self.size = (16, 9)  # 16x9 pixels
 51          self.pixels = bytearray(16 * 9)  # Pixel buffer
 52  
 53      def tobytes(self):
 54          """IS31 lib requests image pixels this way, more dirty pool."""
 55          return self.pixels
 56  
 57  
 58  # Okay, back to business...
 59  # Instantiate the two I2C buses. 400 KHz bus speed is recommended.
 60  # Default 100 KHz is a bit slow, and 1 MHz has occasional glitches.
 61  I2C = [
 62      busio.I2C(I2C1_SCL, I2C1_SDA, frequency=400000),
 63      busio.I2C(I2C2_SCL, I2C2_SDA, frequency=400000),
 64  ]
 65  # Four matrices on each bus, for a total of eight...
 66  DISPLAY = [
 67      Display(I2C[0], address=0x74, frames=(0, 1)),  # Upper row
 68      Display(I2C[0], address=0x75, frames=(0, 1)),
 69      Display(I2C[0], address=0x76, frames=(0, 1)),
 70      Display(I2C[0], address=0x77, frames=(0, 1)),
 71      Display(I2C[1], address=0x74, frames=(0, 1)),  # Lower row
 72      Display(I2C[1], address=0x75, frames=(0, 1)),
 73      Display(I2C[1], address=0x76, frames=(0, 1)),
 74      Display(I2C[1], address=0x77, frames=(0, 1)),
 75  ]
 76  
 77  IMAGE = FakePILImage()  # Instantiate fake PIL image object
 78  FRAME_INDEX = 0  # Double-buffering frame index
 79  
 80  while True:
 81      # Draw to each display's "back" frame buffer
 82      for disp in DISPLAY:
 83          for pixel in range(0, 16 * 9):  # Randomize each pixel
 84              IMAGE.pixels[pixel] = BRIGHTNESS if random.randint(1, 100) <= PERCENT else 0
 85          # Here's the function that we're NOT supposed to call in
 86          # CircuitPython, but is still present. This writes the pixel
 87          # data to the display's back buffer. Pass along our "fake" PIL
 88          # image and it accepts it.
 89          disp.image(IMAGE, frame=FRAME_INDEX)
 90  
 91      # Then quickly flip all matrix display buffers to FRAME_INDEX
 92      for disp in DISPLAY:
 93          disp.frame(FRAME_INDEX, show=True)
 94      FRAME_INDEX ^= 1  # Swap buffers
 95  
 96  
 97  # This is actually the LESS annoying way to get fast updates. Other involved
 98  # writing IS31 registers directly and accessing intended-as-private methods
 99  # in the IS31 lib. That's a really bad look. It's pretty simple here because
100  # this code is just drawing random dots. Producing a spatially-coherent
101  # image would take a lot more work, because matrices are rotated, etc.
102  # The PIL+Blinka code for Raspberry Pi easily handles such things, so
103  # consider working with that if you need anything more sophisticated.