Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,12 @@ Currently available are:
for details.

``siglent``
Controls *Siglent SPD3000X* series modules via the `vxi11 Python module
<https://pypi.org/project/python-vxi11/>`_.
Controls *Siglent SPD3000X* series modules via the `PyVISA Python module
<https://pypi.org/project/PyVISA/>`_ (with the ``pyvisa-py`` backend).
In addition to switching the output on and off, this backend supports
reading and configuring the per-channel voltage and current limits and
reading the measured voltage, current and power (see the ``show`` and
``config`` actions of ``labgrid-client power``).

``simplerest``
This is a generic backend for PDU implementations which can be controlled via
Expand Down Expand Up @@ -2424,6 +2428,13 @@ Implements:
Arguments:
- delay (float, default=2.0): delay in seconds between off and on

Some programmable power supplies (currently the ``siglent`` backend) additionally
implement :any:`ProgrammablePowerProtocol`, which allows reading the measured
voltage, current and power and configuring the voltage and current limits. These
are exposed via the ``show`` and ``config`` actions of ``labgrid-client power``
(see :ref:`usage-power` for details). Backends that do not implement this
protocol simply do not provide those actions.

PDUDaemonDriver
~~~~~~~~~~~~~~~
A :any:`PDUDaemonDriver` controls a `PDUDaemonPort`_, allowing control of the
Expand Down
18 changes: 18 additions & 0 deletions doc/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,24 @@ The minimum requirement is a call to :code:`super().__attrs_post_init__()`.
All that's left now is to implement the functionality described by the used
protocol, by using the API of the bound drivers and resources.

Programmable Power Supplies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Simple power drivers only implement :any:`PowerProtocol` (``on``, ``off``,
``cycle`` and ``get``). Power supplies that can additionally report and
configure their output implement :any:`ProgrammablePowerProtocol`, which
extends :any:`PowerProtocol` with three methods:

- ``show()`` returns a dict with the measured ``voltage``, ``amps`` and
``watts`` as well as the configured ``v_limit`` and ``a_limit``.
- ``voltage(value)`` sets the output voltage.
- ``amps(value)`` sets the output current limit.

For the :any:`NetworkPowerDriver`, these are dispatched to the configured
backend module, which has to provide matching ``power_show``,
``power_voltage`` and ``power_amps`` functions (see
``labgrid/driver/power/siglent.py`` for a reference implementation).

Writing a Resource
-------------------

Expand Down
38 changes: 38 additions & 0 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,44 @@ allocated before returning.
A reservation will time out after a short time, if it is neither refreshed nor
used by locked places.

.. _usage-power:

Controlling Power
~~~~~~~~~~~~~~~~~

The ``labgrid-client power`` command (short alias ``pw``) switches a place's
power on and off:

.. code-block:: bash

$ labgrid-client -p my-place power on
$ labgrid-client -p my-place power off
$ labgrid-client -p my-place power cycle
$ labgrid-client -p my-place power get

For programmable power supplies (currently the ``siglent`` backend of
:any:`NetworkPowerPort`), two additional actions are available.

``power config`` sets the voltage and/or current limit:

.. code-block:: bash

$ labgrid-client -p my-place power config --voltage 5.0 --amps 1.5
set voltage to 5.0V
set amps to 1.5A

``power show`` reads back the measured voltage, current and power together with
the configured limits (shown in square brackets):

.. code-block:: bash

$ labgrid-client -p my-place power show
Voltage: 5.001V [5.0V], Current: 0.209A [1.5A], Power: 1.045W

The power value is read directly from the device rather than being computed from
voltage times current, so it stays accurate even though those two values are
sampled at slightly different times.

Library
-------
labgrid can be used directly as a Python library, without the infrastructure
Expand Down
80 changes: 61 additions & 19 deletions labgrid/driver/power/siglent.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,77 @@
"""tested with Siglent SPD3303X-E, and should be compatible with all SPD3000X series modules"""
""" tested with Siglent SPD3303X-E, SPD1168X and should be compatible with all SPD3000X series modules """

import warnings

import vxi11
import pyvisa

