Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/module-collisions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ name: Plugin Structure
# scroll_display_legacy.py. Three plugins shipped as those two files
# concatenated, carrying ~2,000 lines nothing referenced, and that dead copy
# is what hid the missing separator-icon constants that broke scroll mode.
#
# 3. Pixel-perfect text -- fails when a Draw() that renders text leaves PIL's
# fontmode at the anti-aliasing default. An LED panel has no partial
# brightness, so the grey fringe just smears the glyph.

on:
pull_request:
Expand All @@ -25,6 +29,7 @@ on:
- 'scripts/test_check_scroll_adoption.py'
- 'scripts/check_sports_display_contract.py'
- 'scripts/test_check_sports_display_contract.py'
- 'scripts/test_pixel_perfect_text.py'
# Without this, a PR that only edits this workflow matches no path and
# the workflow never runs against its own change.
- '.github/workflows/module-collisions.yml'
Expand Down Expand Up @@ -61,3 +66,11 @@ jobs:
- name: Test the sports display()-contract gate
if: always()
run: python scripts/test_check_sports_display_contract.py
# PIL anti-aliases TrueType text by default, which on a panel with no
# partial brightness only smears glyphs -- 4x6-font.ttf at 6 puts 74% of
# its lit pixels at partial coverage. Static, because the plugins most at
# risk render only with live data the harness has none of, so a
# render-based check would skip exactly those.
- name: Check text draws are 1-bit (no anti-aliasing)
if: always()
run: python scripts/test_pixel_perfect_text.py
56 changes: 28 additions & 28 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.40.1"
"latest_version": "1.40.2"
},
{
"id": "basketball-scoreboard",
Expand All @@ -101,7 +101,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.29.1"
"latest_version": "1.29.3"
},
{
"id": "calendar",
Expand All @@ -124,7 +124,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.2.4"
"latest_version": "1.2.5"
},
{
"id": "christmas-countdown",
Expand Down Expand Up @@ -240,7 +240,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "3.4.1"
"latest_version": "3.4.3"
},
{
"id": "geochron",
Expand Down Expand Up @@ -335,7 +335,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.25.1",
"latest_version": "1.25.3",
"icon": "fas fa-hockey-puck"
},
{
Expand All @@ -359,7 +359,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.24.1",
"latest_version": "1.24.3",
"icon": "fas fa-baseball-ball"
},
{
Expand All @@ -385,7 +385,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.2.2"
"latest_version": "1.2.3"
},
{
"id": "leaderboard",
Expand All @@ -412,7 +412,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.3.2"
"latest_version": "1.3.3"
},
{
"id": "ledmatrix-flights",
Expand All @@ -439,7 +439,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.12.19"
"latest_version": "1.12.20"
},
{
"id": "march-madness",
Expand All @@ -463,7 +463,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.0.6"
"latest_version": "1.1.1"
},
{
"id": "masters-tournament",
Expand All @@ -487,7 +487,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "2.5.5"
"latest_version": "2.5.6"
},
{
"id": "mqtt-notifications",
Expand All @@ -510,7 +510,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.2.0"
"latest_version": "1.2.1"
},
{
"id": "music",
Expand Down Expand Up @@ -581,7 +581,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.2.6",
"latest_version": "1.2.7",
"icon": "fa-circle-dot"
},
{
Expand All @@ -605,7 +605,7 @@
"last_updated": "2026-08-05",
"verified": true,
"screenshot": "",
"latest_version": "1.4.2",
"latest_version": "1.4.4",
"icon": "fas fa-football-ball"
},
{
Expand Down Expand Up @@ -656,7 +656,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.4.2"
"latest_version": "1.4.3"
},
{
"id": "olympics",
Expand All @@ -681,7 +681,7 @@
"last_updated": "2026-05-15",
"verified": true,
"screenshot": "",
"latest_version": "2.0.1"
"latest_version": "2.0.3"
},
{
"id": "pga-tour-leaderboard",
Expand Down Expand Up @@ -760,7 +760,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "2.24.1"
"latest_version": "2.24.3"
},
{
"id": "static-image",
Expand All @@ -783,7 +783,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.0.6"
"latest_version": "1.0.7"
},
{
"id": "stock-news",
Expand Down Expand Up @@ -856,7 +856,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.1.6"
"latest_version": "1.1.7"
},
{
"id": "tide-display",
Expand Down Expand Up @@ -905,7 +905,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.8.1",
"latest_version": "1.8.2",
"icon": "fas fa-fist-raised"
},
{
Expand All @@ -928,7 +928,7 @@
"repo": "https://github.com/ChuckBuilds/ledmatrix-plugins",
"branch": "main",
"plugin_path": "plugins/ledmatrix-weather",
"latest_version": "2.6.6",
"latest_version": "2.6.7",
"stars": 0,
"downloads": 0,
"last_updated": "2026-09-03",
Expand All @@ -955,7 +955,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.1.0"
"latest_version": "1.1.1"
},
{
"id": "youtube-stats",
Expand All @@ -978,7 +978,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.2.0"
"latest_version": "1.2.1"
},
{
"id": "ledmatrix-elections",
Expand All @@ -1001,7 +1001,7 @@
"last_updated": "2026-07-31",
"verified": false,
"screenshot": "",
"latest_version": "1.2.1"
"latest_version": "1.2.2"
},
{
"id": "ledmatrix-dresden-departures",
Expand Down Expand Up @@ -1048,7 +1048,7 @@
"downloads": 0,
"verified": true,
"screenshot": "",
"latest_version": "1.22.1",
"latest_version": "1.22.3",
"last_updated": "2026-09-02"
},
{
Expand Down Expand Up @@ -1095,7 +1095,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.21.1"
"latest_version": "1.21.3"
},
{
"id": "jellyfin-now-playing",
Expand All @@ -1119,7 +1119,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.2.0"
"latest_version": "1.2.1"
},
{
"id": "incoming-packages",
Expand All @@ -1143,7 +1143,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins/incoming-packages/assets/screenshot.png",
"latest_version": "1.0.1",
"latest_version": "1.0.2",
"icon": "fas fa-box"
},
{
Expand Down
2 changes: 2 additions & 0 deletions plugins/afl-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ def render_game_card(
main_img = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 255))
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
draw_overlay = ImageDraw.Draw(overlay)
draw_overlay.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).

