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

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

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

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