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

4Labjack Device for hvl_ccb. 

5Originally developed and tested for LabJack T7-PRO. 

6 

7Makes use of the LabJack LJM Library Python wrapper. 

8This wrapper needs an installation of the LJM Library for Windows, Mac OS X or Linux. 

9Go to: 

10https://labjack.com/support/software/installers/ljm 

11and 

12https://labjack.com/support/software/examples/ljm/python 

13""" 

14from __future__ import annotations 

15 

16import logging 

17from collections.abc import Sequence 

18from numbers import Real 

19from typing import Union, Optional, List, cast 

20 

21from aenum import Enum, IntEnum 

22 

23from .._dev import labjack 

24from ..comm import LJMCommunication 

25from ..dev import SingleCommDevice 

26from ..utils.enum import NameEnum, StrEnumBase 

27 

28 

29class LabJackError(Exception): 

30 """ 

31 Errors of the LabJack device. 

32 """ 

33 

34 pass 

35 

36 

37class LabJackIdentifierDIOError(Exception): 

38 """ 

39 Error indicating a wrong DIO identifier 

40 """ 

41 

42 pass 

43 

44 

45class LabJack(SingleCommDevice): 

46 """ 

47 LabJack Device. 

48 

49 This class is tested with a LabJack T7-Pro and should also work with T4 and T7 

50 devices communicating through the LJM Library. Other or older hardware versions and 

51 variants of LabJack devices are not supported. 

52 """ 

53 

54 DeviceType = labjack.DeviceType 

55 """ 

56 LabJack device types. 

57 """ 

58 

59 def __init__(self, com, dev_config=None) -> None: 

60 """ 

61 Constructor for a LabJack Device. 

62 

63 :param com: Communication protocol object of type 

64 LJMCommunication. If a configuration (dict or configdataclass) is given, 

65 a new communication protocol object will be instantiated. 

66 :param dev_config: There is no device configuration for LabJack yet. 

67 """ 

68 super().__init__(com, dev_config) 

69 

70 # cached device type 

71 self._device_type: Optional[labjack.DeviceType] = None 

72 

73 @staticmethod 

74 def default_com_cls(): 

75 return LJMCommunication 

76 

77 def start(self) -> None: 

78 """ 

79 Start the Device. 

80 """ 

81 

82 logging.info(f"Starting device {str(self)}") 

83 super().start() 

84 

85 def stop(self) -> None: 

86 """ 

87 Stop the Device. 

88 """ 

89 

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

91 super().stop() 

92 

93 def _read_float(self, *names: str) -> Union[float, Sequence[float]]: 

94 """ 

95 Read a numeric value. 

96 

97 :param name: name to read via communication protocol 

98 :return: read numeric value 

99 """ 

100 return self.com.read_name(*names, return_num_type=float) 

101 

102 def _read_int(self, *names: str) -> Union[int, Sequence[int]]: 

103 """ 

104 Read an integer value. 

105 

106 :param name: name to read via communication protocol 

107 :return: read integer value 

108 """ 

109 return self.com.read_name(*names, return_num_type=int) 

110 

111 def get_serial_number(self) -> int: 

112 """ 

113 Returns the serial number of the connected LabJack. 

114 

115 :return: Serial number. 

116 """ 

117 

118 return cast(int, self._read_int("SERIAL_NUMBER")) 

119 

120 def get_sbus_temp(self, number: int) -> float: 

121 """ 

122 Read the temperature value from a serial SBUS sensor. 

123 

124 :param number: port number (0..22) 

125 :return: temperature in Kelvin 

126 """ 

127 

128 return cast(float, self._read_float(f"SBUS{number}_TEMP")) 

129 

130 def get_sbus_rh(self, number: int) -> float: 

131 """ 

132 Read the relative humidity value from a serial SBUS sensor. 

133 

134 :param number: port number (0..22) 

135 :return: relative humidity in %RH 

136 """ 

137 

138 return cast(float, self._read_float(f"SBUS{number}_RH")) 

139 

140 class AInRange(StrEnumBase): 

141 _init_ = "value_str" 

142 TEN = "10" 

143 ONE = "1" 

144 ONE_TENTH = "0.1" 

145 ONE_HUNDREDTH = "0.01" 

146 

147 def __str__(self) -> str: 

148 return self.value_str 

149 

150 @property 

151 def value(self) -> float: 

152 return float(self.value_str) 

153 

154 def get_ain(self, *channels: int) -> Union[float, Sequence[float]]: 

155 """ 

