"""Instrument factory for creating configured instrument sets. This module provides a factory pattern for creating sets of instruments based on configuration. It abstracts away the choice between simulated and real hardware, allowing test code to be written once and run against either backend. """ from dataclasses import dataclass from typing import Literal from py_dvt_ate.instruments.interfaces import IMultimeter, IPowerSupply, IThermalChamber @dataclass class InstrumentSet: """Container for a complete set of instruments. Holds all instruments needed for DVT testing. All instruments implement the interface protocols (IThermalChamber, IPowerSupply, IMultimeter), allowing them to be simulated or real hardware. Attributes: chamber: Thermal chamber for temperature control. psu: Programmable power supply for DUT power. dmm: Digital multimeter for precision measurements. """ chamber: IThermalChamber psu: IPowerSupply dmm: IMultimeter @dataclass class InstrumentConfig: """Configuration for instrument connections. Defines how to connect to instruments. The backend determines whether to use simulated instruments (TCP connections to virtual instruments) or real hardware (PyVISA connections). Attributes: backend: "simulator" for virtual instruments, "pyvisa" for real hardware. Simulator Settings: simulator_host: Hostname/IP of simulation server. Default "localhost". chamber_port: TCP port for thermal chamber simulator. Default 5001. psu_port: TCP port for power supply simulator. Default 5002. dmm_port: TCP port for multimeter simulator. Default 5003. PyVISA Settings (for real hardware): chamber_visa: VISA resource string for thermal chamber (e.g., "TCPIP::192.168.1.10::INSTR"). psu_visa: VISA resource string for power supply. dmm_visa: VISA resource string for multimeter. """ backend: Literal["simulator", "pyvisa"] # Simulator settings simulator_host: str = "localhost" chamber_port: int = 5001 psu_port: int = 5002 dmm_port: int = 5003 # PyVISA settings (for real hardware) chamber_visa: str | None = None psu_visa: str | None = None dmm_visa: str | None = None class InstrumentFactory: """Factory for creating instrument sets from configuration. This factory encapsulates the creation logic for instrument sets, hiding the complexity of instantiating transports and drivers based on the chosen backend. Example: >>> config = InstrumentConfig(backend="simulator") >>> instruments = InstrumentFactory.create(config) >>> instruments.chamber.set_temperature(85.0) >>> instruments.psu.set_voltage(1, 3.3) >>> voltage = instruments.dmm.measure_dc_voltage() """ @staticmethod def create(config: InstrumentConfig) -> InstrumentSet: """Create instrument set based on configuration. Args: config: Configuration specifying backend and connection details. Returns: InstrumentSet containing all configured instruments. Raises: ValueError: If backend is unknown or configuration is invalid. ConnectionError: If unable to connect to instruments. """ if config.backend == "simulator": return InstrumentFactory._create_simulated(config) elif config.backend == "pyvisa": return InstrumentFactory._create_pyvisa(config) else: raise ValueError(f"Unknown backend: {config.backend}") @staticmethod def _create_simulated(config: InstrumentConfig) -> InstrumentSet: """Create simulated instruments connected via TCP. Creates TCP transports for each virtual instrument and wraps them in SCPI drivers. The simulation server must be running and listening on the configured ports. Args: config: Configuration with simulator_host and port settings. Returns: InstrumentSet with simulated instruments. Raises: ConnectionError: If unable to connect to simulation server. """ from py_dvt_ate.instruments.drivers.chamber import ThermalChamberDriver from py_dvt_ate.instruments.drivers.multimeter import MultimeterDriver from py_dvt_ate.instruments.drivers.power_supply import PowerSupplyDriver from py_dvt_ate.instruments.transport.tcp import TCPTransport # Create transports for each instrument chamber_transport = TCPTransport(config.simulator_host, config.chamber_port) psu_transport = TCPTransport(config.simulator_host, config.psu_port) dmm_transport = TCPTransport(config.simulator_host, config.dmm_port) # Wrap transports in drivers return InstrumentSet( chamber=ThermalChamberDriver(chamber_transport), psu=PowerSupplyDriver(psu_transport), dmm=MultimeterDriver(dmm_transport), ) @staticmethod def _create_pyvisa(config: InstrumentConfig) -> InstrumentSet: """Create PyVISA instruments for real hardware. Creates VISA transports for each real instrument and wraps them in SCPI drivers. Requires PyVISA to be installed and VISA resource strings to be configured. Args: config: Configuration with chamber_visa, psu_visa, dmm_visa settings. Returns: InstrumentSet with real hardware instruments. Raises: NotImplementedError: PyVISA backend not yet implemented. ValueError: If required VISA resource strings are missing. """ # Future implementation would use pyvisa.ResourceManager # to create VISA transports: # # import pyvisa # from py_dvt_ate.instruments.transport.visa import VISATransport # # rm = pyvisa.ResourceManager() # chamber_transport = VISATransport(rm.open_resource(config.chamber_visa)) # psu_transport = VISATransport(rm.open_resource(config.psu_visa)) # dmm_transport = VISATransport(rm.open_resource(config.dmm_visa)) # # return InstrumentSet( # chamber=ThermalChamberDriver(chamber_transport), # psu=PowerSupplyDriver(psu_transport), # dmm=MultimeterDriver(dmm_transport), # ) raise NotImplementedError("PyVISA backend not yet implemented")