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

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
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
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>`_
24"""
25import logging
26from time import sleep
27from typing import Type, Union, Optional
29from . import SingleCommDevice
30from .utils import Poller
31from ..comm.serial import (
32 SerialCommunicationParity,
33 SerialCommunicationStopbits,
34 SerialCommunicationBytesize,
35)
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
49class TechnixError(Exception):
50 """
51 Technix related errors.
52 """
55@configdataclass
56class TechnixSerialCommunicationConfig(SerialCommunicationConfig):
57 #: Baudrate for Technix power supplies is 9600 baud
58 baudrate: int = 9600
60 #: Technix does not use parity
61 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
63 #: Technix uses one stop bit
64 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
66 #: One byte is eight bits long
67 bytesize: Union[
68 int, SerialCommunicationBytesize
69 ] = SerialCommunicationBytesize.EIGHTBITS
71 #: The terminator is CR
72 terminator: bytes = b"\r"
74 #: use 3 seconds timeout as default
75 timeout: Number = 3
77 #: default time to wait between attempts of reading a non-empty text
78 wait_sec_read_text_nonempty: Number = 0.5
80 #: default number of attempts to read a non-empty text
81 default_n_attempts_read_text_nonempty: int = 10
84class TechnixSerialCommunication(SerialCommunication):
85 @staticmethod
86 def config_cls():
87 return TechnixSerialCommunicationConfig
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.
94 :param command: Command to send
95 :raises TechnixError: if the connection is broken
96 :return: Answer from the interface
97 """
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
112@configdataclass
113class TechnixTelnetCommunicationConfig(TelnetCommunicationConfig):
114 #: Port at which Technix is listening
115 port: int = 4660
118class TechnixTelnetCommunication(TelnetCommunication):
119 @staticmethod
120 def config_cls():
121 return TechnixTelnetCommunicationConfig
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.
128 :param command: Command to send
129 :raises TechnixError: if the connection is broken
130 :return: Answer from the interface
131 """
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
147TechnixCommunicationClasses = Union[
148 Type[TechnixSerialCommunication], Type[TechnixTelnetCommunication]
149]
152@configdataclass
153class TechnixConfig:
154 #: communication channel between computer and Technix
155 communication_channel: TechnixCommunicationClasses
157 #: Maximal Output voltage
158 max_voltage: Number
160 #: Maximal Output current
161 max_current: Number
163 #: Watchdog repetition time in s
164 watchdog_time: Number = 4
167class TechnixSetRegisters(NameEnum):
168 VOLTAGE = "d1"
169 CURRENT = "d2"
170 HVON = "P5"
171 HVOFF = "P6"
172 LOCAL = "P7"
173 INHIBIT = "P8"
176class TechnixGetRegisters(NameEnum):
177 VOLTAGE = "a1"
178 CURRENT = "a2"
179 STATUS = "E"
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)]
188 def __str__(self):
189 return "".join(str(int(ii)) for ii in self._status)
191 def __repr__(self):
192 return f"StatusByte: {self}"
194 def msb_first(self, idx: int) -> Optional[bool]:
195 """
196 Give the Bit at position idx with MSB first
198 :param idx: Position of Bit as 1...8
199 :return:
200 """
201 if idx < 1 or idx > 8:
202 return None
204 return self._status[8 - idx]
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.")
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
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
227 self._watchdog: Poller = Poller(
228 spoll_handler=self.maintain_watchdog,
229 polling_interval_sec=self.config.watchdog_time,
230 )
232 @staticmethod
233 def config_cls():
234 return TechnixConfig
236 def default_com_cls(self) -> TechnixCommunicationClasses: # type: ignore
237 return self.config.communication_channel
239 @property
240 def voltage_regulation(self) -> Optional[bool]:
241 if self._real_status:
242 return self._voltage_regulation
243 return None
245 @property
246 def max_current(self) -> Number:
247 return self._max_current_hardware
249 @property
250 def max_voltage(self) -> Number:
251 return self._max_voltage_hardware
253 def start(self):
254 super().start()
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()
262 logging.debug("Technix: Started communication")
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)
272 super().stop()
274 logging.debug("Technix: Stopped communication")
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
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
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
294 @property
295 def voltage(self) -> Number:
296 return (
297 self.get_register(TechnixGetRegisters.VOLTAGE) # type: ignore
298 / 4095
299 * self.max_voltage
300 )
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
309 @property
310 def current(self) -> Number:
311 return (
312 self.get_register(TechnixGetRegisters.CURRENT) # type: ignore
313 / 4095
314 * self.max_current
315 )
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
324 @property
325 def hv(self) -> Optional[bool]:
326 if self._real_status:
327 return self._hv
328 return None
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")
344 @property
345 def remote(self) -> Optional[bool]:
346 if self._real_status:
347 return not self._local
348 return None
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")
357 @property
358 def inhibit(self) -> Optional[bool]:
359 if self._real_status:
360 return not self._inhibit
361 return None
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")
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