Move InstrumentServer to instruments/transport
Some checks failed
CI / Lint (push) Failing after 11m12s
CI / Type Check (push) Failing after 2m10s
CI / Test (push) Successful in 9s
CI / Release (push) Has been skipped

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
This commit is contained in:
2025-12-02 16:09:32 +00:00
parent 3bdd2e6c48
commit ad8d61b649
5 changed files with 47 additions and 18 deletions

View File

@@ -1,6 +1,11 @@
"""Transport layer for instrument communication. """Transport layer for instrument communication.
Provides connection abstractions for different backends: Provides connection abstractions for different backends:
- TCP server for hosting SCPI instruments
- TCP sockets (for simulation server) - TCP sockets (for simulation server)
- PyVISA (for real instruments) - PyVISA (for real instruments)
""" """
from py_dvt_ate.instruments.transport.server import InstrumentServer, SCPIDevice
__all__ = ["InstrumentServer", "SCPIDevice"]

View File

@@ -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 instruments over TCP, allowing client applications to communicate using
standard SCPI commands over a network connection. 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 from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import TYPE_CHECKING from typing import Protocol, runtime_checkable
if TYPE_CHECKING: __all__ = ["InstrumentServer", "SCPIDevice"]
from py_dvt_ate.simulation.virtual.base import BaseInstrument
# Re-export for type checking - actual import happens at runtime via registration
__all__ = ["InstrumentServer"]
logger = logging.getLogger(__name__) 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: 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 Each instrument is assigned a port. Clients connect via TCP and send
SCPI commands as newline-terminated strings. Responses are also SCPI commands as newline-terminated strings. Responses are also
newline-terminated. newline-terminated.
This server can host any device implementing the SCPIDevice protocol,
including both virtual instruments (simulators) and adapters for
real hardware.
Attributes: Attributes:
host: Host address to bind to. host: Host address to bind to.
""" """
@@ -38,7 +61,7 @@ class InstrumentServer:
host: Host address to bind to. Defaults to localhost. host: Host address to bind to. Defaults to localhost.
""" """
self._host = host self._host = host
self._instruments: dict[int, BaseInstrument] = {} self._instruments: dict[int, SCPIDevice] = {}
self._servers: list[asyncio.Server] = [] self._servers: list[asyncio.Server] = []
self._running = False self._running = False
@@ -52,12 +75,12 @@ class InstrumentServer:
"""Check if server is currently running.""" """Check if server is currently running."""
return self._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. """Register an instrument to be served on a specific port.
Args: Args:
port: TCP port number to serve the instrument on. 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: Raises:
ValueError: If port is already registered. ValueError: If port is already registered.
@@ -76,7 +99,7 @@ class InstrumentServer:
port, port,
) )
def get_instrument(self, port: int) -> BaseInstrument | None: def get_instrument(self, port: int) -> SCPIDevice | None:
"""Get the instrument registered on a port. """Get the instrument registered on a port.
Args: Args:
@@ -154,7 +177,7 @@ class InstrumentServer:
self, self,
reader: asyncio.StreamReader, reader: asyncio.StreamReader,
writer: asyncio.StreamWriter, writer: asyncio.StreamWriter,
instrument: BaseInstrument, instrument: SCPIDevice,
port: int, port: int,
) -> None: ) -> None:
"""Handle a client connection. """Handle a client connection.

View File

@@ -2,9 +2,10 @@
Provides virtual instruments backed by a coupled thermal-electrical Provides virtual instruments backed by a coupled thermal-electrical
physics engine. Used for development and testing without real hardware. 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.server import ServerConfig, SimulationServer
from py_dvt_ate.simulation.tcp_server import InstrumentServer
__all__ = ["InstrumentServer", "ServerConfig", "SimulationServer"] __all__ = ["ServerConfig", "SimulationServer"]

View File

@@ -11,8 +11,8 @@ import logging
import signal import signal
from dataclasses import dataclass 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.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.chamber import ThermalChamberSim
from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim
from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim

View File

@@ -9,9 +9,9 @@ import asyncio
import pytest import pytest
from py_dvt_ate.instruments.transport import InstrumentServer
from py_dvt_ate.simulation.physics.engine import PhysicsEngine from py_dvt_ate.simulation.physics.engine import PhysicsEngine
from py_dvt_ate.simulation.server import ServerConfig, SimulationServer 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.chamber import ThermalChamberSim
from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim from py_dvt_ate.simulation.virtual.multimeter import MultimeterSim
from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim from py_dvt_ate.simulation.virtual.power_supply import PowerSupplySim