ora il software si chiudere correttamente

This commit is contained in:
VALLONGOL 2026-01-12 08:18:56 +01:00
parent 652c9605ba
commit 837a9dc274
9 changed files with 233 additions and 76 deletions

View File

@ -1,5 +1,5 @@
{ {
"window_geometry": "1600x996+312+216", "window_geometry": "1600x996+52+52",
"main_sash_position": [ "main_sash_position": [
1, 1,
793 793

View File

@ -203,33 +203,12 @@ def main():
app._start_refresh() app._start_refresh()
logger.info("GUI refresh active - widgets will update automatically.") logger.info("GUI refresh active - widgets will update automatically.")
# Graceful shutdown handler # Store cleanup references in app for use in _on_close
def shutdown(): app._cleanup_time_thread = lambda: setattr(globals(), '_time_update_running', False)
"""Cleanup on application exit.""" app._time_update_thread = _time_update_thread
logger.info("Shutting down ARTOS...")
# Stop time update thread
global _time_update_running
if _time_update_running:
_time_update_running = False
logger.info("Stopping date/time synchronization thread...")
if _time_update_thread and _time_update_thread.is_alive():
_time_update_thread.join(timeout=3.0)
# Stop bus session if running
if bus_module.is_running:
bus_module.stop_session()
# Export performance metrics
logger.info("Exporting performance metrics...")
save_stats_to_csv("performance_report.csv")
print_stats()
logger.info("ARTOS shutdown complete.")
app.root.quit()
app.root.destroy()
app.root.protocol("WM_DELETE_WINDOW", shutdown) # Note: MainDockingWindow handles WM_DELETE_WINDOW with its own _on_close()
# which calls os._exit(0) for immediate termination
# Start the GUI main loop # Start the GUI main loop
logger.info("ARTOS GUI ready. Starting main loop...") logger.info("ARTOS GUI ready. Starting main loop...")

View File

@ -5,5 +5,6 @@ ARTOS Docking System - Tkinter-based modular GUI framework
from .dock_frame import DockFrame from .dock_frame import DockFrame
from .workspace_manager import WorkspaceManager from .workspace_manager import WorkspaceManager
from .title_panel import TitlePanel
__all__ = ['DockFrame', 'WorkspaceManager'] __all__ = ['DockFrame', 'WorkspaceManager', 'TitlePanel']

View File

@ -5,10 +5,10 @@ Logger Dock: Real-time system logs display.
import tkinter as tk import tkinter as tk
from tkinter import ttk, scrolledtext from tkinter import ttk, scrolledtext
import logging import logging
from pymsc.gui.docking import DockFrame from pymsc.gui.docking.title_panel import TitlePanel
class LoggerDock(DockFrame): class LoggerDock(TitlePanel):
""" """
Logger dockable panel with scrollable text display. Logger dockable panel with scrollable text display.
Shows real-time logs from the ARTOS system. Shows real-time logs from the ARTOS system.
@ -24,30 +24,82 @@ class LoggerDock(DockFrame):
self.existing_widget = existing_widget self.existing_widget = existing_widget
self.existing_handler = existing_handler self.existing_handler = existing_handler
super().__init__( super().__init__(parent, title="System Logger", closable=False, **kwargs)
parent,
title="System Logger",
closable=False, # Logger always visible
**kwargs
)
self.log_handler = existing_handler self.log_handler = existing_handler
def populate_content(self): def populate_content(self):
"""Create or reuse the scrollable log text widget.""" """Create or reuse the scrollable log text widget."""
# Remove outer padding from the content frame so the text widget
# can occupy the full dock area without an unwanted gap.
try:
self.content_frame.configure(padding=0)
except Exception:
pass
if self.existing_widget: if self.existing_widget:
# Reuse existing widget - reparent it properly # Try to reparent the existing widget. If that doesn't render
self.log_text = self.existing_widget # correctly (some platforms don't fully support reparenting),
# create a new ScrolledText inside our content_frame and copy
# the text/tags across, then update the handler to point to it.
old_widget = self.existing_widget
# Read existing content (if any) and current view
try:
old_text = old_widget.get('1.0', tk.END)
except Exception:
old_text = ''
# Remove old widget from geometry managers
try:
old_widget.pack_forget()
except Exception:
pass
# Create a fresh ScrolledText in our content frame
self.log_text = scrolledtext.ScrolledText(
self.content_frame,
wrap=tk.WORD,
height=10,
font=('Consolas', 9),
bg='#ffffff',
fg='#000000',
state='disabled'
)
# Configure tags for different log levels on the new widget
self.log_text.tag_config('INFO', foreground='#0066cc')
self.log_text.tag_config('WARNING', foreground='#ff8800')
self.log_text.tag_config('ERROR', foreground='#cc0000')
self.log_text.tag_config('DEBUG', foreground='#666666')
# Insert previous content into the new widget
try:
self.log_text.config(state='normal')
if old_text:
self.log_text.insert(tk.END, old_text)
self.log_text.see(tk.END)
self.log_text.config(state='disabled')
except Exception:
pass
# Pack the new widget to fill area
self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
# Update or create the handler so logs go to the new widget
if self.existing_handler:
try:
self.existing_handler.text_widget = self.log_text
self.log_handler = self.existing_handler
except Exception:
self.log_handler = TextWidgetHandler(self.log_text)
logging.getLogger().addHandler(self.log_handler)
else:
self.log_handler = TextWidgetHandler(self.log_text)
logging.getLogger().addHandler(self.log_handler)
# Load previous logs from file to show startup messages
self._load_previous_logs()
# Change the parent of the widget # Log that we've transferred
self.log_text.master = self.content_frame
self.log_text.pack_forget() # Remove from old parent
# Re-pack in new parent
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Handler already exists and is attached
# Just log that we've transferred
import logging
logger = logging.getLogger('ARTOS') logger = logging.getLogger('ARTOS')
logger.info("System Logger dock created - continuing to capture logs") logger.info("System Logger dock created - continuing to capture logs")
else: else:
@ -61,7 +113,8 @@ class LoggerDock(DockFrame):
fg='#000000', fg='#000000',
state='disabled' # Read-only state='disabled' # Read-only
) )
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Pack without extra padding so it fills the dock content fully
self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
# Configure tags for different log levels # Configure tags for different log levels
self.log_text.tag_config('INFO', foreground='#0066cc') self.log_text.tag_config('INFO', foreground='#0066cc')

View File

@ -5,12 +5,12 @@ Contains all MCS controls, commands, and telemetry widgets.
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from pymsc.gui.docking import DockFrame from pymsc.gui.docking.title_panel import TitlePanel
from pymsc.gui.pages.control_page import ControlPage from pymsc.gui.pages.control_page import ControlPage
from pymsc.gui.pages.mission_page import MissionPage from pymsc.gui.pages.mission_page import MissionPage
class MCSDock(DockFrame): class MCSDock(TitlePanel):
""" """
MCS (Mission Control System) dockable panel. MCS (Mission Control System) dockable panel.
Contains tabs for different MCS functions: Contains tabs for different MCS functions:
@ -27,12 +27,7 @@ class MCSDock(DockFrame):
""" """
self.bus_module = bus_module self.bus_module = bus_module
super().__init__( super().__init__(parent, title="MCS - Mission Control System", closable=False, **kwargs)
parent,
title="MCS - Mission Control System",
closable=False, # MCS is always visible
**kwargs
)
def populate_content(self): def populate_content(self):
"""Create the MCS tab structure and pages.""" """Create the MCS tab structure and pages."""

View File

@ -4,10 +4,10 @@ Placeholder Dock: Generic empty dock for future modules.
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from pymsc.gui.docking import DockFrame from pymsc.gui.docking.title_panel import TitlePanel
class PlaceholderDock(DockFrame): class PlaceholderDock(TitlePanel):
""" """
Placeholder dock for modules not yet implemented. Placeholder dock for modules not yet implemented.
Shows module name and description. Shows module name and description.
@ -23,7 +23,6 @@ class PlaceholderDock(DockFrame):
**kwargs: Additional DockFrame options **kwargs: Additional DockFrame options
""" """
self.description = description self.description = description
super().__init__(parent, title=title, **kwargs) super().__init__(parent, title=title, **kwargs)
def populate_content(self): def populate_content(self):

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""
TitlePanel: Lightweight titled panel (Label-like) used instead of full docking.
Provides a simple titled frame with a content area, basic minimize/restore
and visibility API compatible with existing docks so WorkspaceManager can
save/restore state unchanged.
"""
import tkinter as tk
from tkinter import ttk
from typing import Optional, Callable
class TitlePanel(ttk.Frame):
"""Simple titled panel that mimics DockFrame API minimally.
Use this when you want a lighter weight, non-dockable box with a title
and a content area that expands to fill available space.
"""
def __init__(self, parent: tk.Widget, title: str = "", closable: bool = False, on_close: Optional[Callable] = None, **kwargs):
super().__init__(parent, **kwargs)
self.title = title
self.closable = closable
self.on_close_callback = on_close
self.is_minimized = False
self.is_visible = True
self._create_ui()
def _create_ui(self):
# Title area (simple label)
self.title_bar = ttk.Frame(self, relief=tk.FLAT)
self.title_bar.pack(side=tk.TOP, fill=tk.X)
self.title_label = ttk.Label(self.title_bar, text=self.title, font=('Arial', 10, 'bold'), padding=(4, 2))
self.title_label.pack(side=tk.LEFT)
if self.closable:
self.close_btn = ttk.Button(self.title_bar, text='', width=3, command=self.close)
self.close_btn.pack(side=tk.RIGHT, padx=(4, 0))
# Content container
self.content_frame = ttk.Frame(self)
self.content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
def populate_content(self):
"""Subclasses should override this to populate `self.content_frame`."""
pass
def minimize(self):
if not self.is_minimized:
self.content_frame.pack_forget()
self.is_minimized = True
def restore(self):
if self.is_minimized:
self.content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.is_minimized = False
def toggle_minimize(self):
if self.is_minimized:
self.restore()
else:
self.minimize()
def close(self):
self.pack_forget()
self.is_visible = False
if self.on_close_callback:
self.on_close_callback(self)
def show(self):
if not self.is_visible:
self.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.is_visible = True
if self.is_minimized:
self.restore()
def set_title(self, new_title: str):
self.title = new_title
self.title_label.configure(text=new_title)

View File

@ -9,6 +9,7 @@ import json
import os import os
from typing import Dict, Optional from typing import Dict, Optional
from .dock_frame import DockFrame from .dock_frame import DockFrame
from .title_panel import TitlePanel
class WorkspaceManager: class WorkspaceManager:
@ -30,7 +31,8 @@ class WorkspaceManager:
""" """
self.root = root self.root = root
self.layouts_dir = layouts_dir self.layouts_dir = layouts_dir
self.docks: Dict[str, DockFrame] = {} # docks may be legacy DockFrame or new TitlePanel instances
self.docks: Dict[str, object] = {}
# Create layouts directory if needed # Create layouts directory if needed
if not os.path.exists(self.layouts_dir): if not os.path.exists(self.layouts_dir):
@ -90,8 +92,8 @@ class WorkspaceManager:
self.bottom_container = ttk.Frame(self.main_pane) self.bottom_container = ttk.Frame(self.main_pane)
self.main_pane.add(self.bottom_container, minsize=150, height=200) self.main_pane.add(self.bottom_container, minsize=150, height=200)
def add_dock(self, dock_id: str, dock: DockFrame, def add_dock(self, dock_id: str, dock,
position: str = 'left') -> DockFrame: position: str = 'left'):
""" """
Register and place a dock in the workspace. Register and place a dock in the workspace.
@ -103,6 +105,7 @@ class WorkspaceManager:
Returns: Returns:
The dock instance The dock instance
""" """
# Accept either DockFrame or TitlePanel (or any widget with packable API)
self.docks[dock_id] = dock self.docks[dock_id] = dock
if position == 'left': if position == 'left':

View File

@ -6,6 +6,9 @@ Modular GUI with dockable panels for different system modules.
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import logging import logging
import sys
import os
import threading
from pymsc.core.bus_1553_module import Bus1553Module from pymsc.core.bus_1553_module import Bus1553Module
from pymsc.gui.docking.workspace_manager import WorkspaceManager from pymsc.gui.docking.workspace_manager import WorkspaceManager
@ -35,6 +38,8 @@ class MainDockingWindow:
bus_module: Bus1553Module instance for message access bus_module: Bus1553Module instance for message access
""" """
self.bus_module = bus_module 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 # Create main window
self.root = tk.Tk() self.root = tk.Tk()
@ -179,29 +184,40 @@ class MainDockingWindow:
def _start_refresh(self): def _start_refresh(self):
"""Start the GUI refresh loop to update all widgets.""" """Start the GUI refresh loop to update all widgets."""
self._refresh_active = True
self.gui_refresh_loop() self.gui_refresh_loop()
def _stop_refresh(self): def _stop_refresh(self):
"""Stop refresh loops (cleanup on exit).""" """Stop refresh loops (cleanup on exit)."""
# The refresh loop will stop automatically when window closes self._refresh_active = False
pass
def gui_refresh_loop(self): def gui_refresh_loop(self):
""" """
Periodically refreshes all custom widgets in all visible docks. Periodically refreshes all custom widgets in all visible docks.
This updates telemetry displays from received 1553 messages. 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) # First, sync all messages from the 1553 bus (read latest telemetry)
if self.bus_module and self.bus_module.is_running: try:
self.bus_module.sync_all_messages() 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 # Then refresh widgets in all visible docks
for dock_id, dock in self.workspace.docks.items(): try:
if dock.is_visible and not dock.is_minimized: for dock_id, dock in self.workspace.docks.items():
self._refresh_widgets_recursive(dock.content_frame) if dock.is_visible and not dock.is_minimized:
self._refresh_widgets_recursive(dock.content_frame)
except Exception:
pass
# Schedule next refresh (100ms = 10 Hz update rate) # Schedule next refresh only if still active (100ms = 10 Hz update rate)
self.root.after(100, self.gui_refresh_loop) if self._refresh_active:
self.root.after(100, self.gui_refresh_loop)
def _refresh_widgets_recursive(self, container: tk.Widget): def _refresh_widgets_recursive(self, container: tk.Widget):
""" """
@ -301,13 +317,40 @@ Built with Tkinter and ttkbootstrap
def _on_close(self): def _on_close(self):
"""Handle window close event.""" """Handle window close event."""
# Prevent multiple close attempts
if self._closing:
return
self._closing = True
# Save layout automatically before closing (includes window geometry) # Save layout automatically before closing (includes window geometry)
self.workspace.save_layout("user_layout") try:
self.workspace.save_layout("user_layout")
except Exception:
pass
if messagebox.askokcancel("Quit", "Do you want to quit ARTOS?"): if messagebox.askokcancel("Quit", "Do you want to quit ARTOS?"):
self._stop_refresh() # Stop GUI refresh loop FIRST to prevent any further after() calls
self.root.quit() self._refresh_active = False
self.root.destroy()
# Remove GUI log handler to avoid callbacks after widgets destroyed
try:
if hasattr(self, '_log_handler'):
logging.getLogger().removeHandler(self._log_handler)
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): def run(self):
"""Start the main event loop.""" """Start the main event loop."""