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": [
1,
793

View File

@ -203,33 +203,12 @@ def main():
app._start_refresh()
logger.info("GUI refresh active - widgets will update automatically.")
# Graceful shutdown handler
def shutdown():
"""Cleanup on application exit."""
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()
# Store cleanup references in app for use in _on_close
app._cleanup_time_thread = lambda: setattr(globals(), '_time_update_running', False)
app._time_update_thread = _time_update_thread
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
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 .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
from tkinter import ttk, scrolledtext
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.
Shows real-time logs from the ARTOS system.
@ -24,30 +24,82 @@ class LoggerDock(DockFrame):
self.existing_widget = existing_widget
self.existing_handler = existing_handler
super().__init__(
parent,
title="System Logger",
closable=False, # Logger always visible
**kwargs
)
super().__init__(parent, title="System Logger", closable=False, **kwargs)
self.log_handler = existing_handler
def populate_content(self):
"""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:
# Reuse existing widget - reparent it properly
self.log_text = self.existing_widget
# Try to reparent the existing widget. If that doesn't render
# 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
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
# Log that we've transferred
logger = logging.getLogger('ARTOS')
logger.info("System Logger dock created - continuing to capture logs")
else:
@ -61,7 +113,8 @@ class LoggerDock(DockFrame):
fg='#000000',
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
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
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.mission_page import MissionPage
class MCSDock(DockFrame):
class MCSDock(TitlePanel):
"""
MCS (Mission Control System) dockable panel.
Contains tabs for different MCS functions:
@ -27,12 +27,7 @@ class MCSDock(DockFrame):
"""
self.bus_module = bus_module
super().__init__(
parent,
title="MCS - Mission Control System",
closable=False, # MCS is always visible
**kwargs
)
super().__init__(parent, title="MCS - Mission Control System", closable=False, **kwargs)
def populate_content(self):
"""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
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.
Shows module name and description.
@ -23,7 +23,6 @@ class PlaceholderDock(DockFrame):
**kwargs: Additional DockFrame options
"""
self.description = description
super().__init__(parent, title=title, **kwargs)
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
from typing import Dict, Optional
from .dock_frame import DockFrame
from .title_panel import TitlePanel
class WorkspaceManager:
@ -30,7 +31,8 @@ class WorkspaceManager:
"""
self.root = root
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
if not os.path.exists(self.layouts_dir):
@ -90,8 +92,8 @@ class WorkspaceManager:
self.bottom_container = ttk.Frame(self.main_pane)
self.main_pane.add(self.bottom_container, minsize=150, height=200)
def add_dock(self, dock_id: str, dock: DockFrame,
position: str = 'left') -> DockFrame:
def add_dock(self, dock_id: str, dock,
position: str = 'left'):
"""
Register and place a dock in the workspace.
@ -103,6 +105,7 @@ class WorkspaceManager:
Returns:
The dock instance
"""
# Accept either DockFrame or TitlePanel (or any widget with packable API)
self.docks[dock_id] = dock
if position == 'left':

View File

@ -6,6 +6,9 @@ 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
from pymsc.core.bus_1553_module import Bus1553Module
from pymsc.gui.docking.workspace_manager import WorkspaceManager
@ -35,6 +38,8 @@ class MainDockingWindow:
bus_module: Bus1553Module instance for message access
"""
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()
@ -179,29 +184,40 @@ class MainDockingWindow:
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)."""
# The refresh loop will stop automatically when window closes
pass
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)
if self.bus_module and self.bus_module.is_running:
self.bus_module.sync_all_messages()
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
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)
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 (100ms = 10 Hz update rate)
self.root.after(100, self.gui_refresh_loop)
# 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):
"""
@ -301,13 +317,40 @@ Built with Tkinter and ttkbootstrap
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)
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?"):
self._stop_refresh()
self.root.quit()
self.root.destroy()
# Stop GUI refresh loop FIRST to prevent any further after() calls
self._refresh_active = False
# 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):
"""Start the main event loop."""