pychilaslasers 1.0.0__tar.gz
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.
- pychilaslasers-1.0.0/PKG-INFO +11 -0
- pychilaslasers-1.0.0/README.md +2 -0
- pychilaslasers-1.0.0/pyproject.toml +25 -0
- pychilaslasers-1.0.0/setup.cfg +4 -0
- pychilaslasers-1.0.0/src/pychilaslasers/__init__.py +27 -0
- pychilaslasers-1.0.0/src/pychilaslasers/comm.py +312 -0
- pychilaslasers-1.0.0/src/pychilaslasers/exceptions/__init__.py +17 -0
- pychilaslasers-1.0.0/src/pychilaslasers/exceptions/laser_error.py +22 -0
- pychilaslasers-1.0.0/src/pychilaslasers/exceptions/mode_error.py +54 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser.py +427 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/__init__.py +34 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/diode.py +177 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/heaters/__init__.py +27 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/heaters/heater_channels.py +11 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/heaters/heaters.py +224 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/laser_component.py +79 -0
- pychilaslasers-1.0.0/src/pychilaslasers/laser_components/tec.py +144 -0
- pychilaslasers-1.0.0/src/pychilaslasers/modes/__init__.py +39 -0
- pychilaslasers-1.0.0/src/pychilaslasers/modes/calibrated.py +126 -0
- pychilaslasers-1.0.0/src/pychilaslasers/modes/manual_mode.py +186 -0
- pychilaslasers-1.0.0/src/pychilaslasers/modes/mode.py +88 -0
- pychilaslasers-1.0.0/src/pychilaslasers/modes/steady_mode.py +432 -0
- pychilaslasers-1.0.0/src/pychilaslasers/modes/sweep_mode.py +462 -0
- pychilaslasers-1.0.0/src/pychilaslasers/utils.py +222 -0
- pychilaslasers-1.0.0/src/pychilaslasers.egg-info/PKG-INFO +11 -0
- pychilaslasers-1.0.0/src/pychilaslasers.egg-info/SOURCES.txt +28 -0
- pychilaslasers-1.0.0/src/pychilaslasers.egg-info/dependency_links.txt +1 -0
- pychilaslasers-1.0.0/src/pychilaslasers.egg-info/requires.txt +2 -0
- pychilaslasers-1.0.0/src/pychilaslasers.egg-info/top_level.txt +1 -0
- pychilaslasers-1.0.0/tests/test_calibration_reading.py +116 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pychilaslasers
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library for controlling Chilas lasers
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: packaging>=25.0
|
|
8
|
+
Requires-Dist: pyserial>=3.5
|
|
9
|
+
|
|
10
|
+
# PyChilasLasers
|
|
11
|
+
Python library for Chilas Comet and Atlas lasers
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pychilaslasers"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python library for controlling Chilas lasers"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"packaging>=25.0",
|
|
9
|
+
"pyserial>=3.5",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"pychilaslasers",
|
|
15
|
+
"pytest>=8.4.1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
pychilaslasers = { workspace = true }
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[tool.pyright]
|
|
23
|
+
|
|
24
|
+
reportOptionalMemberAccess = "warning"
|
|
25
|
+
reportAttributeAccessIssue = "warning"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyChilasLasers Module
|
|
3
|
+
<p>
|
|
4
|
+
This module provides functionality for controlling and interfacing with laser systems.
|
|
5
|
+
<p>
|
|
6
|
+
The package has the following structure:
|
|
7
|
+
- `laser.py`: Contains the main `Laser` class for laser control.
|
|
8
|
+
- `utils.py`: Contains utility functions and data structures for calibration and other operations.
|
|
9
|
+
- `modes/`: Contains laser modes which encompass specific laser behaviors as well as enums used interacting with these modes.
|
|
10
|
+
- `laser_components/`: Contains classes for various laser components such as TEC, diode, and drivers.
|
|
11
|
+
These classes are used to encapsulate the behavior, properties and state of these components.
|
|
12
|
+
<p>
|
|
13
|
+
Interaction with the laser should be done through the `Laser` class.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .laser import Laser
|
|
17
|
+
|
|
18
|
+
__all__: list[str] = [
|
|
19
|
+
# Main laser class
|
|
20
|
+
"Laser",
|
|
21
|
+
"__version__"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Package metadata
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
__author__ = "Chilas B.V."
|
|
27
|
+
__email__ = "info@chilasbv.com"
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the `Communication` class for handling communication with the laser
|
|
3
|
+
driver over serial.
|
|
4
|
+
<p>
|
|
5
|
+
It contains the methods for sending commands to the laser, receiving responses, and managing
|
|
6
|
+
the serial connection.
|
|
7
|
+
<p>
|
|
8
|
+
**Authors:** RLK, AVR, SDU
|
|
9
|
+
**Last Revision:** Aug 5, 2025 - Created deconstructor
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# ✅ Standard library imports
|
|
13
|
+
import atexit
|
|
14
|
+
import logging
|
|
15
|
+
import signal
|
|
16
|
+
|
|
17
|
+
# ✅ Third-party imports
|
|
18
|
+
import serial
|
|
19
|
+
|
|
20
|
+
# ✅ Local imports
|
|
21
|
+
from pychilaslasers.exceptions.laser_error import LaserError
|
|
22
|
+
from pychilaslasers.utils import Constants
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Communication:
|
|
28
|
+
"""
|
|
29
|
+
Communication class for handling communication with the laser driver over serial.
|
|
30
|
+
<p>
|
|
31
|
+
This class provides methods for sending commands to the laser, receiving responses,
|
|
32
|
+
and managing the serial connection. It also handles the prefix mode for the laser driver,
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, com_port: str) -> None:
|
|
36
|
+
"""Initialize the Communication class with the specified serial port.
|
|
37
|
+
<p>
|
|
38
|
+
This method sets up the serial connection to the laser driver and initializes
|
|
39
|
+
the communication parameters. It also registers cleanup functions to ensure
|
|
40
|
+
the serial connection is properly closed on exit or signal termination. And
|
|
41
|
+
sets the initial baudrate to the default value. When a connection fails, it will
|
|
42
|
+
attempt to reconnect using the next supported baudrate until a connection is established
|
|
43
|
+
as this is one of the most common issues when connecting to the laser driver.
|
|
44
|
+
<p>
|
|
45
|
+
Args:
|
|
46
|
+
com_port: The serial port to connect to the laser driver.
|
|
47
|
+
this can be found by using the `pychilaslasers.utils.get_serial_ports()` function.
|
|
48
|
+
"""
|
|
49
|
+
# Validate the com_port input
|
|
50
|
+
if not isinstance(com_port, str):
|
|
51
|
+
raise ValueError("The com_port must be a string representing the serial port.")
|
|
52
|
+
|
|
53
|
+
# Initialize serial connection to the laser
|
|
54
|
+
self._serial: serial.Serial = serial.Serial(
|
|
55
|
+
port=com_port,
|
|
56
|
+
baudrate=Constants.TLM_INITIAL_BAUDRATE, # Use the first supported baudrate
|
|
57
|
+
bytesize=serial.EIGHTBITS,
|
|
58
|
+
parity=serial.PARITY_NONE,
|
|
59
|
+
stopbits=serial.STOPBITS_ONE,
|
|
60
|
+
timeout=1.0,
|
|
61
|
+
)
|
|
62
|
+
self._previous_command: str = "None"
|
|
63
|
+
|
|
64
|
+
self._prefix_mode: bool = True
|
|
65
|
+
# Attempt to open the serial connection by trying different baudrates
|
|
66
|
+
baudrates: set[int] = Constants.SUPPORTED_BAUDRATES.copy() # Copy to avoid modifying the original set
|
|
67
|
+
rate = Constants.TLM_INITIAL_BAUDRATE
|
|
68
|
+
while True:
|
|
69
|
+
try:
|
|
70
|
+
self.prefix_mode = True
|
|
71
|
+
break
|
|
72
|
+
except Exception:
|
|
73
|
+
try:
|
|
74
|
+
logger.error(f"Serial connection failed at {rate} baud.Attempting new connection with baudrate {(rate:=baudrates.pop())}.")
|
|
75
|
+
self.baudrate = rate # Try next baudrate if the current one fails
|
|
76
|
+
except KeyError:
|
|
77
|
+
logger.critical("No more supported baudrates available. Cannot establish serial connection.")
|
|
78
|
+
raise RuntimeError("Failed to establish serial connection with the laser driver. " +
|
|
79
|
+
"Please check the connection and supported baudrates.") from None
|
|
80
|
+
self.baudrate = Constants.DEFAULT_BAUDRATE
|
|
81
|
+
|
|
82
|
+
# Ensure proper closing of the serial connection on exit or signal
|
|
83
|
+
atexit.register(self.close_connection)
|
|
84
|
+
signal.signal(signal.SIGINT,self.close_connection)
|
|
85
|
+
signal.signal(signal.SIGTERM, self.close_connection)
|
|
86
|
+
|
|
87
|
+
def __del__(self) -> None:
|
|
88
|
+
"""Destructor that ensures the serial connection is closed when the object is deleted.
|
|
89
|
+
<p>
|
|
90
|
+
This method is called when the object is garbage collected, providing an additional
|
|
91
|
+
safety mechanism to ensure the serial connection is properly closed even if the
|
|
92
|
+
user forgets to call close explicitly or if the program terminates unexpectedly.
|
|
93
|
+
"""
|
|
94
|
+
self.close_connection()
|
|
95
|
+
|
|
96
|
+
########## Main Methods ##########
|
|
97
|
+
|
|
98
|
+
def query(self, data: str) -> str:
|
|
99
|
+
"""Main method for communication with the laser.
|
|
100
|
+
<p>
|
|
101
|
+
This method sends a command to the laser over the serial connection and returns
|
|
102
|
+
the response. It also handles the logging of the command and response. The
|
|
103
|
+
response code of the reply is checked and an error is raised if the response
|
|
104
|
+
code is not 0. Commands that are sent multiple times may be replaced with a
|
|
105
|
+
semicolon to speed up communication.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
data: The serial command to be sent to the laser.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The response from the laser. The response is stripped of any leading
|
|
112
|
+
or trailing whitespace as well as the return code. Response may be
|
|
113
|
+
empty if the command does not return a value.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
serial.SerialException: If there is an error in the serial communication,
|
|
117
|
+
such as a decoding error or an empty reply.
|
|
118
|
+
LaserError: If the response code from the laser is not 0, indicating an error.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# Write the command to the serial port
|
|
122
|
+
logger.debug(msg=f"W {data}") # Logs the command being sent
|
|
123
|
+
self._serial.write(
|
|
124
|
+
f"{self._semicolon_replace(data)}\r\n"
|
|
125
|
+
.encode("ascii")
|
|
126
|
+
)
|
|
127
|
+
self._serial.flush()
|
|
128
|
+
|
|
129
|
+
if not self.prefix_mode:
|
|
130
|
+
return "" # If prefix mode is off, return empty string immediately
|
|
131
|
+
|
|
132
|
+
# Read the response from the laser
|
|
133
|
+
try:
|
|
134
|
+
reply: str = self._serial.readline().decode("ascii").rstrip()
|
|
135
|
+
except UnicodeDecodeError as e:
|
|
136
|
+
logger.error(f"Failed to decode reply from device: {e}")
|
|
137
|
+
raise serial.SerialException(f"Failed to decode reply from device: {e}. " +
|
|
138
|
+
"Please check the connection and baudrate settings.")
|
|
139
|
+
|
|
140
|
+
# Error handling
|
|
141
|
+
if not reply or reply == "":
|
|
142
|
+
logger.error("Empty reply from device")
|
|
143
|
+
raise serial.SerialException("Empty reply from device. " \
|
|
144
|
+
"Please check the connection and prefix mode.")
|
|
145
|
+
|
|
146
|
+
if reply[0] != "0":
|
|
147
|
+
logger.error(f"Nonzero return code: {reply[2:]}")
|
|
148
|
+
raise LaserError(code=reply[0],message=reply[2:]) # Raise a custom error with the reply message
|
|
149
|
+
else:
|
|
150
|
+
logger.debug(f"R {reply}")
|
|
151
|
+
|
|
152
|
+
return reply[2:]
|
|
153
|
+
|
|
154
|
+
def close_connection(self, signum = None, fname = None) -> None:
|
|
155
|
+
"""Closes the serial connection to the laser driver safely.
|
|
156
|
+
Attempts to reset the baudrate to the initial value before closing the connection.
|
|
157
|
+
<p>
|
|
158
|
+
This method is registered to be called on exit or when a signal is received.
|
|
159
|
+
"""
|
|
160
|
+
if self._serial and self._serial.is_open:
|
|
161
|
+
if signum is not None:
|
|
162
|
+
logger.error(f"Received signal {signal.Signals(signum).name} ({signum}): closing connection")
|
|
163
|
+
else:
|
|
164
|
+
logger.debug("Closing connection")
|
|
165
|
+
self.query("SYST:STAT 0")
|
|
166
|
+
self._serial.write(f"SYST:SER:BAUD {Constants.TLM_INITIAL_BAUDRATE}\r\n".encode("ascii"))
|
|
167
|
+
logger.debug("Resetting serial baudrate to initial value")
|
|
168
|
+
self._serial.close()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
########## Private Methods ##########
|
|
172
|
+
|
|
173
|
+
def _semicolon_replace(self, cmd: str) -> str:
|
|
174
|
+
"""To speed up communication, a repeating command can be replaced with a semicolon.
|
|
175
|
+
<p>
|
|
176
|
+
Check if the command was previously sent to the device. In that case, replace
|
|
177
|
+
it with a semicolon.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
cmd: The command to be replaced with semicolon.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The command with semicolon inserted
|
|
184
|
+
"""
|
|
185
|
+
if cmd.split(" ")[0] == self._previous_command and self._previous_command in Constants.SEMICOLON_COMMANDS:
|
|
186
|
+
cmd = cmd.replace(cmd.split(" ")[0], ";")
|
|
187
|
+
else:
|
|
188
|
+
self._previous_command = cmd.split(" ")[0]
|
|
189
|
+
return cmd
|
|
190
|
+
|
|
191
|
+
def _initialize_variables(self) -> None:
|
|
192
|
+
"""Initialize private variables."""
|
|
193
|
+
self._previous_command: str = "None"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
########## Properties (Getters/Setters) ##########
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def prefix_mode(self) -> bool:
|
|
200
|
+
"""Gets prefix mode for the laser driver.
|
|
201
|
+
<p>
|
|
202
|
+
The laser driver can be operated in two different communication modes:
|
|
203
|
+
1. Prefix mode on
|
|
204
|
+
2. Prefix mode off
|
|
205
|
+
<p>
|
|
206
|
+
When prefix mode is on, every message over the serial connection will be
|
|
207
|
+
replied to by the driver with a response, and every response will be
|
|
208
|
+
prefixed with a return code (rc), either `0` or `1` for an OK or ERROR
|
|
209
|
+
respectively.
|
|
210
|
+
<p>
|
|
211
|
+
With prefix mode is off, responses from the laser driver are not prefixed
|
|
212
|
+
with a return code. This means that in the case for a serial write command
|
|
213
|
+
without an expected return value, the driver will not send back a reply.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
whether prefix mode is enabled (True) or disabled (False)
|
|
217
|
+
"""
|
|
218
|
+
return self._prefix_mode
|
|
219
|
+
|
|
220
|
+
@prefix_mode.setter
|
|
221
|
+
def prefix_mode(self, mode: bool) -> None:
|
|
222
|
+
"""Sets prefix mode for the laser driver.
|
|
223
|
+
<p>
|
|
224
|
+
The laser driver can be operated in two different communication modes:
|
|
225
|
+
1. Prefix mode on
|
|
226
|
+
2. Prefix mode off
|
|
227
|
+
<p>
|
|
228
|
+
When prefix mode is on, every message over the serial connection will be
|
|
229
|
+
replied to by the driver with a response, and every response will be
|
|
230
|
+
prefixed with a return code (rc), either `0` or `1` for an OK or ERROR
|
|
231
|
+
respectively.
|
|
232
|
+
<p>
|
|
233
|
+
With prefix mode is off, responses from the laser driver are not prefixed
|
|
234
|
+
with a return code. This means that in the case for a serial write command
|
|
235
|
+
without an expected return value, the driver will not send back a reply.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
mode: whether to enable prefix mode (True) or disable it (False)
|
|
239
|
+
"""
|
|
240
|
+
self.query(f"SYST:COMM:PFX {mode:d}")
|
|
241
|
+
self._prefix_mode = mode
|
|
242
|
+
logger.info(f"Changed prefix mode to {mode}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def baudrate(self) -> int:
|
|
247
|
+
"""Gets the baudrate of the serial connection to the driver
|
|
248
|
+
|
|
249
|
+
The baudrate can be changed, but does require a serial reconnect
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
(int): baudrate currently in use
|
|
253
|
+
"""
|
|
254
|
+
driver_baudrate = int(self.query("SYST:SER:BAUD?"))
|
|
255
|
+
if driver_baudrate != self._serial.baudrate:
|
|
256
|
+
logger.error("There seems to be a baudrate mismatch between driver and connection baudrate settings")
|
|
257
|
+
return driver_baudrate
|
|
258
|
+
|
|
259
|
+
@baudrate.setter
|
|
260
|
+
def baudrate(self, new_baudrate: int) -> None:
|
|
261
|
+
"""Sets the baudrate of the serial connection to the driver
|
|
262
|
+
|
|
263
|
+
The baudrate can be changed, but this requires a serial reconnect.
|
|
264
|
+
|
|
265
|
+
Currently supported baudrates are:
|
|
266
|
+
- 9600
|
|
267
|
+
- 14400
|
|
268
|
+
- 19200
|
|
269
|
+
- 28800
|
|
270
|
+
- 38400
|
|
271
|
+
- 57600, default
|
|
272
|
+
- 115200
|
|
273
|
+
- 230400
|
|
274
|
+
- 460800
|
|
275
|
+
- 912600
|
|
276
|
+
|
|
277
|
+
This method will first check if there is already a serial connection open.
|
|
278
|
+
If not, it will do nothing and return immediately (None). If a serial connection
|
|
279
|
+
is open, it will first check if new baudrate requested, is supported.
|
|
280
|
+
If not, it will return None. Otherwise, continue to check if the new baudrate
|
|
281
|
+
needs to be set, by comparing with the current baudrate in use. If the new requested
|
|
282
|
+
baudrate is different then it will set the new baudrate as follows:
|
|
283
|
+
1. Instruct the driver to use a new baudrate
|
|
284
|
+
2. Close the serial connection
|
|
285
|
+
3. Change the serial connection attribute to use the new baudrate as well
|
|
286
|
+
4. Reopen the serial connection
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
new_baudrate (int): new baudrate to use
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
# Input validation
|
|
293
|
+
if not self._serial.is_open:
|
|
294
|
+
return
|
|
295
|
+
if new_baudrate == self._serial.baudrate:
|
|
296
|
+
return
|
|
297
|
+
if new_baudrate not in Constants.SUPPORTED_BAUDRATES:
|
|
298
|
+
raise ValueError(f"The given baudrate {new_baudrate} is not supported.")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# 1. Instruct driver to use new baudrate
|
|
302
|
+
logger.info(f"Switching baudrates from {self._serial.baudrate} to {new_baudrate}.")
|
|
303
|
+
self._serial.write(f"SYST:SER:BAUD {new_baudrate:d}\r\n".encode("ascii"))
|
|
304
|
+
logger.debug(f"[baudrate_switch] Writing to serial: SYST:SER:BAUD {new_baudrate:d}")
|
|
305
|
+
# 2. Close serial connection
|
|
306
|
+
logger.debug("[baudrate_switch] Closing serial connection")
|
|
307
|
+
self._serial.close()
|
|
308
|
+
# 3. Change serial connection baudrate attribute
|
|
309
|
+
self._serial.baudrate = new_baudrate
|
|
310
|
+
# 4. Reopen serial connection
|
|
311
|
+
logger.debug("[baudrate_switch] Reopening serial connection with new baudrate")
|
|
312
|
+
self._serial.open()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""PyChilasLasers exceptions module.
|
|
2
|
+
|
|
3
|
+
This module contains all custom exceptions for the PyChilasLasers library.
|
|
4
|
+
These exceptions provide specific error handling for laser operations and modes.
|
|
5
|
+
<p>
|
|
6
|
+
Authors: SDU
|
|
7
|
+
Last Revision: Aug 4, 2025 - Created
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# ✅ Local imports
|
|
11
|
+
from .laser_error import LaserError
|
|
12
|
+
from .mode_error import ModeError
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"LaserError",
|
|
16
|
+
"ModeError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class representing errors received from the laser
|
|
3
|
+
<p>
|
|
4
|
+
Authors: SDU
|
|
5
|
+
Last Revision: Aug 4, 2025 - Created the LaserError class
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LaserError(Exception):
|
|
10
|
+
def __init__(self, code: str, message: str) -> None:
|
|
11
|
+
"""Class representing errors received from the laser.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
code (str): The error code sent by the laser. Typically a 1 but kept
|
|
15
|
+
abstract to allow for future expansion.
|
|
16
|
+
message (str): The error message.
|
|
17
|
+
"""
|
|
18
|
+
self.code: str = code
|
|
19
|
+
self.message: str = message
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return f"LaserError {self.code}: The laser has responded with an error {self.message}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception class for laser mode-related errors.
|
|
3
|
+
<p>
|
|
4
|
+
This module defines the ModeError exception which is raised when operations
|
|
5
|
+
are attempted in or for incompatible laser modes. It provides detailed information
|
|
6
|
+
about the current mode and suggests the correct mode for the operation.
|
|
7
|
+
<p>
|
|
8
|
+
**Authors:** SDU
|
|
9
|
+
**Last Revision:** August 4, 2025 - Created
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# ⚛️ Type checking
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from pychilaslasers.modes.mode import Mode
|
|
18
|
+
|
|
19
|
+
# ✅ Local imports
|
|
20
|
+
from pychilaslasers.modes.mode import LaserMode
|
|
21
|
+
|
|
22
|
+
class ModeError(Exception):
|
|
23
|
+
"""Exception raised for errors related to the laser mode."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, current_mode: LaserMode | Mode, desired_mode: LaserMode | Mode | None = None) -> None:
|
|
26
|
+
"""Exception raised in case of an error related to the mode the laser is in.
|
|
27
|
+
<p>
|
|
28
|
+
This exception is used to indicate that an operation cannot be performed in the current mode of the laser.
|
|
29
|
+
It provides information about the current mode and the desired mode that would allow the operation to succeed
|
|
30
|
+
<p>
|
|
31
|
+
Args:
|
|
32
|
+
message (str): The error message.
|
|
33
|
+
current_mode (LaserMode): The current mode of the laser.
|
|
34
|
+
desired_mode (LaserMode | None, optional): The laser mode that would allow
|
|
35
|
+
for the operation to succeed. Defaults to None.
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.message: str = message
|
|
39
|
+
|
|
40
|
+
# Checking to allow for the use of both LaserMode and Mode types
|
|
41
|
+
self.current_mode: LaserMode = current_mode if isinstance(current_mode, LaserMode) else current_mode.mode
|
|
42
|
+
self.desired_mode: LaserMode | None = (
|
|
43
|
+
desired_mode if isinstance(desired_mode, LaserMode)
|
|
44
|
+
else ( desired_mode.mode if desired_mode is not None else None)
|
|
45
|
+
)
|
|
46
|
+
# Constructing the error message
|
|
47
|
+
if self.desired_mode:
|
|
48
|
+
self.message += f" (current mode: {self.current_mode.name}, mode this" + \
|
|
49
|
+
f" operation is possible in: {self.desired_mode.name})"
|
|
50
|
+
else:
|
|
51
|
+
self.message += f" (current mode: {self.current_mode.name})"
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
return f"ModeError: {self.message}"
|