57de32b172
Fixes 25 remaining manual lint errors after auto-fix pass (94 auto-fixed earlier): - GUI_V6.py: noqa on availability imports, bare except, unused vars, F811 redefs - GUI_V6_Demo.py: unused app variable - v7/models.py: noqa F401 on 8 try/except availability-check imports - FPGA cosim: unused header/status/span vars, ambiguous 'l' renamed to 'line', E701 while-on-one-line split, F841 padding vars annotated Also adds v7/ module, GUI_PyQt_Map.py, and GUI_V7_PyQt.py to version control. Expands CI lint job to cover all 21 maintained Python files (was 4). All 58 Python tests pass. Zero ruff errors on all target files.
1219 lines
47 KiB
Python
1219 lines
47 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Radar System GUI - Fully Functional Demo Version
|
|
All buttons work, simulated radar data is generated in real-time
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
import time
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
from matplotlib.figure import Figure
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import List, Dict
|
|
import random
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# 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:
|
|
id: int
|
|
range: float
|
|
velocity: float
|
|
azimuth: float
|
|
elevation: float
|
|
snr: float
|
|
|
|
@dataclass
|
|
class RadarSettings:
|
|
frequency: float = 10.0 # GHz
|
|
long_chirp_us: float = 30.0
|
|
short_chirp_us: float = 0.5
|
|
chirps_per_frame: int = 32
|
|
range_bins: int = 1024
|
|
doppler_bins: int = 32
|
|
prf: float = 1000
|
|
max_range: float = 5000
|
|
max_velocity: float = 100
|
|
cfar_threshold: float = 13.0
|
|
|
|
# ============================================================================
|
|
# SIMULATED RADAR PROCESSOR
|
|
# ============================================================================
|
|
|
|
class SimulatedRadarProcessor:
|
|
"""Generates realistic simulated radar data"""
|
|
|
|
def __init__(self):
|
|
self.settings = RadarSettings()
|
|
self.frame_count = 0
|
|
self.targets = self._create_targets()
|
|
self.noise_floor = 10
|
|
self.clutter_level = 5
|
|
|
|
def _create_targets(self) -> List[Dict]:
|
|
"""Create moving targets"""
|
|
return [
|
|
{
|
|
'id': 1,
|
|
'range': 2500,
|
|
'velocity': -80,
|
|
'azimuth': 45,
|
|
'elevation': 5,
|
|
'snr': 25,
|
|
'range_drift': -0.8,
|
|
'azimuth_drift': 0.15,
|
|
'velocity_drift': 0.1
|
|
},
|
|
{
|
|
'id': 2,
|
|
'range': 800,
|
|
'velocity': 15,
|
|
'azimuth': -30,
|
|
'elevation': 0,
|
|
'snr': 18,
|
|
'range_drift': 0.3,
|
|
'azimuth_drift': -0.1,
|
|
'velocity_drift': -0.05
|
|
},
|
|
{
|
|
'id': 3,
|
|
'range': 1500,
|
|
'velocity': 0,
|
|
'azimuth': 10,
|
|
'elevation': 2,
|
|
'snr': 22,
|
|
'range_drift': 0,
|
|
'azimuth_drift': 0.05,
|
|
'velocity_drift': 0
|
|
},
|
|
{
|
|
'id': 4,
|
|
'range': 3500,
|
|
'velocity': 50,
|
|
'azimuth': -15,
|
|
'elevation': 3,
|
|
'snr': 15,
|
|
'range_drift': 0.5,
|
|
'azimuth_drift': -0.2,
|
|
'velocity_drift': -0.3
|
|
},
|
|
{
|
|
'id': 5,
|
|
'range': 500,
|
|
'velocity': -20,
|
|
'azimuth': 60,
|
|
'elevation': 1,
|
|
'snr': 30,
|
|
'range_drift': -0.2,
|
|
'azimuth_drift': 0.3,
|
|
'velocity_drift': 0.2
|
|
}
|
|
]
|
|
|
|
def generate_frame(self) -> tuple:
|
|
"""Generate a complete radar frame"""
|
|
self.frame_count += 1
|
|
|
|
# Update target positions
|
|
for target in self.targets:
|
|
target['range'] += target['range_drift']
|
|
target['azimuth'] += target['azimuth_drift']
|
|
target['velocity'] += target['velocity_drift']
|
|
|
|
# Keep within bounds with wrapping/reflection
|
|
if target['range'] < 100:
|
|
target['range'] = 100
|
|
target['range_drift'] *= -1
|
|
elif target['range'] > 4800:
|
|
target['range'] = 4800
|
|
target['range_drift'] *= -1
|
|
|
|
if target['azimuth'] < -90:
|
|
target['azimuth'] = -90
|
|
target['azimuth_drift'] *= -1
|
|
elif target['azimuth'] > 90:
|
|
target['azimuth'] = 90
|
|
target['azimuth_drift'] *= -1
|
|
|
|
if target['velocity'] < -95:
|
|
target['velocity'] = -95
|
|
target['velocity_drift'] *= -1
|
|
elif target['velocity'] > 95:
|
|
target['velocity'] = 95
|
|
target['velocity_drift'] *= -1
|
|
|
|
# Generate range-Doppler map
|
|
rd_map = self._generate_range_doppler()
|
|
|
|
# Extract detected targets
|
|
detected = self._detect_targets()
|
|
|
|
return rd_map, detected
|
|
|
|
def _generate_range_doppler(self) -> np.ndarray:
|
|
"""Generate simulated range-Doppler map"""
|
|
# Base noise
|
|
noise = self.noise_floor * np.random.random(
|
|
(self.settings.range_bins, self.settings.doppler_bins)
|
|
)
|
|
|
|
# Add clutter (constant at low velocities)
|
|
clutter = np.zeros_like(noise)
|
|
clutter[:, 14:18] = self.clutter_level * (0.8 + 0.4 * np.random.random())
|
|
|
|
# Add targets
|
|
targets = np.zeros_like(noise)
|
|
for t in self.targets:
|
|
# Convert to bin indices
|
|
r_bin = int((t['range'] / self.settings.max_range) *
|
|
(self.settings.range_bins - 1))
|
|
v_bin = int(((t['velocity'] + self.settings.max_velocity) /
|
|
(2 * self.settings.max_velocity)) *
|
|
(self.settings.doppler_bins - 1))
|
|
|
|
# Ensure valid indices
|
|
r_bin = max(0, min(self.settings.range_bins - 1, r_bin))
|
|
v_bin = max(0, min(self.settings.doppler_bins - 1, v_bin))
|
|
|
|
# Add target with spreading
|
|
for dr in range(-2, 3):
|
|
for dv in range(-2, 3):
|
|
rr = r_bin + dr
|
|
vv = v_bin + dv
|
|
if 0 <= rr < self.settings.range_bins and 0 <= vv < self.settings.doppler_bins:
|
|
distance = np.sqrt(dr**2 + dv**2)
|
|
if distance < 2.5:
|
|
amplitude = t['snr'] * np.exp(-distance/1.5)
|
|
targets[rr, vv] += amplitude * (0.7 + 0.6 * random.random())
|
|
|
|
# Combine
|
|
rd_map = noise + clutter + targets
|
|
|
|
# Add some range-varying gain
|
|
range_gain = np.linspace(1, 0.3, self.settings.range_bins)
|
|
rd_map *= range_gain[:, np.newaxis]
|
|
|
|
return rd_map
|
|
|
|
def _detect_targets(self) -> List[RadarTarget]:
|
|
"""Detect targets from current state"""
|
|
detected = []
|
|
for t in self.targets:
|
|
# Random detection based on SNR
|
|
if random.random() < (t['snr'] / 35):
|
|
# Add some measurement noise
|
|
detected.append(RadarTarget(
|
|
id=t['id'],
|
|
range=t['range'] + random.gauss(0, 10),
|
|
velocity=t['velocity'] + random.gauss(0, 2),
|
|
azimuth=t['azimuth'] + random.gauss(0, 1),
|
|
elevation=t['elevation'] + random.gauss(0, 0.5),
|
|
snr=t['snr'] + random.gauss(0, 2)
|
|
))
|
|
return detected
|
|
|
|
# ============================================================================
|
|
# MAIN GUI APPLICATION
|
|
# ============================================================================
|
|
|
|
class RadarDemoGUI:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Radar System Demo - Fully Functional")
|
|
self.root.geometry("1400x900")
|
|
|
|
# Set minimum window size
|
|
self.root.minsize(1200, 700)
|
|
|
|
# Configure style
|
|
self.style = ttk.Style()
|
|
self.style.theme_use('clam')
|
|
|
|
# Initialize components
|
|
self.settings = RadarSettings()
|
|
self.processor = SimulatedRadarProcessor()
|
|
self.running = False
|
|
self.recording = False
|
|
self.frame_count = 0
|
|
self.fps = 0
|
|
self.last_frame_time = time.time()
|
|
self.recorded_frames = []
|
|
|
|
# Data storage
|
|
self.current_rd_map = np.zeros((1024, 32))
|
|
self.current_targets = []
|
|
self.target_history = []
|
|
|
|
# Settings variables
|
|
self.settings_vars = {}
|
|
|
|
# Create GUI
|
|
self.create_menu()
|
|
self.create_main_layout()
|
|
self.create_status_bar()
|
|
|
|
# Start animation
|
|
self.animate()
|
|
|
|
# Handle window close
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
logger.info("Radar Demo GUI initialized")
|
|
|
|
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="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)
|
|
self.show_grid = tk.BooleanVar(value=True)
|
|
self.show_targets = tk.BooleanVar(value=True)
|
|
self.color_map = tk.StringVar(value='hot')
|
|
|
|
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_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']:
|
|
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", command=self.show_calibration)
|
|
tools_menu.add_command(label="Diagnostics", command=self.show_diagnostics)
|
|
tools_menu.add_command(label="Reset Simulation", command=self.reset_simulation)
|
|
|
|
# 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_main_layout(self):
|
|
"""Create main application 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_spectrum_tab()
|
|
self.create_settings_tab()
|
|
|
|
def create_control_panel(self, parent):
|
|
"""Create control panel with working buttons"""
|
|
# Left side - Status and controls
|
|
left_frame = ttk.Frame(parent)
|
|
left_frame.pack(side='left', fill='x', expand=True)
|
|
|
|
# Mode indicator
|
|
ttk.Label(left_frame, text="Mode:", font=('Arial', 10, 'bold')).grid(
|
|
row=0, column=0, padx=5, pady=2, sticky='w')
|
|
self.mode_label = ttk.Label(left_frame, text="DEMO",
|
|
foreground='green', font=('Arial', 10, 'bold'))
|
|
self.mode_label.grid(row=0, column=1, padx=5, pady=2, sticky='w')
|
|
|
|
# Device indicator
|
|
ttk.Label(left_frame, text="Device:", font=('Arial', 10)).grid(
|
|
row=0, column=2, padx=(20,5), pady=2, sticky='w')
|
|
self.device_label = ttk.Label(left_frame, text="Simulated FT601")
|
|
self.device_label.grid(row=0, column=3, padx=5, pady=2, sticky='w')
|
|
|
|
# Frame counter
|
|
ttk.Label(left_frame, text="Frame:", font=('Arial', 10)).grid(
|
|
row=0, column=4, padx=(20,5), pady=2, sticky='w')
|
|
self.frame_label = ttk.Label(left_frame, text="0")
|
|
self.frame_label.grid(row=0, column=5, padx=5, pady=2, sticky='w')
|
|
|
|
# Right side - Control buttons (ALL WORKING)
|
|
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)
|
|
|
|
self.record_button = ttk.Button(right_frame, text="● RECORD",
|
|
command=self.toggle_recording, width=10,
|
|
state='disabled')
|
|
self.record_button.pack(side='left', padx=2)
|
|
|
|
ttk.Button(right_frame, text="⚙ SETTINGS",
|
|
command=lambda: self.notebook.select(3)).pack(side='left', padx=2)
|
|
|
|
def create_radar_tab(self):
|
|
"""Create main 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.zeros((1024, 32)),
|
|
aspect='auto',
|
|
cmap='hot',
|
|
extent=[-100, 100, 5000, 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.set_title('Real-Time Radar Data', color='white', fontsize=12, fontweight='bold')
|
|
self.rd_ax.tick_params(colors='white')
|
|
self.rd_ax.grid(True, alpha=0.3) if self.show_grid.get() else None
|
|
|
|
# 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)
|
|
|
|
# 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, anchor='center')
|
|
self.target_tree.column('Range', width=80, anchor='center')
|
|
self.target_tree.column('Velocity', width=80, anchor='center')
|
|
self.target_tree.column('Azimuth', width=70, anchor='center')
|
|
self.target_tree.column('Elevation', width=70, anchor='center')
|
|
self.target_tree.column('SNR', width=70, anchor='center')
|
|
|
|
# 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')
|
|
|
|
# Clear targets button
|
|
ttk.Button(target_frame, text="Clear List",
|
|
command=self.clear_targets).pack(pady=5)
|
|
|
|
def create_scope_tab(self):
|
|
"""Create A-scope tab"""
|
|
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.5)
|
|
self.scope_ax.set_xlim(0, 5000)
|
|
self.scope_ax.set_ylim(0, 50)
|
|
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', color='white', fontsize=12, fontweight='bold')
|
|
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)
|
|
|
|
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.5)
|
|
self.spec_ax.set_xlim(-100, 100)
|
|
self.spec_ax.set_ylim(0, 50)
|
|
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', fontsize=12, fontweight='bold')
|
|
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 bin selector
|
|
control_frame = ttk.Frame(tab)
|
|
control_frame.pack(fill='x', pady=5)
|
|
|
|
ttk.Label(control_frame, text="Range Bin:").pack(side='left', padx=5)
|
|
self.range_slider = ttk.Scale(control_frame, from_=0, to=1023,
|
|
orient='horizontal', length=400,
|
|
command=self.update_range_label)
|
|
self.range_slider.pack(side='left', padx=5)
|
|
self.range_slider.set(512)
|
|
|
|
self.range_label = ttk.Label(control_frame, text="512")
|
|
self.range_label.pack(side='left', padx=5)
|
|
|
|
def create_settings_tab(self):
|
|
"""Create settings tab with working controls"""
|
|
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)
|
|
|
|
# Simulation settings
|
|
sim_frame = ttk.Frame(settings_notebook)
|
|
settings_notebook.add(sim_frame, text="Simulation")
|
|
self.create_simulation_settings(sim_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 with variables
|
|
settings = [
|
|
('Frequency (GHz):', 'freq', 10.0, 1.0, 20.0),
|
|
('Long Chirp (µs):', 'long_dur', 30.0, 1.0, 100.0),
|
|
('Short Chirp (µs):', 'short_dur', 0.5, 0.1, 10.0),
|
|
('Chirps/Frame:', 'chirps', 32, 8, 128),
|
|
('Range Bins:', 'range_bins', 1024, 256, 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),
|
|
('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.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=20).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)
|
|
self.update_rate_value = ttk.Label(frame, text="20")
|
|
self.update_rate_value.grid(row=0, column=2, sticky='w')
|
|
self.update_rate.configure(command=lambda v: self.update_rate_value.config(text=f"{float(v):.0f}"))
|
|
|
|
# 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)
|
|
|
|
# Apply display button
|
|
ttk.Button(frame, text="Apply Display Settings",
|
|
command=self.apply_display_settings).grid(row=4, column=0,
|
|
columnspan=2, pady=20)
|
|
|
|
def create_simulation_settings(self, parent):
|
|
"""Create simulation settings controls"""
|
|
frame = ttk.Frame(parent)
|
|
frame.pack(fill='both', expand=True, padx=20, pady=20)
|
|
|
|
# Noise floor
|
|
ttk.Label(frame, text="Noise Floor:").grid(row=0, column=0, sticky='w', pady=5)
|
|
self.noise_floor = ttk.Scale(frame, from_=0, to=20,
|
|
orient='horizontal', length=200)
|
|
self.noise_floor.set(10)
|
|
self.noise_floor.grid(row=0, column=1, padx=10, pady=5)
|
|
self.noise_value = ttk.Label(frame, text="10")
|
|
self.noise_value.grid(row=0, column=2, sticky='w')
|
|
self.noise_floor.configure(command=lambda v: self.noise_value.config(text=f"{float(v):.1f}"))
|
|
|
|
# Clutter level
|
|
ttk.Label(frame, text="Clutter Level:").grid(row=1, column=0, sticky='w', pady=5)
|
|
self.clutter_level = ttk.Scale(frame, from_=0, to=20,
|
|
orient='horizontal', length=200)
|
|
self.clutter_level.set(5)
|
|
self.clutter_level.grid(row=1, column=1, padx=10, pady=5)
|
|
self.clutter_value = ttk.Label(frame, text="5")
|
|
self.clutter_value.grid(row=1, column=2, sticky='w')
|
|
self.clutter_level.configure(command=lambda v: self.clutter_value.config(text=f"{float(v):.1f}"))
|
|
|
|
# Number of targets
|
|
ttk.Label(frame, text="Number of Targets:").grid(row=2, column=0, sticky='w', pady=5)
|
|
self.num_targets = ttk.Scale(frame, from_=1, to=10,
|
|
orient='horizontal', length=200)
|
|
self.num_targets.set(5)
|
|
self.num_targets.grid(row=2, column=1, padx=10, pady=5)
|
|
self.targets_value = ttk.Label(frame, text="5")
|
|
self.targets_value.grid(row=2, column=2, sticky='w')
|
|
self.num_targets.configure(command=lambda v: self.targets_value.config(text=f"{float(v):.0f}"))
|
|
|
|
# Reset button
|
|
ttk.Button(frame, text="Reset Simulation",
|
|
command=self.reset_simulation).grid(row=3, column=0,
|
|
columnspan=2, pady=20)
|
|
|
|
def create_status_bar(self):
|
|
"""Create status bar at bottom"""
|
|
status_frame = ttk.Frame(self.root)
|
|
status_frame.pack(side='bottom', fill='x')
|
|
|
|
# Left status
|
|
self.status_label = ttk.Label(status_frame, text="Status: READY",
|
|
relief='sunken', padding=2)
|
|
self.status_label.pack(side='left', fill='x', expand=True)
|
|
|
|
# Right indicators
|
|
self.fps_label = ttk.Label(status_frame, text="FPS: 0",
|
|
relief='sunken', width=10)
|
|
self.fps_label.pack(side='right', padx=1)
|
|
|
|
self.targets_label = ttk.Label(status_frame, text="Targets: 0",
|
|
relief='sunken', width=12)
|
|
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 - updates all displays"""
|
|
if not hasattr(self, 'animation_running'):
|
|
self.animation_running = True
|
|
|
|
try:
|
|
# Calculate FPS
|
|
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_data()
|
|
self.frame_count += 1
|
|
self.frame_label.config(text=str(self.frame_count))
|
|
|
|
# Update status bar
|
|
self.update_status_bar()
|
|
|
|
# Update time
|
|
self.time_label.config(text=time.strftime("%H:%M:%S"))
|
|
|
|
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_data(self):
|
|
"""Generate and display new radar data"""
|
|
# Generate frame
|
|
rd_map, targets = self.processor.generate_frame()
|
|
|
|
# Apply simulation settings
|
|
self.processor.noise_floor = self.noise_floor.get()
|
|
self.processor.clutter_level = self.clutter_level.get()
|
|
|
|
# Store current data
|
|
self.current_rd_map = rd_map
|
|
self.current_targets = targets
|
|
|
|
# Update range-Doppler map
|
|
log_map = 10 * np.log10(rd_map + 1)
|
|
self.rd_img.set_data(log_map)
|
|
self.rd_img.set_cmap(self.color_map.get())
|
|
|
|
# Update color limits
|
|
vmin = np.percentile(log_map, 5)
|
|
vmax = np.percentile(log_map, 95)
|
|
self.rd_img.set_clim(vmin, vmax)
|
|
|
|
# Draw target markers if enabled
|
|
if self.show_targets.get():
|
|
# Clear previous 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 markers
|
|
for target in targets:
|
|
x = target.velocity
|
|
y = target.range
|
|
marker = self.rd_ax.plot(x, y, 'wo', markersize=8,
|
|
markeredgecolor='red', markeredgewidth=2)[0]
|
|
marker.is_target_marker = True
|
|
text = self.rd_ax.text(x, y-150, str(target.id), color='white',
|
|
ha='center', va='top', fontsize=8,
|
|
fontweight='bold')
|
|
text.is_target_marker = True
|
|
|
|
# Update grid
|
|
if self.show_grid.get():
|
|
self.rd_ax.grid(True, alpha=0.3)
|
|
else:
|
|
self.rd_ax.grid(False)
|
|
|
|
# Update canvas
|
|
self.rd_canvas.draw_idle()
|
|
|
|
# Update target list
|
|
self.update_target_list()
|
|
|
|
# Update A-scope
|
|
range_profile = np.mean(rd_map, axis=1)
|
|
range_axis = np.linspace(0, 5000, 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()
|
|
|
|
# Update Doppler spectrum
|
|
range_bin = int(self.range_slider.get())
|
|
spectrum = rd_map[range_bin, :]
|
|
vel_axis = np.linspace(-100, 100, 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()
|
|
|
|
# Record if enabled
|
|
if self.recording:
|
|
self.recorded_frames.append({
|
|
'frame': self.frame_count,
|
|
'time': time.time(),
|
|
'map': rd_map.copy(),
|
|
'targets': [(t.range, t.velocity, t.azimuth, t.snr) for t in targets]
|
|
})
|
|
|
|
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.current_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)
|
|
|
|
# Update targets label
|
|
self.targets_label.config(text=f"Targets: {len(self.current_targets)}")
|
|
|
|
def update_status_bar(self):
|
|
"""Update status bar information"""
|
|
if self.running:
|
|
status = "RUNNING"
|
|
if self.recording:
|
|
status = "RECORDING"
|
|
else:
|
|
status = "READY"
|
|
|
|
self.status_label.config(text=f"Status: {status}")
|
|
self.fps_label.config(text=f"FPS: {self.fps:.1f}")
|
|
|
|
def update_range_label(self, value):
|
|
"""Update range bin label"""
|
|
self.range_label.config(text=f"{int(float(value))}")
|
|
|
|
# ============================================================================
|
|
# COMMAND HANDLERS (ALL WORKING)
|
|
# ============================================================================
|
|
|
|
def start_radar(self):
|
|
"""Start radar simulation"""
|
|
self.running = True
|
|
self.start_button.config(state='disabled')
|
|
self.stop_button.config(state='normal')
|
|
self.record_button.config(state='normal')
|
|
self.mode_label.config(text="RUNNING", foreground='green')
|
|
logger.info("Radar started")
|
|
|
|
def stop_radar(self):
|
|
"""Stop radar simulation"""
|
|
self.running = False
|
|
self.recording = False
|
|
self.start_button.config(state='normal')
|
|
self.stop_button.config(state='disabled')
|
|
self.record_button.config(state='disabled', text='● RECORD')
|
|
self.mode_label.config(text="STOPPED", foreground='red')
|
|
logger.info("Radar stopped")
|
|
|
|
def toggle_recording(self):
|
|
"""Toggle data recording"""
|
|
if not self.running:
|
|
messagebox.showwarning("Warning", "Start radar first")
|
|
return
|
|
|
|
self.recording = not self.recording
|
|
if self.recording:
|
|
self.record_button.config(text="● RECORDING", foreground='red')
|
|
self.recorded_frames = [] # Clear previous recording
|
|
logger.info("Recording started")
|
|
else:
|
|
self.record_button.config(text="● RECORD", foreground='black')
|
|
logger.info(f"Recording stopped. Captured {len(self.recorded_frames)} frames")
|
|
|
|
def clear_targets(self):
|
|
"""Clear target list"""
|
|
for item in self.target_tree.get_children():
|
|
self.target_tree.delete(item)
|
|
self.current_targets = []
|
|
logger.info("Target list cleared")
|
|
|
|
def apply_settings(self):
|
|
"""Apply radar settings"""
|
|
try:
|
|
self.settings.frequency = self.settings_vars['freq'].get()
|
|
self.settings.long_chirp_us = self.settings_vars['long_dur'].get()
|
|
self.settings.short_chirp_us = self.settings_vars['short_dur'].get()
|
|
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()
|
|
self.settings.cfar_threshold = self.settings_vars['cfar'].get()
|
|
|
|
# Update processor settings
|
|
self.processor.settings = self.settings
|
|
|
|
# Update plot extents
|
|
self.rd_ax.set_xlim(-self.settings.max_velocity, self.settings.max_velocity)
|
|
self.rd_ax.set_ylim(self.settings.max_range, 0)
|
|
self.spec_ax.set_xlim(-self.settings.max_velocity, self.settings.max_velocity)
|
|
self.scope_ax.set_xlim(0, self.settings.max_range)
|
|
|
|
messagebox.showinfo("Success", "Settings applied")
|
|
logger.info("Settings updated")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Invalid settings: {e}")
|
|
|
|
def apply_display_settings(self):
|
|
"""Apply display settings"""
|
|
# Update grid
|
|
if self.show_grid.get():
|
|
self.rd_ax.grid(True, alpha=0.3)
|
|
self.scope_ax.grid(True, alpha=0.3)
|
|
self.spec_ax.grid(True, alpha=0.3)
|
|
else:
|
|
self.rd_ax.grid(False)
|
|
self.scope_ax.grid(False)
|
|
self.spec_ax.grid(False)
|
|
|
|
# Redraw
|
|
self.rd_canvas.draw_idle()
|
|
self.scope_canvas.draw_idle()
|
|
self.spec_canvas.draw_idle()
|
|
|
|
messagebox.showinfo("Success", "Display settings applied")
|
|
|
|
def reset_simulation(self):
|
|
"""Reset the simulation"""
|
|
if messagebox.askyesno("Confirm", "Reset simulation to initial state?"):
|
|
self.processor = SimulatedRadarProcessor()
|
|
self.frame_count = 0
|
|
self.frame_label.config(text="0")
|
|
self.current_targets = []
|
|
self.update_target_list()
|
|
logger.info("Simulation reset")
|
|
|
|
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 settings
|
|
for key, value in config.get('settings', {}).items():
|
|
if key in self.settings_vars:
|
|
self.settings_vars[key].set(value)
|
|
|
|
# Apply display settings
|
|
display = config.get('display', {})
|
|
if 'color_map' in display:
|
|
self.color_map.set(display['color_map'])
|
|
if 'show_grid' in display:
|
|
self.show_grid.set(display['show_grid'])
|
|
if 'show_targets' in display:
|
|
self.show_targets.set(display['show_targets'])
|
|
|
|
self.apply_settings()
|
|
self.apply_display_settings()
|
|
|
|
messagebox.showinfo("Success", f"Loaded configuration from {filename}")
|
|
logger.info(f"Configuration loaded 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()
|
|
}
|
|
}
|
|
with open(filename, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
messagebox.showinfo("Success", f"Saved configuration to {filename}")
|
|
logger.info(f"Configuration saved to {filename}")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to save: {e}")
|
|
|
|
def export_data(self):
|
|
"""Export recorded data"""
|
|
if not self.recorded_frames:
|
|
messagebox.showwarning("Warning", "No recorded data to export")
|
|
return
|
|
|
|
from tkinter import filedialog
|
|
filename = filedialog.asksaveasfilename(
|
|
title="Export Data",
|
|
defaultextension=".npz",
|
|
filetypes=[("NumPy files", "*.npz"), ("All files", "*.*")]
|
|
)
|
|
if filename:
|
|
try:
|
|
# Prepare data for export
|
|
frames = np.array([f['map'] for f in self.recorded_frames])
|
|
times = np.array([f['time'] for f in self.recorded_frames])
|
|
|
|
# Save
|
|
np.savez(filename,
|
|
frames=frames,
|
|
times=times,
|
|
settings=vars(self.settings))
|
|
|
|
messagebox.showinfo("Success", f"Exported {len(frames)} frames to {filename}")
|
|
logger.info(f"Data exported to {filename}")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to export: {e}")
|
|
|
|
def show_calibration(self):
|
|
"""Show calibration dialog"""
|
|
messagebox.showinfo("Calibration",
|
|
"Calibration Wizard\n\n"
|
|
"1. Set noise floor\n"
|
|
"2. Run noise measurement\n"
|
|
"3. Apply calibration factors\n\n"
|
|
f"Current noise floor: {self.processor.noise_floor:.1f} dB")
|
|
|
|
def show_diagnostics(self):
|
|
"""Show system diagnostics"""
|
|
import platform
|
|
info = f"""
|
|
SYSTEM DIAGNOSTICS
|
|
=================
|
|
|
|
Radar Status
|
|
------------
|
|
Mode: {'RUNNING' if self.running else 'STOPPED'}
|
|
Frames: {self.frame_count}
|
|
Targets: {len(self.current_targets)}
|
|
FPS: {self.fps:.1f}
|
|
|
|
Simulation Parameters
|
|
---------------------
|
|
Noise Floor: {self.processor.noise_floor:.1f} dB
|
|
Clutter Level: {self.processor.clutter_level:.1f} dB
|
|
Active Targets: {len(self.processor.targets)}
|
|
|
|
Display Settings
|
|
----------------
|
|
Color Map: {self.color_map.get()}
|
|
Update Rate: {self.update_rate.get():.0f} Hz
|
|
Grid: {'On' if self.show_grid.get() else 'Off'}
|
|
|
|
System Info
|
|
-----------
|
|
Platform: {platform.platform()}
|
|
Python: {platform.python_version()}
|
|
Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
"""
|
|
|
|
# Create diagnostics window
|
|
diag_window = tk.Toplevel(self.root)
|
|
diag_window.title("Diagnostics")
|
|
diag_window.geometry("500x600")
|
|
|
|
text_widget = tk.Text(diag_window, bg='#2b2b2b', fg='#e0e0e0',
|
|
font=('Courier', 10), wrap='none')
|
|
text_widget.pack(fill='both', expand=True, padx=10, pady=10)
|
|
text_widget.insert('1.0', info)
|
|
text_widget.config(state='disabled')
|
|
|
|
# Add scrollbar
|
|
scrollbar = ttk.Scrollbar(diag_window, orient='vertical',
|
|
command=text_widget.yview)
|
|
scrollbar.pack(side='right', fill='y')
|
|
text_widget.config(yscrollcommand=scrollbar.set)
|
|
|
|
def show_docs(self):
|
|
"""Show documentation"""
|
|
docs = """
|
|
RADAR SYSTEM DEMO - USER GUIDE
|
|
===============================
|
|
|
|
Getting Started
|
|
---------------
|
|
1. Click START to begin radar simulation
|
|
2. Watch real-time range-Doppler display
|
|
3. Detected targets appear in the list
|
|
4. Use tabs to view different displays
|
|
|
|
Controls
|
|
--------
|
|
• START/STOP: Control radar simulation
|
|
• RECORD: Capture data for export
|
|
• SETTINGS: Configure radar parameters
|
|
• Clear List: Remove targets from display
|
|
|
|
Display Tabs
|
|
------------
|
|
• Radar Display: Main range-Doppler view
|
|
• A-Scope: Range profile plot
|
|
• Doppler Spectrum: Velocity analysis
|
|
• Settings: Configure all parameters
|
|
|
|
Tips
|
|
----
|
|
• Adjust update rate in Display settings
|
|
• Change color map for better visibility
|
|
• Export recorded data for analysis
|
|
• Reset simulation to restart targets
|
|
|
|
For more information, visit:
|
|
https://github.com/radar-system/docs
|
|
"""
|
|
|
|
messagebox.showinfo("Documentation", docs)
|
|
|
|
def show_about(self):
|
|
"""Show about dialog"""
|
|
about = """
|
|
Radar System Demo
|
|
Version 2.0.0
|
|
|
|
A fully functional radar simulation
|
|
and visualization tool.
|
|
|
|
Features:
|
|
• Real-time range-Doppler processing
|
|
• Multiple moving targets
|
|
• A-scope and spectrum displays
|
|
• Data recording and export
|
|
• Configurable parameters
|
|
|
|
Created for demonstration and testing
|
|
of radar signal processing concepts.
|
|
|
|
© 2025 Radar Systems Inc.
|
|
"""
|
|
|
|
messagebox.showinfo("About", about)
|
|
|
|
def on_closing(self):
|
|
"""Handle window closing"""
|
|
if messagebox.askokcancel("Quit", "Exit radar demo?"):
|
|
self.animation_running = False
|
|
self.running = False
|
|
self.root.destroy()
|
|
|
|
# ============================================================================
|
|
# MAIN ENTRY POINT
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""Main application entry point"""
|
|
try:
|
|
# Create root window
|
|
root = tk.Tk()
|
|
|
|
# Create application
|
|
_app = RadarDemoGUI(root) # noqa: F841 — keeps reference alive
|
|
|
|
# Center window
|
|
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()
|