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.
Files changed (48) hide show
  1. horiba_sdk/__init__.py +19 -0
  2. horiba_sdk/communication/__init__.py +44 -0
  3. horiba_sdk/communication/abstract_communicator.py +59 -0
  4. horiba_sdk/communication/communication_exception.py +19 -0
  5. horiba_sdk/communication/messages.py +87 -0
  6. horiba_sdk/communication/websocket_communicator.py +213 -0
  7. horiba_sdk/core/resolution.py +45 -0
  8. horiba_sdk/devices/__init__.py +11 -0
  9. horiba_sdk/devices/abstract_device_discovery.py +7 -0
  10. horiba_sdk/devices/abstract_device_manager.py +68 -0
  11. horiba_sdk/devices/ccd_discovery.py +57 -0
  12. horiba_sdk/devices/device_manager.py +250 -0
  13. horiba_sdk/devices/fake_device_manager.py +133 -0
  14. horiba_sdk/devices/fake_icl_server.py +56 -0
  15. horiba_sdk/devices/fake_responses/ccd.json +168 -0
  16. horiba_sdk/devices/fake_responses/icl.json +29 -0
  17. horiba_sdk/devices/fake_responses/monochromator.json +187 -0
  18. horiba_sdk/devices/monochromator_discovery.py +48 -0
  19. horiba_sdk/devices/single_devices/__init__.py +5 -0
  20. horiba_sdk/devices/single_devices/abstract_device.py +79 -0
  21. horiba_sdk/devices/single_devices/ccd.py +443 -0
  22. horiba_sdk/devices/single_devices/monochromator.py +395 -0
  23. horiba_sdk/icl_error/__init__.py +34 -0
  24. horiba_sdk/icl_error/abstract_error.py +65 -0
  25. horiba_sdk/icl_error/abstract_error_db.py +25 -0
  26. horiba_sdk/icl_error/error_list.json +265 -0
  27. horiba_sdk/icl_error/icl_error.py +30 -0
  28. horiba_sdk/icl_error/icl_error_db.py +81 -0
  29. horiba_sdk/sync/__init__.py +0 -0
  30. horiba_sdk/sync/communication/__init__.py +7 -0
  31. horiba_sdk/sync/communication/abstract_communicator.py +48 -0
  32. horiba_sdk/sync/communication/test_client.py +16 -0
  33. horiba_sdk/sync/communication/websocket_communicator.py +212 -0
  34. horiba_sdk/sync/devices/__init__.py +15 -0
  35. horiba_sdk/sync/devices/abstract_device_discovery.py +17 -0
  36. horiba_sdk/sync/devices/abstract_device_manager.py +68 -0
  37. horiba_sdk/sync/devices/device_discovery.py +82 -0
  38. horiba_sdk/sync/devices/device_manager.py +209 -0
  39. horiba_sdk/sync/devices/fake_device_manager.py +91 -0
  40. horiba_sdk/sync/devices/fake_icl_server.py +79 -0
  41. horiba_sdk/sync/devices/single_devices/__init__.py +5 -0
  42. horiba_sdk/sync/devices/single_devices/abstract_device.py +83 -0
  43. horiba_sdk/sync/devices/single_devices/ccd.py +219 -0
  44. horiba_sdk/sync/devices/single_devices/monochromator.py +150 -0
  45. horiba_sdk-0.3.2.dist-info/LICENSE +20 -0
  46. horiba_sdk-0.3.2.dist-info/METADATA +438 -0
  47. horiba_sdk-0.3.2.dist-info/RECORD +48 -0
  48. horiba_sdk-0.3.2.dist-info/WHEEL +4 -0