# Calculate score text width first to determine available space for logos
home_score = str(game.get("home_score", "0"))
Expand All @@ -523,6 +524,7 @@ def render_game_card(
if not home_logo or not away_logo:
# Draw placeholder text if logos fail
draw = ImageDraw.Draw(main_img)
draw.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).
self._draw_text_with_outline(
draw,
f"{game.get('away_abbr', '?')}@{game.get('home_abbr', '?')}",
Expand Down
8 changes: 7 additions & 1 deletion plugins/afl-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "afl-scoreboard",
"name": "AFL Scoreboard",
"version": "1.22.1",
"version": "1.22.3",
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming AFL (Australian Football League) games with real-time scores and game status.",
"category": "sports",
Expand All @@ -18,6 +18,12 @@
"afl_upcoming"
],
"versions": [
{
"released": "2026-09-04",
"version": "1.22.3",
"changelog": "Render text 1-bit (no anti-aliasing) so glyphs stay crisp on the LED grid",
"ledmatrix_min_version": "3.3.0"
},
{
"version": "1.22.1",
"released": "2026-09-02",
Expand Down
4 changes: 4 additions & 0 deletions plugins/baseball-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ def _render_live_game(self, game: Dict) -> Image.Image:
main_img = Image.new("RGBA", (self.display_width, self.display_height), (0, 0, 0, 255))
overlay = Image.new("RGBA", (self.display_width, self.display_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
draw.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).

league = game.get('league', 'mlb')
home_logo = self._load_and_resize_logo(league, game.get('home_abbr', ''))
Expand Down Expand Up @@ -591,6 +592,7 @@ def _render_recent_game(self, game: Dict) -> Image.Image:
main_img = Image.new("RGBA", (self.display_width, self.display_height), (0, 0, 0, 255))
overlay = Image.new("RGBA", (self.display_width, self.display_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
draw.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).

league = game.get('league', 'mlb')
home_logo = self._load_and_resize_logo(league, game.get('home_abbr', ''))
Expand Down Expand Up @@ -769,6 +771,7 @@ def _render_upcoming_game(self, game: Dict) -> Image.Image:
main_img = Image.new("RGBA", (self.display_width, self.display_height), (0, 0, 0, 255))
overlay = Image.new("RGBA", (self.display_width, self.display_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
draw.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).

league = game.get('league', 'mlb')
home_logo = self._load_and_resize_logo(league, game.get('home_abbr', ''))
Expand Down Expand Up @@ -1098,5 +1101,6 @@ def _render_error_card(self, message: str) -> Image.Image:
"""Render an error message card."""
img = Image.new('RGB', (self.display_width, self.display_height), (0, 0, 0))
draw = ImageDraw.Draw(img)
draw.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).
self._draw_text_with_outline(draw, message, (5, 5), self.fonts['status'])
return img
8 changes: 7 additions & 1 deletion plugins/baseball-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "baseball-scoreboard",
"name": "Baseball Scoreboard",
"version": "1.40.1",
"version": "1.40.2",
"update_interval": 60,
"author": "ChuckBuilds",
"description": "Live, recent, and upcoming baseball games across MLB, MiLB, and NCAA Baseball with real-time scores and schedules",
Expand Down Expand Up @@ -31,6 +31,12 @@
"branch": "main",
"plugin_path": "plugins/baseball-scoreboard",
"versions": [
{
"released": "2026-09-04",
"version": "1.40.2",
"changelog": "Render text 1-bit (no anti-aliasing) so glyphs stay crisp on the LED grid",
"ledmatrix_min_version": "3.3.0"
},
{
"version": "1.40.1",
"released": "2026-09-02",
Expand Down
2 changes: 2 additions & 0 deletions plugins/basketball-scoreboard/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ def render_game_card(
main_img = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 255))
overlay = Image.new('RGBA', (self.display_width, self.display_height), (0, 0, 0, 0))
draw_overlay = ImageDraw.Draw(overlay)
draw_overlay.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).

# Get league for logo directory
league = game.get('league', 'nba')
Expand Down Expand Up @@ -503,6 +504,7 @@ def render_game_card(
if not home_logo or not away_logo:
# Draw placeholder text if logos fail
draw = ImageDraw.Draw(main_img)
draw.fontmode = "1" # Pixel fonts on an LED panel: 1-bit text so every lit pixel is fully lit (no AA fringe).
self._draw_text_with_outline(
draw,
f"{away_abbr or '?'}@{home_abbr or '?'}",
Expand Down
Loading
Loading