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

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

## 408 statements

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

4 Device classes for a CryLas pulsed laser controller and a CryLas laser attenuator,

5 using serial communication.

6

7 There are three modes of operation for the laser

8 1. Laser-internal hardware trigger (default): fixed to 20 Hz and max energy per pulse.

9 2. Laser-internal software trigger (for diagnosis only).

10 3. 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

13 After switching on the laser with laser_on(), the system must stabilize

14 for some minutes. Do not apply abrupt changes of pulse energy or repetition rate.

15

16 Manufacturer homepage:

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

18"""

19

20 import logging

21 import math

22 import re

23 import time

24 from datetime import datetime

25 from threading import Event

26 from typing import Union, Tuple, Optional, Callable

27

28 from aenum import Enum, IntEnum

29

30 from .base import SingleCommDevice

31 from .utils import Poller

32 from ..comm import SerialCommunication, SerialCommunicationConfig

33 from ..comm.serial import (

34 SerialCommunicationParity,

35 SerialCommunicationStopbits,

36 SerialCommunicationBytesize,

37)

38 from ..configuration import configdataclass

39 from ..utils.typing import Number

40

41

42@configdataclass

43 class 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

69 class 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

148 class CryLasLaserShutterStatus(Enum):

149 """

150 Status of the CryLas laser shutter

151 """

152

153 CLOSED = 0

154 OPENED = 1

155

156

157@configdataclass

158 class 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

196 class 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

265 class 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

832 class CryLasLaserError(Exception):

833 """

834 General error with the CryLas Laser.

835 """

836

837 pass

838

839

840 class 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

849 class 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

875 class 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

888 class 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

904 class 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

1002 class CryLasAttenuatorError(Exception):

1003 """

1004 General error with the CryLas Attenuator.

1005 """

1006

1007 pass