fix: full-repo ruff lint cleanup and CI migration to uv
Resolve all 374 ruff errors across 36 Python files (E501, E702, E722, E741, F821, F841, invalid-syntax) bringing `ruff check .` to zero errors repo-wide with line-length=100. Rewrite CI workflow to use uv for dependency management, whole-repo `ruff check .`, py_compile syntax gate, and merged python-tests job. Add pyproject.toml with ruff config and uv dependency groups. CI structure proposed by hcm444.
This commit is contained in:
@@ -8,100 +8,77 @@ on:
|
|||||||
|
|
||||||
jobs:
|
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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,12 +15,20 @@ def generate_radar_csv(filename="pulse_compression_output.csv"):
|
|||||||
timestamp_ns = 0
|
timestamp_ns = 0
|
||||||
|
|
||||||
# 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
|
||||||
noise_std = 5
|
noise_std = 5
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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})")
|
||||||
|
|
||||||
|
|||||||
@@ -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) + '°</span></div>' +
|
target.azimuth.toFixed(1) + '°</span></div>' +
|
||||||
'<div class="popup-row"><span class="popup-label">Elevation:</span><span class="popup-value">' +
|
(
|
||||||
|
'<div class="popup-row"><span class="popup-label">Elevation:</span>' +
|
||||||
|
'<span class="popup-value">'
|
||||||
|
) +
|
||||||
target.elevation.toFixed(1) + '°</span></div>' +
|
target.elevation.toFixed(1) + '°</span></div>' +
|
||||||
'<div class="popup-row"><span class="popup-label">SNR:</span><span class="popup-value">' +
|
(
|
||||||
|
'<div class="popup-row"><span class="popup-label">SNR:</span>' +
|
||||||
|
'<span class="popup-value">'
|
||||||
|
) +
|
||||||
target.snr.toFixed(1) + ' dB</span></div>' +
|
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], {{
|
||||||
|
|||||||
@@ -1,37 +1,52 @@
|
|||||||
|
import logging
|
||||||
def update_gps_display(self):
|
import queue
|
||||||
"""Step 18: Update GPS display and center map"""
|
import tkinter as tk
|
||||||
try:
|
from tkinter import messagebox
|
||||||
while not self.gps_data_queue.empty():
|
|
||||||
gps_data = self.gps_data_queue.get_nowait()
|
|
||||||
self.current_gps = gps_data
|
class RadarGUI:
|
||||||
|
def update_gps_display(self):
|
||||||
# Update GPS label
|
"""Step 18: Update GPS display and center map"""
|
||||||
self.gps_label.config(
|
try:
|
||||||
text=f"GPS: Lat {gps_data.latitude:.6f}, Lon {gps_data.longitude:.6f}, Alt {gps_data.altitude:.1f}m")
|
while not self.gps_data_queue.empty():
|
||||||
|
gps_data = self.gps_data_queue.get_nowait()
|
||||||
# Update map
|
self.current_gps = gps_data
|
||||||
self.update_map_display(gps_data)
|
|
||||||
|
# Update GPS label
|
||||||
except queue.Empty:
|
self.gps_label.config(
|
||||||
pass
|
text=(
|
||||||
|
f"GPS: Lat {gps_data.latitude:.6f}, "
|
||||||
def update_map_display(self, gps_data):
|
f"Lon {gps_data.longitude:.6f}, "
|
||||||
"""Step 18: Update map display with current GPS position"""
|
f"Alt {gps_data.altitude:.1f}m"
|
||||||
try:
|
)
|
||||||
self.map_label.config(text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
|
)
|
||||||
f"Altitude: {gps_data.altitude:.1f}m\n"
|
|
||||||
f"Coverage: 50km radius\n"
|
# Update map
|
||||||
f"Map centered on GPS coordinates")
|
self.update_map_display(gps_data)
|
||||||
|
|
||||||
except Exception as e:
|
except queue.Empty:
|
||||||
logging.error(f"Error updating map display: {e}")
|
pass
|
||||||
|
|
||||||
|
def update_map_display(self, gps_data):
|
||||||
|
"""Step 18: Update map display with current GPS position"""
|
||||||
|
try:
|
||||||
|
self.map_label.config(
|
||||||
|
text=(
|
||||||
|
f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n"
|
||||||
|
f"Altitude: {gps_data.altitude:.1f}m\n"
|
||||||
|
f"Coverage: 50km radius\n"
|
||||||
|
f"Map centered on GPS coordinates"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as 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}")
|
||||||
|
|||||||
+439
-374
File diff suppressed because it is too large
Load Diff
+490
-411
File diff suppressed because it is too large
Load Diff
+546
-460
File diff suppressed because it is too large
Load Diff
+312
-275
@@ -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,12 +26,13 @@ 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
|
||||||
self.velocity_resolution = 0.1 # m/s
|
self.velocity_resolution = 0.1 # m/s
|
||||||
self.cfar_threshold = 15.0 # dB
|
self.cfar_threshold = 15.0 # dB
|
||||||
|
|
||||||
def doppler_fft(self, iq_data: np.ndarray, fs: float = 100e6) -> Tuple[np.ndarray, np.ndarray]:
|
def doppler_fft(self, iq_data: np.ndarray, fs: float = 100e6) -> Tuple[np.ndarray, np.ndarray]:
|
||||||
"""
|
"""
|
||||||
Perform Doppler FFT on IQ data
|
Perform Doppler FFT on IQ data
|
||||||
@@ -42,101 +40,111 @@ 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)
|
||||||
doppler_fft = fftshift(doppler_fft)
|
doppler_fft = fftshift(doppler_fft)
|
||||||
|
|
||||||
# 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
|
||||||
wavelength = 3e8 / radar_freq
|
wavelength = 3e8 / radar_freq
|
||||||
velocity_axis = freq_axis * wavelength / 2
|
velocity_axis = freq_axis * wavelength / 2
|
||||||
|
|
||||||
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
|
||||||
"""
|
"""
|
||||||
if iq_data is None or len(iq_data) < 2:
|
if iq_data is None or len(iq_data) < 2:
|
||||||
return np.array([], dtype=complex)
|
return np.array([], dtype=complex)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 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:
|
||||||
return complex_data
|
return complex_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
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)
|
||||||
|
|
||||||
# Ensure guard_cells and training_cells are integers
|
# Ensure guard_cells and training_cells are integers
|
||||||
guard_cells = int(guard_cells)
|
guard_cells = int(guard_cells)
|
||||||
training_cells = int(training_cells)
|
training_cells = int(training_cells)
|
||||||
|
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
# Convert to integer indices
|
# Convert to integer indices
|
||||||
i_int = int(i)
|
i_int = int(i)
|
||||||
if i_int < guard_cells + training_cells or i_int >= N - guard_cells - training_cells:
|
if i_int < guard_cells + training_cells or i_int >= N - guard_cells - training_cells:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Leading window - ensure integer indices
|
# Leading window - ensure integer indices
|
||||||
lead_start = i_int - guard_cells - training_cells
|
lead_start = i_int - guard_cells - training_cells
|
||||||
lead_end = i_int - guard_cells
|
lead_end = i_int - guard_cells
|
||||||
lead_cells = range_profile[lead_start:lead_end]
|
lead_cells = range_profile[lead_start:lead_end]
|
||||||
|
|
||||||
# Lagging window - ensure integer indices
|
# Lagging window - ensure integer indices
|
||||||
lag_start = i_int + guard_cells + 1
|
lag_start = i_int + guard_cells + 1
|
||||||
lag_end = i_int + guard_cells + training_cells + 1
|
lag_end = i_int + guard_cells + training_cells + 1
|
||||||
lag_cells = range_profile[lag_start:lag_end]
|
lag_cells = range_profile[lag_start:lag_end]
|
||||||
|
|
||||||
# Combine training cells
|
# Combine training cells
|
||||||
training_cells_combined = np.concatenate([lead_cells, lag_cells])
|
training_cells_combined = np.concatenate([lead_cells, lag_cells])
|
||||||
|
|
||||||
# Calculate noise floor (mean of training cells)
|
# Calculate noise floor (mean of training cells)
|
||||||
if len(training_cells_combined) > 0:
|
if len(training_cells_combined) > 0:
|
||||||
noise_floor = np.mean(training_cells_combined)
|
noise_floor = np.mean(training_cells_combined)
|
||||||
|
|
||||||
# Apply threshold
|
# Apply threshold
|
||||||
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
|
||||||
@@ -144,54 +152,55 @@ class SignalProcessor:
|
|||||||
# Window function
|
# Window function
|
||||||
window = np.hanning(len(iq_data))
|
window = np.hanning(len(iq_data))
|
||||||
windowed_data = np.abs(iq_data) * window
|
windowed_data = np.abs(iq_data) * window
|
||||||
|
|
||||||
# Perform FFT
|
# Perform FFT
|
||||||
range_fft = fft(windowed_data)
|
range_fft = fft(windowed_data)
|
||||||
|
|
||||||
# Range calculation
|
# Range calculation
|
||||||
N = len(iq_data)
|
N = len(iq_data)
|
||||||
range_max = (3e8 * N) / (2 * bw)
|
range_max = (3e8 * N) / (2 * bw)
|
||||||
range_axis = np.linspace(0, range_max, N)
|
range_axis = np.linspace(0, range_max, N)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
# Create range-Doppler matrix with consistent shape
|
# Create range-Doppler matrix with consistent shape
|
||||||
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:
|
||||||
range_doppler_matrix[:, i] = iq_data
|
range_doppler_matrix[:, i] = iq_data
|
||||||
|
|
||||||
# Apply MTI filter along slow-time (chirp-to-chirp)
|
# Apply MTI filter along slow-time (chirp-to-chirp)
|
||||||
mti_filtered = np.zeros_like(range_doppler_matrix)
|
mti_filtered = np.zeros_like(range_doppler_matrix)
|
||||||
for i in range(samples_per_chirp):
|
for i in range(samples_per_chirp):
|
||||||
@@ -204,178 +213,184 @@ 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]
|
||||||
|
|
||||||
# Perform Doppler FFT along slow-time dimension
|
# Perform Doppler FFT along slow-time dimension
|
||||||
doppler_fft_result = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
|
doppler_fft_result = np.zeros((samples_per_chirp, num_chirps), dtype=complex)
|
||||||
for i in range(samples_per_chirp):
|
for i in range(samples_per_chirp):
|
||||||
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
|
||||||
self.root.title("Radar Signal Processor - CSV Analysis")
|
self.root.title("Radar Signal Processor - CSV Analysis")
|
||||||
self.root.geometry("1400x900")
|
self.root.geometry("1400x900")
|
||||||
|
|
||||||
# Initialize processor
|
# Initialize processor
|
||||||
self.processor = SignalProcessor()
|
self.processor = SignalProcessor()
|
||||||
|
|
||||||
# Data storage
|
# Data storage
|
||||||
self.df = None
|
self.df = None
|
||||||
self.processed_data = {}
|
self.processed_data = {}
|
||||||
self.detected_targets = []
|
self.detected_targets = []
|
||||||
|
|
||||||
# Create GUI
|
# Create GUI
|
||||||
self.create_gui()
|
self.create_gui()
|
||||||
|
|
||||||
# Start background processing
|
# Start background processing
|
||||||
self.processing_queue = queue.Queue()
|
self.processing_queue = queue.Queue()
|
||||||
self.processing_thread = threading.Thread(target=self.background_processing, daemon=True)
|
self.processing_thread = threading.Thread(target=self.background_processing, daemon=True)
|
||||||
self.processing_thread.start()
|
self.processing_thread.start()
|
||||||
|
|
||||||
# Update GUI periodically
|
# Update GUI periodically
|
||||||
self.root.after(100, self.update_gui)
|
self.root.after(100, self.update_gui)
|
||||||
|
|
||||||
def create_gui(self):
|
def create_gui(self):
|
||||||
"""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",
|
|
||||||
command=self.run_cfar_detection).pack(side='left', padx=5, pady=5)
|
ttk.Button(control_frame, text="Run CFAR Detection", command=self.run_cfar_detection).pack(
|
||||||
|
side="left", padx=5, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
# Status
|
# 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",
|
||||||
self.targets_tree.heading('Range', text='Range (m)')
|
height=8,
|
||||||
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("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('Chirp Type', text='Chirp Type')
|
self.targets_tree.heading("Azimuth", text="Azimuth (°)")
|
||||||
|
self.targets_tree.heading("Elevation", text="Elevation (°)")
|
||||||
self.targets_tree.column('Range', width=100)
|
self.targets_tree.heading("SNR", text="SNR (dB)")
|
||||||
self.targets_tree.column('Velocity', width=100)
|
self.targets_tree.heading("Chirp Type", text="Chirp Type")
|
||||||
self.targets_tree.column('Azimuth', width=80)
|
|
||||||
self.targets_tree.column('Elevation', width=80)
|
self.targets_tree.column("Range", width=100)
|
||||||
self.targets_tree.column('SNR', width=80)
|
self.targets_tree.column("Velocity", width=100)
|
||||||
self.targets_tree.column('Chirp Type', width=100)
|
self.targets_tree.column("Azimuth", width=80)
|
||||||
|
self.targets_tree.column("Elevation", width=80)
|
||||||
self.targets_tree.pack(fill='x', padx=5, pady=5)
|
self.targets_tree.column("SNR", width=80)
|
||||||
|
self.targets_tree.column("Chirp Type", width=100)
|
||||||
|
|
||||||
|
self.targets_tree.pack(fill="x", padx=5, pady=5)
|
||||||
|
|
||||||
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
|
||||||
self.ax2 = self.fig.add_subplot(222) # Doppler spectrum
|
self.ax2 = self.fig.add_subplot(222) # Doppler spectrum
|
||||||
self.ax3 = self.fig.add_subplot(223) # Range-Doppler map
|
self.ax3 = self.fig.add_subplot(223) # Range-Doppler map
|
||||||
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()
|
||||||
|
|
||||||
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...")
|
||||||
self.df = pd.read_csv(filename)
|
self.df = pd.read_csv(filename)
|
||||||
self.file_label.config(text=f"Loaded: {filename.split('/')[-1]}")
|
self.file_label.config(text=f"Loaded: {filename.split('/')[-1]}")
|
||||||
self.status_label.config(text=f"Status: Loaded {len(self.df)} samples")
|
self.status_label.config(text=f"Status: Loaded {len(self.df)} samples")
|
||||||
|
|
||||||
# Show basic info
|
# Show basic info
|
||||||
self.show_file_info()
|
self.show_file_info()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Error", f"Failed to load CSV file: {e}")
|
messagebox.showerror("Error", f"Failed to load CSV file: {e}")
|
||||||
self.status_label.config(text="Status: Error loading file")
|
self.status_label.config(text="Status: Error loading file")
|
||||||
|
|
||||||
def show_file_info(self):
|
def show_file_info(self):
|
||||||
"""Display basic information about loaded data"""
|
"""Display basic information about loaded data"""
|
||||||
if self.df is not None:
|
if self.df is not None:
|
||||||
@@ -383,296 +398,318 @@ class RadarGUI:
|
|||||||
info_text += f"Chirps: {self.df['chirp_number'].nunique()} | "
|
info_text += f"Chirps: {self.df['chirp_number'].nunique()} | "
|
||||||
info_text += f"Long: {len(self.df[self.df['chirp_type'] == 'LONG'])} | "
|
info_text += f"Long: {len(self.df[self.df['chirp_type'] == 'LONG'])} | "
|
||||||
info_text += f"Short: {len(self.df[self.df['chirp_type'] == 'SHORT'])}"
|
info_text += f"Short: {len(self.df[self.df['chirp_type'] == 'SHORT'])}"
|
||||||
|
|
||||||
self.file_label.config(text=info_text)
|
self.file_label.config(text=info_text)
|
||||||
|
|
||||||
def process_data(self):
|
def process_data(self):
|
||||||
"""Process loaded CSV data"""
|
"""Process loaded CSV data"""
|
||||||
if self.df is None:
|
if self.df is None:
|
||||||
messagebox.showwarning("Warning", "Please load a CSV file first")
|
messagebox.showwarning("Warning", "Please load a CSV file first")
|
||||||
return
|
return
|
||||||
|
|
||||||
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"""
|
||||||
if self.df is None:
|
if self.df is None:
|
||||||
messagebox.showwarning("Warning", "Please load and process data first")
|
messagebox.showwarning("Warning", "Please load and process data first")
|
||||||
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):
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
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}")
|
||||||
|
|
||||||
self.processing_queue.task_done()
|
self.processing_queue.task_done()
|
||||||
|
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
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)
|
||||||
|
|
||||||
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(
|
||||||
text=f"Status: Processing error - {msg}"
|
0,
|
||||||
))
|
lambda msg=error_msg: self.status_label.config(
|
||||||
|
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)
|
||||||
|
|
||||||
# Run CFAR detection
|
# Run CFAR detection
|
||||||
detections = self.processor.cfar_detection(range_profile)
|
detections = self.processor.cfar_detection(range_profile)
|
||||||
|
|
||||||
# Convert to target objects
|
# Convert to target objects
|
||||||
self.detected_targets = []
|
self.detected_targets = []
|
||||||
for range_bin, magnitude in detections:
|
for range_bin, magnitude in detections:
|
||||||
target = RadarTarget(
|
target = RadarTarget(
|
||||||
range=range_axis[range_bin],
|
range=range_axis[range_bin],
|
||||||
velocity=0, # Would need Doppler processing for velocity
|
velocity=0, # Would need Doppler processing for velocity
|
||||||
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:
|
||||||
# Clear all plots
|
# Clear all plots
|
||||||
for ax in [self.ax1, self.ax2, self.ax3, self.ax4]:
|
for ax in [self.ax1, self.ax2, self.ax3, self.ax4]:
|
||||||
ax.clear()
|
ax.clear()
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
# MTI filtered
|
# MTI filtered
|
||||||
mti_filtered = self.processor.mti_filter(iq_data)
|
mti_filtered = self.processor.mti_filter(iq_data)
|
||||||
|
|
||||||
if mti_filtered is not None and len(mti_filtered) > 0:
|
if mti_filtered is not None and len(mti_filtered) > 0:
|
||||||
mti_mag = np.abs(mti_filtered)
|
mti_mag = np.abs(mti_filtered)
|
||||||
|
|
||||||
# Use integer indices for plotting
|
# Use integer indices for plotting
|
||||||
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()
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
self.status_label.config(text="Status: Processing complete")
|
self.status_label.config(text="Status: Processing complete")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Plot update error: {e}")
|
logging.error(f"Plot update error: {e}")
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
self.status_label.config(text=f"Status: Plot error - {error_msg}")
|
self.status_label.config(text=f"Status: Plot error - {error_msg}")
|
||||||
|
|
||||||
def _update_cfar_results(self, range_axis, range_profile, detections):
|
def _update_cfar_results(self, range_axis, range_profile, detections):
|
||||||
try:
|
try:
|
||||||
# Clear the plot
|
# Clear the plot
|
||||||
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:
|
||||||
detection_ranges = []
|
detection_ranges = []
|
||||||
detection_mags = []
|
detection_mags = []
|
||||||
|
|
||||||
for bin_idx, mag in detections:
|
for bin_idx, mag in detections:
|
||||||
# Convert bin_idx to integer and ensure it's within bounds
|
# Convert bin_idx to integer and ensure it's within bounds
|
||||||
bin_idx_int = int(bin_idx)
|
bin_idx_int = int(bin_idx)
|
||||||
if 0 <= bin_idx_int < len(range_axis):
|
if 0 <= bin_idx_int < len(range_axis):
|
||||||
detection_ranges.append(range_axis[bin_idx_int])
|
detection_ranges.append(range_axis[bin_idx_int])
|
||||||
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,
|
||||||
self.ax1.set_title('Range Profile with CFAR Detections')
|
"ro",
|
||||||
self.ax1.set_xlabel('Range (m)')
|
markersize=8,
|
||||||
self.ax1.set_ylabel('Magnitude')
|
label="CFAR Detections",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ax1.set_title("Range Profile with CFAR Detections")
|
||||||
|
self.ax1.set_xlabel("Range (m)")
|
||||||
|
self.ax1.set_ylabel("Magnitude")
|
||||||
self.ax1.legend()
|
self.ax1.legend()
|
||||||
self.ax1.grid(True)
|
self.ax1.grid(True)
|
||||||
|
|
||||||
# Update targets list
|
# Update targets list
|
||||||
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}")
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
self.status_label.config(text=f"Status: CFAR results error - {error_msg}")
|
self.status_label.config(text=f"Status: CFAR results error - {error_msg}")
|
||||||
|
|
||||||
def update_targets_list(self):
|
def update_targets_list(self):
|
||||||
"""Update the targets list display"""
|
"""Update the targets list display"""
|
||||||
# Clear current list
|
# Clear current list
|
||||||
for item in self.targets_tree.get_children():
|
for item in self.targets_tree.get_children():
|
||||||
self.targets_tree.delete(item)
|
self.targets_tree.delete(item)
|
||||||
|
|
||||||
# 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(
|
||||||
f"{target.range:.1f}",
|
"",
|
||||||
f"{target.velocity:.1f}",
|
"end",
|
||||||
f"{target.azimuth}",
|
values=(
|
||||||
f"{target.elevation}",
|
f"{target.range:.1f}",
|
||||||
f"{target.snr:.1f}",
|
f"{target.velocity:.1f}",
|
||||||
target.chirp_type
|
f"{target.azimuth}",
|
||||||
))
|
f"{target.elevation}",
|
||||||
|
f"{target.snr:.1f}",
|
||||||
|
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()
|
||||||
|
|||||||
+700
-608
File diff suppressed because it is too large
Load Diff
+499
-419
File diff suppressed because it is too large
Load Diff
+130
-30
@@ -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)
|
||||||
@@ -1688,8 +1784,12 @@ class RadarGUI:
|
|||||||
try:
|
try:
|
||||||
# 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'):
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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)+'°</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)+'°</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)+'°</span></div>'
|
||||||
|
)+
|
||||||
|
(
|
||||||
|
'<div class="popup-row"><span class="popup-label">Elevation:</span>'+
|
||||||
|
'<span class="popup-value">'+t.elevation.toFixed(1)+'°</span></div>'
|
||||||
|
)+
|
||||||
|
(
|
||||||
|
'<div class="popup-row"><span class="popup-label">SNR:</span>'+
|
||||||
|
'<span class="popup-value">'+t.snr.toFixed(1)+' dB</span></div>'
|
||||||
|
)+
|
||||||
|
(
|
||||||
|
'<div class="popup-row"><span class="popup-label">Track:</span>'+
|
||||||
|
'<span class="popup-value">'+t.track_id+'</span></div>'
|
||||||
|
)+
|
||||||
|
(
|
||||||
|
'<div class="popup-row"><span class="popup-label">Status:</span>'+
|
||||||
|
'<span class="popup-value '+sc+'">'+st+'</span></div>'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[project]
|
||||||
|
name = "aeris-10-radar"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "AERIS-10 FMCW Radar Platform — host software & FPGA cosim tools"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
# Runtime dependencies intentionally empty — GUI deps are optional and
|
||||||
|
# listed in requirements_*.txt files for local installs.
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"ruff>=0.5",
|
||||||
|
"pytest>=8",
|
||||||
|
"numpy>=1.26",
|
||||||
|
"h5py>=3.10",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ruff configuration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py312"
|
||||||
|
line-length = 100
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F"]
|
||||||
Reference in New Issue
Block a user