Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b374bfa8c6 | ||
|
|
49287bdd1a | ||
|
|
1d31465df0 | ||
|
|
2a7a318cf7 |
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
exclude_paths:
|
|
||||||
- "plugin-repos/**"
|
|
||||||
- "plugins/**"
|
|
||||||
- "assets/**"
|
|
||||||
- "test/**"
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 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/`
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# Implementation Plan: Fix Config Schema Validation Issues
|
||||||
|
|
||||||
|
Based on audit results showing 186 issues across 20 plugins.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Three priority fixes identified from audit:
|
||||||
|
1. **Priority 1 (HIGH)**: Remove core properties from required array - will fix ~150 issues
|
||||||
|
2. **Priority 2 (MEDIUM)**: Verify default merging logic - will fix remaining required field issues
|
||||||
|
3. **Priority 3 (LOW)**: Calendar plugin schema cleanup - will fix 3 extra field warnings
|
||||||
|
|
||||||
|
## Priority 1: Remove Core Properties from Required Array
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Core properties (`enabled`, `display_duration`, `live_priority`) are system-managed but listed in schema `required` arrays. SchemaManager injects them into properties but doesn't remove them from `required`, causing validation failures.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
**File**: `src/plugin_system/schema_manager.py`
|
||||||
|
**Location**: `validate_config_against_schema()` method, after line 295
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
|
||||||
|
1. **Add code to remove core properties from required array**:
|
||||||
|
```python
|
||||||
|
# After injecting core properties (around line 295), add:
|
||||||
|
# Remove core properties from required array (they're system-managed)
|
||||||
|
if "required" in enhanced_schema:
|
||||||
|
core_prop_names = list(core_properties.keys())
|
||||||
|
enhanced_schema["required"] = [
|
||||||
|
field for field in enhanced_schema["required"]
|
||||||
|
if field not in core_prop_names
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add logging for debugging** (optional but helpful):
|
||||||
|
```python
|
||||||
|
if "required" in enhanced_schema and core_prop_names:
|
||||||
|
removed_from_required = [
|
||||||
|
field for field in enhanced_schema.get("required", [])
|
||||||
|
if field in core_prop_names
|
||||||
|
]
|
||||||
|
if removed_from_required and plugin_id:
|
||||||
|
self.logger.debug(
|
||||||
|
f"Removed core properties from required array for {plugin_id}: {removed_from_required}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Test the fix**:
|
||||||
|
- Run audit script: `python scripts/audit_plugin_configs.py`
|
||||||
|
- Expected: Issue count drops from 186 to ~30-40
|
||||||
|
- All "enabled" related errors should be eliminated
|
||||||
|
|
||||||
|
### Expected Outcome
|
||||||
|
- All 20 plugins should no longer fail validation due to missing `enabled` field
|
||||||
|
- ~150 issues resolved (all enabled-related validation errors)
|
||||||
|
|
||||||
|
## Priority 2: Verify Default Merging Logic
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Some plugins have required fields with defaults that should be applied before validation. Need to verify the default merging happens correctly and handles nested objects.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
**File**: `web_interface/blueprints/api_v3.py`
|
||||||
|
**Location**: `save_plugin_config()` method, around lines 3218-3221
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
|
||||||
|
1. **Review current default merging logic**:
|
||||||
|
- Check that `merge_with_defaults()` is called before validation (line 3220)
|
||||||
|
- Verify it's called after preserving enabled state but before validation
|
||||||
|
|
||||||
|
2. **Verify merge_with_defaults handles nested objects**:
|
||||||
|
- Check `src/plugin_system/schema_manager.py` → `merge_with_defaults()` method
|
||||||
|
- Ensure it recursively merges nested objects (it does use deep_merge)
|
||||||
|
- Test with plugins that have nested required fields
|
||||||
|
|
||||||
|
3. **Check if defaults are applied for nested required fields**:
|
||||||
|
- Review how `generate_default_config()` extracts defaults from nested schemas
|
||||||
|
- Verify nested required fields with defaults are included
|
||||||
|
|
||||||
|
4. **Test with problematic plugins**:
|
||||||
|
- `ledmatrix-weather`: required fields `api_key`, `location_city` (check if defaults exist)
|
||||||
|
- `mqtt-notifications`: required field `mqtt` object (check if default exists)
|
||||||
|
- `text-display`: required field `text` (check if default exists)
|
||||||
|
- `ledmatrix-music`: required field `preferred_source` (check if default exists)
|
||||||
|
|
||||||
|
5. **If defaults don't exist in schemas**:
|
||||||
|
- Either add defaults to schemas, OR
|
||||||
|
- Make fields optional in schemas if they're truly optional
|
||||||
|
|
||||||
|
### Expected Outcome
|
||||||
|
- Plugins with required fields that have schema defaults should pass validation
|
||||||
|
- Issue count further reduced from ~30-40 to ~5-10
|
||||||
|
|
||||||
|
## Priority 3: Calendar Plugin Schema Cleanup
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Calendar plugin config has fields not in schema:
|
||||||
|
- `show_all_day` (config) but schema has `show_all_day_events` (field name mismatch)
|
||||||
|
- `date_format` (not in schema, not used in manager.py)
|
||||||
|
- `time_format` (not in schema, not used in manager.py)
|
||||||
|
|
||||||
|
### Investigation Results
|
||||||
|
- Schema defines: `show_all_day_events` (boolean, default: true)
|
||||||
|
- Manager.py uses: `show_all_day_events` (line 82: `config.get('show_all_day_events', True)`)
|
||||||
|
- Config has: `show_all_day` (wrong field name - should be `show_all_day_events`)
|
||||||
|
- `date_format` and `time_format` appear to be deprecated (not used in manager.py)
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
**File**: `config/config.json` → `calendar` section
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
|
||||||
|
1. **Fix field name mismatch**:
|
||||||
|
- Rename `show_all_day` → `show_all_day_events` in config.json
|
||||||
|
- This matches the schema and manager.py code
|
||||||
|
|
||||||
|
2. **Remove deprecated fields**:
|
||||||
|
- Remove `date_format` from config (not used in code)
|
||||||
|
- Remove `time_format` from config (not used in code)
|
||||||
|
|
||||||
|
3. **Alternative (if fields are needed)**: Add `date_format` and `time_format` to schema
|
||||||
|
- Only if these fields should be supported
|
||||||
|
- Check if they're used anywhere else in the codebase
|
||||||
|
|
||||||
|
4. **Test calendar plugin**:
|
||||||
|
- Run audit for calendar plugin specifically
|
||||||
|
- Verify no extra field warnings remain
|
||||||
|
- Test calendar plugin functionality to ensure it still works
|
||||||
|
|
||||||
|
### Expected Outcome
|
||||||
|
- Calendar plugin shows 0 extra field warnings
|
||||||
|
- Final issue count: ~3-5 (only edge cases remain)
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### After Each Priority Fix
|
||||||
|
|
||||||
|
1. **Run local audit**:
|
||||||
|
```bash
|
||||||
|
python scripts/audit_plugin_configs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check issue count reduction**:
|
||||||
|
- Priority 1: Should drop from 186 to ~30-40
|
||||||
|
- Priority 2: Should drop from ~30-40 to ~5-10
|
||||||
|
- Priority 3: Should drop from ~5-10 to ~3-5
|
||||||
|
|
||||||
|
3. **Review specific plugin results**:
|
||||||
|
```bash
|
||||||
|
python scripts/audit_plugin_configs.py --plugin <plugin-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### After All Fixes
|
||||||
|
|
||||||
|
1. **Full audit run**:
|
||||||
|
```bash
|
||||||
|
python scripts/audit_plugin_configs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Deploy to Pi**:
|
||||||
|
```bash
|
||||||
|
./scripts/deploy_to_pi.sh src/plugin_system/schema_manager.py web_interface/blueprints/api_v3.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run audit on Pi**:
|
||||||
|
```bash
|
||||||
|
./scripts/run_audit_on_pi.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Manual web interface testing**:
|
||||||
|
- Access each problematic plugin's config page
|
||||||
|
- Try saving configuration
|
||||||
|
- Verify no validation errors appear
|
||||||
|
- Check that configs save successfully
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] Priority 1: All "enabled" related validation errors eliminated
|
||||||
|
- [ ] Priority 1: Issue count reduced from 186 to ~30-40
|
||||||
|
- [ ] Priority 2: Plugins with required fields + defaults pass validation
|
||||||
|
- [ ] Priority 2: Issue count reduced to ~5-10
|
||||||
|
- [ ] Priority 3: Calendar plugin extra field warnings resolved
|
||||||
|
- [ ] Priority 3: Final issue count at ~3-5 (only edge cases)
|
||||||
|
- [ ] All fixes work on Pi (not just local)
|
||||||
|
- [ ] Web interface saves configs without validation errors
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
1. `src/plugin_system/schema_manager.py` - Remove core properties from required array
|
||||||
|
2. `plugins/calendar/config_schema.json` OR `config/config.json` - Calendar cleanup (if needed)
|
||||||
|
3. `web_interface/blueprints/api_v3.py` - May need minor adjustments for default merging (if needed)
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
**Priority 1**: Low risk - Only affects validation logic, doesn't change behavior
|
||||||
|
**Priority 2**: Low risk - Only ensures defaults are applied (already intended behavior)
|
||||||
|
**Priority 3**: Very low risk - Only affects calendar plugin, cosmetic issue
|
||||||
|
|
||||||
|
All changes are backward compatible and improve the system rather than changing core functionality.
|
||||||
|
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
||||||
@@ -0,0 +1,742 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
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**
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
# 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,15 +1,2 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
# Auto detect text files and perform LF normalization
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
# Files the Pi executes must stay LF even in a Windows checkout with
|
|
||||||
# core.autocrlf=true: a CRLF shebang line fails with "bad interpreter",
|
|
||||||
# and systemd rejects CRLF unit files.
|
|
||||||
*.sh text eol=lf
|
|
||||||
*.service text eol=lf
|
|
||||||
|
|
||||||
# Generated by scripts/build_css.py; collapsed in diffs, not hand-edited.
|
|
||||||
web_interface/static/v3/tailwind.css linguist-generated=true
|
|
||||||
web_interface/static/v3/plugin-frame.css linguist-generated=true
|
|
||||||
|
|
||||||
# Installed as an executable (its shebang runs it) by install_service.sh.
|
|
||||||
scripts/install/ledmatrix_refresh_units.py text eol=lf
|
|
||||||
|
|||||||
@@ -1,84 +1,38 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Report a problem with LEDMatrix
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
**Describe the bug**
|
||||||
Before filing: please check existing issues to see if this is already
|
A clear and concise description of what the bug is.
|
||||||
reported. For security issues, see SECURITY.md and report privately.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Describe the bug
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
## Steps to reproduce
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
1.
|
**Desktop (please complete the following information):**
|
||||||
2.
|
- OS: [e.g. iOS]
|
||||||
3.
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
## Expected behavior
|
**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]
|
||||||
|
|
||||||
<!-- What you expected to happen. -->
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
## 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. -->
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# 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. -->
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
name: Claude Code Review
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, ready_for_review, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-review:
|
|
||||||
# Pull requests from forks get no repository secrets, so without this
|
|
||||||
# guard every outside contributor's PR showed this check red for a reason
|
|
||||||
# they can't fix. Skipped checks don't block merging.
|
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code Review
|
|
||||||
id: claude-review
|
|
||||||
uses: anthropics/claude-code-action@756cc22e19660d20e8cc9496b4f242475a7f7790 # v1
|
|
||||||
with:
|
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
||||||
# Review PRs opened by the Claude GitHub App. Without this the action
|
|
||||||
# aborts before reading the diff ("Workflow initiated by non-human
|
|
||||||
# actor"), so every such PR shows this check red. Named rather than
|
|
||||||
# '*': the allow-list is matched against the triggering actor, so
|
|
||||||
# this admits claude[bot] alone and no other app.
|
|
||||||
allowed_bots: 'claude'
|
|
||||||
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 }}'
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code
|
|
||||||
id: claude
|
|
||||||
uses: anthropics/claude-code-action@756cc22e19660d20e8cc9496b4f242475a7f7790 # 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
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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.13"
|
|
||||||
|
|
||||||
# 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 }}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
# Manual runs against any branch — useful when a PR's automatic run
|
|
||||||
# needs a re-run or didn't get created.
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# The jobs only check out the repo and run the tests.
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
plugin-safety:
|
|
||||||
name: Plugin safety harness + unit tests (Python ${{ matrix.python-version }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# The two Pythons the installer supports: Raspberry Pi OS Bookworm ships
|
|
||||||
# 3.11 and Trixie 3.13.
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.11", "3.13"]
|
|
||||||
env:
|
|
||||||
# The bundled fixture plugin gives the harness at least one real plugin
|
|
||||||
# to render, and REQUIRE_PLUGINS turns "discovered zero plugins" into a
|
|
||||||
# hard failure instead of a silent all-skip green run.
|
|
||||||
LEDMATRIX_PLUGINS_DIR: test/fixtures/plugins
|
|
||||||
LEDMATRIX_REQUIRE_PLUGINS: "1"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt -r web_interface/requirements.txt -r requirements-test.txt
|
|
||||||
pip install RGBMatrixEmulator
|
|
||||||
|
|
||||||
- name: Run plugin safety harness
|
|
||||||
run: |
|
|
||||||
pytest --no-cov test/plugins/
|
|
||||||
|
|
||||||
unit-tests:
|
|
||||||
name: Core unit tests (Python ${{ matrix.python-version }})
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Bookworm's Python (3.11) and Trixie's (3.13); see plugin-safety.
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: ["3.11", "3.13"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt -r web_interface/requirements.txt -r requirements-test.txt
|
|
||||||
pip install RGBMatrixEmulator
|
|
||||||
|
|
||||||
# Run the ENTIRE test tree (except test/plugins, which the
|
|
||||||
# plugin-safety job owns). New test files are enrolled automatically;
|
|
||||||
# excluding anything requires a visible, commented --ignore here.
|
|
||||||
# Coverage is measured and enforced only in this step — pytest.ini
|
|
||||||
# deliberately carries no coverage flags so local runs stay fast.
|
|
||||||
- name: Run core unit suites
|
|
||||||
run: |
|
|
||||||
pytest -m "not hardware" test/ \
|
|
||||||
--ignore=test/plugins \
|
|
||||||
--cov=src --cov=web_interface \
|
|
||||||
--cov-report=term \
|
|
||||||
--cov-fail-under=52
|
|
||||||
|
|
||||||
js-tests:
|
|
||||||
name: Web UI JS 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.13"
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
||||||
with:
|
|
||||||
node-version: "22"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt -r web_interface/requirements.txt
|
|
||||||
npm install --no-audit --no-fund --prefix test/js
|
|
||||||
|
|
||||||
# The DOM suites test the real server-rendered pages and API, so they
|
|
||||||
# need the web interface running. REQUIRE_DOM turns "couldn't reach it"
|
|
||||||
# into a failure instead of a silent skip.
|
|
||||||
- name: Start the web interface
|
|
||||||
run: |
|
|
||||||
EMULATOR=true python -c "from web_interface.app import app; app.run(host='127.0.0.1', port=5000, threaded=True)" > web.log 2>&1 &
|
|
||||||
for i in $(seq 60); do curl -sf -o /dev/null http://127.0.0.1:5000/ && exit 0; sleep 1; done
|
|
||||||
cat web.log
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Run JS suites
|
|
||||||
env:
|
|
||||||
BASE: http://127.0.0.1:5000
|
|
||||||
REQUIRE_DOM: "1"
|
|
||||||
run: node test/js/run_all.js
|
|
||||||
|
|
||||||
css-build:
|
|
||||||
name: Tailwind CSS is up to date
|
|
||||||
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.13"
|
|
||||||
|
|
||||||
# Downloads the pinned standalone Tailwind CLI (SHA-256 checked; no
|
|
||||||
# Node), rebuilds static/v3/tailwind.css and plugin-frame.css from the
|
|
||||||
# templates and JS, and fails if the committed files differ. Fix a
|
|
||||||
# failure by running `python3 scripts/build_css.py` and committing.
|
|
||||||
- name: Check the committed CSS matches a fresh build
|
|
||||||
run: python scripts/build_css.py --check
|
|
||||||
|
|
||||||
type-check:
|
|
||||||
name: Type check (mypy ratchet)
|
|
||||||
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.13"
|
|
||||||
cache: pip
|
|
||||||
|
|
||||||
# The runtime requirements are installed so mypy sees the real types of
|
|
||||||
# PIL, requests, psutil and friends -- missing, they'd be Any and the
|
|
||||||
# result would differ from a developer's machine. mypy and the stubs are
|
|
||||||
# pinned so a new release can't turn this red without a code change.
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt -r web_interface/requirements.txt
|
|
||||||
pip install mypy==1.20.2 types-requests==2.33.0.20260906 types-pytz==2026.4.0.20260926
|
|
||||||
|
|
||||||
# mypy on exactly the modules in mypy-clean.txt; fails on any error in
|
|
||||||
# them, or if a listed file is missing. See CONTRIBUTING.md.
|
|
||||||
- name: Run mypy on the ratchet list
|
|
||||||
run: python scripts/check_types.py
|
|
||||||
|
|
||||||
sports-drift-report:
|
|
||||||
name: Sports drift report (report only)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# A progress measure for docs/SPORTS_UNIFICATION.md, never a gate: the
|
|
||||||
# monorepo's own check_sports_drift.py is the gate. The step summary shows
|
|
||||||
# how many bodies each scoreboard method family still has.
|
|
||||||
continue-on-error: true
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Check out ledmatrix-plugins (main)
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
repository: ChuckBuilds/ledmatrix-plugins
|
|
||||||
path: ledmatrix-plugins
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
|
|
||||||
# Stdlib only; exits 0 whatever it finds.
|
|
||||||
- name: Report method-family drift across the nine scoreboards
|
|
||||||
run: |
|
|
||||||
python scripts/sports_drift_report.py --plugins ledmatrix-plugins \
|
|
||||||
--markdown --json sports-drift.json >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
python scripts/sports_drift_report.py --plugins ledmatrix-plugins
|
|
||||||
|
|
||||||
- name: Upload the full report
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
||||||
with:
|
|
||||||
name: sports-drift-report
|
|
||||||
path: sports-drift.json
|
|
||||||
@@ -3,13 +3,11 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# Secrets and per-device state. Everything the software writes into config/
|
# Secrets
|
||||||
# is local to one device -- config.json, config_secrets.json, wifi_config.json,
|
config/config_secrets.json
|
||||||
# ytm_auth.json (a login session), saved_repositories.json, font_overrides.json,
|
config/config.json
|
||||||
# and the temp files atomic writes leave behind when interrupted -- so only
|
config/config.json.backup
|
||||||
# the templates are tracked. Listing files one by one missed several.
|
config/wifi_config.json
|
||||||
config/*
|
|
||||||
!config/*.template.json
|
|
||||||
credentials.json
|
credentials.json
|
||||||
token.pickle
|
token.pickle
|
||||||
|
|
||||||
@@ -37,12 +35,11 @@ htmlcov/
|
|||||||
# Cache directory (root level only, not src/cache which is source code)
|
# Cache directory (root level only, not src/cache which is source code)
|
||||||
/cache/
|
/cache/
|
||||||
|
|
||||||
# Development plugins directory: symlinks into a ledmatrix-plugins checkout
|
# Development plugins directory
|
||||||
# See docs/PLUGIN_DEVELOPMENT_GUIDE.md and docs/MULTI_ROOT_WORKSPACE_SETUP.md
|
# Plugins are managed as separate repositories via multi-root workspace
|
||||||
|
# See docs/MULTI_ROOT_WORKSPACE_SETUP.md for details
|
||||||
plugins/*
|
plugins/*
|
||||||
!plugins/.gitkeep
|
!plugins/.gitkeep
|
||||||
# Local settings for scripts/dev/dev_plugin_setup.sh (template: dev_plugins.json.example)
|
|
||||||
/dev_plugins.json
|
|
||||||
|
|
||||||
# Binary files and backups
|
# Binary files and backups
|
||||||
bin/pixlet/
|
bin/pixlet/
|
||||||
@@ -50,40 +47,3 @@ config/backups/
|
|||||||
|
|
||||||
# Starlark apps runtime storage (installed .star files and cached renders)
|
# Starlark apps runtime storage (installed .star files and cached renders)
|
||||||
/starlark-apps/
|
/starlark-apps/
|
||||||
|
|
||||||
# JS test deps (test/js)
|
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# Team logos fetched at runtime.
|
|
||||||
#
|
|
||||||
# src/logo_downloader.py and LogoHelper write into assets/sports/<league>_logos/
|
|
||||||
# whenever a plugin meets a team whose logo is not on disk. Those directories are
|
|
||||||
# also tracked -- 209 NCAA logos and 153 soccer ones ship with the repo -- so
|
|
||||||
# every rig accumulated untracked files it was never meant to commit and
|
|
||||||
# `git status` was permanently dirty. That noise is not harmless: it trains
|
|
||||||
# everyone to ignore the one signal that says a checkout is not what you think
|
|
||||||
# it is, which is how a stale tree sat unnoticed on a rig until a restart
|
|
||||||
# surfaced four dead sports plugins.
|
|
||||||
#
|
|
||||||
# Ignoring a directory does not untrack what is already in it, so the logos that
|
|
||||||
# ship keep shipping. Only new downloads are hidden.
|
|
||||||
#
|
|
||||||
# Adding a logo on purpose is rare and deliberate -- the last time was #415, four
|
|
||||||
# named NCAA logos a plugin needed, and there has been no other in a year. Do it
|
|
||||||
# with an explicit override:
|
|
||||||
# git add -f assets/sports/ncaa_logos/DUKE.png
|
|
||||||
assets/sports/*_logos/
|
|
||||||
assets/stocks/ticker_icons/
|
|
||||||
assets/stocks/crypto_icons/
|
|
||||||
|
|
||||||
# Plugin operation state written at runtime.
|
|
||||||
#
|
|
||||||
# web_interface/app.py writes data/plugin_state.json and data/operation_history.json
|
|
||||||
# (older releases also data/plugin_operations.json) as the web interface runs, into
|
|
||||||
# a directory that ships tracked (data/.gitkeep). Unignored, every rig that ever
|
|
||||||
# opened the web UI -- and every test run that constructs the app -- would leave
|
|
||||||
# untracked files behind and a permanently dirty `git status`. Same reasoning as
|
|
||||||
# the logo rule above: a checkout that is always dirty is a checkout nobody reads.
|
|
||||||
data/*
|
|
||||||
!data/.gitkeep
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
[submodule "rpi-rgb-led-matrix-master"]
|
[submodule "rpi-rgb-led-matrix-master"]
|
||||||
path = rpi-rgb-led-matrix-master
|
path = rpi-rgb-led-matrix-master
|
||||||
url = https://github.com/hzeller/rpi-rgb-led-matrix.git
|
url = https://github.com/hzeller/rpi-rgb-led-matrix.git
|
||||||
branch = master
|
|
||||||
|
|||||||
@@ -37,22 +37,14 @@ repos:
|
|||||||
types: [python]
|
types: [python]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
# The mypy ratchet -- the same check as CI's "Type check (mypy ratchet)"
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
# job: mypy on exactly the modules listed in mypy-clean.txt. Run it with
|
rev: v1.8.0
|
||||||
# pre-commit run mypy --hook-stage manual
|
|
||||||
# A local hook rather than mirrors-mypy so mypy sees the packages installed
|
|
||||||
# from requirements.txt, as CI does; an isolated hook env without them types
|
|
||||||
# PIL, requests and friends as Any and reports different errors. Needs
|
|
||||||
# mypy==1.20.2 (the version CI pins) in the environment you commit from.
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
name: mypy (ratchet, mypy-clean.txt)
|
additional_dependencies: [types-requests, types-pytz]
|
||||||
entry: python scripts/check_types.py
|
args: [--ignore-missing-imports, --no-error-summary]
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
files: ^src/
|
||||||
stages: [manual]
|
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/PyCQA/bandit
|
||||||
rev: 1.8.3
|
rev: 1.8.3
|
||||||
|
|||||||
@@ -4,57 +4,28 @@
|
|||||||
- `src/plugin_system/` — Plugin loader, manager, store manager, base plugin class
|
- `src/plugin_system/` — Plugin loader, manager, store manager, base plugin class
|
||||||
- `web_interface/` — Flask web UI (blueprints, templates, static JS)
|
- `web_interface/` — Flask web UI (blueprints, templates, static JS)
|
||||||
- `config/config.json` — User plugin configuration (persists across plugin reinstalls)
|
- `config/config.json` — User plugin configuration (persists across plugin reinstalls)
|
||||||
- `plugin-repos/` — **Default** plugin install directory used by the
|
- `plugins/` — Installed plugins directory (gitignored)
|
||||||
Plugin Store, set by `plugin_system.plugins_directory` in
|
- `plugin-repos/` — Development symlinks to monorepo plugin dirs
|
||||||
`config.json` (default per `config/config.template.json`).
|
|
||||||
Not gitignored.
|
|
||||||
- `plugins/` — Legacy/dev plugin location. Gitignored (`plugins/*`).
|
|
||||||
Used by `scripts/dev/dev_plugin_setup.sh` for symlinks. The plugin
|
|
||||||
loader does NOT fall back to it — `PluginManager.discover_plugins()`
|
|
||||||
(`src/plugin_system/plugin_manager.py`) scans only the configured
|
|
||||||
directory. Fallbacks exist in two narrower places: store operations
|
|
||||||
(`PluginStoreManager._find_plugin_path()` in `store_manager.py`, which
|
|
||||||
searches `store_search_dirs()` from `plugin_dirs.py`) and schema lookup
|
|
||||||
(`SchemaManager.get_schema_path()` in `schema_manager.py`, which probes
|
|
||||||
`plugins/` *before* `plugin-repos/`).
|
|
||||||
- `src/plugin_system/plugin_dirs.py` — the one resolver for "which directory
|
|
||||||
holds plugin X" (manifest `id` first, then `<id>` / `ledmatrix-<id>`)
|
|
||||||
|
|
||||||
## Plugin System
|
## Plugin System
|
||||||
- Plugins inherit from `BasePlugin` in `src/plugin_system/base_plugin.py`
|
- Plugins inherit from `BasePlugin` in `src/plugin_system/base_plugin.py`
|
||||||
- Required abstract methods: `update()`, `display(force_clear=False)`
|
- Required abstract methods: `update()`, `display(force_clear=False)`
|
||||||
- Each plugin needs: `manifest.json`, `config_schema.json`, and the entry point (`manager.py` by default); `requirements.txt` if it has dependencies. Required manifest fields: `docs/PLUGIN_API_REFERENCE.md#manifest-required-fields`
|
- Each plugin needs: `manifest.json`, `config_schema.json`, `manager.py`, `requirements.txt`
|
||||||
- Plugin instantiation args: `plugin_id, config, display_manager, cache_manager, plugin_manager`
|
- Plugin instantiation args: `plugin_id, config, display_manager, cache_manager, plugin_manager`
|
||||||
- Config schemas use JSON Schema Draft-7
|
- Config schemas use JSON Schema Draft-7
|
||||||
- Display dimensions: always read dynamically from `self.display_manager.width/height` — not `display_manager.matrix.width/height`, because `matrix` is `None` when hardware init fails (the properties fall back to the canvas size)
|
- 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>` clones the `ledmatrix-plugins` monorepo into `~/.ledmatrix-dev-plugins/` and links its `plugins/<name>` under the manifest id (add a repo URL for a plugin with its own repo; or `link <name> <path>`); symlinks land in `plugins/` — set `plugin_system.plugins_directory` to `plugins` so discovery picks them up. Fork/location overrides: `dev_plugins.json` (from `dev_plugins.json.example`)
|
|
||||||
- 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>`
|
|
||||||
- Soak a rig for frame timing (on the Pi, service running): `python3 scripts/frame_soak.py --preview` — late-frame rate across every scroller; see `docs/SCROLL_PERFORMANCE.md`
|
|
||||||
|
|
||||||
## Plugin Store Architecture
|
## Plugin Store Architecture
|
||||||
- Official plugins live in the `ledmatrix-plugins` monorepo (not individual repos)
|
- Official plugins live in the `ledmatrix-plugins` monorepo (not individual repos)
|
||||||
- Plugin repo naming convention: `ledmatrix-<plugin-id>` (e.g., `ledmatrix-football-scoreboard`)
|
- Plugin repo naming convention: `ledmatrix-<plugin-id>` (e.g., `ledmatrix-football-scoreboard`)
|
||||||
- `plugins.json` registry at `https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins.json`
|
- `plugins.json` registry at `https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins.json`
|
||||||
- Store manager (`PluginStoreManager` in `src/plugin_system/store_manager.py`) handles install/update/uninstall
|
- Store manager (`src/plugin_system/store_manager.py`) handles install/update/uninstall
|
||||||
- Monorepo plugins are installed without a `.git` directory: GitHub Trees API + raw downloads, falling back to ZIP extraction
|
- Monorepo plugins are installed via ZIP extraction (no `.git` directory)
|
||||||
- Update detection for monorepo plugins uses version comparison (manifest version vs registry latest_version)
|
- Update detection for monorepo plugins uses version comparison (manifest version vs registry latest_version)
|
||||||
- Optional registry entry fields (`store_registry.py`): `ledmatrix_min_version` refuses an incompatible install/update before the download (the post-download manifest gate stays as the fallback); `aliases` are the entry's other ids (manifest id `ledmatrix-weather` for `weather`), used with the `plugin_path` name by update/uninstall/reinstall to find the install — only this registry proof counts, never a bare `ledmatrix-<id>` folder (owner decision, #686; such a folder is only logged); `commit` is informational. An older plugins.json has none of them
|
|
||||||
- Plugin configs stored in `config/config.json`, NOT in plugin directories — safe across reinstalls
|
- 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`
|
- Third-party plugins can use their own repo URL with empty `plugin_path`
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
- paho-mqtt 2.x requires a `CallbackAPIVersion` argument: `VERSION1` for code written against v1 callback signatures (the MQTT bridge uses `VERSION2`)
|
- 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()`
|
- 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
|
- When modifying a plugin in the monorepo, you MUST bump `version` in its `manifest.json` and run `python update_registry.py` — otherwise users won't receive the update
|
||||||
- `src/pi5_matrix_support.py` hardcodes what the pinned `rpi-rgb-led-matrix-master` can drive on a Raspberry Pi 5 (`Rp1PioConfigSupported()` in `lib/rp1/rp1_pio_backend.cc`). Re-check it whenever the submodule is bumped: a stale rule blocks Pi 5 settings the new library supports, and a missing one lets the display service crash-loop. `src/matrix_support.py` holds the same kind of rules for every board (rows, chain length, mapping names, parallel per mapping) and needs the same re-check
|
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
# 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/RdrC37rEag) (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
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# 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/RdrC37rEag).
|
|
||||||
- **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), `bandit`,
|
|
||||||
and `gitleaks` — install the CLI with
|
|
||||||
`python -m pip install pre-commit`, then run
|
|
||||||
`pre-commit install` so they run on every commit. Type checking
|
|
||||||
is a ratchet while the existing mypy errors in `src/` are paid
|
|
||||||
down: `mypy-clean.txt` lists the modules that type-check clean, and
|
|
||||||
CI runs `python scripts/check_types.py` (also the manual hook
|
|
||||||
`pre-commit run mypy --hook-stage manual`) to keep every listed
|
|
||||||
module clean. When you make another module clean, add it to the
|
|
||||||
list (sorted); don't take one off to get CI green. Keep type fixes
|
|
||||||
annotation-only where you can -- widen a hint rather than delete a
|
|
||||||
defensive runtime check mypy calls unreachable. HTML/JS in
|
|
||||||
`web_interface/` follows the patterns already in `templates/v3/`
|
|
||||||
and `static/v3/`. If you change a template or a static JS file,
|
|
||||||
run `python3 scripts/build_css.py` and commit the regenerated
|
|
||||||
`static/v3/tailwind.css` with it -- CI fails when the committed CSS
|
|
||||||
is out of date. It needs no Node; see
|
|
||||||
[`web_interface/README.md`](web_interface/README.md#styling-tailwind-css).
|
|
||||||
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.
|
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
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,74 +0,0 @@
|
|||||||
# Product
|
|
||||||
|
|
||||||
<!-- impeccable:product-schema 1 -->
|
|
||||||
|
|
||||||
## Platform
|
|
||||||
|
|
||||||
web
|
|
||||||
|
|
||||||
## Users
|
|
||||||
|
|
||||||
Designed novice-first, with power tools kept within reach.
|
|
||||||
|
|
||||||
- **Primary: hobbyist builders.** People who assembled an LED matrix panel on a Raspberry Pi, often by following the install video, and are frequently new to Linux and the Pi. They set the display up once (panel size, timezone, WiFi), install and enable a few plugins, then come back occasionally to tweak what the panel shows. They usually reach the control panel from a phone or laptop on their home network, sometimes as an installed home-screen app.
|
|
||||||
- **Secondary: tinkerers and plugin developers.** Comfortable with SSH, `config.json`, and GitHub. They lean on the Config Editor, Logs, Cache, Operation History, Tools, GitHub-repo installs, and per-plugin config while building or debugging. Their tools must stay reachable without sitting in the novice's path.
|
|
||||||
|
|
||||||
## Product Purpose
|
|
||||||
|
|
||||||
LEDMatrix turns a Raspberry Pi and an RGB LED matrix panel into an information-rich display (clock, weather, calendar, sports scores, stocks, music, and more) through a plugin platform. The web control panel ("LED Matrix Control") is where the display gets configured, extended, and kept healthy.
|
|
||||||
|
|
||||||
Success means a builder gets from a freshly flashed Pi to a working, personalized display without needing a terminal, and can keep it running (updates, recovery, troubleshooting) the same way.
|
|
||||||
|
|
||||||
## Positioning
|
|
||||||
|
|
||||||
Four strengths define LEDMatrix, and future work must protect all of them:
|
|
||||||
|
|
||||||
1. **Plugin ecosystem.** The core ships only `starlark-apps` and `web-ui-info`; everything else comes from the built-in Plugin Store (the official `ledmatrix-plugins` monorepo), third-party GitHub repos, or Starlark (Tidbyt-style) apps. Each installed plugin gets its own configuration tab, generated from its schema.
|
|
||||||
2. **Runs on tiny Pis.** The UI is served by the same device that drives the matrix, on boards as small as the Pi Zero 2 W (512 MB), Pi 3/3B+, and the 1 GB Pi 4.
|
|
||||||
3. **Recovers without SSH.** WiFi access-point fallback with a captive setup page, backup & restore, in-UI updates, live logs, diagnostics, service control, and plugin health let users fix problems from the browser.
|
|
||||||
4. **Open and community-led.** GPL-3.0, a Discord community, and contributions welcome. The maintainer (ChuckBuilds) builds in public and openly relies on AI development tools.
|
|
||||||
|
|
||||||
## Operating Context
|
|
||||||
|
|
||||||
- **Access.** Served on the local network at `http://<pi-ip>:5000` by the `ledmatrix-web` service. It is installable as a PWA (`web_interface/static/v3/manifest.json`, short name "LEDMatrix").
|
|
||||||
- **First run.** When the Pi has no network it creates its own WiFi access point, so the captive setup page (`templates/v3/captive_setup.html`) may be the very first screen a user sees, on a phone, with no internet connection.
|
|
||||||
- **Navigation.**
|
|
||||||
- System tabs: Overview, General, WiFi, Schedule, Display, Rotation, Config Editor, Backup & Restore, Fonts, Logs, Cache, Operation History, Tools.
|
|
||||||
- A second row holds Plugin Manager (with the Plugin Store), Starlark Apps, and one tab per installed plugin.
|
|
||||||
- **Live data.** The Overview shows system stats (CPU, memory, temperature, power/throttling) and a live display preview, streamed over SSE.
|
|
||||||
- **Getting Started checklist.** The Overview's first-run checklist runs: set panel size → set timezone → install a plugin → enable it → configure it.
|
|
||||||
- **Development.** `python3 scripts/dev_server.py` gives a browser preview without the display loop; `python3 run.py -e` runs the full display in emulator mode.
|
|
||||||
|
|
||||||
## Capabilities and Constraints
|
|
||||||
|
|
||||||
- **Hard constraint: plugin UI compatibility.** Third-party plugins rely on JSON Schema (Draft-7) generated config forms, the widget registry (`static/v3/js/widgets/`), `x-secret` fields, and plugin web-UI actions. UI changes must keep these working.
|
|
||||||
- **Config storage.** Plugin configuration lives in `config/config.json` and secrets in `config/config_secrets.json`, never in plugin directories, so configs survive reinstalls.
|
|
||||||
- **Stack.** An existing Flask + HTMX + Alpine.js app with Jinja templates (`web_interface/templates/v3/`) and static JS/CSS (`web_interface/static/v3/`), with self-hosted vendor assets.
|
|
||||||
- **Terminology.** Plugin, Plugin Store, Starlark app, rotation, display duration, Vegas Scroll Mode, on-demand, AP mode.
|
|
||||||
- **Open decisions** (offered during init, not adopted as constraints):
|
|
||||||
- Whether the UI must work fully offline, with no CDN fallbacks at runtime.
|
|
||||||
- Whether a Node/CSS build step is acceptable for contributors.
|
|
||||||
- Whether a formal accessibility standard (e.g. WCAG 2.2 AA) is a requirement.
|
|
||||||
|
|
||||||
## Brand Commitments
|
|
||||||
|
|
||||||
- **Names.** The product is "LEDMatrix" and the web UI is titled "LED Matrix Control". The maintainer brand is ChuckBuilds.
|
|
||||||
- **Voice.** Friendly, honest, and learning-in-public, as in the README.
|
|
||||||
- **App icons.** They live in `web_interface/static/v3/icons/`.
|
|
||||||
|
|
||||||
No other visual identity has been made binding.
|
|
||||||
|
|
||||||
## Evidence on Hand
|
|
||||||
|
|
||||||
- **Photos.** Real photographs of running displays are linked in `README.md` (clock, weather, calendar, NHL/MLB/NFL/NCAA, stocks, music).
|
|
||||||
- **Video.** YouTube install and walkthrough videos from ChuckBuilds.
|
|
||||||
- **Docs.** Extensive documentation in `docs/`, e.g. `WEB_INTERFACE_GUIDE.md`, `GETTING_STARTED.md`, `WIFI_NETWORK_SETUP.md`, `LOW_MEMORY_BOARDS.md`, `PLUGIN_STORE_GUIDE.md`.
|
|
||||||
- **Absences.** There are no testimonials, user counts, or benchmark figures. Do not fabricate them.
|
|
||||||
|
|
||||||
## Product Principles
|
|
||||||
|
|
||||||
1. **Novice path first, power one click away.** Default views serve the first-time builder, while advanced tools stay discoverable for tinkerers.
|
|
||||||
2. **Never strand the user at a terminal.** Every setup, recovery, and troubleshooting task has a browser path, including from the AP-mode captive page.
|
|
||||||
3. **Respect the Pi.** Every feature is paid for in memory and CPU on a Pi Zero 2 W that is also driving the display.
|
|
||||||
4. **The ecosystem is the product.** Plugins, including third-party ones, must feel first-class and keep working across core UI changes.
|
|
||||||
5. **Honest and welcoming.** Plain language, truthful status, and no overstated claims, in keeping with an open, community-built project.
|
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
# LEDMatrix
|
# 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 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.
|
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.
|
||||||
|
|
||||||
@@ -33,7 +27,7 @@ I'm trying to be open to constructive criticism and support, as long as it's a r
|
|||||||
- Show support on Youtube: https://www.youtube.com/@ChuckBuilds
|
- Show support on Youtube: https://www.youtube.com/@ChuckBuilds
|
||||||
- Check out the write-up on my website: https://www.chuck-builds.com/led-matrix/
|
- Check out the write-up on my website: https://www.chuck-builds.com/led-matrix/
|
||||||
- Stay in touch on Instagram: https://www.instagram.com/ChuckBuilds/
|
- Stay in touch on Instagram: https://www.instagram.com/ChuckBuilds/
|
||||||
- Want to chat? Reach out on the LEDMatrix Discord: [https://discord.gg/RdrC37rEag](https://discord.gg/RdrC37rEag)
|
- Want to chat? Reach out on the LEDMatrix Discord: [https://discord.com/invite/uW36dVAtcT](https://discord.gg/dfFwsasa6W)
|
||||||
- Feeling Generous? Consider sponsoring this project or sending a donation (these AI credits aren't cheap!)
|
- Feeling Generous? Consider sponsoring this project or sending a donation (these AI credits aren't cheap!)
|
||||||
|
|
||||||
-----------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------
|
||||||
@@ -50,15 +44,7 @@ I'm trying to be open to constructive criticism and support, as long as it's a r
|
|||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Core Features</summary>
|
<summary>Core Features</summary>
|
||||||
LEDMatrix is a plugin platform: the displays below are plugins installed
|
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:
|
||||||
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
|
### Time and Weather
|
||||||
- Real-time clock display (2x 64x32 Displays 4mm Pixel Pitch)
|
- Real-time clock display (2x 64x32 Displays 4mm Pixel Pitch)
|
||||||
@@ -140,36 +126,24 @@ 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. |
|
| 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
|
||||||
- **Raspberry Pi 3B, 4, or 5** (a Pi Zero 2 W also works, with the limits described under the 1GB/low-memory bullet below; the original Pi Zero / Zero W doesn't have enough processing power for this project)
|
- 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!)**
|
||||||
[Amazon Affiliate Link – Raspberry Pi 4 4GB RAM](https://amzn.to/4dJixuX)
|
[Amazon Affiliate Link – Raspberry Pi 4 4GB RAM](https://amzn.to/4dJixuX)
|
||||||
[Amazon Affiliate Link – Raspberry Pi 4 8GB RAM](https://amzn.to/4qbqY7F)
|
[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 start `gpio_slowdown` at `1`, raising it a step at a time if the image flickers or shows garbage (see `gpio_slowdown` under Display Settings).
|
|
||||||
- **1GB models (Pi 3B / 3B+), the 512MB Pi Zero 2 W 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`. Once running, keep an eye on memory: see [docs/LOW_MEMORY_BOARDS.md](docs/LOW_MEMORY_BOARDS.md).
|
|
||||||
|
|
||||||
|
|
||||||
### Operating system
|
|
||||||
- **Raspberry Pi OS Lite, Trixie (Debian 13) or Bookworm (Debian 12)**, 64-bit recommended. Trixie is the current release and the one to pick for a new SD card; an existing Bookworm install works as it is, no upgrade needed. The installer checks this first and stops with directions on anything else (Bullseye and older, the desktop edition, other distributions).
|
|
||||||
- **Python**: whatever the OS ships, 3.13 on Trixie and 3.11 on Bookworm. Don't install a different Python; the installer and the services use the system `python3`.
|
|
||||||
- **Networking**: NetworkManager, the default on both. Choosing a WiFi network from the web page and the `LEDMatrix-Setup` hotspot need it; if you switched to dhcpcd in `raspi-config`, switch back (Advanced Options → Network Config → NetworkManager).
|
|
||||||
|
|
||||||
### RGB Matrix Bonnet / HAT
|
### RGB Matrix Bonnet / HAT
|
||||||
- [Adafruit RGB Matrix Bonnet/HAT](https://www.adafruit.com/product/3211) – supports one “chain” of horizontally connected displays
|
- [Adafruit RGB Matrix Bonnet/HAT](https://www.adafruit.com/product/3211) – supports one “chain” of horizontally connected displays
|
||||||
- [Adafruit Triple LED Matrix Bonnet](https://www.adafruit.com/product/6358) – supports up to 3 vertical “chains” of horizontally connected displays *(use `regular` as hardware mapping)*
|
- [Adafruit Triple LED Matrix Bonnet](https://www.adafruit.com/product/6358) – supports up to 3 vertical “chains” of horizontally connected displays *(use `regular-pi1` as hardware mapping)*
|
||||||
- [Electrodragon RGB HAT](https://www.electrodragon.com/product/rgb-matrix-panel-drive-board-raspberry-pi/) – supports up to 3 vertical “chains”
|
- [Electrodragon RGB HAT](https://www.electrodragon.com/product/rgb-matrix-panel-drive-board-raspberry-pi/) – supports up to 3 vertical “chains”
|
||||||
- [Seengreat Matrix Adapter Board](https://amzn.to/3KsnT3j) – single-chain LED Matrix *(use `regular` as hardware mapping)*
|
- [Seengreat Matrix Adapter Board](https://amzn.to/3KsnT3j) – single-chain LED Matrix *(use `regular` as hardware mapping)*
|
||||||
|
|
||||||
### LED Matrix Panels
|
### LED Matrix Panels
|
||||||
(2x in a horizontal chain is recommended)
|
(2x in a horizontal chain is recommended)
|
||||||
- [Adafruit 64×32](https://www.adafruit.com/product/2278) – designed for 128×32 but works with dynamic scaling on many displays (pixel pitch is user preference)
|
- [Adafruit 64×32](https://www.adafruit.com/product/2278) – designed for 128×32 but works with dynamic scaling on many displays (pixel pitch is user preference)
|
||||||
**Warning: Lately the Waveshare Panels have had different variations - only some are compatible with this project. I hope to identify what is different to fix it but so far there is a decent chance you get a mis-matched set of panels if you don't buy them all at once! **
|
|
||||||
- [Waveshare 64×32](https://amzn.to/3Kw55jK) - Does not require E addressable pad
|
- [Waveshare 64×32](https://amzn.to/3Kw55jK) - Does not require E addressable pad
|
||||||
- [Waveshare 96×48](https://amzn.to/4bydNcv) – higher resolution, requires soldering the **E addressable pad** on the [Adafruit RGB Bonnet](https://www.adafruit.com/product/3211) to “8” **OR** toggling the DIP switch on the Adafruit Triple LED Matrix Bonnet *(no soldering required!)*
|
- [Waveshare 96×48](https://amzn.to/4bydNcv) – higher resolution, requires soldering the **E addressable pad** on the [Adafruit RGB Bonnet](https://www.adafruit.com/product/3211) to “8” **OR** toggling the DIP switch on the Adafruit Triple LED Matrix Bonnet *(no soldering required!)*
|
||||||
- There are some Panels on Aliexpress that have worked fine for me, shop around! I think Adafruit is probably the "safest" but they do have some limitation on resolution and layout.
|
> Amazon Affiliate Link – ChuckBuilds receives a small commission on purchases
|
||||||
> Amazon Affiliate Links – ChuckBuilds receives a small commission on purchases
|
|
||||||
|
|
||||||
### Power Supply
|
### Power Supply
|
||||||
- [5V 4A DC Power Supply](https://www.adafruit.com/product/658) (good for 2 -3 displays, depending on brightness and pixel density, you'll need higher amperage for more)
|
- [5V 4A DC Power Supply](https://www.adafruit.com/product/658) (good for 2 -3 displays, depending on brightness and pixel density, you'll need higher amperage for more)
|
||||||
@@ -177,7 +151,7 @@ The system supports live, recent, and upcoming game information for multiple spo
|
|||||||
|
|
||||||
## Optional but recommended mod for Adafruit RGB Matrix Bonnet
|
## Optional but recommended mod for Adafruit RGB Matrix Bonnet
|
||||||
- By soldering a jumper between pins 4 and 18, you can run a specialized command for polling the matrix display. This provides better brightness, less flicker, and better color.
|
- By soldering a jumper between pins 4 and 18, you can run a specialized command for polling the matrix display. This provides better brightness, less flicker, and better color.
|
||||||
- The default config uses `hardware_mapping` `adafruit-hat`. If you do the mod, change it to `adafruit-hat-pwm` (Display settings in the web interface, or `config.json`)
|
- If you do the mod, we will use the default config with led-gpio-mapping=adafruit-hat-pwm, otherwise just adjust your mapping in config.json to adafruit-hat
|
||||||
- More information available: https://github.com/hzeller/rpi-rgb-led-matrix/tree/master?tab=readme-ov-file
|
- More information available: https://github.com/hzeller/rpi-rgb-led-matrix/tree/master?tab=readme-ov-file
|
||||||

|

|
||||||
|
|
||||||
@@ -254,7 +228,7 @@ These are not required and you can probably rig up something basic with stuff yo
|
|||||||
|
|
||||||
<img width="512" height="361" alt="Step 2 Other " src="https://github.com/user-attachments/assets/166a22e8-8067-48df-9f80-50c91f573356" />
|
<img width="512" height="361" alt="Step 2 Other " src="https://github.com/user-attachments/assets/166a22e8-8067-48df-9f80-50c91f573356" />
|
||||||
|
|
||||||
5. Then choose Raspbian OS (64-bit) Lite (Trixie). Bookworm Lite (listed as Legacy) also works; see [Operating system](#operating-system) below
|
5. Then choose Raspbian OS (64-bit) Lite (Trixie)
|
||||||
|
|
||||||
<img width="512" height="361" alt="Step 4 Trixie Lite 64" src="https://github.com/user-attachments/assets/3b8590ce-b810-4dfe-9253-26e0d4f8ed1e" />
|
<img width="512" height="361" alt="Step 4 Trixie Lite 64" src="https://github.com/user-attachments/assets/3b8590ce-b810-4dfe-9253-26e0d4f8ed1e" />
|
||||||
|
|
||||||
@@ -329,13 +303,12 @@ curl -fsSL https://raw.githubusercontent.com/ChuckBuilds/LEDMatrix/main/scripts/
|
|||||||
```
|
```
|
||||||
|
|
||||||
This one-shot installer will automatically:
|
This one-shot installer will automatically:
|
||||||
- Check system prerequisites (network, disk space, memory, sudo access)
|
- Check system prerequisites (network, disk space, sudo access)
|
||||||
- Install required system packages (git, python3, build tools, etc.)
|
- Install required system packages (git, python3, build tools, etc.)
|
||||||
- Clone or update the LEDMatrix repository
|
- Clone or update the LEDMatrix repository
|
||||||
- Run the complete first-time installation script
|
- Run the complete first-time installation script
|
||||||
- Print the web interface address, then **reboot the Pi automatically** (your SSH session will disconnect; give it a few minutes to come back)
|
|
||||||
|
|
||||||
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.
|
The installation process typically takes 10-30 minutes depending on your internet connection and Pi model. All errors are reported explicitly with actionable fixes.
|
||||||
|
|
||||||
**Note:** The script is safe to run multiple times and will handle existing installations gracefully.
|
**Note:** The script is safe to run multiple times and will handle existing installations gracefully.
|
||||||
|
|
||||||
@@ -352,10 +325,10 @@ If you prefer to install manually or the one-shot installer doesn't work for you
|
|||||||
ssh ledpi@ledpi
|
ssh ledpi@ledpi
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Update repositories, upgrade Raspberry Pi OS, and install git (`first_time_install.sh` installs the build dependencies itself: `python3-pip`, `python-dev-is-python3`, `build-essential`, `cmake`, `ninja-build` and the rest):
|
2. Update repositories, upgrade Raspberry Pi OS, and install prerequisites:
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && sudo apt upgrade -y
|
sudo apt update && sudo apt upgrade -y
|
||||||
sudo apt install -y git
|
sudo apt install -y git python3-pip cython3 build-essential python3-dev python3-pillow scons
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Clone this repository:
|
3. Clone this repository:
|
||||||
@@ -372,12 +345,6 @@ sudo bash ./first_time_install.sh
|
|||||||
|
|
||||||
This single script installs services, dependencies, configures permissions and sudoers, and validates the setup.
|
This single script installs services, dependencies, configures permissions and sudoers, and validates the setup.
|
||||||
|
|
||||||
It finishes by asking whether to reboot. If you run it non-interactively — piped, over a script, or with `-y` — there is no one to ask, so **it reboots immediately without prompting**. Pass `--no-reboot-prompt` to install without rebooting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo bash ./first_time_install.sh -y --no-reboot-prompt
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -393,10 +360,6 @@ sudo bash ./first_time_install.sh -y --no-reboot-prompt
|
|||||||
|
|
||||||
### Initial Setup
|
### 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:
|
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 .
|
Edit the project via the web interface at http://[IP ADDRESS or HOSTNAME]:5000 or http://ledpi:5000 .
|
||||||
|
|
||||||
@@ -405,7 +368,7 @@ If you need to manually edit your config file, you can follow the steps below:
|
|||||||
<summary>Manual Config.json editing </summary>
|
<summary>Manual Config.json editing </summary>
|
||||||
|
|
||||||
1. **First-time setup**:
|
1. **First-time setup**:
|
||||||
The previous `first_time_install.sh` script should've already copied the template to create your config.json:
|
The previous "First_time_install.sh" script should've already copied the template to create your config.json:
|
||||||
|
|
||||||
2. **Edit your configuration**:
|
2. **Edit your configuration**:
|
||||||
```bash
|
```bash
|
||||||
@@ -442,7 +405,7 @@ I recommend using the web-ui "Quick Actions" to control the Display.
|
|||||||
## Plugins
|
## Plugins
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### Plugin Store
|
### Plugin Store
|
||||||
See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-plugins) for detailed installation instructions.
|
See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-plugins) for detailed installation instructions.
|
||||||
@@ -464,9 +427,9 @@ You can also install plugins directly from GitHub repositories:
|
|||||||
|
|
||||||
See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-plugins) for detailed installation instructions.
|
See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-plugins) for detailed installation instructions.
|
||||||
|
|
||||||
For plugin development, the `plugins/hello-world/` plugin in the [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) repository is a starter template.
|
For plugin development, check out the [Hello World Plugin](https://github.com/ChuckBuilds/ledmatrix-hello-world) repository as a starter template.
|
||||||
|
|
||||||
**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.
|
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>
|
</details>
|
||||||
|
|
||||||
## Detailed Information
|
## Detailed Information
|
||||||
@@ -481,10 +444,6 @@ If you are copying my exact setup, you can likely leave the defaults alone. Howe
|
|||||||
|
|
||||||
The display settings are located in `config/config.json` under the `"display"` key and are organized into three main sections: `hardware`, `runtime`, and `display_durations`.
|
The display settings are located in `config/config.json` under the `"display"` key and are organized into three main sections: `hardware`, `runtime`, and `display_durations`.
|
||||||
|
|
||||||
The defaults below are the values in `config/config.template.json`. They are what applies when you haven't set a key: on every load, LEDMatrix adds any key your `config.json` lacks from the template, so `DisplayManager`'s own fallbacks are never reached on a normal install.
|
|
||||||
|
|
||||||
The web UI and the config API refuse values the rgbmatrix library can't start with. If one is written into `config.json` by hand anyway, the display logs which setting it is (`Failed to initialize RGB Matrix` in `sudo journalctl -u ledmatrix`), runs in fallback mode, and the Display tab shows the message.
|
|
||||||
|
|
||||||
### Hardware Configuration (`display.hardware`)
|
### Hardware Configuration (`display.hardware`)
|
||||||
|
|
||||||
These settings control the physical hardware configuration and how the matrix is driven.
|
These settings control the physical hardware configuration and how the matrix is driven.
|
||||||
@@ -494,18 +453,15 @@ These settings control the physical hardware configuration and how the matrix is
|
|||||||
- **`rows`** (integer, default: 32)
|
- **`rows`** (integer, default: 32)
|
||||||
- Number of LED rows (vertical pixels) in each panel
|
- Number of LED rows (vertical pixels) in each panel
|
||||||
- Common values: 16, 32, 48, 64
|
- Common values: 16, 32, 48, 64
|
||||||
- An even number from 8 to 64, the most the rgbmatrix library drives per panel
|
|
||||||
- Must match your physical panel configuration
|
- Must match your physical panel configuration
|
||||||
|
|
||||||
- **`cols`** (integer, default: 64)
|
- **`cols`** (integer, default: 64)
|
||||||
- Number of LED columns (horizontal pixels) in each panel
|
- Number of LED columns (horizontal pixels) in each panel
|
||||||
- Common values: 32, 64, 96, 128
|
- Common values: 32, 64, 96, 128
|
||||||
- At least 16, with no upper limit
|
|
||||||
- Must match your physical panel configuration
|
- Must match your physical panel configuration
|
||||||
|
|
||||||
- **`chain_length`** (integer, default: 2)
|
- **`chain_length`** (integer, default: 2)
|
||||||
- Number of LED panels chained together horizontally
|
- Number of LED panels chained together horizontally
|
||||||
- 1 to 255 (the library's Python binding stores it in one byte); longer chains lower the refresh rate
|
|
||||||
- If you have 2 panels side-by-side, set to 2
|
- If you have 2 panels side-by-side, set to 2
|
||||||
- If you have 4 panels in a row, set to 4
|
- If you have 4 panels in a row, set to 4
|
||||||
- Total display width = `cols × chain_length`
|
- Total display width = `cols × chain_length`
|
||||||
@@ -514,70 +470,68 @@ These settings control the physical hardware configuration and how the matrix is
|
|||||||
- Number of parallel chains (panels stacked vertically)
|
- Number of parallel chains (panels stacked vertically)
|
||||||
- Use 1 for a single row of panels
|
- Use 1 for a single row of panels
|
||||||
- Use 2 if you have panels stacked in two rows
|
- Use 2 if you have panels stacked in two rows
|
||||||
- 1–3, and no more than your `hardware_mapping` has outputs: `regular` and `classic` have 3 (e.g. the Adafruit Triple LED Matrix Bonnet); `adafruit-hat`, `adafruit-hat-pwm`, `regular-pi1` and `classic-pi1` have 1. The library stops the display service outright on a mismatch, so it is refused
|
|
||||||
- Total display height = `rows × parallel`
|
- Total display height = `rows × parallel`
|
||||||
|
|
||||||
#### Brightness and Visual Settings
|
#### Brightness and Visual Settings
|
||||||
|
|
||||||
- **`brightness`** (integer, 1-100, default: 90)
|
- **`brightness`** (integer, 0-100, default: 90)
|
||||||
- Display brightness level
|
- Display brightness level
|
||||||
- Lower values (1-50) are dimmer, higher values (50-100) are brighter
|
- Lower values (0-50) are dimmer, higher values (50-100) are brighter
|
||||||
- Recommended: 70-90 for indoor use, 90-100 for bright environments
|
- Recommended: 70-90 for indoor use, 90-100 for bright environments
|
||||||
- Very high brightness may cause distortion or require more power
|
- Very high brightness may cause distortion or require more power
|
||||||
|
|
||||||
#### Hardware Mapping
|
#### Hardware Mapping
|
||||||
|
|
||||||
- **`hardware_mapping`** (string, default: "adafruit-hat")
|
- **`hardware_mapping`** (string, default: "adafruit-hat-pwm")
|
||||||
- Specifies which GPIO pin mapping to use for your hardware
|
- Specifies which GPIO pin mapping to use for your hardware
|
||||||
- **`"adafruit-hat-pwm"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITH the jumper mod (PWM enabled). This is the recommended setting for Adafruit hardware with the PWM jumper soldered.
|
- **`"adafruit-hat-pwm"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITH the jumper mod (PWM enabled). This is the recommended setting for Adafruit hardware with the PWM jumper soldered.
|
||||||
- **`"adafruit-hat"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITHOUT the jumper mod (no PWM). Remove `-pwm` from the value if you did not solder the jumper.
|
- **`"adafruit-hat"`**: Use this for Adafruit RGB Matrix Bonnet/HAT WITHOUT the jumper mod (no PWM). Remove `-pwm` from the value if you did not solder the jumper.
|
||||||
- **`"regular"`**: Standard GPIO pin mapping for direct GPIO connections (Generic). Also the right choice for the Adafruit Triple LED Matrix Bonnet
|
- **`"regular"`**: Standard GPIO pin mapping for direct GPIO connections (Generic)
|
||||||
- **`"regular-pi1"`**: Standard GPIO pin mapping for Raspberry Pi 1 (older hardware or non-standard hat mapping)
|
- **`"regular-pi1"`**: Standard GPIO pin mapping for Raspberry Pi 1 (older hardware or non-standard hat mapping)
|
||||||
- **`"classic"`** / **`"classic-pi1"`**: the library's original pin-outs, for old adapter boards wired to them. Not used by current HATs
|
|
||||||
- Any other name is refused. `compute-module` is only compiled in when the library is built with `ENABLE_WIDE_GPIO_COMPUTE_MODULE`, which the installer doesn't do. On a Raspberry Pi 5, `classic-pi1` isn't supported
|
|
||||||
- Choose the option that matches your specific hardware setup, if aren't sure try them all.
|
- Choose the option that matches your specific hardware setup, if aren't sure try them all.
|
||||||
- Hardware pulsing (see `disable_hardware_pulsing`) needs the panel's OE line on GPIO 18, which `adafruit-hat-pwm` and `regular` provide and `adafruit-hat` does not
|
|
||||||
|
|
||||||
#### PWM (Pulse Width Modulation) Settings
|
#### PWM (Pulse Width Modulation) Settings
|
||||||
|
|
||||||
These settings affect color fidelity and smoothness of color transitions:
|
These settings affect color fidelity and smoothness of color transitions:
|
||||||
|
|
||||||
- **`pwm_bits`** (integer, 1-11, default: 9)
|
- **`pwm_bits`** (integer, default: 9)
|
||||||
- Color depth per channel: how many brightness levels each LED gets
|
- Number of bits used for PWM (affects color depth)
|
||||||
- Higher values (9-11) = more color levels, smoother gradients, lower refresh rate
|
- Higher values (9-11) = more color levels, smoother gradients
|
||||||
- Lower values (7-8) = the subtlest shades are dropped for a higher refresh rate; `1` gives 8 colors
|
- Lower values (7-8) = fewer color levels, but may improve stability on some hardware
|
||||||
- Recommended: 9-10
|
- Range: 1-11, recommended: 9-10
|
||||||
|
|
||||||
- **`pwm_dither_bits`** (integer, 0-2, default: 1)
|
- **`pwm_dither_bits`** (integer, default: 1)
|
||||||
- Time-dithers the lowest color bits: their brightness comes from showing them on only some frames
|
- Additional dithering bits for smoother color transitions
|
||||||
- Raises the refresh rate; the cost is that dark shades can shimmer slightly
|
- Helps reduce color banding in gradients
|
||||||
- `0` = steadiest dim colors, `2` = fastest
|
- Higher values (1-2) = smoother gradients but may impact performance
|
||||||
- The rgbmatrix library accepts only 0-2; a higher value stops the display starting
|
- Range: 0-2, recommended: 1
|
||||||
|
|
||||||
- **`pwm_lsb_nanoseconds`** (integer, 50-3000, default: 130)
|
- **`pwm_lsb_nanoseconds`** (integer, default: 130)
|
||||||
- On-time of the least significant color bit; each higher bit doubles it
|
- Least significant bit timing in nanoseconds
|
||||||
- Lower values = higher refresh rate, but can cost color accuracy or add ghosting on some panels
|
- Controls the base timing for PWM signals
|
||||||
- Higher values = less ghosting (faint trails behind bright text on black), lower refresh rate
|
- Lower values = faster PWM, higher values = slower PWM
|
||||||
- Typical range: 100-300 nanoseconds
|
- Typical range: 100-300 nanoseconds
|
||||||
|
- May need adjustment if you see flickering or color issues
|
||||||
|
|
||||||
#### Advanced Hardware Settings
|
#### Advanced Hardware Settings
|
||||||
|
|
||||||
- **`scan_mode`** (integer, 0-1, default: 0)
|
- **`scan_mode`** (integer, default: 0)
|
||||||
- Order the rows are refreshed in: `0` = progressive, `1` = interlaced
|
- Panel scan mode (how rows are addressed)
|
||||||
- Interlaced can look a little smoother when the refresh rate is very low, but usually shows a comb effect on anything moving
|
- Common values: 0 (progressive), 1 (interlaced)
|
||||||
- Leave at `0` unless you are tuning a slow setup
|
- Most panels use 0, but some require 1
|
||||||
|
- Check your panel datasheet if colors appear incorrect
|
||||||
|
|
||||||
- **`limit_refresh_rate_hz`** (integer, default: 100)
|
- **`limit_refresh_rate_hz`** (integer, default: 100)
|
||||||
- Caps the panel refresh rate in Hz; `0` = no cap
|
- Maximum refresh rate in Hz (frames per second)
|
||||||
- A steady cap reduces flicker caused by other activity on the Pi, and in camera recordings
|
- Caps the refresh rate for better stability
|
||||||
- Scroll speeds are worked out against this value (against 100 Hz when it is `0`), so a cap the panel can actually hold keeps scrolling even
|
- Lower values (60-80) = more stable, less CPU usage
|
||||||
- Recommended: 80-120. `sudo python3 scripts/scroll_speeds.py --measure` reports the rate your panel really achieves
|
- Higher values (100-120) = smoother animations, more CPU usage
|
||||||
|
- Recommended: 80-100 for most setups
|
||||||
|
|
||||||
- **`disable_hardware_pulsing`** (boolean, default: false)
|
- **`disable_hardware_pulsing`** (boolean, default: false)
|
||||||
- `false` = the Pi's hardware PWM times each brightness pulse; `true` = software timing
|
- Disables hardware pulsing (usually leave as false)
|
||||||
- Leave `false` where possible. Software timing is less exact, so a row, or the whole panel, can briefly flash brighter
|
- Set to `true` only if you experience timing issues
|
||||||
- Hardware pulsing needs the panel's OE line on GPIO 18 (`adafruit-hat-pwm`, `regular`, the Adafruit Triple LED Matrix Bonnet). With `adafruit-hat` the library uses software timing anyway
|
- Most users should leave this as `false`
|
||||||
- It also needs the Pi's onboard sound driver (`snd_bcm2835`) disabled, which `first_time_install.sh` does. Set `true` only if you need the Pi's own audio
|
|
||||||
|
|
||||||
- **`inverse_colors`** (boolean, default: false)
|
- **`inverse_colors`** (boolean, default: false)
|
||||||
- Inverts all colors (red becomes cyan, etc.)
|
- Inverts all colors (red becomes cyan, etc.)
|
||||||
@@ -585,9 +539,9 @@ These settings affect color fidelity and smoothness of color transitions:
|
|||||||
- Set to `true` only if colors appear inverted
|
- Set to `true` only if colors appear inverted
|
||||||
|
|
||||||
- **`show_refresh_rate`** (boolean, default: false)
|
- **`show_refresh_rate`** (boolean, default: false)
|
||||||
- Prints the live refresh rate to the console; nothing is drawn on the panel
|
- Displays the current refresh rate on the matrix (for debugging)
|
||||||
- Readable when you stop the service and run `sudo python3 run.py` in a terminal; under the service the output is buffered
|
- Set to `true` to see FPS on the display
|
||||||
- `sudo python3 scripts/scroll_speeds.py --measure` is an easier way to see the real refresh rate
|
- Useful for troubleshooting performance issues
|
||||||
|
|
||||||
#### Advanced Panel Configuration (Advanced Users Only)
|
#### Advanced Panel Configuration (Advanced Users Only)
|
||||||
|
|
||||||
@@ -597,7 +551,6 @@ These settings are typically only needed for non-standard panels or custom confi
|
|||||||
- Color channel order for your LED panel
|
- Color channel order for your LED panel
|
||||||
- Common values: "RGB", "RBG", "GRB", "GBR", "BRG", "BGR"
|
- Common values: "RGB", "RBG", "GRB", "GBR", "BRG", "BGR"
|
||||||
- Most panels use "RGB", but some use "GRB" or other orders
|
- Most panels use "RGB", but some use "GRB" or other orders
|
||||||
- If red shows as blue, try "BGR" (the Waveshare 96x48 V2 needs it)
|
|
||||||
- Check your panel datasheet if colors appear wrong
|
- Check your panel datasheet if colors appear wrong
|
||||||
|
|
||||||
- **`pixel_mapper_config`** (string, default: "")
|
- **`pixel_mapper_config`** (string, default: "")
|
||||||
@@ -607,76 +560,40 @@ These settings are typically only needed for non-standard panels or custom confi
|
|||||||
- Leave empty unless you need custom mapping
|
- Leave empty unless you need custom mapping
|
||||||
- See rpi-rgb-led-matrix documentation for full options
|
- See rpi-rgb-led-matrix documentation for full options
|
||||||
|
|
||||||
- **`orientation`** (string, default: "normal")
|
|
||||||
- Rotates the rendered image to match how the panel is physically mounted
|
|
||||||
- Set to `"180"` (or use the "Upside Down" option in the web UI's Display
|
|
||||||
settings) if the panel is mounted upside down — useful for optimizing
|
|
||||||
where the Raspberry Pi and wiring sit relative to the mounting location
|
|
||||||
- `"90"` and `"270"` are for a panel mounted on its side; they swap the
|
|
||||||
display's width and height
|
|
||||||
- Applied independently of `pixel_mapper_config` (appended as a trailing
|
|
||||||
`Rotate:<degrees>` mapper), so custom mapper configs keep working alongside it
|
|
||||||
|
|
||||||
- **`row_address_type`** (integer, default: 0)
|
- **`row_address_type`** (integer, default: 0)
|
||||||
- How rows are addressed on the panel
|
- How rows are addressed on the panel
|
||||||
- Most panels use 0 (direct addressing)
|
- Most panels use 0 (direct addressing)
|
||||||
- 1 = AB-addressed, 2 = direct row select, 3 = ABC-addressed,
|
- Some panels require 1 (AB addressing) or 2 (ABC addressing)
|
||||||
4 = ABC shift + DE direct (SM5266), 5 = SM5368 / B707 row shift register
|
|
||||||
- ABC panels (no E line, e.g. many 128x64 FM6124 panels) use 3
|
|
||||||
- Panels with SM5368 row drivers use 5 with `led_rgb_sequence` `"BGR"` —
|
|
||||||
e.g. the Waveshare 96x48 V2 (back silkscreen `24S-A1`; the V1, `24S-A2.1`,
|
|
||||||
uses the defaults). This is what Waveshare's `96X48_1_24_SM5368` panel
|
|
||||||
type sets in their library fork.
|
|
||||||
- SM5368 row drivers are timing-sensitive: if rows jump up and down or the
|
|
||||||
bottom row shows a copy of other rows, raise `gpio_slowdown`. On a Pi 4
|
|
||||||
with an Adafruit Triple LED Matrix Bonnet, 4 left rows jumping; 6–8 gave a
|
|
||||||
stable image.
|
|
||||||
- On a Raspberry Pi 5 the rgbmatrix library currently supports only 0 and 2
|
|
||||||
(and `parallel` 1-3). Anything else would crash the display service, so on
|
|
||||||
a Pi 5 the web UI offers only 0 and 2, the config API refuses the others,
|
|
||||||
and if one is set in `config.json` anyway the display logs why and runs in
|
|
||||||
fallback mode
|
|
||||||
- Check your panel datasheet if display appears corrupted
|
- Check your panel datasheet if display appears corrupted
|
||||||
|
|
||||||
- **`multiplexing`** (integer, 0-22, default: 0)
|
- **`multiplexing`** (integer, default: 0)
|
||||||
- How pixels are wired on outdoor/specialty panels (P10, P8, P4 and P3 outdoor modules and similar) whose LEDs aren't laid out in straight rows
|
- Panel multiplexing type
|
||||||
- `0` = direct (standard indoor panels)
|
- 0 = no multiplexing (standard panels)
|
||||||
- `1` Stripe, `2` Checkered, `3` Spiral, `4` ZStripe, `5` ZnMirrorZStripe,
|
- Higher values for panels with different multiplexing schemes
|
||||||
`6` Coreman, `7` Kaler2Scan, `8` ZStripeUneven, `9` P10-128x4-Z,
|
- Check your panel datasheet for the correct value
|
||||||
`10` QiangLiQ8, `11` InversedZStripe, `12`–`14` P10Outdoor1R1G1B v1–v3,
|
|
||||||
`15` P10CoremanMapper, `16` P8Outdoor1R1G1B, `17` FlippedStripe,
|
|
||||||
`18` P10-32x16-HalfScan, `19` P10-32x16-QuarterScan, `20` P3Outdoor-64x64,
|
|
||||||
`21` DoubleZMultiplex, `22` P4Outdoor-80x40
|
|
||||||
- If the image is scrambled in a repeating pattern, try the value named after your panel first
|
|
||||||
|
|
||||||
- **`panel_type`** (string, default: `""`)
|
|
||||||
- Sends a start-up initialization sequence to driver chips that need one
|
|
||||||
- `""` = Standard (no initialization) — right for most panels, including FM6124 / FM6124D / FM6124DJ
|
|
||||||
- `"FM6126A"` or `"FM6127"` for panels with those chips; try `"FM6126A"` if the panel stays dark or lights only the first pixel on Standard
|
|
||||||
|
|
||||||
### Runtime Configuration (`display.runtime`)
|
### Runtime Configuration (`display.runtime`)
|
||||||
|
|
||||||
These settings control runtime behavior and GPIO timing:
|
These settings control runtime behavior and GPIO timing:
|
||||||
|
|
||||||
- **`gpio_slowdown`** (integer, default: 3)
|
- **`gpio_slowdown`** (integer, default: 3)
|
||||||
- GPIO timing slowdown factor (0-10): slows GPIO writes so the panel electronics keep up. Higher is more reliable but lowers the refresh rate
|
- GPIO timing slowdown factor
|
||||||
- **Critical setting**: depends on your Raspberry Pi model and your panel
|
- **Critical setting**: Must match your Raspberry Pi model for stability
|
||||||
- **Raspberry Pi Zero/1**: 0-1
|
- **Raspberry Pi 3**: Use 3
|
||||||
- **Raspberry Pi 2/3**: 1-3
|
- **Raspberry Pi 4**: Use 4
|
||||||
- **Raspberry Pi 4**: 2-4 (the config template ships 3)
|
- **Raspberry Pi 5**: Use 5 (or higher if needed)
|
||||||
- **Raspberry Pi 5**: 1–3 in PIO mode (`rp1_rio: 0`, the default). Start at `1` (the library treats `0` as `1` there) and raise it a step at a time if the image flickers or shows garbage — chained panels are the likeliest to need it
|
- **Raspberry Pi Zero/1**: Use 1-2
|
||||||
- Panels on `row_address_type` 5 (SM5368 row drivers) can need 6-8 on a Pi 4
|
- Incorrect values can cause display corruption, flickering, or system instability
|
||||||
- Too low: garbage, flicker or rows jumping. Too high: a lower refresh rate
|
|
||||||
- If you experience issues, try adjusting this value up or down by 1
|
- If you experience issues, try adjusting this value up or down by 1
|
||||||
|
|
||||||
- **`rp1_rio`** (integer, 0 or 1, default: 0) — Raspberry Pi 5 only
|
|
||||||
- Which driver the Pi 5's RP1 chip uses: `0` = PIO (default, less CPU), `1` = RIO (registered I/O, can reach a higher refresh rate)
|
|
||||||
- In RIO mode the effect of `gpio_slowdown` is inverted: higher values may be faster
|
|
||||||
- Ignored on a Pi 0-4, and applied only if the installed rgbmatrix library supports it
|
|
||||||
|
|
||||||
### Display Durations (`display.display_durations`)
|
### Display Durations (`display.display_durations`)
|
||||||
|
|
||||||
Controls how long each installed plugin stays visible in seconds before switching to the next one, keyed by plugin id.
|
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
|
||||||
|
|
||||||
- **Plugin-specific durations**
|
- **Plugin-specific durations**
|
||||||
- Each plugin can have its own duration setting
|
- Each plugin can have its own duration setting
|
||||||
@@ -695,10 +612,9 @@ Controls how long each installed plugin stays visible in seconds before switchin
|
|||||||
### Display Format Settings
|
### Display Format Settings
|
||||||
|
|
||||||
- **`use_short_date_format`** (boolean, default: true)
|
- **`use_short_date_format`** (boolean, default: true)
|
||||||
- Currently has no effect. The web UI still saves it, but no core code
|
- Use short date format (e.g., "Jan 15") instead of long format (e.g., "January 15th")
|
||||||
reads it. Scoreboard plugins that offer a short date format read the
|
- Set to `false` for longer, more readable dates
|
||||||
setting from their own plugin config instead. See
|
- Set to `true` to save space and show more information
|
||||||
[CONFIG_REFERENCE.md](docs/CONFIG_REFERENCE.md#display--other-keys).
|
|
||||||
|
|
||||||
### Dynamic Duration Settings (`display.dynamic_duration`)
|
### Dynamic Duration Settings (`display.dynamic_duration`)
|
||||||
|
|
||||||
@@ -707,7 +623,7 @@ Controls how long each installed plugin stays visible in seconds before switchin
|
|||||||
- Some plugins can automatically adjust their display time based on content
|
- Some plugins can automatically adjust their display time based on content
|
||||||
- This setting limits how long they can extend (prevents one display from dominating)
|
- This setting limits how long they can extend (prevents one display from dominating)
|
||||||
- Example: If set to 60, a plugin can extend up to 60 seconds even if it requests longer
|
- Example: If set to 60, a plugin can extend up to 60 seconds even if it requests longer
|
||||||
- Leave unset to use the default cap (180 seconds; the web UI accepts 30-1800)
|
- Leave unset to use the default cap (typically 90 seconds)
|
||||||
|
|
||||||
### Example Configuration
|
### Example Configuration
|
||||||
|
|
||||||
@@ -754,14 +670,6 @@ Controls how long each installed plugin stays visible in seconds before switchin
|
|||||||
- Verify `hardware_mapping` matches your HAT/connection type
|
- Verify `hardware_mapping` matches your HAT/connection type
|
||||||
- Try adjusting `gpio_slowdown`
|
- Try adjusting `gpio_slowdown`
|
||||||
- Ensure your display doesn't need the E-Addressable line
|
- Ensure your display doesn't need the E-Addressable line
|
||||||
- If it went blank right after a settings change, the Display tab shows a "simulation mode" banner, and `sudo journalctl -u ledmatrix` shows `Failed to initialize RGB Matrix` followed by the reason. When LEDMatrix refused the settings (for example more than 64 `rows`, `parallel` 2 on an `adafruit-hat` mapping, a misspelled `hardware_mapping`, or on a Raspberry Pi 5 a `row_address_type` other than 0 or 2), the message names each one: change them, save, and restart the display service. Otherwise the library itself failed, and its own message just before names the problem
|
|
||||||
- A repeating scramble points at `row_address_type` or `multiplexing`; a panel that stays dark, at `panel_type`
|
|
||||||
|
|
||||||
**Rows jump up and down, or the bottom row repeats other rows:**
|
|
||||||
- Raise `gpio_slowdown` a step at a time (SM5368 panels on `row_address_type` 5 can need 6-8 on a Pi 4)
|
|
||||||
|
|
||||||
**A row or the whole panel briefly flashes brighter:**
|
|
||||||
- Set `disable_hardware_pulsing` to `false` (needs the OE line on GPIO 18; see `hardware_mapping`)
|
|
||||||
|
|
||||||
**Colors are wrong or inverted:**
|
**Colors are wrong or inverted:**
|
||||||
- Check `led_rgb_sequence` (try "GRB" if "RGB" doesn't work)
|
- Check `led_rgb_sequence` (try "GRB" if "RGB" doesn't work)
|
||||||
@@ -786,21 +694,15 @@ Controls how long each installed plugin stays visible in seconds before switchin
|
|||||||
<details>
|
<details>
|
||||||
<summary>Manual SSH Commands (for reference)</summary>
|
<summary>Manual SSH Commands (for reference)</summary>
|
||||||
|
|
||||||
The web interface's quick actions (Start/Stop/Restart Display) call
|
The quick actions essentially just execute the following commands on the Pi.
|
||||||
`sudo systemctl start|stop|restart ledmatrix.service` — see
|
|
||||||
`execute_system_action()` in
|
|
||||||
[`web_interface/blueprints/api_v3/system.py`](web_interface/blueprints/api_v3/system.py).
|
|
||||||
The service runs [`run.py`](run.py) as root.
|
|
||||||
|
|
||||||
To run the display in the foreground instead (for debugging), stop the service
|
From the project root directory (ex: /home/ledpi/LEDMatrix):
|
||||||
first, then from the project root (e.g. `/home/ledpi/LEDMatrix`):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl stop ledmatrix.service
|
sudo python3 display_controller.py
|
||||||
sudo python3 run.py # add -d for debug logging
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This only runs as long as your SSH session stays open.
|
This will start the display cycle but only stays active as long as your ssh session is active.
|
||||||
|
|
||||||
### Convenience Scripts
|
### Convenience Scripts
|
||||||
|
|
||||||
@@ -842,11 +744,9 @@ sudo ./scripts/install/install_service.sh
|
|||||||
|
|
||||||
The script will:
|
The script will:
|
||||||
- Detect your user account and home directory
|
- Detect your user account and home directory
|
||||||
- Install `ledmatrix.service` (display, runs as root), `ledmatrix-web.service`
|
- Install the service file with the correct paths
|
||||||
(web interface, runs as your user) and the `ledmatrix-update-verify` units,
|
- Enable the service to start on boot
|
||||||
with the correct paths
|
- Start the service immediately
|
||||||
- Enable them to start on boot
|
|
||||||
- Start them immediately
|
|
||||||
|
|
||||||
### Managing the Service
|
### Managing the Service
|
||||||
|
|
||||||
@@ -953,7 +853,7 @@ sudo systemctl enable ledmatrix-web.service
|
|||||||
- **On-Demand Controls**: Start specific displays (weather, stocks, sports) on demand
|
- **On-Demand Controls**: Start specific displays (weather, stocks, sports) on demand
|
||||||
- **Service Management**: Start/stop the main display service
|
- **Service Management**: Start/stop the main display service
|
||||||
- **System Controls**: Restart, update code, and manage the system
|
- **System Controls**: Restart, update code, and manage the system
|
||||||
- **System Stats**: CPU, memory and temperature on the Overview tab
|
- **API Metrics**: Monitor API usage and system performance
|
||||||
- **Logs**: View system logs in real-time
|
- **Logs**: View system logs in real-time
|
||||||
|
|
||||||
### Troubleshooting Web Interface
|
### Troubleshooting Web Interface
|
||||||
@@ -970,36 +870,11 @@ sudo systemctl enable ledmatrix-web.service
|
|||||||
3. Check if another service is using port 5000
|
3. Check if another service is using port 5000
|
||||||
|
|
||||||
**Service Fails to Start:**
|
**Service Fails to Start:**
|
||||||
1. Check Python dependencies are installed. The installer puts them in the
|
1. Check Python dependencies are installed
|
||||||
system Python with `pip install --break-system-packages` (there is no
|
2. Verify the virtual environment is set up correctly
|
||||||
virtual environment), so `python3 -c "import flask"` should succeed.
|
3. Check file permissions and ownership
|
||||||
2. Check file permissions and ownership
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
### If you've read this far — thanks!
|
### 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).
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
# 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/RdrC37rEag). 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:
|
|
||||||
|
|
||||||
- **Web UI authentication is optional and off by default.** Out of the
|
|
||||||
box the web interface assumes the network it's running on is trusted.
|
|
||||||
Setting a password under **General > Security** makes every page and
|
|
||||||
API route require a login or an API token (`Authorization: Bearer`),
|
|
||||||
with wrong passwords rate-limited per address
|
|
||||||
(`web_interface/auth.py`). Deliberately left open even then: requests
|
|
||||||
from the Pi itself (loopback without proxy headers; a reverse proxy on
|
|
||||||
the Pi must add `X-Forwarded-For`, or every request it relays counts as
|
|
||||||
local), the Wi-Fi setup flow while the Pi is in access-point mode,
|
|
||||||
static files, and a status-only `/api/v3/health`. The password is a
|
|
||||||
werkzeug hash and tokens are stored as SHA-256, in
|
|
||||||
`config/config_secrets.json`, which no API returns. There is no TLS:
|
|
||||||
over plain HTTP the password and tokens cross the LAN in the clear, so
|
|
||||||
still don't expose port 5000 to the internet; put a TLS reverse proxy
|
|
||||||
or a VPN in front for remote access. Anyone with shell access to the Pi
|
|
||||||
can turn login off (`scripts/reset_web_password.py`), which is the
|
|
||||||
documented recovery path.
|
|
||||||
"Trusted network" does not mean "trusted websites", though: any page
|
|
||||||
a LAN user opens could make their browser POST to the Pi. So the
|
|
||||||
interface refuses a `POST`/`PUT`/`PATCH`/`DELETE` whose `Origin` (or
|
|
||||||
`Referer`) header names another site (`web_interface/origin_guard.py`),
|
|
||||||
and `/api/v3/system/action` only accepts JSON or HTMX requests. Tools
|
|
||||||
that send neither header (curl, Home Assistant, the MQTT bridge) are
|
|
||||||
unaffected. Not covered: DNS rebinding, and anyone who can reach the
|
|
||||||
port directly.
|
|
||||||
- **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.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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: 102 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 657 KiB After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 105 KiB |
@@ -1,29 +0,0 @@
|
|||||||
# 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,47 +1,43 @@
|
|||||||
{
|
{
|
||||||
"web_display_autostart": true,
|
"web_display_autostart": true,
|
||||||
"auto_update": {
|
|
||||||
"enabled": false,
|
|
||||||
"channel": "stable"
|
|
||||||
},
|
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"mode": "per-day",
|
"mode": "per-day",
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00",
|
"end_time": "23:00",
|
||||||
"days": {
|
"days": {
|
||||||
"monday": {
|
"monday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
},
|
},
|
||||||
"tuesday": {
|
"tuesday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
},
|
},
|
||||||
"wednesday": {
|
"wednesday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
},
|
},
|
||||||
"thursday": {
|
"thursday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
},
|
},
|
||||||
"friday": {
|
"friday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
},
|
},
|
||||||
"saturday": {
|
"saturday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
},
|
},
|
||||||
"sunday": {
|
"sunday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "07:00",
|
"start_time": "07:00",
|
||||||
"end_time": "23:00"
|
"end_time": "23:00"
|
||||||
}
|
}
|
||||||
@@ -55,47 +51,46 @@
|
|||||||
"end_time": "07:00",
|
"end_time": "07:00",
|
||||||
"days": {
|
"days": {
|
||||||
"monday": {
|
"monday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
},
|
},
|
||||||
"tuesday": {
|
"tuesday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
},
|
},
|
||||||
"wednesday": {
|
"wednesday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
},
|
},
|
||||||
"thursday": {
|
"thursday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
},
|
},
|
||||||
"friday": {
|
"friday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
},
|
},
|
||||||
"saturday": {
|
"saturday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
},
|
},
|
||||||
"sunday": {
|
"sunday": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"start_time": "20:00",
|
"start_time": "20:00",
|
||||||
"end_time": "07:00"
|
"end_time": "07:00"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timezone": "America/New_York",
|
"timezone": "America/Chicago",
|
||||||
"target_fps": 100,
|
|
||||||
"location": {
|
"location": {
|
||||||
"city": "Tampa",
|
"city": "Dallas",
|
||||||
"state": "Florida",
|
"state": "Texas",
|
||||||
"country": "US"
|
"country": "US"
|
||||||
},
|
},
|
||||||
"display": {
|
"display": {
|
||||||
@@ -114,75 +109,27 @@
|
|||||||
"inverse_colors": false,
|
"inverse_colors": false,
|
||||||
"show_refresh_rate": false,
|
"show_refresh_rate": false,
|
||||||
"led_rgb_sequence": "RGB",
|
"led_rgb_sequence": "RGB",
|
||||||
"limit_refresh_rate_hz": 100,
|
"limit_refresh_rate_hz": 100
|
||||||
"pixel_mapper_config": "",
|
|
||||||
"orientation": "normal",
|
|
||||||
"row_address_type": 0,
|
|
||||||
"multiplexing": 0,
|
|
||||||
"panel_type": ""
|
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"gpio_slowdown": 3,
|
"gpio_slowdown": 3
|
||||||
"rp1_rio": 0
|
|
||||||
},
|
|
||||||
"double_sided": {
|
|
||||||
"enabled": false,
|
|
||||||
"copies": 2,
|
|
||||||
"axis": "horizontal"
|
|
||||||
},
|
},
|
||||||
"display_durations": {},
|
"display_durations": {},
|
||||||
"plugin_rotation_order": [],
|
|
||||||
"use_short_date_format": true,
|
"use_short_date_format": true,
|
||||||
"vegas_scroll": {
|
"vegas_scroll": {
|
||||||
"live_in_ticker": true,
|
|
||||||
"live_weight": 3,
|
|
||||||
"favorite_live_weight": 5,
|
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"scroll_speed": 50,
|
"scroll_speed": 50,
|
||||||
"separator_width": 32,
|
"separator_width": 32,
|
||||||
"plugin_order": [],
|
"plugin_order": [],
|
||||||
"excluded_plugins": [],
|
"excluded_plugins": [],
|
||||||
"target_fps": 125,
|
"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": 0.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": {
|
"plugin_system": {
|
||||||
"plugins_directory": "plugin-repos"
|
"plugins_directory": "plugin-repos",
|
||||||
},
|
"auto_discover": true,
|
||||||
"fetch_service": {
|
"auto_load_enabled": true
|
||||||
"enabled": true,
|
|
||||||
"max_wait_seconds": 2,
|
|
||||||
"rate_limits": {
|
|
||||||
"*.espn.com": {
|
|
||||||
"per_second": 20,
|
|
||||||
"burst": 200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"web-ui-info": {
|
"web-ui-info": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"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": {
|
"github": {
|
||||||
"api_token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"
|
"api_token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"dev_plugins_dir": "~/.ledmatrix-dev-plugins",
|
|
||||||
"github_user": "ChuckBuilds",
|
|
||||||
"plugins_repo": "ledmatrix-plugins",
|
|
||||||
"plugins_branch": "main"
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Legacy entry point: runs ``run.py``, which is the one to use.
|
|
||||||
|
|
||||||
``python3 run.py`` (``-e`` for the emulator, ``-d`` for debug logging) is how
|
|
||||||
the display service and the docs start LEDMatrix. This file used to import
|
|
||||||
``src.display_controller.main`` directly, which skipped what run.py sets up
|
|
||||||
first -- ``sys.dont_write_bytecode`` (root-owned ``__pycache__`` in plugin
|
|
||||||
directories blocks the web service from updating them), the ``-e``/``-d``
|
|
||||||
flags, and the logging configuration. It now runs run.py exactly as
|
|
||||||
``python3 run.py`` would, with the same arguments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import runpy
|
import sys
|
||||||
|
|
||||||
|
# Add the project root directory to Python path
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from src.display_controller import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
runpy.run_path(
|
main()
|
||||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "run.py"),
|
|
||||||
run_name="__main__",
|
|
||||||
)
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
# 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 <plugin-id> --sizes 64x32,128x32,96x48,128x64,256x64
|
|
||||||
python scripts/render_plugin.py --plugin <plugin-id> --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"`.
|
|
||||||
@@ -10,27 +10,22 @@ This guide covers advanced LEDMatrix features for users and developers, includin
|
|||||||
|
|
||||||
Vegas scroll mode displays content from multiple plugins in a continuous horizontal scroll, similar to news tickers seen in Las Vegas casinos. Plugins contribute content segments that flow across the display in a seamless ticker-style presentation.
|
Vegas scroll mode displays content from multiple plugins in a continuous horizontal scroll, similar to news tickers seen in Las Vegas casinos. Plugins contribute content segments that flow across the display in a seamless ticker-style presentation.
|
||||||
|
|
||||||
### How a Plugin Takes Part
|
### Display Modes
|
||||||
|
|
||||||
Each plugin has a *Vegas participation*:
|
**SCROLL (Continuous Scrolling):**
|
||||||
|
- Content scrolls continuously left
|
||||||
|
- Smooth, fluid motion
|
||||||
|
- Best for news-ticker style displays
|
||||||
|
|
||||||
**`scroll` (the default):**
|
**FIXED_SEGMENT (Fixed-Width Block):**
|
||||||
- The plugin's content scrolls by with everyone else's
|
- Plugin gets fixed-width block on display
|
||||||
- Best for news-ticker style content: scores, headlines, prices, the time
|
- Content doesn't scroll out of its segment
|
||||||
|
- Multiple plugins can share the display simultaneously
|
||||||
|
|
||||||
**`pause`:**
|
**STATIC (Scroll Pauses):**
|
||||||
- The scroll stops when the plugin's turn comes round
|
- Scrolling pauses when content is fully visible
|
||||||
- The plugin draws the whole panel for its display duration, then the
|
- Displays for specified duration, then resumes scrolling
|
||||||
scroll resumes
|
- Best for content that needs to be fully read
|
||||||
- Best for content that needs to be read in full, or alerts
|
|
||||||
|
|
||||||
**`exclude`:**
|
|
||||||
- The plugin is left out of Vegas mode
|
|
||||||
|
|
||||||
A plugin declares its default; set `vegas_participation` in a plugin's
|
|
||||||
config to override it (see [Per-Plugin Configuration](#per-plugin-configuration)).
|
|
||||||
Older documentation also describes a *fixed segment* mode; Vegas never
|
|
||||||
implemented one, and it has always behaved exactly like `scroll`.
|
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -52,11 +47,6 @@ 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:**
|
**Configuration Options:**
|
||||||
|
|
||||||
| Setting | Default | Description |
|
| Setting | Default | Description |
|
||||||
@@ -67,113 +57,7 @@ JSON is optional.
|
|||||||
| `plugin_order` | `[]` | Plugin display order (empty = auto) |
|
| `plugin_order` | `[]` | Plugin display order (empty = auto) |
|
||||||
| `excluded_plugins` | `[]` | Plugins to exclude from Vegas mode |
|
| `excluded_plugins` | `[]` | Plugins to exclude from Vegas mode |
|
||||||
| `target_fps` | `125` | Target frame rate |
|
| `target_fps` | `125` | Target frame rate |
|
||||||
| `buffer_ahead` | `2` | Number of plugins buffered ahead |
|
| `buffer_ahead` | `2` | Number of panels to render ahead |
|
||||||
|
|
||||||
This table is a subset — `display.vegas_scroll` supports 30 keys in
|
|
||||||
total. See the full list in
|
|
||||||
[CONFIG_REFERENCE.md](CONFIG_REFERENCE.md#displayvegas_scroll--continuous-scroll-mode).
|
|
||||||
|
|
||||||
### Live Content in the Ticker
|
|
||||||
|
|
||||||
By default (since 3.8.0) live content **stays in the ticker** and takes
|
|
||||||
**extra turns inside it**, and a scoreboard that supports live cards updates
|
|
||||||
the score on a card already crossing the screen (`live_refresh`, "Update live
|
|
||||||
content while it scrolls").
|
|
||||||
|
|
||||||
To get the old behaviour back -- live content **preempts** Vegas mode: while
|
|
||||||
any plugin reports live priority the ticker stops and that plugin's
|
|
||||||
full-screen display is shown instead -- untick **Keep live games in the
|
|
||||||
ticker** under Vegas mode, or set `live_in_ticker` to `false`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"vegas_scroll": {
|
|
||||||
"live_in_ticker": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Until 3.8.0 `false` was the default and every config held it, copied from
|
|
||||||
the template. The first start on 3.8.0 turns it on once (a backup of the
|
|
||||||
config is kept as `config.json.backup`, and `live_in_ticker_migrated` records
|
|
||||||
that it ran); a `false` set after that is left alone.
|
|
||||||
|
|
||||||
The weights below apply while live content is in the ticker:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"vegas_scroll": {
|
|
||||||
"live_weight": 3,
|
|
||||||
"favorite_live_weight": 5
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Why weights exist
|
|
||||||
|
|
||||||
The rotation is otherwise a strict round robin — every plugin appears exactly
|
|
||||||
once per cycle. With a dozen plugins enabled, a live score comes round once a
|
|
||||||
lap and can be minutes old by the time you see it. A weight of *N* gives a
|
|
||||||
plugin *N* slots per cycle.
|
|
||||||
|
|
||||||
The slots are placed by **Smooth Weighted Round-Robin**, the same scheduler
|
|
||||||
the sports plugins use internally to rotate their own games. The important
|
|
||||||
property is that repeats are *spread through the cycle* rather than clumped:
|
|
||||||
three appearances in a row followed by a long silence would be worse than not
|
|
||||||
boosting at all.
|
|
||||||
|
|
||||||
Twelve plugins, with a favorite's baseball game and an ordinary live hockey
|
|
||||||
game (`live_weight: 3`, `favorite_live_weight: 5`):
|
|
||||||
|
|
||||||
```
|
|
||||||
baseball > hockey > weather > clock > baseball
|
|
||||||
stocks > news > flights > baseball > hockey
|
|
||||||
calendar > f1 > music > baseball > tides
|
|
||||||
birds > hockey > baseball
|
|
||||||
```
|
|
||||||
|
|
||||||
18 slots for 12 plugins. Baseball appears 5 times, hockey 3, everything else
|
|
||||||
once, and no plugin ever appears twice in a row — **including across the seam**
|
|
||||||
where the cycle loops back on itself. Smooth Weighted Round-Robin schedules the
|
|
||||||
heaviest item first and usually last as well, so the strip would otherwise show
|
|
||||||
it twice running at exactly the one join a within-cycle check cannot see. The
|
|
||||||
trailing repeat is moved into the widest remaining gap. Where a double is
|
|
||||||
unavoidable — a plugin holding most of the slots has to neighbour itself — the
|
|
||||||
schedule is left as it is.
|
|
||||||
|
|
||||||
#### Where the weight comes from
|
|
||||||
|
|
||||||
For each plugin in the rotation, in order:
|
|
||||||
|
|
||||||
1. **The plugin's own answer.** If it implements
|
|
||||||
`get_vegas_priority_weight()` and returns a number, that wins. This is the
|
|
||||||
only route for favorite-team awareness — the core can see *that* a game is
|
|
||||||
live, but not *whose*, so a scoreboard has to say so itself.
|
|
||||||
2. **The core's default.** When the plugin returns `None` (the base-class
|
|
||||||
default), a plugin where both `has_live_priority()` and `has_live_content()`
|
|
||||||
are true gets `live_weight`.
|
|
||||||
3. **Everything else** gets 1.
|
|
||||||
|
|
||||||
Because of step 2, **existing plugins need no changes** — any scoreboard with
|
|
||||||
`live_priority` enabled already gets extra turns. Step 1 is opt-in, for
|
|
||||||
plugins that want to distinguish a favorite's game from any other live game.
|
|
||||||
|
|
||||||
Weights are clamped to 1–10. A weight of 1 is no boost; a weight below 1 would
|
|
||||||
drop the plugin from the rotation entirely, which is never what is meant.
|
|
||||||
|
|
||||||
#### Things worth knowing
|
|
||||||
|
|
||||||
- **Weights are per plugin, not per game.** A scoreboard showing four live
|
|
||||||
games still occupies one slot at a time, rotating its own games within that
|
|
||||||
slot using its own `favorite_live_boost`. This controls how often the
|
|
||||||
*plugin* comes round.
|
|
||||||
- **The ticker is zero-sum.** Giving baseball 5 slots does not make the cycle
|
|
||||||
faster; it makes the cycle *longer* and everything else proportionally
|
|
||||||
rarer. If you want live scores sooner in wall-clock terms, pair this with a
|
|
||||||
smaller `plugins_per_cycle`.
|
|
||||||
- **Frequency is not freshness.** Each appearance redraws from the plugin's
|
|
||||||
current data (`refresh_updated_plugins()` drops cached content when a
|
|
||||||
plugin's data changes), but how current that data is depends on the
|
|
||||||
plugin's own `live_update_interval`. Showing a stale score five times a lap
|
|
||||||
is no better than showing it once.
|
|
||||||
- **Everything still appears.** A boost never starves another plugin out of
|
|
||||||
the cycle; low-weight plugins keep their single slot.
|
|
||||||
|
|
||||||
### Per-Plugin Configuration
|
### Per-Plugin Configuration
|
||||||
|
|
||||||
@@ -183,7 +67,8 @@ Override Vegas behavior for specific plugins:
|
|||||||
{
|
{
|
||||||
"my_plugin": {
|
"my_plugin": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vegas_participation": "pause",
|
"vegas_mode": "scroll",
|
||||||
|
"vegas_panel_count": 2,
|
||||||
"display_duration": 10
|
"display_duration": 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,80 +78,77 @@ Override Vegas behavior for specific plugins:
|
|||||||
|
|
||||||
| Setting | Values | Description |
|
| Setting | Values | Description |
|
||||||
|---------|--------|-------------|
|
|---------|--------|-------------|
|
||||||
| `vegas_participation` | `scroll`, `pause`, `exclude` | How this plugin takes part: its content scrolls by, the scroll pauses for its turn and shows it full screen, or it is left out. Unset uses the plugin's own default |
|
| `vegas_mode` | `scroll`, `fixed`, `static` | Display mode for this plugin |
|
||||||
| `display_duration` | seconds | How long a `pause` plugin holds the screen |
|
| `vegas_panel_count` | `1-10` | Width in panels (1 panel = display width) |
|
||||||
| `vegas_width_pct` | 10–100 | Width of this plugin's card, as a percentage of the panel |
|
| `display_duration` | seconds | Pause duration for STATIC mode |
|
||||||
| `vegas_overflow` | `rotate`, `truncate` | What to do when its content is wider than its allowance |
|
|
||||||
| `vegas_max_width_screens` | number of screens | The widest its card may be |
|
|
||||||
|
|
||||||
These are core-owned settings (see
|
|
||||||
[PLUGIN_CONFIG_CORE_PROPERTIES.md](PLUGIN_CONFIG_CORE_PROPERTIES.md)): every
|
|
||||||
plugin accepts them whether or not its own schema lists them. Set them in
|
|
||||||
the plugin's section of config.json, in the web UI's **Config Editor**
|
|
||||||
tab.
|
|
||||||
|
|
||||||
Some plugins also offer a `vegas_mode` setting of their own (`scroll`,
|
|
||||||
`fixed` or `static`). It still works — `static` pauses, the other two scroll
|
|
||||||
— but `vegas_participation` takes precedence, and `fixed` has never done
|
|
||||||
anything different from `scroll`. The old `vegas_panel_count` setting never
|
|
||||||
had an effect and is deprecated (removed in 3.9.0).
|
|
||||||
|
|
||||||
### Plugin Integration (Developer Guide)
|
### Plugin Integration (Developer Guide)
|
||||||
|
|
||||||
All of these have defaults in
|
|
||||||
[`BasePlugin`](../src/plugin_system/base_plugin.py); override only what you
|
|
||||||
need. The reference is
|
|
||||||
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#vegas-scroll-hooks).
|
|
||||||
|
|
||||||
**1. Implement Content Method:**
|
**1. Implement Content Method:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def get_vegas_content(self):
|
def get_vegas_content(self):
|
||||||
# Return a PIL Image, a list of Images, or None.
|
"""
|
||||||
# A single image is one block; a list becomes one item per image.
|
Return PIL Image or list of Images for Vegas mode.
|
||||||
return [self._render_game(game) for game in self.games]
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image or list[PIL.Image]: Content to display
|
||||||
|
- Single image: fixed-width content
|
||||||
|
- List of images: multiple segments
|
||||||
|
- None: skip this cycle
|
||||||
|
"""
|
||||||
|
# Example: Return single wide image
|
||||||
|
img = Image.new('RGB', (256, 32))
|
||||||
|
# ... render your content ...
|
||||||
|
return img
|
||||||
|
|
||||||
|
# Example: Return multiple segments
|
||||||
|
return [image1, image2, image3]
|
||||||
```
|
```
|
||||||
|
|
||||||
If it returns `None` (the default), Vegas falls back to the plugin's
|
**2. Specify Content Type:**
|
||||||
`scroll_helper` image, then to capturing `display()` output
|
|
||||||
(`PluginAdapter.get_content()` in
|
|
||||||
[`src/vegas_mode/plugin_adapter.py`](../src/vegas_mode/plugin_adapter.py)).
|
|
||||||
|
|
||||||
**2. Declare how the plugin takes part:**
|
|
||||||
|
|
||||||
Most plugins need nothing: the default is `scroll`. A plugin that should
|
|
||||||
pause the scroll, or stay out of Vegas, says so in `manifest.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"vegas_participation": "pause"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The user's own `vegas_participation` setting overrides the manifest. When
|
|
||||||
the answer depends on state, override the method instead:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def get_vegas_participation(self):
|
def get_vegas_content_type(self):
|
||||||
# 'scroll' | 'pause' | 'exclude'
|
"""
|
||||||
return 'pause' if self._alert_is_live() else 'scroll'
|
Specify how content should be handled.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 'multi' | 'static' | 'none'
|
||||||
|
"""
|
||||||
|
return 'multi' # Default for most plugins
|
||||||
```
|
```
|
||||||
|
|
||||||
A plugin written for an older core that declares nothing keeps its
|
**3. Optionally Specify Display Mode:**
|
||||||
behaviour: `get_vegas_display_mode()` returning `VegasDisplayMode.STATIC`
|
|
||||||
pauses, `get_vegas_content_type()` returning `'none'` excludes, and
|
```python
|
||||||
everything else scrolls. `get_supported_vegas_modes()`,
|
def get_vegas_display_mode(self):
|
||||||
`get_vegas_segment_width()` and the SCROLL / FIXED_SEGMENT distinction are
|
"""
|
||||||
deprecated (removed in 3.9.0): Vegas never read them.
|
Preferred display mode for this plugin.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 'scroll' | 'fixed' | 'static'
|
||||||
|
"""
|
||||||
|
return 'scroll'
|
||||||
|
|
||||||
|
def get_supported_vegas_modes(self):
|
||||||
|
"""
|
||||||
|
List of supported modes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: ['scroll', 'fixed', 'static']
|
||||||
|
"""
|
||||||
|
return ['scroll', 'static']
|
||||||
|
```
|
||||||
|
|
||||||
### Content Rendering Guidelines
|
### Content Rendering Guidelines
|
||||||
|
|
||||||
**Image Dimensions:**
|
**Image Dimensions:**
|
||||||
- **Height:** Must match display height (typically 32 pixels)
|
- **Height:** Must match display height (typically 32 pixels)
|
||||||
- **Width:** Any width for `scroll` (recommended 64-512 pixels);
|
- **Width:** Varies by mode:
|
||||||
`get_vegas_render_width()` is the width Vegas would like, and it narrows
|
- SCROLL: Any width (recommended 64-512 pixels)
|
||||||
`display_manager` to match while it asks. A `pause` plugin draws the
|
- FIXED_SEGMENT: `panel_count * display_width`
|
||||||
whole panel in `display()`.
|
- STATIC: Any width, optimized for readability
|
||||||
|
|
||||||
**Color Mode:**
|
**Color Mode:**
|
||||||
- Use RGB color mode
|
- Use RGB color mode
|
||||||
@@ -316,9 +198,16 @@ class WeatherPlugin(BasePlugin):
|
|||||||
def get_vegas_content(self):
|
def get_vegas_content(self):
|
||||||
"""Return cached Vegas image"""
|
"""Return cached Vegas image"""
|
||||||
return self.vegas_image
|
return self.vegas_image
|
||||||
```
|
|
||||||
|
|
||||||
It scrolls, the default participation, so it declares nothing else.
|
def get_vegas_content_type(self):
|
||||||
|
return 'multi'
|
||||||
|
|
||||||
|
def get_vegas_display_mode(self):
|
||||||
|
return 'scroll'
|
||||||
|
|
||||||
|
def get_supported_vegas_modes(self):
|
||||||
|
return ['scroll', 'static']
|
||||||
|
```
|
||||||
|
|
||||||
### System Architecture
|
### System Architecture
|
||||||
|
|
||||||
@@ -387,23 +276,15 @@ Vegas mode consists of four core components working together to provide smooth 1
|
|||||||
5. Compose into continuous stream with separators
|
5. Compose into continuous stream with separators
|
||||||
|
|
||||||
**Key Methods:**
|
**Key Methods:**
|
||||||
- `get_next_segment()` - Returns the next buffered `ContentSegment` (or `None`)
|
- `get_stream_content()` - Returns current stream content as PIL Image
|
||||||
- `take_next_group(count=None, offscreen_only=False)` - Hands over the next
|
- `advance_stream(pixels)` - Advances stream by N pixels
|
||||||
slice of the rotation as `(plugin_id, images)` groups
|
- `refresh_stream()` - Regenerates stream from current plugins
|
||||||
- `get_grouped_content_for_composition()` - Buffered images grouped by plugin
|
|
||||||
- `mark_plugin_updated(plugin_id)` / `process_updates()` - Refresh one
|
|
||||||
plugin's segment in place when its data changes
|
|
||||||
- `refresh()` - Re-read the plugin list and config
|
|
||||||
- `advance_cycle()` - Clear the active buffer when a scroll cycle completes
|
|
||||||
|
|
||||||
(`src/vegas_mode/stream_manager.py`)
|
|
||||||
|
|
||||||
#### 3. PluginAdapter
|
#### 3. PluginAdapter
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
- Convert plugin content to scrollable images
|
- Convert plugin content to scrollable images
|
||||||
- Fetch the content of `scroll` plugins (a `pause` plugin is drawn by
|
- Handle different Vegas display modes (SCROLL, FIXED, STATIC)
|
||||||
its own `display()` when the scroll pauses; see StreamManager)
|
|
||||||
- Manage fallback for plugins without Vegas support
|
- Manage fallback for plugins without Vegas support
|
||||||
- Cache plugin content for performance
|
- Cache plugin content for performance
|
||||||
|
|
||||||
@@ -412,16 +293,21 @@ Vegas mode consists of four core components working together to provide smooth 1
|
|||||||
- Calls `get_vegas_content()` if available
|
- Calls `get_vegas_content()` if available
|
||||||
- Falls back to `display()` method if not
|
- Falls back to `display()` method if not
|
||||||
|
|
||||||
2. **Participation** is decided by the StreamManager, not here
|
2. **Handle display mode:**
|
||||||
(`resolve_vegas_participation()` in
|
- SCROLL: Returns image as-is for continuous scrolling
|
||||||
[`base_plugin.py`](../src/plugin_system/base_plugin.py)): `exclude`
|
- FIXED_SEGMENT: Creates fixed-width block (panel_count * display_width)
|
||||||
plugins never reach the adapter, and `pause` plugins are not fetched.
|
- STATIC: Marks content for pause-when-visible behavior
|
||||||
|
|
||||||
|
3. **Content type handling:**
|
||||||
|
- `multi`: Multiple segments (list of images)
|
||||||
|
- `static`: Single static image
|
||||||
|
- `none`: Skip this plugin in current cycle
|
||||||
|
|
||||||
**Fallback Behavior:**
|
**Fallback Behavior:**
|
||||||
- If plugin doesn't implement Vegas methods:
|
- If plugin doesn't implement Vegas methods:
|
||||||
- Calls plugin's `display()` method
|
- Calls plugin's `display()` method
|
||||||
- Captures rendered display as static image
|
- Captures rendered display as static image
|
||||||
- Scrolls it by as one block
|
- Treats as fixed segment
|
||||||
- Ensures all plugins work in Vegas mode without explicit support
|
- Ensures all plugins work in Vegas mode without explicit support
|
||||||
|
|
||||||
#### 4. RenderPipeline
|
#### 4. RenderPipeline
|
||||||
@@ -446,14 +332,10 @@ Vegas mode consists of four core components working together to provide smooth 1
|
|||||||
- **Frame Rate Control:** Precise timing to maintain 125 FPS
|
- **Frame Rate Control:** Precise timing to maintain 125 FPS
|
||||||
- **Pre-rendered Content:** Plugins pre-render during update()
|
- **Pre-rendered Content:** Plugins pre-render during update()
|
||||||
|
|
||||||
**Scroll Speed Calculation:** motion is by elapsed time; `target_fps` paces
|
**Scroll Speed Calculation:**
|
||||||
the render loop, not the speed.
|
|
||||||
```python
|
```python
|
||||||
# frame_based_scrolling: false
|
pixels_per_frame = (scroll_speed / target_fps)
|
||||||
scroll_position += scroll_speed * elapsed_time # scroll_speed in px/s
|
scroll_position += pixels_per_frame * elapsed_time
|
||||||
# frame_based_scrolling: true (the default) -- not stepping, just a clamp
|
|
||||||
applied = clamp(scroll_speed * scroll_delay, 0.1, 5) / scroll_delay
|
|
||||||
scroll_position += applied * elapsed_time
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Component Interactions
|
#### Component Interactions
|
||||||
@@ -524,7 +406,7 @@ All components use thread-safe patterns:
|
|||||||
If a plugin doesn't implement Vegas methods:
|
If a plugin doesn't implement Vegas methods:
|
||||||
- System calls the plugin's `display()` method
|
- System calls the plugin's `display()` method
|
||||||
- Captures the rendered display as a static image
|
- Captures the rendered display as a static image
|
||||||
- Scrolls it by as one block
|
- Treats it as a fixed segment
|
||||||
|
|
||||||
This ensures all plugins work in Vegas mode, even without explicit support.
|
This ensures all plugins work in Vegas mode, even without explicit support.
|
||||||
|
|
||||||
@@ -569,8 +451,7 @@ time when something is active.
|
|||||||
|
|
||||||
### REST API Reference
|
### REST API Reference
|
||||||
|
|
||||||
The API is mounted at `/api/v3` (the `api_v3` blueprint, registered in
|
The API is mounted at `/api/v3` (`web_interface/app.py:144`).
|
||||||
`web_interface/app.py`). Full details: [REST_API_REFERENCE.md](REST_API_REFERENCE.md#display-control).
|
|
||||||
|
|
||||||
#### Start On-Demand Display
|
#### Start On-Demand Display
|
||||||
|
|
||||||
@@ -626,38 +507,19 @@ curl http://localhost:5000/api/v3/display/on-demand/status
|
|||||||
|
|
||||||
# Response:
|
# Response:
|
||||||
{
|
{
|
||||||
"status": "success",
|
"active": true,
|
||||||
"data": {
|
"plugin_id": "weather",
|
||||||
"state": {
|
"mode": "weather",
|
||||||
"active": true,
|
"remaining": 25.5,
|
||||||
"plugin_id": "weather",
|
"pinned": false,
|
||||||
"mode": "weather",
|
"status": "active"
|
||||||
"duration": 30,
|
|
||||||
"pinned": false,
|
|
||||||
"status": "running",
|
|
||||||
"last_updated": 1234567890.1
|
|
||||||
},
|
|
||||||
"service": {"active": true, "returncode": 0, "stdout": "active", "stderr": ""}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When nothing is running on demand, `data.state` is
|
|
||||||
`{"active": false, "status": "idle", "last_updated": null}`.
|
|
||||||
|
|
||||||
> There is no public Python on-demand API. The display controller's
|
> There is no public Python on-demand API. The display controller's
|
||||||
> on-demand machinery is internal — drive it through the REST endpoints
|
> on-demand machinery is internal — drive it through the REST endpoints
|
||||||
> above (or the web UI buttons). The API handlers
|
> above (or the web UI buttons), which write a request into the cache
|
||||||
> (`start_on_demand_display()` / `stop_on_demand_display()` in
|
> manager (`display_on_demand_config` key) that the controller polls.
|
||||||
> `web_interface/blueprints/api_v3/display.py`) send the request over the
|
|
||||||
> display's control socket ([IPC_CONTROL_SOCKET.md](IPC_CONTROL_SOCKET.md)).
|
|
||||||
> Only when the socket cannot carry it do they write it 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
|
### Duration Modes
|
||||||
|
|
||||||
@@ -749,14 +611,8 @@ keys helps troubleshoot stuck states.
|
|||||||
"timestamp": 1234567890.123
|
"timestamp": 1234567890.123
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
**Purpose:** Communication from web interface to display controller, as the
|
**Purpose:** Communication from web interface to display controller
|
||||||
fallback when the control socket cannot carry the request (deprecated; it
|
**When Set:** API endpoint receives request
|
||||||
will be removed in a later release)
|
|
||||||
**When Set:** API endpoint receives a request and the display's control
|
|
||||||
socket is unavailable (display stopped, or older than the socket or the
|
|
||||||
command); some plugins also write it directly
|
|
||||||
**Read:** once a second while the display serves the control socket (0.25 s
|
|
||||||
without it), and only when the file changed since the last look
|
|
||||||
**Auto-Cleared:** After processing or 1 hour TTL
|
**Auto-Cleared:** After processing or 1 hour TTL
|
||||||
|
|
||||||
**2. display_on_demand_config** (No TTL)
|
**2. display_on_demand_config** (No TTL)
|
||||||
@@ -785,13 +641,13 @@ without it), and only when the file changed since the last look
|
|||||||
**When Set:** Every display loop iteration
|
**When Set:** Every display loop iteration
|
||||||
**Auto-Cleared:** Never (continuously updated)
|
**Auto-Cleared:** Never (continuously updated)
|
||||||
|
|
||||||
**4. display_on_demand_processed_id** (TTL: 1 hour)
|
**4. display_on_demand_processed_id** (TTL: 5 minutes)
|
||||||
```text
|
```
|
||||||
"uuid-string-of-last-processed-request"
|
"uuid-string-of-last-processed-request"
|
||||||
```
|
```
|
||||||
**Purpose:** Prevents duplicate request processing
|
**Purpose:** Prevents duplicate request processing
|
||||||
**When Set:** After processing request
|
**When Set:** After processing request
|
||||||
**Auto-Cleared:** After 1 hour TTL
|
**Auto-Cleared:** After 5 minutes TTL
|
||||||
|
|
||||||
### When Manual Clearing is Needed
|
### When Manual Clearing is Needed
|
||||||
|
|
||||||
@@ -824,9 +680,9 @@ without it), and only when the file changed since the last look
|
|||||||
The cache is stored as JSON files under one of:
|
The cache is stored as JSON files under one of:
|
||||||
|
|
||||||
- `/var/cache/ledmatrix/` (preferred when the service has permission)
|
- `/var/cache/ledmatrix/` (preferred when the service has permission)
|
||||||
- `~/.ledmatrix_cache/`
|
- `~/.cache/ledmatrix/`
|
||||||
- `/opt/ledmatrix/cache/`
|
- `/opt/ledmatrix/cache/`
|
||||||
- `$TMPDIR/ledmatrix_cache/` (fallback)
|
- `/tmp/ledmatrix-cache/` (fallback)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Find the cache dir actually in use
|
# Find the cache dir actually in use
|
||||||
@@ -850,9 +706,8 @@ cache.clear_cache('display_on_demand_request')
|
|||||||
cache.clear_cache('display_on_demand_processed_id')
|
cache.clear_cache('display_on_demand_processed_id')
|
||||||
```
|
```
|
||||||
|
|
||||||
> `CacheManager` also has a `delete(key)` method — a thin wrapper over
|
> The actual public method is `clear_cache(key=None)` — there is no
|
||||||
> `clear_cache(key)` — so `cache.delete('display_on_demand_config')`
|
> `delete()` method on `CacheManager`.
|
||||||
> works equally well.
|
|
||||||
|
|
||||||
### Cache Impact on Running Service
|
### Cache Impact on Running Service
|
||||||
|
|
||||||
@@ -870,7 +725,7 @@ The display controller automatically handles cleanup:
|
|||||||
- **Config key**: Cleared when on-demand stops
|
- **Config key**: Cleared when on-demand stops
|
||||||
- **State key**: Updated every display loop iteration
|
- **State key**: Updated every display loop iteration
|
||||||
- **Request key**: Expires after 1 hour TTL (or after processing)
|
- **Request key**: Expires after 1 hour TTL (or after processing)
|
||||||
- **Processed ID**: Expires after 1 hour TTL
|
- **Processed ID**: Expires after 5 minutes TTL
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -900,13 +755,7 @@ Cache Check → Background Fetch → Partial Data → Completion → Cache
|
|||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
Core does not read a `background_service` config block: the service itself
|
Enable background service per plugin in `config/config.json`:
|
||||||
(`src/background_data_service.py`) is a process-wide singleton, and its
|
|
||||||
worker count is whatever the first caller of `get_background_service()`
|
|
||||||
passes. The sports scoreboard plugins read their own
|
|
||||||
`background_service` settings and pass them to it, so the exact keys and
|
|
||||||
where they sit (top level or per league) are defined by each plugin's
|
|
||||||
`config_schema.json`. A typical block looks like:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -927,11 +776,11 @@ where they sit (top level or per league) are defined by each plugin's
|
|||||||
|
|
||||||
| Setting | Default | Description |
|
| Setting | Default | Description |
|
||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| `enabled` | plugin-defined | Use the background service for this plugin's fetches |
|
| `enabled` | `false` | Enable background service for this plugin |
|
||||||
| `max_workers` | `3` | Max concurrent background tasks |
|
| `max_workers` | `3` | Max concurrent background tasks |
|
||||||
| `request_timeout` | `30` | Timeout per API request (seconds) |
|
| `request_timeout` | `30` | Timeout per API request (seconds) |
|
||||||
| `max_retries` | `3` | Retry attempts on failure |
|
| `max_retries` | `3` | Retry attempts on failure |
|
||||||
| `priority` | `1` | Stored on each request (higher number = higher priority, per `FetchRequest`), but the service runs requests in submission order; it does not reorder by priority |
|
| `priority` | `1` | Task priority (1=highest, 10=lowest) |
|
||||||
|
|
||||||
### Performance Impact
|
### Performance Impact
|
||||||
|
|
||||||
@@ -946,11 +795,12 @@ where they sit (top level or per league) are defined by each plugin's
|
|||||||
|
|
||||||
### Plugins using the background service
|
### Plugins using the background service
|
||||||
|
|
||||||
The background data service is used by all of the sports scoreboard
|
The background data service is now used by all of the sports scoreboard
|
||||||
plugins (football, hockey, baseball/MLB, basketball, soccer, lacrosse,
|
plugins (football, hockey, baseball, basketball, soccer, lacrosse, F1,
|
||||||
F1, UFC), the odds ticker, and the leaderboard plugin. Each plugin reads
|
UFC), the odds ticker, and the leaderboard plugin. Each plugin's
|
||||||
its own `background_service` block (under its own config namespace); check
|
`background_service` block (under its own config namespace) follows the
|
||||||
that plugin's `config_schema.json` for the keys it accepts.
|
same shape as the example above.
|
||||||
|
- ⏳ MLB (baseball)
|
||||||
|
|
||||||
### Error Handling & Fallback
|
### Error Handling & Fallback
|
||||||
|
|
||||||
@@ -967,6 +817,9 @@ that plugin's `config_schema.json` for the keys it accepts.
|
|||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Run background service test
|
||||||
|
python test_background_service.py
|
||||||
|
|
||||||
# Check logs for background operations
|
# Check logs for background operations
|
||||||
sudo journalctl -u ledmatrix -f | grep "background"
|
sudo journalctl -u ledmatrix -f | grep "background"
|
||||||
```
|
```
|
||||||
@@ -975,21 +828,15 @@ sudo journalctl -u ledmatrix -f | grep "background"
|
|||||||
|
|
||||||
**View Statistics:**
|
**View Statistics:**
|
||||||
```python
|
```python
|
||||||
from src.background_data_service import get_background_service
|
from src.background_data_service import BackgroundDataService
|
||||||
from src.cache_manager import CacheManager
|
|
||||||
|
|
||||||
service = get_background_service(CacheManager())
|
service = BackgroundDataService()
|
||||||
stats = service.get_statistics()
|
stats = service.get_statistics()
|
||||||
print(f"Active: {stats['active_requests']}")
|
print(f"Active tasks: {stats['active_tasks']}")
|
||||||
print(f"Completed: {stats['completed_requests']}")
|
print(f"Completed: {stats['completed']}")
|
||||||
print(f"Failed: {stats['failed_requests']}")
|
print(f"Failed: {stats['failed']}")
|
||||||
```
|
```
|
||||||
|
|
||||||
Other keys: `total_requests`, `cached_hits`, `cache_misses`,
|
|
||||||
`average_fetch_time`, `completed_requests_count` (results currently held in
|
|
||||||
memory) — see `BackgroundDataService.get_statistics()` in
|
|
||||||
[`src/background_data_service.py`](../src/background_data_service.py).
|
|
||||||
|
|
||||||
**Enable Debug Logging:**
|
**Enable Debug Logging:**
|
||||||
```python
|
```python
|
||||||
import logging
|
import logging
|
||||||
@@ -1000,10 +847,6 @@ logging.getLogger('src.background_data_service').setLevel(logging.DEBUG)
|
|||||||
|
|
||||||
## 5. Permission Management
|
## 5. Permission Management
|
||||||
|
|
||||||
Ownership, modes, sudo rules and the repair scripts are listed in
|
|
||||||
[PERMISSIONS.md](PERMISSIONS.md). This section covers the helpers code uses
|
|
||||||
to keep files shareable.
|
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
LEDMatrix uses a dual-user architecture: the display service runs as root (hardware access), while the web interface runs as a non-privileged user. Centralized permission management ensures both can access necessary files.
|
LEDMatrix uses a dual-user architecture: the display service runs as root (hardware access), while the web interface runs as a non-privileged user. Centralized permission management ensures both can access necessary files.
|
||||||
@@ -1028,7 +871,6 @@ from src.common.permission_utils import (
|
|||||||
ensure_file_permissions,
|
ensure_file_permissions,
|
||||||
get_config_file_mode,
|
get_config_file_mode,
|
||||||
get_assets_file_mode,
|
get_assets_file_mode,
|
||||||
get_assets_dir_mode,
|
|
||||||
get_plugin_file_mode,
|
get_plugin_file_mode,
|
||||||
get_cache_dir_mode
|
get_cache_dir_mode
|
||||||
)
|
)
|
||||||
@@ -1037,10 +879,7 @@ from src.common.permission_utils import (
|
|||||||
ensure_directory_permissions(Path("assets/sports"), get_assets_dir_mode())
|
ensure_directory_permissions(Path("assets/sports"), get_assets_dir_mode())
|
||||||
|
|
||||||
# Set file permissions after writing
|
# Set file permissions after writing
|
||||||
# (get_config_file_mode requires the file path — secrets files get a
|
ensure_file_permissions(Path("config/config.json"), get_config_file_mode())
|
||||||
# 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
|
### When to Use Utilities
|
||||||
@@ -1067,7 +906,7 @@ ensure_file_permissions(config_path, get_config_file_mode(config_path))
|
|||||||
| Config (secrets) | `rw-r-----` | `0o640` | Owner write, group read |
|
| Config (secrets) | `rw-r-----` | `0o640` | Owner write, group read |
|
||||||
| Assets | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
| Assets | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
||||||
| Plugins | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
| Plugins | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
||||||
| Cache files | `rw-rw----` | `0o660` | Owner/group write, no world access (`_CACHE_FILE_MODE` in `src/cache/disk_cache.py`) |
|
| Cache files | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
||||||
|
|
||||||
**Directory Permissions:**
|
**Directory Permissions:**
|
||||||
|
|
||||||
@@ -1095,7 +934,7 @@ from src.common.permission_utils import ensure_file_permissions, get_config_file
|
|||||||
config_path = Path("config/config.json")
|
config_path = Path("config/config.json")
|
||||||
with open(config_path, 'w') as f:
|
with open(config_path, 'w') as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
ensure_file_permissions(config_path, get_config_file_mode(config_path))
|
ensure_file_permissions(config_path, get_config_file_mode())
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pattern 3: Downloading Logo**
|
**Pattern 3: Downloading Logo**
|
||||||
@@ -1138,28 +977,43 @@ These core utilities **already handle permissions** - you don't need to call per
|
|||||||
|
|
||||||
### Manual Fixes
|
### Manual Fixes
|
||||||
|
|
||||||
[PERMISSIONS.md](PERMISSIONS.md) lists who owns what on an installed system,
|
If you encounter permission issues:
|
||||||
the expected modes, and which `scripts/fix_perms/` script to run as which
|
|
||||||
user. In short:
|
|
||||||
|
|
||||||
- `fix_assets_permissions.sh`, `fix_cache_permissions.sh` and
|
```bash
|
||||||
`fix_plugin_permissions.sh` are run with `sudo`.
|
# Fix all permissions at once
|
||||||
- `fix_web_permissions.sh` is run as the web interface user, without
|
sudo ./scripts/fix_permissions.sh
|
||||||
`sudo` (it refuses to run as root and calls `sudo` itself where needed).
|
|
||||||
It resets project file ownership for that user, then makes the two
|
|
||||||
helper scripts the web user may run as root (`safe_plugin_rm.sh`,
|
|
||||||
`safe_pip_install.sh`) root-owned again and restores `config_secrets.json`
|
|
||||||
to its owner, the `ledmatrix` group and mode `640`. It does not write
|
|
||||||
sudoers rules; `scripts/install/configure_web_sudo.sh` does that.
|
|
||||||
|
|
||||||
Do not `chmod` the whole `config/` directory: `config_secrets.json` must stay
|
# Fix specific directory
|
||||||
`640`.
|
sudo chown -R ledpi:ledpi /home/ledpi/LEDMatrix/config
|
||||||
|
sudo chmod -R 2775 /home/ledpi/LEDMatrix/config
|
||||||
|
sudo find /home/ledpi/LEDMatrix/config -type f -exec chmod 664 {} \;
|
||||||
|
|
||||||
|
# Verify permissions
|
||||||
|
ls -la config/
|
||||||
|
ls -la assets/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check directory has setgid bit
|
||||||
|
ls -ld assets/
|
||||||
|
# Should show: drwxrwsr-x (note the 's')
|
||||||
|
|
||||||
|
# Check file has correct group
|
||||||
|
ls -l assets/logo.png
|
||||||
|
# Should show group 'ledpi'
|
||||||
|
|
||||||
|
# Check file permissions
|
||||||
|
stat -c "%a %n" config/config.json
|
||||||
|
# Should show: 644 config/config.json
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [PLUGIN_DEVELOPMENT_GUIDE.md](PLUGIN_DEVELOPMENT_GUIDE.md) - Creating plugins with Vegas/on-demand support
|
- [PLUGIN_DEVELOPMENT.md](PLUGIN_DEVELOPMENT.md) - Creating plugins with Vegas/on-demand support
|
||||||
- [WEB_INTERFACE_GUIDE.md](WEB_INTERFACE_GUIDE.md) - Using on-demand controls in web UI
|
- [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
|
- [PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md) - Complete API documentation
|
||||||
- [DEVELOPMENT.md](DEVELOPMENT.md) - Development environment and testing
|
- [DEVELOPMENT.md](DEVELOPMENT.md) - Development environment and testing
|
||||||
|
|||||||
@@ -2,18 +2,12 @@
|
|||||||
|
|
||||||
Advanced patterns, examples, and best practices for developing LEDMatrix plugins.
|
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
|
## Table of Contents
|
||||||
|
|
||||||
- [Using Weather Icons](#using-weather-icons)
|
- [Using Weather Icons](#using-weather-icons)
|
||||||
- [Implementing Scrolling with Deferred Updates](#implementing-scrolling-with-deferred-updates)
|
- [Implementing Scrolling with Deferred Updates](#implementing-scrolling-with-deferred-updates)
|
||||||
- [Cache Strategy Patterns](#cache-strategy-patterns)
|
- [Cache Strategy Patterns](#cache-strategy-patterns)
|
||||||
- [Font Management](#font-management)
|
- [Font Management and Overrides](#font-management-and-overrides)
|
||||||
- [Error Handling Best Practices](#error-handling-best-practices)
|
- [Error Handling Best Practices](#error-handling-best-practices)
|
||||||
- [Performance Optimization](#performance-optimization)
|
- [Performance Optimization](#performance-optimization)
|
||||||
- [Testing Plugins with Mocks](#testing-plugins-with-mocks)
|
- [Testing Plugins with Mocks](#testing-plugins-with-mocks)
|
||||||
@@ -25,12 +19,69 @@ Advanced patterns, examples, and best practices for developing LEDMatrix plugins
|
|||||||
|
|
||||||
## Using Weather Icons
|
## Using Weather Icons
|
||||||
|
|
||||||
The Display Manager's icon methods — `draw_weather_icon()`, `draw_sun()`,
|
The Display Manager provides built-in weather icon drawing methods for easy visual representation of weather conditions.
|
||||||
`draw_cloud()`, `draw_rain()`, `draw_snow()` and `draw_text_with_icons()` —
|
|
||||||
were removed in 3.8.0. Draw your own icons instead: render them
|
### Basic Weather Icon Usage
|
||||||
onto a PIL image and paste it onto `self.display_manager.image`, or ship
|
|
||||||
icon images with the plugin. The weather plugin's `WeatherIcons` class is an
|
```python
|
||||||
example. See [Deprecated APIs](PLUGIN_API_REFERENCE.md#deprecated-apis).
|
def display(self, force_clear=False):
|
||||||
|
if force_clear:
|
||||||
|
self.display_manager.clear()
|
||||||
|
|
||||||
|
# Draw weather icon based on condition
|
||||||
|
condition = self.data.get('condition', 'clear')
|
||||||
|
self.display_manager.draw_weather_icon(condition, x=5, y=5, size=16)
|
||||||
|
|
||||||
|
# Draw temperature next to icon
|
||||||
|
temp = self.data.get('temp', 72)
|
||||||
|
self.display_manager.draw_text(
|
||||||
|
f"{temp}°F",
|
||||||
|
x=25, y=10,
|
||||||
|
color=(255, 255, 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.display_manager.update_display()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Weather Conditions
|
||||||
|
|
||||||
|
The `draw_weather_icon()` method automatically maps condition strings to appropriate icons:
|
||||||
|
|
||||||
|
- `"clear"`, `"sunny"` → Sun icon
|
||||||
|
- `"clouds"`, `"cloudy"`, `"partly cloudy"` → Cloud icon
|
||||||
|
- `"rain"`, `"drizzle"`, `"shower"` → Rain icon
|
||||||
|
- `"snow"`, `"sleet"`, `"hail"` → Snow icon
|
||||||
|
- `"thunderstorm"`, `"storm"` → Storm icon
|
||||||
|
|
||||||
|
### Custom Weather Icons
|
||||||
|
|
||||||
|
For more control, use individual icon methods:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Draw specific icons
|
||||||
|
self.display_manager.draw_sun(x=10, y=10, size=16)
|
||||||
|
self.display_manager.draw_cloud(x=10, y=10, size=16, color=(150, 150, 150))
|
||||||
|
self.display_manager.draw_rain(x=10, y=10, size=16)
|
||||||
|
self.display_manager.draw_snow(x=10, y=10, size=16)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text with Weather Icons
|
||||||
|
|
||||||
|
Use `draw_text_with_icons()` to combine text and icons:
|
||||||
|
|
||||||
|
```python
|
||||||
|
icons = [
|
||||||
|
("sun", 5, 5), # Sun icon at (5, 5)
|
||||||
|
("cloud", 100, 5) # Cloud icon at (100, 5)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.display_manager.draw_text_with_icons(
|
||||||
|
"Weather: Sunny, Cloudy",
|
||||||
|
icons=icons,
|
||||||
|
x=10, y=20,
|
||||||
|
color=(255, 255, 255)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,53 +91,31 @@ For plugins that scroll content (tickers, news feeds, etc.), use scrolling state
|
|||||||
|
|
||||||
### Basic Scrolling Implementation
|
### Basic Scrolling Implementation
|
||||||
|
|
||||||
Scroll with `ScrollHelper`, configured by `src.common.scroll_config`, and
|
|
||||||
render one frame per `display()` call. Don't pace the scroll with
|
|
||||||
`time.sleep()`: `update_display()` blocks on the panel's
|
|
||||||
vsync, which is what paces a scroll. Pass the `frame_hold` that
|
|
||||||
`scroll_config.configure()` returned to `set_scrolling_state()`, or the
|
|
||||||
scroll runs faster than the configured speed (see
|
|
||||||
`set_scrolling_state()` in [PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md)).
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
from src.common import scroll_config
|
|
||||||
from src.common.scroll_helper import ScrollHelper
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.scroll_helper = ScrollHelper(
|
|
||||||
self.display_manager.width, self.display_manager.height, self.logger)
|
|
||||||
self.scroll_settings = scroll_config.configure(
|
|
||||||
self.scroll_helper,
|
|
||||||
plugin_config=self.config,
|
|
||||||
global_config=self.global_config,
|
|
||||||
display_manager=self.display_manager,
|
|
||||||
plugin_logger=self.logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _build_scroll_image(self, text):
|
|
||||||
font = self.display_manager.regular_font
|
|
||||||
width = self.display_manager.get_text_width(text, font)
|
|
||||||
img = Image.new("RGB", (width, self.display_manager.height))
|
|
||||||
ImageDraw.Draw(img).text((0, 0), text, font=font, fill=(255, 255, 255))
|
|
||||||
self.scroll_helper.set_scrolling_image(img)
|
|
||||||
|
|
||||||
def display(self, force_clear=False):
|
def display(self, force_clear=False):
|
||||||
if force_clear or self.scroll_helper.cached_image is None:
|
if force_clear:
|
||||||
self._build_scroll_image(
|
self.display_manager.clear()
|
||||||
"This is a long scrolling message that needs to scroll across the display...")
|
|
||||||
|
|
||||||
# Mark as scrolling (calling it every frame is fine)
|
# Mark as scrolling
|
||||||
self.display_manager.set_scrolling_state(
|
self.display_manager.set_scrolling_state(True)
|
||||||
True, frame_hold=self.scroll_settings.frame_hold)
|
|
||||||
self.scroll_helper.update_scroll_position()
|
|
||||||
self.display_manager.image = self.scroll_helper.get_visible_portion()
|
|
||||||
self.display_manager.update_display()
|
|
||||||
|
|
||||||
if self.scroll_helper.is_scroll_complete():
|
try:
|
||||||
# Mark as not scrolling when done
|
# Scroll content
|
||||||
|
text = "This is a long scrolling message that needs to scroll across the display..."
|
||||||
|
text_width = self.display_manager.get_text_width(text, self.display_manager.regular_font)
|
||||||
|
display_width = self.display_manager.width
|
||||||
|
|
||||||
|
# Scroll from right to left
|
||||||
|
for x in range(display_width, -text_width, -2):
|
||||||
|
self.display_manager.clear()
|
||||||
|
self.display_manager.draw_text(text, x=x, y=16, color=(255, 255, 255))
|
||||||
|
self.display_manager.update_display()
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Update scroll activity timestamp
|
||||||
|
self.display_manager.set_scrolling_state(True)
|
||||||
|
finally:
|
||||||
|
# Always mark as not scrolling when done
|
||||||
self.display_manager.set_scrolling_state(False)
|
self.display_manager.set_scrolling_state(False)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -194,8 +223,11 @@ def update(self):
|
|||||||
sport_key = "nhl"
|
sport_key = "nhl"
|
||||||
cache_key = f"{self.plugin_id}_{sport_key}_games"
|
cache_key = f"{self.plugin_id}_{sport_key}_games"
|
||||||
|
|
||||||
# get_background_cached_data() was removed in 3.8.0 — use get()
|
# Uses sport-specific live_update_interval from config
|
||||||
cached = self.cache_manager.get(cache_key, max_age=60)
|
cached = self.cache_manager.get_background_cached_data(
|
||||||
|
cache_key,
|
||||||
|
sport_key=sport_key
|
||||||
|
)
|
||||||
|
|
||||||
if cached:
|
if cached:
|
||||||
self.games = cached
|
self.games = cached
|
||||||
@@ -222,9 +254,9 @@ def on_config_change(self, new_config):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Font Management
|
## Font Management and Overrides
|
||||||
|
|
||||||
The display manager's built-in fonts and text measurement. For fonts shipped with a plugin, see [FONT_MANAGER.md](FONT_MANAGER.md).
|
Use the Font Manager for advanced font handling and user customization.
|
||||||
|
|
||||||
### Using Different Fonts
|
### Using Different Fonts
|
||||||
|
|
||||||
@@ -596,12 +628,14 @@ def update(self):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def update(self):
|
def update(self):
|
||||||
# get_enabled_plugins() was removed in 3.8.0 — check the instance's
|
# Check if another plugin is enabled
|
||||||
# `enabled` flag instead
|
enabled_plugins = self.plugin_manager.get_enabled_plugins()
|
||||||
weather_plugin = self.plugin_manager.get_plugin("weather")
|
if "weather" in enabled_plugins:
|
||||||
if weather_plugin is not None and weather_plugin.enabled:
|
# Weather plugin is available
|
||||||
# Use weather data
|
weather_plugin = self.plugin_manager.get_plugin("weather")
|
||||||
pass
|
if weather_plugin:
|
||||||
|
# Use weather data
|
||||||
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sharing Data Between Plugins
|
### Sharing Data Between Plugins
|
||||||
|
|||||||
@@ -1,416 +0,0 @@
|
|||||||
# Architecture
|
|
||||||
|
|
||||||
A map of the codebase for a new contributor: which process does what, how
|
|
||||||
they talk to each other, and where to start reading for common changes.
|
|
||||||
|
|
||||||
## Processes
|
|
||||||
|
|
||||||
| systemd unit | Runs as | Runs | Installed by |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `ledmatrix.service` | root | [`run.py`](../run.py) → `DisplayController` | [`install_service.sh`](../scripts/install/install_service.sh) |
|
|
||||||
| `ledmatrix-web.service` | the installing user | [`start_web_conditionally.py`](../scripts/utils/start_web_conditionally.py) → [`web_interface/start.py`](../web_interface/start.py) (Flask, port 5000) | `install_service.sh`, [`install_web_service.sh`](../scripts/install/install_web_service.sh) |
|
|
||||||
| `ledmatrix-update-verify.path` / `.service` | the web user | Health check after an automatic update | the same installers, or [`src/auto_update_setup.py`](../src/auto_update_setup.py) at runtime |
|
|
||||||
| `ledmatrix-wifi-monitor.service` | root | [`wifi_monitor_daemon.py`](../scripts/utils/wifi_monitor_daemon.py) | [`install_wifi_monitor.sh`](../scripts/install/install_wifi_monitor.sh) |
|
|
||||||
| `ledmatrix-mqtt-bridge.service` | root | [MQTT bridge](../integrations/mqtt_bridge/README.md) (optional) | [`install_mqtt_bridge.sh`](../scripts/install/install_mqtt_bridge.sh) |
|
|
||||||
| `ledmatrix-dns-fix.service` | root | DNS workaround (optional) | [`install_dns_fix.sh`](../scripts/install/install_dns_fix.sh) |
|
|
||||||
|
|
||||||
Unit templates are in [`systemd/`](../systemd/README.md). The display runs as
|
|
||||||
root because the LED matrix library needs direct GPIO access. The web
|
|
||||||
interface runs unprivileged and uses a fixed list of `sudo` rules for the
|
|
||||||
few privileged things it does; see [PERMISSIONS.md](PERMISSIONS.md).
|
|
||||||
|
|
||||||
`start_web_conditionally.py` exits without starting Flask when
|
|
||||||
`web_display_autostart` is explicitly false in `config.json`.
|
|
||||||
|
|
||||||
## How the two main processes share state
|
|
||||||
|
|
||||||
The display and the web interface are separate processes that never call
|
|
||||||
each other. They share three things:
|
|
||||||
|
|
||||||
1. **`config/config.json` and `config/config_secrets.json`.** The web
|
|
||||||
interface writes them through `ConfigManager`
|
|
||||||
([`src/config_manager.py`](../src/config_manager.py)); the display
|
|
||||||
notices through `ConfigService` (below).
|
|
||||||
2. **The disk cache**, `/var/cache/ledmatrix` (owned `root:ledmatrix`,
|
|
||||||
setgid, files `0660`), read and written through `CacheManager`
|
|
||||||
([`src/cache_manager.py`](../src/cache_manager.py),
|
|
||||||
[`src/cache/disk_cache.py`](../src/cache/disk_cache.py)). Readers in the
|
|
||||||
other process pass `memory_ttl=0` so they do not serve a stale in-memory
|
|
||||||
copy.
|
|
||||||
3. **A few files in `/tmp`.**
|
|
||||||
|
|
||||||
| State | Where | Written by | Read by |
|
|
||||||
|---|---|---|---|
|
|
||||||
| On-demand command | control socket `/run/ledmatrix/control.sock` ([IPC_CONTROL_SOCKET.md](IPC_CONTROL_SOCKET.md)) | web: `start_on_demand_display()` / `stop_on_demand_display()` in [`api_v3/display.py`](../web_interface/blueprints/api_v3/display.py), via [`src/ipc/client.py`](../src/ipc/client.py) | display: [`src/ipc/server.py`](../src/ipc/server.py) acks; the render thread applies it in `_poll_on_demand_requests()` |
|
|
||||||
| On-demand request (fallback) | cache `display_on_demand_request` | web, only when the socket could not carry the request (`should_fall_back`); four plugins write it directly | display: `_poll_on_demand_requests()`, a `stat()` every 1 s while the socket is up (0.25 s without), read only when the file changed |
|
|
||||||
| On-demand state | cache `display_on_demand_state` | display: `_publish_on_demand_state()` | web: `/api/v3/display/on-demand/status` |
|
|
||||||
| Current screen | cache `display_current_state` | display | web: `/api/v3/display/current-status` |
|
|
||||||
| Plugin errors | cache `plugin_error_snapshot` | display: `ErrorSnapshotPublisher` ([`src/error_aggregator.py`](../src/error_aggregator.py)) | web: `read_error_report()` for `/api/v3/errors/*` |
|
|
||||||
| Error clear | control socket `errors.clear`; cache `plugin_error_clear_request` as the fallback | web: `POST /api/v3/errors/clear` | display: applied before the socket answers; the mailbox on the error publisher's 5 s tick, read only when the file changed |
|
|
||||||
| Font usage | cache `font_usage_snapshot` | display: `FontUsagePublisher` ([`src/font_usage.py`](../src/font_usage.py)) | web: Fonts tab |
|
|
||||||
| Fetch statistics (requests per plugin and host) | cache `fetch_stats_snapshot` | display: `FetchStatsPublisher` ([`src/common/fetch_service.py`](../src/common/fetch_service.py)), at most once a minute on change | web: `read_fetch_stats()` for `/api/v3/plugins/fetch-stats` |
|
|
||||||
| Plugin health | cache `plugin_health:<id>` | display (web writes on reset) | web: `/api/v3/plugins/health` |
|
|
||||||
| Plugin runtime (loaded, state, last error, version) | cache `plugin_runtime_snapshot` | display: `PluginRuntimePublisher` ([`src/plugin_system/plugin_runtime.py`](../src/plugin_system/plugin_runtime.py)) | web: `read_plugin_runtime()` for `/api/v3/plugins/installed`, `/plugins/state`, reconciliation |
|
|
||||||
| Preview frame | `/tmp/led_matrix_preview.png` | display: `DisplayManager`, gated by [`snapshot_policy`](../src/common/snapshot_policy.py): a changed frame at most once a second with a viewer, every 30 s without | web: display SSE stream (checks the mtime every 0.25 s), `/api/v3/health` (file age) |
|
|
||||||
| Preview viewer marker | `/tmp/led_matrix_preview_viewer` | web, about once a second while a preview is open | display: writes viewer-rate snapshots only while it is fresh (5 s) |
|
|
||||||
| Hardware init status | `/tmp/led_matrix_hw_status.json` | display | web: `/api/v3/hardware/status` |
|
|
||||||
| Render-loop heartbeat | `/run/ledmatrix/display-heartbeat.json` (tmpfs) | display: the render thread, via [`display_watchdog`](../src/display_watchdog.py) | web: `/api/v3/health` (`checks.display_loop`); the update health check |
|
|
||||||
|
|
||||||
The on-demand start route starts `ledmatrix.service` when it is not running
|
|
||||||
(`start_service`, on by default) but never restarts a running one. The routes
|
|
||||||
send the command over the display's control socket and get an ack; only when
|
|
||||||
the socket could not carry it (a stopped display, one older than the socket
|
|
||||||
or the command) do they write the mailbox instead. A display that had the
|
|
||||||
request and refused it is answered with the error, not posted a mailbox
|
|
||||||
copy. The display looks at the mailbox every
|
|
||||||
`MAILBOX_POLL_INTERVAL_WITH_SOCKET` (1 s) while it serves the socket, and
|
|
||||||
every `ON_DEMAND_POLL_INTERVAL` (0.25 s) without one, from its dwell sleep,
|
|
||||||
its render loops and Vegas's interrupt check as well as the main loop; a
|
|
||||||
look is one `stat()` unless the file changed. Both ways end in the same
|
|
||||||
handler, `_handle_on_demand_request()`.
|
|
||||||
The socket's handlers only queue; see [IPC_CONTROL_SOCKET.md](IPC_CONTROL_SOCKET.md)
|
|
||||||
for the protocol, the permission model and the plan to retire the mailboxes.
|
|
||||||
|
|
||||||
### Web and display processes: who runs plugins
|
|
||||||
|
|
||||||
Only the display process imports plugin code, instantiates plugins and calls
|
|
||||||
their lifecycle hooks (`update`, `display`, `on_config_change`, `on_enable`,
|
|
||||||
`on_disable`). The web process is metadata-only: it reads plugins as files
|
|
||||||
-- manifests and directories through `PluginCatalog`
|
|
||||||
([`src/plugin_system/plugin_catalog.py`](../src/plugin_system/plugin_catalog.py)),
|
|
||||||
config schemas through `SchemaManager`, and each plugin's section of
|
|
||||||
`config.json` through `ConfigManager`. The catalog keeps the
|
|
||||||
read-only method names of `PluginManager` and has nothing that can run a
|
|
||||||
plugin (no `load_plugin`, `get_plugin` or `plugins`).
|
|
||||||
|
|
||||||
How a web-side change reaches the running plugins:
|
|
||||||
|
|
||||||
| Change | How the display picks it up |
|
|
||||||
|---|---|
|
|
||||||
| Plugin settings saved, config reset | `ConfigService` sees the new `config.json` and calls the plugin's `on_config_change` with the prepared section |
|
|
||||||
| Plugin enabled or disabled | `ConfigService` → `_controller_config_change` flags a reconcile; `_reconcile_enabled_plugins` loads it (fresh from disk) or unloads it on the render thread |
|
|
||||||
| Plugin uninstalled (config removed) | the removed section flips its `enabled` flag, and the reconcile unloads it |
|
|
||||||
| Plugin installed, not enabled | nothing to do until it is enabled, which loads it |
|
|
||||||
| Plugin updated while enabled | the update route asks the display over the control socket (`plugin.reload`) to reload it on the render thread, and answers `restart_required: false` once the new code runs. Without the socket, as the next row |
|
|
||||||
| Plugin installed while already enabled, updated while enabled and not reloaded, or uninstalled with its config kept | **not picked up**: the display keeps running what it loaded. The route answers `restart_required: true` and the UI shows its restart banner |
|
|
||||||
|
|
||||||
`display_restart_required()` in `plugin_catalog.py` holds that last rule;
|
|
||||||
routes return it as `restart_required` (with the banner's wording in
|
|
||||||
`restart_message`), and `window.noteRestartRequired()` in
|
|
||||||
`static/v3/app.js` raises the banner for any response that carries it,
|
|
||||||
`POST /api/v3/config/main` included.
|
|
||||||
|
|
||||||
Runtime state shown in the UI comes from what the display publishes to the
|
|
||||||
shared cache: health and metrics (`/api/v3/plugins/health`,
|
|
||||||
`/plugins/metrics`), errors (`/api/v3/errors/*`), the current mode, and the
|
|
||||||
plugin runtime snapshot described below. `enabled` is read from
|
|
||||||
`config.json` by the display's rule (a missing flag is disabled).
|
|
||||||
|
|
||||||
Plugin code still runs in the web process in one place,
|
|
||||||
`_import_plugin_code_in_web_process()` in
|
|
||||||
[`api_v3/__init__.py`](../web_interface/blueprints/api_v3/__init__.py): the
|
|
||||||
Starlark routes import the starlark-apps plugin's `tronbyte_repository` and
|
|
||||||
`pixlet_renderer` helper modules (never the plugin class), and a web-UI
|
|
||||||
action with `oauth_flow` imports its script for `get_auth_url()`. Every
|
|
||||||
other web-UI action runs its script as a subprocess. A later, explicit
|
|
||||||
**plugin web-entry contract** -- a declared entry point for plugin web code
|
|
||||||
-- replaces that function.
|
|
||||||
|
|
||||||
The **control socket** from the web process to the display
|
|
||||||
([IPC_CONTROL_SOCKET.md](IPC_CONTROL_SOCKET.md)) carries on-demand
|
|
||||||
commands and reloads an updated plugin; its next stages stream the
|
|
||||||
display's state and retire the cache-key mailboxes. The plugin web-entry
|
|
||||||
contract above is still to come.
|
|
||||||
|
|
||||||
### Plugin state: desired, observed, and who owns it
|
|
||||||
|
|
||||||
There is one plugin state machine, and the display owns it:
|
|
||||||
`PluginStateManager` in
|
|
||||||
[`plugin_state.py`](../src/plugin_system/plugin_state.py) (unloaded →
|
|
||||||
loaded → enabled ⇄ running, error, disabled), held by the display's
|
|
||||||
`PluginManager`. It also records, per loaded plugin, the manifest version it
|
|
||||||
loaded and when. Nothing else keeps plugin state:
|
|
||||||
|
|
||||||
| Question | Answered by |
|
|
||||||
|---|---|
|
|
||||||
| Is it installed, at which version? | the plugins directory (`manifest.json`) |
|
|
||||||
| Should it run? | `config.json` (`<id>.enabled`, missing = disabled) |
|
|
||||||
| Has the user uninstalled it for good? | the store's uninstalled-plugins record |
|
|
||||||
| Is the display running it, at which version, and why not? | the display's runtime snapshot |
|
|
||||||
|
|
||||||
**The runtime snapshot.** `PluginRuntimePublisher`
|
|
||||||
([`plugin_runtime.py`](../src/plugin_system/plugin_runtime.py)), started by
|
|
||||||
`DisplayController` right after it creates the `PluginManager`, writes the
|
|
||||||
cache key `plugin_runtime_snapshot`: per plugin `loaded`, `state`, `error`
|
|
||||||
(type, a redacted message of at most 200 characters, when, recoverable),
|
|
||||||
`version`, `loaded_at` and `modes` (the display modes `DisplayController`
|
|
||||||
registered -- `plugin.modes` when the plugin computes them, else the
|
|
||||||
manifest's), plus `published_at`, `stale_after` and `running`.
|
|
||||||
The cache is on disk, usually the SD card, so it writes when something a
|
|
||||||
reader sees changes -- throttled to once per 10 s -- and otherwise once a
|
|
||||||
minute as a heartbeat. RUNNING, which every `update()` passes through, is
|
|
||||||
published as ENABLED, so plugin updates alone never cause a write.
|
|
||||||
`cleanup()` publishes `running: false`.
|
|
||||||
|
|
||||||
**Reading it.** `read_plugin_runtime()` judges the snapshot before anyone
|
|
||||||
uses it: `live` (fresh, from a running display), `stale` (older than
|
|
||||||
`stale_after`, 3 minutes: a hung or crashed display), `stopped` or
|
|
||||||
`unknown` (none, unreadable, or another schema). Only a live view reports
|
|
||||||
per-plugin facts; every other status answers `null` for them, so stale
|
|
||||||
truth cannot leak into a response. `/api/v3/plugins/installed` returns
|
|
||||||
`loaded`, `state`, `error_info`, `loaded_version` and `loaded_at` per
|
|
||||||
plugin and `data.runtime` (`status`, `published_at`, `age_seconds`);
|
|
||||||
`/api/v3/plugins/state` returns the same beside the desired state.
|
|
||||||
`PluginCatalog.get_plugin_display_modes` and `find_plugin_for_mode` prefer a
|
|
||||||
live view's `modes` to the manifest's `display_modes`, so `/display/modes`
|
|
||||||
and on-demand see modes a plugin generates from its config (#668).
|
|
||||||
|
|
||||||
**Reconciliation**
|
|
||||||
([`state_reconciliation.py`](../src/plugin_system/state_reconciliation.py))
|
|
||||||
compares desired state (config + disk) with observed state (the snapshot).
|
|
||||||
It fixes desired-state gaps -- a plugin on disk with no config section gets
|
|
||||||
`{"enabled": false}`, a configured plugin missing from disk is reinstalled
|
|
||||||
unless the user uninstalled it -- and only reports observed-state gaps
|
|
||||||
(enabled but not loaded, loaded at an older version): the display loads and
|
|
||||||
unloads by config on its own, and a version gap needs a restart.
|
|
||||||
|
|
||||||
**`data/plugin_state.json` is retired.** The web process used to keep a
|
|
||||||
second `PluginStateManager` (`state_manager.py`) persisted to that file:
|
|
||||||
per plugin an enabled flag copied from config, a version copied from the
|
|
||||||
manifest (when set at all), a status derived from those, and install/update
|
|
||||||
timestamps. Reconciliation mostly synced it back to config and backups
|
|
||||||
merged it into their plugin list. Every field is derivable (the timestamps
|
|
||||||
from the operation history), so nothing is migrated: no code reads or
|
|
||||||
writes the file, and a copy left on a device is inert and safe to delete.
|
|
||||||
The two classes shared a name but not a concern -- a persisted install
|
|
||||||
record versus the live lifecycle -- so they were not merged; the persisted
|
|
||||||
one had nothing left to hold and was removed.
|
|
||||||
|
|
||||||
## Display loop
|
|
||||||
|
|
||||||
[`src/display_controller.py`](../src/display_controller.py), class
|
|
||||||
`DisplayController`. `__init__` loads config, starts the cache and the
|
|
||||||
error-snapshot publisher, runs the startup validator, creates the
|
|
||||||
`DisplayManager` ([`src/display_manager.py`](../src/display_manager.py)),
|
|
||||||
`FontManager` and `PluginManager`, loads the enabled plugins in parallel,
|
|
||||||
runs an initial `update()` pass within a 20-second budget
|
|
||||||
(`_INITIAL_UPDATE_BUDGET_SECONDS`; a plugin that misses it is deferred to
|
|
||||||
the scheduler), and sets up Vegas mode.
|
|
||||||
|
|
||||||
`run()` is the main loop. Each pass, in order: apply a pending plugin
|
|
||||||
enable/disable, poll on-demand requests, run scheduled plugin updates, check
|
|
||||||
the on/off schedule and brightness, then show one screen. Priority is
|
|
||||||
on-demand, then WiFi status messages, then live priority, then Vegas mode,
|
|
||||||
then normal rotation. [RUN_LOOP_REDESIGN.md](RUN_LOOP_REDESIGN.md) is the
|
|
||||||
plan for restructuring this loop and lists its golden trace tests.
|
|
||||||
|
|
||||||
- **Rotation.** `available_modes` is the ordered list of display modes;
|
|
||||||
`current_mode_index` advances after each screen.
|
|
||||||
`_apply_plugin_rotation_order()` applies `display.plugin_rotation_order`.
|
|
||||||
- **Durations.** `_get_display_duration()`: `display.display_durations[mode]`,
|
|
||||||
else the plugin's `get_display_duration()`, else 30 s. Plugins that
|
|
||||||
support dynamic duration run until `is_cycle_complete()`, capped by
|
|
||||||
`display.dynamic_duration.max_duration_seconds` (default 180 s).
|
|
||||||
- **On-demand.** A request from the web interface pins one plugin (or mode)
|
|
||||||
for a duration. `_activate_on_demand()` / `_clear_on_demand()`; the
|
|
||||||
session is saved under `display_on_demand_config` so it survives a
|
|
||||||
restart. It also keeps the display on during scheduled off hours. A
|
|
||||||
request for a plugin that is disabled in config loads it live
|
|
||||||
(`_load_plugin_for_on_demand()`, `load_plugin(force_enabled=True)`)
|
|
||||||
without writing `config.json`; the main loop unloads it once on-demand
|
|
||||||
moves off it (`_release_on_demand_plugins()`).
|
|
||||||
- **Live priority.** `_check_live_priority()` looks for a plugin whose
|
|
||||||
`has_live_priority()` and `has_live_content()` are both true and switches
|
|
||||||
to it, rotating between several live games.
|
|
||||||
- **Schedule and dim schedule.** `_check_schedule()` reads `schedule`;
|
|
||||||
`_check_dim_schedule()` reads `dim_schedule` and
|
|
||||||
`display.hardware.brightness`. Both are re-evaluated once a minute, and
|
|
||||||
both windows are half-open: on (or dimmed) from the start time, off at
|
|
||||||
the end time. When an on-demand session ends, the on/off schedule is
|
|
||||||
re-checked at once rather than at the next minute.
|
|
||||||
- **Long screens.** While a screen is showing (a dwell, a scroll, a Vegas
|
|
||||||
iteration), `_service_pending_changes()` repeats the on-demand, schedule
|
|
||||||
and brightness checks every 0.25 s, so a change does not wait for the
|
|
||||||
screen to end.
|
|
||||||
- **Config hot reload.** `ConfigService`
|
|
||||||
([`src/config_service.py`](../src/config_service.py)) polls the config and
|
|
||||||
secrets files' mtimes every 2 s and notifies subscribers when the content
|
|
||||||
changes. The controller refreshes its cached settings; enabling or
|
|
||||||
disabling a plugin queues `_reconcile_enabled_plugins()`, which loads or
|
|
||||||
unloads it on the display thread; each plugin gets `on_config_change()`
|
|
||||||
for its own section, under its plugin lock
|
|
||||||
(`PluginManager.apply_config_change()`). Set `LEDMATRIX_HOT_RELOAD=false` to turn this off.
|
|
||||||
Matrix hardware settings are only read at start-up.
|
|
||||||
- **Vegas mode.** [`src/vegas_mode/`](../src/vegas_mode/): the display loop
|
|
||||||
calls `VegasModeCoordinator.run_iteration()`
|
|
||||||
([`coordinator.py`](../src/vegas_mode/coordinator.py)) when
|
|
||||||
`display.vegas_scroll.enabled` is set. `PluginAdapter` gets each plugin's
|
|
||||||
content (`get_vegas_content()`, else its `scroll_helper` image, else a
|
|
||||||
capture of `display()`), `StreamManager` orders it and `RenderPipeline`
|
|
||||||
scrolls it. See [ADVANCED_FEATURES.md](ADVANCED_FEATURES.md).
|
|
||||||
- **Multi-display sync.** `DisplaySyncManager`
|
|
||||||
([`src/common/sync_manager.py`](../src/common/sync_manager.py)), enabled by
|
|
||||||
`sync.role`: a leader sends a follower its share of each frame over UDP
|
|
||||||
(port 5765).
|
|
||||||
|
|
||||||
### Liveness
|
|
||||||
|
|
||||||
A render thread stuck inside a plugin leaves the service "active" and the
|
|
||||||
panel frozen, so liveness is reported by the render thread itself
|
|
||||||
([`src/display_watchdog.py`](../src/display_watchdog.py), standard library
|
|
||||||
only). `beat()` from any other thread is ignored: the update worker, Vegas's
|
|
||||||
tick thread and the prefetcher keep running while the render thread is stuck,
|
|
||||||
and must not vouch for it.
|
|
||||||
|
|
||||||
- **Check-in points.** The top of `run()`'s loop (`loop_pass()`), every
|
|
||||||
dwell second (`_sleep_with_plugin_updates`), every frame of the per-screen
|
|
||||||
loops (`_display_once`), every frame of Vegas's own loop and static pause
|
|
||||||
(`coordinator.run_iteration`), each plugin fetched for a Vegas cycle
|
|
||||||
(`StreamManager._fetch_plugin_content`), each update on the
|
|
||||||
`synchronous_updates` path, and every frame pushed
|
|
||||||
(`DisplayManager.update_display` -> `note_frame()`). Beats are
|
|
||||||
rate-limited to one ping and one heartbeat write every 5 s.
|
|
||||||
- **systemd watchdog.** `ledmatrix.service` is `Type=simple` with
|
|
||||||
`WatchdogSec=120` and `NotifyAccess=main`. `run.py` sends
|
|
||||||
`WATCHDOG_USEC` = 15 minutes before importing anything heavy (start-up loads
|
|
||||||
plugins and runs the 20 s update budget, and the watchdog clock starts with
|
|
||||||
the process). After the first frame -- or the first full pass, when there is
|
|
||||||
nothing to draw -- the loop sends `READY=1`, restores the unit's 120 s and
|
|
||||||
pings. `PluginManager.load_plugin()` on the render thread (a plugin enabled
|
|
||||||
from the web UI, or loaded for on-demand) gets 15 minutes again, since it
|
|
||||||
can run pip. A missed deadline is a SIGABRT; faulthandler, enabled on
|
|
||||||
arming, dumps every thread's stack to the journal.
|
|
||||||
- **Heartbeat.** `/run/ledmatrix/display-heartbeat.json`
|
|
||||||
(`{"pid", "mono", "wall"}`; `RuntimeDirectory=ledmatrix`, 0755, file 0644 so
|
|
||||||
the web user can read it). Readers compare `mono` with their own
|
|
||||||
`time.monotonic()` -- CLOCK_MONOTONIC is shared by every process and does not
|
|
||||||
jump when NTP first sets an RTC-less Pi's clock. `/api/v3/health` calls it
|
|
||||||
`stalled` past 60 s; no file is `not_reported` and changes nothing. A clean
|
|
||||||
stop removes it. Without `RuntimeDirectory=` (an older unit) the display,
|
|
||||||
as root, creates the directory itself; off Linux, or without root, there
|
|
||||||
is no heartbeat.
|
|
||||||
|
|
||||||
## Plugin system
|
|
||||||
|
|
||||||
[`src/plugin_system/`](../src/plugin_system/):
|
|
||||||
|
|
||||||
| Area | Where |
|
|
||||||
|---|---|
|
|
||||||
| Base class plugins implement | [`base_plugin.py`](../src/plugin_system/base_plugin.py) (`BasePlugin`, `VegasDisplayMode`) |
|
|
||||||
| Finding a plugin's directory | [`plugin_dirs.py`](../src/plugin_system/plugin_dirs.py): manifest `id` first, then directory `<id>` or `ledmatrix-<id>` |
|
|
||||||
| Discovery, load, unload, scheduled updates (display process) | [`plugin_manager.py`](../src/plugin_system/plugin_manager.py) (`PluginManager`) |
|
|
||||||
| Manifest reads (web process) | [`plugin_catalog.py`](../src/plugin_system/plugin_catalog.py) (`PluginCatalog`; see [who runs plugins](#web-and-display-processes-who-runs-plugins)) |
|
|
||||||
| Import and instantiate | [`plugin_loader.py`](../src/plugin_system/plugin_loader.py) (`PluginLoader.load_plugin()`: dependencies, module, class) |
|
|
||||||
| Timeouts | [`plugin_executor.py`](../src/plugin_system/plugin_executor.py) (`PluginExecutor`, 30 s default; a timed-out thread is abandoned, not killed) |
|
|
||||||
| Circuit breaker | [`plugin_health.py`](../src/plugin_system/plugin_health.py) (`PluginHealthTracker`: 3 consecutive failures open the circuit for 300 s) |
|
|
||||||
| Resource metrics | [`resource_monitor.py`](../src/plugin_system/resource_monitor.py) |
|
|
||||||
| Config schemas and defaults | [`schema_manager.py`](../src/plugin_system/schema_manager.py) |
|
|
||||||
| Install, update, uninstall | [`store_manager.py`](../src/plugin_system/store_manager.py) (`PluginStoreManager`), with its methods split across [`store_registry.py`](../src/plugin_system/store_registry.py) (registry, GitHub), [`store_install.py`](../src/plugin_system/store_install.py) and [`store_update.py`](../src/plugin_system/store_update.py) |
|
|
||||||
| Core-version gate | [`compatibility.py`](../src/plugin_system/compatibility.py) |
|
|
||||||
|
|
||||||
Discovery scans only `plugin_system.plugins_directory` (default
|
|
||||||
`plugin-repos/`). Scheduled `update()` calls run on one background worker
|
|
||||||
thread; a per-plugin lock keeps `display()` from running during an update.
|
|
||||||
|
|
||||||
**Store flow.** `install_plugin()` renames any existing copy aside
|
|
||||||
(`<id>.standalone-backup-preinstall`), installs the new one, and puts the old
|
|
||||||
copy back if the install fails. Monorepo plugins come from the GitHub Trees
|
|
||||||
API, falling back to the repository ZIP; other plugins by `git clone` or
|
|
||||||
download. The manifest is checked (see
|
|
||||||
[required fields](PLUGIN_API_REFERENCE.md#manifest-required-fields)), the core
|
|
||||||
version gate runs, then dependencies are installed as root through
|
|
||||||
`scripts/fix_perms/safe_pip_install.sh`. `update_plugin()` pulls git
|
|
||||||
installs, undoing a pull whose new version is incompatible, and reinstalls
|
|
||||||
everything else through `_reinstall_with_rollback()`.
|
|
||||||
|
|
||||||
## Web interface
|
|
||||||
|
|
||||||
- **App.** [`web_interface/app.py`](../web_interface/app.py) builds the
|
|
||||||
Flask `app` at import time, creates the managers -- a `PluginCatalog`,
|
|
||||||
never a `PluginManager` -- and registers two blueprints.
|
|
||||||
`web_interface/start.py` runs it on port 5000.
|
|
||||||
- **Pages.** [`blueprints/pages_v3.py`](../web_interface/blueprints/pages_v3.py)
|
|
||||||
serves the shell `templates/v3/base.html` at `/` and each tab as a
|
|
||||||
partial at `/partials/<name>` (templates in
|
|
||||||
`web_interface/templates/v3/partials/`). Plugin configuration tabs are
|
|
||||||
rendered from the plugin's schema by `plugin_config.html`.
|
|
||||||
- **API.** [`blueprints/api_v3/`](../web_interface/blueprints/api_v3/) is one
|
|
||||||
blueprint at `/api/v3`, split by area: `backup.py`, `config.py`,
|
|
||||||
`display.py`, `fonts.py`, `misc.py` (health, logs, errors, cache, sync),
|
|
||||||
`starlark.py`, `system.py` (service actions, updates, git), `wifi.py`, and
|
|
||||||
the plugin routes: `plugins.py` (installed list, enable/disable, plugin
|
|
||||||
actions), `plugin_store.py` (install, update, uninstall, store),
|
|
||||||
`plugin_config.py` (config, schema, reset), `plugin_assets.py` (uploads,
|
|
||||||
plugin static files), `plugin_health.py` (health, metrics, limits),
|
|
||||||
`plugin_operations.py` (operation history, state reconciliation) and
|
|
||||||
`plugin_calendar.py`. `__init__.py` defines the blueprint and shared helpers and
|
|
||||||
imports the modules so their routes register. Endpoints are listed in
|
|
||||||
[REST_API_REFERENCE.md](REST_API_REFERENCE.md).
|
|
||||||
- **Front end.** HTMX loads each tab's partial on first open
|
|
||||||
(`hx-trigger="loadtab"`); Alpine.js holds page state. Scripts are in
|
|
||||||
`web_interface/static/v3/js/`; form widgets are bundled from
|
|
||||||
[`js/widgets/`](../web_interface/static/v3/js/widgets/README.md).
|
|
||||||
- **Server-sent events** (`app.py`): `/api/v3/stream/stats` (CPU, memory,
|
|
||||||
temperature, service state, every 10 s), `/api/v3/stream/display` (preview
|
|
||||||
frames when the PNG changes) and `/api/v3/stream/logs` (journal of both
|
|
||||||
services). One generator thread per stream is shared by all clients.
|
|
||||||
|
|
||||||
## Updates
|
|
||||||
|
|
||||||
- **Update Code** on the Overview tab and the automatic updater both call
|
|
||||||
`perform_core_update()` in
|
|
||||||
[`api_v3/system.py`](../web_interface/blueprints/api_v3/system.py):
|
|
||||||
fetch branches and tags, move the checkout for the update channel, reinstall
|
|
||||||
changed requirement files, report whether a restart is needed.
|
|
||||||
- **Update channels** (`auto_update.channel`):
|
|
||||||
[`web_interface/update_channel.py`](../web_interface/update_channel.py)
|
|
||||||
decides the move. `stable` checks out the newest `vX.Y.Z` tag (detached
|
|
||||||
HEAD) when it contains the current commit; `beta` is
|
|
||||||
`git pull --rebase --autostash` on the current branch, and leaves a
|
|
||||||
detached release for `main` first. A stable device newer than the newest
|
|
||||||
release keeps pulling `main` until a release contains its commit, so no
|
|
||||||
update ever moves backwards; a config without the key is written as
|
|
||||||
`stable` once the device reaches a release. Checkouts carry uncommitted
|
|
||||||
edits across with `git stash create`/`apply`, and keep them in the stash
|
|
||||||
list if they no longer apply.
|
|
||||||
- **Automatic updates** (`auto_update.enabled`, off by default):
|
|
||||||
`AutoUpdater` in [`web_interface/auto_update.py`](../web_interface/auto_update.py)
|
|
||||||
runs in the web process, checks every 30 minutes, and updates at most
|
|
||||||
weekly between 02:00 and 05:00. Before pulling it copies
|
|
||||||
[`scripts/utils/auto_update_verify.py`](../scripts/utils/auto_update_verify.py)
|
|
||||||
to `data/auto_update_verifier.py`, then writes
|
|
||||||
`data/auto_update_verify.request`. That file triggers
|
|
||||||
`ledmatrix-update-verify.path`, which runs the verifier as a separate unit
|
|
||||||
(so restarting the web service does not kill it). The verifier restarts
|
|
||||||
both services, waits for the web API to answer and the display service to
|
|
||||||
stay up -- and, when the display wrote a heartbeat before the update, to
|
|
||||||
keep one fresh from the restarted process (see Liveness) -- and on failure
|
|
||||||
returns to where HEAD was (the branch, or detached on the previous
|
|
||||||
release; `old_ref` in the pending file), resets to the previous commit
|
|
||||||
and restarts again.
|
|
||||||
Plugin updates run only after a verified core update. State is in
|
|
||||||
`data/auto_update_state.json` and `data/auto_update_pending.json`.
|
|
||||||
- **Startup validator.** `StartupValidator`
|
|
||||||
([`src/startup_validator.py`](../src/startup_validator.py)) runs twice in
|
|
||||||
`DisplayController.__init__`: config and cache directory first, then
|
|
||||||
enabled plugins once the plugin manager exists. It also warns when an
|
|
||||||
installed systemd unit differs from its template in `systemd/`. Results
|
|
||||||
are logged; startup continues either way. Nothing rewrites installed units
|
|
||||||
on update: a unit change such as the watchdog reaches an existing install
|
|
||||||
only when `install_service.sh` is re-run.
|
|
||||||
|
|
||||||
## Where to start reading
|
|
||||||
|
|
||||||
| Task | Start with |
|
|
||||||
|---|---|
|
|
||||||
| Change rotation, durations or priorities | `DisplayController.run()` and `_get_display_duration()` in [`display_controller.py`](../src/display_controller.py) |
|
|
||||||
| Add a config key | [CONFIG_REFERENCE.md](CONFIG_REFERENCE.md), [`config/config.template.json`](../config/config.template.json), the tab's partial and `api_v3/config.py` |
|
|
||||||
| Change drawing or fonts | [`display_manager.py`](../src/display_manager.py), [`font_manager.py`](../src/font_manager.py), [`src/common/bdf_font.py`](../src/common/bdf_font.py) |
|
|
||||||
| Add a plugin-facing API | [`base_plugin.py`](../src/plugin_system/base_plugin.py) or [`src/common/`](../src/common/README.md); document it in [PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md) |
|
|
||||||
| Plugin install/update bugs | `PluginStoreManager` in [`store_manager.py`](../src/plugin_system/store_manager.py) |
|
|
||||||
| A plugin that won't load | `PluginManager.load_plugin()` and `PluginLoader.load_plugin()`; `python3 scripts/check_plugin.py --plugin <id>` |
|
|
||||||
| Add an API endpoint | the matching module in [`api_v3/`](../web_interface/blueprints/api_v3/) |
|
|
||||||
| Add a web UI tab or control | `templates/v3/base.html`, the tab's partial, `pages_v3.py` |
|
|
||||||
| Vegas scroll | [`src/vegas_mode/coordinator.py`](../src/vegas_mode/coordinator.py) |
|
|
||||||
| Installer or permissions | [`first_time_install.sh`](../first_time_install.sh), [`scripts/install/`](../scripts/install/), [PERMISSIONS.md](PERMISSIONS.md) |
|
|
||||||
| Work without a Pi | [DEV_PREVIEW.md](DEV_PREVIEW.md), [EMULATOR_SETUP_GUIDE.md](EMULATOR_SETUP_GUIDE.md), [HOW_TO_RUN_TESTS.md](HOW_TO_RUN_TESTS.md) |
|
|
||||||
@@ -172,14 +172,10 @@ ERROR - Plugin football-scoreboard configuration validation failed: 'api_key' is
|
|||||||
|
|
||||||
### Enable Debug Logging
|
### Enable Debug Logging
|
||||||
|
|
||||||
Run the display in the foreground with `-d`, or set `LEDMATRIX_DEBUG=true`
|
Set environment variable:
|
||||||
(the value must be `true`; `1` is ignored — see `setup_logging()` in
|
|
||||||
[`src/logging_config.py`](../src/logging_config.py)):
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl stop ledmatrix.service
|
export LEDMATRIX_DEBUG=1
|
||||||
sudo python3 run.py -d
|
python run.py
|
||||||
# or
|
|
||||||
sudo LEDMATRIX_DEBUG=true python3 run.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Check Merged Configuration
|
### Check Merged Configuration
|
||||||
@@ -254,38 +250,19 @@ WARNING - Plugin ID 'Football-Scoreboard' may conflict with 'football-scoreboard
|
|||||||
|
|
||||||
## Checking Configuration via API
|
## Checking Configuration via API
|
||||||
|
|
||||||
The API blueprint (`web_interface/blueprints/api_v3/`) is registered at
|
|
||||||
`/api/v3` in `web_interface/app.py`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Get full main config (includes all plugin sections; credential-named
|
# Get current config
|
||||||
# fields are blanked in the response)
|
curl http://localhost:5000/api/v3/config
|
||||||
curl http://localhost:5000/api/v3/config/main
|
|
||||||
|
|
||||||
# Change some settings: only the keys you send are changed
|
# Get specific plugin config
|
||||||
curl -X POST http://localhost:5000/api/v3/config/main \
|
curl http://localhost:5000/api/v3/config/plugin/football-scoreboard
|
||||||
|
|
||||||
|
# Validate config without saving
|
||||||
|
curl -X POST http://localhost:5000/api/v3/config/validate \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"timezone": "America/Chicago", "brightness": 80}'
|
-d '{"football-scoreboard": {"enabled": true}}'
|
||||||
|
|
||||||
# Replace config.json wholesale (advanced)
|
|
||||||
curl -X POST http://localhost:5000/api/v3/config/raw/main \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-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. `POST /plugins/config` validates against the plugin's schema
|
|
||||||
> and rejects an invalid config with `400`; `POST /config/main` checks the
|
|
||||||
> individual fields it knows (display hardware values, durations, Vegas
|
|
||||||
> and sync settings). See
|
|
||||||
> [REST_API_REFERENCE.md](REST_API_REFERENCE.md) for the full list.
|
|
||||||
|
|
||||||
## Backup and Recovery
|
## Backup and Recovery
|
||||||
|
|
||||||
### Manual Backup
|
### Manual Backup
|
||||||
@@ -296,12 +273,9 @@ cp config/config.json config/config.backup.json
|
|||||||
|
|
||||||
### Automatic Backups
|
### Automatic Backups
|
||||||
|
|
||||||
LEDMatrix creates backups before saves (`src/config_manager_atomic.py`):
|
LEDMatrix creates backups before saves:
|
||||||
- Location: `config/backups/`
|
- Location: `config/backups/`
|
||||||
- Format: `config.json.backup.YYYYMMDD_HHMMSS_ffffff` (microseconds last),
|
- Format: `config_YYYYMMDD_HHMMSS.json`
|
||||||
plus a matching `config_secrets.json.backup.<timestamp>` when a secrets
|
|
||||||
file exists
|
|
||||||
- The five most recent are kept
|
|
||||||
|
|
||||||
### Recovery
|
### Recovery
|
||||||
|
|
||||||
@@ -310,7 +284,7 @@ LEDMatrix creates backups before saves (`src/config_manager_atomic.py`):
|
|||||||
ls -la config/backups/
|
ls -la config/backups/
|
||||||
|
|
||||||
# Restore from backup
|
# Restore from backup
|
||||||
cp config/backups/config.json.backup.20240115_120000_000000 config/config.json
|
cp config/backups/config_20240115_120000.json config/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting Checklist
|
## Troubleshooting Checklist
|
||||||
@@ -325,10 +299,8 @@ cp config/backups/config.json.backup.20240115_120000_000000 config/config.json
|
|||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
1. Check logs. Both services log to journald, not to a file:
|
1. Check logs: `tail -f logs/ledmatrix.log`
|
||||||
`sudo journalctl -u ledmatrix.service -f` (display) and
|
2. Enable debug: `LEDMATRIX_DEBUG=1`
|
||||||
`sudo journalctl -u ledmatrix-web.service -f` (web interface)
|
|
||||||
2. Enable debug: `LEDMATRIX_DEBUG=true` or `python3 run.py -d`
|
|
||||||
3. Check error dashboard: `/api/v3/errors/summary`
|
3. Check error dashboard: `/api/v3/errors/summary`
|
||||||
4. Validate JSON: https://jsonlint.com/
|
4. Validate JSON: https://jsonlint.com/
|
||||||
5. File an issue: https://github.com/ChuckBuilds/LEDMatrix/issues
|
5. File an issue: https://github.com/ChuckBuilds/LEDMatrix/issues
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
# 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` |
|
|
||||||
| `auto_update.enabled` | bool, `false` | Weekly automatic updates: LEDMatrix code first (health-checked, rolled back on failure), then installed plugins. Toggle in the General tab or install with `first_time_install.sh --enable-auto-update` | `web_interface/auto_update.py`, `src/auto_update_setup.py` (`is_enabled()`) |
|
|
||||||
| `auto_update.channel` | `"stable"` or `"beta"`, `"stable"` (template) | What Update Code and the weekly update install. `stable`: the newest `vX.Y.Z` release tag (pre-releases ignored), checked out with a detached HEAD. `beta`: `main`. Never moves a device backwards: one newer than the newest release keeps following `main` until a release contains its commit. Missing (configs from before channels) behaves like `stable` and is saved as `stable` once the device is on a release. General tab, Update Channel | `web_interface/update_channel.py` (`resolve()`) |
|
|
||||||
| `timezone` | string, `"America/New_York"` | IANA timezone for schedules and displays | `ConfigManager.get_timezone()` |
|
|
||||||
| `target_fps` | int, `100` | Legacy "Scroll Frame Rate". Core scrolling no longer reads it: scroll frames are presented at `display.hardware.limit_refresh_rate_hz` divided by each scroll's frame hold, and speed comes from each plugin's scroll settings. Still exposed to plugins via `BasePlugin.global_config` | `src/plugin_system/base_plugin.py` |
|
|
||||||
| `location` | object | `city` / `state` / `country`. Supplies the **default** for a plugin's own `location_city` / `location_state` / `location_country` setting, so weather, radar and friends follow this device without being configured twice. A value saved on the plugin itself still overrides it. Starlark (Tidbyt) apps get the same treatment: a `Location` field left blank on the app renders at this city (geocoded once via Open-Meteo, coordinates cached permanently) instead of the app author's default, which is usually San Francisco. If the city can't be looked up (no match, or the geocoder is unreachable; retried after 30 minutes), the app keeps its own default. | `SchemaManager.apply_device_location()`, then plugins via merged config; `src/device_location.py` for Starlark apps |
|
|
||||||
|
|
||||||
## `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 |
|
|
||||||
|
|
||||||
The display is on from `start_time` up to, but not including, `end_time`:
|
|
||||||
with `07:00`–`23:00` it turns on at 07:00 and off at 23:00. An end earlier
|
|
||||||
than the start crosses midnight (`22:00`–`07:00` is on overnight). In
|
|
||||||
per-day mode, the entry for the current day decides. An on-demand session
|
|
||||||
keeps the display on during off hours; once it ends or is stopped, the
|
|
||||||
display blanks within about a second.
|
|
||||||
|
|
||||||
Read by `DisplayController._check_schedule()` (`src/display_controller.py`).
|
|
||||||
Managed in the web UI under Schedule.
|
|
||||||
|
|
||||||
## `dim_schedule` — scheduled brightness dimming
|
|
||||||
|
|
||||||
Same shape as `schedule` (the template sets its `mode` to `"global"`), plus:
|
|
||||||
|
|
||||||
| Key | Type / default | Meaning |
|
|
||||||
|---|---|---|
|
|
||||||
| `dim_brightness` | int, `30` | Brightness percentage applied while the dim window is active |
|
|
||||||
|
|
||||||
Read by `DisplayController._check_dim_schedule()` (`src/display_controller.py`;
|
|
||||||
saved via `POST /api/v3/config/dim-schedule`). The display returns to
|
|
||||||
`display.hardware.brightness` outside the window. The window has the same
|
|
||||||
boundaries as `schedule`: dimmed from `start_time` up to, but not including,
|
|
||||||
`end_time`.
|
|
||||||
|
|
||||||
## `display.hardware` — matrix panel hardware
|
|
||||||
|
|
||||||
All keys map to the corresponding `rpi-rgb-led-matrix` options and are read
|
|
||||||
in `DisplayManager._setup_matrix` (`src/display_manager.py`). Defaults are the
|
|
||||||
`config/config.template.json` values: `ConfigManager` adds any key missing from
|
|
||||||
`config.json` from the template on load, so `DisplayManager`'s own fallbacks
|
|
||||||
don't apply on a normal install.
|
|
||||||
|
|
||||||
The ranges are what the pinned rgbmatrix library and its Python binding accept
|
|
||||||
(`src/matrix_support.py`). The config API refuses anything else; a value
|
|
||||||
hand-edited into `config.json` makes the display log the setting and run in
|
|
||||||
fallback mode instead of starting the matrix.
|
|
||||||
|
|
||||||
| Key | Type / default |
|
|
||||||
|---|---|
|
|
||||||
| `rows` / `cols` | int, `32` / `64` — rows: even, 8–64; cols: at least 16 |
|
|
||||||
| `chain_length` | int, `2` — 1–255 (the Python binding stores it in one byte) |
|
|
||||||
| `parallel` | int, `1` — 1–3, and no more than `hardware_mapping` has outputs (`regular`, `classic`: 3; the others: 1) |
|
|
||||||
| `brightness` | int, `90` — 1–100 |
|
|
||||||
| `hardware_mapping` | string, `"adafruit-hat"` — `"adafruit-hat-pwm"`, `"adafruit-hat"`, `"regular"`, `"regular-pi1"`, `"classic"` or `"classic-pi1"` (case-insensitive; `compute-module` isn't in the installed build). A Pi 5 doesn't support `"classic-pi1"` |
|
|
||||||
| `scan_mode` | int, `0` — `0` progressive, `1` interlaced |
|
|
||||||
| `pwm_bits` | int, `9` — 1–11 |
|
|
||||||
| `pwm_dither_bits` | int, `1` — 0–2 |
|
|
||||||
| `pwm_lsb_nanoseconds` | int, `130` — 50–3000 |
|
|
||||||
| `disable_hardware_pulsing` | bool, `false` — `true` times brightness pulses in software (less exact); hardware pulsing needs the OE line on GPIO 18 and the Pi's onboard sound driver off |
|
|
||||||
| `inverse_colors` | bool, `false` |
|
|
||||||
| `show_refresh_rate` | bool, `false` — prints the refresh rate to stdout; draws nothing on the panel |
|
|
||||||
| `led_rgb_sequence` | string, `"RGB"` — `"RGB"`, `"RBG"`, `"GRB"`, `"GBR"`, `"BRG"` or `"BGR"` |
|
|
||||||
| `limit_refresh_rate_hz` | int, `100` — `0` = no cap; scroll timing assumes 100 Hz when `0` |
|
|
||||||
| `pixel_mapper_config` | string, `""` — e.g. `"U-mapper"` / `"Rotate:90"`; mappers that rotate or fold the chain change the display size plugins and the web preview see |
|
|
||||||
| `orientation` | string, `"normal"` — `"180"` rotates the rendered image 180° for panels physically mounted upside down (e.g. to move the Pi/wiring to a more convenient side); `"90"` / `"270"` for a panel on its side, swapping width and height; composed onto `pixel_mapper_config` as a trailing `Rotate:<degrees>` mapper, so it stays independent of any custom `pixel_mapper_config` value |
|
|
||||||
| `row_address_type` | int, `0` — non-standard panel row addressing: `1` AB, `2` direct row select, `3` ABC, `4` ABC shift + DE direct, `5` SM5368 / B707 row shift register (e.g. Waveshare 96x48 V2, with `led_rgb_sequence` `"BGR"`). On a Pi 5 the library supports only `0` and `2`, and LEDMatrix enforces that (`src/pi5_matrix_support.py`) |
|
|
||||||
| `multiplexing` | int, `0` — 0–22, pixel wiring scheme for outdoor/specialty panels (names listed in the README) |
|
|
||||||
| `panel_type` | string, `""` — set to `"FM6126A"` or `"FM6127"` for panels needing init; FM6124 / FM6124D / FM6124DJ panels need none, so leave it `""` |
|
|
||||||
|
|
||||||
## `display.runtime`
|
|
||||||
|
|
||||||
| Key | Type / default | Meaning |
|
|
||||||
|---|---|---|
|
|
||||||
| `gpio_slowdown` | int, `3` | GPIO timing slowdown for faster Pis (0–10). On a Pi 5 in PIO mode start at `1` (`0` acts as `1`) and raise it if the image flickers or shows garbage. Panels on `row_address_type` `5` (SM5368 row drivers) can need 6–8 on a Pi 4 — lower values make rows jump |
|
|
||||||
| `rp1_rio` | int, `0` | Pi 5 only: `0` = PIO (less CPU), `1` = RIO (higher refresh; `gpio_slowdown` effect inverted). 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`) | `DisplayController._get_display_duration()` (`src/display_controller.py`) |
|
|
||||||
| `plugin_rotation_order` | array, `[]` | Explicit rotation order of plugin ids; empty = all enabled plugins in discovery order | `DisplayController._apply_plugin_rotation_order()` (`src/display_controller.py`) |
|
|
||||||
| `use_short_date_format` | bool, `true` | Compact date rendering in sports scoreboards | Nothing since `src/base_classes` was removed; scoreboards read `display.use_short_date_format` from their own plugin config |
|
|
||||||
| `scan_order_compensation` | string, `"auto"` | `"auto"` shows one half of each panel a refresh behind while something scrolls at one frame per refresh, which removes the 1px step a 1:N-scan panel shows across its middle; `"off"` disables it. Applies only to layouts whose row order is known: plain or parallel chains, 0 or 180 degree orientation, `multiplexing` 0, `scan_mode` 0, and not in the emulator | `DisplayManager._setup_scan_order_compensation()` (`src/display_manager.py`, `src/scan_order.py`) |
|
|
||||||
| `dynamic_duration.max_duration_seconds` | int, optional | Cap for plugins that request dynamic display time | `DisplayController._get_global_dynamic_cap()` (`src/display_controller.py`) |
|
|
||||||
|
|
||||||
## `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, including
|
|
||||||
[live content in the ticker](ADVANCED_FEATURES.md#live-content-in-the-ticker).
|
|
||||||
|
|
||||||
| 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` |
|
|
||||||
| `offscreen_prefetch` | bool, `true` — render every plugin's ticker content on the background thread, each on its own canvas. `false` restores handing canvas-bound plugins to the render thread, one pause at a time. Temporary; see [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
|
||||||
| `prefetch_gate` | bool, `true` — let that background thread run Python only while the render thread is waiting for the panel, so the render thread never waits for the GIL when a refresh comes round. Only takes effect with the rebuilt rgbmatrix binding (`scripts/build_rgbmatrix_nogil.sh`). See [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
|
||||||
| `switch_interval_ms` | float, `0` — experimental: shorten Python's GIL switch interval to this many ms while Vegas runs. `0` leaves the default (5 ms) alone |
|
|
||||||
| `live_refresh` | bool, `true` — live elements: a plugin that supports them (scores, the flight map) has what is already scrolling updated when its data changes, instead of freezing each card as it was drawn. Always off under multi-display sync, in swap mode and with `offscreen_prefetch` off. `false` restores the frozen behaviour exactly. Per plugin: `vegas_live` in the plugin's section |
|
|
||||||
| `live_max_hz` | float, `5` (0–10) — ceiling on how often an animated live element (a moving aircraft) is redrawn; `0` keeps data updates and turns animation off. Capped at 1 Hz without the rebuilt rgbmatrix binding |
|
|
||||||
| `live_min_interval` | float, `2` (0.5–60) — shortest time between two data redraws of one plugin; a faster plugin is redrawn at this rate, never skipped |
|
|
||||||
| `live_lead_screens` | float, `1` (0–5) — how far ahead of the screen, in screen widths, an animated element starts being redrawn |
|
|
||||||
| `smooth_scroll` | bool, `true` — move a whole number of pixels per panel refresh, locked to vsync. `scroll_speed` is snapped to the nearest speed the panel can show that way (at 95Hz: 95, 47.5, 31.7 px/s…), measured against the panel's real refresh rate once scrolling starts |
|
|
||||||
| `sub_pixel_blend` | bool, `false` — the older smoothing: advance by elapsed time and blend neighbouring pixel columns. Looks anti-aliased in the web preview but shimmers on the panel and is not locked to the refresh. Overrides `smooth_scroll` when on |
|
|
||||||
| `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, `0.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` — does not step or set a frame rate; motion is by elapsed time either way. When `true`, `scroll_speed` passes through a clamp of 0.1–5 px per `scroll_delay` (see next row) |
|
|
||||||
| `scroll_delay` | float, `0.02` — not a frame period. Only used with `frame_based_scrolling`: the applied speed is `clamp(scroll_speed × scroll_delay, 0.1, 5) / scroll_delay` px/s, so at `0.02` speeds under 5 px/s run at 5, and at `0.001` nothing runs slower than 100 px/s |
|
|
||||||
| `live_in_ticker` | bool, `true` — keep scrolling during live games instead of handing the display to a full-screen scoreboard. `false` was the default before 3.8.0; the first start on 3.8.0 turns a stored `false` on once and sets `live_in_ticker_migrated` |
|
|
||||||
| `live_weight` | int, `3` (1–10) — slots per cycle for a plugin with live content |
|
|
||||||
| `favorite_live_weight` | int, `5` (1–10) — slots per cycle when a plugin reports a favorite team is live |
|
|
||||||
|
|
||||||
## `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`) |
|
|
||||||
|
|
||||||
## `plugin_system`
|
|
||||||
|
|
||||||
| Key | Type / default | Meaning |
|
|
||||||
|---|---|---|
|
|
||||||
| `plugins_directory` | string, `"plugin-repos"` | Where the Plugin Store installs plugins and the only directory the plugin loader scans. Read by `PluginManager` and `PluginStoreManager` (`src/plugin_system/`); editable under General settings |
|
|
||||||
| `auto_discover`, `auto_load_enabled`, `development_mode` | bool | **Unused.** Legacy keys, read by nothing and no longer in the template; older configs may still carry them. Plugins are always discovered, and every plugin with `enabled: true` is loaded — to keep a plugin installed but dormant, set its own `enabled` to `false`. Not shown in the web UI; may be left in or removed from config.json |
|
|
||||||
|
|
||||||
## 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_registry.py`) |
|
|
||||||
| `<plugin-id>.*` | Secrets a plugin declares with `"x-secret": true` in its config schema; merged into that plugin's config at load time |
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
# Deprecated plugin APIs: usage scan
|
|
||||||
|
|
||||||
Generated by `scripts/plugin_api_usage.py` — do not edit by hand; re-run it (see [How to re-run](#how-to-re-run)).
|
|
||||||
|
|
||||||
- Scanned: 2026-10-05, core 3.8.2
|
|
||||||
- Monorepo: [ChuckBuilds/ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) (main @ 662fb86f), 46 plugins
|
|
||||||
- Third-party plugins: 8 with their own repo in `plugins.json` (f1-live, gif-player, pga-tour-leaderboard, plex-marquee, ledmatrix-dresden-departures, tidbyt-baseball-scoreboard, sleeper-fantasy, ledmatrix-nascar)
|
|
||||||
|
|
||||||
**45 deprecated methods: 31 unused, 3 still used, 11 need review.**
|
|
||||||
|
|
||||||
Counted per plugin: a *call* is `<receiver>.method` on an object named like the owner (`cache_manager`, `display_manager`, `font_manager`, `plugin_manager`), or on `self`/`super()` in a subclass; an *override* is `def method` in a subclass of the owner. *Review* hits are `.method` on a receiver whose type the scan cannot tell. *Internal* hits sit inside another deprecated core method and go with it. *Unrelated* hits are a different class's own method with the same name (a name collision), and never block removal; neither do hits in test files.
|
|
||||||
|
|
||||||
| Method | Removal | Core | Plugins (calls / overrides) | Name collisions & tests | Verdict |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `BackgroundDataService.get_result` | 3.10.0 | core tests (15 test reviews) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `BackgroundDataService.is_request_complete` | 3.10.0 | core tests (11 test reviews) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `BackgroundDataService.get_request_status` | 3.10.0 | core tests (2 test reviews) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `BaseOddsManager.get_odds_for_games` | 3.10.0 | core tests (3 test reviews) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `BaseOddsManager.format_odds_summary` | 3.10.0 | core tests (5 test reviews) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `CacheManager.load_cache` | 3.10.0 | core tests (1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `CacheManager.generate_sport_cache_key` | 3.10.0 | core tests (2 test calls, 1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | 3.10.0 | core tests (1 test call, 4 test reviews) | — | football-scoreboard (1 test review); hockey-scoreboard (1 test review); ufc-scoreboard (3 test reviews) | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.fetch_espn_standings` | 3.10.0 | core tests (1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.fetch_espn_rankings` | 3.10.0 | core tests (1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.set_cache` | 3.10.0 | core tests (1 test call, 1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.get_cache` | 3.10.0 | core tests (1 test call) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.set_rate_limit` | 3.10.0 | core tests (10 test calls, 3 test reviews) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `APIHelper.get_request_stats` | 3.10.0 | — | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `ConfigManager.rollback_config` | 3.10.0 | core (1 internal); core tests (1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `ConfigManager.list_backups` | 3.10.0 | core (1 internal); core tests (1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `ConfigManager.validate_config_file` | 3.10.0 | core (1 internal) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `ConfigManager.get_secret` | 3.10.0 | core tests (5 test calls, 1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `ConfigManager.cleanup_orphaned_plugin_configs` | 3.10.0 | core tests (2 test calls, 1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `ConfigManager.validate_all_plugin_configs` | 3.10.0 | core tests (1 test call, 1 test review) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `DynamicTeamResolver.get_available_dynamic_teams` | 3.10.0 | — | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `DynamicTeamResolver.is_dynamic_team` | 3.10.0 | — | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `FontManager.get_native_bdf_size` | 3.10.0 | core tests (3 test calls) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `FontManager.measure_text` | 3.10.0 | core tests (5 test calls) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `LogoDownloader.fetch_teams_data` | 3.10.0 | core (2 internals) | — | — | still used by core — keep or migrate first |
|
|
||||||
| `LogoDownloader.extract_teams_from_data` | 3.10.0 | core (2 internals) | — | — | still used by core — keep or migrate first |
|
|
||||||
| `LogoDownloader.download_missing_logos_for_league` | 3.10.0 | core (1 call, 1 internal); core tests (2 test calls) | — | — | still used by core — keep or migrate first |
|
|
||||||
| `LogoDownloader.download_all_ncaa_football_logos` | 3.10.0 | core tests (2 test calls) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `LogoDownloader.download_all_missing_logos` | 3.10.0 | — | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `LogoDownloader.convert_image_to_rgba` | 3.10.0 | core (1 internal) | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `LogoDownloader.convert_all_logos_to_rgba` | 3.10.0 | — | — | — | unused — safe to remove in 3.10.0 |
|
|
||||||
| `BasePlugin.get_supported_vegas_modes` | 3.9.0 | core tests (2 test reviews) | — | — | unused — safe to remove in 3.9.0 |
|
|
||||||
| `BasePlugin.get_vegas_segment_width` | 3.9.0 | core tests (1 test review) | — | — | unused — safe to remove in 3.9.0 |
|
|
||||||
| `PluginManager.get_all_plugins` | 3.10.0 | — | football-scoreboard (1 review) | football-scoreboard (2 test reviews); hockey-scoreboard (1 test review) | needs review: possible use in football-scoreboard |
|
|
||||||
| `PluginManager.get_plugin_info` | 3.10.0 | core (12 reviews, 1 internal); core tests (6 test calls, 9 test reviews) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginManager.get_all_plugin_info` | 3.10.0 | core (3 reviews); core tests (2 test calls, 11 test reviews) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | 3.10.0 | core (4 reviews); core tests (3 test calls, 5 test reviews) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginManager.find_plugin_for_mode` | 3.10.0 | core (2 reviews) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginStateManager.is_loaded` | 3.10.0 | core (1 internal) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginStateManager.is_running` | 3.10.0 | core (1 internal) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginStateManager.is_error` | 3.10.0 | core (1 internal) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginStateManager.get_error_info` | 3.10.0 | core (1 internal); core tests (4 test calls) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginStateManager.get_last_update` | 3.10.0 | core (1 internal) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginStateManager.get_state_info` | 3.10.0 | core (1 internal); core tests (7 test reviews) | — | — | needs review: possible use in core |
|
|
||||||
| `PluginTestCase.setUp` | 3.10.0 | — | — | basketball-scoreboard (2 test unrelateds); cricket-scoreboard (1 test unrelated); hockey-scoreboard (1 test unrelated); nrl-scoreboard (1 test unrelated) | unused — safe to remove in 3.10.0 |
|
|
||||||
|
|
||||||
## Unused — safe to remove (31)
|
|
||||||
|
|
||||||
`BackgroundDataService.get_result`, `BackgroundDataService.is_request_complete`, `BackgroundDataService.get_request_status`, `BaseOddsManager.get_odds_for_games`, `BaseOddsManager.format_odds_summary`, `CacheManager.load_cache`, `CacheManager.generate_sport_cache_key`, `APIHelper.fetch_espn_scoreboard`, `APIHelper.fetch_espn_standings`, `APIHelper.fetch_espn_rankings`, `APIHelper.set_cache`, `APIHelper.get_cache`, `APIHelper.set_rate_limit`, `APIHelper.get_request_stats`, `ConfigManager.rollback_config`, `ConfigManager.list_backups`, `ConfigManager.validate_config_file`, `ConfigManager.get_secret`, `ConfigManager.cleanup_orphaned_plugin_configs`, `ConfigManager.validate_all_plugin_configs`, `DynamicTeamResolver.get_available_dynamic_teams`, `DynamicTeamResolver.is_dynamic_team`, `FontManager.get_native_bdf_size`, `FontManager.measure_text`, `LogoDownloader.download_all_ncaa_football_logos`, `LogoDownloader.download_all_missing_logos`, `LogoDownloader.convert_image_to_rgba`, `LogoDownloader.convert_all_logos_to_rgba`, `BasePlugin.get_supported_vegas_modes`, `BasePlugin.get_vegas_segment_width`, `PluginTestCase.setUp`
|
|
||||||
|
|
||||||
## Still used — keep or migrate first (3)
|
|
||||||
|
|
||||||
`LogoDownloader.fetch_teams_data`, `LogoDownloader.extract_teams_from_data`, `LogoDownloader.download_missing_logos_for_league`
|
|
||||||
|
|
||||||
## Needs review (11)
|
|
||||||
|
|
||||||
`PluginManager.get_all_plugins`, `PluginManager.get_plugin_info`, `PluginManager.get_all_plugin_info`, `PluginManager.get_plugin_display_modes`, `PluginManager.find_plugin_for_mode`, `PluginStateManager.is_loaded`, `PluginStateManager.is_running`, `PluginStateManager.is_error`, `PluginStateManager.get_error_info`, `PluginStateManager.get_last_update`, `PluginStateManager.get_state_info`
|
|
||||||
|
|
||||||
## Every hit
|
|
||||||
|
|
||||||
File paths are relative to the plugin's directory (core: the repo root).
|
|
||||||
|
|
||||||
| Method | Where | File:line | Kind | Code |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_data_service.py:85 | test review | `result = service.get_result(req_id)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_data_service.py:107 | test review | `seen["filed"] = service.get_result(result.request_id) is result` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_data_service.py:148 | test review | `result = service.get_result(req_id)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_data_service.py:165 | test review | `result = service.get_result(req_id)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_data_service.py:254 | test review | `assert service.get_result("unknown") is None` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_data_service.py:391 | test review | `assert service.get_result(rid).cached is True` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_fetch_dedupe.py:198 | test review | `assert service.get_result(req).success is True` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:85 | test review | `result = service.get_result(req_id)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:117 | test review | `stored = service.get_result(req_id)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:144 | test review | `assert service.get_result(req_id).data == PAYLOAD` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:154 | test review | `stored = service.get_result(req_id)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:171 | test review | `assert service.get_result(req_id).data is None` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:193 | test review | `assert service.get_result(req_id).data == PAYLOAD` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_background_payload_release.py:278 | test review | `stored = service.get_result(first)` |
|
|
||||||
| `BackgroundDataService.get_result` | core tests | test/test_fetch_service.py:703 | test review | `assert bds.get_result(request_id).success` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service.py:145 | test review | `while not service.is_request_complete(req_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service.py:162 | test review | `while not service.is_request_complete(req_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service.py:194 | test review | `while not service.is_request_complete(req_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service.py:211 | test review | `while not service.is_request_complete(req_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service.py:246 | test review | `assert service.is_request_complete("r2") is False` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service.py:251 | test review | `assert service.is_request_complete("r3") is True` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service_espn_ranges.py:91 | test review | `while not service.is_request_complete(request_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_data_service_espn_ranges.py:154 | test review | `while not service.is_request_complete(request_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_fetch_dedupe.py:55 | test review | `while not service.is_request_complete(req_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_background_payload_release.py:67 | test review | `while not service.is_request_complete(req_id) and time.time() < deadline:` |
|
|
||||||
| `BackgroundDataService.is_request_complete` | core tests | test/test_fetch_service.py:701 | test review | `while not bds.is_request_complete(request_id) and time.monotonic() < deadline:` |
|
|
||||||
| `BackgroundDataService.get_request_status` | core tests | test/test_background_data_service.py:223 | test review | `assert service.get_request_status("nonexistent") is None` |
|
|
||||||
| `BackgroundDataService.get_request_status` | core tests | test/test_background_fetch_dedupe.py:448 | test review | `assert service.get_request_status(rid) is FetchStatus.CANCELLED, (` |
|
|
||||||
| `BaseOddsManager.get_odds_for_games` | core tests | test/test_base_odds_manager.py:356 | test review | `result = manager.get_odds_for_games(games)` |
|
|
||||||
| `BaseOddsManager.get_odds_for_games` | core tests | test/test_base_odds_manager.py:374 | test review | `result = manager.get_odds_for_games(games)` |
|
|
||||||
| `BaseOddsManager.get_odds_for_games` | core tests | test/test_base_odds_manager.py:385 | test review | `result = manager.get_odds_for_games([game])` |
|
|
||||||
| `BaseOddsManager.format_odds_summary` | core tests | test/test_base_odds_manager.py:322 | test review | `result = manager.format_odds_summary({` |
|
|
||||||
| `BaseOddsManager.format_odds_summary` | core tests | test/test_base_odds_manager.py:329 | test review | `result = manager.format_odds_summary(FULL_EXTRACTED)` |
|
|
||||||
| `BaseOddsManager.format_odds_summary` | core tests | test/test_base_odds_manager.py:333 | test review | `assert manager.format_odds_summary(None) == 'No odds available'` |
|
|
||||||
| `BaseOddsManager.format_odds_summary` | core tests | test/test_base_odds_manager.py:336 | test review | `assert manager.format_odds_summary({}) == 'No odds available'` |
|
|
||||||
| `BaseOddsManager.format_odds_summary` | core tests | test/test_base_odds_manager.py:339 | test review | `assert manager.format_odds_summary(` |
|
|
||||||
| `CacheManager.load_cache` | core tests | test/conftest.py:219 | test review | `mock.load_cache = Mock(side_effect=mock_get)` |
|
|
||||||
| `CacheManager.generate_sport_cache_key` | core tests | test/test_background_data_service.py:42 | test review | `m.generate_sport_cache_key.return_value = "test_key"` |
|
|
||||||
| `CacheManager.generate_sport_cache_key` | core tests | test/test_background_data_service.py:356 | test call | `expected = CacheManager.generate_sport_cache_key(None, sport, date_str)` |
|
|
||||||
| `CacheManager.generate_sport_cache_key` | core tests | test/test_background_data_service.py:367 | test call | `theirs = cm_module.CacheManager.generate_sport_cache_key(None, "nba")` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | core tests | test/test_api_helper.py:214 | test review | `result = helper.fetch_espn_scoreboard('football', 'nfl')` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | core tests | test/test_api_helper.py:230 | test review | `helper.fetch_espn_scoreboard('basketball', 'nba', date='20250115')` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | core tests | test/test_api_helper.py:239 | test review | `helper.fetch_espn_scoreboard('basketball', 'nba', date='20250115', cache_key='mine')` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | core tests | test/test_api_helper.py:251 | test review | `assert helper.fetch_espn_scoreboard('basketball', 'nba', date='20250115') == {` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | core tests | test/test_espn_scoreboard_cache.py:199 | test call | `helper.fetch_espn_scoreboard("football", "nfl", date=self.DAY)` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | football-scoreboard | test_espn_date_ranges.py:119 | test review | `helper = sys.modules[sports.fetch_espn_scoreboard.__module__]` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | hockey-scoreboard | test_espn_date_ranges.py:106 | test review | `helper = sys.modules[sports.fetch_espn_scoreboard.__module__]` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | ufc-scoreboard | test_every_bout_is_its_own_fight.py:103 | test review | `_real_fetch = sports.fetch_espn_scoreboard` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | ufc-scoreboard | test_every_bout_is_its_own_fight.py:108 | test review | `sports.fetch_espn_scoreboard = lambda *a, **k: board(name)` |
|
|
||||||
| `APIHelper.fetch_espn_scoreboard` | ufc-scoreboard | test_every_bout_is_its_own_fight.py:113 | test review | `sports.fetch_espn_scoreboard = _real_fetch` |
|
|
||||||
| `APIHelper.fetch_espn_standings` | core tests | test/test_api_helper.py:258 | test review | `helper.fetch_espn_standings('football', 'nfl')` |
|
|
||||||
| `APIHelper.fetch_espn_rankings` | core tests | test/test_api_helper.py:269 | test review | `helper.fetch_espn_rankings('football', 'college-football')` |
|
|
||||||
| `APIHelper.set_cache` | core tests | test/test_api_helper.py:128 | test review | `helper.set_cache('k', {'a': 1}, ttl=42)` |
|
|
||||||
| `APIHelper.set_cache` | core tests | test/test_api_helper.py:350 | test call | `assert helper.set_cache('k', {'a': 1}) is None` |
|
|
||||||
| `APIHelper.get_cache` | core tests | test/test_api_helper.py:348 | test call | `assert helper.get_cache('k') is None` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:42 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:57 | test review | `helper.set_rate_limit(5)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:72 | test review | `helper.set_rate_limit(5)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:91 | test review | `helper.set_rate_limit(5)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:150 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:305 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:313 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:326 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:334 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_api_helper.py:346 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_espn_scoreboard_cache.py:197 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_fetch_service.py:726 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `APIHelper.set_rate_limit` | core tests | test/test_fetch_service.py:1143 | test call | `helper.set_rate_limit(0)` |
|
|
||||||
| `ConfigManager.rollback_config` | core | src/config_manager.py:191 | internal (in `ConfigManager.rollback_config`) | `success = atomic_mgr.rollback_config(backup_version)` |
|
|
||||||
| `ConfigManager.rollback_config` | core | src/config_manager_atomic.py:297 | unrelated | `def rollback_config(self, backup_version: Optional[str] = None) -> bool:` |
|
|
||||||
| `ConfigManager.rollback_config` | core tests | test/test_config_durable_writes.py:345 | test review | `assert manager.rollback_config()` |
|
|
||||||
| `ConfigManager.list_backups` | core | src/config_manager.py:212 | internal (in `ConfigManager.list_backups`) | `return atomic_mgr.list_backups()` |
|
|
||||||
| `ConfigManager.list_backups` | core | src/config_manager_atomic.py:334 | unrelated | `def list_backups(self) -> List[BackupInfo]:` |
|
|
||||||
| `ConfigManager.list_backups` | core | src/config_manager_atomic.py:309 | unrelated | `backups = self.list_backups()` |
|
|
||||||
| `ConfigManager.list_backups` | core tests | test/test_config_durable_writes.py:323 | test review | `assert [b.path for b in manager.list_backups()] == [` |
|
|
||||||
| `ConfigManager.validate_config_file` | core | src/config_manager.py:226 | internal (in `ConfigManager.validate_config_file`) | `return atomic_mgr.validate_config_file(config_path)` |
|
|
||||||
| `ConfigManager.validate_config_file` | core | src/config_manager_atomic.py:426 | unrelated | `def validate_config_file(self, config_path: Optional[str] = None) -> ValidationResult:` |
|
|
||||||
| `ConfigManager.get_secret` | core tests | test/conftest.py:246 | test review | `mock.get_secret = Mock(side_effect=mock_get_secret)` |
|
|
||||||
| `ConfigManager.get_secret` | core tests | test/test_config_manager.py:317 | test call | `assert manager.get_secret("api_key") == "secret123"` |
|
|
||||||
| `ConfigManager.get_secret` | core tests | test/test_config_manager.py:318 | test call | `assert manager.get_secret("token") == "token456"` |
|
|
||||||
| `ConfigManager.get_secret` | core tests | test/test_config_manager.py:319 | test call | `assert manager.get_secret("nonexistent") is None` |
|
|
||||||
| `ConfigManager.get_secret` | core tests | test/test_config_manager.py:325 | test call | `assert manager.get_secret("api_key") is None` |
|
|
||||||
| `ConfigManager.get_secret` | core tests | test/test_config_manager.py:337 | test call | `assert manager.get_secret("api_key") is None` |
|
|
||||||
| `ConfigManager.cleanup_orphaned_plugin_configs` | core tests | test/test_config_manager.py:447 | test call | `removed = manager.cleanup_orphaned_plugin_configs(["plugin1", "plugin2"])` |
|
|
||||||
| `ConfigManager.cleanup_orphaned_plugin_configs` | core tests | test/test_core_config_key_adopters.py:80 | test review | `removed = manager.cleanup_orphaned_plugin_configs(['installed'])` |
|
|
||||||
| `ConfigManager.cleanup_orphaned_plugin_configs` | core tests | test/test_web_auth.py:616 | test call | `config_manager.cleanup_orphaned_plugin_configs([])` |
|
|
||||||
| `ConfigManager.validate_all_plugin_configs` | core tests | test/test_core_config_key_adopters.py:91 | test review | `results = manager.validate_all_plugin_configs(schema_manager)` |
|
|
||||||
| `ConfigManager.validate_all_plugin_configs` | core tests | test/test_retired_plugin_config_keys.py:112 | test call | `results = config_manager.validate_all_plugin_configs(schema_manager)` |
|
|
||||||
| `FontManager.get_native_bdf_size` | core tests | test/test_font_manager.py:104 | test call | `assert fm.get_native_bdf_size("five_by_seven") == 7` |
|
|
||||||
| `FontManager.get_native_bdf_size` | core tests | test/test_font_manager.py:107 | test call | `assert fm.get_native_bdf_size("press_start") is None` |
|
|
||||||
| `FontManager.get_native_bdf_size` | core tests | test/test_font_manager.py:110 | test call | `assert fm.get_native_bdf_size("no-such-family") is None` |
|
|
||||||
| `FontManager.measure_text` | core tests | test/test_font_manager.py:116 | test call | `width, height, baseline = fm.measure_text("SCORE", font)` |
|
|
||||||
| `FontManager.measure_text` | core tests | test/test_font_manager.py:119 | test call | `assert fm.measure_text("SCORE", font) == (width, height, baseline)` |
|
|
||||||
| `FontManager.measure_text` | core tests | test/test_font_manager.py:124 | test call | `short, _, _ = fm.measure_text("AB", font)` |
|
|
||||||
| `FontManager.measure_text` | core tests | test/test_font_manager.py:125 | test call | `long, _, _ = fm.measure_text("ABCD", font)` |
|
|
||||||
| `FontManager.measure_text` | core tests | test/test_font_manager.py:132 | test call | `fm.measure_text("X", font)` |
|
|
||||||
| `LogoDownloader.fetch_teams_data` | core | src/logo_downloader.py:639 | internal (in `LogoDownloader.download_missing_logos_for_league`) | `data = self.fetch_teams_data(league)` |
|
|
||||||
| `LogoDownloader.fetch_teams_data` | core | src/logo_downloader.py:695 | internal (in `LogoDownloader.download_all_ncaa_football_logos`) | `data = self.fetch_teams_data(league)` |
|
|
||||||
| `LogoDownloader.extract_teams_from_data` | core | src/logo_downloader.py:645 | internal (in `LogoDownloader.download_missing_logos_for_league`) | `teams = self.extract_teams_from_data(data, league)` |
|
|
||||||
| `LogoDownloader.extract_teams_from_data` | core | src/logo_downloader.py:701 | internal (in `LogoDownloader.download_all_ncaa_football_logos`) | `teams = self.extract_teams_from_data(data, league)` |
|
|
||||||
| `LogoDownloader.download_missing_logos_for_league` | core | src/logo_downloader.py:786 | internal (in `LogoDownloader.download_all_missing_logos`) | `downloaded, failed = self.download_missing_logos_for_league(league, force_download)` |
|
|
||||||
| `LogoDownloader.download_missing_logos_for_league` | core | src/logo_downloader.py:1034 | call | `return downloader.download_missing_logos_for_league(league, force_download)` |
|
|
||||||
| `LogoDownloader.download_missing_logos_for_league` | core tests | test/test_logo_downloader.py:292 | test call | `downloader.download_missing_logos_for_league("nfl")` |
|
|
||||||
| `LogoDownloader.download_missing_logos_for_league` | core tests | test/test_logo_downloader.py:304 | test call | `downloader.download_missing_logos_for_league("nfl")` |
|
|
||||||
| `LogoDownloader.download_all_ncaa_football_logos` | core tests | test/test_logo_downloader.py:319 | test call | `downloader.download_all_ncaa_football_logos()` |
|
|
||||||
| `LogoDownloader.download_all_ncaa_football_logos` | core tests | test/test_logo_downloader.py:332 | test call | `downloader.download_all_ncaa_football_logos()` |
|
|
||||||
| `LogoDownloader.convert_image_to_rgba` | core | src/logo_downloader.py:897 | internal (in `LogoDownloader.convert_all_logos_to_rgba`) | `if self.convert_image_to_rgba(logo_file):` |
|
|
||||||
| `BasePlugin.get_supported_vegas_modes` | core tests | test/test_vegas_participation.py:356 | test review | `assert plugin.get_supported_vegas_modes() == [` |
|
|
||||||
| `BasePlugin.get_supported_vegas_modes` | core tests | test/test_vegas_participation.py:358 | test review | `assert plugin.get_supported_vegas_modes()` |
|
|
||||||
| `BasePlugin.get_vegas_segment_width` | core tests | test/test_vegas_participation.py:359 | test review | `assert plugin.get_vegas_segment_width() == 2` |
|
|
||||||
| `PluginManager.get_all_plugins` | core | src/plugin_system/testing/mocks.py:218 | unrelated | `def get_all_plugins(self) -> Dict[str, Any]:` |
|
|
||||||
| `PluginManager.get_all_plugins` | football-scoreboard | emulator_demo.py:68 | review | `mock_plugin_manager.get_all_plugins = Mock(return_value=[])` |
|
|
||||||
| `PluginManager.get_all_plugins` | football-scoreboard | test_dynamic_duration.py:64 | test review | `mock_plugin_manager.get_all_plugins = Mock(return_value=[])` |
|
|
||||||
| `PluginManager.get_all_plugins` | football-scoreboard | test_football_plugin.py:74 | test review | `mock_plugin_manager.get_all_plugins = Mock(return_value=[])` |
|
|
||||||
| `PluginManager.get_all_plugins` | hockey-scoreboard | test_hockey_emulator.py:99 | test review | `mock_plugin_manager.get_all_plugins = Mock(return_value=[])` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/plugin_catalog.py:120 | unrelated | `def get_plugin_info(self, plugin_id: str) -> Optional[Dict[str, Any]]:` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/plugin_catalog.py:133 | unrelated | `return [info for info in (self.get_plugin_info(pid) for pid in ids) if info]` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/plugin_manager.py:1001 | internal (in `PluginManager.get_all_plugin_info`) | `return [info for info in [self.get_plugin_info(pid) for pid in pids] if info]` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/store_install.py:208 | review | `plugin_info = self.get_plugin_info(plugin_id, fetch_latest_from_github=True, force_refresh=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/store_registry.py:801 | unrelated | `def get_plugin_info(self, plugin_id: str, fetch_latest_from_github: bool = True, force_refresh: bool = False) -> Optional[Dict]:` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/store_update.py:357 | review | `plugin_info_remote = self.get_plugin_info(plugin_id, fetch_latest_from_github=True, force_refresh=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/store_update.py:362 | review | `plugin_info_remote = self.get_plugin_info(alt_id, fetch_latest_from_github=True, force_refresh=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/store_update.py:685 | review | `plugin_info_remote = self.get_plugin_info(plugin_id, fetch_latest_from_github=True, force_refresh=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/store_update.py:691 | review | `plugin_info_remote = self.get_plugin_info(alt_id, fetch_latest_from_github=True, force_refresh=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | src/plugin_system/testing/mocks.py:223 | unrelated | `def get_plugin_info(self, plugin_id: str) -> Optional[Dict[str, Any]]:` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/api_v3/plugin_store.py:216 | review | `remote_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id, fetch_latest_from_github=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/api_v3/plugin_store.py:334 | review | `plugin_info = api_v3.plugin_store_manager.get_plugin_info(plugin_id)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/api_v3/plugin_store.py:545 | review | `elif not api_v3.plugin_store_manager.get_plugin_info(plugin_id):` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/api_v3/plugin_store.py:602 | review | `elif not api_v3.plugin_store_manager.get_plugin_info(plugin_id):` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/pages_v3.py:224 | review | `info = pages_v3.plugin_catalog.get_plugin_info(pid) or {}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/pages_v3.py:722 | review | `plugin_info = pages_v3.plugin_catalog.get_plugin_info(plugin_id)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core | web_interface/blueprints/pages_v3.py:727 | review | `plugin_info = pages_v3.plugin_catalog.get_plugin_info(plugin_id)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_api_v3_plugin_install_endpoints.py:111 | test review | `manager.get_plugin_info.return_value = None` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_api_v3_plugin_install_endpoints.py:119 | test review | `manager.get_plugin_info.return_value = {"id": "clock"}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_pages_v3_path_guards.py:47 | test call | `plugin_manager.get_plugin_info.return_value = {"name": "Weather", "version": "1.0.0"}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_registry_id_resolution.py:81 | test review | `_ids(store.get_plugin_info("ledmatrix-weather", fetch_latest_from_github=False))` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_store_manager_caches.py:612 | test review | `info = self.sm.get_plugin_info("foo", fetch_latest_from_github=True, force_refresh=True)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_store_non_plugin_entries.py:37 | test review | `store.get_plugin_info = MagicMock(return_value=dict(SKIN))` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_web_plugin_dir_resolution.py:86 | test review | `api.plugin_store_manager.get_plugin_info = MagicMock(return_value=None)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_web_plugin_dir_resolution.py:118 | test review | `api.plugin_store_manager.get_plugin_info = MagicMock(return_value=None)` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/test_web_plugin_dir_resolution.py:178 | test call | `plugin_manager.get_plugin_info.return_value = {"id": "weather", "name": "Weather"}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/web_interface/test_plugin_config_form_defaults.py:119 | test call | `pm.get_plugin_info.return_value = {"name": "Demo", "version": "1.0.0"}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/web_interface/test_plugin_config_schema_expansion.py:88 | test call | `pm.get_plugin_info.return_value = {"id": plugin_id, "name": plugin_id}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/web_interface/test_plugin_widget_route.py:226 | test call | `pm.get_plugin_info.return_value = {"id": plugin_id, "name": plugin_id}` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/web_interface/test_plugin_widget_route.py:228 | test call | `pm.get_plugin_info.return_value["version"] = version` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/web_interface/test_update_all_plugins.py:41 | test review | `sm.get_plugin_info.return_value = None` |
|
|
||||||
| `PluginManager.get_plugin_info` | core tests | test/web_interface/test_web_process_runs_no_plugin_code.py:132 | test review | `store.get_plugin_info.return_value = None` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core | src/plugin_system/plugin_catalog.py:129 | unrelated | `def get_all_plugin_info(self) -> List[Dict[str, Any]]:` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core | web_interface/blueprints/api_v3/plugins.py:68 | review | `all_plugin_info = api_v3.plugin_catalog.get_all_plugin_info()` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core | web_interface/blueprints/pages_v3.py:209 | review | `pi.get('id') for pi in pages_v3.plugin_catalog.get_all_plugin_info()` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core | web_interface/blueprints/pages_v3.py:559 | review | `infos = sorted(pages_v3.plugin_catalog.get_all_plugin_info(),` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_api_v3_installed_display_modes.py:30 | test review | `api.plugin_catalog.get_all_plugin_info = MagicMock(return_value=[info])` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_api_v3_installed_plugin_icon.py:24 | test review | `api.plugin_catalog.get_all_plugin_info = MagicMock(return_value=[info])` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_installed_list_registry_offline.py:71 | test review | `api.plugin_catalog.get_all_plugin_info = MagicMock(return_value=[` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_onboarding_checklist.py:68 | test review | `mock_pm.get_all_plugin_info.return_value = []` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_plugin_manager_load_failures.py:78 | test call | `infos = {i["id"]: i for i in pm.get_all_plugin_info()}` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_plugin_runtime_snapshot.py:422 | test review | `api.plugin_catalog.get_all_plugin_info = MagicMock(return_value=[` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_vegas_participation.py:447 | test review | `api.plugin_catalog.get_all_plugin_info = MagicMock(return_value=[info])` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_web_api.py:551 | test review | `mock_plugin_catalog.get_all_plugin_info.return_value = [` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_web_api.py:570 | test review | `mock_plugin_catalog.get_all_plugin_info.return_value = [` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_web_api.py:592 | test review | `mock_plugin_catalog.get_all_plugin_info.return_value = [` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_web_plugin_dir_resolution.py:61 | test review | `api.plugin_catalog.get_all_plugin_info = MagicMock(return_value=[info])` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_web_plugin_dir_resolution.py:195 | test call | `plugin_manager.get_all_plugin_info.assert_not_called()` |
|
|
||||||
| `PluginManager.get_all_plugin_info` | core tests | test/test_web_smoke.py:95 | test review | `mock_pm.get_all_plugin_info.return_value = [` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core | src/plugin_system/plugin_catalog.py:175 | unrelated | `def get_plugin_display_modes(self, plugin_id: str) -> List[str]:` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core | web_interface/blueprints/api_v3/display.py:197 | review | `plugin_modes = api_v3.plugin_catalog.get_plugin_display_modes(plugin_id) or [plugin_id]` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core | web_interface/blueprints/api_v3/display.py:267 | review | `modes = api_v3.plugin_catalog.get_plugin_display_modes(resolved_plugin)` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core | web_interface/blueprints/api_v3/plugins.py:157 | review | `declared_modes = api_v3.plugin_catalog.get_plugin_display_modes(plugin_id)` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core | web_interface/blueprints/pages_v3.py:565 | review | `modes = pages_v3.plugin_catalog.get_plugin_display_modes(pid) or [pid]` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_api_v3_display_modes.py:40 | test call | `pm.get_plugin_display_modes = MagicMock(` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_api_v3_display_modes.py:100 | test call | `pm.get_plugin_display_modes = MagicMock(return_value=[])` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_api_v3_display_modes.py:122 | test call | `pm.get_plugin_display_modes = MagicMock(` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_api_v3_installed_display_modes.py:31 | test review | `api.plugin_catalog.get_plugin_display_modes = MagicMock(return_value=declared_modes)` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_api_v3_installed_display_modes.py:39 | test review | `api.plugin_catalog.get_plugin_display_modes.assert_any_call('football-scoreboard')` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_installed_list_registry_offline.py:75 | test review | `api.plugin_catalog.get_plugin_display_modes = MagicMock(return_value=[])` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_onboarding_checklist.py:69 | test review | `mock_pm.get_plugin_display_modes.side_effect = lambda pid: []` |
|
|
||||||
| `PluginManager.get_plugin_display_modes` | core tests | test/test_web_smoke.py:99 | test review | `mock_pm.get_plugin_display_modes.side_effect = (` |
|
|
||||||
| `PluginManager.find_plugin_for_mode` | core | src/plugin_system/plugin_catalog.py:186 | unrelated | `def find_plugin_for_mode(self, mode: str) -> Optional[str]:` |
|
|
||||||
| `PluginManager.find_plugin_for_mode` | core | web_interface/blueprints/api_v3/display.py:271 | review | `resolved_plugin = api_v3.plugin_catalog.find_plugin_for_mode(resolved_mode)` |
|
|
||||||
| `PluginManager.find_plugin_for_mode` | core | web_interface/blueprints/api_v3/display.py:276 | review | `resolved_plugin = api_v3.plugin_catalog.find_plugin_for_mode(resolved_mode)` |
|
|
||||||
| `PluginStateManager.is_loaded` | core | src/plugin_system/plugin_state.py:299 | internal (in `PluginStateManager.get_state_info`) | `'is_loaded': self.is_loaded(plugin_id),` |
|
|
||||||
| `PluginStateManager.is_running` | core | src/plugin_system/plugin_state.py:301 | internal (in `PluginStateManager.get_state_info`) | `'is_running': self.is_running(plugin_id),` |
|
|
||||||
| `PluginStateManager.is_error` | core | src/plugin_system/plugin_state.py:302 | internal (in `PluginStateManager.get_state_info`) | `'is_error': self.is_error(plugin_id),` |
|
|
||||||
| `PluginStateManager.get_error_info` | core | src/plugin_system/plugin_state.py:305 | internal (in `PluginStateManager.get_state_info`) | `'error_info': self.get_error_info(plugin_id),` |
|
|
||||||
| `PluginStateManager.get_error_info` | core tests | test/test_async_plugin_updates.py:236 | test call | `error = pm.state_manager.get_error_info(plugin_id)` |
|
|
||||||
| `PluginStateManager.get_error_info` | core tests | test/test_plugin_hang_containment.py:191 | test call | `error_info = pm.state_manager.get_error_info('hung')` |
|
|
||||||
| `PluginStateManager.get_error_info` | core tests | test/test_sports_sunset_matrix.py:212 | test call | `f"{manager.state_manager.get_error_info(plugin_id)}"` |
|
|
||||||
| `PluginStateManager.get_error_info` | core tests | test/test_sports_sunset_matrix.py:285 | test call | `info = manager.state_manager.get_error_info(plugin_id)` |
|
|
||||||
| `PluginStateManager.get_last_update` | core | src/plugin_system/plugin_state.py:304 | internal (in `PluginStateManager.get_state_info`) | `'last_update': self.get_last_update(plugin_id),` |
|
|
||||||
| `PluginStateManager.get_state_info` | core | src/plugin_system/plugin_manager.py:987 | internal (in `PluginManager.get_plugin_info`) | `info['state'] = self.state_manager.get_state_info(plugin_id)` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:39 | test review | `info = manager.get_state_info("clock")` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:51 | test review | `info = manager.get_state_info("clock")` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:62 | test review | `assert manager.get_state_info("clock")["state_history_count"] == 101` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:63 | test review | `assert manager.get_state_info("weather")["state_history_count"] == 1` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:74 | test review | `info = manager.get_state_info("clock")` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:95 | test review | `info = m.get_state_info("clock")` |
|
|
||||||
| `PluginStateManager.get_state_info` | core tests | test/test_plugin_state_transition_count.py:130 | test review | `info = manager.get_state_info("clock")` |
|
|
||||||
|
|
||||||
## Sources scanned
|
|
||||||
|
|
||||||
| Source | Group | Python files | Hits |
|
|
||||||
|---|---|---|---|
|
|
||||||
| core | core | 188 | 50 |
|
|
||||||
| core tests | core-tests | 416 | 176 |
|
|
||||||
| 7-segment-clock | monorepo | 3 | 0 |
|
|
||||||
| afl-scoreboard | monorepo | 36 | 0 |
|
|
||||||
| baseball-scoreboard | monorepo | 71 | 0 |
|
|
||||||
| basketball-scoreboard | monorepo | 51 | 2 |
|
|
||||||
| birdnet-go | monorepo | 3 | 0 |
|
|
||||||
| blackjack | monorepo | 7 | 0 |
|
|
||||||
| calendar | monorepo | 5 | 0 |
|
|
||||||
| christmas-countdown | monorepo | 3 | 0 |
|
|
||||||
| clock-simple | monorepo | 2 | 0 |
|
|
||||||
| countdown | monorepo | 5 | 0 |
|
|
||||||
| cricket-scoreboard | monorepo | 8 | 1 |
|
|
||||||
| f1-scoreboard | monorepo | 15 | 0 |
|
|
||||||
| fantasy-blitz | monorepo | 13 | 0 |
|
|
||||||
| football-scoreboard | monorepo | 78 | 4 |
|
|
||||||
| geochron | monorepo | 10 | 0 |
|
|
||||||
| hello-world | monorepo | 2 | 0 |
|
|
||||||
| hockey-scoreboard | monorepo | 57 | 3 |
|
|
||||||
| incoming-packages | monorepo | 8 | 0 |
|
|
||||||
| jellyfin-now-playing | monorepo | 4 | 0 |
|
|
||||||
| lacrosse-scoreboard | monorepo | 41 | 0 |
|
|
||||||
| ledmatrix-elections | monorepo | 12 | 0 |
|
|
||||||
| ledmatrix-flights | monorepo | 48 | 0 |
|
|
||||||
| ledmatrix-leaderboard | monorepo | 10 | 0 |
|
|
||||||
| ledmatrix-music | monorepo | 12 | 0 |
|
|
||||||
| ledmatrix-stocks | monorepo | 7 | 0 |
|
|
||||||
| ledmatrix-weather | monorepo | 15 | 0 |
|
|
||||||
| march-madness | monorepo | 4 | 0 |
|
|
||||||
| masters-tournament | monorepo | 10 | 0 |
|
|
||||||
| mqtt-notifications | monorepo | 4 | 0 |
|
|
||||||
| news | monorepo | 7 | 0 |
|
|
||||||
| nfl-draft | monorepo | 3 | 0 |
|
|
||||||
| nfl-stat-leaders | monorepo | 8 | 0 |
|
|
||||||
| nrl-scoreboard | monorepo | 31 | 1 |
|
|
||||||
| odds-ticker | monorepo | 10 | 0 |
|
|
||||||
| of-the-day | monorepo | 14 | 0 |
|
|
||||||
| olympics | monorepo | 16 | 0 |
|
|
||||||
| on-air | monorepo | 3 | 0 |
|
|
||||||
| pomodoro-timer | monorepo | 3 | 0 |
|
|
||||||
| soccer-scoreboard | monorepo | 50 | 0 |
|
|
||||||
| static-image | monorepo | 4 | 0 |
|
|
||||||
| stock-news | monorepo | 4 | 0 |
|
|
||||||
| text-display | monorepo | 4 | 0 |
|
|
||||||
| tide-display | monorepo | 3 | 0 |
|
|
||||||
| ufc-scoreboard | monorepo | 40 | 3 |
|
|
||||||
| web-ui-info | monorepo | 2 | 0 |
|
|
||||||
| youtube-stats | monorepo | 5 | 0 |
|
|
||||||
| f1-live | third-party | 10 | 0 |
|
|
||||||
| gif-player | third-party | 1 | 0 |
|
|
||||||
| pga-tour-leaderboard | third-party | 2 | 0 |
|
|
||||||
| plex-marquee | third-party | 1 | 0 |
|
|
||||||
| ledmatrix-dresden-departures | third-party | 1 | 0 |
|
|
||||||
| tidbyt-baseball-scoreboard | third-party | 2 | 0 |
|
|
||||||
| sleeper-fantasy | third-party | 1 | 0 |
|
|
||||||
| ledmatrix-nascar | third-party | 1 | 0 |
|
|
||||||
|
|
||||||
## How to re-run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clones the monorepo and each third-party plugin (depth 1) into a temp cache:
|
|
||||||
python3 scripts/plugin_api_usage.py --output docs/DEPRECATIONS_3.8.md
|
|
||||||
# Or scan a local monorepo checkout (read only) instead of cloning it:
|
|
||||||
python3 scripts/plugin_api_usage.py --monorepo ../ledmatrix-plugins
|
|
||||||
```
|
|
||||||
|
|
||||||
Before removing a method in its release, re-run the scan against the current monorepo and registry: a plugin added since this file was generated may have started calling it. Remove only methods the fresh scan reports unused; move the rest to a later release (the test in `test/test_deprecation.py` fails while a marker names a release at or below `src.__version__`).
|
|
||||||
@@ -31,7 +31,7 @@ POST /api/v3/system/action
|
|||||||
|
|
||||||
**Base URL**: `http://your-pi-ip:5000/api/v3`
|
**Base URL**: `http://your-pi-ip:5000/api/v3`
|
||||||
|
|
||||||
See [REST_API_REFERENCE.md](REST_API_REFERENCE.md) for complete documentation.
|
See [API_REFERENCE.md](API_REFERENCE.md) for complete documentation.
|
||||||
|
|
||||||
## Display Manager Quick Methods
|
## Display Manager Quick Methods
|
||||||
|
|
||||||
@@ -48,14 +48,8 @@ display_manager.draw_text("Centered", centered=True) # Auto-center
|
|||||||
width = display_manager.get_text_width("Text", font)
|
width = display_manager.get_text_width("Text", font)
|
||||||
height = display_manager.get_font_height(font)
|
height = display_manager.get_font_height(font)
|
||||||
|
|
||||||
# Adaptive layout (recommended for multi-size support — text and images
|
# Weather icons
|
||||||
# that scale to any panel; see docs/ADAPTIVE_LAYOUT.md)
|
display_manager.draw_weather_icon("rain", x=10, y=10, size=16)
|
||||||
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: draw_weather_icon() was removed in 3.8.0 — draw your
|
|
||||||
# own icons (the weather plugin ships WeatherIcons)
|
|
||||||
|
|
||||||
# Scrolling state
|
# Scrolling state
|
||||||
display_manager.set_scrolling_state(True)
|
display_manager.set_scrolling_state(True)
|
||||||
@@ -68,27 +62,24 @@ display_manager.defer_update(lambda: self.update_cache(), priority=0)
|
|||||||
# Basic caching
|
# Basic caching
|
||||||
cached = cache_manager.get("key", max_age=3600)
|
cached = cache_manager.get("key", max_age=3600)
|
||||||
cache_manager.set("key", data)
|
cache_manager.set("key", data)
|
||||||
cache_manager.delete("key") # alias for clear_cache(key)
|
cache_manager.delete("key")
|
||||||
|
|
||||||
# Advanced caching
|
# Advanced caching
|
||||||
data = cache_manager.get_cached_data_with_strategy("key", data_type="weather")
|
data = cache_manager.get_cached_data_with_strategy("key", data_type="weather")
|
||||||
|
data = cache_manager.get_background_cached_data("key", sport_key="nhl")
|
||||||
|
|
||||||
# Strategy
|
# Strategy
|
||||||
strategy = cache_manager.get_cache_strategy("weather")
|
strategy = cache_manager.get_cache_strategy("weather")
|
||||||
|
interval = cache_manager.get_sport_live_interval("nhl")
|
||||||
```
|
```
|
||||||
|
|
||||||
`get_background_cached_data()` (use `get()`) and `get_sport_live_interval()`
|
|
||||||
were removed in 3.8.0. See
|
|
||||||
[Deprecated APIs](PLUGIN_API_REFERENCE.md#deprecated-apis).
|
|
||||||
|
|
||||||
## Plugin Manager Quick Methods
|
## Plugin Manager Quick Methods
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Get plugins
|
# Get plugins
|
||||||
plugin = plugin_manager.get_plugin("plugin-id")
|
plugin = plugin_manager.get_plugin("plugin-id")
|
||||||
all_plugins = plugin_manager.get_all_plugins()
|
all_plugins = plugin_manager.get_all_plugins()
|
||||||
# get_enabled_plugins() was removed in 3.8.0 — check `enabled` on the
|
enabled = plugin_manager.get_enabled_plugins()
|
||||||
# entries in plugin_manager.plugins
|
|
||||||
|
|
||||||
# Get info
|
# Get info
|
||||||
info = plugin_manager.get_plugin_info("plugin-id")
|
info = plugin_manager.get_plugin_info("plugin-id")
|
||||||
@@ -171,7 +162,7 @@ def display(self, force_clear=False):
|
|||||||
|
|
||||||
- [ ] Plugin inherits from `BasePlugin`
|
- [ ] Plugin inherits from `BasePlugin`
|
||||||
- [ ] Implements `update()` and `display()` methods
|
- [ ] Implements `update()` and `display()` methods
|
||||||
- [ ] `manifest.json` with the [required fields](PLUGIN_API_REFERENCE.md#manifest-required-fields)
|
- [ ] `manifest.json` with required fields
|
||||||
- [ ] `config_schema.json` for web UI (recommended)
|
- [ ] `config_schema.json` for web UI (recommended)
|
||||||
- [ ] `README.md` with documentation
|
- [ ] `README.md` with documentation
|
||||||
- [ ] Error handling implemented
|
- [ ] Error handling implemented
|
||||||
@@ -193,13 +184,12 @@ def display(self, force_clear=False):
|
|||||||
|
|
||||||
```
|
```
|
||||||
LEDMatrix/
|
LEDMatrix/
|
||||||
├── plugin-repos/ # Installed plugins (default; plugins/ is only
|
├── plugins/ # Installed plugins
|
||||||
│ # for dev symlinks via scripts/dev/dev_plugin_setup.sh)
|
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── config.json # Main configuration
|
│ ├── config.json # Main configuration
|
||||||
│ └── config_secrets.json # API keys and secrets
|
│ └── config_secrets.json # API keys and secrets
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
│ ├── REST_API_REFERENCE.md
|
│ ├── API_REFERENCE.md
|
||||||
│ ├── PLUGIN_API_REFERENCE.md
|
│ ├── PLUGIN_API_REFERENCE.md
|
||||||
│ └── ...
|
│ └── ...
|
||||||
└── src/
|
└── src/
|
||||||
@@ -211,7 +201,7 @@ LEDMatrix/
|
|||||||
|
|
||||||
## Quick Links
|
## Quick Links
|
||||||
|
|
||||||
- [Complete REST API Reference](REST_API_REFERENCE.md)
|
- [Complete API Reference](API_REFERENCE.md)
|
||||||
- [Plugin API Reference](PLUGIN_API_REFERENCE.md)
|
- [Plugin API Reference](PLUGIN_API_REFERENCE.md)
|
||||||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md)
|
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md)
|
||||||
- [Advanced Patterns](ADVANCED_PLUGIN_DEVELOPMENT.md)
|
- [Advanced Patterns](ADVANCED_PLUGIN_DEVELOPMENT.md)
|
||||||
|
|||||||
@@ -43,21 +43,16 @@ git submodule update --init --recursive rpi-rgb-led-matrix-master
|
|||||||
|
|
||||||
#### Building the Submodule
|
#### Building the Submodule
|
||||||
|
|
||||||
After initializing the submodule, build and install the `rgbmatrix` Python
|
After initializing the submodule, you need to build the Python bindings:
|
||||||
package from the submodule root. Upstream's `pyproject.toml` builds it with
|
|
||||||
scikit-build-core, CMake and Ninja; there is no separate `make` step:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd rpi-rgb-led-matrix-master
|
cd rpi-rgb-led-matrix-master
|
||||||
|
make build-python
|
||||||
|
cd bindings/python
|
||||||
python3 -m pip install --break-system-packages .
|
python3 -m pip install --break-system-packages .
|
||||||
```
|
```
|
||||||
|
|
||||||
On a board with 1 GB of RAM or less, cap the compile so it doesn't run out of
|
**Note:** The `first_time_install.sh` script automates this process during installation.
|
||||||
memory: `CMAKE_BUILD_PARALLEL_LEVEL=1 python3 -m pip install --break-system-packages .`
|
|
||||||
|
|
||||||
**Note:** The `first_time_install.sh` script automates this process during
|
|
||||||
installation, including the parallelism cap and a temporary swapfile on
|
|
||||||
low-memory boards.
|
|
||||||
|
|
||||||
#### Troubleshooting
|
#### Troubleshooting
|
||||||
|
|
||||||
@@ -74,7 +69,7 @@ git submodule update --init --recursive rpi-rgb-led-matrix-master
|
|||||||
**Build fails:**
|
**Build fails:**
|
||||||
Ensure you have the required build dependencies installed:
|
Ensure you have the required build dependencies installed:
|
||||||
```bash
|
```bash
|
||||||
sudo apt install -y build-essential python-dev-is-python3 cmake ninja-build
|
sudo apt install -y build-essential python3-dev cython3 scons
|
||||||
```
|
```
|
||||||
|
|
||||||
**Import error for `rgbmatrix` module:**
|
**Import error for `rgbmatrix` module:**
|
||||||
@@ -102,6 +97,8 @@ When setting up CI/CD pipelines, ensure submodules are initialized before buildi
|
|||||||
- name: Build rpi-rgb-led-matrix
|
- name: Build rpi-rgb-led-matrix
|
||||||
run: |
|
run: |
|
||||||
cd rpi-rgb-led-matrix-master
|
cd rpi-rgb-led-matrix-master
|
||||||
|
make build-python
|
||||||
|
cd bindings/python
|
||||||
pip install .
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,6 +110,8 @@ variables:
|
|||||||
build:
|
build:
|
||||||
script:
|
script:
|
||||||
- cd rpi-rgb-led-matrix-master
|
- cd rpi-rgb-led-matrix-master
|
||||||
|
- make build-python
|
||||||
|
- cd bindings/python
|
||||||
- pip install .
|
- pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ 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.
|
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
|
### Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ The LEDMatrix emulator allows you to run and test LEDMatrix displays on your com
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### System Requirements
|
### System Requirements
|
||||||
- Python 3.11 or higher (3.11 and 3.13 are tested)
|
- Python 3.7 or higher
|
||||||
- Windows, macOS, or Linux
|
- Windows, macOS, or Linux
|
||||||
- At least 2GB RAM (4GB recommended)
|
- At least 2GB RAM (4GB recommended)
|
||||||
- Internet connection for plugin downloads
|
- Internet connection for plugin downloads
|
||||||
|
|
||||||
### Required Software
|
### Required Software
|
||||||
- Python 3.11+
|
- Python 3.7+
|
||||||
- pip (Python package manager)
|
- pip (Python package manager)
|
||||||
- Git (for plugin management)
|
- Git (for plugin management)
|
||||||
|
|
||||||
@@ -50,7 +50,8 @@ pip install -r requirements-emulator.txt
|
|||||||
```
|
```
|
||||||
|
|
||||||
This installs:
|
This installs:
|
||||||
- `RGBMatrixEmulator` - the emulation library (and whatever it depends on)
|
- `RGBMatrixEmulator` - The core emulation library
|
||||||
|
- Additional dependencies for display adapters
|
||||||
|
|
||||||
### 3. Install Standard Dependencies
|
### 3. Install Standard Dependencies
|
||||||
|
|
||||||
@@ -62,31 +63,29 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
### 1. Emulator Configuration File
|
### 1. Emulator Configuration File
|
||||||
|
|
||||||
The emulator uses `emulator_config.json` for configuration. It isn't in
|
The emulator uses `emulator_config.json` for configuration. Here's the
|
||||||
the repo (it's gitignored): RGBMatrixEmulator writes it on first run.
|
default configuration as it ships in the repo:
|
||||||
A typical file looks like this:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"pixel_outline": 0,
|
"pixel_outline": 0,
|
||||||
"pixel_size": 16,
|
"pixel_size": 5,
|
||||||
"pixel_style": "square",
|
"pixel_style": "square",
|
||||||
"pixel_glow": 6,
|
"pixel_glow": 6,
|
||||||
"display_adapter": "browser",
|
"display_adapter": "pygame",
|
||||||
"allow_adapter_fallback": true,
|
|
||||||
"icon_path": null,
|
"icon_path": null,
|
||||||
"emulator_title": null,
|
"emulator_title": null,
|
||||||
"suppress_font_warnings": false,
|
"suppress_font_warnings": false,
|
||||||
|
"suppress_adapter_load_errors": false,
|
||||||
"browser": {
|
"browser": {
|
||||||
"_comment": "For use with the browser adapter only.",
|
"_comment": "For use with the browser adapter only.",
|
||||||
"port": 8888,
|
"port": 8888,
|
||||||
"target_fps": 60,
|
"target_fps": 24,
|
||||||
"fps_display": false,
|
"fps_display": false,
|
||||||
"quality": 70,
|
"quality": 70,
|
||||||
"image_border": true,
|
"image_border": true,
|
||||||
"debug_text": false,
|
"debug_text": false,
|
||||||
"image_format": "JPEG",
|
"image_format": "JPEG"
|
||||||
"open_immediately": false
|
|
||||||
},
|
},
|
||||||
"log_level": "info"
|
"log_level": "info"
|
||||||
}
|
}
|
||||||
@@ -97,13 +96,13 @@ A typical file looks like this:
|
|||||||
| Option | Description | Default | Values |
|
| Option | Description | Default | Values |
|
||||||
|--------|-------------|---------|--------|
|
|--------|-------------|---------|--------|
|
||||||
| `pixel_outline` | Pixel border thickness | 0 | 0-5 |
|
| `pixel_outline` | Pixel border thickness | 0 | 0-5 |
|
||||||
| `pixel_size` | Size of each pixel | 16 | 1-64 (8–16 is typical for testing) |
|
| `pixel_size` | Size of each pixel | 5 | 1-64 (8–16 is typical for testing) |
|
||||||
| `pixel_style` | Pixel shape | "square" | "square", "circle" |
|
| `pixel_style` | Pixel shape | "square" | "square", "circle" |
|
||||||
| `pixel_glow` | Glow effect intensity | 6 | 0-20 |
|
| `pixel_glow` | Glow effect intensity | 6 | 0-20 |
|
||||||
| `display_adapter` | Display backend | "browser" | "browser", "pygame" |
|
| `display_adapter` | Display backend | "pygame" | "pygame", "browser" |
|
||||||
| `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 |
|
| `emulator_title` | Window title | null | Any string |
|
||||||
| `suppress_font_warnings` | Hide font warnings | false | true/false |
|
| `suppress_font_warnings` | Hide font warnings | false | true/false |
|
||||||
|
| `suppress_adapter_load_errors` | Hide adapter errors | false | true/false |
|
||||||
|
|
||||||
### 3. Browser Adapter Configuration
|
### 3. Browser Adapter Configuration
|
||||||
|
|
||||||
@@ -112,32 +111,18 @@ When using the browser adapter, additional options are available:
|
|||||||
| Option | Description | Default |
|
| Option | Description | Default |
|
||||||
|--------|-------------|---------|
|
|--------|-------------|---------|
|
||||||
| `port` | Web server port | 8888 |
|
| `port` | Web server port | 8888 |
|
||||||
| `target_fps` | Target frames per second | 60 |
|
| `target_fps` | Target frames per second | 24 |
|
||||||
| `fps_display` | Show FPS counter | false |
|
| `fps_display` | Show FPS counter | false |
|
||||||
| `quality` | Image compression quality | 70 |
|
| `quality` | Image compression quality | 70 |
|
||||||
| `image_border` | Show image border | true |
|
| `image_border` | Show image border | true |
|
||||||
| `debug_text` | Show debug information | false |
|
| `debug_text` | Show debug information | false |
|
||||||
| `image_format` | Image format | "JPEG" |
|
| `image_format` | Image format | "JPEG" |
|
||||||
| `open_immediately` | Open the browser page automatically on start | false |
|
|
||||||
|
|
||||||
## Running the Emulator
|
## Running the Emulator
|
||||||
|
|
||||||
### 1. Use the `-e` Flag (Recommended)
|
### 1. Set Environment Variable
|
||||||
|
|
||||||
`run.py` accepts exactly two flags: `-e`/`--emulator` and
|
Enable emulator mode by setting the `EMULATOR` environment variable:
|
||||||
`-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):**
|
**Windows (Command Prompt):**
|
||||||
```cmd
|
```cmd
|
||||||
@@ -152,6 +137,15 @@ python run.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Linux/macOS:**
|
**Linux/macOS:**
|
||||||
|
```bash
|
||||||
|
export EMULATOR=true
|
||||||
|
python3 run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Alternative: Direct Python Execution
|
||||||
|
|
||||||
|
You can also run the emulator directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
EMULATOR=true python3 run.py
|
EMULATOR=true python3 run.py
|
||||||
```
|
```
|
||||||
@@ -159,8 +153,7 @@ EMULATOR=true python3 run.py
|
|||||||
### 3. Verify Emulator Mode
|
### 3. Verify Emulator Mode
|
||||||
|
|
||||||
When running in emulator mode, you should see:
|
When running in emulator mode, you should see:
|
||||||
- The emulated matrix — a web page at `http://localhost:8888` with the
|
- A window displaying the LED matrix simulation
|
||||||
default browser adapter, or a desktop window with the pygame adapter
|
|
||||||
- Console output indicating emulator mode
|
- Console output indicating emulator mode
|
||||||
- No hardware initialization errors
|
- No hardware initialization errors
|
||||||
|
|
||||||
@@ -168,36 +161,7 @@ When running in emulator mode, you should see:
|
|||||||
|
|
||||||
LEDMatrix supports two display adapters for the emulator:
|
LEDMatrix supports two display adapters for the emulator:
|
||||||
|
|
||||||
### 1. Browser Adapter (Default)
|
### 1. Pygame 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.
|
The pygame adapter provides a native desktop window with real-time display.
|
||||||
|
|
||||||
@@ -222,6 +186,33 @@ The pygame adapter provides a native desktop window with real-time display.
|
|||||||
- `+/-` - Zoom in/out
|
- `+/-` - Zoom in/out
|
||||||
- `R` - Reset zoom
|
- `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
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
@@ -283,7 +274,8 @@ Enable debug logging:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"log_level": "debug",
|
"log_level": "debug",
|
||||||
"suppress_font_warnings": false
|
"suppress_font_warnings": false,
|
||||||
|
"suppress_adapter_load_errors": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -307,18 +299,17 @@ Modify the display dimensions in your main config:
|
|||||||
|
|
||||||
### 2. Plugin Development
|
### 2. Plugin Development
|
||||||
|
|
||||||
`run.py` always runs the full rotation — it has no single-plugin flag.
|
For plugin development with the emulator:
|
||||||
To preview or check one plugin in isolation, use the dev tools:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run the full display in emulator mode (optionally with debug logging)
|
# Enable emulator mode
|
||||||
python3 run.py -e -d
|
export EMULATOR=true
|
||||||
|
|
||||||
# Live single-plugin preview in the browser (port 5001)
|
# Run with specific plugin
|
||||||
python3 scripts/dev_server.py
|
python run.py --plugin my-plugin
|
||||||
|
|
||||||
# Headless render/validation of one plugin
|
# Debug mode
|
||||||
python3 scripts/check_plugin.py --plugin my-plugin
|
python run.py --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Performance Tuning
|
### 3. Performance Tuning
|
||||||
@@ -353,10 +344,11 @@ The emulator can work alongside the web interface:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Terminal 1: Start emulator
|
# Terminal 1: Start emulator
|
||||||
python3 run.py -e
|
export EMULATOR=true
|
||||||
|
python run.py
|
||||||
|
|
||||||
# Terminal 2: Start web interface (supported entry point)
|
# Terminal 2: Start web interface
|
||||||
python3 web_interface/start.py
|
python web_interface/app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Access the web interface at `http://localhost:5000` while the emulator runs.
|
Access the web interface at `http://localhost:5000` while the emulator runs.
|
||||||
@@ -373,14 +365,13 @@ Access the web interface at `http://localhost:5000` while the emulator runs.
|
|||||||
### 2. Plugin Testing
|
### 2. Plugin Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Test a specific plugin (headless check)
|
# Test specific plugin
|
||||||
python3 scripts/check_plugin.py --plugin clock-simple
|
export EMULATOR=true
|
||||||
|
python run.py --plugin clock-simple
|
||||||
|
|
||||||
# Preview a single plugin live in the browser (port 5001)
|
# Test all plugins
|
||||||
python3 scripts/dev_server.py
|
export EMULATOR=true
|
||||||
|
python run.py --test-plugins
|
||||||
# Test the full rotation in the emulator
|
|
||||||
python3 run.py -e
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Configuration Management
|
### 3. Configuration Management
|
||||||
@@ -394,8 +385,9 @@ python3 run.py -e
|
|||||||
### Basic Clock Display
|
### Basic Clock Display
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start emulator with clock enabled in config.json
|
# Start emulator with clock
|
||||||
python3 run.py -e
|
export EMULATOR=true
|
||||||
|
python run.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sports Scores
|
### Sports Scores
|
||||||
@@ -403,16 +395,16 @@ python3 run.py -e
|
|||||||
```bash
|
```bash
|
||||||
# Configure for sports display
|
# Configure for sports display
|
||||||
# Edit config/config.json to enable sports plugins
|
# Edit config/config.json to enable sports plugins
|
||||||
python3 run.py -e
|
export EMULATOR=true
|
||||||
|
python run.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Text Display
|
### Custom Text Display
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Preview the text display plugin on its own
|
# Use text display plugin
|
||||||
python3 scripts/check_plugin.py --plugin text-display
|
export EMULATOR=true
|
||||||
# or use the live dev preview server
|
python run.py --plugin text-display --text "Hello World"
|
||||||
python3 scripts/dev_server.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|||||||
@@ -1,94 +1,146 @@
|
|||||||
# FontManager Usage Guide
|
# 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
|
## Overview
|
||||||
|
|
||||||
[`src/font_manager.py`](../src/font_manager.py) loads and caches the TTF and
|
The enhanced FontManager provides comprehensive font management for the LEDMatrix application with support for:
|
||||||
BDF fonts in `assets/fonts/`, registers fonts that plugins ship, and records
|
- Manager font registration and detection
|
||||||
which plugin uses which font so the web UI can show it.
|
- Plugin font management
|
||||||
|
- Manual font overrides via web interface
|
||||||
|
- Performance monitoring and caching
|
||||||
|
- Dynamic font discovery
|
||||||
|
|
||||||
Several methods were removed in LEDMatrix 3.8.0 after a release of
|
## Architecture
|
||||||
deprecation warnings; [Removed methods](#removed-methods) below lists them
|
|
||||||
with what to use instead.
|
|
||||||
|
|
||||||
## Getting the FontManager
|
### Manager-Centric Design
|
||||||
|
|
||||||
There is one shared FontManager per display process. The display controller
|
Managers define their own fonts, but the FontManager:
|
||||||
creates it and hands it to the `PluginManager`, so a plugin reaches it
|
1. **Loads and caches fonts** for performance
|
||||||
through its `plugin_manager`:
|
2. **Detects font usage** for visibility
|
||||||
|
3. **Allows manual overrides** when needed
|
||||||
|
4. **Supports plugin fonts** with namespacing
|
||||||
|
|
||||||
```python
|
### Font Resolution Flow
|
||||||
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)
|
Manager requests font → Check manual overrides → Apply manager choice → Cache & return
|
||||||
self.font_manager = self._get_font_manager()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`BasePlugin._get_font_manager()` returns `plugin_manager.font_manager`, or a
|
## For Manager Developers
|
||||||
standalone FontManager when none is available (test harnesses, mocks).
|
|
||||||
`DisplayManager` has **no** `font_manager` attribute —
|
|
||||||
`display_manager.font_manager` raises `AttributeError`.
|
|
||||||
|
|
||||||
## Resolving a font
|
### Basic Font Usage
|
||||||
|
|
||||||
```python
|
```python
|
||||||
element_key = f"{self.plugin_id}.title"
|
from src.font_manager import FontManager
|
||||||
|
|
||||||
# Register the choice so the web UI's Fonts tab can list it.
|
class MyManager:
|
||||||
self.font_manager.register_manager_font(
|
def __init__(self, config, display_manager, cache_manager):
|
||||||
manager_id=self.plugin_id,
|
self.font_manager = display_manager.font_manager # Access shared FontManager
|
||||||
element_key=element_key,
|
self.manager_id = "my_manager"
|
||||||
family="press_start",
|
|
||||||
size_px=10,
|
|
||||||
color=(255, 255, 255),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
# Define your font choices
|
||||||
|
element_key = "my_manager.title"
|
||||||
|
font_family = "press_start"
|
||||||
|
font_size_px = 10
|
||||||
|
color = (255, 255, 255) # RGB white
|
||||||
|
|
||||||
|
# Register your font choice (for detection and future overrides)
|
||||||
|
self.font_manager.register_manager_font(
|
||||||
|
manager_id=self.manager_id,
|
||||||
|
element_key=element_key,
|
||||||
|
family=font_family,
|
||||||
|
size_px=font_size_px,
|
||||||
|
color=color
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the font (checks for manual overrides automatically)
|
||||||
|
font = self.font_manager.resolve_font(
|
||||||
|
element_key=element_key,
|
||||||
|
family=font_family,
|
||||||
|
size_px=font_size_px
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use the font for rendering
|
||||||
|
self.display_manager.draw_text(
|
||||||
|
"Hello World",
|
||||||
|
x=10, y=10,
|
||||||
|
color=color,
|
||||||
|
font=font
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Font Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AdvancedManager:
|
||||||
|
def __init__(self, config, display_manager, cache_manager):
|
||||||
|
self.font_manager = display_manager.font_manager
|
||||||
|
self.manager_id = "advanced_manager"
|
||||||
|
|
||||||
|
# Define your font specifications
|
||||||
|
self.font_specs = {
|
||||||
|
"title": {"family": "press_start", "size_px": 12, "color": (255, 255, 0)},
|
||||||
|
"body": {"family": "four_by_six", "size_px": 8, "color": (255, 255, 255)},
|
||||||
|
"footer": {"family": "five_by_seven", "size_px": 7, "color": (128, 128, 128)}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register all font specs
|
||||||
|
for element_type, spec in self.font_specs.items():
|
||||||
|
element_key = f"{self.manager_id}.{element_type}"
|
||||||
|
self.font_manager.register_manager_font(
|
||||||
|
manager_id=self.manager_id,
|
||||||
|
element_key=element_key,
|
||||||
|
family=spec["family"],
|
||||||
|
size_px=spec["size_px"],
|
||||||
|
color=spec["color"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_font(self, element_type: str):
|
||||||
|
"""Helper method to get fonts with override support."""
|
||||||
|
spec = self.font_specs[element_type]
|
||||||
|
element_key = f"{self.manager_id}.{element_type}"
|
||||||
|
|
||||||
|
return self.font_manager.resolve_font(
|
||||||
|
element_key=element_key,
|
||||||
|
family=spec["family"],
|
||||||
|
size_px=spec["size_px"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
# Get fonts (automatically checks for overrides)
|
||||||
|
title_font = self.get_font("title")
|
||||||
|
body_font = self.get_font("body")
|
||||||
|
footer_font = self.get_font("footer")
|
||||||
|
|
||||||
|
# Render with fonts
|
||||||
|
self.display_manager.draw_text("Title", font=title_font, color=self.font_specs["title"]["color"])
|
||||||
|
self.display_manager.draw_text("Body Text", font=body_font, color=self.font_specs["body"]["color"])
|
||||||
|
self.display_manager.draw_text("Footer", font=footer_font, color=self.font_specs["footer"]["color"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Size Tokens
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get available size tokens
|
||||||
|
tokens = self.font_manager.get_size_tokens()
|
||||||
|
# Returns: {'xs': 6, 'sm': 8, 'md': 10, 'lg': 12, 'xl': 14, 'xxl': 16}
|
||||||
|
|
||||||
|
# Use token to get size
|
||||||
|
size_px = tokens.get('md', 10) # 10px
|
||||||
|
|
||||||
|
# Then use in font resolution
|
||||||
font = self.font_manager.resolve_font(
|
font = self.font_manager.resolve_font(
|
||||||
element_key=element_key,
|
element_key="my_manager.text",
|
||||||
family="press_start",
|
family="press_start",
|
||||||
size_px=10,
|
size_px=size_px
|
||||||
)
|
)
|
||||||
self.display_manager.draw_text("Hello", x=10, y=10, font=font)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`resolve_font()` applies any entry for `element_key` in
|
## For Plugin Developers
|
||||||
`config/font_overrides.json`, maps a plugin-local family to its namespaced
|
|
||||||
name when `plugin_id` is passed, and then calls `get_font(family, size_px)`.
|
|
||||||
On error it returns a fallback font rather than raising.
|
|
||||||
|
|
||||||
`get_font(family, size_px)` looks the family up in `font_catalog` and loads
|
### Plugin Font Registration
|
||||||
it (cached per family and size).
|
|
||||||
|
|
||||||
## Font families
|
In your plugin's `manifest.json`:
|
||||||
|
|
||||||
At start-up the FontManager scans `assets/fonts/` for `.ttf` and `.bdf`
|
|
||||||
files. Each becomes a family named after the file, lower-cased and without
|
|
||||||
the extension (`PressStart2P-Regular.ttf` → `pressstart2p-regular`). Four
|
|
||||||
aliases are added on top:
|
|
||||||
|
|
||||||
| Alias | File |
|
|
||||||
|---|---|
|
|
||||||
| `press_start` | `assets/fonts/PressStart2P-Regular.ttf` |
|
|
||||||
| `four_by_six` | `assets/fonts/4x6-font.ttf` |
|
|
||||||
| `five_by_seven` | `assets/fonts/5x7.bdf` |
|
|
||||||
| `tom_thumb` | `assets/fonts/tom-thumb.bdf` |
|
|
||||||
|
|
||||||
Read the catalog directly: `font_manager.font_catalog` is a dict of family
|
|
||||||
name to file path. Files added later are picked up on the next start of the
|
|
||||||
display service.
|
|
||||||
|
|
||||||
## Plugin fonts
|
|
||||||
|
|
||||||
Plugins that ship their own fonts declare them in a `"fonts"` block in
|
|
||||||
`manifest.json`. The plugin manager calls
|
|
||||||
`FontManager.register_plugin_fonts()` during plugin load. `plugin://…`
|
|
||||||
sources are resolved relative to the plugin's install directory.
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -99,126 +151,213 @@ sources are resolved relative to the plugin's install directory.
|
|||||||
{
|
{
|
||||||
"family": "custom_font",
|
"family": "custom_font",
|
||||||
"source": "plugin://fonts/custom.ttf",
|
"source": "plugin://fonts/custom.ttf",
|
||||||
"metadata": {"description": "Custom plugin font", "license": "MIT"}
|
"metadata": {
|
||||||
|
"description": "Custom plugin font",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"family": "web_font",
|
"family": "web_font",
|
||||||
"source": "https://example.com/fonts/font.ttf",
|
"source": "https://example.com/fonts/font.ttf",
|
||||||
"metadata": {"checksum": "sha256:abc123..."}
|
"metadata": {
|
||||||
|
"description": "Downloaded font",
|
||||||
|
"checksum": "sha256:abc123..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Registered families are namespaced as `<plugin_id>::<family>`. Pass
|
### Using Plugin Fonts
|
||||||
`plugin_id` to `resolve_font()` to use the short name:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
font = self.font_manager.resolve_font(
|
class PluginManager:
|
||||||
element_key=f"{self.plugin_id}.text",
|
def __init__(self, config, display_manager, cache_manager, plugin_id):
|
||||||
family="custom_font", # resolved as "my-plugin::custom_font"
|
self.font_manager = display_manager.font_manager
|
||||||
size_px=10,
|
self.plugin_id = plugin_id
|
||||||
plugin_id=self.plugin_id,
|
|
||||||
)
|
def display(self):
|
||||||
|
# Use plugin font (automatically namespaced)
|
||||||
|
font = self.font_manager.resolve_font(
|
||||||
|
element_key=f"{self.plugin_id}.text",
|
||||||
|
family="custom_font", # Will be resolved as "my-plugin::custom_font"
|
||||||
|
size_px=10,
|
||||||
|
plugin_id=self.plugin_id
|
||||||
|
)
|
||||||
|
|
||||||
|
self.display_manager.draw_text("Plugin Text", font=font)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Overrides
|
## Manual Font Overrides
|
||||||
|
|
||||||
`resolve_font()` still honours `config/font_overrides.json` (a map of
|
Users can override any font through the web interface:
|
||||||
element key to `family` and/or `size_px`), which is read once at start-up.
|
|
||||||
The methods that edited it — `set_override()`, `remove_override()`,
|
|
||||||
`get_overrides()` — were removed in 3.8.0, and there is no web UI or REST endpoint
|
|
||||||
for overrides (the override editor and `/api/v3/fonts/overrides` were
|
|
||||||
removed). To let users choose a font, add a field to your plugin's config
|
|
||||||
schema.
|
|
||||||
|
|
||||||
## Font usage in the web UI
|
1. Navigate to **Fonts** tab
|
||||||
|
2. View **Detected Manager Fonts** to see what's currently in use
|
||||||
|
3. In **Element Overrides** section:
|
||||||
|
- Select the element (e.g., "nfl.live.score")
|
||||||
|
- Choose a different font family
|
||||||
|
- Choose a different size
|
||||||
|
- Click **Add Override**
|
||||||
|
|
||||||
The web UI's **Fonts** tab lists, uploads, previews and deletes the font
|
Overrides are stored in `config/font_overrides.json` and persist across restarts.
|
||||||
files in `assets/fonts/`. The web interface runs in its own process and has
|
|
||||||
no FontManager, so the display service publishes which plugin uses which
|
|
||||||
font ([`src/font_usage.py`](../src/font_usage.py)), and the tab's **Used by**
|
|
||||||
column reads it:
|
|
||||||
|
|
||||||
- **Source**: `register_manager_font()` registrations of the loaded
|
### Programmatic Overrides
|
||||||
plugins. `get_font()` and `resolve_font()` do not know the calling plugin
|
|
||||||
and are not counted, and neither is a plugin that opens a font file
|
|
||||||
directly with PIL — register the fonts your plugin draws with if you want
|
|
||||||
them listed.
|
|
||||||
- **Names**: a family, alias or path is resolved through `font_catalog` to
|
|
||||||
the file it loads and reported under that file's name without extension
|
|
||||||
(`PressStart2P-Regular`, `4x6-font`, `5x7`, `tom-thumb`), which is how the
|
|
||||||
Fonts tab keys its rows. Fonts outside `assets/fonts/` (a plugin's own
|
|
||||||
`plugin_id::family` fonts) and families that resolve to nothing are left
|
|
||||||
out.
|
|
||||||
- **When**: a daemon thread started once plugins have loaded checks every
|
|
||||||
10 seconds and writes the `font_usage_snapshot` cache key only when the
|
|
||||||
usage changed (and once a day, so the cache's cleanup never expires it).
|
|
||||||
Unloading a plugin drops its registrations (`forget_manager_fonts`).
|
|
||||||
- **Unknown**: until the display service has published, the column reads
|
|
||||||
"unknown" and `GET /api/v3/fonts/catalog` returns `used_by: null`.
|
|
||||||
- The tab warns before deleting a font that a loaded plugin registered.
|
|
||||||
|
|
||||||
## Text measurement
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Set override
|
||||||
|
font_manager.set_override(
|
||||||
|
element_key="nfl.live.score",
|
||||||
|
family="four_by_six",
|
||||||
|
size_px=8
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove override
|
||||||
|
font_manager.remove_override("nfl.live.score")
|
||||||
|
|
||||||
|
# Get all overrides
|
||||||
|
overrides = font_manager.get_overrides()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Font Discovery
|
||||||
|
|
||||||
|
### Available Fonts
|
||||||
|
|
||||||
|
The FontManager automatically scans `assets/fonts/` for TTF and BDF fonts:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get all available fonts
|
||||||
|
fonts = font_manager.get_available_fonts()
|
||||||
|
# Returns: {'press_start': 'assets/fonts/PressStart2P-Regular.ttf', ...}
|
||||||
|
|
||||||
|
# Check if font exists
|
||||||
|
if "my_font" in fonts:
|
||||||
|
font = font_manager.get_font("my_font", 10)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Custom Fonts
|
||||||
|
|
||||||
|
Place font files in `assets/fonts/` directory:
|
||||||
|
- Supported formats: `.ttf`, `.bdf`
|
||||||
|
- Font family name is derived from filename (without extension)
|
||||||
|
- Will be automatically discovered on next initialization
|
||||||
|
|
||||||
|
## Performance Monitoring
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get performance stats
|
||||||
|
stats = font_manager.get_performance_stats()
|
||||||
|
|
||||||
|
print(f"Cache hit rate: {stats['cache_hit_rate']*100:.1f}%")
|
||||||
|
print(f"Total fonts cached: {stats['total_fonts_cached']}")
|
||||||
|
print(f"Failed loads: {stats['failed_loads']}")
|
||||||
|
print(f"Manager fonts: {stats['manager_fonts']}")
|
||||||
|
print(f"Plugin fonts: {stats['plugin_fonts']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text Measurement
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Measure text dimensions
|
||||||
width, height, baseline = font_manager.measure_text("Hello", font)
|
width, height, baseline = font_manager.measure_text("Hello", font)
|
||||||
|
|
||||||
|
# Get font height
|
||||||
font_height = font_manager.get_font_height(font)
|
font_height = font_manager.get_font_height(font)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tips
|
## Best Practices
|
||||||
|
|
||||||
- BDF fonts usually look better than TTF at small sizes on LED panels.
|
### For Managers
|
||||||
- Use `{plugin_id}.{element}` element keys.
|
|
||||||
- Register the fonts you draw with, so the Fonts tab can warn before one is
|
1. **Register all fonts** you use for visibility
|
||||||
deleted.
|
2. **Use consistent element keys** (e.g., `{manager_id}.{element_type}`)
|
||||||
- Replace direct `ImageFont.truetype("assets/fonts/...", 8)` calls with
|
3. **Cache font references** if using same font multiple times
|
||||||
`resolve_font()`: it caches, resolves paths against the install directory,
|
4. **Use `resolve_font()`** not `get_font()` directly to support overrides
|
||||||
and handles BDF files.
|
5. **Define sensible defaults** that work well on LED matrix
|
||||||
|
|
||||||
|
### For Plugins
|
||||||
|
|
||||||
|
1. **Use plugin-relative paths** (`plugin://fonts/...`)
|
||||||
|
2. **Include font metadata** (license, description)
|
||||||
|
3. **Provide fallback** fonts if custom fonts fail to load
|
||||||
|
4. **Test with different display sizes**
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
1. **BDF fonts** are often better for small sizes on LED matrices
|
||||||
|
2. **TTF fonts** work well for larger sizes
|
||||||
|
3. **Monospace fonts** are easier to align
|
||||||
|
4. **Test on actual hardware** - what looks good on screen may not work on LED matrix
|
||||||
|
|
||||||
|
## Migration from Old System
|
||||||
|
|
||||||
|
### Old Way (Direct Font Loading)
|
||||||
|
```python
|
||||||
|
self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Way (FontManager)
|
||||||
|
```python
|
||||||
|
element_key = f"{self.manager_id}.text"
|
||||||
|
self.font_manager.register_manager_font(
|
||||||
|
manager_id=self.manager_id,
|
||||||
|
element_key=element_key,
|
||||||
|
family="pressstart2p-regular",
|
||||||
|
size_px=8
|
||||||
|
)
|
||||||
|
self.font = self.font_manager.resolve_font(
|
||||||
|
element_key=element_key,
|
||||||
|
family="pressstart2p-regular",
|
||||||
|
size_px=8
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Font not found**
|
### Font Not Found
|
||||||
- Check the file exists in `assets/fonts/`.
|
- Check font file exists in `assets/fonts/`
|
||||||
- The family name is the filename without extension, lower-cased.
|
- Verify font family name matches filename (without extension, lowercase)
|
||||||
- Check the display service log for font discovery errors.
|
- Check logs for font discovery errors
|
||||||
|
|
||||||
**Plugin fonts not loading**
|
### Override Not Working
|
||||||
- Check the manifest's `"fonts"` block.
|
- Verify element key matches exactly what manager registered
|
||||||
- Check the log for download or registration errors, and that font URLs are
|
- Check `config/font_overrides.json` for correct syntax
|
||||||
reachable.
|
- Restart application to ensure overrides are loaded
|
||||||
|
|
||||||
## API reference
|
### Performance Issues
|
||||||
|
- Check cache hit rate in performance stats
|
||||||
|
- Reduce number of unique font/size combinations
|
||||||
|
- Clear cache if it grows too large: `font_manager.clear_cache()`
|
||||||
|
|
||||||
Current methods:
|
### Plugin Fonts Not Loading
|
||||||
|
- Verify plugin manifest syntax
|
||||||
|
- Check plugin directory structure
|
||||||
|
- Review logs for download/registration errors
|
||||||
|
- Ensure font URLs are accessible
|
||||||
|
|
||||||
| Method | Purpose |
|
## API Reference
|
||||||
|---|---|
|
|
||||||
| `register_manager_font(manager_id, element_key, family, size_px, color=None)` | Record a font choice (feeds the Fonts tab) |
|
|
||||||
| `forget_manager_fonts(manager_id)` | Drop a manager's registrations (core calls it when a plugin unloads) |
|
|
||||||
| `resolve_font(element_key, family, size_px, plugin_id=None)` | Get a font, applying overrides and plugin namespacing |
|
|
||||||
| `get_font(family, size_px)` | Get a font directly |
|
|
||||||
| `get_native_bdf_size(family)` | Native pixel size of a BDF family, or `None` |
|
|
||||||
| `measure_text(text, font)` | `(width, height, baseline)` |
|
|
||||||
| `get_font_height(font)` | Line height |
|
|
||||||
| `register_plugin_fonts(plugin_id, font_manifest)` | Register a plugin's fonts (core calls it at load) |
|
|
||||||
| `forget_plugin_fonts(plugin_id)` | Drop a plugin's manifest fonts and their cached objects (core calls it when a plugin unloads) |
|
|
||||||
| `clear_cache()` | Drop cached fonts and metrics |
|
|
||||||
| `font_catalog` (attribute) | Family name → file path |
|
|
||||||
|
|
||||||
### Removed methods
|
### FontManager Methods
|
||||||
|
|
||||||
Removed in 3.8.0, after logging a deprecation warning on first call since
|
- `register_manager_font(manager_id, element_key, family, size_px, color=None)` - Register font usage
|
||||||
3.5.0.
|
- `resolve_font(element_key, family, size_px, plugin_id=None)` - Get font with override support
|
||||||
|
- `get_font(family, size_px)` - Get font directly (bypasses overrides)
|
||||||
|
- `measure_text(text, font)` - Measure text dimensions
|
||||||
|
- `get_font_height(font)` - Get font height
|
||||||
|
- `set_override(element_key, family=None, size_px=None)` - Set manual override
|
||||||
|
- `remove_override(element_key)` - Remove override
|
||||||
|
- `get_overrides()` - Get all overrides
|
||||||
|
- `get_detected_fonts()` - Get all detected font usage
|
||||||
|
- `get_manager_fonts(manager_id=None)` - Get fonts by manager
|
||||||
|
- `get_available_fonts()` - Get font catalog
|
||||||
|
- `get_size_tokens()` - Get size token definitions
|
||||||
|
- `get_performance_stats()` - Get performance metrics
|
||||||
|
- `clear_cache()` - Clear font cache
|
||||||
|
- `register_plugin_fonts(plugin_id, font_manifest)` - Register plugin fonts
|
||||||
|
- `unregister_plugin_fonts(plugin_id)` - Unregister plugin fonts
|
||||||
|
|
||||||
|
## Example: Complete Manager Implementation
|
||||||
|
|
||||||
|
See `test/font_manager_example.py` for a complete working example.
|
||||||
|
|
||||||
| Method | Use instead |
|
|
||||||
|---|---|
|
|
||||||
| `get_available_fonts()`, `get_font_catalog()` | read `font_catalog` |
|
|
||||||
| `get_size_tokens()` | pass a pixel size |
|
|
||||||
| `get_performance_stats()` | — |
|
|
||||||
| `set_override()`, `remove_override()`, `get_overrides()` | a font field in your plugin's config schema |
|
|
||||||
| `get_manager_fonts()`, `get_detected_fonts()` | — |
|
|
||||||
| `get_plugin_fonts()` | — |
|
|
||||||
| `unregister_plugin_fonts()` | `forget_plugin_fonts()` (core calls it on unload) |
|
|
||||||
| `add_font()`, `remove_font()`, `validate_font()` | the web UI's Fonts tab |
|
|
||||||
|
|||||||
@@ -15,54 +15,24 @@ This guide will help you set up your LEDMatrix display for the first time and ge
|
|||||||
- Power supply (5V, 4A minimum recommended)
|
- Power supply (5V, 4A minimum recommended)
|
||||||
- MicroSD card (16GB minimum)
|
- MicroSD card (16GB minimum)
|
||||||
|
|
||||||
**Software:**
|
|
||||||
- Raspberry Pi OS Lite, Trixie (Debian 13) or Bookworm (Debian 12). Trixie
|
|
||||||
is the current release; Bookworm is listed as Legacy in Raspberry Pi
|
|
||||||
Imager. No other system is supported, and the installer says so up front.
|
|
||||||
- The OS's own Python: 3.13 on Trixie, 3.11 on Bookworm
|
|
||||||
|
|
||||||
**Network:**
|
**Network:**
|
||||||
- WiFi network (or Ethernet cable)
|
- WiFi network (or Ethernet cable)
|
||||||
- Computer with web browser on same network
|
- Computer with web browser on same network
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start (5 Minutes)
|
||||||
|
|
||||||
### 1. Install LEDMatrix
|
### 1. First Boot
|
||||||
|
|
||||||
There is no prebuilt SD card image — you install LEDMatrix onto stock
|
1. Insert the MicroSD card with LEDMatrix installed
|
||||||
Raspberry Pi OS Lite yourself:
|
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)
|
||||||
|
|
||||||
1. Flash Raspberry Pi OS Lite (Trixie, or Bookworm) to the MicroSD card
|
**Expected Behavior:**
|
||||||
(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
|
|
||||||
|
|
||||||
The one-shot installer installs the newest release (the **stable** update
|
|
||||||
channel). To run the newest, unreleased code from `main` instead (the
|
|
||||||
**beta** channel), put `LEDMATRIX_CHANNEL=beta` in front of `bash`:
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/ChuckBuilds/LEDMatrix/main/scripts/install/one-shot-install.sh | LEDMATRIX_CHANNEL=beta bash
|
|
||||||
```
|
|
||||||
A manual clone starts on `main`; add `--beta` to `first_time_install.sh`
|
|
||||||
to stay on it, or leave it off and the first update after the next
|
|
||||||
release moves the device onto releases. You can switch channels later on
|
|
||||||
the General tab.
|
|
||||||
|
|
||||||
**Expected Behavior after install:**
|
|
||||||
- LED matrix will light up
|
- LED matrix will light up
|
||||||
- A fresh install ships only the bundled `starlark-apps` and
|
- Display will show default plugins (clock, weather, etc.)
|
||||||
`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
|
- Pi creates WiFi network "LEDMatrix-Setup" if not connected
|
||||||
|
|
||||||
### 2. Connect to WiFi
|
### 2. Connect to WiFi
|
||||||
@@ -101,10 +71,8 @@ You should see:
|
|||||||
|
|
||||||
1. Open the **Display** tab
|
1. Open the **Display** tab
|
||||||
2. Set your matrix configuration:
|
2. Set your matrix configuration:
|
||||||
- **Rows**: match your panel — commonly 32 or 64; any even number
|
- **Rows**: 32 or 64 (match your hardware)
|
||||||
from 8 to 64
|
- **Columns**: 64 or 96 (match your hardware)
|
||||||
- **Columns**: match your panel — commonly 64 or 96; at least 16,
|
|
||||||
with no upper limit
|
|
||||||
- **Chain Length**: Number of panels chained horizontally
|
- **Chain Length**: Number of panels chained horizontally
|
||||||
- **Hardware Mapping**: usually `adafruit-hat-pwm` (with the PWM jumper
|
- **Hardware Mapping**: usually `adafruit-hat-pwm` (with the PWM jumper
|
||||||
mod) or `adafruit-hat` (without). See the root README for the full list.
|
mod) or `adafruit-hat` (without). See the root README for the full list.
|
||||||
@@ -134,8 +102,8 @@ weather and other location-aware plugins.
|
|||||||
4. Wait for installation to finish — installed plugins appear in the
|
4. Wait for installation to finish — installed plugins appear in the
|
||||||
**Installed Plugins** section above and get their own tab in the second
|
**Installed Plugins** section above and get their own tab in the second
|
||||||
nav row
|
nav row
|
||||||
5. Toggle the plugin to enabled. The running display loads it within a
|
5. Toggle the plugin to enabled
|
||||||
few seconds; no restart is needed
|
6. From **Overview**, click **Restart Display Service**
|
||||||
|
|
||||||
You can also install community plugins straight from a GitHub URL using the
|
You can also install community plugins straight from a GitHub URL using the
|
||||||
**Install from GitHub** section further down the same tab — see
|
**Install from GitHub** section further down the same tab — see
|
||||||
@@ -145,15 +113,10 @@ You can also install community plugins straight from a GitHub URL using the
|
|||||||
|
|
||||||
1. Each installed plugin gets its own tab in the second navigation row
|
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,
|
2. Open that plugin's tab to edit its settings (favorite teams, API keys,
|
||||||
update intervals, etc.)
|
update intervals, display duration, etc.)
|
||||||
3. Click **Save**. The display service watches `config.json` and hands the
|
3. Click **Save**
|
||||||
new settings to the running plugin, so no restart is needed. If a plugin
|
4. Restart the display service from **Overview** so the new settings take
|
||||||
still shows old settings, restart the display service from **Overview**
|
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**
|
**Example: Weather Plugin**
|
||||||
- Set your location (city, state, country)
|
- Set your location (city, state, country)
|
||||||
@@ -215,15 +178,14 @@ The fastest way to verify a plugin works without waiting for the rotation:
|
|||||||
|
|
||||||
**Check:**
|
**Check:**
|
||||||
1. Plugin is enabled (toggle on the **Plugin Manager** tab)
|
1. Plugin is enabled (toggle on the **Plugin Manager** tab)
|
||||||
2. Plugin's display duration is non-zero
|
2. Display service was restarted after enabling
|
||||||
3. No errors in the **Logs** tab for that plugin. A plugin whose
|
3. Plugin's display duration is non-zero
|
||||||
`validate_config()` fails is not loaded until its settings are fixed
|
4. No errors in the **Logs** tab for that plugin
|
||||||
|
|
||||||
**Fix:**
|
**Fix:**
|
||||||
1. Enable the plugin from **Plugin Manager**
|
1. Enable the plugin from **Plugin Manager**
|
||||||
2. Check the **Logs** tab for plugin-specific errors
|
2. Click **Restart Display Service** on **Overview**
|
||||||
3. If it still does not appear, click **Restart Display Service** on
|
3. Check the **Logs** tab for plugin-specific errors
|
||||||
**Overview**
|
|
||||||
|
|
||||||
### Weather Plugin Shows "No Data"
|
### Weather Plugin Shows "No Data"
|
||||||
|
|
||||||
@@ -244,36 +206,18 @@ The fastest way to verify a plugin works without waiting for the rotation:
|
|||||||
### Customize Your Display
|
### Customize Your Display
|
||||||
|
|
||||||
**Adjust display durations:**
|
**Adjust display durations:**
|
||||||
- Open the **Rotation** tab and use the **Screen Durations** section to
|
- Each plugin's tab has a **Display Duration (seconds)** field — set how
|
||||||
set how long each plugin stays on screen per rotation (saved to
|
long that plugin stays on screen each rotation.
|
||||||
`display.display_durations`).
|
|
||||||
|
|
||||||
**Organize plugin order:**
|
**Organize plugin order:**
|
||||||
- The **Rotation** tab also has a drag-and-drop **Rotation Order** list
|
- Use the **Plugin Manager** tab to enable/disable plugins. The display
|
||||||
(saved to `display.plugin_rotation_order`). Enable/disable plugins
|
cycles through enabled plugins in the order they appear.
|
||||||
from the **Plugin Manager** tab.
|
|
||||||
|
|
||||||
**Add more plugins:**
|
**Add more plugins:**
|
||||||
- Check the **Plugin Store** section of **Plugin Manager** for new plugins.
|
- Check the **Plugin Store** section of **Plugin Manager** for new plugins.
|
||||||
- Install community plugins straight from a GitHub URL via
|
- Install community plugins straight from a GitHub URL via
|
||||||
**Install from GitHub** on the same tab.
|
**Install from GitHub** on the same tab.
|
||||||
|
|
||||||
### Keep LEDMatrix Up to Date
|
|
||||||
|
|
||||||
- **Update Code** on the **Overview** tab installs the newest version, and a
|
|
||||||
banner at the top of the page says when one is available.
|
|
||||||
- **General → Automatic Updates** does it once a week, overnight, with a
|
|
||||||
health check that undoes an update that breaks the device.
|
|
||||||
- **General → Update Channel** picks which version that is. **Stable** (the
|
|
||||||
default) installs releases, which have been tested and have release
|
|
||||||
notes. **Beta** installs the newest code as soon as it is written, before
|
|
||||||
it is released: fixes arrive sooner, and so do new problems.
|
|
||||||
- Switching to Stable never installs an older version than the one you
|
|
||||||
have. If your device is already newer than the latest release (which is
|
|
||||||
normal if it was set up or updated from the newest code), it keeps
|
|
||||||
getting the newest code until the next release includes it, then follows
|
|
||||||
releases from there. The General tab says when this is the case.
|
|
||||||
|
|
||||||
### Enable Advanced Features
|
### Enable Advanced Features
|
||||||
|
|
||||||
**Vegas Scroll Mode:**
|
**Vegas Scroll Mode:**
|
||||||
@@ -334,21 +278,13 @@ sudo journalctl -u ledmatrix-web -f
|
|||||||
│ ├── config_secrets.json # API keys and secrets
|
│ ├── config_secrets.json # API keys and secrets
|
||||||
│ └── wifi_config.json # WiFi settings
|
│ └── wifi_config.json # WiFi settings
|
||||||
├── plugin-repos/ # Installed plugins (default location)
|
├── plugin-repos/ # Installed plugins (default location)
|
||||||
|
├── cache/ # Cached data
|
||||||
└── web_interface/ # Web interface files
|
└── 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
|
> The plugin install location is configurable via
|
||||||
> `plugin_system.plugins_directory` in `config.json`. The default is
|
> `plugin_system.plugins_directory` in `config.json`. The default is
|
||||||
> `plugin-repos/`. Plugin discovery (`PluginManager.discover_plugins()`)
|
> `plugin-repos/`; the loader also searches `plugins/` as a fallback.
|
||||||
> 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
|
### Web Interface
|
||||||
|
|
||||||
@@ -361,14 +297,11 @@ System tabs:
|
|||||||
- WiFi Network selection and AP-mode setup
|
- WiFi Network selection and AP-mode setup
|
||||||
- Schedule Power and dim schedules
|
- Schedule Power and dim schedules
|
||||||
- Display Matrix hardware configuration
|
- Display Matrix hardware configuration
|
||||||
- Rotation Rotation order (drag-and-drop) and screen durations
|
|
||||||
- Config Editor Raw config.json editor
|
- Config Editor Raw config.json editor
|
||||||
- Backup & Restore Config backup and restore
|
|
||||||
- Fonts Upload and manage fonts
|
- Fonts Upload and manage fonts
|
||||||
- Logs Real-time log viewing
|
- Logs Real-time log viewing
|
||||||
- Cache Cached data inspection and cleanup
|
- Cache Cached data inspection and cleanup
|
||||||
- Operation History Recent service operations
|
- Operation History Recent service operations
|
||||||
- Tools System diagnostics, updates, dependencies, maintenance
|
|
||||||
|
|
||||||
Plugin tabs (second row):
|
Plugin tabs (second row):
|
||||||
- Plugin Manager Browse the Plugin Store, install/enable plugins
|
- Plugin Manager Browse the Plugin Store, install/enable plugins
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ Make sure you have the testing packages installed:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install all dependencies including test packages
|
# Install all dependencies including test packages
|
||||||
pip install -r requirements.txt -r requirements-test.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Or install just the test dependencies
|
||||||
|
pip install pytest pytest-cov pytest-mock
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Set Environment Variables
|
### 2. Set Environment Variables
|
||||||
@@ -52,26 +55,28 @@ pytest test/test_display_controller.py test/test_plugin_system.py
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run a specific test class
|
# Run a specific test class
|
||||||
pytest test/test_display_controller.py::TestDisplayControllerLivePriority
|
pytest test/test_display_controller.py::TestDisplayControllerModeRotation
|
||||||
|
|
||||||
# Run a specific test function
|
# Run a specific test function
|
||||||
pytest test/test_display_controller.py::TestDisplayControllerSchedule::test_active_hours
|
pytest test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run Tests by Marker
|
### Run Tests by Marker
|
||||||
|
|
||||||
`pytest.ini` declares the markers `unit`, `integration`, `hardware`, `slow`
|
The tests use markers to categorize them:
|
||||||
and `plugin` (with `--strict-markers`, so a typo in a marker name is an
|
|
||||||
error). Few tests are marked: only a handful carry `unit`, and none currently
|
|
||||||
carry `integration`, `slow` or `hardware`, so `-m integration` and `-m slow`
|
|
||||||
select nothing. Select tests by file, directory or `-k` instead.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# What CI runs for the core suites (excludes anything marked hardware)
|
# Run only unit tests (fast, isolated)
|
||||||
pytest -m "not hardware" test/ --ignore=test/plugins
|
pytest -m unit
|
||||||
|
|
||||||
# Tests whose name matches an expression
|
# Run only integration tests
|
||||||
pytest -k "config and not secrets"
|
pytest -m integration
|
||||||
|
|
||||||
|
# Run tests that don't require hardware
|
||||||
|
pytest -m "not hardware"
|
||||||
|
|
||||||
|
# Run slow tests
|
||||||
|
pytest -m slow
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run Tests in a Directory
|
### Run Tests in a Directory
|
||||||
@@ -98,7 +103,7 @@ When you run `pytest`, you'll see:
|
|||||||
|
|
||||||
```
|
```
|
||||||
test/test_display_controller.py::TestDisplayControllerInitialization::test_init_success PASSED
|
test/test_display_controller.py::TestDisplayControllerInitialization::test_init_success PASSED
|
||||||
test/test_display_controller.py::TestDisplayControllerOnDemand::test_activate_on_demand PASSED
|
test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation PASSED
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -136,73 +141,71 @@ pytest -sv
|
|||||||
|
|
||||||
## Coverage Reports
|
## Coverage Reports
|
||||||
|
|
||||||
Coverage is not collected by a plain `pytest` run: `pytest.ini` deliberately
|
The test suite is configured to generate coverage reports.
|
||||||
has no coverage flags, so local runs stay fast. Ask for it explicitly
|
|
||||||
(needs `pytest-cov`, which is in `requirements-test.txt`):
|
### View Coverage in Terminal
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Terminal summary
|
# Coverage is automatically shown when running pytest
|
||||||
pytest --cov=src --cov=web_interface --cov-report=term test/ --ignore=test/plugins
|
pytest
|
||||||
|
|
||||||
# HTML report in htmlcov/
|
# The output will show something like:
|
||||||
pytest --cov=src --cov=web_interface --cov-report=html test/ --ignore=test/plugins
|
# ----------- coverage: platform linux, python 3.11.5 -----------
|
||||||
|
# Name Stmts Miss Cover Missing
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# src/display_controller.py 450 120 73% 45-67, 89-102
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open `htmlcov/index.html` in your browser (`xdg-open` on Linux, `open`
|
### Generate HTML Coverage Report
|
||||||
on macOS, `start` on Windows).
|
|
||||||
|
```bash
|
||||||
|
# HTML report is automatically generated in htmlcov/
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Then open the report in your browser
|
||||||
|
# On Linux:
|
||||||
|
xdg-open htmlcov/index.html
|
||||||
|
|
||||||
|
# On macOS:
|
||||||
|
open htmlcov/index.html
|
||||||
|
|
||||||
|
# On Windows:
|
||||||
|
start htmlcov/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
The HTML report shows:
|
||||||
|
- Line-by-line coverage
|
||||||
|
- Files with low coverage highlighted
|
||||||
|
- Interactive navigation
|
||||||
|
|
||||||
### Coverage Threshold
|
### Coverage Threshold
|
||||||
|
|
||||||
The only threshold is in CI: the core unit-test job in
|
The tests are configured to fail if coverage drops below 30%. To change this, edit `pytest.ini`:
|
||||||
[`.github/workflows/test.yml`](../.github/workflows/test.yml) runs with
|
|
||||||
`--cov-fail-under=52`. To check it locally, add that flag to the command
|
```ini
|
||||||
above.
|
--cov-fail-under=30 # Change this value
|
||||||
|
```
|
||||||
|
|
||||||
## Common Test Scenarios
|
## Common Test Scenarios
|
||||||
|
|
||||||
### Run Tests After Making Changes
|
### Run Tests After Making Changes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Quick run: just the tests for the area you changed
|
# Quick test run (just unit tests)
|
||||||
pytest test/test_config_manager.py
|
pytest -m unit
|
||||||
|
|
||||||
# Full test suite
|
# Full test suite
|
||||||
pytest
|
pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web UI JavaScript Tests
|
|
||||||
|
|
||||||
The suites in `test/js` need node; the DOM ones also need jsdom and a running
|
|
||||||
web interface (details in [`test/js/README.md`](../test/js/README.md)):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install --no-audit --no-fund --prefix test/js # jsdom; node_modules is gitignored
|
|
||||||
EMULATOR=true python3 web_interface/app.py # in another shell
|
|
||||||
BASE=http://localhost:5000 REQUIRE_DOM=1 node test/js/run_all.js
|
|
||||||
```
|
|
||||||
|
|
||||||
`pytest test/test_js_unit_suites.py` runs just the unit suites.
|
|
||||||
|
|
||||||
### Plugin Config Form Parity
|
|
||||||
|
|
||||||
`test/test_field_model_parity.py` checks `build_field_model` against the
|
|
||||||
`render_field` macro for every plugin schema it finds
|
|
||||||
([WEB_FRONTEND_ARCHITECTURE.md](WEB_FRONTEND_ARCHITECTURE.md)). It always
|
|
||||||
covers `plugin-repos/` and the test fixtures; point it at a checkout of the
|
|
||||||
official plugins to cover those too:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
LEDMATRIX_MONOREPO_PLUGINS=../ledmatrix-plugins/plugins pytest test/test_field_model_parity.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug a Failing Test
|
### Debug a Failing Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run with maximum verbosity and show print statements
|
# Run with maximum verbosity and show print statements
|
||||||
pytest -vv -s test/test_display_controller.py::TestDisplayControllerSchedule::test_active_hours
|
pytest -vv -s test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation
|
||||||
|
|
||||||
# Run with Python debugger (pdb)
|
# Run with Python debugger (pdb)
|
||||||
pytest --pdb test/test_display_controller.py::TestDisplayControllerSchedule::test_active_hours
|
pytest --pdb test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run Tests in Parallel (Faster)
|
### Run Tests in Parallel (Faster)
|
||||||
@@ -245,15 +248,22 @@ test/
|
|||||||
├── test_config_service.py # Config service tests
|
├── test_config_service.py # Config service tests
|
||||||
├── test_config_validation_edge_cases.py # Config edge cases
|
├── test_config_validation_edge_cases.py # Config edge cases
|
||||||
├── test_font_manager.py # Font manager tests
|
├── test_font_manager.py # Font manager tests
|
||||||
|
├── test_layout_manager.py # Layout manager tests
|
||||||
├── test_text_helper.py # Text helper tests
|
├── test_text_helper.py # Text helper tests
|
||||||
├── test_error_handling.py # Error handling tests
|
├── test_error_handling.py # Error handling tests
|
||||||
├── test_error_aggregator.py # Error aggregation tests
|
├── test_error_aggregator.py # Error aggregation tests
|
||||||
├── test_schema_manager.py # Schema manager tests
|
├── test_schema_manager.py # Schema manager tests
|
||||||
├── test_web_api.py # Web API tests
|
├── test_web_api.py # Web API tests
|
||||||
├── plugins/ # Plugin rendering suites
|
├── test_nba_*.py # NBA-specific test suites
|
||||||
│ ├── test_plugin_matrix.py # Every discovered plugin, across panel sizes
|
├── plugins/ # Per-plugin test suites
|
||||||
│ ├── test_harness.py
|
│ ├── test_clock_simple.py
|
||||||
│ └── test_visual_rendering.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/
|
└── web_interface/
|
||||||
├── test_config_manager_atomic.py
|
├── test_config_manager_atomic.py
|
||||||
├── test_state_reconciliation.py
|
├── test_state_reconciliation.py
|
||||||
@@ -278,8 +288,8 @@ test/
|
|||||||
If you see import errors:
|
If you see import errors:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Make sure you're in the project root (wherever you cloned it)
|
# Make sure you're in the project root
|
||||||
cd ~/LEDMatrix
|
cd /home/chuck/Github/LEDMatrix
|
||||||
|
|
||||||
# Check Python path
|
# Check Python path
|
||||||
python -c "import sys; print(sys.path)"
|
python -c "import sys; print(sys.path)"
|
||||||
@@ -294,7 +304,7 @@ If tests fail due to missing packages:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install all dependencies
|
# Install all dependencies
|
||||||
pip install -r requirements.txt -r requirements-test.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# Or install specific missing package
|
# Or install specific missing package
|
||||||
pip install <package-name>
|
pip install <package-name>
|
||||||
@@ -320,37 +330,33 @@ If coverage reports aren't generating:
|
|||||||
# Make sure pytest-cov is installed
|
# Make sure pytest-cov is installed
|
||||||
pip install pytest-cov
|
pip install pytest-cov
|
||||||
|
|
||||||
# Coverage is opt-in; ask for it explicitly
|
# Run with explicit coverage
|
||||||
pytest --cov=src --cov=web_interface --cov-report=html
|
pytest --cov=src --cov-report=html
|
||||||
```
|
```
|
||||||
|
|
||||||
## Continuous Integration
|
## Continuous Integration
|
||||||
|
|
||||||
The repo runs the pytest suite via
|
Tests are configured to run automatically in CI/CD. The GitHub Actions workflow (`.github/workflows/tests.yml`) runs:
|
||||||
[`.github/workflows/test.yml`](../.github/workflows/test.yml) on every
|
|
||||||
push and pull request: a plugin-safety job that runs `test/plugins/`, and a
|
- All tests on multiple Python versions (3.10, 3.11, 3.12)
|
||||||
core unit-test job that runs the whole `test/` tree except `test/plugins/`
|
- Coverage reporting
|
||||||
with `-m "not hardware"` and enforces coverage (`--cov-fail-under=52`). New
|
- Uploads coverage to Codecov (if configured)
|
||||||
test files are picked up automatically. 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
|
## Best Practices
|
||||||
|
|
||||||
1. **Run tests before committing**:
|
1. **Run tests before committing**:
|
||||||
```bash
|
```bash
|
||||||
pytest test/test_<area>.py # Quick check of what you touched
|
pytest -m unit # Quick check
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run full suite before pushing**:
|
2. **Run full suite before pushing**:
|
||||||
```bash
|
```bash
|
||||||
pytest # Full test suite (add --cov flags for coverage)
|
pytest # Full test suite with coverage
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Fix failing tests immediately** - Don't let them accumulate
|
3. **Fix failing tests immediately** - Don't let them accumulate
|
||||||
|
|
||||||
4. **Keep coverage above threshold** - CI fails below 52%
|
4. **Keep coverage above threshold** - Aim for 70%+ coverage
|
||||||
|
|
||||||
5. **Write tests for new features** - Add tests when adding new functionality
|
5. **Write tests for new features** - Add tests when adding new functionality
|
||||||
|
|
||||||
@@ -358,9 +364,9 @@ Bandit, flake8, mypy and gitleaks run as pre-commit hooks (see
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Most common commands
|
# Most common commands
|
||||||
pytest # Run all tests (no coverage)
|
pytest # Run all tests with coverage
|
||||||
pytest -v # Verbose output
|
pytest -v # Verbose output
|
||||||
pytest test/test_x.py # Run one file
|
pytest -m unit # Run only unit tests
|
||||||
pytest -k "test_name" # Run tests matching pattern
|
pytest -k "test_name" # Run tests matching pattern
|
||||||
pytest --cov=src # Generate coverage report
|
pytest --cov=src # Generate coverage report
|
||||||
pytest -x # Stop on first failure
|
pytest -x # Stop on first failure
|
||||||
|
|||||||