156 Read currently measured value (voltage, resistance, ...) from one or more 

157 of analog inputs. 

158 

159 :param channels: AIN number or numbers (0..254) 

160 :return: the read value (voltage, resistance, ...) as `float`or `tuple` of 

161 them in case multiple channels given 

162 """ 

163 ch_str = [f"AIN{ch}" for ch in channels] 

164 return self._read_float(*ch_str) 

165 

166 def set_ain_range(self, channel: int, vrange: Union[Real, AInRange]) -> None: 

167 """ 

168 Set the range of an analog input port. 

169 

170 :param channel: is the AIN number (0..254) 

171 :param vrange: is the voltage range to be set 

172 """ 

173 vrange = self.AInRange(str(vrange)) 

174 self.com.write_name(f"AIN{channel}_RANGE", vrange.value) 

175 

176 def set_ain_resolution(self, channel: int, resolution: int) -> None: 

177 """ 

178 Set the resolution index of an analog input port. 

179 

180 :param channel: is the AIN number (0..254) 

181 :param resolution: is the resolution index within 

182 0...`get_product_type().ain_max_resolution` range; 0 will set the 

183 resolution index to default value. 

184 """ 

185 

186 ain_max_resolution = self.get_product_type().ain_max_resolution # type: ignore 

187 if resolution not in range(ain_max_resolution + 1): 

188 raise LabJackError(f"Not supported resolution index: {resolution}") 

189 

190 self.com.write_name(f"AIN{channel}_RESOLUTION_INDEX", resolution) 

191 

192 def set_ain_differential(self, pos_channel: int, differential: bool) -> None: 

193 """ 

194 Sets an analog input to differential mode or not. 

195 T7-specific: For base differential channels, positive must be even channel 

196 from 0-12 and negative must be positive+1. For extended channels 16-127, 

197 see Mux80 datasheet. 

198 

199 :param pos_channel: is the AIN number (0..12) 

200 :param differential: True or False 

201 :raises LabJackError: if parameters are unsupported 

202 """ 

203 

204 if pos_channel not in range(13): 

205 raise LabJackError(f"Not supported pos_channel: {pos_channel}") 

206 

207 if pos_channel % 2 != 0: 

208 raise LabJackError( 

209 f"AIN pos_channel for positive part of differential pair" 

210 f" must be even: {pos_channel}" 

211 ) 

212 

213 neg_channel = pos_channel + 1 

214 

215 self.com.write_name( 

216 f"AIN{pos_channel}_NEGATIVE_CH", neg_channel if differential else 199 

217 ) 

218 

219 class ThermocoupleType(NameEnum): 

220 """ 

221 Thermocouple type; NONE means disable thermocouple mode. 

222 """ 

223 

224 _init_ = "ef_index" 

225 NONE = 0 

226 E = 20 

227 J = 21 

228 K = 22 

229 R = 23 

230 T = 24 

231 S = 25 

232 C = 30 

233 PT100 = 40 

234 PT500 = 41 

235 PT1000 = 42 

236 

237 class CjcType(NameEnum): 

238 """ 

239 CJC slope and offset 

240 """ 

241 

242 _init_ = "slope offset" 

243 internal = 1, 0 

244 lm34 = 55.56, 255.37 

245 

246 class TemperatureUnit(NameEnum): 

247 """ 

248 Temperature unit (to be returned) 

249 """ 

250 

251 _init_ = "ef_config_a" 

252 K = 0 

253 C = 1 

254 F = 2 

255 

256 def set_ain_thermocouple( 

257 self, 

258 pos_channel: int, 

259 thermocouple: Union[None, str, ThermocoupleType], 

260 cjc_address: int = 60050, 

261 cjc_type: Union[str, CjcType] = ( 

262 CjcType.internal # type: ignore 

263 ), 

264 vrange: Union[Real, AInRange] = ( 

265 AInRange.ONE_HUNDREDTH # type: ignore 

266 ), 

267 resolution: int = 10, 

268 unit: Union[str, TemperatureUnit] = ( 

269 TemperatureUnit.K # type: ignore 

270 ), 

271 ) -> None: 

272 """ 

273 Set the analog input channel to thermocouple mode. 

274 

275 :param pos_channel: is the analog input channel of the positive part of the 

276 differential pair 

277 :param thermocouple: None to disable thermocouple mode, or string specifying 

278 the thermocouple type 

279 :param cjc_address: modbus register address to read the CJC temperature 

280 :param cjc_type: determines cjc slope and offset, 'internal' or 'lm34' 

