Hide keyboard shortcuts

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. 

6 

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. 

12 

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. 

15 

16Manufacturer homepage: 

17https://www.crylas.de/products/pulsed_laser.html 

18""" 

19 

20import logging 

21import math 

22import re 

23import time 

24from datetime import datetime 

25from threading import Event 

26from typing import Union, Tuple, Optional, Callable 

27 

28from aenum import Enum, IntEnum 

29 

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 

40 

41 

42@configdataclass 

43class CryLasLaserSerialCommunicationConfig(SerialCommunicationConfig): 

44 #: Baudrate for CryLas laser is 19200 baud 

45 baudrate: int = 19200 

46 

47 #: CryLas laser does not use parity 

48 parity: Union[ 

49 str, SerialCommunicationParity 

50 ] = SerialCommunicationParity.NONE 

51 

52 #: CryLas laser uses one stop bit 

53 stopbits: Union[ 

54 int, SerialCommunicationStopbits 

55 ] = SerialCommunicationStopbits.ONE 

56 

57 #: One byte is eight bits long 

58 bytesize: Union[ 

59 int, SerialCommunicationBytesize 

60 ] = SerialCommunicationBytesize.EIGHTBITS 

61 

62 #: The terminator is LF 

63 terminator: bytes = b"\n" 

64 

65 #: use 10 seconds timeout as default (a long timeout is needed!) 

66 timeout: Number = 10 

67 

68 

69class CryLasLaserSerialCommunication(SerialCommunication): 

70 """ 

71 Specific communication protocol implementation for the CryLas laser controller. 

72 Already predefines device-specific protocol parameters in config. 

73 """ 

74 

75 @staticmethod 

76 def config_cls(): 

77 return CryLasLaserSerialCommunicationConfig 

78 

79 READ_TEXT_SKIP_PREFIXES = (">", "MODE:") 

80 """Prefixes of lines that are skipped when read from the serial port.""" 

81 

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`. 

86 

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 

97 

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. 

102 

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 """ 

109 

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 

121 

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. 

127 

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 """ 

133 

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 

146 

147 

148class CryLasLaserShutterStatus(Enum): 

149 """ 

150 Status of the CryLas laser shutter 

151 """ 

152 

153 CLOSED = 0 

154 OPENED = 1 

155 

156 

157@configdataclass 

158class CryLasLaserConfig: 

159 """ 

160 Device configuration dataclass for the CryLas laser controller. 

161 """ 

162 

163 # status of the shutter 

164 ShutterStatus = CryLasLaserShutterStatus 

165 

166 # calibration factor for the pulse energy 

167 calibration_factor: Number = 4.35 

168 

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 

172 

173 # timeout (s) when polling the laser status, CryLasLaserError is raised on timeout 

174 polling_timeout: Number = 300 

175 

176 # automatically turn on the laser when ready 

177 auto_laser_on: bool = True 

178 

179 # status of the shutter to be set on start 

180 init_shutter_status: Union[ 

181 int, CryLasLaserShutterStatus 

182 ] = CryLasLaserShutterStatus.CLOSED 

183 

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 ) 

194 

195 

196class CryLasLaserPoller(Poller): 

197 """ 

198 Poller class for polling the laser status until the laser is ready. 

199 

200 :raises CryLasLaserError: if the timeout is reached before the laser is ready 

201 :raises SerialCommunicationIOError: when communication port is closed. 

202 """ 

203 

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. 

215 

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 

230 

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. 

236 

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() 

244 

245 return if_poll_again and not is_laser_ready 

246 

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. 

250 

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) 

255 

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() 

261 

262 return last_result 

263 

264 

265class CryLasLaser(SingleCommDevice): 

266 """ 

267 CryLas laser controller device class. 

268 """ 

269 

270 class LaserStatus(Enum): 

271 """ 

272 Status of the CryLas laser 

273 """ 

274 

275 UNREADY_INACTIVE = 0 

276 READY_INACTIVE = 1 

277 READY_ACTIVE = 2 

278 

279 @property 

280 def is_ready(self): 

281 return self is not CryLasLaser.LaserStatus.UNREADY_INACTIVE 

282 

283 @property 

284 def is_inactive(self): 

285 return self is not CryLasLaser.LaserStatus.READY_ACTIVE 

286 

287 # status of the shutter 

288 ShutterStatus = CryLasLaserShutterStatus 

289 

290 class AnswersShutter(Enum): 

291 """ 

292 Standard answers of the CryLas laser controller to `'Shutter'` command passed 

293 via `com`. 

294 """ 

295 

296 OPENED = "Shutter aktiv" 

297 CLOSED = "Shutter inaktiv" 

298 

299 class AnswersStatus(Enum): 

300 """ 

301 Standard answers of the CryLas laser controller to `'STATUS'` command passed 

