Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\comm\labjack_ljm.py : 35%

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"""
4Communication protocol for LabJack using the LJM Library.
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"""
15import logging
16from numbers import Real, Integral
17from typing import Union, Dict, Sequence, Type
19from labjack import ljm
21from .base import CommunicationProtocol
22from .._dev import labjack
23from ..configuration import configdataclass
24from ..utils.enum import AutoNumberNameEnum
27class LJMCommunicationError(Exception):
28 """
29 Errors coming from LJMCommunication.
30 """
32 pass
35@configdataclass
36class LJMCommunicationConfig:
37 """
38 Configuration dataclass for :class:`LJMCommunication`.
39 """
41 DeviceType = labjack.DeviceType
43 #: Can be either string 'ANY', 'T7_PRO', 'T7', 'T4', or of enum :class:`DeviceType`.
44 device_type: Union[str, labjack.DeviceType] = "ANY"
46 class ConnectionType(AutoNumberNameEnum):
47 """
48 LabJack connection type.
49 """
51 ANY = ()
52 USB = ()
53 TCP = ()
54 ETHERNET = ()
55 WIFI = ()
57 #: Can be either string or of enum :class:`ConnectionType`.
58 connection_type: Union[str, ConnectionType] = "ANY"
60 identifier: str = "ANY"
61 """
62 The identifier specifies information for the connection to be used. This can
63 be an IP address, serial number, or device name. See the LabJack docs (
64 https://labjack.com/support/software/api/ljm/function-reference/ljmopens/\
65identifier-parameter) for more information.
66 """
68 def clean_values(self) -> None:
69 """
70 Performs value checks on device_type and connection_type.
71 """
72 if not isinstance(self.device_type, self.DeviceType):
73 self.force_value( # type: ignore
74 "device_type", self.DeviceType(self.device_type)
75 )
77 if not isinstance(self.connection_type, self.ConnectionType):
78 self.force_value( # type: ignore
79 "connection_type", self.ConnectionType(self.connection_type)
80 )
83class LJMCommunication(CommunicationProtocol):
84 """
85 Communication protocol implementing the LabJack LJM Library Python wrapper.
86 """
88 def __init__(self, configuration) -> None:
89 """
90 Constructor for LJMCommunication.
91 """
92 super().__init__(configuration)
94 # reference to the ctypes handle
95 self._handle = None
97 self.logger = logging.getLogger(__name__)
99 @staticmethod
100 def config_cls():
101 return LJMCommunicationConfig
103 def open(self) -> None:
104 """
105 Open the communication port.
106 """
108 self.logger.info("Open connection")
110 # open connection and store handle
111 # may throw 1227 LJME_DEVICE_NOT_FOUND if device is not found
112 try:
113 with self.access_lock:
114 self._handle = ljm.openS(
115 self.config.device_type.type_str,
116 str(self.config.connection_type),
117 str(self.config.identifier),
118 )
119 except ljm.LJMError as e:
120 self.logger.error(e)
121 # only catch "1229 LJME_DEVICE_ALREADY_OPEN", never observed
122 if e.errorCode != 1229:
123 raise LJMCommunicationError from e
125 def close(self) -> None:
126 """
127 Close the communication port.
128 """
130 self.logger.info("Closing connection")
132 try:
133 with self.access_lock:
134 ljm.close(self._handle)
135 except ljm.LJMError as e:
136 self.logger.error(e)
137 # only catch "1224 LJME_DEVICE_NOT_OPEN", thrown on invalid handle
138 if e.errorCode != 1224:
139 raise LJMCommunicationError from e
140 self._handle = None
142 @property
143 def is_open(self) -> bool:
144 """
145 Flag indicating if the communication port is open.
147 :return: `True` if the port is open, otherwise `False`
148 """
149 # getHandleInfo does not work with LJM DEMO_MODE - consider it always opened
150 # if only set
151 if str(self._handle) == labjack.constants.DEMO_MODE:
152 return True
154 try:
155 ljm.getHandleInfo(self._handle)
156 except ljm.LJMError as e:
157 if e.errorCode == 1224: # "1224 LJME_DEVICE_NOT_OPEN"
158 return False
159 raise LJMCommunicationError from e
160 return True
162 def __del__(self) -> None:
163 """
164 Finalizer, closes port
165 """
167 self.close()
169 @staticmethod
170 def _cast_read_value(
171 name: str,
172 val: Real,
173 return_num_type: Type[Real] = float, # type: ignore
174 # see: https://github.com/python/mypy/issues/3186
175 ) -> Real:
176 """
177 Cast a read value to a numeric type, performing some extra cast validity checks.
179 :param name: name of the read value, only for error reporting
180 :param val: value to cast
181 :param return_num_type: optional numeric type specification for return values;
182 by default `float`
183 :return: input value `val` casted to `return_num_type`
184 :raises TypeError: if read value of type not compatible with `return_num_type`
185 """
186 # Note: the underlying library returns already `float` (or
187 # `ctypes.c_double`?); but defensively cast again via `str`:
188 # 1) in case the underlying lib behaviour changes, and
189 # 2) to raise `TypeError` when got non integer `float` value and expecting
190 # `int` value
191 invalid_value_type = False
192 try:
193 fval = float(str(val))
194 if issubclass(return_num_type, Integral) and not fval.is_integer():
195 invalid_value_type = True
196 else:
197 ret = return_num_type(fval) # type: ignore
198 except ValueError:
199 invalid_value_type = True
200 if invalid_value_type:
201 raise TypeError(
202 f"Expected {return_num_type} value for '{name}' "
203 f"name, got {type(val)} value of {val}"
204 )
205 return ret
207 def read_name(
208 self,
209 *names: str,
210 return_num_type: Type[Real] = float, # type: ignore
211 # see: https://github.com/python/mypy/issues/3186
212 ) -> Union[Real, Sequence[Real]]:
213 """
214 Read one or more input numeric values by name.
216 :param names: one or more names to read out from the LabJack
217 :param return_num_type: optional numeric type specification for return values;
218 by default `float`.
219 :return: answer of the LabJack, either single number or multiple numbers in a
220 sequence, respectively, when one or multiple names to read were given
221 :raises TypeError: if read value of type not compatible with `return_num_type`
222 """
224 # Errors that can be returned here:
225 # 1224 LJME_DEVICE_NOT_OPEN if the device is not open
226 # 1239 LJME_DEVICE_RECONNECT_FAILED if the device was opened, but connection
227 # lost
229 with self.access_lock:
230 try:
231 if len(names) == 1:
232 ret = ljm.eReadName(self._handle, names[0])
233 ret = self._cast_read_value(
234 names[0], ret, return_num_type=return_num_type)
235 else:
236 ret = ljm.eReadNames(self._handle, len(names), names)
237 for (i, (iname, iret)) in enumerate(zip(names, ret)):
238 ret[i] = self._cast_read_value(
239 iname, iret, return_num_type=return_num_type)
240 except ljm.LJMError as e:
241 self.logger.error(e)
242 raise LJMCommunicationError from e
244 return ret
246 def write_name(self, name: str, value: Real) -> None:
247 """
248 Write one value to a named output.
250 :param name: String or with name of LabJack IO
251 :param value: is the value to write to the named IO port
252 """
254 with self.access_lock:
255 try:
256 ljm.eWriteName(self._handle, name, value)
257 except ljm.LJMError as e:
258 self.logger.error(e)
259 raise LJMCommunicationError from e
261 def write_names(self, name_value_dict: Dict[str, Real]) -> None:
262 """
263 Write more than one value at once to named outputs.
265 :param name_value_dict: is a dictionary with string names of LabJack IO as keys
266 and corresponding numeric values
267 """
268 names = list(name_value_dict.keys())
269 values = list(name_value_dict.values())
270 with self.access_lock:
271 try:
272 ljm.eWriteNames(self._handle, len(names), names, values)
273 except ljm.LJMError as e:
274 self.logger.error(e)
275 raise LJMCommunicationError from e
277 # def write_address(self, address: int, value: Real) -> None:
278 # """
279 # **NOT IMPLEMENTED.**
280 # Write one or more values to Modbus addresses.
281 #
282 # :param address: One or more Modbus address on the LabJack.
283 # :param value: One or more values to be written to the addresses.
284 # """
285 #
286 # raise NotImplementedError
287 # # TODO: Implement function to write on addresses. Problem so far: I also need
288 # # to bring in the data types (INT32, FLOAT32...)