From 4c23786238483f76cf880cfef1afc18440f93b22 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Fri, 19 Sep 2025 10:21:43 +0200 Subject: [PATCH 1/5] first version of driver for delta elektronika PS via PSC-ETH interface --- src/instruments/__init__.py | 1 + src/instruments/delta_elektronika/__init__.py | 6 + src/instruments/delta_elektronika/psc_eth.py | 200 ++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 src/instruments/delta_elektronika/__init__.py create mode 100644 src/instruments/delta_elektronika/psc_eth.py diff --git a/src/instruments/__init__.py b/src/instruments/__init__.py index 6fd26402..4cafbfcc 100644 --- a/src/instruments/__init__.py +++ b/src/instruments/__init__.py @@ -14,6 +14,7 @@ from . import agilent 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..98977f4f --- /dev/null +++ b/src/instruments/delta_elektronika/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +""" +Module containing Dressler 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..5e66162a --- /dev/null +++ b/src/instruments/delta_elektronika/psc_eth.py @@ -0,0 +1,200 @@ +"""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. + This setting seems to be reset when the device is power-cycled and cannot be + set remotely. + + 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 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}" + print(cmd) + self.sendcmd(cmd) From 22fe130b59492c2753980902536739683538551a Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Tue, 23 Sep 2025 16:32:18 +0200 Subject: [PATCH 2/5] add docs, extend commands --- doc/source/apiref/delta_elektronika.rst | 12 +++++++++ src/instruments/delta_elektronika/psc_eth.py | 27 +++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 doc/source/apiref/delta_elektronika.rst 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/delta_elektronika/psc_eth.py b/src/instruments/delta_elektronika/psc_eth.py index 5e66162a..52e85d7f 100644 --- a/src/instruments/delta_elektronika/psc_eth.py +++ b/src/instruments/delta_elektronika/psc_eth.py @@ -17,8 +17,6 @@ class PscEth(Instrument): PSC-ETH-2 ethernet interface. For communication, make sure the device is set to "ethernet" mode. - This setting seems to be reset when the device is power-cycled and cannot be - set remotely. Example: >>> import instruments as ik @@ -43,7 +41,7 @@ def name(self) -> str: return self.query("*IDN?") @property - def current_limit(self) -> Tuple["PscEth.LimitStatus", u.Quantity]: + 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. @@ -55,7 +53,7 @@ def current_limit(self) -> Tuple["PscEth.LimitStatus", u.Quantity]: return ls, assume_units(float(val), u.A) @property - def voltage_limit(self) -> Tuple["PscEth.LimitStatus", u.Quantity]: + 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. @@ -182,6 +180,10 @@ 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: @@ -198,3 +200,20 @@ def set_current_limit( cmd = f"SYST:LIM:CUR {val:.15f},{stat.name}" print(cmd) 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}" + print(cmd) + self.sendcmd(cmd) From a5c9f4b0e2d270ded67e70a3a00be6dad99770fd Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Tue, 23 Sep 2025 21:46:34 +0200 Subject: [PATCH 3/5] add tests --- src/instruments/delta_elektronika/psc_eth.py | 6 +- tests/test_delta_elektronika/__init__.py | 0 tests/test_delta_elektronika/test_psc_eth.py | 234 +++++++++++++++++++ 3 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 tests/test_delta_elektronika/__init__.py create mode 100644 tests/test_delta_elektronika/test_psc_eth.py diff --git a/src/instruments/delta_elektronika/psc_eth.py b/src/instruments/delta_elektronika/psc_eth.py index 52e85d7f..59b65773 100644 --- a/src/instruments/delta_elektronika/psc_eth.py +++ b/src/instruments/delta_elektronika/psc_eth.py @@ -95,7 +95,7 @@ def voltage_limit(self) -> tuple["PscEth.LimitStatus", u.Quantity]: ) current_measure = unitful_property( - "MEAS:CURR?", + "MEAS:CURR", u.A, format_code="{:.15f}", readonly=True, @@ -149,7 +149,7 @@ def voltage_limit(self) -> tuple["PscEth.LimitStatus", u.Quantity]: ) voltage_measure = unitful_property( - "MEAS:VOLT?", + "MEAS:VOLT", u.V, format_code="{:.15f}", readonly=True, @@ -198,7 +198,6 @@ def set_current_limit( 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}" - print(cmd) self.sendcmd(cmd) def set_voltage_limit( @@ -215,5 +214,4 @@ def set_voltage_limit( 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}" - print(cmd) 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..c6d71507 --- /dev/null +++ b/tests/test_delta_elektronika/test_psc_eth.py @@ -0,0 +1,234 @@ +#!/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 # + + +def test_name(): + """Get the instrument 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) From 33438a2c6d7d9737b0ce80c364b7231175d122d0 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Tue, 23 Sep 2025 22:03:40 +0200 Subject: [PATCH 4/5] fix name test --- tests/test_delta_elektronika/test_psc_eth.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_delta_elektronika/test_psc_eth.py b/tests/test_delta_elektronika/test_psc_eth.py index c6d71507..1027fe14 100644 --- a/tests/test_delta_elektronika/test_psc_eth.py +++ b/tests/test_delta_elektronika/test_psc_eth.py @@ -11,9 +11,7 @@ # TEST CLASS PROPERTIES # -def test_name(): - """Get the instrument name.""" - make_name_test(ik.delta_elektronika.PscEth) +test_psc_eth_device_name = make_name_test(ik.delta_elektronika.PscEth) def test_current_limit(): From 23102c1a72042d2c6e528166627c7591058b0b99 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Tue, 23 Sep 2025 22:57:39 +0200 Subject: [PATCH 5/5] fix name in init file --- src/instruments/delta_elektronika/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instruments/delta_elektronika/__init__.py b/src/instruments/delta_elektronika/__init__.py index 98977f4f..c7358388 100644 --- a/src/instruments/delta_elektronika/__init__.py +++ b/src/instruments/delta_elektronika/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python """ -Module containing Dressler instruments +Module containing Delta Elektronika instruments """ from instruments.delta_elektronika.psc_eth import PscEth