Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\technix.py: 100%

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

## 216 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 "RS 232" and "Ethernet" Interfaces which are used to control power

5 supplies from Technix.

6 Manufacturer homepage:

7 https://www.technix-hv.com

8

9 The Regulated power Supplies Series and Capacitor Chargers Series from Technix are

10 series of low and high voltage direct current power supplies as well as capacitor

11 chargers.

12 The class `Technix` is tested with a CCR10KV-7,5KJ via an ethernet connection as well

13 as a CCR15-P-2500-OP via a serial connection.

14 Check the code carefully before using it with other devices or device series

15

16 This Python package may support the following interfaces from Technix:

17 - `Remote Interface RS232

18 <https://www.technix-hv.com/remote-interface-rs232.php>`_

19 - `Ethernet Remote Interface

20 <https://www.technix-hv.com/remote-interface-ethernet.php>`_

21 - `Optic Fiber Remote Interface

22 <https://www.technix-hv.com/remote-interface-optic-fiber.php>`_

23

24"""

25 import logging

26 from time import sleep

27 from typing import Type, Union, Optional

28

29 from . import SingleCommDevice

30 from .utils import Poller

31 from ..comm.serial import (

32 SerialCommunicationParity,

33 SerialCommunicationStopbits,

34 SerialCommunicationBytesize,

35)

36

37 from ..configuration import configdataclass

38 from hvl_ccb.comm import (

39 SerialCommunicationConfig,

40 SerialCommunication,

41 TelnetCommunicationConfig,

42 TelnetCommunication,

43 TelnetError,

44)

45 from ..utils.enum import NameEnum

46 from ..utils.typing import Number

47

48

49 class TechnixError(Exception):

50 """

51 Technix related errors.

52 """

53

54

55@configdataclass

56 class TechnixSerialCommunicationConfig(SerialCommunicationConfig):

57 #: Baudrate for Technix power supplies is 9600 baud

58 baudrate: int = 9600

59

60 #: Technix does not use parity

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

62

63 #: Technix uses one stop bit

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

65

66 #: One byte is eight bits long

67 bytesize: Union[

68 int, SerialCommunicationBytesize

69 ] = SerialCommunicationBytesize.EIGHTBITS

70

71 #: The terminator is CR

72 terminator: bytes = b"\r"

73

74 #: use 3 seconds timeout as default

75 timeout: Number = 3

76

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

78 wait_sec_read_text_nonempty: Number = 0.5

79

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

81 default_n_attempts_read_text_nonempty: int = 10

82

83

84 class TechnixSerialCommunication(SerialCommunication):

85 @staticmethod

86 def config_cls():

87 return TechnixSerialCommunicationConfig

88

89 def query(self, command: str) -> str:

90 """

91 Send a command to the interface and handle the status message.

92 Eventually raises an exception.

93

94 :param command: Command to send

95 :raises TechnixError: if the connection is broken

96 :return: Answer from the interface

97 """

98

99 with self.access_lock:

100 logging.debug(f"TechnixSerialCommunication, send: {command}")

101 self.write_text(command)

102 answer: str = self.read_text_nonempty() # expects an answer string or

103 logging.debug(f"TechnixSerialCommunication, receive: {answer}")

104 if answer == "":

105 raise TechnixError(

106 f"TechnixSerialCommunication did get no answer on "

107 f"command: {command}"

108 )

109 return answer

110

111

112@configdataclass

113 class TechnixTelnetCommunicationConfig(TelnetCommunicationConfig):

114 #: Port at which Technix is listening

115 port: int = 4660

116

117

118 class TechnixTelnetCommunication(TelnetCommunication):

119 @staticmethod

120 def config_cls():

121 return TechnixTelnetCommunicationConfig

122

123 def query(self, command: str) -> str:

124 """

125 Send a command to the interface and handle the status message.

126 Eventually raises an exception.

127

128 :param command: Command to send

129 :raises TechnixError: if the connection is broken

130 :return: Answer from the interface

