rework
This commit is contained in:
+39
-185
@@ -1,193 +1,47 @@
|
||||
import os
|
||||
import govee
|
||||
import pygame
|
||||
from time import sleep
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from rgbmatrix import RGBMatrix, RGBMatrixOptions
|
||||
from dotenv import load_dotenv
|
||||
from utils.vars import Colors, LOGO_DIR, ASSET_DIR
|
||||
from PIL import Image
|
||||
import os
|
||||
import vars
|
||||
|
||||
# modes
|
||||
import modes.score as score_mode
|
||||
import modes.fantasy as fantasy_mode
|
||||
class ScoreboardMatrix():
|
||||
logo_cache = {}
|
||||
current_slide = "score"
|
||||
slide_count = 0
|
||||
options = RGBMatrixOptions()
|
||||
options.rows = 32
|
||||
options.cols = 64
|
||||
options.chain_length = 4
|
||||
options.parallel = 1
|
||||
options.hardware_mapping = "regular"
|
||||
options.gpio_slowdown = 5
|
||||
options.disable_hardware_pulsing = True
|
||||
options.brightness = 80
|
||||
|
||||
# --- Load environment vars ---
|
||||
load_dotenv()
|
||||
matrix = RGBMatrix(options=options)
|
||||
canvas = matrix.CreateFrameCanvas()
|
||||
|
||||
# --- Matrix config ---
|
||||
options = RGBMatrixOptions()
|
||||
options.rows = 32
|
||||
options.cols = 64
|
||||
options.chain_length = 4
|
||||
options.parallel = 1
|
||||
options.hardware_mapping = "regular"
|
||||
options.gpio_slowdown = 5
|
||||
options.disable_hardware_pulsing = True
|
||||
options.brightness = 80
|
||||
def load_logo_to_image(self, league, abbr, width, height, x_offset, y_offset):
|
||||
key = f"{league}_{abbr}.png"
|
||||
logo_path = os.path.join(vars.LOGOS_DIR, key)
|
||||
if not os.path.exists(logo_path):
|
||||
return None
|
||||
|
||||
if key in self.logo_cache:
|
||||
loaded_image = self.logo_cache[key]
|
||||
else:
|
||||
loaded_image = Image.open(logo_path).convert("RGB")
|
||||
self.logo_cache[key] = loaded_image
|
||||
|
||||
image = Image.new("RGB", (width, height), (0,0,0))
|
||||
image.paste(loaded_image.resize((width, height), (x_offset, y_offset)))
|
||||
|
||||
matrix = RGBMatrix(options=options)
|
||||
canvas = matrix.CreateFrameCanvas()
|
||||
return image
|
||||
|
||||
# --- Govee API ---
|
||||
if os.environ.get('GOVEE_API_KEY'):
|
||||
govee_api = govee.GoveeApi(key=os.environ["GOVEE_API_KEY"])
|
||||
def iterate_slide_count(self):
|
||||
self.slide_count += 1
|
||||
|
||||
# --- PyGame Audio ---
|
||||
# FIX: guard pygame init so a missing audio device on headless Pi doesn't segfault
|
||||
try:
|
||||
pygame.mixer.init()
|
||||
audio_available = True
|
||||
except pygame.error as e:
|
||||
print(f"Audio init failed: {e}")
|
||||
audio_available = False
|
||||
def next_slide(self, slide_name):
|
||||
self.slide_count = 0
|
||||
self.current_slide = slide_name
|
||||
|
||||
# --- Goal celebrations ---
|
||||
def render_goal_frame(text, text_scale, bg_color, text_color):
|
||||
big_h = max(8, int(32 * text_scale))
|
||||
big_img = Image.new("RGB", (1024, 128), bg_color)
|
||||
big_draw = ImageDraw.Draw(big_img)
|
||||
|
||||
try:
|
||||
pil_font = ImageFont.truetype(
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", big_h
|
||||
)
|
||||
except:
|
||||
pil_font = ImageFont.load_default()
|
||||
|
||||
bbox = big_draw.textbbox((0, 0), text, font=pil_font)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
|
||||
tx = (1024 - tw) // 2
|
||||
ty = (128 - th) // 2 - bbox[1]
|
||||
big_draw.text((tx, ty), text, font=pil_font, fill=text_color)
|
||||
|
||||
scaled = big_img.resize((256, 32), Image.LANCZOS)
|
||||
|
||||
logo_path = os.path.join(LOGO_DIR, "nhl_BUF.png")
|
||||
if os.path.exists(logo_path):
|
||||
try:
|
||||
logo = Image.open(logo_path).convert("RGBA")
|
||||
logo = logo.resize((28, 28), Image.LANCZOS)
|
||||
|
||||
r, g, b, a = logo.split()
|
||||
logo_rbg = Image.merge("RGBA", (r, b, g, a))
|
||||
|
||||
pixels = logo_rbg.load()
|
||||
for px in range(logo_rbg.width):
|
||||
for py in range(logo_rbg.height):
|
||||
rv, gv, bv, av = pixels[px, py]
|
||||
if rv < 30 and gv < 30 and bv < 30:
|
||||
pixels[px, py] = (rv, gv, bv, 0)
|
||||
|
||||
bg_left = Image.new("RGBA", (28, 28), bg_color + (255,))
|
||||
bg_left.paste(logo_rbg, mask=logo_rbg.split()[3])
|
||||
scaled.paste(bg_left.convert("RGB"), (2, 2))
|
||||
|
||||
bg_right = Image.new("RGBA", (28, 28), bg_color + (255,))
|
||||
bg_right.paste(logo_rbg, mask=logo_rbg.split()[3])
|
||||
scaled.paste(bg_right.convert("RGB"), (226, 2))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Logo paste error: {e}")
|
||||
|
||||
return scaled
|
||||
|
||||
def play_goal_celebration(text, color1, color2):
|
||||
global canvas
|
||||
|
||||
# Phase 1: zoom in from tiny to full, alternating bg color
|
||||
zoom_steps = [0.1, 0.2, 0.35, 0.5, 0.65, 0.8, 0.95, 1.1, 1.0]
|
||||
for _ in range(5):
|
||||
for i, scale in enumerate(zoom_steps):
|
||||
bg = color1 if i % 2 == 0 else color2
|
||||
fg = color2 if i % 2 == 0 else color1
|
||||
frame = render_goal_frame(text, scale, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.05)
|
||||
|
||||
# Phase 2: rapid flashing at full size
|
||||
for i in range(10):
|
||||
bg = color1 if i % 2 == 0 else color2
|
||||
fg = color2 if i % 2 == 0 else color1
|
||||
frame = render_goal_frame(text, 1.0, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.12)
|
||||
|
||||
# Phase 3: zoom back out and fade to white flash
|
||||
zoom_out = [1.0, 1.1, 1.2, 1.3, 1.4]
|
||||
for i, scale in enumerate(zoom_out):
|
||||
bg = color2 if i % 2 == 0 else color1
|
||||
fg = color1 if i % 2 == 0 else color2
|
||||
frame = render_goal_frame(text, scale, bg, fg)
|
||||
canvas.Clear()
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.08)
|
||||
|
||||
# Phase 4: white flash to end
|
||||
for _ in range(3):
|
||||
canvas.Clear()
|
||||
frame = render_goal_frame(
|
||||
text, 1.0, Colors.SABRES_GOLD.value, Colors.SABRES_BLUE.value
|
||||
)
|
||||
draw_pil_image(canvas, frame)
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
sleep(0.1)
|
||||
|
||||
# FIX: don't reassign canvas on .Clear() — it returns None in some bindings
|
||||
canvas.Clear()
|
||||
canvas = matrix.SwapOnVSync(canvas)
|
||||
|
||||
# stop music if playing
|
||||
if audio_available:
|
||||
pygame.mixer.music.stop()
|
||||
|
||||
sleep(0.5)
|
||||
|
||||
def play_audio(filename):
|
||||
if not audio_available:
|
||||
return
|
||||
pygame.mixer.music.load(os.path.join(ASSET_DIR, filename))
|
||||
pygame.mixer.music.play()
|
||||
|
||||
# --- Utilities ---
|
||||
def draw_pil_image(canvas, img):
|
||||
# FIX: ensure RGB (no alpha channel) to avoid 4-tuple unpack crash
|
||||
img = img.convert("RGB")
|
||||
for x in range(img.width):
|
||||
for y in range(img.height):
|
||||
# FIX: bounds check so we never call SetPixel out of matrix range
|
||||
if x >= 256 or y >= 32:
|
||||
continue
|
||||
r, g, b = img.getpixel((x, y))
|
||||
canvas.SetPixel(x, y, b, g, r) # bgr panels
|
||||
|
||||
def run():
|
||||
global canvas
|
||||
times_ran = 0
|
||||
supported_modes = ["fantasy", "score"]
|
||||
current_mode = 0
|
||||
|
||||
while True:
|
||||
if times_ran >= 3:
|
||||
times_ran = 0
|
||||
current_mode += 1
|
||||
if current_mode >= len(supported_modes):
|
||||
current_mode = 0
|
||||
|
||||
if supported_modes[current_mode] == "score":
|
||||
canvas_ref = score_mode.draw_frame(canvas)
|
||||
canvas = matrix.SwapOnVSync(canvas_ref)
|
||||
elif supported_modes[current_mode] == "fantasy":
|
||||
canvas_ref = fantasy_mode.draw_frame(canvas)
|
||||
canvas = matrix.SwapOnVSync(canvas_ref)
|
||||
|
||||
times_ran += 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
Reference in New Issue
Block a user