281 :param vrange: measurement voltage range 

282 :param resolution: resolution index (T7-Pro: 0-12) 

283 :param unit: is the temperature unit to be returned ('K', 'C' or 'F') 

284 :raises LabJackError: if parameters are unsupported 

285 """ 

286 

287 if thermocouple is None: 

288 thermocouple = self.ThermocoupleType.NONE # type: ignore 

289 

290 thermocouple = self.ThermocoupleType(thermocouple) 

291 

292 # validate separately from `set_ain_range` to fail before any write happens 

293 # (in `set_ain_differential` first) 

294 vrange = self.AInRange(str(vrange)) 

295 

296 unit = self.TemperatureUnit(unit) 

297 

298 cjc_type = self.CjcType(cjc_type) 

299 

300 self.set_ain_differential(pos_channel=pos_channel, differential=True) 

301 self.set_ain_range(pos_channel, vrange) 

302 self.set_ain_resolution(pos_channel, resolution) 

303 self.set_ain_range(pos_channel + 1, vrange) 

304 self.set_ain_resolution(pos_channel + 1, resolution) 

305 

306 # specify thermocouple mode 

307 self.com.write_name(f"AIN{pos_channel}_EF_INDEX", thermocouple.ef_index) 

308 

309 # specify the units for AIN#_EF_READ_A and AIN#_EF_READ_C (0 = K, 1 = C, 2 = F) 

310 self.com.write_name(f"AIN{pos_channel}_EF_CONFIG_A", unit.ef_config_a) 

311 

312 # specify modbus address for cold junction reading CJC 

313 self.com.write_name(f"AIN{pos_channel}_EF_CONFIG_B", cjc_address) 

314 

315 # set slope for the CJC reading, typically 1 

316 self.com.write_name(f"AIN{pos_channel}_EF_CONFIG_D", cjc_type.slope) 

317 

318 # set the offset for the CJC reading, typically 0 

319 self.com.write_name(f"AIN{pos_channel}_EF_CONFIG_E", cjc_type.offset) 

320 

321 def read_thermocouple(self, pos_channel: int) -> float: 

322 """ 

323 Read the temperature of a connected thermocouple. 

324 

325 :param pos_channel: is the AIN number of the positive pin 

326 :return: temperature in specified unit 

327 """ 

328 

329 return round(cast(float, self._read_float(f"AIN{pos_channel}_EF_READ_A")), 2) 

330 

331 class DIOStatus(IntEnum): 

332 """ 

333 State of a digital I/O channel. 

334 """ 

335 

336 LOW = 0 

337 HIGH = 1 

338 

339 def set_digital_output(self, address: str, state: Union[int, DIOStatus]) -> None: 

340 """ 

341 Set the value of a digital output. 

342 

343 :param address: name of the output -> `'FIO0'` 

344 :param state: state of the output -> `DIOStatus` instance or corresponding `int` 

345 value 

346 """ 

347 dt = self.get_product_type() 

348 if address not in ( 

349 dt.dio # type: ignore 

350 ): 

351 raise LabJackIdentifierDIOError 

352 state = self.DIOStatus(state) 

353 self.com.write_name(address, state) 

354 

355 DIOChannel = labjack.TSeriesDIOChannel 

356 

357 def get_digital_input( 

358 self, address: Union[str, labjack.TSeriesDIOChannel] 

359 ) -> LabJack.DIOStatus: 

360 """ 

361 Get the value of a digital input. 

362 

363 allowed names for T7 (Pro): FIO0 - FIO7, EIO0 - EIO 7, CIO0- CIO3, MIO0 - MIO2 

364 :param address: name of the output -> 'FIO0' 

365 :return: HIGH when `address` DIO is high, and LOW when `address` DIO is low 

366 """ 

367 if not isinstance(address, self.DIOChannel): 

368 address = self.DIOChannel(address) 

369 dt = self.get_product_type() 

370 if address not in ( 

371 dt.dio # type: ignore 

372 ): 

373 dt_name = dt.name # type: ignore 

374 raise LabJackIdentifierDIOError( 

375 f"DIO {address.name} is not available for this device type: {dt_name}." 

376 ) 

377 try: 

378 ret = self._read_int(address.name) 

379 return self.DIOStatus(ret) 

380 except ValueError: 

381 raise LabJackIdentifierDIOError(f"Expected 0 or 1 return value, got {ret}.") 

382 

383 class CalMicroAmpere(Enum): 

384 """ 

385 Pre-defined microampere (uA) values for calibration current source query. 

