491 lines
17 KiB
Python
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()
|