1543 lines
60 KiB
Python
1543 lines
60 KiB
Python
|
|
"""
|
|
Radar System GUI - Demo Version
|
|
Compatible with FT601 USB 3.0 interface
|
|
Includes simulated radar data for demonstration
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
import threading
|
|
import queue
|
|
import time
|
|
import struct
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
|
|
from matplotlib.figure import Figure
|
|
from matplotlib.patches import Rectangle, Circle
|
|
import matplotlib.animation as animation
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Tuple, Optional
|
|
import math
|
|
import random
|
|
from datetime import datetime
|
|
import json
|
|
import os
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ============================================================================
|
|
# DATA CLASSES
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class RadarTarget:
|
|
"""Represents a detected radar target"""
|
|
id: int
|
|
range: float # meters
|
|
velocity: float # m/s
|
|
azimuth: float # degrees
|
|
elevation: float # degrees
|
|
snr: float # dB
|
|
timestamp: float
|
|
track_id: int = -1
|
|
range_bin: int = 0
|
|
doppler_bin: int = 0
|
|
|
|
@dataclass
|
|
class RadarSettings:
|
|
"""Radar system settings"""
|
|
system_frequency: float = 10e9 # Hz
|
|
chirp_duration_long: float = 30e-6 # seconds
|
|
chirp_duration_short: float = 0.5e-6 # seconds
|
|
chirps_per_frame: int = 32
|
|
range_bins: int = 1024
|
|
doppler_bins: int = 32
|
|
prf: float = 1000 # Hz
|
|
max_range: float = 5000 # meters
|
|
max_velocity: float = 100 # m/s
|
|
cfar_threshold: float = 13.0 # dB
|
|
beam_width: float = 3.0 # degrees
|
|
|
|
@dataclass
|
|
class SystemStatus:
|
|
"""System status information"""
|
|
uptime: float = 0
|
|
temperature: float = 25.0
|
|
fpga_utilization: float = 0
|
|
usb_data_rate: float = 0
|
|
packets_received: int = 0
|
|
packets_lost: int = 0
|
|
errors: int = 0
|
|
warnings: List[str] = None
|
|
|
|
# ============================================================================
|
|
# SIMULATED RADAR DATA GENERATOR
|
|
# ============================================================================
|
|
|
|
class RadarDataSimulator:
|
|
"""Generates simulated radar data for demo mode"""
|
|
|
|
def __init__(self):
|
|
self.settings = RadarSettings()
|
|
self.targets = []
|
|
self.frame_count = 0
|
|
self.create_test_scenario()
|
|
|
|
def create_test_scenario(self):
|
|
"""Create a test scenario with moving targets"""
|
|
# Target 1: Fast-moving aircraft
|
|
self.targets.append({
|
|
'id': 1,
|
|
'range': 2500,
|
|
'velocity': -80, # Approaching
|
|
'azimuth': 45,
|
|
'elevation': 5,
|
|
'snr': 25,
|
|
'range_drift': -0.5, # m per frame
|
|
'azimuth_drift': 0.1, # deg per frame
|
|
'velocity_drift': 0
|
|
})
|
|
|
|
# Target 2: Slow-moving vehicle
|
|
self.targets.append({
|
|
'id': 2,
|
|
'range': 800,
|
|
'velocity': 15, # Receding
|
|
'azimuth': -30,
|
|
'elevation': 0,
|
|
'snr': 18,
|
|
'range_drift': 0.2,
|
|
'azimuth_drift': -0.05,
|
|
'velocity_drift': 0.1
|
|
})
|
|
|
|
# Target 3: Stationary object
|
|
self.targets.append({
|
|
'id': 3,
|
|
'range': 1500,
|
|
'velocity': 0,
|
|
'azimuth': 10,
|
|
'elevation': 2,
|
|
'snr': 22,
|
|
'range_drift': 0,
|
|
'azimuth_drift': 0,
|
|
'velocity_drift': 0
|
|
})
|
|
|
|
# Target 4: Helicopter (low speed, high SNR)
|
|
self.targets.append({
|
|
'id': 4,
|
|
'range': 1200,
|
|
'velocity': -5,
|
|
'azimuth': 75,
|
|
'elevation': 15,
|
|
'snr': 30,
|
|
'range_drift': -0.1,
|
|
'azimuth_drift': -0.2,
|
|
'velocity_drift': 0.05
|
|
})
|
|
|
|
# Target 5: Drone (small, fluctuating)
|
|
self.targets.append({
|
|
'id': 5,
|
|
'range': 300,
|
|
'velocity': 10,
|
|
'azimuth': -60,
|
|
'elevation': 8,
|
|
'snr': 12,
|
|
'range_drift': 0.3,
|
|
'azimuth_drift': 0.4,
|
|
'velocity_drift': -0.2
|
|
})
|
|
|
|
def generate_range_doppler_map(self):
|
|
"""Generate simulated range-Doppler map"""
|
|
map_data = np.zeros((self.settings.range_bins, self.settings.doppler_bins))
|
|
|
|
# Add noise floor
|
|
noise_floor = 10 * np.random.random(map_data.shape)
|
|
map_data += noise_floor
|
|
|
|
# Add targets
|
|
for target in self.targets:
|
|
# Update target position
|
|
target['range'] += target['range_drift']
|
|
target['azimuth'] += target['azimuth_drift']
|
|
target['velocity'] += target['velocity_drift']
|
|
|
|
# Keep within bounds
|
|
target['range'] = max(100, min(5000, target['range']))
|
|
target['azimuth'] = max(-90, min(90, target['azimuth']))
|
|
target['velocity'] = max(-100, min(100, target['velocity']))
|
|
|
|
# Convert to bin indices
|
|
range_bin = int((target['range'] / self.settings.max_range) *
|
|
(self.settings.range_bins - 1))
|
|
doppler_bin = int(((target['velocity'] + self.settings.max_velocity) /
|
|
(2 * self.settings.max_velocity)) *
|
|
(self.settings.doppler_bins - 1))
|
|
|
|
# Ensure valid indices
|
|
range_bin = max(0, min(self.settings.range_bins - 1, range_bin))
|
|
doppler_bin = max(0, min(self.settings.doppler_bins - 1, doppler_bin))
|
|
|
|
# Add target peak with spreading
|
|
for r in range(max(0, range_bin-2), min(self.settings.range_bins, range_bin+3)):
|
|
for d in range(max(0, doppler_bin-2), min(self.settings.doppler_bins, doppler_bin+3)):
|
|
distance = np.sqrt((r - range_bin)**2 + (d - doppler_bin)**2)
|
|
if distance < 3:
|
|
amplitude = target['snr'] * np.exp(-distance/2)
|
|
map_data[r, d] += amplitude * (0.8 + 0.4 * np.random.random())
|
|
|
|
# Add some clutter
|
|
clutter_bins = np.random.randint(0, self.settings.range_bins, 20)
|
|
for cb in clutter_bins:
|
|
map_data[cb, :] += 5 * np.random.random(self.settings.doppler_bins)
|
|
|
|
self.frame_count += 1
|
|
return map_data
|
|
|
|
def get_detected_targets(self):
|
|
"""Get list of detected targets"""
|
|
detected = []
|
|
for i, target in enumerate(self.targets):
|
|
# Random detection probability based on SNR
|
|
if np.random.random() < (target['snr'] / 40):
|
|
detected.append(RadarTarget(
|
|
id=target['id'],
|
|
range=target['range'],
|
|
velocity=target['velocity'],
|
|
azimuth=target['azimuth'],
|
|
elevation=target['elevation'],
|
|
snr=target['snr'] + 2 * np.random.random() - 1,
|
|
timestamp=time.time(),
|
|
track_id=target['id'],
|
|
range_bin=int((target['range'] / self.settings.max_range) *
|
|
(self.settings.range_bins - 1)),
|
|
doppler_bin=int(((target['velocity'] + self.settings.max_velocity) /
|
|
(2 * self.settings.max_velocity)) *
|
|
(self.settings.doppler_bins - 1))
|
|
))
|
|
return detected
|
|
|
|
# ============================================================================
|
|
# SIMULATED USB INTERFACE
|
|
# ============================================================================
|
|
|
|
class SimulatedUSBInterface:
|
|
"""Simulates FT601 USB interface for demo mode"""
|
|
|
|
def __init__(self):
|
|
self.is_open = False
|
|
self.data_rate = 0
|
|
self.packet_count = 0
|
|
self.byte_count = 0
|
|
self.start_time = time.time()
|
|
|
|
def list_devices(self):
|
|
"""Return simulated devices"""
|
|
return [
|
|
{'description': 'FT601 USB 3.0 Device (Demo Mode)',
|
|
'vid': 0x0403, 'pid': 0x6030},
|
|
{'description': 'FT601 USB 3.0 Device - Channel 1',
|
|
'vid': 0x0403, 'pid': 0x6030}
|
|
]
|
|
|
|
def open_device(self, device_info):
|
|
"""Simulate opening device"""
|
|
self.is_open = True
|
|
self.start_time = time.time()
|
|
logger.info(f"Demo mode: Opened simulated FT601 device")
|
|
return True
|
|
|
|
def close(self):
|
|
"""Simulate closing device"""
|
|
self.is_open = False
|
|
logger.info("Demo mode: Closed simulated FT601 device")
|
|
|
|
def read_data(self, size=4096):
|
|
"""Simulate reading data (returns None in demo mode)"""
|
|
# In demo mode, we generate data separately
|
|
return None
|
|
|
|
def write_data(self, data):
|
|
"""Simulate writing data"""
|
|
self.packet_count += 1
|
|
self.byte_count += len(data)
|
|
elapsed = time.time() - self.start_time
|
|
if elapsed > 0:
|
|
self.data_rate = self.byte_count / elapsed / 1024 # KB/s
|
|
return True
|
|
|
|
def get_stats(self):
|
|
"""Get interface statistics"""
|
|
return {
|
|
'packets': self.packet_count,
|
|
'bytes': self.byte_count,
|
|
'data_rate': self.data_rate
|
|
}
|
|
|
|
# ============================================================================
|
|
# MAIN GUI APPLICATION
|
|
# ============================================================================
|
|
|
|
class RadarGUI:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Advanced Radar System GUI - Demo Mode")
|
|
self.root.geometry("1600x900")
|
|
|
|
# Set icon if available
|
|
try:
|
|
self.root.iconbitmap(default='radar.ico')
|
|
except:
|
|
pass
|
|
|
|
# Configure dark theme
|
|
self.setup_dark_theme()
|
|
|
|
# Initialize components
|
|
self.settings = RadarSettings()
|
|
self.status = SystemStatus()
|
|
self.simulator = RadarDataSimulator()
|
|
self.usb_interface = SimulatedUSBInterface()
|
|
|
|
# Data queues
|
|
self.radar_data_queue = queue.Queue(maxsize=100)
|
|
self.command_queue = queue.Queue()
|
|
|
|
# Control flags
|
|
self.demo_mode = tk.BooleanVar(value=True)
|
|
self.running = False
|
|
self.recording = False
|
|
self.auto_scan = tk.BooleanVar(value=True)
|
|
self.show_grid = tk.BooleanVar(value=True)
|
|
self.show_targets = tk.BooleanVar(value=True)
|
|
self.color_map = tk.StringVar(value='hot')
|
|
|
|
# Data storage
|
|
self.range_doppler_map = np.zeros((1024, 32))
|
|
self.detected_targets = []
|
|
self.target_history = []
|
|
self.recorded_frames = []
|
|
|
|
# Animation
|
|
self.animation_running = True
|
|
self.frame_count = 0
|
|
self.fps = 0
|
|
self.last_frame_time = time.time()
|
|
|
|
# Create GUI
|
|
self.create_menu()
|
|
self.create_gui()
|
|
self.create_status_bar()
|
|
|
|
# Start background threads
|
|
self.start_background_threads()
|
|
|
|
# Start animation
|
|
self.animate()
|
|
|
|
# Handle window close
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
logger.info("Radar GUI initialized in demo mode")
|
|
|
|
def setup_dark_theme(self):
|
|
"""Configure dark theme for the GUI"""
|
|
self.style = ttk.Style()
|
|
self.style.theme_use('clam')
|
|
|
|
# Dark theme colors
|
|
dark_bg = '#2b2b2b'
|
|
dark_fg = '#e0e0e0'
|
|
dark_select = '#404040'
|
|
dark_button = '#3c3f41'
|
|
|
|
# Configure styles
|
|
self.style.configure('.',
|
|
background=dark_bg,
|
|
foreground=dark_fg,
|
|
fieldbackground=dark_bg,
|
|
troughcolor=dark_select)
|
|
|
|
self.style.configure('TLabel', background=dark_bg, foreground=dark_fg)
|
|
self.style.configure('TFrame', background=dark_bg)
|
|
self.style.configure('TLabelframe', background=dark_bg, foreground=dark_fg)
|
|
self.style.configure('TLabelframe.Label', background=dark_bg, foreground=dark_fg)
|
|
|
|
self.style.configure('TButton',
|
|
background=dark_button,
|
|
foreground=dark_fg,
|
|
borderwidth=1,
|
|
focuscolor='none')
|
|
self.style.map('TButton',
|
|
background=[('active', '#4e5254'),
|
|
('pressed', '#2d2f31')])
|
|
|
|
self.style.configure('TNotebook', background=dark_bg)
|
|
self.style.configure('TNotebook.Tab',
|
|
background=dark_select,
|
|
foreground=dark_fg,
|
|
padding=[10, 2])
|
|
self.style.map('TNotebook.Tab',
|
|
background=[('selected', dark_button)])
|
|
|
|
self.style.configure('Treeview',
|
|
background=dark_select,
|
|
foreground=dark_fg,
|
|
fieldbackground=dark_select)
|
|
self.style.map('Treeview',
|
|
background=[('selected', '#4e5254')])
|
|
|
|
self.style.configure('Horizontal.TScale', background=dark_bg)
|
|
self.style.configure('Vertical.TScale', background=dark_bg)
|
|
|
|
def create_menu(self):
|
|
"""Create application menu"""
|
|
menubar = tk.Menu(self.root)
|
|
self.root.config(menu=menubar)
|
|
|
|
# File menu
|
|
file_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="File", menu=file_menu)
|
|
file_menu.add_command(label="Connect to Hardware", command=self.connect_hardware)
|
|
file_menu.add_command(label="Demo Mode", command=self.enable_demo_mode)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Start Recording", command=self.start_recording)
|
|
file_menu.add_command(label="Stop Recording", command=self.stop_recording)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Load Configuration", command=self.load_config)
|
|
file_menu.add_command(label="Save Configuration", command=self.save_config)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Export Data", command=self.export_data)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Exit", command=self.on_closing)
|
|
|
|
# View menu
|
|
view_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="View", menu=view_menu)
|
|
view_menu.add_checkbutton(label="Show Grid", variable=self.show_grid)
|
|
view_menu.add_checkbutton(label="Show Targets", variable=self.show_targets)
|
|
view_menu.add_checkbutton(label="Auto Scan", variable=self.auto_scan)
|
|
view_menu.add_separator()
|
|
|
|
# Color map submenu
|
|
color_menu = tk.Menu(view_menu, tearoff=0)
|
|
view_menu.add_cascade(label="Color Map", menu=color_menu)
|
|
for cmap in ['hot', 'jet', 'viridis', 'plasma', 'inferno', 'magma']:
|
|
color_menu.add_radiobutton(label=cmap.capitalize(),
|
|
variable=self.color_map,
|
|
value=cmap)
|
|
|
|
# Tools menu
|
|
tools_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="Tools", menu=tools_menu)
|
|
tools_menu.add_command(label="Calibration Wizard", command=self.calibration_wizard)
|
|
tools_menu.add_command(label="Beam Pattern Analysis", command=self.beam_analysis)
|
|
tools_menu.add_command(label="Noise Floor Measurement", command=self.noise_measurement)
|
|
tools_menu.add_separator()
|
|
tools_menu.add_command(label="Diagnostics", command=self.show_diagnostics)
|
|
|
|
# Help menu
|
|
help_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="Help", menu=help_menu)
|
|
help_menu.add_command(label="Documentation", command=self.show_docs)
|
|
help_menu.add_command(label="About", command=self.show_about)
|
|
|
|
def create_gui(self):
|
|
"""Create main GUI layout"""
|
|
# Main container
|
|
main_frame = ttk.Frame(self.root)
|
|
main_frame.pack(fill='both', expand=True, padx=5, pady=5)
|
|
|
|
# Control panel (top)
|
|
control_frame = ttk.LabelFrame(main_frame, text="System Control", padding=5)
|
|
control_frame.pack(fill='x', pady=(0, 5))
|
|
|
|
self.create_control_panel(control_frame)
|
|
|
|
# Notebook for tabs
|
|
self.notebook = ttk.Notebook(main_frame)
|
|
self.notebook.pack(fill='both', expand=True)
|
|
|
|
# Create tabs
|
|
self.create_radar_tab()
|
|
self.create_scope_tab()
|
|
self.create_targets_tab()
|
|
self.create_spectrum_tab()
|
|
self.create_history_tab()
|
|
self.create_settings_tab()
|
|
|
|
def create_control_panel(self, parent):
|
|
"""Create system control panel"""
|
|
# Left side - Mode and connection
|
|
left_frame = ttk.Frame(parent)
|
|
left_frame.pack(side='left', fill='x', expand=True)
|
|
|
|
ttk.Label(left_frame, text="Mode:").grid(row=0, column=0, padx=5, pady=2, sticky='w')
|
|
mode_combo = ttk.Combobox(left_frame, textvariable=tk.StringVar(value="Demo Mode"),
|
|
values=["Demo Mode", "Hardware Mode"],
|
|
state="readonly", width=15)
|
|
mode_combo.grid(row=0, column=1, padx=5, pady=2, sticky='w')
|
|
|
|
ttk.Label(left_frame, text="Device:").grid(row=0, column=2, padx=5, pady=2, sticky='w')
|
|
self.device_combo = ttk.Combobox(left_frame,
|
|
values=["FT601 Demo Device"],
|
|
state="readonly", width=25)
|
|
self.device_combo.grid(row=0, column=3, padx=5, pady=2, sticky='w')
|
|
self.device_combo.current(0)
|
|
|
|
ttk.Button(left_frame, text="Refresh",
|
|
command=self.refresh_devices).grid(row=0, column=4, padx=2, pady=2)
|
|
|
|
ttk.Button(left_frame, text="Connect",
|
|
command=self.connect_device).grid(row=0, column=5, padx=2, pady=2)
|
|
|
|
# Right side - Start/Stop
|
|
right_frame = ttk.Frame(parent)
|
|
right_frame.pack(side='right', padx=10)
|
|
|
|
self.start_button = ttk.Button(right_frame, text="▶ Start",
|
|
command=self.start_radar, width=10)
|
|
self.start_button.pack(side='left', padx=2)
|
|
|
|
self.stop_button = ttk.Button(right_frame, text="■ Stop",
|
|
command=self.stop_radar, width=10,
|
|
state='disabled')
|
|
self.stop_button.pack(side='left', padx=2)
|
|
|
|
ttk.Button(right_frame, text="⚙ Settings",
|
|
command=lambda: self.notebook.select(5)).pack(side='left', padx=2)
|
|
|
|
def create_radar_tab(self):
|
|
"""Create radar display tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Radar Display")
|
|
|
|
# Main display area
|
|
display_frame = ttk.Frame(tab)
|
|
display_frame.pack(fill='both', expand=True, padx=5, pady=5)
|
|
|
|
# Range-Doppler map
|
|
map_frame = ttk.LabelFrame(display_frame, text="Range-Doppler Map", padding=5)
|
|
map_frame.pack(side='left', fill='both', expand=True)
|
|
|
|
# Create matplotlib figure
|
|
self.rd_fig = Figure(figsize=(8, 6), facecolor='#2b2b2b')
|
|
self.rd_ax = self.rd_fig.add_subplot(111)
|
|
self.rd_ax.set_facecolor('#1a1a1a')
|
|
|
|
# Initialize plot
|
|
self.rd_img = self.rd_ax.imshow(
|
|
np.random.rand(1024, 32),
|
|
aspect='auto',
|
|
cmap=self.color_map.get(),
|
|
extent=[-self.settings.max_velocity,
|
|
self.settings.max_velocity,
|
|
self.settings.max_range, 0],
|
|
interpolation='bilinear'
|
|
)
|
|
|
|
self.rd_ax.set_xlabel('Velocity (m/s)', color='white')
|
|
self.rd_ax.set_ylabel('Range (m)', color='white')
|
|
self.rd_ax.tick_params(colors='white')
|
|
|
|
# Add colorbar
|
|
self.rd_cbar = self.rd_fig.colorbar(self.rd_img, ax=self.rd_ax)
|
|
self.rd_cbar.ax.yaxis.set_tick_params(color='white')
|
|
self.rd_cbar.ax.set_ylabel('Power (dB)', color='white')
|
|
plt.setp(plt.getp(self.rd_cbar.ax.axes, 'yticklabels'), color='white')
|
|
|
|
# Embed in tkinter
|
|
self.rd_canvas = FigureCanvasTkAgg(self.rd_fig, map_frame)
|
|
self.rd_canvas.draw()
|
|
self.rd_canvas.get_tk_widget().pack(fill='both', expand=True)
|
|
|
|
# Toolbar
|
|
toolbar = NavigationToolbar2Tk(self.rd_canvas, map_frame)
|
|
toolbar.update()
|
|
|
|
# Target list panel
|
|
target_frame = ttk.LabelFrame(display_frame, text="Detected Targets", padding=5, width=300)
|
|
target_frame.pack(side='right', fill='y', padx=(5, 0))
|
|
target_frame.pack_propagate(False)
|
|
|
|
# Treeview for targets
|
|
columns = ('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR')
|
|
self.target_tree = ttk.Treeview(target_frame, columns=columns, show='headings', height=20)
|
|
|
|
# Define headings
|
|
self.target_tree.heading('ID', text='ID')
|
|
self.target_tree.heading('Range', text='Range (m)')
|
|
self.target_tree.heading('Velocity', text='Vel (m/s)')
|
|
self.target_tree.heading('Azimuth', text='Az (°)')
|
|
self.target_tree.heading('Elevation', text='El (°)')
|
|
self.target_tree.heading('SNR', text='SNR (dB)')
|
|
|
|
# Set column widths
|
|
self.target_tree.column('ID', width=40)
|
|
self.target_tree.column('Range', width=70)
|
|
self.target_tree.column('Velocity', width=70)
|
|
self.target_tree.column('Azimuth', width=60)
|
|
self.target_tree.column('Elevation', width=60)
|
|
self.target_tree.column('SNR', width=60)
|
|
|
|
# Add scrollbar
|
|
scrollbar = ttk.Scrollbar(target_frame, orient='vertical',
|
|
command=self.target_tree.yview)
|
|
self.target_tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
self.target_tree.pack(side='left', fill='both', expand=True)
|
|
scrollbar.pack(side='right', fill='y')
|
|
|
|
def create_scope_tab(self):
|
|
"""Create A-scope tab (range profile)"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="A-Scope")
|
|
|
|
# Create figure
|
|
self.scope_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b')
|
|
self.scope_ax = self.scope_fig.add_subplot(111)
|
|
self.scope_ax.set_facecolor('#1a1a1a')
|
|
|
|
# Initialize plot
|
|
self.scope_line, = self.scope_ax.plot([], [], 'g-', linewidth=1)
|
|
self.scope_ax.set_xlim(0, self.settings.max_range)
|
|
self.scope_ax.set_ylim(0, 100)
|
|
self.scope_ax.set_xlabel('Range (m)', color='white')
|
|
self.scope_ax.set_ylabel('Amplitude (dB)', color='white')
|
|
self.scope_ax.set_title('Range Profile (A-Scope)', color='white')
|
|
self.scope_ax.grid(True, alpha=0.3)
|
|
self.scope_ax.tick_params(colors='white')
|
|
|
|
self.scope_canvas = FigureCanvasTkAgg(self.scope_fig, tab)
|
|
self.scope_canvas.draw()
|
|
self.scope_canvas.get_tk_widget().pack(fill='both', expand=True)
|
|
|
|
# Control panel
|
|
control_frame = ttk.Frame(tab)
|
|
control_frame.pack(fill='x', pady=5)
|
|
|
|
ttk.Label(control_frame, text="Azimuth:").pack(side='left', padx=5)
|
|
self.scope_azimuth = ttk.Scale(control_frame, from_=-90, to=90,
|
|
orient='horizontal', length=200)
|
|
self.scope_azimuth.pack(side='left', padx=5)
|
|
self.scope_azimuth.set(0)
|
|
|
|
ttk.Label(control_frame, text="Elevation:").pack(side='left', padx=5)
|
|
self.scope_elevation = ttk.Scale(control_frame, from_=-45, to=45,
|
|
orient='horizontal', length=200)
|
|
self.scope_elevation.pack(side='left', padx=5)
|
|
self.scope_elevation.set(0)
|
|
|
|
def create_targets_tab(self):
|
|
"""Create PPI scope tab (plan position indicator)"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="PPI Scope")
|
|
|
|
# Create figure
|
|
self.ppi_fig = Figure(figsize=(8, 8), facecolor='#2b2b2b')
|
|
self.ppi_ax = self.ppi_fig.add_subplot(111, projection='polar')
|
|
self.ppi_ax.set_facecolor('#1a1a1a')
|
|
|
|
# Initialize plot
|
|
self.ppi_scatter = self.ppi_ax.scatter([], [], c=[], s=50, alpha=0.8, cmap='hot')
|
|
self.ppi_ax.set_ylim(0, self.settings.max_range)
|
|
self.ppi_ax.grid(True, alpha=0.3)
|
|
self.ppi_ax.tick_params(colors='white')
|
|
|
|
# Convert azimuth to radar convention (0° = North, clockwise)
|
|
self.ppi_ax.set_theta_zero_location('N')
|
|
self.ppi_ax.set_theta_direction(-1)
|
|
|
|
self.ppi_canvas = FigureCanvasTkAgg(self.ppi_fig, tab)
|
|
self.ppi_canvas.draw()
|
|
self.ppi_canvas.get_tk_widget().pack(fill='both', expand=True)
|
|
|
|
def create_spectrum_tab(self):
|
|
"""Create Doppler spectrum tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Doppler Spectrum")
|
|
|
|
# Create figure
|
|
self.spec_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b')
|
|
self.spec_ax = self.spec_fig.add_subplot(111)
|
|
self.spec_ax.set_facecolor('#1a1a1a')
|
|
|
|
# Initialize plot
|
|
self.spec_line, = self.spec_ax.plot([], [], 'b-', linewidth=1)
|
|
self.spec_ax.set_xlim(-self.settings.max_velocity, self.settings.max_velocity)
|
|
self.spec_ax.set_ylim(0, 100)
|
|
self.spec_ax.set_xlabel('Velocity (m/s)', color='white')
|
|
self.spec_ax.set_ylabel('Power (dB)', color='white')
|
|
self.spec_ax.set_title('Doppler Spectrum', color='white')
|
|
self.spec_ax.grid(True, alpha=0.3)
|
|
self.spec_ax.tick_params(colors='white')
|
|
|
|
self.spec_canvas = FigureCanvasTkAgg(self.spec_fig, tab)
|
|
self.spec_canvas.draw()
|
|
self.spec_canvas.get_tk_widget().pack(fill='both', expand=True)
|
|
|
|
# Range selector
|
|
range_frame = ttk.Frame(tab)
|
|
range_frame.pack(fill='x', pady=5)
|
|
|
|
ttk.Label(range_frame, text="Range Bin:").pack(side='left', padx=5)
|
|
self.spec_range = ttk.Scale(range_frame, from_=0, to=1023,
|
|
orient='horizontal', length=400)
|
|
self.spec_range.pack(side='left', padx=5)
|
|
self.spec_range.set(512)
|
|
|
|
ttk.Label(range_frame, text="512").pack(side='left', padx=5)
|
|
|
|
def create_history_tab(self):
|
|
"""Create target history tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Target History")
|
|
|
|
# Create figure
|
|
self.hist_fig = Figure(figsize=(10, 6), facecolor='#2b2b2b')
|
|
|
|
# Range history
|
|
self.hist_ax1 = self.hist_fig.add_subplot(211)
|
|
self.hist_ax1.set_facecolor('#1a1a1a')
|
|
self.hist_ax1.set_xlabel('Time (s)', color='white')
|
|
self.hist_ax1.set_ylabel('Range (m)', color='white')
|
|
self.hist_ax1.set_title('Target Range History', color='white')
|
|
self.hist_ax1.grid(True, alpha=0.3)
|
|
self.hist_ax1.tick_params(colors='white')
|
|
|
|
# Velocity history
|
|
self.hist_ax2 = self.hist_fig.add_subplot(212)
|
|
self.hist_ax2.set_facecolor('#1a1a1a')
|
|
self.hist_ax2.set_xlabel('Time (s)', color='white')
|
|
self.hist_ax2.set_ylabel('Velocity (m/s)', color='white')
|
|
self.hist_ax2.set_title('Target Velocity History', color='white')
|
|
self.hist_ax2.grid(True, alpha=0.3)
|
|
self.hist_ax2.tick_params(colors='white')
|
|
|
|
self.hist_canvas = FigureCanvasTkAgg(self.hist_fig, tab)
|
|
self.hist_canvas.draw()
|
|
self.hist_canvas.get_tk_widget().pack(fill='both', expand=True)
|
|
|
|
# Clear button
|
|
ttk.Button(tab, text="Clear History",
|
|
command=self.clear_history).pack(pady=5)
|
|
|
|
def create_settings_tab(self):
|
|
"""Create settings tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Settings")
|
|
|
|
# Create notebook for settings categories
|
|
settings_notebook = ttk.Notebook(tab)
|
|
settings_notebook.pack(fill='both', expand=True, padx=5, pady=5)
|
|
|
|
# Radar settings
|
|
radar_frame = ttk.Frame(settings_notebook)
|
|
settings_notebook.add(radar_frame, text="Radar")
|
|
|
|
self.create_radar_settings(radar_frame)
|
|
|
|
# Display settings
|
|
display_frame = ttk.Frame(settings_notebook)
|
|
settings_notebook.add(display_frame, text="Display")
|
|
|
|
self.create_display_settings(display_frame)
|
|
|
|
# Detection settings
|
|
detection_frame = ttk.Frame(settings_notebook)
|
|
settings_notebook.add(detection_frame, text="Detection")
|
|
|
|
self.create_detection_settings(detection_frame)
|
|
|
|
# Recording settings
|
|
record_frame = ttk.Frame(settings_notebook)
|
|
settings_notebook.add(record_frame, text="Recording")
|
|
|
|
self.create_recording_settings(record_frame)
|
|
|
|
# System settings
|
|
system_frame = ttk.Frame(settings_notebook)
|
|
settings_notebook.add(system_frame, text="System")
|
|
|
|
self.create_system_settings(system_frame)
|
|
|
|
def create_radar_settings(self, parent):
|
|
"""Create radar settings controls"""
|
|
# Create scrollable frame
|
|
canvas = tk.Canvas(parent, bg='#2b2b2b', highlightthickness=0)
|
|
scrollbar = ttk.Scrollbar(parent, orient='vertical', command=canvas.yview)
|
|
scrollable_frame = ttk.Frame(canvas)
|
|
|
|
scrollable_frame.bind(
|
|
"<Configure>",
|
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
|
)
|
|
|
|
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
|
|
# Settings variables
|
|
self.settings_vars = {}
|
|
|
|
settings = [
|
|
('System Frequency (GHz):', 'freq', 10.0, 1.0, 20.0),
|
|
('Long Chirp Duration (µs):', 'long_dur', 30.0, 1.0, 100.0),
|
|
('Short Chirp Duration (µs):', 'short_dur', 0.5, 0.1, 10.0),
|
|
('Chirps per Frame:', 'chirps', 32, 1, 128),
|
|
('Range Bins:', 'range_bins', 1024, 64, 2048),
|
|
('Doppler Bins:', 'doppler_bins', 32, 8, 128),
|
|
('PRF (Hz):', 'prf', 1000, 100, 10000),
|
|
('Max Range (m):', 'max_range', 5000, 100, 50000),
|
|
('Max Velocity (m/s):', 'max_vel', 100, 10, 500),
|
|
('Beam Width (°):', 'beam_width', 3.0, 0.5, 10.0)
|
|
]
|
|
|
|
for i, (label, key, default, minv, maxv) in enumerate(settings):
|
|
frame = ttk.Frame(scrollable_frame)
|
|
frame.pack(fill='x', padx=10, pady=5)
|
|
|
|
ttk.Label(frame, text=label, width=25).pack(side='left')
|
|
|
|
var = tk.DoubleVar(value=default)
|
|
self.settings_vars[key] = var
|
|
|
|
entry = ttk.Entry(frame, textvariable=var, width=15)
|
|
entry.pack(side='left', padx=5)
|
|
|
|
ttk.Label(frame, text=f"({minv}-{maxv})").pack(side='left')
|
|
|
|
canvas.pack(side='left', fill='both', expand=True)
|
|
scrollbar.pack(side='right', fill='y')
|
|
|
|
# Apply button
|
|
ttk.Button(scrollable_frame, text="Apply Settings",
|
|
command=self.apply_settings).pack(pady=10)
|
|
|
|
def create_display_settings(self, parent):
|
|
"""Create display settings controls"""
|
|
frame = ttk.Frame(parent)
|
|
frame.pack(fill='both', expand=True, padx=20, pady=20)
|
|
|
|
# Update rate
|
|
ttk.Label(frame, text="Update Rate (Hz):").grid(row=0, column=0,
|
|
sticky='w', pady=5)
|
|
self.update_rate = ttk.Scale(frame, from_=1, to=60,
|
|
orient='horizontal', length=200)
|
|
self.update_rate.set(20)
|
|
self.update_rate.grid(row=0, column=1, padx=10, pady=5)
|
|
ttk.Label(frame, text="20").grid(row=0, column=2, sticky='w')
|
|
|
|
# Color map
|
|
ttk.Label(frame, text="Color Map:").grid(row=1, column=0,
|
|
sticky='w', pady=5)
|
|
cmap_combo = ttk.Combobox(frame, textvariable=self.color_map,
|
|
values=['hot', 'jet', 'viridis', 'plasma'],
|
|
state='readonly', width=15)
|
|
cmap_combo.grid(row=1, column=1, padx=10, pady=5, sticky='w')
|
|
|
|
# Grid
|
|
ttk.Checkbutton(frame, text="Show Grid",
|
|
variable=self.show_grid).grid(row=2, column=0,
|
|
columnspan=2, sticky='w', pady=5)
|
|
|
|
# Targets
|
|
ttk.Checkbutton(frame, text="Show Targets",
|
|
variable=self.show_targets).grid(row=3, column=0,
|
|
columnspan=2, sticky='w', pady=5)
|
|
|
|
# Auto scan
|
|
ttk.Checkbutton(frame, text="Auto Scan",
|
|
variable=self.auto_scan).grid(row=4, column=0,
|
|
columnspan=2, sticky='w', pady=5)
|
|
|
|
# Background color
|
|
ttk.Label(frame, text="Background:").grid(row=5, column=0,
|
|
sticky='w', pady=5)
|
|
bg_colors = ['Dark', 'Black', 'White']
|
|
bg_combo = ttk.Combobox(frame, values=bg_colors, state='readonly', width=15)
|
|
bg_combo.set('Dark')
|
|
bg_combo.grid(row=5, column=1, padx=10, pady=5, sticky='w')
|
|
|
|
def create_detection_settings(self, parent):
|
|
"""Create detection settings controls"""
|
|
frame = ttk.Frame(parent)
|
|
frame.pack(fill='both', expand=True, padx=20, pady=20)
|
|
|
|
# CFAR threshold
|
|
ttk.Label(frame, text="CFAR Threshold (dB):").grid(row=0, column=0,
|
|
sticky='w', pady=5)
|
|
self.cfar_threshold = ttk.Scale(frame, from_=5, to=30,
|
|
orient='horizontal', length=200)
|
|
self.cfar_threshold.set(13)
|
|
self.cfar_threshold.grid(row=0, column=1, padx=10, pady=5)
|
|
ttk.Label(frame, text="13.0").grid(row=0, column=2, sticky='w')
|
|
|
|
# Detection sensitivity
|
|
ttk.Label(frame, text="Sensitivity:").grid(row=1, column=0,
|
|
sticky='w', pady=5)
|
|
self.sensitivity = ttk.Scale(frame, from_=0, to=100,
|
|
orient='horizontal', length=200)
|
|
self.sensitivity.set(75)
|
|
self.sensitivity.grid(row=1, column=1, padx=10, pady=5)
|
|
ttk.Label(frame, text="75%").grid(row=1, column=2, sticky='w')
|
|
|
|
# Min SNR
|
|
ttk.Label(frame, text="Min SNR (dB):").grid(row=2, column=0,
|
|
sticky='w', pady=5)
|
|
self.min_snr = ttk.Scale(frame, from_=0, to=20,
|
|
orient='horizontal', length=200)
|
|
self.min_snr.set(10)
|
|
self.min_snr.grid(row=2, column=1, padx=10, pady=5)
|
|
ttk.Label(frame, text="10.0").grid(row=2, column=2, sticky='w')
|
|
|
|
# Detection mode
|
|
ttk.Label(frame, text="Detection Mode:").grid(row=3, column=0,
|
|
sticky='w', pady=5)
|
|
mode_combo = ttk.Combobox(frame,
|
|
values=['CFAR', 'Threshold', 'Peak'],
|
|
state='readonly', width=15)
|
|
mode_combo.set('CFAR')
|
|
mode_combo.grid(row=3, column=1, padx=10, pady=5, sticky='w')
|
|
|
|
def create_recording_settings(self, parent):
|
|
"""Create recording settings controls"""
|
|
frame = ttk.Frame(parent)
|
|
frame.pack(fill='both', expand=True, padx=20, pady=20)
|
|
|
|
# Recording directory
|
|
ttk.Label(frame, text="Save Directory:").grid(row=0, column=0,
|
|
sticky='w', pady=5)
|
|
self.record_dir = tk.StringVar(value="./recordings")
|
|
ttk.Entry(frame, textvariable=self.record_dir, width=40).grid(
|
|
row=0, column=1, padx=10, pady=5, columnspan=2)
|
|
|
|
# Browse button
|
|
ttk.Button(frame, text="Browse",
|
|
command=self.browse_directory).grid(row=0, column=3, padx=5)
|
|
|
|
# File format
|
|
ttk.Label(frame, text="File Format:").grid(row=1, column=0,
|
|
sticky='w', pady=5)
|
|
format_combo = ttk.Combobox(frame,
|
|
values=['HDF5', 'NPZ', 'CSV', 'MAT'],
|
|
state='readonly', width=15)
|
|
format_combo.set('HDF5')
|
|
format_combo.grid(row=1, column=1, padx=10, pady=5, sticky='w')
|
|
|
|
# Auto naming
|
|
ttk.Checkbutton(frame, text="Auto-generate filenames",
|
|
variable=tk.BooleanVar(value=True)).grid(
|
|
row=2, column=0, columnspan=2, sticky='w', pady=5)
|
|
|
|
# Max file size
|
|
ttk.Label(frame, text="Max File Size (MB):").grid(row=3, column=0,
|
|
sticky='w', pady=5)
|
|
ttk.Entry(frame, width=15).grid(row=3, column=1, padx=10, pady=5, sticky='w')
|
|
|
|
def create_system_settings(self, parent):
|
|
"""Create system settings controls"""
|
|
frame = ttk.Frame(parent)
|
|
frame.pack(fill='both', expand=True, padx=20, pady=20)
|
|
|
|
# System info
|
|
info_frame = ttk.LabelFrame(frame, text="System Information", padding=10)
|
|
info_frame.pack(fill='x', pady=10)
|
|
|
|
ttk.Label(info_frame, text="FPGA Temperature:").grid(row=0, column=0, sticky='w', pady=2)
|
|
ttk.Label(info_frame, text="25.5 °C").grid(row=0, column=1, padx=20, sticky='w')
|
|
|
|
ttk.Label(info_frame, text="FPGA Utilization:").grid(row=1, column=0, sticky='w', pady=2)
|
|
ttk.Label(info_frame, text="45%").grid(row=1, column=1, padx=20, sticky='w')
|
|
|
|
ttk.Label(info_frame, text="USB Data Rate:").grid(row=2, column=0, sticky='w', pady=2)
|
|
ttk.Label(info_frame, text="0 KB/s").grid(row=2, column=1, padx=20, sticky='w')
|
|
|
|
ttk.Label(info_frame, text="Uptime:").grid(row=3, column=0, sticky='w', pady=2)
|
|
ttk.Label(info_frame, text="00:00:00").grid(row=3, column=1, padx=20, sticky='w')
|
|
|
|
# Control buttons
|
|
button_frame = ttk.Frame(frame)
|
|
button_frame.pack(fill='x', pady=10)
|
|
|
|
ttk.Button(button_frame, text="System Diagnostics",
|
|
command=self.show_diagnostics).pack(side='left', padx=5)
|
|
ttk.Button(button_frame, text="Reset System",
|
|
command=self.reset_system).pack(side='left', padx=5)
|
|
ttk.Button(button_frame, text="Factory Reset",
|
|
command=self.factory_reset).pack(side='left', padx=5)
|
|
|
|
def create_status_bar(self):
|
|
"""Create status bar at bottom of window"""
|
|
status_frame = ttk.Frame(self.root)
|
|
status_frame.pack(side='bottom', fill='x')
|
|
|
|
# Left side status
|
|
self.status_label = ttk.Label(status_frame, text="Status: Demo Mode",
|
|
relief='sunken', padding=2)
|
|
self.status_label.pack(side='left', fill='x', expand=True)
|
|
|
|
# Right side indicators
|
|
self.fps_label = ttk.Label(status_frame, text="FPS: 0",
|
|
relief='sunken', width=10)
|
|
self.fps_label.pack(side='right', padx=1)
|
|
|
|
self.data_label = ttk.Label(status_frame, text="Data: 0 KB/s",
|
|
relief='sunken', width=12)
|
|
self.data_label.pack(side='right', padx=1)
|
|
|
|
self.targets_label = ttk.Label(status_frame, text="Targets: 0",
|
|
relief='sunken', width=10)
|
|
self.targets_label.pack(side='right', padx=1)
|
|
|
|
self.time_label = ttk.Label(status_frame, text=time.strftime("%H:%M:%S"),
|
|
relief='sunken', width=8)
|
|
self.time_label.pack(side='right', padx=1)
|
|
|
|
# ============================================================================
|
|
# GUI UPDATE METHODS
|
|
# ============================================================================
|
|
|
|
def animate(self):
|
|
"""Animation loop for updating plots"""
|
|
if not self.animation_running:
|
|
return
|
|
|
|
try:
|
|
# Update FPS counter
|
|
current_time = time.time()
|
|
dt = current_time - self.last_frame_time
|
|
if dt > 0:
|
|
self.fps = 0.9 * self.fps + 0.1 / dt
|
|
self.last_frame_time = current_time
|
|
|
|
# Update displays if running
|
|
if self.running:
|
|
self.update_radar_display()
|
|
self.update_target_list()
|
|
self.update_scopes()
|
|
self.frame_count += 1
|
|
|
|
# Update status bar
|
|
self.update_status_bar()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Animation error: {e}")
|
|
|
|
# Schedule next update
|
|
update_ms = int(1000 / max(1, self.update_rate.get()))
|
|
self.root.after(update_ms, self.animate)
|
|
|
|
def update_radar_display(self):
|
|
"""Update radar display with new data"""
|
|
try:
|
|
# Generate or get new radar data
|
|
if self.demo_mode.get():
|
|
# Generate simulated data
|
|
rd_map = self.simulator.generate_range_doppler_map()
|
|
targets = self.simulator.get_detected_targets()
|
|
else:
|
|
# Get data from queue
|
|
try:
|
|
data = self.radar_data_queue.get_nowait()
|
|
rd_map = data['map']
|
|
targets = data['targets']
|
|
except queue.Empty:
|
|
return
|
|
|
|
# Update range-Doppler map
|
|
self.range_doppler_map = rd_map
|
|
log_map = 10 * np.log10(rd_map + 1)
|
|
|
|
# Update image
|
|
self.rd_img.set_data(log_map)
|
|
self.rd_img.set_cmap(self.color_map.get())
|
|
|
|
# Update colorbar limits
|
|
vmin = np.percentile(log_map, 5)
|
|
vmax = np.percentile(log_map, 95)
|
|
self.rd_img.set_clim(vmin, vmax)
|
|
|
|
# Draw targets if enabled
|
|
if self.show_targets.get():
|
|
# Clear previous target markers
|
|
for artist in self.rd_ax.lines + self.rd_ax.texts:
|
|
if hasattr(artist, 'is_target_marker') and artist.is_target_marker:
|
|
artist.remove()
|
|
|
|
# Add new target markers
|
|
for target in targets:
|
|
x = target.velocity
|
|
y = target.range
|
|
self.rd_ax.plot(x, y, 'wo', markersize=8,
|
|
markeredgecolor='red', markeredgewidth=2,
|
|
is_target_marker=True)
|
|
self.rd_ax.text(x, y-100, str(target.id), color='white',
|
|
ha='center', va='top', fontsize=8,
|
|
is_target_marker=True)
|
|
|
|
# Update detected targets list
|
|
self.detected_targets = targets
|
|
|
|
# Add to history
|
|
self.target_history.append({
|
|
'time': current_time,
|
|
'targets': targets
|
|
})
|
|
|
|
# Limit history size
|
|
if len(self.target_history) > 1000:
|
|
self.target_history = self.target_history[-1000:]
|
|
|
|
# Update canvas
|
|
self.rd_canvas.draw_idle()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating radar display: {e}")
|
|
|
|
def update_target_list(self):
|
|
"""Update the targets treeview"""
|
|
# Clear existing items
|
|
for item in self.target_tree.get_children():
|
|
self.target_tree.delete(item)
|
|
|
|
# Add new targets
|
|
for target in self.detected_targets:
|
|
values = (
|
|
target.id,
|
|
f"{target.range:.1f}",
|
|
f"{target.velocity:.1f}",
|
|
f"{target.azimuth:.1f}",
|
|
f"{target.elevation:.1f}",
|
|
f"{target.snr:.1f}"
|
|
)
|
|
self.target_tree.insert('', 'end', values=values)
|
|
|
|
def update_scopes(self):
|
|
"""Update A-scope and spectrum displays"""
|
|
if not self.detected_targets:
|
|
return
|
|
|
|
try:
|
|
# A-scope (range profile)
|
|
range_profile = np.mean(self.range_doppler_map, axis=1)
|
|
range_axis = np.linspace(0, self.settings.max_range, len(range_profile))
|
|
|
|
self.scope_line.set_data(range_axis, 10 * np.log10(range_profile + 1))
|
|
self.scope_ax.relim()
|
|
self.scope_ax.autoscale_view(scalex=False)
|
|
self.scope_canvas.draw_idle()
|
|
|
|
# Doppler spectrum at selected range
|
|
range_bin = int(self.spec_range.get())
|
|
if range_bin < self.range_doppler_map.shape[0]:
|
|
spectrum = self.range_doppler_map[range_bin, :]
|
|
vel_axis = np.linspace(-self.settings.max_velocity,
|
|
self.settings.max_velocity,
|
|
len(spectrum))
|
|
|
|
self.spec_line.set_data(vel_axis, 10 * np.log10(spectrum + 1))
|
|
self.spec_ax.relim()
|
|
self.spec_ax.autoscale_view(scalex=False)
|
|
self.spec_canvas.draw_idle()
|
|
|
|
# PPI scope
|
|
if self.detected_targets:
|
|
theta = [np.radians(90 - t.azimuth) for t in self.detected_targets]
|
|
r = [t.range for t in self.detected_targets]
|
|
colors = [t.snr for t in self.detected_targets]
|
|
|
|
self.ppi_scatter.set_offsets(np.c_[theta, r])
|
|
self.ppi_scatter.set_array(np.array(colors))
|
|
self.ppi_scatter.set_clim(0, 40)
|
|
self.ppi_canvas.draw_idle()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating scopes: {e}")
|
|
|
|
def update_status_bar(self):
|
|
"""Update status bar information"""
|
|
# Time
|
|
self.time_label.config(text=time.strftime("%H:%M:%S"))
|
|
|
|
# Targets count
|
|
self.targets_label.config(text=f"Targets: {len(self.detected_targets)}")
|
|
|
|
# Data rate
|
|
if self.usb_interface.is_open:
|
|
stats = self.usb_interface.get_stats()
|
|
data_rate = stats['data_rate']
|
|
self.data_label.config(text=f"Data: {data_rate:.1f} KB/s")
|
|
else:
|
|
self.data_label.config(text="Data: 0 KB/s")
|
|
|
|
# FPS
|
|
self.fps_label.config(text=f"FPS: {self.fps:.1f}")
|
|
|
|
# Status text
|
|
if self.running:
|
|
status = f"Status: {'Recording' if self.recording else 'Running'} - Frame {self.frame_count}"
|
|
else:
|
|
status = "Status: Demo Mode - Stopped"
|
|
|
|
if self.recording:
|
|
status += f" - Recording to {self.record_dir.get()}"
|
|
|
|
self.status_label.config(text=status)
|
|
|
|
# ============================================================================
|
|
# BACKGROUND THREADS
|
|
# ============================================================================
|
|
|
|
def start_background_threads(self):
|
|
"""Start background processing threads"""
|
|
self.running = False
|
|
self.background_thread = threading.Thread(target=self.background_worker,
|
|
daemon=True)
|
|
self.background_thread.start()
|
|
|
|
def background_worker(self):
|
|
"""Background worker thread for data processing"""
|
|
while True:
|
|
if self.running and not self.demo_mode.get():
|
|
# Hardware mode - read from USB
|
|
try:
|
|
data = self.usb_interface.read_data(4096)
|
|
if data:
|
|
# Parse and process data
|
|
processed = self.process_usb_data(data)
|
|
if processed:
|
|
self.radar_data_queue.put(processed, timeout=1)
|
|
except Exception as e:
|
|
logger.error(f"Background worker error: {e}")
|
|
else:
|
|
# Demo mode - generate data at lower rate
|
|
time.sleep(0.05) # 20 Hz
|
|
|
|
def process_usb_data(self, data):
|
|
"""Process raw USB data into radar frames"""
|
|
# This would parse actual USB packets
|
|
# For demo, return None
|
|
return None
|
|
|
|
# ============================================================================
|
|
# COMMAND HANDLERS
|
|
# ============================================================================
|
|
|
|
def refresh_devices(self):
|
|
"""Refresh device list"""
|
|
devices = self.usb_interface.list_devices()
|
|
device_names = [d['description'] for d in devices]
|
|
self.device_combo['values'] = device_names
|
|
if device_names:
|
|
self.device_combo.current(0)
|
|
logger.info(f"Found {len(devices)} devices")
|
|
|
|
def connect_device(self):
|
|
"""Connect to selected device"""
|
|
if self.demo_mode.get():
|
|
device_name = self.device_combo.get()
|
|
if self.usb_interface.open_device({'description': device_name}):
|
|
messagebox.showinfo("Success", f"Connected to {device_name} in demo mode")
|
|
self.status_label.config(text=f"Status: Connected - {device_name}")
|
|
else:
|
|
messagebox.showerror("Error", "Failed to connect to device")
|
|
else:
|
|
messagebox.showinfo("Info", "Hardware mode not available in demo version")
|
|
|
|
def connect_hardware(self):
|
|
"""Switch to hardware mode"""
|
|
self.demo_mode.set(False)
|
|
messagebox.showinfo("Info", "Hardware mode requires actual FT601 device")
|
|
|
|
def enable_demo_mode(self):
|
|
"""Enable demo mode"""
|
|
self.demo_mode.set(True)
|
|
self.status_label.config(text="Status: Demo Mode")
|
|
|
|
def start_radar(self):
|
|
"""Start radar operation"""
|
|
self.running = True
|
|
self.start_button.config(state='disabled')
|
|
self.stop_button.config(state='normal')
|
|
self.status_label.config(text="Status: Running")
|
|
logger.info("Radar started")
|
|
|
|
def stop_radar(self):
|
|
"""Stop radar operation"""
|
|
self.running = False
|
|
self.start_button.config(state='normal')
|
|
self.stop_button.config(state='disabled')
|
|
self.status_label.config(text="Status: Stopped")
|
|
logger.info("Radar stopped")
|
|
|
|
def start_recording(self):
|
|
"""Start recording data"""
|
|
if not self.running:
|
|
messagebox.showwarning("Warning", "Start radar first")
|
|
return
|
|
|
|
self.recording = True
|
|
logger.info(f"Recording started to {self.record_dir.get()}")
|
|
|
|
def stop_recording(self):
|
|
"""Stop recording data"""
|
|
self.recording = False
|
|
logger.info("Recording stopped")
|
|
|
|
def apply_settings(self):
|
|
"""Apply radar settings"""
|
|
try:
|
|
self.settings.system_frequency = self.settings_vars['freq'].get() * 1e9
|
|
self.settings.chirp_duration_long = self.settings_vars['long_dur'].get() * 1e-6
|
|
self.settings.chirp_duration_short = self.settings_vars['short_dur'].get() * 1e-6
|
|
self.settings.chirps_per_frame = int(self.settings_vars['chirps'].get())
|
|
self.settings.range_bins = int(self.settings_vars['range_bins'].get())
|
|
self.settings.doppler_bins = int(self.settings_vars['doppler_bins'].get())
|
|
self.settings.prf = self.settings_vars['prf'].get()
|
|
self.settings.max_range = self.settings_vars['max_range'].get()
|
|
self.settings.max_velocity = self.settings_vars['max_vel'].get()
|
|
|
|
messagebox.showinfo("Success", "Settings applied")
|
|
logger.info("Settings updated")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Invalid settings: {e}")
|
|
|
|
def load_config(self):
|
|
"""Load configuration from file"""
|
|
from tkinter import filedialog
|
|
filename = filedialog.askopenfilename(
|
|
title="Load Configuration",
|
|
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
|
|
)
|
|
if filename:
|
|
try:
|
|
with open(filename, 'r') as f:
|
|
config = json.load(f)
|
|
# Apply config...
|
|
messagebox.showinfo("Success", f"Loaded configuration from {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to load: {e}")
|
|
|
|
def save_config(self):
|
|
"""Save configuration to file"""
|
|
from tkinter import filedialog
|
|
filename = filedialog.asksaveasfilename(
|
|
title="Save Configuration",
|
|
defaultextension=".json",
|
|
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
|
|
)
|
|
if filename:
|
|
try:
|
|
config = {
|
|
'settings': {k: v.get() for k, v in self.settings_vars.items()},
|
|
'display': {
|
|
'color_map': self.color_map.get(),
|
|
'show_grid': self.show_grid.get(),
|
|
'show_targets': self.show_targets.get(),
|
|
'auto_scan': self.auto_scan.get()
|
|
}
|
|
}
|
|
with open(filename, 'w') as f:
|
|
json.dump(config, f, indent=2)
|
|
messagebox.showinfo("Success", f"Saved configuration to {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to save: {e}")
|
|
|
|
def export_data(self):
|
|
"""Export radar data"""
|
|
from tkinter import filedialog
|
|
filename = filedialog.asksaveasfilename(
|
|
title="Export Data",
|
|
defaultextension=".npz",
|
|
filetypes=[("NumPy files", "*.npz"), ("All files", "*.*")]
|
|
)
|
|
if filename:
|
|
try:
|
|
np.savez(filename,
|
|
map=self.range_doppler_map,
|
|
targets=[(t.range, t.velocity, t.azimuth, t.snr)
|
|
for t in self.detected_targets])
|
|
messagebox.showinfo("Success", f"Exported data to {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to export: {e}")
|
|
|
|
def clear_history(self):
|
|
"""Clear target history"""
|
|
self.target_history = []
|
|
self.hist_ax1.clear()
|
|
self.hist_ax2.clear()
|
|
self.hist_canvas.draw()
|
|
|
|
def browse_directory(self):
|
|
"""Browse for directory"""
|
|
from tkinter import filedialog
|
|
directory = filedialog.askdirectory(title="Select Recording Directory")
|
|
if directory:
|
|
self.record_dir.set(directory)
|
|
|
|
def calibration_wizard(self):
|
|
"""Open calibration wizard"""
|
|
messagebox.showinfo("Calibration", "Calibration wizard not available in demo mode")
|
|
|
|
def beam_analysis(self):
|
|
"""Open beam pattern analysis"""
|
|
messagebox.showinfo("Beam Analysis", "Beam analysis not available in demo mode")
|
|
|
|
def noise_measurement(self):
|
|
"""Measure noise floor"""
|
|
messagebox.showinfo("Noise Measurement", "Noise measurement not available in demo mode")
|
|
|
|
def show_diagnostics(self):
|
|
"""Show system diagnostics"""
|
|
import platform
|
|
info = f"""
|
|
System Diagnostics
|
|
=================
|
|
|
|
Platform: {platform.platform()}
|
|
Python: {platform.python_version()}
|
|
|
|
Radar System
|
|
------------
|
|
Mode: {'Demo' if self.demo_mode.get() else 'Hardware'}
|
|
Status: {'Running' if self.running else 'Stopped'}
|
|
Frames: {self.frame_count}
|
|
Targets: {len(self.detected_targets)}
|
|
|
|
USB Interface
|
|
-------------
|
|
Connected: {self.usb_interface.is_open}
|
|
Packets: {self.usb_interface.packet_count}
|
|
Bytes: {self.usb_interface.byte_count}
|
|
Data Rate: {self.usb_interface.data_rate:.1f} KB/s
|
|
|
|
Display
|
|
-------
|
|
FPS: {self.fps:.1f}
|
|
Update Rate: {self.update_rate.get()} Hz
|
|
Color Map: {self.color_map.get()}
|
|
"""
|
|
|
|
messagebox.showinfo("Diagnostics", info)
|
|
|
|
def show_docs(self):
|
|
"""Show documentation"""
|
|
messagebox.showinfo("Documentation",
|
|
"Radar System GUI Documentation\n\n"
|
|
"Version 1.0\n\n"
|
|
"For more information, visit:\n"
|
|
"https://github.com/radar-system/docs")
|
|
|
|
def show_about(self):
|
|
"""Show about dialog"""
|
|
about_text = """
|
|
Advanced Radar System GUI
|
|
Version 1.0.0
|
|
|
|
A comprehensive radar control and visualization
|
|
interface with FT601 USB 3.0 support.
|
|
|
|
Features:
|
|
• Real-time Range-Doppler display
|
|
• Target detection and tracking
|
|
• A-scope and PPI displays
|
|
• Doppler spectrum analysis
|
|
• Data recording and playback
|
|
|
|
© 2025 Radar Systems Inc.
|
|
"""
|
|
messagebox.showinfo("About", about_text)
|
|
|
|
def reset_system(self):
|
|
"""Reset system"""
|
|
if messagebox.askyesno("Confirm Reset", "Reset all system parameters?"):
|
|
self.frame_count = 0
|
|
self.detected_targets = []
|
|
self.target_history = []
|
|
self.status.uptime = 0
|
|
logger.info("System reset")
|
|
|
|
def factory_reset(self):
|
|
"""Factory reset"""
|
|
if messagebox.askyesno("Confirm Factory Reset",
|
|
"This will reset ALL settings to defaults. Continue?"):
|
|
# Reset to defaults
|
|
for key, var in self.settings_vars.items():
|
|
var.set(self.get_default_value(key))
|
|
self.color_map.set('hot')
|
|
self.show_grid.set(True)
|
|
self.show_targets.set(True)
|
|
self.auto_scan.set(True)
|
|
logger.info("Factory reset performed")
|
|
|
|
def get_default_value(self, key):
|
|
"""Get default value for settings key"""
|
|
defaults = {
|
|
'freq': 10.0,
|
|
'long_dur': 30.0,
|
|
'short_dur': 0.5,
|
|
'chirps': 32,
|
|
'range_bins': 1024,
|
|
'doppler_bins': 32,
|
|
'prf': 1000,
|
|
'max_range': 5000,
|
|
'max_vel': 100,
|
|
'beam_width': 3.0
|
|
}
|
|
return defaults.get(key, 0)
|
|
|
|
def on_closing(self):
|
|
"""Handle window closing"""
|
|
if messagebox.askokcancel("Quit", "Do you want to quit?"):
|
|
self.animation_running = False
|
|
self.running = False
|
|
self.usb_interface.close()
|
|
self.root.destroy()
|
|
|
|
# ============================================================================
|
|
# MAIN ENTRY POINT
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""Main application entry point"""
|
|
try:
|
|
# Create root window
|
|
root = tk.Tk()
|
|
|
|
# Set application icon if available
|
|
try:
|
|
root.iconbitmap(default='radar.ico')
|
|
except:
|
|
pass
|
|
|
|
# Create application
|
|
app = RadarGUI(root)
|
|
|
|
# Center window on screen
|
|
root.update_idletasks()
|
|
width = root.winfo_width()
|
|
height = root.winfo_height()
|
|
x = (root.winfo_screenwidth() // 2) - (width // 2)
|
|
y = (root.winfo_screenheight() // 2) - (height // 2)
|
|
root.geometry(f'{width}x{height}+{x}+{y}')
|
|
|
|
# Start main loop
|
|
root.mainloop()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fatal error: {e}")
|
|
messagebox.showerror("Fatal Error", f"Application failed to start:\n{e}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|