From c5f1aeaeeecd3892f64a46226981e668d04afaef Mon Sep 17 00:00:00 2001 From: Thomas Vreys Date: Wed, 22 Jul 2026 11:57:49 +0200 Subject: [PATCH] Make the measured power reading optional Not every SPD model implements MEAS:POWE?. Such a model leaves the query unanswered, so the read times out and pyvisa raises VisaIOError. Tested on Siglent SPD1305X model by querying watts from non-existend channel "MEAS:POWE? CH9" Add tests in labgrid for both failure modes: an unanswered query and a non-numeric reply. This work was sponsored by OIP Sensor Systems. Signed-off-by: Thomas Vreys --- labgrid/driver/power/siglent.py | 9 +++++++-- labgrid/driver/powerdriver.py | 2 +- labgrid/remote/client.py | 19 +++++++----------- tests/test_powerdriver.py | 34 +++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/labgrid/driver/power/siglent.py b/labgrid/driver/power/siglent.py index 88e51268f..e238ec115 100644 --- a/labgrid/driver/power/siglent.py +++ b/labgrid/driver/power/siglent.py @@ -38,13 +38,18 @@ def power_show(host, port, index): with _get_psu(host) as psu: v_measured = psu.query(f"MEAS:VOLT? CH{index}") a_measured = psu.query(f"MEAS:CURR? CH{index}") - w_measured = psu.query(f"MEAS:POWE? CH{index}") + # Not every SPD model implements MEAS:POWE?; such a model leaves the + # query unanswered, so the read times out and raises VisaIOError. + try: + w_measured = float(psu.query(f"MEAS:POWE? CH{index}")) + except (ValueError, pyvisa.VisaIOError): + w_measured = None v_set = psu.query(f"CH{index}:VOLT?") a_set = psu.query(f"CH{index}:CURR?") return { "voltage": float(v_measured), "amps": float(a_measured), - "watts": float(w_measured), + "watts": w_measured, "v_limit": float(v_set), "a_limit": float(a_set) } diff --git a/labgrid/driver/powerdriver.py b/labgrid/driver/powerdriver.py index 991ce3091..e4c22c019 100644 --- a/labgrid/driver/powerdriver.py +++ b/labgrid/driver/powerdriver.py @@ -7,7 +7,7 @@ from ..exceptions import InvalidConfigError from ..factory import target_factory -from ..protocol import PowerProtocol, DigitalOutputProtocol, ResetProtocol, ProgrammablePowerProtocol +from ..protocol import PowerProtocol, DigitalOutputProtocol, ResetProtocol from ..resource import NetworkPowerPort from ..step import step from ..util.proxy import proxymanager diff --git a/labgrid/remote/client.py b/labgrid/remote/client.py index 280b48178..1632bd4b9 100755 --- a/labgrid/remote/client.py +++ b/labgrid/remote/client.py @@ -988,12 +988,11 @@ def power(self): if action == "get": print(f"power{' ' + name if name else ''} for place {place.name} is {'on' if res else 'off'}") elif action == "show": - print( - f"Voltage: {res['voltage']}V [{res['v_limit']}V], " - f"Current: {res['amps']}A [{res['a_limit']}A], " - f"Power: {res['watts']}W" - ) - + line = f"Voltage: {res['voltage']}V [{res['v_limit']}V], Current: {res['amps']}A [{res['a_limit']}A]" + # not every programmable PSU can measure power directly + if res.get("watts") is not None: + line += f", Power: {res['watts']}W" + print(line) def digital_io(self): place = self.get_acquired_place() @@ -2006,12 +2005,8 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg subparser.add_argument( "-t", "--delay", type=float, default=None, help="wait time in seconds between off and on during cycle" ) - subparser.add_argument( - "-v", "--voltage", type=float, default=None, help="voltage value to be configured" - ) - subparser.add_argument( - "-a", "--amps", type=float, default=None, help="amps value to be configured" - ) + subparser.add_argument("-v", "--voltage", type=float, default=None, help="voltage value to be configured") + subparser.add_argument("-a", "--amps", type=float, default=None, help="amps value to be configured") subparser.add_argument("--name", "-n", help="optional resource name") subparser.set_defaults(func=ClientSession.power) diff --git a/tests/test_powerdriver.py b/tests/test_powerdriver.py index 3486fdd04..8edc02dfe 100644 --- a/tests/test_powerdriver.py +++ b/tests/test_powerdriver.py @@ -368,6 +368,40 @@ def test_siglent_show(self, target, mocker): "a_limit": 1.5, } + @pytest.mark.parametrize("failure", ["unanswered", "non-numeric"]) + def test_siglent_show_without_power_query(self, target, mocker, failure): + pytest.importorskip("pyvisa") + from pyvisa import constants, errors + + # Not every SPD model implements MEAS:POWE?. Such a model leaves the + # query unanswered, so the read times out; report no power rather than + # losing the other four readings with it. + responses = { + "MEAS:VOLT? CH1": "5.001", + "MEAS:CURR? CH1": "0.209", + "CH1:VOLT?": "5.0", + "CH1:CURR?": "1.5", + } + + def query(cmd): + if cmd in responses: + return responses[cmd] + if failure == "unanswered": + raise errors.VisaIOError(constants.StatusCode.error_timeout) + return "not a number" + + psu = self._mock_siglent_psu(mocker) + psu.query.side_effect = query + d = self._activate_siglent_driver(target) + + assert d.show() == { + "voltage": 5.001, + "amps": 0.209, + "watts": None, + "v_limit": 5.0, + "a_limit": 1.5, + } + def test_siglent_voltage_amps(self, target, mocker): pytest.importorskip("pyvisa")