added remote k209 setup
This commit is contained in:
@@ -22,8 +22,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
from python_app.models.run_config_model import RadarSweepModel
|
||||
from python_app.hardware_full.single_radar_service import create_single_radar_service
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.shm_reader import ShmRingReader
|
||||
|
||||
|
||||
@@ -71,22 +71,15 @@ def _read_native_summary(config_path: Path) -> str:
|
||||
|
||||
def _prepare_radar_if_needed(config_path: Path, *, strict: bool) -> str | None:
|
||||
"""Preconfigure native radar through Python service when requested."""
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
radar = config["radar"]
|
||||
if radar["driver_mode"] != "native":
|
||||
config_payload = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
config = RunConfigModel.from_dict(config_payload)
|
||||
if config.radar.driver_mode != "native":
|
||||
return "Radar pre-configuration skipped (mock mode)."
|
||||
if config.is_multi_device:
|
||||
return "Radar pre-configuration skipped (multi-device producer config)."
|
||||
|
||||
sweep = radar["sweep"]
|
||||
sweep_model = RadarSweepModel(
|
||||
start_hz=float(sweep["start_hz"]),
|
||||
stop_hz=float(sweep["stop_hz"]),
|
||||
points=int(sweep["points"]),
|
||||
if_bandwidth_hz=float(sweep["if_bandwidth_hz"]),
|
||||
power_dbm=float(sweep.get("stimulus_power_dbm", -10.0)),
|
||||
)
|
||||
|
||||
radar_service = LibreVnaService(serial=radar.get("serial") or None)
|
||||
if not radar_service.driver_available:
|
||||
radar_service = create_single_radar_service(config)
|
||||
if not getattr(radar_service, "driver_available", True):
|
||||
message = "LibreVNA Python driver is unavailable: skipping pre-configuration"
|
||||
if strict:
|
||||
raise RuntimeError(message)
|
||||
@@ -94,7 +87,7 @@ def _prepare_radar_if_needed(config_path: Path, *, strict: bool) -> str | None:
|
||||
|
||||
try:
|
||||
radar_service.open()
|
||||
radar_service.configure(sweep_model)
|
||||
radar_service.configure(config.radar.sweep)
|
||||
return "Radar pre-configuration completed."
|
||||
except Exception as exc:
|
||||
message = f"Radar pre-configuration failed ({exc})"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Run a TCP acquisition server for a locally connected Compact-M K209."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from python_app.hardware_full.compact_m_k209_service import CompactMK209Service
|
||||
from python_app.hardware_full.k209_remote_protocol import (
|
||||
COMMAND_ACQUIRE,
|
||||
COMMAND_CONFIGURE,
|
||||
COMMAND_IDENTITY,
|
||||
COMMAND_LIMITS,
|
||||
CONFIG_STRUCT,
|
||||
DEFAULT_REMOTE_PORT,
|
||||
LIMITS_STRUCT,
|
||||
STATUS_OK,
|
||||
recv_exact,
|
||||
send_error,
|
||||
send_float32_array,
|
||||
send_u32,
|
||||
)
|
||||
from python_app.models.run_config_model import RadarSweepModel
|
||||
|
||||
DEFAULT_RESOURCE = "TCPIP0::127.0.0.1::hislip0,4880::INSTR"
|
||||
|
||||
|
||||
class K209RemoteRequestHandler(socketserver.StreamRequestHandler):
|
||||
"""Handle one persistent K209 remote client connection."""
|
||||
|
||||
def setup(self) -> None:
|
||||
super().setup()
|
||||
self.request.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.service = CompactMK209Service(
|
||||
resource=self.server.resource,
|
||||
timeout_ms=self.server.timeout_ms,
|
||||
preset_on_open=False,
|
||||
visa_library="@ivi",
|
||||
)
|
||||
self.service.open()
|
||||
|
||||
def finish(self) -> None:
|
||||
try:
|
||||
self.service.close()
|
||||
finally:
|
||||
super().finish()
|
||||
|
||||
def handle(self) -> None:
|
||||
while True:
|
||||
command = self.rfile.read(1)
|
||||
if not command:
|
||||
return
|
||||
try:
|
||||
if command == COMMAND_IDENTITY:
|
||||
self._handle_identity()
|
||||
elif command == COMMAND_LIMITS:
|
||||
self._handle_limits()
|
||||
elif command == COMMAND_CONFIGURE:
|
||||
self._handle_configure()
|
||||
elif command == COMMAND_ACQUIRE:
|
||||
self._handle_acquire()
|
||||
else:
|
||||
raise RuntimeError(f"unsupported command byte {command!r}")
|
||||
self.wfile.flush()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
send_error(self.wfile, str(exc))
|
||||
self.wfile.flush()
|
||||
|
||||
def _handle_identity(self) -> None:
|
||||
payload = self.service.query_identity().encode("utf-8")
|
||||
self.wfile.write(STATUS_OK)
|
||||
send_u32(self.wfile, len(payload))
|
||||
self.wfile.write(payload)
|
||||
|
||||
def _handle_limits(self) -> None:
|
||||
limits = self.service.read_device_limits()
|
||||
self.wfile.write(STATUS_OK)
|
||||
self.wfile.write(
|
||||
LIMITS_STRUCT.pack(
|
||||
float(limits["min_frequency_hz"]),
|
||||
float(limits["max_frequency_hz"]),
|
||||
float(limits["min_ifbw_hz"]),
|
||||
float(limits["max_ifbw_hz"]),
|
||||
int(limits["max_points"]),
|
||||
float(limits["min_power_dbm"]),
|
||||
float(limits["max_power_dbm"]),
|
||||
)
|
||||
)
|
||||
|
||||
def _handle_configure(self) -> None:
|
||||
start_hz, stop_hz, points, ifbw_hz, power_dbm = CONFIG_STRUCT.unpack(
|
||||
recv_exact(self.rfile, CONFIG_STRUCT.size)
|
||||
)
|
||||
sweep = RadarSweepModel(
|
||||
start_hz=start_hz,
|
||||
stop_hz=stop_hz,
|
||||
points=int(points),
|
||||
if_bandwidth_hz=ifbw_hz,
|
||||
power_dbm=power_dbm,
|
||||
)
|
||||
self.service.configure(sweep)
|
||||
self.wfile.write(STATUS_OK)
|
||||
send_u32(self.wfile, int(points))
|
||||
send_float32_array(self.wfile, self.service.frequency_axis())
|
||||
|
||||
def _handle_acquire(self) -> None:
|
||||
sweep = self.service.acquire_interleaved()
|
||||
self.wfile.write(STATUS_OK)
|
||||
send_u32(self.wfile, int(sweep.frequency_hz.size))
|
||||
send_float32_array(self.wfile, sweep.s11_values)
|
||||
send_float32_array(self.wfile, sweep.s21_values)
|
||||
|
||||
|
||||
class K209RemoteServer(socketserver.TCPServer):
|
||||
"""Single-client TCP server with K209 connection settings."""
|
||||
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address: tuple[str, int], resource: str, timeout_ms: int) -> None:
|
||||
self.resource = resource
|
||||
self.timeout_ms = timeout_ms
|
||||
super().__init__(server_address, K209RemoteRequestHandler)
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Serve a locally connected Compact-M K209 over TCP.")
|
||||
parser.add_argument("--host", default="0.0.0.0", help="Server bind address.")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_REMOTE_PORT, help="Server TCP port.")
|
||||
parser.add_argument("--resource", default=DEFAULT_RESOURCE, help="Local S2VNA VISA resource.")
|
||||
parser.add_argument("--timeout-ms", type=int, default=20_000, help="K209 VISA timeout.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parse_args()
|
||||
with K209RemoteServer((args.host, args.port), resource=args.resource, timeout_ms=args.timeout_ms) as server:
|
||||
print(f"K209 remote server listening on {args.host}:{args.port}")
|
||||
print(f"Local S2VNA resource: {args.resource}")
|
||||
server.serve_forever()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Smoke test for a remote Compact-M K209 server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from python_app.hardware_full.k209_remote_protocol import DEFAULT_REMOTE_HOST, DEFAULT_REMOTE_PORT
|
||||
from python_app.hardware_full.remote_compact_m_k209_service import RemoteCompactMK209Service
|
||||
from python_app.models.run_config_model import RadarSweepModel
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Validate remote K209 connection and one sweep.")
|
||||
parser.add_argument("--host", default=DEFAULT_REMOTE_HOST, help="K209 remote server host.")
|
||||
parser.add_argument("--port", type=int, default=DEFAULT_REMOTE_PORT, help="K209 remote server port.")
|
||||
parser.add_argument("--start-hz", type=float, default=10_000_000.0)
|
||||
parser.add_argument("--stop-hz", type=float, default=100_000_000.0)
|
||||
parser.add_argument("--points", type=int, default=11)
|
||||
parser.add_argument("--ifbw-hz", type=float, default=10_000.0)
|
||||
parser.add_argument("--power-dbm", type=float, default=-20.0)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parse_args()
|
||||
sweep = RadarSweepModel(
|
||||
start_hz=args.start_hz,
|
||||
stop_hz=args.stop_hz,
|
||||
points=args.points,
|
||||
if_bandwidth_hz=args.ifbw_hz,
|
||||
power_dbm=args.power_dbm,
|
||||
)
|
||||
service = RemoteCompactMK209Service(host=args.host, port=args.port)
|
||||
try:
|
||||
service.open()
|
||||
print(f"K209 IDN: {service.query_identity()}")
|
||||
service.configure(sweep)
|
||||
result = service.acquire()
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
s11 = result.trace("s11")
|
||||
s21 = result.trace("s21")
|
||||
if result.x.size != args.points or s11.size != args.points or s21.size != args.points:
|
||||
raise RuntimeError("Remote K209 sweep returned an unexpected point count")
|
||||
if not np.all(np.isfinite(result.x)) or not np.all(np.isfinite(s11)) or not np.all(np.isfinite(s21)):
|
||||
raise RuntimeError("Remote K209 sweep contains non-finite values")
|
||||
if not np.all(np.diff(result.x) >= 0):
|
||||
raise RuntimeError("Remote K209 frequency axis is not monotonic")
|
||||
|
||||
print(
|
||||
"Remote K209 sweep OK: "
|
||||
f"points={args.points}, first_hz={result.x[0]:.3f}, last_hz={result.x[-1]:.3f}, "
|
||||
f"mean_abs_s11={float(np.mean(np.abs(s11))):.6g}, "
|
||||
f"mean_abs_s21={float(np.mean(np.abs(s21))):.6g}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user