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