Files
moonlight-drive-in/enhanced_debug_console.py
jessikitty 05a8bd237b Upload files to "/"
Add core Python modules
2025-12-09 16:49:13 +11:00

1038 lines
42 KiB
Python

# enhanced_debug_console.py
"""
Enhanced Debug Console with Web Interface Integration
Maintains all original functionality while adding web interface support
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
import time
import logging
import socket
from datetime import datetime
from collections import deque
import sys
def make_json_safe(data):
"""Convert data to be JSON-safe by handling datetime objects"""
if isinstance(data, dict):
return {key: make_json_safe(value) for key, value in data.items()}
elif isinstance(data, list):
return [make_json_safe(item) for item in data]
elif isinstance(data, datetime):
return data.isoformat()
elif hasattr(data, 'total_seconds'): # timedelta
return str(data)
else:
return data
class EnhancedDebugConsoleWithWeb:
def __init__(self, enable_web=True, web_port=8547):
self.root = tk.Tk()
self.text_widget = None
self.running = False
self.logger = logging.getLogger(__name__)
self.video_player = None
self.message_queue = deque(maxlen=1000)
self.lock = threading.Lock()
self.nfc_buffer = ""
self.nfc_timeout = 0.3
self.last_nfc_input = 0
# Web interface integration
self.enable_web = enable_web
self.web_port = web_port
self.web_interface = None
self.web_thread = None
# Global input capture
self.global_listener = None
self.global_input_method = None
self.global_input_active = False
# Track mute state instead of volume
self.is_muted = False
self.stats = {
'videos_played': 0,
'key_presses': 0,
'errors': 0,
'start_time': datetime.now(),
'queue_depth': 0,
'current_video': 'None',
'fullscreen': True
}
self.setup_gui()
if self.enable_web:
self.setup_web_interface()
self.setup_global_input_capture()
self.log("Enhanced debug console with web interface initialized")
def get_local_ip(self):
"""Get the local IP address of this machine"""
try:
# Try to connect to a remote address to determine local IP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# Use Google's DNS server - doesn't actually connect
s.connect(('8.8.8.8', 80))
local_ip = s.getsockname()[0]
return local_ip
except Exception:
try:
# Fallback method - get hostname IP
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
if local_ip.startswith('127.'):
# If we get localhost, try alternative method
return self.get_network_ip_alternative()
return local_ip
except Exception:
return 'localhost'
def get_network_ip_alternative(self):
"""Alternative method to get network IP"""
try:
import subprocess
import platform
system = platform.system().lower()
if system == 'windows':
# Windows: use ipconfig
result = subprocess.run(['ipconfig'], capture_output=True, text=True)
lines = result.stdout.split('\n')
for i, line in enumerate(lines):
if 'Wireless LAN adapter' in line or 'Ethernet adapter' in line:
# Look for IPv4 address in next few lines
for j in range(i+1, min(i+10, len(lines))):
if 'IPv4 Address' in lines[j] and '192.168.' in lines[j]:
ip = lines[j].split(':')[-1].strip()
return ip
elif system == 'linux' or system == 'darwin':
# Linux/Mac: use hostname -I or ifconfig
try:
result = subprocess.run(['hostname', '-I'], capture_output=True, text=True)
if result.returncode == 0:
ips = result.stdout.strip().split()
for ip in ips:
if ip.startswith('192.168.') or ip.startswith('10.') or ip.startswith('172.'):
return ip
except:
pass
return 'localhost'
except Exception:
return 'localhost'
def setup_web_interface(self):
"""Setup web interface in separate thread"""
try:
from web_interface import WebInterface
# Get local IP address
local_ip = self.get_local_ip()
# Create web interface instance
self.web_interface = WebInterface(debug_console=self)
self.log(f"Web interface created - will start on port {self.web_port}")
# Log URLs
self.log(f"Local URL: http://localhost:{self.web_port}")
self.log(f"Network URL: http://{local_ip}:{self.web_port}")
# Start web interface in separate thread
def start_web_interface():
try:
self.log(f"Starting web interface on port {self.web_port}")
self.web_interface.run(host='0.0.0.0', port=self.web_port, debug=False)
except Exception as e:
self.log_error(f"Web interface failed to start: {e}")
self.web_thread = threading.Thread(
target=start_web_interface,
daemon=True,
name="WebInterfaceThread"
)
self.web_thread.start()
self.log("Web interface thread started")
# Update GUI with URL information
if hasattr(self, 'web_status_label'):
self.web_status_label.config(
text=f"🌐 Web: http://{local_ip}:{self.web_port}",
foreground="green"
)
except ImportError:
self.log_error("Web interface dependencies not available - install flask and flask-socketio")
self.enable_web = False
except Exception as e:
self.log_error(f"Web interface setup failed: {e}")
self.enable_web = False
def setup_global_input_capture(self):
"""Setup global keyboard capture using the best available method"""
# Method 1: Try pynput (cross-platform, most reliable)
if self.try_pynput_global_capture():
return
# Method 2: Try Windows-specific hook
if sys.platform == "win32" and self.try_windows_global_capture():
return
# Method 3: Try Linux-specific listener
if sys.platform.startswith('linux') and self.try_linux_global_capture():
return
# Fallback: No global capture
self.log("Warning: Global input capture not available")
self.log("NFC input will only work when debug window is in focus")
def try_pynput_global_capture(self):
"""Try setting up pynput global keyboard listener"""
try:
from pynput import keyboard
def on_key_press(key):
try:
current_time = time.time()
# Handle Enter key
if key == keyboard.Key.enter:
if self.nfc_buffer:
self.log(f"Global NFC input: {self.nfc_buffer}")
self.process_global_nfc_input(self.nfc_buffer.strip())
self.nfc_buffer = ""
return
# Handle digit keys
if hasattr(key, 'char') and key.char and key.char.isdigit():
# Reset buffer if timeout exceeded
if current_time - self.last_nfc_input > self.nfc_timeout:
self.nfc_buffer = ""
self.nfc_buffer += key.char
self.last_nfc_input = current_time
except Exception as e:
self.logger.debug(f"Global key press error: {e}")
# Create and start the global listener
self.global_listener = keyboard.Listener(on_press=on_key_press)
self.global_listener.start()
self.global_input_method = "pynput"
self.global_input_active = True
self.log("Global input capture active (pynput)")
# Update web interface if available
if self.web_interface:
self.web_interface.update_global_input_status(True, "pynput")
return True
except ImportError:
self.logger.debug("pynput not available")
return False
except Exception as e:
self.logger.debug(f"pynput setup failed: {e}")
return False
def try_windows_global_capture(self):
"""Try Windows-specific global keyboard hook"""
try:
import win32api
import win32con
import win32gui
def low_level_keyboard_proc(nCode, wParam, lParam):
try:
if nCode >= 0 and wParam == win32con.WM_KEYDOWN:
vk_code = lParam[0]
current_time = time.time()
# Handle Enter key (VK_RETURN = 13)
if vk_code == 13:
if self.nfc_buffer:
self.log(f"Global NFC input: {self.nfc_buffer}")
self.process_global_nfc_input(self.nfc_buffer.strip())
self.nfc_buffer = ""
# Handle digit keys (VK_0 to VK_9 = 48-57)
elif 48 <= vk_code <= 57:
# Reset buffer if timeout exceeded
if current_time - self.last_nfc_input > self.nfc_timeout:
self.nfc_buffer = ""
digit = chr(vk_code)
self.nfc_buffer += digit
self.last_nfc_input = current_time
except Exception as e:
self.logger.debug(f"Windows hook error: {e}")
return win32gui.CallNextHookEx(None, nCode, wParam, lParam)
# Install the hook
hook = win32gui.SetWindowsHookEx(
win32con.WH_KEYBOARD_LL,
low_level_keyboard_proc,
win32api.GetModuleHandle(None),
0
)
if hook:
self.global_listener = hook
self.global_input_method = "windows"
self.global_input_active = True
self.log("Global input capture active (Windows)")
# Update web interface if available
if self.web_interface:
self.web_interface.update_global_input_status(True, "windows")
return True
except ImportError:
self.logger.debug("pywin32 not available")
return False
except Exception as e:
self.logger.debug(f"Windows hook setup failed: {e}")
return False
return False
def try_linux_global_capture(self):
"""Try Linux-specific global keyboard listener"""
try:
from Xlib import display, X
from Xlib.ext import record
from Xlib.protocol import rq
def record_callback(reply):
try:
if reply.category != record.FromServer:
return
if reply.client_swapped:
return
if not len(reply.data) or reply.data[0] < 2:
return
data = reply.data
while len(data):
event, data = rq.EventField(None).parse_binary_value(
data, self.record_dpy.display, None, None)
if event.type == X.KeyPress:
keycode = event.detail
current_time = time.time()
# Convert keycode to character
keysym = self.local_dpy.keycode_to_keysym(keycode, 0)
# Handle Enter key
if keysym == 65293: # Return key
if self.nfc_buffer:
self.log(f"Global NFC input: {self.nfc_buffer}")
self.process_global_nfc_input(self.nfc_buffer.strip())
self.nfc_buffer = ""
# Handle digit keys
elif 48 <= keysym <= 57: # 0-9 keys
# Reset buffer if timeout exceeded
if current_time - self.last_nfc_input > self.nfc_timeout:
self.nfc_buffer = ""
digit = chr(keysym)
self.nfc_buffer += digit
self.last_nfc_input = current_time
except Exception as e:
self.logger.debug(f"Linux record callback error: {e}")
# Setup X11 recording
self.local_dpy = display.Display()
self.record_dpy = display.Display()
# Create a recording context
ctx = self.record_dpy.record_create_context(
0,
[record.AllClients],
[{
'core_requests': (0, 0),
'core_replies': (0, 0),
'ext_requests': (0, 0, 0, 0),
'ext_replies': (0, 0, 0, 0),
'delivered_events': (0, 0),
'device_events': (X.KeyPress, X.KeyRelease),
'errors': (0, 0),
'client_started': False,
'client_died': False,
}]
)
# Start recording in a separate thread
def start_recording():
self.record_dpy.record_enable_context(ctx, record_callback)
self.record_dpy.record_free_context(ctx)
thread = threading.Thread(target=start_recording, daemon=True)
thread.start()
self.global_listener = ctx
self.global_input_method = "linux"
self.global_input_active = True
self.log("Global input capture active (Linux)")
# Update web interface if available
if self.web_interface:
self.web_interface.update_global_input_status(True, "linux")
return True
except ImportError:
self.logger.debug("python-xlib not available")
return False
except Exception as e:
self.logger.debug(f"Linux listener setup failed: {e}")
return False
def process_global_nfc_input(self, nfc_id):
"""Process NFC input captured globally"""
if not nfc_id:
return
# Update the GUI entry field to show what was captured
try:
if hasattr(self, 'nfc_entry'):
self.nfc_entry.delete(0, tk.END)
self.nfc_entry.insert(0, nfc_id)
except:
pass
# Process the NFC input
self.process_nfc_input(nfc_id)
def setup_gui(self):
"""Setup enhanced GUI with web interface status"""
self.root.title("Video Player Controller (Enhanced + Web)")
self.root.geometry("900x700")
# Handle window close properly
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
try:
self.root.state('zoomed')
except:
self.root.geometry("{0}x{1}+0+0".format(
self.root.winfo_screenwidth()//2,
self.root.winfo_screenheight()
))
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# Enhanced controls with web interface status
control_frame = ttk.LabelFrame(main_frame, text="Controls", padding="10")
control_frame.pack(fill=tk.X)
# Web interface status
web_frame = ttk.Frame(control_frame)
web_frame.pack(fill=tk.X, pady=(0, 10))
if self.enable_web:
local_ip = self.get_local_ip()
# Create a more prominent web status display
web_info_frame = ttk.LabelFrame(web_frame, text="Web Interface Access", padding="10")
web_info_frame.pack(fill=tk.X)
# Local URL
local_url_frame = ttk.Frame(web_info_frame)
local_url_frame.pack(fill=tk.X, pady=2)
ttk.Label(local_url_frame, text="Local:").pack(side=tk.LEFT)
self.local_url_label = ttk.Label(
local_url_frame,
text=f"http://localhost:{self.web_port}",
foreground="blue",
font=('Consolas', 10, 'bold')
)
self.local_url_label.pack(side=tk.LEFT, padx=10)
ttk.Button(
local_url_frame,
text="📋 Copy",
command=self.copy_local_url,
width=8
).pack(side=tk.RIGHT)
# Network URL
network_url_frame = ttk.Frame(web_info_frame)
network_url_frame.pack(fill=tk.X, pady=2)
ttk.Label(network_url_frame, text="Network:").pack(side=tk.LEFT)
self.network_url_label = ttk.Label(
network_url_frame,
text=f"http://{local_ip}:{self.web_port}",
foreground="blue",
font=('Consolas', 10, 'bold')
)
self.network_url_label.pack(side=tk.LEFT, padx=10)
ttk.Button(
network_url_frame,
text="📋 Copy",
command=self.copy_network_url,
width=8
).pack(side=tk.RIGHT)
# Status and button
button_frame = ttk.Frame(web_info_frame)
button_frame.pack(fill=tk.X, pady=(5, 0))
self.web_status_label = ttk.Label(
button_frame,
text="🌐 Starting...",
foreground="orange"
)
self.web_status_label.pack(side=tk.LEFT)
ttk.Button(
button_frame,
text="Open in Browser",
command=self.open_web_interface
).pack(side=tk.RIGHT, padx=5)
else:
ttk.Label(
web_frame,
text="🌐 Web Interface: Disabled",
foreground="red"
).pack(side=tk.LEFT)
# Primary controls
controls_row1 = ttk.Frame(control_frame)
controls_row1.pack(fill=tk.X, pady=(0, 5))
ttk.Button(controls_row1, text="Skip Video", command=self.skip_video).pack(side=tk.LEFT, padx=5)
ttk.Button(controls_row1, text="Toggle Fullscreen", command=self.toggle_fullscreen).pack(side=tk.LEFT, padx=5)
# Mute/Unmute button
self.mute_button = ttk.Button(controls_row1, text="Mute", command=self.toggle_mute)
self.mute_button.pack(side=tk.LEFT, padx=5)
# Global input status indicator
self.global_input_label = ttk.Label(controls_row1, text="Global Input: Checking...", foreground="orange")
self.global_input_label.pack(side=tk.LEFT, padx=20)
# Exit button with confirmation
exit_button = ttk.Button(controls_row1, text="Exit Application", command=self.exit_application)
exit_button.pack(side=tk.RIGHT, padx=5)
# Secondary controls
controls_row2 = ttk.Frame(control_frame)
controls_row2.pack(fill=tk.X, pady=(5, 0))
ttk.Button(controls_row2, text="Clear Log", command=self.clear_log).pack(side=tk.LEFT, padx=5)
# Manual NFC input for testing
ttk.Label(controls_row2, text="NFC Test:").pack(side=tk.LEFT, padx=(20, 5))
self.nfc_entry = ttk.Entry(controls_row2, width=15)
self.nfc_entry.pack(side=tk.LEFT, padx=5)
self.nfc_entry.bind('<Return>', self.manual_nfc_input)
ttk.Button(controls_row2, text="Send", command=self.manual_nfc_input).pack(side=tk.LEFT, padx=2)
# Status indicator
self.status_label = ttk.Label(controls_row2, text="Status: Starting...", foreground="orange")
self.status_label.pack(side=tk.RIGHT, padx=5)
# Enhanced logging display
log_frame = ttk.LabelFrame(main_frame, text="System Log", padding="5")
log_frame.pack(fill=tk.BOTH, expand=True)
self.text_widget = scrolledtext.ScrolledText(
log_frame,
wrap=tk.WORD,
font=('Consolas', 9),
height=20
)
self.text_widget.pack(fill=tk.BOTH, expand=True)
# Enhanced statistics
stats_frame = ttk.LabelFrame(main_frame, text="Statistics", padding="5")
stats_frame.pack(fill=tk.X)
self.stats_labels = {}
stats_grid = ttk.Frame(stats_frame)
stats_grid.pack(fill=tk.X)
stats_keys = [
('videos_played', 'Videos Played'),
('key_presses', 'NFC Scans'),
('errors', 'Errors'),
('uptime', 'Uptime'),
('queue_depth', 'Queue Depth'),
('current_video', 'Current Video')
]
for i, (key, label) in enumerate(stats_keys):
row = i // 2
col = (i % 2) * 2
ttk.Label(stats_grid, text=f"{label}:").grid(row=row, column=col, sticky=tk.W, padx=5)
self.stats_labels[key] = ttk.Label(stats_grid, text="0", font=('Consolas', 9))
self.stats_labels[key].grid(row=row, column=col+1, sticky=tk.W, padx=5)
# Still bind local keys as fallback
self.root.bind('<Key>', self.on_key_press)
self.root.focus_set()
def open_web_interface(self):
"""Open web interface in default browser"""
try:
import webbrowser
url = f"http://localhost:{self.web_port}"
webbrowser.open(url)
self.log(f"Opened web interface in browser: {url}")
except Exception as e:
self.log_error(f"Failed to open web interface: {e}")
def copy_local_url(self):
"""Copy local URL to clipboard"""
try:
url = f"http://localhost:{self.web_port}"
self.root.clipboard_clear()
self.root.clipboard_append(url)
self.root.update()
self.log(f"Local URL copied to clipboard: {url}")
if hasattr(self, 'web_status_label'):
original_text = self.web_status_label.cget("text")
self.web_status_label.config(text="📋 Local URL copied!", foreground="green")
self.root.after(2000, lambda: self.web_status_label.config(text=original_text, foreground="green"))
except Exception as e:
self.log_error(f"Failed to copy URL: {e}")
def copy_network_url(self):
"""Copy network URL to clipboard"""
try:
local_ip = self.get_local_ip()
url = f"http://{local_ip}:{self.web_port}"
self.root.clipboard_clear()
self.root.clipboard_append(url)
self.root.update()
self.log(f"Network URL copied to clipboard: {url}")
if hasattr(self, 'web_status_label'):
original_text = self.web_status_label.cget("text")
self.web_status_label.config(text="📋 Network URL copied!", foreground="green")
self.root.after(2000, lambda: self.web_status_label.config(text=original_text, foreground="green"))
except Exception as e:
self.log_error(f"Failed to copy URL: {e}")
def update_web_status(self, status, color="black"):
"""Update web interface status"""
if hasattr(self, 'web_status_label'):
self.web_status_label.config(text=f"🌐 {status}", foreground=color)
def update_global_input_status(self):
"""Update the global input status indicator"""
if self.global_input_active:
self.global_input_label.config(
text=f"Global Input: Active ({self.global_input_method})",
foreground="green"
)
else:
self.global_input_label.config(
text="Global Input: Inactive (Focus Required)",
foreground="red"
)
def on_closing(self):
"""Handle window close event with confirmation"""
if messagebox.askokcancel("Exit", "Do you want to exit the video player?"):
self.exit_application()
def exit_application(self):
"""Exit the entire application cleanly"""
try:
self.log("Exit requested - shutting down application")
self.status_label.config(text="Status: Shutting down...", foreground="red")
# Stop global input capture
if self.global_listener:
try:
if self.global_input_method == "pynput":
self.global_listener.stop()
elif self.global_input_method == "windows":
import win32gui
win32gui.UnhookWindowsHookEx(self.global_listener)
# Linux cleanup is automatic with daemon thread
self.log("Global input capture stopped")
except Exception as e:
self.log_error(f"Error stopping global input: {e}")
# Stop web interface
if self.web_interface:
try:
self.web_interface.stop()
self.log("Web interface stopped")
except Exception as e:
self.log_error(f"Error stopping web interface: {e}")
# Stop video player first
if self.video_player:
self.video_player.stop()
self.log("Video player stopped")
# Stop debug console
self.running = False
# Close the application
self.root.quit()
self.root.destroy()
# Force exit if needed
import sys
sys.exit(0)
except Exception as e:
self.log_error(f"Error during exit: {e}")
import sys
sys.exit(1)
def clear_log(self):
"""Clear the log display"""
if self.text_widget:
self.text_widget.delete(1.0, tk.END)
if self.web_interface:
with self.web_interface.lock:
self.web_interface.message_queue.clear()
self.log("Log cleared")
def manual_nfc_input(self, event=None):
"""Handle manual NFC input from entry field"""
nfc_id = self.nfc_entry.get().strip()
if nfc_id:
self.log(f"Manual NFC input: {nfc_id}")
self.process_nfc_input(nfc_id)
self.nfc_entry.delete(0, tk.END)
def on_key_press(self, event):
"""Enhanced NFC input handling (fallback when window has focus)"""
# Only process if global input is not active
if self.global_input_active:
return
current_time = time.time()
try:
# Process Enter key
if event.keysym == 'Return':
if self.nfc_buffer:
self.log(f"Local NFC input: {self.nfc_buffer}")
self.process_nfc_input(self.nfc_buffer)
self.nfc_buffer = ""
return
# Process number keys
if event.char and event.char.isdigit():
if current_time - self.last_nfc_input > self.nfc_timeout:
self.nfc_buffer = ""
self.nfc_buffer += event.char
self.last_nfc_input = current_time
except Exception as e:
self.log_error(f"Local key press handling error: {e}")
def log(self, message, level="INFO"):
"""Enhanced logging with timestamps and levels"""
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
thread_name = threading.current_thread().name[:10]
formatted = f"[{timestamp}][{thread_name}] {level}: {message}"
with self.lock:
self.message_queue.append(formatted)
# Also log to web interface if available
if self.web_interface:
self.web_interface.log(message, level)
if level == "ERROR":
self.stats['errors'] += 1
def log_error(self, message):
"""Log an error message"""
self.log(message, "ERROR")
def log_video_played(self, video_name):
"""Log video playback"""
self.log(f"Video played: {video_name}")
self.stats['videos_played'] += 1
self.stats['current_video'] = video_name
# Also notify web interface
if self.web_interface:
self.web_interface.log_video_played(video_name)
# Update status
if self.status_label:
self.status_label.config(text="Status: Playing video", foreground="green")
def log_key_press(self, key):
"""Log key press"""
self.log(f"Key pressed: {key}")
self.stats['key_presses'] += 1
def process_nfc_input(self, nfc_id=None):
"""Process completed NFC input with enhanced error handling"""
if nfc_id is None:
nfc_id = self.nfc_buffer.strip()
if not nfc_id:
return
self.log(f"Processing NFC ID: {nfc_id}")
self.log_key_press(nfc_id)
# Update status
if self.status_label:
self.status_label.config(text=f"Status: Processing NFC {nfc_id}", foreground="orange")
if self.video_player:
try:
# Force immediate processing
self.video_player.play_specific_video(nfc_id)
self.log(f"Sent NFC {nfc_id} to video player")
# Update status after short delay
self.root.after(2000, lambda: self.status_label.config(text="Status: Ready", foreground="green"))
except Exception as e:
self.log_error(f"Failed to send NFC to video player: {e}")
self.status_label.config(text="Status: Error", foreground="red")
else:
self.log_error("Video player not connected")
self.status_label.config(text="Status: No video player", foreground="red")
def set_video_player(self, video_player):
"""Set reference to video player"""
self.video_player = video_player
self.log("Video player connected to debug console")
# Also connect to web interface
if self.web_interface:
self.web_interface.set_video_player(video_player)
if self.status_label:
self.status_label.config(text="Status: Connected", foreground="green")
if self.enable_web:
self.update_web_status("Ready", "green")
def skip_video(self):
"""Skip current video with better error handling"""
if self.video_player:
try:
self.log("Skipping video...")
self.status_label.config(text="Status: Skipping...", foreground="orange")
self.video_player.skip_current_video()
# Force next video immediately
self.video_player.force_next_video()
self.log("Video skip requested")
# Update status after short delay
self.root.after(1000, lambda: self.status_label.config(text="Status: Ready", foreground="green"))
except Exception as e:
self.log_error(f"Skip video failed: {e}")
self.status_label.config(text="Status: Error", foreground="red")
else:
self.log_error("Video player not connected")
self.status_label.config(text="Status: No video player", foreground="red")
def toggle_mute(self):
"""Toggle mute/unmute with status updates"""
if self.video_player:
try:
if self.is_muted:
# Unmute - set to reasonable volume
self.video_player.set_volume(0.7)
self.mute_button.config(text="Mute")
self.is_muted = False
self.log("Audio unmuted")
self.status_label.config(text="Status: Audio unmuted", foreground="green")
else:
# Mute
self.video_player.set_volume(0.0)
self.mute_button.config(text="Unmute")
self.is_muted = True
self.log("Audio muted")
self.status_label.config(text="Status: Audio muted", foreground="orange")
# Reset status after delay
self.root.after(2000, lambda: self.status_label.config(text="Status: Ready", foreground="green"))
except Exception as e:
self.log_error(f"Mute toggle failed: {e}")
self.status_label.config(text="Status: Error", foreground="red")
else:
self.log_error("Video player not connected")
self.status_label.config(text="Status: No video player", foreground="red")
def toggle_fullscreen(self):
"""Toggle fullscreen mode with status updates"""
if self.video_player:
try:
self.status_label.config(text="Status: Toggling fullscreen...", foreground="orange")
self.video_player.toggle_fullscreen()
self.log("Fullscreen toggled")
# Update status after delay
self.root.after(1000, lambda: self.status_label.config(text="Status: Ready", foreground="green"))
except Exception as e:
self.log_error(f"Fullscreen toggle failed: {e}")
self.status_label.config(text="Status: Error", foreground="red")
else:
self.log_error("Video player not connected")
self.status_label.config(text="Status: No video player", foreground="red")
def update_queue_depth(self, depth):
"""Update video queue depth"""
self.stats['queue_depth'] = depth
if self.web_interface:
self.web_interface.update_queue_depth(depth)
def update_display(self):
"""Update the console display with enhanced formatting"""
messages = []
with self.lock:
while self.message_queue:
messages.append(self.message_queue.popleft())
for msg in messages:
self.text_widget.insert(tk.END, msg + "\n")
# Enhanced color coding for different log levels
if "ERROR" in msg:
self.text_widget.tag_add("error", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("error", foreground="red", background="#ffe6e6")
elif "WARNING" in msg:
self.text_widget.tag_add("warning", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("warning", foreground="orange", background="#fff3cd")
elif "Global NFC input" in msg or "NFC" in msg:
self.text_widget.tag_add("nfc", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("nfc", foreground="blue", background="#e6f3ff")
elif "Video played:" in msg or "SPECIFIC VIDEO" in msg:
self.text_widget.tag_add("video", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("video", foreground="green", background="#e6ffe6")
elif "Web interface" in msg:
self.text_widget.tag_add("web", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("web", foreground="purple", background="#f0e6ff")
elif "Global input capture" in msg:
self.text_widget.tag_add("global", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("global", foreground="purple", background="#f0e6ff")
elif "transition" in msg.lower():
self.text_widget.tag_add("transition", f"end-2l linestart", f"end-1l lineend")
self.text_widget.tag_config("transition", foreground="purple")
self.text_widget.see(tk.END)
# Limit text widget size to prevent memory issues
lines = self.text_widget.get("1.0", tk.END).count('\n')
if lines > 1000:
self.text_widget.delete("1.0", "200.0")
# Update statistics
uptime = str(datetime.now() - self.stats['start_time']).split('.')[0]
self.stats_labels['uptime'].config(text=uptime)
for key in self.stats_labels:
if key in self.stats and key != 'uptime':
value = str(self.stats[key])
# Truncate long video names
if key == 'current_video' and len(value) > 30:
value = value[:27] + "..."
self.stats_labels[key].config(text=value)
# Update global input status
self.update_global_input_status()
def run(self):
"""Run the debug console with enhanced error handling"""
self.running = True
self.log("Enhanced debug console with web interface started")
if self.enable_web:
self.log(f"Web interface accessible at: http://localhost:{self.web_port}")
self.update_web_status("Starting...", "orange")
# Update web status after initialization
def update_web_status_delayed():
time.sleep(3)
if self.web_interface and self.running:
self.root.after(0, lambda: self.update_web_status("Ready", "green"))
threading.Thread(target=update_web_status_delayed, daemon=True).start()
if self.status_label:
self.status_label.config(text="Status: Starting...", foreground="orange")
def update_loop():
if self.running:
try:
self.update_display()
# Update status to ready if no recent activity
if (self.status_label and
"Starting" in self.status_label.cget("text") and
self.video_player):
self.status_label.config(text="Status: Ready", foreground="green")
self.root.after(100, update_loop)
except Exception as e:
self.log_error(f"Display update error: {e}")
if self.running:
self.root.after(1000, update_loop) # Retry after delay
update_loop()
try:
self.root.mainloop()
except Exception as e:
self.logger.error(f"Debug console error: {e}")
finally:
self.running = False
def stop(self):
"""Stop the debug console"""
self.running = False
# Stop global input capture
if self.global_listener:
try:
if self.global_input_method == "pynput":
self.global_listener.stop()
elif self.global_input_method == "windows":
import win32gui
win32gui.UnhookWindowsHookEx(self.global_listener)
# Linux cleanup is automatic with daemon thread
except Exception as e:
self.logger.debug(f"Error stopping global input: {e}")
# Stop web interface
if self.web_interface:
try:
self.web_interface.stop()
except Exception as e:
self.logger.debug(f"Error stopping web interface: {e}")
if self.root:
try:
self.root.quit()
self.root.destroy()
except:
pass