302 via `com`. 

303 """ 

304 

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" 

311 

312 class RepetitionRates(IntEnum): 

313 """ 

314 Repetition rates for the internal software trigger in Hz 

315 """ 

316 

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 

323 

324 def __init__(self, com, dev_config=None): 

325 

326 # Call superclass constructor 

327 super().__init__(com, dev_config) 

328 

329 # laser status 

330 self.laser_status = self.LaserStatus.UNREADY_INACTIVE 

331 

332 # shutter status 

333 self.shutter_status = None 

334 

335 # command repetition rate 

336 self.repetition_rate = None 

337 

338 # command pulse energy (micro joule) 

339 self._target_pulse_energy = None 

340 

341 # thread that polls the laser status until it is ready 

342 self._status_poller = None 

343 

344 @property 

345 def target_pulse_energy(self): 

346 return self._target_pulse_energy 

347 

348 @staticmethod 

349 def default_com_cls(): 

350 return CryLasLaserSerialCommunication 

351 

352 @staticmethod 

353 def config_cls(): 

354 return CryLasLaserConfig 

355 

356 def start(self) -> None: 

357 """ 

358 Opens the communication protocol and configures the device. 

359 

360 :raises SerialCommunicationIOError: when communication port cannot be opened 

361 """ 

362 

363 logging.info("Starting device " + str(self)) 

364 

365 # open the com 

366 super().start() 

367 

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() 

380 

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() 

395 

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 

401 

402 def _after_polling_check_status(self) -> None: 

403 """ 

404 Thread for polling the laser status until the laser is ready. 

405 

406 :raises CryLasLaserError: if the timeout is reached before the laser is ready 

407 :raises SerialCommunicationIOError: when communication port is closed. 

408 """ 

409 

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) 

414 

415 logging.info(f"[{datetime.now()}] Polling status: ... DONE") 

416 if self.config.auto_laser_on: 

417 self.laser_on() 

418 

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. 

423 

424 :return: polling function result 

425 :raises: polling function errors 

426 """ 

427 return self._status_poller.wait_for_polling_result() 

428 

429 def _stop_polling(self): 

430 """ 

431 Stop polling laser status. 

432 

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() 

440 

441 def _is_polling(self) -> bool: 

442 """ 

443 Check if device status is being polled. 

444 

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() 

448 

449 def wait_until_ready(self) -> None: 

450 """ 

451 Block execution until the laser is ready 

452 

453 :raises CryLasLaserError: if the polling thread stops before the laser is ready 

454 """ 

455 

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.") 

463 

464 def stop(self) -> None: 

465 """ 

466 Stops the device and closes the communication protocol. 

467 

468 :raises SerialCommunicationIOError: if com port is closed unexpectedly 

469 :raises CryLasLaserError: if laser_off() or close_shutter() fail 

470 """ 

471 

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.") 

485 

486 def update_laser_status(self) -> None: 

487 """ 

488 Update the laser status to `LaserStatus.NOT_READY` or `LaserStatus.INACTIVE` or 

489 `LaserStatus.ACTIVE`. 

490 

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. 

497 

498 :raises SerialCommunicationIOError: when communication port is not opened 

499 """ 

500 

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) 

531 

532 def update_shutter_status(self) -> None: 

533 """ 

534 Update the shutter status (OPENED or CLOSED) 

535 

536 :raises SerialCommunicationIOError: when communication port is not opened 

537 :raises CryLasLaserError: if success is not confirmed by the device 

538 """ 

539 

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 

555 

556 def update_repetition_rate(self) -> None: 

557 """ 

558 Query the laser repetition rate. 

559 

560 :raises SerialCommunicationIOError: when communication port is not opened 

561 :raises CryLasLaserError: if success is not confirmed by the device 

562 """ 

563 

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 ) 

591 

592 def update_target_pulse_energy(self) -> None: 

593 """ 

594 Query the laser pulse energy. 

595 

596 :raises SerialCommunicationIOError: when communication port is not opened 

597 :raises CryLasLaserError: if success is not confirmed by the device 

598 """ 

599 

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.") 

615 

616 def laser_on(self) -> None: 

617 """ 

618 Turn the laser on. 

619 

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 """ 

624 

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.") 

644 

645 def laser_off(self) -> None: 

646 """ 

647 Turn the laser off. 

648 

649 :raises SerialCommunicationIOError: when communication port is not opened 

650 :raises CryLasLaserError: if success is not confirmed by the device 

651 """ 

652 

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.") 

669 

670 def open_shutter(self) -> None: 

671 """ 

672 Open the laser shutter. 

673 

674 :raises SerialCommunicationIOError: when communication port is not opened 

675 :raises CryLasLaserError: if success is not confirmed by the device 

676 """ 

