Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
+30 -28
View File
@@ -5,11 +5,11 @@ from __future__ import annotations
import logging
import mmap
from pathlib import Path
import struct
import time
from typing import Final
from python_app.models.dataset_model import ResultCollection, SweepCollection
from python_app.orchestration.shm import atomics
from python_app.orchestration.shm.decoder import (
PREPROC_MAGIC,
RAW_MAGIC,
@@ -30,8 +30,8 @@ class ShmRingReader:
def __init__(self, ring_name: str, open_timeout_s: float = 2.0, open_poll_s: float = 0.01) -> None:
"""Open and validate ring by name, for example `/radar_results`."""
if not ring_name.startswith("/"):
raise ValueError("ring_name must start with '/'")
if not ring_name.startswith("/") or len(ring_name) < 2 or "/" in ring_name[1:] or "\x00" in ring_name:
raise ValueError("ring_name must be '/' followed by a single nonempty filename")
self._ring_name = ring_name
self._path = Path("/dev/shm") / ring_name[1:]
@@ -79,7 +79,7 @@ class ShmRingReader:
if sequence != read_seq + 1:
# Producer overwrote this slot before we read it. Resync to latest.
logger.debug("Ring %s: slot lapped before read, resyncing to write_seq=%d", self._ring_name, write_seq)
self._write_u64(32, write_seq)
atomics.advance(self._mmap, 32, write_seq)
return None
payload_size = self._read_u32(slot_offset)
@@ -92,20 +92,21 @@ class ShmRingReader:
payload_size,
self.slot_size_bytes,
)
self._write_u64(32, write_seq)
atomics.advance(self._mmap, 32, write_seq)
return None
payload_offset = slot_offset + _SLOT_HEADER_SIZE
payload = bytes(self._mmap[payload_offset : payload_offset + payload_size])
atomics.fence()
# Re-read the slot sequence after the copy; if it changed, the producer
# overwrote this slot mid-copy and the payload is torn — discard and resync.
if self._read_u64(slot_offset + 8) != read_seq + 1:
logger.debug("Ring %s: slot overwritten mid-copy, discarding torn payload", self._ring_name)
self._write_u64(32, write_seq)
atomics.advance(self._mmap, 32, write_seq)
return None
self._write_u64(32, read_seq + 1)
atomics.advance(self._mmap, 32, read_seq + 1)
return payload
def pop_raw_collection(self) -> SweepCollection | None:
@@ -157,17 +158,11 @@ class ShmRingReader:
return None
payload_offset = slot_offset + _SLOT_HEADER_SIZE
payload = bytes(self._mmap[payload_offset : payload_offset + payload_size])
atomics.fence()
if self._read_u64(slot_offset + 8) != latest_seq + 1:
return None
return payload
def peek_latest_result_collection(self) -> ResultCollection | None:
"""Return the most recently published result collection without consuming it."""
payload = self.peek_latest_payload()
if payload is None:
return None
return decode_result_collection(payload)
def drop_all(self) -> int:
"""Mark all unread slots as consumed and return number of dropped payloads."""
write_seq = self._read_u64(24)
@@ -175,7 +170,7 @@ class ShmRingReader:
if read_seq >= write_seq:
return 0
dropped = int(write_seq - read_seq)
self._write_u64(32, write_seq)
atomics.advance(self._mmap, 32, write_seq)
logger.debug("Ring %s: dropped %d unread payload(s)", self._ring_name, dropped)
return dropped
@@ -196,7 +191,11 @@ class ShmRingReader:
magic = self._mmap[:8]
version = self._read_u32(8)
if magic == _MAGIC and version == _VERSION:
return
capacity = self.capacity
slot_size = self.slot_size_bytes
expected_size = _HEADER_SIZE + capacity * (_SLOT_HEADER_SIZE + slot_size)
if capacity > 0 and slot_size > 0 and slot_size % 8 == 0 and expected_size == len(self._mmap):
return
if time.monotonic() >= deadline:
if magic != _MAGIC:
@@ -206,29 +205,32 @@ class ShmRingReader:
f"Shared memory ring magic mismatch for {self._ring_name}: "
f"got=0x{got_magic}, expected=0x{expected_magic}"
)
raise RuntimeError(
f"Shared memory ring version mismatch for {self._ring_name}: "
f"got={version}, expected={_VERSION}"
)
if version != _VERSION:
raise RuntimeError(
f"Shared memory ring version mismatch for {self._ring_name}: "
f"got={version}, expected={_VERSION}"
)
raise RuntimeError(f"Shared memory ring geometry mismatch for {self._ring_name}")
time.sleep(poll_s)
def _wait_for_ring_file(self, timeout_s: float, poll_s: float) -> None:
"""Wait until ring file appears in `/dev/shm`."""
"""Wait for the creator to allocate the ring before mapping its header."""
deadline = time.monotonic() + timeout_s
while not self._path.exists():
while True:
try:
if self._path.stat().st_size >= _HEADER_SIZE:
return
except FileNotFoundError:
pass
if time.monotonic() >= deadline:
raise FileNotFoundError(f"Shared memory ring does not exist: {self._ring_name}")
time.sleep(poll_s)
def _read_u32(self, offset: int) -> int:
"""Read little-endian u32 at mmap offset."""
return struct.unpack_from("<I", self._mmap, offset)[0]
return atomics.load(self._mmap, offset, 4)
def _read_u64(self, offset: int) -> int:
"""Read little-endian u64 at mmap offset."""
return struct.unpack_from("<Q", self._mmap, offset)[0]
def _write_u64(self, offset: int, value: int) -> None:
"""Write little-endian u64 at mmap offset."""
struct.pack_into("<Q", self._mmap, offset, value)
return atomics.load(self._mmap, offset, 8)