Hide keyboard shortcuts

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 

6 

7This interface is used for many FuG power units. 

8Manufacturer homepage: 

9https://www.fug-elektronik.de 

10 

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/ 

18 

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 

21 

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""" 

27 

28import logging 

29import re 

30from abc import ABC, abstractmethod 

31from enum import IntEnum 

32from typing import Union, cast 

33 

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 

44 

45 

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 """ 

53 

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 ) 

154 

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 ) 

164 

165 

166class FuGDigitalVal(IntEnum): 

167 OFF = 0 

168 ON = 1 

169 YES = 1 

170 NO = 0 

171 

172 

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""" 

184 

185 

186class FuGReadbackChannels(IntEnum): 

187 VOLTAGE = 0 

188 CURRENT = 1 

189 STATUSBYTE = 2 

190 RATEDVOLTAGE = 3 

191 RATEDCURRENT = 4 

192 FIRMWARE = 5 

193 SN = 6 

194 

195 

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""" 

213 

214 

215class FuGPolarities(IntEnum): 

216 POSITIVE = 0 

217 NEGATIVE = 1 

218 

219 

220class FuGTerminators(IntEnum): 

221 CRLF = 0 

222 LFCR = 1 

223 LF = 2 

224 CR = 3 

225 

226 

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) 

244 

245 

246@configdataclass 

247class FuGSerialCommunicationConfig(SerialCommunicationConfig): 

248 #: Baudrate for FuG power supplies is 9600 baud 

249 baudrate: int = 9600 

250 

251 #: FuG does not use parity 

252 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE 

253 

254 #: FuG uses one stop bit 

255 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE 

256 

257 #: One byte is eight bits long 

258 bytesize: Union[ 

259 int, SerialCommunicationBytesize 

260 ] = SerialCommunicationBytesize.EIGHTBITS 

261 

262 #: The terminator is LF 

263 terminator: bytes = b"\n" 

264 

265 #: use 3 seconds timeout as default 

266 timeout: Number = 3 

267 

268 #: default time to wait between attempts of reading a non-empty text 

269 wait_sec_read_text_nonempty: Number = 0.5 

270 

271 #: default number of attempts to read a non-empty text 

272 default_n_attempts_read_text_nonempty: int = 10 

273 

274 

275class FuGSerialCommunication(SerialCommunication): 

276 """ 

277 Specific communication protocol implementation for 

278 FuG power supplies. 

279 Already predefines device-specific protocol parameters in config. 

280 """ 

281 

282 @staticmethod 

283 def config_cls(): 

284 return FuGSerialCommunicationConfig 

285 

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. 

290 

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 """ 

296 

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 

311 

312 

313@configdataclass 

314class FuGConfig: 

315 """ 

316 Device configuration dataclass for FuG power supplies. 

317 """ 

318 

319 #: Time to wait after subsequent commands during stop (in seconds) 

320 wait_sec_stop_commands: Number = 0.5 

321 

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 ) 

328 

329 

330class FuGProbusIV(SingleCommDevice, ABC): 

331 """ 

332 FuG Probus IV device class 

333 

334 Sends basic SCPI commands and reads the answer. 

335 Only the special commands and PROBUS IV instruction set is implemented. 

336 """ 

337 

338 def __init__(self, com, dev_config=None): 

339 

340 # Call superclass constructor 

341 super().__init__(com, dev_config) 

342 

343 # Version of the interface (will be retrieved after com is opened) 

344 self._interface_version = "" 

345 

346 def __repr__(self): 

347 return f"FuGProbus({self._interface_version})" 

348 

349 @staticmethod 

350 def default_com_cls(): 

351 return FuGSerialCommunication 

352 

353 @staticmethod 

354 def config_cls(): 

355 return FuGConfig 

356 

357 # @abstractmethod 

358 def start(self): 

359 """ 

360 Opens the communication protocol. 

