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