/ 9_Firmware / 9_3_GUI / GUI_V2.py
GUI_V2.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 logging
  12  from dataclasses import dataclass
  13  from typing import Dict, List, Tuple, Optional
  14  from scipy import signal
  15  from sklearn.cluster import DBSCAN
  16  from filterpy.kalman import KalmanFilter
  17  import crcmod
  18  import math
  19  
  20  try:
  21      import usb.core
  22      import usb.util
  23      USB_AVAILABLE = True
  24  except ImportError:
  25      USB_AVAILABLE = False
  26      logging.warning("pyusb not available. USB CDC functionality will be disabled.")
  27  
  28  try:
  29      from pyftdi.ftdi import Ftdi
  30      from pyftdi.usbtools import UsbTools
  31      FTDI_AVAILABLE = True
  32  except ImportError:
  33      FTDI_AVAILABLE = False
  34      logging.warning("pyftdi not available. FTDI functionality will be disabled.")
  35  
  36  # Configure logging
  37  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  38  
  39  @dataclass
  40  class RadarTarget:
  41      id: int
  42      range: float
  43      velocity: float
  44      azimuth: int
  45      elevation: int
  46      snr: float
  47      timestamp: float
  48      track_id: int = -1
  49  
  50  @dataclass
  51  class RadarSettings:
  52      system_frequency: float = 10e9
  53      chirp_duration: float = 30e-6
  54      chirps_per_position: int = 32
  55      freq_min: float = 10e6
  56      freq_max: float = 30e6
  57      prf1: float = 1000
  58      prf2: float = 2000
  59      max_distance: float = 50000
  60  
  61  @dataclass
  62  class GPSData:
  63      latitude: float
  64      longitude: float
  65      altitude: float
  66      timestamp: float
  67  
  68  class STM32USBInterface:
  69      def __init__(self):
  70          self.device = None
  71          self.is_open = False
  72          self.ep_in = None
  73          self.ep_out = None
  74          
  75      def list_devices(self):
  76          """List available STM32 USB CDC devices"""
  77          if not USB_AVAILABLE:
  78              logging.warning("USB not available - please install pyusb")
  79              return []
  80              
  81          try:
  82              devices = []
  83              # STM32 USB CDC devices typically use these vendor/product IDs
  84              stm32_vid_pids = [
  85                  (0x0483, 0x5740),  # STM32 Virtual COM Port
  86                  (0x0483, 0x3748),  # STM32 Discovery
  87                  (0x0483, 0x374B),  # STM32 CDC
  88                  (0x0483, 0x374D),  # STM32 CDC
  89                  (0x0483, 0x374E),  # STM32 CDC
  90                  (0x0483, 0x3752),  # STM32 CDC
  91              ]
  92              
  93              for vid, pid in stm32_vid_pids:
  94                  found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
  95                  for dev in found_devices:
  96                      try:
  97                          product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
  98                          serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
  99                          devices.append({
 100                              'description': f"{product} ({serial})",
 101                              'vendor_id': vid,
 102                              'product_id': pid,
 103                              'device': dev
 104                          })
 105                      except:
 106                          devices.append({
 107                              'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})",
 108                              'vendor_id': vid,
 109                              'product_id': pid,
 110                              'device': dev
 111                          })
 112              
 113              return devices
 114          except Exception as e:
 115              logging.error(f"Error listing USB devices: {e}")
 116              # Return mock devices for testing
 117              return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}]
 118      
 119      def open_device(self, device_info):
 120          """Open STM32 USB CDC device"""
 121          if not USB_AVAILABLE:
 122              logging.error("USB not available - cannot open device")
 123              return False
 124              
 125          try:
 126              self.device = device_info['device']
 127              
 128              # Detach kernel driver if active
 129              if self.device.is_kernel_driver_active(0):
 130                  self.device.detach_kernel_driver(0)
 131              
 132              # Set configuration
 133              self.device.set_configuration()
 134              
 135              # Get CDC endpoints
 136              cfg = self.device.get_active_configuration()
 137              intf = cfg[(0,0)]
 138              
 139              # Find bulk endpoints (CDC data interface)
 140              self.ep_out = usb.util.find_descriptor(
 141                  intf,
 142                  custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
 143              )
 144              
 145              self.ep_in = usb.util.find_descriptor(
 146                  intf,
 147                  custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
 148              )
 149              
 150              if self.ep_out is None or self.ep_in is None:
 151                  logging.error("Could not find CDC endpoints")
 152                  return False
 153              
 154              self.is_open = True
 155              logging.info(f"STM32 USB device opened: {device_info['description']}")
 156              return True
 157              
 158          except Exception as e:
 159              logging.error(f"Error opening USB device: {e}")
 160              return False
 161      
 162      def send_start_flag(self):
 163          """Step 12: Send start flag to STM32 via USB"""
 164          start_packet = bytes([23, 46, 158, 237])
 165          logging.info("Sending start flag to STM32 via USB...")
 166          return self._send_data(start_packet)
 167      
 168      def send_settings(self, settings):
 169          """Step 13: Send radar settings to STM32 via USB"""
 170          try:
 171              packet = self._create_settings_packet(settings)
 172              logging.info("Sending radar settings to STM32 via USB...")
 173              return self._send_data(packet)
 174          except Exception as e:
 175              logging.error(f"Error sending settings via USB: {e}")
 176              return False
 177      
 178      def read_data(self, size=64, timeout=1000):
 179          """Read data from STM32 via USB"""
 180          if not self.is_open or self.ep_in is None:
 181              return None
 182              
 183          try:
 184              data = self.ep_in.read(size, timeout=timeout)
 185              return bytes(data)
 186          except usb.core.USBError as e:
 187              if e.errno == 110:  # Timeout
 188                  return None
 189              logging.error(f"USB read error: {e}")
 190              return None
 191          except Exception as e:
 192              logging.error(f"Error reading from USB: {e}")
 193              return None
 194      
 195      def _send_data(self, data):
 196          """Send data to STM32 via USB"""
 197          if not self.is_open or self.ep_out is None:
 198              return False
 199              
 200          try:
 201              # USB CDC typically uses 64-byte packets
 202              packet_size = 64
 203              for i in range(0, len(data), packet_size):
 204                  chunk = data[i:i + packet_size]
 205                  # Pad to packet size if needed
 206                  if len(chunk) < packet_size:
 207                      chunk += b'\x00' * (packet_size - len(chunk))
 208                  self.ep_out.write(chunk)
 209              
 210              return True
 211          except Exception as e:
 212              logging.error(f"Error sending data via USB: {e}")
 213              return False
 214      
 215      def _create_settings_packet(self, settings):
 216          """Create binary settings packet for USB transmission"""
 217          packet = b'SET'
 218          packet += struct.pack('>d', settings.system_frequency)
 219          packet += struct.pack('>d', settings.chirp_duration)
 220          packet += struct.pack('>I', settings.chirps_per_position)
 221          packet += struct.pack('>d', settings.freq_min)
 222          packet += struct.pack('>d', settings.freq_max)
 223          packet += struct.pack('>d', settings.prf1)
 224          packet += struct.pack('>d', settings.prf2)
 225          packet += struct.pack('>d', settings.max_distance)
 226          packet += b'END'
 227          return packet
 228      
 229      def close(self):
 230          """Close USB device"""
 231          if self.device and self.is_open:
 232              try:
 233                  usb.util.dispose_resources(self.device)
 234                  self.is_open = False
 235              except Exception as e:
 236                  logging.error(f"Error closing USB device: {e}")
 237  
 238  class FTDIInterface:
 239      def __init__(self):
 240          self.ftdi = None
 241          self.is_open = False
 242          
 243      def list_devices(self):
 244          """List available FTDI devices using pyftdi"""
 245          if not FTDI_AVAILABLE:
 246              logging.warning("FTDI not available - please install pyftdi")
 247              return []
 248              
 249          try:
 250              devices = []
 251              # Get list of all FTDI devices
 252              for device in UsbTools.find_all([(0x0403, 0x6010)]):  # FT2232H vendor/product ID
 253                  devices.append({
 254                      'description': f"FTDI Device {device}",
 255                      'url': f"ftdi://{device}/1"
 256                  })
 257              return devices
 258          except Exception as e:
 259              logging.error(f"Error listing FTDI devices: {e}")
 260              # Return mock devices for testing
 261              return [{'description': 'FT2232H Device A', 'url': 'ftdi://device/1'}]
 262      
 263      def open_device(self, device_url):
 264          """Open FTDI device using pyftdi"""
 265          if not FTDI_AVAILABLE:
 266              logging.error("FTDI not available - cannot open device")
 267              return False
 268              
 269          try:
 270              self.ftdi = Ftdi()
 271              self.ftdi.open_from_url(device_url)
 272              
 273              # Configure for synchronous FIFO mode
 274              self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.SYNCFF)
 275              
 276              # Set latency timer
 277              self.ftdi.set_latency_timer(2)
 278              
 279              # Purge buffers
 280              self.ftdi.purge_buffers()
 281              
 282              self.is_open = True
 283              logging.info(f"FTDI device opened: {device_url}")
 284              return True
 285              
 286          except Exception as e:
 287              logging.error(f"Error opening FTDI device: {e}")
 288              return False
 289      
 290      def read_data(self, bytes_to_read):
 291          """Read data from FTDI"""
 292          if not self.is_open or self.ftdi is None:
 293              return None
 294              
 295          try:
 296              data = self.ftdi.read_data(bytes_to_read)
 297              if data:
 298                  return bytes(data)
 299              return None
 300          except Exception as e:
 301              logging.error(f"Error reading from FTDI: {e}")
 302              return None
 303      
 304      def close(self):
 305          """Close FTDI device"""
 306          if self.ftdi and self.is_open:
 307              self.ftdi.close()
 308              self.is_open = False
 309  
 310  class RadarProcessor:
 311      def __init__(self):
 312          self.range_doppler_map = np.zeros((1024, 32))
 313          self.detected_targets = []
 314          self.track_id_counter = 0
 315          self.tracks = {}
 316          self.frame_count = 0
 317          
 318      def dual_cpi_fusion(self, range_profiles_1, range_profiles_2):
 319          """Dual-CPI fusion for better detection"""
 320          fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0)
 321          return fused_profile
 322      
 323      def multi_prf_unwrap(self, doppler_measurements, prf1, prf2):
 324          """Multi-PRF velocity unwrapping"""
 325          lambda_wavelength = 3e8 / 10e9
 326          v_max1 = prf1 * lambda_wavelength / 2
 327          v_max2 = prf2 * lambda_wavelength / 2
 328          
 329          unwrapped_velocities = []
 330          for doppler in doppler_measurements:
 331              v1 = doppler * lambda_wavelength / 2
 332              v2 = doppler * lambda_wavelength / 2
 333              
 334              velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2)
 335              unwrapped_velocities.append(velocity)
 336              
 337          return unwrapped_velocities
 338      
 339      def _solve_chinese_remainder(self, v1, v2, max1, max2):
 340          for k in range(-5, 6):
 341              candidate = v1 + k * max1
 342              if abs(candidate - v2) < max2 / 2:
 343                  return candidate
 344          return v1
 345      
 346      def clustering(self, detections, eps=100, min_samples=2):
 347          """DBSCAN clustering of detections"""
 348          if len(detections) == 0:
 349              return []
 350              
 351          points = np.array([[d.range, d.velocity] for d in detections])
 352          clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(points)
 353          
 354          clusters = []
 355          for label in set(clustering.labels_):
 356              if label != -1:
 357                  cluster_points = points[clustering.labels_ == label]
 358                  clusters.append({
 359                      'center': np.mean(cluster_points, axis=0),
 360                      'points': cluster_points,
 361                      'size': len(cluster_points)
 362                  })
 363                  
 364          return clusters
 365      
 366      def association(self, detections, clusters):
 367          """Association of detections to tracks"""
 368          associated_detections = []
 369          
 370          for detection in detections:
 371              best_track = None
 372              min_distance = float('inf')
 373              
 374              for track_id, track in self.tracks.items():
 375                  distance = np.sqrt(
 376                      (detection.range - track['state'][0])**2 +
 377                      (detection.velocity - track['state'][2])**2
 378                  )
 379                  
 380                  if distance < min_distance and distance < 500:
 381                      min_distance = distance
 382                      best_track = track_id
 383              
 384              if best_track is not None:
 385                  detection.track_id = best_track
 386                  associated_detections.append(detection)
 387              else:
 388                  detection.track_id = self.track_id_counter
 389                  self.track_id_counter += 1
 390                  associated_detections.append(detection)
 391                  
 392          return associated_detections
 393      
 394      def tracking(self, associated_detections):
 395          """Kalman filter tracking"""
 396          current_time = time.time()
 397          
 398          for detection in associated_detections:
 399              if detection.track_id not in self.tracks:
 400                  kf = KalmanFilter(dim_x=4, dim_z=2)
 401                  kf.x = np.array([detection.range, 0, detection.velocity, 0])
 402                  kf.F = np.array([[1, 1, 0, 0],
 403                                 [0, 1, 0, 0],
 404                                 [0, 0, 1, 1],
 405                                 [0, 0, 0, 1]])
 406                  kf.H = np.array([[1, 0, 0, 0],
 407                                 [0, 0, 1, 0]])
 408                  kf.P *= 1000
 409                  kf.R = np.diag([10, 1])
 410                  kf.Q = np.eye(4) * 0.1
 411                  
 412                  self.tracks[detection.track_id] = {
 413                      'filter': kf,
 414                      'state': kf.x,
 415                      'last_update': current_time,
 416                      'hits': 1
 417                  }
 418              else:
 419                  track = self.tracks[detection.track_id]
 420                  track['filter'].predict()
 421                  track['filter'].update([detection.range, detection.velocity])
 422                  track['state'] = track['filter'].x
 423                  track['last_update'] = current_time
 424                  track['hits'] += 1
 425          
 426          stale_tracks = [tid for tid, track in self.tracks.items() 
 427                         if current_time - track['last_update'] > 5.0]
 428          for tid in stale_tracks:
 429              del self.tracks[tid]
 430  
 431  class USBPacketParser:
 432      def __init__(self):
 433          self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000)
 434          
 435      def parse_gps_data(self, data):
 436          """Parse GPS data from STM32 USB CDC"""
 437          if not data:
 438              return None
 439              
 440          try:
 441              # Try text format first: "GPS:lat,lon,alt\r\n"
 442              text_data = data.decode('utf-8', errors='ignore').strip()
 443              if text_data.startswith('GPS:'):
 444                  parts = text_data.split(':')[1].split(',')
 445                  if len(parts) == 3:
 446                      lat = float(parts[0])
 447                      lon = float(parts[1])
 448                      alt = float(parts[2])
 449                      return GPSData(latitude=lat, longitude=lon, altitude=alt, timestamp=time.time())
 450              
 451              # Try binary format
 452              if len(data) >= 26 and data[0:4] == b'GPSB':
 453                  return self._parse_binary_gps(data)
 454                  
 455          except Exception as e:
 456              logging.error(f"Error parsing GPS data: {e}")
 457              
 458          return None
 459      
 460      def _parse_binary_gps(self, data):
 461          """Parse binary GPS format"""
 462          try:
 463              # Binary format: [Header 4][Latitude 8][Longitude 8][Altitude 4][CRC 2]
 464              if len(data) < 26:
 465                  return None
 466                  
 467              # Verify CRC (simple checksum)
 468              crc_received = (data[24] << 8) | data[25]
 469              crc_calculated = sum(data[0:24]) & 0xFFFF
 470              
 471              if crc_received != crc_calculated:
 472                  logging.warning("GPS CRC mismatch")
 473                  return None
 474              
 475              # Parse latitude (double, big-endian)
 476              lat_bits = 0
 477              for i in range(8):
 478                  lat_bits = (lat_bits << 8) | data[4 + i]
 479              latitude = struct.unpack('>d', struct.pack('>Q', lat_bits))[0]
 480              
 481              # Parse longitude (double, big-endian)
 482              lon_bits = 0
 483              for i in range(8):
 484                  lon_bits = (lon_bits << 8) | data[12 + i]
 485              longitude = struct.unpack('>d', struct.pack('>Q', lon_bits))[0]
 486              
 487              # Parse altitude (float, big-endian)
 488              alt_bits = 0
 489              for i in range(4):
 490                  alt_bits = (alt_bits << 8) | data[20 + i]
 491              altitude = struct.unpack('>f', struct.pack('>I', alt_bits))[0]
 492              
 493              return GPSData(latitude=latitude, longitude=longitude, altitude=altitude, timestamp=time.time())
 494              
 495          except Exception as e:
 496              logging.error(f"Error parsing binary GPS: {e}")
 497              return None
 498  
 499  class RadarPacketParser:
 500      def __init__(self):
 501          self.sync_pattern = b'\xA5\xC3'
 502          self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000)
 503          
 504      def parse_packet(self, data):
 505          if len(data) < 6:
 506              return None
 507              
 508          sync_index = data.find(self.sync_pattern)
 509          if sync_index == -1:
 510              return None
 511              
 512          packet = data[sync_index:]
 513          
 514          if len(packet) < 6:
 515              return None
 516              
 517          sync = packet[0:2]
 518          packet_type = packet[2]
 519          length = packet[3]
 520          
 521          if len(packet) < (4 + length + 2):
 522              return None
 523              
 524          payload = packet[4:4+length]
 525          crc_received = struct.unpack('<H', packet[4+length:4+length+2])[0]
 526          
 527          crc_calculated = self.calculate_crc(packet[0:4+length])
 528          if crc_calculated != crc_received:
 529              logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}")
 530              return None
 531          
 532          if packet_type == 0x01:
 533              return self.parse_range_packet(payload)
 534          elif packet_type == 0x02:
 535              return self.parse_doppler_packet(payload)
 536          elif packet_type == 0x03:
 537              return self.parse_detection_packet(payload)
 538          else:
 539              logging.warning(f"Unknown packet type: {packet_type:02X}")
 540              return None
 541      
 542      def calculate_crc(self, data):
 543          return self.crc16_func(data)
 544      
 545      def parse_range_packet(self, payload):
 546          if len(payload) < 12:
 547              return None
 548              
 549          try:
 550              range_value = struct.unpack('>I', payload[0:4])[0]
 551              elevation = payload[4] & 0x1F
 552              azimuth = payload[5] & 0x3F
 553              chirp_counter = payload[6] & 0x1F
 554              
 555              return {
 556                  'type': 'range',
 557                  'range': range_value,
 558                  'elevation': elevation,
 559                  'azimuth': azimuth,
 560                  'chirp': chirp_counter,
 561                  'timestamp': time.time()
 562              }
 563          except Exception as e:
 564              logging.error(f"Error parsing range packet: {e}")
 565              return None
 566      
 567      def parse_doppler_packet(self, payload):
 568          if len(payload) < 12:
 569              return None
 570              
 571          try:
 572              doppler_real = struct.unpack('>h', payload[0:2])[0]
 573              doppler_imag = struct.unpack('>h', payload[2:4])[0]
 574              elevation = payload[4] & 0x1F
 575              azimuth = payload[5] & 0x3F
 576              chirp_counter = payload[6] & 0x1F
 577              
 578              return {
 579                  'type': 'doppler',
 580                  'doppler_real': doppler_real,
 581                  'doppler_imag': doppler_imag,
 582                  'elevation': elevation,
 583                  'azimuth': azimuth,
 584                  'chirp': chirp_counter,
 585                  'timestamp': time.time()
 586              }
 587          except Exception as e:
 588              logging.error(f"Error parsing Doppler packet: {e}")
 589              return None
 590      
 591      def parse_detection_packet(self, payload):
 592          if len(payload) < 8:
 593              return None
 594              
 595          try:
 596              detection_flag = (payload[0] & 0x01) != 0
 597              elevation = payload[1] & 0x1F
 598              azimuth = payload[2] & 0x3F
 599              chirp_counter = payload[3] & 0x1F
 600              
 601              return {
 602                  'type': 'detection',
 603                  'detected': detection_flag,
 604                  'elevation': elevation,
 605                  'azimuth': azimuth,
 606                  'chirp': chirp_counter,
 607                  'timestamp': time.time()
 608              }
 609          except Exception as e:
 610              logging.error(f"Error parsing detection packet: {e}")
 611              return None
 612  
 613  class RadarGUI:
 614      def __init__(self, root):
 615          self.root = root
 616          self.root.title("Advanced Radar System GUI - USB CDC")
 617          self.root.geometry("1400x900")
 618          
 619          # Initialize interfaces
 620          self.stm32_usb_interface = STM32USBInterface()
 621          self.ftdi_interface = FTDIInterface()
 622          self.radar_processor = RadarProcessor()
 623          self.usb_packet_parser = USBPacketParser()
 624          self.radar_packet_parser = RadarPacketParser()
 625          self.settings = RadarSettings()
 626          
 627          # Data queues
 628          self.radar_data_queue = queue.Queue()
 629          self.gps_data_queue = queue.Queue()
 630          
 631          # Thread control
 632          self.running = False
 633          self.radar_thread = None
 634          self.gps_thread = None
 635          
 636          # Counters
 637          self.received_packets = 0
 638          self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, timestamp=0)
 639          
 640          self.create_gui()
 641          self.start_background_threads()
 642      
 643      def create_gui(self):
 644          """Create the main GUI with tabs"""
 645          self.notebook = ttk.Notebook(self.root)
 646          self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
 647          
 648          self.tab_main = ttk.Frame(self.notebook)
 649          self.tab_map = ttk.Frame(self.notebook)
 650          self.tab_diagnostics = ttk.Frame(self.notebook)
 651          self.tab_settings = ttk.Frame(self.notebook)
 652          
 653          self.notebook.add(self.tab_main, text='Main View')
 654          self.notebook.add(self.tab_map, text='Map View')
 655          self.notebook.add(self.tab_diagnostics, text='Diagnostics')
 656          self.notebook.add(self.tab_settings, text='Settings')
 657          
 658          self.setup_main_tab()
 659          self.setup_map_tab()
 660          self.setup_settings_tab()
 661      
 662      def setup_main_tab(self):
 663          """Setup the main radar display tab"""
 664          # Control frame
 665          control_frame = ttk.Frame(self.tab_main)
 666          control_frame.pack(fill='x', padx=10, pady=5)
 667          
 668          # USB Device selection
 669          ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5)
 670          self.stm32_usb_combo = ttk.Combobox(control_frame, state="readonly", width=40)
 671          self.stm32_usb_combo.grid(row=0, column=1, padx=5)
 672          
 673          ttk.Label(control_frame, text="FTDI Device:").grid(row=0, column=2, padx=5)
 674          self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30)
 675          self.ftdi_combo.grid(row=0, column=3, padx=5)
 676          
 677          ttk.Button(control_frame, text="Refresh Devices", 
 678                    command=self.refresh_devices).grid(row=0, column=4, padx=5)
 679          
 680          self.start_button = ttk.Button(control_frame, text="Start Radar", 
 681                                        command=self.start_radar)
 682          self.start_button.grid(row=0, column=5, padx=5)
 683          
 684          self.stop_button = ttk.Button(control_frame, text="Stop Radar", 
 685                                       command=self.stop_radar, state="disabled")
 686          self.stop_button.grid(row=0, column=6, padx=5)
 687          
 688          # GPS info
 689          self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...")
 690          self.gps_label.grid(row=1, column=0, columnspan=4, sticky='w', padx=5, pady=2)
 691          
 692          # Status info
 693          self.status_label = ttk.Label(control_frame, text="Status: Ready")
 694          self.status_label.grid(row=1, column=4, columnspan=3, sticky='e', padx=5, pady=2)
 695          
 696          # Main display area
 697          display_frame = ttk.Frame(self.tab_main)
 698          display_frame.pack(fill='both', expand=True, padx=10, pady=5)
 699          
 700          # Range-Doppler Map
 701          fig = Figure(figsize=(10, 6))
 702          self.range_doppler_ax = fig.add_subplot(111)
 703          self.range_doppler_plot = self.range_doppler_ax.imshow(
 704              np.random.rand(1024, 32), aspect='auto', cmap='hot', 
 705              extent=[0, 32, 0, 1024])
 706          self.range_doppler_ax.set_title('Range-Doppler Map')
 707          self.range_doppler_ax.set_xlabel('Doppler Bin')
 708          self.range_doppler_ax.set_ylabel('Range Bin')
 709          
 710          self.canvas = FigureCanvasTkAgg(fig, display_frame)
 711          self.canvas.draw()
 712          self.canvas.get_tk_widget().pack(side='left', fill='both', expand=True)
 713          
 714          # Targets list
 715          targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets")
 716          targets_frame.pack(side='right', fill='y', padx=5)
 717          
 718          self.targets_tree = ttk.Treeview(targets_frame, 
 719                                         columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR'), 
 720                                         show='headings', height=20)
 721          self.targets_tree.heading('ID', text='Track ID')
 722          self.targets_tree.heading('Range', text='Range (m)')
 723          self.targets_tree.heading('Velocity', text='Velocity (m/s)')
 724          self.targets_tree.heading('Azimuth', text='Azimuth')
 725          self.targets_tree.heading('Elevation', text='Elevation')
 726          self.targets_tree.heading('SNR', text='SNR (dB)')
 727          
 728          self.targets_tree.column('ID', width=80)
 729          self.targets_tree.column('Range', width=100)
 730          self.targets_tree.column('Velocity', width=100)
 731          self.targets_tree.column('Azimuth', width=80)
 732          self.targets_tree.column('Elevation', width=80)
 733          self.targets_tree.column('SNR', width=80)
 734          
 735          self.targets_tree.pack(fill='both', expand=True, padx=5, pady=5)
 736      
 737      def setup_map_tab(self):
 738          """Setup the map display tab"""
 739          self.map_frame = ttk.Frame(self.tab_map)
 740          self.map_frame.pack(fill='both', expand=True, padx=10, pady=10)
 741          
 742          # Map placeholder
 743          self.map_label = ttk.Label(self.map_frame, text="Map will be displayed here after GPS data is received", 
 744                                   font=('Arial', 12))
 745          self.map_label.pack(expand=True)
 746      
 747      def setup_settings_tab(self):
 748          """Setup the settings tab"""
 749          settings_frame = ttk.Frame(self.tab_settings)
 750          settings_frame.pack(fill='both', expand=True, padx=10, pady=10)
 751          
 752          entries = [
 753              ('System Frequency (Hz):', 'system_frequency', 10e9),
 754              ('Chirp Duration (s):', 'chirp_duration', 30e-6),
 755              ('Chirps per Position:', 'chirps_per_position', 32),
 756              ('Frequency Min (Hz):', 'freq_min', 10e6),
 757              ('Frequency Max (Hz):', 'freq_max', 30e6),
 758              ('PRF1 (Hz):', 'prf1', 1000),
 759              ('PRF2 (Hz):', 'prf2', 2000),
 760              ('Max Distance (m):', 'max_distance', 50000)
 761          ]
 762          
 763          self.settings_vars = {}
 764          
 765          for i, (label, attr, default) in enumerate(entries):
 766              ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky='w', padx=5, pady=5)
 767              var = tk.StringVar(value=str(default))
 768              entry = ttk.Entry(settings_frame, textvariable=var, width=20)
 769              entry.grid(row=i, column=1, padx=5, pady=5)
 770              self.settings_vars[attr] = var
 771          
 772          ttk.Button(settings_frame, text="Apply Settings", 
 773                    command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10)
 774      
 775      def refresh_devices(self):
 776          """Refresh available USB devices"""
 777          # STM32 USB devices
 778          stm32_devices = self.stm32_usb_interface.list_devices()
 779          stm32_names = [dev['description'] for dev in stm32_devices]
 780          self.stm32_usb_combo['values'] = stm32_names
 781          
 782          # FTDI devices
 783          ftdi_devices = self.ftdi_interface.list_devices()
 784          ftdi_names = [dev['description'] for dev in ftdi_devices]
 785          self.ftdi_combo['values'] = ftdi_names
 786          
 787          if stm32_names:
 788              self.stm32_usb_combo.current(0)
 789          if ftdi_names:
 790              self.ftdi_combo.current(0)
 791      
 792      def start_radar(self):
 793          """Step 11: Start button pressed - Begin radar operation"""
 794          try:
 795              # Open STM32 USB device
 796              stm32_index = self.stm32_usb_combo.current()
 797              if stm32_index == -1:
 798                  messagebox.showerror("Error", "Please select an STM32 USB device")
 799                  return
 800                  
 801              stm32_devices = self.stm32_usb_interface.list_devices()
 802              if stm32_index >= len(stm32_devices):
 803                  messagebox.showerror("Error", "Invalid STM32 device selection")
 804                  return
 805                  
 806              if not self.stm32_usb_interface.open_device(stm32_devices[stm32_index]):
 807                  messagebox.showerror("Error", "Failed to open STM32 USB device")
 808                  return
 809              
 810              # Open FTDI device
 811              if FTDI_AVAILABLE:
 812                  ftdi_index = self.ftdi_combo.current()
 813                  if ftdi_index != -1:
 814                      ftdi_devices = self.ftdi_interface.list_devices()
 815                      if ftdi_index < len(ftdi_devices):
 816                          device_url = ftdi_devices[ftdi_index]['url']
 817                          if not self.ftdi_interface.open_device(device_url):
 818                              logging.warning("Failed to open FTDI device, continuing without radar data")
 819                  else:
 820                      logging.warning("No FTDI device selected, continuing without radar data")
 821              else:
 822                  logging.warning("FTDI not available, continuing without radar data")
 823              
 824              # Step 12: Send start flag to STM32 via USB
 825              if not self.stm32_usb_interface.send_start_flag():
 826                  messagebox.showerror("Error", "Failed to send start flag to STM32")
 827                  return
 828              
 829              # Step 13: Send settings to STM32 via USB
 830              self.apply_settings()
 831              
 832              # Start radar operation
 833              self.running = True
 834              self.start_button.config(state="disabled")
 835              self.stop_button.config(state="normal")
 836              self.status_label.config(text="Status: Radar running - Waiting for GPS data...")
 837              
 838              logging.info("Radar system started successfully via USB CDC")
 839              
 840          except Exception as e:
 841              messagebox.showerror("Error", f"Failed to start radar: {e}")
 842              logging.error(f"Start radar error: {e}")
 843      
 844      def stop_radar(self):
 845          """Stop radar operation"""
 846          self.running = False
 847          self.start_button.config(state="normal")
 848          self.stop_button.config(state="disabled")
 849          self.status_label.config(text="Status: Radar stopped")
 850          
 851          self.stm32_usb_interface.close()
 852          self.ftdi_interface.close()
 853          
 854          logging.info("Radar system stopped")
 855      
 856      def apply_settings(self):
 857          """Step 13: Apply and send radar settings via USB"""
 858          try:
 859              self.settings.system_frequency = float(self.settings_vars['system_frequency'].get())
 860              self.settings.chirp_duration = float(self.settings_vars['chirp_duration'].get())
 861              self.settings.chirps_per_position = int(self.settings_vars['chirps_per_position'].get())
 862              self.settings.freq_min = float(self.settings_vars['freq_min'].get())
 863              self.settings.freq_max = float(self.settings_vars['freq_max'].get())
 864              self.settings.prf1 = float(self.settings_vars['prf1'].get())
 865              self.settings.prf2 = float(self.settings_vars['prf2'].get())
 866              self.settings.max_distance = float(self.settings_vars['max_distance'].get())
 867              
 868              if self.stm32_usb_interface.is_open:
 869                  self.stm32_usb_interface.send_settings(self.settings)
 870              
 871              messagebox.showinfo("Success", "Settings applied and sent to STM32 via USB")
 872              logging.info("Radar settings applied via USB")
 873              
 874          except ValueError as e:
 875              messagebox.showerror("Error", f"Invalid setting value: {e}")
 876      
 877      def start_background_threads(self):
 878          """Start background data processing threads"""
 879          self.radar_thread = threading.Thread(target=self.process_radar_data, daemon=True)
 880          self.radar_thread.start()
 881          
 882          self.gps_thread = threading.Thread(target=self.process_gps_data, daemon=True)
 883          self.gps_thread.start()
 884          
 885          self.root.after(100, self.update_gui)
 886      
 887      def process_radar_data(self):
 888          """Step 39: Process incoming radar data from FTDI"""
 889          buffer = b''
 890          while True:
 891              if self.running and self.ftdi_interface.is_open:
 892                  try:
 893                      data = self.ftdi_interface.read_data(4096)
 894                      if data:
 895                          buffer += data
 896                          
 897                          while len(buffer) >= 6:
 898                              packet = self.radar_packet_parser.parse_packet(buffer)
 899                              if packet:
 900                                  self.process_radar_packet(packet)
 901                                  packet_length = 4 + len(packet.get('payload', b'')) + 2
 902                                  buffer = buffer[packet_length:]
 903                                  self.received_packets += 1
 904                              else:
 905                                  break
 906                                  
 907                  except Exception as e:
 908                      logging.error(f"Error processing radar data: {e}")
 909                      time.sleep(0.1)
 910              else:
 911                  time.sleep(0.1)
 912      
 913      def process_gps_data(self):
 914          """Step 16/17: Process GPS data from STM32 via USB CDC"""
 915          while True:
 916              if self.running and self.stm32_usb_interface.is_open:
 917                  try:
 918                      # Read data from STM32 USB
 919                      data = self.stm32_usb_interface.read_data(64, timeout=100)
 920                      if data:
 921                          gps_data = self.usb_packet_parser.parse_gps_data(data)
 922                          if gps_data:
 923                              self.gps_data_queue.put(gps_data)
 924                              logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
 925                  except Exception as e:
 926                      logging.error(f"Error processing GPS data via USB: {e}")
 927              time.sleep(0.1)
 928      
 929      def process_radar_packet(self, packet):
 930          """Step 40: Process radar data and update displays"""
 931          try:
 932              if packet['type'] == 'range':
 933                  range_meters = packet['range'] * 0.1
 934                  
 935                  target = RadarTarget(
 936                      id=packet['chirp'],
 937                      range=range_meters,
 938                      velocity=0,
 939                      azimuth=packet['azimuth'],
 940                      elevation=packet['elevation'],
 941                      snr=20.0,
 942                      timestamp=packet['timestamp']
 943                  )
 944                  
 945                  self.update_range_doppler_map(target)
 946                  
 947              elif packet['type'] == 'doppler':
 948                  lambda_wavelength = 3e8 / self.settings.system_frequency
 949                  velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2)
 950                  self.update_target_velocity(packet, velocity)
 951                  
 952              elif packet['type'] == 'detection':
 953                  if packet['detected']:
 954                      logging.info(f"CFAR Detection: Elevation {packet['elevation']}, Azimuth {packet['azimuth']}")
 955                      
 956          except Exception as e:
 957              logging.error(f"Error processing radar packet: {e}")
 958      
 959      def update_range_doppler_map(self, target):
 960          """Update range-Doppler map with new target"""
 961          range_bin = min(int(target.range / 50), 1023)
 962          doppler_bin = min(abs(int(target.velocity)), 31)
 963          
 964          self.radar_processor.range_doppler_map[range_bin, doppler_bin] += 1
 965          
 966          self.radar_processor.detected_targets.append(target)
 967          
 968          if len(self.radar_processor.detected_targets) > 100:
 969              self.radar_processor.detected_targets = self.radar_processor.detected_targets[-100:]
 970      
 971      def update_target_velocity(self, packet, velocity):
 972          """Update target velocity information"""
 973          for target in self.radar_processor.detected_targets:
 974              if (target.azimuth == packet['azimuth'] and 
 975                  target.elevation == packet['elevation'] and
 976                  target.id == packet['chirp']):
 977                  target.velocity = velocity
 978                  break
 979      
 980      def update_gui(self):
 981          """Step 40: Update all GUI displays"""
 982          try:
 983              # Update status
 984              if self.running:
 985                  self.status_label.config(
 986                      text=f"Status: Running - Packets: {self.received_packets} - GPS: {self.current_gps.latitude:.4f}, {self.current_gps.longitude:.4f}")
 987              
 988              # Update range-Doppler map
 989              if hasattr(self, 'range_doppler_plot'):
 990                  display_data = np.log10(self.radar_processor.range_doppler_map + 1)
 991                  self.range_doppler_plot.set_array(display_data)
 992                  self.canvas.draw_idle()
 993              
 994              # Update targets list
 995              self.update_targets_list()
 996              
 997              # Update GPS display
 998              self.update_gps_display()
 999              
