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

5supplies from Technix. 

6Manufacturer homepage: 

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

8 

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

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

11chargers. 

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

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

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

15 

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

25import logging 

26from time import sleep 

27from typing import Type, Union, Optional 

28 

29from . import SingleCommDevice 

30from .utils import Poller 

31from ..comm.serial import ( 

32 SerialCommunicationParity, 

33 SerialCommunicationStopbits, 

34 SerialCommunicationBytesize, 

35) 

36 

37from ..configuration import configdataclass 

38from hvl_ccb.comm import ( 

39 SerialCommunicationConfig, 

40 SerialCommunication, 

41 TelnetCommunicationConfig, 

42 TelnetCommunication, 

43 TelnetError, 

44) 

45from ..utils.enum import NameEnum 

46from ..utils.typing import Number 

47 

48 

49class TechnixError(Exception): 

50 """ 

51 Technix related errors. 

52 """ 

53 

54 

55@configdataclass 

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

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

113class TechnixTelnetCommunicationConfig(TelnetCommunicationConfig): 

114 #: Port at which Technix is listening 

115 port: int = 4660 

116 

117 

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

147TechnixCommunicationClasses = Union[ 

148 Type[TechnixSerialCommunication], Type[TechnixTelnetCommunication] 

149] 

150 

151 

152@configdataclass 

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

167class TechnixSetRegisters(NameEnum): 

168 VOLTAGE = "d1" 

169 CURRENT = "d2" 

170 HVON = "P5" 

171 HVOFF = "P6" 

172 LOCAL = "P7" 

173 INHIBIT = "P8" 

174 

175 

176class TechnixGetRegisters(NameEnum): 

177 VOLTAGE = "a1" 

178 CURRENT = "a2" 

179 STATUS = "E" 

180 

181 

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

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