Compare commits
116 Commits
v2.5
...
35df06b8e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35df06b8e1 | ||
|
|
77e9eba294 | ||
|
|
6eccb74415 | ||
|
|
2c2fca2219 | ||
|
|
640a4c1706 | ||
|
|
81a022dbe8 | ||
|
|
48ff624a85 | ||
|
|
31ed854d4e | ||
|
|
442638dd2c | ||
|
|
8391832c90 | ||
|
|
c8737d1a6c | ||
|
|
28a374485f | ||
|
|
fa92bfbdd8 | ||
|
|
f3e7c639ba | ||
|
|
f718305886 | ||
|
|
f0dc094cd6 | ||
|
|
178dfb0c2a | ||
|
|
76c5bf5781 | ||
|
|
feee1dffde | ||
|
|
f05c357d57 | ||
|
|
fe5c1d0d5e | ||
|
|
3e50fa5b1d | ||
|
|
8ae82321ce | ||
|
|
eb143c44fa | ||
|
|
275fed402e | ||
|
|
38a9c1ed1b | ||
|
|
23f0176c18 | ||
|
|
9465fcda6e | ||
|
|
976c10c4ac | ||
|
|
b92ff3dfbd | ||
|
|
4c4efd614a | ||
|
|
14b6a0c6a3 | ||
|
|
c2763d6447 | ||
|
|
1f0de9b354 | ||
|
|
ed90654bf2 | ||
|
|
302235a357 | ||
|
|
636d0e181c | ||
|
|
963c4d3b91 | ||
|
|
22c495ea7c | ||
|
|
5b0ad5ab71 | ||
|
|
bc8568604a | ||
|
|
878f339fb3 | ||
|
|
51616f1bc4 | ||
|
|
82370a0253 | ||
|
|
3975940cff | ||
|
|
158e07c82b | ||
|
|
9a72adbde1 | ||
|
|
9d3bc55c18 | ||
|
|
df3cf9bb56 | ||
|
|
448a15c1e6 | ||
|
|
b99be88cec | ||
|
|
4a9fc2df3a | ||
|
|
d207e7c6dd | ||
|
|
7e98fa9bd8 | ||
|
|
0d5510d8f7 | ||
|
|
18fecd3cda | ||
|
|
1c3269c0f3 | ||
|
|
ea61331d46 | ||
|
|
8fb2800495 | ||
|
|
8912501604 | ||
|
|
68c4259370 | ||
|
|
7f5c7399fb | ||
|
|
14c50f316e | ||
|
|
ddd300a117 | ||
|
|
7524747e44 | ||
|
|
10d70d911a | ||
|
|
a8c85dd015 | ||
|
|
0203c5c1b5 | ||
|
|
384ed096ff | ||
|
|
f9de9fa29e | ||
|
|
d0ad2031c8 | ||
|
|
1833e30c1d | ||
|
|
2381ead03f | ||
|
|
bc23b7c75c | ||
|
|
bff16d3e00 | ||
|
|
23ada60544 | ||
|
|
fadcf0f407 | ||
|
|
71584d4361 | ||
|
|
3b8910ac09 | ||
|
|
94d5a38358 | ||
|
|
4a63ff87cb | ||
|
|
fdf09fabd2 | ||
|
|
75a8219a29 | ||
|
|
a9798e1a7f | ||
|
|
c35769cefb | ||
|
|
f1f33989b2 | ||
|
|
f9e21c6033 | ||
|
|
0f4dbb6c1a | ||
|
|
b9f839af3d | ||
|
|
f438f9dfe3 | ||
|
|
7f230f625d | ||
|
|
3fa032f7f6 | ||
|
|
5f4839b4f6 | ||
|
|
20d58754b8 | ||
|
|
f7d72f88b5 | ||
|
|
a13bd971b3 | ||
|
|
9f1711f9a3 | ||
|
|
67197635c9 | ||
|
|
a5c10d6f78 | ||
|
|
cca495f306 | ||
|
|
24c34c5a40 | ||
|
|
1815a5b791 | ||
|
|
947a0fbe8f | ||
|
|
00a1b9dc90 | ||
|
|
f22910207b | ||
|
|
97a301a1a9 | ||
|
|
f7e8266e64 | ||
|
|
7ec4323ff4 | ||
|
|
d14b5ffb8f | ||
|
|
33e4f3680c | ||
|
|
b0d65581df | ||
|
|
f412350110 | ||
|
|
f8a2f687ba | ||
|
|
4be4caae08 | ||
|
|
e4976b65c6 | ||
|
|
7d71656cf1 |
132
.cursor/README.md
Normal file
@@ -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/`
|
||||
|
||||
202
.cursor/plans/implement_audit_fixes_plan.md
Normal file
@@ -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.
|
||||
|
||||
233
.cursor/plugin_templates/QUICK_START.md
Normal file
@@ -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
|
||||
|
||||
156
.cursor/plugin_templates/README.md.template
Normal file
@@ -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
|
||||
|
||||
44
.cursor/plugin_templates/config_schema.json.template
Normal file
@@ -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
|
||||
}
|
||||
|
||||
226
.cursor/plugin_templates/manager.py.template
Normal file
@@ -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()
|
||||
|
||||
55
.cursor/plugin_templates/manifest.json.template
Normal file
@@ -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": ""
|
||||
}
|
||||
|
||||
13
.cursor/plugin_templates/requirements.txt.template
Normal file
@@ -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
|
||||
|
||||
136
.cursor/plugin_templates/test_manager.py.template
Normal file
@@ -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()
|
||||
|
||||
742
.cursor/plugins_guide.md
Normal file
@@ -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
|
||||
|
||||
1
.cursorignore
Normal file
@@ -0,0 +1 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
312
.cursorrules
Normal file
@@ -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`
|
||||
|
||||
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
github: ChuckBuilds
|
||||
buy_me_a_coffee: chuckbuilds
|
||||
ko_fi: chuckbuilds
|
||||
|
||||
24
.gitignore
vendored
@@ -7,6 +7,7 @@ __pycache__/
|
||||
config/config_secrets.json
|
||||
config/config.json
|
||||
config/config.json.backup
|
||||
config/wifi_config.json
|
||||
credentials.json
|
||||
token.pickle
|
||||
|
||||
@@ -25,5 +26,24 @@ ENV/
|
||||
*.swo
|
||||
emulator_config.json
|
||||
|
||||
# Cache directory
|
||||
cache/
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.mypy_cache/
|
||||
|
||||
# Cache directory (root level only, not src/cache which is source code)
|
||||
/cache/
|
||||
|
||||
# Development plugins directory
|
||||
# Plugins are managed as separate repositories via multi-root workspace
|
||||
# See docs/MULTI_ROOT_WORKSPACE_SETUP.md for details
|
||||
plugins/*
|
||||
!plugins/.gitkeep
|
||||
|
||||
# Binary files and backups
|
||||
bin/pixlet/
|
||||
config/backups/
|
||||
|
||||
# Starlark apps runtime storage (installed .star files and cached renders)
|
||||
/starlark-apps/
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "rpi-rgb-led-matrix-master"]
|
||||
path = rpi-rgb-led-matrix-master
|
||||
url = https://github.com/hzeller/rpi-rgb-led-matrix.git
|
||||
64
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
# Pre-commit hooks for LEDMatrix
|
||||
# Install: pip install pre-commit && pre-commit install
|
||||
# Run manually: pre-commit run --all-files
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-json
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: check-merge-conflict
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ['--select=E9,F63,F7,F82,B', '--ignore=E501']
|
||||
additional_dependencies: [flake8-bugbear]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-bare-except
|
||||
name: Check for bare except clauses
|
||||
entry: bash -c 'if grep -rn "except:\s*pass" src/; then echo "Found bare except:pass - please handle exceptions properly"; exit 1; fi'
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
|
||||
- id: no-hardcoded-paths
|
||||
name: Check for hardcoded user paths
|
||||
entry: bash -c 'if grep -rn "/home/chuck/" src/; then echo "Found hardcoded user paths - please use relative paths or config"; exit 1; fi'
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-requests, types-pytz]
|
||||
args: [--ignore-missing-imports, --no-error-summary]
|
||||
pass_filenames: false
|
||||
files: ^src/
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.8.3
|
||||
hooks:
|
||||
- id: bandit
|
||||
args:
|
||||
- '-r'
|
||||
- '-ll'
|
||||
- '-c'
|
||||
- 'bandit.yaml'
|
||||
- '-x'
|
||||
- './tests,./test,./venv,./.venv,./scripts/prove_security.py,./rpi-rgb-led-matrix-master'
|
||||
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.24.3
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
@@ -1,148 +0,0 @@
|
||||
# AP Top 25 Dynamic Teams Implementation Summary
|
||||
|
||||
## 🎯 Feature Overview
|
||||
|
||||
Successfully implemented dynamic team resolution for AP Top 25 rankings in the LEDMatrix project. Users can now add `"AP_TOP_25"` to their `favorite_teams` list and it will automatically resolve to the current AP Top 25 teams, updating weekly as rankings change.
|
||||
|
||||
## 🚀 What Was Implemented
|
||||
|
||||
### 1. Dynamic Team Resolver (`src/dynamic_team_resolver.py`)
|
||||
- **Core Functionality**: Resolves dynamic team names like `"AP_TOP_25"` into actual team abbreviations
|
||||
- **API Integration**: Fetches current AP Top 25 rankings from ESPN API
|
||||
- **Caching**: 1-hour cache to reduce API calls and improve performance
|
||||
- **Error Handling**: Graceful fallback when rankings unavailable
|
||||
- **Multiple Patterns**: Supports `AP_TOP_25`, `AP_TOP_10`, `AP_TOP_5`
|
||||
|
||||
### 2. Sports Core Integration (`src/base_classes/sports.py`)
|
||||
- **Automatic Resolution**: Favorite teams are automatically resolved at initialization
|
||||
- **Seamless Integration**: Works with existing favorite teams system
|
||||
- **Logging**: Clear logging of dynamic team resolution
|
||||
- **Backward Compatibility**: Regular team names work exactly as before
|
||||
|
||||
### 3. Configuration Updates (`config/config.template.json`)
|
||||
- **Example Usage**: Added `"AP_TOP_25"` to NCAA FB configuration example
|
||||
- **Documentation**: Clear examples of how to use dynamic teams
|
||||
|
||||
### 4. Comprehensive Testing
|
||||
- **Unit Tests**: `test/test_dynamic_team_resolver.py` - Core functionality
|
||||
- **Integration Tests**: `test/test_dynamic_teams_simple.py` - Configuration integration
|
||||
- **Edge Cases**: Unknown dynamic teams, empty lists, mixed teams
|
||||
- **Performance**: Caching verification and performance testing
|
||||
|
||||
### 5. Documentation (`LEDMatrix.wiki/AP_TOP_25_DYNAMIC_TEAMS.md`)
|
||||
- **Complete Guide**: How to use the feature
|
||||
- **Configuration Examples**: Multiple usage scenarios
|
||||
- **Technical Details**: API integration, caching, performance
|
||||
- **Troubleshooting**: Common issues and solutions
|
||||
- **Best Practices**: Recommendations for optimal usage
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### Dynamic Team Resolution Process
|
||||
1. **Detection**: Check if team name is in `DYNAMIC_PATTERNS`
|
||||
2. **API Fetch**: Retrieve current rankings from ESPN API
|
||||
3. **Resolution**: Convert dynamic name to actual team abbreviations
|
||||
4. **Caching**: Store results for 1 hour to reduce API calls
|
||||
5. **Integration**: Seamlessly work with existing favorite teams logic
|
||||
|
||||
### Supported Dynamic Teams
|
||||
| Dynamic Team | Description | Teams Returned |
|
||||
|-------------|-------------|----------------|
|
||||
| `"AP_TOP_25"` | Current AP Top 25 | All 25 ranked teams |
|
||||
| `"AP_TOP_10"` | Current AP Top 10 | Top 10 ranked teams |
|
||||
| `"AP_TOP_5"` | Current AP Top 5 | Top 5 ranked teams |
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
#### Basic AP Top 25 Usage
|
||||
```json
|
||||
{
|
||||
"ncaa_fb_scoreboard": {
|
||||
"enabled": true,
|
||||
"show_favorite_teams_only": true,
|
||||
"favorite_teams": ["AP_TOP_25"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mixed Regular and Dynamic Teams
|
||||
```json
|
||||
{
|
||||
"ncaa_fb_scoreboard": {
|
||||
"enabled": true,
|
||||
"show_favorite_teams_only": true,
|
||||
"favorite_teams": [
|
||||
"UGA",
|
||||
"AUB",
|
||||
"AP_TOP_25"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ Testing Results
|
||||
|
||||
### All Tests Passing
|
||||
- **Core Functionality**: ✅ Dynamic team resolution works correctly
|
||||
- **API Integration**: ✅ Successfully fetches AP Top 25 from ESPN
|
||||
- **Caching**: ✅ 1-hour cache reduces API calls significantly
|
||||
- **Edge Cases**: ✅ Unknown dynamic teams, empty lists handled properly
|
||||
- **Performance**: ✅ Second call uses cache (0.000s vs 0.062s)
|
||||
- **Integration**: ✅ Works seamlessly with existing sports managers
|
||||
|
||||
### Test Coverage
|
||||
- **Unit Tests**: 6 test categories, all passing
|
||||
- **Integration Tests**: Configuration integration verified
|
||||
- **Edge Cases**: 4 edge case scenarios tested
|
||||
- **Performance**: Caching and API call optimization verified
|
||||
|
||||
## 🎉 Benefits for Users
|
||||
|
||||
### Automatic Updates
|
||||
- **Weekly Updates**: Rankings automatically update when ESPN releases new rankings
|
||||
- **No Manual Work**: Users don't need to manually update team lists
|
||||
- **Always Current**: Always shows games for the current top-ranked teams
|
||||
|
||||
### Flexible Options
|
||||
- **Multiple Ranges**: Choose from AP_TOP_5, AP_TOP_10, or AP_TOP_25
|
||||
- **Mixed Usage**: Combine with regular favorite teams
|
||||
- **Easy Configuration**: Simple addition to existing config
|
||||
|
||||
### Performance Optimized
|
||||
- **Efficient Caching**: 1-hour cache reduces API calls
|
||||
- **Background Updates**: Rankings fetched in background
|
||||
- **Minimal Overhead**: Only fetches when dynamic teams are used
|
||||
|
||||
## 🔮 Future Enhancements
|
||||
|
||||
The system is designed to be extensible for future dynamic team types:
|
||||
|
||||
- `"PLAYOFF_TEAMS"`: Teams in playoff contention
|
||||
- `"CONFERENCE_LEADERS"`: Conference leaders
|
||||
- `"HEISMAN_CANDIDATES"`: Teams with Heisman candidates
|
||||
- `"RIVALRY_GAMES"`: Traditional rivalry matchups
|
||||
|
||||
## 📋 Usage Instructions
|
||||
|
||||
### For Users
|
||||
1. **Add to Config**: Add `"AP_TOP_25"` to your `favorite_teams` list
|
||||
2. **Enable Filtering**: Set `"show_favorite_teams_only": true`
|
||||
3. **Enjoy**: System automatically shows games for current top 25 teams
|
||||
|
||||
### For Developers
|
||||
1. **Import**: `from src.dynamic_team_resolver import DynamicTeamResolver`
|
||||
2. **Resolve**: `resolver.resolve_teams(["AP_TOP_25"], 'ncaa_fb')`
|
||||
3. **Integrate**: Works automatically with existing SportsCore classes
|
||||
|
||||
## 🎯 Success Metrics
|
||||
|
||||
- **✅ Feature Complete**: All planned functionality implemented
|
||||
- **✅ Fully Tested**: Comprehensive test suite with 100% pass rate
|
||||
- **✅ Well Documented**: Complete documentation and examples
|
||||
- **✅ Performance Optimized**: Efficient caching and API usage
|
||||
- **✅ User Friendly**: Simple configuration, automatic updates
|
||||
- **✅ Backward Compatible**: Existing configurations continue to work
|
||||
|
||||
## 🚀 Ready for Production
|
||||
|
||||
The AP Top 25 Dynamic Teams feature is fully implemented, tested, and ready for production use. Users can now enjoy automatically updating favorite teams that follow the current AP Top 25 rankings without any manual intervention.
|
||||
31
CLAUDE.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# LEDMatrix
|
||||
|
||||
## Project Structure
|
||||
- `src/plugin_system/` — Plugin loader, manager, store manager, base plugin class
|
||||
- `web_interface/` — Flask web UI (blueprints, templates, static JS)
|
||||
- `config/config.json` — User plugin configuration (persists across plugin reinstalls)
|
||||
- `plugins/` — Installed plugins directory (gitignored)
|
||||
- `plugin-repos/` — Development symlinks to monorepo plugin dirs
|
||||
|
||||
## Plugin System
|
||||
- Plugins inherit from `BasePlugin` in `src/plugin_system/base_plugin.py`
|
||||
- Required abstract methods: `update()`, `display(force_clear=False)`
|
||||
- Each plugin needs: `manifest.json`, `config_schema.json`, `manager.py`, `requirements.txt`
|
||||
- Plugin instantiation args: `plugin_id, config, display_manager, cache_manager, plugin_manager`
|
||||
- Config schemas use JSON Schema Draft-7
|
||||
- Display dimensions: always read dynamically from `self.display_manager.matrix.width/height`
|
||||
|
||||
## Plugin Store Architecture
|
||||
- Official plugins live in the `ledmatrix-plugins` monorepo (not individual repos)
|
||||
- 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`
|
||||
- Store manager (`src/plugin_system/store_manager.py`) handles install/update/uninstall
|
||||
- Monorepo plugins are installed via ZIP extraction (no `.git` directory)
|
||||
- Update detection for monorepo plugins uses version comparison (manifest version vs registry latest_version)
|
||||
- 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`
|
||||
|
||||
## Common Pitfalls
|
||||
- paho-mqtt 2.x needs `callback_api_version=mqtt.CallbackAPIVersion.VERSION1` for v1 compat
|
||||
- BasePlugin uses `get_logger()` from `src.logging_config`, not standard `logging.getLogger()`
|
||||
- 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
|
||||
19
LEDMatrix.code-workspace
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".",
|
||||
"name": "LEDMatrix (Main)"
|
||||
},
|
||||
{
|
||||
"path": "../ledmatrix-plugins",
|
||||
"name": "Plugins (Monorepo)"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/__pycache__": true,
|
||||
"**/*.pyc": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
This thing was created by Thingiverse user randomwire, and is licensed under cc.
|
||||
@@ -1 +0,0 @@
|
||||
P4 Matrix Stand by randomwire on Thingiverse: https://www.thingiverse.com/thing:5169867
|
||||
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 230 KiB |
@@ -9,20 +9,20 @@ from the utils/ directory.
|
||||
|
||||
Tom-Thumb.bdf is included in this directory under [MIT license](http://vt100.tarunz.org/LICENSE). Tom-thumb.bdf was created by [@robey](http://twitter.com/robey) and originally published at https://robey.lag.net/2010/01/23/tiny-monospace-font.html
|
||||
|
||||
The texguire-27.bdf font was created using the [otf2bdf] tool from the TeX Gyre font.
|
||||
```
|
||||
The texgyre-27.bdf font was created using the [otf2bdf] tool from the TeX Gyre font.
|
||||
```bash
|
||||
otf2bdf -v -o texgyre-27.bdf -r 72 -p 27 texgyreadventor-regular.otf
|
||||
```
|
||||
|
||||
## Create your own
|
||||
|
||||
Fonts are in a human readable and editbable `*.bdf` format, but unless you
|
||||
Fonts are in a human-readable and editable `*.bdf` format, but unless you
|
||||
like reading and writing pixels in hex, generating them is probably easier :)
|
||||
|
||||
You can use any font-editor to generate a BDF font or use the conversion
|
||||
tool [otf2bdf] to create one from some other font format.
|
||||
|
||||
Here is an example how you could create a 30pixel high BDF font from some
|
||||
Here is an example how you could create a 30-pixel high BDF font from some
|
||||
TrueType font:
|
||||
|
||||
```bash
|
||||
@@ -31,9 +31,9 @@ otf2bdf -v -o myfont.bdf -r 72 -p 30 /path/to/font-Bold.ttf
|
||||
|
||||
## Getting otf2bdf
|
||||
|
||||
Installing the tool should be fairly straight-foward
|
||||
Installing the tool should be fairly straightforward.
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo apt-get install otf2bdf
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ If you like to compile otf2bdf, you might notice that the configure script
|
||||
uses some old way of getting the freetype configuration. There does not seem
|
||||
to be much activity on the mature code, so let's patch that first:
|
||||
|
||||
```
|
||||
```bash
|
||||
sudo apt-get install -y libfreetype6-dev pkg-config autoconf
|
||||
git clone https://github.com/jirutka/otf2bdf.git # check it out
|
||||
cd otf2bdf
|
||||
BIN
assets/sports/nba_logos/nba.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/sports/ncaa_logos/CHAMPIONSHIP.png
Normal file
|
After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
assets/sports/ncaa_logos/ELITE_8.png
Normal file
|
After Width: | Height: | Size: 459 B |
BIN
assets/sports/ncaa_logos/FINAL_4.png
Normal file
|
After Width: | Height: | Size: 545 B |
BIN
assets/sports/ncaa_logos/MARCH_MADNESS.png
Normal file
|
After Width: | Height: | Size: 496 B |
BIN
assets/sports/ncaa_logos/MOST.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
assets/sports/ncaa_logos/NCSU.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/sports/ncaa_logos/ROUND_32.png
Normal file
|
After Width: | Height: | Size: 561 B |
BIN
assets/sports/ncaa_logos/ROUND_64.png
Normal file
|
After Width: | Height: | Size: 538 B |
BIN
assets/sports/ncaa_logos/SWEET_16.png
Normal file
|
After Width: | Height: | Size: 521 B |
BIN
assets/sports/ncaa_logos/TXST.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/sports/ncaa_logos/WIS.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/sports/ncaa_mens_logos/AKFB.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
assets/sports/ncaa_mens_logos/DART.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/sports/ncaa_mens_logos/QUIN.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
assets/sports/ncaa_mens_logos/YALE.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/sports/ncaa_womens_logos/ASP.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/sports/ncaa_womens_logos/BU.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/sports/ncaa_womens_logos/CONN.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/sports/ncaa_womens_logos/DART.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/sports/ncaa_womens_logos/FRANK.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
assets/sports/ncaa_womens_logos/HARV.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/sports/ncaa_womens_logos/HC.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/sports/ncaa_womens_logos/LIU.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
assets/sports/ncaa_womens_logos/ME.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/sports/ncaa_womens_logos/NE.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/sports/ncaa_womens_logos/PSU.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/sports/ncaa_womens_logos/RMU.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/sports/ncaa_womens_logos/STA.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/sports/ncaa_womens_logos/UVM.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
assets/sports/ncaa_womens_logos/YALE.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import json
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google.auth.transport.requests import Request
|
||||
import pickle
|
||||
import requests
|
||||
|
||||
# If modifying these scopes, delete the file token.pickle.
|
||||
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
|
||||
|
||||
def load_config():
|
||||
with open('config/config.json', 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_credentials(creds, token_path):
|
||||
# Save the credentials for the next run
|
||||
with open(token_path, 'wb') as token:
|
||||
pickle.dump(creds, token)
|
||||
|
||||
def get_device_code(client_id, client_secret):
|
||||
"""Get device code for TV and Limited Input Device flow."""
|
||||
url = 'https://oauth2.googleapis.com/device/code'
|
||||
data = {
|
||||
'client_id': client_id,
|
||||
'scope': ' '.join(SCOPES)
|
||||
}
|
||||
response = requests.post(url, data=data)
|
||||
return response.json()
|
||||
|
||||
def poll_for_token(client_id, client_secret, device_code):
|
||||
"""Poll for token using device code."""
|
||||
url = 'https://oauth2.googleapis.com/token'
|
||||
data = {
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'device_code': device_code,
|
||||
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'
|
||||
}
|
||||
response = requests.post(url, data=data)
|
||||
return response.json()
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
calendar_config = config.get('calendar', {})
|
||||
|
||||
creds_file = calendar_config.get('credentials_file', 'credentials.json')
|
||||
token_file = calendar_config.get('token_file', 'token.pickle')
|
||||
|
||||
creds = None
|
||||
# The file token.pickle stores the user's access and refresh tokens
|
||||
if os.path.exists(token_file):
|
||||
print("Existing token found, but you may continue to generate a new one.")
|
||||
choice = input("Generate new token? (y/n): ")
|
||||
if choice.lower() != 'y':
|
||||
print("Keeping existing token. Exiting...")
|
||||
return
|
||||
|
||||
# If there are no (valid) credentials available, let the user log in.
|
||||
if not os.path.exists(creds_file):
|
||||
print(f"Error: No credentials file found at {creds_file}")
|
||||
print("Please download the credentials file from Google Cloud Console")
|
||||
print("1. Go to https://console.cloud.google.com")
|
||||
print("2. Create a project or select existing project")
|
||||
print("3. Enable the Google Calendar API")
|
||||
print("4. Configure the OAuth consent screen (select TV and Limited Input Device)")
|
||||
print("5. Create OAuth 2.0 credentials (TV and Limited Input Device)")
|
||||
print("6. Download the credentials and save as credentials.json")
|
||||
return
|
||||
|
||||
# Load client credentials
|
||||
with open(creds_file, 'r') as f:
|
||||
client_config = json.load(f)
|
||||
|
||||
client_id = client_config['installed']['client_id']
|
||||
client_secret = client_config['installed']['client_secret']
|
||||
|
||||
# Get device code
|
||||
device_info = get_device_code(client_id, client_secret)
|
||||
|
||||
print("\nTo authorize this application, visit:")
|
||||
print(device_info['verification_url'])
|
||||
print("\nAnd enter the code:")
|
||||
print(device_info['user_code'])
|
||||
print("\nWaiting for authorization...")
|
||||
|
||||
# Poll for token
|
||||
while True:
|
||||
token_info = poll_for_token(client_id, client_secret, device_info['device_code'])
|
||||
|
||||
if 'access_token' in token_info:
|
||||
# Create credentials object
|
||||
creds = Credentials(
|
||||
token=token_info['access_token'],
|
||||
refresh_token=token_info.get('refresh_token'),
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
scopes=SCOPES
|
||||
)
|
||||
|
||||
# Save the credentials
|
||||
save_credentials(creds, token_file)
|
||||
print(f"\nCredentials saved successfully to {token_file}")
|
||||
print("You can now run the LED Matrix display with calendar integration!")
|
||||
break
|
||||
elif token_info.get('error') == 'authorization_pending':
|
||||
import time
|
||||
time.sleep(device_info['interval'])
|
||||
else:
|
||||
print(f"\nError during authorization: {token_info.get('error')}")
|
||||
print("Please try again.")
|
||||
return
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to clear NHL cache so managers will fetch fresh data.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Add the src directory to the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
def clear_nhl_cache():
|
||||
"""Clear NHL cache to force fresh data fetch."""
|
||||
print("Clearing NHL cache...")
|
||||
|
||||
try:
|
||||
from cache_manager import CacheManager
|
||||
|
||||
# Create cache manager
|
||||
cache_manager = CacheManager()
|
||||
|
||||
# Clear NHL cache for current season
|
||||
now = datetime.now()
|
||||
season_year = now.year
|
||||
if now.month < 9:
|
||||
season_year = now.year - 1
|
||||
|
||||
cache_key = f"nhl_api_data_{season_year}"
|
||||
print(f"Clearing cache key: {cache_key}")
|
||||
|
||||
# Clear the cache
|
||||
cache_manager.clear_cache(cache_key)
|
||||
print(f"Successfully cleared cache for {cache_key}")
|
||||
|
||||
# Also clear any other NHL-related cache keys
|
||||
nhl_keys = [
|
||||
f"nhl_api_data_{season_year}",
|
||||
f"nhl_api_data_{season_year-1}",
|
||||
f"nhl_api_data_{season_year+1}",
|
||||
"nhl_live_games",
|
||||
"nhl_recent_games",
|
||||
"nhl_upcoming_games"
|
||||
]
|
||||
|
||||
for key in nhl_keys:
|
||||
try:
|
||||
cache_manager.clear_cache(key)
|
||||
print(f"Cleared cache key: {key}")
|
||||
except:
|
||||
pass # Key might not exist
|
||||
|
||||
print("NHL cache cleared successfully!")
|
||||
print("NHL managers will now fetch fresh data from ESPN API.")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Could not import cache manager: {e}")
|
||||
print("This script needs to be run on the Raspberry Pi where the cache manager is available.")
|
||||
except Exception as e:
|
||||
print(f"Error clearing cache: {e}")
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
print("=" * 50)
|
||||
print("NHL Cache Clearer")
|
||||
print("=" * 50)
|
||||
|
||||
clear_nhl_cache()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Cache clearing complete!")
|
||||
print("=" * 50)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,8 +2,90 @@
|
||||
"web_display_autostart": true,
|
||||
"schedule": {
|
||||
"enabled": true,
|
||||
"mode": "per-day",
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
"end_time": "23:00",
|
||||
"days": {
|
||||
"monday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"friday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": true,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dim_schedule": {
|
||||
"enabled": false,
|
||||
"dim_brightness": 30,
|
||||
"mode": "global",
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00",
|
||||
"days": {
|
||||
"monday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"thursday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"friday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"saturday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
},
|
||||
"sunday": {
|
||||
"enabled": true,
|
||||
"start_time": "20:00",
|
||||
"end_time": "07:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timezone": "America/Chicago",
|
||||
"location": {
|
||||
@@ -26,629 +108,31 @@
|
||||
"disable_hardware_pulsing": false,
|
||||
"inverse_colors": false,
|
||||
"show_refresh_rate": false,
|
||||
"led_rgb_sequence": "RGB",
|
||||
"limit_refresh_rate_hz": 100
|
||||
},
|
||||
"runtime": {
|
||||
"gpio_slowdown": 3
|
||||
},
|
||||
"display_durations": {
|
||||
"clock": 15,
|
||||
"weather": 30,
|
||||
"stocks": 30,
|
||||
"hourly_forecast": 30,
|
||||
"daily_forecast": 30,
|
||||
"stock_news": 20,
|
||||
"odds_ticker": 60,
|
||||
"leaderboard": 300,
|
||||
"nhl_live": 30,
|
||||
"nhl_recent": 30,
|
||||
"nhl_upcoming": 30,
|
||||
"nba_live": 30,
|
||||
"nba_recent": 30,
|
||||
"nba_upcoming": 30,
|
||||
"nfl_live": 30,
|
||||
"nfl_recent": 30,
|
||||
"nfl_upcoming": 30,
|
||||
"ncaa_fb_live": 30,
|
||||
"ncaa_fb_recent": 30,
|
||||
"ncaa_fb_upcoming": 30,
|
||||
"ncaa_baseball_live": 30,
|
||||
"ncaa_baseball_recent": 30,
|
||||
"ncaa_baseball_upcoming": 30,
|
||||
"calendar": 30,
|
||||
"youtube": 30,
|
||||
"mlb_live": 30,
|
||||
"mlb_recent": 30,
|
||||
"mlb_upcoming": 30,
|
||||
"milb_live": 30,
|
||||
"milb_recent": 30,
|
||||
"milb_upcoming": 30,
|
||||
"text_display": 10,
|
||||
"soccer_live": 30,
|
||||
"soccer_recent": 30,
|
||||
"soccer_upcoming": 30,
|
||||
"ncaam_basketball_live": 30,
|
||||
"ncaam_basketball_recent": 30,
|
||||
"ncaam_basketball_upcoming": 30,
|
||||
"music": 30,
|
||||
"of_the_day": 40,
|
||||
"news_manager": 60,
|
||||
"static_image": 10
|
||||
},
|
||||
"use_short_date_format": true
|
||||
"display_durations": {},
|
||||
"use_short_date_format": true,
|
||||
"vegas_scroll": {
|
||||
"enabled": false,
|
||||
"scroll_speed": 50,
|
||||
"separator_width": 32,
|
||||
"plugin_order": [],
|
||||
"excluded_plugins": [],
|
||||
"target_fps": 125,
|
||||
"buffer_ahead": 2
|
||||
}
|
||||
},
|
||||
"clock": {
|
||||
"plugin_system": {
|
||||
"plugins_directory": "plugin-repos",
|
||||
"auto_discover": true,
|
||||
"auto_load_enabled": true
|
||||
},
|
||||
"web-ui-info": {
|
||||
"enabled": true,
|
||||
"format": "%I:%M %p",
|
||||
"update_interval": 1
|
||||
},
|
||||
"weather": {
|
||||
"enabled": false,
|
||||
"update_interval": 1800,
|
||||
"units": "imperial",
|
||||
"display_format": "{temp}°F\n{condition}"
|
||||
},
|
||||
"stocks": {
|
||||
"enabled": false,
|
||||
"update_interval": 600,
|
||||
"scroll_speed": 1,
|
||||
"scroll_delay": 0.01,
|
||||
"toggle_chart": true,
|
||||
"dynamic_duration": true,
|
||||
"min_duration": 30,
|
||||
"max_duration": 300,
|
||||
"duration_buffer": 0.1,
|
||||
"symbols": [
|
||||
"ASTS",
|
||||
"SCHD",
|
||||
"INTC",
|
||||
"NVDA",
|
||||
"T",
|
||||
"VOO",
|
||||
"SMCI"
|
||||
],
|
||||
"display_format": "{symbol}: ${price} ({change}%)"
|
||||
},
|
||||
"crypto": {
|
||||
"enabled": false,
|
||||
"update_interval": 600,
|
||||
"symbols": [
|
||||
"BTC-USD",
|
||||
"ETH-USD"
|
||||
],
|
||||
"display_format": "{symbol}: ${price} ({change}%)"
|
||||
},
|
||||
"stock_news": {
|
||||
"enabled": false,
|
||||
"update_interval": 3600,
|
||||
"scroll_speed": 1,
|
||||
"scroll_delay": 0.01,
|
||||
"max_headlines_per_symbol": 1,
|
||||
"headlines_per_rotation": 2,
|
||||
"dynamic_duration": true,
|
||||
"min_duration": 30,
|
||||
"max_duration": 300,
|
||||
"duration_buffer": 0.1
|
||||
},
|
||||
"odds_ticker": {
|
||||
"enabled": false,
|
||||
"show_favorite_teams_only": true,
|
||||
"games_per_favorite_team": 1,
|
||||
"max_games_per_league": 5,
|
||||
"show_odds_only": false,
|
||||
"sort_order": "soonest",
|
||||
"enabled_leagues": [
|
||||
"nfl",
|
||||
"mlb",
|
||||
"ncaa_fb",
|
||||
"milb"
|
||||
],
|
||||
"update_interval": 3600,
|
||||
"scroll_speed": 1,
|
||||
"scroll_delay": 0.01,
|
||||
"loop": true,
|
||||
"future_fetch_days": 50,
|
||||
"show_channel_logos": true,
|
||||
"dynamic_duration": true,
|
||||
"min_duration": 30,
|
||||
"max_duration": 300,
|
||||
"duration_buffer": 0.1
|
||||
},
|
||||
"leaderboard": {
|
||||
"enabled": false,
|
||||
"enabled_sports": {
|
||||
"nfl": {
|
||||
"enabled": true,
|
||||
"top_teams": 10
|
||||
},
|
||||
"nba": {
|
||||
"enabled": false,
|
||||
"top_teams": 10
|
||||
},
|
||||
"mlb": {
|
||||
"enabled": false,
|
||||
"top_teams": 10
|
||||
},
|
||||
"ncaa_fb": {
|
||||
"enabled": true,
|
||||
"top_teams": 25,
|
||||
"show_ranking": true
|
||||
},
|
||||
"nhl": {
|
||||
"enabled": false,
|
||||
"top_teams": 10
|
||||
},
|
||||
"ncaam_basketball": {
|
||||
"enabled": false,
|
||||
"top_teams": 25
|
||||
},
|
||||
"ncaam_hockey": {
|
||||
"enabled": true,
|
||||
"top_teams": 10,
|
||||
"show_ranking": true
|
||||
}
|
||||
},
|
||||
"update_interval": 3600,
|
||||
"scroll_speed": 1,
|
||||
"scroll_delay": 0.01,
|
||||
"loop": false,
|
||||
"request_timeout": 30,
|
||||
"dynamic_duration": true,
|
||||
"min_duration": 30,
|
||||
"max_display_time": 600
|
||||
},
|
||||
"calendar": {
|
||||
"enabled": false,
|
||||
"credentials_file": "credentials.json",
|
||||
"token_file": "token.pickle",
|
||||
"update_interval": 3600,
|
||||
"max_events": 3,
|
||||
"calendars": [
|
||||
"birthdays"
|
||||
]
|
||||
},
|
||||
"nhl_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"show_shots_on_goal": false,
|
||||
"favorite_teams": [
|
||||
"TB"
|
||||
],
|
||||
"logo_dir": "assets/sports/nhl_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"nhl_live": true,
|
||||
"nhl_recent": true,
|
||||
"nhl_upcoming": true
|
||||
}
|
||||
},
|
||||
"nba_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"favorite_teams": [
|
||||
"DAL"
|
||||
],
|
||||
"logo_dir": "assets/sports/nba_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"nba_live": true,
|
||||
"nba_recent": true,
|
||||
"nba_upcoming": true
|
||||
}
|
||||
},
|
||||
"wnba_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"favorite_teams": [
|
||||
"CHI"
|
||||
],
|
||||
"logo_dir": "assets/sports/wnba_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"wnba_live": true,
|
||||
"wnba_recent": true,
|
||||
"wnba_upcoming": true
|
||||
}
|
||||
},
|
||||
"nfl_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 30,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"favorite_teams": [
|
||||
"TB",
|
||||
"DAL"
|
||||
],
|
||||
"logo_dir": "assets/sports/nfl_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"nfl_live": true,
|
||||
"nfl_recent": true,
|
||||
"nfl_upcoming": true
|
||||
}
|
||||
},
|
||||
"ncaa_fb_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"favorite_teams": [
|
||||
"UGA",
|
||||
"AP_TOP_25"
|
||||
],
|
||||
"logo_dir": "assets/sports/ncaa_logos",
|
||||
"show_records": true,
|
||||
"show_ranking": true,
|
||||
"display_modes": {
|
||||
"ncaa_fb_live": true,
|
||||
"ncaa_fb_recent": true,
|
||||
"ncaa_fb_upcoming": true
|
||||
}
|
||||
},
|
||||
"ncaa_baseball_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 30,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"show_series_summary": false,
|
||||
"favorite_teams": [
|
||||
"UGA",
|
||||
"AUB"
|
||||
],
|
||||
"logo_dir": "assets/sports/ncaa_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"ncaa_baseball_live": true,
|
||||
"ncaa_baseball_recent": true,
|
||||
"ncaa_baseball_upcoming": true
|
||||
}
|
||||
},
|
||||
"ncaam_basketball_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"favorite_teams": [
|
||||
"UGA",
|
||||
"AUB"
|
||||
],
|
||||
"logo_dir": "assets/sports/ncaa_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"ncaam_basketball_live": true,
|
||||
"ncaam_basketball_recent": true,
|
||||
"ncaam_basketball_upcoming": true
|
||||
}
|
||||
},
|
||||
"ncaaw_basketball_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"favorite_teams": [
|
||||
"UGA",
|
||||
"AUB"
|
||||
],
|
||||
"logo_dir": "assets/sports/ncaa_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"ncaaw_basketball_live": true,
|
||||
"ncaaw_basketball_recent": true,
|
||||
"ncaaw_basketball_upcoming": true
|
||||
}
|
||||
},
|
||||
"ncaam_hockey_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"show_shots_on_goal": false,
|
||||
"favorite_teams": [
|
||||
"RIT"
|
||||
],
|
||||
"logo_dir": "assets/sports/ncaa_logos",
|
||||
"show_records": true,
|
||||
"show_ranking": true,
|
||||
"display_modes": {
|
||||
"ncaam_hockey_live": true,
|
||||
"ncaam_hockey_recent": true ,
|
||||
"ncaam_hockey_upcoming": true
|
||||
}
|
||||
},
|
||||
"ncaaw_hockey_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 20,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"show_shots_on_goal": false,
|
||||
"favorite_teams": [
|
||||
"RIT"
|
||||
],
|
||||
"logo_dir": "assets/sports/ncaa_logos",
|
||||
"show_records": false,
|
||||
"show_ranking": false,
|
||||
"display_modes": {
|
||||
"ncaaw_hockey_live": true,
|
||||
"ncaaw_hockey_recent": true ,
|
||||
"ncaaw_hockey_upcoming": true
|
||||
}
|
||||
},
|
||||
"youtube": {
|
||||
"enabled": false,
|
||||
"update_interval": 3600
|
||||
},
|
||||
"mlb_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": false,
|
||||
"live_game_duration": 30,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"live_odds_update_interval": 3600,
|
||||
"odds_update_interval": 3600,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"show_all_live": false,
|
||||
"show_series_summary": false,
|
||||
"favorite_teams": [
|
||||
"TB",
|
||||
"TEX"
|
||||
],
|
||||
"logo_dir": "assets/sports/mlb_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"mlb_live": true,
|
||||
"mlb_recent": true,
|
||||
"mlb_upcoming": true
|
||||
}
|
||||
},
|
||||
"milb_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": false,
|
||||
"live_game_duration": 30,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"favorite_teams": [
|
||||
"TAM"
|
||||
],
|
||||
"logo_dir": "assets/sports/milb_logos",
|
||||
"show_records": true,
|
||||
"upcoming_fetch_days": 7,
|
||||
"display_modes": {
|
||||
"milb_live": true,
|
||||
"milb_recent": true,
|
||||
"milb_upcoming": true
|
||||
}
|
||||
},
|
||||
"text_display": {
|
||||
"enabled": false,
|
||||
"text": "Subscribe to ChuckBuilds",
|
||||
"font_path": "assets/fonts/press-start-2p.ttf",
|
||||
"font_size": 8,
|
||||
"scroll": true,
|
||||
"scroll_speed": 40,
|
||||
"text_color": [
|
||||
255,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"background_color": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"scroll_gap_width": 32
|
||||
},
|
||||
"soccer_scoreboard": {
|
||||
"enabled": false,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 30,
|
||||
"show_odds": true,
|
||||
"test_mode": false,
|
||||
"update_interval_seconds": 3600,
|
||||
"live_update_interval": 30,
|
||||
"recent_update_interval": 3600,
|
||||
"upcoming_update_interval": 3600,
|
||||
"recent_games_to_show": 1,
|
||||
"upcoming_games_to_show": 1,
|
||||
"show_favorite_teams_only": true,
|
||||
"favorite_teams": [
|
||||
"DAL"
|
||||
],
|
||||
"leagues": [
|
||||
"usa.1"
|
||||
],
|
||||
"logo_dir": "assets/sports/soccer_logos",
|
||||
"show_records": true,
|
||||
"display_modes": {
|
||||
"soccer_live": true,
|
||||
"soccer_recent": true,
|
||||
"soccer_upcoming": true
|
||||
}
|
||||
},
|
||||
"music": {
|
||||
"enabled": false,
|
||||
"preferred_source": "ytm",
|
||||
"YTM_COMPANION_URL": "http://192.168.86.12:9863",
|
||||
"POLLING_INTERVAL_SECONDS": 1,
|
||||
"skip_when_nothing_playing": true,
|
||||
"skip_delay_seconds": 2,
|
||||
"live_priority": true,
|
||||
"live_game_duration": 30
|
||||
},
|
||||
"of_the_day": {
|
||||
"enabled": false,
|
||||
"display_rotate_interval": 20,
|
||||
"update_interval": 3600,
|
||||
"subtitle_rotate_interval": 10,
|
||||
"category_order": [
|
||||
"word_of_the_day",
|
||||
"slovenian_word_of_the_day"
|
||||
],
|
||||
"categories": {
|
||||
"word_of_the_day": {
|
||||
"enabled": true,
|
||||
"data_file": "of_the_day/word_of_the_day.json",
|
||||
"display_name": "Word of the Day"
|
||||
},
|
||||
"slovenian_word_of_the_day": {
|
||||
"enabled": true,
|
||||
"data_file": "of_the_day/slovenian_word_of_the_day.json",
|
||||
"display_name": "Slovenian Word of the Day"
|
||||
}
|
||||
}
|
||||
},
|
||||
"news_manager": {
|
||||
"enabled": false,
|
||||
"update_interval": 300,
|
||||
"scroll_speed": 1,
|
||||
"scroll_delay": 0.01,
|
||||
"headlines_per_feed": 2,
|
||||
"enabled_feeds": [
|
||||
"NFL",
|
||||
"NCAA FB",
|
||||
"F1",
|
||||
"BBC F1"
|
||||
],
|
||||
"custom_feeds": {
|
||||
"F1": "https://www.espn.com/espn/rss/rpm/news",
|
||||
"BBC F1": "http://feeds.bbci.co.uk/sport/formula1/rss.xml"
|
||||
},
|
||||
"rotation_enabled": true,
|
||||
"rotation_threshold": 3,
|
||||
"dynamic_duration": true,
|
||||
"min_duration": 30,
|
||||
"max_duration": 300,
|
||||
"duration_buffer": 0.1,
|
||||
"font_size": 8,
|
||||
"font_path": "assets/fonts/PressStart2P-Regular.ttf",
|
||||
"text_color": [
|
||||
255,
|
||||
255,
|
||||
255
|
||||
],
|
||||
"separator_color": [
|
||||
255,
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"static_image": {
|
||||
"enabled": false,
|
||||
"image_path": "assets/static_images/default.png",
|
||||
"fit_to_display": true,
|
||||
"preserve_aspect_ratio": true,
|
||||
"background_color": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
"display_duration": 10
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"weather": {
|
||||
"ledmatrix-weather": {
|
||||
"api_key": "YOUR_OPENWEATHERMAP_API_KEY"
|
||||
},
|
||||
"youtube": {
|
||||
@@ -10,5 +10,8 @@
|
||||
"SPOTIFY_CLIENT_ID": "YOUR_SPOTIFY_CLIENT_ID_HERE",
|
||||
"SPOTIFY_CLIENT_SECRET": "YOUR_SPOTIFY_CLIENT_SECRET_HERE",
|
||||
"SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
|
||||
},
|
||||
"github": {
|
||||
"api_token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LED Matrix Web Interface Sudo Configuration Script
|
||||
# This script configures passwordless sudo access for the web interface user
|
||||
|
||||
set -e
|
||||
|
||||
echo "Configuring passwordless sudo access for LED Matrix Web Interface..."
|
||||
|
||||
# Get the current user (should be the user running the web interface)
|
||||
WEB_USER=$(whoami)
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "Detected web interface user: $WEB_USER"
|
||||
echo "Project directory: $PROJECT_DIR"
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
echo "Error: This script should not be run as root."
|
||||
echo "Run it as the user that will be running the web interface."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the full paths to commands
|
||||
PYTHON_PATH=$(which python3)
|
||||
SYSTEMCTL_PATH=$(which systemctl)
|
||||
REBOOT_PATH=$(which reboot)
|
||||
POWEROFF_PATH=$(which poweroff)
|
||||
BASH_PATH=$(which bash)
|
||||
|
||||
echo "Command paths:"
|
||||
echo " Python: $PYTHON_PATH"
|
||||
echo " Systemctl: $SYSTEMCTL_PATH"
|
||||
echo " Reboot: $REBOOT_PATH"
|
||||
echo " Poweroff: $POWEROFF_PATH"
|
||||
echo " Bash: $BASH_PATH"
|
||||
|
||||
# Create a temporary sudoers file
|
||||
TEMP_SUDOERS="/tmp/ledmatrix_web_sudoers_$$"
|
||||
|
||||
cat > "$TEMP_SUDOERS" << EOF
|
||||
# LED Matrix Web Interface passwordless sudo configuration
|
||||
# This allows the web interface user to run specific commands without a password
|
||||
|
||||
# Allow $WEB_USER to run specific commands without a password for the LED Matrix web interface
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $REBOOT_PATH
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $POWEROFF_PATH
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH start ledmatrix.service
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH stop ledmatrix.service
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH restart ledmatrix.service
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH enable ledmatrix.service
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH disable ledmatrix.service
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $SYSTEMCTL_PATH status ledmatrix.service
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $PYTHON_PATH $PROJECT_DIR/display_controller.py
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_DIR/start_display.sh
|
||||
$WEB_USER ALL=(ALL) NOPASSWD: $BASH_PATH $PROJECT_DIR/stop_display.sh
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "Generated sudoers configuration:"
|
||||
echo "--------------------------------"
|
||||
cat "$TEMP_SUDOERS"
|
||||
echo "--------------------------------"
|
||||
|
||||
echo ""
|
||||
echo "This configuration will allow the web interface to:"
|
||||
echo "- Start/stop/restart the ledmatrix service"
|
||||
echo "- Enable/disable the ledmatrix service"
|
||||
echo "- Check service status"
|
||||
echo "- Run display_controller.py directly"
|
||||
echo "- Execute start_display.sh and stop_display.sh"
|
||||
echo "- Reboot and shutdown the system"
|
||||
echo ""
|
||||
|
||||
# Ask for confirmation
|
||||
read -p "Do you want to apply this configuration? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Configuration cancelled."
|
||||
rm -f "$TEMP_SUDOERS"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Apply the configuration using visudo
|
||||
echo "Applying sudoers configuration..."
|
||||
if sudo cp "$TEMP_SUDOERS" /etc/sudoers.d/ledmatrix_web; then
|
||||
echo "Configuration applied successfully!"
|
||||
echo ""
|
||||
echo "Testing sudo access..."
|
||||
|
||||
# Test a few commands
|
||||
if sudo -n systemctl status ledmatrix.service > /dev/null 2>&1; then
|
||||
echo "✓ systemctl status ledmatrix.service - OK"
|
||||
else
|
||||
echo "✗ systemctl status ledmatrix.service - Failed"
|
||||
fi
|
||||
|
||||
if sudo -n test -f "$PROJECT_DIR/start_display.sh"; then
|
||||
echo "✓ File access test - OK"
|
||||
else
|
||||
echo "✗ File access test - Failed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Configuration complete! The web interface should now be able to:"
|
||||
echo "- Execute system commands without password prompts"
|
||||
echo "- Start and stop the LED matrix display"
|
||||
echo "- Restart the system if needed"
|
||||
echo ""
|
||||
echo "You may need to restart the web interface service for changes to take effect:"
|
||||
echo " sudo systemctl restart ledmatrix-web.service"
|
||||
|
||||
else
|
||||
echo "Error: Failed to apply sudoers configuration."
|
||||
echo "You may need to run this script with sudo privileges."
|
||||
rm -f "$TEMP_SUDOERS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f "$TEMP_SUDOERS"
|
||||
|
||||
echo ""
|
||||
echo "Configuration script completed successfully!"
|
||||
0
data/.gitkeep
Normal file
1032
docs/ADVANCED_FEATURES.md
Normal file
817
docs/ADVANCED_PLUGIN_DEVELOPMENT.md
Normal file
@@ -0,0 +1,817 @@
|
||||
# Advanced Plugin Development
|
||||
|
||||
Advanced patterns, examples, and best practices for developing LEDMatrix plugins.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Using Weather Icons](#using-weather-icons)
|
||||
- [Implementing Scrolling with Deferred Updates](#implementing-scrolling-with-deferred-updates)
|
||||
- [Cache Strategy Patterns](#cache-strategy-patterns)
|
||||
- [Font Management and Overrides](#font-management-and-overrides)
|
||||
- [Error Handling Best Practices](#error-handling-best-practices)
|
||||
- [Performance Optimization](#performance-optimization)
|
||||
- [Testing Plugins with Mocks](#testing-plugins-with-mocks)
|
||||
- [Inter-Plugin Communication](#inter-plugin-communication)
|
||||
- [Live Priority Implementation](#live-priority-implementation)
|
||||
- [Dynamic Duration Support](#dynamic-duration-support)
|
||||
|
||||
---
|
||||
|
||||
## Using Weather Icons
|
||||
|
||||
The Display Manager provides built-in weather icon drawing methods for easy visual representation of weather conditions.
|
||||
|
||||
### Basic Weather Icon Usage
|
||||
|
||||
```python
|
||||
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)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementing Scrolling with Deferred Updates
|
||||
|
||||
For plugins that scroll content (tickers, news feeds, etc.), use scrolling state management to coordinate with the display system.
|
||||
|
||||
### Basic Scrolling Implementation
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
|
||||
# Mark as scrolling
|
||||
self.display_manager.set_scrolling_state(True)
|
||||
|
||||
try:
|
||||
# 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)
|
||||
```
|
||||
|
||||
### Deferred Updates During Scrolling
|
||||
|
||||
Use `defer_update()` to queue non-critical updates until scrolling completes:
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
# Critical update - do immediately
|
||||
self.fetch_latest_data()
|
||||
|
||||
# Non-critical metadata update - defer until not scrolling
|
||||
self.display_manager.defer_update(
|
||||
lambda: self.update_cache_metadata(),
|
||||
priority=1
|
||||
)
|
||||
|
||||
# Low priority cleanup - defer
|
||||
self.display_manager.defer_update(
|
||||
lambda: self.cleanup_old_data(),
|
||||
priority=2
|
||||
)
|
||||
```
|
||||
|
||||
### Checking Scroll State
|
||||
|
||||
Check if currently scrolling before performing expensive operations:
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
# Only do expensive operations when not scrolling
|
||||
if not self.display_manager.is_currently_scrolling():
|
||||
self.perform_expensive_operation()
|
||||
else:
|
||||
# Defer until scrolling stops
|
||||
self.display_manager.defer_update(
|
||||
lambda: self.perform_expensive_operation(),
|
||||
priority=0
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Strategy Patterns
|
||||
|
||||
Use appropriate cache strategies for different data types to optimize performance and reduce API calls.
|
||||
|
||||
### Basic Caching Pattern
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
cache_key = f"{self.plugin_id}_data"
|
||||
|
||||
# Try to get from cache first
|
||||
cached = self.cache_manager.get(cache_key, max_age=3600)
|
||||
if cached:
|
||||
self.data = cached
|
||||
self.logger.debug("Using cached data")
|
||||
return
|
||||
|
||||
# Fetch from API if not cached
|
||||
try:
|
||||
self.data = self._fetch_from_api()
|
||||
self.cache_manager.set(cache_key, self.data)
|
||||
self.logger.info("Fetched and cached new data")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to fetch data: {e}")
|
||||
# Use stale cache if available (re-fetch with large max_age to bypass expiration)
|
||||
expired_cached = self.cache_manager.get(cache_key, max_age=31536000) # 1 year
|
||||
if expired_cached:
|
||||
self.data = expired_cached
|
||||
self.logger.warning("Using stale cached data due to API error")
|
||||
```
|
||||
|
||||
### Using Cache Strategies
|
||||
|
||||
For automatic TTL selection based on data type:
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
cache_key = f"{self.plugin_id}_weather"
|
||||
|
||||
# Automatically uses appropriate cache duration for weather data
|
||||
cached = self.cache_manager.get_cached_data_with_strategy(
|
||||
cache_key,
|
||||
data_type="weather"
|
||||
)
|
||||
|
||||
if cached:
|
||||
self.data = cached
|
||||
return
|
||||
|
||||
# Fetch new data
|
||||
self.data = self._fetch_from_api()
|
||||
self.cache_manager.set(cache_key, self.data)
|
||||
```
|
||||
|
||||
### Sport-Specific Caching
|
||||
|
||||
For sports plugins, use sport-specific cache strategies:
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
sport_key = "nhl"
|
||||
cache_key = f"{self.plugin_id}_{sport_key}_games"
|
||||
|
||||
# Uses sport-specific live_update_interval from config
|
||||
cached = self.cache_manager.get_background_cached_data(
|
||||
cache_key,
|
||||
sport_key=sport_key
|
||||
)
|
||||
|
||||
if cached:
|
||||
self.games = cached
|
||||
return
|
||||
|
||||
# Fetch new games
|
||||
self.games = self._fetch_games(sport_key)
|
||||
self.cache_manager.set(cache_key, self.games)
|
||||
```
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
Clear cache when needed:
|
||||
|
||||
```python
|
||||
def on_config_change(self, new_config):
|
||||
# Clear cache when API key changes
|
||||
if new_config.get('api_key') != self.config.get('api_key'):
|
||||
self.cache_manager.clear_cache(f"{self.plugin_id}_data")
|
||||
self.logger.info("Cleared cache due to API key change")
|
||||
|
||||
super().on_config_change(new_config)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Font Management and Overrides
|
||||
|
||||
Use the Font Manager for advanced font handling and user customization.
|
||||
|
||||
### Using Different Fonts
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
|
||||
# Use regular font for title
|
||||
self.display_manager.draw_text(
|
||||
"Title",
|
||||
x=10, y=5,
|
||||
font=self.display_manager.regular_font,
|
||||
color=(255, 255, 255)
|
||||
)
|
||||
|
||||
# Use small font for details
|
||||
self.display_manager.draw_text(
|
||||
"Details",
|
||||
x=10, y=20,
|
||||
font=self.display_manager.small_font,
|
||||
color=(200, 200, 200)
|
||||
)
|
||||
|
||||
# Use calendar font for compact text
|
||||
self.display_manager.draw_text(
|
||||
"Compact",
|
||||
x=10, y=30,
|
||||
font=self.display_manager.calendar_font,
|
||||
color=(150, 150, 150)
|
||||
)
|
||||
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
### Measuring Text
|
||||
|
||||
Calculate text dimensions for layout:
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
|
||||
text = "Hello, World!"
|
||||
font = self.display_manager.regular_font
|
||||
|
||||
# Get text dimensions
|
||||
text_width = self.display_manager.get_text_width(text, font)
|
||||
font_height = self.display_manager.get_font_height(font)
|
||||
|
||||
# Center text horizontally
|
||||
x = (self.display_manager.width - text_width) // 2
|
||||
|
||||
# Center text vertically
|
||||
y = (self.display_manager.height - font_height) // 2
|
||||
|
||||
self.display_manager.draw_text(text, x=x, y=y, font=font)
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
### Multi-line Text
|
||||
|
||||
Render multiple lines of text:
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
|
||||
lines = [
|
||||
"Line 1",
|
||||
"Line 2",
|
||||
"Line 3"
|
||||
]
|
||||
|
||||
font = self.display_manager.small_font
|
||||
font_height = self.display_manager.get_font_height(font)
|
||||
y = 5
|
||||
|
||||
for line in lines:
|
||||
# Center each line
|
||||
text_width = self.display_manager.get_text_width(line, font)
|
||||
x = (self.display_manager.width - text_width) // 2
|
||||
|
||||
self.display_manager.draw_text(line, x=x, y=y, font=font)
|
||||
y += font_height + 2 # Add spacing between lines
|
||||
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Best Practices
|
||||
|
||||
Implement robust error handling to ensure plugins fail gracefully.
|
||||
|
||||
### API Error Handling
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
cache_key = f"{self.plugin_id}_data"
|
||||
|
||||
try:
|
||||
# Try to fetch from API
|
||||
self.data = self._fetch_from_api()
|
||||
self.cache_manager.set(cache_key, self.data)
|
||||
self.logger.info("Successfully updated data")
|
||||
except requests.exceptions.Timeout:
|
||||
self.logger.warning("API request timed out, using cached data")
|
||||
cached = self.cache_manager.get(cache_key, max_age=7200) # Use older cache
|
||||
if cached:
|
||||
self.data = cached
|
||||
else:
|
||||
self.data = None
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"API request failed: {e}")
|
||||
# Try to use cached data
|
||||
cached = self.cache_manager.get(cache_key, max_age=7200)
|
||||
if cached:
|
||||
self.data = cached
|
||||
self.logger.info("Using cached data due to API error")
|
||||
else:
|
||||
self.data = None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error in update(): {e}", exc_info=True)
|
||||
self.data = None
|
||||
```
|
||||
|
||||
### Display Error Handling
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
try:
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
|
||||
# Check if we have data
|
||||
if not self.data:
|
||||
self._display_no_data()
|
||||
return
|
||||
|
||||
# Render main content
|
||||
self._render_content()
|
||||
self.display_manager.update_display()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in display(): {e}", exc_info=True)
|
||||
# Show error message to user
|
||||
try:
|
||||
self.display_manager.clear()
|
||||
self.display_manager.draw_text(
|
||||
"Error",
|
||||
x=10, y=16,
|
||||
color=(255, 0, 0)
|
||||
)
|
||||
self.display_manager.update_display()
|
||||
except Exception:
|
||||
# If even error display fails, log and continue
|
||||
self.logger.critical("Failed to display error message")
|
||||
|
||||
def _display_no_data(self):
|
||||
"""Display a message when no data is available."""
|
||||
self.display_manager.clear()
|
||||
self.display_manager.draw_text(
|
||||
"No data",
|
||||
x=10, y=16,
|
||||
color=(128, 128, 128)
|
||||
)
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
### Validation Error Handling
|
||||
|
||||
```python
|
||||
def validate_config(self) -> bool:
|
||||
"""Validate plugin configuration."""
|
||||
try:
|
||||
# Check required fields
|
||||
required_fields = ['api_key', 'city']
|
||||
for field in required_fields:
|
||||
if field not in self.config or not self.config[field]:
|
||||
self.logger.error(f"Missing required field: {field}")
|
||||
return False
|
||||
|
||||
# Validate field types
|
||||
if not isinstance(self.config.get('display_duration'), (int, float)):
|
||||
self.logger.error("display_duration must be a number")
|
||||
return False
|
||||
|
||||
# Validate ranges
|
||||
duration = self.config.get('display_duration', 15)
|
||||
if duration < 1 or duration > 300:
|
||||
self.logger.error("display_duration must be between 1 and 300 seconds")
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error validating config: {e}", exc_info=True)
|
||||
return False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
Optimize plugin performance for smooth operation on Raspberry Pi hardware.
|
||||
|
||||
### Efficient Data Fetching
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
# Only fetch if cache is stale
|
||||
cache_key = f"{self.plugin_id}_data"
|
||||
cached = self.cache_manager.get(cache_key, max_age=3600)
|
||||
|
||||
if cached and self._is_data_fresh(cached):
|
||||
self.data = cached
|
||||
return
|
||||
|
||||
# Fetch only what's needed
|
||||
try:
|
||||
# Use appropriate cache strategy
|
||||
self.data = self._fetch_minimal_data()
|
||||
self.cache_manager.set(cache_key, self.data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Update failed: {e}")
|
||||
# Use stale cache if available (re-fetch with large max_age to bypass expiration)
|
||||
expired_cached = self.cache_manager.get(cache_key, max_age=31536000) # 1 year
|
||||
if expired_cached:
|
||||
self.data = expired_cached
|
||||
self.logger.warning("Using stale cached data due to update failure")
|
||||
```
|
||||
|
||||
### Optimized Rendering
|
||||
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
# Only clear if necessary
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
else:
|
||||
# Reuse existing canvas when possible
|
||||
pass
|
||||
|
||||
# Batch drawing operations
|
||||
self._draw_background()
|
||||
self._draw_content()
|
||||
self._draw_overlay()
|
||||
|
||||
# Single update call at the end
|
||||
self.display_manager.update_display()
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
```python
|
||||
def cleanup(self):
|
||||
"""Clean up resources to free memory."""
|
||||
# Clear large data structures
|
||||
if hasattr(self, 'large_cache'):
|
||||
self.large_cache.clear()
|
||||
|
||||
# Close connections
|
||||
if hasattr(self, 'api_client'):
|
||||
self.api_client.close()
|
||||
|
||||
# Stop threads
|
||||
if hasattr(self, 'worker_thread'):
|
||||
self.worker_thread.stop()
|
||||
|
||||
super().cleanup()
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```python
|
||||
def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
|
||||
super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
|
||||
self._heavy_resource = None # Load on demand
|
||||
|
||||
def _get_heavy_resource(self):
|
||||
"""Lazy load expensive resource."""
|
||||
if self._heavy_resource is None:
|
||||
self._heavy_resource = self._load_expensive_resource()
|
||||
return self._heavy_resource
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plugins with Mocks
|
||||
|
||||
Use mock objects for testing plugins without hardware dependencies.
|
||||
|
||||
### Basic Mock Setup
|
||||
|
||||
```python
|
||||
from src.plugin_system.testing.mocks import MockDisplayManager, MockCacheManager, MockPluginManager
|
||||
|
||||
def test_plugin_display():
|
||||
# Create mocks
|
||||
display_manager = MockDisplayManager()
|
||||
cache_manager = MockCacheManager()
|
||||
plugin_manager = MockPluginManager()
|
||||
|
||||
# Create plugin instance
|
||||
config = {"enabled": True, "display_duration": 15}
|
||||
plugin = MyPlugin("my-plugin", config, display_manager, cache_manager, plugin_manager)
|
||||
|
||||
# Test display
|
||||
plugin.display(force_clear=True)
|
||||
|
||||
# Verify display calls
|
||||
assert len(display_manager.draw_calls) > 0
|
||||
assert display_manager.draw_calls[0]['text'] == "Hello"
|
||||
```
|
||||
|
||||
### Testing Cache Behavior
|
||||
|
||||
```python
|
||||
def test_plugin_caching():
|
||||
cache_manager = MockCacheManager()
|
||||
plugin = MyPlugin("my-plugin", config, display_manager, cache_manager, plugin_manager)
|
||||
|
||||
# Test cache miss
|
||||
plugin.update()
|
||||
assert len(cache_manager.get_calls) > 0
|
||||
assert len(cache_manager.set_calls) > 0
|
||||
|
||||
# Test cache hit
|
||||
cache_manager.set("my-plugin_data", {"test": "data"})
|
||||
plugin.update()
|
||||
# Verify no API call was made
|
||||
```
|
||||
|
||||
### Testing Error Handling
|
||||
|
||||
```python
|
||||
def test_error_handling():
|
||||
display_manager = MockDisplayManager()
|
||||
cache_manager = MockCacheManager()
|
||||
plugin = MyPlugin("my-plugin", config, display_manager, cache_manager, plugin_manager)
|
||||
|
||||
# Simulate API error
|
||||
with patch('plugin._fetch_from_api', side_effect=Exception("API Error")):
|
||||
plugin.update()
|
||||
# Verify plugin handles error gracefully
|
||||
assert plugin.data is not None or hasattr(plugin, 'error_state')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inter-Plugin Communication
|
||||
|
||||
Plugins can communicate with each other through the Plugin Manager.
|
||||
|
||||
### Getting Data from Another Plugin
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
# Get weather data from weather plugin
|
||||
weather_plugin = self.plugin_manager.get_plugin("weather")
|
||||
if weather_plugin and hasattr(weather_plugin, 'current_temp'):
|
||||
self.weather_temp = weather_plugin.current_temp
|
||||
self.logger.info(f"Got temperature from weather plugin: {self.weather_temp}")
|
||||
```
|
||||
|
||||
### Checking Plugin Status
|
||||
|
||||
```python
|
||||
def update(self):
|
||||
# Check if another plugin is enabled
|
||||
enabled_plugins = self.plugin_manager.get_enabled_plugins()
|
||||
if "weather" in enabled_plugins:
|
||||
# Weather plugin is available
|
||||
weather_plugin = self.plugin_manager.get_plugin("weather")
|
||||
if weather_plugin:
|
||||
# Use weather data
|
||||
pass
|
||||
```
|
||||
|
||||
### Sharing Data Between Plugins
|
||||
|
||||
```python
|
||||
class MyPlugin(BasePlugin):
|
||||
def __init__(self, ...):
|
||||
super().__init__(...)
|
||||
self.shared_data = {} # Data accessible to other plugins
|
||||
|
||||
def update(self):
|
||||
self.shared_data['last_update'] = time.time()
|
||||
self.shared_data['status'] = 'active'
|
||||
|
||||
# In another plugin
|
||||
def update(self):
|
||||
my_plugin = self.plugin_manager.get_plugin("my-plugin")
|
||||
if my_plugin and hasattr(my_plugin, 'shared_data'):
|
||||
status = my_plugin.shared_data.get('status')
|
||||
self.logger.info(f"MyPlugin status: {status}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Live Priority Implementation
|
||||
|
||||
Implement live priority to automatically take over the display when your plugin has urgent content.
|
||||
|
||||
### Basic Live Priority
|
||||
|
||||
```python
|
||||
class MyPlugin(BasePlugin):
|
||||
def __init__(self, ...):
|
||||
super().__init__(...)
|
||||
# Enable live priority in config
|
||||
# "live_priority": true
|
||||
|
||||
def has_live_content(self) -> bool:
|
||||
"""Check if plugin has live content."""
|
||||
# Check for live games, breaking news, etc.
|
||||
return hasattr(self, 'live_items') and len(self.live_items) > 0
|
||||
|
||||
def get_live_modes(self) -> List[str]:
|
||||
"""Return modes to show during live priority."""
|
||||
return ['live_mode'] # Only show live mode, not other modes
|
||||
```
|
||||
|
||||
### Sports Plugin Example
|
||||
|
||||
```python
|
||||
class SportsPlugin(BasePlugin):
|
||||
def has_live_content(self) -> bool:
|
||||
"""Check if there are any live games."""
|
||||
if not hasattr(self, 'games'):
|
||||
return False
|
||||
|
||||
for game in self.games:
|
||||
if game.get('status') == 'live':
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_live_modes(self) -> List[str]:
|
||||
"""Only show live game modes during live priority."""
|
||||
return ['nhl_live', 'nba_live'] # Exclude recent/upcoming modes
|
||||
```
|
||||
|
||||
### News Plugin Example
|
||||
|
||||
```python
|
||||
class NewsPlugin(BasePlugin):
|
||||
def has_live_content(self) -> bool:
|
||||
"""Check for breaking news."""
|
||||
if not hasattr(self, 'headlines'):
|
||||
return False
|
||||
|
||||
# Check for breaking news flag
|
||||
for headline in self.headlines:
|
||||
if headline.get('breaking', False):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_live_modes(self) -> List[str]:
|
||||
"""Show breaking news mode during live priority."""
|
||||
return ['breaking_news']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Duration Support
|
||||
|
||||
Implement dynamic duration to extend display time until content cycle completes.
|
||||
|
||||
### Basic Dynamic Duration
|
||||
|
||||
```python
|
||||
class MyPlugin(BasePlugin):
|
||||
def __init__(self, ...):
|
||||
super().__init__(...)
|
||||
self.current_step = 0
|
||||
self.total_steps = 5
|
||||
|
||||
def supports_dynamic_duration(self) -> bool:
|
||||
"""Enable dynamic duration in config."""
|
||||
return self.config.get('dynamic_duration', {}).get('enabled', False)
|
||||
|
||||
def is_cycle_complete(self) -> bool:
|
||||
"""Return True when all content has been shown."""
|
||||
return self.current_step >= self.total_steps
|
||||
|
||||
def reset_cycle_state(self) -> None:
|
||||
"""Reset cycle tracking when starting new display session."""
|
||||
self.current_step = 0
|
||||
|
||||
def display(self, force_clear=False):
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
self.reset_cycle_state()
|
||||
|
||||
# Display current step
|
||||
self._display_step(self.current_step)
|
||||
self.display_manager.update_display()
|
||||
|
||||
# Advance to next step
|
||||
self.current_step += 1
|
||||
```
|
||||
|
||||
### Scrolling Content Example
|
||||
|
||||
```python
|
||||
class ScrollingPlugin(BasePlugin):
|
||||
def __init__(self, ...):
|
||||
super().__init__(...)
|
||||
self.scroll_position = 0
|
||||
self.scroll_complete = False
|
||||
|
||||
def supports_dynamic_duration(self) -> bool:
|
||||
return True
|
||||
|
||||
def is_cycle_complete(self) -> bool:
|
||||
"""Return True when scrolling is complete."""
|
||||
return self.scroll_complete
|
||||
|
||||
def reset_cycle_state(self) -> None:
|
||||
"""Reset scroll state."""
|
||||
self.scroll_position = 0
|
||||
self.scroll_complete = False
|
||||
|
||||
def display(self, force_clear=False):
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
self.reset_cycle_state()
|
||||
|
||||
# Scroll content
|
||||
text = "Long scrolling message..."
|
||||
text_width = self.display_manager.get_text_width(text, self.display_manager.regular_font)
|
||||
|
||||
if self.scroll_position < -text_width:
|
||||
# Scrolling complete
|
||||
self.scroll_complete = True
|
||||
else:
|
||||
self.display_manager.clear()
|
||||
self.display_manager.draw_text(
|
||||
text,
|
||||
x=self.scroll_position,
|
||||
y=16
|
||||
)
|
||||
self.display_manager.update_display()
|
||||
self.scroll_position -= 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Plugin API Reference](PLUGIN_API_REFERENCE.md) - Complete API documentation
|
||||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - Development workflow
|
||||
- [Plugin Architecture Spec](PLUGIN_ARCHITECTURE_SPEC.md) - System architecture
|
||||
- [BasePlugin Source](../src/plugin_system/base_plugin.py) - Base class implementation
|
||||
|
||||
306
docs/CONFIG_DEBUGGING.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Configuration Debugging Guide
|
||||
|
||||
This guide helps troubleshoot configuration issues in LEDMatrix.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Main Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `config/config.json` | Main configuration |
|
||||
| `config/config_secrets.json` | API keys and sensitive data |
|
||||
| `config/config.template.json` | Template for new installations |
|
||||
|
||||
### Plugin Configuration
|
||||
|
||||
Each plugin's configuration is a top-level key in `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"football-scoreboard": {
|
||||
"enabled": true,
|
||||
"display_duration": 30,
|
||||
"nfl": {
|
||||
"enabled": true,
|
||||
"live_priority": false
|
||||
}
|
||||
},
|
||||
"odds-ticker": {
|
||||
"enabled": true,
|
||||
"display_duration": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Schema Validation
|
||||
|
||||
Plugins define their configuration schema in `config_schema.json`. This enables:
|
||||
- Automatic default value population
|
||||
- Configuration validation
|
||||
- Web UI form generation
|
||||
|
||||
### Missing Schema Warning
|
||||
|
||||
If a plugin doesn't have `config_schema.json`, you'll see:
|
||||
|
||||
```
|
||||
WARNING - Plugin 'my-plugin' has no config_schema.json - configuration will not be validated.
|
||||
```
|
||||
|
||||
**Fix**: Add a `config_schema.json` to your plugin directory.
|
||||
|
||||
### Schema Example
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"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 in seconds"
|
||||
},
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"description": "API key for data access"
|
||||
}
|
||||
},
|
||||
"required": ["api_key"]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Configuration Issues
|
||||
|
||||
### 1. Type Mismatches
|
||||
|
||||
**Problem**: String value where number expected
|
||||
|
||||
```json
|
||||
{
|
||||
"display_duration": "30" // Wrong: string
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use correct types
|
||||
|
||||
```json
|
||||
{
|
||||
"display_duration": 30 // Correct: number
|
||||
}
|
||||
```
|
||||
|
||||
**Logged Warning**:
|
||||
```
|
||||
WARNING - Config display_duration has invalid string value '30', using default 15.0
|
||||
```
|
||||
|
||||
### 2. Missing Required Fields
|
||||
|
||||
**Problem**: Required field not in config
|
||||
|
||||
```json
|
||||
{
|
||||
"football-scoreboard": {
|
||||
"enabled": true
|
||||
// Missing api_key which is required
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Logged Error**:
|
||||
```
|
||||
ERROR - Plugin football-scoreboard configuration validation failed: 'api_key' is a required property
|
||||
```
|
||||
|
||||
### 3. Invalid Nested Objects
|
||||
|
||||
**Problem**: Wrong structure for nested config
|
||||
|
||||
```json
|
||||
{
|
||||
"football-scoreboard": {
|
||||
"nfl": "enabled" // Wrong: should be object
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use correct structure
|
||||
|
||||
```json
|
||||
{
|
||||
"football-scoreboard": {
|
||||
"nfl": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Invalid JSON Syntax
|
||||
|
||||
**Problem**: Malformed JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": {
|
||||
"enabled": true, // Trailing comma
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Remove trailing commas, ensure valid JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tip**: Validate JSON at https://jsonlint.com/
|
||||
|
||||
## Debugging Configuration Loading
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Set environment variable:
|
||||
```bash
|
||||
export LEDMATRIX_DEBUG=1
|
||||
python run.py
|
||||
```
|
||||
|
||||
### Check Merged Configuration
|
||||
|
||||
The configuration is merged with schema defaults. To see the final merged config:
|
||||
|
||||
1. Enable debug logging
|
||||
2. Look for log entries like:
|
||||
```
|
||||
DEBUG - Merged config with schema defaults for football-scoreboard
|
||||
```
|
||||
|
||||
### Configuration Load Order
|
||||
|
||||
1. Load `config.json`
|
||||
2. Load `config_secrets.json`
|
||||
3. Merge secrets into main config
|
||||
4. For each plugin:
|
||||
- Load plugin's `config_schema.json`
|
||||
- Extract default values from schema
|
||||
- Merge user config with defaults
|
||||
- Validate merged config against schema
|
||||
|
||||
## Web Interface Issues
|
||||
|
||||
### Changes Not Saving
|
||||
|
||||
1. Check file permissions on `config/` directory
|
||||
2. Check disk space
|
||||
3. Look for errors in browser console
|
||||
4. Check server logs for save errors
|
||||
|
||||
### Form Fields Not Appearing
|
||||
|
||||
1. Plugin may not have `config_schema.json`
|
||||
2. Schema may have syntax errors
|
||||
3. Check browser console for JavaScript errors
|
||||
|
||||
### Checkboxes Not Working
|
||||
|
||||
Boolean values from checkboxes should be actual booleans, not strings:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true, // Correct
|
||||
"enabled": "true" // Wrong
|
||||
}
|
||||
```
|
||||
|
||||
## Config Key Collision Detection
|
||||
|
||||
LEDMatrix detects potential config key conflicts:
|
||||
|
||||
### Reserved Keys
|
||||
|
||||
These plugin IDs will trigger a warning:
|
||||
- `display`, `schedule`, `timezone`, `plugin_system`
|
||||
- `display_modes`, `system`, `hardware`, `debug`
|
||||
- `log_level`, `emulator`, `web_interface`
|
||||
|
||||
**Warning**:
|
||||
```
|
||||
WARNING - Plugin ID 'display' conflicts with reserved config key.
|
||||
```
|
||||
|
||||
### Case Collisions
|
||||
|
||||
Plugin IDs that differ only in case:
|
||||
```
|
||||
WARNING - Plugin ID 'Football-Scoreboard' may conflict with 'football-scoreboard' on case-insensitive file systems.
|
||||
```
|
||||
|
||||
## Checking Configuration via API
|
||||
|
||||
```bash
|
||||
# Get current config
|
||||
curl http://localhost:5000/api/v3/config
|
||||
|
||||
# Get specific plugin config
|
||||
curl http://localhost:5000/api/v3/config/plugin/football-scoreboard
|
||||
|
||||
# Validate config without saving
|
||||
curl -X POST http://localhost:5000/api/v3/config/validate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"football-scoreboard": {"enabled": true}}'
|
||||
```
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Manual Backup
|
||||
|
||||
```bash
|
||||
cp config/config.json config/config.backup.json
|
||||
```
|
||||
|
||||
### Automatic Backups
|
||||
|
||||
LEDMatrix creates backups before saves:
|
||||
- Location: `config/backups/`
|
||||
- Format: `config_YYYYMMDD_HHMMSS.json`
|
||||
|
||||
### Recovery
|
||||
|
||||
```bash
|
||||
# List backups
|
||||
ls -la config/backups/
|
||||
|
||||
# Restore from backup
|
||||
cp config/backups/config_20240115_120000.json config/config.json
|
||||
```
|
||||
|
||||
## Troubleshooting Checklist
|
||||
|
||||
- [ ] JSON syntax is valid (no trailing commas, quotes correct)
|
||||
- [ ] Data types match schema (numbers are numbers, not strings)
|
||||
- [ ] Required fields are present
|
||||
- [ ] Nested objects have correct structure
|
||||
- [ ] File permissions allow read/write
|
||||
- [ ] No reserved config key collisions
|
||||
- [ ] Plugin has `config_schema.json` for validation
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. Check logs: `tail -f logs/ledmatrix.log`
|
||||
2. Enable debug: `LEDMATRIX_DEBUG=1`
|
||||
3. Check error dashboard: `/api/v3/errors/summary`
|
||||
4. Validate JSON: https://jsonlint.com/
|
||||
5. File an issue: https://github.com/ChuckBuilds/LEDMatrix/issues
|
||||
213
docs/DEVELOPER_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Developer Quick Reference
|
||||
|
||||
One-page quick reference for common LEDMatrix development tasks.
|
||||
|
||||
## REST API Endpoints
|
||||
|
||||
### Most Common Endpoints
|
||||
|
||||
```bash
|
||||
# Get installed plugins
|
||||
GET /api/v3/plugins/installed
|
||||
|
||||
# Get plugin configuration
|
||||
GET /api/v3/plugins/config?plugin_id=<plugin_id>
|
||||
|
||||
# Save plugin configuration
|
||||
POST /api/v3/plugins/config
|
||||
{"plugin_id": "my-plugin", "config": {...}}
|
||||
|
||||
# Start on-demand display
|
||||
POST /api/v3/display/on-demand/start
|
||||
{"plugin_id": "my-plugin", "duration": 30}
|
||||
|
||||
# Get system status
|
||||
GET /api/v3/system/status
|
||||
|
||||
# Execute system action
|
||||
POST /api/v3/system/action
|
||||
{"action": "start_display"}
|
||||
```
|
||||
|
||||
**Base URL**: `http://your-pi-ip:5000/api/v3`
|
||||
|
||||
See [API_REFERENCE.md](API_REFERENCE.md) for complete documentation.
|
||||
|
||||
## Display Manager Quick Methods
|
||||
|
||||
```python
|
||||
# Core operations
|
||||
display_manager.clear() # Clear display
|
||||
display_manager.update_display() # Update physical display
|
||||
|
||||
# Text rendering
|
||||
display_manager.draw_text("Hello", x=10, y=16, color=(255, 255, 255))
|
||||
display_manager.draw_text("Centered", centered=True) # Auto-center
|
||||
|
||||
# Utilities
|
||||
width = display_manager.get_text_width("Text", font)
|
||||
height = display_manager.get_font_height(font)
|
||||
|
||||
# Weather icons
|
||||
display_manager.draw_weather_icon("rain", x=10, y=10, size=16)
|
||||
|
||||
# Scrolling state
|
||||
display_manager.set_scrolling_state(True)
|
||||
display_manager.defer_update(lambda: self.update_cache(), priority=0)
|
||||
```
|
||||
|
||||
## Cache Manager Quick Methods
|
||||
|
||||
```python
|
||||
# Basic caching
|
||||
cached = cache_manager.get("key", max_age=3600)
|
||||
cache_manager.set("key", data)
|
||||
cache_manager.delete("key")
|
||||
|
||||
# Advanced caching
|
||||
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 = cache_manager.get_cache_strategy("weather")
|
||||
interval = cache_manager.get_sport_live_interval("nhl")
|
||||
```
|
||||
|
||||
## Plugin Manager Quick Methods
|
||||
|
||||
```python
|
||||
# Get plugins
|
||||
plugin = plugin_manager.get_plugin("plugin-id")
|
||||
all_plugins = plugin_manager.get_all_plugins()
|
||||
enabled = plugin_manager.get_enabled_plugins()
|
||||
|
||||
# Get info
|
||||
info = plugin_manager.get_plugin_info("plugin-id")
|
||||
modes = plugin_manager.get_plugin_display_modes("plugin-id")
|
||||
```
|
||||
|
||||
## BasePlugin Quick Reference
|
||||
|
||||
```python
|
||||
class MyPlugin(BasePlugin):
|
||||
def update(self):
|
||||
# Fetch data (called based on update_interval)
|
||||
cache_key = f"{self.plugin_id}_data"
|
||||
cached = self.cache_manager.get(cache_key, max_age=3600)
|
||||
if cached:
|
||||
self.data = cached
|
||||
return
|
||||
self.data = self._fetch_from_api()
|
||||
self.cache_manager.set(cache_key, self.data)
|
||||
|
||||
def display(self, force_clear=False):
|
||||
# Render display
|
||||
if force_clear:
|
||||
self.display_manager.clear()
|
||||
self.display_manager.draw_text("Hello", x=10, y=16)
|
||||
self.display_manager.update_display()
|
||||
|
||||
# Optional methods
|
||||
def has_live_content(self) -> bool:
|
||||
return len(self.live_items) > 0
|
||||
|
||||
def validate_config(self) -> bool:
|
||||
return "api_key" in self.config
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Caching Pattern
|
||||
```python
|
||||
def update(self):
|
||||
cache_key = f"{self.plugin_id}_data"
|
||||
cached = self.cache_manager.get(cache_key, max_age=3600)
|
||||
if cached:
|
||||
self.data = cached
|
||||
return
|
||||
self.data = self._fetch_from_api()
|
||||
self.cache_manager.set(cache_key, self.data)
|
||||
```
|
||||
|
||||
### Error Handling Pattern
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
try:
|
||||
if not self.data:
|
||||
self._display_no_data()
|
||||
return
|
||||
self._render_content()
|
||||
self.display_manager.update_display()
|
||||
except Exception as e:
|
||||
self.logger.error(f"Display error: {e}", exc_info=True)
|
||||
self._display_error()
|
||||
```
|
||||
|
||||
### Scrolling Pattern
|
||||
```python
|
||||
def display(self, force_clear=False):
|
||||
self.display_manager.set_scrolling_state(True)
|
||||
try:
|
||||
# Scroll content...
|
||||
for x in range(width, -text_width, -2):
|
||||
self.display_manager.clear()
|
||||
self.display_manager.draw_text(text, x=x, y=16)
|
||||
self.display_manager.update_display()
|
||||
time.sleep(0.05)
|
||||
finally:
|
||||
self.display_manager.set_scrolling_state(False)
|
||||
```
|
||||
|
||||
## Plugin Development Checklist
|
||||
|
||||
- [ ] Plugin inherits from `BasePlugin`
|
||||
- [ ] Implements `update()` and `display()` methods
|
||||
- [ ] `manifest.json` with required fields
|
||||
- [ ] `config_schema.json` for web UI (recommended)
|
||||
- [ ] `README.md` with documentation
|
||||
- [ ] Error handling implemented
|
||||
- [ ] Uses caching appropriately
|
||||
- [ ] Tested on Raspberry Pi hardware
|
||||
- [ ] Follows versioning best practices
|
||||
|
||||
## Common Errors & Solutions
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| Plugin not discovered | Check `manifest.json` exists and `id` matches directory name |
|
||||
| Import errors | Check `requirements.txt` and dependencies |
|
||||
| Config validation fails | Verify `config_schema.json` syntax |
|
||||
| Display not updating | Call `update_display()` after drawing |
|
||||
| Cache not working | Check cache directory permissions |
|
||||
|
||||
## File Locations
|
||||
|
||||
```
|
||||
LEDMatrix/
|
||||
├── plugins/ # Installed plugins
|
||||
├── config/
|
||||
│ ├── config.json # Main configuration
|
||||
│ └── config_secrets.json # API keys and secrets
|
||||
├── docs/ # Documentation
|
||||
│ ├── API_REFERENCE.md
|
||||
│ ├── PLUGIN_API_REFERENCE.md
|
||||
│ └── ...
|
||||
└── src/
|
||||
├── display_manager.py
|
||||
├── cache_manager.py
|
||||
└── plugin_system/
|
||||
└── base_plugin.py
|
||||
```
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Complete API Reference](API_REFERENCE.md)
|
||||
- [Plugin API Reference](PLUGIN_API_REFERENCE.md)
|
||||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md)
|
||||
- [Advanced Patterns](ADVANCED_PLUGIN_DEVELOPMENT.md)
|
||||
- [Configuration Guide](PLUGIN_CONFIGURATION_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
**Tip**: Bookmark this page for quick access to common methods and patterns!
|
||||
|
||||
159
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Development Guide
|
||||
|
||||
This guide provides information for developers and contributors working on the LEDMatrix project.
|
||||
|
||||
## Git Submodules
|
||||
|
||||
### rpi-rgb-led-matrix-master Submodule
|
||||
|
||||
The `rpi-rgb-led-matrix-master` submodule is a foundational dependency located at the repository root (not in `plugins/`). This submodule provides the core hardware abstraction layer for controlling RGB LED matrices via the Raspberry Pi GPIO pins.
|
||||
|
||||
#### Architectural Rationale
|
||||
|
||||
**Why at the root?**
|
||||
- **Core Dependency**: Unlike plugins in the `plugins/` directory, `rpi-rgb-led-matrix-master` is a foundational library required by the core LEDMatrix system, not an optional plugin
|
||||
- **System-Level Integration**: The `rgbmatrix` Python module (built from this submodule) is imported by `src/display_manager.py`, which is part of the core display system
|
||||
- **Build Requirements**: The submodule must be compiled to create the `rgbmatrix` Python bindings before the system can run
|
||||
- **Separation of Concerns**: Keeping core dependencies at the root level separates them from user-installable plugins, maintaining a clear architectural distinction
|
||||
|
||||
**Why not in `plugins/`?**
|
||||
- Plugins are optional, user-installable modules that depend on the core system
|
||||
- `rpi-rgb-led-matrix-master` is a required build dependency, not an optional plugin
|
||||
- The core system cannot function without this dependency
|
||||
|
||||
#### Initializing the Submodule
|
||||
|
||||
When cloning the repository, you must initialize the submodule:
|
||||
|
||||
**First-time clone (recommended):**
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/ChuckBuilds/LEDMatrix.git
|
||||
cd LEDMatrix
|
||||
```
|
||||
|
||||
**If you already cloned without submodules:**
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
**To initialize only the rpi-rgb-led-matrix-master submodule:**
|
||||
```bash
|
||||
git submodule update --init --recursive rpi-rgb-led-matrix-master
|
||||
```
|
||||
|
||||
#### Building the Submodule
|
||||
|
||||
After initializing the submodule, you need to build the Python bindings:
|
||||
|
||||
```bash
|
||||
cd rpi-rgb-led-matrix-master
|
||||
make build-python
|
||||
cd bindings/python
|
||||
python3 -m pip install --break-system-packages .
|
||||
```
|
||||
|
||||
**Note:** The `first_time_install.sh` script automates this process during installation.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
**Submodule appears empty:**
|
||||
If the `rpi-rgb-led-matrix-master` directory exists but is empty or lacks a `Makefile`:
|
||||
```bash
|
||||
# Remove the empty directory
|
||||
rm -rf rpi-rgb-led-matrix-master
|
||||
|
||||
# Re-initialize the submodule
|
||||
git submodule update --init --recursive rpi-rgb-led-matrix-master
|
||||
```
|
||||
|
||||
**Build fails:**
|
||||
Ensure you have the required build dependencies installed:
|
||||
```bash
|
||||
sudo apt install -y build-essential python3-dev cython3 scons
|
||||
```
|
||||
|
||||
**Import error for `rgbmatrix` module:**
|
||||
- Verify the submodule is initialized: `ls rpi-rgb-led-matrix-master/Makefile`
|
||||
- Ensure the Python bindings are built and installed (see "Building the Submodule" above)
|
||||
- Check that the module is installed: `python3 -c "from rgbmatrix import RGBMatrix; print('OK')"`
|
||||
|
||||
**Submodule out of sync:**
|
||||
If the submodule commit doesn't match what the main repository expects:
|
||||
```bash
|
||||
git submodule update --remote rpi-rgb-led-matrix-master
|
||||
```
|
||||
|
||||
#### CI/CD Configuration
|
||||
|
||||
When setting up CI/CD pipelines, ensure submodules are initialized before building:
|
||||
|
||||
**GitHub Actions Example:**
|
||||
```yaml
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build rpi-rgb-led-matrix
|
||||
run: |
|
||||
cd rpi-rgb-led-matrix-master
|
||||
make build-python
|
||||
cd bindings/python
|
||||
pip install .
|
||||
```
|
||||
|
||||
**GitLab CI Example:**
|
||||
```yaml
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
build:
|
||||
script:
|
||||
- cd rpi-rgb-led-matrix-master
|
||||
- make build-python
|
||||
- cd bindings/python
|
||||
- pip install .
|
||||
```
|
||||
|
||||
**Jenkins Example:**
|
||||
```groovy
|
||||
stage('Checkout') {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: '*/main']],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'SubmoduleOption',
|
||||
disableSubmodules: false,
|
||||
parentCredentials: true,
|
||||
recursiveSubmodules: true,
|
||||
reference: '',
|
||||
trackingSubmodules: false]],
|
||||
userRemoteConfigs: [[url: 'https://github.com/ChuckBuilds/LEDMatrix.git']]
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
**General CI/CD Checklist:**
|
||||
- ✓ Use `--recurse-submodules` flag when cloning (or equivalent in your CI system)
|
||||
- ✓ Initialize submodules before any build steps
|
||||
- ✓ Build the Python bindings if your tests require the `rgbmatrix` module
|
||||
- ✓ Note: Emulator mode (using `RGBMatrixEmulator`) doesn't require the submodule to be built
|
||||
|
||||
---
|
||||
|
||||
## Plugin Submodules
|
||||
|
||||
Plugin submodules are located in the `plugins/` directory and are managed similarly:
|
||||
|
||||
**Initialize all plugin submodules:**
|
||||
```bash
|
||||
git submodule update --init --recursive plugins/
|
||||
```
|
||||
|
||||
**Initialize a specific plugin:**
|
||||
```bash
|
||||
git submodule update --init --recursive plugins/hockey-scoreboard
|
||||
```
|
||||
|
||||
For more information about plugins, see the [Plugin Development Guide](.cursor/plugins_guide.md) and [Plugin Architecture Specification](docs/PLUGIN_ARCHITECTURE_SPEC.md).
|
||||
|
||||
166
docs/DEV_PREVIEW.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Dev Preview & Visual Testing
|
||||
|
||||
Tools for rapid plugin development without deploying to the RPi.
|
||||
|
||||
## Dev Preview Server
|
||||
|
||||
Interactive web UI for tweaking plugin configs and seeing the rendered display in real time.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
python scripts/dev_server.py
|
||||
# Opens at http://localhost:5001
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```bash
|
||||
python scripts/dev_server.py --port 8080 # Custom port
|
||||
python scripts/dev_server.py --extra-dir /path/to/custom-plugin # 3rd party plugins
|
||||
python scripts/dev_server.py --debug # Flask debug mode
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Select a plugin from the dropdown (auto-discovers from `plugins/` and `plugin-repos/`)
|
||||
2. The config form auto-generates from the plugin's `config_schema.json`
|
||||
3. Tweak any config value — the display preview updates automatically
|
||||
4. Toggle "Auto" off for plugins with slow `update()` calls, then click "Render" manually
|
||||
5. Use the zoom slider to scale the tiny display (128x32) up for detailed inspection
|
||||
6. Toggle the grid overlay to see individual pixel boundaries
|
||||
|
||||
### Mock Data for API-dependent Plugins
|
||||
|
||||
Many plugins fetch data from APIs (sports scores, weather, stocks). To render these locally, expand "Mock Data" and paste a JSON object with cache keys the plugin expects.
|
||||
|
||||
To find the cache keys a plugin uses, search its `manager.py` for `self.cache_manager.set(` calls.
|
||||
|
||||
Example for a sports plugin:
|
||||
```json
|
||||
{
|
||||
"football_scores": {
|
||||
"games": [
|
||||
{"home": "Eagles", "away": "Chiefs", "home_score": 24, "away_score": 21, "status": "Final"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Render Script
|
||||
|
||||
Render any plugin to a PNG image from the command line. Useful for AI-assisted development and scripted workflows.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Basic — renders with default config
|
||||
python scripts/render_plugin.py --plugin hello-world --output /tmp/hello.png
|
||||
|
||||
# Custom config
|
||||
python scripts/render_plugin.py --plugin clock-simple \
|
||||
--config '{"timezone":"America/New_York","time_format":"12h"}' \
|
||||
--output /tmp/clock.png
|
||||
|
||||
# Different display dimensions
|
||||
python scripts/render_plugin.py --plugin hello-world --width 64 --height 32 --output /tmp/small.png
|
||||
|
||||
# 3rd party plugin from a custom directory
|
||||
python scripts/render_plugin.py --plugin my-plugin --plugin-dir /path/to/repo --output /tmp/my.png
|
||||
|
||||
# With mock API data
|
||||
python scripts/render_plugin.py --plugin football-scoreboard \
|
||||
--mock-data /tmp/mock_scores.json \
|
||||
--output /tmp/football.png
|
||||
```
|
||||
|
||||
### Using with Claude Code / AI
|
||||
|
||||
Claude can run the render script, then read the output PNG (Claude is multimodal and can see images). This enables a visual feedback loop:
|
||||
|
||||
```bash
|
||||
Claude → bash: python scripts/render_plugin.py --plugin hello-world --output /tmp/render.png
|
||||
Claude → Read /tmp/render.png ← Claude sees the actual rendered display
|
||||
Claude → (makes code changes based on what it sees)
|
||||
Claude → bash: python scripts/render_plugin.py --plugin hello-world --output /tmp/render2.png
|
||||
Claude → Read /tmp/render2.png ← verifies the visual change
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VisualTestDisplayManager (for test suites)
|
||||
|
||||
A display manager that renders real pixels for use in pytest, without requiring hardware.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from src.plugin_system.testing import VisualTestDisplayManager, MockCacheManager, MockPluginManager
|
||||
|
||||
def test_my_plugin_renders_title():
|
||||
display = VisualTestDisplayManager(width=128, height=32)
|
||||
cache = MockCacheManager()
|
||||
pm = MockPluginManager()
|
||||
|
||||
plugin = MyPlugin(
|
||||
plugin_id='my-plugin',
|
||||
config={'enabled': True, 'title': 'Hello'},
|
||||
display_manager=display,
|
||||
cache_manager=cache,
|
||||
plugin_manager=pm
|
||||
)
|
||||
|
||||
plugin.update()
|
||||
plugin.display(force_clear=True)
|
||||
|
||||
# Verify pixels were drawn (not just that methods were called)
|
||||
pixels = list(display.image.getdata())
|
||||
assert any(p != (0, 0, 0) for p in pixels), "Display should not be blank"
|
||||
|
||||
# Save snapshot for manual inspection
|
||||
display.save_snapshot('/tmp/test_my_plugin.png')
|
||||
```
|
||||
|
||||
### Pytest Fixture
|
||||
|
||||
A `visual_display_manager` fixture is available in plugin tests:
|
||||
|
||||
```python
|
||||
def test_rendering(visual_display_manager):
|
||||
visual_display_manager.draw_text("Test", x=10, y=10, color=(255, 255, 255))
|
||||
assert visual_display_manager.width == 128
|
||||
pixels = list(visual_display_manager.image.getdata())
|
||||
assert any(p != (0, 0, 0) for p in pixels)
|
||||
```
|
||||
|
||||
### Key Differences from MockDisplayManager
|
||||
|
||||
| Feature | MockDisplayManager | VisualTestDisplayManager |
|
||||
|---------|-------------------|--------------------------|
|
||||
| Renders pixels | No (logs calls only) | Yes (real PIL rendering) |
|
||||
| Loads fonts | No | Yes (same fonts as production) |
|
||||
| Save to PNG | No | Yes (`save_snapshot()`) |
|
||||
| Call tracking | Yes | Yes (backwards compatible) |
|
||||
| Use case | Unit tests (method call assertions) | Visual tests, dev preview |
|
||||
|
||||
---
|
||||
|
||||
## Plugin Test Runner
|
||||
|
||||
The test runner auto-detects `plugin-repos/` for monorepo development:
|
||||
|
||||
```bash
|
||||
# Auto-detect (tries plugins/ then plugin-repos/)
|
||||
python scripts/run_plugin_tests.py
|
||||
|
||||
# Test specific plugin
|
||||
python scripts/run_plugin_tests.py --plugin clock-simple
|
||||
|
||||
# Explicit directory
|
||||
python scripts/run_plugin_tests.py --plugins-dir plugin-repos/
|
||||
|
||||
# With coverage
|
||||
python scripts/run_plugin_tests.py --coverage --verbose
|
||||
```
|
||||
417
docs/EMULATOR_SETUP_GUIDE.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# LEDMatrix Emulator Setup Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The LEDMatrix emulator allows you to run and test LEDMatrix displays on your computer without requiring physical LED matrix hardware. This is perfect for development, testing, and demonstration purposes.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Installation](#installation)
|
||||
3. [Configuration](#configuration)
|
||||
4. [Running the Emulator](#running-the-emulator)
|
||||
5. [Display Adapters](#display-adapters)
|
||||
6. [Troubleshooting](#troubleshooting)
|
||||
7. [Advanced Configuration](#advanced-configuration)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
- Python 3.7 or higher
|
||||
- Windows, macOS, or Linux
|
||||
- At least 2GB RAM (4GB recommended)
|
||||
- Internet connection for plugin downloads
|
||||
|
||||
### Required Software
|
||||
- Python 3.7+
|
||||
- pip (Python package manager)
|
||||
- Git (for plugin management)
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/LEDMatrix.git
|
||||
cd LEDMatrix
|
||||
```
|
||||
|
||||
### 2. Install Emulator Dependencies
|
||||
|
||||
Install the emulator-specific requirements:
|
||||
|
||||
```bash
|
||||
pip install -r requirements-emulator.txt
|
||||
```
|
||||
|
||||
This installs:
|
||||
- `RGBMatrixEmulator` - The core emulation library
|
||||
- Additional dependencies for display adapters
|
||||
|
||||
### 3. Install Standard Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### 1. Emulator Configuration File
|
||||
|
||||
The emulator uses `emulator_config.json` for configuration. Here's the default configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"pixel_outline": 0,
|
||||
"pixel_size": 16,
|
||||
"pixel_style": "square",
|
||||
"pixel_glow": 6,
|
||||
"display_adapter": "pygame",
|
||||
"icon_path": null,
|
||||
"emulator_title": null,
|
||||
"suppress_font_warnings": false,
|
||||
"suppress_adapter_load_errors": false,
|
||||
"browser": {
|
||||
"_comment": "For use with the browser adapter only.",
|
||||
"port": 8888,
|
||||
"target_fps": 24,
|
||||
"fps_display": false,
|
||||
"quality": 70,
|
||||
"image_border": true,
|
||||
"debug_text": false,
|
||||
"image_format": "JPEG"
|
||||
},
|
||||
"log_level": "info"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Configuration Options
|
||||
|
||||
| Option | Description | Default | Values |
|
||||
|--------|-------------|---------|--------|
|
||||
| `pixel_outline` | Pixel border thickness | 0 | 0-5 |
|
||||
| `pixel_size` | Size of each pixel | 16 | 8-64 |
|
||||
| `pixel_style` | Pixel shape | "square" | "square", "circle" |
|
||||
| `pixel_glow` | Glow effect intensity | 6 | 0-20 |
|
||||
| `display_adapter` | Display backend | "pygame" | "pygame", "browser" |
|
||||
| `emulator_title` | Window title | null | Any string |
|
||||
| `suppress_font_warnings` | Hide font warnings | false | true/false |
|
||||
| `suppress_adapter_load_errors` | Hide adapter errors | false | true/false |
|
||||
|
||||
### 3. Browser Adapter Configuration
|
||||
|
||||
When using the browser adapter, additional options are available:
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `port` | Web server port | 8888 |
|
||||
| `target_fps` | Target frames per second | 24 |
|
||||
| `fps_display` | Show FPS counter | false |
|
||||
| `quality` | Image compression quality | 70 |
|
||||
| `image_border` | Show image border | true |
|
||||
| `debug_text` | Show debug information | false |
|
||||
| `image_format` | Image format | "JPEG" |
|
||||
|
||||
## Running the Emulator
|
||||
|
||||
### 1. Set Environment Variable
|
||||
|
||||
Enable emulator mode by setting the `EMULATOR` environment variable:
|
||||
|
||||
**Windows (Command Prompt):**
|
||||
```cmd
|
||||
set EMULATOR=true
|
||||
python run.py
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
$env:EMULATOR="true"
|
||||
python run.py
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
export EMULATOR=true
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
### 2. Alternative: Direct Python Execution
|
||||
|
||||
You can also run the emulator directly:
|
||||
|
||||
```bash
|
||||
EMULATOR=true python3 run.py
|
||||
```
|
||||
|
||||
### 3. Verify Emulator Mode
|
||||
|
||||
When running in emulator mode, you should see:
|
||||
- A window displaying the LED matrix simulation
|
||||
- Console output indicating emulator mode
|
||||
- No hardware initialization errors
|
||||
|
||||
## Display Adapters
|
||||
|
||||
LEDMatrix supports two display adapters for the emulator:
|
||||
|
||||
### 1. Pygame Adapter (Default)
|
||||
|
||||
The pygame adapter provides a native desktop window with real-time display.
|
||||
|
||||
**Features:**
|
||||
- Real-time rendering
|
||||
- Keyboard controls
|
||||
- Window resizing
|
||||
- High performance
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"display_adapter": "pygame",
|
||||
"pixel_size": 16,
|
||||
"pixel_style": "square"
|
||||
}
|
||||
```
|
||||
|
||||
**Keyboard Controls:**
|
||||
- `ESC` - Exit emulator
|
||||
- `F11` - Toggle fullscreen
|
||||
- `+/-` - Zoom in/out
|
||||
- `R` - Reset zoom
|
||||
|
||||
### 2. Browser Adapter
|
||||
|
||||
The browser adapter runs a web server and displays the matrix in a web browser.
|
||||
|
||||
**Features:**
|
||||
- Web-based interface
|
||||
- Remote access capability
|
||||
- Mobile-friendly
|
||||
- Screenshot capture
|
||||
|
||||
**Configuration:**
|
||||
```json
|
||||
{
|
||||
"display_adapter": "browser",
|
||||
"browser": {
|
||||
"port": 8888,
|
||||
"target_fps": 24,
|
||||
"quality": 70
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
1. Start the emulator with browser adapter
|
||||
2. Open browser to `http://localhost:8888`
|
||||
3. View the LED matrix display
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. "ModuleNotFoundError: No module named 'RGBMatrixEmulator'"
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
pip install RGBMatrixEmulator
|
||||
```
|
||||
|
||||
#### 2. Pygame Window Not Opening
|
||||
|
||||
**Possible Causes:**
|
||||
- Missing pygame installation
|
||||
- Display server issues (Linux)
|
||||
- Graphics driver problems
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# Install pygame
|
||||
pip install pygame
|
||||
|
||||
# For Linux, ensure X11 is running
|
||||
echo $DISPLAY
|
||||
|
||||
# For WSL, install X server
|
||||
# Windows: Install VcXsrv or Xming
|
||||
```
|
||||
|
||||
#### 3. Browser Adapter Not Working
|
||||
|
||||
**Check:**
|
||||
- Port 8888 is available
|
||||
- Firewall allows connections
|
||||
- Browser can access localhost
|
||||
|
||||
**Solutions:**
|
||||
```bash
|
||||
# Check if port is in use
|
||||
netstat -an | grep 8888
|
||||
|
||||
# Try different port in config
|
||||
"port": 8889
|
||||
```
|
||||
|
||||
#### 4. Performance Issues
|
||||
|
||||
**Optimizations:**
|
||||
- Reduce `pixel_size` in config
|
||||
- Lower `target_fps` for browser adapter
|
||||
- Close other applications
|
||||
- Use pygame adapter for better performance
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```json
|
||||
{
|
||||
"log_level": "debug",
|
||||
"suppress_font_warnings": false,
|
||||
"suppress_adapter_load_errors": false
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### 1. Custom Display Dimensions
|
||||
|
||||
Modify the display dimensions in your main config:
|
||||
|
||||
```json
|
||||
{
|
||||
"display": {
|
||||
"hardware": {
|
||||
"rows": 32,
|
||||
"cols": 64,
|
||||
"chain_length": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Plugin Development
|
||||
|
||||
For plugin development with the emulator:
|
||||
|
||||
```bash
|
||||
# Enable emulator mode
|
||||
export EMULATOR=true
|
||||
|
||||
# Run with specific plugin
|
||||
python run.py --plugin my-plugin
|
||||
|
||||
# Debug mode
|
||||
python run.py --debug
|
||||
```
|
||||
|
||||
### 3. Performance Tuning
|
||||
|
||||
**For High-Resolution Displays:**
|
||||
```json
|
||||
{
|
||||
"pixel_size": 8,
|
||||
"pixel_glow": 2,
|
||||
"browser": {
|
||||
"target_fps": 15,
|
||||
"quality": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**For Low-End Systems:**
|
||||
```json
|
||||
{
|
||||
"pixel_size": 12,
|
||||
"pixel_glow": 0,
|
||||
"browser": {
|
||||
"target_fps": 10,
|
||||
"quality": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Integration with Web Interface
|
||||
|
||||
The emulator can work alongside the web interface:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Start emulator
|
||||
export EMULATOR=true
|
||||
python run.py
|
||||
|
||||
# Terminal 2: Start web interface
|
||||
python web_interface/app.py
|
||||
```
|
||||
|
||||
Access the web interface at `http://localhost:5000` while the emulator runs.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Development Workflow
|
||||
|
||||
1. **Start with emulator** for initial development
|
||||
2. **Test plugins** using emulator mode
|
||||
3. **Validate configuration** before hardware deployment
|
||||
4. **Use browser adapter** for remote testing
|
||||
|
||||
### 2. Plugin Testing
|
||||
|
||||
```bash
|
||||
# Test specific plugin
|
||||
export EMULATOR=true
|
||||
python run.py --plugin clock-simple
|
||||
|
||||
# Test all plugins
|
||||
export EMULATOR=true
|
||||
python run.py --test-plugins
|
||||
```
|
||||
|
||||
### 3. Configuration Management
|
||||
|
||||
- Keep `emulator_config.json` in version control
|
||||
- Use different configs for different environments
|
||||
- Document custom configurations
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Clock Display
|
||||
|
||||
```bash
|
||||
# Start emulator with clock
|
||||
export EMULATOR=true
|
||||
python run.py
|
||||
```
|
||||
|
||||
### Sports Scores
|
||||
|
||||
```bash
|
||||
# Configure for sports display
|
||||
# Edit config/config.json to enable sports plugins
|
||||
export EMULATOR=true
|
||||
python run.py
|
||||
```
|
||||
|
||||
### Custom Text Display
|
||||
|
||||
```bash
|
||||
# Use text display plugin
|
||||
export EMULATOR=true
|
||||
python run.py --plugin text-display --text "Hello World"
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For additional help:
|
||||
|
||||
1. **Check the logs** - Enable debug mode for detailed output
|
||||
2. **Review configuration** - Ensure all settings are correct
|
||||
3. **Test with minimal config** - Start with default settings
|
||||
4. **Community support** - Check GitHub issues and discussions
|
||||
|
||||
## Conclusion
|
||||
|
||||
The LEDMatrix emulator provides a powerful way to develop, test, and demonstrate LED matrix displays without physical hardware. With support for multiple display adapters and comprehensive configuration options, it's an essential tool for LEDMatrix development and deployment.
|
||||
|
||||
For more information, see the main [README.md](../README.md) and other documentation in the `docs/` directory.
|
||||
363
docs/FONT_MANAGER.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# FontManager Usage Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The enhanced FontManager provides comprehensive font management for the LEDMatrix application with support for:
|
||||
- Manager font registration and detection
|
||||
- Plugin font management
|
||||
- Manual font overrides via web interface
|
||||
- Performance monitoring and caching
|
||||
- Dynamic font discovery
|
||||
|
||||
## Architecture
|
||||
|
||||
### Manager-Centric Design
|
||||
|
||||
Managers define their own fonts, but the FontManager:
|
||||
1. **Loads and caches fonts** for performance
|
||||
2. **Detects font usage** for visibility
|
||||
3. **Allows manual overrides** when needed
|
||||
4. **Supports plugin fonts** with namespacing
|
||||
|
||||
### Font Resolution Flow
|
||||
|
||||
```
|
||||
Manager requests font → Check manual overrides → Apply manager choice → Cache & return
|
||||
```
|
||||
|
||||
## For Manager Developers
|
||||
|
||||
### Basic Font Usage
|
||||
|
||||
```python
|
||||
from src.font_manager import FontManager
|
||||
|
||||
class MyManager:
|
||||
def __init__(self, config, display_manager, cache_manager):
|
||||
self.font_manager = display_manager.font_manager # Access shared FontManager
|
||||
self.manager_id = "my_manager"
|
||||
|
||||
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(
|
||||
element_key="my_manager.text",
|
||||
family="press_start",
|
||||
size_px=size_px
|
||||
)
|
||||
```
|
||||
|
||||
## For Plugin Developers
|
||||
|
||||
### Plugin Font Registration
|
||||
|
||||
In your plugin's `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "my-plugin",
|
||||
"name": "My Plugin",
|
||||
"fonts": {
|
||||
"fonts": [
|
||||
{
|
||||
"family": "custom_font",
|
||||
"source": "plugin://fonts/custom.ttf",
|
||||
"metadata": {
|
||||
"description": "Custom plugin font",
|
||||
"license": "MIT"
|
||||
}
|
||||
},
|
||||
{
|
||||
"family": "web_font",
|
||||
"source": "https://example.com/fonts/font.ttf",
|
||||
"metadata": {
|
||||
"description": "Downloaded font",
|
||||
"checksum": "sha256:abc123..."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Plugin Fonts
|
||||
|
||||
```python
|
||||
class PluginManager:
|
||||
def __init__(self, config, display_manager, cache_manager, plugin_id):
|
||||
self.font_manager = display_manager.font_manager
|
||||
self.plugin_id = 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)
|
||||
```
|
||||
|
||||
## Manual Font Overrides
|
||||
|
||||
Users can override any font through the web interface:
|
||||
|
||||
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**
|
||||
|
||||
Overrides are stored in `config/font_overrides.json` and persist across restarts.
|
||||
|
||||
### Programmatic Overrides
|
||||
|
||||
```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)
|
||||
|
||||
# Get font height
|
||||
font_height = font_manager.get_font_height(font)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Managers
|
||||
|
||||
1. **Register all fonts** you use for visibility
|
||||
2. **Use consistent element keys** (e.g., `{manager_id}.{element_type}`)
|
||||
3. **Cache font references** if using same font multiple times
|
||||
4. **Use `resolve_font()`** not `get_font()` directly to support overrides
|
||||
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
|
||||
|
||||
### Font Not Found
|
||||
- Check font file exists in `assets/fonts/`
|
||||
- Verify font family name matches filename (without extension, lowercase)
|
||||
- Check logs for font discovery errors
|
||||
|
||||
### Override Not Working
|
||||
- Verify element key matches exactly what manager registered
|
||||
- Check `config/font_overrides.json` for correct syntax
|
||||
- Restart application to ensure overrides are loaded
|
||||
|
||||
### 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()`
|
||||
|
||||
### Plugin Fonts Not Loading
|
||||
- Verify plugin manifest syntax
|
||||
- Check plugin directory structure
|
||||
- Review logs for download/registration errors
|
||||
- Ensure font URLs are accessible
|
||||
|
||||
## API Reference
|
||||
|
||||
### FontManager Methods
|
||||
|
||||
- `register_manager_font(manager_id, element_key, family, size_px, color=None)` - Register font usage
|
||||
- `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.
|
||||
|
||||