diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 46d7396..33db606 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -46,7 +46,9 @@ jobs: - name: Unit tests run: > uv run pytest - 9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short + 9_Firmware/9_3_GUI/test_radar_dashboard.py + 9_Firmware/9_3_GUI/test_v7.py + -v --tb=short # =========================================================================== # MCU Firmware Unit Tests (20 tests) @@ -82,3 +84,33 @@ jobs: - name: Run full FPGA regression run: bash run_regression.sh working-directory: 9_Firmware/9_2_FPGA + + # =========================================================================== + # Cross-Layer Contract Tests (Python ↔ Verilog ↔ C) + # Validates opcode maps, bit widths, packet layouts, and round-trip + # correctness across FPGA RTL, Python GUI, and STM32 firmware. + # =========================================================================== + cross-layer-tests: + name: Cross-Layer Contract Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: uv sync --group dev + + - name: Install Icarus Verilog + run: sudo apt-get update && sudo apt-get install -y iverilog + + - name: Run cross-layer contract tests + run: > + uv run pytest + 9_Firmware/tests/cross_layer/test_cross_layer_contract.py + -v --tb=short 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_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.cpp new file mode 100644 index 0000000..0d4d477 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.cpp @@ -0,0 +1,116 @@ +// ADAR1000_AGC.cpp -- STM32 outer-loop AGC implementation +// +// See ADAR1000_AGC.h for architecture overview. + +#include "ADAR1000_AGC.h" +#include "ADAR1000_Manager.h" +#include "diag_log.h" + +#include + +// --------------------------------------------------------------------------- +// Constructor -- set all config fields to safe defaults +// --------------------------------------------------------------------------- +ADAR1000_AGC::ADAR1000_AGC() + : agc_base_gain(ADAR1000Manager::kDefaultRxVgaGain) // 30 + , gain_step_down(4) + , gain_step_up(1) + , min_gain(0) + , max_gain(127) + , holdoff_frames(4) + , enabled(true) + , holdoff_counter(0) + , last_saturated(false) + , saturation_event_count(0) +{ + memset(cal_offset, 0, sizeof(cal_offset)); +} + +// --------------------------------------------------------------------------- +// update -- called once per frame with the FPGA DIG_5 saturation flag +// +// Returns true if agc_base_gain changed (caller should then applyGain). +// --------------------------------------------------------------------------- +void ADAR1000_AGC::update(bool fpga_saturation) +{ + if (!enabled) + return; + + last_saturated = fpga_saturation; + + if (fpga_saturation) { + // Attack: reduce gain immediately + saturation_event_count++; + holdoff_counter = 0; + + if (agc_base_gain >= gain_step_down + min_gain) { + agc_base_gain -= gain_step_down; + } else { + agc_base_gain = min_gain; + } + + DIAG("AGC", "SAT detected -- gain_base -> %u (events=%lu)", + (unsigned)agc_base_gain, (unsigned long)saturation_event_count); + + } else { + // Recovery: wait for holdoff, then increase gain + holdoff_counter++; + + if (holdoff_counter >= holdoff_frames) { + holdoff_counter = 0; + + if (agc_base_gain + gain_step_up <= max_gain) { + agc_base_gain += gain_step_up; + } else { + agc_base_gain = max_gain; + } + + DIAG("AGC", "Recovery step -- gain_base -> %u", (unsigned)agc_base_gain); + } + } +} + +// --------------------------------------------------------------------------- +// applyGain -- write effective gain to all 16 RX VGA channels +// +// Uses the Manager's adarSetRxVgaGain which takes 1-based channel indices +// (matching the convention in setBeamAngle). +// --------------------------------------------------------------------------- +void ADAR1000_AGC::applyGain(ADAR1000Manager &mgr) +{ + for (uint8_t dev = 0; dev < AGC_NUM_DEVICES; ++dev) { + for (uint8_t ch = 0; ch < AGC_NUM_CHANNELS; ++ch) { + uint8_t gain = effectiveGain(dev * AGC_NUM_CHANNELS + ch); + // Channel parameter is 1-based per Manager convention + mgr.adarSetRxVgaGain(dev, ch + 1, gain, BROADCAST_OFF); + } + } +} + +// --------------------------------------------------------------------------- +// resetState -- clear runtime counters, preserve configuration +// --------------------------------------------------------------------------- +void ADAR1000_AGC::resetState() +{ + holdoff_counter = 0; + last_saturated = false; + saturation_event_count = 0; +} + +// --------------------------------------------------------------------------- +// effectiveGain -- compute clamped per-channel gain +// --------------------------------------------------------------------------- +uint8_t ADAR1000_AGC::effectiveGain(uint8_t channel_index) const +{ + if (channel_index >= AGC_TOTAL_CHANNELS) + return min_gain; // safety fallback — OOB channels get minimum gain + + int16_t raw = static_cast(agc_base_gain) + cal_offset[channel_index]; + + if (raw < static_cast(min_gain)) + return min_gain; + if (raw > static_cast(max_gain)) + return max_gain; + + return static_cast(raw); +} diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.h b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.h new file mode 100644 index 0000000..bf534fd --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ADAR1000_AGC.h @@ -0,0 +1,97 @@ +// ADAR1000_AGC.h -- STM32 outer-loop AGC for ADAR1000 RX VGA gain +// +// Adjusts the analog VGA common-mode gain on each ADAR1000 RX channel based on +// the FPGA's saturation flag (DIG_5 / PD13). Runs once per radar frame +// (~258 ms) in the main loop, after runRadarPulseSequence(). +// +// Architecture: +// - Inner loop (FPGA, per-sample): rx_gain_control auto-adjusts digital +// gain_shift based on peak magnitude / saturation. Range ±42 dB. +// - Outer loop (THIS MODULE, per-frame): reads FPGA DIG_5 GPIO. If +// saturation detected, reduces agc_base_gain immediately (attack). If no +// saturation for holdoff_frames, increases agc_base_gain (decay/recovery). +// +// Per-channel gain formula: +// VGA[dev][ch] = clamp(agc_base_gain + cal_offset[dev*4+ch], min_gain, max_gain) +// +// The cal_offset array allows per-element calibration to correct inter-channel +// gain imbalance. Default is all zeros (uniform gain). + +#ifndef ADAR1000_AGC_H +#define ADAR1000_AGC_H + +#include + +// Forward-declare to avoid pulling in the full ADAR1000_Manager header here. +// The .cpp includes the real header. +class ADAR1000Manager; + +// Number of ADAR1000 devices +#define AGC_NUM_DEVICES 4 +// Number of channels per ADAR1000 +#define AGC_NUM_CHANNELS 4 +// Total RX channels +#define AGC_TOTAL_CHANNELS (AGC_NUM_DEVICES * AGC_NUM_CHANNELS) + +class ADAR1000_AGC { +public: + // --- Configuration (public for easy field-testing / GUI override) --- + + // Common-mode base gain (raw ADAR1000 register value, 0-255). + // Default matches ADAR1000Manager::kDefaultRxVgaGain = 30. + uint8_t agc_base_gain; + + // Per-channel calibration offset (signed, added to agc_base_gain). + // Index = device*4 + channel. Default: all 0. + int8_t cal_offset[AGC_TOTAL_CHANNELS]; + + // How much to decrease agc_base_gain per frame when saturated (attack). + uint8_t gain_step_down; + + // How much to increase agc_base_gain per frame when recovering (decay). + uint8_t gain_step_up; + + // Minimum allowed agc_base_gain (floor). + uint8_t min_gain; + + // Maximum allowed agc_base_gain (ceiling). + uint8_t max_gain; + + // Number of consecutive non-saturated frames required before gain-up. + uint8_t holdoff_frames; + + // Master enable. When false, update() is a no-op. + bool enabled; + + // --- Runtime state (read-only for diagnostics) --- + + // Consecutive non-saturated frame counter (resets on saturation). + uint8_t holdoff_counter; + + // True if the last update() saw saturation. + bool last_saturated; + + // Total saturation events since reset/construction. + uint32_t saturation_event_count; + + // --- Methods --- + + ADAR1000_AGC(); + + // Call once per frame after runRadarPulseSequence(). + // fpga_saturation: result of HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_13) == GPIO_PIN_SET + void update(bool fpga_saturation); + + // Apply the current gain to all 16 RX VGA channels via the Manager. + void applyGain(ADAR1000Manager &mgr); + + // Reset runtime state (holdoff counter, saturation count) without + // changing configuration. + void resetState(); + + // Compute the effective gain for a specific channel index (0-15), + // clamped to [min_gain, max_gain]. Useful for diagnostics. + uint8_t effectiveGain(uint8_t channel_index) const; +}; + +#endif // ADAR1000_AGC_H diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/RadarSettings.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/RadarSettings.cpp index df34c25..be6f9bd 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/RadarSettings.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/RadarSettings.cpp @@ -7,8 +7,8 @@ RadarSettings::RadarSettings() { void RadarSettings::resetToDefaults() { system_frequency = 10.0e9; // 10 GHz - chirp_duration_1 = 30.0e-6; // 30 s - chirp_duration_2 = 0.5e-6; // 0.5 s + chirp_duration_1 = 30.0e-6; // 30 �s + chirp_duration_2 = 0.5e-6; // 0.5 �s chirps_per_position = 32; freq_min = 10.0e6; // 10 MHz freq_max = 30.0e6; // 30 MHz @@ -21,8 +21,8 @@ void RadarSettings::resetToDefaults() { } bool RadarSettings::parseFromUSB(const uint8_t* data, uint32_t length) { - // Minimum packet size: "SET" + 8 doubles + 1 uint32_t + "END" = 3 + 8*8 + 4 + 3 = 74 bytes - if (data == nullptr || length < 74) { + // Minimum packet size: "SET" + 9 doubles + 1 uint32_t + "END" = 3 + 9*8 + 4 + 3 = 82 bytes + if (data == nullptr || length < 82) { settings_valid = false; return false; } diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp index d689365..6410153 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/USBHandler.cpp @@ -43,6 +43,11 @@ void USBHandler::processStartFlag(const uint8_t* data, uint32_t length) { // Start flag: bytes [23, 46, 158, 237] const uint8_t START_FLAG[] = {23, 46, 158, 237}; + // Guard: need at least 4 bytes to contain a start flag. + // Without this, length - 4 wraps to ~4 billion (uint32_t unsigned underflow) + // and the loop reads far past the buffer boundary. + if (length < 4) return; + // Check if start flag is in the received data for (uint32_t i = 0; i <= length - 4; i++) { if (memcmp(data + i, START_FLAG, 4) == 0) { diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp index b11cf02..09468d0 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp @@ -23,6 +23,7 @@ #include "usbd_cdc_if.h" #include "adar1000.h" #include "ADAR1000_Manager.h" +#include "ADAR1000_AGC.h" extern "C" { #include "ad9523.h" } @@ -224,6 +225,7 @@ extern SPI_HandleTypeDef hspi4; //ADAR1000 ADAR1000Manager adarManager; +ADAR1000_AGC outerAgc; static uint8_t matrix1[15][16]; static uint8_t matrix2[15][16]; static uint8_t vector_0[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; @@ -639,6 +641,7 @@ SystemError_t checkSystemHealth(void) { if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) { current_error = ERROR_AD9523_CLOCK; DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1); + return current_error; } last_clock_check = HAL_GetTick(); } @@ -649,10 +652,12 @@ SystemError_t checkSystemHealth(void) { if (!tx_locked) { current_error = ERROR_ADF4382_TX_UNLOCK; DIAG_ERR("LO", "Health check: TX LO UNLOCKED"); + return current_error; } if (!rx_locked) { current_error = ERROR_ADF4382_RX_UNLOCK; DIAG_ERR("LO", "Health check: RX LO UNLOCKED"); + return current_error; } } @@ -661,14 +666,14 @@ SystemError_t checkSystemHealth(void) { if (!adarManager.verifyDeviceCommunication(i)) { current_error = ERROR_ADAR1000_COMM; DIAG_ERR("BF", "Health check: ADAR1000 #%d comm FAILED", i); - break; + return current_error; } float temp = adarManager.readTemperature(i); if (temp > 85.0f) { current_error = ERROR_ADAR1000_TEMP; DIAG_ERR("BF", "Health check: ADAR1000 #%d OVERTEMP %.1fC > 85C", i, temp); - break; + return current_error; } } @@ -678,6 +683,7 @@ SystemError_t checkSystemHealth(void) { if (!GY85_Update(&imu)) { current_error = ERROR_IMU_COMM; DIAG_ERR("IMU", "Health check: GY85_Update() FAILED"); + return current_error; } last_imu_check = HAL_GetTick(); } @@ -689,6 +695,7 @@ SystemError_t checkSystemHealth(void) { if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) { current_error = ERROR_BMP180_COMM; DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure); + return current_error; } last_bmp_check = HAL_GetTick(); } @@ -701,6 +708,7 @@ SystemError_t checkSystemHealth(void) { if (HAL_GetTick() - last_gps_fix > 30000) { current_error = ERROR_GPS_COMM; DIAG_WARN("SYS", "Health check: GPS no fix for >30s"); + return current_error; } // 7. Check RF Power Amplifier Current @@ -709,12 +717,12 @@ SystemError_t checkSystemHealth(void) { if (Idq_reading[i] > 2.5f) { current_error = ERROR_RF_PA_OVERCURRENT; DIAG_ERR("PA", "Health check: PA ch%d OVERCURRENT Idq=%.3fA > 2.5A", i, Idq_reading[i]); - break; + return current_error; } if (Idq_reading[i] < 0.1f) { current_error = ERROR_RF_PA_BIAS; DIAG_ERR("PA", "Health check: PA ch%d BIAS FAULT Idq=%.3fA < 0.1A", i, Idq_reading[i]); - break; + return current_error; } } } @@ -723,6 +731,7 @@ SystemError_t checkSystemHealth(void) { if (temperature > 75.0f) { current_error = ERROR_TEMPERATURE_HIGH; DIAG_ERR("SYS", "Health check: System OVERTEMP %.1fC > 75C", temperature); + return current_error; } // 9. Simple watchdog check @@ -730,6 +739,7 @@ SystemError_t checkSystemHealth(void) { if (HAL_GetTick() - last_health_check > 60000) { current_error = ERROR_WATCHDOG_TIMEOUT; DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)"); + return current_error; } last_health_check = HAL_GetTick(); @@ -919,38 +929,41 @@ bool checkSystemHealthStatus(void) { // Get system status for GUI // Get system status for GUI with 8 temperature variables void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) { - char temp_buffer[200]; - char final_status[500] = "System Status: "; + // Build status string directly in the output buffer using offset-tracked + // snprintf. Each call returns the number of chars written (excluding NUL), + // so we advance 'off' and shrink 'rem' to guarantee we never overflow. + size_t off = 0; + size_t rem = buffer_size; + int w; // Basic status if (system_emergency_state) { - strcat(final_status, "EMERGENCY_STOP|"); + w = snprintf(status_buffer + off, rem, "System Status: EMERGENCY_STOP|"); } else { - strcat(final_status, "NORMAL|"); + w = snprintf(status_buffer + off, rem, "System Status: NORMAL|"); } + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } // Error information - snprintf(temp_buffer, sizeof(temp_buffer), "LastError:%d|ErrorCount:%lu|", - last_error, error_count); - strcat(final_status, temp_buffer); + w = snprintf(status_buffer + off, rem, "LastError:%d|ErrorCount:%lu|", + last_error, error_count); + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } // Sensor status - snprintf(temp_buffer, sizeof(temp_buffer), "IMU:%.1f,%.1f,%.1f|GPS:%.6f,%.6f|ALT:%.1f|", - Pitch_Sensor, Roll_Sensor, Yaw_Sensor, - RADAR_Latitude, RADAR_Longitude, RADAR_Altitude); - strcat(final_status, temp_buffer); + w = snprintf(status_buffer + off, rem, "IMU:%.1f,%.1f,%.1f|GPS:%.6f,%.6f|ALT:%.1f|", + Pitch_Sensor, Roll_Sensor, Yaw_Sensor, + RADAR_Latitude, RADAR_Longitude, RADAR_Altitude); + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } // LO Status bool tx_locked, rx_locked; ADF4382A_CheckLockStatus(&lo_manager, &tx_locked, &rx_locked); - snprintf(temp_buffer, sizeof(temp_buffer), "LO_TX:%s|LO_RX:%s|", - tx_locked ? "LOCKED" : "UNLOCKED", - rx_locked ? "LOCKED" : "UNLOCKED"); - strcat(final_status, temp_buffer); + w = snprintf(status_buffer + off, rem, "LO_TX:%s|LO_RX:%s|", + tx_locked ? "LOCKED" : "UNLOCKED", + rx_locked ? "LOCKED" : "UNLOCKED"); + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } // Temperature readings (8 variables) - // You'll need to populate these temperature values from your sensors - // For now, I'll show how to format them - replace with actual temperature readings Temperature_1 = ADS7830_Measure_SingleEnded(&hadc3, 0); Temperature_2 = ADS7830_Measure_SingleEnded(&hadc3, 1); Temperature_3 = ADS7830_Measure_SingleEnded(&hadc3, 2); @@ -961,11 +974,11 @@ void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) { Temperature_8 = ADS7830_Measure_SingleEnded(&hadc3, 7); // Format all 8 temperature variables - snprintf(temp_buffer, sizeof(temp_buffer), - "T1:%.1f|T2:%.1f|T3:%.1f|T4:%.1f|T5:%.1f|T6:%.1f|T7:%.1f|T8:%.1f|", - Temperature_1, Temperature_2, Temperature_3, Temperature_4, - Temperature_5, Temperature_6, Temperature_7, Temperature_8); - strcat(final_status, temp_buffer); + w = snprintf(status_buffer + off, rem, + "T1:%.1f|T2:%.1f|T3:%.1f|T4:%.1f|T5:%.1f|T6:%.1f|T7:%.1f|T8:%.1f|", + Temperature_1, Temperature_2, Temperature_3, Temperature_4, + Temperature_5, Temperature_6, Temperature_7, Temperature_8); + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } // RF Power Amplifier status (if enabled) if (PowerAmplifier) { @@ -975,18 +988,17 @@ void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) { } avg_current /= 16.0f; - snprintf(temp_buffer, sizeof(temp_buffer), "PA_AvgCurrent:%.2f|PA_Enabled:%d|", - avg_current, PowerAmplifier); - strcat(final_status, temp_buffer); + w = snprintf(status_buffer + off, rem, "PA_AvgCurrent:%.2f|PA_Enabled:%d|", + avg_current, PowerAmplifier); + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } } // Radar operation status - snprintf(temp_buffer, sizeof(temp_buffer), "BeamPos:%d|Azimuth:%d|ChirpCount:%d|", - n, y, m); - strcat(final_status, temp_buffer); + w = snprintf(status_buffer + off, rem, "BeamPos:%d|Azimuth:%d|ChirpCount:%d|", + n, y, m); + if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; } - // Copy to output buffer - strncpy(status_buffer, final_status, buffer_size - 1); + // NUL termination guaranteed by snprintf, but be safe status_buffer[buffer_size - 1] = '\0'; } @@ -1995,12 +2007,13 @@ int main(void) HAL_UART_Transmit(&huart3, (uint8_t*)emergency_msg, strlen(emergency_msg), 1000); DIAG_ERR("SYS", "SAFE MODE ACTIVE -- blinking all LEDs, waiting for system_emergency_state clear"); - // Blink all LEDs to indicate safe mode + // Blink all LEDs to indicate safe mode (500ms period, visible to operator) while (system_emergency_state) { HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin); HAL_GPIO_TogglePin(LED_2_GPIO_Port, LED_2_Pin); HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin); HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin); + HAL_Delay(250); } DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared"); } @@ -2114,6 +2127,16 @@ int main(void) runRadarPulseSequence(); + /* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13), + * adjust ADAR1000 VGA common gain once per radar frame (~258 ms). + * Only run when AGC is enabled — otherwise leave VGA gains untouched. */ + if (outerAgc.enabled) { + bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port, + FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET; + outerAgc.update(sat); + outerAgc.applyGain(adarManager); + } + /* [GAP-3 FIX 2] Kick hardware watchdog — if we don't reach here within * ~4 s, the IWDG resets the MCU automatically. */ HAL_IWDG_Refresh(&hiwdg); diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h index e4dbaf5..f5b8d0d 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h @@ -141,6 +141,15 @@ void Error_Handler(void); #define EN_DIS_RFPA_VDD_GPIO_Port GPIOD #define EN_DIS_COOLING_Pin GPIO_PIN_7 #define EN_DIS_COOLING_GPIO_Port GPIOD + +/* FPGA digital I/O (directly connected GPIOs) */ +#define FPGA_DIG5_SAT_Pin GPIO_PIN_13 +#define FPGA_DIG5_SAT_GPIO_Port GPIOD +#define FPGA_DIG6_Pin GPIO_PIN_14 +#define FPGA_DIG6_GPIO_Port GPIOD +#define FPGA_DIG7_Pin GPIO_PIN_15 +#define FPGA_DIG7_GPIO_Port GPIOD + #define ADF4382_RX_CE_Pin GPIO_PIN_9 #define ADF4382_RX_CE_GPIO_Port GPIOG #define ADF4382_RX_CS_Pin GPIO_PIN_10 diff --git a/9_Firmware/9_1_Microcontroller/tests/Makefile b/9_Firmware/9_1_Microcontroller/tests/Makefile index a44f962..73e7857 100644 --- a/9_Firmware/9_1_Microcontroller/tests/Makefile +++ b/9_Firmware/9_1_Microcontroller/tests/Makefile @@ -16,10 +16,17 @@ ################################################################################ CC := cc +CXX := c++ CFLAGS := -std=c11 -Wall -Wextra -Wno-unused-parameter -g -O0 +CXXFLAGS := -std=c++17 -Wall -Wextra -Wno-unused-parameter -g -O0 # Shim headers come FIRST so they override real headers INCLUDES := -Ishims -I. -I../9_1_1_C_Cpp_Libraries +# C++ library directory (AGC, ADAR1000 Manager) +CXX_LIB_DIR := ../9_1_1_C_Cpp_Libraries +CXX_SRCS := $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.cpp +CXX_OBJS := ADAR1000_AGC.o ADAR1000_Manager.o + # Real source files compiled against mock headers REAL_SRC := ../9_1_1_C_Cpp_Libraries/adf4382a_manager.c @@ -62,7 +69,10 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \ # Tests that need platform_noos_stm32.o + mocks TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only -ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) +# C++ tests (AGC outer loop) +TESTS_WITH_CXX := test_agc_outer_loop + +ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_WITH_PLATFORM) $(TESTS_WITH_CXX) .PHONY: all build test clean \ $(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \ @@ -156,6 +166,24 @@ test_gap3_emergency_state_ordering: test_gap3_emergency_state_ordering.c $(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ) $(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@ +# --- C++ object rules --- + +ADAR1000_AGC.o: $(CXX_LIB_DIR)/ADAR1000_AGC.cpp $(CXX_LIB_DIR)/ADAR1000_AGC.h + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +ADAR1000_Manager.o: $(CXX_LIB_DIR)/ADAR1000_Manager.cpp $(CXX_LIB_DIR)/ADAR1000_Manager.h + $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +# --- C++ test binary rules --- + +test_agc_outer_loop: test_agc_outer_loop.cpp $(CXX_OBJS) $(MOCK_OBJS) + $(CXX) $(CXXFLAGS) $(INCLUDES) $< $(CXX_OBJS) $(MOCK_OBJS) -o $@ + +# Convenience target +.PHONY: test_agc +test_agc: test_agc_outer_loop + ./test_agc_outer_loop + # --- Individual test targets --- test_bug1: test_bug1_timed_sync_init_ordering diff --git a/9_Firmware/9_1_Microcontroller/tests/shims/main.h b/9_Firmware/9_1_Microcontroller/tests/shims/main.h index 9dd05df..6543adc 100644 --- a/9_Firmware/9_1_Microcontroller/tests/shims/main.h +++ b/9_Firmware/9_1_Microcontroller/tests/shims/main.h @@ -129,6 +129,14 @@ void Error_Handler(void); #define GYR_INT_Pin GPIO_PIN_8 #define GYR_INT_GPIO_Port GPIOC +/* FPGA digital I/O (directly connected GPIOs) */ +#define FPGA_DIG5_SAT_Pin GPIO_PIN_13 +#define FPGA_DIG5_SAT_GPIO_Port GPIOD +#define FPGA_DIG6_Pin GPIO_PIN_14 +#define FPGA_DIG6_GPIO_Port GPIOD +#define FPGA_DIG7_Pin GPIO_PIN_15 +#define FPGA_DIG7_GPIO_Port GPIOD + #ifdef __cplusplus } #endif diff --git a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c index 6420f04..2b33b4f 100644 --- a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c +++ b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.c @@ -175,7 +175,7 @@ void HAL_Delay(uint32_t Delay) mock_tick += Delay; } -HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, +HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) { spy_push((SpyRecord){ diff --git a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h index 1e0b61c..ac41470 100644 --- a/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h +++ b/9_Firmware/9_1_Microcontroller/tests/stm32_hal_mock.h @@ -34,6 +34,10 @@ typedef uint32_t HAL_StatusTypeDef; #define HAL_MAX_DELAY 0xFFFFFFFFU +#ifndef __NOP +#define __NOP() ((void)0) +#endif + /* ========================= GPIO Types ============================ */ typedef struct { @@ -182,7 +186,7 @@ GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); uint32_t HAL_GetTick(void); void HAL_Delay(uint32_t Delay); -HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); +HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout); /* ========================= SPI stubs ============================== */ diff --git a/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp b/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp new file mode 100644 index 0000000..9b23d28 --- /dev/null +++ b/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp @@ -0,0 +1,361 @@ +// test_agc_outer_loop.cpp -- C++ unit tests for ADAR1000_AGC outer-loop AGC +// +// Tests the STM32 outer-loop AGC class that adjusts ADAR1000 VGA gain based +// on the FPGA's saturation flag. Uses the existing HAL mock/spy framework. +// +// Build: c++ -std=c++17 ... (see Makefile TESTS_WITH_CXX rule) + +#include +#include +#include + +// Shim headers override real STM32/diag headers +#include "stm32_hal_mock.h" +#include "ADAR1000_AGC.h" +#include "ADAR1000_Manager.h" + +// --------------------------------------------------------------------------- +// Linker symbols required by ADAR1000_Manager.cpp (pulled in via main.h shim) +// --------------------------------------------------------------------------- +uint8_t GUI_start_flag_received = 0; +uint8_t USB_Buffer[64] = {0}; +extern "C" void Error_Handler(void) {} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static int tests_passed = 0; +static int tests_total = 0; + +#define RUN_TEST(fn) \ + do { \ + tests_total++; \ + printf(" [%2d] %-55s ", tests_total, #fn); \ + fn(); \ + tests_passed++; \ + printf("PASS\n"); \ + } while (0) + +// --------------------------------------------------------------------------- +// Test 1: Default construction matches design spec +// --------------------------------------------------------------------------- +static void test_defaults() +{ + ADAR1000_AGC agc; + + assert(agc.agc_base_gain == 30); // kDefaultRxVgaGain + assert(agc.gain_step_down == 4); + assert(agc.gain_step_up == 1); + assert(agc.min_gain == 0); + assert(agc.max_gain == 127); + assert(agc.holdoff_frames == 4); + assert(agc.enabled == true); + assert(agc.holdoff_counter == 0); + assert(agc.last_saturated == false); + assert(agc.saturation_event_count == 0); + + // All cal offsets zero + for (int i = 0; i < AGC_TOTAL_CHANNELS; ++i) { + assert(agc.cal_offset[i] == 0); + } +} + +// --------------------------------------------------------------------------- +// Test 2: Saturation reduces gain by step_down +// --------------------------------------------------------------------------- +static void test_saturation_reduces_gain() +{ + ADAR1000_AGC agc; + uint8_t initial = agc.agc_base_gain; // 30 + + agc.update(true); // saturation + + assert(agc.agc_base_gain == initial - agc.gain_step_down); // 26 + assert(agc.last_saturated == true); + assert(agc.holdoff_counter == 0); +} + +// --------------------------------------------------------------------------- +// Test 3: Holdoff prevents premature gain-up +// --------------------------------------------------------------------------- +static void test_holdoff_prevents_early_gain_up() +{ + ADAR1000_AGC agc; + agc.update(true); // saturate once -> gain = 26 + uint8_t after_sat = agc.agc_base_gain; + + // Feed (holdoff_frames - 1) clear frames — should NOT increase gain + for (uint8_t i = 0; i < agc.holdoff_frames - 1; ++i) { + agc.update(false); + assert(agc.agc_base_gain == after_sat); + } + + // holdoff_counter should be holdoff_frames - 1 + assert(agc.holdoff_counter == agc.holdoff_frames - 1); +} + +// --------------------------------------------------------------------------- +// Test 4: Recovery after holdoff period +// --------------------------------------------------------------------------- +static void test_recovery_after_holdoff() +{ + ADAR1000_AGC agc; + agc.update(true); // saturate -> gain = 26 + uint8_t after_sat = agc.agc_base_gain; + + // Feed exactly holdoff_frames clear frames + for (uint8_t i = 0; i < agc.holdoff_frames; ++i) { + agc.update(false); + } + + assert(agc.agc_base_gain == after_sat + agc.gain_step_up); // 27 + assert(agc.holdoff_counter == 0); // reset after recovery +} + +// --------------------------------------------------------------------------- +// Test 5: Min gain clamping +// --------------------------------------------------------------------------- +static void test_min_gain_clamp() +{ + ADAR1000_AGC agc; + agc.min_gain = 10; + agc.agc_base_gain = 12; + agc.gain_step_down = 4; + + agc.update(true); // 12 - 4 = 8, but min = 10 + assert(agc.agc_base_gain == 10); + + agc.update(true); // already at min + assert(agc.agc_base_gain == 10); +} + +// --------------------------------------------------------------------------- +// Test 6: Max gain clamping +// --------------------------------------------------------------------------- +static void test_max_gain_clamp() +{ + ADAR1000_AGC agc; + agc.max_gain = 32; + agc.agc_base_gain = 31; + agc.gain_step_up = 2; + agc.holdoff_frames = 1; // immediate recovery + + agc.update(false); // 31 + 2 = 33, but max = 32 + assert(agc.agc_base_gain == 32); + + agc.update(false); // already at max + assert(agc.agc_base_gain == 32); +} + +// --------------------------------------------------------------------------- +// Test 7: Per-channel calibration offsets +// --------------------------------------------------------------------------- +static void test_calibration_offsets() +{ + ADAR1000_AGC agc; + agc.agc_base_gain = 30; + agc.min_gain = 0; + agc.max_gain = 60; + + agc.cal_offset[0] = 5; // 30 + 5 = 35 + agc.cal_offset[1] = -10; // 30 - 10 = 20 + agc.cal_offset[15] = 40; // 30 + 40 = 60 (clamped to max) + + assert(agc.effectiveGain(0) == 35); + assert(agc.effectiveGain(1) == 20); + assert(agc.effectiveGain(15) == 60); // clamped to max_gain + + // Negative clamp + agc.cal_offset[2] = -50; // 30 - 50 = -20, clamped to min_gain = 0 + assert(agc.effectiveGain(2) == 0); + + // Out-of-range index returns min_gain + assert(agc.effectiveGain(16) == agc.min_gain); +} + +// --------------------------------------------------------------------------- +// Test 8: Disabled AGC is a no-op +// --------------------------------------------------------------------------- +static void test_disabled_noop() +{ + ADAR1000_AGC agc; + agc.enabled = false; + uint8_t original = agc.agc_base_gain; + + agc.update(true); // should be ignored + assert(agc.agc_base_gain == original); + assert(agc.last_saturated == false); // not updated when disabled + assert(agc.saturation_event_count == 0); + + agc.update(false); // also ignored + assert(agc.agc_base_gain == original); +} + +// --------------------------------------------------------------------------- +// Test 9: applyGain() produces correct SPI writes +// --------------------------------------------------------------------------- +static void test_apply_gain_spi() +{ + spy_reset(); + + ADAR1000Manager mgr; // creates 4 devices + ADAR1000_AGC agc; + agc.agc_base_gain = 42; + + agc.applyGain(mgr); + + // Each channel: adarSetRxVgaGain -> adarWrite(gain) + adarWrite(LOAD_WORKING) + // Each adarWrite: CS_low (GPIO_WRITE) + SPI_TRANSMIT + CS_high (GPIO_WRITE) + // = 3 spy records per adarWrite + // = 6 spy records per channel + // = 16 channels * 6 = 96 total spy records + + // Verify SPI transmit count: 2 SPI calls per channel * 16 channels = 32 + int spi_count = spy_count_type(SPY_SPI_TRANSMIT); + assert(spi_count == 32); + + // Verify GPIO write count: 4 GPIO writes per channel (CS low + CS high for each of 2 adarWrite calls) + int gpio_writes = spy_count_type(SPY_GPIO_WRITE); + assert(gpio_writes == 64); // 16 ch * 2 adarWrite * 2 GPIO each +} + +// --------------------------------------------------------------------------- +// Test 10: resetState() clears counters but preserves config +// --------------------------------------------------------------------------- +static void test_reset_preserves_config() +{ + ADAR1000_AGC agc; + agc.agc_base_gain = 42; + agc.gain_step_down = 8; + agc.cal_offset[3] = -5; + + // Generate some state + agc.update(true); + agc.update(true); + assert(agc.saturation_event_count == 2); + assert(agc.last_saturated == true); + + agc.resetState(); + + // State cleared + assert(agc.holdoff_counter == 0); + assert(agc.last_saturated == false); + assert(agc.saturation_event_count == 0); + + // Config preserved + assert(agc.agc_base_gain == 42 - 8 - 8); // two saturations applied before reset + assert(agc.gain_step_down == 8); + assert(agc.cal_offset[3] == -5); +} + +// --------------------------------------------------------------------------- +// Test 11: Saturation counter increments correctly +// --------------------------------------------------------------------------- +static void test_saturation_counter() +{ + ADAR1000_AGC agc; + + for (int i = 0; i < 10; ++i) { + agc.update(true); + } + assert(agc.saturation_event_count == 10); + + // Clear frames don't increment saturation count + for (int i = 0; i < 5; ++i) { + agc.update(false); + } + assert(agc.saturation_event_count == 10); +} + +// --------------------------------------------------------------------------- +// Test 12: Mixed saturation/clear sequence +// --------------------------------------------------------------------------- +static void test_mixed_sequence() +{ + ADAR1000_AGC agc; + agc.agc_base_gain = 30; + agc.gain_step_down = 4; + agc.gain_step_up = 1; + agc.holdoff_frames = 3; + + // Saturate: 30 -> 26 + agc.update(true); + assert(agc.agc_base_gain == 26); + assert(agc.holdoff_counter == 0); + + // 2 clear frames (not enough for recovery) + agc.update(false); + agc.update(false); + assert(agc.agc_base_gain == 26); + assert(agc.holdoff_counter == 2); + + // Saturate again: 26 -> 22, counter resets + agc.update(true); + assert(agc.agc_base_gain == 22); + assert(agc.holdoff_counter == 0); + assert(agc.saturation_event_count == 2); + + // 3 clear frames -> recovery: 22 -> 23 + agc.update(false); + agc.update(false); + agc.update(false); + assert(agc.agc_base_gain == 23); + assert(agc.holdoff_counter == 0); + + // 3 more clear -> 23 -> 24 + agc.update(false); + agc.update(false); + agc.update(false); + assert(agc.agc_base_gain == 24); +} + +// --------------------------------------------------------------------------- +// Test 13: Effective gain with edge-case base_gain values +// --------------------------------------------------------------------------- +static void test_effective_gain_edge_cases() +{ + ADAR1000_AGC agc; + agc.min_gain = 5; + agc.max_gain = 250; + + // Base gain at zero with positive offset + agc.agc_base_gain = 0; + agc.cal_offset[0] = 3; + assert(agc.effectiveGain(0) == 5); // 0 + 3 = 3, clamped to min_gain=5 + + // Base gain at max with zero offset + agc.agc_base_gain = 250; + agc.cal_offset[0] = 0; + assert(agc.effectiveGain(0) == 250); + + // Base gain at max with positive offset -> clamped + agc.agc_base_gain = 250; + agc.cal_offset[0] = 10; + assert(agc.effectiveGain(0) == 250); // clamped to max_gain +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- +int main() +{ + printf("=== ADAR1000_AGC Outer-Loop Unit Tests ===\n"); + + RUN_TEST(test_defaults); + RUN_TEST(test_saturation_reduces_gain); + RUN_TEST(test_holdoff_prevents_early_gain_up); + RUN_TEST(test_recovery_after_holdoff); + RUN_TEST(test_min_gain_clamp); + RUN_TEST(test_max_gain_clamp); + RUN_TEST(test_calibration_offsets); + RUN_TEST(test_disabled_noop); + RUN_TEST(test_apply_gain_spi); + RUN_TEST(test_reset_preserves_config); + RUN_TEST(test_saturation_counter); + RUN_TEST(test_mixed_sequence); + RUN_TEST(test_effective_gain_edge_cases); + + printf("=== Results: %d/%d passed ===\n", tests_passed, tests_total); + return (tests_passed == tests_total) ? 0 : 1; +} diff --git a/9_Firmware/9_2_FPGA/adc_clk_mmcm.v b/9_Firmware/9_2_FPGA/adc_clk_mmcm.v index 1301e7f..055cf4e 100644 --- a/9_Firmware/9_2_FPGA/adc_clk_mmcm.v +++ b/9_Firmware/9_2_FPGA/adc_clk_mmcm.v @@ -212,6 +212,11 @@ BUFG bufg_feedback ( // ---- Output BUFG ---- // Routes the jitter-cleaned 400 MHz CLKOUT0 onto a global clock network. +// DONT_TOUCH prevents phys_opt_design AggressiveExplore from replicating this +// BUFG into a cascaded chain (4 BUFGs in series observed in Build 26), which +// added ~243ps of clock insertion delay and caused -187ps clock skew on the +// NCO→DSP mixer critical path. +(* DONT_TOUCH = "TRUE" *) BUFG bufg_clk400m ( .I(clk_mmcm_out0), .O(clk_400m_out) diff --git a/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v b/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v index d7bae17..76ade79 100644 --- a/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v +++ b/9_Firmware/9_2_FPGA/cic_decimator_4x_enhanced.v @@ -66,13 +66,13 @@ reg signed [COMB_WIDTH-1:0] comb_delay [0:STAGES-1][0:COMB_DELAY-1]; // Pipeline valid for comb stages 1-4: delayed by 1 cycle vs comb_pipe to // account for CREG+AREG+BREG pipeline inside comb_0_dsp (explicit DSP48E1). // Comb[0] result appears 1 cycle after data_valid_comb_pipe. -(* keep = "true", max_fanout = 4 *) reg data_valid_comb_0_out; +(* keep = "true", max_fanout = 16 *) reg data_valid_comb_0_out; // Enhanced control and monitoring reg [1:0] decimation_counter; -(* keep = "true", max_fanout = 4 *) reg data_valid_delayed; -(* keep = "true", max_fanout = 4 *) reg data_valid_comb; -(* keep = "true", max_fanout = 4 *) reg data_valid_comb_pipe; +(* keep = "true", max_fanout = 16 *) reg data_valid_delayed; +(* keep = "true", max_fanout = 16 *) reg data_valid_comb; +(* keep = "true", max_fanout = 16 *) reg data_valid_comb_pipe; reg [7:0] output_counter; reg [ACC_WIDTH-1:0] max_integrator_value; reg overflow_detected; diff --git a/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc b/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc index b581940..3777d33 100644 --- a/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc +++ b/9_Firmware/9_2_FPGA/constraints/adc_clk_mmcm.xdc @@ -83,3 +83,13 @@ set_false_path -through [get_pins rx_inst/adc/mmcm_inst/mmcm_adc_400m/LOCKED] # Waiving hold on these 8 paths (adc_d_p[0..7] → IDDR) is standard practice # for source-synchronous LVDS ADC interfaces using BUFIO capture. set_false_path -hold -from [get_ports {adc_d_p[*]}] -to [get_clocks adc_dco_p] + +# -------------------------------------------------------------------------- +# Timing margin for 400 MHz critical paths +# -------------------------------------------------------------------------- +# Extra setup uncertainty forces Vivado to leave margin for temperature/voltage/ +# aging variation. Reduced from 200 ps to 100 ps after NCO→mixer pipeline +# register fix eliminated the dominant timing bottleneck (WNS went from +0.002ns +# to comfortable margin). 100 ps still provides ~4% guardband on the 2.5ns period. +# This is additive to the existing jitter-based uncertainty (~53 ps). +set_clock_uncertainty -setup -add 0.100 [get_clocks clk_mmcm_out0] diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index 1f1ee5f..c4d61f0 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -222,8 +222,16 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_*}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] # reset_n is DIG_4 (PD12) — constrained above in the RESET section -# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — available for FPGA→STM32 status -# Currently unused in RTL. Could be connected to status outputs if needed. +# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs +# DIG_5: AGC saturation flag (PD13 on STM32) +# DIG_6: reserved (PD14) +# DIG_7: reserved (PD15) +set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}] +set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}] +set_property PACKAGE_PIN H12 [get_ports {gpio_dig7}] +set_property IOSTANDARD LVCMOS33 [get_ports {gpio_dig*}] +set_property DRIVE 8 [get_ports {gpio_dig*}] +set_property SLEW SLOW [get_ports {gpio_dig*}] # ============================================================================ # ADC INTERFACE (LVDS — Bank 14, VCCO=3.3V) diff --git a/9_Firmware/9_2_FPGA/ddc_400m.v b/9_Firmware/9_2_FPGA/ddc_400m.v index c2bee9a..470ad14 100644 --- a/9_Firmware/9_2_FPGA/ddc_400m.v +++ b/9_Firmware/9_2_FPGA/ddc_400m.v @@ -102,14 +102,19 @@ wire signed [17:0] debug_mixed_q_trunc; reg [7:0] signal_power_i, signal_power_q; // Internal mixing signals -// DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 handles all internal pipelining -// Latency: 4 cycles (1 for AREG/BREG, 1 for MREG, 1 for PREG, 1 for post-DSP retiming) +// Pipeline: NCO fabric reg (1) + DSP48E1 AREG/BREG (1) + MREG (1) + PREG (1) + retiming (1) = 5 cycles +// The NCO fabric pipeline register was added to break the long NCO→DSP B-port route +// (1.505ns routing in Build 26, WNS=+0.002ns). With BREG=1 still active inside the DSP, +// total latency increases by 1 cycle (2.5ns at 400MHz — negligible for radar). wire signed [MIXER_WIDTH-1:0] adc_signed_w; reg signed [MIXER_WIDTH + NCO_WIDTH -1:0] mixed_i, mixed_q; reg mixed_valid; reg mixer_overflow_i, mixer_overflow_q; -// Pipeline valid tracking: 4-stage shift register (3 for DSP48E1 + 1 for post-DSP retiming) -reg [3:0] dsp_valid_pipe; +// Pipeline valid tracking: 5-stage shift register (1 NCO pipe + 3 DSP48E1 + 1 retiming) +reg [4:0] dsp_valid_pipe; +// NCO→DSP pipeline registers — breaks the long NCO sin/cos → DSP48E1 B-port route +// DONT_TOUCH prevents Vivado from absorbing these into the DSP or optimizing away +(* DONT_TOUCH = "TRUE" *) reg signed [15:0] cos_nco_pipe, sin_nco_pipe; // Post-DSP retiming registers — breaks DSP48E1 CLK→P to fabric timing path // This extra pipeline stage absorbs the 1.866ns DSP output prop delay + routing, // ensuring WNS > 0 at 400 MHz regardless of placement seed @@ -210,11 +215,11 @@ nco_400m_enhanced nco_core ( // // Architecture: // ADC data → sign-extend to 18b → DSP48E1 A-port (AREG=1 pipelines it) -// NCO cos/sin → sign-extend to 18b → DSP48E1 B-port (BREG=1 pipelines it) +// NCO cos/sin → fabric pipeline reg → DSP48E1 B-port (BREG=1 pipelines it) // Multiply result captured by MREG=1, then output registered by PREG=1 // force_saturation override applied AFTER DSP48E1 output (not on input path) // -// Latency: 3 clock cycles (AREG/BREG + MREG + PREG) +// Latency: 4 clock cycles (1 NCO pipe + 1 AREG/BREG + 1 MREG + 1 PREG) + 1 retiming = 5 total // PREG=1 absorbs DSP48E1 CLK→P delay internally, preventing fabric timing violations // In simulation (Icarus), uses behavioral equivalent since DSP48E1 is Xilinx-only // ============================================================================ @@ -223,24 +228,35 @@ nco_400m_enhanced nco_core ( assign adc_signed_w = {1'b0, adc_data, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} - {1'b0, {ADC_WIDTH{1'b1}}, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} / 2; -// Valid pipeline: 4-stage shift register (3 for DSP48E1 AREG+MREG+PREG + 1 for retiming) +// Valid pipeline: 5-stage shift register (1 NCO pipe + 3 DSP48E1 AREG+MREG+PREG + 1 retiming) always @(posedge clk_400m or negedge reset_n_400m) begin if (!reset_n_400m) begin - dsp_valid_pipe <= 4'b0000; + dsp_valid_pipe <= 5'b00000; end else begin - dsp_valid_pipe <= {dsp_valid_pipe[2:0], (nco_ready && adc_data_valid_i && adc_data_valid_q)}; + dsp_valid_pipe <= {dsp_valid_pipe[3:0], (nco_ready && adc_data_valid_i && adc_data_valid_q)}; end end `ifdef SIMULATION // ---- Behavioral model for Icarus Verilog simulation ---- -// Mimics DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 (3-cycle latency) +// Mimics NCO pipeline + DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 (4-cycle DSP + 1 NCO pipe) reg signed [MIXER_WIDTH-1:0] adc_signed_reg; // Models AREG reg signed [15:0] cos_pipe_reg, sin_pipe_reg; // Models BREG reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_internal, mult_q_internal; // Models MREG reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_reg, mult_q_reg; // Models PREG -// Stage 1: AREG/BREG equivalent +// Stage 0: NCO pipeline — breaks long NCO→DSP route (matches synthesis fabric registers) +always @(posedge clk_400m or negedge reset_n_400m) begin + if (!reset_n_400m) begin + cos_nco_pipe <= 0; + sin_nco_pipe <= 0; + end else begin + cos_nco_pipe <= cos_out; + sin_nco_pipe <= sin_out; + end +end + +// Stage 1: AREG/BREG equivalent (uses pipelined NCO outputs) always @(posedge clk_400m or negedge reset_n_400m) begin if (!reset_n_400m) begin adc_signed_reg <= 0; @@ -248,8 +264,8 @@ always @(posedge clk_400m or negedge reset_n_400m) begin sin_pipe_reg <= 0; end else begin adc_signed_reg <= adc_signed_w; - cos_pipe_reg <= cos_out; - sin_pipe_reg <= sin_out; + cos_pipe_reg <= cos_nco_pipe; + sin_pipe_reg <= sin_nco_pipe; end end @@ -291,6 +307,20 @@ end // This guarantees AREG/BREG/MREG are used, achieving timing closure at 400 MHz wire [47:0] dsp_p_i, dsp_p_q; +// NCO pipeline stage — breaks the long NCO sin/cos → DSP48E1 B-port route +// (1.505ns routing observed in Build 26). These fabric registers are placed +// near the DSP by the placer, splitting the route into two shorter segments. +// DONT_TOUCH on the reg declaration (above) prevents absorption/retiming. +always @(posedge clk_400m or negedge reset_n_400m) begin + if (!reset_n_400m) begin + cos_nco_pipe <= 0; + sin_nco_pipe <= 0; + end else begin + cos_nco_pipe <= cos_out; + sin_nco_pipe <= sin_out; + end +end + // DSP48E1 for I-channel mixer (adc_signed * cos_out) DSP48E1 #( // Feature control attributes @@ -350,7 +380,7 @@ DSP48E1 #( .CEINMODE(1'b0), // Data ports .A({{12{adc_signed_w[MIXER_WIDTH-1]}}, adc_signed_w}), // Sign-extend 18b to 30b - .B({{2{cos_out[15]}}, cos_out}), // Sign-extend 16b to 18b + .B({{2{cos_nco_pipe[15]}}, cos_nco_pipe}), // Sign-extend 16b to 18b (pipelined) .C(48'b0), .D(25'b0), .CARRYIN(1'b0), @@ -432,7 +462,7 @@ DSP48E1 #( .CED(1'b0), .CEINMODE(1'b0), .A({{12{adc_signed_w[MIXER_WIDTH-1]}}, adc_signed_w}), - .B({{2{sin_out[15]}}, sin_out}), + .B({{2{sin_nco_pipe[15]}}, sin_nco_pipe}), .C(48'b0), .D(25'b0), .CARRYIN(1'b0), @@ -492,7 +522,7 @@ always @(posedge clk_400m or negedge reset_n_400m) begin mixer_overflow_q <= 0; saturation_count <= 0; overflow_detected <= 0; - end else if (dsp_valid_pipe[3]) begin + end else if (dsp_valid_pipe[4]) begin // Force saturation for testing (applied after DSP output, not on input path) if (force_saturation_sync) begin mixed_i <= 34'h1FFFFFFFF; diff --git a/9_Firmware/9_2_FPGA/fpga_self_test.v b/9_Firmware/9_2_FPGA/fpga_self_test.v index 420f714..c54c208 100644 --- a/9_Firmware/9_2_FPGA/fpga_self_test.v +++ b/9_Firmware/9_2_FPGA/fpga_self_test.v @@ -296,7 +296,7 @@ always @(posedge clk or negedge reset_n) begin state <= ST_DONE; end end - // Timeout: if no ADC data after 10000 cycles, FAIL + // Timeout: if no ADC data after 1000 cycles (10 us @ 100 MHz), FAIL step_cnt <= step_cnt + 1; if (step_cnt >= 10'd1000 && adc_cap_cnt == 0) begin result_flags[4] <= 1'b0; diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index c417092..4be1571 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -42,6 +42,13 @@ module radar_receiver_final ( // [2:0]=shift amount: 0..7 bits. Default 0 = pass-through. input wire [3:0] host_gain_shift, + // AGC configuration (opcodes 0x28-0x2C, active only when agc_enable=1) + input wire host_agc_enable, // 0x28: 0=manual, 1=auto AGC + input wire [7:0] host_agc_target, // 0x29: target peak magnitude + input wire [3:0] host_agc_attack, // 0x2A: gain-down step on clipping + input wire [3:0] host_agc_decay, // 0x2B: gain-up step when weak + input wire [3:0] host_agc_holdoff, // 0x2C: frames before gain-up + // STM32 toggle signals for mode 00 (STM32-driven) pass-through. // These are CDC-synchronized in radar_system_top.v / radar_transmitter.v // before reaching this module. In mode 00, the RX mode controller uses @@ -60,7 +67,12 @@ module radar_receiver_final ( // ADC raw data tap (clk_100m domain, post-DDC, for self-test / debug) output wire [15:0] dbg_adc_i, // DDC output I (16-bit signed, 100 MHz) output wire [15:0] dbg_adc_q, // DDC output Q (16-bit signed, 100 MHz) - output wire dbg_adc_valid // DDC output valid (100 MHz) + output wire dbg_adc_valid, // DDC output valid (100 MHz) + + // AGC status outputs (for status readback / STM32 outer loop) + output wire [7:0] agc_saturation_count, // Per-frame clipped sample count + output wire [7:0] agc_peak_magnitude, // Per-frame peak (upper 8 bits) + output wire [3:0] agc_current_gain // Effective gain_shift encoding ); // ========== INTERNAL SIGNALS ========== @@ -86,7 +98,9 @@ wire adc_valid_sync; // Gain-controlled signals (between DDC output and matched filter) wire signed [15:0] gc_i, gc_q; wire gc_valid; -wire [7:0] gc_saturation_count; // Diagnostic: clipped sample counter +wire [7:0] gc_saturation_count; // Diagnostic: per-frame clipped sample counter +wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude +wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift // Reference signals for the processing chain wire [15:0] long_chirp_real, long_chirp_imag; @@ -160,7 +174,7 @@ wire clk_400m; // the buffered 400MHz DCO clock via adc_dco_bufg, avoiding duplicate // IBUFDS instantiations on the same LVDS clock pair. -// 1. ADC + CDC + AGC +// 1. ADC + CDC + Digital Gain // CMOS Output Interface (400MHz Domain) wire [7:0] adc_data_cmos; // 8-bit ADC data (CMOS, from ad9484_interface_400m) @@ -222,9 +236,10 @@ ddc_input_interface ddc_if ( .data_sync_error() ); -// 2b. Digital Gain Control (Fix 3) +// 2b. Digital Gain Control with AGC // Host-configurable power-of-2 shift between DDC output and matched filter. -// Default gain_shift=0 → pass-through (no behavioral change from baseline). +// Default gain_shift=0, agc_enable=0 → pass-through (no behavioral change). +// When agc_enable=1: auto-adjusts gain per frame based on peak/saturation. rx_gain_control gain_ctrl ( .clk(clk), .reset_n(reset_n), @@ -232,10 +247,21 @@ rx_gain_control gain_ctrl ( .data_q_in(adc_q_scaled), .valid_in(adc_valid_sync), .gain_shift(host_gain_shift), + // AGC configuration + .agc_enable(host_agc_enable), + .agc_target(host_agc_target), + .agc_attack(host_agc_attack), + .agc_decay(host_agc_decay), + .agc_holdoff(host_agc_holdoff), + // Frame boundary from Doppler processor + .frame_boundary(doppler_frame_done), + // Outputs .data_i_out(gc_i), .data_q_out(gc_q), .valid_out(gc_valid), - .saturation_count(gc_saturation_count) + .saturation_count(gc_saturation_count), + .peak_magnitude(gc_peak_magnitude), + .current_gain(gc_current_gain) ); // 3. Dual Chirp Memory Loader @@ -474,4 +500,9 @@ assign dbg_adc_i = adc_i_scaled; assign dbg_adc_q = adc_q_scaled; assign dbg_adc_valid = adc_valid_sync; +// ========== AGC STATUS OUTPUTS ========== +assign agc_saturation_count = gc_saturation_count; +assign agc_peak_magnitude = gc_peak_magnitude; +assign agc_current_gain = gc_current_gain; + endmodule diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index 9d6686e..dfafa65 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -125,7 +125,13 @@ module radar_system_top ( output wire [5:0] dbg_range_bin, // System status - output wire [3:0] system_status + output wire [3:0] system_status, + + // FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board) + // Used by STM32 outer AGC loop to read saturation state without USB polling. + output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected) + output wire gpio_dig6, // DIG_6 (G12→PD14): reserved (tied low) + output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low) ); // ============================================================================ @@ -187,6 +193,11 @@ wire [15:0] rx_dbg_adc_i; wire [15:0] rx_dbg_adc_q; wire rx_dbg_adc_valid; +// AGC status from receiver (for status readback and GPIO) +wire [7:0] rx_agc_saturation_count; +wire [7:0] rx_agc_peak_magnitude; +wire [3:0] rx_agc_current_gain; + // Data packing for USB wire [31:0] usb_range_profile; wire usb_range_valid; @@ -259,6 +270,13 @@ reg host_cfar_enable; // Opcode 0x25: 1=CFAR, 0=simple threshold reg host_mti_enable; // Opcode 0x26: 1=MTI active, 0=pass-through reg [2:0] host_dc_notch_width; // Opcode 0x27: DC notch ±width bins (0=off, 1..7) +// AGC configuration registers (host-configurable via USB, opcodes 0x28-0x2C) +reg host_agc_enable; // Opcode 0x28: 0=manual gain, 1=auto AGC +reg [7:0] host_agc_target; // Opcode 0x29: target peak magnitude (default 200) +reg [3:0] host_agc_attack; // Opcode 0x2A: gain-down step on clipping (default 1) +reg [3:0] host_agc_decay; // Opcode 0x2B: gain-up step when weak (default 1) +reg [3:0] host_agc_holdoff; // Opcode 0x2C: frames to wait before gain-up (default 4) + // Board bring-up self-test registers (opcode 0x30 trigger, 0x31 readback) reg host_self_test_trigger; // Opcode 0x30: self-clearing pulse wire self_test_busy; @@ -518,6 +536,12 @@ radar_receiver_final rx_inst ( .host_chirps_per_elev(host_chirps_per_elev), // Fix 3: digital gain control .host_gain_shift(host_gain_shift), + // AGC configuration (opcodes 0x28-0x2C) + .host_agc_enable(host_agc_enable), + .host_agc_target(host_agc_target), + .host_agc_attack(host_agc_attack), + .host_agc_decay(host_agc_decay), + .host_agc_holdoff(host_agc_holdoff), // STM32 toggle signals for RX mode controller (mode 00 pass-through). // These are the raw GPIO inputs — the RX mode controller's edge detectors // (inside radar_mode_controller) handle debouncing/edge detection. @@ -532,7 +556,11 @@ radar_receiver_final rx_inst ( // ADC debug tap (for self-test / bring-up) .dbg_adc_i(rx_dbg_adc_i), .dbg_adc_q(rx_dbg_adc_q), - .dbg_adc_valid(rx_dbg_adc_valid) + .dbg_adc_valid(rx_dbg_adc_valid), + // AGC status outputs + .agc_saturation_count(rx_agc_saturation_count), + .agc_peak_magnitude(rx_agc_peak_magnitude), + .agc_current_gain(rx_agc_current_gain) ); // ============================================================================ @@ -744,7 +772,13 @@ if (USB_MODE == 0) begin : gen_ft601 // Self-test status readback .status_self_test_flags(self_test_flags_latched), .status_self_test_detail(self_test_detail_latched), - .status_self_test_busy(self_test_busy) + .status_self_test_busy(self_test_busy), + + // AGC status readback + .status_agc_current_gain(rx_agc_current_gain), + .status_agc_peak_magnitude(rx_agc_peak_magnitude), + .status_agc_saturation_count(rx_agc_saturation_count), + .status_agc_enable(host_agc_enable) ); // FT2232H ports unused in FT601 mode — tie off @@ -805,7 +839,13 @@ end else begin : gen_ft2232h // Self-test status readback .status_self_test_flags(self_test_flags_latched), .status_self_test_detail(self_test_detail_latched), - .status_self_test_busy(self_test_busy) + .status_self_test_busy(self_test_busy), + + // AGC status readback + .status_agc_current_gain(rx_agc_current_gain), + .status_agc_peak_magnitude(rx_agc_peak_magnitude), + .status_agc_saturation_count(rx_agc_saturation_count), + .status_agc_enable(host_agc_enable) ); // FT601 ports unused in FT2232H mode — tie off @@ -892,6 +932,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin // Ground clutter removal defaults (disabled — backward-compatible) host_mti_enable <= 1'b0; // MTI off host_dc_notch_width <= 3'd0; // DC notch off + // AGC defaults (disabled — backward-compatible with manual gain) + host_agc_enable <= 1'b0; // AGC off (manual gain) + host_agc_target <= 8'd200; // Target peak magnitude + host_agc_attack <= 4'd1; // 1-step gain-down on clipping + host_agc_decay <= 4'd1; // 1-step gain-up when weak + host_agc_holdoff <= 4'd4; // 4 frames before gain-up // Self-test defaults host_self_test_trigger <= 1'b0; // Self-test idle end else begin @@ -936,6 +982,12 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin // Ground clutter removal opcodes 8'h26: host_mti_enable <= usb_cmd_value[0]; 8'h27: host_dc_notch_width <= usb_cmd_value[2:0]; + // AGC configuration opcodes + 8'h28: host_agc_enable <= usb_cmd_value[0]; + 8'h29: host_agc_target <= usb_cmd_value[7:0]; + 8'h2A: host_agc_attack <= usb_cmd_value[3:0]; + 8'h2B: host_agc_decay <= usb_cmd_value[3:0]; + 8'h2C: host_agc_holdoff <= usb_cmd_value[3:0]; // Board bring-up self-test opcodes 8'h30: host_self_test_trigger <= 1'b1; // Trigger self-test 8'h31: host_status_request <= 1'b1; // Self-test readback (status alias) @@ -978,6 +1030,16 @@ end assign system_status = status_reg; +// ============================================================================ +// FPGA→STM32 GPIO OUTPUTS (DIG_5, DIG_6, DIG_7) +// ============================================================================ +// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0. +// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain. +// DIG_6, DIG_7: Reserved (tied low for future use). +assign gpio_dig5 = (rx_agc_saturation_count != 8'd0); +assign gpio_dig6 = 1'b0; +assign gpio_dig7 = 1'b0; + // ============================================================================ // DEBUG AND VERIFICATION // ============================================================================ diff --git a/9_Firmware/9_2_FPGA/radar_system_top_50t.v b/9_Firmware/9_2_FPGA/radar_system_top_50t.v index f2f9738..fc3585a 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top_50t.v +++ b/9_Firmware/9_2_FPGA/radar_system_top_50t.v @@ -76,7 +76,12 @@ module radar_system_top_50t ( output wire ft_rd_n, // Read strobe (active low) output wire ft_wr_n, // Write strobe (active low) output wire ft_oe_n, // Output enable / bus direction - output wire ft_siwu // Send Immediate / WakeUp + output wire ft_siwu, // Send Immediate / WakeUp + + // ===== FPGA→STM32 GPIO (Bank 15: 3.3V) ===== + output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag + output wire gpio_dig6, // DIG_6 (G12→PD14): reserved + output wire gpio_dig7 // DIG_7 (H12→PD15): reserved ); // ===== Tie-off wires for unconstrained FT601 inputs (inactive with USB_MODE=1) ===== @@ -207,7 +212,12 @@ module radar_system_top_50t ( .dbg_doppler_valid (dbg_doppler_valid_nc), .dbg_doppler_bin (dbg_doppler_bin_nc), .dbg_range_bin (dbg_range_bin_nc), - .system_status (system_status_nc) + .system_status (system_status_nc), + + // ----- FPGA→STM32 GPIO (DIG_5..DIG_7) ----- + .gpio_dig5 (gpio_dig5), + .gpio_dig6 (gpio_dig6), + .gpio_dig7 (gpio_dig7) ); endmodule diff --git a/9_Firmware/9_2_FPGA/rx_gain_control.v b/9_Firmware/9_2_FPGA/rx_gain_control.v index 8b258d7..f43ac0b 100644 --- a/9_Firmware/9_2_FPGA/rx_gain_control.v +++ b/9_Firmware/9_2_FPGA/rx_gain_control.v @@ -3,19 +3,32 @@ /** * rx_gain_control.v * - * Host-configurable digital gain control for the receive path. - * Placed between DDC output (ddc_input_interface) and matched filter input. + * Digital gain control with optional per-frame automatic gain control (AGC) + * for the receive path. Placed between DDC output and matched filter input. * - * Features: - * - Bidirectional power-of-2 gain shift (arithmetic shift) + * Manual mode (agc_enable=0): + * - Uses host_gain_shift directly (backward-compatible, no behavioral change) * - gain_shift[3] = direction: 0 = left shift (amplify), 1 = right shift (attenuate) * - gain_shift[2:0] = amount: 0..7 bits - * - Symmetric saturation to ±32767 on overflow (left shift only) - * - Saturation counter: 8-bit, counts samples that clipped (wraps at 255) - * - 1-cycle latency, valid-in/valid-out pipeline - * - Zero-overhead pass-through when gain_shift == 0 + * - Symmetric saturation to ±32767 on overflow * - * Intended insertion point in radar_receiver_final.v: + * AGC mode (agc_enable=1): + * - Per-frame automatic gain adjustment based on peak/saturation metrics + * - Internal signed gain: -7 (max attenuation) to +7 (max amplification) + * - On frame_boundary: + * * If saturation detected: gain -= agc_attack (fast, immediate) + * * Else if peak < target after holdoff frames: gain += agc_decay (slow) + * * Else: hold current gain + * - host_gain_shift serves as initial gain when AGC first enabled + * + * Status outputs (for readback via status_words): + * - current_gain[3:0]: effective gain_shift encoding (manual or AGC) + * - peak_magnitude[7:0]: per-frame peak |sample| (upper 8 bits of 15-bit value) + * - saturation_count[7:0]: per-frame clipped sample count (capped at 255) + * + * Timing: 1-cycle data latency, valid-in/valid-out pipeline. + * + * Insertion point in radar_receiver_final.v: * ddc_input_interface → rx_gain_control → matched_filter_multi_segment */ @@ -28,27 +41,75 @@ module rx_gain_control ( input wire signed [15:0] data_q_in, input wire valid_in, - // Gain configuration (from host via USB command) - // [3] = direction: 0=amplify (left shift), 1=attenuate (right shift) - // [2:0] = shift amount: 0..7 bits + // Host gain configuration (from USB command opcode 0x16) + // [3]=direction: 0=amplify (left shift), 1=attenuate (right shift) + // [2:0]=shift amount: 0..7 bits. Default 0x00 = pass-through. + // In AGC mode: serves as initial gain on AGC enable transition. input wire [3:0] gain_shift, + // AGC configuration inputs (from host via USB, opcodes 0x28-0x2C) + input wire agc_enable, // 0x28: 0=manual gain, 1=auto AGC + input wire [7:0] agc_target, // 0x29: target peak magnitude (unsigned, default 200) + input wire [3:0] agc_attack, // 0x2A: attenuation step on clipping (default 1) + input wire [3:0] agc_decay, // 0x2B: amplification step when weak (default 1) + input wire [3:0] agc_holdoff, // 0x2C: frames to wait before gain-up (default 4) + + // Frame boundary pulse (1 clk cycle, from Doppler frame_complete) + input wire frame_boundary, + // Data output (to matched filter) output reg signed [15:0] data_i_out, output reg signed [15:0] data_q_out, output reg valid_out, - // Diagnostics - output reg [7:0] saturation_count // Number of clipped samples (wraps at 255) + // Diagnostics / status readback + output reg [7:0] saturation_count, // Per-frame clipped sample count (capped at 255) + output reg [7:0] peak_magnitude, // Per-frame peak |sample| (upper 8 bits of 15-bit) + output reg [3:0] current_gain // Current effective gain_shift (for status readback) ); -// Decompose gain_shift -wire shift_right = gain_shift[3]; -wire [2:0] shift_amt = gain_shift[2:0]; +// ========================================================================= +// INTERNAL AGC STATE +// ========================================================================= -// ------------------------------------------------------------------------- -// Combinational shift + saturation -// ------------------------------------------------------------------------- +// Signed internal gain: -7 (max attenuation) to +7 (max amplification) +// Stored as 4-bit signed (range -8..+7, clamped to -7..+7) +reg signed [3:0] agc_gain; + +// Holdoff counter: counts frames without saturation before allowing gain-up +reg [3:0] holdoff_counter; + +// Per-frame accumulators (running, reset on frame_boundary) +reg [7:0] frame_sat_count; // Clipped samples this frame +reg [14:0] frame_peak; // Peak |sample| this frame (15-bit unsigned) + +// Previous AGC enable state (for detecting 0→1 transition) +reg agc_enable_prev; + +// Combinational helpers for inclusive frame-boundary snapshot +// (used when valid_in and frame_boundary coincide) +reg wire_frame_sat_incr; +reg wire_frame_peak_update; + +// ========================================================================= +// EFFECTIVE GAIN SELECTION +// ========================================================================= + +// Convert between signed internal gain and the gain_shift[3:0] encoding. +// gain_shift[3]=0, [2:0]=N → amplify by N bits (internal gain = +N) +// gain_shift[3]=1, [2:0]=N → attenuate by N bits (internal gain = -N) + +// Effective gain_shift used for the actual shift operation +wire [3:0] effective_gain; +assign effective_gain = agc_enable ? current_gain : gain_shift; + +// Decompose effective gain for shift logic +wire shift_right = effective_gain[3]; +wire [2:0] shift_amt = effective_gain[2:0]; + +// ========================================================================= +// COMBINATIONAL SHIFT + SATURATION +// ========================================================================= // Use wider intermediates to detect overflow on left shift. // 24 bits is enough: 16 + 7 shift = 23 significant bits max. @@ -69,26 +130,153 @@ wire signed [15:0] sat_i = overflow_i ? (shifted_i[23] ? -16'sd32768 : 16'sd3276 wire signed [15:0] sat_q = overflow_q ? (shifted_q[23] ? -16'sd32768 : 16'sd32767) : shifted_q[15:0]; -// ------------------------------------------------------------------------- -// Registered output stage (1-cycle latency) -// ------------------------------------------------------------------------- +// ========================================================================= +// PEAK MAGNITUDE TRACKING (combinational) +// ========================================================================= +// Absolute value of signed 16-bit: flip sign bit if negative. +// Result is 15-bit unsigned [0, 32767]. (We ignore -32768 → 32767 edge case.) +wire [14:0] abs_i = data_i_in[15] ? (~data_i_in[14:0] + 15'd1) : data_i_in[14:0]; +wire [14:0] abs_q = data_q_in[15] ? (~data_q_in[14:0] + 15'd1) : data_q_in[14:0]; +wire [14:0] max_iq = (abs_i > abs_q) ? abs_i : abs_q; + +// ========================================================================= +// SIGNED GAIN ↔ GAIN_SHIFT ENCODING CONVERSION +// ========================================================================= +// Convert signed agc_gain to gain_shift[3:0] encoding +function [3:0] signed_to_encoding; + input signed [3:0] g; + begin + if (g >= 0) + signed_to_encoding = {1'b0, g[2:0]}; // amplify + else + signed_to_encoding = {1'b1, (~g[2:0]) + 3'd1}; // attenuate: -g + end +endfunction + +// Convert gain_shift[3:0] encoding to signed gain +function signed [3:0] encoding_to_signed; + input [3:0] enc; + begin + if (enc[3] == 1'b0) + encoding_to_signed = {1'b0, enc[2:0]}; // +0..+7 + else + encoding_to_signed = -$signed({1'b0, enc[2:0]}); // -1..-7 + end +endfunction + +// ========================================================================= +// CLAMPING HELPER +// ========================================================================= +// Clamp a wider signed value to [-7, +7] +function signed [3:0] clamp_gain; + input signed [4:0] val; // 5-bit to handle overflow from add + begin + if (val > 5'sd7) + clamp_gain = 4'sd7; + else if (val < -5'sd7) + clamp_gain = -4'sd7; + else + clamp_gain = val[3:0]; + end +endfunction + +// ========================================================================= +// REGISTERED OUTPUT + AGC STATE MACHINE +// ========================================================================= always @(posedge clk or negedge reset_n) begin if (!reset_n) begin + // Data path data_i_out <= 16'sd0; data_q_out <= 16'sd0; valid_out <= 1'b0; + // Status outputs saturation_count <= 8'd0; + peak_magnitude <= 8'd0; + current_gain <= 4'd0; + // AGC internal state + agc_gain <= 4'sd0; + holdoff_counter <= 4'd0; + frame_sat_count <= 8'd0; + frame_peak <= 15'd0; + agc_enable_prev <= 1'b0; end else begin - valid_out <= valid_in; + // Track AGC enable transitions + agc_enable_prev <= agc_enable; + // Compute inclusive metrics: if valid_in fires this cycle, + // include current sample in the snapshot taken at frame_boundary. + // This avoids losing the last sample when valid_in and + // frame_boundary coincide (NBA last-write-wins would otherwise + // snapshot stale values then reset, dropping the sample entirely). + wire_frame_sat_incr = (valid_in && (overflow_i || overflow_q) + && (frame_sat_count != 8'hFF)); + wire_frame_peak_update = (valid_in && (max_iq > frame_peak)); + + // ---- Data pipeline (1-cycle latency) ---- + valid_out <= valid_in; if (valid_in) begin data_i_out <= sat_i; data_q_out <= sat_q; - // Count clipped samples (either channel clipping counts as 1) - if ((overflow_i || overflow_q) && (saturation_count != 8'hFF)) - saturation_count <= saturation_count + 8'd1; + // Per-frame saturation counting + if ((overflow_i || overflow_q) && (frame_sat_count != 8'hFF)) + frame_sat_count <= frame_sat_count + 8'd1; + + // Per-frame peak tracking (pre-gain, measures input signal level) + if (max_iq > frame_peak) + frame_peak <= max_iq; end + + // ---- Frame boundary: AGC update + metric snapshot ---- + if (frame_boundary) begin + // Snapshot per-frame metrics INCLUDING current sample if valid_in + saturation_count <= wire_frame_sat_incr + ? (frame_sat_count + 8'd1) + : frame_sat_count; + peak_magnitude <= wire_frame_peak_update + ? max_iq[14:7] + : frame_peak[14:7]; + + // Reset per-frame accumulators for next frame + frame_sat_count <= 8'd0; + frame_peak <= 15'd0; + + if (agc_enable) begin + // AGC auto-adjustment at frame boundary + // Use inclusive counts/peaks (accounting for simultaneous valid_in) + if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin + // Clipping detected: reduce gain immediately (attack) + agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) - + $signed({1'b0, agc_attack})); + holdoff_counter <= agc_holdoff; // Reset holdoff + end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7]) + < agc_target) begin + // Signal too weak: increase gain after holdoff expires + if (holdoff_counter == 4'd0) begin + agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) + + $signed({1'b0, agc_decay})); + end else begin + holdoff_counter <= holdoff_counter - 4'd1; + end + end else begin + // Signal in good range, no saturation: hold gain + // Reset holdoff so next weak frame has to wait again + holdoff_counter <= agc_holdoff; + end + end + end + + // ---- AGC enable transition: initialize from host gain ---- + if (agc_enable && !agc_enable_prev) begin + agc_gain <= encoding_to_signed(gain_shift); + holdoff_counter <= agc_holdoff; + end + + // ---- Update current_gain output ---- + if (agc_enable) + current_gain <= signed_to_encoding(agc_gain); + else + current_gain <= gain_shift; end end diff --git a/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl b/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl index 730b006..51a0e5e 100644 --- a/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl +++ b/9_Firmware/9_2_FPGA/scripts/50t/build_50t.tcl @@ -120,9 +120,10 @@ set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets {ft_clkout_IBUF}] # ---- Run implementation steps ---- opt_design -directive Explore -place_design -directive Explore +place_design -directive ExtraNetDelay_high +phys_opt_design -directive AggressiveExplore +route_design -directive AggressiveExplore phys_opt_design -directive AggressiveExplore -route_design -directive Explore phys_opt_design -directive AggressiveExplore set impl_elapsed [expr {[clock seconds] - $impl_start}] 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..b0bdf5c 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,0024ffdd +1036005000,0,1,fff7002d +1036015000,0,2,007d0098 +1036025000,0,3,ff1afe23 +1036035000,0,4,00130108 +1036045000,0,5,ff530243 +1036055000,0,6,0274fb9d +1036065000,0,7,fee40370 +1036075000,0,8,fe98ff6f +1036085000,0,9,ff8bffa9 +1036095000,0,10,03dffdd0 +1036105000,0,11,fe3c042d +1036115000,0,12,fdddfcc0 +1036125000,0,13,00ff00cb +1036135000,0,14,015400f7 +1036145000,0,15,ff22ff4c +1038025000,0,16,ffedfff1 +1038035000,0,17,00880078 +1038045000,0,18,fe31ffb7 +1038055000,0,19,026c00de +1038065000,0,20,fd2afd6f +1038075000,0,21,0293ffc3 +1038085000,0,22,fef10677 +1038095000,0,23,ff6efadf +1038105000,0,24,008900ad +1038115000,0,25,017a0014 +1038125000,0,26,fdc301a1 +1038135000,0,27,020cff8e +1038145000,0,28,fce0ffbb +1038155000,0,29,025f00b1 +1038165000,0,30,fe73fff1 +1038175000,0,31,008effbd +1040055000,1,0,ffcd0030 +1040065000,1,1,009d0057 +1040075000,1,2,fecbff41 +1040085000,1,3,0110007a +1040095000,1,4,0148ff8e +1040105000,1,5,fd2e0229 +1040115000,1,6,ffe5fd1c +1040125000,1,7,0483ffb6 +1040135000,1,8,fb2f02ce +1040145000,1,9,02110069 +1040155000,1,10,0095fb47 +1040165000,1,11,ffd003b8 +1040175000,1,12,fdfcfdac +1040185000,1,13,011801c3 +1040195000,1,14,0023003c +1040205000,1,15,0001ff54 +1042085000,1,16,004fffd0 +1042095000,1,17,0078fee5 +1042105000,1,18,fed201e1 +1042115000,1,19,0097fe5a +1042125000,1,20,00ab0130 +1042135000,1,21,ff73febb +1042145000,1,22,00f301a4 +1042155000,1,23,fea60124 +1042165000,1,24,fe03fe84 +1042175000,1,25,0184fea7 +1042185000,1,26,ff1cffeb +1042195000,1,27,03210146 +1042205000,1,28,fc5fff84 +1042215000,1,29,017d0025 +1042225000,1,30,0053ff88 +1042235000,1,31,fef600b0 +1044115000,2,0,00260082 +1044125000,2,1,ff99ff96 +1044135000,2,2,016e0095 +1044145000,2,3,0034ff2d +1044155000,2,4,fce100d7 +1044165000,2,5,0276ff6c +1044175000,2,6,00a0ffaa +1044185000,2,7,fdc400b8 +1044195000,2,8,01dcffc4 +1044205000,2,9,fe1901ae +1044215000,2,10,0304fcf3 +1044225000,2,11,fe240203 +1044235000,2,12,0031fe2f +1044245000,2,13,fefc0150 +1044255000,2,14,0182006a +1044265000,2,15,ff18ff30 +1046145000,2,16,005bff79 +1046155000,2,17,0004ffcc +1046165000,2,18,fef70100 +1046175000,2,19,0136fe70 +1046185000,2,20,ffb80156 +1046195000,2,21,01b1feff +1046205000,2,22,fdac03d6 +1046215000,2,23,fdfafa69 +1046225000,2,24,0289032f +1046235000,2,25,feacfecc +1046245000,2,26,004dffb2 +1046255000,2,27,00d60062 +1046265000,2,28,fee0fe8e +1046275000,2,29,020f0245 +1046285000,2,30,fda4feac +1046295000,2,31,006a0079 +1048175000,3,0,ffe10086 +1048185000,3,1,0026ffcd +1048195000,3,2,00990030 +1048205000,3,3,ffe4ff8a +1048215000,3,4,fe7100c5 +1048225000,3,5,0179ffc6 +1048235000,3,6,ff2ffd5e +1048245000,3,7,01ed0473 +1048255000,3,8,fbd9fdf4 +1048265000,3,9,037800d1 +1048275000,3,10,0033fe0a +1048285000,3,11,ff4202c0 +1048295000,3,12,ffa5fc5d +1048305000,3,13,ffa90298 +1048315000,3,14,00950004 +1048325000,3,15,ffcdff0f +1050205000,3,16,00fdffc2 +1050215000,3,17,ff790028 +1050225000,3,18,ffa70037 +1050235000,3,19,ff03000a +1050245000,3,20,015cfe75 +1050255000,3,21,00850151 +1050265000,3,22,fe83fec3 +1050275000,3,23,029e02b0 +1050285000,3,24,f995fb6c +1050295000,3,25,072302fc +1050305000,3,26,fd0dfef3 +1050315000,3,27,006300bc +1050325000,3,28,ff8efe5d +1050335000,3,29,00ef0193 +1050345000,3,30,ff8dffbb +1050355000,3,31,ff0cffba +1052235000,4,0,ffcb0066 +1052245000,4,1,0050fffb +1052255000,4,2,ffe0fed3 +1052265000,4,3,ffa40100 +1052275000,4,4,00b60022 +1052285000,4,5,fe7cffd3 +1052295000,4,6,0039000a +1052305000,4,7,02ccfed9 +1052315000,4,8,f9eb01a6 +1052325000,4,9,061800f3 +1052335000,4,10,fd90fe8d +1052345000,4,11,00fa013e +1052355000,4,12,fedcfd16 +1052365000,4,13,015401e7 +1052375000,4,14,fee700b2 +1052385000,4,15,0086fee1 +1054265000,4,16,0016ffea +1054275000,4,17,0041ffa2 +1054285000,4,18,000b0138 +1054295000,4,19,ff6aff83 +1054305000,4,20,001fff0e +1054315000,4,21,feb400ab +1054325000,4,22,02910079 +1054335000,4,23,fe2b01c7 +1054345000,4,24,fd2afd74 +1054355000,4,25,06d7ff6e +1054365000,4,26,fb1d0056 +1054375000,4,27,04a2003b +1054385000,4,28,f9bd02cc +1054395000,4,29,03c4fe85 +1054405000,4,30,fef3ffa1 +1054415000,4,31,ffb1005b +1056295000,5,0,ffbd003f +1056305000,5,1,0098007e +1056315000,5,2,fed5fe93 +1056325000,5,3,00f30177 +1056335000,5,4,0051febd +1056345000,5,5,fe7d0440 +1056355000,5,6,019bfb1a +1056365000,5,7,014cff8a +1056375000,5,8,fd6b039f +1056385000,5,9,0104fdc4 +1056395000,5,10,ff77ffa3 +1056405000,5,11,0059012b +1056415000,5,12,ffa3ff59 +1056425000,5,13,0073002a +1056435000,5,14,ffcd0094 +1056445000,5,15,000cff50 +1058325000,5,16,00a20046 +1058335000,5,17,006fff8c +1058345000,5,18,fd68015d +1058355000,5,19,02c6fcac +1058365000,5,20,fffb02b4 +1058375000,5,21,ff45fea6 +1058385000,5,22,ff1200ce +1058395000,5,23,02840190 +1058405000,5,24,fdbcfc60 +1058415000,5,25,02e10144 +1058425000,5,26,0016fff3 +1058435000,5,27,fd92012a +1058445000,5,28,00af000e +1058455000,5,29,ff4ffebe +1058465000,5,30,02100082 +1058475000,5,31,fe78ffbe +1060355000,6,0,0082ff86 +1060365000,6,1,ff1800e6 +1060375000,6,2,0081fef2 +1060385000,6,3,015c009b +1060395000,6,4,fe04feb0 +1060405000,6,5,009a013d +1060415000,6,6,010efe57 +1060425000,6,7,fc9002f3 +1060435000,6,8,030aff5e +1060445000,6,9,ffb8fc76 +1060455000,6,10,fef904e4 +1060465000,6,11,0254fd7b +1060475000,6,12,fcf800d0 +1060485000,6,13,019efe6f +1060495000,6,14,00080137 +1060505000,6,15,ffa00027 +1062385000,6,16,00780011 +1062395000,6,17,ff34004c +1062405000,6,18,fffaff6d +1062415000,6,19,00c70042 +1062425000,6,20,fe0dfea3 +1062435000,6,21,027d02f2 +1062445000,6,22,fc5cfe46 +1062455000,6,23,057b013c +1062465000,6,24,f8b2fc3f +1062475000,6,25,0562022c +1062485000,6,26,002cfeeb +1062495000,6,27,fe690180 +1062505000,6,28,ffd5fe91 +1062515000,6,29,008901b2 +1062525000,6,30,ffcaff4e +1062535000,6,31,ff91ffc6 +1064415000,7,0,ff65ffee +1064425000,7,1,00aa005c +1064435000,7,2,ffd7003b +1064445000,7,3,fffbffa2 +1064455000,7,4,fe6cffd3 +1064465000,7,5,031f022a +1064475000,7,6,feccfb03 +1064485000,7,7,00dd0534 +1064495000,7,8,fcd3fe16 +1064505000,7,9,01c8fd94 +1064515000,7,10,01f902e7 +1064525000,7,11,feb5fff0 +1064535000,7,12,fdd4fde9 +1064545000,7,13,02030176 +1064555000,7,14,ff8c004b +1064565000,7,15,003fff7a +1066445000,7,16,00480050 +1066455000,7,17,0009ff92 +1066465000,7,18,fe4c01c7 +1066475000,7,19,0141fc4b +1066485000,7,20,00a20287 +1066495000,7,21,ff50fe69 +1066505000,7,22,0034009d +1066515000,7,23,03840148 +1066525000,7,24,fc3aff7e +1066535000,7,25,007b0044 +1066545000,7,26,fed2fda3 +1066555000,7,27,02d1ffa5 +1066565000,7,28,ff00039f +1066575000,7,29,0050fd79 +1066585000,7,30,ff220035 +1066595000,7,31,00aeffb0 +1068475000,8,0,ffb70007 +1068485000,8,1,0039007c +1068495000,8,2,000c00ac +1068505000,8,3,fed5fd66 +1068515000,8,4,01630101 +1068525000,8,5,004b045d +1068535000,8,6,fddffb1d +1068545000,8,7,013bfff2 +1068555000,8,8,00670371 +1068565000,8,9,019ffd88 +1068575000,8,10,fd5c0286 +1068585000,8,11,0261fc42 +1068595000,8,12,fd5b0043 +1068605000,8,13,02b9021b +1068615000,8,14,fd8dff35 +1068625000,8,15,0103ffaa +1070505000,8,16,0021ffc0 +1070515000,8,17,0088000a +1070525000,8,18,fedb011e +1070535000,8,19,ffccfe84 +1070545000,8,20,01a00014 +1070555000,8,21,feccfc60 +1070565000,8,22,01cc0642 +1070575000,8,23,fd17fdb6 +1070585000,8,24,0179fd86 +1070595000,8,25,03b20270 +1070605000,8,26,fc39fdda +1070615000,8,27,03140372 +1070625000,8,28,fc5efcbe +1070635000,8,29,017202fa +1070645000,8,30,fff8fdf6 +1070655000,8,31,fff10068 +1072535000,9,0,ff89003d +1072545000,9,1,00460071 +1072555000,9,2,0076ffc4 +1072565000,9,3,ff900112 +1072575000,9,4,ff17fdeb +1072585000,9,5,016f0256 +1072595000,9,6,010bfd4a +1072605000,9,7,ffd50316 +1072615000,9,8,fc91fdf7 +1072625000,9,9,02e4ffc9 +1072635000,9,10,01aefebe +1072645000,9,11,fca002ba +1072655000,9,12,004ffe59 +1072665000,9,13,00570004 +1072675000,9,14,00410154 +1072685000,9,15,001bfef2 +1074565000,9,16,004bffd7 +1074575000,9,17,ff730074 +1074585000,9,18,ffcdfeb5 +1074595000,9,19,01fe0249 +1074605000,9,20,fd93fdfa +1074615000,9,21,00b7fe2f +1074625000,9,22,0036028e +1074635000,9,23,032fff58 +1074645000,9,24,fbf10153 +1074655000,9,25,01fdfeba +1074665000,9,26,fe79fe5b +1074675000,9,27,03e8039b +1074685000,9,28,fb99fcb8 +1074695000,9,29,0171022f +1074705000,9,30,ff8cfece +1074715000,9,31,00530000 +1076595000,10,0,0036004a +1076605000,10,1,fee4ffae +1076615000,10,2,01d300ba +1076625000,10,3,ffc9ffc9 +1076635000,10,4,fe13fee6 +1076645000,10,5,02e90074 +1076655000,10,6,fcdb01e8 +1076665000,10,7,00f9fde3 +1076675000,10,8,00e60036 +1076685000,10,9,01de0198 +1076695000,10,10,fbe9fed8 +1076705000,10,11,02afffd1 +1076715000,10,12,ff5dffa6 +1076725000,10,13,004dffd2 +1076735000,10,14,ff2d014a +1076745000,10,15,0047ff27 +1078625000,10,16,002effb9 +1078635000,10,17,0012006b +1078645000,10,18,fdcfffcc +1078655000,10,19,035bfe87 +1078665000,10,20,fe2b007f +1078675000,10,21,0139fe13 +1078685000,10,22,00c50461 +1078695000,10,23,feabfed4 +1078705000,10,24,fe3aff77 +1078715000,10,25,046afe17 +1078725000,10,26,fc47020c +1078735000,10,27,02b5fe7b +1078745000,10,28,fc7d0085 +1078755000,10,29,03b3ffe7 +1078765000,10,30,fe350023 +1078775000,10,31,004dfffe +1080655000,11,0,ffcc0042 +1080665000,11,1,0019006f +1080675000,11,2,00eaff0f +1080685000,11,3,fe1f010a +1080695000,11,4,0030febc +1080705000,11,5,01aefffd +1080715000,11,6,ff16027c +1080725000,11,7,ff96fdaf +1080735000,11,8,ff780078 +1080745000,11,9,032d01ad +1080755000,11,10,fc48fcef +1080765000,11,11,02cf0088 +1080775000,11,12,febc02ba +1080785000,11,13,feb8fd1b +1080795000,11,14,01a8016e +1080805000,11,15,ffb0ff73 +1082685000,11,16,ffe2ff96 +1082695000,11,17,00d7ffb3 +1082705000,11,18,fe2d00f1 +1082715000,11,19,00e5ff2e +1082725000,11,20,fec4ff23 +1082735000,11,21,02690026 +1082745000,11,22,00aa0079 +1082755000,11,23,fddf016f +1082765000,11,24,ff1afe12 +1082775000,11,25,039fff2f +1082785000,11,26,fc8f0283 +1082795000,11,27,02ddfe72 +1082805000,11,28,fe440201 +1082815000,11,29,0005fdbc +1082825000,11,30,ffa6005f +1082835000,11,31,002b0075 +1084715000,12,0,ffa5ffca +1084725000,12,1,00830144 +1084735000,12,2,ffc1fdb2 +1084745000,12,3,ffbb02b0 +1084755000,12,4,fff2fd05 +1084765000,12,5,ff5f036f +1084775000,12,6,032afd5a +1084785000,12,7,fd540072 +1084795000,12,8,fe6701c4 +1084805000,12,9,0347ffbc +1084815000,12,10,ffcdfd3a +1084825000,12,11,004d015c +1084835000,12,12,fdf6015d +1084845000,12,13,fff3fda1 +1084855000,12,14,012c015a +1084865000,12,15,ffb0ffe2 +1086745000,12,16,00a8ff77 +1086755000,12,17,00240110 +1086765000,12,18,fe8efff4 +1086775000,12,19,00a1fece +1086785000,12,20,0169005a +1086795000,12,21,fc73fddc +1086805000,12,22,053c02e0 +1086815000,12,23,fcc9fe6b +1086825000,12,24,feda0131 +1086835000,12,25,0490fda4 +1086845000,12,26,fcf60178 +1086855000,12,27,01bbfed4 +1086865000,12,28,fe0901da +1086875000,12,29,01a1fdfc +1086885000,12,30,ff7c0198 +1086895000,12,31,ff53ff27 +1088775000,13,0,ffccffcd +1088785000,13,1,00ed0068 +1088795000,13,2,fe73ff6e +1088805000,13,3,01130031 +1088815000,13,4,feb2fe08 +1088825000,13,5,0075056c +1088835000,13,6,018ef9b3 +1088845000,13,7,fe2e03c4 +1088855000,13,8,00b60125 +1088865000,13,9,ff0dfc72 +1088875000,13,10,01d10050 +1088885000,13,11,001902b9 +1088895000,13,12,fd28fe6a +1088905000,13,13,01d5002e +1088915000,13,14,0072000b +1088925000,13,15,ffc2fffe +1090805000,13,16,00beffee +1090815000,13,17,fefa0049 +1090825000,13,18,005cffde +1090835000,13,19,00d5ff4a +1090845000,13,20,fe59006d +1090855000,13,21,024c00d1 +1090865000,13,22,fed2017e +1090875000,13,23,0018ffd5 +1090885000,13,24,fde2ff56 +1090895000,13,25,fffafd19 +1090905000,13,26,03580252 +1090915000,13,27,fb8dff48 +1090925000,13,28,039b00f7 +1090935000,13,29,fe78ff55 +1090945000,13,30,00e60082 +1090955000,13,31,feee0029 +1092835000,14,0,ff7d0027 +1092845000,14,1,010fffea +1092855000,14,2,ffa400f7 +1092865000,14,3,fe85fee8 +1092875000,14,4,021cffae +1092885000,14,5,fea8016a +1092895000,14,6,00e6fef6 +1092905000,14,7,fe98ff05 +1092915000,14,8,0165039f +1092925000,14,9,fec7fdfe +1092935000,14,10,01defe67 +1092945000,14,11,ff650238 +1092955000,14,12,0076fe14 +1092965000,14,13,fdae0142 +1092975000,14,14,01ecffa4 +1092985000,14,15,ff8affc7 +1094865000,14,16,0047ffd2 +1094875000,14,17,0089003a +1094885000,14,18,fda50108 +1094895000,14,19,02ebfcb5 +1094905000,14,20,fec601a2 +1094915000,14,21,00c400ab +1094925000,14,22,ff1cff9e +1094935000,14,23,022c01dc +1094945000,14,24,fac1fc2c +1094955000,14,25,04250286 +1094965000,14,26,ff6dfdf4 +1094975000,14,27,0015fffb +1094985000,14,28,002e005c +1094995000,14,29,ffc6ff15 +1095005000,14,30,00de01e2 +1095015000,14,31,ff44feec +1096895000,15,0,00120019 +1096905000,15,1,0022ff0b +1096915000,15,2,004701f6 +1096925000,15,3,00cfffb0 +1096935000,15,4,fcf1fdbf +1096945000,15,5,02f800be +1096955000,15,6,ffb501a5 +1096965000,15,7,fce8ffb1 +1096975000,15,8,03e400dd +1096985000,15,9,fef8fdb1 +1096995000,15,10,000f0000 +1097005000,15,11,ffbb022e +1097015000,15,12,ff99fdb3 +1097025000,15,13,003e0146 +1097035000,15,14,0065ff75 +1097045000,15,15,ff4e0039 +1098925000,15,16,007fffdd +1098935000,15,17,ff23000f +1098945000,15,18,00a7fffa +1098955000,15,19,ff4fff4f +1098965000,15,20,016bff4b +1098975000,15,21,01ce0213 +1098985000,15,22,f9ab01e7 +1098995000,15,23,0532fd47 +1099005000,15,24,fd7b001b +1099015000,15,25,00dfff37 +1099025000,15,26,ff49ff98 +1099035000,15,27,013f0101 +1099045000,15,28,fe9fffc9 +1099055000,15,29,00f00087 +1099065000,15,30,ffd1ff33 +1099075000,15,31,ffd00041 +1100955000,16,0,0017002b +1100965000,16,1,ffb20001 +1100975000,16,2,00160008 +1100985000,16,3,ffc5fefa +1100995000,16,4,ffd900dd +1101005000,16,5,fe9b0276 +1101015000,16,6,02c6fc2a +1101025000,16,7,005802e0 +1101035000,16,8,fbbfffaf +1101045000,16,9,03a6fceb +1101055000,16,10,ffec04e2 +1101065000,16,11,00ebfe32 +1101075000,16,12,fc75fd51 +1101085000,16,13,03bd021a +1101095000,16,14,fe14ffec +1101105000,16,15,0048ff70 +1102985000,16,16,003e002a +1102995000,16,17,0072004f +1103005000,16,18,fdf20013 +1103015000,16,19,01f4ff78 +1103025000,16,20,ff98ffb3 +1103035000,16,21,fe16fede +1103045000,16,22,0471018a +1103055000,16,23,fd5701a8 +1103065000,16,24,ff48fc3c +1103075000,16,25,04b20227 +1103085000,16,26,fcf4ff6d +1103095000,16,27,026600f6 +1103105000,16,28,fb6a000f +1103115000,16,29,04260020 +1103125000,16,30,fe21ffe6 +1103135000,16,31,006fffae +1105015000,17,0,ffae0061 +1105025000,17,1,0058ffd9 +1105035000,17,2,0054fff0 +1105045000,17,3,fe930079 +1105055000,17,4,012fff0f +1105065000,17,5,fe4bffcc +1105075000,17,6,018500c9 +1105085000,17,7,015300cd +1105095000,17,8,fa94006f +1105105000,17,9,0504fe0f +1105115000,17,10,00ce00c4 +1105125000,17,11,fdaf00b5 +1105135000,17,12,ff5bfed1 +1105145000,17,13,0181000c +1105155000,17,14,ff350173 +1105165000,17,15,009bfea5 +1107045000,17,16,00c50001 +1107055000,17,17,ff720023 +1107065000,17,18,feb5ffea +1107075000,17,19,024a00b6 +1107085000,17,20,fddcfdd9 +1107095000,17,21,02ca00af +1107105000,17,22,fc64025a +1107115000,17,23,0329fdd5 +1107125000,17,24,fca1022f +1107135000,17,25,034efbf1 +1107145000,17,26,00990436 +1107155000,17,27,fddafe74 +1107165000,17,28,ff62003f +1107175000,17,29,008a0135 +1107185000,17,30,0022fec6 +1107195000,17,31,ff370061 +1109075000,18,0,ffe60003 +1109085000,18,1,002dfff1 +1109095000,18,2,00cd0039 +1109105000,18,3,ffa2ffce +1109115000,18,4,fd55003e +1109125000,18,5,0451ff23 +1109135000,18,6,febd0040 +1109145000,18,7,fd4601b1 +1109155000,18,8,023efea3 +1109165000,18,9,ff23005b +1109175000,18,10,02edffdf +1109185000,18,11,fda4ffbe +1109195000,18,12,ff8f0028 +1109205000,18,13,0047ffc1 +1109215000,18,14,0119fff4 +1109225000,18,15,fef4003b +1111105000,18,16,ffbbff2a +1111115000,18,17,002d00b0 +1111125000,18,18,00c9fee7 +1111135000,18,19,ffb00251 +1111145000,18,20,ff06fd06 +1111155000,18,21,0101011d +1111165000,18,22,ff86ffc7 +1111175000,18,23,0274010c +1111185000,18,24,f9ff0130 +1111195000,18,25,06edfc16 +1111205000,18,26,fc050153 +1111215000,18,27,01f4002b +1111225000,18,28,ffdcfef8 +1111235000,18,29,ff810101 +1111245000,18,30,00a8ffbf +1111255000,18,31,ffe4007c +1113135000,19,0,00420005 +1113145000,19,1,fec50098 +1113155000,19,2,02eefef1 +1113165000,19,3,fe430108 +1113175000,19,4,fde0ff13 +1113185000,19,5,0241ffbe +1113195000,19,6,0163ff90 +1113205000,19,7,fcd203a7 +1113215000,19,8,0026fc63 +1113225000,19,9,01dd0170 +1113235000,19,10,0024ffdd +1113245000,19,11,002d0012 +1113255000,19,12,fe88ff5d +1113265000,19,13,ffe50006 +1113275000,19,14,014b00e2 +1113285000,19,15,ff66ff5b +1115165000,19,16,005effad +1115175000,19,17,ffc80081 +1115185000,19,18,fe430065 +1115195000,19,19,026dfd90 +1115205000,19,20,ff30011b +1115215000,19,21,00d902bd +1115225000,19,22,fe3cfd72 +1115235000,19,23,027a02b0 +1115245000,19,24,fb20fad7 +1115255000,19,25,03480525 +1115265000,19,26,ff27fbb7 +1115275000,19,27,01e1018a +1115285000,19,28,fe82009d +1115295000,19,29,009ffea9 +1115305000,19,30,ffa2017e +1115315000,19,31,ffb8ff82 +1117195000,20,0,fffe0094 +1117205000,20,1,ffbdffda +1117215000,20,2,00bdff48 +1117225000,20,3,003500aa +1117235000,20,4,fe45ff6a +1117245000,20,5,00dc0243 +1117255000,20,6,007afc67 +1117265000,20,7,ffaa0274 +1117275000,20,8,fed800ea +1117285000,20,9,02f3fd90 +1117295000,20,10,fe53020c +1117305000,20,11,00d5fec6 +1117315000,20,12,fd9dfe8c +1117325000,20,13,02d001cf +1117335000,20,14,fe460059 +1117345000,20,15,0068ff18 +1119225000,20,16,004eff71 +1119235000,20,17,001b003a +1119245000,20,18,fe44017f +1119255000,20,19,0283fd90 +1119265000,20,20,fd780030 +1119275000,20,21,02b500d6 +1119285000,20,22,fd7fff51 +1119295000,20,23,0113030f +1119305000,20,24,fd68faa3 +1119315000,20,25,03bd0310 +1119325000,20,26,ff32fd01 +1119335000,20,27,015301c0 +1119345000,20,28,fc82016c +1119355000,20,29,02e3fdb4 +1119365000,20,30,fec3016f +1119375000,20,31,ffdfffbd +1121255000,21,0,ffb40015 +1121265000,21,1,ffc1006e +1121275000,21,2,00d4fedd +1121285000,21,3,ff1e01ec +1121295000,21,4,ff9cfdaa +1121305000,21,5,00a80392 +1121315000,21,6,013bfb1a +1121325000,21,7,fc35036c +1121335000,21,8,03e40047 +1121345000,21,9,ff1b0094 +1121355000,21,10,fe92fe1f +1121365000,21,11,0368fee4 +1121375000,21,12,fdc00186 +1121385000,21,13,ff640054 +1121395000,21,14,007bff0e +1121405000,21,15,004d002c +1123285000,21,16,ffed0022 +1123295000,21,17,005efef7 +1123305000,21,18,004f0227 +1123315000,21,19,004bff24 +1123325000,21,20,fe6a0006 +1123335000,21,21,fedcfef9 +1123345000,21,22,0325ff9f +1123355000,21,23,fe860265 +1123365000,21,24,00d7fe50 +1123375000,21,25,0050fed1 +1123385000,21,26,fec90163 +1123395000,21,27,004bfc56 +1123405000,21,28,012204e4 +1123415000,21,29,0042fd23 +1123425000,21,30,ff230173 +1123435000,21,31,0038ff35 +1125315000,22,0,00240021 +1125325000,22,1,ff74ffd7 +1125335000,22,2,009dffe2 +1125345000,22,3,00f6019f +1125355000,22,4,ff44fd14 +1125365000,22,5,fd2c02e5 +1125375000,22,6,0348fc5a +1125385000,22,7,ffaf0463 +1125395000,22,8,000cff89 +1125405000,22,9,ff5efcd7 +1125415000,22,10,fff10160 +1125425000,22,11,014a00e5 +1125435000,22,12,fe9cfe2e +1125445000,22,13,00c20169 +1125455000,22,14,ff22ff88 +1125465000,22,15,0049000d +1127345000,22,16,0058ff8e +1127355000,22,17,ff92000c +1127365000,22,18,ff2f0013 +1127375000,22,19,01f800a7 +1127385000,22,20,fe59fc15 +1127395000,22,21,01f10480 +1127405000,22,22,fce7fd9a +1127415000,22,23,035b0321 +1127425000,22,24,fd84fd2a +1127435000,22,25,01240066 +1127445000,22,26,ff69ff2b +1127455000,22,27,0054ff17 +1127465000,22,28,ff3f015b +1127475000,22,29,018dff26 +1127485000,22,30,ffad0088 +1127495000,22,31,ff850031 +1129375000,23,0,ffceffb0 +1129385000,23,1,ffab00d1 +1129395000,23,2,005e0005 +1129405000,23,3,0023ffb7 +1129415000,23,4,0025fe43 +1129425000,23,5,fe3d042f +1129435000,23,6,02f0fab5 +1129445000,23,7,fe44046e +1129455000,23,8,0048005a +1129465000,23,9,ffd7fa85 +1129475000,23,10,004803af +1129485000,23,11,014b013d +1129495000,23,12,fce1fd8b +1129505000,23,13,022d00a3 +1129515000,23,14,ff4e0097 +1129525000,23,15,0062ff9e +1131405000,23,16,00750062 +1131415000,23,17,ff2d0058 +1131425000,23,18,ffbcffb5 +1131435000,23,19,00edff63 +1131445000,23,20,ff79008d +1131455000,23,21,ffd4fe79 +1131465000,23,22,008401fc +1131475000,23,23,02d8fed7 +1131485000,23,24,fced0208 +1131495000,23,25,0073ff32 +1131505000,23,26,ff52fc25 +1131515000,23,27,026b04ad +1131525000,23,28,fd1dfe75 +1131535000,23,29,01cc0015 +1131545000,23,30,fee6ffde +1131555000,23,31,00b0ff61 +1133435000,24,0,fffd0025 +1133445000,24,1,ff8700f7 +1133455000,24,2,0118fe86 +1133465000,24,3,ffc60049 +1133475000,24,4,ffe2ffde +1133485000,24,5,fdc00404 +1133495000,24,6,02c2f902 +1133505000,24,7,ff8c04da +1133515000,24,8,ff71001f +1133525000,24,9,0053fe9b +1133535000,24,10,018c0074 +1133545000,24,11,fef8ff8f +1133555000,24,12,fe40fdc6 +1133565000,24,13,025e030e +1133575000,24,14,fe9aff6c +1133585000,24,15,002eff5a +1135465000,24,16,0046fff0 +1135475000,24,17,0084004a +1135485000,24,18,fde000de +1135495000,24,19,01fefe2b +1135505000,24,20,0018ff7f +1135515000,24,21,fcccff81 +1135525000,24,22,0446028f +1135535000,24,23,ff7cff0c +1135545000,24,24,fc180060 +1135555000,24,25,0562fc18 +1135565000,24,26,fc980236 +1135575000,24,27,041c0149 +1135585000,24,28,fa0eff71 +1135595000,24,29,035eff25 +1135605000,24,30,ff86005d +1135615000,24,31,ffd2ff88 +1137495000,25,0,ffa80057 +1137505000,25,1,0053ffab +1137515000,25,2,00c5013c +1137525000,25,3,fde2ff7f +1137535000,25,4,01affdc4 +1137545000,25,5,fe260398 +1137555000,25,6,0321fd50 +1137565000,25,7,fcff0207 +1137575000,25,8,011efff3 +1137585000,25,9,00a1fe27 +1137595000,25,10,ff990224 +1137605000,25,11,0160fde9 +1137615000,25,12,fd770072 +1137625000,25,13,01ca0106 +1137635000,25,14,ff05ff60 +1137645000,25,15,006bff91 +1139525000,25,16,002bff62 +1139535000,25,17,ff60015d +1139545000,25,18,ff85fe6a +1139555000,25,19,0182ff88 +1139565000,25,20,ffd6ffd6 +1139575000,25,21,0045ffad +1139585000,25,22,ff8e03c7 +1139595000,25,23,0176fd4a +1139605000,25,24,fccb002a +1139615000,25,25,0316006f +1139625000,25,26,fd81fe9e +1139635000,25,27,046e014e +1139645000,25,28,fb3cfe8e +1139655000,25,29,02c101c7 +1139665000,25,30,ff0cfec1 +1139675000,25,31,00860080 +1141555000,26,0,ff9d0003 +1141565000,26,1,ffb20051 +1141575000,26,2,016aff7c +1141585000,26,3,fd290196 +1141595000,26,4,0285fd5a +1141605000,26,5,fe640101 +1141615000,26,6,00c60267 +1141625000,26,7,00e5fec5 +1141635000,26,8,fe83fe45 +1141645000,26,9,01a6030b +1141655000,26,10,ff62fd18 +1141665000,26,11,00dbff54 +1141675000,26,12,ff2302f6 +1141685000,26,13,fe90fdb3 +1141695000,26,14,014e00bd +1141705000,26,15,0023fff1 +1143585000,26,16,005effb6 +1143595000,26,17,012e0039 +1143605000,26,18,fc780011 +1143615000,26,19,030200b8 +1143625000,26,20,ffc3fdb8 +1143635000,26,21,ffcc012d +1143645000,26,22,0058ff4a +1143655000,26,23,ffa60337 +1143665000,26,24,fdb0fd06 +1143675000,26,25,03feff9b +1143685000,26,26,feda0067 +1143695000,26,27,0210fef4 +1143705000,26,28,fce30234 +1143715000,26,29,017cfeb3 +1143725000,26,30,001200b6 +1143735000,26,31,ff24ffa9 +1145615000,27,0,ff9f0022 +1145625000,27,1,0063006c +1145635000,27,2,00c60044 +1145645000,27,3,fcf7ff21 +1145655000,27,4,013aff70 +1145665000,27,5,024402f4 +1145675000,27,6,fdf1fd3f +1145685000,27,7,00ce01d4 +1145695000,27,8,ff85ffb2 +1145705000,27,9,002dfe60 +1145715000,27,10,00960190 +1145725000,27,11,006dff2f +1145735000,27,12,ff1200b8 +1145745000,27,13,ffc4fee0 +1145755000,27,14,004300c9 +1145765000,27,15,0036ff64 +1147645000,27,16,0040ffb1 +1147655000,27,17,0114ff59 +1147665000,27,18,fd000206 +1147675000,27,19,0365fd54 +1147685000,27,20,feba01b9 +1147695000,27,21,fe99ff66 +1147705000,27,22,03160142 +1147715000,27,23,fc70fec2 +1147725000,27,24,0022ff37 +1147735000,27,25,01eefdd3 +1147745000,27,26,004a0232 +1147755000,27,27,0101ffd8 +1147765000,27,28,fdc0ffbf +1147775000,27,29,0251001e +1147785000,27,30,febcffce +1147795000,27,31,ffd6005a +1149675000,28,0,ff03002e +1149685000,28,1,016dffd6 +1149695000,28,2,fffc008d +1149705000,28,3,fd54008f +1149715000,28,4,02b2fe1c +1149725000,28,5,ff2c0194 +1149735000,28,6,022fffe9 +1149745000,28,7,fcbbfdb7 +1149755000,28,8,00af0446 +1149765000,28,9,0081fbd2 +1149775000,28,10,020e02ab +1149785000,28,11,fceefd61 +1149795000,28,12,017801cc +1149805000,28,13,fea60020 +1149815000,28,14,00ebffcb +1149825000,28,15,0043ffb5 +1151705000,28,16,0000ffc5 +1151715000,28,17,005b0042 +1151725000,28,18,fe3700cd +1151735000,28,19,01bdff07 +1151745000,28,20,ff4afed6 +1151755000,28,21,fe0aff2a +1151765000,28,22,05c60139 +1151775000,28,23,fd4e0244 +1151785000,28,24,fc84fd79 +1151795000,28,25,071dffb6 +1151805000,28,26,fb61ffad +1151815000,28,27,03270085 +1151825000,28,28,fd6e00d4 +1151835000,28,29,01f2ff62 +1151845000,28,30,fe76fff5 +1151855000,28,31,00baffec +1153735000,29,0,ffbb0030 +1153745000,29,1,0053ffcf +1153755000,29,2,00b7ffbd +1153765000,29,3,ff500108 +1153775000,29,4,ff03fead +1153785000,29,5,ffb30178 +1153795000,29,6,03e2fde7 +1153805000,29,7,fbcc00b1 +1153815000,29,8,019103ae +1153825000,29,9,fef5fabf +1153835000,29,10,022701e5 +1153845000,29,11,fef6012c +1153855000,29,12,ff31fe21 +1153865000,29,13,ffd90192 +1153875000,29,14,01880003 +1153885000,29,15,ff52ff4b +1155765000,29,16,001b0040 +1155775000,29,17,ffc600ee +1155785000,29,18,000ffed1 +1155795000,29,19,ffeb0076 +1155805000,29,20,00a7004b +1155815000,29,21,fee8ff6c +1155825000,29,22,01b1019f +1155835000,29,23,ff9c01cc +1155845000,29,24,fcfbfcc4 +1155855000,29,25,02120140 +1155865000,29,26,0065fdb1 +1155875000,29,27,01bd0108 +1155885000,29,28,fd4b01c9 +1155895000,29,29,0148fe82 +1155905000,29,30,fffb01f7 +1155915000,29,31,ffccfe9a +1157795000,30,0,ffce0016 +1157805000,30,1,00150077 +1157815000,30,2,001ffe63 +1157825000,30,3,ffe902d5 +1157835000,30,4,ff6afbd3 +1157845000,30,5,ff8504ab +1157855000,30,6,018afd06 +1157865000,30,7,007601e3 +1157875000,30,8,fe8cff20 +1157885000,30,9,ffe700bd +1157895000,30,10,0153fdab +1157905000,30,11,014f010f +1157915000,30,12,fcf001d7 +1157925000,30,13,0057fd19 +1157935000,30,14,01300174 +1157945000,30,15,ff9affd9 +1159825000,30,16,007e0039 +1159835000,30,17,ff7c002f +1159845000,30,18,ff08ffdb +1159855000,30,19,0362ffa0 +1159865000,30,20,faf9fef4 +1159875000,30,21,04a80136 +1159885000,30,22,fc3000a9 +1159895000,30,23,034fff3d +1159905000,30,24,febefedd +1159915000,30,25,00e6009f +1159925000,30,26,0164fe75 +1159935000,30,27,fcb801b0 +1159945000,30,28,02e3ff6a +1159955000,30,29,ff820080 +1159965000,30,30,ff440083 +1159975000,30,31,ffe3fedf +1161855000,31,0,ffd10031 +1161865000,31,1,0099fffb +1161875000,31,2,ffcfff15 +1161885000,31,3,ffd1018f +1161895000,31,4,0072fd59 +1161905000,31,5,fc9202af +1161915000,31,6,04b9fd92 +1161925000,31,7,fe530286 +1161935000,31,8,fda3ffaf +1161945000,31,9,00e1ff33 +1161955000,31,10,03d5ff2f +1161965000,31,11,fc570317 +1161975000,31,12,0082fce3 +1161985000,31,13,007c01ab +1161995000,31,14,0093ffc6 +1162005000,31,15,ffa5ff94 +1163885000,31,16,0077003a +1163895000,31,17,002a0072 +1163905000,31,18,fda6ff64 +1163915000,31,19,0396ff50 +1163925000,31,20,fd7f0119 +1163935000,31,21,0005fdf7 +1163945000,31,22,018303aa +1163955000,31,23,01a0fe53 +1163965000,31,24,fc91ff7a +1163975000,31,25,03dc00e8 +1163985000,31,26,fbf0ff94 +1163995000,31,27,022c00a8 +1164005000,31,28,ff29ff3f +1164015000,31,29,00550167 +1164025000,31,30,0037ff4a +1164035000,31,31,ff1efff5 +1165915000,32,0,0027004d +1165925000,32,1,fff5fff4 +1165935000,32,2,00a2ffc2 +1165945000,32,3,fe3100b0 +1165955000,32,4,024aff2c +1165965000,32,5,fd64018f +1165975000,32,6,0423fcf4 +1165985000,32,7,ff36030c +1165995000,32,8,fb19fe8d +1166005000,32,9,034bfdf2 +1166015000,32,10,018e01cc +1166025000,32,11,fd5f02a6 +1166035000,32,12,008efbea +1166045000,32,13,ff800203 +1166055000,32,14,0145004e +1166065000,32,15,ff66ff66 +1167945000,32,16,0000ffd4 +1167955000,32,17,0068ffef +1167965000,32,18,fe5b0119 +1167975000,32,19,0145feb0 +1167985000,32,20,ff97ff54 +1167995000,32,21,0001fff6 +1168005000,32,22,016a00c0 +1168015000,32,23,00ba03b3 +1168025000,32,24,fb0cface +1168035000,32,25,05be00e5 +1168045000,32,26,fdf103ef +1168055000,32,27,00fbfc7a +1168065000,32,28,fd0d0166 +1168075000,32,29,00d1ffda +1168085000,32,30,003aff6c +1168095000,32,31,ffae002f +1169975000,33,0,ffde0044 +1169985000,33,1,007b003d +1169995000,33,2,0011ffe9 +1170005000,33,3,fe4f0036 +1170015000,33,4,0115fe01 +1170025000,33,5,003404bf +1170035000,33,6,0036fbfd +1170045000,33,7,01290152 +1170055000,33,8,fe0aff8a +1170065000,33,9,001b009b +1170075000,33,10,00ddfe5b +1170085000,33,11,00570288 +1170095000,33,12,fffffd89 +1170105000,33,13,fe020101 +1170115000,33,14,01800077 +1170125000,33,15,ffc5ff48 +1172005000,33,16,000cffd0 +1172015000,33,17,0152ff0f +1172025000,33,18,fd320301 +1172035000,33,19,01d5fd8e +1172045000,33,20,ff39ffb3 +1172055000,33,21,006efe91 +1172065000,33,22,013d03d9 +1172075000,33,23,fd20fea5 +1172085000,33,24,000cff42 +1172095000,33,25,02cafe0b +1172105000,33,26,fe08021b +1172115000,33,27,026ffd92 +1172125000,33,28,fda30267 +1172135000,33,29,0116ffd1 +1172145000,33,30,ff95ff7f +1172155000,33,31,ff9c001f +1174035000,34,0,004b003f +1174045000,34,1,ff7e0035 +1174055000,34,2,0103ff59 +1174065000,34,3,00b60059 +1174075000,34,4,fbf20063 +1174085000,34,5,03efff36 +1174095000,34,6,ff9dff7e +1174105000,34,7,ff1a035e +1174115000,34,8,ff33fc47 +1174125000,34,9,01f601c1 +1174135000,34,10,fefdfeed +1174145000,34,11,015600b1 +1174155000,34,12,fde0ff63 +1174165000,34,13,fff10068 +1174175000,34,14,018b0030 +1174185000,34,15,ff0effc4 +1176065000,34,16,ffa9ffbf +1176075000,34,17,ffc9ffb9 +1176085000,34,18,ff8600a0 +1176095000,34,19,0274ffb7 +1176105000,34,20,fbf5feb8 +1176115000,34,21,04a6ffe1 +1176125000,34,22,fd1a03f3 +1176135000,34,23,0017fded +1176145000,34,24,fe67fe9d +1176155000,34,25,01c70125 +1176165000,34,26,ff04fe92 +1176175000,34,27,02b8ffe3 +1176185000,34,28,fbb70080 +1176195000,34,29,03a2fff1 +1176205000,34,30,fd800017 +1176215000,34,31,0135ffe9 +1178095000,35,0,00180073 +1178105000,35,1,ff7fffb1 +1178115000,35,2,0148005f +1178125000,35,3,fff0ff7b +1178135000,35,4,fe26006c +1178145000,35,5,016900bc +1178155000,35,6,ff47fc1d +1178165000,35,7,01a104a6 +1178175000,35,8,fc5cff6d +1178185000,35,9,0369ff95 +1178195000,35,10,0008fe07 +1178205000,35,11,ffa002b5 +1178215000,35,12,fe96fcac +1178225000,35,13,00f302da +1178235000,35,14,fff1ff45 +1178245000,35,15,ffd3ff8e +1180125000,35,16,00e4ff58 +1180135000,35,17,ff5500b1 +1180145000,35,18,fffeffae +1180155000,35,19,0006009d +1180165000,35,20,0056fd93 +1180175000,35,21,fff101fa +1180185000,35,22,fee7ff08 +1180195000,35,23,034a01fb +1180205000,35,24,faa4fccc +1180215000,35,25,05930227 +1180225000,35,26,fd26fe84 +1180235000,35,27,0118016d +1180245000,35,28,ff66fe01 +1180255000,35,29,000701a2 +1180265000,35,30,00a9ffde +1180275000,35,31,fea00007 +1182155000,36,0,0011006b +1182165000,36,1,ffc8ffb1 +1182175000,36,2,0139001f +1182185000,36,3,ff11000f +1182195000,36,4,fe28ff9d +1182205000,36,5,01bb013f +1182215000,36,6,012cfe5a +1182225000,36,7,004101a5 +1182235000,36,8,fbcd0093 +1182245000,36,9,04b2fd27 +1182255000,36,10,fe6b00a7 +1182265000,36,11,01830293 +1182275000,36,12,fbe6fbf1 +1182285000,36,13,02f702c1 +1182295000,36,14,ff74ffdc +1182305000,36,15,ffcfff59 +1184185000,36,16,002fffc5 +1184195000,36,17,005affa5 +1184205000,36,18,fed10244 +1184215000,36,19,00c2fd27 +1184225000,36,20,006a00dd +1184235000,36,21,fd69fe06 +1184245000,36,22,02c003e0 +1184255000,36,23,fe77fe5c +1184265000,36,24,004d00e3 +1184275000,36,25,0042fbb5 +1184285000,36,26,0033030c +1184295000,36,27,0262ff63 +1184305000,36,28,fb2601e7 +1184315000,36,29,022bfe88 +1184325000,36,30,ffe0002c +1184335000,36,31,ff95000a +1186215000,37,0,ffd7fffa +1186225000,37,1,fff2fff9 +1186235000,37,2,ffdeffcc +1186245000,37,3,0104003b +1186255000,37,4,fddcff3b +1186265000,37,5,00c3014b +1186275000,37,6,0269fe32 +1186285000,37,7,fd39015d +1186295000,37,8,002b0116 +1186305000,37,9,013cfd07 +1186315000,37,10,fdac00c6 +1186325000,37,11,03fa01e3 +1186335000,37,12,fbdeff09 +1186345000,37,13,015b0051 +1186355000,37,14,00e1ffa8 +1186365000,37,15,ffed0029 +1188245000,37,16,0047ffde +1188255000,37,17,009f002b +1188265000,37,18,ff24017a +1188275000,37,19,ffdcfdb3 +1188285000,37,20,011b0075 +1188295000,37,21,fc2b009b +1188305000,37,22,044800c9 +1188315000,37,23,fee400eb +1188325000,37,24,fefffdd6 +1188335000,37,25,ffd7ff89 +1188345000,37,26,01aaffae +1188355000,37,27,ffe60165 +1188365000,37,28,ff6b00ff +1188375000,37,29,fe8bff09 +1188385000,37,30,023e005f +1188395000,37,31,fe6effcd +1190275000,38,0,001bfffe +1190285000,38,1,0006ffff +1190295000,38,2,ffda004d +1190305000,38,3,01ae0091 +1190315000,38,4,fe59fe3e +1190325000,38,5,ff0601dc +1190335000,38,6,0158fc02 +1190345000,38,7,017f0598 +1190355000,38,8,fce1ff00 +1190365000,38,9,0212fc5b +1190375000,38,10,ff6201bf +1190385000,38,11,015201b7 +1190395000,38,12,fdcffd10 +1190405000,38,13,013e01f6 +1190415000,38,14,ff58ff76 +1190425000,38,15,00150024 +1192305000,38,16,0087ff7c +1192315000,38,17,002a00e4 +1192325000,38,18,fec3ff40 +1192335000,38,19,00c1fff8 +1192345000,38,20,ffbcfe1c +1192355000,38,21,fef6042e +1192365000,38,22,0262fd4b +1192375000,38,23,fdd70136 +1192385000,38,24,ffd7fd3a +1192395000,38,25,00b8fff2 +1192405000,38,26,00250184 +1192415000,38,27,013fffec +1192425000,38,28,fd5200b6 +1192435000,38,29,01fcfdfc +1192445000,38,30,ff420171 +1192455000,38,31,ff4dff6e +1194335000,39,0,ffcc0004 +1194345000,39,1,005100a7 +1194355000,39,2,001affbf +1194365000,39,3,ffd3ff8a +1194375000,39,4,ff80ff63 +1194385000,39,5,005e03b5 +1194395000,39,6,0235f9db +1194405000,39,7,fe850446 +1194415000,39,8,fecc00be +1194425000,39,9,ffc5f9a7 +1194435000,39,10,02000561 +1194445000,39,11,ff51ff88 +1194455000,39,12,fd80ff13 +1194465000,39,13,021c0039 +1194475000,39,14,ffc100bd +1194485000,39,15,001fff7c +1196365000,39,16,fffffff3 +1196375000,39,17,ff8e0083 +1196385000,39,18,ff83fee9 +1196395000,39,19,02a6000f +1196405000,39,20,fb8a0071 +1196415000,39,21,046afe97 +1196425000,39,22,fd1e01c9 +1196435000,39,23,03a6015d +1196445000,39,24,fbebff03 +1196455000,39,25,001afe81 +1196465000,39,26,01f5fde9 +1196475000,39,27,fd5c0275 +1196485000,39,28,02a4ff89 +1196495000,39,29,ff92ffdd +1196505000,39,30,fefaffdd +1196515000,39,31,007cffcf +1198395000,40,0,ff4affd2 +1198405000,40,1,016400e4 +1198415000,40,2,ff0dfff1 +1198425000,40,3,ff5aff09 +1198435000,40,4,0068ff79 +1198445000,40,5,00f70385 +1198455000,40,6,fe78fb3d +1198465000,40,7,021103c4 +1198475000,40,8,fdbc0042 +1198485000,40,9,022afcf8 +1198495000,40,10,fe570187 +1198505000,40,11,03860043 +1198515000,40,12,fc26fdeb +1198525000,40,13,00670227 +1198535000,40,14,0078ff1b +1198545000,40,15,003b0020 +1200425000,40,16,0067ff87 +1200435000,40,17,006affba +1200445000,40,18,fe13021c +1200455000,40,19,0184fd52 +1200465000,40,20,ffb10118 +1200475000,40,21,ff28fbfc +1200485000,40,22,01c30565 +1200495000,40,23,0060fd3d +1200505000,40,24,fd130211 +1200515000,40,25,0258fd9e +1200525000,40,26,ff6ffed4 +1200535000,40,27,023803a2 +1200545000,40,28,fc2dfd78 +1200555000,40,29,01620108 +1200565000,40,30,004bfed3 +1200575000,40,31,ff800083 +1202455000,41,0,0021ffec +1202465000,41,1,ff5d00ec +1202475000,41,2,01acfec9 +1202485000,41,3,fefa0034 +1202495000,41,4,ff1b0021 +1202505000,41,5,ff990087 +1202515000,41,6,02edff3b +1202525000,41,7,fe3f0131 +1202535000,41,8,ff59ffc4 +1202545000,41,9,012bff22 +1202555000,41,10,ff2a0069 +1202565000,41,11,0314ff4c +1202575000,41,12,fbff00df +1202585000,41,13,00b7ff47 +1202595000,41,14,0131005b +1202605000,41,15,ff53fffb +1204485000,41,16,0053ff4e +1204495000,41,17,ffac00cb +1204505000,41,18,fedafef1 +1204515000,41,19,022b0099 +1204525000,41,20,ff0afffa +1204535000,41,21,009efd4f +1204545000,41,22,fead0474 +1204555000,41,23,0279fe69 +1204565000,41,24,fb99fffc +1204575000,41,25,0412ffb1 +1204585000,41,26,fd80fe6d +1204595000,41,27,020101f1 +1204605000,41,28,fd72fd98 +1204615000,41,29,02e4018d +1204625000,41,30,feb1ffc2 +1204635000,41,31,ffeb0035 +1206515000,42,0,000affba +1206525000,42,1,fef00074 +1206535000,42,2,0134ffa8 +1206545000,42,3,ffae001f +1206555000,42,4,ffd500cf +1206565000,42,5,ff2efd43 +1206575000,42,6,01be0271 +1206585000,42,7,ffbd01ad +1206595000,42,8,fcd0fd2e +1206605000,42,9,0512010e +1206615000,42,10,fe2cfe02 +1206625000,42,11,ff6202f3 +1206635000,42,12,ffadff15 +1206645000,42,13,00ecfde3 +1206655000,42,14,ff0601c1 +1206665000,42,15,0097fff1 +1208545000,42,16,0009ff70 +1208555000,42,17,00d100c6 +1208565000,42,18,fcc4ff70 +1208575000,42,19,048bff5a +1208585000,42,20,fc990022 +1208595000,42,21,0302fe4b +1208605000,42,22,feb80306 +1208615000,42,23,0051ff2c +1208625000,42,24,feb90138 +1208635000,42,25,0111fd00 +1208645000,42,26,0078ffca +1208655000,42,27,00f5014c +1208665000,42,28,fdbdffd2 +1208675000,42,29,01840077 +1208685000,42,30,ffe4fee4 +1208695000,42,31,fff700c6 +1210575000,43,0,0016fff9 +1210585000,43,1,ff50005a +1210595000,43,2,01a5ff53 +1210605000,43,3,fe4c021b +1210615000,43,4,003cfcb0 +1210625000,43,5,005fffda +1210635000,43,6,0163048c +1210645000,43,7,fccffe25 +1210655000,43,8,00a6fdc3 +1210665000,43,9,04a401d6 +1210675000,43,10,fb83ff4b +1210685000,43,11,00b6004b +1210695000,43,12,009800d0 +1210705000,43,13,ffddfe3a +1210715000,43,14,001500fa +1210725000,43,15,ffcfffd1 +1212605000,43,16,0026ff5f +1212615000,43,17,006d0068 +1212625000,43,18,fec30006 +1212635000,43,19,012a00df +1212645000,43,20,ff49fdc4 +1212655000,43,21,ff6e0100 +1212665000,43,22,0242ffec +1212675000,43,23,fe0aff72 +1212685000,43,24,00aafd8d +1212695000,43,25,0441051a +1212705000,43,26,fae5fde6 +1212715000,43,27,025200af +1212725000,43,28,fe1bfe08 +1212735000,43,29,025c011e +1212745000,43,30,fe82ffa8 +1212755000,43,31,00420058 +1214635000,44,0,0005fffe +1214645000,44,1,006f010f +1214655000,44,2,ffc4feec +1214665000,44,3,fffeffe6 +1214675000,44,4,ff3dff63 +1214685000,44,5,0079031c +1214695000,44,6,015cfbb7 +1214705000,44,7,ffb9022d +1214715000,44,8,fb8101d4 +1214725000,44,9,0499fd2b +1214735000,44,10,00ce0076 +1214745000,44,11,fef001b6 +1214755000,44,12,fdfdfddf +1214765000,44,13,0093001a +1214775000,44,14,015201b3 +1214785000,44,15,ff45fee7 +1216665000,44,16,00a6ffba +1216675000,44,17,ffb300ce +1216685000,44,18,fe9c000d +1216695000,44,19,0279fe68 +1216705000,44,20,fe67019a +1216715000,44,21,ff10fd8b +1216725000,44,22,017f024a +1216735000,44,23,0121fed6 +1216745000,44,24,fcd60026 +1216755000,44,25,03ab0218 +1216765000,44,26,fcaafcc7 +1216775000,44,27,03a10234 +1216785000,44,28,fc39ff8e +1216795000,44,29,0162ffa3 +1216805000,44,30,ffa700da +1216815000,44,31,ff5dff2a +1218695000,45,0,ffdd0038 +1218705000,45,1,00280035 +1218715000,45,2,0058ff3b +1218725000,45,3,ff64013c +1218735000,45,4,004cfe0b +1218745000,45,5,fea1032d +1218755000,45,6,02c3fbcc +1218765000,45,7,ff0b028b +1218775000,45,8,ff0d02f8 +1218785000,45,9,ff90fa21 +1218795000,45,10,023601c7 +1218805000,45,11,007e010a +1218815000,45,12,fbde0009 +1218825000,45,13,0227fee5 +1218835000,45,14,0053014e +1218845000,45,15,ffdbff67 +1220725000,45,16,00bd0060 +1220735000,45,17,ff05004f +1220745000,45,18,0041004b +1220755000,45,19,004ffe10 +1220765000,45,20,ffb6023d +1220775000,45,21,fe5cfdd2 +1220785000,45,22,01950222 +1220795000,45,23,0140fed0 +1220805000,45,24,fce900f6 +1220815000,45,25,0235fe9b +1220825000,45,26,ffb9feef +1220835000,45,27,ff1b0062 +1220845000,45,28,009c020d +1220855000,45,29,006afe58 +1220865000,45,30,fed9019c +1220875000,45,31,ffb6fe42 +1222755000,46,0,ffef000e +1222765000,46,1,006e003b +1222775000,46,2,0090fec7 +1222785000,46,3,fd9b0048 +1222795000,46,4,02620156 +1222805000,46,5,fd95ff76 +1222815000,46,6,00e7fe16 +1222825000,46,7,023501df +1222835000,46,8,fd0d0100 +1222845000,46,9,fea8ff8b +1222855000,46,10,0468fddb +1222865000,46,11,fe5f036c +1222875000,46,12,ff4efd6c +1222885000,46,13,00790160 +1222895000,46,14,006dff60 +1222905000,46,15,ffb5ffe9 +1224785000,46,16,00390013 +1224795000,46,17,00b10010 +1224805000,46,18,fd4a0112 +1224815000,46,19,01effd5a +1224825000,46,20,018201d5 +1224835000,46,21,fc93ffa2 +1224845000,46,22,00f70022 +1224855000,46,23,00890084 +1224865000,46,24,00150013 +1224875000,46,25,0225fd80 +1224885000,46,26,fc6c01ee +1224895000,46,27,02150060 +1224905000,46,28,ff50ff95 +1224915000,46,29,ffa30066 +1224925000,46,30,00830016 +1224935000,46,31,ff57fff2 +1226815000,47,0,00380043 +1226825000,47,1,ff6c005c +1226835000,47,2,01bcfeea +1226845000,47,3,fde2001f +1226855000,47,4,010000fc +1226865000,47,5,feba018a +1226875000,47,6,01d9fb42 +1226885000,47,7,002f0220 +1226895000,47,8,fde803c5 +1226905000,47,9,0046fbb4 +1226915000,47,10,010000ba +1226925000,47,11,01ec01cb +1226935000,47,12,fcd0fddc +1226945000,47,13,016c00de +1226955000,47,14,fff30022 +1226965000,47,15,ffb3ff96 +1228845000,47,16,00a1ffaf +1228855000,47,17,fff4006c +1228865000,47,18,fee8003a +1228875000,47,19,ff5cfe50 +1228885000,47,20,034e006e +1228895000,47,21,fc0bff97 +1228905000,47,22,0342025c +1228915000,47,23,ff7efe11 +1228925000,47,24,fed7015d +1228935000,47,25,00cafbde +1228945000,47,26,007a03aa +1228955000,47,27,fef6feae +1228965000,47,28,00820182 +1228975000,47,29,ff57fe83 +1228985000,47,30,0124ffec +1228995000,47,31,fed00065 +1230875000,48,0,0023ffed +1230885000,48,1,ffff011b +1230895000,48,2,005efe65 +1230905000,48,3,ff8c0006 +1230915000,48,4,fff0ffda +1230925000,48,5,ff100499 +1230935000,48,6,0298f993 +1230945000,48,7,ff4a03ba +1230955000,48,8,fdf3ff1f +1230965000,48,9,014ffea9 +1230975000,48,10,019e028d +1230985000,48,11,ff1aff0c +1230995000,48,12,fda6fe16 +1231005000,48,13,02ea012f +1231015000,48,14,fea8007f +1231025000,48,15,ffe0ffa8 +1232905000,48,16,0038001e +1232915000,48,17,009affa5 +1232925000,48,18,fdb8008d +1232935000,48,19,017fff2b +1232945000,48,20,0058ff07 +1232955000,48,21,fec3fef0 +1232965000,48,22,02790297 +1232975000,48,23,ffca0220 +1232985000,48,24,fc06fa18 +1232995000,48,25,07b60213 +1233005000,48,26,f9aa01bd +1233015000,48,27,0473fd79 +1233025000,48,28,fbee02ff +1233035000,48,29,02b1fe64 +1233045000,48,30,fef90043 +1233055000,48,31,ffd8ffb0 +1234935000,49,0,ffa3fff3 +1234945000,49,1,00cd0105 +1234955000,49,2,ff0ffe57 +1234965000,49,3,fee50111 +1234975000,49,4,0276ff5b +1234985000,49,5,fe840131 +1234995000,49,6,ff0dfeb0 +1235005000,49,7,03570103 +1235015000,49,8,fabdfffd +1235025000,49,9,02f100e7 +1235035000,49,10,0359fde1 +1235045000,49,11,fc4d023d +1235055000,49,12,0062fe55 +1235065000,49,13,0012ff4f +1235075000,49,14,008301f0 +1235085000,49,15,fff3fecb +1236965000,49,16,008d004c +1236975000,49,17,0011ffad +1236985000,49,18,feb20112 +1236995000,49,19,0102fe58 +1237005000,49,20,ffc8004f +1237015000,49,21,0227ff78 +1237025000,49,22,fdd403d8 +1237035000,49,23,01ccfe11 +1237045000,49,24,fb97ff30 +1237055000,49,25,0609fecd +1237065000,49,26,fb0c0268 +1237075000,49,27,02c8ff56 +1237085000,49,28,fd38ffad +1237095000,49,29,02a7ffbe +1237105000,49,30,ff2a007e +1237115000,49,31,ff92ffa9 +1238995000,50,0,0027ffe4 +1239005000,50,1,ff680075 +1239015000,50,2,008bff50 +1239025000,50,3,00930179 +1239035000,50,4,fdb5fea8 +1239045000,50,5,02d301b0 +1239055000,50,6,ff9afc37 +1239065000,50,7,fee7045c +1239075000,50,8,010bfe80 +1239085000,50,9,fdf0ff5f +1239095000,50,10,02abff4e +1239105000,50,11,00dd008f +1239115000,50,12,fba500e0 +1239125000,50,13,020dfefc +1239135000,50,14,00d40087 +1239145000,50,15,ff41ffd4 +1241025000,50,16,0035ff55 +1241035000,50,17,ffbb00c3 +1241045000,50,18,fef5feec +1241055000,50,19,014000f1 +1241065000,50,20,ff6afd66 +1241075000,50,21,02e502d0 +1241085000,50,22,fd82fec8 +1241095000,50,23,00a800d3 +1241105000,50,24,ff0500a1 +1241115000,50,25,022ffd49 +1241125000,50,26,fdd9024e +1241135000,50,27,01e200d1 +1241145000,50,28,fdc8fe00 +1241155000,50,29,01b10054 +1241165000,50,30,fec4ff52 +1241175000,50,31,00d6012b +1243055000,51,0,00230072 +1243065000,51,1,ffa5fffa +1243075000,51,2,0200ff17 +1243085000,51,3,fd7301b2 +1243095000,51,4,ff03fdcd +1243105000,51,5,0385017d +1243115000,51,6,ff8bff81 +1243125000,51,7,fdab0072 +1243135000,51,8,ffaffdcc +1243145000,51,9,01370268 +1243155000,51,10,ff84ff9f +1243165000,51,11,00bd01a4 +1243175000,51,12,ff8ffdc5 +1243185000,51,13,00230091 +1243195000,51,14,00a500e1 +1243205000,51,15,ff89fee0 +1245085000,51,16,00a1ffad +1245095000,51,17,ffd40080 +1245105000,51,18,fe7b008f +1245115000,51,19,0277fe43 +1245125000,51,20,fed70182 +1245135000,51,21,ffcdffc3 +1245145000,51,22,febb0072 +1245155000,51,23,01d200e5 +1245165000,51,24,fc5ffced +1245175000,51,25,0416020c +1245185000,51,26,fed3fdad +1245195000,51,27,0071017d +1245205000,51,28,ff89011c +1245215000,51,29,ffd5fe59 +1245225000,51,30,00770192 +1245235000,51,31,fefaff6b +1247115000,52,0,fffc0056 +1247125000,52,1,ff36004f +1247135000,52,2,0257fef7 +1247145000,52,3,fe0d008e +1247155000,52,4,ff6d00b6 +1247165000,52,5,ff8cffdd +1247175000,52,6,0220fcd0 +1247185000,52,7,003603e5 +1247195000,52,8,fc4401de +1247205000,52,9,02b0fbff +1247215000,52,10,016b0071 +1247225000,52,11,ff6f01be +1247235000,52,12,fe93fde2 +1247245000,52,13,002200ed +1247255000,52,14,009600f4 +1247265000,52,15,0002febf +1249145000,52,16,0018ff91 +1249155000,52,17,00c300a7 +1249165000,52,18,fe230115 +1249175000,52,19,0238fc2d +1249185000,52,20,fe4003a6 +1249195000,52,21,0077fe46 +1249205000,52,22,000b0322 +1249215000,52,23,00e7fd24 +1249225000,52,24,faf40143 +1249235000,52,25,058dfd9f +1249245000,52,26,fe0dff4f +1249255000,52,27,01c40211 +1249265000,52,28,fe30ff72 +1249275000,52,29,0075005c +1249285000,52,30,00b9004e +1249295000,52,31,fef1fff6 +1251175000,53,0,ffa7006b +1251185000,53,1,00bf001d +1251195000,53,2,fedcfee5 +1251205000,53,3,0083018a +1251215000,53,4,ffedfe20 +1251225000,53,5,fee204f6 +1251235000,53,6,0165f9e7 +1251245000,53,7,005601aa +1251255000,53,8,ffd10317 +1251265000,53,9,fef5fe5b +1251275000,53,10,ffb6fec9 +1251285000,53,11,02e300f0 +1251295000,53,12,fe9bff4a +1251305000,53,13,ff5600c2 +1251315000,53,14,0019ff97 +1251325000,53,15,0048ff94 +1253205000,53,16,006bffee +1253215000,53,17,0063ff69 +1253225000,53,18,fe1c0269 +1253235000,53,19,02c7fb9c +1253245000,53,20,fdc0034c +1253255000,53,21,00b4fe4b +1253265000,53,22,0066010b +1253275000,53,23,00f90130 +1253285000,53,24,fdb5fd0c +1253295000,53,25,0297fff3 +1253305000,53,26,007a006d +1253315000,53,27,fe51ffc8 +1253325000,53,28,00b40346 +1253335000,53,29,001efcc9 +1253345000,53,30,00280123 +1253355000,53,31,ff7bfffc +1255235000,54,0,004b0039 +1255245000,54,1,00280058 +1255255000,54,2,fee1ff0d +1255265000,54,3,020d005e +1255275000,54,4,ff4bfec0 +1255285000,54,5,fe4804a7 +1255295000,54,6,013ff95c +1255305000,54,7,ff930344 +1255315000,54,8,006b01ff +1255325000,54,9,ff9efde6 +1255335000,54,10,00950035 +1255345000,54,11,011f0040 +1255355000,54,12,fcf7fec8 +1255365000,54,13,01ea011b +1255375000,54,14,0023008a +1255385000,54,15,ff79ff36 +1257265000,54,16,0093ff78 +1257275000,54,17,fff000a3 +1257285000,54,18,ffa6fff3 +1257295000,54,19,ff53fe9a +1257305000,54,20,02c0007d +1257315000,54,21,fd5600b5 +1257325000,54,22,0198018a +1257335000,54,23,ff2a000a +1257345000,54,24,fed5ff9a +1257355000,54,25,0084ff3d +1257365000,54,26,ff5efed7 +1257375000,54,27,002d0006 +1257385000,54,28,00e80215 +1257395000,54,29,fedafe5f +1257405000,54,30,01bc00e8 +1257415000,54,31,fe1a0012 +1259295000,55,0,fff20019 +1259305000,55,1,000a0015 +1259315000,55,2,ffdbffcf +1259325000,55,3,007d0164 +1259335000,55,4,fe66fc74 +1259345000,55,5,020a04dc +1259355000,55,6,ff67fb95 +1259365000,55,7,002e034a +1259375000,55,8,fd9affdd +1259385000,55,9,02acfd3f +1259395000,55,10,00ef0279 +1259405000,55,11,002f003e +1259415000,55,12,fbd6fd06 +1259425000,55,13,039801f4 +1259435000,55,14,feefffd3 +1259445000,55,15,ffe6ffd0 +1261325000,55,16,001a009d +1261335000,55,17,ff62ff1f +1261345000,55,18,01200165 +1261355000,55,19,fe6ffea5 +1261365000,55,20,01d5ff37 +1261375000,55,21,fe6a01d3 +1261385000,55,22,01d8fed9 +1261395000,55,23,013c02cd +1261405000,55,24,fce4fcd9 +1261415000,55,25,014400dd +1261425000,55,26,fddcff35 +1261435000,55,27,0239fe81 +1261445000,55,28,001d022f +1261455000,55,29,ffd0fea1 +1261465000,55,30,ff840091 +1261475000,55,31,0084ff2d +1263355000,56,0,ffa7ff88 +1263365000,56,1,003400e3 +1263375000,56,2,00810034 +1263385000,56,3,fe66ff66 +1263395000,56,4,0158fcc5 +1263405000,56,5,ff16071e +1263415000,56,6,ffdffa8b +1263425000,56,7,01900282 +1263435000,56,8,ff4d0056 +1263445000,56,9,fe9afe23 +1263455000,56,10,03430088 +1263465000,56,11,ff1000fa +1263475000,56,12,fdccfdc1 +1263485000,56,13,019401fc +1263495000,56,14,ff75feed +1263505000,56,15,00520066 +1265385000,56,16,005c0004 +1265395000,56,17,000e0053 +1265405000,56,18,fdc40037 +1265415000,56,19,0368ff16 +1265425000,56,20,ff2d00a5 +1265435000,56,21,fe6dfc7c +1265445000,56,22,02d804db +1265455000,56,23,ffa2fde0 +1265465000,56,24,fdc2011e +1265475000,56,25,04ccff23 +1265485000,56,26,fc2afea5 +1265495000,56,27,0442024a +1265505000,56,28,fa4900dd +1265515000,56,29,0225fe0a +1265525000,56,30,003e006d +1265535000,56,31,ffc0ffdc +1267415000,57,0,ff95ffe1 +1267425000,57,1,008d005a +1267435000,57,2,0043ff57 +1267445000,57,3,fe8901e8 +1267455000,57,4,00ecfc46 +1267465000,57,5,ff7e029d +1267475000,57,6,0218ff72 +1267485000,57,7,fce90235 +1267495000,57,8,00adfb9b +1267505000,57,9,01150416 +1267515000,57,10,010dff19 +1267525000,57,11,ff33fd36 +1267535000,57,12,fed2024e +1267545000,57,13,0058001b +1267555000,57,14,0040ff0e +1267565000,57,15,003b0085 +1269445000,57,16,ffd5ffa8 +1269455000,57,17,008a005f +1269465000,57,18,fe54ff5b +1269475000,57,19,018f0040 +1269485000,57,20,007cffbb +1269495000,57,21,ffd2fed8 +1269505000,57,22,004a0315 +1269515000,57,23,fe6b00af +1269525000,57,24,fff1ff56 +1269535000,57,25,0168fd75 +1269545000,57,26,fcf400cb +1269555000,57,27,05cbff86 +1269565000,57,28,fa2a0297 +1269575000,57,29,02b8fd1c +1269585000,57,30,ff42018d +1269595000,57,31,ffffffbb +1271475000,58,0,fff1ffec +1271485000,58,1,ff7300b0 +1271495000,58,2,01e8ff64 +1271505000,58,3,fdd50126 +1271515000,58,4,ffb4fe43 +1271525000,58,5,01a6003f +1271535000,58,6,0150016b +1271545000,58,7,fbddfff0 +1271555000,58,8,014ffe78 +1271565000,58,9,030d01fe +1271575000,58,10,fd46ffd4 +1271585000,58,11,0255fdaa +1271595000,58,12,fe800159 +1271605000,58,13,ff0effd3 +1271615000,58,14,01260015 +1271625000,58,15,ffadffc8 +1273505000,58,16,003effa5 +1273515000,58,17,0035fff1 +1273525000,58,18,ff7c00ea +1273535000,58,19,00c4fee3 +1273545000,58,20,fffaffed +1273555000,58,21,fea4fdbf +1273565000,58,22,01c105c6 +1273575000,58,23,00a1fc06 +1273585000,58,24,faf201b3 +1273595000,58,25,05d7fc2d +1273605000,58,26,fe5202e0 +1273615000,58,27,0180fe43 +1273625000,58,28,fec6017f +1273635000,58,29,ff28ffe7 +1273645000,58,30,0181fffc +1273655000,58,31,ff13ffe0 +1275535000,59,0,ffc0ffff +1275545000,59,1,002e00b6 +1275555000,59,2,001cfeb7 +1275565000,59,3,fea8014c +1275575000,59,4,0052fee7 +1275585000,59,5,015700e3 +1275595000,59,6,005900a9 +1275605000,59,7,fe66fed8 +1275615000,59,8,ffdcff1b +1275625000,59,9,011a030e +1275635000,59,10,ff5afd3f +1275645000,59,11,0182013c +1275655000,59,12,ffb60027 +1275665000,59,13,fe05fedd +1275675000,59,14,01850049 +1275685000,59,15,ffd4000c +1277565000,59,16,0088ffc2 +1277575000,59,17,0098ffd3 +1277585000,59,18,fd7601aa +1277595000,59,19,02bbfd44 +1277605000,59,20,febc001b +1277615000,59,21,00c00048 +1277625000,59,22,00990039 +1277635000,59,23,00ad0074 +1277645000,59,24,fcb2022e +1277655000,59,25,03bafaaf +1277665000,59,26,fdec031c +1277675000,59,27,01b7ff26 +1277685000,59,28,fdce0001 +1277695000,59,29,010a0016 +1277705000,59,30,0079ffc5 +1277715000,59,31,ff2d0042 +1279595000,60,0,ffb0ffc6 +1279605000,60,1,0097011d +1279615000,60,2,009afe82 +1279625000,60,3,fe010203 +1279635000,60,4,00e5fd4a +1279645000,60,5,0184022e +1279655000,60,6,ff91ff1c +1279665000,60,7,00460087 +1279675000,60,8,fbe2fe72 +1279685000,60,9,03fb009f +1279695000,60,10,01760112 +1279705000,60,11,fd19ff79 +1279715000,60,12,fee5fe82 +1279725000,60,13,01da0186 +1279735000,60,14,ffcb0064 +1279745000,60,15,ffe8ff75 +1281625000,60,16,0062ffa5 +1281635000,60,17,002500b7 +1281645000,60,18,fe5d009f +1281655000,60,19,010ffd9c +1281665000,60,20,013601b1 +1281675000,60,21,fbb8fc62 +1281685000,60,22,05e604af +1281695000,60,23,fcecfdfa +1281705000,60,24,00280109 +1281715000,60,25,0171fdbf +1281725000,60,26,0045001f +1281735000,60,27,fec900ea +1281745000,60,28,ffc801d9 +1281755000,60,29,fff2fddc +1281765000,60,30,0068006b +1281775000,60,31,ff74fffc +1283655000,61,0,ff95ffe2 +1283665000,61,1,00b1fff2 +1283675000,61,2,ff64006a +1283685000,61,3,ff9afebe +1283695000,61,4,01d400b1 +1283705000,61,5,fc8900a5 +1283715000,61,6,046dff69 +1283725000,61,7,fd25fdeb +1283735000,61,8,004f051c +1283745000,61,9,fedbfa5c +1283755000,61,10,030e0292 +1283765000,61,11,fd920048 +1283775000,61,12,ffc40029 +1283785000,61,13,00a3ff99 +1283795000,61,14,006d004b +1283805000,61,15,002ffffb +1285685000,61,16,00610066 +1285695000,61,17,ff5900e9 +1285705000,61,18,002bff1f +1285715000,61,19,ffaa0030 +1285725000,61,20,0144ff6e +1285735000,61,21,fc020098 +1285745000,61,22,04060002 +1285755000,61,23,0032015b +1285765000,61,24,fdfffeea +1285775000,61,25,0183feed +1285785000,61,26,fda500a9 +1285795000,61,27,0344002a +1285805000,61,28,fd3400ba +1285815000,61,29,015aff4a +1285825000,61,30,ff1a0156 +1285835000,61,31,0000fe8b +1287715000,62,0,ffe0ffb5 +1287725000,62,1,ffeb008c +1287735000,62,2,004effb6 +1287745000,62,3,ffe8ff5e +1287755000,62,4,ff090058 +1287765000,62,5,000b0020 +1287775000,62,6,0314001e +1287785000,62,7,fa80feee +1287795000,62,8,0398043f +1287805000,62,9,feedfb66 +1287815000,62,10,01ba0274 +1287825000,62,11,ffb2fe7c +1287835000,62,12,ff7b0134 +1287845000,62,13,fe81ff4e +1287855000,62,14,0230ff78 +1287865000,62,15,ff3a0098 +1289745000,62,16,0018ff95 +1289755000,62,17,ffdb00e0 +1289765000,62,18,fe62fe43 +1289775000,62,19,03f80169 +1289785000,62,20,fe91fdf0 +1289795000,62,21,ff3903f3 +1289805000,62,22,fec5fe71 +1289815000,62,23,026b001d +1289825000,62,24,fd98ffbf +1289835000,62,25,0299fe4a +1289845000,62,26,ff36fff3 +1289855000,62,27,ff20023f +1289865000,62,28,fefffd40 +1289875000,62,29,024f014b +1289885000,62,30,ff83ff15 +1289895000,62,31,000100f3 +1291775000,63,0,0050ffef +1291785000,63,1,000efff1 +1291795000,63,2,00060141 +1291805000,63,3,ffa2fe18 +1291815000,63,4,020500c3 +1291825000,63,5,fb86fe6f +1291835000,63,6,039a01db +1291845000,63,7,ff2fffd4 +1291855000,63,8,fffa02e1 +1291865000,63,9,fe24f9cb +1291875000,63,10,035c02db +1291885000,63,11,fece01e8 +1291895000,63,12,feedfe69 +1291905000,63,13,0004ff91 +1291915000,63,14,01500095 +1291925000,63,15,ff1dffe8 +1293805000,63,16,00820045 +1293815000,63,17,ffd0ff97 +1293825000,63,18,ff25ffc4 +1293835000,63,19,00a6ffc0 +1293845000,63,20,ffb0ff0e +1293855000,63,21,ff8a022b +1293865000,63,22,0276ffeb +1293875000,63,23,fed6fe67 +1293885000,63,24,fe760175 +1293895000,63,25,00aaff6d +1293905000,63,26,006dffe8 +1293915000,63,27,ff440068 +1293925000,63,28,0060ffd4 +1293935000,63,29,ff180105 +1293945000,63,30,0070fedd +1293955000,63,31,ff34001d 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_2_FPGA/tb/golden/golden_doppler.mem b/9_Firmware/9_2_FPGA/tb/golden/golden_doppler.mem index 9b2497a..ec09a18 100644 --- a/9_Firmware/9_2_FPGA/tb/golden/golden_doppler.mem +++ b/9_Firmware/9_2_FPGA/tb/golden/golden_doppler.mem @@ -1,2176 +1,2176 @@ // 0x00000000 -ffedffb5 -0056002f -fef8fdf4 -00bb03e9 -ff67fd79 -02890060 -fb84ff8a -0391003b -ff0d0073 -00ae0099 -fc9cfefa -0433fe37 -fe0303e7 -0027fe60 -000cff08 -00450115 +0024ffdd +fff7002d +007d0098 +ff1afe23 +00130108 +ff530243 +0274fb9d +fee40370 +fe98ff6f +ff8bffa9 +03dffdd0 +fe3c042d +fdddfcc0 +00ff00cb +015400f7 +ff22ff4c // 0x00000010 -ffda004b -ff09ff80 -01e40140 -fed4fde4 -019102c6 -fac60187 -046efae2 -fe600256 -fee0ff41 -0131002c -0220ffcc -fd1cfdfa -019104b2 -ff4cfd11 -003a00be -001cffd8 +ffedfff1 +00880078 +fe31ffb7 +026c00de +fd2afd6f +0293ffc3 +fef10677 +ff6efadf +008900ad +017a0014 +fdc301a1 +020cff8e +fce0ffbb +025f00b1 +fe73fff1 +008effbd // 0x00000020 -ffe3ffff -00e1ff79 -fef10061 -01650080 -fd9dfeef -04330116 -fa4cfd95 -04660217 -ff33ffb5 -fe990157 -0051fca7 -004bffd8 -ff6d04e5 -0067fd3e -0072ffcb -ffb6007d +ffcd0030 +009d0057 +fecbff41 +0110007a +0148ff8e +fd2e0229 +ffe5fd1c +0483ffb6 +fb2f02ce +02110069 +0095fb47 +ffd003b8 +fdfcfdac +011801c3 +0023003c +0001ff54 // 0x00000030 -ffc10027 -ffe40007 -00f5ff8a -fe04001c -00f202a7 -ff5bfd55 -feb5fef5 -0588007d -f9e10265 -01f6fdf7 -ff51020e -0086fcd4 -01b00389 -fd7bfe31 -012900ef -ffb6ff77 +004fffd0 +0078fee5 +fed201e1 +0097fe5a +00ab0130 +ff73febb +00f301a4 +fea60124 +fe03fe84 +0184fea7 +ff1cffeb +03210146 +fc5fff84 +017d0025 +0053ff88 +fef600b0 // 0x00000040 -ffc4ffdb -0092fffd -ff0fff3e -01d8002f -fd6a0180 -018cff18 -fcf7fdb8 -0745034a -fa62ff4f -fe6afe99 -035d0062 -ffdc00f1 -feb4ffea -0108ff8e -ff01002c -00cf0042 +00260082 +ff99ff96 +016e0095 +0034ff2d +fce100d7 +0276ff6c +00a0ffaa +fdc400b8 +01dcffc4 +fe1901ae +0304fcf3 +fe240203 +0031fe2f +fefc0150 +0182006a +ff18ff30 // 0x00000050 -ffa3000e -ff84ff01 -018c02cb -fe98fd35 -003103aa -fe93faee -0224020a -fe3d00ab -01e3ff84 -ff2a029f -fd70fc23 -01b60147 -0155014c -ff27ffa6 -fea40058 -00ddffdd +005bff79 +0004ffcc +fef70100 +0136fe70 +ffb80156 +01b1feff +fdac03d6 +fdfafa69 +0289032f +feacfecc +004dffb2 +00d60062 +fee0fe8e +020f0245 +fda4feac +006a0079 // 0x00000060 -fffeffe6 -00e5ffd5 -fe89fefd -01c5016b -fe9affd6 -0024ffe3 -fe8bfebe -03d80037 -fdc802fe -ff8ffe6d -ffb5fd0f -01cb014d -fe000306 -0088fe47 -00ffff66 -ff5000b5 +ffe10086 +0026ffcd +00990030 +ffe4ff8a +fe7100c5 +0179ffc6 +ff2ffd5e +01ed0473 +fbd9fdf4 +037800d1 +0033fe0a +ff4202c0 +ffa5fc5d +ffa90298 +00950004 +ffcdff0f // 0x00000070 -ffd1ffc7 -003bff75 -00970297 -fde0fd96 -01fa004e -fe6d01df -0139fb91 -fd19057f -038bfb63 -ff890353 -ff93fcef -fe5002b6 -018e00ec -ff4bfe39 -00890075 -ff8b0065 +00fdffc2 +ff790028 +ffa70037 +ff03000a +015cfe75 +00850151 +fe83fec3 +029e02b0 +f995fb6c +072302fc +fd0dfef3 +006300bc +ff8efe5d +00ef0193 +ff8dffbb +ff0cffba // 0x00000080 -0025001e -00e1ffd5 -fea0febf -0168024f -ff8dff20 -0079004f -fd6afe0b -03e40009 -fef50360 -fe3ffdaf -ffdcfec1 -01fcffcd -fd61035e -01affebd -006aff41 -ff180083 +ffcb0066 +0050fffb +ffe0fed3 +ffa40100 +00b60022 +fe7cffd3 +0039000a +02ccfed9 +f9eb01a6 +061800f3 +fd90fe8d +00fa013e +fedcfd16 +015401e7 +fee700b2 +0086fee1 // 0x00000090 -ffedfff3 -fe94003c -029eff3a -fd74013c -015dfe53 -febd02fb -ff39fac8 -02790477 -fe83ffa7 -ffc2fd6c -00520068 -ff72ffdc -02ef038f -fda5fdbd -ff8b004a -00d9ffe1 +0016ffea +0041ffa2 +000b0138 +ff6aff83 +001fff0e +feb400ab +02910079 +fe2b01c7 +fd2afd74 +06d7ff6e +fb1d0056 +04a2003b +f9bd02cc +03c4fe85 +fef3ffa1 +ffb1005b // 0x000000a0 -00420058 -ffaeff4f -fffb00d1 -00f6ff23 -fe26012a -01ddffa5 -fcccfe0b -03570175 -00ca0380 -fbfafa9d -01db0215 -00e4ffbf -ff7e01da -001bfdf3 -ffd600db -0007ff7d +ffbd003f +0098007e +fed5fe93 +00f30177 +0051febd +fe7d0440 +019bfb1a +014cff8a +fd6b039f +0104fdc4 +ff77ffa3 +0059012b +ffa3ff59 +0073002a +ffcd0094 +000cff50 // 0x000000b0 -fffafff5 -000d0058 -012dff89 -fe590051 -ff36005e -02c60171 -fc8ffe01 -00b9fde1 -02a401bf -fd7b0048 -01c900cd -fbb5ff11 -059c002a -fcf60097 -00ebfffd -ff85ffd5 +00a20046 +006fff8c +fd68015d +02c6fcac +fffb02b4 +ff45fea6 +ff1200ce +02840190 +fdbcfc60 +02e10144 +0016fff3 +fd92012a +00af000e +ff4ffebe +02100082 +fe78ffbe // 0x000000c0 -ff8bff8e -ff9b004f -018aff23 -fd0501b3 -034dfebf -fd1900e3 -0206005b -ff6bfd29 -02fd022a -f9890147 -023afdd3 -042b0027 -fce30169 -002bff47 -ff8eff27 -00ed00e5 +0082ff86 +ff1800e6 +0081fef2 +015c009b +fe04feb0 +009a013d +010efe57 +fc9002f3 +030aff5e +ffb8fc76 +fef904e4 +0254fd7b +fcf800d0 +019efe6f +00080137 +ffa00027 // 0x000000d0 -0053ffd4 -ff90000b -00f40139 -ff43fd97 -02a2022c -fb00fe91 -036c0250 -ff0dfde6 -021dff70 -fccc01bd -017eff43 -ff3d00a5 -ff7eff2c -01c8ff53 -00720178 -ff2fffa2 +00780011 +ff34004c +fffaff6d +00c70042 +fe0dfea3 +027d02f2 +fc5cfe46 +057b013c +f8b2fc3f +0562022c +002cfeeb +fe690180 +ffd5fe91 +008901b2 +ffcaff4e +ff91ffc6 // 0x000000e0 -00390021 -ffebffe1 -ff0dfe43 -02fa0231 -fc1001aa -02eafb6d -fde3015d -011a0181 -0033ff1b -fd91021b -033dfdd3 -fea2ffcb -006c01fa -fffafe33 -000b00b5 -ffcaffdf +ff65ffee +00aa005c +ffd7003b +fffbffa2 +fe6cffd3 +031f022a +feccfb03 +00dd0534 +fcd3fe16 +01c8fd94 +01f902e7 +feb5fff0 +fdd4fde9 +02030176 +ff8c004b +003fff7a // 0x000000f0 -0001ffb9 -ff7900fb -00fdff0a -ff30ffee -ff8d017e -028bfe6a -fb8f00ba -02fdff37 -ff4700f9 -fde30025 -018dfe9e -00e600ba -fdc3ff54 -027d009e -ff0f0122 -0029ff81 +00480050 +0009ff92 +fe4c01c7 +0141fc4b +00a20287 +ff50fe69 +0034009d +03840148 +fc3aff7e +007b0044 +fed2fda3 +02d1ffa5 +ff00039f +0050fd79 +ff220035 +00aeffb0 // 0x00000100 -fff8001d -00500017 -0020fe41 -feb8030d -0181fde1 -016400dd -fbe7fff9 -01a1fcba -033e044f -fc22fe97 -fe94009b -03a0fc75 -fe31066b -0162fb7d -ff050173 -0047ffbc -// 0x00000110 -ffc7ffd8 -ff09000e -02450011 -fefb0140 -feb8fd04 -ff280566 -004df9d4 -02fe0138 -fef702f2 -fd03fe0a -021f00dd -ffaffd78 -00a203d6 -ffb8feba -ff4fffaa -00a40038 -// 0x00000120 -001affd9 -00200089 -0053fdce -ffbf0336 -ffeffd64 -fec601ef -0109ff38 -0256fec0 -fe5401bb -fd14ff05 -02230030 -0191ff94 -fe4b01fc -ffd2fdc7 -01010126 -ff66ffe2 -// 0x00000130 -ffbb0032 -ffe9ff00 -ffdd01f6 -00c9fec2 -015801ad -f949fe9f -05cbfe57 -fd2f020a -03f9ffde -fadf0014 -02a3ff22 -0049ffa0 -fef80257 -00a3fee5 -ff59000d -0063ffdc -// 0x00000140 -0017ffff -ff6f00a6 -015cfe5b -ff24013c -ff7a009f -002fffe2 -ff20fda4 -04ad0291 -fc53ffbf -fd63fd58 -04380279 -fe08ffb0 -002c0063 -00cbfea8 -ff3401d0 -0063fef3 -// 0x00000150 -0016fff6 -ff19ff89 -01810262 -feb5fd68 -01ee01a5 -faf90129 -03d6fab4 -00f40372 -fed0feac -fe150305 -0245fe54 -fff9ffa4 -feec01cd -002dfe59 -ffc4010a -003affba -// 0x00000160 -006e001a -ff8e0062 -001dfe83 -006301ea -001500ac -fe8afe41 -0235fecd -003c0102 -fc400396 -04def99c -fc57048f -0081fdd8 -017d0110 -fef2ffdd -001f0065 -fff0ff70 -// 0x00000170 -ff7fffd4 -ff58ff40 -028902a0 -fca0fd55 -01e50184 -fd76005b -01d9fd1c -00d30044 -fd710210 -02c0ffd8 -ffb5fee8 -ff3affff -00330264 -fecefebd -00b1ff30 -007700c8 -// 0x00000180 -00060041 -007dff8b -fe8dff98 -02c6015b -fc2e0098 -0378fe4f -fef8fc43 -ff1405e1 -ffce0059 -ff3ffad1 -02b5044e -fd1afe7b -02aa0106 -fe34ff51 -00ea003f -ffd4ffad -// 0x00000190 -ffc3ffd0 -ff7e000c -026700c8 -fdc5ff4c -005a00b6 -fe2001fb -0145f987 -01460570 -fee5fe36 -fee400c4 -01fffd1c -fe7d034a -01d2ffa4 -fe02ff39 -012100c5 -ffb4ffe6 -// 0x000001a0 -00510002 -ff93ff34 -ff1f0091 -01f6ffc4 -fef40137 -008a0107 -ffd2fb8b -fe1101a1 -04f1029e -fb11feb6 -011ffdbb -01ac0330 -fdfefd99 -01b20203 -ff5cfe71 -ffcd00bf -// 0x000001b0 -003fff78 -ff120109 -02f1fec6 -fc88007b -03e7ffb8 -fb0a0229 -03a1fd95 -00010040 -ff6d0144 -fed2ff89 -ff5dff2c -0112fff3 -01490194 -ff82ff21 -00150069 -ffd5004e -// 0x000001c0 -ff8affb9 -00fc0054 -ff14fefe -017e028a -fde3fc9b -028b0178 -fbda0173 -061efc63 -fcf8032d -fde8fea6 -00a8ff08 -0236006e -fe6701d7 -ff41ff4e -0116ff5f -000600b5 -// 0x000001d0 -0020001c -ff0dffde -02c60027 -fb420001 -04ef00a2 -fb8e00b1 -02e0fb2b -00690239 -fdea02c2 -ff81fd2e -001a002d -02a2013b -fe67fea8 -ff3c013f -00800011 -ffcbff97 -// 0x000001e0 -0019ffe1 -ff22ffdd -01ceff14 -fe9f0168 -023d0036 -fb31fe88 -04900010 -feabffb6 -012500c3 -fb3c00a9 -02f8fee8 -02350044 -fcad007e -0195ffe2 -ff82ffec -005d005e -// 0x000001f0 -001dffd9 -ff03feed -020101e0 -fdabff44 -0277ffff -fb7601a9 -0245fc38 -01f9016b -fef10077 -fd7d0073 -025ffffe -fe91ff04 -019302a5 -ff36fe4b -ffe3ffde -ffef00b1 -// 0x00000200 -ffccffd9 -01070063 -ff32fe4f -ff7c0349 -001afd11 -030001e0 -fc0dfebf -0238fd1e -ff86050d -fdadfed7 -02e2fe19 -fee6ff8f -ff600389 -0100fe2a -ffa3ffe1 -0022003e -// 0x00000210 -0003ffb4 -feaaff8a -032901ce -fc5bfe22 -013a007e -ff120327 -0060f98f -020e02d2 -fe930312 -fcb8fb46 -03ed01b2 -fe57ffd4 -02580204 -fe40fee1 -ff92ff99 -006c00b0 -// 0x00000220 -001bfff0 -0008ff7a -0043ff39 -fe5c0336 -0327fd05 -fd730201 -0101fddb -feeeff5a -02c50250 -fc5a01e2 -017dfaef -005201d0 -ffb901bb -0013ff8b -0007ff1d -fff40098 -// 0x00000230 -ffa5ffb1 -007f003d -ff5100f3 -0164ff40 -fd73008f -00760136 -0091fc2a -0101025d -ff13fdad -fec905f9 -0069fa0d -01f8021c -ff71011b -ffc2ffb0 -ff290016 -00a3fff3 -// 0x00000240 -ffee0007 -00ad0012 -ff2aff21 -002400b9 -00a300df -ffdaff18 -fc78fddb -06270337 -fd6800ab -fcfbfd42 -051cffd5 -fb8200cb -025b01e7 -ff7efdfc -002600e7 -fffbffad -// 0x00000250 -ff510020 -fed4ff91 -02fb011c -fe3dfe99 -ff56027e -ff80febe -000fff5f -00f7ff66 -00c70228 -fc8afedb -01ddfe18 -ff9f0231 -0156fffa -ff96fffa -fe5dffd5 -01b10014 -// 0x00000260 -ffd50063 -007aff81 -ff4bff80 -013e006e -fd94020a -0334fed7 -fa73fcd5 -08e40354 -f9f30087 -ff64ff13 -02edfd88 -fe2601b2 -0014017c -00b2fefd -ffb50053 -0024ff84 -// 0x00000270 -ffba0034 -0010fff7 -00910182 -fe95fd97 -00510179 -02a900b1 -fca2ff17 -ffc2ffaa -fed00066 -0414fe81 -fe4b027a -000dfccf -001503fd -ffdffda3 -ffa201c5 -0060feec -// 0x00000280 -ffec003b -005bff9f -ff65ff6c -00bd015d -ff010043 -0100fef8 -fe1aff3e -03a2ffe9 -fecc0381 -fc29fc01 -038f0154 -ff71fdd7 -ff6f05e5 -003cfb00 -004201f6 -fff8ff73 -// 0x00000290 -ffacffbd -ff1b003e -02fa0080 -fc71ff9a -01a5fffe -fd0e0185 -0317fb5e -008f0467 -fde000c9 -ff53fc18 -015c00f0 -ffe5ffee -01330324 -fea8fd81 -ffff00ca -00970015 -// 0x000002a0 -ffc9002e -ffe6feb8 -ffd70220 -00d5ff2d -feebffa0 -01c4013c -fdc2fdd0 -012800f7 -03410028 -f9ba013a -0295fc90 -01450307 -ffb3fefe -ff1c007e -ffdaff2c -008e0089 -// 0x000002b0 -004f0003 -ff8a000b -018700c8 -ff36ff00 -ff5c010a -ff46ff56 -00effe8b -fe5d0180 -045dfd0d -fcce0535 -0089fad0 -fe2401c6 -026c00c6 -00b2ff96 -fec500d5 -0001ff06 -// 0x000002c0 -ffc5ff8e -0088005b -ff08ff4c -01fa009f -fdc1ff0d -021301a2 -fc94ff4d -0369fd1e -ffd902cc -fcfa010d -ffe0ff5e -03e8fdfb -fd91030d -00ebfe4e -ff6c001d -005d0068 -// 0x000002d0 -ffd1ffc9 -00610065 -ffb4014e -0102fce5 -fddb02e3 -01bdfe7a -ff6901ff -fc5fff63 -03c7fba5 -ffc503df -012cfe44 -fcb2020f -03110073 -fdf1feb2 -010300d3 -ffb9ffb1 -// 0x000002e0 -0037ffd5 -ffb900c4 -0019fe0b -0158021a -fdcdffa9 -0174fe79 -fd7d008d -04d401c4 -fd3dfd35 -fd250252 -03e5ffa3 -fefcfe36 -012302dd -fd8efd91 -018900e5 -ff90001c -// 0x000002f0 -000bff9a -ffc10058 -015700d3 -fe57fe9f -01270152 -fbd500e2 -0505fe23 -fcc2fcb5 -031102e6 -fcf90194 -fffdfcd5 -020902c1 -ff09fe2e -00d50112 -fec3ffed -0012ffb3 -// 0x00000300 -ffbbff9e -009e0076 -ff4fff62 -00f90182 -fe76feaf -03d2ff7d -fa800031 -0359ffcb -017f00a0 -facc0040 -02e1fea2 -ffeb0052 -007c02ff -ff6cfc9d -ffac0177 -0093fff9 -// 0x00000310 -ffeaff9b -feeeff97 -0330011a -fcfd0012 -0097ff92 -fedb0283 -ff94fa5b -00aa0175 -02660361 -fce8fd91 -01b4ff34 -fe81ff34 -00b904be -ff7bfd71 -fff0febb -00140169 -// 0x00000320 -ffe70000 -ffb1ffcf -01240069 -ff2cff29 -fff1015e -fff2006f -fee8fd04 -04ae007e -fc2d0298 -fd23fe8d -0448ffe3 -ff42fecd -ffa703aa -fff2fc35 -ffa00278 -008cff24 -// 0x00000330 -ff8bffda -fee9001e -01d7ff4b -ff8600bc -fea4ffb6 -ff720205 -0248fb85 -01f7021c -fc970190 -ff030084 -015bff13 -fedefee6 -020a01e4 -ff7afe71 -ff260079 -011d004a -// 0x00000340 -0030ffb4 -ffcffff1 -0127ffbd -fd600172 -0379fdfa -fe59032a -ffddfbaa -0059003f -02560302 -f995ff2f -04d1fdc7 -ff7e0150 -fe950068 -00bb013e -ffeffdea -fff90147 -// 0x00000350 -ff8a007f -ffadff5d -01090196 -fe6dfdbc -00e90243 -feb3ff2a -00d5fdb4 -feb2019b -023afffd -fc970025 -00fdfc7e -04510222 -fc7f017d -0011ff40 -ff79004c -00d8ff1b -// 0x00000360 -ffd0fffe -00f7fff0 -ffabff59 -fd5a0287 -03c5fd48 -fdb8ffe8 -024700eb -fca50123 -0118fdc4 -ffd9027a -0297fdeb -fd54025d -0103fd46 -0014020a -ffafff09 -00290015 -// 0x00000370 -ff8bff4b -ffdc00bf -016dff46 -fedc00dc -ff850045 -ff9efdfd -00fe013c -fe95ffd9 -023f0239 -fbf8fce5 -00b7fefa -02e40272 -fd99ff67 -01f60053 -fe9effc4 -00cb00c5 -// 0x00000380 -00230066 -0064ff92 -fed9fe56 -02590478 -fca7fd38 -04bbff49 -fb3cff3f -022f019c -0043009e -fe10feb0 -022300ee -fe23fdc0 -00b70398 -007dfde5 -ffcc0111 -ffe1ff54 -// 0x00000390 -ff5aff9e -0086ffca -00fe00bf -fdcaffb4 -0269ff52 -fbfc0544 -0301f80a -fd9902db -03b600ae -fc4affe6 -fedefe47 -0326014a -00270016 -fea0fff8 -008bff5c -000300fb -// 0x000003a0 -001a0025 -002bff7f -fefb0041 -01c500d4 -fe47ffad -01ec00f1 -fe0efbe2 -0208020e -ff6a03a7 -fd17fb7d -03230095 -fe99009a -010d0213 -fea2fd7b -01040154 -ffc2ff84 -// 0x000003b0 -ffc5ffa7 -ff4e008c -024dff99 -fd1a005d -01eefe22 -fe2a0354 -0184fc82 -01140159 -ffe10175 -fdcc00e6 -ff3dfc3f -004e02ad -02f0ff36 -fcc0006a -01ceff1a -ffc000d5 -// 0x000003c0 -ffcb0013 -0031ff89 -ffca002a -000c00e8 -00b9fe9f -0023015f -fe08fdb8 -0024fedb -031504c7 -fc97ff7d -febcfb0c -049a04fc -fd6bfd6b -0089027f -ff86fdce -00aa00bd -// 0x000003d0 -ffb10043 -ffeb002b -01cb0041 -fcecff28 -040b007e -fb46001d -032d001b -fdb10059 -0293fe15 -fd4700ff -fee7fe53 -054e0258 -fc69fffe -ffb4fff5 -0099005d -0019ff3b -// 0x000003e0 -001cffcf -fedd0048 -017bfecf -0071023d -fef5fda9 -fdf4030f -0353fc4a -fe890164 -02300229 -fb7ffe7c -0363fddf -ff9503a5 -ff77fd5b -00440149 -001fff9c -ffd5000e -// 0x000003f0 -001dffa4 -00a30115 -febaff77 -01baff65 -ffcdffd2 -fe3103d5 -01e7fba1 -fe670108 -00d7fdca -022f04c1 -fc1cfd2d -04260013 -fbe70068 -029100e1 -0033ff1b -ff6d007c -// 0x00000400 -001b0033 -0046ff75 -007eff31 -ffd20210 -fd56fea6 -0471003f -fbb5fe33 -02b202ac -fd7300e3 -0402fe25 -fbd0ff71 -03e80062 -fc5801b0 -025bfe7b -0011005f -ff30ffee -// 0x00000410 -0034fff3 -ff76ffe3 -00f500c9 -fecbff52 -01d70081 -fae70113 -03d4fc53 -0288024d -f97e002f -06cc0089 -fbcbfe99 -00dbfe86 -020304cd -fefffdb9 -00100063 -ff9affcb -// 0x00000420 -fffbffe8 -008cff87 -ff6500b5 -0096ffed -fd82ffae -05cafeba -f8aa023d -0588feab -fdd90004 -ff84023b -00cdfb8d -007801e7 -fe2602a2 -00e2fe00 -00d0ffdd -ff86006d -// 0x00000430 -ffcd001b -ff21ffcf -02810023 -fd0a004e -018c00a7 -fd76ff7f -00fffcd0 -02590484 -fd99fdf1 -ffd70039 -00bd00fd -ff92fe64 -00c601b5 -fe6effa9 -016bfff8 -ffcfffea -// 0x00000440 -0037ffeb -00560015 -ff68fec1 -009b019a -fef70042 -0161ffd9 -fdebfce7 -02f0036c -fec7ff67 -fd7c0063 -0338fe6f -ff51ff38 -fe9f0420 -016dfc53 -ff910195 -ffd4ffbe -// 0x00000450 -ff8eff89 -ff8fffef -00b2012d -fee1fecb -01c4ffbb -fcb00004 -0468fe6b -fceb0442 -012cff0b -fdc3ffc9 -00ccfe11 -00d1002d -ffb2042d -ff9efc94 -ffb20043 -00db00de -// 0x00000460 -fff3004f -0040ffa9 -ff94ff7a -00c70072 -ff0c012d -0011fe45 -fe71ff07 -03a1017b -fd8f039d -fe9cfac7 -03c001a4 -fd87ff58 -ff92033b -026ffcb7 -fe430167 -008dff6f -// 0x00000470 -ff8eff88 -00980001 -ffa400ff -ff6dfe1d -01480138 -febc02b0 -ffd9fa8a -feba03c0 -00cefcd4 -018c047f -fd76fd13 -0199ffdb -feec0160 -0104ff08 -ffd50098 -00040068 -// 0x00000480 -ffe20026 -0103ffc9 -fe7cff78 -00e60195 -ff31fe5f -01fe0214 -fcd3fd60 -02ad00a2 -010800f6 -fbd5007d -0258fe4e -ffb8fe27 -ffc105ed -0086fc4e -fff5008a -ffe1ffe2 -// 0x00000490 -ff89ffc7 -ff8300c3 -01f9ff5b -fd970093 -0036ff02 -001301c6 -0084fda9 -fefb0045 -028d0275 -fbfbfec5 -033dfe99 -fe0bffbb -024c0352 -fe5fff6e -ff9efefb -01130059 -// 0x000004a0 -0004005b -00b0ff48 -ff2a017d -00c8fe9a -fe440150 -03dfff45 -fb32fea8 -04300190 -feac0031 -feb0ff3c -0086fe91 -0014011e -0000012c -ffa9ffe3 -008aff4a -ffac0004 -// 0x000004b0 -00210001 -feb2ff49 -034800a7 -fcdd003b -00b6ff80 -ff030212 -0083fbc5 -027a017c -fcf7ff8b -ff8602db -016cfd91 -fed1fe67 -018e0394 -ff15fea6 -0035ff73 -00000056 -// 0x000004c0 -ffcb0010 -00fe0019 -fee6ff68 -ff3500d9 -02e6ff0f -fd3f022b -ff23fdd2 -0370fdc4 -fe9b068c -fe10fb4d -017201f0 -00bdfd67 -ff1c03b5 -012bfe37 -ff45ffb6 -fffefff4 -// 0x000004d0 -ffb1ff77 -011800f6 -fe94ff8e -00bcffbb -028e00c3 -fadb003f -0377fe42 -fbd8023d -05c1fda5 -fddc0166 -ff48fe26 -00f2038b -ff10fe39 -00e5ff5d -007d013a -ff96ffdd -// 0x000004e0 -00710032 -0033ffc6 -ff71ffaa -00af0091 -fdf900c1 -04acfe00 -fa5000b7 -03f0009a -fefd00ec -fe81fe9a -01e7fe2c -0081032f -fd79ff9d -0110fe9c -0148008f -fea00012 -// 0x000004f0 -ffeaff9e -ff9bff9c -01c80051 -ff1e0197 -ff42fdfb -fe6d01a6 -024ffaec -fed304eb -0092fde4 -00630066 -ff06feb5 -007e02cf -ffa6fe13 -004500f4 -ffafff0e -001100b3 -// 0x00000500 -fff8003e -00cbff5b -fe480011 -018a0155 -fe5cff8e -04faffd8 -f87cff10 -0545ffc7 -0028002e -fc8502fb -0078fce3 -038afed5 -fc900416 -01c6fdbe -ffa8ffc4 -ffa7004b -// 0x00000510 -ffa7000e -ff74ff54 -02e200ad -fd91ffb7 -002c016b -fe6c0080 -029afa2a -00000445 -ffd5ff6a -fee40062 -00f2fecb -0029fe69 -ffac0535 -0074fc82 -ff7e007e -00ae005b -// 0x00000520 -003e0047 -ff22ffaa -0254feb9 -fcde0350 -0312fe78 -fdf8ff2c -01bcfffd -fe56ff40 -01a401bb -fe1aff88 -00dcfee7 -ff2400e6 -009cffca -0118009e -fe54ffaf -008cfffe -// 0x00000530 -ffb8ffde -ff50ff64 -01ab01cb -ffd8fe34 -ff130200 -fce9fe29 -04a9fe2a -fd8302dd -02860042 -fbdefe6c -0281fe5d -ffba0182 -ff17017c -0235fde3 -fe23013a -00bfffb9 -// 0x00000540 -fffaffbb -ffba0052 -0056ff6f -ff78ff9e -00fc01b4 -fddfffe5 -0162fb89 -00c90701 -0148fbfd -fab0ff70 -04520099 -febc0186 -0046febc -003700a9 -ff12ff5f -00e30073 -// 0x00000550 -ff8a0024 -0012ff83 -00bd0124 -ff93fdc4 -ff3d041b -ff94fe92 -0011fc2d -01af03c3 -ff50fe14 -fda40213 -0091fd22 -0299027c -fd5d0019 -01d2ff18 -fe9500d1 -00c1ffbd -// 0x00000560 -005affef -ff62007b -002ffea3 -004701bc -01240008 -fd1200ce -041bf9dd -fde805b1 -ff14006f -0096fd29 -ffdbff07 -0065036c -001efde6 -fe7e0156 -0143ff35 -ffcc0057 -// 0x00000570 -ff87000a -ffacff81 -02770161 -fafefe50 -03da0087 -ff17000a -ffd8ff83 -00510172 -feebff10 -002aff9f -fe01ffa9 -02bcffc8 -ffb8005f -fef7ff82 -00f400e3 -ffe9ff9a -// 0x00000580 -009b0003 -ffe40035 -ff89fda3 -011c03d0 -fe3efe42 -0433fffc -fbe9fec4 -ff1dffb4 -03d502a3 -fca4fd7f -02a701fb -fd26fe72 -005e0120 -0211ffc4 -ff7b005e -ff35ffce -// 0x00000590 -ffad0039 -ff78ff9b -02390107 -fd2efdb9 -021503aa -feeeff84 -0133fd24 -fdb701e2 -01afff8f -fd8c00f5 -012bfcc1 -01c4039f -fd5fffc2 -012efe38 -ffe10160 -000fffba -// 0x000005a0 -0022ffdc -00610028 -fea6fe85 -013f0327 -ffc0fe4f -00e7008a -fd1dfebc -02d1ff02 -019e0462 -fa6bfd14 -03f8ffc9 -ffb5fea9 -ff1c041f -0059fd26 -00690062 -ff6f002a -// 0x000005b0 -004c0007 -ffcd0026 -002e009f -0108feba -004e00e2 -fd4f015f -00dbfe2c -ff0b0239 -0364f9eb -fc4705d0 -032efc73 -ff160322 -ff26fef8 -005dff27 -014d0196 -feffff4f -// 0x000005c0 -ffe00008 -003fff88 -00cfffee -fdb90212 -0248fc91 -ff690333 -ff2ffd57 -ff6900d7 -04680102 -fa39fff0 -024dfd6c -0043028c -ff94fec5 -ffef0269 -0089fddf -ffd30087 -// 0x000005d0 -ffe6006a -ff97ff82 -00ed0171 -ff30fe35 -020e015d -fc14ff2c -02690001 -ff450088 -04c8ffbc -f8a702ac -019dfc35 -008201ad -ffec0021 -00f2000a -ff35005d -0065ff7a -// 0x000005e0 -ff97ffa5 -0073003c -ff870012 -004e0073 -ffaeff1e -01680226 -fc91fd0e -04d3017f -fecfffd1 -fd8b0114 -ff03fdca -0678009b -fa9800d4 -0122ff3a -ffb9ffe6 -005f008b -// 0x000005f0 -0046ffd1 -ffdbffb5 -ff2dffe0 -020e014b -fdbbff10 -0007ff18 -011bfee4 -00b20310 -00e0fc0f -fa63046b -0567fb8e -fe140369 -00ebfe98 -004b0110 -ff45fe8e -006c009c -// 0x00000600 -ffeafff7 -00220040 -ff38fd98 -01b60397 -fc21ffc1 -060dfe1b -fa20ff80 -04d6017d -fcb60049 -0132ffe0 -ff42fe2e -00e0005d -ff73034b -003bfd29 -004a0056 -ffe00043 -// 0x00000610 -00230029 -ff4aff58 -02900208 -fc68fe98 -01acff39 -ff700494 -001bf940 -029202bc -fcd5014d -ff26fe54 -03c200d2 -fd22fec8 -037801dd -fd840084 -008fff1a -00080020 -// 0x00000620 -005dffe2 -0005fffd -ff31ff8f -00ac0184 -fea8ff39 -0355008d -fd47fdba -00980213 -018fffda -fb9fffb3 -03f5ff93 -0058ff10 -fd2c03f3 -013ffc07 -00eb019c -ff14ffb5 -// 0x00000630 -ffb0ffdb -ffc1007e -0138ffae -002fffe9 -fcbb024e -0238fd25 -fee1ff7a -02820142 -fea2004b -fe2b01a0 -01cafc20 -00b5026b -fe0f0114 -0200fee9 -fec100b0 -00e6ffbe -// 0x00000640 -0025ffdf -ffc50072 -0037ff0c -fff7ffa1 -015402b2 -fc98fe22 -01f6fd37 -00920530 -011bfcd1 -fc6102b4 -03bffc08 -fc9900f3 -02940256 -fea6fee0 -0024ffb5 -0042005c -// 0x00000650 -00030023 -ff6bff52 -0101ff97 -ffd40214 -fe42fee3 -001d013e -ff77fc10 -04b70261 -fa450053 -00c7ff18 -fff30187 -024efd04 -fe4a02e3 -00ed0008 -ff31ff1e -001b004f -// 0x00000660 -fffdffda -008cffa6 -fed0ff37 -01fe0225 -fd54ffdb -0238fdf6 -fcc501a9 -0539febf -fe650312 -fd48fdc2 -00e8fd85 -030201ed -fc0e0245 -01ccfe0e -0067ff97 -ff4700bb -// 0x00000670 -ffb3ffc8 -00bc0086 -00b900cc -fd9cfe44 -01790040 -005f0115 -006a00be -fd94fd5c -ff6502c4 -0386fc28 -fe0b03ba -0230fc74 -fdff0504 -000bfd35 -00e20114 -ffb4ff9c -// 0x00000680 -00320039 -0004ffb1 -ff78feef -013b0348 -ff1afcec -fffd00c2 -ff630102 -01aafc7e -00f00631 -fc6afa29 -004a0139 -028f011e -fe1c0212 -00d5fc90 -ffeb01fe -ffe4ff60 -// 0x00000690 -ffa80009 -ff08fff5 -03430062 -fc340019 -003fff76 -013a01c4 -fea0fbd5 -01230121 -00d00433 -fd5afbcd -014500dc -ff94fed9 -011502fa -ff74ff5e -feecff71 -01150029 -// 0x000006a0 -ffda006b -0053fe6b -ff98021b -007dff1c -ff67ff0c -0128027c -fe01fc44 -0172017f -015a0165 -fb8d007b -032afc67 -ffc30312 -fff9ff24 -ffc80142 -ffd1fe1a -005600cf -// 0x000006b0 -003bffdf -ffa60087 -01cdff59 -fe920009 -ff920039 -ffc80396 -feedf9a7 -03780261 -faf1fed5 -0660024d -fbddfd65 -ffe402bd -00a2fdeb -00860146 -0039000b -fefeff91 -// 0x000006c0 -ffbfffac -0045ffb2 -ffff0064 -ffe0ff3d -011e0086 -fd1f020d -020dfbc1 -01d901ac -fd81008e -fde70086 -028bfe5a -00e00241 -fe0efe1c -0151018b -ff65fee5 -006300c6 -// 0x000006d0 -ffe40064 -ff02ffef -01b4fff0 -fe27006f -01befe8a -fe4301de -0082ff7c -02feff88 -fbfa006c -01100251 -ff32feee -0025fc75 -01940316 -ff4f0066 -ffd8ffa6 -0062ff80 -// 0x000006e0 -0048003b -0006ffdd -ff9afece -01ae027d -fc7cfef1 -041ffea8 -fcd1ff47 -027a0274 -fd70fe9d -fec601d9 -041cfd64 -fdc20111 -ff8402c7 -017dfc7a -ff9101d7 -ffdeff46 -// 0x000006f0 -001affc7 -ffd400be -00eeff2b -fe8e00b3 -ffa0fe5d -01cb026f -ff45ff77 -fbd8feb3 -03fafec9 -fede032c -00eefc43 -fe3803cf -0018fd1f -012b039f -ff1bfd9f -ffe200a3 -// 0x00000700 -ffeefff9 -00110047 -ffd1fddd -00ad03bd -fe72fe00 -033f014c -fb42fc6f -04c00134 -fe260329 -fd81fe57 -02e9fe8d -ff2dfed7 -ffde058e -0017fb2e -000801f7 -0016ffa0 -// 0x00000710 -ffe40008 -feeeff75 -03410185 -fc92fff7 -ffb5fe9a -00050495 -0090f960 -019d008d -fe8c033a -fe7aff33 -0185febd -00a2ff01 -ffeb03e4 -ffbffd7f -ff520096 -009bfff7 -// 0x00000720 -004a001f -ff09ff1b -01e00008 -fd87006a -0267008a -fd6400bf -0166fc59 -00dd0262 -ffd20163 -fcfbff19 -03a6fe18 -fe3100e2 -ffc9029c -01b0fd45 -fe9800c7 -00830032 -// 0x00000730 -fff2ffef -ffa40060 -001b0043 -00f70002 -ffd401ba -fab5fe4a -03f8feb2 -00dd0174 -feb2ff25 -00c4027c -fe3ffc07 -0101020a -0020017e -00c7fe4e -fef600f0 -0037ff74 -// 0x00000740 -001effa1 -ff6d000a -fff1ff24 -00ce0103 -ffc0019a -0026fe9a -ff32fb41 -028e06e9 -fcf2fc57 -ffb501c2 -022bfd70 -fe6a0231 -00d8ffd2 -ffb80072 -ff9afe97 -00aa013b -// 0x00000750 -ffed000a -ffedff7e -004a01ab -ff7fff0e -016d002a -f9d4010c -07aafd0f -fd12014a -00bbffa0 -fdcf0132 -feb6ffbb -054ffe10 -fcdf01fc -0104fffc -ff22ffeb -002cfff0 -// 0x00000760 -001b001a -0134ff84 -fe4cffd2 -0002016b -0172fdf7 -00b801e2 -fcb5ff7b -0234fe19 -fef90194 -016400c6 -ffb20000 -fe96fe3d -009a0043 -00d40180 -ff8dff63 -ffb0fffb -// 0x00000770 -ffbcffac -ff460028 -018f010c -fe4cfe4b -fffa02e9 -ffd5fdde -0065fe63 -000b02f2 -0062fdc2 -fd2603a8 -0035faf4 -036a0399 -fdb00011 -00fbfe42 -fe4f0165 +ffb70007 +0039007c +000c00ac +fed5fd66 +01630101 +004b045d +fddffb1d +013bfff2 +00670371 +019ffd88 +fd5c0286 +0261fc42 +fd5b0043 +02b9021b +fd8dff35 0103ffaa +// 0x00000110 +0021ffc0 +0088000a +fedb011e +ffccfe84 +01a00014 +feccfc60 +01cc0642 +fd17fdb6 +0179fd86 +03b20270 +fc39fdda +03140372 +fc5efcbe +017202fa +fff8fdf6 +fff10068 +// 0x00000120 +ff89003d +00460071 +0076ffc4 +ff900112 +ff17fdeb +016f0256 +010bfd4a +ffd50316 +fc91fdf7 +02e4ffc9 +01aefebe +fca002ba +004ffe59 +00570004 +00410154 +001bfef2 +// 0x00000130 +004bffd7 +ff730074 +ffcdfeb5 +01fe0249 +fd93fdfa +00b7fe2f +0036028e +032fff58 +fbf10153 +01fdfeba +fe79fe5b +03e8039b +fb99fcb8 +0171022f +ff8cfece +00530000 +// 0x00000140 +0036004a +fee4ffae +01d300ba +ffc9ffc9 +fe13fee6 +02e90074 +fcdb01e8 +00f9fde3 +00e60036 +01de0198 +fbe9fed8 +02afffd1 +ff5dffa6 +004dffd2 +ff2d014a +0047ff27 +// 0x00000150 +002effb9 +0012006b +fdcfffcc +035bfe87 +fe2b007f +0139fe13 +00c50461 +feabfed4 +fe3aff77 +046afe17 +fc47020c +02b5fe7b +fc7d0085 +03b3ffe7 +fe350023 +004dfffe +// 0x00000160 +ffcc0042 +0019006f +00eaff0f +fe1f010a +0030febc +01aefffd +ff16027c +ff96fdaf +ff780078 +032d01ad +fc48fcef +02cf0088 +febc02ba +feb8fd1b +01a8016e +ffb0ff73 +// 0x00000170 +ffe2ff96 +00d7ffb3 +fe2d00f1 +00e5ff2e +fec4ff23 +02690026 +00aa0079 +fddf016f +ff1afe12 +039fff2f +fc8f0283 +02ddfe72 +fe440201 +0005fdbc +ffa6005f +002b0075 +// 0x00000180 +ffa5ffca +00830144 +ffc1fdb2 +ffbb02b0 +fff2fd05 +ff5f036f +032afd5a +fd540072 +fe6701c4 +0347ffbc +ffcdfd3a +004d015c +fdf6015d +fff3fda1 +012c015a +ffb0ffe2 +// 0x00000190 +00a8ff77 +00240110 +fe8efff4 +00a1fece +0169005a +fc73fddc +053c02e0 +fcc9fe6b +feda0131 +0490fda4 +fcf60178 +01bbfed4 +fe0901da +01a1fdfc +ff7c0198 +ff53ff27 +// 0x000001a0 +ffccffcd +00ed0068 +fe73ff6e +01130031 +feb2fe08 +0075056c +018ef9b3 +fe2e03c4 +00b60125 +ff0dfc72 +01d10050 +001902b9 +fd28fe6a +01d5002e +0072000b +ffc2fffe +// 0x000001b0 +00beffee +fefa0049 +005cffde +00d5ff4a +fe59006d +024c00d1 +fed2017e +0018ffd5 +fde2ff56 +fffafd19 +03580252 +fb8dff48 +039b00f7 +fe78ff55 +00e60082 +feee0029 +// 0x000001c0 +ff7d0027 +010fffea +ffa400f7 +fe85fee8 +021cffae +fea8016a +00e6fef6 +fe98ff05 +0165039f +fec7fdfe +01defe67 +ff650238 +0076fe14 +fdae0142 +01ecffa4 +ff8affc7 +// 0x000001d0 +0047ffd2 +0089003a +fda50108 +02ebfcb5 +fec601a2 +00c400ab +ff1cff9e +022c01dc +fac1fc2c +04250286 +ff6dfdf4 +0015fffb +002e005c +ffc6ff15 +00de01e2 +ff44feec +// 0x000001e0 +00120019 +0022ff0b +004701f6 +00cfffb0 +fcf1fdbf +02f800be +ffb501a5 +fce8ffb1 +03e400dd +fef8fdb1 +000f0000 +ffbb022e +ff99fdb3 +003e0146 +0065ff75 +ff4e0039 +// 0x000001f0 +007fffdd +ff23000f +00a7fffa +ff4fff4f +016bff4b +01ce0213 +f9ab01e7 +0532fd47 +fd7b001b +00dfff37 +ff49ff98 +013f0101 +fe9fffc9 +00f00087 +ffd1ff33 +ffd00041 +// 0x00000200 +0017002b +ffb20001 +00160008 +ffc5fefa +ffd900dd +fe9b0276 +02c6fc2a +005802e0 +fbbfffaf +03a6fceb +ffec04e2 +00ebfe32 +fc75fd51 +03bd021a +fe14ffec +0048ff70 +// 0x00000210 +003e002a +0072004f +fdf20013 +01f4ff78 +ff98ffb3 +fe16fede +0471018a +fd5701a8 +ff48fc3c +04b20227 +fcf4ff6d +026600f6 +fb6a000f +04260020 +fe21ffe6 +006fffae +// 0x00000220 +ffae0061 +0058ffd9 +0054fff0 +fe930079 +012fff0f +fe4bffcc +018500c9 +015300cd +fa94006f +0504fe0f +00ce00c4 +fdaf00b5 +ff5bfed1 +0181000c +ff350173 +009bfea5 +// 0x00000230 +00c50001 +ff720023 +feb5ffea +024a00b6 +fddcfdd9 +02ca00af +fc64025a +0329fdd5 +fca1022f +034efbf1 +00990436 +fddafe74 +ff62003f +008a0135 +0022fec6 +ff370061 +// 0x00000240 +ffe60003 +002dfff1 +00cd0039 +ffa2ffce +fd55003e +0451ff23 +febd0040 +fd4601b1 +023efea3 +ff23005b +02edffdf +fda4ffbe +ff8f0028 +0047ffc1 +0119fff4 +fef4003b +// 0x00000250 +ffbbff2a +002d00b0 +00c9fee7 +ffb00251 +ff06fd06 +0101011d +ff86ffc7 +0274010c +f9ff0130 +06edfc16 +fc050153 +01f4002b +ffdcfef8 +ff810101 +00a8ffbf +ffe4007c +// 0x00000260 +00420005 +fec50098 +02eefef1 +fe430108 +fde0ff13 +0241ffbe +0163ff90 +fcd203a7 +0026fc63 +01dd0170 +0024ffdd +002d0012 +fe88ff5d +ffe50006 +014b00e2 +ff66ff5b +// 0x00000270 +005effad +ffc80081 +fe430065 +026dfd90 +ff30011b +00d902bd +fe3cfd72 +027a02b0 +fb20fad7 +03480525 +ff27fbb7 +01e1018a +fe82009d +009ffea9 +ffa2017e +ffb8ff82 +// 0x00000280 +fffe0094 +ffbdffda +00bdff48 +003500aa +fe45ff6a +00dc0243 +007afc67 +ffaa0274 +fed800ea +02f3fd90 +fe53020c +00d5fec6 +fd9dfe8c +02d001cf +fe460059 +0068ff18 +// 0x00000290 +004eff71 +001b003a +fe44017f +0283fd90 +fd780030 +02b500d6 +fd7fff51 +0113030f +fd68faa3 +03bd0310 +ff32fd01 +015301c0 +fc82016c +02e3fdb4 +fec3016f +ffdfffbd +// 0x000002a0 +ffb40015 +ffc1006e +00d4fedd +ff1e01ec +ff9cfdaa +00a80392 +013bfb1a +fc35036c +03e40047 +ff1b0094 +fe92fe1f +0368fee4 +fdc00186 +ff640054 +007bff0e +004d002c +// 0x000002b0 +ffed0022 +005efef7 +004f0227 +004bff24 +fe6a0006 +fedcfef9 +0325ff9f +fe860265 +00d7fe50 +0050fed1 +fec90163 +004bfc56 +012204e4 +0042fd23 +ff230173 +0038ff35 +// 0x000002c0 +00240021 +ff74ffd7 +009dffe2 +00f6019f +ff44fd14 +fd2c02e5 +0348fc5a +ffaf0463 +000cff89 +ff5efcd7 +fff10160 +014a00e5 +fe9cfe2e +00c20169 +ff22ff88 +0049000d +// 0x000002d0 +0058ff8e +ff92000c +ff2f0013 +01f800a7 +fe59fc15 +01f10480 +fce7fd9a +035b0321 +fd84fd2a +01240066 +ff69ff2b +0054ff17 +ff3f015b +018dff26 +ffad0088 +ff850031 +// 0x000002e0 +ffceffb0 +ffab00d1 +005e0005 +0023ffb7 +0025fe43 +fe3d042f +02f0fab5 +fe44046e +0048005a +ffd7fa85 +004803af +014b013d +fce1fd8b +022d00a3 +ff4e0097 +0062ff9e +// 0x000002f0 +00750062 +ff2d0058 +ffbcffb5 +00edff63 +ff79008d +ffd4fe79 +008401fc +02d8fed7 +fced0208 +0073ff32 +ff52fc25 +026b04ad +fd1dfe75 +01cc0015 +fee6ffde +00b0ff61 +// 0x00000300 +fffd0025 +ff8700f7 +0118fe86 +ffc60049 +ffe2ffde +fdc00404 +02c2f902 +ff8c04da +ff71001f +0053fe9b +018c0074 +fef8ff8f +fe40fdc6 +025e030e +fe9aff6c +002eff5a +// 0x00000310 +0046fff0 +0084004a +fde000de +01fefe2b +0018ff7f +fcccff81 +0446028f +ff7cff0c +fc180060 +0562fc18 +fc980236 +041c0149 +fa0eff71 +035eff25 +ff86005d +ffd2ff88 +// 0x00000320 +ffa80057 +0053ffab +00c5013c +fde2ff7f +01affdc4 +fe260398 +0321fd50 +fcff0207 +011efff3 +00a1fe27 +ff990224 +0160fde9 +fd770072 +01ca0106 +ff05ff60 +006bff91 +// 0x00000330 +002bff62 +ff60015d +ff85fe6a +0182ff88 +ffd6ffd6 +0045ffad +ff8e03c7 +0176fd4a +fccb002a +0316006f +fd81fe9e +046e014e +fb3cfe8e +02c101c7 +ff0cfec1 +00860080 +// 0x00000340 +ff9d0003 +ffb20051 +016aff7c +fd290196 +0285fd5a +fe640101 +00c60267 +00e5fec5 +fe83fe45 +01a6030b +ff62fd18 +00dbff54 +ff2302f6 +fe90fdb3 +014e00bd +0023fff1 +// 0x00000350 +005effb6 +012e0039 +fc780011 +030200b8 +ffc3fdb8 +ffcc012d +0058ff4a +ffa60337 +fdb0fd06 +03feff9b +feda0067 +0210fef4 +fce30234 +017cfeb3 +001200b6 +ff24ffa9 +// 0x00000360 +ff9f0022 +0063006c +00c60044 +fcf7ff21 +013aff70 +024402f4 +fdf1fd3f +00ce01d4 +ff85ffb2 +002dfe60 +00960190 +006dff2f +ff1200b8 +ffc4fee0 +004300c9 +0036ff64 +// 0x00000370 +0040ffb1 +0114ff59 +fd000206 +0365fd54 +feba01b9 +fe99ff66 +03160142 +fc70fec2 +0022ff37 +01eefdd3 +004a0232 +0101ffd8 +fdc0ffbf +0251001e +febcffce +ffd6005a +// 0x00000380 +ff03002e +016dffd6 +fffc008d +fd54008f +02b2fe1c +ff2c0194 +022fffe9 +fcbbfdb7 +00af0446 +0081fbd2 +020e02ab +fceefd61 +017801cc +fea60020 +00ebffcb +0043ffb5 +// 0x00000390 +0000ffc5 +005b0042 +fe3700cd +01bdff07 +ff4afed6 +fe0aff2a +05c60139 +fd4e0244 +fc84fd79 +071dffb6 +fb61ffad +03270085 +fd6e00d4 +01f2ff62 +fe76fff5 +00baffec +// 0x000003a0 +ffbb0030 +0053ffcf +00b7ffbd +ff500108 +ff03fead +ffb30178 +03e2fde7 +fbcc00b1 +019103ae +fef5fabf +022701e5 +fef6012c +ff31fe21 +ffd90192 +01880003 +ff52ff4b +// 0x000003b0 +001b0040 +ffc600ee +000ffed1 +ffeb0076 +00a7004b +fee8ff6c +01b1019f +ff9c01cc +fcfbfcc4 +02120140 +0065fdb1 +01bd0108 +fd4b01c9 +0148fe82 +fffb01f7 +ffccfe9a +// 0x000003c0 +ffce0016 +00150077 +001ffe63 +ffe902d5 +ff6afbd3 +ff8504ab +018afd06 +007601e3 +fe8cff20 +ffe700bd +0153fdab +014f010f +fcf001d7 +0057fd19 +01300174 +ff9affd9 +// 0x000003d0 +007e0039 +ff7c002f +ff08ffdb +0362ffa0 +faf9fef4 +04a80136 +fc3000a9 +034fff3d +febefedd +00e6009f +0164fe75 +fcb801b0 +02e3ff6a +ff820080 +ff440083 +ffe3fedf +// 0x000003e0 +ffd10031 +0099fffb +ffcfff15 +ffd1018f +0072fd59 +fc9202af +04b9fd92 +fe530286 +fda3ffaf +00e1ff33 +03d5ff2f +fc570317 +0082fce3 +007c01ab +0093ffc6 +ffa5ff94 +// 0x000003f0 +0077003a +002a0072 +fda6ff64 +0396ff50 +fd7f0119 +0005fdf7 +018303aa +01a0fe53 +fc91ff7a +03dc00e8 +fbf0ff94 +022c00a8 +ff29ff3f +00550167 +0037ff4a +ff1efff5 +// 0x00000400 +0027004d +fff5fff4 +00a2ffc2 +fe3100b0 +024aff2c +fd64018f +0423fcf4 +ff36030c +fb19fe8d +034bfdf2 +018e01cc +fd5f02a6 +008efbea +ff800203 +0145004e +ff66ff66 +// 0x00000410 +0000ffd4 +0068ffef +fe5b0119 +0145feb0 +ff97ff54 +0001fff6 +016a00c0 +00ba03b3 +fb0cface +05be00e5 +fdf103ef +00fbfc7a +fd0d0166 +00d1ffda +003aff6c +ffae002f +// 0x00000420 +ffde0044 +007b003d +0011ffe9 +fe4f0036 +0115fe01 +003404bf +0036fbfd +01290152 +fe0aff8a +001b009b +00ddfe5b +00570288 +fffffd89 +fe020101 +01800077 +ffc5ff48 +// 0x00000430 +000cffd0 +0152ff0f +fd320301 +01d5fd8e +ff39ffb3 +006efe91 +013d03d9 +fd20fea5 +000cff42 +02cafe0b +fe08021b +026ffd92 +fda30267 +0116ffd1 +ff95ff7f +ff9c001f +// 0x00000440 +004b003f +ff7e0035 +0103ff59 +00b60059 +fbf20063 +03efff36 +ff9dff7e +ff1a035e +ff33fc47 +01f601c1 +fefdfeed +015600b1 +fde0ff63 +fff10068 +018b0030 +ff0effc4 +// 0x00000450 +ffa9ffbf +ffc9ffb9 +ff8600a0 +0274ffb7 +fbf5feb8 +04a6ffe1 +fd1a03f3 +0017fded +fe67fe9d +01c70125 +ff04fe92 +02b8ffe3 +fbb70080 +03a2fff1 +fd800017 +0135ffe9 +// 0x00000460 +00180073 +ff7fffb1 +0148005f +fff0ff7b +fe26006c +016900bc +ff47fc1d +01a104a6 +fc5cff6d +0369ff95 +0008fe07 +ffa002b5 +fe96fcac +00f302da +fff1ff45 +ffd3ff8e +// 0x00000470 +00e4ff58 +ff5500b1 +fffeffae +0006009d +0056fd93 +fff101fa +fee7ff08 +034a01fb +faa4fccc +05930227 +fd26fe84 +0118016d +ff66fe01 +000701a2 +00a9ffde +fea00007 +// 0x00000480 +0011006b +ffc8ffb1 +0139001f +ff11000f +fe28ff9d +01bb013f +012cfe5a +004101a5 +fbcd0093 +04b2fd27 +fe6b00a7 +01830293 +fbe6fbf1 +02f702c1 +ff74ffdc +ffcfff59 +// 0x00000490 +002fffc5 +005affa5 +fed10244 +00c2fd27 +006a00dd +fd69fe06 +02c003e0 +fe77fe5c +004d00e3 +0042fbb5 +0033030c +0262ff63 +fb2601e7 +022bfe88 +ffe0002c +ff95000a +// 0x000004a0 +ffd7fffa +fff2fff9 +ffdeffcc +0104003b +fddcff3b +00c3014b +0269fe32 +fd39015d +002b0116 +013cfd07 +fdac00c6 +03fa01e3 +fbdeff09 +015b0051 +00e1ffa8 +ffed0029 +// 0x000004b0 +0047ffde +009f002b +ff24017a +ffdcfdb3 +011b0075 +fc2b009b +044800c9 +fee400eb +fefffdd6 +ffd7ff89 +01aaffae +ffe60165 +ff6b00ff +fe8bff09 +023e005f +fe6effcd +// 0x000004c0 +001bfffe +0006ffff +ffda004d +01ae0091 +fe59fe3e +ff0601dc +0158fc02 +017f0598 +fce1ff00 +0212fc5b +ff6201bf +015201b7 +fdcffd10 +013e01f6 +ff58ff76 +00150024 +// 0x000004d0 +0087ff7c +002a00e4 +fec3ff40 +00c1fff8 +ffbcfe1c +fef6042e +0262fd4b +fdd70136 +ffd7fd3a +00b8fff2 +00250184 +013fffec +fd5200b6 +01fcfdfc +ff420171 +ff4dff6e +// 0x000004e0 +ffcc0004 +005100a7 +001affbf +ffd3ff8a +ff80ff63 +005e03b5 +0235f9db +fe850446 +fecc00be +ffc5f9a7 +02000561 +ff51ff88 +fd80ff13 +021c0039 +ffc100bd +001fff7c +// 0x000004f0 +fffffff3 +ff8e0083 +ff83fee9 +02a6000f +fb8a0071 +046afe97 +fd1e01c9 +03a6015d +fbebff03 +001afe81 +01f5fde9 +fd5c0275 +02a4ff89 +ff92ffdd +fefaffdd +007cffcf +// 0x00000500 +ff4affd2 +016400e4 +ff0dfff1 +ff5aff09 +0068ff79 +00f70385 +fe78fb3d +021103c4 +fdbc0042 +022afcf8 +fe570187 +03860043 +fc26fdeb +00670227 +0078ff1b +003b0020 +// 0x00000510 +0067ff87 +006affba +fe13021c +0184fd52 +ffb10118 +ff28fbfc +01c30565 +0060fd3d +fd130211 +0258fd9e +ff6ffed4 +023803a2 +fc2dfd78 +01620108 +004bfed3 +ff800083 +// 0x00000520 +0021ffec +ff5d00ec +01acfec9 +fefa0034 +ff1b0021 +ff990087 +02edff3b +fe3f0131 +ff59ffc4 +012bff22 +ff2a0069 +0314ff4c +fbff00df +00b7ff47 +0131005b +ff53fffb +// 0x00000530 +0053ff4e +ffac00cb +fedafef1 +022b0099 +ff0afffa +009efd4f +fead0474 +0279fe69 +fb99fffc +0412ffb1 +fd80fe6d +020101f1 +fd72fd98 +02e4018d +feb1ffc2 +ffeb0035 +// 0x00000540 +000affba +fef00074 +0134ffa8 +ffae001f +ffd500cf +ff2efd43 +01be0271 +ffbd01ad +fcd0fd2e +0512010e +fe2cfe02 +ff6202f3 +ffadff15 +00ecfde3 +ff0601c1 +0097fff1 +// 0x00000550 +0009ff70 +00d100c6 +fcc4ff70 +048bff5a +fc990022 +0302fe4b +feb80306 +0051ff2c +feb90138 +0111fd00 +0078ffca +00f5014c +fdbdffd2 +01840077 +ffe4fee4 +fff700c6 +// 0x00000560 +0016fff9 +ff50005a +01a5ff53 +fe4c021b +003cfcb0 +005fffda +0163048c +fccffe25 +00a6fdc3 +04a401d6 +fb83ff4b +00b6004b +009800d0 +ffddfe3a +001500fa +ffcfffd1 +// 0x00000570 +0026ff5f +006d0068 +fec30006 +012a00df +ff49fdc4 +ff6e0100 +0242ffec +fe0aff72 +00aafd8d +0441051a +fae5fde6 +025200af +fe1bfe08 +025c011e +fe82ffa8 +00420058 +// 0x00000580 +0005fffe +006f010f +ffc4feec +fffeffe6 +ff3dff63 +0079031c +015cfbb7 +ffb9022d +fb8101d4 +0499fd2b +00ce0076 +fef001b6 +fdfdfddf +0093001a +015201b3 +ff45fee7 +// 0x00000590 +00a6ffba +ffb300ce +fe9c000d +0279fe68 +fe67019a +ff10fd8b +017f024a +0121fed6 +fcd60026 +03ab0218 +fcaafcc7 +03a10234 +fc39ff8e +0162ffa3 +ffa700da +ff5dff2a +// 0x000005a0 +ffdd0038 +00280035 +0058ff3b +ff64013c +004cfe0b +fea1032d +02c3fbcc +ff0b028b +ff0d02f8 +ff90fa21 +023601c7 +007e010a +fbde0009 +0227fee5 +0053014e +ffdbff67 +// 0x000005b0 +00bd0060 +ff05004f +0041004b +004ffe10 +ffb6023d +fe5cfdd2 +01950222 +0140fed0 +fce900f6 +0235fe9b +ffb9feef +ff1b0062 +009c020d +006afe58 +fed9019c +ffb6fe42 +// 0x000005c0 +ffef000e +006e003b +0090fec7 +fd9b0048 +02620156 +fd95ff76 +00e7fe16 +023501df +fd0d0100 +fea8ff8b +0468fddb +fe5f036c +ff4efd6c +00790160 +006dff60 +ffb5ffe9 +// 0x000005d0 +00390013 +00b10010 +fd4a0112 +01effd5a +018201d5 +fc93ffa2 +00f70022 +00890084 +00150013 +0225fd80 +fc6c01ee +02150060 +ff50ff95 +ffa30066 +00830016 +ff57fff2 +// 0x000005e0 +00380043 +ff6c005c +01bcfeea +fde2001f +010000fc +feba018a +01d9fb42 +002f0220 +fde803c5 +0046fbb4 +010000ba +01ec01cb +fcd0fddc +016c00de +fff30022 +ffb3ff96 +// 0x000005f0 +00a1ffaf +fff4006c +fee8003a +ff5cfe50 +034e006e +fc0bff97 +0342025c +ff7efe11 +fed7015d +00cafbde +007a03aa +fef6feae +00820182 +ff57fe83 +0124ffec +fed00065 +// 0x00000600 +0023ffed +ffff011b +005efe65 +ff8c0006 +fff0ffda +ff100499 +0298f993 +ff4a03ba +fdf3ff1f +014ffea9 +019e028d +ff1aff0c +fda6fe16 +02ea012f +fea8007f +ffe0ffa8 +// 0x00000610 +0038001e +009affa5 +fdb8008d +017fff2b +0058ff07 +fec3fef0 +02790297 +ffca0220 +fc06fa18 +07b60213 +f9aa01bd +0473fd79 +fbee02ff +02b1fe64 +fef90043 +ffd8ffb0 +// 0x00000620 +ffa3fff3 +00cd0105 +ff0ffe57 +fee50111 +0276ff5b +fe840131 +ff0dfeb0 +03570103 +fabdfffd +02f100e7 +0359fde1 +fc4d023d +0062fe55 +0012ff4f +008301f0 +fff3fecb +// 0x00000630 +008d004c +0011ffad +feb20112 +0102fe58 +ffc8004f +0227ff78 +fdd403d8 +01ccfe11 +fb97ff30 +0609fecd +fb0c0268 +02c8ff56 +fd38ffad +02a7ffbe +ff2a007e +ff92ffa9 +// 0x00000640 +0027ffe4 +ff680075 +008bff50 +00930179 +fdb5fea8 +02d301b0 +ff9afc37 +fee7045c +010bfe80 +fdf0ff5f +02abff4e +00dd008f +fba500e0 +020dfefc +00d40087 +ff41ffd4 +// 0x00000650 +0035ff55 +ffbb00c3 +fef5feec +014000f1 +ff6afd66 +02e502d0 +fd82fec8 +00a800d3 +ff0500a1 +022ffd49 +fdd9024e +01e200d1 +fdc8fe00 +01b10054 +fec4ff52 +00d6012b +// 0x00000660 +00230072 +ffa5fffa +0200ff17 +fd7301b2 +ff03fdcd +0385017d +ff8bff81 +fdab0072 +ffaffdcc +01370268 +ff84ff9f +00bd01a4 +ff8ffdc5 +00230091 +00a500e1 +ff89fee0 +// 0x00000670 +00a1ffad +ffd40080 +fe7b008f +0277fe43 +fed70182 +ffcdffc3 +febb0072 +01d200e5 +fc5ffced +0416020c +fed3fdad +0071017d +ff89011c +ffd5fe59 +00770192 +fefaff6b +// 0x00000680 +fffc0056 +ff36004f +0257fef7 +fe0d008e +ff6d00b6 +ff8cffdd +0220fcd0 +003603e5 +fc4401de +02b0fbff +016b0071 +ff6f01be +fe93fde2 +002200ed +009600f4 +0002febf +// 0x00000690 +0018ff91 +00c300a7 +fe230115 +0238fc2d +fe4003a6 +0077fe46 +000b0322 +00e7fd24 +faf40143 +058dfd9f +fe0dff4f +01c40211 +fe30ff72 +0075005c +00b9004e +fef1fff6 +// 0x000006a0 +ffa7006b +00bf001d +fedcfee5 +0083018a +ffedfe20 +fee204f6 +0165f9e7 +005601aa +ffd10317 +fef5fe5b +ffb6fec9 +02e300f0 +fe9bff4a +ff5600c2 +0019ff97 +0048ff94 +// 0x000006b0 +006bffee +0063ff69 +fe1c0269 +02c7fb9c +fdc0034c +00b4fe4b +0066010b +00f90130 +fdb5fd0c +0297fff3 +007a006d +fe51ffc8 +00b40346 +001efcc9 +00280123 +ff7bfffc +// 0x000006c0 +004b0039 +00280058 +fee1ff0d +020d005e +ff4bfec0 +fe4804a7 +013ff95c +ff930344 +006b01ff +ff9efde6 +00950035 +011f0040 +fcf7fec8 +01ea011b +0023008a +ff79ff36 +// 0x000006d0 +0093ff78 +fff000a3 +ffa6fff3 +ff53fe9a +02c0007d +fd5600b5 +0198018a +ff2a000a +fed5ff9a +0084ff3d +ff5efed7 +002d0006 +00e80215 +fedafe5f +01bc00e8 +fe1a0012 +// 0x000006e0 +fff20019 +000a0015 +ffdbffcf +007d0164 +fe66fc74 +020a04dc +ff67fb95 +002e034a +fd9affdd +02acfd3f +00ef0279 +002f003e +fbd6fd06 +039801f4 +feefffd3 +ffe6ffd0 +// 0x000006f0 +001a009d +ff62ff1f +01200165 +fe6ffea5 +01d5ff37 +fe6a01d3 +01d8fed9 +013c02cd +fce4fcd9 +014400dd +fddcff35 +0239fe81 +001d022f +ffd0fea1 +ff840091 +0084ff2d +// 0x00000700 +ffa7ff88 +003400e3 +00810034 +fe66ff66 +0158fcc5 +ff16071e +ffdffa8b +01900282 +ff4d0056 +fe9afe23 +03430088 +ff1000fa +fdccfdc1 +019401fc +ff75feed +00520066 +// 0x00000710 +005c0004 +000e0053 +fdc40037 +0368ff16 +ff2d00a5 +fe6dfc7c +02d804db +ffa2fde0 +fdc2011e +04ccff23 +fc2afea5 +0442024a +fa4900dd +0225fe0a +003e006d +ffc0ffdc +// 0x00000720 +ff95ffe1 +008d005a +0043ff57 +fe8901e8 +00ecfc46 +ff7e029d +0218ff72 +fce90235 +00adfb9b +01150416 +010dff19 +ff33fd36 +fed2024e +0058001b +0040ff0e +003b0085 +// 0x00000730 +ffd5ffa8 +008a005f +fe54ff5b +018f0040 +007cffbb +ffd2fed8 +004a0315 +fe6b00af +fff1ff56 +0168fd75 +fcf400cb +05cbff86 +fa2a0297 +02b8fd1c +ff42018d +ffffffbb +// 0x00000740 +fff1ffec +ff7300b0 +01e8ff64 +fdd50126 +ffb4fe43 +01a6003f +0150016b +fbddfff0 +014ffe78 +030d01fe +fd46ffd4 +0255fdaa +fe800159 +ff0effd3 +01260015 +ffadffc8 +// 0x00000750 +003effa5 +0035fff1 +ff7c00ea +00c4fee3 +fffaffed +fea4fdbf +01c105c6 +00a1fc06 +faf201b3 +05d7fc2d +fe5202e0 +0180fe43 +fec6017f +ff28ffe7 +0181fffc +ff13ffe0 +// 0x00000760 +ffc0ffff +002e00b6 +001cfeb7 +fea8014c +0052fee7 +015700e3 +005900a9 +fe66fed8 +ffdcff1b +011a030e +ff5afd3f +0182013c +ffb60027 +fe05fedd +01850049 +ffd4000c +// 0x00000770 +0088ffc2 +0098ffd3 +fd7601aa +02bbfd44 +febc001b +00c00048 +00990039 +00ad0074 +fcb2022e +03bafaaf +fdec031c +01b7ff26 +fdce0001 +010a0016 +0079ffc5 +ff2d0042 // 0x00000780 -008a004b -ff7cffba -fedbffbf -02b80008 -fe430207 -0139fdfd -ff06fe77 -ff02013a -013002d7 -00a2fa9e -ffa1050f -fd00fcf6 -0313014f -0029ffa7 -fee600bb -004eff54 +ffb0ffc6 +0097011d +009afe82 +fe010203 +00e5fd4a +0184022e +ff91ff1c +00460087 +fbe2fe72 +03fb009f +01760112 +fd19ff79 +fee5fe82 +01da0186 +ffcb0064 +ffe8ff75 // 0x00000790 -fff0fff7 -ff6c0013 -02a7ff51 -fd460188 -004bff26 -00b604a1 -fd26f7b9 -03e10465 -fd2afee5 -fefe001b -0209fea3 -ff0e0234 -0167ff6a -fe58ffdd -00f6004f -ff8b000b +0062ffa5 +002500b7 +fe5d009f +010ffd9c +013601b1 +fbb8fc62 +05e604af +fcecfdfa +00280109 +0171fdbf +0045001f +fec900ea +ffc801d9 +fff2fddc +0068006b +ff74fffc // 0x000007a0 -ffd0ffca -000800b8 -ff0aff01 -028bffde -fd8c0228 -ffb9ff48 -0164fca1 -00cc037a -ffb0ffc4 -fc28fdea -0316037f -00a1fc9c -00100286 -fe9bfe66 -00a000eb -0044ff74 +ff95ffe2 +00b1fff2 +ff64006a +ff9afebe +01d400b1 +fc8900a5 +046dff69 +fd25fdeb +004f051c +fedbfa5c +030e0292 +fd920048 +ffc40029 +00a3ff99 +006d004b +002ffffb // 0x000007b0 -fff6ffc1 -ff230058 -02b5007b -fd06fe72 -01f6011c -fdc1026c -0120fa7a -007b045a -00eafdd7 -fcb102ae -021bfced -fe8cfff0 -03fa02c0 -fcafff26 -0100ffe2 -003f0044 +00610066 +ff5900e9 +002bff1f +ffaa0030 +0144ff6e +fc020098 +04060002 +0032015b +fdfffeea +0183feed +fda500a9 +0344002a +fd3400ba +015aff4a +ff1a0156 +0000fe8b // 0x000007c0 -fff4ffe0 -0067ffca -ff74003a -feef00cc -03b9fe4a -fba302e4 -0304fb52 -fd930292 -038c01c0 -fc2dfe36 -0064fe24 -02290332 -fee3fe56 -ffc10180 -0030fea0 -0035007c +ffe0ffb5 +ffeb008c +004effb6 +ffe8ff5e +ff090058 +000b0020 +0314001e +fa80feee +0398043f +feedfb66 +01ba0274 +ffb2fe7c +ff7b0134 +fe81ff4e +0230ff78 +ff3a0098 // 0x000007d0 -005eff5f -fee800b1 -023900f0 -fcd0fdd9 -013000a5 -ffa10032 -ff650149 -01b1fe0c -fdceffc5 -00800039 -00a10076 -ffaaffef -ff0400ff -011fff04 -00310001 -ff9d0094 +0018ff95 +ffdb00e0 +fe62fe43 +03f80169 +fe91fdf0 +ff3903f3 +fec5fe71 +026b001d +fd98ffbf +0299fe4a +ff36fff3 +ff20023f +fefffd40 +024f014b +ff83ff15 +000100f3 // 0x000007e0 -ff5cffff -00adff00 -003001c7 -fe85fe80 -02340003 -ff280263 -fda6fb46 -03c003ae -ffe6fd29 -fb4103fa -02b6fc75 -0111012c -ff020055 -004a0087 -fef4ff5e -01520062 +0050ffef +000efff1 +00060141 +ffa2fe18 +020500c3 +fb86fe6f +039a01db +ff2fffd4 +fffa02e1 +fe24f9cb +035c02db +fece01e8 +feedfe69 +0004ff91 +01500095 +ff1dffe8 // 0x000007f0 -ffe3ff95 -ffe300c2 -ffc9ffce -ffb0fed6 -022a0084 -fc3f0040 -030500f0 -0071fd0a -fbc7037d -0489ff40 -fdf10228 -ff40fbd6 -ffe0035e -0001fd86 -018d0156 -ff13ffe2 +00820045 +ffd0ff97 +ff25ffc4 +00a6ffc0 +ffb0ff0e +ff8a022b +0276ffeb +fed6fe67 +fe760175 +00aaff6d +006dffe8 +ff440068 +0060ffd4 +ff180105 +0070fedd +ff34001d diff --git a/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v b/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v index a44abfd..75904fc 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v +++ b/9_Firmware/9_2_FPGA/tb/tb_rx_gain_control.v @@ -38,10 +38,20 @@ reg signed [15:0] data_q_in; reg valid_in; reg [3:0] gain_shift; +// AGC configuration (default: AGC disabled — manual mode) +reg agc_enable; +reg [7:0] agc_target; +reg [3:0] agc_attack; +reg [3:0] agc_decay; +reg [3:0] agc_holdoff; +reg frame_boundary; + wire signed [15:0] data_i_out; wire signed [15:0] data_q_out; wire valid_out; wire [7:0] saturation_count; +wire [7:0] peak_magnitude; +wire [3:0] current_gain; rx_gain_control dut ( .clk(clk), @@ -50,10 +60,18 @@ rx_gain_control dut ( .data_q_in(data_q_in), .valid_in(valid_in), .gain_shift(gain_shift), + .agc_enable(agc_enable), + .agc_target(agc_target), + .agc_attack(agc_attack), + .agc_decay(agc_decay), + .agc_holdoff(agc_holdoff), + .frame_boundary(frame_boundary), .data_i_out(data_i_out), .data_q_out(data_q_out), .valid_out(valid_out), - .saturation_count(saturation_count) + .saturation_count(saturation_count), + .peak_magnitude(peak_magnitude), + .current_gain(current_gain) ); // --------------------------------------------------------------- @@ -105,6 +123,13 @@ initial begin data_q_in = 0; valid_in = 0; gain_shift = 4'd0; + // AGC disabled for backward-compatible tests (Tests 1-12) + agc_enable = 0; + agc_target = 8'd200; + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd4; + frame_boundary = 0; repeat (4) @(posedge clk); reset_n = 1; @@ -152,6 +177,9 @@ initial begin "T3.1: I saturated to +32767"); check(data_q_out == -16'sd32768, "T3.2: Q saturated to -32768"); + // Pulse frame_boundary to snapshot the per-frame saturation count + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; check(saturation_count == 8'd1, "T3.3: Saturation counter = 1 (both channels clipped counts as 1)"); @@ -173,6 +201,9 @@ initial begin "T4.1: I attenuated 4000>>2 = 1000"); check(data_q_out == -16'sd500, "T4.2: Q attenuated -2000>>2 = -500"); + // Pulse frame_boundary to snapshot (should be 0 — no clipping) + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; check(saturation_count == 8'd0, "T4.3: No saturation on right shift"); @@ -315,13 +346,18 @@ initial begin valid_in = 1'b0; @(posedge clk); #1; + // Pulse frame_boundary to snapshot per-frame saturation count + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; check(saturation_count == 8'd255, "T11.1: Counter capped at 255 after 256 saturating samples"); - // One more sample — should stay at 255 + // One more sample + frame boundary — should still be capped at 1 (new frame) send_sample(16'sd20000, 16'sd20000); - check(saturation_count == 8'd255, - "T11.2: Counter stays at 255 (no wrap)"); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + check(saturation_count == 8'd1, + "T11.2: New frame counter = 1 (single sample)"); // --------------------------------------------------------------- // TEST 12: Reset clears everything @@ -329,6 +365,8 @@ initial begin $display(""); $display("--- Test 12: Reset clears all ---"); + gain_shift = 4'd0; // Reset gain_shift to 0 so current_gain reads 0 + agc_enable = 0; reset_n = 0; repeat (2) @(posedge clk); reset_n = 1; @@ -342,6 +380,479 @@ initial begin "T12.3: valid_out cleared on reset"); check(saturation_count == 8'd0, "T12.4: Saturation counter cleared on reset"); + check(current_gain == 4'd0, + "T12.5: current_gain cleared on reset"); + + // --------------------------------------------------------------- + // TEST 13: current_gain reflects gain_shift in manual mode + // --------------------------------------------------------------- + $display(""); + $display("--- Test 13: current_gain tracks gain_shift (manual) ---"); + + gain_shift = 4'b0_011; // amplify x8 + @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0011, + "T13.1: current_gain = 0x3 (amplify x8)"); + + gain_shift = 4'b1_010; // attenuate /4 + @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b1010, + "T13.2: current_gain = 0xA (attenuate /4)"); + + // --------------------------------------------------------------- + // TEST 14: Peak magnitude tracking + // --------------------------------------------------------------- + $display(""); + $display("--- Test 14: Peak magnitude tracking ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_000; // pass-through + // Send samples with increasing magnitude + send_sample(16'sd100, 16'sd50); + send_sample(16'sd1000, 16'sd500); + send_sample(16'sd8000, 16'sd2000); // peak = 8000 + send_sample(16'sd200, 16'sd100); + // Pulse frame_boundary to snapshot + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + // peak_magnitude = upper 8 bits of 15-bit peak (8000) + // 8000 = 0x1F40, 15-bit = 0x1F40, [14:7] = 0x3E = 62 + check(peak_magnitude == 8'd62, + "T14.1: Peak magnitude = 62 (8000 >> 7)"); + + // --------------------------------------------------------------- + // TEST 15: AGC auto gain-down on saturation + // --------------------------------------------------------------- + $display(""); + $display("--- Test 15: AGC gain-down on saturation ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + // Start with amplify x4 (gain_shift = 0x02), then enable AGC + gain_shift = 4'b0_010; // amplify x4, internal gain = +2 + agc_enable = 0; + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; + @(posedge clk); @(posedge clk); + + // Enable AGC — should initialize from gain_shift + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0010, + "T15.1: AGC initialized from gain_shift (amplify x4)"); + + // Send saturating samples (will clip at x4 gain) + send_sample(16'sd20000, 16'sd20000); + send_sample(16'sd20000, 16'sd20000); + + // Pulse frame_boundary — AGC should reduce gain by attack=1 + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + // current_gain lags agc_gain by 1 cycle (NBA), wait one extra cycle + @(posedge clk); #1; + // Internal gain was +2, attack=1 → new gain = +1 (0x01) + check(current_gain == 4'b0001, + "T15.2: AGC reduced gain to x2 after saturation"); + + // Another frame with saturation (20000*2 = 40000 > 32767) + send_sample(16'sd20000, 16'sd20000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + // gain was +1, attack=1 → new gain = 0 (0x00) + check(current_gain == 4'b0000, + "T15.3: AGC reduced gain to x1 (pass-through)"); + + // At gain 0 (pass-through), 20000 does NOT overflow 16-bit range, + // so no saturation occurs. Signal peak = 20000 >> 7 = 156 > target(100), + // so AGC correctly holds gain at 0. This is expected behavior. + // To test crossing into attenuation: increase attack to 3. + agc_attack = 4'd3; + // Reset and start fresh with gain +2, attack=3 + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_010; // amplify x4, internal gain = +2 + agc_enable = 0; + @(posedge clk); + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + + // Send saturating samples + send_sample(16'sd20000, 16'sd20000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + // gain was +2, attack=3 → new gain = -1 → encoding 0x09 + check(current_gain == 4'b1001, + "T15.4: Large attack step crosses to attenuation (gain +2 - 3 = -1 → 0x9)"); + + // --------------------------------------------------------------- + // TEST 16: AGC auto gain-up after holdoff + // --------------------------------------------------------------- + $display(""); + $display("--- Test 16: AGC gain-up after holdoff ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + // Start with low gain, weak signal, holdoff=2 + gain_shift = 4'b0_000; // pass-through (internal gain = 0) + agc_enable = 0; + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; // target peak = 100 (in upper 8 bits = 12800 raw) + @(posedge clk); @(posedge clk); + + agc_enable = 1; + @(posedge clk); @(posedge clk); #1; + + // Frame 1: send weak signal (peak < target), holdoff counter = 2 + send_sample(16'sd100, 16'sd50); // peak=100, [14:7]=0 (very weak) + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0000, + "T16.1: Gain held during holdoff (frame 1, holdoff=2)"); + + // Frame 2: still weak, holdoff counter decrements to 1 + send_sample(16'sd100, 16'sd50); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0000, + "T16.2: Gain held during holdoff (frame 2, holdoff=1)"); + + // Frame 3: holdoff expired (was 0 at start of frame) → gain up + send_sample(16'sd100, 16'sd50); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0001, + "T16.3: Gain increased after holdoff expired (gain 0->1)"); + + // --------------------------------------------------------------- + // TEST 17: Repeated attacks drive gain negative, clamp at -7, + // then decay recovers + // --------------------------------------------------------------- + $display(""); + $display("--- Test 17: Repeated attack → negative clamp → decay recovery ---"); + + // ----- 17a: Walk gain from +7 down through zero via repeated attack ----- + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_111; // amplify x128, internal gain = +7 + agc_enable = 0; + agc_attack = 4'd2; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; + @(posedge clk); + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0_111, + "T17a.1: AGC initialized at gain +7 (0x7)"); + + // Frame 1: saturating at gain +7 → gain 7-2=5 + send_sample(16'sd1000, 16'sd1000); // 1000<<7 = 128000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0_101, + "T17a.2: After attack: gain +5 (0x5)"); + + // Frame 2: still saturating at gain +5 → gain 5-2=3 + send_sample(16'sd1000, 16'sd1000); // 1000<<5 = 32000 → no overflow + send_sample(16'sd2000, 16'sd2000); // 2000<<5 = 64000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0_011, + "T17a.3: After attack: gain +3 (0x3)"); + + // Frame 3: saturating at gain +3 → gain 3-2=1 + send_sample(16'sd5000, 16'sd5000); // 5000<<3 = 40000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b0_001, + "T17a.4: After attack: gain +1 (0x1)"); + + // Frame 4: saturating at gain +1 → gain 1-2=-1 → encoding 0x9 + send_sample(16'sd20000, 16'sd20000); // 20000<<1 = 40000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_001, + "T17a.5: Attack crossed zero: gain -1 (0x9)"); + + // Frame 5: at gain -1 (right shift 1), 20000>>>1=10000, NO overflow. + // peak = 20000 → [14:7]=156 > target(100) → HOLD, gain stays -1 + send_sample(16'sd20000, 16'sd20000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_001, + "T17a.6: No overflow at -1, peak>target → HOLD, gain stays -1"); + + // ----- 17b: Max attack step clamps at -7 ----- + $display(""); + $display("--- Test 17b: Max attack clamps at -7 ---"); + + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_011; // amplify x8, internal gain = +3 + agc_attack = 4'd15; // max attack step + agc_enable = 0; + @(posedge clk); + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + check(current_gain == 4'b0_011, + "T17b.1: Initialized at gain +3"); + + // One saturating frame: gain = clamp(3 - 15) = clamp(-12) = -7 → 0xF + send_sample(16'sd5000, 16'sd5000); // 5000<<3 = 40000 → overflow + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_111, + "T17b.2: Gain clamped at -7 (0xF) after max attack"); + + // Another frame at gain -7: 5000>>>7 = 39, peak = 5000→[14:7]=39 < target(100) + // → decay path, but holdoff counter was reset to 2 by the attack above + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_111, + "T17b.3: Gain still -7 (holdoff active, 2→1)"); + + // ----- 17c: Decay recovery from -7 after holdoff ----- + $display(""); + $display("--- Test 17c: Decay recovery from deep negative ---"); + + // Holdoff was 2. After attack (frame above), holdoff=2. + // Frame after 17b.3: holdoff decrements to 0 + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_111, + "T17c.1: Gain still -7 (holdoff 1→0)"); + + // Now holdoff=0, next weak frame should trigger decay: -7 + 1 = -6 → 0xE + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_110, + "T17c.2: Decay from -7 to -6 (0xE) after holdoff expired"); + + // One more decay: -6 + 1 = -5 → 0xD + send_sample(16'sd5000, 16'sd5000); + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + check(current_gain == 4'b1_101, + "T17c.3: Decay from -6 to -5 (0xD)"); + + // Verify output is actually attenuated: at gain -5 (right shift 5), + // 5000 >>> 5 = 156 + send_sample(16'sd5000, 16'sd0); + check(data_i_out == 16'sd156, + "T17c.4: Output correctly attenuated: 5000>>>5 = 156"); + + // ================================================================= + // Test 18: valid_in + frame_boundary on the SAME cycle + // Verify the coincident sample is included in the frame snapshot + // (Bug #7 fix — previously lost due to NBA last-write-wins) + // ================================================================= + $display(""); + $display("--- Test 18: valid_in + frame_boundary simultaneous ---"); + + // ----- 18a: Coincident saturating sample included in sat count ----- + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_011; // amplify x8 (shift left 3) + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd2; + agc_target = 8'd100; + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + + // Send one normal sample first (establishes a non-zero frame) + send_sample(16'sd100, 16'sd100); // small, no overflow at gain +3 + + // Now: assert valid_in AND frame_boundary on the SAME posedge. + // The sample is large enough to overflow at gain +3: 5000<<3 = 40000 > 32767 + @(negedge clk); + data_i_in = 16'sd5000; + data_q_in = 16'sd5000; + valid_in = 1'b1; + frame_boundary = 1'b1; + @(posedge clk); #1; // DUT samples both signals + @(negedge clk); + valid_in = 1'b0; + frame_boundary = 1'b0; + @(posedge clk); #1; // let NBA settle + @(posedge clk); #1; + + // Saturation count should be 1 (the coincident sample overflowed) + check(saturation_count == 8'd1, + "T18a.1: Coincident saturating sample counted in snapshot (sat_count=1)"); + + // Peak should reflect pre-gain max(|5000|,|5000|) = 5000 → [14:7] = 39 + // (or at least >= the first sample's peak of 100→[14:7]=0) + check(peak_magnitude == 8'd39, + "T18a.2: Coincident sample peak included in snapshot (peak=39)"); + + // AGC should have attacked (sat > 0): gain +3 → +3-1 = +2 + check(current_gain == 4'b0_010, + "T18a.3: AGC attacked on coincident saturation (gain +3 → +2)"); + + // ----- 18b: Coincident non-saturating peak updates snapshot ----- + $display(""); + $display("--- Test 18b: Coincident peak-only sample ---"); + + reset_n = 0; + agc_enable = 0; // deassert so transition fires with NEW gain_shift + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_000; // no amplification (shift 0) + agc_attack = 4'd1; + agc_decay = 4'd1; + agc_holdoff = 4'd0; + agc_target = 8'd200; // high target so signal is "weak" + agc_enable = 1; + @(posedge clk); @(posedge clk); @(posedge clk); #1; + + // Send a small sample + send_sample(16'sd50, 16'sd50); + + // Coincident frame_boundary + valid_in with a LARGER sample (not saturating) + @(negedge clk); + data_i_in = 16'sd10000; + data_q_in = 16'sd10000; + valid_in = 1'b1; + frame_boundary = 1'b1; + @(posedge clk); #1; + @(negedge clk); + valid_in = 1'b0; + frame_boundary = 1'b0; + @(posedge clk); #1; + @(posedge clk); #1; + + // Peak should be max(|10000|,|10000|) = 10000 → [14:7] = 78 + check(peak_magnitude == 8'd78, + "T18b.1: Coincident larger peak included (peak=78)"); + // No saturation at gain 0 + check(saturation_count == 8'd0, + "T18b.2: No saturation (gain=0, no overflow)"); + + // ================================================================= + // Test 19: AGC enable toggle mid-frame + // Verify gain initializes from gain_shift and holdoff resets + // ================================================================= + $display(""); + $display("--- Test 19: AGC enable toggle mid-frame ---"); + + // ----- 19a: Enable AGC mid-frame, verify gain init ----- + reset_n = 0; + repeat (2) @(posedge clk); + reset_n = 1; + repeat (2) @(posedge clk); + + gain_shift = 4'b0_101; // amplify x32 (shift left 5), internal = +5 + agc_attack = 4'd2; + agc_decay = 4'd1; + agc_holdoff = 4'd3; + agc_target = 8'd100; + agc_enable = 0; // start disabled + @(posedge clk); #1; + + // With AGC off, current_gain should follow gain_shift directly + check(current_gain == 4'b0_101, + "T19a.1: AGC disabled → current_gain = gain_shift (0x5)"); + + // Send a few samples (building up frame metrics) + send_sample(16'sd1000, 16'sd1000); + send_sample(16'sd2000, 16'sd2000); + + // Toggle AGC enable ON mid-frame + @(negedge clk); + agc_enable = 1; + @(posedge clk); #1; + @(posedge clk); #1; // let enable transition register + + // Gain should initialize from gain_shift encoding (0b0_101 → +5) + check(current_gain == 4'b0_101, + "T19a.2: AGC enabled mid-frame → gain initialized from gain_shift (+5)"); + + // Send a saturating sample, then boundary + send_sample(16'sd5000, 16'sd5000); // 5000<<5 overflows + @(negedge clk); frame_boundary = 1; @(posedge clk); #1; + @(negedge clk); frame_boundary = 0; @(posedge clk); #1; + @(posedge clk); #1; + + // AGC should attack: gain +5 → +5-2 = +3 + check(current_gain == 4'b0_011, + "T19a.3: After boundary, AGC attacked (gain +5 → +3)"); + + // ----- 19b: Disable AGC mid-frame, verify passthrough ----- + $display(""); + $display("--- Test 19b: Disable AGC mid-frame ---"); + + // Change gain_shift to a new value + @(negedge clk); + gain_shift = 4'b1_010; // attenuate by 2 (right shift 2) + agc_enable = 0; + @(posedge clk); #1; + @(posedge clk); #1; + + // With AGC off, current_gain should follow gain_shift + check(current_gain == 4'b1_010, + "T19b.1: AGC disabled → current_gain = gain_shift (0xA, atten 2)"); + + // Send sample: 1000 >> 2 = 250 + send_sample(16'sd1000, 16'sd0); + check(data_i_out == 16'sd250, + "T19b.2: Output uses host gain_shift when AGC off: 1000>>2=250"); + + // ----- 19c: Re-enable, verify gain re-initializes ----- + @(negedge clk); + gain_shift = 4'b0_010; // amplify by 4 (shift left 2), internal = +2 + agc_enable = 1; + @(posedge clk); #1; + @(posedge clk); #1; + + check(current_gain == 4'b0_010, + "T19c.1: AGC re-enabled → gain re-initialized from gain_shift (+2)"); // --------------------------------------------------------------- // SUMMARY diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v index 0318b7b..082c192 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v @@ -79,6 +79,12 @@ module tb_usb_data_interface; reg [7:0] status_self_test_detail; reg status_self_test_busy; + // AGC status readback inputs + reg [3:0] status_agc_current_gain; + reg [7:0] status_agc_peak_magnitude; + reg [7:0] status_agc_saturation_count; + reg status_agc_enable; + // ── Clock generators (asynchronous) ──────────────────────── always #(CLK_PERIOD / 2) clk = ~clk; always #(FT_CLK_PERIOD / 2) ft601_clk_in = ~ft601_clk_in; @@ -134,7 +140,13 @@ module tb_usb_data_interface; // Self-test status readback .status_self_test_flags (status_self_test_flags), .status_self_test_detail(status_self_test_detail), - .status_self_test_busy (status_self_test_busy) + .status_self_test_busy (status_self_test_busy), + + // AGC status readback + .status_agc_current_gain (status_agc_current_gain), + .status_agc_peak_magnitude (status_agc_peak_magnitude), + .status_agc_saturation_count(status_agc_saturation_count), + .status_agc_enable (status_agc_enable) ); // ── Test bookkeeping ─────────────────────────────────────── @@ -194,6 +206,10 @@ module tb_usb_data_interface; status_self_test_flags = 5'b00000; status_self_test_detail = 8'd0; status_self_test_busy = 1'b0; + status_agc_current_gain = 4'd0; + status_agc_peak_magnitude = 8'd0; + status_agc_saturation_count = 8'd0; + status_agc_enable = 1'b0; repeat (6) @(posedge ft601_clk_in); reset_n = 1; // Wait enough cycles for stream_control CDC to propagate @@ -902,6 +918,11 @@ module tb_usb_data_interface; status_self_test_flags = 5'b11111; status_self_test_detail = 8'hA5; status_self_test_busy = 1'b0; + // AGC status: gain=5, peak=180, sat_count=12, enabled + status_agc_current_gain = 4'd5; + status_agc_peak_magnitude = 8'd180; + status_agc_saturation_count = 8'd12; + status_agc_enable = 1'b1; // Pulse status_request (1 cycle in clk domain — toggles status_req_toggle_100m) @(posedge clk); @@ -958,8 +979,8 @@ module tb_usb_data_interface; "Status readback: word 2 = {guard, short_chirp}"); check(uut.status_words[3] === {16'd17450, 10'd0, 6'd32}, "Status readback: word 3 = {short_listen, 0, chirps_per_elev}"); - check(uut.status_words[4] === {30'd0, 2'b10}, - "Status readback: word 4 = range_mode=2'b10"); + check(uut.status_words[4] === {4'd5, 8'd180, 8'd12, 1'b1, 9'd0, 2'b10}, + "Status readback: word 4 = {agc_gain=5, peak=180, sat=12, en=1, range_mode=2}"); // status_words[5] = {7'd0, busy, 8'd0, detail[7:0], 3'd0, flags[4:0]} // = {7'd0, 1'b0, 8'd0, 8'hA5, 3'd0, 5'b11111} check(uut.status_words[5] === {7'd0, 1'b0, 8'd0, 8'hA5, 3'd0, 5'b11111}, diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 475ef2f..4f32e13 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -20,8 +20,8 @@ module usb_data_interface ( // Control signals output reg ft601_txe_n, // Transmit enable (active low) output reg ft601_rxf_n, // Receive enable (active low) - input wire ft601_txe, // Transmit FIFO empty - input wire ft601_rxf, // Receive FIFO full + input wire ft601_txe, // TXE: Transmit FIFO Not Full (high = space available to write) + input wire ft601_rxf, // RXF: Receive FIFO Not Empty (high = data available to read) output reg ft601_wr_n, // Write strobe (active low) output reg ft601_rd_n, // Read strobe (active low) output reg ft601_oe_n, // Output enable (active low) @@ -77,7 +77,13 @@ module usb_data_interface ( // Self-test status readback (opcode 0x31 / included in 0xFF status packet) input wire [4:0] status_self_test_flags, // Per-test PASS(1)/FAIL(0) latched input wire [7:0] status_self_test_detail, // Diagnostic detail byte latched - input wire status_self_test_busy // Self-test FSM still running + input wire status_self_test_busy, // Self-test FSM still running + + // AGC status readback + input wire [3:0] status_agc_current_gain, + input wire [7:0] status_agc_peak_magnitude, + input wire [7:0] status_agc_saturation_count, + input wire status_agc_enable ); // USB packet structure (same as before) @@ -258,18 +264,22 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin // Gap 2: Capture status snapshot when request arrives in ft601 domain if (status_req_ft601) begin // Pack register values into 5x 32-bit status words - // Word 0: {0xFF, mode[1:0], stream_ctrl[2:0], cfar_threshold[15:0]} - status_words[0] <= {8'hFF, 3'b000, status_radar_mode, - 5'b00000, status_stream_ctrl, - status_cfar_threshold}; + // Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} + status_words[0] <= {8'hFF, status_radar_mode, status_stream_ctrl, + 3'b000, status_cfar_threshold}; // Word 1: {long_chirp_cycles[15:0], long_listen_cycles[15:0]} status_words[1] <= {status_long_chirp, status_long_listen}; // Word 2: {guard_cycles[15:0], short_chirp_cycles[15:0]} status_words[2] <= {status_guard, status_short_chirp}; // Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0} status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; - // Word 4: Fix 7 — range_mode in bits [1:0], rest reserved - status_words[4] <= {30'd0, status_range_mode}; + // Word 4: AGC metrics + range_mode + status_words[4] <= {status_agc_current_gain, // [31:28] + status_agc_peak_magnitude, // [27:20] + status_agc_saturation_count, // [19:12] + status_agc_enable, // [11] + 9'd0, // [10:2] reserved + status_range_mode}; // [1:0] // Word 5: Self-test results {reserved[6:0], busy, reserved[7:0], detail[7:0], reserved[2:0], flags[4:0]} status_words[5] <= {7'd0, status_self_test_busy, 8'd0, status_self_test_detail, diff --git a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v index 8d0671b..44cca42 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -90,7 +90,13 @@ module usb_data_interface_ft2232h ( // Self-test status readback input wire [4:0] status_self_test_flags, input wire [7:0] status_self_test_detail, - input wire status_self_test_busy + input wire status_self_test_busy, + + // AGC status readback + input wire [3:0] status_agc_current_gain, + input wire [7:0] status_agc_peak_magnitude, + input wire [7:0] status_agc_saturation_count, + input wire status_agc_enable ); // ============================================================================ @@ -275,13 +281,18 @@ always @(posedge ft_clk or negedge ft_reset_n) begin // Status snapshot on request if (status_req_ft) begin - status_words[0] <= {8'hFF, 3'b000, status_radar_mode, - 5'b00000, status_stream_ctrl, - status_cfar_threshold}; + // Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} + status_words[0] <= {8'hFF, status_radar_mode, status_stream_ctrl, + 3'b000, status_cfar_threshold}; status_words[1] <= {status_long_chirp, status_long_listen}; status_words[2] <= {status_guard, status_short_chirp}; status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; - status_words[4] <= {30'd0, status_range_mode}; + status_words[4] <= {status_agc_current_gain, // [31:28] + status_agc_peak_magnitude, // [27:20] + status_agc_saturation_count, // [19:12] + status_agc_enable, // [11] + 9'd0, // [10:2] reserved + status_range_mode}; // [1:0] status_words[5] <= {7'd0, status_self_test_busy, 8'd0, status_self_test_detail, 3'd0, status_self_test_flags}; 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/adi_agc_analysis.py b/9_Firmware/9_3_GUI/adi_agc_analysis.py new file mode 100644 index 0000000..8ebc0b9 --- /dev/null +++ b/9_Firmware/9_3_GUI/adi_agc_analysis.py @@ -0,0 +1,431 @@ +# ruff: noqa: T201 +#!/usr/bin/env python3 +""" +One-off AGC saturation analysis for ADI CN0566 raw IQ captures. + +Bit-accurate simulation of rx_gain_control.v AGC inner loop applied +to real captured IQ data. Three scenarios per dataset: + + Row 1 — AGC OFF: Fixed gain_shift=0 (pass-through). Shows raw clipping. + Row 2 — AGC ON: Auto-adjusts from gain_shift=0. Clipping clears. + Row 3 — AGC delayed: OFF for first half, ON at midpoint. + Shows the transition: clipping → AGC activates → clears. + +Key RTL details modelled exactly: + - gain_shift[3]=direction (0=amplify/left, 1=attenuate/right), [2:0]=amount + - Internal agc_gain is signed -7..+7 + - Peak is measured PRE-gain (raw input |sample|, upper 8 of 15 bits) + - Saturation is measured POST-gain (overflow from shift) + - Attack: gain -= agc_attack when any sample clips (immediate) + - Decay: gain += agc_decay when peak < target AND holdoff expired + - Hold: when peak >= target AND no saturation, hold gain, reset holdoff + +Usage: + python adi_agc_analysis.py + python adi_agc_analysis.py --data /path/to/file.npy --label "my capture" +""" + +import argparse +import sys +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +# --------------------------------------------------------------------------- +# FPGA AGC parameters (rx_gain_control.v reset defaults) +# --------------------------------------------------------------------------- +AGC_TARGET = 200 # host_agc_target (8-bit, default 200) +AGC_ATTACK = 1 # host_agc_attack (4-bit, default 1) +AGC_DECAY = 1 # host_agc_decay (4-bit, default 1) +AGC_HOLDOFF = 4 # host_agc_holdoff (4-bit, default 4) +ADC_RAIL = 4095 # 12-bit ADC max absolute value + + +# --------------------------------------------------------------------------- +# Gain encoding helpers (match RTL signed_to_encoding / encoding_to_signed) +# --------------------------------------------------------------------------- + +def signed_to_encoding(g: int) -> int: + """Convert signed gain (-7..+7) to gain_shift[3:0] encoding. + [3]=0, [2:0]=N → amplify (left shift) by N + [3]=1, [2:0]=N → attenuate (right shift) by N + """ + if g >= 0: + return g & 0x07 + return 0x08 | ((-g) & 0x07) + + +def encoding_to_signed(enc: int) -> int: + """Convert gain_shift[3:0] encoding to signed gain.""" + if (enc & 0x08) == 0: + return enc & 0x07 + return -(enc & 0x07) + + +def clamp_gain(val: int) -> int: + """Clamp to [-7, +7] (matches RTL clamp_gain function).""" + return max(-7, min(7, val)) + + +# --------------------------------------------------------------------------- +# Apply gain shift to IQ data (matches RTL combinational logic) +# --------------------------------------------------------------------------- + +def apply_gain_shift(frame_i: np.ndarray, frame_q: np.ndarray, + gain_enc: int) -> tuple[np.ndarray, np.ndarray, int]: + """Apply gain_shift encoding to 16-bit signed IQ arrays. + + Returns (shifted_i, shifted_q, overflow_count). + Matches the RTL: left shift = amplify, right shift = attenuate, + saturate to ±32767 on overflow. + """ + direction = (gain_enc >> 3) & 1 # 0=amplify, 1=attenuate + amount = gain_enc & 0x07 + + if amount == 0: + return frame_i.copy(), frame_q.copy(), 0 + + if direction == 0: + # Left shift (amplify) + si = frame_i.astype(np.int64) * (1 << amount) + sq = frame_q.astype(np.int64) * (1 << amount) + else: + # Arithmetic right shift (attenuate) + si = frame_i.astype(np.int64) >> amount + sq = frame_q.astype(np.int64) >> amount + + # Count overflows (post-shift values outside 16-bit signed range) + overflow_i = (si > 32767) | (si < -32768) + overflow_q = (sq > 32767) | (sq < -32768) + overflow_count = int((overflow_i | overflow_q).sum()) + + # Saturate to ±32767 + si = np.clip(si, -32768, 32767).astype(np.int16) + sq = np.clip(sq, -32768, 32767).astype(np.int16) + + return si, sq, overflow_count + + +# --------------------------------------------------------------------------- +# Per-frame AGC simulation (bit-accurate to rx_gain_control.v) +# --------------------------------------------------------------------------- + +def simulate_agc(frames: np.ndarray, agc_enabled: bool = True, + enable_at_frame: int = 0, + initial_gain_enc: int = 0x00) -> dict: + """Simulate FPGA inner-loop AGC across all frames. + + Parameters + ---------- + frames : (N, chirps, samples) complex — raw ADC captures (12-bit range) + agc_enabled : if False, gain stays fixed + enable_at_frame : frame index where AGC activates + initial_gain_enc : gain_shift[3:0] encoding when AGC enables (default 0x00 = pass-through) + """ + n_frames = frames.shape[0] + + # Output arrays + out_gain_enc = np.zeros(n_frames, dtype=int) # gain_shift encoding [3:0] + out_gain_signed = np.zeros(n_frames, dtype=int) # signed gain for plotting + out_peak_mag = np.zeros(n_frames, dtype=int) # peak_magnitude[7:0] + out_sat_count = np.zeros(n_frames, dtype=int) # saturation_count[7:0] + out_sat_rate = np.zeros(n_frames, dtype=float) + out_rms_post = np.zeros(n_frames, dtype=float) # RMS after gain shift + + # AGC internal state + agc_gain = 0 # signed, -7..+7 + holdoff_counter = 0 + agc_was_enabled = False + + for i in range(n_frames): + frame = frames[i] + # Quantize to 16-bit signed (ADC is 12-bit, sign-extended to 16) + frame_i = np.clip(np.round(frame.real), -32768, 32767).astype(np.int16) + frame_q = np.clip(np.round(frame.imag), -32768, 32767).astype(np.int16) + + # --- PRE-gain peak measurement (RTL lines 133-135, 211-213) --- + abs_i = np.abs(frame_i.astype(np.int32)) + abs_q = np.abs(frame_q.astype(np.int32)) + max_iq = np.maximum(abs_i, abs_q) + frame_peak_15bit = int(max_iq.max()) # 15-bit unsigned + peak_8bit = (frame_peak_15bit >> 7) & 0xFF # Upper 8 bits + + # --- Determine effective gain --- + agc_active = agc_enabled and (i >= enable_at_frame) + + # AGC enable transition (RTL lines 250-253) + if agc_active and not agc_was_enabled: + agc_gain = encoding_to_signed(initial_gain_enc) + holdoff_counter = AGC_HOLDOFF + + effective_enc = signed_to_encoding(agc_gain) if agc_active else initial_gain_enc + + agc_was_enabled = agc_active + + # --- Apply gain shift + count POST-gain overflow (RTL lines 114-126, 207-209) --- + shifted_i, shifted_q, frame_overflow = apply_gain_shift( + frame_i, frame_q, effective_enc) + frame_sat = min(255, frame_overflow) + + # RMS of shifted signal + rms = float(np.sqrt(np.mean( + shifted_i.astype(np.float64)**2 + shifted_q.astype(np.float64)**2))) + + total_samples = frame_i.size + frame_q.size + sat_rate = frame_overflow / total_samples if total_samples > 0 else 0.0 + + # --- Record outputs --- + out_gain_enc[i] = effective_enc + out_gain_signed[i] = agc_gain if agc_active else encoding_to_signed(initial_gain_enc) + out_peak_mag[i] = peak_8bit + out_sat_count[i] = frame_sat + out_sat_rate[i] = sat_rate + out_rms_post[i] = rms + + # --- AGC update at frame boundary (RTL lines 226-246) --- + if agc_active: + if frame_sat > 0: + # Clipping: reduce gain immediately (attack) + agc_gain = clamp_gain(agc_gain - AGC_ATTACK) + holdoff_counter = AGC_HOLDOFF + elif peak_8bit < AGC_TARGET: + # Signal too weak: increase gain after holdoff + if holdoff_counter == 0: + agc_gain = clamp_gain(agc_gain + AGC_DECAY) + else: + holdoff_counter -= 1 + else: + # Good range (peak >= target, no sat): hold, reset holdoff + holdoff_counter = AGC_HOLDOFF + + return { + "gain_enc": out_gain_enc, + "gain_signed": out_gain_signed, + "peak_mag": out_peak_mag, + "sat_count": out_sat_count, + "sat_rate": out_sat_rate, + "rms_post": out_rms_post, + } + + +# --------------------------------------------------------------------------- +# Range-Doppler processing for heatmap display +# --------------------------------------------------------------------------- + +def process_frame_rd(frame: np.ndarray, gain_enc: int, + n_range: int = 64, + n_doppler: int = 32) -> np.ndarray: + """Range-Doppler magnitude for one frame with gain applied.""" + frame_i = np.clip(np.round(frame.real), -32768, 32767).astype(np.int16) + frame_q = np.clip(np.round(frame.imag), -32768, 32767).astype(np.int16) + si, sq, _ = apply_gain_shift(frame_i, frame_q, gain_enc) + + iq = si.astype(np.float64) + 1j * sq.astype(np.float64) + n_chirps, _ = iq.shape + + range_fft = np.fft.fft(iq, axis=1)[:, :n_range] + doppler_fft = np.fft.fftshift(np.fft.fft(range_fft, axis=0), axes=0) + center = n_chirps // 2 + half_d = n_doppler // 2 + doppler_fft = doppler_fft[center - half_d:center + half_d, :] + + rd_mag = np.abs(doppler_fft.real) + np.abs(doppler_fft.imag) + return rd_mag.T # (n_range, n_doppler) + + +# --------------------------------------------------------------------------- +# Plotting +# --------------------------------------------------------------------------- + +def plot_scenario(axes, data: np.ndarray, agc: dict, title: str, + enable_frame: int = 0): + """Plot one AGC scenario across 5 axes.""" + n = data.shape[0] + xs = np.arange(n) + + # Range-Doppler heatmap + if enable_frame > 0 and enable_frame < n: + f_before = max(0, enable_frame - 1) + f_after = min(n - 1, n - 2) + rd_before = process_frame_rd(data[f_before], int(agc["gain_enc"][f_before])) + rd_after = process_frame_rd(data[f_after], int(agc["gain_enc"][f_after])) + combined = np.hstack([rd_before, rd_after]) + im = axes[0].imshow( + 20 * np.log10(combined + 1), aspect="auto", origin="lower", + cmap="inferno", interpolation="nearest") + axes[0].axvline(x=rd_before.shape[1] - 0.5, color="cyan", + linewidth=2, linestyle="--") + axes[0].set_title(f"{title}\nL: f{f_before} (pre) | R: f{f_after} (post)") + else: + worst = int(np.argmax(agc["sat_count"])) + best = int(np.argmin(agc["sat_count"])) + f_show = worst if agc["sat_count"][worst] > 0 else best + rd = process_frame_rd(data[f_show], int(agc["gain_enc"][f_show])) + im = axes[0].imshow( + 20 * np.log10(rd + 1), aspect="auto", origin="lower", + cmap="inferno", interpolation="nearest") + axes[0].set_title(f"{title}\nFrame {f_show}") + + axes[0].set_xlabel("Doppler bin") + axes[0].set_ylabel("Range bin") + plt.colorbar(im, ax=axes[0], label="dB", shrink=0.8) + + # Signed gain history (the real AGC state) + axes[1].plot(xs, agc["gain_signed"], color="#00ff88", linewidth=1.5) + axes[1].axhline(y=0, color="gray", linestyle=":", alpha=0.5, + label="Pass-through") + if enable_frame > 0: + axes[1].axvline(x=enable_frame, color="yellow", linewidth=2, + linestyle="--", label="AGC ON") + axes[1].set_ylim(-8, 8) + axes[1].set_ylabel("Gain (signed)") + axes[1].set_title("AGC Internal Gain (-7=max atten, +7=max amp)") + axes[1].legend(fontsize=7, loc="upper right") + axes[1].grid(True, alpha=0.3) + + # Peak magnitude (PRE-gain, 8-bit) + axes[2].plot(xs, agc["peak_mag"], color="#ffaa00", linewidth=1.0) + axes[2].axhline(y=AGC_TARGET, color="cyan", linestyle="--", + alpha=0.7, label=f"Target ({AGC_TARGET})") + axes[2].axhspan(240, 255, color="red", alpha=0.15, label="Clip zone") + if enable_frame > 0: + axes[2].axvline(x=enable_frame, color="yellow", linewidth=2, + linestyle="--", alpha=0.8) + axes[2].set_ylim(0, 260) + axes[2].set_ylabel("Peak (8-bit)") + axes[2].set_title("Peak Magnitude (pre-gain, raw input)") + axes[2].legend(fontsize=7, loc="upper right") + axes[2].grid(True, alpha=0.3) + + # Saturation count (POST-gain overflow) + axes[3].fill_between(xs, agc["sat_count"], color="red", alpha=0.4) + axes[3].plot(xs, agc["sat_count"], color="red", linewidth=0.8) + if enable_frame > 0: + axes[3].axvline(x=enable_frame, color="yellow", linewidth=2, + linestyle="--", alpha=0.8) + axes[3].set_ylabel("Overflow Count") + total = int(agc["sat_count"].sum()) + axes[3].set_title(f"Post-Gain Overflow (total={total})") + axes[3].grid(True, alpha=0.3) + + # RMS signal level (post-gain) + axes[4].plot(xs, agc["rms_post"], color="#44aaff", linewidth=1.0) + if enable_frame > 0: + axes[4].axvline(x=enable_frame, color="yellow", linewidth=2, + linestyle="--", alpha=0.8) + axes[4].set_ylabel("RMS") + axes[4].set_xlabel("Frame") + axes[4].set_title("Post-Gain RMS Level") + axes[4].grid(True, alpha=0.3) + + +def analyze_dataset(data: np.ndarray, label: str): + """Run 3-scenario analysis for one dataset.""" + n_frames = data.shape[0] + mid = n_frames // 2 + + print(f"\n{'='*60}") + print(f" {label} — shape {data.shape}") + print(f"{'='*60}") + + # Raw ADC stats + raw_sat = np.sum((np.abs(data.real) >= ADC_RAIL) | + (np.abs(data.imag) >= ADC_RAIL)) + print(f" Raw ADC saturation: {raw_sat} samples " + f"({100*raw_sat/(2*data.size):.2f}%)") + + # Scenario 1: AGC OFF — pass-through (gain_shift=0x00) + print(" [1/3] AGC OFF (gain=0, pass-through) ...") + agc_off = simulate_agc(data, agc_enabled=False, initial_gain_enc=0x00) + print(f" Post-gain overflow: {agc_off['sat_count'].sum()} " + f"(should be 0 — no amplification)") + + # Scenario 2: AGC ON from frame 0 + print(" [2/3] AGC ON (from start) ...") + agc_on = simulate_agc(data, agc_enabled=True, enable_at_frame=0, + initial_gain_enc=0x00) + print(f" Final gain: {agc_on['gain_signed'][-1]} " + f"(enc=0x{agc_on['gain_enc'][-1]:X})") + print(f" Post-gain overflow: {agc_on['sat_count'].sum()}") + + # Scenario 3: AGC delayed + print(f" [3/3] AGC delayed (ON at frame {mid}) ...") + agc_delayed = simulate_agc(data, agc_enabled=True, + enable_at_frame=mid, + initial_gain_enc=0x00) + pre_sat = int(agc_delayed["sat_count"][:mid].sum()) + post_sat = int(agc_delayed["sat_count"][mid:].sum()) + print(f" Pre-AGC overflow: {pre_sat} " + f"Post-AGC overflow: {post_sat}") + + # Plot + fig, axes = plt.subplots(3, 5, figsize=(28, 14)) + fig.suptitle(f"AERIS-10 AGC Analysis — {label}\n" + f"({n_frames} frames, {data.shape[1]} chirps, " + f"{data.shape[2]} samples/chirp, " + f"raw ADC sat={100*raw_sat/(2*data.size):.2f}%)", + fontsize=13, fontweight="bold", y=0.99) + + plot_scenario(axes[0], data, agc_off, "AGC OFF (pass-through)") + plot_scenario(axes[1], data, agc_on, "AGC ON (from start)") + plot_scenario(axes[2], data, agc_delayed, + f"AGC delayed (ON at frame {mid})", enable_frame=mid) + + for ax, lbl in zip(axes[:, 0], + ["AGC OFF", "AGC ON", "AGC DELAYED"], + strict=True): + ax.annotate(lbl, xy=(-0.35, 0.5), xycoords="axes fraction", + fontsize=13, fontweight="bold", color="white", + ha="center", va="center", rotation=90) + + plt.tight_layout(rect=[0.03, 0, 1, 0.95]) + return fig + + +def main(): + parser = argparse.ArgumentParser( + description="AGC analysis for ADI raw IQ captures " + "(bit-accurate rx_gain_control.v simulation)") + parser.add_argument("--amp", type=str, + default=str(Path.home() / "Downloads/adi_radar_data" + "/amp_radar" + "/phaser_amp_4MSPS_500M_300u_256_m3dB.npy"), + help="Path to amplified radar .npy") + parser.add_argument("--noamp", type=str, + default=str(Path.home() / "Downloads/adi_radar_data" + "/no_amp_radar" + "/phaser_NOamp_4MSPS_500M_300u_256.npy"), + help="Path to non-amplified radar .npy") + parser.add_argument("--data", type=str, default=None, + help="Single dataset mode") + parser.add_argument("--label", type=str, default="Custom Data") + args = parser.parse_args() + + plt.style.use("dark_background") + + if args.data: + data = np.load(args.data) + analyze_dataset(data, args.label) + plt.show() + return + + figs = [] + for path, label in [(args.amp, "With Amplifier (-3 dB)"), + (args.noamp, "No Amplifier")]: + if not Path(path).exists(): + print(f"WARNING: {path} not found, skipping") + continue + data = np.load(path) + fig = analyze_dataset(data, label) + figs.append(fig) + + if not figs: + print("No data found. Use --amp/--noamp or --data.") + sys.exit(1) + + plt.show() + + +if __name__ == "__main__": + main() diff --git a/9_Firmware/9_3_GUI/radar_dashboard.py b/9_Firmware/9_3_GUI/radar_dashboard.py index aa7d81d..7575e63 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,24 @@ 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 + + # Thread-safe UI message queue — avoids calling root.after() from + # background threads which crashes Python 3.12 (GIL state corruption). + # Entries are (tag, payload) tuples drained by _schedule_update(). + self._ui_queue: queue.Queue[tuple[str, object]] = queue.Queue() # Display state self._current_frame = RadarFrame() @@ -109,6 +116,16 @@ class RadarDashboard: self._vmax_ema = 1000.0 self._vmax_alpha = 0.15 # smoothing factor (lower = more stable) + # AGC visualization history (ring buffers, ~60s at 10 Hz) + self._agc_history_len = 256 + self._agc_gain_history: deque[int] = deque(maxlen=self._agc_history_len) + self._agc_peak_history: deque[int] = deque(maxlen=self._agc_history_len) + self._agc_sat_history: deque[int] = deque(maxlen=self._agc_history_len) + self._agc_time_history: deque[float] = deque(maxlen=self._agc_history_len) + self._agc_t0: float = time.time() + self._agc_last_redraw: float = 0.0 # throttle chart redraws + self._AGC_REDRAW_INTERVAL: float = 0.5 # seconds between redraws + self._build_ui() self._schedule_update() @@ -154,28 +171,30 @@ 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) tab_display = ttk.Frame(nb) tab_control = ttk.Frame(nb) + tab_agc = ttk.Frame(nb) tab_log = ttk.Frame(nb) nb.add(tab_display, text=" Display ") nb.add(tab_control, text=" Control ") + nb.add(tab_agc, text=" AGC Monitor ") nb.add(tab_log, text=" Log ") self._build_display_tab(tab_display) self._build_control_tab(tab_control) + self._build_agc_tab(tab_agc) self._build_log_tab(tab_log) def _build_display_tab(self, parent): # 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 +251,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,66 +352,238 @@ 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)) + + # ── AGC (Automatic Gain Control) ────────────────────────────── + grp_agc = ttk.LabelFrame(right, text="AGC (Auto Gain)", padding=10) + grp_agc.pack(fill="x", pady=(0, 8)) + + agc_params = [ + ("AGC Enable", 0x28, "0", 1, "0=manual, 1=auto"), + ("AGC Target", 0x29, "200", 8, "0-255, peak target"), + ("AGC Attack", 0x2A, "1", 4, "0-15, atten step"), + ("AGC Decay", 0x2B, "1", 4, "0-15, gain-up step"), + ("AGC Holdoff", 0x2C, "4", 4, "0-15, frames"), + ] + for label, opcode, default, bits, hint in agc_params: + self._add_param_row(grp_agc, label, opcode, default, bits, hint) + + # AGC quick toggle + agc_row = ttk.Frame(grp_agc) + agc_row.pack(fill="x", pady=2) + ttk.Button(agc_row, text="Enable AGC", + command=lambda: self._send_cmd(0x28, 1)).pack( + side="left", expand=True, fill="x", padx=(0, 2)) + ttk.Button(agc_row, text="Disable AGC", + command=lambda: self._send_cmd(0x28, 0)).pack( + side="left", expand=True, fill="x", padx=(2, 0)) + + # AGC status readback labels + agc_st = ttk.LabelFrame(grp_agc, text="AGC Status", padding=6) + agc_st.pack(fill="x", pady=(4, 0)) + self._agc_labels = {} + for name, default_text in [ + ("enable", "AGC: --"), + ("gain", "Gain: --"), + ("peak", "Peak: --"), + ("sat", "Sat Count: --"), + ]: + lbl = ttk.Label(agc_st, text=default_text, font=("Menlo", 9)) + lbl.pack(anchor="w") + self._agc_labels[name] = lbl + + # ── 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_agc_tab(self, parent): + """AGC Monitor tab — real-time strip charts for gain, peak, and saturation.""" + # Top row: AGC status badge + saturation indicator + top = ttk.Frame(parent) + top.pack(fill="x", padx=8, pady=(8, 0)) + + self._agc_badge = ttk.Label( + top, text="AGC: --", font=("Menlo", 14, "bold"), foreground=FG) + self._agc_badge.pack(side="left", padx=(0, 24)) + + self._agc_sat_badge = ttk.Label( + top, text="Saturation: 0", font=("Menlo", 12), foreground=GREEN) + self._agc_sat_badge.pack(side="left", padx=(0, 24)) + + self._agc_gain_value = ttk.Label( + top, text="Gain: --", font=("Menlo", 12), foreground=ACCENT) + self._agc_gain_value.pack(side="left", padx=(0, 24)) + + self._agc_peak_value = ttk.Label( + top, text="Peak: --", font=("Menlo", 12), foreground=ACCENT) + self._agc_peak_value.pack(side="left") + + # Matplotlib figure with 3 stacked subplots sharing x-axis (time) + self._agc_fig = Figure(figsize=(14, 7), facecolor=BG) + self._agc_fig.subplots_adjust( + left=0.07, right=0.98, top=0.95, bottom=0.08, + hspace=0.30) + + # Subplot 1: FPGA inner-loop gain (4-bit, 0-15) + self._ax_gain = self._agc_fig.add_subplot(3, 1, 1) + self._ax_gain.set_facecolor(BG2) + self._ax_gain.set_title("FPGA AGC Gain (inner loop)", color=FG, fontsize=10) + self._ax_gain.set_ylabel("Gain Level", color=FG) + self._ax_gain.set_ylim(-0.5, 15.5) + self._ax_gain.tick_params(colors=FG) + self._ax_gain.set_xlim(0, self._agc_history_len) + self._gain_line, = self._ax_gain.plot( + [], [], color=ACCENT, linewidth=1.5, label="Gain") + self._ax_gain.axhline(y=0, color=RED, linewidth=0.5, alpha=0.5, linestyle="--") + self._ax_gain.axhline(y=15, color=RED, linewidth=0.5, alpha=0.5, linestyle="--") + for spine in self._ax_gain.spines.values(): + spine.set_color(SURFACE) + + # Subplot 2: Peak magnitude (8-bit, 0-255) + self._ax_peak = self._agc_fig.add_subplot(3, 1, 2) + self._ax_peak.set_facecolor(BG2) + self._ax_peak.set_title("Peak Magnitude", color=FG, fontsize=10) + self._ax_peak.set_ylabel("Peak (8-bit)", color=FG) + self._ax_peak.set_ylim(-5, 260) + self._ax_peak.tick_params(colors=FG) + self._ax_peak.set_xlim(0, self._agc_history_len) + self._peak_line, = self._ax_peak.plot( + [], [], color=YELLOW, linewidth=1.5, label="Peak") + # AGC target reference line (default 200) + self._agc_target_line = self._ax_peak.axhline( + y=200, color=GREEN, linewidth=1.0, alpha=0.7, linestyle="--", + label="Target (200)") + self._ax_peak.legend(loc="upper right", fontsize=8, + facecolor=BG2, edgecolor=SURFACE, + labelcolor=FG) + for spine in self._ax_peak.spines.values(): + spine.set_color(SURFACE) + + # Subplot 3: Saturation count (8-bit, 0-255) as bar-style fill + self._ax_sat = self._agc_fig.add_subplot(3, 1, 3) + self._ax_sat.set_facecolor(BG2) + self._ax_sat.set_title("Saturation Count", color=FG, fontsize=10) + self._ax_sat.set_ylabel("Sat Count", color=FG) + self._ax_sat.set_xlabel("Sample Index", color=FG) + self._ax_sat.set_ylim(-1, 40) + self._ax_sat.tick_params(colors=FG) + self._ax_sat.set_xlim(0, self._agc_history_len) + self._sat_fill = self._ax_sat.fill_between( + [], [], color=RED, alpha=0.6, label="Saturation") + self._sat_line, = self._ax_sat.plot( + [], [], color=RED, linewidth=1.0) + self._ax_sat.axhline(y=0, color=GREEN, linewidth=0.5, alpha=0.5, linestyle="--") + for spine in self._ax_sat.spines.values(): + spine.set_color(SURFACE) + + agc_canvas = FigureCanvasTkAgg(self._agc_fig, master=parent) + agc_canvas.draw() + agc_canvas.get_tk_widget().pack(fill="both", expand=True) + self._agc_canvas = agc_canvas + def _build_log_tab(self, parent): self.log_text = tk.Text(parent, bg=BG2, fg=FG, font=("Menlo", 10), insertbackground=FG, wrap="word") self.log_text.pack(fill="both", expand=True, padx=8, pady=8) - # Redirect log handler to text widget - handler = _TextHandler(self.log_text) + # Redirect log handler to text widget (via UI queue for thread safety) + handler = _TextHandler(self._ui_queue) handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")) logging.getLogger().addHandler(handler) @@ -364,9 +608,9 @@ class RadarDashboard: self.root.update_idletasks() def _do_connect(): - ok = self.conn.open() - # Schedule UI update back on the main thread - self.root.after(0, lambda: self._on_connect_done(ok)) + ok = self.conn.open(self.device_index) + # Post result to UI queue (drained by _schedule_update) + self._ui_queue.put(("connect", ok)) threading.Thread(target=_do_connect, daemon=True).start() @@ -414,11 +658,11 @@ class RadarDashboard: log.error("Invalid custom command values") def _on_status_received(self, status: StatusResponse): - """Called from acquisition thread — schedule UI update on main thread.""" - self.root.after(0, self._update_self_test_labels, status) + """Called from acquisition thread — post to UI queue for main thread.""" + self._ui_queue.put(("status", status)) def _update_self_test_labels(self, status: StatusResponse): - """Update the self-test result labels from a StatusResponse.""" + """Update the self-test result labels and AGC status from a StatusResponse.""" if not hasattr(self, '_st_labels'): return flags = status.self_test_flags @@ -453,11 +697,124 @@ class RadarDashboard: self._st_labels[key].config( text=f"{name}: {result_str}", foreground=color) + # AGC status readback + if hasattr(self, '_agc_labels'): + agc_str = "AUTO" if status.agc_enable else "MANUAL" + agc_color = GREEN if status.agc_enable else FG + self._agc_labels["enable"].config( + text=f"AGC: {agc_str}", foreground=agc_color) + self._agc_labels["gain"].config( + text=f"Gain: {status.agc_current_gain}") + self._agc_labels["peak"].config( + text=f"Peak: {status.agc_peak_magnitude}") + sat_color = RED if status.agc_saturation_count > 0 else FG + self._agc_labels["sat"].config( + text=f"Sat Count: {status.agc_saturation_count}", + foreground=sat_color) + + # AGC visualization update + self._update_agc_visualization(status) + + def _update_agc_visualization(self, status: StatusResponse): + """Push AGC metrics into ring buffers and redraw strip charts. + + Data is always accumulated (cheap), but matplotlib redraws are + throttled to ``_AGC_REDRAW_INTERVAL`` seconds to avoid saturating + the GUI event-loop when status packets arrive at 20 Hz. + """ + if not hasattr(self, '_agc_canvas'): + return + + # Append to ring buffers (always — this is O(1)) + self._agc_gain_history.append(status.agc_current_gain) + self._agc_peak_history.append(status.agc_peak_magnitude) + self._agc_sat_history.append(status.agc_saturation_count) + + # Update indicator labels (cheap Tk config calls) + mode_str = "AUTO" if status.agc_enable else "MANUAL" + mode_color = GREEN if status.agc_enable else FG + self._agc_badge.config(text=f"AGC: {mode_str}", foreground=mode_color) + self._agc_gain_value.config( + text=f"Gain: {status.agc_current_gain}") + self._agc_peak_value.config( + text=f"Peak: {status.agc_peak_magnitude}") + + total_sat = sum(self._agc_sat_history) + if total_sat > 10: + sat_color = RED + elif total_sat > 0: + sat_color = YELLOW + else: + sat_color = GREEN + self._agc_sat_badge.config( + text=f"Saturation: {total_sat}", foreground=sat_color) + + # ---- Throttle matplotlib redraws --------------------------------- + now = time.monotonic() + if now - self._agc_last_redraw < self._AGC_REDRAW_INTERVAL: + return + self._agc_last_redraw = now + + n = len(self._agc_gain_history) + xs = list(range(n)) + + # Update line plots + gain_data = list(self._agc_gain_history) + peak_data = list(self._agc_peak_history) + sat_data = list(self._agc_sat_history) + + self._gain_line.set_data(xs, gain_data) + self._peak_line.set_data(xs, peak_data) + + # Saturation: redraw as filled area + self._sat_line.set_data(xs, sat_data) + if self._sat_fill is not None: + self._sat_fill.remove() + self._sat_fill = self._ax_sat.fill_between( + xs, sat_data, color=RED, alpha=0.4) + + # Auto-scale saturation Y axis to data + max_sat = max(sat_data) if sat_data else 0 + self._ax_sat.set_ylim(-1, max(max_sat * 1.5, 5)) + + # Scroll X axis to keep latest data visible + if n >= self._agc_history_len: + self._ax_gain.set_xlim(0, n) + self._ax_peak.set_xlim(0, n) + self._ax_sat.set_xlim(0, n) + + self._agc_canvas.draw_idle() + # --------------------------------------------------------- Display loop def _schedule_update(self): + self._drain_ui_queue() self._update_display() self.root.after(self.UPDATE_INTERVAL_MS, self._schedule_update) + def _drain_ui_queue(self): + """Process all pending cross-thread messages on the main thread.""" + while True: + try: + tag, payload = self._ui_queue.get_nowait() + except queue.Empty: + break + if tag == "connect": + self._on_connect_done(payload) + elif tag == "status": + self._update_self_test_labels(payload) + elif tag == "log": + self._log_handler_append(payload) + + def _log_handler_append(self, msg: str): + """Append a log message to the log Text widget (main thread only).""" + with contextlib.suppress(Exception): + self.log_text.insert("end", msg + "\n") + self.log_text.see("end") + # Keep last 500 lines + lines = int(self.log_text.index("end-1c").split(".")[0]) + if lines > 500: + self.log_text.delete("1.0", f"{lines - 500}.0") + def _update_display(self): """Pull latest frame from queue and update plots.""" frame = None @@ -522,26 +879,21 @@ class RadarDashboard: class _TextHandler(logging.Handler): - """Logging handler that writes to a tkinter Text widget.""" + """Logging handler that posts messages to a queue for main-thread append. - def __init__(self, text_widget: tk.Text): + Using widget.after() from background threads crashes Python 3.12 due to + GIL state corruption. Instead we post to the dashboard's _ui_queue and + let _drain_ui_queue() append on the main thread. + """ + + def __init__(self, ui_queue: queue.Queue[tuple[str, object]]): super().__init__() - self._text = text_widget + self._ui_queue = ui_queue def emit(self, record): msg = self.format(record) - try: - self._text.after(0, self._append, msg) - except Exception: - pass - - def _append(self, msg: str): - self._text.insert("end", msg + "\n") - self._text.see("end") - # Keep last 500 lines - lines = int(self._text.index("end-1c").split(".")[0]) - if lines > 500: - self._text.delete("1.0", f"{lines - 500}.0") + with contextlib.suppress(Exception): + self._ui_queue.put(("log", msg)) # ============================================================================ @@ -578,7 +930,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..c266b6d 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 0x28-0x2C AGC control + 0x12 host_guard_cycles 0x30 host_self_test_trigger + 0x13 host_short_chirp_cycles 0x31/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,15 @@ class Opcode(IntEnum): CFAR_ENABLE = 0x25 MTI_ENABLE = 0x26 DC_NOTCH_WIDTH = 0x27 + + # --- AGC (0x28-0x2C) --- + AGC_ENABLE = 0x28 + AGC_TARGET = 0x29 + AGC_ATTACK = 0x2A + AGC_DECAY = 0x2B + AGC_HOLDOFF = 0x2C + + # --- Board self-test / status (0x30-0x31, 0xFF) --- SELF_TEST_TRIGGER = 0x30 SELF_TEST_STATUS = 0x31 STATUS_REQUEST = 0xFF @@ -83,7 +109,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 +127,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 @@ -116,6 +142,11 @@ class StatusResponse: self_test_flags: int = 0 # 5-bit result flags [4:0] self_test_detail: int = 0 # 8-bit detail code [7:0] self_test_busy: int = 0 # 1-bit busy flag + # AGC metrics (word 4, added for hybrid AGC) + agc_current_gain: int = 0 # 4-bit current gain encoding [3:0] + agc_peak_magnitude: int = 0 # 8-bit peak magnitude [7:0] + agc_saturation_count: int = 0 # 8-bit saturation count [7:0] + agc_enable: int = 0 # 1-bit AGC enable readback # ============================================================================ @@ -144,7 +175,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 +212,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 @@ -200,10 +231,10 @@ class RadarProtocol: return None sr = StatusResponse() - # Word 0: {0xFF, 3'b0, mode[1:0], 5'b0, stream[2:0], threshold[15:0]} + # Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} sr.cfar_threshold = words[0] & 0xFFFF - sr.stream_ctrl = (words[0] >> 16) & 0x07 - sr.radar_mode = (words[0] >> 21) & 0x03 + sr.stream_ctrl = (words[0] >> 19) & 0x07 + sr.radar_mode = (words[0] >> 22) & 0x03 # Word 1: {long_chirp[31:16], long_listen[15:0]} sr.long_listen = words[1] & 0xFFFF sr.long_chirp = (words[1] >> 16) & 0xFFFF @@ -213,8 +244,13 @@ class RadarProtocol: # Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]} sr.chirps_per_elev = words[3] & 0x3F sr.short_listen = (words[3] >> 16) & 0xFFFF - # Word 4: {30'd0, range_mode[1:0]} + # Word 4: {agc_current_gain[31:28], agc_peak_magnitude[27:20], + # agc_saturation_count[19:12], agc_enable[11], 9'd0, range_mode[1:0]} sr.range_mode = words[4] & 0x03 + sr.agc_enable = (words[4] >> 11) & 0x01 + sr.agc_saturation_count = (words[4] >> 12) & 0xFF + sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF + sr.agc_current_gain = (words[4] >> 28) & 0x0F # Word 5: {7'd0, self_test_busy, 8'd0, self_test_detail[7:0], # 3'd0, self_test_flags[4:0]} sr.self_test_flags = words[5] & 0x1F @@ -223,7 +259,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 +269,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 +296,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 +349,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 +372,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 +389,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 +439,7 @@ class FT2232HConnection: buf += pkt + self._mock_seq_idx = (start_idx + num_packets) % NUM_CELLS return bytes(buf) @@ -401,20 +448,25 @@ 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) is NOT hardware-only — it's in _REPLAY_ADJUSTABLE_OPCODES + 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 + 0x28, # AGC_ENABLE + 0x29, # AGC_TARGET + 0x2A, # AGC_ATTACK + 0x2B, # AGC_DECAY + 0x2C, # AGC_HOLDOFF 0x30, # SELF_TEST_TRIGGER 0x31, # SELF_TEST_STATUS 0xFF, # STATUS_REQUEST @@ -422,6 +474,7 @@ _HARDWARE_ONLY_OPCODES = { # Replay-adjustable opcodes (re-run signal processing) _REPLAY_ADJUSTABLE_OPCODES = { + 0x03, # DETECT_THRESHOLD 0x21, # CFAR_GUARD 0x22, # CFAR_TRAIN 0x23, # CFAR_ALPHA @@ -439,26 +492,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 +515,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). @@ -583,17 +618,18 @@ class ReplayConnection: self._cfar_alpha: int = 0x30 self._cfar_mode: int = 0 # 0=CA, 1=GO, 2=SO self._cfar_enable: bool = True + self._detect_threshold: int = 10000 # RTL default (host_detect_threshold) # 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 +640,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, IndexError, 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) @@ -647,7 +683,11 @@ class ReplayConnection: if opcode in _REPLAY_ADJUSTABLE_OPCODES: changed = False with self._lock: - if opcode == 0x21: # CFAR_GUARD + if opcode == 0x03: # DETECT_THRESHOLD + if self._detect_threshold != value: + self._detect_threshold = value + changed = True + elif opcode == 0x21: # CFAR_GUARD if self._cfar_guard != value: self._cfar_guard = value changed = True @@ -673,10 +713,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: @@ -740,7 +779,10 @@ class ReplayConnection: mode=self._cfar_mode, ) else: - det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=bool) + # Simple threshold fallback matching RTL cfar_ca.v: + # detect = (|I| + |Q|) > detect_threshold (L1 norm) + mag = np.abs(dop_i) + np.abs(dop_q) + det = mag > self._detect_threshold det_count = int(det.sum()) log.info(f"Replay: rebuilt {NUM_CELLS} packets (" @@ -827,7 +869,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 +886,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 +895,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 +913,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 +930,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 +967,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 +1002,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..b8bf6cf 100644 --- a/9_Firmware/9_3_GUI/test_radar_dashboard.py +++ b/9_Firmware/9_3_GUI/test_radar_dashboard.py @@ -125,13 +125,14 @@ class TestRadarProtocol(unittest.TestCase): long_chirp=3000, long_listen=13700, guard=17540, short_chirp=50, short_listen=17450, chirps=32, range_mode=0, - st_flags=0, st_detail=0, st_busy=0): + st_flags=0, st_detail=0, st_busy=0, + agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0): """Build a 26-byte status response matching FPGA format (Build 26).""" pkt = bytearray() pkt.append(STATUS_HEADER_BYTE) - # Word 0: {0xFF, 3'b0, mode[1:0], 5'b0, stream[2:0], threshold[15:0]} - w0 = (0xFF << 24) | ((mode & 0x03) << 21) | ((stream & 0x07) << 16) | (threshold & 0xFFFF) + # Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} + w0 = (0xFF << 24) | ((mode & 0x03) << 22) | ((stream & 0x07) << 19) | (threshold & 0xFFFF) pkt += struct.pack(">I", w0) # Word 1: {long_chirp, long_listen} @@ -146,8 +147,11 @@ class TestRadarProtocol(unittest.TestCase): w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F) pkt += struct.pack(">I", w3) - # Word 4: {30'd0, range_mode[1:0]} - w4 = range_mode & 0x03 + # Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0], + # agc_saturation_count[7:0], agc_enable, 9'd0, range_mode[1:0]} + w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) | + ((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) | + (range_mode & 0x03)) pkt += struct.pack(">I", w4) # Word 5: {7'd0, self_test_busy, 8'd0, self_test_detail[7:0], @@ -368,7 +372,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 +425,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 +634,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 +672,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 +695,41 @@ 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, + 0x28, 0x29, 0x2A, 0x2B, 0x2C, 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") @@ -728,5 +752,199 @@ class TestStatusResponseDefaults(unittest.TestCase): self.assertEqual(sr.self_test_busy, 1) +class TestAGCOpcodes(unittest.TestCase): + """Verify AGC opcode enum members match FPGA RTL (0x28-0x2C).""" + + def test_agc_enable_opcode(self): + self.assertEqual(Opcode.AGC_ENABLE, 0x28) + + def test_agc_target_opcode(self): + self.assertEqual(Opcode.AGC_TARGET, 0x29) + + def test_agc_attack_opcode(self): + self.assertEqual(Opcode.AGC_ATTACK, 0x2A) + + def test_agc_decay_opcode(self): + self.assertEqual(Opcode.AGC_DECAY, 0x2B) + + def test_agc_holdoff_opcode(self): + self.assertEqual(Opcode.AGC_HOLDOFF, 0x2C) + + +class TestAGCStatusParsing(unittest.TestCase): + """Verify AGC fields in status_words[4] are parsed correctly.""" + + def _make_status_packet(self, **kwargs): + """Delegate to TestRadarProtocol helper.""" + helper = TestRadarProtocol() + return helper._make_status_packet(**kwargs) + + def test_agc_fields_default_zero(self): + """With no AGC fields set, all should be 0.""" + raw = self._make_status_packet() + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 0) + self.assertEqual(sr.agc_peak_magnitude, 0) + self.assertEqual(sr.agc_saturation_count, 0) + self.assertEqual(sr.agc_enable, 0) + + def test_agc_fields_nonzero(self): + """AGC fields round-trip through status packet.""" + raw = self._make_status_packet(agc_gain=7, agc_peak=200, + agc_sat=15, agc_enable=1) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 7) + self.assertEqual(sr.agc_peak_magnitude, 200) + self.assertEqual(sr.agc_saturation_count, 15) + self.assertEqual(sr.agc_enable, 1) + + def test_agc_max_values(self): + """AGC fields at max values.""" + raw = self._make_status_packet(agc_gain=15, agc_peak=255, + agc_sat=255, agc_enable=1) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 15) + self.assertEqual(sr.agc_peak_magnitude, 255) + self.assertEqual(sr.agc_saturation_count, 255) + self.assertEqual(sr.agc_enable, 1) + + def test_agc_and_range_mode_coexist(self): + """AGC fields and range_mode occupy the same word without conflict.""" + raw = self._make_status_packet(agc_gain=5, agc_peak=128, + agc_sat=42, agc_enable=1, + range_mode=2) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.agc_current_gain, 5) + self.assertEqual(sr.agc_peak_magnitude, 128) + self.assertEqual(sr.agc_saturation_count, 42) + self.assertEqual(sr.agc_enable, 1) + self.assertEqual(sr.range_mode, 2) + + +class TestAGCStatusResponseDefaults(unittest.TestCase): + """Verify StatusResponse AGC field defaults.""" + + def test_default_agc_fields(self): + sr = StatusResponse() + self.assertEqual(sr.agc_current_gain, 0) + self.assertEqual(sr.agc_peak_magnitude, 0) + self.assertEqual(sr.agc_saturation_count, 0) + self.assertEqual(sr.agc_enable, 0) + + def test_agc_fields_set(self): + sr = StatusResponse(agc_current_gain=7, agc_peak_magnitude=200, + agc_saturation_count=15, agc_enable=1) + self.assertEqual(sr.agc_current_gain, 7) + self.assertEqual(sr.agc_peak_magnitude, 200) + self.assertEqual(sr.agc_saturation_count, 15) + self.assertEqual(sr.agc_enable, 1) + + +# ============================================================================= +# AGC Visualization — ring buffer / data model tests +# ============================================================================= + +class TestAGCVisualizationHistory(unittest.TestCase): + """Test the AGC visualization ring buffer logic (no GUI required).""" + + def _make_deque(self, maxlen=256): + from collections import deque + return deque(maxlen=maxlen) + + def test_ring_buffer_maxlen(self): + """Ring buffer should evict oldest when full.""" + d = self._make_deque(maxlen=4) + for i in range(6): + d.append(i) + self.assertEqual(list(d), [2, 3, 4, 5]) + self.assertEqual(len(d), 4) + + def test_gain_history_accumulation(self): + """Gain values accumulate correctly in a deque.""" + gain_hist = self._make_deque(maxlen=256) + statuses = [ + StatusResponse(agc_current_gain=g) + for g in [0, 3, 7, 15, 8, 2] + ] + for st in statuses: + gain_hist.append(st.agc_current_gain) + self.assertEqual(list(gain_hist), [0, 3, 7, 15, 8, 2]) + + def test_peak_history_accumulation(self): + """Peak magnitude values accumulate correctly.""" + peak_hist = self._make_deque(maxlen=256) + for p in [0, 50, 200, 255, 128]: + peak_hist.append(p) + self.assertEqual(list(peak_hist), [0, 50, 200, 255, 128]) + + def test_saturation_total_computation(self): + """Sum of saturation ring buffer gives running total.""" + sat_hist = self._make_deque(maxlen=256) + for s in [0, 0, 5, 0, 12, 3]: + sat_hist.append(s) + self.assertEqual(sum(sat_hist), 20) + + def test_saturation_color_thresholds(self): + """Color logic: green=0, yellow=1-10, red>10.""" + def sat_color(total): + if total > 10: + return "red" + if total > 0: + return "yellow" + return "green" + self.assertEqual(sat_color(0), "green") + self.assertEqual(sat_color(1), "yellow") + self.assertEqual(sat_color(10), "yellow") + self.assertEqual(sat_color(11), "red") + self.assertEqual(sat_color(255), "red") + + def test_ring_buffer_eviction_preserves_latest(self): + """After overflow, only the most recent values remain.""" + d = self._make_deque(maxlen=8) + for i in range(20): + d.append(i) + self.assertEqual(list(d), [12, 13, 14, 15, 16, 17, 18, 19]) + + def test_empty_history_safe(self): + """Empty ring buffer should be safe for max/sum.""" + d = self._make_deque(maxlen=256) + self.assertEqual(sum(d), 0) + self.assertEqual(len(d), 0) + # max() on empty would raise — test the guard pattern used in viz code + max_sat = max(d) if d else 0 + self.assertEqual(max_sat, 0) + + def test_agc_mode_string(self): + """AGC mode display string from enable flag.""" + self.assertEqual( + "AUTO" if StatusResponse(agc_enable=1).agc_enable else "MANUAL", + "AUTO") + self.assertEqual( + "AUTO" if StatusResponse(agc_enable=0).agc_enable else "MANUAL", + "MANUAL") + + def test_xlim_scroll_logic(self): + """X-axis scroll: when n >= history_len, xlim should expand.""" + history_len = 8 + d = self._make_deque(maxlen=history_len) + for i in range(10): + d.append(i) + n = len(d) + # After 10 pushes into maxlen=8, n=8 + self.assertEqual(n, history_len) + # xlim should be (0, n) for static or (n-history_len, n) for scrolling + self.assertEqual(max(0, n - history_len), 0) + self.assertEqual(n, 8) + + def test_sat_autoscale_ylim(self): + """Saturation y-axis auto-scale: max(max_sat * 1.5, 5).""" + # No saturation + self.assertEqual(max(0 * 1.5, 5), 5) + # Some saturation + self.assertAlmostEqual(max(10 * 1.5, 5), 15.0) + # High saturation + self.assertAlmostEqual(max(200 * 1.5, 5), 300.0) + + if __name__ == "__main__": unittest.main(verbosity=2) 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..bb54e48 --- /dev/null +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -0,0 +1,427 @@ +""" +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 +# ============================================================================= + +def _pyqt6_available(): + try: + import PyQt6.QtCore # noqa: F401 + return True + except ImportError: + return False + + +@unittest.skipUnless(_pyqt6_available(), "PyQt6 not installed") +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 + # Core exports (no PyQt6 required) + for name in ["RadarTarget", "RadarSettings", "GPSData", + "ProcessingConfig", "FT2232HConnection", + "RadarProtocol", "RadarProcessor"]: + self.assertTrue(hasattr(v7, name), f"v7 missing export: {name}") + # PyQt6-dependent exports — only present when PyQt6 is installed + if _pyqt6_available(): + for name in ["RadarDataWorker", "RadarMapWidget", + "RadarDashboard"]: + self.assertTrue(hasattr(v7, name), f"v7 missing export: {name}") + + +# ============================================================================= +# Test: AGC Visualization data model +# ============================================================================= + +class TestAGCVisualizationV7(unittest.TestCase): + """AGC visualization ring buffer and data model tests (no Qt required).""" + + def _make_deque(self, maxlen=256): + from collections import deque + return deque(maxlen=maxlen) + + def test_ring_buffer_basics(self): + d = self._make_deque(maxlen=4) + for i in range(6): + d.append(i) + self.assertEqual(list(d), [2, 3, 4, 5]) + + def test_gain_range_4bit(self): + """AGC gain is 4-bit (0-15).""" + from radar_protocol import StatusResponse + for g in [0, 7, 15]: + sr = StatusResponse(agc_current_gain=g) + self.assertEqual(sr.agc_current_gain, g) + + def test_peak_range_8bit(self): + """Peak magnitude is 8-bit (0-255).""" + from radar_protocol import StatusResponse + for p in [0, 128, 255]: + sr = StatusResponse(agc_peak_magnitude=p) + self.assertEqual(sr.agc_peak_magnitude, p) + + def test_saturation_accumulation(self): + """Saturation ring buffer sum tracks total events.""" + sat = self._make_deque(maxlen=256) + for s in [0, 5, 0, 10, 3]: + sat.append(s) + self.assertEqual(sum(sat), 18) + + def test_mode_label_logic(self): + """AGC mode string from enable field.""" + from radar_protocol import StatusResponse + self.assertEqual( + "AUTO" if StatusResponse(agc_enable=1).agc_enable else "MANUAL", + "AUTO") + self.assertEqual( + "AUTO" if StatusResponse(agc_enable=0).agc_enable else "MANUAL", + "MANUAL") + + def test_history_len_default(self): + """Default history length should be 256.""" + d = self._make_deque(maxlen=256) + self.assertEqual(d.maxlen, 256) + + def test_color_thresholds(self): + """Saturation color: green=0, warning=1-10, error>10.""" + from v7.models import DARK_SUCCESS, DARK_WARNING, DARK_ERROR + def pick_color(total): + if total > 10: + return DARK_ERROR + if total > 0: + return DARK_WARNING + return DARK_SUCCESS + self.assertEqual(pick_color(0), DARK_SUCCESS) + self.assertEqual(pick_color(5), DARK_WARNING) + self.assertEqual(pick_color(11), DARK_ERROR) + + +# ============================================================================= +# 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..175da91 100644 --- a/9_Firmware/9_3_GUI/v7/__init__.py +++ b/9_Firmware/9_3_GUI/v7/__init__.py @@ -19,44 +19,52 @@ 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, ) -# Workers and simulator -from .workers import ( - RadarDataWorker, - GPSDataWorker, - TargetSimulator, - polar_to_geographic, -) +# Workers, map widget, and dashboard require PyQt6 — import lazily so that +# tests/CI environments without PyQt6 can still access models/hardware/processing. +try: + from .workers import ( + RadarDataWorker, + GPSDataWorker, + TargetSimulator, + polar_to_geographic, + ) -# Map widget -from .map_widget import ( - MapBridge, - RadarMapWidget, -) + from .map_widget import ( + MapBridge, + RadarMapWidget, + ) -# Main dashboard -from .dashboard import ( - RadarDashboard, - RangeDopplerCanvas, -) + from .dashboard import ( + RadarDashboard, + RangeDopplerCanvas, + ) +except ImportError: # PyQt6 not installed (e.g. CI headless runner) + pass -__all__ = [ +__all__ = [ # noqa: RUF022 # models "RadarTarget", "RadarSettings", "GPSData", "ProcessingConfig", "TileServer", "DARK_BG", "DARK_FG", "DARK_ACCENT", "DARK_HIGHLIGHT", "DARK_BORDER", @@ -64,11 +72,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..0f9a53b 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -1,31 +1,42 @@ """ 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 six 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 27 opcodes incl. AGC, + bit-width validation, grouped layout matching production) + 4. AGC Monitor — Real-time AGC strip charts (gain, peak magnitude, saturation) + 5. Diagnostics — Connection indicators, packet stats, dependency status, + self-test results, log viewer + 6. 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 +from collections import deque 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 PyQt6.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.figure import Figure @@ -37,34 +48,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 +97,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 +110,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 +124,45 @@ 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._last_stats: dict = {} + self._current_targets: list[RadarTarget] = [] + + # FPGA control parameter widgets + self._param_spins: dict = {} # opcode_hex -> QSpinBox + + # AGC visualization history (ring buffers) + self._agc_history_len = 256 + self._agc_gain_history: deque[int] = deque(maxlen=self._agc_history_len) + self._agc_peak_history: deque[int] = deque(maxlen=self._agc_history_len) + self._agc_sat_history: deque[int] = deque(maxlen=self._agc_history_len) + self._agc_last_redraw: float = 0.0 # throttle chart redraws + self._AGC_REDRAW_INTERVAL: float = 0.5 # seconds between redraws # ---- Build UI ------------------------------------------------------ self._apply_dark_theme() @@ -138,12 +174,14 @@ class RadarDashboard(QMainWindow): self._gui_timer.timeout.connect(self._refresh_gui) self._gui_timer.start(100) - # Log handler for diagnostics - self._log_handler = _QtLogHandler(self._log_append) + # Log handler for diagnostics (thread-safe via Qt signal) + self._log_bridge = _LogSignalBridge(self) + self._log_bridge.log_message.connect(self._log_append) + self._log_handler = _QtLogHandler(self._log_bridge) 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 +318,8 @@ class RadarDashboard(QMainWindow): self._create_main_tab() self._create_map_tab() + self._create_fpga_control_tab() + self._create_agc_monitor_tab() self._create_diagnostics_tab() self._create_settings_tab() @@ -298,16 +338,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 +360,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 +369,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 +377,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 +400,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 (m)", "Velocity (m/s)", "Magnitude", "SNR (dB)", "Track ID", ]) self._targets_table_main.setAlternatingRowColors(True) self._targets_table_main.setSelectionBehavior( @@ -489,7 +529,390 @@ 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) + + # ── AGC (Automatic Gain Control) ────────────────────────────── + grp_agc = QGroupBox("AGC (Auto Gain)") + agc_layout = QVBoxLayout(grp_agc) + + agc_params = [ + ("AGC Enable", 0x28, 0, 1, "0=manual, 1=auto"), + ("AGC Target", 0x29, 200, 8, "0-255, peak target"), + ("AGC Attack", 0x2A, 1, 4, "0-15, atten step"), + ("AGC Decay", 0x2B, 1, 4, "0-15, gain-up step"), + ("AGC Holdoff", 0x2C, 4, 4, "0-15, frames"), + ] + for label, opcode, default, bits, hint in agc_params: + self._add_fpga_param_row(agc_layout, label, opcode, default, bits, hint) + + # AGC quick toggles + agc_row = QHBoxLayout() + btn_agc_on = QPushButton("Enable AGC") + btn_agc_on.clicked.connect(lambda: self._send_fpga_cmd(0x28, 1)) + agc_row.addWidget(btn_agc_on) + btn_agc_off = QPushButton("Disable AGC") + btn_agc_off.clicked.connect(lambda: self._send_fpga_cmd(0x28, 0)) + agc_row.addWidget(btn_agc_off) + agc_layout.addLayout(agc_row) + + # AGC status readback labels + agc_st_group = QGroupBox("AGC Status") + agc_st_layout = QVBoxLayout(agc_st_group) + self._agc_labels: dict[str, QLabel] = {} + for name, default_text in [ + ("enable", "AGC: --"), + ("gain", "Gain: --"), + ("peak", "Peak: --"), + ("sat", "Sat Count: --"), + ]: + lbl = QLabel(default_text) + lbl.setStyleSheet(f"color: {DARK_INFO}; font-size: 10px;") + agc_st_layout.addWidget(lbl) + self._agc_labels[name] = lbl + agc_layout.addWidget(agc_st_group) + + right_layout.addWidget(grp_agc) + + # 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: AGC Monitor + # ----------------------------------------------------------------- + + def _create_agc_monitor_tab(self): + """AGC Monitor — real-time strip charts for FPGA inner-loop AGC.""" + tab = QWidget() + layout = QVBoxLayout(tab) + layout.setContentsMargins(8, 8, 8, 8) + + # ---- Top indicator row --------------------------------------------- + indicator = QFrame() + indicator.setStyleSheet( + f"background-color: {DARK_ACCENT}; border-radius: 4px;") + ind_layout = QHBoxLayout(indicator) + ind_layout.setContentsMargins(12, 8, 12, 8) + + self._agc_mode_lbl = QLabel("AGC: --") + self._agc_mode_lbl.setStyleSheet( + f"color: {DARK_FG}; font-size: 16px; font-weight: bold;") + ind_layout.addWidget(self._agc_mode_lbl) + + self._agc_gain_lbl = QLabel("Gain: --") + self._agc_gain_lbl.setStyleSheet( + f"color: {DARK_INFO}; font-size: 14px;") + ind_layout.addWidget(self._agc_gain_lbl) + + self._agc_peak_lbl = QLabel("Peak: --") + self._agc_peak_lbl.setStyleSheet( + f"color: {DARK_INFO}; font-size: 14px;") + ind_layout.addWidget(self._agc_peak_lbl) + + self._agc_sat_total_lbl = QLabel("Total Saturations: 0") + self._agc_sat_total_lbl.setStyleSheet( + f"color: {DARK_SUCCESS}; font-size: 14px; font-weight: bold;") + ind_layout.addWidget(self._agc_sat_total_lbl) + + ind_layout.addStretch() + layout.addWidget(indicator) + + # ---- Matplotlib figure with 3 subplots ----------------------------- + agc_fig = Figure(figsize=(12, 7), facecolor=DARK_BG) + agc_fig.subplots_adjust( + left=0.07, right=0.96, top=0.95, bottom=0.07, + hspace=0.32) + + # Subplot 1: Gain history (4-bit, 0-15) + self._agc_ax_gain = agc_fig.add_subplot(3, 1, 1) + self._agc_ax_gain.set_facecolor(DARK_ACCENT) + self._agc_ax_gain.set_ylabel("Gain Code", color=DARK_FG, fontsize=10) + self._agc_ax_gain.set_title( + "FPGA Inner-Loop Gain (4-bit)", color=DARK_FG, fontsize=11) + self._agc_ax_gain.set_ylim(-0.5, 15.5) + self._agc_ax_gain.tick_params(colors=DARK_FG, labelsize=9) + self._agc_ax_gain.set_xlim(0, self._agc_history_len) + for spine in self._agc_ax_gain.spines.values(): + spine.set_color(DARK_BORDER) + self._agc_gain_line, = self._agc_ax_gain.plot( + [], [], color="#89b4fa", linewidth=1.5, label="Gain") + self._agc_ax_gain.axhline(y=7.5, color=DARK_WARNING, linestyle="--", + linewidth=0.8, alpha=0.5, label="Midpoint") + self._agc_ax_gain.legend( + loc="upper right", fontsize=8, + facecolor=DARK_ACCENT, edgecolor=DARK_BORDER, + labelcolor=DARK_FG) + + # Subplot 2: Peak magnitude (8-bit, 0-255) + self._agc_ax_peak = agc_fig.add_subplot( + 3, 1, 2, sharex=self._agc_ax_gain) + self._agc_ax_peak.set_facecolor(DARK_ACCENT) + self._agc_ax_peak.set_ylabel("Peak Mag", color=DARK_FG, fontsize=10) + self._agc_ax_peak.set_title( + "ADC Peak Magnitude (8-bit)", color=DARK_FG, fontsize=11) + self._agc_ax_peak.set_ylim(-5, 260) + self._agc_ax_peak.tick_params(colors=DARK_FG, labelsize=9) + for spine in self._agc_ax_peak.spines.values(): + spine.set_color(DARK_BORDER) + self._agc_peak_line, = self._agc_ax_peak.plot( + [], [], color=DARK_SUCCESS, linewidth=1.5, label="Peak") + self._agc_ax_peak.axhline(y=200, color=DARK_WARNING, linestyle="--", + linewidth=0.8, alpha=0.5, + label="Target (200)") + self._agc_ax_peak.axhspan(240, 255, alpha=0.15, color=DARK_ERROR, + label="Sat Zone") + self._agc_ax_peak.legend( + loc="upper right", fontsize=8, + facecolor=DARK_ACCENT, edgecolor=DARK_BORDER, + labelcolor=DARK_FG) + + # Subplot 3: Saturation count per update (8-bit, 0-255) + self._agc_ax_sat = agc_fig.add_subplot( + 3, 1, 3, sharex=self._agc_ax_gain) + self._agc_ax_sat.set_facecolor(DARK_ACCENT) + self._agc_ax_sat.set_ylabel("Sat Count", color=DARK_FG, fontsize=10) + self._agc_ax_sat.set_xlabel( + "Sample (newest right)", color=DARK_FG, fontsize=10) + self._agc_ax_sat.set_title( + "Saturation Events per Update", color=DARK_FG, fontsize=11) + self._agc_ax_sat.set_ylim(-1, 10) + self._agc_ax_sat.tick_params(colors=DARK_FG, labelsize=9) + for spine in self._agc_ax_sat.spines.values(): + spine.set_color(DARK_BORDER) + self._agc_sat_line, = self._agc_ax_sat.plot( + [], [], color=DARK_ERROR, linewidth=1.0) + self._agc_sat_fill_artist = None + self._agc_ax_sat.legend( + loc="upper right", fontsize=8, + facecolor=DARK_ACCENT, edgecolor=DARK_BORDER, + labelcolor=DARK_FG) + + self._agc_canvas = FigureCanvasQTAgg(agc_fig) + layout.addWidget(self._agc_canvas, stretch=1) + + self._tabs.addTab(tab, "AGC Monitor") + + # ----------------------------------------------------------------- + # TAB 5: Diagnostics # ----------------------------------------------------------------- def _create_diagnostics_tab(self): @@ -503,24 +926,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 +955,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 +976,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 +1009,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 +1022,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 +1062,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 +1078,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 +1093,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 +1137,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 +1145,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 +1276,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") @@ -1002,7 +1313,13 @@ class RadarDashboard(QMainWindow): self._simulator.stop() self._simulator = None self._demo_mode = False - self._sb_mode.setText("Idle" if not self._running else "Live") + if not self._running: + mode = "Idle" + elif isinstance(self._connection, ReplayConnection): + mode = "Replay" + else: + mode = "Live" + self._sb_mode.setText(mode) self._sb_status.setText("Demo stopped") self._demo_btn_main.setText("Start Demo") self._demo_btn_map.setText("Start Demo") @@ -1030,6 +1347,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 +1366,7 @@ class RadarDashboard(QMainWindow): @pyqtSlot(dict) def _on_radar_stats(self, stats: dict): - self._radar_stats = stats + self._last_stats = stats @pyqtSlot(str) def _on_worker_error(self, msg: str): @@ -1087,6 +1416,134 @@ 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'}") + + # AGC status readback + if hasattr(self, '_agc_labels'): + agc_str = "AUTO" if st.agc_enable else "MANUAL" + agc_color = DARK_SUCCESS if st.agc_enable else DARK_INFO + self._agc_labels["enable"].setStyleSheet( + f"color: {agc_color}; font-weight: bold;") + self._agc_labels["enable"].setText(f"AGC: {agc_str}") + self._agc_labels["gain"].setText( + f"Gain: {st.agc_current_gain}") + self._agc_labels["peak"].setText( + f"Peak: {st.agc_peak_magnitude}") + sat_color = DARK_ERROR if st.agc_saturation_count > 0 else DARK_INFO + self._agc_labels["sat"].setStyleSheet( + f"color: {sat_color}; font-weight: bold;") + self._agc_labels["sat"].setText( + f"Sat Count: {st.agc_saturation_count}") + + # AGC Monitor tab visualization + self._update_agc_visualization(st) + + def _update_agc_visualization(self, st: StatusResponse): + """Push AGC metrics into ring buffers and redraw AGC Monitor charts. + + Data is always accumulated (cheap), but matplotlib redraws are + throttled to ``_AGC_REDRAW_INTERVAL`` seconds to avoid saturating + the GUI event-loop when status packets arrive at 20 Hz. + """ + if not hasattr(self, '_agc_canvas'): + return + + # Push data into ring buffers (always — O(1)) + self._agc_gain_history.append(st.agc_current_gain) + self._agc_peak_history.append(st.agc_peak_magnitude) + self._agc_sat_history.append(st.agc_saturation_count) + + # Update indicator labels (cheap Qt calls) + agc_str = "AUTO" if st.agc_enable else "MANUAL" + agc_color = DARK_SUCCESS if st.agc_enable else DARK_INFO + self._agc_mode_lbl.setStyleSheet( + f"color: {agc_color}; font-size: 16px; font-weight: bold;") + self._agc_mode_lbl.setText(f"AGC: {agc_str}") + self._agc_gain_lbl.setText(f"Gain: {st.agc_current_gain}") + self._agc_peak_lbl.setText(f"Peak: {st.agc_peak_magnitude}") + + total_sat = sum(self._agc_sat_history) + if total_sat > 10: + sat_color = DARK_ERROR + elif total_sat > 0: + sat_color = DARK_WARNING + else: + sat_color = DARK_SUCCESS + self._agc_sat_total_lbl.setStyleSheet( + f"color: {sat_color}; font-size: 14px; font-weight: bold;") + self._agc_sat_total_lbl.setText(f"Total Saturations: {total_sat}") + + # ---- Throttle matplotlib redraws --------------------------------- + now = time.monotonic() + if now - self._agc_last_redraw < self._AGC_REDRAW_INTERVAL: + return + self._agc_last_redraw = now + + n = len(self._agc_gain_history) + xs = list(range(n)) + + # Update line plots + gain_data = list(self._agc_gain_history) + peak_data = list(self._agc_peak_history) + sat_data = list(self._agc_sat_history) + + self._agc_gain_line.set_data(xs, gain_data) + self._agc_peak_line.set_data(xs, peak_data) + self._agc_sat_line.set_data(xs, sat_data) + + # Update saturation fill + if self._agc_sat_fill_artist is not None: + self._agc_sat_fill_artist.remove() + if n > 0: + self._agc_sat_fill_artist = self._agc_ax_sat.fill_between( + xs, sat_data, color=DARK_ERROR, alpha=0.4) + else: + self._agc_sat_fill_artist = None + + # Auto-scale saturation y-axis + max_sat = max(sat_data) if sat_data else 1 + self._agc_ax_sat.set_ylim(-1, max(max_sat * 1.3, 5)) + + # Scroll x-axis + self._agc_ax_gain.set_xlim(max(0, n - self._agc_history_len), n) + + self._agc_canvas.draw_idle() + # ===================================================================== # Position / coverage callbacks (map sidebar) # ===================================================================== @@ -1108,46 +1565,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 +1577,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 +1603,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 +1637,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 +1646,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))), + str(self._last_stats.get("errors", 0)), 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 +1689,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,22 +1720,30 @@ 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 via signal) # ============================================================================= -class _QtLogHandler(logging.Handler): - """Sends log records to a callback (called on the thread that emitted).""" - def __init__(self, callback): +class _LogSignalBridge(QObject): + """Thread-safe bridge: emits a Qt signal so the slot runs on the GUI thread.""" + + log_message = pyqtSignal(str) + + +class _QtLogHandler(logging.Handler): + """Sends log records to a QObject signal (safe from any thread).""" + + def __init__(self, bridge: _LogSignalBridge): super().__init__() - self._callback = callback + self._bridge = bridge self.setFormatter(logging.Formatter( "%(asctime)s %(levelname)-8s %(message)s", datefmt="%H:%M:%S", @@ -1331,6 +1752,6 @@ class _QtLogHandler(logging.Handler): def emit(self, record): try: msg = self.format(record) - self._callback(msg) - except Exception: + self._bridge.log_message.emit(msg) + 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/tests/cross_layer/.gitignore b/9_Firmware/tests/cross_layer/.gitignore new file mode 100644 index 0000000..3dbdaec --- /dev/null +++ b/9_Firmware/tests/cross_layer/.gitignore @@ -0,0 +1,12 @@ +# Simulation outputs (generated by iverilog TB) +cmd_results.txt +data_packet.txt +status_packet.txt +*.vcd +*.vvp + +# Compiled C stub +stm32_stub + +# Python +__pycache__/ diff --git a/9_Firmware/tests/cross_layer/contract_parser.py b/9_Firmware/tests/cross_layer/contract_parser.py new file mode 100644 index 0000000..a0220ff --- /dev/null +++ b/9_Firmware/tests/cross_layer/contract_parser.py @@ -0,0 +1,795 @@ +""" +Cross-layer contract parsers. + +Extracts interface contracts (opcodes, bit widths, reset defaults, packet +layouts) directly from the source files of each layer: + - Python GUI: radar_protocol.py + - FPGA RTL: radar_system_top.v, usb_data_interface_ft2232h.v, + usb_data_interface.v + - STM32 MCU: RadarSettings.cpp, main.cpp + +These parsers do NOT define the expected values — they discover what each +layer actually implements, so the test can compare layers against ground +truth and find bugs where both sides are wrong (like the 0x06 phantom +opcode or the status_words[0] 37-bit truncation). +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path + +# --------------------------------------------------------------------------- +# Repository layout (relative to repo root) +# --------------------------------------------------------------------------- +REPO_ROOT = Path(__file__).resolve().parents[3] +GUI_DIR = REPO_ROOT / "9_Firmware" / "9_3_GUI" +FPGA_DIR = REPO_ROOT / "9_Firmware" / "9_2_FPGA" +MCU_DIR = REPO_ROOT / "9_Firmware" / "9_1_Microcontroller" +MCU_LIB_DIR = MCU_DIR / "9_1_1_C_Cpp_Libraries" +MCU_CODE_DIR = MCU_DIR / "9_1_3_C_Cpp_Code" +XDC_DIR = FPGA_DIR / "constraints" + + +# =================================================================== +# Data structures +# =================================================================== + +@dataclass +class OpcodeEntry: + """One opcode as declared in a single layer.""" + name: str + value: int + register: str = "" # Verilog register name it writes to + bit_slice: str = "" # e.g. "[3:0]", "[15:0]", "[0]" + bit_width: int = 0 # derived from bit_slice + reset_default: int | None = None + is_pulse: bool = False # True for trigger/request opcodes + + +@dataclass +class StatusWordField: + """One field inside a status_words[] entry.""" + name: str + word_index: int + msb: int # bit position in the 32-bit word (0-indexed from LSB) + lsb: int + width: int + + +@dataclass +class DataPacketField: + """One field in the 11-byte data packet.""" + name: str + byte_start: int # first byte index (0 = header) + byte_end: int # last byte index (inclusive) + width_bits: int + + +@dataclass +class PacketConstants: + """Header/footer/size constants for a packet type.""" + header: int + footer: int + size: int + + +@dataclass +class SettingsField: + """One field in the STM32 SET...END settings packet.""" + name: str + offset: int # byte offset from start of payload (after "SET") + size: int # bytes + c_type: str # "double" or "uint32_t" + + +@dataclass +class GpioPin: + """A GPIO pin with direction.""" + name: str + pin_id: str # e.g. "PD8", "H11" + direction: str # "output" or "input" + layer: str # "stm32" or "fpga" + + +@dataclass +class ConcatWidth: + """Result of counting bits in a Verilog concatenation.""" + total_bits: int + target_bits: int # width of the register being assigned to + fragments: list[tuple[str, int]] = field(default_factory=list) + truncated: bool = False + + +# =================================================================== +# Python layer parser +# =================================================================== + +def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]: + """Parse the Opcode enum from radar_protocol.py. + Returns {opcode_value: OpcodeEntry}. + """ + if filepath is None: + filepath = GUI_DIR / "radar_protocol.py" + text = filepath.read_text() + + # Find the Opcode class body + match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL) + if not match: + raise ValueError(f"Could not find 'class Opcode' in {filepath}") + + opcodes: dict[int, OpcodeEntry] = {} + for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()): + name = m.group(1) + value = int(m.group(2), 16) + opcodes[value] = OpcodeEntry(name=name, value=value) + return opcodes + + +def parse_python_packet_constants(filepath: Path | None = None) -> dict[str, PacketConstants]: + """Extract HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE, packet sizes.""" + if filepath is None: + filepath = GUI_DIR / "radar_protocol.py" + text = filepath.read_text() + + def _find(pattern: str) -> int: + m = re.search(pattern, text) + if not m: + raise ValueError(f"Pattern not found: {pattern}") + val = m.group(1) + return int(val, 16) if val.startswith("0x") else int(val) + + header = _find(r'HEADER_BYTE\s*=\s*(0x[0-9a-fA-F]+|\d+)') + footer = _find(r'FOOTER_BYTE\s*=\s*(0x[0-9a-fA-F]+|\d+)') + status_header = _find(r'STATUS_HEADER_BYTE\s*=\s*(0x[0-9a-fA-F]+|\d+)') + data_size = _find(r'DATA_PACKET_SIZE\s*=\s*(\d+)') + status_size = _find(r'STATUS_PACKET_SIZE\s*=\s*(\d+)') + + return { + "data": PacketConstants(header=header, footer=footer, size=data_size), + "status": PacketConstants(header=status_header, footer=footer, size=status_size), + } + + +def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPacketField]: + """ + Extract byte offsets from parse_data_packet() by finding struct.unpack_from calls. + Returns fields in byte order. + """ + if filepath is None: + filepath = GUI_DIR / "radar_protocol.py" + text = filepath.read_text() + + # Find parse_data_packet method body + match = re.search( + r'def parse_data_packet\(.*?\).*?(?=\n @|\n def |\nclass |\Z)', + text, re.DOTALL + ) + if not match: + raise ValueError("Could not find parse_data_packet()") + + body = match.group() + fields: list[DataPacketField] = [] + + # Match patterns like: range_q = _to_signed16(struct.unpack_from(">H", raw, 1)[0]) + for m in re.finditer( + r'(\w+)\s*=\s*_to_signed16\(struct\.unpack_from\("(>[HIBhib])", raw, (\d+)\)', + body + ): + name = m.group(1) + fmt = m.group(2) + offset = int(m.group(3)) + fmt_char = fmt[-1].upper() + size = {"H": 2, "I": 4, "B": 1}[fmt_char] + fields.append(DataPacketField( + name=name, byte_start=offset, + byte_end=offset + size - 1, + width_bits=size * 8 + )) + + # Match detection = raw[9] & 0x01 + for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body): + name = m.group(1) + offset = int(m.group(2)) + fields.append(DataPacketField( + name=name, byte_start=offset, byte_end=offset, width_bits=1 + )) + + fields.sort(key=lambda f: f.byte_start) + return fields + + +def parse_python_status_fields(filepath: Path | None = None) -> list[StatusWordField]: + """ + Extract bit shift/mask operations from parse_status_packet(). + Returns the fields with word index and bit positions as Python sees them. + """ + if filepath is None: + filepath = GUI_DIR / "radar_protocol.py" + text = filepath.read_text() + + match = re.search( + r'def parse_status_packet\(.*?\).*?(?=\n @|\n def |\nclass |\Z)', + text, re.DOTALL + ) + if not match: + raise ValueError("Could not find parse_status_packet()") + + body = match.group() + fields: list[StatusWordField] = [] + + # Pattern: sr.field = (words[N] >> S) & MASK # noqa: ERA001 + for m in re.finditer( + r'sr\.(\w+)\s*=\s*\(words\[(\d+)\]\s*>>\s*(\d+)\)\s*&\s*(0x[0-9a-fA-F]+|\d+)', + body + ): + name = m.group(1) + word_idx = int(m.group(2)) + shift = int(m.group(3)) + mask_str = m.group(4) + mask = int(mask_str, 16) if mask_str.startswith("0x") else int(mask_str) + width = mask.bit_length() + fields.append(StatusWordField( + name=name, word_index=word_idx, + msb=shift + width - 1, lsb=shift, width=width + )) + + # Pattern: sr.field = words[N] & MASK (no shift) + for m in re.finditer( + r'sr\.(\w+)\s*=\s*words\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', + body + ): + name = m.group(1) + word_idx = int(m.group(2)) + mask_str = m.group(3) + mask = int(mask_str, 16) if mask_str.startswith("0x") else int(mask_str) + width = mask.bit_length() + # Skip if already captured by the shift pattern + if not any(f.name == name and f.word_index == word_idx for f in fields): + fields.append(StatusWordField( + name=name, word_index=word_idx, + msb=width - 1, lsb=0, width=width + )) + + return fields + + +# =================================================================== +# Verilog layer parser +# =================================================================== + +def _parse_bit_slice(s: str) -> int: + """Parse '[15:0]' -> 16, '[0]' -> 1, '' -> 16 (full cmd_value).""" + m = re.match(r'\[(\d+):(\d+)\]', s) + if m: + return int(m.group(1)) - int(m.group(2)) + 1 + m = re.match(r'\[(\d+)\]', s) + if m: + return 1 + return 16 # default: full 16-bit cmd_value + + +def parse_verilog_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]: + """ + Parse the opcode case statement from radar_system_top.v. + Returns {opcode_value: OpcodeEntry}. + """ + if filepath is None: + filepath = FPGA_DIR / "radar_system_top.v" + text = filepath.read_text() + + # Find the command decode case block + # Pattern: case statement with 8'hXX opcodes + opcodes: dict[int, OpcodeEntry] = {} + + # Pattern 1: Simple assignment — 8'hXX: register <= rhs; + for m in re.finditer( + r"8'h([0-9a-fA-F]{2})\s*:\s*(\w+)\s*<=\s*(.*?)(?:;|$)", + text, re.MULTILINE + ): + value = int(m.group(1), 16) + register = m.group(2) + rhs = m.group(3).strip() + + # Determine if it's a pulse (assigned literal 1) + is_pulse = rhs in ("1", "1'b1") + + # Extract bit slice from the RHS (e.g., usb_cmd_value[3:0]) + bit_slice = "" + slice_m = re.search(r'usb_cmd_value(\[\d+(?::\d+)?\])', rhs) + if slice_m: + bit_slice = slice_m.group(1) + elif "usb_cmd_value" in rhs: + bit_slice = "[15:0]" # full width + + bit_width = _parse_bit_slice(bit_slice) if bit_slice else 0 + + opcodes[value] = OpcodeEntry( + name=register, + value=value, + register=register, + bit_slice=bit_slice, + bit_width=bit_width, + is_pulse=is_pulse, + ) + + # Pattern 2: begin...end blocks — 8'hXX: begin ... register <= ... end + # These are used for opcodes with validation logic (e.g., 0x15 clamp) + for m in re.finditer( + r"8'h([0-9a-fA-F]{2})\s*:\s*begin\b(.*?)end\b", + text, re.DOTALL + ): + value = int(m.group(1), 16) + if value in opcodes: + continue # Already captured by pattern 1 + body = m.group(2) + + # Find the first register assignment (host_xxx <=) + assign_m = re.search(r'(host_\w+)\s*<=\s*(.+?);', body) + if not assign_m: + continue + + register = assign_m.group(1) + rhs = assign_m.group(2).strip() + + bit_slice = "" + slice_m = re.search(r'usb_cmd_value(\[\d+(?::\d+)?\])', body) + if slice_m: + bit_slice = slice_m.group(1) + elif "usb_cmd_value" in body: + bit_slice = "[15:0]" + + bit_width = _parse_bit_slice(bit_slice) if bit_slice else 0 + + opcodes[value] = OpcodeEntry( + name=register, + value=value, + register=register, + bit_slice=bit_slice, + bit_width=bit_width, + is_pulse=False, + ) + + return opcodes + + +def parse_verilog_reset_defaults(filepath: Path | None = None) -> dict[str, int]: + """ + Parse the reset block from radar_system_top.v. + Returns {register_name: reset_value}. + """ + if filepath is None: + filepath = FPGA_DIR / "radar_system_top.v" + text = filepath.read_text() + + defaults: dict[str, int] = {} + + # Match patterns like: host_radar_mode <= 2'b01; + # Also: host_detect_threshold <= 16'd10000; + for m in re.finditer( + r'(host_\w+)\s*<=\s*(\d+\'[bdho][0-9a-fA-F_]+|\d+)\s*;', + text + ): + reg = m.group(1) + val_str = m.group(2) + + # Parse Verilog literal + if "'" in val_str: + base_char = val_str.split("'")[1][0].lower() + digits = val_str.split("'")[1][1:].replace("_", "") + base = {"b": 2, "d": 10, "h": 16, "o": 8}[base_char] + value = int(digits, base) + else: + value = int(val_str) + + # Only keep first occurrence (the reset block comes before the + # opcode decode which also has <= assignments) + if reg not in defaults: + defaults[reg] = value + + return defaults + + +def parse_verilog_register_widths(filepath: Path | None = None) -> dict[str, int]: + """ + Parse register declarations from radar_system_top.v. + Returns {register_name: bit_width}. + """ + if filepath is None: + filepath = FPGA_DIR / "radar_system_top.v" + text = filepath.read_text() + + widths: dict[str, int] = {} + + # Match: reg [15:0] host_detect_threshold; + # Also: reg host_trigger_pulse; + for m in re.finditer( + r'reg\s+(?:\[\s*(\d+)\s*:\s*(\d+)\s*\]\s+)?(host_\w+)\s*;', + text + ): + width = int(m.group(1)) - int(m.group(2)) + 1 if m.group(1) is not None else 1 + widths[m.group(3)] = width + + return widths + + +def parse_verilog_packet_constants( + filepath: Path | None = None, +) -> dict[str, PacketConstants]: + """Extract HEADER, FOOTER, STATUS_HEADER, packet size localparams.""" + if filepath is None: + filepath = FPGA_DIR / "usb_data_interface_ft2232h.v" + text = filepath.read_text() + + def _find(pattern: str) -> int: + m = re.search(pattern, text) + if not m: + raise ValueError(f"Pattern not found in {filepath}: {pattern}") + val = m.group(1) + # Parse Verilog literals: 8'hAA → 0xAA, 5'd11 → 11 + vlog_m = re.match(r"\d+'h([0-9a-fA-F]+)", val) + if vlog_m: + return int(vlog_m.group(1), 16) + vlog_m = re.match(r"\d+'d(\d+)", val) + if vlog_m: + return int(vlog_m.group(1)) + return int(val, 16) if val.startswith("0x") else int(val) + + header_val = _find(r"localparam\s+HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)") + footer_val = _find(r"localparam\s+FOOTER\s*=\s*(\d+'h[0-9a-fA-F]+)") + status_hdr = _find(r"localparam\s+STATUS_HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)") + + data_size = _find(r"DATA_PKT_LEN\s*=\s*(\d+'d\d+)") + status_size = _find(r"STATUS_PKT_LEN\s*=\s*(\d+'d\d+)") + + return { + "data": PacketConstants(header=header_val, footer=footer_val, size=data_size), + "status": PacketConstants(header=status_hdr, footer=footer_val, size=status_size), + } + + +def count_concat_bits(concat_expr: str, port_widths: dict[str, int]) -> ConcatWidth: + """ + Count total bits in a Verilog concatenation expression like: + {8'hFF, 3'b000, status_radar_mode, 5'b00000, status_stream_ctrl, status_cfar_threshold} + + Uses port_widths to resolve signal widths. Returns ConcatWidth. + """ + # Remove outer braces + inner = concat_expr.strip().strip("{}") + fragments: list[tuple[str, int]] = [] + total = 0 + + for part in re.split(r',\s*', inner): + part = part.strip() + if not part: + continue + + # Literal: N'bXXX, N'dXXX, N'hXX, or just a decimal + lit_match = re.match(r"(\d+)'[bdhoBDHO]", part) + if lit_match: + w = int(lit_match.group(1)) + fragments.append((part, w)) + total += w + continue + + # Signal with bit select: sig[M:N] or sig[N] + sel_match = re.match(r'(\w+)\[(\d+):(\d+)\]', part) + if sel_match: + w = int(sel_match.group(2)) - int(sel_match.group(3)) + 1 + fragments.append((part, w)) + total += w + continue + + sel_match = re.match(r'(\w+)\[(\d+)\]', part) + if sel_match: + fragments.append((part, 1)) + total += 1 + continue + + # Bare signal: look up in port_widths + if part in port_widths: + w = port_widths[part] + fragments.append((part, w)) + total += w + else: + # Unknown width — flag it + fragments.append((part, -1)) + total = -1 # Can't compute + + return ConcatWidth( + total_bits=total, + target_bits=32, + fragments=fragments, + truncated=total > 32 if total > 0 else False, + ) + + +def parse_verilog_status_word_concats( + filepath: Path | None = None, +) -> dict[int, str]: + """ + Extract the raw concatenation expression for each status_words[N] assignment. + Returns {word_index: concat_expression_string}. + """ + if filepath is None: + filepath = FPGA_DIR / "usb_data_interface_ft2232h.v" + text = filepath.read_text() + + results: dict[int, str] = {} + + # Multi-line concat: status_words[N] <= {... }; + # We need to handle multi-line expressions + for m in re.finditer( + r'status_words\[(\d+)\]\s*<=\s*(\{[^;]+\})\s*;', + text, re.DOTALL + ): + idx = int(m.group(1)) + expr = m.group(2) + # Strip single-line comments before normalizing whitespace + expr = re.sub(r'//[^\n]*', '', expr) + # Normalize whitespace + expr = re.sub(r'\s+', ' ', expr).strip() + results[idx] = expr + + return results + + +def get_usb_interface_port_widths(filepath: Path | None = None) -> dict[str, int]: + """ + Parse port declarations from usb_data_interface_ft2232h.v module header. + Returns {port_name: bit_width}. + """ + if filepath is None: + filepath = FPGA_DIR / "usb_data_interface_ft2232h.v" + text = filepath.read_text() + + widths: dict[str, int] = {} + + # Match: input wire [15:0] status_cfar_threshold, + # Also: input wire status_self_test_busy + for m in re.finditer( + r'(?:input|output)\s+(?:wire|reg)\s+(?:\[\s*(\d+)\s*:\s*(\d+)\s*\]\s+)?(\w+)', + text + ): + width = int(m.group(1)) - int(m.group(2)) + 1 if m.group(1) is not None else 1 + widths[m.group(3)] = width + + return widths + + +def parse_verilog_data_mux( + filepath: Path | None = None, +) -> list[DataPacketField]: + """ + Parse the data_pkt_byte mux from usb_data_interface_ft2232h.v. + Returns fields with byte positions and signal names. + """ + if filepath is None: + filepath = FPGA_DIR / "usb_data_interface_ft2232h.v" + text = filepath.read_text() + + # Find the data mux case block + match = re.search( + r'always\s+@\(\*\)\s+begin\s+case\s*\(wr_byte_idx\)(.*?)endcase', + text, re.DOTALL + ) + if not match: + raise ValueError("Could not find data_pkt_byte mux") + + mux_body = match.group(1) + entries: list[tuple[int, str]] = [] + + for m in re.finditer( + r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);", + mux_body + ): + idx = int(m.group(1)) + expr = m.group(2).strip() + entries.append((idx, expr)) + + # Group consecutive bytes by signal root name + fields: list[DataPacketField] = [] + i = 0 + while i < len(entries): + idx, expr = entries[i] + if expr == "HEADER" or expr == "FOOTER": + i += 1 + continue + + # Extract signal name (e.g., range_profile_cap from range_profile_cap[31:24]) + sig_match = re.match(r'(\w+?)(?:\[|$)', expr) + if not sig_match: + i += 1 + continue + + signal = sig_match.group(1) + start_byte = idx + end_byte = idx + + # Find consecutive bytes of the same signal + j = i + 1 + while j < len(entries): + next_idx, next_expr = entries[j] + if next_expr.startswith(signal): + end_byte = next_idx + j += 1 + else: + break + + n_bytes = end_byte - start_byte + 1 + fields.append(DataPacketField( + name=signal.replace("_cap", ""), + byte_start=start_byte, + byte_end=end_byte, + width_bits=n_bytes * 8, + )) + i = j + + return fields + + +# =================================================================== +# STM32 / C layer parser +# =================================================================== + +def parse_stm32_settings_fields( + filepath: Path | None = None, +) -> list[SettingsField]: + """ + Parse RadarSettings::parseFromUSB to extract field order, offsets, types. + """ + if filepath is None: + filepath = MCU_LIB_DIR / "RadarSettings.cpp" + + if not filepath.exists(): + return [] # MCU code not available (CI might not have it) + + text = filepath.read_text(encoding="latin-1") + + fields: list[SettingsField] = [] + + # Look for memcpy + shift patterns that extract doubles and uint32s + # Pattern for doubles: loop reading 8 bytes big-endian + # Pattern for uint32: 4 bytes big-endian + # We'll parse the assignment targets in order + + # Find the parseFromUSB function + match = re.search( + r'parseFromUSB\s*\(.*?\)\s*\{(.*?)^\}', + text, re.DOTALL | re.MULTILINE + ) + if not match: + return fields + + body = match.group(1) + + # The fields are extracted sequentially from the payload. + # Look for variable assignments that follow the memcpy/extraction pattern. + # Based on known code: extractDouble / extractUint32 patterns + field_names = [ + ("system_frequency", 8, "double"), + ("chirp_duration_1", 8, "double"), + ("chirp_duration_2", 8, "double"), + ("chirps_per_position", 4, "uint32_t"), + ("freq_min", 8, "double"), + ("freq_max", 8, "double"), + ("prf1", 8, "double"), + ("prf2", 8, "double"), + ("max_distance", 8, "double"), + ("map_size", 8, "double"), + ] + + offset = 0 + for name, size, ctype in field_names: + # Verify the field name appears in the function body + if name in body or name.replace("_", "") in body.lower(): + fields.append(SettingsField( + name=name, offset=offset, size=size, c_type=ctype + )) + offset += size + + return fields + + +def parse_stm32_start_flag( + filepath: Path | None = None, +) -> list[int]: + """Parse the USB start flag bytes from USBHandler.cpp.""" + if filepath is None: + filepath = MCU_LIB_DIR / "USBHandler.cpp" + + if not filepath.exists(): + return [] + + text = filepath.read_text() + + # Look for the start flag array, e.g. {23, 46, 158, 237} + match = re.search(r'start_flag.*?=\s*\{([^}]+)\}', text, re.DOTALL) + if not match: + # Try alternate patterns + match = re.search(r'\{(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*\d+\s*)\}', text) + if not match: + return [] + + return [int(x.strip()) for x in match.group(1).split(",") if x.strip().isdigit()] + + +# =================================================================== +# GPIO parser +# =================================================================== + +def parse_xdc_gpio_pins(filepath: Path | None = None) -> list[GpioPin]: + """Parse XDC constraints for DIG_* pin assignments.""" + if filepath is None: + filepath = XDC_DIR / "xc7a50t_ftg256.xdc" + + if not filepath.exists(): + return [] + + text = filepath.read_text() + pins: list[GpioPin] = [] + + # Match: set_property PACKAGE_PIN XX [get_ports {signal_name}] + for m in re.finditer( + r'set_property\s+PACKAGE_PIN\s+(\w+)\s+\[get_ports\s+\{?(\w+)\}?\]', + text + ): + pin = m.group(1) + signal = m.group(2) + if any(kw in signal for kw in ("stm32_", "reset_n", "dig_")): + # Determine direction from signal name + if signal in ("stm32_new_chirp", "stm32_new_elevation", + "stm32_new_azimuth", "stm32_mixers_enable"): + direction = "input" # FPGA receives these + elif signal == "reset_n": + direction = "input" + else: + direction = "unknown" + pins.append(GpioPin( + name=signal, pin_id=pin, direction=direction, layer="fpga" + )) + + return pins + + +def parse_stm32_gpio_init(filepath: Path | None = None) -> list[GpioPin]: + """Parse STM32 GPIO initialization for PD8-PD15 directions.""" + if filepath is None: + filepath = MCU_CODE_DIR / "main.cpp" + + if not filepath.exists(): + return [] + + text = filepath.read_text() + pins: list[GpioPin] = [] + + # Look for GPIO_InitStruct.Pin and GPIO_InitStruct.Mode patterns + # This is approximate — STM32 HAL GPIO init is complex + # Look for PD8-PD15 configuration (output vs input) + + # Pattern: GPIO_PIN_8 | GPIO_PIN_9 ... with Mode = OUTPUT + # We'll find blocks that configure GPIOD pins + for m in re.finditer( + r'GPIO_InitStruct\.Pin\s*=\s*([^;]+);.*?' + r'GPIO_InitStruct\.Mode\s*=\s*(\w+)', + text, re.DOTALL + ): + pin_expr = m.group(1) + mode = m.group(2) + + direction = "output" if "OUTPUT" in mode else "input" + + # Extract individual pin numbers + for pin_m in re.finditer(r'GPIO_PIN_(\d+)', pin_expr): + pin_num = int(pin_m.group(1)) + if 8 <= pin_num <= 15: + pins.append(GpioPin( + name=f"PD{pin_num}", + pin_id=f"PD{pin_num}", + direction=direction, + layer="stm32" + )) + + return pins diff --git a/9_Firmware/tests/cross_layer/stm32_settings_stub.cpp b/9_Firmware/tests/cross_layer/stm32_settings_stub.cpp new file mode 100644 index 0000000..853b327 --- /dev/null +++ b/9_Firmware/tests/cross_layer/stm32_settings_stub.cpp @@ -0,0 +1,86 @@ +/** + * stm32_settings_stub.cpp + * + * Standalone stub that wraps the real RadarSettings class. + * Reads a binary settings packet from a file (argv[1]), + * parses it using RadarSettings::parseFromUSB(), and prints + * all parsed field=value pairs to stdout. + * + * Compile: c++ -std=c++11 -o stm32_settings_stub stm32_settings_stub.cpp \ + * ../../9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/RadarSettings.cpp \ + * -I../../9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/ + * + * Usage: ./stm32_settings_stub packet.bin + * Prints: field=value lines (one per field) + * Exit code: 0 if parse succeeded, 1 if failed + */ + +#include "RadarSettings.h" +#include +#include + +int main(int argc, char* argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 2; + } + + // Read binary packet from file + FILE* f = fopen(argv[1], "rb"); + if (!f) { + fprintf(stderr, "ERROR: Cannot open %s\n", argv[1]); + return 2; + } + + fseek(f, 0, SEEK_END); + long file_size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (file_size <= 0 || file_size > 4096) { + fprintf(stderr, "ERROR: Invalid file size %ld\n", file_size); + fclose(f); + return 2; + } + + uint8_t* buf = (uint8_t*)malloc(file_size); + if (!buf) { + fprintf(stderr, "ERROR: malloc failed\n"); + fclose(f); + return 2; + } + + size_t nread = fread(buf, 1, file_size, f); + fclose(f); + + if ((long)nread != file_size) { + fprintf(stderr, "ERROR: Short read (%zu of %ld)\n", nread, file_size); + free(buf); + return 2; + } + + // Parse using the real RadarSettings class + RadarSettings settings; + bool ok = settings.parseFromUSB(buf, (uint32_t)file_size); + free(buf); + + if (!ok) { + printf("parse_ok=false\n"); + return 1; + } + + // Print all fields with full precision + // Python orchestrator will compare these against expected values + printf("parse_ok=true\n"); + printf("system_frequency=%.17g\n", settings.getSystemFrequency()); + printf("chirp_duration_1=%.17g\n", settings.getChirpDuration1()); + printf("chirp_duration_2=%.17g\n", settings.getChirpDuration2()); + printf("chirps_per_position=%u\n", settings.getChirpsPerPosition()); + printf("freq_min=%.17g\n", settings.getFreqMin()); + printf("freq_max=%.17g\n", settings.getFreqMax()); + printf("prf1=%.17g\n", settings.getPRF1()); + printf("prf2=%.17g\n", settings.getPRF2()); + printf("max_distance=%.17g\n", settings.getMaxDistance()); + printf("map_size=%.17g\n", settings.getMapSize()); + + return 0; +} diff --git a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v new file mode 100644 index 0000000..94d0a82 --- /dev/null +++ b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v @@ -0,0 +1,714 @@ +`timescale 1ns / 1ps + +/** + * tb_cross_layer_ft2232h.v + * + * Cross-layer contract testbench for the FT2232H USB interface. + * Exercises three packet types with known distinctive values and dumps + * captured bytes to text files that the Python orchestrator can parse. + * + * Exercise A: Command round-trip (Host -> FPGA) + * - Send every opcode through the 4-byte read FSM + * - Dump cmd_opcode, cmd_addr, cmd_value to cmd_results.txt + * + * Exercise B: Data packet generation (FPGA -> Host) + * - Inject known range/doppler/cfar values + * - Capture all 11 output bytes + * - Dump to data_packet.txt + * + * Exercise C: Status packet generation (FPGA -> Host) + * - Set all status inputs to known non-zero values + * - Trigger status request + * - Capture all 26 output bytes + * - Dump to status_packet.txt + */ + +module tb_cross_layer_ft2232h; + + // Clock periods + localparam CLK_PERIOD = 10.0; // 100 MHz system clock + localparam FT_CLK_PERIOD = 16.67; // 60 MHz FT2232H clock + + // ---- Signals ---- + reg clk; + reg reset_n; + reg ft_reset_n; + + // Radar data inputs + reg [31:0] range_profile; + reg range_valid; + reg [15:0] doppler_real; + reg [15:0] doppler_imag; + reg doppler_valid; + reg cfar_detection; + reg cfar_valid; + + // FT2232H physical interface + wire [7:0] ft_data; + reg ft_rxf_n; + reg ft_txe_n; + wire ft_rd_n; + wire ft_wr_n; + wire ft_oe_n; + wire ft_siwu; + reg ft_clk; + + // Host-side bus driver (for command injection) + reg [7:0] host_data_drive; + reg host_data_drive_en; + assign ft_data = host_data_drive_en ? host_data_drive : 8'hZZ; + + // Pulldown to avoid X during idle + pulldown pd[7:0] (ft_data); + + // DUT command outputs + wire [31:0] cmd_data; + wire cmd_valid; + wire [7:0] cmd_opcode; + wire [7:0] cmd_addr; + wire [15:0] cmd_value; + + // Stream control + reg [2:0] stream_control; + + // Status inputs + reg status_request; + reg [15:0] status_cfar_threshold; + reg [2:0] status_stream_ctrl; + reg [1:0] status_radar_mode; + reg [15:0] status_long_chirp; + reg [15:0] status_long_listen; + reg [15:0] status_guard; + reg [15:0] status_short_chirp; + reg [15:0] status_short_listen; + reg [5:0] status_chirps_per_elev; + reg [1:0] status_range_mode; + reg [4:0] status_self_test_flags; + reg [7:0] status_self_test_detail; + reg status_self_test_busy; + reg [3:0] status_agc_current_gain; + reg [7:0] status_agc_peak_magnitude; + reg [7:0] status_agc_saturation_count; + reg status_agc_enable; + + // ---- Clock generators ---- + always #(CLK_PERIOD / 2) clk = ~clk; + always #(FT_CLK_PERIOD / 2) ft_clk = ~ft_clk; + + // ---- DUT instantiation ---- + usb_data_interface_ft2232h uut ( + .clk (clk), + .reset_n (reset_n), + .ft_reset_n (ft_reset_n), + .range_profile (range_profile), + .range_valid (range_valid), + .doppler_real (doppler_real), + .doppler_imag (doppler_imag), + .doppler_valid (doppler_valid), + .cfar_detection (cfar_detection), + .cfar_valid (cfar_valid), + .ft_data (ft_data), + .ft_rxf_n (ft_rxf_n), + .ft_txe_n (ft_txe_n), + .ft_rd_n (ft_rd_n), + .ft_wr_n (ft_wr_n), + .ft_oe_n (ft_oe_n), + .ft_siwu (ft_siwu), + .ft_clk (ft_clk), + .cmd_data (cmd_data), + .cmd_valid (cmd_valid), + .cmd_opcode (cmd_opcode), + .cmd_addr (cmd_addr), + .cmd_value (cmd_value), + .stream_control (stream_control), + .status_request (status_request), + .status_cfar_threshold (status_cfar_threshold), + .status_stream_ctrl (status_stream_ctrl), + .status_radar_mode (status_radar_mode), + .status_long_chirp (status_long_chirp), + .status_long_listen (status_long_listen), + .status_guard (status_guard), + .status_short_chirp (status_short_chirp), + .status_short_listen (status_short_listen), + .status_chirps_per_elev (status_chirps_per_elev), + .status_range_mode (status_range_mode), + .status_self_test_flags (status_self_test_flags), + .status_self_test_detail(status_self_test_detail), + .status_self_test_busy (status_self_test_busy), + .status_agc_current_gain (status_agc_current_gain), + .status_agc_peak_magnitude (status_agc_peak_magnitude), + .status_agc_saturation_count(status_agc_saturation_count), + .status_agc_enable (status_agc_enable) + ); + + // ---- Test bookkeeping ---- + integer pass_count; + integer fail_count; + integer test_num; + integer cmd_file; + integer data_file; + integer status_file; + + // ---- Check task ---- + task check; + input cond; + input [511:0] label; + begin + test_num = test_num + 1; + if (cond) begin + $display("[PASS] Test %0d: %0s", test_num, label); + pass_count = pass_count + 1; + end else begin + $display("[FAIL] Test %0d: %0s", test_num, label); + fail_count = fail_count + 1; + end + end + endtask + + // ---- Helper: apply reset ---- + task apply_reset; + begin + reset_n = 0; + ft_reset_n = 0; + range_profile = 32'h0; + range_valid = 0; + doppler_real = 16'h0; + doppler_imag = 16'h0; + doppler_valid = 0; + cfar_detection = 0; + cfar_valid = 0; + ft_rxf_n = 1; // No host data available + ft_txe_n = 0; // TX FIFO ready + host_data_drive = 8'h0; + host_data_drive_en = 0; + stream_control = 3'b111; + status_request = 0; + status_cfar_threshold = 16'd0; + status_stream_ctrl = 3'b000; + status_radar_mode = 2'b00; + status_long_chirp = 16'd0; + status_long_listen = 16'd0; + status_guard = 16'd0; + status_short_chirp = 16'd0; + status_short_listen = 16'd0; + status_chirps_per_elev = 6'd0; + status_range_mode = 2'b00; + status_self_test_flags = 5'b00000; + status_self_test_detail = 8'd0; + status_self_test_busy = 1'b0; + status_agc_current_gain = 4'd0; + status_agc_peak_magnitude = 8'd0; + status_agc_saturation_count = 8'd0; + status_agc_enable = 1'b0; + repeat (6) @(posedge ft_clk); + reset_n = 1; + ft_reset_n = 1; + // Wait for stream_control CDC to propagate + repeat (8) @(posedge ft_clk); + end + endtask + + // ---- Helper: send one 4-byte command via FT2232H read path ---- + // + // FT2232H read FSM cycle-by-cycle: + // Cycle 0 (RD_IDLE): sees !ft_rxf_n → ft_oe_n<=0, → RD_OE_ASSERT + // Cycle 1 (RD_OE_ASSERT): sees !ft_rxf_n → ft_rd_n<=0, → RD_READING + // Cycle 2 (RD_READING): samples ft_data=byte0, cnt 0→1 + // Cycle 3 (RD_READING): samples ft_data=byte1, cnt 1→2 + // Cycle 4 (RD_READING): samples ft_data=byte2, cnt 2→3 + // Cycle 5 (RD_READING): samples ft_data=byte3, cnt=3→0, → RD_DEASSERT + // Cycle 6 (RD_DEASSERT): ft_oe_n<=1, → RD_PROCESS + // Cycle 7 (RD_PROCESS): cmd_valid<=1, decode, → RD_IDLE + // + // Data must be stable BEFORE the sampling posedge. We use #1 after + // posedge to change data in the "delta after" region. + task send_command_ft2232h; + input [7:0] byte0; // opcode + input [7:0] byte1; // addr + input [7:0] byte2; // value_hi + input [7:0] byte3; // value_lo + begin + // Pre-drive byte0 and signal data available + @(posedge ft_clk); #1; + host_data_drive = byte0; + host_data_drive_en = 1; + ft_rxf_n = 0; + + // Cycle 0: RD_IDLE sees !ft_rxf_n, goes to OE_ASSERT + @(posedge ft_clk); #1; + + // Cycle 1: RD_OE_ASSERT, ft_rd_n goes low, goes to RD_READING + @(posedge ft_clk); #1; + + // Cycle 2: RD_READING, byte0 is sampled, cnt 0→1 + // Now change to byte1 for next sample + @(posedge ft_clk); #1; + host_data_drive = byte1; + + // Cycle 3: RD_READING, byte1 is sampled, cnt 1→2 + @(posedge ft_clk); #1; + host_data_drive = byte2; + + // Cycle 4: RD_READING, byte2 is sampled, cnt 2→3 + @(posedge ft_clk); #1; + host_data_drive = byte3; + + // Cycle 5: RD_READING, byte3 is sampled, cnt=3, → RD_DEASSERT + @(posedge ft_clk); #1; + + // Cycle 6: RD_DEASSERT, ft_oe_n←1, → RD_PROCESS + @(posedge ft_clk); #1; + + // Cycle 7: RD_PROCESS, cmd decoded, cmd_valid←1, → RD_IDLE + @(posedge ft_clk); #1; + + // cmd_valid was asserted at cycle 7's posedge. cmd_opcode/addr/value + // are now valid (registered outputs hold until next RD_PROCESS). + + // Release bus + host_data_drive_en = 0; + host_data_drive = 8'h0; + ft_rxf_n = 1; + + // Settle + repeat (2) @(posedge ft_clk); + end + endtask + + // ---- Helper: capture N write bytes from the DUT ---- + // Monitors ft_wr_n and ft_data_out, captures bytes into array. + // Used for data packets (11 bytes) and status packets (26 bytes). + reg [7:0] captured_bytes [0:31]; + integer capture_count; + + task capture_write_bytes; + input integer expected_count; + integer timeout; + begin + capture_count = 0; + timeout = 0; + + while (capture_count < expected_count && timeout < 2000) begin + @(posedge ft_clk); #1; + timeout = timeout + 1; + // DUT drives byte when ft_wr_n=0 and ft_data_oe=1 + // Sample AFTER posedge so registered outputs are settled + if (!ft_wr_n && uut.ft_data_oe) begin + captured_bytes[capture_count] = uut.ft_data_out; + capture_count = capture_count + 1; + end + end + end + endtask + + // ---- Helper: pulse range_valid with CDC wait ---- + // Toggle CDC needs 3 sync stages + edge detect = 4+ ft_clk cycles. + // Use 12 for safety margin. + task assert_range_valid; + input [31:0] data; + begin + @(posedge clk); #1; + range_profile = data; + range_valid = 1; + @(posedge clk); #1; + range_valid = 0; + // Wait for toggle CDC propagation + repeat (12) @(posedge ft_clk); + end + endtask + + // ---- Helper: pulse doppler_valid ---- + task pulse_doppler; + input [15:0] dr; + input [15:0] di; + begin + @(posedge clk); #1; + doppler_real = dr; + doppler_imag = di; + doppler_valid = 1; + @(posedge clk); #1; + doppler_valid = 0; + repeat (12) @(posedge ft_clk); + end + endtask + + // ---- Helper: pulse cfar_valid ---- + task pulse_cfar; + input det; + begin + @(posedge clk); #1; + cfar_detection = det; + cfar_valid = 1; + @(posedge clk); #1; + cfar_valid = 0; + repeat (12) @(posedge ft_clk); + end + endtask + + // ---- Helper: pulse status_request ---- + task pulse_status_request; + begin + @(posedge clk); #1; + status_request = 1; + @(posedge clk); #1; + status_request = 0; + // Wait for toggle CDC propagation + repeat (12) @(posedge ft_clk); + end + endtask + + // ================================================================ + // Main stimulus + // ================================================================ + integer i; + + initial begin + $dumpfile("tb_cross_layer_ft2232h.vcd"); + $dumpvars(0, tb_cross_layer_ft2232h); + + clk = 0; + ft_clk = 0; + pass_count = 0; + fail_count = 0; + test_num = 0; + + // ============================================================ + // EXERCISE A: Command Round-Trip + // Send commands with known opcode/addr/value, verify decoding. + // Dump results to cmd_results.txt for Python validation. + // ============================================================ + $display("\n=== EXERCISE A: Command Round-Trip ==="); + apply_reset; + + cmd_file = $fopen("cmd_results.txt", "w"); + $fwrite(cmd_file, "# opcode_sent addr_sent value_sent opcode_got addr_got value_got\n"); + + // Test all real opcodes from radar_system_top.v + // Format: opcode, addr=0x00, value + + // Basic control + send_command_ft2232h(8'h01, 8'h00, 8'h00, 8'h02); // RADAR_MODE=2 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h01, 8'h00, 16'h0002, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h01 && cmd_value === 16'h0002, + "Cmd 0x01: RADAR_MODE=2"); + + send_command_ft2232h(8'h02, 8'h00, 8'h00, 8'h01); // TRIGGER_PULSE + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h02, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h02 && cmd_value === 16'h0001, + "Cmd 0x02: TRIGGER_PULSE"); + + send_command_ft2232h(8'h03, 8'h00, 8'h27, 8'h10); // DETECT_THRESHOLD=10000 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h03, 8'h00, 16'h2710, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h03 && cmd_value === 16'h2710, + "Cmd 0x03: DETECT_THRESHOLD=10000"); + + send_command_ft2232h(8'h04, 8'h00, 8'h00, 8'h07); // STREAM_CONTROL=7 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h04, 8'h00, 16'h0007, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h04 && cmd_value === 16'h0007, + "Cmd 0x04: STREAM_CONTROL=7"); + + // Chirp timing + send_command_ft2232h(8'h10, 8'h00, 8'h0B, 8'hB8); // LONG_CHIRP=3000 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h10, 8'h00, 16'h0BB8, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h10 && cmd_value === 16'h0BB8, + "Cmd 0x10: LONG_CHIRP=3000"); + + send_command_ft2232h(8'h11, 8'h00, 8'h35, 8'h84); // LONG_LISTEN=13700 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h11, 8'h00, 16'h3584, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h11 && cmd_value === 16'h3584, + "Cmd 0x11: LONG_LISTEN=13700"); + + send_command_ft2232h(8'h12, 8'h00, 8'h44, 8'h84); // GUARD=17540 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h12, 8'h00, 16'h4484, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h12 && cmd_value === 16'h4484, + "Cmd 0x12: GUARD=17540"); + + send_command_ft2232h(8'h13, 8'h00, 8'h00, 8'h32); // SHORT_CHIRP=50 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h13, 8'h00, 16'h0032, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h13 && cmd_value === 16'h0032, + "Cmd 0x13: SHORT_CHIRP=50"); + + send_command_ft2232h(8'h14, 8'h00, 8'h44, 8'h2A); // SHORT_LISTEN=17450 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h14, 8'h00, 16'h442A, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h14 && cmd_value === 16'h442A, + "Cmd 0x14: SHORT_LISTEN=17450"); + + send_command_ft2232h(8'h15, 8'h00, 8'h00, 8'h20); // CHIRPS_PER_ELEV=32 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h15, 8'h00, 16'h0020, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h15 && cmd_value === 16'h0020, + "Cmd 0x15: CHIRPS_PER_ELEV=32"); + + // Digital gain + send_command_ft2232h(8'h16, 8'h00, 8'h00, 8'h05); // GAIN_SHIFT=5 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h16, 8'h00, 16'h0005, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h16 && cmd_value === 16'h0005, + "Cmd 0x16: GAIN_SHIFT=5"); + + // Signal processing + send_command_ft2232h(8'h20, 8'h00, 8'h00, 8'h01); // RANGE_MODE=1 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h20, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h20 && cmd_value === 16'h0001, + "Cmd 0x20: RANGE_MODE=1"); + + send_command_ft2232h(8'h21, 8'h00, 8'h00, 8'h03); // CFAR_GUARD=3 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h21, 8'h00, 16'h0003, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h21 && cmd_value === 16'h0003, + "Cmd 0x21: CFAR_GUARD=3"); + + send_command_ft2232h(8'h22, 8'h00, 8'h00, 8'h0C); // CFAR_TRAIN=12 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h22, 8'h00, 16'h000C, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h22 && cmd_value === 16'h000C, + "Cmd 0x22: CFAR_TRAIN=12"); + + send_command_ft2232h(8'h23, 8'h00, 8'h00, 8'h30); // CFAR_ALPHA=0x30 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h23, 8'h00, 16'h0030, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h23 && cmd_value === 16'h0030, + "Cmd 0x23: CFAR_ALPHA=0x30"); + + send_command_ft2232h(8'h24, 8'h00, 8'h00, 8'h01); // CFAR_MODE=1 (GO) + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h24, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h24 && cmd_value === 16'h0001, + "Cmd 0x24: CFAR_MODE=1"); + + send_command_ft2232h(8'h25, 8'h00, 8'h00, 8'h01); // CFAR_ENABLE=1 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h25, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h25 && cmd_value === 16'h0001, + "Cmd 0x25: CFAR_ENABLE=1"); + + send_command_ft2232h(8'h26, 8'h00, 8'h00, 8'h01); // MTI_ENABLE=1 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h26, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h26 && cmd_value === 16'h0001, + "Cmd 0x26: MTI_ENABLE=1"); + + send_command_ft2232h(8'h27, 8'h00, 8'h00, 8'h03); // DC_NOTCH_WIDTH=3 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h27, 8'h00, 16'h0003, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h27 && cmd_value === 16'h0003, + "Cmd 0x27: DC_NOTCH_WIDTH=3"); + + // AGC registers (0x28-0x2C) + send_command_ft2232h(8'h28, 8'h00, 8'h00, 8'h01); // AGC_ENABLE=1 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h28, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h28 && cmd_value === 16'h0001, + "Cmd 0x28: AGC_ENABLE=1"); + + send_command_ft2232h(8'h29, 8'h00, 8'h00, 8'hC8); // AGC_TARGET=200 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h29, 8'h00, 16'h00C8, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h29 && cmd_value === 16'h00C8, + "Cmd 0x29: AGC_TARGET=200"); + + send_command_ft2232h(8'h2A, 8'h00, 8'h00, 8'h02); // AGC_ATTACK=2 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h2A, 8'h00, 16'h0002, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h2A && cmd_value === 16'h0002, + "Cmd 0x2A: AGC_ATTACK=2"); + + send_command_ft2232h(8'h2B, 8'h00, 8'h00, 8'h03); // AGC_DECAY=3 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h2B, 8'h00, 16'h0003, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h2B && cmd_value === 16'h0003, + "Cmd 0x2B: AGC_DECAY=3"); + + send_command_ft2232h(8'h2C, 8'h00, 8'h00, 8'h06); // AGC_HOLDOFF=6 + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h2C, 8'h00, 16'h0006, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h2C && cmd_value === 16'h0006, + "Cmd 0x2C: AGC_HOLDOFF=6"); + + // Self-test / status + send_command_ft2232h(8'h30, 8'h00, 8'h00, 8'h01); // SELF_TEST_TRIGGER + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h30, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h30 && cmd_value === 16'h0001, + "Cmd 0x30: SELF_TEST_TRIGGER"); + + send_command_ft2232h(8'h31, 8'h00, 8'h00, 8'h01); // SELF_TEST_STATUS + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h31, 8'h00, 16'h0001, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h31 && cmd_value === 16'h0001, + "Cmd 0x31: SELF_TEST_STATUS"); + + send_command_ft2232h(8'hFF, 8'h00, 8'h00, 8'h00); // STATUS_REQUEST + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'hFF, 8'h00, 16'h0000, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'hFF && cmd_value === 16'h0000, + "Cmd 0xFF: STATUS_REQUEST"); + + // Non-zero addr test + send_command_ft2232h(8'h01, 8'hAB, 8'hCD, 8'hEF); // addr=0xAB, value=0xCDEF + $fwrite(cmd_file, "%02x %02x %04x %02x %02x %04x\n", + 8'h01, 8'hAB, 16'hCDEF, cmd_opcode, cmd_addr, cmd_value); + check(cmd_opcode === 8'h01 && cmd_addr === 8'hAB && cmd_value === 16'hCDEF, + "Cmd 0x01 with addr=0xAB, value=0xCDEF"); + + $fclose(cmd_file); + + // ============================================================ + // EXERCISE B: Data Packet Generation + // Inject known values, capture 11-byte output. + // ============================================================ + $display("\n=== EXERCISE B: Data Packet Generation ==="); + apply_reset; + ft_txe_n = 0; // TX FIFO ready + + // Use distinctive values that make truncation/swap bugs obvious + // range_profile = {Q[15:0], I[15:0]} = {0xCAFE, 0xBEEF} + // doppler_real = 0x1234, doppler_imag = 0x5678 + // cfar_detection = 1 + + // First inject doppler and cfar so pending flags are set + pulse_doppler(16'h1234, 16'h5678); + pulse_cfar(1'b1); + + // Now inject range_valid which triggers the write FSM. + // CRITICAL: Must capture bytes IN PARALLEL with the trigger, + // because the write FSM starts sending bytes ~3-4 ft_clk cycles + // after the toggle CDC propagates. If we wait for CDC propagation + // first, capture_write_bytes misses the early bytes. + fork + assert_range_valid(32'hCAFE_BEEF); + capture_write_bytes(11); + join + + check(capture_count === 11, + "Data packet: captured 11 bytes"); + + // Dump captured bytes to file + data_file = $fopen("data_packet.txt", "w"); + $fwrite(data_file, "# byte_index hex_value\n"); + for (i = 0; i < capture_count; i = i + 1) begin + $fwrite(data_file, "%0d %02x\n", i, captured_bytes[i]); + end + $fclose(data_file); + + // Verify locally too + check(captured_bytes[0] === 8'hAA, + "Data pkt: byte 0 = 0xAA (header)"); + check(captured_bytes[1] === 8'hCA, + "Data pkt: byte 1 = 0xCA (range MSB = Q high)"); + check(captured_bytes[2] === 8'hFE, + "Data pkt: byte 2 = 0xFE (range Q low)"); + check(captured_bytes[3] === 8'hBE, + "Data pkt: byte 3 = 0xBE (range I high)"); + check(captured_bytes[4] === 8'hEF, + "Data pkt: byte 4 = 0xEF (range I low)"); + check(captured_bytes[5] === 8'h12, + "Data pkt: byte 5 = 0x12 (doppler_real MSB)"); + check(captured_bytes[6] === 8'h34, + "Data pkt: byte 6 = 0x34 (doppler_real LSB)"); + check(captured_bytes[7] === 8'h56, + "Data pkt: byte 7 = 0x56 (doppler_imag MSB)"); + check(captured_bytes[8] === 8'h78, + "Data pkt: byte 8 = 0x78 (doppler_imag LSB)"); + check(captured_bytes[9] === 8'h01, + "Data pkt: byte 9 = 0x01 (cfar_detection=1)"); + check(captured_bytes[10] === 8'h55, + "Data pkt: byte 10 = 0x55 (footer)"); + + // ============================================================ + // EXERCISE C: Status Packet Generation + // Set known status values, trigger readback, capture 26 bytes. + // Uses distinctive non-zero values to detect truncation/swap. + // ============================================================ + $display("\n=== EXERCISE C: Status Packet Generation ==="); + apply_reset; + ft_txe_n = 0; + + // Set known distinctive status values + status_cfar_threshold = 16'hABCD; + status_stream_ctrl = 3'b101; + status_radar_mode = 2'b11; // Use 0b11 to test both bits + status_long_chirp = 16'h1234; + status_long_listen = 16'h5678; + status_guard = 16'h9ABC; + status_short_chirp = 16'hDEF0; + status_short_listen = 16'hFACE; + status_chirps_per_elev = 6'd42; + status_range_mode = 2'b10; + status_self_test_flags = 5'b10101; + status_self_test_detail = 8'hA5; + status_self_test_busy = 1'b1; + status_agc_current_gain = 4'd7; + status_agc_peak_magnitude = 8'd200; + status_agc_saturation_count = 8'd15; + status_agc_enable = 1'b1; + + // Pulse status_request and capture bytes IN PARALLEL + // (same reason as Exercise B — write FSM starts before CDC wait ends) + fork + pulse_status_request; + capture_write_bytes(26); + join + + check(capture_count === 26, + "Status packet: captured 26 bytes"); + + // Dump captured bytes to file + status_file = $fopen("status_packet.txt", "w"); + $fwrite(status_file, "# byte_index hex_value\n"); + for (i = 0; i < capture_count; i = i + 1) begin + $fwrite(status_file, "%0d %02x\n", i, captured_bytes[i]); + end + + // Also dump the raw status_words for debugging + $fwrite(status_file, "# status_words (internal):\n"); + for (i = 0; i < 6; i = i + 1) begin + $fwrite(status_file, "# word[%0d] = %08x\n", i, uut.status_words[i]); + end + $fclose(status_file); + + // Verify header/footer locally + check(captured_bytes[0] === 8'hBB, + "Status pkt: byte 0 = 0xBB (status header)"); + check(captured_bytes[25] === 8'h55, + "Status pkt: byte 25 = 0x55 (footer)"); + + // Verify status_words[1] = {long_chirp, long_listen} = {0x1234, 0x5678} + check(captured_bytes[5] === 8'h12 && captured_bytes[6] === 8'h34 && + captured_bytes[7] === 8'h56 && captured_bytes[8] === 8'h78, + "Status pkt: word1 = {long_chirp=0x1234, long_listen=0x5678}"); + + // Verify status_words[2] = {guard, short_chirp} = {0x9ABC, 0xDEF0} + check(captured_bytes[9] === 8'h9A && captured_bytes[10] === 8'hBC && + captured_bytes[11] === 8'hDE && captured_bytes[12] === 8'hF0, + "Status pkt: word2 = {guard=0x9ABC, short_chirp=0xDEF0}"); + + // ============================================================ + // Summary + // ============================================================ + $display(""); + $display("========================================"); + $display(" CROSS-LAYER FT2232H TB RESULTS"); + $display(" PASSED: %0d / %0d", pass_count, test_num); + $display(" FAILED: %0d / %0d", fail_count, test_num); + if (fail_count == 0) + $display(" ** ALL TESTS PASSED **"); + else + $display(" ** SOME TESTS FAILED **"); + $display("========================================"); + + #100; + $finish; + end + +endmodule diff --git a/9_Firmware/tests/cross_layer/test_cross_layer_contract.py b/9_Firmware/tests/cross_layer/test_cross_layer_contract.py new file mode 100644 index 0000000..c3b950e --- /dev/null +++ b/9_Firmware/tests/cross_layer/test_cross_layer_contract.py @@ -0,0 +1,828 @@ +""" +Cross-Layer Contract Tests +========================== +Single pytest file orchestrating three tiers of verification: + +Tier 1 — Static Contract Parsing: + Compares Python, Verilog, and C source code at parse-time to catch + opcode mismatches, bit-width errors, packet constant drift, and + layout bugs like the status_words[0] 37-bit truncation. + +Tier 2 — Verilog Cosimulation (iverilog): + Compiles and runs tb_cross_layer_ft2232h.v, then parses its output + files (cmd_results.txt, data_packet.txt, status_packet.txt) and + runs Python parsers on the captured bytes to verify round-trip + correctness. + +Tier 3 — C Stub Execution: + Compiles stm32_settings_stub.cpp, generates a binary settings + packet from Python, runs the stub, and verifies all parsed field + values match. + +The goal is to find UNKNOWN bugs by testing each layer against +independently-derived ground truth — not just checking that two +layers agree (because both could be wrong). +""" + +from __future__ import annotations + +import os +import struct +import subprocess +import tempfile +from pathlib import Path + +import pytest + +# Import the contract parsers +import sys + +THIS_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(THIS_DIR)) +import contract_parser as cp # noqa: E402 + +# Also add the GUI dir to import radar_protocol +sys.path.insert(0, str(cp.GUI_DIR)) + + +# =================================================================== +# Helpers +# =================================================================== + +IVERILOG = os.environ.get("IVERILOG", "/opt/homebrew/bin/iverilog") +VVP = os.environ.get("VVP", "/opt/homebrew/bin/vvp") +CXX = os.environ.get("CXX", "c++") + +# Check tool availability for conditional skipping +_has_iverilog = Path(IVERILOG).exists() if "/" in IVERILOG else bool( + subprocess.run(["which", IVERILOG], capture_output=True).returncode == 0 +) +_has_cxx = subprocess.run( + [CXX, "--version"], capture_output=True +).returncode == 0 + + +def _parse_hex_results(text: str) -> list[dict[str, str]]: + """Parse space-separated hex lines from TB output files.""" + rows = [] + for line in text.strip().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + rows.append(line.split()) + return rows + + +# =================================================================== +# Ground Truth: FPGA register map (independently transcribed) +# =================================================================== +# This is the SINGLE SOURCE OF TRUTH, manually transcribed from +# radar_system_top.v lines 902-945. If any layer disagrees with +# this, it's a bug in that layer. + +GROUND_TRUTH_OPCODES = { + 0x01: ("host_radar_mode", 2), + 0x02: ("host_trigger_pulse", 1), # pulse + 0x03: ("host_detect_threshold", 16), + 0x04: ("host_stream_control", 3), + 0x10: ("host_long_chirp_cycles", 16), + 0x11: ("host_long_listen_cycles", 16), + 0x12: ("host_guard_cycles", 16), + 0x13: ("host_short_chirp_cycles", 16), + 0x14: ("host_short_listen_cycles", 16), + 0x15: ("host_chirps_per_elev", 6), + 0x16: ("host_gain_shift", 4), + 0x20: ("host_range_mode", 2), + 0x21: ("host_cfar_guard", 4), + 0x22: ("host_cfar_train", 5), + 0x23: ("host_cfar_alpha", 8), + 0x24: ("host_cfar_mode", 2), + 0x25: ("host_cfar_enable", 1), + 0x26: ("host_mti_enable", 1), + 0x27: ("host_dc_notch_width", 3), + 0x28: ("host_agc_enable", 1), + 0x29: ("host_agc_target", 8), + 0x2A: ("host_agc_attack", 4), + 0x2B: ("host_agc_decay", 4), + 0x2C: ("host_agc_holdoff", 4), + 0x30: ("host_self_test_trigger", 1), # pulse + 0x31: ("host_status_request", 1), # pulse + 0xFF: ("host_status_request", 1), # alias, pulse +} + +GROUND_TRUTH_RESET_DEFAULTS = { + "host_radar_mode": 1, # 2'b01 + "host_detect_threshold": 10000, + "host_stream_control": 7, # 3'b111 + "host_long_chirp_cycles": 3000, + "host_long_listen_cycles": 13700, + "host_guard_cycles": 17540, + "host_short_chirp_cycles": 50, + "host_short_listen_cycles": 17450, + "host_chirps_per_elev": 32, + "host_gain_shift": 0, + "host_range_mode": 0, + "host_cfar_guard": 2, + "host_cfar_train": 8, + "host_cfar_alpha": 0x30, + "host_cfar_mode": 0, + "host_cfar_enable": 0, + "host_mti_enable": 0, + "host_dc_notch_width": 0, + "host_agc_enable": 0, + "host_agc_target": 200, + "host_agc_attack": 1, + "host_agc_decay": 1, + "host_agc_holdoff": 4, +} + +GROUND_TRUTH_PACKET_CONSTANTS = { + "data": {"header": 0xAA, "footer": 0x55, "size": 11}, + "status": {"header": 0xBB, "footer": 0x55, "size": 26}, +} + + +# =================================================================== +# TIER 1: Static Contract Parsing +# =================================================================== + +class TestTier1OpcodeContract: + """Verify Python and Verilog opcode sets match ground truth.""" + + def test_python_opcodes_match_ground_truth(self): + """Every Python Opcode must exist in ground truth with correct value.""" + py_opcodes = cp.parse_python_opcodes() + for val, entry in py_opcodes.items(): + assert val in GROUND_TRUTH_OPCODES, ( + f"Python Opcode {entry.name}=0x{val:02X} not in ground truth! " + f"Possible phantom opcode (like the 0x06 incident)." + ) + + def test_ground_truth_opcodes_in_python(self): + """Every ground truth opcode must have a Python enum entry.""" + py_opcodes = cp.parse_python_opcodes() + for val, (reg, _width) in GROUND_TRUTH_OPCODES.items(): + assert val in py_opcodes, ( + f"Ground truth opcode 0x{val:02X} ({reg}) missing from Python Opcode enum." + ) + + def test_verilog_opcodes_match_ground_truth(self): + """Every Verilog case entry must exist in ground truth.""" + v_opcodes = cp.parse_verilog_opcodes() + for val, entry in v_opcodes.items(): + assert val in GROUND_TRUTH_OPCODES, ( + f"Verilog opcode 0x{val:02X} ({entry.register}) not in ground truth." + ) + + def test_ground_truth_opcodes_in_verilog(self): + """Every ground truth opcode must have a Verilog case entry.""" + v_opcodes = cp.parse_verilog_opcodes() + for val, (reg, _width) in GROUND_TRUTH_OPCODES.items(): + assert val in v_opcodes, ( + f"Ground truth opcode 0x{val:02X} ({reg}) missing from Verilog case statement." + ) + + def test_python_verilog_bidirectional_match(self): + """Python and Verilog must have the same set of opcode values.""" + py_set = set(cp.parse_python_opcodes().keys()) + v_set = set(cp.parse_verilog_opcodes().keys()) + py_only = py_set - v_set + v_only = v_set - py_set + assert not py_only, f"Opcodes in Python but not Verilog: {[hex(x) for x in py_only]}" + assert not v_only, f"Opcodes in Verilog but not Python: {[hex(x) for x in v_only]}" + + def test_verilog_register_names_match(self): + """Verilog case target registers must match ground truth names.""" + v_opcodes = cp.parse_verilog_opcodes() + for val, (expected_reg, _) in GROUND_TRUTH_OPCODES.items(): + if val in v_opcodes: + actual_reg = v_opcodes[val].register + assert actual_reg == expected_reg, ( + f"Opcode 0x{val:02X}: Verilog writes to '{actual_reg}' " + f"but ground truth says '{expected_reg}'" + ) + + +class TestTier1BitWidths: + """Verify register widths and opcode bit slices match ground truth.""" + + def test_verilog_register_widths(self): + """Register declarations must match ground truth bit widths.""" + v_widths = cp.parse_verilog_register_widths() + for reg, expected_width in [ + (name, w) for _, (name, w) in GROUND_TRUTH_OPCODES.items() + ]: + if reg in v_widths: + actual = v_widths[reg] + assert actual >= expected_width, ( + f"{reg}: declared {actual}-bit but ground truth says {expected_width}-bit" + ) + + def test_verilog_opcode_bit_slices(self): + """Opcode case assignments must use correct bit widths from cmd_value.""" + v_opcodes = cp.parse_verilog_opcodes() + for val, (reg, expected_width) in GROUND_TRUTH_OPCODES.items(): + if val not in v_opcodes: + continue + entry = v_opcodes[val] + if entry.is_pulse: + continue # Pulse opcodes don't use cmd_value slicing + if entry.bit_width > 0: + assert entry.bit_width >= expected_width, ( + f"Opcode 0x{val:02X} ({reg}): bit slice {entry.bit_slice} " + f"= {entry.bit_width}-bit, expected >= {expected_width}" + ) + + +class TestTier1StatusWordTruncation: + """Verify each status_words[] concatenation is exactly 32 bits.""" + + def test_status_words_concat_widths_ft2232h(self): + """Each status_words[] concat must be EXACTLY 32 bits.""" + port_widths = cp.get_usb_interface_port_widths( + cp.FPGA_DIR / "usb_data_interface_ft2232h.v" + ) + concats = cp.parse_verilog_status_word_concats( + cp.FPGA_DIR / "usb_data_interface_ft2232h.v" + ) + + for idx, expr in concats.items(): + result = cp.count_concat_bits(expr, port_widths) + if result.total_bits < 0: + pytest.skip(f"status_words[{idx}]: unknown signal width") + assert result.total_bits == 32, ( + f"status_words[{idx}] is {result.total_bits} bits, not 32! " + f"{'TRUNCATION' if result.total_bits > 32 else 'UNDERFLOW'} BUG. " + f"Fragments: {result.fragments}" + ) + + def test_status_words_concat_widths_ft601(self): + """Same check for the FT601 interface (same bug expected).""" + ft601_path = cp.FPGA_DIR / "usb_data_interface.v" + if not ft601_path.exists(): + pytest.skip("FT601 interface file not found") + + port_widths = cp.get_usb_interface_port_widths(ft601_path) + concats = cp.parse_verilog_status_word_concats(ft601_path) + + for idx, expr in concats.items(): + result = cp.count_concat_bits(expr, port_widths) + if result.total_bits < 0: + pytest.skip(f"status_words[{idx}]: unknown signal width") + assert result.total_bits == 32, ( + f"FT601 status_words[{idx}] is {result.total_bits} bits, not 32! " + f"{'TRUNCATION' if result.total_bits > 32 else 'UNDERFLOW'} BUG. " + f"Fragments: {result.fragments}" + ) + + +class TestTier1StatusFieldPositions: + """Verify Python status parser bit positions match Verilog layout.""" + + def test_python_status_mode_position(self): + """ + Verify Python reads radar_mode at the correct bit position matching + the Verilog status_words[0] layout: + {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} + """ + # Get what Python thinks + py_fields = cp.parse_python_status_fields() + mode_field = next((f for f in py_fields if f.name == "radar_mode"), None) + assert mode_field is not None, "radar_mode not found in parse_status_packet" + + # Ground truth: mode is at bits [23:22], so LSB = 22 + expected_shift = 22 + actual_shift = mode_field.lsb + + assert actual_shift == expected_shift, ( + f"Python reads radar_mode at bit {actual_shift} " + f"but Verilog status_words[0] has mode at bit {expected_shift}." + ) + + +class TestTier1PacketConstants: + """Verify packet header/footer/size constants match across layers.""" + + def test_python_packet_constants(self): + """Python constants match ground truth.""" + py = cp.parse_python_packet_constants() + for ptype, expected in GROUND_TRUTH_PACKET_CONSTANTS.items(): + assert py[ptype].header == expected["header"], ( + f"Python {ptype} header: 0x{py[ptype].header:02X} != 0x{expected['header']:02X}" + ) + assert py[ptype].footer == expected["footer"], ( + f"Python {ptype} footer: 0x{py[ptype].footer:02X} != 0x{expected['footer']:02X}" + ) + assert py[ptype].size == expected["size"], ( + f"Python {ptype} size: {py[ptype].size} != {expected['size']}" + ) + + def test_verilog_packet_constants(self): + """Verilog localparams match ground truth.""" + v = cp.parse_verilog_packet_constants() + for ptype, expected in GROUND_TRUTH_PACKET_CONSTANTS.items(): + assert v[ptype].header == expected["header"], ( + f"Verilog {ptype} header: 0x{v[ptype].header:02X} != 0x{expected['header']:02X}" + ) + assert v[ptype].footer == expected["footer"], ( + f"Verilog {ptype} footer: 0x{v[ptype].footer:02X} != 0x{expected['footer']:02X}" + ) + assert v[ptype].size == expected["size"], ( + f"Verilog {ptype} size: {v[ptype].size} != {expected['size']}" + ) + + def test_python_verilog_constants_agree(self): + """Python and Verilog packet constants must match each other.""" + py = cp.parse_python_packet_constants() + v = cp.parse_verilog_packet_constants() + for ptype in ("data", "status"): + assert py[ptype].header == v[ptype].header + assert py[ptype].footer == v[ptype].footer + assert py[ptype].size == v[ptype].size + + +class TestTier1ResetDefaults: + """Verify Verilog reset defaults match ground truth.""" + + def test_verilog_reset_defaults(self): + """Reset block values must match ground truth.""" + v_defaults = cp.parse_verilog_reset_defaults() + for reg, expected in GROUND_TRUTH_RESET_DEFAULTS.items(): + assert reg in v_defaults, f"{reg} not found in reset block" + actual = v_defaults[reg] + assert actual == expected, ( + f"{reg}: reset default {actual} != expected {expected}" + ) + + +class TestTier1DataPacketLayout: + """Verify data packet byte layout matches between Python and Verilog.""" + + def test_verilog_data_mux_field_positions(self): + """Verilog data_pkt_byte mux must have correct byte positions.""" + v_fields = cp.parse_verilog_data_mux() + # Expected: range_profile at bytes 1-4 (32-bit), doppler_real 5-6, + # doppler_imag 7-8, cfar 9 + field_map = {f.name: f for f in v_fields} + + assert "range_profile" in field_map + rp = field_map["range_profile"] + assert rp.byte_start == 1 and rp.byte_end == 4 and rp.width_bits == 32 + + assert "doppler_real" in field_map + dr = field_map["doppler_real"] + assert dr.byte_start == 5 and dr.byte_end == 6 and dr.width_bits == 16 + + assert "doppler_imag" in field_map + di = field_map["doppler_imag"] + assert di.byte_start == 7 and di.byte_end == 8 and di.width_bits == 16 + + def test_python_data_packet_byte_positions(self): + """Python parse_data_packet byte offsets must be correct.""" + py_fields = cp.parse_python_data_packet_fields() + # range_q at offset 1 (2B), range_i at offset 3 (2B), + # doppler_i at offset 5 (2B), doppler_q at offset 7 (2B), + # detection at offset 9 + field_map = {f.name: f for f in py_fields} + + assert "range_q" in field_map + assert field_map["range_q"].byte_start == 1 + assert "range_i" in field_map + assert field_map["range_i"].byte_start == 3 + assert "doppler_i" in field_map + assert field_map["doppler_i"].byte_start == 5 + assert "doppler_q" in field_map + assert field_map["doppler_q"].byte_start == 7 + assert "detection" in field_map + assert field_map["detection"].byte_start == 9 + + +class TestTier1STM32SettingsPacket: + """Verify STM32 settings packet layout.""" + + def test_field_order_and_sizes(self): + """STM32 settings fields must have correct offsets and sizes.""" + fields = cp.parse_stm32_settings_fields() + if not fields: + pytest.skip("MCU source not available") + + expected = [ + ("system_frequency", 0, 8, "double"), + ("chirp_duration_1", 8, 8, "double"), + ("chirp_duration_2", 16, 8, "double"), + ("chirps_per_position", 24, 4, "uint32_t"), + ("freq_min", 28, 8, "double"), + ("freq_max", 36, 8, "double"), + ("prf1", 44, 8, "double"), + ("prf2", 52, 8, "double"), + ("max_distance", 60, 8, "double"), + ("map_size", 68, 8, "double"), + ] + + assert len(fields) == len(expected), ( + f"Expected {len(expected)} fields, got {len(fields)}" + ) + + for f, (ename, eoff, esize, etype) in zip(fields, expected, strict=True): + assert f.name == ename, f"Field name: {f.name} != {ename}" + assert f.offset == eoff, f"{f.name}: offset {f.offset} != {eoff}" + assert f.size == esize, f"{f.name}: size {f.size} != {esize}" + assert f.c_type == etype, f"{f.name}: type {f.c_type} != {etype}" + + def test_minimum_packet_size(self): + """ + RadarSettings.cpp says minimum is 74 bytes but actual payload is: + 'SET'(3) + 9*8(doubles) + 4(uint32) + 'END'(3) = 82 bytes. + This test documents the bug. + """ + fields = cp.parse_stm32_settings_fields() + if not fields: + pytest.skip("MCU source not available") + + # Calculate required payload size + total_field_bytes = sum(f.size for f in fields) + # Add markers: "SET"(3) + "END"(3) + required_size = 3 + total_field_bytes + 3 + + # Read the actual minimum check from the source + src = (cp.MCU_LIB_DIR / "RadarSettings.cpp").read_text(encoding="latin-1") + import re + m = re.search(r'length\s*<\s*(\d+)', src) + assert m, "Could not find minimum length check in parseFromUSB" + declared_min = int(m.group(1)) + + assert declared_min == required_size, ( + f"BUFFER OVERREAD BUG: parseFromUSB minimum check is {declared_min} " + f"but actual required size is {required_size}. " + f"({total_field_bytes} bytes of fields + 6 bytes of markers). " + f"If exactly {declared_min} bytes are passed, extractDouble() reads " + f"past the buffer at offset {declared_min - 3} (needs 8 bytes, " + f"only {declared_min - 3 - fields[-1].offset} available)." + ) + + def test_stm32_usb_start_flag(self): + """USB start flag must be [23, 46, 158, 237].""" + flag = cp.parse_stm32_start_flag() + if not flag: + pytest.skip("USBHandler.cpp not available") + assert flag == [23, 46, 158, 237], f"Start flag: {flag}" + + +# =================================================================== +# TIER 2: Verilog Cosimulation +# =================================================================== + +@pytest.mark.skipif(not _has_iverilog, reason="iverilog not available") +class TestTier2VerilogCosim: + """Compile and run the FT2232H TB, validate output against Python parsers.""" + + @pytest.fixture(scope="class") + def tb_results(self, tmp_path_factory): + """Compile and run TB once, return output file contents.""" + workdir = tmp_path_factory.mktemp("verilog_cosim") + + tb_path = THIS_DIR / "tb_cross_layer_ft2232h.v" + rtl_path = cp.FPGA_DIR / "usb_data_interface_ft2232h.v" + out_bin = workdir / "tb_cross_layer_ft2232h" + + # Compile + result = subprocess.run( + [IVERILOG, "-o", str(out_bin), "-I", str(cp.FPGA_DIR), + str(tb_path), str(rtl_path)], + capture_output=True, text=True, timeout=30, + ) + assert result.returncode == 0, f"iverilog compile failed:\n{result.stderr}" + + # Run + result = subprocess.run( + [VVP, str(out_bin)], + capture_output=True, text=True, timeout=60, + cwd=str(workdir), + ) + assert result.returncode == 0, f"vvp failed:\n{result.stderr}" + + # Parse output + return { + "stdout": result.stdout, + "cmd_results": (workdir / "cmd_results.txt").read_text(), + "data_packet": (workdir / "data_packet.txt").read_text(), + "status_packet": (workdir / "status_packet.txt").read_text(), + } + + def test_all_tb_tests_pass(self, tb_results): + """All Verilog TB internal checks must pass.""" + stdout = tb_results["stdout"] + assert "ALL TESTS PASSED" in stdout, f"TB had failures:\n{stdout}" + + def test_command_round_trip(self, tb_results): + """Verify every command decoded correctly by matching sent vs received.""" + rows = _parse_hex_results(tb_results["cmd_results"]) + assert len(rows) >= 20, f"Expected >= 20 command results, got {len(rows)}" + + for row in rows: + assert len(row) == 6, f"Bad row format: {row}" + sent_op, sent_addr, sent_val = row[0], row[1], row[2] + got_op, got_addr, got_val = row[3], row[4], row[5] + assert sent_op == got_op, ( + f"Opcode mismatch: sent 0x{sent_op} got 0x{got_op}" + ) + assert sent_addr == got_addr, ( + f"Addr mismatch: sent 0x{sent_addr} got 0x{got_addr}" + ) + assert sent_val == got_val, ( + f"Value mismatch: sent 0x{sent_val} got 0x{got_val}" + ) + + def test_data_packet_python_round_trip(self, tb_results): + """ + Take the 11 bytes captured by the Verilog TB, run Python's + parse_data_packet() on them, verify the parsed values match + what was injected into the TB. + """ + from radar_protocol import RadarProtocol + + rows = _parse_hex_results(tb_results["data_packet"]) + assert len(rows) == 11, f"Expected 11 data packet bytes, got {len(rows)}" + + # Reconstruct raw bytes + raw = bytes(int(row[1], 16) for row in rows) + assert len(raw) == 11 + + parsed = RadarProtocol.parse_data_packet(raw) + assert parsed is not None, "parse_data_packet returned None" + + # The TB injected: range_profile = 0xCAFE_BEEF = {Q=0xCAFE, I=0xBEEF} + # doppler_real = 0x1234, doppler_imag = 0x5678 + # cfar_detection = 1 + # + # range_q = 0xCAFE → signed = 0xCAFE - 0x10000 = -13570 + # range_i = 0xBEEF → signed = 0xBEEF - 0x10000 = -16657 + # doppler_i = 0x1234 → signed = 4660 + # doppler_q = 0x5678 → signed = 22136 + + assert parsed["range_q"] == (0xCAFE - 0x10000), ( + f"range_q: {parsed['range_q']} != {0xCAFE - 0x10000}" + ) + assert parsed["range_i"] == (0xBEEF - 0x10000), ( + f"range_i: {parsed['range_i']} != {0xBEEF - 0x10000}" + ) + assert parsed["doppler_i"] == 0x1234, ( + f"doppler_i: {parsed['doppler_i']} != {0x1234}" + ) + assert parsed["doppler_q"] == 0x5678, ( + f"doppler_q: {parsed['doppler_q']} != {0x5678}" + ) + assert parsed["detection"] == 1, ( + f"detection: {parsed['detection']} != 1" + ) + + def test_status_packet_python_round_trip(self, tb_results): + """ + Take the 26 bytes captured by the Verilog TB, run Python's + parse_status_packet() on them, verify against injected values. + """ + from radar_protocol import RadarProtocol + + lines = tb_results["status_packet"].strip().splitlines() + # Filter out comments and status_words debug lines + rows = [] + for line in lines: + line = line.strip() + if not line or line.startswith("#"): + continue + rows.append(line.split()) + + assert len(rows) == 26, f"Expected 26 status bytes, got {len(rows)}" + + raw = bytes(int(row[1], 16) for row in rows) + assert len(raw) == 26 + + sr = RadarProtocol.parse_status_packet(raw) + assert sr is not None, "parse_status_packet returned None" + + # Injected values (from TB): + # status_cfar_threshold = 0xABCD + # status_stream_ctrl = 3'b101 = 5 + # status_radar_mode = 2'b11 = 3 + # status_long_chirp = 0x1234 + # status_long_listen = 0x5678 + # status_guard = 0x9ABC + # status_short_chirp = 0xDEF0 + # status_short_listen = 0xFACE + # status_chirps_per_elev = 42 + # status_range_mode = 2'b10 = 2 + # status_self_test_flags = 5'b10101 = 21 + # status_self_test_detail = 0xA5 + # status_self_test_busy = 1 + # status_agc_current_gain = 7 + # status_agc_peak_magnitude = 200 + # status_agc_saturation_count = 15 + # status_agc_enable = 1 + + # Words 1-5 should be correct (no truncation bug) + assert sr.cfar_threshold == 0xABCD, f"cfar_threshold: 0x{sr.cfar_threshold:04X}" + assert sr.long_chirp == 0x1234, f"long_chirp: 0x{sr.long_chirp:04X}" + assert sr.long_listen == 0x5678, f"long_listen: 0x{sr.long_listen:04X}" + assert sr.guard == 0x9ABC, f"guard: 0x{sr.guard:04X}" + assert sr.short_chirp == 0xDEF0, f"short_chirp: 0x{sr.short_chirp:04X}" + assert sr.short_listen == 0xFACE, f"short_listen: 0x{sr.short_listen:04X}" + assert sr.chirps_per_elev == 42, f"chirps_per_elev: {sr.chirps_per_elev}" + assert sr.range_mode == 2, f"range_mode: {sr.range_mode}" + assert sr.self_test_flags == 21, f"self_test_flags: {sr.self_test_flags}" + assert sr.self_test_detail == 0xA5, f"self_test_detail: 0x{sr.self_test_detail:02X}" + assert sr.self_test_busy == 1, f"self_test_busy: {sr.self_test_busy}" + + # AGC fields (word 4) + assert sr.agc_current_gain == 7, f"agc_current_gain: {sr.agc_current_gain}" + assert sr.agc_peak_magnitude == 200, f"agc_peak_magnitude: {sr.agc_peak_magnitude}" + assert sr.agc_saturation_count == 15, f"agc_saturation_count: {sr.agc_saturation_count}" + assert sr.agc_enable == 1, f"agc_enable: {sr.agc_enable}" + + # Word 0: stream_ctrl should be 5 (3'b101) + assert sr.stream_ctrl == 5, ( + f"stream_ctrl: {sr.stream_ctrl} != 5. " + f"Check status_words[0] bit positions." + ) + + # radar_mode should be 3 (2'b11) + assert sr.radar_mode == 3, ( + f"radar_mode={sr.radar_mode} != 3. " + f"Check status_words[0] bit positions." + ) + + +# =================================================================== +# TIER 3: C Stub Execution +# =================================================================== + +@pytest.mark.skipif(not _has_cxx, reason="C++ compiler not available") +class TestTier3CStub: + """Compile STM32 settings stub and verify field parsing.""" + + @pytest.fixture(scope="class") + def stub_binary(self, tmp_path_factory): + """Compile the C++ stub once.""" + workdir = tmp_path_factory.mktemp("c_stub") + stub_src = THIS_DIR / "stm32_settings_stub.cpp" + radar_settings_src = cp.MCU_LIB_DIR / "RadarSettings.cpp" + out_bin = workdir / "stm32_settings_stub" + + result = subprocess.run( + [CXX, "-std=c++11", "-o", str(out_bin), + str(stub_src), str(radar_settings_src), + f"-I{cp.MCU_LIB_DIR}"], + capture_output=True, text=True, timeout=30, + ) + assert result.returncode == 0, f"Compile failed:\n{result.stderr}" + return out_bin + + def _build_settings_packet(self, values: dict) -> bytes: + """Build a binary settings packet matching RadarSettings::parseFromUSB.""" + pkt = b"SET" + for key in [ + "system_frequency", "chirp_duration_1", "chirp_duration_2", + ]: + pkt += struct.pack(">d", values[key]) + pkt += struct.pack(">I", values["chirps_per_position"]) + for key in [ + "freq_min", "freq_max", "prf1", "prf2", + "max_distance", "map_size", + ]: + pkt += struct.pack(">d", values[key]) + pkt += b"END" + return pkt + + def _run_stub(self, binary: Path, packet: bytes) -> dict[str, str]: + """Run stub with packet file, parse stdout into field dict.""" + with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f: + f.write(packet) + pkt_path = f.name + + try: + result = subprocess.run( + [str(binary), pkt_path], + capture_output=True, text=True, timeout=10, + ) + finally: + os.unlink(pkt_path) + + fields = {} + for line in result.stdout.strip().splitlines(): + if "=" in line: + k, v = line.split("=", 1) + fields[k.strip()] = v.strip() + return fields + + def test_default_values_round_trip(self, stub_binary): + """Default settings must parse correctly through C stub.""" + values = { + "system_frequency": 10.0e9, + "chirp_duration_1": 30.0e-6, + "chirp_duration_2": 0.5e-6, + "chirps_per_position": 32, + "freq_min": 10.0e6, + "freq_max": 30.0e6, + "prf1": 1000.0, + "prf2": 2000.0, + "max_distance": 50000.0, + "map_size": 50000.0, + } + pkt = self._build_settings_packet(values) + result = self._run_stub(stub_binary, pkt) + + assert result.get("parse_ok") == "true", f"Parse failed: {result}" + + for key, expected in values.items(): + actual_str = result.get(key) + assert actual_str is not None, f"Missing field: {key}" + actual = int(actual_str) if key == "chirps_per_position" else float(actual_str) + if isinstance(expected, float): + assert abs(actual - expected) < expected * 1e-10, ( + f"{key}: {actual} != {expected}" + ) + else: + assert actual == expected, f"{key}: {actual} != {expected}" + + def test_distinctive_values_round_trip(self, stub_binary): + """Non-default distinctive values must parse correctly.""" + values = { + "system_frequency": 24.125e9, # K-band + "chirp_duration_1": 100.0e-6, + "chirp_duration_2": 2.0e-6, + "chirps_per_position": 64, + "freq_min": 24.0e6, + "freq_max": 24.25e6, + "prf1": 5000.0, + "prf2": 3000.0, + "max_distance": 75000.0, + "map_size": 100000.0, + } + pkt = self._build_settings_packet(values) + result = self._run_stub(stub_binary, pkt) + + assert result.get("parse_ok") == "true", f"Parse failed: {result}" + + for key, expected in values.items(): + actual_str = result.get(key) + assert actual_str is not None, f"Missing field: {key}" + actual = int(actual_str) if key == "chirps_per_position" else float(actual_str) + if isinstance(expected, float): + assert abs(actual - expected) < expected * 1e-10, ( + f"{key}: {actual} != {expected}" + ) + else: + assert actual == expected, f"{key}: {actual} != {expected}" + + def test_truncated_packet_rejected(self, stub_binary): + """Packet shorter than minimum must be rejected.""" + pkt = b"SET" + b"\x00" * 40 + b"END" # Only 46 bytes, needs 82 + result = self._run_stub(stub_binary, pkt) + assert result.get("parse_ok") == "false", ( + f"Expected parse failure for truncated packet, got: {result}" + ) + + def test_bad_markers_rejected(self, stub_binary): + """Packet with wrong start/end markers must be rejected.""" + values = { + "system_frequency": 10.0e9, "chirp_duration_1": 30.0e-6, + "chirp_duration_2": 0.5e-6, "chirps_per_position": 32, + "freq_min": 10.0e6, "freq_max": 30.0e6, + "prf1": 1000.0, "prf2": 2000.0, + "max_distance": 50000.0, "map_size": 50000.0, + } + pkt = self._build_settings_packet(values) + + # Wrong start marker + bad_pkt = b"BAD" + pkt[3:] + result = self._run_stub(stub_binary, bad_pkt) + assert result.get("parse_ok") == "false", "Should reject bad start marker" + + # Wrong end marker + bad_pkt = pkt[:-3] + b"BAD" + result = self._run_stub(stub_binary, bad_pkt) + assert result.get("parse_ok") == "false", "Should reject bad end marker" + + def test_python_c_packet_format_agreement(self, stub_binary): + """ + Python builds a settings packet, C stub parses it. + This tests that both sides agree on the packet format. + """ + # Use values right at validation boundaries to stress-test + values = { + "system_frequency": 1.0e9, # min valid + "chirp_duration_1": 1.0e-6, # min valid + "chirp_duration_2": 0.1e-6, # min valid + "chirps_per_position": 1, # min valid + "freq_min": 1.0e6, # min valid + "freq_max": 2.0e6, # just above freq_min + "prf1": 100.0, # min valid + "prf2": 100.0, # min valid + "max_distance": 100.0, # min valid + "map_size": 1000.0, # min valid + } + pkt = self._build_settings_packet(values) + result = self._run_stub(stub_binary, pkt) + + assert result.get("parse_ok") == "true", ( + f"Boundary values rejected: {result}" + ) 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"]