# Copyright (c) ETH Zurich, SIS ID and HVL D-ITET
#
"""
Device classes for "Probus V - ADDAT30" Interfaces which are used to control power
supplies from FuG Elektronik GmbH
This interface is used for many FuG power units.
Manufacturer homepage:
https://www.fug-elektronik.de
The Professional Series of Power Supplies from FuG is a series of low, medium and high
voltage direct current power supplies as well as capacitor chargers.
The class FuG is tested with a HCK 800-20 000 in Standard Mode.
The addressable mode is not implemented.
Check the code carefully before using it with other devices.
Manufacturer homepage:
https://www.fug-elektronik.de/netzgeraete/professional-series/
The documentation of the interface from the manufacturer can be found here:
https://www.fug-elektronik.de/wp-content/uploads/download/de/SOFTWARE/Probus_V.zip
The provided classes support the basic and some advanced commands.
The commands for calibrating the power supplies are not implemented, as they are only
for very special porpoises and
should not used by "normal" customers.
"""
import logging
import re
from abc import ABC, abstractmethod
from enum import IntEnum
from typing import Optional, Union, cast
from hvl_ccb.comm.serial import (
SerialCommunication,
SerialCommunicationBytesize,
SerialCommunicationConfig,
SerialCommunicationParity,
SerialCommunicationStopbits,
)
from hvl_ccb.configuration import configdataclass
from hvl_ccb.dev.base import DeviceError, SingleCommDevice
from hvl_ccb.utils.enum import NameEnum
from hvl_ccb.utils.typing import Number
logger = logging.getLogger(__name__)
[docs]
class FuGErrorcodes(NameEnum, init="description possible_reason"): # type:ignore
"""
The power supply can return an errorcode. These errorcodes are handled by this
class. The original errorcodes from the source are with one or two digits,
see documentation of Probus V chapter 5.
All three-digit errorcodes are from this python module.
"""
E0 = "no error", "standard response on each command"
E1 = (
"no data available",
(
"Customer tried to read from GPIB but there were no data prepared. "
"(IBIG50 sent command ~T2 to ADDA)"
),
)
E2 = "unknown register type", "No valid register type after '>'"
E4 = (
"invalid argument",
"The argument of the command was rejected .i.e. malformed number",
)
E5 = "argument out of range", "i.e. setvalue higher than type value"
E6 = (
"register is read only",
"Some registers can only be read but not written to. (i.e. monitor registers)",
)
E7 = "Receive Overflow", "Command string was longer than 50 characters."
E8 = (
"EEPROM is write protected",
(
"Write attempt to calibration data while the write protection switch was"
" set to write protected."
),
)
E9 = (
"address error",
(
"A non addressed command was sent to ADDA while it was in addressable mode "
"(and vice versa)."
),
)
E10 = "unknown SCPI command", "This SCPI command is not implemented"
E11 = (
"not allowed Trigger-on-Talk",
(
"Not allowed attempt to Trigger-on-Talk (~T1) while ADDA was in addressable"
" mode."
),
)
E12 = "invalid argument in ~Tn command", "Only ~T1 and ~T2 is implemented."
E13 = (
"invalid N-value",
(
"Register > K8 contained an invalid value. Error code is output on an"
" attempt to query data with ? or ~T1"
),
)
E14 = "register is write only", "Some registers can only be writte to (i.e.> H0)"
E15 = "string too long", "i.e.serial number string too long during calibration"
E16 = (
"wrong checksum",
(
"checksum over command string was not correct, refer also to 4.4 of the "
"Probus V documentation"
),
)
E100 = (
"Command is not implemented",
"You tried to execute a command, which is not implemented or does not exist",
)
E106 = (
"The rampstate is a read-only register",
(
"You tried to write data to the register, which can only give "
"you the status of the ramping."
),
)
E206 = (
"This status register is read-only",
(
"You tried to write data to this "
"register, which can only give you "
"the actual status of the "
"corresponding digital output."
),
)
E306 = (
"The monitor register is read-only",
"You tried to write data to a monitor, which can only give you measured data.",
)
E115 = (
"The given index to select a digital value is out of range",
"Only integer values between 0 and 1 are allowed.",
)
E125 = (
"The given index to select a ramp mode is out of range",
"Only integer values between 0 and 4 are allowed.",
)
E135 = (
"The given index to select the readback channel is out of range",
"Only integer values between 0 and 6 are allowed.",
)
E145 = (
"The given value for the AD-conversion is unknown",
'Valid values for the ad-conversion are integer values from "0" to "7".',
)
E155 = (
"The given value to select a polarity is out range.",
"The value should be 0 or 1.",
)
E165 = "The given index to select the terminator string is out of range", ""
E504 = "Empty string as response", "The connection is broken."
E505 = (
"The returned register is not the requested.",
"Maybe the connection is overburden.",
)
E666 = (
(
"You cannot overwrite the most recent error in the interface of the power "
"supply. But, well: You created an error anyway..."
),
"",
)
[docs]
def raise_(self):
if self is FuGErrorcodes.E0:
logger.debug('Communication with FuG successful, errorcode "E0" received.')
return
logger.debug(f"A FuGError with the errorcode {self.name} was detected.")
raise FuGError(
f"{self.description}. Possible reason: {self.possible_reason}",
errorcode=self.name,
)
[docs]
class FuGDigitalVal(IntEnum):
OFF = 0
ON = 1
YES = 1
NO = 0
[docs]
class FuGRampModes(IntEnum):
IMMEDIATELY = 0
"""Standard mode: no ramp"""
FOLLOWRAMP = 1
"""Follow the ramp up- and downwards"""
RAMPUPWARDS = 2
"""Follow the ramp only upwards, downwards immediately"""
SPECIALRAMPUPWARDS = 3
"""Follow a special ramp function only upwards"""
ONLYUPWARDSOFFTOZERO = 4
"""Follow the ramp up- and downwards, if output is OFF set value is zero"""
[docs]
class FuGReadbackChannels(IntEnum):
VOLTAGE = 0
CURRENT = 1
STATUSBYTE = 2
RATEDVOLTAGE = 3
RATEDCURRENT = 4
FIRMWARE = 5
SN = 6
[docs]
class FuGMonitorModes(IntEnum):
T256US = 0
"""14 bit + sign, 256 us integration time"""
T1MS = 1
"""15 bit + sign, 1 ms integration time"""
T4MS = 2
"""15 bit + sign, 4 ms integration time"""
T20MS = 3
"""17 bit + sign, 20 ms integration time"""
T40MS = 4
"""17 bit + sign, 40 ms integration time"""
T80MS = 5
"""typ. 18 bit + sign, 80 ms integration time"""
T200MS = 6
"""typ. 19 bit + sign, 200 ms integration time"""
T800MS = 7
"""typ. 20 bit + sign, 800 ms integration time"""
[docs]
class FuGPolarities(IntEnum):
POSITIVE = 0
NEGATIVE = 1
[docs]
class FuGTerminators(IntEnum):
CRLF = 0
LFCR = 1
LF = 2
CR = 3
[docs]
class FuGProbusIVCommands(NameEnum, init="command input_type"): # type:ignore
ID = "*IDN?", None
RESET = "=", None
OUTPUT = "F", (FuGDigitalVal, int)
VOLTAGE = "U", (int, float)
CURRENT = "I", (int, float)
READBACKCHANNEL = "N", (FuGReadbackChannels, int)
QUERY = "?", None
ADMODE = "S", (FuGMonitorModes, int)
POLARITY = "P", (FuGPolarities, int)
XOUTPUTS = "R", int
"""TODO: the possible values are limited to 0..13"""
EXECUTEONX = "G", (FuGDigitalVal, int)
"""Wait for "X" to execute pending commands"""
EXECUTE = "X", None
TERMINATOR = "Y", (FuGTerminators, int)
[docs]
@configdataclass
class FuGSerialCommunicationConfig(SerialCommunicationConfig):
#: Baudrate for FuG power supplies is 9600 baud
baudrate: int = 9600
#: FuG does not use parity
parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
#: FuG uses one stop bit
stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
#: One byte is eight bits long
bytesize: Union[int, SerialCommunicationBytesize] = (
SerialCommunicationBytesize.EIGHTBITS
)
#: The terminator is LF
terminator: bytes = b"\n"
#: use 3 seconds timeout as default
timeout: Number = 3
#: default time to wait between attempts of reading a non-empty text
wait_sec_read_text_nonempty: Number = 0.5
#: default number of attempts to read a non-empty text
default_n_attempts_read_text_nonempty: int = 10
[docs]
class FuGSerialCommunication(SerialCommunication):
"""
Specific communication protocol implementation for
FuG power supplies.
Already predefines device-specific protocol parameters in config.
"""
[docs]
@staticmethod
def config_cls():
return FuGSerialCommunicationConfig
[docs]
def query(self, command: str) -> str:
"""
Send a command to the interface and handle the status message.
Raises an error, if the answer starts with "E".
:param command: Command to send
:raises FuGError: if the connection is broken or the error from the power
source itself
:return: Answer from the interface or empty string
"""
with self.access_lock:
logger.debug(f"FuG communication, send: {command}")
self.write_text(command)
answer: Optional[str] = self.read_text_nonempty() # expects an answer
logger.debug(f"FuG communication, receive: {answer}")
if not answer:
cast(FuGErrorcodes, FuGErrorcodes.E504).raise_()
assert answer is not None
try:
FuGErrorcodes(answer).raise_()
return ""
except ValueError as e:
logger.error(str(e), exc_info=e)
if answer.startswith("E"):
raise FuGError(f'The unknown errorcode "{answer}" was detected.')
return answer
[docs]
@configdataclass
class FuGConfig:
"""
Device configuration dataclass for FuG power supplies.
"""
#: Time to wait after subsequent commands during stop (in seconds)
wait_sec_stop_commands: Number = 0.5
[docs]
def clean_values(self):
if self.wait_sec_stop_commands <= 0:
raise ValueError(
"Wait time after subsequent commands during stop must be be a "
"positive value (in seconds)."
)
[docs]
class FuGProbusIV(SingleCommDevice, ABC):
"""
FuG Probus IV device class
Sends basic SCPI commands and reads the answer.
Only the special commands and PROBUS IV instruction set is implemented.
"""
def __init__(self, com, dev_config=None):
# Call superclass constructor
super().__init__(com, dev_config)
# Version of the interface (will be retrieved after com is opened)
self._interface_version = ""
def __repr__(self):
return f"FuGProbus({self._interface_version})"
[docs]
@staticmethod
def default_com_cls():
return FuGSerialCommunication
[docs]
@staticmethod
def config_cls():
return FuGConfig
[docs]
@abstractmethod
def start(self):
logger.info("Starting device " + str(self))
super().start()
self._interface_version = self.command(FuGProbusIVCommands.ID)
logger.info(f"Connection to {self._interface_version} established.")
[docs]
def stop(self) -> None:
with self.com.access_lock:
logger.info(f"Stopping device {self}")
self.output_off()
self.reset()
super().stop()
[docs]
def command(self, command: FuGProbusIVCommands, value=None) -> str:
"""
:param command: one of the commands given within FuGProbusIVCommands
:param value: an optional value, depending on the command
:return: a String if a query was performed
"""
if not (
(value is None and command.input_type is None)
or isinstance(value, command.input_type)
):
raise FuGError(
"Wrong value for data was given. Expected: "
f"{command.input_type} and given: {value.__class__}"
)
# Differentiate between with and without optional value
if command.input_type is None:
return self.com.query(f"{command.command}")
else:
return self.com.query(f"{command.command}{value}")
# Special commands
[docs]
def reset(self) -> None:
"""
Reset of the interface:
All setvalues are set to zero
"""
self.command(FuGProbusIVCommands.RESET) # type: ignore
[docs]
def output_off(self) -> None:
"""
Switch DC voltage output off.
"""
self.command(
FuGProbusIVCommands.OUTPUT, # type: ignore
FuGDigitalVal.OFF,
)
[docs]
class FuGProbusV(FuGProbusIV):
"""
FuG Probus V class which uses register based commands to control the power supplies
"""
def __init__(self, com, dev_config=None):
# Call superclass constructor
super().__init__(com, dev_config)
# Version of the interface (will be retrieved after com is opened)
# self._interface_version = ""
[docs]
def set_register(self, register: str, value: Union[Number, str]) -> None:
"""
generic method to set value to register
:param register: the name of the register to set the value
:param value: which should be written to the register
"""
self.com.query(f">{register} {value}")
[docs]
def get_register(self, register: str) -> str:
"""
get the value from a register
:param register: the register from which the value is requested
:returns: the value of the register as a String
"""
answer = self.com.query(f">{register} ?").split(":")
if not answer[0] == register:
cast(FuGErrorcodes, FuGErrorcodes.E505).raise_()
return answer[1]
[docs]
class FuGProbusVRegisterGroups(NameEnum):
SETVOLTAGE = "S0"
SETCURRENT = "S1"
OUTPUTX0 = "B0"
OUTPUTX1 = "B1"
OUTPUTX2 = "B2"
OUTPUTXCMD = "BX"
OUTPUTONCMD = "BON"
MONITOR_V = "M0"
MONITOR_I = "M1"
INPUT = "D"
CONFIG = "K"
def _check_for_value_limits(value, max_limit) -> bool:
if value > max_limit:
raise FuGError(
f"The requested value of {value} exceeds the maximal"
f"technically permissible value of {max_limit}."
)
elif value < 0:
raise ValueError("The value must be positive.")
return True
[docs]
class FuGProbusVSetRegisters:
"""
Setvalue control acc. 4.2.1 for the voltage and the current output
"""
def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
self._fug = fug
_super_register = super_register.value
self._setvalue: str = _super_register
self.__max_setvalue: float = 0
self._actualsetvalue: str = _super_register + "A"
self._ramprate: str = _super_register + "R"
self._rampmode: str = _super_register + "B"
self._rampstate: str = _super_register + "S"
self._high_resolution: str = _super_register + "H"
@property
def _max_setvalue(self) -> float:
return self.__max_setvalue
@_max_setvalue.setter
def _max_setvalue(self, value: Number):
self.__max_setvalue = float(value)
@property
def setvalue(self) -> float:
"""
For the voltage or current output this setvalue was programmed.
:return: the programmed setvalue
"""
return float(self._fug.get_register(self._setvalue))
@setvalue.setter
def setvalue(self, value: Number):
"""
This sets the value for the voltage or current output
:param value: value in V or A
"""
_check_for_value_limits(value, self._max_setvalue)
self._fug.set_register(self._setvalue, value)
@property
def actualsetvalue(self) -> float:
"""
The actual valid set value, which depends on the ramp function.
:return: actual valid set value
"""
return float(self._fug.get_register(self._actualsetvalue))
@actualsetvalue.setter
def actualsetvalue(self, value: Number):
_check_for_value_limits(value, self._max_setvalue)
self._fug.set_register(self._actualsetvalue, value)
@property
def ramprate(self) -> float:
"""
The set ramp rate in V/s.
:return: ramp rate in V/s
"""
return float(self._fug.get_register(self._ramprate))
@ramprate.setter
def ramprate(self, value: Number):
"""
The ramp rate can be set in V/s.
:param value: ramp rate in V/s
"""
self._fug.set_register(self._ramprate, value)
@property
def rampmode(self) -> FuGRampModes:
"""
The set ramp mode to control the setvalue.
:return: the mode of the ramp as instance of FuGRampModes
"""
return FuGRampModes(int(self._fug.get_register(self._rampmode)))
@rampmode.setter
def rampmode(self, value: Union[int, FuGRampModes]):
"""
Sets the ramp mode.
:param value: index for the ramp mode from FuGRampModes
:raise FuGError: if a wrong ramp mode is chosen
"""
try:
self._fug.set_register(self._rampmode, FuGRampModes(value))
except ValueError as e:
logger.error(str(e), exc_info=e)
cast(FuGErrorcodes, FuGErrorcodes.E125).raise_()
@property
def rampstate(self) -> FuGDigitalVal:
"""
Status of ramp function.
:return 0: if final setvalue is reached
:return 1: if still ramping up
"""
return FuGDigitalVal(int(self._fug.get_register(self._rampstate)))
@rampstate.setter
def rampstate(self, _):
"""
The rampstate is only an output. Writing data to this register will raise an
error
:raise FuGError: if something is written to this attribute
"""
cast(FuGErrorcodes, FuGErrorcodes.E106).raise_()
@property
def high_resolution(self) -> FuGDigitalVal:
"""
Status of the high resolution mode of the output.
:return 0: normal operation
:return 1: High Res. Mode
"""
return FuGDigitalVal(int(self._fug.get_register(self._high_resolution)))
@high_resolution.setter
def high_resolution(self, value: Union[int, FuGDigitalVal]):
"""
Enables/disables the high resolution mode of the output.
:param value: FuGDigitalVal
:raise FuGError: if not a FuGDigitalVal is given
"""
try:
if FuGDigitalVal(value) is FuGDigitalVal.ON:
self._fug.set_register(self._high_resolution, FuGDigitalVal.ON)
else:
self._fug.set_register(self._high_resolution, FuGDigitalVal.OFF)
except ValueError as e:
logger.error(str(e), exc_info=e)
cast(FuGErrorcodes, FuGErrorcodes.E115).raise_()
[docs]
class FuGProbusVDORegisters:
"""
Digital outputs acc. 4.2.2
"""
def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
self._fug = fug
_super_register = super_register.value
self._out = _super_register
self._status = _super_register + "A"
@property
def out(self) -> Union[int, FuGDigitalVal]:
"""
Status of the output according to the last setting. This can differ from the
actual state if output should only pulse.
:return: FuGDigitalVal
"""
return FuGDigitalVal(int(self._fug.get_register(self._out)))
@out.setter
def out(self, value: Union[int, FuGDigitalVal]):
"""
Set the output ON or OFF. If pulsing is enabled, it only pulses.
:param value: FuGDigitalVal
:raise FuGError: if a non FuGDigitalVal is given
"""
try:
if FuGDigitalVal(value) is FuGDigitalVal.ON:
self._fug.set_register(self._out, FuGDigitalVal.ON)
else:
self._fug.set_register(self._out, FuGDigitalVal.OFF)
except ValueError as e:
logger.error(str(e), exc_info=e)
FuGErrorcodes("E115").raise_()
@property
def status(self) -> FuGDigitalVal:
"""
Returns the actual value of output. This can differ from the set value if
pulse function is used.
:return: FuGDigitalVal
"""
return FuGDigitalVal(int(self._fug.get_register(self._status)))
@status.setter
def status(self, _):
"""
The status is only an output. Writing data to this register will raise an
error
:raise FuGError: read only
"""
FuGErrorcodes("E206").raise_()
[docs]
class FuGProbusVMonitorRegisters:
"""
Analog monitors acc. 4.2.3
"""
def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
self._fug = fug
_super_register = super_register.value
self._value = _super_register
self._value_raw = _super_register + "R"
self._adc_mode = _super_register + "I"
@property
def value(self) -> float:
"""
Value from the monitor.
:return: a float value in V or A
"""
return float(self._fug.get_register(self._value))
@value.setter
def value(self, _):
"""
Monitor is read-only!
:raise FuGError: read-only
"""
FuGErrorcodes("E306").raise_()
@property
def value_raw(self) -> float:
"""
uncalibrated raw value from AD converter
:return: float value from ADC
"""
return float(self._fug.get_register(self._value_raw))
@value_raw.setter
def value_raw(self, _):
"""
Monitor is read-only!
:raise FuGError: read-only
"""
FuGErrorcodes("E306").raise_()
@property
def adc_mode(self) -> FuGMonitorModes:
"""
The programmed resolution and integration time of the AD converter
:return: FuGMonitorModes
"""
return FuGMonitorModes(int(self._fug.get_register(self._adc_mode)))
@adc_mode.setter
def adc_mode(self, value: Union[int, FuGMonitorModes]):
"""
Sets the resolution and integration time of the AD converter with the given
settings in FuGMonitorModes.
:param value: index of the monitor mode from FuGMonitorModes
:raise FuGError: if index is not in FuGMonitorModes
"""
try:
self._fug.set_register(self._adc_mode, FuGMonitorModes(value))
except ValueError as e:
logger.error(str(e), exc_info=e)
raise FuGErrorcodes("E145").raise_()
[docs]
class FuGProbusVDIRegisters:
"""
Digital Inputs acc. 4.2.4
"""
def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
self._fug = fug
_super_register = super_register.value
self._cv_mode = _super_register + "VR"
self._cc_mode = _super_register + "IR"
self._reg_3 = _super_register + "3R"
self._x_stat = _super_register + "X"
self._on = _super_register + "ON"
self._digital_control = _super_register + "SD"
self._analog_control = _super_register + "SA"
self._calibration_mode = _super_register + "CAL"
@property
def cv_mode(self) -> FuGDigitalVal:
"""
:return: shows 1 if power supply is in CV mode
"""
return FuGDigitalVal(int(self._fug.get_register(self._cv_mode)))
@property
def cc_mode(self) -> FuGDigitalVal:
"""
:return: shows 1 if power supply is in CC mode
"""
return FuGDigitalVal(int(self._fug.get_register(self._cc_mode)))
@property
def reg_3(self) -> FuGDigitalVal:
"""
For special applications.
:return: input from bit 3-REG
"""
return FuGDigitalVal(int(self._fug.get_register(self._reg_3)))
@property
def x_stat(self) -> FuGPolarities:
"""
:return: polarity of HVPS with polarity reversal
"""
return FuGPolarities(int(self._fug.get_register(self._x_stat)))
@property
def on(self) -> FuGDigitalVal:
"""
:return: shows 1 if power supply ON
"""
return FuGDigitalVal(int(self._fug.get_register(self._on)))
@property
def digital_control(self) -> FuGDigitalVal:
"""
:return: shows 1 if power supply is digitally controlled
"""
return FuGDigitalVal(int(self._fug.get_register(self._digital_control)))
@property
def analog_control(self) -> FuGDigitalVal:
"""
:return: shows 1 if power supply is controlled by the analog interface
"""
return FuGDigitalVal(int(self._fug.get_register(self._analog_control)))
@property
def calibration_mode(self) -> FuGDigitalVal:
"""
:return: shows 1 if power supply is in calibration mode
"""
return FuGDigitalVal(int(self._fug.get_register(self._calibration_mode)))
[docs]
class FuGProbusVConfigRegisters:
"""
Configuration and Status values, acc. 4.2.5
"""
def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
self._fug = fug
_super_register = super_register.value
self._terminator = _super_register + "T"
self._status = _super_register + "S"
self._srq_status = _super_register + "QS"
self._srq_mask = _super_register + "QM"
self._execute_on_x = _super_register + "X"
self._readback_data = _super_register + "N"
self._most_recent_error = _super_register + "E"
@property
def terminator(self) -> FuGTerminators:
"""
Terminator character for answer strings from ADDA
:return: FuGTerminators
"""
return FuGTerminators(int(self._fug.get_register(self._terminator)))
@terminator.setter
def terminator(self, value: Union[int, FuGTerminators]):
"""
Sets the terminator character for answer string from ADDA
:param value: index from FuGTerminators
:raise FuGError: if index is not in FuGTerminators
"""
try:
self._fug.set_register(self._terminator, FuGTerminators(value))
except ValueError as e:
logger.error(str(e), exc_info=e)
FuGErrorcodes("E165").raise_()
@property
def status(self) -> str:
"""
Statusbyte as a string of 0/1. Combined status (compatibel to Probus IV),
MSB first:
Bit 7: I-REG
Bit 6: V-REG
Bit 5: ON-Status
Bit 4: 3-Reg
Bit 3: X-Stat (polarity)
Bit 2: Cal-Mode
Bit 1: unused
Bit 0: SEL-D
:return: string of 0/1
"""
return self._fug.get_register(self._status)
@status.setter
def status(self, _):
"""
Stautsbyte is read-only
:raise FuGError: read-only
"""
FuGErrorcodes("E206").raise_()
@property
def srq_status(self) -> str:
"""
SRQ-Statusbyte output as a decimal number:
Bit 2: PS is in CC mode
Bit 1: PS is in CV mode
:return: representative string
"""
return self._fug.get_register(self._srq_status)
@srq_status.setter
def srq_status(self, _):
"""
SRQ-Statusbyte is read-only
:raise FuGError: read-only
"""
FuGErrorcodes("E206").raise_()
@property
def srq_mask(self) -> int:
"""
SRQ-Mask, Service-Request
Enable status bits for SRQ
0: no SRQ
Bit 2: SRQ on change of status to CC
Bit 1: SRQ on change to CV
:return: representative integer value
"""
return int(float(self._fug.get_register(self._srq_mask)))
@srq_mask.setter
def srq_mask(self, value: int):
"""
Sets the SRQ-Mask
:param value: representative integer value
"""
self._fug.set_register(self._srq_mask, value)
@property
def execute_on_x(self) -> FuGDigitalVal:
"""
status of Execute-on-X
:return: FuGDigitalVal of the status
"""
return FuGDigitalVal(int(self._fug.get_register(self._execute_on_x)))
@execute_on_x.setter
def execute_on_x(self, value: Union[int, FuGDigitalVal]):
"""
Enable/disable the Execute-on-X-mode
0: immediate execution
1: execution pending until X-command
:param value: FuGDigitalVal
:raise FuGError: if a non FuGDigitalVal is given
"""
try:
if FuGDigitalVal(value) is FuGDigitalVal.YES:
self._fug.set_register(self._execute_on_x, FuGDigitalVal.YES)
else:
self._fug.set_register(self._execute_on_x, FuGDigitalVal.NO)
except ValueError as e:
logger.error(str(e), exc_info=e)
FuGErrorcodes("E115").raise_()
@property
def readback_data(self) -> FuGReadbackChannels:
"""
Preselection of readout data for Trigger-on-Talk
:return: index for the readback channel
"""
return FuGReadbackChannels(int(self._fug.get_register(self._readback_data)))
@readback_data.setter
def readback_data(self, value: Union[int, FuGReadbackChannels]):
"""
Sets the readback channel according to the index given within the
FuGReadbackChannels
:param value: index of readback channel
:raise FuGError: if index in not in FuGReadbackChannels
"""
try:
self._fug.set_register(self._readback_data, FuGReadbackChannels(value))
except ValueError as e:
logger.error(str(e), exc_info=e)
FuGErrorcodes("E135").raise_()
@property
def most_recent_error(self) -> FuGErrorcodes:
"""
Reads the Error-Code of the most recent command
:return FuGError:
:raise FuGError: if code is not "E0"
"""
return FuGErrorcodes(self._fug.get_register(self._most_recent_error))
@most_recent_error.setter
def most_recent_error(self, _):
FuGErrorcodes("E666").raise_()
[docs]
class FuG(FuGProbusV):
"""
FuG power supply device class.
The power supply is controlled over a FuG ADDA Interface with the PROBUS V protocol
"""
def __init__(self, com, dev_config=None):
"""
Constructor for configuring the power supply.
:param com:
:param dev_config:
"""
# Call superclass constructor
super().__init__(com, dev_config)
self._id_string = ""
"""ID String of the device (will be retrieved after com is opened) contains
Serial number and model"""
# Serial number of the device (will be retrieved after com is opened)
self._serial_number = ""
# model class of the device (derived from serial number)
self._model = ""
# maximum output current of the hardware
self._max_current_hardware = 0
# maximum output charging power of the hardware
self._max_power_hardware = 0
# maximum output voltage of the hardware
self._max_voltage_hardware = 0
self._voltage = FuGProbusVSetRegisters(
self, FuGProbusVRegisterGroups("SETVOLTAGE")
)
self._current = FuGProbusVSetRegisters(
self, FuGProbusVRegisterGroups("SETCURRENT")
)
self._outX0 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX0"))
self._outX1 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX1"))
self._outX2 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX2"))
self._outXCMD = FuGProbusVDORegisters(
self, FuGProbusVRegisterGroups("OUTPUTXCMD")
)
self._on = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTONCMD"))
self._voltage_monitor = FuGProbusVMonitorRegisters(
self, FuGProbusVRegisterGroups("MONITOR_V")
)
self._current_monitor = FuGProbusVMonitorRegisters(
self, FuGProbusVRegisterGroups("MONITOR_I")
)
self._di = FuGProbusVDIRegisters(self, FuGProbusVRegisterGroups("INPUT"))
self._config_status = FuGProbusVConfigRegisters(
self, FuGProbusVRegisterGroups("CONFIG")
)
def __repr__(self):
return f"{self._id_string}"
@property
def max_current_hardware(self) -> Number:
"""
Returns the maximal current which could provided with the power supply
:return:
"""
return self._max_current_hardware
@property
def max_voltage_hardware(self) -> Number:
"""
Returns the maximal voltage which could provided with the power supply
:return:
"""
return self._max_voltage_hardware
@property
def max_current(self) -> Number:
"""
Returns the maximal current which could provided within the test setup
:return:
"""
return self.current._max_setvalue
@property
def max_voltage(self) -> Number:
"""
Returns the maximal voltage which could provided within the test setup
:return:
"""
return self.voltage._max_setvalue
@property
def voltage(self) -> FuGProbusVSetRegisters:
"""
Returns the registers for the voltage output
:return:
"""
return self._voltage
@voltage.setter
def voltage(self, value: Number):
"""
The output voltage can be set directly with this property.
This is the short version for "self.voltage.setvalue"
:param value: voltage in V
"""
self.voltage.setvalue = value
@property
def current(self) -> FuGProbusVSetRegisters:
"""
Returns the registers for the current output
:return:
"""
return self._current
@current.setter
def current(self, value: Number):
"""
The output current can be set directly with this property.
This is the short version for "self.current.setvalue"
:param value: Current in A
"""
self.current.setvalue = value
@property
def outX0(self) -> FuGProbusVDORegisters:
"""
Returns the registers for the digital output X0
:return: FuGProbusVDORegisters
"""
return self._outX0
@property
def outX1(self) -> FuGProbusVDORegisters:
"""
Returns the registers for the digital output X1
:return: FuGProbusVDORegisters
"""
return self._outX1
@property
def outX2(self) -> FuGProbusVDORegisters:
"""
Returns the registers for the digital output X2
:return: FuGProbusVDORegisters
"""
return self._outX2
@property
def outXCMD(self) -> FuGProbusVDORegisters:
"""
Returns the registers for the digital outputX-CMD
:return: FuGProbusVDORegisters
"""
return self._outXCMD
@property
def on(self) -> FuGProbusVDORegisters:
"""
Returns the registers for the output switch to turn the output on or off
:return: FuGProbusVDORegisters
"""
return self._on
@on.setter
def on(self, value: Union[int, FuGDigitalVal]):
"""
The output can be directly en- and disabled with this property.
It is the short version for "self.on.out"
:param value: instance of FuGDigitalVal
"""
self.on.out = value
@property
def voltage_monitor(self) -> FuGProbusVMonitorRegisters:
"""
Returns the registers for the voltage monitor.
A typically usage will be "self.voltage_monitor.value" to measure the output
voltage
:return:
"""
return self._voltage_monitor
@property
def current_monitor(self) -> FuGProbusVMonitorRegisters:
"""
Returns the registers for the current monitor.
A typically usage will be "self.current_monitor.value" to measure the output
current
:return:
"""
return self._current_monitor
@property
def di(self) -> FuGProbusVDIRegisters:
"""
Returns the registers for the digital inputs
:return: FuGProbusVDIRegisters
"""
return self._di
@property
def config_status(self) -> FuGProbusVConfigRegisters:
"""
Returns the registers for the registers with the configuration and status values
:return: FuGProbusVConfigRegisters
"""
return self._config_status
[docs]
def start(self, max_voltage=0, max_current=0) -> None:
"""
Opens the communication protocol and configures the device.
:param max_voltage: Configure here the maximal permissible voltage which is
allowed in the given experimental setup
:param max_current: Configure here the maximal permissible current which is
allowed in the given experimental setup
"""
# starting FuG Probus Interface
super().start()
self.voltage._max_setvalue = max_voltage
self.current._max_setvalue = max_current
# find out which type of source this is:
self.identify_device()
[docs]
def identify_device(self) -> None:
"""
Identify the device nominal voltage and current based on its model number.
:raises SerialCommunicationIOError: when communication port is not opened
"""
id_string = str(self.command(FuGProbusIVCommands("ID")))
# "'FUG HCK
# 800
# - 20 000
# MOD 17022-01-01'"
# regex to find the model of the device
regex_model = (
"FUG (?P<model>[A-Z]{3})"
" (?P<power>[0-9 ]+)"
" - (?P<voltage>[0-9 ]+)"
" MOD (?P<sn>[0-9-]+)"
)
result = re.search(regex_model, id_string)
if not result:
raise FuGError(
f'The device with the ID string "{id_string}" could not be recognized.'
)
self._id_string = id_string
results = result.groupdict()
self._model = results.get("model")
self._max_power_hardware = int(
results.get("power").replace(" ", "") # type: ignore
)
self._max_voltage_hardware = int(
results.get("voltage").replace(" ", "") # type: ignore
)
self._max_current_hardware = (
2 * self._max_power_hardware / self._max_voltage_hardware
)
self._serial_number = results.get("sn")
logger.info(f"Device {id_string} successfully identified:")
logger.info(f"Model class: {self._model}")
logger.info(f"Maximal voltage: {self._max_voltage_hardware} V")
logger.info(f"Maximal current: {self._max_current_hardware} A")
logger.info(f"Maximal charging power: {self._max_power_hardware} J/s")
logger.info(f"Serial number: {self._serial_number}")
# if limits for test setup were not predefined, set them to hardware limits
# or if the previous limits were to high, limit them to the hardware limits
if 0 == self.max_voltage:
self.voltage._max_setvalue = self.max_voltage_hardware
elif self.max_voltage > self.max_voltage_hardware:
logger.warning(
"FuG power source should supply up to "
f"{self.max_voltage} V, but the hardware only goes up "
f"to {self.max_voltage_hardware} V."
)
self.voltage._max_setvalue = self.max_voltage_hardware
logger.info(
"For this setup the maximal output voltage of the power "
f"supply is limited to {self.max_voltage} V."
)
if 0 == self.max_current:
self.current._max_setvalue = self.max_current_hardware
elif self.max_current > self.max_current_hardware:
logger.warning(
"FuG power source should supply up to "
f"{self.max_current} A, but the hardware only goes up "
f"to {self.max_current_hardware} A."
)
self.current._max_setvalue = self.max_current_hardware
logger.info(
"For this setup the maximal output current of the power "
f"supply is limited to {self.max_current} A."
)
[docs]
class FuGError(DeviceError):
"""
Error with the FuG voltage source.
"""
def __init__(self, *args, **kwargs) -> None:
self.errorcode: str = kwargs.pop("errorcode", "")
"""
Errorcode from the Probus, see documentation of Probus V chapter 5.
Errors with three-digit errorcodes are thrown by this python module.
"""
super().__init__(*args, **kwargs)