sistemato log

This commit is contained in:
VALLONGOL 2026-01-12 09:15:25 +01:00
parent 837a9dc274
commit 18f3aba5c6
5 changed files with 171 additions and 244 deletions

View File

@ -1,5 +1,5 @@
{
"window_geometry": "1600x996+52+52",
"window_geometry": "1600x1099+310+130",
"main_sash_position": [
1,
793
@ -13,6 +13,10 @@
243
],
"docks": {
"logger": {
"visible": true,
"minimized": false
},
"mcs": {
"visible": true,
"minimized": false
@ -24,10 +28,6 @@
"irst": {
"visible": true,
"minimized": false
},
"logger": {
"visible": true,
"minimized": false
}
}
}

View File

@ -10,6 +10,9 @@ import threading
import time
from datetime import datetime
# Add external logger module to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '_external', 'externals', 'python-tkinter-logger'))
# Ensure local imports work correctly
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@ -21,6 +24,7 @@ from pymsc.utils.profiler import print_stats, save_stats_to_csv
# Global flag to control the time update thread
_time_update_running = False
_time_update_thread = None
_logger_system = None
def update_radar_datetime_periodically(bus_module):
@ -104,15 +108,23 @@ def main():
# Create logs directory first
os.makedirs('logs', exist_ok=True)
# Setup basic logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/artos.log'),
logging.StreamHandler()
]
# Initialize TkinterLogger IMMEDIATELY (before any logging)
# This ensures consistent formatting from the start
from tkinter_logger import TkinterLogger
# Create logger system without Tkinter widget initially (console + file only)
global _logger_system
_logger_system = TkinterLogger(tk_root=None)
_logger_system.setup(
enable_console=True,
enable_file=True,
file_path='logs/artos.log',
file_max_bytes=10 * 1024 * 1024, # 10MB
file_backup_count=5,
enable_tkinter=False, # Will add Tkinter handler later
root_level=logging.INFO
)
logger = logging.getLogger('ARTOS')
logger.info("=" * 60)
logger.info("Starting ARTOS - Advanced Radar Test & Orchestration System")
@ -196,7 +208,7 @@ def main():
# Create and launch the modular docking GUI
logger.info("Launching ARTOS Docking GUI...")
app = MainDockingWindow(bus_module)
app = MainDockingWindow(bus_module, logger_system=_logger_system)
# Start widget refresh loops to display live data
logger.info("Starting GUI refresh loops...")

View File

