/ 9_Firmware / 9_3_GUI / GUI_V6_Demo.py
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()