Rework radar system
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""Shared CLI, interruptible device retry, and raw-ring publication."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from contextlib import ExitStack, suppress
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import threading
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
from python_app.logging_setup import coerce_level
|
||||
from python_app.models.dataset_model import SweepCollection
|
||||
from python_app.models.run_config_schema import RingEndpointModel, RingsModel, RunConfigModel
|
||||
from python_app.models.run_config_validation import validate_acquisition_config
|
||||
from python_app.orchestration.shm.decoder import RAW_MAGIC
|
||||
from python_app.orchestration.shm.ring_writer import ShmRingWriter
|
||||
from python_app.storage.npz.serialize import serialize_trace_collection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def load_producer_config(description: str) -> tuple[Path, RunConfigModel, threading.Event]:
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
parser.add_argument("--config", required=True, type=Path, help="Path to run_config.json")
|
||||
path = parser.parse_args().config
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
stop_requested = threading.Event()
|
||||
|
||||
def request_stop(_signum: int, _frame: object) -> None:
|
||||
stop_requested.set()
|
||||
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
config = RunConfigModel.load_from_path(path)
|
||||
config.ensure_combos()
|
||||
validate_acquisition_config(config)
|
||||
logging.getLogger("python_app").setLevel(coerce_level(config.logging.level))
|
||||
return path, config, stop_requested
|
||||
|
||||
|
||||
class _Closable(Protocol):
|
||||
def close(self) -> None: ...
|
||||
|
||||
|
||||
def close_devices(*devices: _Closable | None) -> None:
|
||||
"""Attempt every hardware cleanup even if one disconnected device raises."""
|
||||
for device in devices:
|
||||
if device is not None:
|
||||
with suppress(Exception):
|
||||
device.close()
|
||||
|
||||
|
||||
def retry_open(
|
||||
attempt: Callable[[], T], stop_requested: threading.Event, device_name: str,
|
||||
) -> T | None:
|
||||
"""Retry an attempt that cleans up on failure, with capped, interruptible backoff."""
|
||||
failures = 0
|
||||
delay = 1.0
|
||||
while not stop_requested.is_set():
|
||||
try:
|
||||
device = attempt()
|
||||
except Exception as exc:
|
||||
failures += 1
|
||||
if failures == 1 or failures % 30 == 0:
|
||||
logger.warning("%s unavailable (attempt %d); retrying until present: %s", device_name, failures, exc)
|
||||
if stop_requested.wait(delay):
|
||||
break
|
||||
delay = min(delay * 2, 10.0)
|
||||
else:
|
||||
if failures:
|
||||
logger.info("%s opened after %d attempt(s)", device_name, failures + 1)
|
||||
return device
|
||||
return None
|
||||
|
||||
|
||||
class RawPublisher:
|
||||
"""Publish one validated frame to raw and, when enabled, its independent GUI tap."""
|
||||
|
||||
def __init__(self, rings: RingsModel, cleanup: ExitStack) -> None:
|
||||
def open_writer(endpoint: RingEndpointModel) -> ShmRingWriter:
|
||||
writer = ShmRingWriter(endpoint.name, endpoint.capacity, endpoint.slot_size_bytes)
|
||||
cleanup.callback(writer.close)
|
||||
return writer
|
||||
|
||||
self._raw = open_writer(rings.raw)
|
||||
self._tap = open_writer(rings.raw_tap) if rings.raw_tap.name else None
|
||||
self._dropped = 0
|
||||
|
||||
def publish(self, collection: SweepCollection) -> bool:
|
||||
payload = serialize_trace_collection(collection, RAW_MAGIC, require_capture_window=True)
|
||||
if not self._raw.push(payload):
|
||||
self._dropped += 1
|
||||
if self._dropped == 1 or self._dropped % 100 == 0:
|
||||
logger.error(
|
||||
"Raw payload %d B exceeds ring slot %d B; dropping collection %d (drops=%d)",
|
||||
len(payload), self._raw.slot_size_bytes, collection.collection_id, self._dropped,
|
||||
)
|
||||
return False
|
||||
if self._tap is not None:
|
||||
self._tap.push(payload) # independent latest-wins tap; never backpressure raw
|
||||
return True
|
||||
Reference in New Issue
Block a user