mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-06 19:28:06 +00:00
A second-pass content audit checked the guides' substantive claims against the code (the first pass only fixed mechanical drift). Fixes: - GETTING_STARTED: described booting a prebuilt SD image and seeing default clock/weather plugins — neither exists. Now documents the real install (Pi OS Lite + one-shot installer / first_time_install.sh) and that displays come from the Plugin Store. Duration and ordering instructions moved to the Rotation tab where the controls actually live. - WEB_INTERFACE_GUIDE: three whole tabs were undocumented (Rotation, Backup & Restore, Tools) and the Display tab's Vegas Scroll section was unmentioned. Fonts overrides are per display element (not per plugin); Logs has an Auto-scroll checkbox (not a Pause button); the aspirational keyboard-shortcut list and no-JS claim removed. - TROUBLESHOOTING: the hand-written service-file template (wrong user, wrong ExecStart, dropped the autostart gate) replaced with the real systemd/ units + install scripts; recovery steps no longer copy placeholder units verbatim; WiFi curl endpoint corrected to /api/v3/; cache-clearing advice now targets the real cache locations. - ADVANCED_FEATURES: removed a false claim that CacheManager has no delete(); fixed two example snippets that raise TypeError (BackgroundDataService and get_config_file_mode signatures); fixed cache paths, a 5-minute TTL that is actually 1 hour, and the vegas table now links the complete 26-key reference. - EMULATOR_SETUP_GUIDE: documented run.py flags that don't exist (--plugin/--test-plugins) removed in favor of dev_server.py and check_plugin.py; shipped emulator config values corrected (browser adapter default on :8888, not pygame). - PLUGIN_QUICK_REFERENCE: drag-and-drop reordering is shipped, not 'not yet supported'; discovery-fallback and registry-repo claims corrected. PLUGIN_API_REFERENCE: get_vegas_segment_width returns panels, not pixels. CONTRIBUTING: the repo uses flake8/mypy/bandit pre-commit hooks, not black/ruff, and tests need requirements-test.txt. - SKIN_SYSTEM/DEVELOPER_QUICK_REFERENCE: stale module paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SXb4mKcAkVaxkeTb3YnAdr
1045 lines
31 KiB
Markdown
1045 lines
31 KiB
Markdown
# Advanced Features Guide
|
|
|
|
This guide covers advanced LEDMatrix features for users and developers, including Vegas scroll mode, on-demand display, cache management, background services, and permission management.
|
|
|
|
---
|
|
|
|
## 1. Vegas Scroll Mode
|
|
|
|
### Overview
|
|
|
|
Vegas scroll mode displays content from multiple plugins in a continuous horizontal scroll, similar to news tickers seen in Las Vegas casinos. Plugins contribute content segments that flow across the display in a seamless ticker-style presentation.
|
|
|
|
### Display Modes
|
|
|
|
**SCROLL (Continuous Scrolling):**
|
|
- Content scrolls continuously left
|
|
- Smooth, fluid motion
|
|
- Best for news-ticker style displays
|
|
|
|
**FIXED_SEGMENT (Fixed-Width Block):**
|
|
- Plugin gets fixed-width block on display
|
|
- Content doesn't scroll out of its segment
|
|
- Multiple plugins can share the display simultaneously
|
|
|
|
**STATIC (Scroll Pauses):**
|
|
- Scrolling pauses when content is fully visible
|
|
- Displays for specified duration, then resumes scrolling
|
|
- Best for content that needs to be fully read
|
|
|
|
### Configuration
|
|
|
|
Enable Vegas mode in `config/config.json`:
|
|
|
|
```json
|
|
{
|
|
"display": {
|
|
"vegas_scroll": {
|
|
"enabled": true,
|
|
"scroll_speed": 50,
|
|
"separator_width": 32,
|
|
"plugin_order": ["clock", "weather", "sports"],
|
|
"excluded_plugins": ["debug_plugin"],
|
|
"target_fps": 125,
|
|
"buffer_ahead": 2
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Vegas mode can also be configured entirely from the web UI — the
|
|
**Display** tab has a Vegas Scroll Mode section (enable toggle, scroll
|
|
speed, separator width, dynamic duration, and more), so hand-editing
|
|
JSON is optional.
|
|
|
|
**Configuration Options:**
|
|
|
|
| Setting | Default | Description |
|
|
|---------|---------|-------------|
|
|
| `enabled` | `false` | Enable Vegas scroll mode |
|
|
| `scroll_speed` | `50` | Pixels per second scroll speed |
|
|
| `separator_width` | `32` | Width between plugin segments (pixels) |
|
|
| `plugin_order` | `[]` | Plugin display order (empty = auto) |
|
|
| `excluded_plugins` | `[]` | Plugins to exclude from Vegas mode |
|
|
| `target_fps` | `125` | Target frame rate |
|
|
| `buffer_ahead` | `2` | Number of plugins buffered ahead |
|
|
|
|
This table is a subset — `display.vegas_scroll` supports 26 keys in
|
|
total. See the full list in
|
|
[CONFIG_REFERENCE.md](CONFIG_REFERENCE.md#displayvegas_scroll--continuous-scroll-mode).
|
|
|
|
### Per-Plugin Configuration
|
|
|
|
Override Vegas behavior for specific plugins:
|
|
|
|
```json
|
|
{
|
|
"my_plugin": {
|
|
"enabled": true,
|
|
"vegas_mode": "scroll",
|
|
"vegas_panel_count": 2,
|
|
"display_duration": 10
|
|
}
|
|
}
|
|
```
|
|
|
|
**Per-Plugin Options:**
|
|
|
|
| Setting | Values | Description |
|
|
|---------|--------|-------------|
|
|
| `vegas_mode` | `scroll`, `fixed`, `static` | Display mode for this plugin |
|
|
| `vegas_panel_count` | any positive integer | Width in panels (1 panel = display width) |
|
|
| `display_duration` | seconds | Pause duration for STATIC mode |
|
|
|
|
Plugins may also set `vegas_overflow` and `vegas_max_width_screens` in
|
|
their config section to control how oversized content is handled (see
|
|
`PluginManager` in `src/plugin_system/plugin_manager.py`).
|
|
|
|
### Plugin Integration (Developer Guide)
|
|
|
|
**1. Implement Content Method:**
|
|
|
|
```python
|
|
def get_vegas_content(self):
|
|
"""
|
|
Return PIL Image or list of Images for Vegas mode.
|
|
|
|
Returns:
|
|
PIL.Image or list[PIL.Image]: Content to display
|
|
- Single image: fixed-width content
|
|
- List of images: multiple segments
|
|
- None: skip this cycle
|
|
"""
|
|
# Example: Return single wide image
|
|
img = Image.new('RGB', (256, 32))
|
|
# ... render your content ...
|
|
return img
|
|
|
|
# Example: Return multiple segments
|
|
return [image1, image2, image3]
|
|
```
|
|
|
|
**2. Specify Content Type:**
|
|
|
|
```python
|
|
def get_vegas_content_type(self):
|
|
"""
|
|
Specify how content should be handled.
|
|
|
|
Returns:
|
|
str: 'multi' | 'static' | 'none'
|
|
"""
|
|
return 'multi' # Default for most plugins
|
|
```
|
|
|
|
**3. Optionally Specify Display Mode:**
|
|
|
|
```python
|
|
def get_vegas_display_mode(self):
|
|
"""
|
|
Preferred display mode for this plugin.
|
|
|
|
Returns:
|
|
str: 'scroll' | 'fixed' | 'static'
|
|
"""
|
|
return 'scroll'
|
|
|
|
def get_supported_vegas_modes(self):
|
|
"""
|
|
List of supported modes.
|
|
|
|
Returns:
|
|
list: ['scroll', 'fixed', 'static']
|
|
"""
|
|
return ['scroll', 'static']
|
|
```
|
|
|
|
### Content Rendering Guidelines
|
|
|
|
**Image Dimensions:**
|
|
- **Height:** Must match display height (typically 32 pixels)
|
|
- **Width:** Varies by mode:
|
|
- SCROLL: Any width (recommended 64-512 pixels)
|
|
- FIXED_SEGMENT: `panel_count * display_width`
|
|
- STATIC: Any width, optimized for readability
|
|
|
|
**Color Mode:**
|
|
- Use RGB color mode
|
|
- 24-bit color (8 bits per channel)
|
|
|
|
**Performance Tips:**
|
|
1. **Cache rendered images** - Render in `update()`, not in `get_vegas_content()`
|
|
2. **Keep images small** - Larger images use more memory
|
|
3. **Pre-render on update** - Don't create images on-demand
|
|
4. **Reuse images** - Return same image if content unchanged
|
|
|
|
### Example Integration
|
|
|
|
Complete example for a weather plugin:
|
|
|
|
```python
|
|
class WeatherPlugin(BasePlugin):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.vegas_image = None
|
|
|
|
def update(self):
|
|
"""Update data and pre-render Vegas image"""
|
|
# Fetch weather data
|
|
weather_data = self.fetch_weather()
|
|
|
|
# Pre-render Vegas image
|
|
self.vegas_image = self._render_vegas_content(weather_data)
|
|
|
|
def _render_vegas_content(self, data):
|
|
"""Render weather content for Vegas mode"""
|
|
img = Image.new('RGB', (384, 32))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Draw temperature
|
|
draw.text((10, 0), f"{data['temp']}°F", fill=(255, 255, 255))
|
|
|
|
# Draw condition
|
|
draw.text((100, 0), data['condition'], fill=(200, 200, 200))
|
|
|
|
# Draw icon
|
|
icon = Image.open(f"assets/{data['icon']}.png")
|
|
img.paste(icon, (250, 0))
|
|
|
|
return img
|
|
|
|
def get_vegas_content(self):
|
|
"""Return cached Vegas image"""
|
|
return self.vegas_image
|
|
|
|
def get_vegas_content_type(self):
|
|
return 'multi'
|
|
|
|
def get_vegas_display_mode(self):
|
|
return 'scroll'
|
|
|
|
def get_supported_vegas_modes(self):
|
|
return ['scroll', 'static']
|
|
```
|
|
|
|
### System Architecture
|
|
|
|
Vegas mode consists of four core components working together to provide smooth 125 FPS continuous scrolling:
|
|
|
|
#### Component Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ VegasModeCoordinator │
|
|
│ Main orchestrator - manages lifecycle and coordination │
|
|
└───────┬──────────────────┬──────────────────┬──────────────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
┌───────────────┐ ┌──────────────┐ ┌─────────────────┐
|
|
│ PluginAdapter │ │StreamManager │ │ RenderPipeline │
|
|
│ │ │ │ │ │
|
|
│ Converts │─▶│ Manages │─▶│ 125 FPS render │
|
|
│ plugin content│ │ content │ │ Double-buffered │
|
|
│ to images │ │ stream with │ │ Smooth scroll │
|
|
│ │ │ 1-2 ahead │ │ │
|
|
└───────────────┘ │ buffering │ └─────────────────┘
|
|
└──────────────┘
|
|
```
|
|
|
|
#### 1. VegasModeCoordinator
|
|
|
|
**Responsibilities:**
|
|
- Initialize and coordinate all Vegas mode components
|
|
- Manage the high-FPS render loop (target: 125 FPS)
|
|
- Handle live priority interruptions
|
|
- Process config updates during runtime
|
|
- Provide status and control interface
|
|
|
|
**Key Features:**
|
|
- Thread-safe state management
|
|
- Config hot-reload support
|
|
- Live priority integration
|
|
- Interrupt checking for yielding control back to display controller
|
|
- Static pause handling (pauses scroll when content fully visible)
|
|
|
|
**Main Loop:**
|
|
1. Check for interrupts (live priority, on-demand, config updates)
|
|
2. If static pause active, wait for duration
|
|
3. Otherwise, delegate to render pipeline for frame rendering
|
|
4. Sleep to maintain target FPS
|
|
|
|
#### 2. StreamManager
|
|
|
|
**Responsibilities:**
|
|
- Manage plugin content streaming with look-ahead buffering
|
|
- Coordinate with PluginAdapter to fetch plugin content
|
|
- Handle plugin ordering and exclusions
|
|
- Optimize content generation timing
|
|
|
|
**Buffering Strategy:**
|
|
- **Buffer Ahead:** 1-2 panels (configurable)
|
|
- **Just-in-Time Generation:** Fetch content only when needed
|
|
- **Memory Efficient:** Only keep necessary content in memory
|
|
|
|
**Content Flow:**
|
|
1. Determine which plugins should appear in stream
|
|
2. Respect `plugin_order` configuration (or use default order)
|
|
3. Exclude plugins in `excluded_plugins` list
|
|
4. Request content from each plugin via PluginAdapter
|
|
5. Compose into continuous stream with separators
|
|
|
|
**Key Methods:**
|
|
- `get_stream_content()` - Returns current stream content as PIL Image
|
|
- `advance_stream(pixels)` - Advances stream by N pixels
|
|
- `refresh_stream()` - Regenerates stream from current plugins
|
|
|
|
#### 3. PluginAdapter
|
|
|
|
**Responsibilities:**
|
|
- Convert plugin content to scrollable images
|
|
- Handle different Vegas display modes (SCROLL, FIXED, STATIC)
|
|
- Manage fallback for plugins without Vegas support
|
|
- Cache plugin content for performance
|
|
|
|
**Plugin Integration:**
|
|
1. **Check for Vegas support:**
|
|
- Calls `get_vegas_content()` if available
|
|
- Falls back to `display()` method if not
|
|
|
|
2. **Handle display mode:**
|
|
- SCROLL: Returns image as-is for continuous scrolling
|
|
- FIXED_SEGMENT: Creates fixed-width block (panel_count * display_width)
|
|
- STATIC: Marks content for pause-when-visible behavior
|
|
|
|
3. **Content type handling:**
|
|
- `multi`: Multiple segments (list of images)
|
|
- `static`: Single static image
|
|
- `none`: Skip this plugin in current cycle
|
|
|
|
**Fallback Behavior:**
|
|
- If plugin doesn't implement Vegas methods:
|
|
- Calls plugin's `display()` method
|
|
- Captures rendered display as static image
|
|
- Treats as fixed segment
|
|
- Ensures all plugins work in Vegas mode without explicit support
|
|
|
|
#### 4. RenderPipeline
|
|
|
|
**Responsibilities:**
|
|
- High-performance 125 FPS rendering
|
|
- Double-buffered composition for smooth scrolling
|
|
- Scroll position management
|
|
- Frame rate control
|
|
|
|
**Rendering Process:**
|
|
1. **Fetch Stream Content:** Get current stream from StreamManager
|
|
2. **Extract Viewport:** Calculate which portion of stream is visible
|
|
3. **Compose Frame:** Create frame with visible content
|
|
4. **Double Buffer:** Render to off-screen buffer
|
|
5. **Display:** Swap buffer to display
|
|
6. **Advance:** Update scroll position based on speed and elapsed time
|
|
|
|
**Performance Optimizations:**
|
|
- **Double Buffering:** Eliminates flicker
|
|
- **Viewport Extraction:** Only processes visible region
|
|
- **Frame Rate Control:** Precise timing to maintain 125 FPS
|
|
- **Pre-rendered Content:** Plugins pre-render during update()
|
|
|
|
**Scroll Speed Calculation:**
|
|
```python
|
|
pixels_per_frame = (scroll_speed / target_fps)
|
|
scroll_position += pixels_per_frame * elapsed_time
|
|
```
|
|
|
|
#### Component Interactions
|
|
|
|
**Initialization Flow:**
|
|
```
|
|
1. VegasModeCoordinator created
|
|
2. Coordinator creates PluginAdapter
|
|
3. Coordinator creates StreamManager (with PluginAdapter)
|
|
4. Coordinator creates RenderPipeline (with StreamManager)
|
|
5. All components initialized and ready
|
|
```
|
|
|
|
**Render Loop Flow:**
|
|
```
|
|
1. Coordinator starts render loop
|
|
2. Check for interrupts (live priority, on-demand)
|
|
3. RenderPipeline.render_frame():
|
|
a. Request current stream from StreamManager
|
|
b. StreamManager uses PluginAdapter to get plugin content
|
|
c. PluginAdapter calls plugin Vegas methods or fallback
|
|
d. Stream content returned to RenderPipeline
|
|
e. RenderPipeline extracts viewport and renders
|
|
4. Update scroll position
|
|
5. Sleep to maintain target FPS
|
|
6. Repeat from step 2
|
|
```
|
|
|
|
**Config Update Flow:**
|
|
```
|
|
1. Config change detected by Coordinator
|
|
2. Set _pending_config_update flag
|
|
3. On next render loop iteration:
|
|
a. Pause rendering
|
|
b. Update VegasModeConfig
|
|
c. Notify StreamManager of config change
|
|
d. StreamManager refreshes stream
|
|
e. Resume rendering
|
|
```
|
|
|
|
#### Thread Safety
|
|
|
|
All components use thread-safe patterns:
|
|
- **Coordinator:** Uses `threading.Lock` for state management
|
|
- **StreamManager:** Thread-safe content access
|
|
- **RenderPipeline:** Atomic frame composition
|
|
- **PluginAdapter:** Stateless operations (except caching)
|
|
|
|
#### Performance Characteristics
|
|
|
|
**Frame Rate:**
|
|
- Target: 125 FPS
|
|
- Actual: 100-125 FPS (depends on content complexity)
|
|
- Render time budget: ~8ms per frame
|
|
|
|
**Memory Usage:**
|
|
- Stream buffer: ~2-3 panels ahead
|
|
- Plugin content: Cached in plugin's `update()` method
|
|
- Double buffer: 2x display size
|
|
|
|
**CPU Usage:**
|
|
- Light load: 5-10% (simple content)
|
|
- Heavy load: 15-25% (complex content, many plugins)
|
|
- Optimized with numpy for pixel operations
|
|
|
|
### Fallback Behavior
|
|
|
|
If a plugin doesn't implement Vegas methods:
|
|
- System calls the plugin's `display()` method
|
|
- Captures the rendered display as a static image
|
|
- Treats it as a fixed segment
|
|
|
|
This ensures all plugins work in Vegas mode, even without explicit support.
|
|
|
|
---
|
|
|
|
## 2. On-Demand Display
|
|
|
|
### Overview
|
|
|
|
On-demand display allows users to manually trigger specific plugins to show immediately on the LED matrix, overriding the normal rotation. This is useful for:
|
|
- Quick checks (weather, scores, time)
|
|
- Pinning important information
|
|
- Testing plugins during development
|
|
- Showing specific content to visitors
|
|
|
|
### Priority Hierarchy
|
|
|
|
On-demand display has the highest priority:
|
|
|
|
```
|
|
Priority Order (highest to lowest):
|
|
1. On-Demand Display (manual trigger)
|
|
2. Live Priority (games in progress)
|
|
3. Normal Rotation
|
|
```
|
|
|
|
When on-demand expires or is cleared, the display returns to the next highest priority (live priority or normal rotation).
|
|
|
|
### Web Interface Controls
|
|
|
|
Each installed plugin has its own tab in the second nav row of the web
|
|
UI. Inside the plugin's tab, scroll to **On-Demand Controls**:
|
|
|
|
- **Run On-Demand** — triggers the plugin immediately, even if it's
|
|
disabled in the rotation
|
|
- **Stop On-Demand** — clears on-demand and returns to the normal
|
|
rotation
|
|
|
|
The display service must be running. The status banner at the top of
|
|
the plugin tab shows the active on-demand plugin, mode, and remaining
|
|
time when something is active.
|
|
|
|
### REST API Reference
|
|
|
|
The API is mounted at `/api/v3` (`web_interface/app.py:199`).
|
|
|
|
#### Start On-Demand Display
|
|
|
|
```bash
|
|
POST /api/v3/display/on-demand/start
|
|
|
|
# Body:
|
|
{
|
|
"plugin_id": "weather",
|
|
"duration": 30, # Optional: seconds (0 = indefinite, null = default)
|
|
"pinned": false # Optional: keep until manually cleared
|
|
}
|
|
|
|
# Examples:
|
|
# 30-second preview
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"plugin_id": "weather", "duration": 30}'
|
|
|
|
# Pin indefinitely
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"plugin_id": "hockey-scoreboard", "pinned": true}'
|
|
```
|
|
|
|
#### Stop On-Demand Display
|
|
|
|
```bash
|
|
POST /api/v3/display/on-demand/stop
|
|
|
|
# Body:
|
|
{
|
|
"stop_service": false # Optional: also stop display service
|
|
}
|
|
|
|
# Examples:
|
|
# Clear on-demand
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/stop
|
|
|
|
# Stop service too
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/stop \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"stop_service": true}'
|
|
```
|
|
|
|
#### Get On-Demand Status
|
|
|
|
```bash
|
|
GET /api/v3/display/on-demand/status
|
|
|
|
# Example:
|
|
curl http://localhost:5000/api/v3/display/on-demand/status
|
|
|
|
# Response:
|
|
{
|
|
"active": true,
|
|
"plugin_id": "weather",
|
|
"mode": "weather",
|
|
"remaining": 25.5,
|
|
"pinned": false,
|
|
"status": "active"
|
|
}
|
|
```
|
|
|
|
> There is no public Python on-demand API. The display controller's
|
|
> on-demand machinery is internal — drive it through the REST endpoints
|
|
> above (or the web UI buttons). The API handlers
|
|
> (`start_on_demand_display()` / `stop_on_demand_display()` in
|
|
> `web_interface/blueprints/api_v3.py`) write a request into the cache
|
|
> manager under the `display_on_demand_request` key, which
|
|
> `DisplayController._poll_on_demand_requests()`
|
|
> (`src/display_controller.py`) picks up. A separate
|
|
> `display_on_demand_config` key is used by the controller itself
|
|
> during activation (`_activate_on_demand()`) to track what's
|
|
> currently running, and is cleared by `_clear_on_demand()`.
|
|
|
|
### Duration Modes
|
|
|
|
| Duration | Pinned | Behavior |
|
|
|----------|--------|----------|
|
|
| `None` | `false` | Use plugin's default duration, auto-clear when expires |
|
|
| `0` | `false` | Indefinite, clears manually or on error |
|
|
| `> 0` | `false` | Timed display, auto-clear after N seconds |
|
|
| Any | `true` | Pin until manually cleared (ignores duration) |
|
|
|
|
### Use Case Examples
|
|
|
|
**Quick check (30-second preview):**
|
|
```bash
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"plugin_id": "ledmatrix-weather", "duration": 30}'
|
|
```
|
|
|
|
**Pin important information:**
|
|
```bash
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"plugin_id": "hockey-scoreboard", "pinned": true}'
|
|
# ... later ...
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/stop
|
|
```
|
|
|
|
**Indefinite display:**
|
|
```bash
|
|
curl -X POST http://localhost:5000/api/v3/display/on-demand/start \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"plugin_id": "text-display", "duration": 0}'
|
|
```
|
|
|
|
**Testing a plugin during development:** the same call works, or just
|
|
click **Run On-Demand** in the plugin's tab.
|
|
|
|
### Best Practices
|
|
|
|
**For Users:**
|
|
1. Use timed display as default (prevents forgetting to clear)
|
|
2. Pin only when necessary
|
|
3. Clear when done to return to normal rotation
|
|
|
|
**For Developers:**
|
|
1. Validate plugin ID exists before calling
|
|
2. Provide visual feedback in UI (loading state, status updates)
|
|
3. Handle concurrent requests gracefully
|
|
4. Log on-demand activations for debugging
|
|
|
|
### Security Considerations
|
|
|
|
**Authentication:**
|
|
- Add authentication to API endpoints
|
|
- Restrict on-demand to authorized users
|
|
|
|
**Rate Limiting:**
|
|
- Prevent abuse from rapid requests
|
|
- Implement cooldown between activations
|
|
|
|
**Input Validation:**
|
|
- Sanitize plugin IDs
|
|
- Validate duration values
|
|
- Check plugin exists before activation
|
|
|
|
---
|
|
|
|
## 3. On-Demand Cache Management
|
|
|
|
### Overview
|
|
|
|
On-demand display uses cache keys (managed by `src/cache_manager.py` —
|
|
file-based, not Redis) to coordinate state between the web interface
|
|
and the display controller across service restarts. Understanding these
|
|
keys helps troubleshoot stuck states.
|
|
|
|
### Cache Keys
|
|
|
|
**1. display_on_demand_request** (TTL: 1 hour)
|
|
```json
|
|
{
|
|
"request_id": "uuid-string",
|
|
"action": "start|stop",
|
|
"plugin_id": "plugin-name",
|
|
"mode": "mode-name",
|
|
"duration": 30.0,
|
|
"pinned": true,
|
|
"timestamp": 1234567890.123
|
|
}
|
|
```
|
|
**Purpose:** Communication from web interface to display controller
|
|
**When Set:** API endpoint receives request
|
|
**Auto-Cleared:** After processing or 1 hour TTL
|
|
|
|
**2. display_on_demand_config** (No TTL)
|
|
```json
|
|
{
|
|
"mode": "mode-name",
|
|
"duration": 30.0,
|
|
"pinned": true
|
|
}
|
|
```
|
|
**Purpose:** Persistent configuration for display controller
|
|
**When Set:** Controller processes start request
|
|
**Auto-Cleared:** When on-demand stops
|
|
|
|
**3. display_on_demand_state** (Continuously updated)
|
|
```json
|
|
{
|
|
"active": true,
|
|
"mode": "mode-name",
|
|
"remaining": 25.5,
|
|
"pinned": true,
|
|
"status": "active|idle|restarting|error"
|
|
}
|
|
```
|
|
**Purpose:** Real-time state for web interface status card
|
|
**When Set:** Every display loop iteration
|
|
**Auto-Cleared:** Never (continuously updated)
|
|
|
|
**4. display_on_demand_processed_id** (TTL: 1 hour)
|
|
```
|
|
"uuid-string-of-last-processed-request"
|
|
```
|
|
**Purpose:** Prevents duplicate request processing
|
|
**When Set:** After processing request
|
|
**Auto-Cleared:** After 1 hour TTL
|
|
|
|
### When Manual Clearing is Needed
|
|
|
|
**Scenario 1: Stuck in On-Demand State**
|
|
- Symptom: Display stays on one plugin, won't return to rotation
|
|
- Clear: `config`, `state`, `request`
|
|
|
|
**Scenario 2: Mode Switching Issues**
|
|
- Symptom: Can't change to different plugin
|
|
- Clear: `request`, `processed_id`, `state`
|
|
|
|
**Scenario 3: On-Demand Not Activating**
|
|
- Symptom: Button click does nothing
|
|
- Clear: `processed_id`, `request`
|
|
|
|
**Scenario 4: After Service Crash**
|
|
- Symptom: Strange behavior after crash/restart
|
|
- Clear: All four keys
|
|
|
|
### Manual Recovery Procedures
|
|
|
|
**Via Web Interface (Recommended):**
|
|
1. Open the **Cache** tab in the web UI
|
|
2. Find the `display_on_demand_*` entries
|
|
3. Delete them
|
|
4. Restart display: `sudo systemctl restart ledmatrix`
|
|
|
|
**Via Command Line:**
|
|
|
|
The cache is stored as JSON files under one of:
|
|
|
|
- `/var/cache/ledmatrix/` (preferred when the service has permission)
|
|
- `~/.ledmatrix_cache/`
|
|
- `/opt/ledmatrix/cache/`
|
|
- `$TMPDIR/ledmatrix_cache/` (fallback)
|
|
|
|
```bash
|
|
# Find the cache dir actually in use
|
|
journalctl -u ledmatrix | grep -i "cache directory" | tail -1
|
|
|
|
# Clear all on-demand keys (replace path with the one above)
|
|
rm /var/cache/ledmatrix/display_on_demand_*
|
|
|
|
# Restart service
|
|
sudo systemctl restart ledmatrix
|
|
```
|
|
|
|
**Via Python:**
|
|
```python
|
|
from src.cache_manager import CacheManager
|
|
|
|
cache = CacheManager()
|
|
cache.clear_cache('display_on_demand_config')
|
|
cache.clear_cache('display_on_demand_state')
|
|
cache.clear_cache('display_on_demand_request')
|
|
cache.clear_cache('display_on_demand_processed_id')
|
|
```
|
|
|
|
> `CacheManager` also has a `delete(key)` method — a thin wrapper over
|
|
> `clear_cache(key)` — so `cache.delete('display_on_demand_config')`
|
|
> works equally well.
|
|
|
|
### Cache Impact on Running Service
|
|
|
|
**IMPORTANT:** Clearing cache keys does NOT immediately affect the running controller in memory.
|
|
|
|
**To fully reset:**
|
|
1. Stop the service: `sudo systemctl stop ledmatrix`
|
|
2. Clear cache keys (web UI Cache tab or `rm` from the cache directory)
|
|
3. Clear systemd environment: `sudo systemctl daemon-reload`
|
|
4. Start the service: `sudo systemctl start ledmatrix`
|
|
|
|
### Automatic Cleanup
|
|
|
|
The display controller automatically handles cleanup:
|
|
- **Config key**: Cleared when on-demand stops
|
|
- **State key**: Updated every display loop iteration
|
|
- **Request key**: Expires after 1 hour TTL (or after processing)
|
|
- **Processed ID**: Expires after 1 hour TTL
|
|
|
|
---
|
|
|
|
## 4. Background Data Service
|
|
|
|
### Overview
|
|
|
|
The Background Data Service enables non-blocking data fetching through background threading. This prevents the main display loop from freezing during slow API requests, maintaining smooth display rotation.
|
|
|
|
### Benefits
|
|
|
|
**Performance:**
|
|
- Display loop never freezes during API calls
|
|
- Immediate response with cached/partial data
|
|
- Complete data loads in background
|
|
|
|
**User Experience:**
|
|
- No "frozen" display during data updates
|
|
- Smooth transitions between plugins
|
|
- Faster perceived load times
|
|
|
|
**Architecture:**
|
|
```
|
|
Cache Check → Background Fetch → Partial Data → Completion → Cache
|
|
(0.1s) (async) (<1s) (10-30s) (cache)
|
|
```
|
|
|
|
### Configuration
|
|
|
|
Enable background service per plugin in `config/config.json`:
|
|
|
|
```json
|
|
{
|
|
"football-scoreboard": {
|
|
"enabled": true,
|
|
"background_service": {
|
|
"enabled": true,
|
|
"max_workers": 3,
|
|
"request_timeout": 30,
|
|
"max_retries": 3,
|
|
"priority": 2
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Configuration Options:**
|
|
|
|
| Setting | Default | Description |
|
|
|---------|---------|-------------|
|
|
| `enabled` | `false` | Enable background service for this plugin |
|
|
| `max_workers` | `3` | Max concurrent background tasks |
|
|
| `request_timeout` | `30` | Timeout per API request (seconds) |
|
|
| `max_retries` | `3` | Retry attempts on failure |
|
|
| `priority` | `1` | Task priority (1=highest, 10=lowest) |
|
|
|
|
### Performance Impact
|
|
|
|
**First Request (Cache Empty):**
|
|
- Returns partial data: < 1 second
|
|
- Background completes: 10-30 seconds
|
|
- Subsequent requests use cache: < 0.1 seconds
|
|
|
|
**Subsequent Requests (Cache Hit):**
|
|
- Returns immediately: < 0.1 seconds
|
|
- Background refresh (if stale): async, no blocking
|
|
|
|
### Plugins using the background service
|
|
|
|
The background data service is used by all of the sports scoreboard
|
|
plugins (football, hockey, baseball/MLB, basketball, soccer, lacrosse,
|
|
F1, UFC), the odds ticker, and the leaderboard plugin. Each plugin's
|
|
`background_service` block (under its own config namespace) follows the
|
|
same shape as the example above.
|
|
|
|
### Error Handling & Fallback
|
|
|
|
**Automatic Retry:**
|
|
- Exponential backoff (1s, 2s, 4s, 8s, ...)
|
|
- Maximum retry attempts configurable
|
|
- Logs all retry attempts
|
|
|
|
**Fallback Behavior:**
|
|
- If background service disabled: reverts to synchronous fetching
|
|
- If background fetch fails: returns cached data
|
|
- If no cache: returns empty/error state
|
|
|
|
### Testing
|
|
|
|
```bash
|
|
# Check logs for background operations
|
|
sudo journalctl -u ledmatrix -f | grep "background"
|
|
```
|
|
|
|
### Monitoring
|
|
|
|
**View Statistics:**
|
|
```python
|
|
from src.background_data_service import get_background_service
|
|
from src.cache_manager import CacheManager
|
|
|
|
service = get_background_service(CacheManager())
|
|
stats = service.get_statistics()
|
|
print(f"Active tasks: {stats['active_tasks']}")
|
|
print(f"Completed: {stats['completed']}")
|
|
print(f"Failed: {stats['failed']}")
|
|
```
|
|
|
|
**Enable Debug Logging:**
|
|
```python
|
|
import logging
|
|
logging.getLogger('src.background_data_service').setLevel(logging.DEBUG)
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Permission Management
|
|
|
|
### Overview
|
|
|
|
LEDMatrix uses a dual-user architecture: the display service runs as root (hardware access), while the web interface runs as a non-privileged user. Centralized permission management ensures both can access necessary files.
|
|
|
|
### Why It Matters
|
|
|
|
**Problem:**
|
|
- Root service creates files with root ownership
|
|
- Web user cannot read/write those files
|
|
- Results in `PermissionError` exceptions
|
|
|
|
**Solution:**
|
|
- Set group ownership to shared group
|
|
- Grant group write permissions
|
|
- Use setgid bit for automatic inheritance
|
|
|
|
### Permission Utilities
|
|
|
|
```python
|
|
from src.common.permission_utils import (
|
|
ensure_directory_permissions,
|
|
ensure_file_permissions,
|
|
get_config_file_mode,
|
|
get_assets_file_mode,
|
|
get_assets_dir_mode,
|
|
get_plugin_file_mode,
|
|
get_cache_dir_mode
|
|
)
|
|
|
|
# Create directory with correct permissions
|
|
ensure_directory_permissions(Path("assets/sports"), get_assets_dir_mode())
|
|
|
|
# Set file permissions after writing
|
|
# (get_config_file_mode requires the file path — secrets files get a
|
|
# stricter mode than the main config)
|
|
config_path = Path("config/config.json")
|
|
ensure_file_permissions(config_path, get_config_file_mode(config_path))
|
|
```
|
|
|
|
### When to Use Utilities
|
|
|
|
**Use permission utilities when:**
|
|
1. Creating new directories
|
|
2. Writing configuration files
|
|
3. Downloading/creating asset files (logos, fonts)
|
|
4. Creating plugin files
|
|
5. Writing cache files
|
|
|
|
**Don't use for:**
|
|
1. Reading files (permissions don't change)
|
|
2. Temporary files in `/tmp`
|
|
3. Files in already-managed directories (if parent has setgid)
|
|
|
|
### Permission Standards
|
|
|
|
**File Permissions:**
|
|
|
|
| File Type | Mode | Octal | Description |
|
|
|-----------|------|-------|-------------|
|
|
| Config (main) | `rw-r--r--` | `0o644` | Owner write, all read |
|
|
| Config (secrets) | `rw-r-----` | `0o640` | Owner write, group read |
|
|
| Assets | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
|
| Plugins | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
|
| Cache files | `rw-rw-r--` | `0o664` | Owner/group write, all read |
|
|
|
|
**Directory Permissions:**
|
|
|
|
| Directory Type | Mode | Octal | Description |
|
|
|----------------|------|-------|-------------|
|
|
| All directories | `rwxrwsr-x` | `0o2775` | With setgid bit for inheritance |
|
|
|
|
**Note:** The `s` in `rwxrwsr-x` is the setgid bit (2000), which makes new files inherit the directory's group ownership.
|
|
|
|
### Common Patterns
|
|
|
|
**Pattern 1: Creating Config Directory**
|
|
```python
|
|
from pathlib import Path
|
|
from src.common.permission_utils import ensure_directory_permissions, get_config_dir_mode
|
|
|
|
config_dir = Path("config/plugins")
|
|
ensure_directory_permissions(config_dir, get_config_dir_mode())
|
|
```
|
|
|
|
**Pattern 2: Saving Config File**
|
|
```python
|
|
from src.common.permission_utils import ensure_file_permissions, get_config_file_mode
|
|
|
|
config_path = Path("config/config.json")
|
|
with open(config_path, 'w') as f:
|
|
json.dump(data, f)
|
|
ensure_file_permissions(config_path, get_config_file_mode(config_path))
|
|
```
|
|
|
|
**Pattern 3: Downloading Logo**
|
|
```python
|
|
from src.common.permission_utils import ensure_directory_permissions, ensure_file_permissions
|
|
from src.common.permission_utils import get_assets_dir_mode, get_assets_file_mode
|
|
|
|
logo_path = Path("assets/sports/nhl/logo.png")
|
|
ensure_directory_permissions(logo_path.parent, get_assets_dir_mode())
|
|
# ... download and save logo ...
|
|
ensure_file_permissions(logo_path, get_assets_file_mode())
|
|
```
|
|
|
|
**Pattern 4: Creating Plugin File**
|
|
```python
|
|
from src.common.permission_utils import ensure_file_permissions, get_plugin_file_mode
|
|
|
|
plugin_file = Path("plugins/my-plugin/data.json")
|
|
with open(plugin_file, 'w') as f:
|
|
json.dump(data, f)
|
|
ensure_file_permissions(plugin_file, get_plugin_file_mode())
|
|
```
|
|
|
|
**Pattern 5: Cache Directory Setup**
|
|
```python
|
|
from src.common.permission_utils import ensure_directory_permissions, get_cache_dir_mode
|
|
|
|
cache_dir = Path("cache/plugin-name")
|
|
ensure_directory_permissions(cache_dir, get_cache_dir_mode())
|
|
```
|
|
|
|
### Integration with Core Utilities
|
|
|
|
These core utilities **already handle permissions** - you don't need to call permission utilities when using them:
|
|
|
|
- **ConfigManager** - Handles config file permissions
|
|
- **CacheManager** - Handles cache file permissions
|
|
- **LogoHelper** - Handles logo file permissions
|
|
- **PluginManager** - Handles plugin file permissions
|
|
|
|
### Manual Fixes
|
|
|
|
If you encounter permission issues:
|
|
|
|
```bash
|
|
# Targeted permission fixes (see scripts/fix_perms/README.md)
|
|
sudo ./scripts/fix_perms/fix_assets_permissions.sh # assets/ tree (logos, fonts)
|
|
sudo ./scripts/fix_perms/fix_cache_permissions.sh # all cache directories
|
|
sudo ./scripts/fix_perms/fix_plugin_permissions.sh # plugin directories
|
|
sudo ./scripts/fix_perms/fix_web_permissions.sh # web interface files
|
|
|
|
# Fix specific directory
|
|
sudo chown -R ledpi:ledpi /home/ledpi/LEDMatrix/config
|
|
sudo chmod -R 2775 /home/ledpi/LEDMatrix/config
|
|
sudo find /home/ledpi/LEDMatrix/config -type f -exec chmod 664 {} \;
|
|
|
|
# Verify permissions
|
|
ls -la config/
|
|
ls -la assets/
|
|
```
|
|
|
|
### Verification
|
|
|
|
```bash
|
|
# Check directory has setgid bit
|
|
ls -ld assets/
|
|
# Should show: drwxrwsr-x (note the 's')
|
|
|
|
# Check file has correct group
|
|
ls -l assets/logo.png
|
|
# Should show group 'ledpi'
|
|
|
|
# Check file permissions
|
|
stat -c "%a %n" config/config.json
|
|
# Should show: 644 config/config.json
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [PLUGIN_DEVELOPMENT_GUIDE.md](PLUGIN_DEVELOPMENT_GUIDE.md) - Creating plugins with Vegas/on-demand support
|
|
- [WEB_INTERFACE_GUIDE.md](WEB_INTERFACE_GUIDE.md) - Using on-demand controls in web UI
|
|
- [PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md) - Complete API documentation
|
|
- [DEVELOPMENT.md](DEVELOPMENT.md) - Development environment and testing
|