131 """

132

133 with self.access_lock:

134 logging.debug(f"TechnixTelnetCommunication, send: {command}")

135 self.write_text(command)

136 try:

137 answer: str = self.read_text_nonempty() # expects an answer string or

138 logging.debug(f"TechnixTelnetCommunication, receive: {answer}")

139 except TelnetError as telerr:

140 raise TechnixError(

141 f"TechnixSerialCommunication did get no answer on "

142 f"command: {command}"

143 ) from telerr

144 return answer

145

146

147 TechnixCommunicationClasses = Union[

148 Type[TechnixSerialCommunication], Type[TechnixTelnetCommunication]

149]

150

151

152@configdataclass

153 class TechnixConfig:

154 #: communication channel between computer and Technix

155 communication_channel: TechnixCommunicationClasses

156

157 #: Maximal Output voltage

158 max_voltage: Number

159

160 #: Maximal Output current

161 max_current: Number

162

163 #: Watchdog repetition time in s

164 watchdog_time: Number = 4

165

166

167 class TechnixSetRegisters(NameEnum):

168 VOLTAGE = "d1"

169 CURRENT = "d2"

170 HVON = "P5"

171 HVOFF = "P6"

172 LOCAL = "P7"

173 INHIBIT = "P8"

174

175

176 class TechnixGetRegisters(NameEnum):

177 VOLTAGE = "a1"

178 CURRENT = "a2"

179 STATUS = "E"

180

181

182 class TechnixStatusByte:

183 def __init__(self, value: int):

184 if value < 0 or value > 255:

185 raise TechnixError(f"Cannot convert '{value}' into StatusByte")

186 self._status: list = [bool(value & 1 << 7 - ii) for ii in range(8)]

187

188 def __str__(self):

189 return "".join(str(int(ii)) for ii in self._status)

190

191 def __repr__(self):

192 return f"StatusByte: {self}"

193

194 def msb_first(self, idx: int) -> Optional[bool]:

195 """

196 Give the Bit at position idx with MSB first

197

198 :param idx: Position of Bit as 1...8

199 :return:

