From dd6e16fa886fdae3a7c246427aef18bf0919fc1b Mon Sep 17 00:00:00 2001 From: Sergey Lukin Date: Sun, 8 Mar 2026 18:01:00 +0100 Subject: [PATCH 1/3] Add Kiprim DC310S power supply driver Add support for the Kiprim DC310S single-output serial power supply. This adds a conservative InstrumentKit-style driver with: - voltage and current setpoints - output enable/disable - measured voltage, current, and power readback - documented 0-30 V and 0-10 A bounds on setpoints Also add transcript-based unit tests, package exports, API reference docs, and a minimal example script. --- doc/examples/kiprim/ex_kiprim_dc310s.py | 14 ++ doc/source/apiref/index.rst | 1 + doc/source/apiref/kiprim.rst | 12 ++ src/instruments/__init__.py | 1 + src/instruments/kiprim/__init__.py | 6 + src/instruments/kiprim/dc310s.py | 180 ++++++++++++++++++++++++ tests/test_kiprim/__init__.py | 1 + tests/test_kiprim/test_dc310s.py | 122 ++++++++++++++++ 8 files changed, 337 insertions(+) create mode 100644 doc/examples/kiprim/ex_kiprim_dc310s.py create mode 100644 doc/source/apiref/kiprim.rst create mode 100644 src/instruments/kiprim/__init__.py create mode 100644 src/instruments/kiprim/dc310s.py create mode 100644 tests/test_kiprim/__init__.py create mode 100644 tests/test_kiprim/test_dc310s.py diff --git a/doc/examples/kiprim/ex_kiprim_dc310s.py b/doc/examples/kiprim/ex_kiprim_dc310s.py new file mode 100644 index 00000000..30129444 --- /dev/null +++ b/doc/examples/kiprim/ex_kiprim_dc310s.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import instruments as ik + +psu = ik.kiprim.DC310S.open_serial("COM8", baud=115200, timeout=0.5) + +print(psu.name) +print(f"Setpoint: {psu.voltage}, {psu.current}, output={psu.output}") +print(f"Measured: {psu.voltage_sense}, {psu.current_sense}, {psu.power_sense}") + +# Uncomment to configure the supply explicitly. +# psu.voltage = 5 * ik.units.volt +# psu.current = 0.25 * ik.units.ampere +# psu.output = True diff --git a/doc/source/apiref/index.rst b/doc/source/apiref/index.rst index 4ad370ae..271aaa24 100644 --- a/doc/source/apiref/index.rst +++ b/doc/source/apiref/index.rst @@ -24,6 +24,7 @@ Contents: holzworth hp keithley + kiprim lakeshore minghe mettler_toledo diff --git a/doc/source/apiref/kiprim.rst b/doc/source/apiref/kiprim.rst new file mode 100644 index 00000000..9429cf6c --- /dev/null +++ b/doc/source/apiref/kiprim.rst @@ -0,0 +1,12 @@ +.. currentmodule:: instruments.kiprim + +====== +Kiprim +====== + +:class:`DC310S` Power Supply +============================ + +.. autoclass:: DC310S + :members: + :undoc-members: diff --git a/src/instruments/__init__.py b/src/instruments/__init__.py index 660326bf..2b54bfd1 100644 --- a/src/instruments/__init__.py +++ b/src/instruments/__init__.py @@ -24,6 +24,7 @@ from . import holzworth from . import hp from . import keithley +from . import kiprim from . import lakeshore from . import mettler_toledo from . import minghe diff --git a/src/instruments/kiprim/__init__.py b/src/instruments/kiprim/__init__.py new file mode 100644 index 00000000..baf4c316 --- /dev/null +++ b/src/instruments/kiprim/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +""" +Module containing Kiprim instruments. +""" + +from .dc310s import DC310S diff --git a/src/instruments/kiprim/dc310s.py b/src/instruments/kiprim/dc310s.py new file mode 100644 index 00000000..ced6afda --- /dev/null +++ b/src/instruments/kiprim/dc310s.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +""" +Driver for the Kiprim DC310S single-output power supply. +""" + +# IMPORTS ##################################################################### + +from instruments.abstract_instruments import PowerSupply +from instruments.units import ureg as u +from instruments.util_fns import bounded_unitful_property, unitful_property + +# FUNCTIONS ################################################################### + + +def _parse_output_state(reply): + """ + Normalize the DC310S output-state reply into a boolean value. + + The DC310S has been observed to report either ``ON``/``OFF`` or ``1``/``0`` + depending on firmware and transport state. + """ + + reply = reply.strip().upper() + if reply in {"1", "ON"}: + return True + if reply in {"0", "OFF"}: + return False + raise ValueError(f"Unexpected output-state reply: {reply}") + + +# CLASSES ##################################################################### + + +class DC310S(PowerSupply, PowerSupply.Channel): + """ + The Kiprim DC310S is a single-output programmable DC power supply. + + Because the supply has one programmable output, this object inherits from + both `~instruments.abstract_instruments.power_supply.PowerSupply` and + `~instruments.abstract_instruments.power_supply.PowerSupply.Channel`. + + Example usage: + + >>> import instruments as ik + >>> psu = ik.kiprim.DC310S.open_serial("COM8", baud=115200, timeout=0.5) + >>> psu.voltage = 5 * ik.units.volt + >>> psu.current = 0.25 * ik.units.ampere + >>> psu.output = True + >>> psu.voltage_sense + + """ + + voltage, voltage_min, voltage_max = bounded_unitful_property( + "VOLT", + u.volt, + format_code="{:.3f}", + input_decoration=str.strip, + valid_range=(0 * u.volt, 30 * u.volt), + doc=""" + Gets/sets the programmed output voltage. + + The DC310S product documentation specifies a programmable output range + of 0 V to 30 V. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~pint.Quantity` + """, + ) + + current, current_min, current_max = bounded_unitful_property( + "CURR", + u.amp, + format_code="{:.3f}", + input_decoration=str.strip, + valid_range=(0 * u.amp, 10 * u.amp), + doc=""" + Gets/sets the programmed output current limit. + + The DC310S product documentation specifies a programmable output range + of 0 A to 10 A. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~pint.Quantity` + """, + ) + + voltage_sense = unitful_property( + "MEAS:VOLT", + u.volt, + readonly=True, + input_decoration=str.strip, + doc=""" + Gets the measured output voltage. + + :units: :math:`\\text{V}` + :rtype: `~pint.Quantity` + """, + ) + + current_sense = unitful_property( + "MEAS:CURR", + u.amp, + readonly=True, + input_decoration=str.strip, + doc=""" + Gets the measured output current. + + :units: :math:`\\text{A}` + :rtype: `~pint.Quantity` + """, + ) + + power_sense = unitful_property( + "MEAS:POW", + u.watt, + readonly=True, + input_decoration=str.strip, + doc=""" + Gets the measured output power. + + :units: :math:`\\text{W}` + :rtype: `~pint.Quantity` + """, + ) + + @property + def output(self): + """ + Gets/sets the output state. + + :type: `bool` + """ + + return _parse_output_state(self.query("OUTP?")) + + @output.setter + def output(self, newval): + if not isinstance(newval, bool): + raise TypeError("Output state must be specified with a boolean value.") + self.sendcmd(f"OUTP {'ON' if newval else 'OFF'}") + + @property + def name(self): + """ + Gets the instrument name as reported by ``*IDN?``. + + :rtype: `str` + """ + + idn_string = self.query("*IDN?") + idn_parts = [part.strip() for part in idn_string.split(",")] + if len(idn_parts) >= 2: + return " ".join(idn_parts[:2]) + return idn_string.strip() + + @property + def mode(self): + """ + Unimplemented. + """ + + raise NotImplementedError("The DC310S does not expose a stable mode query.") + + @mode.setter + def mode(self, newval): + """ + Unimplemented. + """ + + raise NotImplementedError("The DC310S does not expose a stable mode query.") + + @property + def channel(self): + """ + Return the single output channel. + + :rtype: `tuple` + """ + + return (self,) diff --git a/tests/test_kiprim/__init__.py b/tests/test_kiprim/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/test_kiprim/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_kiprim/test_dc310s.py b/tests/test_kiprim/test_dc310s.py new file mode 100644 index 00000000..ae318623 --- /dev/null +++ b/tests/test_kiprim/test_dc310s.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +""" +Unit tests for the Kiprim DC310S single-output power supply. +""" + +# IMPORTS ##################################################################### + +import pytest + +import instruments as ik +from instruments.units import ureg as u +from tests import expected_protocol, unit_eq + +# TESTS ####################################################################### + + +def test_channel(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + assert psu.channel[0] == psu + assert len(psu.channel) == 1 + + +def test_name(): + with expected_protocol( + ik.kiprim.DC310S, ["*IDN?"], ["KIPRIM,DC310S,22371243,FV:V3.7.0"], sep="\n" + ) as psu: + assert psu.name == "KIPRIM DC310S" + + +def test_voltage(): + with expected_protocol( + ik.kiprim.DC310S, ["VOLT 5.000", "VOLT?"], ["5.000"], sep="\n" + ) as psu: + psu.voltage = 5 * u.volt + unit_eq(psu.voltage, 5 * u.volt) + + +def test_voltage_bounds(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + unit_eq(psu.voltage_min, 0 * u.volt) + unit_eq(psu.voltage_max, 30 * u.volt) + with pytest.raises(ValueError): + psu.voltage = 30.001 * u.volt + with pytest.raises(ValueError): + psu.voltage = -0.001 * u.volt + + +def test_current(): + with expected_protocol( + ik.kiprim.DC310S, ["CURR 0.250", "CURR?"], ["0.250"], sep="\n" + ) as psu: + psu.current = 0.25 * u.amp + unit_eq(psu.current, 0.25 * u.amp) + + +def test_current_bounds(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + unit_eq(psu.current_min, 0 * u.amp) + unit_eq(psu.current_max, 10 * u.amp) + with pytest.raises(ValueError): + psu.current = 10.001 * u.amp + with pytest.raises(ValueError): + psu.current = -0.001 * u.amp + + +def test_voltage_sense(): + with expected_protocol( + ik.kiprim.DC310S, ["MEAS:VOLT?"], ["12.340"], sep="\n" + ) as psu: + unit_eq(psu.voltage_sense, 12.34 * u.volt) + + +def test_current_sense(): + with expected_protocol( + ik.kiprim.DC310S, ["MEAS:CURR?"], ["0.456"], sep="\n" + ) as psu: + unit_eq(psu.current_sense, 0.456 * u.amp) + + +def test_power_sense(): + with expected_protocol(ik.kiprim.DC310S, ["MEAS:POW?"], ["5.624"], sep="\n") as psu: + unit_eq(psu.power_sense, 5.624 * u.watt) + + +def test_output_on(): + with expected_protocol( + ik.kiprim.DC310S, ["OUTP ON", "OUTP?"], ["ON"], sep="\n" + ) as psu: + psu.output = True + assert psu.output + + +def test_output_off_numeric_reply(): + with expected_protocol( + ik.kiprim.DC310S, ["OUTP OFF", "OUTP?"], ["0"], sep="\n" + ) as psu: + psu.output = False + assert psu.output is False + + +def test_output_invalid_reply(): + with expected_protocol(ik.kiprim.DC310S, ["OUTP?"], ["MAYBE"], sep="\n") as psu: + with pytest.raises(ValueError): + _ = psu.output + + +def test_output_setter_requires_bool(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + with pytest.raises(TypeError): + psu.output = 1 + + +def test_mode_getter_unimplemented(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + with pytest.raises(NotImplementedError): + _ = psu.mode + + +def test_mode_setter_unimplemented(): + with expected_protocol(ik.kiprim.DC310S, [], [], sep="\n") as psu: + with pytest.raises(NotImplementedError): + psu.mode = "cv" From 07c1ccbaabac1d0b3f418723d6ba8b1afc8e1e8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:09:44 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_kiprim/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_kiprim/__init__.py b/tests/test_kiprim/__init__.py index 8b137891..e69de29b 100644 --- a/tests/test_kiprim/__init__.py +++ b/tests/test_kiprim/__init__.py @@ -1 +0,0 @@ - From 9c8dd59d15c4704b8410c53dee3cdfc5af585a22 Mon Sep 17 00:00:00 2001 From: Sergey Lukin Date: Mon, 9 Mar 2026 00:24:03 +0100 Subject: [PATCH 3/3] Add missing DC310S name fallback coverage --- tests/test_kiprim/test_dc310s.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_kiprim/test_dc310s.py b/tests/test_kiprim/test_dc310s.py index ae318623..98d3617e 100644 --- a/tests/test_kiprim/test_dc310s.py +++ b/tests/test_kiprim/test_dc310s.py @@ -27,6 +27,11 @@ def test_name(): assert psu.name == "KIPRIM DC310S" +def test_name_single_field_reply(): + with expected_protocol(ik.kiprim.DC310S, ["*IDN?"], ["DC310S"], sep="\n") as psu: + assert psu.name == "DC310S" + + def test_voltage(): with expected_protocol( ik.kiprim.DC310S, ["VOLT 5.000", "VOLT?"], ["5.000"], sep="\n"