"""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())