386 """ 

387 

388 _init_ = "value current_source_query" 

389 TEN = "10uA", "CURRENT_SOURCE_10UA_CAL_VALUE" 

390 TWO_HUNDRED = "200uA", "CURRENT_SOURCE_200UA_CAL_VALUE" 

391 

392 def get_cal_current_source(self, name: Union[str, CalMicroAmpere]) -> float: 

393 """ 

394 This function will return the calibration of the chosen current source, 

395 this ist not a measurement! 

396 

397 The value was stored during fabrication. 

398 

399 :param name: '200uA' or '10uA' current source 

400 :return: calibration of the chosen current source in ampere 

401 """ 

402 if not isinstance(name, self.CalMicroAmpere): 

403 name = self.CalMicroAmpere(name) 

404 return cast(float, self._read_float(name.current_source_query)) 

405 

406 def get_product_id(self) -> int: 

407 """ 

408 This function returns the product ID reported by the connected device. 

409 

410 Attention: returns `7` for both T7 and T7-Pro devices! 

411 

412 :return: integer product ID of the device 

413 """ 

414 return cast(int, self._read_int("PRODUCT_ID")) 

415 

416 def get_product_type(self, force_query_id: bool = False) -> labjack.DeviceType: 

417 """ 

418 This function will return the device type based on reported device type and 

419 in case of unambiguity based on configuration of device's communication 

420 protocol (e.g. for "T7" and "T7_PRO" devices), or, if not available first 

421 matching. 

422 

423 

424 :param force_query_id: boolean flag to force `get_product_id` query to device 

425 instead of using cached device type from previous queries. 

426 :return: `DeviceType` instance 

427 :raises LabJackIdentifierDIOError: when read Product ID is unknown 

428 """ 

429 if force_query_id or not self._device_type: 

430 try: 

431 device_type_or_list = self.DeviceType.get_by_p_id(self.get_product_id()) 

432 except ValueError as e: 

433 raise LabJackIdentifierDIOError from e 

434 if isinstance(device_type_or_list, self.DeviceType): 

435 device_type = device_type_or_list 

436 else: # isinstance(device_type_or_list, list): 

437 device_type_list: List[labjack.DeviceType] = device_type_or_list 

438 # can be None in case a non-default com or its config was used 

439 conf_device_type = getattr(self.com.config, "device_type", None) 

440 if conf_device_type: 

441 if conf_device_type not in device_type_list: 

442 raise LabJackIdentifierDIOError( 

443 f"Configured devices type {conf_device_type!s} does not " 

444 f"match any of the unambiguously reported device types: " 

445 f"{','.join(str(dt) for dt in device_type_list)}." 

446 ) 

447 device_type = conf_device_type 

448 else: 

449 device_type = device_type_list[0] 

450 self._device_type = device_type 

451 return self._device_type 

452 

453 def get_product_name(self, force_query_id=False) -> str: 

454 """ 

455 This function will return the product name based on product ID reported by 

456 the device. 

457 

458 Attention: returns "T7" for both T7 and T7-Pro devices! 

459 

460 :param force_query_id: boolean flag to force `get_product_id` query to device 

461 instead of using cached device type from previous queries. 

462 :return: device name string, compatible with `LabJack.DeviceType` 

463 """ 

464 return self.get_product_type(force_query_id=force_query_id).name 

465 

466 def set_ain_resistance( 

467 self, channel: int, vrange: Union[Real, AInRange], resolution: int 

468 ) -> None: 

469 """ 

470 Set the specified channel to resistance mode. It utilized the 200uA current 

471 source of the LabJack. 

472 

473 :param channel: channel that should measure the resistance 

474 :param vrange: voltage range of the channel 

475 :param resolution: resolution index of the channel T4: 0-5, T7: 0-8, T7-Pro 0-12 

476 """ 

477 self.set_ain_range(channel, vrange) 

478 self.set_ain_resolution(channel, resolution) 

479 

480 # resistance mode 

481 self.com.write_name(f"AIN{channel}_EF_INDEX", 4) 

482 # excitation with 200uA current source 

483 self.com.write_name(f"AIN{channel}_EF_CONFIG_B", 0) 

484 

485 def read_resistance(self, channel: int) -> float: 

486 """ 

487 Read resistance from specified channel. 

488 

489 :param channel: channel with resistor 

490 :return: resistance value with 2 decimal places 

491 """ 

492 return round(cast(float, self._read_float(f"AIN{channel}_EF_READ_A")), 2)