Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38a0ea898d | ||
|
|
d9683e28be | ||
|
|
2af41c561b | ||
|
|
5b81cca684 | ||
|
|
d305be6089 | ||
|
|
53af53b4a1 | ||
|
|
183e23edb3 | ||
|
|
970ca2d04f | ||
|
|
f2b246ef03 | ||
|
|
963ab8292a | ||
|
|
16fbb7ebeb | ||
|
|
21825cbfbc | ||
|
|
82a65ad2a2 | ||
|
|
83f20b64fe | ||
|
|
5b45f35888 | ||
|
|
e2acbfb566 | ||
|
|
3872a68ff7 | ||
|
|
989162d28f | ||
|
|
cdf03fb107 | ||
|
|
6a9d8014e5 | ||
|
|
c90129285c | ||
|
|
66f9950a30 | ||
|
|
4abcd0e4f9 | ||
|
|
2a1c47fa76 | ||
|
|
9db1d2391a | ||
|
|
14a59c863c | ||
|
|
bff13129c4 | ||
|
|
6499794c12 | ||
|
|
3d347a368a | ||
|
|
0aca40cf3a | ||
|
|
9837315308 | ||
|
|
c1fa5094be | ||
|
|
4d49b0f892 | ||
|
|
efe76d3add | ||
|
|
273d9962d1 | ||
|
|
9e3b5f366e | ||
|
|
6edd80d9f3 | ||
|
|
1c7a0cef66 | ||
|
|
6052a60d22 | ||
|
|
7f7f0d6464 | ||
|
|
05e7c43b27 | ||
|
|
2ffc57cf40 | ||
|
|
aab0e9ade0 | ||
|
|
978a03b42d | ||
|
|
bd9f461f70 | ||
|
|
3b93024993 | ||
|
|
85d321cf33 | ||
|
|
63a233f3ed | ||
|
|
7a9d01342a | ||
|
|
9b2f02681d | ||
|
|
7a6bad29fe | ||
|
|
bea00448d3 | ||
|
|
deaa3d7a98 | ||
|
|
cbb8ec41e8 | ||
|
|
c6ce332d49 | ||
|
|
8e5f66501a | ||
|
|
639e1c3a93 | ||
|
|
6096a22c3d | ||
|
|
fefc2d44a2 | ||
|
|
d297dd6217 | ||
|
|
974d7ea57a | ||
|
|
ab0cfd2362 | ||
|
|
d22d0a3754 | ||
|
|
5beef0aa01 | ||
|
|
cf28a8c0d5 | ||
|
|
a06682981c | ||
|
|
bc027c921d | ||
|
|
e0bd7088fa | ||
|
|
313e35a98f | ||
|
|
122e6d6863 | ||
|
|
d488e8a2ad | ||
|
|
b9dcbb5152 | ||
|
|
f27fd260f7 | ||
|
|
eedf680a8c | ||
|
|
ac3a15bfaa | ||
|
|
4961697251 | ||
|
|
cac9644b6d | ||
|
|
f96fdd9f24 | ||
|
|
35c540d0e0 | ||
|
|
7603909c59 | ||
|
|
34b186125a | ||
|
|
ea95f37d73 | ||
|
|
0c7d03a476 | ||
|
|
321a87f734 | ||
|
|
9930bd33b1 | ||
|
|
713539e491 | ||
|
|
327e87f735 | ||
|
|
b5426da2a7 | ||
|
|
302ab1da4f | ||
|
|
9cd2bd14ce | ||
|
|
53ee184bc5 | ||
|
|
e00d75bbb5 | ||
|
|
33f76b4895 | ||
|
|
c6b79e11d5 | ||
|
|
d941c91f24 | ||
|
|
054ad78d7b | ||
|
|
05b3fa56cb | ||
|
|
44d1a08db4 | ||
|
|
6a4644007d | ||
|
|
1c4d5c5271 | ||
|
|
dbb53da31d | ||
|
|
452afacd12 | ||
|
|
3b45a75f75 | ||
|
|
1a0f1c8015 | ||
|
|
b361866679 | ||
|
|
ceb4c4105f | ||
|
|
e9af18cdf1 | ||
|
|
5e6c40ad55 | ||
|
|
d6bd1ee215 | ||
|
|
acaf8a248e | ||
|
|
db9585cea9 | ||
|
|
65e3e8319b | ||
|
|
4ef3f8cad5 | ||
|
|
338bdc44cb | ||
|
|
73c00140df | ||
|
|
68a38c39f7 | ||
|
|
941291561a | ||
|
|
39ccdcf00d | ||
|
|
781224591f | ||
|
|
601fedb9b4 | ||
|
|
6812dfe7a6 |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
exclude_paths:
|
||||
- "plugin-repos/**"
|
||||
- "plugins/**"
|
||||
- "assets/**"
|
||||
- "test/**"
|
||||
- "scripts/debug/**"
|
||||
@@ -1,132 +0,0 @@
|
||||
# Cursor Helper Files for LEDMatrix Plugin Development
|
||||
|
||||
This directory contains Cursor-specific helper files to assist with plugin development in the LEDMatrix project.
|
||||
|
||||
## Files Overview
|
||||
|
||||
### `.cursorrules`
|
||||
Comprehensive rules file that Cursor uses to understand plugin development patterns, best practices, and workflows. This file is automatically loaded by Cursor and helps guide AI-assisted development.
|
||||
|
||||
### `plugins_guide.md`
|
||||
Detailed guide covering:
|
||||
- Plugin system overview
|
||||
- Creating new plugins
|
||||
- Running plugins (emulator and hardware)
|
||||
- Loading and configuring plugins
|
||||
- Development workflow
|
||||
- Testing strategies
|
||||
- Troubleshooting
|
||||
|
||||
### `plugin_templates/`
|
||||
Template files for quick plugin creation:
|
||||
- `manifest.json.template` - Plugin metadata template
|
||||
- `manager.py.template` - Plugin class template
|
||||
- `config_schema.json.template` - Configuration schema template
|
||||
- `README.md.template` - Plugin documentation template
|
||||
- `requirements.txt.template` - Dependencies template
|
||||
- `QUICK_START.md` - Quick start guide for using templates
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Creating a New Plugin
|
||||
|
||||
1. **Using templates** (recommended):
|
||||
```bash
|
||||
# See QUICK_START.md in plugin_templates/
|
||||
cd plugins
|
||||
mkdir my-plugin
|
||||
cd my-plugin
|
||||
cp ../../.cursor/plugin_templates/*.template .
|
||||
# Edit files, replacing PLUGIN_ID and other placeholders
|
||||
```
|
||||
|
||||
2. **Using dev_plugin_setup.sh**:
|
||||
```bash
|
||||
# Link from GitHub
|
||||
./dev_plugin_setup.sh link-github my-plugin
|
||||
|
||||
# Link local repo
|
||||
./dev_plugin_setup.sh link my-plugin /path/to/repo
|
||||
```
|
||||
|
||||
### Running Plugins
|
||||
|
||||
```bash
|
||||
# Emulator (development)
|
||||
python run.py --emulator
|
||||
|
||||
# Hardware (production)
|
||||
python run.py
|
||||
|
||||
# As service
|
||||
sudo systemctl start ledmatrix
|
||||
```
|
||||
|
||||
### Managing Plugins
|
||||
|
||||
```bash
|
||||
# List plugins
|
||||
./dev_plugin_setup.sh list
|
||||
|
||||
# Check status
|
||||
./dev_plugin_setup.sh status
|
||||
|
||||
# Update plugin(s)
|
||||
./dev_plugin_setup.sh update [plugin-name]
|
||||
|
||||
# Unlink plugin
|
||||
./dev_plugin_setup.sh unlink <plugin-name>
|
||||
```
|
||||
|
||||
## Using These Files with Cursor
|
||||
|
||||
### `.cursorrules`
|
||||
Cursor automatically reads this file to understand:
|
||||
- Plugin structure and requirements
|
||||
- Development workflows
|
||||
- Best practices
|
||||
- Common patterns
|
||||
- API reference
|
||||
|
||||
When asking Cursor to help with plugins, it will use this context to provide better assistance.
|
||||
|
||||
### Plugin Templates
|
||||
Use templates when creating new plugins:
|
||||
1. Copy templates from `.cursor/plugin_templates/`
|
||||
2. Replace placeholders (PLUGIN_ID, PluginClassName, etc.)
|
||||
3. Customize for your plugin's needs
|
||||
4. Follow the guide in `plugins_guide.md`
|
||||
|
||||
### Documentation
|
||||
Refer to `plugins_guide.md` for:
|
||||
- Detailed explanations
|
||||
- Troubleshooting steps
|
||||
- Best practices
|
||||
- Examples and patterns
|
||||
|
||||
## Plugin Development Workflow
|
||||
|
||||
1. **Plan**: Determine plugin functionality and requirements
|
||||
2. **Create**: Use templates or dev_plugin_setup.sh to create plugin structure
|
||||
3. **Develop**: Implement plugin logic following BasePlugin interface
|
||||
4. **Test**: Test with emulator first, then on hardware
|
||||
5. **Configure**: Add plugin config to config/config.json
|
||||
6. **Iterate**: Refine based on testing and feedback
|
||||
|
||||
## Resources
|
||||
|
||||
- **Plugin System**: `src/plugin_system/`
|
||||
- **Base Plugin**: `src/plugin_system/base_plugin.py`
|
||||
- **Plugin Manager**: `src/plugin_system/plugin_manager.py`
|
||||
- **Example Plugins**: `plugins/hockey-scoreboard/`, `plugins/football-scoreboard/`
|
||||
- **Architecture Docs**: `docs/PLUGIN_ARCHITECTURE_SPEC.md`
|
||||
- **Development Setup**: `dev_plugin_setup.sh`
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. Check `plugins_guide.md` for detailed documentation
|
||||
2. Review `.cursorrules` for development patterns
|
||||
3. Look at existing plugins for examples
|
||||
4. Check logs for error messages
|
||||
5. Review plugin system code in `src/plugin_system/`
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
# Quick Start: Creating a New Plugin
|
||||
|
||||
This guide will help you create a new plugin using the templates in `.cursor/plugin_templates/`.
|
||||
|
||||
## Step 1: Create Plugin Directory
|
||||
|
||||
```bash
|
||||
cd /path/to/LEDMatrix
|
||||
mkdir -p plugins/my-plugin
|
||||
cd plugins/my-plugin
|
||||
```
|
||||
|
||||
## Step 2: Copy Templates
|
||||
|
||||
```bash
|
||||
# Copy all template files
|
||||
cp ../../.cursor/plugin_templates/manifest.json.template ./manifest.json
|
||||
cp ../../.cursor/plugin_templates/manager.py.template ./manager.py
|
||||
cp ../../.cursor/plugin_templates/config_schema.json.template ./config_schema.json
|
||||
cp ../../.cursor/plugin_templates/README.md.template ./README.md
|
||||
cp ../../.cursor/plugin_templates/requirements.txt.template ./requirements.txt
|
||||
```
|
||||
|
||||
## Step 3: Customize Files
|
||||
|
||||
### manifest.json
|
||||
|
||||
Replace placeholders:
|
||||
- `PLUGIN_ID` → `my-plugin` (lowercase, use hyphens)
|
||||
- `Plugin Name` → Your plugin's display name
|
||||
- `PluginClassName` → `MyPlugin` (PascalCase)
|
||||
- Update description, author, homepage, etc.
|
||||
|
||||
### manager.py
|
||||
|
||||
Replace placeholders:
|
||||
- `PluginClassName` → `MyPlugin` (must match manifest)
|
||||
- Implement `_fetch_data()` method
|
||||
- Implement `_render_content()` method
|
||||
- Add any custom validation in `validate_config()`
|
||||
|
||||
### config_schema.json
|
||||
|
||||
Customize:
|
||||
- Update description
|
||||
- Add/remove configuration properties
|
||||
- Set default values
|
||||
- Add validation rules
|
||||
|
||||
### README.md
|
||||
|
||||
Replace placeholders:
|
||||
- `PLUGIN_ID` → `my-plugin`
|
||||
- `Plugin Name` → Your plugin's name
|
||||
- Fill in features, installation, configuration sections
|
||||
|
||||
### requirements.txt
|
||||
|
||||
Add your plugin's dependencies:
|
||||
```txt
|
||||
requests>=2.28.0
|
||||
pillow>=9.0.0
|
||||
```
|
||||
|
||||
## Step 4: Enable Plugin
|
||||
|
||||
Edit `config/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"my-plugin": {
|
||||
"enabled": true,
|
||||
"display_duration": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Test Plugin
|
||||
|
||||
### Test with Emulator
|
||||
|
||||
```bash
|
||||
cd /path/to/LEDMatrix
|
||||
python run.py --emulator
|
||||
```
|
||||
|
||||
### Check Plugin Loading
|
||||
|
||||
Look for logs like:
|
||||
```
|
||||
[INFO] Discovered 1 plugin(s)
|
||||
[INFO] Loaded plugin: my-plugin v1.0.0
|
||||
[INFO] Added plugin mode: my-plugin
|
||||
```
|
||||
|
||||
### Test Plugin Display
|
||||
|
||||
The plugin should appear in the display rotation. Check logs for any errors.
|
||||
|
||||
## Step 6: Develop and Iterate
|
||||
|
||||
1. Edit `manager.py` to implement your plugin logic
|
||||
2. Test with emulator: `python run.py --emulator`
|
||||
3. Check logs for errors
|
||||
4. Iterate until working correctly
|
||||
|
||||
## Step 7: Test on Hardware (Optional)
|
||||
|
||||
When ready, test on Raspberry Pi:
|
||||
|
||||
```bash
|
||||
# Deploy to Pi
|
||||
rsync -avz plugins/my-plugin/ pi@raspberrypi:/path/to/LEDMatrix/plugins/my-plugin/
|
||||
|
||||
# Or if using git
|
||||
ssh pi@raspberrypi "cd /path/to/LEDMatrix/plugins/my-plugin && git pull"
|
||||
|
||||
# Restart service
|
||||
ssh pi@raspberrypi "sudo systemctl restart ledmatrix"
|
||||
```
|
||||
|
||||
## Common Customizations
|
||||
|
||||
### Adding API Integration
|
||||
|
||||
1. Add API key to `config_schema.json`:
|
||||
```json
|
||||
{
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"description": "API key for service"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Implement API call in `_fetch_data()`:
|
||||
```python
|
||||
import requests
|
||||
|
||||
def _fetch_data(self):
|
||||
response = requests.get(
|
||||
"https://api.example.com/data",
|
||||
headers={"Authorization": f"Bearer {self.api_key}"}
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
3. Store API key in `config/config_secrets.json`:
|
||||
```json
|
||||
{
|
||||
"my-plugin": {
|
||||
"api_key": "your-secret-key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Image Rendering
|
||||
|
||||
```python
|
||||
def _render_content(self):
|
||||
# Load and render image
|
||||
image = Image.open("assets/logo.png")
|
||||
self.display_manager.draw_image(image, x=0, y=0)
|
||||
|
||||
# Draw text overlay
|
||||
self.display_manager.draw_text(
|
||||
"Text",
|
||||
x=10, y=20,
|
||||
color=(255, 255, 255)
|
||||
)
|
||||
```
|
||||
|
||||
### Adding Live Priority
|
||||
|
||||
1. Enable in config:
|
||||
```json
|
||||
{
|
||||
"my-plugin": {
|
||||
"live_priority": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Implement `has_live_content()`:
|
||||
```python
|
||||
def has_live_content(self) -> bool:
|
||||
return self.data and self.data.get("is_live", False)
|
||||
```
|
||||
|
||||
3. Override `get_live_modes()` if needed:
|
||||
```python
|
||||
def get_live_modes(self) -> list:
|
||||
return ["my_plugin_live_mode"]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
- Check `manifest.json` syntax (must be valid JSON)
|
||||
- Verify `entry_point` file exists
|
||||
- Ensure `class_name` matches class name in manager.py
|
||||
- Check for import errors in logs
|
||||
|
||||
### Configuration Errors
|
||||
|
||||
- Validate config against `config_schema.json`
|
||||
- Check required fields are present
|
||||
- Verify data types match schema
|
||||
|
||||
### Display Issues
|
||||
|
||||
- Check display dimensions: `display_manager.width`, `display_manager.height`
|
||||
- Verify coordinates are within bounds
|
||||
- Ensure `update_display()` is called
|
||||
- Test with emulator first
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review existing plugins for patterns:
|
||||
- `plugins/hockey-scoreboard/` - Sports scoreboard example
|
||||
- `plugins/ledmatrix-music/` - Real-time data example
|
||||
- `plugins/ledmatrix-stocks/` - Data display example
|
||||
|
||||
- Read full documentation:
|
||||
- `.cursor/plugins_guide.md` - Comprehensive guide
|
||||
- `docs/PLUGIN_ARCHITECTURE_SPEC.md` - Architecture details
|
||||
- `.cursorrules` - Development rules
|
||||
|
||||
- Check plugin system code:
|
||||
- `src/plugin_system/base_plugin.py` - Base class
|
||||
- `src/plugin_system/plugin_manager.py` - Plugin manager
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
# Plugin Name
|
||||
|
||||
Brief description of what this plugin does.
|
||||
|
||||
## Features
|
||||
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
- Feature 3
|
||||
|
||||
## Installation
|
||||
|
||||
1. Link the plugin to your LEDMatrix installation:
|
||||
|
||||
```bash
|
||||
cd /path/to/LEDMatrix
|
||||
./scripts/dev/dev_plugin_setup.sh link-github PLUGIN_ID
|
||||
```
|
||||
|
||||
Or for local development:
|
||||
|
||||
```bash
|
||||
./scripts/dev/dev_plugin_setup.sh link PLUGIN_ID /path/to/plugin/repo
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r plugins/PLUGIN_ID/requirements.txt
|
||||
```
|
||||
|
||||
3. Configure the plugin in `config/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"PLUGIN_ID": {
|
||||
"enabled": true,
|
||||
"display_duration": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** API keys and other sensitive credentials must be stored in `config/config_secrets.json`, not in `config/config.json`.
|
||||
|
||||
4. Store API keys in `config/config_secrets.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"PLUGIN_ID": {
|
||||
"api_key": "your-secret-api-key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required Settings
|
||||
|
||||
- `enabled` (boolean): Enable or disable the plugin
|
||||
- `api_key` (string): API key for external service (if required)
|
||||
|
||||
### Optional Settings
|
||||
|
||||
- `display_duration` (number): How long to display this plugin (default: 15 seconds)
|
||||
- `refresh_interval` (integer): How often to refresh data in seconds (default: 60)
|
||||
- `live_priority` (boolean): Enable live priority takeover (default: false)
|
||||
|
||||
## Display Modes
|
||||
|
||||
This plugin provides the following display modes:
|
||||
|
||||
- `PLUGIN_ID`: Main display mode
|
||||
|
||||
## API Requirements
|
||||
|
||||
This plugin requires:
|
||||
|
||||
- **API Name**: Description of API requirements
|
||||
- URL: https://api.example.com
|
||||
- Rate Limit: X requests per minute
|
||||
- Authentication: API key required
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cd plugins/PLUGIN_ID
|
||||
python test_PLUGIN_ID.py
|
||||
```
|
||||
|
||||
### Testing with Emulator
|
||||
|
||||
```bash
|
||||
cd /path/to/LEDMatrix
|
||||
python run.py --emulator
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable debug logging in `config/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"logging": {
|
||||
"level": "DEBUG"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Check logs:
|
||||
|
||||
```bash
|
||||
# On Raspberry Pi (if running as service)
|
||||
journalctl -u ledmatrix -f
|
||||
|
||||
# Direct execution
|
||||
python run.py
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
1. Check that `manifest.json` exists and is valid
|
||||
2. Verify `entry_point` file exists
|
||||
3. Check that `class_name` matches the class in manager.py
|
||||
4. Review logs for import errors
|
||||
|
||||
### Configuration Errors
|
||||
|
||||
1. Validate config against `config_schema.json`
|
||||
2. Check required fields are present
|
||||
3. Verify data types match schema
|
||||
|
||||
### API Errors
|
||||
|
||||
1. Verify API key is correct
|
||||
2. Check API rate limits
|
||||
3. Review network connectivity
|
||||
4. Check API service status
|
||||
|
||||
## License
|
||||
|
||||
[License information]
|
||||
|
||||
## Author
|
||||
|
||||
Your Name
|
||||
|
||||
## Links
|
||||
|
||||
- GitHub: https://github.com/username/ledmatrix-PLUGIN_ID
|
||||
- Documentation: [Link to docs]
|
||||
- Issues: https://github.com/username/ledmatrix-PLUGIN_ID/issues
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "Plugin Configuration Schema",
|
||||
"description": "Configuration schema for Plugin Name",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable or disable this plugin"
|
||||
},
|
||||
"display_duration": {
|
||||
"type": "number",
|
||||
"default": 15,
|
||||
"minimum": 1,
|
||||
"maximum": 300,
|
||||
"description": "How long to display this plugin in seconds"
|
||||
},
|
||||
"live_priority": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable live priority takeover when plugin has live content"
|
||||
},
|
||||
"refresh_interval": {
|
||||
"type": "integer",
|
||||
"default": 60,
|
||||
"minimum": 1,
|
||||
"description": "How often to refresh data in seconds"
|
||||
},
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"description": "API key for external service (store in config_secrets.json)",
|
||||
"default": ""
|
||||
},
|
||||
"custom_setting": {
|
||||
"type": "string",
|
||||
"description": "Example custom setting - replace with your plugin's settings",
|
||||
"default": "default_value"
|
||||
}
|
||||
},
|
||||
"required": ["enabled"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
"""
|
||||
Plugin Name
|
||||
|
||||
Brief description of what this plugin does.
|
||||
|
||||
API Version: 1.0.0
|
||||
"""
|
||||
|
||||
from src.plugin_system.base_plugin import BasePlugin
|
||||
from PIL import Image
|
||||
from typing import Dict, Any, Optional
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
class PluginClassName(BasePlugin):
|
||||
"""
|
||||
Plugin class that inherits from BasePlugin.
|
||||
|
||||
This plugin demonstrates the basic structure and common patterns
|
||||
for LEDMatrix plugins.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugin_id: str,
|
||||
config: Dict[str, Any],
|
||||
display_manager,
|
||||
cache_manager,
|
||||
plugin_manager,
|
||||
):
|
||||
"""Initialize the plugin."""
|
||||
super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
|
||||
|
||||
# Initialize plugin-specific data
|
||||
self.data = None
|
||||
self.last_update_time = None
|
||||
|
||||
# Load configuration values
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.refresh_interval = config.get("refresh_interval", 60)
|
||||
|
||||
self.logger.info(f"Plugin {plugin_id} initialized")
|
||||
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Fetch/update data for this plugin.
|
||||
|
||||
This method is called periodically based on update_interval
|
||||
specified in the manifest. Use cache_manager to avoid
|
||||
excessive API calls.
|
||||
"""
|
||||
cache_key = f"{self.plugin_id}_data"
|
||||
|
||||
# Check cache first
|
||||
cached = self.cache_manager.get(cache_key, max_age=self.refresh_interval)
|
||||
if cached:
|
||||
self.data = cached
|
||||
self.logger.debug("Using cached data")
|
||||
return
|
||||
|
||||
try:
|
||||
# Fetch new data
|
||||
self.data = self._fetch_data()
|
||||
|
||||
# Cache the data
|
||||
self.cache_manager.set(cache_key, self.data, ttl=self.refresh_interval)
|
||||
self.last_update_time = time.time()
|
||||
|
||||
self.logger.info("Data updated successfully")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update data: {e}")
|
||||
# Use cached data if available, even if expired
|
||||
# Use a very large max_age (1 year) to effectively bypass expiration for fallback
|
||||
expired_cached = self.cache_manager.get(cache_key, max_age=31536000)
|
||||
if expired_cached:
|
||||
self.data = expired_cached
|
||||
self.logger.warning("Using expired cache due to update failure")
|
||||
|
||||
def display(self, force_clear: bool = False) -> None:
|
||||
"""
|
||||
Render this plugin's display.
|
||||
|
||||
Args:
|
||||
force_clear: If True, clear display before rendering
|
||||
"""
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
|
||||
# Check if we have data to display
|
||||
if not self.data:
|
||||
self._display_error("No data available")
|
||||
return
|
||||
|
||||
try:
|
||||
# Render plugin content
|
||||
self._render_content()
|
||||
|
||||
# Update the display
|
||||
self.display_manager.update_display()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Display error: {e}")
|
||||
self._display_error("Display error")
|
||||
|
||||
def _fetch_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch data from external source.
|
||||
|
||||
Returns:
|
||||
Dictionary containing fetched data
|
||||
"""
|
||||
# TODO: Implement data fetching logic
|
||||
# Example:
|
||||
# import requests
|
||||
# response = requests.get("https://api.example.com/data",
|
||||
# headers={"Authorization": f"Bearer {self.api_key}"})
|
||||
# return response.json()
|
||||
|
||||
# Placeholder
|
||||
return {
|
||||
"message": "Hello, World!",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
def _render_content(self) -> None:
|
||||
"""Render the plugin content on the display."""
|
||||
# Get display dimensions
|
||||
width = self.display_manager.width
|
||||
height = self.display_manager.height
|
||||
|
||||
# Example: Draw text
|
||||
text = self.data.get("message", "No data")
|
||||
x = 5
|
||||
y = height // 2
|
||||
|
||||
self.display_manager.draw_text(
|
||||
text,
|
||||
x=x,
|
||||
y=y,
|
||||
color=(255, 255, 255) # White
|
||||
)
|
||||
|
||||
# Example: Draw image
|
||||
# if hasattr(self, 'logo_image'):
|
||||
# self.display_manager.draw_image(
|
||||
# self.logo_image,
|
||||
# x=0,
|
||||
# y=0
|
||||
# )
|
||||
|
||||
def _display_error(self, message: str) -> None:
|
||||
"""Display an error message."""
|
||||
self.display_manager.clear()
|
||||
width = self.display_manager.width
|
||||
height = self.display_manager.height
|
||||
|
||||
self.display_manager.draw_text(
|
||||
message,
|
||||
x=5,
|
||||
y=height // 2,
|
||||
color=(255, 0, 0) # Red
|
||||
)
|
||||
self.display_manager.update_display()
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
"""
|
||||
Validate plugin configuration.
|
||||
|
||||
Returns:
|
||||
True if config is valid, False otherwise
|
||||
"""
|
||||
# Call parent validation first
|
||||
if not super().validate_config():
|
||||
return False
|
||||
|
||||
# Add custom validation
|
||||
# Example: Check for required API key
|
||||
# if self.config.get("require_api_key", True):
|
||||
# if not self.api_key:
|
||||
# self.logger.error("API key is required but not provided")
|
||||
# return False
|
||||
|
||||
return True
|
||||
|
||||
def has_live_content(self) -> bool:
|
||||
"""
|
||||
Check if plugin has live content to display.
|
||||
|
||||
Override this method to enable live priority features.
|
||||
|
||||
Returns:
|
||||
True if plugin has live content, False otherwise
|
||||
"""
|
||||
# Example: Check if there's live data
|
||||
# return self.data and self.data.get("is_live", False)
|
||||
return False
|
||||
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Return plugin info for display in web UI.
|
||||
|
||||
Returns:
|
||||
Dictionary with plugin information
|
||||
"""
|
||||
info = super().get_info()
|
||||
|
||||
# Add plugin-specific info
|
||||
info.update({
|
||||
"data_available": self.data is not None,
|
||||
"last_update": self.last_update_time,
|
||||
# Add more info as needed
|
||||
})
|
||||
|
||||
return info
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Cleanup resources when plugin is unloaded."""
|
||||
# Clean up any resources (threads, connections, etc.)
|
||||
# Example:
|
||||
# if hasattr(self, 'api_client'):
|
||||
# self.api_client.close()
|
||||
|
||||
super().cleanup()
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"id": "PLUGIN_ID",
|
||||
"name": "Plugin Name",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"description": "Brief description of what this plugin does",
|
||||
"homepage": "https://github.com/username/ledmatrix-PLUGIN_ID",
|
||||
"entry_point": "manager.py",
|
||||
"class_name": "PluginClassName",
|
||||
"category": "custom",
|
||||
"tags": ["custom", "example"],
|
||||
"icon": "fas fa-icon-name",
|
||||
"compatible_versions": [">=2.0.0"],
|
||||
"min_ledmatrix_version": "2.0.0",
|
||||
"max_ledmatrix_version": "3.0.0",
|
||||
"requires": {
|
||||
"python": ">=3.9",
|
||||
"display_size": {
|
||||
"min_width": 64,
|
||||
"min_height": 32
|
||||
}
|
||||
},
|
||||
"config_schema": "config_schema.json",
|
||||
"assets": {
|
||||
"logos": "Optional: Description of asset requirements"
|
||||
},
|
||||
"update_interval": 60,
|
||||
"default_duration": 15,
|
||||
"display_modes": [
|
||||
"PLUGIN_ID"
|
||||
],
|
||||
"api_requirements": [
|
||||
{
|
||||
"name": "API Name",
|
||||
"required": false,
|
||||
"description": "Description of API requirements",
|
||||
"url": "https://api.example.com",
|
||||
"rate_limit": "Rate limit information"
|
||||
}
|
||||
],
|
||||
"download_url_template": "https://github.com/username/ledmatrix-PLUGIN_ID/archive/refs/tags/v{version}.zip",
|
||||
"versions": [
|
||||
{
|
||||
"released": "2025-01-01",
|
||||
"version": "1.0.0",
|
||||
"ledmatrix_min_version": "2.0.0"
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-01-01",
|
||||
"stars": 0,
|
||||
"downloads": 0,
|
||||
"verified": false,
|
||||
"screenshot": ""
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Plugin Dependencies
|
||||
# Add your plugin's Python dependencies here
|
||||
|
||||
# Example dependencies (uncomment and modify as needed):
|
||||
# requests>=2.28.0
|
||||
# pillow>=9.0.0
|
||||
# python-dateutil>=2.8.0
|
||||
|
||||
# Note: Core LEDMatrix dependencies are already available:
|
||||
# - PIL/Pillow (for image handling)
|
||||
# - Core plugin system classes
|
||||
# - Display manager, cache manager, config manager
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
Test file for Plugin Name plugin.
|
||||
|
||||
This file provides example unit tests for your plugin.
|
||||
Run tests with: python -m pytest test_manager.py
|
||||
Or: python test_manager.py
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.plugin_system.testing import PluginTestCase
|
||||
from manager import PluginClassName
|
||||
|
||||
|
||||
class TestPluginClassName(PluginTestCase):
|
||||
"""Test cases for PluginClassName plugin."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
super().setUp()
|
||||
|
||||
# Update plugin_id to match the plugin being tested
|
||||
self.plugin_id = 'PLUGIN_ID'
|
||||
|
||||
# Create plugin instance
|
||||
self.plugin = self.create_plugin_instance(
|
||||
PluginClassName,
|
||||
plugin_id='PLUGIN_ID',
|
||||
config=self.get_mock_config()
|
||||
)
|
||||
|
||||
def test_plugin_initialization(self):
|
||||
"""Test that plugin initializes correctly."""
|
||||
self.assert_plugin_initialized(self.plugin)
|
||||
self.assertTrue(self.plugin.enabled)
|
||||
|
||||
def test_config_validation(self):
|
||||
"""Test configuration validation."""
|
||||
# Valid config should pass
|
||||
self.assertTrue(self.plugin.validate_config())
|
||||
|
||||
# Test with invalid config if applicable
|
||||
# invalid_config = self.get_mock_config(enabled='not-a-boolean')
|
||||
# invalid_plugin = self.create_plugin_instance(
|
||||
# PluginClassName,
|
||||
# config=invalid_config
|
||||
# )
|
||||
# self.assertFalse(invalid_plugin.validate_config())
|
||||
|
||||
def test_update_method(self):
|
||||
"""Test the update() method."""
|
||||
# Reset mocks
|
||||
self.cache_manager.reset()
|
||||
|
||||
# Call update
|
||||
self.plugin.update()
|
||||
|
||||
# Assertions
|
||||
# Example: Check that cache was used
|
||||
# self.assert_cache_get('PLUGIN_ID_data')
|
||||
|
||||
# Example: Check that data was fetched and cached
|
||||
# self.assert_cache_set('PLUGIN_ID_data')
|
||||
|
||||
def test_display_method(self):
|
||||
"""Test the display() method."""
|
||||
# Ensure plugin has data (call update first if needed)
|
||||
# self.plugin.update()
|
||||
|
||||
# Call display
|
||||
self.plugin.display(force_clear=True)
|
||||
|
||||
# Assertions
|
||||
self.assert_display_cleared()
|
||||
self.assert_display_updated()
|
||||
|
||||
# Example: Check that text was drawn
|
||||
# self.assert_text_drawn("Expected Text")
|
||||
|
||||
# Example: Check that image was drawn
|
||||
# self.assert_image_drawn()
|
||||
|
||||
def test_display_without_data(self):
|
||||
"""Test display() behavior when no data is available."""
|
||||
# Clear any cached data
|
||||
self.cache_manager.reset()
|
||||
|
||||
# Call display
|
||||
self.plugin.display()
|
||||
|
||||
# Should handle gracefully (no exceptions)
|
||||
# May show error message or fallback content
|
||||
self.assert_display_updated()
|
||||
|
||||
def test_get_display_duration(self):
|
||||
"""Test display duration configuration."""
|
||||
duration = self.plugin.get_display_duration()
|
||||
self.assertIsInstance(duration, (int, float))
|
||||
self.assertGreater(duration, 0)
|
||||
|
||||
# Test with custom duration
|
||||
custom_config = self.get_mock_config(display_duration=30.0)
|
||||
custom_plugin = self.create_plugin_instance(
|
||||
PluginClassName,
|
||||
config=custom_config
|
||||
)
|
||||
self.assertEqual(custom_plugin.get_display_duration(), 30.0)
|
||||
|
||||
def test_enable_disable(self):
|
||||
"""Test plugin enable/disable functionality."""
|
||||
self.assertTrue(self.plugin.enabled)
|
||||
|
||||
self.plugin.on_disable()
|
||||
self.assertFalse(self.plugin.enabled)
|
||||
|
||||
self.plugin.on_enable()
|
||||
self.assertTrue(self.plugin.enabled)
|
||||
|
||||
def test_config_change(self):
|
||||
"""Test configuration change handling."""
|
||||
new_config = self.get_mock_config(display_duration=20.0)
|
||||
self.plugin.on_config_change(new_config)
|
||||
|
||||
self.assertEqual(self.plugin.config.get('display_duration'), 20.0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -1,742 +0,0 @@
|
||||
# LEDMatrix Plugin Development Guide
|
||||
|
||||
This guide provides comprehensive instructions for creating, running, and loading plugins in the LEDMatrix project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Plugin System Overview](#plugin-system-overview)
|
||||
2. [Creating a New Plugin](#creating-a-new-plugin)
|
||||
3. [Running Plugins](#running-plugins)
|
||||
4. [Loading Plugins](#loading-plugins)
|
||||
5. [Plugin Development Workflow](#plugin-development-workflow)
|
||||
6. [Testing Plugins](#testing-plugins)
|
||||
7. [Troubleshooting](#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
|
||||
|
||||
1. **Discovery**: PluginManager scans `plugins/` for directories with `manifest.json`
|
||||
2. **Loading**: Plugin module is imported and class is instantiated
|
||||
3. **Configuration**: Plugin config is loaded from `config/config.json`
|
||||
4. **Validation**: `validate_config()` is called to verify configuration
|
||||
5. **Registration**: Plugin is added to available display modes
|
||||
6. **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
|
||||
|
||||
```bash
|
||||
# Link a plugin from GitHub (auto-detects URL)
|
||||
./dev_plugin_setup.sh link-github <plugin-name>
|
||||
|
||||
# Example: Link hockey-scoreboard plugin
|
||||
./dev_plugin_setup.sh link-github hockey-scoreboard
|
||||
|
||||
# With custom URL
|
||||
./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
|
||||
|
||||
```bash
|
||||
# Link a local plugin repository
|
||||
./dev_plugin_setup.sh link <plugin-name> <path-to-repo>
|
||||
|
||||
# Example: Link a local plugin
|
||||
./dev_plugin_setup.sh link my-plugin ../ledmatrix-my-plugin
|
||||
```
|
||||
|
||||
### Method 2: Manual Plugin Creation
|
||||
|
||||
1. **Create Plugin Directory**
|
||||
|
||||
```bash
|
||||
mkdir -p plugins/my-plugin
|
||||
cd plugins/my-plugin
|
||||
```
|
||||
|
||||
2. **Create manifest.json**
|
||||
|
||||
```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"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create manager.py**
|
||||
|
||||
```python
|
||||
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
|
||||
```
|
||||
|
||||
4. **Create config_schema.json**
|
||||
|
||||
```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"]
|
||||
}
|
||||
```
|
||||
|
||||
5. **Create requirements.txt** (if needed)
|
||||
|
||||
```
|
||||
requests>=2.28.0
|
||||
pillow>=9.0.0
|
||||
```
|
||||
|
||||
6. **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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```python
|
||||
# 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`:
|
||||
|
||||
```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`:
|
||||
|
||||
```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`:
|
||||
|
||||
```json
|
||||
{
|
||||
"my-plugin": {
|
||||
"api_key": "secret-api-key-here"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference secrets in main config:
|
||||
|
||||
```json
|
||||
{
|
||||
"my-plugin": {
|
||||
"enabled": true,
|
||||
"config_secrets": {
|
||||
"api_key": "my-plugin.api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
# Using dev_plugin_setup.sh
|
||||
./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:
|
||||
|
||||
```bash
|
||||
./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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
./dev_plugin_setup.sh link my-plugin ../ledmatrix-my-plugin
|
||||
```
|
||||
|
||||
### 2. Development Cycle
|
||||
|
||||
1. **Edit plugin code** in linked repository
|
||||
2. **Test with emulator**: `python run.py --emulator`
|
||||
3. **Check logs** for errors or warnings
|
||||
4. **Update configuration** in `config/config.json` if needed
|
||||
5. **Iterate** until plugin works correctly
|
||||
|
||||
### 3. Testing on Hardware
|
||||
|
||||
```bash
|
||||
# Deploy to Raspberry Pi
|
||||
rsync -avz plugins/my-plugin/ pi@raspberrypi:/path/to/LEDMatrix/plugins/my-plugin/
|
||||
|
||||
# Or if using git, pull on Pi
|
||||
ssh pi@raspberrypi "cd /path/to/LEDMatrix/plugins/my-plugin && git pull"
|
||||
|
||||
# Restart service
|
||||
ssh pi@raspberrypi "sudo systemctl restart ledmatrix"
|
||||
```
|
||||
|
||||
### 4. Updating Plugins
|
||||
|
||||
```bash
|
||||
# Update single plugin from git
|
||||
./dev_plugin_setup.sh update my-plugin
|
||||
|
||||
# Update all linked plugins
|
||||
./dev_plugin_setup.sh update
|
||||
```
|
||||
|
||||
### 5. Unlinking Plugins
|
||||
|
||||
```bash
|
||||
# Remove symlink (preserves repository)
|
||||
./dev_plugin_setup.sh unlink my-plugin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plugins
|
||||
|
||||
### Unit Testing
|
||||
|
||||
Create test files in plugin directory:
|
||||
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
cd plugins/my-plugin
|
||||
python -m pytest test_my_plugin.py
|
||||
# or
|
||||
python test_my_plugin.py
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
Test plugin with actual managers:
|
||||
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# Run with emulator
|
||||
python run.py --emulator
|
||||
|
||||
# Plugin should appear in display rotation
|
||||
# Check logs for plugin loading and execution
|
||||
```
|
||||
|
||||
### Hardware Testing
|
||||
|
||||
1. Deploy plugin to Raspberry Pi
|
||||
2. Enable in `config/config.json`
|
||||
3. Restart LEDMatrix service
|
||||
4. Observe LED matrix display
|
||||
5. Check logs: `journalctl -u ledmatrix -f`
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
**Symptoms**: Plugin doesn't appear in available modes, no logs about plugin
|
||||
|
||||
**Solutions**:
|
||||
1. Check plugin directory exists: `ls plugins/my-plugin/`
|
||||
2. Verify `manifest.json` exists and is valid JSON
|
||||
3. Check manifest has required fields: `id`, `entry_point`, `class_name`
|
||||
4. Verify entry_point file exists: `ls plugins/my-plugin/manager.py`
|
||||
5. Check class name matches: `grep "class.*Plugin" plugins/my-plugin/manager.py`
|
||||
6. Review logs for import errors
|
||||
|
||||
### Plugin Loading but Not Displaying
|
||||
|
||||
**Symptoms**: Plugin loads successfully but doesn't appear in rotation
|
||||
|
||||
**Solutions**:
|
||||
1. Check plugin is enabled: `config/config.json` has `"enabled": true`
|
||||
2. Verify display_modes in manifest match config
|
||||
3. Check plugin is in rotation schedule
|
||||
4. Review `display()` method for errors
|
||||
5. Check logs for runtime errors
|
||||
|
||||
### Configuration Errors
|
||||
|
||||
**Symptoms**: Plugin fails to load, validation errors in logs
|
||||
|
||||
**Solutions**:
|
||||
1. Validate config against `config_schema.json`
|
||||
2. Check required fields are present
|
||||
3. Verify data types match schema
|
||||
4. Check for typos in config keys
|
||||
5. Review `validate_config()` method
|
||||
|
||||
### Import Errors
|
||||
|
||||
**Symptoms**: ModuleNotFoundError or ImportError in logs
|
||||
|
||||
**Solutions**:
|
||||
1. Install plugin dependencies: `pip install -r plugins/my-plugin/requirements.txt`
|
||||
2. Check Python path includes plugin directory
|
||||
3. Verify relative imports are correct
|
||||
4. Check for circular import issues
|
||||
5. Ensure all dependencies are in requirements.txt
|
||||
|
||||
### Display Issues
|
||||
|
||||
**Symptoms**: Plugin renders incorrectly or not at all
|
||||
|
||||
**Solutions**:
|
||||
1. Check display dimensions: `display_manager.width`, `display_manager.height`
|
||||
2. Verify coordinates are within display bounds
|
||||
3. Check color values are valid (0-255)
|
||||
4. Ensure `update_display()` is called after rendering
|
||||
5. Test with emulator first to debug rendering
|
||||
|
||||
### Performance Issues
|
||||
|
||||
**Symptoms**: Slow display updates, high CPU usage
|
||||
|
||||
**Solutions**:
|
||||
1. Use `cache_manager` to avoid excessive API calls
|
||||
2. Implement background data fetching
|
||||
3. Optimize rendering code
|
||||
4. Consider using `high_performance_transitions`
|
||||
5. Profile plugin code to identify bottlenecks
|
||||
|
||||
### Git/Symlink Issues
|
||||
|
||||
**Symptoms**: Plugin changes not appearing, broken symlinks
|
||||
|
||||
**Solutions**:
|
||||
1. Check symlink: `ls -la plugins/my-plugin`
|
||||
2. Verify target exists: `readlink -f plugins/my-plugin`
|
||||
3. Update plugin: `./dev_plugin_setup.sh update my-plugin`
|
||||
4. Re-link plugin if needed: `./dev_plugin_setup.sh unlink my-plugin && ./dev_plugin_setup.sh link my-plugin <path>`
|
||||
5. 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.json` for 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 example
|
||||
- `plugins/football-scoreboard/` - Complex multi-league example
|
||||
- `plugins/ledmatrix-music/` - Real-time data example
|
||||
- **Development Setup**: `dev_plugin_setup.sh`
|
||||
- **Example Config**: `dev_plugins.json.example`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Link plugin from GitHub
|
||||
./dev_plugin_setup.sh link-github <name>
|
||||
|
||||
# Link local plugin
|
||||
./dev_plugin_setup.sh link <name> <path>
|
||||
|
||||
# List all plugins
|
||||
./dev_plugin_setup.sh list
|
||||
|
||||
# Check plugin status
|
||||
./dev_plugin_setup.sh status
|
||||
|
||||
# Update plugin(s)
|
||||
./dev_plugin_setup.sh update [name]
|
||||
|
||||
# Unlink plugin
|
||||
./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 identifier
|
||||
- `entry_point`: Python file (usually "manager.py")
|
||||
- `class_name`: Plugin class name
|
||||
- `display_modes`: Array of mode names
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
globs: *.py
|
||||
---
|
||||
|
||||
# Python Coding Standards
|
||||
|
||||
## Code Quality Principles
|
||||
- **Simplicity First**: Prefer clear, readable code over clever optimizations
|
||||
- **Explicit over Implicit**: Make intentions clear through naming and structure
|
||||
- **Fail Fast**: Validate inputs and handle errors early
|
||||
- **Documentation**: Use docstrings for classes and complex functions
|
||||
|
||||
## Naming Conventions
|
||||
- **Classes**: PascalCase (e.g., `NHLRecentManager`)
|
||||
- **Functions/Variables**: snake_case (e.g., `fetch_game_data`)
|
||||
- **Constants**: UPPER_SNAKE_CASE (e.g., `ESPN_NHL_SCOREBOARD_URL`)
|
||||
- **Private methods**: Leading underscore (e.g., `_fetch_data`)
|
||||
|
||||
## Error Handling
|
||||
- **Logging**: Use structured logging with context (e.g., `[NHL Recent]`)
|
||||
- **Exceptions**: Catch specific exceptions, not bare `except:`
|
||||
- **User-friendly messages**: Explain what went wrong and potential solutions
|
||||
- **Graceful degradation**: Continue operation when non-critical features fail
|
||||
|
||||
## Manager Pattern
|
||||
All sports managers should follow this structure:
|
||||
```python
|
||||
class BaseManager:
|
||||
def __init__(self, config, display_manager, cache_manager)
|
||||
def update(self) # Fetch and process data
|
||||
def display(self, force_clear=False) # Render to display
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
- **Type hints**: Use for function parameters and return values
|
||||
- **Configuration validation**: Check required fields on initialization
|
||||
- **Default values**: Provide sensible defaults in code, not config
|
||||
- **Environment awareness**: Handle different deployment contexts
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
globs: config/*.json,src/*.py
|
||||
---
|
||||
|
||||
# Configuration Management
|
||||
|
||||
## Configuration Structure
|
||||
- **Main config**: [config/config.json](mdc:config/config.json) - Primary configuration
|
||||
- **Secrets**: [config/config_secrets.json](mdc:config/config_secrets.json) - API keys and sensitive data
|
||||
- **Templates**: [config/config.template.json](mdc:config/config.template.json) - Default values
|
||||
|
||||
## Configuration Principles
|
||||
- **Validation**: Check required fields and data types on startup
|
||||
- **Defaults**: Provide sensible defaults in code, not just config
|
||||
- **Environment awareness**: Handle development vs production differences
|
||||
- **Security**: Never commit secrets to version control
|
||||
|
||||
## Manager Configuration Pattern
|
||||
```python
|
||||
def __init__(self, config, display_manager, cache_manager):
|
||||
self.mode_config = config.get("sport_scoreboard", {})
|
||||
self.favorite_teams = self.mode_config.get("favorite_teams", [])
|
||||
self.show_favorite_only = self.mode_config.get("show_favorite_teams_only", False)
|
||||
```
|
||||
|
||||
## Required Configuration Sections
|
||||
- **Display settings**: Update intervals, display durations
|
||||
- **API settings**: Timeouts, retry logic, rate limiting
|
||||
- **Background service**: Threading, caching, priority settings
|
||||
- **Team preferences**: Favorite teams, filtering options
|
||||
|
||||
## Configuration Validation
|
||||
- **Type checking**: Ensure numeric values are numbers, lists are lists
|
||||
- **Range validation**: Check that intervals are reasonable
|
||||
- **Dependency checking**: Verify required services are available
|
||||
- **Fallback values**: Provide defaults when config is missing or invalid
|
||||
|
||||
## Best Practices
|
||||
- **Documentation**: Comment complex configuration options
|
||||
- **Examples**: Provide working examples in templates
|
||||
- **Migration**: Handle configuration changes between versions
|
||||
- **Testing**: Validate configuration in test environments
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
globs: src/*.py
|
||||
---
|
||||
|
||||
# Error Handling and Logging
|
||||
|
||||
## Logging Standards
|
||||
- **Structured prefixes**: Use consistent tags like `[NHL Recent]`, `[NFL Live]`
|
||||
- **Context information**: Include relevant details (team names, game status, dates)
|
||||
- **Appropriate levels**:
|
||||
- `info`: Normal operations and status updates
|
||||
- `debug`: Detailed information for troubleshooting
|
||||
- `warning`: Non-critical issues that should be noted
|
||||
- `error`: Problems that need attention
|
||||
|
||||
## Error Handling Patterns
|
||||
```python
|
||||
try:
|
||||
data = self._fetch_data()
|
||||
if not data or 'events' not in data:
|
||||
self.logger.warning("[Manager] No events found in API response")
|
||||
return
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"[Manager] API error: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
## User-Friendly Messages
|
||||
- **Explain the situation**: "No games available during off-season"
|
||||
- **Provide context**: "NHL season typically runs October-June"
|
||||
- **Suggest solutions**: "Check back when season starts"
|
||||
- **Distinguish issues**: API problems vs no data vs filtering results
|
||||
|
||||
## Graceful Degradation
|
||||
- **Fallback content**: Show alternative games when favorites unavailable
|
||||
- **Cached data**: Use cached data when API fails
|
||||
- **Service continuity**: Continue operation when non-critical features fail
|
||||
- **Clear communication**: Explain what's happening to users
|
||||
|
||||
## Debugging Support
|
||||
- **Comprehensive logging**: Log API responses, filtering results, display updates
|
||||
- **State tracking**: Log current state and transitions
|
||||
- **Performance monitoring**: Track timing and resource usage
|
||||
- **Error context**: Include stack traces for debugging
|
||||
|
||||
## Off-Season Awareness
|
||||
- **Seasonal messaging**: Different messages for different times of year
|
||||
- **Helpful context**: Explain why no games are available
|
||||
- **Future planning**: Mention when season starts
|
||||
- **Realistic expectations**: Set appropriate expectations during off-season
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Git Workflow and Branching
|
||||
|
||||
## Branch Naming Conventions
|
||||
- **Features**: `feature/description-of-feature` (e.g., `feature/weather-forecast-improvements`)
|
||||
- **Bug fixes**: `fix/description-of-bug` (e.g., `fix/nhl-manager-improvements`)
|
||||
- **Hotfixes**: `hotfix/critical-issue-description`
|
||||
- **Refactoring**: `refactor/description-of-refactor`
|
||||
|
||||
## Commit Message Format
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types**: feat, fix, docs, style, refactor, test, chore
|
||||
**Examples**:
|
||||
- `feat(nhl): Add enhanced logging for data visibility`
|
||||
- `fix(display): Resolve rendering performance issue`
|
||||
- `docs(api): Update ESPN API integration guide`
|
||||
|
||||
## Pull Request Guidelines
|
||||
- **Self-review**: Review your own PR before requesting review
|
||||
- **Testing**: Test thoroughly on Raspberry Pi hardware
|
||||
- **Documentation**: Update relevant documentation if needed
|
||||
- **Clean history**: Squash commits if necessary for clean history
|
||||
|
||||
## Code Review Checklist
|
||||
- **Code Quality**: Proper error handling, logging, type hints
|
||||
- **Architecture**: Follows project patterns, doesn't break existing functionality
|
||||
- **Performance**: No negative impact on display performance
|
||||
- **Testing**: Works on Raspberry Pi hardware
|
||||
- **Documentation**: Comments added for complex logic
|
||||
|
||||
## Merge Strategies
|
||||
- **Squash and Merge**: Preferred for feature branches and bug fixes
|
||||
- **Merge Commit**: For complex features with multiple logical commits
|
||||
- **Rebase and Merge**: For simple, single-commit changes
|
||||
|
||||
## Best Practices
|
||||
- **Keep branches small and focused**
|
||||
- **Commit frequently with meaningful messages**
|
||||
- **Update branch regularly with main**
|
||||
- **Test changes incrementally**
|
||||
- **Delete feature branches after merge**
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
description: GitHub branching and pull request best practices for LEDMatrix project
|
||||
globs: ["**/*.py", "**/*.md", "**/*.json", "**/*.sh"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# GitHub Branching and Pull Request Guidelines
|
||||
|
||||
## Branch Naming Conventions
|
||||
|
||||
### Feature Branches
|
||||
- **Format**: `feature/description-of-feature`
|
||||
- **Examples**:
|
||||
- `feature/weather-forecast-improvements`
|
||||
- `feature/stock-api-integration`
|
||||
- `feature/nba-live-scores`
|
||||
|
||||
### Bug Fix Branches
|
||||
- **Format**: `fix/description-of-bug`
|
||||
- **Examples**:
|
||||
- `fix/leaderboard-scrolling-performance`
|
||||
- `fix/weather-api-timeout`
|
||||
- `fix/display-rendering-issue`
|
||||
|
||||
### Hotfix Branches
|
||||
- **Format**: `hotfix/critical-issue-description`
|
||||
- **Examples**:
|
||||
- `hotfix/display-crash-fix`
|
||||
- `hotfix/api-rate-limit-fix`
|
||||
|
||||
### Refactoring Branches
|
||||
- **Format**: `refactor/description-of-refactor`
|
||||
- **Examples**:
|
||||
- `refactor/sports-manager-architecture`
|
||||
- `refactor/cache-management-system`
|
||||
|
||||
## Branch Management Rules
|
||||
|
||||
### Main Branch Protection
|
||||
- **`main`** branch is protected and requires PR reviews
|
||||
- Never commit directly to `main`
|
||||
- All changes must go through pull requests
|
||||
|
||||
### Branch Lifecycle
|
||||
1. **Create** branch from `main` when starting work
|
||||
2. **Keep** branch up-to-date with `main` regularly
|
||||
3. **Test** thoroughly before creating PR
|
||||
4. **Delete** branch after successful merge
|
||||
|
||||
### Branch Updates
|
||||
```bash
|
||||
# Before starting new work
|
||||
git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Create new branch
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# Keep branch updated during development
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout feature/your-feature-name
|
||||
git merge main
|
||||
```
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### PR Title Format
|
||||
- **Feature**: `feat: Add weather forecast improvements`
|
||||
- **Fix**: `fix: Resolve leaderboard scrolling performance issue`
|
||||
- **Refactor**: `refactor: Improve sports manager architecture`
|
||||
- **Docs**: `docs: Update API integration guide`
|
||||
- **Test**: `test: Add unit tests for weather manager`
|
||||
|
||||
### PR Description Template
|
||||
```markdown
|
||||
## Description
|
||||
Brief description of changes and motivation.
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix (non-breaking change)
|
||||
- [ ] New feature (non-breaking change)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Documentation update
|
||||
- [ ] Performance improvement
|
||||
- [ ] Refactoring
|
||||
|
||||
## Testing
|
||||
- [ ] Tested on Raspberry Pi hardware
|
||||
- [ ] Verified display rendering works correctly
|
||||
- [ ] Checked API integration functionality
|
||||
- [ ] Tested error handling scenarios
|
||||
|
||||
## Screenshots/Videos
|
||||
(If applicable, add screenshots or videos of the changes)
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows project style guidelines
|
||||
- [ ] Self-review completed
|
||||
- [ ] Comments added for complex logic
|
||||
- [ ] No hardcoded values or API keys
|
||||
- [ ] Error handling implemented
|
||||
- [ ] Logging added where appropriate
|
||||
```
|
||||
|
||||
### PR Review Requirements
|
||||
|
||||
#### For Reviewers
|
||||
- **Code Quality**: Check for proper error handling, logging, and type hints
|
||||
- **Architecture**: Ensure changes follow project patterns and don't break existing functionality
|
||||
- **Performance**: Verify changes don't negatively impact display performance
|
||||
- **Testing**: Confirm changes work on Raspberry Pi hardware
|
||||
- **Documentation**: Check if documentation needs updates
|
||||
|
||||
#### For Authors
|
||||
- **Self-Review**: Review your own PR before requesting review
|
||||
- **Testing**: Test thoroughly on Pi hardware before submitting
|
||||
- **Documentation**: Update relevant documentation if needed
|
||||
- **Clean History**: Squash commits if necessary for clean history
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
### Format
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
### Types
|
||||
- **feat**: New feature
|
||||
- **fix**: Bug fix
|
||||
- **docs**: Documentation changes
|
||||
- **style**: Code style changes (formatting, etc.)
|
||||
- **refactor**: Code refactoring
|
||||
- **test**: Adding or updating tests
|
||||
- **chore**: Maintenance tasks
|
||||
|
||||
### Examples
|
||||
```
|
||||
feat(weather): Add hourly forecast display
|
||||
fix(nba): Resolve live score update issue
|
||||
docs(api): Update ESPN API integration guide
|
||||
refactor(sports): Improve base class architecture
|
||||
```
|
||||
|
||||
## Merge Strategies
|
||||
|
||||
### Squash and Merge (Preferred)
|
||||
- Use for feature branches and bug fixes
|
||||
- Creates clean, linear history
|
||||
- Combines all commits into single commit
|
||||
|
||||
### Merge Commit
|
||||
- Use for complex features with multiple logical commits
|
||||
- Preserves commit history
|
||||
- Use when commit messages are meaningful
|
||||
|
||||
### Rebase and Merge
|
||||
- Use sparingly for simple, single-commit changes
|
||||
- Creates linear history without merge commits
|
||||
|
||||
## Release Management
|
||||
|
||||
### Version Tags
|
||||
- Use semantic versioning: `v1.2.3`
|
||||
- Tag releases on `main` branch
|
||||
- Create release notes with technical details
|
||||
|
||||
### Release Branches
|
||||
- **Format**: `release/v1.2.3`
|
||||
- Use for release preparation
|
||||
- Include version bumps and final testing
|
||||
|
||||
## Emergency Procedures
|
||||
|
||||
### Hotfix Process
|
||||
1. Create `hotfix/` branch from `main`
|
||||
2. Make minimal fix
|
||||
3. Test thoroughly
|
||||
4. Create PR with expedited review
|
||||
5. Merge to `main` and tag release
|
||||
6. Cherry-pick to other branches if needed
|
||||
|
||||
### Rollback Process
|
||||
1. Identify last known good commit
|
||||
2. Create revert PR if possible
|
||||
3. Use `git revert` for clean rollback
|
||||
4. Tag rollback release
|
||||
5. Document issue and resolution
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Before Creating PR
|
||||
- [ ] Run all tests locally
|
||||
- [ ] Test on Raspberry Pi hardware
|
||||
- [ ] Check for linting errors
|
||||
- [ ] Update documentation if needed
|
||||
- [ ] Ensure commit messages are clear
|
||||
|
||||
### During Development
|
||||
- [ ] Keep branches small and focused
|
||||
- [ ] Commit frequently with meaningful messages
|
||||
- [ ] Update branch regularly with main
|
||||
- [ ] Test changes incrementally
|
||||
|
||||
### After PR Approval
|
||||
- [ ] Delete feature branch after merge
|
||||
- [ ] Update local main branch
|
||||
- [ ] Verify changes work in production
|
||||
- [ ] Update any related documentation
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# LEDMatrix Project Structure
|
||||
|
||||
## Core Architecture
|
||||
- **Main entry point**: [run.py](mdc:run.py) - Primary application launcher
|
||||
- **Configuration**: [config/config.json](mdc:config/config.json) - Main configuration file
|
||||
- **Display management**: [src/display_controller.py](mdc:src/display_controller.py) - Core display logic
|
||||
- **Web interface**: [web_interface_v2.py](mdc:web_interface_v2.py) - Modern web UI
|
||||
|
||||
## Source Code Organization
|
||||
- **Managers**: [src/](mdc:src/) - All sports/weather/stock managers
|
||||
- **Assets**: [assets/](mdc:assets/) - Logos, fonts, and static resources
|
||||
- **Tests**: [test/](mdc:test/) - Unit and integration tests
|
||||
- **Documentation**: [LEDMatrix.wiki/](mdc:LEDMatrix.wiki/) - Comprehensive guides
|
||||
|
||||
## Key Design Principles
|
||||
- **Single Responsibility**: Each manager handles one sport/domain
|
||||
- **Consistent Patterns**: All managers follow similar structure
|
||||
- **Configuration-Driven**: Behavior controlled via [config/config.json](mdc:config/config.json)
|
||||
- **Raspberry Pi Focus**: Optimized for Pi hardware, not Windows development
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Raspberry Pi Development Guidelines
|
||||
|
||||
## Hardware Constraints
|
||||
- **Pi-only execution**: Code must run on Raspberry Pi, not Windows development machine
|
||||
- **LED matrix library**: Uses [rpi-rgb-led-matrix-master/](mdc:rpi-rgb-led-matrix-master/) for hardware control
|
||||
- **Memory limitations**: Optimize for Pi's limited RAM
|
||||
- **Performance**: Consider Pi's CPU capabilities in design
|
||||
|
||||
## Development Workflow
|
||||
- **Local development**: Write and test code on Windows
|
||||
- **Pi deployment**: Deploy and test on actual Pi hardware
|
||||
- **SSH access**: Use SSH for Pi-based testing and debugging
|
||||
- **Service management**: Use systemd services for production deployment
|
||||
|
||||
## Testing Strategy
|
||||
- **Unit tests**: Test logic without hardware dependencies
|
||||
- **Integration tests**: Test with mock display managers
|
||||
- **Hardware tests**: Validate on actual Pi with LED matrix
|
||||
- **Performance tests**: Monitor memory and CPU usage
|
||||
|
||||
## Deployment Considerations
|
||||
- **Service files**: [ledmatrix.service](mdc:ledmatrix.service), [ledmatrix-web.service](mdc:ledmatrix-web.service)
|
||||
- **Installation scripts**: [first_time_install.sh](mdc:first_time_install.sh), [install_service.sh](mdc:install_service.sh)
|
||||
- **Dependencies**: [requirements.txt](mdc:requirements.txt) for Pi environment
|
||||
- **Permissions**: Handle file permissions for Pi user
|
||||
|
||||
## Performance Optimization
|
||||
- **Caching**: Use [src/cache_manager.py](mdc:src/cache_manager.py) for data persistence
|
||||
- **Background services**: Non-blocking data fetching
|
||||
- **Memory management**: Clean up resources regularly
|
||||
- **Display optimization**: Minimize unnecessary redraws
|
||||
|
||||
## Debugging on Pi
|
||||
- **Logging**: Comprehensive logging for remote debugging
|
||||
- **Error reporting**: Clear error messages for troubleshooting
|
||||
- **Status monitoring**: Health checks and status reporting
|
||||
- **Remote access**: Web interface for configuration and monitoring
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
globs: src/*_managers.py
|
||||
---
|
||||
|
||||
# Sports Manager Development
|
||||
|
||||
## Manager Architecture
|
||||
All sports managers inherit from base classes and follow consistent patterns:
|
||||
- **Base classes**: [src/nhl_managers.py](mdc:src/nhl_managers.py), [src/nfl_managers.py](mdc:src/nfl_managers.py)
|
||||
- **Common functionality**: Data fetching, caching, display rendering
|
||||
- **Configuration-driven**: Behavior controlled via config sections
|
||||
|
||||
## Required Methods
|
||||
```python
|
||||
def __init__(self, config, display_manager, cache_manager)
|
||||
def update(self) # Fetch fresh data
|
||||
def display(self, force_clear=False) # Render current data
|
||||
```
|
||||
|
||||
## Data Flow Pattern
|
||||
1. **Fetch**: Get data from API (with caching)
|
||||
2. **Process**: Extract relevant game information
|
||||
3. **Filter**: Apply favorite team preferences
|
||||
4. **Display**: Render to LED matrix
|
||||
|
||||
## Logging Standards
|
||||
- **Structured prefixes**: `[NHL Recent]`, `[NFL Live]`, etc.
|
||||
- **Context information**: Include team names, game status, dates
|
||||
- **Debug levels**: Use appropriate log levels (info, debug, warning, error)
|
||||
- **User-friendly messages**: Explain what's happening and why
|
||||
|
||||
## Error Handling
|
||||
- **API failures**: Log and continue with cached data if available
|
||||
- **No data scenarios**: Distinguish between API issues vs no games available
|
||||
- **Off-season awareness**: Provide helpful context during non-active periods
|
||||
- **Fallback behavior**: Show alternative content when preferred content unavailable
|
||||
|
||||
## Configuration Integration
|
||||
- **Required settings**: Validate on initialization
|
||||
- **Optional settings**: Provide sensible defaults
|
||||
- **Background service**: Use for non-blocking data fetching
|
||||
- **Caching strategy**: Implement intelligent cache management
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
globs: test/*.py,src/*.py
|
||||
---
|
||||
|
||||
# Testing Standards
|
||||
|
||||
## Test Organization
|
||||
- **Test directory**: [test/](mdc:test/) - All test files
|
||||
- **Unit tests**: Test individual components in isolation
|
||||
- **Integration tests**: Test component interactions
|
||||
- **Hardware tests**: Validate on Raspberry Pi with actual LED matrix
|
||||
|
||||
## Testing Principles
|
||||
- **Test behavior, not implementation**: Focus on what the code does, not how
|
||||
- **Mock external dependencies**: Use mocks for APIs, display managers, cache
|
||||
- **Test edge cases**: Empty data, API failures, configuration errors
|
||||
- **Pi-specific testing**: Validate hardware integration
|
||||
|
||||
## Test Structure
|
||||
```python
|
||||
def test_manager_initialization():
|
||||
"""Test that manager initializes with valid config"""
|
||||
config = {"sport_scoreboard": {"enabled": True}}
|
||||
manager = ManagerClass(config, mock_display, mock_cache)
|
||||
assert manager.enabled == True
|
||||
|
||||
def test_api_failure_handling():
|
||||
"""Test graceful handling of API failures"""
|
||||
# Test that system continues when API fails
|
||||
# Verify fallback to cached data
|
||||
# Check appropriate error logging
|
||||
```
|
||||
|
||||
## Mock Patterns
|
||||
- **Display Manager**: Mock for testing without hardware
|
||||
- **Cache Manager**: Mock for testing data persistence
|
||||
- **API responses**: Mock for consistent test data
|
||||
- **Configuration**: Use test-specific configs
|
||||
|
||||
## Test Categories
|
||||
- **Unit tests**: Individual manager methods
|
||||
- **Integration tests**: Manager interactions with services
|
||||
- **Configuration tests**: Validate config loading and validation
|
||||
- **Error handling tests**: API failures, invalid data, edge cases
|
||||
|
||||
## Testing Best Practices
|
||||
- **Descriptive names**: Test names should explain what they test
|
||||
- **Single responsibility**: Each test should verify one thing
|
||||
- **Independent tests**: Tests should not depend on each other
|
||||
- **Clean setup/teardown**: Reset state between tests
|
||||
- **Pi compatibility**: Ensure tests work in Pi environment
|
||||
@@ -1 +0,0 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
@@ -1,312 +0,0 @@
|
||||
# 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 `plugins/` directory.
|
||||
|
||||
## 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
|
||||
./dev_plugin_setup.sh link-github <plugin-name>
|
||||
|
||||
# Link local repository
|
||||
./dev_plugin_setup.sh link <plugin-name> <path-to-repo>
|
||||
```
|
||||
|
||||
**Option B: Manual Setup**
|
||||
1. Create directory in `plugins/<plugin-id>/`
|
||||
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:**
|
||||
- Use emulator: `python run.py --emulator` or `./run_emulator.sh`
|
||||
- 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 `plugins/<plugin-id>/`
|
||||
- Use shared assets from `assets/` directory when possible
|
||||
- Follow existing plugin patterns (see `plugins/hockey-scoreboard/` as reference)
|
||||
- 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` (not in main config)
|
||||
- Reference secrets via `config_secrets` key in main config
|
||||
- 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)`: Draw text
|
||||
- `draw_image(image, x, y)`: Draw PIL Image
|
||||
- `update_display()`: Update physical display
|
||||
- `width`, `height`: Display dimensions
|
||||
|
||||
### Cache Manager
|
||||
Located in: `src/cache_manager.py`
|
||||
|
||||
**Key Methods:**
|
||||
- `get(key, max_age=None)`: Get cached value
|
||||
- `set(key, value, ttl=None)`: Cache a value
|
||||
- `delete(key)`: Remove cached value
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -1,38 +1,84 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
about: Report a problem with LEDMatrix
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
<!--
|
||||
Before filing: please check existing issues to see if this is already
|
||||
reported. For security issues, see SECURITY.md and report privately.
|
||||
-->
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
## Describe the bug
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
## Steps to reproduce
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
## Expected behavior
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
<!-- What you expected to happen. -->
|
||||
|
||||
## Actual behavior
|
||||
|
||||
<!-- What actually happened. Include any error messages. -->
|
||||
|
||||
## Hardware
|
||||
|
||||
- **Raspberry Pi model**: <!-- e.g. Pi 3B+, Pi 4 8GB, Pi Zero 2W -->
|
||||
- **OS / kernel**: <!-- output of `cat /etc/os-release` and `uname -a` -->
|
||||
- **LED matrix panels**: <!-- e.g. 2x Adafruit 64x32, 1x Waveshare 96x48 -->
|
||||
- **HAT / Bonnet**: <!-- e.g. Adafruit RGB Matrix Bonnet, Electrodragon HAT -->
|
||||
- **PWM jumper mod soldered?**: <!-- yes / no -->
|
||||
- **Display chain**: <!-- chain_length × parallel, e.g. "2x1" -->
|
||||
|
||||
## LEDMatrix version
|
||||
|
||||
<!-- Run `git rev-parse HEAD` in the LEDMatrix directory, or paste the
|
||||
release tag if you installed from a release. -->
|
||||
|
||||
```
|
||||
git commit:
|
||||
```
|
||||
|
||||
## Plugin involved (if any)
|
||||
|
||||
- **Plugin id**:
|
||||
- **Plugin version** (from `manifest.json`):
|
||||
|
||||
## Configuration
|
||||
|
||||
<!-- Paste the relevant section from config/config.json. Redact any
|
||||
API keys before pasting. For display issues, the `display.hardware`
|
||||
block is most relevant. For plugin issues, paste that plugin's section. -->
|
||||
|
||||
```json
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
<!-- The first 50 lines of the relevant log are usually enough. Run:
|
||||
sudo journalctl -u ledmatrix -n 100 --no-pager
|
||||
or for the web service:
|
||||
sudo journalctl -u ledmatrix-web -n 100 --no-pager
|
||||
-->
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## Screenshots / video (optional)
|
||||
|
||||
<!-- A photo of the actual display, or a screenshot of the web UI,
|
||||
helps a lot for visual issues. -->
|
||||
|
||||
## Additional context
|
||||
|
||||
<!-- Anything else that might be relevant: when did this start happening,
|
||||
what's different about your setup, what have you already tried, etc. -->
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Pull Request
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- 1-3 sentences describing what this PR does and why. -->
|
||||
|
||||
## Type of change
|
||||
|
||||
<!-- Check all that apply. -->
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Documentation
|
||||
- [ ] Refactor (no functional change)
|
||||
- [ ] Build / CI
|
||||
- [ ] Plugin work (link to the plugin)
|
||||
|
||||
## Related issues
|
||||
|
||||
<!-- "Fixes #123" or "Refs #123". Use "Fixes" for bug PRs so the issue
|
||||
auto-closes when this merges. -->
|
||||
|
||||
## Test plan
|
||||
|
||||
<!-- How did you test this? Check all that apply. Add details for any
|
||||
checked box. -->
|
||||
|
||||
- [ ] Ran on a real Raspberry Pi with hardware
|
||||
- [ ] Ran in emulator mode (`EMULATOR=true python3 run.py`)
|
||||
- [ ] Ran the dev preview server (`scripts/dev_server.py`)
|
||||
- [ ] Ran the test suite (`pytest`)
|
||||
- [ ] Manually verified the affected code path in the web UI
|
||||
- [ ] N/A — documentation-only change
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] I updated `README.md` if user-facing behavior changed
|
||||
- [ ] I updated the relevant doc in `docs/` if developer behavior changed
|
||||
- [ ] I added/updated docstrings on new public functions
|
||||
- [ ] N/A — no docs needed
|
||||
|
||||
## Plugin compatibility
|
||||
|
||||
<!-- For changes to BasePlugin, the plugin loader, the web UI, or the
|
||||
config schema. -->
|
||||
|
||||
- [ ] No plugin breakage expected
|
||||
- [ ] Some plugins will need updates — listed below
|
||||
- [ ] N/A — change doesn't touch the plugin system
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My commits follow the message convention in `CONTRIBUTING.md`
|
||||
- [ ] I read `CONTRIBUTING.md` and `CODE_OF_CONDUCT.md`
|
||||
- [ ] I've not committed any secrets or hardcoded API keys
|
||||
- [ ] If this adds a new config key, the form in the web UI was
|
||||
verified (the form is generated from `config_schema.json`)
|
||||
|
||||
## Notes for reviewer
|
||||
|
||||
<!-- Anything reviewers should know — gotchas, things you weren't
|
||||
sure about, decisions you'd like a second opinion on. -->
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr *)'
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Release version check
|
||||
|
||||
# A release tag, the CHANGELOG, and src.__version__ must agree. They have not
|
||||
# always: v3.1.0 was tagged while src/__init__.py still said "1.0.0", which
|
||||
# silently exempted every device installed from that release from plugin
|
||||
# compatibility warnings. See docs/SPORTS_UNIFICATION.md (phase B4).
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
release:
|
||||
types: [published]
|
||||
# Pre-flight: run this against the tag you are about to create.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag to check (e.g. v3.2.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
version-matches-tag:
|
||||
name: Tag matches src.__version__
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
# No dependencies: the script reads src/__init__.py and CHANGELOG.md only.
|
||||
- name: Assert the tag, CHANGELOG and src.__version__ agree
|
||||
run: python scripts/check_release_version.py "${TAG}"
|
||||
env:
|
||||
TAG: ${{ inputs.tag || github.ref_name }}
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
# Both jobs only check out the repo and run pytest.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
plugin-safety:
|
||||
name: Plugin safety harness + unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt -r requirements-test.txt
|
||||
pip install RGBMatrixEmulator
|
||||
|
||||
- name: Run harness + visual rendering tests
|
||||
run: |
|
||||
pytest --no-cov \
|
||||
test/plugins/test_harness.py \
|
||||
test/plugins/test_visual_rendering.py \
|
||||
test/plugins/test_plugin_matrix.py
|
||||
|
||||
unit-tests:
|
||||
name: Core unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt -r requirements-test.txt
|
||||
pip install RGBMatrixEmulator
|
||||
|
||||
# Safety net for the shared sports/scroll/style infrastructure. These
|
||||
# suites existed but were not enrolled in CI, so a refactor of
|
||||
# src/base_classes or src/common could regress them silently. Enrolled
|
||||
# explicitly (not `pytest test/`) so known hardware-only suites don't
|
||||
# break CI; grow this list as more suites are made headless.
|
||||
- name: Run core unit suites
|
||||
run: |
|
||||
pytest --no-cov \
|
||||
test/test_skin_system.py \
|
||||
test/test_font_manager.py \
|
||||
test/test_data_sources.py \
|
||||
test/test_api_extractors.py \
|
||||
test/test_scroll_helper.py \
|
||||
test/test_scroll_helper_continuous.py \
|
||||
test/test_adaptive_layout.py \
|
||||
test/test_loader_compat_warning.py \
|
||||
test/test_sports_base_characterization.py \
|
||||
test/test_element_style.py \
|
||||
test/test_sports_core_promotions.py \
|
||||
test/test_sports_modes_promotions.py \
|
||||
test/test_sports_capabilities.py \
|
||||
test/test_sports_scroll.py \
|
||||
test/test_version_consistency.py \
|
||||
test/test_plugin_compatibility_gate.py \
|
||||
test/test_install_preserves_existing.py \
|
||||
test/test_core_owned_config_keys.py \
|
||||
test/test_async_plugin_updates.py \
|
||||
test/test_plugin_update_reservation.py \
|
||||
test/test_template_targets.py \
|
||||
test/test_widget_scripts.py \
|
||||
test/test_enum_option_labels.py \
|
||||
test/test_doc_links.py \
|
||||
test/web_interface/test_cache.py
|
||||
@@ -8,6 +8,7 @@ config/config_secrets.json
|
||||
config/config.json
|
||||
config/config.json.backup
|
||||
config/wifi_config.json
|
||||
config/uninstalled_plugins.json
|
||||
credentials.json
|
||||
token.pickle
|
||||
|
||||
@@ -47,3 +48,4 @@ config/backups/
|
||||
|
||||
# Starlark apps runtime storage (installed .star files and cached renders)
|
||||
/starlark-apps/
|
||||
skin_renders/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[submodule "rpi-rgb-led-matrix-master"]
|
||||
path = rpi-rgb-led-matrix-master
|
||||
url = https://github.com/hzeller/rpi-rgb-led-matrix.git
|
||||
branch = master
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
# Changelog
|
||||
|
||||
Notable changes to the LEDMatrix core. The version below is the value of
|
||||
`src.__version__`, which the plugin loader reports to compatibility checks and
|
||||
which plugin manifests reference via `ledmatrix_min_version`.
|
||||
|
||||
**Why this file exists:** the plugin monorepo bundles fallback copies of several
|
||||
core modules (see `docs/plugin-development/08-shared-sports-code.md` in
|
||||
[ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins)). A plugin
|
||||
may delete its bundled copy only when its manifest floors on the first core
|
||||
release that ships the module — which requires module additions to be recorded
|
||||
here, against a version number. When you add a module plugins will import via
|
||||
`src.*`, note it in the Unreleased section and bump `src/__init__.py` in the
|
||||
release that ships it.
|
||||
|
||||
**Use `ledmatrix_min_version` in manifests, not `ledmatrix_min`.** The loader
|
||||
accepts both, but the store flags the old spelling as deprecated
|
||||
(`store_manager.py`) and only the new one is in `schema/manifest_schema.json`.
|
||||
|
||||
## 3.2.0
|
||||
|
||||
**The first release shipping the unified sports library.** This is the version
|
||||
a sports plugin floors `ledmatrix_min_version` at before deleting its bundled
|
||||
copy of `sports.py`, `scroll_display.py`, `data_sources.py` or
|
||||
`base_odds_manager.py` — the sunset rule in
|
||||
`docs/plugin-development/08-shared-sports-code.md` keys on exactly this number.
|
||||
|
||||
Adoption is deliberately staged: the modules below ship here, plugins adopt them
|
||||
behind guarded imports, and only then do the bundled copies go away. Nothing in
|
||||
this release changes what an existing plugin loads.
|
||||
|
||||
**This is also the first release that *enforces* `ledmatrix_min_version`.**
|
||||
Before it, the floor was advisory — the loader logged a warning and continued,
|
||||
and the plugin store never compared the core version at all, so an update could
|
||||
deliver a plugin that could not run. From 3.2.0 the store refuses such an
|
||||
install. That matters for the sunset rule: a plugin may only delete its bundled
|
||||
fallback once the cores in the field actually enforce the floor, which means
|
||||
waiting for 3.2.0 to be widely installed rather than merely released. See
|
||||
`docs/SPORTS_UNIFICATION.md`, phase B6.
|
||||
|
||||
One deliberate exception: a core reporting a version below `2.0.0` is treated as
|
||||
*unknown* rather than old and is never blocked. The v3.1.0 release ships
|
||||
`__version__ = "1.0.0"` (the tag was cut before the string was bumped), and
|
||||
nearly every published manifest floors at `2.0.0` — so blocking on that number
|
||||
would lock those users out of the plugin store entirely.
|
||||
|
||||
### Added
|
||||
- `src/element_style.py` — per-element style resolver backing the
|
||||
`x-style-elements` config-schema extension. Already consumed (behind guarded
|
||||
imports with classic fallbacks) by the `of-the-day`, `ledmatrix-music`, and
|
||||
`football-scoreboard` plugins.
|
||||
- Core unit-test CI job enrolling the previously unenrolled suites (skin
|
||||
system, data sources, API extractors, scroll helper, adaptive layout, loader
|
||||
compatibility warning) plus new characterization tests for
|
||||
`src/base_classes/sports.py` ahead of the shared sports-code unification.
|
||||
- `src/base_classes/sports/` — `sports.py` is now a package (`core.py` +
|
||||
`modes.py`). The import path is unchanged: `from src.base_classes.sports
|
||||
import SportsCore` still works.
|
||||
- Nine methods promoted onto the sports base classes from the plugins'
|
||||
bundled copies, plus the override points `_favorite_key`,
|
||||
`_config_schema_path` and `_font_root` and the class attributes
|
||||
`FINAL_PERIOD` / `CLOCK_COUNTS_DOWN`. See `docs/SPORTS_UNIFICATION.md`.
|
||||
A plugin may start calling these once its manifest floors
|
||||
`ledmatrix_min_version` at the release that ships them.
|
||||
|
||||
- `src/base_classes/sports/capabilities/` — opt-in capabilities for the sports
|
||||
scoreboards, composed by inheritance rather than gated by config branches
|
||||
inside the base classes:
|
||||
- `CelebrationMixin` — the score/win takeover, merging the goal and score
|
||||
dialects behind the `score_phrase()` / `win_phrase()` hooks, the
|
||||
`COALESCE_SCORING_SEQUENCE` class attribute and the `_favorite_key` seam.
|
||||
Reads both the `celebrate_opponent_goals` and `celebrate_opponent_scores`
|
||||
config spellings. Sports that do not mix it in have none of this code in
|
||||
their MRO.
|
||||
- `RotationStrategy` + a name registry (`swrr`, `weighted`, `simple`,
|
||||
plus `register_rotation_strategy` for plugin-supplied orderings). Each
|
||||
built-in is verified against a verbatim transcription of the plugin
|
||||
implementation it replaces. An unknown name degrades to `simple`.
|
||||
|
||||
- `src/common/sports_scroll.py` — `SportsScrollDisplay` and
|
||||
`SportsScrollDisplayManager`, the shared scroll **orchestration** layer for
|
||||
the sports scoreboards, plus native support for
|
||||
`global_config['target_fps']` (the bundled plugin copies hardcode ~100 FPS
|
||||
via `scroll_delay` and never consult the global target). Content building
|
||||
(`prepare_scroll_content`, `_load_separator_icons`) is per-sport and stays an
|
||||
override point — see `docs/SPORTS_UNIFICATION.md` for where the line falls
|
||||
and why.
|
||||
|
||||
- `src/plugin_system/compatibility.py` — the single place that answers "can this
|
||||
plugin run on this core?", shared by the loader (advisory, at load time) and
|
||||
the store (blocking, at install/update time) so the two cannot drift. Reads
|
||||
every spelling published manifests use, including the deprecated
|
||||
`versions[].ledmatrix_min`. It does **not** yet evaluate `compatible_versions`,
|
||||
which is the schema-required field and can express upper bounds; closing that
|
||||
is tracked in `docs/SPORTS_UNIFICATION.md` before B6.
|
||||
- `scripts/check_release_version.py` and a `Release version check` workflow —
|
||||
assert that a tag, the newest CHANGELOG heading and `src.__version__` agree,
|
||||
on pushed `v*` tags and published releases. Runnable via `workflow_dispatch`
|
||||
to check a tag *before* creating it. Added because `v3.1.0` was tagged six
|
||||
weeks before `src/__init__.py` was bumped to match, which is why devices
|
||||
installed from that release report `1.0.0`.
|
||||
|
||||
### Changed
|
||||
- `src/__init__.py` bumped to **3.2.0** — the number the sunset rule keys on.
|
||||
- **The plugin store refuses an incompatible install.**
|
||||
`StoreManager.install_plugin` now checks the downloaded manifest's declared
|
||||
floor against `src.__version__` and refuses when the plugin needs a newer
|
||||
core. The check sits in `install_plugin` because `_reinstall_with_rollback`
|
||||
calls it, so a refused *update* restores the version the user already had.
|
||||
Refusal requires evidence: an undeclared floor, an unparseable version on
|
||||
either side, or an untrustworthy core version all allow the install.
|
||||
- **A failed install no longer destroys the plugin it replaced.**
|
||||
`install_plugin` previously deleted the existing plugin directory before
|
||||
downloading, so any later failure — a dropped connection, a malformed
|
||||
manifest, or the new compatibility refusal — left the user with nothing. The
|
||||
existing copy is now set aside and restored if the install fails, matching
|
||||
the protection `_reinstall_with_rollback` already gave the update path.
|
||||
- `web_interface.__version__` re-exports `src.__version__` instead of carrying
|
||||
its own hardcoded `"3.0.0"`, which had drifted two majors from the core.
|
||||
- **Live games are no longer dropped when the feed omits a game clock.**
|
||||
`SportsLive._is_game_really_over` previously (in the baseball and UFC
|
||||
plugin lineages) coerced a missing or non-string clock to the literal
|
||||
`"0:00"` and then treated the game as finished once `period >= 4`. Baseball
|
||||
has no game clock and `period` is the inning, so live MLB games disappeared
|
||||
from the scoreboard from the 5th inning onward; UFC was affected the same
|
||||
way. The clock check is now skipped when the clock is unusable, and the
|
||||
period threshold is the per-sport `FINAL_PERIOD` (hockey ends in P3).
|
||||
Sports whose clocks count up — soccer, AFL, NRL — set
|
||||
`CLOCK_COUNTS_DOWN = False` and never run the check at all, since `0:00`
|
||||
there means kickoff rather than expiry.
|
||||
|
||||
### Fixed
|
||||
- **Plugin updates could hang the web request thread.** The per-plugin reinstall
|
||||
locks were non-reentrant, and `_reinstall_with_rollback` holds one across its
|
||||
call to `install_plugin` — which now takes the same lock to protect the
|
||||
set-aside/restore above. That nesting deadlocked
|
||||
`update_plugin → _reinstall_with_rollback → install_plugin`, the standard
|
||||
path for every monorepo plugin update. The locks are now `RLock`s.
|
||||
- `FontManager` resolves `assets/fonts` against the core install root instead
|
||||
of the process working directory, so font loading works when the process
|
||||
starts elsewhere (e.g. the plugin safety harness on CI).
|
||||
- Hockey events whose competitors carry no `statistics` array are no longer
|
||||
discarded. The extractor read `competitor["statistics"]` unguarded, so a
|
||||
`KeyError` inside the generator dropped the entire event despite valid
|
||||
scores and status; shot counts now fall back to `0`.
|
||||
- Live baseball events that populate status only at the competition level are
|
||||
no longer discarded. The extractor read the event top-level
|
||||
`game_event["status"]` for the inning; real ESPN events duplicate it, but
|
||||
MiLB events synthesized from the MLB Stats API do not, so the lookup raised
|
||||
a bare `KeyError`. It now reads the already-validated competition-level
|
||||
status.
|
||||
- `SportsLive._is_game_really_over` no longer crashes the live-update pass when
|
||||
a feed sends an explicit null `period`. `None >= FINAL_PERIOD` raised
|
||||
`TypeError`, and the only caller (`_detect_stale_games`) has no `try/except`
|
||||
— the same failure shape as the already-fixed null `period_text`.
|
||||
- An expired clock spelled `"00:00"` now ends the game. The check compared the
|
||||
colon-stripped clock against a hand-listed set of literals, which `"0000"` is
|
||||
not a member of, so a finished game with a two-digit-minute clock stayed on
|
||||
the scoreboard indefinitely. The comparison is now numeric.
|
||||
- `SportsCore._load_fonts` resolves `assets/fonts` through the `_font_root()`
|
||||
seam instead of the process working directory. Started outside the install
|
||||
root, every scoreboard font silently degraded to PIL's default bitmap face.
|
||||
- `SportsCore._should_log` no longer raises `AttributeError` on the first
|
||||
warning of a run; `_last_warning_time` is initialized in `__init__` rather
|
||||
than lazily by an unrelated method.
|
||||
- `SportsCore._resolve_project_path` resolved relative logo directories
|
||||
against `<root>/src` instead of the repo root after `sports.py` became a
|
||||
package — the class bodies moved byte-identically but `__file__` gained a
|
||||
directory. Both it and `_font_root` now derive from one `_INSTALL_ROOT`
|
||||
constant.
|
||||
|
||||
## 3.1.0
|
||||
|
||||
Baseline for this changelog. Highlights already shipped at this version:
|
||||
skin system for sports scoreboards (#419), Vegas continuous-scroll overhaul
|
||||
(#423), plugin update surfacing (#421).
|
||||
@@ -4,8 +4,18 @@
|
||||
- `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)
|
||||
- `plugins/` — Installed plugins directory (gitignored)
|
||||
- `plugin-repos/` — Development symlinks to monorepo plugin dirs
|
||||
- `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:167`).
|
||||
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`
|
||||
@@ -14,6 +24,16 @@
|
||||
- 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.matrix.width/height`
|
||||
- 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)
|
||||
@@ -25,7 +45,18 @@
|
||||
- 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)
|
||||
- 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); hook: `SportsCore._render_game()` in `src/base_classes/sports/core.py`
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
This includes the LEDMatrix Discord server, GitHub repositories owned by
|
||||
ChuckBuilds, and any other forums hosted by or affiliated with the project.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement on the
|
||||
[LEDMatrix Discord](https://discord.gg/uW36dVAtcT) (DM a moderator or
|
||||
ChuckBuilds directly) or by opening a private GitHub Security Advisory if
|
||||
the issue involves account safety. All complaints will be reviewed and
|
||||
investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||
at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -0,0 +1,117 @@
|
||||
# Contributing to LEDMatrix
|
||||
|
||||
Thanks for considering a contribution! LEDMatrix is built with help from
|
||||
the community and we welcome bug reports, plugins, documentation
|
||||
improvements, and code changes.
|
||||
|
||||
## Quick links
|
||||
|
||||
- **Bugs / feature requests**: open an issue using one of the templates
|
||||
in [`.github/ISSUE_TEMPLATE/`](.github/ISSUE_TEMPLATE/).
|
||||
- **Real-time discussion**: the
|
||||
[LEDMatrix Discord](https://discord.gg/uW36dVAtcT).
|
||||
- **Plugin development**:
|
||||
[`docs/PLUGIN_DEVELOPMENT_GUIDE.md`](docs/PLUGIN_DEVELOPMENT_GUIDE.md)
|
||||
and the [`ledmatrix-plugins`](https://github.com/ChuckBuilds/ledmatrix-plugins)
|
||||
repository.
|
||||
- **Security issues**: see [`SECURITY.md`](SECURITY.md). Please don't
|
||||
open public issues for vulnerabilities.
|
||||
|
||||
## Setting up a development environment
|
||||
|
||||
1. Clone with submodules:
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/ChuckBuilds/LEDMatrix.git
|
||||
cd LEDMatrix
|
||||
```
|
||||
2. For development without hardware, run the dev preview server:
|
||||
```bash
|
||||
python3 scripts/dev_server.py
|
||||
# then open http://localhost:5001
|
||||
```
|
||||
See [`docs/DEV_PREVIEW.md`](docs/DEV_PREVIEW.md) for details.
|
||||
3. To run the full display in emulator mode:
|
||||
```bash
|
||||
EMULATOR=true python3 run.py
|
||||
```
|
||||
4. To target real hardware on a Raspberry Pi, follow the install
|
||||
instructions in the root [`README.md`](README.md).
|
||||
|
||||
## Running the tests
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt -r requirements-test.txt
|
||||
pytest
|
||||
```
|
||||
|
||||
See [`docs/HOW_TO_RUN_TESTS.md`](docs/HOW_TO_RUN_TESTS.md) for details
|
||||
on test markers, the per-plugin tests, and the web-interface
|
||||
integration tests.
|
||||
|
||||
## Submitting changes
|
||||
|
||||
1. **Open an issue first** for non-trivial changes. This avoids
|
||||
wasted work on PRs that don't fit the project direction.
|
||||
2. **Create a topic branch** off `main`:
|
||||
`feat/<short-description>`, `fix/<short-description>`,
|
||||
`docs/<short-description>`.
|
||||
3. **Keep PRs focused.** One conceptual change per PR. If you find
|
||||
adjacent bugs while working, fix them in a separate PR.
|
||||
4. **Follow the existing code style.** The pre-commit hooks run
|
||||
`flake8` (E9, F63, F7, F82 plus bugbear `B` checks), `mypy` on
|
||||
`src/`, `bandit`, and `gitleaks` — install the CLI with
|
||||
`python -m pip install pre-commit`, then run
|
||||
`pre-commit install` so they run on every commit; HTML/JS in
|
||||
`web_interface/` follows the patterns already in `templates/v3/`
|
||||
and `static/v3/`.
|
||||
5. **Update documentation** alongside code changes. If you add a
|
||||
config key, document it in the relevant `*.md` file (or, for
|
||||
plugins, in `config_schema.json` so the form is auto-generated).
|
||||
6. **Run the tests** locally before opening the PR.
|
||||
7. **Use the PR template** — `.github/PULL_REQUEST_TEMPLATE.md` will
|
||||
prompt you for what we need.
|
||||
|
||||
## Commit message convention
|
||||
|
||||
Conventional Commits is encouraged but not strictly enforced:
|
||||
|
||||
- `feat: add NHL playoff bracket display`
|
||||
- `fix(plugin-loader): handle missing class_name in manifest`
|
||||
- `docs: correct web UI port in TROUBLESHOOTING.md`
|
||||
- `refactor(cache): consolidate strategy lookup`
|
||||
|
||||
Keep the subject under 72 characters; put the why in the body.
|
||||
|
||||
## Contributing a plugin
|
||||
|
||||
LEDMatrix plugins live in their own repository:
|
||||
[`ledmatrix-plugins`](https://github.com/ChuckBuilds/ledmatrix-plugins).
|
||||
Plugin contributions go through that repo's
|
||||
[`SUBMISSION.md`](https://github.com/ChuckBuilds/ledmatrix-plugins/blob/main/SUBMISSION.md)
|
||||
process. The
|
||||
[`hello-world` plugin](https://github.com/ChuckBuilds/ledmatrix-plugins/tree/main/plugins/hello-world)
|
||||
is the canonical starter template.
|
||||
|
||||
## Reviewing pull requests
|
||||
|
||||
Maintainer review is by [@ChuckBuilds](https://github.com/ChuckBuilds).
|
||||
Community review is welcome on any open PR — leave constructive
|
||||
comments, test on your hardware if applicable, and call out anything
|
||||
unclear.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). By
|
||||
participating you agree to abide by its terms.
|
||||
|
||||
## License
|
||||
|
||||
LEDMatrix is licensed under the [GNU General Public License v3.0 or
|
||||
later](LICENSE). By submitting a contribution you agree to license it
|
||||
under the same terms (the standard "inbound = outbound" rule that
|
||||
GitHub applies by default).
|
||||
|
||||
LEDMatrix builds on
|
||||
[`rpi-rgb-led-matrix`](https://github.com/hzeller/rpi-rgb-led-matrix),
|
||||
which is GPL-2.0-or-later. The "or later" clause makes it compatible
|
||||
with GPL-3.0 distribution.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,4 +1,10 @@
|
||||
# LEDMatrix
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/RdrC37rEag)
|
||||
[](https://github.com/ChuckBuilds/ledmatrix)
|
||||
[](https://app.codacy.com/gh/ChuckBuilds/LEDMatrix/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||
|
||||
|
||||
## Welcome to LEDMatrix!
|
||||
Welcome to the LEDMatrix Project! This open-source project enables you to run an information-rich display on a Raspberry Pi connected to an LED RGB Matrix panel. Whether you want to see your calendar, weather forecasts, sports scores, stock prices, or any other information at a glance, LEDMatrix brings it all together.
|
||||
|
||||
@@ -44,7 +50,15 @@ I'm trying to be open to constructive criticism and support, as long as it's a r
|
||||
|
||||
<details>
|
||||
<summary>Core Features</summary>
|
||||
The following plugins are available inside of the LEDMatrix project. These modular, rotating Displays that can be individually enabled or disabled per the user's needs with some configuration around display durations, teams, stocks, weather, timezones, and more. Displays include:
|
||||
LEDMatrix is a plugin platform: the displays below are plugins installed
|
||||
from the built-in Plugin Store (web interface → Plugins), where each can be
|
||||
individually enabled, ordered, and configured — display durations, teams,
|
||||
stocks, weather, timezones, and more. The core repo ships with just two
|
||||
bundled plugins (`starlark-apps` and `web-ui-info`); the official plugins
|
||||
live in the [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins)
|
||||
monorepo and install with one click, and third-party plugins can be
|
||||
installed from their own GitHub repositories. Displays available in the
|
||||
store include:
|
||||
|
||||
### Time and Weather
|
||||
- Real-time clock display (2x 64x32 Displays 4mm Pixel Pitch)
|
||||
@@ -126,10 +140,16 @@ The system supports live, recent, and upcoming game information for multiple spo
|
||||
| This project can be finnicky! RGB LED Matrix displays are not built the same or to a high-quality standard. We have seen many displays arrive dead or partially working in our discord. Please purchase from a reputable vendor. |
|
||||
|
||||
### Raspberry Pi
|
||||
- Raspberry Pi Zero's don't have enough processing power for this project and the Pi 5 is unsupported due to new GPIO output.
|
||||
- **Raspberry Pi 3B or 4 (NOT RPi 5!)**
|
||||
- Raspberry Pi Zero's don't have enough processing power for this project.
|
||||
- **Raspberry Pi 3B, 4, or 5**
|
||||
[Amazon Affiliate Link – Raspberry Pi 4 4GB RAM](https://amzn.to/4dJixuX)
|
||||
[Amazon Affiliate Link – Raspberry Pi 4 8GB RAM](https://amzn.to/4qbqY7F)
|
||||
- **Pi 5 users**: the installer automatically detects Pi 5 and builds the `rpi-rgb-led-matrix` library with RP1 support. If you previously installed on a Pi 4 and migrated the SD card, or if you see `mmap` errors in the logs, force a fresh library build:
|
||||
```bash
|
||||
sudo RPI_RGB_FORCE_REBUILD=1 ./first_time_install.sh
|
||||
```
|
||||
- Pi 5 config: leave `rp1_rio` at `0` (PIO mode, default) and set `gpio_slowdown` to `1` or `2`.
|
||||
- **1GB models (Pi 3B / 3B+) and other low-memory boards**: supported, but the `rpi-rgb-led-matrix` C++ build needs more memory than the Pi has. The installer detects this automatically, compiles with fewer parallel jobs, and adds a temporary swapfile for the build which it removes afterwards. Expect that step to take 15-25 minutes instead of 2-5, and leave at least **3GB free** on the SD card. If you manage swap yourself, opt out with `--skip-swap`. To pin the compiler down further, use `--build-jobs 1`.
|
||||
|
||||
|
||||
### RGB Matrix Bonnet / HAT
|
||||
@@ -303,12 +323,12 @@ curl -fsSL https://raw.githubusercontent.com/ChuckBuilds/LEDMatrix/main/scripts/
|
||||
```
|
||||
|
||||
This one-shot installer will automatically:
|
||||
- Check system prerequisites (network, disk space, sudo access)
|
||||
- Check system prerequisites (network, disk space, memory, sudo access)
|
||||
- Install required system packages (git, python3, build tools, etc.)
|
||||
- Clone or update the LEDMatrix repository
|
||||
- Run the complete first-time installation script
|
||||
|
||||
The installation process typically takes 10-30 minutes depending on your internet connection and Pi model. All errors are reported explicitly with actionable fixes.
|
||||
The installation process typically takes 10-30 minutes depending on your internet connection and Pi model. Pi 3B/3B+ and other 1GB boards land at the top of that range, because the C++ library is compiled serially to stay within available memory. All errors are reported explicitly with actionable fixes.
|
||||
|
||||
**Note:** The script is safe to run multiple times and will handle existing installations gracefully.
|
||||
|
||||
@@ -360,6 +380,10 @@ This single script installs services, dependencies, configures permissions and s
|
||||
|
||||
### Initial Setup
|
||||
|
||||
For a complete list of every key in `config.json` and
|
||||
`config_secrets.json`, see
|
||||
[docs/CONFIG_REFERENCE.md](docs/CONFIG_REFERENCE.md).
|
||||
|
||||
For most settings I recommend using the web interface:
|
||||
Edit the project via the web interface at http://[IP ADDRESS or HOSTNAME]:5000 or http://ledpi:5000 .
|
||||
|
||||
@@ -405,7 +429,7 @@ I recommend using the web-ui "Quick Actions" to control the Display.
|
||||
## Plugins
|
||||
|
||||
<details>
|
||||
LEDMatrix uses a plugin-based architecture where all display functionality (except the core calendar) is implemented as plugins. All managers that were previously built into the core system are now available as plugins through the Plugin Store.
|
||||
LEDMatrix uses a plugin-based architecture where all display functionality is implemented as plugins. All managers that were previously built into the core system are now available as plugins through the Plugin Store.
|
||||
|
||||
### Plugin Store
|
||||
See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-plugins) for detailed installation instructions.
|
||||
@@ -429,6 +453,16 @@ 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.
|
||||
|
||||
### Visual Skins for Scoreboards
|
||||
|
||||
Want a different look for a sports scoreboard without forking the plugin?
|
||||
**Skins** restyle the live/recent/upcoming screens while the plugin keeps
|
||||
handling data, scheduling, caching, and vegas mode. Install one with
|
||||
`git clone <skin repo> skins/<skin-id>`, select it in the plugin's config,
|
||||
and you're done — see [docs/SKIN_SYSTEM.md](docs/SKIN_SYSTEM.md) (how it
|
||||
works) and [docs/CREATING_SKINS.md](docs/CREATING_SKINS.md) (build your own,
|
||||
including a ready-made Claude Code prompt).
|
||||
|
||||
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>
|
||||
|
||||
@@ -581,19 +615,14 @@ These settings control runtime behavior and GPIO timing:
|
||||
- **Critical setting**: Must match your Raspberry Pi model for stability
|
||||
- **Raspberry Pi 3**: Use 3
|
||||
- **Raspberry Pi 4**: Use 4
|
||||
- **Raspberry Pi 5**: Use 5 (or higher if needed)
|
||||
- **Raspberry Pi 5**: Use 1–2 in PIO mode (`rp1_rio: 0`, the default); start with `1` and increase if you see flickering
|
||||
- **Raspberry Pi Zero/1**: Use 1-2
|
||||
- Incorrect values can cause display corruption, flickering, or system instability
|
||||
- If you experience issues, try adjusting this value up or down by 1
|
||||
|
||||
### Display Durations (`display.display_durations`)
|
||||
|
||||
Controls how long each display module stays visible in seconds before switching to the next one.
|
||||
|
||||
- **`calendar`** (integer, default: 30)
|
||||
- Duration in seconds for the calendar display
|
||||
- Increase for more time to read dates/events
|
||||
- Decrease to cycle through other displays faster
|
||||
Controls how long each installed plugin stays visible in seconds before switching to the next one, keyed by plugin id.
|
||||
|
||||
- **Plugin-specific durations**
|
||||
- Each plugin can have its own duration setting
|
||||
@@ -782,14 +811,18 @@ The LEDMatrix system includes Web Interface that runs on port 5000 and provides
|
||||
|
||||
### Installing the Web Interface Service
|
||||
|
||||
> The first-time installer (`first_time_install.sh`) already installs the
|
||||
> web service. The steps below only apply if you need to (re)install it
|
||||
> manually.
|
||||
|
||||
1. Make the install script executable:
|
||||
```bash
|
||||
chmod +x install_web_service.sh
|
||||
chmod +x scripts/install/install_web_service.sh
|
||||
```
|
||||
|
||||
2. Run the install script with sudo:
|
||||
```bash
|
||||
sudo ./install_web_service.sh
|
||||
sudo ./scripts/install/install_web_service.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
@@ -874,3 +907,27 @@ sudo systemctl enable ledmatrix-web.service
|
||||
|
||||
|
||||
### If you've read this far — thanks!
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
|
||||
## License
|
||||
|
||||
LEDMatrix is licensed under the
|
||||
[GNU General Public License v3.0 or later](LICENSE).
|
||||
|
||||
LEDMatrix builds on
|
||||
[`rpi-rgb-led-matrix`](https://github.com/hzeller/rpi-rgb-led-matrix),
|
||||
which is GPL-2.0-or-later. The "or later" clause makes it compatible
|
||||
with GPL-3.0 distribution.
|
||||
|
||||
Plugin contributions in
|
||||
[`ledmatrix-plugins`](https://github.com/ChuckBuilds/ledmatrix-plugins)
|
||||
are also GPL-3.0-or-later unless individual plugins specify otherwise.
|
||||
|
||||
## Contributing
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, the PR
|
||||
flow, and how to add a plugin. Bug reports and feature requests go in
|
||||
the [issue tracker](https://github.com/ChuckBuilds/LEDMatrix/issues).
|
||||
Security issues should be reported privately per
|
||||
[SECURITY.md](SECURITY.md).
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you've found a security issue in LEDMatrix, **please don't open a
|
||||
public GitHub issue**. Disclose it privately so we can fix it before it's
|
||||
exploited.
|
||||
|
||||
### How to report
|
||||
|
||||
Use one of these channels, in order of preference:
|
||||
|
||||
1. **GitHub Security Advisories** (preferred). On the LEDMatrix repo,
|
||||
go to **Security → Advisories → Report a vulnerability**. This
|
||||
creates a private discussion thread visible only to you and the
|
||||
maintainer.
|
||||
- Direct link: <https://github.com/ChuckBuilds/LEDMatrix/security/advisories/new>
|
||||
2. **Discord DM**. Send a direct message to a moderator on the
|
||||
[LEDMatrix Discord](https://discord.gg/uW36dVAtcT). Don't post in
|
||||
public channels.
|
||||
|
||||
Please include:
|
||||
|
||||
- A description of the issue
|
||||
- The version / commit hash you're testing against
|
||||
- Steps to reproduce, ideally a minimal proof of concept
|
||||
- The impact you can demonstrate
|
||||
- Any suggested mitigation
|
||||
|
||||
### What to expect
|
||||
|
||||
- An acknowledgement within a few days (this is a hobby project, not
|
||||
a 24/7 ops team).
|
||||
- A discussion of the issue's severity and a plan for the fix.
|
||||
- Credit in the release notes when the fix ships, unless you'd
|
||||
prefer to remain anonymous.
|
||||
- For high-severity issues affecting active deployments, we'll
|
||||
coordinate disclosure timing with you.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope for this policy:
|
||||
|
||||
- The LEDMatrix display controller, web interface, and plugin loader
|
||||
in this repository
|
||||
- The official plugins in
|
||||
[`ledmatrix-plugins`](https://github.com/ChuckBuilds/ledmatrix-plugins)
|
||||
- Installation scripts and systemd unit files
|
||||
|
||||
Out of scope (please report upstream):
|
||||
|
||||
- Vulnerabilities in `rpi-rgb-led-matrix` itself —
|
||||
report to <https://github.com/hzeller/rpi-rgb-led-matrix>
|
||||
- Vulnerabilities in Python packages we depend on — report to the
|
||||
upstream package maintainer
|
||||
- Issues in third-party plugins not in `ledmatrix-plugins` — report
|
||||
to that plugin's repository
|
||||
|
||||
## Known security model
|
||||
|
||||
LEDMatrix is designed for trusted local networks. Several limitations
|
||||
are intentional rather than vulnerabilities:
|
||||
|
||||
- **No web UI authentication.** The web interface assumes the network
|
||||
it's running on is trusted. Don't expose port 5000 to the internet.
|
||||
- **Plugins run unsandboxed.** Installed plugins execute in the same
|
||||
Python process as the display loop with full file-system and
|
||||
network access. Review plugin code (especially third-party plugins
|
||||
from arbitrary GitHub URLs) before installing. The Plugin Store
|
||||
marks community plugins as **Custom** to highlight this.
|
||||
- **The display service runs as root** for hardware GPIO access. This
|
||||
is required by `rpi-rgb-led-matrix`.
|
||||
- **`config_secrets.json` is plaintext.** API keys and tokens are
|
||||
stored unencrypted on the Pi. Lock down filesystem permissions on
|
||||
the config directory if this matters for your deployment.
|
||||
|
||||
These are documented as known limitations rather than bugs. If you
|
||||
have ideas for improving them while keeping the project usable on a
|
||||
Pi, open a discussion — we're interested.
|
||||
|
||||
## Supported versions
|
||||
|
||||
LEDMatrix is rolling-release on `main`. Security fixes land on `main`
|
||||
and become available the next time users run **Update Code** from the
|
||||
web UI's Overview tab (which does a `git pull`). There are no LTS
|
||||
branches.
|
||||
@@ -0,0 +1,22 @@
|
||||
# assets/
|
||||
|
||||
Static assets bundled with LEDMatrix. **Do not delete these directories** —
|
||||
several look unused from core code alone but are resolved at runtime by
|
||||
installed store plugins.
|
||||
|
||||
| Directory | Used by |
|
||||
|---|---|
|
||||
| `fonts/` | Core (`FontManager`, `DisplayManager`) and most plugins |
|
||||
| `sports/` | Core logo tooling (`src/logo_downloader.py`) and the sports scoreboard plugins; team logos are downloaded here on demand |
|
||||
| `stocks/` | `ledmatrix-stocks` plugin (`crypto_icons/`, `ticker_icons/`) |
|
||||
| `weather/` | `ledmatrix-weather` plugin (weather icons) |
|
||||
| `news_logos/` | `news` plugin |
|
||||
| `broadcast_logos/` | `news` and `odds-ticker` plugins |
|
||||
| `static_images/` | Legacy examples referenced in the `static-image` plugin's docs; the plugin itself stores uploads under `assets/plugins/<plugin-id>/uploads/` |
|
||||
| `plugins/` | Per-plugin uploaded files (`assets/plugins/<plugin-id>/uploads/`), served by the web interface |
|
||||
|
||||
Plugins resolve these paths relative to the LEDMatrix install directory, so
|
||||
the directories are part of the de-facto plugin API even where no file in
|
||||
this repo references them. New plugins should bundle their own assets or
|
||||
use the per-plugin upload directory instead of adding top-level
|
||||
directories here.
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 657 KiB |
@@ -0,0 +1,29 @@
|
||||
# bandit.yaml — LEDMatrix bandit configuration
|
||||
# https://bandit.readthedocs.io/en/latest/config.html
|
||||
#
|
||||
# Skips are justified by the specific codebase context documented below.
|
||||
# Do not remove skips without updating the justification comment.
|
||||
|
||||
skips:
|
||||
# B104: Binding to all interfaces (0.0.0.0)
|
||||
# Intentional — the Flask server binds 0.0.0.0 for LAN access on a Raspberry Pi.
|
||||
# This is not internet-facing and is documented in web_interface/app.py.
|
||||
- B104
|
||||
|
||||
# B603: subprocess call without shell=True
|
||||
# All subprocess.run() calls in this codebase use list arguments (confirmed by
|
||||
# grep — zero uses of shell=True in src/ or web_interface/). List args prevent
|
||||
# shell injection. See src/common/permission_utils.py for the primary usage.
|
||||
- B603
|
||||
|
||||
# B607: Starting a process with a partial executable path
|
||||
# The subprocess calls invoke system utilities (systemctl, sudo, git) by name.
|
||||
# These are fixed-list invocations, not user-controlled, and rely on PATH.
|
||||
- B607
|
||||
|
||||
exclude_dirs:
|
||||
- tests
|
||||
- test
|
||||
- venv
|
||||
- .venv
|
||||
- rpi-rgb-led-matrix-master
|
||||
@@ -1,43 +1,43 @@
|
||||
{
|
||||
"web_display_autostart": true,
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"mode": "per-day",
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00",
|
||||
"days": {
|
||||
"monday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"friday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
}
|
||||
@@ -51,46 +51,47 @@
|
||||
"end_time": "07:00",
|
||||
"days": {
|
||||
"monday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"friday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timezone": "America/Chicago",
|
||||
"timezone": "America/New_York",
|
||||
"target_fps": 100,
|
||||
"location": {
|
||||
"city": "Dallas",
|
||||
"state": "Texas",
|
||||
"city": "Tampa",
|
||||
"state": "Florida",
|
||||
"country": "US"
|
||||
},
|
||||
"display": {
|
||||
@@ -109,12 +110,23 @@
|
||||
"inverse_colors": false,
|
||||
"show_refresh_rate": false,
|
||||
"led_rgb_sequence": "RGB",
|
||||
"limit_refresh_rate_hz": 100
|
||||
"limit_refresh_rate_hz": 100,
|
||||
"pixel_mapper_config": "",
|
||||
"row_address_type": 0,
|
||||
"multiplexing": 0,
|
||||
"panel_type": ""
|
||||
},
|
||||
"runtime": {
|
||||
"gpio_slowdown": 3
|
||||
"gpio_slowdown": 3,
|
||||
"rp1_rio": 0
|
||||
},
|
||||
"double_sided": {
|
||||
"enabled": false,
|
||||
"copies": 2,
|
||||
"axis": "horizontal"
|
||||
},
|
||||
"display_durations": {},
|
||||
"plugin_rotation_order": [],
|
||||
"use_short_date_format": true,
|
||||
"vegas_scroll": {
|
||||
"enabled": false,
|
||||
@@ -123,13 +135,39 @@
|
||||
"plugin_order": [],
|
||||
"excluded_plugins": [],
|
||||
"target_fps": 125,
|
||||
"buffer_ahead": 2
|
||||
"buffer_ahead": 2,
|
||||
"intra_plugin_gap": 8,
|
||||
"render_width_pct": 100,
|
||||
"min_content_separation": 24,
|
||||
"min_cut_gap": 6,
|
||||
"continuous_scroll": true,
|
||||
"smooth_scroll": true,
|
||||
"extend_threshold_screens": 2.0,
|
||||
"auto_trim": true,
|
||||
"trim_threshold": 10,
|
||||
"content_padding": 8,
|
||||
"min_plugin_width": 8,
|
||||
"lead_in_width": 0,
|
||||
"plugins_per_cycle": 6,
|
||||
"max_plugin_width_ratio": 3.0,
|
||||
"overflow_mode": "rotate",
|
||||
"dynamic_duration_enabled": true,
|
||||
"min_cycle_duration": 60,
|
||||
"max_cycle_duration": 240,
|
||||
"frame_based_scrolling": true,
|
||||
"scroll_delay": 0.02
|
||||
}
|
||||
},
|
||||
"sync": {
|
||||
"role": "standalone",
|
||||
"port": 5765,
|
||||
"follower_position": "left"
|
||||
},
|
||||
"plugin_system": {
|
||||
"plugins_directory": "plugin-repos",
|
||||
"auto_discover": true,
|
||||
"auto_load_enabled": true
|
||||
"auto_load_enabled": true,
|
||||
"development_mode": false
|
||||
},
|
||||
"web-ui-info": {
|
||||
"enabled": true,
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
{
|
||||
"ledmatrix-weather": {
|
||||
"api_key": "YOUR_OPENWEATHERMAP_API_KEY"
|
||||
},
|
||||
"youtube": {
|
||||
"api_key": "YOUR_YOUTUBE_API_KEY",
|
||||
"channel_id": "YOUR_YOUTUBE_CHANNEL_ID"
|
||||
},
|
||||
"music": {
|
||||
"SPOTIFY_CLIENT_ID": "YOUR_SPOTIFY_CLIENT_ID_HERE",
|
||||
"SPOTIFY_CLIENT_SECRET": "YOUR_SPOTIFY_CLIENT_SECRET_HERE",
|
||||
"SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
|
||||
},
|
||||
"github": {
|
||||
"api_token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
# Adaptive Layout & Font Scaling
|
||||
|
||||
`src/adaptive_layout.py` lets a plugin render legibly on **any** panel size
|
||||
(64x32, 128x32, 96x48, 128x64, 256x64, ...) without hand-tuned per-display
|
||||
layouts. It is **opt-in**: nothing changes for plugins that don't use it.
|
||||
|
||||
It generalizes three patterns proven in the plugin ecosystem:
|
||||
|
||||
| Pattern | Origin | Core API |
|
||||
|---|---|---|
|
||||
| Geometry scale factor vs. a design size | f1-scoreboard | `ctx.px(base)` / `ctx.scale` |
|
||||
| Breakpoint tiers | masters-tournament | `ctx.tier` / `ctx.by_tier({...})` |
|
||||
| "Largest crisp font that fits" ladder | baseball-scoreboard | `ctx.fit_text(...)` and friends |
|
||||
|
||||
## Quick start
|
||||
|
||||
Every `BasePlugin` has a lazy `self.layout` (a `LayoutContext` for the
|
||||
current logical display size, rebuilt automatically if the size changes)
|
||||
and a one-liner `self.draw_fit(...)`:
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
from src.adaptive_layout import LADDER_ARCADE
|
||||
|
||||
b = self.layout.bounds.inset(1) # Region(0,0,W,H) minus 1px margin
|
||||
rows = b.split_v(3, 1, 1, gap=1) # 3/5 for time, 1/5 each for the rest
|
||||
|
||||
self.draw_fit(self.time_str, rows[0], ladder=LADDER_ARCADE)
|
||||
self.draw_fit(self.weekday, rows[1]) # default LADDER_GRID
|
||||
self.draw_fit(self.date_str, rows[2])
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
On 128x64 the time renders at press_start 24px; on 64x32 it steps down to
|
||||
8px. The rows partition the height, so bands can never overlap — no more
|
||||
`y = height - 7` magic numbers.
|
||||
|
||||
## Region — rect algebra
|
||||
|
||||
`Region(x, y, w, h)` is a frozen dataclass. All carving clamps to
|
||||
non-negative dimensions, so degenerate panels behave.
|
||||
|
||||
- Carving: `inset(dx, dy)`, `top_band(h)`, `bottom_band(h)`,
|
||||
`middle(top_h, bottom_h)`, `left_col(w)`, `right_col(w)`,
|
||||
`split_h(*weights, gap=0)`, `split_v(*weights, gap=0)`
|
||||
- Placement: `align_xy(w, h, align, valign)`, `center_xy(w, h)`,
|
||||
`contains(w, h)`, `.center`, `.right`, `.bottom`
|
||||
|
||||
Scoreboard-style layout:
|
||||
|
||||
```python
|
||||
b = self.layout.bounds
|
||||
status = b.top_band(self.layout.px(7))
|
||||
detail = b.bottom_band(self.layout.px(7))
|
||||
score_area = b.middle(status.h, detail.h)
|
||||
away_slot, home_slot = b.left_col(b.h), b.right_col(b.h)
|
||||
```
|
||||
|
||||
## Font ladders — discrete, never fractional
|
||||
|
||||
Pixel fonts (BDF, PressStart2P) only look right at native/integer sizes, so
|
||||
fonts are never scaled continuously. A `FontLadder` is an ordered tuple of
|
||||
`FontStep(family, size_px)` rungs, largest first; fitting walks down until
|
||||
the measured text fits.
|
||||
|
||||
- `LADDER_GRID` (default): X11 BDFs at native sizes — 10x20 → 9x18 → 9x15 →
|
||||
8x13 → 7x13 → 6x13 → 6x12 → 6x10 → 6x9 → 5x8 → 5x7 → 4x6 → tom-thumb.
|
||||
Body text, labels, multi-row content.
|
||||
- `LADDER_ARCADE`: PressStart2P at 32/24/16/8 (integer multiples of its 8px
|
||||
grid). Headline text: clocks, scores.
|
||||
|
||||
Custom ladders are just tuples — e.g. to add your plugin's registered font
|
||||
on top: `(FontStep("myplugin::digits", 16),) + LADDER_GRID`.
|
||||
|
||||
## LayoutContext
|
||||
|
||||
Built per (width, height); exposes facts and fit queries:
|
||||
|
||||
- `bounds`, `width`, `height`, `aspect`
|
||||
- `tier` by height (`xs`≤16, `sm`≤32, `md`≤48, `lg`≤64, `xl`) and
|
||||
`width_tier` (`narrow`≤64, `normal`≤128, `wide`≤256, `ultrawide`)
|
||||
- `is_wide_short` — aspect ≥ 2.5 and height ≤ 32 (the classic 128x32 shape)
|
||||
- `scale` — `min(w/design_w, h/design_h)` vs. your manifest's
|
||||
`display.design_size` (default 128x32). **Geometry only** — gaps, icon
|
||||
and logo sizes via `px(base, minimum, maximum)`; fonts use ladders.
|
||||
- `by_tier({"sm": 10, "lg": 18})` — value for the nearest defined tier
|
||||
at-or-below the panel's tier.
|
||||
- `fit_text(text, box, ladder, ellipsis=True)` → `FitResult` — largest rung
|
||||
that fits; ellipsizes as a last resort. Cached per (text, box, ladder).
|
||||
- `fit_text_proportional(text, box, base_size_px, ladder, ellipsis=True, scale=None)` —
|
||||
rung closest to (not exceeding) `base_size_px * scale`, still capped to
|
||||
what fits the box. Use this instead of `fit_text` when several
|
||||
independently-fitted elements need to stay visually harmonious as the
|
||||
panel grows — `fit_text` maximizes *each one* within its own region,
|
||||
which can make one element (e.g. a score with a generous box) balloon
|
||||
out of proportion to a neighbor that scales by geometry (e.g. logos
|
||||
sized via `px()`), even though each individual pick is "correct" in
|
||||
isolation. `base_size_px` is normally the element's existing classic/
|
||||
fixed font size. `scale` defaults to `self.scale` (the conservative
|
||||
min-of-both-axes factor `px()` uses); pass an axis-specific value when
|
||||
the surrounding composition already scales that way — e.g. a scoreboard
|
||||
whose logo slots track height alone (`min(height, width // 2)`) should
|
||||
size its text by `height / design_height` too, or the text reads as
|
||||
under-scaled next to bigger logos on a panel that only grew taller.
|
||||
- `fit_lines(lines, box, ladder, spacing)` — every line fits the width and
|
||||
the stack fits the height (measures the actual strings).
|
||||
- `font_for_rows(rows, box_h, ladder)` — largest rung whose line height
|
||||
fits `rows` rows.
|
||||
|
||||
`FitResult` carries the ready-to-use `font` (drops straight into
|
||||
`display_manager.draw_text(font=...)`), the possibly-ellipsized `text`,
|
||||
ink `width`/`height`, `baseline`, `y_offset`, `line_height`, and `fits`.
|
||||
|
||||
## Adaptive images
|
||||
|
||||
`src/adaptive_images.py` is the image counterpart to `fit_text`, exposed as
|
||||
`self.layout.fit_image(...)` (cached per panel size) and the one-liner
|
||||
`self.draw_image(...)`:
|
||||
|
||||
```python
|
||||
# Team logo: trim its transparent padding, fill the slot height (the
|
||||
# football/hockey pattern), cached across frames by a stable key
|
||||
self.draw_image(logo, regs.away_slot, mode="fill_height",
|
||||
crop_to_ink=True, cache_key=f"logo:{abbr}")
|
||||
|
||||
# Album art: cover-crop a square, faces kept by the top anchor
|
||||
self.draw_image(art, row.art, mode="cover", anchor="top")
|
||||
|
||||
# Pixel flags / sprite icons: NEAREST keeps hard edges
|
||||
from src.adaptive_images import RESAMPLE_NEAREST
|
||||
self.draw_image(flag, box, resample=RESAMPLE_NEAREST)
|
||||
```
|
||||
|
||||
Modes: `contain` (letterbox, default), `cover` (crop-to-fill),
|
||||
`fill_height` (logo-style), `stretch`. Unlike PIL's `thumbnail()`
|
||||
(downscale-only — why imagery stays tiny on big panels) fitting **upscales
|
||||
by default**; pass `upscale=False` for the legacy behavior. Results are
|
||||
cached per (image, box size, options) with a bounded LRU — always pass a
|
||||
stable `cache_key` (e.g. `"logo:KC"`) for images you reload. The module
|
||||
also exports the Pillow-compat `RESAMPLE_LANCZOS`/`RESAMPLE_NEAREST`
|
||||
constants so plugins can drop their local shims.
|
||||
|
||||
## Composite layouts
|
||||
|
||||
Pre-carved Region arrangements for the layouts plugins keep rebuilding:
|
||||
|
||||
```python
|
||||
from src.adaptive_layout import scoreboard_regions, media_row
|
||||
|
||||
regs = scoreboard_regions(self.layout.bounds, ctx=self.layout)
|
||||
# regs.away_slot / home_slot — logo slots (logo_slot = min(H, W // 2),
|
||||
# capped so a center reserve always exists —
|
||||
# see below)
|
||||
# regs.status_band — top band (replaces the magic y = 1)
|
||||
# regs.score_area — center gap, plus a controlled bleed into
|
||||
# each logo slot (replaces y = H//2 - 3)
|
||||
# regs.detail_band — bottom band (replaces y = H - 7)
|
||||
# regs.bottom_left / bottom_right — record/timeout corners
|
||||
|
||||
row = media_row(self.layout.bounds, ctx=self.layout) # art left, text right
|
||||
```
|
||||
|
||||
Both work on the full panel or on a scroll-mode card Region. They return
|
||||
Regions and never draw — compose them with `draw_fit`/`draw_image`.
|
||||
|
||||
**`scoreboard_regions`'s center reserve.** The raw `logo_slot = min(H, W//2)`
|
||||
formula has a blind spot: at exactly 2:1 aspect ratio (width = 2×height —
|
||||
two, four, or more square modules stacked into a taller panel, e.g.
|
||||
96x48, 128x64, 256x128) the two logo slots mathematically claim the
|
||||
*entire* width, leaving zero pixels for a center column no matter how
|
||||
big the panel gets. Wide panels (the 128x32 design baseline, 192x48,
|
||||
256x32) never hit this, since height is already the tighter constraint
|
||||
there. Two parameters fix it without any plugin-side code:
|
||||
`min_center_fraction`/`min_center_design_px` guarantee a real minimum
|
||||
center reserve at any aspect ratio, and `score_bleed_fraction` lets the
|
||||
score's *fit box* extend a controlled amount into each logo slot — the
|
||||
same way a real broadcast scoreboard's numbers cross slightly into the
|
||||
team marks flanking them — so a short score string never has to truncate
|
||||
even on the tightest aspect ratios. All three have sane defaults; override
|
||||
them per call if a plugin's card proportions genuinely differ.
|
||||
|
||||
## Preserving user customization
|
||||
|
||||
Adaptive layout supplies *defaults*; explicit user configuration wins:
|
||||
|
||||
- **User-set fonts win.** If the plugin's config has an explicit
|
||||
`font`/`font_size` for an element, load it as before and skip the ladder —
|
||||
fit only when the user hasn't overridden (see the football-scoreboard
|
||||
`_resolve_element_fit` pattern).
|
||||
- **Offsets apply on top.** `customization.layout.<element>.{x_offset,y_offset}`
|
||||
style knobs translate the *computed* region as a final step:
|
||||
`region.offset(user_dx, user_dy)`. `draw_image(..., offset=(dx, dy))`
|
||||
does the same for images.
|
||||
- **Colors pass through.** `draw_fit`/`draw_fitted_text` take explicit
|
||||
`color=` params; adaptive mode never repaints semantic or user-chosen
|
||||
colors.
|
||||
|
||||
## Manifest declaration
|
||||
|
||||
Declare the size your layout was authored against so `ctx.scale` means
|
||||
something:
|
||||
|
||||
```json
|
||||
"display": { "design_size": { "width": 128, "height": 32 } }
|
||||
```
|
||||
|
||||
Also available under `requires.display_size`: `min_width`, `min_height`,
|
||||
`max_width`, `max_height`.
|
||||
|
||||
## Performance notes (Pi)
|
||||
|
||||
Fit queries are cached, so cost is O(unique strings). For per-second text
|
||||
(clocks, live scores), fit on a **shape placeholder** and reuse the font:
|
||||
|
||||
```python
|
||||
fit = self.layout.fit_text("00:00", box, ladder=LADDER_ARCADE) # cached once
|
||||
self.display_manager.draw_text(current_time, font=fit.font, ...)
|
||||
```
|
||||
|
||||
## Testing across sizes
|
||||
|
||||
The harness already renders every plugin at a spread of sizes (now
|
||||
including 96x48):
|
||||
|
||||
```bash
|
||||
python scripts/check_plugin.py <plugin-dir> --sizes 64x32,128x32,96x48,128x64,256x64
|
||||
python scripts/render_plugin.py <plugin-dir> --width 96 --height 48
|
||||
```
|
||||
|
||||
`BoundsCheckingDisplayManager` flags right/bottom overflow and now records
|
||||
mediated draw calls with negative coordinates in
|
||||
`negative_coordinate_calls` (raw-PIL draws remain uncovered).
|
||||
|
||||
Reference migration: the **text-display** plugin's `font_mode: "auto"`.
|
||||
@@ -47,6 +47,11 @@ Enable Vegas mode in `config/config.json`:
|
||||
}
|
||||
```
|
||||
|
||||
Vegas mode can also be configured entirely from the web UI — the
|
||||
**Display** tab has a Vegas Scroll Mode section (enable toggle, scroll
|
||||
speed, separator width, dynamic duration, and more), so hand-editing
|
||||
JSON is optional.
|
||||
|
||||
**Configuration Options:**
|
||||
|
||||
| Setting | Default | Description |
|
||||
@@ -57,7 +62,11 @@ Enable Vegas mode in `config/config.json`:
|
||||
| `plugin_order` | `[]` | Plugin display order (empty = auto) |
|
||||
| `excluded_plugins` | `[]` | Plugins to exclude from Vegas mode |
|
||||
| `target_fps` | `125` | Target frame rate |
|
||||
| `buffer_ahead` | `2` | Number of panels to render ahead |
|
||||
| `buffer_ahead` | `2` | Number of plugins buffered ahead |
|
||||
|
||||
This table is a subset — `display.vegas_scroll` supports 26 keys in
|
||||
total. See the full list in
|
||||
[CONFIG_REFERENCE.md](CONFIG_REFERENCE.md#displayvegas_scroll--continuous-scroll-mode).
|
||||
|
||||
### Per-Plugin Configuration
|
||||
|
||||
@@ -79,9 +88,13 @@ Override Vegas behavior for specific plugins:
|
||||
| Setting | Values | Description |
|
||||
|---------|--------|-------------|
|
||||
| `vegas_mode` | `scroll`, `fixed`, `static` | Display mode for this plugin |
|
||||
| `vegas_panel_count` | `1-10` | Width in panels (1 panel = display width) |
|
||||
| `vegas_panel_count` | any positive integer | Width in panels (1 panel = display width) |
|
||||
| `display_duration` | seconds | Pause duration for STATIC mode |
|
||||
|
||||
Plugins may also set `vegas_overflow` and `vegas_max_width_screens` in
|
||||
their config section to control how oversized content is handled (see
|
||||
`PluginManager` in `src/plugin_system/plugin_manager.py`).
|
||||
|
||||
### Plugin Integration (Developer Guide)
|
||||
|
||||
**1. Implement Content Method:**
|
||||
@@ -437,26 +450,26 @@ When on-demand expires or is cleared, the display returns to the next highest pr
|
||||
|
||||
### Web Interface Controls
|
||||
|
||||
**Access:** Navigate to Settings → Plugin Management
|
||||
Each installed plugin has its own tab in the second nav row of the web
|
||||
UI. Inside the plugin's tab, scroll to **On-Demand Controls**:
|
||||
|
||||
**Controls:**
|
||||
- **Show Now Button** - Triggers plugin immediately
|
||||
- **Duration Slider** - Set display time (0 = indefinite)
|
||||
- **Pin Checkbox** - Keep showing until manually cleared
|
||||
- **Stop Button** - Clear on-demand and return to rotation
|
||||
- **Shift+Click Stop** - Stop the entire display service
|
||||
- **Run On-Demand** — triggers the plugin immediately, even if it's
|
||||
disabled in the rotation
|
||||
- **Stop On-Demand** — clears on-demand and returns to the normal
|
||||
rotation
|
||||
|
||||
**Status Card:**
|
||||
- Real-time status updates
|
||||
- Shows active plugin and remaining time
|
||||
- Pin status indicator
|
||||
The display service must be running. The status banner at the top of
|
||||
the plugin tab shows the active on-demand plugin, mode, and remaining
|
||||
time when something is active.
|
||||
|
||||
### REST API Reference
|
||||
|
||||
The API is mounted at `/api/v3` (`web_interface/app.py:199`).
|
||||
|
||||
#### Start On-Demand Display
|
||||
|
||||
```bash
|
||||
POST /api/display/on-demand/start
|
||||
POST /api/v3/display/on-demand/start
|
||||
|
||||
# Body:
|
||||
{
|
||||
@@ -467,20 +480,20 @@ POST /api/display/on-demand/start
|
||||
|
||||
# Examples:
|
||||
# 30-second preview
|
||||
curl -X POST http://localhost:5050/api/display/on-demand/start \
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"plugin_id": "weather", "duration": 30}'
|
||||
|
||||
# Pin indefinitely
|
||||
curl -X POST http://localhost:5050/api/display/on-demand/start \
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"plugin_id": "hockey-scores", "pinned": true}'
|
||||
-d '{"plugin_id": "hockey-scoreboard", "pinned": true}'
|
||||
```
|
||||
|
||||
#### Stop On-Demand Display
|
||||
|
||||
```bash
|
||||
POST /api/display/on-demand/stop
|
||||
POST /api/v3/display/on-demand/stop
|
||||
|
||||
# Body:
|
||||
{
|
||||
@@ -489,10 +502,10 @@ POST /api/display/on-demand/stop
|
||||
|
||||
# Examples:
|
||||
# Clear on-demand
|
||||
curl -X POST http://localhost:5050/api/display/on-demand/stop
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/stop
|
||||
|
||||
# Stop service too
|
||||
curl -X POST http://localhost:5050/api/display/on-demand/stop \
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/stop \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"stop_service": true}'
|
||||
```
|
||||
@@ -500,10 +513,10 @@ curl -X POST http://localhost:5050/api/display/on-demand/stop \
|
||||
#### Get On-Demand Status
|
||||
|
||||
```bash
|
||||
GET /api/display/on-demand/status
|
||||
GET /api/v3/display/on-demand/status
|
||||
|
||||
# Example:
|
||||
curl http://localhost:5050/api/display/on-demand/status
|
||||
curl http://localhost:5000/api/v3/display/on-demand/status
|
||||
|
||||
# Response:
|
||||
{
|
||||
@@ -516,35 +529,17 @@ curl http://localhost:5050/api/display/on-demand/status
|
||||
}
|
||||
```
|
||||
|
||||
### Python API Methods
|
||||
|
||||
```python
|
||||
from src.display_controller import DisplayController
|
||||
|
||||
controller = DisplayController()
|
||||
|
||||
# Show plugin for 30 seconds
|
||||
controller.show_on_demand('weather', duration=30)
|
||||
|
||||
# Pin plugin until manually cleared
|
||||
controller.show_on_demand('hockey-scores', pinned=True)
|
||||
|
||||
# Show indefinitely (not pinned, clears on expiry if duration set later)
|
||||
controller.show_on_demand('weather', duration=0)
|
||||
|
||||
# Use plugin's default duration
|
||||
controller.show_on_demand('weather')
|
||||
|
||||
# Clear on-demand
|
||||
controller.clear_on_demand()
|
||||
|
||||
# Check status
|
||||
is_active = controller.is_on_demand_active()
|
||||
|
||||
# Get detailed info
|
||||
info = controller.get_on_demand_info()
|
||||
# Returns: {'active': bool, 'mode': str, 'duration': float, 'remaining': float, 'pinned': bool}
|
||||
```
|
||||
> There is no public Python on-demand API. The display controller's
|
||||
> on-demand machinery is internal — drive it through the REST endpoints
|
||||
> above (or the web UI buttons). The API handlers
|
||||
> (`start_on_demand_display()` / `stop_on_demand_display()` in
|
||||
> `web_interface/blueprints/api_v3.py`) write a request into the cache
|
||||
> manager under the `display_on_demand_request` key, which
|
||||
> `DisplayController._poll_on_demand_requests()`
|
||||
> (`src/display_controller.py`) picks up. A separate
|
||||
> `display_on_demand_config` key is used by the controller itself
|
||||
> during activation (`_activate_on_demand()`) to track what's
|
||||
> currently running, and is cleared by `_clear_on_demand()`.
|
||||
|
||||
### Duration Modes
|
||||
|
||||
@@ -557,27 +552,31 @@ info = controller.get_on_demand_info()
|
||||
|
||||
### Use Case Examples
|
||||
|
||||
**Quick Check (30-second preview):**
|
||||
```python
|
||||
controller.show_on_demand('weather', duration=30)
|
||||
**Quick check (30-second preview):**
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"plugin_id": "ledmatrix-weather", "duration": 30}'
|
||||
```
|
||||
|
||||
**Pin Important Information:**
|
||||
```python
|
||||
controller.show_on_demand('game-score', pinned=True)
|
||||
**Pin important information:**
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"plugin_id": "hockey-scoreboard", "pinned": true}'
|
||||
# ... later ...
|
||||
controller.clear_on_demand()
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/stop
|
||||
```
|
||||
|
||||
**Indefinite Display:**
|
||||
```python
|
||||
controller.show_on_demand('welcome-message', duration=0)
|
||||
**Indefinite display:**
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"plugin_id": "text-display", "duration": 0}'
|
||||
```
|
||||
|
||||
**Testing Plugin:**
|
||||
```python
|
||||
controller.show_on_demand('my-new-plugin', duration=60)
|
||||
```
|
||||
**Testing a plugin during development:** the same call works, or just
|
||||
click **Run On-Demand** in the plugin's tab.
|
||||
|
||||
### Best Practices
|
||||
|
||||
@@ -613,7 +612,10 @@ controller.show_on_demand('my-new-plugin', duration=60)
|
||||
|
||||
### Overview
|
||||
|
||||
On-demand display uses Redis cache keys to manage state across service restarts and coordinate between web interface and display controller. Understanding these keys helps troubleshoot stuck states.
|
||||
On-demand display uses cache keys (managed by `src/cache_manager.py` —
|
||||
file-based, not Redis) to coordinate state between the web interface
|
||||
and the display controller across service restarts. Understanding these
|
||||
keys helps troubleshoot stuck states.
|
||||
|
||||
### Cache Keys
|
||||
|
||||
@@ -659,13 +661,13 @@ On-demand display uses Redis cache keys to manage state across service restarts
|
||||
**When Set:** Every display loop iteration
|
||||
**Auto-Cleared:** Never (continuously updated)
|
||||
|
||||
**4. display_on_demand_processed_id** (TTL: 5 minutes)
|
||||
```
|
||||
**4. display_on_demand_processed_id** (TTL: 1 hour)
|
||||
```text
|
||||
"uuid-string-of-last-processed-request"
|
||||
```
|
||||
**Purpose:** Prevents duplicate request processing
|
||||
**When Set:** After processing request
|
||||
**Auto-Cleared:** After 5 minutes TTL
|
||||
**Auto-Cleared:** After 1 hour TTL
|
||||
|
||||
### When Manual Clearing is Needed
|
||||
|
||||
@@ -688,19 +690,26 @@ On-demand display uses Redis cache keys to manage state across service restarts
|
||||
### Manual Recovery Procedures
|
||||
|
||||
**Via Web Interface (Recommended):**
|
||||
1. Navigate to Settings → Cache Management
|
||||
2. Search for "on_demand" keys
|
||||
3. Select keys to delete
|
||||
4. Click "Delete Selected"
|
||||
5. Restart display: `sudo systemctl restart ledmatrix`
|
||||
1. Open the **Cache** tab in the web UI
|
||||
2. Find the `display_on_demand_*` entries
|
||||
3. Delete them
|
||||
4. Restart display: `sudo systemctl restart ledmatrix`
|
||||
|
||||
**Via Command Line:**
|
||||
```bash
|
||||
# Clear specific key
|
||||
redis-cli DEL display_on_demand_config
|
||||
|
||||
# Clear all on-demand keys
|
||||
redis-cli KEYS "display_on_demand_*" | xargs redis-cli DEL
|
||||
The cache is stored as JSON files under one of:
|
||||
|
||||
- `/var/cache/ledmatrix/` (preferred when the service has permission)
|
||||
- `~/.ledmatrix_cache/`
|
||||
- `/opt/ledmatrix/cache/`
|
||||
- `$TMPDIR/ledmatrix_cache/` (fallback)
|
||||
|
||||
```bash
|
||||
# Find the cache dir actually in use
|
||||
journalctl -u ledmatrix | grep -i "cache directory" | tail -1
|
||||
|
||||
# Clear all on-demand keys (replace path with the one above)
|
||||
rm /var/cache/ledmatrix/display_on_demand_*
|
||||
|
||||
# Restart service
|
||||
sudo systemctl restart ledmatrix
|
||||
@@ -711,19 +720,23 @@ sudo systemctl restart ledmatrix
|
||||
from src.cache_manager import CacheManager
|
||||
|
||||
cache = CacheManager()
|
||||
cache.delete('display_on_demand_config')
|
||||
cache.delete('display_on_demand_state')
|
||||
cache.delete('display_on_demand_request')
|
||||
cache.delete('display_on_demand_processed_id')
|
||||
cache.clear_cache('display_on_demand_config')
|
||||
cache.clear_cache('display_on_demand_state')
|
||||
cache.clear_cache('display_on_demand_request')
|
||||
cache.clear_cache('display_on_demand_processed_id')
|
||||
```
|
||||
|
||||
> `CacheManager` also has a `delete(key)` method — a thin wrapper over
|
||||
> `clear_cache(key)` — so `cache.delete('display_on_demand_config')`
|
||||
> works equally well.
|
||||
|
||||
### Cache Impact on Running Service
|
||||
|
||||
**IMPORTANT:** Clearing cache keys does NOT immediately affect the running controller in memory.
|
||||
|
||||
**To fully reset:**
|
||||
1. Stop the service: `sudo systemctl stop ledmatrix`
|
||||
2. Clear cache keys (web UI or redis-cli)
|
||||
2. Clear cache keys (web UI Cache tab or `rm` from the cache directory)
|
||||
3. Clear systemd environment: `sudo systemctl daemon-reload`
|
||||
4. Start the service: `sudo systemctl start ledmatrix`
|
||||
|
||||
@@ -733,7 +746,7 @@ The display controller automatically handles cleanup:
|
||||
- **Config key**: Cleared when on-demand stops
|
||||
- **State key**: Updated every display loop iteration
|
||||
- **Request key**: Expires after 1 hour TTL (or after processing)
|
||||
- **Processed ID**: Expires after 5 minutes TTL
|
||||
- **Processed ID**: Expires after 1 hour TTL
|
||||
|
||||
---
|
||||
|
||||
@@ -767,7 +780,7 @@ Enable background service per plugin in `config/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"nfl_scoreboard": {
|
||||
"football-scoreboard": {
|
||||
"enabled": true,
|
||||
"background_service": {
|
||||
"enabled": true,
|
||||
@@ -801,19 +814,13 @@ Enable background service per plugin in `config/config.json`:
|
||||
- Returns immediately: < 0.1 seconds
|
||||
- Background refresh (if stale): async, no blocking
|
||||
|
||||
### Implementation Status
|
||||
### Plugins using the background service
|
||||
|
||||
**Phase 1 (Complete):**
|
||||
- ✅ NFL scoreboard implemented
|
||||
- ✅ Background threading architecture
|
||||
- ✅ Cache integration
|
||||
- ✅ Error handling and retry logic
|
||||
|
||||
**Phase 2 (Planned):**
|
||||
- ⏳ NCAAFB (college football)
|
||||
- ⏳ NBA (basketball)
|
||||
- ⏳ NHL (hockey)
|
||||
- ⏳ MLB (baseball)
|
||||
The background data service is used by all of the sports scoreboard
|
||||
plugins (football, hockey, baseball/MLB, basketball, soccer, lacrosse,
|
||||
F1, UFC), the odds ticker, and the leaderboard plugin. Each plugin's
|
||||
`background_service` block (under its own config namespace) follows the
|
||||
same shape as the example above.
|
||||
|
||||
### Error Handling & Fallback
|
||||
|
||||
@@ -830,9 +837,6 @@ Enable background service per plugin in `config/config.json`:
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run background service test
|
||||
python test_background_service.py
|
||||
|
||||
# Check logs for background operations
|
||||
sudo journalctl -u ledmatrix -f | grep "background"
|
||||
```
|
||||
@@ -841,9 +845,10 @@ sudo journalctl -u ledmatrix -f | grep "background"
|
||||
|
||||
**View Statistics:**
|
||||
```python
|
||||
from src.background_data_service import BackgroundDataService
|
||||
from src.background_data_service import get_background_service
|
||||
from src.cache_manager import CacheManager
|
||||
|
||||
service = BackgroundDataService()
|
||||
service = get_background_service(CacheManager())
|
||||
stats = service.get_statistics()
|
||||
print(f"Active tasks: {stats['active_tasks']}")
|
||||
print(f"Completed: {stats['completed']}")
|
||||
@@ -884,6 +889,7 @@ from src.common.permission_utils import (
|
||||
ensure_file_permissions,
|
||||
get_config_file_mode,
|
||||
get_assets_file_mode,
|
||||
get_assets_dir_mode,
|
||||
get_plugin_file_mode,
|
||||
get_cache_dir_mode
|
||||
)
|
||||
@@ -892,7 +898,10 @@ from src.common.permission_utils import (
|
||||
ensure_directory_permissions(Path("assets/sports"), get_assets_dir_mode())
|
||||
|
||||
# Set file permissions after writing
|
||||
ensure_file_permissions(Path("config/config.json"), get_config_file_mode())
|
||||
# (get_config_file_mode requires the file path — secrets files get a
|
||||
# stricter mode than the main config)
|
||||
config_path = Path("config/config.json")
|
||||
ensure_file_permissions(config_path, get_config_file_mode(config_path))
|
||||
```
|
||||
|
||||
### When to Use Utilities
|
||||
@@ -947,7 +956,7 @@ from src.common.permission_utils import ensure_file_permissions, get_config_file
|
||||
config_path = Path("config/config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(data, f)
|
||||
ensure_file_permissions(config_path, get_config_file_mode())
|
||||
ensure_file_permissions(config_path, get_config_file_mode(config_path))
|
||||
```
|
||||
|
||||
**Pattern 3: Downloading Logo**
|
||||
@@ -993,8 +1002,11 @@ These core utilities **already handle permissions** - you don't need to call per
|
||||
If you encounter permission issues:
|
||||
|
||||
```bash
|
||||
# Fix all permissions at once
|
||||
sudo ./scripts/fix_permissions.sh
|
||||
# Targeted permission fixes (see scripts/fix_perms/README.md)
|
||||
sudo ./scripts/fix_perms/fix_assets_permissions.sh # assets/ tree (logos, fonts)
|
||||
sudo ./scripts/fix_perms/fix_cache_permissions.sh # all cache directories
|
||||
sudo ./scripts/fix_perms/fix_plugin_permissions.sh # plugin directories
|
||||
sudo ./scripts/fix_perms/fix_web_permissions.sh # web interface files
|
||||
|
||||
# Fix specific directory
|
||||
sudo chown -R ledpi:ledpi /home/ledpi/LEDMatrix/config
|
||||
@@ -1026,7 +1038,7 @@ stat -c "%a %n" config/config.json
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [PLUGIN_DEVELOPMENT.md](PLUGIN_DEVELOPMENT.md) - Creating plugins with Vegas/on-demand support
|
||||
- [PLUGIN_DEVELOPMENT_GUIDE.md](PLUGIN_DEVELOPMENT_GUIDE.md) - Creating plugins with Vegas/on-demand support
|
||||
- [WEB_INTERFACE_GUIDE.md](WEB_INTERFACE_GUIDE.md) - Using on-demand controls in web UI
|
||||
- [PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md) - Complete API documentation
|
||||
- [DEVELOPMENT.md](DEVELOPMENT.md) - Development environment and testing
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
Advanced patterns, examples, and best practices for developing LEDMatrix plugins.
|
||||
|
||||
> **Adaptive layout:** for plugins that should render legibly on any panel
|
||||
> size (fonts that grow on big panels, layouts that degrade gracefully on
|
||||
> small ones), use the adaptive layout system — `self.layout`, `draw_fit`,
|
||||
> `draw_image`, `scoreboard_regions` — documented in
|
||||
> [ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Using Weather Icons](#using-weather-icons)
|
||||
|
||||
@@ -250,19 +250,29 @@ WARNING - Plugin ID 'Football-Scoreboard' may conflict with 'football-scoreboard
|
||||
|
||||
## Checking Configuration via API
|
||||
|
||||
The API blueprint mounts at `/api/v3` (`web_interface/app.py:144`).
|
||||
|
||||
```bash
|
||||
# Get current config
|
||||
curl http://localhost:5000/api/v3/config
|
||||
# Get full main config (includes all plugin sections)
|
||||
curl http://localhost:5000/api/v3/config/main
|
||||
|
||||
# Get specific plugin config
|
||||
curl http://localhost:5000/api/v3/config/plugin/football-scoreboard
|
||||
|
||||
# Validate config without saving
|
||||
curl -X POST http://localhost:5000/api/v3/config/validate \
|
||||
# Save updated main config
|
||||
curl -X POST http://localhost:5000/api/v3/config/main \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"football-scoreboard": {"enabled": true}}'
|
||||
-d @new-config.json
|
||||
|
||||
# Get config schema for a specific plugin
|
||||
curl "http://localhost:5000/api/v3/plugins/schema?plugin_id=football-scoreboard"
|
||||
|
||||
# Get a single plugin's current config
|
||||
curl "http://localhost:5000/api/v3/plugins/config?plugin_id=football-scoreboard"
|
||||
```
|
||||
|
||||
> There is no dedicated `/config/plugin/<id>` or `/config/validate`
|
||||
> endpoint — config validation runs server-side automatically when you
|
||||
> POST to `/config/main` or `/plugins/config`. See
|
||||
> [REST_API_REFERENCE.md](REST_API_REFERENCE.md) for the full list.
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Manual Backup
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Configuration Reference
|
||||
|
||||
Every key in `config/config.json`, what it does, its default, and where the
|
||||
code reads it. The file is created from `config/config.template.json` on
|
||||
first run, and `ConfigManager._migrate_config()` merges any template keys
|
||||
added by later releases into your existing config (your values are never
|
||||
overwritten). Secrets live in `config/config_secrets.json` and are merged
|
||||
into the config at load time.
|
||||
|
||||
Most settings are editable from the web interface; this page documents the
|
||||
underlying keys for people editing `config.json` directly or writing
|
||||
tooling against it.
|
||||
|
||||
## Top level
|
||||
|
||||
| Key | Type / default | Meaning | Read by |
|
||||
|---|---|---|---|
|
||||
| `web_display_autostart` | bool, `true` | Whether the web interface service starts with the system | `scripts/utils/start_web_conditionally.py` |
|
||||
| `timezone` | string, `"America/New_York"` | IANA timezone for schedules and displays | `ConfigManager.get_timezone()` |
|
||||
| `target_fps` | int, `100` | Frame-rate ceiling for plugin rendering | `src/plugin_system/base_plugin.py`, `src/common/sports_scroll.py` |
|
||||
| `location` | object | `city` / `state` / `country`, offered to plugins that need a location (weather, etc.) | plugins via merged config |
|
||||
|
||||
## `schedule` — display on/off hours
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `enabled` | bool, `false` | Master switch for scheduled display on/off |
|
||||
| `mode` | `"global"` or `"per-day"`, template uses `"per-day"` | Whether one time range applies to all days or each day has its own |
|
||||
| `start_time` / `end_time` | `"HH:MM"`, `07:00`–`23:00` | Global-mode on/off times |
|
||||
| `days.<weekday>.{enabled,start_time,end_time}` | per-day objects | Per-day-mode overrides |
|
||||
|
||||
Read by `DisplayController` (`src/display_controller.py`, `_check_schedule`
|
||||
around line 603). Managed in the web UI under Schedule.
|
||||
|
||||
## `dim_schedule` — scheduled brightness dimming
|
||||
|
||||
Same shape as `schedule`, plus:
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `dim_brightness` | int, `30` | Brightness percentage applied while the dim window is active |
|
||||
|
||||
Read by `DisplayController` (`src/display_controller.py` around line 770;
|
||||
saved via `POST /api/v3/config/dim-schedule`). The display returns to
|
||||
`display.hardware.brightness` outside the window.
|
||||
|
||||
## `display.hardware` — matrix panel hardware
|
||||
|
||||
All keys map to the corresponding `rpi-rgb-led-matrix` options and are read
|
||||
in `DisplayManager` (`src/display_manager.py`, ~lines 270–295).
|
||||
|
||||
| Key | Type / default |
|
||||
|---|---|
|
||||
| `rows` / `cols` | int, `32` / `64` |
|
||||
| `chain_length` | int, `2` |
|
||||
| `parallel` | int, `1` |
|
||||
| `brightness` | int, `90` |
|
||||
| `hardware_mapping` | string, `"adafruit-hat"` (code default `"adafruit-hat-pwm"`) |
|
||||
| `scan_mode` | int, `0` |
|
||||
| `pwm_bits` | int, `9` (code default 10) |
|
||||
| `pwm_dither_bits` | int, `1` |
|
||||
| `pwm_lsb_nanoseconds` | int, `130` (code default 150) |
|
||||
| `disable_hardware_pulsing` | bool, `false` |
|
||||
| `inverse_colors` | bool, `false` |
|
||||
| `show_refresh_rate` | bool, `false` |
|
||||
| `led_rgb_sequence` | string, `"RGB"` |
|
||||
| `limit_refresh_rate_hz` | int, `100` (code default 90) |
|
||||
| `pixel_mapper_config` | string, `""` — e.g. `"U-mapper"` / `"Rotate:90"` |
|
||||
| `row_address_type` | int, `0` — non-standard panel row addressing |
|
||||
| `multiplexing` | int, `0` — panel multiplexing scheme |
|
||||
| `panel_type` | string, `""` — set to `"FM6126A"` or `"FM6127"` for panels needing init |
|
||||
|
||||
Where "code default" differs from the template value, the code default only
|
||||
applies if the key is missing entirely from your config.
|
||||
|
||||
## `display.runtime`
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `gpio_slowdown` | int, `3` | GPIO timing slowdown for faster Pis |
|
||||
| `rp1_rio` | int, `0` | RP1 RIO mode on Pi 5 (applied only if the installed matrix library supports it) |
|
||||
|
||||
## `display.double_sided`
|
||||
|
||||
Drives `_LogicalMatrix` in `src/display_manager.py` — renders the same
|
||||
logical image to multiple chained physical panels.
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `enabled` | bool, `false` | Mirror output across panel copies |
|
||||
| `copies` | int, `2` | Number of physical copies in the chain |
|
||||
| `axis` | `"horizontal"`, default | Axis along which panels are chained |
|
||||
|
||||
## `display` — other keys
|
||||
|
||||
| Key | Type / default | Meaning | Read by |
|
||||
|---|---|---|---|
|
||||
| `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` |
|
||||
| `use_short_date_format` | bool, `true` | Compact date rendering in sports scoreboards | `src/base_classes/sports/core.py` |
|
||||
| `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
|
||||
|
||||
Read by `src/vegas_mode/config.py` (`VegasScrollConfig.from_config`). See
|
||||
[ADVANCED_FEATURES.md](ADVANCED_FEATURES.md) for behavior details.
|
||||
|
||||
| Key | Type / default |
|
||||
|---|---|
|
||||
| `enabled` | bool, `false` |
|
||||
| `scroll_speed` | int, `50` (px/s) |
|
||||
| `separator_width` | int, `32` |
|
||||
| `plugin_order` | array, `[]` |
|
||||
| `excluded_plugins` | array, `[]` |
|
||||
| `target_fps` | int, `125` |
|
||||
| `buffer_ahead` | int, `2` |
|
||||
| `intra_plugin_gap` | int, `8` |
|
||||
| `render_width_pct` | int, `100` |
|
||||
| `min_content_separation` | int, `24` |
|
||||
| `min_cut_gap` | int, `6` |
|
||||
| `continuous_scroll` | bool, `true` |
|
||||
| `smooth_scroll` | bool, `true` |
|
||||
| `extend_threshold_screens` | float, `2.0` |
|
||||
| `auto_trim` | bool, `true` |
|
||||
| `trim_threshold` | int, `10` |
|
||||
| `content_padding` | int, `8` |
|
||||
| `min_plugin_width` | int, `8` |
|
||||
| `lead_in_width` | int, `0` |
|
||||
| `plugins_per_cycle` | int, `6` |
|
||||
| `max_plugin_width_ratio` | float, `3.0` |
|
||||
| `overflow_mode` | string, `"rotate"` |
|
||||
| `dynamic_duration_enabled` | bool, `true` |
|
||||
| `min_cycle_duration` | int, `60` |
|
||||
| `max_cycle_duration` | int, `240` |
|
||||
| `frame_based_scrolling` | bool, `true` — frame-count-based scroll stepping |
|
||||
| `scroll_delay` | float, `0.02` — seconds between scroll updates (~50 FPS) |
|
||||
|
||||
## `sync` — multi-display synchronization
|
||||
|
||||
Read by `src/common/sync_manager.py` and `src/display_controller.py`.
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `role` | `"standalone"` (default), `"leader"`, or `"follower"` | This device's role in a synced pair |
|
||||
| `port` | int, `5765` | TCP port used for sync traffic |
|
||||
| `follower_position` | `"left"` (default) or `"right"` | Which half of the combined image this follower renders (`src/display_controller.py:522`) |
|
||||
|
||||
## `plugin_system`
|
||||
|
||||
Read by the plugin loader/manager (`src/plugin_system/`).
|
||||
|
||||
| Key | Type / default | Meaning |
|
||||
|---|---|---|
|
||||
| `plugins_directory` | string, `"plugin-repos"` | Where the Plugin Store installs plugins |
|
||||
| `auto_discover` | bool, `true` | Scan the plugins directory at startup |
|
||||
| `auto_load_enabled` | bool, `true` | Load discovered plugins automatically |
|
||||
| `development_mode` | bool, `false` | Development conveniences in the web UI (editable under General settings) |
|
||||
|
||||
## Plugin config blocks
|
||||
|
||||
Every installed plugin stores its settings under a top-level key equal to
|
||||
its plugin id (the template ships one for the bundled `web-ui-info`
|
||||
plugin). The shape of each block is defined by that plugin's
|
||||
`config_schema.json`; common keys are `enabled` and `display_duration`.
|
||||
See [PLUGIN_CONFIG_CORE_PROPERTIES.md](PLUGIN_CONFIG_CORE_PROPERTIES.md).
|
||||
|
||||
## `config/config_secrets.json`
|
||||
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `github.api_token` | Optional GitHub token the Plugin Store uses to avoid API rate limits (`src/plugin_system/store_manager.py:348`) |
|
||||
| `<plugin-id>.*` | Secrets a plugin declares with `"x-secret": true` in its config schema; merged into that plugin's config at load time |
|
||||
@@ -0,0 +1,242 @@
|
||||
# Creating Skins
|
||||
|
||||
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 see it on your matrix, add to your plugin's section in `config/config.json`:
|
||||
|
||||
```json
|
||||
"baseball-scoreboard": {
|
||||
"skin": "my-skin",
|
||||
"skin_options": { }
|
||||
}
|
||||
```
|
||||
|
||||
or pick it from the **Visual Skin** dropdown in the web UI (it appears once a
|
||||
matching skin is installed). `"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>`), or submit it to the plugin registry as an
|
||||
entry with `"type": "skin"` (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.
|
||||
@@ -31,7 +31,7 @@ POST /api/v3/system/action
|
||||
|
||||
**Base URL**: `http://your-pi-ip:5000/api/v3`
|
||||
|
||||
See [API_REFERENCE.md](API_REFERENCE.md) for complete documentation.
|
||||
See [REST_API_REFERENCE.md](REST_API_REFERENCE.md) for complete documentation.
|
||||
|
||||
## Display Manager Quick Methods
|
||||
|
||||
@@ -48,6 +48,12 @@ display_manager.draw_text("Centered", centered=True) # Auto-center
|
||||
width = display_manager.get_text_width("Text", font)
|
||||
height = display_manager.get_font_height(font)
|
||||
|
||||
# Adaptive layout (recommended for multi-size support — text and images
|
||||
# that scale to any panel; see docs/ADAPTIVE_LAYOUT.md)
|
||||
rows = self.layout.bounds.inset(1).split_v(3, 1, gap=1)
|
||||
self.draw_fit("12:34", rows[0]) # largest crisp font that fits
|
||||
self.draw_image(logo, rows[1], mode="fill_height", crop_to_ink=True)
|
||||
|
||||
# Weather icons
|
||||
display_manager.draw_weather_icon("rain", x=10, y=10, size=16)
|
||||
|
||||
@@ -62,7 +68,7 @@ display_manager.defer_update(lambda: self.update_cache(), priority=0)
|
||||
# Basic caching
|
||||
cached = cache_manager.get("key", max_age=3600)
|
||||
cache_manager.set("key", data)
|
||||
cache_manager.delete("key")
|
||||
cache_manager.delete("key") # alias for clear_cache(key)
|
||||
|
||||
# Advanced caching
|
||||
data = cache_manager.get_cached_data_with_strategy("key", data_type="weather")
|
||||
@@ -184,12 +190,13 @@ def display(self, force_clear=False):
|
||||
|
||||
```
|
||||
LEDMatrix/
|
||||
├── plugins/ # Installed plugins
|
||||
├── plugin-repos/ # Installed plugins (default; plugins/ is only
|
||||
│ # for dev symlinks via scripts/dev/dev_plugin_setup.sh)
|
||||
├── config/
|
||||
│ ├── config.json # Main configuration
|
||||
│ └── config_secrets.json # API keys and secrets
|
||||
├── docs/ # Documentation
|
||||
│ ├── API_REFERENCE.md
|
||||
│ ├── REST_API_REFERENCE.md
|
||||
│ ├── PLUGIN_API_REFERENCE.md
|
||||
│ └── ...
|
||||
└── src/
|
||||
@@ -201,7 +208,7 @@ LEDMatrix/
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Complete API Reference](API_REFERENCE.md)
|
||||
- [Complete REST API Reference](REST_API_REFERENCE.md)
|
||||
- [Plugin API Reference](PLUGIN_API_REFERENCE.md)
|
||||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md)
|
||||
- [Advanced Patterns](ADVANCED_PLUGIN_DEVELOPMENT.md)
|
||||
|
||||
@@ -141,19 +141,27 @@ stage('Checkout') {
|
||||
|
||||
---
|
||||
|
||||
## Plugin Submodules
|
||||
## Plugins
|
||||
|
||||
Plugin submodules are located in the `plugins/` directory and are managed similarly:
|
||||
Plugins are **not** git submodules of this repository. The plugins
|
||||
directory (configured by `plugin_system.plugins_directory` in
|
||||
`config/config.json`, default `plugin-repos/`) is populated at install
|
||||
time by the plugin loader as users install plugins from the Plugin Store
|
||||
or from a GitHub URL via the web interface. Plugin source lives in a
|
||||
separate repository:
|
||||
[ChuckBuilds/ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins).
|
||||
|
||||
**Initialize all plugin submodules:**
|
||||
```bash
|
||||
git submodule update --init --recursive plugins/
|
||||
```
|
||||
To work on a plugin locally without going through the Plugin Store, clone
|
||||
that repo and symlink (or copy) the plugin directory into your configured
|
||||
plugins directory — by default `plugin-repos/<plugin-id>/`. The plugin
|
||||
loader will pick it up on the next display restart. The directory name
|
||||
must match the plugin's `id` in `manifest.json`.
|
||||
|
||||
**Initialize a specific plugin:**
|
||||
```bash
|
||||
git submodule update --init --recursive plugins/hockey-scoreboard
|
||||
```
|
||||
|
||||
For more information about plugins, see the [Plugin Development Guide](.cursor/plugins_guide.md) and [Plugin Architecture Specification](docs/PLUGIN_ARCHITECTURE_SPEC.md).
|
||||
For more information, see:
|
||||
|
||||
- [PLUGIN_DEVELOPMENT_GUIDE.md](PLUGIN_DEVELOPMENT_GUIDE.md) — end-to-end
|
||||
plugin development workflow
|
||||
- [PLUGIN_ARCHITECTURE_SPEC.md](PLUGIN_ARCHITECTURE_SPEC.md) — plugin system
|
||||
specification
|
||||
- [DEV_PREVIEW.md](DEV_PREVIEW.md) — preview plugins on a desktop without a
|
||||
Pi
|
||||
|
||||
@@ -6,6 +6,12 @@ Tools for rapid plugin development without deploying to the RPi.
|
||||
|
||||
Interactive web UI for tweaking plugin configs and seeing the rendered display in real time.
|
||||
|
||||
The size inputs have a preset dropdown with the harness's standard panel
|
||||
sizes, and the **All Sizes** button renders the current config at every
|
||||
harness size in a side-by-side gallery (`POST /api/render-matrix`) — the
|
||||
quickest way to eyeball adaptive-layout behavior across panels
|
||||
(see [ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md)).
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
|
||||
@@ -32,10 +32,15 @@ The LEDMatrix emulator allows you to run and test LEDMatrix displays on your com
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/LEDMatrix.git
|
||||
git clone --recurse-submodules https://github.com/ChuckBuilds/LEDMatrix.git
|
||||
cd LEDMatrix
|
||||
```
|
||||
|
||||
> The emulator does **not** require building the
|
||||
> `rpi-rgb-led-matrix-master` submodule (it uses `RGBMatrixEmulator`
|
||||
> instead), so `--recurse-submodules` is optional here. Run it anyway if
|
||||
> you also want to test the real-hardware code path.
|
||||
|
||||
### 2. Install Emulator Dependencies
|
||||
|
||||
Install the emulator-specific requirements:
|
||||
@@ -58,7 +63,8 @@ pip install -r requirements.txt
|
||||
|
||||
### 1. Emulator Configuration File
|
||||
|
||||
The emulator uses `emulator_config.json` for configuration. Here's the default configuration:
|
||||
The emulator uses `emulator_config.json` for configuration. Here's the
|
||||
default configuration as it ships in the repo:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -66,20 +72,21 @@ The emulator uses `emulator_config.json` for configuration. Here's the default c
|
||||
"pixel_size": 16,
|
||||
"pixel_style": "square",
|
||||
"pixel_glow": 6,
|
||||
"display_adapter": "pygame",
|
||||
"display_adapter": "browser",
|
||||
"allow_adapter_fallback": true,
|
||||
"icon_path": null,
|
||||
"emulator_title": null,
|
||||
"suppress_font_warnings": false,
|
||||
"suppress_adapter_load_errors": false,
|
||||
"browser": {
|
||||
"_comment": "For use with the browser adapter only.",
|
||||
"port": 8888,
|
||||
"target_fps": 24,
|
||||
"target_fps": 60,
|
||||
"fps_display": false,
|
||||
"quality": 70,
|
||||
"image_border": true,
|
||||
"debug_text": false,
|
||||
"image_format": "JPEG"
|
||||
"image_format": "JPEG",
|
||||
"open_immediately": false
|
||||
},
|
||||
"log_level": "info"
|
||||
}
|
||||
@@ -90,13 +97,13 @@ The emulator uses `emulator_config.json` for configuration. Here's the default c
|
||||
| Option | Description | Default | Values |
|
||||
|--------|-------------|---------|--------|
|
||||
| `pixel_outline` | Pixel border thickness | 0 | 0-5 |
|
||||
| `pixel_size` | Size of each pixel | 16 | 8-64 |
|
||||
| `pixel_size` | Size of each pixel | 16 | 1-64 (8–16 is typical for testing) |
|
||||
| `pixel_style` | Pixel shape | "square" | "square", "circle" |
|
||||
| `pixel_glow` | Glow effect intensity | 6 | 0-20 |
|
||||
| `display_adapter` | Display backend | "pygame" | "pygame", "browser" |
|
||||
| `display_adapter` | Display backend | "browser" | "browser", "pygame" |
|
||||
| `allow_adapter_fallback` | Fall back to another adapter if the configured one fails to load | true | true/false |
|
||||
| `emulator_title` | Window title | null | Any string |
|
||||
| `suppress_font_warnings` | Hide font warnings | false | true/false |
|
||||
| `suppress_adapter_load_errors` | Hide adapter errors | false | true/false |
|
||||
|
||||
### 3. Browser Adapter Configuration
|
||||
|
||||
@@ -105,18 +112,32 @@ When using the browser adapter, additional options are available:
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `port` | Web server port | 8888 |
|
||||
| `target_fps` | Target frames per second | 24 |
|
||||
| `target_fps` | Target frames per second | 60 |
|
||||
| `fps_display` | Show FPS counter | false |
|
||||
| `quality` | Image compression quality | 70 |
|
||||
| `image_border` | Show image border | true |
|
||||
| `debug_text` | Show debug information | false |
|
||||
| `image_format` | Image format | "JPEG" |
|
||||
| `open_immediately` | Open the browser page automatically on start | false |
|
||||
|
||||
## Running the Emulator
|
||||
|
||||
### 1. Set Environment Variable
|
||||
### 1. Use the `-e` Flag (Recommended)
|
||||
|
||||
Enable emulator mode by setting the `EMULATOR` environment variable:
|
||||
`run.py` accepts exactly two flags: `-e`/`--emulator` and
|
||||
`-d`/`--debug`.
|
||||
|
||||
```bash
|
||||
python3 run.py -e
|
||||
|
||||
# With verbose logging
|
||||
python3 run.py -e -d
|
||||
```
|
||||
|
||||
### 2. Alternative: Set the Environment Variable
|
||||
|
||||
You can also enable emulator mode via the `EMULATOR` environment
|
||||
variable:
|
||||
|
||||
**Windows (Command Prompt):**
|
||||
```cmd
|
||||
@@ -131,15 +152,6 @@ python run.py
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
export EMULATOR=true
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
### 2. Alternative: Direct Python Execution
|
||||
|
||||
You can also run the emulator directly:
|
||||
|
||||
```bash
|
||||
EMULATOR=true python3 run.py
|
||||
```
|
||||
@@ -147,7 +159,8 @@ EMULATOR=true python3 run.py
|
||||
### 3. Verify Emulator Mode
|
||||
|
||||
When running in emulator mode, you should see:
|
||||
- A window displaying the LED matrix simulation
|
||||
- The emulated matrix — a web page at `http://localhost:8888` with the
|
||||
default browser adapter, or a desktop window with the pygame adapter
|
||||
- Console output indicating emulator mode
|
||||
- No hardware initialization errors
|
||||
|
||||
@@ -155,7 +168,36 @@ When running in emulator mode, you should see:
|
||||
|
||||
LEDMatrix supports two display adapters for the emulator:
|
||||
|
||||
### 1. Pygame Adapter (Default)
|
||||
### 1. Browser Adapter (Default)
|
||||
|
||||
The browser adapter runs a web server and displays the matrix as a web
|
||||
page at `http://localhost:8888`. This is the adapter the shipped
|
||||
`emulator_config.json` uses.
|
||||
|
||||
**Features:**
|
||||
- Web-based interface
|
||||
- Remote access capability
|
||||
- Mobile-friendly
|
||||
- Screenshot capture
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"display_adapter": "browser",
|
||||
"browser": {
|
||||
"port": 8888,
|
||||
"target_fps": 60,
|
||||
"quality": 70
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
1. Start the emulator (`python3 run.py -e`)
|
||||
2. Open browser to `http://localhost:8888`
|
||||
3. View the LED matrix display
|
||||
|
||||
### 2. Pygame Adapter (Alternative)
|
||||
|
||||
The pygame adapter provides a native desktop window with real-time display.
|
||||
|
||||
@@ -180,33 +222,6 @@ The pygame adapter provides a native desktop window with real-time display.
|
||||
- `+/-` - Zoom in/out
|
||||
- `R` - Reset zoom
|
||||
|
||||
### 2. Browser Adapter
|
||||
|
||||
The browser adapter runs a web server and displays the matrix in a web browser.
|
||||
|
||||
**Features:**
|
||||
- Web-based interface
|
||||
- Remote access capability
|
||||
- Mobile-friendly
|
||||
- Screenshot capture
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"display_adapter": "browser",
|
||||
"browser": {
|
||||
"port": 8888,
|
||||
"target_fps": 24,
|
||||
"quality": 70
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
1. Start the emulator with browser adapter
|
||||
2. Open browser to `http://localhost:8888`
|
||||
3. View the LED matrix display
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
@@ -268,8 +283,7 @@ Enable debug logging:
|
||||
```json
|
||||
{
|
||||
"log_level": "debug",
|
||||
"suppress_font_warnings": false,
|
||||
"suppress_adapter_load_errors": false
|
||||
"suppress_font_warnings": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -293,17 +307,18 @@ Modify the display dimensions in your main config:
|
||||
|
||||
### 2. Plugin Development
|
||||
|
||||
For plugin development with the emulator:
|
||||
`run.py` always runs the full rotation — it has no single-plugin flag.
|
||||
To preview or check one plugin in isolation, use the dev tools:
|
||||
|
||||
```bash
|
||||
# Enable emulator mode
|
||||
export EMULATOR=true
|
||||
# Run the full display in emulator mode (optionally with debug logging)
|
||||
python3 run.py -e -d
|
||||
|
||||
# Run with specific plugin
|
||||
python run.py --plugin my-plugin
|
||||
# Live single-plugin preview in the browser (port 5001)
|
||||
python3 scripts/dev_server.py
|
||||
|
||||
# Debug mode
|
||||
python run.py --debug
|
||||
# Headless render/validation of one plugin
|
||||
python3 scripts/check_plugin.py --plugin my-plugin
|
||||
```
|
||||
|
||||
### 3. Performance Tuning
|
||||
@@ -338,11 +353,10 @@ The emulator can work alongside the web interface:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start emulator
|
||||
export EMULATOR=true
|
||||
python run.py
|
||||
python3 run.py -e
|
||||
|
||||
# Terminal 2: Start web interface
|
||||
python web_interface/app.py
|
||||
# Terminal 2: Start web interface (supported entry point)
|
||||
python3 web_interface/start.py
|
||||
```
|
||||
|
||||
Access the web interface at `http://localhost:5000` while the emulator runs.
|
||||
@@ -359,13 +373,14 @@ Access the web interface at `http://localhost:5000` while the emulator runs.
|
||||
### 2. Plugin Testing
|
||||
|
||||
```bash
|
||||
# Test specific plugin
|
||||
export EMULATOR=true
|
||||
python run.py --plugin clock-simple
|
||||
# Test a specific plugin (headless check)
|
||||
python3 scripts/check_plugin.py --plugin clock-simple
|
||||
|
||||
# Test all plugins
|
||||
export EMULATOR=true
|
||||
python run.py --test-plugins
|
||||
# Preview a single plugin live in the browser (port 5001)
|
||||
python3 scripts/dev_server.py
|
||||
|
||||
# Test the full rotation in the emulator
|
||||
python3 run.py -e
|
||||
```
|
||||
|
||||
### 3. Configuration Management
|
||||
@@ -379,9 +394,8 @@ python run.py --test-plugins
|
||||
### Basic Clock Display
|
||||
|
||||
```bash
|
||||
# Start emulator with clock
|
||||
export EMULATOR=true
|
||||
python run.py
|
||||
# Start emulator with clock enabled in config.json
|
||||
python3 run.py -e
|
||||
```
|
||||
|
||||
### Sports Scores
|
||||
@@ -389,16 +403,16 @@ python run.py
|
||||
```bash
|
||||
# Configure for sports display
|
||||
# Edit config/config.json to enable sports plugins
|
||||
export EMULATOR=true
|
||||
python run.py
|
||||
python3 run.py -e
|
||||
```
|
||||
|
||||
### Custom Text Display
|
||||
|
||||
```bash
|
||||
# Use text display plugin
|
||||
export EMULATOR=true
|
||||
python run.py --plugin text-display --text "Hello World"
|
||||
# Preview the text display plugin on its own
|
||||
python3 scripts/check_plugin.py --plugin text-display
|
||||
# or use the live dev preview server
|
||||
python3 scripts/dev_server.py
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# FontManager Usage Guide
|
||||
|
||||
> **Picking a size automatically:** if you want the *largest font that fits
|
||||
> a given area* rather than a fixed size, use the adaptive layout system's
|
||||
> font ladders, which resolve through this FontManager. `BasePlugin`
|
||||
> subclasses get this as `self.layout.fit_text(...)`; other code can build
|
||||
> a `LayoutContext(width, height, font_manager)` directly — see
|
||||
> [ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md).
|
||||
|
||||
## Overview
|
||||
|
||||
The enhanced FontManager provides comprehensive font management for the LEDMatrix application with support for:
|
||||
@@ -138,6 +145,27 @@ font = self.font_manager.resolve_font(
|
||||
|
||||
## For Plugin Developers
|
||||
|
||||
> **Note**: plugins that ship their own fonts via a `"fonts"` block
|
||||
> in `manifest.json` are registered automatically during plugin load
|
||||
> (`src/plugin_system/plugin_manager.py` calls
|
||||
> `FontManager.register_plugin_fonts()`). The `plugin://…` source
|
||||
> URIs documented below are resolved relative to the plugin's
|
||||
> install directory.
|
||||
>
|
||||
> The **Fonts** tab in the web UI that lists detected
|
||||
> manager-registered fonts is still a **placeholder
|
||||
> implementation** — fonts that managers register through
|
||||
> `register_manager_font()` do not yet appear there. The
|
||||
> programmatic per-element override workflow described in
|
||||
> [Manual Font Overrides](#manual-font-overrides) below
|
||||
> (`set_override()` / `remove_override()` / the
|
||||
> `config/font_overrides.json` store) **does** work today and is
|
||||
> the supported way to override a font for an element until the
|
||||
> Fonts tab is wired up. If you can't wait and need a workaround
|
||||
> right now, you can also just load the font directly with PIL
|
||||
> (or `freetype-py` for BDF) inside your plugin's `manager.py`
|
||||
> and skip the override system entirely.
|
||||
|
||||
### Plugin Font Registration
|
||||
|
||||
In your plugin's `manifest.json`:
|
||||
@@ -359,5 +387,8 @@ self.font = self.font_manager.resolve_font(
|
||||
|
||||
## Example: Complete Manager Implementation
|
||||
|
||||
See `test/font_manager_example.py` for a complete working example.
|
||||
For a working example of the font manager API in use, see
|
||||
`src/font_manager.py` itself and the bundled scoreboard base classes
|
||||
in `src/base_classes/` (e.g., `hockey.py`, `football.py`) which
|
||||
register and resolve fonts via the patterns documented above.
|
||||
|
||||
|
||||
@@ -21,25 +21,37 @@ This guide will help you set up your LEDMatrix display for the first time and ge
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (5 Minutes)
|
||||
## Quick Start
|
||||
|
||||
### 1. First Boot
|
||||
### 1. Install LEDMatrix
|
||||
|
||||
1. Insert the MicroSD card with LEDMatrix installed
|
||||
2. Connect the LED matrix to your Raspberry Pi
|
||||
3. Plug in the power supply
|
||||
4. Wait for the Pi to boot (about 60 seconds)
|
||||
There is no prebuilt SD card image — you install LEDMatrix onto stock
|
||||
Raspberry Pi OS Lite yourself:
|
||||
|
||||
**Expected Behavior:**
|
||||
1. Flash Raspberry Pi OS Lite to the MicroSD card (Raspberry Pi Imager)
|
||||
2. Connect the LED matrix to your Raspberry Pi, insert the card, and
|
||||
power on
|
||||
3. SSH into the Pi and run the one-shot installer:
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/ChuckBuilds/LEDMatrix/main/scripts/install/one-shot-install.sh | bash
|
||||
```
|
||||
or clone the repo and run `sudo ./first_time_install.sh` — see the
|
||||
[README Installation Steps / Quick Install](../README.md#installation-steps)
|
||||
for full details
|
||||
|
||||
**Expected Behavior after install:**
|
||||
- LED matrix will light up
|
||||
- Display will show default plugins (clock, weather, etc.)
|
||||
- A fresh install ships only the bundled `starlark-apps` and
|
||||
`web-ui-info` plugins — clock, weather, sports, etc. must be
|
||||
installed from the Plugin Store (web UI → Plugin Manager) before
|
||||
anything else displays
|
||||
- Pi creates WiFi network "LEDMatrix-Setup" if not connected
|
||||
|
||||
### 2. Connect to WiFi
|
||||
|
||||
**If you see "LEDMatrix-Setup" WiFi network:**
|
||||
1. Connect your device to "LEDMatrix-Setup" (open network, no password)
|
||||
2. Open browser to: `http://192.168.4.1:5050`
|
||||
2. Open browser to: `http://192.168.4.1:5000`
|
||||
3. Navigate to the WiFi tab
|
||||
4. Click "Scan" to find your WiFi network
|
||||
5. Select your network, enter password
|
||||
@@ -48,14 +60,14 @@ This guide will help you set up your LEDMatrix display for the first time and ge
|
||||
|
||||
**If already connected to WiFi:**
|
||||
1. Find your Pi's IP address (check your router, or run `hostname -I` on the Pi)
|
||||
2. Open browser to: `http://your-pi-ip:5050`
|
||||
2. Open browser to: `http://your-pi-ip:5000`
|
||||
|
||||
### 3. Access the Web Interface
|
||||
|
||||
Once connected, access the web interface:
|
||||
|
||||
```
|
||||
http://your-pi-ip:5050
|
||||
http://your-pi-ip:5000
|
||||
```
|
||||
|
||||
You should see:
|
||||
@@ -69,84 +81,89 @@ You should see:
|
||||
|
||||
### Step 1: Configure Display Hardware
|
||||
|
||||
1. Navigate to Settings → **Display Settings**
|
||||
1. Open the **Display** tab
|
||||
2. Set your matrix configuration:
|
||||
- **Rows**: 32 or 64 (match your hardware)
|
||||
- **Columns**: 64, 128, or 256 (match your hardware)
|
||||
- **Chain Length**: Number of panels chained together
|
||||
- **Brightness**: 50-75% recommended for indoor use
|
||||
3. Click **Save Configuration**
|
||||
4. Click **Restart Display** to apply changes
|
||||
- **Columns**: commonly 64 or 96; the web UI accepts any integer
|
||||
in the 1–128 range, but 64 and 96 are the values the bundled
|
||||
panel hardware ships with
|
||||
- **Chain Length**: Number of panels chained horizontally
|
||||
- **Hardware Mapping**: usually `adafruit-hat-pwm` (with the PWM jumper
|
||||
mod) or `adafruit-hat` (without). See the root README for the full list.
|
||||
- **Brightness**: 70–90 is fine for indoor use
|
||||
3. Click **Save**
|
||||
4. From the **Overview** tab, click **Restart Display Service** to apply
|
||||
|
||||
**Tip:** If the display doesn't look right, try different hardware mapping options.
|
||||
**Tip:** if the display shows garbage or nothing, the most common culprits
|
||||
are an incorrect `hardware_mapping`, a `gpio_slowdown` value that doesn't
|
||||
match your Pi model, or panels needing the E-line mod. See
|
||||
[TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
||||
|
||||
### Step 2: Set Timezone and Location
|
||||
|
||||
1. Navigate to Settings → **General Settings**
|
||||
2. Set your timezone (e.g., "America/New_York")
|
||||
3. Set your location (city, state, country)
|
||||
4. Click **Save Configuration**
|
||||
1. Open the **General** tab
|
||||
2. Set your timezone (e.g., `America/New_York`) and location
|
||||
3. Click **Save**
|
||||
|
||||
**Why it matters:** Correct timezone ensures accurate time display. Location enables weather and location-based features.
|
||||
Correct timezone ensures accurate time display, and location is used by
|
||||
weather and other location-aware plugins.
|
||||
|
||||
### Step 3: Install Plugins
|
||||
|
||||
1. Navigate to **Plugin Store** tab
|
||||
2. Browse available plugins:
|
||||
- **Time & Date**: Clock, calendar
|
||||
- **Weather**: Weather forecasts
|
||||
- **Sports**: NHL, NBA, NFL, MLB scores
|
||||
- **Finance**: Stocks, crypto
|
||||
- **Custom**: Community plugins
|
||||
3. Click **Install** on desired plugins
|
||||
4. Wait for installation to complete
|
||||
5. Navigate to **Plugin Management** tab
|
||||
6. Enable installed plugins (toggle switch)
|
||||
7. Click **Restart Display**
|
||||
1. Open the **Plugin Manager** tab
|
||||
2. Scroll to the **Plugin Store** section to browse available plugins
|
||||
3. Click **Install** on the plugins you want
|
||||
4. Wait for installation to finish — installed plugins appear in the
|
||||
**Installed Plugins** section above and get their own tab in the second
|
||||
nav row
|
||||
5. Toggle the plugin to enabled
|
||||
6. From **Overview**, click **Restart Display Service**
|
||||
|
||||
**Popular First Plugins:**
|
||||
- `clock-simple` - Simple digital clock
|
||||
- `weather` - Weather forecast
|
||||
- `nhl-scores` - NHL scores (if you're a hockey fan)
|
||||
You can also install community plugins straight from a GitHub URL using the
|
||||
**Install from GitHub** section further down the same tab — see
|
||||
[PLUGIN_STORE_GUIDE.md](PLUGIN_STORE_GUIDE.md) for details.
|
||||
|
||||
### Step 4: Configure Plugins
|
||||
|
||||
1. Navigate to **Plugin Management** tab
|
||||
2. Find a plugin you installed
|
||||
3. Click the ⚙️ **Configure** button
|
||||
4. Edit settings (e.g., favorite teams, update intervals)
|
||||
5. Click **Save**
|
||||
6. Click **Restart Display**
|
||||
1. Each installed plugin gets its own tab in the second navigation row
|
||||
2. Open that plugin's tab to edit its settings (favorite teams, API keys,
|
||||
update intervals, etc.)
|
||||
3. Click **Save**
|
||||
4. Restart the display service from **Overview** so the new settings take
|
||||
effect
|
||||
|
||||
**Note:** how long each plugin stays on screen is not set in the
|
||||
plugin's own tab — use the **Rotation** tab's **Screen Durations**
|
||||
section instead (saved to `display.display_durations` in
|
||||
`config.json`).
|
||||
|
||||
**Example: Weather Plugin**
|
||||
- Set your location (city, state, country)
|
||||
- Add API key from OpenWeatherMap (free signup)
|
||||
- Set update interval (300 seconds recommended)
|
||||
- Add an API key from OpenWeatherMap (free signup) to
|
||||
`config/config_secrets.json` or directly in the plugin's config screen
|
||||
- Set the update interval (300 seconds is reasonable)
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Display
|
||||
|
||||
### Quick Test
|
||||
### Run a single plugin on demand
|
||||
|
||||
1. Navigate to **Overview** tab
|
||||
2. Click **Test Display** button
|
||||
3. You should see a test pattern on your LED matrix
|
||||
The fastest way to verify a plugin works without waiting for the rotation:
|
||||
|
||||
### Manual Plugin Trigger
|
||||
1. Open the plugin's tab (second nav row)
|
||||
2. Scroll to **On-Demand Controls**
|
||||
3. Click **Run On-Demand** — the plugin runs immediately even if disabled
|
||||
4. Click **Stop On-Demand** to return to the normal rotation
|
||||
|
||||
1. Navigate to **Plugin Management** tab
|
||||
2. Find a plugin
|
||||
3. Click **Show Now** button
|
||||
4. The plugin should display immediately
|
||||
5. Click **Stop** to return to rotation
|
||||
### Check the live preview and logs
|
||||
|
||||
### Check Logs
|
||||
|
||||
1. Navigate to **Logs** tab
|
||||
2. Watch real-time logs
|
||||
3. Look for any ERROR messages
|
||||
4. Normal operation shows INFO messages about plugin rotation
|
||||
- The **Overview** tab shows a **Live Display Preview** that mirrors what's
|
||||
on the matrix in real time — handy for debugging without looking at the
|
||||
panel.
|
||||
- The **Logs** tab streams the display and web service logs. Look for
|
||||
`ERROR` lines if something isn't working; normal operation just shows
|
||||
`INFO` messages about plugin rotation.
|
||||
|
||||
---
|
||||
|
||||
@@ -156,12 +173,12 @@ You should see:
|
||||
|
||||
**Check:**
|
||||
1. Power supply connected and adequate (5V, 4A minimum)
|
||||
2. LED matrix connected to GPIO pins correctly
|
||||
2. LED matrix connected to the bonnet/HAT correctly
|
||||
3. Display service running: `sudo systemctl status ledmatrix`
|
||||
4. Hardware configuration matches your matrix (rows/columns)
|
||||
4. Hardware configuration matches your matrix (rows/cols/chain length)
|
||||
|
||||
**Fix:**
|
||||
1. Restart display: Settings → Overview → Restart Display
|
||||
1. Restart from the **Overview** tab → **Restart Display Service**
|
||||
2. Or via SSH: `sudo systemctl restart ledmatrix`
|
||||
|
||||
### Web Interface Won't Load
|
||||
@@ -169,8 +186,8 @@ You should see:
|
||||
**Check:**
|
||||
1. Pi is connected to network: `ping your-pi-ip`
|
||||
2. Web service running: `sudo systemctl status ledmatrix-web`
|
||||
3. Correct port: Use `:5050` not `:5000`
|
||||
4. Firewall not blocking port 5050
|
||||
3. Correct port: the web UI listens on `:5000`
|
||||
4. Firewall not blocking port 5000
|
||||
|
||||
**Fix:**
|
||||
1. Restart web service: `sudo systemctl restart ledmatrix-web`
|
||||
@@ -179,15 +196,15 @@ You should see:
|
||||
### Plugins Not Showing
|
||||
|
||||
**Check:**
|
||||
1. Plugins are enabled (toggle switch in Plugin Management)
|
||||
2. Display has been restarted after enabling
|
||||
3. Plugin duration is reasonable (not too short)
|
||||
4. No errors in logs for the plugin
|
||||
1. Plugin is enabled (toggle on the **Plugin Manager** tab)
|
||||
2. Display service was restarted after enabling
|
||||
3. Plugin's display duration is non-zero
|
||||
4. No errors in the **Logs** tab for that plugin
|
||||
|
||||
**Fix:**
|
||||
1. Enable plugin in Plugin Management
|
||||
2. Restart display
|
||||
3. Check logs for plugin-specific errors
|
||||
1. Enable the plugin from **Plugin Manager**
|
||||
2. Click **Restart Display Service** on **Overview**
|
||||
3. Check the **Logs** tab for plugin-specific errors
|
||||
|
||||
### Weather Plugin Shows "No Data"
|
||||
|
||||
@@ -207,18 +224,20 @@ You should see:
|
||||
|
||||
### Customize Your Display
|
||||
|
||||
**Adjust Display Durations:**
|
||||
- Navigate to Settings → Durations
|
||||
- Set how long each plugin displays
|
||||
- Save and restart
|
||||
**Adjust display durations:**
|
||||
- Open the **Rotation** tab and use the **Screen Durations** section to
|
||||
set how long each plugin stays on screen per rotation (saved to
|
||||
`display.display_durations`).
|
||||
|
||||
**Organize Plugin Order:**
|
||||
- Use Plugin Management to enable/disable plugins
|
||||
- Display cycles through enabled plugins in order
|
||||
**Organize plugin order:**
|
||||
- The **Rotation** tab also has a drag-and-drop **Rotation Order** list
|
||||
(saved to `display.plugin_rotation_order`). Enable/disable plugins
|
||||
from the **Plugin Manager** tab.
|
||||
|
||||
**Add More Plugins:**
|
||||
- Check Plugin Store regularly for new plugins
|
||||
- Install from GitHub URLs for custom/community plugins
|
||||
**Add more plugins:**
|
||||
- Check the **Plugin Store** section of **Plugin Manager** for new plugins.
|
||||
- Install community plugins straight from a GitHub URL via
|
||||
**Install from GitHub** on the same tab.
|
||||
|
||||
### Enable Advanced Features
|
||||
|
||||
@@ -279,26 +298,46 @@ sudo journalctl -u ledmatrix-web -f
|
||||
│ ├── config.json # Main configuration
|
||||
│ ├── config_secrets.json # API keys and secrets
|
||||
│ └── wifi_config.json # WiFi settings
|
||||
├── plugins/ # Installed plugins
|
||||
├── cache/ # Cached data
|
||||
├── plugin-repos/ # Installed plugins (default location)
|
||||
└── web_interface/ # Web interface files
|
||||
```
|
||||
|
||||
> Cached data does not live in the project directory — the cache manager
|
||||
> uses the first writable location among `/var/cache/ledmatrix`,
|
||||
> `~/.ledmatrix_cache`, `/opt/ledmatrix/cache`, and
|
||||
> `$TMPDIR/ledmatrix_cache`.
|
||||
>
|
||||
> The plugin install location is configurable via
|
||||
> `plugin_system.plugins_directory` in `config.json`. The default is
|
||||
> `plugin-repos/`. Plugin discovery (`PluginManager.discover_plugins()`)
|
||||
> only scans the configured directory — it does not fall back to
|
||||
> `plugins/`. However, the Plugin Store install/update path and the
|
||||
> web UI's schema loader do also probe `plugins/` so the dev symlinks
|
||||
> created by `scripts/dev/dev_plugin_setup.sh` keep working.
|
||||
|
||||
### Web Interface
|
||||
|
||||
```
|
||||
Main Interface: http://your-pi-ip:5050
|
||||
Main Interface: http://your-pi-ip:5000
|
||||
|
||||
Tabs:
|
||||
- Overview: System stats and quick actions
|
||||
- General Settings: Timezone, location, autostart
|
||||
- Display Settings: Hardware configuration
|
||||
- Durations: Plugin display times
|
||||
- Sports Configuration: Per-league settings
|
||||
- Plugin Management: Enable/disable, configure
|
||||
- Plugin Store: Install new plugins
|
||||
- Font Management: Upload and manage fonts
|
||||
- Logs: Real-time log viewing
|
||||
System tabs:
|
||||
- Overview System stats, live preview, quick actions
|
||||
- General Timezone, location, plugin-system settings
|
||||
- WiFi Network selection and AP-mode setup
|
||||
- Schedule Power and dim schedules
|
||||
- Display Matrix hardware configuration
|
||||
- Rotation Rotation order (drag-and-drop) and screen durations
|
||||
- Config Editor Raw config.json editor
|
||||
- Backup & Restore Config backup and restore
|
||||
- Fonts Upload and manage fonts
|
||||
- Logs Real-time log viewing
|
||||
- Cache Cached data inspection and cleanup
|
||||
- Operation History Recent service operations
|
||||
- Tools System diagnostics, updates, dependencies, maintenance
|
||||
|
||||
Plugin tabs (second row):
|
||||
- Plugin Manager Browse the Plugin Store, install/enable plugins
|
||||
- <plugin-id> One tab per installed plugin for its config
|
||||
```
|
||||
|
||||
### WiFi Access Point
|
||||
@@ -306,7 +345,7 @@ Tabs:
|
||||
```
|
||||
Network Name: LEDMatrix-Setup
|
||||
Password: (none - open network)
|
||||
URL when connected: http://192.168.4.1:5050
|
||||
URL when connected: http://192.168.4.1:5000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -10,10 +10,7 @@ Make sure you have the testing packages installed:
|
||||
|
||||
```bash
|
||||
# Install all dependencies including test packages
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Or install just the test dependencies
|
||||
pip install pytest pytest-cov pytest-mock pytest-timeout
|
||||
pip install -r requirements.txt -r requirements-test.txt
|
||||
```
|
||||
|
||||
### 2. Set Environment Variables
|
||||
@@ -85,8 +82,14 @@ pytest -m slow
|
||||
# Run all tests in the test directory
|
||||
pytest test/
|
||||
|
||||
# Run all integration tests
|
||||
pytest test/integration/
|
||||
# Run plugin tests only
|
||||
pytest test/plugins/
|
||||
|
||||
# Run web interface tests only
|
||||
pytest test/web_interface/
|
||||
|
||||
# Run web interface integration tests
|
||||
pytest test/web_interface/integration/
|
||||
```
|
||||
|
||||
## Understanding Test Output
|
||||
@@ -231,20 +234,39 @@ pytest --maxfail=3
|
||||
|
||||
```
|
||||
test/
|
||||
├── conftest.py # Shared fixtures and configuration
|
||||
├── test_display_controller.py # Display controller tests
|
||||
├── test_plugin_system.py # Plugin system tests
|
||||
├── test_display_manager.py # Display manager tests
|
||||
├── test_config_service.py # Config service tests
|
||||
├── test_cache_manager.py # Cache manager tests
|
||||
├── test_font_manager.py # Font manager tests
|
||||
├── test_error_handling.py # Error handling tests
|
||||
├── test_config_manager.py # Config manager tests
|
||||
├── integration/ # Integration tests
|
||||
│ ├── test_e2e.py # End-to-end tests
|
||||
│ └── test_plugin_integration.py # Plugin integration tests
|
||||
├── test_error_scenarios.py # Error scenario tests
|
||||
└── test_edge_cases.py # Edge case tests
|
||||
├── conftest.py # Shared fixtures and configuration
|
||||
├── test_display_controller.py # Display controller tests
|
||||
├── test_display_manager.py # Display manager tests
|
||||
├── test_plugin_system.py # Plugin system tests
|
||||
├── test_plugin_loader.py # Plugin discovery/loading tests
|
||||
├── test_plugin_loading_failures.py # Plugin failure-mode tests
|
||||
├── test_cache_manager.py # Cache manager tests
|
||||
├── test_config_manager.py # Config manager tests
|
||||
├── test_config_service.py # Config service tests
|
||||
├── test_config_validation_edge_cases.py # Config edge cases
|
||||
├── test_font_manager.py # Font manager tests
|
||||
├── test_text_helper.py # Text helper tests
|
||||
├── test_error_handling.py # Error handling tests
|
||||
├── test_error_aggregator.py # Error aggregation tests
|
||||
├── test_schema_manager.py # Schema manager tests
|
||||
├── test_web_api.py # Web API tests
|
||||
├── plugins/ # Per-plugin test suites
|
||||
│ ├── test_clock_simple.py
|
||||
│ ├── test_calendar.py
|
||||
│ ├── test_basketball_scoreboard.py
|
||||
│ ├── test_soccer_scoreboard.py
|
||||
│ ├── test_odds_ticker.py
|
||||
│ ├── test_text_display.py
|
||||
│ ├── test_visual_rendering.py
|
||||
│ └── test_plugin_base.py
|
||||
└── web_interface/
|
||||
├── test_config_manager_atomic.py
|
||||
├── test_state_reconciliation.py
|
||||
├── test_plugin_operation_queue.py
|
||||
├── test_dedup_unique_arrays.py
|
||||
└── integration/ # Web interface integration tests
|
||||
├── test_config_flows.py
|
||||
└── test_plugin_operations.py
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
@@ -277,7 +299,7 @@ If tests fail due to missing packages:
|
||||
|
||||
```bash
|
||||
# Install all dependencies
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements.txt -r requirements-test.txt
|
||||
|
||||
# Or install specific missing package
|
||||
pip install <package-name>
|
||||
@@ -309,11 +331,15 @@ pytest --cov=src --cov-report=html
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Tests are configured to run automatically in CI/CD. The GitHub Actions workflow (`.github/workflows/tests.yml`) runs:
|
||||
|
||||
- All tests on multiple Python versions (3.10, 3.11, 3.12)
|
||||
- Coverage reporting
|
||||
- Uploads coverage to Codecov (if configured)
|
||||
The repo runs the pytest suite via
|
||||
[`.github/workflows/test.yml`](../.github/workflows/test.yml) on every
|
||||
push and pull request: a plugin-safety job (harness, visual rendering
|
||||
and plugin-matrix tests) plus a unit-test job that runs an explicit
|
||||
allowlist of suites — new test files must be added to that list to run
|
||||
in CI. Release version consistency is checked by
|
||||
[`.github/workflows/release-version-check.yml`](../.github/workflows/release-version-check.yml).
|
||||
Bandit, flake8, mypy and gitleaks run as pre-commit hooks (see
|
||||
`.pre-commit-config.yaml`), not in CI.
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -86,10 +86,10 @@ The plugin system has been enhanced but remains backward compatible with existin
|
||||
|
||||
If you encounter issues during migration:
|
||||
|
||||
1. Check the [README.md](README.md) for current installation and usage instructions
|
||||
1. Check the [project root README](../README.md) for current installation and usage instructions
|
||||
2. Review script README files:
|
||||
- `scripts/install/README.md` - Installation scripts documentation
|
||||
- `scripts/fix_perms/README.md` (if exists) - Permission scripts documentation
|
||||
- [`scripts/install/README.md`](../scripts/install/README.md) - Installation scripts documentation
|
||||
- [`scripts/fix_perms/README.md`](../scripts/fix_perms/README.md) - Permission scripts documentation
|
||||
3. Check system logs: `journalctl -u ledmatrix -f` or `journalctl -u ledmatrix-web -f`
|
||||
4. Review the troubleshooting section in the main README
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
Complete API reference for plugin developers. This document describes all methods and properties available to plugins through the Display Manager, Cache Manager, and Plugin Manager.
|
||||
|
||||
> **Adaptive layout:** every `BasePlugin` also exposes `self.layout`,
|
||||
> `self.draw_fit(text, region)` and `self.draw_image(img, region, ...)` —
|
||||
> the recommended way to render text and images that scale to any panel
|
||||
> size. See [ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [BasePlugin](#baseplugin)
|
||||
@@ -114,6 +119,96 @@ Get display duration for this plugin. Can be overridden for dynamic durations.
|
||||
|
||||
Return plugin info for display in web UI. Override to provide additional state information.
|
||||
|
||||
### Dynamic-duration hooks
|
||||
|
||||
Plugins that render multi-step content (e.g. cycling through several games)
|
||||
can extend their display time until they've shown everything. To opt in,
|
||||
either set `dynamic_duration.enabled: true` in the plugin's config or
|
||||
override `supports_dynamic_duration()`.
|
||||
|
||||
#### `supports_dynamic_duration() -> bool`
|
||||
|
||||
Return `True` if this plugin should use dynamic durations. Default reads
|
||||
`config["dynamic_duration"]["enabled"]`.
|
||||
|
||||
#### `get_dynamic_duration_cap() -> Optional[float]`
|
||||
|
||||
Maximum number of seconds the controller will keep this plugin on screen
|
||||
in dynamic mode. Default reads
|
||||
`config["dynamic_duration"]["max_duration_seconds"]`.
|
||||
|
||||
#### `is_cycle_complete() -> bool`
|
||||
|
||||
Override this to return `True` only after the plugin has rendered all of
|
||||
its content for the current rotation. Default returns `True` immediately,
|
||||
which means a single `display()` call counts as a full cycle.
|
||||
|
||||
#### `reset_cycle_state() -> None`
|
||||
|
||||
Called by the controller before each new dynamic-duration session. Reset
|
||||
internal counters/iterators here.
|
||||
|
||||
### Live priority hooks
|
||||
|
||||
Live priority lets a plugin temporarily take over the rotation when it has
|
||||
urgent content (live games, breaking news). Enable by setting
|
||||
`live_priority: true` in the plugin's config and overriding
|
||||
`has_live_content()`.
|
||||
|
||||
#### `has_live_priority() -> bool`
|
||||
|
||||
Whether live priority is enabled in config (default reads
|
||||
`config["live_priority"]`).
|
||||
|
||||
#### `has_live_content() -> bool`
|
||||
|
||||
Override to return `True` when the plugin currently has urgent content.
|
||||
Default returns `False`.
|
||||
|
||||
#### `get_live_modes() -> List[str]`
|
||||
|
||||
List of display modes to show during a live takeover. Default returns the
|
||||
plugin's `display_modes` from its manifest.
|
||||
|
||||
### Vegas scroll hooks
|
||||
|
||||
Vegas mode shows multiple plugins as a single continuous scroll instead of
|
||||
rotating one at a time. Plugins control how their content appears via
|
||||
these hooks. See [ADVANCED_FEATURES.md](ADVANCED_FEATURES.md) for the user
|
||||
side of Vegas mode.
|
||||
|
||||
#### `get_vegas_content() -> Optional[PIL.Image | List[PIL.Image] | None]`
|
||||
|
||||
Return content to inject into the scroll. Multi-item plugins (sports,
|
||||
odds, news) should return a *list* of PIL Images so each item scrolls
|
||||
independently. Static plugins (clock, weather) can return a single image.
|
||||
Returning `None` falls back to capturing whatever `display()` produces.
|
||||
|
||||
#### `get_vegas_content_type() -> str`
|
||||
|
||||
`'multi'`, `'static'`, or `'none'`. Affects how Vegas mode treats the
|
||||
plugin. Default `'static'`.
|
||||
|
||||
#### `get_vegas_display_mode() -> VegasDisplayMode`
|
||||
|
||||
Returns one of `VegasDisplayMode.SCROLL`, `FIXED_SEGMENT`, or `STATIC`.
|
||||
Read from `config["vegas_mode"]` or override directly.
|
||||
|
||||
#### `get_supported_vegas_modes() -> List[VegasDisplayMode]`
|
||||
|
||||
The set of Vegas modes this plugin can render. Used by the UI to populate
|
||||
the mode selector for this plugin.
|
||||
|
||||
#### `get_vegas_segment_width() -> Optional[int]`
|
||||
|
||||
For `FIXED_SEGMENT` plugins, the number of *panels* the segment
|
||||
occupies in the scroll (pixel width = panels × `single_panel_width`,
|
||||
from `display.hardware.cols`). `None` uses the default of 1 panel.
|
||||
|
||||
> The full source for `BasePlugin` lives in
|
||||
> `src/plugin_system/base_plugin.py`. If a method here disagrees with the
|
||||
> source, the source wins — please open an issue or PR to fix the doc.
|
||||
|
||||
---
|
||||
|
||||
## Display Manager
|
||||
@@ -228,23 +323,31 @@ date_str = self.display_manager.format_date_with_ordinal(datetime.now())
|
||||
|
||||
### Image Rendering
|
||||
|
||||
#### `draw_image(image: PIL.Image, x: int, y: int) -> None`
|
||||
The display manager doesn't provide a dedicated `draw_image()` method.
|
||||
Instead, plugins paste directly onto the underlying PIL Image
|
||||
(`display_manager.image`), then call `update_display()` to push the buffer
|
||||
to the matrix.
|
||||
|
||||
Draw a PIL Image object on the canvas.
|
||||
|
||||
**Parameters**:
|
||||
- `image`: PIL Image object
|
||||
- `x` (int): X position (left edge)
|
||||
- `y` (int): Y position (top edge)
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
from PIL import Image
|
||||
logo = Image.open("assets/logo.png")
|
||||
self.display_manager.draw_image(logo, x=10, y=10)
|
||||
|
||||
logo = Image.open("assets/logo.png").convert("RGB")
|
||||
self.display_manager.image.paste(logo, (10, 10))
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
For transparency support, paste using a mask:
|
||||
|
||||
```python
|
||||
icon = Image.open("assets/icon.png").convert("RGBA")
|
||||
self.display_manager.image.paste(icon, (5, 5), icon)
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
This is the same pattern the bundled scoreboard base classes
|
||||
(`src/base_classes/baseball.py`, `basketball.py`, `football.py`,
|
||||
`hockey.py`) use, so it's the canonical way to render arbitrary images.
|
||||
|
||||
### Weather Icons
|
||||
|
||||
#### `draw_weather_icon(condition: str, x: int, y: int, size: int = 16) -> None`
|
||||
@@ -440,12 +543,23 @@ self.cache_manager.set("weather_data", {
|
||||
})
|
||||
```
|
||||
|
||||
#### `delete(key: str) -> None`
|
||||
#### `clear_cache(key: Optional[str] = None) -> None`
|
||||
|
||||
Remove a specific cache entry.
|
||||
Remove a specific cache entry, or all cache entries when called without
|
||||
arguments.
|
||||
|
||||
**Parameters**:
|
||||
- `key` (str): Cache key to delete
|
||||
- `key` (str, optional): Cache key to delete. If omitted, every cached
|
||||
entry (memory + disk) is cleared.
|
||||
|
||||
**Example**:
|
||||
```python
|
||||
# Drop one stale entry
|
||||
self.cache_manager.clear_cache("weather_data")
|
||||
|
||||
# Nuke everything (rare — typically only used by maintenance tooling)
|
||||
self.cache_manager.clear_cache()
|
||||
```
|
||||
|
||||
### Advanced Methods
|
||||
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# LEDMatrix Plugin Architecture Specification
|
||||
|
||||
> **Historical design document.** This spec was written *before* the
|
||||
> plugin system was built. Most of it is still architecturally
|
||||
> accurate, but specific details have drifted from the shipped
|
||||
> implementation:
|
||||
>
|
||||
> - Code paths reference `web_interface_v2.py`; the current web UI is
|
||||
> `web_interface/app.py` with v3 Blueprint-based templates.
|
||||
> - The example Flask routes use `/api/plugins/*`; the real API
|
||||
> blueprint is mounted at `/api/v3` (`web_interface/app.py:199`).
|
||||
> - The default plugin location is `plugin-repos/` (configurable via
|
||||
> `plugin_system.plugins_directory`), not `./plugins/`.
|
||||
> - Example imports use `src/plugin_system/base_classes/*_plugin.py`;
|
||||
> the shipped base classes live in `src/base_classes/` (e.g.
|
||||
> `src.base_classes.sports.SportsCore`, `src.base_classes.hockey.Hockey`).
|
||||
> - The "Migration Strategy" and "Implementation Roadmap" sections
|
||||
> describe work that has now shipped.
|
||||
>
|
||||
> For the current system, see:
|
||||
> [PLUGIN_DEVELOPMENT_GUIDE.md](PLUGIN_DEVELOPMENT_GUIDE.md),
|
||||
> [PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md), and
|
||||
> [REST_API_REFERENCE.md](REST_API_REFERENCE.md).
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the transformation of the LEDMatrix project into a modular, plugin-based architecture that enables user-created displays. The goal is to create a flexible, extensible system similar to Home Assistant Community Store (HACS) where users can discover, install, and manage custom display managers from GitHub repositories.
|
||||
@@ -9,22 +31,22 @@ This document outlines the transformation of the LEDMatrix project into a modula
|
||||
1. **Gradual Migration**: Existing managers remain in core while new plugin infrastructure is built
|
||||
2. **Migration Required**: Breaking changes with migration tools provided
|
||||
3. **GitHub-Based Store**: Simple discovery system, packages served from GitHub repos
|
||||
4. **Plugin Location**: `./plugins/` directory in project root
|
||||
4. **Plugin Location**: `./plugins/` directory in project root *(actual default is now `plugin-repos/`)*
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Current Architecture Analysis](#current-architecture-analysis)
|
||||
2. [Plugin System Design](#plugin-system-design)
|
||||
3. [Plugin Store & Discovery](#plugin-store--discovery)
|
||||
4. [Web UI Transformation](#web-ui-transformation)
|
||||
5. [Migration Strategy](#migration-strategy)
|
||||
6. [Plugin Developer Guidelines](#plugin-developer-guidelines)
|
||||
7. [Technical Implementation Details](#technical-implementation-details)
|
||||
8. [Best Practices & Standards](#best-practices--standards)
|
||||
9. [Security Considerations](#security-considerations)
|
||||
10. [Implementation Roadmap](#implementation-roadmap)
|
||||
1. [Current Architecture Analysis](#1-current-architecture-analysis)
|
||||
2. [Plugin System Design](#2-plugin-system-design)
|
||||
3. [Plugin Store & Discovery](#3-plugin-store--discovery)
|
||||
4. [Web UI Transformation](#4-web-ui-transformation)
|
||||
5. [Migration Strategy](#5-migration-strategy)
|
||||
6. [Plugin Developer Guidelines](#6-plugin-developer-guidelines)
|
||||
7. [Technical Implementation Details](#7-technical-implementation-details)
|
||||
8. [Best Practices & Standards](#8-best-practices--standards)
|
||||
9. [Security Considerations](#9-security-considerations)
|
||||
10. [Implementation Roadmap](#10-implementation-roadmap)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -184,37 +184,45 @@ plugin-repos/
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-plugin",
|
||||
"name": "My Plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Plugin description",
|
||||
"author": "Your Name",
|
||||
"entry_point": "manager.py",
|
||||
"class_name": "MyPlugin",
|
||||
"display_modes": ["my_plugin"],
|
||||
"config_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean", "default": false},
|
||||
"update_interval": {"type": "integer", "default": 3600}
|
||||
}
|
||||
}
|
||||
"config_schema": "config_schema.json"
|
||||
}
|
||||
```
|
||||
|
||||
The required fields the plugin loader will check for are `id`,
|
||||
`name`, `version`, `class_name`, and `display_modes`. `entry_point`
|
||||
defaults to `manager.py` if omitted. `config_schema` must be a
|
||||
**file path** (relative to the plugin directory) — the schema itself
|
||||
lives in a separate JSON file, not inline in the manifest. The
|
||||
`class_name` value must match the actual class defined in the entry
|
||||
point file **exactly** (case-sensitive, no spaces); otherwise the
|
||||
loader fails with `AttributeError` at load time.
|
||||
|
||||
### Plugin Manager Class
|
||||
|
||||
```python
|
||||
from src.plugin_system.base_plugin import BasePlugin
|
||||
|
||||
class MyPluginManager(BasePlugin):
|
||||
def __init__(self, config, display_manager, cache_manager, font_manager):
|
||||
super().__init__(config, display_manager, cache_manager, font_manager)
|
||||
self.enabled = config.get('enabled', False)
|
||||
|
||||
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.config, self.display_manager, self.cache_manager,
|
||||
# self.plugin_manager, self.logger, and self.enabled are
|
||||
# all set up by BasePlugin.__init__.
|
||||
|
||||
def update(self):
|
||||
"""Update plugin data"""
|
||||
"""Fetch/update data. Called based on update_interval."""
|
||||
pass
|
||||
|
||||
|
||||
def display(self, force_clear=False):
|
||||
"""Display plugin content"""
|
||||
"""Render plugin content to the LED matrix."""
|
||||
pass
|
||||
|
||||
def get_duration(self):
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Plugin Configuration Tabs
|
||||
|
||||
> **Status note:** this doc was written during the rollout of the
|
||||
> per-plugin configuration tab feature. The feature itself is shipped
|
||||
> and working in the current v3 web interface, but a few file paths
|
||||
> in the "Implementation Details" section below still reference the
|
||||
> pre-v3 file layout (`web_interface_v2.py`, `templates/index_v2.html`).
|
||||
> The current implementation lives in `web_interface/app.py`,
|
||||
> `web_interface/blueprints/api_v3.py`, and `web_interface/templates/v3/`.
|
||||
> The user-facing description (Overview, Features, Form Generation
|
||||
> Process) is still accurate.
|
||||
|
||||
## Overview
|
||||
|
||||
Each installed plugin now gets its own dedicated configuration tab in the web interface. This provides a clean, organized way to configure plugins without cluttering the main Plugins management tab.
|
||||
@@ -29,10 +39,14 @@ Each installed plugin now gets its own dedicated configuration tab in the web in
|
||||
3. Click **Save Configuration**
|
||||
4. Restart the display service to apply changes
|
||||
|
||||
### Plugin Management vs Configuration
|
||||
### Plugin Manager vs Per-Plugin Configuration
|
||||
|
||||
- **Plugins Tab**: Used for plugin management (install, enable/disable, update, uninstall)
|
||||
- **Plugin-Specific Tabs**: Used for configuring plugin behavior and settings
|
||||
- **Plugin Manager tab** (second nav row): used for browsing the
|
||||
Plugin Store, installing plugins, toggling installed plugins on/off,
|
||||
and updating/uninstalling them
|
||||
- **Per-plugin tabs** (one per installed plugin, also in the second
|
||||
nav row): used for configuring that specific plugin's behavior and
|
||||
settings via a form auto-generated from its `config_schema.json`
|
||||
|
||||
## For Plugin Developers
|
||||
|
||||
@@ -194,12 +208,12 @@ Renders as: Dropdown select
|
||||
|
||||
### Form Generation Process
|
||||
|
||||
1. Web UI loads installed plugins via `/api/plugins/installed`
|
||||
1. Web UI loads installed plugins via `/api/v3/plugins/installed`
|
||||
2. For each plugin, the backend loads its `config_schema.json`
|
||||
3. Frontend generates a tab button with plugin name
|
||||
4. Frontend generates a form based on the JSON Schema
|
||||
5. Current config values from `config.json` are populated
|
||||
6. When saved, each field is sent to `/api/plugins/config` endpoint
|
||||
6. When saved, each field is sent to `/api/v3/plugins/config` endpoint
|
||||
|
||||
## Implementation Details
|
||||
|
||||
@@ -207,7 +221,7 @@ Renders as: Dropdown select
|
||||
|
||||
**File**: `web_interface_v2.py`
|
||||
|
||||
- Modified `/api/plugins/installed` endpoint to include `config_schema_data`
|
||||
- Modified `/api/v3/plugins/installed` endpoint to include `config_schema_data`
|
||||
- Loads each plugin's `config_schema.json` if it exists
|
||||
- Returns schema data along with plugin info
|
||||
|
||||
@@ -227,7 +241,7 @@ New Functions:
|
||||
```
|
||||
Page Load
|
||||
→ refreshPlugins()
|
||||
→ /api/plugins/installed
|
||||
→ /api/v3/plugins/installed
|
||||
→ Returns plugins with config_schema_data
|
||||
→ generatePluginTabs()
|
||||
→ Creates tab buttons
|
||||
@@ -241,7 +255,7 @@ User Saves
|
||||
→ savePluginConfiguration()
|
||||
→ Reads form data
|
||||
→ Converts types per schema
|
||||
→ Sends to /api/plugins/config
|
||||
→ Sends to /api/v3/plugins/config
|
||||
→ Updates config.json
|
||||
→ Shows success notification
|
||||
```
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Plugin Configuration Tabs - Architecture
|
||||
|
||||
> This page covers internals (how the config system works under the
|
||||
> hood). For designing a plugin's config schema, the canonical guide is
|
||||
> [PLUGIN_CONFIGURATION_GUIDE.md](PLUGIN_CONFIGURATION_GUIDE.md); for
|
||||
> the user-facing tabs feature, see
|
||||
> [PLUGIN_CONFIGURATION_TABS.md](PLUGIN_CONFIGURATION_TABS.md).
|
||||
|
||||
## System Architecture
|
||||
|
||||
### Component Overview
|
||||
@@ -31,7 +37,7 @@
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Flask Backend │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ /api/plugins/installed │ │
|
||||
│ │ /api/v3/plugins/installed │ │
|
||||
│ │ • Discover plugins in plugins/ directory │ │
|
||||
│ │ • Load manifest.json for each plugin │ │
|
||||
│ │ • Load config_schema.json if exists │ │
|
||||
@@ -40,7 +46,7 @@
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ /api/plugins/config │ │
|
||||
│ │ /api/v3/plugins/config │ │
|
||||
│ │ • Receive key-value pair │ │
|
||||
│ │ • Update config.json │ │
|
||||
│ │ • Return success/error │ │
|
||||
@@ -88,7 +94,7 @@ DOMContentLoaded Event
|
||||
refreshPlugins()
|
||||
│
|
||||
▼
|
||||
GET /api/plugins/installed
|
||||
GET /api/v3/plugins/installed
|
||||
│
|
||||
├─→ For each plugin directory:
|
||||
│ ├─→ Read manifest.json
|
||||
@@ -146,7 +152,7 @@ savePluginConfiguration(pluginId)
|
||||
│ │ • array: split(',')
|
||||
│ │ • string: as-is
|
||||
│ │
|
||||
│ └─→ POST /api/plugins/config
|
||||
│ └─→ POST /api/v3/plugins/config
|
||||
│ {
|
||||
│ plugin_id: "hello-world",
|
||||
│ key: "message",
|
||||
@@ -174,7 +180,7 @@ Refresh Plugins
|
||||
Window Load
|
||||
└── DOMContentLoaded
|
||||
└── refreshPlugins()
|
||||
├── fetch('/api/plugins/installed')
|
||||
├── fetch('/api/v3/plugins/installed')
|
||||
├── renderInstalledPlugins(plugins)
|
||||
└── generatePluginTabs(plugins)
|
||||
└── For each plugin:
|
||||
@@ -198,19 +204,19 @@ User Interactions
|
||||
│ ├── Process form data
|
||||
│ ├── Convert types per schema
|
||||
│ └── For each field:
|
||||
│ └── POST /api/plugins/config
|
||||
│ └── POST /api/v3/plugins/config
|
||||
│
|
||||
└── resetPluginConfig(pluginId)
|
||||
├── Get schema defaults
|
||||
└── For each field:
|
||||
└── POST /api/plugins/config
|
||||
└── POST /api/v3/plugins/config
|
||||
```
|
||||
|
||||
### Backend (Python)
|
||||
|
||||
```
|
||||
Flask Routes
|
||||
├── /api/plugins/installed (GET)
|
||||
├── /api/v3/plugins/installed (GET)
|
||||
│ └── api_plugins_installed()
|
||||
│ ├── PluginManager.discover_plugins()
|
||||
│ ├── For each plugin:
|
||||
@@ -219,7 +225,7 @@ Flask Routes
|
||||
│ │ └── Load config from config.json
|
||||
│ └── Return JSON response
|
||||
│
|
||||
└── /api/plugins/config (POST)
|
||||
└── /api/v3/plugins/config (POST)
|
||||
└── api_plugin_config()
|
||||
├── Parse request JSON
|
||||
├── Load current config
|
||||
@@ -279,7 +285,7 @@ LEDMatrix/
|
||||
### 3. Individual Config Updates
|
||||
|
||||
**Why**: Simplifies backend API
|
||||
**How**: Each field saved separately via `/api/plugins/config`
|
||||
**How**: Each field saved separately via `/api/v3/plugins/config`
|
||||
**Benefit**: Atomic updates, easier error handling
|
||||
|
||||
### 4. Type Conversion in Frontend
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
### For Users
|
||||
|
||||
1. Open the web interface: `http://your-pi-ip:5001`
|
||||
2. Go to the **Plugin Store** tab
|
||||
3. Install a plugin (e.g., "Hello World")
|
||||
4. Notice a new tab appears with the plugin's name
|
||||
5. Click on the plugin's tab to configure it
|
||||
6. Modify settings and click **Save Configuration**
|
||||
7. Restart the display to see changes
|
||||
1. Open the web interface: `http://your-pi-ip:5000`
|
||||
2. Open the **Plugin Manager** tab
|
||||
3. Find a plugin in the **Plugin Store** section (e.g., "Hello World")
|
||||
and click **Install**
|
||||
4. Notice a new tab appears in the second nav row with the plugin's name
|
||||
5. Click that tab to configure the plugin
|
||||
6. Modify settings and click **Save**
|
||||
7. From **Overview**, click **Restart Display Service** to see changes
|
||||
|
||||
That's it! Each installed plugin automatically gets its own configuration tab.
|
||||
|
||||
@@ -171,9 +172,11 @@ User enters: `255, 0, 0`
|
||||
### For Users
|
||||
|
||||
1. **Reset Anytime**: Use "Reset to Defaults" to restore original settings
|
||||
2. **Navigate Back**: Click "Back to Plugin Management" to return to Plugins tab
|
||||
2. **Navigate Back**: Switch to the **Plugin Manager** tab to see the
|
||||
full list of installed plugins
|
||||
3. **Check Help Text**: Each field has a description explaining what it does
|
||||
4. **Restart Required**: Remember to restart the display after saving
|
||||
4. **Restart Required**: Remember to restart the display service from
|
||||
**Overview** after saving
|
||||
|
||||
### For Developers
|
||||
|
||||
@@ -206,8 +209,10 @@ User enters: `255, 0, 0`
|
||||
## 📚 Next Steps
|
||||
|
||||
- Read the full documentation: [PLUGIN_CONFIGURATION_TABS.md](PLUGIN_CONFIGURATION_TABS.md)
|
||||
- Check implementation details: [PLUGIN_CONFIG_TABS_SUMMARY.md](PLUGIN_CONFIG_TABS_SUMMARY.md)
|
||||
- Browse example plugins: `plugins/hello-world/`, `plugins/clock-simple/`
|
||||
- Check the configuration architecture: [PLUGIN_CONFIG_ARCHITECTURE.md](PLUGIN_CONFIG_ARCHITECTURE.md)
|
||||
- Browse example plugins in the
|
||||
[ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins)
|
||||
repo, especially `plugins/hello-world/` and `plugins/clock-simple/`
|
||||
- Join the community for help and suggestions
|
||||
|
||||
## 🎉 That's It!
|
||||
|
||||
@@ -296,7 +296,7 @@ Want to change icons programmatically? While not officially supported, you could
|
||||
## Related Documentation
|
||||
|
||||
- [Plugin Configuration Tabs](PLUGIN_CONFIGURATION_TABS.md) - Main plugin tabs documentation
|
||||
- [Plugin Development Guide](plugin_docs/) - How to create plugins
|
||||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - How to create plugins
|
||||
- [Font Awesome Icons](https://fontawesome.com/icons) - Browse all available icons
|
||||
- [Emoji Reference](https://unicode.org/emoji/charts/full-emoji-list.html) - All emoji options
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ sudo systemctl start ledmatrix-web
|
||||
|
||||
### ✅ Scenario 2: Web Interface Plugin Installation
|
||||
|
||||
**What:** Installing/enabling plugins via web interface at `http://pi-ip:5001`
|
||||
**What:** Installing/enabling plugins via web interface at `http://pi-ip:5000`
|
||||
|
||||
- **Web service runs as:** root (ledmatrix-web.service)
|
||||
- **Installs to:** System-wide
|
||||
|
||||
@@ -77,10 +77,12 @@ sudo chmod -R 755 /root/.cache
|
||||
|
||||
The web interface handles dependency installation correctly in the service context:
|
||||
|
||||
1. Access the web interface (usually http://ledpi:8080)
|
||||
2. Navigate to Plugin Store or Plugin Management
|
||||
3. Install plugins through the web UI
|
||||
4. The system will automatically handle dependencies
|
||||
1. Access the web interface (`http://ledpi:5000` or `http://your-pi-ip:5000`)
|
||||
2. Open the **Plugin Manager** tab (use the **Plugin Store** section to
|
||||
find the plugin, or **Install from GitHub**)
|
||||
3. Install the plugin through the web UI
|
||||
4. The system automatically handles dependency installation in the
|
||||
service context (which has the right permissions)
|
||||
|
||||
## Prevention
|
||||
|
||||
@@ -167,6 +169,6 @@ If you continue to experience issues:
|
||||
## Related Documentation
|
||||
|
||||
- [Plugin Dependency Guide](PLUGIN_DEPENDENCY_GUIDE.md)
|
||||
- [Plugin Development Guide](docs/plugin_development.md)
|
||||
- [Troubleshooting Quick Start](TROUBLESHOOTING_QUICK_START.md)
|
||||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md)
|
||||
- [Troubleshooting](TROUBLESHOOTING.md)
|
||||
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
This guide explains how to set up a development workflow for plugins that are maintained in separate Git repositories while still being able to test them within the LEDMatrix project.
|
||||
|
||||
> **Rendering guidance:** plugins should read the display size dynamically
|
||||
> (`self.display_manager.matrix.width/height`) rather than hardcoding one
|
||||
> panel. For plugins that want to *scale* their layout to any panel, the
|
||||
> opt-in adaptive layout system ([ADAPTIVE_LAYOUT.md](ADAPTIVE_LAYOUT.md))
|
||||
> provides the shared helpers — fonts, images, and composite layouts that
|
||||
> scale. Existing plugins keep their classic rendering unless they adopt
|
||||
> those APIs; nothing migrates automatically.
|
||||
|
||||
> **Just want a different look for an existing sports scoreboard?** You may
|
||||
> not need a plugin at all — a **skin** restyles the live/recent/upcoming
|
||||
> rendering while the plugin keeps handling data, scheduling, caching, and
|
||||
> vegas mode, in ~100 lines of drawing code. See
|
||||
> [CREATING_SKINS.md](CREATING_SKINS.md).
|
||||
|
||||
## Overview
|
||||
|
||||
When developing plugins in separate repositories, you need a way to:
|
||||
@@ -12,6 +26,21 @@ When developing plugins in separate repositories, you need a way to:
|
||||
|
||||
The solution uses **symbolic links** to connect plugin repositories to the `plugins/` directory, combined with a helper script to manage the linking process.
|
||||
|
||||
> **Plugin directory note:** the dev workflow described here puts
|
||||
> symlinks in `plugins/`. The plugin loader's *production* default is
|
||||
> `plugin-repos/` (set by `plugin_system.plugins_directory` in
|
||||
> `config.json`). Importantly, the main discovery path
|
||||
> (`PluginManager.discover_plugins()`) only scans the configured
|
||||
> directory — it does **not** fall back to `plugins/`. Two narrower
|
||||
> paths do: the Plugin Store install/update logic in `store_manager.py`,
|
||||
> and `schema_manager.get_schema_path()` (which the web UI form
|
||||
> generator uses to find `config_schema.json`). That's why plugins
|
||||
> installed via the Plugin Store still work even with symlinks in
|
||||
> `plugins/`, but your own dev plugin won't appear in the rotation
|
||||
> until you either move it to `plugin-repos/` or change
|
||||
> `plugin_system.plugins_directory` to `plugins` in the General tab
|
||||
> of the web UI. The latter is the smoother dev setup.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Link a Plugin from GitHub
|
||||
@@ -466,7 +495,9 @@ When developing plugins, you'll need to use the APIs provided by the LEDMatrix s
|
||||
|
||||
**Display Manager** (`self.display_manager`):
|
||||
- `clear()`, `update_display()` - Core display operations
|
||||
- `draw_text()`, `draw_image()` - Rendering methods
|
||||
- `draw_text()` - Text rendering. For images, paste directly onto
|
||||
`display_manager.image` (a PIL Image) and call `update_display()`;
|
||||
there is no `draw_image()` helper method.
|
||||
- `draw_weather_icon()`, `draw_sun()`, `draw_cloud()` - Weather icons
|
||||
- `get_text_width()`, `get_font_height()` - Text utilities
|
||||
- `set_scrolling_state()`, `defer_update()` - Scrolling state management
|
||||
@@ -558,11 +589,24 @@ Your plugin must:
|
||||
### Versioning Best Practices
|
||||
|
||||
- **Use semantic versioning**: `MAJOR.MINOR.PATCH` (e.g., `1.2.3`)
|
||||
- **Automatic version bumping**: Use the pre-push git hook for automatic patch version bumps
|
||||
- **Manual versioning**: Only needed for major/minor bumps or special cases
|
||||
- **GitHub as source of truth**: Plugin store fetches versions from GitHub releases/tags/manifest
|
||||
- **GitHub as source of truth**: the plugin store resolves versions in this
|
||||
order: GitHub Releases → GitHub Tags → manifest from branch → git commit hash
|
||||
- **Automatic version bumping**: install the self-contained pre-push hook in
|
||||
your plugin repo and patch versions bump themselves on push (a git tag
|
||||
`v{version}` is created and `manifest.json` staged automatically):
|
||||
|
||||
See the [Git Workflow rules](../.cursorrules) for version management details.
|
||||
```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
|
||||
```
|
||||
|
||||
Set `SKIP_TAG=1` in the environment to skip auto-tagging for one push.
|
||||
- **Manual versioning**: only needed for major/minor bumps, CI pipelines that
|
||||
bypass hooks, or forks without the hook — use
|
||||
`scripts/bump_plugin_version.py`.
|
||||
- **Registry stores no versions**: `plugins.json` holds only metadata (name,
|
||||
description, repo URL).
|
||||
|
||||
### Submitting to Official Registry
|
||||
|
||||
@@ -636,5 +680,5 @@ For your plugin to work well in the plugin store:
|
||||
- [Advanced Plugin Development](ADVANCED_PLUGIN_DEVELOPMENT.md) - Advanced patterns and examples
|
||||
- [Plugin Quick Reference](PLUGIN_QUICK_REFERENCE.md) - Quick development reference
|
||||
- [Plugin Configuration Guide](PLUGIN_CONFIGURATION_GUIDE.md) - Configuration setup
|
||||
- [Plugin Store User Guide](PLUGIN_STORE_USER_GUIDE.md) - Using the plugin store
|
||||
- [Plugin Store Guide](PLUGIN_STORE_GUIDE.md) - Using the plugin store
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# LEDMatrix Plugin System - Implementation Summary
|
||||
|
||||
> **Status note:** this is a high-level summary written during the
|
||||
> initial plugin system rollout. Most of it is accurate, but a few
|
||||
> sections describe features that are aspirational or only partially
|
||||
> implemented (per-plugin virtual envs, resource limits, registry
|
||||
> manager). Drift from current reality is called out inline.
|
||||
|
||||
This document provides a comprehensive overview of the plugin architecture implementation, consolidating details from multiple plugin-related implementation summaries.
|
||||
|
||||
## Executive Summary
|
||||
@@ -14,16 +20,25 @@ The LEDMatrix plugin system transforms the project into a modular, extensible pl
|
||||
LEDMatrix/
|
||||
├── src/plugin_system/
|
||||
│ ├── base_plugin.py # Plugin interface contract
|
||||
│ ├── plugin_loader.py # Discovery + dynamic import
|
||||
│ ├── plugin_manager.py # Lifecycle management
|
||||
│ ├── store_manager.py # GitHub integration
|
||||
│ └── registry_manager.py # Plugin discovery
|
||||
├── plugins/ # User-installed plugins
|
||||
│ ├── store_manager.py # GitHub install / store integration
|
||||
│ ├── schema_manager.py # Config schema validation
|
||||
│ ├── health_monitor.py # Plugin health metrics
|
||||
│ ├── operation_queue.py # Async install/update operations
|
||||
│ └── state_manager.py # Persistent plugin state
|
||||
├── plugin-repos/ # Default plugin install location
|
||||
│ ├── football-scoreboard/
|
||||
│ ├── ledmatrix-music/
|
||||
│ └── ledmatrix-stocks/
|
||||
└── config/config.json # Plugin configurations
|
||||
```
|
||||
|
||||
> Earlier drafts of this doc referenced `registry_manager.py`. It was
|
||||
> never created — discovery happens in `plugin_loader.py`. The earlier
|
||||
> default plugin location of `plugins/` has been replaced with
|
||||
> `plugin-repos/` (see `config/config.template.json:130`).
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
✅ **Gradual Migration**: Plugin system added alongside existing managers
|
||||
@@ -77,14 +92,26 @@ LEDMatrix/
|
||||
- **Fallback System**: Default icons when custom ones unavailable
|
||||
|
||||
#### Dependency Management
|
||||
- **Requirements.txt**: Per-plugin dependencies
|
||||
- **Virtual Environments**: Isolated dependency management
|
||||
- **Version Pinning**: Explicit version constraints
|
||||
- **Requirements.txt**: Per-plugin dependencies, installed system-wide
|
||||
via pip on first plugin load
|
||||
- **Version Pinning**: Standard pip version constraints in
|
||||
`requirements.txt`
|
||||
|
||||
#### Permission System
|
||||
- **File Access Control**: Configurable file system permissions
|
||||
- **Network Access**: Controlled API access
|
||||
- **Resource Limits**: CPU and memory constraints
|
||||
> Earlier plans called for per-plugin virtual environments. That isn't
|
||||
> implemented — plugin Python deps install into the system Python
|
||||
> environment (or whatever environment the LEDMatrix service is using).
|
||||
> Conflicting versions across plugins are not auto-resolved.
|
||||
|
||||
#### Health monitoring
|
||||
- **Resource Monitor** (`src/plugin_system/resource_monitor.py`): tracks
|
||||
CPU and memory metrics per plugin and warns about slow plugins
|
||||
- **Health Monitor** (`src/plugin_system/health_monitor.py`): tracks
|
||||
plugin failures and last-success timestamps
|
||||
|
||||
> Earlier plans called for hard CPU/memory limits and a sandboxed
|
||||
> permission system. Neither is implemented. Plugins run in the same
|
||||
> process as the display loop with full file-system and network access
|
||||
> — review third-party plugin code before installing.
|
||||
|
||||
## Plugin Development
|
||||
|
||||
|
||||