361 """ 

362 

363 logging.info("Starting device " + str(self)) 

364 super().start() 

365 

366 self._interface_version = self.command(FuGProbusIVCommands.ID) 

367 logging.info(f"Connection to {self._interface_version} established.") 

368 

369 def stop(self) -> None: 

370 """ 

371 Stop the device. Closes also the communication protocol. 

372 """ 

373 

374 with self.com.access_lock: 

375 logging.info(f"Stopping device {self}") 

376 self.output_off() 

377 self.reset() 

378 super().stop() 

379 

380 def command(self, command: FuGProbusIVCommands, value=None) -> str: 

381 """ 

382 

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 ) 

395 

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}") 

401 

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 ) 

411 

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 ) 

420 

421 

422class FuGProbusV(FuGProbusIV): 

423 """ 

424 FuG Probus V class which uses register based commands to control the power supplies 

425 """ 

426 

427 def __init__(self, com, dev_config=None): 

428 

429 # Call superclass constructor 

430 super().__init__(com, dev_config) 

431 

432 # Version of the interface (will be retrieved after com is opened) 

433 # self._interface_version = "" 

434 

435 @abstractmethod 

436 def start(self): 

437 """ 

438 Opens the communication protocol. 

439 """ 

440 

441 super().start() 

442 

443 def set_register(self, register: str, value: Union[Number, str]) -> None: 

444 """ 

445 generic method to set value to register 

446 

447 :param register: the name of the register to set the value 

448 :param value: which should be written to the register 

449 """ 

450 

451 self.com.query(f">{register} {value}") 

452 

453 def get_register(self, register: str) -> str: 

454 """ 

455 get the value from a register 

456 

457 :param register: the register from which the value is requested 

458 :returns: the value of the register as a String 

459 """ 

460 

461 answer = self.com.query(f">{register} ?").split(":") 

462 if not answer[0] == register: 

463 cast(FuGErrorcodes, FuGErrorcodes.E505).raise_() 

464 

465 return answer[1] 

466 

467 

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" 

480 

481 

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 

491 

492 

493class FuGProbusVSetRegisters: 

494 """ 

495 Setvalue control acc. 4.2.1 for the voltage and the current output 

496 """ 

497 

498 def __init__(self, fug, super_register: FuGProbusVRegisterGroups): 

499 

500 self._fug = fug 

501 

502 _super_register = super_register.value 

503 

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" 

511 

512 @property 

513 def _max_setvalue(self) -> float: 

514 return self.__max_setvalue 

515 

516 @_max_setvalue.setter 

517 def _max_setvalue(self, value: Number): 

518 self.__max_setvalue = float(value) 

519 

520 @property 

521 def setvalue(self) -> float: 

522 """ 

523 For the voltage or current output this setvalue was programmed. 

524 

525 :return: the programmed setvalue 

526 """ 

527 return float(self._fug.get_register(self._setvalue)) 

528 

529 @setvalue.setter 

530 def setvalue(self, value: Number): 

531 """ 

532 This sets the value for the voltage or current output 

533 

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) 

538 

539 @property 

540 def actualsetvalue(self) -> float: 

541 """ 

542 The actual valid set value, which depends on the ramp function. 

543 

544 :return: actual valid set value 

545 """ 

546 return float(self._fug.get_register(self._actualsetvalue)) 

547 

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) 

552 

553 @property 

554 def ramprate(self) -> float: 

555 """ 

556 The set ramp rate in V/s. 

557 

558 :return: ramp rate in V/s 

559 """ 

560 return float(self._fug.get_register(self._ramprate)) 

561 

562 @ramprate.setter 

563 def ramprate(self, value: Number): 

564 """ 

565 The ramp rate can be set in V/s. 

566 

567 :param value: ramp rate in V/s 

568 """ 

569 self._fug.set_register(self._ramprate, value) 

570 

571 @property 

572 def rampmode(self) -> FuGRampModes: 

573 """ 

574 The set ramp mode to control the setvalue. 

575 

