Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
65 changes: 46 additions & 19 deletions labgrid/driver/power/siglent.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
"""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}")
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),
"v_limit": float(v_set),
"a_limit": float(a_set)
}

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
26 changes: 25 additions & 1 deletion labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,7 @@ def power(self):
from ..resource.power import NetworkPowerPort, PDUDaemonPort
from ..resource.remote import NetworkUSBPowerPort, NetworkSiSPMPowerPort, NetworkSysfsGPIO
from ..resource import TasmotaPowerPort, NetworkYKUSHPowerPort
from ..protocol import ProgrammablePowerProtocol

drv = None
try:
Expand Down Expand Up @@ -971,9 +972,26 @@ 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":
res = drv.show()
print(f"Voltage: {res['voltage']}V [{res['v_limit']}V], Current: {res['amps']}A [{res['a_limit']}A]")


def digital_io(self):
place = self.get_acquired_place()
Expand Down Expand Up @@ -1982,10 +2000,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
Loading