Source code for hvl_ccb.dev.fug.fug

#  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)