576 :return: the mode of the ramp as instance of FuGRampModes 

577 """ 

578 return FuGRampModes(int(self._fug.get_register(self._rampmode))) 

579 

580 @rampmode.setter 

581 def rampmode(self, value: Union[int, FuGRampModes]): 

582 """ 

583 Sets the ramp mode. 

584 

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_() 

592 

593 @property 

594 def rampstate(self) -> FuGDigitalVal: 

595 """ 

596 Status of ramp function. 

597 

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

602 

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 

608 

609 :raise FuGError: if something is written to this attribute 

610 """ 

611 cast(FuGErrorcodes, FuGErrorcodes.E106).raise_() 

612 

613 @property 

614 def high_resolution(self) -> FuGDigitalVal: 

615 """ 

616 Status of the high resolution mode of the output. 

617 

618 :return 0: normal operation 

619 :return 1: High Res. Mode 

620 """ 

621 return FuGDigitalVal(int(self._fug.get_register(self._high_resolution))) 

622 

623 @high_resolution.setter 

624 def high_resolution(self, value: Union[int, FuGDigitalVal]): 

625 """ 

626 Enables/disables the high resolution mode of the output. 

627 

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_() 

638 

639 

640class FuGProbusVDORegisters: 

641 """ 

642 Digital outputs acc. 4.2.2 

643 """ 

644 

645 def __init__(self, fug, super_register: FuGProbusVRegisterGroups): 

646 

647 self._fug = fug 

648 

649 _super_register = super_register.value 

650 

651 self._out = _super_register 

652 self._status = _super_register + "A" 

653 

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. 

659 

660 :return: FuGDigitalVal 

661 """ 

662 return FuGDigitalVal(int(self._fug.get_register(self._out))) 

663 

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. 

668 

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_() 

679 

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. 

685 

686 :return: FuGDigitalVal 

687 """ 

688 return FuGDigitalVal(int(self._fug.get_register(self._status))) 

689 

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 

695 

696 :raise FuGError: read only 

697 """ 

698 FuGErrorcodes("E206").raise_() 

699 

700 

701class FuGProbusVMonitorRegisters: 

702 """ 

703 Analog monitors acc. 4.2.3 

704 """ 

705 

706 def __init__(self, fug, super_register: FuGProbusVRegisterGroups): 

707 

708 self._fug = fug 

709 _super_register = super_register.value 

710 

711 self._value = _super_register 

712 self._value_raw = _super_register + "R" 

713 self._adc_mode = _super_register + "I" 

714 

715 @property 

716 def value(self) -> float: 

717 """ 

718 Value from the monitor. 

719 

720 :return: a float value in V or A 

721 """ 

722 return float(self._fug.get_register(self._value)) 

723 

724 @value.setter 

725 def value(self, _): 

726 """ 

727 Monitor is read-only! 

728 

729 :raise FuGError: read-only 

730 """ 

731 FuGErrorcodes("E306").raise_() 

732 

733 @property 

734 def value_raw(self) -> float: 

735 """ 

736 uncalibrated raw value from AD converter 

737 

738 :return: float value from ADC 

739 """ 

740 return float(self._fug.get_register(self._value_raw)) 

741 

742 @value_raw.setter 

743 def value_raw(self, _): 

744 """ 

745 Monitor is read-only! 

746 

747 :raise FuGError: read-only 

748 """ 

749 FuGErrorcodes("E306").raise_() 

750 

751 @property 

752 def adc_mode(self) -> FuGMonitorModes: 

753 """ 

754 The programmed resolution and integration time of the AD converter 

755 

756 :return: FuGMonitorModes 

757 """ 

758 return FuGMonitorModes(int(self._fug.get_register(self._adc_mode))) 

759 

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. 

765 

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_() 

773 

774 

775class FuGProbusVDIRegisters: 

776 """ 

777 Digital Inputs acc. 4.2.4 

778 """ 

779 

780 def __init__(self, fug, super_register: FuGProbusVRegisterGroups): 

