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

136 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-15 11:57 +0200

1""" 

2The device driver for the dummy device. 

3 

4The 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 

13import logging 

14import socket 

15import time 

16 

17from egse.device import DeviceConnectionError 

18from egse.device import DeviceConnectionInterface 

19from egse.device import DeviceError 

20from egse.device import DeviceTimeoutError 

21from egse.device import DeviceTransport 

22from egse.settings import Settings 

23 

24_VERSION = "0.0.1" 

25 

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

27 

28 

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

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

31 

32IDENTIFICATION_QUERY = "*IDN?" 

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

34 

35READ_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 

39class 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