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