781 

782 self._fug = fug 

783 

784 _super_register = super_register.value 

785 

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" 

794 

795 @property 

796 def cv_mode(self) -> FuGDigitalVal: 

797 """ 

798 

799 :return: shows 1 if power supply is in CV mode 

800 """ 

801 return FuGDigitalVal(int(self._fug.get_register(self._cv_mode))) 

802 

803 @property 

804 def cc_mode(self) -> FuGDigitalVal: 

805 """ 

806 

807 :return: shows 1 if power supply is in CC mode 

808 """ 

809 return FuGDigitalVal(int(self._fug.get_register(self._cc_mode))) 

810 

811 @property 

812 def reg_3(self) -> FuGDigitalVal: 

813 """ 

814 For special applications. 

815 

816 :return: input from bit 3-REG 

817 """ 

818 return FuGDigitalVal(int(self._fug.get_register(self._reg_3))) 

819 

820 @property 

821 def x_stat(self) -> FuGPolarities: 

822 """ 

823 

824 :return: polarity of HVPS with polarity reversal 

825 """ 

826 return FuGPolarities(int(self._fug.get_register(self._x_stat))) 

827 

828 @property 

829 def on(self) -> FuGDigitalVal: 

830 """ 

831 

832 :return: shows 1 if power supply ON 

833 """ 

834 return FuGDigitalVal(int(self._fug.get_register(self._on))) 

835 

836 @property 

837 def digital_control(self) -> FuGDigitalVal: 

838 """ 

839 

840 :return: shows 1 if power supply is digitally controlled 

841 """ 

842 return FuGDigitalVal(int(self._fug.get_register(self._digital_control))) 

843 

844 @property 

845 def analog_control(self) -> FuGDigitalVal: 

846 """ 

847 

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

851 

852 @property 

853 def calibration_mode(self) -> FuGDigitalVal: 

854 """ 

855 

856 :return: shows 1 if power supply is in calibration mode 

857 """ 

858 return FuGDigitalVal(int(self._fug.get_register(self._calibration_mode))) 

859 

860 

861class FuGProbusVConfigRegisters: 

862 """ 

863 Configuration and Status values, acc. 4.2.5 

864 """ 

865 

866 def __init__(self, fug, super_register: FuGProbusVRegisterGroups): 

867 

868 self._fug = fug 

869 

870 _super_register = super_register.value 

871 

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" 

879 

880 @property 

881 def terminator(self) -> FuGTerminators: 

882 """ 

883 Terminator character for answer strings from ADDA 

884 

885 :return: FuGTerminators 

886 """ 

887 return FuGTerminators(int(self._fug.get_register(self._terminator))) 

888 

889 @terminator.setter 

890 def terminator(self, value: Union[int, FuGTerminators]): 

891 """ 

892 Sets the terminator character for answer string from ADDA 

893 

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_() 

901 

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 

915 

916 :return: string of 0/1 

917 """ 

918 return self._fug.get_register(self._status) 

919 

920 @status.setter 

921 def status(self, _): 

922 """ 

923 Stautsbyte is read-only 

924 

925 :raise FuGError: read-only 

926 """ 

927 FuGErrorcodes("E206").raise_() 

928 

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 

935 

936 :return: representative string 

937 """ 

938 return self._fug.get_register(self._srq_status) 

939 

940 @srq_status.setter 

941 def srq_status(self, _): 

942 """ 

943 SRQ-Statusbyte is read-only 

944 

945 :raise FuGError: read-only 

946 """ 

947 FuGErrorcodes("E206").raise_() 

948 

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 

957 

958 :return: representative integer value 

959 """ 

960 return int(float(self._fug.get_register(self._srq_mask))) 

961 

962 @srq_mask.setter 

963 def srq_mask(self, value: int): 

964 """ 

965 Sets the SRQ-Mask 

966 

967 :param value: representative integer value 

