GUI_V6_Demo.py
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 """ 5 Radar System GUI - Fully Functional Demo Version 6 All buttons work, simulated radar data is generated in real-time 7 """ 8 9 import tkinter as tk 10 from tkinter import ttk, messagebox 11 import threading 12 import queue 13 import time 14 import numpy as np 15 import matplotlib.pyplot as plt 16 from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 17 from matplotlib.figure import Figure 18 import logging 19 from dataclasses import dataclass 20 from typing import List, Dict, Optional 21 import random 22 import json 23 import os 24 from datetime import datetime 25 26 # Configure logging 27 logging.basicConfig(level=logging.INFO, 28 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 29 logger = logging.getLogger(__name__) 30 31 # ============================================================================ 32 # DATA CLASSES 33 # ============================================================================ 34 35 @dataclass 36 class RadarTarget: 37 id: int 38 range: float 39 velocity: float 40 azimuth: float 41 elevation: float 42 snr: float 43 44 @dataclass 45 class RadarSettings: 46 frequency: float = 10.0 # GHz 47 long_chirp_us: float = 30.0 48 short_chirp_us: float = 0.5 49 chirps_per_frame: int = 32 50 range_bins: int = 1024 51 doppler_bins: int = 32 52 prf: float = 1000 53 max_range: float = 5000 54 max_velocity: float = 100 55 cfar_threshold: float = 13.0 56 57 # ============================================================================ 58 # SIMULATED RADAR PROCESSOR 59 # ============================================================================ 60 61 class SimulatedRadarProcessor: 62 """Generates realistic simulated radar data""" 63 64 def __init__(self): 65 self.settings = RadarSettings() 66 self.frame_count = 0 67 self.targets = self._create_targets() 68 self.noise_floor = 10 69 self.clutter_level = 5 70 71 def _create_targets(self) -> List[Dict]: 72 """Create moving targets""" 73 return [ 74 { 75 'id': 1, 76 'range': 2500, 77 'velocity': -80, 78 'azimuth': 45, 79 'elevation': 5, 80 'snr': 25, 81 'range_drift': -0.8, 82 'azimuth_drift': 0.15, 83 'velocity_drift': 0.1 84 }, 85 { 86 'id': 2, 87 'range': 800, 88 'velocity': 15, 89 'azimuth': -30, 90 'elevation': 0, 91 'snr': 18, 92 'range_drift': 0.3, 93 'azimuth_drift': -0.1, 94 'velocity_drift': -0.05 95 }, 96 { 97 'id': 3, 98 'range': 1500, 99 'velocity': 0, 100 'azimuth': 10, 101 'elevation': 2, 102 'snr': 22, 103 'range_drift': 0, 104 'azimuth_drift': 0.05, 105 'velocity_drift': 0 106 }, 107 { 108 'id': 4, 109 'range': 3500, 110 'velocity': 50, 111 'azimuth': -15, 112 'elevation': 3, 113 'snr': 15, 114 'range_drift': 0.5, 115 'azimuth_drift': -0.2, 116 'velocity_drift': -0.3 117 }, 118 { 119 'id': 5, 120 'range': 500, 121 'velocity': -20, 122 'azimuth': 60, 123 'elevation': 1, 124 'snr': 30, 125 'range_drift': -0.2, 126 'azimuth_drift': 0.3, 127 'velocity_drift': 0.2 128 } 129 ] 130 131 def generate_frame(self) -> tuple: 132 """Generate a complete radar frame""" 133 self.frame_count += 1 134 135 # Update target positions 136 for target in self.targets: 137 target['range'] += target['range_drift'] 138 target['azimuth'] += target['azimuth_drift'] 139 target['velocity'] += target['velocity_drift'] 140 141 # Keep within bounds with wrapping/reflection 142 if target['range'] < 100: 143 target['range'] = 100 144 target['range_drift'] *= -1 145 elif target['range'] > 4800: 146 target['range'] = 4800 147 target['range_drift'] *= -1 148 149 if target['azimuth'] < -90: 150 target['azimuth'] = -90 151 target['azimuth_drift'] *= -1 152 elif target['azimuth'] > 90: 153 target['azimuth'] = 90 154 target['azimuth_drift'] *= -1 155 156 if target['velocity'] < -95: 157 target['velocity'] = -95 158 target['velocity_drift'] *= -1 159 elif target['velocity'] > 95: 160 target['velocity'] = 95 161 target['velocity_drift'] *= -1 162 163 # Generate range-Doppler map 164 rd_map = self._generate_range_doppler() 165 166 # Extract detected targets 167 detected = self._detect_targets() 168 169 return rd_map, detected 170 171 def _generate_range_doppler(self) -> np.ndarray: 172 """Generate simulated range-Doppler map""" 173 # Base noise 174 noise = self.noise_floor * np.random.random( 175 (self.settings.range_bins, self.settings.doppler_bins) 176 ) 177 178 # Add clutter (constant at low velocities) 179 clutter = np.zeros_like(noise) 180 clutter[:, 14:18] = self.clutter_level * (0.8 + 0.4 * np.random.random()) 181 182 # Add targets 183 targets = np.zeros_like(noise) 184 for t in self.targets: 185 # Convert to bin indices 186 r_bin = int((t['range'] / self.settings.max_range) * 187 (self.settings.range_bins - 1)) 188 v_bin = int(((t['velocity'] + self.settings.max_velocity) / 189 (2 * self.settings.max_velocity)) * 190 (self.settings.doppler_bins - 1)) 191 192 # Ensure valid indices 193 r_bin = max(0, min(self.settings.range_bins - 1, r_bin)) 194 v_bin = max(0, min(self.settings.doppler_bins - 1, v_bin)) 195 196 # Add target with spreading 197 for dr in range(-2, 3): 198 for dv in range(-2, 3): 199 rr = r_bin + dr 200 vv = v_bin + dv 201 if 0 <= rr < self.settings.range_bins and 0 <= vv < self.settings.doppler_bins: 202 distance = np.sqrt(dr**2 + dv**2) 203 if distance < 2.5: 204 amplitude = t['snr'] * np.exp(-distance/1.5) 205 targets[rr, vv] += amplitude * (0.7 + 0.6 * random.random()) 206 207 # Combine 208 rd_map = noise + clutter + targets 209 210 # Add some range-varying gain 211 range_gain = np.linspace(1, 0.3, self.settings.range_bins) 212 rd_map *= range_gain[:, np.newaxis] 213 214 return rd_map 215 216 def _detect_targets(self) -> List[RadarTarget]: 217 """Detect targets from current state""" 218 detected = [] 219 for t in self.targets: 220 # Random detection based on SNR 221 if random.random() < (t['snr'] / 35): 222 # Add some measurement noise 223 detected.append(RadarTarget( 224 id=t['id'], 225 range=t['range'] + random.gauss(0, 10), 226 velocity=t['velocity'] + random.gauss(0, 2), 227 azimuth=t['azimuth'] + random.gauss(0, 1), 228 elevation=t['elevation'] + random.gauss(0, 0.5), 229 snr=t['snr'] + random.gauss(0, 2) 230 )) 231 return detected 232 233 # ============================================================================ 234 # MAIN GUI APPLICATION 235 # ============================================================================ 236 237 class RadarDemoGUI: 238 def __init__(self, root): 239 self.root = root 240 self.root.title("Radar System Demo - Fully Functional") 241 self.root.geometry("1400x900") 242 243 # Set minimum window size 244 self.root.minsize(1200, 700) 245 246 # Configure style 247 self.style = ttk.Style() 248 self.style.theme_use('clam') 249 250 # Initialize components 251 self.settings = RadarSettings() 252 self.processor = SimulatedRadarProcessor() 253 self.running = False 254 self.recording = False 255 self.frame_count = 0 256 self.fps = 0 257 self.last_frame_time = time.time() 258 self.recorded_frames = [] 259 260 # Data storage 261 self.current_rd_map = np.zeros((1024, 32)) 262 self.current_targets = [] 263 self.target_history = [] 264 265 # Settings variables 266 self.settings_vars = {} 267 268 # Create GUI 269 self.create_menu() 270 self.create_main_layout() 271 self.create_status_bar() 272 273 # Start animation 274 self.animate() 275 276 # Handle window close 277 self.root.protocol("WM_DELETE_WINDOW", self.on_closing) 278 279 logger.info("Radar Demo GUI initialized") 280 281 def create_menu(self): 282 """Create application menu""" 283 menubar = tk.Menu(self.root) 284 self.root.config(menu=menubar) 285 286 # File menu 287 file_menu = tk.Menu(menubar, tearoff=0) 288 menubar.add_cascade(label="File", menu=file_menu) 289 file_menu.add_command(label="Load Configuration", command=self.load_config) 290 file_menu.add_command(label="Save Configuration", command=self.save_config) 291 file_menu.add_separator() 292 file_menu.add_command(label="Export Data", command=self.export_data) 293 file_menu.add_separator() 294 file_menu.add_command(label="Exit", command=self.on_closing) 295 296 # View menu 297 view_menu = tk.Menu(menubar, tearoff=0) 298 menubar.add_cascade(label="View", menu=view_menu) 299 self.show_grid = tk.BooleanVar(value=True) 300 self.show_targets = tk.BooleanVar(value=True) 301 self.color_map = tk.StringVar(value='hot') 302 303 view_menu.add_checkbutton(label="Show Grid", variable=self.show_grid) 304 view_menu.add_checkbutton(label="Show Targets", variable=self.show_targets) 305 view_menu.add_separator() 306 307 # Color map submenu 308 color_menu = tk.Menu(view_menu, tearoff=0) 309 view_menu.add_cascade(label="Color Map", menu=color_menu) 310 for cmap in ['hot', 'jet', 'viridis', 'plasma']: 311 color_menu.add_radiobutton(label=cmap.capitalize(), 312 variable=self.color_map, 313 value=cmap) 314 315 # Tools menu 316 tools_menu = tk.Menu(menubar, tearoff=0) 317 menubar.add_cascade(label="Tools", menu=tools_menu) 318 tools_menu.add_command(label="Calibration", command=self.show_calibration) 319 tools_menu.add_command(label="Diagnostics", command=self.show_diagnostics) 320 tools_menu.add_command(label="Reset Simulation", command=self.reset_simulation) 321 322 # Help menu 323 help_menu = tk.Menu(menubar, tearoff=0) 324 menubar.add_cascade(label="Help", menu=help_menu) 325 help_menu.add_command(label="Documentation", command=self.show_docs) 326 help_menu.add_command(label="About", command=self.show_about) 327 328 def create_main_layout(self): 329 """Create main application layout""" 330 # Main container 331 main_frame = ttk.Frame(self.root) 332 main_frame.pack(fill='both', expand=True, padx=5, pady=5) 333 334 # Control panel (top) 335 control_frame = ttk.LabelFrame(main_frame, text="System Control", padding=5) 336 control_frame.pack(fill='x', pady=(0, 5)) 337 338 self.create_control_panel(control_frame) 339 340 # Notebook for tabs 341 self.notebook = ttk.Notebook(main_frame) 342 self.notebook.pack(fill='both', expand=True) 343 344 # Create tabs 345 self.create_radar_tab() 346 self.create_scope_tab() 347 self.create_spectrum_tab() 348 self.create_settings_tab() 349 350 def create_control_panel(self, parent): 351 """Create control panel with working buttons""" 352 # Left side - Status and controls 353 left_frame = ttk.Frame(parent) 354 left_frame.pack(side='left', fill='x', expand=True) 355 356 # Mode indicator 357 ttk.Label(left_frame, text="Mode:", font=('Arial', 10, 'bold')).grid( 358 row=0, column=0, padx=5, pady=2, sticky='w') 359 self.mode_label = ttk.Label(left_frame, text="DEMO", 360 foreground='green', font=('Arial', 10, 'bold')) 361 self.mode_label.grid(row=0, column=1, padx=5, pady=2, sticky='w') 362 363 # Device indicator 364 ttk.Label(left_frame, text="Device:", font=('Arial', 10)).grid( 365 row=0, column=2, padx=(20,5), pady=2, sticky='w') 366 self.device_label = ttk.Label(left_frame, text="Simulated FT601") 367 self.device_label.grid(row=0, column=3, padx=5, pady=2, sticky='w') 368 369 # Frame counter 370 ttk.Label(left_frame, text="Frame:", font=('Arial', 10)).grid( 371 row=0, column=4, padx=(20,5), pady=2, sticky='w') 372 self.frame_label = ttk.Label(left_frame, text="0") 373 self.frame_label.grid(row=0, column=5, padx=5, pady=2, sticky='w') 374 375 # Right side - Control buttons (ALL WORKING) 376 right_frame = ttk.Frame(parent) 377 right_frame.pack(side='right', padx=10) 378 379 self.start_button = ttk.Button(right_frame, text="▶ START", 380 command=self.start_radar, width=10) 381 self.start_button.pack(side='left', padx=2) 382 383 self.stop_button = ttk.Button(right_frame, text="■ STOP", 384 command=self.stop_radar, width=10, 385 state='disabled') 386 self.stop_button.pack(side='left', padx=2) 387 388 self.record_button = ttk.Button(right_frame, text="● RECORD", 389 command=self.toggle_recording, width=10, 390 state='disabled') 391 self.record_button.pack(side='left', padx=2) 392 393 ttk.Button(right_frame, text="⚙ SETTINGS", 394 command=lambda: self.notebook.select(3)).pack(side='left', padx=2) 395 396 def create_radar_tab(self): 397 """Create main radar display tab""" 398 tab = ttk.Frame(self.notebook) 399 self.notebook.add(tab, text="Radar Display") 400 401 # Main display area 402 display_frame = ttk.Frame(tab) 403 display_frame.pack(fill='both', expand=True, padx=5, pady=5) 404 405 # Range-Doppler map 406 map_frame = ttk.LabelFrame(display_frame, text="Range-Doppler Map", padding=5) 407 map_frame.pack(side='left', fill='both', expand=True) 408 409 # Create matplotlib figure 410 self.rd_fig = Figure(figsize=(8, 6), facecolor='#2b2b2b') 411 self.rd_ax = self.rd_fig.add_subplot(111) 412 self.rd_ax.set_facecolor('#1a1a1a') 413 414 # Initialize plot 415 self.rd_img = self.rd_ax.imshow( 416 np.zeros((1024, 32)), 417 aspect='auto', 418 cmap='hot', 419 extent=[-100, 100, 5000, 0], 420 interpolation='bilinear' 421 ) 422 423 self.rd_ax.set_xlabel('Velocity (m/s)', color='white') 424 self.rd_ax.set_ylabel('Range (m)', color='white') 425 self.rd_ax.set_title('Real-Time Radar Data', color='white', fontsize=12, fontweight='bold') 426 self.rd_ax.tick_params(colors='white') 427 self.rd_ax.grid(True, alpha=0.3) if self.show_grid.get() else None 428 429 # Add colorbar 430 self.rd_cbar = self.rd_fig.colorbar(self.rd_img, ax=self.rd_ax) 431 self.rd_cbar.ax.yaxis.set_tick_params(color='white') 432 self.rd_cbar.ax.set_ylabel('Power (dB)', color='white') 433 plt.setp(plt.getp(self.rd_cbar.ax.axes, 'yticklabels'), color='white') 434 435 # Embed in tkinter 436 self.rd_canvas = FigureCanvasTkAgg(self.rd_fig, map_frame) 437 self.rd_canvas.draw() 438 self.rd_canvas.get_tk_widget().pack(fill='both', expand=True) 439 440 # Target list panel 441 target_frame = ttk.LabelFrame(display_frame, text="Detected Targets", padding=5, width=300) 442 target_frame.pack(side='right', fill='y', padx=(5, 0)) 443 target_frame.pack_propagate(False) 444 445 # Treeview for targets 446 columns = ('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR') 447 self.target_tree = ttk.Treeview(target_frame, columns=columns, show='headings', height=20) 448 449 # Define headings 450 self.target_tree.heading('ID', text='ID') 451 self.target_tree.heading('Range', text='Range (m)') 452 self.target_tree.heading('Velocity', text='Vel (m/s)') 453 self.target_tree.heading('Azimuth', text='Az (°)') 454 self.target_tree.heading('Elevation', text='El (°)') 455 self.target_tree.heading('SNR', text='SNR (dB)') 456 457 # Set column widths 458 self.target_tree.column('ID', width=40, anchor='center') 459 self.target_tree.column('Range', width=80, anchor='center') 460 self.target_tree.column('Velocity', width=80, anchor='center') 461 self.target_tree.column('Azimuth', width=70, anchor='center') 462 self.target_tree.column('Elevation', width=70, anchor='center') 463 self.target_tree.column('SNR', width=70, anchor='center') 464 465 # Add scrollbar 466 scrollbar = ttk.Scrollbar(target_frame, orient='vertical', 467 command=self.target_tree.yview) 468 self.target_tree.configure(yscrollcommand=scrollbar.set) 469 470 self.target_tree.pack(side='left', fill='both', expand=True) 471 scrollbar.pack(side='right', fill='y') 472 473 # Clear targets button 474 ttk.Button(target_frame, text="Clear List", 475 command=self.clear_targets).pack(pady=5) 476 477 def create_scope_tab(self): 478 """Create A-scope tab""" 479 tab = ttk.Frame(self.notebook) 480 self.notebook.add(tab, text="A-Scope") 481 482 # Create figure 483 self.scope_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b') 484 self.scope_ax = self.scope_fig.add_subplot(111) 485 self.scope_ax.set_facecolor('#1a1a1a') 486 487 # Initialize plot 488 self.scope_line, = self.scope_ax.plot([], [], 'g-', linewidth=1.5) 489 self.scope_ax.set_xlim(0, 5000) 490 self.scope_ax.set_ylim(0, 50) 491 self.scope_ax.set_xlabel('Range (m)', color='white') 492 self.scope_ax.set_ylabel('Amplitude (dB)', color='white') 493 self.scope_ax.set_title('Range Profile', color='white', fontsize=12, fontweight='bold') 494 self.scope_ax.grid(True, alpha=0.3) 495 self.scope_ax.tick_params(colors='white') 496 497 self.scope_canvas = FigureCanvasTkAgg(self.scope_fig, tab) 498 self.scope_canvas.draw() 499 self.scope_canvas.get_tk_widget().pack(fill='both', expand=True) 500 501 def create_spectrum_tab(self): 502 """Create Doppler spectrum tab""" 503 tab = ttk.Frame(self.notebook) 504 self.notebook.add(tab, text="Doppler Spectrum") 505 506 # Create figure 507 self.spec_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b') 508 self.spec_ax = self.spec_fig.add_subplot(111) 509 self.spec_ax.set_facecolor('#1a1a1a') 510 511 # Initialize plot 512 self.spec_line, = self.spec_ax.plot([], [], 'b-', linewidth=1.5) 513 self.spec_ax.set_xlim(-100, 100) 514 self.spec_ax.set_ylim(0, 50) 515 self.spec_ax.set_xlabel('Velocity (m/s)', color='white') 516 self.spec_ax.set_ylabel('Power (dB)', color='white') 517 self.spec_ax.set_title('Doppler Spectrum', color='white', fontsize=12, fontweight='bold') 518 self.spec_ax.grid(True, alpha=0.3) 519 self.spec_ax.tick_params(colors='white') 520 521 self.spec_canvas = FigureCanvasTkAgg(self.spec_fig, tab) 522 self.spec_canvas.draw() 523 self.spec_canvas.get_tk_widget().pack(fill='both', expand=True) 524 525 # Range bin selector 526 control_frame = ttk.Frame(tab) 527 control_frame.pack(fill='x', pady=5) 528 529 ttk.Label(control_frame, text="Range Bin:").pack(side='left', padx=5) 530 self.range_slider = ttk.Scale(control_frame, from_=0, to=1023, 531 orient='horizontal', length=400, 532 command=self.update_range_label) 533 self.range_slider.pack(side='left', padx=5) 534 self.range_slider.set(512) 535 536 self.range_label = ttk.Label(control_frame, text="512") 537 self.range_label.pack(side='left', padx=5) 538 539 def create_settings_tab(self): 540 """Create settings tab with working controls""" 541 tab = ttk.Frame(self.notebook) 542 self.notebook.add(tab, text="Settings") 543 544 # Create notebook for settings categories 545 settings_notebook = ttk.Notebook(tab) 546 settings_notebook.pack(fill='both', expand=True, padx=5, pady=5) 547 548 # Radar settings 549 radar_frame = ttk.Frame(settings_notebook) 550 settings_notebook.add(radar_frame, text="Radar") 551 self.create_radar_settings(radar_frame) 552 553 # Display settings 554 display_frame = ttk.Frame(settings_notebook) 555 settings_notebook.add(display_frame, text="Display") 556 self.create_display_settings(display_frame) 557 558 # Simulation settings 559 sim_frame = ttk.Frame(settings_notebook) 560 settings_notebook.add(sim_frame, text="Simulation") 561 self.create_simulation_settings(sim_frame) 562 563 def create_radar_settings(self, parent): 564 """Create radar settings controls""" 565 # Create scrollable frame 566 canvas = tk.Canvas(parent, bg='#2b2b2b', highlightthickness=0) 567 scrollbar = ttk.Scrollbar(parent, orient='vertical', command=canvas.yview) 568 scrollable_frame = ttk.Frame(canvas) 569 570 scrollable_frame.bind( 571 "<Configure>", 572 lambda e: canvas.configure(scrollregion=canvas.bbox("all")) 573 ) 574 575 canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") 576 canvas.configure(yscrollcommand=scrollbar.set) 577 578 # Settings with variables 579 settings = [ 580 ('Frequency (GHz):', 'freq', 10.0, 1.0, 20.0), 581 ('Long Chirp (µs):', 'long_dur', 30.0, 1.0, 100.0), 582 ('Short Chirp (µs):', 'short_dur', 0.5, 0.1, 10.0), 583 ('Chirps/Frame:', 'chirps', 32, 8, 128), 584 ('Range Bins:', 'range_bins', 1024, 256, 2048), 585 ('Doppler Bins:', 'doppler_bins', 32, 8, 128), 586 ('PRF (Hz):', 'prf', 1000, 100, 10000), 587 ('Max Range (m):', 'max_range', 5000, 100, 50000), 588 ('Max Velocity (m/s):', 'max_vel', 100, 10, 500), 589 ('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.0) 590 ] 591 592 for i, (label, key, default, minv, maxv) in enumerate(settings): 593 frame = ttk.Frame(scrollable_frame) 594 frame.pack(fill='x', padx=10, pady=5) 595 596 ttk.Label(frame, text=label, width=20).pack(side='left') 597 598 var = tk.DoubleVar(value=default) 599 self.settings_vars[key] = var 600 601 entry = ttk.Entry(frame, textvariable=var, width=15) 602 entry.pack(side='left', padx=5) 603 604 ttk.Label(frame, text=f"({minv}-{maxv})").pack(side='left') 605 606 canvas.pack(side='left', fill='both', expand=True) 607 scrollbar.pack(side='right', fill='y') 608 609 # Apply button 610 ttk.Button(scrollable_frame, text="Apply Settings", 611 command=self.apply_settings).pack(pady=10) 612 613 def create_display_settings(self, parent): 614 """Create display settings controls""" 615 frame = ttk.Frame(parent) 616 frame.pack(fill='both', expand=True, padx=20, pady=20) 617 618 # Update rate 619 ttk.Label(frame, text="Update Rate (Hz):").grid(row=0, column=0, 620 sticky='w', pady=5) 621 self.update_rate = ttk.Scale(frame, from_=1, to=60, 622 orient='horizontal', length=200) 623 self.update_rate.set(20) 624 self.update_rate.grid(row=0, column=1, padx=10, pady=5) 625 self.update_rate_value = ttk.Label(frame, text="20") 626 self.update_rate_value.grid(row=0, column=2, sticky='w') 627 self.update_rate.configure(command=lambda v: self.update_rate_value.config(text=f"{float(v):.0f}")) 628 629 # Color map 630 ttk.Label(frame, text="Color Map:").grid(row=1, column=0, sticky='w', pady=5) 631 cmap_combo = ttk.Combobox(frame, textvariable=self.color_map, 632 values=['hot', 'jet', 'viridis', 'plasma'], 633 state='readonly', width=15) 634 cmap_combo.grid(row=1, column=1, padx=10, pady=5, sticky='w') 635 636 # Grid 637 ttk.Checkbutton(frame, text="Show Grid", 638 variable=self.show_grid).grid(row=2, column=0, 639 columnspan=2, sticky='w', pady=5) 640 641 # Targets 642 ttk.Checkbutton(frame, text="Show Targets", 643 variable=self.show_targets).grid(row=3, column=0, 644 columnspan=2, sticky='w', pady=5) 645 646 # Apply display button 647 ttk.Button(frame, text="Apply Display Settings", 648 command=self.apply_display_settings).grid(row=4, column=0, 649 columnspan=2, pady=20) 650 651 def create_simulation_settings(self, parent): 652 """Create simulation settings controls""" 653 frame = ttk.Frame(parent) 654 frame.pack(fill='both', expand=True, padx=20, pady=20) 655 656 # Noise floor 657 ttk.Label(frame, text="Noise Floor:").grid(row=0, column=0, sticky='w', pady=5) 658 self.noise_floor = ttk.Scale(frame, from_=0, to=20, 659 orient='horizontal', length=200) 660 self.noise_floor.set(10) 661 self.noise_floor.grid(row=0, column=1, padx=10, pady=5) 662 self.noise_value = ttk.Label(frame, text="10") 663 self.noise_value.grid(row=0, column=2, sticky='w') 664 self.noise_floor.configure(command=lambda v: self.noise_value.config(text=f"{float(v):.1f}")) 665 666 # Clutter level 667 ttk.Label(frame, text="Clutter Level:").grid(row=1, column=0, sticky='w', pady=5) 668 self.clutter_level = ttk.Scale(frame, from_=0, to=20, 669 orient='horizontal', length=200) 670 self.clutter_level.set(5) 671 self.clutter_level.grid(row=1, column=1, padx=10, pady=5) 672 self.clutter_value = ttk.Label(frame, text="5") 673 self.clutter_value.grid(row=1, column=2, sticky='w') 674 self.clutter_level.configure(command=lambda v: self.clutter_value.config(text=f"{float(v):.1f}")) 675 676 # Number of targets 677 ttk.Label(frame, text="Number of Targets:").grid(row=2, column=0, sticky='w', pady=5) 678 self.num_targets = ttk.Scale(frame, from_=1, to=10, 679 orient='horizontal', length=200) 680 self.num_targets.set(5) 681 self.num_targets.grid(row=2, column=1, padx=10, pady=5) 682 self.targets_value = ttk.Label(frame, text="5") 683 self.targets_value.grid(row=2, column=2, sticky='w') 684 self.num_targets.configure(command=lambda v: self.targets_value.config(text=f"{float(v):.0f}")) 685 686 # Reset button 687 ttk.Button(frame, text="Reset Simulation", 688 command=self.reset_simulation).grid(row=3, column=0, 689 columnspan=2, pady=20) 690 691 def create_status_bar(self): 692 """Create status bar at bottom""" 693 status_frame = ttk.Frame(self.root) 694 status_frame.pack(side='bottom', fill='x') 695 696 # Left status 697 self.status_label = ttk.Label(status_frame, text="Status: READY", 698 relief='sunken', padding=2) 699 self.status_label.pack(side='left', fill='x', expand=True) 700 701 # Right indicators 702 self.fps_label = ttk.Label(status_frame, text="FPS: 0", 703 relief='sunken', width=10) 704 self.fps_label.pack(side='right', padx=1) 705 706 self.targets_label = ttk.Label(status_frame, text="Targets: 0", 707 relief='sunken', width=12) 708 self.targets_label.pack(side='right', padx=1) 709 710 self.time_label = ttk.Label(status_frame, text=time.strftime("%H:%M:%S"), 711 relief='sunken', width=8) 712 self.time_label.pack(side='right', padx=1) 713 714 # ============================================================================ 715 # GUI UPDATE METHODS 716 # ============================================================================ 717 718 def animate(self): 719 """Animation loop - updates all displays""" 720 if not hasattr(self, 'animation_running'): 721 self.animation_running = True 722 723 try: 724 # Calculate FPS 725 current_time = time.time() 726 dt = current_time - self.last_frame_time 727 if dt > 0: 728 self.fps = 0.9 * self.fps + 0.1 / dt 729 self.last_frame_time = current_time 730 731 # Update displays if running 732 if self.running: 733 self.update_radar_data() 734 self.frame_count += 1 735 self.frame_label.config(text=str(self.frame_count)) 736 737 # Update status bar 738 self.update_status_bar() 739 740 # Update time 741 self.time_label.config(text=time.strftime("%H:%M:%S")) 742 743 except Exception as e: 744 logger.error(f"Animation error: {e}") 745 746 # Schedule next update 747 update_ms = int(1000 / max(1, self.update_rate.get())) 748 self.root.after(update_ms, self.animate) 749 750 def update_radar_data(self): 751 """Generate and display new radar data""" 752 # Generate frame 753 rd_map, targets = self.processor.generate_frame() 754 755 # Apply simulation settings 756 self.processor.noise_floor = self.noise_floor.get() 757 self.processor.clutter_level = self.clutter_level.get() 758 759 # Store current data 760 self.current_rd_map = rd_map 761 self.current_targets = targets 762 763 # Update range-Doppler map 764 log_map = 10 * np.log10(rd_map + 1) 765 self.rd_img.set_data(log_map) 766 self.rd_img.set_cmap(self.color_map.get()) 767 768 # Update color limits 769 vmin = np.percentile(log_map, 5) 770 vmax = np.percentile(log_map, 95) 771 self.rd_img.set_clim(vmin, vmax) 772 773 # Draw target markers if enabled 774 if self.show_targets.get(): 775 # Clear previous markers 776 for artist in self.rd_ax.lines + self.rd_ax.texts: 777 if hasattr(artist, 'is_target_marker') and artist.is_target_marker: 778 artist.remove() 779 780 # Add new markers 781 for target in targets: 782 x = target.velocity 783 y = target.range 784 marker = self.rd_ax.plot(x, y, 'wo', markersize=8, 785 markeredgecolor='red', markeredgewidth=2)[0] 786 marker.is_target_marker = True 787 text = self.rd_ax.text(x, y-150, str(target.id), color='white', 788 ha='center', va='top', fontsize=8, 789 fontweight='bold') 790 text.is_target_marker = True 791 792 # Update grid 793 if self.show_grid.get(): 794 self.rd_ax.grid(True, alpha=0.3) 795 else: 796 self.rd_ax.grid(False) 797 798 # Update canvas 799 self.rd_canvas.draw_idle() 800 801 # Update target list 802 self.update_target_list() 803 804 # Update A-scope 805 range_profile = np.mean(rd_map, axis=1) 806 range_axis = np.linspace(0, 5000, len(range_profile)) 807 self.scope_line.set_data(range_axis, 10 * np.log10(range_profile + 1)) 808 self.scope_ax.relim() 809 self.scope_ax.autoscale_view(scalex=False) 810 self.scope_canvas.draw_idle() 811 812 # Update Doppler spectrum 813 range_bin = int(self.range_slider.get()) 814 spectrum = rd_map[range_bin, :] 815 vel_axis = np.linspace(-100, 100, len(spectrum)) 816 self.spec_line.set_data(vel_axis, 10 * np.log10(spectrum + 1)) 817 self.spec_ax.relim() 818 self.spec_ax.autoscale_view(scalex=False) 819 self.spec_canvas.draw_idle() 820 821 # Record if enabled 822 if self.recording: 823 self.recorded_frames.append({ 824 'frame': self.frame_count, 825 'time': time.time(), 826 'map': rd_map.copy(), 827 'targets': [(t.range, t.velocity, t.azimuth, t.snr) for t in targets] 828 }) 829 830 def update_target_list(self): 831 """Update the targets treeview""" 832 # Clear existing items 833 for item in self.target_tree.get_children(): 834 self.target_tree.delete(item) 835 836 # Add new targets 837 for target in self.current_targets: 838 values = ( 839 target.id, 840 f"{target.range:.1f}", 841 f"{target.velocity:.1f}", 842 f"{target.azimuth:.1f}", 843 f"{target.elevation:.1f}", 844 f"{target.snr:.1f}" 845 ) 846 self.target_tree.insert('', 'end', values=values) 847 848 # Update targets label 849 self.targets_label.config(text=f"Targets: {len(self.current_targets)}") 850 851 def update_status_bar(self): 852 """Update status bar information""" 853 if self.running: 854 status = "RUNNING" 855 if self.recording: 856 status = "RECORDING" 857 else: 858 status = "READY" 859 860 self.status_label.config(text=f"Status: {status}") 861 self.fps_label.config(text=f"FPS: {self.fps:.1f}") 862 863 def update_range_label(self, value): 864 """Update range bin label""" 865 self.range_label.config(text=f"{int(float(value))}") 866 867 # ============================================================================ 868 # COMMAND HANDLERS (ALL WORKING) 869 # ============================================================================ 870 871 def start_radar(self): 872 """Start radar simulation""" 873 self.running = True 874 self.start_button.config(state='disabled') 875 self.stop_button.config(state='normal') 876 self.record_button.config(state='normal') 877 self.mode_label.config(text="RUNNING", foreground='green') 878 logger.info("Radar started") 879 880 def stop_radar(self): 881 """Stop radar simulation""" 882 self.running = False 883 self.recording = False 884 self.start_button.config(state='normal') 885 self.stop_button.config(state='disabled') 886 self.record_button.config(state='disabled', text='● RECORD') 887 self.mode_label.config(text="STOPPED", foreground='red') 888 logger.info("Radar stopped") 889 890 def toggle_recording(self): 891 """Toggle data recording""" 892 if not self.running: 893 messagebox.showwarning("Warning", "Start radar first") 894 return 895 896 self.recording = not self.recording 897 if self.recording: 898 self.record_button.config(text="● RECORDING", foreground='red') 899 self.recorded_frames = [] # Clear previous recording 900 logger.info("Recording started") 901 else: 902 self.record_button.config(text="● RECORD", foreground='black') 903 logger.info(f"Recording stopped. Captured {len(self.recorded_frames)} frames") 904 905 def clear_targets(self): 906 """Clear target list""" 907 for item in self.target_tree.get_children(): 908 self.target_tree.delete(item) 909 self.current_targets = [] 910 logger.info("Target list cleared") 911 912 def apply_settings(self): 913 """Apply radar settings""" 914 try: 915 self.settings.frequency = self.settings_vars['freq'].get() 916 self.settings.long_chirp_us = self.settings_vars['long_dur'].get() 917 self.settings.short_chirp_us = self.settings_vars['short_dur'].get() 918 self.settings.chirps_per_frame = int(self.settings_vars['chirps'].get()) 919 self.settings.range_bins = int(self.settings_vars['range_bins'].get()) 920 self.settings.doppler_bins = int(self.settings_vars['doppler_bins'].get()) 921 self.settings.prf = self.settings_vars['prf'].get() 922 self.settings.max_range = self.settings_vars['max_range'].get() 923 self.settings.max_velocity = self.settings_vars['max_vel'].get() 924 self.settings.cfar_threshold = self.settings_vars['cfar'].get() 925 926 # Update processor settings 927 self.processor.settings = self.settings 928 929 # Update plot extents 930 self.rd_ax.set_xlim(-self.settings.max_velocity, self.settings.max_velocity) 931 self.rd_ax.set_ylim(self.settings.max_range, 0) 932 self.spec_ax.set_xlim(-self.settings.max_velocity, self.settings.max_velocity) 933 self.scope_ax.set_xlim(0, self.settings.max_range) 934 935 messagebox.showinfo("Success", "Settings applied") 936 logger.info("Settings updated") 937 938 except Exception as e: 939 messagebox.showerror("Error", f"Invalid settings: {e}") 940 941 def apply_display_settings(self): 942 """Apply display settings""" 943 # Update grid 944 if self.show_grid.get(): 945 self.rd_ax.grid(True, alpha=0.3) 946 self.scope_ax.grid(True, alpha=0.3) 947 self.spec_ax.grid(True, alpha=0.3) 948 else: 949 self.rd_ax.grid(False) 950 self.scope_ax.grid(False) 951 self.spec_ax.grid(False) 952 953 # Redraw 954 self.rd_canvas.draw_idle() 955 self.scope_canvas.draw_idle() 956 self.spec_canvas.draw_idle() 957 958 messagebox.showinfo("Success", "Display settings applied") 959 960 def reset_simulation(self): 961 """Reset the simulation""" 962 if messagebox.askyesno("Confirm", "Reset simulation to initial state?"): 963 self.processor = SimulatedRadarProcessor() 964 self.frame_count = 0 965 self.frame_label.config(text="0") 966 self.current_targets = [] 967 self.update_target_list() 968 logger.info("Simulation reset") 969 970 def load_config(self): 971 """Load configuration from file""" 972 from tkinter import filedialog 973 filename = filedialog.askopenfilename( 974 title="Load Configuration", 975 filetypes=[("JSON files", "*.json"), ("All files", "*.*")] 976 ) 977 if filename: 978 try: 979 with open(filename, 'r') as f: 980 config = json.load(f) 981 982 # Apply settings 983 for key, value in config.get('settings', {}).items(): 984 if key in self.settings_vars: 985 self.settings_vars[key].set(value) 986 987 # Apply display settings 988 display = config.get('display', {}) 989 if 'color_map' in display: 990 self.color_map.set(display['color_map']) 991 if 'show_grid' in display: 992 self.show_grid.set(display['show_grid']) 993 if 'show_targets' in display: 994 self.show_targets.set(display['show_targets']) 995 996 self.apply_settings() 997 self.apply_display_settings() 998 999 messagebox.showinfo("Success", f"Loaded configuration from {filename}") 1000 logger.info(f"Configuration loaded from {filename}") 1001 1002 except Exception as e: 1003 messagebox.showerror("Error", f"Failed to load: {e}") 1004 1005 def save_config(self): 1006 """Save configuration to file""" 1007 from tkinter import filedialog 1008 filename = filedialog.asksaveasfilename( 1009 title="Save Configuration", 1010 defaultextension=".json", 1011 filetypes=[("JSON files", "*.json"), ("All files", "*.*")] 1012 ) 1013 if filename: 1014 try: 1015 config = { 1016 'settings': {k: v.get() for k, v in self.settings_vars.items()}, 1017 'display': { 1018 'color_map': self.color_map.get(), 1019 'show_grid': self.show_grid.get(), 1020 'show_targets': self.show_targets.get() 1021 } 1022 } 1023 with open(filename, 'w') as f: 1024 json.dump(config, f, indent=2) 1025 1026 messagebox.showinfo("Success", f"Saved configuration to {filename}") 1027 logger.info(f"Configuration saved to {filename}") 1028 1029 except Exception as e: 1030 messagebox.showerror("Error", f"Failed to save: {e}") 1031 1032 def export_data(self): 1033 """Export recorded data""" 1034 if not self.recorded_frames: 1035 messagebox.showwarning("Warning", "No recorded data to export") 1036 return 1037 1038 from tkinter import filedialog 1039 filename = filedialog.asksaveasfilename( 1040 title="Export Data", 1041 defaultextension=".npz", 1042 filetypes=[("NumPy files", "*.npz"), ("All files", "*.*")] 1043 ) 1044 if filename: 1045 try: 1046 # Prepare data for export 1047 frames = np.array([f['map'] for f in self.recorded_frames]) 1048 times = np.array([f['time'] for f in self.recorded_frames]) 1049 1050 # Save 1051 np.savez(filename, 1052 frames=frames, 1053 times=times, 1054 settings=vars(self.settings)) 1055 1056 messagebox.showinfo("Success", f"Exported {len(frames)} frames to {filename}") 1057 logger.info(f"Data exported to {filename}") 1058 1059 except Exception as e: 1060 messagebox.showerror("Error", f"Failed to export: {e}") 1061 1062 def show_calibration(self): 1063 """Show calibration dialog""" 1064 messagebox.showinfo("Calibration", 1065 "Calibration Wizard\n\n" 1066 "1. Set noise floor\n" 1067 "2. Run noise measurement\n" 1068 "3. Apply calibration factors\n\n" 1069 f"Current noise floor: {self.processor.noise_floor:.1f} dB") 1070 1071 def show_diagnostics(self): 1072 """Show system diagnostics""" 1073 import platform 1074 info = f""" 1075 SYSTEM DIAGNOSTICS 1076 ================= 1077 1078 Radar Status 1079 ------------ 1080 Mode: {'RUNNING' if self.running else 'STOPPED'} 1081 Frames: {self.frame_count} 1082 Targets: {len(self.current_targets)} 1083 FPS: {self.fps:.1f} 1084 1085 Simulation Parameters 1086 --------------------- 1087 Noise Floor: {self.processor.noise_floor:.1f} dB 1088 Clutter Level: {self.processor.clutter_level:.1f} dB 1089 Active Targets: {len(self.processor.targets)} 1090 1091 Display Settings 1092 ---------------- 1093 Color Map: {self.color_map.get()} 1094 Update Rate: {self.update_rate.get():.0f} Hz 1095 Grid: {'On' if self.show_grid.get() else 'Off'} 1096 1097 System Info 1098 ----------- 1099 Platform: {platform.platform()} 1100 Python: {platform.python_version()} 1101 Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} 1102 """ 1103 1104 # Create diagnostics window 1105 diag_window = tk.Toplevel(self.root) 1106 diag_window.title("Diagnostics") 1107 diag_window.geometry("500x600") 1108 1109 text_widget = tk.Text(diag_window, bg='#2b2b2b', fg='#e0e0e0', 1110 font=('Courier', 10), wrap='none') 1111 text_widget.pack(fill='both', expand=True, padx=10, pady=10) 1112 text_widget.insert('1.0', info) 1113 text_widget.config(state='disabled') 1114 1115 # Add scrollbar 1116 scrollbar = ttk.Scrollbar(diag_window, orient='vertical', 1117 command=text_widget.yview) 1118 scrollbar.pack(side='right', fill='y') 1119 text_widget.config(yscrollcommand=scrollbar.set) 1120 1121 def show_docs(self): 1122 """Show documentation""" 1123 docs = """ 1124 RADAR SYSTEM DEMO - USER GUIDE 1125 =============================== 1126 1127 Getting Started 1128 --------------- 1129 1. Click START to begin radar simulation 1130 2. Watch real-time range-Doppler display 1131 3. Detected targets appear in the list 1132 4. Use tabs to view different displays 1133 1134 Controls 1135 -------- 1136 • START/STOP: Control radar simulation 1137 • RECORD: Capture data for export 1138 • SETTINGS: Configure radar parameters 1139 • Clear List: Remove targets from display 1140 1141 Display Tabs 1142 ------------ 1143 • Radar Display: Main range-Doppler view 1144 • A-Scope: Range profile plot 1145 • Doppler Spectrum: Velocity analysis 1146 • Settings: Configure all parameters 1147 1148 Tips 1149 ---- 1150 • Adjust update rate in Display settings 1151 • Change color map for better visibility 1152 • Export recorded data for analysis 1153 • Reset simulation to restart targets 1154 1155 For more information, visit: 1156 https://github.com/radar-system/docs 1157 """ 1158 1159 messagebox.showinfo("Documentation", docs) 1160 1161 def show_about(self): 1162 """Show about dialog""" 1163 about = """ 1164 Radar System Demo 1165 Version 2.0.0 1166 1167 A fully functional radar simulation 1168 and visualization tool. 1169 1170 Features: 1171 • Real-time range-Doppler processing 1172 • Multiple moving targets 1173 • A-scope and spectrum displays 1174 • Data recording and export 1175 • Configurable parameters 1176 1177 Created for demonstration and testing 1178 of radar signal processing concepts. 1179 1180 © 2025 Radar Systems Inc. 1181 """ 1182 1183 messagebox.showinfo("About", about) 1184 1185 def on_closing(self): 1186 """Handle window closing""" 1187 if messagebox.askokcancel("Quit", "Exit radar demo?"): 1188 self.animation_running = False 1189 self.running = False 1190 self.root.destroy() 1191 1192 # ============================================================================ 1193 # MAIN ENTRY POINT 1194 # ============================================================================ 1195 1196 def main(): 1197 """Main application entry point""" 1198 try: 1199 # Create root window 1200 root = tk.Tk() 1201 1202 # Create application 1203 app = RadarDemoGUI(root) 1204 1205 # Center window 1206 root.update_idletasks() 1207 width = root.winfo_width() 1208 height = root.winfo_height() 1209 x = (root.winfo_screenwidth() // 2) - (width // 2) 1210 y = (root.winfo_screenheight() // 2) - (height // 2) 1211 root.geometry(f'{width}x{height}+{x}+{y}') 1212 1213 # Start main loop 1214 root.mainloop() 1215 1216 except Exception as e: 1217 logger.error(f"Fatal error: {e}") 1218 messagebox.showerror("Fatal Error", f"Application failed to start:\n{e}") 1219 1220 if __name__ == "__main__": 1221 main()