Files
PLFM_RADAR/9_Firmware/9_3_GUI/GUI_V6_Demo.py
T
2026-03-10 01:43:19 +00:00

1543 lines
60 KiB
Python

"""
Radar System GUI - Demo Version
Compatible with FT601 USB 3.0 interface
Includes simulated radar data for demonstration
"""
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import queue
import time
import struct
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle, Circle
import matplotlib.animation as animation
import logging
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional
import math
import random
from datetime import datetime
import json
import os
# Configure logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# ============================================================================
# DATA CLASSES
# ============================================================================
@dataclass
class RadarTarget:
"""Represents a detected radar target"""
id: int
range: float # meters
velocity: float # m/s
azimuth: float # degrees
elevation: float # degrees
snr: float # dB
timestamp: float
track_id: int = -1
range_bin: int = 0
doppler_bin: int = 0
@dataclass
class RadarSettings:
"""Radar system settings"""
system_frequency: float = 10e9 # Hz
chirp_duration_long: float = 30e-6 # seconds
chirp_duration_short: float = 0.5e-6 # seconds
chirps_per_frame: int = 32
range_bins: int = 1024
doppler_bins: int = 32
prf: float = 1000 # Hz
max_range: float = 5000 # meters
max_velocity: float = 100 # m/s
cfar_threshold: float = 13.0 # dB
beam_width: float = 3.0 # degrees
@dataclass
class SystemStatus:
"""System status information"""
uptime: float = 0
temperature: float = 25.0
fpga_utilization: float = 0
usb_data_rate: float = 0
packets_received: int = 0
packets_lost: int = 0
errors: int = 0
warnings: List[str] = None
# ============================================================================
# SIMULATED RADAR DATA GENERATOR
# ============================================================================
class RadarDataSimulator:
"""Generates simulated radar data for demo mode"""
def __init__(self):
self.settings = RadarSettings()
self.targets = []
self.frame_count = 0
self.create_test_scenario()
def create_test_scenario(self):
"""Create a test scenario with moving targets"""
# Target 1: Fast-moving aircraft
self.targets.append({
'id': 1,
'range': 2500,
'velocity': -80, # Approaching
'azimuth': 45,
'elevation': 5,
'snr': 25,
'range_drift': -0.5, # m per frame
'azimuth_drift': 0.1, # deg per frame
'velocity_drift': 0
})
# Target 2: Slow-moving vehicle
self.targets.append({
'id': 2,
'range': 800,
'velocity': 15, # Receding
'azimuth': -30,
'elevation': 0,
'snr': 18,
'range_drift': 0.2,
'azimuth_drift': -0.05,
'velocity_drift': 0.1
})
# Target 3: Stationary object
self.targets.append({
'id': 3,
'range': 1500,
'velocity': 0,
'azimuth': 10,
'elevation': 2,
'snr': 22,
'range_drift': 0,
'azimuth_drift': 0,
'velocity_drift': 0
})
# Target 4: Helicopter (low speed, high SNR)
self.targets.append({
'id': 4,
'range': 1200,
'velocity': -5,
'azimuth': 75,
'elevation': 15,
'snr': 30,
'range_drift': -0.1,
'azimuth_drift': -0.2,
'velocity_drift': 0.05
})
# Target 5: Drone (small, fluctuating)
self.targets.append({
'id': 5,
'range': 300,
'velocity': 10,
'azimuth': -60,
'elevation': 8,
'snr': 12,
'range_drift': 0.3,
'azimuth_drift': 0.4,
'velocity_drift': -0.2
})
def generate_range_doppler_map(self):
"""Generate simulated range-Doppler map"""
map_data = np.zeros((self.settings.range_bins, self.settings.doppler_bins))
# Add noise floor
noise_floor = 10 * np.random.random(map_data.shape)
map_data += noise_floor
# Add targets
for target in self.targets:
# Update target position
target['range'] += target['range_drift']
target['azimuth'] += target['azimuth_drift']
target['velocity'] += target['velocity_drift']
# Keep within bounds
target['range'] = max(100, min(5000, target['range']))
target['azimuth'] = max(-90, min(90, target['azimuth']))
target['velocity'] = max(-100, min(100, target['velocity']))
# Convert to bin indices
range_bin = int((target['range'] / self.settings.max_range) *
(self.settings.range_bins - 1))
doppler_bin = int(((target['velocity'] + self.settings.max_velocity) /
(2 * self.settings.max_velocity)) *
(self.settings.doppler_bins - 1))
# Ensure valid indices
range_bin = max(0, min(self.settings.range_bins - 1, range_bin))
doppler_bin = max(0, min(self.settings.doppler_bins - 1, doppler_bin))
# Add target peak with spreading
for r in range(max(0, range_bin-2), min(self.settings.range_bins, range_bin+3)):
for d in range(max(0, doppler_bin-2), min(self.settings.doppler_bins, doppler_bin+3)):
distance = np.sqrt((r - range_bin)**2 + (d - doppler_bin)**2)
if distance < 3:
amplitude = target['snr'] * np.exp(-distance/2)
map_data[r, d] += amplitude * (0.8 + 0.4 * np.random.random())
# Add some clutter
clutter_bins = np.random.randint(0, self.settings.range_bins, 20)
for cb in clutter_bins:
map_data[cb, :] += 5 * np.random.random(self.settings.doppler_bins)
self.frame_count += 1
return map_data
def get_detected_targets(self):
"""Get list of detected targets"""
detected = []
for i, target in enumerate(self.targets):
# Random detection probability based on SNR
if np.random.random() < (target['snr'] / 40):
detected.append(RadarTarget(
id=target['id'],
range=target['range'],
velocity=target['velocity'],
azimuth=target['azimuth'],
elevation=target['elevation'],
snr=target['snr'] + 2 * np.random.random() - 1,
timestamp=time.time(),
track_id=target['id'],
range_bin=int((target['range'] / self.settings.max_range) *
(self.settings.range_bins - 1)),
doppler_bin=int(((target['velocity'] + self.settings.max_velocity) /
(2 * self.settings.max_velocity)) *
(self.settings.doppler_bins - 1))
))
return detected
# ============================================================================
# SIMULATED USB INTERFACE
# ============================================================================
class SimulatedUSBInterface:
"""Simulates FT601 USB interface for demo mode"""
def __init__(self):
self.is_open = False
self.data_rate = 0
self.packet_count = 0
self.byte_count = 0
self.start_time = time.time()
def list_devices(self):
"""Return simulated devices"""
return [
{'description': 'FT601 USB 3.0 Device (Demo Mode)',
'vid': 0x0403, 'pid': 0x6030},
{'description': 'FT601 USB 3.0 Device - Channel 1',
'vid': 0x0403, 'pid': 0x6030}
]
def open_device(self, device_info):
"""Simulate opening device"""
self.is_open = True
self.start_time = time.time()
logger.info(f"Demo mode: Opened simulated FT601 device")
return True
def close(self):
"""Simulate closing device"""
self.is_open = False
logger.info("Demo mode: Closed simulated FT601 device")
def read_data(self, size=4096):
"""Simulate reading data (returns None in demo mode)"""
# In demo mode, we generate data separately
return None
def write_data(self, data):
"""Simulate writing data"""
self.packet_count += 1
self.byte_count += len(data)
elapsed = time.time() - self.start_time
if elapsed > 0:
self.data_rate = self.byte_count / elapsed / 1024 # KB/s
return True
def get_stats(self):
"""Get interface statistics"""
return {
'packets': self.packet_count,
'bytes': self.byte_count,
'data_rate': self.data_rate
}
# ============================================================================
# MAIN GUI APPLICATION
# ============================================================================
class RadarGUI:
def __init__(self, root):
self.root = root
self.root.title("Advanced Radar System GUI - Demo Mode")
self.root.geometry("1600x900")
# Set icon if available
try:
self.root.iconbitmap(default='radar.ico')
except:
pass
# Configure dark theme
self.setup_dark_theme()
# Initialize components
self.settings = RadarSettings()
self.status = SystemStatus()
self.simulator = RadarDataSimulator()
self.usb_interface = SimulatedUSBInterface()
# Data queues
self.radar_data_queue = queue.Queue(maxsize=100)
self.command_queue = queue.Queue()
# Control flags
self.demo_mode = tk.BooleanVar(value=True)
self.running = False
self.recording = False
self.auto_scan = tk.BooleanVar(value=True)
self.show_grid = tk.BooleanVar(value=True)
self.show_targets = tk.BooleanVar(value=True)
self.color_map = tk.StringVar(value='hot')
# Data storage
self.range_doppler_map = np.zeros((1024, 32))
self.detected_targets = []
self.target_history = []
self.recorded_frames = []
# Animation
self.animation_running = True
self.frame_count = 0
self.fps = 0
self.last_frame_time = time.time()
# Create GUI
self.create_menu()
self.create_gui()
self.create_status_bar()
# Start background threads
self.start_background_threads()
# Start animation
self.animate()
# Handle window close
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
logger.info("Radar GUI initialized in demo mode")
def setup_dark_theme(self):
"""Configure dark theme for the GUI"""
self.style = ttk.Style()
self.style.theme_use('clam')
# Dark theme colors
dark_bg = '#2b2b2b'
dark_fg = '#e0e0e0'
dark_select = '#404040'
dark_button = '#3c3f41'
# Configure styles
self.style.configure('.',
background=dark_bg,
foreground=dark_fg,
fieldbackground=dark_bg,
troughcolor=dark_select)
self.style.configure('TLabel', background=dark_bg, foreground=dark_fg)
self.style.configure('TFrame', background=dark_bg)
self.style.configure('TLabelframe', background=dark_bg, foreground=dark_fg)
self.style.configure('TLabelframe.Label', background=dark_bg, foreground=dark_fg)
self.style.configure('TButton',
background=dark_button,
foreground=dark_fg,
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#4e5254'),
('pressed', '#2d2f31')])
self.style.configure('TNotebook', background=dark_bg)
self.style.configure('TNotebook.Tab',
background=dark_select,
foreground=dark_fg,
padding=[10, 2])
self.style.map('TNotebook.Tab',
background=[('selected', dark_button)])
self.style.configure('Treeview',
background=dark_select,
foreground=dark_fg,
fieldbackground=dark_select)
self.style.map('Treeview',
background=[('selected', '#4e5254')])
self.style.configure('Horizontal.TScale', background=dark_bg)
self.style.configure('Vertical.TScale', background=dark_bg)
def create_menu(self):
"""Create application menu"""
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 to Hardware", command=self.connect_hardware)
file_menu.add_command(label="Demo Mode", command=self.enable_demo_mode)
file_menu.add_separator()
file_menu.add_command(label="Start Recording", command=self.start_recording)
file_menu.add_command(label="Stop Recording", command=self.stop_recording)
file_menu.add_separator()
file_menu.add_command(label="Load Configuration", command=self.load_config)
file_menu.add_command(label="Save Configuration", command=self.save_config)
file_menu.add_separator()
file_menu.add_command(label="Export Data", command=self.export_data)
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self.on_closing)
# View menu
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="View", menu=view_menu)
view_menu.add_checkbutton(label="Show Grid", variable=self.show_grid)
view_menu.add_checkbutton(label="Show Targets", variable=self.show_targets)
view_menu.add_checkbutton(label="Auto Scan", variable=self.auto_scan)
view_menu.add_separator()
# Color map submenu
color_menu = tk.Menu(view_menu, tearoff=0)
view_menu.add_cascade(label="Color Map", menu=color_menu)
for cmap in ['hot', 'jet', 'viridis', 'plasma', 'inferno', 'magma']:
color_menu.add_radiobutton(label=cmap.capitalize(),
variable=self.color_map,
value=cmap)
# Tools menu
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tools", menu=tools_menu)
tools_menu.add_command(label="Calibration Wizard", command=self.calibration_wizard)
tools_menu.add_command(label="Beam Pattern Analysis", command=self.beam_analysis)
tools_menu.add_command(label="Noise Floor Measurement", command=self.noise_measurement)
tools_menu.add_separator()
tools_menu.add_command(label="Diagnostics", command=self.show_diagnostics)
# Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="Documentation", command=self.show_docs)
help_menu.add_command(label="About", command=self.show_about)
def create_gui(self):
"""Create main GUI layout"""
# Main container
main_frame = ttk.Frame(self.root)
main_frame.pack(fill='both', expand=True, padx=5, pady=5)
# Control panel (top)
control_frame = ttk.LabelFrame(main_frame, text="System Control", padding=5)
control_frame.pack(fill='x', pady=(0, 5))
self.create_control_panel(control_frame)
# Notebook for tabs
self.notebook = ttk.Notebook(main_frame)
self.notebook.pack(fill='both', expand=True)
# Create tabs
self.create_radar_tab()
self.create_scope_tab()
self.create_targets_tab()
self.create_spectrum_tab()
self.create_history_tab()
self.create_settings_tab()
def create_control_panel(self, parent):
"""Create system control panel"""
# Left side - Mode and connection
left_frame = ttk.Frame(parent)
left_frame.pack(side='left', fill='x', expand=True)
ttk.Label(left_frame, text="Mode:").grid(row=0, column=0, padx=5, pady=2, sticky='w')
mode_combo = ttk.Combobox(left_frame, textvariable=tk.StringVar(value="Demo Mode"),
values=["Demo Mode", "Hardware Mode"],
state="readonly", width=15)
mode_combo.grid(row=0, column=1, padx=5, pady=2, sticky='w')
ttk.Label(left_frame, text="Device:").grid(row=0, column=2, padx=5, pady=2, sticky='w')
self.device_combo = ttk.Combobox(left_frame,
values=["FT601 Demo Device"],
state="readonly", width=25)
self.device_combo.grid(row=0, column=3, padx=5, pady=2, sticky='w')
self.device_combo.current(0)
ttk.Button(left_frame, text="Refresh",
command=self.refresh_devices).grid(row=0, column=4, padx=2, pady=2)
ttk.Button(left_frame, text="Connect",
command=self.connect_device).grid(row=0, column=5, padx=2, pady=2)
# Right side - Start/Stop
right_frame = ttk.Frame(parent)
right_frame.pack(side='right', padx=10)
self.start_button = ttk.Button(right_frame, text="▶ Start",
command=self.start_radar, width=10)
self.start_button.pack(side='left', padx=2)
self.stop_button = ttk.Button(right_frame, text="■ Stop",
command=self.stop_radar, width=10,
state='disabled')
self.stop_button.pack(side='left', padx=2)
ttk.Button(right_frame, text="⚙ Settings",
command=lambda: self.notebook.select(5)).pack(side='left', padx=2)
def create_radar_tab(self):
"""Create radar display tab"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Radar Display")
# Main display area
display_frame = ttk.Frame(tab)
display_frame.pack(fill='both', expand=True, padx=5, pady=5)
# Range-Doppler map
map_frame = ttk.LabelFrame(display_frame, text="Range-Doppler Map", padding=5)
map_frame.pack(side='left', fill='both', expand=True)
# Create matplotlib figure
self.rd_fig = Figure(figsize=(8, 6), facecolor='#2b2b2b')
self.rd_ax = self.rd_fig.add_subplot(111)
self.rd_ax.set_facecolor('#1a1a1a')
# Initialize plot
self.rd_img = self.rd_ax.imshow(
np.random.rand(1024, 32),
aspect='auto',
cmap=self.color_map.get(),
extent=[-self.settings.max_velocity,
self.settings.max_velocity,
self.settings.max_range, 0],
interpolation='bilinear'
)
self.rd_ax.set_xlabel('Velocity (m/s)', color='white')
self.rd_ax.set_ylabel('Range (m)', color='white')
self.rd_ax.tick_params(colors='white')
# Add colorbar
self.rd_cbar = self.rd_fig.colorbar(self.rd_img, ax=self.rd_ax)
self.rd_cbar.ax.yaxis.set_tick_params(color='white')
self.rd_cbar.ax.set_ylabel('Power (dB)', color='white')
plt.setp(plt.getp(self.rd_cbar.ax.axes, 'yticklabels'), color='white')
# Embed in tkinter
self.rd_canvas = FigureCanvasTkAgg(self.rd_fig, map_frame)
self.rd_canvas.draw()
self.rd_canvas.get_tk_widget().pack(fill='both', expand=True)
# Toolbar
toolbar = NavigationToolbar2Tk(self.rd_canvas, map_frame)
toolbar.update()
# Target list panel
target_frame = ttk.LabelFrame(display_frame, text="Detected Targets", padding=5, width=300)
target_frame.pack(side='right', fill='y', padx=(5, 0))
target_frame.pack_propagate(False)
# Treeview for targets
columns = ('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR')
self.target_tree = ttk.Treeview(target_frame, columns=columns, show='headings', height=20)
# Define headings
self.target_tree.heading('ID', text='ID')
self.target_tree.heading('Range', text='Range (m)')
self.target_tree.heading('Velocity', text='Vel (m/s)')
self.target_tree.heading('Azimuth', text='Az (°)')
self.target_tree.heading('Elevation', text='El (°)')
self.target_tree.heading('SNR', text='SNR (dB)')
# Set column widths
self.target_tree.column('ID', width=40)
self.target_tree.column('Range', width=70)
self.target_tree.column('Velocity', width=70)
self.target_tree.column('Azimuth', width=60)
self.target_tree.column('Elevation', width=60)
self.target_tree.column('SNR', width=60)
# Add scrollbar
scrollbar = ttk.Scrollbar(target_frame, orient='vertical',
command=self.target_tree.yview)
self.target_tree.configure(yscrollcommand=scrollbar.set)
self.target_tree.pack(side='left', fill='both', expand=True)
scrollbar.pack(side='right', fill='y')
def create_scope_tab(self):
"""Create A-scope tab (range profile)"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="A-Scope")
# Create figure
self.scope_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b')
self.scope_ax = self.scope_fig.add_subplot(111)
self.scope_ax.set_facecolor('#1a1a1a')
# Initialize plot
self.scope_line, = self.scope_ax.plot([], [], 'g-', linewidth=1)
self.scope_ax.set_xlim(0, self.settings.max_range)
self.scope_ax.set_ylim(0, 100)
self.scope_ax.set_xlabel('Range (m)', color='white')
self.scope_ax.set_ylabel('Amplitude (dB)', color='white')
self.scope_ax.set_title('Range Profile (A-Scope)', color='white')
self.scope_ax.grid(True, alpha=0.3)
self.scope_ax.tick_params(colors='white')
self.scope_canvas = FigureCanvasTkAgg(self.scope_fig, tab)
self.scope_canvas.draw()
self.scope_canvas.get_tk_widget().pack(fill='both', expand=True)
# Control panel
control_frame = ttk.Frame(tab)
control_frame.pack(fill='x', pady=5)
ttk.Label(control_frame, text="Azimuth:").pack(side='left', padx=5)
self.scope_azimuth = ttk.Scale(control_frame, from_=-90, to=90,
orient='horizontal', length=200)
self.scope_azimuth.pack(side='left', padx=5)
self.scope_azimuth.set(0)
ttk.Label(control_frame, text="Elevation:").pack(side='left', padx=5)
self.scope_elevation = ttk.Scale(control_frame, from_=-45, to=45,
orient='horizontal', length=200)
self.scope_elevation.pack(side='left', padx=5)
self.scope_elevation.set(0)
def create_targets_tab(self):
"""Create PPI scope tab (plan position indicator)"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="PPI Scope")
# Create figure
self.ppi_fig = Figure(figsize=(8, 8), facecolor='#2b2b2b')
self.ppi_ax = self.ppi_fig.add_subplot(111, projection='polar')
self.ppi_ax.set_facecolor('#1a1a1a')
# Initialize plot
self.ppi_scatter = self.ppi_ax.scatter([], [], c=[], s=50, alpha=0.8, cmap='hot')
self.ppi_ax.set_ylim(0, self.settings.max_range)
self.ppi_ax.grid(True, alpha=0.3)
self.ppi_ax.tick_params(colors='white')
# Convert azimuth to radar convention (0° = North, clockwise)
self.ppi_ax.set_theta_zero_location('N')
self.ppi_ax.set_theta_direction(-1)
self.ppi_canvas = FigureCanvasTkAgg(self.ppi_fig, tab)
self.ppi_canvas.draw()
self.ppi_canvas.get_tk_widget().pack(fill='both', expand=True)
def create_spectrum_tab(self):
"""Create Doppler spectrum tab"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Doppler Spectrum")
# Create figure
self.spec_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b')
self.spec_ax = self.spec_fig.add_subplot(111)
self.spec_ax.set_facecolor('#1a1a1a')
# Initialize plot
self.spec_line, = self.spec_ax.plot([], [], 'b-', linewidth=1)
self.spec_ax.set_xlim(-self.settings.max_velocity, self.settings.max_velocity)
self.spec_ax.set_ylim(0, 100)
self.spec_ax.set_xlabel('Velocity (m/s)', color='white')
self.spec_ax.set_ylabel('Power (dB)', color='white')
self.spec_ax.set_title('Doppler Spectrum', color='white')
self.spec_ax.grid(True, alpha=0.3)
self.spec_ax.tick_params(colors='white')
self.spec_canvas = FigureCanvasTkAgg(self.spec_fig, tab)
self.spec_canvas.draw()
self.spec_canvas.get_tk_widget().pack(fill='both', expand=True)
# Range selector
range_frame = ttk.Frame(tab)
range_frame.pack(fill='x', pady=5)
ttk.Label(range_frame, text="Range Bin:").pack(side='left', padx=5)
self.spec_range = ttk.Scale(range_frame, from_=0, to=1023,
orient='horizontal', length=400)
self.spec_range.pack(side='left', padx=5)
self.spec_range.set(512)
ttk.Label(range_frame, text="512").pack(side='left', padx=5)
def create_history_tab(self):
"""Create target history tab"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Target History")
# Create figure
self.hist_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b')
# Range history
self.hist_ax1 = self.hist_fig.add_subplot(211)
self.hist_ax1.set_facecolor('#1a1a1a')
self.hist_ax1.set_xlabel('Time (s)', color='white')
self.hist_ax1.set_ylabel('Range (m)', color='white')
self.hist_ax1.set_title('Target Range History', color='white')
self.hist_ax1.grid(True, alpha=0.3)
self.hist_ax1.tick_params(colors='white')
# Velocity history
self.hist_ax2 = self.hist_fig.add_subplot(212)
self.hist_ax2.set_facecolor('#1a1a1a')
self.hist_ax2.set_xlabel('Time (s)', color='white')
self.hist_ax2.set_ylabel('Velocity (m/s)', color='white')
self.hist_ax2.set_title('Target Velocity History', color='white')
self.hist_ax2.grid(True, alpha=0.3)
self.hist_ax2.tick_params(colors='white')
self.hist_canvas = FigureCanvasTkAgg(self.hist_fig, tab)
self.hist_canvas.draw()
self.hist_canvas.get_tk_widget().pack(fill='both', expand=True)
# Clear button
ttk.Button(tab, text="Clear History",
command=self.clear_history).pack(pady=5)
def create_settings_tab(self):
"""Create settings tab"""
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Settings")
# Create notebook for settings categories
settings_notebook = ttk.Notebook(tab)
settings_notebook.pack(fill='both', expand=True, padx=5, pady=5)
# Radar settings
radar_frame = ttk.Frame(settings_notebook)
settings_notebook.add(radar_frame, text="Radar")
self.create_radar_settings(radar_frame)
# Display settings
display_frame = ttk.Frame(settings_notebook)
settings_notebook.add(display_frame, text="Display")
self.create_display_settings(display_frame)
# Detection settings
detection_frame = ttk.Frame(settings_notebook)
settings_notebook.add(detection_frame, text="Detection")
self.create_detection_settings(detection_frame)
# Recording settings
record_frame = ttk.Frame(settings_notebook)
settings_notebook.add(record_frame, text="Recording")
self.create_recording_settings(record_frame)
# System settings
system_frame = ttk.Frame(settings_notebook)
settings_notebook.add(system_frame, text="System")
self.create_system_settings(system_frame)
def create_radar_settings(self, parent):
"""Create radar settings controls"""
# Create scrollable frame
canvas = tk.Canvas(parent, bg='#2b2b2b', highlightthickness=0)
scrollbar = ttk.Scrollbar(parent, orient='vertical', command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# Settings variables
self.settings_vars = {}
settings = [
('System Frequency (GHz):', 'freq', 10.0, 1.0, 20.0),
('Long Chirp Duration (µs):', 'long_dur', 30.0, 1.0, 100.0),
('Short Chirp Duration (µs):', 'short_dur', 0.5, 0.1, 10.0),
('Chirps per Frame:', 'chirps', 32, 1, 128),
('Range Bins:', 'range_bins', 1024, 64, 2048),
('Doppler Bins:', 'doppler_bins', 32, 8, 128),
('PRF (Hz):', 'prf', 1000, 100, 10000),
('Max Range (m):', 'max_range', 5000, 100, 50000),
('Max Velocity (m/s):', 'max_vel', 100, 10, 500),
('Beam Width (°):', 'beam_width', 3.0, 0.5, 10.0)
]
for i, (label, key, default, minv, maxv) in enumerate(settings):
frame = ttk.Frame(scrollable_frame)
frame.pack(fill='x', padx=10, pady=5)
ttk.Label(frame, text=label, width=25).pack(side='left')
var = tk.DoubleVar(value=default)
self.settings_vars[key] = var
entry = ttk.Entry(frame, textvariable=var, width=15)
entry.pack(side='left', padx=5)
ttk.Label(frame, text=f"({minv}-{maxv})").pack(side='left')
canvas.pack(side='left', fill='both', expand=True)
scrollbar.pack(side='right', fill='y')
# Apply button
ttk.Button(scrollable_frame, text="Apply Settings",
command=self.apply_settings).pack(pady=10)
def create_display_settings(self, parent):
"""Create display settings controls"""
frame = ttk.Frame(parent)
frame.pack(fill='both', expand=True, padx=20, pady=20)
# Update rate
ttk.Label(frame, text="Update Rate (Hz):").grid(row=0, column=0,
sticky='w', pady=5)
self.update_rate = ttk.Scale(frame, from_=1, to=60,
orient='horizontal', length=200)
self.update_rate.set(20)
self.update_rate.grid(row=0, column=1, padx=10, pady=5)
ttk.Label(frame, text="20").grid(row=0, column=2, sticky='w')
# Color map
ttk.Label(frame, text="Color Map:").grid(row=1, column=0,
sticky='w', pady=5)
cmap_combo = ttk.Combobox(frame, textvariable=self.color_map,
values=['hot', 'jet', 'viridis', 'plasma'],
state='readonly', width=15)
cmap_combo.grid(row=1, column=1, padx=10, pady=5, sticky='w')
# Grid
ttk.Checkbutton(frame, text="Show Grid",
variable=self.show_grid).grid(row=2, column=0,
columnspan=2, sticky='w', pady=5)
# Targets
ttk.Checkbutton(frame, text="Show Targets",
variable=self.show_targets).grid(row=3, column=0,
columnspan=2, sticky='w', pady=5)
# Auto scan
ttk.Checkbutton(frame, text="Auto Scan",
variable=self.auto_scan).grid(row=4, column=0,
columnspan=2, sticky='w', pady=5)
# Background color
ttk.Label(frame, text="Background:").grid(row=5, column=0,
sticky='w', pady=5)
bg_colors = ['Dark', 'Black', 'White']
bg_combo = ttk.Combobox(frame, values=bg_colors, state='readonly', width=15)
bg_combo.set('Dark')
bg_combo.grid(row=5, column=1, padx=10, pady=5, sticky='w')
def create_detection_settings(self, parent):
"""Create detection settings controls"""
frame = ttk.Frame(parent)
frame.pack(fill='both', expand=True, padx=20, pady=20)
# CFAR threshold
ttk.Label(frame, text="CFAR Threshold (dB):").grid(row=0, column=0,
sticky='w', pady=5)
self.cfar_threshold = ttk.Scale(frame, from_=5, to=30,
orient='horizontal', length=200)
self.cfar_threshold.set(13)
self.cfar_threshold.grid(row=0, column=1, padx=10, pady=5)
ttk.Label(frame, text="13.0").grid(row=0, column=2, sticky='w')
# Detection sensitivity
ttk.Label(frame, text="Sensitivity:").grid(row=1, column=0,
sticky='w', pady=5)
self.sensitivity = ttk.Scale(frame, from_=0, to=100,
orient='horizontal', length=200)
self.sensitivity.set(75)
self.sensitivity.grid(row=1, column=1, padx=10, pady=5)
ttk.Label(frame, text="75%").grid(row=1, column=2, sticky='w')
# Min SNR
ttk.Label(frame, text="Min SNR (dB):").grid(row=2, column=0,
sticky='w', pady=5)
self.min_snr = ttk.Scale(frame, from_=0, to=20,
orient='horizontal', length=200)
self.min_snr.set(10)
self.min_snr.grid(row=2, column=1, padx=10, pady=5)
ttk.Label(frame, text="10.0").grid(row=2, column=2, sticky='w')
# Detection mode
ttk.Label(frame, text="Detection Mode:").grid(row=3, column=0,
sticky='w', pady=5)
mode_combo = ttk.Combobox(frame,
values=['CFAR', 'Threshold', 'Peak'],
state='readonly', width=15)
mode_combo.set('CFAR')
mode_combo.grid(row=3, column=1, padx=10, pady=5, sticky='w')
def create_recording_settings(self, parent):
"""Create recording settings controls"""
frame = ttk.Frame(parent)
frame.pack(fill='both', expand=True, padx=20, pady=20)
# Recording directory
ttk.Label(frame, text="Save Directory:").grid(row=0, column=0,
sticky='w', pady=5)
self.record_dir = tk.StringVar(value="./recordings")
ttk.Entry(frame, textvariable=self.record_dir, width=40).grid(
row=0, column=1, padx=10, pady=5, columnspan=2)
# Browse button
ttk.Button(frame, text="Browse",
command=self.browse_directory).grid(row=0, column=3, padx=5)
# File format
ttk.Label(frame, text="File Format:").grid(row=1, column=0,
sticky='w', pady=5)
format_combo = ttk.Combobox(frame,
values=['HDF5', 'NPZ', 'CSV', 'MAT'],
state='readonly', width=15)
format_combo.set('HDF5')
format_combo.grid(row=1, column=1, padx=10, pady=5, sticky='w')
# Auto naming
ttk.Checkbutton(frame, text="Auto-generate filenames",
variable=tk.BooleanVar(value=True)).grid(
row=2, column=0, columnspan=2, sticky='w', pady=5)
# Max file size
ttk.Label(frame, text="Max File Size (MB):").grid(row=3, column=0,
sticky='w', pady=5)
ttk.Entry(frame, width=15).grid(row=3, column=1, padx=10, pady=5, sticky='w')
def create_system_settings(self, parent):
"""Create system settings controls"""
frame = ttk.Frame(parent)
frame.pack(fill='both', expand=True, padx=20, pady=20)
# System info
info_frame = ttk.LabelFrame(frame, text="System Information", padding=10)
info_frame.pack(fill='x', pady=10)
ttk.Label(info_frame, text="FPGA Temperature:").grid(row=0, column=0, sticky='w', pady=2)
ttk.Label(info_frame, text="25.5 °C").grid(row=0, column=1, padx=20, sticky='w')
ttk.Label(info_frame, text="FPGA Utilization:").grid(row=1, column=0, sticky='w', pady=2)
ttk.Label(info_frame, text="45%").grid(row=1, column=1, padx=20, sticky='w')
ttk.Label(info_frame, text="USB Data Rate:").grid(row=2, column=0, sticky='w', pady=2)
ttk.Label(info_frame, text="0 KB/s").grid(row=2, column=1, padx=20, sticky='w')
ttk.Label(info_frame, text="Uptime:").grid(row=3, column=0, sticky='w', pady=2)
ttk.Label(info_frame, text="00:00:00").grid(row=3, column=1, padx=20, sticky='w')
# Control buttons
button_frame = ttk.Frame(frame)
button_frame.pack(fill='x', pady=10)
ttk.Button(button_frame, text="System Diagnostics",
command=self.show_diagnostics).pack(side='left', padx=5)
ttk.Button(button_frame, text="Reset System",
command=self.reset_system).pack(side='left', padx=5)
ttk.Button(button_frame, text="Factory Reset",
command=self.factory_reset).pack(side='left', padx=5)
def create_status_bar(self):
"""Create status bar at bottom of window"""
status_frame = ttk.Frame(self.root)
status_frame.pack(side='bottom', fill='x')
# Left side status
self.status_label = ttk.Label(status_frame, text="Status: Demo Mode",
relief='sunken', padding=2)
self.status_label.pack(side='left', fill='x', expand=True)
# Right side indicators
self.fps_label = ttk.Label(status_frame, text="FPS: 0",
relief='sunken', width=10)
self.fps_label.pack(side='right', padx=1)
self.data_label = ttk.Label(status_frame, text="Data: 0 KB/s",
relief='sunken', width=12)
self.data_label.pack(side='right', padx=1)
self.targets_label = ttk.Label(status_frame, text="Targets: 0",
relief='sunken', width=10)
self.targets_label.pack(side='right', padx=1)
self.time_label = ttk.Label(status_frame, text=time.strftime("%H:%M:%S"),
relief='sunken', width=8)
self.time_label.pack(side='right', padx=1)
# ============================================================================
# GUI UPDATE METHODS
# ============================================================================
def animate(self):
"""Animation loop for updating plots"""
if not self.animation_running:
return
try:
# Update FPS counter
current_time = time.time()
dt = current_time - self.last_frame_time
if dt > 0:
self.fps = 0.9 * self.fps + 0.1 / dt
self.last_frame_time = current_time
# Update displays if running
if self.running:
self.update_radar_display()
self.update_target_list()
self.update_scopes()
self.frame_count += 1
# Update status bar
self.update_status_bar()
except Exception as e:
logger.error(f"Animation error: {e}")
# Schedule next update
update_ms = int(1000 / max(1, self.update_rate.get()))
self.root.after(update_ms, self.animate)
def update_radar_display(self):
"""Update radar display with new data"""
try:
# Generate or get new radar data
if self.demo_mode.get():
# Generate simulated data
rd_map = self.simulator.generate_range_doppler_map()
targets = self.simulator.get_detected_targets()
else:
# Get data from queue
try:
data = self.radar_data_queue.get_nowait()
rd_map = data['map']
targets = data['targets']
except queue.Empty:
return
# Update range-Doppler map
self.range_doppler_map = rd_map
log_map = 10 * np.log10(rd_map + 1)
# Update image
self.rd_img.set_data(log_map)
self.rd_img.set_cmap(self.color_map.get())
# Update colorbar limits
vmin = np.percentile(log_map, 5)
vmax = np.percentile(log_map, 95)
self.rd_img.set_clim(vmin, vmax)
# Draw targets if enabled
if self.show_targets.get():
# Clear previous target markers
for artist in self.rd_ax.lines + self.rd_ax.texts:
if hasattr(artist, 'is_target_marker') and artist.is_target_marker:
artist.remove()
# Add new target markers
for target in targets:
x = target.velocity
y = target.range
self.rd_ax.plot(x, y, 'wo', markersize=8,
markeredgecolor='red', markeredgewidth=2,
is_target_marker=True)
self.rd_ax.text(x, y-100, str(target.id), color='white',
ha='center', va='top', fontsize=8,
is_target_marker=True)
# Update detected targets list
self.detected_targets = targets
# Add to history
self.target_history.append({
'time': current_time,
'targets': targets
})
# Limit history size
if len(self.target_history) > 1000:
self.target_history = self.target_history[-1000:]
# Update canvas
self.rd_canvas.draw_idle()
except Exception as e:
logger.error(f"Error updating radar display: {e}")
def update_target_list(self):
"""Update the targets treeview"""
# Clear existing items
for item in self.target_tree.get_children():
self.target_tree.delete(item)
# Add new targets
for target in self.detected_targets:
values = (
target.id,
f"{target.range:.1f}",
f"{target.velocity:.1f}",
f"{target.azimuth:.1f}",
f"{target.elevation:.1f}",
f"{target.snr:.1f}"
)
self.target_tree.insert('', 'end', values=values)
def update_scopes(self):
"""Update A-scope and spectrum displays"""
if not self.detected_targets:
return
try:
# A-scope (range profile)
range_profile = np.mean(self.range_doppler_map, axis=1)
range_axis = np.linspace(0, self.settings.max_range, len(range_profile))
self.scope_line.set_data(range_axis, 10 * np.log10(range_profile + 1))
self.scope_ax.relim()
self.scope_ax.autoscale_view(scalex=False)
self.scope_canvas.draw_idle()
# Doppler spectrum at selected range
range_bin = int(self.spec_range.get())
if range_bin < self.range_doppler_map.shape[0]:
spectrum = self.range_doppler_map[range_bin, :]
vel_axis = np.linspace(-self.settings.max_velocity,
self.settings.max_velocity,
len(spectrum))
self.spec_line.set_data(vel_axis, 10 * np.log10(spectrum + 1))
self.spec_ax.relim()
self.spec_ax.autoscale_view(scalex=False)
self.spec_canvas.draw_idle()
# PPI scope
if self.detected_targets:
theta = [np.radians(90 - t.azimuth) for t in self.detected_targets]
r = [t.range for t in self.detected_targets]
colors = [t.snr for t in self.detected_targets]
self.ppi_scatter.set_offsets(np.c_[theta, r])
self.ppi_scatter.set_array(np.array(colors))
self.ppi_scatter.set_clim(0, 40)
self.ppi_canvas.draw_idle()
except Exception as e:
logger.error(f"Error updating scopes: {e}")
def update_status_bar(self):
"""Update status bar information"""
# Time
self.time_label.config(text=time.strftime("%H:%M:%S"))
# Targets count
self.targets_label.config(text=f"Targets: {len(self.detected_targets)}")
# Data rate
if self.usb_interface.is_open:
stats = self.usb_interface.get_stats()
data_rate = stats['data_rate']
self.data_label.config(text=f"Data: {data_rate:.1f} KB/s")
else:
self.data_label.config(text="Data: 0 KB/s")
# FPS
self.fps_label.config(text=f"FPS: {self.fps:.1f}")
# Status text
if self.running:
status = f"Status: {'Recording' if self.recording else 'Running'} - Frame {self.frame_count}"
else:
status = "Status: Demo Mode - Stopped"
if self.recording:
status += f" - Recording to {self.record_dir.get()}"
self.status_label.config(text=status)
# ============================================================================
# BACKGROUND THREADS
# ============================================================================
def start_background_threads(self):
"""Start background processing threads"""
self.running = False
self.background_thread = threading.Thread(target=self.background_worker,
daemon=True)
self.background_thread.start()
def background_worker(self):
"""Background worker thread for data processing"""
while True:
if self.running and not self.demo_mode.get():
# Hardware mode - read from USB
try:
data = self.usb_interface.read_data(4096)
if data:
# Parse and process data
processed = self.process_usb_data(data)
if processed:
self.radar_data_queue.put(processed, timeout=1)
except Exception as e:
logger.error(f"Background worker error: {e}")
else:
# Demo mode - generate data at lower rate
time.sleep(0.05) # 20 Hz
def process_usb_data(self, data):
"""Process raw USB data into radar frames"""
# This would parse actual USB packets
# For demo, return None
return None
# ============================================================================
# COMMAND HANDLERS
# ============================================================================
def refresh_devices(self):
"""Refresh device list"""
devices = self.usb_interface.list_devices()
device_names = [d['description'] for d in devices]
self.device_combo['values'] = device_names
if device_names:
self.device_combo.current(0)
logger.info(f"Found {len(devices)} devices")
def connect_device(self):
"""Connect to selected device"""
if self.demo_mode.get():
device_name = self.device_combo.get()
if self.usb_interface.open_device({'description': device_name}):
messagebox.showinfo("Success", f"Connected to {device_name} in demo mode")
self.status_label.config(text=f"Status: Connected - {device_name}")
else:
messagebox.showerror("Error", "Failed to connect to device")
else:
messagebox.showinfo("Info", "Hardware mode not available in demo version")
def connect_hardware(self):
"""Switch to hardware mode"""
self.demo_mode.set(False)
messagebox.showinfo("Info", "Hardware mode requires actual FT601 device")
def enable_demo_mode(self):
"""Enable demo mode"""
self.demo_mode.set(True)
self.status_label.config(text="Status: Demo Mode")
def start_radar(self):
"""Start radar operation"""
self.running = True
self.start_button.config(state='disabled')
self.stop_button.config(state='normal')
self.status_label.config(text="Status: Running")
logger.info("Radar started")
def stop_radar(self):
"""Stop radar operation"""
self.running = False
self.start_button.config(state='normal')
self.stop_button.config(state='disabled')
self.status_label.config(text="Status: Stopped")
logger.info("Radar stopped")
def start_recording(self):
"""Start recording data"""
if not self.running:
messagebox.showwarning("Warning", "Start radar first")
return
self.recording = True
logger.info(f"Recording started to {self.record_dir.get()}")
def stop_recording(self):
"""Stop recording data"""
self.recording = False
logger.info("Recording stopped")
def apply_settings(self):
"""Apply radar settings"""
try:
self.settings.system_frequency = self.settings_vars['freq'].get() * 1e9
self.settings.chirp_duration_long = self.settings_vars['long_dur'].get() * 1e-6
self.settings.chirp_duration_short = self.settings_vars['short_dur'].get() * 1e-6
self.settings.chirps_per_frame = int(self.settings_vars['chirps'].get())
self.settings.range_bins = int(self.settings_vars['range_bins'].get())
self.settings.doppler_bins = int(self.settings_vars['doppler_bins'].get())
self.settings.prf = self.settings_vars['prf'].get()
self.settings.max_range = self.settings_vars['max_range'].get()
self.settings.max_velocity = self.settings_vars['max_vel'].get()
messagebox.showinfo("Success", "Settings applied")
logger.info("Settings updated")
except Exception as e:
messagebox.showerror("Error", f"Invalid settings: {e}")
def load_config(self):
"""Load configuration from file"""
from tkinter import filedialog
filename = filedialog.askopenfilename(
title="Load Configuration",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if filename:
try:
with open(filename, 'r') as f:
config = json.load(f)
# Apply config...
messagebox.showinfo("Success", f"Loaded configuration from {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to load: {e}")
def save_config(self):
"""Save configuration to file"""
from tkinter import filedialog
filename = filedialog.asksaveasfilename(
title="Save Configuration",
defaultextension=".json",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
)
if filename:
try:
config = {
'settings': {k: v.get() for k, v in self.settings_vars.items()},
'display': {
'color_map': self.color_map.get(),
'show_grid': self.show_grid.get(),
'show_targets': self.show_targets.get(),
'auto_scan': self.auto_scan.get()
}
}
with open(filename, 'w') as f:
json.dump(config, f, indent=2)
messagebox.showinfo("Success", f"Saved configuration to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to save: {e}")
def export_data(self):
"""Export radar data"""
from tkinter import filedialog
filename = filedialog.asksaveasfilename(
title="Export Data",
defaultextension=".npz",
filetypes=[("NumPy files", "*.npz"), ("All files", "*.*")]
)
if filename:
try:
np.savez(filename,
map=self.range_doppler_map,
targets=[(t.range, t.velocity, t.azimuth, t.snr)
for t in self.detected_targets])
messagebox.showinfo("Success", f"Exported data to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to export: {e}")
def clear_history(self):
"""Clear target history"""
self.target_history = []
self.hist_ax1.clear()
self.hist_ax2.clear()
self.hist_canvas.draw()
def browse_directory(self):
"""Browse for directory"""
from tkinter import filedialog
directory = filedialog.askdirectory(title="Select Recording Directory")
if directory:
self.record_dir.set(directory)
def calibration_wizard(self):
"""Open calibration wizard"""
messagebox.showinfo("Calibration", "Calibration wizard not available in demo mode")
def beam_analysis(self):
"""Open beam pattern analysis"""
messagebox.showinfo("Beam Analysis", "Beam analysis not available in demo mode")
def noise_measurement(self):
"""Measure noise floor"""
messagebox.showinfo("Noise Measurement", "Noise measurement not available in demo mode")
def show_diagnostics(self):
"""Show system diagnostics"""
import platform
info = f"""
System Diagnostics
=================
Platform: {platform.platform()}
Python: {platform.python_version()}
Radar System
------------
Mode: {'Demo' if self.demo_mode.get() else 'Hardware'}
Status: {'Running' if self.running else 'Stopped'}
Frames: {self.frame_count}
Targets: {len(self.detected_targets)}
USB Interface
-------------
Connected: {self.usb_interface.is_open}
Packets: {self.usb_interface.packet_count}
Bytes: {self.usb_interface.byte_count}
Data Rate: {self.usb_interface.data_rate:.1f} KB/s
Display
-------
FPS: {self.fps:.1f}
Update Rate: {self.update_rate.get()} Hz
Color Map: {self.color_map.get()}
"""
messagebox.showinfo("Diagnostics", info)
def show_docs(self):
"""Show documentation"""
messagebox.showinfo("Documentation",
"Radar System GUI Documentation\n\n"
"Version 1.0\n\n"
"For more information, visit:\n"
"https://github.com/radar-system/docs")
def show_about(self):
"""Show about dialog"""
about_text = """
Advanced Radar System GUI
Version 1.0.0
A comprehensive radar control and visualization
interface with FT601 USB 3.0 support.
Features:
• Real-time Range-Doppler display
• Target detection and tracking
• A-scope and PPI displays
• Doppler spectrum analysis
• Data recording and playback
© 2025 Radar Systems Inc.
"""
messagebox.showinfo("About", about_text)
def reset_system(self):
"""Reset system"""
if messagebox.askyesno("Confirm Reset", "Reset all system parameters?"):
self.frame_count = 0
self.detected_targets = []
self.target_history = []
self.status.uptime = 0
logger.info("System reset")
def factory_reset(self):
"""Factory reset"""
if messagebox.askyesno("Confirm Factory Reset",
"This will reset ALL settings to defaults. Continue?"):
# Reset to defaults
for key, var in self.settings_vars.items():
var.set(self.get_default_value(key))
self.color_map.set('hot')
self.show_grid.set(True)
self.show_targets.set(True)
self.auto_scan.set(True)
logger.info("Factory reset performed")
def get_default_value(self, key):
"""Get default value for settings key"""
defaults = {
'freq': 10.0,
'long_dur': 30.0,
'short_dur': 0.5,
'chirps': 32,
'range_bins': 1024,
'doppler_bins': 32,
'prf': 1000,
'max_range': 5000,
'max_vel': 100,
'beam_width': 3.0
}
return defaults.get(key, 0)
def on_closing(self):
"""Handle window closing"""
if messagebox.askokcancel("Quit", "Do you want to quit?"):
self.animation_running = False
self.running = False
self.usb_interface.close()
self.root.destroy()
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
def main():
"""Main application entry point"""
try:
# Create root window
root = tk.Tk()
# Set application icon if available
try:
root.iconbitmap(default='radar.ico')
except:
pass
# Create application
app = RadarGUI(root)
# Center window on screen
root.update_idletasks()
width = root.winfo_width()
height = root.winfo_height()
x = (root.winfo_screenwidth() // 2) - (width // 2)
y = (root.winfo_screenheight() // 2) - (height // 2)
root.geometry(f'{width}x{height}+{x}+{y}')
# Start main loop
root.mainloop()
except Exception as e:
logger.error(f"Fatal error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start:\n{e}")
if __name__ == "__main__":
main()