def _get_psu(host):
"""Helper to initialize the raw network socket resource session via PyVISA."""
rm = pyvisa.ResourceManager("@py")
resource_string = f"TCPIP0::{host}::5025::SOCKET"
psu = rm.open_resource(resource_string)
# Siglent scopes and PSUs require explicit newline termination on raw sockets
psu.read_termination = "\n"
psu.write_termination = "\n"
psu.timeout = 5000 # 5 second safety timeout
return psu

def power_set(host, port, index, value):
warnings.warn(
"siglent power backend uses vxi11 module using deprecated xdrlib module, see https://github.com/labgrid-project/labgrid/issues/1507",
DeprecationWarning,
)

assert port is None
index = int(index)
assert 1 <= index <= 2
value = "ON" if value else "OFF"
psu = vxi11.Instrument(host)
psu.write(f"OUTPUT CH{index},{value}")

with _get_psu(host) as psu:
psu.write(f"OUTPUT CH{index},{value}")

def power_get(host, port, index):
warnings.warn(
"siglent power backend uses vxi11 module using deprecated xdrlib module, see https://github.com/labgrid-project/labgrid/issues/1507",
DeprecationWarning,
)

assert port is None
index = int(index)
assert 1 <= index <= 2
psu = vxi11.Instrument(host)
state = psu.ask("SYSTEM:STATUS?")
with _get_psu(host) as psu:
state = psu.query("SYSTEM:STATUS?")
state = int(state, 16)
bitmask = 1 << (index + 3)
return bool(state & bitmask)

def power_show(host, port, index):
assert port is None
index = int(index)
assert 1 <= index <= 2
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}")
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),
"v_limit": float(v_set),
"a_limit": float(a_set)
}

def power_watts(host, port, index):
"""Read the power (in watts) measured by the device on the given channel.

The PSU reports true power directly, so this avoids errors from
multiplying voltage and current sampled at slightly different times.
"""
assert port is None
index = int(index)
assert 1 <= index <= 2
with _get_psu(host) as psu:
w_measured = psu.query(f"MEAS:POWE? CH{index}")
return float(w_measured)

def power_voltage(host, port, index, voltage):
assert port is None
index = int(index)
assert 1 <= index <= 2
with _get_psu(host) as psu:
psu.write(f"CH{index}:VOLT {voltage}")

def power_amps(host, port, index, amps):
assert port is None
index = int(index)
with _get_psu(host) as psu:
psu.write(f"CH{index}:CURR {amps}")

17 changes: 16 additions & 1 deletion labgrid/driver/powerdriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ..exceptions import InvalidConfigError
from ..factory import target_factory
from ..protocol import PowerProtocol, DigitalOutputProtocol, ResetProtocol
from ..protocol import PowerProtocol, DigitalOutputProtocol, ResetProtocol, ProgrammablePowerProtocol
from ..resource import NetworkPowerPort
from ..step import step
from ..util.proxy import proxymanager
Expand Down Expand Up @@ -233,6 +233,21 @@ def cycle(self):
def get(self):
return self.backend.power_get(self._host, self._port, self.port.index)

@Driver.check_active
@step()
def show(self):
return self.backend.power_show(self._host, self._port, self.port.index)

@Driver.check_active

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could power (W) also be added? Some PowerSupplies (also Siglent) do allow this.
Reading Voltage and Current and multiply it in software will introduce and error because the time difference between the two reads.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vermaete : I'll try to have a look into it this weekend and push something, after that I'm travelling for around four weeks. So, in general "amps" -> "current" and "power" in [W]. Let me know if something else is missing. I have to have a look into our siglent material here, let's see. ty

@Bastian-Krause : There seems to be some interest in this. Pls, what should I additionally need to provide as test and documentation here? Can you point me to something similar/driver you'd like to see? If not, I might figure out and come up with anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rubusch Thomas-Vreys did some work on your fork and has created a PR with some of the remarks solved.
Thomas will be available at Wednesday to continue with it, if needed.
We do have some power-supplies to test this.
Thanks for the useful Labgrid feature.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vermaete I merged this. I had a look at my hardware, unfortunately I cannot verify power readings. So, I appreaciate the effort. Thanks.

