Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\fug.py : 55%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4Device classes for "Probus V - ADDAT30" Interfaces which are used to control power
5supplies from FuG Elektronik GmbH
7This interface is used for many FuG power units.
8Manufacturer homepage:
9https://www.fug-elektronik.de
11The Professional Series of Power Supplies from FuG is a series of low, medium and high
12voltage direct current power supplies as well as capacitor chargers.
13The class FuG is tested with a HCK 800-20 000 in Standard Mode.
14The addressable mode is not implemented.
15Check the code carefully before using it with other devices.
16Manufacturer homepage:
17https://www.fug-elektronik.de/netzgeraete/professional-series/
19The documentation of the interface from the manufacturer can be found here:
20https://www.fug-elektronik.de/wp-content/uploads/download/de/SOFTWARE/Probus_V.zip
22The provided classes support the basic and some advanced commands.
23The commands for calibrating the power supplies are not implemented, as they are only
24for very special porpoises and
25should not used by "normal" customers.
26"""
28import logging
29import re
30from abc import ABC, abstractmethod
31from enum import IntEnum
32from typing import Union, cast
34from .base import SingleCommDevice
35from ..comm import SerialCommunication, SerialCommunicationConfig
36from ..comm.serial import (
37 SerialCommunicationParity,
38 SerialCommunicationStopbits,
39 SerialCommunicationBytesize,
40)
41from ..configuration import configdataclass
42from ..utils.enum import NameEnum
43from ..utils.typing import Number
46class FuGErrorcodes(NameEnum):
47 """
48 The power supply can return an errorcode. These errorcodes are handled by this
49 class. The original errorcodes from the source are with one or two digits,
50 see documentation of Probus V chapter 5.
51 All three-digit errorcodes are from this python module.
52 """
54 _init_ = "description possible_reason"
55 E0 = "no error", "standard response on each command"
56 E1 = (
57 "no data available",
58 "Customer tried to read from GPIB but there were no data prepared. "
59 "(IBIG50 sent command ~T2 to ADDA)",
60 )
61 E2 = "unknown register type", "No valid register type after '>'"
62 E4 = (
63 "invalid argument",
64 "The argument of the command was rejected .i.e. malformed number",
65 )
66 E5 = "argument out of range", "i.e. setvalue higher than type value"
67 E6 = (
68 "register is read only",
69 "Some registers can only be read but not written to. (i.e. monitor registers)",
70 )
71 E7 = "Receive Overflow", "Command string was longer than 50 characters."
72 E8 = (
73 "EEPROM is write protected",
74 "Write attempt to calibration data while the write protection switch was set "
75 "to write protected.",
76 )
77 E9 = (
78 "address error",
79 "A non addressed command was sent to ADDA while it was in addressable mode "
80 "(and vice versa).",
81 )
82 E10 = "unknown SCPI command", "This SCPI command is not implemented"
83 E11 = (
84 "not allowed Trigger-on-Talk",
85 "Not allowed attempt to Trigger-on-Talk (~T1) while ADDA was in addressable "
86 "mode.",
87 )
88 E12 = "invalid argument in ~Tn command", "Only ~T1 and ~T2 is implemented."
89 E13 = (
90 "invalid N-value",
91 "Register > K8 contained an invalid value. Error code is output on an attempt "
92 "to query data with ? or ~T1",
93 )
94 E14 = "register is write only", "Some registers can only be writte to (i.e.> H0)"
95 E15 = "string too long", "i.e.serial number string too long during calibration"
96 E16 = (
97 "wrong checksum",
98 "checksum over command string was not correct, refer also to 4.4 of the "
99 "Probus V documentation",
100 )
101 E100 = (
102 "Command is not implemented",
103 "You tried to execute a command, which is not implemented or does not exist",
104 )
105 E106 = (
106 "The rampstate is a read-only register",
107 "You tried to write data to the register, which can only give "
108 "you the status of the ramping.",
109 )
110 E206 = (
111 "This status register is read-only",
112 "You tried to write data to this "
113 "register, which can only give you "
114 "the actual status of the "
115 "corresponding digital output.",
116 )
117 E306 = (
118 "The monitor register is read-only",
119 "You tried to write data to a "
120 "monitor, which can only give you "
121 "measured data.",
122 )
123 E115 = (
124 "The given index to select a digital value is out of range",
125 "Only integer values between 0 and 1 are allowed.",
126 )
127 E125 = (
128 "The given index to select a ramp mode is out of range",
129 "Only integer " "values between 0 and 4 are allowed.",
130 )
131 E135 = (
132 "The given index to select the readback channel is out of range",
133 "Only integer values between 0 and 6 are allowed.",
134 )
135 E145 = (
136 "The given value for the AD-conversion is unknown",
137 'Valid values for the ad-conversion are integer values from "0" to "7".',
138 )
139 E155 = (
140 "The given value to select a polarity is out range.",
141 "The value should be 0 or 1.",
142 )
143 E165 = "The given index to select the terminator string is out of range", ""
144 E504 = "Empty string as response", "The connection is broken."
145 E505 = (
146 "The returned register is not the requested.",
147 "Maybe the connection is overburden.",
148 )
149 E666 = (
150 "You cannot overwrite the most recent error in the interface of the power "
151 "supply. But, well: You created an error anyway...",
152 "",
153 )
155 def raise_(self):
156 if self is FuGErrorcodes.E0:
157 logging.debug('Communication with FuG successful, errorcode "E0" received.')
158 return
159 logging.debug(f"A FuGError with the errorcode {self.name} was detected.")
160 raise FuGError(
161 f"{self.description}. Possible reason: {self.possible_reason}",
162 errorcode=self.name,
163 )
166class FuGDigitalVal(IntEnum):
167 OFF = 0
168 ON = 1
169 YES = 1
170 NO = 0
173class FuGRampModes(IntEnum):
174 IMMEDIATELY = 0
175 """Standard mode: no ramp"""
176 FOLLOWRAMP = 1
177 """Follow the ramp up- and downwards"""
178 RAMPUPWARDS = 2
179 """Follow the ramp only upwards, downwards immediately"""
180 SPECIALRAMPUPWARDS = 3
181 """Follow a special ramp function only upwards"""
182 ONLYUPWARDSOFFTOZERO = 4
183 """Follow the ramp up- and downwards, if output is OFF set value is zero"""
186class FuGReadbackChannels(IntEnum):
187 VOLTAGE = 0
188 CURRENT = 1
189 STATUSBYTE = 2
190 RATEDVOLTAGE = 3
191 RATEDCURRENT = 4
192 FIRMWARE = 5
193 SN = 6
196class FuGMonitorModes(IntEnum):
197 T256US = 0
198 """14 bit + sign, 256 us integration time"""
199 T1MS = 1
200 """15 bit + sign, 1 ms integration time"""
201 T4MS = 2
202 """15 bit + sign, 4 ms integration time"""
203 T20MS = 3
204 """17 bit + sign, 20 ms integration time"""
205 T40MS = 4
206 """17 bit + sign, 40 ms integration time"""
207 T80MS = 5
208 """typ. 18 bit + sign, 80 ms integration time"""
209 T200MS = 6
210 """typ. 19 bit + sign, 200 ms integration time"""
211 T800MS = 7
212 """typ. 20 bit + sign, 800 ms integration time"""
215class FuGPolarities(IntEnum):
216 POSITIVE = 0
217 NEGATIVE = 1
220class FuGTerminators(IntEnum):
221 CRLF = 0
222 LFCR = 1
223 LF = 2
224 CR = 3
227class FuGProbusIVCommands(NameEnum):
228 _init_ = "command input_type"
229 ID = "*IDN?", None
230 RESET = "=", None
231 OUTPUT = "F", (FuGDigitalVal, int)
232 VOLTAGE = "U", (int, float)
233 CURRENT = "I", (int, float)
234 READBACKCHANNEL = "N", (FuGReadbackChannels, int)
235 QUERY = "?", None
236 ADMODE = "S", (FuGMonitorModes, int)
237 POLARITY = "P", (FuGPolarities, int)
238 XOUTPUTS = "R", int
239 """TODO: the possible values are limited to 0..13"""
240 EXECUTEONX = "G", (FuGDigitalVal, int)
241 """Wait for "X" to execute pending commands"""
242 EXECUTE = "X", None
243 TERMINATOR = "Y", (FuGTerminators, int)
246@configdataclass
247class FuGSerialCommunicationConfig(SerialCommunicationConfig):
248 #: Baudrate for FuG power supplies is 9600 baud
249 baudrate: int = 9600
251 #: FuG does not use parity
252 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
254 #: FuG uses one stop bit
255 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
257 #: One byte is eight bits long
258 bytesize: Union[
259 int, SerialCommunicationBytesize
260 ] = SerialCommunicationBytesize.EIGHTBITS
262 #: The terminator is LF
263 terminator: bytes = b"\n"
265 #: use 3 seconds timeout as default
266 timeout: Number = 3
268 #: default time to wait between attempts of reading a non-empty text
269 wait_sec_read_text_nonempty: Number = 0.5
271 #: default number of attempts to read a non-empty text
272 default_n_attempts_read_text_nonempty: int = 10
275class FuGSerialCommunication(SerialCommunication):
276 """
277 Specific communication protocol implementation for
278 FuG power supplies.
279 Already predefines device-specific protocol parameters in config.
280 """
282 @staticmethod
283 def config_cls():
284 return FuGSerialCommunicationConfig
286 def query(self, command: str) -> str:
287 """
288 Send a command to the interface and handle the status message.
289 Eventually raises an exception.
291 :param command: Command to send
292 :raises FuGError: if the connection is broken or the error from the power
293 source itself
294 :return: Answer from the interface or empty string
295 """
297 with self.access_lock:
298 logging.debug(f"FuG communication, send: {command}")
299 self.write_text(command)
300 answer: str = self.read_text_nonempty() # expects an answer string or
301 logging.debug(f"FuG communication, receive: {answer}")
302 if answer == "":
303 cast(FuGErrorcodes, FuGErrorcodes.E504).raise_()
304 try:
305 FuGErrorcodes(answer).raise_()
306 return ""
307 except ValueError:
308 if answer.startswith("E"):
309 raise FuGError(f'The unknown errorcode "{answer}" was detected.')
310 return answer
313@configdataclass
314class FuGConfig:
315 """
316 Device configuration dataclass for FuG power supplies.
317 """
319 #: Time to wait after subsequent commands during stop (in seconds)
320 wait_sec_stop_commands: Number = 0.5
322 def clean_values(self):
323 if self.wait_sec_stop_commands <= 0:
324 raise ValueError(
325 "Wait time after subsequent commands during stop must be be a "
326 "positive value (in seconds)."
327 )
330class FuGProbusIV(SingleCommDevice, ABC):
331 """
332 FuG Probus IV device class
334 Sends basic SCPI commands and reads the answer.
335 Only the special commands and PROBUS IV instruction set is implemented.
336 """
338 def __init__(self, com, dev_config=None):
340 # Call superclass constructor
341 super().__init__(com, dev_config)
343 # Version of the interface (will be retrieved after com is opened)
344 self._interface_version = ""
346 def __repr__(self):
347 return f"FuGProbus({self._interface_version})"
349 @staticmethod
350 def default_com_cls():
351 return FuGSerialCommunication
353 @staticmethod
354 def config_cls():
355 return FuGConfig
357 # @abstractmethod
358 def start(self):
359 """
360 Opens the communication protocol.
361 """
363 logging.info("Starting device " + str(self))
364 super().start()
366 self._interface_version = self.command(FuGProbusIVCommands.ID)
367 logging.info(f"Connection to {self._interface_version} established.")
369 def stop(self) -> None:
370 """
371 Stop the device. Closes also the communication protocol.
372 """
374 with self.com.access_lock:
375 logging.info(f"Stopping device {self}")
376 self.output_off()
377 self.reset()
378 super().stop()
380 def command(self, command: FuGProbusIVCommands, value=None) -> str:
381 """
383 :param command: one of the commands given within FuGProbusIVCommands
384 :param value: an optional value, depending on the command
385 :return: a String if a query was performed
386 """
387 if not (
388 (value is None and command.input_type is None)
389 or isinstance(value, command.input_type)
390 ):
391 raise FuGError(
392 f"Wrong value for data was given. Expected: "
393 f"{command.input_type} and given: {value.__class__}"
394 )
396 # Differentiate between with and without optional value
397 if command.input_type is None:
398 return self.com.query(f"{command.command}")
399 else:
400 return self.com.query(f"{command.command}{value}")
402 # Special commands
403 def reset(self) -> None:
404 """
405 Reset of the interface:
406 All setvalues are set to zero
407 """
408 self.command(
409 FuGProbusIVCommands.RESET # type: ignore
410 )
412 def output_off(self) -> None:
413 """
414 Switch DC voltage output off.
415 """
416 self.command(
417 FuGProbusIVCommands.OUTPUT, # type: ignore
418 FuGDigitalVal.OFF,
419 )
422class FuGProbusV(FuGProbusIV):
423 """
424 FuG Probus V class which uses register based commands to control the power supplies
425 """
427 def __init__(self, com, dev_config=None):
429 # Call superclass constructor
430 super().__init__(com, dev_config)
432 # Version of the interface (will be retrieved after com is opened)
433 # self._interface_version = ""
435 @abstractmethod
436 def start(self):
437 """
438 Opens the communication protocol.
439 """
441 super().start()
443 def set_register(self, register: str, value: Union[Number, str]) -> None:
444 """
445 generic method to set value to register
447 :param register: the name of the register to set the value
448 :param value: which should be written to the register
449 """
451 self.com.query(f">{register} {value}")
453 def get_register(self, register: str) -> str:
454 """
455 get the value from a register
457 :param register: the register from which the value is requested
458 :returns: the value of the register as a String
459 """
461 answer = self.com.query(f">{register} ?").split(":")
462 if not answer[0] == register:
463 cast(FuGErrorcodes, FuGErrorcodes.E505).raise_()
465 return answer[1]
468class FuGProbusVRegisterGroups(NameEnum):
469 SETVOLTAGE = "S0"
470 SETCURRENT = "S1"
471 OUTPUTX0 = "B0"
472 OUTPUTX1 = "B1"
473 OUTPUTX2 = "B2"
474 OUTPUTXCMD = "BX"
475 OUTPUTONCMD = "BON"
476 MONITOR_V = "M0"
477 MONITOR_I = "M1"
478 INPUT = "D"
479 CONFIG = "K"
482def _check_for_value_limits(value, max_limit) -> bool:
483 if value > max_limit:
484 raise FuGError(
485 f"The requested value of {value} exceeds the maximal"
486 f"technically permissible value of {max_limit}."
487 )
488 elif value < 0:
489 raise ValueError("The value must be positive.")
490 return True
493class FuGProbusVSetRegisters:
494 """
495 Setvalue control acc. 4.2.1 for the voltage and the current output
496 """
498 def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
500 self._fug = fug
502 _super_register = super_register.value
504 self._setvalue: str = _super_register
505 self.__max_setvalue: float = 0
506 self._actualsetvalue: str = _super_register + "A"
507 self._ramprate: str = _super_register + "R"
508 self._rampmode: str = _super_register + "B"
509 self._rampstate: str = _super_register + "S"
510 self._high_resolution: str = _super_register + "H"
512 @property
513 def _max_setvalue(self) -> float:
514 return self.__max_setvalue
516 @_max_setvalue.setter
517 def _max_setvalue(self, value: Number):
518 self.__max_setvalue = float(value)
520 @property
521 def setvalue(self) -> float:
522 """
523 For the voltage or current output this setvalue was programmed.
525 :return: the programmed setvalue
526 """
527 return float(self._fug.get_register(self._setvalue))
529 @setvalue.setter
530 def setvalue(self, value: Number):
531 """
532 This sets the value for the voltage or current output
534 :param value: value in V or A
535 """
536 _check_for_value_limits(value, self._max_setvalue)
537 self._fug.set_register(self._setvalue, value)
539 @property
540 def actualsetvalue(self) -> float:
541 """
542 The actual valid set value, which depends on the ramp function.
544 :return: actual valid set value
545 """
546 return float(self._fug.get_register(self._actualsetvalue))
548 @actualsetvalue.setter
549 def actualsetvalue(self, value: Number):
550 _check_for_value_limits(value, self._max_setvalue)
551 self._fug.set_register(self._actualsetvalue, value)
553 @property
554 def ramprate(self) -> float:
555 """
556 The set ramp rate in V/s.
558 :return: ramp rate in V/s
559 """
560 return float(self._fug.get_register(self._ramprate))
562 @ramprate.setter
563 def ramprate(self, value: Number):
564 """
565 The ramp rate can be set in V/s.
567 :param value: ramp rate in V/s
568 """
569 self._fug.set_register(self._ramprate, value)
571 @property
572 def rampmode(self) -> FuGRampModes:
573 """
574 The set ramp mode to control the setvalue.
576 :return: the mode of the ramp as instance of FuGRampModes
577 """
578 return FuGRampModes(int(self._fug.get_register(self._rampmode)))
580 @rampmode.setter
581 def rampmode(self, value: Union[int, FuGRampModes]):
582 """
583 Sets the ramp mode.
585 :param value: index for the ramp mode from FuGRampModes
586 :raise FuGError: if a wrong ramp mode is chosen
587 """
588 try:
589 self._fug.set_register(self._rampmode, FuGRampModes(value))
590 except ValueError:
591 cast(FuGErrorcodes, FuGErrorcodes.E125).raise_()
593 @property
594 def rampstate(self) -> FuGDigitalVal:
595 """
596 Status of ramp function.
598 :return 0: if final setvalue is reached
599 :return 1: if still ramping up
600 """
601 return FuGDigitalVal(int(self._fug.get_register(self._rampstate)))
603 @rampstate.setter
604 def rampstate(self, _):
605 """
606 The rampstate is only an output. Writing data to this register will raise an
607 exception
609 :raise FuGError: if something is written to this attribute
610 """
611 cast(FuGErrorcodes, FuGErrorcodes.E106).raise_()
613 @property
614 def high_resolution(self) -> FuGDigitalVal:
615 """
616 Status of the high resolution mode of the output.
618 :return 0: normal operation
619 :return 1: High Res. Mode
620 """
621 return FuGDigitalVal(int(self._fug.get_register(self._high_resolution)))
623 @high_resolution.setter
624 def high_resolution(self, value: Union[int, FuGDigitalVal]):
625 """
626 Enables/disables the high resolution mode of the output.
628 :param value: FuGDigitalVal
629 :raise FuGError: if not a FuGDigitalVal is given
630 """
631 try:
632 if FuGDigitalVal(value) is FuGDigitalVal.ON:
633 self._fug.set_register(self._high_resolution, FuGDigitalVal.ON)
634 else:
635 self._fug.set_register(self._high_resolution, FuGDigitalVal.OFF)
636 except ValueError:
637 cast(FuGErrorcodes, FuGErrorcodes.E115).raise_()
640class FuGProbusVDORegisters:
641 """
642 Digital outputs acc. 4.2.2
643 """
645 def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
647 self._fug = fug
649 _super_register = super_register.value
651 self._out = _super_register
652 self._status = _super_register + "A"
654 @property
655 def out(self) -> Union[int, FuGDigitalVal]:
656 """
657 Status of the output according to the last setting. This can differ from the
658 actual state if output should only pulse.
660 :return: FuGDigitalVal
661 """
662 return FuGDigitalVal(int(self._fug.get_register(self._out)))
664 @out.setter
665 def out(self, value: Union[int, FuGDigitalVal]):
666 """
667 Set the output ON or OFF. If pulsing is enabled, it only pulses.
669 :param value: FuGDigitalVal
670 :raise FuGError: if a non FuGDigitalVal is given
671 """
672 try:
673 if FuGDigitalVal(value) is FuGDigitalVal.ON:
674 self._fug.set_register(self._out, FuGDigitalVal.ON)
675 else:
676 self._fug.set_register(self._out, FuGDigitalVal.OFF)
677 except ValueError:
678 FuGErrorcodes("E115").raise_()
680 @property
681 def status(self) -> FuGDigitalVal:
682 """
683 Returns the actual value of output. This can differ from the set value if
684 pulse function is used.
686 :return: FuGDigitalVal
687 """
688 return FuGDigitalVal(int(self._fug.get_register(self._status)))
690 @status.setter
691 def status(self, _):
692 """
693 The status is only an output. Writing data to this register will raise an
694 exception
696 :raise FuGError: read only
697 """
698 FuGErrorcodes("E206").raise_()
701class FuGProbusVMonitorRegisters:
702 """
703 Analog monitors acc. 4.2.3
704 """
706 def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
708 self._fug = fug
709 _super_register = super_register.value
711 self._value = _super_register
712 self._value_raw = _super_register + "R"
713 self._adc_mode = _super_register + "I"
715 @property
716 def value(self) -> float:
717 """
718 Value from the monitor.
720 :return: a float value in V or A
721 """
722 return float(self._fug.get_register(self._value))
724 @value.setter
725 def value(self, _):
726 """
727 Monitor is read-only!
729 :raise FuGError: read-only
730 """
731 FuGErrorcodes("E306").raise_()
733 @property
734 def value_raw(self) -> float:
735 """
736 uncalibrated raw value from AD converter
738 :return: float value from ADC
739 """
740 return float(self._fug.get_register(self._value_raw))
742 @value_raw.setter
743 def value_raw(self, _):
744 """
745 Monitor is read-only!
747 :raise FuGError: read-only
748 """
749 FuGErrorcodes("E306").raise_()
751 @property
752 def adc_mode(self) -> FuGMonitorModes:
753 """
754 The programmed resolution and integration time of the AD converter
756 :return: FuGMonitorModes
757 """
758 return FuGMonitorModes(int(self._fug.get_register(self._adc_mode)))
760 @adc_mode.setter
761 def adc_mode(self, value: Union[int, FuGMonitorModes]):
762 """
763 Sets the resolution and integration time of the AD converter with the given
764 settings in FuGMonitorModes.
766 :param value: index of the monitor mode from FuGMonitorModes
767 :raise FuGError: if index is not in FuGMonitorModes
768 """
769 try:
770 self._fug.set_register(self._adc_mode, FuGMonitorModes(value))
771 except ValueError:
772 raise FuGErrorcodes("E145").raise_()
775class FuGProbusVDIRegisters:
776 """
777 Digital Inputs acc. 4.2.4
778 """
780 def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
782 self._fug = fug
784 _super_register = super_register.value
786 self._cv_mode = _super_register + "VR"
787 self._cc_mode = _super_register + "IR"
788 self._reg_3 = _super_register + "3R"
789 self._x_stat = _super_register + "X"
790 self._on = _super_register + "ON"
791 self._digital_control = _super_register + "SD"
792 self._analog_control = _super_register + "SA"
793 self._calibration_mode = _super_register + "CAL"
795 @property
796 def cv_mode(self) -> FuGDigitalVal:
797 """
799 :return: shows 1 if power supply is in CV mode
800 """
801 return FuGDigitalVal(int(self._fug.get_register(self._cv_mode)))
803 @property
804 def cc_mode(self) -> FuGDigitalVal:
805 """
807 :return: shows 1 if power supply is in CC mode
808 """
809 return FuGDigitalVal(int(self._fug.get_register(self._cc_mode)))
811 @property
812 def reg_3(self) -> FuGDigitalVal:
813 """
814 For special applications.
816 :return: input from bit 3-REG
817 """
818 return FuGDigitalVal(int(self._fug.get_register(self._reg_3)))
820 @property
821 def x_stat(self) -> FuGPolarities:
822 """
824 :return: polarity of HVPS with polarity reversal
825 """
826 return FuGPolarities(int(self._fug.get_register(self._x_stat)))
828 @property
829 def on(self) -> FuGDigitalVal:
830 """
832 :return: shows 1 if power supply ON
833 """
834 return FuGDigitalVal(int(self._fug.get_register(self._on)))
836 @property
837 def digital_control(self) -> FuGDigitalVal:
838 """
840 :return: shows 1 if power supply is digitally controlled
841 """
842 return FuGDigitalVal(int(self._fug.get_register(self._digital_control)))
844 @property
845 def analog_control(self) -> FuGDigitalVal:
846 """
848 :return: shows 1 if power supply is controlled by the analog interface
849 """
850 return FuGDigitalVal(int(self._fug.get_register(self._analog_control)))
852 @property
853 def calibration_mode(self) -> FuGDigitalVal:
854 """
856 :return: shows 1 if power supply is in calibration mode
857 """
858 return FuGDigitalVal(int(self._fug.get_register(self._calibration_mode)))
861class FuGProbusVConfigRegisters:
862 """
863 Configuration and Status values, acc. 4.2.5
864 """
866 def __init__(self, fug, super_register: FuGProbusVRegisterGroups):
868 self._fug = fug
870 _super_register = super_register.value
872 self._terminator = _super_register + "T"
873 self._status = _super_register + "S"
874 self._srq_status = _super_register + "QS"
875 self._srq_mask = _super_register + "QM"
876 self._execute_on_x = _super_register + "X"
877 self._readback_data = _super_register + "N"
878 self._most_recent_error = _super_register + "E"
880 @property
881 def terminator(self) -> FuGTerminators:
882 """
883 Terminator character for answer strings from ADDA
885 :return: FuGTerminators
886 """
887 return FuGTerminators(int(self._fug.get_register(self._terminator)))
889 @terminator.setter
890 def terminator(self, value: Union[int, FuGTerminators]):
891 """
892 Sets the terminator character for answer string from ADDA
894 :param value: index from FuGTerminators
895 :raise FuGError: if index is not in FuGTerminators
896 """
897 try:
898 self._fug.set_register(self._terminator, FuGTerminators(value))
899 except ValueError:
900 FuGErrorcodes("E165").raise_()
902 @property
903 def status(self) -> str:
904 """
905 Statusbyte as a string of 0/1. Combined status (compatibel to Probus IV),
906 MSB first:
907 Bit 7: I-REG
908 Bit 6: V-REG
909 Bit 5: ON-Status
910 Bit 4: 3-Reg
911 Bit 3: X-Stat (polarity)
912 Bit 2: Cal-Mode
913 Bit 1: unused
914 Bit 0: SEL-D
916 :return: string of 0/1
917 """
918 return self._fug.get_register(self._status)
920 @status.setter
921 def status(self, _):
922 """
923 Stautsbyte is read-only
925 :raise FuGError: read-only
926 """
927 FuGErrorcodes("E206").raise_()
929 @property
930 def srq_status(self) -> str:
931 """
932 SRQ-Statusbyte output as a decimal number:
933 Bit 2: PS is in CC mode
934 Bit 1: PS is in CV mode
936 :return: representative string
937 """
938 return self._fug.get_register(self._srq_status)
940 @srq_status.setter
941 def srq_status(self, _):
942 """
943 SRQ-Statusbyte is read-only
945 :raise FuGError: read-only
946 """
947 FuGErrorcodes("E206").raise_()
949 @property
950 def srq_mask(self) -> int:
951 """
952 SRQ-Mask, Service-Request
953 Enable status bits for SRQ
954 0: no SRQ
955 Bit 2: SRQ on change of status to CC
956 Bit 1: SRQ on change to CV
958 :return: representative integer value
959 """
960 return int(float(self._fug.get_register(self._srq_mask)))
962 @srq_mask.setter
963 def srq_mask(self, value: int):
964 """
965 Sets the SRQ-Mask
967 :param value: representative integer value
968 """
969 self._fug.set_register(self._srq_mask, value)
971 @property
972 def execute_on_x(self) -> FuGDigitalVal:
973 """
974 status of Execute-on-X
976 :return: FuGDigitalVal of the status
977 """
978 return FuGDigitalVal(int(self._fug.get_register(self._execute_on_x)))
980 @execute_on_x.setter
981 def execute_on_x(self, value: Union[int, FuGDigitalVal]):
982 """
983 Enable/disable the Execute-on-X-mode
984 0: immediate execution
985 1: execution pending until X-command
987 :param value: FuGDigitalVal
988 :raise FuGError: if a non FuGDigitalVal is given
989 """
990 try:
991 if FuGDigitalVal(value) is FuGDigitalVal.YES:
992 self._fug.set_register(self._execute_on_x, FuGDigitalVal.YES)
993 else:
994 self._fug.set_register(self._execute_on_x, FuGDigitalVal.NO)
995 except ValueError:
996 FuGErrorcodes("E115").raise_()
998 @property
999 def readback_data(self) -> FuGReadbackChannels:
1000 """
1001 Preselection of readout data for Trigger-on-Talk
1003 :return: index for the readback channel
1004 """
1005 return FuGReadbackChannels(int(self._fug.get_register(self._readback_data)))
1007 @readback_data.setter
1008 def readback_data(self, value: Union[int, FuGReadbackChannels]):
1009 """
1010 Sets the readback channel according to the index given within the
1011 FuGReadbackChannels
1013 :param value: index of readback channel
1014 :raise FuGError: if index in not in FuGReadbackChannels
1015 """
1016 try:
1017 self._fug.set_register(self._readback_data, FuGReadbackChannels(value))
1018 except ValueError:
1019 FuGErrorcodes("E135").raise_()
1021 @property
1022 def most_recent_error(self) -> FuGErrorcodes:
1023 """
1024 Reads the Error-Code of the most recent command
1026 :return FuGError:
1027 :raise FuGError: if code is not "E0"
1028 """
1029 return FuGErrorcodes(self._fug.get_register(self._most_recent_error))
1031 @most_recent_error.setter
1032 def most_recent_error(self, _):
1033 FuGErrorcodes("E666").raise_()
1036class FuG(FuGProbusV):
1037 """
1038 FuG power supply device class.
1040 The power supply is controlled over a FuG ADDA Interface with the PROBUS V protocol
1041 """
1043 def __init__(self, com, dev_config=None):
1044 """
1045 Constructor for configuring the power supply.
1047 :param com:
1048 :param dev_config:
1049 """
1051 # Call superclass constructor
1052 super().__init__(com, dev_config)
1054 self._id_string = ""
1055 """ID String of the device (will be retrieved after com is opened) contains
1056 Serial number and model"""
1058 # Serial number of the device (will be retrieved after com is opened)
1059 self._serial_number = ""
1060 # model class of the device (derived from serial number)
1061 self._model = ""
1062 # maximum output current of the hardware
1063 self._max_current_hardware = 0
1064 # maximum output charging power of the hardware
1065 self._max_power_hardware = 0
1066 # maximum output voltage of the hardware
1067 self._max_voltage_hardware = 0
1069 self._voltage = FuGProbusVSetRegisters(
1070 self, FuGProbusVRegisterGroups("SETVOLTAGE")
1071 )
1072 self._current = FuGProbusVSetRegisters(
1073 self, FuGProbusVRegisterGroups("SETCURRENT")
1074 )
1075 self._outX0 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX0"))
1076 self._outX1 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX1"))
1077 self._outX2 = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTX2"))
1078 self._outXCMD = FuGProbusVDORegisters(
1079 self, FuGProbusVRegisterGroups("OUTPUTXCMD")
1080 )
1081 self._on = FuGProbusVDORegisters(self, FuGProbusVRegisterGroups("OUTPUTONCMD"))
1082 self._voltage_monitor = FuGProbusVMonitorRegisters(
1083 self, FuGProbusVRegisterGroups("MONITOR_V")
1084 )
1085 self._current_monitor = FuGProbusVMonitorRegisters(
1086 self, FuGProbusVRegisterGroups("MONITOR_I")
1087 )
1088 self._di = FuGProbusVDIRegisters(self, FuGProbusVRegisterGroups("INPUT"))
1089 self._config_status = FuGProbusVConfigRegisters(
1090 self, FuGProbusVRegisterGroups("CONFIG")
1091 )
1093 def __repr__(self):
1094 return f"{self._id_string}"
1096 @property
1097 def max_current_hardware(self) -> Number:
1098 """
1099 Returns the maximal current which could provided with the power supply
1101 :return:
1102 """
1103 return self._max_current_hardware
1105 @property
1106 def max_voltage_hardware(self) -> Number:
1107 """
1108 Returns the maximal voltage which could provided with the power supply
1110 :return:
1111 """
1112 return self._max_voltage_hardware
1114 @property
1115 def max_current(self) -> Number:
1116 """
1117 Returns the maximal current which could provided within the test setup
1119 :return:
1120 """
1121 return self.current._max_setvalue
1123 @property
1124 def max_voltage(self) -> Number:
1125 """
1126 Returns the maximal voltage which could provided within the test setup
1128 :return:
1129 """
1130 return self.voltage._max_setvalue
1132 @property
1133 def voltage(self) -> FuGProbusVSetRegisters:
1134 """
1135 Returns the registers for the voltage output
1137 :return:
1138 """
1139 return self._voltage
1141 @voltage.setter
1142 def voltage(self, value: Number):
1143 """
1144 The output voltage can be set directly with this property.
1146 This is the short version for "self.voltage.setvalue"
1148 :param value: voltage in V
1149 """
1150 self.voltage.setvalue = value
1152 @property
1153 def current(self) -> FuGProbusVSetRegisters:
1154 """
1155 Returns the registers for the current output
1157 :return:
1158 """
1159 return self._current
1161 @current.setter
1162 def current(self, value: Number):
1163 """
1164 The output current can be set directly with this property.
1166 This is the short version for "self.current.setvalue"
1168 :param value: Current in A
1169 """
1170 self.current.setvalue = value
1172 @property
1173 def outX0(self) -> FuGProbusVDORegisters:
1174 """
1175 Returns the registers for the digital output X0
1177 :return: FuGProbusVDORegisters
1178 """
1179 return self._outX0
1181 @property
1182 def outX1(self) -> FuGProbusVDORegisters:
1183 """
1184 Returns the registers for the digital output X1
1186 :return: FuGProbusVDORegisters
1187 """
1188 return self._outX1
1190 @property
1191 def outX2(self) -> FuGProbusVDORegisters:
1192 """
1193 Returns the registers for the digital output X2
1195 :return: FuGProbusVDORegisters
1196 """
1197 return self._outX2
1199 @property
1200 def outXCMD(self) -> FuGProbusVDORegisters:
1201 """
1202 Returns the registers for the digital outputX-CMD
1204 :return: FuGProbusVDORegisters
1205 """
1206 return self._outXCMD
1208 @property
1209 def on(self) -> FuGProbusVDORegisters:
1210 """
1211 Returns the registers for the output switch to turn the output on or off
1213 :return: FuGProbusVDORegisters
1214 """
1215 return self._on
1217 @on.setter
1218 def on(self, value: Union[int, FuGDigitalVal]):
1219 """
1220 The output can be directly en- and disabled with this property.
1222 It is the short version for "self.on.out"
1224 :param value: instance of FuGDigitalVal
1225 """
1226 self.on.out = value
1228 @property
1229 def voltage_monitor(self) -> FuGProbusVMonitorRegisters:
1230 """
1231 Returns the registers for the voltage monitor.
1233 A typically usage will be "self.voltage_monitor.value" to measure the output
1234 voltage
1236 :return:
1237 """
1238 return self._voltage_monitor
1240 @property
1241 def current_monitor(self) -> FuGProbusVMonitorRegisters:
1242 """
1243 Returns the registers for the current monitor.
1245 A typically usage will be "self.current_monitor.value" to measure the output
1246 current
1248 :return:
1249 """
1250 return self._current_monitor
1252 @property
1253 def di(self) -> FuGProbusVDIRegisters:
1254 """
1255 Returns the registers for the digital inputs
1257 :return: FuGProbusVDIRegisters
1258 """
1259 return self._di
1261 @property
1262 def config_status(self) -> FuGProbusVConfigRegisters:
1263 """
1264 Returns the registers for the registers with the configuration and status values
1266 :return: FuGProbusVConfigRegisters
1267 """
1268 return self._config_status
1270 def start(self, max_voltage=0, max_current=0) -> None:
1271 """
1272 Opens the communication protocol and configures the device.
1274 :param max_voltage: Configure here the maximal permissible voltage which is
1275 allowed in the given experimental setup
1276 :param max_current: Configure here the maximal permissible current which is
1277 allowed in the given experimental setup
1278 """
1280 # starting FuG Probus Interface
1281 super().start()
1283 self.voltage._max_setvalue = max_voltage
1284 self.current._max_setvalue = max_current
1286 # find out which type of source this is:
1287 self.identify_device()
1289 def identify_device(self) -> None:
1290 """
1291 Identify the device nominal voltage and current based on its model number.
1293 :raises SerialCommunicationIOError: when communication port is not opened
1294 """
1295 id_string = str(self.command(FuGProbusIVCommands("ID")))
1296 # "'FUG HCK
1297 # 800
1298 # - 20 000
1299 # MOD 17022-01-01'"
1300 # regex to find the model of the device
1301 regex_model = (
1302 "FUG (?P<model>[A-Z]{3})"
1303 " (?P<power>[0-9 ]+)"
1304 " - (?P<voltage>[0-9 ]+)"
1305 " MOD (?P<sn>[0-9-]+)"
1306 )
1308 result = re.search(regex_model, id_string)
1309 if not result:
1310 raise FuGError(
1311 f'The device with the ID string "{id_string}" could not be recognized.'
1312 )
1314 self._id_string = id_string
1315 results = result.groupdict()
1316 self._model = results.get("model")
1317 self._max_power_hardware = int(
1318 results.get("power").replace(" ", "") # type: ignore
1319 )
1320 self._max_voltage_hardware = int(
1321 results.get("voltage").replace(" ", "") # type: ignore
1322 )
1323 self._max_current_hardware = (
1324 2 * self._max_power_hardware / self._max_voltage_hardware
1325 )
1326 self._serial_number = results.get("sn")
1328 logging.info(f"Device {id_string} successfully identified:")
1329 logging.info(f"Model class: {self._model}")
1330 logging.info(f"Maximal voltage: {self._max_voltage_hardware} V")
1331 logging.info(f"Maximal current: {self._max_current_hardware} A")
1332 logging.info(f"Maximal charging power: {self._max_power_hardware} J/s")
1333 logging.info(f"Serial number: {self._serial_number}")
1335 # if limits for test setup were not predefined, set them to hardware limits
1336 # or if the previous limits were to high, limit them to the hardware limits
1337 if 0 == self.max_voltage:
1338 self.voltage._max_setvalue = self.max_voltage_hardware
1339 elif self.max_voltage > self.max_voltage_hardware:
1340 logging.warning(
1341 f"FuG power source should supply up to "
1342 f"{self.max_voltage} V, but the hardware only goes up "
1343 f"to {self.max_voltage_hardware} V."
1344 )
1345 self.voltage._max_setvalue = self.max_voltage_hardware
1346 logging.info(
1347 f"For this setup the maximal output voltage of the power "
1348 f"supply is limited to {self.max_voltage} V."
1349 )
1351 if 0 == self.max_current:
1352 self.current._max_setvalue = self.max_current_hardware
1353 elif self.max_current > self.max_current_hardware:
1354 logging.warning(
1355 f"FuG power source should supply up to "
1356 f"{self.max_current} A, but the hardware only goes up "
1357 f"to {self.max_current_hardware} A."
1358 )
1359 self.current._max_setvalue = self.max_current_hardware
1360 logging.info(
1361 f"For this setup the maximal output current of the power "
1362 f"supply is limited to {self.max_current} A."
1363 )
1366class FuGError(Exception):
1367 """
1368 Error with the FuG voltage source.
1369 """
1371 def __init__(self, *args, **kwargs):
1372 self.errorcode: str = kwargs.pop("errorcode", "")
1373 """
1374 Errorcode from the Probus, see documentation of Probus V chapter 5.
1375 Errors with three-digit errorcodes are thrown by this python module.
1376 """
1377 super().__init__(*args, **kwargs)