diff --git a/doc/source/apiref/delta_elektronika.rst b/doc/source/apiref/delta_elektronika.rst new file mode 100644 index 00000000..d6b6bfce --- /dev/null +++ b/doc/source/apiref/delta_elektronika.rst @@ -0,0 +1,12 @@ +.. currentmodule:: instruments.delta_elektronika + +================= +Delta Elektronika +================= + +:class:`PscEth` Power Supply over Ethernet controller +===================================================== + +.. autoclass:: PscEth + :members: + :undoc-members: diff --git a/src/instruments/__init__.py b/src/instruments/__init__.py index 1994f8d5..660326bf 100644 --- a/src/instruments/__init__.py +++ b/src/instruments/__init__.py @@ -15,6 +15,7 @@ from . import aimtti from . import comet from . import dressler +from . import delta_elektronika from . import generic_scpi from . import fluke from . import gentec_eo diff --git a/src/instruments/delta_elektronika/__init__.py b/src/instruments/delta_elektronika/__init__.py new file mode 100644 index 00000000..c7358388 --- /dev/null +++ b/src/instruments/delta_elektronika/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +""" +Module containing Delta Elektronika instruments +""" + +from instruments.delta_elektronika.psc_eth import PscEth diff --git a/src/instruments/delta_elektronika/psc_eth.py b/src/instruments/delta_elektronika/psc_eth.py new file mode 100644 index 00000000..59b65773 --- /dev/null +++ b/src/instruments/delta_elektronika/psc_eth.py @@ -0,0 +1,217 @@ +"""Support for Delta Elektronika DC power supplies with PSC-ETH-2 interface.""" + +# IMPORTS ##################################################################### + +from enum import IntEnum +from typing import Tuple, Union + +from instruments.abstract_instruments import Instrument +from instruments.units import ureg as u +from instruments.util_fns import assume_units, unitful_property + +# CLASSES ##################################################################### + + +class PscEth(Instrument): + """Communicate with a Delta Elektronica one channel power supply via the + PSC-ETH-2 ethernet interface. + + For communication, make sure the device is set to "ethernet" mode. + + Example: + >>> import instruments as ik + >>> from instruments import units as u + >>> i = ik.delta_elektronika.PscEth.open_tcpip("192.168.127.100", port=8462) + >>> print(i.name) + """ + + def __init__(self, filelike): + super().__init__(filelike) + + class LimitStatus(IntEnum): + """Enum class for the limit status.""" + + OFF = 0 + ON = 1 + + # CLASS PROPERTIES # + + @property + def name(self) -> str: + return self.query("*IDN?") + + @property + def current_limit(self) -> tuple["PscEth.LimitStatus", u.Quantity]: + """Get the current limit status. + + :return: A tuple of the current limit status and the current limit value. + :rtype: `tuple` of (`PscEth.LimitStatus`, `~pint.Quantity`) + """ + resp = self.query("SYST:LIM:CUR?") + val, status = resp.split(",") + ls = self.LimitStatus.OFF if "off" in status.lower() else self.LimitStatus.ON + return ls, assume_units(float(val), u.A) + + @property + def voltage_limit(self) -> tuple["PscEth.LimitStatus", u.Quantity]: + """Get the voltage limit status. + + :return: A tuple of the voltage limit status and the voltage limit value. + :rtype: `tuple` of (`PscEth.LimitStatus`, `~pint.Quantity`) + """ + resp = self.query("SYST:LIM:VOL?") + val, status = resp.split(",") + ls = self.LimitStatus.OFF if "off" in status.lower() else self.LimitStatus.ON + return ls, assume_units(float(val), u.V) + + current = unitful_property( + "SOUR:CURR", + u.A, + format_code="{:.15f}", + doc=""" + Set/get the output current. + + Note: There is no bound checking of the value specified. + + :newval: The output current to set. + :uval: `float` (assumes milliamps) or `~pint.Quantity` + """, + ) + + current_max = unitful_property( + "SOUR:CURR:MAX", + u.A, + format_code="{:.15f}", + doc=""" + Set/get the maximum output current. + + Note: This value should generally not be used. It sets the maximum + capable current of the power supply, which is fixed by the hardware. + If you set this to other values, you will get strange measurement results. + + :newval: The maximum output current to set. + :uval: `float` (assumes milliamps) or `~pint.Quantity` + """, + ) + + current_measure = unitful_property( + "MEAS:CURR", + u.A, + format_code="{:.15f}", + readonly=True, + doc=""" + Get the measured output current. + + :rtype: `~pint.Quantity` + """, + ) + + current_stepsize = unitful_property( + "SOUR:CUR:STE", + u.A, + format_code="{:.15f}", + readonly=True, + doc=""" + Get the output current step size. + + :rtype: `~pint.Quantity` + """, + ) + + voltage = unitful_property( + "SOUR:VOL", + u.V, + format_code="{:.15f}", + doc=""" + Set/get the output voltage. + + Note: There is no bound checking of the value specified. + + :newval: The output voltage to set. + :uval: `float` (assumes volts) or `~pint.Quantity` + """, + ) + + voltage_max = unitful_property( + "SOUR:VOLT:MAX", + u.V, + format_code="{:.15f}", + doc=""" + Set/get the maximum output voltage. + + Note: This value should generally not be used. It sets the maximum + capable voltage of the power supply, which is fixed by the hardware. + If you set this to other values, you will get strange measurement results. + + :newval: The maximum output voltage to set. + :uval: `float` (assumes volts) or `~pint.Quantity` + """, + ) + + voltage_measure = unitful_property( + "MEAS:VOLT", + u.V, + format_code="{:.15f}", + readonly=True, + doc=""" + Get the measured output voltage. + + :rtype: `~pint.Quantity` + """, + ) + + voltage_stepsize = unitful_property( + "SOUR:VOL:STE", + u.V, + format_code="{:.15f}", + readonly=True, + doc=""" + Get the output voltage step size. + + :rtype: `~pint.Quantity` + """, + ) + + def recall(self) -> None: + """Recall the settings from non-volatile memory.""" + self.sendcmd("*RCL") + + def reset(self) -> None: + """Reset the instrument to default settings.""" + self.sendcmd("*RST") + + def save(self) -> None: + """Save the current settings to non-volatile memory.""" + self.sendcmd("*SAV") + + def set_current_limit( + self, stat: "PscEth.LimitStatus", val: Union[float, u.Quantity] = 0 + ) -> None: + """Set the current limit. + + :param stat: The limit status to set. + :type stat: `PscEth.LimitStatus` + :param val: The current limit value to set. Only requiered when turning it on. + :type val: `float` (assumes milliamps) or `~pint.Quantity` + """ + if not isinstance(stat, PscEth.LimitStatus): + raise TypeError("stat must be of type PscEth.LimitStatus") + val = assume_units(val, u.A).to(u.A).magnitude + cmd = f"SYST:LIM:CUR {val:.15f},{stat.name}" + self.sendcmd(cmd) + + def set_voltage_limit( + self, stat: "PscEth.LimitStatus", val: Union[float, u.Quantity] = 0 + ) -> None: + """Set the voltage limit. + + :param stat: The limit status to set. + :type stat: `PscEth.LimitStatus` + :param val: The voltage limit value to set. Only requiered when turning it on. + :type val: `float` (assumes volts) or `~pint.Quantity` + """ + if not isinstance(stat, PscEth.LimitStatus): + raise TypeError("stat must be of type PscEth.LimitStatus") + val = assume_units(val, u.V).to(u.V).magnitude + cmd = f"SYST:LIM:VOL {val:.15f},{stat.name}" + self.sendcmd(cmd) diff --git a/tests/test_delta_elektronika/__init__.py b/tests/test_delta_elektronika/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_delta_elektronika/test_psc_eth.py b/tests/test_delta_elektronika/test_psc_eth.py new file mode 100644 index 00000000..1027fe14 --- /dev/null +++ b/tests/test_delta_elektronika/test_psc_eth.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +"""Tests for the Delta Elektronika PSC-ETH interface.""" + +from hypothesis import given, strategies as st +import pytest + +import instruments as ik +from instruments.units import ureg as u +from tests import expected_protocol, make_name_test, unit_eq + +# TEST CLASS PROPERTIES # + + +test_psc_eth_device_name = make_name_test(ik.delta_elektronika.PscEth) + + +def test_current_limit(): + """Get the current limit of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SYST:LIM:CUR?", "SYST:LIM:CUR?"], + ["0.0,OFF", "0.2,ON"], + sep="\n", + ) as rf: + status, value = rf.current_limit + assert status == ik.delta_elektronika.PscEth.LimitStatus.OFF + unit_eq(value, 0.0 * u.A) + + status, value = rf.current_limit + assert status == ik.delta_elektronika.PscEth.LimitStatus.ON + unit_eq(value, 0.2 * u.A) + + +def test_voltage_limit(): + """Get the voltage limit of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SYST:LIM:VOL?", "SYST:LIM:VOL?"], + ["0.0,OFF", "20.0,ON"], + sep="\n", + ) as rf: + status, value = rf.voltage_limit + assert status == ik.delta_elektronika.PscEth.LimitStatus.OFF + unit_eq(value, 0.0 * u.V) + + status, value = rf.voltage_limit + assert status == ik.delta_elektronika.PscEth.LimitStatus.ON + unit_eq(value, 20.0 * u.V) + + +def test_current(): + """Get/set the output current of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SOUR:CURR?", f"SOUR:CURR {0.1:.15f}", "SOUR:CURR?"], + ["0.0", "0.1"], + sep="\n", + ) as rf: + unit_eq(rf.current, 0.0 * u.A) + rf.current = 0.1 * u.A + unit_eq(rf.current, 0.1 * u.A) + + +def test_current_max(): + """Get/set the maximum output current of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SOUR:CURR:MAX?", f"SOUR:CURR:MAX {0.2:.15f}", "SOUR:CURR:MAX?"], + ["0.1", "0.2"], + sep="\n", + ) as rf: + unit_eq(rf.current_max, 0.1 * u.A) + rf.current_max = 0.2 * u.A + unit_eq(rf.current_max, 0.2 * u.A) + + +def test_current_measure(): + """Get the measured output current of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["MEAS:CURR?", "MEAS:CURR?"], + ["0.0", "0.1"], + sep="\n", + ) as rf: + unit_eq(rf.current_measure, 0.0 * u.A) + unit_eq(rf.current_measure, 0.1 * u.A) + + +def test_current_stepsize(): + """Get the current stepsize of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SOUR:CUR:STE?", "SOUR:CUR:STE?"], + ["0.001", "0.01"], + sep="\n", + ) as rf: + unit_eq(rf.current_stepsize, 0.001 * u.A) + unit_eq(rf.current_stepsize, 0.01 * u.A) + + +def test_voltage(): + """Get/set the output voltage of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SOUR:VOL?", f"SOUR:VOL {10.0:.15f}", "SOUR:VOL?"], + ["0.0", "10.0"], + sep="\n", + ) as rf: + unit_eq(rf.voltage, 0.0 * u.V) + rf.voltage = 10.0 * u.V + unit_eq(rf.voltage, 10.0 * u.V) + + +def test_voltage_max(): + """Get/set the maximum output voltage of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SOUR:VOLT:MAX?", f"SOUR:VOLT:MAX {20.0:.15f}", "SOUR:VOLT:MAX?"], + ["10.0", "20.0"], + sep="\n", + ) as rf: + unit_eq(rf.voltage_max, 10.0 * u.V) + rf.voltage_max = 20.0 * u.V + unit_eq(rf.voltage_max, 20.0 * u.V) + + +def test_voltage_measure(): + """Get the measured output voltage of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["MEAS:VOLT?", "MEAS:VOLT?"], + ["0.0", "10.0"], + sep="\n", + ) as rf: + unit_eq(rf.voltage_measure, 0.0 * u.V) + unit_eq(rf.voltage_measure, 10.0 * u.V) + + +def test_voltage_stepsize(): + """Get the voltage stepsize of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["SOUR:VOL:STE?", "SOUR:VOL:STE?"], + ["0.01", "0.1"], + sep="\n", + ) as rf: + unit_eq(rf.voltage_stepsize, 0.01 * u.V) + unit_eq(rf.voltage_stepsize, 0.1 * u.V) + + +# TEST CLASS METHODS # + + +def test_recall(): + """Recall a stored setting from non-volatile memory.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["*RCL"], + [], + sep="\n", + ) as rf: + rf.recall() + + +def test_reset(): + """Reset the instrument to default settings.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["*RST"], + [], + sep="\n", + ) as rf: + rf.reset() + + +def test_save(): + """Save the current settings to non-volatile memory.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + ["*SAV"], + [], + sep="\n", + ) as rf: + rf.save() + + +def test_set_current_limit(): + """Set the current limit of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + [f"SYST:LIM:CUR {0.0:.15f},OFF", f"SYST:LIM:CUR {0.2:.15f},ON"], + [], + sep="\n", + ) as rf: + rf.set_current_limit(ik.delta_elektronika.PscEth.LimitStatus.OFF) + rf.set_current_limit(ik.delta_elektronika.PscEth.LimitStatus.ON, 0.2 * u.A) + + +def test_set_current_limit_invalid_type(): + """Setting current limit with invalid type raises TypeError.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + [], + [], + sep="\n", + ) as rf: + with pytest.raises(TypeError): + rf.set_current_limit("ON", 0.2 * u.A) + + +def test_set_voltage_limit(): + """Set the voltage limit of the instrument.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + [f"SYST:LIM:VOL {0.0:.15f},OFF", f"SYST:LIM:VOL {20.0:.15f},ON"], + [], + sep="\n", + ) as rf: + rf.set_voltage_limit(ik.delta_elektronika.PscEth.LimitStatus.OFF) + rf.set_voltage_limit(ik.delta_elektronika.PscEth.LimitStatus.ON, 20.0 * u.V) + + +def test_set_voltage_limit_invalid_type(): + """Setting voltage limit with invalid type raises TypeError.""" + with expected_protocol( + ik.delta_elektronika.PscEth, + [], + [], + sep="\n", + ) as rf: + with pytest.raises(TypeError): + rf.set_voltage_limit("ON", 20.0 * u.V)