# 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 # Link local repository ./scripts/dev/dev_plugin_setup.sh link ``` **Option B: Manual Setup** 1. Create directory in `plugin-repos//` (or `plugins//` 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 `"": {"enabled": true}` ### 2. Plugin Configuration Plugins are configured in `config/config.json`: ```json { "": { "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//` (or its dev-time symlink in `plugins//`) - 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 - `clear_cache(key=None)`: Remove a cache entry, or all entries if `key` is omitted. There is no `delete()` method. - `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/ / 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-` **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`