"""Device driver for AimTTI PL series power supplies."""
import hashlib
import json
from types import SimpleNamespace
from herosdevices.core import DeviceCommandQuantity
from herosdevices.core.templates import SerialDeviceTemplate as SerialDevice
from herosdevices.helper import limits, mark_driver
from herosdevices.interfaces.atomiq import CurrentSource, Switch, VoltageSource
[docs]
@mark_driver(
info="Generic Power Supply",
product_page="https://www.aimtti.com/product-category/dc-power-supplies",
state="beta",
)
class GenericPSU(SerialDevice):
"""Device driver for a generic network capable AimTTI Power Supplies.
Args:
address: IP address of the device. Example: "192.168.1.20"
port: Socket port of the device. Default (9221) should be correct for all PSUs.
timeout: timeout for read operations.
"""
def __init__(self, address: str, port: int = 9221, timeout: float = 1.0) -> None:
SerialDevice.__init__(
self,
f"socket://{address}:{port}",
baudrate=9600,
timeout=timeout,
read_line_termination=b"\r",
write_line_termination=b"\r",
)
[docs]
def enable_all_outputs(self, enable: bool = True) -> None:
"""Enable or disable all outputs simultaneously.
Args:
enable: True to enable all outputs, False to disable all outputs.
"""
value = 1 if enable else 0
self.connection.write(f"OPALL {value}")
[docs]
def local(self) -> None:
"""Switch the device to local control mode."""
self.connection.write("LOCAL")
[docs]
def read(self, *args, **kwargs) -> str | None:
"""Read data from the device connection.
Used for access from the channel classes like :py:class:`PLSeriesChannel`. Should not be used directly.
"""
return self.connection.read(*args, **kwargs)
[docs]
def write(self, *args, **kwargs) -> str | None:
"""Write data to the device connection.
Used for access from the channel classes like :py:class:`PLSeriesChannel`. Should not be used directly.
"""
return self.connection.write(*args, **kwargs)
[docs]
@mark_driver(
name="PL (and more) Series Channel",
info="Single Channel Representation of a PL Series Power Supply (also works with PLH and possibly others)",
product_page="https://www.aimtti.com/product-category/dc-power-supplies/aim-plseries",
state="beta",
)
class PLSeriesChannel(VoltageSource, CurrentSource, Switch):
"""A single channel of a PL Series power supply.
This class provides an :py:class:`herosdevices.interfaces.atomiq.VoltageSource` compatible interface to control a
single channel of a PL Series power supply.
Note:
This class does not directly connect to the hardware but to another object given by the host_device argument
which can also be a HERO running on another machine. It can be used to provide a universal interface which does
not require setting a channel for every operation.
Args:
host_device: The host :py:class:`GenericPSU` device.
voltage_limits: Voltage limits of the channel. Can be used to set artificial guard rails.
current_limits: Current limits of the channel. Can be used to set artificial guard rails.
channel: The channel to control (1, 2, or 3)
"""
def __new__(
cls, voltage_limits: tuple[float, float], current_limits: tuple[float, float], channel: int = 1, **_kwargs
) -> "PLSeriesChannel":
"""Return a channel class with custom class attributes."""
if channel == 0:
raise ValueError("Channel numbering for PLSeriesChannel starts at 1, 0 is not valid.")
namespace = {
"voltage_set": DeviceCommandQuantity(
command_set=f"V{channel} {{}}",
command_get=f"V{channel}?",
dtype=float,
value_check_fun=limits(*voltage_limits),
unit="V",
format_fun=lambda x: x.rstrip("V\r").lstrip(f"V{channel} "),
),
"current_set": DeviceCommandQuantity(
command_set=f"I{channel} {{}}",
command_get=f"I{channel}?",
dtype=float,
value_check_fun=limits(*current_limits),
unit="A",
format_fun=lambda x: x.rstrip("A\r").lstrip(f"I{channel}"),
),
"voltage_act": DeviceCommandQuantity(
command_set=None, # Read-only
command_get=f"V{channel}O?",
dtype=float,
unit="V",
format_fun=lambda x: x.rstrip("V\r"),
),
"current_act": DeviceCommandQuantity(
command_set=None, # Read-only
command_get=f"I{channel}O?",
dtype=float,
unit="A",
format_fun=lambda x: x.rstrip("A\r"),
),
"output_enabled": DeviceCommandQuantity(
command_set=f"OP{channel} {{}}",
command_get=f"OP{channel}?",
dtype=bool,
format_fun=lambda x: "1" in x,
),
}
signature = json.dumps(
{"channel": channel, "current_limits": current_limits, "voltage_limits": voltage_limits},
sort_keys=True,
default=str,
)
cls_hash = hashlib.blake2s(signature.encode()).hexdigest()
return super().__new__(type(f"{cls.__name__}_{cls_hash}", (cls,), namespace))
def __init__(
self,
host_device: GenericPSU,
voltage_limits: tuple[float, float],
current_limits: tuple[float, float],
channel: int = 1,
**_kwargs,
) -> None:
super().__init__()
self.connection = SimpleNamespace()
self.connection.write = host_device.write
self.connection.read = host_device.read
self.channel: int = channel
self.min_current, self.max_current = float(current_limits[0]), float(current_limits[1])
self.min_voltage, self.max_voltage = float(voltage_limits[0]), float(voltage_limits[1])
def _set_voltage(self, value: float) -> None:
"""Set the voltage for this channel.
Args:
value: The voltage value to set in volts.
"""
self.voltage_set = value
def _set_current(self, value: float) -> None:
"""Set the current for this channel.
Args:
value: The current value to set in amps.
"""
self.current_set = value
[docs]
def on(self) -> None:
"""Enable the output for this channel."""
self.output_enabled = True
[docs]
def off(self) -> None:
"""Disable the output for this channel."""
self.output_enabled = False
[docs]
def is_on(self) -> bool:
"""Check if the output for this channel is enabled."""
return self.output_enabled