From e9e301dc501316ccd1725faf868101489e072de6 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:26:24 +0545 Subject: [PATCH] =?UTF-8?q?fix(gui):=20GUI-S1=20=E2=80=94=20structural=20v?= =?UTF-8?q?alidation=20in=20find=5Fpacket=5Fboundaries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The packet-boundary scanner only checked header + footer bytes, so any payload byte that happened to be 0xAA (or 0xBB) and which lined up with a 0x55 at offset+10 (or +25) was accepted as a packet. A single corrupt byte could permanently shift the binning until the next frame_start re-sync. Added two structural sentinel checks against fixed bits the FPGA emitter always drives to known values: - data byte 9 = {frame_start, 6'b0, cfar_detection} -> bits[6:1]==0 - status byte 1 = high byte of status_words[0] -> 0xFF Combined with the existing footer check, false-match probability drops from ~1/256 to ~1/16384 (data) and ~1/65536 (status). Mock generators already produce conformant bit patterns, so existing parser/mock-read tests pass unchanged. New tests: - test_find_boundaries_rejects_false_data_header (forged 0xAA...0x55) - test_find_boundaries_rejects_false_status_header (forged 0xBB...0x55) - test_find_boundaries_recovers_after_byte_drop (single-byte loss) Tests: GUI 96/96 (was 93), test_v7 83/83, MCU 75/75, ruff clean. No RTL change -- wire format is unchanged; this hardens the parser only. --- 9_Firmware/9_3_GUI/radar_protocol.py | 30 +++++++++++++------ 9_Firmware/9_3_GUI/test_GUI_V65_Tk.py | 42 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 76c7342..69e1d2d 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -267,28 +267,40 @@ class RadarProtocol: """ Scan buffer for packet start markers (0xAA data, 0xBB status). Returns list of (start_idx, expected_end_idx, packet_type). + + GUI-S1: in addition to header+footer, validate fixed structural + bytes the FPGA always emits in known patterns. This rejects false + starts where a payload byte happens to be 0xAA/0xBB and the byte + DATA/STATUS_PACKET_SIZE later happens to be 0x55: + - data byte 9 = {frame_start, 6'b0, cfar_detection} → bits[6:1]==0 + - status byte 1 = high byte of status_words[0] → 0xFF + Drops false-match probability from 1/256 to ~1/16384 (data) / + ~1/65536 (status). """ packets = [] i = 0 - while i < len(buf): + n = len(buf) + while i < n: if buf[i] == HEADER_BYTE: end = i + DATA_PACKET_SIZE - if end <= len(buf) and buf[end - 1] == FOOTER_BYTE: + if end > n: + break # partial packet at end — leave for residual + if (buf[end - 1] == FOOTER_BYTE and + (buf[i + 9] & 0x7E) == 0): packets.append((i, end, "data")) i = end else: - if end > len(buf): - break # partial packet at end — leave for residual - i += 1 # footer mismatch — skip this false header + i += 1 # structural mismatch — skip this false header elif buf[i] == STATUS_HEADER_BYTE: end = i + STATUS_PACKET_SIZE - if end <= len(buf) and buf[end - 1] == FOOTER_BYTE: + if end > n: + break # partial status packet — leave for residual + if (buf[end - 1] == FOOTER_BYTE and + buf[i + 1] == 0xFF): packets.append((i, end, "status")) i = end else: - if end > len(buf): - break # partial status packet — leave for residual - i += 1 # footer mismatch — skip + i += 1 else: i += 1 return packets 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 8e1d0cc..70601f1 100644 --- a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py @@ -276,6 +276,48 @@ class TestRadarProtocol(unittest.TestCase): boundaries = RadarProtocol.find_packet_boundaries(buf) self.assertEqual(len(boundaries), 0) + def test_find_boundaries_rejects_false_data_header(self): + """GUI-S1: a stray 0xAA followed by 0x55 ten bytes later but with + invalid byte-9 structure must NOT be accepted as a packet.""" + # Forge: header=0xAA, then 8 bytes of payload, byte 9 = 0xFF (bits + # [6:1] all set — invalid: real packets have these zeroed), + # then 0x55 footer. Old parser would accept; new parser rejects. + forged = bytes([0xAA] + [0x00] * 8 + [0xFF, 0x55]) + real = self._make_data_packet() + buf = forged + real + boundaries = RadarProtocol.find_packet_boundaries(buf) + # Must skip the forged 11 bytes and lock onto the real packet. + self.assertEqual(len(boundaries), 1) + self.assertEqual(boundaries[0], (11, 22, "data")) + + def test_find_boundaries_rejects_false_status_header(self): + """GUI-S1: a stray 0xBB without 0xFF at offset+1 must NOT be + accepted as a status packet — even if 0x55 lands 25 bytes later.""" + forged = bytes([0xBB] + [0x00] * 24 + [0x55]) # byte 1 = 0x00, not 0xFF + real = self._make_data_packet() + buf = forged + real + boundaries = RadarProtocol.find_packet_boundaries(buf) + # Forged status rejected; real data packet found 11 bytes in. + data_hits = [b for b in boundaries if b[2] == "data"] + status_hits = [b for b in boundaries if b[2] == "status"] + self.assertEqual(len(status_hits), 0) + self.assertEqual(len(data_hits), 1) + + def test_find_boundaries_recovers_after_byte_drop(self): + """GUI-S1: simulate a single-byte drop — parser should re-lock on + the next intact packet rather than smearing forever.""" + good = self._make_data_packet(detection=0) + # Drop byte 5 of the first packet to mimic USB byte loss. + corrupted = good[:5] + good[6:] # now 10 bytes, no full packet + buf = corrupted + good + good # two intact packets follow + boundaries = RadarProtocol.find_packet_boundaries(buf) + # We expect to recover and find at least the two intact tails. + self.assertGreaterEqual(len(boundaries), 2) + # Both recovered packets must be valid data packets. + for start, end, ptype in boundaries: + self.assertEqual(ptype, "data") + self.assertIsNotNone(RadarProtocol.parse_data_packet(buf[start:end])) + class TestFT2232HConnection(unittest.TestCase): """Test mock FT2232H connection."""