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

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

## 531 statements

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

4 Device classes for "Probus V - ADDAT30" Interfaces which are used to control power

5 supplies from FuG Elektronik GmbH

6

7 This interface is used for many FuG power units.

8 Manufacturer homepage:

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

10

11 The Professional Series of Power Supplies from FuG is a series of low, medium and high

12 voltage direct current power supplies as well as capacitor chargers.

13 The class FuG is tested with a HCK 800-20 000 in Standard Mode.

14 The addressable mode is not implemented.

15 Check the code carefully before using it with other devices.

16 Manufacturer homepage:

17 https://www.fug-elektronik.de/netzgeraete/professional-series/

18

19 The documentation of the interface from the manufacturer can be found here:

20 https://www.fug-elektronik.de/wp-content/uploads/download/de/SOFTWARE/Probus_V.zip

21

22 The provided classes support the basic and some advanced commands.

23 The commands for calibrating the power supplies are not implemented, as they are only

24 for very special porpoises and

25 should not used by "normal" customers.

26"""

27

28 import logging

29 import re

30 from abc import ABC, abstractmethod

31 from enum import IntEnum

32 from typing import Union, cast

33

34 from .base import SingleCommDevice

35 from ..comm import SerialCommunication, SerialCommunicationConfig

36 from ..comm.serial import (

37 SerialCommunicationParity,

38 SerialCommunicationStopbits,

39 SerialCommunicationBytesize,

40)

41 from ..configuration import configdataclass

42 from ..utils.enum import NameEnum

43 from ..utils.typing import Number

44

45

46 class 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

166 class FuGDigitalVal(IntEnum):

167 OFF = 0

168 ON = 1

169 YES = 1

170 NO = 0

171

172

173 class 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

186 class 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

196 class 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

215 class FuGPolarities(IntEnum):

216 POSITIVE = 0

217 NEGATIVE = 1

218

219

220 class FuGTerminators(IntEnum):

221 CRLF = 0

222 LFCR = 1

223 LF = 2

224 CR = 3

225

226

227 class 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

247 class 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

275 class 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

314 class 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

330 class 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

422 class 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

468 class 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

482 def _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

493 class 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

640 class 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

701 class 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

775 class 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

861 class 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

1036 class 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

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