some kamil_adc fixes
This commit is contained in:
@@ -47,6 +47,10 @@ _REJECT_LOG_EVERY = 50
|
||||
# brief window to release the device cleanly before escalating to SIGKILL. Caps
|
||||
# the configured stop_timeout_s so a stop can never hang.
|
||||
_STOP_KILL_GRACE_S = 0.5
|
||||
# Sweeps to skip after a Python-driven switch change before trusting a capture: one
|
||||
# for the pre-switch sweep still in the mailbox, one for a possible transition
|
||||
# straddler in flight. Calibration speed is not critical, so we err on safety.
|
||||
_SWITCH_DRAIN_SWEEPS = 2
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -54,6 +58,10 @@ class KamilAdcService:
|
||||
"""Launch the external Kamil ADC collector and serve its processed sweeps."""
|
||||
|
||||
config: RunConfigModel
|
||||
# When set, the collector is launched with ``config:<path>`` so it drives the RF
|
||||
# switches itself (switch-aware mode) from this run_config.json. ``None`` keeps
|
||||
# the standalone collector that streams a single channel (calibration / mock).
|
||||
switch_config_path: str | None = None
|
||||
_process: subprocess.Popen[bytes] | None = field(init=False, default=None, repr=False)
|
||||
_reader: KamilAdcTtyReader | None = field(init=False, default=None, repr=False)
|
||||
_processor: KamilAdcSweepProcessor | None = field(init=False, default=None, repr=False)
|
||||
@@ -64,9 +72,18 @@ class KamilAdcService:
|
||||
|
||||
@property
|
||||
def command(self) -> list[str]:
|
||||
"""External collector command, including the generated ``tty:`` argument."""
|
||||
"""External collector command, including the generated ``tty:`` argument.
|
||||
|
||||
In switch-aware mode (``switch_config_path`` set) the collector also gets
|
||||
``config:<path>`` so it reads the switch/combo configuration and drives the
|
||||
switches in lock-step with the sweeps.
|
||||
"""
|
||||
adc = self.config.radar.kamil_adc
|
||||
return [str(self._resolve_executable()), *adc.args, f"tty:{adc.tty_path}"]
|
||||
cmd = [str(self._resolve_executable()), *adc.args]
|
||||
if self.switch_config_path is not None:
|
||||
cmd.append(f"config:{self.switch_config_path}")
|
||||
cmd.append(f"tty:{adc.tty_path}")
|
||||
return cmd
|
||||
|
||||
def open(self, *, stop_event: threading.Event | None = None) -> None:
|
||||
"""Launch the collector and start the TTY reader thread.
|
||||
@@ -122,12 +139,17 @@ class KamilAdcService:
|
||||
"""Kamil ADC has no runtime-readable sweep-limit API."""
|
||||
raise RuntimeError("Kamil ADC device limits are not available")
|
||||
|
||||
def acquire(self) -> SweepResult:
|
||||
def acquire(self, combo: tuple[int, int] | None = None) -> SweepResult:
|
||||
"""Return the next sweep that covers the band, as S21 on the fixed grid.
|
||||
|
||||
Sweeps whose floated frequency range does not span the configured band are
|
||||
rejected and the next sweep is read, until one passes or the sweep timeout
|
||||
elapses (which then surfaces as a :class:`TimeoutError`).
|
||||
|
||||
When ``combo`` is given (switch-aware mode), only the clean sweep captured
|
||||
under that switch combination is returned; the collector drives the switches
|
||||
and tags each sweep. When ``None`` (calibration / non-switch mode), the
|
||||
single newest sweep is returned regardless of combination.
|
||||
"""
|
||||
if self._processor is None:
|
||||
raise RuntimeError("Kamil ADC service is not configured")
|
||||
@@ -147,7 +169,10 @@ class KamilAdcService:
|
||||
raise TimeoutError(
|
||||
"Timed out waiting for a Kamil ADC sweep covering the configured band"
|
||||
)
|
||||
raw = self._reader.read_sweep(timeout_s=remaining_s, process=process)
|
||||
if combo is None:
|
||||
raw = self._reader.read_sweep(timeout_s=remaining_s, process=process)
|
||||
else:
|
||||
raw = self._reader.read_sweep_for(combo, timeout_s=remaining_s, process=process)
|
||||
s21 = self._processor.process(raw.main, raw.reference)
|
||||
if s21 is not None:
|
||||
return SweepResult(
|
||||
@@ -159,6 +184,31 @@ class KamilAdcService:
|
||||
)
|
||||
self._log_rejected_sweep(raw)
|
||||
|
||||
def drain_after_switch(self, sweeps: int = _SWITCH_DRAIN_SWEEPS) -> None:
|
||||
"""Discard sweeps captured before / across a just-applied switch change.
|
||||
|
||||
The collector free-runs, so right after the RF switches move the reader
|
||||
still holds a sweep captured in the *previous* combination, and a sweep that
|
||||
straddles the transition may still be in flight. Without this, the next
|
||||
:meth:`acquire` would return that stale data and the capture would be
|
||||
attributed to the wrong combination (an off-by-one across the sequence).
|
||||
|
||||
Block until ``sweeps`` freshly-published sweeps have gone by, so the next
|
||||
:meth:`acquire` returns a sweep captured entirely in the new switch state.
|
||||
Used by the Python-driven calibration capture, where the collector does not
|
||||
tag sweeps; the switch-aware collector path handles this with combo tags
|
||||
instead. No-op when the service is not open.
|
||||
"""
|
||||
if self._reader is None:
|
||||
return
|
||||
target = self._reader.published_count + max(1, int(sweeps))
|
||||
deadline = time.monotonic() + self.config.radar.kamil_adc.sweep_timeout_s
|
||||
while self._reader.published_count < target:
|
||||
if time.monotonic() > deadline:
|
||||
raise TimeoutError("Timed out draining Kamil ADC sweeps after a switch change")
|
||||
raise_if_process_exited(self._process)
|
||||
time.sleep(0.005)
|
||||
|
||||
def read_raw_sweep(self) -> RawSweep:
|
||||
"""Return the next raw (main, reference) sweep without any processing.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user