Files
LEDMatrix/src/plugin_system/vegas_elements.py
T
ChuckandClaude Opus 5.5 701c220ad0 feat(vegas): live elements -- a plugin API for content that changes while it scrolls
Vegas bakes each plugin's pictures into one strip, so a card already on its
way across the panel keeps what it showed when it was drawn. This adds the
API and bookkeeping for content that can be updated in place; the worker
that redraws and swaps it follows separately. No shipped plugin implements
the hook yet, so nothing changes for users.

Plugin API (core 3.8.0), all no-ops by default:
- BasePlugin.get_vegas_elements() -> [VegasElement(key, image, version,
  live, refresh_hz)]: named, fixed-width pieces of Vegas content.
- BasePlugin.redraw_vegas_element(key, width, height, at): a lock-free
  redraw for content that changes with time.
- BasePlugin.notify_vegas_data_changed(): data that lands outside update().
- src/plugin_system/vegas_elements.py (VegasElement, re-exported from
  base_plugin).

Core:
- PluginAdapter asks a plugin that implements the hook for elements on the
  background fetch only (under its lock, on its own canvas); every other
  path keeps get_vegas_content(). Live elements are pinned (padded with
  content_padding, never trimmed), tagged with their key, digest and data
  epoch in Image.info so the existing cache and group plumbing carry them
  unchanged, and untagged if a width budget crops them.
- RenderPipeline records where each live element lands (ElementRecord), in
  absolute strip columns a trim does not move; the block-start arithmetic
  is shared with the STATIC markers.
- PluginManager update listeners (add/remove_update_listener,
  notify_data_changed): told the moment update() completes, not at the
  next ~4s Vegas poll. The coordinator uses one to move each plugin's data
  epoch on.
- vegas_scroll.live_refresh (kill switch), live_max_hz, live_min_interval,
  live_lead_screens; per-plugin core-owned vegas_live. Live elements are
  off under multi-display sync, in swap mode and with offscreen_prefetch off.
- scripts/check_plugin.py checks the element contract
  (src/plugin_system/testing/vegas.py); test/fixtures/plugins/vegas-live-stub
  is a working example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:36:39 -04:00

74 lines
3.5 KiB
Python

"""Live elements: Vegas content that can change while it is on screen.
A plugin's ``get_vegas_content()`` hands the Vegas ticker pictures, and the
ticker bakes them into its strip: a score drawn when the plugin's turn was
prefetched scrolls past with that score, however many goals are scored while
it crosses the panel. A plugin that returns **elements** instead gives each
picture a name and a fixed width. The ticker then keeps track of where each
one is in the strip, and when the plugin's data changes it asks for just the
changed elements and swaps their pixels in place -- on screen included,
between two frames, without anything next to them moving.
A plugin opts in by implementing ``BasePlugin.get_vegas_elements()``, and, for
content that changes with time rather than with data (an aircraft moving
between position reports), ``BasePlugin.redraw_vegas_element()``. See "Live
Vegas elements" in docs/PLUGIN_API_REFERENCE.md.
Added in LEDMatrix 3.8.0. Import it guarded, so the plugin still loads on an
older core (which never calls the hooks)::
try:
from src.plugin_system.vegas_elements import VegasElement
except ImportError: # core older than 3.8.0
VegasElement = None
def get_vegas_elements(self):
if VegasElement is None:
return None
return [VegasElement(key=f"game:{g['id']}", image=self._card(g),
version=self._fingerprint(g))
for g in self._games]
"""
from dataclasses import dataclass
from typing import Hashable, Optional
from PIL import Image
@dataclass(frozen=True, eq=False)
class VegasElement:
"""One named, fixed-width piece of a plugin's Vegas content.
Attributes:
key: Names the element across redraws, unique within one list the
plugin returns: ``"game:nfl:401547417"``, ``"sep:0:nfl"``,
``"map"``. The ticker matches a redraw to the pixels already in
its strip by this key, so it must stay the same for the same
logical thing and must not be reused for a different one.
image: The element as drawn now, at the display's height. For a
``live`` element its width is fixed for as long as the key is on
the strip: a redraw at a different width is never swapped in (it
appears the next time the plugin comes round instead), because
nothing on screen may move. Draw live elements at a width that
does not depend on the data -- a fixed card width, not the
width of the text.
version: Anything hashable that changes exactly when the pixels
would, such as the tuple of fields the element draws. The ticker
skips work for an unchanged version. ``None`` means "compare the
pixels", which is always correct and costs a checksum.
live: False places the element exactly as plain content is placed
(trimmed to its ink, never refreshed): separators, decoration.
refresh_hz: More than 0 asks for ``redraw_vegas_element()`` about
this often while the element is on or near the screen, for
content that changes with time rather than with data. The ticker
caps the rate (``vegas_scroll.live_max_hz``, 1 Hz on a display
without the rebuilt rgbmatrix binding) and slows it for an
element that is slow to draw.
"""
key: str
image: Image.Image
version: Optional[Hashable] = None
live: bool = True
refresh_hz: float = 0.0