Files
LEDMatrix/CLAUDE.md
T
ChuckandClaude Opus 5 f9b3d6ae52 fix(web): accept every panel size and row address type the rgbmatrix library does (#586)
* fix(web): accept every panel size and row address type the rgbmatrix library does

The Display form capped columns at 128 and chain length at 24, and its
submit handler (fixInvalidNumberInputs) rewrote anything larger to the cap,
so wide panels and long chains silently saved as the wrong size. The config
API checked none of the hardware numbers, so values the library rejects (odd
rows, parallel 4, PWM dither bits 3) saved and the matrix then refused to
start.

- Form limits now match the pinned library: rows even 8-64, cols >= 16 and
  chain_length >= 1 with no upper bound, parallel 1-3, PWM dither bits 0-2,
  PWM LSB nanoseconds 50-3000.
- save_main_config rejects out-of-range rows, cols, chain_length, parallel,
  brightness, scan_mode, pwm_bits, pwm_dither_bits, pwm_lsb_nanoseconds and
  gpio_slowdown with a 400.
- A stored gpio_slowdown or pwm_dither_bits of 0 renders as 0 instead of the
  default, so saving the tab no longer overwrites it.
- Row Address Type offers 5 (SM5368 / B707 row shift register). Verified on a
  Waveshare 96x48 V2 (24S-A1) on a Pi 4 with the Adafruit Triple LED Matrix
  Bonnet: rows 48, cols 96, row address type 5, BGR, GPIO slowdown 8.
- Help text and docs: FM6124-family panels use Panel Type Standard; on a Pi 5
  the library supports only row address types 0 and 2.

No change to the rpi-rgb-led-matrix submodule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): drop the rows cap and document every display setting accurately

Rows: no upper limit in the form or the API. Still even and at least 8. The
current rgbmatrix library rejects more than 64 per panel, so a larger value
saves but the matrix won't start; the help tip, README, config reference and
troubleshooting section all say so, and nothing here needs changing if the
library lifts the limit.

limit_refresh_rate_hz: the form accepts 0 (the library's "no cap"), a stored
0 no longer renders and re-saves as 120, and the API rejects negatives.

pwm_dither_bits stays 0-2: the library rejects 3 and 4, so the old form's
0-4 only ever let users save a config the display couldn't start with.

Docs and help tips, checked against the pinned library and its README:
- panel_type and rp1_rio get README entries
- show_refresh_rate prints to stdout; it never drew on the panel
- dither bits raise the refresh rate; the tip said they lowered it
- scan_mode is about interlacing at low refresh, not wrong colours
- disable_hardware_pulsing: hardware pulsing needs OE on GPIO 18 and the
  onboard sound driver off; software timing makes rows flash brighter
- gpio_slowdown guidance agrees between the README and the UI
- all 22 multiplexing values listed; every numeric setting states its range
- troubleshooting for a blank panel after a settings change, jumping rows
  and brightness flashes

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): reject true and 5.5 for row_address_type and multiplexing

Both still went straight through int(), so a JSON true saved as 1 and 5.5
as 5. They now use the shared hardware range check like the other panel
fields. Review feedback on #586.

Also: the RP1 Backend tooltip said it is ignored on Pi 3/4 (it is ignored
on every model but the Pi 5), and the README gave the dynamic-duration
default cap as 90s; the code default is 180s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: refuse matrix settings a Raspberry Pi 5 can't drive

On a Pi 5 the pinned rgbmatrix library drives the panel through the RP1
chip, and that path supports only row address types 0 and 2, parallel 1-3
and the regular / regular-pi1 / classic / adafruit-hat(-pwm) mappings
(Rp1PioConfigSupported in lib/rp1/rp1_pio_backend.cc). For anything else
CreateFromOptions returns NULL; the Python binding doesn't check, so the
display process crashed on its first call into the matrix and systemd
restarted it into the same crash every 10 seconds.

- src/pi5_matrix_support.py: the rule and Pi 5 detection, matching the
  library's /proc/device-tree/model check
