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 typing import cast

from hvl_ccb.configuration import configdataclass
from hvl_ccb.dev.base import SingleCommDevice
from hvl_ccb.utils.typing import Number

from .comm import FuGSerialCommunication
from .constants import FuGDigitalVal, FuGProbusIVCommands
from .errors import FuGError, FuGErrorcodes
from .registers import (
    FuGProbusVConfigRegisters,
    FuGProbusVDIRegisters,
    FuGProbusVDORegisters,
    FuGProbusVMonitorRegisters,
    FuGProbusVRegisterGroups,
    FuGProbusVSetRegisters,
)

logger = logging.getLogger(__name__)


[docs] @configdataclass class FuGConfig: """ Device configuration dataclass for FuG power supplies. """
[docs] class FuGProbusIV(SingleCommDevice): """ 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) -> None: # Call superclass constructor super().__init__(com, dev_config) # Version of the interface (will be retrieved after com is opened) self._interface_version: str = "" def __repr__(self) -> str: return f"FuGProbus({self._interface_version})"
[docs] @staticmethod def default_com_cls(): return FuGSerialCommunication
[docs] @staticmethod def config_cls(): return FuGConfig
[docs] def start(self) -> None: logger.info(f"Starting device {self}") super().start() self._interface_version = self.command(FuGProbusIVCommands.ID) # type: ignore[arg-type] 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) ): msg = ( "Wrong value for data was given. Expected: " f"{command.input_type} and given: {value.__class__}" ) raise FuGError(msg) # Differentiate between with and without optional value if command.input_type is None: answer = self.com.query(f"{command.command}") else: answer = self.com.query(f"{command.command}{value}") return cast(str, answer) # for typing: query of fug never returns None
# Special commands
[docs] def reset(self) -> None: """ Reset of the interface: All setvalues are set to zero """ self.command(FuGProbusIVCommands.RESET) # type: ignore[arg-type]
[docs] def output_off(self) -> None: """ Switch DC voltage output off. """ self.command( FuGProbusIVCommands.OUTPUT, # type: ignore[arg-type] FuGDigitalVal.OFF, )
[docs] class FuGProbusV(FuGProbusIV): """ FuG Probus V class which uses register based commands to control the power supplies """
[docs] def set_register(self, register: str, value: 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 = cast(str, self.com.query(f">{register} ?")).split(":") # cast for typing: query of fug never returns None if answer[0] != register: cast(FuGErrorcodes, FuGErrorcodes.E505).raise_() return answer[1]
[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) -> None: """ Constructor for configuring the power supply. :param com: :param dev_config: """ # Call superclass constructor super().__init__(com, dev_config) self._id_string: str = "" """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: str = "" # model class of the device (derived from serial number) self._model: str = "" # maximum output current of the hardware self._max_current_hardware: Number = 0 # maximum output charging power of the hardware self._max_power_hardware: Number = 0 # maximum output voltage of the hardware self._max_voltage_hardware: Number = 0 self._voltage = FuGProbusVSetRegisters( self, FuGProbusVRegisterGroups("SETVOLTAGE") ) self._current = FuGProbusVSetRegisters( self, FuGProbusVRegisterGroups("SETCURRENT") ) self._out_x0 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX0")) self._out_x1 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX1")) self._out_x2 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX2")) self._out_xcmd = FuGProbusVDORegisters( self, FuGProbusVRegisterGroups("OUTPUTXCMD") ) self._output = 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) -> str: return f"{self._id_string}"
[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()
@property def voltage(self) -> Number: """ Return the measured output voltage in V """ return self.voltage_monitor.value @voltage.setter def voltage(self, value: Number) -> None: """ Set the output voltage in V :param value: voltage in V """ self._voltage.setvalue = value @property def current(self) -> Number: """ Return the measured output current in A """ return self.current_monitor.value @current.setter def current(self, value: Number) -> None: """ Set the output current :param value: Current in A """ self._current.setvalue = value @property def set_voltage(self) -> Number: """ Return the set voltage (may differ from actual voltage) in V """ return self._voltage.setvalue @set_voltage.setter def set_voltage(self, value: Number) -> None: """Set the output voltage""" self.voltage = value @property def set_current(self) -> Number: """Return the set current (may differ from actual value) in A""" return self._current.setvalue @set_current.setter def set_current(self, value: Number) -> None: """Set the output current""" self.current = value @property def max_voltage(self) -> Number: """ Returns the maximal voltage which could provided within the test setup :return: max voltage in V """ return self._voltage._max_setvalue @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: max current in A """ return self._current._max_setvalue @property def max_current_hardware(self) -> Number: """ Returns the maximal current which could provided with the power supply :return: """ return self._max_current_hardware # Output stage @property def output(self) -> bool | None: """State of the high voltage output""" return self._output.status == FuGDigitalVal.ON @output.setter def output(self, value: bool) -> None: """ Activates the output of the source :param value: `True` for activation, `False` for deactivation """ self._output.out = FuGDigitalVal.ON if value else FuGDigitalVal.OFF # Utility Registers (Not implementing `Source`-protocol) @property def voltage_register(self) -> FuGProbusVSetRegisters: """ Returns the registers for the voltage output """ return self._voltage @property def current_register(self) -> FuGProbusVSetRegisters: """ Returns the registers for the current output """ return self._current @property def out_x0(self) -> FuGProbusVDORegisters: """ Returns the registers for the digital output X0 :return: FuGProbusVDORegisters """ return self._out_x0 @property def out_x1(self) -> FuGProbusVDORegisters: """ Returns the registers for the digital output X1 :return: FuGProbusVDORegisters """ return self._out_x1 @property def out_x2(self) -> FuGProbusVDORegisters: """ Returns the registers for the digital output X2 :return: FuGProbusVDORegisters """ return self._out_x2 @property def out_xcmd(self) -> FuGProbusVDORegisters: """ Returns the registers for the digital outputX-CMD :return: FuGProbusVDORegisters """ return self._out_xcmd @property def output_register(self) -> FuGProbusVDORegisters: """ Returns the registers for the output switch to turn the output on or off :return: FuGProbusVDORegisters """ return self._output @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 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: msg = ( f'The device with the ID string "{id_string}" could not be recognized.' ) raise FuGError(msg) self._id_string = id_string results = result.groupdict() self._model = str(results.get("model")) self._max_power_hardware = int(results.get("power").replace(" ", "")) # type: ignore[union-attr] self._max_voltage_hardware = int(results.get("voltage").replace(" ", "")) # type: ignore[union-attr] self._max_current_hardware = ( 2 * self._max_power_hardware / self._max_voltage_hardware ) self._serial_number = str(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 self.max_voltage == 0: 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 self.max_current == 0: 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." )