horiba-sdk 0.5.2__py3-none-any.whl → 0.6.0__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 (36) hide show
  1. horiba_sdk/communication/messages.py +5 -2
  2. horiba_sdk/communication/websocket_communicator.py +1 -1
  3. horiba_sdk/core/stitching/__init__.py +6 -0
  4. horiba_sdk/core/stitching/labspec6_spectra_stitch.py +90 -0
  5. horiba_sdk/core/stitching/linear_spectra_stitch.py +107 -0
  6. horiba_sdk/core/stitching/simple_cut_spectra_stitch.py +84 -0
  7. horiba_sdk/core/stitching/spectra_stitch.py +16 -0
  8. horiba_sdk/core/stitching/y_displacement_spectra_stitch.py +87 -0
  9. horiba_sdk/core/trigger_input_polarity.py +6 -0
  10. horiba_sdk/devices/device_manager.py +19 -1
  11. horiba_sdk/devices/fake_icl_server.py +7 -0
  12. horiba_sdk/devices/fake_responses/spectracq3.json +217 -0
  13. horiba_sdk/devices/single_devices/__init__.py +2 -1
  14. horiba_sdk/devices/single_devices/ccd.py +4 -2
  15. horiba_sdk/devices/single_devices/spectracq3.py +392 -0
  16. horiba_sdk/devices/spectracq3_discovery.py +55 -0
  17. {horiba_sdk-0.5.2.dist-info → horiba_sdk-0.6.0.dist-info}/METADATA +3 -1
  18. {horiba_sdk-0.5.2.dist-info → horiba_sdk-0.6.0.dist-info}/RECORD +20 -26
  19. horiba_sdk/sync/__init__.py +0 -0
  20. horiba_sdk/sync/communication/__init__.py +0 -7
  21. horiba_sdk/sync/communication/abstract_communicator.py +0 -47
  22. horiba_sdk/sync/communication/test_client.py +0 -16
  23. horiba_sdk/sync/communication/websocket_communicator.py +0 -232
  24. horiba_sdk/sync/devices/__init__.py +0 -15
  25. horiba_sdk/sync/devices/abstract_device_discovery.py +0 -17
  26. horiba_sdk/sync/devices/abstract_device_manager.py +0 -68
  27. horiba_sdk/sync/devices/device_discovery.py +0 -58
  28. horiba_sdk/sync/devices/device_manager.py +0 -213
  29. horiba_sdk/sync/devices/fake_device_manager.py +0 -91
  30. horiba_sdk/sync/devices/fake_icl_server.py +0 -82
  31. horiba_sdk/sync/devices/single_devices/__init__.py +0 -5
  32. horiba_sdk/sync/devices/single_devices/abstract_device.py +0 -87
  33. horiba_sdk/sync/devices/single_devices/ccd.py +0 -674
  34. horiba_sdk/sync/devices/single_devices/monochromator.py +0 -413
  35. {horiba_sdk-0.5.2.dist-info → horiba_sdk-0.6.0.dist-info}/LICENSE +0 -0
  36. {horiba_sdk-0.5.2.dist-info → horiba_sdk-0.6.0.dist-info}/WHEEL +0 -0