@ -79,6 +79,12 @@ class CommandFrameCheckBox(BaseCommandFrame):
self.start_time = time.time()
val = 1 if self.command_var.get() else 0
self.info['message'].set_value_for_field(self.info['field'], val)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
state = "ON" if val == 1 else "OFF"
logger.info(f"User changed '{self.info['label']}'{state}")
if self.script_manager.is_recording:
action_val = "on" if val == 1 else "off"
self.script_manager.write_command(self.info['label'], "toggle", action_val)
@ -144,6 +150,11 @@ class CommandFrameComboBox(BaseCommandFrame):
self.start_time = time.time()
idx = self.combo.current()
self.info['message'].set_value_for_field(self.info['field'], idx)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
logger.info(f"User changed '{self.info['label']}'{self.combo.get()}")
if self.script_manager.is_recording:
self.script_manager.write_command(self.info['label'], "set_value", self.combo.get())
if self.info.get('message_tb'):
@ -277,6 +288,13 @@ class CommandFrameSpinBox(BaseCommandFrame):
val = float(self.cmd_var.get())
raw_val = set_correct_value(self.info, -1, val)
self.info['message'].set_value_for_field(self.info['field'], raw_val)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
unit = self.info.get('unit', '')
unit_str = f" {unit}" if unit else ""
logger.info(f"User changed '{self.info['label']}'{val}{unit_str}")
# Clear editing flag after sending
self.user_editing = False
except (tk.TclError, ValueError):
@ -298,6 +316,13 @@ class CommandFrameSpinBox(BaseCommandFrame):
return
raw_val = set_correct_value(self.info, -1, val)
self.info['message'].set_value_for_field(self.info['field'], raw_val)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
unit = self.info.get('unit', '')
unit_str = f" {unit}" if unit else ""
logger.info(f"User changed '{self.info['label']}'{val}{unit_str}")
if self.script_manager.is_recording:
self.script_manager.write_command(self.info['label'], "set_value", val)
# Clear editing flag after successful change

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
"""
Logger Dock: Real-time system logs display.
Logger Dock: Real-time system logs display using external TkinterLogger module.
"""
import tkinter as tk
from tkinter import ttk, scrolledtext
@ -11,203 +11,35 @@ from pymsc.gui.docking.title_panel import TitlePanel
class LoggerDock(TitlePanel):
"""
Logger dockable panel with scrollable text display.
Shows real-time logs from the ARTOS system.
Shows real-time logs from the ARTOS system using external TkinterLogger.
"""
def __init__(self, parent: tk.Widget, existing_widget=None, existing_handler=None, **kwargs):
def __init__(self, parent: tk.Widget, **kwargs):
"""
Args:
parent: Parent widget
existing_widget: Optional pre-created ScrolledText widget to use
existing_handler: Optional pre-created TextWidgetHandler to use
"""
self.existing_widget = existing_widget
self.existing_handler = existing_handler
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.
"""Create the scrollable log text widget."""
# Remove outer padding from the content frame
try:
self.content_frame.configure(padding=0)
except Exception:
pass
if 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()
# Log that we've transferred
logger = logging.getLogger('ARTOS')
logger.info("System Logger dock created - continuing to capture logs")
else:
# Create new widget (fallback if called without existing widget)
self.log_text = scrolledtext.ScrolledText(
self.content_frame,
wrap=tk.WORD,
height=10,
font=('Consolas', 9),
bg='#ffffff',
fg='#000000',
state='disabled' # Read-only
)
# 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')
self.log_text.tag_config('WARNING', foreground='#ff8800')
self.log_text.tag_config('ERROR', foreground='#cc0000')
self.log_text.tag_config('DEBUG', foreground='#666666')
# Create custom log handler that writes to this widget
self.log_handler = TextWidgetHandler(self.log_text)
self.log_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
self.log_handler.setFormatter(formatter)
# Add handler to root logger
logging.getLogger().addHandler(self.log_handler)
# Add initial message
self.append_log("System Logger initialized. Ready to display logs.", 'INFO')
def _load_previous_logs(self):
"""Load and display logs from the log file that were written before GUI started."""
import os
log_file = 'logs/artos.log'
if not os.path.exists(log_file):
return
try:
with open(log_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Load last 100 lines to avoid overwhelming the widget
recent_lines = lines[-100:] if len(lines) > 100 else lines
self.log_text.config(state='normal')
for line in recent_lines:
line = line.rstrip('\n')
if not line:
continue
# Detect log level from line content
level = 'INFO'
if ' - ERROR - ' in line:
level = 'ERROR'
elif ' - WARNING - ' in line:
level = 'WARNING'
elif ' - DEBUG - ' in line:
level = 'DEBUG'
self.log_text.insert(tk.END, line + '\n', level)
self.log_text.see(tk.END)
self.log_text.config(state='disabled')
except Exception as e:
# Silently ignore if we can't read the file
pass
# Always create a new widget in this panel
self.log_text = scrolledtext.ScrolledText(
self.content_frame,
wrap=tk.WORD,
height=10,
font=('Consolas', 9),
bg='#ffffff',
fg='#000000'
)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=0, pady=0)
def append_log(self, message: str, level: str = 'INFO'):
"""
Append a log message to the text widget.
Args:
message: Log message text
level: Log level (INFO, WARNING, ERROR, DEBUG)
"""
self.log_text.config(state='normal')
self.log_text.insert(tk.END, message + '\n', level)
self.log_text.see(tk.END) # Auto-scroll to bottom
self.log_text.config(state='disabled')
class TextWidgetHandler(logging.Handler):
"""
Custom logging handler that outputs to a Tkinter Text widget.
"""
def __init__(self, text_widget):
super().__init__()
self.text_widget = text_widget
def emit(self, record):
"""Emit a log record to the text widget."""
try:
msg = self.format(record)
level = record.levelname
# Thread-safe update
self.text_widget.after(0, self._append_log, msg, level)
except Exception:
self.handleError(record)
def _append_log(self, message, level):
"""Append message to text widget (must be called from main thread)."""
self.text_widget.config(state='normal')
self.text_widget.insert(tk.END, message + '\n', level)
self.text_widget.see(tk.END)
self.text_widget.config(state='disabled')
def get_log_widget(self):
"""Return the log widget for external handlers to use."""
return self.log_text

