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
@@ -0,0 +1,265 @@
|
|
1
|
+
{
|
2
|
+
"errors": [
|
3
|
+
{
|
4
|
+
"number": -1,
|
5
|
+
"text": "ICL error: no parser found",
|
6
|
+
"level": "fatal"
|
7
|
+
},
|
8
|
+
{
|
9
|
+
"number": -2,
|
10
|
+
"text": "ICL error: unknown command",
|
11
|
+
"level": "fatal"
|
12
|
+
},
|
13
|
+
{
|
14
|
+
"number": -3,
|
15
|
+
"text": "ICL error: invalid bin mode",
|
16
|
+
"level": "fatal"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"number": -300,
|
20
|
+
"text": "CCD error: already initialized",
|
21
|
+
"level": "fatal"
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"number": -301,
|
25
|
+
"text": "CCD error: already open",
|
26
|
+
"level": "fatal"
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"number": -302,
|
30
|
+
"text": "CCD error: already closed",
|
31
|
+
"level": "fatal"
|
32
|
+
},
|
33
|
+
{
|
34
|
+
"number": -303,
|
35
|
+
"text": "CCD error: already uninitialized",
|
36
|
+
"level": "fatal"
|
37
|
+
},
|
38
|
+
{
|
39
|
+
"number": -304,
|
40
|
+
"text": "CCD error: not initialized",
|
41
|
+
"level": "fatal"
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"number": -305,
|
45
|
+
"text": "CCD error: not open",
|
46
|
+
"level": "fatal"
|
47
|
+
},
|
48
|
+
{
|
49
|
+
"number": -306,
|
50
|
+
"text": "CCD error: not found",
|
51
|
+
"level": "fatal"
|
52
|
+
},
|
53
|
+
{
|
54
|
+
"number": -307,
|
55
|
+
"text": "CCD error: invalid device index",
|
56
|
+
"level": "fatal"
|
57
|
+
},
|
58
|
+
{
|
59
|
+
"number": -308,
|
60
|
+
"text": "CCD error: initialization failure",
|
61
|
+
"level": "fatal"
|
62
|
+
},
|
63
|
+
{
|
64
|
+
"number": -309,
|
65
|
+
"text": "CCD error: acquiring",
|
66
|
+
"level": "fatal"
|
67
|
+
},
|
68
|
+
{
|
69
|
+
"number": -310,
|
70
|
+
"text": "CCD error: acquisition preparation failed",
|
71
|
+
"level": "fatal"
|
72
|
+
},
|
73
|
+
{
|
74
|
+
"number": -311,
|
75
|
+
"text": "CCD error: not ready for acquisition",
|
76
|
+
"level": "fatal"
|
77
|
+
},
|
78
|
+
{
|
79
|
+
"number": -312,
|
80
|
+
"text": "CCD error: get spectra failed",
|
81
|
+
"level": "fatal"
|
82
|
+
},
|
83
|
+
{
|
84
|
+
"number": -313,
|
85
|
+
"text": "CCD error: go failed",
|
86
|
+
"level": "fatal"
|
87
|
+
},
|
88
|
+
{
|
89
|
+
"number": -314,
|
90
|
+
"text": "CCD error: no free packet",
|
91
|
+
"level": "fatal"
|
92
|
+
},
|
93
|
+
{
|
94
|
+
"number": -315,
|
95
|
+
"text": "CCD error: command not supported",
|
96
|
+
"level": "fatal"
|
97
|
+
},
|
98
|
+
{
|
99
|
+
"number": -316,
|
100
|
+
"text": "CCD error: command failed",
|
101
|
+
"level": "fatal"
|
102
|
+
},
|
103
|
+
{
|
104
|
+
"number": -317,
|
105
|
+
"text": "CCD error: invalid token",
|
106
|
+
"level": "fatal"
|
107
|
+
},
|
108
|
+
{
|
109
|
+
"number": -318,
|
110
|
+
"text": "CCD error: invalid value",
|
111
|
+
"level": "fatal"
|
112
|
+
},
|
113
|
+
{
|
114
|
+
"number": -319,
|
115
|
+
"text": "CCD error: capabilities read error",
|
116
|
+
"level": "fatal"
|
117
|
+
},
|
118
|
+
{
|
119
|
+
"number": -320,
|
120
|
+
"text": "CCD error: acquisition already running",
|
121
|
+
"level": "fatal"
|
122
|
+
},
|
123
|
+
{
|
124
|
+
"number": -321,
|
125
|
+
"text": "CCD error: acquisition data format error",
|
126
|
+
"level": "fatal"
|
127
|
+
},
|
128
|
+
{
|
129
|
+
"number": -322,
|
130
|
+
"text": "CCD error: unsupported acquisition format",
|
131
|
+
"level": "fatal"
|
132
|
+
},
|
133
|
+
{
|
134
|
+
"number": -323,
|
135
|
+
"text": "CCD error: command execution exception",
|
136
|
+
"level": "fatal"
|
137
|
+
},
|
138
|
+
{
|
139
|
+
"number": -324,
|
140
|
+
"text": "CCD error: missing parameter",
|
141
|
+
"level": "fatal"
|
142
|
+
},
|
143
|
+
{
|
144
|
+
"number": -500,
|
145
|
+
"text": "MONO error: already initialized",
|
146
|
+
"level": "fatal"
|
147
|
+
},
|
148
|
+
{
|
149
|
+
"number": -501,
|
150
|
+
"text": "MONO error: already open",
|
151
|
+
"level": "fatal"
|
152
|
+
},
|
153
|
+
{
|
154
|
+
"number": -502,
|
155
|
+
"text": "MONO error: already opening",
|
156
|
+
"level": "fatal"
|
157
|
+
},
|
158
|
+
{
|
159
|
+
"number": -503,
|
160
|
+
"text": "MONO error: already closed",
|
161
|
+
"level": "fatal"
|
162
|
+
},
|
163
|
+
{
|
164
|
+
"number": -504,
|
165
|
+
"text": "MONO error: already uninitialized",
|
166
|
+
"level": "fatal"
|
167
|
+
},
|
168
|
+
{
|
169
|
+
"number": -505,
|
170
|
+
"text": "MONO error: not initialized",
|
171
|
+
"level": "fatal"
|
172
|
+
},
|
173
|
+
{
|
174
|
+
"number": -506,
|
175
|
+
"text": "MONO error: not open",
|
176
|
+
"level": "fatal"
|
177
|
+
},
|
178
|
+
{
|
179
|
+
"number": -507,
|
180
|
+
"text": "MONO error: not found",
|
181
|
+
"level": "fatal"
|
182
|
+
},
|
183
|
+
{
|
184
|
+
"number": -508,
|
185
|
+
"text": "MONO error: invalid device index",
|
186
|
+
"level": "fatal"
|
187
|
+
},
|
188
|
+
{
|
189
|
+
"number": -509,
|
190
|
+
"text": "MONO error: initialization failure",
|
191
|
+
"level": "fatal"
|
192
|
+
},
|
193
|
+
{
|
194
|
+
"number": -510,
|
195
|
+
"text": "MONO error: command not supported",
|
196
|
+
"level": "fatal"
|
197
|
+
},
|
198
|
+
{
|
199
|
+
"number": -511,
|
200
|
+
"text": "MONO error: discovery",
|
201
|
+
"level": "fatal"
|
202
|
+
},
|
203
|
+
{
|
204
|
+
"number": -512,
|
205
|
+
"text": "MONO error: communication error",
|
206
|
+
"level": "fatal"
|
207
|
+
},
|
208
|
+
{
|
209
|
+
"number": -513,
|
210
|
+
"text": "MONO error: invalid parameter",
|
211
|
+
"level": "fatal"
|
212
|
+
},
|
213
|
+
{
|
214
|
+
"number": -514,
|
215
|
+
"text": "MONO error: lost USB connection",
|
216
|
+
"level": "fatal"
|
217
|
+
},
|
218
|
+
{
|
219
|
+
"number": -515,
|
220
|
+
"text": "MONO error: open error",
|
221
|
+
"level": "fatal"
|
222
|
+
},
|
223
|
+
{
|
224
|
+
"number": -516,
|
225
|
+
"text": "MONO error: error log",
|
226
|
+
"level": "fatal"
|
227
|
+
},
|
228
|
+
{
|
229
|
+
"number": -517,
|
230
|
+
"text": "MONO error: initialization error",
|
231
|
+
"level": "fatal"
|
232
|
+
},
|
233
|
+
{
|
234
|
+
"number": -518,
|
235
|
+
"text": "MONO error: get configuration",
|
236
|
+
"level": "fatal"
|
237
|
+
},
|
238
|
+
{
|
239
|
+
"number": -519,
|
240
|
+
"text": "MONO error: command error",
|
241
|
+
"level": "fatal"
|
242
|
+
},
|
243
|
+
{
|
244
|
+
"number": -520,
|
245
|
+
"text": "MONO error: communication failed",
|
246
|
+
"level": "fatal"
|
247
|
+
},
|
248
|
+
{
|
249
|
+
"number": -521,
|
250
|
+
"text": "MONO error: missing parameter",
|
251
|
+
"level": "fatal"
|
252
|
+
},
|
253
|
+
{
|
254
|
+
"number": -600,
|
255
|
+
"text": "SCD error: command not supported",
|
256
|
+
"level": "fatal"
|
257
|
+
},
|
258
|
+
{
|
259
|
+
"number": -729,
|
260
|
+
"text": "Missing parameter argument: ",
|
261
|
+
"level": "fatal"
|
262
|
+
}
|
263
|
+
|
264
|
+
]
|
265
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import final
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
from overrides import override
|
5
|
+
|
6
|
+
from horiba_sdk.icl_error import AbstractError, Severity
|
7
|
+
|
8
|
+
|
9
|
+
@final
|
10
|
+
class ICLError(AbstractError):
|
11
|
+
"""Represents an Error from the ICL. It has an error code, a message and severity."""
|
12
|
+
|
13
|
+
def __init__(self, code: int, message: str, severity: Severity) -> None:
|
14
|
+
self._code = code
|
15
|
+
self._message = message
|
16
|
+
self._severity = severity
|
17
|
+
|
18
|
+
@override
|
19
|
+
def log(self) -> None:
|
20
|
+
"""Logs the error with the appropriate severity"""
|
21
|
+
logger.log(self._severity.name, self._message)
|
22
|
+
|
23
|
+
@override
|
24
|
+
def message(self) -> str:
|
25
|
+
"""Message of error.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
str: message
|
29
|
+
"""
|
30
|
+
return self._message
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import json
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import final
|
4
|
+
|
5
|
+
from loguru import logger
|
6
|
+
from overrides import override
|
7
|
+
|
8
|
+
from horiba_sdk.icl_error import AbstractError, AbstractErrorDB, ICLError, Severity, StringAsSeverity
|
9
|
+
|
10
|
+
|
11
|
+
@final
|
12
|
+
class ICLErrorDB(AbstractErrorDB):
|
13
|
+
"""ICL Error Database
|
14
|
+
|
15
|
+
This class loads a error database in json format. Based on an error string from the ICL, it returns a
|
16
|
+
`horiba_sdk.icl_error.ICLError`
|
17
|
+
object.
|
18
|
+
|
19
|
+
The json databse has to look like the following example:
|
20
|
+
|
21
|
+
.. code-block:: json
|
22
|
+
|
23
|
+
{
|
24
|
+
"errors": [
|
25
|
+
{
|
26
|
+
"number": -1,
|
27
|
+
"text": "ICL error: no parser found",
|
28
|
+
"level": "fatal"
|
29
|
+
},
|
30
|
+
{
|
31
|
+
"number": -2,
|
32
|
+
"text": "ICL error: unknown command",
|
33
|
+
"level": "fatal"
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
A database exists in this module under :code:`horiba_sdk/icl_error/error_list.json`
|
38
|
+
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self, json_db_path: Path) -> None:
|
42
|
+
if not json_db_path.is_file():
|
43
|
+
raise FileNotFoundError(f'ICL Json DB does not exist at {json_db_path}')
|
44
|
+
|
45
|
+
with open(json_db_path) as file:
|
46
|
+
self._icl_error_db = json.load(file)
|
47
|
+
|
48
|
+
@override
|
49
|
+
def error_from(self, string: str) -> AbstractError:
|
50
|
+
"""Searches an error in the database and when successfull returns a corresponding
|
51
|
+
`horiba_sdk.icl_error.ICLError`.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
string (str): ICL error string in the format :code:`'[E];<error code>;<error string>'`
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
ICLError: the corresponding error
|
58
|
+
|
59
|
+
Raises:
|
60
|
+
Exception: when the error string is not formatted as explained above or when no error is found with the
|
61
|
+
given error code.
|
62
|
+
"""
|
63
|
+
parsed_error = string.split(';')
|
64
|
+
|
65
|
+
if len(parsed_error) != 3:
|
66
|
+
raise Exception(f'Invalid length of ICL error string, was {len(parsed_error)} should be 3')
|
67
|
+
|
68
|
+
error_code: int = int(parsed_error[1])
|
69
|
+
found_error = next(
|
70
|
+
(error for error in self._icl_error_db.get('errors', []) if error.get('number') == error_code), None
|
71
|
+
)
|
72
|
+
|
73
|
+
if found_error is None:
|
74
|
+
logger.error(f'Error with number #{error_code} not found in error db')
|
75
|
+
text: str = parsed_error[2]
|
76
|
+
return ICLError(error_code, f'Unknown error: {text}', Severity.CRITICAL)
|
77
|
+
|
78
|
+
level: str = found_error.get('level')
|
79
|
+
severity: Severity = StringAsSeverity(level).to_severity()
|
80
|
+
message: str = found_error.get('text')
|
81
|
+
return ICLError(error_code, message, severity)
|
File without changes
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from horiba_sdk.communication.messages import Command, Response
|
4
|
+
|
5
|
+
|
6
|
+
class AbstractCommunicator(ABC):
|
7
|
+
"""
|
8
|
+
Abstract base class for communication protocols.
|
9
|
+
"""
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
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
|
+
def request_with_response(self, command: Command, time_to_wait_for_response_in_s: float = 0.1) -> 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
|
+
time_to_wait_for_response_in_s (float, optional): Time, in seconds, to wait between request and response.
|
36
|
+
Defaults to 0.1s
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Response: The response corresponding to the sent command.
|
40
|
+
"""
|
41
|
+
pass
|
42
|
+
|
43
|
+
@abstractmethod
|
44
|
+
def close(self) -> None:
|
45
|
+
"""
|
46
|
+
Abstract method to close the connection.
|
47
|
+
"""
|
48
|
+
pass
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Closing frame is apparently not sent. Here is a minimal example to prove the point:
|
2
|
+
# 1. First run icl.exe
|
3
|
+
# 2. Then execute this script:
|
4
|
+
# poetry run python ./horiba_sdk/sync/communication/test_client.py
|
5
|
+
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
|
6
|
+
from websockets.sync.client import connect
|
7
|
+
|
8
|
+
websocket = connect(uri='ws://localhost:25010')
|
9
|
+
try:
|
10
|
+
websocket.send('{"command":"icl_shutdown"}')
|
11
|
+
for message in websocket:
|
12
|
+
print(message)
|
13
|
+
except ConnectionClosedOK as e:
|
14
|
+
print(f'Connection normally closed: {e}')
|
15
|
+
except ConnectionClosedError as e:
|
16
|
+
print(f'Protocol error or network failure: {e}')
|
@@ -0,0 +1,212 @@
|
|
1
|
+
import time
|
2
|
+
from queue import Queue
|
3
|
+
from threading import Thread
|
4
|
+
from types import TracebackType
|
5
|
+
from typing import Any, Callable, Optional, final
|
6
|
+
|
7
|
+
import websockets
|
8
|
+
from loguru import logger
|
9
|
+
from overrides import override
|
10
|
+
from websockets.sync.client import ClientConnection
|
11
|
+
|
12
|
+
from horiba_sdk.communication.communication_exception import CommunicationException
|
13
|
+
from horiba_sdk.communication.messages import Command, JSONResponse, Response
|
14
|
+
from horiba_sdk.sync.communication.abstract_communicator import AbstractCommunicator
|
15
|
+
|
16
|
+
|
17
|
+
@final
|
18
|
+
class WebsocketCommunicator(AbstractCommunicator):
|
19
|
+
"""
|
20
|
+
The WebsocketCommunicator implements the `horiba_sdk.sync.communication.AbstractCommunicator` via websockets.
|
21
|
+
A background thread listens continuously for incoming binary data.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, uri: str = 'ws://127.0.0.1:25010') -> None:
|
25
|
+
self.uri: str = uri
|
26
|
+
self.websocket: Optional[ClientConnection] = None
|
27
|
+
self.running_listen_thread: bool = False
|
28
|
+
self.listen_thread: Optional[Thread] = None
|
29
|
+
self.running_binary_message_handling_thread: bool = False
|
30
|
+
self.binary_message_handling_thread: Optional[Thread] = None
|
31
|
+
self.json_message_queue: Queue[str] = Queue()
|
32
|
+
self.binary_message_queue: Queue[bytes] = Queue()
|
33
|
+
self.binary_message_callback: Optional[Callable[[bytes], Any]] = None
|
34
|
+
self.icl_info: dict[str, Any] = {}
|
35
|
+
|
36
|
+
def __enter__(self) -> 'WebsocketCommunicator':
|
37
|
+
self.open()
|
38
|
+
return self
|
39
|
+
|
40
|
+
def __exit__(
|
41
|
+
self,
|
42
|
+
exc_type: Optional[type[BaseException]],
|
43
|
+
exc_value: Optional[BaseException],
|
44
|
+
traceback: Optional[TracebackType],
|
45
|
+
) -> None:
|
46
|
+
self.close()
|
47
|
+
|
48
|
+
@override
|
49
|
+
def open(self) -> None:
|
50
|
+
"""
|
51
|
+
Opens the WebSocket connection and starts listening for binary data.
|
52
|
+
|
53
|
+
Raises:
|
54
|
+
CommunicationException: When the websocket is already opened or
|
55
|
+
there is an issue with the underlying websockets connection attempt.
|
56
|
+
"""
|
57
|
+
if self.opened():
|
58
|
+
raise CommunicationException(None, 'websocket already opened')
|
59
|
+
|
60
|
+
try:
|
61
|
+
self.websocket = websockets.sync.client.connect(self.uri)
|
62
|
+
except websockets.WebSocketException as e:
|
63
|
+
raise CommunicationException(None, 'websocket connection issue') from e
|
64
|
+
|
65
|
+
self.running_listen_thread = True
|
66
|
+
self.listen_thread = Thread(target=self._receive_data)
|
67
|
+
self.listen_thread.start()
|
68
|
+
|
69
|
+
logger.debug(f'Websocket connection established to {self.uri}')
|
70
|
+
|
71
|
+
def send(self, command: Command) -> None:
|
72
|
+
"""
|
73
|
+
Sends a command to the WebSocket server.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
command (Command): The command to send to the server.
|
77
|
+
|
78
|
+
Raises:
|
79
|
+
CommunicationException: When trying to send a command while the websocket is closed.
|
80
|
+
|
81
|
+
"""
|
82
|
+
if not self.opened():
|
83
|
+
raise CommunicationException(None, 'WebSocket is not opened.')
|
84
|
+
|
85
|
+
try:
|
86
|
+
# mypy cannot infer the check from self.opened() done above
|
87
|
+
logger.debug(f'Sending JSON command: {command.json()}')
|
88
|
+
self.websocket.send(command.json()) # type: ignore
|
89
|
+
except websockets.exceptions.ConnectionClosed as e:
|
90
|
+
raise CommunicationException(None, 'Trying to send data while websocket is closed') from e
|
91
|
+
|
92
|
+
@override
|
93
|
+
def opened(self) -> bool:
|
94
|
+
"""
|
95
|
+
Returns if the websocket connection is open or not
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
bool: True if the websocket connection is open, False otherwise
|
99
|
+
"""
|
100
|
+
return self.websocket is not None
|
101
|
+
|
102
|
+
def response(self) -> Response:
|
103
|
+
"""Fetches the next response
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
Response: The response from the server
|
107
|
+
|
108
|
+
Raises:
|
109
|
+
CommunicationException: When the connection terminated with an error
|
110
|
+
"""
|
111
|
+
if not self.json_message_queue or self.json_message_queue.empty():
|
112
|
+
raise CommunicationException(None, 'No message to be received.')
|
113
|
+
|
114
|
+
logger.debug(f'#{self.json_message_queue.qsize()} messages in the queue, taking first')
|
115
|
+
response: str = self.json_message_queue.get()
|
116
|
+
logger.debug('retrieved message in queue')
|
117
|
+
return JSONResponse(response)
|
118
|
+
|
119
|
+
@override
|
120
|
+
def close(self) -> None:
|
121
|
+
"""
|
122
|
+
Closes the WebSocket connection.
|
123
|
+
|
124
|
+
Raises:
|
125
|
+
CommunicationException: When the websocket is already closed
|
126
|
+
"""
|
127
|
+
if not self.opened():
|
128
|
+
raise CommunicationException(None, 'cannot close already closed websocket')
|
129
|
+
if self.websocket:
|
130
|
+
logger.debug('Waiting websocket close...')
|
131
|
+
self.websocket.close()
|
132
|
+
|
133
|
+
if self.binary_message_handling_thread:
|
134
|
+
logger.debug('Canceling binary listening thread...')
|
135
|
+
self.running_binary_message_handling_thread = False
|
136
|
+
self.binary_message_handling_thread.join()
|
137
|
+
self.binary_message_handling_thread = None
|
138
|
+
|
139
|
+
if self.listen_thread:
|
140
|
+
logger.debug('Canceling listening thread...')
|
141
|
+
self.running_listen_thread = False
|
142
|
+
self.listen_thread.join()
|
143
|
+
self.listen_thread = None
|
144
|
+
|
145
|
+
self.websocket = None
|
146
|
+
logger.debug('Websocket connection closed')
|
147
|
+
|
148
|
+
def register_binary_message_callback(self, callback: Callable[[bytes], Any]) -> None:
|
149
|
+
"""Registers a callback to be called with every incoming binary message."""
|
150
|
+
if self.binary_message_callback:
|
151
|
+
raise CommunicationException(None, 'Binary message callback already registered')
|
152
|
+
|
153
|
+
self.binary_message_callback = callback
|
154
|
+
logger.info('Binary message callback registered.')
|
155
|
+
self.running_binary_message_handling_thread = True
|
156
|
+
self.binary_message_handling_thread = Thread(target=self._run_binary_message_callback)
|
157
|
+
self.binary_message_handling_thread.start()
|
158
|
+
logger.info('Started binary message thread')
|
159
|
+
|
160
|
+
def _receive_data(self) -> None:
|
161
|
+
if not self.websocket:
|
162
|
+
raise CommunicationException(None, 'Websocket is not open')
|
163
|
+
|
164
|
+
while self.running_listen_thread:
|
165
|
+
try:
|
166
|
+
for message in self.websocket:
|
167
|
+
logger.debug(f'Received message: {message!r}')
|
168
|
+
if isinstance(message, str):
|
169
|
+
self.json_message_queue.put(message)
|
170
|
+
elif isinstance(message, bytes) and self.binary_message_callback:
|
171
|
+
self.binary_message_queue.put(message)
|
172
|
+
else:
|
173
|
+
raise CommunicationException(None, f'Unknown type of message {type(message)}')
|
174
|
+
except websockets.ConnectionClosedOK:
|
175
|
+
logger.debug('websocket connection terminated properly')
|
176
|
+
except websockets.ConnectionClosedError as e:
|
177
|
+
raise CommunicationException(None, 'connection terminated with error') from e
|
178
|
+
except Exception as e:
|
179
|
+
raise CommunicationException(None, 'failure to process binary data') from e
|
180
|
+
|
181
|
+
def _run_binary_message_callback(self) -> None:
|
182
|
+
if not self.binary_message_callback:
|
183
|
+
raise CommunicationException(None, 'No binary message callback registered')
|
184
|
+
|
185
|
+
while self.running_binary_message_handling_thread:
|
186
|
+
while self.binary_message_queue.empty():
|
187
|
+
time.sleep(0.5)
|
188
|
+
if not self.running_binary_message_handling_thread:
|
189
|
+
return
|
190
|
+
|
191
|
+
binary_message = self.binary_message_queue.get()
|
192
|
+
self.binary_message_callback(binary_message)
|
193
|
+
|
194
|
+
@override
|
195
|
+
def request_with_response(self, command: Command, time_to_wait_for_response_in_s: float = 0.1) -> Response:
|
196
|
+
"""
|
197
|
+
Concrete method to fetch a response from a command.
|
198
|
+
|
199
|
+
Args:
|
200
|
+
command (Command): Command for which a response is desired
|
201
|
+
time_to_wait_for_response_in_s (float, optional): Time, in seconds, to wait between request and response.
|
202
|
+
Defaults to 0.1s
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Response: The response corresponding to the sent command.
|
206
|
+
"""
|
207
|
+
self.send(command)
|
208
|
+
logger.debug('sent command, waiting for response')
|
209
|
+
time.sleep(time_to_wait_for_response_in_s)
|
210
|
+
response: Response = self.response()
|
211
|
+
|
212
|
+
return response
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from .abstract_device_discovery import AbstractDeviceDiscovery
|
2
|
+
from .abstract_device_manager import AbstractDeviceManager
|
3
|
+
from .device_discovery import DeviceDiscovery
|
4
|
+
from .device_manager import DeviceManager
|
5
|
+
from .fake_device_manager import FakeDeviceManager
|
6
|
+
from .fake_icl_server import FakeICLServer
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
'AbstractDeviceDiscovery',
|
10
|
+
'AbstractDeviceManager',
|
11
|
+
'DeviceDiscovery',
|
12
|
+
'DeviceManager',
|
13
|
+
'FakeDeviceManager',
|
14
|
+
'FakeICLServer',
|
15
|
+
]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
|
3
|
+
from horiba_sdk.sync.devices.single_devices import ChargeCoupledDevice, Monochromator
|
4
|
+
|
5
|
+
|
6
|
+
class AbstractDeviceDiscovery(ABC):
|
7
|
+
@abstractmethod
|
8
|
+
def execute(self, error_on_no_device: bool = False) -> None:
|
9
|
+
pass
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
|
13
|
+
pass
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def monochromators(self) -> list[Monochromator]:
|
17
|
+
pass
|