- DisplayManager raises before creating the matrix, so it is a logged init
  failure (reported by /api/v3/hardware/status) and fallback mode
- the config API rejects those settings on a Pi 5 when a request sets
  row_address_type, parallel or hardware_mapping
- the Display form offers only row address types 0 and 2 on a Pi 5, and
  warns when a stored value can't be used
- CLAUDE.md: re-check the rule whenever the submodule is bumped

Review feedback on #586.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:17:48 -04:00

5.5 KiB

LEDMatrix

Project Structure

  • src/plugin_system/ — Plugin loader, manager, store manager, base plugin class
  • web_interface/ — Flask web UI (blueprints, templates, static JS)
  • config/config.json — User plugin configuration (persists across plugin reinstalls)
  • plugin-repos/ — Default plugin install directory used by the Plugin Store, set by plugin_system.plugins_directory in config.json (default per config/config.template.json). Not gitignored.
  • plugins/ — Legacy/dev plugin location. Gitignored (plugins/*). Used by scripts/dev/dev_plugin_setup.sh for symlinks. The plugin loader does NOT fall back to it — PluginManager.discover_plugins() (src/plugin_system/plugin_manager.py) scans only the configured directory. Fallbacks exist in two narrower places: store operations (StoreManager._find_plugin_path() in store_manager.py) and schema lookup (SchemaManager.get_schema_path() in schema_manager.py, which probes plugins/ before plugin-repos/).

Plugin System

  • Plugins inherit from BasePlugin in src/plugin_system/base_plugin.py
  • Required abstract methods: update(), display(force_clear=False)
  • Each plugin needs: manifest.json, config_schema.json, manager.py, requirements.txt
  • Plugin instantiation args: plugin_id, config, display_manager, cache_manager, plugin_manager
  • Config schemas use JSON Schema Draft-7
  • Display dimensions: always read dynamically from self.display_manager.width/height — not display_manager.matrix.width/height, because matrix is None when hardware init fails (the properties fall back to the canvas size)
  • Secrets: namespaced by plugin id in config/config_secrets.json, declared via "x-secret": true in the plugin's config schema, and deep-merged into the plugin's config dict at load time — plugins read them with plain config.get(...), never a separate accessor

Dev Workflow

  • Link a plugin for development: ./scripts/dev/dev_plugin_setup.sh link-github <name> (or link <name> <path>); symlinks land in plugins/ — set plugin_system.plugins_directory to plugins so discovery picks them up
  • Browser preview without the display loop: python3 scripts/dev_server.py → http://localhost:5001
  • Full display in emulator mode: python3 run.py -e (or EMULATOR=true python3 run.py)
  • Validate one plugin headlessly: python3 scripts/check_plugin.py --plugin <id>

Plugin Store Architecture

  • Official plugins live in the ledmatrix-plugins monorepo (not individual repos)
  • Plugin repo naming convention: ledmatrix-<plugin-id> (e.g., ledmatrix-football-scoreboard)
  • plugins.json registry at https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins.json
  • Store manager (src/plugin_system/store_manager.py) handles install/update/uninstall
  • Monorepo plugins are installed via ZIP extraction (no .git directory)
  • Update detection for monorepo plugins uses version comparison (manifest version vs registry latest_version)
  • 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

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

  • 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()
  • DisplayManager has no draw_image() — paste onto the PIL image directly: self.display_manager.image.paste(img, (x, y)) then update_display() (use a mask for transparency: image.paste(rgba, (x, y), rgba))
  • When modifying a plugin in the monorepo, you MUST bump version in its manifest.json and run python update_registry.py — otherwise users won't receive the update
  • src/pi5_matrix_support.py hardcodes what the pinned rpi-rgb-led-matrix-master can drive on a Raspberry Pi 5 (Rp1PioConfigSupported() in lib/rp1/rp1_pio_backend.cc). Re-check it whenever the submodule is bumped: a stale rule blocks Pi 5 settings the new library supports, and a missing one lets the display service crash-loop