cm1.py
1 # SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 """ 6 Base class for Little Connection Machine projects. Allows drawing on a 7 single PIL image spanning eight IS31fl3731 Charlieplex matrices and handles 8 double-buffered updates. Matrices are arranged four across, two down, "the 9 tall way" (9 pixels across, 16 down) with I2C pins at the bottom. 10 11 IS31fl3731 can be jumpered for one of four addresses. Because this uses 12 eight, two groups are split across a pair of "soft" I2C buses by adding this 13 to /boot/config.txt: 14 dtoverlay=i2c-gpio,bus=2,i2c_gpio_scl=17,i2c_gpio_sda=27,i2c_gpio_delay_us=1 15 dtoverlay=i2c-gpio,bus=3,i2c_gpio_scl=23,i2c_gpio_sda=24,i2c_gpio_delay_us=1 16 And run: 17 pip3 install adafruit-extended-bus adafruit-circuitpython-is31fl3731 18 The extra buses will be /dev/i2c-2 and /dev/i2c-3. These are not as fast as 19 the "true" I2C bus, but are adequate for this application. 20 """ 21 22 import argparse 23 import signal 24 import sys 25 from PIL import Image 26 from PIL import ImageDraw 27 from adafruit_extended_bus import ExtendedI2C as I2C 28 from adafruit_is31fl3731.matrix import Matrix as Display 29 30 DEFAULT_BRIGHTNESS = 40 31 32 33 class CM1: 34 """A base class for Little Connection Machine projects, handling common 35 functionality like LED matrix init, updates and signal handler.""" 36 37 # pylint: disable=unused-argument 38 def __init__(self, *args, **kwargs): 39 self.brightness = DEFAULT_BRIGHTNESS 40 41 parser = argparse.ArgumentParser() 42 parser.add_argument( 43 "-b", 44 action="store", 45 help="Brightness, 0-255. Default: %d" % DEFAULT_BRIGHTNESS, 46 default=42, 47 type=int, 48 ) 49 args = parser.parse_args() 50 if args.b: 51 self.brightness = min(max(args.b, 0), 255) 52 53 i2c = [ 54 I2C(2), # Extended bus on 17, 27 (clock, data) 55 I2C(3), # Extended bus on 23, 24 (clock, data) 56 ] 57 self.display = [ 58 Display(i2c[0], address=0x74, frames=(0, 1)), # Upper row 59 Display(i2c[0], address=0x75, frames=(0, 1)), 60 Display(i2c[0], address=0x76, frames=(0, 1)), 61 Display(i2c[0], address=0x77, frames=(0, 1)), 62 Display(i2c[1], address=0x74, frames=(0, 1)), # Lower row 63 Display(i2c[1], address=0x75, frames=(0, 1)), 64 Display(i2c[1], address=0x76, frames=(0, 1)), 65 Display(i2c[1], address=0x77, frames=(0, 1)), 66 ] 67 self.image = Image.new("L", (9 * 4, 16 * 2)) 68 self.draw = ImageDraw.Draw(self.image) 69 self.frame_index = 0 # Front/back buffer index 70 signal.signal(signal.SIGTERM, self.signal_handler) # Kill signal 71 72 def run(self): 73 """Placeholder. Override this in subclass.""" 74 75 # pylint: disable=unused-argument 76 def signal_handler(self, signum, frame): 77 """Signal handler. Clears all matrices and exits.""" 78 self.clear() 79 self.redraw() 80 sys.exit(0) 81 82 def clear(self): 83 """Clears PIL image. Does not invoke refresh(), just clears.""" 84 self.draw.rectangle([0, 0, self.image.size[0], self.image.size[1]], fill=0) 85 86 def redraw(self): 87 """Update matrices with PIL image contents, swap buffers.""" 88 # First pass crops out sections over the overall image, rotates 89 # them to matrix space, and writes this data to each matrix. 90 for num, display in enumerate(self.display): 91 col = (num % 4) * 9 92 row = (num // 4) * 16 93 cropped = self.image.crop((col, row, col + 9, row + 16)) 94 cropped = cropped.rotate(angle=-90, expand=1) 95 display.image(cropped, frame=self.frame_index) 96 # Swapping frames is done in a separate pass so they all occur 97 # close together, no conspicuous per-matrix refresh. 98 for display in self.display: 99 display.frame(self.frame_index, show=True) # True = show frame 100 self.frame_index ^= 1 # Swap frame index 101 102 def process(self): 103 """Call CM1 subclass run() function, with keyboard interrupt trapping 104 centralized here so it doesn't need to be implemented everywhere.""" 105 try: 106 self.run() # In subclass 107 except KeyboardInterrupt: 108 self.signal_handler(0, 0) # clear/redraw/exit