Add RFG UDP source to rph_radar

This commit is contained in:
Ayzen
2026-10-06 19:36:00 +03:00
parent a185e61495
commit cfe81f782b
29 changed files with 817 additions and 279 deletions
+78 -6
View File
@@ -1,10 +1,15 @@
"""Whole-sweep RPH1 packets from the collector's PTY (little endian).
"""RPH sweep sources, both little endian.
The 48-byte header carries payload size, source sweep/cycle IDs, monotonic capture
window, input/output (65535 for untagged) and dirty flag. Payload points are
8-byte ``marker, step, I, Q`` frames: A for main and A8 for reference.
Only complete queued packets may be dropped; a packet already being written is
always finished. Thus overflow cannot splice two physical sweeps together.
E-502 collector PTY: whole-sweep RPH1 packets. The 48-byte header carries payload
size, source sweep/cycle IDs, monotonic capture window, input/output (65535 for
untagged) and dirty flag. Payload points are 8-byte ``marker, step, I, Q`` frames:
A for main and A8 for reference. Only complete queued packets may be dropped; a
packet already being written is always finished, so overflow cannot splice sweeps.
RFG board UDP (``SWEEP_AVG=1``): a ``u16 values_per_point, u16 q_shift`` header and
28-byte points ``u32 (sweep << 24) | step`` plus six int32 Q(q_shift) step averages
``[a1c0, a1c1, a1c2, a2c0, a2c1, a2c2]``. Channel k is ``a1ck + j*a2ck``; the main
signal is the synchronous detector ch0 - ch1 and the reference is ch2.
"""
from dataclasses import dataclass
@@ -18,6 +23,8 @@ REFERENCE_MARKER = 0x00A8
_HEADER = struct.Struct("<4sIQQQQHHI")
_POINT = struct.Struct("<HHhh")
_MAX_PAYLOAD_BYTES = 65534 * 2 * _POINT.size
_RFG_HEADER = struct.Struct("<HH")
_RFG_POINT = struct.Struct("<I6i")
@dataclass(frozen=True, slots=True)
@@ -87,3 +94,68 @@ class RphRadarStreamParser:
None if inp == 65535 else (inp, out), bool(flags & 1),
))
return sweeps
class RfgSweepAssembler:
"""Collect RFG points into sweeps of exactly the laser task's ``steps``.
A sweep closes when the next sweep id appears. Ids advance by one per sweep, so an
id that is not ahead of the current one (mod 256) is a late point of an older
sweep and is ignored — unless 128 sweep periods passed, when any id is legitimate. It is kept only with steps ``0..steps-1`` in order: the
reference phase advances up to ~1.8 rad per step, so a lost, duplicate or
reordered point would break the unwrapped frequency axis. Arrival times are
kernel receive stamps (see the reader). Step k leaves the board
``(k + 1) * step_ns`` after SWEEP_START and arrives no earlier, so the lower
envelope of ``arrival - (k + 1) * step_ns`` is the sweep start.
"""
def __init__(self, steps: int, step_ns: int, period_ns: int) -> None:
self._steps, self._step_ns, self._period_ns = steps, step_ns, period_ns
self._sweep_id: int | None = None # newest sweep; its points may still be arriving
self._last_ns = 0
self._points: list[tuple[int, complex, complex, int]] = []
self._count = 0
def reset(self) -> None:
"""Drop the sweep in progress; its remaining points then form a dirty sweep."""
self._sweep_id, self._points = None, []
def feed(self, datagram: bytes, received_ns: int) -> list[RawSweep]:
if len(datagram) < _RFG_HEADER.size:
raise ValueError("RFG protocol violation: truncated datagram")
values, q_shift = _RFG_HEADER.unpack_from(datagram)
if values != 6 or q_shift > 30 or (len(datagram) - _RFG_HEADER.size) % _RFG_POINT.size:
raise ValueError("RFG protocol violation: unexpected datagram layout")
scale = 1.0 / (1 << q_shift)
sweeps = []
for word, a1c0, a1c1, a1c2, a2c0, a2c1, a2c2 in _RFG_POINT.iter_unpack(datagram[_RFG_HEADER.size:]):
if word >> 24 != self._sweep_id:
if (self._sweep_id is not None and not 0 < ((word >> 24) - self._sweep_id) % 256 < 128
and received_ns - self._last_ns < 128 * self._period_ns):
continue
sweeps.extend(self._close())
self._sweep_id = word >> 24
self._last_ns = received_ns
if len(self._points) <= self._steps: # A stuck id cannot grow the buffer; the sweep is dirty.
self._points.append((word & 0xFFFFFF, complex(a1c0 - a1c1, a2c0 - a2c1) * scale,
complex(a1c2, a2c2) * scale, received_ns))
return sweeps
def _close(self) -> list[RawSweep]:
points, self._points = self._points, []
if not points:
return []
steps = np.asarray([point[0] for point in points], dtype=np.int32)
starts = [received - (step + 1) * self._step_ns for step, _, _, received in points]
start = min(starts)
# Every point of one sweep implies the same start within batching/latency jitter; an
# 8-bit id reused after an outage (256 sweeps, ~5 s) would splice two sweeps otherwise.
spliced = max(starts) - start > self._steps * self._step_ns
self._count += 1
return [RawSweep(
steps,
np.asarray([point[1] for point in points], dtype=np.complex64),
np.asarray([point[2] for point in points], dtype=np.complex64),
self._count, self._count, start, start + self._steps * self._step_ns,
dirty=spliced or not np.array_equal(steps, np.arange(self._steps)),
)]