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) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET 

2# 

3""" 

4Device class for Pfeiffer TPG controllers. 

5 

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. 

8 

9Manufacturer homepage: 

10https://www.pfeiffer-vacuum.com/en/products/measurement-analysis/ 

11measurement/activeline/controllers/ 

12""" 

13 

14import logging 

15from enum import Enum, IntEnum 

16from typing import Dict, List, Tuple, Union, cast 

17 

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 

28 

29 

30class PfeifferTPGError(Exception): 

31 """ 

32 Error with the Pfeiffer TPG Controller. 

33 """ 

34 

35 pass 

36 

37 

38@configdataclass 

39class PfeifferTPGSerialCommunicationConfig(SerialCommunicationConfig): 

40 #: Baudrate for Pfeiffer TPG controllers is 9600 baud 

41 baudrate: int = 9600 

42 

43 #: Pfeiffer TPG controllers do not use parity 

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

45 

46 #: Pfeiffer TPG controllers use one stop bit 

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

48 

49 #: One byte is eight bits long 

50 bytesize: Union[ 

51 int, SerialCommunicationBytesize 

52 ] = SerialCommunicationBytesize.EIGHTBITS 

53 

54 #: The terminator is <CR><LF> 

55 terminator: bytes = b"\r\n" 

56 

57 #: use 3 seconds timeout as default 

58 timeout: Number = 3 

59 

60 

61class PfeifferTPGSerialCommunication(SerialCommunication): 

62 """ 

63 Specific communication protocol implementation for Pfeiffer TPG controllers. 

64 Already predefines device-specific protocol parameters in config. 

65 """ 

66 

67 @staticmethod 

68 def config_cls(): 

69 return PfeifferTPGSerialCommunicationConfig 

70 

71 def send_command(self, cmd: str) -> None: 

72 """ 

73 Send a command to the device and check for acknowledgement. 

74 

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

80 

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) 

92 

93 def query(self, cmd: str) -> str: 

94 """ 

95 Send a query, then read and returns the first line from the com port. 

96 

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

103 

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 

124 

125 

126@configdataclass 

127class PfeifferTPGConfig: 

128 """ 

129 Device configuration dataclass for Pfeiffer TPG controllers. 

130 """ 

131 

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 } 

157 

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 } 

163 

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. 

167 

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 

178 

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 

182 

183 def clean_values(self): 

184 if not isinstance(self.model, self.Model): 

185 self.force_value("model", self.Model(self.model)) 

186 

187 

188class PfeifferTPG(SingleCommDevice): 

189 """ 

190 Pfeiffer TPG control unit device class 

191 """ 

192 

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

199 

200 mbar = 0 

201 bar = 0 

202 Torr = 1 

203 Pascal = 2 

204 Micron = 3 

205 hPascal = 4 

206 Volt = 5 

207 

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 ) 

234 

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 

243 

244 def __init__(self, com, dev_config=None): 

245 

246 # Call superclass constructor 

247 super().__init__(com, dev_config) 

248 

249 # list of sensors connected to the TPG 

250 self.sensors: List[str] = [] 

251 

252 def __repr__(self): 

253 return f"Pfeiffer TPG with {self.number_of_sensors} sensors: {self.sensors}" 

254 

255 @property 

256 def number_of_sensors(self): 

257 return len(self.sensors) 

258 

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" 

265 

266 @staticmethod 

267 def default_com_cls(): 

268 return PfeifferTPGSerialCommunication 

269 

270 @staticmethod 

271 def config_cls(): 

272 return PfeifferTPGConfig 

273 

274 def start(self) -> None: 

275 """ 

276 Start this device. Opens the communication protocol, 

277 and identify the sensors. 

278 

279 :raises SerialCommunicationIOError: when communication port cannot be opened 

280 """ 

281 

282 logging.info("Starting Pfeiffer TPG") 

283 super().start() 

284 

285 # identify the sensors connected to the TPG 

286 # and also find out the number of channels 

287 self.identify_sensors() 

288 

289 def stop(self) -> None: 

290 """ 

291 Stop the device. Closes also the communication protocol. 

292 """ 

293 

294 logging.info(f"Stopping device {self}") 

295 super().stop() 

296 

297 def identify_sensors(self) -> None: 

298 """ 

299 Send identification request TID to sensors on all channels. 

300 

301 :raises SerialCommunicationIOError: when communication port is not opened 

302 :raises PfeifferTPGError: if command fails 

303 """ 

304 

305 try: 

306 answer = self.com.query("TID") 

307 except PfeifferTPGError: 

308 logging.error("Pressure sensor identification failed.") 

309 raise 

310 

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

321 

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. 

325 

326 :raises SerialCommunicationIOError: when communication port is not opened 

327 :raises PfeifferTPGError: if command fails 

328 """ 

329 

330 if not isinstance(unit, self.PressureUnits): 

331 unit = self.PressureUnits(unit) 

332 

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 

342 

343 def measure(self, channel: int) -> Tuple[str, float]: 

344 """ 

345 Get the status and measurement of one sensor 

346 

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

354 

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) 

362 

363 try: 

364 answer = self.com.query(f"PR{channel}") 

365 except PfeifferTPGError: 

366 logging.error(f"Reading sensor {channel} failed.") 

367 raise 

368 

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) 

382 

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) 

387 

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

393 

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 

402 

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 

410 

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. 

415 

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 ) 

428 

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 ) 

440 

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 

450 

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. 

455 

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

463 

464 try: 

465 answer = self.com.query("FSR") 

466 except PfeifferTPGError: 

467 logging.error("Query full scale range of all sensors failed.") 

468 raise 

469 

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 ) 

481 

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 

491 

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. 

496 

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) 

502 

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. 

507 

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

513 

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) 

517 

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) 

523 

524 def get_full_scale_mbar(self) -> List[Number]: 

525 """ 

526 Get the full scale range of the attached sensors 

527 

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

532 

533 return self._get_full_scale(False)