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

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

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

2#

3"""

4 Device class for Pfeiffer TPG controllers.

5

6 The Pfeiffer TPG control units are used to control Pfeiffer Compact Gauges.

7 Models: TPG 251 A, TPG 252 A, TPG 256A, TPG 261, TPG 262, TPG 361, TPG 362 and TPG 366.

8

9 Manufacturer homepage:

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

11 measurement/activeline/controllers/

12"""

13

14 import logging

15 from enum import Enum, IntEnum

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

17

18 from .base import SingleCommDevice

19 from ..comm import SerialCommunication, SerialCommunicationConfig

20 from ..comm.serial import (

21 SerialCommunicationParity,

22 SerialCommunicationStopbits,

23 SerialCommunicationBytesize,

24)

25 from ..configuration import configdataclass

26 from ..utils.enum import NameEnum

27 from ..utils.typing import Number

28

29

30 class PfeifferTPGError(Exception):

31 """

32 Error with the Pfeiffer TPG Controller.

33 """

34

35 pass

36

37

38@configdataclass

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

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

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

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