968 """ 

969 self._fug.set_register(self._srq_mask, value) 

970 

971 @property 

972 def execute_on_x(self) -> FuGDigitalVal: 

973 """ 

974 status of Execute-on-X 

975 

976 :return: FuGDigitalVal of the status 

977 """ 

978 return FuGDigitalVal(int(self._fug.get_register(self._execute_on_x))) 

979 

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 

986 

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_() 

997 

998 @property 

999 def readback_data(self) -> FuGReadbackChannels: 

1000 """ 

1001 Preselection of readout data for Trigger-on-Talk 

1002 

1003 :return: index for the readback channel 

1004 """ 

1005 return FuGReadbackChannels(int(self._fug.get_register(self._readback_data))) 

1006 

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 

1012 

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_() 

1020 

1021 @property 

1022 def most_recent_error(self) -> FuGErrorcodes: 

1023 """ 

1024 Reads the Error-Code of the most recent command 

1025 

1026 :return FuGError: 

1027 :raise FuGError: if code is not "E0" 

1028 """ 

1029 return FuGErrorcodes(self._fug.get_register(self._most_recent_error)) 

1030 

1031 @most_recent_error.setter 

1032 def most_recent_error(self, _): 

1033 FuGErrorcodes("E666").raise_() 

1034 

1035 

1036class FuG(FuGProbusV): 

1037 """ 

1038 FuG power supply device class. 

1039 

1040 The power supply is controlled over a FuG ADDA Interface with the PROBUS V protocol 

1041 """ 

1042 

1043 def __init__(self, com, dev_config=None): 

1044 """ 

1045 Constructor for configuring the power supply. 

1046 

1047 :param com: 

1048 :param dev_config: 

1049 """ 

1050 

1051 # Call superclass constructor 

1052 super().__init__(com, dev_config) 

1053 

1054 self._id_string = "" 

1055 """ID String of the device (will be retrieved after com is opened) contains 

1056 Serial number and model""" 

1057 

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 

1068 

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 ) 

1092 

1093 def __repr__(self): 

1094 return f"{self._id_string}" 

1095 

1096 @property 

1097 def max_current_hardware(self) -> Number: 

1098 """ 

1099 Returns the maximal current which could provided with the power supply 

1100 

1101 :return: 

1102 """ 

1103 return self._max_current_hardware 

1104 

1105 @property 

1106 def max_voltage_hardware(self) -> Number: 

1107 """ 

1108 Returns the maximal voltage which could provided with the power supply 

1109 

1110 :return: 

1111 """ 

1112 return self._max_voltage_hardware 

1113 

1114 @property 

1115 def max_current(self) -> Number: 

1116 """ 

1117 Returns the maximal current which could provided within the test setup 

1118 

1119 :return: 

1120 """ 

1121 return self.current._max_setvalue 

1122 

1123 @property 

1124 def max_voltage(self) -> Number: 

1125 """ 

1126 Returns the maximal voltage which could provided within the test setup 

1127 

1128 :return: 

1129 """ 

1130 return self.voltage._max_setvalue 

1131 

1132 @property 

1133 def voltage(self) -> FuGProbusVSetRegisters: 

1134 """ 

1135 Returns the registers for the voltage output 

1136 

1137 :return: 

1138 """ 

1139 return self._voltage 

1140 

1141 @voltage.setter 

1142 def voltage(self, value: Number): 

1143 """ 

1144 The output voltage can be set directly with this property. 

1145 

1146 This is the short version for "self.voltage.setvalue" 

1147 

1148 :param value: voltage in V 

1149 """ 

1150 self.voltage.setvalue = value 

1151 

1152 @property 

1153 def current(self) -> FuGProbusVSetRegisters: 

1154 """ 

1155 Returns the registers for the current output 

1156 

1157 :return: 

1158 """ 

1159 return self._current 

1160 

1161 @current.setter 

1162 def current(self, value: Number): 

1163 """ 

1164 The output current can be set directly with this property. 

1165 

1166 This is the short version for "self.current.setvalue" 

