Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\crylas.py : 33%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4Device classes for a CryLas pulsed laser controller and a CryLas laser attenuator,
5using serial communication.
7There are three modes of operation for the laser
81. Laser-internal hardware trigger (default): fixed to 20 Hz and max energy per pulse.
92. Laser-internal software trigger (for diagnosis only).
103. External trigger: required for arbitrary pulse energy or repetition rate. Switch to
11"external" on the front panel of laser controller for using option 3.
13After switching on the laser with laser_on(), the system must stabilize
14for some minutes. Do not apply abrupt changes of pulse energy or repetition rate.
16Manufacturer homepage:
17https://www.crylas.de/products/pulsed_laser.html
18"""
20import logging
21import math
22import re
23import time
24from datetime import datetime
25from threading import Event
26from typing import Union, Tuple, Optional, Callable
28from aenum import Enum, IntEnum
30from .base import SingleCommDevice
31from .utils import Poller
32from ..comm import SerialCommunication, SerialCommunicationConfig
33from ..comm.serial import (
34 SerialCommunicationParity,
35 SerialCommunicationStopbits,
36 SerialCommunicationBytesize,
37)
38from ..configuration import configdataclass
39from ..utils.typing import Number
42@configdataclass
43class CryLasLaserSerialCommunicationConfig(SerialCommunicationConfig):
44 #: Baudrate for CryLas laser is 19200 baud
45 baudrate: int = 19200
47 #: CryLas laser does not use parity
48 parity: Union[
49 str, SerialCommunicationParity
50 ] = SerialCommunicationParity.NONE
52 #: CryLas laser uses one stop bit
53 stopbits: Union[
54 int, SerialCommunicationStopbits
55 ] = SerialCommunicationStopbits.ONE
57 #: One byte is eight bits long
58 bytesize: Union[
59 int, SerialCommunicationBytesize
60 ] = SerialCommunicationBytesize.EIGHTBITS
62 #: The terminator is LF
63 terminator: bytes = b"\n"
65 #: use 10 seconds timeout as default (a long timeout is needed!)
66 timeout: Number = 10
69class CryLasLaserSerialCommunication(SerialCommunication):
70 """
71 Specific communication protocol implementation for the CryLas laser controller.
72 Already predefines device-specific protocol parameters in config.
73 """
75 @staticmethod
76 def config_cls():
77 return CryLasLaserSerialCommunicationConfig
79 READ_TEXT_SKIP_PREFIXES = (">", "MODE:")
80 """Prefixes of lines that are skipped when read from the serial port."""
82 def read_text(self) -> str:
83 """
84 Read first line of text from the serial port that does not start with any of
85 `self.READ_TEXT_SKIP_PREFIXES`.
87 :return: String read from the serial port; `''` if there was nothing to read.
88 :raises SerialCommunicationIOError: when communication port is not opened
89 """
90 with self.access_lock:
91 line = super().read_text()
92 while line.startswith(self.READ_TEXT_SKIP_PREFIXES):
93 logging.debug(f'Laser com: "{line.rstrip()}" SKIPPED')
94 line = super().read_text()
95 logging.debug(f'Laser com: "{line.rstrip()}"')
96 return line
98 def query(self, cmd: str, prefix: str, post_cmd: str = None) -> str:
99 """
100 Send a command, then read the com until a line starting with prefix,
101 or an empty line, is found. Returns the line in question.
103 :param cmd: query message to send to the device
104 :param prefix: start of the line to look for in the device answer
105 :param post_cmd: optional additional command to send after the query
106 :return: line in question as a string
107 :raises SerialCommunicationIOError: when communication port is not opened
108 """
110 with self.access_lock:
111 # send the command
112 self.write_text(cmd)
113 # read the com until the prefix or an empty line is found
114 line = self.read_text().strip()
115 while line and not line.startswith(prefix):
116 line = self.read_text().strip()
117 if post_cmd is not None:
118 # send an optional post-command message to send to the device
119 self.write_text(post_cmd)
120 return line
122 def query_all(self, cmd: str, prefix: str):
123 """
124 Send a command, then read the com until a line starting with prefix,
125 or an empty line, is found. Returns a list of successive lines starting with
126 prefix.
128 :param cmd: query message to send to the device
129 :param prefix: start of the line to look for in the device answer
130 :return: line in question as a string
131 :raises SerialCommunicationIOError: when communication port is not opened
132 """
134 with self.access_lock:
135 # send the command
136 self.write_text(cmd)
137 # read the com until the prefix or an empty line is found
138 line = self.read_text().strip()
139 while line and not line.startswith(prefix):
140 line = self.read_text().strip()
141 answer = []
142 while line.startswith(prefix):
143 answer.append(line)
144 line = self.read_text().strip()
145 return answer
148class CryLasLaserShutterStatus(Enum):
149 """
150 Status of the CryLas laser shutter
151 """
153 CLOSED = 0
154 OPENED = 1
157@configdataclass
158class CryLasLaserConfig:
159 """
160 Device configuration dataclass for the CryLas laser controller.
161 """
163 # status of the shutter
164 ShutterStatus = CryLasLaserShutterStatus
166 # calibration factor for the pulse energy
167 calibration_factor: Number = 4.35
169 # polling period (s) to check back on the laser status if not ready
170 # it should be longer than the communication timeout
171 polling_period: Union[int, float] = 12
173 # timeout (s) when polling the laser status, CryLasLaserError is raised on timeout
174 polling_timeout: Number = 300
176 # automatically turn on the laser when ready
177 auto_laser_on: bool = True
179 # status of the shutter to be set on start
180 init_shutter_status: Union[
181 int, CryLasLaserShutterStatus
182 ] = CryLasLaserShutterStatus.CLOSED
184 def clean_values(self):
185 if self.calibration_factor <= 0:
186 raise ValueError("The calibration factor should be positive.")
187 if self.polling_period <= 0:
188 raise ValueError("The polling period should be positive.")
189 if self.polling_timeout <= 0:
190 raise ValueError("The polling timeout should be positive.")
191 self.force_value(
192 "init_shutter_status", self.ShutterStatus(self.init_shutter_status)
193 )
196class CryLasLaserPoller(Poller):
197 """
198 Poller class for polling the laser status until the laser is ready.
200 :raises CryLasLaserError: if the timeout is reached before the laser is ready
201 :raises SerialCommunicationIOError: when communication port is closed.
202 """
204 def __init__(
205 self,
206 spoll_handler: Callable,
207 check_handler: Callable,
208 check_laser_status_handler: Callable,
209 polling_delay_sec: Number = 0,
210 polling_interval_sec: Number = 1,
211 polling_timeout_sec: Optional[Number] = None,
212 ):
213 """
214 Initialize the polling helper.
216 :param spoll_handler: Polling function.
217 :param check_handler: Check polling results.
218 :param check_laser_status_handler: Check laser status.
219 :param polling_delay_sec: Delay before starting the polling, in seconds.
220 :param polling_interval_sec: Polling interval, in seconds.
221 """
222 super().__init__(
223 spoll_handler,
224 polling_delay_sec,
225 polling_interval_sec,
226 polling_timeout_sec
227 )
228 self._check_handler = check_handler
229 self._check_laser_status_handler = check_laser_status_handler
231 def _if_poll_again(
232 self, stop_event: Event, delay_sec: Number, stop_time: Optional[Number]
233 ) -> bool:
234 """
235 Check if to poll again.
237 :param stop_event: Polling stop event.
238 :param delay_sec: Delay time (in seconds).
239 :param stop_time: Absolute stop time.
240 :return: `True` if another polling handler call is due, `False` otherwise.
241 """
242 if_poll_again = super()._if_poll_again(stop_event, delay_sec, stop_time)
243 is_laser_ready = self._check_laser_status_handler()
245 return if_poll_again and not is_laser_ready
247 def _poll_until_stop_or_timeout(self, stop_event: Event) -> Optional[object]:
248 """
249 Thread for polling until stopped, timed-out or the laser is ready.
251 :param stop_event: Event used to stop the polling
252 :return: Last result of the polling function call
253 """
254 last_result = super()._poll_until_stop_or_timeout(stop_event)
256 if stop_event.wait(0):
257 logging.info(f"[{datetime.now()}] Polling status: ... STOPPED")
258 return last_result
259 else:
260 self._check_handler()
262 return last_result
265class CryLasLaser(SingleCommDevice):
266 """
267 CryLas laser controller device class.
268 """
270 class LaserStatus(Enum):
271 """
272 Status of the CryLas laser
273 """
275 UNREADY_INACTIVE = 0
276 READY_INACTIVE = 1
277 READY_ACTIVE = 2
279 @property
280 def is_ready(self):
281 return self is not CryLasLaser.LaserStatus.UNREADY_INACTIVE
283 @property
284 def is_inactive(self):
285 return self is not CryLasLaser.LaserStatus.READY_ACTIVE
287 # status of the shutter
288 ShutterStatus = CryLasLaserShutterStatus
290 class AnswersShutter(Enum):
291 """
292 Standard answers of the CryLas laser controller to `'Shutter'` command passed
293 via `com`.
294 """
296 OPENED = "Shutter aktiv"
297 CLOSED = "Shutter inaktiv"
299 class AnswersStatus(Enum):
300 """
301 Standard answers of the CryLas laser controller to `'STATUS'` command passed
302 via `com`.
303 """
305 TEC1 = "STATUS: TEC1 Regulation ok"
306 TEC2 = "STATUS: TEC2 Regulation ok"
307 HEAD = "STATUS: Head ok"
308 READY = "STATUS: System ready"
309 ACTIVE = "STATUS: Laser active"
310 INACTIVE = "STATUS: Laser inactive"
312 class RepetitionRates(IntEnum):
313 """
314 Repetition rates for the internal software trigger in Hz
315 """
317 _init_ = "value send_value"
318 HARDWARE = 0, 0 # software trigger is disabled, hardware trigger is used
319 # instead (hardware trigger may be internal or external).
320 SOFTWARE_INTERNAL_TEN = 10, 1
321 SOFTWARE_INTERNAL_TWENTY = 20, 2
322 SOFTWARE_INTERNAL_SIXTY = 60, 3
324 def __init__(self, com, dev_config=None):
326 # Call superclass constructor
327 super().__init__(com, dev_config)
329 # laser status
330 self.laser_status = self.LaserStatus.UNREADY_INACTIVE
332 # shutter status
333 self.shutter_status = None
335 # command repetition rate
336 self.repetition_rate = None
338 # command pulse energy (micro joule)
339 self._target_pulse_energy = None
341 # thread that polls the laser status until it is ready
342 self._status_poller = None
344 @property
345 def target_pulse_energy(self):
346 return self._target_pulse_energy
348 @staticmethod
349 def default_com_cls():
350 return CryLasLaserSerialCommunication
352 @staticmethod
353 def config_cls():
354 return CryLasLaserConfig
356 def start(self) -> None:
357 """
358 Opens the communication protocol and configures the device.
360 :raises SerialCommunicationIOError: when communication port cannot be opened
361 """
363 logging.info("Starting device " + str(self))
365 # open the com
366 super().start()
368 # set the init shutter status
369 self.set_init_shutter_status()
370 # check if the laser is ready to be turned on
371 self.update_laser_status()
372 if not self.laser_status.is_ready:
373 logging.info("Laser not ready yet.")
374 # optionally, block execution until laser is ready
375 if self.config.auto_laser_on and not self._is_polling():
376 self._start_polling()
377 elif self.config.auto_laser_on:
378 # turn on the laser
379 self.laser_on()
381 def _start_polling(self):
382 """
383 Start polling laser status.
384 """
385 logging.info("Start polling laser status.")
386 self._status_poller = CryLasLaserPoller(
387 spoll_handler=self.update_laser_status,
388 check_handler=self._after_polling_check_status,
389 check_laser_status_handler=self._check_laser_status_handler,
390 polling_delay_sec=self.config.polling_period,
391 polling_interval_sec=self.config.polling_period,
392 polling_timeout_sec=self.config.polling_timeout
393 )
394 self._status_poller.start_polling()
396 def _check_laser_status_handler(self) -> bool:
397 """
398 Checks whether the laser status is ready.
399 """
400 return self.laser_status.is_ready
402 def _after_polling_check_status(self) -> None:
403 """
404 Thread for polling the laser status until the laser is ready.
406 :raises CryLasLaserError: if the timeout is reached before the laser is ready
407 :raises SerialCommunicationIOError: when communication port is closed.
408 """
410 if not self.laser_status.is_ready:
411 err_msg = "Laser is not yet ready but status polling timed out."
412 logging.error(err_msg)
413 raise CryLasLaserError(err_msg)
415 logging.info(f"[{datetime.now()}] Polling status: ... DONE")
416 if self.config.auto_laser_on:
417 self.laser_on()
419 def _wait_for_polling_result(self):
420 """
421 Wait for until polling function returns a result as well as any exception that
422 might have been raised within a thread.
424 :return: polling function result
425 :raises: polling function errors
426 """
427 return self._status_poller.wait_for_polling_result()
429 def _stop_polling(self):
430 """
431 Stop polling laser status.
433 :return: polling function result
434 :raises: polling function errors
435 """
436 # use the event to stop the thread
437 logging.info("Stop polling laser status.")
438 self._status_poller.stop_polling()
439 return self._wait_for_polling_result()
441 def _is_polling(self) -> bool:
442 """
443 Check if device status is being polled.
445 :return: `True` when polling thread is set and alive
446 """
447 return self._status_poller is not None and self._status_poller.is_polling()
449 def wait_until_ready(self) -> None:
450 """
451 Block execution until the laser is ready
453 :raises CryLasLaserError: if the polling thread stops before the laser is ready
454 """
456 if not self.laser_status.is_ready:
457 if not self._is_polling():
458 self._start_polling()
459 logging.info("Waiting until the laser is ready...")
460 self._wait_for_polling_result()
461 else:
462 logging.info("No need waiting, the laser is already ready.")
464 def stop(self) -> None:
465 """
466 Stops the device and closes the communication protocol.
468 :raises SerialCommunicationIOError: if com port is closed unexpectedly
469 :raises CryLasLaserError: if laser_off() or close_shutter() fail
470 """
472 if self.com.is_open:
473 # turn off the laser
474 self.laser_off()
475 # close the laser shutter
476 self.close_shutter()
477 # cancel the polling thread in case still running
478 if self._is_polling():
479 self._stop_polling()
480 logging.info("The laser polling thread was stopped.")
481 # close the com
482 super().stop()
483 else:
484 logging.warning("Could not turn off the laser, com was already closed.")
486 def update_laser_status(self) -> None:
487 """
488 Update the laser status to `LaserStatus.NOT_READY` or `LaserStatus.INACTIVE` or
489 `LaserStatus.ACTIVE`.
491 Note: laser never explicitly says that it is not ready (
492 `LaserStatus.NOT_READY`) in response to `'STATUS'` command. It only says
493 that it is ready (heated-up and implicitly inactive/off) or active (on). If
494 it's not either of these then the answer is `Answers.HEAD`. Moreover,
495 the only time the laser explicitly says that its status is inactive (
496 `Answers.INACTIVE`) is after issuing a 'LASER OFF' command.
498 :raises SerialCommunicationIOError: when communication port is not opened
499 """
501 # query the status
502 answer_list = self.com.query_all("STATUS", "STATUS:")
503 # analyze the answer
504 if not answer_list:
505 err_msg = "Command to query the laser status did not return an answer."
506 logging.error(err_msg)
507 raise CryLasLaserError(err_msg)
508 else:
509 # at least one line in answer_list
510 for line in answer_list:
511 answer = self.AnswersStatus(line)
512 if answer in (
513 self.AnswersStatus.TEC1,
514 self.AnswersStatus.TEC2,
515 self.AnswersStatus.HEAD,
516 ):
517 new_status = self.LaserStatus.UNREADY_INACTIVE
518 info_msg = "The laser is not ready."
519 elif answer is self.AnswersStatus.READY:
520 new_status = self.LaserStatus.READY_INACTIVE
521 info_msg = "The laser is ready."
522 elif answer is self.AnswersStatus.ACTIVE:
523 new_status = self.LaserStatus.READY_ACTIVE
524 info_msg = "The laser is on."
525 else:
526 err_msg = f'Unexpected "STATUS" query response {answer.value}.'
527 logging.error(err_msg)
528 raise CryLasLaserError(err_msg)
529 self.laser_status = new_status
530 logging.info(info_msg)
532 def update_shutter_status(self) -> None:
533 """
534 Update the shutter status (OPENED or CLOSED)
536 :raises SerialCommunicationIOError: when communication port is not opened
537 :raises CryLasLaserError: if success is not confirmed by the device
538 """
540 # query the status
541 answer = self.com.query("Shutter", "Shutter")
542 # analyse the answer
543 if not answer:
544 err_msg = "Command to query the shutter status did not return an answer."
545 logging.error(err_msg)
546 raise CryLasLaserError(err_msg)
547 else:
548 answer = self.AnswersShutter(answer)
549 if answer is self.AnswersShutter.CLOSED:
550 logging.info("The laser shutter is currently CLOSED.")
551 self.shutter_status = self.ShutterStatus.CLOSED
552 elif answer is self.AnswersShutter.OPENED:
553 logging.info("The laser shutter is currently OPENED.")
554 self.shutter_status = self.ShutterStatus.OPENED
556 def update_repetition_rate(self) -> None:
557 """
558 Query the laser repetition rate.
560 :raises SerialCommunicationIOError: when communication port is not opened
561 :raises CryLasLaserError: if success is not confirmed by the device
562 """
564 # query the repetition rate
565 answer = self.com.query("BOO IP", "Impuls=")
566 # analyse the answer
567 if not answer:
568 err_msg = "Querying the repetition rate did not return an answer."
569 logging.error(err_msg)
570 raise CryLasLaserError(err_msg)
571 elif answer == "Impuls=disabled, extern Trigger":
572 logging.info(
573 "The laser is using a hardware trigger "
574 "(may be internal or external)."
575 )
576 self.repetition_rate = self.RepetitionRates.HARDWARE
577 elif answer.startswith("Impuls=enabled"):
578 match = re.search(r"\d+", answer)
579 if match is None:
580 err_msg = (
581 f"Expected rate integer to follow 'Impuls=enabled' answer "
582 f"in {answer} while querying the repetition."
583 )
584 logging.error(err_msg)
585 raise CryLasLaserError(err_msg)
586 rate = int(match.group(0))
587 self.repetition_rate = self.RepetitionRates(rate)
588 logging.info(
589 "The laser is using the internal " f"software trigger at {rate} Hz."
590 )
592 def update_target_pulse_energy(self) -> None:
593 """
594 Query the laser pulse energy.
596 :raises SerialCommunicationIOError: when communication port is not opened
597 :raises CryLasLaserError: if success is not confirmed by the device
598 """
600 # query the repetition rate
601 answer = self.com.query("BOO SE", "PD-Sollwert=")
602 # analyse the answer
603 if not answer:
604 err_msg = (
605 "Command to query the target pulse energy did not return an answer."
606 )
607 logging.error(err_msg)
608 raise CryLasLaserError(err_msg)
609 else:
610 current_value = int(answer.split("=")[1])
611 current_energy = int(current_value * self.config.calibration_factor / 1000)
612 # update the pulse energy attribute
613 self._target_pulse_energy = current_energy
614 logging.info(f"The target pulse energy is {current_energy} uJ.")
616 def laser_on(self) -> None:
617 """
618 Turn the laser on.
620 :raises SerialCommunicationIOError: when communication port is not opened
621 :raises CryLasLaserNotReadyError: if the laser is not ready to be turned on
622 :raises CryLasLaserError: if success is not confirmed by the device
623 """
625 if not self.laser_status.is_ready:
626 raise CryLasLaserNotReadyError("Laser not ready, cannot be turned on yet.")
627 else:
628 # send the command
629 answer = self.com.query("LASER ON", "STATUS: Laser")
630 # analyse the answer
631 if not answer or self.AnswersStatus(answer) != self.AnswersStatus.ACTIVE:
632 if not self.laser_status.is_inactive:
633 logging.info("Laser is already on.")
634 else:
635 err_msg = (
636 f"Command to turn on the laser "
637 f"{'failed' if answer else 'did not return an answer'}."
638 )
639 logging.error(err_msg)
640 raise CryLasLaserError(err_msg)
641 else:
642 self.laser_status = self.LaserStatus.READY_ACTIVE
643 logging.info("Laser is turned on.")
645 def laser_off(self) -> None:
646 """
647 Turn the laser off.
649 :raises SerialCommunicationIOError: when communication port is not opened
650 :raises CryLasLaserError: if success is not confirmed by the device
651 """
653 # send the command
654 answer = self.com.query("LASER OFF", "STATUS: Laser")
655 # analyse the answer
656 if not answer or self.AnswersStatus(answer) != self.AnswersStatus.INACTIVE:
657 if self.laser_status.is_inactive:
658 logging.info("Laser is already off.")
659 else:
660 err_msg = (
661 f"Command to turn off the laser "
662 f"{'failed' if answer else 'did not return an answer'}."
663 )
664 logging.error(err_msg)
665 raise CryLasLaserError(err_msg)
666 else:
667 self.laser_status = self.LaserStatus.READY_INACTIVE
668 logging.info("Laser is turned off.")
670 def open_shutter(self) -> None:
671 """
672 Open the laser shutter.
674 :raises SerialCommunicationIOError: when communication port is not opened
675 :raises CryLasLaserError: if success is not confirmed by the device
676 """
678 # send the command
679 answer = self.com.query(
680 f"Shutter {self.config.ShutterStatus.OPENED.value}", "Shutter"
681 )
682 # analyse the answer
683 if not answer or self.AnswersShutter(answer) != self.AnswersShutter.OPENED:
684 err_msg = (
685 f"Opening laser shutter "
686 f"{'failed' if answer else 'did not return an answer'}."
687 )
688 logging.error(err_msg)
689 raise CryLasLaserError(err_msg)
690 else:
691 logging.info("Opening laser shutter succeeded.")
692 self.shutter_status = self.ShutterStatus.OPENED
694 def close_shutter(self) -> None:
695 """
696 Close the laser shutter.
698 :raises SerialCommunicationIOError: when communication port is not opened
699 :raises CryLasLaserError: if success is not confirmed by the device
700 """
702 # send the command
703 answer = self.com.query(
704 f"Shutter {self.config.ShutterStatus.CLOSED.value}", "Shutter"
705 )
706 # analyse the answer
707 if not answer or self.AnswersShutter(answer) != self.AnswersShutter.CLOSED:
708 err_msg = (
709 f"Closing laser shutter "
710 f"{'failed' if answer else 'did not return an answer'}."
711 )
712 logging.error(err_msg)
713 raise CryLasLaserError(err_msg)
714 else:
715 logging.info("Closing laser shutter succeeded.")
716 self.shutter_status = self.ShutterStatus.CLOSED
718 def set_init_shutter_status(self) -> None:
719 """
720 Open or close the shutter, to match the configured shutter_status.
722 :raises SerialCommunicationIOError: when communication port is not opened
723 :raises CryLasLaserError: if success is not confirmed by the device
724 """
726 self.update_shutter_status()
727 if self.config.init_shutter_status != self.shutter_status:
728 if self.config.init_shutter_status == self.ShutterStatus.CLOSED:
729 self.close_shutter()
730 elif self.config.init_shutter_status == self.ShutterStatus.OPENED:
731 self.open_shutter()
733 def get_pulse_energy_and_rate(self) -> Tuple[int, int]:
734 """
735 Use the debug mode, return the measured pulse energy and rate.
737 :return: (energy in micro joule, rate in Hz)
738 :raises SerialCommunicationIOError: when communication port is not opened
739 :raises CryLasLaserError: if the device does not answer the query
740 """
742 # send the command (enter the debug mode)
743 answer = self.com.query("DB1", "IST:", "DB0")
744 if not answer:
745 err_msg = "Command to find laser energy and rate did not return an answer."
746 logging.error(err_msg)
747 raise CryLasLaserError(err_msg)
748 else:
749 # the answer should look like 'IST: LDTemp=x, NLOTemp=x, CaseTemp=x,
750 # PD=x, D_I= x, freq=x, LDE=x, RegOFF=x' where x are integers
751 # find the values of interest
752 values = re.findall(r"\d+", answer)
753 energy = int(int(values[3]) * self.config.calibration_factor / 1000)
754 rate = int(values[5])
755 logging.info(f"Laser energy: {energy} uJ, repetition rate: {rate} Hz.")
756 return energy, rate
758 def set_repetition_rate(self, rate: Union[int, RepetitionRates]) -> None:
759 """
760 Sets the repetition rate of the internal software trigger.
762 :param rate: frequency (Hz) as an integer
763 :raises ValueError: if rate is not an accepted value in RepetitionRates Enum
764 :raises SerialCommunicationIOError: when communication port is not opened
765 :raises CryLasLaserError: if success is not confirmed by the device
766 """
768 # check if the value is ok
769 if not isinstance(rate, self.RepetitionRates):
770 try:
771 rate = self.RepetitionRates(rate)
772 except ValueError as e:
773 logging.error(str(e))
774 raise e
775 # send the corresponding value to the controller
776 answer = self.com.query(f"BOO IP{rate.send_value}", "Impuls=")
777 # check the success of the command
778 if rate != self.RepetitionRates.HARDWARE:
779 # check whether the expected answer is obtained
780 if answer != f"Impuls=enabled {rate}Hz":
781 err_msg = (
782 f"Setting the repetition rate failed. Controller answered:"
783 f" {answer}."
784 )
785 logging.error(err_msg)
786 raise CryLasLaserError(err_msg)
787 else:
788 logging.info(f"Laser internal software trigger set to {rate.value} Hz")
789 else:
790 # For the internal hardware trigger rate, there is no way to check success
791 logging.info("Using laser internal hardware trigger.")
792 # update the repetition rate attribute
793 self.repetition_rate = rate
795 def set_pulse_energy(self, energy: int) -> None:
796 """
797 Sets the energy of pulses (works only with external hardware trigger).
798 Proceed with small energy steps, or the regulation may fail.
800 :param energy: energy in micro joule
801 :raises SerialCommunicationIOError: when communication port is not opened
802 :raises CryLasLaserError: if the device does not confirm success
803 """
805 # convert the input value into the required command value
806 command_value = int(energy * 1000 / self.config.calibration_factor)
807 # send the command
808 answer = self.com.query(f"BOO SE {command_value}", "PD-Sollwert=")
809 # analyse the answer
810 if not answer:
811 err_msg = "Command to set the pulse energy did not return an answer."
812 logging.error(err_msg)
813 raise CryLasLaserError(err_msg)
814 else:
815 current_value = int(answer.split("=")[1])
816 current_energy = int(current_value * self.config.calibration_factor / 1000)
817 # update the pulse energy attribute
818 self._target_pulse_energy = current_energy
819 if current_value != command_value:
820 message = (
821 f"Command to set the pulse energy failed. "
822 f"The pulse energy is currently set to {current_energy} "
823 f"micro joule. Setting a different value is only possible "
824 f"when using an external hardware trigger."
825 )
826 logging.error(message)
827 raise CryLasLaserError(message)
828 else:
829 logging.info(f"Successfully set pulse energy to {energy}")
832class CryLasLaserError(Exception):
833 """
834 General error with the CryLas Laser.
835 """
837 pass
840class CryLasLaserNotReadyError(CryLasLaserError):
841 """
842 Error when trying to turn on the CryLas Laser before it is ready.
843 """
845 pass
848@configdataclass
849class CryLasAttenuatorSerialCommunicationConfig(SerialCommunicationConfig):
850 #: Baudrate for CryLas attenuator is 9600 baud
851 baudrate: int = 9600
853 #: CryLas attenuator does not use parity
854 parity: Union[
855 str, SerialCommunicationParity
856 ] = SerialCommunicationParity.NONE
858 #: CryLas attenuator uses one stop bit
859 stopbits: Union[
860 int, SerialCommunicationStopbits
861 ] = SerialCommunicationStopbits.ONE
863 #: One byte is eight bits long
864 bytesize: Union[
865 int, SerialCommunicationBytesize
866 ] = SerialCommunicationBytesize.EIGHTBITS
868 #: No terminator
869 terminator: bytes = b""
871 #: use 3 seconds timeout as default
872 timeout: Number = 3
875class CryLasAttenuatorSerialCommunication(SerialCommunication):
876 """
877 Specific communication protocol implementation for
878 the CryLas attenuator.
879 Already predefines device-specific protocol parameters in config.
880 """
882 @staticmethod
883 def config_cls():
884 return CryLasAttenuatorSerialCommunicationConfig
887@configdataclass
888class CryLasAttenuatorConfig:
889 """
890 Device configuration dataclass for CryLas attenuator.
891 """
893 # initial/default attenuation value which is set on start()
894 init_attenuation: Number = 0
895 response_sleep_time: Number = 1
897 def clean_values(self):
898 if not 0 <= self.init_attenuation <= 100:
899 raise ValueError("Attenuation should be " "between 0 and 100 included.")
900 if self.response_sleep_time <= 0:
901 raise ValueError("Response sleep time should be positive.")
904class CryLasAttenuator(SingleCommDevice):
905 """
906 Device class for the CryLas laser attenuator.
907 """
909 def __init__(self, com, dev_config=None):
910 # Call superclass constructor
911 super().__init__(com, dev_config)
913 # attenuation of the laser light in percent (not determined yet)
914 self._attenuation = None
916 @staticmethod
917 def default_com_cls():
918 return CryLasAttenuatorSerialCommunication
920 @staticmethod
921 def config_cls():
922 return CryLasAttenuatorConfig
924 @property
925 def attenuation(self) -> Number:
926 return self._attenuation
928 @property
929 def transmission(self) -> Number:
930 return 100 - self._attenuation
932 def start(self) -> None:
933 """
934 Open the com, apply the config value 'init_attenuation'
936 :raises SerialCommunicationIOError: when communication port cannot be opened
937 """
939 super().start()
940 self.set_init_attenuation()
942 def set_init_attenuation(self):
943 """
944 Sets the attenuation to its configured initial/default value
946 :raises SerialCommunicationIOError: when communication port is not opened
947 """
949 self.set_attenuation(self.config.init_attenuation)
951 def set_attenuation(self, percent: Number) -> None:
952 """
953 Set the percentage of attenuated light (inverse of set_transmission).
954 :param percent: percentage of attenuation, number between 0 and 100
955 :raises ValueError: if param percent not between 0 and 100
956 :raises SerialCommunicationIOError: when communication port is not opened
957 :raises CryLasAttenuatorError: if the device does not confirm success
958 """
960 if not 0 <= percent <= 100:
961 raise ValueError("Attenuation should be between 0 and 100 included.")
962 else:
963 pulse = int(math.asin((50 - percent) * 0.02) * 536.5 + 6000)
964 prepulse = pulse - 400
965 # prepare the values to send with the motor protocol
966 lsb = prepulse % 128
967 msb = math.floor(prepulse / 128)
968 # send the values
969 self.com.write_bytes(bytes([132, 0, lsb, msb]))
970 time.sleep(self.config.response_sleep_time)
971 lsb = pulse % 128
972 msb = math.floor(pulse / 128)
973 self.com.write_bytes(bytes([132, 0, lsb, msb]))
974 time.sleep(self.config.response_sleep_time / 10)
975 self.com.write_bytes(bytes([161]))
976 time.sleep(self.config.response_sleep_time / 10)
977 b1 = self.com.read_bytes()
978 b2 = self.com.read_bytes()
979 logging.debug(f"b1 = {b1}, b2 = {b2}")
980 if b1 != b"\x00" or b2 != b"\x00":
981 err_msg = f"Setting laser attenuation to {percent} percents failed."
982 logging.error(err_msg)
983 raise CryLasAttenuatorError(err_msg)
984 else:
985 logging.info(
986 f"Successfully set laser attenuation to {percent} Cpercents."
987 )
988 self._attenuation = percent
990 def set_transmission(self, percent: Number) -> None:
991 """
992 Set the percentage of transmitted light (inverse of set_attenuation).
993 :param percent: percentage of transmitted light
994 :raises ValueError: if param percent not between 0 and 100
995 :raises SerialCommunicationIOError: when communication port is not opened
996 :raises CryLasAttenuatorError: if the device does not confirm success
997 """
999 self.set_attenuation(100 - percent)
1002class CryLasAttenuatorError(Exception):
1003 """
1004 General error with the CryLas Attenuator.
1005 """
1007 pass