fix(gui): GUI-S1 — structural validation in find_packet_boundaries

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.
This commit is contained in:
Jason
2026-04-27 13:26:24 +05:45
parent 760288037f
commit e9e301dc50
2 changed files with 63 additions and 9 deletions
+21 -9
View File
@@ -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
+42
View File
@@ -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."""