mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 17:16:36 +00:00
docs: drop the skin system and src/base_classes from the docs
Deletes docs/SKIN_SYSTEM.md and docs/CREATING_SKINS.md and every link to them (docs/README.md, README.md, PLUGIN_DEVELOPMENT_GUIDE.md, the /skins section of REST_API_REFERENCE.md), the skin section of CLAUDE.md and the term in PRODUCT.md. SPORTS_UNIFICATION.md now says src/base_classes was removed and shared code lives in src/common, in the Layering section and the view-model-contract rule. Other docs stop pointing at the removed package. CHANGELOG records both removals under Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,21 @@ accepts both, but the store flags the old spelling as deprecated
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **The skin system.** Skins never rendered with the current scoreboard
|
||||||
|
plugins, so they are gone rather than "not supported yet": `src/skin_system/`,
|
||||||
|
`skins/`, `scripts/validate_skin.py`, `GET /api/v3/skins`, the store's
|
||||||
|
`"type": "skin"` handling and `docs/SKIN_SYSTEM.md` / `docs/CREATING_SKINS.md`.
|
||||||
|
A `skin` or `skin_options` key left in a plugin's saved config still loads
|
||||||
|
and saves without a validation error; it is ignored, and the next save of
|
||||||
|
that plugin's settings removes it (unless the plugin's own schema declares
|
||||||
|
the key).
|
||||||
|
- **`src/base_classes/`** (`SportsCore`, the sport and mode classes,
|
||||||
|
`CelebrationMixin`, the rotation strategies, `data_sources`,
|
||||||
|
`api_extractors`). No known plugin imports it. A plugin that does must use
|
||||||
|
`src.common` or its own copy of the code.
|
||||||
|
|
||||||
## 3.5.0
|
## 3.5.0
|
||||||
|
|
||||||
New modules a plugin may import via `src.*` (floor on 3.5.0):
|
New modules a plugin may import via `src.*` (floor on 3.5.0):
|
||||||
|
|||||||
@@ -45,17 +45,6 @@
|
|||||||
- Plugin configs stored in `config/config.json`, NOT in plugin directories — safe across reinstalls
|
- Plugin configs stored in `config/config.json`, NOT in plugin directories — safe across reinstalls
|
||||||
- Third-party plugins can use their own repo URL with empty `plugin_path`
|
- Third-party plugins can use their own repo URL with empty `plugin_path`
|
||||||
|
|
||||||
## Skin System (visual overlays for sports scoreboards) — NOT SUPPORTED YET
|
|
||||||
- Skins do not render with the current scoreboard plugins: the only hook is `SportsCore._render_game()` in `src/base_classes/sports/core.py`, and no current scoreboard plugin (monorepo or third-party registry) builds on `src.base_classes`
|
|
||||||
- So core doesn't offer them: no Visual Skin dropdown (`get_plugin_schema` skips `inject_skin_selector`), the store hides/refuses `"type": "skin"` entries, `GET /api/v3/skins` reports `"supported": false`. Switch: `SKINS_RENDER_SUPPORTED` in `src/skin_system/__init__.py`
|
|
||||||
- Stored `skin` / `skin_options` config values must keep loading and saving (base schema allows them; form saves deep-merge over the stored section)
|
|
||||||
- Skins live in `skins/<skin-id>/` (skin.json + skin.py), NOT in plugin dirs — plugin reinstall deletes plugin dirs
|
|
||||||
- Core: `src/skin_system/` (ScoreboardSkin, SkinContext, runtime); keep it and its tests
|
|
||||||
- Skins render onto `ctx.canvas` only; fallback to built-in renderer on `False`/exception (3 strikes disables for session)
|
|
||||||
- View-model guaranteed keys are frozen (see `test/test_skin_system.py::TestViewModelContract`) — renaming keys in `_extract_game_details_common` or sport extractors breaks published skins
|
|
||||||
- Validate skins headlessly: `python scripts/validate_skin.py --skin <id>`; docs: `docs/SKIN_SYSTEM.md`, `docs/CREATING_SKINS.md`
|
|
||||||
- Skins are NOT monorepo plugins: no manifest bump / update_registry.py needed
|
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
- paho-mqtt 2.x needs `callback_api_version=mqtt.CallbackAPIVersion.VERSION1` for v1 compat
|
- paho-mqtt 2.x needs `callback_api_version=mqtt.CallbackAPIVersion.VERSION1` for v1 compat
|
||||||
- BasePlugin uses `get_logger()` from `src.logging_config`, not standard `logging.getLogger()`
|
- BasePlugin uses `get_logger()` from `src.logging_config`, not standard `logging.getLogger()`
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ Four strengths define LEDMatrix, and future work must protect all of them:
|
|||||||
- **Hard constraint: plugin UI compatibility.** Third-party plugins rely on JSON Schema (Draft-7) generated config forms, the widget registry (`static/v3/js/widgets/`), `x-secret` fields, and plugin web-UI actions. UI changes must keep these working.
|
- **Hard constraint: plugin UI compatibility.** Third-party plugins rely on JSON Schema (Draft-7) generated config forms, the widget registry (`static/v3/js/widgets/`), `x-secret` fields, and plugin web-UI actions. UI changes must keep these working.
|
||||||
- **Config storage.** Plugin configuration lives in `config/config.json` and secrets in `config/config_secrets.json`, never in plugin directories, so configs survive reinstalls.
|
- **Config storage.** Plugin configuration lives in `config/config.json` and secrets in `config/config_secrets.json`, never in plugin directories, so configs survive reinstalls.
|
||||||
- **Stack.** An existing Flask + HTMX + Alpine.js app with Jinja templates (`web_interface/templates/v3/`) and static JS/CSS (`web_interface/static/v3/`), with self-hosted vendor assets.
|
- **Stack.** An existing Flask + HTMX + Alpine.js app with Jinja templates (`web_interface/templates/v3/`) and static JS/CSS (`web_interface/static/v3/`), with self-hosted vendor assets.
|
||||||
- **Terminology.** Plugin, Plugin Store, Starlark app, rotation, display duration, Vegas Scroll Mode, skin, on-demand, AP mode.
|
- **Terminology.** Plugin, Plugin Store, Starlark app, rotation, display duration, Vegas Scroll Mode, on-demand, AP mode.
|
||||||
- **Open decisions** (offered during init, not adopted as constraints):
|
- **Open decisions** (offered during init, not adopted as constraints):
|
||||||
- Whether the UI must work fully offline, with no CDN fallbacks at runtime.
|
- Whether the UI must work fully offline, with no CDN fallbacks at runtime.
|
||||||
- Whether a Node/CSS build step is acceptable for contributors.
|
- Whether a Node/CSS build step is acceptable for contributors.
|
||||||
|
|||||||
@@ -461,15 +461,6 @@ See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-pl
|
|||||||
|
|
||||||
For plugin development, check out the [Hello World Plugin](https://github.com/ChuckBuilds/ledmatrix-hello-world) repository as a starter template.
|
For plugin development, check out the [Hello World Plugin](https://github.com/ChuckBuilds/ledmatrix-hello-world) repository as a starter template.
|
||||||
|
|
||||||
### Visual Skins for Scoreboards
|
|
||||||
|
|
||||||
**Not supported yet.** Skins are meant to restyle a sports scoreboard's
|
|
||||||
live/recent/upcoming screens without forking the plugin, but the current
|
|
||||||
scoreboard plugins don't render them: a selected skin has no effect. The web
|
|
||||||
UI doesn't offer skin install or selection for that reason. The skin system
|
|
||||||
and its docs stay in place for when scoreboards adopt it; see
|
|
||||||
[docs/SKIN_SYSTEM.md](docs/SKIN_SYSTEM.md) for why.
|
|
||||||
|
|
||||||
2. **Built-in Managers Deprecated**: The built-in managers (hockey, football, stocks, etc.) are now deprecated and have been moved to the plugin system. **You must install replacement plugins from the Plugin Store** in the web interface instead. The plugin system provides the same functionality with better maintainability and extensibility.
|
2. **Built-in Managers Deprecated**: The built-in managers (hockey, football, stocks, etc.) are now deprecated and have been moved to the plugin system. **You must install replacement plugins from the Plugin Store** in the web interface instead. The plugin system provides the same functionality with better maintainability and extensibility.
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ logical image to multiple chained physical panels.
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `display_durations` | object, `{}` | Per-plugin display duration in seconds, keyed by plugin id (e.g. `"clock": 15`) | `src/display_controller.py:1030` |
|
| `display_durations` | object, `{}` | Per-plugin display duration in seconds, keyed by plugin id (e.g. `"clock": 15`) | `src/display_controller.py:1030` |
|
||||||
| `plugin_rotation_order` | array, `[]` | Explicit rotation order of plugin ids; empty = all enabled plugins in discovery order | `src/display_controller.py:2894` |
|
| `plugin_rotation_order` | array, `[]` | Explicit rotation order of plugin ids; empty = all enabled plugins in discovery order | `src/display_controller.py:2894` |
|
||||||
| `use_short_date_format` | bool, `true` | Compact date rendering in sports scoreboards | `src/base_classes/sports/core.py` |
|
| `use_short_date_format` | bool, `true` | Compact date rendering in sports scoreboards | Nothing since `src/base_classes` was removed; scoreboards read `display.use_short_date_format` from their own plugin config |
|
||||||
| `dynamic_duration.max_duration_seconds` | int, optional | Cap for plugins that request dynamic display time | `src/display_controller.py:405` |
|
| `dynamic_duration.max_duration_seconds` | int, optional | Cap for plugins that request dynamic display time | `src/display_controller.py:405` |
|
||||||
|
|
||||||
## `display.vegas_scroll` — continuous scroll mode
|
## `display.vegas_scroll` — continuous scroll mode
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
# Creating Skins
|
|
||||||
|
|
||||||
> **Not supported yet: skins don't render with the current scoreboard
|
|
||||||
> plugins.** The only render hook is `SportsCore._render_game()` in
|
|
||||||
> `src/base_classes/sports/core.py`, and no current scoreboard (monorepo or
|
|
||||||
> third-party) builds on `src.base_classes`, so a skin you build here passes
|
|
||||||
> `validate_skin.py` but never appears on the matrix. The web UI and Plugin
|
|
||||||
> Store don't offer skins for that reason. Details:
|
|
||||||
> [SKIN_SYSTEM.md](SKIN_SYSTEM.md#status-not-supported-yet). The guide below
|
|
||||||
> stays accurate for the skin API itself.
|
|
||||||
|
|
||||||
A skin restyles a sports scoreboard (live / recent / upcoming) without
|
|
||||||
forking the plugin: the plugin keeps fetching data, scheduling, caching, and
|
|
||||||
doing vegas mode; your skin only draws. Architecture background:
|
|
||||||
[SKIN_SYSTEM.md](SKIN_SYSTEM.md).
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp -r skins/example-classic-baseball skins/my-skin
|
|
||||||
# edit skins/my-skin/skin.json -> set id ("my-skin"), name, author, class_name
|
|
||||||
# edit skins/my-skin/skin.py -> rename the class, start restyling
|
|
||||||
python scripts/validate_skin.py --skin my-skin
|
|
||||||
```
|
|
||||||
|
|
||||||
The validator renders your skin against bundled fixture games at several
|
|
||||||
panel sizes with **no hardware, no network, no running service**, saves PNGs
|
|
||||||
(plus 4x previews) to `skin_renders/`, and fails loudly on errors. Iterate:
|
|
||||||
edit → validate → look at the PNGs.
|
|
||||||
|
|
||||||
To select it, add to your plugin's section in `config/config.json` (this is
|
|
||||||
stored and validated, but has no visible effect until a scoreboard uses the
|
|
||||||
skin hook — see the note at the top):
|
|
||||||
|
|
||||||
```json
|
|
||||||
"baseball-scoreboard": {
|
|
||||||
"skin": "my-skin",
|
|
||||||
"skin_options": { }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The web UI's **Visual Skin** dropdown is hidden while skins are unsupported.
|
|
||||||
`"skin"` also accepts a per-mode mapping:
|
|
||||||
`{"live": "my-skin", "recent": "built-in"}`.
|
|
||||||
|
|
||||||
## The manifest (`skin.json`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "my-skin",
|
|
||||||
"name": "My Skin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "you",
|
|
||||||
"description": "What it looks like",
|
|
||||||
"skin_api_version": "1.0.0",
|
|
||||||
"targets": {
|
|
||||||
"sports": ["baseball"],
|
|
||||||
"sport_keys": ["mlb", "milb"],
|
|
||||||
"plugins": []
|
|
||||||
},
|
|
||||||
"entry_point": "skin.py",
|
|
||||||
"class_name": "MySkin",
|
|
||||||
"modes": ["live", "recent", "upcoming"],
|
|
||||||
"preview": "preview.png"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Field notes: `id` must equal the directory name; `skin_api_version`'s major
|
|
||||||
version must match the host's `SKIN_API_VERSION` or the skin is refused at
|
|
||||||
load; `targets` takes sport families (`sports`), exact sport keys
|
|
||||||
(`sport_keys`), and/or exact plugin ids (`plugins`) — any match applies.
|
|
||||||
|
|
||||||
## The renderer (`skin.py`)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from src.skin_system.skin_base import ScoreboardSkin, SkinContext
|
|
||||||
|
|
||||||
class MySkin(ScoreboardSkin):
|
|
||||||
def render_live(self, ctx: SkinContext, game: dict) -> bool:
|
|
||||||
score = f"{game.get('away_score', '0')}-{game.get('home_score', '0')}"
|
|
||||||
fit = ctx.layout.fit_text(score, ctx.layout.bounds)
|
|
||||||
ctx.draw_fit(fit, ctx.layout.bounds)
|
|
||||||
return True # True = "I drew it"; False = use the built-in layout
|
|
||||||
```
|
|
||||||
|
|
||||||
Implement only the modes you care about — anything else falls back to the
|
|
||||||
plugin's built-in rendering. Return `False` to decline a specific game (e.g.
|
|
||||||
a layout that only makes sense while a game is live).
|
|
||||||
|
|
||||||
### The rules (they keep your skin from breaking the display)
|
|
||||||
|
|
||||||
1. **Draw only onto `ctx.canvas`** (via the helpers or `ctx.draw`). Never
|
|
||||||
reassign `ctx.canvas`, never touch the display or call any update method.
|
|
||||||
2. **No I/O in render paths.** No network, no file loads per frame —
|
|
||||||
`render_live` runs every display pass, and a slow render stalls the whole
|
|
||||||
matrix (the host warns at >150 ms). Use `ctx.load_logo` (cached) and
|
|
||||||
`cache_key=` for images.
|
|
||||||
3. **Derive everything from `(ctx, game)`.** Skins must be stateless: the
|
|
||||||
live/recent/upcoming modes each get their own instance.
|
|
||||||
4. **Always `.get()` optional keys.** Only the guaranteed keys below are
|
|
||||||
promised to exist.
|
|
||||||
5. **Never hardcode pixel positions for the panel.** Use `ctx.width`/
|
|
||||||
`ctx.height`, `ctx.layout` regions and `fit_text` — your skin will be run
|
|
||||||
at sizes you didn't test (64x32, 128x64, vegas cards).
|
|
||||||
6. **No third-party dependencies.** Stdlib + PIL + what `ctx` provides.
|
|
||||||
|
|
||||||
A skin that raises 3 renders in a row is disabled until the service restarts
|
|
||||||
(the built-in layout takes over), so a bug is cosmetic — but check your logs.
|
|
||||||
|
|
||||||
## SkinContext reference
|
|
||||||
|
|
||||||
| Member | What it is |
|
|
||||||
|---|---|
|
|
||||||
| `ctx.canvas` / `ctx.draw` | Fresh RGB `PIL.Image` at display size + its `ImageDraw` (raw-PIL escape hatch) |
|
|
||||||
| `ctx.width`, `ctx.height` | Canvas size — the only size truth |
|
|
||||||
| `ctx.layout` | `LayoutContext` (see [ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md)): `bounds`, `fit_text`, `fit_text_proportional`, `fit_image`, `px`, `by_tier` |
|
|
||||||
| `ctx.draw_fit(fit, box, color, align, valign)` | Draw a `fit_text` result aligned in a `Region` (handles BDF fonts) |
|
|
||||||
| `ctx.draw_text(text, x, y, color, font)` | Positioned text (handles BDF fonts) |
|
|
||||||
| `ctx.draw_image(img, box, mode, align, valign, cache_key)` | Fit + paste an image with alpha; no-ops on `None` |
|
|
||||||
| `ctx.load_logo("home" \| "away")` | Team logo as RGBA, or `None` (always handle `None`). Cached after first use; see note below |
|
|
||||||
| `ctx.draw_text_outlined(text, (x, y), font, fill, outline_color)` | The classic scorebug outlined text (TTF fonts only) |
|
|
||||||
| `ctx.fonts` | The host's font dict — keys `score`, `time`, `team`, `status`, `detail`, `rank` |
|
|
||||||
| `ctx.options` | Your user's `skin_options` from config |
|
|
||||||
| `ctx.sport`, `ctx.view_model_version`, `ctx.logger` | Context metadata + logger |
|
|
||||||
|
|
||||||
**A note on `ctx.load_logo` vs the no-I/O rule:** `load_logo` is the one
|
|
||||||
sanctioned exception. It goes through the host's logo cache — after the
|
|
||||||
first call per team it's a pure in-memory lookup. If a logo file is missing
|
|
||||||
on disk, the *first* call may download it, exactly like the built-in
|
|
||||||
renderer does for the same game (a skin is never worse than built-in here).
|
|
||||||
Always pass a stable `cache_key` when drawing it, never load image files
|
|
||||||
yourself in a render path, and always handle `None`.
|
|
||||||
|
|
||||||
The default layout idiom — carve regions, then fit text into them:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from src.adaptive_layout import scoreboard_regions
|
|
||||||
|
|
||||||
regions = scoreboard_regions(ctx.layout.bounds, ctx=ctx.layout)
|
|
||||||
ctx.draw_image(ctx.load_logo("away"), regions.away_slot, cache_key=f"logo:{game.get('away_abbr')}")
|
|
||||||
ctx.draw_image(ctx.load_logo("home"), regions.home_slot, cache_key=f"logo:{game.get('home_abbr')}")
|
|
||||||
fit = ctx.layout.fit_text("3-5", regions.score_area)
|
|
||||||
ctx.draw_fit(fit, regions.score_area)
|
|
||||||
```
|
|
||||||
|
|
||||||
`Region` supports `split_h`/`split_v`/`inset`/`top_band`/`bottom_band`/
|
|
||||||
`left_col`/`right_col` for custom carves. Raw `ctx.draw.rectangle/polygon/
|
|
||||||
ellipse/...` is always available for custom marks (see the bases diamond in
|
|
||||||
the example skin).
|
|
||||||
|
|
||||||
## The game view model
|
|
||||||
|
|
||||||
Guaranteed for every sport (view model v1.0 — renaming these breaks skins and
|
|
||||||
is treated as a breaking change upstream):
|
|
||||||
|
|
||||||
| Key | Notes |
|
|
||||||
|---|---|
|
|
||||||
| `id` | Event id (string) |
|
|
||||||
| `status_text` | Display-ready status, e.g. `"Final"`, `"7:30 PM"`, `"Bot 7th"` |
|
|
||||||
| `is_live`, `is_final`, `is_upcoming`, `is_halftime` | Booleans |
|
|
||||||
| `game_date`, `game_time` | Pre-formatted local date/time strings |
|
|
||||||
| `start_time_utc` | UTC `datetime` |
|
|
||||||
| `home_abbr`, `away_abbr` | Team abbreviations (can be 2–5 chars — fit, don't assume) |
|
|
||||||
| `home_id`, `away_id` | Team ids |
|
|
||||||
| `home_score`, `away_score` | **Strings**, not ints |
|
|
||||||
| `home_record`, `away_record` | `"58-33"` or `""` (0-0 records are blanked) |
|
|
||||||
| `home_logo_path`, `away_logo_path` | Prefer `ctx.load_logo` over touching these |
|
|
||||||
|
|
||||||
Sport extras (present for that sport, still `.get()` defensively):
|
|
||||||
|
|
||||||
- **baseball**: `inning` (int), `inning_half` (`"top"`/`"bottom"`), `balls`,
|
|
||||||
`strikes`, `outs` (ints), `bases_occupied` (`[first, second, third]`
|
|
||||||
booleans), `series_summary` (str)
|
|
||||||
- **football**: `period`, `period_text`, `clock`, `home_timeouts`,
|
|
||||||
`away_timeouts`, `down_distance_text`, `down_distance_text_long`,
|
|
||||||
`is_redzone`, `possession`, `possession_indicator` (`"home"`/`"away"`),
|
|
||||||
`scoring_event`
|
|
||||||
- **basketball**: `period`, `period_text`, `clock`
|
|
||||||
- **hockey**: `period`, `period_text`, `clock`, `power_play`, `penalties`,
|
|
||||||
`home_shots`, `away_shots`
|
|
||||||
|
|
||||||
Optional everywhere (only when the user enabled the feature): `odds` (dict),
|
|
||||||
`series_summary`, rankings-related fields.
|
|
||||||
|
|
||||||
Fixture copies of these dicts live in `src/skin_system/fixtures/` — that's
|
|
||||||
exactly what the validator feeds your skin.
|
|
||||||
|
|
||||||
## Vegas mode
|
|
||||||
|
|
||||||
You get vegas support for free: vegas captures the normal display output,
|
|
||||||
which is already your skin's rendering. Optionally implement
|
|
||||||
`render_vegas_card(ctx, game)` to return a purpose-built card at
|
|
||||||
`ctx.width x ctx.height` (sizes vary — never assume 128x32).
|
|
||||||
|
|
||||||
## Building a skin with Claude Code
|
|
||||||
|
|
||||||
Skins are ideal Claude Code projects: small, isolated, and verifiable with
|
|
||||||
one command. Paste this to start:
|
|
||||||
|
|
||||||
> You are building a **display skin** for LEDMatrix — a visual overlay for a
|
|
||||||
> sports scoreboard on a small LED matrix (commonly 128x32 or 64x32 pixels).
|
|
||||||
> First read `docs/CREATING_SKINS.md` and the reference skin in
|
|
||||||
> `skins/example-classic-baseball/`.
|
|
||||||
>
|
|
||||||
> Rules:
|
|
||||||
> - Create/modify files ONLY under `skins/<my-skin-id>/`. Do NOT modify
|
|
||||||
> anything in `src/`, `scripts/`, the plugins, or any other skin.
|
|
||||||
> - Render only from the `game` dict and `ctx` helpers. No network calls, no
|
|
||||||
> per-frame file I/O, no new pip dependencies, no touching the display —
|
|
||||||
> draw onto `ctx.canvas` and return True.
|
|
||||||
> - Use `ctx.layout` regions and `fit_text` for positioning so the skin works
|
|
||||||
> at any panel size; use `.get()` for every optional game key.
|
|
||||||
> - After every change run
|
|
||||||
> `python scripts/validate_skin.py --skin <my-skin-id>` and LOOK at the
|
|
||||||
> PNGs it writes to `skin_renders/` (the `_x4.png` files are easiest to
|
|
||||||
> read). Iterate until it passes and looks right at both 128x32 and 64x32.
|
|
||||||
>
|
|
||||||
> What I want it to look like: <describe your layout — where logos, score,
|
|
||||||
> status go; colors; what shows during live vs upcoming vs final>
|
|
||||||
|
|
||||||
Tips that keep Claude (and you) out of trouble:
|
|
||||||
|
|
||||||
- One mode at a time: get `render_live` right before touching the others —
|
|
||||||
unimplemented modes automatically use the built-in look.
|
|
||||||
- Ask for edge-case renders: long team abbreviations, missing logos
|
|
||||||
(`ctx.load_logo` returning `None`), 0-0 records, extra innings/OT.
|
|
||||||
- If the render looks cramped at 64x32, ask Claude to use
|
|
||||||
`ctx.layout.by_tier(...)` to drop elements on small panels rather than
|
|
||||||
shrinking everything.
|
|
||||||
- Never let it "fix" a problem by editing `src/` — if the skin can't do
|
|
||||||
something within its directory, that's a feature request, not a workaround.
|
|
||||||
|
|
||||||
## Pre-publish checklist
|
|
||||||
|
|
||||||
- [ ] `python scripts/validate_skin.py --skin <id> --size 128x32 --size 64x32 --size 128x64` passes
|
|
||||||
- [ ] Looked at every PNG in `skin_renders/` — nothing clipped or overlapping
|
|
||||||
- [ ] Handles a missing logo (`None`) without crashing — temporarily point a
|
|
||||||
fixture's logo path at a nonexistent file to test
|
|
||||||
- [ ] Long abbreviations (`"TA&M"`, 4–5 chars) don't overflow
|
|
||||||
- [ ] No render warning above the time budget
|
|
||||||
- [ ] `skin.json`: `id` matches the directory, `version` set,
|
|
||||||
`skin_api_version` matches the host, targets correct
|
|
||||||
- [ ] `preview.png` added (grab your favorite `_x4` render)
|
|
||||||
- [ ] Tested on real hardware if you have it — a Pi is much slower than your
|
|
||||||
dev machine
|
|
||||||
|
|
||||||
Distribute by publishing the directory as a git repo (users
|
|
||||||
`git clone <repo> skins/<id>`). Registry entries with `"type": "skin"` are
|
|
||||||
hidden and refused by the Plugin Store while skins are unsupported (see
|
|
||||||
[SKIN_SYSTEM.md](SKIN_SYSTEM.md) §Distribution).
|
|
||||||
|
|
||||||
**Trust note:** a skin is Python running inside the display service — the
|
|
||||||
same trust level as a plugin. Review code before installing skins from
|
|
||||||
others.
|
|
||||||
@@ -393,7 +393,5 @@ self.font = self.font_manager.resolve_font(
|
|||||||
## Example: Complete Manager Implementation
|
## Example: Complete Manager Implementation
|
||||||
|
|
||||||
For a working example of the font manager API in use, see
|
For a working example of the font manager API in use, see
|
||||||
`src/font_manager.py` itself and the bundled scoreboard base classes
|
`src/font_manager.py` itself.
|
||||||
in `src/base_classes/` (e.g., `hockey.py`, `football.py`) which
|
|
||||||
register and resolve fonts via the patterns documented above.
|
|
||||||
|
|
||||||
|
|||||||
@@ -429,9 +429,7 @@ self.display_manager.image.paste(icon, (5, 5), icon)
|
|||||||
self.display_manager.update_display()
|
self.display_manager.update_display()
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the same pattern the bundled scoreboard base classes
|
This is the canonical way to render arbitrary images.
|
||||||
(`src/base_classes/baseball.py`, `basketball.py`, `football.py`,
|
|
||||||
`hockey.py`) use, so it's the canonical way to render arbitrary images.
|
|
||||||
|
|
||||||
### Weather Icons
|
### Weather Icons
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
> in `web_interface/app.py`.
|
> in `web_interface/app.py`.
|
||||||
> - The default plugin location is `plugin-repos/` (configurable via
|
> - The default plugin location is `plugin-repos/` (configurable via
|
||||||
> `plugin_system.plugins_directory`), not `./plugins/`.
|
> `plugin_system.plugins_directory`), not `./plugins/`.
|
||||||
> - Example imports use `src/plugin_system/base_classes/*_plugin.py`;
|
> - Example imports use `src/plugin_system/base_classes/*_plugin.py`,
|
||||||
> the shipped base classes live in `src/base_classes/` (e.g.
|
> which do not exist. The old `src/base_classes/` package has been
|
||||||
> `src.base_classes.sports.SportsCore`, `src.base_classes.hockey.Hockey`).
|
> removed; shared sports code lives in `src/common/`.
|
||||||
> - The "Migration Strategy" and "Implementation Roadmap" sections
|
> - The "Migration Strategy" and "Implementation Roadmap" sections
|
||||||
> describe work that has now shipped.
|
> describe work that has now shipped.
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,12 +12,6 @@ This guide explains how to set up a development workflow for plugins that are ma
|
|||||||
> scale. Existing plugins keep their classic rendering unless they adopt
|
> scale. Existing plugins keep their classic rendering unless they adopt
|
||||||
> those APIs; nothing migrates automatically.
|
> those APIs; nothing migrates automatically.
|
||||||
|
|
||||||
> **Want a different look for an existing sports scoreboard?** Skins are
|
|
||||||
> meant for that, but they are **not supported yet**: the current scoreboard
|
|
||||||
> plugins don't render them (see [SKIN_SYSTEM.md](SKIN_SYSTEM.md#status-not-supported-yet)).
|
|
||||||
> For now, change the look through the plugin's own display settings or its
|
|
||||||
> code.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
When developing plugins in separate repositories, you need a way to:
|
When developing plugins in separate repositories, you need a way to:
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ Going deeper:
|
|||||||
- [ADVANCED_FEATURES.md](ADVANCED_FEATURES.md) — Vegas scroll, on-demand display,
|
- [ADVANCED_FEATURES.md](ADVANCED_FEATURES.md) — Vegas scroll, on-demand display,
|
||||||
cache management, background services, permissions
|
cache management, background services, permissions
|
||||||
- [FONT_MANAGER.md](FONT_MANAGER.md) — font system
|
- [FONT_MANAGER.md](FONT_MANAGER.md) — font system
|
||||||
- [SKIN_SYSTEM.md](SKIN_SYSTEM.md) — skin architecture for sports scoreboards (not supported yet: current scoreboards don't render skins)
|
|
||||||
- [CREATING_SKINS.md](CREATING_SKINS.md) — writing and validating a skin (same caveat)
|
|
||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ the entry below says so.
|
|||||||
- [Integrations](#integrations)
|
- [Integrations](#integrations)
|
||||||
- [Plugin-specific endpoints](#plugin-specific-endpoints)
|
- [Plugin-specific endpoints](#plugin-specific-endpoints)
|
||||||
- [Starlark Apps](#starlark-apps)
|
- [Starlark Apps](#starlark-apps)
|
||||||
- [Skins](#skins)
|
|
||||||
|
|
||||||
> The API blueprint is the `api_v3` package in
|
> The API blueprint is the `api_v3` package in
|
||||||
> `web_interface/blueprints/api_v3/` (one module per area: `config.py`,
|
> `web_interface/blueprints/api_v3/` (one module per area: `config.py`,
|
||||||
@@ -2057,17 +2056,6 @@ runs.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Skins
|
|
||||||
|
|
||||||
**GET** `/api/v3/skins`
|
|
||||||
|
|
||||||
Installed scoreboard skins (optional `?plugin_id=` filter). Skins are not
|
|
||||||
supported by the current scoreboard plugins, so the response carries
|
|
||||||
`data.supported: false` and a `data.message`; clients must not offer these
|
|
||||||
as selectable. See [SKIN_SYSTEM.md](SKIN_SYSTEM.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Responses
|
## Error Responses
|
||||||
|
|
||||||
Errors use one of two shapes. Most endpoints answer:
|
Errors use one of two shapes. Most endpoints answer:
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
# Skin System Architecture
|
|
||||||
|
|
||||||
## Status: not supported yet
|
|
||||||
|
|
||||||
**Skins don't render with the current scoreboard plugins.** The skin system
|
|
||||||
below works in isolation (it loads, validates and renders skins in
|
|
||||||
`scripts/validate_skin.py` and `test/test_skin_system.py`), but nothing on a
|
|
||||||
running display calls it:
|
|
||||||
|
|
||||||
- The only render hook is `SportsCore._render_game()` in
|
|
||||||
`src/base_classes/sports/core.py`.
|
|
||||||
- None of the current scoreboard plugins build on `src.base_classes`. The
|
|
||||||
official scoreboards in the `ledmatrix-plugins` monorepo, and the
|
|
||||||
third-party scoreboards in the plugin registry, carry their own sports and
|
|
||||||
rendering code (with the shared `src/common/sports_*` helpers) and never
|
|
||||||
reach `SportsCore._render_game()`.
|
|
||||||
|
|
||||||
So a skin can be dropped into `skins/` and named in a plugin's config, but the
|
|
||||||
scoreboard keeps drawing its built-in layout. Until a scoreboard adopts the
|
|
||||||
hook, core does not offer skins to users:
|
|
||||||
|
|
||||||
- The plugin config page shows no **Visual Skin** dropdown.
|
|
||||||
- The Plugin Store hides registry entries with `"type": "skin"` and refuses
|
|
||||||
to install one (`POST /api/v3/plugins/install` answers 400 with the reason).
|
|
||||||
- `GET /api/v3/skins` still lists what is in `skins/`, with
|
|
||||||
`"supported": false` and a `message`.
|
|
||||||
- A config that already contains `"skin"` / `"skin_options"` still loads,
|
|
||||||
validates and saves unchanged; the value is simply unused.
|
|
||||||
|
|
||||||
The rest of this document describes the design as built, for whoever wires a
|
|
||||||
scoreboard to it.
|
|
||||||
|
|
||||||
Skins are user-installable **visual overlays** for the sports scoreboards.
|
|
||||||
A skin replaces only the *look* of a scoreboard — the host plugin keeps doing
|
|
||||||
data fetching, scheduling, caching, dedup, live-priority takeover, and vegas
|
|
||||||
mode. If you only want to **build** a skin, read
|
|
||||||
[CREATING_SKINS.md](CREATING_SKINS.md); this document explains how the system
|
|
||||||
works and why it is shaped this way.
|
|
||||||
|
|
||||||
## Why skins instead of forks
|
|
||||||
|
|
||||||
Before skins, changing a scoreboard's layout meant forking the whole plugin
|
|
||||||
(e.g. the community MLB scoreboard fork). The fork gets the new look but loses
|
|
||||||
everything the maintained plugin keeps earning: duration/scheduling behavior,
|
|
||||||
vegas mode support, caching and background-fetch improvements, bug fixes. It
|
|
||||||
also silently drifts: every upstream improvement now has to be re-ported by
|
|
||||||
hand.
|
|
||||||
|
|
||||||
A skin inverts that trade. The plugin remains stock and keeps updating through
|
|
||||||
the store; the skin is ~100 lines of pure rendering code that receives the
|
|
||||||
plugin's already-fetched data each frame. Uninstalling the skin (or the skin
|
|
||||||
crashing) simply restores the built-in look.
|
|
||||||
|
|
||||||
```text
|
|
||||||
(unchanged) (the skin seam)
|
|
||||||
ESPN API ──► update() ──► game view model ──► _render_game() ──► display
|
|
||||||
fetching (a dict) │ │
|
|
||||||
caching │ └─ built-in
|
|
||||||
scheduling └─ skin.render_<mode>(ctx, game)
|
|
||||||
live priority draws onto ctx.canvas
|
|
||||||
```
|
|
||||||
|
|
||||||
## The render funnel
|
|
||||||
|
|
||||||
A sports scoreboard built on the `src/base_classes/sports/` package
|
|
||||||
(`core.py`) renders through exactly one seam. No current scoreboard plugin is
|
|
||||||
built on it (see [Status](#status-not-supported-yet)), so for them this seam is
|
|
||||||
never reached:
|
|
||||||
`SportsCore._render_game(game, force_clear)`.
|
|
||||||
|
|
||||||
1. The mode class's `display()` (live, `SportsUpcoming`, `SportsRecent`)
|
|
||||||
picks `self.current_game` and calls `_render_game`.
|
|
||||||
2. `_render_game` lazily loads the configured skin (once, on first render —
|
|
||||||
a broken skin can never block plugin startup).
|
|
||||||
3. If a skin is active, the host builds a `SkinContext` — a fresh black
|
|
||||||
canvas at the current display size plus layout/font/logo helpers — and
|
|
||||||
calls the skin's `render_live` / `render_recent` / `render_upcoming`
|
|
||||||
with a **copy** of the game dict.
|
|
||||||
4. If the skin returns `True`, the canvas is composited onto the display.
|
|
||||||
If it returns `False`, isn't implemented for that mode, or raises, the
|
|
||||||
built-in `_draw_scorebug_layout` runs instead.
|
|
||||||
|
|
||||||
Key properties that fall out of this design:
|
|
||||||
|
|
||||||
- **Per-mode fallback.** A skin that only implements `render_live` gets the
|
|
||||||
stock recent/upcoming screens for free.
|
|
||||||
- **Three strikes.** A skin that raises 3 times in a row is disabled for the
|
|
||||||
rest of the session (one loud error log per failure); the display never
|
|
||||||
goes dark. Restarting the service re-arms it.
|
|
||||||
- **Copies, not references.** Skins receive a shallow copy of the game dict,
|
|
||||||
so a buggy skin cannot corrupt the plugin's scheduling state.
|
|
||||||
- **Vegas mode works untouched.** Vegas capture falls back to grabbing the
|
|
||||||
regular `display()` output, which is already skin-rendered. Skins can
|
|
||||||
additionally implement `render_vegas_card` for purpose-built scroll cards,
|
|
||||||
and hosts can call `SportsCore.render_skin_card(game, size)` to use it.
|
|
||||||
- **Hot-loop caution.** `render_live` runs every display-loop pass during a
|
|
||||||
live game. The host logs a warning when a skin render exceeds 150 ms, and
|
|
||||||
`scripts/validate_skin.py` enforces a budget at development time — but
|
|
||||||
Python cannot forcibly time-out a stuck render, so a skin that blocks
|
|
||||||
(network I/O, giant image ops) stalls the display. This is why the rules
|
|
||||||
in CREATING_SKINS.md ban I/O in render paths.
|
|
||||||
|
|
||||||
## The view model contract
|
|
||||||
|
|
||||||
The `game` dict a skin receives is the plugin's already-extracted view model
|
|
||||||
(`SportsCore._extract_game_details_common` plus per-sport extras from
|
|
||||||
`src/base_classes/{baseball,basketball,football,hockey}.py`).
|
|
||||||
|
|
||||||
- **Guaranteed keys (view model v1.0)** — always present for every sport:
|
|
||||||
`id`, `game_time`, `game_date`, `start_time_utc` (a UTC `datetime`),
|
|
||||||
`status_text`, `is_live`, `is_final`, `is_upcoming`, `is_halftime`,
|
|
||||||
`home_abbr`/`away_abbr`, `home_id`/`away_id`, `home_score`/`away_score`
|
|
||||||
(**strings**), `home_logo_path`/`away_logo_path`, `home_record`/`away_record`.
|
|
||||||
- **Sport extras** — documented per sport in CREATING_SKINS.md (e.g. baseball
|
|
||||||
adds `inning`, `inning_half`, `balls`, `strikes`, `outs`, `bases_occupied`).
|
|
||||||
- **Optional keys** (`odds`, rankings, `series_summary`, …) are present only
|
|
||||||
when the feature is enabled — skins must always use `.get()`.
|
|
||||||
|
|
||||||
Versioning policy: additive changes bump the minor version
|
|
||||||
(`VIEW_MODEL_VERSION` in `src/skin_system/skin_base.py`, surfaced to skins as
|
|
||||||
`ctx.view_model_version`); renaming or removing a guaranteed key requires a
|
|
||||||
major bump plus a compat shim. `test/test_skin_system.py::TestViewModelContract`
|
|
||||||
fails CI if a guaranteed key disappears from the extractor.
|
|
||||||
|
|
||||||
Separately, `SKIN_API_VERSION` versions the Python API (`ScoreboardSkin`,
|
|
||||||
`SkinContext`). The loader refuses a skin whose manifest declares a different
|
|
||||||
major version and falls back to the built-in renderer with a clear
|
|
||||||
"skin needs an update" log line.
|
|
||||||
|
|
||||||
## Package layout and lifecycle
|
|
||||||
|
|
||||||
```text
|
|
||||||
skins/<skin-id>/
|
|
||||||
skin.json # manifest (required)
|
|
||||||
skin.py # ScoreboardSkin subclass (required)
|
|
||||||
preview.png # optional, shown by the web UI
|
|
||||||
assets/ # optional skin-local images
|
|
||||||
helpers.py ... # optional extra modules (namespaced per skin at import)
|
|
||||||
```
|
|
||||||
|
|
||||||
Skins live in the central `skins/` directory — deliberately **not** inside the
|
|
||||||
plugin's directory, because plugin reinstall/update deletes the whole plugin
|
|
||||||
directory and a skin must survive that. One skin can also target several
|
|
||||||
plugins (mlb + milb).
|
|
||||||
|
|
||||||
Lifecycle: discovered lazily on first render → manifest validated → API major
|
|
||||||
version gated → module imported under a namespaced `sys.modules` key (two
|
|
||||||
skins can both ship a `helpers.py`, same scheme plugins use) → instantiated
|
|
||||||
with `(manifest, options)`. Every failure logs and falls back to built-in.
|
|
||||||
|
|
||||||
Skins should be **stateless**: the live, recent, and upcoming mode classes
|
|
||||||
each hold their own skin instance, so derive everything from `(ctx, game)`.
|
|
||||||
|
|
||||||
## Selection and configuration
|
|
||||||
|
|
||||||
Inside the plugin's own config section in `config/config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"baseball-scoreboard": {
|
|
||||||
"skin": "retro-baseball",
|
|
||||||
"skin_options": { "accent_color": [255, 80, 0] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`"skin"` is either one id for all modes or a per-mode mapping
|
|
||||||
(`{"live": "retro-baseball", "recent": "built-in"}`). Absent, empty, or
|
|
||||||
`"built-in"` means the stock renderer. Because this rides the plugin's config
|
|
||||||
section, it persists across plugin reinstalls like every other setting.
|
|
||||||
|
|
||||||
`SchemaManager.inject_skin_selector` can add a **Visual Skin** enum to the
|
|
||||||
*served* schema for plugins with matching skins installed. While skins are
|
|
||||||
unsupported the plugin schema endpoint does not call it, so the dropdown is
|
|
||||||
not shown. Validation never sees the enum either way: the base schema allows
|
|
||||||
any `skin` value, so a config that references an uninstalled skin stays valid.
|
|
||||||
`GET /api/v3/skins` lists installed skins (optionally filtered by
|
|
||||||
`?plugin_id=`) and reports `"supported": false`.
|
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
- **Manual:** `git clone <skin repo> skins/<skin-id>` — that's the whole
|
|
||||||
install. No manifest bumps, no `update_registry.py`; skins are not monorepo
|
|
||||||
plugins.
|
|
||||||
- **Store (disabled while unsupported):** registry entries with
|
|
||||||
`"type": "skin"` are hidden from the store list and refused on install.
|
|
||||||
`PluginStoreManager._install_skin_from_info` is kept: once
|
|
||||||
`SKINS_RENDER_SUPPORTED` in `src/skin_system/__init__.py` is true, such
|
|
||||||
entries install through the same `plugins.json` pipeline, land in `skins/`,
|
|
||||||
are validated against `skin.json` (including the API major version) instead
|
|
||||||
of `manifest.json`, and never install dependencies — skins are render-only
|
|
||||||
(stdlib + PIL + the provided context, no third-party packages in v1).
|
|
||||||
|
|
||||||
## Trust model
|
|
||||||
|
|
||||||
A skin is Python executing inside the display service — **exactly the same
|
|
||||||
trust level as a plugin**, even though "skin" sounds cosmetic. Only install
|
|
||||||
skins from sources you'd be willing to install a plugin from.
|
|
||||||
|
|
||||||
## v2 directions (not in v1)
|
|
||||||
|
|
||||||
- A generic `BasePlugin` opt-in (`render_with_skin()`) so non-sports plugins
|
|
||||||
(weather, music) can offer skinnable layouts; `skin_runtime` is already
|
|
||||||
sports-agnostic in anticipation.
|
|
||||||
- Store UI: preview gallery, one-click install from the skin browser.
|
|
||||||
- An update path for git-cloned skins (today: re-clone or store reinstall).
|
|
||||||
- Animation support in skins (today the API is one frame per render call;
|
|
||||||
stateful tricks work but are at-your-own-risk).
|
|
||||||
+21
-27
@@ -7,9 +7,9 @@ becoming nine clients of a god class.
|
|||||||
|
|
||||||
Nine plugins (`afl`, `baseball`, `basketball`, `football`, `hockey`, `lacrosse`,
|
Nine plugins (`afl`, `baseball`, `basketball`, `football`, `hockey`, `lacrosse`,
|
||||||
`nrl`, `soccer`, `ufc`) each ship a ~3,000-line `sports.py` descended from this
|
`nrl`, `soccer`, `ufc`) each ship a ~3,000-line `sports.py` descended from this
|
||||||
repo's `src/base_classes/sports.py`. They have drifted into three lineages, and
|
repo's former `src/base_classes/sports.py` (since removed). They have drifted
|
||||||
only 28 of the 66 methods appearing across them are present in all nine. One
|
into three lineages, and only 28 of the 66 methods appearing across them are
|
||||||
logical fix (the UTC start-time bug) cost 75 files.
|
present in all nine. One logical fix (the UTC start-time bug) cost 75 files.
|
||||||
|
|
||||||
Merging everything into one base class would fix the duplication and create a
|
Merging everything into one base class would fix the duplication and create a
|
||||||
worse problem: a single 2,500-line class that all nine plugins inherit, where any
|
worse problem: a single 2,500-line class that all nine plugins inherit, where any
|
||||||
@@ -26,7 +26,7 @@ These are independent concerns. Conflating them is what produces god classes.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Plugin loads on a core that predates a module | Guarded import with a bundled fallback (`try: from src.X import Y / except ModuleNotFoundError: from y import Y`) |
|
| Plugin loads on a core that predates a module | Guarded import with a bundled fallback (`try: from src.X import Y / except ModuleNotFoundError: from y import Y`) |
|
||||||
| Plugin loads on a core that predates a *method* | Capability probing — `hasattr(SportsCore, "_detect_stale_games")` — never a version comparison. The loader's compat check is advisory-only (it logs and continues), so probing is the real protection. |
|
| Plugin loads on a core that predates a *method* | Capability probing — `hasattr(SportsCore, "_detect_stale_games")` — never a version comparison. The loader's compat check is advisory-only (it logs and continues), so probing is the real protection. |
|
||||||
| Core changes never break a plugin's rendering | The **view-model contract**: `_extract_game_details_common` returns a dict whose `GUARANTEED_KEYS` are frozen by `test/test_skin_system.py::TestViewModelContract`. Keys may be added, never renamed or removed. |
|
| Core changes never break a plugin's rendering | The **view-model contract**: the game dict each plugin's `_extract_game_details_common` builds is read by the shared `src/common` renderers, so its keys may be added, never renamed or removed. |
|
||||||
| A plugin can drop its bundled copy safely | The **sunset rule**: its manifest must floor `ledmatrix_min_version` at the first core release shipping the module (recorded in `CHANGELOG.md`) — *necessary but not sufficient*. The store enforces that floor on every registry-managed install and on both supported update paths (sideloading via `install_from_url` is not gated), but a floor cannot reach a user who never updates, so the copy also waits for the B6 gate below. |
|
| A plugin can drop its bundled copy safely | The **sunset rule**: its manifest must floor `ledmatrix_min_version` at the first core release shipping the module (recorded in `CHANGELOG.md`) — *necessary but not sufficient*. The store enforces that floor on every registry-managed install and on both supported update paths (sideloading via `install_from_url` is not gated), but a floor cannot reach a user who never updates, so the copy also waits for the B6 gate below. |
|
||||||
|
|
||||||
The core API is **additive-only**. A method the plugins call is never removed or
|
The core API is **additive-only**. A method the plugins call is never removed or
|
||||||
@@ -67,16 +67,13 @@ This is the property the naive merge destroys, and it is enforced structurally:
|
|||||||
|
|
||||||
## Layering
|
## Layering
|
||||||
|
|
||||||
```
|
`src/base_classes/` has been removed: no scoreboard plugin built on it. B1 and
|
||||||
src/base_classes/sports/
|
B2 below promoted code into it (`SportsCore`, the mode classes,
|
||||||
__init__.py re-exports the public API (import path unchanged)
|
`CelebrationMixin`, the rotation strategies); the override points and
|
||||||
core.py SportsCore — fetch, cache, config, logos, fonts, odds,
|
capabilities sections record that design, but none of it ships in core any
|
||||||
view-model extraction, the skin seam
|
more. Shared sports code lives in `src/common`:
|
||||||
modes.py SportsUpcoming / SportsRecent / SportsLive
|
|
||||||
capabilities/
|
|
||||||
celebrations.py CelebrationMixin (opt-in: 4 of 9 plugins)
|
|
||||||
rotation.py RotationStrategy + registry
|
|
||||||
|
|
||||||
|
```
|
||||||
src/common/
|
src/common/
|
||||||
sports_scroll.py SportsScrollDisplay / …Manager — scroll orchestration
|
sports_scroll.py SportsScrollDisplay / …Manager — scroll orchestration
|
||||||
(content building stays in the plugins)
|
(content building stays in the plugins)
|
||||||
@@ -85,13 +82,10 @@ src/common/
|
|||||||
plugins' sports.py, and the _favorite_key seam
|
plugins' sports.py, and the _favorite_key seam
|
||||||
```
|
```
|
||||||
|
|
||||||
`from src.base_classes.sports import SportsCore` keeps working — the package
|
|
||||||
`__init__` re-exports, so the conversion is invisible to every existing importer.
|
|
||||||
|
|
||||||
### Converging on `src/common`
|
### Converging on `src/common`
|
||||||
|
|
||||||
The scoreboards do not build on `src/base_classes`; their own `sports.py` copies
|
The scoreboards never built on `src/base_classes` (now removed); their own
|
||||||
have moved past it. So shared code now lands in hardware-free `src/common`
|
`sports.py` copies had moved past it. So shared code now lands in hardware-free `src/common`
|
||||||
modules taken from the plugin copies, each a **new module** rather than growth
|
modules taken from the plugin copies, each a **new module** rather than growth
|
||||||
on an existing one: a plugin that deletes a method copy and relies on an older
|
on an existing one: a plugin that deletes a method copy and relies on an older
|
||||||
module having gained it fails at runtime with an `AttributeError`, while a
|
module having gained it fails at runtime with an `AttributeError`, while a
|
||||||
@@ -100,7 +94,7 @@ missing module fails at load, where the version checks can see it.
|
|||||||
listed below, for later phases); its parity test compares every body against
|
listed below, for later phases); its parity test compares every body against
|
||||||
the plugin copies when `LEDMATRIX_PLUGINS` points at a checkout, and
|
the plugin copies when `LEDMATRIX_PLUGINS` points at a checkout, and
|
||||||
`test/test_common_is_hardware_free.py` keeps `src/common` free of
|
`test/test_common_is_hardware_free.py` keeps `src/common` free of
|
||||||
`rgbmatrix`, `src.base_classes` and `src.plugin_system`. How a plugin adopts a
|
`rgbmatrix`, `src.display_manager` and `src.plugin_system`. How a plugin adopts a
|
||||||
module and drops its copy is documented in the plugins repo's
|
module and drops its copy is documented in the plugins repo's
|
||||||
`docs/plugin-development/08-shared-sports-code.md`.
|
`docs/plugin-development/08-shared-sports-code.md`.
|
||||||
|
|
||||||
@@ -116,7 +110,6 @@ deprecation cycle.
|
|||||||
| `_extract_game_details(event)` | Sport-specific view-model fields on top of the common ones | delegates to `_extract_game_details_common` |
|
| `_extract_game_details(event)` | Sport-specific view-model fields on top of the common ones | delegates to `_extract_game_details_common` |
|
||||||
| `_draw_scorebug_layout(game, force_clear)` | Sport's card rendering | base layout |
|
| `_draw_scorebug_layout(game, force_clear)` | Sport's card rendering | base layout |
|
||||||
| `_custom_scorebug_layout(game, draw)` | Per-sport overlay on the base layout | no-op |
|
| `_custom_scorebug_layout(game, draw)` | Per-sport overlay on the base layout | no-op |
|
||||||
| `render_skin_card(game, size)` | Skin-system entry point | built-in fallback |
|
|
||||||
| `score_phrase(points, team_abbr)` | Celebration wording (`"GOOOOAAALLL!"` vs `"TOUCHDOWN!"`). `points` is the score delta, which sports with variable-value scores use to name the play | `"<abbr> SCORES!"` — only consulted when `CelebrationMixin` is present |
|
| `score_phrase(points, team_abbr)` | Celebration wording (`"GOOOOAAALLL!"` vs `"TOUCHDOWN!"`). `points` is the score delta, which sports with variable-value scores use to name the play | `"<abbr> SCORES!"` — only consulted when `CelebrationMixin` is present |
|
||||||
| `win_phrase(team_abbr)` | Win-celebration wording | `"<abbr> WINS!"` — mixin only |
|
| `win_phrase(team_abbr)` | Win-celebration wording | `"<abbr> WINS!"` — mixin only |
|
||||||
| `_favorite_key(game, side)` | Which view-model field identifies a team for favorites matching | `game["<side>_abbr"]` |
|
| `_favorite_key(game, side)` | Which view-model field identifies a team for favorites matching | `game["<side>_abbr"]` |
|
||||||
@@ -189,9 +182,9 @@ and a typo should cost the boost, not the scoreboard. When a plugin needs an
|
|||||||
ordering that core does not ship, it calls `register_rotation_strategy` to add
|
ordering that core does not ship, it calls `register_rotation_strategy` to add
|
||||||
its own — rather than core growing a branch for it.
|
its own — rather than core growing a branch for it.
|
||||||
|
|
||||||
`test_sports_capabilities.py` checks each strategy against a **verbatim
|
`test_sports_capabilities.py` (removed with `src/base_classes`) checked each
|
||||||
transcription** of the plugin code it replaces, over every live-game shape up to
|
strategy against a **verbatim transcription** of the plugin code it replaces,
|
||||||
four games. That differential is what B5 deletes the bundled copies on the
|
over every live-game shape up to four games. That differential is what B5 deletes the bundled copies on the
|
||||||
strength of.
|
strength of.
|
||||||
|
|
||||||
## Scroll display — where the promotion line falls
|
## Scroll display — where the promotion line falls
|
||||||
@@ -507,9 +500,9 @@ What actually remains, smallest first:
|
|||||||
3. **Reconsider the held modules** (`data_sources.py`, `game_renderer.py`,
|
3. **Reconsider the held modules** (`data_sources.py`, `game_renderer.py`,
|
||||||
`base_odds_manager.py`) now that the sunset has closed. `game_renderer.py` is
|
`base_odds_manager.py`) now that the sunset has closed. `game_renderer.py` is
|
||||||
the largest single duplication left: ~11,500 lines across eight plugins, with
|
the largest single duplication left: ~11,500 lines across eight plugins, with
|
||||||
~36,500 more in the eight `sports.py`. Note that core already ships
|
~36,500 more in the eight `sports.py`. The `src/base_classes/sports/`
|
||||||
`src/base_classes/sports/` (~143KB, promoted in B1/B2) that **no plugin
|
package promoted in B1/B2 was never imported by a plugin and has been
|
||||||
imports** — check whether it has drifted before treating it as the target.
|
removed, so the plugin copies are the only starting point.
|
||||||
|
|
||||||
## How to keep this project healthy
|
## How to keep this project healthy
|
||||||
|
|
||||||
@@ -542,6 +535,7 @@ Lessons this migration paid for, worth applying beyond it:
|
|||||||
- **A capability that is not opted into must not execute.** If you find yourself
|
- **A capability that is not opted into must not execute.** If you find yourself
|
||||||
writing `if self.<capability>_enabled` inside a base class, it belongs in a
|
writing `if self.<capability>_enabled` inside a base class, it belongs in a
|
||||||
mixin.
|
mixin.
|
||||||
- **Touch the view-model keys only additively.** Published skins depend on them.
|
- **Touch the view-model keys only additively.** The shared `src/common`
|
||||||
|
renderers read them.
|
||||||
- **Every promotion lands with the characterization suite green**, and every
|
- **Every promotion lands with the characterization suite green**, and every
|
||||||
pilot adoption lands with that plugin's harness and golden suites green.
|
pilot adoption lands with that plugin's harness and golden suites green.
|
||||||
|
|||||||
Reference in New Issue
Block a user