Rename kamil_adc to rph_radar and fix its data path
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
"""Standalone laser temperature checker process.
|
||||
|
||||
Reads the target setpoints frozen at variation start and tails the JSONL readings
|
||||
channel produced by the monitor. For every reading it validates both lasers and
|
||||
prints a console warning whenever a measured temperature drifts from its target by
|
||||
more than the tolerance (default 0.03 °C). Never touches the serial port, so it is
|
||||
fully independent of the monitor and can be started/stopped at any time.
|
||||
|
||||
Example::
|
||||
|
||||
python -m python_app.scripts.laser_temp_checker
|
||||
python -m python_app.scripts.laser_temp_checker --session s.json --readings r.jsonl
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import signal
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from python_app.hardware_full.laser_control.monitoring import (
|
||||
DEFAULT_READINGS_PATH,
|
||||
DEFAULT_SESSION_PATH,
|
||||
LaserTemperatureChecker,
|
||||
LaserVariationSession,
|
||||
ReadingReader,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("laser_temp_checker")
|
||||
|
||||
_POLL_INTERVAL_S = 0.2
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Validate laser temperature against setpoints")
|
||||
parser.add_argument("--session", type=Path, default=DEFAULT_SESSION_PATH,
|
||||
help="Session snapshot with target setpoints + tolerance")
|
||||
parser.add_argument("--readings", type=Path, default=DEFAULT_READINGS_PATH,
|
||||
help="JSONL readings channel to tail")
|
||||
parser.add_argument("--tolerance", type=float, default=None,
|
||||
help="Override tolerance in °C (default: from session)")
|
||||
parser.add_argument("--reminder-every", type=int, default=0,
|
||||
help="Repeat a warning every N readings while off target (0=off)")
|
||||
parser.add_argument("--from-start", action="store_true",
|
||||
help="Validate the whole readings file, not just new lines")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
|
||||
session = LaserVariationSession.load(args.session)
|
||||
if args.tolerance is not None:
|
||||
session.tolerance_c = args.tolerance
|
||||
checker = LaserTemperatureChecker.from_session(session, reminder_every=args.reminder_every)
|
||||
logger.info(
|
||||
"Checking against T1=%.3f T2=%.3f °C, tolerance ±%.3f °C (%s)",
|
||||
session.target_temp1, session.target_temp2, session.tolerance_c, session.variation_type,
|
||||
)
|
||||
|
||||
reader = ReadingReader(args.readings, from_start=args.from_start)
|
||||
stop_event = threading.Event()
|
||||
|
||||
def request_stop(_signum: int, _frame: object) -> None:
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
|
||||
while not stop_event.is_set():
|
||||
for reading in reader.poll():
|
||||
checker.process(reading)
|
||||
stop_event.wait(_POLL_INTERVAL_S)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,149 +0,0 @@
|
||||
"""Standalone laser temperature monitor process.
|
||||
|
||||
Owns the laser serial port, (optionally) starts a current-variation task, then
|
||||
polls the board once per sweep and appends each reading to a JSONL channel that
|
||||
the temperature checker tails. Runs until SIGINT/SIGTERM.
|
||||
|
||||
Examples::
|
||||
|
||||
# Start LD1 current variation from a run config, then monitor:
|
||||
python -m python_app.scripts.laser_temp_monitor --config run_config.json --start
|
||||
|
||||
# Monitor a variation that is already running:
|
||||
python -m python_app.scripts.laser_temp_monitor --config run_config.json
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import signal
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from python_app.hardware_full.laser_control.controller import (
|
||||
DEVICE_MAIN_MESSAGE_ID,
|
||||
LaserController,
|
||||
)
|
||||
from python_app.hardware_full.laser_control.exceptions import PortBusyError
|
||||
from python_app.hardware_full.laser_control.models import VariationType
|
||||
from python_app.hardware_full.laser_control.monitoring import (
|
||||
DEFAULT_READINGS_PATH,
|
||||
DEFAULT_SESSION_PATH,
|
||||
LaserTemperatureMonitor,
|
||||
LaserVariationSession,
|
||||
ReadingWriter,
|
||||
resolve_period_s,
|
||||
)
|
||||
from python_app.models.run_config_schema import RunConfigModel
|
||||
|
||||
logger = logging.getLogger("laser_temp_monitor")
|
||||
|
||||
|
||||
def _start_variation(controller: LaserController, variation) -> None:
|
||||
"""Send the CHANGE_CURRENT_LD1 task and freeze the session snapshot."""
|
||||
controller.reset()
|
||||
controller.set_manual_mode(
|
||||
temp1=variation.static_temp1,
|
||||
temp2=variation.static_temp2,
|
||||
current1=variation.static_current1,
|
||||
current2=variation.static_current2,
|
||||
message_id=DEVICE_MAIN_MESSAGE_ID,
|
||||
)
|
||||
controller.start_variation(
|
||||
variation_type=VariationType[variation.variation_type],
|
||||
params={
|
||||
"static_temp1": variation.static_temp1,
|
||||
"static_temp2": variation.static_temp2,
|
||||
"static_current1": variation.static_current1,
|
||||
"static_current2": variation.static_current2,
|
||||
"min_value": variation.min_value,
|
||||
"max_value": variation.max_value,
|
||||
"step": variation.step,
|
||||
"time_step": variation.time_step,
|
||||
"delay_time": variation.delay_time,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Poll laser temperature once per sweep")
|
||||
parser.add_argument("--config", required=True, type=Path, help="Path to run_config.json")
|
||||
parser.add_argument("--readings", type=Path, default=DEFAULT_READINGS_PATH,
|
||||
help="JSONL readings channel to append to")
|
||||
parser.add_argument("--session", type=Path, default=DEFAULT_SESSION_PATH,
|
||||
help="Session snapshot path (written with --start)")
|
||||
parser.add_argument("--strategy", default="computed",
|
||||
help="'computed' (per sweep) or 'interval:<ms>'")
|
||||
parser.add_argument("--start", action="store_true",
|
||||
help="Send CHANGE_CURRENT_LD1 before monitoring")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
|
||||
config = RunConfigModel.load_from_path(args.config)
|
||||
laser = config.radar.laser_control
|
||||
variation = laser.variation
|
||||
if variation.variation_type != "CHANGE_CURRENT_LD1":
|
||||
logger.warning(
|
||||
"Only CHANGE_CURRENT_LD1 is supported by firmware; got %s",
|
||||
variation.variation_type,
|
||||
)
|
||||
|
||||
period_s = resolve_period_s(
|
||||
args.strategy,
|
||||
min_value=variation.min_value,
|
||||
max_value=variation.max_value,
|
||||
step=variation.step,
|
||||
time_step_us=variation.time_step,
|
||||
delay_time_ms=variation.delay_time,
|
||||
)
|
||||
|
||||
stop_event = threading.Event()
|
||||
|
||||
def request_stop(_signum: int, _frame: object) -> None:
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
|
||||
controller = LaserController(
|
||||
port=laser.port or None,
|
||||
pi_coeff1_p=laser.pi_coeff1_p,
|
||||
pi_coeff1_i=laser.pi_coeff1_i,
|
||||
pi_coeff2_p=laser.pi_coeff2_p,
|
||||
pi_coeff2_i=laser.pi_coeff2_i,
|
||||
)
|
||||
try:
|
||||
controller.connect()
|
||||
except PortBusyError as exc:
|
||||
# Expected, benign conflict: the manual-control UI (or another monitor)
|
||||
# already owns the port. Exit cleanly with guidance, not a traceback.
|
||||
logger.error("%s", exc)
|
||||
return 2
|
||||
try:
|
||||
if args.start:
|
||||
_start_variation(controller, variation)
|
||||
LaserVariationSession(
|
||||
variation_type=variation.variation_type,
|
||||
target_temp1=variation.static_temp1,
|
||||
target_temp2=variation.static_temp2,
|
||||
tolerance_c=variation.temp_tolerance_c,
|
||||
started_at_iso=datetime.now().isoformat(timespec="seconds"),
|
||||
).save(args.session)
|
||||
logger.info("Started CHANGE_CURRENT_LD1 and wrote session %s", args.session)
|
||||
|
||||
with ReadingWriter(args.readings) as writer:
|
||||
monitor = LaserTemperatureMonitor(
|
||||
controller=controller, writer=writer, period_s=period_s
|
||||
)
|
||||
logger.info("Monitoring to %s (period=%.3fs)", args.readings, period_s)
|
||||
monitor.run(stop_event)
|
||||
finally:
|
||||
controller.disconnect()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Measure only RPH reference phase anchors using the current acquisition settings.
|
||||
|
||||
Start the laser with Apply Radar and stop acquisition before running this tool.
|
||||
No laser commands are sent. --apply replaces only phase0_rad and phase1_rad;
|
||||
frequency anchors, band, collector settings and JSON formatting are preserved.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from python_app.file_io import atomic_write_text
|
||||
from python_app.hardware_full.rph_radar.service import RphRadarService
|
||||
from python_app.models.run_config_schema import RunConfigModel
|
||||
|
||||
|
||||
def _value_span(text: str, path: tuple[str, ...]) -> tuple[int, int]:
|
||||
"""Locate a unique JSON member without reserializing its neighbours."""
|
||||
decoder = json.JSONDecoder()
|
||||
def skip_space(index: int) -> int:
|
||||
while text[index] in " \t\r\n":
|
||||
index += 1
|
||||
return index
|
||||
|
||||
start, end = 0, len(text)
|
||||
for member in path:
|
||||
start = skip_space(start)
|
||||
if text[start] != "{":
|
||||
raise ValueError(f"Expected JSON object before {member}")
|
||||
cursor, found = start + 1, None
|
||||
while True:
|
||||
cursor = skip_space(cursor)
|
||||
if text[cursor] == "}":
|
||||
break
|
||||
key, cursor = decoder.raw_decode(text, cursor)
|
||||
cursor = skip_space(cursor) + 1 # colon; JSON already validated
|
||||
cursor = skip_space(cursor)
|
||||
_, next_cursor = decoder.raw_decode(text, cursor)
|
||||
if key == member:
|
||||
if found is not None:
|
||||
raise ValueError(f"Duplicate JSON member: {member}")
|
||||
found = (cursor, next_cursor)
|
||||
cursor = skip_space(next_cursor)
|
||||
if text[cursor] == "}":
|
||||
break
|
||||
cursor += 1 # comma
|
||||
if found is None:
|
||||
raise ValueError(f"Profile must contain explicit {'.'.join(path)}")
|
||||
start, end = found
|
||||
return start, end
|
||||
|
||||
|
||||
def calibrate(config: RunConfigModel, sweeps: int, warmup: int) -> dict[str, float]:
|
||||
"""Median endpoints after aligning each sweep to the preceding phase branch."""
|
||||
if sweeps < 1 or warmup < 0:
|
||||
raise ValueError("sweeps must be positive and warmup nonnegative")
|
||||
if not config.is_rph_radar or config.radar.driver_mode != "native":
|
||||
raise ValueError("Calibration requires a native rph_radar profile")
|
||||
previous = config.radar.rph_radar.phase_calibration.phase0_rad
|
||||
endpoints = []
|
||||
service = RphRadarService(config)
|
||||
try:
|
||||
service.open()
|
||||
for index in range(warmup + sweeps):
|
||||
raw = service.read_raw_sweep()
|
||||
if index < warmup:
|
||||
continue
|
||||
reference = np.asarray(raw.reference, dtype=np.complex128)
|
||||
if (raw.dirty or reference.ndim != 1 or reference.size < 2
|
||||
or not np.all(np.isfinite(reference)) or np.any(np.abs(reference) <= 1e-9)):
|
||||
raise ValueError("Invalid reference sweep; phase calibration was not changed")
|
||||
phase = np.unwrap(np.angle(reference))
|
||||
phase += round((previous - float(phase[0])) / (2 * np.pi)) * (2 * np.pi)
|
||||
if abs(float(phase[-1] - phase[0])) <= 1e-9:
|
||||
raise ValueError("Degenerate reference phase span")
|
||||
endpoints.append((phase[0], phase[-1]))
|
||||
previous = float(phase[0])
|
||||
finally:
|
||||
service.close()
|
||||
phase0, phase1 = np.median(endpoints, axis=0)
|
||||
if abs(float(phase1 - phase0)) <= 1e-9:
|
||||
raise ValueError("Degenerate median phase span")
|
||||
return {"phase0_rad": float(phase0), "phase1_rad": float(phase1)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Calibrate RadioPhotonic radar (RPH) phase anchors")
|
||||
parser.add_argument("--config", required=True, type=Path)
|
||||
parser.add_argument("--sweeps", type=int, default=200)
|
||||
parser.add_argument("--warmup", type=int, default=10)
|
||||
parser.add_argument("--apply", action="store_true", help="Write only the two phase anchors")
|
||||
args = parser.parse_args()
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
original = args.config.read_bytes()
|
||||
text = original.decode("utf-8")
|
||||
config = RunConfigModel.from_dict(json.loads(text))
|
||||
fields = ("phase0_rad", "phase1_rad")
|
||||
spans = {name: _value_span(text, ("radar", "rph_radar", "phase_calibration", name)) for name in fields}
|
||||
result = calibrate(config, args.sweeps, args.warmup)
|
||||
print(json.dumps(result, allow_nan=False))
|
||||
if args.apply:
|
||||
for name, (start, end) in sorted(spans.items(), key=lambda item: item[1][0], reverse=True):
|
||||
text = text[:start] + json.dumps(result[name], allow_nan=False) + text[end:]
|
||||
if args.config.read_bytes() != original:
|
||||
raise RuntimeError("Profile changed during capture; refusing to overwrite it")
|
||||
atomic_write_text(args.config, text, durable=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+31
-32
@@ -1,4 +1,4 @@
|
||||
"""Raw acquisition producer for the external Kamil ADC radar mode."""
|
||||
"""Raw acquisition producer for RadioPhotonic radar (RPH)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -9,7 +9,7 @@ import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from python_app.hardware_full.kamil_adc import KamilAdcService
|
||||
from python_app.hardware_full.rph_radar.service import RphRadarService
|
||||
from python_app.hardware_full.switch_service import SwitchService, select_and_settle
|
||||
from python_app.models.dataset_model import ComboKey, SweepCollection, TraceData
|
||||
from python_app.models.run_config_schema import RunConfigModel
|
||||
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def _open_radar_with_retry(
|
||||
config: RunConfigModel,
|
||||
radar: KamilAdcService,
|
||||
radar: RphRadarService,
|
||||
input_switch: SwitchService | None,
|
||||
output_switch: SwitchService | None,
|
||||
stop_requested: threading.Event,
|
||||
@@ -28,7 +28,7 @@ def _open_radar_with_retry(
|
||||
"""Reopen the ADC and Python-owned switches, cleaning partial opens before retry."""
|
||||
close_devices(input_switch, output_switch, radar)
|
||||
|
||||
def attempt() -> KamilAdcService:
|
||||
def attempt() -> RphRadarService:
|
||||
try:
|
||||
radar.open(stop_event=stop_requested)
|
||||
radar.configure(config.radar.sweep)
|
||||
@@ -41,16 +41,16 @@ def _open_radar_with_retry(
|
||||
raise
|
||||
return radar
|
||||
|
||||
return retry_open(attempt, stop_requested, "Kamil ADC") is not None
|
||||
return retry_open(attempt, stop_requested, "RadioPhotonic radar (RPH)") is not None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run producer process until config or signal requests exit."""
|
||||
config_path, config, stop_requested = load_producer_config("Publish Kamil ADC raw sweeps to SHM rings")
|
||||
if not config.is_kamil_adc:
|
||||
raise RuntimeError("kamil_adc_raw_producer requires radar.model='kamil_adc'")
|
||||
config_path, config, stop_requested = load_producer_config("Publish RadioPhotonic radar (RPH) raw sweeps to SHM rings")
|
||||
if not config.is_rph_radar:
|
||||
raise RuntimeError("rph_radar_raw_producer requires radar.model='rph_radar'")
|
||||
logger.info(
|
||||
"Kamil ADC raw producer starting: config=%s, combos=%d, continuous=%s",
|
||||
"RadioPhotonic radar (RPH) raw producer starting: config=%s, combos=%d, continuous=%s",
|
||||
config_path, len(config.combos), config.runtime.continuous,
|
||||
)
|
||||
|
||||
@@ -59,24 +59,21 @@ def main() -> int:
|
||||
try:
|
||||
publisher = RawPublisher(config.rings, cleanup)
|
||||
# Switch-aware mode: with native switches the collector drives the RF switches
|
||||
# itself, in the hardware gap between sweeps, and tags each sweep with its combo
|
||||
# — no sweep lost at a switch boundary. The producer then only reads tagged
|
||||
# sweeps and must not touch the GPIO lines the collector owns. With mock switches
|
||||
# (dev/tests) we keep the Python-driven path, which is fine where speed and the
|
||||
# in-gap timing do not matter.
|
||||
# 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.
|
||||
collector_driven = (
|
||||
config.input_switch.driver_mode == "native"
|
||||
and config.output_switch.driver_mode == "native"
|
||||
)
|
||||
radar = KamilAdcService(
|
||||
radar = RphRadarService(
|
||||
config,
|
||||
switch_config_path=str(config_path) if collector_driven else None,
|
||||
collector_driven=collector_driven,
|
||||
)
|
||||
input_switch = None if collector_driven else SwitchService.from_model(config.input_switch)
|
||||
output_switch = None if collector_driven else SwitchService.from_model(config.output_switch)
|
||||
logger.info(
|
||||
"Kamil ADC switch control: %s",
|
||||
"collector-driven (in-gap, lossless)" if collector_driven else "producer-driven",
|
||||
"RadioPhotonic radar (RPH) switch control: %s",
|
||||
"collector-driven" if collector_driven else "producer-driven",
|
||||
)
|
||||
|
||||
if not _open_radar_with_retry(config, radar, input_switch, output_switch, stop_requested):
|
||||
@@ -85,8 +82,8 @@ def main() -> int:
|
||||
collection_id = 1
|
||||
while not stop_requested.is_set():
|
||||
collection_start = time.monotonic()
|
||||
capture_start_ns = time.monotonic_ns()
|
||||
traces: list[TraceData] = []
|
||||
cycle_id = None
|
||||
|
||||
try:
|
||||
for combo in config.combos:
|
||||
@@ -95,30 +92,30 @@ def main() -> int:
|
||||
if collector_driven:
|
||||
# The collector already switched and tagged the sweep; just
|
||||
# read the clean capture for this combination.
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
sweep = radar.acquire(combo=(combo.input, combo.output))
|
||||
if cycle_id is not None and cycle_id != radar.last_cycle_id:
|
||||
break # A dropped or overwritten pair invalidates this cycle.
|
||||
cycle_id = radar.last_cycle_id
|
||||
else:
|
||||
if input_switch is None or output_switch is None:
|
||||
raise RuntimeError("Producer-driven switches are not initialized")
|
||||
if not select_and_settle(output_switch, input_switch, combo.output, combo.input,
|
||||
config.runtime.settling_ms, wait=stop_requested.wait):
|
||||
break
|
||||
# Stamped after switching and settling so the window covers
|
||||
# the sweep alone, not the dead time before it.
|
||||
sweep_start_ns = time.monotonic_ns()
|
||||
radar.prepare_combo(ComboKey(combo.input, combo.output))
|
||||
sweep = radar.acquire()
|
||||
traces.append(
|
||||
TraceData(
|
||||
combo=ComboKey(input=combo.input, output=combo.output),
|
||||
frequency_hz=np.asarray(sweep.x, dtype=np.float32),
|
||||
s21=np.asarray(sweep.trace("s21"), dtype=np.complex64),
|
||||
capture_start_ns=sweep_start_ns,
|
||||
capture_end_ns=time.monotonic_ns(),
|
||||
capture_start_ns=sweep.capture_start_ns,
|
||||
capture_end_ns=sweep.capture_end_ns,
|
||||
)
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — reconnect forever, never give up
|
||||
logger.warning(
|
||||
"Kamil ADC acquisition failed; reconnecting and waiting for the device: %s",
|
||||
"RadioPhotonic radar (RPH) acquisition failed; reconnecting and waiting for the device: %s",
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
@@ -132,8 +129,10 @@ def main() -> int:
|
||||
if len(traces) != len(config.combos):
|
||||
if stop_requested.is_set():
|
||||
break
|
||||
if collector_driven:
|
||||
continue # Latest-wins transport skipped part of a source cycle.
|
||||
logger.warning(
|
||||
"Kamil ADC produced an incomplete collection (%d of %d combos); "
|
||||
"RadioPhotonic radar (RPH) produced an incomplete collection (%d of %d combos); "
|
||||
"reconnecting and waiting for the device",
|
||||
len(traces),
|
||||
len(config.combos),
|
||||
@@ -143,11 +142,11 @@ def main() -> int:
|
||||
continue
|
||||
|
||||
collection = SweepCollection(
|
||||
collection_id=collection_id,
|
||||
collection_id=cycle_id if cycle_id is not None else collection_id,
|
||||
monotonic_ns=time.monotonic_ns(),
|
||||
traces=traces,
|
||||
capture_start_ns=capture_start_ns,
|
||||
capture_end_ns=time.monotonic_ns(),
|
||||
capture_start_ns=min(trace.capture_start_ns for trace in traces),
|
||||
capture_end_ns=max(trace.capture_end_ns for trace in traces),
|
||||
)
|
||||
publisher.publish(collection)
|
||||
|
||||
@@ -155,13 +154,13 @@ def main() -> int:
|
||||
break
|
||||
collection_duration_s = time.monotonic() - collection_start
|
||||
if collection_id == 1 or collection_id % 20 == 0 or collection_duration_s > 2.0:
|
||||
logger.info("Kamil ADC collection %d acquired in %.3f s", collection_id, collection_duration_s)
|
||||
logger.info("RadioPhotonic radar (RPH) collection %d acquired in %.3f s", collection_id, collection_duration_s)
|
||||
collection_id += 1
|
||||
finally:
|
||||
close_devices(output_switch, input_switch, radar)
|
||||
cleanup.close()
|
||||
|
||||
logger.info("Kamil ADC raw producer stopped")
|
||||
logger.info("RadioPhotonic radar (RPH) raw producer stopped")
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user