GUI_V4.py
1 import tkinter as tk 2 from tkinter import ttk, messagebox 3 import threading 4 import queue 5 import time 6 import struct 7 import numpy as np 8 import matplotlib.pyplot as plt 9 from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 10 from matplotlib.figure import Figure 11 import matplotlib.patches as patches 12 import logging 13 from dataclasses import dataclass 14 from typing import Dict, List, Tuple, Optional 15 from scipy import signal 16 from sklearn.cluster import DBSCAN 17 from filterpy.kalman import KalmanFilter 18 import crcmod 19 import math 20 import webbrowser 21 import tempfile 22 import os 23 24 try: 25 import usb.core 26 import usb.util 27 USB_AVAILABLE = True 28 except ImportError: 29 USB_AVAILABLE = False 30 logging.warning("pyusb not available. USB CDC functionality will be disabled.") 31 32 try: 33 from pyftdi.ftdi import Ftdi 34 from pyftdi.usbtools import UsbTools 35 FTDI_AVAILABLE = True 36 except ImportError: 37 FTDI_AVAILABLE = False 38 logging.warning("pyftdi not available. FTDI functionality will be disabled.") 39 40 # Configure logging 41 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 42 43 @dataclass 44 class RadarTarget: 45 id: int 46 range: float 47 velocity: float 48 azimuth: int 49 elevation: int 50 latitude: float = 0.0 51 longitude: float = 0.0 52 snr: float = 0.0 53 timestamp: float = 0.0 54 track_id: int = -1 55 56 @dataclass 57 class RadarSettings: 58 system_frequency: float = 10e9 59 chirp_duration_1: float = 30e-6 # Long chirp duration 60 chirp_duration_2: float = 0.5e-6 # Short chirp duration 61 chirps_per_position: int = 32 62 freq_min: float = 10e6 63 freq_max: float = 30e6 64 prf1: float = 1000 65 prf2: float = 2000 66 max_distance: float = 50000 67 map_size: float = 50000 # Map size in meters 68 69 @dataclass 70 class GPSData: 71 latitude: float 72 longitude: float 73 altitude: float 74 pitch: float # Pitch angle in degrees 75 timestamp: float 76 77 class MapGenerator: 78 def __init__(self): 79 self.map_html_template = """ 80 <!DOCTYPE html> 81 <html> 82 <head> 83 <title>Radar Map</title> 84 <meta charset="utf-8"> 85 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 86 <style> 87 #map {{ 88 height: 100vh; 89 width: 100%; 90 }} 91 .radar-marker {{ 92 background-color: red; 93 border: 2px solid white; 94 border-radius: 50%; 95 width: 12px; 96 height: 12px; 97 }} 98 .target-marker {{ 99 background-color: blue; 100 border: 2px solid white; 101 border-radius: 50%; 102 width: 8px; 103 height: 8px; 104 }} 105 .info-window {{ 106 font-family: Arial, sans-serif; 107 font-size: 12px; 108 }} 109 </style> 110 </head> 111 <body> 112 <div id="map"></div> 113 114 <script> 115 var map; 116 var radarMarker; 117 var coverageCircle; 118 var targetMarkers = []; 119 120 function initMap() {{ 121 var radarPosition = {{lat: {lat}, lng: {lon}}}; 122 123 map = new google.maps.Map(document.getElementById('map'), {{ 124 center: radarPosition, 125 zoom: 12, 126 mapTypeId: google.maps.MapTypeId.ROADMAP 127 }}); 128 129 // Radar position marker 130 radarMarker = new google.maps.Marker({{ 131 position: radarPosition, 132 map: map, 133 title: 'Radar System', 134 icon: {{ 135 path: google.maps.SymbolPath.CIRCLE, 136 scale: 8, 137 fillColor: '#FF0000', 138 fillOpacity: 1, 139 strokeColor: '#FFFFFF', 140 strokeWeight: 2 141 }} 142 }}); 143 144 // Radar coverage area 145 coverageCircle = new google.maps.Circle({{ 146 strokeColor: '#FF0000', 147 strokeOpacity: 0.8, 148 strokeWeight: 2, 149 fillColor: '#FF0000', 150 fillOpacity: 0.1, 151 map: map, 152 center: radarPosition, 153 radius: {coverage_radius} 154 }}); 155 156 // Info window for radar 157 var radarInfo = new google.maps.InfoWindow({{ 158 content: ` 159 <div class="info-window"> 160 <h3>Radar System</h3> 161 <p>Lat: {lat:.6f}</p> 162 <p>Lon: {lon:.6f}</p> 163 <p>Alt: {alt:.1f}m</p> 164 <p>Pitch: {pitch:+.1f}°</p> 165 <p>Coverage: {coverage_radius/1000:.1f}km</p> 166 </div> 167 ` 168 }}); 169 170 radarMarker.addListener('click', function() {{ 171 radarInfo.open(map, radarMarker); 172 }}); 173 174 // Add existing targets 175 {targets_script} 176 }} 177 178 function updateTargets(targets) {{ 179 // Clear existing targets 180 targetMarkers.forEach(marker => marker.setMap(null)); 181 targetMarkers = []; 182 183 // Add new targets 184 targets.forEach(target => {{ 185 var targetMarker = new google.maps.Marker({{ 186 position: {{lat: target.lat, lng: target.lng}}, 187 map: map, 188 title: `Target: ${'{'}$'{'}'{target.range:.1f}m, ${'{'}$'{'}'{target.velocity:.1f}m/s`, 189 icon: {{ 190 path: google.maps.SymbolPath.CIRCLE, 191 scale: 6, 192 fillColor: '#0000FF', 193 fillOpacity: 0.8, 194 strokeColor: '#FFFFFF', 195 strokeWeight: 1 196 }} 197 }}); 198 199 var targetInfo = new google.maps.InfoWindow({{ 200 content: ` 201 <div class="info-window"> 202 <h3>Target #{target.id}</h3> 203 <p>Range: ${'{'}$'{'}'{target.range:.1f}m</p> 204 <p>Velocity: ${'{'}$'{'}'{target.velocity:.1f}m/s</p> 205 <p>Azimuth: ${'{'}$'{'}'{target.azimuth}°</p> 206 <p>Elevation: ${'{'}$'{'}'{target.elevation:.1f}°</p> 207 <p>SNR: ${'{'}$'{'}'{target.snr:.1f}dB</p> 208 </div> 209 ` 210 }}); 211 212 targetMarker.addListener('click', function() {{ 213 targetInfo.open(map, targetMarker); 214 }}); 215 216 targetMarkers.push(targetMarker); 217 }}); 218 }} 219 220 function updateRadarPosition(lat, lon, alt, pitch) {{ 221 var newPosition = new google.maps.LatLng(lat, lon); 222 radarMarker.setPosition(newPosition); 223 coverageCircle.setCenter(newPosition); 224 map.setCenter(newPosition); 225 }} 226 </script> 227 228 <script async defer 229 src="https://maps.googleapis.com/maps/api/js?key={api_key}&callback=initMap"> 230 </script> 231 </body> 232 </html> 233 """ 234 235 def generate_map(self, gps_data, targets, coverage_radius, api_key="YOUR_GOOGLE_MAPS_API_KEY"): 236 """Generate HTML map with radar and targets""" 237 # Convert targets to map coordinates 238 map_targets = [] 239 for target in targets: 240 # Convert polar coordinates (range, azimuth) to geographic coordinates 241 target_lat, target_lon = self.polar_to_geographic( 242 gps_data.latitude, gps_data.longitude, 243 target.range, target.azimuth 244 ) 245 map_targets.append({ 246 'id': target.track_id, 247 'lat': target_lat, 248 'lng': target_lon, 249 'range': target.range, 250 'velocity': target.velocity, 251 'azimuth': target.azimuth, 252 'elevation': target.elevation, 253 'snr': target.snr 254 }) 255 256 # Generate targets script 257 targets_script = "" 258 if map_targets: 259 targets_json = str(map_targets).replace("'", '"') 260 targets_script = f"updateTargets({targets_json});" 261 262 # Fill template 263 map_html = self.map_html_template.format( 264 lat=gps_data.latitude, 265 lon=gps_data.longitude, 266 alt=gps_data.altitude, 267 pitch=gps_data.pitch, 268 coverage_radius=coverage_radius, 269 targets_script=targets_script, 270 api_key=api_key 271 ) 272 273 return map_html 274 275 def polar_to_geographic(self, radar_lat, radar_lon, range_m, azimuth_deg): 276 """ 277 Convert polar coordinates (range, azimuth) to geographic coordinates 278 using simple flat-earth approximation (good for small distances) 279 """ 280 # Earth radius in meters 281 earth_radius = 6371000 282 283 # Convert azimuth to radians (0° = North, 90° = East) 284 azimuth_rad = math.radians(90 - azimuth_deg) # Convert to math convention 285 286 # Convert range to angular distance 287 angular_distance = range_m / earth_radius 288 289 # Convert to geographic coordinates 290 target_lat = radar_lat + math.cos(azimuth_rad) * angular_distance * (180 / math.pi) 291 target_lon = radar_lon + math.sin(azimuth_rad) * angular_distance * (180 / math.pi) / math.cos(math.radians(radar_lat)) 292 293 return target_lat, target_lon 294 295 class STM32USBInterface: 296 def __init__(self): 297 self.device = None 298 self.is_open = False 299 self.ep_in = None 300 self.ep_out = None 301 302 def list_devices(self): 303 """List available STM32 USB CDC devices""" 304 if not USB_AVAILABLE: 305 logging.warning("USB not available - please install pyusb") 306 return [] 307 308 try: 309 devices = [] 310 # STM32 USB CDC devices typically use these vendor/product IDs 311 stm32_vid_pids = [ 312 (0x0483, 0x5740), # STM32 Virtual COM Port 313 (0x0483, 0x3748), # STM32 Discovery 314 (0x0483, 0x374B), # STM32 CDC 315 (0x0483, 0x374D), # STM32 CDC 316 (0x0483, 0x374E), # STM32 CDC 317 (0x0483, 0x3752), # STM32 CDC 318 ] 319 320 for vid, pid in stm32_vid_pids: 321 found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid) 322 for dev in found_devices: 323 try: 324 product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC" 325 serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown" 326 devices.append({ 327 'description': f"{product} ({serial})", 328 'vendor_id': vid, 329 'product_id': pid, 330 'device': dev 331 }) 332 except: 333 devices.append({ 334 'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", 335 'vendor_id': vid, 336 'product_id': pid, 337 'device': dev 338 }) 339 340 return devices 341 except Exception as e: 342 logging.error(f"Error listing USB devices: {e}") 343 # Return mock devices for testing 344 return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}] 345 346 def open_device(self, device_info): 347 """Open STM32 USB CDC device""" 348 if not USB_AVAILABLE: 349 logging.error("USB not available - cannot open device") 350 return False 351 352 try: 353 self.device = device_info['device'] 354 355 # Detach kernel driver if active 356 if self.device.is_kernel_driver_active(0): 357 self.device.detach_kernel_driver(0) 358 359 # Set configuration 360 self.device.set_configuration() 361 362 # Get CDC endpoints 363 cfg = self.device.get_active_configuration() 364 intf = cfg[(0,0)] 365 366 # Find bulk endpoints (CDC data interface) 367 self.ep_out = usb.util.find_descriptor( 368 intf, 369 custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT 370 ) 371 372 self.ep_in = usb.util.find_descriptor( 373 intf, 374 custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN 375 ) 376 377 if self.ep_out is None or self.ep_in is None: 378 logging.error("Could not find CDC endpoints") 379 return False 380 381 self.is_open = True 382 logging.info(f"STM32 USB device opened: {device_info['description']}") 383 return True 384 385 except Exception as e: 386 logging.error(f"Error opening USB device: {e}") 387 return False 388 389 def send_start_flag(self): 390 """Step 12: Send start flag to STM32 via USB""" 391 start_packet = bytes([23, 46, 158, 237]) 392 logging.info("Sending start flag to STM32 via USB...") 393 return self._send_data(start_packet) 394 395 def send_settings(self, settings): 396 """Step 13: Send radar settings to STM32 via USB""" 397 try: 398 packet = self._create_settings_packet(settings) 399 logging.info("Sending radar settings to STM32 via USB...") 400 return self._send_data(packet) 401 except Exception as e: 402 logging.error(f"Error sending settings via USB: {e}") 403 return False 404 405 def read_data(self, size=64, timeout=1000): 406 """Read data from STM32 via USB""" 407 if not self.is_open or self.ep_in is None: 408 return None 409 410 try: 411 data = self.ep_in.read(size, timeout=timeout) 412 return bytes(data) 413 except usb.core.USBError as e: 414 if e.errno == 110: # Timeout 415 return None 416 logging.error(f"USB read error: {e}") 417 return None 418 except Exception as e: 419 logging.error(f"Error reading from USB: {e}") 420 return None 421 422 def _send_data(self, data): 423 """Send data to STM32 via USB""" 424 if not self.is_open or self.ep_out is None: 425 return False 426 427 try: 428 # USB CDC typically uses 64-byte packets 429 packet_size = 64 430 for i in range(0, len(data), packet_size): 431 chunk = data[i:i + packet_size] 432 # Pad to packet size if needed 433 if len(chunk) < packet_size: 434 chunk += b'\x00' * (packet_size - len(chunk)) 435 self.ep_out.write(chunk) 436 437 return True 438 except Exception as e: 439 logging.error(f"Error sending data via USB: {e}") 440 return False 441 442 def _create_settings_packet(self, settings): 443 """Create binary settings packet for USB transmission""" 444 packet = b'SET' 445 packet += struct.pack('>d', settings.system_frequency) 446 packet += struct.pack('>d', settings.chirp_duration_1) 447 packet += struct.pack('>d', settings.chirp_duration_2) 448 packet += struct.pack('>I', settings.chirps_per_position) 449 packet += struct.pack('>d', settings.freq_min) 450 packet += struct.pack('>d', settings.freq_max) 451 packet += struct.pack('>d', settings.prf1) 452 packet += struct.pack('>d', settings.prf2) 453 packet += struct.pack('>d', settings.max_distance) 454 packet += struct.pack('>d', settings.map_size) 455 packet += b'END' 456 return packet 457 458 def close(self): 459 """Close USB device""" 460 if self.device and self.is_open: 461 try: 462 usb.util.dispose_resources(self.device) 463 self.is_open = False 464 except Exception as e: 465 logging.error(f"Error closing USB device: {e}") 466 467 class FTDIInterface: 468 def __init__(self): 469 self.ftdi = None 470 self.is_open = False 471 472 def list_devices(self): 473 """List available FTDI devices using pyftdi""" 474 if not FTDI_AVAILABLE: 475 logging.warning("FTDI not available - please install pyftdi") 476 return [] 477 478 try: 479 devices = [] 480 # Get list of all FTDI devices 481 for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID 482 devices.append({ 483 'description': f"FTDI Device {device}", 484 'url': f"ftdi://{device}/1" 485 }) 486 return devices 487 except Exception as e: 488 logging.error(f"Error listing FTDI devices: {e}") 489 # Return mock devices for testing 490 return [{'description': 'FT2232H Device A', 'url': 'ftdi://device/1'}] 491 492 def open_device(self, device_url): 493 """Open FTDI device using pyftdi""" 494 if not FTDI_AVAILABLE: 495 logging.error("FTDI not available - cannot open device") 496 return False 497 498 try: 499 self.ftdi = Ftdi() 500 self.ftdi.open_from_url(device_url) 501 502 # Configure for synchronous FIFO mode 503 self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.SYNCFF) 504 505 # Set latency timer 506 self.ftdi.set_latency_timer(2) 507 508 # Purge buffers 509 self.ftdi.purge_buffers() 510 511 self.is_open = True 512 logging.info(f"FTDI device opened: {device_url}") 513 return True 514 515 except Exception as e: 516 logging.error(f"Error opening FTDI device: {e}") 517 return False 518 519 def read_data(self, bytes_to_read): 520 """Read data from FTDI""" 521 if not self.is_open or self.ftdi is None: 522 return None 523 524 try: 525 data = self.ftdi.read_data(bytes_to_read) 526 if data: 527 return bytes(data) 528 return None 529 except Exception as e: 530 logging.error(f"Error reading from FTDI: {e}") 531 return None 532 533 def close(self): 534 """Close FTDI device""" 535 if self.ftdi and self.is_open: 536 self.ftdi.close() 537 self.is_open = False 538 539 class RadarProcessor: 540 def __init__(self): 541 self.range_doppler_map = np.zeros((1024, 32)) 542 self.detected_targets = [] 543 self.track_id_counter = 0 544 self.tracks = {} 545 self.frame_count = 0 546 547 def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): 548 """Dual-CPI fusion for better detection""" 549 fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) 550 return fused_profile 551 552 def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): 553 """Multi-PRF velocity unwrapping""" 554 lambda_wavelength = 3e8 / 10e9 555 v_max1 = prf1 * lambda_wavelength / 2 556 v_max2 = prf2 * lambda_wavelength / 2 557 558 unwrapped_velocities = [] 559 for doppler in doppler_measurements: 560 v1 = doppler * lambda_wavelength / 2 561 v2 = doppler * lambda_wavelength / 2 562 563 velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2) 564 unwrapped_velocities.append(velocity) 565 566 return unwrapped_velocities 567 568 def _solve_chinese_remainder(self, v1, v2, max1, max2): 569 for k in range(-5, 6): 570 candidate = v1 + k * max1 571 if abs(candidate - v2) < max2 / 2: 572 return candidate 573 return v1 574 575 def clustering(self, detections, eps=100, min_samples=2): 576 """DBSCAN clustering of detections""" 577 if len(detections) == 0: 578 return [] 579 580 points = np.array([[d.range, d.velocity] for d in detections]) 581 clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(points) 582 583 clusters = [] 584 for label in set(clustering.labels_): 585 if label != -1: 586 cluster_points = points[clustering.labels_ == label] 587 clusters.append({ 588 'center': np.mean(cluster_points, axis=0), 589 'points': cluster_points, 590 'size': len(cluster_points) 591 }) 592 593 return clusters 594 595 def association(self, detections, clusters): 596 """Association of detections to tracks""" 597 associated_detections = [] 598 599 for detection in detections: 600 best_track = None 601 min_distance = float('inf') 602 603 for track_id, track in self.tracks.items(): 604 distance = np.sqrt( 605 (detection.range - track['state'][0])**2 + 606 (detection.velocity - track['state'][2])**2 607 ) 608 609 if distance < min_distance and distance < 500: 610 min_distance = distance 611 best_track = track_id 612 613 if best_track is not None: 614 detection.track_id = best_track 615 associated_detections.append(detection) 616 else: 617 detection.track_id = self.track_id_counter 618 self.track_id_counter += 1 619 associated_detections.append(detection) 620 621 return associated_detections 622 623 def tracking(self, associated_detections): 624 """Kalman filter tracking""" 625 current_time = time.time() 626 627 for detection in associated_detections: 628 if detection.track_id not in self.tracks: 629 kf = KalmanFilter(dim_x=4, dim_z=2) 630 kf.x = np.array([detection.range, 0, detection.velocity, 0]) 631 kf.F = np.array([[1, 1, 0, 0], 632 [0, 1, 0, 0], 633 [0, 0, 1, 1], 634 [0, 0, 0, 1]]) 635 kf.H = np.array([[1, 0, 0, 0], 636 [0, 0, 1, 0]]) 637 kf.P *= 1000 638 kf.R = np.diag([10, 1]) 639 kf.Q = np.eye(4) * 0.1 640 641 self.tracks[detection.track_id] = { 642 'filter': kf, 643 'state': kf.x, 644 'last_update': current_time, 645 'hits': 1 646 } 647 else: 648 track = self.tracks[detection.track_id] 649 track['filter'].predict() 650 track['filter'].update([detection.range, detection.velocity]) 651 track['state'] = track['filter'].x 652 track['last_update'] = current_time 653 track['hits'] += 1 654 655 stale_tracks = [tid for tid, track in self.tracks.items() 656 if current_time - track['last_update'] > 5.0] 657 for tid in stale_tracks: 658 del self.tracks[tid] 659 660 class USBPacketParser: 661 def __init__(self): 662 self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) 663 664 def parse_gps_data(self, data): 665 """Parse GPS data from STM32 USB CDC with pitch angle""" 666 if not data: 667 return None 668 669 try: 670 # Try text format first: "GPS:lat,lon,alt,pitch\r\n" 671 text_data = data.decode('utf-8', errors='ignore').strip() 672 if text_data.startswith('GPS:'): 673 parts = text_data.split(':')[1].split(',') 674 if len(parts) == 4: # Now expecting 4 values 675 lat = float(parts[0]) 676 lon = float(parts[1]) 677 alt = float(parts[2]) 678 pitch = float(parts[3]) # Pitch angle in degrees 679 return GPSData(latitude=lat, longitude=lon, altitude=alt, pitch=pitch, timestamp=time.time()) 680 681 # Try binary format (30 bytes with pitch) 682 if len(data) >= 30 and data[0:4] == b'GPSB': 683 return self._parse_binary_gps_with_pitch(data) 684 685 except Exception as e: 686 logging.error(f"Error parsing GPS data: {e}") 687 688 return None 689 690 def _parse_binary_gps_with_pitch(self, data): 691 """Parse binary GPS format with pitch angle (30 bytes)""" 692 try: 693 # Binary format: [Header 4][Latitude 8][Longitude 8][Altitude 4][Pitch 4][CRC 2] 694 if len(data) < 30: 695 return None 696 697 # Verify CRC (simple checksum) 698 crc_received = (data[28] << 8) | data[29] 699 crc_calculated = sum(data[0:28]) & 0xFFFF 700 701 if crc_received != crc_calculated: 702 logging.warning("GPS CRC mismatch") 703 return None 704 705 # Parse latitude (double, big-endian) 706 lat_bits = 0 707 for i in range(8): 708 lat_bits = (lat_bits << 8) | data[4 + i] 709 latitude = struct.unpack('>d', struct.pack('>Q', lat_bits))[0] 710 711 # Parse longitude (double, big-endian) 712 lon_bits = 0 713 for i in range(8): 714 lon_bits = (lon_bits << 8) | data[12 + i] 715 longitude = struct.unpack('>d', struct.pack('>Q', lon_bits))[0] 716 717 # Parse altitude (float, big-endian) 718 alt_bits = 0 719 for i in range(4): 720 alt_bits = (alt_bits << 8) | data[20 + i] 721 altitude = struct.unpack('>f', struct.pack('>I', alt_bits))[0] 722 723 # Parse pitch angle (float, big-endian) 724 pitch_bits = 0 725 for i in range(4): 726 pitch_bits = (pitch_bits << 8) | data[24 + i] 727 pitch = struct.unpack('>f', struct.pack('>I', pitch_bits))[0] 728 729 return GPSData( 730 latitude=latitude, 731 longitude=longitude, 732 altitude=altitude, 733 pitch=pitch, 734 timestamp=time.time() 735 ) 736 737 except Exception as e: 738 logging.error(f"Error parsing binary GPS with pitch: {e}") 739 return None 740 741 class RadarPacketParser: 742 def __init__(self): 743 self.sync_pattern = b'\xA5\xC3' 744 self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) 745 746 def parse_packet(self, data): 747 if len(data) < 6: 748 return None 749 750 sync_index = data.find(self.sync_pattern) 751 if sync_index == -1: 752 return None 753 754 packet = data[sync_index:] 755 756 if len(packet) < 6: 757 return None 758 759 sync = packet[0:2] 760 packet_type = packet[2] 761 length = packet[3] 762 763 if len(packet) < (4 + length + 2): 764 return None 765 766 payload = packet[4:4+length] 767 crc_received = struct.unpack('<H', packet[4+length:4+length+2])[0] 768 769 crc_calculated = self.calculate_crc(packet[0:4+length]) 770 if crc_calculated != crc_received: 771 logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}") 772 return None 773 774 if packet_type == 0x01: 775 return self.parse_range_packet(payload) 776 elif packet_type == 0x02: 777 return self.parse_doppler_packet(payload) 778 elif packet_type == 0x03: 779 return self.parse_detection_packet(payload) 780 else: 781 logging.warning(f"Unknown packet type: {packet_type:02X}") 782 return None 783 784 def calculate_crc(self, data): 785 return self.crc16_func(data) 786 787 def parse_range_packet(self, payload): 788 if len(payload) < 12: 789 return None 790 791 try: 792 range_value = struct.unpack('>I', payload[0:4])[0] 793 elevation = payload[4] & 0x1F 794 azimuth = payload[5] & 0x3F 795 chirp_counter = payload[6] & 0x1F 796 797 return { 798 'type': 'range', 799 'range': range_value, 800 'elevation': elevation, 801 'azimuth': azimuth, 802 'chirp': chirp_counter, 803 'timestamp': time.time() 804 } 805 except Exception as e: 806 logging.error(f"Error parsing range packet: {e}") 807 return None 808 809 def parse_doppler_packet(self, payload): 810 if len(payload) < 12: 811 return None 812 813 try: 814 doppler_real = struct.unpack('>h', payload[0:2])[0] 815 doppler_imag = struct.unpack('>h', payload[2:4])[0] 816 elevation = payload[4] & 0x1F 817 azimuth = payload[5] & 0x3F 818 chirp_counter = payload[6] & 0x1F 819 820 return { 821 'type': 'doppler', 822 'doppler_real': doppler_real, 823 'doppler_imag': doppler_imag, 824 'elevation': elevation, 825 'azimuth': azimuth, 826 'chirp': chirp_counter, 827 'timestamp': time.time() 828 } 829 except Exception as e: 830 logging.error(f"Error parsing Doppler packet: {e}") 831 return None 832 833 def parse_detection_packet(self, payload): 834 if len(payload) < 8: 835 return None 836 837 try: 838 detection_flag = (payload[0] & 0x01) != 0 839 elevation = payload[1] & 0x1F 840 azimuth = payload[2] & 0x3F 841 chirp_counter = payload[3] & 0x1F 842 843 return { 844 'type': 'detection', 845 'detected': detection_flag, 846 'elevation': elevation, 847 'azimuth': azimuth, 848 'chirp': chirp_counter, 849 'timestamp': time.time() 850 } 851 except Exception as e: 852 logging.error(f"Error parsing detection packet: {e}") 853 return None 854 855 class RadarGUI: 856 def __init__(self, root): 857 self.root = root 858 self.root.title("Advanced Radar System GUI - USB CDC with Google Maps") 859 self.root.geometry("1400x900") 860 861 # Initialize interfaces 862 self.stm32_usb_interface = STM32USBInterface() 863 self.ftdi_interface = FTDIInterface() 864 self.radar_processor = RadarProcessor() 865 self.usb_packet_parser = USBPacketParser() 866 self.radar_packet_parser = RadarPacketParser() 867 self.map_generator = MapGenerator() 868 self.settings = RadarSettings() 869 870 # Data queues 871 self.radar_data_queue = queue.Queue() 872 self.gps_data_queue = queue.Queue() 873 874 # Thread control 875 self.running = False 876 self.radar_thread = None 877 self.gps_thread = None 878 879 # Counters 880 self.received_packets = 0 881 self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0) 882 self.corrected_elevations = [] # Store corrected elevation values 883 self.map_file_path = None 884 self.google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY" # Replace with your API key 885 886 self.create_gui() 887 self.start_background_threads() 888 889 def create_gui(self): 890 """Create the main GUI with tabs""" 891 self.notebook = ttk.Notebook(self.root) 892 self.notebook.pack(fill='both', expand=True, padx=10, pady=10) 893 894 self.tab_main = ttk.Frame(self.notebook) 895 self.tab_map = ttk.Frame(self.notebook) 896 self.tab_diagnostics = ttk.Frame(self.notebook) 897 self.tab_settings = ttk.Frame(self.notebook) 898 899 self.notebook.add(self.tab_main, text='Main View') 900 self.notebook.add(self.tab_map, text='Map View') 901 self.notebook.add(self.tab_diagnostics, text='Diagnostics') 902 self.notebook.add(self.tab_settings, text='Settings') 903 904 self.setup_main_tab() 905 self.setup_map_tab() 906 self.setup_settings_tab() 907 908 def setup_main_tab(self): 909 """Setup the main radar display tab""" 910 # Control frame 911 control_frame = ttk.Frame(self.tab_main) 912 control_frame.pack(fill='x', padx=10, pady=5) 913 914 # USB Device selection 915 ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5) 916 self.stm32_usb_combo = ttk.Combobox(control_frame, state="readonly", width=40) 917 self.stm32_usb_combo.grid(row=0, column=1, padx=5) 918 919 ttk.Label(control_frame, text="FTDI Device:").grid(row=0, column=2, padx=5) 920 self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30) 921 self.ftdi_combo.grid(row=0, column=3, padx=5) 922 923 ttk.Button(control_frame, text="Refresh Devices", 924 command=self.refresh_devices).grid(row=0, column=4, padx=5) 925 926 self.start_button = ttk.Button(control_frame, text="Start Radar", 927 command=self.start_radar) 928 self.start_button.grid(row=0, column=5, padx=5) 929 930 self.stop_button = ttk.Button(control_frame, text="Stop Radar", 931 command=self.stop_radar, state="disabled") 932 self.stop_button.grid(row=0, column=6, padx=5) 933 934 # GPS and Pitch info 935 self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...") 936 self.gps_label.grid(row=1, column=0, columnspan=4, sticky='w', padx=5, pady=2) 937 938 # Pitch display 939 self.pitch_label = ttk.Label(control_frame, text="Pitch: --.--°") 940 self.pitch_label.grid(row=1, column=4, columnspan=2, padx=5, pady=2) 941 942 # Status info 943 self.status_label = ttk.Label(control_frame, text="Status: Ready") 944 self.status_label.grid(row=1, column=6, sticky='e', padx=5, pady=2) 945 946 # Main display area 947 display_frame = ttk.Frame(self.tab_main) 948 display_frame.pack(fill='both', expand=True, padx=10, pady=5) 949 950 # Range-Doppler Map 951 fig = Figure(figsize=(10, 6)) 952 self.range_doppler_ax = fig.add_subplot(111) 953 self.range_doppler_plot = self.range_doppler_ax.imshow( 954 np.random.rand(1024, 32), aspect='auto', cmap='hot', 955 extent=[0, 32, 0, 1024]) 956 self.range_doppler_ax.set_title('Range-Doppler Map (Pitch Corrected)') 957 self.range_doppler_ax.set_xlabel('Doppler Bin') 958 self.range_doppler_ax.set_ylabel('Range Bin') 959 960 self.canvas = FigureCanvasTkAgg(fig, display_frame) 961 self.canvas.draw() 962 self.canvas.get_tk_widget().pack(side='left', fill='both', expand=True) 963 964 # Targets list with corrected elevation 965 targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)") 966 targets_frame.pack(side='right', fill='y', padx=5) 967 968 self.targets_tree = ttk.Treeview(targets_frame, 969 columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'Corrected Elev', 'SNR'), 970 show='headings', height=20) 971 self.targets_tree.heading('ID', text='Track ID') 972 self.targets_tree.heading('Range', text='Range (m)') 973 self.targets_tree.heading('Velocity', text='Velocity (m/s)') 974 self.targets_tree.heading('Azimuth', text='Azimuth') 975 self.targets_tree.heading('Elevation', text='Raw Elev') 976 self.targets_tree.heading('Corrected Elev', text='Corr Elev') 977 self.targets_tree.heading('SNR', text='SNR (dB)') 978 979 self.targets_tree.column('ID', width=70) 980 self.targets_tree.column('Range', width=90) 981 self.targets_tree.column('Velocity', width=90) 982 self.targets_tree.column('Azimuth', width=70) 983 self.targets_tree.column('Elevation', width=70) 984 self.targets_tree.column('Corrected Elev', width=70) 985 self.targets_tree.column('SNR', width=70) 986 987 self.targets_tree.pack(fill='both', expand=True, padx=5, pady=5) 988 989 def setup_map_tab(self): 990 """Setup the map display tab with Google Maps""" 991 map_frame = ttk.Frame(self.tab_map) 992 map_frame.pack(fill='both', expand=True, padx=10, pady=10) 993 994 # Map controls 995 controls_frame = ttk.Frame(map_frame) 996 controls_frame.pack(fill='x', pady=5) 997 998 ttk.Button(controls_frame, text="Open Map in Browser", 999 command=self.open_map_in_browser).pack(side='left', padx=5) 1000 1001 ttk.Button(controls_frame, text="Refresh Map", 1002 command=self.refresh_map).pack(side='left', padx=5) 1003 1004 self.map_status_label = ttk.Label(controls_frame, text="Map: Ready to generate") 1005 self.map_status_label.pack(side='left', padx=20) 1006 1007 # Map info display 1008 info_frame = ttk.Frame(map_frame) 1009 info_frame.pack(fill='x', pady=5) 1010 1011 self.map_info_label = ttk.Label(info_frame, text="No GPS data received yet", font=('Arial', 10)) 1012 self.map_info_label.pack() 1013 1014 def setup_settings_tab(self): 1015 """Setup the settings tab with additional chirp durations and map size""" 1016 settings_frame = ttk.Frame(self.tab_settings) 1017 settings_frame.pack(fill='both', expand=True, padx=10, pady=10) 1018 1019 entries = [ 1020 ('System Frequency (Hz):', 'system_frequency', 10e9), 1021 ('Chirp Duration 1 - Long (s):', 'chirp_duration_1', 30e-6), 1022 ('Chirp Duration 2 - Short (s):', 'chirp_duration_2', 0.5e-6), 1023 ('Chirps per Position:', 'chirps_per_position', 32), 1024 ('Frequency Min (Hz):', 'freq_min', 10e6), 1025 ('Frequency Max (Hz):', 'freq_max', 30e6), 1026 ('PRF1 (Hz):', 'prf1', 1000), 1027 ('PRF2 (Hz):', 'prf2', 2000), 1028 ('Max Distance (m):', 'max_distance', 50000), 1029 ('Map Size (m):', 'map_size', 50000), 1030 ('Google Maps API Key:', 'google_maps_api_key', 'YOUR_GOOGLE_MAPS_API_KEY') 1031 ] 1032 1033 self.settings_vars = {} 1034 1035 for i, (label, attr, default) in enumerate(entries): 1036 ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky='w', padx=5, pady=5) 1037 var = tk.StringVar(value=str(default)) 1038 entry = ttk.Entry(settings_frame, textvariable=var, width=25) 1039 entry.grid(row=i, column=1, padx=5, pady=5) 1040 self.settings_vars[attr] = var 1041 1042 ttk.Button(settings_frame, text="Apply Settings", 1043 command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10) 1044 1045 def apply_pitch_correction(self, raw_elevation, pitch_angle): 1046 """ 1047 Apply pitch correction to elevation angle 1048 raw_elevation: measured elevation from radar (degrees) 1049 pitch_angle: antenna pitch angle from IMU (degrees) 1050 Returns: corrected elevation angle (degrees) 1051 """ 1052 # Convert to radians for trigonometric functions 1053 raw_elev_rad = math.radians(raw_elevation) 1054 pitch_rad = math.radians(pitch_angle) 1055 1056 # Apply pitch correction: corrected_elev = raw_elev - pitch 1057 # This assumes the pitch angle is positive when antenna is tilted up 1058 corrected_elev_rad = raw_elev_rad - pitch_rad 1059 1060 # Convert back to degrees and ensure it's within valid range 1061 corrected_elev_deg = math.degrees(corrected_elev_rad) 1062 1063 # Normalize to 0-180 degree range 1064 corrected_elev_deg = corrected_elev_deg % 180 1065 if corrected_elev_deg < 0: 1066 corrected_elev_deg += 180 1067 1068 return corrected_elev_deg 1069 1070 def refresh_devices(self): 1071 """Refresh available USB devices""" 1072 # STM32 USB devices 1073 stm32_devices = self.stm32_usb_interface.list_devices() 1074 stm32_names = [dev['description'] for dev in stm32_devices] 1075 self.stm32_usb_combo['values'] = stm32_names 1076 1077 # FTDI devices 1078 ftdi_devices = self.ftdi_interface.list_devices() 1079 ftdi_names = [dev['description'] for dev in ftdi_devices] 1080 self.ftdi_combo['values'] = ftdi_names 1081 1082 if stm32_names: 1083 self.stm32_usb_combo.current(0) 1084 if ftdi_names: 1085 self.ftdi_combo.current(0) 1086 1087 def start_radar(self): 1088 """Step 11: Start button pressed - Begin radar operation""" 1089 try: 1090 # Open STM32 USB device 1091 stm32_index = self.stm32_usb_combo.current() 1092 if stm32_index == -1: 1093 messagebox.showerror("Error", "Please select an STM32 USB device") 1094 return 1095 1096 stm32_devices = self.stm32_usb_interface.list_devices() 1097 if stm32_index >= len(stm32_devices): 1098 messagebox.showerror("Error", "Invalid STM32 device selection") 1099 return 1100 1101 if not self.stm32_usb_interface.open_device(stm32_devices[stm32_index]): 1102 messagebox.showerror("Error", "Failed to open STM32 USB device") 1103 return 1104 1105 # Open FTDI device 1106 if FTDI_AVAILABLE: 1107 ftdi_index = self.ftdi_combo.current() 1108 if ftdi_index != -1: 1109 ftdi_devices = self.ftdi_interface.list_devices() 1110 if ftdi_index < len(ftdi_devices): 1111 device_url = ftdi_devices[ftdi_index]['url'] 1112 if not self.ftdi_interface.open_device(device_url): 1113 logging.warning("Failed to open FTDI device, continuing without radar data") 1114 else: 1115 logging.warning("No FTDI device selected, continuing without radar data") 1116 else: 1117 logging.warning("FTDI not available, continuing without radar data") 1118 1119 # Step 12: Send start flag to STM32 via USB 1120 if not self.stm32_usb_interface.send_start_flag(): 1121 messagebox.showerror("Error", "Failed to send start flag to STM32") 1122 return 1123 1124 # Step 13: Send settings to STM32 via USB 1125 self.apply_settings() 1126 1127 # Start radar operation 1128 self.running = True 1129 self.start_button.config(state="disabled") 1130 self.stop_button.config(state="normal") 1131 self.status_label.config(text="Status: Radar running - Waiting for GPS data...") 1132 1133 logging.info("Radar system started successfully via USB CDC") 1134 1135 except Exception as e: 1136 messagebox.showerror("Error", f"Failed to start radar: {e}") 1137 logging.error(f"Start radar error: {e}") 1138 1139 def stop_radar(self): 1140 """Stop radar operation""" 1141 self.running = False 1142 self.start_button.config(state="normal") 1143 self.stop_button.config(state="disabled") 1144 self.status_label.config(text="Status: Radar stopped") 1145 1146 self.stm32_usb_interface.close() 1147 self.ftdi_interface.close() 1148 1149 logging.info("Radar system stopped") 1150 1151 def apply_settings(self): 1152 """Step 13: Apply and send radar settings via USB""" 1153 try: 1154 self.settings.system_frequency = float(self.settings_vars['system_frequency'].get()) 1155 self.settings.chirp_duration_1 = float(self.settings_vars['chirp_duration_1'].get()) 1156 self.settings.chirp_duration_2 = float(self.settings_vars['chirp_duration_2'].get()) 1157 self.settings.chirps_per_position = int(self.settings_vars['chirps_per_position'].get()) 1158 self.settings.freq_min = float(self.settings_vars['freq_min'].get()) 1159 self.settings.freq_max = float(self.settings_vars['freq_max'].get()) 1160 self.settings.prf1 = float(self.settings_vars['prf1'].get()) 1161 self.settings.prf2 = float(self.settings_vars['prf2'].get()) 1162 self.settings.max_distance = float(self.settings_vars['max_distance'].get()) 1163 self.settings.map_size = float(self.settings_vars['map_size'].get()) 1164 self.google_maps_api_key = self.settings_vars['google_maps_api_key'].get() 1165 1166 if self.stm32_usb_interface.is_open: 1167 self.stm32_usb_interface.send_settings(self.settings) 1168 1169 messagebox.showinfo("Success", "Settings applied and sent to STM32 via USB") 1170 logging.info("Radar settings applied via USB") 1171 1172 except ValueError as e: 1173 messagebox.showerror("Error", f"Invalid setting value: {e}") 1174 1175 def start_background_threads(self): 1176 """Start background data processing threads""" 1177 self.radar_thread = threading.Thread(target=self.process_radar_data, daemon=True) 1178 self.radar_thread.start() 1179 1180 self.gps_thread = threading.Thread(target=self.process_gps_data, daemon=True) 1181 self.gps_thread.start() 1182 1183 self.root.after(100, self.update_gui) 1184 1185 def process_radar_data(self): 1186 """Step 39: Process incoming radar data from FTDI""" 1187 buffer = b'' 1188 while True: 1189 if self.running and self.ftdi_interface.is_open: 1190 try: 1191 data = self.ftdi_interface.read_data(4096) 1192 if data: 1193 buffer += data 1194 1195 while len(buffer) >= 6: 1196 packet = self.radar_packet_parser.parse_packet(buffer) 1197 if packet: 1198 self.process_radar_packet(packet) 1199 packet_length = 4 + len(packet.get('payload', b'')) + 2 1200 buffer = buffer[packet_length:] 1201 self.received_packets += 1 1202 else: 1203 break 1204 1205 except Exception as e: 1206 logging.error(f"Error processing radar data: {e}") 1207 time.sleep(0.1) 1208 else: 1209 time.sleep(0.1) 1210 1211 def process_gps_data(self): 1212 """Step 16/17: Process GPS data from STM32 via USB CDC""" 1213 while True: 1214 if self.running and self.stm32_usb_interface.is_open: 1215 try: 1216 # Read data from STM32 USB 1217 data = self.stm32_usb_interface.read_data(64, timeout=100) 1218 if data: 1219 gps_data = self.usb_packet_parser.parse_gps_data(data) 1220 if gps_data: 1221 self.gps_data_queue.put(gps_data) 1222 logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°") 1223 except Exception as e: 1224 logging.error(f"Error processing GPS data via USB: {e}") 1225 time.sleep(0.1) 1226 1227 def process_radar_packet(self, packet): 1228 """Step 40: Process radar data and apply pitch correction""" 1229 try: 1230 if packet['type'] == 'range': 1231 range_meters = packet['range'] * 0.1 1232 1233 # Apply pitch correction to elevation 1234 raw_elevation = packet['elevation'] 1235 corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch) 1236 1237 # Store correction for display 1238 self.corrected_elevations.append({ 1239 'raw': raw_elevation, 1240 'corrected': corrected_elevation, 1241 'pitch': self.current_gps.pitch, 1242 'timestamp': packet['timestamp'] 1243 }) 1244 1245 # Keep only recent corrections 1246 if len(self.corrected_elevations) > 100: 1247 self.corrected_elevations = self.corrected_elevations[-100:] 1248 1249 target = RadarTarget( 1250 id=packet['chirp'], 1251 range=range_meters, 1252 velocity=0, 1253 azimuth=packet['azimuth'], 1254 elevation=corrected_elevation, # Use corrected elevation 1255 snr=20.0, 1256 timestamp=packet['timestamp'] 1257 ) 1258 1259 self.update_range_doppler_map(target) 1260 1261 elif packet['type'] == 'doppler': 1262 lambda_wavelength = 3e8 / self.settings.system_frequency 1263 velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2) 1264 self.update_target_velocity(packet, velocity) 1265 1266 elif packet['type'] == 'detection': 1267 if packet['detected']: 1268 # Apply pitch correction to detection elevation 1269 raw_elevation = packet['elevation'] 1270 corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch) 1271 1272 logging.info(f"CFAR Detection: Raw Elev {raw_elevation}°, Corrected Elev {corrected_elevation:.1f}°, Pitch {self.current_gps.pitch:.1f}°") 1273 1274 except Exception as e: 1275 logging.error(f"Error processing radar packet: {e}") 1276 1277 def update_range_doppler_map(self, target): 1278 """Update range-Doppler map with new target""" 1279 range_bin = min(int(target.range / 50), 1023) 1280 doppler_bin = min(abs(int(target.velocity)), 31) 1281 1282 self.radar_processor.range_doppler_map[range_bin, doppler_bin] += 1 1283 1284 self.radar_processor.detected_targets.append(target) 1285 1286 if len(self.radar_processor.detected_targets) > 100: 1287 self.radar_processor.detected_targets = self.radar_processor.detected_targets[-100:] 1288 1289 def update_target_velocity(self, packet, velocity): 1290 """Update target velocity information""" 1291 for target in self.radar_processor.detected_targets: 1292 if (target.azimuth == packet['azimuth'] and 1293 target.elevation == packet['elevation'] and 1294 target.id == packet['chirp']): 1295 target.velocity = velocity 1296 break 1297 1298 def open_map_in_browser(self): 1299 """Open the generated map in the default web browser""" 1300 if self.map_file_path and os.path.exists(self.map_file_path): 1301 webbrowser.open('file://' + os.path.abspath(self.map_file_path)) 1302 else: 1303 messagebox.showwarning("Warning", "No map file available. Generate map first by receiving GPS data.") 1304 1305 def refresh_map(self): 1306 """Refresh the map with current data""" 1307 self.generate_map() 1308 1309 def generate_map(self): 1310 """Generate Google Maps HTML file with current targets""" 1311 if self.current_gps.latitude == 0 and self.current_gps.longitude == 0: 1312 self.map_status_label.config(text="Map: Waiting for GPS data") 1313 return 1314 1315 try: 1316 # Create temporary HTML file 1317 with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f: 1318 map_html = self.map_generator.generate_map( 1319 self.current_gps, 1320 self.radar_processor.detected_targets, 1321 self.settings.map_size, 1322 self.google_maps_api_key 1323 ) 1324 f.write(map_html) 1325 self.map_file_path = f.name 1326 1327 self.map_status_label.config(text=f"Map: Generated at {self.map_file_path}") 1328 self.map_info_label.config( 1329 text=f"Radar: {self.current_gps.latitude:.6f}, {self.current_gps.longitude:.6f} | " 1330 f"Targets: {len(self.radar_processor.detected_targets)} | " 1331 f"Coverage: {self.settings.map_size/1000:.1f}km" 1332 ) 1333 logging.info(f"Map generated: {self.map_file_path}") 1334 1335 except Exception as e: 1336 logging.error(f"Error generating map: {e}") 1337 self.map_status_label.config(text=f"Map: Error - {str(e)}") 1338 1339 def update_gps_display(self): 1340 """Step 18: Update GPS and pitch display""" 1341 try: 1342 while not self.gps_data_queue.empty(): 1343 gps_data = self.gps_data_queue.get_nowait() 1344 self.current_gps = gps_data 1345 1346 # Update GPS label 1347 self.gps_label.config( 1348 text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m") 1349 1350 # Update pitch label with color coding 1351 pitch_text = f"Pitch: {gps_data.pitch:+.1f}°" 1352 self.pitch_label.config(text=pitch_text) 1353 1354 # Color code based on pitch magnitude 1355 if abs(gps_data.pitch) > 10: 1356 self.pitch_label.config(foreground='red') # High pitch warning 1357 elif abs(gps_data.pitch) > 5: 1358 self.pitch_label.config(foreground='orange') # Medium pitch 1359 else: 1360 self.pitch_label.config(foreground='green') # Normal pitch 1361 1362 # Generate/update map when new GPS data arrives 1363 self.generate_map() 1364 1365 except queue.Empty: 1366 pass 1367 1368 def update_targets_list(self): 1369 """Update the targets list display with corrected elevations""" 1370 for item in self.targets_tree.get_children(): 1371 self.targets_tree.delete(item) 1372 1373 for target in self.radar_processor.detected_targets[-20:]: 1374 # Find the corresponding raw elevation if available 1375 raw_elevation = "N/A" 1376 for correction in self.corrected_elevations[-20:]: 1377 if abs(correction['corrected'] - target.elevation) < 0.1: # Fuzzy match 1378 raw_elevation = f"{correction['raw']}" 1379 break 1380 1381 self.targets_tree.insert('', 'end', values=( 1382 target.track_id, 1383 f"{target.range:.1f}", 1384 f"{target.velocity:.1f}", 1385 target.azimuth, 1386 raw_elevation, # Show raw elevation 1387 f"{target.elevation:.1f}", # Show corrected elevation 1388 f"{target.snr:.1f}" 1389 )) 1390 1391 def update_gui(self): 1392 """Step 40: Update all GUI displays""" 1393 try: 1394 # Update status with pitch information 1395 if self.running: 1396 self.status_label.config( 1397 text=f"Status: Running - Packets: {self.received_packets} - Pitch: {self.current_gps.pitch:+.1f}°") 1398 1399 # Update range-Doppler map 1400 if hasattr(self, 'range_doppler_plot'): 1401 display_data = np.log10(self.radar_processor.range_doppler_map + 1) 1402 self.range_doppler_plot.set_array(display_data) 1403 self.canvas.draw_idle() 1404 1405 # Update targets list 1406 self.update_targets_list() 1407 1408 # Update GPS and pitch display 1409 self.update_gps_display() 1410 1411 except Exception as e: 1412 logging.error(f"Error updating GUI: {e}") 1413 1414 self.root.after(100, self.update_gui) 1415 1416 def main(): 1417 """Main application entry point""" 1418 try: 1419 root = tk.Tk() 1420 app = RadarGUI(root) 1421 root.mainloop() 1422 except Exception as e: 1423 logging.error(f"Application error: {e}") 1424 messagebox.showerror("Fatal Error", f"Application failed to start: {e}") 1425 1426 if __name__ == "__main__": 1427 main()