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
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 11:57 +0200
1"""
2The device driver for the dummy device.
4The device driver has an Ethernet interface that listens to the port specified in the Settings file in the section
5'DUMMY DEVICE'.
7"""
9__all__ = [
10 "Dummy",
11]
13import logging
14import socket
15import time
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
24_VERSION = "0.0.1"
26logger = logging.getLogger("egse.dummy")
29device_settings = Settings.load("DUMMY DEVICE")
30cs_settings = Settings.load("DUMMY CS")
32IDENTIFICATION_QUERY = "*IDN?"
33"""SCPI command to request device identification."""
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)"""
39class Dummy(DeviceConnectionInterface, DeviceTransport):
40 """
41 Defines the low-level interface to the DUMMY Instruments DAQ-1234 Simulator.
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
47 """
49 def __init__(self, hostname: str = None, port: int = None):
50 """Initialisation of an Ethernet interface for the DAQ-1234.
52 Args:
53 hostname(str): Hostname to which to open a socket
54 port (int): Port to which to open a socket
55 """
57 super().__init__()
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
63 self.name = device_settings.MODEL
65 self.is_connection_open = False
67 def __enter__(self):
68 self.connect()
69 return self
71 def __exit__(self, exc_type, exc_val, exc_tb):
72 self.disconnect()
74 def connect(self) -> None:
75 """
76 Connects the device.
78 If the connection is already open, a warning will be issued and the function returns.
80 Raises:
81 DeviceConnectionError: When the connection could not be established. Check the logging messages for more
82 details.
84 DeviceTimeoutError: When the connection timed out.
86 ValueError: When hostname or port number are not provided.
87 """
89 # Sanity checks
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
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.")
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 )
105 # Create a new socket instance
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
117 # Attempt to establish a connection to the remote host
119 # FIXME: Socket shall be closed on exception?
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.
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
156 self.is_connection_open = True
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.
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 )
167 def disconnect(self) -> None:
168 """
169 Disconnects from the Ethernet connection.
171 Raises:
172 DeviceConnectionError when the socket could not be closed.
173 """
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
185 def reconnect(self):
186 """Reconnects to the device controller.
188 Raises:
189 ConnectionError when the device cannot be reconnected for some reason.
190 """
192 if self.is_connection_open:
193 self.disconnect()
194 self.connect()
196 def is_connected(self) -> bool:
197 """
198 Checks if the device is connected.
200 This will send a query for the device identification and validate the answer.
202 Returns:
203 True is the device is connected and answered with the proper IDN; False otherwise.
204 """
206 # FIXME: This should only return the self.is_connection_open flag. Hanfdling of connection state shall be done
207 # by properly handling exceptions.
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
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
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
230 return True
232 def write(self, command: str):
233 """
234 Sends a single command to the device controller without waiting for a response.
236 Args:
237 command (str): Command to send to the controller
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 """
244 try:
245 command += "\n" if not command.endswith("\n") else ""
247 self.sock.sendall(command.encode())
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
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.
268 This is seen as a transaction.
270 Args:
271 command (str): Command to send to the controller
273 Returns:
274 Either a bytes object returned by the controller (on success), or an error message (on failure).
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 """
281 try:
282 # Attempt to send the complete command
284 command += "\n" if not command.endswith("\n") else ""
286 self.sock.sendall(command.encode())
288 # wait for, read and return the response from HUBER (will be at most TBD chars)
290 response = self.read()
292 return response
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
315 def read(self) -> bytes:
316 """
317 Reads from the device buffer.
319 Returns:
320 The content of the device buffer.
321 """
323 n_total = 0
324 buf_size = 2048
326 data = b""
328 # Set a timeout of READ_TIMEOUT to the socket.recv
330 saved_timeout = self.sock.gettimeout()
331 self.sock.settimeout(READ_TIMEOUT)
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)
349 # logger.debug(f"Total number of bytes received is {n_total}, idx={idx}")
351 return data