1149 lines
45 KiB
Python
1149 lines
45 KiB
Python
"""
|
|
PyBusMonitor1553 - Main GUI Window
|
|
Tkinter-based interface for MIL-STD-1553 Bus Monitoring
|
|
"""
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter.scrolledtext import ScrolledText
|
|
import threading
|
|
import time
|
|
import queue
|
|
import sys
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional, Any
|
|
from collections import OrderedDict
|
|
import logging
|
|
|
|
from ..lib1553.fields import Field
|
|
|
|
# Import TkinterLogger from externals submodule
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'externals', 'python-tkinter-logger'))
|
|
try:
|
|
from tkinter_logger import TkinterLogger
|
|
except ImportError:
|
|
TkinterLogger = None # Fallback if module not available
|
|
|
|
|
|
@dataclass
|
|
class MessageStats:
|
|
"""Statistics for a single message type"""
|
|
name: str
|
|
cw: str # Command Word formatted string
|
|
sw: str # Status Word formatted string
|
|
count: int = 0
|
|
errors: int = 0
|
|
last_update: float = 0.0
|
|
period_ms: float = 0.0
|
|
word_count: int = 0
|
|
is_transmit: bool = False # True = RT->BC (B-msg), False = BC->RT (A-msg)
|
|
last_data: Optional[Any] = None
|
|
raw_words: List[int] = field(default_factory=list)
|
|
|
|
|
|
class BusMonitorApp:
|
|
"""Main application window for PyBusMonitor1553"""
|
|
|
|
def __init__(self, root: tk.Tk, radar_controller=None):
|
|
self.root = root
|
|
self.root.title("PyBusMonitor1553 - MIL-STD-1553 Bus Monitor")
|
|
self.root.geometry("1200x900") # Increased height for log panel
|
|
self.root.minsize(800, 700)
|
|
|
|
# Radar controller for command control (optional)
|
|
self.radar_controller = radar_controller
|
|
|
|
# Message statistics storage
|
|
self.message_stats: Dict[str, MessageStats] = OrderedDict()
|
|
self._init_message_stats()
|
|
|
|
# Selected message for detail view
|
|
self.selected_message: Optional[str] = None
|
|
|
|
# Connection status
|
|
self.is_connected = False
|
|
self.tx_count = 0
|
|
self.rx_count = 0
|
|
self.last_error = ""
|
|
|
|
# Thread-safe queue for message updates (disaccoppiamento GUI/motore)
|
|
self._update_queue = queue.Queue(maxsize=1000)
|
|
self._status_queue = queue.Queue(maxsize=100)
|
|
|
|
# GUI update timer (rate limiting a 100ms invece di aggiornamento continuo)
|
|
self._gui_update_interval_ms = 100
|
|
|
|
# Logger system
|
|
self.logger_system: Optional[Any] = None # TkinterLogger instance
|
|
|
|
# Build UI
|
|
self._create_menu()
|
|
self._create_main_layout()
|
|
self._create_log_panel() # NEW: Log panel at bottom
|
|
self._create_status_bar()
|
|
|
|
# Initialize logging system
|
|
self._setup_logging()
|
|
|
|
# Update timer
|
|
self._update_id = None
|
|
|
|
# Avvia il timer periodico per elaborare la coda aggiornamenti
|
|
self._start_gui_update_timer()
|
|
|
|
# Log startup
|
|
logging.getLogger(__name__).info("PyBusMonitor1553 GUI initialized")
|
|
|
|
def _init_message_stats(self):
|
|
"""Initialize message statistics for all known message types"""
|
|
# A-messages (BC -> RT, Receive by RT)
|
|
a_messages = [
|
|
("A1", 1, False, 10), # Settings
|
|
("A2", 2, False, 3), # Operation Command
|
|
("A3", 3, False, 32), # Graphics Command
|
|
("A4", 4, False, 31), # Navigation Data
|
|
("A5", 5, False, 23), # INU/GPS Data
|
|
("A6", 6, False, 31), # Weapon Aiming
|
|
("A7", 7, False, 31), # Reserved
|
|
("A8", 8, False, 30), # Reserved
|
|
]
|
|
|
|
# B-messages (RT -> BC, Transmit by RT)
|
|
b_messages = [
|
|
("B1", 11, True, 29), # Target Report #1
|
|
("B2", 12, True, 27), # Target Report #2
|
|
("B3", 13, True, 27), # Target Report #3
|
|
("B4", 14, True, 27), # Terrain Avoidance
|
|
("B5", 15, True, 27), # Reserved
|
|
("B6", 16, True, 23), # Settings Tell-Back
|
|
("B7", 17, True, 3), # Status Tell-Back
|
|
("B8", 18, True, 32), # Reserved
|
|
]
|
|
|
|
for name, sa, is_tx, wc in a_messages + b_messages:
|
|
direction = "T" if is_tx else "R"
|
|
cw_str = f"20-{direction}-{sa}-{wc}"
|
|
self.message_stats[name] = MessageStats(
|
|
name=name,
|
|
cw=cw_str,
|
|
sw="0-0-0",
|
|
word_count=wc,
|
|
is_transmit=is_tx
|
|
)
|
|
|
|
def _create_menu(self):
|
|
"""Create application 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="Connect", command=self._on_connect)
|
|
file_menu.add_command(label="Disconnect", command=self._on_disconnect)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Exit", command=self.root.quit)
|
|
|
|
# View menu
|
|
view_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="View", menu=view_menu)
|
|
view_menu.add_command(label="Reset Counters", command=self._reset_counters)
|
|
|
|
# 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)
|
|
|
|
def _create_main_layout(self):
|
|
"""Create the main application layout with notebook tabs"""
|
|
# Control panel at top
|
|
self._create_control_panel()
|
|
|
|
# Main container with PanedWindow for resizable split
|
|
self.main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
|
|
self.main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
# Left panel - Message list
|
|
left_frame = ttk.Frame(self.main_paned)
|
|
self.main_paned.add(left_frame, weight=1)
|
|
|
|
# Right panel - Notebook with tabs
|
|
right_frame = ttk.Frame(self.main_paned)
|
|
self.main_paned.add(right_frame, weight=2)
|
|
|
|
# Create message list (left side)
|
|
self._create_message_list(left_frame)
|
|
|
|
# Create notebook tabs (right side)
|
|
self._create_notebook(right_frame)
|
|
|
|
def _create_control_panel(self):
|
|
"""Create the control panel with Run/Stop buttons"""
|
|
control_frame = ttk.Frame(self.root)
|
|
control_frame.pack(fill=tk.X, padx=5, pady=(5, 0))
|
|
|
|
# Run button (green)
|
|
self.btn_run = tk.Button(
|
|
control_frame,
|
|
text="▶ RUN",
|
|
font=('Helvetica', 10, 'bold'),
|
|
bg='#28a745',
|
|
fg='white',
|
|
activebackground='#218838',
|
|
activeforeground='white',
|
|
width=12,
|
|
height=1,
|
|
relief=tk.RAISED,
|
|
command=self._on_run
|
|
)
|
|
self.btn_run.pack(side=tk.LEFT, padx=5)
|
|
|
|
# Stop button (red)
|
|
self.btn_stop = tk.Button(
|
|
control_frame,
|
|
text="■ STOP",
|
|
font=('Helvetica', 10, 'bold'),
|
|
bg='#dc3545',
|
|
fg='white',
|
|
activebackground='#c82333',
|
|
activeforeground='white',
|
|
width=12,
|
|
height=1,
|
|
relief=tk.RAISED,
|
|
command=self._on_stop,
|
|
state=tk.DISABLED # Initially disabled
|
|
)
|
|
self.btn_stop.pack(side=tk.LEFT, padx=5)
|
|
|
|
# Status label
|
|
self.lbl_status = ttk.Label(
|
|
control_frame,
|
|
text="● Ready - Press RUN to start",
|
|
font=('Helvetica', 9),
|
|
foreground='orange'
|
|
)
|
|
self.lbl_status.pack(side=tk.LEFT, padx=20)
|
|
|
|
def _create_message_list(self, parent):
|
|
"""Create the message list TreeView (like GrifoScope)"""
|
|
# Header
|
|
header = ttk.Label(parent, text="1553 Bus Messages", font=('Helvetica', 11, 'bold'))
|
|
header.pack(pady=(0, 5))
|
|
|
|
# TreeView with scrollbar
|
|
tree_frame = ttk.Frame(parent)
|
|
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
columns = ('name', 'cw', 'sw', 'count', 'errs', 'period', 'wc')
|
|
self.msg_tree = ttk.Treeview(tree_frame, columns=columns, show='headings',
|
|
selectmode='browse')
|
|
|
|
# Column headers
|
|
self.msg_tree.heading('name', text='Name')
|
|
self.msg_tree.heading('cw', text='CW')
|
|
self.msg_tree.heading('sw', text='SW')
|
|
self.msg_tree.heading('count', text='Num')
|
|
self.msg_tree.heading('errs', text='Errs')
|
|
self.msg_tree.heading('period', text='Period')
|
|
self.msg_tree.heading('wc', text='WC')
|
|
|
|
# Column widths
|
|
self.msg_tree.column('name', width=50, anchor='center')
|
|
self.msg_tree.column('cw', width=90, anchor='center')
|
|
self.msg_tree.column('sw', width=70, anchor='center')
|
|
self.msg_tree.column('count', width=60, anchor='center')
|
|
self.msg_tree.column('errs', width=50, anchor='center')
|
|
self.msg_tree.column('period', width=70, anchor='center')
|
|
self.msg_tree.column('wc', width=40, anchor='center')
|
|
|
|
# Scrollbar
|
|
scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.msg_tree.yview)
|
|
self.msg_tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
self.msg_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
# Bind selection event
|
|
self.msg_tree.bind('<<TreeviewSelect>>', self._on_message_select)
|
|
|
|
# Configure row tags for coloring
|
|
self.msg_tree.tag_configure('a_msg', background='#E8F4E8') # Light green for A-msgs
|
|
self.msg_tree.tag_configure('b_msg', background='#E8E8F4') # Light blue for B-msgs
|
|
self.msg_tree.tag_configure('error', foreground='red')
|
|
|
|
# Populate initial data
|
|
self._populate_message_list()
|
|
|
|
def _populate_message_list(self):
|
|
"""Populate the message list with initial data"""
|
|
for name, stats in self.message_stats.items():
|
|
tag = 'b_msg' if stats.is_transmit else 'a_msg'
|
|
period_str = f"{stats.period_ms:.1f}" if stats.period_ms > 0 else "-"
|
|
self.msg_tree.insert('', 'end', iid=name, values=(
|
|
name,
|
|
stats.cw,
|
|
stats.sw,
|
|
stats.count,
|
|
stats.errors,
|
|
period_str,
|
|
stats.word_count
|
|
), tags=(tag,))
|
|
|
|
def _create_notebook(self, parent):
|
|
"""Create the right-side notebook with tabs"""
|
|
self.notebook = ttk.Notebook(parent)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Tab 1: Message Detail
|
|
self.detail_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.detail_frame, text="Message Detail")
|
|
self._create_detail_tab(self.detail_frame)
|
|
|
|
# Tab 2: Radar Status Dashboard
|
|
self.dashboard_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.dashboard_frame, text="Radar Status")
|
|
self._create_dashboard_tab(self.dashboard_frame)
|
|
|
|
# Tab 3: Radar Command (NEW - Control panel for A2 message)
|
|
self.command_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.command_frame, text="Radar Command")
|
|
self._create_command_tab(self.command_frame)
|
|
|
|
# Tab 4: Raw Data
|
|
self.raw_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.raw_frame, text="Raw Data")
|
|
self._create_raw_tab(self.raw_frame)
|
|
|
|
def _create_log_panel(self):
|
|
"""Create the log panel at the bottom of the window"""
|
|
# Separator
|
|
separator = ttk.Separator(self.root, orient=tk.HORIZONTAL)
|
|
separator.pack(fill=tk.X, padx=5)
|
|
|
|
# Log panel container
|
|
log_container = ttk.LabelFrame(self.root, text="Application Log", padding=5)
|
|
log_container.pack(fill=tk.BOTH, expand=False, padx=5, pady=(5, 0), side=tk.BOTTOM)
|
|
|
|
# ScrolledText widget for log display
|
|
self.log_text = ScrolledText(
|
|
log_container,
|
|
height=8, # 8 lines of log
|
|
width=80,
|
|
state=tk.DISABLED, # Read-only
|
|
wrap=tk.WORD,
|
|
background='#F5F5F5',
|
|
font=('Consolas', 9)
|
|
)
|
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
|
|
|
def _setup_logging(self):
|
|
"""Initialize the TkinterLogger system"""
|
|
if TkinterLogger is None:
|
|
# Fallback: basic logging to console
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
|
datefmt='%H:%M:%S'
|
|
)
|
|
logging.getLogger(__name__).warning("TkinterLogger not available, using console logging")
|
|
return
|
|
|
|
# Initialize TkinterLogger system
|
|
self.logger_system = TkinterLogger(self.root)
|
|
self.logger_system.setup(
|
|
enable_console=True, # Also log to console
|
|
enable_tkinter=True,
|
|
enable_file=False, # No file logging for now
|
|
log_format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
|
date_format='%H:%M:%S',
|
|
root_level=logging.DEBUG # Set to DEBUG to see detailed logs
|
|
)
|
|
|
|
# Add the Tkinter widget handler
|
|
self.logger_system.add_tkinter_handler(
|
|
self.log_text,
|
|
max_lines=500 # Keep last 500 log lines
|
|
)
|
|
|
|
logging.getLogger(__name__).info("Logging system initialized")
|
|
|
|
def _create_detail_tab(self, parent):
|
|
"""Create the message detail view tab"""
|
|
# Header with selected message name
|
|
self.detail_header = ttk.Label(parent, text="Select a message to view details",
|
|
font=('Helvetica', 12, 'bold'))
|
|
self.detail_header.pack(pady=10)
|
|
|
|
# Scrollable frame for fields
|
|
canvas = tk.Canvas(parent)
|
|
scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
|
|
self.detail_scroll_frame = ttk.Frame(canvas)
|
|
|
|
self.detail_scroll_frame.bind(
|
|
"<Configure>",
|
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
)
|
|
|
|
canvas.create_window((0, 0), window=self.detail_scroll_frame, anchor="nw")
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
|
canvas.pack(side="left", fill="both", expand=True)
|
|
scrollbar.pack(side="right", fill="y")
|
|
|
|
# Store reference to canvas for updates
|
|
self.detail_canvas = canvas
|
|
|
|
# Dictionary to hold field labels for updating
|
|
self.detail_field_labels: Dict[str, ttk.Label] = {}
|
|
|
|
# Track which message detail view is currently built (cache)
|
|
self._current_detail_message: Optional[str] = None
|
|
self._detail_widgets_cache: Dict[str, Dict[str, tuple]] = {}
|
|
# Structure: {msg_name: {field_name: (raw_label, enum_label, value_label)}}
|
|
|
|
def _create_dashboard_tab(self, parent):
|
|
"""Create the radar status dashboard tab"""
|
|
# Title
|
|
title = ttk.Label(parent, text="Radar Status Dashboard", font=('Helvetica', 14, 'bold'))
|
|
title.pack(pady=10)
|
|
|
|
# Main frame with grid
|
|
dashboard = ttk.Frame(parent)
|
|
dashboard.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
|
|
|
|
# Left column - Mode and Status
|
|
left_col = ttk.LabelFrame(dashboard, text="Radar Mode", padding=10)
|
|
left_col.grid(row=0, column=0, padx=10, pady=5, sticky='nsew')
|
|
|
|
self.dashboard_labels = {}
|
|
|
|
# Mode indicator
|
|
ttk.Label(left_col, text="Master Mode:").grid(row=0, column=0, sticky='e', padx=5)
|
|
self.dashboard_labels['mode'] = ttk.Label(left_col, text="---", font=('Helvetica', 11, 'bold'))
|
|
self.dashboard_labels['mode'].grid(row=0, column=1, sticky='w', padx=5)
|
|
|
|
ttk.Label(left_col, text="Standby:").grid(row=1, column=0, sticky='e', padx=5)
|
|
self.dashboard_labels['standby'] = ttk.Label(left_col, text="---")
|
|
self.dashboard_labels['standby'].grid(row=1, column=1, sticky='w', padx=5)
|
|
|
|
ttk.Label(left_col, text="RF Radiation:").grid(row=2, column=0, sticky='e', padx=5)
|
|
self.dashboard_labels['rf'] = ttk.Label(left_col, text="---")
|
|
self.dashboard_labels['rf'].grid(row=2, column=1, sticky='w', padx=5)
|
|
|
|
ttk.Label(left_col, text="Transition:").grid(row=3, column=0, sticky='e', padx=5)
|
|
self.dashboard_labels['transition'] = ttk.Label(left_col, text="---")
|
|
self.dashboard_labels['transition'].grid(row=3, column=1, sticky='w', padx=5)
|
|
|
|
# Right column - Health
|
|
right_col = ttk.LabelFrame(dashboard, text="Health Status", padding=10)
|
|
right_col.grid(row=0, column=1, padx=10, pady=5, sticky='nsew')
|
|
|
|
health_items = [
|
|
('radar_failed', 'Radar'),
|
|
('array_failed', 'Array'),
|
|
('transmitter_failed', 'Transmitter'),
|
|
('receiver_failed', 'Receiver'),
|
|
('processor_failed', 'Processor'),
|
|
('tx_overtemp', 'TX Overtemp'),
|
|
]
|
|
|
|
for i, (key, label) in enumerate(health_items):
|
|
ttk.Label(right_col, text=f"{label}:").grid(row=i, column=0, sticky='e', padx=5)
|
|
self.dashboard_labels[key] = ttk.Label(right_col, text="---", width=8)
|
|
self.dashboard_labels[key].grid(row=i, column=1, sticky='w', padx=5)
|
|
|
|
# Bottom - Scan Settings
|
|
bottom_col = ttk.LabelFrame(dashboard, text="Scan Parameters", padding=10)
|
|
bottom_col.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky='nsew')
|
|
|
|
scan_items = [
|
|
('range_scale', 'Range Scale'),
|
|
('bar_scan', 'Bars'),
|
|
('azimuth_scan', 'Az Scan'),
|
|
]
|
|
|
|
for i, (key, label) in enumerate(scan_items):
|
|
ttk.Label(bottom_col, text=f"{label}:").grid(row=0, column=i*2, sticky='e', padx=5)
|
|
self.dashboard_labels[key] = ttk.Label(bottom_col, text="---", width=10)
|
|
self.dashboard_labels[key].grid(row=0, column=i*2+1, sticky='w', padx=5)
|
|
|
|
dashboard.columnconfigure(0, weight=1)
|
|
dashboard.columnconfigure(1, weight=1)
|
|
|
|
def _create_command_tab(self, parent):
|
|
"""Create the radar command control tab (A2 message editor)"""
|
|
from ..lib1553.constants import RadarMode, StandbyStatus, RangeScale
|
|
|
|
# Main container with padding
|
|
main_container = ttk.Frame(parent, padding=20)
|
|
main_container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Title
|
|
title = ttk.Label(
|
|
main_container,
|
|
text="Radar Operation Command (Message A2)",
|
|
font=('Helvetica', 14, 'bold')
|
|
)
|
|
title.grid(row=0, column=0, columnspan=2, pady=(0, 20), sticky='w')
|
|
|
|
# Control frame
|
|
control_frame = ttk.LabelFrame(main_container, text="Command Parameters", padding=15)
|
|
control_frame.grid(row=1, column=0, columnspan=2, sticky='nsew', pady=10)
|
|
|
|
# 1. Master Mode
|
|
row = 0
|
|
ttk.Label(control_frame, text="Master Mode:", font=('Helvetica', 10, 'bold')).grid(
|
|
row=row, column=0, sticky='w', pady=5
|
|
)
|
|
self.cmd_master_mode = ttk.Combobox(control_frame, width=20, state='readonly')
|
|
self.cmd_master_mode.grid(row=row, column=1, sticky='w', pady=5, padx=(10, 0))
|
|
self.cmd_master_mode['values'] = [
|
|
'RWS (Range While Search)',
|
|
'VS (Velocity Search)',
|
|
'ACM (Air Combat Mode)',
|
|
'TWS (Track While Scan)',
|
|
'GM (Ground Mapping)',
|
|
'SEA_LOW',
|
|
'SEA_HIGH',
|
|
'GMTI (Ground Moving Target)'
|
|
]
|
|
self.cmd_master_mode.current(0) # Default: RWS
|
|
|
|
# 2. Standby Status
|
|
row += 1
|
|
ttk.Label(control_frame, text="Standby:", font=('Helvetica', 10, 'bold')).grid(
|
|
row=row, column=0, sticky='w', pady=5
|
|
)
|
|
self.cmd_standby = ttk.Combobox(control_frame, width=20, state='readonly')
|
|
self.cmd_standby.grid(row=row, column=1, sticky='w', pady=5, padx=(10, 0))
|
|
self.cmd_standby['values'] = ['OFF (Operational)', 'ON (Standby)']
|
|
self.cmd_standby.current(0) # Default: OFF
|
|
|
|
# 3. Range Scale
|
|
row += 1
|
|
ttk.Label(control_frame, text="Range Scale:", font=('Helvetica', 10, 'bold')).grid(
|
|
row=row, column=0, sticky='w', pady=5
|
|
)
|
|
self.cmd_range_scale = ttk.Combobox(control_frame, width=20, state='readonly')
|
|
self.cmd_range_scale.grid(row=row, column=1, sticky='w', pady=5, padx=(10, 0))
|
|
self.cmd_range_scale['values'] = ['80 NM', '40 NM', '20 NM', '10 NM']
|
|
self.cmd_range_scale.current(0) # Default: 80 NM
|
|
|
|
# 4. Silence (Reserved)
|
|
row += 1
|
|
ttk.Label(control_frame, text="Silence (Reserved):", font=('Helvetica', 10, 'bold')).grid(
|
|
row=row, column=0, sticky='w', pady=5
|
|
)
|
|
self.cmd_silence = ttk.Combobox(control_frame, width=20, state='readonly')
|
|
self.cmd_silence.grid(row=row, column=1, sticky='w', pady=5, padx=(10, 0))
|
|
self.cmd_silence['values'] = ['OFF (Normal)', 'ON (Silent)']
|
|
self.cmd_silence.current(0) # Default: OFF
|
|
|
|
# Apply button
|
|
row += 1
|
|
self.btn_apply_command = ttk.Button(
|
|
control_frame,
|
|
text="▶ Apply Command to Radar",
|
|
command=self._on_apply_command,
|
|
style='Accent.TButton'
|
|
)
|
|
self.btn_apply_command.grid(row=row, column=0, columnspan=2, pady=(20, 5), sticky='ew')
|
|
|
|
# Current values display
|
|
current_frame = ttk.LabelFrame(main_container, text="Current Transmitted Values", padding=15)
|
|
current_frame.grid(row=2, column=0, columnspan=2, sticky='nsew', pady=10)
|
|
|
|
self.cmd_current_display = tk.Text(
|
|
current_frame,
|
|
height=8,
|
|
width=50,
|
|
font=('Courier', 9),
|
|
state='disabled',
|
|
background='#F0F0F0'
|
|
)
|
|
self.cmd_current_display.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Initialize display
|
|
self._update_command_display()
|
|
|
|
# Configure grid weights
|
|
main_container.rowconfigure(1, weight=1)
|
|
main_container.rowconfigure(2, weight=1)
|
|
main_container.columnconfigure(0, weight=1)
|
|
|
|
def _create_raw_tab(self, parent):
|
|
"""Create the raw data view tab"""
|
|
# Text widget for raw hex data
|
|
self.raw_text = tk.Text(parent, font=('Courier', 10), wrap=tk.WORD)
|
|
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.raw_text.yview)
|
|
self.raw_text.configure(yscrollcommand=scrollbar.set)
|
|
|
|
self.raw_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
self.raw_text.insert('1.0', "Select a message to view raw data...")
|
|
self.raw_text.configure(state='disabled')
|
|
|
|
def _create_status_bar(self):
|
|
"""Create the status bar at the bottom"""
|
|
status_frame = ttk.Frame(self.root)
|
|
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
# Connection status
|
|
self.status_connection = ttk.Label(status_frame, text="● Disconnected", foreground='red')
|
|
self.status_connection.pack(side=tk.LEFT, padx=10)
|
|
|
|
# TX/RX counters
|
|
self.status_tx = ttk.Label(status_frame, text="TX: 0")
|
|
self.status_tx.pack(side=tk.LEFT, padx=10)
|
|
|
|
self.status_rx = ttk.Label(status_frame, text="RX: 0")
|
|
self.status_rx.pack(side=tk.LEFT, padx=10)
|
|
|
|
# Error display
|
|
self.status_error = ttk.Label(status_frame, text="", foreground='red')
|
|
self.status_error.pack(side=tk.RIGHT, padx=10)
|
|
|
|
# Separator
|
|
ttk.Separator(self.root, orient=tk.HORIZONTAL).pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
def _on_message_select(self, event):
|
|
"""Handle message selection in the tree view"""
|
|
selection = self.msg_tree.selection()
|
|
if selection:
|
|
msg_name = selection[0]
|
|
self.selected_message = msg_name
|
|
self._update_detail_view(msg_name)
|
|
self._update_raw_view(msg_name)
|
|
|
|
def _update_detail_view(self, msg_name: str):
|
|
"""Update the detail view for the selected message"""
|
|
stats = self.message_stats.get(msg_name)
|
|
if not stats:
|
|
return
|
|
|
|
# Update header
|
|
direction = "RT → BC" if stats.is_transmit else "BC → RT"
|
|
self.detail_header.config(text=f"{msg_name} - {direction}")
|
|
|
|
# Se non abbiamo ancora dati per questo messaggio
|
|
if not stats.last_data:
|
|
self._show_no_data_message()
|
|
return
|
|
|
|
# Se stiamo cambiando messaggio, dobbiamo ricostruire il layout
|
|
if self._current_detail_message != msg_name:
|
|
self._build_detail_table(msg_name, stats.last_data)
|
|
self._current_detail_message = msg_name
|
|
|
|
# Aggiorna solo i valori (veloce, senza ricostruire layout)
|
|
self._update_detail_values(msg_name, stats.last_data)
|
|
|
|
def _show_no_data_message(self):
|
|
"""Mostra messaggio quando non ci sono dati"""
|
|
# Clear existing
|
|
for widget in self.detail_scroll_frame.winfo_children():
|
|
widget.destroy()
|
|
self.detail_field_labels.clear()
|
|
self._current_detail_message = None
|
|
|
|
no_data = ttk.Label(self.detail_scroll_frame,
|
|
text="No data received yet for this message",
|
|
font=('Helvetica', 10, 'italic'))
|
|
no_data.pack(pady=20)
|
|
|
|
def _build_detail_table(self, msg_name: str, msg_obj):
|
|
"""Costruisce la tabella dei dettagli (chiamato solo alla prima selezione del messaggio)"""
|
|
# Clear existing widgets
|
|
for widget in self.detail_scroll_frame.winfo_children():
|
|
widget.destroy()
|
|
self.detail_field_labels.clear()
|
|
|
|
cls = msg_obj.__class__
|
|
|
|
# Get all Field descriptors
|
|
fields = []
|
|
for name, obj in cls.__dict__.items():
|
|
if isinstance(obj, Field):
|
|
fields.append((name, obj))
|
|
|
|
# Sort by word_index then start_bit
|
|
fields.sort(key=lambda x: (x[1].word_index, x[1].start_bit))
|
|
|
|
# Cache per questo messaggio
|
|
widget_cache = {}
|
|
|
|
# Crea header tabella
|
|
header_frame = ttk.Frame(self.detail_scroll_frame)
|
|
header_frame.pack(fill=tk.X, padx=10, pady=5)
|
|
|
|
ttk.Label(header_frame, text="Field Name", width=25, anchor='w',
|
|
font=('Helvetica', 9, 'bold')).grid(row=0, column=0, padx=2)
|
|
ttk.Label(header_frame, text="Raw Value", width=12, anchor='center',
|
|
font=('Helvetica', 9, 'bold')).grid(row=0, column=1, padx=2)
|
|
ttk.Label(header_frame, text="Decoded Value", width=25, anchor='w',
|
|
font=('Helvetica', 9, 'bold')).grid(row=0, column=2, padx=2)
|
|
|
|
ttk.Separator(self.detail_scroll_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=2)
|
|
|
|
# Create field rows
|
|
current_word = -1
|
|
for field_name, field_desc in fields:
|
|
# Add word separator
|
|
if field_desc.word_index != current_word:
|
|
current_word = field_desc.word_index
|
|
|
|
sep = ttk.Separator(self.detail_scroll_frame, orient=tk.HORIZONTAL)
|
|
sep.pack(fill=tk.X, pady=3)
|
|
|
|
word_label = ttk.Label(self.detail_scroll_frame,
|
|
text=f"Word {current_word:02d}",
|
|
font=('Helvetica', 9, 'bold'),
|
|
background='#e0e0e0')
|
|
word_label.pack(fill=tk.X, padx=5, pady=2)
|
|
|
|
# Field row con tabella
|
|
row_frame = ttk.Frame(self.detail_scroll_frame)
|
|
row_frame.pack(fill=tk.X, padx=10, pady=1)
|
|
|
|
# Nome campo (fisso)
|
|
name_label = ttk.Label(row_frame, text=f"{field_name}",
|
|
width=25, anchor='w',
|
|
font=('Courier', 9))
|
|
name_label.grid(row=0, column=0, padx=2, sticky='w')
|
|
|
|
# Valore raw (aggiornabile)
|
|
raw_label = ttk.Label(row_frame, text="---",
|
|
width=12, anchor='center',
|
|
font=('Courier', 9, 'bold'),
|
|
foreground='#0066cc')
|
|
raw_label.grid(row=0, column=1, padx=2)
|
|
|
|
# Valore decodificato (aggiornabile)
|
|
decoded_label = ttk.Label(row_frame, text="---",
|
|
width=25, anchor='w',
|
|
font=('Courier', 9))
|
|
decoded_label.grid(row=0, column=2, padx=2, sticky='w')
|
|
|
|
# Salva riferimenti per aggiornamenti futuri
|
|
widget_cache[field_name] = (raw_label, decoded_label, field_desc)
|
|
|
|
# Salva cache per questo messaggio
|
|
self._detail_widgets_cache[msg_name] = widget_cache
|
|
|
|
def _update_detail_values(self, msg_name: str, msg_obj):
|
|
"""Aggiorna solo i valori nella tabella (veloce, senza ricostruire layout)"""
|
|
widget_cache = self._detail_widgets_cache.get(msg_name)
|
|
if not widget_cache:
|
|
return
|
|
|
|
for field_name, (raw_label, decoded_label, field_desc) in widget_cache.items():
|
|
try:
|
|
# Ottieni valore raw dal campo
|
|
raw_value = field_desc._get_raw_value(msg_obj)
|
|
|
|
# Ottieni valore interpretato
|
|
value = getattr(msg_obj, field_name)
|
|
|
|
# Formatta valore raw (hex per bit fields)
|
|
if field_desc.width <= 8:
|
|
raw_str = f"0x{raw_value:0{(field_desc.width+3)//4}X}" # Hex
|
|
else:
|
|
raw_str = f"{raw_value}" # Decimale per campi grandi
|
|
|
|
# Formatta valore decodificato
|
|
if isinstance(value, float):
|
|
decoded_str = f"{value:.4f}"
|
|
elif hasattr(value, 'name'): # Enum
|
|
decoded_str = f"{value.name} ({value.value})"
|
|
else:
|
|
decoded_str = str(value)
|
|
|
|
# Aggiorna labels (VELOCE - no widget creation)
|
|
raw_label.config(text=raw_str)
|
|
decoded_label.config(text=decoded_str)
|
|
|
|
except Exception as e:
|
|
raw_label.config(text="ERR")
|
|
decoded_label.config(text=f"<{str(e)[:20]}>")
|
|
|
|
def _update_raw_view(self, msg_name: str):
|
|
"""Update the raw data view for the selected message"""
|
|
stats = self.message_stats.get(msg_name)
|
|
if not stats:
|
|
return
|
|
|
|
self.raw_text.configure(state='normal')
|
|
self.raw_text.delete('1.0', tk.END)
|
|
|
|
text = f"Message: {msg_name}\n"
|
|
text += f"Command Word: {stats.cw}\n"
|
|
text += f"Status Word: {stats.sw}\n"
|
|
text += f"Word Count: {stats.word_count}\n"
|
|
text += f"Message Count: {stats.count}\n"
|
|
text += f"Errors: {stats.errors}\n\n"
|
|
|
|
if stats.raw_words:
|
|
text += "Raw Data Words (hex):\n"
|
|
text += "-" * 40 + "\n"
|
|
for i, word in enumerate(stats.raw_words):
|
|
text += f" Word {i:02d}: 0x{word:04X} ({word:5d})\n"
|
|
else:
|
|
text += "No raw data available.\n"
|
|
|
|
self.raw_text.insert('1.0', text)
|
|
self.raw_text.configure(state='disabled')
|
|
|
|
def _update_dashboard(self, b6_data, b7_data):
|
|
"""Update the radar dashboard with B6/B7 data"""
|
|
if b7_data:
|
|
# Mode
|
|
mode = getattr(b7_data, 'master_mode_tb', None)
|
|
if mode:
|
|
self.dashboard_labels['mode'].config(
|
|
text=mode.name if hasattr(mode, 'name') else str(mode)
|
|
)
|
|
|
|
# Standby
|
|
stby = getattr(b7_data, 'standby_status', None)
|
|
if stby is not None:
|
|
text = stby.name if hasattr(stby, 'name') else str(stby)
|
|
color = 'orange' if 'ON' in text.upper() else 'green'
|
|
self.dashboard_labels['standby'].config(text=text, foreground=color)
|
|
|
|
# RF Radiation
|
|
rf = getattr(b7_data, 'rf_radiation_status', None)
|
|
if rf is not None:
|
|
text = rf.name if hasattr(rf, 'name') else str(rf)
|
|
color = 'green' if 'ON' in text.upper() else 'gray'
|
|
self.dashboard_labels['rf'].config(text=text, foreground=color)
|
|
|
|
# Transition
|
|
trans = getattr(b7_data, 'transition_status', None)
|
|
if trans is not None:
|
|
text = trans.name if hasattr(trans, 'name') else str(trans)
|
|
self.dashboard_labels['transition'].config(text=text)
|
|
|
|
# Scan parameters
|
|
range_s = getattr(b7_data, 'range_scale_tb', None)
|
|
if range_s:
|
|
self.dashboard_labels['range_scale'].config(
|
|
text=range_s.name if hasattr(range_s, 'name') else str(range_s)
|
|
)
|
|
|
|
bar_s = getattr(b7_data, 'bar_scan_tb', None)
|
|
if bar_s:
|
|
self.dashboard_labels['bar_scan'].config(
|
|
text=bar_s.name if hasattr(bar_s, 'name') else str(bar_s)
|
|
)
|
|
|
|
az_s = getattr(b7_data, 'azimuth_scan_tb', None)
|
|
if az_s:
|
|
self.dashboard_labels['azimuth_scan'].config(
|
|
text=az_s.name if hasattr(az_s, 'name') else str(az_s)
|
|
)
|
|
|
|
if b6_data:
|
|
# Health status from B6
|
|
health_fields = [
|
|
'radar_failed', 'array_failed', 'transmitter_failed',
|
|
'receiver_failed', 'processor_failed', 'tx_overtemp'
|
|
]
|
|
|
|
for field in health_fields:
|
|
value = getattr(b6_data, field, None)
|
|
if value is not None:
|
|
if value == 0:
|
|
self.dashboard_labels[field].config(text="OK", foreground='green')
|
|
else:
|
|
self.dashboard_labels[field].config(text="FAIL", foreground='red')
|
|
|
|
def _start_gui_update_timer(self):
|
|
"""Avvia il timer periodico per elaborare gli aggiornamenti dalla queue"""
|
|
self._process_update_queue()
|
|
|
|
def _process_update_queue(self):
|
|
"""Elabora tutti gli aggiornamenti in coda in batch (rate limiting)"""
|
|
# Processa fino a 50 aggiornamenti per ciclo per evitare blocchi
|
|
updates_processed = 0
|
|
max_updates_per_cycle = 50
|
|
|
|
# Processa aggiornamenti messaggi
|
|
while updates_processed < max_updates_per_cycle:
|
|
try:
|
|
msg_name, msg_obj, raw_words = self._update_queue.get_nowait()
|
|
self._update_message_stats_internal(msg_name, msg_obj, raw_words)
|
|
updates_processed += 1
|
|
except queue.Empty:
|
|
break
|
|
|
|
# Processa aggiornamenti status (prendi solo l'ultimo)
|
|
last_status = None
|
|
while True:
|
|
try:
|
|
last_status = self._status_queue.get_nowait()
|
|
except queue.Empty:
|
|
break
|
|
|
|
if last_status:
|
|
connected, tx, rx, error = last_status
|
|
self._update_connection_status_internal(connected, tx, rx, error)
|
|
|
|
# Riprogramma il prossimo ciclo
|
|
self._update_id = self.root.after(self._gui_update_interval_ms, self._process_update_queue)
|
|
|
|
def queue_message_update(self, msg_name: str, msg_obj, raw_words: List[int] = None):
|
|
"""Accoda un aggiornamento messaggio (chiamato da thread esterni)"""
|
|
try:
|
|
# Non bloccare se la coda è piena, scarta l'aggiornamento
|
|
self._update_queue.put_nowait((msg_name, msg_obj, raw_words))
|
|
except queue.Full:
|
|
# Coda piena, scarta aggiornamento (evita blocco)
|
|
pass
|
|
|
|
def queue_status_update(self, connected: bool, tx: int, rx: int, error: str = ""):
|
|
"""Accoda un aggiornamento status (chiamato da thread esterni)"""
|
|
try:
|
|
self._status_queue.put_nowait((connected, tx, rx, error))
|
|
except queue.Full:
|
|
# Svuota e metti l'ultimo
|
|
try:
|
|
self._status_queue.get_nowait()
|
|
self._status_queue.put_nowait((connected, tx, rx, error))
|
|
except:
|
|
pass
|
|
|
|
def _update_message_stats_internal(self, msg_name: str, msg_obj, raw_words: List[int] = None):
|
|
"""Aggiorna statistiche messaggio (chiamato solo dal thread GUI)"""
|
|
if msg_name not in self.message_stats:
|
|
return
|
|
|
|
stats = self.message_stats[msg_name]
|
|
now = time.time()
|
|
|
|
# Calculate period
|
|
if stats.last_update > 0:
|
|
stats.period_ms = (now - stats.last_update) * 1000
|
|
|
|
stats.last_update = now
|
|
stats.count += 1
|
|
stats.last_data = msg_obj
|
|
|
|
if raw_words:
|
|
stats.raw_words = raw_words
|
|
|
|
# Update tree view
|
|
period_str = f"{stats.period_ms:.1f}" if stats.period_ms > 0 else "-"
|
|
self.msg_tree.item(msg_name, values=(
|
|
msg_name,
|
|
stats.cw,
|
|
stats.sw,
|
|
stats.count,
|
|
stats.errors,
|
|
period_str,
|
|
stats.word_count
|
|
))
|
|
|
|
# Update detail view if this message is selected
|
|
if self.selected_message == msg_name:
|
|
self._update_detail_view(msg_name)
|
|
self._update_raw_view(msg_name)
|
|
|
|
# Update dashboard for B6/B7
|
|
if msg_name == 'B6':
|
|
self._update_dashboard(msg_obj, None)
|
|
elif msg_name == 'B7':
|
|
self._update_dashboard(None, msg_obj)
|
|
|
|
def update_message_stats(self, msg_name: str, msg_obj, raw_words: List[int] = None):
|
|
"""Update statistics for a received message (metodo pubblico deprecato, usa queue_message_update)"""
|
|
# Backward compatibility: accoda invece di aggiornare direttamente
|
|
self.queue_message_update(msg_name, msg_obj, raw_words)
|
|
|
|
def _update_connection_status_internal(self, connected: bool, tx: int, rx: int, error: str):
|
|
"""Aggiorna status bar (chiamato solo dal thread GUI)"""
|
|
self.is_connected = connected
|
|
self.tx_count = tx
|
|
self.rx_count = rx
|
|
self.last_error = error
|
|
|
|
if connected:
|
|
self.status_connection.config(text="● Connected", foreground='green')
|
|
else:
|
|
self.status_connection.config(text="○ Disconnected", foreground='red')
|
|
|
|
self.status_tx.config(text=f"TX: {tx}")
|
|
self.status_rx.config(text=f"RX: {rx}")
|
|
|
|
if error:
|
|
self.status_error.config(text=f"Error: {error}", foreground='red')
|
|
else:
|
|
self.status_error.config(text="")
|
|
|
|
def update_connection_status(self, connected: bool, tx: int = 0, rx: int = 0, error: str = ""):
|
|
"""Update the status bar (metodo pubblico, usa queue)"""
|
|
self.queue_status_update(connected, tx, rx, error)
|
|
|
|
def _on_run(self):
|
|
"""Handle RUN button - start connection and apply defaults"""
|
|
logging.getLogger(__name__).info("RUN button pressed - starting system...")
|
|
|
|
# Disable RUN, enable STOP
|
|
self.btn_run.config(state=tk.DISABLED)
|
|
self.btn_stop.config(state=tk.NORMAL)
|
|
self.lbl_status.config(text="● Starting...", foreground='orange')
|
|
|
|
# Call the external connect handler (wired in __main__.py)
|
|
if hasattr(self, '_on_connect') and callable(self._on_connect):
|
|
self._on_connect()
|
|
|
|
self.lbl_status.config(text="● Running", foreground='green')
|
|
|
|
def _on_stop(self):
|
|
"""Handle STOP button - disconnect"""
|
|
logging.getLogger(__name__).info("STOP button pressed - stopping system...")
|
|
|
|
# Disable STOP, enable RUN
|
|
self.btn_stop.config(state=tk.DISABLED)
|
|
self.btn_run.config(state=tk.NORMAL)
|
|
self.lbl_status.config(text="● Stopping...", foreground='orange')
|
|
|
|
# Call the external disconnect handler (wired in __main__.py)
|
|
if hasattr(self, '_on_disconnect') and callable(self._on_disconnect):
|
|
self._on_disconnect()
|
|
|
|
self.lbl_status.config(text="● Stopped - Press RUN to restart", foreground='red')
|
|
|
|
def _on_connect(self):
|
|
"""Handle connect menu action (deprecated - use RUN button)"""
|
|
self._on_run()
|
|
|
|
def _on_disconnect(self):
|
|
"""Handle disconnect menu action (deprecated - use STOP button)"""
|
|
self._on_stop()
|
|
|
|
def _on_apply_command(self):
|
|
"""Handle Apply Command button - modify A2 message fields"""
|
|
if not self.radar_controller:
|
|
logging.getLogger(__name__).warning("Radar controller not available")
|
|
return
|
|
|
|
try:
|
|
# Import enums
|
|
from ..lib1553.constants import RadarMode, RangeScale
|
|
|
|
# Get selections from Comboboxes
|
|
mode_str = self.cmd_master_mode.get()
|
|
standby_str = self.cmd_standby.get()
|
|
range_str = self.cmd_range_scale.get()
|
|
silence_str = self.cmd_silence.get()
|
|
|
|
# Map to enum values
|
|
mode_map = {m.name: m for m in RadarMode}
|
|
master_mode = mode_map.get(mode_str, RadarMode.RWS)
|
|
|
|
standby_cmd = 1 if standby_str == "ON" else 0
|
|
|
|
range_map = {
|
|
"80 NM": RangeScale.NM_80,
|
|
"40 NM": RangeScale.NM_40,
|
|
"20 NM": RangeScale.NM_20,
|
|
"10 NM": RangeScale.NM_10
|
|
}
|
|
range_scale = range_map.get(range_str, RangeScale.NM_80)
|
|
|
|
silence_reserved = 1 if silence_str == "ON" else 0
|
|
|
|
# Update A2 message fields
|
|
self.radar_controller.msg_a2.master_mode = master_mode
|
|
self.radar_controller.msg_a2.standby_cmd = standby_cmd
|
|
self.radar_controller.msg_a2.range_scale = range_scale
|
|
self.radar_controller.msg_a2.silence_reserved = silence_reserved
|
|
|
|
# Update display
|
|
self._update_command_display()
|
|
|
|
# Log action
|
|
logging.getLogger(__name__).info(
|
|
f"Command applied: Mode={master_mode.name}, "
|
|
f"Standby={standby_cmd}, Range={range_str}, Silence={silence_reserved}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.getLogger(__name__).error(f"Error applying command: {e}")
|
|
|
|
def _update_command_display(self):
|
|
"""Update command display with current A2 message state"""
|
|
if not self.radar_controller:
|
|
return
|
|
|
|
try:
|
|
msg = self.radar_controller.msg_a2
|
|
|
|
# Build display text
|
|
lines = [
|
|
"=== Current Radar Command State ===",
|
|
"",
|
|
f"Master Mode: {msg.master_mode.name} ({msg.master_mode.value})",
|
|
f"Standby Command: {'ON' if msg.standby_cmd else 'OFF'} ({msg.standby_cmd})",
|
|
f"Range Scale: {msg.range_scale.name} ({msg.range_scale.value})",
|
|
f"Silence (Rsv): {'ON' if msg.silence_reserved else 'OFF'} ({msg.silence_reserved})",
|
|
"",
|
|
"(Values transmitted in A2 message at 25Hz)"
|
|
]
|
|
|
|
display_text = "\n".join(lines)
|
|
|
|
# Update Text widget
|
|
self.cmd_current_display.config(state='normal')
|
|
self.cmd_current_display.delete('1.0', 'end')
|
|
self.cmd_current_display.insert('1.0', display_text)
|
|
self.cmd_current_display.config(state='disabled')
|
|
|
|
except Exception as e:
|
|
logging.getLogger(__name__).error(f"Error updating command display: {e}")
|
|
|
|
def _reset_counters(self):
|
|
"""Reset all message counters"""
|
|
for name, stats in self.message_stats.items():
|
|
stats.count = 0
|
|
stats.errors = 0
|
|
stats.period_ms = 0.0
|
|
stats.last_update = 0.0
|
|
|
|
self.msg_tree.item(name, values=(
|
|
name, stats.cw, stats.sw, 0, 0, "-", stats.word_count
|
|
))
|
|
|
|
def _show_about(self):
|
|
"""Show about dialog"""
|
|
from tkinter import messagebox
|
|
messagebox.showinfo(
|
|
"About PyBusMonitor1553",
|
|
"PyBusMonitor1553\n\n"
|
|
"MIL-STD-1553 Bus Monitor\n"
|
|
"Version 1.0\n\n"
|
|
"Python implementation for GRIFO-F radar interface"
|
|
)
|
|
|
|
def shutdown(self):
|
|
"""Clean shutdown of GUI and logging system"""
|
|
logging.getLogger(__name__).info("Shutting down GUI...")
|
|
if self.logger_system:
|
|
self.logger_system.shutdown()
|
|
|
|
|
|
def run_gui():
|
|
"""Launch the GUI application"""
|
|
root = tk.Tk()
|
|
app = BusMonitorApp(root)
|
|
|
|
# Register shutdown handler
|
|
def on_closing():
|
|
app.shutdown()
|
|
root.destroy()
|
|
|
|
root.protocol("WM_DELETE_WINDOW", on_closing)
|
|
|
|
return root, app
|