200 """

201 if idx < 1 or idx > 8:

202 return None

203

204 return self._status[8 - idx]

205

206

207 class Technix(SingleCommDevice):

208 def __init__(self, com, dev_config):

209 # Call superclass constructor

210 super().__init__(com, dev_config)

211 logging.debug("Technix Power Supply initialised.")

212

213 # maximum output current of the hardware

214 self._max_current_hardware = self.config.max_current

215 # maximum output voltage of the hardware

216 self._max_voltage_hardware = self.config.max_voltage

217

218 #: status of Technix

219 self._voltage_regulation: Optional[bool] = None

220 self._fault: Optional[bool] = None

221 self._open_interlock: Optional[bool] = None

222 self._hv: Optional[bool] = None

223 self._local: Optional[bool] = None

224 self._inhibit: Optional[bool] = None

225 self._real_status = False

226

227 self._watchdog: Poller = Poller(

228 spoll_handler=self.maintain_watchdog,

229 polling_interval_sec=self.config.watchdog_time,

230 )

231

232 @staticmethod

233 def config_cls():

234 return TechnixConfig

235

236 def default_com_cls(self) -> TechnixCommunicationClasses: # type: ignore

237 return self.config.communication_channel

238

239 @property

240 def voltage_regulation(self) -> Optional[bool]:

241 if self._real_status:

242 return self._voltage_regulation

243 return None

244

245 @property

246 def max_current(self) -> Number:

247 return self._max_current_hardware

248

249 @property

250 def max_voltage(self) -> Number:

251 return self._max_voltage_hardware

252

253 def start(self):

254 super().start()

255

256 with self.com.access_lock:

257 logging.debug("Technix: Set remote = True")

258 self.remote = True

259 self.hv = False

260 self._watchdog.start_polling()

261

262 logging.debug("Technix: Started communication")

263

264 def stop(self):

265 with self.com.access_lock:

266 self._watchdog.stop_polling()

267 self.hv = False

268 self.remote = False

269 self._real_status = False

270 sleep(1)

271

272 super().stop()

273

274 logging.debug("Technix: Stopped communication")

275

276 def maintain_watchdog(self):

277 try:

278 self.get_status_byte()

279 except TechnixError as exception:

280 self._watchdog.stop_polling()

281 raise TechnixError("Connection is broken") from exception

282

283 def set_register(self, register: TechnixSetRegisters, value: Union[bool, int]):

284 command = register.value + "," + str(int(value))

285 if not self.com.query(command) == command:

286 raise TechnixError

287

288 def get_register(self, register: TechnixGetRegisters) -> int:

289 answer = self.com.query(register.value)

290 if not answer[: register.value.__len__()] == register.value:

291 raise TechnixError

292 return int(answer[register.value.__len__() :]) # noqa: E203

293

294 @property

295 def voltage(self) -> Number:

296 return (

297 self.get_register(TechnixGetRegisters.VOLTAGE) # type: ignore

298 / 4095

299 * self.max_voltage

300 )

301

302 @voltage.setter

303 def voltage(self, value: Number):

304 _voltage = int(4095 * value / self.max_voltage)

305 if _voltage < 0 or _voltage > 4095:

306 raise TechnixError(f"Voltage '{value}' is out of range")

307 self.set_register(TechnixSetRegisters.VOLTAGE, _voltage) # type: ignore

308

309 @property

310 def current(self) -> Number:

311 return (

312 self.get_register(TechnixGetRegisters.CURRENT) # type: ignore

313 / 4095

314 * self.max_current

315 )

316

317 @current.setter

318 def current(self, value: Number):

319 _current = int(4095 * value / self.max_current)

320 if _current < 0 or _current > 4095:

321 raise TechnixError(f"Current '{value}' is out of range")

322 self.set_register(TechnixSetRegisters.CURRENT, _current) # type: ignore

323

324 @property

325 def hv(self) -> Optional[bool]:

326 if self._real_status:

327 return self._hv

328 return None

329

330 @hv.setter

331 def hv(self, value: Union[bool, Number]):

332 if int(value) < 0 or int(value) > 1:

333 raise TechnixError(f"HV '{value}' is out of range")

334 if value:

335 self.set_register(TechnixSetRegisters.HVON, True) # type: ignore

336 sleep(0.1)

337 self.set_register(TechnixSetRegisters.HVON, False) # type: ignore

338 else:

339 self.set_register(TechnixSetRegisters.HVOFF, True) # type: ignore

340 sleep(0.1)

341 self.set_register(TechnixSetRegisters.HVOFF, False) # type: ignore

342 logging.debug(f"Technix: HV-Output is {'' if value else 'de'}activated")

343

344 @property

345 def remote(self) -> Optional[bool]:

346 if self._real_status:

347 return not self._local

348 return None

349

350 @remote.setter

351 def remote(self, value: Union[bool, Number]):

352 if int(value) < 0 or int(value) > 1:

353 raise TechnixError(f"Remote '{value}' is out of range")

354 self.set_register(TechnixSetRegisters.LOCAL, not value) # type: ignore

355 logging.debug(f"Technix: Remote control is {'' if value else 'de'}activated")

356

357 @property

358 def inhibit(self) -> Optional[bool]:

359 if self._real_status:

360 return not self._inhibit

361 return None

362

363 @inhibit.setter

364 def inhibit(self, value: Union[bool, Number]):

365 if int(value) < 0 or int(value) > 1:

366 raise TechnixError(f"Remote '{value}' is out of range")

367 self.set_register(TechnixSetRegisters.INHIBIT, not value) # type: ignore

368 logging.debug(f"Technix: Inhibit is {'' if value else 'de'}activated")

369

370 def get_status_byte(self) -> TechnixStatusByte:

371 answer = TechnixStatusByte(

372 self.get_register(TechnixGetRegisters.STATUS) # type: ignore

373 )

374 self._inhibit = answer.msb_first(8)

375 self._local = answer.msb_first(7)

376 # HV-Off (1 << (6 - 1))

377 # HV-On (1 << (5 - 1))

378 self._hv = answer.msb_first(4)

379 self._open_interlock = answer.msb_first(3)

380 self._fault = answer.msb_first(2)

381 self._voltage_regulation = answer.msb_first(1)

382 self._real_status = True

383 if self._fault:

384 self.stop()

385 raise TechnixError(

386 "Technix returned the status code with the fault flag set"

387 )

388 logging.debug(f"Technix: Recieved status code: {answer}")

389 return answer