Add RFG UDP source to rph_radar
This commit is contained in:
@@ -19,7 +19,8 @@ skips overwritten frames and continues from the retained tail:
|
|||||||
|
|
||||||
1. **Acquisition** — Python producers configure the radar, acquire frequency sweeps,
|
1. **Acquisition** — Python producers configure the radar, acquire frequency sweeps,
|
||||||
and publish raw S21 traces. Both single and multi-device LibreVNA use
|
and publish raw S21 traces. Both single and multi-device LibreVNA use
|
||||||
`matrix_raw_producer`; RadioPhotonic radar (RPH) uses `rph_radar_raw_producer` with its C++ collector.
|
`matrix_raw_producer`; RadioPhotonic radar (RPH) uses `rph_radar_raw_producer` with its C++
|
||||||
|
collector (E-502) or the RFG board's UDP stream.
|
||||||
2. **Preprocessing** — `data_preprocessor` applies through calibration,
|
2. **Preprocessing** — `data_preprocessor` applies through calibration,
|
||||||
reference subtraction, optional frequency-domain notch filtering, and an
|
reference subtraction, optional frequency-domain notch filtering, and an
|
||||||
optional object-based MIMO correction of S21.
|
optional object-based MIMO correction of S21.
|
||||||
@@ -47,9 +48,18 @@ controls, processing settings and a copy of the live plot through an embedded we
|
|||||||
- **librevna_multi** — synchronized multi-device LibreVNA.
|
- **librevna_multi** — synchronized multi-device LibreVNA.
|
||||||
- **rph_radar** — [RadioPhotonic radar (RPH) collector](data_acq_and_processing/rph_radar_collector/README.md),
|
- **rph_radar** — [RadioPhotonic radar (RPH) collector](data_acq_and_processing/rph_radar_collector/README.md),
|
||||||
with manual laser setpoints or LD1 current variation via **Apply Radar**.
|
with manual laser setpoints or LD1 current variation via **Apply Radar**.
|
||||||
The application owns the collector and exchanges complete sweeps over a PTY.
|
`radar.rph_radar.source` selects the ADC:
|
||||||
Switching to a different antenna pair starts a fresh ADC session after settling;
|
- `e502` (default) — the application owns the collector and exchanges complete
|
||||||
queued samples and an initial partial sweep are discarded before using that pair.
|
sweeps over a PTY. Switching to a different antenna pair starts a fresh ADC session
|
||||||
|
after settling, discarding queued samples and an initial partial sweep.
|
||||||
|
- `rfg_udp` — the RFG board sends averaged sweep points to `udp_bind`
|
||||||
|
(`run_config_rph_radar_rfg.example.json`); main = ch0 − ch1, reference = ch2. It
|
||||||
|
needs `laser_control.mode: variation` (even when the laser is started elsewhere):
|
||||||
|
a sweep must have exactly the task's step count, `ceil((max − min) / step)` in DAC
|
||||||
|
codes, with no lost points; others are dropped (the board loses a few percent).
|
||||||
|
Capture windows come from kernel receive timestamps. Only one antenna pair (both
|
||||||
|
switches with one position): without a source-side marker a sweep after a switch
|
||||||
|
cannot be proven fresh.
|
||||||
- **mock / simulator** — driver-less mode for development without hardware.
|
- **mock / simulator** — driver-less mode for development without hardware.
|
||||||
|
|
||||||
## Processing modes
|
## Processing modes
|
||||||
@@ -123,7 +133,7 @@ python3 -m venv .venv
|
|||||||
.venv/bin/python -m pip install --upgrade pip
|
.venv/bin/python -m pip install --upgrade pip
|
||||||
.venv/bin/python -m pip install -r requirements.txt
|
.venv/bin/python -m pip install -r requirements.txt
|
||||||
make # data_preprocessor and data_processor in build/bin/
|
make # data_preprocessor and data_processor in build/bin/
|
||||||
make rph_radar_collector # also required for RadioPhotonic radar (RPH) acquisition
|
make rph_radar_collector # RadioPhotonic radar (RPH) with radar.rph_radar.source: e502
|
||||||
```
|
```
|
||||||
|
|
||||||
`make` builds the sar3d GPU code when it finds CUDA (see Requirements) and
|
`make` builds the sar3d GPU code when it finds CUDA (see Requirements) and
|
||||||
@@ -360,7 +370,8 @@ measurement key and are not selected by the renamed mode.
|
|||||||
|
|
||||||
### RPH phase calibration
|
### RPH phase calibration
|
||||||
|
|
||||||
Use `run_config_examples/run_config_rph_radar.example.json` for **RadioPhotonic radar (RPH)**.
|
Use `run_config_examples/run_config_rph_radar.example.json` (E-502) or
|
||||||
|
`run_config_rph_radar_rfg.example.json` (RFG board) for **RadioPhotonic radar (RPH)**.
|
||||||
Apply the desired laser settings with **Apply Radar**, then stop acquisition before
|
Apply the desired laser settings with **Apply Radar**, then stop acquisition before
|
||||||
running the calibration tool:
|
running the calibration tool:
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ dual-channel main + reference IQ stream and publishes complete `RPH1` sweeps ove
|
|||||||
a PTY. `RphRadarService` in `python_app/hardware_full/rph_radar` reads and
|
a PTY. `RphRadarService` in `python_app/hardware_full/rph_radar` reads and
|
||||||
processes that stream for `python_app.scripts.rph_radar_raw_producer`.
|
processes that stream for `python_app.scripts.rph_radar_raw_producer`.
|
||||||
|
|
||||||
This is the data *source* for `radar.model == "rph_radar"`, paired with the Python
|
This is the data *source* for `radar.model == "rph_radar"` with
|
||||||
`rph_radar_raw_producer`. LibreVNA acquisition uses Python drivers.
|
`radar.rph_radar.source: "e502"`, paired with the Python `rph_radar_raw_producer`.
|
||||||
|
The `rfg_udp` source reads the RFG board's UDP stream directly and does not use this
|
||||||
|
binary. LibreVNA acquisition uses Python drivers.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ TEMP_MIN_C = 15.0 # Minimum allowed laser temperature, °C
|
|||||||
TEMP_MAX_C = 40.0 # Maximum allowed laser temperature, °C
|
TEMP_MAX_C = 40.0 # Maximum allowed laser temperature, °C
|
||||||
|
|
||||||
CURRENT_MIN_MA = 15.0 # Minimum allowed laser current, mA
|
CURRENT_MIN_MA = 15.0 # Minimum allowed laser current, mA
|
||||||
CURRENT_MAX_MA = 60.0 # Maximum allowed laser current, mA
|
CURRENT_MAX_MA = 110.0 # Maximum allowed laser current, mA (lab sweeps reach 107 mA)
|
||||||
|
|
||||||
# Variation step limits
|
# Variation step limits
|
||||||
CURRENT_STEP_MIN_MA = 0.002 # Minimum current variation step, mA
|
CURRENT_STEP_MIN_MA = 0.002 # Minimum current variation step, mA
|
||||||
|
|||||||
@@ -8,17 +8,13 @@ from .exceptions import CommunicationError
|
|||||||
from .protocol import Protocol
|
from .protocol import Protocol
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
DEFAULT_PI_P = 2560
|
|
||||||
DEFAULT_PI_I = 128
|
|
||||||
DEVICE_MAIN_MESSAGE_ID = 0x00FF
|
DEVICE_MAIN_MESSAGE_ID = 0x00FF
|
||||||
|
|
||||||
|
|
||||||
class LaserController:
|
class LaserController:
|
||||||
"""Control constant setpoints or LD1 current variation through one serial port."""
|
"""Control constant setpoints or LD1 current variation through one serial port."""
|
||||||
|
|
||||||
def __init__(self, port: str,
|
def __init__(self, port: str, *, pi_coeff1_p: int, pi_coeff1_i: int, pi_coeff2_p: int, pi_coeff2_i: int):
|
||||||
pi_coeff1_p: int = DEFAULT_PI_P, pi_coeff1_i: int = DEFAULT_PI_I,
|
|
||||||
pi_coeff2_p: int = DEFAULT_PI_P, pi_coeff2_i: int = DEFAULT_PI_I):
|
|
||||||
self._protocol = Protocol(port)
|
self._protocol = Protocol(port)
|
||||||
self._pi = dict(pi_coeff1_p=pi_coeff1_p, pi_coeff1_i=pi_coeff1_i,
|
self._pi = dict(pi_coeff1_p=pi_coeff1_p, pi_coeff1_i=pi_coeff1_i,
|
||||||
pi_coeff2_p=pi_coeff2_p, pi_coeff2_i=pi_coeff2_i)
|
pi_coeff2_p=pi_coeff2_p, pi_coeff2_i=pi_coeff2_i)
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ def current_ma_to_n(current_ma: float) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
Integer in [0, 65535] for the DAC.
|
Integer in [0, 65535] for the DAC.
|
||||||
"""
|
"""
|
||||||
n = int(ADC_BITS_16 / 2000 * RREF * current_ma)
|
# Rounded like the lab GUI (_device_main.py), so both send identical DAC codes.
|
||||||
n = max(0, min(ADC_BITS_16, n))
|
return max(0, min(ADC_BITS_16, round(ADC_BITS_16 / 2000 * RREF * current_ma)))
|
||||||
return n
|
|
||||||
|
|
||||||
|
def variation_codes(min_value: float, max_value: float, step: float) -> tuple[int, int, int]:
|
||||||
|
low, high, delta = (current_ma_to_n(value) for value in (min_value, max_value, step))
|
||||||
|
if delta == 0 or low >= high:
|
||||||
|
raise ValueError("Current variation range and step must be representable by the DAC")
|
||||||
|
return low, high, delta
|
||||||
|
|
||||||
|
|
||||||
|
def variation_step_count(min_value: float, max_value: float, step: float) -> int:
|
||||||
|
"""SWEEP_STEP edges per sweep: firmware loops ``while (current < max) current += delta`` on these codes."""
|
||||||
|
low, high, delta = variation_codes(min_value, max_value, step)
|
||||||
|
return math.ceil((high - low) / delta)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class LaserControlError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(LaserControlError):
|
class ValidationError(LaserControlError, ValueError):
|
||||||
"""Base exception for validation errors."""
|
"""Base exception for validation errors."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from operator import xor
|
|||||||
import serial
|
import serial
|
||||||
|
|
||||||
from .constants import BAUDRATE, SERIAL_TIMEOUT_SEC
|
from .constants import BAUDRATE, SERIAL_TIMEOUT_SEC
|
||||||
from .conversions import current_ma_to_n, temp_c_to_n
|
from .conversions import current_ma_to_n, temp_c_to_n, variation_codes
|
||||||
from .validators import validate_manual, validate_variation
|
from .validators import validate_manual, validate_variation
|
||||||
from .exceptions import CommunicationError, PortBusyError, ProtocolError
|
from .exceptions import CommunicationError, PortBusyError, ProtocolError
|
||||||
|
|
||||||
@@ -116,9 +116,7 @@ class Protocol:
|
|||||||
raise ValueError("radar.laser_control.variation.variation_type must be CHANGE_CURRENT_LD1")
|
raise ValueError("radar.laser_control.variation.variation_type must be CHANGE_CURRENT_LD1")
|
||||||
validate_manual(static_temp1, static_temp2, static_current1, static_current2)
|
validate_manual(static_temp1, static_temp2, static_current1, static_current2)
|
||||||
validate_variation(min_value, max_value, step, time_step, delay_time)
|
validate_variation(min_value, max_value, step, time_step, delay_time)
|
||||||
low, high, delta = (current_ma_to_n(value) for value in (min_value, max_value, step))
|
low, high, delta = variation_codes(min_value, max_value, step)
|
||||||
if delta == 0 or low >= high:
|
|
||||||
raise ValueError("Current variation range and step must be representable by the DAC")
|
|
||||||
return _command(
|
return _command(
|
||||||
0x7777, 0x37ff, 1, low, high, delta, int(time_step * 100),
|
0x7777, 0x37ff, 1, low, high, delta, int(time_step * 100),
|
||||||
temp_c_to_n(static_temp1), current_ma_to_n(static_current2), temp_c_to_n(static_temp2),
|
temp_c_to_n(static_temp1), current_ma_to_n(static_current2), temp_c_to_n(static_temp2),
|
||||||
|
|||||||
@@ -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
|
E-502 collector PTY: whole-sweep RPH1 packets. The 48-byte header carries payload
|
||||||
window, input/output (65535 for untagged) and dirty flag. Payload points are
|
size, source sweep/cycle IDs, monotonic capture window, input/output (65535 for
|
||||||
8-byte ``marker, step, I, Q`` frames: A for main and A8 for reference.
|
untagged) and dirty flag. Payload points are 8-byte ``marker, step, I, Q`` frames:
|
||||||
Only complete queued packets may be dropped; a packet already being written is
|
A for main and A8 for reference. Only complete queued packets may be dropped; a
|
||||||
always finished. Thus overflow cannot splice two physical sweeps together.
|
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
|
from dataclasses import dataclass
|
||||||
@@ -18,6 +23,8 @@ REFERENCE_MARKER = 0x00A8
|
|||||||
_HEADER = struct.Struct("<4sIQQQQHHI")
|
_HEADER = struct.Struct("<4sIQQQQHHI")
|
||||||
_POINT = struct.Struct("<HHhh")
|
_POINT = struct.Struct("<HHhh")
|
||||||
_MAX_PAYLOAD_BYTES = 65534 * 2 * _POINT.size
|
_MAX_PAYLOAD_BYTES = 65534 * 2 * _POINT.size
|
||||||
|
_RFG_HEADER = struct.Struct("<HH")
|
||||||
|
_RFG_POINT = struct.Struct("<I6i")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -87,3 +94,68 @@ class RphRadarStreamParser:
|
|||||||
None if inp == 65535 else (inp, out), bool(flags & 1),
|
None if inp == 65535 else (inp, out), bool(flags & 1),
|
||||||
))
|
))
|
||||||
return sweeps
|
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)),
|
||||||
|
)]
|
||||||
|
|||||||
@@ -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
|
With ``source == "e502"`` it owns the external collector process and its TTY; with
|
||||||
each raw (main, reference) sweep to an :class:`SweepResult` on the fixed
|
``"rfg_udp"`` it reads the RFG board's UDP stream directly. Each raw (main,
|
||||||
processing grid via :class:`RphRadarSweepProcessor`.
|
reference) sweep maps to a :class:`SweepResult` on the fixed processing grid via
|
||||||
|
:class:`RphRadarSweepProcessor`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -22,15 +23,13 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from python_app.hardware_full.rph_radar.processing import RphRadarSweepProcessor
|
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.laser_control.conversions import variation_step_count
|
||||||
from python_app.hardware_full.rph_radar.tty_reader import (
|
from python_app.hardware_full.rph_radar.protocol import RawSweep, RfgSweepAssembler
|
||||||
RphRadarTtyReader,
|
from python_app.hardware_full.rph_radar.stream_reader import RphRadarStreamReader, raise_if_process_exited
|
||||||
raise_if_process_exited,
|
|
||||||
)
|
|
||||||
from python_app.hardware_full.librevna_driver.models import SweepResult
|
from python_app.hardware_full.librevna_driver.models import SweepResult
|
||||||
from python_app.models.dataset_model import ComboKey
|
from python_app.models.dataset_model import ComboKey
|
||||||
from python_app.models.run_config_schema import RadarSweepModel, RunConfigModel
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -51,12 +50,12 @@ _STOP_KILL_GRACE_S = 0.5
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class RphRadarService:
|
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
|
config: RunConfigModel
|
||||||
collector_driven: bool = False
|
collector_driven: bool = False
|
||||||
_process: subprocess.Popen[bytes] | None = field(init=False, default=None, repr=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)
|
_processor: RphRadarSweepProcessor | None = field(init=False, default=None, repr=False)
|
||||||
_rejected_count: int = field(init=False, default=0, repr=False)
|
_rejected_count: int = field(init=False, default=0, repr=False)
|
||||||
_prepared_combo: ComboKey | None = field(init=False, default=None, repr=False)
|
_prepared_combo: ComboKey | None = field(init=False, default=None, repr=False)
|
||||||
@@ -81,13 +80,25 @@ class RphRadarService:
|
|||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def open(self, *, stop_event: threading.Event | None = None) -> None:
|
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
|
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.
|
(e.g. on shutdown) instead of blocking for the full startup timeout.
|
||||||
"""
|
"""
|
||||||
if self._reader is not None:
|
if self._reader is not None:
|
||||||
return
|
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:
|
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")
|
raise RuntimeError("Previous RPH collector did not stop; cannot start a fresh capture")
|
||||||
self._stop_event = stop_event
|
self._stop_event = stop_event
|
||||||
@@ -95,7 +106,7 @@ class RphRadarService:
|
|||||||
try:
|
try:
|
||||||
self._start_process()
|
self._start_process()
|
||||||
self._wait_for_tty(previous_tty_identity, stop_event=stop_event)
|
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()
|
reader.open()
|
||||||
self._reader = reader
|
self._reader = reader
|
||||||
logger.info("RadioPhotonic radar (RPH) service opened")
|
logger.info("RadioPhotonic radar (RPH) service opened")
|
||||||
@@ -105,7 +116,7 @@ class RphRadarService:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def close(self) -> None:
|
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")
|
logger.debug("Closing RadioPhotonic radar (RPH) service")
|
||||||
self._prepared_combo = None
|
self._prepared_combo = None
|
||||||
if self._reader is not None:
|
if self._reader is not None:
|
||||||
@@ -157,7 +168,7 @@ class RphRadarService:
|
|||||||
if self._reader is None:
|
if self._reader is None:
|
||||||
raise RuntimeError("RadioPhotonic radar (RPH) service is not open")
|
raise RuntimeError("RadioPhotonic radar (RPH) service is not open")
|
||||||
process = self._process
|
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()
|
return_code = None if process is None else process.poll()
|
||||||
raise RuntimeError(f"RadioPhotonic radar (RPH) collector is not running (code={return_code})")
|
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
|
adc = self.config.radar.rph_radar
|
||||||
validate_rph_radar_model(adc)
|
validate_rph_radar_model(adc)
|
||||||
|
if adc.source == "rfg_udp":
|
||||||
|
validate_rfg_source(self.config)
|
||||||
|
return
|
||||||
if not adc.executable_path:
|
if not adc.executable_path:
|
||||||
raise ValueError("radar.rph_radar.executable_path is required")
|
raise ValueError("radar.rph_radar.executable_path is required")
|
||||||
if not adc.tty_path:
|
if not adc.tty_path:
|
||||||
|
|||||||
+105
-33
@@ -1,8 +1,9 @@
|
|||||||
"""Background TTY reader publishing the latest completed RadioPhotonic radar (RPH) sweep.
|
"""Background reader publishing the latest completed RadioPhotonic radar (RPH) sweep.
|
||||||
|
|
||||||
The collector emits sweeps continuously, faster than callers invoke
|
The source (E-502 collector TTY or RFG board UDP socket) emits sweeps continuously,
|
||||||
:meth:`RphRadarService.acquire`. A daemon thread drains the device end of the
|
faster than callers invoke :meth:`RphRadarService.acquire`. A daemon thread drains
|
||||||
TTY non-stop, feeds the bytes to a :class:`RphRadarStreamParser`, and stores the
|
the descriptor non-stop, feeds it to :class:`RphRadarStreamParser` or
|
||||||
|
:class:`RfgSweepAssembler`, and stores the
|
||||||
most recent :class:`RawSweep` in a single-slot mailbox. :meth:`read_sweep`
|
most recent :class:`RawSweep` in a single-slot mailbox. :meth:`read_sweep`
|
||||||
returns the freshest sweep; if a newer one arrives before the consumer reads, it
|
returns the freshest sweep; if a newer one arrives before the consumer reads, it
|
||||||
overwrites the previous unread value — by design, since consumers always want the
|
overwrites the previous unread value — by design, since consumers always want the
|
||||||
@@ -12,16 +13,20 @@ thread (fail-fast; the supervisor relaunches a clean collector).
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from python_app.hardware_full.rph_radar.protocol import RphRadarStreamParser, RawSweep
|
from python_app.hardware_full.rph_radar.protocol import RawSweep, RfgSweepAssembler, RphRadarStreamParser
|
||||||
|
from python_app.models.run_config_validation import parse_udp_bind
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -31,6 +36,13 @@ _READ_CHUNK_BYTES = 65536
|
|||||||
# select() poll interval — short enough to react to close() promptly, long enough
|
# select() poll interval — short enough to react to close() promptly, long enough
|
||||||
# that idle CPU stays near zero.
|
# that idle CPU stays near zero.
|
||||||
_READ_POLL_INTERVAL_S = 0.1
|
_READ_POLL_INTERVAL_S = 0.1
|
||||||
|
# Kernel receive stamps (Linux SO_TIMESTAMPNS == SCM_TIMESTAMPNS on x86_64 and aarch64,
|
||||||
|
# a 64-bit timespec) are immune to the reader thread's scheduling stalls.
|
||||||
|
_SO_TIMESTAMPNS = 35
|
||||||
|
_TIMESPEC = struct.Struct("qq")
|
||||||
|
# A REALTIME step between the kernel stamp and the clock read would mistranslate it;
|
||||||
|
# NTP slewing moves the REALTIME-monotonic offset by microseconds only.
|
||||||
|
_CLOCK_JUMP_NS = 1_000_000
|
||||||
|
|
||||||
|
|
||||||
def raise_if_process_exited(process: subprocess.Popen[bytes] | None) -> None:
|
def raise_if_process_exited(process: subprocess.Popen[bytes] | None) -> None:
|
||||||
@@ -42,11 +54,18 @@ def raise_if_process_exited(process: subprocess.Popen[bytes] | None) -> None:
|
|||||||
raise RuntimeError(f"RadioPhotonic radar (RPH) collector exited with code {return_code}")
|
raise RuntimeError(f"RadioPhotonic radar (RPH) collector exited with code {return_code}")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
def _clock_offset_ns() -> int:
|
||||||
class RphRadarTtyReader:
|
return time.clock_gettime_ns(time.CLOCK_REALTIME) - time.monotonic_ns()
|
||||||
"""Daemon-thread TTY reader publishing the latest completed sweep."""
|
|
||||||
|
|
||||||
tty_path: str
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class RphRadarStreamReader:
|
||||||
|
"""Daemon-thread reader of the collector TTY, or of the RFG UDP stream when ``rfg`` is set."""
|
||||||
|
|
||||||
|
endpoint: str # TTY path, or HOST:PORT bind address for the RFG stream
|
||||||
|
assembler: RfgSweepAssembler | None = None
|
||||||
|
_socket: socket.socket | None = field(init=False, default=None, repr=False)
|
||||||
|
_clock_offset_ns: int | None = field(init=False, default=None, repr=False)
|
||||||
_fd: int | None = field(init=False, default=None, repr=False)
|
_fd: int | None = field(init=False, default=None, repr=False)
|
||||||
_thread: threading.Thread | None = field(init=False, default=None, repr=False)
|
_thread: threading.Thread | None = field(init=False, default=None, repr=False)
|
||||||
_stop_event: threading.Event = field(init=False, default_factory=threading.Event, repr=False)
|
_stop_event: threading.Event = field(init=False, default_factory=threading.Event, repr=False)
|
||||||
@@ -58,12 +77,29 @@ class RphRadarTtyReader:
|
|||||||
_combo_slots: dict[tuple[int, int], RawSweep] = field(init=False, default_factory=dict, repr=False)
|
_combo_slots: dict[tuple[int, int], RawSweep] = field(init=False, default_factory=dict, repr=False)
|
||||||
_reader_error: Exception | None = field(init=False, default=None, repr=False)
|
_reader_error: Exception | None = field(init=False, default=None, repr=False)
|
||||||
_published_count: int = field(init=False, default=0, repr=False)
|
_published_count: int = field(init=False, default=0, repr=False)
|
||||||
|
last_dirty_size: int | None = field(init=False, default=None)
|
||||||
|
|
||||||
def open(self) -> None:
|
def open(self) -> None:
|
||||||
"""Open the TTY and start the background reader thread."""
|
"""Open the TTY or bind the UDP socket and start the background reader thread."""
|
||||||
if self._fd is not None:
|
if self._fd is not None:
|
||||||
return
|
return
|
||||||
self._fd = os.open(self.tty_path, os.O_RDONLY | os.O_NOCTTY | os.O_NONBLOCK)
|
if self.assembler is None:
|
||||||
|
self._fd = os.open(self.endpoint, os.O_RDONLY | os.O_NOCTTY | os.O_NONBLOCK)
|
||||||
|
else:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
# Taken before bind, so a REALTIME step while the first datagrams queue is still seen.
|
||||||
|
offset_ns = _clock_offset_ns()
|
||||||
|
try:
|
||||||
|
# ~10k datagrams/s: a deep buffer rides out receiver stalls; the kernel caps it at rmem_max.
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 << 20)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, _SO_TIMESTAMPNS, 1)
|
||||||
|
sock.bind(parse_udp_bind(self.endpoint))
|
||||||
|
sock.setblocking(False)
|
||||||
|
except OSError:
|
||||||
|
sock.close()
|
||||||
|
raise
|
||||||
|
self._socket, self._fd, self._clock_offset_ns = sock, sock.fileno(), offset_ns
|
||||||
|
logger.info("RFG socket receive buffer: %d bytes", sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF))
|
||||||
self._stop_event.clear()
|
self._stop_event.clear()
|
||||||
self._latest_sweep = None
|
self._latest_sweep = None
|
||||||
self._combo_slots = {}
|
self._combo_slots = {}
|
||||||
@@ -71,21 +107,20 @@ class RphRadarTtyReader:
|
|||||||
self._published_count = 0
|
self._published_count = 0
|
||||||
self._thread = threading.Thread(
|
self._thread = threading.Thread(
|
||||||
target=self._reader_loop,
|
target=self._reader_loop,
|
||||||
name=f"rph-radar-tty-reader[{self.tty_path}]",
|
name=f"rph-radar-reader[{self.endpoint}]",
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
except Exception:
|
except Exception:
|
||||||
self._thread = None
|
self._thread = None
|
||||||
fd, self._fd = self._fd, None
|
self._close_descriptor()
|
||||||
os.close(fd)
|
|
||||||
raise
|
raise
|
||||||
logger.info("RadioPhotonic radar (RPH) TTY reader started on %s", self.tty_path)
|
logger.info("RadioPhotonic radar (RPH) reader started on %s", self.endpoint)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Stop the reader thread and close the TTY descriptor."""
|
"""Stop the reader thread and close the descriptor."""
|
||||||
logger.debug("Stopping RadioPhotonic radar (RPH) TTY reader on %s", self.tty_path)
|
logger.debug("Stopping RadioPhotonic radar (RPH) reader on %s", self.endpoint)
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
with self._mailbox_cv:
|
with self._mailbox_cv:
|
||||||
self._mailbox_cv.notify_all()
|
self._mailbox_cv.notify_all()
|
||||||
@@ -94,15 +129,18 @@ class RphRadarTtyReader:
|
|||||||
if self._thread.is_alive():
|
if self._thread.is_alive():
|
||||||
logger.warning("RadioPhotonic radar (RPH) reader thread did not stop within 1.0s")
|
logger.warning("RadioPhotonic radar (RPH) reader thread did not stop within 1.0s")
|
||||||
self._thread = None
|
self._thread = None
|
||||||
if self._fd is not None:
|
self._close_descriptor()
|
||||||
try:
|
|
||||||
os.close(self._fd)
|
|
||||||
finally:
|
|
||||||
self._fd = None
|
|
||||||
self._latest_sweep = None
|
self._latest_sweep = None
|
||||||
self._combo_slots = {}
|
self._combo_slots = {}
|
||||||
self._reader_error = None
|
self._reader_error = None
|
||||||
|
|
||||||
|
def _close_descriptor(self) -> None:
|
||||||
|
fd, sock, self._fd, self._socket = self._fd, self._socket, None, None
|
||||||
|
if sock is not None:
|
||||||
|
sock.close()
|
||||||
|
elif fd is not None:
|
||||||
|
os.close(fd)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def published_count(self) -> int:
|
def published_count(self) -> int:
|
||||||
"""Total number of sweeps the reader thread has produced."""
|
"""Total number of sweeps the reader thread has produced."""
|
||||||
@@ -123,12 +161,12 @@ class RphRadarTtyReader:
|
|||||||
error caught by the reader thread.
|
error caught by the reader thread.
|
||||||
"""
|
"""
|
||||||
if self._thread is None:
|
if self._thread is None:
|
||||||
raise RuntimeError("RadioPhotonic radar (RPH) TTY reader is not open")
|
raise RuntimeError("RadioPhotonic radar (RPH) reader is not open")
|
||||||
deadline = time.monotonic() + float(timeout_s)
|
deadline = time.monotonic() + float(timeout_s)
|
||||||
with self._mailbox_cv:
|
with self._mailbox_cv:
|
||||||
while True:
|
while True:
|
||||||
if self._stop_event.is_set():
|
if self._stop_event.is_set():
|
||||||
raise RuntimeError("RadioPhotonic radar (RPH) TTY reader is closed")
|
raise RuntimeError("RadioPhotonic radar (RPH) reader is closed")
|
||||||
# Deliver a pending sweep first: if the reader both published a
|
# Deliver a pending sweep first: if the reader both published a
|
||||||
# sweep and then died, the consumer still sees the good data and
|
# sweep and then died, the consumer still sees the good data and
|
||||||
# only meets the error on the next call.
|
# only meets the error on the next call.
|
||||||
@@ -142,23 +180,27 @@ class RphRadarTtyReader:
|
|||||||
raise_if_process_exited(process)
|
raise_if_process_exited(process)
|
||||||
remaining_s = deadline - time.monotonic()
|
remaining_s = deadline - time.monotonic()
|
||||||
if remaining_s <= 0.0:
|
if remaining_s <= 0.0:
|
||||||
|
rejected = "" if self.last_dirty_size is None else f"; last rejected sweep had {self.last_dirty_size} points"
|
||||||
raise TimeoutError(
|
raise TimeoutError(
|
||||||
f"Timed out waiting for RadioPhotonic radar (RPH) sweep after {float(timeout_s):.3f}s"
|
f"Timed out waiting for RadioPhotonic radar (RPH) sweep after {float(timeout_s):.3f}s{rejected}"
|
||||||
)
|
)
|
||||||
self._mailbox_cv.wait(timeout=min(_READ_POLL_INTERVAL_S, remaining_s))
|
self._mailbox_cv.wait(timeout=min(_READ_POLL_INTERVAL_S, remaining_s))
|
||||||
|
|
||||||
def _reader_loop(self) -> None:
|
def _reader_loop(self) -> None:
|
||||||
"""Drain the TTY, parse sweeps, and publish each completed one until stop."""
|
"""Drain the descriptor, parse sweeps, and publish each completed one until stop."""
|
||||||
parser = RphRadarStreamParser()
|
parser = RphRadarStreamParser()
|
||||||
try:
|
try:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
|
if self.assembler is None:
|
||||||
chunk = self._read_available()
|
chunk = self._read_available()
|
||||||
if not chunk:
|
sweeps = parser.feed(chunk) if chunk else []
|
||||||
continue
|
else:
|
||||||
for sweep in parser.feed(chunk):
|
datagram = self._receive_datagram()
|
||||||
|
sweeps = self.assembler.feed(*datagram) if datagram is not None else []
|
||||||
|
for sweep in sweeps:
|
||||||
self._publish_sweep(sweep)
|
self._publish_sweep(sweep)
|
||||||
except Exception as exc: # noqa: BLE001 — surfaced to the consumer via read_sweep
|
except Exception as exc: # noqa: BLE001 — surfaced to the consumer via read_sweep
|
||||||
logger.exception("RadioPhotonic radar (RPH) reader thread failed on %s", self.tty_path)
|
logger.exception("RadioPhotonic radar (RPH) reader thread failed on %s", self.endpoint)
|
||||||
self._publish_error(exc)
|
self._publish_error(exc)
|
||||||
|
|
||||||
def _read_available(self) -> bytes:
|
def _read_available(self) -> bytes:
|
||||||
@@ -183,15 +225,44 @@ class RphRadarTtyReader:
|
|||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
if exc.errno in {errno.EAGAIN, errno.EWOULDBLOCK}:
|
if exc.errno in {errno.EAGAIN, errno.EWOULDBLOCK}:
|
||||||
return b""
|
return b""
|
||||||
raise RuntimeError(f"Failed to read RadioPhotonic radar (RPH) TTY `{self.tty_path}`: {exc}") from exc
|
raise RuntimeError(f"Failed to read RadioPhotonic radar (RPH) source `{self.endpoint}`: {exc}") from exc
|
||||||
if not chunk:
|
if not chunk:
|
||||||
raise RuntimeError(f"RadioPhotonic radar (RPH) TTY `{self.tty_path}` closed while reading")
|
raise RuntimeError(f"RadioPhotonic radar (RPH) source `{self.endpoint}` closed while reading")
|
||||||
return chunk
|
return chunk
|
||||||
|
|
||||||
|
def _receive_datagram(self) -> tuple[bytes, int] | None:
|
||||||
|
"""Return one RFG datagram with its kernel receive time on the monotonic clock."""
|
||||||
|
sock = self._socket
|
||||||
|
if sock is None or self._stop_event.is_set():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
readable, _, _ = select.select([sock], [], [], _READ_POLL_INTERVAL_S)
|
||||||
|
if not readable:
|
||||||
|
return None
|
||||||
|
datagram, ancillary, _, _ = sock.recvmsg(_READ_CHUNK_BYTES, socket.CMSG_SPACE(_TIMESPEC.size))
|
||||||
|
except (BlockingIOError, InterruptedError):
|
||||||
|
return None
|
||||||
|
offset_ns, now_ns = _clock_offset_ns(), time.monotonic_ns()
|
||||||
|
stamps = [data for level, kind, data in ancillary if level == socket.SOL_SOCKET and kind == _SO_TIMESTAMPNS]
|
||||||
|
if not stamps:
|
||||||
|
raise RuntimeError("RFG datagram arrived without a kernel receive timestamp")
|
||||||
|
seconds, nanoseconds = _TIMESPEC.unpack(stamps[0])
|
||||||
|
received_ns = seconds * 1_000_000_000 + nanoseconds - offset_ns
|
||||||
|
previous, self._clock_offset_ns = self._clock_offset_ns, offset_ns
|
||||||
|
if previous is None or abs(offset_ns - previous) > _CLOCK_JUMP_NS or received_ns > now_ns:
|
||||||
|
# Queued stamps may predate the step: drop them and the sweep in progress.
|
||||||
|
with suppress(BlockingIOError):
|
||||||
|
while True:
|
||||||
|
sock.recv(_READ_CHUNK_BYTES)
|
||||||
|
assert self.assembler is not None
|
||||||
|
self.assembler.reset()
|
||||||
|
return None
|
||||||
|
return datagram, received_ns
|
||||||
|
|
||||||
def _publish_sweep(self, sweep: RawSweep) -> None:
|
def _publish_sweep(self, sweep: RawSweep) -> None:
|
||||||
"""Publish a completed sweep to the mailbox(es), waking any waiter.
|
"""Publish a completed sweep to the mailbox(es), waking any waiter.
|
||||||
|
|
||||||
Dirty sweeps (those that straddled a switch transition) are counted but not
|
Dirty sweeps (straddling a switch, or incomplete RFG sweeps) are counted but not
|
||||||
delivered: the collector re-takes that combination on the next sweep. Clean
|
delivered: the collector re-takes that combination on the next sweep. Clean
|
||||||
tagged sweeps go to their per-combo slot; untagged sweeps (non-switch mode)
|
tagged sweeps go to their per-combo slot; untagged sweeps (non-switch mode)
|
||||||
only update the single newest-sweep mailbox that read_sweep() serves.
|
only update the single newest-sweep mailbox that read_sweep() serves.
|
||||||
@@ -199,6 +270,7 @@ class RphRadarTtyReader:
|
|||||||
with self._mailbox_cv:
|
with self._mailbox_cv:
|
||||||
self._published_count += 1
|
self._published_count += 1
|
||||||
if sweep.dirty:
|
if sweep.dirty:
|
||||||
|
self.last_dirty_size = sweep.size
|
||||||
self._mailbox_cv.notify_all()
|
self._mailbox_cv.notify_all()
|
||||||
return
|
return
|
||||||
self._latest_sweep = sweep
|
self._latest_sweep = sweep
|
||||||
@@ -165,6 +165,8 @@ def run_config_from_dict(payload: object) -> RunConfigModel:
|
|||||||
"stimulus_switch_positions",
|
"stimulus_switch_positions",
|
||||||
model.radar.multi_device.stimulus_switch_positions,
|
model.radar.multi_device.stimulus_switch_positions,
|
||||||
)
|
)
|
||||||
|
model.radar.rph_radar.source = _read_str(rph_radar_payload, "source", model.radar.rph_radar.source)
|
||||||
|
model.radar.rph_radar.udp_bind = _read_str(rph_radar_payload, "udp_bind", model.radar.rph_radar.udp_bind)
|
||||||
model.radar.rph_radar.project_dir = _read_str(
|
model.radar.rph_radar.project_dir = _read_str(
|
||||||
rph_radar_payload, "project_dir", model.radar.rph_radar.project_dir
|
rph_radar_payload, "project_dir", model.radar.rph_radar.project_dir
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,8 +89,12 @@ class RphRadarBandModel:
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class RphRadarModel:
|
class RphRadarModel:
|
||||||
"""External RadioPhotonic radar (RPH) acquisition process settings."""
|
"""RadioPhotonic radar (RPH) acquisition: E-502 collector process or RFG board UDP stream."""
|
||||||
|
|
||||||
|
# "e502": the C++ collector streams over tty_path (project_dir..stop_timeout_s are its
|
||||||
|
# settings); "rfg_udp": the RFG ADC board sends averaged sweep points to udp_bind.
|
||||||
|
source: str = "e502"
|
||||||
|
udp_bind: str = "0.0.0.0:5000"
|
||||||
project_dir: str = ""
|
project_dir: str = ""
|
||||||
executable_path: str = ""
|
executable_path: str = ""
|
||||||
tty_path: str = ""
|
tty_path: str = ""
|
||||||
@@ -107,12 +111,12 @@ class RphRadarModel:
|
|||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class LaserManualModeModel:
|
class LaserManualModeModel:
|
||||||
"""Manual laser-control setpoints."""
|
"""Manual laser-control setpoints (defaults: the lab GUI, RadioPhotonic_PCB_PC_software)."""
|
||||||
|
|
||||||
temp1: float = 25.0
|
temp1: float = 24.0
|
||||||
temp2: float = 25.0
|
temp2: float = 24.9
|
||||||
current1: float = 30.0
|
current1: float = 97.3
|
||||||
current2: float = 30.0
|
current2: float = 102.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -120,15 +124,15 @@ class LaserVariationModeModel:
|
|||||||
"""Laser-control variation task parameters."""
|
"""Laser-control variation task parameters."""
|
||||||
|
|
||||||
variation_type: str = "CHANGE_CURRENT_LD1"
|
variation_type: str = "CHANGE_CURRENT_LD1"
|
||||||
static_temp1: float = 25.0
|
static_temp1: float = 24.0
|
||||||
static_temp2: float = 25.0
|
static_temp2: float = 24.9
|
||||||
static_current1: float = 30.0
|
static_current1: float = 97.3
|
||||||
static_current2: float = 30.0
|
static_current2: float = 102.0
|
||||||
min_value: float = 30.0
|
min_value: float = 97.3
|
||||||
max_value: float = 35.0
|
max_value: float = 107.0
|
||||||
step: float = 0.1
|
step: float = 0.05
|
||||||
time_step: int = 20
|
time_step: int = 50
|
||||||
delay_time: int = 3
|
delay_time: int = 10
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -138,10 +142,10 @@ class LaserControlModel:
|
|||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
port: str = ""
|
port: str = ""
|
||||||
mode: str = "manual"
|
mode: str = "manual"
|
||||||
pi_coeff1_p: int = 2560
|
pi_coeff1_p: int = 1280
|
||||||
pi_coeff1_i: int = 128
|
pi_coeff1_i: int = 25
|
||||||
pi_coeff2_p: int = 2560
|
pi_coeff2_p: int = 1280
|
||||||
pi_coeff2_i: int = 128
|
pi_coeff2_i: int = 179
|
||||||
manual: LaserManualModeModel = field(default_factory=LaserManualModeModel)
|
manual: LaserManualModeModel = field(default_factory=LaserManualModeModel)
|
||||||
variation: LaserVariationModeModel = field(default_factory=LaserVariationModeModel)
|
variation: LaserVariationModeModel = field(default_factory=LaserVariationModeModel)
|
||||||
|
|
||||||
@@ -410,10 +414,14 @@ class RunConfigModel:
|
|||||||
laser = radar.laser_control
|
laser = radar.laser_control
|
||||||
mode = laser.mode.strip().lower()
|
mode = laser.mode.strip().lower()
|
||||||
optical = asdict(laser.variation if mode == "variation" else laser.manual)
|
optical = asdict(laser.variation if mode == "variation" else laser.manual)
|
||||||
physical = (asdict(radar.rph_radar.band), asdict(radar.rph_radar.phase_calibration),
|
rph = radar.rph_radar
|
||||||
radar.rph_radar.args, (self.input_switch.positions, self.output_switch.positions),
|
# The RFG source always runs the profile's sweep (its length and step timing), even when
|
||||||
|
# the laser is started elsewhere; E-502 keys stay as they were before the source field.
|
||||||
|
rfg = rph.source == "rfg_udp"
|
||||||
|
physical = (asdict(rph.band), asdict(rph.phase_calibration), rph.source if rfg else rph.args,
|
||||||
|
(self.input_switch.positions, self.output_switch.positions),
|
||||||
(mode, optical, laser.pi_coeff1_p, laser.pi_coeff1_i,
|
(mode, optical, laser.pi_coeff1_p, laser.pi_coeff1_i,
|
||||||
laser.pi_coeff2_p, laser.pi_coeff2_i) if laser.enabled else None)
|
laser.pi_coeff2_p, laser.pi_coeff2_i) if laser.enabled or rfg else None)
|
||||||
digest = hashlib.sha256(json.dumps(physical, sort_keys=True).encode()).hexdigest()[:16]
|
digest = hashlib.sha256(json.dumps(physical, sort_keys=True).encode()).hexdigest()[:16]
|
||||||
return f"{radar.model}_{digest}"
|
return f"{radar.model}_{digest}"
|
||||||
serials = [radar.serial or "no_serial"]
|
serials = [radar.serial or "no_serial"]
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from python_app.hardware_full.laser_control.conversions import variation_step_count
|
||||||
|
from python_app.hardware_full.laser_control.validators import validate_variation
|
||||||
from python_app.models.json_fields import (
|
from python_app.models.json_fields import (
|
||||||
finite_number,
|
finite_number,
|
||||||
read_bool as _require_bool, read_int as _require_int, read_str as _require_str,
|
read_bool as _require_bool, read_int as _require_int, read_str as _require_str,
|
||||||
@@ -138,6 +141,8 @@ def validate_acquisition_config(config: RunConfigModel) -> None:
|
|||||||
|
|
||||||
if config.is_rph_radar:
|
if config.is_rph_radar:
|
||||||
validate_rph_radar_model(config.radar.rph_radar)
|
validate_rph_radar_model(config.radar.rph_radar)
|
||||||
|
if config.radar.rph_radar.source == "rfg_udp":
|
||||||
|
validate_rfg_source(config)
|
||||||
|
|
||||||
for switch, physical_positions in (
|
for switch, physical_positions in (
|
||||||
(config.input_switch, config.matrix_input_switch_positions),
|
(config.input_switch, config.matrix_input_switch_positions),
|
||||||
@@ -170,8 +175,40 @@ def validate_acquisition_config(config: RunConfigModel) -> None:
|
|||||||
raise ValueError(f"rings.{name}.slot_size_bytes must be at least {minimum} for this sweep")
|
raise ValueError(f"rings.{name}.slot_size_bytes must be at least {minimum} for this sweep")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_udp_bind(text: str) -> tuple[str, int]:
|
||||||
|
"""``HOST:PORT`` (empty host = all interfaces) -> socket address."""
|
||||||
|
host, colon, port = text.rpartition(":")
|
||||||
|
try:
|
||||||
|
ipaddress.IPv4Address(host or "0.0.0.0")
|
||||||
|
except ValueError:
|
||||||
|
colon = ""
|
||||||
|
if not colon or not port.isdigit() or not 1 <= int(port) <= 65535:
|
||||||
|
raise ValueError(f"radar.rph_radar.udp_bind must be IPV4:PORT, got {text!r}")
|
||||||
|
return host or "0.0.0.0", int(port)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_rfg_source(config: RunConfigModel) -> None:
|
||||||
|
"""The RFG board numbers steps of the laser current sweep; its exact length must be known."""
|
||||||
|
laser = config.radar.laser_control
|
||||||
|
if laser.mode.strip().lower() != "variation":
|
||||||
|
raise ValueError("radar.rph_radar.source rfg_udp requires radar.laser_control.mode 'variation'")
|
||||||
|
variation = laser.variation
|
||||||
|
validate_variation(variation.min_value, variation.max_value, variation.step,
|
||||||
|
variation.time_step, variation.delay_time)
|
||||||
|
if variation_step_count(variation.min_value, variation.max_value, variation.step) < 2:
|
||||||
|
raise ValueError("The laser current sweep must have at least two steps")
|
||||||
|
# Without a source-side marker a sweep after a switch cannot be proven fresh, so every
|
||||||
|
# capture path (run combos and the full calibration matrix) must see a single pair.
|
||||||
|
if config.input_switch.positions != 1 or config.output_switch.positions != 1:
|
||||||
|
raise ValueError("radar.rph_radar.source rfg_udp supports one antenna pair: both switches need 1 position")
|
||||||
|
|
||||||
|
|
||||||
def validate_rph_radar_model(adc: RphRadarModel) -> None:
|
def validate_rph_radar_model(adc: RphRadarModel) -> None:
|
||||||
"""Reject phase/grid errors before stopping a running pipeline."""
|
"""Reject phase/grid errors before stopping a running pipeline."""
|
||||||
|
if adc.source not in ("e502", "rfg_udp"):
|
||||||
|
raise ValueError("radar.rph_radar.source must be e502 or rfg_udp")
|
||||||
|
if adc.source == "rfg_udp":
|
||||||
|
parse_udp_bind(adc.udp_bind)
|
||||||
phase, band = adc.phase_calibration, adc.band
|
phase, band = adc.phase_calibration, adc.band
|
||||||
for name, value in {**asdict(phase), **asdict(band)}.items():
|
for name, value in {**asdict(phase), **asdict(band)}.items():
|
||||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
|
||||||
|
|||||||
@@ -58,11 +58,12 @@ def main() -> int:
|
|||||||
cleanup = ExitStack()
|
cleanup = ExitStack()
|
||||||
try:
|
try:
|
||||||
publisher = RawPublisher(config.rings, cleanup)
|
publisher = RawPublisher(config.rings, cleanup)
|
||||||
# Switch-aware mode: with native switches the collector drives the RF switches
|
# Switch-aware mode: with native switches the E-502 collector drives the RF switches
|
||||||
# itself and tags each sweep with its combo and source cycle. Otherwise Python
|
# itself and tags each sweep with its combo and source cycle. Otherwise Python
|
||||||
# owns the switches and starts a fresh collector session after each changed pair.
|
# owns the switches and starts a fresh source session after each changed pair.
|
||||||
collector_driven = (
|
collector_driven = (
|
||||||
config.input_switch.driver_mode == "native"
|
config.radar.rph_radar.source == "e502"
|
||||||
|
and config.input_switch.driver_mode == "native"
|
||||||
and config.output_switch.driver_mode == "native"
|
and config.output_switch.driver_mode == "native"
|
||||||
)
|
)
|
||||||
radar = RphRadarService(
|
radar = RphRadarService(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from python_app.hardware_full.rph_radar.tty_reader import RphRadarTtyReader
|
from python_app.hardware_full.rph_radar.stream_reader import RphRadarStreamReader
|
||||||
from python_app.hardware_full.laser_control.protocol import Protocol
|
from python_app.hardware_full.laser_control.protocol import Protocol
|
||||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||||
from python_app.hardware_full.librevna_driver.api.vna import VNAController
|
from python_app.hardware_full.librevna_driver.api.vna import VNAController
|
||||||
@@ -252,7 +252,7 @@ class TtyLifecycleTest(unittest.TestCase):
|
|||||||
with self.subTest(combination=combination):
|
with self.subTest(combination=combination):
|
||||||
master, slave = pty.openpty()
|
master, slave = pty.openpty()
|
||||||
tty.setraw(slave)
|
tty.setraw(slave)
|
||||||
reader = RphRadarTtyReader(os.ttyname(slave))
|
reader = RphRadarStreamReader(os.ttyname(slave))
|
||||||
errors = []
|
errors = []
|
||||||
started = threading.Event()
|
started = threading.Event()
|
||||||
reader.open()
|
reader.open()
|
||||||
@@ -280,7 +280,7 @@ class TtyLifecycleTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_failed_reader_thread_start_closes_descriptor(self) -> None:
|
def test_failed_reader_thread_start_closes_descriptor(self) -> None:
|
||||||
read_fd, write_fd = os.pipe()
|
read_fd, write_fd = os.pipe()
|
||||||
reader = RphRadarTtyReader("/dev/test")
|
reader = RphRadarStreamReader("/dev/test")
|
||||||
try:
|
try:
|
||||||
with patch("os.open", return_value=read_fd), patch("threading.Thread.start", side_effect=RuntimeError("thread failed")):
|
with patch("os.open", return_value=read_fd), patch("threading.Thread.start", side_effect=RuntimeError("thread failed")):
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(RuntimeError):
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ from unittest.mock import Mock, patch
|
|||||||
from python_app.hardware_full.laser_control.controller import DEVICE_MAIN_MESSAGE_ID, LaserController
|
from python_app.hardware_full.laser_control.controller import DEVICE_MAIN_MESSAGE_ID, LaserController
|
||||||
from python_app.hardware_full.laser_control.exceptions import CommunicationError, ValidationError
|
from python_app.hardware_full.laser_control.exceptions import CommunicationError, ValidationError
|
||||||
from python_app.hardware_full.laser_control.protocol import Protocol
|
from python_app.hardware_full.laser_control.protocol import Protocol
|
||||||
|
from python_app.hardware_full.laser_control.conversions import variation_step_count
|
||||||
from python_app.hardware_full.rph_radar.laser import apply_rph_radar_laser_control
|
from python_app.hardware_full.rph_radar.laser import apply_rph_radar_laser_control
|
||||||
from python_app.models.run_config_schema import RunConfigModel
|
from python_app.models.run_config_schema import RunConfigModel
|
||||||
|
|
||||||
|
|
||||||
DEVICE_MAIN_MANUAL_HEX = "1111ff37ffa518ab000000000000000a8000000a8000ff003d2acc2c163f"
|
# Generated by the lab GUI encoder (RadioPhotonic_PCB_PC_software device_commands) for these inputs.
|
||||||
|
DEVICE_MAIN_MANUAL_HEX = "1111ff37ffa518ab000000000000000a8000000a8000ff003d2acd2c173f"
|
||||||
DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX = (
|
DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX = (
|
||||||
"7777ff3701003d2acc2c10008813ffa5cc2c18ab0a00000a8000000a8000b600"
|
"7777ff3701003d2acd2c10008813ffa5cd2c18ab0a00000a8000000a8000b600"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -82,6 +84,9 @@ class LaserControlProtocolCompatibilityTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(ld1_command.hex(), DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX)
|
self.assertEqual(ld1_command.hex(), DEVICE_MAIN_CHANGE_CURRENT_LD1_HEX)
|
||||||
|
|
||||||
|
def test_sweep_step_count_follows_the_firmware_loop(self) -> None:
|
||||||
|
self.assertEqual(variation_step_count(97.3, 107, 0.05), 199) # 199 steps seen on the RFG stand
|
||||||
|
|
||||||
def test_start_sequence_matches_device_main_order(self) -> None:
|
def test_start_sequence_matches_device_main_order(self) -> None:
|
||||||
cleanup = ExitStack()
|
cleanup = ExitStack()
|
||||||
self.addCleanup(cleanup.close)
|
self.addCleanup(cleanup.close)
|
||||||
@@ -198,7 +203,7 @@ class LaserControlProtocolCompatibilityTest(unittest.TestCase):
|
|||||||
apply_rph_radar_laser_control(config)
|
apply_rph_radar_laser_control(config)
|
||||||
|
|
||||||
def test_state_failure_cannot_report_command_success(self) -> None:
|
def test_state_failure_cannot_report_command_success(self) -> None:
|
||||||
controller = LaserController("/dev/ttyLASER")
|
controller = LaserController("/dev/ttyLASER", pi_coeff1_p=1280, pi_coeff1_i=25, pi_coeff2_p=1280, pi_coeff2_i=179)
|
||||||
for reply in (b"", b"\x00", b"\x04\x00", b"\x03\x00"):
|
for reply in (b"", b"\x00", b"\x04\x00", b"\x03\x00"):
|
||||||
with self.subTest(reply=reply), patch.object(controller, "_protocol") as protocol:
|
with self.subTest(reply=reply), patch.object(controller, "_protocol") as protocol:
|
||||||
protocol.receive_raw.return_value = reply
|
protocol.receive_raw.return_value = reply
|
||||||
@@ -208,9 +213,9 @@ class LaserControlProtocolCompatibilityTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_invalid_settings_are_rejected_before_connect_or_reset(self) -> None:
|
def test_invalid_settings_are_rejected_before_connect_or_reset(self) -> None:
|
||||||
for mode, field, value in (("manual", "temp1", 100.0),
|
for mode, field, value in (("manual", "temp1", 100.0),
|
||||||
("variation", "static_current2", 100.0),
|
("variation", "static_current2", 150.0),
|
||||||
("variation", "step", 0.0),
|
("variation", "step", 0.0),
|
||||||
("variation", "step", 0.002),
|
("variation", "max_value", 97.3001), # same DAC code as min
|
||||||
("variation", "variation_type", "CHANGE_TEMPERATURE_LD1"),
|
("variation", "variation_type", "CHANGE_TEMPERATURE_LD1"),
|
||||||
("pi", "pi_coeff1_p", 65536)):
|
("pi", "pi_coeff1_p", 65536)):
|
||||||
config = self._rph_config({"enabled": True, "port": "/dev/ttyUSB0", "mode": "manual" if mode == "pi" else mode})
|
config = self._rph_config({"enabled": True, "port": "/dev/ttyUSB0", "mode": "manual" if mode == "pi" else mode})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import unittest
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from python_app.hardware_full.rph_radar.protocol import RphRadarStreamParser
|
from python_app.hardware_full.rph_radar.protocol import RfgSweepAssembler, RphRadarStreamParser
|
||||||
|
|
||||||
|
|
||||||
def packet(*, source_id=1, cycle_id=1, start=100, end=200, combo=(1, 2), dirty=0, points=None):
|
def packet(*, source_id=1, cycle_id=1, start=100, end=200, combo=(1, 2), dirty=0, points=None):
|
||||||
@@ -65,5 +65,80 @@ class RphRadarStreamParserTest(unittest.TestCase):
|
|||||||
self.assertTrue(sweep.dirty)
|
self.assertTrue(sweep.dirty)
|
||||||
|
|
||||||
|
|
||||||
|
def rfg_datagram(sweep_id, *steps, values=(3, 1, 5, 4, 2, 6), q_shift=27):
|
||||||
|
"""RFG SWEEP_AVG datagram; ``values`` are in units of 2**(q_shift - 7)."""
|
||||||
|
return struct.pack("<HH", 6, q_shift) + b"".join(
|
||||||
|
struct.pack("<I6i", (sweep_id << 24) | step, *[value << (q_shift - 7) for value in values]) for step in steps)
|
||||||
|
|
||||||
|
|
||||||
|
class RfgSweepAssemblerTest(unittest.TestCase):
|
||||||
|
STEP_NS = 50_000
|
||||||
|
PERIOD_NS = 20_000_000
|
||||||
|
|
||||||
|
def feed(self, assembler, sweep_id, steps, start_ns, delay_ns=7_000):
|
||||||
|
sweeps = []
|
||||||
|
for step in steps:
|
||||||
|
sweeps += assembler.feed(rfg_datagram(sweep_id, step), start_ns + (step + 1) * self.STEP_NS + delay_ns)
|
||||||
|
return sweeps
|
||||||
|
|
||||||
|
def test_complete_sweep_maps_sync_detector_and_reference_and_its_true_window(self):
|
||||||
|
assembler = RfgSweepAssembler(steps=3, step_ns=self.STEP_NS, period_ns=self.PERIOD_NS)
|
||||||
|
self.feed(assembler, 9, [0, 1, 2], 1_000_000_000)
|
||||||
|
(sweep,) = assembler.feed(rfg_datagram(10, 0), 1_020_000_000)
|
||||||
|
self.assertFalse(sweep.dirty)
|
||||||
|
self.assertEqual(sweep.steps.tolist(), [0, 1, 2])
|
||||||
|
np.testing.assert_allclose(sweep.main, [(3 - 1 + 1j * (4 - 2)) / 128] * 3) # ch0 - ch1
|
||||||
|
np.testing.assert_allclose(sweep.reference, [(5 + 6j) / 128] * 3) # ch2
|
||||||
|
self.assertEqual(sweep.capture_start_ns, 1_000_007_000)
|
||||||
|
self.assertEqual(sweep.capture_end_ns - sweep.capture_start_ns, 3 * self.STEP_NS)
|
||||||
|
|
||||||
|
def test_window_takes_the_least_delayed_point_when_the_board_batches(self):
|
||||||
|
assembler = RfgSweepAssembler(steps=3, step_ns=self.STEP_NS, period_ns=self.PERIOD_NS)
|
||||||
|
start = 2_000_000_000
|
||||||
|
assembler.feed(rfg_datagram(1, 0, 1), start + 2 * self.STEP_NS + 10_000) # steps 0-1 batched
|
||||||
|
assembler.feed(rfg_datagram(1, 2), start + 3 * self.STEP_NS + 10_000)
|
||||||
|
(sweep,) = assembler.feed(rfg_datagram(2, 0), start + 30_000_000)
|
||||||
|
self.assertEqual(sweep.capture_start_ns, start + 10_000)
|
||||||
|
|
||||||
|
def test_points_of_older_sweeps_are_ignored_until_the_id_space_could_have_wrapped(self):
|
||||||
|
assembler = RfgSweepAssembler(steps=3, step_ns=self.STEP_NS, period_ns=self.PERIOD_NS)
|
||||||
|
sweeps = []
|
||||||
|
for sweep_id, start in ((1, 0), (3, 20_000_000), (2, 40_000_000), (4, 60_000_000), (3, 80_000_000)):
|
||||||
|
sweeps += self.feed(assembler, sweep_id, [0, 1, 2], start)
|
||||||
|
self.assertEqual([sweep.source_id for sweep in sweeps], [1, 2]) # late 2 and replayed 3 are dropped
|
||||||
|
sweeps = self.feed(assembler, 2, [0, 1, 2], 80_000_000 + 128 * self.PERIOD_NS) # 4 closes, 2 is new
|
||||||
|
sweeps += self.feed(assembler, 3, [0], 100_000_000 + 128 * self.PERIOD_NS)
|
||||||
|
self.assertEqual([(sweep.source_id, sweep.dirty) for sweep in sweeps], [(3, False), (4, False)])
|
||||||
|
|
||||||
|
def test_reset_turns_the_rest_of_the_sweep_in_progress_dirty(self):
|
||||||
|
assembler = RfgSweepAssembler(steps=3, step_ns=self.STEP_NS, period_ns=self.PERIOD_NS)
|
||||||
|
self.feed(assembler, 4, [0], 0)
|
||||||
|
assembler.reset()
|
||||||
|
self.feed(assembler, 4, [1, 2], 0)
|
||||||
|
(sweep,) = self.feed(assembler, 5, [0], 20_000_000)
|
||||||
|
self.assertTrue(sweep.dirty)
|
||||||
|
|
||||||
|
def test_partial_gapped_shortened_or_reordered_sweeps_are_dirty(self):
|
||||||
|
for steps in ([1, 2], [0, 2], [0, 1], [0, 2, 1], [0, 1, 1, 2]):
|
||||||
|
assembler = RfgSweepAssembler(steps=3, step_ns=self.STEP_NS, period_ns=self.PERIOD_NS)
|
||||||
|
self.feed(assembler, 255, steps, 0)
|
||||||
|
with self.subTest(steps=steps):
|
||||||
|
(sweep,) = assembler.feed(rfg_datagram(0, 0), 10_000_000) # 255 -> 0 wraps normally
|
||||||
|
self.assertTrue(sweep.dirty)
|
||||||
|
|
||||||
|
def test_a_reused_id_after_an_outage_does_not_splice_two_sweeps(self):
|
||||||
|
assembler = RfgSweepAssembler(steps=3, step_ns=self.STEP_NS, period_ns=self.PERIOD_NS)
|
||||||
|
self.feed(assembler, 5, [0], 0)
|
||||||
|
self.feed(assembler, 5, [1, 2], 4_920_000_000) # 256 sweep periods later
|
||||||
|
(sweep,) = self.feed(assembler, 6, [0], 4_940_000_000)
|
||||||
|
self.assertTrue(sweep.dirty)
|
||||||
|
|
||||||
|
def test_unexpected_layout_fails_fast(self):
|
||||||
|
for datagram in (b"\x06", struct.pack("<HH", 5, 27), struct.pack("<HH", 6, 31) + rfg_datagram(1, 0)[4:],
|
||||||
|
rfg_datagram(1, 0)[:-1]):
|
||||||
|
with self.subTest(datagram=datagram), self.assertRaisesRegex(ValueError, "protocol"):
|
||||||
|
RfgSweepAssembler(steps=3, step_ns=1, period_ns=1).feed(datagram, 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pty
|
import pty
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -16,14 +17,15 @@ from unittest import mock
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from python_app.hardware_full.rph_radar.service import RphRadarService
|
from python_app.hardware_full.rph_radar.service import RphRadarService
|
||||||
from python_app.hardware_full.rph_radar.tty_reader import RphRadarTtyReader
|
from python_app.hardware_full.rph_radar import stream_reader
|
||||||
|
from python_app.hardware_full.rph_radar.stream_reader import RphRadarStreamReader
|
||||||
from python_app.hardware_full.rph_radar.protocol import RphRadarStreamParser
|
from python_app.hardware_full.rph_radar.protocol import RphRadarStreamParser
|
||||||
from python_app.hardware_full.librevna_driver.models import SweepResult
|
from python_app.hardware_full.librevna_driver.models import SweepResult
|
||||||
from python_app.models.dataset_model import ComboKey
|
from python_app.models.dataset_model import ComboKey
|
||||||
from python_app.models.run_config_schema import RunConfigModel
|
from python_app.models.run_config_schema import RunConfigModel
|
||||||
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
||||||
from python_app.scripts import rph_radar_raw_producer as producer
|
from python_app.scripts import rph_radar_raw_producer as producer
|
||||||
from python_app.tests.test_rph_radar_protocol import packet
|
from python_app.tests.test_rph_radar_protocol import packet, rfg_datagram
|
||||||
|
|
||||||
|
|
||||||
def config():
|
def config():
|
||||||
@@ -35,11 +37,61 @@ def config():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class RphRadarTtyReaderTest(unittest.TestCase):
|
class RfgStreamServiceTest(unittest.TestCase):
|
||||||
|
def test_udp_stream_delivers_complete_sweeps_of_the_laser_task_length(self):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
|
||||||
|
probe.bind(("127.0.0.1", 0))
|
||||||
|
port = probe.getsockname()[1]
|
||||||
|
rfg = RunConfigModel.from_dict({
|
||||||
|
"radar": {"model": "rph_radar", "driver_mode": "native",
|
||||||
|
"rph_radar": {"source": "rfg_udp", "udp_bind": f"127.0.0.1:{port}"},
|
||||||
|
"laser_control": {"mode": "variation", "variation": {
|
||||||
|
"min_value": 30.0, "max_value": 30.15, "step": 0.05, "time_step": 50}}},
|
||||||
|
"switches": {"port1": {"positions": 1}, "port2": {"positions": 1}},
|
||||||
|
"run": {"continuous": False, "combos": [{"input": 0, "output": 0}]},
|
||||||
|
})
|
||||||
|
service = RphRadarService(rfg)
|
||||||
|
service.open()
|
||||||
|
self.addCleanup(service.close)
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as board:
|
||||||
|
for datagram in (rfg_datagram(7, 2, 3), rfg_datagram(8, 0, 1, 2, 3), rfg_datagram(9, 0)):
|
||||||
|
board.sendto(datagram, ("127.0.0.1", port))
|
||||||
|
raw = service.read_raw_sweep()
|
||||||
|
self.assertEqual(raw.steps.tolist(), [0, 1, 2, 3]) # ceil((9879 - 9830) / 16) DAC codes
|
||||||
|
self.assertFalse(raw.dirty)
|
||||||
|
self.assertEqual(raw.capture_end_ns - raw.capture_start_ns, 4 * 50_000)
|
||||||
|
|
||||||
|
|
||||||
|
class RfgClockJumpTest(unittest.TestCase):
|
||||||
|
def test_a_realtime_step_drops_queued_datagrams_and_the_sweep_in_progress(self):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
|
||||||
|
probe.bind(("127.0.0.1", 0))
|
||||||
|
port = probe.getsockname()[1]
|
||||||
|
assembler = mock.Mock(feed=mock.Mock(return_value=[]))
|
||||||
|
reader = RphRadarStreamReader(f"127.0.0.1:{port}", assembler)
|
||||||
|
real_offset, step = stream_reader._clock_offset_ns, [0]
|
||||||
|
with mock.patch.object(stream_reader, "_clock_offset_ns", lambda: real_offset() + step[0]):
|
||||||
|
reader.open()
|
||||||
|
self.addCleanup(reader.close)
|
||||||
|
step[0] = 2_000_000 # REALTIME stepped 2 ms after bind (stamps stay real, so forward)
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as board:
|
||||||
|
board.sendto(rfg_datagram(1, 0), ("127.0.0.1", port))
|
||||||
|
deadline = time.monotonic() + 1
|
||||||
|
while not assembler.reset.called and time.monotonic() < deadline:
|
||||||
|
time.sleep(.005)
|
||||||
|
assembler.feed.assert_not_called()
|
||||||
|
board.sendto(rfg_datagram(1, 1), ("127.0.0.1", port))
|
||||||
|
while not assembler.feed.called and time.monotonic() < deadline:
|
||||||
|
time.sleep(.005)
|
||||||
|
assembler.reset.assert_called_once()
|
||||||
|
assembler.feed.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class RphRadarStreamReaderTest(unittest.TestCase):
|
||||||
def test_pty_delivers_latest_complete_clean_sweeps_by_pair(self):
|
def test_pty_delivers_latest_complete_clean_sweeps_by_pair(self):
|
||||||
master, slave = pty.openpty()
|
master, slave = pty.openpty()
|
||||||
tty.setraw(slave)
|
tty.setraw(slave)
|
||||||
reader = RphRadarTtyReader(os.ttyname(slave))
|
reader = RphRadarStreamReader(os.ttyname(slave))
|
||||||
reader.open()
|
reader.open()
|
||||||
try:
|
try:
|
||||||
os.write(master, packet(source_id=1, combo=(0, 0))
|
os.write(master, packet(source_id=1, combo=(0, 0))
|
||||||
@@ -216,7 +268,7 @@ class RphRadarConfigTest(unittest.TestCase):
|
|||||||
with mock.patch("python_app.hardware_full.rph_radar.service._prepare_tty_path_for_collector") as unlink, \
|
with mock.patch("python_app.hardware_full.rph_radar.service._prepare_tty_path_for_collector") as unlink, \
|
||||||
mock.patch.object(RphRadarService, "_start_process") as start, \
|
mock.patch.object(RphRadarService, "_start_process") as start, \
|
||||||
mock.patch.object(RphRadarService, "_wait_for_tty"), \
|
mock.patch.object(RphRadarService, "_wait_for_tty"), \
|
||||||
mock.patch("python_app.hardware_full.rph_radar.service.RphRadarTtyReader"):
|
mock.patch("python_app.hardware_full.rph_radar.service.RphRadarStreamReader"):
|
||||||
with self.assertRaisesRegex(RuntimeError, "did not stop"):
|
with self.assertRaisesRegex(RuntimeError, "did not stop"):
|
||||||
service.open()
|
service.open()
|
||||||
unlink.assert_not_called()
|
unlink.assert_not_called()
|
||||||
|
|||||||
@@ -183,6 +183,38 @@ class AcquisitionValidationTest(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(ValueError, "native"):
|
with self.assertRaisesRegex(ValueError, "native"):
|
||||||
validate_acquisition_config(self.config("rph_radar"))
|
validate_acquisition_config(self.config("rph_radar"))
|
||||||
|
|
||||||
|
def test_rfg_source_needs_a_known_laser_sweep_one_pair_and_a_bind_address(self) -> None:
|
||||||
|
config = self.config("rph_radar")
|
||||||
|
config.radar.driver_mode = "native"
|
||||||
|
config.radar.rph_radar.source = "rfg_udp"
|
||||||
|
config.radar.laser_control.mode = "variation"
|
||||||
|
config.input_switch.positions = config.output_switch.positions = 1
|
||||||
|
config.combos = config.combos[:1]
|
||||||
|
validate_acquisition_config(config)
|
||||||
|
restored = RunConfigModel.from_dict(config.to_dict())
|
||||||
|
self.assertEqual((restored.radar.rph_radar.source, restored.radar.rph_radar.udp_bind),
|
||||||
|
("rfg_udp", "0.0.0.0:5000"))
|
||||||
|
e502 = RunConfigModel.from_dict(config.to_dict())
|
||||||
|
e502.radar.rph_radar.source = "e502"
|
||||||
|
self.assertNotEqual(config.radar_key(), e502.radar_key())
|
||||||
|
for field, value, message in (("source", "pcie", "source"), ("udp_bind", "5000", "udp_bind"),
|
||||||
|
("udp_bind", "192.168.2.1:70000", "udp_bind"),
|
||||||
|
("udp_bind", "192.168.2.l:5000", "udp_bind")):
|
||||||
|
broken = RunConfigModel.from_dict(config.to_dict())
|
||||||
|
setattr(broken.radar.rph_radar, field, value)
|
||||||
|
with self.subTest(field=field, value=value), self.assertRaisesRegex(ValueError, message):
|
||||||
|
validate_acquisition_config(broken)
|
||||||
|
for change, message in (
|
||||||
|
(lambda c: setattr(c.radar.laser_control, "mode", "manual"), "variation"),
|
||||||
|
(lambda c: setattr(c.input_switch, "positions", 2), "one antenna pair"),
|
||||||
|
(lambda c: setattr(c.radar.laser_control.variation, "time_step", 0), "time_step"),
|
||||||
|
(lambda c: setattr(c.radar.laser_control.variation, "max_value", 97.32), "two steps"),
|
||||||
|
):
|
||||||
|
broken = RunConfigModel.from_dict(config.to_dict())
|
||||||
|
change(broken)
|
||||||
|
with self.subTest(message=message), self.assertRaisesRegex(ValueError, message):
|
||||||
|
validate_acquisition_config(broken)
|
||||||
|
|
||||||
def test_adc_phase_band_and_timeouts_fail_preflight(self) -> None:
|
def test_adc_phase_band_and_timeouts_fail_preflight(self) -> None:
|
||||||
for section, field, value in (
|
for section, field, value in (
|
||||||
("phase_calibration", "phase1_rad", 0.0),
|
("phase_calibration", "phase1_rad", 0.0),
|
||||||
|
|||||||
+17
-17
@@ -18,27 +18,27 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "manual",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
@@ -11,28 +11,28 @@
|
|||||||
"laser_control": {
|
"laser_control": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
@@ -14,28 +14,28 @@
|
|||||||
"laser_control": {
|
"laser_control": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
@@ -14,28 +14,28 @@
|
|||||||
"laser_control": {
|
"laser_control": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
@@ -17,28 +17,28 @@
|
|||||||
"laser_control": {
|
"laser_control": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
@@ -17,28 +17,28 @@
|
|||||||
"laser_control": {
|
"laser_control": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"radar": {
|
"radar": {
|
||||||
"model": "rph_radar",
|
"model": "rph_radar",
|
||||||
"serial": "kamil_adc",
|
|
||||||
"driver_mode": "native",
|
"driver_mode": "native",
|
||||||
"multi_device": {
|
"multi_device": {
|
||||||
"slave_serials": [],
|
"slave_serials": [],
|
||||||
@@ -47,24 +46,24 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"port": "/dev/ttyUSB0",
|
"port": "/dev/ttyUSB0",
|
||||||
"mode": "variation",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 28.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 28.9,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 33.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 35.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 33.0,
|
"min_value": 97.3,
|
||||||
"max_value": 60.0,
|
"max_value": 107.0,
|
||||||
"step": 0.05,
|
"step": 0.05,
|
||||||
"time_step": 50,
|
"time_step": 50,
|
||||||
"delay_time": 10
|
"delay_time": 10
|
||||||
@@ -220,27 +219,27 @@
|
|||||||
},
|
},
|
||||||
"rings": {
|
"rings": {
|
||||||
"raw": {
|
"raw": {
|
||||||
"name": "/radar_raw_kamil_adc",
|
"name": "/radar_raw_rph_radar",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"raw_tap": {
|
"raw_tap": {
|
||||||
"name": "/radar_raw_tap_kamil_adc",
|
"name": "/radar_raw_tap_rph_radar",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"preprocessed": {
|
"preprocessed": {
|
||||||
"name": "/radar_preprocessed_kamil_adc",
|
"name": "/radar_preprocessed_rph_radar",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"preprocessed_tap": {
|
"preprocessed_tap": {
|
||||||
"name": "/radar_preprocessed_tap_kamil_adc",
|
"name": "/radar_preprocessed_tap_rph_radar",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
"name": "/radar_results_kamil_adc",
|
"name": "/radar_results_rph_radar",
|
||||||
"capacity": 50,
|
"capacity": 50,
|
||||||
"slot_size_bytes": 2097152
|
"slot_size_bytes": 2097152
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"radar": {
|
||||||
|
"model": "rph_radar",
|
||||||
|
"driver_mode": "native",
|
||||||
|
"rph_radar": {
|
||||||
|
"source": "rfg_udp",
|
||||||
|
"udp_bind": "192.168.2.1:5000",
|
||||||
|
"sweep_timeout_s": 15.0,
|
||||||
|
"phase_calibration": {
|
||||||
|
"phase0_rad": -1.6053902786733596,
|
||||||
|
"freq0_hz": 2046000000.0,
|
||||||
|
"phase1_rad": 197.7844181612878,
|
||||||
|
"freq1_hz": 5612000000.0
|
||||||
|
},
|
||||||
|
"band": {
|
||||||
|
"start_hz": 2100000000.0,
|
||||||
|
"stop_hz": 5500000000.0,
|
||||||
|
"points": 2048
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"laser_control": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": "/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0",
|
||||||
|
"mode": "variation",
|
||||||
|
"pi_coeff1_p": 1280,
|
||||||
|
"pi_coeff1_i": 25,
|
||||||
|
"pi_coeff2_p": 1280,
|
||||||
|
"pi_coeff2_i": 179,
|
||||||
|
"manual": {
|
||||||
|
"temp1": 24.0,
|
||||||
|
"temp2": 24.9,
|
||||||
|
"current1": 97.3,
|
||||||
|
"current2": 102.0
|
||||||
|
},
|
||||||
|
"variation": {
|
||||||
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
|
"static_temp1": 24.0,
|
||||||
|
"static_temp2": 24.9,
|
||||||
|
"static_current1": 97.3,
|
||||||
|
"static_current2": 102.0,
|
||||||
|
"min_value": 97.3,
|
||||||
|
"max_value": 107.0,
|
||||||
|
"step": 0.05,
|
||||||
|
"time_step": 50,
|
||||||
|
"delay_time": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"switches": {
|
||||||
|
"port1": {
|
||||||
|
"name": "port1",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"radar_port": 1,
|
||||||
|
"positions": 1,
|
||||||
|
"default_position": 0
|
||||||
|
},
|
||||||
|
"port2": {
|
||||||
|
"name": "port2",
|
||||||
|
"driver_mode": "mock",
|
||||||
|
"radar_port": 2,
|
||||||
|
"positions": 1,
|
||||||
|
"default_position": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"run": {
|
||||||
|
"settling_ms": 0,
|
||||||
|
"idle_sleep_ms": 2,
|
||||||
|
"continuous": true,
|
||||||
|
"processing_live_config_path": "python_app/runtime/processing_live.json",
|
||||||
|
"locator_server": {
|
||||||
|
"device_id": 3,
|
||||||
|
"protocol_version": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8888,
|
||||||
|
"max_payload_bytes": 65536,
|
||||||
|
"client_queue_size": 32,
|
||||||
|
"logger_name": "locator_runtime"
|
||||||
|
},
|
||||||
|
"combos": [
|
||||||
|
{
|
||||||
|
"input": 0,
|
||||||
|
"output": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"preprocess": {
|
||||||
|
"s21": {
|
||||||
|
"calibration": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"set_name": "",
|
||||||
|
"bundle_path": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notch": {
|
||||||
|
"enabled": true,
|
||||||
|
"bands_hz": [],
|
||||||
|
"taper_width_hz": 40000000.0,
|
||||||
|
"taper_type": "cosine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gpr": {
|
||||||
|
"relative_permittivity": 1.0,
|
||||||
|
"tx_geometry": [
|
||||||
|
{
|
||||||
|
"output_pos": 0,
|
||||||
|
"x_m": 0.905,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rx_geometry": [
|
||||||
|
{
|
||||||
|
"input_pos": 0,
|
||||||
|
"x_m": -0.18,
|
||||||
|
"y_m": 0.0,
|
||||||
|
"z_m": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rings": {
|
||||||
|
"raw": {
|
||||||
|
"name": "/radar_raw_rph_radar",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"raw_tap": {
|
||||||
|
"name": "/radar_raw_tap_rph_radar",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed": {
|
||||||
|
"name": "/radar_preprocessed_rph_radar",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"preprocessed_tap": {
|
||||||
|
"name": "/radar_preprocessed_tap_rph_radar",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"name": "/radar_results_rph_radar",
|
||||||
|
"capacity": 50,
|
||||||
|
"slot_size_bytes": 2097152
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,28 +11,28 @@
|
|||||||
"laser_control": {
|
"laser_control": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "variation",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
+17
-17
@@ -12,27 +12,27 @@
|
|||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": "",
|
"port": "",
|
||||||
"mode": "manual",
|
"mode": "manual",
|
||||||
"pi_coeff1_p": 2560,
|
"pi_coeff1_p": 1280,
|
||||||
"pi_coeff1_i": 128,
|
"pi_coeff1_i": 25,
|
||||||
"pi_coeff2_p": 2560,
|
"pi_coeff2_p": 1280,
|
||||||
"pi_coeff2_i": 128,
|
"pi_coeff2_i": 179,
|
||||||
"manual": {
|
"manual": {
|
||||||
"temp1": 25.0,
|
"temp1": 24.0,
|
||||||
"temp2": 25.0,
|
"temp2": 24.9,
|
||||||
"current1": 30.0,
|
"current1": 97.3,
|
||||||
"current2": 30.0
|
"current2": 102.0
|
||||||
},
|
},
|
||||||
"variation": {
|
"variation": {
|
||||||
"variation_type": "CHANGE_CURRENT_LD1",
|
"variation_type": "CHANGE_CURRENT_LD1",
|
||||||
"static_temp1": 25.0,
|
"static_temp1": 24.0,
|
||||||
"static_temp2": 25.0,
|
"static_temp2": 24.9,
|
||||||
"static_current1": 30.0,
|
"static_current1": 97.3,
|
||||||
"static_current2": 30.0,
|
"static_current2": 102.0,
|
||||||
"min_value": 30.0,
|
"min_value": 97.3,
|
||||||
"max_value": 35.0,
|
"max_value": 107.0,
|
||||||
"step": 0.1,
|
"step": 0.05,
|
||||||
"time_step": 20,
|
"time_step": 50,
|
||||||
"delay_time": 3
|
"delay_time": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sweep": {
|
"sweep": {
|
||||||
|
|||||||
Reference in New Issue
Block a user