677 

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 

693 

694 def close_shutter(self) -> None: 

695 """ 

696 Close the laser shutter. 

697 

698 :raises SerialCommunicationIOError: when communication port is not opened 

699 :raises CryLasLaserError: if success is not confirmed by the device 

700 """ 

701 

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 

717 

718 def set_init_shutter_status(self) -> None: 

719 """ 

720 Open or close the shutter, to match the configured shutter_status. 

721 

722 :raises SerialCommunicationIOError: when communication port is not opened 

723 :raises CryLasLaserError: if success is not confirmed by the device 

724 """ 

725 

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() 

732 

733 def get_pulse_energy_and_rate(self) -> Tuple[int, int]: 

734 """ 

735 Use the debug mode, return the measured pulse energy and rate. 

736 

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 """ 

741 

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 

757 

758 def set_repetition_rate(self, rate: Union[int, RepetitionRates]) -> None: 

759 """ 

760 Sets the repetition rate of the internal software trigger. 

761 

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 """ 

767 

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 

794 

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. 

799 

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 """ 

804 

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}") 

830 

831 

832class CryLasLaserError(Exception): 

833 """ 

834 General error with the CryLas Laser. 

835 """ 

836 

837 pass 

838 

839 

840class CryLasLaserNotReadyError(CryLasLaserError): 

841 """ 

842 Error when trying to turn on the CryLas Laser before it is ready. 

843 """ 

844 

845 pass 

846 

847 

848@configdataclass 

849class CryLasAttenuatorSerialCommunicationConfig(SerialCommunicationConfig): 

850 #: Baudrate for CryLas attenuator is 9600 baud 

851 baudrate: int = 9600 

852 

853 #: CryLas attenuator does not use parity 

854 parity: Union[ 

855 str, SerialCommunicationParity 

856 ] = SerialCommunicationParity.NONE 

857 

858 #: CryLas attenuator uses one stop bit 

859 stopbits: Union[ 

860 int, SerialCommunicationStopbits 

861 ] = SerialCommunicationStopbits.ONE 

862 

863 #: One byte is eight bits long 

864 bytesize: Union[ 

865 int, SerialCommunicationBytesize 

866 ] = SerialCommunicationBytesize.EIGHTBITS 

867 

868 #: No terminator 

869 terminator: bytes = b"" 

870 

871 #: use 3 seconds timeout as default 

872 timeout: Number = 3 

873 

874 

875class CryLasAttenuatorSerialCommunication(SerialCommunication): 

876 """ 

877 Specific communication protocol implementation for 

878 the CryLas attenuator. 

879 Already predefines device-specific protocol parameters in config. 

880 """ 

881 

882 @staticmethod 

883 def config_cls(): 

884 return CryLasAttenuatorSerialCommunicationConfig 

885 

886 

887@configdataclass 

888class CryLasAttenuatorConfig: 

889 """ 

890 Device configuration dataclass for CryLas attenuator. 

891 """ 

892 

893 # initial/default attenuation value which is set on start() 

894 init_attenuation: Number = 0 

895 response_sleep_time: Number = 1 

896 

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.") 

902 

903 

904class CryLasAttenuator(SingleCommDevice): 

905 """ 

906 Device class for the CryLas laser attenuator. 

907 """ 

908 

909 def __init__(self, com, dev_config=None): 

910 # Call superclass constructor 

911 super().__init__(com, dev_config) 

912 

913 # attenuation of the laser light in percent (not determined yet) 

914 self._attenuation = None 

915 

916 @staticmethod 

917 def default_com_cls(): 

918 return CryLasAttenuatorSerialCommunication 

919 

920 @staticmethod 

921 def config_cls(): 

922 return CryLasAttenuatorConfig 

923 

924 @property 

925 def attenuation(self) -> Number: 

926 return self._attenuation 

927 

928 @property 

929 def transmission(self) -> Number: 

930 return 100 - self._attenuation 

931 

932 def start(self) -> None: 

933 """ 

934 Open the com, apply the config value 'init_attenuation' 

935 

936 :raises SerialCommunicationIOError: when communication port cannot be opened 

937 """ 

938 

939 super().start() 

940 self.set_init_attenuation() 

941 

942 def set_init_attenuation(self): 

943 """ 

944 Sets the attenuation to its configured initial/default value 

945 

946 :raises SerialCommunicationIOError: when communication port is not opened 

947 """ 

948 

949 self.set_attenuation(self.config.init_attenuation) 

950 

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 """ 

959 

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 

989 

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 """ 

998 

999 self.set_attenuation(100 - percent) 

1000 

1001 

1002class CryLasAttenuatorError(Exception): 

1003 """ 

1004 General error with the CryLas Attenuator. 

1005 """ 

1006 

1007 pass