@@ -1,213 +0,0 @@
1
- """
2
- Synchronous Device Manager Module
3
-
4
- This Device Manager uses threads instead of asyncio
5
- """
6
-
7
- import importlib.resources
8
- import platform
9
- import subprocess
10
- from pathlib import Path
11
- from subprocess import Popen
12
- from typing import Optional, final
13
-
14
- import psutil
15
- from loguru import logger
16
- from overrides import override
17
-
18
- from horiba_sdk.communication import Command, CommunicationException, Response
19
- from horiba_sdk.icl_error import AbstractError, AbstractErrorDB, ICLErrorDB
20
- from horiba_sdk.sync.communication import AbstractCommunicator, WebsocketCommunicator
21
- from horiba_sdk.sync.devices import AbstractDeviceManager, DeviceDiscovery
22
- from horiba_sdk.sync.devices.single_devices import ChargeCoupledDevice, Monochromator
23
-
24
-
25
- @final
26
- class DeviceManager(AbstractDeviceManager):
27
- """
28
- DeviceManager class manages the lifecycle and interactions with devices.
29
- """
30
-
31
- def __init__(
32
- self,
33
- start_icl: bool = True,
34
- icl_ip: str = '127.0.0.1',
35
- icl_port: str = '25010',
36
- enable_binary_messages: bool = True,
37
- ):
38
- """
39
- Initializes the DeviceManager with the specified communicator class.
40
-
41
- Args:
42
- start_icl (bool) = True: If True, the ICL software is started and communication is established.
43
- icl_ip (str) = '127.0.0.1': websocket IP
44
- icl_port (str) = '25010': websocket port
45
- enable_binary_messages (bool) = True: If True, binary messages are enabled.
46
- """
47
- super().__init__()
48
- self._start_icl = start_icl
49
- self._icl_communicator: WebsocketCommunicator = WebsocketCommunicator('ws://' + icl_ip + ':' + str(icl_port))
50
- self._icl_websocket_ip: str = icl_ip
51
- self._icl_websocket_port: str = icl_port
52
- self._icl_process: Optional[Popen[bytes]] = None
53
- self._binary_messages: bool = enable_binary_messages
54
- self._charge_coupled_devices: list[ChargeCoupledDevice] = []
55
- self._monochromators: list[Monochromator] = []
56
-
57
- error_list_path: Path = Path(str(importlib.resources.files('horiba_sdk.icl_error') / 'error_list.json'))
58
- self._icl_error_db: AbstractErrorDB = ICLErrorDB(error_list_path)
59
-
60
- @override
61
- def start(self) -> None:
62
- if self._start_icl:
63
- self.start_icl()
64
-
65
- self._icl_communicator.register_binary_message_callback(self._binary_message_callback)
66
- self._icl_communicator.open()
67
-
68
- icl_info: Response = self._icl_communicator.request_with_response(Command('icl_info', {}))
69
- logger.info(f'ICL info: {icl_info.results}')
70
-
71
- if self._binary_messages:
72
- self._enable_binary_messages()
73
-
74
- self.discover_devices()
75
-
76
- @override
77
- def stop(self) -> None:
78
- if self._start_icl:
79
- self.stop_icl()
80
- return
81
-
82
- if self._icl_communicator.opened():
83
- self._icl_communicator.close()
84
-
85
- def start_icl(self) -> None:
86
- """
87
- Starts the ICL software and establishes communication.
88
- """
89
- logger.info('Starting ICL software...')
90
- # try:
91
- if platform.system() != 'Windows':
92
- logger.info('Only Windows is supported for ICL software. Skip starting of ICL...')
93
- return
94
-
95
- icl_running = 'icl.exe' in (p.name() for p in psutil.process_iter())
96
- if not icl_running:
97
- logger.info('icl not running, starting it...')
98
- try:
99
- self._icl_process = subprocess.Popen([r'C:\Program Files\HORIBA Scientific\SDK\icl.exe'])
100
- except subprocess.CalledProcessError as error:
101
- logger.error('Failed to start ICL software.')
102
- raise Exception('Failed to start ICL software') from error
103
-
104
- def _enable_binary_messages(self) -> None:
105
- bin_mode_command: Command = Command('icl_binMode', {'mode': 'all'})
106
- response: Response = self._icl_communicator.request_with_response(bin_mode_command)
107
-
108
- if response.errors:
109
- self._handle_errors(response.errors)
110
-
111
- def _handle_errors(self, errors: list[str]) -> None:
112
- for error in errors:
113
- icl_error: AbstractError = self._icl_error_db.error_from(error)
114
- icl_error.log()
115
- # TODO: [saga] only throw depending on the log level, tbd
116
- raise Exception(f'Error from the ICL: {icl_error.message()}')
117
-
118
- def stop_icl(self) -> None:
119
- """
120
- Stops the communication and cleans up resources.
121
- """
122
- logger.info('Requesting shutdown of ICL...')
123
-
124
- if not self._icl_communicator.opened():
125
- self._icl_communicator.open()
126
-
127
- try:
128
- info_command: Command = Command('icl_info', {})
129
- response: Response = self._icl_communicator.request_with_response(info_command)
130
-
131
- logger.info(f'ICL info: {response.results}')
132
- shutdown_command: Command = Command('icl_shutdown', {})
133
- # _response: Response = self._icl_communicator.request_with_response(shutdown_command, timeout=10)
134
- _response: Response = self._icl_communicator.request_with_response(shutdown_command)
135
- except CommunicationException as e:
136
- logger.debug(f'CommunicationException: {e.message}')
137
-
138
- if self._icl_communicator.opened():
139
- self._icl_communicator.close()
140
-
141
- if self._icl_process is not None:
142
- self._icl_process.terminate()
143
- icl_running = 'icl.exe' in (p.name() for p in psutil.process_iter())
144
- if icl_running:
145
- raise Exception('Failed to shutdown ICL software.')
146
-
147
- logger.info('icl_shutdown command sent')
148
-
149
- def _format_icl_binary_to_string(self, message: bytes) -> str:
150
- return ' '.join(format(byte, '02x') for byte in message[::-1])
151
-
152
- def _binary_message_callback(self, message: bytes) -> None:
153
- # hex_data = ' '.join(format(byte, '02x') for byte in message)
154
- # if len(message) < 18:
155
- # logger.warning(f'binary message not valid: {len(message)} < 16')
156
- # logger.info(f'Received binary message: {hex_data}')
157
- # logger.info(f'magic number: {self._format_icl_binary_to_string(message[:2])}')
158
- # logger.info(f'message type: {self._format_icl_binary_to_string(message[2:4])}')
159
- # logger.info(f'element type: {self._format_icl_binary_to_string(message[4:6])}')
160
- # logger.info(f'element count: {self._format_icl_binary_to_string(message[6:10])}')
161
- # logger.info(f'tag 1: {self._format_icl_binary_to_string(message[10:12])}')
162
- # logger.info(f'tag 2: {self._format_icl_binary_to_string(message[12:14])}')
163
- # logger.info(f'tag 3: {self._format_icl_binary_to_string(message[14:16])}')
164
- # logger.info(f'tag 4: {self._format_icl_binary_to_string(message[16:18])}')
165
- # logger.info(f'payload: {self._format_icl_binary_to_string(message[18:])}')
166
- # logger.info(f'payload as string: {str(message[18:])}')
167
- pass
168
-
169
- @override
170
- def discover_devices(self, error_on_no_device: bool = False) -> None:
171
- """
172
- Discovers the connected devices and saves them internally.
173
-
174
- Args:
175
- error_on_no_device (bool): If True, an exception is raised if no device is connected.
176
- """
177
- device_discovery: DeviceDiscovery = DeviceDiscovery(self._icl_communicator, self._icl_error_db)
178
- device_discovery.execute(error_on_no_device)
179
- self._charge_coupled_devices = device_discovery.charge_coupled_devices()
180
- self._monochromators = device_discovery.monochromators()
181
-
182
- @property
183
- @override
184
- def communicator(self) -> AbstractCommunicator:
185
- """
186
- Getter method for the communicator attribute.
187
-
188
- Returns:
189
- horiba_sdk.communication.AbstractCommunicator: Returns a new communicator instance.
190
- """
191
- return self._icl_communicator
192
-
193
- @property
194
- @override
195
- def monochromators(self) -> list[Monochromator]:
196
- """
197
- The detected monochromators, should be called after :meth:`discover_devices`
198
-
199
- Returns:
200
- List[Monochromator]: The detected monochromators
201
- """
202
- return self._monochromators
203
-
204
- @property
205
- @override
206
- def charge_coupled_devices(self) -> list[ChargeCoupledDevice]:
207
- """
208
- The detected CCDs, should be called after :meth:`discover_devices`
209
-
210
- Returns:
211
- List[ChargeCoupledDevice]: The detected CCDS.
212
- """
213
- return self._charge_coupled_devices
@@ -1,91 +0,0 @@
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)]
@@ -1,82 +0,0 @@
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 = self.icl_responses[command['command']]
59
- response['id'] = command['id']
60
- websocket.send(json.dumps(response))
61
- elif command['command'].startswith('mono_'):
62
- response = self.monochromator_responses[command['command']]
63
- response['id'] = command['id']
64
- websocket.send(json.dumps(response))
65
- elif command['command'].startswith('ccd_'):
66
- response = self.ccd_responses[command['command']]
67
- response['id'] = command['id']
68
- websocket.send(json.dumps(response))
69
- else:
70
- logger.info('unknown command, responding with message')
71
- websocket.send(message)
72
-
73
- def start(self):
74
- self._server = serve(self.echo, host=self._fake_icl_host, port=self._fake_icl_port)
75
- self._server.serve_forever()
76
-
77
- def stop(self):
78
- if self._server:
79
- logger.info('shutting down websocket server...')
80
- self._server.shutdown()
81
- self._server = None
82
- logger.info('shutdown websocket server')
@@ -1,5 +0,0 @@
1
- from .abstract_device import AbstractDevice
2
- from .ccd import ChargeCoupledDevice
3
- from .monochromator import Monochromator
4
-
5
- __all__ = ['AbstractDevice', 'Monochromator', 'ChargeCoupledDevice']
@@ -1,87 +0,0 @@
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
- def id(self) -> int:
27
- """Return the ID of the device.
28
-
29
- Returns:
30
- int: ID of the device.
31
- """
32
- return self._id
33
-
34
- @abstractmethod
35
- def open(self) -> None:
36
- """
37
- Open a connection to the device.
38
-
39
- Returns:
40
- Result: Result object indicating success or failure.
41
- """
42
- if not self._communicator.opened():
43
- self._communicator.open()
44
-
45
- @abstractmethod
46
- def close(self) -> None:
47
- """
48
- Close the connection to the device.
49
-
50
- Returns:
51
- Result: Result object indicating success or failure.
52
- """
53
- pass
54
-
55
- def _execute_command(self, command_name: str, parameters: dict[Any, Any], timeout_in_s: float = 5) -> Response:
56
- """
57
- Creates a command from the command name, and it's parameters
58
- Executes a command and handles the response.
59
-
60
- Args:
61
- command_name (str): The name of the command to execute.
62
- parameters (dict): The parameters for the command.
63
- timeout_in_s (float, optional): The timeout in seconds.
64
-
65
- Returns:
66
- Response: The response from the device.
67
-
68
- Raises:
69
- Exception: When an error occurred on the device side.
70
- """
71
- response: Response = self._communicator.request_with_response(Command(command_name, parameters), timeout_in_s)
72
- if response.errors:
73
- self._handle_errors(response.errors)
74
- return response
75
-
76
- def _handle_errors(self, errors: list[str]) -> None:
77
- """
78
- Handles errors, logs them, and may take corrective actions.
79
-
80
- Args:
81
- errors (Exception): The exception or error to handle.
82
- """
83
- for error in errors:
84
- icl_error: AbstractError = self._error_db.error_from(error)
85
- icl_error.log()
86
- # TODO: [saga] only throw depending on the log level, tbd
87
- raise Exception(f'Error from the ICL: {icl_error.message()}')