1000          except Exception as e:
1001              logging.error(f"Error updating GUI: {e}")
1002          
1003          self.root.after(100, self.update_gui)
1004      
1005      def update_targets_list(self):
1006          """Update the targets list display"""
1007          for item in self.targets_tree.get_children():
1008              self.targets_tree.delete(item)
1009          
1010          for target in self.radar_processor.detected_targets[-20:]:
1011              self.targets_tree.insert('', 'end', values=(
1012                  target.track_id, 
1013                  f"{target.range:.1f}", 
1014                  f"{target.velocity:.1f}",
1015                  target.azimuth,
1016                  target.elevation,
1017                  f"{target.snr:.1f}"
1018              ))
1019      
1020      def update_gps_display(self):
1021          """Step 18: Update GPS display and center map"""
1022          try:
1023              while not self.gps_data_queue.empty():
1024                  gps_data = self.gps_data_queue.get_nowait()
1025                  self.current_gps = gps_data
1026                  
1027                  # Update GPS label
1028                  self.gps_label.config(
1029                      text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
1030                  
1031                  # Update map
1032                  self.update_map_display(gps_data)
1033                  
1034          except queue.Empty:
1035              pass
1036      
1037      def update_map_display(self, gps_data):
1038          """Step 18: Update map display with current GPS position"""
1039          try:
1040              self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
1041                                       f"Altitude: {gps_data.altitude:.1f}m\n"
1042                                       f"Coverage: 50km radius\n"
1043                                       f"Map centered on GPS coordinates")
1044              
1045          except Exception as e:
1046              logging.error(f"Error updating map display: {e}")
1047  
1048  def main():
1049      """Main application entry point"""
1050      try:
1051          root = tk.Tk()
1052          app = RadarGUI(root)
1053          root.mainloop()
1054      except Exception as e:
1055          logging.error(f"Application error: {e}")
1056          messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
1057  
1058  if __name__ == "__main__":
1059      main()