1167 

1168 :param value: Current in A 

1169 """ 

1170 self.current.setvalue = value 

1171 

1172 @property 

1173 def outX0(self) -> FuGProbusVDORegisters: 

1174 """ 

1175 Returns the registers for the digital output X0 

1176 

1177 :return: FuGProbusVDORegisters 

1178 """ 

1179 return self._outX0 

1180 

1181 @property 

1182 def outX1(self) -> FuGProbusVDORegisters: 

1183 """ 

1184 Returns the registers for the digital output X1 

1185 

1186 :return: FuGProbusVDORegisters 

1187 """ 

1188 return self._outX1 

1189 

1190 @property 

1191 def outX2(self) -> FuGProbusVDORegisters: 

1192 """ 

1193 Returns the registers for the digital output X2 

1194 

1195 :return: FuGProbusVDORegisters 

1196 """ 

1197 return self._outX2 

1198 

1199 @property 

1200 def outXCMD(self) -> FuGProbusVDORegisters: 

1201 """ 

1202 Returns the registers for the digital outputX-CMD 

1203 

1204 :return: FuGProbusVDORegisters 

1205 """ 

1206 return self._outXCMD 

1207 

1208 @property 

1209 def on(self) -> FuGProbusVDORegisters: 

1210 """ 

1211 Returns the registers for the output switch to turn the output on or off 

1212 

1213 :return: FuGProbusVDORegisters 

1214 """ 

1215 return self._on 

1216 

1217 @on.setter 

1218 def on(self, value: Union[int, FuGDigitalVal]): 

1219 """ 

1220 The output can be directly en- and disabled with this property. 

1221 

1222 It is the short version for "self.on.out" 

1223 

1224 :param value: instance of FuGDigitalVal 

1225 """ 

1226 self.on.out = value 

1227 

1228 @property 

1229 def voltage_monitor(self) -> FuGProbusVMonitorRegisters: 

1230 """ 

1231 Returns the registers for the voltage monitor. 

1232 

1233 A typically usage will be "self.voltage_monitor.value" to measure the output 

1234 voltage 

1235 

1236 :return: 

1237 """ 

1238 return self._voltage_monitor 

1239 

1240 @property 

1241 def current_monitor(self) -> FuGProbusVMonitorRegisters: 

1242 """ 

1243 Returns the registers for the current monitor. 

1244 

1245 A typically usage will be "self.current_monitor.value" to measure the output 

1246 current 

1247 

1248 :return: 

1249 """ 

1250 return self._current_monitor 

1251 

1252 @property 

1253 def di(self) -> FuGProbusVDIRegisters: 

1254 """ 

1255 Returns the registers for the digital inputs 

1256 

1257 :return: FuGProbusVDIRegisters 

1258 """ 

1259 return self._di 

1260 

1261 @property 

1262 def config_status(self) -> FuGProbusVConfigRegisters: 

1263 """ 

1264 Returns the registers for the registers with the configuration and status values 

1265 

1266 :return: FuGProbusVConfigRegisters 

1267 """ 

1268 return self._config_status 

1269 

1270 def start(self, max_voltage=0, max_current=0) -> None: 

1271 """ 

1272 Opens the communication protocol and configures the device. 

1273 

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 """ 

1279 

1280 # starting FuG Probus Interface 

1281 super().start() 

1282 

1283 self.voltage._max_setvalue = max_voltage 

1284 self.current._max_setvalue = max_current 

1285 

1286 # find out which type of source this is: 

1287 self.identify_device() 

1288 

1289 def identify_device(self) -> None: 

1290 """ 

1291 Identify the device nominal voltage and current based on its model number. 

1292 

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 ) 

1307 

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 ) 

1313 

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

1327 

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}") 

1334 

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 ) 

1350 

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 ) 

1364 

1365 

1366class FuGError(Exception): 

1367 """ 

1368 Error with the FuG voltage source. 

1369 """ 

1370 

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)