Enhance LED Matrix web interface with comprehensive config and monitoring

Co-authored-by: charlesmynard <charlesmynard@gmail.com>
This commit is contained in:
Cursor Agent
2025-07-28 03:08:55 +00:00
parent 4a7138205c
commit 20082cbadf
4 changed files with 1662 additions and 123 deletions

View File

@@ -0,0 +1,156 @@
# LED Matrix Web Interface V2 - Enhanced Summary
## Overview
The enhanced LED Matrix Web Interface V2 now includes comprehensive configuration options, improved display preview, CPU utilization monitoring, and all features from the original web interface while maintaining a modern, user-friendly design.
## Key Enhancements
### 1. Complete LED Matrix Configuration Options
- **Hardware Settings**: All LED Matrix hardware options are now configurable through the web UI
- Rows, Columns, Chain Length, Parallel chains
- Brightness (with real-time slider)
- Hardware Mapping (Adafruit HAT PWM, HAT, Regular, Pi1)
- GPIO Slowdown, Scan Mode
- PWM Bits, PWM Dither Bits, PWM LSB Nanoseconds
- Limit Refresh Rate, Hardware Pulsing, Inverse Colors
- Show Refresh Rate, Short Date Format options
### 2. Enhanced System Monitoring
- **CPU Utilization**: Real-time CPU usage percentage display
- **Memory Usage**: Improved memory monitoring using psutil
- **Disk Usage**: Added disk space monitoring
- **CPU Temperature**: Existing temperature monitoring preserved
- **System Uptime**: Real-time uptime display
- **Service Status**: LED Matrix service status monitoring
### 3. Improved Display Preview
- **8x Scaling**: Increased from 4x to 8x scaling for better visibility
- **Better Error Handling**: Proper fallback when no display data is available
- **Smoother Updates**: Increased update frequency from 10fps to 20fps
- **Enhanced Styling**: Better border and background styling for the preview area
### 4. Comprehensive Configuration Tabs
- **Overview**: System stats with CPU, memory, temperature, disk usage
- **Schedule**: Display on/off scheduling
- **Display**: Complete LED Matrix hardware configuration
- **Sports**: Sports leagues configuration (placeholder for full implementation)
- **Weather**: Weather service configuration
- **Stocks**: Stock and cryptocurrency ticker configuration
- **Features**: Additional features like clock, text display, etc.
- **Music**: Music display configuration (YouTube Music, Spotify)
- **Calendar**: Google Calendar integration settings
- **News**: RSS news feeds management with custom feeds
- **API Keys**: Secure API key management for all services
- **Editor**: Visual display editor for custom layouts
- **Actions**: System control actions (start/stop, reboot, updates)
- **Raw JSON**: Direct JSON configuration editing with validation
- **Logs**: System logs viewing and refresh
### 5. Enhanced JSON Editor
- **Real-time Validation**: Live JSON syntax validation
- **Visual Status Indicators**: Color-coded status (Valid/Invalid/Warning)
- **Format Function**: Automatic JSON formatting
- **Error Details**: Detailed error messages with line numbers
- **Syntax Highlighting**: Monospace font with proper styling
### 6. News Manager Integration
- **RSS Feed Management**: Add/remove custom RSS feeds
- **Feed Selection**: Enable/disable built-in news feeds
- **Headlines Configuration**: Configure headlines per feed
- **Rotation Settings**: Enable headline rotation
- **Status Monitoring**: Real-time news manager status
### 7. Form Handling & Validation
- **Async Form Submission**: All forms use modern async/await patterns
- **Real-time Feedback**: Immediate success/error notifications
- **Input Validation**: Client-side and server-side validation
- **Auto-save Features**: Some settings auto-save on change
### 8. Responsive Design Improvements
- **Mobile Friendly**: Better mobile responsiveness
- **Flexible Layout**: Grid-based responsive layout
- **Tab Wrapping**: Tabs wrap on smaller screens
- **Scrollable Content**: Tab content scrolls when needed
### 9. Backend Enhancements
- **psutil Integration**: Added psutil for better system monitoring
- **Route Compatibility**: All original web interface routes preserved
- **Error Handling**: Improved error handling and logging
- **Configuration Management**: Better config file handling
### 10. User Experience Improvements
- **Loading States**: Loading indicators for async operations
- **Connection Status**: WebSocket connection status indicator
- **Notifications**: Toast-style notifications for all actions
- **Tooltips & Descriptions**: Helpful descriptions for all settings
- **Visual Feedback**: Hover effects and transitions
## Technical Implementation
### Dependencies Added
- `psutil>=5.9.0` - System monitoring
- Updated Flask and related packages for better compatibility
### File Structure
```
├── web_interface_v2.py # Enhanced backend with all features
├── templates/index_v2.html # Complete frontend with all tabs
├── requirements_web_v2.txt # Updated dependencies
├── start_web_v2.py # Startup script (unchanged)
└── WEB_INTERFACE_V2_ENHANCED_SUMMARY.md # This summary
```
### Key Features Preserved from Original
- All configuration options from the original web interface
- JSON linter with validation and formatting
- System actions (start/stop service, reboot, git pull)
- API key management
- News manager functionality
- Sports configuration
- Display duration settings
- All form validation and error handling
### New Features Added
- CPU utilization monitoring
- Enhanced display preview (8x scaling, 20fps)
- Complete LED Matrix hardware configuration
- Improved responsive design
- Better error handling and user feedback
- Real-time system stats updates
- Enhanced JSON editor with validation
- Visual status indicators throughout
## Usage
1. **Start the Enhanced Interface**:
```bash
python3 start_web_v2.py
```
2. **Access the Interface**:
Open browser to `http://your-pi-ip:5001`
3. **Configure LED Matrix**:
- Go to "Display" tab for hardware settings
- Use "Schedule" tab for timing
- Configure services in respective tabs
4. **Monitor System**:
- "Overview" tab shows real-time stats
- CPU, memory, disk, and temperature monitoring
5. **Edit Configurations**:
- Use individual tabs for specific settings
- "Raw JSON" tab for direct configuration editing
- Real-time validation and error feedback
## Benefits
1. **Complete Control**: Every LED Matrix configuration option is now accessible
2. **Better Monitoring**: Real-time system performance monitoring
3. **Improved Usability**: Modern, responsive interface with better UX
4. **Enhanced Preview**: Better display preview with higher resolution
5. **Comprehensive Management**: All features in one unified interface
6. **Backward Compatibility**: All original features preserved and enhanced
The enhanced web interface provides a complete, professional-grade management system for LED Matrix displays while maintaining ease of use and reliability.

