SXXXXXXX_PyMsc/pymsc/gui/main_docking_window.py

491 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""
ARTOS Main Docking Window
Modular GUI with dockable panels for different system modules.
"""
import tkinter as tk
from tkinter import ttk, messagebox
import logging
import sys
import os
import threading
# Add external modules to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '_external', 'externals', 'python-tkinter-logger'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '_external', 'externals', 'python-resource-monitor'))
from tkinter_logger import TkinterLogger
from resource_monitor import ResourceMonitor
from tkinter.scrolledtext import ScrolledText
from pymsc.core.bus_1553_module import Bus1553Module
from pymsc.gui.docking.workspace_manager import WorkspaceManager
from pymsc.gui.docking.mcs_dock import MCSDock
from pymsc.gui.docking.placeholder_dock import PlaceholderDock
from pymsc.gui.docking.logger_dock import LoggerDock
from pymsc.gui.components.command_widgets import (
CommandFrameCheckBox, CommandFrameComboBox,
CommandFrameSpinBox, CommandFrameLabels,
CommandFrameControls
)
class MainDockingWindow:
"""
ARTOS main window with modular docking system.
Architecture:
- Left panel: MCS (Mission Control System)
- Right top: Navigation, IRST, Algorithms (placeholders)
- Right bottom: Logger, Status (placeholders)
"""
def __init__(self, bus_module: Bus1553Module, logger_system=None):
"""
Args:
bus_module: Bus1553Module instance for message access
logger_system: Pre-configured TkinterLogger instance (optional)
"""
self.bus_module = bus_module
self._refresh_active = False # Flag to control GUI refresh loop
self._closing = False # Flag to prevent multiple close attempts
# Create main window
self.root = tk.Tk()
self.root.title("ARTOS - Advanced Radar Test & Orchestration System")
self.root.geometry("1600x900")
# Configure light theme
style = ttk.Style()
style.theme_use('clam')
# Light theme configuration
bg_light = '#f0f0f0'
bg_lighter = '#ffffff'
fg_dark = '#000000'
accent = '#0078d7'
self.root.configure(bg=bg_light)
style.configure('TFrame', background=bg_light)
style.configure('TLabel', background=bg_light, foreground=fg_dark)
style.configure('TButton', background=accent, foreground='white')
style.map('TButton', background=[('active', '#005a9e')])
style.configure('TLabelframe', background=bg_light, foreground=fg_dark)
style.configure('TLabelframe.Label', background=bg_light, foreground=fg_dark)
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
# Use existing TkinterLogger or create new one
if logger_system:
self.logger_system = logger_system
# Update root for after() scheduling
self.logger_system.tk_root = self.root
else:
# Fallback: create new logger system
self.logger_system = TkinterLogger(self.root)
self.logger_system.setup(
enable_console=True,
enable_file=True,
file_path='logs/artos.log',
enable_tkinter=False, # Will add widget after LoggerDock is created
root_level=logging.INFO
)
# Create menu bar
self._create_menu()
# Create status bar at bottom (before workspace)
self._create_status_bar()
# Create workspace manager FIRST (so containers exist)
self.workspace = WorkspaceManager(self.root, layouts_dir="layouts")
# Create LoggerDock FIRST so it creates its widget in the correct container
self.logger_dock = LoggerDock(self.workspace.bottom_container)
self.workspace.add_dock('logger', self.logger_dock, position='bottom')
# Get the widget from LoggerDock and connect it to TkinterLogger
log_widget = self.logger_dock.get_log_widget()
self.logger_system.add_tkinter_handler(
log_widget,
level_colors={
logging.DEBUG: '#666666',
logging.INFO: '#0066cc',
logging.WARNING: '#ff8800',
logging.ERROR: '#cc0000',
logging.CRITICAL: '#8B0000'
},
max_lines=1000
)
logger = logging.getLogger('ARTOS')
logger.info("GUI logger initialized - capturing all subsequent logs")
# Create and register other docks
self._create_docks()
# Load saved layout automatically after widgets are fully rendered
self.root.after(200, lambda: self.workspace.load_layout("user_layout"))
# Start refresh loops
self._start_refresh()
def _create_status_bar(self):
"""Create status bar at bottom with resource monitor on the right."""
self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN, borderwidth=1)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# Left side: general status message
self.status_label = ttk.Label(
self.status_bar,
text="ARTOS Ready",
anchor=tk.W,
padding=(5, 2)
)
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Right side: resource monitor
self.resource_label = ttk.Label(
self.status_bar,
text="",
anchor=tk.E,
padding=(5, 2)
)
self.resource_label.pack(side=tk.RIGHT)
# Initialize ResourceMonitor
def update_resource_stats(stats_str):
try:
self.resource_label.config(text=stats_str)
except Exception:
pass
self.resource_monitor = ResourceMonitor(
update_callback=update_resource_stats,
poll_interval=1.0
)
self.resource_monitor.start()
def _create_menu(self):
"""Create the main menu bar."""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# File menu
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Save Layout", command=self._save_layout)
file_menu.add_command(label="Load Layout", command=self._load_layout)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self._on_close)
# View menu - toggle dock visibility
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="View", menu=view_menu)
view_menu.add_command(label="Toggle MCS",
command=lambda: self.workspace.toggle_dock_visibility('mcs'))
view_menu.add_command(label="Toggle Navigation",
command=lambda: self.workspace.toggle_dock_visibility('nav'))
view_menu.add_command(label="Toggle IRST",
command=lambda: self.workspace.toggle_dock_visibility('irst'))
view_menu.add_separator()
view_menu.add_command(label="Restore All", command=self._restore_all_docks)
# System menu
system_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="System", menu=system_menu)
system_menu.add_command(label="Start System", command=self._start_system)
system_menu.add_command(label="Stop System", command=self._stop_system)
system_menu.add_separator()
system_menu.add_command(label="System Info", command=self._show_system_info)
system_menu.add_separator()
system_menu.add_command(label="1553 Debug", command=self._open_1553_debug)
# Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="About", command=self._show_about)
help_menu.add_command(label="Documentation", command=self._show_docs)
def _create_docks(self):
"""Create and register remaining dock panels (logger was created first)."""
# Left panel: MCS
mcs_dock = MCSDock(
self.workspace.left_container,
bus_module=self.bus_module
)
self.workspace.add_dock('mcs', mcs_dock, position='left')
# Right top: Navigation placeholder
nav_dock = PlaceholderDock(
self.workspace.right_top_container,
title="Navigation System",
description="Navigation algorithms and data processing\n\n(Coming in Phase 3)"
)
self.workspace.add_dock('nav', nav_dock, position='right_top')
# Right bottom: IRST placeholder
irst_dock = PlaceholderDock(
self.workspace.right_bottom_container,
title="IRST Module",
description="Infrared Search & Track\n\n(Coming in Phase 4)"
)
self.workspace.add_dock('irst', irst_dock, position='right_bottom')
# Logger was already created first in __init__
def _start_refresh(self):
"""Start the GUI refresh loop to update all widgets."""
self._refresh_active = True
self.gui_refresh_loop()
def _stop_refresh(self):
"""Stop refresh loops (cleanup on exit)."""
self._refresh_active = False
def gui_refresh_loop(self):
"""
Periodically refreshes all custom widgets in all visible docks.
This updates telemetry displays from received 1553 messages.
"""
# Check if we should continue refreshing
if not self._refresh_active:
return
# First, sync all messages from the 1553 bus (read latest telemetry)
try:
if self.bus_module and self.bus_module.is_running:
self.bus_module.sync_all_messages()
except Exception:
pass
# Then refresh widgets in all visible docks
try:
for dock_id, dock in self.workspace.docks.items():
if dock.is_visible and not dock.is_minimized:
self._refresh_widgets_recursive(dock.content_frame)
except Exception:
pass
# Schedule next refresh only if still active (100ms = 10 Hz update rate)
if self._refresh_active:
self.root.after(100, self.gui_refresh_loop)
def _refresh_widgets_recursive(self, container: tk.Widget):
"""
Traverses the widget tree to find and update PyMsc custom components.
Calls check_updated_value() on each command widget found.
"""
for child in container.winfo_children():
# Check if it's one of our custom command widgets
is_custom = isinstance(child, (
CommandFrameCheckBox, CommandFrameComboBox,
CommandFrameSpinBox, CommandFrameLabels,
CommandFrameControls
))
if is_custom and hasattr(child, 'check_updated_value'):
child.check_updated_value()
# Continue searching if the child has its own children (like sub-frames)
if child.winfo_children():
self._refresh_widgets_recursive(child)
def _save_layout(self):
"""Save current workspace layout."""
self.workspace.save_layout("user_layout")
messagebox.showinfo("Layout Saved", "Workspace layout saved successfully.")
def _load_layout(self):
"""Load saved workspace layout."""
self.workspace.load_layout("user_layout")
messagebox.showinfo("Layout Loaded", "Workspace layout loaded successfully.")
def _restore_all_docks(self):
"""Restore all docks to visible state."""
for dock_id, dock in self.workspace.docks.items():
if not dock.is_visible:
dock.show()
if dock.is_minimized:
dock.restore()
def _start_system(self):
"""Start the 1553 bus system."""
if hasattr(self.bus_module, 'start'):
self.bus_module.start()
messagebox.showinfo("System", "Bus system started.")
def _stop_system(self):
"""Stop the 1553 bus system."""
if hasattr(self.bus_module, 'stop'):
self.bus_module.stop()
def _open_1553_debug(self):
"""Open a separate window with the PyBusMonitor1553 DebugView (1553 debug)."""
try:
# Import the simplified DebugView (not MonitorApp)
from pymsc.PyBusMonitor1553.gui.debug_view import DebugView
except Exception as e:
messagebox.showerror("1553 Debug", f"Cannot import DebugView: {e}")
return
try:
win = tk.Toplevel(self.root)
win.title("1553 Debug Monitor")
win.geometry("1000x700")
# Pass existing BusMonitorCore instance
bus_monitor_instance = getattr(self.bus_module, 'core', None)
if not bus_monitor_instance:
messagebox.showerror("1553 Debug", "BusMonitorCore not initialized")
win.destroy()
return
# Create DebugView with existing bus_monitor
app = DebugView(parent=win, bus_monitor=bus_monitor_instance)
# Keep references
self._1553_debug_win = win
self._1553_debug_app = app
# Safe close handler
def _close_debug():
try:
app.stop()
except Exception:
pass
try:
win.destroy()
except Exception:
pass
win.protocol("WM_DELETE_WINDOW", _close_debug)
logger = logging.getLogger('ARTOS')
logger.info("1553 Debug window opened")
except Exception as e:
messagebox.showerror("1553 Debug", f"Failed to create debug window: {e}")
return
messagebox.showinfo("System", "Bus system stopped.")
def _show_system_info(self):
"""Display system information."""
info = f"""
ARTOS System Information
Architecture: Layered Modular Design
- L0: Hardware Abstraction (1553 Bus)
- L1: Test Orchestration (TestContext)
- L2: Algorithm Library
- L3: Test Execution
Active Modules:
- MCS: Mission Control System ✓
- Navigation: Placeholder
- IRST: Placeholder
- Logger: Placeholder
Framework: Tkinter + Custom Docking
Bus Protocol: MIL-STD-1553B over UDP
"""
messagebox.showinfo("System Information", info.strip())
def _show_about(self):
"""Show about dialog."""
about_text = """
ARTOS v2.0
Advanced Radar Test & Orchestration System
Modular GUI with docking panels
Built with Tkinter and ttkbootstrap
© 2025
"""
messagebox.showinfo("About ARTOS", about_text.strip())
def _show_docs(self):
"""Open documentation."""
messagebox.showinfo(
"Documentation",
"Documentation is available in the 'doc/' folder.\n\n"
"Key files:\n"
"- ARTOS_Architecture_Decisions.md\n"
"- ARTOS_design.md\n"
"- English-manual.md"
)
def _on_close(self):
"""Handle window close event."""
# Prevent multiple close attempts
if self._closing:
return
self._closing = True
# Save layout automatically before closing (includes window geometry)
try:
self.workspace.save_layout("user_layout")
except Exception:
pass
if messagebox.askokcancel("Quit", "Do you want to quit ARTOS?"):
# Stop GUI refresh loop FIRST to prevent any further after() calls
self._refresh_active = False
# Stop resource monitor
try:
if hasattr(self, 'resource_monitor'):
self.resource_monitor.stop()
except Exception:
pass
# Shutdown TkinterLogger system
try:
if hasattr(self, 'logger_system'):
self.logger_system.shutdown()
except Exception:
pass
# Destroy window immediately
try:
self.root.destroy()
except Exception:
pass
# Force immediate exit - don't wait for threads to clean up
# This is necessary because TimeSync and BusMonitor threads may block
os._exit(0)
else:
# User cancelled - allow close again later
self._closing = False
def run(self):
"""Start the main event loop."""
self.root.mainloop()
def main():
"""Main entry point for the ARTOS GUI application."""
# Initialize the bus module
from pymsc.core.bus_1553_module import Bus1553Module
bus_module = Bus1553Module()
# Initialize with configuration
config = {
'udp_send_ip': '127.0.0.1',
'udp_send_port': 51553,
'udp_recv_ip': '0.0.0.0',
'udp_recv_port': 61553
}
bus_module.initialize(config)
# Create and run the main window
app = MainDockingWindow(bus_module)
app.run()
if __name__ == '__main__':
main()