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

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.
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
16import logging
17from collections.abc import Sequence
18from numbers import Real
19from typing import Union, Optional, List, cast
21from aenum import Enum, IntEnum
23from .._dev import labjack
24from ..comm import LJMCommunication
25from ..dev import SingleCommDevice
26from ..utils.enum import NameEnum, StrEnumBase
29class LabJackError(Exception):
30 """
31 Errors of the LabJack device.
32 """
34 pass
37class LabJackIdentifierDIOError(Exception):
38 """
39 Error indicating a wrong DIO identifier
40 """
42 pass
45class LabJack(SingleCommDevice):
46 """
47 LabJack Device.
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 """
54 DeviceType = labjack.DeviceType
55 """
56 LabJack device types.
57 """
59 def __init__(self, com, dev_config=None) -> None:
60 """
61 Constructor for a LabJack Device.
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)
70 # cached device type
71 self._device_type: Optional[labjack.DeviceType] = None
73 @staticmethod
74 def default_com_cls():
75 return LJMCommunication
77 def start(self) -> None:
78 """
79 Start the Device.
80 """
82 logging.info(f"Starting device {str(self)}")
83 super().start()
85 def stop(self) -> None:
86 """
87 Stop the Device.
88 """
90 logging.info(f"Stopping device {str(self)}")
91 super().stop()
93 def _read_float(self, *names: str) -> Union[float, Sequence[float]]:
94 """
95 Read a numeric value.
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)
102 def _read_int(self, *names: str) -> Union[int, Sequence[int]]:
103 """
104 Read an integer value.
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)
111 def get_serial_number(self) -> int:
112 """
113 Returns the serial number of the connected LabJack.
115 :return: Serial number.
116 """
118 return cast(int, self._read_int("SERIAL_NUMBER"))
120 def get_sbus_temp(self, number: int) -> float:
121 """
122 Read the temperature value from a serial SBUS sensor.
124 :param number: port number (0..22)
125 :return: temperature in Kelvin
126 """
128 return cast(float, self._read_float(f"SBUS{number}_TEMP"))
130 def get_sbus_rh(self, number: int) -> float:
131 """
132 Read the relative humidity value from a serial SBUS sensor.
134 :param number: port number (0..22)
135 :return: relative humidity in %RH
136 """
138 return cast(float, self._read_float(f"SBUS{number}_RH"))
140 class AInRange(StrEnumBase):
141 _init_ = "value_str"
142 TEN = "10"
143 ONE = "1"
144 ONE_TENTH = "0.1"
145 ONE_HUNDREDTH = "0.01"
147 def __str__(self) -> str:
148 return self.value_str
150 @property
151 def value(self) -> float:
152 return float(self.value_str)
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.
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)
166 def set_ain_range(self, channel: int, vrange: Union[Real, AInRange]) -> None:
167 """
168 Set the range of an analog input port.
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)
176 def set_ain_resolution(self, channel: int, resolution: int) -> None:
177 """
178 Set the resolution index of an analog input port.
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 """
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}")
190 self.com.write_name(f"AIN{channel}_RESOLUTION_INDEX", resolution)
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.
199 :param pos_channel: is the AIN number (0..12)
200 :param differential: True or False
201 :raises LabJackError: if parameters are unsupported
202 """
204 if pos_channel not in range(13):
205 raise LabJackError(f"Not supported pos_channel: {pos_channel}")
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 )
213 neg_channel = pos_channel + 1
215 self.com.write_name(
216 f"AIN{pos_channel}_NEGATIVE_CH", neg_channel if differential else 199
217 )
219 class ThermocoupleType(NameEnum):
220 """
221 Thermocouple type; NONE means disable thermocouple mode.
222 """
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
237 class CjcType(NameEnum):
238 """
239 CJC slope and offset
240 """
242 _init_ = "slope offset"
243 internal = 1, 0
244 lm34 = 55.56, 255.37
246 class TemperatureUnit(NameEnum):
247 """
248 Temperature unit (to be returned)
249 """
251 _init_ = "ef_config_a"
252 K = 0
253 C = 1
254 F = 2
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.
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 """
287 if thermocouple is None:
288 thermocouple = self.ThermocoupleType.NONE # type: ignore
290 thermocouple = self.ThermocoupleType(thermocouple)
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))
296 unit = self.TemperatureUnit(unit)
298 cjc_type = self.CjcType(cjc_type)
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)
306 # specify thermocouple mode
307 self.com.write_name(f"AIN{pos_channel}_EF_INDEX", thermocouple.ef_index)
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)
312 # specify modbus address for cold junction reading CJC
313 self.com.write_name(f"AIN{pos_channel}_EF_CONFIG_B", cjc_address)
315 # set slope for the CJC reading, typically 1
316 self.com.write_name(f"AIN{pos_channel}_EF_CONFIG_D", cjc_type.slope)
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)
321 def read_thermocouple(self, pos_channel: int) -> float:
322 """
323 Read the temperature of a connected thermocouple.
325 :param pos_channel: is the AIN number of the positive pin
326 :return: temperature in specified unit
327 """
329 return round(cast(float, self._read_float(f"AIN{pos_channel}_EF_READ_A")), 2)
331 class DIOStatus(IntEnum):
332 """
333 State of a digital I/O channel.
334 """
336 LOW = 0
337 HIGH = 1
339 def set_digital_output(self, address: str, state: Union[int, DIOStatus]) -> None:
340 """
341 Set the value of a digital output.
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)
355 DIOChannel = labjack.TSeriesDIOChannel
357 def get_digital_input(
358 self, address: Union[str, labjack.TSeriesDIOChannel]
359 ) -> LabJack.DIOStatus:
360 """
361 Get the value of a digital input.
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}.")
383 class CalMicroAmpere(Enum):
384 """
385 Pre-defined microampere (uA) values for calibration current source query.
386 """
388 _init_ = "value current_source_query"
389 TEN = "10uA", "CURRENT_SOURCE_10UA_CAL_VALUE"
390 TWO_HUNDRED = "200uA", "CURRENT_SOURCE_200UA_CAL_VALUE"
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!
397 The value was stored during fabrication.
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))
406 def get_product_id(self) -> int:
407 """
408 This function returns the product ID reported by the connected device.
410 Attention: returns `7` for both T7 and T7-Pro devices!
412 :return: integer product ID of the device
413 """
414 return cast(int, self._read_int("PRODUCT_ID"))
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.
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
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.
458 Attention: returns "T7" for both T7 and T7-Pro devices!
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
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.
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)
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)
485 def read_resistance(self, channel: int) -> float:
486 """
487 Read resistance from specified channel.
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)