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
+30 -16
View File
@@ -1,8 +1,9 @@
"""Service that launches the RadioPhotonic radar (RPH) collector and serves processed sweeps.
"""Service that serves processed RadioPhotonic radar (RPH) sweeps.
Owns the lifecycle of the external collector process and its TTY reader, and maps
each raw (main, reference) sweep to an :class:`SweepResult` on the fixed
processing grid via :class:`RphRadarSweepProcessor`.
With ``source == "e502"`` it owns the external collector process and its TTY; with
``"rfg_udp"`` it reads the RFG board's UDP stream directly. Each raw (main,
reference) sweep maps to a :class:`SweepResult` on the fixed processing grid via
:class:`RphRadarSweepProcessor`.
"""
from __future__ import annotations
@@ -22,15 +23,13 @@ import time
import numpy as np
from python_app.hardware_full.rph_radar.processing import RphRadarSweepProcessor
from python_app.hardware_full.rph_radar.protocol import RawSweep
from python_app.hardware_full.rph_radar.tty_reader import (
RphRadarTtyReader,
raise_if_process_exited,
)
from python_app.hardware_full.laser_control.conversions import variation_step_count
from python_app.hardware_full.rph_radar.protocol import RawSweep, RfgSweepAssembler
from python_app.hardware_full.rph_radar.stream_reader import RphRadarStreamReader, raise_if_process_exited
from python_app.hardware_full.librevna_driver.models import SweepResult
from python_app.models.dataset_model import ComboKey
from python_app.models.run_config_schema import RadarSweepModel, RunConfigModel
from python_app.models.run_config_validation import validate_rph_radar_model
from python_app.models.run_config_validation import validate_rfg_source, validate_rph_radar_model
logger = logging.getLogger(__name__)
@@ -51,12 +50,12 @@ _STOP_KILL_GRACE_S = 0.5
@dataclass(slots=True)
class RphRadarService:
"""Launch the external RadioPhotonic radar (RPH) collector and serve its processed sweeps."""
"""Serve processed RadioPhotonic radar (RPH) sweeps from the E-502 collector or the RFG stream."""
config: RunConfigModel
collector_driven: bool = False
_process: subprocess.Popen[bytes] | None = field(init=False, default=None, repr=False)
_reader: RphRadarTtyReader | None = field(init=False, default=None, repr=False)
_reader: RphRadarStreamReader | None = field(init=False, default=None, repr=False)
_processor: RphRadarSweepProcessor | None = field(init=False, default=None, repr=False)
_rejected_count: int = field(init=False, default=0, repr=False)
_prepared_combo: ComboKey | None = field(init=False, default=None, repr=False)
@@ -81,13 +80,25 @@ class RphRadarService:
return cmd
def open(self, *, stop_event: threading.Event | None = None) -> None:
"""Launch the collector and start the TTY reader thread.
"""Bind the RFG UDP stream, or launch the E-502 collector and start its TTY reader.
An optional ``stop_event`` lets a caller abort the TTY-wait loop promptly
(e.g. on shutdown) instead of blocking for the full startup timeout.
"""
if self._reader is not None:
return
adc = self.config.radar.rph_radar
if adc.source == "rfg_udp":
variation = self.config.radar.laser_control.variation
steps = variation_step_count(variation.min_value, variation.max_value, variation.step)
step_ns = variation.time_step * 1000
# Lower bound of the sweep period: the firmware idles at least delay_time - 1 ms.
period_ns = steps * step_ns + (variation.delay_time - 1) * 1_000_000
reader = RphRadarStreamReader(adc.udp_bind, RfgSweepAssembler(steps, step_ns, period_ns))
reader.open()
self._reader = reader
logger.info("RadioPhotonic radar (RPH) RFG stream opened on %s (%d steps per sweep)", adc.udp_bind, steps)
return
if self._process is not None and self._process.poll() is None:
raise RuntimeError("Previous RPH collector did not stop; cannot start a fresh capture")
self._stop_event = stop_event
@@ -95,7 +106,7 @@ class RphRadarService:
try:
self._start_process()
self._wait_for_tty(previous_tty_identity, stop_event=stop_event)
reader = RphRadarTtyReader(self.config.radar.rph_radar.tty_path)
reader = RphRadarStreamReader(self.config.radar.rph_radar.tty_path)
reader.open()
self._reader = reader
logger.info("RadioPhotonic radar (RPH) service opened")
@@ -105,7 +116,7 @@ class RphRadarService:
raise
def close(self) -> None:
"""Stop the TTY reader and the external collector process."""
"""Stop the reader and, for E-502, the external collector process."""
logger.debug("Closing RadioPhotonic radar (RPH) service")
self._prepared_combo = None
if self._reader is not None:
@@ -157,7 +168,7 @@ class RphRadarService:
if self._reader is None:
raise RuntimeError("RadioPhotonic radar (RPH) service is not open")
process = self._process
if process is None or process.poll() is not None:
if self.config.radar.rph_radar.source == "e502" and (process is None or process.poll() is not None):
return_code = None if process is None else process.poll()
raise RuntimeError(f"RadioPhotonic radar (RPH) collector is not running (code={return_code})")
@@ -334,6 +345,9 @@ class RphRadarService:
adc = self.config.radar.rph_radar
validate_rph_radar_model(adc)
if adc.source == "rfg_udp":
validate_rfg_source(self.config)
return
if not adc.executable_path:
raise ValueError("radar.rph_radar.executable_path is required")
if not adc.tty_path: