/ 9_Firmware / 9_3_GUI / GUI_V4_2_CSV.py
GUI_V4_2_CSV.py
  1  import tkinter as tk
  2  from tkinter import ttk, filedialog, messagebox
  3  import pandas as pd
  4  import numpy as np
  5  import matplotlib.pyplot as plt
  6  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
  7  from matplotlib.figure import Figure
  8  import matplotlib.patches as patches
  9  from scipy import signal
 10  from scipy.fft import fft, fftshift
 11  from scipy.signal import butter, filtfilt
 12  import logging
 13  from dataclasses import dataclass
 14  from typing import List, Dict, Tuple
 15  import threading
 16  import queue
 17  import time
 18  
 19  # Configure logging
 20  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 21  
 22  @dataclass
 23  class RadarTarget:
 24      range: float
 25      velocity: float
 26      azimuth: int
 27      elevation: int
 28      snr: float
 29      chirp_type: str
 30      timestamp: float
 31  
 32  class SignalProcessor:
 33      def __init__(self):
 34          self.range_resolution = 1.0  # meters
 35          self.velocity_resolution = 0.1  # m/s
 36          self.cfar_threshold = 15.0  # dB
 37          
 38      def doppler_fft(self, iq_data: np.ndarray, fs: float = 100e6) -> Tuple[np.ndarray, np.ndarray]:
 39          """
 40          Perform Doppler FFT on IQ data
 41          Returns Doppler frequencies and spectrum
 42          """
 43          # Window function for FFT
 44          window = np.hanning(len(iq_data))
 45          windowed_data = (iq_data['I_value'].values + 1j * iq_data['Q_value'].values) * window
 46          
 47          # Perform FFT
 48          doppler_fft = fft(windowed_data)
 49          doppler_fft = fftshift(doppler_fft)
 50          
 51          # Frequency axis
 52          N = len(iq_data)
 53          freq_axis = np.linspace(-fs/2, fs/2, N)
 54          
 55          # Convert to velocity (assuming radar frequency = 10 GHz)
 56          radar_freq = 10e9
 57          wavelength = 3e8 / radar_freq
 58          velocity_axis = freq_axis * wavelength / 2
 59          
 60          return velocity_axis, np.abs(doppler_fft)
 61      
 62      def mti_filter(self, iq_data: np.ndarray, filter_type: str = 'single_canceler') -> np.ndarray:
 63          """
 64          Moving Target Indicator filter
 65          Removes stationary clutter with better shape handling
 66          """
 67          if iq_data is None or len(iq_data) < 2:
 68              return np.array([], dtype=complex)
 69              
 70          try:
 71              # Ensure we're working with complex data
 72              complex_data = iq_data.astype(complex)
 73              
 74              if filter_type == 'single_canceler':
 75                  # Single delay line canceler
 76                  if len(complex_data) < 2:
 77                      return np.array([], dtype=complex)
 78                  filtered = np.zeros(len(complex_data) - 1, dtype=complex)
 79                  for i in range(1, len(complex_data)):
 80                      filtered[i-1] = complex_data[i] - complex_data[i-1]
 81                  return filtered
 82                  
 83              elif filter_type == 'double_canceler':
 84                  # Double delay line canceler
 85                  if len(complex_data) < 3:
 86                      return np.array([], dtype=complex)
 87                  filtered = np.zeros(len(complex_data) - 2, dtype=complex)
 88                  for i in range(2, len(complex_data)):
 89                      filtered[i-2] = complex_data[i] - 2*complex_data[i-1] + complex_data[i-2]
 90                  return filtered
 91                  
 92              else:
 93                  return complex_data
 94          except Exception as e:
 95              logging.error(f"MTI filter error: {e}")
 96              return np.array([], dtype=complex)
 97  
 98      
 99      def cfar_detection(self, range_profile: np.ndarray, guard_cells: int = 2, 
100                        training_cells: int = 10, threshold_factor: float = 3.0) -> List[Tuple[int, float]]:
101          detections = []
102          N = len(range_profile)
103          
104          # Ensure guard_cells and training_cells are integers
105          guard_cells = int(guard_cells)
106          training_cells = int(training_cells)
107          
108          for i in range(N):
109              # Convert to integer indices
110              i_int = int(i)
111              if i_int < guard_cells + training_cells or i_int >= N - guard_cells - training_cells:
112                  continue
113                  
114              # Leading window - ensure integer indices
115              lead_start = i_int - guard_cells - training_cells
116              lead_end = i_int - guard_cells
117              lead_cells = range_profile[lead_start:lead_end]
118              
119              # Lagging window - ensure integer indices
120              lag_start = i_int + guard_cells + 1
121              lag_end = i_int + guard_cells + training_cells + 1
122              lag_cells = range_profile[lag_start:lag_end]
123              
124              # Combine training cells
125              training_cells_combined = np.concatenate([lead_cells, lag_cells])
126              
127              # Calculate noise floor (mean of training cells)
128              if len(training_cells_combined) > 0:
129                  noise_floor = np.mean(training_cells_combined)
130                  
131                  # Apply threshold
132                  threshold = noise_floor * threshold_factor
133                  
134                  if range_profile[i_int] > threshold:
135                      detections.append((i_int, float(range_profile[i_int])))  # Ensure float magnitude
136          
137          return detections
138      
139      def range_fft(self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6) -> Tuple[np.ndarray, np.ndarray]:
140          """
141          Perform range FFT on IQ data
142          Returns range profile
143          """
144          # Window function
145          window = np.hanning(len(iq_data))
146          windowed_data = np.abs(iq_data) * window
147          
148          # Perform FFT
149          range_fft = fft(windowed_data)
150          
151          # Range calculation
152          N = len(iq_data)
153          range_max = (3e8 * N) / (2 * bw)
154          range_axis = np.linspace(0, range_max, N)
155          
156          return range_axis, np.abs(range_fft)
157      
158      def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = 'LONG') -> Dict:
159          try:
160              # Filter data by chirp type
161              chirp_data = df[df['chirp_type'] == chirp_type]
162              
163              if len(chirp_data) == 0:
164                  return {}
165              
166              # Group by chirp number
167              chirp_numbers = chirp_data['chirp_number'].unique()
168              num_chirps = len(chirp_numbers)
169              
170              if num_chirps == 0:
171                  return {}
172              
173              # Get samples per chirp and ensure consistency
174              samples_per_chirp_list = [len(chirp_data[chirp_data['chirp_number'] == num]) 
175                                      for num in chirp_numbers]
176              
177              # Use minimum samples to ensure consistent shape
178              samples_per_chirp = min(samples_per_chirp_list)
179              
180              # Create range-Doppler matrix with consistent shape
181              range_doppler_matrix = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
182              
183              for i, chirp_num in enumerate(chirp_numbers):
184                  chirp_samples = chirp_data[chirp_data['chirp_number'] == chirp_num]
185                  # Take only the first samples_per_chirp samples to ensure consistent shape
186                  chirp_samples = chirp_samples.head(samples_per_chirp)
187                  
188                  # Create complex IQ data
189                  iq_data = chirp_samples['I_value'].values + 1j * chirp_samples['Q_value'].values
190                  
191                  # Ensure the shape matches
192                  if len(iq_data) == samples_per_chirp:
193                      range_doppler_matrix[:, i] = iq_data
194              
195              # Apply MTI filter along slow-time (chirp-to-chirp)
196              mti_filtered = np.zeros_like(range_doppler_matrix)
197              for i in range(samples_per_chirp):
198                  slow_time_data = range_doppler_matrix[i, :]
199                  filtered = self.mti_filter(slow_time_data)
200                  # Ensure filtered data matches expected shape
201                  if len(filtered) == num_chirps:
202                      mti_filtered[i, :] = filtered
203                  else:
204                      # Handle shape mismatch by padding or truncating
205                      if len(filtered) < num_chirps:
206                          padded = np.zeros(num_chirps, dtype=complex)
207                          padded[:len(filtered)] = filtered
208                          mti_filtered[i, :] = padded
209                      else:
210                          mti_filtered[i, :] = filtered[:num_chirps]
211              
212              # Perform Doppler FFT along slow-time dimension
213              doppler_fft_result = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
214              for i in range(samples_per_chirp):
215                  doppler_fft_result[i, :] = fft(mti_filtered[i, :])
216              
217              return {
218                  'range_doppler_matrix': np.abs(doppler_fft_result),
219                  'chirp_type': chirp_type,
220                  'num_chirps': num_chirps,
221                  'samples_per_chirp': samples_per_chirp
222              }
223          
224          except Exception as e:
225              logging.error(f"Error in process_chirp_sequence: {e}")
226              return {}
227  
228  class RadarGUI:
229      def __init__(self, root):
230          self.root = root
231          self.root.title("Radar Signal Processor - CSV Analysis")
232          self.root.geometry("1400x900")
233          
234          # Initialize processor
235          self.processor = SignalProcessor()
236          
237          # Data storage
238          self.df = None
239          self.processed_data = {}
240          self.detected_targets = []
241          
242          # Create GUI
243          self.create_gui()
244          
245          # Start background processing
246          self.processing_queue = queue.Queue()
247          self.processing_thread = threading.Thread(target=self.background_processing, daemon=True)
248          self.processing_thread.start()
249          
250          # Update GUI periodically
251          self.root.after(100, self.update_gui)
252      
253      def create_gui(self):
254          """Create the main GUI layout"""
255          # Main frame
256          main_frame = ttk.Frame(self.root)
257          main_frame.pack(fill='both', expand=True, padx=10, pady=10)
258          
259          # Control panel
260          control_frame = ttk.LabelFrame(main_frame, text="File Controls")
261          control_frame.pack(fill='x', pady=5)
262          
263          # File selection
264          ttk.Button(control_frame, text="Load CSV File", 
265                    command=self.load_csv_file).pack(side='left', padx=5, pady=5)
266          
267          self.file_label = ttk.Label(control_frame, text="No file loaded")
268          self.file_label.pack(side='left', padx=10, pady=5)
269          
270          # Processing controls
271          ttk.Button(control_frame, text="Process Data", 
272                    command=self.process_data).pack(side='left', padx=5, pady=5)
273          
274          ttk.Button(control_frame, text="Run CFAR Detection", 
275                    command=self.run_cfar_detection).pack(side='left', padx=5, pady=5)
276          
277          # Status
278          self.status_label = ttk.Label(control_frame, text="Status: Ready")
279          self.status_label.pack(side='right', padx=10, pady=5)
280          
281          # Display area
282          display_frame = ttk.Frame(main_frame)
283          display_frame.pack(fill='both', expand=True, pady=5)
284          
285          # Create matplotlib figures
286          self.create_plots(display_frame)
287          
288          # Targets list
289          targets_frame = ttk.LabelFrame(main_frame, text="Detected Targets")
290          targets_frame.pack(fill='x', pady=5)
291          
292          self.targets_tree = ttk.Treeview(targets_frame, 
293                                         columns=('Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR', 'Chirp Type'), 
294                                         show='headings', height=8)
295          
296          self.targets_tree.heading('Range', text='Range (m)')
297          self.targets_tree.heading('Velocity', text='Velocity (m/s)')
298          self.targets_tree.heading('Azimuth', text='Azimuth (°)')
299          self.targets_tree.heading('Elevation', text='Elevation (°)')
300          self.targets_tree.heading('SNR', text='SNR (dB)')
301          self.targets_tree.heading('Chirp Type', text='Chirp Type')
302          
303          self.targets_tree.column('Range', width=100)
304          self.targets_tree.column('Velocity', width=100)
305          self.targets_tree.column('Azimuth', width=80)
306          self.targets_tree.column('Elevation', width=80)
307          self.targets_tree.column('SNR', width=80)
308          self.targets_tree.column('Chirp Type', width=100)
309          
310          self.targets_tree.pack(fill='x', padx=5, pady=5)
311      
312      def create_plots(self, parent):
313          """Create matplotlib plots"""
314          # Create figure with subplots
315          self.fig = Figure(figsize=(12, 8))
316          self.canvas = FigureCanvasTkAgg(self.fig, parent)
317          self.canvas.get_tk_widget().pack(fill='both', expand=True)
318          
319          # Create subplots
320          self.ax1 = self.fig.add_subplot(221)  # Range profile
321          self.ax2 = self.fig.add_subplot(222)  # Doppler spectrum
322          self.ax3 = self.fig.add_subplot(223)  # Range-Doppler map
323          self.ax4 = self.fig.add_subplot(224)  # MTI filtered data
324          
325          # Set titles
326          self.ax1.set_title('Range Profile')
327          self.ax1.set_xlabel('Range (m)')
328          self.ax1.set_ylabel('Magnitude')
329          self.ax1.grid(True)
330          
331          self.ax2.set_title('Doppler Spectrum')
332          self.ax2.set_xlabel('Velocity (m/s)')
333          self.ax2.set_ylabel('Magnitude')
334          self.ax2.grid(True)
335          
336          self.ax3.set_title('Range-Doppler Map')
337          self.ax3.set_xlabel('Doppler Bin')
338          self.ax3.set_ylabel('Range Bin')
339          
340          self.ax4.set_title('MTI Filtered Data')
341          self.ax4.set_xlabel('Sample')
342          self.ax4.set_ylabel('Magnitude')
343          self.ax4.grid(True)
344          
345          self.fig.tight_layout()
346      
347      def load_csv_file(self):
348          """Load CSV file generated by testbench"""
349          filename = filedialog.askopenfilename(
350              title="Select CSV file",
351              filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
352          )
353      
354      # Add magnitude and phase calculations after loading CSV
355          if self.df is not None:
356              # Calculate magnitude from I/Q values
357              self.df['magnitude'] = np.sqrt(self.df['I_value']**2 + self.df['Q_value']**2)
358              
359              # Calculate phase from I/Q values  
360              self.df['phase_rad'] = np.arctan2(self.df['Q_value'], self.df['I_value'])
361              
362              # If you used magnitude_squared in CSV, calculate actual magnitude
363              if 'magnitude_squared' in self.df.columns:
364                  self.df['magnitude'] = np.sqrt(self.df['magnitude_squared'])
365          if filename:
366              try:
367                  self.status_label.config(text="Status: Loading CSV file...")
368                  self.df = pd.read_csv(filename)
369                  self.file_label.config(text=f"Loaded: {filename.split('/')[-1]}")
370                  self.status_label.config(text=f"Status: Loaded {len(self.df)} samples")
371                  
372                  # Show basic info
373                  self.show_file_info()
374                  
375              except Exception as e:
376                  messagebox.showerror("Error", f"Failed to load CSV file: {e}")
377                  self.status_label.config(text="Status: Error loading file")
378      
379      def show_file_info(self):
380          """Display basic information about loaded data"""
381          if self.df is not None:
382              info_text = f"Samples: {len(self.df)} | "
383              info_text += f"Chirps: {self.df['chirp_number'].nunique()} | "
384              info_text += f"Long: {len(self.df[self.df['chirp_type'] == 'LONG'])} | "
385              info_text += f"Short: {len(self.df[self.df['chirp_type'] == 'SHORT'])}"
386              
387              self.file_label.config(text=info_text)
388      
389      def process_data(self):
390          """Process loaded CSV data"""
391          if self.df is None:
392              messagebox.showwarning("Warning", "Please load a CSV file first")
393              return
394          
395          self.status_label.config(text="Status: Processing data...")
396          
397          # Add to processing queue
398          self.processing_queue.put(('process', self.df))
399      
400      def run_cfar_detection(self):
401          """Run CFAR detection on processed data"""
402          if self.df is None:
403              messagebox.showwarning("Warning", "Please load and process data first")
404              return
405          
406          self.status_label.config(text="Status: Running CFAR detection...")
407          self.processing_queue.put(('cfar', self.df))
408      
409      def background_processing(self):
410  
411          while True:
412              try:
413                  task_type, data = self.processing_queue.get(timeout=1.0)
414                  
415                  if task_type == 'process':
416                      self._process_data_background(data)
417                  elif task_type == 'cfar':
418                      self._run_cfar_background(data)
419                  else:
420                      logging.warning(f"Unknown task type: {task_type}")
421                      
422                  self.processing_queue.task_done()
423                      
424              except queue.Empty:
425                  continue
426              except Exception as e:
427                  logging.error(f"Background processing error: {e}")
428                  # Update GUI to show error state
429                  self.root.after(0, lambda: self.status_label.config(
430                      text=f"Status: Processing error - {e}"
431                  ))
432      
433      def _process_data_background(self, df):
434          try:
435              # Process long chirps
436              long_chirp_data = self.processor.process_chirp_sequence(df, 'LONG')
437              
438              # Process short chirps
439              short_chirp_data = self.processor.process_chirp_sequence(df, 'SHORT')
440              
441              # Store results
442              self.processed_data = {
443                  'long': long_chirp_data,
444                  'short': short_chirp_data
445              }
446              
447              # Update GUI in main thread
448              self.root.after(0, self._update_plots_after_processing)
449          
450          except Exception as e:
451              logging.error(f"Processing error: {e}")
452              error_msg = str(e)
453              self.root.after(0, lambda msg=error_msg: self.status_label.config(
454                  text=f"Status: Processing error - {msg}"
455              ))
456      
457      def _run_cfar_background(self, df):
458          try:
459              # Get first chirp for CFAR demonstration
460              first_chirp = df[df['chirp_number'] == df['chirp_number'].min()]
461              
462              if len(first_chirp) == 0:
463                  return
464              
465              # Create IQ data - FIXED TYPO: first_chirp not first_chip
466              iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
467              
468              # Perform range FFT
469              range_axis, range_profile = self.processor.range_fft(iq_data)
470              
471              # Run CFAR detection
472              detections = self.processor.cfar_detection(range_profile)
473              
474              # Convert to target objects
475              self.detected_targets = []
476              for range_bin, magnitude in detections:
477                  target = RadarTarget(
478                      range=range_axis[range_bin],
479                      velocity=0,  # Would need Doppler processing for velocity
480                      azimuth=0,   # From actual data
481                      elevation=0, # From actual data
482                      snr=20 * np.log10(magnitude + 1e-9),  # Convert to dB
483                      chirp_type='LONG',
484                      timestamp=time.time()
485                  )
486                  self.detected_targets.append(target)
487              
488              # Update GUI in main thread
489              self.root.after(0, lambda: self._update_cfar_results(range_axis, range_profile, detections))
490              
491          except Exception as e:
492              logging.error(f"CFAR detection error: {e}")
493              error_msg = str(e)
494              self.root.after(0, lambda msg=error_msg: self.status_label.config(
495                  text=f"Status: CFAR error - {msg}"
496              ))
497      
498      def _update_plots_after_processing(self):
499          try:
500              # Clear all plots
501              for ax in [self.ax1, self.ax2, self.ax3, self.ax4]:
502                  ax.clear()
503              
504              # Plot 1: Range profile from first chirp
505              if self.df is not None and len(self.df) > 0:
506                  try:
507                      first_chirp_num = self.df['chirp_number'].min()
508                      first_chirp = self.df[self.df['chirp_number'] == first_chirp_num]
509                      
510                      if len(first_chirp) > 0:
511                          iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
512                          range_axis, range_profile = self.processor.range_fft(iq_data)
513                          
514                          if len(range_axis) > 0 and len(range_profile) > 0:
515                              self.ax1.plot(range_axis, range_profile, 'b-')
516                              self.ax1.set_title('Range Profile - First Chirp')
517                              self.ax1.set_xlabel('Range (m)')
518                              self.ax1.set_ylabel('Magnitude')
519                              self.ax1.grid(True)
520                  except Exception as e:
521                      logging.warning(f"Range profile plot error: {e}")
522                      self.ax1.set_title('Range Profile - Error')
523              
524              # Plot 2: Doppler spectrum
525              if self.df is not None and len(self.df) > 0:
526                  try:
527                      sample_data = self.df.head(1024)
528                      if len(sample_data) > 10:
529                          iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
530                          velocity_axis, doppler_spectrum = self.processor.doppler_fft(iq_data)
531                          
532                          if len(velocity_axis) > 0 and len(doppler_spectrum) > 0:
533                              self.ax2.plot(velocity_axis, doppler_spectrum, 'g-')
534                              self.ax2.set_title('Doppler Spectrum')
535                              self.ax2.set_xlabel('Velocity (m/s)')
536                              self.ax2.set_ylabel('Magnitude')
537                              self.ax2.grid(True)
538                  except Exception as e:
539                      logging.warning(f"Doppler spectrum plot error: {e}")
540                      self.ax2.set_title('Doppler Spectrum - Error')
541              
542              # Plot 3: Range-Doppler map
543              if (self.processed_data.get('long') and 
544                  'range_doppler_matrix' in self.processed_data['long'] and
545                  self.processed_data['long']['range_doppler_matrix'].size > 0):
546                  
547                  try:
548                      rd_matrix = self.processed_data['long']['range_doppler_matrix']
549                      # Use integer indices for extent
550                      extent = [0, int(rd_matrix.shape[1]), 0, int(rd_matrix.shape[0])]
551                      
552                      im = self.ax3.imshow(10 * np.log10(rd_matrix + 1e-9), 
553                                         aspect='auto', cmap='hot',
554                                         extent=extent)
555                      self.ax3.set_title('Range-Doppler Map (Long Chirps)')
556                      self.ax3.set_xlabel('Doppler Bin')
557                      self.ax3.set_ylabel('Range Bin')
558                      self.fig.colorbar(im, ax=self.ax3, label='dB')
559                  except Exception as e:
560                      logging.warning(f"Range-Doppler map plot error: {e}")
561                      self.ax3.set_title('Range-Doppler Map - Error')
562              
563              # Plot 4: MTI filtered data
564              if self.df is not None and len(self.df) > 0:
565                  try:
566                      sample_data = self.df.head(100)
567                      if len(sample_data) > 10:
568                          iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
569                          
570                          # Original data
571                          original_mag = np.abs(iq_data)
572                          
573                          # MTI filtered
574                          mti_filtered = self.processor.mti_filter(iq_data)
575                          
576                          if mti_filtered is not None and len(mti_filtered) > 0:
577                              mti_mag = np.abs(mti_filtered)
578                              
579                              # Use integer indices for plotting
580                              x_original = np.arange(len(original_mag))
581                              x_mti = np.arange(len(mti_mag))
582                              
583                              self.ax4.plot(x_original, original_mag, 'b-', label='Original', alpha=0.7)
584                              self.ax4.plot(x_mti, mti_mag, 'r-', label='MTI Filtered', alpha=0.7)
585                              self.ax4.set_title('MTI Filter Comparison')
586                              self.ax4.set_xlabel('Sample Index')
587                              self.ax4.set_ylabel('Magnitude')
588                              self.ax4.legend()
589                              self.ax4.grid(True)
590                  except Exception as e:
591                      logging.warning(f"MTI filter plot error: {e}")
592                      self.ax4.set_title('MTI Filter - Error')
593              
594              # Adjust layout and draw
595              self.fig.tight_layout()
596              self.canvas.draw()
597              self.status_label.config(text="Status: Processing complete")
598              
599          except Exception as e:
600              logging.error(f"Plot update error: {e}")
601              error_msg = str(e)
602              self.status_label.config(text=f"Status: Plot error - {error_msg}")
603      
604      def _update_cfar_results(self, range_axis, range_profile, detections):
605          try:
606              # Clear the plot
607              self.ax1.clear()
608              
609              # Plot range profile
610              self.ax1.plot(range_axis, range_profile, 'b-', label='Range Profile')
611              
612              # Plot detections - ensure we use integer indices
613              if detections and len(range_axis) > 0:
614                  detection_ranges = []
615                  detection_mags = []
616                  
617                  for bin_idx, mag in detections:
618                      # Convert bin_idx to integer and ensure it's within bounds
619                      bin_idx_int = int(bin_idx)
620                      if 0 <= bin_idx_int < len(range_axis):
621                          detection_ranges.append(range_axis[bin_idx_int])
622                          detection_mags.append(mag)
623                  
624                  if detection_ranges:  # Only plot if we have valid detections
625                      self.ax1.plot(detection_ranges, detection_mags, 'ro', 
626                                  markersize=8, label='CFAR Detections')
627              
628              self.ax1.set_title('Range Profile with CFAR Detections')
629              self.ax1.set_xlabel('Range (m)')
630              self.ax1.set_ylabel('Magnitude')
631              self.ax1.legend()
632              self.ax1.grid(True)
633              
634              # Update targets list
635              self.update_targets_list()
636              
637              self.canvas.draw()
638              self.status_label.config(text=f"Status: CFAR complete - {len(detections)} targets detected")
639              
640          except Exception as e:
641              logging.error(f"CFAR results update error: {e}")
642              error_msg = str(e)
643              self.status_label.config(text=f"Status: CFAR results error - {error_msg}")
644      
645      def update_targets_list(self):
646          """Update the targets list display"""
647          # Clear current list
648          for item in self.targets_tree.get_children():
649              self.targets_tree.delete(item)
650          
651          # Add detected targets
652          for i, target in enumerate(self.detected_targets):
653              self.targets_tree.insert('', 'end', values=(
654                  f"{target.range:.1f}",
655                  f"{target.velocity:.1f}",
656                  f"{target.azimuth}",
657                  f"{target.elevation}",
658                  f"{target.snr:.1f}",
659                  target.chirp_type
660              ))
661      
662      def update_gui(self):
663          """Periodic GUI update"""
664          # You can add any periodic updates here
665          self.root.after(100, self.update_gui)
666  
667  def main():
668      """Main application entry point"""
669      try:
670          root = tk.Tk()
671          app = RadarGUI(root)
672          root.mainloop()
673      except Exception as e:
674          logging.error(f"Application error: {e}")
675          messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
676  
677  if __name__ == "__main__":
678      main()