From 2106e24952133c920598a695e03e6d7a48d5c338 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:18:34 +0545 Subject: [PATCH] fix: enforce strict ruff lint (17 rule sets) across entire repo - Expand ruff config from E/F to 17 rule sets (B, RUF, SIM, PIE, T20, ARG, ERA, A, BLE, RET, ISC, TCH, UP, C4, PERF) - Fix 907 lint errors across all Python files (GUI, FPGA cosim, schematics scripts, simulations, utilities, tools) - Replace all blind except-Exception with specific exception types - Remove commented-out dead code (ERA001) from cosim/simulation files - Modernize typing: deprecated typing.List/Dict/Tuple to builtins - Fix unused args/loop vars, ambiguous unicode, perf anti-patterns - Delete legacy GUI files V1-V4 - Add V7 test suite, requirements files - All CI jobs pass: ruff (0 errors), py_compile, pytest (92/92), MCU tests (20/20), FPGA regression (25/25) --- 5_Simulations/AAF_openEMS/aaf_simulation.py | 438 ++ 5_Simulations/Antenna/Quartz_Waveguide.py | 13 +- .../openems_quartz_slotted_wg_10p5GHz.py | 30 +- .../Generate_ChirpcsvFile.py | 9 +- 5_Simulations/Fencing/Via_fencing.py | 3 +- 5_Simulations/Fencing/Via_fencing2.py | 7 +- .../array_pattern_Kaiser25dB_like.py | 7 +- 8_Utils/Python/CSV_radar.py | 30 +- 8_Utils/Python/CSV_radar_2.py | 2 - .../Python/Generic_Triangular_Frequency.py | 1 - 8_Utils/Python/LUT.py | 4 +- 8_Utils/Python/RADAR_eq.py | 36 +- 8_Utils/Python/patch_antenna.py | 5 - 9_Firmware/9_2_FPGA/tb/cosim/compare.py | 91 +- 9_Firmware/9_2_FPGA/tb/cosim/compare_dc.csv | 1 - .../9_2_FPGA/tb/cosim/compare_doppler.py | 86 +- 9_Firmware/9_2_FPGA/tb/cosim/compare_mf.py | 83 +- 9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py | 99 +- 9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py | 65 +- .../9_2_FPGA/tb/cosim/gen_doppler_golden.py | 46 +- .../9_2_FPGA/tb/cosim/gen_mf_cosim_golden.py | 30 +- .../9_2_FPGA/tb/cosim/gen_multiseg_golden.py | 26 +- 9_Firmware/9_2_FPGA/tb/cosim/radar_scene.py | 66 +- .../tb/cosim/real_data/golden_reference.py | 186 +- .../tb/cosim/rx_final_doppler_out.csv | 4096 ++++++++--------- .../9_2_FPGA/tb/cosim/validate_mem_files.py | 165 +- 9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py | 31 +- 9_Firmware/9_3_GUI/GUI_PyQt_Map.py | 19 +- 9_Firmware/9_3_GUI/GUI_V1.py | 56 - 9_Firmware/9_3_GUI/GUI_V2.py | 1124 ----- 9_Firmware/9_3_GUI/GUI_V3.py | 1225 ----- 9_Firmware/9_3_GUI/GUI_V4.py | 1513 ------ 9_Firmware/9_3_GUI/GUI_V4_2_CSV.py | 715 --- 9_Firmware/9_3_GUI/GUI_V5.py | 105 +- 9_Firmware/9_3_GUI/GUI_V5_Demo.py | 128 +- 9_Firmware/9_3_GUI/GUI_V6.py | 97 +- 9_Firmware/9_3_GUI/GUI_V6_Demo.py | 52 +- 9_Firmware/9_3_GUI/radar_dashboard.py | 267 +- 9_Firmware/9_3_GUI/radar_protocol.py | 193 +- 9_Firmware/9_3_GUI/requirements_pyqt_gui.txt | 20 + 9_Firmware/9_3_GUI/requirements_v7.txt | 22 + 9_Firmware/9_3_GUI/smoke_test.py | 14 +- 9_Firmware/9_3_GUI/test_radar_dashboard.py | 59 +- 9_Firmware/9_3_GUI/test_v7.py | 347 ++ 9_Firmware/9_3_GUI/v7/__init__.py | 26 +- 9_Firmware/9_3_GUI/v7/dashboard.py | 932 ++-- 9_Firmware/9_3_GUI/v7/hardware.py | 219 +- 9_Firmware/9_3_GUI/v7/map_widget.py | 197 +- 9_Firmware/9_3_GUI/v7/models.py | 39 +- 9_Firmware/9_3_GUI/v7/processing.py | 231 +- 9_Firmware/9_3_GUI/v7/workers.py | 287 +- 9_Firmware/tools/uart_capture.py | 111 +- README.md | 2 +- pyproject.toml | 26 +- 54 files changed, 4619 insertions(+), 9063 deletions(-) create mode 100644 5_Simulations/AAF_openEMS/aaf_simulation.py delete mode 100644 9_Firmware/9_3_GUI/GUI_V1.py delete mode 100644 9_Firmware/9_3_GUI/GUI_V2.py delete mode 100644 9_Firmware/9_3_GUI/GUI_V3.py delete mode 100644 9_Firmware/9_3_GUI/GUI_V4.py delete mode 100644 9_Firmware/9_3_GUI/GUI_V4_2_CSV.py create mode 100644 9_Firmware/9_3_GUI/requirements_pyqt_gui.txt create mode 100644 9_Firmware/9_3_GUI/requirements_v7.txt create mode 100644 9_Firmware/9_3_GUI/test_v7.py diff --git a/5_Simulations/AAF_openEMS/aaf_simulation.py b/5_Simulations/AAF_openEMS/aaf_simulation.py new file mode 100644 index 0000000..b456d65 --- /dev/null +++ b/5_Simulations/AAF_openEMS/aaf_simulation.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 +""" +AERIS-10 FMC Anti-Alias Filter — openEMS 3D EM Simulation +========================================================== +5th-order differential Butterworth LC LPF, fc ≈ 195 MHz +All components are 0402 (1.0 x 0.5 mm) on FR4 4-layer stackup. + +Filter topology (each half of differential): + IN → R_series(49.9Ω) → L1(24nH) → C1(27pF)↓GND → L2(82nH) → C2(27pF)↓GND → L3(24nH) → OUT + Plus R_diff(100Ω) across input and output differential pairs. + +PCB stackup: + L1: F.Cu (signal + components) — 35µm copper + Prepreg: 0.2104 mm + L2: In1.Cu (GND plane) — 35µm copper + Core: 1.0 mm + L3: In2.Cu (Power plane) — 35µm copper + Prepreg: 0.2104 mm + L4: B.Cu (signal) — 35µm copper + +Total board thickness ≈ 1.6 mm + +Differential trace: W=0.23mm, S=0.12mm gap → Zdiff≈100Ω +All 0402 pads: 0.5mm x 0.55mm with 0.5mm gap between pads + +Simulation extracts 4-port S-parameters (differential in → differential out) +then converts to mixed-mode (Sdd11, Sdd21, Scc21) for analysis. +""" + +import os +import sys +import numpy as np + +sys.path.insert(0, '/Users/ganeshpanth/openEMS-Project/CSXCAD/python') +sys.path.insert(0, '/Users/ganeshpanth/openEMS-Project/openEMS/python') +os.environ['PATH'] = '/Users/ganeshpanth/opt/openEMS/bin:' + os.environ.get('PATH', '') + +from CSXCAD import ContinuousStructure +from openEMS import openEMS +from openEMS.physical_constants import C0, EPS0 + +unit = 1e-3 + +f_start = 1e6 +f_stop = 1e9 +f_center = 150e6 +f_IF_low = 120e6 +f_IF_high = 180e6 + +max_res = C0 / f_stop / unit / 20 + +copper_t = 0.035 +prepreg_t = 0.2104 +core_t = 1.0 +sub_er = 4.3 +sub_tand = 0.02 +cu_cond = 5.8e7 + +z_L4_bot = 0.0 +z_L4_top = z_L4_bot + copper_t +z_pre2_top = z_L4_top + prepreg_t +z_L3_top = z_pre2_top + copper_t +z_core_top = z_L3_top + core_t +z_L2_top = z_core_top + copper_t +z_pre1_top = z_L2_top + prepreg_t +z_L1_bot = z_pre1_top +z_L1_top = z_L1_bot + copper_t + +pad_w = 0.50 +pad_l = 0.55 +pad_gap = 0.50 +comp_pitch = 1.5 + +trace_w = 0.23 +trace_s = 0.12 +pair_pitch = trace_w + trace_s + +R_series = 49.9 +R_diff_in = 100.0 +R_diff_out = 100.0 + +L1_val = 24e-9 +L2_val = 82e-9 +L3_val = 24e-9 + +C1_val = 27e-12 +C2_val = 27e-12 + +FDTD = openEMS(NrTS=50000, EndCriteria=1e-5) +FDTD.SetGaussExcite(0.5 * (f_start + f_stop), 0.5 * (f_stop - f_start)) +FDTD.SetBoundaryCond(['PML_8'] * 6) + +CSX = ContinuousStructure() +FDTD.SetCSX(CSX) + +copper = CSX.AddMetal('copper') +gnd_metal = CSX.AddMetal('gnd_plane') +fr4_pre1 = CSX.AddMaterial( + 'prepreg1', epsilon=sub_er, kappa=sub_tand * 2 * np.pi * f_center * EPS0 * sub_er +) +fr4_core = CSX.AddMaterial( + 'core', epsilon=sub_er, kappa=sub_tand * 2 * np.pi * f_center * EPS0 * sub_er +) +fr4_pre2 = CSX.AddMaterial( + 'prepreg2', epsilon=sub_er, kappa=sub_tand * 2 * np.pi * f_center * EPS0 * sub_er +) + +y_P = +pair_pitch / 2 +y_N = -pair_pitch / 2 + +x_port_in = -1.0 +x_R_series = 0.0 +x_L1 = x_R_series + comp_pitch +x_C1 = x_L1 + comp_pitch +x_L2 = x_C1 + comp_pitch +x_C2 = x_L2 + comp_pitch +x_L3 = x_C2 + comp_pitch +x_port_out = x_L3 + comp_pitch + 1.0 + +x_Rdiff_in = x_port_in - 0.5 +x_Rdiff_out = x_port_out + 0.5 + +margin = 3.0 +x_min = x_Rdiff_in - margin +x_max = x_Rdiff_out + margin +y_min = y_N - margin +y_max = y_P + margin +z_min = z_L4_bot - margin +z_max = z_L1_top + margin + +fr4_pre1.AddBox([x_min, y_min, z_L2_top], [x_max, y_max, z_L1_bot], priority=1) +fr4_core.AddBox([x_min, y_min, z_L3_top], [x_max, y_max, z_core_top], priority=1) +fr4_pre2.AddBox([x_min, y_min, z_L4_top], [x_max, y_max, z_pre2_top], priority=1) + +gnd_metal.AddBox( + [x_min + 0.5, y_min + 0.5, z_core_top], [x_max - 0.5, y_max - 0.5, z_L2_top], priority=10 +) + + +def add_trace_segment(x_start, x_end, y_center, z_bot, z_top, w, metal, priority=20): + metal.AddBox( + [x_start, y_center - w / 2, z_bot], [x_end, y_center + w / 2, z_top], priority=priority + ) + + +def add_0402_pads(x_center, y_center, z_bot, z_top, metal, priority=20): + x_left = x_center - pad_gap / 2 - pad_w / 2 + metal.AddBox( + [x_left - pad_w / 2, y_center - pad_l / 2, z_bot], + [x_left + pad_w / 2, y_center + pad_l / 2, z_top], + priority=priority, + ) + x_right = x_center + pad_gap / 2 + pad_w / 2 + metal.AddBox( + [x_right - pad_w / 2, y_center - pad_l / 2, z_bot], + [x_right + pad_w / 2, y_center + pad_l / 2, z_top], + priority=priority, + ) + return (x_left, x_right) + + +def add_lumped_element( + CSX, name, element_type, value, x_center, y_center, z_bot, z_top, direction='x' +): + x_left = x_center - pad_gap / 2 - pad_w / 2 + x_right = x_center + pad_gap / 2 + pad_w / 2 + if direction == 'x': + start = [x_left, y_center - pad_l / 4, z_bot] + stop = [x_right, y_center + pad_l / 4, z_top] + edir = 'x' + elif direction == 'y': + start = [x_center - pad_l / 4, y_center - pad_gap / 2 - pad_w / 2, z_bot] + stop = [x_center + pad_l / 4, y_center + pad_gap / 2 + pad_w / 2, z_top] + edir = 'y' + if element_type == 'R': + elem = CSX.AddLumpedElement(name, ny=edir, caps=True, R=value) + elif element_type == 'L': + elem = CSX.AddLumpedElement(name, ny=edir, caps=True, L=value) + elif element_type == 'C': + elem = CSX.AddLumpedElement(name, ny=edir, caps=True, C=value) + elem.AddBox(start, stop, priority=30) + return elem + + +def add_shunt_cap( + CSX, name, value, x_center, y_trace, _z_top_signal, _z_gnd_top, metal, priority=20, +): + metal.AddBox( + [x_center - pad_w / 2, y_trace - pad_l / 2, z_L1_bot], + [x_center + pad_w / 2, y_trace + pad_l / 2, z_L1_top], + priority=priority, + ) + via_drill = 0.15 + cap = CSX.AddLumpedElement(name, ny='z', caps=True, C=value) + cap.AddBox( + [x_center - via_drill, y_trace - via_drill, z_L2_top], + [x_center + via_drill, y_trace + via_drill, z_L1_bot], + priority=30, + ) + via_metal = CSX.AddMetal(name + '_via') + via_metal.AddBox( + [x_center - via_drill, y_trace - via_drill, z_L2_top], + [x_center + via_drill, y_trace + via_drill, z_L1_bot], + priority=25, + ) + + +add_trace_segment( + x_port_in, x_R_series - pad_gap / 2 - pad_w, y_P, z_L1_bot, z_L1_top, trace_w, copper +) +add_0402_pads(x_R_series, y_P, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'R10', 'R', R_series, x_R_series, y_P, z_L1_bot, z_L1_top) +add_trace_segment( + x_R_series + pad_gap / 2 + pad_w, + x_L1 - pad_gap / 2 - pad_w, + y_P, + z_L1_bot, + z_L1_top, + trace_w, + copper, +) +add_0402_pads(x_L1, y_P, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'L5', 'L', L1_val, x_L1, y_P, z_L1_bot, z_L1_top) +add_trace_segment(x_L1 + pad_gap / 2 + pad_w, x_C1, y_P, z_L1_bot, z_L1_top, trace_w, copper) +add_shunt_cap(CSX, 'C53', C1_val, x_C1, y_P, z_L1_top, z_L2_top, copper) +add_trace_segment(x_C1, x_L2 - pad_gap / 2 - pad_w, y_P, z_L1_bot, z_L1_top, trace_w, copper) +add_0402_pads(x_L2, y_P, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'L8', 'L', L2_val, x_L2, y_P, z_L1_bot, z_L1_top) +add_trace_segment(x_L2 + pad_gap / 2 + pad_w, x_C2, y_P, z_L1_bot, z_L1_top, trace_w, copper) +add_shunt_cap(CSX, 'C55', C2_val, x_C2, y_P, z_L1_top, z_L2_top, copper) +add_trace_segment(x_C2, x_L3 - pad_gap / 2 - pad_w, y_P, z_L1_bot, z_L1_top, trace_w, copper) +add_0402_pads(x_L3, y_P, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'L10', 'L', L3_val, x_L3, y_P, z_L1_bot, z_L1_top) +add_trace_segment(x_L3 + pad_gap / 2 + pad_w, x_port_out, y_P, z_L1_bot, z_L1_top, trace_w, copper) + +add_trace_segment( + x_port_in, x_R_series - pad_gap / 2 - pad_w, y_N, z_L1_bot, z_L1_top, trace_w, copper +) +add_0402_pads(x_R_series, y_N, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'R11', 'R', R_series, x_R_series, y_N, z_L1_bot, z_L1_top) +add_trace_segment( + x_R_series + pad_gap / 2 + pad_w, + x_L1 - pad_gap / 2 - pad_w, + y_N, + z_L1_bot, + z_L1_top, + trace_w, + copper, +) +add_0402_pads(x_L1, y_N, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'L6', 'L', L1_val, x_L1, y_N, z_L1_bot, z_L1_top) +add_trace_segment(x_L1 + pad_gap / 2 + pad_w, x_C1, y_N, z_L1_bot, z_L1_top, trace_w, copper) +add_shunt_cap(CSX, 'C54', C1_val, x_C1, y_N, z_L1_top, z_L2_top, copper) +add_trace_segment(x_C1, x_L2 - pad_gap / 2 - pad_w, y_N, z_L1_bot, z_L1_top, trace_w, copper) +add_0402_pads(x_L2, y_N, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'L7', 'L', L2_val, x_L2, y_N, z_L1_bot, z_L1_top) +add_trace_segment(x_L2 + pad_gap / 2 + pad_w, x_C2, y_N, z_L1_bot, z_L1_top, trace_w, copper) +add_shunt_cap(CSX, 'C56', C2_val, x_C2, y_N, z_L1_top, z_L2_top, copper) +add_trace_segment(x_C2, x_L3 - pad_gap / 2 - pad_w, y_N, z_L1_bot, z_L1_top, trace_w, copper) +add_0402_pads(x_L3, y_N, z_L1_bot, z_L1_top, copper) +add_lumped_element(CSX, 'L9', 'L', L3_val, x_L3, y_N, z_L1_bot, z_L1_top) +add_trace_segment(x_L3 + pad_gap / 2 + pad_w, x_port_out, y_N, z_L1_bot, z_L1_top, trace_w, copper) + +R4_x = x_port_in - 0.3 +copper.AddBox( + [R4_x - pad_l / 2, y_P - pad_w / 2, z_L1_bot], + [R4_x + pad_l / 2, y_P + pad_w / 2, z_L1_top], + priority=20, +) +copper.AddBox( + [R4_x - pad_l / 2, y_N - pad_w / 2, z_L1_bot], + [R4_x + pad_l / 2, y_N + pad_w / 2, z_L1_top], + priority=20, +) +R4_elem = CSX.AddLumpedElement('R4', ny='y', caps=True, R=R_diff_in) +R4_elem.AddBox([R4_x - pad_l / 4, y_N, z_L1_bot], [R4_x + pad_l / 4, y_P, z_L1_top], priority=30) + +R18_x = x_port_out + 0.3 +copper.AddBox( + [R18_x - pad_l / 2, y_P - pad_w / 2, z_L1_bot], + [R18_x + pad_l / 2, y_P + pad_w / 2, z_L1_top], + priority=20, +) +copper.AddBox( + [R18_x - pad_l / 2, y_N - pad_w / 2, z_L1_bot], + [R18_x + pad_l / 2, y_N + pad_w / 2, z_L1_top], + priority=20, +) +R18_elem = CSX.AddLumpedElement('R18', ny='y', caps=True, R=R_diff_out) +R18_elem.AddBox([R18_x - pad_l / 4, y_N, z_L1_bot], [R18_x + pad_l / 4, y_P, z_L1_top], priority=30) + +port1 = FDTD.AddLumpedPort( + 1, + 50, + [x_port_in, y_P - trace_w / 2, z_L2_top], + [x_port_in, y_P + trace_w / 2, z_L1_bot], + 'z', + excite=1.0, +) +port2 = FDTD.AddLumpedPort( + 2, + 50, + [x_port_in, y_N - trace_w / 2, z_L2_top], + [x_port_in, y_N + trace_w / 2, z_L1_bot], + 'z', + excite=-1.0, +) +port3 = FDTD.AddLumpedPort( + 3, + 50, + [x_port_out, y_P - trace_w / 2, z_L2_top], + [x_port_out, y_P + trace_w / 2, z_L1_bot], + 'z', + excite=0, +) +port4 = FDTD.AddLumpedPort( + 4, + 50, + [x_port_out, y_N - trace_w / 2, z_L2_top], + [x_port_out, y_N + trace_w / 2, z_L1_bot], + 'z', + excite=0, +) + +mesh = CSX.GetGrid() +mesh.SetDeltaUnit(unit) +mesh.AddLine('x', [x_min, x_max]) +for x_comp in [x_R_series, x_L1, x_C1, x_L2, x_C2, x_L3]: + mesh.AddLine('x', np.linspace(x_comp - 1.0, x_comp + 1.0, 15)) +mesh.AddLine('x', [x_port_in, x_port_out]) +mesh.AddLine('x', [R4_x, R18_x]) +mesh.AddLine('y', [y_min, y_max]) +for y_trace in [y_P, y_N]: + mesh.AddLine('y', np.linspace(y_trace - 0.5, y_trace + 0.5, 10)) +mesh.AddLine('z', [z_min, z_max]) +mesh.AddLine('z', np.linspace(z_L4_bot - 0.1, z_L1_top + 0.1, 25)) +mesh.SmoothMeshLines('x', max_res, ratio=1.4) +mesh.SmoothMeshLines('y', max_res, ratio=1.4) +mesh.SmoothMeshLines('z', max_res / 3, ratio=1.3) + +sim_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'results') +if not os.path.exists(sim_path): + os.makedirs(sim_path) + +CSX_file = os.path.join(sim_path, 'aaf_filter.xml') +CSX.Write2XML(CSX_file) + +FDTD.Run(sim_path, cleanup=True, verbose=3) + +freq = np.linspace(f_start, f_stop, 1001) +port1.CalcPort(sim_path, freq) +port2.CalcPort(sim_path, freq) +port3.CalcPort(sim_path, freq) +port4.CalcPort(sim_path, freq) + +inc1 = port1.uf_inc +ref1 = port1.uf_ref +inc2 = port2.uf_inc +ref2 = port2.uf_ref +inc3 = port3.uf_inc +ref3 = port3.uf_ref +inc4 = port4.uf_inc +ref4 = port4.uf_ref + +a_diff = (inc1 - inc2) / np.sqrt(2) +b_diff_in = (ref1 - ref2) / np.sqrt(2) +b_diff_out = (ref3 - ref4) / np.sqrt(2) + +Sdd11 = b_diff_in / a_diff +Sdd21 = b_diff_out / a_diff + +b_comm_out = (ref3 + ref4) / np.sqrt(2) +Scd21 = b_comm_out / a_diff + +import matplotlib # noqa: E402 +matplotlib.use('Agg') +import matplotlib.pyplot as plt # noqa: E402 + +fig, axes = plt.subplots(3, 1, figsize=(12, 14)) + +ax = axes[0] +Sdd21_dB = 20 * np.log10(np.abs(Sdd21) + 1e-15) +ax.plot(freq / 1e6, Sdd21_dB, 'b-', linewidth=2, label='|Sdd21| (Insertion Loss)') +ax.axvspan( + f_IF_low / 1e6, f_IF_high / 1e6, alpha=0.15, color='green', label='IF Band (120-180 MHz)' +) +ax.axhline(-3, color='r', linestyle='--', alpha=0.5, label='-3 dB') +ax.set_xlabel('Frequency (MHz)') +ax.set_ylabel('|Sdd21| (dB)') +ax.set_title('Anti-Alias Filter — Differential Insertion Loss') +ax.set_xlim([0, 1000]) +ax.set_ylim([-60, 5]) +ax.grid(True, alpha=0.3) +ax.legend() + +ax = axes[1] +Sdd11_dB = 20 * np.log10(np.abs(Sdd11) + 1e-15) +ax.plot(freq / 1e6, Sdd11_dB, 'r-', linewidth=2, label='|Sdd11| (Return Loss)') +ax.axvspan(f_IF_low / 1e6, f_IF_high / 1e6, alpha=0.15, color='green', label='IF Band') +ax.axhline(-10, color='orange', linestyle='--', alpha=0.5, label='-10 dB') +ax.set_xlabel('Frequency (MHz)') +ax.set_ylabel('|Sdd11| (dB)') +ax.set_title('Anti-Alias Filter — Differential Return Loss') +ax.set_xlim([0, 1000]) +ax.set_ylim([-40, 0]) +ax.grid(True, alpha=0.3) +ax.legend() + +ax = axes[2] +phase_Sdd21 = np.unwrap(np.angle(Sdd21)) +group_delay = -np.diff(phase_Sdd21) / np.diff(2 * np.pi * freq) * 1e9 +ax.plot(freq[1:] / 1e6, group_delay, 'g-', linewidth=2, label='Group Delay') +ax.axvspan(f_IF_low / 1e6, f_IF_high / 1e6, alpha=0.15, color='green', label='IF Band') +ax.set_xlabel('Frequency (MHz)') +ax.set_ylabel('Group Delay (ns)') +ax.set_title('Anti-Alias Filter — Group Delay') +ax.set_xlim([0, 500]) +ax.grid(True, alpha=0.3) +ax.legend() + +plt.tight_layout() +plot_file = os.path.join(sim_path, 'aaf_filter_response.png') +plt.savefig(plot_file, dpi=150) + +idx_120 = np.argmin(np.abs(freq - f_IF_low)) +idx_150 = np.argmin(np.abs(freq - f_center)) +idx_180 = np.argmin(np.abs(freq - f_IF_high)) +idx_200 = np.argmin(np.abs(freq - 200e6)) +idx_400 = np.argmin(np.abs(freq - 400e6)) + + +csv_file = os.path.join(sim_path, 'aaf_sparams.csv') +np.savetxt( + csv_file, + np.column_stack([freq / 1e6, Sdd21_dB, Sdd11_dB, 20 * np.log10(np.abs(Scd21) + 1e-15)]), + header='Freq_MHz, Sdd21_dB, Sdd11_dB, Scd21_dB', + delimiter=',', fmt='%.6f' +) diff --git a/5_Simulations/Antenna/Quartz_Waveguide.py b/5_Simulations/Antenna/Quartz_Waveguide.py index 3b37b39..15a7cac 100644 --- a/5_Simulations/Antenna/Quartz_Waveguide.py +++ b/5_Simulations/Antenna/Quartz_Waveguide.py @@ -91,9 +91,9 @@ z_edges = np.concatenate([z_centers - slot_L/2.0, z_centers + slot_L/2.0]) # ------------------------- # Mesh lines — EXPLICIT (no GetLine calls) # ------------------------- -x_lines = sorted(set([x_min, -t_metal, 0.0, a, a+t_metal, x_max] + list(x_edges))) +x_lines = sorted({x_min, -t_metal, 0.0, a, a + t_metal, x_max, *list(x_edges)}) y_lines = [y_min, 0.0, b, b+t_metal, y_max] -z_lines = sorted(set([z_min, 0.0, L, z_max] + list(z_edges))) +z_lines = sorted({z_min, 0.0, L, z_max, *list(z_edges)}) mesh.AddLine('x', x_lines) mesh.AddLine('y', y_lines) @@ -123,7 +123,7 @@ pec.AddBox([-t_metal,-t_metal,0],[a+t_metal,0, L]) # bottom pec.AddBox([-t_metal, b, 0], [a+t_metal,b+t_metal,L]) # top # Slots = AIR boxes overriding the top metal -for zc, xc in zip(z_centers, x_centers): +for zc, xc in zip(z_centers, x_centers, strict=False): x1, x2 = xc - slot_w/2.0, xc + slot_w/2.0 z1, z2 = zc - slot_L/2.0, zc + slot_L/2.0 prim = air.AddBox([x1, b, z1], [x2, b+t_metal, z2]) @@ -181,7 +181,7 @@ if simulate: # Post-processing: S-params & impedance # ------------------------- freq = np.linspace(f_start, f_stop, 401) -ports = [p for p in FDTD.ports] # Port 1 & Port 2 in creation order +ports = list(FDTD.ports) # Port 1 & Port 2 in creation order for p in ports: p.CalcPort(Sim_Path, freq) @@ -226,9 +226,6 @@ mismatch = 1.0 - np.abs(S11[idx_f0])**2 # (1 - |S11|^2) Gmax_lin = Dmax_lin * float(mismatch) Gmax_dBi = 10*np.log10(Gmax_lin) -print(f"Max directivity @ {f0/1e9:.3f} GHz: {10*np.log10(Dmax_lin):.2f} dBi") -print(f"Mismatch term (1-|S11|^2) : {float(mismatch):.3f}") -print(f"Estimated max realized gain : {Gmax_dBi:.2f} dBi") # 3D normalized pattern E = np.squeeze(res.E_norm) # shape [f, th, ph] -> [th, ph] @@ -254,7 +251,7 @@ 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)' ) -for zc, xc in zip(z_centers, x_centers): +for zc, xc in zip(z_centers, x_centers, strict=False): plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0), slot_w, slot_L, fc='#3355ff', ec='k')) plt.xlim(-2, a + 2) diff --git a/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py b/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py index fbdabae..374902a 100644 --- a/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py +++ b/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py @@ -1,6 +1,6 @@ # openems_quartz_slotted_wg_10p5GHz.py # Slotted rectangular waveguide (quartz-filled, εr=3.8) tuned to 10.5 GHz. -# Builds geometry, meshes (no GetLine calls), sweeps S-params/impedance over 9.5–11.5 GHz, +# Builds geometry, meshes (no GetLine calls), sweeps S-params/impedance over 9.5-11.5 GHz, # computes 3D far-field, and reports estimated max realized gain. import os @@ -15,14 +15,14 @@ from openEMS.physical_constants import C0 try: from CSXCAD import ContinuousStructure, AppCSXCAD_BIN HAVE_APP = True -except Exception: +except ImportError: from CSXCAD import ContinuousStructure AppCSXCAD_BIN = None HAVE_APP = False #Set PROFILE to "sanity" first; run and check [mesh] cells: stays reasonable. -#If it’s small, move to "balanced"; once happy, go "full". +#If it's small, move to "balanced"; once happy, go "full". #Toggle VIEW_GEOM=True if you want the 3D viewer (requires AppCSXCAD_BIN available). @@ -123,9 +123,9 @@ x_edges = np.concatenate([x_centers - slot_w/2.0, x_centers + slot_w/2.0]) z_edges = np.concatenate([z_centers - slot_L/2.0, z_centers + slot_L/2.0]) # Mesh lines: explicit (NO GetLine calls) -x_lines = sorted(set([x_min, -t_metal, 0.0, a, a+t_metal, x_max] + list(x_edges))) +x_lines = sorted({x_min, -t_metal, 0.0, a, a + t_metal, x_max, *list(x_edges)}) y_lines = [y_min, 0.0, b, b+t_metal, y_max] -z_lines = sorted(set([z_min, 0.0, guide_length_mm, z_max] + list(z_edges))) +z_lines = sorted({z_min, 0.0, guide_length_mm, z_max, *list(z_edges)}) mesh.AddLine('x', x_lines) mesh.AddLine('y', y_lines) @@ -134,13 +134,10 @@ mesh.AddLine('z', z_lines) # Print complexity and rough memory (to help stay inside 16 GB) Nx, Ny, Nz = len(x_lines)-1, len(y_lines)-1, len(z_lines)-1 Ncells = Nx*Ny*Nz -print(f"[mesh] cells: {Nx} × {Ny} × {Nz} = {Ncells:,}") mem_fields_bytes = Ncells * 6 * 8 # rough ~ (Ex,Ey,Ez,Hx,Hy,Hz) doubles -print(f"[mesh] rough field memory: ~{mem_fields_bytes/1e9:.2f} GB (solver overhead extra)") dx_min = min(np.diff(x_lines)) dy_min = min(np.diff(y_lines)) dz_min = min(np.diff(z_lines)) -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 mesh.SmoothMeshLines('all', mesh_res, ratio=1.4) @@ -165,7 +162,7 @@ pec.AddBox( ) # top (slots will pierce) # Slots (AIR) overriding top metal -for zc, xc in zip(z_centers, x_centers): +for zc, xc in zip(z_centers, x_centers, strict=False): x1, x2 = xc - slot_w/2.0, xc + slot_w/2.0 z1, z2 = zc - slot_L/2.0, zc + slot_L/2.0 prim = airM.AddBox([x1, b, z1], [x2, b+t_metal, z2]) @@ -215,7 +212,6 @@ if VIEW_GEOM and HAVE_APP and AppCSXCAD_BIN: t0 = time.time() FDTD.Run(Sim_Path, cleanup=True, verbose=2, numThreads=THREADS) t1 = time.time() -print(f"[timing] FDTD solve elapsed: {t1 - t0:.2f} s") # ... right before NF2FF (far-field): t2 = time.time() @@ -224,14 +220,12 @@ try: except AttributeError: res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi) # noqa: F821 t3 = time.time() -print(f"[timing] NF2FF (far-field) elapsed: {t3 - t2:.2f} s") # ... S-parameters postproc timing (optional): t4 = time.time() for p in ports: # noqa: F821 p.CalcPort(Sim_Path, freq) # noqa: F821 t5 = time.time() -print(f"[timing] Port/S-params postproc elapsed: {t5 - t4:.2f} s") # ======= @@ -240,11 +234,8 @@ print(f"[timing] Port/S-params postproc elapsed: {t5 - t4:.2f} s") if SIMULATE: FDTD.Run(Sim_Path, cleanup=True, verbose=2, numThreads=THREADS) -# ========================== -# POST: S-PARAMS / IMPEDANCE -# ========================== -freq = np.linspace(f_start, f_stop, profiles[PROFILE]["freq_pts"]) -ports = [p for p in FDTD.ports] # Port 1 & 2 in creation order +freq = np.linspace(f_start, f_stop, profiles[PROFILE]["freq_pts"]) +ports = list(FDTD.ports) # Port 1 & 2 in creation order for p in ports: p.CalcPort(Sim_Path, freq) @@ -288,9 +279,6 @@ mismatch = 1.0 - np.abs(S11[idx_f0])**2 Gmax_lin = Dmax_lin * float(mismatch) Gmax_dBi = 10*np.log10(Gmax_lin) -print(f"[far-field] Dmax @ {f0/1e9:.3f} GHz: {10*np.log10(Dmax_lin):.2f} dBi") -print(f"[far-field] mismatch (1-|S11|^2): {float(mismatch):.3f}") -print(f"[far-field] est. max realized gain: {Gmax_dBi:.2f} dBi") # Normalized 3D pattern E = np.squeeze(res.E_norm) # [th, ph] @@ -324,7 +312,7 @@ plt.fill_between( step='pre', label='WG top aperture', ) -for zc, xc in zip(z_centers, x_centers): +for zc, xc in zip(z_centers, x_centers, strict=False): plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0), slot_w, slot_L, fc='#3355ff', ec='k')) plt.xlim(-2, a + 2) diff --git a/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py b/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py index c264832..395dd1f 100644 --- a/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py +++ b/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py @@ -68,14 +68,8 @@ def generate_multi_ramp_csv(Fs=125e6, Tb=1e-6, Tau=2e-6, fmax=30e6, fmin=10e6, # --- Save CSV (no header) df = pd.DataFrame({"time(s)": t_csv, "voltage(V)": y_csv}) df.to_csv(filename, index=False, header=False) - print(f"CSV saved: {filename}") - print( - f"Total raw samples: {total_samples} | Ramps inserted: {ramps_inserted} " - f"| CSV points: {len(y_csv)}" - ) - # --- Plot (staircase) - if show_plot or save_plot_png: + if show_plot or save_plot_png: # Choose plotting vectors (use raw DAC samples to keep lines crisp) t_plot = t y_plot = y @@ -111,7 +105,6 @@ def generate_multi_ramp_csv(Fs=125e6, Tb=1e-6, Tau=2e-6, fmax=30e6, fmin=10e6, if save_plot_png: plt.savefig(save_plot_png, dpi=150) - print(f"Plot saved: {save_plot_png}") if show_plot: plt.show() else: diff --git a/5_Simulations/Fencing/Via_fencing.py b/5_Simulations/Fencing/Via_fencing.py index 6ad51ca..01e64f1 100644 --- a/5_Simulations/Fencing/Via_fencing.py +++ b/5_Simulations/Fencing/Via_fencing.py @@ -1,7 +1,6 @@ import matplotlib.pyplot as plt -# Dimensions (all in mm) -line_width = 0.204 +line_width = 0.204 substrate_height = 0.102 via_drill = 0.20 via_pad_A = 0.20 # minimal pad case diff --git a/5_Simulations/Fencing/Via_fencing2.py b/5_Simulations/Fencing/Via_fencing2.py index 0fe75ae..2f49051 100644 --- a/5_Simulations/Fencing/Via_fencing2.py +++ b/5_Simulations/Fencing/Via_fencing2.py @@ -1,7 +1,6 @@ import matplotlib.pyplot as plt -# Dimensions (all in mm) -line_width = 0.204 +line_width = 0.204 via_pad_A = 0.20 via_pad_B = 0.45 polygon_offset = 0.30 @@ -50,14 +49,14 @@ ax.text(-2, polygon_y1 + 0.5, "Via B Ø0.45 mm pad", color="red") # Add pitch dimension (horizontal between vias) ax.annotate("", xy=(2, polygon_y1 + 0.2), xytext=(2 + via_pitch, polygon_y1 + 0.2), - arrowprops=dict(arrowstyle="<->", color="purple")) + arrowprops={"arrowstyle": "<->", "color": "purple"}) ax.text(2 + via_pitch/2, polygon_y1 + 0.3, f"{via_pitch:.2f} mm pitch", color="purple", ha="center") # Add distance from RF line edge to via center line_edge_y = rf_line_y + line_width/2 via_center_y = polygon_y1 ax.annotate("", xy=(2.4, line_edge_y), xytext=(2.4, via_center_y), - arrowprops=dict(arrowstyle="<->", color="brown")) + arrowprops={"arrowstyle": "<->", "color": "brown"}) ax.text( 2.5, (line_edge_y + via_center_y) / 2, f"{via_center_offset:.2f} mm", color="brown", va="center" ) diff --git a/5_Simulations/array_pattern_Kaiser25dB_like.py b/5_Simulations/array_pattern_Kaiser25dB_like.py index df06583..3e5106f 100644 --- a/5_Simulations/array_pattern_Kaiser25dB_like.py +++ b/5_Simulations/array_pattern_Kaiser25dB_like.py @@ -27,7 +27,7 @@ n_idx = np.arange(N) - (N-1)/2 y_positions = m_idx * dy z_positions = n_idx * dz -def element_factor(theta_rad, phi_rad): +def element_factor(theta_rad, _phi_rad): return np.abs(np.cos(theta_rad)) def array_factor(theta_rad, phi_rad, y_positions, z_positions, wy, wz, theta0_rad, phi0_rad): @@ -105,8 +105,3 @@ plt.title('Array Pattern Heatmap (|AF·EF|, dB) — Kaiser ~-25 dB') plt.tight_layout() plt.savefig('Heatmap_Kaiser25dB_like.png', bbox_inches='tight') plt.show() - -print( - 'Saved: E_plane_Kaiser25dB_like.png, H_plane_Kaiser25dB_like.png, ' - 'Heatmap_Kaiser25dB_like.png' -) diff --git a/8_Utils/Python/CSV_radar.py b/8_Utils/Python/CSV_radar.py index 7e85ffb..5484c17 100644 --- a/8_Utils/Python/CSV_radar.py +++ b/8_Utils/Python/CSV_radar.py @@ -38,7 +38,6 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): chirp_number = 0 # Generate Long Chirps (30µs duration equivalent) - print("Generating Long Chirps...") for chirp in range(num_long_chirps): for sample in range(samples_per_chirp): # Base noise @@ -90,7 +89,6 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): timestamp_ns += 175400 # 175.4µs guard time # Generate Short Chirps (0.5µs duration equivalent) - print("Generating Short Chirps...") for chirp in range(num_short_chirps): for sample in range(samples_per_chirp): # Base noise @@ -142,11 +140,6 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): # Save to CSV df.to_csv(filename, index=False) - print(f"Generated CSV file: {filename}") - print(f"Total samples: {len(df)}") - print(f"Long chirps: {num_long_chirps}, Short chirps: {num_short_chirps}") - print(f"Samples per chirp: {samples_per_chirp}") - print(f"File size: {len(df) // 1000}K samples") return df @@ -154,15 +147,11 @@ def analyze_generated_data(df): """ Analyze the generated data to verify target detection """ - print("\n=== Data Analysis ===") # Basic statistics - long_chirps = df[df['chirp_type'] == 'LONG'] - short_chirps = df[df['chirp_type'] == 'SHORT'] + df[df['chirp_type'] == 'LONG'] + df[df['chirp_type'] == 'SHORT'] - print(f"Long chirp samples: {len(long_chirps)}") - print(f"Short chirp samples: {len(short_chirps)}") - print(f"Unique chirp numbers: {df['chirp_number'].nunique()}") # Calculate actual magnitude and phase for analysis df['magnitude'] = np.sqrt(df['I_value']**2 + df['Q_value']**2) @@ -172,15 +161,11 @@ def analyze_generated_data(df): high_mag_threshold = df['magnitude'].quantile(0.95) # Top 5% targets_detected = df[df['magnitude'] > high_mag_threshold] - print(f"\nTarget detection threshold: {high_mag_threshold:.2f}") - print(f"High magnitude samples: {len(targets_detected)}") # Group by chirp type - long_targets = targets_detected[targets_detected['chirp_type'] == 'LONG'] - short_targets = targets_detected[targets_detected['chirp_type'] == 'SHORT'] + targets_detected[targets_detected['chirp_type'] == 'LONG'] + targets_detected[targets_detected['chirp_type'] == 'SHORT'] - print(f"Targets in long chirps: {len(long_targets)}") - print(f"Targets in short chirps: {len(short_targets)}") return df @@ -191,10 +176,3 @@ if __name__ == "__main__": # Analyze the generated data analyze_generated_data(df) - print("\n=== CSV File Ready ===") - print("You can now test the Python GUI with this CSV file!") - print("The file contains:") - print("- 16 Long chirps + 16 Short chirps") - print("- 4 simulated targets at different ranges and velocities") - print("- Realistic noise and clutter") - print("- Proper I/Q data for Doppler processing") diff --git a/8_Utils/Python/CSV_radar_2.py b/8_Utils/Python/CSV_radar_2.py index 8aeebd5..9510e6d 100644 --- a/8_Utils/Python/CSV_radar_2.py +++ b/8_Utils/Python/CSV_radar_2.py @@ -90,8 +90,6 @@ def generate_small_radar_csv(filename="small_test_radar_data.csv"): df = pd.DataFrame(data) df.to_csv(filename, index=False) - print(f"Generated small CSV: {filename}") - print(f"Total samples: {len(df)}") return df generate_small_radar_csv() diff --git a/8_Utils/Python/Generic_Triangular_Frequency.py b/8_Utils/Python/Generic_Triangular_Frequency.py index f027403..62315a6 100644 --- a/8_Utils/Python/Generic_Triangular_Frequency.py +++ b/8_Utils/Python/Generic_Triangular_Frequency.py @@ -31,7 +31,6 @@ freq_indices = np.arange(L) T = L*Ts freq = freq_indices/T -print("The Array is: ", x) #printing the array plt.figure(figsize = (12, 6)) plt.subplot(121) diff --git a/8_Utils/Python/LUT.py b/8_Utils/Python/LUT.py index ca11e6e..56a4cb1 100644 --- a/8_Utils/Python/LUT.py +++ b/8_Utils/Python/LUT.py @@ -20,5 +20,5 @@ y = 1 + np.sin(theta_n) # Normalize from 0 to 2 y_scaled = np.round(y * 127.5).astype(int) # Scale to 8-bit range (0-255) # Print values in Verilog-friendly format -for i in range(n): - print(f"waveform_LUT[{i}] = 8'h{y_scaled[i]:02X};") +for _i in range(n): + pass diff --git a/8_Utils/Python/RADAR_eq.py b/8_Utils/Python/RADAR_eq.py index 60ce3a7..d0ecd41 100644 --- a/8_Utils/Python/RADAR_eq.py +++ b/8_Utils/Python/RADAR_eq.py @@ -58,10 +58,10 @@ class RadarCalculatorGUI: scrollbar = ttk.Scrollbar(self.input_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) - scrollable_frame.bind( - "", - lambda e: canvas.configure(scrollregion=canvas.bbox("all")) - ) + scrollable_frame.bind( + "", + lambda _e: canvas.configure(scrollregion=canvas.bbox("all")) + ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) @@ -83,7 +83,7 @@ class RadarCalculatorGUI: self.entries = {} - for i, (label, default) in enumerate(inputs): + for _i, (label, default) in enumerate(inputs): # Create a frame for each input row row_frame = ttk.Frame(scrollable_frame) row_frame.pack(fill=tk.X, pady=5) @@ -119,8 +119,8 @@ class RadarCalculatorGUI: calculate_btn.pack() # Bind hover effect - calculate_btn.bind("", lambda e: calculate_btn.config(bg='#45a049')) - calculate_btn.bind("", lambda e: calculate_btn.config(bg='#4CAF50')) + calculate_btn.bind("", lambda _e: calculate_btn.config(bg='#45a049')) + calculate_btn.bind("", lambda _e: calculate_btn.config(bg='#4CAF50')) def create_results_display(self): """Create the results display area""" @@ -135,10 +135,10 @@ class RadarCalculatorGUI: scrollbar = ttk.Scrollbar(self.results_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) - scrollable_frame.bind( - "", - lambda e: canvas.configure(scrollregion=canvas.bbox("all")) - ) + scrollable_frame.bind( + "", + lambda _e: canvas.configure(scrollregion=canvas.bbox("all")) + ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) @@ -158,7 +158,7 @@ class RadarCalculatorGUI: self.results_labels = {} - for i, (label, key) in enumerate(results): + for _i, (label, key) in enumerate(results): # Create a frame for each result row row_frame = ttk.Frame(scrollable_frame) row_frame.pack(fill=tk.X, pady=10, padx=20) @@ -180,10 +180,10 @@ class RadarCalculatorGUI: note_text = """ NOTES: • Maximum detectable range is calculated using the radar equation - • Range resolution = c × τ / 2, where τ is pulse duration - • Maximum unambiguous range = c / (2 × PRF) - • Maximum detectable speed = λ × PRF / 4 - • Speed resolution = λ × PRF / (2 × N) where N is number of pulses (assumed 1) + • Range resolution = c x τ / 2, where τ is pulse duration + • Maximum unambiguous range = c / (2 x PRF) + • Maximum detectable speed = λ x PRF / 4 + • Speed resolution = λ x PRF / (2 x N) where N is number of pulses (assumed 1) • λ (wavelength) = c / f """ @@ -300,10 +300,10 @@ class RadarCalculatorGUI: # Show success message messagebox.showinfo("Success", "Calculation completed successfully!") - except Exception as e: + except (ValueError, ZeroDivisionError) as e: messagebox.showerror( "Calculation Error", - f"An error occurred during calculation:\n{str(e)}", + f"An error occurred during calculation:\n{e!s}", ) def main(): diff --git a/8_Utils/Python/patch_antenna.py b/8_Utils/Python/patch_antenna.py index 85dd886..7e31c49 100644 --- a/8_Utils/Python/patch_antenna.py +++ b/8_Utils/Python/patch_antenna.py @@ -66,8 +66,3 @@ W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters( frequency, epsilon_r, h_sub, h_cu, array ) -print(f"Width of the patch: {W_mm:.4f} mm") -print(f"Length of the patch: {L_mm:.4f} mm") -print(f"Separation distance in horizontal axis: {dx_mm:.4f} mm") -print(f"Separation distance in vertical axis: {dy_mm:.4f} mm") -print(f"Feeding line width: {W_feed_mm:.2f} mm") diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare.py b/9_Firmware/9_2_FPGA/tb/cosim/compare.py index 7913670..429c1cf 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare.py @@ -93,7 +93,7 @@ SCENARIOS = { def load_adc_hex(filepath): """Load 8-bit unsigned ADC samples from hex file.""" samples = [] - with open(filepath, 'r') as f: + with open(filepath) as f: for line in f: line = line.strip() if not line or line.startswith('//'): @@ -106,7 +106,7 @@ def load_rtl_csv(filepath): """Load RTL baseband output CSV (sample_idx, baseband_i, baseband_q).""" bb_i = [] bb_q = [] - with open(filepath, 'r') as f: + with open(filepath) as f: f.readline() # Skip header for line in f: line = line.strip() @@ -125,7 +125,6 @@ def run_python_model(adc_samples): because the RTL testbench captures the FIR output directly (baseband_i_reg <= fir_i_out in ddc_400m.v). """ - print(" Running Python model...") chain = SignalChain() result = chain.process_adc_block(adc_samples) @@ -135,7 +134,6 @@ def run_python_model(adc_samples): bb_i = result['fir_i_raw'] bb_q = result['fir_q_raw'] - print(f" Python model: {len(bb_i)} baseband I, {len(bb_q)} baseband Q outputs") return bb_i, bb_q @@ -145,7 +143,7 @@ def compute_rms_error(a, b): raise ValueError(f"Length mismatch: {len(a)} vs {len(b)}") if len(a) == 0: return 0.0 - sum_sq = sum((x - y) ** 2 for x, y in zip(a, b)) + sum_sq = sum((x - y) ** 2 for x, y in zip(a, b, strict=False)) return math.sqrt(sum_sq / len(a)) @@ -153,7 +151,7 @@ def compute_max_abs_error(a, b): """Compute maximum absolute error between two equal-length lists.""" if len(a) != len(b) or len(a) == 0: return 0 - return max(abs(x - y) for x, y in zip(a, b)) + return max(abs(x - y) for x, y in zip(a, b, strict=False)) def compute_correlation(a, b): @@ -235,44 +233,29 @@ def compute_signal_stats(samples): def compare_scenario(scenario_name): """Run comparison for one scenario. Returns True if passed.""" if scenario_name not in SCENARIOS: - print(f"ERROR: Unknown scenario '{scenario_name}'") - print(f"Available: {', '.join(SCENARIOS.keys())}") return False cfg = SCENARIOS[scenario_name] base_dir = os.path.dirname(os.path.abspath(__file__)) - print("=" * 60) - print(f"Co-simulation Comparison: {cfg['description']}") - print(f"Scenario: {scenario_name}") - print("=" * 60) # ---- Load ADC data ---- adc_path = os.path.join(base_dir, cfg['adc_hex']) if not os.path.exists(adc_path): - print(f"ERROR: ADC hex file not found: {adc_path}") - print("Run radar_scene.py first to generate test vectors.") return False adc_samples = load_adc_hex(adc_path) - print(f"\nADC samples loaded: {len(adc_samples)}") # ---- Load RTL output ---- rtl_path = os.path.join(base_dir, cfg['rtl_csv']) if not os.path.exists(rtl_path): - print(f"ERROR: RTL CSV not found: {rtl_path}") - print("Run the RTL simulation first:") - print(f" iverilog -g2001 -DSIMULATION -DSCENARIO_{scenario_name.upper()} ...") return False rtl_i, rtl_q = load_rtl_csv(rtl_path) - print(f"RTL outputs loaded: {len(rtl_i)} I, {len(rtl_q)} Q samples") # ---- Run Python model ---- py_i, py_q = run_python_model(adc_samples) # ---- Length comparison ---- - print(f"\nOutput lengths: RTL={len(rtl_i)}, Python={len(py_i)}") len_diff = abs(len(rtl_i) - len(py_i)) - print(f"Length difference: {len_diff} samples") # ---- Signal statistics ---- rtl_i_stats = compute_signal_stats(rtl_i) @@ -280,20 +263,10 @@ def compare_scenario(scenario_name): py_i_stats = compute_signal_stats(py_i) py_q_stats = compute_signal_stats(py_q) - print("\nSignal Statistics:") - print(f" RTL I: mean={rtl_i_stats['mean']:.1f}, rms={rtl_i_stats['rms']:.1f}, " - f"range=[{rtl_i_stats['min']}, {rtl_i_stats['max']}]") - print(f" RTL Q: mean={rtl_q_stats['mean']:.1f}, rms={rtl_q_stats['rms']:.1f}, " - f"range=[{rtl_q_stats['min']}, {rtl_q_stats['max']}]") - print(f" Py I: mean={py_i_stats['mean']:.1f}, rms={py_i_stats['rms']:.1f}, " - f"range=[{py_i_stats['min']}, {py_i_stats['max']}]") - print(f" Py Q: mean={py_q_stats['mean']:.1f}, rms={py_q_stats['rms']:.1f}, " - f"range=[{py_q_stats['min']}, {py_q_stats['max']}]") # ---- Trim to common length ---- common_len = min(len(rtl_i), len(py_i)) if common_len < 10: - print(f"ERROR: Too few common samples ({common_len})") return False rtl_i_trim = rtl_i[:common_len] @@ -302,18 +275,14 @@ def compare_scenario(scenario_name): py_q_trim = py_q[:common_len] # ---- Cross-correlation to find latency offset ---- - print(f"\nLatency alignment (cross-correlation, max lag=±{MAX_LATENCY_DRIFT}):") - lag_i, corr_i = cross_correlate_lag(rtl_i_trim, py_i_trim, + lag_i, _corr_i = cross_correlate_lag(rtl_i_trim, py_i_trim, max_lag=MAX_LATENCY_DRIFT) - lag_q, corr_q = cross_correlate_lag(rtl_q_trim, py_q_trim, + lag_q, _corr_q = cross_correlate_lag(rtl_q_trim, py_q_trim, max_lag=MAX_LATENCY_DRIFT) - print(f" I-channel: best lag={lag_i}, correlation={corr_i:.6f}") - print(f" Q-channel: best lag={lag_q}, correlation={corr_q:.6f}") # ---- Apply latency correction ---- best_lag = lag_i # Use I-channel lag (should be same as Q) if abs(lag_i - lag_q) > 1: - print(f" WARNING: I and Q latency offsets differ ({lag_i} vs {lag_q})") # Use the average best_lag = (lag_i + lag_q) // 2 @@ -341,32 +310,20 @@ def compare_scenario(scenario_name): aligned_py_i = aligned_py_i[:aligned_len] aligned_py_q = aligned_py_q[:aligned_len] - print(f" Applied lag correction: {best_lag} samples") - print(f" Aligned length: {aligned_len} samples") # ---- Error metrics (after alignment) ---- rms_i = compute_rms_error(aligned_rtl_i, aligned_py_i) rms_q = compute_rms_error(aligned_rtl_q, aligned_py_q) - max_err_i = compute_max_abs_error(aligned_rtl_i, aligned_py_i) - max_err_q = compute_max_abs_error(aligned_rtl_q, aligned_py_q) + compute_max_abs_error(aligned_rtl_i, aligned_py_i) + compute_max_abs_error(aligned_rtl_q, aligned_py_q) corr_i_aligned = compute_correlation(aligned_rtl_i, aligned_py_i) corr_q_aligned = compute_correlation(aligned_rtl_q, aligned_py_q) - print("\nError Metrics (after alignment):") - print(f" I-channel: RMS={rms_i:.2f} LSB, max={max_err_i} LSB, corr={corr_i_aligned:.6f}") - print(f" Q-channel: RMS={rms_q:.2f} LSB, max={max_err_q} LSB, corr={corr_q_aligned:.6f}") # ---- First/last sample comparison ---- - print("\nFirst 10 samples (after alignment):") - print( - f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} " - f"{'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}" - ) for k in range(min(10, aligned_len)): ei = aligned_rtl_i[k] - aligned_py_i[k] eq = aligned_rtl_q[k] - aligned_py_q[k] - print(f" {k:4d} {aligned_rtl_i[k]:8d} {aligned_py_i[k]:8d} {ei:6d} " - f"{aligned_rtl_q[k]:8d} {aligned_py_q[k]:8d} {eq:6d}") # ---- Write detailed comparison CSV ---- compare_csv_path = os.path.join(base_dir, f"compare_{scenario_name}.csv") @@ -377,7 +334,6 @@ def compare_scenario(scenario_name): eq = aligned_rtl_q[k] - aligned_py_q[k] f.write(f"{k},{aligned_rtl_i[k]},{aligned_py_i[k]},{ei}," f"{aligned_rtl_q[k]},{aligned_py_q[k]},{eq}\n") - print(f"\nDetailed comparison written to: {compare_csv_path}") # ---- Pass/Fail ---- max_rms = cfg.get('max_rms', MAX_RMS_ERROR_LSB) @@ -443,21 +399,15 @@ def compare_scenario(scenario_name): f"|{best_lag}| <= {MAX_LATENCY_DRIFT}")) # ---- Report ---- - print(f"\n{'─' * 60}") - print("PASS/FAIL Results:") all_pass = True - for name, ok, detail in results: - mark = "[PASS]" if ok else "[FAIL]" - print(f" {mark} {name}: {detail}") + for _name, ok, _detail in results: if not ok: all_pass = False - print(f"\n{'=' * 60}") if all_pass: - print(f"SCENARIO {scenario_name.upper()}: ALL CHECKS PASSED") + pass else: - print(f"SCENARIO {scenario_name.upper()}: SOME CHECKS FAILED") - print(f"{'=' * 60}") + pass return all_pass @@ -481,25 +431,18 @@ def main(): pass_count += 1 else: overall_pass = False - print() else: - print(f"Skipping {name}: RTL CSV not found ({cfg['rtl_csv']})") + pass - print("=" * 60) - print(f"OVERALL: {pass_count}/{run_count} scenarios passed") if overall_pass: - print("ALL SCENARIOS PASSED") + pass else: - print("SOME SCENARIOS FAILED") - print("=" * 60) + pass return 0 if overall_pass else 1 - else: - ok = compare_scenario(scenario) - return 0 if ok else 1 - else: - # Default: DC - ok = compare_scenario('dc') + ok = compare_scenario(scenario) return 0 if ok else 1 + ok = compare_scenario('dc') + return 0 if ok else 1 if __name__ == '__main__': diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare_dc.csv b/9_Firmware/9_2_FPGA/tb/cosim/compare_dc.csv index eaed6a6..e7c5de9 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare_dc.csv +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare_dc.csv @@ -4085,4 +4085,3 @@ idx,rtl_i,py_i,err_i,rtl_q,py_q,err_q 4083,21,20,1,-6,-6,0 4084,20,21,-1,-6,-6,0 4085,20,20,0,-5,-6,1 -4086,20,20,0,-5,-5,0 diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare_doppler.py b/9_Firmware/9_2_FPGA/tb/cosim/compare_doppler.py index 3379ca5..56e0969 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare_doppler.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare_doppler.py @@ -73,7 +73,7 @@ def load_doppler_csv(filepath): Returns dict: {rbin: [(dbin, i, q), ...]} """ data = {} - with open(filepath, 'r') as f: + with open(filepath) as f: f.readline() # Skip header for line in f: line = line.strip() @@ -117,7 +117,7 @@ def pearson_correlation(a, b): def magnitude_l1(i_arr, q_arr): """L1 magnitude: |I| + |Q|.""" - return [abs(i) + abs(q) for i, q in zip(i_arr, q_arr)] + return [abs(i) + abs(q) for i, q in zip(i_arr, q_arr, strict=False)] def find_peak_bin(i_arr, q_arr): @@ -143,7 +143,7 @@ def total_energy(data_dict): """Sum of I^2 + Q^2 across all range bins and Doppler bins.""" total = 0 for rbin in data_dict: - for (dbin, i_val, q_val) in data_dict[rbin]: + for (_dbin, i_val, q_val) in data_dict[rbin]: total += i_val * i_val + q_val * q_val return total @@ -154,44 +154,30 @@ def total_energy(data_dict): def compare_scenario(name, config, base_dir): """Compare one Doppler scenario. Returns (passed, result_dict).""" - print(f"\n{'='*60}") - print(f"Scenario: {name} — {config['description']}") - print(f"{'='*60}") golden_path = os.path.join(base_dir, config['golden_csv']) rtl_path = os.path.join(base_dir, config['rtl_csv']) if not os.path.exists(golden_path): - print(f" ERROR: Golden CSV not found: {golden_path}") - print(" Run: python3 gen_doppler_golden.py") return False, {} if not os.path.exists(rtl_path): - print(f" ERROR: RTL CSV not found: {rtl_path}") - print(" Run the Verilog testbench first") return False, {} py_data = load_doppler_csv(golden_path) rtl_data = load_doppler_csv(rtl_path) - py_rbins = sorted(py_data.keys()) - rtl_rbins = sorted(rtl_data.keys()) + sorted(py_data.keys()) + sorted(rtl_data.keys()) - print(f" Python: {len(py_rbins)} range bins, " - f"{sum(len(v) for v in py_data.values())} total samples") - print(f" RTL: {len(rtl_rbins)} range bins, " - f"{sum(len(v) for v in rtl_data.values())} total samples") # ---- Check 1: Both have data ---- py_total = sum(len(v) for v in py_data.values()) rtl_total = sum(len(v) for v in rtl_data.values()) if py_total == 0 or rtl_total == 0: - print(" ERROR: One or both outputs are empty") return False, {} # ---- Check 2: Output count ---- count_ok = (rtl_total == TOTAL_OUTPUTS) - print(f"\n Output count: RTL={rtl_total}, expected={TOTAL_OUTPUTS} " - f"{'OK' if count_ok else 'MISMATCH'}") # ---- Check 3: Global energy ---- py_energy = total_energy(py_data) @@ -201,10 +187,6 @@ def compare_scenario(name, config, base_dir): else: energy_ratio = 1.0 if rtl_energy == 0 else float('inf') - print("\n Global energy:") - print(f" Python: {py_energy}") - print(f" RTL: {rtl_energy}") - print(f" Ratio: {energy_ratio:.4f}") # ---- Check 4: Per-range-bin analysis ---- peak_agreements = 0 @@ -236,8 +218,8 @@ def compare_scenario(name, config, base_dir): i_correlations.append(corr_i) q_correlations.append(corr_q) - py_rbin_energy = sum(i*i + q*q for i, q in zip(py_i, py_q)) - rtl_rbin_energy = sum(i*i + q*q for i, q in zip(rtl_i, rtl_q)) + py_rbin_energy = sum(i*i + q*q for i, q in zip(py_i, py_q, strict=False)) + rtl_rbin_energy = sum(i*i + q*q for i, q in zip(rtl_i, rtl_q, strict=False)) peak_details.append({ 'rbin': rbin, @@ -255,20 +237,11 @@ def compare_scenario(name, config, base_dir): avg_corr_i = sum(i_correlations) / len(i_correlations) avg_corr_q = sum(q_correlations) / len(q_correlations) - print("\n Per-range-bin metrics:") - print(f" Peak Doppler bin agreement (+/-1 within sub-frame): {peak_agreements}/{RANGE_BINS} " - f"({peak_agreement_frac:.0%})") - print(f" Avg magnitude correlation: {avg_mag_corr:.4f}") - print(f" Avg I-channel correlation: {avg_corr_i:.4f}") - print(f" Avg Q-channel correlation: {avg_corr_q:.4f}") # Show top 5 range bins by Python energy - print("\n Top 5 range bins by Python energy:") top_rbins = sorted(peak_details, key=lambda x: -x['py_energy'])[:5] - for d in top_rbins: - print(f" rbin={d['rbin']:2d}: py_peak={d['py_peak']:2d}, " - f"rtl_peak={d['rtl_peak']:2d}, mag_corr={d['mag_corr']:.3f}, " - f"I_corr={d['corr_i']:.3f}, Q_corr={d['corr_q']:.3f}") + for _d in top_rbins: + pass # ---- Pass/Fail ---- checks = [] @@ -291,11 +264,8 @@ def compare_scenario(name, config, base_dir): checks.append((f'High-energy rbin avg mag_corr >= {MAG_CORR_MIN:.2f} ' f'(actual={he_mag_corr:.3f})', he_ok)) - print("\n Pass/Fail Checks:") all_pass = True - for check_name, passed in checks: - status = "PASS" if passed else "FAIL" - print(f" [{status}] {check_name}") + for _check_name, passed in checks: if not passed: all_pass = False @@ -310,7 +280,6 @@ def compare_scenario(name, config, base_dir): f.write(f'{rbin},{dbin},{py_i[dbin]},{py_q[dbin]},' f'{rtl_i[dbin]},{rtl_q[dbin]},' f'{rtl_i[dbin]-py_i[dbin]},{rtl_q[dbin]-py_q[dbin]}\n') - print(f"\n Detailed comparison: {compare_csv}") result = { 'scenario': name, @@ -333,25 +302,15 @@ def compare_scenario(name, config, base_dir): def main(): base_dir = os.path.dirname(os.path.abspath(__file__)) - if len(sys.argv) > 1: - arg = sys.argv[1].lower() - else: - arg = 'stationary' + arg = sys.argv[1].lower() if len(sys.argv) > 1 else 'stationary' if arg == 'all': run_scenarios = list(SCENARIOS.keys()) elif arg in SCENARIOS: run_scenarios = [arg] else: - print(f"Unknown scenario: {arg}") - print(f"Valid: {', '.join(SCENARIOS.keys())}, all") sys.exit(1) - print("=" * 60) - print("Doppler Processor Co-Simulation Comparison") - print("RTL vs Python model (clean, no pipeline bug replication)") - print(f"Scenarios: {', '.join(run_scenarios)}") - print("=" * 60) results = [] for name in run_scenarios: @@ -359,37 +318,20 @@ def main(): results.append((name, passed, result)) # Summary - print(f"\n{'='*60}") - print("SUMMARY") - print(f"{'='*60}") - print(f"\n {'Scenario':<15} {'Energy Ratio':>13} {'Mag Corr':>10} " - f"{'Peak Agree':>11} {'I Corr':>8} {'Q Corr':>8} {'Status':>8}") - print(f" {'-'*15} {'-'*13} {'-'*10} {'-'*11} {'-'*8} {'-'*8} {'-'*8}") all_pass = True - for name, passed, result in results: + for _name, passed, result in results: if not result: - print(f" {name:<15} {'ERROR':>13} {'—':>10} {'—':>11} " - f"{'—':>8} {'—':>8} {'FAIL':>8}") all_pass = False else: - status = "PASS" if passed else "FAIL" - print(f" {name:<15} {result['energy_ratio']:>13.4f} " - f"{result['avg_mag_corr']:>10.4f} " - f"{result['peak_agreement']:>10.0%} " - f"{result['avg_corr_i']:>8.4f} " - f"{result['avg_corr_q']:>8.4f} " - f"{status:>8}") if not passed: all_pass = False - print() if all_pass: - print("ALL TESTS PASSED") + pass else: - print("SOME TESTS FAILED") - print(f"{'='*60}") + pass sys.exit(0 if all_pass else 1) diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare_mf.py b/9_Firmware/9_2_FPGA/tb/cosim/compare_mf.py index 5269e94..c766a1d 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare_mf.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare_mf.py @@ -79,7 +79,7 @@ def load_csv(filepath): """Load CSV with columns (bin, out_i/range_profile_i, out_q/range_profile_q).""" vals_i = [] vals_q = [] - with open(filepath, 'r') as f: + with open(filepath) as f: f.readline() # Skip header for line in f: line = line.strip() @@ -93,17 +93,17 @@ def load_csv(filepath): def magnitude_spectrum(vals_i, vals_q): """Compute magnitude = |I| + |Q| for each bin (L1 norm, matches RTL).""" - return [abs(i) + abs(q) for i, q in zip(vals_i, vals_q)] + return [abs(i) + abs(q) for i, q in zip(vals_i, vals_q, strict=False)] def magnitude_l2(vals_i, vals_q): """Compute magnitude = sqrt(I^2 + Q^2) for each bin.""" - return [math.sqrt(i*i + q*q) for i, q in zip(vals_i, vals_q)] + return [math.sqrt(i*i + q*q) for i, q in zip(vals_i, vals_q, strict=False)] def total_energy(vals_i, vals_q): """Compute total energy (sum of I^2 + Q^2).""" - return sum(i*i + q*q for i, q in zip(vals_i, vals_q)) + return sum(i*i + q*q for i, q in zip(vals_i, vals_q, strict=False)) def rms_magnitude(vals_i, vals_q): @@ -111,7 +111,7 @@ def rms_magnitude(vals_i, vals_q): n = len(vals_i) if n == 0: return 0.0 - return math.sqrt(sum(i*i + q*q for i, q in zip(vals_i, vals_q)) / n) + return math.sqrt(sum(i*i + q*q for i, q in zip(vals_i, vals_q, strict=False)) / n) def pearson_correlation(a, b): @@ -144,7 +144,7 @@ def find_peak(vals_i, vals_q): def top_n_peaks(mags, n=10): """Find the top-N peak bins by magnitude. Returns set of bin indices.""" indexed = sorted(enumerate(mags), key=lambda x: -x[1]) - return set(idx for idx, _ in indexed[:n]) + return {idx for idx, _ in indexed[:n]} def spectral_peak_overlap(mags_a, mags_b, n=10): @@ -163,30 +163,20 @@ def spectral_peak_overlap(mags_a, mags_b, n=10): def compare_scenario(scenario_name, config, base_dir): """Compare one scenario. Returns (pass/fail, result_dict).""" - print(f"\n{'='*60}") - print(f"Scenario: {scenario_name} — {config['description']}") - print(f"{'='*60}") golden_path = os.path.join(base_dir, config['golden_csv']) rtl_path = os.path.join(base_dir, config['rtl_csv']) if not os.path.exists(golden_path): - print(f" ERROR: Golden CSV not found: {golden_path}") - print(" Run: python3 gen_mf_cosim_golden.py") return False, {} if not os.path.exists(rtl_path): - print(f" ERROR: RTL CSV not found: {rtl_path}") - print(" Run the RTL testbench first") return False, {} py_i, py_q = load_csv(golden_path) rtl_i, rtl_q = load_csv(rtl_path) - print(f" Python model: {len(py_i)} samples") - print(f" RTL output: {len(rtl_i)} samples") if len(py_i) != FFT_SIZE or len(rtl_i) != FFT_SIZE: - print(f" ERROR: Expected {FFT_SIZE} samples from each") return False, {} # ---- Metric 1: Energy ---- @@ -205,28 +195,17 @@ def compare_scenario(scenario_name, config, base_dir): energy_ratio = float('inf') if py_energy == 0 else 0.0 rms_ratio = float('inf') if py_rms == 0 else 0.0 - print("\n Energy:") - print(f" Python total energy: {py_energy}") - print(f" RTL total energy: {rtl_energy}") - print(f" Energy ratio (RTL/Py): {energy_ratio:.4f}") - print(f" Python RMS: {py_rms:.2f}") - print(f" RTL RMS: {rtl_rms:.2f}") - print(f" RMS ratio (RTL/Py): {rms_ratio:.4f}") # ---- Metric 2: Peak location ---- - py_peak_bin, py_peak_mag = find_peak(py_i, py_q) - rtl_peak_bin, rtl_peak_mag = find_peak(rtl_i, rtl_q) + py_peak_bin, _py_peak_mag = find_peak(py_i, py_q) + rtl_peak_bin, _rtl_peak_mag = find_peak(rtl_i, rtl_q) - print("\n Peak location:") - print(f" Python: bin={py_peak_bin}, mag={py_peak_mag}") - print(f" RTL: bin={rtl_peak_bin}, mag={rtl_peak_mag}") # ---- Metric 3: Magnitude spectrum correlation ---- py_mag = magnitude_l2(py_i, py_q) rtl_mag = magnitude_l2(rtl_i, rtl_q) mag_corr = pearson_correlation(py_mag, rtl_mag) - print(f"\n Magnitude spectrum correlation: {mag_corr:.6f}") # ---- Metric 4: Top-N peak overlap ---- # Use L1 magnitudes for peak finding (matches RTL) @@ -235,16 +214,11 @@ def compare_scenario(scenario_name, config, base_dir): peak_overlap_10 = spectral_peak_overlap(py_mag_l1, rtl_mag_l1, n=10) peak_overlap_20 = spectral_peak_overlap(py_mag_l1, rtl_mag_l1, n=20) - print(f" Top-10 peak overlap: {peak_overlap_10:.2%}") - print(f" Top-20 peak overlap: {peak_overlap_20:.2%}") # ---- Metric 5: I and Q channel correlation ---- corr_i = pearson_correlation(py_i, rtl_i) corr_q = pearson_correlation(py_q, rtl_q) - print("\n Channel correlation:") - print(f" I-channel: {corr_i:.6f}") - print(f" Q-channel: {corr_q:.6f}") # ---- Pass/Fail Decision ---- # The SIMULATION branch uses floating-point twiddles ($cos/$sin) while @@ -278,11 +252,8 @@ def compare_scenario(scenario_name, config, base_dir): energy_ok)) # Print checks - print("\n Pass/Fail Checks:") all_pass = True - for name, passed in checks: - status = "PASS" if passed else "FAIL" - print(f" [{status}] {name}") + for _name, passed in checks: if not passed: all_pass = False @@ -310,7 +281,6 @@ def compare_scenario(scenario_name, config, base_dir): f.write(f'{k},{py_i[k]},{py_q[k]},{rtl_i[k]},{rtl_q[k]},' f'{py_mag_l1[k]},{rtl_mag_l1[k]},' f'{rtl_i[k]-py_i[k]},{rtl_q[k]-py_q[k]}\n') - print(f"\n Detailed comparison: {compare_csv}") return all_pass, result @@ -322,25 +292,15 @@ def compare_scenario(scenario_name, config, base_dir): def main(): base_dir = os.path.dirname(os.path.abspath(__file__)) - if len(sys.argv) > 1: - arg = sys.argv[1].lower() - else: - arg = 'chirp' + arg = sys.argv[1].lower() if len(sys.argv) > 1 else 'chirp' if arg == 'all': run_scenarios = list(SCENARIOS.keys()) elif arg in SCENARIOS: run_scenarios = [arg] else: - print(f"Unknown scenario: {arg}") - print(f"Valid: {', '.join(SCENARIOS.keys())}, all") sys.exit(1) - print("=" * 60) - print("Matched Filter Co-Simulation Comparison") - print("RTL (synthesis branch) vs Python model (bit-accurate)") - print(f"Scenarios: {', '.join(run_scenarios)}") - print("=" * 60) results = [] for name in run_scenarios: @@ -348,37 +308,20 @@ def main(): results.append((name, passed, result)) # Summary - print(f"\n{'='*60}") - print("SUMMARY") - print(f"{'='*60}") - print(f"\n {'Scenario':<12} {'Energy Ratio':>13} {'Mag Corr':>10} " - f"{'Peak Ovlp':>10} {'Py Peak':>8} {'RTL Peak':>9} {'Status':>8}") - print(f" {'-'*12} {'-'*13} {'-'*10} {'-'*10} {'-'*8} {'-'*9} {'-'*8}") all_pass = True - for name, passed, result in results: + for _name, passed, result in results: if not result: - print(f" {name:<12} {'ERROR':>13} {'—':>10} {'—':>10} " - f"{'—':>8} {'—':>9} {'FAIL':>8}") all_pass = False else: - status = "PASS" if passed else "FAIL" - print(f" {name:<12} {result['energy_ratio']:>13.4f} " - f"{result['mag_corr']:>10.4f} " - f"{result['peak_overlap_10']:>9.0%} " - f"{result['py_peak_bin']:>8d} " - f"{result['rtl_peak_bin']:>9d} " - f"{status:>8}") if not passed: all_pass = False - print() if all_pass: - print("ALL TESTS PASSED") + pass else: - print("SOME TESTS FAILED") - print(f"{'='*60}") + pass sys.exit(0 if all_pass else 1) diff --git a/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py b/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py index ad98042..b412e10 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py @@ -50,7 +50,7 @@ def saturate(value, bits): return value -def arith_rshift(value, shift, width=None): +def arith_rshift(value, shift, _width=None): """Arithmetic right shift. Python >> on signed int is already arithmetic.""" return value >> shift @@ -129,10 +129,7 @@ class NCO: raw_index = lut_address & 0x3F # RTL: lut_index = (quadrant[0] ^ quadrant[1]) ? ~lut_address[5:0] : lut_address[5:0] - if (quadrant & 1) ^ ((quadrant >> 1) & 1): - lut_index = (~raw_index) & 0x3F - else: - lut_index = raw_index + lut_index = ~raw_index & 63 if quadrant & 1 ^ quadrant >> 1 & 1 else raw_index return quadrant, lut_index @@ -175,7 +172,7 @@ class NCO: # OLD phase_accum_reg (the value from the PREVIOUS call). # We stored self.phase_accum_reg at the start of this call as the # value from last cycle. So: - pass # phase_with_offset computed below from OLD values + # phase_with_offset computed below from OLD values # Compute all NBA assignments from OLD state: # Save old state for NBA evaluation @@ -195,16 +192,8 @@ class NCO: if phase_valid: # Stage 1 NBA: phase_accum_reg <= phase_accumulator (old value) - _new_phase_accum_reg = (self.phase_accumulator - ftw) & 0xFFFFFFFF # noqa: F841 — old accum before add (derivation reference) + _new_phase_accum_reg = (self.phase_accumulator - ftw) & 0xFFFFFFFF # Wait - let me re-derive. The Verilog is: - # phase_accumulator <= phase_accumulator + frequency_tuning_word; - # phase_accum_reg <= phase_accumulator; // OLD value (NBA) - # phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0}; - # // OLD phase_accum_reg - # Since all are NBA (<=), they all read the values from BEFORE this edge. - # So: new_phase_accumulator = old_phase_accumulator + ftw - # new_phase_accum_reg = old_phase_accumulator - # new_phase_with_offset = old_phase_accum_reg + offset old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct self.phase_accum_reg = old_phase_accumulator self.phase_with_offset = ( @@ -706,7 +695,6 @@ class DDCInputInterface: if old_valid_sync: ddc_i = sign_extend(ddc_i_18 & 0x3FFFF, 18) ddc_q = sign_extend(ddc_q_18 & 0x3FFFF, 18) - # adc_i = ddc_i[17:2] + ddc_i[1] (rounding) trunc_i = (ddc_i >> 2) & 0xFFFF # bits [17:2] round_i = (ddc_i >> 1) & 1 # bit [1] trunc_q = (ddc_q >> 2) & 0xFFFF @@ -732,7 +720,7 @@ def load_twiddle_rom(filepath=None): filepath = os.path.join(base, '..', '..', 'fft_twiddle_1024.mem') values = [] - with open(filepath, 'r') as f: + with open(filepath) as f: for line in f: line = line.strip() if not line or line.startswith('//'): @@ -760,12 +748,11 @@ def _twiddle_lookup(k, n, cos_rom): if k == 0: return cos_rom[0], 0 - elif k == n4: + if k == n4: return 0, cos_rom[0] - elif k < n4: + if k < n4: return cos_rom[k], cos_rom[n4 - k] - else: - return sign_extend((-cos_rom[n2 - k]) & 0xFFFF, 16), cos_rom[k - n4] + return sign_extend((-cos_rom[n2 - k]) & 0xFFFF, 16), cos_rom[k - n4] class FFTEngine: @@ -840,11 +827,9 @@ class FFTEngine: # Multiply (49-bit products) if not inverse: - # Forward: t = b * (cos + j*sin) prod_re = b_re * tw_cos + b_im * tw_sin prod_im = b_im * tw_cos - b_re * tw_sin else: - # Inverse: t = b * (cos - j*sin) prod_re = b_re * tw_cos - b_im * tw_sin prod_im = b_im * tw_cos + b_re * tw_sin @@ -923,10 +908,9 @@ class FreqMatchedFilter: # Saturation check if rounded > 0x3FFF8000: return 0x7FFF - elif rounded < -0x3FFF8000: + if rounded < -0x3FFF8000: return sign_extend(0x8000, 16) - else: - return sign_extend((rounded >> 15) & 0xFFFF, 16) + return sign_extend((rounded >> 15) & 0xFFFF, 16) out_re = round_sat_extract(real_sum) out_im = round_sat_extract(imag_sum) @@ -1061,7 +1045,6 @@ class RangeBinDecimator: out_im.append(best_im) elif mode == 2: - # Averaging: sum >> 4 sum_re = 0 sum_im = 0 for s in range(df): @@ -1351,69 +1334,48 @@ def _self_test(): """Quick sanity checks for each module.""" import math - print("=" * 60) - print("FPGA Model Self-Test") - print("=" * 60) # --- NCO test --- - print("\n--- NCO Test ---") nco = NCO() ftw = 0x4CCCCCCD # 120 MHz at 400 MSPS # Run 20 cycles to fill pipeline results = [] - for i in range(20): + for _ in range(20): s, c, ready = nco.step(ftw) if ready: results.append((s, c)) if results: - print(f" First valid output: sin={results[0][0]}, cos={results[0][1]}") - print(f" Got {len(results)} valid outputs from 20 cycles") # Check quadrature: sin^2 + cos^2 should be approximately 32767^2 s, c = results[-1] mag_sq = s * s + c * c expected = 32767 * 32767 - error_pct = abs(mag_sq - expected) / expected * 100 - print( - f" Quadrature check: sin^2+cos^2={mag_sq}, " - f"expected~{expected}, error={error_pct:.2f}%" - ) - print(" NCO: OK") + abs(mag_sq - expected) / expected * 100 # --- Mixer test --- - print("\n--- Mixer Test ---") mixer = Mixer() # Test with mid-scale ADC (128) and known cos/sin - for i in range(5): - mi, mq, mv = mixer.step(128, 0x7FFF, 0, True, True) - print(f" Mixer with adc=128, cos=max, sin=0: I={mi}, Q={mq}, valid={mv}") - print(" Mixer: OK") + for _ in range(5): + _mi, _mq, _mv = mixer.step(128, 0x7FFF, 0, True, True) # --- CIC test --- - print("\n--- CIC Test ---") cic = CICDecimator() dc_val = sign_extend(0x1000, 18) # Small positive DC out_count = 0 - for i in range(100): - out, valid = cic.step(dc_val, True) + for _ in range(100): + _, valid = cic.step(dc_val, True) if valid: out_count += 1 - print(f" CIC: {out_count} outputs from 100 inputs (expect ~25 with 4x decimation + pipeline)") - print(" CIC: OK") # --- FIR test --- - print("\n--- FIR Test ---") fir = FIRFilter() out_count = 0 - for i in range(50): - out, valid = fir.step(1000, True) + for _ in range(50): + _out, valid = fir.step(1000, True) if valid: out_count += 1 - print(f" FIR: {out_count} outputs from 50 inputs (expect ~43 with 7-cycle latency)") - print(" FIR: OK") # --- FFT test --- - print("\n--- FFT Test (1024-pt) ---") try: fft = FFTEngine(n=1024) # Single tone at bin 10 @@ -1425,43 +1387,28 @@ def _self_test(): out_re, out_im = fft.compute(in_re, in_im, inverse=False) # Find peak bin max_mag = 0 - peak_bin = 0 for i in range(512): mag = abs(out_re[i]) + abs(out_im[i]) if mag > max_mag: max_mag = mag - peak_bin = i - print(f" FFT peak at bin {peak_bin} (expected 10), magnitude={max_mag}") # IFFT roundtrip - rt_re, rt_im = fft.compute(out_re, out_im, inverse=True) - max_err = max(abs(rt_re[i] - in_re[i]) for i in range(1024)) - print(f" FFT->IFFT roundtrip max error: {max_err} LSBs") - print(" FFT: OK") + rt_re, _rt_im = fft.compute(out_re, out_im, inverse=True) + max(abs(rt_re[i] - in_re[i]) for i in range(1024)) except FileNotFoundError: - print(" FFT: SKIPPED (twiddle file not found)") + pass # --- Conjugate multiply test --- - print("\n--- Conjugate Multiply Test ---") # (1+j0) * conj(1+j0) = 1+j0 # In Q15: 32767 * 32767 -> should get close to 32767 - r, m = FreqMatchedFilter.conjugate_multiply_sample(0x7FFF, 0, 0x7FFF, 0) - print(f" (32767+j0) * conj(32767+j0) = {r}+j{m} (expect ~32767+j0)") + _r, _m = FreqMatchedFilter.conjugate_multiply_sample(0x7FFF, 0, 0x7FFF, 0) # (0+j32767) * conj(0+j32767) = (0+j32767)(0-j32767) = 32767^2 -> ~32767 - r2, m2 = FreqMatchedFilter.conjugate_multiply_sample(0, 0x7FFF, 0, 0x7FFF) - print(f" (0+j32767) * conj(0+j32767) = {r2}+j{m2} (expect ~32767+j0)") - print(" Conjugate Multiply: OK") + _r2, _m2 = FreqMatchedFilter.conjugate_multiply_sample(0, 0x7FFF, 0, 0x7FFF) # --- Range decimator test --- - print("\n--- Range Bin Decimator Test ---") test_re = list(range(1024)) test_im = [0] * 1024 out_re, out_im = RangeBinDecimator.decimate(test_re, test_im, mode=0) - print(f" Mode 0 (center): first 5 bins = {out_re[:5]} (expect [8, 24, 40, 56, 72])") - print(" Range Decimator: OK") - print("\n" + "=" * 60) - print("ALL SELF-TESTS PASSED") - print("=" * 60) if __name__ == '__main__': diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py index 33c76ee..8bec7b8 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py @@ -82,8 +82,8 @@ def generate_full_long_chirp(): for n in range(LONG_CHIRP_SAMPLES): t = n / FS_SYS phase = math.pi * chirp_rate * t * t - re_val = int(round(Q15_MAX * SCALE * math.cos(phase))) - im_val = int(round(Q15_MAX * SCALE * math.sin(phase))) + re_val = round(Q15_MAX * SCALE * math.cos(phase)) + im_val = round(Q15_MAX * SCALE * math.sin(phase)) chirp_i.append(max(-32768, min(32767, re_val))) chirp_q.append(max(-32768, min(32767, im_val))) @@ -105,8 +105,8 @@ def generate_short_chirp(): for n in range(SHORT_CHIRP_SAMPLES): t = n / FS_SYS phase = math.pi * chirp_rate * t * t - re_val = int(round(Q15_MAX * SCALE * math.cos(phase))) - im_val = int(round(Q15_MAX * SCALE * math.sin(phase))) + re_val = round(Q15_MAX * SCALE * math.cos(phase)) + im_val = round(Q15_MAX * SCALE * math.sin(phase)) chirp_i.append(max(-32768, min(32767, re_val))) chirp_q.append(max(-32768, min(32767, im_val))) @@ -126,40 +126,17 @@ def write_mem_file(filename, values): with open(path, 'w') as f: for v in values: f.write(to_hex16(v) + '\n') - print(f" Wrote {filename}: {len(values)} entries") def main(): - print("=" * 60) - print("AERIS-10 Chirp .mem File Generator") - print("=" * 60) - print() - print("Parameters:") - print(f" CHIRP_BW = {CHIRP_BW/1e6:.1f} MHz") - print(f" FS_SYS = {FS_SYS/1e6:.1f} MHz") - print(f" T_LONG_CHIRP = {T_LONG_CHIRP*1e6:.1f} us") - print(f" T_SHORT_CHIRP = {T_SHORT_CHIRP*1e6:.1f} us") - print(f" LONG_CHIRP_SAMPLES = {LONG_CHIRP_SAMPLES}") - print(f" SHORT_CHIRP_SAMPLES = {SHORT_CHIRP_SAMPLES}") - print(f" FFT_SIZE = {FFT_SIZE}") - print(f" Chirp rate (long) = {CHIRP_BW/T_LONG_CHIRP:.3e} Hz/s") - print(f" Chirp rate (short) = {CHIRP_BW/T_SHORT_CHIRP:.3e} Hz/s") - print(f" Q15 scale = {SCALE}") - print() # ---- Long chirp ---- - print("Generating full long chirp (3000 samples)...") long_i, long_q = generate_full_long_chirp() # Verify first sample matches generate_reference_chirp_q15() from radar_scene.py # (which only generates the first 1024 samples) - print(f" Sample[0]: I={long_i[0]:6d} Q={long_q[0]:6d}") - print(f" Sample[1023]: I={long_i[1023]:6d} Q={long_q[1023]:6d}") - print(f" Sample[2999]: I={long_i[2999]:6d} Q={long_q[2999]:6d}") # Segment into 4 x 1024 blocks - print() - print("Segmenting into 4 x 1024 blocks...") for seg in range(LONG_SEGMENTS): start = seg * FFT_SIZE end = start + FFT_SIZE @@ -177,27 +154,18 @@ def main(): seg_i.append(0) seg_q.append(0) - zero_count = FFT_SIZE - valid_count - print(f" Seg {seg}: indices [{start}:{end-1}], " - f"valid={valid_count}, zeros={zero_count}") + FFT_SIZE - valid_count write_mem_file(f"long_chirp_seg{seg}_i.mem", seg_i) write_mem_file(f"long_chirp_seg{seg}_q.mem", seg_q) # ---- Short chirp ---- - print() - print("Generating short chirp (50 samples)...") short_i, short_q = generate_short_chirp() - print(f" Sample[0]: I={short_i[0]:6d} Q={short_q[0]:6d}") - print(f" Sample[49]: I={short_i[49]:6d} Q={short_q[49]:6d}") write_mem_file("short_chirp_i.mem", short_i) write_mem_file("short_chirp_q.mem", short_q) # ---- Verification summary ---- - print() - print("=" * 60) - print("Verification:") # Cross-check seg0 against radar_scene.py generate_reference_chirp_q15() # That function generates exactly the first 1024 samples of the chirp @@ -206,39 +174,30 @@ def main(): for n in range(FFT_SIZE): t = n / FS_SYS phase = math.pi * chirp_rate * t * t - expected_i = max(-32768, min(32767, int(round(Q15_MAX * SCALE * math.cos(phase))))) - expected_q = max(-32768, min(32767, int(round(Q15_MAX * SCALE * math.sin(phase))))) + expected_i = max(-32768, min(32767, round(Q15_MAX * SCALE * math.cos(phase)))) + expected_q = max(-32768, min(32767, round(Q15_MAX * SCALE * math.sin(phase)))) if long_i[n] != expected_i or long_q[n] != expected_q: mismatches += 1 if mismatches == 0: - print(" [PASS] Seg0 matches radar_scene.py generate_reference_chirp_q15()") + pass else: - print(f" [FAIL] Seg0 has {mismatches} mismatches vs generate_reference_chirp_q15()") return 1 # Check magnitude envelope - max_mag = max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q)) - print(f" Max magnitude: {max_mag:.1f} (expected ~{Q15_MAX * SCALE:.1f})") - print(f" Magnitude ratio: {max_mag / (Q15_MAX * SCALE):.6f}") + max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q, strict=False)) # Check seg3 zero padding seg3_i_path = os.path.join(MEM_DIR, 'long_chirp_seg3_i.mem') - with open(seg3_i_path, 'r') as f: + with open(seg3_i_path) as f: seg3_lines = [line.strip() for line in f if line.strip()] nonzero_seg3 = sum(1 for line in seg3_lines if line != '0000') - print(f" Seg3 non-zero entries: {nonzero_seg3}/{len(seg3_lines)} " - f"(expected 0 since chirp ends at sample 2999)") if nonzero_seg3 == 0: - print(" [PASS] Seg3 is all zeros (chirp 3000 samples < seg3 start 3072)") + pass else: - print(f" [WARN] Seg3 has {nonzero_seg3} non-zero entries") + pass - print() - print(f"Generated 10 .mem files in {os.path.abspath(MEM_DIR)}") - print("Run validate_mem_files.py to do full validation.") - print("=" * 60) return 0 diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_doppler_golden.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_doppler_golden.py index f4fb3e2..61981a9 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_doppler_golden.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_doppler_golden.py @@ -51,7 +51,6 @@ def write_hex_32bit(filepath, samples): for (i_val, q_val) in samples: packed = ((q_val & 0xFFFF) << 16) | (i_val & 0xFFFF) f.write(f"{packed:08X}\n") - print(f" Wrote {len(samples)} packed samples to {filepath}") def write_csv(filepath, headers, *columns): @@ -61,7 +60,6 @@ def write_csv(filepath, headers, *columns): for i in range(len(columns[0])): row = ','.join(str(col[i]) for col in columns) f.write(row + '\n') - print(f" Wrote {len(columns[0])} rows to {filepath}") def write_hex_16bit(filepath, data): @@ -118,22 +116,19 @@ SCENARIOS = { def generate_scenario(name, targets, description, base_dir): """Generate input hex + golden output for one scenario.""" - print(f"\n{'='*60}") - print(f"Scenario: {name} — {description}") - print("Model: CLEAN (dual 16-pt FFT)") - print(f"{'='*60}") # Generate Doppler frame (32 chirps x 64 range bins) frame_i, frame_q = generate_doppler_frame(targets, seed=42) - print(f" Generated frame: {len(frame_i)} chirps x {len(frame_i[0])} range bins") # ---- Write input hex file (packed 32-bit: {Q, I}) ---- # RTL expects data streamed chirp-by-chirp: chirp0[rb0..rb63], chirp1[rb0..rb63], ... packed_samples = [] for chirp in range(CHIRPS_PER_FRAME): - for rb in range(RANGE_BINS): - packed_samples.append((frame_i[chirp][rb], frame_q[chirp][rb])) + packed_samples.extend( + (frame_i[chirp][rb], frame_q[chirp][rb]) + for rb in range(RANGE_BINS) + ) input_hex = os.path.join(base_dir, f"doppler_input_{name}.hex") write_hex_32bit(input_hex, packed_samples) @@ -142,8 +137,6 @@ def generate_scenario(name, targets, description, base_dir): dp = DopplerProcessor() doppler_i, doppler_q = dp.process_frame(frame_i, frame_q) - print(f" Doppler output: {len(doppler_i)} range bins x " - f"{len(doppler_i[0])} doppler bins (2 sub-frames x {DOPPLER_FFT_SIZE})") # ---- Write golden output CSV ---- # Format: range_bin, doppler_bin, out_i, out_q @@ -168,10 +161,9 @@ def generate_scenario(name, targets, description, base_dir): # ---- Write golden hex (for optional RTL $readmemh comparison) ---- golden_hex = os.path.join(base_dir, f"doppler_golden_py_{name}.hex") - write_hex_32bit(golden_hex, list(zip(flat_i, flat_q))) + write_hex_32bit(golden_hex, list(zip(flat_i, flat_q, strict=False))) # ---- Find peak per range bin ---- - print("\n Peak Doppler bins per range bin (top 5 by magnitude):") peak_info = [] for rbin in range(RANGE_BINS): mags = [abs(doppler_i[rbin][d]) + abs(doppler_q[rbin][d]) @@ -182,13 +174,11 @@ def generate_scenario(name, targets, description, base_dir): # Sort by magnitude descending, show top 5 peak_info.sort(key=lambda x: -x[2]) - for rbin, dbin, mag in peak_info[:5]: - i_val = doppler_i[rbin][dbin] - q_val = doppler_q[rbin][dbin] - sf = dbin // DOPPLER_FFT_SIZE - bin_in_sf = dbin % DOPPLER_FFT_SIZE - print(f" rbin={rbin:2d}, dbin={dbin:2d} (sf{sf}:{bin_in_sf:2d}), mag={mag:6d}, " - f"I={i_val:6d}, Q={q_val:6d}") + for rbin, dbin, _mag in peak_info[:5]: + doppler_i[rbin][dbin] + doppler_q[rbin][dbin] + dbin // DOPPLER_FFT_SIZE + dbin % DOPPLER_FFT_SIZE return { 'name': name, @@ -200,10 +190,6 @@ def generate_scenario(name, targets, description, base_dir): def main(): base_dir = os.path.dirname(os.path.abspath(__file__)) - print("=" * 60) - print("Doppler Processor Co-Sim Golden Reference Generator") - print(f"Architecture: dual {DOPPLER_FFT_SIZE}-pt FFT ({DOPPLER_TOTAL_BINS} total bins)") - print("=" * 60) scenarios_to_run = list(SCENARIOS.keys()) @@ -221,17 +207,9 @@ def main(): r = generate_scenario(name, targets, description, base_dir) results.append(r) - print(f"\n{'='*60}") - print("Summary:") - print(f"{'='*60}") - for r in results: - print(f" {r['name']:<15s} top peak: " - f"rbin={r['peak_info'][0][0]}, dbin={r['peak_info'][0][1]}, " - f"mag={r['peak_info'][0][2]}") + for _ in results: + pass - print(f"\nGenerated {len(results)} scenarios.") - print(f"Files written to: {base_dir}") - print("=" * 60) if __name__ == '__main__': diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_mf_cosim_golden.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_mf_cosim_golden.py index dc5eaea..2ac4de4 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_mf_cosim_golden.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_mf_cosim_golden.py @@ -36,7 +36,7 @@ FFT_SIZE = 1024 def load_hex_16bit(filepath): """Load 16-bit hex file (one value per line, with optional // comments).""" values = [] - with open(filepath, 'r') as f: + with open(filepath) as f: for line in f: line = line.strip() if not line or line.startswith('//'): @@ -75,7 +75,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir, Returns dict with case info and results. """ - print(f"\n--- {case_name}: {description} ---") assert len(sig_i) == FFT_SIZE, f"sig_i length {len(sig_i)} != {FFT_SIZE}" assert len(sig_q) == FFT_SIZE @@ -88,8 +87,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir, write_hex_16bit(os.path.join(outdir, f"mf_sig_{case_name}_q.hex"), sig_q) write_hex_16bit(os.path.join(outdir, f"mf_ref_{case_name}_i.hex"), ref_i) write_hex_16bit(os.path.join(outdir, f"mf_ref_{case_name}_q.hex"), ref_q) - print(f" Wrote input hex: mf_sig_{case_name}_{{i,q}}.hex, " - f"mf_ref_{case_name}_{{i,q}}.hex") # Run through bit-accurate Python model mf = MatchedFilterChain(fft_size=FFT_SIZE) @@ -104,9 +101,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir, peak_mag = mag peak_bin = k - print(f" Output: {len(out_i)} samples") - print(f" Peak bin: {peak_bin}, magnitude: {peak_mag}") - print(f" Peak I={out_i[peak_bin]}, Q={out_q[peak_bin]}") # Save golden output hex write_hex_16bit(os.path.join(outdir, f"mf_golden_py_i_{case_name}.hex"), out_i) @@ -135,10 +129,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir, def main(): base_dir = os.path.dirname(os.path.abspath(__file__)) - print("=" * 60) - print("Matched Filter Co-Sim Golden Reference Generator") - print("Using bit-accurate Python model (fpga_model.py)") - print("=" * 60) results = [] @@ -158,8 +148,7 @@ def main(): base_dir) results.append(r) else: - print("\nWARNING: bb_mf_test / ref_chirp hex files not found.") - print("Run radar_scene.py first.") + pass # ---- Case 2: DC autocorrelation ---- dc_val = 0x1000 # 4096 @@ -191,8 +180,8 @@ def main(): sig_q = [] for n in range(FFT_SIZE): angle = 2.0 * math.pi * k * n / FFT_SIZE - sig_i.append(saturate(int(round(amp * math.cos(angle))), 16)) - sig_q.append(saturate(int(round(amp * math.sin(angle))), 16)) + sig_i.append(saturate(round(amp * math.cos(angle)), 16)) + sig_q.append(saturate(round(amp * math.sin(angle)), 16)) ref_i = list(sig_i) ref_q = list(sig_q) r = generate_case("tone5", sig_i, sig_q, ref_i, ref_q, @@ -201,16 +190,9 @@ def main(): results.append(r) # ---- Summary ---- - print("\n" + "=" * 60) - print("Summary:") - print("=" * 60) - for r in results: - print(f" {r['case_name']:10s}: peak at bin {r['peak_bin']}, " - f"mag={r['peak_mag']}, I={r['peak_i']}, Q={r['peak_q']}") + for _ in results: + pass - print(f"\nGenerated {len(results)} golden reference cases.") - print("Files written to:", base_dir) - print("=" * 60) if __name__ == '__main__': diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py index dc7d732..bcd1fb1 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py @@ -5,7 +5,7 @@ gen_multiseg_golden.py Generate golden reference data for matched_filter_multi_segment co-simulation. Tests the overlap-save segmented convolution wrapper: - - Long chirp: 3072 samples (4 segments × 1024, with 128-sample overlap) + - Long chirp: 3072 samples (4 segments x 1024, with 128-sample overlap) - Short chirp: 50 samples zero-padded to 1024 (1 segment) The matched_filter_processing_chain is already verified bit-perfect. @@ -234,7 +234,6 @@ def generate_long_chirp_test(): # In radar_receiver_final.v, the DDC output is sign-extended: # .ddc_i({{2{adc_i_scaled[15]}}, adc_i_scaled}) # So 16-bit -> 18-bit sign-extend -> then multi_segment does: - # ddc_i[17:2] + ddc_i[1] # For sign-extended 18-bit from 16-bit: # ddc_i[17:2] = original 16-bit value (since bits [17:16] = sign extension) # ddc_i[1] = bit 1 of original value @@ -277,9 +276,6 @@ def generate_long_chirp_test(): out_re, out_im = mf_chain.process(seg_data_i, seg_data_q, ref_i, ref_q) segment_results.append((out_re, out_im)) - print(f" Segment {seg}: collected {buffer_write_ptr} buffer samples, " - f"total chirp samples = {chirp_samples_collected}, " - f"input_idx = {input_idx}") # Write hex files for the testbench out_dir = os.path.dirname(os.path.abspath(__file__)) @@ -317,7 +313,6 @@ def generate_long_chirp_test(): for b in range(1024): f.write(f'{seg},{b},{out_re[b]},{out_im[b]}\n') - print(f"\n Written {LONG_SEGMENTS * 1024} golden samples to {csv_path}") return TOTAL_SAMPLES, LONG_SEGMENTS, segment_results @@ -343,8 +338,8 @@ def generate_short_chirp_test(): # Zero-pad to 1024 (as RTL does in ST_ZERO_PAD) # Note: padding computed here for documentation; actual buffer uses buf_i/buf_q below - _padded_i = list(input_i) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) # noqa: F841 - _padded_q = list(input_q) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) # noqa: F841 + _padded_i = list(input_i) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) + _padded_q = list(input_q) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) # The buffer truncation: ddc_i[17:2] + ddc_i[1] # For data already 16-bit sign-extended to 18: result is (val >> 2) + bit1 @@ -381,7 +376,6 @@ def generate_short_chirp_test(): # Write hex files out_dir = os.path.dirname(os.path.abspath(__file__)) - # Input (18-bit) all_input_i_18 = [] all_input_q_18 = [] for n in range(SHORT_SAMPLES): @@ -403,19 +397,12 @@ def generate_short_chirp_test(): for b in range(1024): f.write(f'{b},{out_re[b]},{out_im[b]}\n') - print(f" Written 1024 short chirp golden samples to {csv_path}") return out_re, out_im if __name__ == '__main__': - print("=" * 60) - print("Multi-Segment Matched Filter Golden Reference Generator") - print("=" * 60) - print("\n--- Long Chirp (4 segments, overlap-save) ---") total_samples, num_segs, seg_results = generate_long_chirp_test() - print(f" Total input samples: {total_samples}") - print(f" Segments: {num_segs}") for seg in range(num_segs): out_re, out_im = seg_results[seg] @@ -427,9 +414,7 @@ if __name__ == '__main__': if mag > max_mag: max_mag = mag peak_bin = b - print(f" Seg {seg}: peak at bin {peak_bin}, magnitude {max_mag}") - print("\n--- Short Chirp (1 segment, zero-padded) ---") short_re, short_im = generate_short_chirp_test() max_mag = 0 peak_bin = 0 @@ -438,8 +423,3 @@ if __name__ == '__main__': if mag > max_mag: max_mag = mag peak_bin = b - print(f" Short chirp: peak at bin {peak_bin}, magnitude {max_mag}") - - print("\n" + "=" * 60) - print("ALL GOLDEN FILES GENERATED") - print("=" * 60) diff --git a/9_Firmware/9_2_FPGA/tb/cosim/radar_scene.py b/9_Firmware/9_2_FPGA/tb/cosim/radar_scene.py index c0187e3..205f9e3 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/radar_scene.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/radar_scene.py @@ -155,7 +155,7 @@ def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC): t = n / fs # Instantaneous frequency: f_if - chirp_bw/2 + chirp_rate * t # Phase: integral of 2*pi*f(t)*dt - _f_inst = f_if - chirp_bw / 2 + chirp_rate * t # noqa: F841 — documents instantaneous frequency formula + _f_inst = f_if - chirp_bw / 2 + chirp_rate * t phase = 2 * math.pi * (f_if - chirp_bw / 2) * t + math.pi * chirp_rate * t * t chirp_i.append(math.cos(phase)) chirp_q.append(math.sin(phase)) @@ -163,7 +163,7 @@ def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC): return chirp_i, chirp_q -def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC): +def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, _f_if=F_IF, _fs=FS_ADC): """ Generate a reference chirp in Q15 format for the matched filter. @@ -190,8 +190,8 @@ def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, f_if=F_IF, f # The beat frequency from a target at delay tau is: f_beat = chirp_rate * tau # Reference chirp is the TX chirp at baseband (zero delay) phase = math.pi * chirp_rate * t * t - re_val = int(round(32767 * 0.9 * math.cos(phase))) - im_val = int(round(32767 * 0.9 * math.sin(phase))) + re_val = round(32767 * 0.9 * math.cos(phase)) + im_val = round(32767 * 0.9 * math.sin(phase)) ref_re[n] = max(-32768, min(32767, re_val)) ref_im[n] = max(-32768, min(32767, im_val)) @@ -284,7 +284,7 @@ def generate_adc_samples(targets, n_samples, noise_stddev=3.0, # Quantize to 8-bit unsigned (0-255), centered at 128 adc_samples = [] for val in adc_float: - quantized = int(round(val + 128)) + quantized = round(val + 128) quantized = max(0, min(255, quantized)) adc_samples.append(quantized) @@ -346,8 +346,8 @@ def generate_baseband_samples(targets, n_samples_baseband, noise_stddev=0.5, bb_i = [] bb_q = [] for n in range(n_samples_baseband): - i_val = int(round(bb_i_float[n] + noise_stddev * rand_gaussian())) - q_val = int(round(bb_q_float[n] + noise_stddev * rand_gaussian())) + i_val = round(bb_i_float[n] + noise_stddev * rand_gaussian()) + q_val = round(bb_q_float[n] + noise_stddev * rand_gaussian()) bb_i.append(max(-32768, min(32767, i_val))) bb_q.append(max(-32768, min(32767, q_val))) @@ -398,15 +398,13 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME, for target in targets: # Which range bin does this target fall in? # After matched filter + range decimation: - # range_bin = target_delay_in_baseband_samples / decimation_factor delay_baseband_samples = target.delay_s * FS_SYS range_bin_float = delay_baseband_samples * n_range_bins / FFT_SIZE - range_bin = int(round(range_bin_float)) + range_bin = round(range_bin_float) if range_bin < 0 or range_bin >= n_range_bins: continue - # Amplitude (simplified) amp = target.amplitude / 4.0 # Doppler phase for this chirp. @@ -426,10 +424,7 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME, rb = range_bin + delta if 0 <= rb < n_range_bins: # sinc-like weighting - if delta == 0: - weight = 1.0 - else: - weight = 0.2 / abs(delta) + weight = 1.0 if delta == 0 else 0.2 / abs(delta) chirp_i[rb] += amp * weight * math.cos(total_phase) chirp_q[rb] += amp * weight * math.sin(total_phase) @@ -437,8 +432,8 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME, row_i = [] row_q = [] for rb in range(n_range_bins): - i_val = int(round(chirp_i[rb] + noise_stddev * rand_gaussian())) - q_val = int(round(chirp_q[rb] + noise_stddev * rand_gaussian())) + i_val = round(chirp_i[rb] + noise_stddev * rand_gaussian()) + q_val = round(chirp_q[rb] + noise_stddev * rand_gaussian()) row_i.append(max(-32768, min(32767, i_val))) row_q.append(max(-32768, min(32767, q_val))) @@ -466,7 +461,7 @@ def write_hex_file(filepath, samples, bits=8): with open(filepath, 'w') as f: f.write(f"// {len(samples)} samples, {bits}-bit, hex format for $readmemh\n") - for i, s in enumerate(samples): + for _i, s in enumerate(samples): if bits <= 8: val = s & 0xFF elif bits <= 16: @@ -477,7 +472,6 @@ def write_hex_file(filepath, samples, bits=8): val = s & ((1 << bits) - 1) f.write(fmt.format(val) + "\n") - print(f" Wrote {len(samples)} samples to {filepath}") def write_csv_file(filepath, columns, headers=None): @@ -497,7 +491,6 @@ def write_csv_file(filepath, columns, headers=None): row = [str(col[i]) for col in columns] f.write(",".join(row) + "\n") - print(f" Wrote {n_rows} rows to {filepath}") # ============================================================================= @@ -510,10 +503,6 @@ def scenario_single_target(range_m=500, velocity=0, rcs=0, n_adc_samples=16384): Good for validating matched filter range response. """ target = Target(range_m=range_m, velocity_mps=velocity, rcs_dbsm=rcs) - print(f"Scenario: Single target at {range_m}m") - print(f" {target}") - print(f" Beat freq: {CHIRP_BW / T_LONG_CHIRP * target.delay_s:.0f} Hz") - print(f" Delay: {target.delay_samples:.1f} ADC samples") adc = generate_adc_samples([target], n_adc_samples, noise_stddev=2.0) return adc, [target] @@ -528,9 +517,8 @@ def scenario_two_targets(n_adc_samples=16384): Target(range_m=300, velocity_mps=0, rcs_dbsm=10, phase_deg=0), Target(range_m=315, velocity_mps=0, rcs_dbsm=10, phase_deg=45), ] - print("Scenario: Two targets (range resolution test)") - for t in targets: - print(f" {t}") + for _t in targets: + pass adc = generate_adc_samples(targets, n_adc_samples, noise_stddev=2.0) return adc, targets @@ -547,9 +535,8 @@ def scenario_multi_target(n_adc_samples=16384): Target(range_m=2000, velocity_mps=50, rcs_dbsm=0, phase_deg=45), Target(range_m=5000, velocity_mps=-5, rcs_dbsm=-5, phase_deg=270), ] - print("Scenario: Multi-target (5 targets)") - for t in targets: - print(f" {t}") + for _t in targets: + pass adc = generate_adc_samples(targets, n_adc_samples, noise_stddev=3.0) return adc, targets @@ -559,7 +546,6 @@ def scenario_noise_only(n_adc_samples=16384, noise_stddev=5.0): """ Noise-only scene — baseline for false alarm characterization. """ - print(f"Scenario: Noise only (stddev={noise_stddev})") adc = generate_adc_samples([], n_adc_samples, noise_stddev=noise_stddev) return adc, [] @@ -568,7 +554,6 @@ def scenario_dc_tone(n_adc_samples=16384, adc_value=128): """ DC input — validates CIC decimation and DC response. """ - print(f"Scenario: DC tone (ADC value={adc_value})") return [adc_value] * n_adc_samples, [] @@ -576,11 +561,10 @@ def scenario_sine_wave(n_adc_samples=16384, freq_hz=1e6, amplitude=50): """ Pure sine wave at ADC input — validates NCO/mixer frequency response. """ - print(f"Scenario: Sine wave at {freq_hz/1e6:.1f} MHz, amplitude={amplitude}") adc = [] for n in range(n_adc_samples): t = n / FS_ADC - val = int(round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t))) + val = round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t)) adc.append(max(0, min(255, val))) return adc, [] @@ -606,46 +590,35 @@ def generate_all_test_vectors(output_dir=None): if output_dir is None: output_dir = os.path.dirname(os.path.abspath(__file__)) - print("=" * 60) - print("Generating AERIS-10 Test Vectors") - print(f"Output directory: {output_dir}") - print("=" * 60) n_adc = 16384 # ~41 us of ADC data # --- Scenario 1: Single target --- - print("\n--- Scenario 1: Single Target ---") adc1, targets1 = scenario_single_target(range_m=500, n_adc_samples=n_adc) write_hex_file(os.path.join(output_dir, "adc_single_target.hex"), adc1, bits=8) # --- Scenario 2: Multi-target --- - print("\n--- Scenario 2: Multi-Target ---") adc2, targets2 = scenario_multi_target(n_adc_samples=n_adc) write_hex_file(os.path.join(output_dir, "adc_multi_target.hex"), adc2, bits=8) # --- Scenario 3: Noise only --- - print("\n--- Scenario 3: Noise Only ---") adc3, _ = scenario_noise_only(n_adc_samples=n_adc) write_hex_file(os.path.join(output_dir, "adc_noise_only.hex"), adc3, bits=8) # --- Scenario 4: DC --- - print("\n--- Scenario 4: DC Input ---") adc4, _ = scenario_dc_tone(n_adc_samples=n_adc) write_hex_file(os.path.join(output_dir, "adc_dc.hex"), adc4, bits=8) # --- Scenario 5: Sine wave --- - print("\n--- Scenario 5: 1 MHz Sine ---") adc5, _ = scenario_sine_wave(n_adc_samples=n_adc, freq_hz=1e6, amplitude=50) write_hex_file(os.path.join(output_dir, "adc_sine_1mhz.hex"), adc5, bits=8) # --- Reference chirp for matched filter --- - print("\n--- Reference Chirp ---") ref_re, ref_im = generate_reference_chirp_q15() write_hex_file(os.path.join(output_dir, "ref_chirp_i.hex"), ref_re, bits=16) write_hex_file(os.path.join(output_dir, "ref_chirp_q.hex"), ref_im, bits=16) # --- Baseband samples for matched filter test (bypass DDC) --- - print("\n--- Baseband Samples (bypass DDC) ---") bb_targets = [ Target(range_m=500, velocity_mps=0, rcs_dbsm=10), Target(range_m=1500, velocity_mps=20, rcs_dbsm=5), @@ -655,7 +628,6 @@ def generate_all_test_vectors(output_dir=None): write_hex_file(os.path.join(output_dir, "bb_mf_test_q.hex"), bb_q, bits=16) # --- Scenario info CSV --- - print("\n--- Scenario Info ---") with open(os.path.join(output_dir, "scenario_info.txt"), 'w') as f: f.write("AERIS-10 Test Vector Scenarios\n") f.write("=" * 60 + "\n\n") @@ -685,11 +657,7 @@ def generate_all_test_vectors(output_dir=None): for t in bb_targets: f.write(f" {t}\n") - print(f"\n Wrote scenario info to {os.path.join(output_dir, 'scenario_info.txt')}") - print("\n" + "=" * 60) - print("ALL TEST VECTORS GENERATED") - print("=" * 60) return { 'adc_single': adc1, diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py b/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py index b8b8347..9b0ca86 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py @@ -69,7 +69,6 @@ FIR_COEFFS_HEX = [ # DDC output interface DDC_OUT_BITS = 16 # 18 → 16 bit with rounding + saturation -# FFT (Range) FFT_SIZE = 1024 FFT_DATA_W = 16 FFT_INTERNAL_W = 32 @@ -148,21 +147,15 @@ def load_and_quantize_adi_data(data_path, config_path, frame_idx=0): 4. Upconvert to 120 MHz IF (add I*cos - Q*sin) to create real signal 5. Quantize to 8-bit unsigned (matching AD9484) """ - print(f"[LOAD] Loading ADI dataset from {data_path}") data = np.load(data_path, allow_pickle=True) config = np.load(config_path, allow_pickle=True) - print(f" Shape: {data.shape}, dtype: {data.dtype}") - print(f" Config: sample_rate={config[0]:.0f}, IF={config[1]:.0f}, " - f"RF={config[2]:.0f}, chirps={config[3]:.0f}, BW={config[4]:.0f}, " - f"ramp={config[5]:.6f}s") # Extract one frame frame = data[frame_idx] # (256, 1079) complex # Use first 32 chirps, first 1024 samples iq_block = frame[:DOPPLER_CHIRPS, :FFT_SIZE] # (32, 1024) complex - print(f" Using frame {frame_idx}: {DOPPLER_CHIRPS} chirps x {FFT_SIZE} samples") # The ADI data is baseband complex IQ at 4 MSPS. # AERIS-10 sees a real signal at 400 MSPS with 120 MHz IF. @@ -197,9 +190,6 @@ def load_and_quantize_adi_data(data_path, config_path, frame_idx=0): iq_i = np.clip(iq_i, -32768, 32767) iq_q = np.clip(iq_q, -32768, 32767) - print(f" Scaled to 16-bit (peak target {INPUT_PEAK_TARGET}): " - f"I range [{iq_i.min()}, {iq_i.max()}], " - f"Q range [{iq_q.min()}, {iq_q.max()}]") # Also create 8-bit ADC stimulus for DDC validation # Use just one chirp of real-valued data (I channel only, shifted to unsigned) @@ -243,10 +233,7 @@ def nco_lookup(phase_accum, sin_lut): quadrant = (lut_address >> 6) & 0x3 # Mirror index for odd quadrants - if (quadrant & 1) ^ ((quadrant >> 1) & 1): - lut_idx = (~lut_address) & 0x3F - else: - lut_idx = lut_address & 0x3F + lut_idx = ~lut_address & 63 if quadrant & 1 ^ quadrant >> 1 & 1 else lut_address & 63 sin_abs = int(sin_lut[lut_idx]) cos_abs = int(sin_lut[63 - lut_idx]) @@ -294,7 +281,6 @@ def run_ddc(adc_samples): # Build FIR coefficients as signed integers fir_coeffs = np.array([hex_to_signed(c, 18) for c in FIR_COEFFS_HEX], dtype=np.int64) - print(f"[DDC] Processing {n_samples} ADC samples at 400 MHz") # --- NCO + Mixer --- phase_accum = np.int64(0) @@ -327,7 +313,6 @@ def run_ddc(adc_samples): # Phase accumulator update (ignore dithering for bit-accuracy) phase_accum = (phase_accum + NCO_PHASE_INC) & 0xFFFFFFFF - print(f" Mixer output: I range [{mixed_i.min()}, {mixed_i.max()}]") # --- CIC Decimator (5-stage, decimate-by-4) --- # Integrator section (at 400 MHz rate) @@ -371,7 +356,6 @@ def run_ddc(adc_samples): scaled = comb[CIC_STAGES - 1][k] >> CIC_GAIN_SHIFT cic_output[k] = saturate(scaled, CIC_OUT_BITS) - print(f" CIC output: {n_decimated} samples, range [{cic_output.min()}, {cic_output.max()}]") # --- FIR Filter (32-tap) --- delay_line = np.zeros(FIR_TAPS, dtype=np.int64) @@ -393,7 +377,6 @@ def run_ddc(adc_samples): if fir_output[k] >= (1 << 17): fir_output[k] -= (1 << 18) - print(f" FIR output: range [{fir_output.min()}, {fir_output.max()}]") # --- DDC Interface (18 → 16 bit) --- ddc_output = np.zeros(n_decimated, dtype=np.int64) @@ -410,7 +393,6 @@ def run_ddc(adc_samples): else: ddc_output[k] = saturate(trunc + round_bit, 16) - print(f" DDC output (16-bit): range [{ddc_output.min()}, {ddc_output.max()}]") return ddc_output @@ -421,7 +403,7 @@ def run_ddc(adc_samples): def load_twiddle_rom(twiddle_file): """Load the quarter-wave cosine ROM from .mem file.""" rom = [] - with open(twiddle_file, 'r') as f: + with open(twiddle_file) as f: for line in f: line = line.strip() if not line or line.startswith('//'): @@ -483,7 +465,6 @@ def run_range_fft(iq_i, iq_q, twiddle_file=None): # Generate twiddle factors if file not available cos_rom = np.round(32767 * np.cos(2 * np.pi * np.arange(N // 4) / N)).astype(np.int64) - print(f"[FFT] Running {N}-point range FFT (bit-accurate)") # Bit-reverse and sign-extend to 32-bit internal width def bit_reverse(val, bits): @@ -521,9 +502,6 @@ def run_range_fft(iq_i, iq_q, twiddle_file=None): b_re = mem_re[addr_odd] b_im = mem_im[addr_odd] - # Twiddle multiply: forward FFT - # prod_re = b_re * tw_cos + b_im * tw_sin - # prod_im = b_im * tw_cos - b_re * tw_sin prod_re = b_re * tw_cos + b_im * tw_sin prod_im = b_im * tw_cos - b_re * tw_sin @@ -546,8 +524,6 @@ def run_range_fft(iq_i, iq_q, twiddle_file=None): out_re[n] = saturate(mem_re[n], FFT_DATA_W) out_im[n] = saturate(mem_im[n], FFT_DATA_W) - print(f" FFT output: re range [{out_re.min()}, {out_re.max()}], " - f"im range [{out_im.min()}, {out_im.max()}]") return out_re, out_im @@ -582,11 +558,6 @@ def run_range_bin_decimator(range_fft_i, range_fft_q, decimated_i = np.zeros((n_chirps, output_bins), dtype=np.int64) decimated_q = np.zeros((n_chirps, output_bins), dtype=np.int64) - mode_str = 'peak' if mode == 1 else 'avg' if mode == 2 else 'simple' - print( - f"[DECIM] Decimating {n_in}→{output_bins} bins, mode={mode_str}, " - f"start_bin={start_bin}, {n_chirps} chirps" - ) for c in range(n_chirps): # Index into input, skip start_bin @@ -635,7 +606,7 @@ def run_range_bin_decimator(range_fft_i, range_fft_q, # Averaging: sum group, then >> 4 (divide by 16) sum_i = np.int64(0) sum_q = np.int64(0) - for s in range(decimation_factor): + for _ in range(decimation_factor): if in_idx >= input_bins: break sum_i += int(range_fft_i[c, in_idx]) @@ -645,9 +616,6 @@ def run_range_bin_decimator(range_fft_i, range_fft_q, decimated_i[c, obin] = int(sum_i) >> 4 decimated_q[c, obin] = int(sum_q) >> 4 - print(f" Decimated output: shape ({n_chirps}, {output_bins}), " - f"I range [{decimated_i.min()}, {decimated_i.max()}], " - f"Q range [{decimated_q.min()}, {decimated_q.max()}]") return decimated_i, decimated_q @@ -673,7 +641,6 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None): n_total = DOPPLER_TOTAL_BINS n_sf = CHIRPS_PER_SUBFRAME - print(f"[DOPPLER] Processing {n_range} range bins x {n_chirps} chirps → dual {n_fft}-point FFT") # Build 16-point Hamming window as signed 16-bit hamming = np.array([int(v) for v in HAMMING_Q15], dtype=np.int64) @@ -757,8 +724,6 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None): doppler_map_i[rbin, bin_offset + n] = saturate(mem_re[n], 16) doppler_map_q[rbin, bin_offset + n] = saturate(mem_im[n], 16) - print(f" Doppler map: shape ({n_range}, {n_total}), " - f"I range [{doppler_map_i.min()}, {doppler_map_i.max()}]") return doppler_map_i, doppler_map_q @@ -788,12 +753,10 @@ def run_mti_canceller(decim_i, decim_q, enable=True): mti_i = np.zeros_like(decim_i) mti_q = np.zeros_like(decim_q) - print(f"[MTI] 2-pulse canceller, enable={enable}, {n_chirps} chirps x {n_bins} bins") if not enable: mti_i[:] = decim_i mti_q[:] = decim_q - print(" Pass-through mode (MTI disabled)") return mti_i, mti_q for c in range(n_chirps): @@ -809,9 +772,6 @@ def run_mti_canceller(decim_i, decim_q, enable=True): mti_i[c, r] = saturate(diff_i, 16) mti_q[c, r] = saturate(diff_q, 16) - print(" Chirp 0: muted (zeros)") - print(f" Chirps 1-{n_chirps-1}: I range [{mti_i[1:].min()}, {mti_i[1:].max()}], " - f"Q range [{mti_q[1:].min()}, {mti_q[1:].max()}]") return mti_i, mti_q @@ -838,17 +798,12 @@ def run_dc_notch(doppler_i, doppler_q, width=2): dc_notch_active = (width != 0) && (bin_within_sf < width || bin_within_sf > (15 - width + 1)) """ - n_range, n_doppler = doppler_i.shape + _n_range, n_doppler = doppler_i.shape notched_i = doppler_i.copy() notched_q = doppler_q.copy() - print( - f"[DC NOTCH] width={width}, {n_range} range bins x " - f"{n_doppler} Doppler bins (dual sub-frame)" - ) if width == 0: - print(" Pass-through (width=0)") return notched_i, notched_q zeroed_count = 0 @@ -860,7 +815,6 @@ def run_dc_notch(doppler_i, doppler_q, width=2): notched_q[:, dbin] = 0 zeroed_count += 1 - print(f" Zeroed {zeroed_count} Doppler bin columns") return notched_i, notched_q @@ -868,7 +822,7 @@ def run_dc_notch(doppler_i, doppler_q, width=2): # Stage 3e: CA-CFAR Detector (bit-accurate) # =========================================================================== def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8, - alpha_q44=0x30, mode='CA', simple_threshold=500): + alpha_q44=0x30, mode='CA', _simple_threshold=500): """ Bit-accurate model of cfar_ca.v — Cell-Averaging CFAR detector. @@ -906,9 +860,6 @@ def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8, if train == 0: train = 1 - print(f"[CFAR] mode={mode}, guard={guard}, train={train}, " - f"alpha=0x{alpha_q44:02X} (Q4.4={alpha_q44/16:.2f}), " - f"{n_range} range x {n_doppler} Doppler") # Compute magnitudes: |I| + |Q| (17-bit unsigned, matching RTL L1 norm) # RTL: abs_i = I[15] ? (~I + 1) : I; abs_q = Q[15] ? (~Q + 1) : Q @@ -976,29 +927,19 @@ def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8, else: noise_sum = leading_sum + lagging_sum # Default to CA - # Threshold = (alpha * noise_sum) >> ALPHA_FRAC_BITS - # RTL: noise_product = r_alpha * noise_sum_reg (31-bit) - # threshold = noise_product[ALPHA_FRAC_BITS +: MAG_WIDTH] - # saturate if overflow noise_product = alpha_q44 * noise_sum threshold_raw = noise_product >> ALPHA_FRAC_BITS # Saturate to MAG_WIDTH=17 bits MAX_MAG = (1 << 17) - 1 # 131071 - if threshold_raw > MAX_MAG: - threshold_val = MAX_MAG - else: - threshold_val = int(threshold_raw) + threshold_val = MAX_MAG if threshold_raw > MAX_MAG else int(threshold_raw) - # Detection: magnitude > threshold if int(col[cut_idx]) > threshold_val: detect_flags[cut_idx, dbin] = True total_detections += 1 thresholds[cut_idx, dbin] = threshold_val - print(f" Total detections: {total_detections}") - print(f" Magnitude range: [{magnitudes.min()}, {magnitudes.max()}]") return detect_flags, magnitudes, thresholds @@ -1012,19 +953,16 @@ def run_detection(doppler_i, doppler_q, threshold=10000): cfar_mag = |I| + |Q| (17-bit) detection if cfar_mag > threshold """ - print(f"[DETECT] Running magnitude threshold detection (threshold={threshold})") mag = np.abs(doppler_i) + np.abs(doppler_q) # L1 norm (|I| + |Q|) detections = np.argwhere(mag > threshold) - print(f" {len(detections)} detections found") for d in detections[:20]: # Print first 20 rbin, dbin = d - m = mag[rbin, dbin] - print(f" Range bin {rbin}, Doppler bin {dbin}: magnitude {m}") + mag[rbin, dbin] if len(detections) > 20: - print(f" ... and {len(detections) - 20} more") + pass return mag, detections @@ -1038,7 +976,6 @@ def run_float_reference(iq_i, iq_q): Uses the exact same RTL Hamming window coefficients (Q15) to isolate only the FFT fixed-point quantization error. """ - print("\n[FLOAT REF] Running floating-point reference pipeline") n_chirps, n_samples = iq_i.shape[0], iq_i.shape[1] if iq_i.ndim == 2 else len(iq_i) @@ -1086,8 +1023,6 @@ def write_hex_files(output_dir, iq_i, iq_q, prefix="stim"): fi.write(signed_to_hex(int(iq_i[n]), 16) + '\n') fq.write(signed_to_hex(int(iq_q[n]), 16) + '\n') - print(f" Wrote {fn_i} ({n_samples} samples)") - print(f" Wrote {fn_q} ({n_samples} samples)") elif iq_i.ndim == 2: n_rows, n_cols = iq_i.shape @@ -1101,8 +1036,6 @@ def write_hex_files(output_dir, iq_i, iq_q, prefix="stim"): fi.write(signed_to_hex(int(iq_i[r, c]), 16) + '\n') fq.write(signed_to_hex(int(iq_q[r, c]), 16) + '\n') - print(f" Wrote {fn_i} ({n_rows}x{n_cols} = {n_rows * n_cols} samples)") - print(f" Wrote {fn_q} ({n_rows}x{n_cols} = {n_rows * n_cols} samples)") def write_adc_hex(output_dir, adc_data, prefix="adc_stim"): @@ -1114,13 +1047,12 @@ def write_adc_hex(output_dir, adc_data, prefix="adc_stim"): for n in range(len(adc_data)): f.write(format(int(adc_data[n]) & 0xFF, '02X') + '\n') - print(f" Wrote {fn} ({len(adc_data)} samples)") # =========================================================================== # Comparison metrics # =========================================================================== -def compare_outputs(name, fixed_i, fixed_q, float_i, float_q): +def compare_outputs(_name, fixed_i, fixed_q, float_i, float_q): """Compare fixed-point outputs against floating-point reference. Reports two metrics: @@ -1136,7 +1068,7 @@ def compare_outputs(name, fixed_i, fixed_q, float_i, float_q): # Count saturated bins sat_mask = (np.abs(fi) >= 32767) | (np.abs(fq) >= 32767) - n_saturated = np.sum(sat_mask) + np.sum(sat_mask) # Complex error — overall fixed_complex = fi + 1j * fq @@ -1145,8 +1077,8 @@ def compare_outputs(name, fixed_i, fixed_q, float_i, float_q): signal_power = np.mean(np.abs(ref_complex) ** 2) + 1e-30 noise_power = np.mean(np.abs(error) ** 2) + 1e-30 - snr_db = 10 * np.log10(signal_power / noise_power) - max_error = np.max(np.abs(error)) + 10 * np.log10(signal_power / noise_power) + np.max(np.abs(error)) # Non-saturated comparison non_sat = ~sat_mask @@ -1155,17 +1087,10 @@ def compare_outputs(name, fixed_i, fixed_q, float_i, float_q): sig_ns = np.mean(np.abs(ref_complex[non_sat]) ** 2) + 1e-30 noise_ns = np.mean(np.abs(error_ns) ** 2) + 1e-30 snr_ns = 10 * np.log10(sig_ns / noise_ns) - max_err_ns = np.max(np.abs(error_ns)) + np.max(np.abs(error_ns)) else: snr_ns = 0.0 - max_err_ns = 0.0 - print(f"\n [{name}] Comparison ({n} points):") - print(f" Saturated: {n_saturated}/{n} ({100.0*n_saturated/n:.2f}%)") - print(f" Overall SNR: {snr_db:.1f} dB") - print(f" Overall max error: {max_error:.1f}") - print(f" Non-sat SNR: {snr_ns:.1f} dB") - print(f" Non-sat max error: {max_err_ns:.1f}") return snr_ns # Return the meaningful metric @@ -1198,29 +1123,19 @@ def main(): twiddle_1024 = os.path.join(fpga_dir, "fft_twiddle_1024.mem") output_dir = os.path.join(script_dir, "hex") - print("=" * 72) - print("AERIS-10 FPGA Golden Reference Model") - print("Using ADI CN0566 Phaser Radar Data (10.525 GHz X-band FMCW)") - print("=" * 72) # ----------------------------------------------------------------------- # Load and quantize ADI data # ----------------------------------------------------------------------- - iq_i, iq_q, adc_8bit, config = load_and_quantize_adi_data( + iq_i, iq_q, adc_8bit, _config = load_and_quantize_adi_data( amp_data, amp_config, frame_idx=args.frame ) # iq_i, iq_q: (32, 1024) int64, 16-bit range — post-DDC equivalent - print(f"\n{'=' * 72}") - print("Stage 0: Data loaded and quantized to 16-bit signed") - print(f" IQ block shape: ({iq_i.shape[0]}, {iq_i.shape[1]})") - print(f" ADC stimulus: {len(adc_8bit)} samples (8-bit unsigned)") # ----------------------------------------------------------------------- # Write stimulus files # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Writing hex stimulus files for RTL testbenches") # Post-DDC IQ for each chirp (for FFT + Doppler validation) write_hex_files(output_dir, iq_i, iq_q, "post_ddc") @@ -1234,8 +1149,6 @@ def main(): # ----------------------------------------------------------------------- # Run range FFT on first chirp (bit-accurate) # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Stage 2: Range FFT (1024-point, bit-accurate)") range_fft_i, range_fft_q = run_range_fft(iq_i[0], iq_q[0], twiddle_1024) write_hex_files(output_dir, range_fft_i, range_fft_q, "range_fft_chirp0") @@ -1243,20 +1156,16 @@ def main(): all_range_i = np.zeros((DOPPLER_CHIRPS, FFT_SIZE), dtype=np.int64) all_range_q = np.zeros((DOPPLER_CHIRPS, FFT_SIZE), dtype=np.int64) - print(f"\n Running range FFT for all {DOPPLER_CHIRPS} chirps...") for c in range(DOPPLER_CHIRPS): ri, rq = run_range_fft(iq_i[c], iq_q[c], twiddle_1024) all_range_i[c] = ri all_range_q[c] = rq if (c + 1) % 8 == 0: - print(f" Chirp {c + 1}/{DOPPLER_CHIRPS} done") + pass # ----------------------------------------------------------------------- # Run Doppler FFT (bit-accurate) — "direct" path (first 64 bins) # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Stage 3: Doppler FFT (dual 16-point with Hamming window)") - print(" [direct path: first 64 range bins, no decimation]") twiddle_16 = os.path.join(fpga_dir, "fft_twiddle_16.mem") doppler_i, doppler_q = run_doppler_fft(all_range_i, all_range_q, twiddle_file_16=twiddle_16) write_hex_files(output_dir, doppler_i, doppler_q, "doppler_map") @@ -1266,8 +1175,6 @@ def main(): # This models the actual RTL data flow: # range FFT → range_bin_decimator (peak detection) → Doppler # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Stage 2b: Range Bin Decimator (1024 → 64, peak detection)") decim_i, decim_q = run_range_bin_decimator( all_range_i, all_range_q, @@ -1287,14 +1194,11 @@ def main(): q_val = int(all_range_q[c, b]) & 0xFFFF packed = (q_val << 16) | i_val f.write(f"{packed:08X}\n") - print(f" Wrote {fc_input_file} ({DOPPLER_CHIRPS * FFT_SIZE} packed IQ words)") # Write decimated output reference for standalone decimator test write_hex_files(output_dir, decim_i, decim_q, "decimated_range") # Now run Doppler on the decimated data — this is the full-chain reference - print(f"\n{'=' * 72}") - print("Stage 3b: Doppler FFT on decimated data (full-chain path)") fc_doppler_i, fc_doppler_q = run_doppler_fft( decim_i, decim_q, twiddle_file_16=twiddle_16 ) @@ -1309,10 +1213,6 @@ def main(): q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF packed = (q_val << 16) | i_val f.write(f"{packed:08X}\n") - print( - f" Wrote {fc_doppler_packed_file} (" - f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)" - ) # Save numpy arrays for the full-chain path np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i) @@ -1325,16 +1225,12 @@ def main(): # This models the complete RTL data flow: # range FFT → decimator → MTI canceller → Doppler → DC notch → CFAR # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Stage 3c: MTI Canceller (2-pulse, on decimated data)") mti_i, mti_q = run_mti_canceller(decim_i, decim_q, enable=True) write_hex_files(output_dir, mti_i, mti_q, "fullchain_mti_ref") np.save(os.path.join(output_dir, "fullchain_mti_i.npy"), mti_i) np.save(os.path.join(output_dir, "fullchain_mti_q.npy"), mti_q) # Doppler on MTI-filtered data - print(f"\n{'=' * 72}") - print("Stage 3b+c: Doppler FFT on MTI-filtered decimated data") mti_doppler_i, mti_doppler_q = run_doppler_fft( mti_i, mti_q, twiddle_file_16=twiddle_16 ) @@ -1344,8 +1240,6 @@ def main(): # DC notch on MTI-Doppler data DC_NOTCH_WIDTH = 2 # Default test value: zero bins {0, 1, 31} - print(f"\n{'=' * 72}") - print(f"Stage 3d: DC Notch Filter (width={DC_NOTCH_WIDTH})") notched_i, notched_q = run_dc_notch(mti_doppler_i, mti_doppler_q, width=DC_NOTCH_WIDTH) write_hex_files(output_dir, notched_i, notched_q, "fullchain_notched_ref") @@ -1358,18 +1252,12 @@ def main(): q_val = int(notched_q[rbin, dbin]) & 0xFFFF packed = (q_val << 16) | i_val f.write(f"{packed:08X}\n") - print( - f" Wrote {fc_notched_packed_file} (" - f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)" - ) # CFAR on DC-notched data CFAR_GUARD = 2 CFAR_TRAIN = 8 CFAR_ALPHA = 0x30 # Q4.4 = 3.0 CFAR_MODE = 'CA' - print(f"\n{'=' * 72}") - print(f"Stage 3e: CA-CFAR (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})") cfar_flags, cfar_mag, cfar_thr = run_cfar_ca( notched_i, notched_q, guard=CFAR_GUARD, train=CFAR_TRAIN, @@ -1384,7 +1272,6 @@ def main(): for dbin in range(DOPPLER_TOTAL_BINS): m = int(cfar_mag[rbin, dbin]) & 0x1FFFF f.write(f"{m:05X}\n") - print(f" Wrote {cfar_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} mag values)") # 2. Threshold map (17-bit unsigned) cfar_thr_file = os.path.join(output_dir, "fullchain_cfar_thr.hex") @@ -1393,7 +1280,6 @@ def main(): for dbin in range(DOPPLER_TOTAL_BINS): t = int(cfar_thr[rbin, dbin]) & 0x1FFFF f.write(f"{t:05X}\n") - print(f" Wrote {cfar_thr_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} threshold values)") # 3. Detection flags (1-bit per cell) cfar_det_file = os.path.join(output_dir, "fullchain_cfar_det.hex") @@ -1402,7 +1288,6 @@ def main(): for dbin in range(DOPPLER_TOTAL_BINS): d = 1 if cfar_flags[rbin, dbin] else 0 f.write(f"{d:01X}\n") - print(f" Wrote {cfar_det_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} detection flags)") # 4. Detection list (text) cfar_detections = np.argwhere(cfar_flags) @@ -1418,7 +1303,6 @@ def main(): for det in cfar_detections: r, d = det f.write(f"{r} {d} {cfar_mag[r, d]} {cfar_thr[r, d]}\n") - print(f" Wrote {cfar_det_list_file} ({len(cfar_detections)} detections)") # Save numpy arrays np.save(os.path.join(output_dir, "fullchain_cfar_mag.npy"), cfar_mag) @@ -1426,8 +1310,6 @@ def main(): np.save(os.path.join(output_dir, "fullchain_cfar_flags.npy"), cfar_flags) # Run detection on full-chain Doppler map - print(f"\n{'=' * 72}") - print("Stage 4: Detection on full-chain Doppler map") fc_mag, fc_detections = run_detection(fc_doppler_i, fc_doppler_q, threshold=args.threshold) # Save full-chain detection reference @@ -1439,7 +1321,6 @@ def main(): for d in fc_detections: rbin, dbin = d f.write(f"{rbin} {dbin} {fc_mag[rbin, dbin]}\n") - print(f" Wrote {fc_det_file} ({len(fc_detections)} detections)") # Also write detection reference as hex for RTL comparison fc_det_mag_file = os.path.join(output_dir, "fullchain_detection_mag.hex") @@ -1448,13 +1329,10 @@ def main(): for dbin in range(DOPPLER_TOTAL_BINS): m = int(fc_mag[rbin, dbin]) & 0x1FFFF # 17-bit unsigned f.write(f"{m:05X}\n") - print(f" Wrote {fc_det_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} magnitude values)") # ----------------------------------------------------------------------- # Run detection on direct-path Doppler map (for backward compatibility) # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Stage 4b: Detection on direct-path Doppler map") mag, detections = run_detection(doppler_i, doppler_q, threshold=args.threshold) # Save detection list @@ -1466,26 +1344,23 @@ def main(): for d in detections: rbin, dbin = d f.write(f"{rbin} {dbin} {mag[rbin, dbin]}\n") - print(f" Wrote {det_file} ({len(detections)} detections)") # ----------------------------------------------------------------------- # Float reference and comparison # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("Comparison: Fixed-point vs Float reference") range_fft_float, doppler_float = run_float_reference(iq_i, iq_q) # Compare range FFT (chirp 0) float_range_i = np.real(range_fft_float[0, :]).astype(np.float64) float_range_q = np.imag(range_fft_float[0, :]).astype(np.float64) - snr_range = compare_outputs("Range FFT", range_fft_i, range_fft_q, + compare_outputs("Range FFT", range_fft_i, range_fft_q, float_range_i, float_range_q) # Compare Doppler map float_doppler_i = np.real(doppler_float).flatten().astype(np.float64) float_doppler_q = np.imag(doppler_float).flatten().astype(np.float64) - snr_doppler = compare_outputs("Doppler FFT", + compare_outputs("Doppler FFT", doppler_i.flatten(), doppler_q.flatten(), float_doppler_i, float_doppler_q) @@ -1497,32 +1372,10 @@ def main(): np.save(os.path.join(output_dir, "doppler_map_i.npy"), doppler_i) np.save(os.path.join(output_dir, "doppler_map_q.npy"), doppler_q) np.save(os.path.join(output_dir, "detection_mag.npy"), mag) - print(f"\n Saved numpy reference files to {output_dir}/") # ----------------------------------------------------------------------- # Summary # ----------------------------------------------------------------------- - print(f"\n{'=' * 72}") - print("SUMMARY") - print(f"{'=' * 72}") - print(f" ADI dataset: frame {args.frame} of amp_radar (CN0566, 10.525 GHz)") - print(f" Chirps processed: {DOPPLER_CHIRPS}") - print(f" Samples/chirp: {FFT_SIZE}") - print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float") - print( - f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming " - f"→ {snr_doppler:.1f} dB vs float" - ) - print(f" Detections (direct): {len(detections)} (threshold={args.threshold})") - print(" Full-chain decimator: 1024→64 peak detection") - print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})") - print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR") - print( - f" CFAR detections: {len(cfar_detections)} " - f"(guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})" - ) - print(f" Hex stimulus files: {output_dir}/") - print(" Ready for RTL co-simulation with Icarus Verilog") # ----------------------------------------------------------------------- # Optional plots @@ -1531,7 +1384,7 @@ def main(): try: import matplotlib.pyplot as plt - fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + _fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Range FFT magnitude (chirp 0) range_mag = np.sqrt(range_fft_i.astype(float)**2 + range_fft_q.astype(float)**2) @@ -1573,11 +1426,10 @@ def main(): plt.tight_layout() plot_file = os.path.join(output_dir, "golden_reference_plots.png") plt.savefig(plot_file, dpi=150) - print(f"\n Saved plots to {plot_file}") plt.show() except ImportError: - print("\n [WARN] matplotlib not available, skipping plots") + pass if __name__ == "__main__": diff --git a/9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv b/9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv index cce1e61..ad725f9 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv +++ b/9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv @@ -1,2049 +1,2049 @@ cycle,range_bin,doppler_bin,output_hex -1038395000,0,0,ffbc003a -1038405000,0,1,00840005 -1038415000,0,2,ff29ffad -1038425000,0,3,00d00059 -1038435000,0,4,fe7d016f -1038445000,0,5,02a8fc3d -1038455000,0,6,ff7103e1 -1038465000,0,7,fe2efd50 -1038475000,0,8,ff9401fa -1038485000,0,9,0366ff1e -1038495000,0,10,fac1fd9e -1038505000,0,11,05d606c2 -1038515000,0,12,fcd7faae -1038525000,0,13,00a20014 -1038535000,0,14,0010ffb3 -1038545000,0,15,003f020e -1038555000,0,16,ffacfda4 -1038565000,0,17,ff1e02fd -1038575000,0,18,01abfddd -1038585000,0,19,fee401df -1038595000,0,20,0065ffa5 -1038605000,0,21,ff8efd57 -1038615000,0,22,00910087 -1038625000,0,23,ff74ff24 -1038635000,0,24,fff004e0 -1038645000,0,25,0054fd38 -1038655000,0,26,003bfffc -1038665000,0,27,ff62003e -1038675000,0,28,0023ffd6 -1038685000,0,29,0084ff90 -1038695000,0,30,fff600c1 -1038705000,0,31,ffdbff66 -1042985000,1,0,ffc80043 -1042995000,1,1,fff7ffd4 -1043005000,1,2,fffeffad -1043015000,1,3,00870047 -1043025000,1,4,ff580088 -1043035000,1,5,0060fe86 -1043045000,1,6,ff0b00a0 -1043055000,1,7,00d7011a -1043065000,1,8,fdddffc5 -1043075000,1,9,02f200d5 -1043085000,1,10,ff46fc45 -1043095000,1,11,014a0506 -1043105000,1,12,fd06fcaa -1043115000,1,13,002aff01 -1043125000,1,14,0391035e -1043135000,1,15,fea3fbf3 -1043145000,1,16,fcdc01f9 -1043155000,1,17,0305018c -1043165000,1,18,fdf8fe8f -1043175000,1,19,011f0155 -1043185000,1,20,fcf4fe72 -1043195000,1,21,04fc00c2 -1043205000,1,22,fcfd00ea -1043215000,1,23,0191fb84 -1043225000,1,24,00630833 -1043235000,1,25,fec6f9db -1043245000,1,26,00bc01d7 -1043255000,1,27,fefcff8a -1043265000,1,28,00d20070 -1043275000,1,29,0066ff97 -1043285000,1,30,ff870048 -1043295000,1,31,0049ff83 -1047575000,2,0,ffcf002b -1047585000,2,1,0003002f -1047595000,2,2,000dff5a -1047605000,2,3,001a0091 -1047615000,2,4,fe9300fe -1047625000,2,5,039bfebf -1047635000,2,6,fd0bffae -1047645000,2,7,02c30020 -1047655000,2,8,fc5201a4 -1047665000,2,9,0316ff87 -1047675000,2,10,ff35fcfd -1047685000,2,11,fe6d0225 -1047695000,2,12,00ca0079 -1047705000,2,13,ff2afccc -1047715000,2,14,0295019e -1047725000,2,15,ff9202e1 -1047735000,2,16,fd0dff45 -1047745000,2,17,03e9fd9d -1047755000,2,18,fa730340 -1047765000,2,19,0524fc5b -1047775000,2,20,fe25005a -1047785000,2,21,fefb00c1 -1047795000,2,22,01dd0126 -1047805000,2,23,fe71feb6 -1047815000,2,24,00b202cc -1047825000,2,25,0092fca1 -1047835000,2,26,fecb024d -1047845000,2,27,01f1fe07 -1047855000,2,28,fd6601cf -1047865000,2,29,019cfea8 -1047875000,2,30,ffbb012a -1047885000,2,31,002eff49 -1052165000,3,0,ffe3003d -1052175000,3,1,ffd4fff8 -1052185000,3,2,000affbf -1052195000,3,3,00e0000c -1052205000,3,4,fd480132 -1052215000,3,5,0433fe7f -1052225000,3,6,fd64ffcc -1052235000,3,7,00fd015c -1052245000,3,8,ff11ffc9 -1052255000,3,9,012bff05 -1052265000,3,10,fdd60084 -1052275000,3,11,02250089 -1052285000,3,12,ff64fedc -1052295000,3,13,fe98fe2a -1052305000,3,14,00c60216 -1052315000,3,15,00980204 -1052325000,3,16,026ffefd -1052335000,3,17,fc08ff4c -1052345000,3,18,013c0067 -1052355000,3,19,0112ffe4 -1052365000,3,20,fd4cfeae -1052375000,3,21,02fdfe15 -1052385000,3,22,fc4e043e -1052395000,3,23,03a3fcaa -1052405000,3,24,fe3505a1 -1052415000,3,25,0039fbb3 -1052425000,3,26,00fc0186 -1052435000,3,27,fe7dff0f -1052445000,3,28,01680050 -1052455000,3,29,ffc8ff96 -1052465000,3,30,ff9800c0 -1052475000,3,31,0044ff5e -1056755000,4,0,ffae003f -1056765000,4,1,0028ffde -1056775000,4,2,ff7fff3e -1056785000,4,3,00a60183 -1056795000,4,4,ff91ffd4 -1056805000,4,5,00f5fd71 -1056815000,4,6,ff60023d -1056825000,4,7,ff870080 -1056835000,4,8,00c8fe4c -1056845000,4,9,ff670167 -1056855000,4,10,feb9fe23 -1056865000,4,11,026e03f3 -1056875000,4,12,fe39fb1a -1056885000,4,13,fedd018a -1056895000,4,14,020200e4 -1056905000,4,15,015d0034 -1056915000,4,16,fe6eff53 -1056925000,4,17,00e80316 -1056935000,4,18,fe71fb14 -1056945000,4,19,009402eb -1056955000,4,20,feb9fe74 -1056965000,4,21,0121ff4d -1056975000,4,22,ff420299 -1056985000,4,23,00ebfc1c -1056995000,4,24,ffd40856 -1057005000,4,25,005df875 -1057015000,4,26,006302fb -1057025000,4,27,ff50ff33 -1057035000,4,28,fffdff5a -1057045000,4,29,00a90130 -1057055000,4,30,ff58ffa6 -1057065000,4,31,0089ff94 -1061345000,5,0,ff83ffe1 -1061355000,5,1,006a002f -1061365000,5,2,ff42000f -1061375000,5,3,011dff69 -1061385000,5,4,ff780101 -1061395000,5,5,00bdfe8c -1061405000,5,6,fddd006b -1061415000,5,7,02bf005a -1061425000,5,8,fcd600ac -1061435000,5,9,0208ffe9 -1061445000,5,10,fec0ffe1 -1061455000,5,11,04150097 -1061465000,5,12,fae8fdc2 -1061475000,5,13,0214009f -1061485000,5,14,0294ff48 -1061495000,5,15,fb0a00b1 -1061505000,5,16,053500e9 -1061515000,5,17,fca60091 -1061525000,5,18,fdacfe8b -1061535000,5,19,0325fed5 -1061545000,5,20,fe8402f3 -1061555000,5,21,0165fd62 -1061565000,5,22,ff0f00af -1061575000,5,23,feb3014c -1061585000,5,24,03fa0132 -1061595000,5,25,fd24fdeb -1061605000,5,26,00520045 -1061615000,5,27,00a9005b -1061625000,5,28,ff94002a -1061635000,5,29,008effb7 -1061645000,5,30,ff40ffd6 -1061655000,5,31,00c40021 -1065935000,6,0,002d005f -1065945000,6,1,ffdeff46 -1065955000,6,2,001700f8 -1065965000,6,3,ff84ff23 -1065975000,6,4,008700fa -1065985000,6,5,0137fef5 -1065995000,6,6,ff9101d0 -1066005000,6,7,fd61fcc0 -1066015000,6,8,02fd02cb -1066025000,6,9,ffe3fdd3 -1066035000,6,10,fe2c00d4 -1066045000,6,11,fe520083 -1066055000,6,12,0396ffc3 -1066065000,6,13,fea7020c -1066075000,6,14,003ffc01 -1066085000,6,15,00ce0227 -1066095000,6,16,ffe5ff7f -1066105000,6,17,01c0ff0a -1066115000,6,18,fafd036a -1066125000,6,19,00f2fc3b -1066135000,6,20,01f101da -1066145000,6,21,fd6dff15 -1066155000,6,22,033bfe92 -1066165000,6,23,fec90454 -1066175000,6,24,006dfcab -1066185000,6,25,fe3702fd -1066195000,6,26,0288fe0e -1066205000,6,27,fde00047 -1066215000,6,28,010600dd -1066225000,6,29,001dfe3a -1066235000,6,30,003d0141 -1066245000,6,31,ffa0ff7d -1070525000,7,0,ffb5007e -1070535000,7,1,0054ff99 -1070545000,7,2,ff01002d -1070555000,7,3,018f0045 -1070565000,7,4,fea3ffe9 -1070575000,7,5,015dfe20 -1070585000,7,6,ffff01b8 -1070595000,7,7,0002fff4 -1070605000,7,8,fec900c2 -1070615000,7,9,00350070 -1070625000,7,10,ff5efcde -1070635000,7,11,048c00df -1070645000,7,12,f9030042 -1070655000,7,13,04b3ff61 -1070665000,7,14,fe6b0239 -1070675000,7,15,00dffd33 -1070685000,7,16,fe4302a0 -1070695000,7,17,0208fdc1 -1070705000,7,18,fe710223 -1070715000,7,19,fe19ffcd -1070725000,7,20,03fffdb7 -1070735000,7,21,fd490274 -1070745000,7,22,017ffee8 -1070755000,7,23,ff3effa4 -1070765000,7,24,011f0058 -1070775000,7,25,fd0700e6 -1070785000,7,26,02b0fffe -1070795000,7,27,ff20fed7 -1070805000,7,28,ffe301ce -1070815000,7,29,00c7fee3 -1070825000,7,30,ff7f00d3 -1070835000,7,31,0085ff25 -1075115000,8,0,fff1002f -1075125000,8,1,ffd1ffe1 -1075135000,8,2,fffcffc6 -1075145000,8,3,000500a1 -1075155000,8,4,ffc0006c -1075165000,8,5,0205fd3c -1075175000,8,6,fe0803a0 -1075185000,8,7,006dfe1b -1075195000,8,8,fff2fe1b -1075205000,8,9,ff9202cd -1075215000,8,10,fe5bfda9 -1075225000,8,11,040f05f2 -1075235000,8,12,fd8ef9ce -1075245000,8,13,fee70245 -1075255000,8,14,01d6ffcb -1075265000,8,15,005bfcab -1075275000,8,16,ff0d0429 -1075285000,8,17,02ebff7b -1075295000,8,18,fa7eff36 -1075305000,8,19,03410019 -1075315000,8,20,fe8801e2 -1075325000,8,21,ffc9fd36 -1075335000,8,22,01b600ec -1075345000,8,23,feddfd3d -1075355000,8,24,01c40695 -1075365000,8,25,fd5afc87 -1075375000,8,26,01b3ff6f -1075385000,8,27,009b00f4 -1075395000,8,28,fee6ff94 -1075405000,8,29,00630029 -1075415000,8,30,0004000d -1075425000,8,31,001bff9d -1079705000,9,0,ffda004a -1079715000,9,1,ffeaffd8 -1079725000,9,2,0051ffe1 -1079735000,9,3,ff740018 -1079745000,9,4,feea0062 -1079755000,9,5,030aff25 -1079765000,9,6,fe5e0211 -1079775000,9,7,00fbfde8 -1079785000,9,8,fd540164 -1079795000,9,9,04dafe11 -1079805000,9,10,fb12ff8c -1079815000,9,11,ff690358 -1079825000,9,12,049bfcfc -1079835000,9,13,fc0401b7 -1079845000,9,14,017cfd7b -1079855000,9,15,015b03cb -1079865000,9,16,fdf6ff44 -1079875000,9,17,03fcfe52 -1079885000,9,18,f9c50141 -1079895000,9,19,02dcfe9c -1079905000,9,20,010a0170 -1079915000,9,21,fe22fe57 -1079925000,9,22,01fa013d -1079935000,9,23,ff4bfe98 -1079945000,9,24,ff0802ce -1079955000,9,25,0100fec1 -1079965000,9,26,ff000022 -1079975000,9,27,017bff04 -1079985000,9,28,ff3500da -1079995000,9,29,0078ffc9 -1080005000,9,30,ffb4003f -1080015000,9,31,0023ff6d -1084295000,10,0,ffd6003c -1084305000,10,1,0039ff94 -1084315000,10,2,ffa90079 -1084325000,10,3,ffabffd4 -1084335000,10,4,ff9c0087 -1084345000,10,5,0376ff9e -1084355000,10,6,fce7004a -1084365000,10,7,0186fec8 -1084375000,10,8,fec4025a -1084385000,10,9,017afdeb -1084395000,10,10,fddcfef2 -1084405000,10,11,000004ab -1084415000,10,12,03c7fa98 -1084425000,10,13,fabb012e -1084435000,10,14,053a001f -1084445000,10,15,fe4901bd -1084455000,10,16,fd50fec8 -1084465000,10,17,0301ff76 -1084475000,10,18,fa3f0297 -1084485000,10,19,04e7fc80 -1084495000,10,20,00240391 -1084505000,10,21,ffa6fd5c -1084515000,10,22,ffd10294 -1084525000,10,23,0012fca4 -1084535000,10,24,ff3e02ea -1084545000,10,25,00e8feb3 -1084555000,10,26,ff68ffe6 -1084565000,10,27,00c2ff81 -1084575000,10,28,ff4901b0 -1084585000,10,29,ffedfef8 -1084595000,10,30,004a0043 -1084605000,10,31,000bffbf -1088885000,11,0,ff7a0058 -1088895000,11,1,0094ffba -1088905000,11,2,ff57007d -1088915000,11,3,0023ff19 -1088925000,11,4,ff31023f -1088935000,11,5,0362fd12 -1088945000,11,6,fd770120 -1088955000,11,7,ffbf004a -1088965000,11,8,00b6ffbc -1088975000,11,9,ffee01c5 -1088985000,11,10,fe7afc1d -1088995000,11,11,00fc04e2 -1089005000,11,12,014efa52 -1089015000,11,13,ff04035f -1089025000,11,14,008fff3e -1089035000,11,15,ffa1ff09 -1089045000,11,16,ff7802fc -1089055000,11,17,fd78ffa6 -1089065000,11,18,02b9fd63 -1089075000,11,19,028d0163 -1089085000,11,20,fcf101ad -1089095000,11,21,00fcfc8c -1089105000,11,22,fec502be -1089115000,11,23,00f1fcca -1089125000,11,24,fef00380 -1089135000,11,25,024afeaf -1089145000,11,26,fda2001b -1089155000,11,27,0160005e -1089165000,11,28,ffe8ffba -1089175000,11,29,ff6affe7 -1089185000,11,30,00590064 -1089195000,11,31,0053ff4f -1093475000,12,0,ffbe0016 -1093485000,12,1,0008fffb -1093495000,12,2,ffc70011 -1093505000,12,3,009bffa7 -1093515000,12,4,ff710232 -1093525000,12,5,0152fc2f -1093535000,12,6,ff2a01f3 -1093545000,12,7,0082005c -1093555000,12,8,fe35ffba -1093565000,12,9,00eb013a -1093575000,12,10,fe55fb10 -1093585000,12,11,04450846 -1093595000,12,12,fc08f7b0 -1093605000,12,13,03870161 -1093615000,12,14,fd4e05c9 -1093625000,12,15,017efadf -1093635000,12,16,ff7e02a8 -1093645000,12,17,ff1aff29 -1093655000,12,18,ff1bfed7 -1093665000,12,19,016dffc9 -1093675000,12,20,02570162 -1093685000,12,21,fc7afe19 -1093695000,12,22,00a80341 -1093705000,12,23,ff68fd04 -1093715000,12,24,0257026c -1093725000,12,25,fef7fece -1093735000,12,26,ff810034 -1093745000,12,27,ffa7ffa6 -1093755000,12,28,00f000c0 -1093765000,12,29,ffa9ff83 -1093775000,12,30,fff0002f -1093785000,12,31,0054ffcd -1098065000,13,0,ffe70050 -1098075000,13,1,0035ff8a -1098085000,13,2,ff0a00cb -1098095000,13,3,0104fed0 -1098105000,13,4,00e20282 -1098115000,13,5,feabfb6a -1098125000,13,6,012a02e2 -1098135000,13,7,fdeefee2 -1098145000,13,8,0311025c -1098155000,13,9,fd60fd44 -1098165000,13,10,0064002c -1098175000,13,11,ff5f045e -1098185000,13,12,0094fae5 -1098195000,13,13,01d900d0 -1098205000,13,14,fe60024e -1098215000,13,15,000ffb73 -1098225000,13,16,01750308 -1098235000,13,17,ffa100ea -1098245000,13,18,fd40fe49 -1098255000,13,19,015a012c -1098265000,13,20,ff48fe3a -1098275000,13,21,ff7700e2 -1098285000,13,22,030e00e2 -1098295000,13,23,fc42fffe -1098305000,13,24,02630118 -1098315000,13,25,ff1efdb4 -1098325000,13,26,021201b4 -1098335000,13,27,fde3ff8e -1098345000,13,28,00c2fff3 -1098355000,13,29,0049ffa0 -1098365000,13,30,ff8800ca -1098375000,13,31,0059ff6d -1102655000,14,0,fff20056 -1102665000,14,1,ffdcff98 -1102675000,14,2,0079ffd7 -1102685000,14,3,ff5e00f4 -1102695000,14,4,007d000a -1102705000,14,5,00d2fe89 -1102715000,14,6,feccffd2 -1102725000,14,7,ffbf03bc -1102735000,14,8,ffc2fd65 -1102745000,14,9,02edffc5 -1102755000,14,10,fd65fe94 -1102765000,14,11,017c03d3 -1102775000,14,12,01e2fca2 -1102785000,14,13,fa37004e -1102795000,14,14,0436fdba -1102805000,14,15,00f202f8 -1102815000,14,16,fab4ff44 -1102825000,14,17,05a20252 -1102835000,14,18,fa9bfd4b -1102845000,14,19,02fa010e -1102855000,14,20,fe93fff8 -1102865000,14,21,009efefd -1102875000,14,22,00f40266 -1102885000,14,23,014ffd26 -1102895000,14,24,fe2802ed -1102905000,14,25,005dfd11 -1102915000,14,26,fecf02ba -1102925000,14,27,00a0fe0f -1102935000,14,28,ffde0140 -1102945000,14,29,00b1ff3c -1102955000,14,30,ffb2009e -1102965000,14,31,001cffa2 -1107245000,15,0,0001005d -1107255000,15,1,fffdffba -1107265000,15,2,ffecffff -1107275000,15,3,feefff84 -1107285000,15,4,01050122 -1107295000,15,5,0199feeb -1107305000,15,6,fdcc00b6 -1107315000,15,7,00c7004c -1107325000,15,8,ffcdfeff -1107335000,15,9,025d01a3 -1107345000,15,10,fa36fdba -1107355000,15,11,034002e9 -1107365000,15,12,01ddfb72 -1107375000,15,13,fde903d8 -1107385000,15,14,00affe98 -1107395000,15,15,0164ff0b -1107405000,15,16,fccd01b7 -1107415000,15,17,040bfe0e -1107425000,15,18,fa80038f -1107435000,15,19,0179fc34 -1107445000,15,20,01cb02ce -1107455000,15,21,feabfe25 -1107465000,15,22,01540066 -1107475000,15,23,ffcd0098 -1107485000,15,24,fe95024d -1107495000,15,25,0257fce9 -1107505000,15,26,fee20184 -1107515000,15,27,00c0febf -1107525000,15,28,ff53011e -1107535000,15,29,002ffe7c -1107545000,15,30,fffd0180 -1107555000,15,31,0008ff19 -1111835000,16,0,ff960046 -1111845000,16,1,003effb1 -1111855000,16,2,ffc4ffba -1111865000,16,3,003700d4 -1111875000,16,4,fedc010b -1111885000,16,5,036ffcfb -1111895000,16,6,fd55020c -1111905000,16,7,fefffffd -1111915000,16,8,012cfeb8 -1111925000,16,9,ffa701bb -1111935000,16,10,0022fd14 -1111945000,16,11,0252070f -1111955000,16,12,fde3f7e6 -1111965000,16,13,00120368 -1111975000,16,14,fe9cff7c -1111985000,16,15,048bfd55 -1111995000,16,16,fafc04ee -1112005000,16,17,03cefe39 -1112015000,16,18,fd6afe36 -1112025000,16,19,fdbb021a -1112035000,16,20,037efec9 -1112045000,16,21,ff790027 -1112055000,16,22,fecf019c -1112065000,16,23,0149fc8d -1112075000,16,24,007a0684 -1112085000,16,25,ff65fa6b -1112095000,16,26,00440208 -1112105000,16,27,0020ff3f -1112115000,16,28,ff6bfffe -1112125000,16,29,0046ffde -1112135000,16,30,ffbc0098 -1112145000,16,31,0081ff7d -1116425000,17,0,ffde006d -1116435000,17,1,0005ff70 -1116445000,17,2,ff7100fa -1116455000,17,3,0168fe4e -1116465000,17,4,fded0253 -1116475000,17,5,0267fd45 -1116485000,17,6,ff9103c0 -1116495000,17,7,fe7ffd0d -1116505000,17,8,0056feea -1116515000,17,9,0156039b -1116525000,17,10,fe2dfd5e -1116535000,17,11,014d0360 -1116545000,17,12,0013faf2 -1116555000,17,13,ffc0022f -1116565000,17,14,ff3800a3 -1116575000,17,15,0133fd39 -1116585000,17,16,002e029f -1116595000,17,17,fea3fe4c -1116605000,17,18,fe91037a -1116615000,17,19,01420070 -1116625000,17,20,0067fb8f -1116635000,17,21,ff9b015d -1116645000,17,22,00fb02aa -1116655000,17,23,ffdffb71 -1116665000,17,24,0036057a -1116675000,17,25,ff86fba9 -1116685000,17,26,005d02ca -1116695000,17,27,ffe1fe3e -1116705000,17,28,ff5900a4 -1116715000,17,29,00caffaf -1116725000,17,30,ffb8009f -1116735000,17,31,0027ff3d -1121015000,18,0,fff90044 -1121025000,18,1,ffe8000d -1121035000,18,2,000cff68 -1121045000,18,3,ffe300f5 -1121055000,18,4,ffc600e8 -1121065000,18,5,0228fd28 -1121075000,18,6,fd9c0244 -1121085000,18,7,0175fdc8 -1121095000,18,8,fdab01e1 -1121105000,18,9,030e00b9 -1121115000,18,10,fc6efdcd -1121125000,18,11,03bf01ff -1121135000,18,12,fc87ff80 -1121145000,18,13,011ffd5f -1121155000,18,14,0371022c -1121165000,18,15,fcfb0056 -1121175000,18,16,0019ffde -1121185000,18,17,01e20129 -1121195000,18,18,fb7cfcaa -1121205000,18,19,028902b1 -1121215000,18,20,0036fd18 -1121225000,18,21,00ce01c6 -1121235000,18,22,fd7c028c -1121245000,18,23,0115fbb0 -1121255000,18,24,01e3048d -1121265000,18,25,fe18fd01 -1121275000,18,26,00a20141 -1121285000,18,27,0069fecb -1121295000,18,28,ff650090 -1121305000,18,29,ffab0053 -1121315000,18,30,00e70014 -1121325000,18,31,ffa7ff62 -1125605000,19,0,ffd90015 -1125615000,19,1,0008ffff -1125625000,19,2,ff8bffd2 -1125635000,19,3,0163ffec -1125645000,19,4,fdb40108 -1125655000,19,5,0342fe7a -1125665000,19,6,fdf500c6 -1125675000,19,7,0078feea -1125685000,19,8,00190332 -1125695000,19,9,fe2afd59 -1125705000,19,10,022d00c7 -1125715000,19,11,01aeffe3 -1125725000,19,12,fac6fee5 -1125735000,19,13,02deff17 -1125745000,19,14,017efec5 -1125755000,19,15,ff1205de -1125765000,19,16,fe7ffbfb -1125775000,19,17,ff640385 -1125785000,19,18,0295fe0a -1125795000,19,19,fde7fef8 -1125805000,19,20,fff60012 -1125815000,19,21,031effe4 -1125825000,19,22,fd7f01b8 -1125835000,19,23,003afba6 -1125845000,19,24,ffe70656 -1125855000,19,25,fffafbc7 -1125865000,19,26,00e301e9 -1125875000,19,27,fee0fe8d -1125885000,19,28,00500181 -1125895000,19,29,0032ff97 -1125905000,19,30,0026ffa9 -1125915000,19,31,0004fffe -1130195000,20,0,ffab0047 -1130205000,20,1,0043ffba -1130215000,20,2,ff48001c -1130225000,20,3,0110ffd9 -1130235000,20,4,fefb01b0 -1130245000,20,5,028afc6e -1130255000,20,6,fd230240 -1130265000,20,7,011bffa6 -1130275000,20,8,ff6c0048 -1130285000,20,9,001b0026 -1130295000,20,10,feecfde9 -1130305000,20,11,01070439 -1130315000,20,12,015afc9d -1130325000,20,13,fd59ff31 -1130335000,20,14,01980233 -1130345000,20,15,0246ff60 -1130355000,20,16,fc2b0007 -1130365000,20,17,036d0314 -1130375000,20,18,fc1efb3e -1130385000,20,19,00e8010b -1130395000,20,20,018d0072 -1130405000,20,21,fe92ff50 -1130415000,20,22,00610228 -1130425000,20,23,002dfba8 -1130435000,20,24,014a077a -1130445000,20,25,fe29fbd4 -1130455000,20,26,00c2ffa1 -1130465000,20,27,001dfff3 -1130475000,20,28,002200e1 -1130485000,20,29,ff97ff99 -1130495000,20,30,00600041 -1130505000,20,31,0036ff82 -1134785000,21,0,fffe0006 -1134795000,21,1,fff0ffb7 -1134805000,21,2,ffa2009a -1134815000,21,3,0074fe78 -1134825000,21,4,00d50306 -1134835000,21,5,ff0dfd62 -1134845000,21,6,00bf0076 -1134855000,21,7,ffa6003c -1134865000,21,8,ff18ff4a -1134875000,21,9,00c4017b -1134885000,21,10,0036fce8 -1134895000,21,11,ff5f04f7 -1134905000,21,12,001dfb21 -1134915000,21,13,ff680274 -1134925000,21,14,0166fe35 -1134935000,21,15,fde00330 -1134945000,21,16,03eafd00 -1134955000,21,17,ff2800d9 -1134965000,21,18,fb6201f8 -1134975000,21,19,0396feb6 -1134985000,21,20,ff35fdb8 -1134995000,21,21,ff2d007c -1135005000,21,22,019100a4 -1135015000,21,23,fd0401e4 -1135025000,21,24,02ecff20 -1135035000,21,25,ff080095 -1135045000,21,26,0066005a -1135055000,21,27,006ffec3 -1135065000,21,28,fe7d00c1 -1135075000,21,29,01caff86 -1135085000,21,30,ff3affed -1135095000,21,31,002e0030 -1139375000,22,0,001e0043 -1139385000,22,1,ff95ff75 -1139395000,22,2,009000c4 -1139405000,22,3,000bff84 -1139415000,22,4,ff2b0061 -1139425000,22,5,0148ffda -1139435000,22,6,0056fedc -1139445000,22,7,ffeb005c -1139455000,22,8,fcff00a4 -1139465000,22,9,02eeffa8 -1139475000,22,10,fee50028 -1139485000,22,11,0189006d -1139495000,22,12,fbe8ff81 -1139505000,22,13,046e0124 -1139515000,22,14,fbd8fb1b -1139525000,22,15,04200643 -1139535000,22,16,ffd4fcd9 -1139545000,22,17,fe33fef3 -1139555000,22,18,007a053c -1139565000,22,19,fe35f9d6 -1139575000,22,20,030b0529 -1139585000,22,21,fc12fa7c -1139595000,22,22,023202bc -1139605000,22,23,ffe9003c -1139615000,22,24,009f0210 -1139625000,22,25,ffaafe24 -1139635000,22,26,ffddff8c -1139645000,22,27,003700dd -1139655000,22,28,ffaa002d -1139665000,22,29,00a8ff3a -1139675000,22,30,ffcc00a1 -1139685000,22,31,ffecff89 -1143965000,23,0,ffc8fffe -1143975000,23,1,ffc9ffd4 -1143985000,23,2,00380090 -1143995000,23,3,ffaaffea -1144005000,23,4,ffab000f -1144015000,23,5,023fff3b -1144025000,23,6,fe04fff1 -1144035000,23,7,01770149 -1144045000,23,8,fce5ff7c -1144055000,23,9,03e700a2 -1144065000,23,10,fb3bfd31 -1144075000,23,11,045a04db -1144085000,23,12,ff80fcff -1144095000,23,13,ff220003 -1144105000,23,14,030ffcab -1144115000,23,15,fbe304c7 -1144125000,23,16,ffd0fe76 -1144135000,23,17,0345fe0e -1144145000,23,18,fb8403fc -1144155000,23,19,011cfd5e -1144165000,23,20,02bfff85 -1144175000,23,21,fc4d022b -1144185000,23,22,0248ff1d -1144195000,23,23,00850033 -1144205000,23,24,002fff84 -1144215000,23,25,ff970134 -1144225000,23,26,fe39ff97 -1144235000,23,27,02c8fe7d -1144245000,23,28,fdb20119 -1144255000,23,29,01ceffc7 -1144265000,23,30,fead0023 -1144275000,23,31,00b1ffe5 -1148555000,24,0,ffb0007a -1148565000,24,1,000cff4b -1148575000,24,2,005800bc -1148585000,24,3,ff1dff85 -1148595000,24,4,00eb0226 -1148605000,24,5,00d9fc88 -1148615000,24,6,fe7c01b7 -1148625000,24,7,01170106 -1148635000,24,8,fe23fdab -1148645000,24,9,024a02a9 -1148655000,24,10,fcdafb5c -1148665000,24,11,05f70758 -1148675000,24,12,fadcf95d -1148685000,24,13,004b0248 -1148695000,24,14,01bcff52 -1148705000,24,15,fec000b0 -1148715000,24,16,ffe4010c -1148725000,24,17,0484ffab -1148735000,24,18,f9b60012 -1148745000,24,19,02e7fe23 -1148755000,24,20,ff8f0128 -1148765000,24,21,0031fda2 -1148775000,24,22,ff7c02d9 -1148785000,24,23,ff2ffcde -1148795000,24,24,007d04e7 -1148805000,24,25,011aff29 -1148815000,24,26,fe1cfdc6 -1148825000,24,27,02bd0164 -1148835000,24,28,fe4eff65 -1148845000,24,29,0027ff76 -1148855000,24,30,fff00106 -1148865000,24,31,0052ff58 -1153145000,25,0,0001005e -1153155000,25,1,fffaffc2 -1153165000,25,2,ffdcffd9 -1153175000,25,3,ff6d007b -1153185000,25,4,01c1ffb5 -1153195000,25,5,ff07ffbf -1153205000,25,6,ff48009d -1153215000,25,7,0122fee8 -1153225000,25,8,ff22010a -1153235000,25,9,ff20005c -1153245000,25,10,0167fefe -1153255000,25,11,fe680361 -1153265000,25,12,0334fb5d -1153275000,25,13,fce40061 -1153285000,25,14,02260127 -1153295000,25,15,0281fdf7 -1153305000,25,16,f8e7040a -1153315000,25,17,05c2fcca -1153325000,25,18,fa240347 -1153335000,25,19,02fbfd4f -1153345000,25,20,00c7007f -1153355000,25,21,ffcb0153 -1153365000,25,22,ffe8fe39 -1153375000,25,23,ffe800c4 -1153385000,25,24,017a01d6 -1153395000,25,25,fec4ff2c -1153405000,25,26,fffdfec6 -1153415000,25,27,0100ff89 -1153425000,25,28,ff5801af -1153435000,25,29,ff2afeb9 -1153445000,25,30,013e00e7 -1153455000,25,31,ff95ff19 -1157735000,26,0,ff93003b -1157745000,26,1,008afff8 -1157755000,26,2,ff76ffff -1157765000,26,3,0042ffbc -1157775000,26,4,ff8f00d3 -1157785000,26,5,0206ff71 -1157795000,26,6,fe680053 -1157805000,26,7,fe96ffb5 -1157815000,26,8,01b1fec2 -1157825000,26,9,01030334 -1157835000,26,10,fec6fbd6 -1157845000,26,11,001c0622 -1157855000,26,12,fee6f7ee -1157865000,26,13,026b0559 -1157875000,26,14,fc81fcaa -1157885000,26,15,039c02ea -1157895000,26,16,fe29fe79 -1157905000,26,17,02e4027e -1157915000,26,18,fad8fda1 -1157925000,26,19,013e01f0 -1157935000,26,20,02dbfc0f -1157945000,26,21,fcce0253 -1157955000,26,22,02b4020b -1157965000,26,23,0050fb57 -1157975000,26,24,fce704e6 -1157985000,26,25,0293fc52 -1157995000,26,26,feac028e -1158005000,26,27,0080ff52 -1158015000,26,28,feb4ff5c -1158025000,26,29,01e500c7 -1158035000,26,30,ff4bffdc -1158045000,26,31,003aff9a -1162325000,27,0,ff87004d -1162335000,27,1,0025ffba -1162345000,27,2,ffce002c -1162355000,27,3,012d0046 -1162365000,27,4,fea3008e -1162375000,27,5,022cfd74 -1162385000,27,6,fdb40385 -1162395000,27,7,000bfd8f -1162405000,27,8,001ffe82 -1162415000,27,9,0234035c -1162425000,27,10,fcf1fce6 -1162435000,27,11,022b0103 -1162445000,27,12,fe1e00a6 -1162455000,27,13,0333ff38 -1162465000,27,14,fb94025c -1162475000,27,15,0283fd19 -1162485000,27,16,fdfd02b3 -1162495000,27,17,03b1fd52 -1162505000,27,18,fe28ffa2 -1162515000,27,19,fd5d02ae -1162525000,27,20,03f5fbfa -1162535000,27,21,fc6403c4 -1162545000,27,22,02ba0069 -1162555000,27,23,fe39fee1 -1162565000,27,24,0069ff72 -1162575000,27,25,00320078 -1162585000,27,26,00d5012c -1162595000,27,27,ff7ffeb5 -1162605000,27,28,ff9e0076 -1162615000,27,29,0051ffa0 -1162625000,27,30,ffe200de -1162635000,27,31,00b5ff3b -1166915000,28,0,ffe1001a -1166925000,28,1,ffc30041 -1166935000,28,2,0032ffee -1166945000,28,3,009bff51 -1166955000,28,4,fea30210 -1166965000,28,5,01dcfc10 -1166975000,28,6,00030337 -1166985000,28,7,fe3b0091 -1166995000,28,8,0021fea4 -1167005000,28,9,01410049 -1167015000,28,10,fc04fd02 -1167025000,28,11,07ea080f -1167035000,28,12,fa2ff873 -1167045000,28,13,02b500d8 -1167055000,28,14,fe4b012e -1167065000,28,15,0031ffd0 -1167075000,28,16,0091011a -1167085000,28,17,0137fee3 -1167095000,28,18,fc12ff28 -1167105000,28,19,029f01b1 -1167115000,28,20,ff13fd50 -1167125000,28,21,ff6e038c -1167135000,28,22,004fffad -1167145000,28,23,00effd0b -1167155000,28,24,0111046c -1167165000,28,25,fe0dfc9f -1167175000,28,26,01300108 -1167185000,28,27,ff4cffb7 -1167195000,28,28,00ff0071 -1167205000,28,29,fea9ff78 -1167215000,28,30,00830096 -1167225000,28,31,0025ff84 -1171505000,29,0,ffca000a -1171515000,29,1,0044fffe -1171525000,29,2,ff3c0022 -1171535000,29,3,00f5ffcb -1171545000,29,4,001e017a -1171555000,29,5,ffb8fd15 -1171565000,29,6,00990122 -1171575000,29,7,fe3e0150 -1171585000,29,8,026dfe9b -1171595000,29,9,fca50059 -1171605000,29,10,0218fed2 -1171615000,29,11,ff840493 -1171625000,29,12,ffbefafb -1171635000,29,13,011c004b -1171645000,29,14,ffb002fd -1171655000,29,15,0146fbff -1171665000,29,16,fdf204ca -1171675000,29,17,0268fd98 -1171685000,29,18,fa10ffa6 -1171695000,29,19,05f1005f -1171705000,29,20,fcf8fdf0 -1171715000,29,21,011001c3 -1171725000,29,22,ff25012e -1171735000,29,23,0028feca -1171745000,29,24,026701f9 -1171755000,29,25,fe43fe71 -1171765000,29,26,ff38ffba -1171775000,29,27,00860073 -1171785000,29,28,014c0013 -1171795000,29,29,fe78ff95 -1171805000,29,30,00b6006f -1171815000,29,31,0004ffaf -1176095000,30,0,000fffe5 -1176105000,30,1,ffc9002c -1176115000,30,2,0048ffee -1176125000,30,3,fff7ffe4 -1176135000,30,4,00a100dd -1176145000,30,5,0002fe7d -1176155000,30,6,0014ffe7 -1176165000,30,7,fe2c01af -1176175000,30,8,01e6fe63 -1176185000,30,9,010000c9 -1176195000,30,10,fc56fdac -1176205000,30,11,034d046f -1176215000,30,12,fbe3fd25 -1176225000,30,13,05b5ffc5 -1176235000,30,14,faa500c1 -1176245000,30,15,03f5fe58 -1176255000,30,16,00df0209 -1176265000,30,17,fddfff34 -1176275000,30,18,fef8ffa6 -1176285000,30,19,ff850130 -1176295000,30,20,ffcffe6b -1176305000,30,21,fe7efe5f -1176315000,30,22,055c053b -1176325000,30,23,fd80fc2b -1176335000,30,24,fe7c01e3 -1176345000,30,25,0204fe6b -1176355000,30,26,fefe01c8 -1176365000,30,27,ffd70009 -1176375000,30,28,ffadff57 -1176385000,30,29,0117004b -1176395000,30,30,ffe7ffad -1176405000,30,31,ffe70032 -1180685000,31,0,00270081 -1180695000,31,1,ffc6ff02 -1180705000,31,2,000401dc -1180715000,31,3,fffffe2a -1180725000,31,4,0075002a -1180735000,31,5,ff9d0070 -1180745000,31,6,0188005e -1180755000,31,7,fec8ff8f -1180765000,31,8,fed900b9 -1180775000,31,9,020ffd6d -1180785000,31,10,fd7f0406 -1180795000,31,11,00bfff29 -1180805000,31,12,0228fc72 -1180815000,31,13,fef1024c -1180825000,31,14,0063feff -1180835000,31,15,fe0400bb -1180845000,31,16,03f9ffa5 -1180855000,31,17,fb56005c -1180865000,31,18,00f8029e -1180875000,31,19,0119fc8a -1180885000,31,20,ff73ff80 -1180895000,31,21,ffb3013a -1180905000,31,22,027a0128 -1180915000,31,23,fd7afe89 -1180925000,31,24,ffcbff91 -1180935000,31,25,ffbd0145 -1180945000,31,26,01a5009c -1180955000,31,27,fed5ff73 -1180965000,31,28,015c001c -1180975000,31,29,ff0fff7a -1180985000,31,30,005b0087 -1180995000,31,31,ffc6ff8d -1185275000,32,0,ffe60028 -1185285000,32,1,ffb6fff1 -1185295000,32,2,ffe0ff60 -1185305000,32,3,007500c8 -1185315000,32,4,fee5ffad -1185325000,32,5,02b1ffca -1185335000,32,6,fd430074 -1185345000,32,7,01b50021 -1185355000,32,8,fd7b0055 -1185365000,32,9,0285feba -1185375000,32,10,fd2dfdd7 -1185385000,32,11,02d70657 -1185395000,32,12,fea1fc5d -1185405000,32,13,ff93fd8b -1185415000,32,14,028a05a9 -1185425000,32,15,fe2cfa84 -1185435000,32,16,004e032a -1185445000,32,17,fbc80095 -1185455000,32,18,067cfe6a -1185465000,32,19,fe850158 -1185475000,32,20,fe05fe2b -1185485000,32,21,ffd10024 -1185495000,32,22,028300ac -1185505000,32,23,fd41fe7b -1185515000,32,24,013d05fd -1185525000,32,25,ffa9fa78 -1185535000,32,26,016f0213 -1185545000,32,27,ff43fef1 -1185555000,32,28,0049007f -1185565000,32,29,006fffbf -1185575000,32,30,ff18000b -1185585000,32,31,007affa8 -1189865000,33,0,ffb80041 -1189875000,33,1,005bffc8 -1189885000,33,2,ff71ffc6 -1189895000,33,3,009e001d -1189905000,33,4,ffb3014f -1189915000,33,5,011bfd91 -1189925000,33,6,fd7600ff -1189935000,33,7,0248006b -1189945000,33,8,fd8afffb -1189955000,33,9,0221ffd0 -1189965000,33,10,fe74fcb4 -1189975000,33,11,02ee0548 -1189985000,33,12,fdacfe9a -1189995000,33,13,fd5bfd4b -1190005000,33,14,03a20316 -1190015000,33,15,0087fee8 -1190025000,33,16,fd94ffcb -1190035000,33,17,0177000a -1190045000,33,18,ff290050 -1190055000,33,19,ffaa00ed -1190065000,33,20,febffe23 -1190075000,33,21,040d006d -1190085000,33,22,fce60155 -1190095000,33,23,01dcfd2d -1190105000,33,24,ff060551 -1190115000,33,25,ff59fb8e -1190125000,33,26,00e201ce -1190135000,33,27,feeeffb2 -1190145000,33,28,01b60004 -1190155000,33,29,ff81ffdf -1190165000,33,30,00220046 -1190175000,33,31,0021ff74 -1194455000,34,0,ffa80038 -1194465000,34,1,005cfffe -1194475000,34,2,ff44ffa0 -1194485000,34,3,012f009e -1194495000,34,4,fdff0017 -1194505000,34,5,030eff14 -1194515000,34,6,fe7700a1 -1194525000,34,7,ffd4ffcd -1194535000,34,8,011e005c -1194545000,34,9,fe3fff0f -1194555000,34,10,00d1fed1 -1194565000,34,11,fe9b0298 -1194575000,34,12,00c8ffb0 -1194585000,34,13,00b7fb16 -1194595000,34,14,fe1c0649 -1194605000,34,15,02f4fdd6 -1194615000,34,16,fcac0136 -1194625000,34,17,03f6ff18 -1194635000,34,18,fbd20012 -1194645000,34,19,017300a0 -1194655000,34,20,00bbfd5d -1194665000,34,21,ffa60218 -1194675000,34,22,ff35005b -1194685000,34,23,01a6fd15 -1194695000,34,24,fe4205e2 -1194705000,34,25,013ffd2f -1194715000,34,26,ffa9fea1 -1194725000,34,27,00c300fa -1194735000,34,28,ff6a0000 -1194745000,34,29,ffd5ffca -1194755000,34,30,003800c7 -1194765000,34,31,0052ff18 -1199045000,35,0,ffa00002 -1199055000,35,1,00020038 -1199065000,35,2,ffe1ff9f -1199075000,35,3,00f60014 -1199085000,35,4,fe15000d -1199095000,35,5,0286ffe2 -1199105000,35,6,fe4cffaa -1199115000,35,7,00f5fffc -1199125000,35,8,fe7b018f -1199135000,35,9,0265fe64 -1199145000,35,10,fb48006a -1199155000,35,11,06920099 -1199165000,35,12,fb27fe48 -1199175000,35,13,01a1fe4b -1199185000,35,14,ff7b02c4 -1199195000,35,15,00450014 -1199205000,35,16,017800b4 -1199215000,35,17,fbd2ff9a -1199225000,35,18,00dd0099 -1199235000,35,19,02eafeb6 -1199245000,35,20,fe6dfec5 -1199255000,35,21,01320256 -1199265000,35,22,fe8afff6 -1199275000,35,23,00b3fd5c -1199285000,35,24,00c503e7 -1199295000,35,25,fce7fe72 -1199305000,35,26,037efef2 -1199315000,35,27,feba0109 -1199325000,35,28,000f006a -1199335000,35,29,fff7ff1d -1199345000,35,30,fffb00a8 -1199355000,35,31,0097ff90 -1203635000,36,0,ffac004d -1203645000,36,1,0031ff9e -1203655000,36,2,ff75001f -1203665000,36,3,0186004a -1203675000,36,4,fe6000cf -1203685000,36,5,01effcf2 -1203695000,36,6,fe210248 -1203705000,36,7,0086ffb5 -1203715000,36,8,ffa6ff5b -1203725000,36,9,ff950040 -1203735000,36,10,0115ffbb -1203745000,36,11,ffb2029b -1203755000,36,12,ff68fce2 -1203765000,36,13,ffe3002e -1203775000,36,14,00f5001c -1203785000,36,15,ff180083 -1203795000,36,16,002200f7 -1203805000,36,17,02f7ffde -1203815000,36,18,fb2fff51 -1203825000,36,19,017c00ca -1203835000,36,20,00f0fed7 -1203845000,36,21,ffd1ff0c -1203855000,36,22,fec7024a -1203865000,36,23,0084fb41 -1203875000,36,24,01a408cd -1203885000,36,25,fdc3fa20 -1203895000,36,26,00ff01a1 -1203905000,36,27,00700011 -1203915000,36,28,ffc0ff1c -1203925000,36,29,ffed00f0 -1203935000,36,30,004bffa6 -1203945000,36,31,003aff9f -1208225000,37,0,ffba0009 -1208235000,37,1,0016fffd -1208245000,37,2,fff2ff6e -1208255000,37,3,ffae004e -1208265000,37,4,010901e4 -1208275000,37,5,ffcffd29 -1208285000,37,6,fe6e00d2 -1208295000,37,7,0194006b -1208305000,37,8,ff29fffe -1208315000,37,9,ff6b009d -1208325000,37,10,00a7fcfb -1208335000,37,11,01d2060d -1208345000,37,12,fe73fa6b -1208355000,37,13,fee00109 -1208365000,37,14,03290015 -1208375000,37,15,fe8e005b -1208385000,37,16,fe24fe83 -1208395000,37,17,01a40293 -1208405000,37,18,fc14fe4c -1208415000,37,19,04b402be -1208425000,37,20,fcc3fd08 -1208435000,37,21,033700cd -1208445000,37,22,fe120084 -1208455000,37,23,ffcafe2d -1208465000,37,24,008d044a -1208475000,37,25,0063fd13 -1208485000,37,26,fffb012b -1208495000,37,27,ff94ffd3 -1208505000,37,28,009dff15 -1208515000,37,29,ffe200c1 -1208525000,37,30,ffafff85 -1208535000,37,31,008c0011 -1212815000,38,0,ffd30026 -1212825000,38,1,ff84ffa6 -1212835000,38,2,0079009d -1212845000,38,3,00ebffaa -1212855000,38,4,fe1a002b -1212865000,38,5,0153ff40 -1212875000,38,6,000701ca -1212885000,38,7,fee6fdb7 -1212895000,38,8,01560192 -1212905000,38,9,01eafe86 -1212915000,38,10,fb760066 -1212925000,38,11,019b01c6 -1212935000,38,12,ffc4fe00 -1212945000,38,13,00110005 -1212955000,38,14,ff24ff0b -1212965000,38,15,017c0281 -1212975000,38,16,034b0192 -1212985000,38,17,fb42fd1c -1212995000,38,18,015b014d -1213005000,38,19,ff39fd88 -1213015000,38,20,01f801d5 -1213025000,38,21,fc79fea4 -1213035000,38,22,01bf01f0 -1213045000,38,23,00ecfe71 -1213055000,38,24,00f401ba -1213065000,38,25,fc68ffd8 -1213075000,38,26,042efea0 -1213085000,38,27,fcd50164 -1213095000,38,28,018aff6c -1213105000,38,29,ffd30007 -1213115000,38,30,ffb6fffb -1213125000,38,31,0076ffcb -1217405000,39,0,ff9fff82 -1217415000,39,1,000000bd -1217425000,39,2,000dff32 -1217435000,39,3,00090091 -1217445000,39,4,ffc900d8 -1217455000,39,5,0165fdbe -1217465000,39,6,fdee01fc -1217475000,39,7,02d8ff7e -1217485000,39,8,fdbffecf -1217495000,39,9,002f01e4 -1217505000,39,10,ff8efb11 -1217515000,39,11,02b40715 -1217525000,39,12,fe2bfbbf -1217535000,39,13,fe23fea2 -1217545000,39,14,02200294 -1217555000,39,15,ff0d007d -1217565000,39,16,0161feb2 -1217575000,39,17,fe480183 -1217585000,39,18,0063fe5a -1217595000,39,19,00310091 -1217605000,39,20,ff6ffe96 -1217615000,39,21,01170054 -1217625000,39,22,fee20242 -1217635000,39,23,01a2ff8a -1217645000,39,24,fe61fff9 -1217655000,39,25,0009ff74 -1217665000,39,26,0036ffe3 -1217675000,39,27,00860071 -1217685000,39,28,ffedfec7 -1217695000,39,29,00610194 -1217705000,39,30,feecff5e -1217715000,39,31,01050053 -1221995000,40,0,ffd6002b -1222005000,40,1,ffde0010 -1222015000,40,2,0047ffa6 -1222025000,40,3,ffd4ffb8 -1222035000,40,4,005a02d6 -1222045000,40,5,000afaf4 -1222055000,40,6,005303a2 -1222065000,40,7,fe94fecd -1222075000,40,8,00e10023 -1222085000,40,9,ff29022b -1222095000,40,10,0157fa11 -1222105000,40,11,00f80960 -1222115000,40,12,ff49f8d6 -1222125000,40,13,fd35ffdb -1222135000,40,14,0335015a -1222145000,40,15,ff5afff0 -1222155000,40,16,ff64001f -1222165000,40,17,0190ffb2 -1222175000,40,18,fe4d01d4 -1222185000,40,19,fe86fef0 -1222195000,40,20,0296fefa -1222205000,40,21,fd0e0022 -1222215000,40,22,032dff56 -1222225000,40,23,fe48ff75 -1222235000,40,24,ffe104a7 -1222245000,40,25,ffb1fdf7 -1222255000,40,26,01edfe79 -1222265000,40,27,fe7e01d4 -1222275000,40,28,0073fe3e -1222285000,40,29,fffb015b -1222295000,40,30,ffebffa2 -1222305000,40,31,004affd2 -1226585000,41,0,ffe30044 -1226595000,41,1,ffe2fff8 -1226605000,41,2,fff90009 -1226615000,41,3,ff68ff77 -1226625000,41,4,00bf0150 -1226635000,41,5,01e4fe4d -1226645000,41,6,fee802b1 -1226655000,41,7,fe52fd4f -1226665000,41,8,0113ff4c -1226675000,41,9,00bb042b -1226685000,41,10,fe06f9a5 -1226695000,41,11,ffe106ae -1226705000,41,12,026efb0b -1226715000,41,13,ff2a0223 -1226725000,41,14,fdaaffcc -1226735000,41,15,03180000 -1226745000,41,16,fe850078 -1226755000,41,17,02da0072 -1226765000,41,18,fbe9fd29 -1226775000,41,19,01780375 -1226785000,41,20,0035fd16 -1226795000,41,21,ff6c007b -1226805000,41,22,ffde01c3 -1226815000,41,23,ffc4fdfd -1226825000,41,24,006101ec -1226835000,41,25,ffe1ffb3 -1226845000,41,26,01d0ffd5 -1226855000,41,27,ff1bffa2 -1226865000,41,28,fec2009b -1226875000,41,29,0106ff75 -1226885000,41,30,ffb80084 -1226895000,41,31,003eff60 -1231175000,42,0,ffc9002c -1231185000,42,1,004b0004 -1231195000,42,2,ffe70011 -1231205000,42,3,fff6ff97 -1231215000,42,4,ff7100c7 -1231225000,42,5,01edfee3 -1231235000,42,6,fdcf010e -1231245000,42,7,017efd79 -1231255000,42,8,fdae0385 -1231265000,42,9,0354ff8c -1231275000,42,10,fd41feae -1231285000,42,11,015601b7 -1231295000,42,12,004bfc68 -1231305000,42,13,fefaffd9 -1231315000,42,14,02e103bd -1231325000,42,15,fcfdff5b -1231335000,42,16,fffffca2 -1231345000,42,17,030902fa -1231355000,42,18,f91dff8f -1231365000,42,19,04a0fecf -1231375000,42,20,fff500d7 -1231385000,42,21,fe49ff0f -1231395000,42,22,020d03aa -1231405000,42,23,ff1cfc97 -1231415000,42,24,ffa20179 -1231425000,42,25,00acffda -1231435000,42,26,002f0066 -1231445000,42,27,0038fef7 -1231455000,42,28,fe970106 -1231465000,42,29,0114ff09 -1231475000,42,30,001f00df -1231485000,42,31,fffdff69 -1235765000,43,0,ff980040 -1235775000,43,1,0085ffe8 -1235785000,43,2,ff8b0066 -1235795000,43,3,ffd1ff3a -1235805000,43,4,000f0172 -1235815000,43,5,01a3fdb4 -1235825000,43,6,fed20048 -1235835000,43,7,fd8d0229 -1235845000,43,8,0449fe5c -1235855000,43,9,fd1c01d7 -1235865000,43,10,0127fdcf -1235875000,43,11,fece0297 -1235885000,43,12,ff35fad1 -1235895000,43,13,0544031d -1235905000,43,14,faab0167 -1235915000,43,15,03fcfee0 -1235925000,43,16,fcb6ffa0 -1235935000,43,17,017301f0 -1235945000,43,18,fdd5fbe4 -1235955000,43,19,031b03c2 -1235965000,43,20,fc5dfc8c -1235975000,43,21,027f027e -1235985000,43,22,00a20050 -1235995000,43,23,fe93ff91 -1236005000,43,24,004dffec -1236015000,43,25,00ecff79 -1236025000,43,26,fee50153 -1236035000,43,27,ff66fe89 -1236045000,43,28,01230159 -1236055000,43,29,ffeaff09 -1236065000,43,30,000d00b5 -1236075000,43,31,0034ff5a -1240355000,44,0,00150056 -1240365000,44,1,ffe9001a -1240375000,44,2,0011ffc6 -1240385000,44,3,ffb40012 -1240395000,44,4,ffe50129 -1240405000,44,5,0178fc9e -1240415000,44,6,00620307 -1240425000,44,7,fc80fed6 -1240435000,44,8,025b01c4 -1240445000,44,9,ffd9ff9c -1240455000,44,10,0005fcd3 -1240465000,44,11,0343058e -1240475000,44,12,faaffb53 -1240485000,44,13,051dffba -1240495000,44,14,fa350203 -1240505000,44,15,0387fd53 -1240515000,44,16,ffc90214 -1240525000,44,17,00770046 -1240535000,44,18,fcb7feda -1240545000,44,19,02900116 -1240555000,44,20,006dff21 -1240565000,44,21,fe9e0008 -1240575000,44,22,febe033d -1240585000,44,23,02f0fadc -1240595000,44,24,fdf7042e -1240605000,44,25,fffbfda8 -1240615000,44,26,02770099 -1240625000,44,27,fd9dffde -1240635000,44,28,013f0097 -1240645000,44,29,ff91ffdc -1240655000,44,30,fff7004d -1240665000,44,31,ffedff57 -1244945000,45,0,ffbf004f -1244955000,45,1,0046ff78 -1244965000,45,2,ff7e00b8 -1244975000,45,3,00ddff29 -1244985000,45,4,fed201ff -1244995000,45,5,00edfc60 -1245005000,45,6,02190410 -1245015000,45,7,fca2fca4 -1245025000,45,8,019e01dc -1245035000,45,9,0013feb6 -1245045000,45,10,fdb0ff8b -1245055000,45,11,02ef0324 -1245065000,45,12,fd21fcff -1245075000,45,13,018d0127 -1245085000,45,14,001bfe80 -1245095000,45,15,000b02d9 -1245105000,45,16,0461fd43 -1245115000,45,17,fa22010c -1245125000,45,18,ffae00b2 -1245135000,45,19,ffcbff55 -1245145000,45,20,0386ffb5 -1245155000,45,21,fd6dff24 -1245165000,45,22,ffd10212 -1245175000,45,23,0222fdd0 -1245185000,45,24,ff9a02ba -1245195000,45,25,fd99ff3a -1245205000,45,26,0290ff0f -1245215000,45,27,fded0096 -1245225000,45,28,018f0085 -1245235000,45,29,002dff71 -1245245000,45,30,ff4f002a -1245255000,45,31,0065ffbb -1249535000,46,0,00270035 -1249545000,46,1,ffe7ffce -1249555000,46,2,0017fff2 -1249565000,46,3,ffeaffd2 -1249575000,46,4,0016010c -1249585000,46,5,012dfef3 -1249595000,46,6,ffa50139 -1249605000,46,7,fdd7fea3 -1249615000,46,8,01ab0018 -1249625000,46,9,0161ff62 -1249635000,46,10,fdd7fff8 -1249645000,46,11,ffad01e3 -1249655000,46,12,0077ff0b -1249665000,46,13,0055feb2 -1249675000,46,14,006702c3 -1249685000,46,15,fed4fcee -1249695000,46,16,027d0295 -1249705000,46,17,0245fd82 -1249715000,46,18,f6f7012c -1249725000,46,19,06c200fc -1249735000,46,20,fc3efd3e -1249745000,46,21,024700ef -1249755000,46,22,fef301cb -1249765000,46,23,0039fe99 -1249775000,46,24,001101de -1249785000,46,25,ffbbfdc2 -1249795000,46,26,01ef0316 -1249805000,46,27,fd6ffdfb -1249815000,46,28,0195001b -1249825000,46,29,ff970070 -1249835000,46,30,007dffad -1249845000,46,31,ff9cffe2 -1254125000,47,0,ffde005b -1254135000,47,1,0019ff2d -1254145000,47,2,fffe00a1 -1254155000,47,3,ffe3ff6f -1254165000,47,4,ffd000b6 -1254175000,47,5,ff8bff5d -1254185000,47,6,00e7015e -1254195000,47,7,005eff15 -1254205000,47,8,ff22fee6 -1254215000,47,9,ffbe01c8 -1254225000,47,10,0224fd98 -1254235000,47,11,fca1038d -1254245000,47,12,029cfd35 -1254255000,47,13,fcb4ff8e -1254265000,47,14,04360082 -1254275000,47,15,feac0218 -1254285000,47,16,ffeafce3 -1254295000,47,17,005f010f -1254305000,47,18,fc1a000b -1254315000,47,19,010b0305 -1254325000,47,20,032afd36 -1254335000,47,21,fba5ff83 -1254345000,47,22,0411ff4a -1254355000,47,23,fff2048b -1254365000,47,24,fe26fd74 -1254375000,47,25,016a0058 -1254385000,47,26,feb0fff8 -1254395000,47,27,0229ffef -1254405000,47,28,fe42ff77 -1254415000,47,29,00c4ffd6 -1254425000,47,30,ffde009a -1254435000,47,31,0024ff88 -1258715000,48,0,fffa0024 -1258725000,48,1,0019ffc0 -1258735000,48,2,ff9c0029 -1258745000,48,3,005eff8b -1258755000,48,4,fe6b0233 -1258765000,48,5,03c5fbb6 -1258775000,48,6,fdab0344 -1258785000,48,7,ff4aff65 -1258795000,48,8,0181010a -1258805000,48,9,fc7bfeb5 -1258815000,48,10,02d6fe71 -1258825000,48,11,024705a3 -1258835000,48,12,fe08fa76 -1258845000,48,13,fe59fff1 -1258855000,48,14,036401fa -1258865000,48,15,febdfe78 -1258875000,48,16,fde20142 -1258885000,48,17,00b10052 -1258895000,48,18,009efe31 -1258905000,48,19,fe0a02ad -1258915000,48,20,0219ff47 -1258925000,48,21,ffcbfdea -1258935000,48,22,007d0264 -1258945000,48,23,fddefd01 -1258955000,48,24,03d705cc -1258965000,48,25,fc83fb21 -1258975000,48,26,013801a9 -1258985000,48,27,0001ffe5 -1258995000,48,28,0058ff84 -1259005000,48,29,ffcf004f -1259015000,48,30,0024ffea -1259025000,48,31,ffdbffea -1263305000,49,0,ff9e0067 -1263315000,49,1,008eff53 -1263325000,49,2,ff18011e -1263335000,49,3,013bfe83 -1263345000,49,4,ff09028c -1263355000,49,5,00e2fba1 -1263365000,49,6,ffbd0461 -1263375000,49,7,ffc4fc69 -1263385000,49,8,febf01a8 -1263395000,49,9,fffb017a -1263405000,49,10,0322fbee -1263415000,49,11,fdab04ce -1263425000,49,12,ffe2fc07 -1263435000,49,13,ffe000e7 -1263445000,49,14,01300248 -1263455000,49,15,ff40fcef -1263465000,49,16,ff6a01f7 -1263475000,49,17,01b2ff3d -1263485000,49,18,fca00160 -1263495000,49,19,00ffff01 -1263505000,49,20,0337fee8 -1263515000,49,21,fc700079 -1263525000,49,22,01830149 -1263535000,49,23,00d8fdcf -1263545000,49,24,feb5040e -1263555000,49,25,feb1fdfe -1263565000,49,26,0226fff8 -1263575000,49,27,ff73ff1e -1263585000,49,28,006200d1 -1263595000,49,29,ffe20057 -1263605000,49,30,0010ffda -1263615000,49,31,004cff79 -1267895000,50,0,ff760033 -1267905000,50,1,008d0000 -1267915000,50,2,ffa9ff8a -1267925000,50,3,ffbe0082 -1267935000,50,4,fff6ffb1 -1267945000,50,5,010fff1f -1267955000,50,6,fd6901b1 -1267965000,50,7,03e9fed8 -1267975000,50,8,fcceffdf -1267985000,50,9,024602a3 -1267995000,50,10,fc71fd64 -1268005000,50,11,031601d3 -1268015000,50,12,ff84fce2 -1268025000,50,13,fef8ffdd -1268035000,50,14,03730404 -1268045000,50,15,fd85fc83 -1268055000,50,16,fe36fedd -1268065000,50,17,026d03f6 -1268075000,50,18,fdabfda0 -1268085000,50,19,ffb602a4 -1268095000,50,20,ffcafbfb -1268105000,50,21,0363040d -1268115000,50,22,fd93ff6f -1268125000,50,23,00e7fbd6 -1268135000,50,24,ffda0541 -1268145000,50,25,0034fd8b -1268155000,50,26,ff3f01ce -1268165000,50,27,019afe4f -1268175000,50,28,fe7800b2 -1268185000,50,29,013aff33 -1268195000,50,30,ff3d00d0 -1268205000,50,31,00afff67 -1272485000,51,0,ffdb0044 -1272495000,51,1,ffdaff9c -1272505000,51,2,ffef00b9 -1272515000,51,3,0157fed3 -1272525000,51,4,fe0001e5 -1272535000,51,5,0281fd08 -1272545000,51,6,ff120180 -1272555000,51,7,fece0023 -1272565000,51,8,00f00113 -1272575000,51,9,fe52fdce -1272585000,51,10,010c01be -1272595000,51,11,0126fdd0 -1272605000,51,12,fed90341 -1272615000,51,13,ff8efcf7 -1272625000,51,14,0257fe29 -1272635000,51,15,fd590490 -1272645000,51,16,0191fce8 -1272655000,51,17,fde605a2 -1272665000,51,18,0277faf3 -1272675000,51,19,fd8f006b -1272685000,51,20,001600a3 -1272695000,51,21,0175fe9e -1272705000,51,22,01320222 -1272715000,51,23,ff08fc3f -1272725000,51,24,fe2c067d -1272735000,51,25,00b6fc18 -1272745000,51,26,0116008a -1272755000,51,27,feb00062 -1272765000,51,28,00f1ffc3 -1272775000,51,29,0094001f -1272785000,51,30,ff550029 -1272795000,51,31,0055ff8e -1277075000,52,0,ff95003a -1277085000,52,1,008bffec -1277095000,52,2,ff17fffb -1277105000,52,3,00efffd4 -1277115000,52,4,ff5d01a4 -1277125000,52,5,0226fc4c -1277135000,52,6,fddd020f -1277145000,52,7,008101af -1277155000,52,8,fe7bfcd3 -1277165000,52,9,0214028b -1277175000,52,10,fe67fece -1277185000,52,11,02020120 -1277195000,52,12,0008fd51 -1277205000,52,13,fcd901e8 -1277215000,52,14,0255fe91 -1277225000,52,15,0004fff0 -1277235000,52,16,ff3d026e -1277245000,52,17,02f301bc -1277255000,52,18,f9e3fadb -1277265000,52,19,05210186 -1277275000,52,20,fcef0192 -1277285000,52,21,0110fc7e -1277295000,52,22,016103b3 -1277305000,52,23,fd73fce3 -1277315000,52,24,01eb04f9 -1277325000,52,25,feb2fcf1 -1277335000,52,26,fffb004c -1277345000,52,27,01eaffca -1277355000,52,28,fe7c0055 -1277365000,52,29,005d0042 -1277375000,52,30,0019ffed -1277385000,52,31,004cffa2 -1281665000,53,0,ffbeffbe -1281675000,53,1,ffdb0062 -1281685000,53,2,ffb6ff89 -1281695000,53,3,00e1fff2 -1281705000,53,4,00890151 -1281715000,53,5,ff16fe27 -1281725000,53,6,ffa30056 -1281735000,53,7,00c80080 -1281745000,53,8,fdd5ff6a -1281755000,53,9,029000cb -1281765000,53,10,fd72ff38 -1281775000,53,11,03ed04c0 -1281785000,53,12,fcf7f839 -1281795000,53,13,00c703dc -1281805000,53,14,00f2ffa7 -1281815000,53,15,ffedfef4 -1281825000,53,16,ff84ffda -1281835000,53,17,fe7101ee -1281845000,53,18,019a022b -1281855000,53,19,ff95fb6e -1281865000,53,20,ff470029 -1281875000,53,21,00e2ffcb -1281885000,53,22,ff250232 -1281895000,53,23,0034ffd4 -1281905000,53,24,00e9ff86 -1281915000,53,25,fea80011 -1281925000,53,26,01560108 -1281935000,53,27,ff2dffa0 -1281945000,53,28,ffc9ff95 -1281955000,53,29,017dffbe -1281965000,53,30,feaefffd -1281975000,53,31,00b70050 -1286255000,54,0,00410077 -1286265000,54,1,ffccff9a -1286275000,54,2,fffaffc1 -1286285000,54,3,ffbe0091 -1286295000,54,4,00d60006 -1286305000,54,5,ffb7ff78 -1286315000,54,6,ff6affe7 -1286325000,54,7,00330060 -1286335000,54,8,0121000e -1286345000,54,9,feb7fefc -1286355000,54,10,0018020b -1286365000,54,11,fe1cffb1 -1286375000,54,12,01fefdc7 -1286385000,54,13,005701b3 -1286395000,54,14,ffc8ffc4 -1286405000,54,15,033ffea0 -1286415000,54,16,f9170255 -1286425000,54,17,056efd6a -1286435000,54,18,fb1c048d -1286445000,54,19,035efc37 -1286455000,54,20,ff3800a4 -1286465000,54,21,002100dc -1286475000,54,22,029efe4d -1286485000,54,23,fb850144 -1286495000,54,24,023f012e -1286505000,54,25,ff7ffd50 -1286515000,54,26,00aa02c7 -1286525000,54,27,ffc0feab -1286535000,54,28,ffdc008f -1286545000,54,29,0061ff31 -1286555000,54,30,007800c0 -1286565000,54,31,ff51ff30 -1290845000,55,0,ff60ffd2 -1290855000,55,1,00a40026 -1290865000,55,2,ff07ffed -1290875000,55,3,01a70043 -1290885000,55,4,fe8bffcc -1290895000,55,5,01c2feb5 -1290905000,55,6,fec300e6 -1290915000,55,7,00b5015f -1290925000,55,8,fe83fe2b -1290935000,55,9,fffb00bf -1290945000,55,10,ff97fea3 -1290955000,55,11,0511022b -1290965000,55,12,f861fe1f -1290975000,55,13,07c50026 -1290985000,55,14,f9dcfe9a -1290995000,55,15,02e6039e -1291005000,55,16,fe5eff34 -1291015000,55,17,00cafcc6 -1291025000,55,18,ff5f04fb -1291035000,55,19,ff53fc75 -1291045000,55,20,0247ffea -1291055000,55,21,ff74ff27 -1291065000,55,22,fd770366 -1291075000,55,23,0255fd3f -1291085000,55,24,004f02e3 -1291095000,55,25,fcc3fe0d -1291105000,55,26,01cf0145 -1291115000,55,27,01f9ffa9 -1291125000,55,28,fe7dffa7 -1291135000,55,29,00990056 -1291145000,55,30,ff2effaa -1291155000,55,31,00fc0038 -1295435000,56,0,ffd5007c -1295445000,56,1,0030ff5a -1295455000,56,2,ff9b0092 -1295465000,56,3,0013ff42 -1295475000,56,4,ff2d027a -1295485000,56,5,036dfb56 -1295495000,56,6,fcc303da -1295505000,56,7,0110ff93 -1295515000,56,8,fed9fe91 -1295525000,56,9,0011021c -1295535000,56,10,ffe1fd16 -1295545000,56,11,0354073a -1295555000,56,12,fd15f76b -1295565000,56,13,00a6025c -1295575000,56,14,0196ffd2 -1295585000,56,15,ff50feb0 -1295595000,56,16,fe53037a -1295605000,56,17,0134fefc -1295615000,56,18,fe0dff60 -1295625000,56,19,001b015c -1295635000,56,20,01c9fd72 -1295645000,56,21,ff6701a6 -1295655000,56,22,0011006a -1295665000,56,23,0024fc25 -1295675000,56,24,00a706c5 -1295685000,56,25,fec7fcd2 -1295695000,56,26,005fff44 -1295705000,56,27,00caffdc -1295715000,56,28,ff3d00dd -1295725000,56,29,000aff9c -1295735000,56,30,003e004e -1295745000,56,31,fff0ff7c -1300025000,57,0,0004003a -1300035000,57,1,0019ffe4 -1300045000,57,2,fecb004b -1300055000,57,3,017bff1f -1300065000,57,4,fea40120 -1300075000,57,5,02acfed8 -1300085000,57,6,fcc302d2 -1300095000,57,7,0300fbd7 -1300105000,57,8,fcd602bd -1300115000,57,9,0479ffed -1300125000,57,10,fa93fd24 -1300135000,57,11,0144052f -1300145000,57,12,0113fb1f -1300155000,57,13,002901bb -1300165000,57,14,0077011c -1300175000,57,15,0072fe90 -1300185000,57,16,ff4c0146 -1300195000,57,17,0017ff5c -1300205000,57,18,fe4b0135 -1300215000,57,19,01dbfee5 -1300225000,57,20,ff62fd92 -1300235000,57,21,002c01c8 -1300245000,57,22,ff2f01f8 -1300255000,57,23,006afca1 -1300265000,57,24,010604a3 -1300275000,57,25,fe37fd97 -1300285000,57,26,021bffa0 -1300295000,57,27,ffe2fffd -1300305000,57,28,feab00af -1300315000,57,29,0117ff19 -1300325000,57,30,ffb30096 -1300335000,57,31,ffe0ff70 -1304615000,58,0,ffcd0048 -1304625000,58,1,0069ffbd -1304635000,58,2,ff3600ba -1304645000,58,3,009cfe83 -1304655000,58,4,fee20260 -1304665000,58,5,01ddfd82 -1304675000,58,6,ff8501df -1304685000,58,7,ff7dff24 -1304695000,58,8,0110001e -1304705000,58,9,ff2501d5 -1304715000,58,10,fe43fa44 -1304725000,58,11,011d0940 -1304735000,58,12,00d7f8ef -1304745000,58,13,01feffa3 -1304755000,58,14,fef00279 -1304765000,58,15,ffdcfff3 -1304775000,58,16,fd6dff86 -1304785000,58,17,02e5fe69 -1304795000,58,18,fc9e01ce -1304805000,58,19,017a0063 -1304815000,58,20,ff70fdd0 -1304825000,58,21,00b302bc -1304835000,58,22,025900e5 -1304845000,58,23,fd89fb9a -1304855000,58,24,00da03d8 -1304865000,58,25,ff71fdb1 -1304875000,58,26,0071020c -1304885000,58,27,000dfe5e -1304895000,58,28,feeb00c5 -1304905000,58,29,0136fefb -1304915000,58,30,ffb20133 -1304925000,58,31,fff6ff53 -1309205000,59,0,ffa20062 -1309215000,59,1,0025ffb4 -1309225000,59,2,ff4cffd3 -1309235000,59,3,019e0052 -1309245000,59,4,fda6018f -1309255000,59,5,02e8fd07 -1309265000,59,6,fe19021e -1309275000,59,7,ffeafed2 -1309285000,59,8,ff4e0010 -1309295000,59,9,02f201cd -1309305000,59,10,fe1dfd46 -1309315000,59,11,01860302 -1309325000,59,12,fdd3fe1c -1309335000,59,13,00b2fedf -1309345000,59,14,fe4e01b8 -1309355000,59,15,03b0fe1a -1309365000,59,16,fd320270 -1309375000,59,17,0139fce2 -1309385000,59,18,fe2001f9 -1309395000,59,19,01a002a4 -1309405000,59,20,003cfbaf -1309415000,59,21,fe160341 -1309425000,59,22,003d0060 -1309435000,59,23,02eefcf6 -1309445000,59,24,fcce02be -1309455000,59,25,02e0fd95 -1309465000,59,26,ff4301f6 -1309475000,59,27,ffecfe28 -1309485000,59,28,feb3013e -1309495000,59,29,0138ffc9 -1309505000,59,30,ff58006a -1309515000,59,31,0090ff36 -1313795000,60,0,ffb7002c -1313805000,60,1,006ffffc -1313815000,60,2,ff370067 -1313825000,60,3,0089fedf -1313835000,60,4,005802ad -1313845000,60,5,ffb3fb0c -1313855000,60,6,ff8b038c -1313865000,60,7,0127ff36 -1313875000,60,8,fefe00b8 -1313885000,60,9,008200d7 -1313895000,60,10,fd65fd94 -1313905000,60,11,070c0507 -1313915000,60,12,f9d4f7e1 -1313925000,60,13,02c703d1 -1313935000,60,14,fe110197 -1313945000,60,15,0214fc5f -1313955000,60,16,ff010168 -1313965000,60,17,fef50222 -1313975000,60,18,fee7fb7f -1313985000,60,19,02ab03a7 -1313995000,60,20,ff3c0013 -1314005000,60,21,ff3dfee0 -1314015000,60,22,feab0280 -1314025000,60,23,0101fc38 -1314035000,60,24,01d60344 -1314045000,60,25,feb2fdd3 -1314055000,60,26,006900c6 -1314065000,60,27,ffe80013 -1314075000,60,28,000400d7 -1314085000,60,29,fff1fef3 -1314095000,60,30,ffd50085 -1314105000,60,31,005cffab -1318385000,61,0,ffc80047 -1318395000,61,1,0048ff40 -1318405000,61,2,ffca00f1 -1318415000,61,3,ffd8ff7d -1318425000,61,4,00690135 -1318435000,61,5,00d3fd5f -1318445000,61,6,ff0e0096 -1318455000,61,7,006e0014 -1318465000,61,8,ff570267 -1318475000,61,9,fff5fd7a -1318485000,61,10,fef600a9 -1318495000,61,11,00e80241 -1318505000,61,12,0109fb0a -1318515000,61,13,fee501a8 -1318525000,61,14,009001cf -1318535000,61,15,00d8ff87 -1318545000,61,16,fe50000f -1318555000,61,17,0284feb0 -1318565000,61,18,fa9a0265 -1318575000,61,19,02cefd17 -1318585000,61,20,036f020d -1318595000,61,21,f9a1ff85 -1318605000,61,22,0544feb6 -1318615000,61,23,fbbc010c -1318625000,61,24,03f90253 -1318635000,61,25,ff07fdba -1318645000,61,26,fe7e00a9 -1318655000,61,27,0106ff6b -1318665000,61,28,005700ec -1318675000,61,29,ff3fff50 -1318685000,61,30,00b60055 -1318695000,61,31,fffaffb9 -1322975000,62,0,ff710080 -1322985000,62,1,00d2ff05 -1322995000,62,2,fe670104 -1323005000,62,3,0213ff44 -1323015000,62,4,fe2e01be -1323025000,62,5,02c5fda7 -1323035000,62,6,fda7003d -1323045000,62,7,ff2f00cf -1323055000,62,8,0139fdce -1323065000,62,9,010002f8 -1323075000,62,10,fd7fff1f -1323085000,62,11,011500ee -1323095000,62,12,fdfafd2d -1323105000,62,13,04fa02aa -1323115000,62,14,fcedff82 -1323125000,62,15,00ecfe81 -1323135000,62,16,004bff7e -1323145000,62,17,ff22034d -1323155000,62,18,fe51fda2 -1323165000,62,19,017f004a -1323175000,62,20,fe6a00e8 -1323185000,62,21,01e3fd5f -1323195000,62,22,0001030b -1323205000,62,23,ff7ffefd -1323215000,62,24,ff370078 -1323225000,62,25,0124002a -1323235000,62,26,0071007b -1323245000,62,27,feddff6c -1323255000,62,28,00e20019 -1323265000,62,29,ffe6ff34 -1323275000,62,30,ffd300e6 -1323285000,62,31,0092ff53 -1327565000,63,0,0027002d -1327575000,63,1,ff93ffd2 -1327585000,63,2,fff3ffce -1327595000,63,3,00d1006e -1327605000,63,4,ffe4ff9d -1327615000,63,5,ff7e0140 -1327625000,63,6,0081fe46 -1327635000,63,7,fe66006f -1327645000,63,8,0261ffff -1327655000,63,9,ff2affc1 -1327665000,63,10,ff3affa4 -1327675000,63,11,ff560297 -1327685000,63,12,ffaafe2d -1327695000,63,13,01e5fe96 -1327705000,63,14,00a700ea -1327715000,63,15,ff53ffa8 -1327725000,63,16,fbab005b -1327735000,63,17,06010092 -1327745000,63,18,fbf70488 -1327755000,63,19,01fff960 -1327765000,63,20,fdac034b -1327775000,63,21,0408feda -1327785000,63,22,fde1fece -1327795000,63,23,009a00f7 -1327805000,63,24,fe5900ad -1327815000,63,25,01e2003b -1327825000,63,26,ffc0ff7e -1327835000,63,27,fef6ffff -1327845000,63,28,00ea0177 -1327855000,63,29,ffb5fd68 -1327865000,63,30,0093019a -1327875000,63,31,ffa1ff46 +1035995000,0,0,ffedffb5 +1036005000,0,1,0056002f +1036015000,0,2,fef8fdf4 +1036025000,0,3,00bb03e9 +1036035000,0,4,ff67fd79 +1036045000,0,5,02890060 +1036055000,0,6,fb84ff8a +1036065000,0,7,0391003b +1036075000,0,8,ff0d0073 +1036085000,0,9,00ae0099 +1036095000,0,10,fc9cfefa +1036105000,0,11,0433fe37 +1036115000,0,12,fe0303e7 +1036125000,0,13,0027fe60 +1036135000,0,14,000cff08 +1036145000,0,15,00450115 +1038025000,0,16,ffda004b +1038035000,0,17,ff09ff80 +1038045000,0,18,01e40140 +1038055000,0,19,fed4fde4 +1038065000,0,20,019102c6 +1038075000,0,21,fac60187 +1038085000,0,22,046efae2 +1038095000,0,23,fe600256 +1038105000,0,24,fee0ff41 +1038115000,0,25,0131002c +1038125000,0,26,0220ffcc +1038135000,0,27,fd1cfdfa +1038145000,0,28,019104b2 +1038155000,0,29,ff4cfd11 +1038165000,0,30,003a00be +1038175000,0,31,001cffd8 +1040055000,1,0,ffe3ffff +1040065000,1,1,00e1ff79 +1040075000,1,2,fef10061 +1040085000,1,3,01650080 +1040095000,1,4,fd9dfeef +1040105000,1,5,04330116 +1040115000,1,6,fa4cfd95 +1040125000,1,7,04660217 +1040135000,1,8,ff33ffb5 +1040145000,1,9,fe990157 +1040155000,1,10,0051fca7 +1040165000,1,11,004bffd8 +1040175000,1,12,ff6d04e5 +1040185000,1,13,0067fd3e +1040195000,1,14,0072ffcb +1040205000,1,15,ffb6007d +1042085000,1,16,ffc10027 +1042095000,1,17,ffe40007 +1042105000,1,18,00f5ff8a +1042115000,1,19,fe04001c +1042125000,1,20,00f202a7 +1042135000,1,21,ff5bfd55 +1042145000,1,22,feb5fef5 +1042155000,1,23,0588007d +1042165000,1,24,f9e10265 +1042175000,1,25,01f6fdf7 +1042185000,1,26,ff51020e +1042195000,1,27,0086fcd4 +1042205000,1,28,01b00389 +1042215000,1,29,fd7bfe31 +1042225000,1,30,012900ef +1042235000,1,31,ffb6ff77 +1044115000,2,0,ffc4ffdb +1044125000,2,1,0092fffd +1044135000,2,2,ff0fff3e +1044145000,2,3,01d8002f +1044155000,2,4,fd6a0180 +1044165000,2,5,018cff18 +1044175000,2,6,fcf7fdb8 +1044185000,2,7,0745034a +1044195000,2,8,fa62ff4f +1044205000,2,9,fe6afe99 +1044215000,2,10,035d0062 +1044225000,2,11,ffdc00f1 +1044235000,2,12,feb4ffea +1044245000,2,13,0108ff8e +1044255000,2,14,ff01002c +1044265000,2,15,00cf0042 +1046145000,2,16,ffa3000e +1046155000,2,17,ff84ff01 +1046165000,2,18,018c02cb +1046175000,2,19,fe98fd35 +1046185000,2,20,003103aa +1046195000,2,21,fe93faee +1046205000,2,22,0224020a +1046215000,2,23,fe3d00ab +1046225000,2,24,01e3ff84 +1046235000,2,25,ff2a029f +1046245000,2,26,fd70fc23 +1046255000,2,27,01b60147 +1046265000,2,28,0155014c +1046275000,2,29,ff27ffa6 +1046285000,2,30,fea40058 +1046295000,2,31,00ddffdd +1048175000,3,0,fffeffe6 +1048185000,3,1,00e5ffd5 +1048195000,3,2,fe89fefd +1048205000,3,3,01c5016b +1048215000,3,4,fe9affd6 +1048225000,3,5,0024ffe3 +1048235000,3,6,fe8bfebe +1048245000,3,7,03d80037 +1048255000,3,8,fdc802fe +1048265000,3,9,ff8ffe6d +1048275000,3,10,ffb5fd0f +1048285000,3,11,01cb014d +1048295000,3,12,fe000306 +1048305000,3,13,0088fe47 +1048315000,3,14,00ffff66 +1048325000,3,15,ff5000b5 +1050205000,3,16,ffd1ffc7 +1050215000,3,17,003bff75 +1050225000,3,18,00970297 +1050235000,3,19,fde0fd96 +1050245000,3,20,01fa004e +1050255000,3,21,fe6d01df +1050265000,3,22,0139fb91 +1050275000,3,23,fd19057f +1050285000,3,24,038bfb63 +1050295000,3,25,ff890353 +1050305000,3,26,ff93fcef +1050315000,3,27,fe5002b6 +1050325000,3,28,018e00ec +1050335000,3,29,ff4bfe39 +1050345000,3,30,00890075 +1050355000,3,31,ff8b0065 +1052235000,4,0,0025001e +1052245000,4,1,00e1ffd5 +1052255000,4,2,fea0febf +1052265000,4,3,0168024f +1052275000,4,4,ff8dff20 +1052285000,4,5,0079004f +1052295000,4,6,fd6afe0b +1052305000,4,7,03e40009 +1052315000,4,8,fef50360 +1052325000,4,9,fe3ffdaf +1052335000,4,10,ffdcfec1 +1052345000,4,11,01fcffcd +1052355000,4,12,fd61035e +1052365000,4,13,01affebd +1052375000,4,14,006aff41 +1052385000,4,15,ff180083 +1054265000,4,16,ffedfff3 +1054275000,4,17,fe94003c +1054285000,4,18,029eff3a +1054295000,4,19,fd74013c +1054305000,4,20,015dfe53 +1054315000,4,21,febd02fb +1054325000,4,22,ff39fac8 +1054335000,4,23,02790477 +1054345000,4,24,fe83ffa7 +1054355000,4,25,ffc2fd6c +1054365000,4,26,00520068 +1054375000,4,27,ff72ffdc +1054385000,4,28,02ef038f +1054395000,4,29,fda5fdbd +1054405000,4,30,ff8b004a +1054415000,4,31,00d9ffe1 +1056295000,5,0,00420058 +1056305000,5,1,ffaeff4f +1056315000,5,2,fffb00d1 +1056325000,5,3,00f6ff23 +1056335000,5,4,fe26012a +1056345000,5,5,01ddffa5 +1056355000,5,6,fcccfe0b +1056365000,5,7,03570175 +1056375000,5,8,00ca0380 +1056385000,5,9,fbfafa9d +1056395000,5,10,01db0215 +1056405000,5,11,00e4ffbf +1056415000,5,12,ff7e01da +1056425000,5,13,001bfdf3 +1056435000,5,14,ffd600db +1056445000,5,15,0007ff7d +1058325000,5,16,fffafff5 +1058335000,5,17,000d0058 +1058345000,5,18,012dff89 +1058355000,5,19,fe590051 +1058365000,5,20,ff36005e +1058375000,5,21,02c60171 +1058385000,5,22,fc8ffe01 +1058395000,5,23,00b9fde1 +1058405000,5,24,02a401bf +1058415000,5,25,fd7b0048 +1058425000,5,26,01c900cd +1058435000,5,27,fbb5ff11 +1058445000,5,28,059c002a +1058455000,5,29,fcf60097 +1058465000,5,30,00ebfffd +1058475000,5,31,ff85ffd5 +1060355000,6,0,ff8bff8e +1060365000,6,1,ff9b004f +1060375000,6,2,018aff23 +1060385000,6,3,fd0501b3 +1060395000,6,4,034dfebf +1060405000,6,5,fd1900e3 +1060415000,6,6,0206005b +1060425000,6,7,ff6bfd29 +1060435000,6,8,02fd022a +1060445000,6,9,f9890147 +1060455000,6,10,023afdd3 +1060465000,6,11,042b0027 +1060475000,6,12,fce30169 +1060485000,6,13,002bff47 +1060495000,6,14,ff8eff27 +1060505000,6,15,00ed00e5 +1062385000,6,16,0053ffd4 +1062395000,6,17,ff90000b +1062405000,6,18,00f40139 +1062415000,6,19,ff43fd97 +1062425000,6,20,02a2022c +1062435000,6,21,fb00fe91 +1062445000,6,22,036c0250 +1062455000,6,23,ff0dfde6 +1062465000,6,24,021dff70 +1062475000,6,25,fccc01bd +1062485000,6,26,017eff43 +1062495000,6,27,ff3d00a5 +1062505000,6,28,ff7eff2c +1062515000,6,29,01c8ff53 +1062525000,6,30,00720178 +1062535000,6,31,ff2fffa2 +1064415000,7,0,00390021 +1064425000,7,1,ffebffe1 +1064435000,7,2,ff0dfe43 +1064445000,7,3,02fa0231 +1064455000,7,4,fc1001aa +1064465000,7,5,02eafb6d +1064475000,7,6,fde3015d +1064485000,7,7,011a0181 +1064495000,7,8,0033ff1b +1064505000,7,9,fd91021b +1064515000,7,10,033dfdd3 +1064525000,7,11,fea2ffcb +1064535000,7,12,006c01fa +1064545000,7,13,fffafe33 +1064555000,7,14,000b00b5 +1064565000,7,15,ffcaffdf +1066445000,7,16,0001ffb9 +1066455000,7,17,ff7900fb +1066465000,7,18,00fdff0a +1066475000,7,19,ff30ffee +1066485000,7,20,ff8d017e +1066495000,7,21,028bfe6a +1066505000,7,22,fb8f00ba +1066515000,7,23,02fdff37 +1066525000,7,24,ff4700f9 +1066535000,7,25,fde30025 +1066545000,7,26,018dfe9e +1066555000,7,27,00e600ba +1066565000,7,28,fdc3ff54 +1066575000,7,29,027d009e +1066585000,7,30,ff0f0122 +1066595000,7,31,0029ff81 +1068475000,8,0,fff8001d +1068485000,8,1,00500017 +1068495000,8,2,0020fe41 +1068505000,8,3,feb8030d +1068515000,8,4,0181fde1 +1068525000,8,5,016400dd +1068535000,8,6,fbe7fff9 +1068545000,8,7,01a1fcba +1068555000,8,8,033e044f +1068565000,8,9,fc22fe97 +1068575000,8,10,fe94009b +1068585000,8,11,03a0fc75 +1068595000,8,12,fe31066b +1068605000,8,13,0162fb7d +1068615000,8,14,ff050173 +1068625000,8,15,0047ffbc +1070505000,8,16,ffc7ffd8 +1070515000,8,17,ff09000e +1070525000,8,18,02450011 +1070535000,8,19,fefb0140 +1070545000,8,20,feb8fd04 +1070555000,8,21,ff280566 +1070565000,8,22,004df9d4 +1070575000,8,23,02fe0138 +1070585000,8,24,fef702f2 +1070595000,8,25,fd03fe0a +1070605000,8,26,021f00dd +1070615000,8,27,ffaffd78 +1070625000,8,28,00a203d6 +1070635000,8,29,ffb8feba +1070645000,8,30,ff4fffaa +1070655000,8,31,00a40038 +1072535000,9,0,001affd9 +1072545000,9,1,00200089 +1072555000,9,2,0053fdce +1072565000,9,3,ffbf0336 +1072575000,9,4,ffeffd64 +1072585000,9,5,fec601ef +1072595000,9,6,0109ff38 +1072605000,9,7,0256fec0 +1072615000,9,8,fe5401bb +1072625000,9,9,fd14ff05 +1072635000,9,10,02230030 +1072645000,9,11,0191ff94 +1072655000,9,12,fe4b01fc +1072665000,9,13,ffd2fdc7 +1072675000,9,14,01010126 +1072685000,9,15,ff66ffe2 +1074565000,9,16,ffbb0032 +1074575000,9,17,ffe9ff00 +1074585000,9,18,ffdd01f6 +1074595000,9,19,00c9fec2 +1074605000,9,20,015801ad +1074615000,9,21,f949fe9f +1074625000,9,22,05cbfe57 +1074635000,9,23,fd2f020a +1074645000,9,24,03f9ffde +1074655000,9,25,fadf0014 +1074665000,9,26,02a3ff22 +1074675000,9,27,0049ffa0 +1074685000,9,28,fef80257 +1074695000,9,29,00a3fee5 +1074705000,9,30,ff59000d +1074715000,9,31,0063ffdc +1076595000,10,0,0017ffff +1076605000,10,1,ff6f00a6 +1076615000,10,2,015cfe5b +1076625000,10,3,ff24013c +1076635000,10,4,ff7a009f +1076645000,10,5,002fffe2 +1076655000,10,6,ff20fda4 +1076665000,10,7,04ad0291 +1076675000,10,8,fc53ffbf +1076685000,10,9,fd63fd58 +1076695000,10,10,04380279 +1076705000,10,11,fe08ffb0 +1076715000,10,12,002c0063 +1076725000,10,13,00cbfea8 +1076735000,10,14,ff3401d0 +1076745000,10,15,0063fef3 +1078625000,10,16,0016fff6 +1078635000,10,17,ff19ff89 +1078645000,10,18,01810262 +1078655000,10,19,feb5fd68 +1078665000,10,20,01ee01a5 +1078675000,10,21,faf90129 +1078685000,10,22,03d6fab4 +1078695000,10,23,00f40372 +1078705000,10,24,fed0feac +1078715000,10,25,fe150305 +1078725000,10,26,0245fe54 +1078735000,10,27,fff9ffa4 +1078745000,10,28,feec01cd +1078755000,10,29,002dfe59 +1078765000,10,30,ffc4010a +1078775000,10,31,003affba +1080655000,11,0,006e001a +1080665000,11,1,ff8e0062 +1080675000,11,2,001dfe83 +1080685000,11,3,006301ea +1080695000,11,4,001500ac +1080705000,11,5,fe8afe41 +1080715000,11,6,0235fecd +1080725000,11,7,003c0102 +1080735000,11,8,fc400396 +1080745000,11,9,04def99c +1080755000,11,10,fc57048f +1080765000,11,11,0081fdd8 +1080775000,11,12,017d0110 +1080785000,11,13,fef2ffdd +1080795000,11,14,001f0065 +1080805000,11,15,fff0ff70 +1082685000,11,16,ff7fffd4 +1082695000,11,17,ff58ff40 +1082705000,11,18,028902a0 +1082715000,11,19,fca0fd55 +1082725000,11,20,01e50184 +1082735000,11,21,fd76005b +1082745000,11,22,01d9fd1c +1082755000,11,23,00d30044 +1082765000,11,24,fd710210 +1082775000,11,25,02c0ffd8 +1082785000,11,26,ffb5fee8 +1082795000,11,27,ff3affff +1082805000,11,28,00330264 +1082815000,11,29,fecefebd +1082825000,11,30,00b1ff30 +1082835000,11,31,007700c8 +1084715000,12,0,00060041 +1084725000,12,1,007dff8b +1084735000,12,2,fe8dff98 +1084745000,12,3,02c6015b +1084755000,12,4,fc2e0098 +1084765000,12,5,0378fe4f +1084775000,12,6,fef8fc43 +1084785000,12,7,ff1405e1 +1084795000,12,8,ffce0059 +1084805000,12,9,ff3ffad1 +1084815000,12,10,02b5044e +1084825000,12,11,fd1afe7b +1084835000,12,12,02aa0106 +1084845000,12,13,fe34ff51 +1084855000,12,14,00ea003f +1084865000,12,15,ffd4ffad +1086745000,12,16,ffc3ffd0 +1086755000,12,17,ff7e000c +1086765000,12,18,026700c8 +1086775000,12,19,fdc5ff4c +1086785000,12,20,005a00b6 +1086795000,12,21,fe2001fb +1086805000,12,22,0145f987 +1086815000,12,23,01460570 +1086825000,12,24,fee5fe36 +1086835000,12,25,fee400c4 +1086845000,12,26,01fffd1c +1086855000,12,27,fe7d034a +1086865000,12,28,01d2ffa4 +1086875000,12,29,fe02ff39 +1086885000,12,30,012100c5 +1086895000,12,31,ffb4ffe6 +1088775000,13,0,00510002 +1088785000,13,1,ff93ff34 +1088795000,13,2,ff1f0091 +1088805000,13,3,01f6ffc4 +1088815000,13,4,fef40137 +1088825000,13,5,008a0107 +1088835000,13,6,ffd2fb8b +1088845000,13,7,fe1101a1 +1088855000,13,8,04f1029e +1088865000,13,9,fb11feb6 +1088875000,13,10,011ffdbb +1088885000,13,11,01ac0330 +1088895000,13,12,fdfefd99 +1088905000,13,13,01b20203 +1088915000,13,14,ff5cfe71 +1088925000,13,15,ffcd00bf +1090805000,13,16,003fff78 +1090815000,13,17,ff120109 +1090825000,13,18,02f1fec6 +1090835000,13,19,fc88007b +1090845000,13,20,03e7ffb8 +1090855000,13,21,fb0a0229 +1090865000,13,22,03a1fd95 +1090875000,13,23,00010040 +1090885000,13,24,ff6d0144 +1090895000,13,25,fed2ff89 +1090905000,13,26,ff5dff2c +1090915000,13,27,0112fff3 +1090925000,13,28,01490194 +1090935000,13,29,ff82ff21 +1090945000,13,30,00150069 +1090955000,13,31,ffd5004e +1092835000,14,0,ff8affb9 +1092845000,14,1,00fc0054 +1092855000,14,2,ff14fefe +1092865000,14,3,017e028a +1092875000,14,4,fde3fc9b +1092885000,14,5,028b0178 +1092895000,14,6,fbda0173 +1092905000,14,7,061efc63 +1092915000,14,8,fcf8032d +1092925000,14,9,fde8fea6 +1092935000,14,10,00a8ff08 +1092945000,14,11,0236006e +1092955000,14,12,fe6701d7 +1092965000,14,13,ff41ff4e +1092975000,14,14,0116ff5f +1092985000,14,15,000600b5 +1094865000,14,16,0020001c +1094875000,14,17,ff0dffde +1094885000,14,18,02c60027 +1094895000,14,19,fb420001 +1094905000,14,20,04ef00a2 +1094915000,14,21,fb8e00b1 +1094925000,14,22,02e0fb2b +1094935000,14,23,00690239 +1094945000,14,24,fdea02c2 +1094955000,14,25,ff81fd2e +1094965000,14,26,001a002d +1094975000,14,27,02a2013b +1094985000,14,28,fe67fea8 +1094995000,14,29,ff3c013f +1095005000,14,30,00800011 +1095015000,14,31,ffcbff97 +1096895000,15,0,0019ffe1 +1096905000,15,1,ff22ffdd +1096915000,15,2,01ceff14 +1096925000,15,3,fe9f0168 +1096935000,15,4,023d0036 +1096945000,15,5,fb31fe88 +1096955000,15,6,04900010 +1096965000,15,7,feabffb6 +1096975000,15,8,012500c3 +1096985000,15,9,fb3c00a9 +1096995000,15,10,02f8fee8 +1097005000,15,11,02350044 +1097015000,15,12,fcad007e +1097025000,15,13,0195ffe2 +1097035000,15,14,ff82ffec +1097045000,15,15,005d005e +1098925000,15,16,001dffd9 +1098935000,15,17,ff03feed +1098945000,15,18,020101e0 +1098955000,15,19,fdabff44 +1098965000,15,20,0277ffff +1098975000,15,21,fb7601a9 +1098985000,15,22,0245fc38 +1098995000,15,23,01f9016b +1099005000,15,24,fef10077 +1099015000,15,25,fd7d0073 +1099025000,15,26,025ffffe +1099035000,15,27,fe91ff04 +1099045000,15,28,019302a5 +1099055000,15,29,ff36fe4b +1099065000,15,30,ffe3ffde +1099075000,15,31,ffef00b1 +1100955000,16,0,ffccffd9 +1100965000,16,1,01070063 +1100975000,16,2,ff32fe4f +1100985000,16,3,ff7c0349 +1100995000,16,4,001afd11 +1101005000,16,5,030001e0 +1101015000,16,6,fc0dfebf +1101025000,16,7,0238fd1e +1101035000,16,8,ff86050d +1101045000,16,9,fdadfed7 +1101055000,16,10,02e2fe19 +1101065000,16,11,fee6ff8f +1101075000,16,12,ff600389 +1101085000,16,13,0100fe2a +1101095000,16,14,ffa3ffe1 +1101105000,16,15,0022003e +1102985000,16,16,0003ffb4 +1102995000,16,17,feaaff8a +1103005000,16,18,032901ce +1103015000,16,19,fc5bfe22 +1103025000,16,20,013a007e +1103035000,16,21,ff120327 +1103045000,16,22,0060f98f +1103055000,16,23,020e02d2 +1103065000,16,24,fe930312 +1103075000,16,25,fcb8fb46 +1103085000,16,26,03ed01b2 +1103095000,16,27,fe57ffd4 +1103105000,16,28,02580204 +1103115000,16,29,fe40fee1 +1103125000,16,30,ff92ff99 +1103135000,16,31,006c00b0 +1105015000,17,0,001bfff0 +1105025000,17,1,0008ff7a +1105035000,17,2,0043ff39 +1105045000,17,3,fe5c0336 +1105055000,17,4,0327fd05 +1105065000,17,5,fd730201 +1105075000,17,6,0101fddb +1105085000,17,7,feeeff5a +1105095000,17,8,02c50250 +1105105000,17,9,fc5a01e2 +1105115000,17,10,017dfaef +1105125000,17,11,005201d0 +1105135000,17,12,ffb901bb +1105145000,17,13,0013ff8b +1105155000,17,14,0007ff1d +1105165000,17,15,fff40098 +1107045000,17,16,ffa5ffb1 +1107055000,17,17,007f003d +1107065000,17,18,ff5100f3 +1107075000,17,19,0164ff40 +1107085000,17,20,fd73008f +1107095000,17,21,00760136 +1107105000,17,22,0091fc2a +1107115000,17,23,0101025d +1107125000,17,24,ff13fdad +1107135000,17,25,fec905f9 +1107145000,17,26,0069fa0d +1107155000,17,27,01f8021c +1107165000,17,28,ff71011b +1107175000,17,29,ffc2ffb0 +1107185000,17,30,ff290016 +1107195000,17,31,00a3fff3 +1109075000,18,0,ffee0007 +1109085000,18,1,00ad0012 +1109095000,18,2,ff2aff21 +1109105000,18,3,002400b9 +1109115000,18,4,00a300df +1109125000,18,5,ffdaff18 +1109135000,18,6,fc78fddb +1109145000,18,7,06270337 +1109155000,18,8,fd6800ab +1109165000,18,9,fcfbfd42 +1109175000,18,10,051cffd5 +1109185000,18,11,fb8200cb +1109195000,18,12,025b01e7 +1109205000,18,13,ff7efdfc +1109215000,18,14,002600e7 +1109225000,18,15,fffbffad +1111105000,18,16,ff510020 +1111115000,18,17,fed4ff91 +1111125000,18,18,02fb011c +1111135000,18,19,fe3dfe99 +1111145000,18,20,ff56027e +1111155000,18,21,ff80febe +1111165000,18,22,000fff5f +1111175000,18,23,00f7ff66 +1111185000,18,24,00c70228 +1111195000,18,25,fc8afedb +1111205000,18,26,01ddfe18 +1111215000,18,27,ff9f0231 +1111225000,18,28,0156fffa +1111235000,18,29,ff96fffa +1111245000,18,30,fe5dffd5 +1111255000,18,31,01b10014 +1113135000,19,0,ffd50063 +1113145000,19,1,007aff81 +1113155000,19,2,ff4bff80 +1113165000,19,3,013e006e +1113175000,19,4,fd94020a +1113185000,19,5,0334fed7 +1113195000,19,6,fa73fcd5 +1113205000,19,7,08e40354 +1113215000,19,8,f9f30087 +1113225000,19,9,ff64ff13 +1113235000,19,10,02edfd88 +1113245000,19,11,fe2601b2 +1113255000,19,12,0014017c +1113265000,19,13,00b2fefd +1113275000,19,14,ffb50053 +1113285000,19,15,0024ff84 +1115165000,19,16,ffba0034 +1115175000,19,17,0010fff7 +1115185000,19,18,00910182 +1115195000,19,19,fe95fd97 +1115205000,19,20,00510179 +1115215000,19,21,02a900b1 +1115225000,19,22,fca2ff17 +1115235000,19,23,ffc2ffaa +1115245000,19,24,fed00066 +1115255000,19,25,0414fe81 +1115265000,19,26,fe4b027a +1115275000,19,27,000dfccf +1115285000,19,28,001503fd +1115295000,19,29,ffdffda3 +1115305000,19,30,ffa201c5 +1115315000,19,31,0060feec +1117195000,20,0,ffec003b +1117205000,20,1,005bff9f +1117215000,20,2,ff65ff6c +1117225000,20,3,00bd015d +1117235000,20,4,ff010043 +1117245000,20,5,0100fef8 +1117255000,20,6,fe1aff3e +1117265000,20,7,03a2ffe9 +1117275000,20,8,fecc0381 +1117285000,20,9,fc29fc01 +1117295000,20,10,038f0154 +1117305000,20,11,ff71fdd7 +1117315000,20,12,ff6f05e5 +1117325000,20,13,003cfb00 +1117335000,20,14,004201f6 +1117345000,20,15,fff8ff73 +1119225000,20,16,ffacffbd +1119235000,20,17,ff1b003e +1119245000,20,18,02fa0080 +1119255000,20,19,fc71ff9a +1119265000,20,20,01a5fffe +1119275000,20,21,fd0e0185 +1119285000,20,22,0317fb5e +1119295000,20,23,008f0467 +1119305000,20,24,fde000c9 +1119315000,20,25,ff53fc18 +1119325000,20,26,015c00f0 +1119335000,20,27,ffe5ffee +1119345000,20,28,01330324 +1119355000,20,29,fea8fd81 +1119365000,20,30,ffff00ca +1119375000,20,31,00970015 +1121255000,21,0,ffc9002e +1121265000,21,1,ffe6feb8 +1121275000,21,2,ffd70220 +1121285000,21,3,00d5ff2d +1121295000,21,4,feebffa0 +1121305000,21,5,01c4013c +1121315000,21,6,fdc2fdd0 +1121325000,21,7,012800f7 +1121335000,21,8,03410028 +1121345000,21,9,f9ba013a +1121355000,21,10,0295fc90 +1121365000,21,11,01450307 +1121375000,21,12,ffb3fefe +1121385000,21,13,ff1c007e +1121395000,21,14,ffdaff2c +1121405000,21,15,008e0089 +1123285000,21,16,004f0003 +1123295000,21,17,ff8a000b +1123305000,21,18,018700c8 +1123315000,21,19,ff36ff00 +1123325000,21,20,ff5c010a +1123335000,21,21,ff46ff56 +1123345000,21,22,00effe8b +1123355000,21,23,fe5d0180 +1123365000,21,24,045dfd0d +1123375000,21,25,fcce0535 +1123385000,21,26,0089fad0 +1123395000,21,27,fe2401c6 +1123405000,21,28,026c00c6 +1123415000,21,29,00b2ff96 +1123425000,21,30,fec500d5 +1123435000,21,31,0001ff06 +1125315000,22,0,ffc5ff8e +1125325000,22,1,0088005b +1125335000,22,2,ff08ff4c +1125345000,22,3,01fa009f +1125355000,22,4,fdc1ff0d +1125365000,22,5,021301a2 +1125375000,22,6,fc94ff4d +1125385000,22,7,0369fd1e +1125395000,22,8,ffd902cc +1125405000,22,9,fcfa010d +1125415000,22,10,ffe0ff5e +1125425000,22,11,03e8fdfb +1125435000,22,12,fd91030d +1125445000,22,13,00ebfe4e +1125455000,22,14,ff6c001d +1125465000,22,15,005d0068 +1127345000,22,16,ffd1ffc9 +1127355000,22,17,00610065 +1127365000,22,18,ffb4014e +1127375000,22,19,0102fce5 +1127385000,22,20,fddb02e3 +1127395000,22,21,01bdfe7a +1127405000,22,22,ff6901ff +1127415000,22,23,fc5fff63 +1127425000,22,24,03c7fba5 +1127435000,22,25,ffc503df +1127445000,22,26,012cfe44 +1127455000,22,27,fcb2020f +1127465000,22,28,03110073 +1127475000,22,29,fdf1feb2 +1127485000,22,30,010300d3 +1127495000,22,31,ffb9ffb1 +1129375000,23,0,0037ffd5 +1129385000,23,1,ffb900c4 +1129395000,23,2,0019fe0b +1129405000,23,3,0158021a +1129415000,23,4,fdcdffa9 +1129425000,23,5,0174fe79 +1129435000,23,6,fd7d008d +1129445000,23,7,04d401c4 +1129455000,23,8,fd3dfd35 +1129465000,23,9,fd250252 +1129475000,23,10,03e5ffa3 +1129485000,23,11,fefcfe36 +1129495000,23,12,012302dd +1129505000,23,13,fd8efd91 +1129515000,23,14,018900e5 +1129525000,23,15,ff90001c +1131405000,23,16,000bff9a +1131415000,23,17,ffc10058 +1131425000,23,18,015700d3 +1131435000,23,19,fe57fe9f +1131445000,23,20,01270152 +1131455000,23,21,fbd500e2 +1131465000,23,22,0505fe23 +1131475000,23,23,fcc2fcb5 +1131485000,23,24,031102e6 +1131495000,23,25,fcf90194 +1131505000,23,26,fffdfcd5 +1131515000,23,27,020902c1 +1131525000,23,28,ff09fe2e +1131535000,23,29,00d50112 +1131545000,23,30,fec3ffed +1131555000,23,31,0012ffb3 +1133435000,24,0,ffbbff9e +1133445000,24,1,009e0076 +1133455000,24,2,ff4fff62 +1133465000,24,3,00f90182 +1133475000,24,4,fe76feaf +1133485000,24,5,03d2ff7d +1133495000,24,6,fa800031 +1133505000,24,7,0359ffcb +1133515000,24,8,017f00a0 +1133525000,24,9,facc0040 +1133535000,24,10,02e1fea2 +1133545000,24,11,ffeb0052 +1133555000,24,12,007c02ff +1133565000,24,13,ff6cfc9d +1133575000,24,14,ffac0177 +1133585000,24,15,0093fff9 +1135465000,24,16,ffeaff9b +1135475000,24,17,feeeff97 +1135485000,24,18,0330011a +1135495000,24,19,fcfd0012 +1135505000,24,20,0097ff92 +1135515000,24,21,fedb0283 +1135525000,24,22,ff94fa5b +1135535000,24,23,00aa0175 +1135545000,24,24,02660361 +1135555000,24,25,fce8fd91 +1135565000,24,26,01b4ff34 +1135575000,24,27,fe81ff34 +1135585000,24,28,00b904be +1135595000,24,29,ff7bfd71 +1135605000,24,30,fff0febb +1135615000,24,31,00140169 +1137495000,25,0,ffe70000 +1137505000,25,1,ffb1ffcf +1137515000,25,2,01240069 +1137525000,25,3,ff2cff29 +1137535000,25,4,fff1015e +1137545000,25,5,fff2006f +1137555000,25,6,fee8fd04 +1137565000,25,7,04ae007e +1137575000,25,8,fc2d0298 +1137585000,25,9,fd23fe8d +1137595000,25,10,0448ffe3 +1137605000,25,11,ff42fecd +1137615000,25,12,ffa703aa +1137625000,25,13,fff2fc35 +1137635000,25,14,ffa00278 +1137645000,25,15,008cff24 +1139525000,25,16,ff8bffda +1139535000,25,17,fee9001e +1139545000,25,18,01d7ff4b +1139555000,25,19,ff8600bc +1139565000,25,20,fea4ffb6 +1139575000,25,21,ff720205 +1139585000,25,22,0248fb85 +1139595000,25,23,01f7021c +1139605000,25,24,fc970190 +1139615000,25,25,ff030084 +1139625000,25,26,015bff13 +1139635000,25,27,fedefee6 +1139645000,25,28,020a01e4 +1139655000,25,29,ff7afe71 +1139665000,25,30,ff260079 +1139675000,25,31,011d004a +1141555000,26,0,0030ffb4 +1141565000,26,1,ffcffff1 +1141575000,26,2,0127ffbd +1141585000,26,3,fd600172 +1141595000,26,4,0379fdfa +1141605000,26,5,fe59032a +1141615000,26,6,ffddfbaa +1141625000,26,7,0059003f +1141635000,26,8,02560302 +1141645000,26,9,f995ff2f +1141655000,26,10,04d1fdc7 +1141665000,26,11,ff7e0150 +1141675000,26,12,fe950068 +1141685000,26,13,00bb013e +1141695000,26,14,ffeffdea +1141705000,26,15,fff90147 +1143585000,26,16,ff8a007f +1143595000,26,17,ffadff5d +1143605000,26,18,01090196 +1143615000,26,19,fe6dfdbc +1143625000,26,20,00e90243 +1143635000,26,21,feb3ff2a +1143645000,26,22,00d5fdb4 +1143655000,26,23,feb2019b +1143665000,26,24,023afffd +1143675000,26,25,fc970025 +1143685000,26,26,00fdfc7e +1143695000,26,27,04510222 +1143705000,26,28,fc7f017d +1143715000,26,29,0011ff40 +1143725000,26,30,ff79004c +1143735000,26,31,00d8ff1b +1145615000,27,0,ffd0fffe +1145625000,27,1,00f7fff0 +1145635000,27,2,ffabff59 +1145645000,27,3,fd5a0287 +1145655000,27,4,03c5fd48 +1145665000,27,5,fdb8ffe8 +1145675000,27,6,024700eb +1145685000,27,7,fca50123 +1145695000,27,8,0118fdc4 +1145705000,27,9,ffd9027a +1145715000,27,10,0297fdeb +1145725000,27,11,fd54025d +1145735000,27,12,0103fd46 +1145745000,27,13,0014020a +1145755000,27,14,ffafff09 +1145765000,27,15,00290015 +1147645000,27,16,ff8bff4b +1147655000,27,17,ffdc00bf +1147665000,27,18,016dff46 +1147675000,27,19,fedc00dc +1147685000,27,20,ff850045 +1147695000,27,21,ff9efdfd +1147705000,27,22,00fe013c +1147715000,27,23,fe95ffd9 +1147725000,27,24,023f0239 +1147735000,27,25,fbf8fce5 +1147745000,27,26,00b7fefa +1147755000,27,27,02e40272 +1147765000,27,28,fd99ff67 +1147775000,27,29,01f60053 +1147785000,27,30,fe9effc4 +1147795000,27,31,00cb00c5 +1149675000,28,0,00230066 +1149685000,28,1,0064ff92 +1149695000,28,2,fed9fe56 +1149705000,28,3,02590478 +1149715000,28,4,fca7fd38 +1149725000,28,5,04bbff49 +1149735000,28,6,fb3cff3f +1149745000,28,7,022f019c +1149755000,28,8,0043009e +1149765000,28,9,fe10feb0 +1149775000,28,10,022300ee +1149785000,28,11,fe23fdc0 +1149795000,28,12,00b70398 +1149805000,28,13,007dfde5 +1149815000,28,14,ffcc0111 +1149825000,28,15,ffe1ff54 +1151705000,28,16,ff5aff9e +1151715000,28,17,0086ffca +1151725000,28,18,00fe00bf +1151735000,28,19,fdcaffb4 +1151745000,28,20,0269ff52 +1151755000,28,21,fbfc0544 +1151765000,28,22,0301f80a +1151775000,28,23,fd9902db +1151785000,28,24,03b600ae +1151795000,28,25,fc4affe6 +1151805000,28,26,fedefe47 +1151815000,28,27,0326014a +1151825000,28,28,00270016 +1151835000,28,29,fea0fff8 +1151845000,28,30,008bff5c +1151855000,28,31,000300fb +1153735000,29,0,001a0025 +1153745000,29,1,002bff7f +1153755000,29,2,fefb0041 +1153765000,29,3,01c500d4 +1153775000,29,4,fe47ffad +1153785000,29,5,01ec00f1 +1153795000,29,6,fe0efbe2 +1153805000,29,7,0208020e +1153815000,29,8,ff6a03a7 +1153825000,29,9,fd17fb7d +1153835000,29,10,03230095 +1153845000,29,11,fe99009a +1153855000,29,12,010d0213 +1153865000,29,13,fea2fd7b +1153875000,29,14,01040154 +1153885000,29,15,ffc2ff84 +1155765000,29,16,ffc5ffa7 +1155775000,29,17,ff4e008c +1155785000,29,18,024dff99 +1155795000,29,19,fd1a005d +1155805000,29,20,01eefe22 +1155815000,29,21,fe2a0354 +1155825000,29,22,0184fc82 +1155835000,29,23,01140159 +1155845000,29,24,ffe10175 +1155855000,29,25,fdcc00e6 +1155865000,29,26,ff3dfc3f +1155875000,29,27,004e02ad +1155885000,29,28,02f0ff36 +1155895000,29,29,fcc0006a +1155905000,29,30,01ceff1a +1155915000,29,31,ffc000d5 +1157795000,30,0,ffcb0013 +1157805000,30,1,0031ff89 +1157815000,30,2,ffca002a +1157825000,30,3,000c00e8 +1157835000,30,4,00b9fe9f +1157845000,30,5,0023015f +1157855000,30,6,fe08fdb8 +1157865000,30,7,0024fedb +1157875000,30,8,031504c7 +1157885000,30,9,fc97ff7d +1157895000,30,10,febcfb0c +1157905000,30,11,049a04fc +1157915000,30,12,fd6bfd6b +1157925000,30,13,0089027f +1157935000,30,14,ff86fdce +1157945000,30,15,00aa00bd +1159825000,30,16,ffb10043 +1159835000,30,17,ffeb002b +1159845000,30,18,01cb0041 +1159855000,30,19,fcecff28 +1159865000,30,20,040b007e +1159875000,30,21,fb46001d +1159885000,30,22,032d001b +1159895000,30,23,fdb10059 +1159905000,30,24,0293fe15 +1159915000,30,25,fd4700ff +1159925000,30,26,fee7fe53 +1159935000,30,27,054e0258 +1159945000,30,28,fc69fffe +1159955000,30,29,ffb4fff5 +1159965000,30,30,0099005d +1159975000,30,31,0019ff3b +1161855000,31,0,001cffcf +1161865000,31,1,fedd0048 +1161875000,31,2,017bfecf +1161885000,31,3,0071023d +1161895000,31,4,fef5fda9 +1161905000,31,5,fdf4030f +1161915000,31,6,0353fc4a +1161925000,31,7,fe890164 +1161935000,31,8,02300229 +1161945000,31,9,fb7ffe7c +1161955000,31,10,0363fddf +1161965000,31,11,ff9503a5 +1161975000,31,12,ff77fd5b +1161985000,31,13,00440149 +1161995000,31,14,001fff9c +1162005000,31,15,ffd5000e +1163885000,31,16,001dffa4 +1163895000,31,17,00a30115 +1163905000,31,18,febaff77 +1163915000,31,19,01baff65 +1163925000,31,20,ffcdffd2 +1163935000,31,21,fe3103d5 +1163945000,31,22,01e7fba1 +1163955000,31,23,fe670108 +1163965000,31,24,00d7fdca +1163975000,31,25,022f04c1 +1163985000,31,26,fc1cfd2d +1163995000,31,27,04260013 +1164005000,31,28,fbe70068 +1164015000,31,29,029100e1 +1164025000,31,30,0033ff1b +1164035000,31,31,ff6d007c +1165915000,32,0,001b0033 +1165925000,32,1,0046ff75 +1165935000,32,2,007eff31 +1165945000,32,3,ffd20210 +1165955000,32,4,fd56fea6 +1165965000,32,5,0471003f +1165975000,32,6,fbb5fe33 +1165985000,32,7,02b202ac +1165995000,32,8,fd7300e3 +1166005000,32,9,0402fe25 +1166015000,32,10,fbd0ff71 +1166025000,32,11,03e80062 +1166035000,32,12,fc5801b0 +1166045000,32,13,025bfe7b +1166055000,32,14,0011005f +1166065000,32,15,ff30ffee +1167945000,32,16,0034fff3 +1167955000,32,17,ff76ffe3 +1167965000,32,18,00f500c9 +1167975000,32,19,fecbff52 +1167985000,32,20,01d70081 +1167995000,32,21,fae70113 +1168005000,32,22,03d4fc53 +1168015000,32,23,0288024d +1168025000,32,24,f97e002f +1168035000,32,25,06cc0089 +1168045000,32,26,fbcbfe99 +1168055000,32,27,00dbfe86 +1168065000,32,28,020304cd +1168075000,32,29,fefffdb9 +1168085000,32,30,00100063 +1168095000,32,31,ff9affcb +1169975000,33,0,fffbffe8 +1169985000,33,1,008cff87 +1169995000,33,2,ff6500b5 +1170005000,33,3,0096ffed +1170015000,33,4,fd82ffae +1170025000,33,5,05cafeba +1170035000,33,6,f8aa023d +1170045000,33,7,0588feab +1170055000,33,8,fdd90004 +1170065000,33,9,ff84023b +1170075000,33,10,00cdfb8d +1170085000,33,11,007801e7 +1170095000,33,12,fe2602a2 +1170105000,33,13,00e2fe00 +1170115000,33,14,00d0ffdd +1170125000,33,15,ff86006d +1172005000,33,16,ffcd001b +1172015000,33,17,ff21ffcf +1172025000,33,18,02810023 +1172035000,33,19,fd0a004e +1172045000,33,20,018c00a7 +1172055000,33,21,fd76ff7f +1172065000,33,22,00fffcd0 +1172075000,33,23,02590484 +1172085000,33,24,fd99fdf1 +1172095000,33,25,ffd70039 +1172105000,33,26,00bd00fd +1172115000,33,27,ff92fe64 +1172125000,33,28,00c601b5 +1172135000,33,29,fe6effa9 +1172145000,33,30,016bfff8 +1172155000,33,31,ffcfffea +1174035000,34,0,0037ffeb +1174045000,34,1,00560015 +1174055000,34,2,ff68fec1 +1174065000,34,3,009b019a +1174075000,34,4,fef70042 +1174085000,34,5,0161ffd9 +1174095000,34,6,fdebfce7 +1174105000,34,7,02f0036c +1174115000,34,8,fec7ff67 +1174125000,34,9,fd7c0063 +1174135000,34,10,0338fe6f +1174145000,34,11,ff51ff38 +1174155000,34,12,fe9f0420 +1174165000,34,13,016dfc53 +1174175000,34,14,ff910195 +1174185000,34,15,ffd4ffbe +1176065000,34,16,ff8eff89 +1176075000,34,17,ff8fffef +1176085000,34,18,00b2012d +1176095000,34,19,fee1fecb +1176105000,34,20,01c4ffbb +1176115000,34,21,fcb00004 +1176125000,34,22,0468fe6b +1176135000,34,23,fceb0442 +1176145000,34,24,012cff0b +1176155000,34,25,fdc3ffc9 +1176165000,34,26,00ccfe11 +1176175000,34,27,00d1002d +1176185000,34,28,ffb2042d +1176195000,34,29,ff9efc94 +1176205000,34,30,ffb20043 +1176215000,34,31,00db00de +1178095000,35,0,fff3004f +1178105000,35,1,0040ffa9 +1178115000,35,2,ff94ff7a +1178125000,35,3,00c70072 +1178135000,35,4,ff0c012d +1178145000,35,5,0011fe45 +1178155000,35,6,fe71ff07 +1178165000,35,7,03a1017b +1178175000,35,8,fd8f039d +1178185000,35,9,fe9cfac7 +1178195000,35,10,03c001a4 +1178205000,35,11,fd87ff58 +1178215000,35,12,ff92033b +1178225000,35,13,026ffcb7 +1178235000,35,14,fe430167 +1178245000,35,15,008dff6f +1180125000,35,16,ff8eff88 +1180135000,35,17,00980001 +1180145000,35,18,ffa400ff +1180155000,35,19,ff6dfe1d +1180165000,35,20,01480138 +1180175000,35,21,febc02b0 +1180185000,35,22,ffd9fa8a +1180195000,35,23,feba03c0 +1180205000,35,24,00cefcd4 +1180215000,35,25,018c047f +1180225000,35,26,fd76fd13 +1180235000,35,27,0199ffdb +1180245000,35,28,feec0160 +1180255000,35,29,0104ff08 +1180265000,35,30,ffd50098 +1180275000,35,31,00040068 +1182155000,36,0,ffe20026 +1182165000,36,1,0103ffc9 +1182175000,36,2,fe7cff78 +1182185000,36,3,00e60195 +1182195000,36,4,ff31fe5f +1182205000,36,5,01fe0214 +1182215000,36,6,fcd3fd60 +1182225000,36,7,02ad00a2 +1182235000,36,8,010800f6 +1182245000,36,9,fbd5007d +1182255000,36,10,0258fe4e +1182265000,36,11,ffb8fe27 +1182275000,36,12,ffc105ed +1182285000,36,13,0086fc4e +1182295000,36,14,fff5008a +1182305000,36,15,ffe1ffe2 +1184185000,36,16,ff89ffc7 +1184195000,36,17,ff8300c3 +1184205000,36,18,01f9ff5b +1184215000,36,19,fd970093 +1184225000,36,20,0036ff02 +1184235000,36,21,001301c6 +1184245000,36,22,0084fda9 +1184255000,36,23,fefb0045 +1184265000,36,24,028d0275 +1184275000,36,25,fbfbfec5 +1184285000,36,26,033dfe99 +1184295000,36,27,fe0bffbb +1184305000,36,28,024c0352 +1184315000,36,29,fe5fff6e +1184325000,36,30,ff9efefb +1184335000,36,31,01130059 +1186215000,37,0,0004005b +1186225000,37,1,00b0ff48 +1186235000,37,2,ff2a017d +1186245000,37,3,00c8fe9a +1186255000,37,4,fe440150 +1186265000,37,5,03dfff45 +1186275000,37,6,fb32fea8 +1186285000,37,7,04300190 +1186295000,37,8,feac0031 +1186305000,37,9,feb0ff3c +1186315000,37,10,0086fe91 +1186325000,37,11,0014011e +1186335000,37,12,0000012c +1186345000,37,13,ffa9ffe3 +1186355000,37,14,008aff4a +1186365000,37,15,ffac0004 +1188245000,37,16,00210001 +1188255000,37,17,feb2ff49 +1188265000,37,18,034800a7 +1188275000,37,19,fcdd003b +1188285000,37,20,00b6ff80 +1188295000,37,21,ff030212 +1188305000,37,22,0083fbc5 +1188315000,37,23,027a017c +1188325000,37,24,fcf7ff8b +1188335000,37,25,ff8602db +1188345000,37,26,016cfd91 +1188355000,37,27,fed1fe67 +1188365000,37,28,018e0394 +1188375000,37,29,ff15fea6 +1188385000,37,30,0035ff73 +1188395000,37,31,00000056 +1190275000,38,0,ffcb0010 +1190285000,38,1,00fe0019 +1190295000,38,2,fee6ff68 +1190305000,38,3,ff3500d9 +1190315000,38,4,02e6ff0f +1190325000,38,5,fd3f022b +1190335000,38,6,ff23fdd2 +1190345000,38,7,0370fdc4 +1190355000,38,8,fe9b068c +1190365000,38,9,fe10fb4d +1190375000,38,10,017201f0 +1190385000,38,11,00bdfd67 +1190395000,38,12,ff1c03b5 +1190405000,38,13,012bfe37 +1190415000,38,14,ff45ffb6 +1190425000,38,15,fffefff4 +1192305000,38,16,ffb1ff77 +1192315000,38,17,011800f6 +1192325000,38,18,fe94ff8e +1192335000,38,19,00bcffbb +1192345000,38,20,028e00c3 +1192355000,38,21,fadb003f +1192365000,38,22,0377fe42 +1192375000,38,23,fbd8023d +1192385000,38,24,05c1fda5 +1192395000,38,25,fddc0166 +1192405000,38,26,ff48fe26 +1192415000,38,27,00f2038b +1192425000,38,28,ff10fe39 +1192435000,38,29,00e5ff5d +1192445000,38,30,007d013a +1192455000,38,31,ff96ffdd +1194335000,39,0,00710032 +1194345000,39,1,0033ffc6 +1194355000,39,2,ff71ffaa +1194365000,39,3,00af0091 +1194375000,39,4,fdf900c1 +1194385000,39,5,04acfe00 +1194395000,39,6,fa5000b7 +1194405000,39,7,03f0009a +1194415000,39,8,fefd00ec +1194425000,39,9,fe81fe9a +1194435000,39,10,01e7fe2c +1194445000,39,11,0081032f +1194455000,39,12,fd79ff9d +1194465000,39,13,0110fe9c +1194475000,39,14,0148008f +1194485000,39,15,fea00012 +1196365000,39,16,ffeaff9e +1196375000,39,17,ff9bff9c +1196385000,39,18,01c80051 +1196395000,39,19,ff1e0197 +1196405000,39,20,ff42fdfb +1196415000,39,21,fe6d01a6 +1196425000,39,22,024ffaec +1196435000,39,23,fed304eb +1196445000,39,24,0092fde4 +1196455000,39,25,00630066 +1196465000,39,26,ff06feb5 +1196475000,39,27,007e02cf +1196485000,39,28,ffa6fe13 +1196495000,39,29,004500f4 +1196505000,39,30,ffafff0e +1196515000,39,31,001100b3 +1198395000,40,0,fff8003e +1198405000,40,1,00cbff5b +1198415000,40,2,fe480011 +1198425000,40,3,018a0155 +1198435000,40,4,fe5cff8e +1198445000,40,5,04faffd8 +1198455000,40,6,f87cff10 +1198465000,40,7,0545ffc7 +1198475000,40,8,0028002e +1198485000,40,9,fc8502fb +1198495000,40,10,0078fce3 +1198505000,40,11,038afed5 +1198515000,40,12,fc900416 +1198525000,40,13,01c6fdbe +1198535000,40,14,ffa8ffc4 +1198545000,40,15,ffa7004b +1200425000,40,16,ffa7000e +1200435000,40,17,ff74ff54 +1200445000,40,18,02e200ad +1200455000,40,19,fd91ffb7 +1200465000,40,20,002c016b +1200475000,40,21,fe6c0080 +1200485000,40,22,029afa2a +1200495000,40,23,00000445 +1200505000,40,24,ffd5ff6a +1200515000,40,25,fee40062 +1200525000,40,26,00f2fecb +1200535000,40,27,0029fe69 +1200545000,40,28,ffac0535 +1200555000,40,29,0074fc82 +1200565000,40,30,ff7e007e +1200575000,40,31,00ae005b +1202455000,41,0,003e0047 +1202465000,41,1,ff22ffaa +1202475000,41,2,0254feb9 +1202485000,41,3,fcde0350 +1202495000,41,4,0312fe78 +1202505000,41,5,fdf8ff2c +1202515000,41,6,01bcfffd +1202525000,41,7,fe56ff40 +1202535000,41,8,01a401bb +1202545000,41,9,fe1aff88 +1202555000,41,10,00dcfee7 +1202565000,41,11,ff2400e6 +1202575000,41,12,009cffca +1202585000,41,13,0118009e +1202595000,41,14,fe54ffaf +1202605000,41,15,008cfffe +1204485000,41,16,ffb8ffde +1204495000,41,17,ff50ff64 +1204505000,41,18,01ab01cb +1204515000,41,19,ffd8fe34 +1204525000,41,20,ff130200 +1204535000,41,21,fce9fe29 +1204545000,41,22,04a9fe2a +1204555000,41,23,fd8302dd +1204565000,41,24,02860042 +1204575000,41,25,fbdefe6c +1204585000,41,26,0281fe5d +1204595000,41,27,ffba0182 +1204605000,41,28,ff17017c +1204615000,41,29,0235fde3 +1204625000,41,30,fe23013a +1204635000,41,31,00bfffb9 +1206515000,42,0,fffaffbb +1206525000,42,1,ffba0052 +1206535000,42,2,0056ff6f +1206545000,42,3,ff78ff9e +1206555000,42,4,00fc01b4 +1206565000,42,5,fddfffe5 +1206575000,42,6,0162fb89 +1206585000,42,7,00c90701 +1206595000,42,8,0148fbfd +1206605000,42,9,fab0ff70 +1206615000,42,10,04520099 +1206625000,42,11,febc0186 +1206635000,42,12,0046febc +1206645000,42,13,003700a9 +1206655000,42,14,ff12ff5f +1206665000,42,15,00e30073 +1208545000,42,16,ff8a0024 +1208555000,42,17,0012ff83 +1208565000,42,18,00bd0124 +1208575000,42,19,ff93fdc4 +1208585000,42,20,ff3d041b +1208595000,42,21,ff94fe92 +1208605000,42,22,0011fc2d +1208615000,42,23,01af03c3 +1208625000,42,24,ff50fe14 +1208635000,42,25,fda40213 +1208645000,42,26,0091fd22 +1208655000,42,27,0299027c +1208665000,42,28,fd5d0019 +1208675000,42,29,01d2ff18 +1208685000,42,30,fe9500d1 +1208695000,42,31,00c1ffbd +1210575000,43,0,005affef +1210585000,43,1,ff62007b +1210595000,43,2,002ffea3 +1210605000,43,3,004701bc +1210615000,43,4,01240008 +1210625000,43,5,fd1200ce +1210635000,43,6,041bf9dd +1210645000,43,7,fde805b1 +1210655000,43,8,ff14006f +1210665000,43,9,0096fd29 +1210675000,43,10,ffdbff07 +1210685000,43,11,0065036c +1210695000,43,12,001efde6 +1210705000,43,13,fe7e0156 +1210715000,43,14,0143ff35 +1210725000,43,15,ffcc0057 +1212605000,43,16,ff87000a +1212615000,43,17,ffacff81 +1212625000,43,18,02770161 +1212635000,43,19,fafefe50 +1212645000,43,20,03da0087 +1212655000,43,21,ff17000a +1212665000,43,22,ffd8ff83 +1212675000,43,23,00510172 +1212685000,43,24,feebff10 +1212695000,43,25,002aff9f +1212705000,43,26,fe01ffa9 +1212715000,43,27,02bcffc8 +1212725000,43,28,ffb8005f +1212735000,43,29,fef7ff82 +1212745000,43,30,00f400e3 +1212755000,43,31,ffe9ff9a +1214635000,44,0,009b0003 +1214645000,44,1,ffe40035 +1214655000,44,2,ff89fda3 +1214665000,44,3,011c03d0 +1214675000,44,4,fe3efe42 +1214685000,44,5,0433fffc +1214695000,44,6,fbe9fec4 +1214705000,44,7,ff1dffb4 +1214715000,44,8,03d502a3 +1214725000,44,9,fca4fd7f +1214735000,44,10,02a701fb +1214745000,44,11,fd26fe72 +1214755000,44,12,005e0120 +1214765000,44,13,0211ffc4 +1214775000,44,14,ff7b005e +1214785000,44,15,ff35ffce +1216665000,44,16,ffad0039 +1216675000,44,17,ff78ff9b +1216685000,44,18,02390107 +1216695000,44,19,fd2efdb9 +1216705000,44,20,021503aa +1216715000,44,21,feeeff84 +1216725000,44,22,0133fd24 +1216735000,44,23,fdb701e2 +1216745000,44,24,01afff8f +1216755000,44,25,fd8c00f5 +1216765000,44,26,012bfcc1 +1216775000,44,27,01c4039f +1216785000,44,28,fd5fffc2 +1216795000,44,29,012efe38 +1216805000,44,30,ffe10160 +1216815000,44,31,000fffba +1218695000,45,0,0022ffdc +1218705000,45,1,00610028 +1218715000,45,2,fea6fe85 +1218725000,45,3,013f0327 +1218735000,45,4,ffc0fe4f +1218745000,45,5,00e7008a +1218755000,45,6,fd1dfebc +1218765000,45,7,02d1ff02 +1218775000,45,8,019e0462 +1218785000,45,9,fa6bfd14 +1218795000,45,10,03f8ffc9 +1218805000,45,11,ffb5fea9 +1218815000,45,12,ff1c041f +1218825000,45,13,0059fd26 +1218835000,45,14,00690062 +1218845000,45,15,ff6f002a +1220725000,45,16,004c0007 +1220735000,45,17,ffcd0026 +1220745000,45,18,002e009f +1220755000,45,19,0108feba +1220765000,45,20,004e00e2 +1220775000,45,21,fd4f015f +1220785000,45,22,00dbfe2c +1220795000,45,23,ff0b0239 +1220805000,45,24,0364f9eb +1220815000,45,25,fc4705d0 +1220825000,45,26,032efc73 +1220835000,45,27,ff160322 +1220845000,45,28,ff26fef8 +1220855000,45,29,005dff27 +1220865000,45,30,014d0196 +1220875000,45,31,feffff4f +1222755000,46,0,ffe00008 +1222765000,46,1,003fff88 +1222775000,46,2,00cfffee +1222785000,46,3,fdb90212 +1222795000,46,4,0248fc91 +1222805000,46,5,ff690333 +1222815000,46,6,ff2ffd57 +1222825000,46,7,ff6900d7 +1222835000,46,8,04680102 +1222845000,46,9,fa39fff0 +1222855000,46,10,024dfd6c +1222865000,46,11,0043028c +1222875000,46,12,ff94fec5 +1222885000,46,13,ffef0269 +1222895000,46,14,0089fddf +1222905000,46,15,ffd30087 +1224785000,46,16,ffe6006a +1224795000,46,17,ff97ff82 +1224805000,46,18,00ed0171 +1224815000,46,19,ff30fe35 +1224825000,46,20,020e015d +1224835000,46,21,fc14ff2c +1224845000,46,22,02690001 +1224855000,46,23,ff450088 +1224865000,46,24,04c8ffbc +1224875000,46,25,f8a702ac +1224885000,46,26,019dfc35 +1224895000,46,27,008201ad +1224905000,46,28,ffec0021 +1224915000,46,29,00f2000a +1224925000,46,30,ff35005d +1224935000,46,31,0065ff7a +1226815000,47,0,ff97ffa5 +1226825000,47,1,0073003c +1226835000,47,2,ff870012 +1226845000,47,3,004e0073 +1226855000,47,4,ffaeff1e +1226865000,47,5,01680226 +1226875000,47,6,fc91fd0e +1226885000,47,7,04d3017f +1226895000,47,8,fecfffd1 +1226905000,47,9,fd8b0114 +1226915000,47,10,ff03fdca +1226925000,47,11,0678009b +1226935000,47,12,fa9800d4 +1226945000,47,13,0122ff3a +1226955000,47,14,ffb9ffe6 +1226965000,47,15,005f008b +1228845000,47,16,0046ffd1 +1228855000,47,17,ffdbffb5 +1228865000,47,18,ff2dffe0 +1228875000,47,19,020e014b +1228885000,47,20,fdbbff10 +1228895000,47,21,0007ff18 +1228905000,47,22,011bfee4 +1228915000,47,23,00b20310 +1228925000,47,24,00e0fc0f +1228935000,47,25,fa63046b +1228945000,47,26,0567fb8e +1228955000,47,27,fe140369 +1228965000,47,28,00ebfe98 +1228975000,47,29,004b0110 +1228985000,47,30,ff45fe8e +1228995000,47,31,006c009c +1230875000,48,0,ffeafff7 +1230885000,48,1,00220040 +1230895000,48,2,ff38fd98 +1230905000,48,3,01b60397 +1230915000,48,4,fc21ffc1 +1230925000,48,5,060dfe1b +1230935000,48,6,fa20ff80 +1230945000,48,7,04d6017d +1230955000,48,8,fcb60049 +1230965000,48,9,0132ffe0 +1230975000,48,10,ff42fe2e +1230985000,48,11,00e0005d +1230995000,48,12,ff73034b +1231005000,48,13,003bfd29 +1231015000,48,14,004a0056 +1231025000,48,15,ffe00043 +1232905000,48,16,00230029 +1232915000,48,17,ff4aff58 +1232925000,48,18,02900208 +1232935000,48,19,fc68fe98 +1232945000,48,20,01acff39 +1232955000,48,21,ff700494 +1232965000,48,22,001bf940 +1232975000,48,23,029202bc +1232985000,48,24,fcd5014d +1232995000,48,25,ff26fe54 +1233005000,48,26,03c200d2 +1233015000,48,27,fd22fec8 +1233025000,48,28,037801dd +1233035000,48,29,fd840084 +1233045000,48,30,008fff1a +1233055000,48,31,00080020 +1234935000,49,0,005dffe2 +1234945000,49,1,0005fffd +1234955000,49,2,ff31ff8f +1234965000,49,3,00ac0184 +1234975000,49,4,fea8ff39 +1234985000,49,5,0355008d +1234995000,49,6,fd47fdba +1235005000,49,7,00980213 +1235015000,49,8,018fffda +1235025000,49,9,fb9fffb3 +1235035000,49,10,03f5ff93 +1235045000,49,11,0058ff10 +1235055000,49,12,fd2c03f3 +1235065000,49,13,013ffc07 +1235075000,49,14,00eb019c +1235085000,49,15,ff14ffb5 +1236965000,49,16,ffb0ffdb +1236975000,49,17,ffc1007e +1236985000,49,18,0138ffae +1236995000,49,19,002fffe9 +1237005000,49,20,fcbb024e +1237015000,49,21,0238fd25 +1237025000,49,22,fee1ff7a +1237035000,49,23,02820142 +1237045000,49,24,fea2004b +1237055000,49,25,fe2b01a0 +1237065000,49,26,01cafc20 +1237075000,49,27,00b5026b +1237085000,49,28,fe0f0114 +1237095000,49,29,0200fee9 +1237105000,49,30,fec100b0 +1237115000,49,31,00e6ffbe +1238995000,50,0,0025ffdf +1239005000,50,1,ffc50072 +1239015000,50,2,0037ff0c +1239025000,50,3,fff7ffa1 +1239035000,50,4,015402b2 +1239045000,50,5,fc98fe22 +1239055000,50,6,01f6fd37 +1239065000,50,7,00920530 +1239075000,50,8,011bfcd1 +1239085000,50,9,fc6102b4 +1239095000,50,10,03bffc08 +1239105000,50,11,fc9900f3 +1239115000,50,12,02940256 +1239125000,50,13,fea6fee0 +1239135000,50,14,0024ffb5 +1239145000,50,15,0042005c +1241025000,50,16,00030023 +1241035000,50,17,ff6bff52 +1241045000,50,18,0101ff97 +1241055000,50,19,ffd40214 +1241065000,50,20,fe42fee3 +1241075000,50,21,001d013e +1241085000,50,22,ff77fc10 +1241095000,50,23,04b70261 +1241105000,50,24,fa450053 +1241115000,50,25,00c7ff18 +1241125000,50,26,fff30187 +1241135000,50,27,024efd04 +1241145000,50,28,fe4a02e3 +1241155000,50,29,00ed0008 +1241165000,50,30,ff31ff1e +1241175000,50,31,001b004f +1243055000,51,0,fffdffda +1243065000,51,1,008cffa6 +1243075000,51,2,fed0ff37 +1243085000,51,3,01fe0225 +1243095000,51,4,fd54ffdb +1243105000,51,5,0238fdf6 +1243115000,51,6,fcc501a9 +1243125000,51,7,0539febf +1243135000,51,8,fe650312 +1243145000,51,9,fd48fdc2 +1243155000,51,10,00e8fd85 +1243165000,51,11,030201ed +1243175000,51,12,fc0e0245 +1243185000,51,13,01ccfe0e +1243195000,51,14,0067ff97 +1243205000,51,15,ff4700bb +1245085000,51,16,ffb3ffc8 +1245095000,51,17,00bc0086 +1245105000,51,18,00b900cc +1245115000,51,19,fd9cfe44 +1245125000,51,20,01790040 +1245135000,51,21,005f0115 +1245145000,51,22,006a00be +1245155000,51,23,fd94fd5c +1245165000,51,24,ff6502c4 +1245175000,51,25,0386fc28 +1245185000,51,26,fe0b03ba +1245195000,51,27,0230fc74 +1245205000,51,28,fdff0504 +1245215000,51,29,000bfd35 +1245225000,51,30,00e20114 +1245235000,51,31,ffb4ff9c +1247115000,52,0,00320039 +1247125000,52,1,0004ffb1 +1247135000,52,2,ff78feef +1247145000,52,3,013b0348 +1247155000,52,4,ff1afcec +1247165000,52,5,fffd00c2 +1247175000,52,6,ff630102 +1247185000,52,7,01aafc7e +1247195000,52,8,00f00631 +1247205000,52,9,fc6afa29 +1247215000,52,10,004a0139 +1247225000,52,11,028f011e +1247235000,52,12,fe1c0212 +1247245000,52,13,00d5fc90 +1247255000,52,14,ffeb01fe +1247265000,52,15,ffe4ff60 +1249145000,52,16,ffa80009 +1249155000,52,17,ff08fff5 +1249165000,52,18,03430062 +1249175000,52,19,fc340019 +1249185000,52,20,003fff76 +1249195000,52,21,013a01c4 +1249205000,52,22,fea0fbd5 +1249215000,52,23,01230121 +1249225000,52,24,00d00433 +1249235000,52,25,fd5afbcd +1249245000,52,26,014500dc +1249255000,52,27,ff94fed9 +1249265000,52,28,011502fa +1249275000,52,29,ff74ff5e +1249285000,52,30,feecff71 +1249295000,52,31,01150029 +1251175000,53,0,ffda006b +1251185000,53,1,0053fe6b +1251195000,53,2,ff98021b +1251205000,53,3,007dff1c +1251215000,53,4,ff67ff0c +1251225000,53,5,0128027c +1251235000,53,6,fe01fc44 +1251245000,53,7,0172017f +1251255000,53,8,015a0165 +1251265000,53,9,fb8d007b +1251275000,53,10,032afc67 +1251285000,53,11,ffc30312 +1251295000,53,12,fff9ff24 +1251305000,53,13,ffc80142 +1251315000,53,14,ffd1fe1a +1251325000,53,15,005600cf +1253205000,53,16,003bffdf +1253215000,53,17,ffa60087 +1253225000,53,18,01cdff59 +1253235000,53,19,fe920009 +1253245000,53,20,ff920039 +1253255000,53,21,ffc80396 +1253265000,53,22,feedf9a7 +1253275000,53,23,03780261 +1253285000,53,24,faf1fed5 +1253295000,53,25,0660024d +1253305000,53,26,fbddfd65 +1253315000,53,27,ffe402bd +1253325000,53,28,00a2fdeb +1253335000,53,29,00860146 +1253345000,53,30,0039000b +1253355000,53,31,fefeff91 +1255235000,54,0,ffbfffac +1255245000,54,1,0045ffb2 +1255255000,54,2,ffff0064 +1255265000,54,3,ffe0ff3d +1255275000,54,4,011e0086 +1255285000,54,5,fd1f020d +1255295000,54,6,020dfbc1 +1255305000,54,7,01d901ac +1255315000,54,8,fd81008e +1255325000,54,9,fde70086 +1255335000,54,10,028bfe5a +1255345000,54,11,00e00241 +1255355000,54,12,fe0efe1c +1255365000,54,13,0151018b +1255375000,54,14,ff65fee5 +1255385000,54,15,006300c6 +1257265000,54,16,ffe40064 +1257275000,54,17,ff02ffef +1257285000,54,18,01b4fff0 +1257295000,54,19,fe27006f +1257305000,54,20,01befe8a +1257315000,54,21,fe4301de +1257325000,54,22,0082ff7c +1257335000,54,23,02feff88 +1257345000,54,24,fbfa006c +1257355000,54,25,01100251 +1257365000,54,26,ff32feee +1257375000,54,27,0025fc75 +1257385000,54,28,01940316 +1257395000,54,29,ff4f0066 +1257405000,54,30,ffd8ffa6 +1257415000,54,31,0062ff80 +1259295000,55,0,0048003b +1259305000,55,1,0006ffdd +1259315000,55,2,ff9afece +1259325000,55,3,01ae027d +1259335000,55,4,fc7cfef1 +1259345000,55,5,041ffea8 +1259355000,55,6,fcd1ff47 +1259365000,55,7,027a0274 +1259375000,55,8,fd70fe9d +1259385000,55,9,fec601d9 +1259395000,55,10,041cfd64 +1259405000,55,11,fdc20111 +1259415000,55,12,ff8402c7 +1259425000,55,13,017dfc7a +1259435000,55,14,ff9101d7 +1259445000,55,15,ffdeff46 +1261325000,55,16,001affc7 +1261335000,55,17,ffd400be +1261345000,55,18,00eeff2b +1261355000,55,19,fe8e00b3 +1261365000,55,20,ffa0fe5d +1261375000,55,21,01cb026f +1261385000,55,22,ff45ff77 +1261395000,55,23,fbd8feb3 +1261405000,55,24,03fafec9 +1261415000,55,25,fede032c +1261425000,55,26,00eefc43 +1261435000,55,27,fe3803cf +1261445000,55,28,0018fd1f +1261455000,55,29,012b039f +1261465000,55,30,ff1bfd9f +1261475000,55,31,ffe200a3 +1263355000,56,0,ffeefff9 +1263365000,56,1,00110047 +1263375000,56,2,ffd1fddd +1263385000,56,3,00ad03bd +1263395000,56,4,fe72fe00 +1263405000,56,5,033f014c +1263415000,56,6,fb42fc6f +1263425000,56,7,04c00134 +1263435000,56,8,fe260329 +1263445000,56,9,fd81fe57 +1263455000,56,10,02e9fe8d +1263465000,56,11,ff2dfed7 +1263475000,56,12,ffde058e +1263485000,56,13,0017fb2e +1263495000,56,14,000801f7 +1263505000,56,15,0016ffa0 +1265385000,56,16,ffe40008 +1265395000,56,17,feeeff75 +1265405000,56,18,03410185 +1265415000,56,19,fc92fff7 +1265425000,56,20,ffb5fe9a +1265435000,56,21,00050495 +1265445000,56,22,0090f960 +1265455000,56,23,019d008d +1265465000,56,24,fe8c033a +1265475000,56,25,fe7aff33 +1265485000,56,26,0185febd +1265495000,56,27,00a2ff01 +1265505000,56,28,ffeb03e4 +1265515000,56,29,ffbffd7f +1265525000,56,30,ff520096 +1265535000,56,31,009bfff7 +1267415000,57,0,004a001f +1267425000,57,1,ff09ff1b +1267435000,57,2,01e00008 +1267445000,57,3,fd87006a +1267455000,57,4,0267008a +1267465000,57,5,fd6400bf +1267475000,57,6,0166fc59 +1267485000,57,7,00dd0262 +1267495000,57,8,ffd20163 +1267505000,57,9,fcfbff19 +1267515000,57,10,03a6fe18 +1267525000,57,11,fe3100e2 +1267535000,57,12,ffc9029c +1267545000,57,13,01b0fd45 +1267555000,57,14,fe9800c7 +1267565000,57,15,00830032 +1269445000,57,16,fff2ffef +1269455000,57,17,ffa40060 +1269465000,57,18,001b0043 +1269475000,57,19,00f70002 +1269485000,57,20,ffd401ba +1269495000,57,21,fab5fe4a +1269505000,57,22,03f8feb2 +1269515000,57,23,00dd0174 +1269525000,57,24,feb2ff25 +1269535000,57,25,00c4027c +1269545000,57,26,fe3ffc07 +1269555000,57,27,0101020a +1269565000,57,28,0020017e +1269575000,57,29,00c7fe4e +1269585000,57,30,fef600f0 +1269595000,57,31,0037ff74 +1271475000,58,0,001effa1 +1271485000,58,1,ff6d000a +1271495000,58,2,fff1ff24 +1271505000,58,3,00ce0103 +1271515000,58,4,ffc0019a +1271525000,58,5,0026fe9a +1271535000,58,6,ff32fb41 +1271545000,58,7,028e06e9 +1271555000,58,8,fcf2fc57 +1271565000,58,9,ffb501c2 +1271575000,58,10,022bfd70 +1271585000,58,11,fe6a0231 +1271595000,58,12,00d8ffd2 +1271605000,58,13,ffb80072 +1271615000,58,14,ff9afe97 +1271625000,58,15,00aa013b +1273505000,58,16,ffed000a +1273515000,58,17,ffedff7e +1273525000,58,18,004a01ab +1273535000,58,19,ff7fff0e +1273545000,58,20,016d002a +1273555000,58,21,f9d4010c +1273565000,58,22,07aafd0f +1273575000,58,23,fd12014a +1273585000,58,24,00bbffa0 +1273595000,58,25,fdcf0132 +1273605000,58,26,feb6ffbb +1273615000,58,27,054ffe10 +1273625000,58,28,fcdf01fc +1273635000,58,29,0104fffc +1273645000,58,30,ff22ffeb +1273655000,58,31,002cfff0 +1275535000,59,0,001b001a +1275545000,59,1,0134ff84 +1275555000,59,2,fe4cffd2 +1275565000,59,3,0002016b +1275575000,59,4,0172fdf7 +1275585000,59,5,00b801e2 +1275595000,59,6,fcb5ff7b +1275605000,59,7,0234fe19 +1275615000,59,8,fef90194 +1275625000,59,9,016400c6 +1275635000,59,10,ffb20000 +1275645000,59,11,fe96fe3d +1275655000,59,12,009a0043 +1275665000,59,13,00d40180 +1275675000,59,14,ff8dff63 +1275685000,59,15,ffb0fffb +1277565000,59,16,ffbcffac +1277575000,59,17,ff460028 +1277585000,59,18,018f010c +1277595000,59,19,fe4cfe4b +1277605000,59,20,fffa02e9 +1277615000,59,21,ffd5fdde +1277625000,59,22,0065fe63 +1277635000,59,23,000b02f2 +1277645000,59,24,0062fdc2 +1277655000,59,25,fd2603a8 +1277665000,59,26,0035faf4 +1277675000,59,27,036a0399 +1277685000,59,28,fdb00011 +1277695000,59,29,00fbfe42 +1277705000,59,30,fe4f0165 +1277715000,59,31,0103ffaa +1279595000,60,0,008a004b +1279605000,60,1,ff7cffba +1279615000,60,2,fedbffbf +1279625000,60,3,02b80008 +1279635000,60,4,fe430207 +1279645000,60,5,0139fdfd +1279655000,60,6,ff06fe77 +1279665000,60,7,ff02013a +1279675000,60,8,013002d7 +1279685000,60,9,00a2fa9e +1279695000,60,10,ffa1050f +1279705000,60,11,fd00fcf6 +1279715000,60,12,0313014f +1279725000,60,13,0029ffa7 +1279735000,60,14,fee600bb +1279745000,60,15,004eff54 +1281625000,60,16,fff0fff7 +1281635000,60,17,ff6c0013 +1281645000,60,18,02a7ff51 +1281655000,60,19,fd460188 +1281665000,60,20,004bff26 +1281675000,60,21,00b604a1 +1281685000,60,22,fd26f7b9 +1281695000,60,23,03e10465 +1281705000,60,24,fd2afee5 +1281715000,60,25,fefe001b +1281725000,60,26,0209fea3 +1281735000,60,27,ff0e0234 +1281745000,60,28,0167ff6a +1281755000,60,29,fe58ffdd +1281765000,60,30,00f6004f +1281775000,60,31,ff8b000b +1283655000,61,0,ffd0ffca +1283665000,61,1,000800b8 +1283675000,61,2,ff0aff01 +1283685000,61,3,028bffde +1283695000,61,4,fd8c0228 +1283705000,61,5,ffb9ff48 +1283715000,61,6,0164fca1 +1283725000,61,7,00cc037a +1283735000,61,8,ffb0ffc4 +1283745000,61,9,fc28fdea +1283755000,61,10,0316037f +1283765000,61,11,00a1fc9c +1283775000,61,12,00100286 +1283785000,61,13,fe9bfe66 +1283795000,61,14,00a000eb +1283805000,61,15,0044ff74 +1285685000,61,16,fff6ffc1 +1285695000,61,17,ff230058 +1285705000,61,18,02b5007b +1285715000,61,19,fd06fe72 +1285725000,61,20,01f6011c +1285735000,61,21,fdc1026c +1285745000,61,22,0120fa7a +1285755000,61,23,007b045a +1285765000,61,24,00eafdd7 +1285775000,61,25,fcb102ae +1285785000,61,26,021bfced +1285795000,61,27,fe8cfff0 +1285805000,61,28,03fa02c0 +1285815000,61,29,fcafff26 +1285825000,61,30,0100ffe2 +1285835000,61,31,003f0044 +1287715000,62,0,fff4ffe0 +1287725000,62,1,0067ffca +1287735000,62,2,ff74003a +1287745000,62,3,feef00cc +1287755000,62,4,03b9fe4a +1287765000,62,5,fba302e4 +1287775000,62,6,0304fb52 +1287785000,62,7,fd930292 +1287795000,62,8,038c01c0 +1287805000,62,9,fc2dfe36 +1287815000,62,10,0064fe24 +1287825000,62,11,02290332 +1287835000,62,12,fee3fe56 +1287845000,62,13,ffc10180 +1287855000,62,14,0030fea0 +1287865000,62,15,0035007c +1289745000,62,16,005eff5f +1289755000,62,17,fee800b1 +1289765000,62,18,023900f0 +1289775000,62,19,fcd0fdd9 +1289785000,62,20,013000a5 +1289795000,62,21,ffa10032 +1289805000,62,22,ff650149 +1289815000,62,23,01b1fe0c +1289825000,62,24,fdceffc5 +1289835000,62,25,00800039 +1289845000,62,26,00a10076 +1289855000,62,27,ffaaffef +1289865000,62,28,ff0400ff +1289875000,62,29,011fff04 +1289885000,62,30,00310001 +1289895000,62,31,ff9d0094 +1291775000,63,0,ff5cffff +1291785000,63,1,00adff00 +1291795000,63,2,003001c7 +1291805000,63,3,fe85fe80 +1291815000,63,4,02340003 +1291825000,63,5,ff280263 +1291835000,63,6,fda6fb46 +1291845000,63,7,03c003ae +1291855000,63,8,ffe6fd29 +1291865000,63,9,fb4103fa +1291875000,63,10,02b6fc75 +1291885000,63,11,0111012c +1291895000,63,12,ff020055 +1291905000,63,13,004a0087 +1291915000,63,14,fef4ff5e +1291925000,63,15,01520062 +1293805000,63,16,ffe3ff95 +1293815000,63,17,ffe300c2 +1293825000,63,18,ffc9ffce +1293835000,63,19,ffb0fed6 +1293845000,63,20,022a0084 +1293855000,63,21,fc3f0040 +1293865000,63,22,030500f0 +1293875000,63,23,0071fd0a +1293885000,63,24,fbc7037d +1293895000,63,25,0489ff40 +1293905000,63,26,fdf10228 +1293915000,63,27,ff40fbd6 +1293925000,63,28,ffe0035e +1293935000,63,29,0001fd86 +1293945000,63,30,018d0156 +1293955000,63,31,ff13ffe2 diff --git a/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py b/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py index 4ccd6d0..8b9d79e 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py @@ -44,25 +44,22 @@ pass_count = 0 fail_count = 0 warn_count = 0 -def check(condition, label): +def check(condition, _label): global pass_count, fail_count if condition: - print(f" [PASS] {label}") pass_count += 1 else: - print(f" [FAIL] {label}") fail_count += 1 -def warn(label): +def warn(_label): global warn_count - print(f" [WARN] {label}") warn_count += 1 def read_mem_hex(filename): """Read a .mem file, return list of integer values (16-bit signed).""" path = os.path.join(MEM_DIR, filename) values = [] - with open(path, 'r') as f: + with open(path) as f: for line in f: line = line.strip() if not line or line.startswith('//'): @@ -79,7 +76,6 @@ def read_mem_hex(filename): # TEST 1: Structural validation of all .mem files # ============================================================================ def test_structural(): - print("\n=== TEST 1: Structural Validation ===") expected = { # FFT twiddle files (quarter-wave cosine ROMs) @@ -119,16 +115,13 @@ def test_structural(): # TEST 2: FFT Twiddle Factor Validation # ============================================================================ def test_twiddle_1024(): - print("\n=== TEST 2a: FFT Twiddle 1024 Validation ===") vals = read_mem_hex('fft_twiddle_1024.mem') - # Expected: cos(2*pi*k/1024) for k=0..255, in Q15 format - # Q15: value = round(cos(angle) * 32767) max_err = 0 err_details = [] for k in range(min(256, len(vals))): angle = 2.0 * math.pi * k / 1024.0 - expected = int(round(math.cos(angle) * 32767.0)) + expected = round(math.cos(angle) * 32767.0) expected = max(-32768, min(32767, expected)) actual = vals[k] err = abs(actual - expected) @@ -140,19 +133,17 @@ def test_twiddle_1024(): check(max_err <= 1, f"fft_twiddle_1024.mem: max twiddle error = {max_err} LSB (tolerance: 1)") if err_details: - for k, act, exp, e in err_details[:5]: - print(f" k={k}: got {act} (0x{act & 0xFFFF:04x}), expected {exp}, err={e}") - print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries") + for _, _act, _exp, _e in err_details[:5]: + pass def test_twiddle_16(): - print("\n=== TEST 2b: FFT Twiddle 16 Validation ===") vals = read_mem_hex('fft_twiddle_16.mem') max_err = 0 for k in range(min(4, len(vals))): angle = 2.0 * math.pi * k / 16.0 - expected = int(round(math.cos(angle) * 32767.0)) + expected = round(math.cos(angle) * 32767.0) expected = max(-32768, min(32767, expected)) actual = vals[k] err = abs(actual - expected) @@ -161,23 +152,17 @@ def test_twiddle_16(): check(max_err <= 1, f"fft_twiddle_16.mem: max twiddle error = {max_err} LSB (tolerance: 1)") - print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries") # Print all 4 entries for reference - print(" Twiddle 16 entries:") for k in range(min(4, len(vals))): angle = 2.0 * math.pi * k / 16.0 - expected = int(round(math.cos(angle) * 32767.0)) - print(f" k={k}: file=0x{vals[k] & 0xFFFF:04x} ({vals[k]:6d}), " - f"expected=0x{expected & 0xFFFF:04x} ({expected:6d}), " - f"err={abs(vals[k] - expected)}") + expected = round(math.cos(angle) * 32767.0) # ============================================================================ # TEST 3: Long Chirp .mem File Analysis # ============================================================================ def test_long_chirp(): - print("\n=== TEST 3: Long Chirp .mem File Analysis ===") # Load all 4 segments all_i = [] @@ -193,36 +178,29 @@ def test_long_chirp(): f"Total long chirp samples: {total_samples} (expected 4096 = 4 segs x 1024)") # Compute magnitude envelope - magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(all_i, all_q)] + magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(all_i, all_q, strict=False)] max_mag = max(magnitudes) - min_mag = min(magnitudes) - avg_mag = sum(magnitudes) / len(magnitudes) + min(magnitudes) + sum(magnitudes) / len(magnitudes) - print(f" Magnitude: min={min_mag:.1f}, max={max_mag:.1f}, avg={avg_mag:.1f}") - print( - f" Max magnitude as fraction of Q15 range: " - f"{max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)" - ) # Check if this looks like it came from generate_reference_chirp_q15 # That function uses 32767 * 0.9 scaling => max magnitude ~29490 expected_max_from_model = 32767 * 0.9 uses_model_scaling = max_mag > expected_max_from_model * 0.8 if uses_model_scaling: - print(" Scaling: CONSISTENT with radar_scene.py model (0.9 * Q15)") + pass else: warn(f"Magnitude ({max_mag:.0f}) is much lower than expected from Python model " f"({expected_max_from_model:.0f}). .mem files may have unknown provenance.") # Check non-zero content: how many samples are non-zero? - nonzero_i = sum(1 for v in all_i if v != 0) - nonzero_q = sum(1 for v in all_q if v != 0) - print(f" Non-zero samples: I={nonzero_i}/{total_samples}, Q={nonzero_q}/{total_samples}") + sum(1 for v in all_i if v != 0) + sum(1 for v in all_q if v != 0) # Analyze instantaneous frequency via phase differences - # Phase = atan2(Q, I) phases = [] - for i_val, q_val in zip(all_i, all_q): + for i_val, q_val in zip(all_i, all_q, strict=False): if abs(i_val) > 5 or abs(q_val) > 5: # Skip near-zero samples phases.append(math.atan2(q_val, i_val)) else: @@ -243,19 +221,12 @@ def test_long_chirp(): freq_estimates.append(f_inst) if freq_estimates: - f_start = sum(freq_estimates[:50]) / 50 if len(freq_estimates) > 50 else freq_estimates[0] - f_end = sum(freq_estimates[-50:]) / 50 if len(freq_estimates) > 50 else freq_estimates[-1] + sum(freq_estimates[:50]) / 50 if len(freq_estimates) > 50 else freq_estimates[0] + sum(freq_estimates[-50:]) / 50 if len(freq_estimates) > 50 else freq_estimates[-1] f_min = min(freq_estimates) f_max = max(freq_estimates) f_range = f_max - f_min - print("\n Instantaneous frequency analysis (post-DDC baseband):") - print(f" Start freq: {f_start/1e6:.3f} MHz") - print(f" End freq: {f_end/1e6:.3f} MHz") - print(f" Min freq: {f_min/1e6:.3f} MHz") - print(f" Max freq: {f_max/1e6:.3f} MHz") - print(f" Freq range: {f_range/1e6:.3f} MHz") - print(f" Expected BW: {CHIRP_BW/1e6:.3f} MHz") # A chirp should show frequency sweep is_chirp = f_range > 0.5e6 # At least 0.5 MHz sweep @@ -265,23 +236,19 @@ def test_long_chirp(): # Check if bandwidth roughly matches expected bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50% if bw_match: - print( - f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected " - f"{CHIRP_BW/1e6:.2f} MHz" - ) + pass else: warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz") # Compare segment boundaries for overlap-save consistency # In proper overlap-save, the chirp data should be segmented at 896-sample boundaries # with segments being 1024-sample FFT blocks - print("\n Segment boundary analysis:") for seg in range(4): seg_i = read_mem_hex(f'long_chirp_seg{seg}_i.mem') seg_q = read_mem_hex(f'long_chirp_seg{seg}_q.mem') - seg_mags = [math.sqrt(i*i + q*q) for i, q in zip(seg_i, seg_q)] - seg_avg = sum(seg_mags) / len(seg_mags) - seg_max = max(seg_mags) + seg_mags = [math.sqrt(i*i + q*q) for i, q in zip(seg_i, seg_q, strict=False)] + sum(seg_mags) / len(seg_mags) + max(seg_mags) # Check segment 3 zero-padding (chirp is 3000 samples, seg3 starts at 3072) # Samples 3000-4095 should be zero (or near-zero) if chirp is exactly 3000 samples @@ -293,21 +260,18 @@ def test_long_chirp(): # Wait, but the .mem files have 1024 lines with non-trivial data... # Let's check if seg3 has significant data zero_count = sum(1 for m in seg_mags if m < 2) - print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}, " - f"near-zero={zero_count}/{len(seg_mags)}") if zero_count > 500: - print(" -> Seg 3 mostly zeros (chirp shorter than 4096 samples)") + pass else: - print(" -> Seg 3 has significant data throughout") + pass else: - print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}") + pass # ============================================================================ # TEST 4: Short Chirp .mem File Analysis # ============================================================================ def test_short_chirp(): - print("\n=== TEST 4: Short Chirp .mem File Analysis ===") short_i = read_mem_hex('short_chirp_i.mem') short_q = read_mem_hex('short_chirp_q.mem') @@ -320,19 +284,17 @@ def test_short_chirp(): check(len(short_i) == expected_samples, f"Short chirp length matches T_SHORT_CHIRP * FS_SYS = {expected_samples}") - magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(short_i, short_q)] - max_mag = max(magnitudes) - avg_mag = sum(magnitudes) / len(magnitudes) + magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(short_i, short_q, strict=False)] + max(magnitudes) + sum(magnitudes) / len(magnitudes) - print(f" Magnitude: max={max_mag:.1f}, avg={avg_mag:.1f}") - print(f" Max as fraction of Q15: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)") # Check non-zero nonzero = sum(1 for m in magnitudes if m > 1) check(nonzero == len(short_i), f"All {nonzero}/{len(short_i)} samples non-zero") # Check it looks like a chirp (phase should be quadratic) - phases = [math.atan2(q, i) for i, q in zip(short_i, short_q)] + phases = [math.atan2(q, i) for i, q in zip(short_i, short_q, strict=False)] freq_est = [] for n in range(1, len(phases)): dp = phases[n] - phases[n-1] @@ -343,17 +305,14 @@ def test_short_chirp(): freq_est.append(dp * FS_SYS / (2 * math.pi)) if freq_est: - f_start = freq_est[0] - f_end = freq_est[-1] - print(f" Freq start: {f_start/1e6:.3f} MHz, end: {f_end/1e6:.3f} MHz") - print(f" Freq range: {abs(f_end - f_start)/1e6:.3f} MHz") + freq_est[0] + freq_est[-1] # ============================================================================ # TEST 5: Generate Expected Chirp .mem and Compare # ============================================================================ def test_chirp_vs_model(): - print("\n=== TEST 5: Compare .mem Files vs Python Model ===") # Generate reference using the same method as radar_scene.py chirp_rate = CHIRP_BW / T_LONG_CHIRP # Hz/s @@ -365,8 +324,8 @@ def test_chirp_vs_model(): for n in range(n_chirp): t = n / FS_SYS phase = math.pi * chirp_rate * t * t - re_val = int(round(32767 * 0.9 * math.cos(phase))) - im_val = int(round(32767 * 0.9 * math.sin(phase))) + re_val = round(32767 * 0.9 * math.cos(phase)) + im_val = round(32767 * 0.9 * math.sin(phase)) model_i.append(max(-32768, min(32767, re_val))) model_q.append(max(-32768, min(32767, im_val))) @@ -375,37 +334,31 @@ def test_chirp_vs_model(): mem_q = read_mem_hex('long_chirp_seg0_q.mem') # Compare magnitudes - model_mags = [math.sqrt(i*i + q*q) for i, q in zip(model_i, model_q)] - mem_mags = [math.sqrt(i*i + q*q) for i, q in zip(mem_i, mem_q)] + model_mags = [math.sqrt(i*i + q*q) for i, q in zip(model_i, model_q, strict=False)] + mem_mags = [math.sqrt(i*i + q*q) for i, q in zip(mem_i, mem_q, strict=False)] model_max = max(model_mags) mem_max = max(mem_mags) - print(f" Python model seg0: max_mag={model_max:.1f} (Q15 * 0.9)") - print(f" .mem file seg0: max_mag={mem_max:.1f}") - print(f" Ratio (mem/model): {mem_max/model_max:.4f}") # Check if they match (they almost certainly won't based on magnitude analysis) - matches = sum(1 for a, b in zip(model_i, mem_i) if a == b) - print(f" Exact I matches: {matches}/{len(model_i)}") + matches = sum(1 for a, b in zip(model_i, mem_i, strict=False) if a == b) if matches > len(model_i) * 0.9: - print(" -> .mem files MATCH Python model") + pass else: warn(".mem files do NOT match Python model. They likely have different provenance.") # Try to detect scaling if mem_max > 0: - ratio = model_max / mem_max - print(f" Scale factor (model/mem): {ratio:.2f}") - print(f" This suggests the .mem files used ~{1.0/ratio:.4f} scaling instead of 0.9") + model_max / mem_max # Check phase correlation (shape match regardless of scaling) - model_phases = [math.atan2(q, i) for i, q in zip(model_i, model_q)] - mem_phases = [math.atan2(q, i) for i, q in zip(mem_i, mem_q)] + model_phases = [math.atan2(q, i) for i, q in zip(model_i, model_q, strict=False)] + mem_phases = [math.atan2(q, i) for i, q in zip(mem_i, mem_q, strict=False)] # Compute phase differences phase_diffs = [] - for mp, fp in zip(model_phases, mem_phases): + for mp, fp in zip(model_phases, mem_phases, strict=False): d = mp - fp while d > math.pi: d -= 2 * math.pi @@ -413,12 +366,9 @@ def test_chirp_vs_model(): d += 2 * math.pi phase_diffs.append(d) - avg_phase_diff = sum(phase_diffs) / len(phase_diffs) + sum(phase_diffs) / len(phase_diffs) max_phase_diff = max(abs(d) for d in phase_diffs) - print("\n Phase comparison (shape regardless of amplitude):") - print(f" Avg phase diff: {avg_phase_diff:.4f} rad ({math.degrees(avg_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 check( @@ -432,7 +382,6 @@ def test_chirp_vs_model(): # TEST 6: Latency Buffer LATENCY=3187 Validation # ============================================================================ def test_latency_buffer(): - print("\n=== TEST 6: Latency Buffer LATENCY=3187 Validation ===") # The latency buffer delays the reference chirp data to align with # the matched filter processing chain output. @@ -491,16 +440,10 @@ def test_latency_buffer(): f"LATENCY={LATENCY} in reasonable range [1000, 4095]") # Check that the module name vs parameter is consistent - print(f" LATENCY parameter: {LATENCY}") - print(f" Module name: latency_buffer (parameterized, LATENCY={LATENCY})") # Module name was renamed from latency_buffer_2159 to latency_buffer # to match the actual parameterized LATENCY value. No warning needed. # Validate address arithmetic won't overflow - # read_ptr = (write_ptr - LATENCY) mod 4096 - # With 12-bit address, max write_ptr = 4095 - # When write_ptr < LATENCY: read_ptr = 4096 + write_ptr - LATENCY - # Minimum: 4096 + 0 - 3187 = 909 (valid) min_read_ptr = 4096 + 0 - LATENCY check(min_read_ptr >= 0 and min_read_ptr < 4096, f"Min read_ptr after wrap = {min_read_ptr} (valid: 0..4095)") @@ -508,14 +451,12 @@ def test_latency_buffer(): # The latency buffer uses valid_in gated reads, so it only counts # valid samples. The number of valid_in pulses between first write # and first read is LATENCY. - print(f" Buffer primes after {LATENCY} valid_in pulses, then outputs continuously") # ============================================================================ # TEST 7: Cross-check chirp memory loader addressing # ============================================================================ def test_memory_addressing(): - print("\n=== TEST 7: Chirp Memory Loader Addressing ===") # chirp_memory_loader_param uses: long_addr = {segment_select[1:0], sample_addr[9:0]} # This creates a 12-bit address: seg[1:0] ++ addr[9:0] @@ -541,15 +482,12 @@ def test_memory_addressing(): # Memory is declared as: reg [15:0] long_chirp_i [0:4095] # $readmemh loads seg0 to [0:1023], seg1 to [1024:2047], etc. # Addressing via {segment_select, sample_addr} maps correctly. - print(" Addressing scheme: {segment_select[1:0], sample_addr[9:0]} -> 12-bit address") - print(" Memory size: [0:4095] (4096 entries) — matches 4 segments x 1024 samples") # ============================================================================ # TEST 8: Seg3 zero-padding analysis # ============================================================================ def test_seg3_padding(): - print("\n=== TEST 8: Segment 3 Data Analysis ===") # The long chirp has 3000 samples (30 us at 100 MHz). # With 4 segments of 1024 samples = 4096 total memory slots. @@ -578,7 +516,7 @@ def test_seg3_padding(): seg3_i = read_mem_hex('long_chirp_seg3_i.mem') seg3_q = read_mem_hex('long_chirp_seg3_q.mem') - mags = [math.sqrt(i*i + q*q) for i, q in zip(seg3_i, seg3_q)] + mags = [math.sqrt(i*i + q*q) for i, q in zip(seg3_i, seg3_q, strict=False)] # Count trailing zeros (samples after chirp ends) trailing_zeros = 0 @@ -590,14 +528,8 @@ def test_seg3_padding(): nonzero = sum(1 for m in mags if m > 2) - print(f" Seg3 non-zero samples: {nonzero}/{len(seg3_i)}") - print(f" Seg3 trailing near-zeros: {trailing_zeros}") - print(f" Seg3 max magnitude: {max(mags):.1f}") - print(f" Seg3 first 5 magnitudes: {[f'{m:.1f}' for m in mags[:5]]}") - print(f" Seg3 last 5 magnitudes: {[f'{m:.1f}' for m in mags[-5:]]}") if nonzero == 1024: - print(" -> Seg3 has data throughout (chirp extends beyond 3072 samples or is padded)") # This means the .mem files encode 4096 chirp samples, not 3000 # The chirp duration used for .mem generation was different from T_LONG_CHIRP actual_chirp_samples = 4 * 1024 # = 4096 @@ -607,17 +539,13 @@ def test_seg3_padding(): f"({T_LONG_CHIRP*1e6:.1f} us)") elif trailing_zeros > 100: # Some padding at end - actual_valid = 3072 + (1024 - trailing_zeros) - print(f" -> Estimated valid chirp samples in .mem: ~{actual_valid}") + 3072 + (1024 - trailing_zeros) # ============================================================================ # MAIN # ============================================================================ def main(): - print("=" * 70) - print("AERIS-10 .mem File Validation") - print("=" * 70) test_structural() test_twiddle_1024() @@ -629,13 +557,10 @@ def main(): test_memory_addressing() test_seg3_padding() - print("\n" + "=" * 70) - print(f"SUMMARY: {pass_count} PASS, {fail_count} FAIL, {warn_count} WARN") if fail_count == 0: - print("ALL CHECKS PASSED") + pass else: - print("SOME CHECKS FAILED") - print("=" * 70) + pass return 0 if fail_count == 0 else 1 diff --git a/9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py b/9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py index e3f7d52..161e9d9 100644 --- a/9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py +++ b/9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py @@ -28,8 +28,7 @@ N = 1024 # FFT length def to_q15(value): """Clamp a floating-point value to 16-bit signed range [-32768, 32767].""" v = int(np.round(value)) - v = max(-32768, min(32767, v)) - return v + return max(-32768, min(32767, v)) def to_hex16(value): @@ -108,7 +107,7 @@ def generate_case(case_num, sig_i, sig_q, ref_i, ref_q, description, outdir): f"mf_golden_out_q_case{case_num}.hex", ] - summary = { + return { "case": case_num, "description": description, "peak_bin": peak_bin, @@ -119,7 +118,6 @@ def generate_case(case_num, sig_i, sig_q, ref_i, ref_q, description, outdir): "peak_q_quant": peak_q_q, "files": files, } - return summary def main(): @@ -149,7 +147,6 @@ def main(): # ========================================================================= # Case 2: Tone autocorrelation at bin 5 # Signal and reference: complex tone at bin 5, amplitude 8000 (Q15) - # sig[n] = 8000 * exp(j * 2*pi*5*n/N) # Autocorrelation of a tone => peak at bin 0 (lag 0) # ========================================================================= amp = 8000.0 @@ -243,28 +240,12 @@ def main(): # ========================================================================= # Print summary to stdout # ========================================================================= - print("=" * 72) - print("Matched Filter Golden Reference Generator") - print(f"Output directory: {outdir}") - print(f"FFT length: {N}") - print("=" * 72) - for s in summaries: - print() - print(f"Case {s['case']}: {s['description']}") - print(f" Peak bin: {s['peak_bin']}") - print(f" Peak magnitude (float):{s['peak_mag_float']:.6f}") - print(f" Peak I (float): {s['peak_i_float']:.6f}") - print(f" Peak Q (float): {s['peak_q_float']:.6f}") - print(f" Peak I (quantized): {s['peak_i_quant']}") - print(f" Peak Q (quantized): {s['peak_q_quant']}") + for _ in summaries: + pass - print() - print(f"Generated {len(all_files)} files:") - for fname in all_files: - print(f" {fname}") - print() - print("Done.") + for _ in all_files: + pass if __name__ == "__main__": diff --git a/9_Firmware/9_3_GUI/GUI_PyQt_Map.py b/9_Firmware/9_3_GUI/GUI_PyQt_Map.py index 798fb33..67b4555 100644 --- a/9_Firmware/9_3_GUI/GUI_PyQt_Map.py +++ b/9_Firmware/9_3_GUI/GUI_PyQt_Map.py @@ -26,7 +26,6 @@ import time import random import logging from dataclasses import dataclass, asdict -from typing import List, Dict, Optional, Tuple from enum import Enum # PyQt6 imports @@ -198,12 +197,12 @@ class RadarMapWidget(QWidget): altitude=100.0, pitch=0.0 ) - self._targets: List[RadarTarget] = [] + self._targets: list[RadarTarget] = [] self._coverage_radius = 50000 # meters self._tile_server = TileServer.OPENSTREETMAP self._show_coverage = True self._show_trails = False - self._target_history: Dict[int, List[Tuple[float, float]]] = {} + self._target_history: dict[int, list[tuple[float, float]]] = {} # Setup UI self._setup_ui() @@ -908,7 +907,7 @@ class RadarMapWidget(QWidget): """Handle marker click events""" self.targetSelected.emit(target_id) - def _on_tile_server_changed(self, index: int): + def _on_tile_server_changed(self, _index: int): """Handle tile server change""" server = self._tile_combo.currentData() self._tile_server = server @@ -947,7 +946,7 @@ class RadarMapWidget(QWidget): f"{gps_data.altitude}, {gps_data.pitch}, {gps_data.heading})" ) - def set_targets(self, targets: List[RadarTarget]): + def set_targets(self, targets: list[RadarTarget]): """Update all targets on the map""" self._targets = targets @@ -980,7 +979,7 @@ def polar_to_geographic( radar_lon: float, range_m: float, azimuth_deg: float -) -> Tuple[float, float]: +) -> tuple[float, float]: """ Convert polar coordinates (range, azimuth) relative to radar to geographic coordinates (latitude, longitude). @@ -1028,7 +1027,7 @@ class TargetSimulator(QObject): super().__init__(parent) self._radar_position = radar_position - self._targets: List[RadarTarget] = [] + self._targets: list[RadarTarget] = [] self._next_id = 1 self._timer = QTimer() self._timer.timeout.connect(self._update_targets) @@ -1164,7 +1163,7 @@ class RadarDashboard(QMainWindow): timestamp=time.time() ) self._settings = RadarSettings() - self._simulator: Optional[TargetSimulator] = None + self._simulator: TargetSimulator | None = None self._demo_mode = True # Setup UI @@ -1571,7 +1570,7 @@ class RadarDashboard(QMainWindow): self._simulator._add_random_target() logger.info("Added random target") - def _on_targets_updated(self, targets: List[RadarTarget]): + def _on_targets_updated(self, targets: list[RadarTarget]): """Handle updated target list from simulator""" # Update map self._map_widget.set_targets(targets) @@ -1582,7 +1581,7 @@ class RadarDashboard(QMainWindow): # Update table self._update_targets_table(targets) - def _update_targets_table(self, targets: List[RadarTarget]): + def _update_targets_table(self, targets: list[RadarTarget]): """Update the targets table""" self._targets_table.setRowCount(len(targets)) diff --git a/9_Firmware/9_3_GUI/GUI_V1.py b/9_Firmware/9_3_GUI/GUI_V1.py deleted file mode 100644 index e740882..0000000 --- a/9_Firmware/9_3_GUI/GUI_V1.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -import queue -import tkinter as tk -from tkinter import messagebox - - -class RadarGUI: - def update_gps_display(self): - """Step 18: Update GPS display and center map""" - try: - while not self.gps_data_queue.empty(): - gps_data = self.gps_data_queue.get_nowait() - self.current_gps = gps_data - - # Update GPS label - self.gps_label.config( - text=( - f"GPS: Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m" - ) - ) - - # Update map - self.update_map_display(gps_data) - - except queue.Empty: - 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(): - """Main application entry point""" - try: - root = tk.Tk() - _app = RadarGUI(root) - root.mainloop() - except Exception as e: - logging.error(f"Application error: {e}") - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") - -if __name__ == "__main__": - main() diff --git a/9_Firmware/9_3_GUI/GUI_V2.py b/9_Firmware/9_3_GUI/GUI_V2.py deleted file mode 100644 index b6c913b..0000000 --- a/9_Firmware/9_3_GUI/GUI_V2.py +++ /dev/null @@ -1,1124 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox -import threading -import queue -import time -import struct -import numpy as np -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.figure import Figure -import logging -from dataclasses import dataclass -from sklearn.cluster import DBSCAN -from filterpy.kalman import KalmanFilter -import crcmod - -try: - import usb.core - import usb.util - - USB_AVAILABLE = True -except ImportError: - USB_AVAILABLE = False - logging.warning("pyusb not available. USB CDC functionality will be disabled.") - -try: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools - - FTDI_AVAILABLE = True -except ImportError: - FTDI_AVAILABLE = False - logging.warning("pyftdi not available. FTDI functionality will be disabled.") - -# Configure logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - - -@dataclass -class RadarTarget: - id: int - range: float - velocity: float - azimuth: int - elevation: int - snr: float - timestamp: float - track_id: int = -1 - - -@dataclass -class RadarSettings: - system_frequency: float = 10e9 - chirp_duration: float = 30e-6 - chirps_per_position: int = 32 - freq_min: float = 10e6 - freq_max: float = 30e6 - prf1: float = 1000 - prf2: float = 2000 - max_distance: float = 50000 - - -@dataclass -class GPSData: - latitude: float - longitude: float - altitude: float - timestamp: float - - -class STM32USBInterface: - def __init__(self): - self.device = None - self.is_open = False - self.ep_in = None - self.ep_out = None - - def list_devices(self): - """List available STM32 USB CDC devices""" - if not USB_AVAILABLE: - logging.warning("USB not available - please install pyusb") - return [] - - try: - devices = [] - # STM32 USB CDC devices typically use these vendor/product IDs - stm32_vid_pids = [ - (0x0483, 0x5740), # STM32 Virtual COM Port - (0x0483, 0x3748), # STM32 Discovery - (0x0483, 0x374B), # STM32 CDC - (0x0483, 0x374D), # STM32 CDC - (0x0483, 0x374E), # STM32 CDC - (0x0483, 0x3752), # STM32 CDC - ] - - for vid, pid in stm32_vid_pids: - found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid) - for dev in found_devices: - try: - product = ( - usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC" - ) - serial = ( - usb.util.get_string(dev, dev.iSerialNumber) - if dev.iSerialNumber - else "Unknown" - ) - devices.append( - { - "description": f"{product} ({serial})", - "vendor_id": vid, - "product_id": pid, - "device": dev, - } - ) - except Exception: - devices.append( - { - "description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", - "vendor_id": vid, - "product_id": pid, - "device": dev, - } - ) - - return devices - except Exception as e: - logging.error(f"Error listing USB devices: {e}") - # Return mock devices for testing - return [ - {"description": "STM32 Virtual COM Port", "vendor_id": 0x0483, "product_id": 0x5740} - ] - - def open_device(self, device_info): - """Open STM32 USB CDC device""" - if not USB_AVAILABLE: - logging.error("USB not available - cannot open device") - return False - - try: - self.device = device_info["device"] - - # Detach kernel driver if active - if self.device.is_kernel_driver_active(0): - self.device.detach_kernel_driver(0) - - # Set configuration - self.device.set_configuration() - - # Get CDC endpoints - cfg = self.device.get_active_configuration() - intf = cfg[(0, 0)] - - # Find bulk endpoints (CDC data interface) - self.ep_out = usb.util.find_descriptor( - intf, - custom_match=lambda e: ( - usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT - ), - ) - - self.ep_in = usb.util.find_descriptor( - intf, - 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: - logging.error("Could not find CDC endpoints") - return False - - self.is_open = True - logging.info(f"STM32 USB device opened: {device_info['description']}") - return True - - except Exception as e: - logging.error(f"Error opening USB device: {e}") - return False - - def send_start_flag(self): - """Step 12: Send start flag to STM32 via USB""" - start_packet = bytes([23, 46, 158, 237]) - logging.info("Sending start flag to STM32 via USB...") - return self._send_data(start_packet) - - def send_settings(self, settings): - """Step 13: Send radar settings to STM32 via USB""" - try: - packet = self._create_settings_packet(settings) - logging.info("Sending radar settings to STM32 via USB...") - return self._send_data(packet) - except Exception as e: - logging.error(f"Error sending settings via USB: {e}") - return False - - def read_data(self, size=64, timeout=1000): - """Read data from STM32 via USB""" - if not self.is_open or self.ep_in is None: - return None - - try: - data = self.ep_in.read(size, timeout=timeout) - return bytes(data) - except usb.core.USBError as e: - if e.errno == 110: # Timeout - return None - logging.error(f"USB read error: {e}") - return None - except Exception as e: - logging.error(f"Error reading from USB: {e}") - return None - - def _send_data(self, data): - """Send data to STM32 via USB""" - if not self.is_open or self.ep_out is None: - return False - - try: - # USB CDC typically uses 64-byte packets - packet_size = 64 - for i in range(0, len(data), packet_size): - chunk = data[i : i + packet_size] - # Pad to packet size if needed - if len(chunk) < packet_size: - chunk += b"\x00" * (packet_size - len(chunk)) - self.ep_out.write(chunk) - - return True - except Exception as e: - logging.error(f"Error sending data via USB: {e}") - return False - - def _create_settings_packet(self, settings): - """Create binary settings packet for USB transmission""" - packet = b"SET" - packet += struct.pack(">d", settings.system_frequency) - packet += struct.pack(">d", settings.chirp_duration) - packet += struct.pack(">I", settings.chirps_per_position) - packet += struct.pack(">d", settings.freq_min) - packet += struct.pack(">d", settings.freq_max) - packet += struct.pack(">d", settings.prf1) - packet += struct.pack(">d", settings.prf2) - packet += struct.pack(">d", settings.max_distance) - packet += b"END" - return packet - - def close(self): - """Close USB device""" - if self.device and self.is_open: - try: - usb.util.dispose_resources(self.device) - self.is_open = False - except Exception as e: - logging.error(f"Error closing USB device: {e}") - - -class FTDIInterface: - def __init__(self): - self.ftdi = None - self.is_open = False - - def list_devices(self): - """List available FTDI devices using pyftdi""" - if not FTDI_AVAILABLE: - logging.warning("FTDI not available - please install pyftdi") - return [] - - try: - devices = [] - # Get list of all FTDI devices - for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID - devices.append( - {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} - ) - return devices - except Exception as e: - logging.error(f"Error listing FTDI devices: {e}") - # Return mock devices for testing - return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}] - - def open_device(self, device_url): - """Open FTDI device using pyftdi""" - if not FTDI_AVAILABLE: - logging.error("FTDI not available - cannot open device") - return False - - try: - self.ftdi = Ftdi() - self.ftdi.open_from_url(device_url) - - # Configure for synchronous FIFO mode - self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.SYNCFF) - - # Set latency timer - self.ftdi.set_latency_timer(2) - - # Purge buffers - self.ftdi.purge_buffers() - - self.is_open = True - logging.info(f"FTDI device opened: {device_url}") - return True - - except Exception as e: - logging.error(f"Error opening FTDI device: {e}") - return False - - def read_data(self, bytes_to_read): - """Read data from FTDI""" - if not self.is_open or self.ftdi is None: - return None - - try: - data = self.ftdi.read_data(bytes_to_read) - if data: - return bytes(data) - return None - except Exception as e: - logging.error(f"Error reading from FTDI: {e}") - return None - - def close(self): - """Close FTDI device""" - if self.ftdi and self.is_open: - self.ftdi.close() - self.is_open = False - - -class RadarProcessor: - def __init__(self): - self.range_doppler_map = np.zeros((1024, 32)) - self.detected_targets = [] - self.track_id_counter = 0 - self.tracks = {} - self.frame_count = 0 - - def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): - """Dual-CPI fusion for better detection""" - fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - return fused_profile - - def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): - """Multi-PRF velocity unwrapping""" - lambda_wavelength = 3e8 / 10e9 - v_max1 = prf1 * lambda_wavelength / 2 - v_max2 = prf2 * lambda_wavelength / 2 - - unwrapped_velocities = [] - for doppler in doppler_measurements: - v1 = doppler * lambda_wavelength / 2 - v2 = doppler * lambda_wavelength / 2 - - velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2) - unwrapped_velocities.append(velocity) - - return unwrapped_velocities - - def _solve_chinese_remainder(self, v1, v2, max1, max2): - for k in range(-5, 6): - candidate = v1 + k * max1 - if abs(candidate - v2) < max2 / 2: - return candidate - return v1 - - def clustering(self, detections, eps=100, min_samples=2): - """DBSCAN clustering of detections""" - if len(detections) == 0: - return [] - - points = np.array([[d.range, d.velocity] for d in detections]) - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(points) - - clusters = [] - for label in set(clustering.labels_): - if label != -1: - cluster_points = points[clustering.labels_ == label] - clusters.append( - { - "center": np.mean(cluster_points, axis=0), - "points": cluster_points, - "size": len(cluster_points), - } - ) - - return clusters - - def association(self, detections, clusters): - """Association of detections to tracks""" - associated_detections = [] - - for detection in detections: - best_track = None - min_distance = float("inf") - - for track_id, track in self.tracks.items(): - distance = np.sqrt( - (detection.range - track["state"][0]) ** 2 - + (detection.velocity - track["state"][2]) ** 2 - ) - - if distance < min_distance and distance < 500: - min_distance = distance - best_track = track_id - - if best_track is not None: - detection.track_id = best_track - associated_detections.append(detection) - else: - detection.track_id = self.track_id_counter - self.track_id_counter += 1 - associated_detections.append(detection) - - return associated_detections - - def tracking(self, associated_detections): - """Kalman filter tracking""" - current_time = time.time() - - for detection in associated_detections: - if detection.track_id not in self.tracks: - kf = KalmanFilter(dim_x=4, dim_z=2) - kf.x = np.array([detection.range, 0, detection.velocity, 0]) - kf.F = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]]) - kf.H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]]) - kf.P *= 1000 - kf.R = np.diag([10, 1]) - kf.Q = np.eye(4) * 0.1 - - self.tracks[detection.track_id] = { - "filter": kf, - "state": kf.x, - "last_update": current_time, - "hits": 1, - } - else: - track = self.tracks[detection.track_id] - track["filter"].predict() - track["filter"].update([detection.range, detection.velocity]) - track["state"] = track["filter"].x - track["last_update"] = current_time - track["hits"] += 1 - - stale_tracks = [ - tid for tid, track in self.tracks.items() if current_time - track["last_update"] > 5.0 - ] - for tid in stale_tracks: - del self.tracks[tid] - - -class USBPacketParser: - def __init__(self): - self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - - def parse_gps_data(self, data): - """Parse GPS data from STM32 USB CDC""" - if not data: - return None - - try: - # Try text format first: "GPS:lat,lon,alt\r\n" - text_data = data.decode("utf-8", errors="ignore").strip() - if text_data.startswith("GPS:"): - parts = text_data.split(":")[1].split(",") - if len(parts) == 3: - lat = float(parts[0]) - lon = float(parts[1]) - alt = float(parts[2]) - return GPSData(latitude=lat, longitude=lon, altitude=alt, timestamp=time.time()) - - # Try binary format - if len(data) >= 26 and data[0:4] == b"GPSB": - return self._parse_binary_gps(data) - - except Exception as e: - logging.error(f"Error parsing GPS data: {e}") - - return None - - def _parse_binary_gps(self, data): - """Parse binary GPS format""" - try: - # Binary format: [Header 4][Latitude 8][Longitude 8][Altitude 4][CRC 2] - if len(data) < 26: - return None - - # Verify CRC (simple checksum) - crc_received = (data[24] << 8) | data[25] - crc_calculated = sum(data[0:24]) & 0xFFFF - - if crc_received != crc_calculated: - logging.warning("GPS CRC mismatch") - return None - - # Parse latitude (double, big-endian) - lat_bits = 0 - for i in range(8): - lat_bits = (lat_bits << 8) | data[4 + i] - latitude = struct.unpack(">d", struct.pack(">Q", lat_bits))[0] - - # Parse longitude (double, big-endian) - lon_bits = 0 - for i in range(8): - lon_bits = (lon_bits << 8) | data[12 + i] - longitude = struct.unpack(">d", struct.pack(">Q", lon_bits))[0] - - # Parse altitude (float, big-endian) - alt_bits = 0 - for i in range(4): - alt_bits = (alt_bits << 8) | data[20 + i] - altitude = struct.unpack(">f", struct.pack(">I", alt_bits))[0] - - return GPSData( - latitude=latitude, longitude=longitude, altitude=altitude, timestamp=time.time() - ) - - except Exception as e: - logging.error(f"Error parsing binary GPS: {e}") - return None - - -class RadarPacketParser: - def __init__(self): - self.sync_pattern = b"\xa5\xc3" - self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - - def parse_packet(self, data): - if len(data) < 6: - return None - - sync_index = data.find(self.sync_pattern) - if sync_index == -1: - return None - - packet = data[sync_index:] - - if len(packet) < 6: - return None - - _sync = packet[0:2] - packet_type = packet[2] - length = packet[3] - - if len(packet) < (4 + length + 2): - return None - - payload = packet[4 : 4 + length] - crc_received = struct.unpack("I", payload[0:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp_counter = payload[6] & 0x1F - - return { - "type": "range", - "range": range_value, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing range packet: {e}") - return None - - def parse_doppler_packet(self, payload): - if len(payload) < 12: - return None - - try: - doppler_real = struct.unpack(">h", payload[0:2])[0] - doppler_imag = struct.unpack(">h", payload[2:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp_counter = payload[6] & 0x1F - - return { - "type": "doppler", - "doppler_real": doppler_real, - "doppler_imag": doppler_imag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing Doppler packet: {e}") - return None - - def parse_detection_packet(self, payload): - if len(payload) < 8: - return None - - try: - detection_flag = (payload[0] & 0x01) != 0 - elevation = payload[1] & 0x1F - azimuth = payload[2] & 0x3F - chirp_counter = payload[3] & 0x1F - - return { - "type": "detection", - "detected": detection_flag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing detection packet: {e}") - return None - - -class RadarGUI: - def __init__(self, root): - self.root = root - self.root.title("Advanced Radar System GUI - USB CDC") - self.root.geometry("1400x900") - - # Initialize interfaces - self.stm32_usb_interface = STM32USBInterface() - self.ftdi_interface = FTDIInterface() - self.radar_processor = RadarProcessor() - self.usb_packet_parser = USBPacketParser() - self.radar_packet_parser = RadarPacketParser() - self.settings = RadarSettings() - - # Data queues - self.radar_data_queue = queue.Queue() - self.gps_data_queue = queue.Queue() - - # Thread control - self.running = False - self.radar_thread = None - self.gps_thread = None - - # Counters - self.received_packets = 0 - self.current_gps = GPSData(latitude=41.9028, longitude=12.4964, altitude=0, timestamp=0) - - self.create_gui() - self.start_background_threads() - - def create_gui(self): - """Create the main GUI with tabs""" - self.notebook = ttk.Notebook(self.root) - self.notebook.pack(fill="both", expand=True, padx=10, pady=10) - - self.tab_main = ttk.Frame(self.notebook) - self.tab_map = ttk.Frame(self.notebook) - self.tab_diagnostics = ttk.Frame(self.notebook) - self.tab_settings = ttk.Frame(self.notebook) - - self.notebook.add(self.tab_main, text="Main View") - self.notebook.add(self.tab_map, text="Map View") - self.notebook.add(self.tab_diagnostics, text="Diagnostics") - self.notebook.add(self.tab_settings, text="Settings") - - self.setup_main_tab() - self.setup_map_tab() - self.setup_settings_tab() - - def setup_main_tab(self): - """Setup the main radar display tab""" - # Control frame - control_frame = ttk.Frame(self.tab_main) - control_frame.pack(fill="x", padx=10, pady=5) - - # USB Device selection - ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5) - self.stm32_usb_combo = ttk.Combobox(control_frame, state="readonly", width=40) - self.stm32_usb_combo.grid(row=0, column=1, padx=5) - - ttk.Label(control_frame, text="FTDI Device:").grid(row=0, column=2, padx=5) - self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30) - self.ftdi_combo.grid(row=0, column=3, padx=5) - - ttk.Button(control_frame, text="Refresh Devices", command=self.refresh_devices).grid( - row=0, column=4, padx=5 - ) - - self.start_button = ttk.Button(control_frame, text="Start Radar", command=self.start_radar) - self.start_button.grid(row=0, column=5, padx=5) - - self.stop_button = ttk.Button( - control_frame, text="Stop Radar", command=self.stop_radar, state="disabled" - ) - self.stop_button.grid(row=0, column=6, padx=5) - - # GPS info - self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...") - self.gps_label.grid(row=1, column=0, columnspan=4, sticky="w", padx=5, pady=2) - - # Status info - self.status_label = ttk.Label(control_frame, text="Status: Ready") - self.status_label.grid(row=1, column=4, columnspan=3, sticky="e", padx=5, pady=2) - - # Main display area - display_frame = ttk.Frame(self.tab_main) - display_frame.pack(fill="both", expand=True, padx=10, pady=5) - - # Range-Doppler Map - fig = Figure(figsize=(10, 6)) - self.range_doppler_ax = fig.add_subplot(111) - self.range_doppler_plot = self.range_doppler_ax.imshow( - np.random.rand(1024, 32), aspect="auto", cmap="hot", extent=[0, 32, 0, 1024] - ) - self.range_doppler_ax.set_title("Range-Doppler Map") - self.range_doppler_ax.set_xlabel("Doppler Bin") - self.range_doppler_ax.set_ylabel("Range Bin") - - self.canvas = FigureCanvasTkAgg(fig, display_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(side="left", fill="both", expand=True) - - # Targets list - targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets") - targets_frame.pack(side="right", fill="y", padx=5) - - self.targets_tree = ttk.Treeview( - targets_frame, - columns=("ID", "Range", "Velocity", "Azimuth", "Elevation", "SNR"), - show="headings", - height=20, - ) - self.targets_tree.heading("ID", text="Track ID") - self.targets_tree.heading("Range", text="Range (m)") - self.targets_tree.heading("Velocity", text="Velocity (m/s)") - self.targets_tree.heading("Azimuth", text="Azimuth") - self.targets_tree.heading("Elevation", text="Elevation") - self.targets_tree.heading("SNR", text="SNR (dB)") - - self.targets_tree.column("ID", width=80) - self.targets_tree.column("Range", width=100) - self.targets_tree.column("Velocity", width=100) - self.targets_tree.column("Azimuth", width=80) - self.targets_tree.column("Elevation", width=80) - self.targets_tree.column("SNR", width=80) - - self.targets_tree.pack(fill="both", expand=True, padx=5, pady=5) - - def setup_map_tab(self): - """Setup the map display tab""" - self.map_frame = ttk.Frame(self.tab_map) - self.map_frame.pack(fill="both", expand=True, padx=10, pady=10) - - # Map placeholder - self.map_label = ttk.Label( - self.map_frame, - text="Map will be displayed here after GPS data is received", - font=("Arial", 12), - ) - self.map_label.pack(expand=True) - - def setup_settings_tab(self): - """Setup the settings tab""" - settings_frame = ttk.Frame(self.tab_settings) - settings_frame.pack(fill="both", expand=True, padx=10, pady=10) - - entries = [ - ("System Frequency (Hz):", "system_frequency", 10e9), - ("Chirp Duration (s):", "chirp_duration", 30e-6), - ("Chirps per Position:", "chirps_per_position", 32), - ("Frequency Min (Hz):", "freq_min", 10e6), - ("Frequency Max (Hz):", "freq_max", 30e6), - ("PRF1 (Hz):", "prf1", 1000), - ("PRF2 (Hz):", "prf2", 2000), - ("Max Distance (m):", "max_distance", 50000), - ] - - self.settings_vars = {} - - for i, (label, attr, default) in enumerate(entries): - ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky="w", padx=5, pady=5) - var = tk.StringVar(value=str(default)) - entry = ttk.Entry(settings_frame, textvariable=var, width=20) - entry.grid(row=i, column=1, padx=5, pady=5) - self.settings_vars[attr] = var - - ttk.Button(settings_frame, text="Apply Settings", command=self.apply_settings).grid( - row=len(entries), column=0, columnspan=2, pady=10 - ) - - def refresh_devices(self): - """Refresh available USB devices""" - # STM32 USB devices - stm32_devices = self.stm32_usb_interface.list_devices() - stm32_names = [dev["description"] for dev in stm32_devices] - self.stm32_usb_combo["values"] = stm32_names - - # FTDI devices - ftdi_devices = self.ftdi_interface.list_devices() - ftdi_names = [dev["description"] for dev in ftdi_devices] - self.ftdi_combo["values"] = ftdi_names - - if stm32_names: - self.stm32_usb_combo.current(0) - if ftdi_names: - self.ftdi_combo.current(0) - - def start_radar(self): - """Step 11: Start button pressed - Begin radar operation""" - try: - # Open STM32 USB device - stm32_index = self.stm32_usb_combo.current() - if stm32_index == -1: - messagebox.showerror("Error", "Please select an STM32 USB device") - return - - stm32_devices = self.stm32_usb_interface.list_devices() - if stm32_index >= len(stm32_devices): - messagebox.showerror("Error", "Invalid STM32 device selection") - return - - if not self.stm32_usb_interface.open_device(stm32_devices[stm32_index]): - messagebox.showerror("Error", "Failed to open STM32 USB device") - return - - # Open FTDI device - if FTDI_AVAILABLE: - ftdi_index = self.ftdi_combo.current() - if ftdi_index != -1: - ftdi_devices = self.ftdi_interface.list_devices() - if ftdi_index < len(ftdi_devices): - device_url = ftdi_devices[ftdi_index]["url"] - if not self.ftdi_interface.open_device(device_url): - logging.warning( - "Failed to open FTDI device, continuing without radar data" - ) - else: - logging.warning("No FTDI device selected, continuing without radar data") - else: - logging.warning("FTDI not available, continuing without radar data") - - # Step 12: Send start flag to STM32 via USB - if not self.stm32_usb_interface.send_start_flag(): - messagebox.showerror("Error", "Failed to send start flag to STM32") - return - - # Step 13: Send settings to STM32 via USB - self.apply_settings() - - # Start radar operation - self.running = True - self.start_button.config(state="disabled") - self.stop_button.config(state="normal") - self.status_label.config(text="Status: Radar running - Waiting for GPS data...") - - logging.info("Radar system started successfully via USB CDC") - - except Exception as e: - messagebox.showerror("Error", f"Failed to start radar: {e}") - logging.error(f"Start radar error: {e}") - - def stop_radar(self): - """Stop radar operation""" - self.running = False - self.start_button.config(state="normal") - self.stop_button.config(state="disabled") - self.status_label.config(text="Status: Radar stopped") - - self.stm32_usb_interface.close() - self.ftdi_interface.close() - - logging.info("Radar system stopped") - - def apply_settings(self): - """Step 13: Apply and send radar settings via USB""" - try: - self.settings.system_frequency = float(self.settings_vars["system_frequency"].get()) - self.settings.chirp_duration = float(self.settings_vars["chirp_duration"].get()) - self.settings.chirps_per_position = int(self.settings_vars["chirps_per_position"].get()) - self.settings.freq_min = float(self.settings_vars["freq_min"].get()) - self.settings.freq_max = float(self.settings_vars["freq_max"].get()) - self.settings.prf1 = float(self.settings_vars["prf1"].get()) - self.settings.prf2 = float(self.settings_vars["prf2"].get()) - self.settings.max_distance = float(self.settings_vars["max_distance"].get()) - - if self.stm32_usb_interface.is_open: - self.stm32_usb_interface.send_settings(self.settings) - - messagebox.showinfo("Success", "Settings applied and sent to STM32 via USB") - logging.info("Radar settings applied via USB") - - except ValueError as e: - messagebox.showerror("Error", f"Invalid setting value: {e}") - - def start_background_threads(self): - """Start background data processing threads""" - self.radar_thread = threading.Thread(target=self.process_radar_data, daemon=True) - self.radar_thread.start() - - self.gps_thread = threading.Thread(target=self.process_gps_data, daemon=True) - self.gps_thread.start() - - self.root.after(100, self.update_gui) - - def process_radar_data(self): - """Step 39: Process incoming radar data from FTDI""" - buffer = b"" - while True: - if self.running and self.ftdi_interface.is_open: - try: - data = self.ftdi_interface.read_data(4096) - if data: - buffer += data - - while len(buffer) >= 6: - packet = self.radar_packet_parser.parse_packet(buffer) - if packet: - self.process_radar_packet(packet) - packet_length = 4 + len(packet.get("payload", b"")) + 2 - buffer = buffer[packet_length:] - self.received_packets += 1 - else: - break - - except Exception as e: - logging.error(f"Error processing radar data: {e}") - time.sleep(0.1) - else: - time.sleep(0.1) - - def process_gps_data(self): - """Step 16/17: Process GPS data from STM32 via USB CDC""" - while True: - if self.running and self.stm32_usb_interface.is_open: - try: - # Read data from STM32 USB - data = self.stm32_usb_interface.read_data(64, timeout=100) - if data: - gps_data = self.usb_packet_parser.parse_gps_data(data) - if gps_data: - self.gps_data_queue.put(gps_data) - logging.info( - "GPS Data received via USB: " - f"Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m" - ) - except Exception as e: - logging.error(f"Error processing GPS data via USB: {e}") - time.sleep(0.1) - - def process_radar_packet(self, packet): - """Step 40: Process radar data and update displays""" - try: - if packet["type"] == "range": - range_meters = packet["range"] * 0.1 - - target = RadarTarget( - id=packet["chirp"], - range=range_meters, - velocity=0, - azimuth=packet["azimuth"], - elevation=packet["elevation"], - snr=20.0, - timestamp=packet["timestamp"], - ) - - self.update_range_doppler_map(target) - - elif packet["type"] == "doppler": - lambda_wavelength = 3e8 / self.settings.system_frequency - velocity = (packet["doppler_real"] / 32767.0) * ( - self.settings.prf1 * lambda_wavelength / 2 - ) - self.update_target_velocity(packet, velocity) - - elif packet["type"] == "detection": - if packet["detected"]: - logging.info( - f"CFAR Detection: Elevation {packet['elevation']}, " - f"Azimuth {packet['azimuth']}" - ) - - except Exception as e: - logging.error(f"Error processing radar packet: {e}") - - def update_range_doppler_map(self, target): - """Update range-Doppler map with new target""" - range_bin = min(int(target.range / 50), 1023) - doppler_bin = min(abs(int(target.velocity)), 31) - - self.radar_processor.range_doppler_map[range_bin, doppler_bin] += 1 - - self.radar_processor.detected_targets.append(target) - - if len(self.radar_processor.detected_targets) > 100: - self.radar_processor.detected_targets = self.radar_processor.detected_targets[-100:] - - def update_target_velocity(self, packet, velocity): - """Update target velocity information""" - for target in self.radar_processor.detected_targets: - if ( - target.azimuth == packet["azimuth"] - and target.elevation == packet["elevation"] - and target.id == packet["chirp"] - ): - target.velocity = velocity - break - - def update_gui(self): - """Step 40: Update all GUI displays""" - try: - # Update status - if self.running: - self.status_label.config( - text=( - f"Status: Running - Packets: {self.received_packets} - " - f"GPS: {self.current_gps.latitude:.4f}, " - f"{self.current_gps.longitude:.4f}" - ) - ) - - # Update range-Doppler map - if hasattr(self, "range_doppler_plot"): - display_data = np.log10(self.radar_processor.range_doppler_map + 1) - self.range_doppler_plot.set_array(display_data) - self.canvas.draw_idle() - - # Update targets list - self.update_targets_list() - - # Update GPS display - self.update_gps_display() - - except Exception as e: - logging.error(f"Error updating GUI: {e}") - - self.root.after(100, self.update_gui) - - def update_targets_list(self): - """Update the targets list display""" - for item in self.targets_tree.get_children(): - self.targets_tree.delete(item) - - for target in self.radar_processor.detected_targets[-20:]: - self.targets_tree.insert( - "", - "end", - values=( - target.track_id, - f"{target.range:.1f}", - f"{target.velocity:.1f}", - target.azimuth, - target.elevation, - f"{target.snr:.1f}", - ), - ) - - def update_gps_display(self): - """Step 18: Update GPS display and center map""" - try: - while not self.gps_data_queue.empty(): - gps_data = self.gps_data_queue.get_nowait() - self.current_gps = gps_data - - # Update GPS label - self.gps_label.config( - text=( - f"GPS: Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m" - ) - ) - - # Update map - self.update_map_display(gps_data) - - except queue.Empty: - 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(): - """Main application entry point""" - try: - root = tk.Tk() - _app = RadarGUI(root) - root.mainloop() - except Exception as e: - logging.error(f"Application error: {e}") - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") - - -if __name__ == "__main__": - main() diff --git a/9_Firmware/9_3_GUI/GUI_V3.py b/9_Firmware/9_3_GUI/GUI_V3.py deleted file mode 100644 index cda7da5..0000000 --- a/9_Firmware/9_3_GUI/GUI_V3.py +++ /dev/null @@ -1,1225 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox -import threading -import queue -import time -import struct -import numpy as np -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.figure import Figure -import logging -from dataclasses import dataclass -from sklearn.cluster import DBSCAN -from filterpy.kalman import KalmanFilter -import crcmod -import math - -try: - import usb.core - import usb.util - - USB_AVAILABLE = True -except ImportError: - USB_AVAILABLE = False - logging.warning("pyusb not available. USB CDC functionality will be disabled.") - -try: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools - - FTDI_AVAILABLE = True -except ImportError: - FTDI_AVAILABLE = False - logging.warning("pyftdi not available. FTDI functionality will be disabled.") - -# Configure logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - - -@dataclass -class RadarTarget: - id: int - range: float - velocity: float - azimuth: int - elevation: int - snr: float - timestamp: float - track_id: int = -1 - - -@dataclass -class RadarSettings: - system_frequency: float = 10e9 - chirp_duration: float = 30e-6 - chirps_per_position: int = 32 - freq_min: float = 10e6 - freq_max: float = 30e6 - prf1: float = 1000 - prf2: float = 2000 - max_distance: float = 50000 - - -@dataclass -class GPSData: - latitude: float - longitude: float - altitude: float - pitch: float # Pitch angle in degrees - timestamp: float - - -class STM32USBInterface: - def __init__(self): - self.device = None - self.is_open = False - self.ep_in = None - self.ep_out = None - - def list_devices(self): - """List available STM32 USB CDC devices""" - if not USB_AVAILABLE: - logging.warning("USB not available - please install pyusb") - return [] - - try: - devices = [] - # STM32 USB CDC devices typically use these vendor/product IDs - stm32_vid_pids = [ - (0x0483, 0x5740), # STM32 Virtual COM Port - (0x0483, 0x3748), # STM32 Discovery - (0x0483, 0x374B), # STM32 CDC - (0x0483, 0x374D), # STM32 CDC - (0x0483, 0x374E), # STM32 CDC - (0x0483, 0x3752), # STM32 CDC - ] - - for vid, pid in stm32_vid_pids: - found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid) - for dev in found_devices: - try: - product = ( - usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC" - ) - serial = ( - usb.util.get_string(dev, dev.iSerialNumber) - if dev.iSerialNumber - else "Unknown" - ) - devices.append( - { - "description": f"{product} ({serial})", - "vendor_id": vid, - "product_id": pid, - "device": dev, - } - ) - except Exception: - devices.append( - { - "description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", - "vendor_id": vid, - "product_id": pid, - "device": dev, - } - ) - - return devices - except Exception as e: - logging.error(f"Error listing USB devices: {e}") - # Return mock devices for testing - return [ - {"description": "STM32 Virtual COM Port", "vendor_id": 0x0483, "product_id": 0x5740} - ] - - def open_device(self, device_info): - """Open STM32 USB CDC device""" - if not USB_AVAILABLE: - logging.error("USB not available - cannot open device") - return False - - try: - self.device = device_info["device"] - - # Detach kernel driver if active - if self.device.is_kernel_driver_active(0): - self.device.detach_kernel_driver(0) - - # Set configuration - self.device.set_configuration() - - # Get CDC endpoints - cfg = self.device.get_active_configuration() - intf = cfg[(0, 0)] - - # Find bulk endpoints (CDC data interface) - self.ep_out = usb.util.find_descriptor( - intf, - custom_match=lambda e: ( - usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT - ), - ) - - self.ep_in = usb.util.find_descriptor( - intf, - 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: - logging.error("Could not find CDC endpoints") - return False - - self.is_open = True - logging.info(f"STM32 USB device opened: {device_info['description']}") - return True - - except Exception as e: - logging.error(f"Error opening USB device: {e}") - return False - - def send_start_flag(self): - """Step 12: Send start flag to STM32 via USB""" - start_packet = bytes([23, 46, 158, 237]) - logging.info("Sending start flag to STM32 via USB...") - return self._send_data(start_packet) - - def send_settings(self, settings): - """Step 13: Send radar settings to STM32 via USB""" - try: - packet = self._create_settings_packet(settings) - logging.info("Sending radar settings to STM32 via USB...") - return self._send_data(packet) - except Exception as e: - logging.error(f"Error sending settings via USB: {e}") - return False - - def read_data(self, size=64, timeout=1000): - """Read data from STM32 via USB""" - if not self.is_open or self.ep_in is None: - return None - - try: - data = self.ep_in.read(size, timeout=timeout) - return bytes(data) - except usb.core.USBError as e: - if e.errno == 110: # Timeout - return None - logging.error(f"USB read error: {e}") - return None - except Exception as e: - logging.error(f"Error reading from USB: {e}") - return None - - def _send_data(self, data): - """Send data to STM32 via USB""" - if not self.is_open or self.ep_out is None: - return False - - try: - # USB CDC typically uses 64-byte packets - packet_size = 64 - for i in range(0, len(data), packet_size): - chunk = data[i : i + packet_size] - # Pad to packet size if needed - if len(chunk) < packet_size: - chunk += b"\x00" * (packet_size - len(chunk)) - self.ep_out.write(chunk) - - return True - except Exception as e: - logging.error(f"Error sending data via USB: {e}") - return False - - def _create_settings_packet(self, settings): - """Create binary settings packet for USB transmission""" - packet = b"SET" - packet += struct.pack(">d", settings.system_frequency) - packet += struct.pack(">d", settings.chirp_duration) - packet += struct.pack(">I", settings.chirps_per_position) - packet += struct.pack(">d", settings.freq_min) - packet += struct.pack(">d", settings.freq_max) - packet += struct.pack(">d", settings.prf1) - packet += struct.pack(">d", settings.prf2) - packet += struct.pack(">d", settings.max_distance) - packet += b"END" - return packet - - def close(self): - """Close USB device""" - if self.device and self.is_open: - try: - usb.util.dispose_resources(self.device) - self.is_open = False - except Exception as e: - logging.error(f"Error closing USB device: {e}") - - -class FTDIInterface: - def __init__(self): - self.ftdi = None - self.is_open = False - - def list_devices(self): - """List available FTDI devices using pyftdi""" - if not FTDI_AVAILABLE: - logging.warning("FTDI not available - please install pyftdi") - return [] - - try: - devices = [] - # Get list of all FTDI devices - for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID - devices.append( - {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} - ) - return devices - except Exception as e: - logging.error(f"Error listing FTDI devices: {e}") - # Return mock devices for testing - return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}] - - def open_device(self, device_url): - """Open FTDI device using pyftdi""" - if not FTDI_AVAILABLE: - logging.error("FTDI not available - cannot open device") - return False - - try: - self.ftdi = Ftdi() - self.ftdi.open_from_url(device_url) - - # Configure for synchronous FIFO mode - self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.SYNCFF) - - # Set latency timer - self.ftdi.set_latency_timer(2) - - # Purge buffers - self.ftdi.purge_buffers() - - self.is_open = True - logging.info(f"FTDI device opened: {device_url}") - return True - - except Exception as e: - logging.error(f"Error opening FTDI device: {e}") - return False - - def read_data(self, bytes_to_read): - """Read data from FTDI""" - if not self.is_open or self.ftdi is None: - return None - - try: - data = self.ftdi.read_data(bytes_to_read) - if data: - return bytes(data) - return None - except Exception as e: - logging.error(f"Error reading from FTDI: {e}") - return None - - def close(self): - """Close FTDI device""" - if self.ftdi and self.is_open: - self.ftdi.close() - self.is_open = False - - -class RadarProcessor: - def __init__(self): - self.range_doppler_map = np.zeros((1024, 32)) - self.detected_targets = [] - self.track_id_counter = 0 - self.tracks = {} - self.frame_count = 0 - - def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): - """Dual-CPI fusion for better detection""" - fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - return fused_profile - - def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): - """Multi-PRF velocity unwrapping""" - lambda_wavelength = 3e8 / 10e9 - v_max1 = prf1 * lambda_wavelength / 2 - v_max2 = prf2 * lambda_wavelength / 2 - - unwrapped_velocities = [] - for doppler in doppler_measurements: - v1 = doppler * lambda_wavelength / 2 - v2 = doppler * lambda_wavelength / 2 - - velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2) - unwrapped_velocities.append(velocity) - - return unwrapped_velocities - - def _solve_chinese_remainder(self, v1, v2, max1, max2): - for k in range(-5, 6): - candidate = v1 + k * max1 - if abs(candidate - v2) < max2 / 2: - return candidate - return v1 - - def clustering(self, detections, eps=100, min_samples=2): - """DBSCAN clustering of detections""" - if len(detections) == 0: - return [] - - points = np.array([[d.range, d.velocity] for d in detections]) - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(points) - - clusters = [] - for label in set(clustering.labels_): - if label != -1: - cluster_points = points[clustering.labels_ == label] - clusters.append( - { - "center": np.mean(cluster_points, axis=0), - "points": cluster_points, - "size": len(cluster_points), - } - ) - - return clusters - - def association(self, detections, clusters): - """Association of detections to tracks""" - associated_detections = [] - - for detection in detections: - best_track = None - min_distance = float("inf") - - for track_id, track in self.tracks.items(): - distance = np.sqrt( - (detection.range - track["state"][0]) ** 2 - + (detection.velocity - track["state"][2]) ** 2 - ) - - if distance < min_distance and distance < 500: - min_distance = distance - best_track = track_id - - if best_track is not None: - detection.track_id = best_track - associated_detections.append(detection) - else: - detection.track_id = self.track_id_counter - self.track_id_counter += 1 - associated_detections.append(detection) - - return associated_detections - - def tracking(self, associated_detections): - """Kalman filter tracking""" - current_time = time.time() - - for detection in associated_detections: - if detection.track_id not in self.tracks: - kf = KalmanFilter(dim_x=4, dim_z=2) - kf.x = np.array([detection.range, 0, detection.velocity, 0]) - kf.F = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]]) - kf.H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]]) - kf.P *= 1000 - kf.R = np.diag([10, 1]) - kf.Q = np.eye(4) * 0.1 - - self.tracks[detection.track_id] = { - "filter": kf, - "state": kf.x, - "last_update": current_time, - "hits": 1, - } - else: - track = self.tracks[detection.track_id] - track["filter"].predict() - track["filter"].update([detection.range, detection.velocity]) - track["state"] = track["filter"].x - track["last_update"] = current_time - track["hits"] += 1 - - stale_tracks = [ - tid for tid, track in self.tracks.items() if current_time - track["last_update"] > 5.0 - ] - for tid in stale_tracks: - del self.tracks[tid] - - -class USBPacketParser: - def __init__(self): - self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - - def parse_gps_data(self, data): - """Parse GPS data from STM32 USB CDC with pitch angle""" - if not data: - return None - - try: - # Try text format first: "GPS:lat,lon,alt,pitch\r\n" - text_data = data.decode("utf-8", errors="ignore").strip() - if text_data.startswith("GPS:"): - parts = text_data.split(":")[1].split(",") - if len(parts) == 4: # Now expecting 4 values - lat = float(parts[0]) - lon = float(parts[1]) - alt = float(parts[2]) - pitch = float(parts[3]) # Pitch angle in degrees - return GPSData( - latitude=lat, - longitude=lon, - altitude=alt, - pitch=pitch, - timestamp=time.time(), - ) - - # Try binary format (30 bytes with pitch) - if len(data) >= 30 and data[0:4] == b"GPSB": - return self._parse_binary_gps_with_pitch(data) - - except Exception as e: - logging.error(f"Error parsing GPS data: {e}") - - return None - - def _parse_binary_gps_with_pitch(self, data): - """Parse binary GPS format with pitch angle (30 bytes)""" - try: - # Binary format: [Header 4][Latitude 8][Longitude 8][Altitude 4][Pitch 4][CRC 2] - if len(data) < 30: - return None - - # Verify CRC (simple checksum) - crc_received = (data[28] << 8) | data[29] - crc_calculated = sum(data[0:28]) & 0xFFFF - - if crc_received != crc_calculated: - logging.warning("GPS CRC mismatch") - return None - - # Parse latitude (double, big-endian) - lat_bits = 0 - for i in range(8): - lat_bits = (lat_bits << 8) | data[4 + i] - latitude = struct.unpack(">d", struct.pack(">Q", lat_bits))[0] - - # Parse longitude (double, big-endian) - lon_bits = 0 - for i in range(8): - lon_bits = (lon_bits << 8) | data[12 + i] - longitude = struct.unpack(">d", struct.pack(">Q", lon_bits))[0] - - # Parse altitude (float, big-endian) - alt_bits = 0 - for i in range(4): - alt_bits = (alt_bits << 8) | data[20 + i] - altitude = struct.unpack(">f", struct.pack(">I", alt_bits))[0] - - # Parse pitch angle (float, big-endian) - pitch_bits = 0 - for i in range(4): - pitch_bits = (pitch_bits << 8) | data[24 + i] - pitch = struct.unpack(">f", struct.pack(">I", pitch_bits))[0] - - return GPSData( - latitude=latitude, - longitude=longitude, - altitude=altitude, - pitch=pitch, - timestamp=time.time(), - ) - - except Exception as e: - logging.error(f"Error parsing binary GPS with pitch: {e}") - return None - - -class RadarPacketParser: - def __init__(self): - self.sync_pattern = b"\xa5\xc3" - self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - - def parse_packet(self, data): - if len(data) < 6: - return None - - sync_index = data.find(self.sync_pattern) - if sync_index == -1: - return None - - packet = data[sync_index:] - - if len(packet) < 6: - return None - - _sync = packet[0:2] - packet_type = packet[2] - length = packet[3] - - if len(packet) < (4 + length + 2): - return None - - payload = packet[4 : 4 + length] - crc_received = struct.unpack("I", payload[0:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp_counter = payload[6] & 0x1F - - return { - "type": "range", - "range": range_value, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing range packet: {e}") - return None - - def parse_doppler_packet(self, payload): - if len(payload) < 12: - return None - - try: - doppler_real = struct.unpack(">h", payload[0:2])[0] - doppler_imag = struct.unpack(">h", payload[2:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp_counter = payload[6] & 0x1F - - return { - "type": "doppler", - "doppler_real": doppler_real, - "doppler_imag": doppler_imag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing Doppler packet: {e}") - return None - - def parse_detection_packet(self, payload): - if len(payload) < 8: - return None - - try: - detection_flag = (payload[0] & 0x01) != 0 - elevation = payload[1] & 0x1F - azimuth = payload[2] & 0x3F - chirp_counter = payload[3] & 0x1F - - return { - "type": "detection", - "detected": detection_flag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing detection packet: {e}") - return None - - -class RadarGUI: - def __init__(self, root): - self.root = root - self.root.title("Advanced Radar System GUI - USB CDC with Pitch Correction") - self.root.geometry("1400x900") - - # Initialize interfaces - self.stm32_usb_interface = STM32USBInterface() - self.ftdi_interface = FTDIInterface() - self.radar_processor = RadarProcessor() - self.usb_packet_parser = USBPacketParser() - self.radar_packet_parser = RadarPacketParser() - self.settings = RadarSettings() - - # Data queues - self.radar_data_queue = queue.Queue() - self.gps_data_queue = queue.Queue() - - # Thread control - self.running = False - self.radar_thread = None - self.gps_thread = None - - # Counters - self.received_packets = 0 - self.current_gps = GPSData( - latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0 - ) - self.corrected_elevations = [] # Store corrected elevation values - - self.create_gui() - self.start_background_threads() - - def create_gui(self): - """Create the main GUI with tabs""" - self.notebook = ttk.Notebook(self.root) - self.notebook.pack(fill="both", expand=True, padx=10, pady=10) - - self.tab_main = ttk.Frame(self.notebook) - self.tab_map = ttk.Frame(self.notebook) - self.tab_diagnostics = ttk.Frame(self.notebook) - self.tab_settings = ttk.Frame(self.notebook) - - self.notebook.add(self.tab_main, text="Main View") - self.notebook.add(self.tab_map, text="Map View") - self.notebook.add(self.tab_diagnostics, text="Diagnostics") - self.notebook.add(self.tab_settings, text="Settings") - - self.setup_main_tab() - self.setup_map_tab() - self.setup_settings_tab() - - def setup_main_tab(self): - """Setup the main radar display tab""" - # Control frame - control_frame = ttk.Frame(self.tab_main) - control_frame.pack(fill="x", padx=10, pady=5) - - # USB Device selection - ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5) - self.stm32_usb_combo = ttk.Combobox(control_frame, state="readonly", width=40) - self.stm32_usb_combo.grid(row=0, column=1, padx=5) - - ttk.Label(control_frame, text="FTDI Device:").grid(row=0, column=2, padx=5) - self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30) - self.ftdi_combo.grid(row=0, column=3, padx=5) - - ttk.Button(control_frame, text="Refresh Devices", command=self.refresh_devices).grid( - row=0, column=4, padx=5 - ) - - self.start_button = ttk.Button(control_frame, text="Start Radar", command=self.start_radar) - self.start_button.grid(row=0, column=5, padx=5) - - self.stop_button = ttk.Button( - control_frame, text="Stop Radar", command=self.stop_radar, state="disabled" - ) - self.stop_button.grid(row=0, column=6, padx=5) - - # GPS and Pitch info - self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...") - self.gps_label.grid(row=1, column=0, columnspan=4, sticky="w", padx=5, pady=2) - - # Pitch display - self.pitch_label = ttk.Label(control_frame, text="Pitch: --.--°") - self.pitch_label.grid(row=1, column=4, columnspan=2, padx=5, pady=2) - - # Status info - self.status_label = ttk.Label(control_frame, text="Status: Ready") - self.status_label.grid(row=1, column=6, sticky="e", padx=5, pady=2) - - # Main display area - display_frame = ttk.Frame(self.tab_main) - display_frame.pack(fill="both", expand=True, padx=10, pady=5) - - # Range-Doppler Map - fig = Figure(figsize=(10, 6)) - self.range_doppler_ax = fig.add_subplot(111) - self.range_doppler_plot = self.range_doppler_ax.imshow( - np.random.rand(1024, 32), aspect="auto", cmap="hot", extent=[0, 32, 0, 1024] - ) - self.range_doppler_ax.set_title("Range-Doppler Map (Pitch Corrected)") - self.range_doppler_ax.set_xlabel("Doppler Bin") - self.range_doppler_ax.set_ylabel("Range Bin") - - self.canvas = FigureCanvasTkAgg(fig, display_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(side="left", fill="both", expand=True) - - # Targets list with corrected elevation - targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)") - targets_frame.pack(side="right", fill="y", padx=5) - - self.targets_tree = ttk.Treeview( - targets_frame, - columns=("ID", "Range", "Velocity", "Azimuth", "Elevation", "Corrected Elev", "SNR"), - show="headings", - height=20, - ) - self.targets_tree.heading("ID", text="Track ID") - self.targets_tree.heading("Range", text="Range (m)") - self.targets_tree.heading("Velocity", text="Velocity (m/s)") - self.targets_tree.heading("Azimuth", text="Azimuth") - self.targets_tree.heading("Elevation", text="Raw Elev") - self.targets_tree.heading("Corrected Elev", text="Corr Elev") - self.targets_tree.heading("SNR", text="SNR (dB)") - - self.targets_tree.column("ID", width=70) - self.targets_tree.column("Range", width=90) - self.targets_tree.column("Velocity", width=90) - self.targets_tree.column("Azimuth", width=70) - self.targets_tree.column("Elevation", width=70) - self.targets_tree.column("Corrected Elev", width=70) - self.targets_tree.column("SNR", width=70) - - self.targets_tree.pack(fill="both", expand=True, padx=5, pady=5) - - def setup_map_tab(self): - """Setup the map display tab""" - self.map_frame = ttk.Frame(self.tab_map) - self.map_frame.pack(fill="both", expand=True, padx=10, pady=10) - - # Map placeholder - self.map_label = ttk.Label( - self.map_frame, - text="Map will be displayed here after GPS data is received", - font=("Arial", 12), - ) - self.map_label.pack(expand=True) - - def setup_settings_tab(self): - """Setup the settings tab""" - settings_frame = ttk.Frame(self.tab_settings) - settings_frame.pack(fill="both", expand=True, padx=10, pady=10) - - entries = [ - ("System Frequency (Hz):", "system_frequency", 10e9), - ("Chirp Duration (s):", "chirp_duration", 30e-6), - ("Chirps per Position:", "chirps_per_position", 32), - ("Frequency Min (Hz):", "freq_min", 10e6), - ("Frequency Max (Hz):", "freq_max", 30e6), - ("PRF1 (Hz):", "prf1", 1000), - ("PRF2 (Hz):", "prf2", 2000), - ("Max Distance (m):", "max_distance", 50000), - ] - - self.settings_vars = {} - - for i, (label, attr, default) in enumerate(entries): - ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky="w", padx=5, pady=5) - var = tk.StringVar(value=str(default)) - entry = ttk.Entry(settings_frame, textvariable=var, width=20) - entry.grid(row=i, column=1, padx=5, pady=5) - self.settings_vars[attr] = var - - ttk.Button(settings_frame, text="Apply Settings", command=self.apply_settings).grid( - row=len(entries), column=0, columnspan=2, pady=10 - ) - - def apply_pitch_correction(self, raw_elevation, pitch_angle): - """ - Apply pitch correction to elevation angle - raw_elevation: measured elevation from radar (degrees) - pitch_angle: antenna pitch angle from IMU (degrees) - Returns: corrected elevation angle (degrees) - """ - # Convert to radians for trigonometric functions - raw_elev_rad = math.radians(raw_elevation) - pitch_rad = math.radians(pitch_angle) - - # Apply pitch correction: corrected_elev = raw_elev - pitch - # This assumes the pitch angle is positive when antenna is tilted up - corrected_elev_rad = raw_elev_rad - pitch_rad - - # Convert back to degrees and ensure it's within valid range - corrected_elev_deg = math.degrees(corrected_elev_rad) - - # Normalize to 0-180 degree range - corrected_elev_deg = corrected_elev_deg % 180 - if corrected_elev_deg < 0: - corrected_elev_deg += 180 - - return corrected_elev_deg - - def refresh_devices(self): - """Refresh available USB devices""" - # STM32 USB devices - stm32_devices = self.stm32_usb_interface.list_devices() - stm32_names = [dev["description"] for dev in stm32_devices] - self.stm32_usb_combo["values"] = stm32_names - - # FTDI devices - ftdi_devices = self.ftdi_interface.list_devices() - ftdi_names = [dev["description"] for dev in ftdi_devices] - self.ftdi_combo["values"] = ftdi_names - - if stm32_names: - self.stm32_usb_combo.current(0) - if ftdi_names: - self.ftdi_combo.current(0) - - def start_radar(self): - """Step 11: Start button pressed - Begin radar operation""" - try: - # Open STM32 USB device - stm32_index = self.stm32_usb_combo.current() - if stm32_index == -1: - messagebox.showerror("Error", "Please select an STM32 USB device") - return - - stm32_devices = self.stm32_usb_interface.list_devices() - if stm32_index >= len(stm32_devices): - messagebox.showerror("Error", "Invalid STM32 device selection") - return - - if not self.stm32_usb_interface.open_device(stm32_devices[stm32_index]): - messagebox.showerror("Error", "Failed to open STM32 USB device") - return - - # Open FTDI device - if FTDI_AVAILABLE: - ftdi_index = self.ftdi_combo.current() - if ftdi_index != -1: - ftdi_devices = self.ftdi_interface.list_devices() - if ftdi_index < len(ftdi_devices): - device_url = ftdi_devices[ftdi_index]["url"] - if not self.ftdi_interface.open_device(device_url): - logging.warning( - "Failed to open FTDI device, continuing without radar data" - ) - else: - logging.warning("No FTDI device selected, continuing without radar data") - else: - logging.warning("FTDI not available, continuing without radar data") - - # Step 12: Send start flag to STM32 via USB - if not self.stm32_usb_interface.send_start_flag(): - messagebox.showerror("Error", "Failed to send start flag to STM32") - return - - # Step 13: Send settings to STM32 via USB - self.apply_settings() - - # Start radar operation - self.running = True - self.start_button.config(state="disabled") - self.stop_button.config(state="normal") - self.status_label.config(text="Status: Radar running - Waiting for GPS data...") - - logging.info("Radar system started successfully via USB CDC") - - except Exception as e: - messagebox.showerror("Error", f"Failed to start radar: {e}") - logging.error(f"Start radar error: {e}") - - def stop_radar(self): - """Stop radar operation""" - self.running = False - self.start_button.config(state="normal") - self.stop_button.config(state="disabled") - self.status_label.config(text="Status: Radar stopped") - - self.stm32_usb_interface.close() - self.ftdi_interface.close() - - logging.info("Radar system stopped") - - def apply_settings(self): - """Step 13: Apply and send radar settings via USB""" - try: - self.settings.system_frequency = float(self.settings_vars["system_frequency"].get()) - self.settings.chirp_duration = float(self.settings_vars["chirp_duration"].get()) - self.settings.chirps_per_position = int(self.settings_vars["chirps_per_position"].get()) - self.settings.freq_min = float(self.settings_vars["freq_min"].get()) - self.settings.freq_max = float(self.settings_vars["freq_max"].get()) - self.settings.prf1 = float(self.settings_vars["prf1"].get()) - self.settings.prf2 = float(self.settings_vars["prf2"].get()) - self.settings.max_distance = float(self.settings_vars["max_distance"].get()) - - if self.stm32_usb_interface.is_open: - self.stm32_usb_interface.send_settings(self.settings) - - messagebox.showinfo("Success", "Settings applied and sent to STM32 via USB") - logging.info("Radar settings applied via USB") - - except ValueError as e: - messagebox.showerror("Error", f"Invalid setting value: {e}") - - def start_background_threads(self): - """Start background data processing threads""" - self.radar_thread = threading.Thread(target=self.process_radar_data, daemon=True) - self.radar_thread.start() - - self.gps_thread = threading.Thread(target=self.process_gps_data, daemon=True) - self.gps_thread.start() - - self.root.after(100, self.update_gui) - - def process_radar_data(self): - """Step 39: Process incoming radar data from FTDI""" - buffer = b"" - while True: - if self.running and self.ftdi_interface.is_open: - try: - data = self.ftdi_interface.read_data(4096) - if data: - buffer += data - - while len(buffer) >= 6: - packet = self.radar_packet_parser.parse_packet(buffer) - if packet: - self.process_radar_packet(packet) - packet_length = 4 + len(packet.get("payload", b"")) + 2 - buffer = buffer[packet_length:] - self.received_packets += 1 - else: - break - - except Exception as e: - logging.error(f"Error processing radar data: {e}") - time.sleep(0.1) - else: - time.sleep(0.1) - - def process_gps_data(self): - """Step 16/17: Process GPS data from STM32 via USB CDC""" - while True: - if self.running and self.stm32_usb_interface.is_open: - try: - # Read data from STM32 USB - data = self.stm32_usb_interface.read_data(64, timeout=100) - if data: - gps_data = self.usb_packet_parser.parse_gps_data(data) - if gps_data: - self.gps_data_queue.put(gps_data) - logging.info( - "GPS Data received via USB: " - f"Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m, " - f"Pitch {gps_data.pitch:.1f}°" - ) - except Exception as e: - logging.error(f"Error processing GPS data via USB: {e}") - time.sleep(0.1) - - def process_radar_packet(self, packet): - """Step 40: Process radar data and apply pitch correction""" - try: - if packet["type"] == "range": - range_meters = packet["range"] * 0.1 - - # Apply pitch correction to elevation - raw_elevation = packet["elevation"] - corrected_elevation = self.apply_pitch_correction( - raw_elevation, self.current_gps.pitch - ) - - # Store correction for display - self.corrected_elevations.append( - { - "raw": raw_elevation, - "corrected": corrected_elevation, - "pitch": self.current_gps.pitch, - "timestamp": packet["timestamp"], - } - ) - - # Keep only recent corrections - if len(self.corrected_elevations) > 100: - self.corrected_elevations = self.corrected_elevations[-100:] - - target = RadarTarget( - id=packet["chirp"], - range=range_meters, - velocity=0, - azimuth=packet["azimuth"], - elevation=corrected_elevation, # Use corrected elevation - snr=20.0, - timestamp=packet["timestamp"], - ) - - self.update_range_doppler_map(target) - - elif packet["type"] == "doppler": - lambda_wavelength = 3e8 / self.settings.system_frequency - velocity = (packet["doppler_real"] / 32767.0) * ( - self.settings.prf1 * lambda_wavelength / 2 - ) - self.update_target_velocity(packet, velocity) - - elif packet["type"] == "detection": - if packet["detected"]: - # Apply pitch correction to detection elevation - raw_elevation = packet["elevation"] - corrected_elevation = self.apply_pitch_correction( - raw_elevation, self.current_gps.pitch - ) - - logging.info( - f"CFAR Detection: Raw Elev {raw_elevation}°, " - f"Corrected Elev {corrected_elevation:.1f}°, " - f"Pitch {self.current_gps.pitch:.1f}°" - ) - - except Exception as e: - logging.error(f"Error processing radar packet: {e}") - - def update_range_doppler_map(self, target): - """Update range-Doppler map with new target""" - range_bin = min(int(target.range / 50), 1023) - doppler_bin = min(abs(int(target.velocity)), 31) - - self.radar_processor.range_doppler_map[range_bin, doppler_bin] += 1 - - self.radar_processor.detected_targets.append(target) - - if len(self.radar_processor.detected_targets) > 100: - self.radar_processor.detected_targets = self.radar_processor.detected_targets[-100:] - - def update_target_velocity(self, packet, velocity): - """Update target velocity information""" - for target in self.radar_processor.detected_targets: - if ( - target.azimuth == packet["azimuth"] - and target.elevation == packet["elevation"] - and target.id == packet["chirp"] - ): - target.velocity = velocity - break - - def update_gps_display(self): - """Step 18: Update GPS and pitch display""" - try: - while not self.gps_data_queue.empty(): - gps_data = self.gps_data_queue.get_nowait() - self.current_gps = gps_data - - # Update GPS label - self.gps_label.config( - text=( - f"GPS: Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m" - ) - ) - - # Update pitch label with color coding - pitch_text = f"Pitch: {gps_data.pitch:+.1f}°" - self.pitch_label.config(text=pitch_text) - - # Color code based on pitch magnitude - if abs(gps_data.pitch) > 10: - self.pitch_label.config(foreground="red") # High pitch warning - elif abs(gps_data.pitch) > 5: - self.pitch_label.config(foreground="orange") # Medium pitch - else: - self.pitch_label.config(foreground="green") # Normal pitch - - # Update map - self.update_map_display(gps_data) - - except queue.Empty: - pass - - def update_targets_list(self): - """Update the targets list display with corrected elevations""" - for item in self.targets_tree.get_children(): - self.targets_tree.delete(item) - - for target in self.radar_processor.detected_targets[-20:]: - # Find the corresponding raw elevation if available - raw_elevation = "N/A" - for correction in self.corrected_elevations[-20:]: - if abs(correction["corrected"] - target.elevation) < 0.1: # Fuzzy match - raw_elevation = f"{correction['raw']}" - break - - self.targets_tree.insert( - "", - "end", - values=( - target.track_id, - f"{target.range:.1f}", - f"{target.velocity:.1f}", - target.azimuth, - raw_elevation, # Show raw elevation - f"{target.elevation:.1f}", # Show corrected elevation - f"{target.snr:.1f}", - ), - ) - - def update_gui(self): - """Step 40: Update all GUI displays""" - try: - # Update status with pitch information - if self.running: - self.status_label.config( - text=( - f"Status: Running - Packets: {self.received_packets} - " - f"Pitch: {self.current_gps.pitch:+.1f}°" - ) - ) - - # Update range-Doppler map - if hasattr(self, "range_doppler_plot"): - display_data = np.log10(self.radar_processor.range_doppler_map + 1) - self.range_doppler_plot.set_array(display_data) - self.canvas.draw_idle() - - # Update targets list - self.update_targets_list() - - # Update GPS and pitch display - self.update_gps_display() - - except Exception as e: - logging.error(f"Error updating GUI: {e}") - - self.root.after(100, self.update_gui) - - def update_map_display(self, gps_data): - """Step 18: Update map display with current GPS position""" - try: - self.map_label.config( - text=f"Radar Position: {gps_data.latitude:.6f}, {gps_data.longitude:.6f}\n" - f"Altitude: {gps_data.altitude:.1f}m\n" - f"Pitch: {gps_data.pitch:+.1f}°\n" - f"Coverage: 50km radius\n" - f"Map centered on GPS coordinates" - ) - - except Exception as e: - logging.error(f"Error updating map display: {e}") - - -def main(): - """Main application entry point""" - try: - root = tk.Tk() - _app = RadarGUI(root) - root.mainloop() - except Exception as e: - logging.error(f"Application error: {e}") - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") - - -if __name__ == "__main__": - main() diff --git a/9_Firmware/9_3_GUI/GUI_V4.py b/9_Firmware/9_3_GUI/GUI_V4.py deleted file mode 100644 index 3df41c3..0000000 --- a/9_Firmware/9_3_GUI/GUI_V4.py +++ /dev/null @@ -1,1513 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox -import threading -import queue -import time -import struct -import numpy as np -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.figure import Figure -import logging -from dataclasses import dataclass -from sklearn.cluster import DBSCAN -from filterpy.kalman import KalmanFilter -import crcmod -import math -import webbrowser -import tempfile -import os - -try: - import usb.core - import usb.util - - USB_AVAILABLE = True -except ImportError: - USB_AVAILABLE = False - logging.warning("pyusb not available. USB CDC functionality will be disabled.") - -try: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools - - FTDI_AVAILABLE = True -except ImportError: - FTDI_AVAILABLE = False - logging.warning("pyftdi not available. FTDI functionality will be disabled.") - -# Configure logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - - -@dataclass -class RadarTarget: - id: int - range: float - velocity: float - azimuth: int - elevation: int - latitude: float = 0.0 - longitude: float = 0.0 - snr: float = 0.0 - timestamp: float = 0.0 - track_id: int = -1 - - -@dataclass -class RadarSettings: - system_frequency: float = 10e9 - chirp_duration_1: float = 30e-6 # Long chirp duration - chirp_duration_2: float = 0.5e-6 # Short chirp duration - chirps_per_position: int = 32 - freq_min: float = 10e6 - freq_max: float = 30e6 - prf1: float = 1000 - prf2: float = 2000 - max_distance: float = 50000 - map_size: float = 50000 # Map size in meters - - -@dataclass -class GPSData: - latitude: float - longitude: float - altitude: float - pitch: float # Pitch angle in degrees - timestamp: float - - -class MapGenerator: - def __init__(self): - self.map_html_template = """ - - - - Radar Map - - - - - -
- - - - - - - """ - - def generate_map(self, gps_data, targets, coverage_radius, api_key="YOUR_GOOGLE_MAPS_API_KEY"): - """Generate HTML map with radar and targets""" - # Convert targets to map coordinates - map_targets = [] - for target in targets: - # Convert polar coordinates (range, azimuth) to geographic coordinates - target_lat, target_lon = self.polar_to_geographic( - gps_data.latitude, gps_data.longitude, target.range, target.azimuth - ) - map_targets.append( - { - "id": target.track_id, - "lat": target_lat, - "lng": target_lon, - "range": target.range, - "velocity": target.velocity, - "azimuth": target.azimuth, - "elevation": target.elevation, - "snr": target.snr, - } - ) - - # Generate targets script - targets_script = "" - if map_targets: - targets_json = str(map_targets).replace("'", '"') - targets_script = f"updateTargets({targets_json});" - - # Fill template - map_html = self.map_html_template.format( - lat=gps_data.latitude, - lon=gps_data.longitude, - alt=gps_data.altitude, - pitch=gps_data.pitch, - coverage_radius=coverage_radius, - targets_script=targets_script, - api_key=api_key, - ) - - return map_html - - def polar_to_geographic(self, radar_lat, radar_lon, range_m, azimuth_deg): - """ - Convert polar coordinates (range, azimuth) to geographic coordinates - using simple flat-earth approximation (good for small distances) - """ - # Earth radius in meters - earth_radius = 6371000 - - # Convert azimuth to radians (0° = North, 90° = East) - azimuth_rad = math.radians(90 - azimuth_deg) # Convert to math convention - - # Convert range to angular distance - angular_distance = range_m / earth_radius - - # Convert to geographic coordinates - target_lat = radar_lat + math.cos(azimuth_rad) * angular_distance * (180 / math.pi) - target_lon = radar_lon + math.sin(azimuth_rad) * angular_distance * ( - 180 / math.pi - ) / math.cos(math.radians(radar_lat)) - - return target_lat, target_lon - - -class STM32USBInterface: - def __init__(self): - self.device = None - self.is_open = False - self.ep_in = None - self.ep_out = None - - def list_devices(self): - """List available STM32 USB CDC devices""" - if not USB_AVAILABLE: - logging.warning("USB not available - please install pyusb") - return [] - - try: - devices = [] - # STM32 USB CDC devices typically use these vendor/product IDs - stm32_vid_pids = [ - (0x0483, 0x5740), # STM32 Virtual COM Port - (0x0483, 0x3748), # STM32 Discovery - (0x0483, 0x374B), # STM32 CDC - (0x0483, 0x374D), # STM32 CDC - (0x0483, 0x374E), # STM32 CDC - (0x0483, 0x3752), # STM32 CDC - ] - - for vid, pid in stm32_vid_pids: - found_devices = usb.core.find(find_all=True, idVendor=vid, idProduct=pid) - for dev in found_devices: - try: - product = ( - usb.util.get_string(dev, dev.iProduct) if dev.iProduct else "STM32 CDC" - ) - serial = ( - usb.util.get_string(dev, dev.iSerialNumber) - if dev.iSerialNumber - else "Unknown" - ) - devices.append( - { - "description": f"{product} ({serial})", - "vendor_id": vid, - "product_id": pid, - "device": dev, - } - ) - except Exception: - devices.append( - { - "description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", - "vendor_id": vid, - "product_id": pid, - "device": dev, - } - ) - - return devices - except Exception as e: - logging.error(f"Error listing USB devices: {e}") - # Return mock devices for testing - return [ - {"description": "STM32 Virtual COM Port", "vendor_id": 0x0483, "product_id": 0x5740} - ] - - def open_device(self, device_info): - """Open STM32 USB CDC device""" - if not USB_AVAILABLE: - logging.error("USB not available - cannot open device") - return False - - try: - self.device = device_info["device"] - - # Detach kernel driver if active - if self.device.is_kernel_driver_active(0): - self.device.detach_kernel_driver(0) - - # Set configuration - self.device.set_configuration() - - # Get CDC endpoints - cfg = self.device.get_active_configuration() - intf = cfg[(0, 0)] - - # Find bulk endpoints (CDC data interface) - self.ep_out = usb.util.find_descriptor( - intf, - custom_match=lambda e: ( - usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT - ), - ) - - self.ep_in = usb.util.find_descriptor( - intf, - 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: - logging.error("Could not find CDC endpoints") - return False - - self.is_open = True - logging.info(f"STM32 USB device opened: {device_info['description']}") - return True - - except Exception as e: - logging.error(f"Error opening USB device: {e}") - return False - - def send_start_flag(self): - """Step 12: Send start flag to STM32 via USB""" - start_packet = bytes([23, 46, 158, 237]) - logging.info("Sending start flag to STM32 via USB...") - return self._send_data(start_packet) - - def send_settings(self, settings): - """Step 13: Send radar settings to STM32 via USB""" - try: - packet = self._create_settings_packet(settings) - logging.info("Sending radar settings to STM32 via USB...") - return self._send_data(packet) - except Exception as e: - logging.error(f"Error sending settings via USB: {e}") - return False - - def read_data(self, size=64, timeout=1000): - """Read data from STM32 via USB""" - if not self.is_open or self.ep_in is None: - return None - - try: - data = self.ep_in.read(size, timeout=timeout) - return bytes(data) - except usb.core.USBError as e: - if e.errno == 110: # Timeout - return None - logging.error(f"USB read error: {e}") - return None - except Exception as e: - logging.error(f"Error reading from USB: {e}") - return None - - def _send_data(self, data): - """Send data to STM32 via USB""" - if not self.is_open or self.ep_out is None: - return False - - try: - # USB CDC typically uses 64-byte packets - packet_size = 64 - for i in range(0, len(data), packet_size): - chunk = data[i : i + packet_size] - # Pad to packet size if needed - if len(chunk) < packet_size: - chunk += b"\x00" * (packet_size - len(chunk)) - self.ep_out.write(chunk) - - return True - except Exception as e: - logging.error(f"Error sending data via USB: {e}") - return False - - def _create_settings_packet(self, settings): - """Create binary settings packet for USB transmission""" - packet = b"SET" - packet += struct.pack(">d", settings.system_frequency) - packet += struct.pack(">d", settings.chirp_duration_1) - packet += struct.pack(">d", settings.chirp_duration_2) - packet += struct.pack(">I", settings.chirps_per_position) - packet += struct.pack(">d", settings.freq_min) - packet += struct.pack(">d", settings.freq_max) - packet += struct.pack(">d", settings.prf1) - packet += struct.pack(">d", settings.prf2) - packet += struct.pack(">d", settings.max_distance) - packet += struct.pack(">d", settings.map_size) - packet += b"END" - return packet - - def close(self): - """Close USB device""" - if self.device and self.is_open: - try: - usb.util.dispose_resources(self.device) - self.is_open = False - except Exception as e: - logging.error(f"Error closing USB device: {e}") - - -class FTDIInterface: - def __init__(self): - self.ftdi = None - self.is_open = False - - def list_devices(self): - """List available FTDI devices using pyftdi""" - if not FTDI_AVAILABLE: - logging.warning("FTDI not available - please install pyftdi") - return [] - - try: - devices = [] - # Get list of all FTDI devices - for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID - devices.append( - {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} - ) - return devices - except Exception as e: - logging.error(f"Error listing FTDI devices: {e}") - # Return mock devices for testing - return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}] - - def open_device(self, device_url): - """Open FTDI device using pyftdi""" - if not FTDI_AVAILABLE: - logging.error("FTDI not available - cannot open device") - return False - - try: - self.ftdi = Ftdi() - self.ftdi.open_from_url(device_url) - - # Configure for synchronous FIFO mode - self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.SYNCFF) - - # Set latency timer - self.ftdi.set_latency_timer(2) - - # Purge buffers - self.ftdi.purge_buffers() - - self.is_open = True - logging.info(f"FTDI device opened: {device_url}") - return True - - except Exception as e: - logging.error(f"Error opening FTDI device: {e}") - return False - - def read_data(self, bytes_to_read): - """Read data from FTDI""" - if not self.is_open or self.ftdi is None: - return None - - try: - data = self.ftdi.read_data(bytes_to_read) - if data: - return bytes(data) - return None - except Exception as e: - logging.error(f"Error reading from FTDI: {e}") - return None - - def close(self): - """Close FTDI device""" - if self.ftdi and self.is_open: - self.ftdi.close() - self.is_open = False - - -class RadarProcessor: - def __init__(self): - self.range_doppler_map = np.zeros((1024, 32)) - self.detected_targets = [] - self.track_id_counter = 0 - self.tracks = {} - self.frame_count = 0 - - def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): - """Dual-CPI fusion for better detection""" - fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - return fused_profile - - def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): - """Multi-PRF velocity unwrapping""" - lambda_wavelength = 3e8 / 10e9 - v_max1 = prf1 * lambda_wavelength / 2 - v_max2 = prf2 * lambda_wavelength / 2 - - unwrapped_velocities = [] - for doppler in doppler_measurements: - v1 = doppler * lambda_wavelength / 2 - v2 = doppler * lambda_wavelength / 2 - - velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2) - unwrapped_velocities.append(velocity) - - return unwrapped_velocities - - def _solve_chinese_remainder(self, v1, v2, max1, max2): - for k in range(-5, 6): - candidate = v1 + k * max1 - if abs(candidate - v2) < max2 / 2: - return candidate - return v1 - - def clustering(self, detections, eps=100, min_samples=2): - """DBSCAN clustering of detections""" - if len(detections) == 0: - return [] - - points = np.array([[d.range, d.velocity] for d in detections]) - clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(points) - - clusters = [] - for label in set(clustering.labels_): - if label != -1: - cluster_points = points[clustering.labels_ == label] - clusters.append( - { - "center": np.mean(cluster_points, axis=0), - "points": cluster_points, - "size": len(cluster_points), - } - ) - - return clusters - - def association(self, detections, clusters): - """Association of detections to tracks""" - associated_detections = [] - - for detection in detections: - best_track = None - min_distance = float("inf") - - for track_id, track in self.tracks.items(): - distance = np.sqrt( - (detection.range - track["state"][0]) ** 2 - + (detection.velocity - track["state"][2]) ** 2 - ) - - if distance < min_distance and distance < 500: - min_distance = distance - best_track = track_id - - if best_track is not None: - detection.track_id = best_track - associated_detections.append(detection) - else: - detection.track_id = self.track_id_counter - self.track_id_counter += 1 - associated_detections.append(detection) - - return associated_detections - - def tracking(self, associated_detections): - """Kalman filter tracking""" - current_time = time.time() - - for detection in associated_detections: - if detection.track_id not in self.tracks: - kf = KalmanFilter(dim_x=4, dim_z=2) - kf.x = np.array([detection.range, 0, detection.velocity, 0]) - kf.F = np.array([[1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 1, 1], [0, 0, 0, 1]]) - kf.H = np.array([[1, 0, 0, 0], [0, 0, 1, 0]]) - kf.P *= 1000 - kf.R = np.diag([10, 1]) - kf.Q = np.eye(4) * 0.1 - - self.tracks[detection.track_id] = { - "filter": kf, - "state": kf.x, - "last_update": current_time, - "hits": 1, - } - else: - track = self.tracks[detection.track_id] - track["filter"].predict() - track["filter"].update([detection.range, detection.velocity]) - track["state"] = track["filter"].x - track["last_update"] = current_time - track["hits"] += 1 - - stale_tracks = [ - tid for tid, track in self.tracks.items() if current_time - track["last_update"] > 5.0 - ] - for tid in stale_tracks: - del self.tracks[tid] - - -class USBPacketParser: - def __init__(self): - self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - - def parse_gps_data(self, data): - """Parse GPS data from STM32 USB CDC with pitch angle""" - if not data: - return None - - try: - # Try text format first: "GPS:lat,lon,alt,pitch\r\n" - text_data = data.decode("utf-8", errors="ignore").strip() - if text_data.startswith("GPS:"): - parts = text_data.split(":")[1].split(",") - if len(parts) == 4: # Now expecting 4 values - lat = float(parts[0]) - lon = float(parts[1]) - alt = float(parts[2]) - pitch = float(parts[3]) # Pitch angle in degrees - return GPSData( - latitude=lat, - longitude=lon, - altitude=alt, - pitch=pitch, - timestamp=time.time(), - ) - - # Try binary format (30 bytes with pitch) - if len(data) >= 30 and data[0:4] == b"GPSB": - return self._parse_binary_gps_with_pitch(data) - - except Exception as e: - logging.error(f"Error parsing GPS data: {e}") - - return None - - def _parse_binary_gps_with_pitch(self, data): - """Parse binary GPS format with pitch angle (30 bytes)""" - try: - # Binary format: [Header 4][Latitude 8][Longitude 8][Altitude 4][Pitch 4][CRC 2] - if len(data) < 30: - return None - - # Verify CRC (simple checksum) - crc_received = (data[28] << 8) | data[29] - crc_calculated = sum(data[0:28]) & 0xFFFF - - if crc_received != crc_calculated: - logging.warning("GPS CRC mismatch") - return None - - # Parse latitude (double, big-endian) - lat_bits = 0 - for i in range(8): - lat_bits = (lat_bits << 8) | data[4 + i] - latitude = struct.unpack(">d", struct.pack(">Q", lat_bits))[0] - - # Parse longitude (double, big-endian) - lon_bits = 0 - for i in range(8): - lon_bits = (lon_bits << 8) | data[12 + i] - longitude = struct.unpack(">d", struct.pack(">Q", lon_bits))[0] - - # Parse altitude (float, big-endian) - alt_bits = 0 - for i in range(4): - alt_bits = (alt_bits << 8) | data[20 + i] - altitude = struct.unpack(">f", struct.pack(">I", alt_bits))[0] - - # Parse pitch angle (float, big-endian) - pitch_bits = 0 - for i in range(4): - pitch_bits = (pitch_bits << 8) | data[24 + i] - pitch = struct.unpack(">f", struct.pack(">I", pitch_bits))[0] - - return GPSData( - latitude=latitude, - longitude=longitude, - altitude=altitude, - pitch=pitch, - timestamp=time.time(), - ) - - except Exception as e: - logging.error(f"Error parsing binary GPS with pitch: {e}") - return None - - -class RadarPacketParser: - def __init__(self): - self.sync_pattern = b"\xa5\xc3" - self.crc16_func = crcmod.mkCrcFun(0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000) - - def parse_packet(self, data): - if len(data) < 6: - return None - - sync_index = data.find(self.sync_pattern) - if sync_index == -1: - return None - - packet = data[sync_index:] - - if len(packet) < 6: - return None - - _sync = packet[0:2] - packet_type = packet[2] - length = packet[3] - - if len(packet) < (4 + length + 2): - return None - - payload = packet[4 : 4 + length] - crc_received = struct.unpack("I", payload[0:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp_counter = payload[6] & 0x1F - - return { - "type": "range", - "range": range_value, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing range packet: {e}") - return None - - def parse_doppler_packet(self, payload): - if len(payload) < 12: - return None - - try: - doppler_real = struct.unpack(">h", payload[0:2])[0] - doppler_imag = struct.unpack(">h", payload[2:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp_counter = payload[6] & 0x1F - - return { - "type": "doppler", - "doppler_real": doppler_real, - "doppler_imag": doppler_imag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing Doppler packet: {e}") - return None - - def parse_detection_packet(self, payload): - if len(payload) < 8: - return None - - try: - detection_flag = (payload[0] & 0x01) != 0 - elevation = payload[1] & 0x1F - azimuth = payload[2] & 0x3F - chirp_counter = payload[3] & 0x1F - - return { - "type": "detection", - "detected": detection_flag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp_counter, - "timestamp": time.time(), - } - except Exception as e: - logging.error(f"Error parsing detection packet: {e}") - return None - - -class RadarGUI: - def __init__(self, root): - self.root = root - self.root.title("Advanced Radar System GUI - USB CDC with Google Maps") - self.root.geometry("1400x900") - - # Initialize interfaces - self.stm32_usb_interface = STM32USBInterface() - self.ftdi_interface = FTDIInterface() - self.radar_processor = RadarProcessor() - self.usb_packet_parser = USBPacketParser() - self.radar_packet_parser = RadarPacketParser() - self.map_generator = MapGenerator() - self.settings = RadarSettings() - - # Data queues - self.radar_data_queue = queue.Queue() - self.gps_data_queue = queue.Queue() - - # Thread control - self.running = False - self.radar_thread = None - self.gps_thread = None - - # Counters - self.received_packets = 0 - self.current_gps = GPSData( - latitude=41.9028, longitude=12.4964, altitude=0, pitch=0.0, timestamp=0 - ) - self.corrected_elevations = [] # Store corrected elevation values - self.map_file_path = None - self.google_maps_api_key = "YOUR_GOOGLE_MAPS_API_KEY" # Replace with your API key - - self.create_gui() - self.start_background_threads() - - def create_gui(self): - """Create the main GUI with tabs""" - self.notebook = ttk.Notebook(self.root) - self.notebook.pack(fill="both", expand=True, padx=10, pady=10) - - self.tab_main = ttk.Frame(self.notebook) - self.tab_map = ttk.Frame(self.notebook) - self.tab_diagnostics = ttk.Frame(self.notebook) - self.tab_settings = ttk.Frame(self.notebook) - - self.notebook.add(self.tab_main, text="Main View") - self.notebook.add(self.tab_map, text="Map View") - self.notebook.add(self.tab_diagnostics, text="Diagnostics") - self.notebook.add(self.tab_settings, text="Settings") - - self.setup_main_tab() - self.setup_map_tab() - self.setup_settings_tab() - - def setup_main_tab(self): - """Setup the main radar display tab""" - # Control frame - control_frame = ttk.Frame(self.tab_main) - control_frame.pack(fill="x", padx=10, pady=5) - - # USB Device selection - ttk.Label(control_frame, text="STM32 USB Device:").grid(row=0, column=0, padx=5) - self.stm32_usb_combo = ttk.Combobox(control_frame, state="readonly", width=40) - self.stm32_usb_combo.grid(row=0, column=1, padx=5) - - ttk.Label(control_frame, text="FTDI Device:").grid(row=0, column=2, padx=5) - self.ftdi_combo = ttk.Combobox(control_frame, state="readonly", width=30) - self.ftdi_combo.grid(row=0, column=3, padx=5) - - ttk.Button(control_frame, text="Refresh Devices", command=self.refresh_devices).grid( - row=0, column=4, padx=5 - ) - - self.start_button = ttk.Button(control_frame, text="Start Radar", command=self.start_radar) - self.start_button.grid(row=0, column=5, padx=5) - - self.stop_button = ttk.Button( - control_frame, text="Stop Radar", command=self.stop_radar, state="disabled" - ) - self.stop_button.grid(row=0, column=6, padx=5) - - # GPS and Pitch info - self.gps_label = ttk.Label(control_frame, text="GPS: Waiting for data...") - self.gps_label.grid(row=1, column=0, columnspan=4, sticky="w", padx=5, pady=2) - - # Pitch display - self.pitch_label = ttk.Label(control_frame, text="Pitch: --.--°") - self.pitch_label.grid(row=1, column=4, columnspan=2, padx=5, pady=2) - - # Status info - self.status_label = ttk.Label(control_frame, text="Status: Ready") - self.status_label.grid(row=1, column=6, sticky="e", padx=5, pady=2) - - # Main display area - display_frame = ttk.Frame(self.tab_main) - display_frame.pack(fill="both", expand=True, padx=10, pady=5) - - # Range-Doppler Map - fig = Figure(figsize=(10, 6)) - self.range_doppler_ax = fig.add_subplot(111) - self.range_doppler_plot = self.range_doppler_ax.imshow( - np.random.rand(1024, 32), aspect="auto", cmap="hot", extent=[0, 32, 0, 1024] - ) - self.range_doppler_ax.set_title("Range-Doppler Map (Pitch Corrected)") - self.range_doppler_ax.set_xlabel("Doppler Bin") - self.range_doppler_ax.set_ylabel("Range Bin") - - self.canvas = FigureCanvasTkAgg(fig, display_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(side="left", fill="both", expand=True) - - # Targets list with corrected elevation - targets_frame = ttk.LabelFrame(display_frame, text="Detected Targets (Pitch Corrected)") - targets_frame.pack(side="right", fill="y", padx=5) - - self.targets_tree = ttk.Treeview( - targets_frame, - columns=("ID", "Range", "Velocity", "Azimuth", "Elevation", "Corrected Elev", "SNR"), - show="headings", - height=20, - ) - self.targets_tree.heading("ID", text="Track ID") - self.targets_tree.heading("Range", text="Range (m)") - self.targets_tree.heading("Velocity", text="Velocity (m/s)") - self.targets_tree.heading("Azimuth", text="Azimuth") - self.targets_tree.heading("Elevation", text="Raw Elev") - self.targets_tree.heading("Corrected Elev", text="Corr Elev") - self.targets_tree.heading("SNR", text="SNR (dB)") - - self.targets_tree.column("ID", width=70) - self.targets_tree.column("Range", width=90) - self.targets_tree.column("Velocity", width=90) - self.targets_tree.column("Azimuth", width=70) - self.targets_tree.column("Elevation", width=70) - self.targets_tree.column("Corrected Elev", width=70) - self.targets_tree.column("SNR", width=70) - - self.targets_tree.pack(fill="both", expand=True, padx=5, pady=5) - - def setup_map_tab(self): - """Setup the map display tab with Google Maps""" - map_frame = ttk.Frame(self.tab_map) - map_frame.pack(fill="both", expand=True, padx=10, pady=10) - - # Map controls - controls_frame = ttk.Frame(map_frame) - controls_frame.pack(fill="x", pady=5) - - ttk.Button( - controls_frame, text="Open Map in Browser", command=self.open_map_in_browser - ).pack(side="left", padx=5) - - ttk.Button(controls_frame, text="Refresh Map", command=self.refresh_map).pack( - side="left", padx=5 - ) - - self.map_status_label = ttk.Label(controls_frame, text="Map: Ready to generate") - self.map_status_label.pack(side="left", padx=20) - - # Map info display - info_frame = ttk.Frame(map_frame) - info_frame.pack(fill="x", pady=5) - - self.map_info_label = ttk.Label( - info_frame, text="No GPS data received yet", font=("Arial", 10) - ) - self.map_info_label.pack() - - def setup_settings_tab(self): - """Setup the settings tab with additional chirp durations and map size""" - settings_frame = ttk.Frame(self.tab_settings) - settings_frame.pack(fill="both", expand=True, padx=10, pady=10) - - entries = [ - ("System Frequency (Hz):", "system_frequency", 10e9), - ("Chirp Duration 1 - Long (s):", "chirp_duration_1", 30e-6), - ("Chirp Duration 2 - Short (s):", "chirp_duration_2", 0.5e-6), - ("Chirps per Position:", "chirps_per_position", 32), - ("Frequency Min (Hz):", "freq_min", 10e6), - ("Frequency Max (Hz):", "freq_max", 30e6), - ("PRF1 (Hz):", "prf1", 1000), - ("PRF2 (Hz):", "prf2", 2000), - ("Max Distance (m):", "max_distance", 50000), - ("Map Size (m):", "map_size", 50000), - ("Google Maps API Key:", "google_maps_api_key", "YOUR_GOOGLE_MAPS_API_KEY"), - ] - - self.settings_vars = {} - - for i, (label, attr, default) in enumerate(entries): - ttk.Label(settings_frame, text=label).grid(row=i, column=0, sticky="w", padx=5, pady=5) - var = tk.StringVar(value=str(default)) - entry = ttk.Entry(settings_frame, textvariable=var, width=25) - entry.grid(row=i, column=1, padx=5, pady=5) - self.settings_vars[attr] = var - - ttk.Button(settings_frame, text="Apply Settings", command=self.apply_settings).grid( - row=len(entries), column=0, columnspan=2, pady=10 - ) - - def apply_pitch_correction(self, raw_elevation, pitch_angle): - """ - Apply pitch correction to elevation angle - raw_elevation: measured elevation from radar (degrees) - pitch_angle: antenna pitch angle from IMU (degrees) - Returns: corrected elevation angle (degrees) - """ - # Convert to radians for trigonometric functions - raw_elev_rad = math.radians(raw_elevation) - pitch_rad = math.radians(pitch_angle) - - # Apply pitch correction: corrected_elev = raw_elev - pitch - # This assumes the pitch angle is positive when antenna is tilted up - corrected_elev_rad = raw_elev_rad - pitch_rad - - # Convert back to degrees and ensure it's within valid range - corrected_elev_deg = math.degrees(corrected_elev_rad) - - # Normalize to 0-180 degree range - corrected_elev_deg = corrected_elev_deg % 180 - if corrected_elev_deg < 0: - corrected_elev_deg += 180 - - return corrected_elev_deg - - def refresh_devices(self): - """Refresh available USB devices""" - # STM32 USB devices - stm32_devices = self.stm32_usb_interface.list_devices() - stm32_names = [dev["description"] for dev in stm32_devices] - self.stm32_usb_combo["values"] = stm32_names - - # FTDI devices - ftdi_devices = self.ftdi_interface.list_devices() - ftdi_names = [dev["description"] for dev in ftdi_devices] - self.ftdi_combo["values"] = ftdi_names - - if stm32_names: - self.stm32_usb_combo.current(0) - if ftdi_names: - self.ftdi_combo.current(0) - - def start_radar(self): - """Step 11: Start button pressed - Begin radar operation""" - try: - # Open STM32 USB device - stm32_index = self.stm32_usb_combo.current() - if stm32_index == -1: - messagebox.showerror("Error", "Please select an STM32 USB device") - return - - stm32_devices = self.stm32_usb_interface.list_devices() - if stm32_index >= len(stm32_devices): - messagebox.showerror("Error", "Invalid STM32 device selection") - return - - if not self.stm32_usb_interface.open_device(stm32_devices[stm32_index]): - messagebox.showerror("Error", "Failed to open STM32 USB device") - return - - # Open FTDI device - if FTDI_AVAILABLE: - ftdi_index = self.ftdi_combo.current() - if ftdi_index != -1: - ftdi_devices = self.ftdi_interface.list_devices() - if ftdi_index < len(ftdi_devices): - device_url = ftdi_devices[ftdi_index]["url"] - if not self.ftdi_interface.open_device(device_url): - logging.warning( - "Failed to open FTDI device, continuing without radar data" - ) - else: - logging.warning("No FTDI device selected, continuing without radar data") - else: - logging.warning("FTDI not available, continuing without radar data") - - # Step 12: Send start flag to STM32 via USB - if not self.stm32_usb_interface.send_start_flag(): - messagebox.showerror("Error", "Failed to send start flag to STM32") - return - - # Step 13: Send settings to STM32 via USB - self.apply_settings() - - # Start radar operation - self.running = True - self.start_button.config(state="disabled") - self.stop_button.config(state="normal") - self.status_label.config(text="Status: Radar running - Waiting for GPS data...") - - logging.info("Radar system started successfully via USB CDC") - - except Exception as e: - messagebox.showerror("Error", f"Failed to start radar: {e}") - logging.error(f"Start radar error: {e}") - - def stop_radar(self): - """Stop radar operation""" - self.running = False - self.start_button.config(state="normal") - self.stop_button.config(state="disabled") - self.status_label.config(text="Status: Radar stopped") - - self.stm32_usb_interface.close() - self.ftdi_interface.close() - - logging.info("Radar system stopped") - - def apply_settings(self): - """Step 13: Apply and send radar settings via USB""" - try: - self.settings.system_frequency = float(self.settings_vars["system_frequency"].get()) - self.settings.chirp_duration_1 = float(self.settings_vars["chirp_duration_1"].get()) - self.settings.chirp_duration_2 = float(self.settings_vars["chirp_duration_2"].get()) - self.settings.chirps_per_position = int(self.settings_vars["chirps_per_position"].get()) - self.settings.freq_min = float(self.settings_vars["freq_min"].get()) - self.settings.freq_max = float(self.settings_vars["freq_max"].get()) - self.settings.prf1 = float(self.settings_vars["prf1"].get()) - self.settings.prf2 = float(self.settings_vars["prf2"].get()) - self.settings.max_distance = float(self.settings_vars["max_distance"].get()) - self.settings.map_size = float(self.settings_vars["map_size"].get()) - self.google_maps_api_key = self.settings_vars["google_maps_api_key"].get() - - if self.stm32_usb_interface.is_open: - self.stm32_usb_interface.send_settings(self.settings) - - messagebox.showinfo("Success", "Settings applied and sent to STM32 via USB") - logging.info("Radar settings applied via USB") - - except ValueError as e: - messagebox.showerror("Error", f"Invalid setting value: {e}") - - def start_background_threads(self): - """Start background data processing threads""" - self.radar_thread = threading.Thread(target=self.process_radar_data, daemon=True) - self.radar_thread.start() - - self.gps_thread = threading.Thread(target=self.process_gps_data, daemon=True) - self.gps_thread.start() - - self.root.after(100, self.update_gui) - - def process_radar_data(self): - """Step 39: Process incoming radar data from FTDI""" - buffer = b"" - while True: - if self.running and self.ftdi_interface.is_open: - try: - data = self.ftdi_interface.read_data(4096) - if data: - buffer += data - - while len(buffer) >= 6: - packet = self.radar_packet_parser.parse_packet(buffer) - if packet: - self.process_radar_packet(packet) - packet_length = 4 + len(packet.get("payload", b"")) + 2 - buffer = buffer[packet_length:] - self.received_packets += 1 - else: - break - - except Exception as e: - logging.error(f"Error processing radar data: {e}") - time.sleep(0.1) - else: - time.sleep(0.1) - - def process_gps_data(self): - """Step 16/17: Process GPS data from STM32 via USB CDC""" - while True: - if self.running and self.stm32_usb_interface.is_open: - try: - # Read data from STM32 USB - data = self.stm32_usb_interface.read_data(64, timeout=100) - if data: - gps_data = self.usb_packet_parser.parse_gps_data(data) - if gps_data: - self.gps_data_queue.put(gps_data) - logging.info( - "GPS Data received via USB: " - f"Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m, " - f"Pitch {gps_data.pitch:.1f}°" - ) - except Exception as e: - logging.error(f"Error processing GPS data via USB: {e}") - time.sleep(0.1) - - def process_radar_packet(self, packet): - """Step 40: Process radar data and apply pitch correction""" - try: - if packet["type"] == "range": - range_meters = packet["range"] * 0.1 - - # Apply pitch correction to elevation - raw_elevation = packet["elevation"] - corrected_elevation = self.apply_pitch_correction( - raw_elevation, self.current_gps.pitch - ) - - # Store correction for display - self.corrected_elevations.append( - { - "raw": raw_elevation, - "corrected": corrected_elevation, - "pitch": self.current_gps.pitch, - "timestamp": packet["timestamp"], - } - ) - - # Keep only recent corrections - if len(self.corrected_elevations) > 100: - self.corrected_elevations = self.corrected_elevations[-100:] - - target = RadarTarget( - id=packet["chirp"], - range=range_meters, - velocity=0, - azimuth=packet["azimuth"], - elevation=corrected_elevation, # Use corrected elevation - snr=20.0, - timestamp=packet["timestamp"], - ) - - self.update_range_doppler_map(target) - - elif packet["type"] == "doppler": - lambda_wavelength = 3e8 / self.settings.system_frequency - velocity = (packet["doppler_real"] / 32767.0) * ( - self.settings.prf1 * lambda_wavelength / 2 - ) - self.update_target_velocity(packet, velocity) - - elif packet["type"] == "detection": - if packet["detected"]: - # Apply pitch correction to detection elevation - raw_elevation = packet["elevation"] - corrected_elevation = self.apply_pitch_correction( - raw_elevation, self.current_gps.pitch - ) - - logging.info( - f"CFAR Detection: Raw Elev {raw_elevation}°, " - f"Corrected Elev {corrected_elevation:.1f}°, " - f"Pitch {self.current_gps.pitch:.1f}°" - ) - - except Exception as e: - logging.error(f"Error processing radar packet: {e}") - - def update_range_doppler_map(self, target): - """Update range-Doppler map with new target""" - range_bin = min(int(target.range / 50), 1023) - doppler_bin = min(abs(int(target.velocity)), 31) - - self.radar_processor.range_doppler_map[range_bin, doppler_bin] += 1 - - self.radar_processor.detected_targets.append(target) - - if len(self.radar_processor.detected_targets) > 100: - self.radar_processor.detected_targets = self.radar_processor.detected_targets[-100:] - - def update_target_velocity(self, packet, velocity): - """Update target velocity information""" - for target in self.radar_processor.detected_targets: - if ( - target.azimuth == packet["azimuth"] - and target.elevation == packet["elevation"] - and target.id == packet["chirp"] - ): - target.velocity = velocity - break - - def open_map_in_browser(self): - """Open the generated map in the default web browser""" - if self.map_file_path and os.path.exists(self.map_file_path): - webbrowser.open("file://" + os.path.abspath(self.map_file_path)) - else: - messagebox.showwarning( - "Warning", "No map file available. Generate map first by receiving GPS data." - ) - - def refresh_map(self): - """Refresh the map with current data""" - self.generate_map() - - def generate_map(self): - """Generate Google Maps HTML file with current targets""" - if self.current_gps.latitude == 0 and self.current_gps.longitude == 0: - self.map_status_label.config(text="Map: Waiting for GPS data") - return - - try: - # Create temporary HTML file - with tempfile.NamedTemporaryFile( - mode="w", suffix=".html", delete=False, encoding="utf-8" - ) as f: - map_html = self.map_generator.generate_map( - self.current_gps, - self.radar_processor.detected_targets, - self.settings.map_size, - self.google_maps_api_key, - ) - f.write(map_html) - self.map_file_path = f.name - - self.map_status_label.config(text=f"Map: Generated at {self.map_file_path}") - self.map_info_label.config( - text=f"Radar: {self.current_gps.latitude:.6f}, {self.current_gps.longitude:.6f} | " - f"Targets: {len(self.radar_processor.detected_targets)} | " - f"Coverage: {self.settings.map_size / 1000:.1f}km" - ) - logging.info(f"Map generated: {self.map_file_path}") - - except Exception as e: - logging.error(f"Error generating map: {e}") - self.map_status_label.config(text=f"Map: Error - {str(e)}") - - def update_gps_display(self): - """Step 18: Update GPS and pitch display""" - try: - while not self.gps_data_queue.empty(): - gps_data = self.gps_data_queue.get_nowait() - self.current_gps = gps_data - - # Update GPS label - self.gps_label.config( - text=( - f"GPS: Lat {gps_data.latitude:.6f}, " - f"Lon {gps_data.longitude:.6f}, " - f"Alt {gps_data.altitude:.1f}m" - ) - ) - - # Update pitch label with color coding - pitch_text = f"Pitch: {gps_data.pitch:+.1f}°" - self.pitch_label.config(text=pitch_text) - - # Color code based on pitch magnitude - if abs(gps_data.pitch) > 10: - self.pitch_label.config(foreground="red") # High pitch warning - elif abs(gps_data.pitch) > 5: - self.pitch_label.config(foreground="orange") # Medium pitch - else: - self.pitch_label.config(foreground="green") # Normal pitch - - # Generate/update map when new GPS data arrives - self.generate_map() - - except queue.Empty: - pass - - def update_targets_list(self): - """Update the targets list display with corrected elevations""" - for item in self.targets_tree.get_children(): - self.targets_tree.delete(item) - - for target in self.radar_processor.detected_targets[-20:]: - # Find the corresponding raw elevation if available - raw_elevation = "N/A" - for correction in self.corrected_elevations[-20:]: - if abs(correction["corrected"] - target.elevation) < 0.1: # Fuzzy match - raw_elevation = f"{correction['raw']}" - break - - self.targets_tree.insert( - "", - "end", - values=( - target.track_id, - f"{target.range:.1f}", - f"{target.velocity:.1f}", - target.azimuth, - raw_elevation, # Show raw elevation - f"{target.elevation:.1f}", # Show corrected elevation - f"{target.snr:.1f}", - ), - ) - - def update_gui(self): - """Step 40: Update all GUI displays""" - try: - # Update status with pitch information - if self.running: - self.status_label.config( - text=( - f"Status: Running - Packets: {self.received_packets} - " - f"Pitch: {self.current_gps.pitch:+.1f}°" - ) - ) - - # Update range-Doppler map - if hasattr(self, "range_doppler_plot"): - display_data = np.log10(self.radar_processor.range_doppler_map + 1) - self.range_doppler_plot.set_array(display_data) - self.canvas.draw_idle() - - # Update targets list - self.update_targets_list() - - # Update GPS and pitch display - self.update_gps_display() - - except Exception as e: - logging.error(f"Error updating GUI: {e}") - - self.root.after(100, self.update_gui) - - -def main(): - """Main application entry point""" - try: - root = tk.Tk() - _app = RadarGUI(root) - root.mainloop() - except Exception as e: - logging.error(f"Application error: {e}") - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") - - -if __name__ == "__main__": - main() diff --git a/9_Firmware/9_3_GUI/GUI_V4_2_CSV.py b/9_Firmware/9_3_GUI/GUI_V4_2_CSV.py deleted file mode 100644 index c749bbf..0000000 --- a/9_Firmware/9_3_GUI/GUI_V4_2_CSV.py +++ /dev/null @@ -1,715 +0,0 @@ -import tkinter as tk -from tkinter import ttk, filedialog, messagebox -import pandas as pd -import numpy as np -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.figure import Figure -from scipy.fft import fft, fftshift -import logging -from dataclasses import dataclass -from typing import List, Dict, Tuple -import threading -import queue -import time - -# Configure logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") - - -@dataclass -class RadarTarget: - range: float - velocity: float - azimuth: int - elevation: int - snr: float - chirp_type: str - timestamp: float - - -class SignalProcessor: - def __init__(self): - self.range_resolution = 1.0 # meters - self.velocity_resolution = 0.1 # m/s - self.cfar_threshold = 15.0 # dB - - def doppler_fft(self, iq_data: np.ndarray, fs: float = 100e6) -> Tuple[np.ndarray, np.ndarray]: - """ - Perform Doppler FFT on IQ data - Returns Doppler frequencies and spectrum - """ - # Window function for FFT - window = np.hanning(len(iq_data)) - windowed_data = (iq_data["I_value"].values + 1j * iq_data["Q_value"].values) * window - - # Perform FFT - doppler_fft = fft(windowed_data) - doppler_fft = fftshift(doppler_fft) - - # Frequency axis - N = len(iq_data) - freq_axis = np.linspace(-fs / 2, fs / 2, N) - - # Convert to velocity (assuming radar frequency = 10 GHz) - radar_freq = 10e9 - wavelength = 3e8 / radar_freq - velocity_axis = freq_axis * wavelength / 2 - - return velocity_axis, np.abs(doppler_fft) - - def mti_filter(self, iq_data: np.ndarray, filter_type: str = "single_canceler") -> np.ndarray: - """ - Moving Target Indicator filter - Removes stationary clutter with better shape handling - """ - if iq_data is None or len(iq_data) < 2: - return np.array([], dtype=complex) - - try: - # Ensure we're working with complex data - complex_data = iq_data.astype(complex) - - if filter_type == "single_canceler": - # Single delay line canceler - if len(complex_data) < 2: - return np.array([], dtype=complex) - filtered = np.zeros(len(complex_data) - 1, dtype=complex) - for i in range(1, len(complex_data)): - filtered[i - 1] = complex_data[i] - complex_data[i - 1] - return filtered - - elif filter_type == "double_canceler": - # Double delay line canceler - if len(complex_data) < 3: - return np.array([], dtype=complex) - filtered = np.zeros(len(complex_data) - 2, dtype=complex) - for i in range(2, len(complex_data)): - filtered[i - 2] = ( - complex_data[i] - 2 * complex_data[i - 1] + complex_data[i - 2] - ) - return filtered - - else: - return complex_data - except Exception as e: - logging.error(f"MTI filter error: {e}") - return np.array([], dtype=complex) - - def cfar_detection( - self, - range_profile: np.ndarray, - guard_cells: int = 2, - training_cells: int = 10, - threshold_factor: float = 3.0, - ) -> List[Tuple[int, float]]: - detections = [] - N = len(range_profile) - - # Ensure guard_cells and training_cells are integers - guard_cells = int(guard_cells) - training_cells = int(training_cells) - - for i in range(N): - # Convert to integer indices - i_int = int(i) - if i_int < guard_cells + training_cells or i_int >= N - guard_cells - training_cells: - continue - - # Leading window - ensure integer indices - lead_start = i_int - guard_cells - training_cells - lead_end = i_int - guard_cells - lead_cells = range_profile[lead_start:lead_end] - - # Lagging window - ensure integer indices - lag_start = i_int + guard_cells + 1 - lag_end = i_int + guard_cells + training_cells + 1 - lag_cells = range_profile[lag_start:lag_end] - - # Combine training cells - training_cells_combined = np.concatenate([lead_cells, lag_cells]) - - # Calculate noise floor (mean of training cells) - if len(training_cells_combined) > 0: - noise_floor = np.mean(training_cells_combined) - - # Apply threshold - threshold = noise_floor * threshold_factor - - if range_profile[i_int] > threshold: - detections.append( - (i_int, float(range_profile[i_int])) - ) # Ensure float magnitude - - return detections - - def range_fft( - self, iq_data: np.ndarray, fs: float = 100e6, bw: float = 20e6 - ) -> Tuple[np.ndarray, np.ndarray]: - """ - Perform range FFT on IQ data - Returns range profile - """ - # Window function - window = np.hanning(len(iq_data)) - windowed_data = np.abs(iq_data) * window - - # Perform FFT - range_fft = fft(windowed_data) - - # Range calculation - N = len(iq_data) - range_max = (3e8 * N) / (2 * bw) - range_axis = np.linspace(0, range_max, N) - - return range_axis, np.abs(range_fft) - - def process_chirp_sequence(self, df: pd.DataFrame, chirp_type: str = "LONG") -> Dict: - try: - # Filter data by chirp type - chirp_data = df[df["chirp_type"] == chirp_type] - - if len(chirp_data) == 0: - return {} - - # Group by chirp number - chirp_numbers = chirp_data["chirp_number"].unique() - num_chirps = len(chirp_numbers) - - if num_chirps == 0: - return {} - - # Get samples per chirp and ensure consistency - samples_per_chirp_list = [ - len(chirp_data[chirp_data["chirp_number"] == num]) for num in chirp_numbers - ] - - # Use minimum samples to ensure consistent shape - samples_per_chirp = min(samples_per_chirp_list) - - # Create range-Doppler matrix with consistent shape - range_doppler_matrix = np.zeros((samples_per_chirp, num_chirps), dtype=complex) - - for i, chirp_num in enumerate(chirp_numbers): - chirp_samples = chirp_data[chirp_data["chirp_number"] == chirp_num] - # Take only the first samples_per_chirp samples to ensure consistent shape - chirp_samples = chirp_samples.head(samples_per_chirp) - - # Create complex IQ data - iq_data = chirp_samples["I_value"].values + 1j * chirp_samples["Q_value"].values - - # Ensure the shape matches - if len(iq_data) == samples_per_chirp: - range_doppler_matrix[:, i] = iq_data - - # Apply MTI filter along slow-time (chirp-to-chirp) - mti_filtered = np.zeros_like(range_doppler_matrix) - for i in range(samples_per_chirp): - slow_time_data = range_doppler_matrix[i, :] - filtered = self.mti_filter(slow_time_data) - # Ensure filtered data matches expected shape - if len(filtered) == num_chirps: - mti_filtered[i, :] = filtered - else: - # Handle shape mismatch by padding or truncating - if len(filtered) < num_chirps: - padded = np.zeros(num_chirps, dtype=complex) - padded[: len(filtered)] = filtered - mti_filtered[i, :] = padded - else: - mti_filtered[i, :] = filtered[:num_chirps] - - # Perform Doppler FFT along slow-time dimension - doppler_fft_result = np.zeros((samples_per_chirp, num_chirps), dtype=complex) - for i in range(samples_per_chirp): - doppler_fft_result[i, :] = fft(mti_filtered[i, :]) - - return { - "range_doppler_matrix": np.abs(doppler_fft_result), - "chirp_type": chirp_type, - "num_chirps": num_chirps, - "samples_per_chirp": samples_per_chirp, - } - - except Exception as e: - logging.error(f"Error in process_chirp_sequence: {e}") - return {} - - -class RadarGUI: - def __init__(self, root): - self.root = root - self.root.title("Radar Signal Processor - CSV Analysis") - self.root.geometry("1400x900") - - # Initialize processor - self.processor = SignalProcessor() - - # Data storage - self.df = None - self.processed_data = {} - self.detected_targets = [] - - # Create GUI - self.create_gui() - - # Start background processing - self.processing_queue = queue.Queue() - self.processing_thread = threading.Thread(target=self.background_processing, daemon=True) - self.processing_thread.start() - - # Update GUI periodically - self.root.after(100, self.update_gui) - - def create_gui(self): - """Create the main GUI layout""" - # Main frame - main_frame = ttk.Frame(self.root) - main_frame.pack(fill="both", expand=True, padx=10, pady=10) - - # Control panel - control_frame = ttk.LabelFrame(main_frame, text="File Controls") - control_frame.pack(fill="x", pady=5) - - # File selection - ttk.Button(control_frame, text="Load CSV File", command=self.load_csv_file).pack( - side="left", padx=5, pady=5 - ) - - self.file_label = ttk.Label(control_frame, text="No file loaded") - self.file_label.pack(side="left", padx=10, pady=5) - - # Processing controls - ttk.Button(control_frame, text="Process Data", command=self.process_data).pack( - side="left", padx=5, pady=5 - ) - - ttk.Button(control_frame, text="Run CFAR Detection", command=self.run_cfar_detection).pack( - side="left", padx=5, pady=5 - ) - - # Status - self.status_label = ttk.Label(control_frame, text="Status: Ready") - self.status_label.pack(side="right", padx=10, pady=5) - - # Display area - display_frame = ttk.Frame(main_frame) - display_frame.pack(fill="both", expand=True, pady=5) - - # Create matplotlib figures - self.create_plots(display_frame) - - # Targets list - targets_frame = ttk.LabelFrame(main_frame, text="Detected Targets") - targets_frame.pack(fill="x", pady=5) - - self.targets_tree = ttk.Treeview( - targets_frame, - columns=("Range", "Velocity", "Azimuth", "Elevation", "SNR", "Chirp Type"), - show="headings", - height=8, - ) - - self.targets_tree.heading("Range", text="Range (m)") - self.targets_tree.heading("Velocity", text="Velocity (m/s)") - self.targets_tree.heading("Azimuth", text="Azimuth (°)") - self.targets_tree.heading("Elevation", text="Elevation (°)") - self.targets_tree.heading("SNR", text="SNR (dB)") - self.targets_tree.heading("Chirp Type", text="Chirp Type") - - self.targets_tree.column("Range", width=100) - self.targets_tree.column("Velocity", width=100) - self.targets_tree.column("Azimuth", width=80) - self.targets_tree.column("Elevation", width=80) - self.targets_tree.column("SNR", width=80) - self.targets_tree.column("Chirp Type", width=100) - - self.targets_tree.pack(fill="x", padx=5, pady=5) - - def create_plots(self, parent): - """Create matplotlib plots""" - # Create figure with subplots - self.fig = Figure(figsize=(12, 8)) - self.canvas = FigureCanvasTkAgg(self.fig, parent) - self.canvas.get_tk_widget().pack(fill="both", expand=True) - - # Create subplots - self.ax1 = self.fig.add_subplot(221) # Range profile - self.ax2 = self.fig.add_subplot(222) # Doppler spectrum - self.ax3 = self.fig.add_subplot(223) # Range-Doppler map - self.ax4 = self.fig.add_subplot(224) # MTI filtered data - - # Set titles - self.ax1.set_title("Range Profile") - self.ax1.set_xlabel("Range (m)") - self.ax1.set_ylabel("Magnitude") - self.ax1.grid(True) - - self.ax2.set_title("Doppler Spectrum") - self.ax2.set_xlabel("Velocity (m/s)") - self.ax2.set_ylabel("Magnitude") - self.ax2.grid(True) - - self.ax3.set_title("Range-Doppler Map") - self.ax3.set_xlabel("Doppler Bin") - self.ax3.set_ylabel("Range Bin") - - self.ax4.set_title("MTI Filtered Data") - self.ax4.set_xlabel("Sample") - self.ax4.set_ylabel("Magnitude") - self.ax4.grid(True) - - self.fig.tight_layout() - - def load_csv_file(self): - """Load CSV file generated by testbench""" - filename = filedialog.askopenfilename( - title="Select CSV file", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")] - ) - - # Add magnitude and phase calculations after loading CSV - if self.df is not None: - # Calculate magnitude from I/Q values - self.df["magnitude"] = np.sqrt(self.df["I_value"] ** 2 + self.df["Q_value"] ** 2) - - # Calculate phase from I/Q values - self.df["phase_rad"] = np.arctan2(self.df["Q_value"], self.df["I_value"]) - - # If you used magnitude_squared in CSV, calculate actual magnitude - if "magnitude_squared" in self.df.columns: - self.df["magnitude"] = np.sqrt(self.df["magnitude_squared"]) - if filename: - try: - self.status_label.config(text="Status: Loading CSV file...") - self.df = pd.read_csv(filename) - self.file_label.config(text=f"Loaded: {filename.split('/')[-1]}") - self.status_label.config(text=f"Status: Loaded {len(self.df)} samples") - - # Show basic info - self.show_file_info() - - except Exception as e: - messagebox.showerror("Error", f"Failed to load CSV file: {e}") - self.status_label.config(text="Status: Error loading file") - - def show_file_info(self): - """Display basic information about loaded data""" - if self.df is not None: - info_text = f"Samples: {len(self.df)} | " - info_text += f"Chirps: {self.df['chirp_number'].nunique()} | " - info_text += f"Long: {len(self.df[self.df['chirp_type'] == 'LONG'])} | " - info_text += f"Short: {len(self.df[self.df['chirp_type'] == 'SHORT'])}" - - self.file_label.config(text=info_text) - - def process_data(self): - """Process loaded CSV data""" - if self.df is None: - messagebox.showwarning("Warning", "Please load a CSV file first") - return - - self.status_label.config(text="Status: Processing data...") - - # Add to processing queue - self.processing_queue.put(("process", self.df)) - - def run_cfar_detection(self): - """Run CFAR detection on processed data""" - if self.df is None: - messagebox.showwarning("Warning", "Please load and process data first") - return - - self.status_label.config(text="Status: Running CFAR detection...") - self.processing_queue.put(("cfar", self.df)) - - def background_processing(self): - - while True: - try: - task_type, data = self.processing_queue.get(timeout=1.0) - - if task_type == "process": - self._process_data_background(data) - elif task_type == "cfar": - self._run_cfar_background(data) - else: - logging.warning(f"Unknown task type: {task_type}") - - self.processing_queue.task_done() - - except queue.Empty: - continue - except Exception as e: - logging.error(f"Background processing error: {e}") - # Update GUI to show error state - self.root.after( - 0, - lambda: self.status_label.config( - text=f"Status: Processing error - {e}" # noqa: F821 - ), - ) - - def _process_data_background(self, df): - try: - # Process long chirps - long_chirp_data = self.processor.process_chirp_sequence(df, "LONG") - - # Process short chirps - short_chirp_data = self.processor.process_chirp_sequence(df, "SHORT") - - # Store results - self.processed_data = {"long": long_chirp_data, "short": short_chirp_data} - - # Update GUI in main thread - self.root.after(0, self._update_plots_after_processing) - - except Exception as e: - logging.error(f"Processing error: {e}") - error_msg = str(e) - self.root.after( - 0, - lambda msg=error_msg: self.status_label.config( - text=f"Status: Processing error - {msg}" - ), - ) - - def _run_cfar_background(self, df): - try: - # Get first chirp for CFAR demonstration - first_chirp = df[df["chirp_number"] == df["chirp_number"].min()] - - if len(first_chirp) == 0: - return - - # Create IQ data - FIXED TYPO: first_chirp not first_chip - iq_data = first_chirp["I_value"].values + 1j * first_chirp["Q_value"].values - - # Perform range FFT - range_axis, range_profile = self.processor.range_fft(iq_data) - - # Run CFAR detection - detections = self.processor.cfar_detection(range_profile) - - # Convert to target objects - self.detected_targets = [] - for range_bin, magnitude in detections: - target = RadarTarget( - range=range_axis[range_bin], - velocity=0, # Would need Doppler processing for velocity - azimuth=0, # From actual data - elevation=0, # From actual data - snr=20 * np.log10(magnitude + 1e-9), # Convert to dB - chirp_type="LONG", - timestamp=time.time(), - ) - self.detected_targets.append(target) - - # Update GUI in main thread - self.root.after( - 0, lambda: self._update_cfar_results(range_axis, range_profile, detections) - ) - - except Exception as e: - logging.error(f"CFAR detection error: {e}") - error_msg = str(e) - self.root.after( - 0, - lambda msg=error_msg: self.status_label.config(text=f"Status: CFAR error - {msg}"), - ) - - def _update_plots_after_processing(self): - try: - # Clear all plots - for ax in [self.ax1, self.ax2, self.ax3, self.ax4]: - ax.clear() - - # Plot 1: Range profile from first chirp - if self.df is not None and len(self.df) > 0: - try: - first_chirp_num = self.df["chirp_number"].min() - first_chirp = self.df[self.df["chirp_number"] == first_chirp_num] - - if len(first_chirp) > 0: - iq_data = first_chirp["I_value"].values + 1j * first_chirp["Q_value"].values - range_axis, range_profile = self.processor.range_fft(iq_data) - - if len(range_axis) > 0 and len(range_profile) > 0: - self.ax1.plot(range_axis, range_profile, "b-") - self.ax1.set_title("Range Profile - First Chirp") - self.ax1.set_xlabel("Range (m)") - self.ax1.set_ylabel("Magnitude") - self.ax1.grid(True) - except Exception as e: - logging.warning(f"Range profile plot error: {e}") - self.ax1.set_title("Range Profile - Error") - - # Plot 2: Doppler spectrum - if self.df is not None and len(self.df) > 0: - try: - sample_data = self.df.head(1024) - if len(sample_data) > 10: - iq_data = sample_data["I_value"].values + 1j * sample_data["Q_value"].values - velocity_axis, doppler_spectrum = self.processor.doppler_fft(iq_data) - - if len(velocity_axis) > 0 and len(doppler_spectrum) > 0: - self.ax2.plot(velocity_axis, doppler_spectrum, "g-") - self.ax2.set_title("Doppler Spectrum") - self.ax2.set_xlabel("Velocity (m/s)") - self.ax2.set_ylabel("Magnitude") - self.ax2.grid(True) - except Exception as e: - logging.warning(f"Doppler spectrum plot error: {e}") - self.ax2.set_title("Doppler Spectrum - Error") - - # Plot 3: Range-Doppler map - if ( - self.processed_data.get("long") - and "range_doppler_matrix" in self.processed_data["long"] - and self.processed_data["long"]["range_doppler_matrix"].size > 0 - ): - try: - rd_matrix = self.processed_data["long"]["range_doppler_matrix"] - # Use integer indices for extent - extent = [0, int(rd_matrix.shape[1]), 0, int(rd_matrix.shape[0])] - - im = self.ax3.imshow( - 10 * np.log10(rd_matrix + 1e-9), aspect="auto", cmap="hot", extent=extent - ) - self.ax3.set_title("Range-Doppler Map (Long Chirps)") - self.ax3.set_xlabel("Doppler Bin") - self.ax3.set_ylabel("Range Bin") - self.fig.colorbar(im, ax=self.ax3, label="dB") - except Exception as e: - logging.warning(f"Range-Doppler map plot error: {e}") - self.ax3.set_title("Range-Doppler Map - Error") - - # Plot 4: MTI filtered data - if self.df is not None and len(self.df) > 0: - try: - sample_data = self.df.head(100) - if len(sample_data) > 10: - iq_data = sample_data["I_value"].values + 1j * sample_data["Q_value"].values - - # Original data - original_mag = np.abs(iq_data) - - # MTI filtered - mti_filtered = self.processor.mti_filter(iq_data) - - if mti_filtered is not None and len(mti_filtered) > 0: - mti_mag = np.abs(mti_filtered) - - # Use integer indices for plotting - x_original = np.arange(len(original_mag)) - x_mti = np.arange(len(mti_mag)) - - self.ax4.plot( - x_original, original_mag, "b-", label="Original", alpha=0.7 - ) - self.ax4.plot(x_mti, mti_mag, "r-", label="MTI Filtered", alpha=0.7) - self.ax4.set_title("MTI Filter Comparison") - self.ax4.set_xlabel("Sample Index") - self.ax4.set_ylabel("Magnitude") - self.ax4.legend() - self.ax4.grid(True) - except Exception as e: - logging.warning(f"MTI filter plot error: {e}") - self.ax4.set_title("MTI Filter - Error") - - # Adjust layout and draw - self.fig.tight_layout() - self.canvas.draw() - self.status_label.config(text="Status: Processing complete") - - except Exception as e: - logging.error(f"Plot update error: {e}") - error_msg = str(e) - self.status_label.config(text=f"Status: Plot error - {error_msg}") - - def _update_cfar_results(self, range_axis, range_profile, detections): - try: - # Clear the plot - self.ax1.clear() - - # Plot range profile - self.ax1.plot(range_axis, range_profile, "b-", label="Range Profile") - - # Plot detections - ensure we use integer indices - if detections and len(range_axis) > 0: - detection_ranges = [] - detection_mags = [] - - for bin_idx, mag in detections: - # Convert bin_idx to integer and ensure it's within bounds - bin_idx_int = int(bin_idx) - if 0 <= bin_idx_int < len(range_axis): - detection_ranges.append(range_axis[bin_idx_int]) - detection_mags.append(mag) - - if detection_ranges: # Only plot if we have valid detections - self.ax1.plot( - detection_ranges, - detection_mags, - "ro", - markersize=8, - label="CFAR Detections", - ) - - self.ax1.set_title("Range Profile with CFAR Detections") - self.ax1.set_xlabel("Range (m)") - self.ax1.set_ylabel("Magnitude") - self.ax1.legend() - self.ax1.grid(True) - - # Update targets list - self.update_targets_list() - - self.canvas.draw() - self.status_label.config( - text=f"Status: CFAR complete - {len(detections)} targets detected" - ) - - except Exception as e: - logging.error(f"CFAR results update error: {e}") - error_msg = str(e) - self.status_label.config(text=f"Status: CFAR results error - {error_msg}") - - def update_targets_list(self): - """Update the targets list display""" - # Clear current list - for item in self.targets_tree.get_children(): - self.targets_tree.delete(item) - - # Add detected targets - for i, target in enumerate(self.detected_targets): - self.targets_tree.insert( - "", - "end", - values=( - f"{target.range:.1f}", - f"{target.velocity:.1f}", - f"{target.azimuth}", - f"{target.elevation}", - f"{target.snr:.1f}", - target.chirp_type, - ), - ) - - def update_gui(self): - """Periodic GUI update""" - # You can add any periodic updates here - self.root.after(100, self.update_gui) - - -def main(): - """Main application entry point""" - try: - root = tk.Tk() - _app = RadarGUI(root) - root.mainloop() - except Exception as e: - logging.error(f"Application error: {e}") - messagebox.showerror("Fatal Error", f"Application failed to start: {e}") - - -if __name__ == "__main__": - main() diff --git a/9_Firmware/9_3_GUI/GUI_V5.py b/9_Firmware/9_3_GUI/GUI_V5.py index 6de79d7..82505a6 100644 --- a/9_Firmware/9_3_GUI/GUI_V5.py +++ b/9_Firmware/9_3_GUI/GUI_V5.py @@ -27,9 +27,9 @@ except ImportError: USB_AVAILABLE = False logging.warning("pyusb not available. USB CDC functionality will be disabled.") -try: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools +try: + from pyftdi.ftdi import Ftdi, FtdiError + from pyftdi.usbtools import UsbTools FTDI_AVAILABLE = True except ImportError: @@ -288,18 +288,16 @@ class MapGenerator: targets_json = str(map_targets).replace("'", '"') targets_script = f"updateTargets({targets_json});" - # Fill template - map_html = self.map_html_template.format( - lat=gps_data.latitude, - lon=gps_data.longitude, - alt=gps_data.altitude, - pitch=gps_data.pitch, - coverage_radius=coverage_radius, - targets_script=targets_script, - api_key=api_key, - ) - - return map_html + # Fill template + return self.map_html_template.format( + lat=gps_data.latitude, + lon=gps_data.longitude, + alt=gps_data.altitude, + pitch=gps_data.pitch, + coverage_radius=coverage_radius, + targets_script=targets_script, + api_key=api_key, + ) def polar_to_geographic(self, radar_lat, radar_lon, range_m, azimuth_deg): """ @@ -369,7 +367,7 @@ class STM32USBInterface: "device": dev, } ) - except Exception: + except (usb.core.USBError, ValueError): devices.append( { "description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", @@ -380,7 +378,7 @@ class STM32USBInterface: ) return devices - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error listing USB devices: {e}") # Return mock devices for testing return [ @@ -430,7 +428,7 @@ class STM32USBInterface: logging.info(f"STM32 USB device opened: {device_info['description']}") return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error opening USB device: {e}") return False @@ -446,7 +444,7 @@ class STM32USBInterface: packet = self._create_settings_packet(settings) logging.info("Sending radar settings to STM32 via USB...") return self._send_data(packet) - except Exception as e: + except (usb.core.USBError, struct.error) as e: logging.error(f"Error sending settings via USB: {e}") return False @@ -463,9 +461,6 @@ class STM32USBInterface: return None logging.error(f"USB read error: {e}") return None - except Exception as e: - logging.error(f"Error reading from USB: {e}") - return None def _send_data(self, data): """Send data to STM32 via USB""" @@ -483,7 +478,7 @@ class STM32USBInterface: self.ep_out.write(chunk) return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error sending data via USB: {e}") return False @@ -509,7 +504,7 @@ class STM32USBInterface: try: usb.util.dispose_resources(self.device) self.is_open = False - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error closing USB device: {e}") @@ -524,16 +519,14 @@ class FTDIInterface: logging.warning("FTDI not available - please install pyftdi") return [] - try: - devices = [] - # Get list of all FTDI devices - for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID - devices.append( - {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} - ) - return devices - except Exception as e: - logging.error(f"Error listing FTDI devices: {e}") + try: + # Get list of all FTDI devices + return [ + {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} + for device in UsbTools.find_all([(0x0403, 0x6010)]) + ] # FT2232H vendor/product ID + except usb.core.USBError as e: + logging.error(f"Error listing FTDI devices: {e}") # Return mock devices for testing return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}] @@ -560,7 +553,7 @@ class FTDIInterface: logging.info(f"FTDI device opened: {device_url}") return True - except Exception as e: + except FtdiError as e: logging.error(f"Error opening FTDI device: {e}") return False @@ -574,7 +567,7 @@ class FTDIInterface: if data: return bytes(data) return None - except Exception as e: + except FtdiError as e: logging.error(f"Error reading from FTDI: {e}") return None @@ -595,8 +588,7 @@ class RadarProcessor: def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): """Dual-CPI fusion for better detection""" - fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - return fused_profile + return np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): """Multi-PRF velocity unwrapping""" @@ -643,7 +635,7 @@ class RadarProcessor: return clusters - def association(self, detections, clusters): + def association(self, detections, _clusters): """Association of detections to tracks""" associated_detections = [] @@ -737,7 +729,7 @@ class USBPacketParser: if len(data) >= 30 and data[0:4] == b"GPSB": return self._parse_binary_gps_with_pitch(data) - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing GPS data: {e}") return None @@ -789,7 +781,7 @@ class USBPacketParser: timestamp=time.time(), ) - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing binary GPS with pitch: {e}") return None @@ -831,13 +823,12 @@ class RadarPacketParser: if packet_type == 0x01: return self.parse_range_packet(payload) - elif packet_type == 0x02: + if packet_type == 0x02: return self.parse_doppler_packet(payload) - elif packet_type == 0x03: + if packet_type == 0x03: return self.parse_detection_packet(payload) - else: - logging.warning(f"Unknown packet type: {packet_type:02X}") - return None + logging.warning(f"Unknown packet type: {packet_type:02X}") + return None def calculate_crc(self, data): return self.crc16_func(data) @@ -860,7 +851,7 @@ class RadarPacketParser: "chirp": chirp_counter, "timestamp": time.time(), } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing range packet: {e}") return None @@ -884,7 +875,7 @@ class RadarPacketParser: "chirp": chirp_counter, "timestamp": time.time(), } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing Doppler packet: {e}") return None @@ -906,7 +897,7 @@ class RadarPacketParser: "chirp": chirp_counter, "timestamp": time.time(), } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing detection packet: {e}") return None @@ -1345,7 +1336,7 @@ class RadarGUI: logging.info("Radar system started successfully via USB CDC") - except Exception as e: + except (usb.core.USBError, FtdiError, ValueError) as e: messagebox.showerror("Error", f"Failed to start radar: {e}") logging.error(f"Start radar error: {e}") @@ -1414,7 +1405,7 @@ class RadarGUI: else: break - except Exception as e: + except FtdiError as e: logging.error(f"Error processing radar data: {e}") time.sleep(0.1) else: @@ -1438,7 +1429,7 @@ class RadarGUI: f"Alt {gps_data.altitude:.1f}m, " f"Pitch {gps_data.pitch:.1f}°" ) - except Exception as e: + except (usb.core.USBError, ValueError, struct.error) as e: logging.error(f"Error processing GPS data via USB: {e}") time.sleep(0.1) @@ -1501,7 +1492,7 @@ class RadarGUI: f"Pitch {self.current_gps.pitch:.1f}°" ) - except Exception as e: + except (ValueError, KeyError) as e: logging.error(f"Error processing radar packet: {e}") def update_range_doppler_map(self, target): @@ -1568,9 +1559,9 @@ class RadarGUI: ) logging.info(f"Map generated: {self.map_file_path}") - except Exception as e: + except (OSError, ValueError) as e: logging.error(f"Error generating map: {e}") - self.map_status_label.config(text=f"Map: Error - {str(e)}") + self.map_status_label.config(text=f"Map: Error - {e!s}") def update_gps_display(self): """Step 18: Update GPS and pitch display""" @@ -1657,7 +1648,7 @@ class RadarGUI: # Update GPS and pitch display self.update_gps_display() - except Exception as e: + except (tk.TclError, RuntimeError) as e: logging.error(f"Error updating GUI: {e}") self.root.after(100, self.update_gui) @@ -1669,7 +1660,7 @@ def main(): root = tk.Tk() _app = RadarGUI(root) root.mainloop() - except Exception as e: + except Exception as e: # noqa: BLE001 logging.error(f"Application error: {e}") messagebox.showerror("Fatal Error", f"Application failed to start: {e}") diff --git a/9_Firmware/9_3_GUI/GUI_V5_Demo.py b/9_Firmware/9_3_GUI/GUI_V5_Demo.py index b2bbc2d..e9f785c 100644 --- a/9_Firmware/9_3_GUI/GUI_V5_Demo.py +++ b/9_Firmware/9_3_GUI/GUI_V5_Demo.py @@ -36,9 +36,9 @@ except ImportError: USB_AVAILABLE = False logging.warning("pyusb not available. USB CDC functionality will be disabled.") -try: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools +try: + from pyftdi.ftdi import Ftdi, FtdiError + from pyftdi.usbtools import UsbTools FTDI_AVAILABLE = True except ImportError: @@ -108,8 +108,7 @@ class RadarProcessor: def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): """Dual-CPI fusion for better detection""" - fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - return fused_profile + return np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): """Multi-PRF velocity unwrapping""" @@ -156,7 +155,7 @@ class RadarProcessor: return clusters - def association(self, detections, clusters): + def association(self, detections, _clusters): """Association of detections to tracks""" associated_detections = [] @@ -250,7 +249,7 @@ class USBPacketParser: if len(data) >= 30 and data[0:4] == b"GPSB": return self._parse_binary_gps_with_pitch(data) - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing GPS data: {e}") return None @@ -302,7 +301,7 @@ class USBPacketParser: timestamp=time.time(), ) - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing binary GPS with pitch: {e}") return None @@ -344,13 +343,12 @@ class RadarPacketParser: if packet_type == 0x01: return self.parse_range_packet(payload) - elif packet_type == 0x02: + if packet_type == 0x02: return self.parse_doppler_packet(payload) - elif packet_type == 0x03: + if packet_type == 0x03: return self.parse_detection_packet(payload) - else: - logging.warning(f"Unknown packet type: {packet_type:02X}") - return None + logging.warning(f"Unknown packet type: {packet_type:02X}") + return None def calculate_crc(self, data): return self.crc16_func(data) @@ -373,7 +371,7 @@ class RadarPacketParser: "chirp": chirp_counter, "timestamp": time.time(), } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing range packet: {e}") return None @@ -397,7 +395,7 @@ class RadarPacketParser: "chirp": chirp_counter, "timestamp": time.time(), } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing Doppler packet: {e}") return None @@ -419,7 +417,7 @@ class RadarPacketParser: "chirp": chirp_counter, "timestamp": time.time(), } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing detection packet: {e}") return None @@ -687,23 +685,22 @@ class MapGenerator: # Calculate coverage radius in km coverage_radius_km = coverage_radius / 1000.0 - # Generate HTML content - map_html = self.map_html_template.replace("{lat}", str(gps_data.latitude)) - map_html = map_html.replace("{lon}", str(gps_data.longitude)) - map_html = map_html.replace("{alt:.1f}", f"{gps_data.altitude:.1f}") - map_html = map_html.replace("{pitch:+.1f}", f"{gps_data.pitch:+.1f}") - map_html = map_html.replace("{coverage_radius}", str(coverage_radius)) - map_html = map_html.replace("{coverage_radius_km:.1f}", f"{coverage_radius_km:.1f}") - map_html = map_html.replace("{target_count}", str(len(map_targets))) - - # Inject initial targets as JavaScript variable - targets_json = json.dumps(map_targets) - map_html = map_html.replace( - "// Display initial targets if any", - f"window.initialTargets = {targets_json};\n // Display initial targets if any", - ) - - return map_html + # Generate HTML content + targets_json = json.dumps(map_targets) + return ( + self.map_html_template.replace("{lat}", str(gps_data.latitude)) + .replace("{lon}", str(gps_data.longitude)) + .replace("{alt:.1f}", f"{gps_data.altitude:.1f}") + .replace("{pitch:+.1f}", f"{gps_data.pitch:+.1f}") + .replace("{coverage_radius}", str(coverage_radius)) + .replace("{coverage_radius_km:.1f}", f"{coverage_radius_km:.1f}") + .replace("{target_count}", str(len(map_targets))) + .replace( + "// Display initial targets if any", + "window.initialTargets = " + f"{targets_json};\n // Display initial targets if any", + ) + ) def polar_to_geographic(self, radar_lat, radar_lon, range_m, azimuth_deg): """ @@ -775,7 +772,7 @@ class STM32USBInterface: "device": dev, } ) - except Exception: + except (usb.core.USBError, ValueError): devices.append( { "description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", @@ -786,7 +783,7 @@ class STM32USBInterface: ) return devices - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error listing USB devices: {e}") # Return mock devices for testing return [ @@ -836,7 +833,7 @@ class STM32USBInterface: logging.info(f"STM32 USB device opened: {device_info['description']}") return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error opening USB device: {e}") return False @@ -852,7 +849,7 @@ class STM32USBInterface: packet = self._create_settings_packet(settings) logging.info("Sending radar settings to STM32 via USB...") return self._send_data(packet) - except Exception as e: + except (usb.core.USBError, struct.error) as e: logging.error(f"Error sending settings via USB: {e}") return False @@ -869,9 +866,6 @@ class STM32USBInterface: return None logging.error(f"USB read error: {e}") return None - except Exception as e: - logging.error(f"Error reading from USB: {e}") - return None def _send_data(self, data): """Send data to STM32 via USB""" @@ -889,7 +883,7 @@ class STM32USBInterface: self.ep_out.write(chunk) return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error sending data via USB: {e}") return False @@ -915,7 +909,7 @@ class STM32USBInterface: try: usb.util.dispose_resources(self.device) self.is_open = False - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error closing USB device: {e}") @@ -930,16 +924,14 @@ class FTDIInterface: logging.warning("FTDI not available - please install pyftdi") return [] - try: - devices = [] - # Get list of all FTDI devices - for device in UsbTools.find_all([(0x0403, 0x6010)]): # FT2232H vendor/product ID - devices.append( - {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} - ) - return devices - except Exception as e: - logging.error(f"Error listing FTDI devices: {e}") + try: + # Get list of all FTDI devices + return [ + {"description": f"FTDI Device {device}", "url": f"ftdi://{device}/1"} + for device in UsbTools.find_all([(0x0403, 0x6010)]) + ] # FT2232H vendor/product ID + except usb.core.USBError as e: + logging.error(f"Error listing FTDI devices: {e}") # Return mock devices for testing return [{"description": "FT2232H Device A", "url": "ftdi://device/1"}] @@ -966,7 +958,7 @@ class FTDIInterface: logging.info(f"FTDI device opened: {device_url}") return True - except Exception as e: + except FtdiError as e: logging.error(f"Error opening FTDI device: {e}") return False @@ -980,7 +972,7 @@ class FTDIInterface: if data: return bytes(data) return None - except Exception as e: + except FtdiError as e: logging.error(f"Error reading from FTDI: {e}") return None @@ -1242,7 +1234,7 @@ class RadarGUI: """ self.browser.load_html(placeholder_html) - except Exception as e: + except (tk.TclError, RuntimeError) as e: logging.error(f"Failed to create embedded browser: {e}") self.create_browser_fallback() else: @@ -1340,7 +1332,7 @@ Map HTML will appear here when generated. self.fallback_text.configure(state="disabled") self.fallback_text.see("1.0") # Scroll to top logging.info("Fallback text widget updated with map HTML") - except Exception as e: + except (tk.TclError, RuntimeError) as e: logging.error(f"Error updating embedded browser: {e}") def generate_map(self): @@ -1386,7 +1378,7 @@ Map HTML will appear here when generated. logging.info(f"Map generated with {len(targets)} targets") - except Exception as e: + except (OSError, ValueError) as e: logging.error(f"Error generating map: {e}") self.map_status_label.config(text=f"Map: Error - {str(e)[:50]}") @@ -1400,19 +1392,19 @@ Map HTML will appear here when generated. # Create temporary HTML file import tempfile - temp_file = tempfile.NamedTemporaryFile( - mode="w", suffix=".html", delete=False, encoding="utf-8" - ) - temp_file.write(self.current_map_html) - temp_file.close() + with tempfile.NamedTemporaryFile( + mode="w", suffix=".html", delete=False, encoding="utf-8" + ) as temp_file: + temp_file.write(self.current_map_html) + temp_file_path = temp_file.name # Open in default browser - webbrowser.open("file://" + os.path.abspath(temp_file.name)) - logging.info(f"Map opened in external browser: {temp_file.name}") + webbrowser.open("file://" + os.path.abspath(temp_file_path)) + logging.info(f"Map opened in external browser: {temp_file_path}") - except Exception as e: - logging.error(f"Error opening external browser: {e}") - messagebox.showerror("Error", f"Failed to open browser: {e}") + except (OSError, ValueError) as e: + logging.error(f"Error opening external browser: {e}") + messagebox.showerror("Error", f"Failed to open browser: {e}") # ... [Rest of the methods remain the same - demo mode, radar processing, etc.] ... @@ -1427,7 +1419,7 @@ def main(): root = tk.Tk() _app = RadarGUI(root) root.mainloop() - except Exception as e: + except Exception as e: # noqa: BLE001 logging.error(f"Application error: {e}") messagebox.showerror("Fatal Error", f"Application failed to start: {e}") diff --git a/9_Firmware/9_3_GUI/GUI_V6.py b/9_Firmware/9_3_GUI/GUI_V6.py index 0396266..5688288 100644 --- a/9_Firmware/9_3_GUI/GUI_V6.py +++ b/9_Firmware/9_3_GUI/GUI_V6.py @@ -26,9 +26,9 @@ except ImportError: logging.warning("pyusb not available. USB functionality will be disabled.") try: - from pyftdi.ftdi import Ftdi # noqa: F401 - from pyftdi.usbtools import UsbTools # noqa: F401 - from pyftdi.ftdi import FtdiError # noqa: F401 + from pyftdi.ftdi import Ftdi + from pyftdi.usbtools import UsbTools # noqa: F401 + from pyftdi.ftdi import FtdiError # noqa: F401 FTDI_AVAILABLE = True except ImportError: FTDI_AVAILABLE = False @@ -242,7 +242,6 @@ class MapGenerator: """ - pass class FT601Interface: """ @@ -298,7 +297,7 @@ class FT601Interface: 'device': dev, 'serial': serial }) - except Exception: + except (usb.core.USBError, ValueError): devices.append({ 'description': f"FT601 USB3.0 (VID:{vid:04X}, PID:{pid:04X})", 'vendor_id': vid, @@ -308,7 +307,7 @@ class FT601Interface: }) return devices - except Exception as e: + except (usb.core.USBError, ValueError) as e: logging.error(f"Error listing FT601 devices: {e}") # Return mock devices for testing return [ @@ -350,7 +349,7 @@ class FT601Interface: logging.info(f"FT601 device opened: {device_url}") return True - except Exception as e: + except OSError as e: logging.error(f"Error opening FT601 device: {e}") return False @@ -403,7 +402,7 @@ class FT601Interface: logging.info(f"FT601 device opened: {device_info['description']}") return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error opening FT601 device: {e}") return False @@ -427,7 +426,7 @@ class FT601Interface: return bytes(data) return None - elif self.device and self.ep_in: + if self.device and self.ep_in: # Direct USB access if bytes_to_read is None: bytes_to_read = 512 @@ -448,7 +447,7 @@ class FT601Interface: return bytes(data) if data else None - except Exception as e: + except (usb.core.USBError, OSError) as e: logging.error(f"Error reading from FT601: {e}") return None @@ -468,7 +467,7 @@ class FT601Interface: self.ftdi.write_data(data) return True - elif self.device and self.ep_out: + if self.device and self.ep_out: # Direct USB access # FT601 supports large transfers max_packet = 512 @@ -479,7 +478,7 @@ class FT601Interface: return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error writing to FT601: {e}") return False @@ -498,7 +497,7 @@ class FT601Interface: self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.RESET) logging.info("FT601 burst mode disabled") return True - except Exception as e: + except OSError as e: logging.error(f"Error configuring burst mode: {e}") return False return False @@ -510,14 +509,14 @@ class FT601Interface: self.ftdi.close() self.is_open = False logging.info("FT601 device closed") - except Exception as e: + except OSError as e: logging.error(f"Error closing FT601 device: {e}") if self.device and self.is_open: try: usb.util.dispose_resources(self.device) self.is_open = False - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error closing FT601 device: {e}") class STM32USBInterface: @@ -563,7 +562,7 @@ class STM32USBInterface: 'product_id': pid, 'device': dev }) - except Exception: + except (usb.core.USBError, ValueError): devices.append({ 'description': f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", 'vendor_id': vid, @@ -572,7 +571,7 @@ class STM32USBInterface: }) return devices - except Exception as e: + except (usb.core.USBError, ValueError) as e: logging.error(f"Error listing USB devices: {e}") # Return mock devices for testing return [{ @@ -626,7 +625,7 @@ class STM32USBInterface: logging.info(f"STM32 USB device opened: {device_info['description']}") return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error opening USB device: {e}") return False @@ -642,7 +641,7 @@ class STM32USBInterface: packet = self._create_settings_packet(settings) logging.info("Sending radar settings to STM32 via USB...") return self._send_data(packet) - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error sending settings via USB: {e}") return False @@ -659,7 +658,7 @@ class STM32USBInterface: return None logging.error(f"USB read error: {e}") return None - except Exception as e: + except ValueError as e: logging.error(f"Error reading from USB: {e}") return None @@ -679,7 +678,7 @@ class STM32USBInterface: self.ep_out.write(chunk) return True - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error sending data via USB: {e}") return False @@ -705,7 +704,7 @@ class STM32USBInterface: try: usb.util.dispose_resources(self.device) self.is_open = False - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error closing USB device: {e}") @@ -720,8 +719,7 @@ class RadarProcessor: def dual_cpi_fusion(self, range_profiles_1, range_profiles_2): """Dual-CPI fusion for better detection""" - fused_profile = np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - return fused_profile + return np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) def multi_prf_unwrap(self, doppler_measurements, prf1, prf2): """Multi-PRF velocity unwrapping""" @@ -766,7 +764,7 @@ class RadarProcessor: return clusters - def association(self, detections, clusters): + def association(self, detections, _clusters): """Association of detections to tracks""" associated_detections = [] @@ -862,7 +860,7 @@ class USBPacketParser: if len(data) >= 30 and data[0:4] == b'GPSB': return self._parse_binary_gps_with_pitch(data) - except Exception as e: + except ValueError as e: logging.error(f"Error parsing GPS data: {e}") return None @@ -914,7 +912,7 @@ class USBPacketParser: timestamp=time.time() ) - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing binary GPS with pitch: {e}") return None @@ -936,7 +934,7 @@ class RadarPacketParser: if len(packet) < 6: return None - _sync = packet[0:2] # noqa: F841 + _sync = packet[0:2] packet_type = packet[2] length = packet[3] @@ -956,13 +954,12 @@ class RadarPacketParser: if packet_type == 0x01: return self.parse_range_packet(payload) - elif packet_type == 0x02: + if packet_type == 0x02: return self.parse_doppler_packet(payload) - elif packet_type == 0x03: + if packet_type == 0x03: return self.parse_detection_packet(payload) - else: - logging.warning(f"Unknown packet type: {packet_type:02X}") - return None + logging.warning(f"Unknown packet type: {packet_type:02X}") + return None def calculate_crc(self, data): return self.crc16_func(data) @@ -985,7 +982,7 @@ class RadarPacketParser: 'chirp': chirp_counter, 'timestamp': time.time() } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing range packet: {e}") return None @@ -1009,7 +1006,7 @@ class RadarPacketParser: 'chirp': chirp_counter, 'timestamp': time.time() } - except Exception as e: + except (ValueError, struct.error) as e: logging.error(f"Error parsing Doppler packet: {e}") return None @@ -1031,7 +1028,7 @@ class RadarPacketParser: 'chirp': chirp_counter, 'timestamp': time.time() } - except Exception as e: + except (usb.core.USBError, ValueError) as e: logging.error(f"Error parsing detection packet: {e}") return None @@ -1371,9 +1368,9 @@ class RadarGUI: logging.info("Radar system started successfully with FT601 USB 3.0") - except Exception as e: - messagebox.showerror("Error", f"Failed to start radar: {e}") - logging.error(f"Start radar error: {e}") + except usb.core.USBError as e: + messagebox.showerror("Error", f"Failed to start radar: {e}") + logging.error(f"Start radar error: {e}") def stop_radar(self): """Stop radar operation""" @@ -1416,13 +1413,13 @@ class RadarGUI: else: break - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error processing radar data: {e}") time.sleep(0.1) else: time.sleep(0.1) - def get_packet_length(self, packet): + def get_packet_length(self, _packet): """Calculate packet length including header and footer""" # This should match your packet structure return 64 # Example: 64-byte packets @@ -1443,7 +1440,7 @@ class RadarGUI: f"Lon {gps_data.longitude:.6f}, " f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°" ) - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error processing GPS data via USB: {e}") time.sleep(0.1) @@ -1506,7 +1503,7 @@ class RadarGUI: f"Pitch {self.current_gps.pitch:.1f}°" ) - except Exception as e: + except (ValueError, IndexError) as e: logging.error(f"Error processing radar packet: {e}") def update_range_doppler_map(self, target): @@ -1604,9 +1601,9 @@ class RadarGUI: ) logging.info(f"Map generated: {self.map_file_path}") - except Exception as e: + except OSError as e: logging.error(f"Error generating map: {e}") - self.map_status_label.config(text=f"Map: Error - {str(e)}") + self.map_status_label.config(text=f"Map: Error - {e!s}") def update_gps_display(self): """Step 18: Update GPS and pitch display""" @@ -1753,7 +1750,7 @@ class RadarGUI: else: break - except Exception as e: + except (usb.core.USBError, ValueError, struct.error) as e: logging.error(f"Error processing radar data: {e}") time.sleep(0.1) else: @@ -1775,7 +1772,7 @@ class RadarGUI: f"Lon {gps_data.longitude:.6f}, " f"Alt {gps_data.altitude:.1f}m, Pitch {gps_data.pitch:.1f}°" ) - except Exception as e: + except usb.core.USBError as e: logging.error(f"Error processing GPS data via USB: {e}") time.sleep(0.1) @@ -1803,7 +1800,7 @@ class RadarGUI: # Update GPS and pitch display self.update_gps_display() - except Exception as e: + except (ValueError, IndexError) as e: logging.error(f"Error updating GUI: {e}") self.root.after(100, self.update_gui) @@ -1812,9 +1809,9 @@ def main(): """Main application entry point""" try: root = tk.Tk() - _app = RadarGUI(root) # noqa: F841 – must stay alive for mainloop + _app = RadarGUI(root) # must stay alive for mainloop root.mainloop() - except Exception as e: + except Exception as e: # noqa: BLE001 logging.error(f"Application error: {e}") messagebox.showerror("Fatal Error", f"Application failed to start: {e}") diff --git a/9_Firmware/9_3_GUI/GUI_V6_Demo.py b/9_Firmware/9_3_GUI/GUI_V6_Demo.py index e68c9bd..dd4135c 100644 --- a/9_Firmware/9_3_GUI/GUI_V6_Demo.py +++ b/9_Firmware/9_3_GUI/GUI_V6_Demo.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ Radar System GUI - Fully Functional Demo Version @@ -15,7 +14,6 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import logging from dataclasses import dataclass -from typing import List, Dict import random import json from datetime import datetime @@ -65,7 +63,7 @@ class SimulatedRadarProcessor: self.noise_floor = 10 self.clutter_level = 5 - def _create_targets(self) -> List[Dict]: + def _create_targets(self) -> list[dict]: """Create moving targets""" return [ { @@ -210,22 +208,20 @@ class SimulatedRadarProcessor: return rd_map - def _detect_targets(self) -> List[RadarTarget]: + def _detect_targets(self) -> list[RadarTarget]: """Detect targets from current state""" - detected = [] - for t in self.targets: - # Random detection based on SNR - if random.random() < (t['snr'] / 35): - # Add some measurement noise - detected.append(RadarTarget( - id=t['id'], - range=t['range'] + random.gauss(0, 10), - velocity=t['velocity'] + random.gauss(0, 2), - azimuth=t['azimuth'] + random.gauss(0, 1), - elevation=t['elevation'] + random.gauss(0, 0.5), - snr=t['snr'] + random.gauss(0, 2) - )) - return detected + return [ + RadarTarget( + id=t['id'], + range=t['range'] + random.gauss(0, 10), + velocity=t['velocity'] + random.gauss(0, 2), + azimuth=t['azimuth'] + random.gauss(0, 1), + elevation=t['elevation'] + random.gauss(0, 0.5), + snr=t['snr'] + random.gauss(0, 2) + ) + for t in self.targets + if random.random() < (t['snr'] / 35) + ] # ============================================================================ # MAIN GUI APPLICATION @@ -566,7 +562,7 @@ class RadarDemoGUI: scrollable_frame.bind( "", - lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + lambda _e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") @@ -586,7 +582,7 @@ class RadarDemoGUI: ('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.0) ] - for i, (label, key, default, minv, maxv) in enumerate(settings): + for _i, (label, key, default, minv, maxv) in enumerate(settings): frame = ttk.Frame(scrollable_frame) frame.pack(fill='x', padx=10, pady=5) @@ -745,7 +741,7 @@ class RadarDemoGUI: # Update time self.time_label.config(text=time.strftime("%H:%M:%S")) - except Exception as e: + except (ValueError, IndexError) as e: logger.error(f"Animation error: {e}") # Schedule next update @@ -940,7 +936,7 @@ class RadarDemoGUI: messagebox.showinfo("Success", "Settings applied") logger.info("Settings updated") - except Exception as e: + except (ValueError, tk.TclError) as e: messagebox.showerror("Error", f"Invalid settings: {e}") def apply_display_settings(self): @@ -981,7 +977,7 @@ class RadarDemoGUI: ) if filename: try: - with open(filename, 'r') as f: + with open(filename) as f: config = json.load(f) # Apply settings @@ -1004,7 +1000,7 @@ class RadarDemoGUI: messagebox.showinfo("Success", f"Loaded configuration from {filename}") logger.info(f"Configuration loaded from {filename}") - except Exception as e: + except (OSError, json.JSONDecodeError, ValueError, tk.TclError) as e: messagebox.showerror("Error", f"Failed to load: {e}") def save_config(self): @@ -1031,7 +1027,7 @@ class RadarDemoGUI: messagebox.showinfo("Success", f"Saved configuration to {filename}") logger.info(f"Configuration saved to {filename}") - except Exception as e: + except (OSError, TypeError, ValueError) as e: messagebox.showerror("Error", f"Failed to save: {e}") def export_data(self): @@ -1061,7 +1057,7 @@ class RadarDemoGUI: messagebox.showinfo("Success", f"Exported {len(frames)} frames to {filename}") logger.info(f"Data exported to {filename}") - except Exception as e: + except (OSError, ValueError) as e: messagebox.showerror("Error", f"Failed to export: {e}") def show_calibration(self): @@ -1205,7 +1201,7 @@ def main(): root = tk.Tk() # Create application - _app = RadarDemoGUI(root) # noqa: F841 — keeps reference alive + _app = RadarDemoGUI(root) # keeps reference alive # Center window root.update_idletasks() @@ -1218,7 +1214,7 @@ def main(): # Start main loop root.mainloop() - except Exception as e: + except Exception as e: # noqa: BLE001 logger.error(f"Fatal error: {e}") messagebox.showerror("Fatal Error", f"Application failed to start:\n{e}") diff --git a/9_Firmware/9_3_GUI/radar_dashboard.py b/9_Firmware/9_3_GUI/radar_dashboard.py index aa7d81d..75779a8 100644 --- a/9_Firmware/9_3_GUI/radar_dashboard.py +++ b/9_Firmware/9_3_GUI/radar_dashboard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -AERIS-10 Radar Dashboard — Board Bring-Up Edition +AERIS-10 Radar Dashboard =================================================== Real-time visualization and control for the AERIS-10 phased-array radar via FT2232H USB 2.0 interface. @@ -10,7 +10,8 @@ Features: - Real-time range-Doppler magnitude heatmap (64x32) - CFAR detection overlay (flagged cells highlighted) - Range profile waterfall plot (range vs. time) - - Host command sender (opcodes 0x01-0x27, 0x30, 0xFF) + - Host command sender (opcodes per radar_system_top.v: + 0x01-0x04, 0x10-0x16, 0x20-0x27, 0x30-0x31, 0xFF) - Configuration panel for all radar parameters - HDF5 data recording for offline analysis - Mock mode for development/testing without hardware @@ -27,7 +28,7 @@ import queue import logging import argparse import threading -from typing import Optional, Dict +import contextlib from collections import deque import numpy as np @@ -82,18 +83,19 @@ class RadarDashboard: C = 3e8 # m/s — speed of light def __init__(self, root: tk.Tk, connection: FT2232HConnection, - recorder: DataRecorder): + recorder: DataRecorder, device_index: int = 0): self.root = root self.conn = connection self.recorder = recorder + self.device_index = device_index - self.root.title("AERIS-10 Radar Dashboard — Bring-Up Edition") + self.root.title("AERIS-10 Radar Dashboard") self.root.geometry("1600x950") self.root.configure(bg=BG) # Frame queue (acquisition → display) self.frame_queue: queue.Queue[RadarFrame] = queue.Queue(maxsize=8) - self._acq_thread: Optional[RadarAcquisition] = None + self._acq_thread: RadarAcquisition | None = None # Display state self._current_frame = RadarFrame() @@ -154,7 +156,7 @@ class RadarDashboard: self.btn_record = ttk.Button(top, text="Record", command=self._on_record) self.btn_record.pack(side="right", padx=4) - # Notebook (tabs) + # -- Tabbed notebook layout -- nb = ttk.Notebook(self.root) nb.pack(fill="both", expand=True, padx=8, pady=8) @@ -173,9 +175,8 @@ class RadarDashboard: # Compute physical axis limits # Range resolution: dR = c / (2 * BW) per range bin # But we decimate 1024→64 bins, so each bin spans 16 FFT bins. - # Range per FFT bin = c / (2 * BW) * (Fs / FFT_SIZE) — simplified: - # max_range = c * Fs / (4 * BW) for Fs-sampled baseband - # range_per_bin = max_range / NUM_RANGE_BINS + # Range resolution derivation: c/(2*BW) gives ~0.3 m per FFT bin. + # After 1024-to-64 decimation each displayed range bin spans 16 FFT bins. range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin # After decimation 1024→64, each range bin = 16 FFT bins range_per_bin = range_res * 16 @@ -232,39 +233,92 @@ class RadarDashboard: self._canvas = canvas def _build_control_tab(self, parent): - """Host command sender and configuration panel.""" - outer = ttk.Frame(parent) - outer.pack(fill="both", expand=True, padx=16, pady=16) + """Host command sender — organized by FPGA register groups. - # Left column: Quick actions - left = ttk.LabelFrame(outer, text="Quick Actions", padding=12) - left.grid(row=0, column=0, sticky="nsew", padx=(0, 8)) + Layout: scrollable canvas with three columns: + Left: Quick Actions + Diagnostics (self-test) + Center: Waveform Timing + Signal Processing + Right: Detection (CFAR) + Custom Command + """ + # Scrollable wrapper for small screens + canvas = tk.Canvas(parent, bg=BG, highlightthickness=0) + scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview) + outer = ttk.Frame(canvas) + outer.bind("", + lambda _e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=outer, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side="left", fill="both", expand=True, padx=8, pady=8) + scrollbar.pack(side="right", fill="y") - ttk.Button(left, text="Trigger Chirp (0x01)", - command=lambda: self._send_cmd(0x01, 1)).pack(fill="x", pady=3) - ttk.Button(left, text="Enable MTI (0x26)", - command=lambda: self._send_cmd(0x26, 1)).pack(fill="x", pady=3) - ttk.Button(left, text="Disable MTI (0x26)", - command=lambda: self._send_cmd(0x26, 0)).pack(fill="x", pady=3) - ttk.Button(left, text="Enable CFAR (0x25)", - command=lambda: self._send_cmd(0x25, 1)).pack(fill="x", pady=3) - ttk.Button(left, text="Disable CFAR (0x25)", - command=lambda: self._send_cmd(0x25, 0)).pack(fill="x", pady=3) - ttk.Button(left, text="Request Status (0xFF)", - command=lambda: self._send_cmd(0xFF, 0)).pack(fill="x", pady=3) + self._param_vars: dict[str, tk.StringVar] = {} - ttk.Separator(left, orient="horizontal").pack(fill="x", pady=6) + # ── Left column: Quick Actions + Diagnostics ────────────────── + left = ttk.Frame(outer) + left.grid(row=0, column=0, sticky="nsew", padx=(0, 6)) - ttk.Label(left, text="FPGA Self-Test", font=("Menlo", 10, "bold")).pack( - anchor="w", pady=(2, 0)) - ttk.Button(left, text="Run Self-Test (0x30)", - command=lambda: self._send_cmd(0x30, 1)).pack(fill="x", pady=3) - ttk.Button(left, text="Read Self-Test Result (0x31)", - command=lambda: self._send_cmd(0x31, 0)).pack(fill="x", pady=3) + # -- Radar Operation -- + grp_op = ttk.LabelFrame(left, text="Radar Operation", padding=10) + grp_op.pack(fill="x", pady=(0, 8)) - # Self-test result display - st_frame = ttk.LabelFrame(left, text="Self-Test Results", padding=6) - st_frame.pack(fill="x", pady=(6, 0)) + ttk.Button(grp_op, text="Radar Mode On", + command=lambda: self._send_cmd(0x01, 1)).pack(fill="x", pady=2) + ttk.Button(grp_op, text="Radar Mode Off", + command=lambda: self._send_cmd(0x01, 0)).pack(fill="x", pady=2) + ttk.Button(grp_op, text="Trigger Chirp", + command=lambda: self._send_cmd(0x02, 1)).pack(fill="x", pady=2) + + # Stream Control (3-bit mask) + sc_row = ttk.Frame(grp_op) + sc_row.pack(fill="x", pady=2) + ttk.Label(sc_row, text="Stream Control").pack(side="left") + var_sc = tk.StringVar(value="7") + self._param_vars["4"] = var_sc + ttk.Entry(sc_row, textvariable=var_sc, width=6).pack(side="left", padx=6) + ttk.Label(sc_row, text="0-7", foreground=ACCENT, + font=("Menlo", 9)).pack(side="left") + ttk.Button(sc_row, text="Set", + command=lambda: self._send_validated( + 0x04, var_sc, bits=3)).pack(side="right") + + ttk.Button(grp_op, text="Request Status", + command=lambda: self._send_cmd(0xFF, 0)).pack(fill="x", pady=2) + + # -- Signal Processing -- + grp_sp = ttk.LabelFrame(left, text="Signal Processing", padding=10) + grp_sp.pack(fill="x", pady=(0, 8)) + + sp_params = [ + # Format: label, opcode, default, bits, hint + ("Detect Threshold", 0x03, "10000", 16, "0-65535"), + ("Gain Shift", 0x16, "0", 4, "0-15, dir+shift"), + ("MTI Enable", 0x26, "0", 1, "0=off, 1=on"), + ("DC Notch Width", 0x27, "0", 3, "0-7 bins"), + ] + for label, opcode, default, bits, hint in sp_params: + self._add_param_row(grp_sp, label, opcode, default, bits, hint) + + # MTI quick toggle + mti_row = ttk.Frame(grp_sp) + mti_row.pack(fill="x", pady=2) + ttk.Button(mti_row, text="Enable MTI", + command=lambda: self._send_cmd(0x26, 1)).pack( + side="left", expand=True, fill="x", padx=(0, 2)) + ttk.Button(mti_row, text="Disable MTI", + command=lambda: self._send_cmd(0x26, 0)).pack( + side="left", expand=True, fill="x", padx=(2, 0)) + + # -- Diagnostics -- + grp_diag = ttk.LabelFrame(left, text="Diagnostics", padding=10) + grp_diag.pack(fill="x", pady=(0, 8)) + + ttk.Button(grp_diag, text="Run Self-Test", + command=lambda: self._send_cmd(0x30, 1)).pack(fill="x", pady=2) + ttk.Button(grp_diag, text="Read Self-Test Result", + command=lambda: self._send_cmd(0x31, 0)).pack(fill="x", pady=2) + + st_frame = ttk.LabelFrame(grp_diag, text="Self-Test Results", padding=6) + st_frame.pack(fill="x", pady=(4, 0)) self._st_labels = {} for name, default_text in [ ("busy", "Busy: --"), @@ -280,59 +334,108 @@ class RadarDashboard: lbl.pack(anchor="w") self._st_labels[name] = lbl - # Right column: Parameter configuration - right = ttk.LabelFrame(outer, text="Parameter Configuration", padding=12) - right.grid(row=0, column=1, sticky="nsew", padx=(8, 0)) + # ── Center column: Waveform Timing ──────────────────────────── + center = ttk.Frame(outer) + center.grid(row=0, column=1, sticky="nsew", padx=6) - self._param_vars: Dict[str, tk.StringVar] = {} - params = [ - ("CFAR Guard (0x21)", 0x21, "2"), - ("CFAR Train (0x22)", 0x22, "8"), - ("CFAR Alpha Q4.4 (0x23)", 0x23, "48"), - ("CFAR Mode (0x24)", 0x24, "0"), - ("Threshold (0x10)", 0x10, "500"), - ("Gain Shift (0x06)", 0x06, "0"), - ("DC Notch Width (0x27)", 0x27, "0"), - ("Range Mode (0x20)", 0x20, "0"), - ("Stream Enable (0x05)", 0x05, "7"), + grp_wf = ttk.LabelFrame(center, text="Waveform Timing", padding=10) + grp_wf.pack(fill="x", pady=(0, 8)) + + wf_params = [ + ("Long Chirp Cycles", 0x10, "3000", 16, "0-65535, rst=3000"), + ("Long Listen Cycles", 0x11, "13700", 16, "0-65535, rst=13700"), + ("Guard Cycles", 0x12, "17540", 16, "0-65535, rst=17540"), + ("Short Chirp Cycles", 0x13, "50", 16, "0-65535, rst=50"), + ("Short Listen Cycles", 0x14, "17450", 16, "0-65535, rst=17450"), + ("Chirps Per Elevation", 0x15, "32", 6, "1-32, clamped"), ] + for label, opcode, default, bits, hint in wf_params: + self._add_param_row(grp_wf, label, opcode, default, bits, hint) - for row_idx, (label, opcode, default) in enumerate(params): - ttk.Label(right, text=label).grid(row=row_idx, column=0, - sticky="w", pady=2) - var = tk.StringVar(value=default) - self._param_vars[str(opcode)] = var - ent = ttk.Entry(right, textvariable=var, width=10) - ent.grid(row=row_idx, column=1, padx=8, pady=2) - ttk.Button( - right, text="Set", - command=lambda op=opcode, v=var: self._send_cmd(op, int(v.get())) - ).grid(row=row_idx, column=2, pady=2) + # ── Right column: Detection (CFAR) + Custom ─────────────────── + right = ttk.Frame(outer) + right.grid(row=0, column=2, sticky="nsew", padx=(6, 0)) - # Custom command - ttk.Separator(right, orient="horizontal").grid( - row=len(params), column=0, columnspan=3, sticky="ew", pady=8) + grp_cfar = ttk.LabelFrame(right, text="Detection (CFAR)", padding=10) + grp_cfar.pack(fill="x", pady=(0, 8)) - ttk.Label(right, text="Custom Opcode (hex)").grid( - row=len(params) + 1, column=0, sticky="w") + cfar_params = [ + ("CFAR Enable", 0x25, "0", 1, "0=off, 1=on"), + ("CFAR Guard Cells", 0x21, "2", 4, "0-15, rst=2"), + ("CFAR Train Cells", 0x22, "8", 5, "1-31, rst=8"), + ("CFAR Alpha (Q4.4)", 0x23, "48", 8, "0-255, rst=0x30=3.0"), + ("CFAR Mode", 0x24, "0", 2, "0=CA 1=GO 2=SO"), + ] + for label, opcode, default, bits, hint in cfar_params: + self._add_param_row(grp_cfar, label, opcode, default, bits, hint) + + # CFAR quick toggle + cfar_row = ttk.Frame(grp_cfar) + cfar_row.pack(fill="x", pady=2) + ttk.Button(cfar_row, text="Enable CFAR", + command=lambda: self._send_cmd(0x25, 1)).pack( + side="left", expand=True, fill="x", padx=(0, 2)) + ttk.Button(cfar_row, text="Disable CFAR", + command=lambda: self._send_cmd(0x25, 0)).pack( + side="left", expand=True, fill="x", padx=(2, 0)) + + # ── Custom Command (advanced / debug) ───────────────────────── + grp_cust = ttk.LabelFrame(right, text="Custom Command", padding=10) + grp_cust.pack(fill="x", pady=(0, 8)) + + r0 = ttk.Frame(grp_cust) + r0.pack(fill="x", pady=2) + ttk.Label(r0, text="Opcode (hex)").pack(side="left") self._custom_op = tk.StringVar(value="01") - ttk.Entry(right, textvariable=self._custom_op, width=10).grid( - row=len(params) + 1, column=1, padx=8) + ttk.Entry(r0, textvariable=self._custom_op, width=8).pack( + side="left", padx=6) - ttk.Label(right, text="Value (dec)").grid( - row=len(params) + 2, column=0, sticky="w") + r1 = ttk.Frame(grp_cust) + r1.pack(fill="x", pady=2) + ttk.Label(r1, text="Value (dec)").pack(side="left") self._custom_val = tk.StringVar(value="0") - ttk.Entry(right, textvariable=self._custom_val, width=10).grid( - row=len(params) + 2, column=1, padx=8) + ttk.Entry(r1, textvariable=self._custom_val, width=8).pack( + side="left", padx=6) - ttk.Button(right, text="Send Custom", - command=self._send_custom).grid( - row=len(params) + 2, column=2, pady=2) + ttk.Button(grp_cust, text="Send", + command=self._send_custom).pack(fill="x", pady=2) + # Column weights outer.columnconfigure(0, weight=1) - outer.columnconfigure(1, weight=2) + outer.columnconfigure(1, weight=1) + outer.columnconfigure(2, weight=1) outer.rowconfigure(0, weight=1) + def _add_param_row(self, parent, label: str, opcode: int, + default: str, bits: int, hint: str): + """Add a single parameter row: label, entry, hint, Set button with validation.""" + row = ttk.Frame(parent) + row.pack(fill="x", pady=2) + ttk.Label(row, text=label).pack(side="left") + var = tk.StringVar(value=default) + self._param_vars[str(opcode)] = var + ttk.Entry(row, textvariable=var, width=8).pack(side="left", padx=6) + ttk.Label(row, text=hint, foreground=ACCENT, + font=("Menlo", 9)).pack(side="left") + ttk.Button(row, text="Set", + command=lambda: self._send_validated( + opcode, var, bits=bits)).pack(side="right") + + def _send_validated(self, opcode: int, var: tk.StringVar, bits: int): + """Parse, clamp to bit-width, send command, and update the entry.""" + try: + raw = int(var.get()) + except ValueError: + log.error(f"Invalid value for opcode 0x{opcode:02X}: {var.get()!r}") + return + max_val = (1 << bits) - 1 + clamped = max(0, min(raw, max_val)) + if clamped != raw: + log.warning(f"Value {raw} clamped to {clamped} " + f"({bits}-bit max={max_val}) for opcode 0x{opcode:02X}") + var.set(str(clamped)) + self._send_cmd(opcode, clamped) + def _build_log_tab(self, parent): self.log_text = tk.Text(parent, bg=BG2, fg=FG, font=("Menlo", 10), insertbackground=FG, wrap="word") @@ -364,7 +467,7 @@ class RadarDashboard: self.root.update_idletasks() def _do_connect(): - ok = self.conn.open() + ok = self.conn.open(self.device_index) # Schedule UI update back on the main thread self.root.after(0, lambda: self._on_connect_done(ok)) @@ -530,10 +633,8 @@ class _TextHandler(logging.Handler): def emit(self, record): msg = self.format(record) - try: + with contextlib.suppress(Exception): self._text.after(0, self._append, msg) - except Exception: - pass def _append(self, msg: str): self._text.insert("end", msg + "\n") @@ -578,7 +679,7 @@ def main(): root = tk.Tk() - dashboard = RadarDashboard(root, conn, recorder) + dashboard = RadarDashboard(root, conn, recorder, device_index=args.device) if args.record: filepath = os.path.join( diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 52e4543..a9382b2 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -10,7 +10,7 @@ USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi USB Packet Protocol (11-byte): TX (FPGA→Host): Data packet: [0xAA] [range_q 2B] [range_i 2B] [dop_re 2B] [dop_im 2B] [det 1B] [0x55] - Status packet: [0xBB] [status 6×32b] [0x55] + Status packet: [0xBB] [status 6x32b] [0x55] RX (Host→FPGA): Command: 4 bytes received sequentially {opcode, addr, value_hi, value_lo} """ @@ -21,8 +21,9 @@ import time import threading import queue import logging +import contextlib from dataclasses import dataclass, field -from typing import Optional, List, Tuple, Dict, Any +from typing import Any from enum import IntEnum @@ -50,20 +51,36 @@ WATERFALL_DEPTH = 64 class Opcode(IntEnum): - """Host register opcodes (matches radar_system_top.v command decode).""" - TRIGGER = 0x01 - PRF_DIV = 0x02 - NUM_CHIRPS = 0x03 - CHIRP_TIMER = 0x04 - STREAM_ENABLE = 0x05 - GAIN_SHIFT = 0x06 - THRESHOLD = 0x10 + """Host register opcodes — must match radar_system_top.v case(usb_cmd_opcode). + + FPGA truth table (from radar_system_top.v lines 902-944): + 0x01 host_radar_mode 0x14 host_short_listen_cycles + 0x02 host_trigger_pulse 0x15 host_chirps_per_elev + 0x03 host_detect_threshold 0x16 host_gain_shift + 0x04 host_stream_control 0x20 host_range_mode + 0x10 host_long_chirp_cycles 0x21-0x27 CFAR / MTI / DC-notch + 0x11 host_long_listen_cycles 0x30 host_self_test_trigger + 0x12 host_guard_cycles 0x31 host_status_request + 0x13 host_short_chirp_cycles 0xFF host_status_request + """ + # --- Basic control (0x01-0x04) --- + RADAR_MODE = 0x01 # 2-bit mode select + TRIGGER_PULSE = 0x02 # self-clearing one-shot trigger + DETECT_THRESHOLD = 0x03 # 16-bit detection threshold value + STREAM_CONTROL = 0x04 # 3-bit stream enable mask + + # --- Digital gain (0x16) --- + GAIN_SHIFT = 0x16 # 4-bit digital gain shift + + # --- Chirp timing (0x10-0x15) --- LONG_CHIRP = 0x10 LONG_LISTEN = 0x11 GUARD = 0x12 SHORT_CHIRP = 0x13 SHORT_LISTEN = 0x14 CHIRPS_PER_ELEV = 0x15 + + # --- Signal processing (0x20-0x27) --- RANGE_MODE = 0x20 CFAR_GUARD = 0x21 CFAR_TRAIN = 0x22 @@ -72,6 +89,8 @@ class Opcode(IntEnum): CFAR_ENABLE = 0x25 MTI_ENABLE = 0x26 DC_NOTCH_WIDTH = 0x27 + + # --- Board self-test / status (0x30-0x31, 0xFF) --- SELF_TEST_TRIGGER = 0x30 SELF_TEST_STATUS = 0x31 STATUS_REQUEST = 0xFF @@ -83,7 +102,7 @@ class Opcode(IntEnum): @dataclass class RadarFrame: - """One complete radar frame (64 range × 32 Doppler).""" + """One complete radar frame (64 range x 32 Doppler).""" timestamp: float = 0.0 range_doppler_i: np.ndarray = field( default_factory=lambda: np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.int16)) @@ -101,7 +120,7 @@ class RadarFrame: @dataclass class StatusResponse: - """Parsed status response from FPGA (8-word packet as of Build 26).""" + """Parsed status response from FPGA (6-word / 26-byte packet).""" radar_mode: int = 0 stream_ctrl: int = 0 cfar_threshold: int = 0 @@ -144,7 +163,7 @@ class RadarProtocol: return struct.pack(">I", word) @staticmethod - def parse_data_packet(raw: bytes) -> Optional[Dict[str, Any]]: + def parse_data_packet(raw: bytes) -> dict[str, Any] | None: """ Parse an 11-byte data packet from the FT2232H byte stream. Returns dict with keys: 'range_i', 'range_q', 'doppler_i', 'doppler_q', @@ -181,10 +200,10 @@ class RadarProtocol: } @staticmethod - def parse_status_packet(raw: bytes) -> Optional[StatusResponse]: + def parse_status_packet(raw: bytes) -> StatusResponse | None: """ Parse a status response packet. - Format: [0xBB] [6×4B status words] [0x55] = 1 + 24 + 1 = 26 bytes + Format: [0xBB] [6x4B status words] [0x55] = 1 + 24 + 1 = 26 bytes """ if len(raw) < 26: return None @@ -223,7 +242,7 @@ class RadarProtocol: return sr @staticmethod - def find_packet_boundaries(buf: bytes) -> List[Tuple[int, int, str]]: + def find_packet_boundaries(buf: bytes) -> list[tuple[int, int, str]]: """ Scan buffer for packet start markers (0xAA data, 0xBB status). Returns list of (start_idx, expected_end_idx, packet_type). @@ -233,19 +252,22 @@ class RadarProtocol: while i < len(buf): if buf[i] == HEADER_BYTE: end = i + DATA_PACKET_SIZE - if end <= len(buf): + if end <= len(buf) and buf[end - 1] == FOOTER_BYTE: packets.append((i, end, "data")) i = end else: - break + if end > len(buf): + break # partial packet at end — leave for residual + i += 1 # footer mismatch — skip this false header elif buf[i] == STATUS_HEADER_BYTE: - # Status packet: 26 bytes (same for both interfaces) end = i + STATUS_PACKET_SIZE - if end <= len(buf): + if end <= len(buf) and buf[end - 1] == FOOTER_BYTE: packets.append((i, end, "status")) i = end else: - break + if end > len(buf): + break # partial status packet — leave for residual + i += 1 # footer mismatch — skip else: i += 1 return packets @@ -257,9 +279,13 @@ class RadarProtocol: # Optional pyftdi import try: - from pyftdi.ftdi import Ftdi as PyFtdi + from pyftdi.ftdi import Ftdi, FtdiError + PyFtdi = Ftdi PYFTDI_AVAILABLE = True except ImportError: + class FtdiError(Exception): + """Fallback FTDI error type when pyftdi is unavailable.""" + PYFTDI_AVAILABLE = False @@ -306,20 +332,18 @@ class FT2232HConnection: self.is_open = True log.info(f"FT2232H device opened: {url}") return True - except Exception as e: + except FtdiError as e: log.error(f"FT2232H open failed: {e}") return False def close(self): if self._ftdi is not None: - try: + with contextlib.suppress(Exception): self._ftdi.close() - except Exception: - pass self._ftdi = None self.is_open = False - def read(self, size: int = 4096) -> Optional[bytes]: + def read(self, size: int = 4096) -> bytes | None: """Read raw bytes from FT2232H. Returns None on error/timeout.""" if not self.is_open: return None @@ -331,7 +355,7 @@ class FT2232HConnection: try: data = self._ftdi.read_data(size) return bytes(data) if data else None - except Exception as e: + except FtdiError as e: log.error(f"FT2232H read error: {e}") return None @@ -348,24 +372,29 @@ class FT2232HConnection: try: written = self._ftdi.write_data(data) return written == len(data) - except Exception as e: + except FtdiError as e: log.error(f"FT2232H write error: {e}") return False def _mock_read(self, size: int) -> bytes: """ - Generate synthetic compact radar data packets (11-byte) for testing. Generate synthetic 11-byte radar data packets for testing. - Simulates a batch of packets with a target near range bin 20, Doppler bin 8. + Emits packets in sequential FPGA order (range_bin 0..63, doppler_bin + 0..31 within each range bin) so that RadarAcquisition._ingest_sample() + places them correctly. A target is injected near range bin 20, + Doppler bin 8. """ time.sleep(0.05) self._mock_frame_num += 1 buf = bytearray() - num_packets = min(32, size // DATA_PACKET_SIZE) - for _ in range(num_packets): - rbin = self._mock_rng.randint(0, NUM_RANGE_BINS) - dbin = self._mock_rng.randint(0, NUM_DOPPLER_BINS) + num_packets = min(NUM_CELLS, size // DATA_PACKET_SIZE) + start_idx = getattr(self, '_mock_seq_idx', 0) + + for n in range(num_packets): + idx = (start_idx + n) % NUM_CELLS + rbin = idx // NUM_DOPPLER_BINS + dbin = idx % NUM_DOPPLER_BINS range_i = int(self._mock_rng.normal(0, 100)) range_q = int(self._mock_rng.normal(0, 100)) @@ -393,6 +422,7 @@ class FT2232HConnection: buf += pkt + self._mock_seq_idx = (start_idx + num_packets) % NUM_CELLS return bytes(buf) @@ -401,19 +431,19 @@ class FT2232HConnection: # ============================================================================ # Hardware-only opcodes that cannot be adjusted in replay mode +# Values must match radar_system_top.v case(usb_cmd_opcode). _HARDWARE_ONLY_OPCODES = { - 0x01, # TRIGGER - 0x02, # PRF_DIV - 0x03, # NUM_CHIRPS - 0x04, # CHIRP_TIMER - 0x05, # STREAM_ENABLE - 0x06, # GAIN_SHIFT - 0x10, # THRESHOLD / LONG_CHIRP + 0x01, # RADAR_MODE + 0x02, # TRIGGER_PULSE + 0x03, # DETECT_THRESHOLD + 0x04, # STREAM_CONTROL + 0x10, # LONG_CHIRP 0x11, # LONG_LISTEN 0x12, # GUARD 0x13, # SHORT_CHIRP 0x14, # SHORT_LISTEN 0x15, # CHIRPS_PER_ELEV + 0x16, # GAIN_SHIFT 0x20, # RANGE_MODE 0x30, # SELF_TEST_TRIGGER 0x31, # SELF_TEST_STATUS @@ -439,26 +469,8 @@ def _saturate(val: int, bits: int) -> int: return max(max_neg, min(max_pos, int(val))) -def _replay_mti(decim_i: np.ndarray, decim_q: np.ndarray, - enable: bool) -> Tuple[np.ndarray, np.ndarray]: - """Bit-accurate 2-pulse MTI canceller (matches mti_canceller.v).""" - n_chirps, n_bins = decim_i.shape - mti_i = np.zeros_like(decim_i) - mti_q = np.zeros_like(decim_q) - if not enable: - return decim_i.copy(), decim_q.copy() - for c in range(n_chirps): - if c == 0: - pass # muted - else: - for r in range(n_bins): - mti_i[c, r] = _saturate(int(decim_i[c, r]) - int(decim_i[c - 1, r]), 16) - mti_q[c, r] = _saturate(int(decim_q[c, r]) - int(decim_q[c - 1, r]), 16) - return mti_i, mti_q - - def _replay_dc_notch(doppler_i: np.ndarray, doppler_q: np.ndarray, - width: int) -> Tuple[np.ndarray, np.ndarray]: + width: int) -> tuple[np.ndarray, np.ndarray]: """Bit-accurate DC notch filter (matches radar_system_top.v inline). Dual sub-frame notch: doppler_bin[4:0] = {sub_frame, bin[3:0]}. @@ -480,7 +492,7 @@ def _replay_dc_notch(doppler_i: np.ndarray, doppler_q: np.ndarray, def _replay_cfar(doppler_i: np.ndarray, doppler_q: np.ndarray, guard: int, train: int, alpha_q44: int, - mode: int) -> Tuple[np.ndarray, np.ndarray]: + mode: int) -> tuple[np.ndarray, np.ndarray]: """ Bit-accurate CA-CFAR detector (matches cfar_ca.v). Returns (detect_flags, magnitudes) both (64, 32). @@ -584,16 +596,16 @@ class ReplayConnection: self._cfar_mode: int = 0 # 0=CA, 1=GO, 2=SO self._cfar_enable: bool = True # Raw source arrays (loaded once, reprocessed on param change) - self._dop_mti_i: Optional[np.ndarray] = None - self._dop_mti_q: Optional[np.ndarray] = None - self._dop_nomti_i: Optional[np.ndarray] = None - self._dop_nomti_q: Optional[np.ndarray] = None - self._range_i_vec: Optional[np.ndarray] = None - self._range_q_vec: Optional[np.ndarray] = None + self._dop_mti_i: np.ndarray | None = None + self._dop_mti_q: np.ndarray | None = None + self._dop_nomti_i: np.ndarray | None = None + self._dop_nomti_q: np.ndarray | None = None + self._range_i_vec: np.ndarray | None = None + self._range_q_vec: np.ndarray | None = None # Rebuild flag self._needs_rebuild = False - def open(self, device_index: int = 0) -> bool: + def open(self, _device_index: int = 0) -> bool: try: self._load_arrays() self._packets = self._build_packets() @@ -604,14 +616,14 @@ class ReplayConnection: f"(MTI={'ON' if self._mti_enable else 'OFF'}, " f"{self._frame_len} bytes/frame)") return True - except Exception as e: + except (OSError, ValueError, struct.error) as e: log.error(f"Replay open failed: {e}") return False def close(self): self.is_open = False - def read(self, size: int = 4096) -> Optional[bytes]: + def read(self, size: int = 4096) -> bytes | None: if not self.is_open: return None # Pace reads to target FPS (spread across ~64 reads per frame) @@ -673,10 +685,9 @@ class ReplayConnection: if self._mti_enable != new_en: self._mti_enable = new_en changed = True - elif opcode == 0x27: # DC_NOTCH_WIDTH - if self._dc_notch_width != value: - self._dc_notch_width = value - changed = True + elif opcode == 0x27 and self._dc_notch_width != value: # DC_NOTCH_WIDTH + self._dc_notch_width = value + changed = True if changed: self._needs_rebuild = True if changed: @@ -827,7 +838,7 @@ class DataRecorder: self._frame_count = 0 self._recording = True log.info(f"Recording started: {filepath}") - except Exception as e: + except (OSError, ValueError) as e: log.error(f"Failed to start recording: {e}") def record_frame(self, frame: RadarFrame): @@ -844,7 +855,7 @@ class DataRecorder: fg.create_dataset("detections", data=frame.detections, compression="gzip") fg.create_dataset("range_profile", data=frame.range_profile, compression="gzip") self._frame_count += 1 - except Exception as e: + except (OSError, ValueError, TypeError) as e: log.error(f"Recording error: {e}") def stop(self): @@ -853,7 +864,7 @@ class DataRecorder: self._file.attrs["end_time"] = time.time() self._file.attrs["total_frames"] = self._frame_count self._file.close() - except Exception: + except (OSError, ValueError, RuntimeError): pass self._file = None self._recording = False @@ -871,7 +882,7 @@ class RadarAcquisition(threading.Thread): """ def __init__(self, connection, frame_queue: queue.Queue, - recorder: Optional[DataRecorder] = None, + recorder: DataRecorder | None = None, status_callback=None): super().__init__(daemon=True) self.conn = connection @@ -888,13 +899,25 @@ class RadarAcquisition(threading.Thread): def run(self): log.info("Acquisition thread started") + residual = b"" while not self._stop_event.is_set(): - raw = self.conn.read(4096) - if raw is None or len(raw) == 0: + chunk = self.conn.read(4096) + if chunk is None or len(chunk) == 0: time.sleep(0.01) continue + raw = residual + chunk packets = RadarProtocol.find_packet_boundaries(raw) + + # Keep unparsed tail bytes for next iteration + if packets: + last_end = packets[-1][1] + residual = raw[last_end:] + else: + # No packets found — keep entire buffer as residual + # but cap at 2x max packet size to avoid unbounded growth + max_residual = 2 * max(DATA_PACKET_SIZE, STATUS_PACKET_SIZE) + residual = raw[-max_residual:] if len(raw) > max_residual else raw for start, end, ptype in packets: if ptype == "data": parsed = RadarProtocol.parse_data_packet( @@ -913,12 +936,12 @@ class RadarAcquisition(threading.Thread): if self._status_callback is not None: try: self._status_callback(status) - except Exception as e: + except Exception as e: # noqa: BLE001 log.error(f"Status callback error: {e}") log.info("Acquisition thread stopped") - def _ingest_sample(self, sample: Dict): + def _ingest_sample(self, sample: dict): """Place sample into current frame and emit when complete.""" rbin = self._sample_idx // NUM_DOPPLER_BINS dbin = self._sample_idx % NUM_DOPPLER_BINS @@ -948,10 +971,8 @@ class RadarAcquisition(threading.Thread): try: self.frame_queue.put_nowait(self._frame) except queue.Full: - try: + with contextlib.suppress(queue.Empty): self.frame_queue.get_nowait() - except queue.Empty: - pass self.frame_queue.put_nowait(self._frame) if self.recorder and self.recorder.recording: diff --git a/9_Firmware/9_3_GUI/requirements_pyqt_gui.txt b/9_Firmware/9_3_GUI/requirements_pyqt_gui.txt new file mode 100644 index 0000000..5578f63 --- /dev/null +++ b/9_Firmware/9_3_GUI/requirements_pyqt_gui.txt @@ -0,0 +1,20 @@ +# Requirements for PLFM Radar Dashboard - PyQt6 Edition +# ====================================================== +# Install with: pip install -r requirements_pyqt_gui.txt + +# Core PyQt6 framework +PyQt6>=6.5.0 + +# Web engine for embedded Leaflet maps +PyQt6-WebEngine>=6.5.0 + +# Optional: Additional dependencies from existing radar code +# (uncomment if integrating with existing radar processing) +# numpy>=1.24 +# scipy>=1.10 +# scikit-learn>=1.2 +# filterpy>=1.4 +# matplotlib>=3.7 + +# Note: The GUI uses Leaflet.js (loaded from CDN) for maps +# No additional Python map libraries required diff --git a/9_Firmware/9_3_GUI/requirements_v7.txt b/9_Firmware/9_3_GUI/requirements_v7.txt new file mode 100644 index 0000000..0a5ea08 --- /dev/null +++ b/9_Firmware/9_3_GUI/requirements_v7.txt @@ -0,0 +1,22 @@ +# PLFM Radar GUI V7 — Python dependencies +# Install with: pip install -r requirements_v7.txt + +# Core (required) +PyQt6>=6.5 +PyQt6-WebEngine>=6.5 +numpy>=1.24 +matplotlib>=3.7 + +# Hardware interfaces (optional — GUI degrades gracefully) +pyusb>=1.2 +pyftdi>=0.54 + +# Signal processing (optional) +scipy>=1.10 + +# Tracking / clustering (optional) +scikit-learn>=1.2 +filterpy>=1.4 + +# CRC validation (optional) +crcmod>=1.7 diff --git a/9_Firmware/9_3_GUI/smoke_test.py b/9_Firmware/9_3_GUI/smoke_test.py index 70e440c..679e722 100644 --- a/9_Firmware/9_3_GUI/smoke_test.py +++ b/9_Firmware/9_3_GUI/smoke_test.py @@ -66,7 +66,7 @@ TEST_NAMES = { class SmokeTest: """Host-side smoke test controller.""" - def __init__(self, connection: FT2232HConnection, adc_dump_path: str = None): + def __init__(self, connection: FT2232HConnection, adc_dump_path: str | None = None): self.conn = connection self.adc_dump_path = adc_dump_path self._adc_samples = [] @@ -82,10 +82,9 @@ class SmokeTest: log.info("") # Step 1: Connect - if not self.conn.is_open: - if not self.conn.open(): - log.error("Failed to open FT2232H connection") - return False + if not self.conn.is_open and not self.conn.open(): + log.error("Failed to open FT2232H connection") + return False # Step 2: Send self-test trigger (opcode 0x30) log.info("Sending self-test trigger (opcode 0x30)...") @@ -188,10 +187,9 @@ class SmokeTest: def _save_adc_dump(self): """Save captured ADC samples to numpy file.""" - if not self._adc_samples: + if not self._adc_samples and self.conn._mock: # In mock mode, generate synthetic ADC data - if self.conn._mock: - self._adc_samples = list(np.random.randint(0, 65536, 256, dtype=np.uint16)) + self._adc_samples = list(np.random.randint(0, 65536, 256, dtype=np.uint16)) if self._adc_samples: arr = np.array(self._adc_samples, dtype=np.uint16) diff --git a/9_Firmware/9_3_GUI/test_radar_dashboard.py b/9_Firmware/9_3_GUI/test_radar_dashboard.py index e5fc05a..e6d2094 100644 --- a/9_Firmware/9_3_GUI/test_radar_dashboard.py +++ b/9_Firmware/9_3_GUI/test_radar_dashboard.py @@ -368,7 +368,7 @@ class TestRadarAcquisition(unittest.TestCase): # Wait for at least one frame (mock produces ~32 samples per read, # need 2048 for a full frame, so may take a few seconds) frame = None - try: + try: # noqa: SIM105 frame = fq.get(timeout=10) except queue.Empty: pass @@ -421,8 +421,8 @@ class TestEndToEnd(unittest.TestCase): def test_command_roundtrip_all_opcodes(self): """Verify all opcodes produce valid 4-byte commands.""" - opcodes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x10, 0x11, 0x12, - 0x13, 0x14, 0x15, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, + opcodes = [0x01, 0x02, 0x03, 0x04, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x30, 0x31, 0xFF] for op in opcodes: cmd = RadarProtocol.build_command(op, 42) @@ -630,8 +630,8 @@ class TestReplayConnection(unittest.TestCase): cmd = RadarProtocol.build_command(0x01, 1) conn.write(cmd) self.assertFalse(conn._needs_rebuild) - # Send STREAM_ENABLE (hardware-only) - cmd = RadarProtocol.build_command(0x05, 7) + # Send STREAM_CONTROL (hardware-only, opcode 0x04) + cmd = RadarProtocol.build_command(0x04, 7) conn.write(cmd) self.assertFalse(conn._needs_rebuild) conn.close() @@ -668,14 +668,14 @@ class TestReplayConnection(unittest.TestCase): class TestOpcodeEnum(unittest.TestCase): - """Verify Opcode enum matches RTL host register map.""" + """Verify Opcode enum matches RTL host register map (radar_system_top.v).""" - def test_gain_shift_is_0x06(self): - """GAIN_SHIFT opcode must be 0x06 (not 0x16).""" - self.assertEqual(Opcode.GAIN_SHIFT, 0x06) + def test_gain_shift_is_0x16(self): + """GAIN_SHIFT opcode must be 0x16 (matches radar_system_top.v:928).""" + self.assertEqual(Opcode.GAIN_SHIFT, 0x16) def test_no_digital_gain_alias(self): - """DIGITAL_GAIN should NOT exist (was bogus 0x16 alias).""" + """DIGITAL_GAIN should NOT exist (use GAIN_SHIFT).""" self.assertFalse(hasattr(Opcode, 'DIGITAL_GAIN')) def test_self_test_trigger(self): @@ -691,21 +691,40 @@ class TestOpcodeEnum(unittest.TestCase): self.assertIn(0x30, _HARDWARE_ONLY_OPCODES) self.assertIn(0x31, _HARDWARE_ONLY_OPCODES) - def test_0x16_not_in_hardware_only(self): - """Bogus 0x16 must not be in _HARDWARE_ONLY_OPCODES.""" - self.assertNotIn(0x16, _HARDWARE_ONLY_OPCODES) + def test_0x16_in_hardware_only(self): + """GAIN_SHIFT 0x16 must be in _HARDWARE_ONLY_OPCODES.""" + self.assertIn(0x16, _HARDWARE_ONLY_OPCODES) - def test_stream_enable_is_0x05(self): - """STREAM_ENABLE must be 0x05 (not 0x04).""" - self.assertEqual(Opcode.STREAM_ENABLE, 0x05) + def test_stream_control_is_0x04(self): + """STREAM_CONTROL must be 0x04 (matches radar_system_top.v:906).""" + self.assertEqual(Opcode.STREAM_CONTROL, 0x04) + + def test_legacy_aliases_removed(self): + """Legacy aliases must NOT exist in production Opcode enum.""" + for name in ("TRIGGER", "PRF_DIV", "NUM_CHIRPS", "CHIRP_TIMER", + "STREAM_ENABLE", "THRESHOLD"): + self.assertFalse(hasattr(Opcode, name), + f"Legacy alias Opcode.{name} should not exist") + + def test_radar_mode_names(self): + """New canonical names must exist and match FPGA opcodes.""" + self.assertEqual(Opcode.RADAR_MODE, 0x01) + self.assertEqual(Opcode.TRIGGER_PULSE, 0x02) + self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03) + self.assertEqual(Opcode.STREAM_CONTROL, 0x04) + + def test_stale_opcodes_not_in_hardware_only(self): + """Old wrong opcode values must not be in _HARDWARE_ONLY_OPCODES.""" + self.assertNotIn(0x05, _HARDWARE_ONLY_OPCODES) # was wrong STREAM_ENABLE + self.assertNotIn(0x06, _HARDWARE_ONLY_OPCODES) # was wrong GAIN_SHIFT def test_all_rtl_opcodes_present(self): - """Every RTL opcode has a matching Opcode enum member.""" - expected = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + """Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member.""" + expected = {0x01, 0x02, 0x03, 0x04, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x30, 0x31, 0xFF} - enum_values = set(int(m) for m in Opcode) + enum_values = {int(m) for m in Opcode} for op in expected: self.assertIn(op, enum_values, f"0x{op:02X} missing from Opcode enum") diff --git a/9_Firmware/9_3_GUI/test_v7.py b/9_Firmware/9_3_GUI/test_v7.py new file mode 100644 index 0000000..e8ca33e --- /dev/null +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -0,0 +1,347 @@ +""" +V7-specific unit tests for the PLFM Radar GUI V7 modules. + +Tests cover: + - v7.models: RadarTarget, RadarSettings, GPSData, ProcessingConfig + - v7.processing: RadarProcessor, USBPacketParser, apply_pitch_correction + - v7.workers: polar_to_geographic + - v7.hardware: STM32USBInterface (basic), production protocol re-exports + +Does NOT require a running Qt event loop — only unit-testable components. +Run with: python -m unittest test_v7 -v +""" + +import struct +import unittest +from dataclasses import asdict + +import numpy as np + + +# ============================================================================= +# Test: v7.models +# ============================================================================= + +class TestRadarTarget(unittest.TestCase): + """RadarTarget dataclass.""" + + def test_defaults(self): + t = _models().RadarTarget(id=1, range=1000.0, velocity=5.0, + azimuth=45.0, elevation=2.0) + self.assertEqual(t.id, 1) + self.assertEqual(t.range, 1000.0) + self.assertEqual(t.snr, 0.0) + self.assertEqual(t.track_id, -1) + self.assertEqual(t.classification, "unknown") + + def test_to_dict(self): + t = _models().RadarTarget(id=1, range=500.0, velocity=-10.0, + azimuth=0.0, elevation=0.0, snr=15.0) + d = t.to_dict() + self.assertIsInstance(d, dict) + self.assertEqual(d["range"], 500.0) + self.assertEqual(d["snr"], 15.0) + + +class TestRadarSettings(unittest.TestCase): + """RadarSettings — verify stale STM32 fields are removed.""" + + def test_no_stale_fields(self): + """chirp_duration, freq_min/max, prf1/2 must NOT exist.""" + s = _models().RadarSettings() + d = asdict(s) + for stale in ["chirp_duration_1", "chirp_duration_2", + "freq_min", "freq_max", "prf1", "prf2", + "chirps_per_position"]: + self.assertNotIn(stale, d, f"Stale field '{stale}' still present") + + def test_has_physical_conversion_fields(self): + s = _models().RadarSettings() + self.assertIsInstance(s.range_resolution, float) + self.assertIsInstance(s.velocity_resolution, float) + self.assertGreater(s.range_resolution, 0) + self.assertGreater(s.velocity_resolution, 0) + + def test_defaults(self): + s = _models().RadarSettings() + self.assertEqual(s.system_frequency, 10e9) + self.assertEqual(s.coverage_radius, 50000) + self.assertEqual(s.max_distance, 50000) + + +class TestGPSData(unittest.TestCase): + def test_to_dict(self): + g = _models().GPSData(latitude=41.9, longitude=12.5, + altitude=100.0, pitch=2.5) + d = g.to_dict() + self.assertAlmostEqual(d["latitude"], 41.9) + self.assertAlmostEqual(d["pitch"], 2.5) + + +class TestProcessingConfig(unittest.TestCase): + def test_defaults(self): + cfg = _models().ProcessingConfig() + self.assertTrue(cfg.clustering_enabled) + self.assertTrue(cfg.tracking_enabled) + self.assertFalse(cfg.mti_enabled) + self.assertFalse(cfg.cfar_enabled) + + +class TestNoCrcmodDependency(unittest.TestCase): + """crcmod was removed — verify it's not exported.""" + + def test_no_crcmod_available(self): + models = _models() + self.assertFalse(hasattr(models, "CRCMOD_AVAILABLE"), + "CRCMOD_AVAILABLE should be removed from models") + + +# ============================================================================= +# Test: v7.processing +# ============================================================================= + +class TestApplyPitchCorrection(unittest.TestCase): + def test_positive_pitch(self): + from v7.processing import apply_pitch_correction + self.assertAlmostEqual(apply_pitch_correction(10.0, 3.0), 7.0) + + def test_zero_pitch(self): + from v7.processing import apply_pitch_correction + self.assertAlmostEqual(apply_pitch_correction(5.0, 0.0), 5.0) + + +class TestRadarProcessorMTI(unittest.TestCase): + def test_mti_order1(self): + from v7.processing import RadarProcessor + from v7.models import ProcessingConfig + proc = RadarProcessor() + proc.set_config(ProcessingConfig(mti_enabled=True, mti_order=1)) + + frame1 = np.ones((64, 32)) + frame2 = np.ones((64, 32)) * 3 + + result1 = proc.mti_filter(frame1) + np.testing.assert_array_equal(result1, np.zeros((64, 32)), + err_msg="First frame should be zeros (no history)") + + result2 = proc.mti_filter(frame2) + expected = frame2 - frame1 + np.testing.assert_array_almost_equal(result2, expected) + + def test_mti_order2(self): + from v7.processing import RadarProcessor + from v7.models import ProcessingConfig + proc = RadarProcessor() + proc.set_config(ProcessingConfig(mti_enabled=True, mti_order=2)) + + f1 = np.ones((4, 4)) + f2 = np.ones((4, 4)) * 2 + f3 = np.ones((4, 4)) * 5 + + proc.mti_filter(f1) # zeros (need 3 frames) + proc.mti_filter(f2) # zeros + result = proc.mti_filter(f3) + # Order 2: x[n] - 2*x[n-1] + x[n-2] = 5 - 4 + 1 = 2 + np.testing.assert_array_almost_equal(result, np.ones((4, 4)) * 2) + + +class TestRadarProcessorCFAR(unittest.TestCase): + def test_cfar_1d_detects_peak(self): + from v7.processing import RadarProcessor + signal = np.ones(64) * 10 + signal[32] = 500 # inject a strong target + det = RadarProcessor.cfar_1d(signal, guard=2, train=4, + threshold_factor=3.0, cfar_type="CA-CFAR") + self.assertTrue(det[32], "Should detect strong peak at bin 32") + + def test_cfar_1d_no_false_alarm(self): + from v7.processing import RadarProcessor + signal = np.ones(64) * 10 # uniform — no target + det = RadarProcessor.cfar_1d(signal, guard=2, train=4, + threshold_factor=3.0) + self.assertEqual(det.sum(), 0, "Should have no detections in flat noise") + + +class TestRadarProcessorProcessFrame(unittest.TestCase): + def test_process_frame_returns_shapes(self): + from v7.processing import RadarProcessor + proc = RadarProcessor() + frame = np.random.randn(64, 32) * 10 + frame[20, 8] = 5000 # inject a target + power, mask = proc.process_frame(frame) + self.assertEqual(power.shape, (64, 32)) + self.assertEqual(mask.shape, (64, 32)) + self.assertEqual(mask.dtype, bool) + + +class TestRadarProcessorWindowing(unittest.TestCase): + def test_hann_window(self): + from v7.processing import RadarProcessor + data = np.ones((4, 32)) + windowed = RadarProcessor.apply_window(data, "Hann") + # Hann window tapers to ~0 at edges + self.assertLess(windowed[0, 0], 0.1) + self.assertGreater(windowed[0, 16], 0.5) + + def test_none_window(self): + from v7.processing import RadarProcessor + data = np.ones((4, 32)) + result = RadarProcessor.apply_window(data, "None") + np.testing.assert_array_equal(result, data) + + +class TestRadarProcessorDCNotch(unittest.TestCase): + def test_dc_removal(self): + from v7.processing import RadarProcessor + data = np.ones((4, 8)) * 100 + data[0, :] += 50 # DC offset in range bin 0 + result = RadarProcessor.dc_notch(data) + # Mean along axis=1 should be ~0 + row_means = np.mean(result, axis=1) + for m in row_means: + self.assertAlmostEqual(m, 0, places=10) + + +class TestRadarProcessorClustering(unittest.TestCase): + def test_clustering_empty(self): + from v7.processing import RadarProcessor + result = RadarProcessor.clustering([], eps=100, min_samples=2) + self.assertEqual(result, []) + + +class TestUSBPacketParser(unittest.TestCase): + def test_parse_gps_text(self): + from v7.processing import USBPacketParser + parser = USBPacketParser() + data = b"GPS:41.9028,12.4964,100.0,2.5\r\n" + gps = parser.parse_gps_data(data) + self.assertIsNotNone(gps) + self.assertAlmostEqual(gps.latitude, 41.9028, places=3) + self.assertAlmostEqual(gps.longitude, 12.4964, places=3) + self.assertAlmostEqual(gps.altitude, 100.0) + self.assertAlmostEqual(gps.pitch, 2.5) + + def test_parse_gps_text_invalid(self): + from v7.processing import USBPacketParser + parser = USBPacketParser() + self.assertIsNone(parser.parse_gps_data(b"NOT_GPS_DATA")) + self.assertIsNone(parser.parse_gps_data(b"")) + self.assertIsNone(parser.parse_gps_data(None)) + + def test_parse_binary_gps(self): + from v7.processing import USBPacketParser + parser = USBPacketParser() + # Build a valid binary GPS packet + pkt = bytearray(b"GPSB") + pkt += struct.pack(">d", 41.9028) # lat + pkt += struct.pack(">d", 12.4964) # lon + pkt += struct.pack(">f", 100.0) # alt + pkt += struct.pack(">f", 2.5) # pitch + # Simple checksum + cksum = sum(pkt) & 0xFFFF + pkt += struct.pack(">H", cksum) + self.assertEqual(len(pkt), 30) + + gps = parser.parse_gps_data(bytes(pkt)) + self.assertIsNotNone(gps) + self.assertAlmostEqual(gps.latitude, 41.9028, places=3) + + def test_no_crc16_func_attribute(self): + """crcmod was removed — USBPacketParser should not have crc16_func.""" + from v7.processing import USBPacketParser + parser = USBPacketParser() + self.assertFalse(hasattr(parser, "crc16_func"), + "crc16_func should be removed (crcmod dead code)") + + def test_no_multi_prf_unwrap(self): + """multi_prf_unwrap was removed (never called, prf fields removed).""" + from v7.processing import RadarProcessor + self.assertFalse(hasattr(RadarProcessor, "multi_prf_unwrap"), + "multi_prf_unwrap should be removed") + + +# ============================================================================= +# Test: v7.workers — polar_to_geographic +# ============================================================================= + +class TestPolarToGeographic(unittest.TestCase): + def test_north_bearing(self): + from v7.workers import polar_to_geographic + lat, lon = polar_to_geographic(0.0, 0.0, 1000.0, 0.0) + # Moving 1km north from equator + self.assertGreater(lat, 0.0) + self.assertAlmostEqual(lon, 0.0, places=4) + + def test_east_bearing(self): + from v7.workers import polar_to_geographic + lat, lon = polar_to_geographic(0.0, 0.0, 1000.0, 90.0) + self.assertAlmostEqual(lat, 0.0, places=4) + self.assertGreater(lon, 0.0) + + def test_zero_range(self): + from v7.workers import polar_to_geographic + lat, lon = polar_to_geographic(41.9, 12.5, 0.0, 0.0) + self.assertAlmostEqual(lat, 41.9, places=6) + self.assertAlmostEqual(lon, 12.5, places=6) + + +# ============================================================================= +# Test: v7.hardware — production protocol re-exports +# ============================================================================= + +class TestHardwareReExports(unittest.TestCase): + """Verify hardware.py re-exports all production protocol classes.""" + + def test_exports(self): + from v7.hardware import ( + FT2232HConnection, + RadarProtocol, + STM32USBInterface, + ) + # Verify these are actual classes/types, not None + self.assertTrue(callable(FT2232HConnection)) + self.assertTrue(callable(RadarProtocol)) + self.assertTrue(callable(STM32USBInterface)) + + def test_stm32_list_devices_no_crash(self): + from v7.hardware import STM32USBInterface + stm = STM32USBInterface() + self.assertFalse(stm.is_open) + # list_devices should return empty list (no USB in test env), not crash + devs = stm.list_devices() + self.assertIsInstance(devs, list) + + +# ============================================================================= +# Test: v7.__init__ — clean exports +# ============================================================================= + +class TestV7Init(unittest.TestCase): + """Verify top-level v7 package exports.""" + + def test_no_crcmod_export(self): + import v7 + self.assertFalse(hasattr(v7, "CRCMOD_AVAILABLE"), + "CRCMOD_AVAILABLE should not be in v7.__all__") + + def test_key_exports(self): + import v7 + for name in ["RadarTarget", "RadarSettings", "GPSData", + "ProcessingConfig", "FT2232HConnection", + "RadarProtocol", "RadarProcessor", + "RadarDataWorker", "RadarMapWidget", + "RadarDashboard"]: + self.assertTrue(hasattr(v7, name), f"v7 missing export: {name}") + + +# ============================================================================= +# Helper: lazy import of v7.models +# ============================================================================= + +def _models(): + import v7.models + return v7.models + + +if __name__ == "__main__": + unittest.main() diff --git a/9_Firmware/9_3_GUI/v7/__init__.py b/9_Firmware/9_3_GUI/v7/__init__.py index dd25c98..c5e5112 100644 --- a/9_Firmware/9_3_GUI/v7/__init__.py +++ b/9_Firmware/9_3_GUI/v7/__init__.py @@ -19,19 +19,25 @@ from .models import ( DARK_TREEVIEW, DARK_TREEVIEW_ALT, DARK_SUCCESS, DARK_WARNING, DARK_ERROR, DARK_INFO, USB_AVAILABLE, FTDI_AVAILABLE, SCIPY_AVAILABLE, - SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, CRCMOD_AVAILABLE, + SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, ) -# Hardware interfaces +# Hardware interfaces — production protocol via radar_protocol.py from .hardware import ( - FT2232HQInterface, + FT2232HConnection, + ReplayConnection, + RadarProtocol, + Opcode, + RadarAcquisition, + RadarFrame, + StatusResponse, + DataRecorder, STM32USBInterface, ) # Processing pipeline from .processing import ( RadarProcessor, - RadarPacketParser, USBPacketParser, apply_pitch_correction, ) @@ -56,7 +62,7 @@ from .dashboard import ( RangeDopplerCanvas, ) -__all__ = [ +__all__ = [ # noqa: RUF022 # models "RadarTarget", "RadarSettings", "GPSData", "ProcessingConfig", "TileServer", "DARK_BG", "DARK_FG", "DARK_ACCENT", "DARK_HIGHLIGHT", "DARK_BORDER", @@ -64,11 +70,13 @@ __all__ = [ "DARK_TREEVIEW", "DARK_TREEVIEW_ALT", "DARK_SUCCESS", "DARK_WARNING", "DARK_ERROR", "DARK_INFO", "USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE", - "SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE", "CRCMOD_AVAILABLE", - # hardware - "FT2232HQInterface", "STM32USBInterface", + "SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE", + # hardware — production FPGA protocol + "FT2232HConnection", "ReplayConnection", "RadarProtocol", "Opcode", + "RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder", + "STM32USBInterface", # processing - "RadarProcessor", "RadarPacketParser", "USBPacketParser", + "RadarProcessor", "USBPacketParser", "apply_pitch_correction", # workers "RadarDataWorker", "GPSDataWorker", "TargetSimulator", diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index dd638b6..0526877 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -1,31 +1,40 @@ """ v7.dashboard — Main application window for the PLFM Radar GUI V7. -RadarDashboard is a QMainWindow with four tabs: - 1. Main View — Range-Doppler matplotlib canvas, device combos, Start/Stop, targets table - 2. Map View — Embedded Leaflet map + sidebar (position, coverage, demo, target info) - 3. Diagnostics — Connection indicators, packet stats, dependency status, log viewer - 4. Settings — All radar parameters + About section +RadarDashboard is a QMainWindow with five tabs: + 1. Main View — Range-Doppler matplotlib canvas (64x32), device combos, + Start/Stop, targets table + 2. Map View — Embedded Leaflet map + sidebar + 3. FPGA Control — Full FPGA register control panel (all 22 opcodes, + bit-width validation, grouped layout matching production) + 4. Diagnostics — Connection indicators, packet stats, dependency status, + self-test results, log viewer + 5. Settings — Host-side DSP parameters + About section -Integrates: hardware interfaces, QThread workers, TargetSimulator, RadarMapWidget. +Uses production radar_protocol.py for all FPGA communication: + - FT2232HConnection for real hardware + - ReplayConnection for offline .npy replay + - Mock mode (FT2232HConnection(mock=True)) for development + +The old STM32 magic-packet start flow has been removed. FPGA registers +are controlled directly via 4-byte {opcode, addr, value_hi, value_lo} +commands sent over FT2232H. """ import time import logging -from typing import List, Optional import numpy as np from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, - QTabWidget, QSplitter, QGroupBox, QFrame, + QTabWidget, QSplitter, QGroupBox, QFrame, QScrollArea, QLabel, QPushButton, QComboBox, QCheckBox, - QDoubleSpinBox, QSpinBox, + QDoubleSpinBox, QSpinBox, QLineEdit, QTableWidget, QTableWidgetItem, QHeaderView, QPlainTextEdit, QStatusBar, QMessageBox, ) from PyQt6.QtCore import Qt, QTimer, pyqtSlot -from PyQt6.QtGui import QColor from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.figure import Figure @@ -37,34 +46,46 @@ from .models import ( DARK_TREEVIEW, DARK_TREEVIEW_ALT, DARK_SUCCESS, DARK_WARNING, DARK_ERROR, DARK_INFO, USB_AVAILABLE, FTDI_AVAILABLE, SCIPY_AVAILABLE, - SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, CRCMOD_AVAILABLE, + SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, ) -from .hardware import FT2232HQInterface, STM32USBInterface -from .processing import RadarProcessor, RadarPacketParser, USBPacketParser +from .hardware import ( + FT2232HConnection, + ReplayConnection, + RadarProtocol, + RadarFrame, + StatusResponse, + DataRecorder, + STM32USBInterface, +) +from .processing import RadarProcessor, USBPacketParser from .workers import RadarDataWorker, GPSDataWorker, TargetSimulator from .map_widget import RadarMapWidget logger = logging.getLogger(__name__) +# Frame dimensions from FPGA +NUM_RANGE_BINS = 64 +NUM_DOPPLER_BINS = 32 + # ============================================================================= # Range-Doppler Canvas (matplotlib) # ============================================================================= class RangeDopplerCanvas(FigureCanvasQTAgg): - """Matplotlib canvas showing the Range-Doppler map with dark theme.""" + """Matplotlib canvas showing the 64x32 Range-Doppler map with dark theme.""" - def __init__(self, parent=None): + def __init__(self, _parent=None): fig = Figure(figsize=(10, 6), facecolor=DARK_BG) self.ax = fig.add_subplot(111, facecolor=DARK_ACCENT) - self._data = np.zeros((1024, 32)) + self._data = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS)) self.im = self.ax.imshow( self._data, aspect="auto", cmap="hot", - extent=[0, 32, 0, 1024], origin="lower", + extent=[0, NUM_DOPPLER_BINS, 0, NUM_RANGE_BINS], origin="lower", ) - self.ax.set_title("Range-Doppler Map (Pitch Corrected)", color=DARK_FG) + self.ax.set_title("Range-Doppler Map (64x32)", color=DARK_FG) self.ax.set_xlabel("Doppler Bin", color=DARK_FG) self.ax.set_ylabel("Range Bin", color=DARK_FG) self.ax.tick_params(colors=DARK_FG) @@ -74,8 +95,9 @@ class RangeDopplerCanvas(FigureCanvasQTAgg): fig.tight_layout() super().__init__(fig) - def update_map(self, rdm: np.ndarray): - display = np.log10(rdm + 1) + def update_map(self, magnitude: np.ndarray, _detections: np.ndarray = None): + """Update the heatmap with new magnitude data.""" + display = np.log10(magnitude + 1) self.im.set_data(display) self.im.set_clim(vmin=display.min(), vmax=max(display.max(), 0.1)) self.draw_idle() @@ -86,11 +108,11 @@ class RangeDopplerCanvas(FigureCanvasQTAgg): # ============================================================================= class RadarDashboard(QMainWindow): - """Main application window with 4 tabs.""" + """Main application window with 5 tabs.""" def __init__(self, parent=None): super().__init__(parent) - self.setWindowTitle("PLFM Radar System GUI V7 — PyQt6") + self.setWindowTitle("AERIS-10 Radar System V7 — PyQt6") self.setGeometry(100, 60, 1500, 950) # ---- Core objects -------------------------------------------------- @@ -100,33 +122,36 @@ class RadarDashboard(QMainWindow): altitude=0.0, pitch=0.0, heading=0.0, timestamp=0.0, ) - # Hardware interfaces + # Hardware interfaces — production protocol + self._connection: FT2232HConnection | None = None self._stm32 = STM32USBInterface() - self._ft2232hq = FT2232HQInterface() + self._recorder = DataRecorder() # Processing self._processor = RadarProcessor() - self._radar_parser = RadarPacketParser() self._usb_parser = USBPacketParser() self._processing_config = ProcessingConfig() - # Device lists (cached for index lookup) + # Device lists self._stm32_devices: list = [] - self._ft2232hq_devices: list = [] # Workers (created on demand) - self._radar_worker: Optional[RadarDataWorker] = None - self._gps_worker: Optional[GPSDataWorker] = None - self._simulator: Optional[TargetSimulator] = None + self._radar_worker: RadarDataWorker | None = None + self._gps_worker: GPSDataWorker | None = None + self._simulator: TargetSimulator | None = None # State self._running = False self._demo_mode = False self._start_time = time.time() - self._radar_stats: dict = {} + self._current_frame: RadarFrame | None = None + self._last_status: StatusResponse | None = None + self._frame_count = 0 self._gps_packet_count = 0 - self._current_targets: List[RadarTarget] = [] - self._corrected_elevations: list = [] + self._current_targets: list[RadarTarget] = [] + + # FPGA control parameter widgets + self._param_spins: dict = {} # opcode_hex -> QSpinBox # ---- Build UI ------------------------------------------------------ self._apply_dark_theme() @@ -143,7 +168,7 @@ class RadarDashboard(QMainWindow): self._log_handler.setLevel(logging.INFO) logging.getLogger().addHandler(self._log_handler) - logger.info("RadarDashboard initialised") + logger.info("RadarDashboard initialised (production protocol)") # ===================================================================== # Dark theme @@ -280,6 +305,7 @@ class RadarDashboard(QMainWindow): self._create_main_tab() self._create_map_tab() + self._create_fpga_control_tab() self._create_diagnostics_tab() self._create_settings_tab() @@ -298,16 +324,17 @@ class RadarDashboard(QMainWindow): ctrl_layout = QGridLayout(ctrl) ctrl_layout.setContentsMargins(8, 6, 8, 6) - # Row 0: device combos & buttons - ctrl_layout.addWidget(QLabel("STM32 USB:"), 0, 0) + # Row 0: connection mode + device combos + buttons + ctrl_layout.addWidget(QLabel("Mode:"), 0, 0) + self._mode_combo = QComboBox() + self._mode_combo.addItems(["Mock", "Live FT2232H", "Replay (.npy)"]) + self._mode_combo.setCurrentIndex(0) + ctrl_layout.addWidget(self._mode_combo, 0, 1) + + ctrl_layout.addWidget(QLabel("STM32 GPS:"), 0, 2) self._stm32_combo = QComboBox() self._stm32_combo.setMinimumWidth(200) - ctrl_layout.addWidget(self._stm32_combo, 0, 1) - - ctrl_layout.addWidget(QLabel("FT2232HQ (Primary):"), 0, 2) - self._ft2232hq_combo = QComboBox() - self._ft2232hq_combo.setMinimumWidth(200) - ctrl_layout.addWidget(self._ft2232hq_combo, 0, 3) + ctrl_layout.addWidget(self._stm32_combo, 0, 3) refresh_btn = QPushButton("Refresh Devices") refresh_btn.clicked.connect(self._refresh_devices) @@ -319,7 +346,7 @@ class RadarDashboard(QMainWindow): f"QPushButton:hover {{ background-color: #66BB6A; }}" ) self._start_btn.clicked.connect(self._start_radar) - ctrl_layout.addWidget(self._start_btn, 0, 8) + ctrl_layout.addWidget(self._start_btn, 0, 7) self._stop_btn = QPushButton("Stop Radar") self._stop_btn.setEnabled(False) @@ -328,7 +355,7 @@ class RadarDashboard(QMainWindow): f"QPushButton:hover {{ background-color: #EF5350; }}" ) self._stop_btn.clicked.connect(self._stop_radar) - ctrl_layout.addWidget(self._stop_btn, 0, 9) + ctrl_layout.addWidget(self._stop_btn, 0, 8) self._demo_btn_main = QPushButton("Start Demo") self._demo_btn_main.setStyleSheet( @@ -336,18 +363,18 @@ class RadarDashboard(QMainWindow): f"QPushButton:hover {{ background-color: #42A5F5; }}" ) self._demo_btn_main.clicked.connect(self._toggle_demo_main) - ctrl_layout.addWidget(self._demo_btn_main, 0, 10) + ctrl_layout.addWidget(self._demo_btn_main, 0, 9) # Row 1: status labels self._gps_label = QLabel("GPS: Waiting for data...") - ctrl_layout.addWidget(self._gps_label, 1, 0, 1, 4) + ctrl_layout.addWidget(self._gps_label, 1, 0, 1, 3) self._pitch_label = QLabel("Pitch: --.--\u00b0") - ctrl_layout.addWidget(self._pitch_label, 1, 4, 1, 2) + ctrl_layout.addWidget(self._pitch_label, 1, 3, 1, 2) self._status_label_main = QLabel("Status: Ready") self._status_label_main.setAlignment(Qt.AlignmentFlag.AlignRight) - ctrl_layout.addWidget(self._status_label_main, 1, 6, 1, 5) + ctrl_layout.addWidget(self._status_label_main, 1, 5, 1, 5) layout.addWidget(ctrl) @@ -359,14 +386,13 @@ class RadarDashboard(QMainWindow): display_splitter.addWidget(self._rdm_canvas) # Targets table - targets_group = QGroupBox("Detected Targets (Pitch Corrected)") + targets_group = QGroupBox("Detected Targets") tg_layout = QVBoxLayout(targets_group) self._targets_table_main = QTableWidget() - self._targets_table_main.setColumnCount(7) + self._targets_table_main.setColumnCount(5) self._targets_table_main.setHorizontalHeaderLabels([ - "Track ID", "Range (m)", "Velocity (m/s)", - "Azimuth", "Raw Elev", "Corr Elev", "SNR (dB)", + "Range Bin", "Doppler Bin", "Magnitude", "SNR (dB)", "Track ID", ]) self._targets_table_main.setAlternatingRowColors(True) self._targets_table_main.setSelectionBehavior( @@ -489,7 +515,233 @@ class RadarDashboard(QMainWindow): self._tabs.addTab(tab, "Map View") # ----------------------------------------------------------------- - # TAB 3: Diagnostics + # TAB 3: FPGA Control (production register map) + # ----------------------------------------------------------------- + + def _create_fpga_control_tab(self): + """FPGA register control panel — all 22 opcodes with validation. + + Layout: 3-column scrollable: + Left: Radar Operation + Signal Processing + Diagnostics + Center: Waveform Timing + Right: Detection (CFAR) + Custom Command + """ + tab = QWidget() + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + + inner = QWidget() + outer_layout = QHBoxLayout(inner) + outer_layout.setContentsMargins(8, 8, 8, 8) + outer_layout.setSpacing(12) + + # ── Left column ────────────────────────────────────────────── + left = QWidget() + left_layout = QVBoxLayout(left) + left_layout.setContentsMargins(0, 0, 0, 0) + + # -- Radar Operation -- + grp_op = QGroupBox("Radar Operation") + op_layout = QVBoxLayout(grp_op) + + btn_mode_on = QPushButton("Radar Mode On") + btn_mode_on.clicked.connect(lambda: self._send_fpga_cmd(0x01, 1)) + op_layout.addWidget(btn_mode_on) + + btn_mode_off = QPushButton("Radar Mode Off") + btn_mode_off.clicked.connect(lambda: self._send_fpga_cmd(0x01, 0)) + op_layout.addWidget(btn_mode_off) + + btn_trigger = QPushButton("Trigger Chirp") + btn_trigger.clicked.connect(lambda: self._send_fpga_cmd(0x02, 1)) + op_layout.addWidget(btn_trigger) + + # Stream Control (3-bit mask) + self._add_fpga_param_row(op_layout, "Stream Control", 0x04, 7, 3, + "0-7, 3-bit mask, rst=7") + + btn_status = QPushButton("Request Status") + btn_status.clicked.connect(lambda: self._send_fpga_cmd(0xFF, 0)) + op_layout.addWidget(btn_status) + + left_layout.addWidget(grp_op) + + # -- Signal Processing -- + grp_sp = QGroupBox("Signal Processing") + sp_layout = QVBoxLayout(grp_sp) + + sp_params = [ + ("Detect Threshold", 0x03, 10000, 16, "0-65535, rst=10000"), + ("Gain Shift", 0x16, 0, 4, "0-15, dir+shift"), + ("MTI Enable", 0x26, 0, 1, "0=off, 1=on"), + ("DC Notch Width", 0x27, 0, 3, "0-7 bins"), + ] + for label, opcode, default, bits, hint in sp_params: + self._add_fpga_param_row(sp_layout, label, opcode, default, bits, hint) + + # MTI quick toggles + mti_row = QHBoxLayout() + btn_mti_on = QPushButton("Enable MTI") + btn_mti_on.clicked.connect(lambda: self._send_fpga_cmd(0x26, 1)) + mti_row.addWidget(btn_mti_on) + btn_mti_off = QPushButton("Disable MTI") + btn_mti_off.clicked.connect(lambda: self._send_fpga_cmd(0x26, 0)) + mti_row.addWidget(btn_mti_off) + sp_layout.addLayout(mti_row) + + left_layout.addWidget(grp_sp) + + # -- Diagnostics -- + grp_diag = QGroupBox("Diagnostics") + diag_layout = QVBoxLayout(grp_diag) + + btn_selftest = QPushButton("Run Self-Test") + btn_selftest.clicked.connect(lambda: self._send_fpga_cmd(0x30, 1)) + diag_layout.addWidget(btn_selftest) + + btn_selftest_read = QPushButton("Read Self-Test Result") + btn_selftest_read.clicked.connect(lambda: self._send_fpga_cmd(0x31, 0)) + diag_layout.addWidget(btn_selftest_read) + + # Self-test result labels + st_group = QGroupBox("Self-Test Results") + st_layout = QVBoxLayout(st_group) + self._st_labels = {} + for name, default_text in [ + ("busy", "Busy: --"), + ("flags", "Flags: -----"), + ("detail", "Detail: 0x--"), + ("t0", "T0 BRAM: --"), + ("t1", "T1 CIC: --"), + ("t2", "T2 FFT: --"), + ("t3", "T3 Arith: --"), + ("t4", "T4 ADC: --"), + ]: + lbl = QLabel(default_text) + lbl.setStyleSheet("font-family: 'Courier New', monospace; font-size: 11px;") + st_layout.addWidget(lbl) + self._st_labels[name] = lbl + diag_layout.addWidget(st_group) + + left_layout.addWidget(grp_diag) + left_layout.addStretch() + outer_layout.addWidget(left, stretch=1) + + # ── Center column: Waveform Timing ──────────────────────────── + center = QWidget() + center_layout = QVBoxLayout(center) + center_layout.setContentsMargins(0, 0, 0, 0) + + grp_wf = QGroupBox("Waveform Timing") + wf_layout = QVBoxLayout(grp_wf) + + wf_params = [ + ("Long Chirp Cycles", 0x10, 3000, 16, "0-65535, rst=3000"), + ("Long Listen Cycles", 0x11, 13700, 16, "0-65535, rst=13700"), + ("Guard Cycles", 0x12, 17540, 16, "0-65535, rst=17540"), + ("Short Chirp Cycles", 0x13, 50, 16, "0-65535, rst=50"), + ("Short Listen Cycles", 0x14, 17450, 16, "0-65535, rst=17450"), + ("Chirps Per Elevation", 0x15, 32, 6, "1-32, clamped"), + ] + for label, opcode, default, bits, hint in wf_params: + self._add_fpga_param_row(wf_layout, label, opcode, default, bits, hint) + + center_layout.addWidget(grp_wf) + center_layout.addStretch() + outer_layout.addWidget(center, stretch=1) + + # ── Right column: Detection (CFAR) + Custom Command ─────────── + right = QWidget() + right_layout = QVBoxLayout(right) + right_layout.setContentsMargins(0, 0, 0, 0) + + grp_cfar = QGroupBox("Detection (CFAR)") + cfar_layout = QVBoxLayout(grp_cfar) + + cfar_params = [ + ("CFAR Enable", 0x25, 0, 1, "0=off, 1=on"), + ("CFAR Guard Cells", 0x21, 2, 4, "0-15, rst=2"), + ("CFAR Train Cells", 0x22, 8, 5, "1-31, rst=8"), + ("CFAR Alpha (Q4.4)", 0x23, 48, 8, "0-255, rst=0x30=3.0"), + ("CFAR Mode", 0x24, 0, 2, "0=CA 1=GO 2=SO"), + ] + for label, opcode, default, bits, hint in cfar_params: + self._add_fpga_param_row(cfar_layout, label, opcode, default, bits, hint) + + # CFAR quick toggles + cfar_row = QHBoxLayout() + btn_cfar_on = QPushButton("Enable CFAR") + btn_cfar_on.clicked.connect(lambda: self._send_fpga_cmd(0x25, 1)) + cfar_row.addWidget(btn_cfar_on) + btn_cfar_off = QPushButton("Disable CFAR") + btn_cfar_off.clicked.connect(lambda: self._send_fpga_cmd(0x25, 0)) + cfar_row.addWidget(btn_cfar_off) + cfar_layout.addLayout(cfar_row) + + right_layout.addWidget(grp_cfar) + + # Custom Command + grp_custom = QGroupBox("Custom Command") + cust_layout = QGridLayout(grp_custom) + + cust_layout.addWidget(QLabel("Opcode (hex):"), 0, 0) + self._custom_opcode = QLineEdit("01") + self._custom_opcode.setMaximumWidth(80) + cust_layout.addWidget(self._custom_opcode, 0, 1) + + cust_layout.addWidget(QLabel("Value (dec):"), 1, 0) + self._custom_value = QLineEdit("0") + self._custom_value.setMaximumWidth(80) + cust_layout.addWidget(self._custom_value, 1, 1) + + btn_send_custom = QPushButton("Send") + btn_send_custom.clicked.connect(self._send_custom_command) + cust_layout.addWidget(btn_send_custom, 2, 0, 1, 2) + + right_layout.addWidget(grp_custom) + right_layout.addStretch() + outer_layout.addWidget(right, stretch=1) + + scroll.setWidget(inner) + tab_layout = QVBoxLayout(tab) + tab_layout.setContentsMargins(0, 0, 0, 0) + tab_layout.addWidget(scroll) + + self._tabs.addTab(tab, "FPGA Control") + + def _add_fpga_param_row(self, parent_layout: QVBoxLayout, label: str, + opcode: int, default: int, bits: int, hint: str): + """Add a single FPGA parameter row: label + spinbox + hint + Set button.""" + row = QHBoxLayout() + + lbl = QLabel(label) + lbl.setMinimumWidth(140) + row.addWidget(lbl) + + max_val = (1 << bits) - 1 + spin = QSpinBox() + spin.setRange(0, max_val) + spin.setValue(default) + spin.setMinimumWidth(80) + row.addWidget(spin) + self._param_spins[f"0x{opcode:02X}"] = spin + + hint_lbl = QLabel(hint) + hint_lbl.setStyleSheet(f"color: {DARK_INFO}; font-size: 10px;") + row.addWidget(hint_lbl) + + btn = QPushButton("Set") + btn.setMaximumWidth(60) + # Capture opcode and spin by value + btn.clicked.connect(lambda _, op=opcode, sp=spin, b=bits: + self._send_fpga_validated(op, sp.value(), b)) + row.addWidget(btn) + + parent_layout.addLayout(row) + + # ----------------------------------------------------------------- + # TAB 4: Diagnostics # ----------------------------------------------------------------- def _create_diagnostics_tab(self): @@ -503,24 +755,23 @@ class RadarDashboard(QMainWindow): conn_group = QGroupBox("Connection Status") conn_layout = QGridLayout(conn_group) + self._conn_ft2232h = self._make_status_label("FT2232H") self._conn_stm32 = self._make_status_label("STM32 USB") - self._conn_ft2232hq = self._make_status_label("FT2232HQ (Primary)") - conn_layout.addWidget(QLabel("STM32 USB:"), 0, 0) - conn_layout.addWidget(self._conn_stm32, 0, 1) - conn_layout.addWidget(QLabel("FT2232HQ:"), 1, 0) - conn_layout.addWidget(self._conn_ft2232hq, 1, 1) + conn_layout.addWidget(QLabel("FT2232H:"), 0, 0) + conn_layout.addWidget(self._conn_ft2232h, 0, 1) + conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0) + conn_layout.addWidget(self._conn_stm32, 1, 1) top_row.addWidget(conn_group) - # Packet statistics - stats_group = QGroupBox("Packet Statistics") + # Frame statistics + stats_group = QGroupBox("Statistics") stats_layout = QGridLayout(stats_group) labels = [ - "Radar Packets:", "Bytes Received:", "GPS Packets:", - "Errors:", "Active Tracks:", "Detected Targets:", - "Uptime:", "Packet Rate:", + "Frames:", "Detections:", "GPS Packets:", + "Errors:", "Uptime:", "Frame Rate:", ] self._diag_values: list = [] for i, text in enumerate(labels): @@ -533,6 +784,17 @@ class RadarDashboard(QMainWindow): top_row.addWidget(stats_group) + # FPGA Status readback + fpga_group = QGroupBox("FPGA Status Readback") + fpga_layout = QVBoxLayout(fpga_group) + self._fpga_status_label = QLabel("No status received yet") + self._fpga_status_label.setWordWrap(True) + self._fpga_status_label.setStyleSheet( + "font-family: 'Courier New', monospace; font-size: 11px; padding: 4px;") + fpga_layout.addWidget(self._fpga_status_label) + + top_row.addWidget(fpga_group) + # Dependency status dep_group = QGroupBox("Optional Dependencies") dep_layout = QGridLayout(dep_group) @@ -543,7 +805,6 @@ class RadarDashboard(QMainWindow): ("scipy", SCIPY_AVAILABLE), ("sklearn", SKLEARN_AVAILABLE), ("filterpy", FILTERPY_AVAILABLE), - ("crcmod", CRCMOD_AVAILABLE), ] for i, (name, avail) in enumerate(deps): dep_layout.addWidget(QLabel(name), i, 0) @@ -577,12 +838,10 @@ class RadarDashboard(QMainWindow): self._tabs.addTab(tab, "Diagnostics") # ----------------------------------------------------------------- - # TAB 4: Settings + # TAB 5: Settings (host-side DSP) # ----------------------------------------------------------------- def _create_settings_tab(self): - from PyQt6.QtWidgets import QScrollArea - tab = QWidget() scroll = QScrollArea() scroll.setWidgetResizable(True) @@ -592,183 +851,22 @@ class RadarDashboard(QMainWindow): layout = QVBoxLayout(inner) layout.setContentsMargins(8, 8, 8, 8) - # ---- Radar parameters group ---------------------------------------- - radar_group = QGroupBox("Radar Parameters") - r_layout = QGridLayout(radar_group) - - self._setting_spins: dict = {} - param_defs = [ - ("System Frequency (GHz):", "system_frequency", 1, 100, 2, - self._settings.system_frequency / 1e9, " GHz"), - ("Chirp Duration 1 (us):", "chirp_duration_1", 0.01, 10000, 2, - self._settings.chirp_duration_1 * 1e6, " us"), - ("Chirp Duration 2 (us):", "chirp_duration_2", 0.001, 10000, 3, - self._settings.chirp_duration_2 * 1e6, " us"), - ("Chirps per Position:", "chirps_per_position", 1, 1024, 0, - self._settings.chirps_per_position, ""), - ("Freq Min (MHz):", "freq_min", 0.1, 1000, 1, - self._settings.freq_min / 1e6, " MHz"), - ("Freq Max (MHz):", "freq_max", 0.1, 1000, 1, - self._settings.freq_max / 1e6, " MHz"), - ("PRF 1 (Hz):", "prf1", 100, 100000, 0, - self._settings.prf1, " Hz"), - ("PRF 2 (Hz):", "prf2", 100, 100000, 0, - self._settings.prf2, " Hz"), - ("Max Distance (km):", "max_distance", 1, 500, 1, - self._settings.max_distance / 1000, " km"), - ("Map Size (km):", "map_size", 1, 500, 1, - self._settings.map_size / 1000, " km"), - ] - - for i, (label, key, lo, hi, dec, default, suffix) in enumerate(param_defs): - r_layout.addWidget(QLabel(label), i, 0) - if dec == 0: - spin = QSpinBox() - spin.setRange(int(lo), int(hi)) - spin.setValue(int(default)) - if suffix: - spin.setSuffix(suffix) - else: - spin = QDoubleSpinBox() - spin.setRange(lo, hi) - spin.setDecimals(dec) - spin.setValue(default) - if suffix: - spin.setSuffix(suffix) - r_layout.addWidget(spin, i, 1) - self._setting_spins[key] = spin - - apply_btn = QPushButton("Apply Settings") - apply_btn.setStyleSheet( - f"QPushButton {{ background-color: {DARK_INFO}; color: white; font-weight: bold; }}" - ) - apply_btn.clicked.connect(self._apply_settings) - r_layout.addWidget(apply_btn, len(param_defs), 0, 1, 2) - - layout.addWidget(radar_group) - - # ---- Signal Processing group --------------------------------------- - proc_group = QGroupBox("Signal Processing") + # ---- Host-side DSP group ------------------------------------------- + proc_group = QGroupBox("Host-Side Signal Processing (post-FPGA)") p_layout = QGridLayout(proc_group) row = 0 - # -- MTI -- - self._mti_check = QCheckBox("MTI (Moving Target Indication)") - self._mti_check.setChecked(self._processing_config.mti_enabled) - p_layout.addWidget(self._mti_check, row, 0, 1, 2) - row += 1 - - p_layout.addWidget(QLabel("MTI Order:"), row, 0) - self._mti_order_spin = QSpinBox() - self._mti_order_spin.setRange(1, 3) - self._mti_order_spin.setValue(self._processing_config.mti_order) - self._mti_order_spin.setToolTip("1 = single canceller, 2 = double, 3 = triple") - p_layout.addWidget(self._mti_order_spin, row, 1) - row += 1 - - # -- Separator -- - sep1 = QFrame() - sep1.setFrameShape(QFrame.Shape.HLine) - sep1.setStyleSheet(f"color: {DARK_BORDER};") - p_layout.addWidget(sep1, row, 0, 1, 2) - row += 1 - - # -- CFAR -- - self._cfar_check = QCheckBox("CFAR (Constant False Alarm Rate)") - self._cfar_check.setChecked(self._processing_config.cfar_enabled) - p_layout.addWidget(self._cfar_check, row, 0, 1, 2) - row += 1 - - p_layout.addWidget(QLabel("CFAR Type:"), row, 0) - self._cfar_type_combo = QComboBox() - self._cfar_type_combo.addItems(["CA-CFAR", "OS-CFAR", "GO-CFAR", "SO-CFAR"]) - self._cfar_type_combo.setCurrentText(self._processing_config.cfar_type) - p_layout.addWidget(self._cfar_type_combo, row, 1) - row += 1 - - p_layout.addWidget(QLabel("Guard Cells:"), row, 0) - self._cfar_guard_spin = QSpinBox() - self._cfar_guard_spin.setRange(1, 20) - self._cfar_guard_spin.setValue(self._processing_config.cfar_guard_cells) - p_layout.addWidget(self._cfar_guard_spin, row, 1) - row += 1 - - p_layout.addWidget(QLabel("Training Cells:"), row, 0) - self._cfar_train_spin = QSpinBox() - self._cfar_train_spin.setRange(1, 50) - self._cfar_train_spin.setValue(self._processing_config.cfar_training_cells) - p_layout.addWidget(self._cfar_train_spin, row, 1) - row += 1 - - p_layout.addWidget(QLabel("Threshold Factor:"), row, 0) - self._cfar_thresh_spin = QDoubleSpinBox() - self._cfar_thresh_spin.setRange(0.1, 50.0) - self._cfar_thresh_spin.setDecimals(1) - self._cfar_thresh_spin.setValue(self._processing_config.cfar_threshold_factor) - self._cfar_thresh_spin.setSingleStep(0.5) - p_layout.addWidget(self._cfar_thresh_spin, row, 1) - row += 1 - - # -- Separator -- - sep2 = QFrame() - sep2.setFrameShape(QFrame.Shape.HLine) - sep2.setStyleSheet(f"color: {DARK_BORDER};") - p_layout.addWidget(sep2, row, 0, 1, 2) - row += 1 - - # -- DC Notch -- - self._dc_notch_check = QCheckBox("DC Notch / Zero-Doppler Removal") - self._dc_notch_check.setChecked(self._processing_config.dc_notch_enabled) - p_layout.addWidget(self._dc_notch_check, row, 0, 1, 2) - row += 1 - - # -- Separator -- - sep3 = QFrame() - sep3.setFrameShape(QFrame.Shape.HLine) - sep3.setStyleSheet(f"color: {DARK_BORDER};") - p_layout.addWidget(sep3, row, 0, 1, 2) - row += 1 - - # -- Windowing -- - p_layout.addWidget(QLabel("Window Function:"), row, 0) - self._window_combo = QComboBox() - self._window_combo.addItems(["None", "Hann", "Hamming", "Blackman", "Kaiser", "Chebyshev"]) - self._window_combo.setCurrentText(self._processing_config.window_type) - if not SCIPY_AVAILABLE: - # Without scipy, only None/Hann/Hamming/Blackman via numpy - self._window_combo.setToolTip("Kaiser and Chebyshev require scipy") - p_layout.addWidget(self._window_combo, row, 1) - row += 1 - - # -- Separator -- - sep4 = QFrame() - sep4.setFrameShape(QFrame.Shape.HLine) - sep4.setStyleSheet(f"color: {DARK_BORDER};") - p_layout.addWidget(sep4, row, 0, 1, 2) - row += 1 - - # -- Detection Threshold -- - p_layout.addWidget(QLabel("Detection Threshold (dB):"), row, 0) - self._det_thresh_spin = QDoubleSpinBox() - self._det_thresh_spin.setRange(0.0, 60.0) - self._det_thresh_spin.setDecimals(1) - self._det_thresh_spin.setValue(self._processing_config.detection_threshold_db) - self._det_thresh_spin.setSuffix(" dB") - self._det_thresh_spin.setSingleStep(1.0) - self._det_thresh_spin.setToolTip( - "SNR threshold above noise floor (used when CFAR is disabled)" + note = QLabel( + "These settings control host-side DSP that runs AFTER the FPGA " + "processing pipeline. FPGA-side MTI, CFAR, and DC notch are " + "controlled from the FPGA Control tab." ) - p_layout.addWidget(self._det_thresh_spin, row, 1) + note.setWordWrap(True) + note.setStyleSheet(f"color: {DARK_WARNING}; padding: 6px;") + p_layout.addWidget(note, row, 0, 1, 2) row += 1 - # -- Separator -- - sep5 = QFrame() - sep5.setFrameShape(QFrame.Shape.HLine) - sep5.setStyleSheet(f"color: {DARK_BORDER};") - p_layout.addWidget(sep5, row, 0, 1, 2) - row += 1 - - # -- Clustering -- + # Clustering self._cluster_check = QCheckBox("DBSCAN Clustering") self._cluster_check.setChecked(self._processing_config.clustering_enabled) if not SKLEARN_AVAILABLE: @@ -793,14 +891,14 @@ class RadarDashboard(QMainWindow): p_layout.addWidget(self._cluster_min_spin, row, 1) row += 1 - # -- Separator -- - sep6 = QFrame() - sep6.setFrameShape(QFrame.Shape.HLine) - sep6.setStyleSheet(f"color: {DARK_BORDER};") - p_layout.addWidget(sep6, row, 0, 1, 2) + # Separator + sep = QFrame() + sep.setFrameShape(QFrame.Shape.HLine) + sep.setStyleSheet(f"color: {DARK_BORDER};") + p_layout.addWidget(sep, row, 0, 1, 2) row += 1 - # -- Kalman Tracking -- + # Kalman Tracking self._tracking_check = QCheckBox("Kalman Tracking") self._tracking_check.setChecked(self._processing_config.tracking_enabled) if not FILTERPY_AVAILABLE: @@ -809,8 +907,8 @@ class RadarDashboard(QMainWindow): p_layout.addWidget(self._tracking_check, row, 0, 1, 2) row += 1 - # Apply Processing button - apply_proc_btn = QPushButton("Apply Processing Settings") + # Apply + apply_proc_btn = QPushButton("Apply Host DSP Settings") apply_proc_btn.setStyleSheet( f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}" f"QPushButton:hover {{ background-color: #66BB6A; }}" @@ -824,12 +922,13 @@ class RadarDashboard(QMainWindow): about_group = QGroupBox("About") about_layout = QVBoxLayout(about_group) about_lbl = QLabel( - "PLFM Radar System GUI V7
" + "AERIS-10 Radar System V7
" "PyQt6 Edition with Embedded Leaflet Map

" - "Data Interface: FT2232HQ (USB 2.0)
" + "Data Interface: FT2232H USB 2.0 (production protocol)
" + "FPGA Protocol: 4-byte register commands, 0xAA/0xBB packets
" "Map: OpenStreetMap + Leaflet.js
" "Framework: PyQt6 + QWebEngine
" - "Version: 7.0.0" + "Version: 7.1.0 (production protocol)" ) about_lbl.setStyleSheet(f"color: {DARK_TEXT}; padding: 12px;") about_layout.addWidget(about_lbl) @@ -867,7 +966,7 @@ class RadarDashboard(QMainWindow): # ===================================================================== def _refresh_devices(self): - # STM32 + # STM32 GPS self._stm32_devices = self._stm32.list_devices() self._stm32_combo.clear() for d in self._stm32_devices: @@ -875,84 +974,121 @@ class RadarDashboard(QMainWindow): if self._stm32_devices: self._stm32_combo.setCurrentIndex(0) - # FT2232HQ (primary) - self._ft2232hq_devices = self._ft2232hq.list_devices() - self._ft2232hq_combo.clear() - for d in self._ft2232hq_devices: - self._ft2232hq_combo.addItem(d["description"]) - if self._ft2232hq_devices: - self._ft2232hq_combo.setCurrentIndex(0) + logger.info(f"Devices refreshed: {len(self._stm32_devices)} STM32") - logger.info( - f"Devices refreshed: {len(self._stm32_devices)} STM32, " - f"{len(self._ft2232hq_devices)} FT2232HQ" - ) + # ===================================================================== + # FPGA command sending + # ===================================================================== + + def _send_fpga_cmd(self, opcode: int, value: int): + """Send a 4-byte register command to the FPGA via FT2232H.""" + if self._connection is None or not self._connection.is_open: + logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection") + return + cmd = RadarProtocol.build_command(opcode, value) + ok = self._connection.write(cmd) + if ok: + logger.info(f"Sent FPGA cmd: 0x{opcode:02X} = {value}") + else: + logger.error(f"Failed to send FPGA cmd: 0x{opcode:02X}") + + def _send_fpga_validated(self, opcode: int, value: int, bits: int): + """Clamp value to bit-width and send.""" + max_val = (1 << bits) - 1 + clamped = max(0, min(value, max_val)) + if clamped != value: + logger.warning(f"Value {value} clamped to {clamped} " + f"({bits}-bit max={max_val}) for opcode 0x{opcode:02X}") + # Update the spinbox + key = f"0x{opcode:02X}" + if key in self._param_spins: + self._param_spins[key].setValue(clamped) + self._send_fpga_cmd(opcode, clamped) + + def _send_custom_command(self): + """Send custom opcode + value from the FPGA Control tab.""" + try: + opcode = int(self._custom_opcode.text(), 16) + value = int(self._custom_value.text()) + self._send_fpga_cmd(opcode, value) + except ValueError: + logger.error("Invalid custom command: check opcode (hex) and value (dec)") # ===================================================================== # Start / Stop radar # ===================================================================== def _start_radar(self): + """Start radar data acquisition using production protocol.""" try: - # Open STM32 - idx = self._stm32_combo.currentIndex() - if idx < 0 or idx >= len(self._stm32_devices): - QMessageBox.warning(self, "Warning", "Please select an STM32 USB device.") - return - if not self._stm32.open_device(self._stm32_devices[idx]): - QMessageBox.critical(self, "Error", "Failed to open STM32 USB device.") + mode = self._mode_combo.currentText() + + if "Mock" in mode: + self._connection = FT2232HConnection(mock=True) + if not self._connection.open(): + QMessageBox.critical(self, "Error", "Failed to open mock connection.") + return + elif "Live" in mode: + self._connection = FT2232HConnection(mock=False) + if not self._connection.open(): + QMessageBox.critical(self, "Error", + "Failed to open FT2232H. Check USB connection.") + return + elif "Replay" in mode: + from PyQt6.QtWidgets import QFileDialog + npy_dir = QFileDialog.getExistingDirectory( + self, "Select .npy replay directory") + if not npy_dir: + return + self._connection = ReplayConnection(npy_dir) + if not self._connection.open(): + QMessageBox.critical(self, "Error", + "Failed to open replay connection.") + return + else: + QMessageBox.warning(self, "Warning", "Unknown connection mode.") return - # Open FT2232HQ (primary) - idx2 = self._ft2232hq_combo.currentIndex() - if idx2 >= 0 and idx2 < len(self._ft2232hq_devices): - url = self._ft2232hq_devices[idx2]["url"] - if not self._ft2232hq.open_device(url): - QMessageBox.warning( - self, - "Warning", - "Failed to open FT2232HQ device. Radar data may not be available.", - ) - - # Send start flag + settings - if not self._stm32.send_start_flag(): - QMessageBox.critical(self, "Error", "Failed to send start flag to STM32.") - return - self._apply_settings_to_model() - self._stm32.send_settings(self._settings) - - # Start workers + # Start radar worker self._radar_worker = RadarDataWorker( - ft2232hq=self._ft2232hq, + connection=self._connection, processor=self._processor, - packet_parser=self._radar_parser, - settings=self._settings, + recorder=self._recorder if self._recorder.recording else None, gps_data_ref=self._radar_position, + settings=self._settings, ) + self._radar_worker.frameReady.connect(self._on_frame_ready) + self._radar_worker.statusReceived.connect(self._on_status_received) self._radar_worker.targetsUpdated.connect(self._on_radar_targets) self._radar_worker.statsUpdated.connect(self._on_radar_stats) self._radar_worker.errorOccurred.connect(self._on_worker_error) self._radar_worker.start() - self._gps_worker = GPSDataWorker( - stm32=self._stm32, - usb_parser=self._usb_parser, - ) - self._gps_worker.gpsReceived.connect(self._on_gps_received) - self._gps_worker.errorOccurred.connect(self._on_worker_error) - self._gps_worker.start() + # Optionally start GPS worker + idx = self._stm32_combo.currentIndex() + if (idx >= 0 and idx < len(self._stm32_devices) + and self._stm32.open_device(self._stm32_devices[idx])): + self._gps_worker = GPSDataWorker( + stm32=self._stm32, + usb_parser=self._usb_parser, + ) + self._gps_worker.gpsReceived.connect(self._on_gps_received) + self._gps_worker.errorOccurred.connect(self._on_worker_error) + self._gps_worker.start() # UI state self._running = True self._start_time = time.time() + self._frame_count = 0 self._start_btn.setEnabled(False) self._stop_btn.setEnabled(True) - self._status_label_main.setText("Status: Radar running") - self._sb_status.setText("Radar running") - self._sb_mode.setText("Live") - logger.info("Radar system started") + self._mode_combo.setEnabled(False) + self._status_label_main.setText(f"Status: Running ({mode})") + self._sb_status.setText(f"Running ({mode})") + self._sb_mode.setText(mode) + logger.info(f"Radar started: {mode}") - except Exception as e: + except RuntimeError as e: QMessageBox.critical(self, "Error", f"Failed to start radar: {e}") logger.error(f"Start radar error: {e}") @@ -969,11 +1105,15 @@ class RadarDashboard(QMainWindow): self._gps_worker.wait(2000) self._gps_worker = None + if self._connection: + self._connection.close() + self._connection = None + self._stm32.close() - self._ft2232hq.close() self._start_btn.setEnabled(True) self._stop_btn.setEnabled(False) + self._mode_combo.setEnabled(True) self._status_label_main.setText("Status: Radar stopped") self._sb_status.setText("Radar stopped") self._sb_mode.setText("Idle") @@ -1030,6 +1170,18 @@ class RadarDashboard(QMainWindow): # Slots — data from workers / simulator # ===================================================================== + @pyqtSlot(object) + def _on_frame_ready(self, frame: RadarFrame): + """Handle a complete 64x32 radar frame from production acquisition.""" + self._current_frame = frame + self._frame_count += 1 + + @pyqtSlot(object) + def _on_status_received(self, status: StatusResponse): + """Handle FPGA status readback.""" + self._last_status = status + self._update_status_display(status) + @pyqtSlot(list) def _on_radar_targets(self, targets: list): self._current_targets = targets @@ -1037,7 +1189,7 @@ class RadarDashboard(QMainWindow): @pyqtSlot(dict) def _on_radar_stats(self, stats: dict): - self._radar_stats = stats + pass # Stats are displayed in _refresh_gui @pyqtSlot(str) def _on_worker_error(self, msg: str): @@ -1087,6 +1239,43 @@ class RadarDashboard(QMainWindow): ) self._target_info_label.setText(info) + # ===================================================================== + # FPGA Status display + # ===================================================================== + + def _update_status_display(self, st: StatusResponse): + """Update FPGA status readback labels.""" + # Diagnostics tab + lines = [ + f"Mode: {st.radar_mode} Stream: {st.stream_ctrl:03b} " + f"Thresh: {st.cfar_threshold}", + f"Long Chirp: {st.long_chirp} Listen: {st.long_listen}", + f"Guard: {st.guard} Short Chirp: {st.short_chirp} " + f"Listen: {st.short_listen}", + f"Chirps/Elev: {st.chirps_per_elev} Range Mode: {st.range_mode}", + ] + self._fpga_status_label.setText("\n".join(lines)) + + # Self-test labels + if st.self_test_busy or st.self_test_flags: + flags = st.self_test_flags + self._st_labels["busy"].setText( + f"Busy: {'YES' if st.self_test_busy else 'no'}") + self._st_labels["flags"].setText( + f"Flags: {flags:05b}") + self._st_labels["detail"].setText( + f"Detail: 0x{st.self_test_detail:02X}") + self._st_labels["t0"].setText( + f"T0 BRAM: {'PASS' if flags & 0x01 else 'FAIL'}") + self._st_labels["t1"].setText( + f"T1 CIC: {'PASS' if flags & 0x02 else 'FAIL'}") + self._st_labels["t2"].setText( + f"T2 FFT: {'PASS' if flags & 0x04 else 'FAIL'}") + self._st_labels["t3"].setText( + f"T3 Arith: {'PASS' if flags & 0x08 else 'FAIL'}") + self._st_labels["t4"].setText( + f"T4 ADC: {'PASS' if flags & 0x10 else 'FAIL'}") + # ===================================================================== # Position / coverage callbacks (map sidebar) # ===================================================================== @@ -1108,46 +1297,10 @@ class RadarDashboard(QMainWindow): # Settings # ===================================================================== - def _apply_settings_to_model(self): - """Read spin values into the RadarSettings model.""" - s = self._settings - sp = self._setting_spins - s.system_frequency = sp["system_frequency"].value() * 1e9 - s.chirp_duration_1 = sp["chirp_duration_1"].value() * 1e-6 - s.chirp_duration_2 = sp["chirp_duration_2"].value() * 1e-6 - s.chirps_per_position = int(sp["chirps_per_position"].value()) - s.freq_min = sp["freq_min"].value() * 1e6 - s.freq_max = sp["freq_max"].value() * 1e6 - s.prf1 = sp["prf1"].value() - s.prf2 = sp["prf2"].value() - s.max_distance = sp["max_distance"].value() * 1000 - s.map_size = sp["map_size"].value() * 1000 - - def _apply_settings(self): - try: - self._apply_settings_to_model() - if self._stm32.is_open: - self._stm32.send_settings(self._settings) - logger.info("Radar settings applied") - QMessageBox.information(self, "Settings", "Radar settings applied.") - except Exception as e: - QMessageBox.critical(self, "Error", f"Invalid setting value: {e}") - logger.error(f"Settings error: {e}") - def _apply_processing_config(self): - """Read signal processing controls into ProcessingConfig and push to processor.""" + """Read host-side DSP controls into ProcessingConfig.""" try: cfg = ProcessingConfig( - mti_enabled=self._mti_check.isChecked(), - mti_order=self._mti_order_spin.value(), - cfar_enabled=self._cfar_check.isChecked(), - cfar_type=self._cfar_type_combo.currentText(), - cfar_guard_cells=self._cfar_guard_spin.value(), - cfar_training_cells=self._cfar_train_spin.value(), - cfar_threshold_factor=self._cfar_thresh_spin.value(), - dc_notch_enabled=self._dc_notch_check.isChecked(), - window_type=self._window_combo.currentText(), - detection_threshold_db=self._det_thresh_spin.value(), clustering_enabled=self._cluster_check.isChecked(), clustering_eps=self._cluster_eps_spin.value(), clustering_min_samples=self._cluster_min_spin.value(), @@ -1156,15 +1309,14 @@ class RadarDashboard(QMainWindow): self._processing_config = cfg self._processor.set_config(cfg) logger.info( - f"Processing config applied: MTI={cfg.mti_enabled}(order {cfg.mti_order}), " - f"CFAR={cfg.cfar_enabled}({cfg.cfar_type}), DC_Notch={cfg.dc_notch_enabled}, " - f"Window={cfg.window_type}, Threshold={cfg.detection_threshold_db} dB, " - f"Clustering={cfg.clustering_enabled}, Tracking={cfg.tracking_enabled}" + f"Host DSP config: Clustering={cfg.clustering_enabled}, " + f"Tracking={cfg.tracking_enabled}" ) - QMessageBox.information(self, "Processing", "Signal processing settings applied.") - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to apply processing settings: {e}") - logger.error(f"Processing config error: {e}") + QMessageBox.information(self, "Settings", "Host DSP settings applied.") + except RuntimeError as e: + QMessageBox.critical(self, "Error", + f"Failed to apply DSP settings: {e}") + logger.error(f"DSP config error: {e}") # ===================================================================== # Periodic GUI refresh (100 ms timer) @@ -1183,23 +1335,32 @@ class RadarDashboard(QMainWindow): pitch_text = f"Pitch: {gps.pitch:+.1f}\u00b0" self._pitch_label.setText(pitch_text) if abs(gps.pitch) > 10: - self._pitch_label.setStyleSheet(f"color: {DARK_ERROR}; font-weight: bold;") + self._pitch_label.setStyleSheet( + f"color: {DARK_ERROR}; font-weight: bold;") elif abs(gps.pitch) > 5: - self._pitch_label.setStyleSheet(f"color: {DARK_WARNING}; font-weight: bold;") + self._pitch_label.setStyleSheet( + f"color: {DARK_WARNING}; font-weight: bold;") else: - self._pitch_label.setStyleSheet(f"color: {DARK_SUCCESS}; font-weight: bold;") + self._pitch_label.setStyleSheet( + f"color: {DARK_SUCCESS}; font-weight: bold;") - # Range-Doppler map - self._rdm_canvas.update_map(self._processor.range_doppler_map) + # Range-Doppler map from current frame + if self._current_frame is not None: + self._rdm_canvas.update_map( + self._current_frame.magnitude, + self._current_frame.detections, + ) # Targets table (main tab) self._update_main_targets_table() # Status label (main tab) if self._running: - pkt = self._radar_stats.get("packets", 0) + det = (self._current_frame.detection_count + if self._current_frame else 0) self._status_label_main.setText( - f"Status: Running \u2014 Packets: {pkt} \u2014 Pitch: {gps.pitch:+.1f}\u00b0" + f"Status: Running \u2014 Frames: {self._frame_count} " + f"\u2014 Detections: {det}" ) # Diagnostics values @@ -1208,7 +1369,7 @@ class RadarDashboard(QMainWindow): # Status-bar target count self._sb_targets.setText(f"Targets: {len(self._current_targets)}") - except Exception as e: + except (RuntimeError, ValueError, IndexError) as e: logger.error(f"GUI refresh error: {e}") def _update_main_targets_table(self): @@ -1217,58 +1378,42 @@ class RadarDashboard(QMainWindow): for row, t in enumerate(targets): self._targets_table_main.setItem( - row, 0, QTableWidgetItem(str(t.track_id))) + row, 0, QTableWidgetItem(f"{t.range:.0f}")) self._targets_table_main.setItem( - row, 1, QTableWidgetItem(f"{t.range:.1f}")) - - vel_item = QTableWidgetItem(f"{t.velocity:+.1f}") - if t.velocity > 1: - vel_item.setForeground(QColor(DARK_ERROR)) - elif t.velocity < -1: - vel_item.setForeground(QColor(DARK_INFO)) - self._targets_table_main.setItem(row, 2, vel_item) + row, 1, QTableWidgetItem(f"{t.velocity:.0f}")) + mag_val = 10 ** (t.snr / 10) if t.snr > 0 else 0 self._targets_table_main.setItem( - row, 3, QTableWidgetItem(f"{t.azimuth:.1f}")) - - # Raw elevation — show stored value from corrections cache - raw_text = "N/A" - for corr in self._corrected_elevations[-20:]: - if abs(corr["corrected"] - t.elevation) < 0.1: - raw_text = f"{corr['raw']}" - break + row, 2, QTableWidgetItem(f"{mag_val:.0f}")) self._targets_table_main.setItem( - row, 4, QTableWidgetItem(raw_text)) + row, 3, QTableWidgetItem(f"{t.snr:.1f}")) self._targets_table_main.setItem( - row, 5, QTableWidgetItem(f"{t.elevation:.1f}")) - self._targets_table_main.setItem( - row, 6, QTableWidgetItem(f"{t.snr:.1f}")) + row, 4, QTableWidgetItem(str(t.track_id))) def _update_diagnostics(self): # Connection indicators + conn_open = (self._connection is not None and self._connection.is_open) + self._set_conn_indicator(self._conn_ft2232h, conn_open) self._set_conn_indicator(self._conn_stm32, self._stm32.is_open) - self._set_conn_indicator(self._conn_ft2232hq, self._ft2232hq.is_open) - stats = self._radar_stats gps_count = self._gps_packet_count if self._gps_worker: gps_count = self._gps_worker.gps_count uptime = time.time() - self._start_time - pkt = stats.get("packets", 0) - pkt_rate = pkt / max(uptime, 1) + frame_rate = self._frame_count / max(uptime, 1) + det = (self._current_frame.detection_count + if self._current_frame else 0) vals = [ - str(pkt), - f"{stats.get('bytes', 0):,}", + str(self._frame_count), + str(det), str(gps_count), - str(stats.get("errors", 0)), - str(stats.get("active_tracks", len(self._processor.tracks))), - str(stats.get("targets", len(self._current_targets))), + "0", # errors f"{uptime:.0f}s", - f"{pkt_rate:.1f}/s", + f"{frame_rate:.1f}/s", ] - for lbl, v in zip(self._diag_values, vals): + for lbl, v in zip(self._diag_values, vals, strict=False): lbl.setText(v) # ===================================================================== @@ -1276,7 +1421,7 @@ class RadarDashboard(QMainWindow): # ===================================================================== @staticmethod - def _make_status_label(name: str) -> QLabel: + def _make_status_label(_name: str) -> QLabel: lbl = QLabel("Disconnected") lbl.setStyleSheet(f"color: {DARK_ERROR}; font-weight: bold;") return lbl @@ -1307,14 +1452,15 @@ class RadarDashboard(QMainWindow): if self._gps_worker: self._gps_worker.stop() self._gps_worker.wait(1000) + if self._connection: + self._connection.close() self._stm32.close() - self._ft2232hq.close() logging.getLogger().removeHandler(self._log_handler) event.accept() # ============================================================================= -# Qt-compatible log handler (routes Python logging → QTextEdit) +# Qt-compatible log handler (routes Python logging -> QTextEdit) # ============================================================================= class _QtLogHandler(logging.Handler): @@ -1332,5 +1478,5 @@ class _QtLogHandler(logging.Handler): try: msg = self.format(record) self._callback(msg) - except Exception: + except RuntimeError: pass diff --git a/9_Firmware/9_3_GUI/v7/hardware.py b/9_Firmware/9_3_GUI/v7/hardware.py index 2b4b474..e0231ef 100644 --- a/9_Firmware/9_3_GUI/v7/hardware.py +++ b/9_Firmware/9_3_GUI/v7/hardware.py @@ -1,141 +1,62 @@ """ v7.hardware — Hardware interface classes for the PLFM Radar GUI V7. -Provides two USB hardware interfaces: - - FT2232HQInterface (PRIMARY — USB 2.0, VID 0x0403 / PID 0x6010) - - STM32USBInterface (USB CDC for commands and GPS) +Provides: + - FT2232H radar data + command interface via production radar_protocol module + - ReplayConnection for offline .npy replay via production radar_protocol module + - STM32USBInterface for GPS data only (USB CDC) + +The FT2232H interface uses the production protocol layer (radar_protocol.py) +which sends 4-byte {opcode, addr, value_hi, value_lo} register commands and +parses 0xAA data / 0xBB status packets from the FPGA. The old magic-packet +and 'SET'...'END' binary settings protocol has been removed — it was +incompatible with the FPGA register interface. """ -import struct +import sys +import os import logging -from typing import List, Dict, Optional +from typing import ClassVar -from .models import ( - USB_AVAILABLE, FTDI_AVAILABLE, - RadarSettings, -) +from .models import USB_AVAILABLE if USB_AVAILABLE: import usb.core import usb.util -if FTDI_AVAILABLE: - from pyftdi.ftdi import Ftdi - from pyftdi.usbtools import UsbTools +# Import production protocol layer — single source of truth for FPGA comms +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from radar_protocol import ( # noqa: F401 — re-exported for v7 package + FT2232HConnection, + ReplayConnection, + RadarProtocol, + Opcode, + RadarAcquisition, + RadarFrame, + StatusResponse, + DataRecorder, +) logger = logging.getLogger(__name__) # ============================================================================= -# FT2232HQ Interface — PRIMARY data path (USB 2.0) -# ============================================================================= - -class FT2232HQInterface: - """ - Interface for FT2232HQ (USB 2.0 Hi-Speed) in synchronous FIFO mode. - - This is the **primary** radar data interface. - VID/PID: 0x0403 / 0x6010 - """ - - VID = 0x0403 - PID = 0x6010 - - def __init__(self): - self.ftdi: Optional[object] = None - self.is_open: bool = False - - # ---- enumeration ------------------------------------------------------- - - def list_devices(self) -> List[Dict]: - """List available FT2232H devices using pyftdi.""" - if not FTDI_AVAILABLE: - logger.warning("pyftdi not available — cannot enumerate FT2232H devices") - return [] - - try: - devices = [] - for device_desc in UsbTools.find_all([(self.VID, self.PID)]): - devices.append({ - "description": f"FT2232H Device {device_desc}", - "url": f"ftdi://{device_desc}/1", - }) - return devices - except Exception as e: - logger.error(f"Error listing FT2232H devices: {e}") - return [] - - # ---- open / close ------------------------------------------------------ - - def open_device(self, device_url: str) -> bool: - """Open FT2232H device in synchronous FIFO mode.""" - if not FTDI_AVAILABLE: - logger.error("pyftdi not available — cannot open device") - return False - - try: - self.ftdi = Ftdi() - self.ftdi.open_from_url(device_url) - - # Synchronous FIFO mode - self.ftdi.set_bitmode(0xFF, Ftdi.BitMode.SYNCFF) - - # Low-latency timer (2 ms) - self.ftdi.set_latency_timer(2) - - # Purge stale data - self.ftdi.purge_buffers() - - self.is_open = True - logger.info(f"FT2232H device opened: {device_url}") - return True - except Exception as e: - logger.error(f"Error opening FT2232H device: {e}") - self.ftdi = None - return False - - def close(self): - """Close FT2232H device.""" - if self.ftdi and self.is_open: - try: - self.ftdi.close() - except Exception as e: - logger.error(f"Error closing FT2232H device: {e}") - finally: - self.is_open = False - self.ftdi = None - - # ---- data I/O ---------------------------------------------------------- - - def read_data(self, bytes_to_read: int = 4096) -> Optional[bytes]: - """Read data from FT2232H.""" - if not self.is_open or self.ftdi is None: - return None - - try: - data = self.ftdi.read_data(bytes_to_read) - if data: - return bytes(data) - return None - except Exception as e: - logger.error(f"Error reading from FT2232H: {e}") - return None - - -# ============================================================================= -# STM32 USB CDC Interface — commands & GPS data +# STM32 USB CDC Interface — GPS data ONLY # ============================================================================= class STM32USBInterface: """ Interface for STM32 USB CDC (Virtual COM Port). - Used to: - - Send start flag and radar settings to the MCU - - Receive GPS data from the MCU + Used ONLY for receiving GPS data from the MCU. + + FPGA register commands are sent via FT2232H (see FT2232HConnection + from radar_protocol.py). The old send_start_flag() / send_settings() + methods have been removed — they used an incompatible magic-packet + protocol that the FPGA does not understand. """ - STM32_VID_PIDS = [ + STM32_VID_PIDS: ClassVar[list[tuple[int, int]]] = [ (0x0483, 0x5740), # STM32 Virtual COM Port (0x0483, 0x3748), # STM32 Discovery (0x0483, 0x374B), @@ -152,7 +73,7 @@ class STM32USBInterface: # ---- enumeration ------------------------------------------------------- - def list_devices(self) -> List[Dict]: + def list_devices(self) -> list[dict]: """List available STM32 USB CDC devices.""" if not USB_AVAILABLE: logger.warning("pyusb not available — cannot enumerate STM32 devices") @@ -174,20 +95,20 @@ class STM32USBInterface: "product_id": pid, "device": dev, }) - except Exception: + except (usb.core.USBError, ValueError): devices.append({ "description": f"STM32 CDC (VID:{vid:04X}, PID:{pid:04X})", "vendor_id": vid, "product_id": pid, "device": dev, }) - except Exception as e: + except (usb.core.USBError, ValueError) as e: logger.error(f"Error listing STM32 devices: {e}") return devices # ---- open / close ------------------------------------------------------ - def open_device(self, device_info: Dict) -> bool: + def open_device(self, device_info: dict) -> bool: """Open STM32 USB CDC device.""" if not USB_AVAILABLE: logger.error("pyusb not available — cannot open STM32 device") @@ -225,7 +146,7 @@ class STM32USBInterface: self.is_open = True logger.info(f"STM32 USB device opened: {device_info.get('description', '')}") return True - except Exception as e: + except (usb.core.USBError, ValueError) as e: logger.error(f"Error opening STM32 device: {e}") return False @@ -234,74 +155,22 @@ class STM32USBInterface: if self.device and self.is_open: try: usb.util.dispose_resources(self.device) - except Exception as e: + except usb.core.USBError as e: logger.error(f"Error closing STM32 device: {e}") self.is_open = False self.device = None self.ep_in = None self.ep_out = None - # ---- commands ---------------------------------------------------------- + # ---- GPS data I/O ------------------------------------------------------ - def send_start_flag(self) -> bool: - """Send start flag to STM32 (4-byte magic).""" - start_packet = bytes([23, 46, 158, 237]) - logger.info("Sending start flag to STM32 via USB...") - return self._send_data(start_packet) - - def send_settings(self, settings: RadarSettings) -> bool: - """Send radar settings binary packet to STM32.""" - try: - packet = self._create_settings_packet(settings) - logger.info("Sending radar settings to STM32 via USB...") - return self._send_data(packet) - except Exception as e: - logger.error(f"Error sending settings via USB: {e}") - return False - - # ---- data I/O ---------------------------------------------------------- - - def read_data(self, size: int = 64, timeout: int = 1000) -> Optional[bytes]: - """Read data from STM32 via USB CDC.""" + def read_data(self, size: int = 64, timeout: int = 1000) -> bytes | None: + """Read GPS data from STM32 via USB CDC.""" if not self.is_open or self.ep_in is None: return None try: data = self.ep_in.read(size, timeout=timeout) return bytes(data) - except Exception: + except usb.core.USBError: # Timeout or other USB error return None - - # ---- internal helpers -------------------------------------------------- - - def _send_data(self, data: bytes) -> bool: - if not self.is_open or self.ep_out is None: - return False - try: - packet_size = 64 - for i in range(0, len(data), packet_size): - chunk = data[i : i + packet_size] - if len(chunk) < packet_size: - chunk += b"\x00" * (packet_size - len(chunk)) - self.ep_out.write(chunk) - return True - except Exception as e: - logger.error(f"Error sending data via USB: {e}") - return False - - @staticmethod - def _create_settings_packet(settings: RadarSettings) -> bytes: - """Create binary settings packet: 'SET' ... 'END'.""" - packet = b"SET" - packet += struct.pack(">d", settings.system_frequency) - packet += struct.pack(">d", settings.chirp_duration_1) - packet += struct.pack(">d", settings.chirp_duration_2) - packet += struct.pack(">I", settings.chirps_per_position) - packet += struct.pack(">d", settings.freq_min) - packet += struct.pack(">d", settings.freq_max) - packet += struct.pack(">d", settings.prf1) - packet += struct.pack(">d", settings.prf2) - packet += struct.pack(">d", settings.max_distance) - packet += struct.pack(">d", settings.map_size) - packet += b"END" - return packet diff --git a/9_Firmware/9_3_GUI/v7/map_widget.py b/9_Firmware/9_3_GUI/v7/map_widget.py index 8c481e4..08a6b04 100644 --- a/9_Firmware/9_3_GUI/v7/map_widget.py +++ b/9_Firmware/9_3_GUI/v7/map_widget.py @@ -12,7 +12,6 @@ coverage circle, target trails, velocity-based color coding, popups, legend. import json import logging -from typing import List from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QFrame, @@ -65,7 +64,7 @@ class MapBridge(QObject): @pyqtSlot(str) def logFromJS(self, message: str): - logger.debug(f"[JS] {message}") + logger.info(f"[JS] {message}") @property def is_ready(self) -> bool: @@ -96,7 +95,8 @@ class RadarMapWidget(QWidget): latitude=radar_lat, longitude=radar_lon, altitude=0.0, pitch=0.0, heading=0.0, ) - self._targets: List[RadarTarget] = [] + self._targets: list[RadarTarget] = [] + self._pending_targets: list[RadarTarget] | None = None self._coverage_radius = 50_000 # metres self._tile_server = TileServer.OPENSTREETMAP self._show_coverage = True @@ -282,15 +282,10 @@ function initMap() {{ .setView([{lat}, {lon}], 10); setTileServer('osm'); - var radarIcon = L.divIcon({{ - className:'radar-icon', - html:'
', - iconSize:[24,24], iconAnchor:[12,12] - }}); - - radarMarker = L.marker([{lat},{lon}], {{ icon:radarIcon, zIndexOffset:1000 }}).addTo(map); + radarMarker = L.circleMarker([{lat},{lon}], {{ + radius:12, fillColor:'#FF5252', color:'white', + weight:3, opacity:1, fillOpacity:1 + }}).addTo(map); updateRadarPopup(); coverageCircle = L.circle([{lat},{lon}], {{ @@ -366,102 +361,99 @@ function updateRadarPosition(lat,lon,alt,pitch,heading) {{ }} function updateTargets(targetsJson) {{ - var targets = JSON.parse(targetsJson); - var currentIds = {{}}; + try {{ + if(!map) {{ + if(bridge) bridge.logFromJS('updateTargets: map not ready yet'); + return; + }} + var targets = JSON.parse(targetsJson); + if(bridge) bridge.logFromJS('updateTargets: parsed '+targets.length+' targets'); + var currentIds = {{}}; - targets.forEach(function(t) {{ - currentIds[t.id] = true; - var lat=t.latitude, lon=t.longitude; - var color = getTargetColor(t.velocity); - var sz = Math.max(10, Math.min(20, 10+t.snr/3)); + targets.forEach(function(t) {{ + currentIds[t.id] = true; + var lat=t.latitude, lon=t.longitude; + var color = getTargetColor(t.velocity); + var radius = Math.max(5, Math.min(12, 5+(t.snr||0)/5)); - if(!targetTrailHistory[t.id]) targetTrailHistory[t.id] = []; - targetTrailHistory[t.id].push([lat,lon]); - if(targetTrailHistory[t.id].length > maxTrailLength) - targetTrailHistory[t.id].shift(); + if(!targetTrailHistory[t.id]) targetTrailHistory[t.id] = []; + targetTrailHistory[t.id].push([lat,lon]); + if(targetTrailHistory[t.id].length > maxTrailLength) + targetTrailHistory[t.id].shift(); - if(targetMarkers[t.id]) {{ - targetMarkers[t.id].setLatLng([lat,lon]); - targetMarkers[t.id].setIcon(makeIcon(color,sz)); - if(targetTrails[t.id]) {{ - targetTrails[t.id].setLatLngs(targetTrailHistory[t.id]); - targetTrails[t.id].setStyle({{ color:color }}); - }} - }} else {{ - var marker = L.marker([lat,lon], {{ icon:makeIcon(color,sz) }}).addTo(map); - marker.on( - 'click', - (function(id){{ - return function(){{ if(bridge) bridge.onMarkerClick(id); }}; - }})(t.id) - ); - targetMarkers[t.id] = marker; - if(showTrails) {{ - targetTrails[t.id] = L.polyline(targetTrailHistory[t.id], {{ - color:color, weight:3, opacity:0.7, lineCap:'round', lineJoin:'round' + if(targetMarkers[t.id]) {{ + targetMarkers[t.id].setLatLng([lat,lon]); + targetMarkers[t.id].setStyle({{ + fillColor:color, color:'white', radius:radius + }}); + if(targetTrails[t.id]) {{ + targetTrails[t.id].setLatLngs(targetTrailHistory[t.id]); + targetTrails[t.id].setStyle({{ color:color }}); + }} + }} else {{ + var marker = L.circleMarker([lat,lon], {{ + radius:radius, fillColor:color, color:'white', + weight:2, opacity:1, fillOpacity:0.9 }}).addTo(map); + marker.on( + 'click', + (function(id){{ + return function(){{ if(bridge) bridge.onMarkerClick(id); }}; + }})(t.id) + ); + targetMarkers[t.id] = marker; + if(showTrails) {{ + targetTrails[t.id] = L.polyline(targetTrailHistory[t.id], {{ + color:color, weight:3, opacity:0.7, + lineCap:'round', lineJoin:'round' + }}).addTo(map); + }} + }} + updateTargetPopup(t); + }}); + + for(var id in targetMarkers) {{ + if(!currentIds[id]) {{ + map.removeLayer(targetMarkers[id]); delete targetMarkers[id]; + if(targetTrails[id]) {{ + map.removeLayer(targetTrails[id]); + delete targetTrails[id]; + }} + delete targetTrailHistory[id]; }} }} - updateTargetPopup(t); - }}); - - for(var id in targetMarkers) {{ - if(!currentIds[id]) {{ - map.removeLayer(targetMarkers[id]); delete targetMarkers[id]; - if(targetTrails[id]) {{ map.removeLayer(targetTrails[id]); delete targetTrails[id]; }} - delete targetTrailHistory[id]; - }} + }} catch(e) {{ + if(bridge) bridge.logFromJS('updateTargets ERROR: '+e.message); }} }} -function makeIcon(color,sz) {{ - return L.divIcon({{ - className:'target-icon', - html:'
Target #'+t.id+'
'+ - ( - '' - )+ - ( - '' - )+ - ( - '' - )+ - ( - '' - )+ - ( - '' - )+ - ( - '' - )+ - ( - '' - ) + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '' ); }} @@ -531,12 +523,19 @@ document.addEventListener('DOMContentLoaded', function() {{ def _on_map_ready(self): self._status_label.setText(f"Map ready - {len(self._targets)} targets") self._status_label.setStyleSheet(f"color: {DARK_SUCCESS};") + # Flush any targets that arrived before the map was ready + if self._pending_targets is not None: + self.set_targets(self._pending_targets) + self._pending_targets = None def _on_marker_clicked(self, tid: int): self.targetSelected.emit(tid) def _run_js(self, script: str): - self._web_view.page().runJavaScript(script) + def _js_callback(result): + if result is not None: + logger.info("JS result: %s", result) + self._web_view.page().runJavaScript(script, 0, _js_callback) # ---- control bar callbacks --------------------------------------------- @@ -571,12 +570,20 @@ document.addEventListener('DOMContentLoaded', function() {{ f"{gps.altitude},{gps.pitch},{gps.heading})" ) - def set_targets(self, targets: List[RadarTarget]): + def set_targets(self, targets: list[RadarTarget]): self._targets = targets + if not self._bridge.is_ready: + logger.info("Map not ready yet — queuing %d targets", len(targets)) + self._pending_targets = targets + return data = [t.to_dict() for t in targets] - js = json.dumps(data).replace("'", "\\'") + js_payload = json.dumps(data).replace("\\", "\\\\").replace("'", "\\'") + logger.info( + "set_targets: %d targets, JSON len=%d, first 200 chars: %s", + len(targets), len(js_payload), js_payload[:200], + ) self._status_label.setText(f"{len(targets)} targets tracked") - self._run_js(f"updateTargets('{js}')") + self._run_js(f"updateTargets('{js_payload}')") def set_coverage_radius(self, radius_m: float): self._coverage_radius = radius_m diff --git a/9_Firmware/9_3_GUI/v7/models.py b/9_Firmware/9_3_GUI/v7/models.py index 45da35c..a5eb40e 100644 --- a/9_Firmware/9_3_GUI/v7/models.py +++ b/9_Firmware/9_3_GUI/v7/models.py @@ -54,13 +54,6 @@ except ImportError: FILTERPY_AVAILABLE = False logging.warning("filterpy not available. Kalman tracking will be disabled.") -try: - import crcmod as _crcmod # noqa: F401 — availability check - CRCMOD_AVAILABLE = True -except ImportError: - CRCMOD_AVAILABLE = False - logging.warning("crcmod not available. CRC validation will use fallback.") - # --------------------------------------------------------------------------- # Dark theme color constants (shared by all modules) # --------------------------------------------------------------------------- @@ -105,15 +98,19 @@ class RadarTarget: @dataclass class RadarSettings: - """Radar system configuration parameters.""" - system_frequency: float = 10e9 # Hz - chirp_duration_1: float = 30e-6 # Long chirp duration (s) - chirp_duration_2: float = 0.5e-6 # Short chirp duration (s) - chirps_per_position: int = 32 - freq_min: float = 10e6 # Hz - freq_max: float = 30e6 # Hz - prf1: float = 1000 # PRF 1 (Hz) - prf2: float = 2000 # PRF 2 (Hz) + """Radar system display/map configuration. + + FPGA register parameters (chirp timing, CFAR, MTI, gain, etc.) are + controlled directly via 4-byte opcode commands — see the FPGA Control + tab and Opcode enum in radar_protocol.py. This dataclass holds only + host-side display/map settings and physical-unit conversion factors. + + range_resolution and velocity_resolution should be calibrated to + the actual waveform parameters. + """ + system_frequency: float = 10e9 # Hz (carrier, used for velocity calc) + range_resolution: float = 781.25 # Meters per range bin (default: 50km/64) + velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform) max_distance: float = 50000 # Max detection range (m) map_size: float = 50000 # Map display size (m) coverage_radius: float = 50000 # Map coverage radius (m) @@ -139,10 +136,14 @@ class GPSData: @dataclass class ProcessingConfig: - """Signal processing pipeline configuration. + """Host-side signal processing pipeline configuration. - Controls: MTI filter, CFAR detector, DC notch removal, - windowing, detection threshold, DBSCAN clustering, and Kalman tracking. + These control host-side DSP that runs AFTER the FPGA processing + pipeline. FPGA-side MTI, CFAR, and DC notch are controlled via + register opcodes from the FPGA Control tab. + + Controls: DBSCAN clustering, Kalman tracking, and optional + host-side reprocessing (MTI, CFAR, windowing, DC notch). """ # MTI (Moving Target Indication) diff --git a/9_Firmware/9_3_GUI/v7/processing.py b/9_Firmware/9_3_GUI/v7/processing.py index e417479..c6ce2cd 100644 --- a/9_Firmware/9_3_GUI/v7/processing.py +++ b/9_Firmware/9_3_GUI/v7/processing.py @@ -1,30 +1,26 @@ """ -v7.processing — Radar signal processing, packet parsing, and GPS parsing. +v7.processing — Radar signal processing and GPS parsing. Classes: - RadarProcessor — dual-CPI fusion, multi-PRF unwrap, DBSCAN clustering, association, Kalman tracking - - RadarPacketParser — parse raw byte streams into typed radar packets - (FIX: returns (parsed_dict, bytes_consumed) tuple) - USBPacketParser — parse GPS text/binary frames from STM32 CDC -Bug fixes vs V6: - 1. RadarPacketParser.parse_packet() now returns (dict, bytes_consumed) tuple - so the caller knows exactly how many bytes to strip from the buffer. - 2. apply_pitch_correction() is a proper standalone function. +Note: RadarPacketParser (old A5/C3 sync + CRC16 format) was removed. + All packet parsing now uses production RadarProtocol (0xAA/0xBB format) + from radar_protocol.py. """ import struct import time import logging import math -from typing import Optional, Tuple, List, Dict import numpy as np from .models import ( RadarTarget, GPSData, ProcessingConfig, - SCIPY_AVAILABLE, SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, CRCMOD_AVAILABLE, + SCIPY_AVAILABLE, SKLEARN_AVAILABLE, FILTERPY_AVAILABLE, ) if SKLEARN_AVAILABLE: @@ -33,9 +29,6 @@ if SKLEARN_AVAILABLE: if FILTERPY_AVAILABLE: from filterpy.kalman import KalmanFilter -if CRCMOD_AVAILABLE: - import crcmod - if SCIPY_AVAILABLE: from scipy.signal import windows as scipy_windows @@ -64,14 +57,14 @@ class RadarProcessor: def __init__(self): self.range_doppler_map = np.zeros((1024, 32)) - self.detected_targets: List[RadarTarget] = [] + self.detected_targets: list[RadarTarget] = [] self.track_id_counter: int = 0 - self.tracks: Dict[int, dict] = {} + self.tracks: dict[int, dict] = {} self.frame_count: int = 0 self.config = ProcessingConfig() # MTI state: store previous frames for cancellation - self._mti_history: List[np.ndarray] = [] + self._mti_history: list[np.ndarray] = [] # ---- Configuration ----------------------------------------------------- @@ -160,12 +153,11 @@ class RadarProcessor: h = self._mti_history if order == 1: return h[-1] - h[-2] - elif order == 2: + if order == 2: return h[-1] - 2.0 * h[-2] + h[-3] - elif order == 3: + if order == 3: return h[-1] - 3.0 * h[-2] + 3.0 * h[-3] - h[-4] - else: - return h[-1] - h[-2] + return h[-1] - h[-2] # ---- CFAR (Constant False Alarm Rate) ----------------------------------- @@ -234,7 +226,7 @@ class RadarProcessor: # ---- Full processing pipeline ------------------------------------------- - def process_frame(self, raw_frame: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + def process_frame(self, raw_frame: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """Run the full signal processing chain on a Range x Doppler frame. Parameters @@ -289,34 +281,10 @@ class RadarProcessor: """Dual-CPI fusion for better detection.""" return np.mean(range_profiles_1, axis=0) + np.mean(range_profiles_2, axis=0) - # ---- Multi-PRF velocity unwrapping ------------------------------------- - - def multi_prf_unwrap(self, doppler_measurements, prf1: float, prf2: float): - """Multi-PRF velocity unwrapping (Chinese Remainder Theorem).""" - lam = 3e8 / 10e9 - v_max1 = prf1 * lam / 2 - v_max2 = prf2 * lam / 2 - - unwrapped = [] - for doppler in doppler_measurements: - v1 = doppler * lam / 2 - v2 = doppler * lam / 2 - velocity = self._solve_chinese_remainder(v1, v2, v_max1, v_max2) - unwrapped.append(velocity) - return unwrapped - - @staticmethod - def _solve_chinese_remainder(v1, v2, max1, max2): - for k in range(-5, 6): - candidate = v1 + k * max1 - if abs(candidate - v2) < max2 / 2: - return candidate - return v1 - # ---- DBSCAN clustering ------------------------------------------------- @staticmethod - def clustering(detections: List[RadarTarget], + def clustering(detections: list[RadarTarget], eps: float = 100, min_samples: int = 2) -> list: """DBSCAN clustering of detections (requires sklearn).""" if not SKLEARN_AVAILABLE or len(detections) == 0: @@ -339,8 +307,8 @@ class RadarProcessor: # ---- Association ------------------------------------------------------- - def association(self, detections: List[RadarTarget], - clusters: list) -> List[RadarTarget]: + def association(self, detections: list[RadarTarget], + _clusters: list) -> list[RadarTarget]: """Associate detections to existing tracks (nearest-neighbour).""" associated = [] for det in detections: @@ -366,7 +334,7 @@ class RadarProcessor: # ---- Kalman tracking --------------------------------------------------- - def tracking(self, associated_detections: List[RadarTarget]): + def tracking(self, associated_detections: list[RadarTarget]): """Kalman filter tracking (requires filterpy).""" if not FILTERPY_AVAILABLE: return @@ -412,158 +380,6 @@ class RadarProcessor: del self.tracks[tid] -# ============================================================================= -# Radar Packet Parser -# ============================================================================= - -class RadarPacketParser: - """ - Parse binary radar packets from the raw byte stream. - - Packet format: - [Sync 2][Type 1][Length 1][Payload N][CRC16 2] - Sync pattern: 0xA5 0xC3 - - Bug fix vs V6: - parse_packet() now returns ``(parsed_dict, bytes_consumed)`` so the - caller can correctly advance the read pointer in the buffer. - """ - - SYNC = b"\xA5\xC3" - - def __init__(self): - if CRCMOD_AVAILABLE: - self.crc16_func = crcmod.mkCrcFun( - 0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000 - ) - else: - self.crc16_func = None - - # ---- main entry point -------------------------------------------------- - - def parse_packet(self, data: bytes) -> Optional[Tuple[dict, int]]: - """ - Attempt to parse one radar packet from *data*. - - Returns - ------- - (parsed_dict, bytes_consumed) on success, or None if no valid packet. - """ - if len(data) < 6: - return None - - idx = data.find(self.SYNC) - if idx == -1: - return None - - pkt = data[idx:] - if len(pkt) < 6: - return None - - pkt_type = pkt[2] - length = pkt[3] - total_len = 4 + length + 2 # sync(2) + type(1) + len(1) + payload + crc(2) - - if len(pkt) < total_len: - return None - - payload = pkt[4 : 4 + length] - crc_received = struct.unpack(" Optional[dict]: - if len(payload) < 12: - return None - try: - range_val = struct.unpack(">I", payload[0:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp = payload[6] & 0x1F - return { - "type": "range", - "range": range_val, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp, - "timestamp": time.time(), - } - except Exception as e: - logger.error(f"Error parsing range packet: {e}") - return None - - @staticmethod - def _parse_doppler(payload: bytes) -> Optional[dict]: - if len(payload) < 12: - return None - try: - real = struct.unpack(">h", payload[0:2])[0] - imag = struct.unpack(">h", payload[2:4])[0] - elevation = payload[4] & 0x1F - azimuth = payload[5] & 0x3F - chirp = payload[6] & 0x1F - return { - "type": "doppler", - "doppler_real": real, - "doppler_imag": imag, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp, - "timestamp": time.time(), - } - except Exception as e: - logger.error(f"Error parsing doppler packet: {e}") - return None - - @staticmethod - def _parse_detection(payload: bytes) -> Optional[dict]: - if len(payload) < 8: - return None - try: - detected = (payload[0] & 0x01) != 0 - elevation = payload[1] & 0x1F - azimuth = payload[2] & 0x3F - chirp = payload[3] & 0x1F - return { - "type": "detection", - "detected": detected, - "elevation": elevation, - "azimuth": azimuth, - "chirp": chirp, - "timestamp": time.time(), - } - except Exception as e: - logger.error(f"Error parsing detection packet: {e}") - return None - - # ============================================================================= # USB / GPS Packet Parser # ============================================================================= @@ -578,14 +394,9 @@ class USBPacketParser: """ def __init__(self): - if CRCMOD_AVAILABLE: - self.crc16_func = crcmod.mkCrcFun( - 0x11021, rev=False, initCrc=0xFFFF, xorOut=0x0000 - ) - else: - self.crc16_func = None + pass - def parse_gps_data(self, data: bytes) -> Optional[GPSData]: + def parse_gps_data(self, data: bytes) -> GPSData | None: """Attempt to parse GPS data from a raw USB CDC frame.""" if not data: return None @@ -607,12 +418,12 @@ class USBPacketParser: # Binary format: [GPSB 4][lat 8][lon 8][alt 4][pitch 4][CRC 2] = 30 bytes if len(data) >= 30 and data[0:4] == b"GPSB": return self._parse_binary_gps(data) - except Exception as e: + except (ValueError, struct.error) as e: logger.error(f"Error parsing GPS data: {e}") return None @staticmethod - def _parse_binary_gps(data: bytes) -> Optional[GPSData]: + def _parse_binary_gps(data: bytes) -> GPSData | None: """Parse 30-byte binary GPS frame.""" try: if len(data) < 30: @@ -637,6 +448,6 @@ class USBPacketParser: pitch=pitch, timestamp=time.time(), ) - except Exception as e: + except (ValueError, struct.error) as e: logger.error(f"Error parsing binary GPS: {e}") return None diff --git a/9_Firmware/9_3_GUI/v7/workers.py b/9_Firmware/9_3_GUI/v7/workers.py index e81616e..c467c98 100644 --- a/9_Firmware/9_3_GUI/v7/workers.py +++ b/9_Firmware/9_3_GUI/v7/workers.py @@ -2,24 +2,39 @@ v7.workers — QThread-based workers and demo target simulator. Classes: - - RadarDataWorker — reads from FT2232HQ, parses packets, - emits signals with processed data. + - RadarDataWorker — reads from FT2232H via production RadarAcquisition, + parses 0xAA/0xBB packets, assembles 64x32 frames, + runs host-side DSP, emits PyQt signals. - GPSDataWorker — reads GPS frames from STM32 CDC, emits GPSData signals. - - TargetSimulator — QTimer-based demo target generator (from GUI_PyQt_Map.py). + - TargetSimulator — QTimer-based demo target generator. + +The old V6/V7 packet parsing (sync A5 C3 + type + CRC16) has been removed. +All packet parsing now uses the production radar_protocol.py which matches +the actual FPGA packet format (0xAA data 11-byte, 0xBB status 26-byte). """ import math import time import random +import queue +import struct import logging -from typing import List + +import numpy as np from PyQt6.QtCore import QThread, QObject, QTimer, pyqtSignal -from .models import RadarTarget, RadarSettings, GPSData -from .hardware import FT2232HQInterface, STM32USBInterface +from .models import RadarTarget, GPSData, RadarSettings +from .hardware import ( + RadarAcquisition, + RadarFrame, + StatusResponse, + DataRecorder, + STM32USBInterface, +) from .processing import ( - RadarProcessor, RadarPacketParser, USBPacketParser, + RadarProcessor, + USBPacketParser, apply_pitch_correction, ) @@ -61,162 +76,196 @@ def polar_to_geographic( # ============================================================================= -# Radar Data Worker (QThread) +# Radar Data Worker (QThread) — production protocol # ============================================================================= class RadarDataWorker(QThread): """ - Background worker that continuously reads radar data from the primary - FT2232HQ interface, parses packets, runs the processing pipeline, and - emits signals with results. + Background worker that reads radar data from FT2232H (or ReplayConnection), + parses 0xAA/0xBB packets via production RadarAcquisition, runs optional + host-side DSP, and emits PyQt signals with results. + + This replaces the old V7 worker which used an incompatible packet format. + Now uses production radar_protocol.py for all packet parsing and frame + assembly (11-byte 0xAA data packets → 64x32 RadarFrame). Signals: - packetReceived(dict) — a single parsed packet dict - targetsUpdated(list) — list of RadarTarget after processing - errorOccurred(str) — error message - statsUpdated(dict) — packet/byte counters + frameReady(RadarFrame) — a complete 64x32 radar frame + statusReceived(object) — StatusResponse from FPGA + targetsUpdated(list) — list of RadarTarget after host-side DSP + errorOccurred(str) — error message + statsUpdated(dict) — frame/byte counters """ - packetReceived = pyqtSignal(dict) - targetsUpdated = pyqtSignal(list) + frameReady = pyqtSignal(object) # RadarFrame + statusReceived = pyqtSignal(object) # StatusResponse + targetsUpdated = pyqtSignal(list) # List[RadarTarget] errorOccurred = pyqtSignal(str) statsUpdated = pyqtSignal(dict) def __init__( self, - ft2232hq: FT2232HQInterface, - processor: RadarProcessor, - packet_parser: RadarPacketParser, - settings: RadarSettings, - gps_data_ref: GPSData, + connection, # FT2232HConnection or ReplayConnection + processor: RadarProcessor | None = None, + recorder: DataRecorder | None = None, + gps_data_ref: GPSData | None = None, + settings: RadarSettings | None = None, parent=None, ): super().__init__(parent) - self._ft2232hq = ft2232hq + self._connection = connection self._processor = processor - self._parser = packet_parser - self._settings = settings + self._recorder = recorder self._gps = gps_data_ref + self._settings = settings or RadarSettings() self._running = False + # Frame queue for production RadarAcquisition → this thread + self._frame_queue: queue.Queue = queue.Queue(maxsize=4) + + # Production acquisition thread (does the actual parsing) + self._acquisition: RadarAcquisition | None = None + # Counters - self._packet_count = 0 + self._frame_count = 0 self._byte_count = 0 self._error_count = 0 def stop(self): self._running = False + if self._acquisition: + self._acquisition.stop() def run(self): - """Main loop: read → parse → process → emit.""" + """ + Start production RadarAcquisition thread, then poll its frame queue + and emit PyQt signals for each complete frame. + """ self._running = True - buffer = bytearray() + + # Create and start the production acquisition thread + self._acquisition = RadarAcquisition( + connection=self._connection, + frame_queue=self._frame_queue, + recorder=self._recorder, + status_callback=self._on_status, + ) + self._acquisition.start() + logger.info("RadarDataWorker started (production protocol)") while self._running: - # Use FT2232HQ interface - iface = None - if self._ft2232hq and self._ft2232hq.is_open: - iface = self._ft2232hq - - if iface is None: - self.msleep(100) - continue - try: - data = iface.read_data(4096) - if data: - buffer.extend(data) - self._byte_count += len(data) + # Poll for complete frames from production acquisition + frame: RadarFrame = self._frame_queue.get(timeout=0.1) + self._frame_count += 1 - # Parse as many packets as possible - while len(buffer) >= 6: - result = self._parser.parse_packet(bytes(buffer)) - if result is None: - # No valid packet at current position — skip one byte - if len(buffer) > 1: - buffer = buffer[1:] - else: - break - continue + # Emit raw frame + self.frameReady.emit(frame) - pkt, consumed = result - buffer = buffer[consumed:] - self._packet_count += 1 + # Run host-side DSP if processor is configured + if self._processor is not None: + targets = self._run_host_dsp(frame) + if targets: + self.targetsUpdated.emit(targets) - # Process the packet - self._process_packet(pkt) - self.packetReceived.emit(pkt) + # Emit stats + self.statsUpdated.emit({ + "frames": self._frame_count, + "detection_count": frame.detection_count, + "errors": self._error_count, + }) - # Emit stats periodically - self.statsUpdated.emit({ - "packets": self._packet_count, - "bytes": self._byte_count, - "errors": self._error_count, - "active_tracks": len(self._processor.tracks), - "targets": len(self._processor.detected_targets), - }) - else: - self.msleep(10) - except Exception as e: + except queue.Empty: + continue + except (ValueError, IndexError) as e: self._error_count += 1 self.errorOccurred.emit(str(e)) logger.error(f"RadarDataWorker error: {e}") - self.msleep(100) - # ---- internal packet handling ------------------------------------------ + # Stop acquisition thread + if self._acquisition: + self._acquisition.stop() + self._acquisition.join(timeout=2.0) + self._acquisition = None - def _process_packet(self, pkt: dict): - """Route a parsed packet through the processing pipeline.""" - try: - if pkt["type"] == "range": - range_m = pkt["range"] * 0.1 - raw_elev = pkt["elevation"] + logger.info("RadarDataWorker stopped") + + def _on_status(self, status: StatusResponse): + """Callback from production RadarAcquisition on status packet.""" + self.statusReceived.emit(status) + + def _run_host_dsp(self, frame: RadarFrame) -> list[RadarTarget]: + """ + Run host-side DSP on a complete frame. + This is where DBSCAN clustering, Kalman tracking, and other + non-timing-critical processing happens. + + The FPGA already does: FFT, MTI, CFAR, DC notch. + Host-side DSP adds: clustering, tracking, geo-coordinate mapping. + + Bin-to-physical conversion uses RadarSettings.range_resolution + and velocity_resolution (should be calibrated to actual waveform). + """ + targets: list[RadarTarget] = [] + + cfg = self._processor.config + if not (cfg.clustering_enabled or cfg.tracking_enabled): + return targets + + # Extract detections from FPGA CFAR flags + det_indices = np.argwhere(frame.detections > 0) + r_res = self._settings.range_resolution + v_res = self._settings.velocity_resolution + + for idx in det_indices: + rbin, dbin = idx + mag = frame.magnitude[rbin, dbin] + snr = 10 * np.log10(max(mag, 1)) if mag > 0 else 0 + + # Convert bin indices to physical units + range_m = float(rbin) * r_res + # Doppler: centre bin (16) = 0 m/s; positive bins = approaching + velocity_ms = float(dbin - 16) * v_res + + # Apply pitch correction if GPS data available + raw_elev = 0.0 # FPGA doesn't send elevation per-detection + corr_elev = raw_elev + if self._gps: corr_elev = apply_pitch_correction(raw_elev, self._gps.pitch) - target = RadarTarget( - id=pkt["chirp"], - range=range_m, - velocity=0, - azimuth=pkt["azimuth"], - elevation=corr_elev, - snr=20.0, - timestamp=pkt["timestamp"], + # Compute geographic position if GPS available + lat, lon = 0.0, 0.0 + azimuth = 0.0 # No azimuth from single-beam; set to heading + if self._gps: + azimuth = self._gps.heading + lat, lon = polar_to_geographic( + self._gps.latitude, self._gps.longitude, + range_m, azimuth, ) - self._update_rdm(target) - elif pkt["type"] == "doppler": - lam = 3e8 / self._settings.system_frequency - velocity = (pkt["doppler_real"] / 32767.0) * ( - self._settings.prf1 * lam / 2 - ) - self._update_velocity(pkt, velocity) + target = RadarTarget( + id=len(targets), + range=range_m, + velocity=velocity_ms, + azimuth=azimuth, + elevation=corr_elev, + latitude=lat, + longitude=lon, + snr=snr, + timestamp=frame.timestamp, + ) + targets.append(target) - elif pkt["type"] == "detection": - if pkt["detected"]: - raw_elev = pkt["elevation"] - corr_elev = apply_pitch_correction(raw_elev, self._gps.pitch) - logger.info( - f"CFAR Detection: raw={raw_elev}, corr={corr_elev:.1f}, " - f"pitch={self._gps.pitch:.1f}" - ) - except Exception as e: - logger.error(f"Error processing packet: {e}") + # DBSCAN clustering + if cfg.clustering_enabled and len(targets) > 0: + clusters = self._processor.clustering( + targets, cfg.clustering_eps, cfg.clustering_min_samples) + # Associate and track + if cfg.tracking_enabled: + targets = self._processor.association(targets, clusters) + self._processor.tracking(targets) - def _update_rdm(self, target: RadarTarget): - range_bin = min(int(target.range / 50), 1023) - doppler_bin = min(abs(int(target.velocity)), 31) - self._processor.range_doppler_map[range_bin, doppler_bin] += 1 - self._processor.detected_targets.append(target) - if len(self._processor.detected_targets) > 100: - self._processor.detected_targets = self._processor.detected_targets[-100:] - - def _update_velocity(self, pkt: dict, velocity: float): - for t in self._processor.detected_targets: - if (t.azimuth == pkt["azimuth"] - and t.elevation == pkt["elevation"] - and t.id == pkt["chirp"]): - t.velocity = velocity - break + return targets # ============================================================================= @@ -269,7 +318,7 @@ class GPSDataWorker(QThread): if gps: self._gps_count += 1 self.gpsReceived.emit(gps) - except Exception as e: + except (ValueError, struct.error) as e: self.errorOccurred.emit(str(e)) logger.error(f"GPSDataWorker error: {e}") self.msleep(100) @@ -292,7 +341,7 @@ class TargetSimulator(QObject): def __init__(self, radar_position: GPSData, parent=None): super().__init__(parent) self._radar_pos = radar_position - self._targets: List[RadarTarget] = [] + self._targets: list[RadarTarget] = [] self._next_id = 1 self._timer = QTimer(self) self._timer.timeout.connect(self._tick) @@ -349,7 +398,7 @@ class TargetSimulator(QObject): def _tick(self): """Update all simulated targets and emit.""" - updated: List[RadarTarget] = [] + updated: list[RadarTarget] = [] for t in self._targets: new_range = t.range - t.velocity * 0.5 diff --git a/9_Firmware/tools/uart_capture.py b/9_Firmware/tools/uart_capture.py index ef646f4..e257b54 100755 --- a/9_Firmware/tools/uart_capture.py +++ b/9_Firmware/tools/uart_capture.py @@ -26,6 +26,7 @@ Usage: """ import argparse +from contextlib import nullcontext import datetime import glob import os @@ -38,7 +39,6 @@ try: import serial import serial.tools.list_ports except ImportError: - print("ERROR: pyserial not installed. Run: pip install pyserial") sys.exit(1) # --------------------------------------------------------------------------- @@ -94,12 +94,9 @@ def list_ports(): """Print available serial ports.""" ports = serial.tools.list_ports.comports() if not ports: - print("No serial ports found.") return - print(f"{'Port':<30} {'Description':<40} {'HWID'}") - print("-" * 100) - for p in sorted(ports, key=lambda x: x.device): - print(f"{p.device:<30} {p.description:<40} {p.hwid}") + for _p in sorted(ports, key=lambda x: x.device): + pass def auto_detect_port(): @@ -172,10 +169,7 @@ def should_display(line, filter_subsys=None, errors_only=False): return False # Subsystem filter - if filter_subsys and subsys not in filter_subsys: - return False - - return True + return not (filter_subsys and subsys not in filter_subsys) # --------------------------------------------------------------------------- @@ -219,8 +213,10 @@ class CaptureStats: ] if self.by_subsys: lines.append("By subsystem:") - for tag in sorted(self.by_subsys, key=self.by_subsys.get, reverse=True): - lines.append(f" {tag:<8} {self.by_subsys[tag]}") + lines.extend( + f" {tag:<8} {self.by_subsys[tag]}" + for tag in sorted(self.by_subsys, key=self.by_subsys.get, reverse=True) + ) return "\n".join(lines) @@ -228,12 +224,12 @@ class CaptureStats: # Main capture loop # --------------------------------------------------------------------------- -def capture(port, baud, log_file, filter_subsys, errors_only, use_color): +def capture(port, baud, log_file, filter_subsys, errors_only, _use_color): """Open serial port and capture DIAG output.""" stats = CaptureStats() running = True - def handle_signal(sig, frame): + def handle_signal(_sig, _frame): nonlocal running running = False @@ -249,69 +245,68 @@ def capture(port, baud, log_file, filter_subsys, errors_only, use_color): stopbits=serial.STOPBITS_ONE, timeout=0.1, # 100ms read timeout for responsive Ctrl-C ) - except serial.SerialException as e: - print(f"ERROR: Could not open {port}: {e}") + except serial.SerialException: sys.exit(1) - print(f"Connected to {port} at {baud} baud") if log_file: - print(f"Logging to {log_file}") + pass if filter_subsys: - print(f"Filter: {', '.join(sorted(filter_subsys))}") + pass if errors_only: - print("Mode: errors/warnings only") - print("Press Ctrl-C to stop.\n") + pass - flog = None if log_file: os.makedirs(os.path.dirname(log_file), exist_ok=True) - flog = open(log_file, "w", encoding=ENCODING) - flog.write(f"# AERIS-10 UART capture — {datetime.datetime.now().isoformat()}\n") - flog.write(f"# Port: {port} Baud: {baud}\n") - flog.write(f"# Host: {os.uname().nodename}\n\n") - flog.flush() + log_context = open(log_file, "w", encoding=ENCODING) # noqa: SIM115 + else: + log_context = nullcontext(None) line_buf = b"" try: - while running: - try: - chunk = ser.read(256) - except serial.SerialException as e: - print(f"\nSerial error: {e}") - break + with log_context as flog: + if flog: + flog.write(f"# AERIS-10 UART capture — {datetime.datetime.now().isoformat()}\n") + flog.write(f"# Port: {port} Baud: {baud}\n") + flog.write(f"# Host: {os.uname().nodename}\n\n") + flog.flush() - if not chunk: - continue + while running: + try: + chunk = ser.read(256) + except serial.SerialException: + break - line_buf += chunk - - # Process complete lines - while b"\n" in line_buf: - raw_line, line_buf = line_buf.split(b"\n", 1) - line = raw_line.decode(ENCODING, errors="replace").rstrip("\r") - - if not line: + if not chunk: continue - stats.update(line) + line_buf += chunk - # Log file always gets everything (unfiltered, no color) - if flog: - wall_ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3] - flog.write(f"{wall_ts} {line}\n") - flog.flush() + # Process complete lines + while b"\n" in line_buf: + raw_line, line_buf = line_buf.split(b"\n", 1) + line = raw_line.decode(ENCODING, errors="replace").rstrip("\r") - # Terminal display respects filters - if should_display(line, filter_subsys, errors_only): - print(colorize(line, use_color)) + if not line: + continue + + stats.update(line) + + # Log file always gets everything (unfiltered, no color) + if flog: + wall_ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3] + flog.write(f"{wall_ts} {line}\n") + flog.flush() + + # Terminal display respects filters + if should_display(line, filter_subsys, errors_only): + pass + + if flog: + flog.write(f"\n{stats.summary()}\n") finally: ser.close() - if flog: - flog.write(f"\n{stats.summary()}\n") - flog.close() - print(stats.summary()) # --------------------------------------------------------------------------- @@ -374,9 +369,7 @@ def main(): if not port: port = auto_detect_port() if not port: - print("ERROR: No serial port detected. Use -p to specify, or --list to see ports.") sys.exit(1) - print(f"Auto-detected port: {port}") # Resolve log file log_file = None @@ -390,7 +383,7 @@ def main(): # Parse filter filter_subsys = None if args.filter: - filter_subsys = set(t.strip().upper() for t in args.filter.split(",")) + filter_subsys = {t.strip().upper() for t in args.filter.split(",")} # Color detection use_color = not args.no_color and sys.stdout.isatty() diff --git a/README.md b/README.md index 34ef879..522a216 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The AERIS-10 main sub-systems are: - **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board: - PLFM Chirps generation via the DAC - Raw ADC data read - - Automatic Gain Control (AGC) + - Digital Gain Control (host-configurable gain shift) - I/Q Baseband Down-Conversion - Decimation - Filtering diff --git a/pyproject.toml b/pyproject.toml index e517b2d..4f6ea09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,4 +24,28 @@ target-version = "py312" line-length = 100 [tool.ruff.lint] -select = ["E", "F"] +select = [ + "E", # pycodestyle errors + "F", # pyflakes (unused imports, undefined names, duplicate keys, assert-tuple) + "B", # flake8-bugbear (mutable defaults, unreachable code, raise-without-from) + "RUF", # ruff-specific (unused noqa, ambiguous chars, implicit Optional) + "SIM", # flake8-simplify (dead branches, collapsible ifs, unnecessary pass) + "PIE", # flake8-pie (no-op expressions, unnecessary spread) + "T20", # flake8-print (stray print() calls — LLMs leave debug prints) + "ARG", # flake8-unused-arguments (LLMs generate params they never use) + "ERA", # eradicate (commented-out code — LLMs leave "alternatives" as comments) + "A", # flake8-builtins (LLMs shadow id, type, list, dict, input, map) + "BLE", # flake8-blind-except (bare except / overly broad except) + "RET", # flake8-return (unreachable code after return, unnecessary else-after-return) + "ISC", # flake8-implicit-str-concat (missing comma in list of strings) + "TCH", # flake8-type-checking (imports only used in type hints — move behind TYPE_CHECKING) + "UP", # pyupgrade (outdated syntax for target Python version) + "C4", # flake8-comprehensions (unnecessary list/dict calls around generators) + "PERF", # perflint (performance anti-patterns: unnecessary list() in for loops, etc.) +] + +[tool.ruff.lint.per-file-ignores] +# Tests: allow unused args (fixtures), prints (debugging), commented code (examples) +"test_*.py" = ["ARG", "T20", "ERA"] +# Re-export modules: unused imports are intentional +"v7/hardware.py" = ["F401"]