Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\pfeiffer_tpg.py : 39%

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) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4Device class for Pfeiffer TPG controllers.
6The Pfeiffer TPG control units are used to control Pfeiffer Compact Gauges.
7Models: TPG 251 A, TPG 252 A, TPG 256A, TPG 261, TPG 262, TPG 361, TPG 362 and TPG 366.
9Manufacturer homepage:
10https://www.pfeiffer-vacuum.com/en/products/measurement-analysis/
11measurement/activeline/controllers/
12"""
14import logging
15from enum import Enum, IntEnum
16from typing import Dict, List, Tuple, Union, cast
18from .base import SingleCommDevice
19from ..comm import SerialCommunication, SerialCommunicationConfig
20from ..comm.serial import (
21 SerialCommunicationParity,
22 SerialCommunicationStopbits,
23 SerialCommunicationBytesize,
24)
25from ..configuration import configdataclass
26from ..utils.enum import NameEnum
27from ..utils.typing import Number
30class PfeifferTPGError(Exception):
31 """
32 Error with the Pfeiffer TPG Controller.
33 """
35 pass
38@configdataclass
39class PfeifferTPGSerialCommunicationConfig(SerialCommunicationConfig):
40 #: Baudrate for Pfeiffer TPG controllers is 9600 baud
41 baudrate: int = 9600
43 #: Pfeiffer TPG controllers do not use parity
44 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
46 #: Pfeiffer TPG controllers use one stop bit
47 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
49 #: One byte is eight bits long
50 bytesize: Union[
51 int, SerialCommunicationBytesize
52 ] = SerialCommunicationBytesize.EIGHTBITS
54 #: The terminator is <CR><LF>
55 terminator: bytes = b"\r\n"
57 #: use 3 seconds timeout as default
58 timeout: Number = 3
61class PfeifferTPGSerialCommunication(SerialCommunication):
62 """
63 Specific communication protocol implementation for Pfeiffer TPG controllers.
64 Already predefines device-specific protocol parameters in config.
65 """
67 @staticmethod
68 def config_cls():
69 return PfeifferTPGSerialCommunicationConfig
71 def send_command(self, cmd: str) -> None:
72 """
73 Send a command to the device and check for acknowledgement.
75 :param cmd: command to send to the device
76 :raises SerialCommunicationIOError: when communication port is not opened
77 :raises PfeifferTPGError: if the answer from the device differs from the
78 expected acknowledgement character 'chr(6)'.
79 """
81 with self.access_lock:
82 # send the command
83 self.write_text(cmd)
84 # check for acknowledgment char (ASCII 6)
85 answer = self.read_text()
86 if len(answer) == 0 or ord(answer[0]) != 6:
87 message = f"Pfeiffer TPG not acknowledging command {cmd}"
88 logging.error(message)
89 if len(answer) > 0:
90 logging.debug(f"Pfeiffer TPG: {answer}")
91 raise PfeifferTPGError(message)
93 def query(self, cmd: str) -> str:
94 """
95 Send a query, then read and returns the first line from the com port.
97 :param cmd: query message to send to the device
98 :return: first line read on the com
99 :raises SerialCommunicationIOError: when communication port is not opened
100 :raises PfeifferTPGError: if the device does not acknowledge the command or if
101 the answer from the device is empty
102 """
104 with self.access_lock:
105 # send the command
106 self.write_text(cmd)
107 # check for acknowledgment char (ASCII 6)
108 answer = self.read_text()
109 if len(answer) == 0 or ord(answer[0]) != 6:
110 message = f"Pfeiffer TPG not acknowledging command {cmd}"
111 logging.error(message)
112 if len(answer) > 0:
113 logging.debug(f"Pfeiffer TPG: {answer}")
114 raise PfeifferTPGError(message)
115 # send enquiry
116 self.write_text(chr(5))
117 # read answer
118 answer = self.read_text().strip()
119 if len(answer) == 0:
120 message = f"Pfeiffer TPG not answering to command {cmd}"
121 logging.error(message)
122 raise PfeifferTPGError(message)
123 return answer
126@configdataclass
127class PfeifferTPGConfig:
128 """
129 Device configuration dataclass for Pfeiffer TPG controllers.
130 """
132 class Model(NameEnum):
133 _init_ = "full_scale_ranges"
134 TPG25xA = {
135 1: 0,
136 10: 1,
137 100: 2,
138 1000: 3,
139 2000: 4,
140 5000: 5,
141 10000: 6,
142 50000: 7,
143 0.1: 8,
144 }
145 TPGx6x = {
146 0.01: 0,
147 0.1: 1,
148 1: 2,
149 10: 3,
150 100: 4,
151 1000: 5,
152 2000: 6,
153 5000: 7,
154 10000: 8,
155 50000: 9,
156 }
158 def __init__(self, *args, **kwargs):
159 super().__init__(*args, **kwargs)
160 self.full_scale_ranges_reversed: Dict[int, int] = {
161 v: k for k, v in self.full_scale_ranges.items()
162 }
164 def is_valid_scale_range_reversed_str(self, v: str) -> bool:
165 """
166 Check if given string represents a valid reversed scale range of a model.
168 :param v: Reversed scale range string.
169 :return: `True` if valid, `False` otherwise.
170 """
171 # Explicit check because otherwise we get `True` for instance for `float`
172 if not isinstance(v, str):
173 raise TypeError(f"Expected `str`, got `{type(v)}` instead.")
174 try:
175 return int(v) in self.full_scale_ranges_reversed
176 except ValueError:
177 return False
179 # model of the TPG (determines which lookup table to use for the
180 # full scale range)
181 model: Union[str, Model] = Model.TPG25xA # type: ignore
183 def clean_values(self):
184 if not isinstance(self.model, self.Model):
185 self.force_value("model", self.Model(self.model))
188class PfeifferTPG(SingleCommDevice):
189 """
190 Pfeiffer TPG control unit device class
191 """
193 class PressureUnits(NameEnum):
194 """
195 Enum of available pressure units for the digital display. "0" corresponds either
196 to bar or to mbar depending on the TPG model. In case of doubt, the unit is
197 visible on the digital display.
198 """
200 mbar = 0
201 bar = 0
202 Torr = 1
203 Pascal = 2
204 Micron = 3
205 hPascal = 4
206 Volt = 5
208 SensorTypes = Enum( # type: ignore
209 value="SensorTypes",
210 names=[
211 ("TPR/PCR Pirani Gauge", 1),
212 ("TPR", 1),
213 ("TPR/PCR", 1),
214 ("IKR Cold Cathode Gauge", 2),
215 ("IKR", 2),
216 ("IKR9", 2),
217 ("IKR11", 2),
218 ("PKR Full range CC", 3),
219 ("PKR", 3),
220 ("APR/CMR Linear Gauge", 4),
221 ("CMR", 4),
222 ("APR/CMR", 4),
223 ("CMR/APR", 4),
224 ("Pirani / High Pressure Gauge", 5),
225 ("IMR", 5),
226 ("Fullrange BA Gauge", 6),
227 ("PBR", 6),
228 ("None", 7),
229 ("no Sensor", 7),
230 ("noSen", 7),
231 ("noSENSOR", 7),
232 ],
233 )
235 class SensorStatus(IntEnum):
236 Ok = 0
237 Underrange = 1
238 Overrange = 2
239 Sensor_error = 3
240 Sensor_off = 4
241 No_sensor = 5
242 Identification_error = 6
244 def __init__(self, com, dev_config=None):
246 # Call superclass constructor
247 super().__init__(com, dev_config)
249 # list of sensors connected to the TPG
250 self.sensors: List[str] = []
252 def __repr__(self):
253 return f"Pfeiffer TPG with {self.number_of_sensors} sensors: {self.sensors}"
255 @property
256 def number_of_sensors(self):
257 return len(self.sensors)
259 @property
260 def unit(self):
261 """
262 The pressure unit of readings is always mbar, regardless of the display unit.
263 """
264 return "mbar"
266 @staticmethod
267 def default_com_cls():
268 return PfeifferTPGSerialCommunication
270 @staticmethod
271 def config_cls():
272 return PfeifferTPGConfig
274 def start(self) -> None:
275 """
276 Start this device. Opens the communication protocol,
277 and identify the sensors.
279 :raises SerialCommunicationIOError: when communication port cannot be opened
280 """
282 logging.info("Starting Pfeiffer TPG")
283 super().start()
285 # identify the sensors connected to the TPG
286 # and also find out the number of channels
287 self.identify_sensors()
289 def stop(self) -> None:
290 """
291 Stop the device. Closes also the communication protocol.
292 """
294 logging.info(f"Stopping device {self}")
295 super().stop()
297 def identify_sensors(self) -> None:
298 """
299 Send identification request TID to sensors on all channels.
301 :raises SerialCommunicationIOError: when communication port is not opened
302 :raises PfeifferTPGError: if command fails
303 """
305 try:
306 answer = self.com.query("TID")
307 except PfeifferTPGError:
308 logging.error("Pressure sensor identification failed.")
309 raise
311 # try matching the sensors:
312 sensors = []
313 for s in answer.split(","):
314 try:
315 sensors.append(self.SensorTypes[s].name)
316 except KeyError:
317 sensors.append("Unknown")
318 self.sensors = sensors
319 # identification successful:
320 logging.info(f"Identified {self}")
322 def set_display_unit(self, unit: Union[str, PressureUnits]) -> None:
323 """
324 Set the unit in which the measurements are shown on the display.
326 :raises SerialCommunicationIOError: when communication port is not opened
327 :raises PfeifferTPGError: if command fails
328 """
330 if not isinstance(unit, self.PressureUnits):
331 unit = self.PressureUnits(unit)
333 try:
334 self.com.send_command(f"UNI,{unit.value}")
335 logging.info(f"Setting display unit to {unit.name}")
336 except PfeifferTPGError:
337 logging.error(
338 f"Setting display unit to {unit.name} failed. Not all units"
339 " are available on all TGP models"
340 )
341 raise
343 def measure(self, channel: int) -> Tuple[str, float]:
344 """
345 Get the status and measurement of one sensor
347 :param channel: int channel on which the sensor is connected, with
348 1 <= channel <= number_of_sensors
349 :return: measured value as float if measurement successful,
350 sensor status as string if not
351 :raises SerialCommunicationIOError: when communication port is not opened
352 :raises PfeifferTPGError: if command fails
353 """
355 if not 1 <= channel <= self.number_of_sensors:
356 message = (
357 f"{channel} is not a valid channel number, it should be between "
358 f"1 and {self.number_of_sensors}"
359 )
360 logging.error(message)
361 raise ValueError(message)
363 try:
364 answer = self.com.query(f"PR{channel}")
365 except PfeifferTPGError:
366 logging.error(f"Reading sensor {channel} failed.")
367 raise
369 status, measurement = answer.split(",")
370 s = self.SensorStatus(int(status))
371 if s == self.SensorStatus.Ok:
372 logging.info(
373 f"Channel {channel} successful reading of "
374 f"pressure: {measurement} mbar."
375 )
376 else:
377 logging.info(
378 f"Channel {channel} no reading of pressure, sensor status is "
379 f"{self.SensorStatus(s).name}."
380 )
381 return s.name, float(measurement)
383 def measure_all(self) -> List[Tuple[str, float]]:
384 """
385 Get the status and measurement of all sensors (this command is
386 not available on all models)
388 :return: list of measured values as float if measurements successful,
389 and or sensor status as strings if not
390 :raises SerialCommunicationIOError: when communication port is not opened
391 :raises PfeifferTPGError: if command fails
392 """
394 try:
395 answer = self.com.query("PRX")
396 except PfeifferTPGError:
397 logging.error(
398 "Getting pressure reading from all sensors failed (this "
399 "command is not available on all TGP models)."
400 )
401 raise
403 ans = answer.split(",")
404 ret = [
405 (self.SensorStatus(int(ans[2 * i])).name, float(ans[2 * i + 1]))
406 for i in range(self.number_of_sensors)
407 ]
408 logging.info(f"Reading all sensors with result: {ret}.")
409 return ret
411 def _set_full_scale(self, fsr: List[Number], unitless: bool) -> None:
412 """
413 Set the full scale range of the attached sensors. See lookup table between
414 command and corresponding pressure in the device user manual.
416 :param fsr: list of full scale range values, like `[0, 1, 3, 3, 2, 0]` for
417 `unitless = True` scale or `[0.01, 1000]` otherwise (mbar units scale)
418 :param unitless: flag to indicate scale of range values; if `False` then mbar
419 units scale
420 :raises SerialCommunicationIOError: when communication port is not opened
421 :raises PfeifferTPGError: if command fails
422 """
423 if len(fsr) != self.number_of_sensors:
424 raise ValueError(
425 f"Argument fsr should be of length {self.number_of_sensors}. "
426 f"Received length {len(fsr)}."
427 )
429 possible_values_map = (
430 self.config.model.full_scale_ranges_reversed if unitless
431 else self.config.model.full_scale_ranges
432 )
433 wrong_values = [v for v in fsr if v not in possible_values_map]
434 if wrong_values:
435 raise ValueError(
436 f"Argument fsr contains invalid values: {wrong_values}. Accepted "
437 f"values are {list(possible_values_map.items())}"
438 f"{'' if unitless else ' mbar'}."
439 )
441 str_fsr = ",".join([
442 str(f if unitless else possible_values_map[f]) for f in fsr
443 ])
444 try:
445 self.com.send_command(f"FSR,{str_fsr}")
446 logging.info(f"Set sensors full scale to {fsr} (unitless) respectively.")
447 except PfeifferTPGError as e:
448 logging.error("Setting sensors full scale failed.")
449 raise e
451 def _get_full_scale(self, unitless: bool) -> List[Number]:
452 """
453 Get the full scale range of the attached sensors. See lookup table between
454 command and corresponding pressure in the device user manual.
456 :param unitless: flag to indicate scale of range values; if `False` then mbar
457 units scale
458 :return: list of full scale range values, like `[0, 1, 3, 3, 2, 0]` for
459 `unitless = True` scale or `[0.01, 1000]` otherwise (mbar units scale)
460 :raises SerialCommunicationIOError: when communication port is not opened
461 :raises PfeifferTPGError: if command fails
462 """
464 try:
465 answer = self.com.query("FSR")
466 except PfeifferTPGError:
467 logging.error("Query full scale range of all sensors failed.")
468 raise
470 answer_values = answer.split(",")
471 wrong_values = [
472 v for v in answer_values
473 if not self.config.model.is_valid_scale_range_reversed_str(v)
474 ]
475 if wrong_values:
476 raise PfeifferTPGError(
477 f"The controller returned the full unitless scale range values: "
478 f"{answer}. The values {wrong_values} are invalid. Accepted values are "
479 f"{list(self.config.model.full_scale_ranges_reversed.keys())}."
480 )
482 fsr = [
483 int(v) if unitless else self.config.model.full_scale_ranges_reversed[int(v)]
484 for v in answer_values
485 ]
486 logging.info(
487 f"Obtained full scale range of all sensors as {fsr}"
488 f"{'' if unitless else ' mbar'}."
489 )
490 return fsr
492 def set_full_scale_unitless(self, fsr: List[int]) -> None:
493 """
494 Set the full scale range of the attached sensors. See lookup table between
495 command and corresponding pressure in the device user manual.
497 :param fsr: list of full scale range values, like `[0, 1, 3, 3, 2, 0]`
498 :raises SerialCommunicationIOError: when communication port is not opened
499 :raises PfeifferTPGError: if command fails
500 """
501 self._set_full_scale(cast(List[Number], fsr), True)
503 def get_full_scale_unitless(self) -> List[int]:
504 """
505 Get the full scale range of the attached sensors. See lookup table between
506 command and corresponding pressure in the device user manual.
508 :return: list of full scale range values, like `[0, 1, 3, 3, 2, 0]`
509 :raises SerialCommunicationIOError: when communication port is not opened
510 :raises PfeifferTPGError: if command fails
511 """
512 return cast(List[int], self._get_full_scale(True))
514 def set_full_scale_mbar(self, fsr: List[Number]) -> None:
515 """
516 Set the full scale range of the attached sensors (in unit mbar)
518 :param fsr: full scale range values in mbar, for example `[0.01, 1000]`
519 :raises SerialCommunicationIOError: when communication port is not opened
520 :raises PfeifferTPGError: if command fails
521 """
522 self._set_full_scale(fsr, False)
524 def get_full_scale_mbar(self) -> List[Number]:
525 """
526 Get the full scale range of the attached sensors
528 :return: full scale range values in mbar, like `[0.01, 1, 0.1, 1000, 50000, 10]`
529 :raises SerialCommunicationIOError: when communication port is not opened
530 :raises PfeifferTPGError: if command fails
531 """
533 return self._get_full_scale(False)