Coverage for src/cgse_dummy/dummy_dev.py: 54%

1"""

2 The device driver for the dummy device.

3

4 The device driver has an Ethernet interface that listens to the port specified in the Settings file in the section

5'DUMMY DEVICE'.

6

7"""

8

9 __all__ = [

10 "Dummy",

11]

12

13 import logging

14 import socket

15 import time

16

17 from egse.device import DeviceConnectionError

18 from egse.device import DeviceConnectionInterface

19 from egse.device import DeviceError

20 from egse.device import DeviceTimeoutError

21 from egse.device import DeviceTransport

22 from egse.settings import Settings

23

24 _VERSION = "0.0.1"

25

26 logger = logging.getLogger("egse.dummy")

27

28

29 device_settings = Settings.load("DUMMY DEVICE")

30 cs_settings = Settings.load("DUMMY CS")

31

32 IDENTIFICATION_QUERY = "*IDN?"

33"""SCPI command to request device identification."""

34

35 READ_TIMEOUT = device_settings.TIMEOUT

36"""The timeout when reading from a socket [s] (can be smaller than the timeout of the Proxy, e.g. 1s)"""

37

38

39 class Dummy(DeviceConnectionInterface, DeviceTransport):

40 """

41 Defines the low-level interface to the DUMMY Instruments DAQ-1234 Simulator.

42

43 Args:

44 - hostname (str): the IP address or hostname of the device, if None, this is taken from the Settings

45 - port (int): the port number for the device connection, if None, this will be taken from the Settings

46

47 """

48

49 def __init__(self, hostname: str = None, port: int = None):

50 """Initialisation of an Ethernet interface for the DAQ-1234.

51

52 Args:

53 hostname(str): Hostname to which to open a socket

54 port (int): Port to which to open a socket

55 """

56

57 super().__init__()

58

59 self.hostname = device_settings.HOSTNAME if hostname is None else hostname

60 self.port = device_settings.PORT if port is None else port

61 self.sock = None

62

63 self.name = device_settings.MODEL

64

65 self.is_connection_open = False

66

67 def __enter__(self):

68 self.connect()

69 return self

70

71 def __exit__(self, exc_type, exc_val, exc_tb):

72 self.disconnect()

73

74 def connect(self) -> None:

75 """

76 Connects the device.

77

78 If the connection is already open, a warning will be issued and the function returns.

79

80 Raises:

81 DeviceConnectionError: When the connection could not be established. Check the logging messages for more

82 details.

83

84 DeviceTimeoutError: When the connection timed out.

85

86 ValueError: When hostname or port number are not provided.

87 """

88

89 # Sanity checks

90

91 if self.is_connection_open: 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true

92 logger.warning(

93 f"{device_settings.MODEL}: trying to connect to an already connected socket."

94 )

95 return

96

97 if self.hostname in (None, ""): 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true

98 raise ValueError(f"{device_settings.MODEL}: hostname is not initialized.")

99

100 if self.port in (None, 0): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 raise ValueError(

102 f"{device_settings.MODEL}: port number is not initialized."

103 )

104

105 # Create a new socket instance

106

107 try:

108 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

109 # The following lines are to experiment with blocking and timeout, but there is no need.

110 # self.sock.setblocking(1)

111 # self.sock.settimeout(3)

112 except socket.error as e_socket:

113 raise DeviceConnectionError(

114 device_settings.MODEL, "Failed to create socket."

115 ) from e_socket

116

117 # Attempt to establish a connection to the remote host

118

119 # FIXME: Socket shall be closed on exception?

120

121 # We set a timeout of 3s before connecting and reset to None (=blocking) after the `connect` method has been

122 # called. This is because when no device is available, e.g. during testing, the timeout will take about

123 # two minutes, which is way too long. It needs to be evaluated if this approach is acceptable and not causing

124 # problems during production.

125

126 try:

127 logger.debug(

128 f'Connecting a socket to host "{self.hostname}" using port {self.port}'

129 )

130 self.sock.settimeout(3)

131 self.sock.connect((self.hostname, self.port))

132 self.sock.settimeout(None)

133 except ConnectionRefusedError as exc:

134 raise DeviceConnectionError(

135 device_settings.MODEL,

136 f"Connection refused to {self.hostname}:{self.port}.",

137 ) from exc

138 except TimeoutError as exc:

139 raise DeviceTimeoutError(

140 device_settings.MODEL,

141 f"Connection to {self.hostname}:{self.port} timed out.",

142 ) from exc

143 except socket.gaierror as exc:

144 raise DeviceConnectionError(

145 device_settings.MODEL, f"Socket address info error for {self.hostname}"

146 ) from exc

147 except socket.herror as exc:

148 raise DeviceConnectionError(

149 device_settings.MODEL, f"Socket host address error for {self.hostname}"

150 ) from exc

151 except OSError as exc:

152 raise DeviceConnectionError(

153 device_settings.MODEL, f"OSError caught ({exc})."

154 ) from exc

155

156 self.is_connection_open = True

157

158 # Check that we are connected to the controller by issuing the "VERSION" or

159 # "*ISDN?" query. If we don't get the right response, then disconnect automatically.

160

161 if not self.is_connected(): 161 ↛ exitline 161 didn't return from function 'connect' because the condition on line 161 was always true

162 raise DeviceConnectionError(

163 device_settings.MODEL,

164 "Device is not connected, check logging messages for the cause.",

165 )

166

167 def disconnect(self) -> None:

168 """

169 Disconnects from the Ethernet connection.

170

171 Raises:

172 DeviceConnectionError when the socket could not be closed.

173 """

174

175 try:

176 if self.is_connection_open: 176 ↛ exitline 176 didn't return from function 'disconnect' because the condition on line 176 was always true

177 logger.debug(f"Disconnecting from {self.hostname}")

178 self.sock.close()

179 self.is_connection_open = False

180 except Exception as e_exc:

181 raise DeviceConnectionError(

182 device_settings.MODEL, f"Could not close socket to {self.hostname}"

183 ) from e_exc

184

185 def reconnect(self):

186 """Reconnects to the device controller.

187

188 Raises:

189 ConnectionError when the device cannot be reconnected for some reason.

190 """

191

192 if self.is_connection_open:

193 self.disconnect()

194 self.connect()

195

196 def is_connected(self) -> bool:

197 """

198 Checks if the device is connected.

199

200 This will send a query for the device identification and validate the answer.

201

202 Returns:

203 True is the device is connected and answered with the proper IDN; False otherwise.

204 """

205

206 # FIXME: This should only return the self.is_connection_open flag. Hanfdling of connection state shall be done

207 # by properly handling exceptions.

208

209 if not self.is_connection_open: 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true

210 return False

211

212 try:

213 idn = self.query(IDENTIFICATION_QUERY).decode()

214 except DeviceError as exc:

215 logger.exception(exc)

216 logger.error(

217 "Most probably the client connection was closed. Disconnecting..."

218 )

219 self.disconnect()

220 return False

221

222 if device_settings.MODEL not in idn: 222 ↛ 230line 222 didn't jump to line 230 because the condition on line 222 was always true

223 logger.error(

224 f'Device did not respond correctly to a "{IDENTIFICATION_QUERY}" command, response={idn}. '

225 f"Disconnecting..."

226 )

227 self.disconnect()

228 return False

229

230 return True

231

232 def write(self, command: str):

233 """

234 Sends a single command to the device controller without waiting for a response.

235

236 Args:

237 command (str): Command to send to the controller

238

239 Raises:

240 DeviceConnectionError when the command could not be sent due to a communication problem.

241 DeviceTimeoutError when the command could not be sent due to a timeout.

242 """

243

244 try:

245 command += "\n" if not command.endswith("\n") else ""

246

247 self.sock.sendall(command.encode())

248

249 except socket.timeout as e_timeout:

250 raise DeviceTimeoutError(

251 device_settings.MODEL, "Socket timeout error"

252 ) from e_timeout

253 except socket.error as e_socket:

254 # Interpret any socket-related error as a connection error

255 raise DeviceConnectionError(

256 device_settings.MODEL, "Socket communication error."

257 ) from e_socket

258 except AttributeError:

259 if not self.is_connection_open:

260 msg = "The DAQ6510 is not connected, use the connect() method."

261 raise DeviceConnectionError(device_settings.MODEL, msg)

262 raise

263

264 def trans(self, command: str) -> bytes:

265 """

266 Sends a single command to the device controller and block until a response from the controller.

267

268 This is seen as a transaction.

269

270 Args:

271 command (str): Command to send to the controller

272

273 Returns:

274 Either a bytes object returned by the controller (on success), or an error message (on failure).

275

276 Raises:

277 DeviceConnectionError when there was an I/O problem during communication with the controller.

278 DeviceTimeoutError when there was a timeout in either sending the command or receiving the response.

279 """

280

281 try:

282 # Attempt to send the complete command

283

284 command += "\n" if not command.endswith("\n") else ""

285

286 self.sock.sendall(command.encode())

287

288 # wait for, read and return the response from HUBER (will be at most TBD chars)

289

290 response = self.read()

291

292 return response

293

294 except ConnectionError as exc:

295 raise DeviceConnectionError(

296 device_settings.MODEL, "Connection error."

297 ) from exc

298 except socket.timeout as e_timeout:

299 raise DeviceTimeoutError(

300 device_settings.MODEL, "Socket timeout error"

301 ) from e_timeout

302 except socket.error as e_socket:

303 # Interpret any socket-related error as an I/O error

304 raise DeviceConnectionError(

305 device_settings.MODEL, "Socket communication error."

306 ) from e_socket

307 except AttributeError:

308 if not self.is_connection_open:

309 raise DeviceConnectionError(

310 device_settings.MODEL,

311 "Device not connected, use the connect() method.",

312 )

313 raise

314

315 def read(self) -> bytes:

316 """

317 Reads from the device buffer.

318

319 Returns:

320 The content of the device buffer.

321 """

322

323 n_total = 0

324 buf_size = 2048

325

326 data = b""

327

328 # Set a timeout of READ_TIMEOUT to the socket.recv

329

330 saved_timeout = self.sock.gettimeout()

331 self.sock.settimeout(READ_TIMEOUT)

332

333 try:

334 for idx in range(100): 334 ↛ 347line 334 didn't jump to line 347 because the loop on line 334 didn't complete

335 time.sleep(0.001) # Give the device time to fill the buffer

336 data = self.sock.recv(buf_size)

337 n = len(data)

338 n_total += n

339 if n < buf_size:

340 break

341 except TimeoutError as exc:

342 logger.warning(

343 f"Socket timeout error for {self.hostname}:{self.port}: {exc}"

344 )

345 return b"\r\n"

346 finally:

347 self.sock.settimeout(saved_timeout)

348

349 # logger.debug(f"Total number of bytes received is {n_total}, idx={idx}")

350

351 return data