The .cursor/ directory holds the dev-side helper docs that Cursor and
contributors using AI tooling rely on to bootstrap plugin development.
Several of them had the same bug patterns as the user-facing docs.
.cursor/plugin_templates/QUICK_START.md
- "Adding Image Rendering" section showed
display_manager.draw_image(image, x=0, y=0). That method doesn't
exist on DisplayManager (same bug as PLUGIN_API_REFERENCE.md and
PLUGIN_DEVELOPMENT_GUIDE.md). Replaced with the canonical
display_manager.image.paste((x,y)) pattern, including the
transparency-mask form.
.cursor/plugins_guide.md
- 10 occurrences of ./dev_plugin_setup.sh — the script lives at
scripts/dev/dev_plugin_setup.sh, so anyone copy-pasting these
examples gets "command not found". Bulk fixed via sed.
- "Test with emulator: python run.py --emulator" — there's no
--emulator flag. Replaced with the real options:
EMULATOR=true python3 run.py for the full display, or
scripts/dev_server.py for the dev preview.
- Secrets management section showed a fictional
"config_secrets": { "api_key": "my-plugin.api_key" } reference
field. Verified in src/config_manager.py:162-172 that secrets are
loaded by deep-merging config_secrets.json into the main config.
There is no separate reference field — just put the secret under
the same plugin namespace and read it from the merged config.
Rewrote the section with the real pattern.
- "ssh pi@raspberrypi" -> "ssh ledpi@your-pi-ip" (consistent with
the rest of LEDMatrix docs which use ledpi as the default user)
.cursor/README.md
- Same ./dev_plugin_setup.sh -> ./scripts/dev/dev_plugin_setup.sh
fix (×6 occurrences via replace_all).
- Same "python run.py --emulator" -> "EMULATOR=true python3 run.py"
fix. Also added a pointer to scripts/dev_server.py for previewing
plugins without running the full display.
- "Example Plugins: plugins/hockey-scoreboard/" — the canonical
source is the ledmatrix-plugins repo. Installed copies land in
plugin-repos/ or plugins/. Updated the line to point at the
ledmatrix-plugins repo and explain both local locations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
19 KiB
LEDMatrix Plugin Development Guide
This guide provides comprehensive instructions for creating, running, and loading plugins in the LEDMatrix project.
Table of Contents
- Plugin System Overview
- Creating a New Plugin
- Running Plugins
- Loading Plugins
- Plugin Development Workflow
- Testing Plugins
- Troubleshooting
Plugin System Overview
The LEDMatrix project uses a plugin-based architecture where all display functionality (except core calendar) is implemented as plugins. Plugins are dynamically loaded from the plugins/ directory and integrated into the display rotation.
Plugin Architecture
LEDMatrix Core
├── Plugin Manager (discovers, loads, manages plugins)
├── Display Manager (handles LED matrix rendering)
├── Cache Manager (data persistence)
├── Config Manager (configuration management)
└── Plugins/ (plugin directory)
├── plugin-1/
├── plugin-2/
└── ...
Plugin Lifecycle
- Discovery: PluginManager scans
plugins/for directories withmanifest.json - Loading: Plugin module is imported and class is instantiated
- Configuration: Plugin config is loaded from
config/config.json - Validation:
validate_config()is called to verify configuration - Registration: Plugin is added to available display modes
- Execution:
update()is called periodically,display()is called during rotation
Creating a New Plugin
Method 1: Using dev_plugin_setup.sh (Recommended)
This method is best for plugins stored in separate Git repositories.
From GitHub Repository
# Link a plugin from GitHub (auto-detects URL)
./scripts/dev/dev_plugin_setup.sh link-github <plugin-name>
# Example: Link hockey-scoreboard plugin
./scripts/dev/dev_plugin_setup.sh link-github hockey-scoreboard
# With custom URL
./scripts/dev/dev_plugin_setup.sh link-github <plugin-name> https://github.com/user/repo.git
The script will:
- Clone the repository to
~/.ledmatrix-dev-plugins/(or configured directory) - Create a symlink in
plugins/<plugin-name>/pointing to the cloned repo - Validate the plugin structure
From Local Repository
# Link a local plugin repository
./scripts/dev/dev_plugin_setup.sh link <plugin-name> <path-to-repo>
# Example: Link a local plugin
./scripts/dev/dev_plugin_setup.sh link my-plugin ../ledmatrix-my-plugin
Method 2: Manual Plugin Creation
- Create Plugin Directory
mkdir -p plugins/my-plugin
cd plugins/my-plugin
- Create manifest.json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"author": "Your Name",
"description": "Description of what this plugin does",
"entry_point": "manager.py",
"class_name": "MyPlugin",
"category": "custom",
"tags": ["custom", "example"],
"display_modes": ["my_plugin"],
"update_interval": 60,
"default_duration": 15,
"requires": {
"python": ">=3.9"
},
"config_schema": "config_schema.json"
}
- Create manager.py
from src.plugin_system.base_plugin import BasePlugin
from PIL import Image
import logging
class MyPlugin(BasePlugin):
"""My custom plugin implementation."""
def update(self):
"""Fetch/update data for this plugin."""
# Fetch data from API, files, etc.
# Use self.cache_manager for caching
cache_key = f"{self.plugin_id}_data"
cached = self.cache_manager.get(cache_key, max_age=3600)
if cached:
self.data = cached
return
# Fetch new data
self.data = self._fetch_data()
self.cache_manager.set(cache_key, self.data)
def display(self, force_clear=False):
"""Render this plugin's display."""
if force_clear:
self.display_manager.clear()
# Render content using display_manager
self.display_manager.draw_text(
"Hello, World!",
x=10, y=15,
color=(255, 255, 255)
)
self.display_manager.update_display()
def _fetch_data(self):
"""Fetch data from external source."""
# Implement your data fetching logic
return {"message": "Hello, World!"}
def validate_config(self):
"""Validate plugin configuration."""
# Check required config fields
if not super().validate_config():
return False
# Add custom validation
required_fields = ['api_key'] # Example
for field in required_fields:
if field not in self.config:
self.logger.error(f"Missing required field: {field}")
return False
return True
- Create config_schema.json
{
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable or disable this plugin"
},
"display_duration": {
"type": "number",
"default": 15,
"minimum": 1,
"description": "How long to display this plugin (seconds)"
},
"api_key": {
"type": "string",
"description": "API key for external service"
}
},
"required": ["enabled"]
}
- Create requirements.txt (if needed)
requests>=2.28.0
pillow>=9.0.0
- Create README.md
Document your plugin's functionality, configuration options, and usage.
Running Plugins
Development Mode (Emulator)
Run the LEDMatrix system with emulator for plugin testing:
# Using run.py
python run.py --emulator
# Using emulator script
./run_emulator.sh
The emulator will:
- Load all enabled plugins
- Display plugin content in a window (simulating LED matrix)
- Show logs for plugin loading and execution
- Allow testing without Raspberry Pi hardware
Production Mode (Raspberry Pi)
Run on actual Raspberry Pi hardware:
# Direct execution
python run.py
# As systemd service
sudo systemctl start ledmatrix
sudo systemctl status ledmatrix
sudo journalctl -u ledmatrix -f # View logs
Plugin-Specific Testing
Test individual plugin loading:
# test_my_plugin.py
from src.plugin_system.plugin_manager import PluginManager
from src.config_manager import ConfigManager
from src.display_manager import DisplayManager
from src.cache_manager import CacheManager
# Initialize managers
config_manager = ConfigManager()
config = config_manager.load_config()
display_manager = DisplayManager(config)
cache_manager = CacheManager()
# Initialize plugin manager
plugin_manager = PluginManager(
plugins_dir="plugins",
config_manager=config_manager,
display_manager=display_manager,
cache_manager=cache_manager
)
# Discover and load plugin
plugins = plugin_manager.discover_plugins()
print(f"Discovered plugins: {plugins}")
if "my-plugin" in plugins:
if plugin_manager.load_plugin("my-plugin"):
plugin = plugin_manager.get_plugin("my-plugin")
plugin.update()
plugin.display()
print("Plugin loaded and displayed successfully!")
else:
print("Failed to load plugin")
Loading Plugins
Enabling Plugins
Plugins are enabled/disabled in config/config.json:
{
"my-plugin": {
"enabled": true,
"display_duration": 15,
"api_key": "your-api-key-here"
}
}
Plugin Configuration Structure
Each plugin has its own section in config/config.json:
{
"<plugin-id>": {
"enabled": true, // Enable/disable plugin
"display_duration": 15, // Display duration in seconds
"live_priority": false, // Enable live priority takeover
"high_performance_transitions": false, // Use 120 FPS transitions
"transition": { // Transition configuration
"type": "redraw", // Transition type
"speed": 2, // Transition speed
"enabled": true // Enable transitions
},
// ... plugin-specific configuration
}
}
Secrets Management
Store sensitive data (API keys, tokens) in config/config_secrets.json
under the same plugin id you use in config/config.json:
{
"my-plugin": {
"api_key": "secret-api-key-here"
}
}
At load time, the config manager deep-merges config_secrets.json into
the main config (verified at src/config_manager.py:162-172). So in
your plugin's code:
class MyPlugin(BasePlugin):
def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
self.api_key = config.get("api_key") # already merged from secrets
There is no separate config_secrets reference field — just put the
secret value under the same plugin namespace and read it from the
merged config.
Plugin Discovery
Plugins are automatically discovered when:
- Directory exists in
plugins/ - Directory contains
manifest.json - Manifest has required fields (
id,entry_point,class_name)
Check discovered plugins:
# Using dev_plugin_setup.sh
./scripts/dev/dev_plugin_setup.sh list
# Output shows:
# ✓ plugin-name (symlink)
# → /path/to/repo
# ✓ Git repo is clean (branch: main)
Plugin Status
Check plugin status and git information:
./scripts/dev/dev_plugin_setup.sh status
# Output shows:
# ✓ plugin-name
# Path: /path/to/repo
# Branch: main
# Remote: https://github.com/user/repo.git
# Status: Clean and up to date
Plugin Development Workflow
1. Initial Setup
# Create or clone plugin repository
git clone https://github.com/user/ledmatrix-my-plugin.git
cd ledmatrix-my-plugin
# Link to LEDMatrix project
cd /path/to/LEDMatrix
./scripts/dev/dev_plugin_setup.sh link my-plugin ../ledmatrix-my-plugin
2. Development Cycle
- Edit plugin code in linked repository
- Test with the dev preview server:
python3 scripts/dev_server.py(then openhttp://localhost:5001). Or run the full display in emulator mode withEMULATOR=true python3 run.py. There is no--emulatorflag. - Check logs for errors or warnings
- Update configuration in
config/config.jsonif needed - Iterate until plugin works correctly
3. Testing on Hardware
# Deploy to Raspberry Pi
rsync -avz plugins/my-plugin/ ledpi@your-pi-ip:/path/to/LEDMatrix/plugins/my-plugin/
# Or if using git, pull on Pi
ssh ledpi@your-pi-ip "cd /path/to/LEDMatrix/plugins/my-plugin && git pull"
# Restart service
ssh ledpi@your-pi-ip "sudo systemctl restart ledmatrix"
4. Updating Plugins
# Update single plugin from git
./scripts/dev/dev_plugin_setup.sh update my-plugin
# Update all linked plugins
./scripts/dev/dev_plugin_setup.sh update
5. Unlinking Plugins
# Remove symlink (preserves repository)
./scripts/dev/dev_plugin_setup.sh unlink my-plugin
Testing Plugins
Unit Testing
Create test files in plugin directory:
# plugins/my-plugin/test_my_plugin.py
import unittest
from unittest.mock import Mock, MagicMock
from manager import MyPlugin
class TestMyPlugin(unittest.TestCase):
def setUp(self):
self.config = {"enabled": True}
self.display_manager = Mock()
self.cache_manager = Mock()
self.plugin_manager = Mock()
self.plugin = MyPlugin(
plugin_id="my-plugin",
config=self.config,
display_manager=self.display_manager,
cache_manager=self.cache_manager,
plugin_manager=self.plugin_manager
)
def test_plugin_initialization(self):
self.assertEqual(self.plugin.plugin_id, "my-plugin")
self.assertTrue(self.plugin.enabled)
def test_config_validation(self):
self.assertTrue(self.plugin.validate_config())
def test_update(self):
self.cache_manager.get.return_value = None
self.plugin.update()
# Assert data was fetched and cached
def test_display(self):
self.plugin.display()
self.display_manager.draw_text.assert_called()
self.display_manager.update_display.assert_called()
if __name__ == '__main__':
unittest.main()
Run tests:
cd plugins/my-plugin
python -m pytest test_my_plugin.py
# or
python test_my_plugin.py
Integration Testing
Test plugin with actual managers:
# test_plugin_integration.py
from src.plugin_system.plugin_manager import PluginManager
from src.config_manager import ConfigManager
from src.display_manager import DisplayManager
from src.cache_manager import CacheManager
def test_plugin_loading():
config_manager = ConfigManager()
config = config_manager.load_config()
display_manager = DisplayManager(config)
cache_manager = CacheManager()
plugin_manager = PluginManager(
plugins_dir="plugins",
config_manager=config_manager,
display_manager=display_manager,
cache_manager=cache_manager
)
plugins = plugin_manager.discover_plugins()
assert "my-plugin" in plugins
assert plugin_manager.load_plugin("my-plugin")
plugin = plugin_manager.get_plugin("my-plugin")
assert plugin is not None
assert plugin.enabled
plugin.update()
plugin.display()
Emulator Testing
Test plugin rendering visually:
# Run with emulator
python run.py --emulator
# Plugin should appear in display rotation
# Check logs for plugin loading and execution
Hardware Testing
- Deploy plugin to Raspberry Pi
- Enable in
config/config.json - Restart LEDMatrix service
- Observe LED matrix display
- Check logs:
journalctl -u ledmatrix -f
Troubleshooting
Plugin Not Loading
Symptoms: Plugin doesn't appear in available modes, no logs about plugin
Solutions:
- Check plugin directory exists:
ls plugins/my-plugin/ - Verify
manifest.jsonexists and is valid JSON - Check manifest has required fields:
id,entry_point,class_name - Verify entry_point file exists:
ls plugins/my-plugin/manager.py - Check class name matches:
grep "class.*Plugin" plugins/my-plugin/manager.py - Review logs for import errors
Plugin Loading but Not Displaying
Symptoms: Plugin loads successfully but doesn't appear in rotation
Solutions:
- Check plugin is enabled:
config/config.jsonhas"enabled": true - Verify display_modes in manifest match config
- Check plugin is in rotation schedule
- Review
display()method for errors - Check logs for runtime errors
Configuration Errors
Symptoms: Plugin fails to load, validation errors in logs
Solutions:
- Validate config against
config_schema.json - Check required fields are present
- Verify data types match schema
- Check for typos in config keys
- Review
validate_config()method
Import Errors
Symptoms: ModuleNotFoundError or ImportError in logs
Solutions:
- Install plugin dependencies:
pip install -r plugins/my-plugin/requirements.txt - Check Python path includes plugin directory
- Verify relative imports are correct
- Check for circular import issues
- Ensure all dependencies are in requirements.txt
Display Issues
Symptoms: Plugin renders incorrectly or not at all
Solutions:
- Check display dimensions:
display_manager.width,display_manager.height - Verify coordinates are within display bounds
- Check color values are valid (0-255)
- Ensure
update_display()is called after rendering - Test with emulator first to debug rendering
Performance Issues
Symptoms: Slow display updates, high CPU usage
Solutions:
- Use
cache_managerto avoid excessive API calls - Implement background data fetching
- Optimize rendering code
- Consider using
high_performance_transitions - Profile plugin code to identify bottlenecks
Git/Symlink Issues
Symptoms: Plugin changes not appearing, broken symlinks
Solutions:
- Check symlink:
ls -la plugins/my-plugin - Verify target exists:
readlink -f plugins/my-plugin - Update plugin:
./scripts/dev/dev_plugin_setup.sh update my-plugin - Re-link plugin if needed:
./scripts/dev/dev_plugin_setup.sh unlink my-plugin && ./scripts/dev/dev_plugin_setup.sh link my-plugin <path> - Check git status:
cd plugins/my-plugin && git status
Best Practices
Code Organization
- Keep plugin code in
plugins/<plugin-id>/directory - Use descriptive class and method names
- Follow existing plugin patterns
- Place shared utilities in
src/common/if reusable
Configuration
- Always use
config_schema.jsonfor validation - Store secrets in
config_secrets.json - Provide sensible defaults
- Document all configuration options in README
Error Handling
- Use plugin logger for all logging
- Handle API failures gracefully
- Provide fallback displays when data unavailable
- Cache data to avoid excessive requests
Performance
- Cache API responses appropriately
- Use background data fetching for long operations
- Optimize rendering for Pi's limited resources
- Test performance on actual hardware
Testing
- Write unit tests for core logic
- Test with emulator before hardware
- Test on Raspberry Pi before deploying
- Test with other plugins enabled
Documentation
- Document plugin functionality in README
- Include configuration examples
- Document API requirements and rate limits
- Provide usage examples
Resources
- Plugin System Documentation:
docs/PLUGIN_ARCHITECTURE_SPEC.md - Base Plugin Class:
src/plugin_system/base_plugin.py - Plugin Manager:
src/plugin_system/plugin_manager.py - Example Plugins:
plugins/hockey-scoreboard/- Sports scoreboard exampleplugins/football-scoreboard/- Complex multi-league exampleplugins/ledmatrix-music/- Real-time data example
- Development Setup:
dev_plugin_setup.sh - Example Config:
dev_plugins.json.example
Quick Reference
Common Commands
# Link plugin from GitHub
./scripts/dev/dev_plugin_setup.sh link-github <name>
# Link local plugin
./scripts/dev/dev_plugin_setup.sh link <name> <path>
# List all plugins
./scripts/dev/dev_plugin_setup.sh list
# Check plugin status
./scripts/dev/dev_plugin_setup.sh status
# Update plugin(s)
./scripts/dev/dev_plugin_setup.sh update [name]
# Unlink plugin
./scripts/dev/dev_plugin_setup.sh unlink <name>
# Run with emulator
python run.py --emulator
# Run on Pi
python run.py
Plugin File Structure
plugins/my-plugin/
├── manifest.json # Required: Plugin metadata
├── manager.py # Required: Plugin class
├── config_schema.json # Required: Config validation
├── requirements.txt # Optional: Dependencies
├── README.md # Optional: Documentation
└── ... # Plugin-specific files
Required Manifest Fields
id: Plugin identifierentry_point: Python file (usually "manager.py")class_name: Plugin class namedisplay_modes: Array of mode names