From 6668df07ab3e02c34ad820e8e0b37cdefc080077 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 2 Dec 2025 22:21:59 +0000 Subject: [PATCH] Add PSU and DMM drivers --- .../instruments/drivers/__init__.py | 9 +- .../instruments/drivers/multimeter.py | 157 ++++++++++++++++++ .../instruments/drivers/power_supply.py | 152 +++++++++++++++++ 3 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 src/py_dvt_ate/instruments/drivers/multimeter.py create mode 100644 src/py_dvt_ate/instruments/drivers/power_supply.py diff --git a/src/py_dvt_ate/instruments/drivers/__init__.py b/src/py_dvt_ate/instruments/drivers/__init__.py index 0c5fe76..12015da 100644 --- a/src/py_dvt_ate/instruments/drivers/__init__.py +++ b/src/py_dvt_ate/instruments/drivers/__init__.py @@ -6,5 +6,12 @@ and handles responses from instruments. from py_dvt_ate.instruments.drivers.base import BaseDriver 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 -__all__ = ["BaseDriver", "ThermalChamberDriver"] +__all__ = [ + "BaseDriver", + "ThermalChamberDriver", + "PowerSupplyDriver", + "MultimeterDriver", +] diff --git a/src/py_dvt_ate/instruments/drivers/multimeter.py b/src/py_dvt_ate/instruments/drivers/multimeter.py new file mode 100644 index 0000000..f1f8c4f --- /dev/null +++ b/src/py_dvt_ate/instruments/drivers/multimeter.py @@ -0,0 +1,157 @@ +"""Multimeter SCPI driver. + +This module implements a client-side driver for digital multimeters +that communicate via SCPI commands. +""" + +from py_dvt_ate.instruments.drivers.base import BaseDriver + + +class MultimeterDriver(BaseDriver): + """SCPI driver for digital multimeters. + + Provides high-level Python API for making measurements with DMMs via + SCPI commands. Implements the IMultimeter interface. + + SCPI Commands Used: + MEAS:VOLT:DC? - Measure DC voltage + MEAS:CURR:DC? - Measure DC current + CONF:VOLT:DC - Configure for DC voltage measurement + CONF:CURR:DC - Configure for DC current measurement + CONF? - Query current configuration + READ? - Take measurement with current configuration + + Example: + >>> transport = TCPTransport("localhost", 5003) + >>> dmm = MultimeterDriver(transport) + >>> dmm.connect() + >>> voltage = dmm.measure_dc_voltage() + >>> current = dmm.measure_dc_current() + """ + + def measure_dc_voltage(self, range: str = "AUTO") -> float: + """Measure DC voltage. + + Configures the meter for DC voltage and takes a measurement. + + Args: + range: Measurement range. Default "AUTO" for auto-ranging. + Note: Range parameter currently not supported by simulator. + + Returns: + Measured voltage in volts. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + # Note: Range parameter not yet implemented in virtual instrument + return self.query_float("MEAS:VOLT:DC?") + + def measure_dc_current(self, range: str = "AUTO") -> float: + """Measure DC current. + + Configures the meter for DC current and takes a measurement. + + Args: + range: Measurement range. Default "AUTO" for auto-ranging. + Note: Range parameter currently not supported by simulator. + + Returns: + Measured current in amps. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + # Note: Range parameter not yet implemented in virtual instrument + return self.query_float("MEAS:CURR:DC?") + + def measure_resistance(self, range: str = "AUTO") -> float: + """Measure resistance. + + Configures the meter for resistance and takes a measurement. + + Args: + range: Measurement range. Default "AUTO" for auto-ranging. + + Returns: + Measured resistance in ohms. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + NotImplementedError: If instrument does not support resistance. + """ + # Note: Resistance measurement not yet implemented in virtual instrument + raise NotImplementedError( + "Resistance measurement not yet supported by virtual instrument" + ) + + def set_integration_time(self, nplc: float) -> None: + """Set the integration time. + + Args: + nplc: Integration time in number of power line cycles (NPLC). + Typical values: 0.02, 0.2, 1, 10, 100. + + Raises: + ConnectionError: If not connected. + IOError: If command fails. + NotImplementedError: If instrument does not support integration time. + """ + # Note: Integration time not yet implemented in virtual instrument + raise NotImplementedError( + "Integration time setting not yet supported by virtual instrument" + ) + + def configure_dc_voltage(self) -> None: + """Configure meter for DC voltage measurement. + + Sets the measurement function without taking a measurement. + Use read() to take a measurement after configuring. + + Raises: + ConnectionError: If not connected. + IOError: If command fails. + """ + self.write("CONF:VOLT:DC") + + def configure_dc_current(self) -> None: + """Configure meter for DC current measurement. + + Sets the measurement function without taking a measurement. + Use read() to take a measurement after configuring. + + Raises: + ConnectionError: If not connected. + IOError: If command fails. + """ + self.write("CONF:CURR:DC") + + def get_configuration(self) -> str: + """Get the current measurement configuration. + + Returns: + Configuration string (e.g., "VOLT:DC", "CURR:DC"). + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query("CONF?").strip('"') + + def read(self) -> float: + """Take a measurement using the current configuration. + + Must call configure_dc_voltage() or configure_dc_current() first + to set the measurement function. + + Returns: + Measured value (voltage in V or current in A). + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query_float("READ?") diff --git a/src/py_dvt_ate/instruments/drivers/power_supply.py b/src/py_dvt_ate/instruments/drivers/power_supply.py new file mode 100644 index 0000000..c5c8a84 --- /dev/null +++ b/src/py_dvt_ate/instruments/drivers/power_supply.py @@ -0,0 +1,152 @@ +"""Power supply SCPI driver. + +This module implements a client-side driver for programmable power supplies +that communicate via SCPI commands. +""" + +from py_dvt_ate.instruments.drivers.base import BaseDriver + + +class PowerSupplyDriver(BaseDriver): + """SCPI driver for programmable power supplies. + + Provides high-level Python API for controlling power supplies via + SCPI commands. Implements the IPowerSupply interface. + + Note: This driver assumes a single-channel instrument. The channel + parameter is accepted for interface compatibility but currently ignored. + + SCPI Commands Used: + VOLT - Set output voltage (V) + VOLT? - Query voltage setpoint + CURR - Set current limit (A) + CURR? - Query current limit + OUTP - Enable/disable output + OUTP? - Query output state (1=on, 0=off) + MEAS:VOLT? - Measure actual output voltage + MEAS:CURR? - Measure actual output current + + Example: + >>> transport = TCPTransport("localhost", 5002) + >>> psu = PowerSupplyDriver(transport) + >>> psu.connect() + >>> psu.set_voltage(1, 3.3) + >>> psu.set_current_limit(1, 0.5) + >>> psu.enable_output(1, True) + >>> voltage = psu.measure_voltage(1) + """ + + def set_voltage(self, channel: int, voltage: float) -> None: + """Set the output voltage setpoint. + + Args: + channel: Channel number (currently ignored, single channel assumed). + voltage: Target voltage in volts. + + Raises: + ConnectionError: If not connected. + IOError: If command fails. + """ + self.write(f"VOLT {voltage:.3f}") + + def get_voltage(self, channel: int) -> float: + """Get the voltage setpoint. + + Args: + channel: Channel number (currently ignored, single channel assumed). + + Returns: + Current voltage setpoint in volts. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query_float("VOLT?") + + def set_current_limit(self, channel: int, current: float) -> None: + """Set the current limit. + + Args: + channel: Channel number (currently ignored, single channel assumed). + current: Current limit in amps. + + Raises: + ConnectionError: If not connected. + IOError: If command fails. + """ + self.write(f"CURR {current:.3f}") + + def get_current_limit(self, channel: int) -> float: + """Get the current limit. + + Args: + channel: Channel number (currently ignored, single channel assumed). + + Returns: + Current limit in amps. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query_float("CURR?") + + def measure_voltage(self, channel: int) -> float: + """Measure the actual output voltage. + + Args: + channel: Channel number (currently ignored, single channel assumed). + + Returns: + Measured voltage in volts. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query_float("MEAS:VOLT?") + + def measure_current(self, channel: int) -> float: + """Measure the actual output current. + + Args: + channel: Channel number (currently ignored, single channel assumed). + + Returns: + Measured current in amps. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query_float("MEAS:CURR?") + + def enable_output(self, channel: int, enable: bool) -> None: + """Enable or disable the output. + + Args: + channel: Channel number (currently ignored, single channel assumed). + enable: True to enable output, False to disable. + + Raises: + ConnectionError: If not connected. + IOError: If command fails. + """ + state = "ON" if enable else "OFF" + self.write(f"OUTP {state}") + + def is_output_enabled(self, channel: int) -> bool: + """Check if output is enabled. + + Args: + channel: Channel number (currently ignored, single channel assumed). + + Returns: + True if output is enabled, False if disabled. + + Raises: + ConnectionError: If not connected. + IOError: If query fails. + """ + return self.query_bool("OUTP?")