mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
* fix: post-audit follow-up code fixes (cache, fonts, icons, dev script, CI) The docs refresh effort (#306, ledmatrix-plugins#92) surfaced seven code bugs that were intentionally left out of the docs PRs because they required code changes rather than doc fixes. This PR addresses the six that belong in LEDMatrix (the seventh — a lacrosse-scoreboard mode rename — lives in the plugins repo). Bug 1: cache_manager.delete() AttributeError src/common/api_helper.py:287 and src/plugin_system/resource_monitor.py:343 both call cache_manager.delete(key), which doesn't exist — only clear_cache(key=None). Added a delete() alias method on CacheManager that forwards to clear_cache(key). Reverts the "There is no delete() method" wording in DEVELOPER_QUICK_REFERENCE, .cursorrules so the docs match the new shim. Bug 2: dev_plugin_setup.sh PROJECT_ROOT resolution scripts/dev/dev_plugin_setup.sh:9 set PROJECT_ROOT to SCRIPT_DIR instead of walking up two levels to the repo root, so PLUGINS_DIR resolved to scripts/dev/plugins/ and created symlinks under the script's own directory. Fixed the path and removed the stray scripts/dev/plugins/of-the-day symlink left by earlier runs. Bug 3: plugin custom icons regressed from v2 to v3 web_interface/blueprints/api_v3.py built the /plugins/installed response without including the manifest's "icon" field, and web_interface/templates/v3/base.html hardcoded fas fa-puzzle-piece in all three plugin-tab render sites. Pass the icon through the API and read it from the templates with a puzzle-piece fallback. Reverts the "currently broken" banners in docs/PLUGIN_CUSTOM_ICONS.md and docs/PLUGIN_CUSTOM_ICONS_FEATURE.md. Bug 4: register_plugin_fonts was never wired up src/font_manager.py:150 defines register_plugin_fonts(plugin_id, font_manifest) but nothing called it, so plugin manifests with a "fonts" block were silently no-ops. Wired the call into PluginManager.load_plugin() right after plugin_loader.load_plugin returns. Reverts the "not currently wired" warning in docs/FONT_MANAGER.md's "For Plugin Developers" section. Bug 5: dead web_interface_v2 import pattern (LEDMatrix half) src/base_odds_manager.py had a try/except importing web_interface_v2.increment_api_counter, falling back to a no-op stub. The module doesn't exist anywhere in the v3 codebase and no API metrics dashboard reads it. Deleted the import block and the single call site; the plugins-repo half of this cleanup lands in ledmatrix-plugins#<next>. Bug 7: no CI test workflow .github/workflows/ only contained security-audit.yml; pytest ran locally but was not gated on PRs. Added .github/workflows/tests.yml running pytest against Python 3.10, 3.11, 3.12 in EMULATOR=true mode, skipping tests marked hardware or slow. Updated docs/HOW_TO_RUN_TESTS.md to reflect that the workflow now exists. Verification done locally: - CacheManager.delete(key) round-trips with set/get - base_odds_manager imports without the v2 module present - dev_plugin_setup.sh PROJECT_ROOT resolves to repo root - api_v3 and plugin_manager compile clean - tests.yml YAML parses - Script syntax check on dev_plugin_setup.sh Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review comments on #307 - src/cache_manager.py: clear_cache(key) treated empty string as "wipe all" because of `if key:`. Switched to `key is None` branching, made delete(key) and clear_cache(key) reject empty strings and None outright with ValueError, and updated both docstrings to make the contract explicit. Verified locally with a round-trip test that clear_cache() (no arg) still wipes everything but clear_cache("") and delete("") raise. - src/plugin_system/plugin_manager.py: was reaching for the font manager via getattr(self.display_manager, 'font_manager', None). PluginManager already takes a dedicated font_manager parameter (line 54) and stores it as self.font_manager (line 69), so the old path was both wrong and could miss the font manager entirely when the host injects them separately. Switched to self.font_manager directly with the same try/except warning behavior. - web_interface/templates/v3/base.html: in the full plugin-tab renderer, the icon was injected with `<i class="${escapeHtml(plugin.icon)}">` — but escapeHtml only escapes <, >, and &, not double quotes, so a manifest with a quote in its icon string could break out of the class attribute. Replaced the innerHTML template with createElement for the <i> tag, set className from plugin.icon directly (no string interpolation), and used a text node for the label. Same fix shape would also harden the two stub-renderer sites at line 515 / 774, but those already escape `"` to " and CodeRabbit only flagged this site, so leaving them for now. - docs/FONT_MANAGER.md: clarified that the Manual Font Overrides *workflow* (set_override / remove_override / font_overrides.json) is the supported override path today, and only the Fonts tab in the web UI is the placeholder. Previous wording conflated the two and made it sound like overrides themselves were broken. - docs/HOW_TO_RUN_TESTS.md: replaced the vague "see the PR adding it" with a concrete link to #307 and a note that the workflow file itself is held back pending the workflow scope. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
365 lines
14 KiB
Plaintext
365 lines
14 KiB
Plaintext
# LEDMatrix Plugin Development Rules
|
|
|
|
## Plugin System Overview
|
|
|
|
The LEDMatrix project uses a plugin-based architecture. All display
|
|
functionality (except core calendar) is implemented as plugins that are
|
|
dynamically loaded from the directory configured by
|
|
`plugin_system.plugins_directory` in `config.json` — the default is
|
|
`plugin-repos/` (per `config/config.template.json:130`).
|
|
|
|
> **Fallback note (scoped):** `PluginManager.discover_plugins()`
|
|
> (`src/plugin_system/plugin_manager.py:154`) only scans the
|
|
> configured directory — there is no fallback to `plugins/` in the
|
|
> main discovery path. A fallback to `plugins/` does exist in two
|
|
> narrower places:
|
|
> - `store_manager.py:1700-1718` — store operations (install/update/
|
|
> uninstall) check `plugins/` if the plugin isn't found in the
|
|
> configured directory, so plugin-store flows work even when your
|
|
> dev symlinks live in `plugins/`.
|
|
> - `schema_manager.py:70-80` — `get_schema_path()` probes both
|
|
> `plugins/` and `plugin-repos/` for `config_schema.json` so the
|
|
> web UI form generation finds the schema regardless of where the
|
|
> plugin lives.
|
|
>
|
|
> The dev workflow in `scripts/dev/dev_plugin_setup.sh` creates
|
|
> symlinks under `plugins/`, which is why the store and schema
|
|
> fallbacks exist. For day-to-day development, set
|
|
> `plugin_system.plugins_directory` to `plugins` so the main
|
|
> discovery path picks up your symlinks.
|
|
|
|
## Plugin Structure
|
|
|
|
### Required Files
|
|
- **manifest.json**: Plugin metadata, entry point, class name, dependencies
|
|
- **manager.py**: Main plugin class (must inherit from `BasePlugin`)
|
|
- **config_schema.json**: JSON schema for plugin configuration validation
|
|
- **requirements.txt**: Python dependencies (if any)
|
|
- **README.md**: Plugin documentation
|
|
|
|
### Plugin Class Requirements
|
|
- Must inherit from `src.plugin_system.base_plugin.BasePlugin`
|
|
- Must implement `update()` method for data fetching
|
|
- Must implement `display()` method for rendering
|
|
- Should implement `validate_config()` for configuration validation
|
|
- Optional: Override `has_live_content()` for live priority features
|
|
|
|
## Plugin Development Workflow
|
|
|
|
### 1. Creating a New Plugin
|
|
|
|
**Option A: Use dev_plugin_setup.sh (Recommended)**
|
|
```bash
|
|
# Link from GitHub
|
|
./scripts/dev/dev_plugin_setup.sh link-github <plugin-name>
|
|
|
|
# Link local repository
|
|
./scripts/dev/dev_plugin_setup.sh link <plugin-name> <path-to-repo>
|
|
```
|
|
|
|
**Option B: Manual Setup**
|
|
1. Create directory in `plugin-repos/<plugin-id>/` (or `plugins/<plugin-id>/`
|
|
if you're using the dev fallback location)
|
|
2. Add `manifest.json` with required fields
|
|
3. Create `manager.py` with plugin class
|
|
4. Add `config_schema.json` for configuration
|
|
5. Enable plugin in `config/config.json` under `"<plugin-id>": {"enabled": true}`
|
|
|
|
### 2. Plugin Configuration
|
|
|
|
Plugins are configured in `config/config.json`:
|
|
```json
|
|
{
|
|
"<plugin-id>": {
|
|
"enabled": true,
|
|
"display_duration": 15,
|
|
"live_priority": false,
|
|
"high_performance_transitions": false,
|
|
"transition": {
|
|
"type": "redraw",
|
|
"speed": 2,
|
|
"enabled": true
|
|
},
|
|
// ... plugin-specific config
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Testing Plugins
|
|
|
|
**On Development Machine:**
|
|
- Run the dev preview server: `python3 scripts/dev_server.py` (then
|
|
open `http://localhost:5001`) — renders plugins in the browser
|
|
without running the full display loop
|
|
- Or run the full display in emulator mode:
|
|
`python3 run.py --emulator` (or equivalently
|
|
`EMULATOR=true python3 run.py`, or `./scripts/dev/run_emulator.sh`).
|
|
The `-e`/`--emulator` CLI flag is defined in `run.py:19-20`.
|
|
- Test plugin loading: Check logs for plugin discovery and loading
|
|
- Validate configuration: Ensure config matches `config_schema.json`
|
|
|
|
**On Raspberry Pi:**
|
|
- Deploy and test on actual hardware
|
|
- Monitor logs: `journalctl -u ledmatrix -f` (if running as service)
|
|
- Check plugin status in web interface
|
|
|
|
### 4. Plugin Development Best Practices
|
|
|
|
**Code Organization:**
|
|
- Keep plugin code in `plugin-repos/<plugin-id>/` (or its dev-time
|
|
symlink in `plugins/<plugin-id>/`)
|
|
- Use shared assets from `assets/` directory when possible
|
|
- Follow existing plugin patterns — canonical sources live in the
|
|
[`ledmatrix-plugins`](https://github.com/ChuckBuilds/ledmatrix-plugins)
|
|
repo (`plugins/hockey-scoreboard/`, `plugins/football-scoreboard/`,
|
|
`plugins/clock-simple/`, etc.)
|
|
- Place shared utilities in `src/common/` if reusable across plugins
|
|
|
|
**Configuration Management:**
|
|
- Use `config_schema.json` for validation
|
|
- Store secrets in `config/config_secrets.json` under the same plugin
|
|
id namespace as the main config — they're deep-merged into the main
|
|
config at load time (`src/config_manager.py:162-172`), so plugin
|
|
code reads them directly from `config.get(...)` like any other key
|
|
- There is no separate `config_secrets` reference field
|
|
- Validate all required fields in `validate_config()`
|
|
|
|
**Error Handling:**
|
|
- Use plugin's logger: `self.logger.info/error/warning()`
|
|
- Handle API failures gracefully
|
|
- Cache data to avoid excessive API calls
|
|
- Provide fallback displays when data unavailable
|
|
|
|
**Performance:**
|
|
- Use `cache_manager` for API response caching
|
|
- Implement background data fetching if needed
|
|
- Use `high_performance_transitions` for smoother animations
|
|
- Optimize rendering for Pi's limited resources
|
|
|
|
**Display Rendering:**
|
|
- Use `display_manager` for all drawing operations
|
|
- Support different display sizes (check `display_manager.width/height`)
|
|
- Use `apply_transition()` for smooth transitions between displays
|
|
- Clear display before rendering: `display_manager.clear()`
|
|
- Always call `display_manager.update_display()` after rendering
|
|
|
|
## Plugin API Reference
|
|
|
|
### BasePlugin Class
|
|
Located in: `src/plugin_system/base_plugin.py`
|
|
|
|
**Required Methods:**
|
|
- `update()`: Fetch/update data (called based on `update_interval` in manifest)
|
|
- `display(force_clear=False)`: Render plugin content
|
|
|
|
**Optional Methods:**
|
|
- `validate_config()`: Validate plugin configuration
|
|
- `has_live_content()`: Return True if plugin has live/urgent content
|
|
- `get_live_modes()`: Return list of modes for live priority
|
|
- `cleanup()`: Clean up resources on unload
|
|
- `on_config_change(new_config)`: Handle config updates
|
|
- `on_enable()`: Called when plugin enabled
|
|
- `on_disable()`: Called when plugin disabled
|
|
|
|
**Available Properties:**
|
|
- `self.plugin_id`: Plugin identifier
|
|
- `self.config`: Plugin configuration dict
|
|
- `self.display_manager`: Display manager instance
|
|
- `self.cache_manager`: Cache manager instance
|
|
- `self.plugin_manager`: Plugin manager reference
|
|
- `self.logger`: Plugin-specific logger
|
|
- `self.enabled`: Boolean enabled status
|
|
- `self.transition_manager`: Transition system (if available)
|
|
|
|
### Display Manager
|
|
Located in: `src/display_manager.py`
|
|
|
|
**Key Methods:**
|
|
- `clear()`: Clear the display
|
|
- `draw_text(text, x, y, color, font, small_font, centered)`: Draw text
|
|
- `update_display()`: Push the buffer to the physical display
|
|
- `draw_weather_icon(condition, x, y, size)`: Draw a weather icon
|
|
- `width`, `height`: Display dimensions
|
|
|
|
**Image rendering**: there is no `draw_image()` helper. Paste directly
|
|
onto the underlying PIL Image:
|
|
```python
|
|
self.display_manager.image.paste(pil_image, (x, y))
|
|
self.display_manager.update_display()
|
|
```
|
|
For transparency, paste with a mask: `image.paste(rgba, (x, y), rgba)`.
|
|
|
|
### Cache Manager
|
|
Located in: `src/cache_manager.py`
|
|
|
|
**Key Methods:**
|
|
- `get(key, max_age=300)`: Get cached value (returns None if missing/stale)
|
|
- `set(key, value, ttl=None)`: Cache a value
|
|
- `delete(key)` / `clear_cache(key=None)`: Remove a single cache entry,
|
|
or (for `clear_cache` with no argument) every cached entry. `delete`
|
|
is an alias for `clear_cache(key)`.
|
|
- `get_cached_data_with_strategy(key, data_type)`: Cache get with
|
|
data-type-aware TTL strategy
|
|
- `get_background_cached_data(key, sport_key)`: Cache get for the
|
|
background-fetch service path
|
|
|
|
## Plugin Manifest Schema
|
|
|
|
Required fields in `manifest.json`:
|
|
- `id`: Unique plugin identifier (matches directory name)
|
|
- `name`: Human-readable plugin name
|
|
- `version`: Semantic version (e.g., "1.0.0")
|
|
- `entry_point`: Python file (usually "manager.py")
|
|
- `class_name`: Plugin class name (must match class in entry_point)
|
|
- `display_modes`: Array of mode names this plugin provides
|
|
|
|
Common optional fields:
|
|
- `description`: Plugin description
|
|
- `author`: Plugin author
|
|
- `homepage`: Plugin homepage URL
|
|
- `category`: Plugin category (e.g., "sports", "weather")
|
|
- `tags`: Array of tags
|
|
- `update_interval`: Seconds between update() calls (default: 60)
|
|
- `default_duration`: Default display duration (default: 15)
|
|
- `requires`: Python version, display size requirements
|
|
- `config_schema`: Path to config schema file
|
|
- `api_requirements`: API dependencies and rate limits
|
|
|
|
## Plugin Loading Process
|
|
|
|
1. **Discovery**: PluginManager scans `plugins/` directory for directories containing `manifest.json`
|
|
2. **Validation**: Validates manifest structure and required fields
|
|
3. **Loading**: Imports plugin module and instantiates plugin class
|
|
4. **Configuration**: Loads plugin config from `config/config.json`
|
|
5. **Validation**: Calls `validate_config()` on plugin instance
|
|
6. **Registration**: Adds plugin to available modes and stores instance
|
|
7. **Enablement**: Calls `on_enable()` if plugin is enabled
|
|
|
|
## Common Plugin Patterns
|
|
|
|
### Sports Scoreboard Plugin
|
|
- Use `background_data_service.py` pattern for API fetching
|
|
- Implement live/recent/upcoming game modes
|
|
- Use `scoreboard_renderer.py` for consistent rendering
|
|
- Support team filtering and game filtering
|
|
- Use shared sports logos from `assets/sports/`
|
|
|
|
### Data Display Plugin
|
|
- Fetch data in `update()` method
|
|
- Cache API responses using `cache_manager`
|
|
- Render in `display()` method
|
|
- Handle API errors gracefully
|
|
- Provide configuration for refresh intervals
|
|
|
|
### Real-time Content Plugin
|
|
- Implement `has_live_content()` for live priority
|
|
- Use `get_live_modes()` to specify which modes are live
|
|
- Set `live_priority: true` in config to enable live takeover
|
|
- Update data frequently when live content exists
|
|
|
|
## Debugging Plugins
|
|
|
|
**Check Plugin Loading:**
|
|
- Review logs for plugin discovery messages
|
|
- Verify manifest.json syntax is valid JSON
|
|
- Check that class_name matches actual class name
|
|
- Ensure entry_point file exists and is importable
|
|
|
|
**Check Plugin Execution:**
|
|
- Add logging statements in `update()` and `display()`
|
|
- Use `self.logger` for plugin-specific logging
|
|
- Check cache_manager for cached data
|
|
- Verify display_manager is rendering correctly
|
|
|
|
**Common Issues:**
|
|
- Import errors: Check Python path and dependencies
|
|
- Config errors: Validate against config_schema.json
|
|
- Display issues: Check display dimensions and coordinate calculations
|
|
- Performance: Monitor CPU/memory usage on Pi
|
|
|
|
## Plugin Testing
|
|
|
|
**Unit Tests:**
|
|
- Test plugin class instantiation
|
|
- Test `update()` data fetching logic
|
|
- Test `display()` rendering logic
|
|
- Test `validate_config()` with various configs
|
|
- Mock `display_manager` and `cache_manager` for testing
|
|
|
|
**Integration Tests:**
|
|
- Test plugin loading via PluginManager
|
|
- Test plugin with actual config
|
|
- Test plugin with emulator display
|
|
- Test plugin with cache_manager
|
|
|
|
**Hardware Tests:**
|
|
- Test on Raspberry Pi with LED matrix
|
|
- Verify display rendering on actual hardware
|
|
- Test performance under load
|
|
- Test with other plugins enabled
|
|
|
|
## File Organization
|
|
|
|
```
|
|
plugins/
|
|
<plugin-id>/
|
|
manifest.json # Plugin metadata
|
|
manager.py # Main plugin class
|
|
config_schema.json # Config validation schema
|
|
requirements.txt # Python dependencies
|
|
README.md # Plugin documentation
|
|
# Plugin-specific files
|
|
data_manager.py
|
|
renderer.py
|
|
etc.
|
|
```
|
|
|
|
## Git Workflow for Plugins
|
|
|
|
**Plugin Development:**
|
|
- Plugins are typically separate repositories
|
|
- Use `dev_plugin_setup.sh` to link plugins for development
|
|
- Symlinks are used to connect plugin repos to `plugins/` directory
|
|
- Plugin repos follow naming: `ledmatrix-<plugin-name>`
|
|
|
|
**Branching:**
|
|
- Develop plugins in feature branches
|
|
- Follow project branching conventions
|
|
- Test plugins before merging to main
|
|
|
|
**Automatic Version Bumping:**
|
|
- **Automatic Version Management**: Version bumping is handled automatically via the pre-push git hook - no manual version bumping is required for normal development workflows
|
|
- **GitHub as Source of Truth**: Plugin store always fetches latest versions from GitHub (releases/tags/manifest/commit)
|
|
- **Pre-Push Hook**: Automatically bumps patch version and creates git tags when pushing code changes
|
|
- The hook is self-contained (no external dependencies) and works on any dev machine
|
|
- Installation: Copy the hook from LEDMatrix repo to your plugin repo:
|
|
```bash
|
|
# From your plugin repository directory
|
|
cp /path/to/LEDMatrix/scripts/git-hooks/pre-push-plugin-version .git/hooks/pre-push
|
|
chmod +x .git/hooks/pre-push
|
|
```
|
|
- Or use the installer script from the main LEDMatrix repo (one-time setup)
|
|
- The hook automatically:
|
|
1. Bumps the patch version (x.y.Z) in manifest.json when code changes are detected
|
|
2. Creates a git tag (v{version}) for the new version
|
|
3. Stages manifest.json for commit
|
|
- Skip auto-tagging: Set `SKIP_TAG=1` environment variable before pushing
|
|
- **Manual Version Bumping (Edge Cases Only)**: Manual version bumps are only needed in rare circumstances:
|
|
- CI/CD pipelines that bypass git hooks
|
|
- Forked repositories without the pre-push hook installed
|
|
- Major/minor version bumps (hook only handles patch versions)
|
|
- When skipping auto-tagging but still needing a version bump
|
|
- For manual bumps, use the standalone script: `scripts/bump_plugin_version.py`
|
|
- **Registry**: The plugin registry (plugins.json) stores only metadata (name, description, repo URL) - no versions
|
|
- **Version Priority**: Plugin store checks versions in this order: GitHub Releases → GitHub Tags → Manifest from branch → Git commit hash
|
|
|
|
## Resources
|
|
|
|
- Plugin System Docs: `docs/PLUGIN_ARCHITECTURE_SPEC.md`
|
|
- Plugin Examples: `plugins/hockey-scoreboard/`, `plugins/football-scoreboard/`
|
|
- Base Plugin: `src/plugin_system/base_plugin.py`
|
|
- Plugin Manager: `src/plugin_system/plugin_manager.py`
|
|
- Development Setup: `dev_plugin_setup.sh`
|
|
- Example Config: `dev_plugins.json.example`
|
|
|