View File

@ -10,11 +10,20 @@ 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, TextWidgetHandler
from pymsc.gui.docking.logger_dock import LoggerDock
from pymsc.gui.components.command_widgets import (
CommandFrameCheckBox, CommandFrameComboBox,
CommandFrameSpinBox, CommandFrameLabels,
@ -32,10 +41,11 @@ class MainDockingWindow:
- Right bottom: Logger, Status (placeholders)
"""
def __init__(self, bus_module: Bus1553Module):
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
@ -66,41 +76,53 @@ class MainDockingWindow:
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
# Create a temporary text widget for logging (will be moved to LoggerDock later)
# This allows us to capture ALL logs from the very beginning
import tkinter.scrolledtext as scrolledtext
self._temp_log_widget = scrolledtext.ScrolledText(
self.root,
wrap=tk.WORD,
font=('Consolas', 9),
bg='#ffffff',
fg='#000000',
state='disabled'
)
# Configure tags for different log levels
self._temp_log_widget.tag_config('INFO', foreground='#0066cc')
self._temp_log_widget.tag_config('WARNING', foreground='#ff8800')
self._temp_log_widget.tag_config('ERROR', foreground='#cc0000')
self._temp_log_widget.tag_config('DEBUG', foreground='#666666')
# Create and attach the log handler immediately
self._log_handler = TextWidgetHandler(self._temp_log_widget)
self._log_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
self._log_handler.setFormatter(formatter)
logging.getLogger().addHandler(self._log_handler)
# Log that GUI logger is now active
logger = logging.getLogger('ARTOS')
logger.info("GUI logger initialized - capturing all subsequent logs")
# 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 workspace manager
# 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 and register docks
# 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
@ -109,6 +131,42 @@ class MainDockingWindow:
# 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)
@ -149,7 +207,7 @@ class MainDockingWindow:
help_menu.add_command(label="Documentation", command=self._show_docs)
def _create_docks(self):
"""Create and register all dock panels."""
"""Create and register remaining dock panels (logger was created first)."""
# Left panel: MCS
mcs_dock = MCSDock(
self.workspace.left_container,
@ -173,14 +231,7 @@ class MainDockingWindow:
)
self.workspace.add_dock('irst', irst_dock, position='right_bottom')
# Bottom: Logger with scrollable text
# Transfer the temporary log widget to the LoggerDock
logger_dock = LoggerDock(
self.workspace.bottom_container,
existing_widget=self._temp_log_widget,
existing_handler=self._log_handler
)
self.workspace.add_dock('logger', logger_dock, position='bottom')
# Logger was already created first in __init__
def _start_refresh(self):
"""Start the GUI refresh loop to update all widgets."""
@ -331,11 +382,18 @@ Built with Tkinter and ttkbootstrap
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
# Remove GUI log handler to avoid callbacks after widgets destroyed
# Stop resource monitor
try:
if hasattr(self, '_log_handler'):
logging.getLogger().removeHandler(self._log_handler)
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