577 lines
31 KiB
Python
577 lines
31 KiB
Python
"""
|
||
Main GUI module for the Radar Scenario Simulator (Tkinter version).
|
||
|
||
This module is responsible for building the main application window, laying out
|
||
widgets, and coordinating the simulation and plotting managers.
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, simpledialog
|
||
from tkinter.scrolledtext import ScrolledText
|
||
import queue
|
||
import logging
|
||
import numpy as np
|
||
|
||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||
from matplotlib.figure import Figure
|
||
|
||
# Import core simulation engine and utility functions
|
||
from ..core.simulation_engine import RadarConfig, AntennaConfig, ScanConfig, Target
|
||
from ..utils import radar_math, config_manager
|
||
|
||
# Import new manager classes and logger
|
||
from .plot_manager import PlotManager
|
||
from .simulation_manager import SimulationManager
|
||
from ..utils import logger
|
||
|
||
# --- Helper Dialog for Adding/Editing Targets ---
|
||
class AddTargetDialog(tk.Toplevel):
|
||
"""Dialog window for adding or editing target parameters."""
|
||
def __init__(self, parent, target_data=None):
|
||
super().__init__(parent)
|
||
self.title("Add New Target" if target_data is None else "Edit Target")
|
||
self.transient(parent)
|
||
self.grab_set()
|
||
self.result = None
|
||
|
||
frame = ttk.Frame(self, padding="10")
|
||
frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||
|
||
self.vars = {
|
||
"pos_x": tk.DoubleVar(value=target_data["pos_x"] if target_data else 5000.0),
|
||
"pos_y": tk.DoubleVar(value=target_data["pos_y"] if target_data else 0.0),
|
||
"pos_z": tk.DoubleVar(value=target_data["pos_z"] if target_data else 0.0),
|
||
"vel_x": tk.DoubleVar(value=target_data["vel_x"] if target_data else -150.0),
|
||
"vel_y": tk.DoubleVar(value=target_data["vel_y"] if target_data else 0.0),
|
||
"vel_z": tk.DoubleVar(value=target_data["vel_z"] if target_data else 0.0),
|
||
"rcs": tk.DoubleVar(value=target_data["rcs"] if target_data else 1.0)
|
||
}
|
||
|
||
labels = ["Initial Position X (m):", "Initial Position Y (m):", "Initial Position Z (m):",
|
||
"Velocity X (m/s):", "Velocity Y (m/s):", "Velocity Z (m/s):", "RCS (m^2):"]
|
||
keys = ["pos_x", "pos_y", "pos_z", "vel_x", "vel_y", "vel_z", "rcs"]
|
||
|
||
for i, (label_text, key) in enumerate(zip(labels, keys)):
|
||
ttk.Label(frame, text=label_text).grid(row=i, column=0, sticky=tk.W, pady=2)
|
||
spinbox = ttk.Spinbox(frame, from_=-1e6, to=1e6, textvariable=self.vars[key])
|
||
spinbox.grid(row=i, column=1, sticky=(tk.W, tk.E), pady=2)
|
||
if key == "rcs": spinbox.config(from_=0.01)
|
||
|
||
button_frame = ttk.Frame(frame)
|
||
button_frame.grid(row=len(labels), column=0, columnspan=2, pady=10)
|
||
ttk.Button(button_frame, text="OK", command=self.on_ok).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(button_frame, text="Cancel", command=self.destroy).pack(side=tk.LEFT, padx=5)
|
||
|
||
def on_ok(self):
|
||
self.result = {key: var.get() for key, var in self.vars.items()}
|
||
self.destroy()
|
||
|
||
def show(self):
|
||
self.wait_window()
|
||
return self.result
|
||
|
||
# --- Main Application Class ---
|
||
class App(tk.Tk):
|
||
"""Main application window for the Radar Scenario Simulator."""
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.title("Radar Scenario Simulator")
|
||
self.attributes('-fullscreen', True)
|
||
self.bind("<Escape>", lambda event: self.attributes('-fullscreen', False))
|
||
|
||
# --- Main Layout ---
|
||
main_paned_window = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||
main_paned_window.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# --- Left Column (Notebook and Logs) ---
|
||
left_column_frame = ttk.Frame(main_paned_window, width=600)
|
||
main_paned_window.add(left_column_frame, weight=1)
|
||
|
||
left_paned_window = ttk.PanedWindow(left_column_frame, orient=tk.VERTICAL)
|
||
left_paned_window.pack(fill=tk.BOTH, expand=True)
|
||
|
||
notebook_frame = ttk.Frame(left_paned_window, height=500)
|
||
left_paned_window.add(notebook_frame, weight=2)
|
||
|
||
log_frame = ttk.LabelFrame(left_paned_window, text="Log", height=200)
|
||
left_paned_window.add(log_frame, weight=1)
|
||
|
||
# --- Right Column (RD and PPI plots) ---
|
||
right_column_frame = ttk.Frame(main_paned_window)
|
||
main_paned_window.add(right_column_frame, weight=3)
|
||
|
||
right_paned_window = ttk.PanedWindow(right_column_frame, orient=tk.VERTICAL)
|
||
right_paned_window.pack(fill=tk.BOTH, expand=True)
|
||
|
||
rd_frame = ttk.LabelFrame(right_paned_window, text="Range-Doppler Map")
|
||
right_paned_window.add(rd_frame, weight=1)
|
||
|
||
ppi_frame = ttk.Frame(right_paned_window)
|
||
right_paned_window.add(ppi_frame, weight=1)
|
||
|
||
# --- Initialize UI Variables ---
|
||
self._init_vars()
|
||
|
||
# --- Create UI Components ---
|
||
notebook = ttk.Notebook(notebook_frame)
|
||
notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
self.config_tab = ttk.Frame(notebook)
|
||
self.target_tab = ttk.Frame(notebook)
|
||
self.scenario_tab = ttk.Frame(notebook)
|
||
notebook.add(self.config_tab, text="Configuration")
|
||
notebook.add(self.target_tab, text="Target")
|
||
notebook.add(self.scenario_tab, text="Scenario")
|
||
|
||
self._populate_config_tab(self.config_tab)
|
||
self._populate_target_tab(self.target_tab)
|
||
self._populate_scenario_tab(self.scenario_tab)
|
||
|
||
# --- Setup Logging Area ---
|
||
self.log_widget = ScrolledText(log_frame, state=tk.DISABLED, wrap=tk.WORD, font=("TkDefaultFont", 9))
|
||
self.log_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
self._init_logging()
|
||
|
||
# --- Setup Plotting Area ---
|
||
ppi_plot_frame = ttk.LabelFrame(ppi_frame, text="PPI Display")
|
||
ppi_plot_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0), pady=5)
|
||
scan_controls_frame = ttk.LabelFrame(ppi_frame, text="Scan & PPI Control")
|
||
scan_controls_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 5), pady=5)
|
||
self._populate_scan_ppi_controls(scan_controls_frame)
|
||
|
||
self.rd_figure = Figure(figsize=(8, 6), dpi=100, facecolor='#3a3a3a')
|
||
self.rd_canvas = FigureCanvasTkAgg(self.rd_figure, master=rd_frame)
|
||
self.rd_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||
|
||
self.ppi_figure = Figure(figsize=(4, 4), dpi=100, facecolor='#3a3a3a')
|
||
self.ppi_canvas = FigureCanvasTkAgg(self.ppi_figure, master=ppi_plot_frame)
|
||
self.ppi_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||
|
||
# --- Initialize Managers ---
|
||
self.plot_manager = PlotManager(self.rd_figure, self.rd_canvas, self.ppi_figure, self.ppi_canvas, self.vars['ppi_range_m'])
|
||
self.simulation_manager = SimulationManager()
|
||
|
||
# --- Finalize Init ---
|
||
self.toggle_amplitude_controls()
|
||
self.update_derived_parameters()
|
||
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||
self.log.info("Application initialized successfully.")
|
||
|
||
def _init_logging(self):
|
||
logging_config = {
|
||
"default_root_level": logging.INFO,
|
||
"format": "%(asctime)s [%(levelname)-8s] %(name)-20s: %(message)s",
|
||
"date_format": "%H:%M:%S",
|
||
"enable_console": True,
|
||
"colors": {
|
||
logging.DEBUG: "gray",
|
||
logging.INFO: "black",
|
||
logging.WARNING: "orange",
|
||
logging.ERROR: "red",
|
||
logging.CRITICAL: "red",
|
||
}
|
||
}
|
||
logger.setup_basic_logging(self, logging_config)
|
||
logger.add_tkinter_handler(self.log_widget, logging_config)
|
||
self.log = logger.get_logger(__name__)
|
||
|
||
def on_closing(self):
|
||
self.log.info("Shutdown sequence initiated.")
|
||
if self.simulation_manager.is_running():
|
||
self.stop_simulation()
|
||
logger.shutdown_logging_system()
|
||
self.destroy()
|
||
|
||
def _init_vars(self):
|
||
self.vars = {
|
||
"carrier_frequency": tk.DoubleVar(value=9.5e9),
|
||
"prf": tk.DoubleVar(value=2000.0),
|
||
"duty_cycle": tk.DoubleVar(value=10.0),
|
||
"sample_rate": tk.DoubleVar(value=5e6),
|
||
"beamwidth_az_deg": tk.DoubleVar(value=3.0),
|
||
"beamwidth_el_deg": tk.DoubleVar(value=3.0),
|
||
"scan_mode": tk.StringVar(value='staring'),
|
||
"min_az_deg": tk.DoubleVar(value=-30.0),
|
||
"max_az_deg": tk.DoubleVar(value=30.0),
|
||
"scan_speed_deg_s": tk.DoubleVar(value=20.0),
|
||
"num_pulses_cpi": tk.IntVar(value=256),
|
||
"simulation_duration_s": tk.DoubleVar(value=10.0),
|
||
"min_db": tk.DoubleVar(value=-60.0),
|
||
"max_db": tk.DoubleVar(value=0.0),
|
||
"auto_scale": tk.BooleanVar(value=True),
|
||
"pulse_width_text": tk.StringVar(),
|
||
"listening_time_text": tk.StringVar(),
|
||
"max_range_text": tk.StringVar(),
|
||
"max_velocity_text": tk.StringVar(),
|
||
"dwell_time_text": tk.StringVar(),
|
||
"pulses_on_target_text": tk.StringVar(),
|
||
"ppi_range_m": tk.DoubleVar(value=15000.0),
|
||
"scan_info_text": tk.StringVar(value="Mode: Staring")
|
||
}
|
||
self.profiles = config_manager.load_profiles()
|
||
self.selected_profile = tk.StringVar()
|
||
for key in ["prf", "carrier_frequency", "duty_cycle", "beamwidth_az_deg", "scan_mode", "scan_speed_deg_s"]:
|
||
self.vars[key].trace_add("write", self.update_derived_parameters)
|
||
|
||
def _create_labeled_spinbox(self, parent, text, var, from_, to, increment=1.0, is_db=False, scientific=False, command=None):
|
||
frame = ttk.Frame(parent)
|
||
frame.pack(fill=tk.X, pady=2)
|
||
ttk.Label(frame, text=text, width=25).pack(side=tk.LEFT)
|
||
if scientific:
|
||
spinbox = ttk.Spinbox(frame, from_=from_, to=to, increment=increment, textvariable=var, format="%.2e", command=command)
|
||
else:
|
||
spinbox = ttk.Spinbox(frame, from_=from_, to=to, increment=increment, textvariable=var, command=command)
|
||
spinbox.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||
if is_db:
|
||
if "min" in text.lower(): self.min_db_spinbox = spinbox
|
||
else: self.max_db_spinbox = spinbox
|
||
return spinbox
|
||
|
||
def _populate_config_tab(self, tab):
|
||
profile_group = ttk.LabelFrame(tab, text="Radar Profiles", padding=10)
|
||
profile_group.pack(fill=tk.X, padx=5, pady=5)
|
||
profile_frame = ttk.Frame(profile_group)
|
||
profile_frame.pack(fill=tk.X, pady=2)
|
||
ttk.Label(profile_frame, text="Profile:").pack(side=tk.LEFT, padx=(0, 5))
|
||
self.profile_combobox = ttk.Combobox(profile_frame, textvariable=self.selected_profile, state='readonly')
|
||
self.profile_combobox.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||
self.profile_combobox.bind('<<ComboboxSelected>>', self.on_profile_select)
|
||
btn_frame = ttk.Frame(profile_group)
|
||
btn_frame.pack(fill=tk.X, pady=5)
|
||
ttk.Button(btn_frame, text="Save Current...", command=self.save_profile).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="Delete Selected", command=self.delete_profile).pack(side=tk.LEFT, padx=5)
|
||
self.refresh_profile_list()
|
||
|
||
radar_group = ttk.LabelFrame(tab, text="Radar Configuration", padding=10)
|
||
radar_group.pack(fill=tk.X, padx=5, pady=5)
|
||
self._create_labeled_spinbox(radar_group, "Carrier Frequency (Hz):", self.vars["carrier_frequency"], 1e6, 100e9, scientific=True)
|
||
self._create_labeled_spinbox(radar_group, "PRF (Hz):", self.vars["prf"], 1, 100000)
|
||
self._create_labeled_spinbox(radar_group, "Duty Cycle (%):", self.vars["duty_cycle"], 0.01, 99.99, increment=0.1)
|
||
self._create_labeled_spinbox(radar_group, "Sample Rate (Hz):", self.vars["sample_rate"], 1e3, 100e6, scientific=True)
|
||
|
||
antenna_group = ttk.LabelFrame(tab, text="Antenna Configuration", padding=10)
|
||
antenna_group.pack(fill=tk.X, padx=5, pady=5)
|
||
self._create_labeled_spinbox(antenna_group, "Azimuth Beamwidth (deg):", self.vars["beamwidth_az_deg"], 0.1, 90.0, increment=0.1)
|
||
|
||
scan_group = ttk.LabelFrame(tab, text="Scan Strategy", padding=10)
|
||
scan_group.pack(fill=tk.X, padx=5, pady=5)
|
||
mode_frame = ttk.Frame(scan_group)
|
||
mode_frame.pack(fill=tk.X, pady=2)
|
||
ttk.Label(mode_frame, text="Scan Mode:", width=25).pack(side=tk.LEFT)
|
||
ttk.Radiobutton(mode_frame, text="Staring", variable=self.vars["scan_mode"], value="staring", command=self.update_scan_mode_controls).pack(side=tk.LEFT)
|
||
ttk.Radiobutton(mode_frame, text="Sector", variable=self.vars["scan_mode"], value="sector", command=self.update_scan_mode_controls).pack(side=tk.LEFT)
|
||
|
||
self.min_az_spinbox = self._create_labeled_spinbox(scan_group, "Min Azimuth (deg):", self.vars["min_az_deg"], -180, 180, increment=1.0)
|
||
self.max_az_spinbox = self._create_labeled_spinbox(scan_group, "Max Azimuth (deg):", self.vars["max_az_deg"], -180, 180, increment=1.0)
|
||
self.scan_speed_spinbox = self._create_labeled_spinbox(scan_group, "Scan Speed (deg/s):", self.vars["scan_speed_deg_s"], 0.1, 1000.0, increment=1.0)
|
||
self.update_scan_mode_controls()
|
||
|
||
derived_group = ttk.LabelFrame(tab, text="Derived & Calculated Values", padding=10)
|
||
derived_group.pack(fill=tk.X, padx=5, pady=5)
|
||
for key in ["pulse_width_text", "listening_time_text"]:
|
||
ttk.Label(derived_group, textvariable=self.vars[key]).pack(anchor=tk.W)
|
||
ttk.Separator(derived_group, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
|
||
self.max_range_label = ttk.Label(derived_group, textvariable=self.vars["max_range_text"])
|
||
self.max_range_label.pack(anchor=tk.W)
|
||
self.max_velocity_label = ttk.Label(derived_group, textvariable=self.vars["max_velocity_text"])
|
||
self.max_velocity_label.pack(anchor=tk.W)
|
||
ttk.Separator(derived_group, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
|
||
for key in ["dwell_time_text", "pulses_on_target_text"]:
|
||
ttk.Label(derived_group, textvariable=self.vars[key]).pack(anchor=tk.W)
|
||
|
||
def _populate_target_tab(self, tab):
|
||
target_group = ttk.LabelFrame(tab, text="Target Management", padding=10)
|
||
target_group.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
self._create_target_table(target_group)
|
||
|
||
def _populate_scenario_tab(self, tab):
|
||
sim_group = ttk.LabelFrame(tab, text="Simulation & Plotting Control", padding=10)
|
||
sim_group.pack(fill=tk.X, padx=5, pady=5)
|
||
self._create_sim_controls(sim_group)
|
||
self.analysis_frame = ttk.LabelFrame(tab, text="Simulation Analysis & Warnings", padding=10)
|
||
self.analysis_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
self.analysis_text = tk.Text(self.analysis_frame, wrap=tk.WORD, height=10, state=tk.DISABLED, font=('TkDefaultFont', 10))
|
||
self.analysis_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
analysis_text_scroll = ttk.Scrollbar(self.analysis_frame, command=self.analysis_text.yview)
|
||
analysis_text_scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.analysis_text.config(yscrollcommand=analysis_text_scroll.set)
|
||
|
||
def _create_target_table(self, parent):
|
||
frame = ttk.Frame(parent)
|
||
frame.pack(fill=tk.BOTH, expand=True)
|
||
cols = ("Pos X", "Pos Y", "Pos Z", "Vel X", "Vel Y", "Vel Z", "RCS")
|
||
self.target_table = ttk.Treeview(frame, columns=cols, show="headings")
|
||
self.target_table.column("#0", width=0, stretch=tk.NO)
|
||
for col in cols:
|
||
self.target_table.heading(col, text=col)
|
||
self.target_table.column(col, width=60, anchor=tk.CENTER)
|
||
self.target_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=self.target_table.yview)
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.target_table.configure(yscrollcommand=scrollbar.set)
|
||
self.target_table.bind("<Double-1>", self.on_target_double_click)
|
||
btn_frame = ttk.Frame(parent)
|
||
btn_frame.pack(fill=tk.X, pady=5)
|
||
ttk.Button(btn_frame, text="Add Target", command=self.open_add_target_dialog).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="Remove Selected", command=self.remove_selected_target).pack(side=tk.LEFT, padx=5)
|
||
|
||
def _create_sim_controls(self, parent):
|
||
self._create_labeled_spinbox(parent, "Pulses per CPI:", self.vars["num_pulses_cpi"], 1, 8192)
|
||
self._create_labeled_spinbox(parent, "Simulation Duration (s):", self.vars["simulation_duration_s"], 0.1, 300.0, increment=0.1)
|
||
auto_scale_check = ttk.Checkbutton(parent, text="Auto-Scale Amplitude", variable=self.vars["auto_scale"], command=self.toggle_amplitude_controls)
|
||
auto_scale_check.pack(fill=tk.X, pady=5)
|
||
self._create_labeled_spinbox(parent, "Min Display Amplitude (dB):", self.vars["min_db"], -200, 100, is_db=True)
|
||
self._create_labeled_spinbox(parent, "Max Display Amplitude (dB):", self.vars["max_db"], -200, 100, is_db=True)
|
||
self.generate_button = ttk.Button(parent, text="Start Simulation", command=self.start_simulation)
|
||
self.generate_button.pack(pady=5)
|
||
self.stop_button = ttk.Button(parent, text="Stop Simulation", command=self.stop_simulation, state=tk.DISABLED)
|
||
self.stop_button.pack(pady=5)
|
||
|
||
def _populate_scan_ppi_controls(self, parent):
|
||
self._create_labeled_spinbox(parent, "PPI Range (m):", self.vars['ppi_range_m'], 100, 150000, command=self.on_ppi_range_change)
|
||
ttk.Label(parent, textvariable=self.vars['scan_info_text'], wraplength=180, justify=tk.LEFT).pack(anchor=tk.W, padx=5, pady=5)
|
||
|
||
def start_simulation(self):
|
||
if self.simulation_manager.is_running():
|
||
return
|
||
self.log.info("Starting simulation...")
|
||
self.generate_button.config(state=tk.DISABLED)
|
||
self.stop_button.config(state=tk.NORMAL)
|
||
radar_cfg = self.get_radar_config_from_gui()
|
||
targets = self.get_targets_from_gui()
|
||
if not targets:
|
||
self.log.warning("Simulation start requested but no targets are defined.")
|
||
messagebox.showwarning("No Targets", "Please add at least one target to simulate.", parent=self)
|
||
self.stop_simulation()
|
||
return
|
||
self.plot_manager.setup_plots_for_simulation(
|
||
self.vars["num_pulses_cpi"].get(), self.vars["min_db"].get(), self.vars["max_db"].get(), targets, radar_cfg
|
||
)
|
||
self.simulation_manager.start(
|
||
radar_cfg, targets, self.vars["simulation_duration_s"].get(), self.vars["num_pulses_cpi"].get()
|
||
)
|
||
self.after(100, self._check_simulation_queue)
|
||
|
||
def stop_simulation(self):
|
||
self.log.info("Stopping simulation...")
|
||
self.simulation_manager.stop()
|
||
self.generate_button.config(state=tk.NORMAL)
|
||
self.stop_button.config(state=tk.DISABLED)
|
||
self.plot_manager.clear_rd_plot()
|
||
self.analysis_text.config(state=tk.NORMAL)
|
||
self.analysis_text.delete(1.0, tk.END)
|
||
self.analysis_text.insert(tk.END, "Simulation stopped or not started.")
|
||
self.analysis_text.config(state=tk.DISABLED)
|
||
|
||
def _check_simulation_queue(self):
|
||
try:
|
||
frame_data = self.simulation_manager.data_queue.get_nowait()
|
||
if frame_data is None:
|
||
self.log.info("Simulation thread finished.")
|
||
self.stop_simulation()
|
||
return
|
||
radar_cfg = self.get_radar_config_from_gui()
|
||
self.plot_manager.update_plots(
|
||
frame_data, radar_cfg, self.vars["auto_scale"].get(), self.vars["min_db"].get(), self.vars["max_db"].get()
|
||
)
|
||
self._update_analysis_text(radar_cfg, self.get_targets_from_gui(), frame_data[0], frame_data[1])
|
||
except queue.Empty:
|
||
pass
|
||
finally:
|
||
if self.simulation_manager.is_running():
|
||
self.after(50, self._check_simulation_queue)
|
||
|
||
def on_ppi_range_change(self, *args):
|
||
if self.plot_manager:
|
||
self.plot_manager.update_ppi_range()
|
||
|
||
def add_target_to_table(self, data):
|
||
self.target_table.insert("", tk.END, values=[f"{v:.2f}" for v in data.values()])
|
||
self.check_target_warnings()
|
||
if self.plot_manager:
|
||
self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
|
||
|
||
def remove_selected_target(self):
|
||
selected_items = self.target_table.selection()
|
||
for i in selected_items:
|
||
self.target_table.delete(i)
|
||
self.check_target_warnings()
|
||
if self.plot_manager:
|
||
self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
|
||
|
||
def get_radar_config_from_gui(self) -> RadarConfig:
|
||
antenna_cfg = AntennaConfig(beamwidth_az_deg=self.vars["beamwidth_az_deg"].get(), beamwidth_el_deg=self.vars["beamwidth_el_deg"].get())
|
||
scan_cfg = ScanConfig(mode=self.vars["scan_mode"].get(), min_az_deg=self.vars["min_az_deg"].get(), max_az_deg=self.vars["max_az_deg"].get(), scan_speed_deg_s=self.vars["scan_speed_deg_s"].get())
|
||
return RadarConfig(carrier_frequency=self.vars["carrier_frequency"].get(), prf=self.vars["prf"].get(), duty_cycle=self.vars["duty_cycle"].get(), sample_rate=self.vars["sample_rate"].get(), antenna_config=antenna_cfg, scan_config=scan_cfg)
|
||
|
||
def get_targets_from_gui(self) -> list[Target]:
|
||
targets = []
|
||
for item in self.target_table.get_children():
|
||
values = self.target_table.item(item)['values']
|
||
try:
|
||
float_values = [float(v) for v in values]
|
||
pos = np.array(float_values[0:3])
|
||
vel = np.array(float_values[3:6])
|
||
rcs = float_values[6]
|
||
targets.append(Target(initial_position=pos, velocity=vel, rcs=rcs))
|
||
except (ValueError, IndexError) as e:
|
||
self.log.error(f"Skipping invalid target data: {values}. Error: {e}")
|
||
messagebox.showwarning("Invalid Data", f"Skipping invalid target data: {values}. Error: {e}", parent=self)
|
||
return targets
|
||
|
||
def _update_analysis_text(self, radar_cfg, targets, current_az_deg, iq_data_cpi):
|
||
report = []
|
||
max_range = radar_math.calculate_max_unambiguous_range(radar_cfg.prf)
|
||
max_vel = radar_math.calculate_max_unambiguous_velocity(radar_cfg.carrier_frequency, radar_cfg.prf)
|
||
report.append(f"--- Radar Configuration Analysis ---")
|
||
report.append(f"Max Unambiguous Range: {max_range:,.0f} m")
|
||
report.append(f"Max Unambiguous Velocity: b1{max_vel:,.1f} m/s")
|
||
report.append(f"Current Antenna Azimuth: {current_az_deg:,.1f} deg")
|
||
if radar_cfg.scan_config.mode != 'staring' and radar_cfg.scan_config.scan_speed_deg_s > 0:
|
||
dwell_time = radar_math.calculate_dwell_time(radar_cfg.antenna_config.beamwidth_az_deg, radar_cfg.scan_config.scan_speed_deg_s)
|
||
pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, radar_cfg.prf)
|
||
report.append(f"Calculated Dwell Time: {dwell_time * 1e3:,.2f} ms")
|
||
report.append(f"Calculated Pulses on Target: {pulses_on_target:,}")
|
||
if pulses_on_target < self.vars["num_pulses_cpi"].get():
|
||
report.append("WARNING: Pulses per CPI > Pulses on Target")
|
||
report.append(f"\n--- Target Analysis ---")
|
||
if not targets:
|
||
report.append("No targets defined.")
|
||
else:
|
||
for i, target in enumerate(targets):
|
||
report.append(f"\nTarget {i+1} (RCS={target.rcs:.1f} m^2) - Analysis based on initial state.")
|
||
self.analysis_text.config(state=tk.NORMAL)
|
||
self.analysis_text.delete(1.0, tk.END)
|
||
self.analysis_text.insert(tk.END, "\n".join(report))
|
||
self.analysis_text.config(state=tk.DISABLED)
|
||
self.analysis_text.see(tk.END)
|
||
|
||
def update_derived_parameters(self, *args):
|
||
try:
|
||
prf = self.vars["prf"].get()
|
||
carrier_freq = self.vars["carrier_frequency"].get()
|
||
duty_cycle = self.vars["duty_cycle"].get()
|
||
beamwidth = self.vars["beamwidth_az_deg"].get()
|
||
scan_mode = self.vars["scan_mode"].get()
|
||
scan_speed = self.vars["scan_speed_deg_s"].get()
|
||
pri = 1.0 / prf
|
||
pulse_width = pri * (duty_cycle / 100.0)
|
||
self.vars["pulse_width_text"].set(f"Pulse Width: {pulse_width * 1e6:, .2f} µs")
|
||
self.vars["listening_time_text"].set(f"Listening Window: {pri * 1e6:, .2f} µs (Max Range Time)")
|
||
max_range = radar_math.calculate_max_unambiguous_range(prf)
|
||
max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf)
|
||
self.vars["max_range_text"].set(f"Max Unambiguous Range: {max_range:, .0f} m")
|
||
self.vars["max_velocity_text"].set(f"Max Unambiguous Velocity: b1{max_vel:, .1f} m/s")
|
||
if scan_mode == 'staring':
|
||
self.vars["dwell_time_text"].set("Dwell Time: N/A (Staring)")
|
||
self.vars["pulses_on_target_text"].set("Pulses on Target: N/A (Staring)")
|
||
else:
|
||
dwell_time = radar_math.calculate_dwell_time(beamwidth, scan_speed)
|
||
pulses_on_target = radar_math.calculate_pulses_on_target(dwell_time, prf)
|
||
self.vars["dwell_time_text"].set(f"Dwell Time: {dwell_time * 1e3:, .2f} ms")
|
||
self.vars["pulses_on_target_text"].set(f"Pulses on Target: {pulses_on_target:,}")
|
||
self.check_target_warnings()
|
||
self._update_scan_info_text()
|
||
except (tk.TclError, ValueError, ZeroDivisionError): pass
|
||
|
||
def _update_scan_info_text(self):
|
||
mode = self.vars['scan_mode'].get()
|
||
if mode == 'staring': self.vars['scan_info_text'].set("Mode: Staring")
|
||
else:
|
||
min_az, max_az, speed = self.vars['min_az_deg'].get(), self.vars['max_az_deg'].get(), self.vars['scan_speed_deg_s'].get()
|
||
self.vars['scan_info_text'].set(f"Mode: Sector Scan\nAz: [{min_az}°, {max_az}°] @ {speed}°/s")
|
||
|
||
def check_target_warnings(self):
|
||
if not hasattr(self, 'target_table'): return
|
||
try:
|
||
prf, carrier_freq = self.vars["prf"].get(), self.vars["carrier_frequency"].get()
|
||
max_range = radar_math.calculate_max_unambiguous_range(prf)
|
||
max_vel = radar_math.calculate_max_unambiguous_velocity(carrier_freq, prf)
|
||
range_warning, vel_warning = False, False
|
||
for item in self.target_table.get_children():
|
||
values = [float(v) for v in self.target_table.item(item)['values']]
|
||
target_initial_range = np.linalg.norm(values[0:3])
|
||
if target_initial_range > 0: target_radial_vel = np.dot(values[3:6], values[0:3] / target_initial_range)
|
||
else: target_radial_vel = 0
|
||
if target_initial_range > max_range: range_warning = True
|
||
if abs(target_radial_vel) > max_vel: vel_warning = True
|
||
self.max_range_label.config(foreground='orange' if range_warning else 'black')
|
||
self.max_velocity_label.config(foreground='orange' if vel_warning else 'black')
|
||
except (tk.TclError, ValueError, ZeroDivisionError):
|
||
if hasattr(self, 'max_range_label'): self.max_range_label.config(foreground='black')
|
||
if hasattr(self, 'max_velocity_label'): self.max_velocity_label.config(foreground='black')
|
||
|
||
def open_add_target_dialog(self):
|
||
dialog = AddTargetDialog(self)
|
||
result = dialog.show()
|
||
if result: self.add_target_to_table(result)
|
||
|
||
def on_target_double_click(self, event):
|
||
selected_item = self.target_table.selection()
|
||
if not selected_item: return
|
||
item_data = self.target_table.item(selected_item[0])['values']
|
||
target_data = { "pos_x": float(item_data[0]), "pos_y": float(item_data[1]), "pos_z": float(item_data[2]), "vel_x": float(item_data[3]), "vel_y": float(item_data[4]), "vel_z": float(item_data[5]), "rcs": float(item_data[6]) }
|
||
dialog = AddTargetDialog(self, target_data=target_data)
|
||
result = dialog.show()
|
||
if result:
|
||
self.target_table.item(selected_item[0], values=[f"{v:.2f}" for v in result.values()])
|
||
self.check_target_warnings()
|
||
if self.plot_manager: self.plot_manager.redraw_ppi_targets(self.get_targets_from_gui())
|
||
|
||
def on_profile_select(self, event=None):
|
||
profile_name = self.selected_profile.get()
|
||
if profile_name in self.profiles:
|
||
profile_data = self.profiles[profile_name]
|
||
for key, value in profile_data.items():
|
||
if key in self.vars: self.vars[key].set(value)
|
||
self.update_scan_mode_controls()
|
||
messagebox.showinfo("Profile Loaded", f"Profile '{profile_name}' has been loaded.", parent=self)
|
||
|
||
def save_profile(self):
|
||
profile_name = simpledialog.askstring("Save Profile", "Enter a name for this profile:", parent=self)
|
||
if not profile_name or not profile_name.strip(): return
|
||
profile_name = profile_name.strip()
|
||
if profile_name in self.profiles:
|
||
if not messagebox.askyesno("Overwrite Profile", f"Profile '{profile_name}' already exists. Overwrite it?", parent=self): return
|
||
current_config = {key: var.get() for key, var in self.vars.items() if isinstance(var, (tk.DoubleVar, tk.StringVar, tk.IntVar))}
|
||
self.profiles[profile_name] = current_config
|
||
if config_manager.save_profiles(self.profiles):
|
||
self.refresh_profile_list()
|
||
self.selected_profile.set(profile_name)
|
||
messagebox.showinfo("Profile Saved", f"Profile '{profile_name}' saved successfully.", parent=self)
|
||
else: messagebox.showerror("Error", "Could not save profiles to file.", parent=self)
|
||
|
||
def delete_profile(self):
|
||
profile_name = self.selected_profile.get()
|
||
if not profile_name: messagebox.showwarning("No Profile Selected", "Please select a profile to delete.", parent=self); return
|
||
if messagebox.askyesno("Delete Profile", f"Are you sure you want to delete the profile '{profile_name}'?", parent=self):
|
||
if profile_name in self.profiles:
|
||
del self.profiles[profile_name]
|
||
if config_manager.save_profiles(self.profiles):
|
||
self.refresh_profile_list()
|
||
messagebox.showinfo("Profile Deleted", f"Profile '{profile_name}' has been deleted.", parent=self)
|
||
else: messagebox.showerror("Error", "Could not save profiles to file.", parent=self)
|
||
|
||
def refresh_profile_list(self):
|
||
self.profile_combobox['values'] = sorted(list(self.profiles.keys()))
|
||
self.selected_profile.set('')
|
||
|
||
def toggle_amplitude_controls(self):
|
||
state = tk.DISABLED if self.vars["auto_scale"].get() else tk.NORMAL
|
||
if hasattr(self, 'min_db_spinbox'): self.min_db_spinbox.config(state=state)
|
||
if hasattr(self, 'max_db_spinbox'): self.max_db_spinbox.config(state=state)
|
||
|
||
def update_scan_mode_controls(self, *args):
|
||
mode = self.vars["scan_mode"].get()
|
||
state = tk.NORMAL if mode != 'staring' else tk.DISABLED
|
||
if mode == 'staring': self.vars["scan_speed_deg_s"].set(0.0001)
|
||
elif self.vars["scan_speed_deg_s"].get() <= 0.0001: self.vars["scan_speed_deg_s"].set(20.0)
|
||
if hasattr(self, 'min_az_spinbox'): self.min_az_spinbox.config(state=state)
|
||
if hasattr(self, 'max_az_spinbox'): self.max_az_spinbox.config(state=state)
|
||
if hasattr(self, 'scan_speed_spinbox'): self.scan_speed_spinbox.config(state=state)
|
||
self.update_derived_parameters()
|
||
|
||
def start_gui():
|
||
"""Entry point to launch the Tkinter GUI application."""
|
||
app = App()
|
||
app.mainloop()
|