Coverage for /Users/rik/github/cgse/libs/cgse-common/src/egse/device.py: 65%
113 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"""
2This module defines the generic interfaces to connect devices.
3"""
5from enum import IntEnum
7from egse.decorators import dynamic_interface
8from egse.exceptions import Error
11class DeviceConnectionState(IntEnum):
12 """Defines connection states for device connections."""
14 # We do not use zero '0' as the connected state to prevent a state to be set
15 # to connected by default without it explicitly being set. Therefore, 0 will
16 # be the state where the connection is not explicitly set.
18 DEVICE_CONNECTION_NOT_SET = 0
19 DEVICE_CONNECTED = 1
20 """The device is connected."""
21 DEVICE_NOT_CONNECTED = 2
22 """The device is not connected."""
25class DeviceError(Error):
26 """Generic device error.
28 Args:
29 device_name (str): The name of the device
30 message (str): a clear and brief description of the problem
31 """
33 def __init__(self, device_name: str, message: str):
34 self.device_name = device_name
35 self.message = message
37 def __str__(self):
38 return f"{self.device_name}: {self.message}"
41class DeviceControllerError(DeviceError):
42 """Any error that is returned by the device controller.
44 When the device controller is connected through an e.g. Ethernet interface, it will usually
45 return error codes as part of the response to a command. When such an error is returned,
46 raise this `DeviceControllerError` instead of passing the return code (response) to the caller.
48 Args:
49 device_name (str): The name of the device
50 message (str): a clear and brief description of the problem
51 """
53 def __init__(self, device_name: str, message: str):
54 super().__init__(device_name, message)
57class DeviceConnectionError(DeviceError):
58 """A generic error for all connection type of problems.
60 Args:
61 device_name (str): The name of the device
62 message (str): a clear and brief description of the problem
63 """
65 def __init__(self, device_name: str, message: str):
66 super().__init__(device_name, message)
69class DeviceTimeoutError(DeviceError):
70 """A timeout on a device that we could not handle.
72 Args:
73 device_name (str): The name of the device
74 message (str): a clear and brief description of the problem
75 """
77 def __init__(self, device_name: str, message: str):
78 super().__init__(device_name, message)
81class DeviceInterfaceError(DeviceError):
82 """Any error that is returned or raised by the higher level interface to the device.
84 Args:
85 device_name (str): The name of the device
86 message (str): a clear and brief description of the problem
87 """
89 def __init__(self, device_name: str, message: str):
90 super().__init__(device_name, message)
93class DeviceConnectionObserver:
94 """
95 An observer for the connection state of a device. Add the subclass of this class to
96 the class that inherits from DeviceConnectionObservable. The observable will notify an
97 update of its state by calling the `update_connection_state()` method.
98 """
100 def __init__(self):
101 self._state = DeviceConnectionState.DEVICE_NOT_CONNECTED
103 def update_connection_state(self, state: DeviceConnectionState):
104 """Updates the connection state with the given state."""
105 self._state = state
107 @property
108 def state(self):
109 """Returns the current connection state of the device."""
110 return self._state
113class DeviceConnectionObservable:
114 """
115 An observable for the connection state of a device. An observer can be added with the
116 `add_observer()` method. Whenever the connection state of the device changes, the subclass
117 is responsible for notifying the observers by calling the `notify_observers()` method
118 with the correct state.
119 """
121 def __init__(self):
122 self._observers: list[DeviceConnectionObserver] = []
124 def add_observer(self, observer: DeviceConnectionObserver):
125 """Add an observer."""
126 if observer not in self._observers:
127 self._observers.append(observer)
129 def delete_observer(self, observer: DeviceConnectionObserver):
130 self._observers.remove(observer)
132 def notify_observers(self, state: DeviceConnectionState):
133 """Notify the observers of a possible state change."""
134 for observer in self._observers:
135 observer.update_connection_state(state)
137 def get_observers(self) -> list[DeviceConnectionObserver]:
138 """Returns a copy of the registered observers."""
139 return self._observers.copy()
142class DeviceConnectionInterface(DeviceConnectionObservable):
143 """Generic connection interface for all Device classes and Controllers.
145 This interface shall be implemented in the Controllers that directly connect to the
146 hardware, but also in the simulators to guarantee an identical interface as the controllers.
148 This interface will be implemented in the Proxy classes through the
149 YAML definitions. Therefore, the YAML files shall define at least
150 the following commands: `connect`, `disconnect`, `reconnect`, `is_connected`.
151 """
153 def __init__(self):
154 super().__init__()
156 def __enter__(self):
157 self.connect()
158 return self
160 def __exit__(self, exc_type, exc_val, exc_tb):
161 self.disconnect()
163 @dynamic_interface
164 def connect(self):
165 """Connect to the device controller.
167 Raises:
168 ConnectionError: when the connection can not be opened.
169 """
171 raise NotImplementedError
173 @dynamic_interface
174 def disconnect(self):
175 """Disconnect from the device controller.
177 Raises:
178 ConnectionError: when the connection can not be closed.
179 """
180 raise NotImplementedError
182 @dynamic_interface
183 def reconnect(self):
184 """Reconnect the device controller.
186 Raises:
187 ConnectionError: when the device can not be reconnected for some reason.
188 """
189 raise NotImplementedError
191 @dynamic_interface
192 def is_connected(self) -> bool:
193 """Check if the device is connected.
195 Returns:
196 True if the device is connected and responds to a command, False otherwise.
197 """
198 raise NotImplementedError
201class DeviceInterface(DeviceConnectionInterface):
202 """Generic interface for all device classes."""
204 @dynamic_interface
205 def is_simulator(self) -> bool:
206 """Checks whether the device is a simulator rather than a real hardware controller.
208 This can be useful for testing purposes or when doing actual movement simulations.
210 Returns:
211 True if the Device is a Simulator; False if the Device is connected to real hardware.
212 """
214 raise NotImplementedError
217class DeviceTransport:
218 """
219 Base class for the device transport layer.
220 """
222 def write(self, command: str):
223 """
224 Sends a complete command to the device, handle line termination, and write timeouts.
226 Args:
227 command: the command to be sent to the instrument.
228 """
230 raise NotImplementedError()
232 def read(self) -> bytes:
233 """
234 Reads a bytes object back from the instrument and returns it unaltered.
235 """
237 raise NotImplementedError
239 def read_string(self, encoding="utf-8") -> str:
240 return self.read().decode(encoding).strip()
242 def trans(self, command: str) -> bytes:
243 """
244 Send a single command to the device controller and block until a response from the
245 controller.
247 Args:
248 command: is the command to be sent to the instrument
250 Returns:
251 Either a string returned by the controller (on success), or an error message (on failure).
253 Raises:
254 DeviceConnectionError: when there was an I/O problem during communication with the controller.
256 DeviceTimeoutError: when there was a timeout in either sending the command or receiving the response.
257 """
259 raise NotImplementedError
261 def query(self, command: str) -> bytes:
262 """
263 Send a query to the device and wait for the response.
265 This `query` method is an alias for the `trans` command. For some commands it might be
266 more intuitive to use the `query` instead of the `trans`action. No need to override this
267 method as it delegates to `trans`.
269 Args:
270 command (str): the query command.
272 Returns:
273 The response to the query.
274 """
275 return self.trans(command)
278class AsyncDeviceTransport:
279 """
280 Base class for the asynchronous device transport layer.
281 """
283 async def write(self, command: str):
284 """
285 Sends a complete command to the device, handle line termination, and write timeouts.
287 Args:
288 command: the command to be sent to the instrument.
289 """
291 raise NotImplementedError()
293 async def read(self) -> bytes:
294 """
295 Reads a bytes object back from the instrument and returns it unaltered.
296 """
298 raise NotImplementedError
300 async def trans(self, command: str) -> bytes:
301 """
302 Send a single command to the device controller and block until a response from the
303 controller.
305 Args:
306 command: is the command to be sent to the instrument
308 Returns:
309 Either a string returned by the controller (on success), or an error message (on failure).
311 Raises:
312 DeviceConnectionError: when there was an I/O problem during communication with the controller.
314 DeviceTimeoutError: when there was a timeout in either sending the command or receiving the response.
315 """
317 raise NotImplementedError
319 async def query(self, command: str) -> bytes:
320 """
321 Send a query to the device and wait for the response.
323 This `query` method is an alias for the `trans` command. For some commands it might be
324 more intuitive to use the `query` instead of the `trans`action. No need to override this
325 method as it delegates to `trans`.
327 Args:
328 command (str): the query command.
330 Returns:
331 The response to the query.
332 """
333 return await self.trans(command)
336class AsyncDeviceConnectionInterface(DeviceConnectionObservable):
337 """Generic connection interface for all Device classes and Controllers.
339 This interface shall be implemented in the Controllers that directly connect to the
340 hardware, but also in the simulators to guarantee an identical interface as the controllers.
342 This interface will be implemented in the Proxy classes through the
343 YAML definitions. Therefore, the YAML files shall define at least
344 the following commands: `connect`, `disconnect`, `reconnect`, `is_connected`.
345 """
347 def __init__(self):
348 super().__init__()
350 def __enter__(self):
351 self.connect()
352 return self
354 def __exit__(self, exc_type, exc_val, exc_tb):
355 self.disconnect()
357 async def connect(self) -> None:
358 """Connect to the device controller.
360 Raises:
361 ConnectionError: when the connection can not be opened.
362 """
364 raise NotImplementedError
366 async def disconnect(self) -> None:
367 """Disconnect from the device controller.
369 Raises:
370 ConnectionError: when the connection can not be closed.
371 """
372 raise NotImplementedError
374 async def reconnect(self):
375 """Reconnect the device controller.
377 Raises:
378 ConnectionError: when the device can not be reconnected for some reason.
379 """
380 raise NotImplementedError
382 async def is_connected(self) -> bool:
383 """Check if the device is connected.
385 Returns:
386 True if the device is connected and responds to a command, False otherwise.
387 """
388 raise NotImplementedError
391class AsyncDeviceInterface(AsyncDeviceConnectionInterface):
392 """Generic interface for all device classes."""
394 def is_simulator(self) -> bool:
395 """Checks whether the device is a simulator rather than a real hardware controller.
397 This can be useful for testing purposes or when doing actual movement simulations.
399 Returns:
400 True if the Device is a Simulator; False if the Device is connected to real hardware.
401 """
403 raise NotImplementedError
406class DeviceFactoryInterface:
407 """
408 Base class for creating a device factory class to access devices.
410 This interface defines one interface method that shall be implemented by the Factory:
411 ```python
412 create(device_name: str, *, device_id: str, **_ignored)
413 ```
414 """
416 def create(self, device_name: str, *, device_id: str, **_ignored):
417 """
418 Create and return a device class that implements the expected device interface.
419 The `device_name` and `device_id` can be useful for identifying the specific device.
421 Additional keyword arguments can be passed to the device factory in order to forward
422 them to the device constructor, but they will usually be ignored.
423 """
424 ...