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