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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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})")
|
||||
|
||||
|
||||
@@ -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) + '°</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) + '°</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], {{
|
||||
|
||||
@@ -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"
|
||||
f"Altitude: {gps_data.altitude:.1f}m\n"
|
||||
f"Coverage: 50km radius\n"
|
||||
f"Map centered on GPS coordinates")
|
||||
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"
|
||||
)
|
||||
)
|
||||
|
||||
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}")
|
||||
|
||||
+268
-203
@@ -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):
|
||||
@@ -134,17 +148,21 @@ class STM32USBInterface:
|
||||
|
||||
# Get CDC endpoints
|
||||
cfg = self.device.get_active_configuration()
|
||||
intf = cfg[(0,0)]
|
||||
intf = cfg[(0, 0)]
|
||||
|
||||
# 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:
|
||||
@@ -201,10 +219,10 @@ class STM32USBInterface:
|
||||
# USB CDC typically uses 64-byte packets
|
||||
packet_size = 64
|
||||
for i in range(0, len(data), packet_size):
|
||||
chunk = data[i:i + packet_size]
|
||||
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,19 +536,21 @@ class RadarPacketParser:
|
||||
if len(packet) < 6:
|
||||
return None
|
||||
|
||||
sync = packet[0:2]
|
||||
_sync = packet[0:2]
|
||||
packet_type = packet[2]
|
||||
length = packet[3]
|
||||
|
||||
if len(packet) < (4 + length + 2):
|
||||
return None
|
||||
|
||||
payload = packet[4:4+length]
|
||||
crc_received = struct.unpack('<H', packet[4+length:4+length+2])[0]
|
||||
payload = packet[4 : 4 + length]
|
||||
crc_received = struct.unpack("<H", packet[4 + length : 4 + length + 2])[0]
|
||||
|
||||
crc_calculated = self.calculate_crc(packet[0:4+length])
|
||||
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=(
|
||||
target.track_id,
|
||||
f"{target.range:.1f}",
|
||||
f"{target.velocity:.1f}",
|
||||
target.azimuth,
|
||||
target.elevation,
|
||||
f"{target.snr:.1f}"
|
||||
))
|
||||
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}",
|
||||
),
|
||||
)
|
||||
|
||||
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"
|
||||
f"Altitude: {gps_data.altitude:.1f}m\n"
|
||||
f"Coverage: 50km radius\n"
|
||||
f"Map centered on GPS coordinates")
|
||||
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"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
+301
-222
@@ -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):
|
||||
@@ -135,17 +150,21 @@ class STM32USBInterface:
|
||||
|
||||
# Get CDC endpoints
|
||||
cfg = self.device.get_active_configuration()
|
||||
intf = cfg[(0,0)]
|
||||
intf = cfg[(0, 0)]
|
||||
|
||||
# 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:
|
||||
@@ -202,10 +221,10 @@ class STM32USBInterface:
|
||||
# USB CDC typically uses 64-byte packets
|
||||
packet_size = 64
|
||||
for i in range(0, len(data), packet_size):
|
||||
chunk = data[i:i + packet_size]
|
||||
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,19 +555,21 @@ class RadarPacketParser:
|
||||
if len(packet) < 6:
|
||||
return None
|
||||
|
||||
sync = packet[0:2]
|
||||
_sync = packet[0:2]
|
||||
packet_type = packet[2]
|
||||
length = packet[3]
|
||||
|
||||
if len(packet) < (4 + length + 2):
|
||||
return None
|
||||
|
||||
payload = packet[4:4+length]
|
||||
crc_received = struct.unpack('<H', packet[4+length:4+length+2])[0]
|
||||
payload = packet[4 : 4 + length]
|
||||
crc_received = struct.unpack("<H", packet[4 + length : 4 + length + 2])[0]
|
||||
|
||||
crc_calculated = self.calculate_crc(packet[0:4+length])
|
||||
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=(
|
||||
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}"
|
||||
))
|
||||
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}",
|
||||
),
|
||||
)
|
||||
|
||||
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"
|
||||
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")
|
||||
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"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
+339
-253
File diff suppressed because it is too large
Load Diff
+190
-153
@@ -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)
|
||||
@@ -50,7 +48,7 @@ class SignalProcessor:
|
||||
|
||||
# Frequency axis
|
||||
N = len(iq_data)
|
||||
freq_axis = np.linspace(-fs/2, fs/2, N)
|
||||
freq_axis = np.linspace(-fs / 2, fs / 2, N)
|
||||
|
||||
# Convert to velocity (assuming radar frequency = 10 GHz)
|
||||
radar_freq = 10e9
|
||||
@@ -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,22 +69,24 @@ 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)
|
||||
filtered = np.zeros(len(complex_data) - 1, dtype=complex)
|
||||
for i in range(1, len(complex_data)):
|
||||
filtered[i-1] = complex_data[i] - complex_data[i-1]
|
||||
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:
|
||||
@@ -204,7 +213,7 @@ class SignalProcessor:
|
||||
# Handle shape mismatch by padding or truncating
|
||||
if len(filtered) < num_chirps:
|
||||
padded = np.zeros(num_chirps, dtype=complex)
|
||||
padded[:len(filtered)] = filtered
|
||||
padded[: len(filtered)] = filtered
|
||||
mti_filtered[i, :] = padded
|
||||
else:
|
||||
mti_filtered[i, :] = filtered[:num_chirps]
|
||||
@@ -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
|
||||
# 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(
|
||||
text=f"Status: Processing error - {msg}"
|
||||
))
|
||||
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)
|
||||
@@ -477,23 +495,26 @@ class RadarGUI:
|
||||
target = RadarTarget(
|
||||
range=range_axis[range_bin],
|
||||
velocity=0, # Would need Doppler processing for velocity
|
||||
azimuth=0, # From actual data
|
||||
elevation=0, # From actual data
|
||||
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=(
|
||||
f"{target.range:.1f}",
|
||||
f"{target.velocity:.1f}",
|
||||
f"{target.azimuth}",
|
||||
f"{target.elevation}",
|
||||
f"{target.snr:.1f}",
|
||||
target.chirp_type
|
||||
))
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
+435
-343
File diff suppressed because it is too large
Load Diff
+330
-250
File diff suppressed because it is too large
Load Diff
+129
-29
@@ -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'):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)+'°</span></div>'+
|
||||
'<div class="popup-row"><span class="popup-label">Elevation:</span><span class="popup-value">'+t.elevation.toFixed(1)+'°</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)+'°</span></div>'
|
||||
)+
|
||||
(
|
||||
'<div class="popup-row"><span class="popup-label">Elevation:</span>'+
|
||||
'<span class="popup-value">'+t.elevation.toFixed(1)+'°</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>'
|
||||
)
|
||||
);
|
||||
}}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
Reference in New Issue
Block a user