mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-09 06:57:15 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user