Add LittleConnectionMachine project (Pi + CircuitPython)
This commit is contained in:
parent
94495f5af7
commit
c2f469b3ef
5 changed files with 539 additions and 0 deletions
103
LittleConnectionMachine/CircuitPython/code.py
Executable file
103
LittleConnectionMachine/CircuitPython/code.py
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
CircuitPython random blinkenlights for Little Connection Machine. For
|
||||
Raspberry Pi Pico RP2040, but could be adapted to other CircuitPython-
|
||||
capable boards with two or more I2C buses. Requires adafruit_bus_device
|
||||
and adafruit_is31fl3731 libraries.
|
||||
|
||||
This code plays dirty pool to get fast matrix updates and is NOT good code
|
||||
to learn from, and might fail to work with future versions of the IS31FL3731
|
||||
library. But doing things The Polite Way wasn't fast enough. Explained as
|
||||
we go...
|
||||
"""
|
||||
|
||||
# pylint: disable=import-error
|
||||
import random
|
||||
import board
|
||||
import busio
|
||||
from adafruit_is31fl3731.matrix import Matrix as Display
|
||||
|
||||
BRIGHTNESS = 40 # CONFIGURABLE: LED brightness, 0 (off) to 255 (max)
|
||||
PERCENT = 33 # CONFIGURABLE: amount of 'on' LEDs, 0 (none) to 100 (all)
|
||||
|
||||
# This code was originally written for the Raspberry Pi Pico, but should be
|
||||
# portable to any CircuitPython-capable board WITH TWO OR MORE I2C BUSES.
|
||||
# IS31FL3731 can have one of four addresses, so to run eight of them we
|
||||
# need *two* I2C buses, and not all boards can provide that. Here's where
|
||||
# you'd define the pin numbers for a board...
|
||||
I2C1_SDA = board.GP4 # First I2C bus
|
||||
I2C1_SCL = board.GP5
|
||||
I2C2_SDA = board.GP26 # Second I2C bus
|
||||
I2C2_SCL = board.GP27
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class FakePILImage:
|
||||
"""Minimal class meant to simulate a small subset of a Python PIL image,
|
||||
so we can pass it to the IS31FL3731 image() function later. THIS IS THE
|
||||
DIRTY POOL PART OF THE CODE, because CircuitPython doesn't have PIL,
|
||||
it's too much to handle. That image() function is normally meant for
|
||||
robust "desktop" Python, using the Blinka package...but it's still
|
||||
present (but normally goes unused) in CircuitPython. Having worked with
|
||||
that library source, I know exactly what object members its looking for,
|
||||
and can fake a minimal set here...BUT THIS MAY BREAK IF THE LIBRARY OR
|
||||
PIL CHANGES!"""
|
||||
|
||||
def __init__(self):
|
||||
self.mode = "L" # Grayscale mode in PIL
|
||||
self.size = (16, 9) # 16x9 pixels
|
||||
self.pixels = bytearray(16 * 9) # Pixel buffer
|
||||
|
||||
def tobytes(self):
|
||||
"""IS31 lib requests image pixels this way, more dirty pool."""
|
||||
return self.pixels
|
||||
|
||||
|
||||
# Okay, back to business...
|
||||
# Instantiate the two I2C buses. 400 KHz bus speed is recommended.
|
||||
# Default 100 KHz is a bit slow, and 1 MHz has occasional glitches.
|
||||
I2C = [
|
||||
busio.I2C(I2C1_SCL, I2C1_SDA, frequency=400000),
|
||||
busio.I2C(I2C2_SCL, I2C2_SDA, frequency=400000),
|
||||
]
|
||||
# Four matrices on each bus, for a total of eight...
|
||||
DISPLAY = [
|
||||
Display(I2C[0], address=0x74, frames=(0, 1)), # Upper row
|
||||
Display(I2C[0], address=0x75, frames=(0, 1)),
|
||||
Display(I2C[0], address=0x76, frames=(0, 1)),
|
||||
Display(I2C[0], address=0x77, frames=(0, 1)),
|
||||
Display(I2C[1], address=0x74, frames=(0, 1)), # Lower row
|
||||
Display(I2C[1], address=0x75, frames=(0, 1)),
|
||||
Display(I2C[1], address=0x76, frames=(0, 1)),
|
||||
Display(I2C[1], address=0x77, frames=(0, 1)),
|
||||
]
|
||||
|
||||
IMAGE = FakePILImage() # Instantiate fake PIL image object
|
||||
FRAME_INDEX = 0 # Double-buffering frame index
|
||||
|
||||
while True:
|
||||
# Draw to each display's "back" frame buffer
|
||||
for disp in DISPLAY:
|
||||
for pixel in range(0, 16 * 9): # Randomize each pixel
|
||||
IMAGE.pixels[pixel] = BRIGHTNESS if random.randint(1, 100) <= PERCENT else 0
|
||||
# Here's the function that we're NOT supposed to call in
|
||||
# CircuitPython, but is still present. This writes the pixel
|
||||
# data to the display's back buffer. Pass along our "fake" PIL
|
||||
# image and it accepts it.
|
||||
disp.image(IMAGE, frame=FRAME_INDEX)
|
||||
|
||||
# Then quickly flip all matrix display buffers to FRAME_INDEX
|
||||
for disp in DISPLAY:
|
||||
disp.frame(FRAME_INDEX, show=True)
|
||||
FRAME_INDEX ^= 1 # Swap buffers
|
||||
|
||||
|
||||
# This is actually the LESS annoying way to get fast updates. Other involved
|
||||
# writing IS31 registers directly and accessing intended-as-private methods
|
||||
# in the IS31 lib. That's a really bad look. It's pretty simple here because
|
||||
# this code is just drawing random dots. Producing a spatially-coherent
|
||||
# image would take a lot more work, because matrices are rotated, etc.
|
||||
# The PIL+Blinka code for Raspberry Pi easily handles such things, so
|
||||
# consider working with that if you need anything more sophisticated.
|
||||
212
LittleConnectionMachine/RaspberryPi/audio.py
Normal file
212
LittleConnectionMachine/RaspberryPi/audio.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Audio spectrum display for Little Connection Machine. This is designed to be
|
||||
fun to look at, not a Serious Audio Tool(tm). Requires USB microphone & ALSA
|
||||
config. Prerequisite libraries include PyAudio and NumPy:
|
||||
sudo apt-get install libatlas-base-dev libportaudio2
|
||||
pip3 install numpy pyaudio
|
||||
See the following for ALSA config (use Stretch directions):
|
||||
learn.adafruit.com/usb-audio-cards-with-a-raspberry-pi/updating-alsa-config
|
||||
"""
|
||||
|
||||
import math
|
||||
import time
|
||||
import numpy as np
|
||||
import pyaudio
|
||||
from cm1 import CM1
|
||||
|
||||
# FFT configurables. These numbers are 'hard,' actual figures:
|
||||
RATE = 11025 # For audio vis, don't want or need high sample rate!
|
||||
FFT_SIZE = 128 # Audio samples to read per frame (for FFT input)
|
||||
ROWS = 32 # FFT output filtered down to this many 'buckets'
|
||||
# Then things start getting subjective. For example, the lower and upper
|
||||
# ends of the FFT output don't make a good contribution to the resulting
|
||||
# graph...either too noisy, or out of musical range. Clip a range between
|
||||
# between 0 and FFT_SIZE-1. These aren't hard science, they were determined
|
||||
# by playing various music and seeing what looked good:
|
||||
LEAST = 1 # Lowest bin of FFT output to use
|
||||
MOST = 111 # Highest bin of FFT output to use
|
||||
# And moreso. Normally, FFT results are linearly spaced by frequency,
|
||||
# and with music this results in a crowded low end and sparse high end.
|
||||
# The visualizer reformats this logarithmically so octaves are linearly
|
||||
# spaced...the low end is expanded, upper end compressed. But just picking
|
||||
# individial FFT bins will cause visual dropouts. Instead, a number of
|
||||
# inputs are merged into each output, and because of the logarithmic scale,
|
||||
# that number needs to be focused near the low end and spread out among
|
||||
# many samples toward the top. Again, not scientific, these were derived
|
||||
# empirically by throwing music at it and adjusting:
|
||||
FIRST_WIDTH = 2 # Width of sampling curve at low end
|
||||
LAST_WIDTH = 40 # Width of sampling curve at high end
|
||||
# Except for ROWS above, none of this is involved in the actual rendering
|
||||
# of the graph, just how the data is massaged. If modifying this for your
|
||||
# own FFT-based visualizer, you could keep this around and just change the
|
||||
# drawing parts of the main loop.
|
||||
|
||||
|
||||
class AudioSpectrum(CM1):
|
||||
"""Audio spectrum display for Little Connection Machine."""
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # CM1 base initialization
|
||||
|
||||
# Access USB mic via PyAudio
|
||||
audio = pyaudio.PyAudio()
|
||||
self.stream = audio.open(
|
||||
format=pyaudio.paInt16, # 16-bit int
|
||||
channels=1, # Mono
|
||||
rate=RATE,
|
||||
input=True,
|
||||
output=False,
|
||||
frames_per_buffer=FFT_SIZE,
|
||||
)
|
||||
|
||||
# Precompute a few items for the math to follow
|
||||
first_center_log = math.log2(LEAST + 0.5)
|
||||
center_log_spread = math.log2(MOST + 0.5) - first_center_log
|
||||
width_low_log = math.log2(FIRST_WIDTH)
|
||||
width_log_spread = math.log2(LAST_WIDTH) - width_low_log
|
||||
|
||||
# As mentioned earlier, each row of the graph is filtered down from
|
||||
# multiple FFT elements. These lists are involved in that filtering,
|
||||
# each has one item per row of output:
|
||||
self.low_bin = [] # First FFT bin that contributes to row
|
||||
self.bin_weight = [] # List of subsequent FFT element weightings
|
||||
self.bin_sum = [] # Precomputed sum of bin_weight for row
|
||||
self.noise = [] # Subtracted from FFT output (see note later)
|
||||
|
||||
for row in range(ROWS): # For each row...
|
||||
# Calc center & spread of cubic curve for bin weighting
|
||||
center_log = first_center_log + center_log_spread * row / (ROWS - 1)
|
||||
center_linear = 2**center_log
|
||||
width_log = width_low_log + width_log_spread * row / (ROWS - 1)
|
||||
width_linear = 2**width_log
|
||||
half_width = width_linear * 0.5
|
||||
lower = center_linear - half_width
|
||||
upper = center_linear + half_width
|
||||
low_bin = int(lower) # First FFT element to use
|
||||
hi_bin = min(FFT_SIZE - 1, int(upper)) # Last "
|
||||
weights = [] # FFT weights for row
|
||||
for bin_num in range(low_bin, hi_bin + 1):
|
||||
bin_center = bin_num + 0.5
|
||||
dist = abs(bin_center - center_linear) / half_width
|
||||
if dist < 1.0: # Filter out a math stragglers at either end
|
||||
# Bin weights have a cubic falloff curve within range:
|
||||
dist = 1.0 - dist # Invert dist so 1.0 is at center
|
||||
weight = ((3.0 - (dist * 2.0)) * dist) * dist
|
||||
weights.append(weight)
|
||||
self.bin_weight.append(weights) # Save list of weights for row
|
||||
self.bin_sum.append(sum(weights)) # And sum of weights
|
||||
self.low_bin.append(low_bin) # And first FFT bin index
|
||||
# FFT output always has a little "sparkle" due to ambient hum.
|
||||
# Subtracting a bit helps. Noise varies per element, more at low
|
||||
# end...this table is just a non-scientific fudge factor...
|
||||
self.noise.append(int(2.4 ** (4 - 4 * row / ROWS)))
|
||||
|
||||
def run(self):
|
||||
"""Main loop for audio visualizer."""
|
||||
|
||||
# Some tables associated with each row of the display. These are
|
||||
# visualizer specific, not part of the FFT processing, so they're
|
||||
# here instead of part of the class above.
|
||||
width = [0 for _ in range(ROWS)] # Current row width
|
||||
peak = [0 for _ in range(ROWS)] # Recent row peak
|
||||
dropv = [0.0 for _ in range(ROWS)] # Current peak falling speed
|
||||
autolevel = [32.0 for _ in range(ROWS)] # Per-row auto adjust
|
||||
|
||||
start_time = time.monotonic()
|
||||
frames = 0
|
||||
|
||||
while True:
|
||||
|
||||
# Read bytes from PyAudio stream, convert to int16, process
|
||||
# via NumPy's FFT function...
|
||||
data_8 = self.stream.read(FFT_SIZE * 2, exception_on_overflow=False)
|
||||
data_16 = np.frombuffer(data_8, np.int16)
|
||||
fft_out = np.fft.fft(data_16, norm="ortho")
|
||||
# fft_out will have FFT_SIZE * 2 elements, mirrored at center
|
||||
|
||||
# Get spectrum of first half. Instead of square root for
|
||||
# magnitude, use something between square and cube root.
|
||||
# No scientific reason, just looked good.
|
||||
spec_y = [
|
||||
(c.real * c.real + c.imag * c.imag) ** 0.4 for c in fft_out[0:FFT_SIZE]
|
||||
]
|
||||
|
||||
self.clear() # Clear canvas before drawing
|
||||
for row in range(ROWS): # Low to high freq...
|
||||
# Weigh & sum up all the FFT outputs affecting this row
|
||||
total = 0
|
||||
for idx, weight in enumerate(self.bin_weight[row]):
|
||||
total += (spec_y[self.low_bin[row] + idx]) * weight
|
||||
total /= self.bin_sum[row]
|
||||
|
||||
# Auto-leveling is intended to make each column 'pop'.
|
||||
# When a particular column isn't getting a lot of input
|
||||
# from the FFT, gradually boost that column's sensitivity.
|
||||
if total > autolevel[row]: # New level is louder
|
||||
# Make autolevel rise quickly if column total exceeds it
|
||||
autolevel[row] = autolevel[row] * 0.25 + total * 0.75
|
||||
else: # New level is softer
|
||||
# And fall slowly otherwise
|
||||
autolevel[row] = autolevel[row] * 0.98 + total * 0.02
|
||||
# Autolevel limit keeps things from getting TOO boosty.
|
||||
# Trial and error, no science to this number.
|
||||
autolevel[row] = max(autolevel[row], 20)
|
||||
|
||||
# Apply autoleveling to weighted input.
|
||||
# This is the prelim. row width before further filtering...
|
||||
total *= 18 / autolevel[row] # 18 is 1/2 display width
|
||||
|
||||
# ...then filter the column width computed above
|
||||
if total > width[row]:
|
||||
# If it's greater than this column's current width,
|
||||
# move column's width quickly in that direction
|
||||
width[row] = width[row] * 0.3 + total * 0.7
|
||||
else:
|
||||
# If less, move slowly down
|
||||
width[row] = width[row] * 0.5 + total * 0.5
|
||||
|
||||
# Compute "peak dots," which sort of show the recent
|
||||
# peak level for each column (mostly just neat to watch).
|
||||
if width[row] > peak[row]:
|
||||
# If column exceeds old peak, move peak immediately,
|
||||
# give it a slight upward boost.
|
||||
dropv[row] = (peak[row] - width[row]) * 0.07
|
||||
peak[row] = min(width[row], 18)
|
||||
else:
|
||||
# Otherwise, peak gradually accelerates down
|
||||
dropv[row] += 0.2
|
||||
peak[row] -= dropv[row]
|
||||
|
||||
# Draw bar for this row. It's done as a gradient,
|
||||
# bright toward center, dim toward edge.
|
||||
iwidth = int(width[row] + 0.5) # Integer width
|
||||
drow = ROWS - 1 - row # Display row, reverse of freq row
|
||||
if iwidth > 0:
|
||||
iwidth = min(iwidth, 18) # Clip to 18 pixels
|
||||
scale = self.brightness * iwidth / 18 # Center brightness
|
||||
for col in range(iwidth):
|
||||
level = int(scale * ((1.0 - col / iwidth) ** 2.6))
|
||||
self.draw.point([17 - col, drow], fill=level)
|
||||
self.draw.point([18 + col, drow], fill=level)
|
||||
|
||||
# Draw peak dot
|
||||
if peak[row] > 0:
|
||||
col = int(peak[row] + 0.5)
|
||||
self.draw.point([17 - col, drow], fill=self.brightness)
|
||||
self.draw.point([18 + col, drow], fill=self.brightness)
|
||||
|
||||
# Update matrices and show est. frames/second
|
||||
self.redraw()
|
||||
frames += 1
|
||||
elapsed = time.monotonic() - start_time
|
||||
print(frames / elapsed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
MY_APP = AudioSpectrum() # Instantiate class, calls __init__() above
|
||||
MY_APP.process()
|
||||
70
LittleConnectionMachine/RaspberryPi/chaser.py
Normal file
70
LittleConnectionMachine/RaspberryPi/chaser.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Chaser lights for Little Connection Machine. Random bit patterns shift
|
||||
left or right in groups of four rows. Nothing functional, just looks cool.
|
||||
Inspired by Jurassic Park's blinky CM-5 prop. As it's a self-contained demo
|
||||
and not connected to any system services or optional installations, this is
|
||||
the simplest of the Little Connection Machine examples, making it a good
|
||||
starting point for your own projects...there's the least to rip out here!
|
||||
"""
|
||||
|
||||
import random
|
||||
import time
|
||||
from cm1 import CM1
|
||||
|
||||
DENSITY = 30 # Percentage of bits to set (0-100)
|
||||
FPS = 6 # Frames/second to update (roughly)
|
||||
|
||||
|
||||
def randbit(bitpos):
|
||||
"""Return a random bit value based on the global DENSITY percentage,
|
||||
shifted into position 'bitpos' (typically 0 or 8 for this code)."""
|
||||
return (random.randint(1, 100) > (100 - DENSITY)) << bitpos
|
||||
|
||||
|
||||
class Chaser(CM1):
|
||||
"""Purely decorative chaser lights for Little Connection Machine."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs) # CM1 base initialization
|
||||
# Initialize all bits to 0. 32 rows, 4 columns of 9-bit patterns.
|
||||
self.bits = [[0 for _ in range(4)] for _ in range(32)]
|
||||
|
||||
def run(self):
|
||||
"""Main loop for Little Connection Machine chaser lights."""
|
||||
|
||||
last_redraw_time = time.monotonic()
|
||||
interval = 1 / FPS # Frame-to-frame time
|
||||
|
||||
while True:
|
||||
self.clear() # Clear PIL self.image, part of the CM1 base class
|
||||
for row in range(self.image.height): # For each row...
|
||||
for col in range(4): # For each of 4 columns...
|
||||
# Rows operate in groups of 4. Some shift left, others
|
||||
# shift right. Empty spots are filled w/random bits.
|
||||
if row & 4:
|
||||
self.bits[row][col] = (self.bits[row][col] >> 1) + randbit(8)
|
||||
else:
|
||||
self.bits[row][col] = (self.bits[row][col] << 1) + randbit(0)
|
||||
# Draw new bit pattern into image...
|
||||
xoffset = col * 9
|
||||
for bit in range(9):
|
||||
mask = 0x100 >> bit
|
||||
if self.bits[row][col] & mask:
|
||||
# self.draw is PIL draw object in CM1 base class
|
||||
self.draw.point([xoffset + bit, row], fill=self.brightness)
|
||||
|
||||
# Dillydally to roughly frames/second refresh. Preferable to
|
||||
# time.sleep() because bit-drawing above isn't deterministic.
|
||||
while (time.monotonic() - last_redraw_time) < interval:
|
||||
pass
|
||||
last_redraw_time = time.monotonic()
|
||||
self.redraw()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
MY_APP = Chaser() # Instantiate class, calls __init__() above
|
||||
MY_APP.process()
|
||||
108
LittleConnectionMachine/RaspberryPi/cm1.py
Normal file
108
LittleConnectionMachine/RaspberryPi/cm1.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
Base class for Little Connection Machine projects. Allows drawing on a
|
||||
single PIL image spanning eight IS31fl3731 Charlieplex matrices and handles
|
||||
double-buffered updates. Matrices are arranged four across, two down, "the
|
||||
tall way" (9 pixels across, 16 down) with I2C pins at the bottom.
|
||||
|
||||
IS31fl3731 can be jumpered for one of four addresses. Because this uses
|
||||
eight, two groups are split across a pair of "soft" I2C buses by adding this
|
||||
to /boot/config.txt:
|
||||
dtoverlay=i2c-gpio,bus=2,i2c_gpio_scl=17,i2c_gpio_sda=27,i2c_gpio_delay_us=1
|
||||
dtoverlay=i2c-gpio,bus=3,i2c_gpio_scl=23,i2c_gpio_sda=24,i2c_gpio_delay_us=1
|
||||
And run:
|
||||
pip3 install adafruit-extended-bus adafruit-circuitpython-is31fl3731
|
||||
The extra buses will be /dev/i2c-2 and /dev/i2c-3. These are not as fast as
|
||||
the "true" I2C bus, but are adequate for this application.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import signal
|
||||
import sys
|
||||
from PIL import Image
|
||||
from PIL import ImageDraw
|
||||
from adafruit_extended_bus import ExtendedI2C as I2C
|
||||
from adafruit_is31fl3731.matrix import Matrix as Display
|
||||
|
||||
DEFAULT_BRIGHTNESS = 40
|
||||
|
||||
|
||||
class CM1:
|
||||
"""A base class for Little Connection Machine projects, handling common
|
||||
functionality like LED matrix init, updates and signal handler."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.brightness = DEFAULT_BRIGHTNESS
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
action="store",
|
||||
help="Brightness, 0-255. Default: %d" % DEFAULT_BRIGHTNESS,
|
||||
default=42,
|
||||
type=int,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.b:
|
||||
self.brightness = min(max(args.b, 0), 255)
|
||||
|
||||
i2c = [
|
||||
I2C(2), # Extended bus on 17, 27 (clock, data)
|
||||
I2C(3), # Extended bus on 23, 24 (clock, data)
|
||||
]
|
||||
self.display = [
|
||||
Display(i2c[0], address=0x74, frames=(0, 1)), # Upper row
|
||||
Display(i2c[0], address=0x75, frames=(0, 1)),
|
||||
Display(i2c[0], address=0x76, frames=(0, 1)),
|
||||
Display(i2c[0], address=0x77, frames=(0, 1)),
|
||||
Display(i2c[1], address=0x74, frames=(0, 1)), # Lower row
|
||||
Display(i2c[1], address=0x75, frames=(0, 1)),
|
||||
Display(i2c[1], address=0x76, frames=(0, 1)),
|
||||
Display(i2c[1], address=0x77, frames=(0, 1)),
|
||||
]
|
||||
self.image = Image.new("L", (9 * 4, 16 * 2))
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.frame_index = 0 # Front/back buffer index
|
||||
signal.signal(signal.SIGTERM, self.signal_handler) # Kill signal
|
||||
|
||||
def run(self):
|
||||
"""Placeholder. Override this in subclass."""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def signal_handler(self, signum, frame):
|
||||
"""Signal handler. Clears all matrices and exits."""
|
||||
self.clear()
|
||||
self.redraw()
|
||||
sys.exit(0)
|
||||
|
||||
def clear(self):
|
||||
"""Clears PIL image. Does not invoke refresh(), just clears."""
|
||||
self.draw.rectangle([0, 0, self.image.size[0], self.image.size[1]], fill=0)
|
||||
|
||||
def redraw(self):
|
||||
"""Update matrices with PIL image contents, swap buffers."""
|
||||
# First pass crops out sections over the overall image, rotates
|
||||
# them to matrix space, and writes this data to each matrix.
|
||||
for num, display in enumerate(self.display):
|
||||
col = (num % 4) * 9
|
||||
row = (num // 4) * 16
|
||||
cropped = self.image.crop((col, row, col + 9, row + 16))
|
||||
cropped = cropped.rotate(angle=-90, expand=1)
|
||||
display.image(cropped, frame=self.frame_index)
|
||||
# Swapping frames is done in a separate pass so they all occur
|
||||
# close together, no conspicuous per-matrix refresh.
|
||||
for display in self.display:
|
||||
display.frame(self.frame_index, show=True) # True = show frame
|
||||
self.frame_index ^= 1 # Swap frame index
|
||||
|
||||
def process(self):
|
||||
"""Call CM1 subclass run() function, with keyboard interrupt trapping
|
||||
centralized here so it doesn't need to be implemented everywhere."""
|
||||
try:
|
||||
self.run() # In subclass
|
||||
except KeyboardInterrupt:
|
||||
self.signal_handler(0, 0) # clear/redraw/exit
|
||||
46
LittleConnectionMachine/RaspberryPi/cpuload.py
Normal file
46
LittleConnectionMachine/RaspberryPi/cpuload.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""
|
||||
CPU load blinkenlights for Little Connection Machine. Random bits flicker
|
||||
in response to overall processor load across all cores. This relies on the
|
||||
'psutil' Python module, which should already be present in Raspbian OS,
|
||||
but if not, install with: pip install psutil
|
||||
|
||||
psutil can provide all sorts of system information -- per-core load, network
|
||||
use, temperature and more -- but this example is meant to be minimal,
|
||||
understandable and work across different Pi models (even single-core).
|
||||
Consider it a stepping off point for your own customizations. See psutil
|
||||
documentation for ideas: https://pypi.org/project/psutil/
|
||||
|
||||
Since this program itself comprises part of the overall CPU load, the LEDs
|
||||
some amount of LEDs will always be blinking. This is normal and by design.
|
||||
"""
|
||||
|
||||
import random
|
||||
import psutil
|
||||
from cm1 import CM1
|
||||
|
||||
|
||||
class CPULoad(CM1):
|
||||
"""Simple CPU load blinkenlights for Little Connection Machine."""
|
||||
|
||||
def run(self):
|
||||
"""Main loop for Little Connection Machine CPU load blinkies."""
|
||||
|
||||
while True:
|
||||
self.clear() # Clear PIL self.image, part of the CM1 base class
|
||||
blinkyness = 100 - int(psutil.cpu_percent() + 0.5)
|
||||
for row in range(self.image.height):
|
||||
for col in range(self.image.width):
|
||||
if random.randint(1, 100) > blinkyness:
|
||||
# self.draw is PIL draw object in CM1 base class
|
||||
self.draw.point([col, row], fill=self.brightness)
|
||||
self.redraw()
|
||||
# No delay in this example, just run full-tilt!
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
MY_APP = CPULoad() # Instantiate class, calls __init__() above
|
||||
MY_APP.process()
|
||||
Loading…
Reference in a new issue