From c49092f52bdf4fecf53660f43b80f169115686f8 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:09:15 +0545 Subject: [PATCH] =?UTF-8?q?test(gui):=20GUI-S3=20=E2=80=94=20pin=20status?= =?UTF-8?q?=20word=204=20bit=20layout=20in=20a=20co-spec=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-verified status word 4 bit positions against the FPGA word builder (usb_data_interface.v:376-380, usb_data_interface_ft2232h.v:675-679) and the GUI parser (radar_protocol.py:252-257) — all positions match. No production change needed; the gap was that nothing in the test suite caught a future drift between the two sides. Added test_parse_status_word4_layout_co_spec: a single canonical layout table is the source of truth; for each field the test sets only that field to its max value, builds the status packet via the existing FPGA-builder-mirror _make_status_packet, parses, and asserts the field round-trips exactly AND every other field reads back zero. Catches both LSB drift and width drift on either side of the wire. Pre-checks that widths plus the reserved [9:2] gap sum to 32 and that no two fields overlap. Also fixed test_default_shapes — stale (64,32)/(64,) literals predated the GUI-C1 / Q3 alignment that bumped NUM_RANGE_BINS 64 -> 512. test_v7 was updated at the time, this one was missed. Replaced with references to NUM_RANGE_BINS/NUM_DOPPLER_BINS so any future bin-count change auto-updates the assertion. Suite now 180/180 PASS. --- 9_Firmware/9_3_GUI/test_GUI_V65_Tk.py | 70 +++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py index 70601f1..73343ab 100644 --- a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py @@ -207,6 +207,63 @@ class TestRadarProtocol(unittest.TestCase): raw[25] = 0x00 # corrupt footer (was at index 21 in old 5-word format) self.assertIsNone(RadarProtocol.parse_status_packet(bytes(raw))) + def test_parse_status_word4_layout_co_spec(self): + """GUI-S3: pin status word 4 bit positions to the FPGA word builder. + + Canonical layout per usb_data_interface.v:376-380 and + usb_data_interface_ft2232h.v:675-679 — exactly one source of truth + in this test, so any future drift between FPGA and GUI trips here: + + [31:28] agc_current_gain (4-bit) + [27:20] agc_peak_magnitude (8-bit) + [19:12] agc_saturation_count (8-bit) + [11] agc_enable (1-bit) + [10] chirps_mismatch (1-bit, TX-G) + [9:2] reserved (8 bits, must be zero from builder) + [1:0] range_mode (2-bit) + + For each field we set ONLY that field to its max, build the packet, + parse, and assert (a) the field reads back correctly and (b) every + other field reads back zero. Catches both LSB drift and width drift + on either side of the wire. + """ + layout = [ + # (field_name, builder_kwarg, lsb, width, parsed_attr) + ("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"), + ("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"), + ("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"), + ("agc_enable", "agc_enable", 11, 1, "agc_enable"), + ("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"), + ("range_mode", "range_mode", 0, 2, "range_mode"), + ] + # Sanity: layout fields + reserved [9:2] must cover exactly 32 bits. + used = sum(width for _, _, _, width, _ in layout) + self.assertEqual(used + 8, 32, + "word 4 layout (incl. reserved [9:2]) must total 32 bits") + + # No two fields may overlap. + occupied = set() + for name, _, lsb, width, _ in layout: + bits = set(range(lsb, lsb + width)) + self.assertFalse(occupied & bits, + f"{name} bits {sorted(bits)} overlap previously-allocated bits") + occupied |= bits + + other_attrs = [attr for _, _, _, _, attr in layout] + + for name, kwarg, _lsb, width, attr in layout: + max_val = (1 << width) - 1 + raw = self._make_status_packet(**{kwarg: max_val}) + sr = RadarProtocol.parse_status_packet(raw) + self.assertIsNotNone(sr, f"{name}: parse failed") + self.assertEqual(getattr(sr, attr), max_val, + f"{name}: round-trip mismatch (set={max_val}, got={getattr(sr, attr)})") + for other in other_attrs: + if other == attr: + continue + self.assertEqual(getattr(sr, other), 0, + f"{name} max value bled into {other} -- bit-position drift?") + def test_parse_status_self_test_all_pass(self): """Status with all self-test flags set (all tests pass).""" raw = self._make_status_packet(st_flags=0x1F, st_detail=0xA5, st_busy=0) @@ -558,12 +615,15 @@ class TestRadarFrameDefaults(unittest.TestCase): """Test RadarFrame default initialization.""" def test_default_shapes(self): + # Stale literals (64,32)/(64,) predated the GUI-C1 / Q3 alignment that + # bumped NUM_RANGE_BINS 64 -> 512 to match FPGA truth. Reference the + # constants so any future bin-count change updates the assertion too. f = RadarFrame() - self.assertEqual(f.range_doppler_i.shape, (64, 32)) - self.assertEqual(f.range_doppler_q.shape, (64, 32)) - self.assertEqual(f.magnitude.shape, (64, 32)) - self.assertEqual(f.detections.shape, (64, 32)) - self.assertEqual(f.range_profile.shape, (64,)) + self.assertEqual(f.range_doppler_i.shape, (NUM_RANGE_BINS, NUM_DOPPLER_BINS)) + self.assertEqual(f.range_doppler_q.shape, (NUM_RANGE_BINS, NUM_DOPPLER_BINS)) + self.assertEqual(f.magnitude.shape, (NUM_RANGE_BINS, NUM_DOPPLER_BINS)) + self.assertEqual(f.detections.shape, (NUM_RANGE_BINS, NUM_DOPPLER_BINS)) + self.assertEqual(f.range_profile.shape, (NUM_RANGE_BINS,)) self.assertEqual(f.detection_count, 0) def test_default_zeros(self):