@@ -0,0 +1,68 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from horiba_sdk.sync.communication import AbstractCommunicator
4
+ from horiba_sdk.sync.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
+ def start(self) -> None:
15
+ """
16
+ Abstract method to start the device manager.
17
+ """
18
+ pass
19
+
20
+ @abstractmethod
21
+ def stop(self) -> None:
22
+ """
23
+ Abstract method to stop the device manager.
24
+ """
25
+ pass
26
+
27
+ @abstractmethod
28
+ 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,82 @@
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 Command, Response
8
+ from horiba_sdk.icl_error import AbstractErrorDB
9
+ from horiba_sdk.sync.communication import AbstractCommunicator
10
+ from horiba_sdk.sync.devices.abstract_device_discovery import AbstractDeviceDiscovery
11
+ from horiba_sdk.sync.devices.single_devices import ChargeCoupledDevice, Monochromator
12
+
13
+
14
+ @final
15
+ class DeviceDiscovery(AbstractDeviceDiscovery):
16
+ def __init__(self, communicator: AbstractCommunicator, error_db: AbstractErrorDB):
17
+ self._communicator: AbstractCommunicator = communicator
18
+ self._charge_coupled_devices: list[ChargeCoupledDevice] = []
19
+ self._monochromators: list[Monochromator] = []
20
+ self._error_db: AbstractErrorDB = error_db
21
+ self._discovered_devices: bool = False
22
+
23
+ @override
24
+ def execute(self, error_on_no_device: bool = False) -> None:
25
+ """
26
+ Discovers the connected devices and saves them internally.
27
+ """
28
+ if not self._communicator.opened():
29
+ self._communicator.open()
30
+
31
+ # Define the commands and device types in a list of tuples for iteration
32
+ commands_and_types = [('ccd_discover', 'ccd_list', 'CCD'), ('mono_discover', 'mono_list', 'Monochromator')]
33
+
34
+ for discover_command, list_command, device_type in commands_and_types:
35
+ response: Response = self._communicator.request_with_response(Command(discover_command, {}))
36
+ if response.results.get('count', 0) == 0 and error_on_no_device:
37
+ raise Exception(f'No {device_type} connected')
38
+ response = self._communicator.request_with_response(Command(list_command, {}))
39
+
40
+ # as the responses of ccd_list and mono_list differ, we need to parse them separately
41
+ if device_type == 'CCD':
42
+ raw_device_list = response.results
43
+ self._charge_coupled_devices = self._parse_ccds(raw_device_list)
44
+ logger.info(f'Found {len(self._charge_coupled_devices)} CCD devices: {self._charge_coupled_devices}')
45
+ elif device_type == 'Monochromator':
46
+ raw_device_list = response.results['list']
47
+ self._monochromators = self._parse_monos(raw_device_list)
48
+ logger.info(f'Found {len(self._monochromators)} Monochromator devices: {self._monochromators}')
49
+
50
+ def _parse_ccds(self, raw_device_list: dict[str, Any]) -> list[ChargeCoupledDevice]:
51
+ detected_ccds: list[ChargeCoupledDevice] = []
52
+ for key, value in raw_device_list.items():
53
+ logger.debug(f'Parsing CCD: {key} - {value}')
54
+ ccd_index: int = int(key.split(':')[0].replace('index', '').strip())
55
+ ccd_type_match = re.search(r'deviceType: (.*?),', value)
56
+ if not ccd_type_match:
57
+ raise Exception(f'Failed to find ccd type "deviceType" in string "{value}"')
58
+ ccd_type: str = str(ccd_type_match.group(1).strip())
59
+
60
+ logger.info(f'Detected CCD: {ccd_type}')
61
+ detected_ccds.append(ChargeCoupledDevice(ccd_index, self._communicator, self._error_db))
62
+
63
+ return detected_ccds
64
+
65
+ def _parse_monos(self, raw_device_list: dict[str, Any]) -> list[Monochromator]:
66
+ detected_monos = []
67
+ for device_string in raw_device_list:
68
+ mono_index: int = int(device_string.split(';')[0])
69
+ mono_type: str = device_string.split(';')[1]
70
+
71
+ logger.info(f'Detected Monochromator: {mono_type}')
72
+ detected_monos.append(Monochromator(mono_index, self._communicator, self._error_db))
73
+
74
+ return detected_monos
75
+
76
+ @override
77
+ def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
78
+ return self._charge_coupled_devices
79
+
80
+ @override
81
+ def monochromators(self) -> list[Monochromator]:
82
+ return self._monochromators
@@ -0,0 +1,209 @@
1
+ """
2
+ Synchronous Device Manager Module
3
+
4
+ This Device Manager uses threads instead of asyncio
5
+ """
6
+ import importlib.resources
7
+ import platform
8
+ import subprocess
9
+ from pathlib import Path
10
+ from subprocess import Popen
11
+ from typing import Optional, final
12
+
13
+ import psutil
14
+ from loguru import logger
15
+ from overrides import override
16
+
17
+ from horiba_sdk.communication import Command, CommunicationException, Response
18
+ from horiba_sdk.icl_error import AbstractError, AbstractErrorDB, ICLErrorDB
19
+ from horiba_sdk.sync.communication import AbstractCommunicator, WebsocketCommunicator
20
+ from horiba_sdk.sync.devices import AbstractDeviceManager, DeviceDiscovery
21
+ from horiba_sdk.sync.devices.single_devices import ChargeCoupledDevice, Monochromator
22
+
23
+
24
+ @final
25
+ class DeviceManager(AbstractDeviceManager):
26
+ """
27
+ DeviceManager class manages the lifecycle and interactions with devices.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ start_icl: bool = True,
33
+ websocket_ip: str = '127.0.0.1',
34
+ websocket_port: str = '25010',
35
+ enable_binary_messages: bool = True,
36
+ ):
37
+ """
38
+ Initializes the DeviceManager with the specified communicator class.
39
+
40
+ Args:
41
+ start_icl (bool) = True: If True, the ICL software is started and communication is established.
42
+ websocket_ip (str) = '127.0.0.1': websocket IP
43
+ websocket_port (str) = '25010': websocket port
44
+ enable_binary_messages (bool) = True: If True, binary messages are enabled.
45
+ """
46
+ super().__init__()
47
+ self._start_icl = start_icl
48
+ self._icl_communicator: WebsocketCommunicator = WebsocketCommunicator(
49
+ 'ws://' + websocket_ip + ':' + str(websocket_port)
50
+ )
51
+ self._icl_websocket_ip: str = websocket_ip
52
+ self._icl_websocket_port: str = websocket_port
53
+ self._icl_process: Optional[Popen[bytes]] = None
54
+ self._binary_messages: bool = enable_binary_messages
55
+ self._charge_coupled_devices: list[ChargeCoupledDevice] = []
56
+ self._monochromators: list[Monochromator] = []
57
+
58
+ error_list_path: Path = Path(str(importlib.resources.files('horiba_sdk.icl_error') / 'error_list.json'))
59
+ self._icl_error_db: AbstractErrorDB = ICLErrorDB(error_list_path)
60
+
61
+ @override
62
+ def start(self) -> None:
63
+ if self._start_icl:
64
+ self.start_icl()
65
+
66
+ self._icl_communicator.register_binary_message_callback(self._binary_message_callback)
67
+ self._icl_communicator.open()
68
+
69
+ icl_info: Response = self._icl_communicator.request_with_response(Command('icl_info', {}))
70
+ logger.info(f'ICL info: {icl_info.results}')
71
+
72
+ if self._binary_messages:
73
+ self._enable_binary_messages()
74
+
75
+ self.discover_devices()
76
+
77
+ @override
78
+ def stop(self) -> None:
79
+ self.stop_icl()
80
+
81
+ def start_icl(self) -> None:
82
+ """
83
+ Starts the ICL software and establishes communication.
84
+ """
85
+ logger.info('Starting ICL software...')
86
+ # try:
87
+ if platform.system() != 'Windows':
88
+ logger.info('Only Windows is supported for ICL software. Skip starting of ICL...')
89
+ return
90
+
91
+ icl_running = 'icl.exe' in (p.name() for p in psutil.process_iter())
92
+ if not icl_running:
93
+ logger.info('icl not running, starting it...')
94
+ try:
95
+ self._icl_process = subprocess.Popen([r'C:\Program Files\HORIBA Scientific\SDK\icl.exe'])
96
+ except subprocess.CalledProcessError as error:
97
+ logger.error('Failed to start ICL software.')
98
+ raise Exception('Failed to start ICL software') from error
99
+
100
+ def _enable_binary_messages(self) -> None:
101
+ bin_mode_command: Command = Command('icl_binMode', {'mode': 'all'})
102
+ response: Response = self._icl_communicator.request_with_response(bin_mode_command)
103
+
104
+ if response.errors:
105
+ self._handle_errors(response.errors)
106
+
107
+ def _handle_errors(self, errors: list[str]) -> None:
108
+ for error in errors:
109
+ icl_error: AbstractError = self._icl_error_db.error_from(error)
110
+ icl_error.log()
111
+ # TODO: [saga] only throw depending on the log level, tbd
112
+ raise Exception(f'Error from the ICL: {icl_error.message()}')
113
+
114
+ def stop_icl(self) -> None:
115
+ """
116
+ Stops the communication and cleans up resources.
117
+ """
118
+ logger.info('Requesting shutdown of ICL...')
119
+
120
+ if not self._icl_communicator.opened():
121
+ self._icl_communicator.open()
122
+
123
+ try:
124
+ info_command: Command = Command('icl_info', {})
125
+ response: Response = self._icl_communicator.request_with_response(info_command)
126
+
127
+ logger.info(f'ICL info: {response.results}')
128
+ shutdown_command: Command = Command('icl_shutdown', {})
129
+ # _response: Response = self._icl_communicator.request_with_response(shutdown_command, timeout=10)
130
+ _response: Response = self._icl_communicator.request_with_response(shutdown_command)
131
+ except CommunicationException as e:
132
+ logger.debug(f'CommunicationException: {e.message}')
133
+
134
+ if self._icl_communicator.opened():
135
+ self._icl_communicator.close()
136
+
137
+ if self._icl_process is not None:
138
+ self._icl_process.terminate()
139
+ icl_running = 'icl.exe' in (p.name() for p in psutil.process_iter())
140
+ if icl_running:
141
+ raise Exception('Failed to shutdown ICL software.')
142
+
143
+ logger.info('icl_shutdown command sent')
144
+
145
+ def _format_icl_binary_to_string(self, message: bytes) -> str:
146
+ return ' '.join(format(byte, '02x') for byte in message[::-1])
147
+
148
+ def _binary_message_callback(self, message: bytes) -> None:
149
+ # hex_data = ' '.join(format(byte, '02x') for byte in message)
150
+ # if len(message) < 18:
151
+ # logger.warning(f'binary message not valid: {len(message)} < 16')
152
+ # logger.info(f'Received binary message: {hex_data}')
153
+ # logger.info(f'magic number: {self._format_icl_binary_to_string(message[:2])}')
154
+ # logger.info(f'message type: {self._format_icl_binary_to_string(message[2:4])}')
155
+ # logger.info(f'element type: {self._format_icl_binary_to_string(message[4:6])}')
156
+ # logger.info(f'element count: {self._format_icl_binary_to_string(message[6:10])}')
157
+ # logger.info(f'tag 1: {self._format_icl_binary_to_string(message[10:12])}')
158
+ # logger.info(f'tag 2: {self._format_icl_binary_to_string(message[12:14])}')
159
+ # logger.info(f'tag 3: {self._format_icl_binary_to_string(message[14:16])}')
160
+ # logger.info(f'tag 4: {self._format_icl_binary_to_string(message[16:18])}')
161
+ # logger.info(f'payload: {self._format_icl_binary_to_string(message[18:])}')
162
+ # logger.info(f'payload as string: {str(message[18:])}')
163
+ pass
164
+
165
+ @override
166
+ def discover_devices(self, error_on_no_device: bool = False) -> None:
167
+ """
168
+ Discovers the connected devices and saves them internally.
169
+
170
+ Args:
171
+ error_on_no_device (bool): If True, an exception is raised if no device is connected.
172
+ """
173
+ device_discovery: DeviceDiscovery = DeviceDiscovery(self._icl_communicator, self._icl_error_db)
174
+ device_discovery.execute(error_on_no_device)
175
+ self._charge_coupled_devices = device_discovery.charge_coupled_devices()
176
+ self._monochromators = device_discovery.monochromators()
177
+
178
+ @property
179
+ @override
180
+ def communicator(self) -> AbstractCommunicator:
181
+ """
182
+ Getter method for the communicator attribute.
183
+
184
+ Returns:
185
+ horiba_sdk.communication.AbstractCommunicator: Returns a new communicator instance.
186
+ """
187
+ return self._icl_communicator
188
+
189
+ @property
190
+ @override
191
+ def monochromators(self) -> list[Monochromator]:
192
+ """
193
+ The detected monochromators, should be called after :meth:`discover_devices`
194
+
195
+ Returns:
196
+ List[Monochromator]: The detected monochromators
197
+ """
198
+ return self._monochromators
199
+
200
+ @property
201
+ @override
202
+ def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
203
+ """
204
+ The detected CCDs, should be called after :meth:`discover_devices`
205
+
206
+ Returns:
207
+ List[ChargeCoupledDevice]: The detected CCDS.
208
+ """
209
+ return self._charge_coupled_devices
@@ -0,0 +1,91 @@
1
+ from typing import final
2
+
3
+ from overrides import override
4
+
5
+ from horiba_sdk.icl_error import FakeErrorDB
6
+ from horiba_sdk.sync.communication.websocket_communicator import WebsocketCommunicator
7
+ from horiba_sdk.sync.devices.abstract_device_manager import AbstractDeviceManager
8
+ from horiba_sdk.sync.devices.single_devices import ChargeCoupledDevice, Monochromator
9
+
10
+
11
+ @final
12
+ class FakeDeviceManager(AbstractDeviceManager):
13
+ """
14
+ The FakeDeviceManager represents a `horiba_sdk.sync.devices.DeviceManager` that can be used in the unit tests.
15
+
16
+ The class should be used in a pytest fixture as follows::
17
+
18
+ fake_icl_host: str = 'localhost'
19
+ fake_icl_port: int = 8765
20
+
21
+ @pytest.fixture(scope='module')
22
+ def fake_sync_icl_exe(): # noqa: ARG001
23
+ sync_server = FakeSyncICLServer(fake_icl_host=fake_icl_host, fake_icl_port=fake_icl_port)
24
+ thread = threading.Thread(target=sync_server.start)
25
+ thread.start()
26
+
27
+ yield thread
28
+
29
+ sync_server.stop()
30
+ thread.join()
31
+
32
+
33
+ @pytest.fixture(scope='module')
34
+ def fake_sync_device_manager(): # noqa: ARG001
35
+ fake_device_manager = FakeSyncDeviceManager(host=fake_icl_host, port=fake_icl_port)
36
+ fake_device_manager.start()
37
+
38
+ yield fake_device_manager
39
+ fake_device_manager.stop()
40
+
41
+ def your_unit_test(fake_sync_icl_exe, fake_sync_device_manager):
42
+ pass
43
+
44
+ """
45
+
46
+ def __init__(self, host: str = '127.0.0.1', port: int = 25011):
47
+ self.host = host
48
+ self.port = port
49
+ self.error_db: FakeErrorDB = FakeErrorDB()
50
+ self.websocket_communicator = WebsocketCommunicator('ws://' + self.host + ':' + str(self.port))
51
+
52
+ def start(self) -> None:
53
+ self.websocket_communicator.open()
54
+
55
+ def stop(self) -> None:
56
+ self.websocket_communicator.close()
57
+
58
+ @override
59
+ def discover_devices(self, error_on_no_device: bool = False) -> None:
60
+ """
61
+ Does nothing.
62
+ """
63
+ pass
64
+
65
+ @property
66
+ @override
67
+ def communicator(self) -> WebsocketCommunicator:
68
+ """Communicator"""
69
+ return self.websocket_communicator
70
+
71
+ @property
72
+ @override
73
+ def monochromators(self) -> list[Monochromator]:
74
+ """
75
+ Abstract method to get the detected monochromators.
76
+
77
+ Returns:
78
+ List[Monochromator]: The detected monochromators
79
+ """
80
+ return [Monochromator(0, self.communicator, self.error_db)]
81
+
82
+ @property
83
+ @override
84
+ def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
85
+ """
86
+ Abstract method to get the detected CCDs.
87
+
88
+ Returns:
89
+ List[ChargeCoupledDevice]: The detected CCDS.
90
+ """
91
+ return [ChargeCoupledDevice(0, self.communicator, self.error_db)]
@@ -0,0 +1,79 @@
1
+ import importlib.resources
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from loguru import logger
7
+ from websockets.sync.server import WebSocketServer, serve
8
+
9
+
10
+ class FakeICLServer:
11
+ """The FakeICLServer is a synchronous fake ICL server that sends only dummy data.
12
+
13
+ It starts a local websocket server and returns the predefined response located in
14
+ `horiba_sdk/devices/fake_responses/*.json`.
15
+ Currently supported devices for fake responses are:
16
+ - The ICL itself
17
+ - The :class:`Monochromator`
18
+ - The :class:`ChargeCoupledDevice`
19
+
20
+ For other unsupported devices, it just responds with the sent command.
21
+
22
+ """
23
+
24
+ def __init__(self, fake_icl_host: str = 'localhost', fake_icl_port: int = 8765):
25
+ self._fake_icl_host: str = fake_icl_host
26
+ self._fake_icl_port: int = fake_icl_port
27
+ self._server: Optional[WebSocketServer] = None
28
+
29
+ fake_responses_path: Path = Path(str(importlib.resources.files('horiba_sdk.devices'))) / Path('fake_responses')
30
+
31
+ icl_fake_responses_path = fake_responses_path / 'icl.json'
32
+ with open(icl_fake_responses_path) as json_file:
33
+ self.icl_responses = json.load(json_file)
34
+
35
+ monochromator_fake_responses_path = fake_responses_path / 'monochromator.json'
36
+ with open(monochromator_fake_responses_path) as json_file:
37
+ self.monochromator_responses = json.load(json_file)
38
+
39
+ ccd_fake_responses_path = fake_responses_path / 'ccd.json'
40
+ with open(ccd_fake_responses_path) as json_file:
41
+ self.ccd_responses = json.load(json_file)
42
+
43
+ def echo(self, websocket):
44
+ for message in websocket:
45
+ logger.info('received: {message}', message=message)
46
+ command = json.loads(message)
47
+
48
+ if 'shutdown' in command['command']:
49
+ logger.info('Shutting down websocket')
50
+ websocket.close()
51
+ continue
52
+
53
+ if 'command' not in command:
54
+ logger.info('unknown message format, responding with message')
55
+ websocket.send(message)
56
+ continue
57
+ if command['command'].startswith('icl_'):
58
+ response = json.dumps(self.icl_responses[command['command']])
59
+ websocket.send(response)
60
+ elif command['command'].startswith('mono_'):
61
+ response = json.dumps(self.monochromator_responses[command['command']])
62
+ websocket.send(response)
63
+ elif command['command'].startswith('ccd_'):
64
+ response = json.dumps(self.ccd_responses[command['command']])
65
+ websocket.send(response)
66
+ else:
67
+ logger.info('unknown command, responding with message')
68
+ websocket.send(message)
69
+
70
+ def start(self):
71
+ self._server = serve(self.echo, host=self._fake_icl_host, port=self._fake_icl_port)
72
+ self._server.serve_forever()
73
+
74
+ def stop(self):
75
+ if self._server:
76
+ logger.info('shutting down websocket server...')
77
+ self._server.shutdown()
78
+ self._server = None
79
+ logger.info('shutdown websocket server')
@@ -0,0 +1,5 @@
1
+ from .abstract_device import AbstractDevice
2
+ from .ccd import ChargeCoupledDevice
3
+ from .monochromator import Monochromator
4
+
5
+ __all__ = ['AbstractDevice', 'Monochromator', 'ChargeCoupledDevice']
@@ -0,0 +1,83 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ from horiba_sdk.communication import Command, Response
5
+ from horiba_sdk.icl_error import AbstractError, AbstractErrorDB
6
+ from horiba_sdk.sync.communication.abstract_communicator import AbstractCommunicator
7
+
8
+
9
+ class AbstractDevice(ABC):
10
+ """
11
+ Abstract base class representing a generic device.
12
+
13
+ This class provides an interface for device-specific operations. Concrete implementations should provide specific
14
+ functionalities for each of the abstract methods.
15
+
16
+ Attributes:
17
+ _id (int):
18
+ _communicator (WebsocketCommunicator):
19
+ """
20
+
21
+ def __init__(self, device_id: int, communicator: AbstractCommunicator, error_db: AbstractErrorDB) -> None:
22
+ self._id: int = device_id
23
+ self._error_db: AbstractErrorDB = error_db
24
+ self._communicator: AbstractCommunicator = communicator
25
+
26
+ @abstractmethod
27
+ def open(self) -> None:
28
+ """
29
+ Open a connection to the device.
30
+
31
+ Returns:
32
+ Result: Result object indicating success or failure.
33
+ """
34
+ if not self._communicator.opened():
35
+ self._communicator.open()
36
+
37
+ @abstractmethod
38
+ def close(self) -> None:
39
+ """
40
+ Close the connection to the device.
41
+
42
+ Returns:
43
+ Result: Result object indicating success or failure.
44
+ """
45
+ pass
46
+
47
+ def _execute_command(
48
+ self, command_name: str, parameters: dict[Any, Any], time_to_wait_for_response_in_s: float = 0.1
49
+ ) -> Response:
50
+ """
51
+ Creates a command from the command name, and it's parameters
52
+ Executes a command and handles the response.
53
+
54
+ Args:
55
+ command_name (str): The name of the command to execute.
56
+ parameters (dict): The parameters for the command.
57
+ time_to_wait_for_response_in_s (float, optional): The time to wait for the response. Defaults to 0.1
58
+
59
+ Returns:
60
+ Response: The response from the device.
61
+
62
+ Raises:
63
+ Exception: When an error occurred on the device side.
64
+ """
65
+ response: Response = self._communicator.request_with_response(
66
+ Command(command_name, parameters), time_to_wait_for_response_in_s
67
+ )
68
+ if response.errors:
69
+ self._handle_errors(response.errors)
70
+ return response
71
+
72
+ def _handle_errors(self, errors: list[str]) -> None:
73
+ """
74
+ Handles errors, logs them, and may take corrective actions.
75
+
76
+ Args:
77
+ errors (Exception): The exception or error to handle.
78
+ """
79
+ for error in errors:
80
+ icl_error: AbstractError = self._error_db.error_from(error)
81
+ icl_error.log()
82
+ # TODO: [saga] only throw depending on the log level, tbd
83
+ raise Exception(f'Error from the ICL: {icl_error.message()}')