View File

@@ -1,8 +1,7 @@
Flask==2.3.3
Flask-SocketIO==5.3.6
Pillow>=9.0.0
python-socketio>=5.0.0
eventlet>=0.33.0
freetype-py==2.5.1
requests>=2.32.0
pytz==2023.3
flask>=2.3.0
flask-socketio>=5.3.0
python-socketio>=5.8.0
eventlet>=0.33.3
Pillow>=10.0.0
psutil>=5.9.0
werkzeug>=2.3.0

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import subprocess
import threading
import time
import base64
import psutil
from pathlib import Path
from src.config_manager import ConfigManager
from src.display_manager import DisplayManager
@@ -51,10 +52,10 @@ class DisplayMonitor:
if display_manager and hasattr(display_manager, 'image'):
# Convert PIL image to base64 for web display
img_buffer = io.BytesIO()
# Scale up the image for better visibility
# Scale up the image for better visibility (8x instead of 4x for better clarity)
scaled_img = display_manager.image.resize((
display_manager.image.width * 4,
display_manager.image.height * 4
display_manager.image.width * 8,
display_manager.image.height * 8
), Image.NEAREST)
scaled_img.save(img_buffer, format='PNG')
img_str = base64.b64encode(img_buffer.getvalue()).decode()
@@ -72,7 +73,7 @@ class DisplayMonitor:
except Exception as e:
print(f"Display monitor error: {e}")
time.sleep(0.1) # Update 10 times per second
time.sleep(0.05) # Update 20 times per second for smoother display
display_monitor = DisplayMonitor()
@@ -82,12 +83,24 @@ def index():
main_config = config_manager.load_config()
schedule_config = main_config.get('schedule', {})
# Get system status
# Get system status including CPU utilization
system_status = get_system_status()
# Get raw config data for JSON editors
main_config_data = config_manager.get_raw_file_content('main')
secrets_config_data = config_manager.get_raw_file_content('secrets')
main_config_json = json.dumps(main_config_data, indent=4)
secrets_config_json = json.dumps(secrets_config_data, indent=4)
return render_template('index_v2.html',
schedule_config=schedule_config,
main_config=main_config,
main_config_data=main_config_data,
secrets_config=secrets_config_data,
main_config_json=main_config_json,
secrets_config_json=secrets_config_json,
main_config_path=config_manager.get_config_path(),
secrets_config_path=config_manager.get_secrets_path(),
system_status=system_status,
editor_mode=editor_mode)
@@ -96,24 +109,29 @@ def index():
return render_template('index_v2.html',
schedule_config={},
main_config={},
main_config_data={},
secrets_config={},
main_config_json="{}",
secrets_config_json="{}",
main_config_path="",
secrets_config_path="",
system_status={},
editor_mode=False)
def get_system_status():
"""Get current system status including display state and performance metrics."""
"""Get current system status including display state, performance metrics, and CPU utilization."""
try:
# Check if display service is running
result = subprocess.run(['sudo', 'systemctl', 'is-active', 'ledmatrix'],
capture_output=True, text=True)
service_active = result.stdout.strip() == 'active'
# Get memory usage
with open('/proc/meminfo', 'r') as f:
meminfo = f.read()
# Get memory usage using psutil for better accuracy
memory = psutil.virtual_memory()
mem_used_percent = round(memory.percent, 1)
mem_total = int([line for line in meminfo.split('\n') if 'MemTotal' in line][0].split()[1])
mem_available = int([line for line in meminfo.split('\n') if 'MemAvailable' in line][0].split()[1])
mem_used_percent = round((mem_total - mem_available) / mem_total * 100, 1)
# Get CPU utilization
cpu_percent = round(psutil.cpu_percent(interval=0.1), 1)
# Get CPU temperature
try:
@@ -129,10 +147,16 @@ def get_system_status():
uptime_hours = int(uptime_seconds // 3600)
uptime_minutes = int((uptime_seconds % 3600) // 60)
# Get disk usage
disk = psutil.disk_usage('/')
disk_used_percent = round((disk.used / disk.total) * 100, 1)
return {
'service_active': service_active,
'memory_used_percent': mem_used_percent,
'cpu_percent': cpu_percent,
'cpu_temp': round(temp, 1),
'disk_used_percent': disk_used_percent,
'uptime': f"{uptime_hours}h {uptime_minutes}m",
'display_connected': display_manager is not None,
'editor_mode': editor_mode
@@ -141,7 +165,9 @@ def get_system_status():
return {
'service_active': False,
'memory_used_percent': 0,
'cpu_percent': 0,
'cpu_temp': 0,
'disk_used_percent': 0,
'uptime': '0h 0m',
'display_connected': False,
'editor_mode': False,
@@ -372,6 +398,380 @@ def get_system_status_api():
"""Get system status as JSON."""
return jsonify(get_system_status())
# Add all the routes from the original web interface for compatibility
@app.route('/save_schedule', methods=['POST'])
def save_schedule_route():
try:
main_config = config_manager.load_config()
schedule_data = {
'enabled': 'schedule_enabled' in request.form,
'start_time': request.form.get('start_time', '07:00'),
'end_time': request.form.get('end_time', '22:00')
}
main_config['schedule'] = schedule_data
config_manager.save_config(main_config)
return jsonify({
'status': 'success',
'message': 'Schedule updated successfully! Restart the display for changes to take effect.'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error saving schedule: {e}'
}), 400
@app.route('/save_config', methods=['POST'])
def save_config_route():
config_type = request.form.get('config_type')
config_data_str = request.form.get('config_data')
try:
if config_type == 'main':
# Handle form-based configuration updates
main_config = config_manager.load_config()
# Update display settings
if 'rows' in request.form:
main_config['display']['hardware']['rows'] = int(request.form.get('rows', 32))
main_config['display']['hardware']['cols'] = int(request.form.get('cols', 64))
main_config['display']['hardware']['chain_length'] = int(request.form.get('chain_length', 2))
main_config['display']['hardware']['parallel'] = int(request.form.get('parallel', 1))
main_config['display']['hardware']['brightness'] = int(request.form.get('brightness', 95))
main_config['display']['hardware']['hardware_mapping'] = request.form.get('hardware_mapping', 'adafruit-hat-pwm')
main_config['display']['runtime']['gpio_slowdown'] = int(request.form.get('gpio_slowdown', 3))
# Add all the missing LED Matrix hardware options
main_config['display']['hardware']['scan_mode'] = int(request.form.get('scan_mode', 0))
main_config['display']['hardware']['pwm_bits'] = int(request.form.get('pwm_bits', 9))
main_config['display']['hardware']['pwm_dither_bits'] = int(request.form.get('pwm_dither_bits', 1))
main_config['display']['hardware']['pwm_lsb_nanoseconds'] = int(request.form.get('pwm_lsb_nanoseconds', 130))
main_config['display']['hardware']['disable_hardware_pulsing'] = 'disable_hardware_pulsing' in request.form
main_config['display']['hardware']['inverse_colors'] = 'inverse_colors' in request.form
main_config['display']['hardware']['show_refresh_rate'] = 'show_refresh_rate' in request.form
main_config['display']['hardware']['limit_refresh_rate_hz'] = int(request.form.get('limit_refresh_rate_hz', 120))
main_config['display']['use_short_date_format'] = 'use_short_date_format' in request.form
# If config_data is provided as JSON, merge it
if config_data_str:
try:
new_data = json.loads(config_data_str)
# Merge the new data with existing config
for key, value in new_data.items():
if key in main_config:
if isinstance(value, dict) and isinstance(main_config[key], dict):
merge_dict(main_config[key], value)
else:
main_config[key] = value
else:
main_config[key] = value
except json.JSONDecodeError:
return jsonify({
'status': 'error',
'message': 'Error: Invalid JSON format in config data.'
}), 400
config_manager.save_config(main_config)
return jsonify({
'status': 'success',
'message': 'Main configuration saved successfully!'
})
elif config_type == 'secrets':
# Handle secrets configuration
secrets_config = config_manager.get_raw_file_content('secrets')
# If config_data is provided as JSON, use it
if config_data_str:
try:
new_data = json.loads(config_data_str)
config_manager.save_raw_file_content('secrets', new_data)
except json.JSONDecodeError:
return jsonify({
'status': 'error',
'message': 'Error: Invalid JSON format for secrets config.'
}), 400
else:
config_manager.save_raw_file_content('secrets', secrets_config)
return jsonify({
'status': 'success',
'message': 'Secrets configuration saved successfully!'
})
except json.JSONDecodeError:
return jsonify({
'status': 'error',
'message': f'Error: Invalid JSON format for {config_type} config.'
}), 400
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error saving {config_type} configuration: {e}'
}), 400
@app.route('/run_action', methods=['POST'])
def run_action_route():
try:
data = request.get_json()
action = data.get('action')
if action == 'start_display':
result = subprocess.run(['sudo', 'systemctl', 'start', 'ledmatrix'],
capture_output=True, text=True)
elif action == 'stop_display':
result = subprocess.run(['sudo', 'systemctl', 'stop', 'ledmatrix'],
capture_output=True, text=True)
elif action == 'enable_autostart':
result = subprocess.run(['sudo', 'systemctl', 'enable', 'ledmatrix'],
capture_output=True, text=True)
elif action == 'disable_autostart':
result = subprocess.run(['sudo', 'systemctl', 'disable', 'ledmatrix'],
capture_output=True, text=True)
elif action == 'reboot_system':
result = subprocess.run(['sudo', 'reboot'],
capture_output=True, text=True)
elif action == 'git_pull':
home_dir = str(Path.home())
project_dir = os.path.join(home_dir, 'LEDMatrix')
result = subprocess.run(['git', 'pull'],
capture_output=True, text=True, cwd=project_dir, check=True)
else:
return jsonify({
'status': 'error',
'message': f'Unknown action: {action}'
}), 400
return jsonify({
'status': 'success' if result.returncode == 0 else 'error',
'message': f'Action {action} completed with return code {result.returncode}',
'stdout': result.stdout,
'stderr': result.stderr
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error running action: {e}'
}), 400
@app.route('/get_logs', methods=['GET'])
def get_logs():
try:
# Get logs from journalctl for the ledmatrix service
result = subprocess.run(
['sudo', 'journalctl', '-u', 'ledmatrix.service', '-n', '500', '--no-pager'],
capture_output=True, text=True, check=True
)
logs = result.stdout
return jsonify({'status': 'success', 'logs': logs})
except subprocess.CalledProcessError as e:
# If the command fails, return the error
error_message = f"Error fetching logs: {e.stderr}"
return jsonify({'status': 'error', 'message': error_message}), 500
except Exception as e:
# Handle other potential exceptions
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/save_raw_json', methods=['POST'])
def save_raw_json_route():
try:
data = request.get_json()
config_type = data.get('config_type')
config_data = data.get('config_data')
if not config_type or not config_data:
return jsonify({
'status': 'error',
'message': 'Missing config_type or config_data'
}), 400
if config_type not in ['main', 'secrets']:
return jsonify({
'status': 'error',
'message': 'Invalid config_type. Must be "main" or "secrets"'
}), 400
# Validate JSON format
try:
parsed_data = json.loads(config_data)
except json.JSONDecodeError as e:
return jsonify({
'status': 'error',
'message': f'Invalid JSON format: {str(e)}'
}), 400
# Save the raw JSON
config_manager.save_raw_file_content(config_type, parsed_data)
return jsonify({
'status': 'success',
'message': f'{config_type.capitalize()} configuration saved successfully!'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error saving raw JSON: {str(e)}'
}), 400
# Add news manager routes for compatibility
@app.route('/news_manager/status', methods=['GET'])
def get_news_manager_status():
"""Get news manager status and configuration"""
try:
config = config_manager.load_config()
news_config = config.get('news_manager', {})
# Try to get status from the running display controller if possible
status = {
'enabled': news_config.get('enabled', False),
'enabled_feeds': news_config.get('enabled_feeds', []),
'available_feeds': [
'MLB', 'NFL', 'NCAA FB', 'NHL', 'NBA', 'TOP SPORTS',
'BIG10', 'NCAA', 'Other'
],
'headlines_per_feed': news_config.get('headlines_per_feed', 2),
'rotation_enabled': news_config.get('rotation_enabled', True),
'custom_feeds': news_config.get('custom_feeds', {})
}
return jsonify({
'status': 'success',
'data': status
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error getting news manager status: {str(e)}'
}), 400
@app.route('/news_manager/update_feeds', methods=['POST'])
def update_news_feeds():
"""Update enabled news feeds"""
try:
data = request.get_json()
enabled_feeds = data.get('enabled_feeds', [])
headlines_per_feed = data.get('headlines_per_feed', 2)
config = config_manager.load_config()
if 'news_manager' not in config:
config['news_manager'] = {}
config['news_manager']['enabled_feeds'] = enabled_feeds
config['news_manager']['headlines_per_feed'] = headlines_per_feed
config_manager.save_config(config)
return jsonify({
'status': 'success',
'message': 'News feeds updated successfully!'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error updating news feeds: {str(e)}'
}), 400
@app.route('/news_manager/add_custom_feed', methods=['POST'])
def add_custom_news_feed():
"""Add a custom RSS feed"""
try:
data = request.get_json()
name = data.get('name', '').strip()
url = data.get('url', '').strip()
if not name or not url:
return jsonify({
'status': 'error',
'message': 'Name and URL are required'
}), 400
config = config_manager.load_config()
if 'news_manager' not in config:
config['news_manager'] = {}
if 'custom_feeds' not in config['news_manager']:
config['news_manager']['custom_feeds'] = {}
config['news_manager']['custom_feeds'][name] = url
config_manager.save_config(config)
return jsonify({
'status': 'success',
'message': f'Custom feed "{name}" added successfully!'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error adding custom feed: {str(e)}'
}), 400
@app.route('/news_manager/remove_custom_feed', methods=['POST'])
def remove_custom_news_feed():
"""Remove a custom RSS feed"""
try:
data = request.get_json()
name = data.get('name', '').strip()
if not name:
return jsonify({
'status': 'error',
'message': 'Feed name is required'
}), 400
config = config_manager.load_config()
custom_feeds = config.get('news_manager', {}).get('custom_feeds', {})
if name in custom_feeds:
del custom_feeds[name]
config_manager.save_config(config)
return jsonify({
'status': 'success',
'message': f'Custom feed "{name}" removed successfully!'
})
else:
return jsonify({
'status': 'error',
'message': f'Custom feed "{name}" not found'
}), 404
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error removing custom feed: {str(e)}'
}), 400
@app.route('/news_manager/toggle', methods=['POST'])
def toggle_news_manager():
"""Toggle news manager on/off"""
try:
data = request.get_json()
enabled = data.get('enabled', False)
config = config_manager.load_config()
if 'news_manager' not in config:
config['news_manager'] = {}
config['news_manager']['enabled'] = enabled
config_manager.save_config(config)
return jsonify({
'status': 'success',
'message': f'News manager {"enabled" if enabled else "disabled"} successfully!'
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Error toggling news manager: {str(e)}'
}), 400
@app.route('/logs')
def view_logs():
"""View system logs."""