* feat(display): add Vegas-style continuous scroll mode Implement an opt-in Vegas ticker mode that composes all enabled plugin content into a single continuous horizontal scroll. Includes a modular package (src/vegas_mode/) with double-buffered streaming, 125 FPS render pipeline using the existing ScrollHelper, live priority interruption support, and a web UI for configuration with drag-drop plugin ordering. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): add three-mode display system (SCROLL, FIXED_SEGMENT, STATIC) Adds a flexible display mode system for Vegas scroll mode that allows plugins to control how their content appears in the continuous scroll: - SCROLL: Content scrolls continuously (multi-item plugins like sports) - FIXED_SEGMENT: Fixed block that scrolls by (clock, weather) - STATIC: Scroll pauses, plugin displays, then resumes (alerts) Changes: - Add VegasDisplayMode enum to base_plugin.py with backward-compatible mapping from legacy get_vegas_content_type() - Add static pause handling to coordinator with scroll position save/restore - Add mode-aware content composition to stream_manager - Add vegas_mode info to /api/v3/plugins/installed endpoint - Add mode indicators to Vegas settings UI - Add comprehensive plugin developer documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas,widgets): address validation, thread safety, and XSS issues Vegas mode fixes: - config.py: align validation limits with UI (scroll_speed max 200, separator_width max 128) - coordinator.py: fix race condition by properly initializing _pending_config - plugin_adapter.py: remove unused import - render_pipeline.py: preserve deque type in reset() method - stream_manager.py: fix lock handling and swap_buffers to truly swap API fixes: - api_v3.py: normalize boolean checkbox values, validate numeric fields, ensure JSON arrays Widget fixes: - day-selector.js: remove escapeHtml from JSON.stringify to prevent corruption - password-input.js: use deterministic color class mapping for Tailwind JIT - radio-group.js: replace inline onchange with addEventListener to prevent XSS - select-dropdown.js: guard global registry access - slider.js: add escapeAttr for attributes, fix null dereference in setValue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve exception handling and static pause state management coordinator.py: - _check_live_priority: use logger.exception for full traceback - _end_static_pause: guard scroll resume on interruption (stop/live priority) - _update_static_mode_plugins: log errors instead of silently swallowing render_pipeline.py: - compose_scroll_content: use specific exceptions and logger.exception - render_frame: use specific exceptions and logger.exception - hot_swap_content: use specific exceptions and logger.exception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add interrupt mechanism and improve config/exception handling - Add interrupt checker callback to Vegas coordinator for responsive handling of on-demand requests and wifi status during Vegas mode - Fix config.py update() to include dynamic duration fields - Fix is_plugin_included() consistency with get_ordered_plugins() - Update _apply_pending_config to propagate config to StreamManager - Change _fetch_plugin_content to use logger.exception for traceback - Replace bare except in _refresh_plugin_list with specific exceptions - Add aria-label accessibility to Vegas toggle checkbox - Fix XSS vulnerability in plugin metadata rendering with escapeHtml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve logging, validation, lock handling, and config updates - display_controller.py: use logger.exception for Vegas errors with traceback - base_plugin.py: validate vegas_panel_count as positive integer with warning - coordinator.py: fix _apply_pending_config to avoid losing concurrent updates by clearing _pending_config while holding lock - plugin_adapter.py: remove broad catch-all, use narrower exception types (AttributeError, TypeError, ValueError, OSError, RuntimeError) and logger.exception for traceback preservation - api_v3.py: only update vegas_config['enabled'] when key is present in data to prevent incorrect disabling when checkbox is omitted Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve cycle advancement, logging, and accessibility - Add advance_cycle() method to StreamManager for clearing buffer between cycles - Call advance_cycle() in RenderPipeline.start_new_cycle() for fresh content - Use logger.exception() for interrupt check and static pause errors (full tracebacks) - Add id="vegas_scroll_label" to h3 for aria-labelledby reference - Call updatePluginConfig() after rendering plugin list for proper initialization Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add thread-safety, preserve updates, and improve logging - display_controller.py: Use logger.exception() for Vegas import errors - plugin_adapter.py: Add thread-safe cache lock, remove unused exception binding - stream_manager.py: In-place merge in process_updates() preserves non-updated plugins - api_v3.py: Change vegas_scroll_enabled default from False to True Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): add debug logging and narrow exception types - stream_manager.py: Log when get_vegas_display_mode() is unavailable - stream_manager.py: Narrow exception type from Exception to (AttributeError, TypeError) - api_v3.py: Log exceptions when reading Vegas display metadata with plugin context Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): fix method call and improve exception logging - Fix _check_vegas_interrupt() calling nonexistent _check_wifi_status(), now correctly calls _check_wifi_status_message() - Update _refresh_plugin_list() exception handler to use logger.exception() with plugin_id and class name for remote debugging Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(web): replace complex toggle with standard checkbox for Vegas mode The Tailwind pseudo-element toggle (after:content-[''], etc.) wasn't rendering because these classes weren't in the CSS bundle. Replaced with a simple checkbox that matches other form controls in the template. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(vegas): add detailed logging to _refresh_plugin_list Track why plugins aren't being found for Vegas scroll: - Log count of loaded plugins - Log enabled status for each plugin - Log content_type and display_mode checks - Log when plugin_manager lacks loaded_plugins Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): use correct attribute name for plugin manager StreamManager and VegasModeCoordinator were checking for plugin_manager.loaded_plugins but PluginManager stores active plugins in plugin_manager.plugins. This caused Vegas scroll to find zero plugins despite plugins being available. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): convert scroll_speed from px/sec to px/frame correctly The config scroll_speed is in pixels per second, but ScrollHelper in frame_based_scrolling mode interprets it as pixels per frame. Previously this caused the speed to be clamped to max 5.0 regardless of the configured value. Now properly converts: pixels_per_frame = scroll_speed * scroll_delay With defaults (50 px/s, 0.02s delay), this gives 1 px/frame = 50 px/s. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): add FPS logging every 5 seconds Logs actual FPS vs target FPS to help diagnose performance issues. Shows frame count in each 5-second interval. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): improve plugin content capture reliability - Call update_data() before capture to ensure fresh plugin data - Try display() without force_clear first, fallback if TypeError - Retry capture with force_clear=True if first attempt is blank - Use histogram-based blank detection instead of point sampling (more reliable for content positioned anywhere in frame) This should help capture content from plugins that don't implement get_vegas_content() natively. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): handle callable width/height on display_manager DisplayManager.width and .height may be methods or properties depending on the implementation. Use callable() check to call them if needed, ensuring display_width and display_height are always integers. Fixes potential TypeError when width/height are methods. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): use logger.exception for display mode errors Replace logger.error with logger.exception to capture full stack trace when get_vegas_display_mode() fails on a plugin. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): protect plugin list updates with buffer lock Move assignment of _ordered_plugins and index resets under _buffer_lock to prevent race conditions with _prefetch_content() which reads these variables under the same lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): catch all exceptions in get_vegas_display_mode Broaden exception handling from AttributeError/TypeError to Exception so any plugin error in get_vegas_display_mode() doesn't abort the entire plugin list refresh. The loop continues with the default FIXED_SEGMENT mode. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(vegas): refresh stream manager when config updates After updating stream_manager.config, force a refresh to pick up changes to plugin_order, excluded_plugins, and buffer_ahead settings. Also use logger.exception to capture full stack traces on config update errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(vegas): add detailed logging for blank image detection * feat(vegas): extract full scroll content from plugins using ScrollHelper Plugins like ledmatrix-stocks and odds-ticker use ScrollHelper with a cached_image that contains their full scrolling content. Instead of falling back to single-frame capture, now check for scroll_helper.cached_image first to get the complete scrolling content for Vegas mode. * debug(vegas): add comprehensive INFO-level logging for plugin content flow - Log each plugin being processed with class name - Log which content methods are tried (native, scroll_helper, fallback) - Log success/failure of each method with image dimensions - Log brightness check results for blank image detection - Add visual separators in logs for easier debugging - Log plugin list refresh with enabled/excluded status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(vegas): trigger scroll content generation when cache is empty When a plugin has a scroll_helper but its cached_image is not yet populated, try to trigger content generation by: 1. Calling _create_scrolling_display() if available (stocks pattern) 2. Calling display(force_clear=True) as a fallback This allows plugins like stocks to provide their full scroll content even when Vegas mode starts before the plugin has run its normal display cycle. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve exception handling in plugin_adapter scroll content retrieval Replace broad except Exception handlers with narrow exception types (AttributeError, TypeError, ValueError, OSError) and use logger.exception instead of logger.warning/info to capture full stack traces for better diagnosability. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: narrow exception handling in coordinator and plugin_adapter - coordinator.py: Replace broad Exception catch around get_vegas_display_mode() with (AttributeError, TypeError) and use logger.exception for stack traces - plugin_adapter.py: Narrow update_data() exception handler to (AttributeError, RuntimeError, OSError) and use logger.exception Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve Vegas mode robustness and API validation - display_controller: Guard against None plugin_manager in Vegas init - coordinator: Restore scrolling state in resume() to match pause() - api_v3: Validate Vegas numeric fields with range checks and 400 errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
Vegas Scroll Mode - Plugin Developer Guide
Vegas scroll mode displays content from multiple plugins in a continuous horizontal scroll, similar to the news tickers seen in Las Vegas casinos. This guide explains how to integrate your plugin with Vegas mode.
Overview
When Vegas mode is enabled, the display controller composes content from all enabled plugins into a single continuous scroll. Each plugin can control how its content appears in the scroll using one of three display modes:
| Mode | Behavior | Best For |
|---|---|---|
| SCROLL | Content scrolls continuously within the stream | Multi-item plugins (sports scores, odds, news) |
| FIXED_SEGMENT | Fixed-width block that scrolls by | Static info (clock, weather, current temp) |
| STATIC | Scroll pauses, plugin displays for duration, then resumes | Important alerts, detailed views |
Quick Start
Minimal Integration (Zero Code Changes)
If you do nothing, your plugin will work with Vegas mode using these defaults:
- Plugins with
get_vegas_content_type() == 'multi'use SCROLL mode - Plugins with
get_vegas_content_type() == 'static'use FIXED_SEGMENT mode - Content is captured by calling your plugin's
display()method
Basic Integration
To provide optimized Vegas content, implement get_vegas_content():
from PIL import Image
class MyPlugin(BasePlugin):
def get_vegas_content(self):
"""Return content for Vegas scroll mode."""
# Return a single image for fixed content
return self._render_current_view()
# OR return multiple images for multi-item content
# return [self._render_item(item) for item in self.items]
Full Integration
For complete control over Vegas behavior, implement these methods:
from src.plugin_system.base_plugin import BasePlugin, VegasDisplayMode
class MyPlugin(BasePlugin):
def get_vegas_content_type(self) -> str:
"""Legacy method - determines default mode mapping."""
return 'multi' # or 'static' or 'none'
def get_vegas_display_mode(self) -> VegasDisplayMode:
"""Specify how this plugin behaves in Vegas scroll."""
return VegasDisplayMode.SCROLL
def get_supported_vegas_modes(self) -> list:
"""Return list of modes users can configure."""
return [VegasDisplayMode.SCROLL, VegasDisplayMode.FIXED_SEGMENT]
def get_vegas_content(self):
"""Return PIL Image(s) for the scroll."""
return [self._render_game(g) for g in self.games]
def get_vegas_segment_width(self) -> int:
"""For FIXED_SEGMENT: width in panels (optional)."""
return 2 # Use 2 panels width
Display Modes Explained
SCROLL Mode
Content scrolls continuously within the Vegas stream. Best for plugins with multiple items.
def get_vegas_display_mode(self):
return VegasDisplayMode.SCROLL
def get_vegas_content(self):
# Return list of images - each scrolls individually
images = []
for game in self.games:
img = Image.new('RGB', (200, 32))
# ... render game info ...
images.append(img)
return images
When to use:
- Sports scores with multiple games
- Stock/odds tickers with multiple items
- News feeds with multiple headlines
FIXED_SEGMENT Mode
Content is rendered as a fixed-width block that scrolls by with other content.
def get_vegas_display_mode(self):
return VegasDisplayMode.FIXED_SEGMENT
def get_vegas_content(self):
# Return single image at your preferred width
img = Image.new('RGB', (128, 32)) # 2 panels wide
# ... render clock/weather/etc ...
return img
def get_vegas_segment_width(self):
# Optional: specify width in panels
return 2
When to use:
- Clock display
- Current weather/temperature
- System status indicators
- Any "at a glance" information
STATIC Mode
Scroll pauses completely, your plugin displays using its normal display() method for its configured duration, then scroll resumes.
def get_vegas_display_mode(self):
return VegasDisplayMode.STATIC
def get_display_duration(self):
# How long to pause and show this plugin
return 10.0 # 10 seconds
When to use:
- Important alerts that need attention
- Detailed information that's hard to read while scrolling
- Interactive or animated content
- Content that requires the full display
User Configuration
Users can override the default display mode per-plugin in their config:
{
"my_plugin": {
"enabled": true,
"vegas_mode": "static", // Override: "scroll", "fixed", or "static"
"vegas_panel_count": 2, // Width in panels for fixed mode
"display_duration": 10 // Duration for static mode
}
}
The get_vegas_display_mode() method checks config first, then falls back to your implementation.
Content Rendering Guidelines
Image Dimensions
- Height: Must match display height (typically 32 pixels)
- Width:
- SCROLL: Any width, content will scroll
- FIXED_SEGMENT:
panels × single_panel_width(e.g., 2 × 64 = 128px)
Color Mode
Always use RGB mode for images:
img = Image.new('RGB', (width, 32), color=(0, 0, 0))
Performance Tips
- Cache rendered images - Don't re-render on every call
- Pre-render on update() - Render images when data changes, not when Vegas requests them
- Keep images small - Memory adds up with multiple plugins
class MyPlugin(BasePlugin):
def __init__(self, ...):
super().__init__(...)
self._cached_vegas_images = None
self._cache_valid = False
def update(self):
# Fetch new data
self.data = self._fetch_data()
# Invalidate cache so next Vegas request re-renders
self._cache_valid = False
def get_vegas_content(self):
if not self._cache_valid:
self._cached_vegas_images = self._render_all_items()
self._cache_valid = True
return self._cached_vegas_images
Fallback Behavior
If your plugin doesn't implement get_vegas_content(), Vegas mode will:
- Create a temporary canvas matching display dimensions
- Call your
display()method - Capture the resulting image
- Use that image in the scroll
This works but is less efficient than providing native Vegas content.
Excluding from Vegas Mode
To exclude your plugin from Vegas scroll entirely:
def get_vegas_content_type(self):
return 'none'
Or users can exclude via config:
{
"display": {
"vegas_scroll": {
"excluded_plugins": ["my_plugin"]
}
}
}
Complete Example
Here's a complete example of a weather plugin with full Vegas integration:
from PIL import Image, ImageDraw
from src.plugin_system.base_plugin import BasePlugin, VegasDisplayMode
class WeatherPlugin(BasePlugin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.temperature = None
self.conditions = None
self._vegas_image = None
def update(self):
"""Fetch weather data."""
data = self._fetch_weather_api()
self.temperature = data['temp']
self.conditions = data['conditions']
self._vegas_image = None # Invalidate cache
def display(self, force_clear=False):
"""Standard display for normal rotation."""
if force_clear:
self.display_manager.clear()
# Full weather display with details
self.display_manager.draw_text(
f"{self.temperature}°F",
x=10, y=8, color=(255, 255, 255)
)
self.display_manager.draw_text(
self.conditions,
x=10, y=20, color=(200, 200, 200)
)
self.display_manager.update_display()
# --- Vegas Mode Integration ---
def get_vegas_content_type(self):
"""Legacy compatibility."""
return 'static'
def get_vegas_display_mode(self):
"""Use FIXED_SEGMENT for compact weather display."""
# Allow user override via config
return super().get_vegas_display_mode()
def get_supported_vegas_modes(self):
"""Weather can work as fixed or static."""
return [VegasDisplayMode.FIXED_SEGMENT, VegasDisplayMode.STATIC]
def get_vegas_segment_width(self):
"""Weather needs 2 panels to show clearly."""
return self.config.get('vegas_panel_count', 2)
def get_vegas_content(self):
"""Render compact weather for Vegas scroll."""
if self._vegas_image is not None:
return self._vegas_image
# Create compact display (2 panels = 128px typical)
panel_width = 64 # From display.hardware.cols
panels = self.get_vegas_segment_width() or 2
width = panel_width * panels
height = 32
img = Image.new('RGB', (width, height), color=(0, 0, 40))
draw = ImageDraw.Draw(img)
# Draw compact weather
temp_text = f"{self.temperature}°"
draw.text((10, 8), temp_text, fill=(255, 255, 255))
draw.text((60, 8), self.conditions[:10], fill=(200, 200, 200))
self._vegas_image = img
return img
API Reference
VegasDisplayMode Enum
from src.plugin_system.base_plugin import VegasDisplayMode
VegasDisplayMode.SCROLL # "scroll" - continuous scrolling
VegasDisplayMode.FIXED_SEGMENT # "fixed" - fixed block in scroll
VegasDisplayMode.STATIC # "static" - pause scroll to display
BasePlugin Vegas Methods
| Method | Returns | Description |
|---|---|---|
get_vegas_content() |
Image or List[Image] or None |
Content for Vegas scroll |
get_vegas_content_type() |
str |
Legacy: 'multi', 'static', or 'none' |
get_vegas_display_mode() |
VegasDisplayMode |
How plugin behaves in Vegas |
get_supported_vegas_modes() |
List[VegasDisplayMode] |
Modes available for user config |
get_vegas_segment_width() |
int or None |
Width in panels for FIXED_SEGMENT |
Configuration Options
Per-plugin config:
{
"plugin_id": {
"vegas_mode": "scroll|fixed|static",
"vegas_panel_count": 2,
"display_duration": 15
}
}
Global Vegas config:
{
"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
}
}
}
Troubleshooting
Plugin not appearing in Vegas scroll
- Check
get_vegas_content_type()doesn't return'none' - Verify plugin is not in
excluded_pluginslist - Ensure plugin is enabled
Content looks wrong in scroll
- Verify image height matches display height (32px typical)
- Check image mode is 'RGB'
- Test with
get_vegas_content()returning a simple test image
STATIC mode not pausing
- Verify
get_vegas_display_mode()returnsVegasDisplayMode.STATIC - Check user hasn't overridden with
vegas_modein config - Ensure
display()method works correctly
Performance issues
- Implement image caching in
get_vegas_content() - Pre-render images in
update()instead of on-demand - Reduce image dimensions if possible