horiba-sdk 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- horiba_sdk/__init__.py +19 -0
- horiba_sdk/communication/__init__.py +44 -0
- horiba_sdk/communication/abstract_communicator.py +59 -0
- horiba_sdk/communication/communication_exception.py +19 -0
- horiba_sdk/communication/messages.py +87 -0
- horiba_sdk/communication/websocket_communicator.py +213 -0
- horiba_sdk/core/resolution.py +45 -0
- horiba_sdk/devices/__init__.py +11 -0
- horiba_sdk/devices/abstract_device_discovery.py +7 -0
- horiba_sdk/devices/abstract_device_manager.py +68 -0
- horiba_sdk/devices/ccd_discovery.py +57 -0
- horiba_sdk/devices/device_manager.py +250 -0
- horiba_sdk/devices/fake_device_manager.py +133 -0
- horiba_sdk/devices/fake_icl_server.py +56 -0
- horiba_sdk/devices/fake_responses/ccd.json +168 -0
- horiba_sdk/devices/fake_responses/icl.json +29 -0
- horiba_sdk/devices/fake_responses/monochromator.json +187 -0
- horiba_sdk/devices/monochromator_discovery.py +48 -0
- horiba_sdk/devices/single_devices/__init__.py +5 -0
- horiba_sdk/devices/single_devices/abstract_device.py +79 -0
- horiba_sdk/devices/single_devices/ccd.py +443 -0
- horiba_sdk/devices/single_devices/monochromator.py +395 -0
- horiba_sdk/icl_error/__init__.py +34 -0
- horiba_sdk/icl_error/abstract_error.py +65 -0
- horiba_sdk/icl_error/abstract_error_db.py +25 -0
- horiba_sdk/icl_error/error_list.json +265 -0
- horiba_sdk/icl_error/icl_error.py +30 -0
- horiba_sdk/icl_error/icl_error_db.py +81 -0
- horiba_sdk/sync/__init__.py +0 -0
- horiba_sdk/sync/communication/__init__.py +7 -0
- horiba_sdk/sync/communication/abstract_communicator.py +48 -0
- horiba_sdk/sync/communication/test_client.py +16 -0
- horiba_sdk/sync/communication/websocket_communicator.py +212 -0
- horiba_sdk/sync/devices/__init__.py +15 -0
- horiba_sdk/sync/devices/abstract_device_discovery.py +17 -0
- horiba_sdk/sync/devices/abstract_device_manager.py +68 -0
- horiba_sdk/sync/devices/device_discovery.py +82 -0
- horiba_sdk/sync/devices/device_manager.py +209 -0
- horiba_sdk/sync/devices/fake_device_manager.py +91 -0
- horiba_sdk/sync/devices/fake_icl_server.py +79 -0
- horiba_sdk/sync/devices/single_devices/__init__.py +5 -0
- horiba_sdk/sync/devices/single_devices/abstract_device.py +83 -0
- horiba_sdk/sync/devices/single_devices/ccd.py +219 -0
- horiba_sdk/sync/devices/single_devices/monochromator.py +150 -0
- horiba_sdk-0.3.2.dist-info/LICENSE +20 -0
- horiba_sdk-0.3.2.dist-info/METADATA +438 -0
- horiba_sdk-0.3.2.dist-info/RECORD +48 -0
- horiba_sdk-0.3.2.dist-info/WHEEL +4 -0
horiba_sdk/__init__.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# mypy: disable-error-code="attr-defined"
|
2
|
+
"""'horiba-sdk' is a package that provides source code for the development with Horiba devices"""
|
3
|
+
|
4
|
+
__version__ = '0.2.0' # It MUST match the version in pyproject.toml file
|
5
|
+
from importlib import metadata as importlib_metadata
|
6
|
+
|
7
|
+
from pint import UnitRegistry
|
8
|
+
|
9
|
+
|
10
|
+
def get_version() -> str:
|
11
|
+
try:
|
12
|
+
return importlib_metadata.version(__name__)
|
13
|
+
except importlib_metadata.PackageNotFoundError: # pragma: no cover
|
14
|
+
return 'unknown'
|
15
|
+
|
16
|
+
|
17
|
+
version: str = get_version()
|
18
|
+
|
19
|
+
ureg = UnitRegistry()
|
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
communication
|
3
|
+
|
4
|
+
A package that provides tools and abstractions for communication. The package includes
|
5
|
+
implementations for WebSocket communication, message formatting and parsing, and an abstract
|
6
|
+
base for defining communication contracts.
|
7
|
+
|
8
|
+
Modules:
|
9
|
+
- base_communication: Contains the abstract base class defining the communication contract.
|
10
|
+
- message_parser: Provides utility functions for message formatting and parsing.
|
11
|
+
- websocket_client: Concrete implementation of the communication contract using WebSockets.
|
12
|
+
|
13
|
+
Directly Available Imports:
|
14
|
+
- WebSocketClient: WebSocket-based communication client.
|
15
|
+
- MessageParsingError: Exception raised for message format errors.
|
16
|
+
- format_message: Utility to format messages for sending.
|
17
|
+
- parse_received_message: Utility to parse received messages into a structured format.
|
18
|
+
|
19
|
+
Typical usage example:
|
20
|
+
|
21
|
+
from communication import WebSocketClient, format_message, parse_received_message
|
22
|
+
|
23
|
+
ws = WebSocketClient("ws://example.com")
|
24
|
+
await ws.connect()
|
25
|
+
await ws.send("command", {"param": "value"})
|
26
|
+
response = await ws.receive()
|
27
|
+
await ws.disconnect()
|
28
|
+
"""
|
29
|
+
|
30
|
+
# Necessary to make Python treat the directory as a package
|
31
|
+
from .abstract_communicator import AbstractCommunicator
|
32
|
+
from .communication_exception import CommunicationException
|
33
|
+
from .messages import BinaryResponse, Command, JSONResponse, Response
|
34
|
+
from .websocket_communicator import WebsocketCommunicator
|
35
|
+
|
36
|
+
__all__ = [
|
37
|
+
'AbstractCommunicator',
|
38
|
+
'WebsocketCommunicator',
|
39
|
+
'CommunicationException',
|
40
|
+
'Command',
|
41
|
+
'Response',
|
42
|
+
'JSONResponse',
|
43
|
+
'BinaryResponse',
|
44
|
+
]
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from .messages import BinaryResponse, Command, Response
|
4
|
+
|
5
|
+
|
6
|
+
class AbstractCommunicator(ABC):
|
7
|
+
"""
|
8
|
+
Abstract base class for communication protocols.
|
9
|
+
"""
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
async def open(self) -> None:
|
13
|
+
"""
|
14
|
+
Abstract method to establish a connection.
|
15
|
+
"""
|
16
|
+
pass
|
17
|
+
|
18
|
+
@abstractmethod
|
19
|
+
def opened(self) -> bool:
|
20
|
+
"""
|
21
|
+
Abstract method that says if the connection is open
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
bool: True if the connection is open, False otherwise
|
25
|
+
"""
|
26
|
+
pass
|
27
|
+
|
28
|
+
@abstractmethod
|
29
|
+
async def request_with_response(self, command: Command, timeout: int = 5) -> Response:
|
30
|
+
"""
|
31
|
+
Abstract method to fetch a response from a command.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
command (Command): Command for which a response is desired
|
35
|
+
timeout (int, optional): Timeout [s] for waiting for the response. Defaults to 5
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
Response: The response corresponding to the sent command.
|
39
|
+
"""
|
40
|
+
pass
|
41
|
+
|
42
|
+
@abstractmethod
|
43
|
+
async def binary_response(self) -> BinaryResponse:
|
44
|
+
"""
|
45
|
+
Abstract method that fetches the next binary response.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
BinaryResponse: The binary response from the server
|
49
|
+
|
50
|
+
.. todo:: `[saga]` is this still needed?
|
51
|
+
"""
|
52
|
+
pass
|
53
|
+
|
54
|
+
@abstractmethod
|
55
|
+
async def close(self) -> None:
|
56
|
+
"""
|
57
|
+
Abstract method to close the connection.
|
58
|
+
"""
|
59
|
+
pass
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from typing import Optional, final
|
2
|
+
|
3
|
+
|
4
|
+
@final
|
5
|
+
class CommunicationException(Exception):
|
6
|
+
"""CommunicationException is raised for issues encountered in the communicator classes.
|
7
|
+
|
8
|
+
Specifically all subclasses of horiba_sdk.communication.AbstractCommunicator
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self, exception: Optional[Exception] = None, message: str = ''):
|
12
|
+
"""__init__.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
exception (Exception): the lower-level exception
|
16
|
+
message (str): explanation of what went wrong
|
17
|
+
"""
|
18
|
+
self.exception = exception
|
19
|
+
self.message = message
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import json
|
2
|
+
from itertools import count
|
3
|
+
from typing import Any, Dict, List, Optional, final
|
4
|
+
|
5
|
+
|
6
|
+
class Command:
|
7
|
+
"""
|
8
|
+
Represents a command to be sent to the server.
|
9
|
+
|
10
|
+
Class Attributes:
|
11
|
+
_id_counter (iterator): An iterator to generate unique IDs.
|
12
|
+
|
13
|
+
Instance Attributes:
|
14
|
+
id (int): Unique identifier for the command.
|
15
|
+
command (str): The command string.
|
16
|
+
parameters (Dict[str, Any]): Parameters for the command.
|
17
|
+
|
18
|
+
Methods:
|
19
|
+
json(): Converts the command to a JSON string.
|
20
|
+
"""
|
21
|
+
|
22
|
+
_id_counter = count(start=1) # Starts counting from 1
|
23
|
+
|
24
|
+
def __init__(self, command: str, parameters: Dict[str, Any]):
|
25
|
+
self.id = next(self._id_counter) # Automatically assigns the next unique ID
|
26
|
+
self.command = command
|
27
|
+
self.parameters = parameters
|
28
|
+
|
29
|
+
def json(self) -> str:
|
30
|
+
"""Converts the command object to a JSON string."""
|
31
|
+
return json.dumps({'id': self.id, 'command': self.command, 'parameters': self.parameters})
|
32
|
+
|
33
|
+
|
34
|
+
class Response:
|
35
|
+
"""
|
36
|
+
Represents a response received from the server.
|
37
|
+
|
38
|
+
Attributes:
|
39
|
+
id (int): Unique identifier matching the command's ID.
|
40
|
+
command (str): The command string echoed back from the server.
|
41
|
+
results (Optional[Dict[str, Any]]): Results returned by the server.
|
42
|
+
errors (Optional[List]): List of errors returned by the server.
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(
|
46
|
+
self, id: int, command: str, results: Optional[Dict[str, Any]] = None, errors: Optional[List[str]] = None
|
47
|
+
):
|
48
|
+
self.id = id
|
49
|
+
self.command = command
|
50
|
+
self.results = results or {}
|
51
|
+
self.errors = errors or []
|
52
|
+
|
53
|
+
|
54
|
+
@final
|
55
|
+
class JSONResponse(Response):
|
56
|
+
"""Represents a JSON response received from the server.
|
57
|
+
|
58
|
+
Attributes:
|
59
|
+
json_response (str): JSON response string
|
60
|
+
"""
|
61
|
+
|
62
|
+
def __init__(self, json_response: str):
|
63
|
+
data = json.loads(json_response)
|
64
|
+
super().__init__(id=data['id'], command=data['command'], results=data.get('results'), errors=data.get('errors'))
|
65
|
+
|
66
|
+
|
67
|
+
@final
|
68
|
+
class BinaryResponse:
|
69
|
+
"""Represents a Binary response received from the server.
|
70
|
+
|
71
|
+
Attributes:
|
72
|
+
binary_data (bytes): Response in binary format
|
73
|
+
"""
|
74
|
+
|
75
|
+
def __init__(self, binary_data: bytes):
|
76
|
+
self._binary_data = binary_data
|
77
|
+
|
78
|
+
|
79
|
+
# # Example usage
|
80
|
+
# command = Command("example_command", {"key1": "value1", "key2": "value2"})
|
81
|
+
# command_json = command.to_json()
|
82
|
+
#
|
83
|
+
# response_json = '{"id": 1357, "command": "example_command","results": ..
|
84
|
+
# ..{"key1": "value1", "key2": "value2"}, "errors":[]}'
|
85
|
+
# response = JSONResponse(response_json)
|
86
|
+
#
|
87
|
+
# # You can now access attributes like response.id, response.command, etc.
|
@@ -0,0 +1,213 @@
|
|
1
|
+
import asyncio
|
2
|
+
import contextlib
|
3
|
+
from types import TracebackType
|
4
|
+
from typing import Any, Callable, Optional, final
|
5
|
+
|
6
|
+
import websockets
|
7
|
+
from loguru import logger
|
8
|
+
from overrides import override
|
9
|
+
from websockets.legacy.client import WebSocketClientProtocol
|
10
|
+
|
11
|
+
from .abstract_communicator import AbstractCommunicator
|
12
|
+
from .communication_exception import CommunicationException
|
13
|
+
from .messages import BinaryResponse, Command, JSONResponse, Response
|
14
|
+
|
15
|
+
|
16
|
+
@final
|
17
|
+
class WebsocketCommunicator(AbstractCommunicator):
|
18
|
+
"""
|
19
|
+
The WebsocketCommunicator implements the `horiba_sdk.communication.AbstractCommunicator` via websockets.
|
20
|
+
A background task listens continuously for incoming binary data.
|
21
|
+
|
22
|
+
It supports Asynchronous Context Managers and can be used like the following::
|
23
|
+
|
24
|
+
websocket_communicator: WebsocketCommunicator = WebsocketCommunicator(uri)
|
25
|
+
async with websocket_communicator:
|
26
|
+
assert websocket_communicator.opened()
|
27
|
+
|
28
|
+
request: str = '{"command": "some_command"}'
|
29
|
+
await websocket_communicator.send(request)
|
30
|
+
response = await websocket_communicator.response()
|
31
|
+
# do something with the response...
|
32
|
+
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, uri: str = 'ws://127.0.0.1:25010') -> None:
|
36
|
+
self.uri: str = uri
|
37
|
+
self.websocket: Optional[WebSocketClientProtocol] = None
|
38
|
+
self.listen_task: Optional[asyncio.Task[Any]] = None
|
39
|
+
self.json_message_queue: asyncio.Queue[str] = asyncio.Queue()
|
40
|
+
self.binary_message_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
41
|
+
self.binary_message_callback: Optional[Callable[[bytes], Any]] = None
|
42
|
+
self.icl_info: dict[str, Any] = {}
|
43
|
+
|
44
|
+
async def __aenter__(self) -> 'WebsocketCommunicator':
|
45
|
+
await self.open()
|
46
|
+
return self
|
47
|
+
|
48
|
+
async def __aexit__(
|
49
|
+
self, exc_type: type[BaseException], exc_value: BaseException, traceback: Optional[TracebackType]
|
50
|
+
) -> None:
|
51
|
+
await self.close()
|
52
|
+
|
53
|
+
@override
|
54
|
+
async def open(self) -> None:
|
55
|
+
"""
|
56
|
+
Opens the WebSocket connection and starts listening for binary data.
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
CommunicationException: When the websocket is already opened or
|
60
|
+
there is an issue with the underlying websockets connection attempt.
|
61
|
+
"""
|
62
|
+
if self.opened():
|
63
|
+
raise CommunicationException(None, 'websocket already opened')
|
64
|
+
|
65
|
+
try:
|
66
|
+
self.websocket = await websockets.connect(self.uri)
|
67
|
+
# self.flush_incoming_messages(self) # Flush any incoming messages (if any
|
68
|
+
|
69
|
+
except websockets.WebSocketException as e:
|
70
|
+
raise CommunicationException(None, 'websocket connection issue') from e
|
71
|
+
|
72
|
+
logger.debug(f'Websocket connection established to {self.uri}')
|
73
|
+
self.listen_task = asyncio.create_task(self._receive_data())
|
74
|
+
|
75
|
+
async def send(self, command: Command) -> None:
|
76
|
+
"""
|
77
|
+
Sends a command to the WebSocket server.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
command (Command): The command to send to the server.
|
81
|
+
|
82
|
+
Raises:
|
83
|
+
CommunicationException: When trying to send a command while the websocket is closed.
|
84
|
+
|
85
|
+
"""
|
86
|
+
if not self.opened():
|
87
|
+
raise CommunicationException(None, 'WebSocket is not opened.')
|
88
|
+
|
89
|
+
try:
|
90
|
+
# mypy cannot infer the check from self.opened() done above
|
91
|
+
logger.debug(f'Sending JSON command: {command.json()}')
|
92
|
+
await self.websocket.send(command.json()) # type: ignore
|
93
|
+
except websockets.exceptions.ConnectionClosed as e:
|
94
|
+
raise CommunicationException(None, 'Trying to send data while websocket is closed') from e
|
95
|
+
|
96
|
+
@override
|
97
|
+
def opened(self) -> bool:
|
98
|
+
"""
|
99
|
+
Returns if the websocket connection is open or not
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
bool: True if the websocket connection is open, False otherwise
|
103
|
+
"""
|
104
|
+
return self.websocket is not None and self.websocket.open
|
105
|
+
|
106
|
+
async def response(self) -> Response:
|
107
|
+
"""Fetches the next response
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Response: The response from the server
|
111
|
+
|
112
|
+
Raises:
|
113
|
+
CommunicationException: When the connection terminated with an error
|
114
|
+
"""
|
115
|
+
try:
|
116
|
+
response: str = await self.json_message_queue.get()
|
117
|
+
return JSONResponse(response)
|
118
|
+
except asyncio.CancelledError as e:
|
119
|
+
raise CommunicationException(None, 'Response reception was canceled') from e
|
120
|
+
|
121
|
+
@override
|
122
|
+
async def binary_response(self) -> BinaryResponse:
|
123
|
+
"""Fetches the next binary response.
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
BinaryResponse: The binary response from the server
|
127
|
+
|
128
|
+
Raises:
|
129
|
+
CommunicationException: When the connection terminated with an error
|
130
|
+
or the binary data failed to be processed.
|
131
|
+
|
132
|
+
.. todo:: `[saga]` is this still needed?
|
133
|
+
"""
|
134
|
+
try:
|
135
|
+
response: bytes = await self.binary_message_queue.get()
|
136
|
+
logger.debug(f'Received binary response: {response!r}')
|
137
|
+
return BinaryResponse(response)
|
138
|
+
except asyncio.CancelledError as e:
|
139
|
+
raise CommunicationException(None, 'Response reception was canceled') from e
|
140
|
+
|
141
|
+
@override
|
142
|
+
async def close(self) -> None:
|
143
|
+
"""
|
144
|
+
Closes the WebSocket connection.
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
CommunicationException: When the websocket is already closed
|
148
|
+
"""
|
149
|
+
if not self.opened():
|
150
|
+
raise CommunicationException(None, 'cannot close already closed websocket')
|
151
|
+
if self.binary_message_callback:
|
152
|
+
self.binary_message_callback = None
|
153
|
+
if self.websocket:
|
154
|
+
logger.debug('Waiting websocket close...')
|
155
|
+
await self.websocket.close()
|
156
|
+
self.websocket = None
|
157
|
+
|
158
|
+
if self.listen_task:
|
159
|
+
logger.debug('Canceling listening task...')
|
160
|
+
self.listen_task.cancel()
|
161
|
+
with contextlib.suppress(asyncio.CancelledError):
|
162
|
+
logger.debug('Await listening task...')
|
163
|
+
await self.listen_task
|
164
|
+
|
165
|
+
logger.debug('Websocket connection closed')
|
166
|
+
|
167
|
+
def register_binary_message_callback(self, callback: Callable[[bytes], Any]) -> None:
|
168
|
+
"""Registers a callback to be called with every incoming binary message."""
|
169
|
+
logger.info('Binary message callback registered.')
|
170
|
+
self.binary_message_callback = callback
|
171
|
+
|
172
|
+
async def _receive_data(self) -> None:
|
173
|
+
try:
|
174
|
+
async for message in self.websocket: # type: ignore
|
175
|
+
logger.info(f'Received message: {message!r}')
|
176
|
+
if isinstance(message, str):
|
177
|
+
await self.json_message_queue.put(message)
|
178
|
+
elif isinstance(message, bytes):
|
179
|
+
if self.binary_message_callback:
|
180
|
+
await asyncio.create_task(self.binary_message_callback(message)) # Call the callback
|
181
|
+
else:
|
182
|
+
raise CommunicationException(None, f'Unknown type of message {type(message)}')
|
183
|
+
except websockets.ConnectionClosedOK:
|
184
|
+
logger.debug('websocket connection terminated properly')
|
185
|
+
except websockets.ConnectionClosedError as e:
|
186
|
+
raise CommunicationException(None, 'connection terminated with error') from e
|
187
|
+
except Exception as e:
|
188
|
+
raise CommunicationException(None, 'failure to process binary data') from e
|
189
|
+
|
190
|
+
@override
|
191
|
+
async def request_with_response(self, command: Command, timeout: int = 5) -> Response:
|
192
|
+
"""
|
193
|
+
Concrete method to fetch a response from a command.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
command (Command): Command for which a response is desired
|
197
|
+
timeout (int): Maximum time to wait for a response
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
Response: The response corresponding to the sent command.
|
201
|
+
|
202
|
+
Raises:
|
203
|
+
Exception: When an error occurred with the communication channel
|
204
|
+
"""
|
205
|
+
# send the command with the send function and wait a maximum of 5 seconds for the response
|
206
|
+
await self.send(command)
|
207
|
+
try:
|
208
|
+
async with asyncio.timeout(timeout):
|
209
|
+
response: Response = await self.response()
|
210
|
+
except TimeoutError as te:
|
211
|
+
raise CommunicationException(None, f'Timeout of {timeout}s while waiting for response.') from te
|
212
|
+
|
213
|
+
return response
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from typing import final
|
2
|
+
|
3
|
+
|
4
|
+
@final
|
5
|
+
class Resolution:
|
6
|
+
"""Width x height, non-zero, non-negative resolution in pixels.
|
7
|
+
|
8
|
+
Attributes:
|
9
|
+
width (int): The width in pixels
|
10
|
+
height (int): The height in pixels
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self, width: int, height: int) -> None:
|
14
|
+
"""Initializes a new Resolution instance
|
15
|
+
|
16
|
+
Args:
|
17
|
+
width (int): width
|
18
|
+
height (int): height
|
19
|
+
|
20
|
+
Raises:
|
21
|
+
Exception: when an invalid (<= 0) width or height is given
|
22
|
+
"""
|
23
|
+
if width <= 0 or height <= 0:
|
24
|
+
raise Exception(f'Cannot have width or height less or equal to 0: {width} x {height} not allowed')
|
25
|
+
|
26
|
+
self._width = width
|
27
|
+
self._height = height
|
28
|
+
|
29
|
+
@property
|
30
|
+
def width(self) -> int:
|
31
|
+
"""Width in pixels.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
int: width
|
35
|
+
"""
|
36
|
+
return self._width
|
37
|
+
|
38
|
+
@property
|
39
|
+
def height(self) -> int:
|
40
|
+
"""Height in pixels.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
int: height
|
44
|
+
"""
|
45
|
+
return self._height
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from .abstract_device_discovery import AbstractDeviceDiscovery
|
2
|
+
from .abstract_device_manager import AbstractDeviceManager
|
3
|
+
from .device_manager import DeviceManager
|
4
|
+
from .fake_device_manager import FakeDeviceManager
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
'AbstractDeviceManager',
|
8
|
+
'DeviceManager',
|
9
|
+
'FakeDeviceManager',
|
10
|
+
'AbstractDeviceDiscovery',
|
11
|
+
]
|
@@ -0,0 +1,68 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from horiba_sdk.communication import AbstractCommunicator
|
4
|
+
from horiba_sdk.devices.single_devices import ChargeCoupledDevice, Monochromator
|
5
|
+
|
6
|
+
|
7
|
+
class AbstractDeviceManager(ABC):
|
8
|
+
"""
|
9
|
+
DeviceManager class manages the lifecycle and interactions with devices.
|
10
|
+
|
11
|
+
"""
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
async def start(self) -> None:
|
15
|
+
"""
|
16
|
+
Abstract method to start the device manager.
|
17
|
+
"""
|
18
|
+
pass
|
19
|
+
|
20
|
+
@abstractmethod
|
21
|
+
async def stop(self) -> None:
|
22
|
+
"""
|
23
|
+
Abstract method to stop the device manager.
|
24
|
+
"""
|
25
|
+
pass
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
async def discover_devices(self, error_on_no_device: bool = False) -> None:
|
29
|
+
"""
|
30
|
+
Abstract method that discovers and registers devices.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
error_on_no_device (bool): If True, an exception is raised if no device is connected.
|
34
|
+
"""
|
35
|
+
pass
|
36
|
+
|
37
|
+
@property
|
38
|
+
@abstractmethod
|
39
|
+
def communicator(self) -> AbstractCommunicator:
|
40
|
+
"""
|
41
|
+
Abstract method to get the communicator attribute.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
AbstractCommunicator: Returns the internal communicator instance.
|
45
|
+
"""
|
46
|
+
pass
|
47
|
+
|
48
|
+
@property
|
49
|
+
@abstractmethod
|
50
|
+
def monochromators(self) -> list[Monochromator]:
|
51
|
+
"""
|
52
|
+
Abstract method to get the detected monochromators.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
List[Monochromator]: The detected monochromators
|
56
|
+
"""
|
57
|
+
pass
|
58
|
+
|
59
|
+
@property
|
60
|
+
@abstractmethod
|
61
|
+
def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
|
62
|
+
"""
|
63
|
+
Abstract method to get the detected CCDs.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
List[ChargeCoupledDevice]: The detected CCDS.
|
67
|
+
"""
|
68
|
+
pass
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Any, final
|
3
|
+
|
4
|
+
from loguru import logger
|
5
|
+
from overrides import override
|
6
|
+
|
7
|
+
from horiba_sdk.communication import AbstractCommunicator, Command, Response
|
8
|
+
from horiba_sdk.devices.abstract_device_discovery import AbstractDeviceDiscovery
|
9
|
+
from horiba_sdk.devices.single_devices import ChargeCoupledDevice
|
10
|
+
from horiba_sdk.icl_error import AbstractErrorDB
|
11
|
+
|
12
|
+
|
13
|
+
@final
|
14
|
+
class ChargeCoupledDevicesDiscovery(AbstractDeviceDiscovery):
|
15
|
+
def __init__(self, communicator: AbstractCommunicator, error_db: AbstractErrorDB):
|
16
|
+
self._communicator: AbstractCommunicator = communicator
|
17
|
+
self._charge_coupled_devices: list[ChargeCoupledDevice] = []
|
18
|
+
self._error_db: AbstractErrorDB = error_db
|
19
|
+
|
20
|
+
@override
|
21
|
+
async def execute(self, error_on_no_device: bool = False) -> None:
|
22
|
+
"""
|
23
|
+
Discovers the connected devices and saves them internally.
|
24
|
+
|
25
|
+
Raises:
|
26
|
+
Exception: When no CCDs are discovered and that `error_on_no_device` is set. Or when there is an issue
|
27
|
+
parsing the CCD list
|
28
|
+
"""
|
29
|
+
if not self._communicator.opened():
|
30
|
+
await self._communicator.open()
|
31
|
+
|
32
|
+
response: Response = await self._communicator.request_with_response(Command('ccd_discover', {}))
|
33
|
+
if response.results.get('count', 0) == 0 and error_on_no_device:
|
34
|
+
raise Exception('No CCDs connected')
|
35
|
+
response = await self._communicator.request_with_response(Command('ccd_list', {}))
|
36
|
+
|
37
|
+
raw_device_list = response.results
|
38
|
+
self._charge_coupled_devices = self._parse_ccds(raw_device_list)
|
39
|
+
logger.info(f'Found {len(self._charge_coupled_devices)} CCD devices: {self._charge_coupled_devices}')
|
40
|
+
|
41
|
+
def _parse_ccds(self, raw_device_list: dict[str, Any]) -> list[ChargeCoupledDevice]:
|
42
|
+
detected_ccds: list[ChargeCoupledDevice] = []
|
43
|
+
for key, value in raw_device_list.items():
|
44
|
+
logger.debug(f'Parsing CCD: {key} - {value}')
|
45
|
+
ccd_index: int = int(key.split(':')[0].replace('index', '').strip())
|
46
|
+
ccd_type_match = re.search(r'deviceType: (.*?),', value)
|
47
|
+
if not ccd_type_match:
|
48
|
+
raise Exception(f'Failed to find ccd type "deviceType" in string "{value}"')
|
49
|
+
ccd_type: str = str(ccd_type_match.group(1).strip())
|
50
|
+
|
51
|
+
logger.info(f'Detected CCD: {ccd_type}')
|
52
|
+
detected_ccds.append(ChargeCoupledDevice(ccd_index, self._communicator, self._error_db))
|
53
|
+
|
54
|
+
return detected_ccds
|
55
|
+
|
56
|
+
def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
|
57
|
+
return self._charge_coupled_devices
|