# -*- 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()