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

4Communication protocol for LabJack using the LJM Library. 

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

14 

15import logging 

16from numbers import Real, Integral 

17from typing import Union, Dict, Sequence, Type 

18 

19from labjack import ljm 

20 

21from .base import CommunicationProtocol 

22from .._dev import labjack 

23from ..configuration import configdataclass 

24from ..utils.enum import AutoNumberNameEnum 

25 

26 

27class LJMCommunicationError(Exception): 

28 """ 

29 Errors coming from LJMCommunication. 

30 """ 

31 

32 pass 

33 

34 

35@configdataclass 

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

65identifier-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 

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