adding scheduling function to web ui and display

This commit is contained in:
Chuck
2025-07-08 19:33:08 -05:00
parent a0e7c662fb
commit 9bb4f088a6
4 changed files with 326 additions and 380 deletions

View File

@@ -1,5 +1,10 @@
{
"web_display_autostart": false,
"schedule": {
"enabled": false,
"start_time": "07:00",
"end_time": "23:00"
},
"timezone": "America/Chicago",
"location": {
"city": "Dallas",

View File

@@ -2,6 +2,7 @@ import time
import logging
import sys
from typing import Dict, Any, List
from datetime import datetime, time as time_obj
# Configure logging
logging.basicConfig(
@@ -411,6 +412,12 @@ class DisplayController:
logger.info(f"Initial display mode: {self.current_display_mode}")
logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager))
# --- SCHEDULING & CONFIG REFRESH ---
self.config_check_interval = 30
self.last_config_check = 0
self.is_display_active = True
self._load_config() # Initial load of schedule
def _handle_music_update(self, track_info: Dict[str, Any], significant_change: bool = False):
"""Callback for when music track info changes."""
# MusicManager now handles its own display state (album art, etc.)
@@ -712,6 +719,44 @@ class DisplayController:
self.ncaa_fb_current_team_index = (self.ncaa_fb_current_team_index + 1) % len(self.ncaa_fb_favorite_teams)
self.ncaa_fb_showing_recent = True # Reset to recent for the new team
# --- SCHEDULING METHODS ---
def _load_config(self):
"""Load configuration from the config manager and parse schedule settings."""
self.config = self.config_manager.load_config()
schedule_config = self.config.get('schedule', {})
self.schedule_enabled = schedule_config.get('enabled', False)
try:
self.start_time = datetime.strptime(schedule_config.get('start_time', '07:00'), '%H:%M').time()
self.end_time = datetime.strptime(schedule_config.get('end_time', '22:00'), '%H:%M').time()
except (ValueError, TypeError):
logger.warning("Invalid time format in schedule config. Using defaults.")
self.start_time = time_obj(7, 0)
self.end_time = time_obj(22, 0)
def _check_schedule(self):
"""Check if the display should be active based on the schedule."""
if not self.schedule_enabled:
if not self.is_display_active:
logger.info("Schedule is disabled. Activating display.")
self.is_display_active = True
return
now_time = datetime.now().time()
# Handle overnight schedules
if self.start_time <= self.end_time:
should_be_active = self.start_time <= now_time < self.end_time
else:
should_be_active = now_time >= self.start_time or now_time < self.end_time
if should_be_active and not self.is_display_active:
logger.info("Within scheduled time. Activating display.")
self.is_display_active = True
elif not should_be_active and self.is_display_active:
logger.info("Outside of scheduled time. Deactivating display.")
self.display_manager.clear()
self.is_display_active = False
def run(self):
"""Run the display controller, switching between displays."""
if not self.available_modes:
@@ -722,6 +767,17 @@ class DisplayController:
try:
while True:
current_time = time.time()
# Periodically check for config changes
if current_time - self.last_config_check > self.config_check_interval:
self._load_config()
self.last_config_check = current_time
# Enforce the schedule
self._check_schedule()
if not self.is_display_active:
time.sleep(60)
continue
# Update data for all modules first
self._update_modules()
@@ -1024,10 +1080,8 @@ class DisplayController:
# Force clear on the next iteration after an error to be safe
self.force_clear = True
# Small sleep removed - updates/drawing should manage timing
# time.sleep(self.update_interval)
#time.sleep(self.update_interval) # Re-add the sleep
# Add a short sleep to prevent high CPU usage
time.sleep(0.1)
except KeyboardInterrupt:
logger.info("Display controller stopped by user")

189
templates/index.html Normal file
View File

@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LED Matrix Config</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { max-width: 800px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
h1, h2 { text-align: center; color: #333; }
.tabs {
display: flex;
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
}
.tab-link {
padding: 10px 20px;
cursor: pointer;
border: none;
background-color: transparent;
font-size: 16px;
border-bottom: 3px solid transparent;
transition: border-bottom 0.3s;
}
.tab-link.active {
border-bottom: 3px solid #4CAF50;
font-weight: bold;
}
.tab-content { display: none; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="time"], input[type="checkbox"] {
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
}
button {
background-color: #4CAF50;
color: white;
padding: 12px 25px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
font-size: 16px;
}
button:hover { background-color: #45a049; }
.flash-messages { list-style: none; padding: 0; margin-bottom: 15px; }
.flash-messages li { padding: 10px; margin-bottom: 10px; border-radius: 4px; }
.flash-messages .success { background-color: #d4edda; color: #155724; }
.flash-messages .error { background-color: #f8d7da; color: #721c24; }
textarea {
width: 100%;
padding: 10px;
margin-top: 5px;
border-radius: 4px;
border: 1px solid #ccc;
box-sizing: border-box;
font-family: monospace;
min-height: 300px;
}
.filepath { font-family: monospace; background-color: #eee; padding: 2px 5px; border-radius: 3px; font-size: 0.9em;}
</style>
</head>
<body>
<div class="container">
<h1>LED Matrix Configuration</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flash-messages">
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div class="tabs">
<button class="tab-link active" onclick="openTab(event, 'schedule')">Schedule</button>
<button class="tab-link" onclick="openTab(event, 'main')">Main Config</button>
<button class="tab-link" onclick="openTab(event, 'secrets')">Secrets Config</button>
<button class="tab-link" onclick="openTab(event, 'actions')">Actions</button>
</div>
<!-- Schedule Tab -->
<div id="schedule" class="tab-content" style="display: block;">
<h2>Display Schedule</h2>
<p>Set the time for the display to be active. A restart is needed for changes to take effect.</p>
<form action="{{ url_for('save_schedule_route') }}" method="POST">
<div class="form-group">
<label for="schedule_enabled">Enable Schedule:</label>
<input type="checkbox" id="schedule_enabled" name="schedule_enabled" {% if schedule_config.enabled %}checked{% endif %}>
</div>
<div class="form-group">
<label for="start_time">Display On Time:</label>
<input type="time" id="start_time" name="start_time" value="{{ schedule_config.start_time }}">
</div>
<div class="form-group">
<label for="end_time">Display Off Time:</label>
<input type="time" id="end_time" name="end_time" value="{{ schedule_config.end_time }}">
</div>
<button type="submit">Save Schedule</button>
</form>
</div>
<!-- Main Config Tab -->
<div id="main" class="tab-content">
<form action="{{ url_for('save_config_route') }}" method="POST">
<input type="hidden" name="config_type" value="main">
<h2>Main Configuration (<span class="filepath">{{ main_config_path }}</span>)</h2>
<textarea name="config_data">{{ main_config_json }}</textarea>
<button type="submit">Save Main Config</button>
</form>
</div>
<!-- Secrets Tab -->
<div id="secrets" class="tab-content">
<form action="{{ url_for('save_config_route') }}" method="POST">
<input type="hidden" name="config_type" value="secrets">
<h2>Secrets Configuration (<span class="filepath">{{ secrets_config_path }}</span>)</h2>
<textarea name="config_data">{{ secrets_config_json }}</textarea>
<button type="submit">Save Secrets</button>
</form>
</div>
<!-- Actions Tab -->
<div id="actions" class="tab-content">
<h2>System Actions</h2>
<p>Control the display service and system.</p>
<div class="action-buttons">
<button type="button" class="action-button" onclick="runAction('start_display')">Start Display</button>
<button type="button" class="action-button" onclick="runAction('stop_display')">Stop Display</button>
<hr>
<button type="button" class="action-button" onclick="runAction('enable_autostart')">Enable Auto-Start</button>
<button type="button" class="action-button" onclick="runAction('disable_autostart')">Disable Auto-Start</button>
<hr>
<button type="button" class="action-button" onclick="runAction('reboot_system')">Reboot System</button>
<hr>
<button type="button" class="action-button" onclick="runAction('git_pull')">Download Latest Update</button>
</div>
<div id="action_output_container" style="margin-top: 20px;">
<h3>Action Output:</h3>
<pre id="action_output">No action run yet.</pre>
</div>
</div>
</div>
<script>
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tab-link");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
}
function runAction(actionName) {
const outputElement = document.getElementById('action_output');
outputElement.textContent = `Running ${actionName.replace(/_/g, ' ')}...`;
fetch("{{ url_for('run_action_route') }}", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: actionName })
})
.then(response => response.json())
.then(data => {
let outputText = `Status: ${data.status}\nMessage: ${data.message}\n`;
if (data.stdout) outputText += `\n--- STDOUT ---\n${data.stdout}`;
if (data.stderr) outputText += `\n--- STDERR ---\n${data.stderr}`;
outputElement.textContent = outputText;
})
.catch(error => {
outputElement.textContent = `Error: ${error}`;
});
}
// Set default active tab
document.addEventListener("DOMContentLoaded", function() {
document.querySelector('.tab-link').click();
});
</script>
</body>
</html>

View File

@@ -1,410 +1,108 @@
from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
import json
import os # Added os import
import subprocess # Added subprocess import
import os
import subprocess
from src.config_manager import ConfigManager
app = Flask(__name__)
app.secret_key = os.urandom(24) # Needed for flash messages
app.secret_key = os.urandom(24)
config_manager = ConfigManager()
CONFIG_PAGE_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LED Matrix Config</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { max-width: 800px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
h1 { text-align: center; color: #333; }
.tabs {
display: flex;
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
cursor: pointer;
border: none;
background-color: transparent;
font-size: 16px;
border-bottom: 3px solid transparent;
}
.tab-button.active {
border-bottom: 3px solid #4CAF50;
font-weight: bold;
}
.tab-content { display: none; }
.tab-content.active { display: block; }
label { display: block; margin-top: 10px; font-weight: bold; }
textarea {
width: 100%;
padding: 10px;
margin-top: 5px;
border-radius: 4px;
border: 1px solid #ccc;
box-sizing: border-box;
font-family: monospace;
min-height: 300px;
}
input[type="submit"] {
background-color: #4CAF50;
color: white;
padding: 12px 25px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 20px;
font-size: 16px;
}
input[type="submit"]:hover { background-color: #45a049; }
.flash-messages {
list-style: none;
padding: 0;
margin-bottom: 15px;
}
.flash-messages li {
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}
.flash-messages .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.flash-messages .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.filepath { font-family: monospace; background-color: #eee; padding: 2px 5px; border-radius: 3px; font-size: 0.9em;}
/* CodeMirror styling */
.CodeMirror { border: 1px solid #ccc; border-radius: 4px; min-height: 300px; font-family: monospace; font-size: 14px; }
</style>
<!-- CodeMirror CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-palenight.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.css">
</head>
<body>
<div class="container">
<h1>LED Matrix Configuration</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flash-messages">
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div class="tabs">
<button class="tab-button {% if active_tab == 'main' %}active{% endif %}" onclick="openTab('main')" data-tab="main">Main Config</button>
<button class="tab-button {% if active_tab == 'secrets' %}active{% endif %}" onclick="openTab('secrets')" data-tab="secrets">Secrets Config</button>
<button class="tab-button {% if active_tab == 'actions' %}active{% endif %}" onclick="openTab('actions')" data-tab="actions">Actions</button>
</div>
<form method="post" action="{{ url_for('save_config_route') }}">
<input type="hidden" name="config_type" id="config_type_hidden" value="{{ active_tab }}">
<div id="main" class="tab-content {% if active_tab == 'main' %}active{% endif %}">
<h2>Main Configuration (<span class="filepath">{{ main_config_path }}</span>)</h2>
<label for="main_config_data">Edit {{ main_config_path }}:</label>
<textarea name="main_config_data" rows="25">{{ main_config_json }}</textarea>
</div>
<div id="secrets" class="tab-content {% if active_tab == 'secrets' %}active{% endif %}">
<h2>Secrets Configuration (<span class="filepath">{{ secrets_config_path }}</span>)</h2>
<label for="secrets_config_data">Edit {{ secrets_config_path }}:</label>
<textarea name="secrets_config_data" rows="25">{{ secrets_config_json }}</textarea>
</div>
<div id="actions" class="tab-content {% if active_tab == 'actions' %}active{% endif %}">
<h2>Display & Service Actions</h2>
<div class="action-buttons">
<button type="button" class="action-button" onclick="runAction('start_display')">Start Display</button>
<button type="button" class="action-button" onclick="runAction('stop_display')">Stop Display</button>
<hr>
<button type="button" class="action-button" onclick="runAction('enable_autostart')">Enable Auto-Start</button>
<button type="button" class="action-button" onclick="runAction('disable_autostart')">Disable Auto-Start</button>
<hr>
<button type="button" class="action-button" onclick="runAction('reboot_system')">Reboot System</button>
<hr>
<button type="button" class="action-button" onclick="runAction('git_pull')">Download Latest Update</button>
</div>
<div id="action_output_container" style="margin-top: 20px;">
<h3>Action Output:</h3>
<pre id="action_output" style="background-color: #333; color: #fff; padding: 15px; border-radius: 4px; min-height: 100px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;">No action run yet.</pre>
</div>
</div>
<input type="submit" value="Save Current Tab's Configuration" id="save_config_button">
</form>
</div>
<script>
var mainEditor = null; // Declare editors in a scope accessible to openTab
var secretsEditor = null;
function openTab(tabName) {
history.pushState(null, null, '{{ url_for("display_config_route") }}?tab=' + tabName);
var i, tabcontent, tabbuttons;
tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.remove("active");
}
tabbuttons = document.getElementsByClassName("tab-button");
for (i = 0; i < tabbuttons.length; i++) {
tabbuttons[i].classList.remove("active");
}
document.getElementById(tabName).classList.add("active");
document.querySelector(".tab-button[data-tab='" + tabName + "']").classList.add("active");
document.getElementById("config_type_hidden").value = tabName;
// Refresh the corresponding CodeMirror instance
if (tabName === 'main' && mainEditor) {
mainEditor.refresh();
}
if (tabName === 'secrets' && secretsEditor) {
secretsEditor.refresh();
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize CodeMirror for the main config textarea
var mainConfigTextArea = document.querySelector("textarea[name='main_config_data']");
if (mainConfigTextArea) {
mainEditor = CodeMirror.fromTextArea(mainConfigTextArea, {
lineNumbers: true,
mode: {name: "javascript", json: true},
theme: "material-palenight",
gutters: ["CodeMirror-lint-markers"],
lint: true
});
new MutationObserver(() => mainEditor.refresh()).observe(document.getElementById('main'), {attributes: true, childList: false, subtree: false});
}
// Initialize CodeMirror for the secrets config textarea
var secretsConfigTextArea = document.querySelector("textarea[name='secrets_config_data']");
if (secretsConfigTextArea) {
secretsEditor = CodeMirror.fromTextArea(secretsConfigTextArea, {
lineNumbers: true,
mode: {name: "javascript", json: true},
theme: "material-palenight",
gutters: ["CodeMirror-lint-markers"],
lint: true
});
new MutationObserver(() => secretsEditor.refresh()).observe(document.getElementById('secrets'), {attributes: true, childList: false, subtree: false});
}
// Ensure CodeMirror instances save their content back to textareas before form submission
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function() {
if (mainEditor) {
mainEditor.save();
}
if (secretsEditor) {
secretsEditor.save();
}
});
}
// Initial tab setup from URL or default
const params = new URLSearchParams(window.location.search);
const initialTab = params.get('tab') || 'main';
openTab(initialTab);
});
function runAction(actionName) {
const outputElement = document.getElementById('action_output');
outputElement.textContent = `Running ${actionName.replace('_', ' ')}...`;
// Disable buttons during action
document.querySelectorAll('.action-button').forEach(button => button.disabled = true);
fetch("{{ url_for('run_action_route') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ action: actionName })
})
.then(response => response.json())
.then(data => {
let outputText = `Action: ${actionName}\nStatus: ${data.status}\nMessage: ${data.message}\n`;
if (data.stdout) {
outputText += `\nStdout:\n${data.stdout}`;
}
if (data.stderr) {
outputText += `\nStderr:\n${data.stderr}`;
}
if (data.error) {
outputText += `\nError: ${data.error}`;
}
outputElement.textContent = outputText;
flash(data.message, data.status);
})
.catch(error => {
outputElement.textContent = `Error running action ${actionName}: ${error}`;
flash(`Client-side error running action: ${error}`, 'error');
})
.finally(() => {
// Re-enable buttons
document.querySelectorAll('.action-button').forEach(button => button.disabled = false);
});
}
// Helper function to show flash messages dynamically (optional)
function flash(message, category) {
const flashContainer = document.querySelector('.flash-messages') || (() => {
const container = document.createElement('ul');
container.className = 'flash-messages';
document.querySelector('.container').prepend(container);
return container;
})();
const listItem = document.createElement('li');
listItem.className = category;
listItem.textContent = message;
flashContainer.appendChild(listItem);
setTimeout(() => listItem.remove(), 5000); // Remove after 5 seconds
}
</script>
<!-- CodeMirror JS (Corrected Order) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsonlint/1.6.3/jsonlint.min.js"></script> <!-- Defines global jsonlint -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/json-lint.min.js"></script> <!-- Uses global jsonlint -->
</body>
</html>
"""
@app.route('/')
def display_config_route():
active_tab = request.args.get('tab', 'main') # Default to 'main' tab
main_config_data = {}
secrets_config_data = {}
error_message = None
def index():
try:
main_config = config_manager.load_config()
schedule_config = main_config.get('schedule', {})
main_config_data = config_manager.get_raw_file_content('main')
except Exception as e:
flash(f"Error loading main config: {str(e)}", "error")
try:
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)
except Exception as e:
flash(f"Error loading secrets config: {str(e)}", "error")
flash(f"Error loading configuration: {e}", "error")
schedule_config = {}
main_config_json = "{}"
secrets_config_json = "{}"
main_config_json = json.dumps(main_config_data, indent=4)
secrets_config_json = json.dumps(secrets_config_data, indent=4)
return render_template_string(CONFIG_PAGE_TEMPLATE,
main_config_json=main_config_json,
secrets_config_json=secrets_config_json,
active_tab=active_tab,
main_config_path=config_manager.get_config_path(),
secrets_config_path=config_manager.get_secrets_path())
@app.route('/save', methods=['POST'])
def save_config_route():
config_type = request.form.get('config_type', 'main')
data_to_save_str = ""
if config_type == 'main':
data_to_save_str = request.form['main_config_data']
elif config_type == 'secrets':
data_to_save_str = request.form['secrets_config_data']
else:
flash("Invalid configuration type specified for saving.", "error")
return redirect(url_for('display_config_route'))
return render_template('index.html',
schedule_config=schedule_config,
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())
@app.route('/save_schedule', methods=['POST'])
def save_schedule_route():
try:
new_data = json.loads(data_to_save_str)
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)
flash("Schedule updated successfully! Restart the display for changes to take effect.", "success")
except Exception as e:
flash(f"Error saving schedule: {e}", "error")
return redirect(url_for('index'))
@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:
new_data = json.loads(config_data_str)
config_manager.save_raw_file_content(config_type, new_data)
flash(f"{config_type.capitalize()} configuration saved successfully!", "success")
except json.JSONDecodeError:
flash(f"Error: Invalid JSON format submitted for {config_type} config.", "error")
flash(f"Error: Invalid JSON format for {config_type} config.", "error")
except Exception as e:
flash(f"Error saving {config_type} configuration: {str(e)}", "error")
return redirect(url_for('display_config_route', tab=config_type)) # Redirect back to the same tab
flash(f"Error saving {config_type} configuration: {e}", "error")
return redirect(url_for('index'))
@app.route('/run_action', methods=['POST'])
def run_action_route():
data = request.get_json()
action = data.get('action')
command_parts = [] # Use a list for subprocess
explanation_msg = ""
commands = {
'start_display': ["sudo", "python3", "display_controller.py"],
'stop_display': ["sudo", "pkill", "-f", "display_controller.py"],
'enable_autostart': ["sudo", "systemctl", "enable", "ledmatrix.service"],
'disable_autostart': ["sudo", "systemctl", "disable", "ledmatrix.service"],
'reboot_system': ["sudo", "reboot"],
'git_pull': ["git", "pull"]
}
if action == 'start_display':
command_parts = ["sudo", "python", "display_controller.py"] # Changed command
explanation_msg = "Starts the LED matrix display by directly running display_controller.py with sudo."
elif action == 'stop_display':
command_parts = ["bash", "stop_display.sh"]
explanation_msg = "Stops the LED matrix display by executing the stop_display.sh script."
elif action == 'enable_autostart':
command_parts = ["sudo", "systemctl", "enable", "ledmatrix.service"]
explanation_msg = "Enables the LED matrix service to start automatically on boot."
elif action == 'disable_autostart':
command_parts = ["sudo", "systemctl", "disable", "ledmatrix.service"]
explanation_msg = "Disables the LED matrix service from starting automatically on boot."
elif action == 'reboot_system':
command_parts = ["sudo", "reboot"]
explanation_msg = "Reboots the system."
elif action == 'git_pull':
command_parts = ["git", "pull"]
explanation_msg = "Downloads the latest updates from the repository."
else:
return jsonify({
"status": "error",
"message": "Invalid action specified.",
"error": "Unknown action"
}), 400
command_parts = commands.get(action)
if not command_parts:
return jsonify({"status": "error", "message": "Invalid action."}), 400
try:
# Direct execution using subprocess
process_result = subprocess.run(command_parts, capture_output=True, text=True, check=False)
result = subprocess.run(command_parts, capture_output=True, text=True, check=False)
status = "success" if result.returncode == 0 else "error"
message = f"Action '{action}' completed."
stdout_content = process_result.stdout
stderr_content = process_result.stderr
exit_code = process_result.returncode
current_status = "success"
message_to_user = f"Action '{action}' executed. Exit code: {exit_code}."
if exit_code != 0:
current_status = "error"
message_to_user = f"Action '{action}' failed. Exit code: {exit_code}."
elif stderr_content: # Even with exit code 0, stderr might contain warnings
current_status = "warning"
message_to_user = f"Action '{action}' executed with output in stderr (Exit code: {exit_code})."
return jsonify({
"status": current_status,
"message": message_to_user,
"stdout": stdout_content,
"stderr": stderr_content
"status": status,
"message": message,
"stdout": result.stdout,
"stderr": result.stderr
})
except FileNotFoundError as e:
# This occurs if the command itself (e.g., 'bash', 'sudo', or the script) isn't found
return jsonify({
"status": "error",
"message": f"Error executing action '{action}': Command or script not found.",
"error": str(e),
"stdout": "",
"stderr": f"Command not found: {command_parts[0] if command_parts else ''}"
}), 500
except Exception as e:
# Catch any other exceptions during subprocess execution or in this route
return jsonify({
"status": "error",
"message": f"An unexpected error occurred while processing action: {action}",
"error": str(e)
}), 500
return jsonify({"status": "error", "message": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)