Add RFG UDP source to rph_radar
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user