I still agree in "current" being more suitable than "amps". Now, I did not like to change before merging and currently busy/travelling. Shall I still add a commit to rename it, or can we live with that as is? I think to user interface it exposes "current".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for merging. Naming as it is now is fine for me.

@step(args=['voltage'])
def voltage(self, voltage):
return self.backend.power_voltage(self._host, self._port, self.port.index, voltage)

@Driver.check_active
@step(args=['amps'])
def amps(self, amps):
return self.backend.power_amps(self._host, self._port, self.port.index, amps)

@target_factory.reg_driver
@attr.s(eq=False)
class DigitalOutputPowerDriver(Driver, PowerResetMixin, PowerProtocol):
Expand Down
2 changes: 1 addition & 1 deletion labgrid/protocol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .commandprotocol import CommandProtocol
from .consoleprotocol import ConsoleProtocol
from .linuxbootprotocol import LinuxBootProtocol
from .powerprotocol import PowerProtocol
from .powerprotocol import PowerProtocol, ProgrammablePowerProtocol
from .filetransferprotocol import FileTransferProtocol
from .infoprotocol import InfoProtocol
from .digitaloutputprotocol import DigitalOutputProtocol
Expand Down
14 changes: 14 additions & 0 deletions labgrid/protocol/powerprotocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ def off(self):
@abc.abstractmethod
def cycle(self):
raise NotImplementedError


class ProgrammablePowerProtocol(PowerProtocol):
@abc.abstractmethod
def show(self, index):
raise NotImplementedError

@abc.abstractmethod
def voltage(self, index, voltage):
raise NotImplementedError

@abc.abstractmethod
def amps(self, index, amps):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 'current' not more suiteable i.s.o. amps?

raise NotImplementedError
28 changes: 27 additions & 1 deletion labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,9 +971,29 @@ def power(self):
raise UserError("target has no compatible resource available")
if delay is not None:
drv.delay = delay

if action == "config":
if self.args.voltage is None and self.args.amps is None:
raise UserError("Option config requires --voltage <float> and/or --amps <float>")

if self.args.voltage is not None:
drv.voltage(self.args.voltage)
print(f"set voltage to {self.args.voltage}V")
if self.args.amps is not None:
drv.amps(self.args.amps)
print(f"set amps to {self.args.amps}A")
return

res = getattr(drv, action)()
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"
)


def digital_io(self):
place = self.get_acquired_place()
Expand Down Expand Up @@ -1982,10 +2002,16 @@ def get_parser(auto_doc_mode=False) -> "argparse.ArgumentParser | AutoProgramArg
subparser.set_defaults(func=ClientSession.print_env)

subparser = subparsers.add_parser("power", aliases=("pw",), help="change (or get) a place's power status")
subparser.add_argument("action", choices=["on", "off", "cycle", "get"])
subparser.add_argument("action", choices=["on", "off", "cycle", "get", "show", "config"])
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("--name", "-n", help="optional resource name")
subparser.set_defaults(func=ClientSession.power)

Expand Down
13 changes: 12 additions & 1 deletion man/labgrid-client.1
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,8 @@ change (or get) a place\(aqs power status
.INDENT 3.5
.sp
.EX
usage: labgrid\-client power|pw [\-t DELAY] [\-\-name NAME] {on,off,cycle,get}
usage: labgrid\-client power|pw [\-t DELAY] [\-v VOLTAGE] [\-a AMPS] [\-\-name NAME]
{on,off,cycle,get,show,config}
.EE
.UNINDENT
.UNINDENT
Expand All @@ -571,6 +572,16 @@ wait time in seconds between off and on during cycle
.UNINDENT
.INDENT 0.0
.TP
.B \-v <voltage>, \-\-voltage <voltage>
voltage value to be configured
.UNINDENT
.INDENT 0.0
.TP
.B \-a <amps>, \-\-amps <amps>
amps value to be configured
.UNINDENT
.INDENT 0.0
.TP
.B \-\-name <name>, \-n <name>
optional resource name
.UNINDENT
Expand Down
Loading