Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\labjack.py: 48%

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

## 167 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 Labjack Device for hvl_ccb.

5 Originally developed and tested for LabJack T7-PRO.

6

7 Makes use of the LabJack LJM Library Python wrapper.

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

9 Go to:

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

11 and

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

13"""

14 from __future__ import annotations

15

16 import logging

17 from collections.abc import Sequence

18 from numbers import Real

19 from typing import Union, Optional, List, cast

20

21 from aenum import Enum, IntEnum

22

23 from .._dev import labjack

24 from ..comm import LJMCommunication

25 from ..dev import SingleCommDevice

26 from ..utils.enum import NameEnum, StrEnumBase

27

28

29 class LabJackError(Exception):

30 """

31 Errors of the LabJack device.

32 """

33

34 pass

35

36

37 class LabJackIdentifierDIOError(Exception):

38 """

39 Error indicating a wrong DIO identifier

40 """

41

42 pass

43

44

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