Skip to content
Merged
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
24 changes: 12 additions & 12 deletions plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"last_updated": "2026-09-04",
"last_updated": "2026-09-07",
"plugins": [
{
"id": "cricket-scoreboard",
Expand Down Expand Up @@ -215,7 +215,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.8.7"
"latest_version": "1.8.8"
},
{
"id": "football-scoreboard",
Expand Down Expand Up @@ -412,7 +412,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.3.4"
"latest_version": "1.3.6"
},
{
"id": "ledmatrix-flights",
Expand Down Expand Up @@ -463,7 +463,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.1.1"
"latest_version": "1.1.2"
},
{
"id": "masters-tournament",
Expand Down Expand Up @@ -557,7 +557,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.4.0"
"latest_version": "1.4.1"
},
{
"id": "on-air",
Expand Down Expand Up @@ -605,7 +605,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.4.4",
"latest_version": "1.4.6",
"icon": "fas fa-football-ball"
},
{
Expand Down Expand Up @@ -633,7 +633,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "1.4.0"
"latest_version": "1.4.2"
},
{
"id": "of-the-day",
Expand Down Expand Up @@ -809,7 +809,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "2.6.1"
"latest_version": "2.6.2"
},
{
"id": "stocks",
Expand All @@ -832,7 +832,7 @@
"last_updated": "2026-09-03",
"verified": true,
"screenshot": "",
"latest_version": "2.9.0",
"latest_version": "2.9.1",
"icon": "fas fa-chart-line"
},
{
Expand All @@ -856,7 +856,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.1.7"
"latest_version": "1.1.8"
},
{
"id": "tide-display",
Expand Down Expand Up @@ -905,7 +905,7 @@
"last_updated": "2026-09-02",
"verified": true,
"screenshot": "",
"latest_version": "1.8.3",
"latest_version": "1.8.4",
"icon": "fas fa-fist-raised"
},
{
Expand Down Expand Up @@ -1001,7 +1001,7 @@
"last_updated": "2026-07-31",
"verified": false,
"screenshot": "",
"latest_version": "1.2.3"
"latest_version": "1.2.4"
},
{
"id": "ledmatrix-dresden-departures",
Expand Down
2 changes: 1 addition & 1 deletion plugins/f1-scoreboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "f1-scoreboard",
"name": "F1 Scoreboard",
"version": "1.8.7",
"version": "1.8.8",
"author": "ChuckBuilds",
"class_name": "F1ScoreboardPlugin",
"entry_point": "manager.py",
Expand Down
31 changes: 31 additions & 0 deletions plugins/f1-scoreboard/scroll_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
"ScrollHelper not available, scrolling disabled: %s",
_scroll_import_err)

try:
# Shared scroll pacing: one resolver for every plugin, so identical config
# means the same speed everywhere, and slow speeds get the frame hold that
# keeps them crisp.
from src.common import scroll_config as _scroll_config
except ImportError: # core predates the shared helper
_scroll_config = None

logger = logging.getLogger(__name__)


Expand All @@ -29,6 +37,11 @@ class ScrollDisplay:
and manages scroll state.
"""

def _scroll_frame_hold(self) -> int:
"""Refreshes to hold each frame for, from the resolved scroll settings."""
settings = getattr(self, "_scroll_settings", None)
return getattr(settings, "frame_hold", 1) if settings else 1

def __init__(self, display_manager, config: Optional[Dict[str, Any]] = None,
custom_logger: Optional[logging.Logger] = None,
global_config: Optional[Dict[str, Any]] = None):
Expand Down Expand Up @@ -69,6 +82,19 @@ def __init__(self, display_manager, config: Optional[Dict[str, Any]] = None,
buffer=self.display_width
)

# Shared resolver wins over the setup above, which stays as the
# fallback for cores that predate it.
if _scroll_config is not None:
self._scroll_settings = _scroll_config.configure(
self.scroll_helper,
plugin_config=self.config,
global_config=self.global_config,
display_manager=self.display_manager,
plugin_logger=self.logger,
)
else:
self._scroll_settings = None

# Honor the global smooth-scrolling FPS target (older cores lack the setter)
target_fps = self.global_config.get('target_fps') or self.global_config.get('scroll_target_fps')
try:
Expand Down Expand Up @@ -188,6 +214,11 @@ class ScrollDisplayManager:
Manages multiple ScrollDisplay instances, one per display mode.
"""

def _scroll_frame_hold(self) -> int:
"""Refreshes to hold each frame for, from the resolved scroll settings."""
settings = getattr(self, "_scroll_settings", None)
return getattr(settings, "frame_hold", 1) if settings else 1

def __init__(self, display_manager, config: Optional[Dict[str, Any]] = None,
custom_logger: Optional[logging.Logger] = None,
global_config: Optional[Dict[str, Any]] = None):
Expand Down
46 changes: 45 additions & 1 deletion plugins/ledmatrix-elections/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
from PIL import Image

from src.plugin_system.base_plugin import BasePlugin
try:
# Shared scroll pacing: resolves speed from any supported config shape,
# snaps it to a speed the panel can show in whole pixels, and reports the
# frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md
from src.common import scroll_config as _scroll_config
except ImportError: # core predates the shared helper
_scroll_config = None

from src.common.scroll_helper import ScrollHelper

# Plugin-local imports (the plugin directory is on sys.path at load time, but be
Expand Down Expand Up @@ -92,6 +100,19 @@ def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_man
self.scroll_helper.set_dynamic_duration_settings(
enabled=True, min_duration=int(self.display_duration), max_duration=300
)

# Shared resolver wins over the setup above, which stays as the
# fallback for cores that predate it.
if _scroll_config is not None:
self._scroll_settings = _scroll_config.configure(
self.scroll_helper,
plugin_config=self.config,
global_config=self.config.get('global', {}) or {},
display_manager=self.display_manager,
plugin_logger=self.logger,
)
else:
self._scroll_settings = None
# Honor the global smooth-scrolling FPS target (older cores lack the setter).
# The convention (news, leaderboard) is a `global` section in the plugin config.
self.global_config = config.get('global', {}) or {}
Expand Down Expand Up @@ -174,6 +195,19 @@ def on_config_change(self, new_config: dict) -> None:
self.scroll_helper.set_dynamic_duration_settings(
enabled=True, min_duration=int(self.display_duration), max_duration=300
)

# Shared resolver wins over the setup above, which stays as the
# fallback for cores that predate it.
if _scroll_config is not None:
self._scroll_settings = _scroll_config.configure(
self.scroll_helper,
plugin_config=self.config,
global_config=self.config.get('global', {}) or {},
display_manager=self.display_manager,
plugin_logger=self.logger,
)
else:
self._scroll_settings = None
# Refresh the FPS target from the (possibly edited) global section.
self.global_config = config.get('global', {}) or {}
target_fps = self.global_config.get('target_fps') or self.global_config.get('scroll_target_fps', 100)
Expand All @@ -199,6 +233,15 @@ def on_config_change(self, new_config: dict) -> None:

# -- Update loop --------------------------------------------------------

def _scroll_frame_hold(self) -> int:
"""Refreshes to hold each frame for, from the resolved scroll settings.

1 without the shared helper, which is the old behaviour: a new frame
every panel refresh.
"""
settings = getattr(self, "_scroll_settings", None)
return getattr(settings, "frame_hold", 1) if settings else 1

def update(self) -> None:
now = time.time()
if self._last_update and (now - self._last_update) < self.update_interval:
Expand Down Expand Up @@ -485,7 +528,8 @@ def _display_ticker(self, force_clear: bool) -> bool:
self.logger.debug("reset_scroll failed: %s", e)

try:
self.display_manager.set_scrolling_state(True)
self.display_manager.set_scrolling_state(
True, frame_hold=self._scroll_frame_hold())
except Exception as e:
self.logger.debug("set_scrolling_state failed: %s", e)

Expand Down
8 changes: 7 additions & 1 deletion plugins/ledmatrix-elections/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ledmatrix-elections",
"name": "Election Results",
"version": "1.2.3",
"version": "1.2.4",
"author": "rpierce99",
"description": "Live election results: a scrolling ticker of important races plus a full-screen interrupt when a race is newly called. Auto-activates for your state's primary + general (dormant otherwise), drains stale calls from the ticker, and survives restarts. Filterable to your state. NYT baseline provider + optional California SoS county/city rollup.",
"entry_point": "manager.py",
Expand All @@ -22,6 +22,12 @@
"cache_manager"
],
"versions": [
{
"released": "2026-09-07",
"version": "1.2.4",
"changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels",
"ledmatrix_min_version": "2.0.0"
},
{
"released": "2026-09-03",
"version": "1.2.3",
Expand Down
2 changes: 1 addition & 1 deletion plugins/ledmatrix-elections/test_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ def __init__(self):
def update_display(self):
pass

def set_scrolling_state(self, *_a):
def set_scrolling_state(self, *_a, **_kw):
pass


Expand Down
38 changes: 37 additions & 1 deletion plugins/ledmatrix-leaderboard/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
from PIL import Image

from src.plugin_system.base_plugin import BasePlugin
try:
# Shared scroll pacing: resolves speed from any supported config shape,
# snaps it to a speed the panel can show in whole pixels, and reports the
# frame hold that keeps slow speeds crisp. Core docs: SCROLL_PERFORMANCE.md
from src.common import scroll_config as _scroll_config
except ImportError: # core predates the shared helper
_scroll_config = None

from src.common.scroll_helper import ScrollHelper

from league_config import LeagueConfig
Expand Down Expand Up @@ -140,6 +148,24 @@ def __init__(self, plugin_id: str, config: Dict[str, Any],
self.scroll_helper.target_fps = max(30.0, min(200.0, target_fps))
self.scroll_helper.frame_time_target = 1.0 / self.scroll_helper.target_fps
self.logger.debug(f"Target FPS set to: {self.scroll_helper.target_fps} FPS (using fallback method)")

# The shared resolver takes precedence over the block above, which is
# kept as the fallback for cores that predate it. Running both costs a
# few microseconds once at construction and avoids re-indenting logic
# that other config shapes still depend on.
if _scroll_config is not None:
self._scroll_settings = _scroll_config.configure(
self.scroll_helper,
plugin_config=self.config,
global_config=self.global_config,
display_manager=self.display_manager,
plugin_logger=self.logger,
)
self.logger.info(
"Scroll pacing came from the shared resolver; any scroll speed "
"logged above this line by the legacy path was superseded")
else:
self._scroll_settings = None

self.scroll_helper.set_dynamic_duration_settings(
enabled=self.dynamic_duration_enabled,
Expand Down Expand Up @@ -192,6 +218,15 @@ def __init__(self, plugin_id: str, config: Dict[str, Any],
else:
self.logger.warning("No leagues are enabled - leaderboard will not display any data")

def _scroll_frame_hold(self) -> int:
"""Refreshes to hold each frame for, from the resolved scroll settings.

1 without the shared helper, which is the old behaviour: a new frame
every panel refresh.
"""
settings = getattr(self, "_scroll_settings", None)
return getattr(settings, "frame_hold", 1) if settings else 1

def update(self, force: bool = False) -> None:
"""
Update standings data for all enabled leagues.
Expand Down Expand Up @@ -307,7 +342,8 @@ def display(self, force_clear: bool = False) -> None:
return

# Signal scrolling state
self.display_manager.set_scrolling_state(True)
self.display_manager.set_scrolling_state(
True, frame_hold=self._scroll_frame_hold())
self.display_manager.process_deferred_updates()

# Update scroll position using the scroll helper
Expand Down
8 changes: 7 additions & 1 deletion plugins/ledmatrix-leaderboard/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "ledmatrix-leaderboard",
"name": "Sports Leaderboard",
"version": "1.3.4",
"version": "1.3.6",
"description": "Displays scrolling leaderboards and standings for multiple sports leagues including NFL, NBA, MLB, NCAA Football, NCAA Basketball, and more",
"author": "ChuckBuilds",
"entry_point": "manager.py",
Expand Down Expand Up @@ -31,6 +31,12 @@
"requirements_file": "requirements.txt",
"min_ledmatrix_version": "2.0.0",
"versions": [
{
"released": "2026-09-07",
"version": "1.3.6",
"changelog": "Resolve scroll speed through the shared resolver and apply the frame hold it reports, so slow speeds move whole pixels",
"ledmatrix_min_version": "2.0.0"
},
{
"released": "2026-09-03",
"version": "1.3.4",
Expand Down
Loading
Loading