actually scroll

This commit is contained in:
2026-06-21 01:02:07 -04:00
parent 8cf50c9413
commit 2f0c8f9afd
2 changed files with 174 additions and 177 deletions
+172 -175
View File
@@ -1,220 +1,217 @@
import requests import requests
import utils.logos as logos import utils.logos as logos
from PIL import Image, ImageDraw
from rgbmatrix import graphics from rgbmatrix import graphics
from utils.colors import Colors from utils.colors import Colors
from utils.fonts import font, font_small from utils.fonts import font, font_small
from time import time from time import time
games = [] # --- State ---
last_fetch = 0 _games = []
current_page = 0 _last_fetch = 0
page_display_time = 8 _preferred_games = []
last_switch = time() _preferred_teams = [
show_preferred = True
current_preferred_game = 0
preferred_games = []
preferred_teams = [
("BUF", "nfl"), ("BUF", "nfl"),
("BUF", "nhl"), ("BUF", "nhl"),
("TOR", "mlb"), ("TOR", "mlb"),
("LAL", "nba"), ("LAL", "nba"),
] ]
# --- Fetch scores --- # Carousel scroll state
def get_scores(sport, league): _scroll_x = 0
_scroll_speed = 1 # pixels per frame
_frames_per_tick = 2 # how many main loop ticks per scroll step (lower = faster)
_tick = 0
_virtual_canvas = None # PIL Image of the full wide render
_virtual_dirty = True # rebuild the virtual canvas on next frame
PANEL_WIDTH = 256 # 4 × 64px panels
PANEL_HEIGHT = 32
GAME_WIDTH = 64 # each game slot is one panel wide
DIVIDER_COLOR = (40, 40, 40)
# --- Fetch ---
def _get_scores(sport, league):
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard" url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard"
try: try:
resp = requests.get(url, timeout=5) resp = requests.get(url, timeout=5)
resp.raise_for_status() resp.raise_for_status()
games = [] result = []
for event in resp.json().get("events", []): for event in resp.json().get("events", []):
comp = event["competitions"][0] comp = event["competitions"][0]
teams = comp["competitors"] teams = comp["competitors"]
home = next(t for t in teams if t["homeAway"] == "home") home = next(t for t in teams if t["homeAway"] == "home")
away = next(t for t in teams if t["homeAway"] == "away") away = next(t for t in teams if t["homeAway"] == "away")
status = event["status"]["type"]["shortDetail"] status = event["status"]["type"]["shortDetail"]
games.append( result.append({
{ "league": league,
"league": league, "away": away["team"]["abbreviation"].upper(),
"away": away["team"]["abbreviation"].upper(), "away_score": away["score"],
"away_score": away["score"], "home": home["team"]["abbreviation"].upper(),
"home": home["team"]["abbreviation"].upper(), "home_score": home["score"],
"home_score": home["score"], "status": status,
"status": status, "id": event["id"],
"id": event["id"], })
} return result
)
return games
except Exception as e: except Exception as e:
print(f"Fetch error ({league}): {e}") print(f"Fetch error ({league}): {e}")
return [] return []
def get_all_scores(): def _get_all_scores():
print('fetching game scores from espn') print("fetching game scores from espn")
games = [] result = []
games += get_scores("hockey", "nhl") result += _get_scores("hockey", "nhl")
games += get_scores("football", "nfl") result += _get_scores("football", "nfl")
games += get_scores("basketball", "nba") result += _get_scores("basketball", "nba")
games += get_scores("baseball", "mlb") result += _get_scores("baseball", "mlb")
return games return result
# --- Game drawing --- # --- Build ordered game list: preferred first, then rest ---
def draw_all_games(canvas, games, start_index): def _ordered_games():
for i in range(4): preferred_ids = set(_preferred_games)
game_index = (start_index + i) % len(games) preferred = [g for g in _games if g["id"] in preferred_ids]
game = games[game_index] others = [g for g in _games if g["id"] not in preferred_ids]
offset = i * 64 return preferred + others
league = game["league"] # --- Render a single game slot into a PIL image at a given x offset ---
away_logo = logos.load_logo(league, game["away"]) def _render_game_to_pil(img, game, x_offset):
home_logo = logos.load_logo(league, game["home"]) draw = ImageDraw.Draw(img)
logos.draw_logo(canvas, away_logo, offset + 0, 0)
logos.draw_logo(canvas, home_logo, offset + 0, 16)
graphics.DrawText(
canvas,
font_small,
offset + 18,
11,
graphics.Color(*Colors.RED.value),
game["away"],
)
graphics.DrawText(
canvas,
font_small,
offset + 18,
27,
graphics.Color(*Colors.WHITE.value),
game["home"],
)
graphics.DrawText(
canvas,
font,
offset + 40,
13,
graphics.Color(*Colors.WHITE.value),
str(game["away_score"]),
)
graphics.DrawText(
canvas,
font,
offset + 40,
29,
graphics.Color(*Colors.WHITE.value),
str(game["home_score"]),
)
if i < 3:
for row in range(32):
canvas.SetPixel(offset + 63, row, 40, 40, 40)
def draw_single_game(canvas, game):
league = game["league"] league = game["league"]
home_logo = logos.load_logo(league, game["home"])
# logos
away_logo = logos.load_logo(league, game["away"]) away_logo = logos.load_logo(league, game["away"])
home_logo = logos.load_logo(league, game["home"])
if away_logo:
# logos.load_logo returns an RGB PIL image — paste directly
img.paste(away_logo.resize((14, 14)), (x_offset, 0))
if home_logo:
img.paste(home_logo.resize((14, 14)), (x_offset, 16))
logos.draw_logo(canvas, away_logo, 0, 0) # divider on right edge (except last slot handled by wrapping)
logos.draw_logo(canvas, home_logo, 0, 16) for row in range(PANEL_HEIGHT):
img.putpixel((x_offset + GAME_WIDTH - 1, row), DIVIDER_COLOR)
graphics.DrawText( # --- Build the full virtual PIL canvas for all ordered games ---
canvas, def _build_virtual_canvas():
font_small, ordered = _ordered_games()
18, if not ordered:
11, return None
graphics.Color(*Colors.WHITE.value),
game["away"],
)
graphics.DrawText(
canvas,
font_small,
18,
27,
graphics.Color(*Colors.WHITE.value),
game["home"],
)
graphics.DrawText( # wide enough for all games, plus one extra copy at the end for seamless wrap
canvas, total_games = len(ordered)
font, total_width = GAME_WIDTH * (total_games + 4) # +4 so wrap tail fills display
40, img = Image.new("RGB", (total_width, PANEL_HEIGHT), (0, 0, 0))
13,
graphics.Color(*Colors.WHITE.value),
str(game["away_score"]),
)
graphics.DrawText(
canvas,
font,
40,
29,
graphics.Color(*Colors.WHITE.value),
str(game["home_score"]),
)
graphics.DrawText(
canvas,
font,
55,
22,
graphics.Color(*Colors.WHITE.value),
str(game["status"])
)
for i, game in enumerate(ordered * 2): # duplicate for seamless wrap
if i >= total_games + 4:
break
_render_game_to_pil(img, game, i * GAME_WIDTH)
return img, total_games
# --- Blit a 256-wide slice of the virtual canvas onto the rgbmatrix canvas ---
def _blit_slice(canvas, pil_img, x_offset):
total_width = pil_img.width
for x in range(PANEL_WIDTH):
src_x = (x_offset + x) % total_width
for y in range(PANEL_HEIGHT):
r, g, b = pil_img.getpixel((src_x, y))
canvas.SetPixel(x, y, b, g, r) # BGR panels
# --- Draw text onto the virtual canvas using PIL (since rgbmatrix fonts need a real canvas) ---
# We use rgbmatrix DrawText on the live canvas offset by -scroll_x for text only,
# and PIL for logos/backgrounds. See draw_frame() for how these combine.
def _draw_text_overlay(canvas, ordered, scroll_x):
"""Draw all game text onto the rgbmatrix canvas accounting for scroll offset."""
total_width = GAME_WIDTH * len(ordered)
for i, game in enumerate(ordered):
base_x = (i * GAME_WIDTH) - scroll_x
# draw twice to handle the wrap-around copy
for wrap in [0, total_width]:
x = base_x + wrap
# cull slots fully off screen
if x + GAME_WIDTH < 0 or x >= PANEL_WIDTH:
continue
graphics.DrawText(canvas, font_small, x + 18, 11,
graphics.Color(*Colors.RED.value), game["away"])
graphics.DrawText(canvas, font_small, x + 18, 27,
graphics.Color(*Colors.WHITE.value), game["home"])
graphics.DrawText(canvas, font, x + 40, 13,
graphics.Color(*Colors.WHITE.value), str(game["away_score"]))
graphics.DrawText(canvas, font, x + 40, 29,
graphics.Color(*Colors.WHITE.value), str(game["home_score"]))
# status line — only on preferred games (they get a wider single-game view)
if game["id"] in set(_preferred_games):
graphics.DrawText(canvas, font_small, x + 18, 20,
graphics.Color(*Colors.YELLOW.value), game["status"])
# --- Preferred / stale game management ---
def _update_preferred():
preferred_id_set = set(_preferred_games)
# remove finished or gone games
active_ids = {g["id"] for g in _games}
for gid in list(_preferred_games):
game = next((g for g in _games if g["id"] == gid), None)
if game is None or "Final" in game["status"]:
_preferred_games.remove(gid)
# add new matching games
for game in _games:
if (game["away"], game["league"]) in _preferred_teams or \
(game["home"], game["league"]) in _preferred_teams:
if "Final" not in game["status"] and game["id"] not in _preferred_games:
_preferred_games.append(game["id"])
# --- Public draw_frame ---
def draw_frame(canvas): def draw_frame(canvas):
now = time() global _games, _last_fetch, _virtual_canvas, _virtual_dirty, _scroll_x, _tick
if now - last_fetch > 30 or len(games) <= 0: now = time()
games = get_all_scores()
last_fetch = now
if games: # refresh scores every 30s
canvas.Clear() if now - _last_fetch > 30 or not _games:
_games = _get_all_scores()
_last_fetch = now
_update_preferred()
_virtual_dirty = True
# clear finished preferred games if not _games:
if len(preferred_games) > 0: canvas.Clear()
for preferred_game in preferred_games[:]: graphics.DrawText(canvas, font, 10, 22,
shown_game = [g for g in games if preferred_game == g['id']] graphics.Color(*Colors.RED.value), "No games today")
if len(shown_game) <= 0 or "Final" in shown_game[0]['status']: return canvas
preferred_games.remove(preferred_game)
# collect new preferred games # rebuild virtual canvas if data changed
for game in games: if _virtual_dirty or _virtual_canvas is None:
if (game['away'], game['league']) in preferred_teams or (game['home'], game['league']) in preferred_teams: result = _build_virtual_canvas()
if 'Final' not in game['status'] and game['id'] not in preferred_games: if result:
preferred_games.append(game['id']) _virtual_canvas, _total_games = result
_virtual_dirty = False
_scroll_x = 0
print(preferred_games) ordered = _ordered_games()
total_scroll_width = GAME_WIDTH * len(ordered)
if now - last_switch > page_display_time: canvas.Clear()
last_switch = now
if show_preferred and len(preferred_games) > 0: # blit the PIL image slice (logos + dividers + backgrounds)
single_preferred_game = next( if _virtual_canvas:
(g for g in games if g['id'] == preferred_games[current_preferred_game]), None _blit_slice(canvas, _virtual_canvas, _scroll_x)
)
if single_preferred_game:
print(f'Showing preferred game {single_preferred_game["home"]} vs {single_preferred_game["away"]}')
draw_single_game(canvas, single_preferred_game)
current_preferred_game += 1 # draw text on top via rgbmatrix (handles fonts correctly)
if current_preferred_game >= len(preferred_games): _draw_text_overlay(canvas, ordered, _scroll_x)
show_preferred = False
current_preferred_game = 0
current_page = 0
else:
print(f'Showing all games page {current_page} / {len(games)}')
draw_all_games(canvas, games, current_page)
current_page += 4
if current_page >= len(games):
current_page = 0
if len(preferred_games) > 0:
show_preferred = True
else:
canvas.Clear()
print('No games available')
graphics.DrawText(canvas, font, 10, 22, graphics.Color(*Colors.RED.value), "No games today")
# canvas = matrix.SwapOnVSync(canvas) # advance scroll every N ticks
# sleep(10) _tick += 1
return canvas if _tick >= _frames_per_tick:
_tick = 0
_scroll_x = (_scroll_x + _scroll_speed) % total_scroll_width
return canvas
+2 -2
View File
@@ -1,9 +1,9 @@
import os import os
import govee import govee
import pygame import pygame
from time import sleep, time from time import sleep
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics from rgbmatrix import RGBMatrix, RGBMatrixOptions
from dotenv import load_dotenv from dotenv import load_dotenv
from utils.data import LOGO_DIR, ASSET_DIR from utils.data import LOGO_DIR, ASSET_DIR
from utils.colors import Colors from utils.colors import Colors