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

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

## 110 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 Communication protocol for LabJack using the LJM Library.

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

15 import logging

16 from numbers import Real, Integral

17 from typing import Union, Dict, Sequence, Type

18

19 from labjack import ljm

20

21 from .base import CommunicationProtocol

22 from .._dev import labjack

23 from ..configuration import configdataclass

24 from ..utils.enum import AutoNumberNameEnum

25

26

27 class LJMCommunicationError(Exception):

28 """

29 Errors coming from LJMCommunication.

30 """

31

32 pass

33

34

35@configdataclass

36 class LJMCommunicationConfig:

37 """

38 Configuration dataclass for :class:`LJMCommunication`.

39 """

40

41 DeviceType = labjack.DeviceType

42

43 #: Can be either string 'ANY', 'T7_PRO', 'T7', 'T4', or of enum :class:`DeviceType`.

44 device_type: Union[str, labjack.DeviceType] = "ANY"

45

46 class ConnectionType(AutoNumberNameEnum):

47 """

48 LabJack connection type.

49 """

50

51 ANY = ()

52 USB = ()

53 TCP = ()

54 ETHERNET = ()

55 WIFI = ()

56

57 #: Can be either string or of enum :class:`ConnectionType`.

58 connection_type: Union[str, ConnectionType] = "ANY"

59

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/\

65 identifier-parameter) for more information.

66 """

67

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 )

76

77 if not isinstance(self.connection_type, self.ConnectionType):

78 self.force_value( # type: ignore

79 "connection_type", self.ConnectionType(self.connection_type)

80 )

81

82

83 class LJMCommunication(CommunicationProtocol):

84 """

85 Communication protocol implementing the LabJack LJM Library Python wrapper.

86 """

87

88 def __init__(self, configuration) -> None:

89 """

90 Constructor for LJMCommunication.

91 """

92 super().__init__(configuration)

93

94 # reference to the ctypes handle

95 self._handle = None

96

97 self.logger = logging.getLogger(__name__)

98

99 @staticmethod

100 def config_cls():

101 return LJMCommunicationConfig

102

103 def open(self) -> None:

104 """

105 Open the communication port.

106 """

107

108 self.logger.info("Open connection")

109

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

124

125 def close(self) -> None:

126 """

127 Close the communication port.

128 """

129

130 self.logger.info("Closing connection")

131

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

141

142 @property

143 def is_open(self) -> bool:

144 """

145 Flag indicating if the communication port is open.

146

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

153

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

161

162 def __del__(self) -> None:

163 """

164 Finalizer, closes port

165 """

166

167 self.close()

168

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.

178

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

206

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.

215

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

223

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

228

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

243

244 return ret

245

246 def write_name(self, name: str, value: Real) -> None:

247 """

248 Write one value to a named output.

249

250 :param name: String or with name of LabJack IO

251 :param value: is the value to write to the named IO port

252 """

253

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

260

261 def write_names(self, name_value_dict: Dict[str, Real]) -> None:

262 """

263 Write more than one value at once to named outputs.

264

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

276

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...)