fix: full-repo ruff lint cleanup and CI migration to uv

Resolve all 374 ruff errors across 36 Python files (E501, E702, E722,
E741, F821, F841, invalid-syntax) bringing `ruff check .` to zero
errors repo-wide with line-length=100.

Rewrite CI workflow to use uv for dependency management, whole-repo
`ruff check .`, py_compile syntax gate, and merged python-tests job.
Add pyproject.toml with ruff config and uv dependency groups.

CI structure proposed by hcm444.
This commit is contained in:
Jason
2026-04-09 02:05:34 +03:00
parent 57de32b172
commit 11aa590cf2
31 changed files with 3633 additions and 2789 deletions
+37 -60
View File
@@ -8,100 +8,77 @@ on:
jobs:
# ===========================================================================
# Job 0: Ruff Lint (all maintained Python files)
# Covers: active GUI files, v6+ GUIs, v7/ module, FPGA cosim scripts
# Excludes: legacy GUI_V1-V5, schematics, simulation, 8_Utils
# ===========================================================================
lint:
name: Ruff Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install ruff
- name: Run ruff on maintained files
run: |
ruff check \
9_Firmware/9_3_GUI/radar_protocol.py \
9_Firmware/9_3_GUI/radar_dashboard.py \
9_Firmware/9_3_GUI/smoke_test.py \
9_Firmware/9_3_GUI/test_radar_dashboard.py \
9_Firmware/9_3_GUI/GUI_V6.py \
9_Firmware/9_3_GUI/GUI_V6_Demo.py \
9_Firmware/9_3_GUI/GUI_PyQt_Map.py \
9_Firmware/9_3_GUI/GUI_V7_PyQt.py \
9_Firmware/9_3_GUI/v7/ \
9_Firmware/9_2_FPGA/tb/cosim/ \
9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py
# ===========================================================================
# Job 1: Python Host Software Tests (58 tests)
# radar_protocol, radar_dashboard, FT2232H connection, replay, opcodes, e2e
# Python: lint (ruff), syntax check (py_compile), unit tests (pytest)
# CI structure proposed by hcm444 — uses uv for dependency management
# ===========================================================================
python-tests:
name: Python Dashboard Tests (58)
name: Python Lint + Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest numpy h5py
- uses: astral-sh/setup-uv@v5
- name: Run test suite
run: python -m pytest 9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short
- name: Install dependencies
run: uv sync --group dev
- name: Ruff lint (whole repo)
run: uv run ruff check .
- name: Syntax check (py_compile)
run: |
uv run python - <<'PY'
import py_compile
from pathlib import Path
skip = {".git", "__pycache__", ".venv", "venv", "docs"}
for p in Path(".").rglob("*.py"):
if skip & set(p.parts):
continue
py_compile.compile(str(p), doraise=True)
PY
- name: Unit tests
run: >
uv run pytest
9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short
# ===========================================================================
# Job 2: MCU Firmware Unit Tests (20 tests)
# MCU Firmware Unit Tests (20 tests)
# Bug regression (15) + Gap-3 safety tests (5)
# ===========================================================================
mcu-tests:
name: MCU Firmware Tests (20)
name: MCU Firmware Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Install build tools
run: sudo apt-get update && sudo apt-get install -y build-essential
- name: Build and run MCU tests
working-directory: 9_Firmware/9_1_Microcontroller/tests
run: make test
working-directory: 9_Firmware/9_1_Microcontroller/tests
# ===========================================================================
# Job 3: FPGA RTL Regression (23 testbenches + lint)
# Phase 0: Vivado-style lint, Phase 1-4: unit + integration + e2e
# FPGA RTL Regression (23 testbenches + lint)
# ===========================================================================
fpga-regression:
name: FPGA Regression (23 TBs + lint)
name: FPGA Regression
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Install Icarus Verilog
run: sudo apt-get update && sudo apt-get install -y iverilog
- name: Run full FPGA regression
working-directory: 9_Firmware/9_2_FPGA
run: bash run_regression.sh
working-directory: 9_Firmware/9_2_FPGA
+22 -8
View File
@@ -106,7 +106,8 @@ mesh.SmoothMeshLines('all', mesh_res, ratio=1.4)
# Materials
# -------------------------
pec = CSX.AddMetal('PEC')
quartz = CSX.AddMaterial('QUARTZ'); quartz.SetMaterialProperty(epsilon=er_quartz)
quartz = CSX.AddMaterial('QUARTZ')
quartz.SetMaterialProperty(epsilon=er_quartz)
air = CSX.AddMaterial('AIR') # explicit for slot holes
# -------------------------
@@ -191,13 +192,19 @@ Zin = ports[0].uf_tot / ports[0].if_tot
plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|')
plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Magnitude (dB)')
plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Magnitude (dB)')
plt.title('S-Parameters: Slotted Quartz-Filled WG')
plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}')
plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Ohms')
plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Ohms')
plt.title('Input Impedance (Port 1)')
# -------------------------
@@ -237,19 +244,26 @@ ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92)
ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)')
ax.set_box_aspect((1,1,1))
ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
# Quick 2D geometry preview (top view at y=b)
plt.figure(figsize=(8.4,2.8))
plt.fill_between([0,a], [0,0], [L,L], color='#dddddd', alpha=0.5, step='pre', label='WG aperture (top)')
plt.fill_between(
[0, a], [0, 0], [L, L], color='#dddddd', alpha=0.5, step='pre', label='WG aperture (top)'
)
for zc, xc in zip(z_centers, x_centers):
plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0),
slot_w, slot_L, fc='#3355ff', ec='k'))
plt.xlim(-2, a+2); plt.ylim(-5, L+5)
plt.xlim(-2, a + 2)
plt.ylim(-5, L + 5)
plt.gca().invert_yaxis()
plt.xlabel('x (mm)'); plt.ylabel('z (mm)')
plt.xlabel('x (mm)')
plt.ylabel('z (mm)')
plt.title('Top-view slot layout (y=b plane)')
plt.grid(True); plt.legend()
plt.grid(True)
plt.legend()
plt.show()
@@ -137,7 +137,9 @@ Ncells = Nx*Ny*Nz
print(f"[mesh] cells: {Nx} × {Ny} × {Nz} = {Ncells:,}")
mem_fields_bytes = Ncells * 6 * 8 # rough ~ (Ex,Ey,Ez,Hx,Hy,Hz) doubles
print(f"[mesh] rough field memory: ~{mem_fields_bytes/1e9:.2f} GB (solver overhead extra)")
dx_min = min(np.diff(x_lines)); dy_min = min(np.diff(y_lines)); dz_min = min(np.diff(z_lines))
dx_min = min(np.diff(x_lines))
dy_min = min(np.diff(y_lines))
dz_min = min(np.diff(z_lines))
print(f"[mesh] min steps (mm): dx={dx_min:.3f}, dy={dy_min:.3f}, dz={dz_min:.3f}")
# Optional smoothing to limit max cell size
@@ -147,7 +149,8 @@ mesh.SmoothMeshLines('all', mesh_res, ratio=1.4)
# MATERIALS & SOLIDS
# =================
pec = CSX.AddMetal('PEC')
quartzM = CSX.AddMaterial('QUARTZ'); quartzM.SetMaterialProperty(epsilon=er_quartz)
quartzM = CSX.AddMaterial('QUARTZ')
quartzM.SetMaterialProperty(epsilon=er_quartz)
airM = CSX.AddMaterial('AIR')
# Quartz full block
@@ -157,7 +160,9 @@ quartzM.AddBox([0, 0, 0], [a, b, guide_length_mm])
pec.AddBox([-t_metal, 0, 0], [0, b, guide_length_mm]) # left
pec.AddBox([a, 0, 0], [a+t_metal,b, guide_length_mm]) # right
pec.AddBox([-t_metal,-t_metal,0],[a+t_metal,0, guide_length_mm]) # bottom
pec.AddBox([-t_metal, b, 0], [a+t_metal,b+t_metal,guide_length_mm]) # top (slots will pierce)
pec.AddBox(
[-t_metal, b, 0], [a + t_metal, b + t_metal, guide_length_mm]
) # top (slots will pierce)
# Slots (AIR) overriding top metal
for zc, xc in zip(z_centers, x_centers):
@@ -215,16 +220,16 @@ print(f"[timing] FDTD solve elapsed: {t1 - t0:.2f} s")
# ... right before NF2FF (far-field):
t2 = time.time()
try:
res = nf2ff.CalcNF2FF(Sim_Path, [f0], theta, phi)
res = nf2ff.CalcNF2FF(Sim_Path, [f0], theta, phi) # noqa: F821
except AttributeError:
res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi)
res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi) # noqa: F821
t3 = time.time()
print(f"[timing] NF2FF (far-field) elapsed: {t3 - t2:.2f} s")
# ... S-parameters postproc timing (optional):
t4 = time.time()
for p in ports:
p.CalcPort(Sim_Path, freq)
for p in ports: # noqa: F821
p.CalcPort(Sim_Path, freq) # noqa: F821
t5 = time.time()
print(f"[timing] Port/S-params postproc elapsed: {t5 - t4:.2f} s")
@@ -250,13 +255,19 @@ Zin = ports[0].uf_tot / ports[0].if_tot
plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|')
plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Magnitude (dB)')
plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Magnitude (dB)')
plt.title(f'S-Parameters (profile: {PROFILE})')
plt.figure(figsize=(7.6,4.6))
plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}')
plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}')
plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Ohms')
plt.grid(True)
plt.legend()
plt.xlabel('Frequency (GHz)')
plt.ylabel('Ohms')
plt.title('Input Impedance (Port 1)')
# ==========================
@@ -295,22 +306,35 @@ ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92)
ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)')
ax.set_box_aspect((1,1,1))
ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.tight_layout()
# ==========================
# QUICK 2D GEOMETRY PREVIEW
# ==========================
plt.figure(figsize=(8.4,2.8))
plt.fill_between([0,a], [0,0], [guide_length_mm, guide_length_mm], color='#dddddd', alpha=0.5, step='pre', label='WG top aperture')
plt.fill_between(
[0, a],
[0, 0],
[guide_length_mm, guide_length_mm],
color='#dddddd',
alpha=0.5,
step='pre',
label='WG top aperture',
)
for zc, xc in zip(z_centers, x_centers):
plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0),
slot_w, slot_L, fc='#3355ff', ec='k'))
plt.xlim(-2, a+2); plt.ylim(-5, guide_length_mm+5)
plt.xlim(-2, a + 2)
plt.ylim(-5, guide_length_mm + 5)
plt.gca().invert_yaxis()
plt.xlabel('x (mm)'); plt.ylabel('z (mm)')
plt.xlabel('x (mm)')
plt.ylabel('z (mm)')
plt.title(f'Top-view slot layout (N={Nslots}, profile={PROFILE})')
plt.grid(True); plt.legend()
plt.grid(True)
plt.legend()
@@ -69,7 +69,10 @@ def generate_multi_ramp_csv(Fs=125e6, Tb=1e-6, Tau=2e-6, fmax=30e6, fmin=10e6,
df = pd.DataFrame({"time(s)": t_csv, "voltage(V)": y_csv})
df.to_csv(filename, index=False, header=False)
print(f"CSV saved: {filename}")
print(f"Total raw samples: {total_samples} | Ramps inserted: {ramps_inserted} | CSV points: {len(y_csv)}")
print(
f"Total raw samples: {total_samples} | Ramps inserted: {ramps_inserted} "
f"| CSV points: {len(y_csv)}"
)
# --- Plot (staircase)
if show_plot or save_plot_png:
+12 -2
View File
@@ -27,10 +27,20 @@ ax.axhline(polygon_y2, color="blue", linestyle="--")
via_positions = [2, 4, 6, 8] # x positions for visualization
for x in via_positions:
# Case A
ax.add_patch(plt.Circle((x, polygon_y1), via_pad_A/2, facecolor="green", alpha=0.5, label="Via pad A" if x==2 else ""))
ax.add_patch(
plt.Circle(
(x, polygon_y1), via_pad_A / 2, facecolor="green", alpha=0.5,
label="Via pad A" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5))
# Case B
ax.add_patch(plt.Circle((-x, polygon_y1), via_pad_B/2, facecolor="red", alpha=0.3, label="Via pad B" if x==2 else ""))
ax.add_patch(
plt.Circle(
(-x, polygon_y1), via_pad_B / 2, facecolor="red", alpha=0.3,
label="Via pad B" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3))
# Add dimensions text
+15 -3
View File
@@ -26,10 +26,20 @@ ax.axhline(polygon_y2, color="blue", linestyle="--")
via_positions = [2, 2 + via_pitch] # two vias for showing spacing
for x in via_positions:
# Case A
ax.add_patch(plt.Circle((x, polygon_y1), via_pad_A/2, facecolor="green", alpha=0.5, label="Via pad A" if x==2 else ""))
ax.add_patch(
plt.Circle(
(x, polygon_y1), via_pad_A / 2, facecolor="green", alpha=0.5,
label="Via pad A" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5))
# Case B
ax.add_patch(plt.Circle((-x, polygon_y1), via_pad_B/2, facecolor="red", alpha=0.3, label="Via pad B" if x==2 else ""))
ax.add_patch(
plt.Circle(
(-x, polygon_y1), via_pad_B / 2, facecolor="red", alpha=0.3,
label="Via pad B" if x == 2 else ""
)
)
ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3))
# Add text annotations
@@ -48,7 +58,9 @@ line_edge_y = rf_line_y + line_width/2
via_center_y = polygon_y1
ax.annotate("", xy=(2.4, line_edge_y), xytext=(2.4, via_center_y),
arrowprops=dict(arrowstyle="<->", color="brown"))
ax.text(2.5, (line_edge_y + via_center_y)/2, f"{via_center_offset:.2f} mm", color="brown", va="center")
ax.text(
2.5, (line_edge_y + via_center_y) / 2, f"{via_center_offset:.2f} mm", color="brown", va="center"
)
# Formatting
ax.set_xlim(-5, 5)
@@ -106,4 +106,7 @@ plt.tight_layout()
plt.savefig('Heatmap_Kaiser25dB_like.png', bbox_inches='tight')
plt.show()
print('Saved: E_plane_Kaiser25dB_like.png, H_plane_Kaiser25dB_like.png, Heatmap_Kaiser25dB_like.png')
print(
'Saved: E_plane_Kaiser25dB_like.png, H_plane_Kaiser25dB_like.png, '
'Heatmap_Kaiser25dB_like.png'
)
+19 -7
View File
@@ -16,10 +16,18 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
# Target parameters
targets = [
{'range': 3000, 'velocity': 25, 'snr': 30, 'azimuth': 10, 'elevation': 5}, # Fast moving target
{'range': 5000, 'velocity': -15, 'snr': 25, 'azimuth': 20, 'elevation': 2}, # Approaching target
{'range': 8000, 'velocity': 5, 'snr': 20, 'azimuth': 30, 'elevation': 8}, # Slow moving target
{'range': 12000, 'velocity': -8, 'snr': 18, 'azimuth': 45, 'elevation': 3}, # Distant target
{
'range': 3000, 'velocity': 25, 'snr': 30, 'azimuth': 10, 'elevation': 5
}, # Fast moving target
{
'range': 5000, 'velocity': -15, 'snr': 25, 'azimuth': 20, 'elevation': 2
}, # Approaching target
{
'range': 8000, 'velocity': 5, 'snr': 20, 'azimuth': 30, 'elevation': 8
}, # Slow moving target
{
'range': 12000, 'velocity': -8, 'snr': 18, 'azimuth': 45, 'elevation': 3
}, # Distant target
]
# Noise parameters
@@ -38,7 +46,7 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
q_val = np.random.normal(0, noise_std)
# Add clutter (stationary targets)
clutter_range = 2000 # Fixed clutter at 2km
_clutter_range = 2000 # Fixed clutter at 2km
if sample < 100: # Simulate clutter in first 100 samples
i_val += np.random.normal(0, clutter_std)
q_val += np.random.normal(0, clutter_std)
@@ -47,7 +55,9 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
for target in targets:
# Calculate range bin (simplified)
range_bin = int(target['range'] / 20) # ~20m per bin
doppler_phase = 2 * math.pi * target['velocity'] * chirp / 100 # Doppler phase shift
doppler_phase = (
2 * math.pi * target['velocity'] * chirp / 100
) # Doppler phase shift
# Target appears around its range bin with some spread
if abs(sample - range_bin) < 10:
@@ -96,7 +106,9 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
for target in targets:
# Range bin calculation (different for short chirps)
range_bin = int(target['range'] / 40) # Different range resolution
doppler_phase = 2 * math.pi * target['velocity'] * (chirp + 5) / 80 # Different Doppler
doppler_phase = (
2 * math.pi * target['velocity'] * (chirp + 5) / 80
) # Different Doppler
# Target appears around its range bin
if abs(sample - range_bin) < 8:
+7 -4
View File
@@ -1,5 +1,5 @@
import numpy as np
from numpy.fft import fft, ifft
from numpy.fft import fft
import matplotlib.pyplot as plt
@@ -15,7 +15,10 @@ theta_n= 2*np.pi*(pow(N,2)*pow(Ts,2)*(fmax-fmin)/(2*Tb)+fmin*N*Ts) # instantaneo
y = 1 + np.sin(theta_n) # ramp signal in time domain
M = np.arange(n, 2*n, 1)
theta_m= 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)-2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) # instantaneous phase
theta_m= (
2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)
- 2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb)
) # instantaneous phase
z = 1 + np.sin(theta_m) # ramp signal in time domain
x = np.concatenate((y, z))
@@ -23,9 +26,9 @@ x = np.concatenate((y, z))
t = Ts*np.arange(0,2*n,1)
X = fft(x)
L =len(X)
l = np.arange(L)
freq_indices = np.arange(L)
T = L*Ts
freq = l/T
freq = freq_indices/T
plt.figure(figsize = (12, 6))
@@ -15,7 +15,10 @@ theta_n= 2*np.pi*(pow(N,2)*pow(Ts,2)*(fmax-fmin)/(2*Tb)+fmin*N*Ts) # instantaneo
y = 1 + np.sin(theta_n) # ramp signal in time domain
M = np.arange(n, 2*n, 1)
theta_m= 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)-2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) # instantaneous phase
theta_m= (
2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)
- 2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb)
) # instantaneous phase
z = 1 + np.sin(theta_m) # ramp signal in time domain
x = np.concatenate((y, z))
@@ -24,9 +27,9 @@ t = Ts*np.arange(0,2*n,1)
plt.plot(t, x)
X = fft(x)
L =len(X)
l = np.arange(L)
freq_indices = np.arange(L)
T = L*Ts
freq = l/T
freq = freq_indices/T
print("The Array is: ", x) #printing the array
+10 -4
View File
@@ -221,7 +221,10 @@ class RadarCalculatorGUI:
temp = self.get_float_value(self.entries["Temperature (K):"])
# Validate inputs
if None in [f_ghz, pulse_duration_us, prf, p_dbm, g_dbi, sens_dbm, rcs, losses_db, nf_db, temp]:
if None in [
f_ghz, pulse_duration_us, prf, p_dbm, g_dbi,
sens_dbm, rcs, losses_db, nf_db, temp,
]:
messagebox.showerror("Error", "Please enter valid numeric values for all fields")
return
@@ -235,7 +238,7 @@ class RadarCalculatorGUI:
g_linear = 10 ** (g_dbi / 10)
sens_linear = 10 ** ((sens_dbm - 30) / 10)
losses_linear = 10 ** (losses_db / 10)
nf_linear = 10 ** (nf_db / 10)
_nf_linear = 10 ** (nf_db / 10)
# Calculate receiver noise power
if k is None:
@@ -298,11 +301,14 @@ class RadarCalculatorGUI:
messagebox.showinfo("Success", "Calculation completed successfully!")
except Exception as e:
messagebox.showerror("Calculation Error", f"An error occurred during calculation:\n{str(e)}")
messagebox.showerror(
"Calculation Error",
f"An error occurred during calculation:\n{str(e)}",
)
def main():
root = tk.Tk()
app = RadarCalculatorGUI(root)
_app = RadarCalculatorGUI(root)
root.mainloop()
if __name__ == "__main__":
+18 -4
View File
@@ -12,13 +12,22 @@ def calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array)
lamb = c /(frequency * 1e9)
# Calculate the effective dielectric constant
epsilon_eff = (epsilon_r + 1) / 2 + (epsilon_r - 1) / 2 * (1 + 12 * h_sub_m / (array[1] * h_cu_m)) ** (-0.5)
epsilon_eff = (
(epsilon_r + 1) / 2
+ (epsilon_r - 1) / 2 * (1 + 12 * h_sub_m / (array[1] * h_cu_m)) ** (-0.5)
)
# Calculate the width of the patch
W = c / (2 * frequency * 1e9) * np.sqrt(2 / (epsilon_r + 1))
# Calculate the effective length
delta_L = 0.412 * h_sub_m * (epsilon_eff + 0.3) * (W / h_sub_m + 0.264) / ((epsilon_eff - 0.258) * (W / h_sub_m + 0.8))
delta_L = (
0.412
* h_sub_m
* (epsilon_eff + 0.3)
* (W / h_sub_m + 0.264)
/ ((epsilon_eff - 0.258) * (W / h_sub_m + 0.8))
)
# Calculate the length of the patch
L = c / (2 * frequency * 1e9 * np.sqrt(epsilon_eff)) - 2 * delta_L
@@ -31,7 +40,10 @@ def calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array)
# Calculate the feeding line width (W_feed)
Z0 = 50 # Characteristic impedance of the feeding line (typically 50 ohms)
A = Z0 / 60 * np.sqrt((epsilon_r + 1) / 2) + (epsilon_r - 1) / (epsilon_r + 1) * (0.23 + 0.11 / epsilon_r)
A = (
Z0 / 60 * np.sqrt((epsilon_r + 1) / 2)
+ (epsilon_r - 1) / (epsilon_r + 1) * (0.23 + 0.11 / epsilon_r)
)
W_feed = 8 * h_sub_m / np.exp(A) - 2 * h_cu_m
# Convert results back to mm
@@ -50,7 +62,9 @@ h_sub = 0.102 # Height of substrate in mm
h_cu = 0.07 # Height of copper in mm
array = [2, 2] # 2x2 array
W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array)
W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters(
frequency, epsilon_r, h_sub, h_cu, array
)
print(f"Width of the patch: {W_mm:.4f} mm")
print(f"Length of the patch: {L_mm:.4f} mm")
+4 -1
View File
@@ -358,7 +358,10 @@ def compare_scenario(scenario_name):
# ---- First/last sample comparison ----
print("\nFirst 10 samples (after alignment):")
print(f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} {'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}")
print(
f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} "
f"{'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}"
)
for k in range(min(10, aligned_len)):
ei = aligned_rtl_i[k] - aligned_py_i[k]
eq = aligned_rtl_q[k] - aligned_py_q[k]
+17 -5
View File
@@ -199,14 +199,17 @@ class NCO:
# Wait - let me re-derive. The Verilog is:
# phase_accumulator <= phase_accumulator + frequency_tuning_word;
# phase_accum_reg <= phase_accumulator; // OLD value (NBA)
# phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0}; // OLD phase_accum_reg
# phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0};
# // OLD phase_accum_reg
# Since all are NBA (<=), they all read the values from BEFORE this edge.
# So: new_phase_accumulator = old_phase_accumulator + ftw
# new_phase_accum_reg = old_phase_accumulator
# new_phase_with_offset = old_phase_accum_reg + offset
old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct
self.phase_accum_reg = old_phase_accumulator
self.phase_with_offset = (old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)) & 0xFFFFFFFF
self.phase_with_offset = (
old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)
) & 0xFFFFFFFF
# phase_accumulator was already updated above
# ---- Stage 3a: Register LUT address + quadrant ----
@@ -607,8 +610,14 @@ class FIRFilter:
if (old_valid_pipe >> 0) & 1:
for i in range(16):
# Sign-extend products to ACCUM_WIDTH
a = sign_extend(mult_results[2*i] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH)
b = sign_extend(mult_results[2*i+1] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH)
a = sign_extend(
mult_results[2 * i] & ((1 << self.PRODUCT_WIDTH) - 1),
self.PRODUCT_WIDTH,
)
b = sign_extend(
mult_results[2 * i + 1] & ((1 << self.PRODUCT_WIDTH) - 1),
self.PRODUCT_WIDTH,
)
self.add_l0[i] = a + b
# ---- Stage 2 (Level 1): 8 pairwise sums ----
@@ -1365,7 +1374,10 @@ def _self_test():
mag_sq = s * s + c * c
expected = 32767 * 32767
error_pct = abs(mag_sq - expected) / expected * 100
print(f" Quadrature check: sin^2+cos^2={mag_sq}, expected~{expected}, error={error_pct:.2f}%")
print(
f" Quadrature check: sin^2+cos^2={mag_sq}, "
f"expected~{expected}, error={error_pct:.2f}%"
)
print(" NCO: OK")
# --- Mixer test ---
@@ -218,7 +218,8 @@ def generate_long_chirp_test():
if seg == 0:
buffer_write_ptr = 0
else:
# Overlap-save: copy buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP]
# Overlap-save: copy
# buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP]
for i in range(OVERLAP_SAMPLES):
input_buffer_i[i] = input_buffer_i[i + SEGMENT_ADVANCE]
input_buffer_q[i] = input_buffer_q[i + SEGMENT_ADVANCE]
@@ -335,7 +335,9 @@ def run_ddc(adc_samples):
for n in range(n_samples):
integrators[0][n + 1] = (integrators[0][n] + mixed_i[n]) & ((1 << CIC_ACC_WIDTH) - 1)
for s in range(1, CIC_STAGES):
integrators[s][n + 1] = (integrators[s][n] + integrators[s - 1][n + 1]) & ((1 << CIC_ACC_WIDTH) - 1)
integrators[s][n + 1] = (
integrators[s][n] + integrators[s - 1][n + 1]
) & ((1 << CIC_ACC_WIDTH) - 1)
# Downsample by 4
n_decimated = n_samples // CIC_DECIMATION
@@ -580,8 +582,11 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
decimated_i = np.zeros((n_chirps, output_bins), dtype=np.int64)
decimated_q = np.zeros((n_chirps, output_bins), dtype=np.int64)
print(f"[DECIM] Decimating {n_in}{output_bins} bins, mode={'peak' if mode==1 else 'avg' if mode==2 else 'simple'}, "
f"start_bin={start_bin}, {n_chirps} chirps")
mode_str = 'peak' if mode == 1 else 'avg' if mode == 2 else 'simple'
print(
f"[DECIM] Decimating {n_in}{output_bins} bins, mode={mode_str}, "
f"start_bin={start_bin}, {n_chirps} chirps"
)
for c in range(n_chirps):
# Index into input, skip start_bin
@@ -678,7 +683,9 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None):
if twiddle_file_16 and os.path.exists(twiddle_file_16):
cos_rom_16 = load_twiddle_rom(twiddle_file_16)
else:
cos_rom_16 = np.round(32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)).astype(np.int64)
cos_rom_16 = np.round(
32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)
).astype(np.int64)
LOG2N_16 = 4
doppler_map_i = np.zeros((n_range, n_total), dtype=np.int64)
@@ -835,7 +842,10 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
notched_i = doppler_i.copy()
notched_q = doppler_q.copy()
print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins (dual sub-frame)")
print(
f"[DC NOTCH] width={width}, {n_range} range bins x "
f"{n_doppler} Doppler bins (dual sub-frame)"
)
if width == 0:
print(" Pass-through (width=0)")
@@ -1167,7 +1177,12 @@ def main():
parser = argparse.ArgumentParser(description="AERIS-10 FPGA golden reference model")
parser.add_argument('--frame', type=int, default=0, help='Frame index to process')
parser.add_argument('--plot', action='store_true', help='Show plots')
parser.add_argument('--threshold', type=int, default=10000, help='Detection threshold (L1 magnitude)')
parser.add_argument(
'--threshold',
type=int,
default=10000,
help='Detection threshold (L1 magnitude)'
)
args = parser.parse_args()
# Paths
@@ -1175,7 +1190,11 @@ def main():
fpga_dir = os.path.abspath(os.path.join(script_dir, '..', '..', '..'))
data_base = os.path.expanduser("~/Downloads/adi_radar_data")
amp_data = os.path.join(data_base, "amp_radar", "phaser_amp_4MSPS_500M_300u_256_m3dB.npy")
amp_config = os.path.join(data_base, "amp_radar", "phaser_amp_4MSPS_500M_300u_256_m3dB_config.npy")
amp_config = os.path.join(
data_base,
"amp_radar",
"phaser_amp_4MSPS_500M_300u_256_m3dB_config.npy"
)
twiddle_1024 = os.path.join(fpga_dir, "fft_twiddle_1024.mem")
output_dir = os.path.join(script_dir, "hex")
@@ -1290,7 +1309,10 @@ def main():
q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF
packed = (q_val << 16) | i_val
f.write(f"{packed:08X}\n")
print(f" Wrote {fc_doppler_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)")
print(
f" Wrote {fc_doppler_packed_file} ("
f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)"
)
# Save numpy arrays for the full-chain path
np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i)
@@ -1336,7 +1358,10 @@ def main():
q_val = int(notched_q[rbin, dbin]) & 0xFFFF
packed = (q_val << 16) | i_val
f.write(f"{packed:08X}\n")
print(f" Wrote {fc_notched_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)")
print(
f" Wrote {fc_notched_packed_file} ("
f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)"
)
# CFAR on DC-notched data
CFAR_GUARD = 2
@@ -1385,7 +1410,10 @@ def main():
with open(cfar_det_list_file, 'w') as f:
f.write("# AERIS-10 Full-Chain CFAR Detection List\n")
f.write(f"# Chain: decim -> MTI -> Doppler -> DC notch(w={DC_NOTCH_WIDTH}) -> CA-CFAR\n")
f.write(f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n")
f.write(
f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, "
f"alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n"
)
f.write("# Format: range_bin doppler_bin magnitude threshold\n")
for det in cfar_detections:
r, d = det
@@ -1481,12 +1509,18 @@ def main():
print(f" Chirps processed: {DOPPLER_CHIRPS}")
print(f" Samples/chirp: {FFT_SIZE}")
print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float")
print(f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming → {snr_doppler:.1f} dB vs float")
print(
f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming "
f"{snr_doppler:.1f} dB vs float"
)
print(f" Detections (direct): {len(detections)} (threshold={args.threshold})")
print(" Full-chain decimator: 1024→64 peak detection")
print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})")
print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR")
print(f" CFAR detections: {len(cfar_detections)} (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})")
print(
f" CFAR detections: {len(cfar_detections)} "
f"(guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})"
)
print(f" Hex stimulus files: {output_dir}/")
print(" Ready for RTL co-simulation with Icarus Verilog")
@@ -199,7 +199,10 @@ def test_long_chirp():
avg_mag = sum(magnitudes) / len(magnitudes)
print(f" Magnitude: min={min_mag:.1f}, max={max_mag:.1f}, avg={avg_mag:.1f}")
print(f" Max magnitude as fraction of Q15 range: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)")
print(
f" Max magnitude as fraction of Q15 range: "
f"{max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)"
)
# Check if this looks like it came from generate_reference_chirp_q15
# That function uses 32767 * 0.9 scaling => max magnitude ~29490
@@ -262,7 +265,10 @@ def test_long_chirp():
# Check if bandwidth roughly matches expected
bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50%
if bw_match:
print(f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected {CHIRP_BW/1e6:.2f} MHz")
print(
f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected "
f"{CHIRP_BW/1e6:.2f} MHz"
)
else:
warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz")
@@ -415,8 +421,11 @@ def test_chirp_vs_model():
print(f" Max phase diff: {max_phase_diff:.4f} rad ({math.degrees(max_phase_diff):.2f} deg)")
phase_match = max_phase_diff < 0.5 # within 0.5 rad
check(phase_match,
f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg (tolerance: 28.6 deg)")
check(
phase_match,
f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg "
f"(tolerance: 28.6 deg)",
)
# ============================================================================
@@ -521,8 +530,11 @@ def test_memory_addressing():
addr_from_concat = (seg << 10) | 0 # {seg[1:0], 10'b0}
addr_end = (seg << 10) | 1023
check(addr_from_concat == base,
f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} (expected {base})")
check(
addr_from_concat == base,
f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} "
f"(expected {base})",
)
check(addr_end == end,
f"Seg {seg} end address: {{{seg}[1:0], 10'h3FF}} = {addr_end} (expected {end})")
+66 -17
View File
@@ -33,7 +33,8 @@ from enum import Enum
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTabWidget, QLabel, QPushButton, QComboBox, QSpinBox, QDoubleSpinBox,
QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox, QTableWidget, QTableWidgetItem,
QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox,
QTableWidget, QTableWidgetItem,
QHeaderView
)
from PyQt6.QtCore import (
@@ -554,11 +555,20 @@ class RadarMapWidget(QWidget):
if (!radarMarker) return;
var content = '<div class="popup-title">Radar System</div>' +
'<div class="popup-row"><span class="popup-label">Latitude:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Latitude:</span>' +
'<span class="popup-value">'
) +
radarMarker.getLatLng().lat.toFixed(6) + '</span></div>' +
'<div class="popup-row"><span class="popup-label">Longitude:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Longitude:</span>' +
'<span class="popup-value">'
) +
radarMarker.getLatLng().lng.toFixed(6) + '</span></div>' +
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value status-approaching">Active</span></div>';
(
'<div class="popup-row"><span class="popup-label">Status:</span>' +
'<span class="popup-value status-approaching">Active</span></div>'
);
radarMarker.bindPopup(content);
}}
@@ -570,10 +580,22 @@ class RadarMapWidget(QWidget):
var div = L.DomUtil.create('div', 'legend');
div.innerHTML =
'<div class="legend-title">Target Legend</div>' +
'<div class="legend-item"><div class="legend-color" style="background:#F44336"></div>Approaching</div>' +
'<div class="legend-item"><div class="legend-color" style="background:#2196F3"></div>Receding</div>' +
'<div class="legend-item"><div class="legend-color" style="background:#9E9E9E"></div>Stationary</div>' +
'<div class="legend-item"><div class="legend-color" style="background:#FF5252"></div>Radar</div>';
(
'<div class="legend-item"><div class="legend-color" ' +
'style="background:#F44336"></div>Approaching</div>'
) +
(
'<div class="legend-item"><div class="legend-color" ' +
'style="background:#2196F3"></div>Receding</div>'
) +
(
'<div class="legend-item"><div class="legend-color" ' +
'style="background:#9E9E9E"></div>Stationary</div>'
) +
(
'<div class="legend-item"><div class="legend-color" ' +
'style="background:#FF5252"></div>Radar</div>'
);
return div;
}};
@@ -590,7 +612,9 @@ class RadarMapWidget(QWidget):
updateRadarPopup();
if (bridge) {{
bridge.logFromJS('Radar position updated: ' + lat.toFixed(4) + ', ' + lon.toFixed(4));
bridge.logFromJS(
'Radar position updated: ' + lat.toFixed(4) + ', ' + lon.toFixed(4)
);
}}
}}
@@ -717,19 +741,40 @@ class RadarMapWidget(QWidget):
(target.velocity < -1 ? 'Receding' : 'Stationary');
var content = '<div class="popup-title">Target #' + target.id + '</div>' +
'<div class="popup-row"><span class="popup-label">Range:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Range:</span>' +
'<span class="popup-value">'
) +
target.range.toFixed(1) + ' m</span></div>' +
'<div class="popup-row"><span class="popup-label">Velocity:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Velocity:</span>' +
'<span class="popup-value">'
) +
target.velocity.toFixed(1) + ' m/s</span></div>' +
'<div class="popup-row"><span class="popup-label">Azimuth:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Azimuth:</span>' +
'<span class="popup-value">'
) +
target.azimuth.toFixed(1) + '&deg;</span></div>' +
'<div class="popup-row"><span class="popup-label">Elevation:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Elevation:</span>' +
'<span class="popup-value">'
) +
target.elevation.toFixed(1) + '&deg;</span></div>' +
'<div class="popup-row"><span class="popup-label">SNR:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">SNR:</span>' +
'<span class="popup-value">'
) +
target.snr.toFixed(1) + ' dB</span></div>' +
'<div class="popup-row"><span class="popup-label">Track ID:</span><span class="popup-value">' +
(
'<div class="popup-row"><span class="popup-label">Track ID:</span>' +
'<span class="popup-value">'
) +
target.track_id + '</span></div>' +
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value ' +
(
'<div class="popup-row"><span class="popup-label">Status:</span>' +
'<span class="popup-value '
) +
statusClass + '">' + statusText + '</span></div>';
targetMarkers[target.id].bindPopup(content);
@@ -770,7 +815,11 @@ class RadarMapWidget(QWidget):
if (visible) {{
// Create trails for all existing markers using stored history
for (var id in targetMarkers) {{
if (!targetTrails[id] && targetTrailHistory[id] && targetTrailHistory[id].length > 1) {{
if (
!targetTrails[id] &&
targetTrailHistory[id] &&
targetTrailHistory[id].length > 1
) {{
// Get color from current marker position (approximate)
var color = '#4CAF50'; // Default green
targetTrails[id] = L.polyline(targetTrailHistory[id], {{
+19 -4
View File
@@ -1,4 +1,10 @@
import logging
import queue
import tkinter as tk
from tkinter import messagebox
class RadarGUI:
def update_gps_display(self):
"""Step 18: Update GPS display and center map"""
try:
@@ -8,7 +14,12 @@
# Update GPS label
self.gps_label.config(
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
text=(
f"GPS: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m"
)
)
# Update map
self.update_map_display(gps_data)
@@ -19,10 +30,14 @@
def update_map_display(self, gps_data):
"""Step 18: Update map display with current GPS position"""
try:
self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
self.map_label.config(
text=(
f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
f"Altitude: {gps_data.altitude:.1f}m\n"
f"Coverage: 50km radius\n"
f"Map centered on GPS coordinates")
f"Map centered on GPS coordinates"
)
)
except Exception as e:
logging.error(f"Error updating map display: {e}")
@@ -31,7 +46,7 @@ def main():
"""Main application entry point"""
try:
root = tk.Tk()
app = RadarGUI(root)
_app = RadarGUI(root)
root.mainloop()
except Exception as e:
logging.error(f"Application error: {e}")
+257 -192
View File
@@ -5,21 +5,18 @@ import queue
import time
import struct
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import logging
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional
from scipy import signal
from sklearn.cluster import DBSCAN
from filterpy.kalman import KalmanFilter
import crcmod
import math
try:
import usb.core
import usb.util
USB_AVAILABLE = True
except ImportError:
USB_AVAILABLE = False
@@ -28,13 +25,15 @@ except ImportError:
try:
from pyftdi.ftdi import Ftdi
from pyftdi.usbtools import UsbTools
FTDI_AVAILABLE = True
except ImportError:
FTDI_AVAILABLE = False
logging.warning("pyftdi not available. FTDI functionality will be disabled.")
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
@dataclass
class RadarTarget:
@@ -47,6 +46,7 @@ class RadarTarget:
timestamp: float
track_id: int = -1
@dataclass
class RadarSettings:
system_frequency: float = 10e9
@@ -58,6 +58,7 @@ class RadarSettings:
prf2: float = 2000
max_distance: float = 50000
@dataclass
class GPSData:
latitude: float
@@ -65,6 +66,7 @@ class GPSData:
altitude: float
timestamp: float
class STM32USBInterface:
def __init__(self):
self.device = None
@@ -94,27 +96,39 @@ class STM32USBInterface:
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
for dev in found_devices:
try:
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
devices.append({
'description': f"{product} ({serial})",
'vendor_id': vid,
'product_id': pid,
'device': dev
})
except:
devices.append({
'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})",
'vendor_id': vid,
'product_id': pid,
'device': dev
})
product = (
usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
)
serial = (
usb.util.get_string(dev, dev.iSerialNumber)
if dev.iSerialNumber
else "Unknown"
)
devices.append(
{
"description": f"{product} ({serial})",
"vendor_id": vid,
"product_id": pid,
"device": dev,
}
)
except Exception:
devices.append(
{
"description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})",
"vendor_id": vid,
"product_id": pid,
"device": dev,
}
)
return devices
except Exception as e:
logging.error(f"Error listing USB devices: {e}")
# Return mock devices for testing
return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}]
return [
{"description": "STM32 Virtual COM Port", "vendor_id": 0x0483, "product_id": 0x5740}
]
def open_device(self, device_info):
"""Open STM32 USB CDC device"""
@@ -123,7 +137,7 @@ class STM32USBInterface:
return False
try:
self.device = device_info['device']
self.device = device_info["device"]
# Detach kernel driver if active
if self.device.is_kernel_driver_active(0):
@@ -139,12 +153,16 @@ class STM32USBInterface:
# Find bulk endpoints (CDC data interface)
self.ep_out = usb.util.find_descriptor(
intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
),
)
self.ep_in = usb.util.find_descriptor(
intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
),
)
if self.ep_out is None or self.ep_in is None:
@@ -204,7 +222,7 @@ class STM32USBInterface:
chunk = data[i : i + packet_size]
# Pad to packet size if needed
if len(chunk) < packet_size:
chunk += b'\x00' * (packet_size - len(chunk))
chunk += b"\x00" * (packet_size - len(chunk))
self.ep_out.write(chunk)
return True
@@ -214,16 +232,16 @@ class STM32USBInterface:
def _create_settings_packet(self, settings):
"""Create binary settings packet for USB transmission"""
packet = b'SET'
packet += struct.pack('>d', settings.system_frequency)
packet += struct.pack('>d', settings.chirp_duration)
packet += struct.pack('>I', settings.chirps_per_position)
packet += struct.pack('>d', settings.freq_min)
packet += struct.pack('>d', settings.freq_max)
packet += struct.pack('>d', settings.prf1)
packet += struct.pack('>d', settings.prf2)
packet += struct.pack('>d', settings.max_distance)
packet += b'END'
packet = b"SET"
packet += struct.pack(">d", settings.system_frequency)
packet += struct.pack(">d", settings.chirp_duration)
packet += struct.pack(">I", settings.chirps_per_position)
packet += struct.pack(">d", settings.freq_min)
packet += struct.pack(">d", settings.freq_max)
packet += struct.pack(">d", settings.prf1)
packet += struct.pack(">d", settings.prf2)
packet += struct.pack(">d", settings.max_distance)
packet += b"END"
return packet
def close(self):
@@ -235,6 +253,7 @@ class STM32USBInterface:
except Exception as e:
logging.error(f"Error closing USB device: {e}")
class FTDIInterface:
def __init__(self):
self.ftdi = None
@@ -250,15 +269,14 @@ class FTDIInterface:
devices = []
# Get list of all FTDI devices
for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID
devices.append({
'description': f"FTDI Device {device}",
'url': f"ftdi://{device}/1"
})
devices.append(
{"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"}
)
return devices
except Exception as e:
logging.error(f"Error listing FTDI devices: {e}")
# Return mock devices for testing
return [{'description': 'FT2232H Device A', 'url': 'ftdi://device/1'}]
return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}]
def open_device(self, device_url):
"""Open FTDI device using pyftdi"""
@@ -307,6 +325,7 @@ class FTDIInterface:
self.ftdi.close()
self.is_open = False
class RadarProcessor:
def __init__(self):
self.range_doppler_map = np.zeros((1024, 32))
@@ -355,11 +374,13 @@ class RadarProcessor:
for label in set(clustering.labels_):
if label != -1:
cluster_points = points[clustering.labels_ == label]
clusters.append({
'center': np.mean(cluster_points, axis=0),
'points': cluster_points,
'size': len(cluster_points)
})
clusters.append(
{
"center": np.mean(cluster_points, axis=0),
"points": cluster_points,
"size": len(cluster_points),
}
)
return clusters
@@ -369,12 +390,12 @@ class RadarProcessor:
for detection in detections:
best_track = None
min_distance = float('inf')
min_distance = float("inf")
for track_id, track in self.tracks.items():
distance = np.sqrt(
(detection.range - track['state'][0])**2 +
(detection.velocity - track['state'][2])**2
(detection.range - track["state"][0]) ** 2
+ (detection.velocity - track["state"][2]) ** 2
)
if distance < min_distance and distance < 500:
@@ -399,35 +420,33 @@ class RadarProcessor:
if detection.track_id not in self.tracks:
kf = KalmanFilter(dim_x=4, dim_z=2)
kf.x = np.array([detection.range, 0, detection.velocity, 0])
kf.F = np.array([[1, 1, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 1],
[0, 0, 0, 1]])
kf.H = np.array([[1, 0, 0, 0],
[0, 0, 1, 0]])
kf.F = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]])
kf.H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]])
kf.P *= 1000
kf.R = np.diag([10, 1])
kf.Q = np.eye(4) * 0.1
self.tracks[detection.track_id] = {
'filter': kf,
'state': kf.x,
'last_update': current_time,
'hits': 1
"filter": kf,
"state": kf.x,
"last_update": current_time,
"hits": 1,
}
else:
track = self.tracks[detection.track_id]
track['filter'].predict()
track['filter'].update([detection.range, detection.velocity])
track['state'] = track['filter'].x
track['last_update'] = current_time
track['hits'] += 1
track["filter"].predict()
track["filter"].update([detection.range, detection.velocity])
track["state"] = track["filter"].x
track["last_update"] = current_time
track["hits"] += 1
stale_tracks = [tid for tid, track in self.tracks.items()
if current_time - track['last_update'] > 5.0]
stale_tracks = [
tid for tid, track in self.tracks.items() if current_time - track["last_update"] > 5.0
]
for tid in stale_tracks:
del self.tracks[tid]
class USBPacketParser:
def __init__(self):
self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000)
@@ -439,9 +458,9 @@ class USBPacketParser:
try:
# Try text format first: "GPS:lat,lon,alt\r\n"
text_data = data.decode('utf-8', errors='ignore').strip()
if text_data.startswith('GPS:'):
parts = text_data.split(':')[1].split(',')
text_data = data.decode("utf-8", errors="ignore").strip()
if text_data.startswith("GPS:"):
parts = text_data.split(":")[1].split(",")
if len(parts) == 3:
lat = float(parts[0])
lon = float(parts[1])
@@ -449,7 +468,7 @@ class USBPacketParser:
return GPSData(latitude=lat, longitude=lon, altitude=alt, timestamp=time.time())
# Try binary format
if len(data) >= 26 and data[0:4] == b'GPSB':
if len(data) >= 26 and data[0:4] == b"GPSB":
return self._parse_binary_gps(data)
except Exception as e:
@@ -476,29 +495,32 @@ class USBPacketParser:
lat_bits = 0
for i in range(8):
lat_bits = (lat_bits << 8) | data[4 + i]
latitude = struct.unpack('>d', struct.pack('>Q', lat_bits))[0]
latitude = struct.unpack(">d", struct.pack(">Q", lat_bits))[0]
# Parse longitude (double, big-endian)
lon_bits = 0
for i in range(8):
lon_bits = (lon_bits << 8) | data[12 + i]
longitude = struct.unpack('>d', struct.pack('>Q', lon_bits))[0]
longitude = struct.unpack(">d", struct.pack(">Q", lon_bits))[0]
# Parse altitude (float, big-endian)
alt_bits = 0
for i in range(4):
alt_bits = (alt_bits << 8) | data[20 + i]
altitude = struct.unpack('>f', struct.pack('>I', alt_bits))[0]
altitude = struct.unpack(">f", struct.pack(">I", alt_bits))[0]
return GPSData(latitude=latitude, longitude=longitude, altitude=altitude, timestamp=time.time())
return GPSData(
latitude=latitude, longitude=longitude, altitude=altitude, timestamp=time.time()
)
except Exception as e:
logging.error(f"Error parsing binary GPS: {e}")
return None
class RadarPacketParser:
def __init__(self):
self.sync_pattern = b'\xA5\xC3'
self.sync_pattern = b"\xa5\xc3"
self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000)
def parse_packet(self, data):
@@ -514,7 +536,7 @@ class RadarPacketParser:
if len(packet) < 6:
return None
sync = packet[0:2]
_sync = packet[0:2]
packet_type = packet[2]
length = packet[3]
@@ -522,11 +544,13 @@ class RadarPacketParser:
return None
payload = packet[4 : 4 + length]
crc_received = struct.unpack('<H', packet[4+length:4+length+2])[0]
crc_received = struct.unpack("<H", packet[4 + length : 4 + length + 2])[0]
crc_calculated = self.calculate_crc(packet[0 : 4 + length])
if crc_calculated != crc_received:
logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}")
logging.warning(
f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}"
)
return None
if packet_type == 0x01:
@@ -547,18 +571,18 @@ class RadarPacketParser:
return None
try:
range_value = struct.unpack('>I', payload[0:4])[0]
range_value = struct.unpack(">I", payload[0:4])[0]
elevation = payload[4] & 0x1F
azimuth = payload[5] & 0x3F
chirp_counter = payload[6] & 0x1F
return {
'type': 'range',
'range': range_value,
'elevation': elevation,
'azimuth': azimuth,
'chirp': chirp_counter,
'timestamp': time.time()
"type": "range",
"range": range_value,
"elevation": elevation,
"azimuth": azimuth,
"chirp": chirp_counter,
"timestamp": time.time(),
}
except Exception as e:
logging.error(f"Error parsing range packet: {e}")
@@ -569,20 +593,20 @@ class RadarPacketParser:
return None
try:
doppler_real = struct.unpack('>h', payload[0:2])[0]
doppler_imag = struct.unpack('>h', payload[2:4])[0]
doppler_real = struct.unpack(">h", payload[0:2])[0]
doppler_imag = struct.unpack(">h", payload[2:4])[0]
elevation = payload[4] & 0x1F
azimuth = payload[5] & 0x3F
chirp_counter = payload[6] & 0x1F
return {
'type': 'doppler',
'doppler_real': doppler_real,
'doppler_imag': doppler_imag,
'elevation': elevation,
'azimuth': azimuth,
'chirp': chirp_counter,
'timestamp': time.time()
"type": "doppler",
"doppler_real": doppler_real,
"doppler_imag": doppler_imag,
"elevation": elevation,
"azimuth": azimuth,
"chirp": chirp_counter,
"timestamp": time.time(),
}
except Exception as e:
logging.error(f"Error parsing Doppler packet: {e}")
@@ -599,17 +623,18 @@ class RadarPacketParser:
chirp_counter = payload[3] & 0x1F
return {
'type': 'detection',
'detected': detection_flag,
'elevation': elevation,
'azimuth': azimuth,
'chirp': chirp_counter,
'timestamp': time.time()
"type": "detection",
"detected": detection_flag,
"elevation": elevation,
"azimuth": azimuth,
"chirp": chirp_counter,
"timestamp": time.time(),
}
except Exception as e:
logging.error(f"Error parsing detection packet: {e}")
return None
class RadarGUI:
def __init__(self, root):
self.root = root
@@ -643,17 +668,17 @@ class RadarGUI:
def create_gui(self):
"""Create the main GUI with tabs"""
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
self.notebook.pack(fill="both", expand=True, padx=10, pady=10)
self.tab_main = ttk.Frame(self.notebook)
self.tab_map = ttk.Frame(self.notebook)
self.tab_diagnostics = ttk.Frame(self.notebook)
self.tab_settings = ttk.Frame(self.notebook)
self.notebook.add(self.tab_main, text='Main View')
self.notebook.add(self.tab_map, text='Map View')
self.notebook.add(self.tab_diagnostics, text='Diagnostics')
self.notebook.add(self.tab_settings, text='Settings')
self.notebook.add(self.tab_main, text="Main View")
self.notebook.add(self.tab_map, text="Map View")
self.notebook.add(self.tab_diagnostics, text="Diagnostics")
self.notebook.add(self.tab_settings, text="Settings")
self.setup_main_tab()
self.setup_map_tab()
@@ -663,7 +688,7 @@ class RadarGUI:
"""Setup the main radar display tab"""
# Control frame
control_frame = ttk.Frame(self.tab_main)
control_frame.pack(fill='x', padx=10, pady=5)
control_frame.pack(fill="x", padx=10, pady=5)
# USB Device selection
ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5)
@@ -674,115 +699,123 @@ class RadarGUI:
self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30)
self.ftdi_combo.grid(row=0, column=3, padx=5)
ttk.Button(control_frame, text="Refresh Devices",
command=self.refresh_devices).grid(row=0, column=4, padx=5)
ttk.Button(control_frame, text="Refresh Devices", command=self.refresh_devices).grid(
row=0, column=4, padx=5
)
self.start_button = ttk.Button(control_frame, text="Start Radar",
command=self.start_radar)
self.start_button = ttk.Button(control_frame, text="Start Radar", command=self.start_radar)
self.start_button.grid(row=0, column=5, padx=5)
self.stop_button = ttk.Button(control_frame, text="Stop Radar",
command=self.stop_radar, state="disabled")
self.stop_button = ttk.Button(
control_frame, text="Stop Radar", command=self.stop_radar, state="disabled"
)
self.stop_button.grid(row=0, column=6, padx=5)
# GPS info
self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...")
self.gps_label.grid(row=1, column=0, columnspan=4, sticky='w', padx=5, pady=2)
self.gps_label.grid(row=1, column=0, columnspan=4, sticky="w", padx=5, pady=2)
# Status info
self.status_label = ttk.Label(control_frame, text="Status: Ready")
self.status_label.grid(row=1, column=4, columnspan=3, sticky='e', padx=5, pady=2)
self.status_label.grid(row=1, column=4, columnspan=3, sticky="e", padx=5, pady=2)
# Main display area
display_frame = ttk.Frame(self.tab_main)
display_frame.pack(fill='both', expand=True, padx=10, pady=5)
display_frame.pack(fill="both", expand=True, padx=10, pady=5)
# Range-Doppler Map
fig = Figure(figsize=(10, 6))
self.range_doppler_ax = fig.add_subplot(111)
self.range_doppler_plot = self.range_doppler_ax.imshow(
np.random.rand(1024, 32), aspect='auto', cmap='hot',
extent=[0, 32, 0, 1024])
self.range_doppler_ax.set_title('Range-Doppler Map')
self.range_doppler_ax.set_xlabel('Doppler Bin')
self.range_doppler_ax.set_ylabel('Range Bin')
np.random.rand(1024, 32), aspect="auto", cmap="hot", extent=[0, 32, 0, 1024]
)
self.range_doppler_ax.set_title("Range-Doppler Map")
self.range_doppler_ax.set_xlabel("Doppler Bin")
self.range_doppler_ax.set_ylabel("Range Bin")
self.canvas = FigureCanvasTkAgg(fig, display_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side='left', fill='both', expand=True)
self.canvas.get_tk_widget().pack(side="left", fill="both", expand=True)
# Targets list
targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets")
targets_frame.pack(side='right', fill='y', padx=5)
targets_frame.pack(side="right", fill="y", padx=5)
self.targets_tree = ttk.Treeview(targets_frame,
columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR'),
show='headings', height=20)
self.targets_tree.heading('ID', text='Track ID')
self.targets_tree.heading('Range', text='Range (m)')
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
self.targets_tree.heading('Azimuth', text='Azimuth')
self.targets_tree.heading('Elevation', text='Elevation')
self.targets_tree.heading('SNR', text='SNR (dB)')
self.targets_tree = ttk.Treeview(
targets_frame,
columns=("ID", "Range", "Velocity", "Azimuth", "Elevation", "SNR"),
show="headings",
height=20,
)
self.targets_tree.heading("ID", text="Track ID")
self.targets_tree.heading("Range", text="Range (m)")
self.targets_tree.heading("Velocity", text="Velocity (m/s)")
self.targets_tree.heading("Azimuth", text="Azimuth")
self.targets_tree.heading("Elevation", text="Elevation")
self.targets_tree.heading("SNR", text="SNR (dB)")
self.targets_tree.column('ID', width=80)
self.targets_tree.column('Range', width=100)
self.targets_tree.column('Velocity', width=100)
self.targets_tree.column('Azimuth', width=80)
self.targets_tree.column('Elevation', width=80)
self.targets_tree.column('SNR', width=80)
self.targets_tree.column("ID", width=80)
self.targets_tree.column("Range", width=100)
self.targets_tree.column("Velocity", width=100)
self.targets_tree.column("Azimuth", width=80)
self.targets_tree.column("Elevation", width=80)
self.targets_tree.column("SNR", width=80)
self.targets_tree.pack(fill='both', expand=True, padx=5, pady=5)
self.targets_tree.pack(fill="both", expand=True, padx=5, pady=5)
def setup_map_tab(self):
"""Setup the map display tab"""
self.map_frame = ttk.Frame(self.tab_map)
self.map_frame.pack(fill='both', expand=True, padx=10, pady=10)
self.map_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Map placeholder
self.map_label = ttk.Label(self.map_frame, text="Map will be displayed here after GPS data is received",
font=('Arial', 12))
self.map_label = ttk.Label(
self.map_frame,
text="Map will be displayed here after GPS data is received",
font=("Arial", 12),
)
self.map_label.pack(expand=True)
def setup_settings_tab(self):
"""Setup the settings tab"""
settings_frame = ttk.Frame(self.tab_settings)
settings_frame.pack(fill='both', expand=True, padx=10, pady=10)
settings_frame.pack(fill="both", expand=True, padx=10, pady=10)
entries = [
('System Frequency (Hz):', 'system_frequency', 10e9),
('Chirp Duration (s):', 'chirp_duration', 30e-6),
('Chirps per Position:', 'chirps_per_position', 32),
('Frequency Min (Hz):', 'freq_min', 10e6),
('Frequency Max (Hz):', 'freq_max', 30e6),
('PRF1 (Hz):', 'prf1', 1000),
('PRF2 (Hz):', 'prf2', 2000),
('Max Distance (m):', 'max_distance', 50000)
("System Frequency (Hz):", "system_frequency", 10e9),
("Chirp Duration (s):", "chirp_duration", 30e-6),
("Chirps per Position:", "chirps_per_position", 32),
("Frequency Min (Hz):", "freq_min", 10e6),
("Frequency Max (Hz):", "freq_max", 30e6),
("PRF1 (Hz):", "prf1", 1000),
("PRF2 (Hz):", "prf2", 2000),
("Max Distance (m):", "max_distance", 50000),
]
self.settings_vars = {}
for i, (label, attr, default) in enumerate(entries):
ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky='w', padx=5, pady=5)
ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky="w", padx=5, pady=5)
var = tk.StringVar(value=str(default))
entry = ttk.Entry(settings_frame, textvariable=var, width=20)
entry.grid(row=i, column=1, padx=5, pady=5)
self.settings_vars[attr] = var
ttk.Button(settings_frame, text="Apply Settings",
command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10)
ttk.Button(settings_frame, text="Apply Settings", command=self.apply_settings).grid(
row=len(entries), column=0, columnspan=2, pady=10
)
def refresh_devices(self):
"""Refresh available USB devices"""
# STM32 USB devices
stm32_devices = self.stm32_usb_interface.list_devices()
stm32_names = [dev['description'] for dev in stm32_devices]
self.stm32_usb_combo['values'] = stm32_names
stm32_names = [dev["description"] for dev in stm32_devices]
self.stm32_usb_combo["values"] = stm32_names
# FTDI devices
ftdi_devices = self.ftdi_interface.list_devices()
ftdi_names = [dev['description'] for dev in ftdi_devices]
self.ftdi_combo['values'] = ftdi_names
ftdi_names = [dev["description"] for dev in ftdi_devices]
self.ftdi_combo["values"] = ftdi_names
if stm32_names:
self.stm32_usb_combo.current(0)
@@ -813,9 +846,11 @@ class RadarGUI:
if ftdi_index != -1:
ftdi_devices = self.ftdi_interface.list_devices()
if ftdi_index < len(ftdi_devices):
device_url = ftdi_devices[ftdi_index]['url']
device_url = ftdi_devices[ftdi_index]["url"]
if not self.ftdi_interface.open_device(device_url):
logging.warning("Failed to open FTDI device, continuing without radar data")
logging.warning(
"Failed to open FTDI device, continuing without radar data"
)
else:
logging.warning("No FTDI device selected, continuing without radar data")
else:
@@ -856,14 +891,14 @@ class RadarGUI:
def apply_settings(self):
"""Step 13: Apply and send radar settings via USB"""
try:
self.settings.system_frequency = float(self.settings_vars['system_frequency'].get())
self.settings.chirp_duration = float(self.settings_vars['chirp_duration'].get())
self.settings.chirps_per_position = int(self.settings_vars['chirps_per_position'].get())
self.settings.freq_min = float(self.settings_vars['freq_min'].get())
self.settings.freq_max = float(self.settings_vars['freq_max'].get())
self.settings.prf1 = float(self.settings_vars['prf1'].get())
self.settings.prf2 = float(self.settings_vars['prf2'].get())
self.settings.max_distance = float(self.settings_vars['max_distance'].get())
self.settings.system_frequency = float(self.settings_vars["system_frequency"].get())
self.settings.chirp_duration = float(self.settings_vars["chirp_duration"].get())
self.settings.chirps_per_position = int(self.settings_vars["chirps_per_position"].get())
self.settings.freq_min = float(self.settings_vars["freq_min"].get())
self.settings.freq_max = float(self.settings_vars["freq_max"].get())
self.settings.prf1 = float(self.settings_vars["prf1"].get())
self.settings.prf2 = float(self.settings_vars["prf2"].get())
self.settings.max_distance = float(self.settings_vars["max_distance"].get())
if self.stm32_usb_interface.is_open:
self.stm32_usb_interface.send_settings(self.settings)
@@ -886,7 +921,7 @@ class RadarGUI:
def process_radar_data(self):
"""Step 39: Process incoming radar data from FTDI"""
buffer = b''
buffer = b""
while True:
if self.running and self.ftdi_interface.is_open:
try:
@@ -898,7 +933,7 @@ class RadarGUI:
packet = self.radar_packet_parser.parse_packet(buffer)
if packet:
self.process_radar_packet(packet)
packet_length = 4 + len(packet.get('payload', b'')) + 2
packet_length = 4 + len(packet.get("payload", b"")) + 2
buffer = buffer[packet_length:]
self.received_packets += 1
else:
@@ -921,7 +956,12 @@ class RadarGUI:
gps_data = self.usb_packet_parser.parse_gps_data(data)
if gps_data:
self.gps_data_queue.put(gps_data)
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
logging.info(
"GPS Data received via USB: "
f"Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m"
)
except Exception as e:
logging.error(f"Error processing GPS data via USB: {e}")
time.sleep(0.1)
@@ -929,29 +969,34 @@ class RadarGUI:
def process_radar_packet(self, packet):
"""Step 40: Process radar data and update displays"""
try:
if packet['type'] == 'range':
range_meters = packet['range'] * 0.1
if packet["type"] == "range":
range_meters = packet["range"] * 0.1
target = RadarTarget(
id=packet['chirp'],
id=packet["chirp"],
range=range_meters,
velocity=0,
azimuth=packet['azimuth'],
elevation=packet['elevation'],
azimuth=packet["azimuth"],
elevation=packet["elevation"],
snr=20.0,
timestamp=packet['timestamp']
timestamp=packet["timestamp"],
)
self.update_range_doppler_map(target)
elif packet['type'] == 'doppler':
elif packet["type"] == "doppler":
lambda_wavelength = 3e8 / self.settings.system_frequency
velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2)
velocity = (packet["doppler_real"] / 32767.0) * (
self.settings.prf1 * lambda_wavelength / 2
)
self.update_target_velocity(packet, velocity)
elif packet['type'] == 'detection':
if packet['detected']:
logging.info(f"CFAR Detection: Elevation {packet['elevation']}, Azimuth {packet['azimuth']}")
elif packet["type"] == "detection":
if packet["detected"]:
logging.info(
f"CFAR Detection: Elevation {packet['elevation']}, "
f"Azimuth {packet['azimuth']}"
)
except Exception as e:
logging.error(f"Error processing radar packet: {e}")
@@ -971,9 +1016,11 @@ class RadarGUI:
def update_target_velocity(self, packet, velocity):
"""Update target velocity information"""
for target in self.radar_processor.detected_targets:
if (target.azimuth == packet['azimuth'] and
target.elevation == packet['elevation'] and
target.id == packet['chirp']):
if (
target.azimuth == packet["azimuth"]
and target.elevation == packet["elevation"]
and target.id == packet["chirp"]
):
target.velocity = velocity
break
@@ -983,10 +1030,15 @@ class RadarGUI:
# Update status
if self.running:
self.status_label.config(
text=f"Status: Running - Packets: {self.received_packets} - GPS: {self.current_gps.latitude:.4f}, {self.current_gps.longitude:.4f}")
text=(
f"Status: Running - Packets: {self.received_packets} - "
f"GPS: {self.current_gps.latitude:.4f}, "
f"{self.current_gps.longitude:.4f}"
)
)
# Update range-Doppler map
if hasattr(self, 'range_doppler_plot'):
if hasattr(self, "range_doppler_plot"):
display_data = np.log10(self.radar_processor.range_doppler_map + 1)
self.range_doppler_plot.set_array(display_data)
self.canvas.draw_idle()
@@ -1008,14 +1060,18 @@ class RadarGUI:
self.targets_tree.delete(item)
for target in self.radar_processor.detected_targets[-20:]:
self.targets_tree.insert('', 'end', values=(
self.targets_tree.insert(
"",
"end",
values=(
target.track_id,
f"{target.range:.1f}",
f"{target.velocity:.1f}",
target.azimuth,
target.elevation,
f"{target.snr:.1f}"
))
f"{target.snr:.1f}",
),
)
def update_gps_display(self):
"""Step 18: Update GPS display and center map"""
@@ -1026,7 +1082,12 @@ class RadarGUI:
# Update GPS label
self.gps_label.config(
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
text=(
f"GPS: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m"
)
)
# Update map
self.update_map_display(gps_data)
@@ -1037,23 +1098,27 @@ class RadarGUI:
def update_map_display(self, gps_data):
"""Step 18: Update map display with current GPS position"""
try:
self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
self.map_label.config(
text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
f"Altitude: {gps_data.altitude:.1f}m\n"
f"Coverage: 50km radius\n"
f"Map centered on GPS coordinates")
f"Map centered on GPS coordinates"
)
except Exception as e:
logging.error(f"Error updating map display: {e}")
def main():
"""Main application entry point"""
try:
root = tk.Tk()
app = RadarGUI(root)
_app = RadarGUI(root)
root.mainloop()
except Exception as e:
logging.error(f"Application error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
if __name__ == "__main__":
main()
+288 -209
View File
@@ -5,13 +5,10 @@ import queue
import time
import struct
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import logging
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional
from scipy import signal
from sklearn.cluster import DBSCAN
from filterpy.kalman import KalmanFilter
import crcmod
@@ -20,6 +17,7 @@ import math
try:
import usb.core
import usb.util
USB_AVAILABLE = True
except ImportError:
USB_AVAILABLE = False
@@ -28,13 +26,15 @@ except ImportError:
try:
from pyftdi.ftdi import Ftdi
from pyftdi.usbtools import UsbTools
FTDI_AVAILABLE = True
except ImportError:
FTDI_AVAILABLE = False
logging.warning("pyftdi not available. FTDI functionality will be disabled.")
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
@dataclass
class RadarTarget:
@@ -47,6 +47,7 @@ class RadarTarget:
timestamp: float
track_id: int = -1
@dataclass
class RadarSettings:
system_frequency: float = 10e9
@@ -58,6 +59,7 @@ class RadarSettings:
prf2: float = 2000
max_distance: float = 50000
@dataclass
class GPSData:
latitude: float
@@ -66,6 +68,7 @@ class GPSData:
pitch: float # Pitch angle in degrees
timestamp: float
class STM32USBInterface:
def __init__(self):
self.device = None
@@ -95,27 +98,39 @@ class STM32USBInterface:
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
for dev in found_devices:
try:
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
devices.append({
'description': f"{product} ({serial})",
'vendor_id': vid,
'product_id': pid,
'device': dev
})
except:
devices.append({
'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})",
'vendor_id': vid,
'product_id': pid,
'device': dev
})
product = (
usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
)
serial = (
usb.util.get_string(dev, dev.iSerialNumber)
if dev.iSerialNumber
else "Unknown"
)
devices.append(
{
"description": f"{product} ({serial})",
"vendor_id": vid,
"product_id": pid,
"device": dev,
}
)
except Exception:
devices.append(
{
"description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})",
"vendor_id": vid,
"product_id": pid,
"device": dev,
}
)
return devices
except Exception as e:
logging.error(f"Error listing USB devices: {e}")
# Return mock devices for testing
return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}]
return [
{"description": "STM32 Virtual COM Port", "vendor_id": 0x0483, "product_id": 0x5740}
]
def open_device(self, device_info):
"""Open STM32 USB CDC device"""
@@ -124,7 +139,7 @@ class STM32USBInterface:
return False
try:
self.device = device_info['device']
self.device = device_info["device"]
# Detach kernel driver if active
if self.device.is_kernel_driver_active(0):
@@ -140,12 +155,16 @@ class STM32USBInterface:
# Find bulk endpoints (CDC data interface)
self.ep_out = usb.util.find_descriptor(
intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
),
)
self.ep_in = usb.util.find_descriptor(
intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
),
)
if self.ep_out is None or self.ep_in is None:
@@ -205,7 +224,7 @@ class STM32USBInterface:
chunk = data[i : i + packet_size]
# Pad to packet size if needed
if len(chunk) < packet_size:
chunk += b'\x00' * (packet_size - len(chunk))
chunk += b"\x00" * (packet_size - len(chunk))
self.ep_out.write(chunk)
return True
@@ -215,16 +234,16 @@ class STM32USBInterface:
def _create_settings_packet(self, settings):
"""Create binary settings packet for USB transmission"""
packet = b'SET'
packet += struct.pack('>d', settings.system_frequency)
packet += struct.pack('>d', settings.chirp_duration)
packet += struct.pack('>I', settings.chirps_per_position)
packet += struct.pack('>d', settings.freq_min)
packet += struct.pack('>d', settings.freq_max)
packet += struct.pack('>d', settings.prf1)
packet += struct.pack('>d', settings.prf2)
packet += struct.pack('>d', settings.max_distance)
packet += b'END'
packet = b"SET"
packet += struct.pack(">d", settings.system_frequency)
packet += struct.pack(">d", settings.chirp_duration)
packet += struct.pack(">I", settings.chirps_per_position)
packet += struct.pack(">d", settings.freq_min)
packet += struct.pack(">d", settings.freq_max)
packet += struct.pack(">d", settings.prf1)
packet += struct.pack(">d", settings.prf2)
packet += struct.pack(">d", settings.max_distance)
packet += b"END"
return packet
def close(self):
@@ -236,6 +255,7 @@ class STM32USBInterface:
except Exception as e:
logging.error(f"Error closing USB device: {e}")
class FTDIInterface:
def __init__(self):
self.ftdi = None
@@ -251,15 +271,14 @@ class FTDIInterface:
devices = []
# Get list of all FTDI devices
for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID
devices.append({
'description': f"FTDI Device {device}",
'url': f"ftdi://{device}/1"
})
devices.append(
{"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"}
)
return devices
except Exception as e:
logging.error(f"Error listing FTDI devices: {e}")
# Return mock devices for testing
return [{'description': 'FT2232H Device A', 'url': 'ftdi://device/1'}]
return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}]
def open_device(self, device_url):
"""Open FTDI device using pyftdi"""
@@ -308,6 +327,7 @@ class FTDIInterface:
self.ftdi.close()
self.is_open = False
class RadarProcessor:
def __init__(self):
self.range_doppler_map = np.zeros((1024, 32))
@@ -356,11 +376,13 @@ class RadarProcessor:
for label in set(clustering.labels_):
if label != -1:
cluster_points = points[clustering.labels_ == label]
clusters.append({
'center': np.mean(cluster_points, axis=0),
'points': cluster_points,
'size': len(cluster_points)
})
clusters.append(
{
"center": np.mean(cluster_points, axis=0),
"points": cluster_points,
"size": len(cluster_points),
}
)
return clusters
@@ -370,12 +392,12 @@ class RadarProcessor:
for detection in detections:
best_track = None
min_distance = float('inf')
min_distance = float("inf")
for track_id, track in self.tracks.items():
distance = np.sqrt(
(detection.range - track['state'][0])**2 +
(detection.velocity - track['state'][2])**2
(detection.range - track["state"][0]) ** 2
+ (detection.velocity - track["state"][2]) ** 2
)
if distance < min_distance and distance < 500:
@@ -400,35 +422,33 @@ class RadarProcessor:
if detection.track_id not in self.tracks:
kf = KalmanFilter(dim_x=4, dim_z=2)
kf.x = np.array([detection.range, 0, detection.velocity, 0])
kf.F = np.array([[1, 1, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 1],
[0, 0, 0, 1]])
kf.H = np.array([[1, 0, 0, 0],
[0, 0, 1, 0]])
kf.F = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]])
kf.H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]])
kf.P *= 1000
kf.R = np.diag([10, 1])
kf.Q = np.eye(4) * 0.1
self.tracks[detection.track_id] = {
'filter': kf,
'state': kf.x,
'last_update': current_time,
'hits': 1
"filter": kf,
"state": kf.x,
"last_update": current_time,
"hits": 1,
}
else:
track = self.tracks[detection.track_id]
track['filter'].predict()
track['filter'].update([detection.range, detection.velocity])
track['state'] = track['filter'].x
track['last_update'] = current_time
track['hits'] += 1
track["filter"].predict()
track["filter"].update([detection.range, detection.velocity])
track["state"] = track["filter"].x
track["last_update"] = current_time
track["hits"] += 1
stale_tracks = [tid for tid, track in self.tracks.items()
if current_time - track['last_update'] > 5.0]
stale_tracks = [
tid for tid, track in self.tracks.items() if current_time - track["last_update"] > 5.0
]
for tid in stale_tracks:
del self.tracks[tid]
class USBPacketParser:
def __init__(self):
self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000)
@@ -440,18 +460,24 @@ class USBPacketParser:
try:
# Try text format first: "GPS:lat,lon,alt,pitch\r\n"
text_data = data.decode('utf-8', errors='ignore').strip()
if text_data.startswith('GPS:'):
parts = text_data.split(':')[1].split(',')
text_data = data.decode("utf-8", errors="ignore").strip()
if text_data.startswith("GPS:"):
parts = text_data.split(":")[1].split(",")
if len(parts) == 4: # Now expecting 4 values
lat = float(parts[0])
lon = float(parts[1])
alt = float(parts[2])
pitch = float(parts[3]) # Pitch angle in degrees
return GPSData(latitude=lat, longitude=lon, altitude=alt, pitch=pitch, timestamp=time.time())
return GPSData(
latitude=lat,
longitude=lon,
altitude=alt,
pitch=pitch,
timestamp=time.time(),
)
# Try binary format (30 bytes with pitch)
if len(data) >= 30 and data[0:4] == b'GPSB':
if len(data) >= 30 and data[0:4] == b"GPSB":
return self._parse_binary_gps_with_pitch(data)
except Exception as e:
@@ -478,41 +504,42 @@ class USBPacketParser:
lat_bits = 0
for i in range(8):
lat_bits = (lat_bits << 8) | data[4 + i]
latitude = struct.unpack('>d', struct.pack('>Q', lat_bits))[0]
latitude = struct.unpack(">d", struct.pack(">Q", lat_bits))[0]
# Parse longitude (double, big-endian)
lon_bits = 0
for i in range(8):
lon_bits = (lon_bits << 8) | data[12 + i]
longitude = struct.unpack('>d', struct.pack('>Q', lon_bits))[0]
longitude = struct.unpack(">d", struct.pack(">Q", lon_bits))[0]
# Parse altitude (float, big-endian)
alt_bits = 0
for i in range(4):
alt_bits = (alt_bits << 8) | data[20 + i]
altitude = struct.unpack('>f', struct.pack('>I', alt_bits))[0]
altitude = struct.unpack(">f", struct.pack(">I", alt_bits))[0]
# Parse pitch angle (float, big-endian)
pitch_bits = 0
for i in range(4):
pitch_bits = (pitch_bits << 8) | data[24 + i]
pitch = struct.unpack('>f', struct.pack('>I', pitch_bits))[0]
pitch = struct.unpack(">f", struct.pack(">I", pitch_bits))[0]
return GPSData(
latitude=latitude,
longitude=longitude,
altitude=altitude,
pitch=pitch,
timestamp=time.time()
timestamp=time.time(),
)
except Exception as e:
logging.error(f"Error parsing binary GPS with pitch: {e}")
return None
class RadarPacketParser:
def __init__(self):
self.sync_pattern = b'\xA5\xC3'
self.sync_pattern = b"\xa5\xc3"
self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000)
def parse_packet(self, data):
@@ -528,7 +555,7 @@ class RadarPacketParser:
if len(packet) < 6:
return None
sync = packet[0:2]
_sync = packet[0:2]
packet_type = packet[2]
length = packet[3]
@@ -536,11 +563,13 @@ class RadarPacketParser:
return None
payload = packet[4 : 4 + length]
crc_received = struct.unpack('<H', packet[4+length:4+length+2])[0]
crc_received = struct.unpack("<H", packet[4 + length : 4 + length + 2])[0]
crc_calculated = self.calculate_crc(packet[0 : 4 + length])
if crc_calculated != crc_received:
logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}")
logging.warning(
f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}"
)
return None
if packet_type == 0x01:
@@ -561,18 +590,18 @@ class RadarPacketParser:
return None
try:
range_value = struct.unpack('>I', payload[0:4])[0]
range_value = struct.unpack(">I", payload[0:4])[0]
elevation = payload[4] & 0x1F
azimuth = payload[5] & 0x3F
chirp_counter = payload[6] & 0x1F
return {
'type': 'range',
'range': range_value,
'elevation': elevation,
'azimuth': azimuth,
'chirp': chirp_counter,
'timestamp': time.time()
"type": "range",
"range": range_value,
"elevation": elevation,
"azimuth": azimuth,
"chirp": chirp_counter,
"timestamp": time.time(),
}
except Exception as e:
logging.error(f"Error parsing range packet: {e}")
@@ -583,20 +612,20 @@ class RadarPacketParser:
return None
try:
doppler_real = struct.unpack('>h', payload[0:2])[0]
doppler_imag = struct.unpack('>h', payload[2:4])[0]
doppler_real = struct.unpack(">h", payload[0:2])[0]
doppler_imag = struct.unpack(">h", payload[2:4])[0]
elevation = payload[4] & 0x1F
azimuth = payload[5] & 0x3F
chirp_counter = payload[6] & 0x1F
return {
'type': 'doppler',
'doppler_real': doppler_real,
'doppler_imag': doppler_imag,
'elevation': elevation,
'azimuth': azimuth,
'chirp': chirp_counter,
'timestamp': time.time()
"type": "doppler",
"doppler_real": doppler_real,
"doppler_imag": doppler_imag,
"elevation": elevation,
"azimuth": azimuth,
"chirp": chirp_counter,
"timestamp": time.time(),
}
except Exception as e:
logging.error(f"Error parsing Doppler packet: {e}")
@@ -613,17 +642,18 @@ class RadarPacketParser:
chirp_counter = payload[3] & 0x1F
return {
'type': 'detection',
'detected': detection_flag,
'elevation': elevation,
'azimuth': azimuth,
'chirp': chirp_counter,
'timestamp': time.time()
"type": "detection",
"detected": detection_flag,
"elevation": elevation,
"azimuth": azimuth,
"chirp": chirp_counter,
"timestamp": time.time(),
}
except Exception as e:
logging.error(f"Error parsing detection packet: {e}")
return None
class RadarGUI:
def __init__(self, root):
self.root = root
@@ -649,7 +679,9 @@ class RadarGUI:
# Counters
self.received_packets = 0
self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0)
self.current_gps = GPSData(
latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0
)
self.corrected_elevations = [] # Store corrected elevation values
self.create_gui()
@@ -658,17 +690,17 @@ class RadarGUI:
def create_gui(self):
"""Create the main GUI with tabs"""
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
self.notebook.pack(fill="both", expand=True, padx=10, pady=10)
self.tab_main = ttk.Frame(self.notebook)
self.tab_map = ttk.Frame(self.notebook)
self.tab_diagnostics = ttk.Frame(self.notebook)
self.tab_settings = ttk.Frame(self.notebook)
self.notebook.add(self.tab_main, text='Main View')
self.notebook.add(self.tab_map, text='Map View')
self.notebook.add(self.tab_diagnostics, text='Diagnostics')
self.notebook.add(self.tab_settings, text='Settings')
self.notebook.add(self.tab_main, text="Main View")
self.notebook.add(self.tab_map, text="Map View")
self.notebook.add(self.tab_diagnostics, text="Diagnostics")
self.notebook.add(self.tab_settings, text="Settings")
self.setup_main_tab()
self.setup_map_tab()
@@ -678,7 +710,7 @@ class RadarGUI:
"""Setup the main radar display tab"""
# Control frame
control_frame = ttk.Frame(self.tab_main)
control_frame.pack(fill='x', padx=10, pady=5)
control_frame.pack(fill="x", padx=10, pady=5)
# USB Device selection
ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5)
@@ -689,20 +721,21 @@ class RadarGUI:
self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30)
self.ftdi_combo.grid(row=0, column=3, padx=5)
ttk.Button(control_frame, text="Refresh Devices",
command=self.refresh_devices).grid(row=0, column=4, padx=5)
ttk.Button(control_frame, text="Refresh Devices", command=self.refresh_devices).grid(
row=0, column=4, padx=5
)
self.start_button = ttk.Button(control_frame, text="Start Radar",
command=self.start_radar)
self.start_button = ttk.Button(control_frame, text="Start Radar", command=self.start_radar)
self.start_button.grid(row=0, column=5, padx=5)
self.stop_button = ttk.Button(control_frame, text="Stop Radar",
command=self.stop_radar, state="disabled")
self.stop_button = ttk.Button(
control_frame, text="Stop Radar", command=self.stop_radar, state="disabled"
)
self.stop_button.grid(row=0, column=6, padx=5)
# GPS and Pitch info
self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...")
self.gps_label.grid(row=1, column=0, columnspan=4, sticky='w', padx=5, pady=2)
self.gps_label.grid(row=1, column=0, columnspan=4, sticky="w", padx=5, pady=2)
# Pitch display
self.pitch_label = ttk.Label(control_frame, text="Pitch: --.--°")
@@ -710,88 +743,95 @@ class RadarGUI:
# Status info
self.status_label = ttk.Label(control_frame, text="Status: Ready")
self.status_label.grid(row=1, column=6, sticky='e', padx=5, pady=2)
self.status_label.grid(row=1, column=6, sticky="e", padx=5, pady=2)
# Main display area
display_frame = ttk.Frame(self.tab_main)
display_frame.pack(fill='both', expand=True, padx=10, pady=5)
display_frame.pack(fill="both", expand=True, padx=10, pady=5)
# Range-Doppler Map
fig = Figure(figsize=(10, 6))
self.range_doppler_ax = fig.add_subplot(111)
self.range_doppler_plot = self.range_doppler_ax.imshow(
np.random.rand(1024, 32), aspect='auto', cmap='hot',
extent=[0, 32, 0, 1024])
self.range_doppler_ax.set_title('Range-Doppler Map (Pitch Corrected)')
self.range_doppler_ax.set_xlabel('Doppler Bin')
self.range_doppler_ax.set_ylabel('Range Bin')
np.random.rand(1024, 32), aspect="auto", cmap="hot", extent=[0, 32, 0, 1024]
)
self.range_doppler_ax.set_title("Range-Doppler Map (Pitch Corrected)")
self.range_doppler_ax.set_xlabel("Doppler Bin")
self.range_doppler_ax.set_ylabel("Range Bin")
self.canvas = FigureCanvasTkAgg(fig, display_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side='left', fill='both', expand=True)
self.canvas.get_tk_widget().pack(side="left", fill="both", expand=True)
# Targets list with corrected elevation
targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)")
targets_frame.pack(side='right', fill='y', padx=5)
targets_frame.pack(side="right", fill="y", padx=5)
self.targets_tree = ttk.Treeview(targets_frame,
columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'Corrected Elev', 'SNR'),
show='headings', height=20)
self.targets_tree.heading('ID', text='Track ID')
self.targets_tree.heading('Range', text='Range (m)')
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
self.targets_tree.heading('Azimuth', text='Azimuth')
self.targets_tree.heading('Elevation', text='Raw Elev')
self.targets_tree.heading('Corrected Elev', text='Corr Elev')
self.targets_tree.heading('SNR', text='SNR (dB)')
self.targets_tree = ttk.Treeview(
targets_frame,
columns=("ID", "Range", "Velocity", "Azimuth", "Elevation", "Corrected Elev", "SNR"),
show="headings",
height=20,
)
self.targets_tree.heading("ID", text="Track ID")
self.targets_tree.heading("Range", text="Range (m)")
self.targets_tree.heading("Velocity", text="Velocity (m/s)")
self.targets_tree.heading("Azimuth", text="Azimuth")
self.targets_tree.heading("Elevation", text="Raw Elev")
self.targets_tree.heading("Corrected Elev", text="Corr Elev")
self.targets_tree.heading("SNR", text="SNR (dB)")
self.targets_tree.column('ID', width=70)
self.targets_tree.column('Range', width=90)
self.targets_tree.column('Velocity', width=90)
self.targets_tree.column('Azimuth', width=70)
self.targets_tree.column('Elevation', width=70)
self.targets_tree.column('Corrected Elev', width=70)
self.targets_tree.column('SNR', width=70)
self.targets_tree.column("ID", width=70)
self.targets_tree.column("Range", width=90)
self.targets_tree.column("Velocity", width=90)
self.targets_tree.column("Azimuth", width=70)
self.targets_tree.column("Elevation", width=70)
self.targets_tree.column("Corrected Elev", width=70)
self.targets_tree.column("SNR", width=70)
self.targets_tree.pack(fill='both', expand=True, padx=5, pady=5)
self.targets_tree.pack(fill="both", expand=True, padx=5, pady=5)
def setup_map_tab(self):
"""Setup the map display tab"""
self.map_frame = ttk.Frame(self.tab_map)
self.map_frame.pack(fill='both', expand=True, padx=10, pady=10)
self.map_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Map placeholder
self.map_label = ttk.Label(self.map_frame, text="Map will be displayed here after GPS data is received",
font=('Arial', 12))
self.map_label = ttk.Label(
self.map_frame,
text="Map will be displayed here after GPS data is received",
font=("Arial", 12),
)
self.map_label.pack(expand=True)
def setup_settings_tab(self):
"""Setup the settings tab"""
settings_frame = ttk.Frame(self.tab_settings)
settings_frame.pack(fill='both', expand=True, padx=10, pady=10)
settings_frame.pack(fill="both", expand=True, padx=10, pady=10)
entries = [
('System Frequency (Hz):', 'system_frequency', 10e9),
('Chirp Duration (s):', 'chirp_duration', 30e-6),
('Chirps per Position:', 'chirps_per_position', 32),
('Frequency Min (Hz):', 'freq_min', 10e6),
('Frequency Max (Hz):', 'freq_max', 30e6),
('PRF1 (Hz):', 'prf1', 1000),
('PRF2 (Hz):', 'prf2', 2000),
('Max Distance (m):', 'max_distance', 50000)
("System Frequency (Hz):", "system_frequency", 10e9),
("Chirp Duration (s):", "chirp_duration", 30e-6),
("Chirps per Position:", "chirps_per_position", 32),
("Frequency Min (Hz):", "freq_min", 10e6),
("Frequency Max (Hz):", "freq_max", 30e6),
("PRF1 (Hz):", "prf1", 1000),
("PRF2 (Hz):", "prf2", 2000),
("Max Distance (m):", "max_distance", 50000),
]
self.settings_vars = {}
for i, (label, attr, default) in enumerate(entries):
ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky='w', padx=5, pady=5)
ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky="w", padx=5, pady=5)
var = tk.StringVar(value=str(default))
entry = ttk.Entry(settings_frame, textvariable=var, width=20)
entry.grid(row=i, column=1, padx=5, pady=5)
self.settings_vars[attr] = var
ttk.Button(settings_frame, text="Apply Settings",
command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10)
ttk.Button(settings_frame, text="Apply Settings", command=self.apply_settings).grid(
row=len(entries), column=0, columnspan=2, pady=10
)
def apply_pitch_correction(self, raw_elevation, pitch_angle):
"""
@@ -822,13 +862,13 @@ class RadarGUI:
"""Refresh available USB devices"""
# STM32 USB devices
stm32_devices = self.stm32_usb_interface.list_devices()
stm32_names = [dev['description'] for dev in stm32_devices]
self.stm32_usb_combo['values'] = stm32_names
stm32_names = [dev["description"] for dev in stm32_devices]
self.stm32_usb_combo["values"] = stm32_names
# FTDI devices
ftdi_devices = self.ftdi_interface.list_devices()
ftdi_names = [dev['description'] for dev in ftdi_devices]
self.ftdi_combo['values'] = ftdi_names
ftdi_names = [dev["description"] for dev in ftdi_devices]
self.ftdi_combo["values"] = ftdi_names
if stm32_names:
self.stm32_usb_combo.current(0)
@@ -859,9 +899,11 @@ class RadarGUI:
if ftdi_index != -1:
ftdi_devices = self.ftdi_interface.list_devices()
if ftdi_index < len(ftdi_devices):
device_url = ftdi_devices[ftdi_index]['url']
device_url = ftdi_devices[ftdi_index]["url"]
if not self.ftdi_interface.open_device(device_url):
logging.warning("Failed to open FTDI device, continuing without radar data")
logging.warning(
"Failed to open FTDI device, continuing without radar data"
)
else:
logging.warning("No FTDI device selected, continuing without radar data")
else:
@@ -902,14 +944,14 @@ class RadarGUI:
def apply_settings(self):
"""Step 13: Apply and send radar settings via USB"""
try:
self.settings.system_frequency = float(self.settings_vars['system_frequency'].get())
self.settings.chirp_duration = float(self.settings_vars['chirp_duration'].get())
self.settings.chirps_per_position = int(self.settings_vars['chirps_per_position'].get())
self.settings.freq_min = float(self.settings_vars['freq_min'].get())
self.settings.freq_max = float(self.settings_vars['freq_max'].get())
self.settings.prf1 = float(self.settings_vars['prf1'].get())
self.settings.prf2 = float(self.settings_vars['prf2'].get())
self.settings.max_distance = float(self.settings_vars['max_distance'].get())
self.settings.system_frequency = float(self.settings_vars["system_frequency"].get())
self.settings.chirp_duration = float(self.settings_vars["chirp_duration"].get())
self.settings.chirps_per_position = int(self.settings_vars["chirps_per_position"].get())
self.settings.freq_min = float(self.settings_vars["freq_min"].get())
self.settings.freq_max = float(self.settings_vars["freq_max"].get())
self.settings.prf1 = float(self.settings_vars["prf1"].get())
self.settings.prf2 = float(self.settings_vars["prf2"].get())
self.settings.max_distance = float(self.settings_vars["max_distance"].get())
if self.stm32_usb_interface.is_open:
self.stm32_usb_interface.send_settings(self.settings)
@@ -932,7 +974,7 @@ class RadarGUI:
def process_radar_data(self):
"""Step 39: Process incoming radar data from FTDI"""
buffer = b''
buffer = b""
while True:
if self.running and self.ftdi_interface.is_open:
try:
@@ -944,7 +986,7 @@ class RadarGUI:
packet = self.radar_packet_parser.parse_packet(buffer)
if packet:
self.process_radar_packet(packet)
packet_length = 4 + len(packet.get('payload', b'')) + 2
packet_length = 4 + len(packet.get("payload", b"")) + 2
buffer = buffer[packet_length:]
self.received_packets += 1
else:
@@ -967,7 +1009,13 @@ class RadarGUI:
gps_data = self.usb_packet_parser.parse_gps_data(data)
if gps_data:
self.gps_data_queue.put(gps_data)
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°")
logging.info(
"GPS Data received via USB: "
f"Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m, "
f"Pitch {gps_data.pitch:.1f}°"
)
except Exception as e:
logging.error(f"Error processing GPS data via USB: {e}")
time.sleep(0.1)
@@ -975,49 +1023,61 @@ class RadarGUI:
def process_radar_packet(self, packet):
"""Step 40: Process radar data and apply pitch correction"""
try:
if packet['type'] == 'range':
range_meters = packet['range'] * 0.1
if packet["type"] == "range":
range_meters = packet["range"] * 0.1
# Apply pitch correction to elevation
raw_elevation = packet['elevation']
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch)
raw_elevation = packet["elevation"]
corrected_elevation = self.apply_pitch_correction(
raw_elevation, self.current_gps.pitch
)
# Store correction for display
self.corrected_elevations.append({
'raw': raw_elevation,
'corrected': corrected_elevation,
'pitch': self.current_gps.pitch,
'timestamp': packet['timestamp']
})
self.corrected_elevations.append(
{
"raw": raw_elevation,
"corrected": corrected_elevation,
"pitch": self.current_gps.pitch,
"timestamp": packet["timestamp"],
}
)
# Keep only recent corrections
if len(self.corrected_elevations) > 100:
self.corrected_elevations = self.corrected_elevations[-100:]
target = RadarTarget(
id=packet['chirp'],
id=packet["chirp"],
range=range_meters,
velocity=0,
azimuth=packet['azimuth'],
azimuth=packet["azimuth"],
elevation=corrected_elevation, # Use corrected elevation
snr=20.0,
timestamp=packet['timestamp']
timestamp=packet["timestamp"],
)
self.update_range_doppler_map(target)
elif packet['type'] == 'doppler':
elif packet["type"] == "doppler":
lambda_wavelength = 3e8 / self.settings.system_frequency
velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2)
velocity = (packet["doppler_real"] / 32767.0) * (
self.settings.prf1 * lambda_wavelength / 2
)
self.update_target_velocity(packet, velocity)
elif packet['type'] == 'detection':
if packet['detected']:
elif packet["type"] == "detection":
if packet["detected"]:
# Apply pitch correction to detection elevation
raw_elevation = packet['elevation']
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch)
raw_elevation = packet["elevation"]
corrected_elevation = self.apply_pitch_correction(
raw_elevation, self.current_gps.pitch
)
logging.info(f"CFAR Detection: Raw Elev {raw_elevation}°, Corrected Elev {corrected_elevation:.1f}°, Pitch {self.current_gps.pitch:.1f}°")
logging.info(
f"CFAR Detection: Raw Elev {raw_elevation}°, "
f"Corrected Elev {corrected_elevation:.1f}°, "
f"Pitch {self.current_gps.pitch:.1f}°"
)
except Exception as e:
logging.error(f"Error processing radar packet: {e}")
@@ -1037,9 +1097,11 @@ class RadarGUI:
def update_target_velocity(self, packet, velocity):
"""Update target velocity information"""
for target in self.radar_processor.detected_targets:
if (target.azimuth == packet['azimuth'] and
target.elevation == packet['elevation'] and
target.id == packet['chirp']):
if (
target.azimuth == packet["azimuth"]
and target.elevation == packet["elevation"]
and target.id == packet["chirp"]
):
target.velocity = velocity
break
@@ -1052,7 +1114,12 @@ class RadarGUI:
# Update GPS label
self.gps_label.config(
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
text=(
f"GPS: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m"
)
)
# Update pitch label with color coding
pitch_text = f"Pitch: {gps_data.pitch:+.1f}°"
@@ -1060,11 +1127,11 @@ class RadarGUI:
# Color code based on pitch magnitude
if abs(gps_data.pitch) > 10:
self.pitch_label.config(foreground='red') # High pitch warning
self.pitch_label.config(foreground="red") # High pitch warning
elif abs(gps_data.pitch) > 5:
self.pitch_label.config(foreground='orange') # Medium pitch
self.pitch_label.config(foreground="orange") # Medium pitch
else:
self.pitch_label.config(foreground='green') # Normal pitch
self.pitch_label.config(foreground="green") # Normal pitch
# Update map
self.update_map_display(gps_data)
@@ -1081,19 +1148,23 @@ class RadarGUI:
# Find the corresponding raw elevation if available
raw_elevation = "N/A"
for correction in self.corrected_elevations[-20:]:
if abs(correction['corrected'] - target.elevation) < 0.1: # Fuzzy match
if abs(correction["corrected"] - target.elevation) < 0.1: # Fuzzy match
raw_elevation = f"{correction['raw']}"
break
self.targets_tree.insert('', 'end', values=(
self.targets_tree.insert(
"",
"end",
values=(
target.track_id,
f"{target.range:.1f}",
f"{target.velocity:.1f}",
target.azimuth,
raw_elevation, # Show raw elevation
f"{target.elevation:.1f}", # Show corrected elevation
f"{target.snr:.1f}"
))
f"{target.snr:.1f}",
),
)
def update_gui(self):
"""Step 40: Update all GUI displays"""
@@ -1101,10 +1172,14 @@ class RadarGUI:
# Update status with pitch information
if self.running:
self.status_label.config(
text=f"Status: Running - Packets: {self.received_packets} - Pitch: {self.current_gps.pitch:+.1f}°")
text=(
f"Status: Running - Packets: {self.received_packets} - "
f"Pitch: {self.current_gps.pitch:+.1f}°"
)
)
# Update range-Doppler map
if hasattr(self, 'range_doppler_plot'):
if hasattr(self, "range_doppler_plot"):
display_data = np.log10(self.radar_processor.range_doppler_map + 1)
self.range_doppler_plot.set_array(display_data)
self.canvas.draw_idle()
@@ -1123,24 +1198,28 @@ class RadarGUI:
def update_map_display(self, gps_data):
"""Step 18: Update map display with current GPS position"""
try:
self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
self.map_label.config(
text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
f"Altitude: {gps_data.altitude:.1f}m\n"
f"Pitch: {gps_data.pitch:+.1f}°\n"
f"Coverage: 50km radius\n"
f"Map centered on GPS coordinates")
f"Map centered on GPS coordinates"
)
except Exception as e:
logging.error(f"Error updating map display: {e}")
def main():
"""Main application entry point"""
try:
root = tk.Tk()
app = RadarGUI(root)
_app = RadarGUI(root)
root.mainloop()
except Exception as e:
logging.error(f"Application error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+178 -141
View File
@@ -2,13 +2,9 @@ import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.patches as patches
from scipy import signal
from scipy.fft import fft, fftshift
from scipy.signal import butter, filtfilt
import logging
from dataclasses import dataclass
from typing import List, Dict, Tuple
@@ -17,7 +13,8 @@ import queue
import time
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
@dataclass
class RadarTarget:
@@ -29,6 +26,7 @@ class RadarTarget:
chirp_type: str
timestamp: float
class SignalProcessor:
def __init__(self):
self.range_resolution = 1.0 # meters
@@ -42,7 +40,7 @@ class SignalProcessor:
"""
# Window function for FFT
window = np.hanning(len(iq_data))
windowed_data = (iq_data['I_value'].values + 1j * iq_data['Q_value'].values) * window
windowed_data = (iq_data["I_value"].values + 1j * iq_data["Q_value"].values) * window
# Perform FFT
doppler_fft = fft(windowed_data)
@@ -59,7 +57,7 @@ class SignalProcessor:
return velocity_axis, np.abs(doppler_fft)
def mti_filter(self, iq_data: np.ndarray, filter_type: str = 'single_canceler') -> np.ndarray:
def mti_filter(self, iq_data: np.ndarray, filter_type: str = "single_canceler") -> np.ndarray:
"""
Moving Target Indicator filter
Removes stationary clutter with better shape handling
@@ -71,7 +69,7 @@ class SignalProcessor:
# Ensure we're working with complex data
complex_data = iq_data.astype(complex)
if filter_type == 'single_canceler':
if filter_type == "single_canceler":
# Single delay line canceler
if len(complex_data) < 2:
return np.array([], dtype=complex)
@@ -80,13 +78,15 @@ class SignalProcessor:
filtered[i - 1] = complex_data[i] - complex_data[i - 1]
return filtered
elif filter_type == 'double_canceler':
elif filter_type == "double_canceler":
# Double delay line canceler
if len(complex_data) < 3:
return np.array([], dtype=complex)
filtered = np.zeros(len(complex_data) - 2, dtype=complex)
for i in range(2, len(complex_data)):
filtered[i-2] = complex_data[i] - 2*complex_data[i-1] + complex_data[i-2]
filtered[i - 2] = (
complex_data[i] - 2 * complex_data[i - 1] + complex_data[i - 2]
)
return filtered
else:
@@ -95,9 +95,13 @@ class SignalProcessor:
logging.error(f"MTI filter error: {e}")
return np.array([], dtype=complex)
def cfar_detection(self, range_profile: np.ndarray, guard_cells: int = 2,
training_cells: int = 10, threshold_factor: float = 3.0) -> List[Tuple[int, float]]:
def cfar_detection(
self,
range_profile: np.ndarray,
guard_cells: int = 2,
training_cells: int = 10,
threshold_factor: float = 3.0,
) -> List[Tuple[int, float]]:
detections = []
N = len(range_profile)
@@ -132,11 +136,15 @@ class SignalProcessor:
threshold = noise_floor * threshold_factor
if range_profile[i_int] > threshold:
detections.append((i_int, float(range_profile[i_int]))) # Ensure float magnitude
detections.append(
(i_int, float(range_profile[i_int]))
) # Ensure float magnitude
return detections
def range_fft(self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6) -> Tuple[np.ndarray, np.ndarray]:
def range_fft(
self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6
) -> Tuple[np.ndarray, np.ndarray]:
"""
Perform range FFT on IQ data
Returns range profile
@@ -155,24 +163,25 @@ class SignalProcessor:
return range_axis, np.abs(range_fft)
def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = 'LONG') -> Dict:
def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = "LONG") -> Dict:
try:
# Filter data by chirp type
chirp_data = df[df['chirp_type'] == chirp_type]
chirp_data = df[df["chirp_type"] == chirp_type]
if len(chirp_data) == 0:
return {}
# Group by chirp number
chirp_numbers = chirp_data['chirp_number'].unique()
chirp_numbers = chirp_data["chirp_number"].unique()
num_chirps = len(chirp_numbers)
if num_chirps == 0:
return {}
# Get samples per chirp and ensure consistency
samples_per_chirp_list = [len(chirp_data[chirp_data['chirp_number'] == num])
for num in chirp_numbers]
samples_per_chirp_list = [
len(chirp_data[chirp_data["chirp_number"] == num]) for num in chirp_numbers
]
# Use minimum samples to ensure consistent shape
samples_per_chirp = min(samples_per_chirp_list)
@@ -181,12 +190,12 @@ class SignalProcessor:
range_doppler_matrix = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
for i, chirp_num in enumerate(chirp_numbers):
chirp_samples = chirp_data[chirp_data['chirp_number'] == chirp_num]
chirp_samples = chirp_data[chirp_data["chirp_number"] == chirp_num]
# Take only the first samples_per_chirp samples to ensure consistent shape
chirp_samples = chirp_samples.head(samples_per_chirp)
# Create complex IQ data
iq_data = chirp_samples['I_value'].values + 1j * chirp_samples['Q_value'].values
iq_data = chirp_samples["I_value"].values + 1j * chirp_samples["Q_value"].values
# Ensure the shape matches
if len(iq_data) == samples_per_chirp:
@@ -215,16 +224,17 @@ class SignalProcessor:
doppler_fft_result[i, :] = fft(mti_filtered[i, :])
return {
'range_doppler_matrix': np.abs(doppler_fft_result),
'chirp_type': chirp_type,
'num_chirps': num_chirps,
'samples_per_chirp': samples_per_chirp
"range_doppler_matrix": np.abs(doppler_fft_result),
"chirp_type": chirp_type,
"num_chirps": num_chirps,
"samples_per_chirp": samples_per_chirp,
}
except Exception as e:
logging.error(f"Error in process_chirp_sequence: {e}")
return {}
class RadarGUI:
def __init__(self, root):
self.root = root
@@ -254,67 +264,73 @@ class RadarGUI:
"""Create the main GUI layout"""
# Main frame
main_frame = ttk.Frame(self.root)
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
# Control panel
control_frame = ttk.LabelFrame(main_frame, text="File Controls")
control_frame.pack(fill='x', pady=5)
control_frame.pack(fill="x", pady=5)
# File selection
ttk.Button(control_frame, text="Load CSV File",
command=self.load_csv_file).pack(side='left', padx=5, pady=5)
ttk.Button(control_frame, text="Load CSV File", command=self.load_csv_file).pack(
side="left", padx=5, pady=5
)
self.file_label = ttk.Label(control_frame, text="No file loaded")
self.file_label.pack(side='left', padx=10, pady=5)
self.file_label.pack(side="left", padx=10, pady=5)
# Processing controls
ttk.Button(control_frame, text="Process Data",
command=self.process_data).pack(side='left', padx=5, pady=5)
ttk.Button(control_frame, text="Process Data", command=self.process_data).pack(
side="left", padx=5, pady=5
)
ttk.Button(control_frame, text="Run CFAR Detection",
command=self.run_cfar_detection).pack(side='left', padx=5, pady=5)
ttk.Button(control_frame, text="Run CFAR Detection", command=self.run_cfar_detection).pack(
side="left", padx=5, pady=5
)
# Status
self.status_label = ttk.Label(control_frame, text="Status: Ready")
self.status_label.pack(side='right', padx=10, pady=5)
self.status_label.pack(side="right", padx=10, pady=5)
# Display area
display_frame = ttk.Frame(main_frame)
display_frame.pack(fill='both', expand=True, pady=5)
display_frame.pack(fill="both", expand=True, pady=5)
# Create matplotlib figures
self.create_plots(display_frame)
# Targets list
targets_frame = ttk.LabelFrame(main_frame, text="Detected Targets")
targets_frame.pack(fill='x', pady=5)
targets_frame.pack(fill="x", pady=5)
self.targets_tree = ttk.Treeview(targets_frame,
columns=('Range', 'Velocity', 'Azimuth', 'Elevation', 'SNR', 'Chirp Type'),
show='headings', height=8)
self.targets_tree = ttk.Treeview(
targets_frame,
columns=("Range", "Velocity", "Azimuth", "Elevation", "SNR", "Chirp Type"),
show="headings",
height=8,
)
self.targets_tree.heading('Range', text='Range (m)')
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
self.targets_tree.heading('Azimuth', text='Azimuth (°)')
self.targets_tree.heading('Elevation', text='Elevation (°)')
self.targets_tree.heading('SNR', text='SNR (dB)')
self.targets_tree.heading('Chirp Type', text='Chirp Type')
self.targets_tree.heading("Range", text="Range (m)")
self.targets_tree.heading("Velocity", text="Velocity (m/s)")
self.targets_tree.heading("Azimuth", text="Azimuth (°)")
self.targets_tree.heading("Elevation", text="Elevation (°)")
self.targets_tree.heading("SNR", text="SNR (dB)")
self.targets_tree.heading("Chirp Type", text="Chirp Type")
self.targets_tree.column('Range', width=100)
self.targets_tree.column('Velocity', width=100)
self.targets_tree.column('Azimuth', width=80)
self.targets_tree.column('Elevation', width=80)
self.targets_tree.column('SNR', width=80)
self.targets_tree.column('Chirp Type', width=100)
self.targets_tree.column("Range", width=100)
self.targets_tree.column("Velocity", width=100)
self.targets_tree.column("Azimuth", width=80)
self.targets_tree.column("Elevation", width=80)
self.targets_tree.column("SNR", width=80)
self.targets_tree.column("Chirp Type", width=100)
self.targets_tree.pack(fill='x', padx=5, pady=5)
self.targets_tree.pack(fill="x", padx=5, pady=5)
def create_plots(self, parent):
"""Create matplotlib plots"""
# Create figure with subplots
self.fig = Figure(figsize=(12, 8))
self.canvas = FigureCanvasTkAgg(self.fig, parent)
self.canvas.get_tk_widget().pack(fill='both', expand=True)
self.canvas.get_tk_widget().pack(fill="both", expand=True)
# Create subplots
self.ax1 = self.fig.add_subplot(221) # Range profile
@@ -323,23 +339,23 @@ class RadarGUI:
self.ax4 = self.fig.add_subplot(224) # MTI filtered data
# Set titles
self.ax1.set_title('Range Profile')
self.ax1.set_xlabel('Range (m)')
self.ax1.set_ylabel('Magnitude')
self.ax1.set_title("Range Profile")
self.ax1.set_xlabel("Range (m)")
self.ax1.set_ylabel("Magnitude")
self.ax1.grid(True)
self.ax2.set_title('Doppler Spectrum')
self.ax2.set_xlabel('Velocity (m/s)')
self.ax2.set_ylabel('Magnitude')
self.ax2.set_title("Doppler Spectrum")
self.ax2.set_xlabel("Velocity (m/s)")
self.ax2.set_ylabel("Magnitude")
self.ax2.grid(True)
self.ax3.set_title('Range-Doppler Map')
self.ax3.set_xlabel('Doppler Bin')
self.ax3.set_ylabel('Range Bin')
self.ax3.set_title("Range-Doppler Map")
self.ax3.set_xlabel("Doppler Bin")
self.ax3.set_ylabel("Range Bin")
self.ax4.set_title('MTI Filtered Data')
self.ax4.set_xlabel('Sample')
self.ax4.set_ylabel('Magnitude')
self.ax4.set_title("MTI Filtered Data")
self.ax4.set_xlabel("Sample")
self.ax4.set_ylabel("Magnitude")
self.ax4.grid(True)
self.fig.tight_layout()
@@ -347,21 +363,20 @@ class RadarGUI:
def load_csv_file(self):
"""Load CSV file generated by testbench"""
filename = filedialog.askopenfilename(
title="Select CSV file",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
title="Select CSV file", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")]
)
# Add magnitude and phase calculations after loading CSV
if self.df is not None:
# Calculate magnitude from I/Q values
self.df['magnitude'] = np.sqrt(self.df['I_value']**2 + self.df['Q_value']**2)
self.df["magnitude"] = np.sqrt(self.df["I_value"] ** 2 + self.df["Q_value"] ** 2)
# Calculate phase from I/Q values
self.df['phase_rad'] = np.arctan2(self.df['Q_value'], self.df['I_value'])
self.df["phase_rad"] = np.arctan2(self.df["Q_value"], self.df["I_value"])
# If you used magnitude_squared in CSV, calculate actual magnitude
if 'magnitude_squared' in self.df.columns:
self.df['magnitude'] = np.sqrt(self.df['magnitude_squared'])
if "magnitude_squared" in self.df.columns:
self.df["magnitude"] = np.sqrt(self.df["magnitude_squared"])
if filename:
try:
self.status_label.config(text="Status: Loading CSV file...")
@@ -395,7 +410,7 @@ class RadarGUI:
self.status_label.config(text="Status: Processing data...")
# Add to processing queue
self.processing_queue.put(('process', self.df))
self.processing_queue.put(("process", self.df))
def run_cfar_detection(self):
"""Run CFAR detection on processed data"""
@@ -404,7 +419,7 @@ class RadarGUI:
return
self.status_label.config(text="Status: Running CFAR detection...")
self.processing_queue.put(('cfar', self.df))
self.processing_queue.put(("cfar", self.df))
def background_processing(self):
@@ -412,9 +427,9 @@ class RadarGUI:
try:
task_type, data = self.processing_queue.get(timeout=1.0)
if task_type == 'process':
if task_type == "process":
self._process_data_background(data)
elif task_type == 'cfar':
elif task_type == "cfar":
self._run_cfar_background(data)
else:
logging.warning(f"Unknown task type: {task_type}")
@@ -426,23 +441,23 @@ class RadarGUI:
except Exception as e:
logging.error(f"Background processing error: {e}")
# Update GUI to show error state
self.root.after(0, lambda: self.status_label.config(
text=f"Status: Processing error - {e}"
))
self.root.after(
0,
lambda: self.status_label.config(
text=f"Status: Processing error - {e}" # noqa: F821
),
)
def _process_data_background(self, df):
try:
# Process long chirps
long_chirp_data = self.processor.process_chirp_sequence(df, 'LONG')
long_chirp_data = self.processor.process_chirp_sequence(df, "LONG")
# Process short chirps
short_chirp_data = self.processor.process_chirp_sequence(df, 'SHORT')
short_chirp_data = self.processor.process_chirp_sequence(df, "SHORT")
# Store results
self.processed_data = {
'long': long_chirp_data,
'short': short_chirp_data
}
self.processed_data = {"long": long_chirp_data, "short": short_chirp_data}
# Update GUI in main thread
self.root.after(0, self._update_plots_after_processing)
@@ -450,20 +465,23 @@ class RadarGUI:
except Exception as e:
logging.error(f"Processing error: {e}")
error_msg = str(e)
self.root.after(0, lambda msg=error_msg: self.status_label.config(
self.root.after(
0,
lambda msg=error_msg: self.status_label.config(
text=f"Status: Processing error - {msg}"
))
),
)
def _run_cfar_background(self, df):
try:
# Get first chirp for CFAR demonstration
first_chirp = df[df['chirp_number'] == df['chirp_number'].min()]
first_chirp = df[df["chirp_number"] == df["chirp_number"].min()]
if len(first_chirp) == 0:
return
# Create IQ data - FIXED TYPO: first_chirp not first_chip
iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
iq_data = first_chirp["I_value"].values + 1j * first_chirp["Q_value"].values
# Perform range FFT
range_axis, range_profile = self.processor.range_fft(iq_data)
@@ -480,20 +498,23 @@ class RadarGUI:
azimuth=0, # From actual data
elevation=0, # From actual data
snr=20 * np.log10(magnitude + 1e-9), # Convert to dB
chirp_type='LONG',
timestamp=time.time()
chirp_type="LONG",
timestamp=time.time(),
)
self.detected_targets.append(target)
# Update GUI in main thread
self.root.after(0, lambda: self._update_cfar_results(range_axis, range_profile, detections))
self.root.after(
0, lambda: self._update_cfar_results(range_axis, range_profile, detections)
)
except Exception as e:
logging.error(f"CFAR detection error: {e}")
error_msg = str(e)
self.root.after(0, lambda msg=error_msg: self.status_label.config(
text=f"Status: CFAR error - {msg}"
))
self.root.after(
0,
lambda msg=error_msg: self.status_label.config(text=f"Status: CFAR error - {msg}"),
)
def _update_plots_after_processing(self):
try:
@@ -504,68 +525,69 @@ class RadarGUI:
# Plot 1: Range profile from first chirp
if self.df is not None and len(self.df) > 0:
try:
first_chirp_num = self.df['chirp_number'].min()
first_chirp = self.df[self.df['chirp_number'] == first_chirp_num]
first_chirp_num = self.df["chirp_number"].min()
first_chirp = self.df[self.df["chirp_number"] == first_chirp_num]
if len(first_chirp) > 0:
iq_data = first_chirp['I_value'].values + 1j * first_chirp['Q_value'].values
iq_data = first_chirp["I_value"].values + 1j * first_chirp["Q_value"].values
range_axis, range_profile = self.processor.range_fft(iq_data)
if len(range_axis) > 0 and len(range_profile) > 0:
self.ax1.plot(range_axis, range_profile, 'b-')
self.ax1.set_title('Range Profile - First Chirp')
self.ax1.set_xlabel('Range (m)')
self.ax1.set_ylabel('Magnitude')
self.ax1.plot(range_axis, range_profile, "b-")
self.ax1.set_title("Range Profile - First Chirp")
self.ax1.set_xlabel("Range (m)")
self.ax1.set_ylabel("Magnitude")
self.ax1.grid(True)
except Exception as e:
logging.warning(f"Range profile plot error: {e}")
self.ax1.set_title('Range Profile - Error')
self.ax1.set_title("Range Profile - Error")
# Plot 2: Doppler spectrum
if self.df is not None and len(self.df) > 0:
try:
sample_data = self.df.head(1024)
if len(sample_data) > 10:
iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
iq_data = sample_data["I_value"].values + 1j * sample_data["Q_value"].values
velocity_axis, doppler_spectrum = self.processor.doppler_fft(iq_data)
if len(velocity_axis) > 0 and len(doppler_spectrum) > 0:
self.ax2.plot(velocity_axis, doppler_spectrum, 'g-')
self.ax2.set_title('Doppler Spectrum')
self.ax2.set_xlabel('Velocity (m/s)')
self.ax2.set_ylabel('Magnitude')
self.ax2.plot(velocity_axis, doppler_spectrum, "g-")
self.ax2.set_title("Doppler Spectrum")
self.ax2.set_xlabel("Velocity (m/s)")
self.ax2.set_ylabel("Magnitude")
self.ax2.grid(True)
except Exception as e:
logging.warning(f"Doppler spectrum plot error: {e}")
self.ax2.set_title('Doppler Spectrum - Error')
self.ax2.set_title("Doppler Spectrum - Error")
# Plot 3: Range-Doppler map
if (self.processed_data.get('long') and
'range_doppler_matrix' in self.processed_data['long'] and
self.processed_data['long']['range_doppler_matrix'].size > 0):
if (
self.processed_data.get("long")
and "range_doppler_matrix" in self.processed_data["long"]
and self.processed_data["long"]["range_doppler_matrix"].size > 0
):
try:
rd_matrix = self.processed_data['long']['range_doppler_matrix']
rd_matrix = self.processed_data["long"]["range_doppler_matrix"]
# Use integer indices for extent
extent = [0, int(rd_matrix.shape[1]), 0, int(rd_matrix.shape[0])]
im = self.ax3.imshow(10 * np.log10(rd_matrix + 1e-9),
aspect='auto', cmap='hot',
extent=extent)
self.ax3.set_title('Range-Doppler Map (Long Chirps)')
self.ax3.set_xlabel('Doppler Bin')
self.ax3.set_ylabel('Range Bin')
self.fig.colorbar(im, ax=self.ax3, label='dB')
im = self.ax3.imshow(
10 * np.log10(rd_matrix + 1e-9), aspect="auto", cmap="hot", extent=extent
)
self.ax3.set_title("Range-Doppler Map (Long Chirps)")
self.ax3.set_xlabel("Doppler Bin")
self.ax3.set_ylabel("Range Bin")
self.fig.colorbar(im, ax=self.ax3, label="dB")
except Exception as e:
logging.warning(f"Range-Doppler map plot error: {e}")
self.ax3.set_title('Range-Doppler Map - Error')
self.ax3.set_title("Range-Doppler Map - Error")
# Plot 4: MTI filtered data
if self.df is not None and len(self.df) > 0:
try:
sample_data = self.df.head(100)
if len(sample_data) > 10:
iq_data = sample_data['I_value'].values + 1j * sample_data['Q_value'].values
iq_data = sample_data["I_value"].values + 1j * sample_data["Q_value"].values
# Original data
original_mag = np.abs(iq_data)
@@ -580,16 +602,18 @@ class RadarGUI:
x_original = np.arange(len(original_mag))
x_mti = np.arange(len(mti_mag))
self.ax4.plot(x_original, original_mag, 'b-', label='Original', alpha=0.7)
self.ax4.plot(x_mti, mti_mag, 'r-', label='MTI Filtered', alpha=0.7)
self.ax4.set_title('MTI Filter Comparison')
self.ax4.set_xlabel('Sample Index')
self.ax4.set_ylabel('Magnitude')
self.ax4.plot(
x_original, original_mag, "b-", label="Original", alpha=0.7
)
self.ax4.plot(x_mti, mti_mag, "r-", label="MTI Filtered", alpha=0.7)
self.ax4.set_title("MTI Filter Comparison")
self.ax4.set_xlabel("Sample Index")
self.ax4.set_ylabel("Magnitude")
self.ax4.legend()
self.ax4.grid(True)
except Exception as e:
logging.warning(f"MTI filter plot error: {e}")
self.ax4.set_title('MTI Filter - Error')
self.ax4.set_title("MTI Filter - Error")
# Adjust layout and draw
self.fig.tight_layout()
@@ -607,7 +631,7 @@ class RadarGUI:
self.ax1.clear()
# Plot range profile
self.ax1.plot(range_axis, range_profile, 'b-', label='Range Profile')
self.ax1.plot(range_axis, range_profile, "b-", label="Range Profile")
# Plot detections - ensure we use integer indices
if detections and len(range_axis) > 0:
@@ -622,12 +646,17 @@ class RadarGUI:
detection_mags.append(mag)
if detection_ranges: # Only plot if we have valid detections
self.ax1.plot(detection_ranges, detection_mags, 'ro',
markersize=8, label='CFAR Detections')
self.ax1.plot(
detection_ranges,
detection_mags,
"ro",
markersize=8,
label="CFAR Detections",
)
self.ax1.set_title('Range Profile with CFAR Detections')
self.ax1.set_xlabel('Range (m)')
self.ax1.set_ylabel('Magnitude')
self.ax1.set_title("Range Profile with CFAR Detections")
self.ax1.set_xlabel("Range (m)")
self.ax1.set_ylabel("Magnitude")
self.ax1.legend()
self.ax1.grid(True)
@@ -635,7 +664,9 @@ class RadarGUI:
self.update_targets_list()
self.canvas.draw()
self.status_label.config(text=f"Status: CFAR complete - {len(detections)} targets detected")
self.status_label.config(
text=f"Status: CFAR complete - {len(detections)} targets detected"
)
except Exception as e:
logging.error(f"CFAR results update error: {e}")
@@ -650,29 +681,35 @@ class RadarGUI:
# Add detected targets
for i, target in enumerate(self.detected_targets):
self.targets_tree.insert('', 'end', values=(
self.targets_tree.insert(
"",
"end",
values=(
f"{target.range:.1f}",
f"{target.velocity:.1f}",
f"{target.azimuth}",
f"{target.elevation}",
f"{target.snr:.1f}",
target.chirp_type
))
target.chirp_type,
),
)
def update_gui(self):
"""Periodic GUI update"""
# You can add any periodic updates here
self.root.after(100, self.update_gui)
def main():
"""Main application entry point"""
try:
root = tk.Tk()
app = RadarGUI(root)
_app = RadarGUI(root)
root.mainloop()
except Exception as e:
logging.error(f"Application error: {e}")
messagebox.showerror("Fatal Error", f"Application failed to start: {e}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+129 -29
View File
@@ -194,7 +194,9 @@ class MapGenerator:
var targetMarker = new google.maps.Marker({{
position: {{lat: target.lat, lng: target.lng}},
map: map,
title: `Target: ${{target.range:.1f}}m, ${{target.velocity:.1f}}m/s`,
title: (
`Target: ${{target.range:.1f}}m, ${{target.velocity:.1f}}m/s`
),
icon: {{
path: google.maps.SymbolPath.CIRCLE,
scale: 6,
@@ -276,8 +278,14 @@ class FT601Interface:
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
for dev in found_devices:
try:
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "FT601 USB3.0"
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
product = (
usb.util.get_string(dev, dev.iProduct)
if dev.iProduct else "FT601 USB3.0"
)
serial = (
usb.util.get_string(dev, dev.iSerialNumber)
if dev.iSerialNumber else "Unknown"
)
# Create FTDI URL for the device
url = f"ftdi://{vid:04x}:{pid:04x}:{serial}/1"
@@ -541,8 +549,14 @@ class STM32USBInterface:
found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid)
for dev in found_devices:
try:
product = usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC"
serial = usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else "Unknown"
product = (
usb.util.get_string(dev, dev.iProduct)
if dev.iProduct else "STM32 CDC"
)
serial = (
usb.util.get_string(dev, dev.iSerialNumber)
if dev.iSerialNumber else "Unknown"
)
devices.append({
'description': f"{product} ({serial})",
'vendor_id': vid,
@@ -561,7 +575,11 @@ class STM32USBInterface:
except Exception as e:
logging.error(f"Error listing USB devices: {e}")
# Return mock devices for testing
return [{'description': 'STM32 Virtual COM Port', 'vendor_id': 0x0483, 'product_id': 0x5740}]
return [{
'description': 'STM32 Virtual COM Port',
'vendor_id': 0x0483,
'product_id': 0x5740,
}]
def open_device(self, device_info):
"""Open STM32 USB CDC device"""
@@ -586,12 +604,18 @@ class STM32USBInterface:
# Find bulk endpoints (CDC data interface)
self.ep_out = usb.util.find_descriptor(
intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress)
== usb.util.ENDPOINT_OUT
)
)
self.ep_in = usb.util.find_descriptor(
intf,
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
custom_match=lambda e: (
usb.util.endpoint_direction(e.bEndpointAddress)
== usb.util.ENDPOINT_IN
)
)
if self.ep_out is None or self.ep_in is None:
@@ -826,7 +850,13 @@ class USBPacketParser:
lon = float(parts[1])
alt = float(parts[2])
pitch = float(parts[3]) # Pitch angle in degrees
return GPSData(latitude=lat, longitude=lon, altitude=alt, pitch=pitch, timestamp=time.time())
return GPSData(
latitude=lat,
longitude=lon,
altitude=alt,
pitch=pitch,
timestamp=time.time(),
)
# Try binary format (30 bytes with pitch)
if len(data) >= 30 and data[0:4] == b'GPSB':
@@ -918,7 +948,10 @@ class RadarPacketParser:
crc_calculated = self.calculate_crc(packet[0:4+length])
if crc_calculated != crc_received:
logging.warning(f"CRC mismatch: got {crc_received:04X}, calculated {crc_calculated:04X}")
logging.warning(
f"CRC mismatch: got {crc_received:04X}, "
f"calculated {crc_calculated:04X}"
)
return None
if packet_type == 0x01:
@@ -1037,7 +1070,13 @@ class RadarGUI:
# Counters
self.received_packets = 0
self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0)
self.current_gps = GPSData(
latitude=41.9028,
longitude=12.4964,
altitude=0,
pitch=0.0,
timestamp=0,
)
self.corrected_elevations = []
self.map_file_path = None
self.google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY"
@@ -1219,9 +1258,20 @@ class RadarGUI:
targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)")
targets_frame.pack(side='right', fill='y', padx=5)
self.targets_tree = ttk.Treeview(targets_frame,
columns=('ID', 'Range', 'Velocity', 'Azimuth', 'Elevation', 'Corrected Elev', 'SNR'),
show='headings', height=20)
self.targets_tree = ttk.Treeview(
targets_frame,
columns=(
'ID',
'Range',
'Velocity',
'Azimuth',
'Elevation',
'Corrected Elev',
'SNR',
),
show='headings',
height=20,
)
self.targets_tree.heading('ID', text='Track ID')
self.targets_tree.heading('Range', text='Range (m)')
self.targets_tree.heading('Velocity', text='Velocity (m/s)')
@@ -1239,7 +1289,11 @@ class RadarGUI:
self.targets_tree.column('SNR', width=70)
# Add scrollbar to targets tree
tree_scroll = ttk.Scrollbar(targets_frame, orient="vertical", command=self.targets_tree.yview)
tree_scroll = ttk.Scrollbar(
targets_frame,
orient="vertical",
command=self.targets_tree.yview,
)
self.targets_tree.configure(yscrollcommand=tree_scroll.set)
self.targets_tree.pack(side='left', fill='both', expand=True, padx=5, pady=5)
tree_scroll.pack(side='right', fill='y', padx=(0, 5), pady=5)
@@ -1288,7 +1342,9 @@ class RadarGUI:
if not self.ft601_interface.open_device_direct(ft601_devices[ft601_index]):
device_url = ft601_devices[ft601_index]['url']
if not self.ft601_interface.open_device(device_url):
logging.warning("Failed to open FT601 device, continuing without radar data")
logging.warning(
"Failed to open FT601 device, continuing without radar data"
)
messagebox.showwarning("Warning", "Failed to open FT601 device")
else:
# Configure burst mode if enabled
@@ -1382,7 +1438,11 @@ class RadarGUI:
gps_data = self.usb_packet_parser.parse_gps_data(data)
if gps_data:
self.gps_data_queue.put(gps_data)
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°")
logging.info(
f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°"
)
except Exception as e:
logging.error(f"Error processing GPS data via USB: {e}")
time.sleep(0.1)
@@ -1395,7 +1455,10 @@ class RadarGUI:
# Apply pitch correction to elevation
raw_elevation = packet['elevation']
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch)
corrected_elevation = self.apply_pitch_correction(
raw_elevation,
self.current_gps.pitch,
)
# Store correction for display
self.corrected_elevations.append({
@@ -1423,16 +1486,25 @@ class RadarGUI:
elif packet['type'] == 'doppler':
lambda_wavelength = 3e8 / self.settings.system_frequency
velocity = (packet['doppler_real'] / 32767.0) * (self.settings.prf1 * lambda_wavelength / 2)
velocity = (packet['doppler_real'] / 32767.0) * (
self.settings.prf1 * lambda_wavelength / 2
)
self.update_target_velocity(packet, velocity)
elif packet['type'] == 'detection':
if packet['detected']:
# Apply pitch correction to detection elevation
raw_elevation = packet['elevation']
corrected_elevation = self.apply_pitch_correction(raw_elevation, self.current_gps.pitch)
corrected_elevation = self.apply_pitch_correction(
raw_elevation,
self.current_gps.pitch,
)
logging.info(f"CFAR Detection: Raw Elev {raw_elevation}°, Corrected Elev {corrected_elevation:.1f}°, Pitch {self.current_gps.pitch:.1f}°")
logging.info(
f"CFAR Detection: Raw Elev {raw_elevation}°, "
f"Corrected Elev {corrected_elevation:.1f}°, "
f"Pitch {self.current_gps.pitch:.1f}°"
)
except Exception as e:
logging.error(f"Error processing radar packet: {e}")
@@ -1480,7 +1552,11 @@ class RadarGUI:
info_frame = ttk.Frame(map_frame)
info_frame.pack(fill='x', pady=5)
self.map_info_label = ttk.Label(info_frame, text="No GPS data received yet", font=('Arial', 10))
self.map_info_label = ttk.Label(
info_frame,
text="No GPS data received yet",
font=('Arial', 10),
)
self.map_info_label.pack()
def open_map_in_browser(self):
@@ -1488,7 +1564,10 @@ class RadarGUI:
if self.map_file_path and os.path.exists(self.map_file_path):
webbrowser.open('file://' + os.path.abspath(self.map_file_path))
else:
messagebox.showwarning("Warning", "No map file available. Generate map first by receiving GPS data.")
messagebox.showwarning(
"Warning",
"No map file available. Generate map first by receiving GPS data.",
)
def refresh_map(self):
"""Refresh the map with current data"""
@@ -1502,7 +1581,12 @@ class RadarGUI:
try:
# Create temporary HTML file
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
with tempfile.NamedTemporaryFile(
mode='w',
suffix='.html',
delete=False,
encoding='utf-8',
) as f:
map_html = self.map_generator.generate_map(
self.current_gps,
self.radar_processor.detected_targets,
@@ -1533,7 +1617,12 @@ class RadarGUI:
# Update GPS label
self.gps_label.config(
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
text=(
f"GPS: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m"
)
)
# Update pitch label with color coding
pitch_text = f"Pitch: {gps_data.pitch:+.1f}°"
@@ -1581,8 +1670,11 @@ class RadarGUI:
entry.grid(row=i, column=1, padx=5, pady=5)
self.settings_vars[attr] = var
ttk.Button(settings_frame, text="Apply Settings",
command=self.apply_settings).grid(row=len(entries), column=0, columnspan=2, pady=10)
ttk.Button(
settings_frame,
text="Apply Settings",
command=self.apply_settings,
).grid(row=len(entries), column=0, columnspan=2, pady=10)
def apply_settings(self):
"""Step 13: Apply and send radar settings via USB"""
@@ -1678,7 +1770,11 @@ class RadarGUI:
gps_data = self.usb_packet_parser.parse_gps_data(data)
if gps_data:
self.gps_data_queue.put(gps_data)
logging.info(f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°")
logging.info(
f"GPS Data received via USB: Lat {gps_data.latitude:.6f}, "
f"Lon {gps_data.longitude:.6f}, "
f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°"
)
except Exception as e:
logging.error(f"Error processing GPS data via USB: {e}")
time.sleep(0.1)
@@ -1689,7 +1785,11 @@ class RadarGUI:
# Update status with pitch information
if self.running:
self.status_label.config(
text=f"Status: Running - Packets: {self.received_packets} - Pitch: {self.current_gps.pitch:+.1f}°")
text=(
f"Status: Running - Packets: {self.received_packets} - "
f"Pitch: {self.current_gps.pitch:+.1f}°"
)
)
# Update range-Doppler map
if hasattr(self, 'range_doppler_plot'):
+12 -4
View File
@@ -621,7 +621,9 @@ class RadarDemoGUI:
self.update_rate.grid(row=0, column=1, padx=10, pady=5)
self.update_rate_value = ttk.Label(frame, text="20")
self.update_rate_value.grid(row=0, column=2, sticky='w')
self.update_rate.configure(command=lambda v: self.update_rate_value.config(text=f"{float(v):.0f}"))
self.update_rate.configure(
command=lambda v: self.update_rate_value.config(text=f"{float(v):.0f}")
)
# Color map
ttk.Label(frame, text="Color Map:").grid(row=1, column=0, sticky='w', pady=5)
@@ -658,7 +660,9 @@ class RadarDemoGUI:
self.noise_floor.grid(row=0, column=1, padx=10, pady=5)
self.noise_value = ttk.Label(frame, text="10")
self.noise_value.grid(row=0, column=2, sticky='w')
self.noise_floor.configure(command=lambda v: self.noise_value.config(text=f"{float(v):.1f}"))
self.noise_floor.configure(
command=lambda v: self.noise_value.config(text=f"{float(v):.1f}")
)
# Clutter level
ttk.Label(frame, text="Clutter Level:").grid(row=1, column=0, sticky='w', pady=5)
@@ -668,7 +672,9 @@ class RadarDemoGUI:
self.clutter_level.grid(row=1, column=1, padx=10, pady=5)
self.clutter_value = ttk.Label(frame, text="5")
self.clutter_value.grid(row=1, column=2, sticky='w')
self.clutter_level.configure(command=lambda v: self.clutter_value.config(text=f"{float(v):.1f}"))
self.clutter_level.configure(
command=lambda v: self.clutter_value.config(text=f"{float(v):.1f}")
)
# Number of targets
ttk.Label(frame, text="Number of Targets:").grid(row=2, column=0, sticky='w', pady=5)
@@ -678,7 +684,9 @@ class RadarDemoGUI:
self.num_targets.grid(row=2, column=1, padx=10, pady=5)
self.targets_value = ttk.Label(frame, text="5")
self.targets_value.grid(row=2, column=2, sticky='w')
self.num_targets.configure(command=lambda v: self.targets_value.config(text=f"{float(v):.0f}"))
self.num_targets.configure(
command=lambda v: self.targets_value.config(text=f"{float(v):.0f}")
)
# Reset button
ttk.Button(frame, text="Reset Simulation",
+4 -1
View File
@@ -321,7 +321,10 @@ class TestDataRecorder(unittest.TestCase):
os.rmdir(self.tmpdir)
@unittest.skipUnless(
(lambda: (__import__("importlib.util") and __import__("importlib").util.find_spec("h5py") is not None))(),
(lambda: (
__import__("importlib.util")
and __import__("importlib").util.find_spec("h5py") is not None
))(),
"h5py not installed"
)
def test_record_and_stop(self):
+8 -3
View File
@@ -755,7 +755,9 @@ class RadarDashboard(QMainWindow):
self._det_thresh_spin.setValue(self._processing_config.detection_threshold_db)
self._det_thresh_spin.setSuffix(" dB")
self._det_thresh_spin.setSingleStep(1.0)
self._det_thresh_spin.setToolTip("SNR threshold above noise floor (used when CFAR is disabled)")
self._det_thresh_spin.setToolTip(
"SNR threshold above noise floor (used when CFAR is disabled)"
)
p_layout.addWidget(self._det_thresh_spin, row, 1)
row += 1
@@ -906,8 +908,11 @@ class RadarDashboard(QMainWindow):
if idx2 >= 0 and idx2 < len(self._ft2232hq_devices):
url = self._ft2232hq_devices[idx2]["url"]
if not self._ft2232hq.open_device(url):
QMessageBox.warning(self, "Warning",
"Failed to open FT2232HQ device. Radar data may not be available.")
QMessageBox.warning(
self,
"Warning",
"Failed to open FT2232HQ device. Radar data may not be available.",
)
# Send start flag + settings
if not self._stm32.send_start_flag():
+73 -18
View File
@@ -305,7 +305,10 @@ function initMap() {{
function setTileServer(id) {{
var cfg = tileServers[id]; if(!cfg) return;
if(currentTileLayer) map.removeLayer(currentTileLayer);
currentTileLayer = L.tileLayer(cfg.url, {{ attribution:cfg.attribution, maxZoom:cfg.maxZoom }}).addTo(map);
currentTileLayer = L.tileLayer(
cfg.url,
{{ attribution:cfg.attribution, maxZoom:cfg.maxZoom }}
).addTo(map);
}}
function updateRadarPopup() {{
@@ -313,9 +316,18 @@ function updateRadarPopup() {{
var ll = radarMarker.getLatLng();
radarMarker.bindPopup(
'<div class="popup-title">Radar System</div>'+
'<div class="popup-row"><span class="popup-label">Lat:</span><span class="popup-value">'+ll.lat.toFixed(6)+'</span></div>'+
'<div class="popup-row"><span class="popup-label">Lon:</span><span class="popup-value">'+ll.lng.toFixed(6)+'</span></div>'+
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value status-approaching">Active</span></div>'
(
'<div class="popup-row"><span class="popup-label">Lat:</span>'+
'<span class="popup-value">'+ll.lat.toFixed(6)+'</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Lon:</span>'+
'<span class="popup-value">'+ll.lng.toFixed(6)+'</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Status:</span>'+
'<span class="popup-value status-approaching">Active</span></div>'
)
);
}}
@@ -325,10 +337,22 @@ function addLegend() {{
var d = L.DomUtil.create('div','legend');
d.innerHTML =
'<div class="legend-title">Target Legend</div>'+
'<div class="legend-item"><div class="legend-color" style="background:#F44336"></div>Approaching</div>'+
'<div class="legend-item"><div class="legend-color" style="background:#2196F3"></div>Receding</div>'+
'<div class="legend-item"><div class="legend-color" style="background:#9E9E9E"></div>Stationary</div>'+
'<div class="legend-item"><div class="legend-color" style="background:#FF5252"></div>Radar</div>';
(
'<div class="legend-item"><div class="legend-color" '+
'style="background:#F44336"></div>Approaching</div>'
)+
(
'<div class="legend-item"><div class="legend-color" '+
'style="background:#2196F3"></div>Receding</div>'
)+
(
'<div class="legend-item"><div class="legend-color" '+
'style="background:#9E9E9E"></div>Stationary</div>'
)+
(
'<div class="legend-item"><div class="legend-color" '+
'style="background:#FF5252"></div>Radar</div>'
);
return d;
}};
legend.addTo(map);
@@ -365,7 +389,12 @@ function updateTargets(targetsJson) {{
}}
}} else {{
var marker = L.marker([lat,lon], {{ icon:makeIcon(color,sz) }}).addTo(map);
marker.on('click', (function(id){{ return function(){{ if(bridge) bridge.onMarkerClick(id); }}; }})(t.id));
marker.on(
'click',
(function(id){{
return function(){{ if(bridge) bridge.onMarkerClick(id); }};
}})(t.id)
);
targetMarkers[t.id] = marker;
if(showTrails) {{
targetTrails[t.id] = L.polyline(targetTrailHistory[t.id], {{
@@ -389,24 +418,50 @@ function makeIcon(color,sz) {{
return L.divIcon({{
className:'target-icon',
html:'<div style="background-color:'+color+';width:'+sz+'px;height:'+sz+'px;'+
'border-radius:50%;border:2px solid white;box-shadow:0 2px 6px rgba(0,0,0,0.4);"></div>',
(
'border-radius:50%;border:2px solid white;'+
'box-shadow:0 2px 6px rgba(0,0,0,0.4);'
)+'</div>',
iconSize:[sz,sz], iconAnchor:[sz/2,sz/2]
}});
}}
function updateTargetPopup(t) {{
if(!targetMarkers[t.id]) return;
var sc = t.velocity>1?'status-approaching':(t.velocity<-1?'status-receding':'status-stationary');
var sc = t.velocity>1
? 'status-approaching'
: (t.velocity<-1 ? 'status-receding' : 'status-stationary');
var st = t.velocity>1?'Approaching':(t.velocity<-1?'Receding':'Stationary');
targetMarkers[t.id].bindPopup(
'<div class="popup-title">Target #'+t.id+'</div>'+
'<div class="popup-row"><span class="popup-label">Range:</span><span class="popup-value">'+t.range.toFixed(1)+' m</span></div>'+
'<div class="popup-row"><span class="popup-label">Velocity:</span><span class="popup-value">'+t.velocity.toFixed(1)+' m/s</span></div>'+
'<div class="popup-row"><span class="popup-label">Azimuth:</span><span class="popup-value">'+t.azimuth.toFixed(1)+'&deg;</span></div>'+
'<div class="popup-row"><span class="popup-label">Elevation:</span><span class="popup-value">'+t.elevation.toFixed(1)+'&deg;</span></div>'+
'<div class="popup-row"><span class="popup-label">SNR:</span><span class="popup-value">'+t.snr.toFixed(1)+' dB</span></div>'+
'<div class="popup-row"><span class="popup-label">Track:</span><span class="popup-value">'+t.track_id+'</span></div>'+
'<div class="popup-row"><span class="popup-label">Status:</span><span class="popup-value '+sc+'">'+st+'</span></div>'
(
'<div class="popup-row"><span class="popup-label">Range:</span>'+
'<span class="popup-value">'+t.range.toFixed(1)+' m</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Velocity:</span>'+
'<span class="popup-value">'+t.velocity.toFixed(1)+' m/s</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Azimuth:</span>'+
'<span class="popup-value">'+t.azimuth.toFixed(1)+'&deg;</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Elevation:</span>'+
'<span class="popup-value">'+t.elevation.toFixed(1)+'&deg;</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">SNR:</span>'+
'<span class="popup-value">'+t.snr.toFixed(1)+' dB</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Track:</span>'+
'<span class="popup-value">'+t.track_id+'</span></div>'
)+
(
'<div class="popup-row"><span class="popup-label">Status:</span>'+
'<span class="popup-value '+sc+'">'+st+'</span></div>'
)
);
}}
+27
View File
@@ -0,0 +1,27 @@
[project]
name = "aeris-10-radar"
version = "1.0.0"
description = "AERIS-10 FMCW Radar Platform — host software & FPGA cosim tools"
requires-python = ">=3.12"
# Runtime dependencies intentionally empty — GUI deps are optional and
# listed in requirements_*.txt files for local installs.
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.5",
"pytest>=8",
"numpy>=1.26",
"h5py>=3.10",
]
# ---------------------------------------------------------------------------
# Ruff configuration
# ---------------------------------------------------------------------------
[tool.ruff]
target-version = "py312"
line-length = 100
[tool.ruff.lint]
select = ["E", "F"]