From 262caf416e3b8af1ae89ce42b3fa9b51bfdbd407 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Sat, 7 Jun 2025 15:15:56 +0000 Subject: [PATCH] Move InstrumentServer to instruments/transport InstrumentServer is a general-purpose SCPI-over-TCP server that can host any device implementing the SCPIDevice protocol (process method). Moving it from simulation/ to instruments/transport/ reflects this: - simulation package now depends on instruments package - InstrumentServer can host both virtual and real instrument adapters - Added SCPIDevice Protocol for type-safe device registration --- .../instruments/transport/__init__.py | 5 ++ .../transport/server.py} | 51 ++++++++++++++----- src/py_dvt_ate/simulation/__init__.py | 5 +- src/py_dvt_ate/simulation/server.py | 2 +- tests/integration/test_tcp_server.py | 2 +- 5 files changed, 47 insertions(+), 18 deletions(-) rename src/py_dvt_ate/{simulation/tcp_server.py => instruments/transport/server.py} (81%) diff --git a/src/py_dvt_ate/instruments/transport/__init__.py b/src/py_dvt_ate/instruments/transport/__init__.py index e682d01..f98e088 100644 --- a/src/py_dvt_ate/instruments/transport/__init__.py +++ b/src/py_dvt_ate/instruments/transport/__init__.py @@ -1,6 +1,11 @@ """Transport layer for instrument communication. Provides connection abstractions for different backends: +- TCP server for hosting SCPI instruments - TCP sockets (for simulation server) - PyVISA (for real instruments) """ + +from py_dvt_ate.instruments.transport.server import InstrumentServer, SCPIDevice + +__all__ = ["InstrumentServer", "SCPIDevice"] diff --git a/src/py_dvt_ate/simulation/tcp_server.py b/src/py_dvt_ate/instruments/transport/server.py similarity index 81% rename from src/py_dvt_ate/simulation/tcp_server.py rename to src/py_dvt_ate/instruments/transport/server.py index 5cc0045..1d2aa05 100644 --- a/src/py_dvt_ate/simulation/tcp_server.py +++ b/src/py_dvt_ate/instruments/transport/server.py @@ -1,32 +1,55 @@ -"""Async TCP server for exposing virtual instruments over network. +"""Async TCP server for exposing instruments over network. -This module provides the InstrumentServer class that hosts virtual SCPI +This module provides the InstrumentServer class that hosts SCPI instruments over TCP, allowing client applications to communicate using standard SCPI commands over a network connection. + +This is a general-purpose server that works with any object implementing +the SCPIDevice protocol (having a process(command) -> str method). """ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING +from typing import Protocol, runtime_checkable -if TYPE_CHECKING: - from py_dvt_ate.simulation.virtual.base import BaseInstrument - -# Re-export for type checking - actual import happens at runtime via registration -__all__ = ["InstrumentServer"] +__all__ = ["InstrumentServer", "SCPIDevice"] logger = logging.getLogger(__name__) +@runtime_checkable +class SCPIDevice(Protocol): + """Protocol for SCPI-compatible devices. + + Any object with a process method matching this signature can be + served by InstrumentServer. + """ + + def process(self, command: str) -> str: + """Process a SCPI command and return the response. + + Args: + command: SCPI command string to process. + + Returns: + Response string (may be empty for commands with no response). + """ + ... + + class InstrumentServer: - """Async TCP server hosting virtual SCPI instruments. + """Async TCP server hosting SCPI instruments. Each instrument is assigned a port. Clients connect via TCP and send SCPI commands as newline-terminated strings. Responses are also newline-terminated. + This server can host any device implementing the SCPIDevice protocol, + including both virtual instruments (simulators) and adapters for + real hardware. + Attributes: host: Host address to bind to. """ @@ -38,7 +61,7 @@ class InstrumentServer: host: Host address to bind to. Defaults to localhost. """ self._host = host - self._instruments: dict[int, BaseInstrument] = {} + self._instruments: dict[int, SCPIDevice] = {} self._servers: list[asyncio.Server] = [] self._running = False @@ -52,12 +75,12 @@ class InstrumentServer: """Check if server is currently running.""" return self._running - def register_instrument(self, port: int, instrument: BaseInstrument) -> None: + def register_instrument(self, port: int, instrument: SCPIDevice) -> None: """Register an instrument to be served on a specific port. Args: port: TCP port number to serve the instrument on. - instrument: Virtual instrument to serve. + instrument: SCPI device to serve (any object with process method). Raises: ValueError: If port is already registered. @@ -76,7 +99,7 @@ class InstrumentServer: port, ) - def get_instrument(self, port: int) -> BaseInstrument | None: + def get_instrument(self, port: int) -> SCPIDevice | None: """Get the instrument registered on a port. Args: @@ -154,7 +177,7 @@ class InstrumentServer: self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, - instrument: BaseInstrument, + instrument: SCPIDevice, port: int, ) -> None: """Handle a client connection. diff --git a/src/py_dvt_ate/simulation/__init__.py b/src/py_dvt_ate/simulation/__init__.py index 1988d10..85d7ea9 100644 --- a/src/py_dvt_ate/simulation/__init__.py +++ b/src/py_dvt_ate/simulation/__init__.py @@ -2,9 +2,10 @@ Provides virtual instruments backed by a coupled thermal-electrical physics engine. Used for development and testing without real hardware. + +Note: InstrumentServer has moved to py_dvt_ate.instruments.transport """ from py_dvt_ate.simulation.server import ServerConfig, SimulationServer -from py_dvt_ate.simulation.tcp_server import InstrumentServer -__all__ = ["InstrumentServer", "ServerConfig", "SimulationServer"] +__all__ = ["ServerConfig", "SimulationServer"] diff --git a/src/py_dvt_ate/simulation/server.py b/src/py_dvt_ate/simulation/server.py index 330b29c..f560ce1 100644 --- a/src/py_dvt_ate/simulation/server.py +++ b/src/py_dvt_ate/simulation/server.py @@ -11,8 +11,8 @@ import logging import signal from dataclasses import dataclass +from py_dvt_ate.instruments.transport import InstrumentServer from py_dvt_ate.simulation.physics.engine import PhysicsEngine -from py_dvt_ate.simulation.tcp_server import InstrumentServer from py_dvt_ate.simulation.virtual.chamber import ThermalChamberSim from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim diff --git a/tests/integration/test_tcp_server.py b/tests/integration/test_tcp_server.py index f8c3fc4..7abd1b9 100644 --- a/tests/integration/test_tcp_server.py +++ b/tests/integration/test_tcp_server.py @@ -9,9 +9,9 @@ import asyncio import pytest +from py_dvt_ate.instruments.transport import InstrumentServer from py_dvt_ate.simulation.physics.engine import PhysicsEngine from py_dvt_ate.simulation.server import ServerConfig, SimulationServer -from py_dvt_ate.simulation.tcp_server import InstrumentServer from py_dvt_ate.simulation.virtual.chamber import ThermalChamberSim from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim