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()