Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\newport.py: 41%

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

## 480 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 class for Newport SMC100PP stepper motor controller with serial communication.

5

6 The SMC100PP is a single axis motion controller/driver for stepper motors up to 48 VDC

7 at 1.5 A rms. Up to 31 controllers can be networked through the internal RS-485

8 communication link.

9

10 Manufacturer homepage:

11 https://www.newport.com/f/smc100-single-axis-dc-or-stepper-motion-controller

12"""

13

14 import logging

15 from time import sleep, time

16 from typing import Union, Dict, List

17

18# Note: PyCharm does not recognize the dependency correctly, it is added as pyserial.

19 import serial

20 from aenum import Enum, IntEnum

21

22 from .base import SingleCommDevice

23 from ..comm import SerialCommunication, SerialCommunicationConfig

24 from ..comm.serial import (

25 SerialCommunicationParity,

26 SerialCommunicationStopbits,

27 SerialCommunicationBytesize,

28 SerialCommunicationIOError,

29)

30 from ..configuration import configdataclass

31 from ..utils.enum import NameEnum, AutoNumberNameEnum

32 from ..utils.typing import Number

33

34 Param = Union[Number, str, None]

35

36

37@configdataclass

38 class NewportSMC100PPSerialCommunicationConfig(SerialCommunicationConfig):

39 #: Baudrate for Heinzinger power supplies is 9600 baud

40 baudrate: int = 57600

41

42 #: Heinzinger does not use parity

43 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE

44

45 #: Heinzinger uses one stop bit

46 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE

47

48 #: One byte is eight bits long

49 bytesize: Union[

50 int, SerialCommunicationBytesize

51 ] = SerialCommunicationBytesize.EIGHTBITS

52

53 #: The terminator is CR/LF

54 terminator: bytes = b"\r\n"

55

56 #: use 10 seconds timeout as default

57 timeout: Number = 10

58

59

60 class NewportSMC100PPSerialCommunication(SerialCommunication):

61 """

62 Specific communication protocol implementation Heinzinger power supplies.

63 Already predefines device-specific protocol parameters in config.

64 """

65

66 class ControllerErrors(Enum):

67 """

68 Possible controller errors with values as returned by the device in response

69 to sent commands.

70 """

71

72 _init_ = "value message"

73

74 NO_ERROR = "@", "No error."

75 CODE_OR_ADDR_INVALID = (

76 "A",

77 "Unknown message code or floating point controller address.",

78 )

79 ADDR_INCORRECT = "B", "Controller address not correct."

80 PARAM_MISSING_OR_INVALID = "C", "Parameter missing or out of range."

81 CMD_NOT_ALLOWED = "D", "Command not allowed."

82 HOME_STARTED = "E", "Home sequence already started."

83 ESP_STAGE_NAME_INVALID = (

84 "F",

85 "ESP stage name unknown.",

86 )

87 DISPLACEMENT_OUT_OF_LIMIT = (

88 "G",

89 "Displacement out of limits.",

90 )

91 CMD_NOT_ALLOWED_NOT_REFERENCED = (

92 "H",

93 "Command not allowed in NOT REFERENCED state.",

94 )

95 CMD_NOT_ALLOWED_CONFIGURATION = (

96 "I",

97 "Command not allowed in CONFIGURATION state.",

98 )

99 CMD_NOT_ALLOWED_DISABLE = "J", "Command not allowed in DISABLE state."

100 CMD_NOT_ALLOWED_READY = "K", "Command not allowed in READY state."

101 CMD_NOT_ALLOWED_HOMING = "L", "Command not allowed in HOMING state."

102 CMD_NOT_ALLOWED_MOVING = "M", "Command not allowed in MOVING state."

103 POSITION_OUT_OF_LIMIT = "N", "Current position out of software limit."

104 COM_TIMEOUT = (

105 "S",

106 "Communication Time Out.",

107 )

108 EEPROM_ACCESS_ERROR = "U", "Error during EEPROM access."

109 CMD_EXEC_ERROR = "V", "Error during command execution."

110 CMD_NOT_ALLOWED_PP = "W", "Command not allowed for PP version."

111 CMD_NOT_ALLOWED_CC = "X", "Command not allowed for CC version."

112

113 def __init__(self, configuration):

114 """

115 Constructor for NewportSMC100PPSerialCommunication.

116 """

117

118 super().__init__(configuration)

119

120 self.logger = logging.getLogger(__name__)

121

122 @staticmethod

123 def config_cls():

124 return NewportSMC100PPSerialCommunicationConfig

125

126 def read_text(self) -> str:

127 """

128 Read one line of text from the serial port, and check for presence of a null

129 char which indicates that the motor power supply was cut and then restored. The

130 input buffer may hold additional data afterwards, since only one line is read.

131

132 This method uses `self.access_lock` to ensure thread-safety.

133

134 :return: String read from the serial port; `''` if there was nothing to read.

135 :raises SerialCommunicationIOError: when communication port is not opened

136 :raises NewportMotorPowerSupplyWasCutError: if a null char is read

137 """

138

139 with self.access_lock:

140 try:

141 line = self._serial_port.readline()

142 if b'\x00' in line:

143 raise NewportMotorPowerSupplyWasCutError(

144 'Unexpected message from motor:', line)

145 return line.decode(self.ENCODING)

146 except serial.SerialException as exc:

147 raise SerialCommunicationIOError from exc

148

149 def _send_command_without_checking_error(

150 self, add: int, cmd: str, param: Param = None

151 ) -> None:

152 """

153 Send a command to the controller.

154

155 :param add: the controller address (1 to 31)

156 :param cmd: the command to be sent

157 :param param: optional parameter (int/float/str) appended to the command

158 """

159

160 if param is None:

161 param = ""

162

163 with self.access_lock:

164 self.write_text(f"{add}{cmd}{param}")

165 self.logger.debug(f"sent: {add}{cmd}{param}")

166

167 def _query_without_checking_errors(

168 self, add: int, cmd: str, param: Param = None

169 ) -> str:

170 """

171 Send a command to the controller and read the answer. The prefix add+cmd is

172 removed from the answer.

173

174 :param add: the controller address (1 to 31)

175 :param cmd: the command to be sent

176 :param param: optional parameter (int/float/str) appended to the command

177 :return: the answer from the device without the prefix

178 :raises SerialCommunicationIOError: if the com is closed

179 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

180 :raises NewportControllerError: if the controller reports an error

181 """

182

183 if param is None:

184 param = ""

185

186 prefix = f"{add}{cmd}"

187 query = f"{add}{cmd}{param}"

188

189 with self.access_lock:

190 self._send_command_without_checking_error(add, cmd, param)

191 sleep(0.01)

192 answer = self.read_text().strip()

193 if len(answer) == 0:

194 message = f"Newport controller {add} did not answer to query {query}."

195 self.logger.error(message)

196 raise NewportSerialCommunicationError(message)

197 elif not answer.startswith(prefix):

198 message = (

199 f"Newport controller {add} answer {answer} to query {query} "

200 f"does not start with expected prefix {prefix}."

201 )

202 self.logger.error(message)

203 raise NewportSerialCommunicationError(message)

204 else:

205 self.logger.debug(f"Newport com: {answer}")

206 return answer[len(prefix):].strip()

207

208 def check_for_error(self, add: int) -> None:

209 """

210 Ask the Newport controller for the last error it recorded.

211

212 This method is called after every command or query.

213

214 :param add: controller address (1 to 31)

215 :raises SerialCommunicationIOError: if the com is closed

216 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

217 :raises NewportControllerError: if the controller reports an error

218 """

219

220 with self.access_lock:

221 error = self.ControllerErrors(

222 self._query_without_checking_errors(add, "TE")

223 )

224 if error is not self.ControllerErrors.NO_ERROR:

225 self.logger.error(f"NewportControllerError: {error.message}")

226 raise NewportControllerError(error.message)

227

228 def send_command(self, add: int, cmd: str, param: Param = None) -> None:

229 """

230 Send a command to the controller, and check for errors.

231

232 :param add: the controller address (1 to 31)

233 :param cmd: the command to be sent

234 :param param: optional parameter (int/float/str) appended to the command

235 :raises SerialCommunicationIOError: if the com is closed

236 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

237 :raises NewportControllerError: if the controller reports an error

238 """

239

240 if param is None:

241 param = ""

242

243 with self.access_lock:

244 self._send_command_without_checking_error(add, cmd, param)

245 self.check_for_error(add)

246

247 def send_stop(self, add: int) -> None:

248 """

249 Send the general stop ST command to the controller, and check for errors.

250

251 :param add: the controller address (1 to 31)

252 :return: ControllerErrors reported by Newport Controller

253 :raises SerialCommunicationIOError: if the com is closed

254 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

255 """

256

257 with self.access_lock:

258 self.write_text("ST")

259 self.check_for_error(add)

260

261 def query(self, add: int, cmd: str, param: Param = None) -> str:

262 """

263 Send a query to the controller, read the answer, and check for errors. The

264 prefix add+cmd is removed from the answer.

265

266 :param add: the controller address (1 to 31)

267 :param cmd: the command to be sent

268 :param param: optional parameter (int/float/str) appended to the command

269 :return: the answer from the device without the prefix

270 :raises SerialCommunicationIOError: if the com is closed

271 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

272 :raises NewportControllerError: if the controller reports an error

273 """

274

275 with self.access_lock:

276

277 try:

278 answer = self._query_without_checking_errors(add, cmd, param)

279 finally:

280 self.check_for_error(add)

281

282 return answer

283

284 def query_multiple(self, add: int, cmd: str, prefixes: List[str]) -> List[str]:

285 """

286 Send a query to the controller, read the answers, and check for errors. The

287 prefixes are removed from the answers.

288

289 :param add: the controller address (1 to 31)

290 :param cmd: the command to be sent

291 :param prefixes: prefixes of each line expected in the answer

292 :return: list of answers from the device without prefix

293 :raises SerialCommunicationIOError: if the com is closed

294 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

295 :raises NewportControllerError: if the controller reports an error

296 """

297

298 with self.access_lock:

299

300 try:

301 self._send_command_without_checking_error(add, cmd)

302 answer = []

303 for prefix in prefixes:

304 line = self.read_text().strip()

305 if not line.startswith(prefix):

306 message = (

307 f"Newport controller {add} answer {line} to command "

308 f"{cmd} does not start with expected prefix {prefix}."

309 )

310 logger = logging.getLogger(__name__)

311 logger.error(message)

312 raise NewportSerialCommunicationError(message)

313 else:

314 answer.append(line[len(prefix):])

315 finally:

316 self.check_for_error(add)

317

318 return answer

319

320

321 class NewportConfigCommands(NameEnum):

322 """

323 Commands predefined by the communication protocol of the SMC100PP

324 """

325

326 AC = "acceleration"

327 BA = "backlash_compensation"

328 BH = "hysteresis_compensation"

329 FRM = "micro_step_per_full_step_factor"

330 FRS = "motion_distance_per_full_step"

331 HT = "home_search_type"

332 JR = "jerk_time"

333 OH = "home_search_velocity"

334 OT = "home_search_timeout"

335 QIL = "peak_output_current_limit"

336 SA = "rs485_address"

337 SL = "negative_software_limit"

338 SR = "positive_software_limit"

339 VA = "velocity"

340 VB = "base_velocity"

341 ZX = "stage_configuration"

342

343

344@configdataclass

345 class NewportSMC100PPConfig:

346 """

347 Configuration dataclass for the Newport motor controller SMC100PP.

348 """

349

350 class HomeSearch(IntEnum):

351 """

352 Different methods for the motor to search its home position during

353 initialization.

354 """

355

356 HomeSwitch_and_Index = 0

357 CurrentPosition = 1

358 HomeSwitch = 2

359 EndOfRunSwitch_and_Index = 3

360 EndOfRunSwitch = 4

361

362 class EspStageConfig(IntEnum):

363 """

364 Different configurations to check or not the motor configuration upon power-up.

365 """

366

367 DisableEspStageCheck = 1

368 UpdateEspStageInfo = 2

369 EnableEspStageCheck = 3

370

371 # The following parameters are added for convenience, they do not correspond to any

372 # actual hardware configuration:

373

374 # controller address (1 to 31)

375 address: int = 1

376

377 # user position offset (mm). For convenience of the user, the motor

378 # position is given relative to this point:

379 user_position_offset: Number = 23.987

380

381 # correction for the scaling between screw turns and distance (should be close to 1)

382 screw_scaling: Number = 1

383

384 # nr of seconds to wait after exit configuration command has been issued

385 exit_configuration_wait_sec: Number = 5

386

387 # waiting time for a move

388 move_wait_sec: Number = 1

389

390 # The following parameters are actual hardware configuration parameters:

391

392 # acceleration (preset units/s^2)

393 acceleration: Number = 10

394

395 # backlash compensation (preset units)

396 # either backlash compensation or hysteresis compensation can be used, not both.

397 backlash_compensation: Number = 0

398

399 # hysteresis compensation (preset units)

400 # either backlash compensation or hysteresis compensation can be used, not both.

401 hysteresis_compensation: Number = 0.015

402

403 # micro step per full step factor, integer between 1 and 2000

404 micro_step_per_full_step_factor: int = 100

405

406 # motion distance per full step (preset units)

407 motion_distance_per_full_step: Number = 0.01

408

409 # home search type

410 home_search_type: Union[int, HomeSearch] = HomeSearch.HomeSwitch

411

412 # jerk time (s) -> time to reach the needed acceleration

413 jerk_time: Number = 0.04

414

415 # home search velocity (preset units/s)

416 home_search_velocity: Number = 4

417

418 # home search time-out (s)

419 home_search_timeout: Number = 27.5

420

421 # home search polling interval (s)

422 home_search_polling_interval: Number = 1

423

424 # peak output current delivered to the motor (A)

425 peak_output_current_limit: Number = 0.4

426

427 # RS485 address, integer between 2 and 31

428 rs485_address: int = 2

429

430 # lower limit for the motor position (mm)

431 negative_software_limit: Number = -23.5

432

433 # upper limit for the motor position (mm)

434 positive_software_limit: Number = 25

435

436 # maximum velocity (preset units/s), this is also the default velocity unless a

437 # lower value is set

438 velocity: Number = 4

439

440 # profile generator base velocity (preset units/s)

441 base_velocity: Number = 0

442

443 # ESP stage configuration

444 stage_configuration: Union[int, EspStageConfig] = EspStageConfig.EnableEspStageCheck

445

446 def clean_values(self):

447 if self.address not in range(1, 32):

448 raise ValueError("Address should be an integer between 1 and 31.")

449 if abs(self.screw_scaling - 1) > 0.1:

450 raise ValueError("The screw scaling should be close to 1.")

451 if not 0 < self.exit_configuration_wait_sec:

452 raise ValueError(

453 "The exit configuration wait time must be a positive "

454 "value (in seconds)."

455 )

456 if not 0 < self.move_wait_sec:

457 raise ValueError(

458 "The wait time for a move to finish must be a "

459 "positive value (in seconds)."

460 )

461 if not 1e-6 < self.acceleration < 1e12:

462 raise ValueError("The acceleration should be between 1e-6 and 1e12.")

463 if not 0 <= self.backlash_compensation < 1e12:

464 raise ValueError("The backlash compensation should be between 0 and 1e12.")

465 if not 0 <= self.hysteresis_compensation < 1e12:

466 raise ValueError(

467 "The hysteresis compensation should be between " "0 and 1e12."

468 )

469 if (

470 not isinstance(self.micro_step_per_full_step_factor, int)

471 or not 1 <= self.micro_step_per_full_step_factor <= 2000

472 ):

473 raise ValueError(

474 "The micro step per full step factor should be between 1 " "and 2000."

475 )

476 if not 1e-6 < self.motion_distance_per_full_step < 1e12:

477 raise ValueError(

478 "The motion distance per full step should be between 1e-6" " and 1e12."

479 )

480 if not isinstance(self.home_search_type, self.HomeSearch):

481 self.force_value("home_search_type", self.HomeSearch(self.home_search_type))

482 if not 1e-3 < self.jerk_time < 1e12:

483 raise ValueError("The jerk time should be between 1e-3 and 1e12.")

484 if not 1e-6 < self.home_search_velocity < 1e12:

485 raise ValueError(

486 "The home search velocity should be between 1e-6 " "and 1e12."

487 )

488 if not 1 < self.home_search_timeout < 1e3:

489 raise ValueError("The home search timeout should be between 1 and 1e3.")

490 if not 0 < self.home_search_polling_interval:

491 raise ValueError(

492 "The home search polling interval (sec) needs to have "

493 "a positive value."

494 )

495 if not 0.05 <= self.peak_output_current_limit <= 3:

496 raise ValueError(

497 "The peak output current limit should be between 0.05 A" "and 3 A."

498 )

499 if self.rs485_address not in range(2, 32):

500 raise ValueError("The RS485 address should be between 2 and 31.")

501 if not -1e12 < self.negative_software_limit <= 0:

502 raise ValueError(

503 "The negative software limit should be between -1e12 " "and 0."

504 )

505 if not 0 <= self.positive_software_limit < 1e12:

506 raise ValueError(

507 "The positive software limit should be between 0 " "and 1e12."

508 )

509 if not 1e-6 < self.velocity < 1e12:

510 raise ValueError("The velocity should be between 1e-6 and 1e12.")

511 if not 0 <= self.base_velocity <= self.velocity:

512 raise ValueError(

513 "The base velocity should be between 0 and the maximum " "velocity."

514 )

515 if not isinstance(self.stage_configuration, self.EspStageConfig):

516 self.force_value(

517 "stage_configuration", self.EspStageConfig(self.stage_configuration)

518 )

519

520 def _build_motor_config(self) -> Dict[str, float]:

521 return {

522 param.value: float(getattr(self, param.value))

523 for param in NewportConfigCommands # type: ignore

524 }

525

526 @property

527 def motor_config(self) -> Dict[str, float]:

528 """

529 Gather the configuration parameters of the motor into a dictionary.

530

531 :return: dict containing the configuration parameters of the motor

532 """

533

534 if not hasattr(self, "_motor_config"):

535 self.force_value( # type: ignore

536 "_motor_config", self._build_motor_config(),

537 )

538 return self._motor_config # type: ignore

539

540 def post_force_value(self, fieldname, value):

541 # if motor config is already cached and field is one of config commands fields..

542 if hasattr(self, "_motor_config") and fieldname in self._motor_config:

543 # ..update directly config dict value

544 self._motor_config[fieldname] = value

545

546

547 class NewportStates(AutoNumberNameEnum):

548 """

549 States of the Newport controller. Certain commands are allowed only in certain

550 states.

551 """

552

553 NO_REF = ()

554 HOMING = ()

555 CONFIG = ()

556 READY = ()

557 MOVING = ()

558 DISABLE = ()

559 JOGGING = ()

560

561

562 class NewportSMC100PP(SingleCommDevice):

563 """

564 Device class of the Newport motor controller SMC100PP

565 """

566

567 States = NewportStates

568

569 class MotorErrors(Enum):

570 """

571 Possible motor errors reported by the motor during get_state().

572 """

573

574 _init_ = "value message"

575

576 OUTPUT_POWER_EXCEEDED = 2, "80W output power exceeded"

577 DC_VOLTAGE_TOO_LOW = 3, "DC voltage too low"

578 WRONG_ESP_STAGE = 4, "Wrong ESP stage"

579 HOMING_TIMEOUT = 5, "Homing timeout"

580 FOLLOWING_ERROR = 6, "Following error"

581 SHORT_CIRCUIT = 7, "Short circuit detection"

582 RMS_CURRENT_LIMIT = 8, "RMS current limit"

583 PEAK_CURRENT_LIMIT = 9, "Peak current limit"

584 POS_END_OF_TURN = 10, "Positive end of turn"

585 NED_END_OF_TURN = 11, "Negative end of turn"

586

587 class StateMessages(Enum):

588 """

589 Possible messages returned by the controller on get_state() query.

590 """

591

592 _init_ = "value message state"

593

594 NO_REF_FROM_RESET = "0A", "NOT REFERENCED from reset.", NewportStates.NO_REF

595 NO_REF_FROM_HOMING = "0B", "NOT REFERENCED from HOMING.", NewportStates.NO_REF

596 NO_REF_FROM_CONFIG = (

597 "0C",

598 "NOT REFERENCED from CONFIGURATION.",

599 NewportStates.NO_REF,

600 )

601 NO_REF_FROM_DISABLED = (

602 "0D",

603 "NOT REFERENCED from DISABLE.",

604 NewportStates.NO_REF,

605 )

606 NO_REF_FROM_READY = "0E", "NOT REFERENCED from READY.", NewportStates.NO_REF

607 NO_REF_FROM_MOVING = "0F", "NOT REFERENCED from MOVING.", NewportStates.NO_REF

608 NO_REF_ESP_STAGE_ERROR = (

609 "10",

610 "NOT REFERENCED ESP stage error.",

611 NewportStates.NO_REF,

612 )

613 NO_REF_FROM_JOGGING = "11", "NOT REFERENCED from JOGGING.", NewportStates.NO_REF

614 CONFIG = "14", "CONFIGURATION.", NewportStates.CONFIG

615 HOMING_FROM_RS232 = (

616 "1E",

617 "HOMING commanded from RS-232-C.",

618 NewportStates.HOMING,

619 )

620 HOMING_FROM_SMC = "1F", "HOMING commanded by SMC-RC.", NewportStates.HOMING

621 MOVING = "28", "MOVING.", NewportStates.MOVING

622 READY_FROM_HOMING = "32", "READY from HOMING.", NewportStates.READY

623 READY_FROM_MOVING = "33", "READY from MOVING.", NewportStates.READY

624 READY_FROM_DISABLE = "34", "READY from DISABLE.", NewportStates.READY

625 READY_FROM_JOGGING = "35", "READY from JOGGING.", NewportStates.READY

626 DISABLE_FROM_READY = "3C", "DISABLE from READY.", NewportStates.DISABLE

627 DISABLE_FROM_MOVING = "3D", "DISABLE from MOVING.", NewportStates.DISABLE

628 DISABLE_FROM_JOGGING = "3E", "DISABLE from JOGGING.", NewportStates.DISABLE

629 JOGGING_FROM_READY = "46", "JOGGING from READY.", NewportStates.JOGGING

630 JOGGING_FROM_DISABLE = "47", "JOGGING from DISABLE.", NewportStates.JOGGING

631

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

633

634 # Call superclass constructor

635 super().__init__(com, dev_config)

636

637 # address of the controller

638 self.address = self.config.address

639

640 # State of the controller (see state diagram in manual)

641 self.state = self.States.NO_REF

642

643 # position of the motor

644 self.position = None

645

646 # logger

647 self.logger = logging.getLogger(__name__)

648

649 def __repr__(self):

650 return f"Newport motor controller SMC100PP {self.address}"

651

652 @staticmethod

653 def default_com_cls():

654 return NewportSMC100PPSerialCommunication

655

656 @staticmethod

657 def config_cls():

658 return NewportSMC100PPConfig

659

660 def start(self):

661 """

662 Opens the communication protocol and applies the config.

663

664 :raises SerialCommunicationIOError: when communication port cannot be opened

665 """

666

667 self.logger.info(f"Starting {self}")

668 super().start()

669

670 self.get_state()

671

672 if self.config.motor_config != self.get_motor_configuration():

673 self.logger.info(f"Updating {self} configuration")

674 if self.state != self.States.NO_REF:

675 self.reset()

676 self.go_to_configuration()

677 self.set_motor_configuration()

678 self.exit_configuration()

679

680 if self.state == self.States.NO_REF:

681 self.initialize()

682 self.wait_until_motor_initialized()

683

684 def stop(self) -> None:

685 """

686 Stop the device. Close the communication protocol.

687 """

688

689 try:

690 if self.com.is_open:

691 self.stop_motion()

692 finally:

693 self.logger.info(f"Stopping {self}")

694 # close the com

695 super().stop()

696

697 def get_state(self, add: int = None) -> "StateMessages":

698 """

699 Check on the motor errors and the controller state

700

701 :param add: controller address (1 to 31)

702 :raises SerialCommunicationIOError: if the com is closed

703 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

704 :raises NewportControllerError: if the controller reports an error

705 :raises NewportMotorError: if the motor reports an error

706 :return: state message from the device (member of StateMessages)

707 """

708

709 if add is None:

710 add = self.address

711

712 try:

713 ans = self.com.query(add, "TS")

714 except NewportMotorPowerSupplyWasCutError:

715 # simply try again once

716 ans = self.com.query(add, "TS")

717

718 # the first symbol is not used, the next 3 symbols

719 # are hexadecimal. Once converted to binary, they

720 # indicate motor errors (see manual).

721 errors = []

722 for i in range(3):

723 bin_errors = bin(int(ans[i + 1], 16))[2:].zfill(4)

724 for j, b in enumerate(bin_errors):

725 if b == "1":

726 errors.append(self.MotorErrors(i * 4 + j).message)

727 if len(errors) > 0:

728 self.logger.error(f"Motor {add} error(s): {', '.join(errors)}")

729 raise NewportMotorError(f"Motor {add} error(s): {', '.join(errors)}")

730 # the next two symbols indicate the controller state

731 s = self.StateMessages(ans[4:6])

732 self.logger.info(f"The newport controller {add} is in state {s.name}")

733 self.state = s.state

734 return s

735

736 def get_motor_configuration(self, add: int = None) -> Dict[str, float]:

737 """

738 Query the motor configuration and returns it in a dictionary.

739

740 :param add: controller address (1 to 31)

741 :return: dictionary containing the motor's configuration

742 :raises SerialCommunicationIOError: if the com is closed

743 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

744 :raises NewportControllerError: if the controller reports an error

745 """

746

747 if add is None:

748 add = self.address

749

750 # The controller answer should be lines starting with the following prefixes

751 prefixes = (

752 [f"{add}PW1", f"{add}ID"]

753 + [f"{add}{p.name}" for p in NewportConfigCommands] # type: ignore

754 + [f"{add}PW0"]

755 )

756 answers = self.com.query_multiple(add, "ZT", prefixes)

757 # first and last line are expected to be only the prefixes

758 assert not (answers[0] + answers[-1])

759 # additionally, second line ID is not relevant

760 answers = answers[2:-1]

761

762 motor_config = {}

763 # for each config param, read the answer given by the controller

764 for prefix, answer in zip(NewportConfigCommands, answers): # type: ignore

765 # cast the config param as a float and add the result to the config dict

766 motor_config[prefix.value] = float(answer)

767 return motor_config

768

769 def go_to_configuration(self, add: int = None) -> None:

770 """

771 This method is executed during start(). It can also be executed after a reset().

772 The controller is put in CONFIG state, where configuration parameters

773 can be changed.

774

775 :param add: controller address (1 to 31)

776 :raises SerialCommunicationIOError: if the com is closed

777 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

778 :raises NewportControllerError: if the controller reports an error

779 """

780

781 if add is None:

782 add = self.address

783

784 self.logger.info(f"Newport controller {add} entering CONFIG state.")

785 self.com.send_command(add, "PW", 1)

786 self.state = self.States.CONFIG

787

788 def set_motor_configuration(self, add: int = None, config: dict = None) -> None:

789 """

790 Set the motor configuration. The motor must be in CONFIG state.

791

792 :param add: controller address (1 to 31)

793 :param config: dictionary containing the motor's configuration

794 :raises SerialCommunicationIOError: if the com is closed

795 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

796 :raises NewportControllerError: if the controller reports an error

797 """

798

799 if add is None:

800 add = self.address

801 if config is None:

802 config = self.config.motor_config

803

804 self.logger.info(f"Setting motor {add} configuration.")

805 for param in config:

806 if "compensation" in param and config[param] == 0:

807 self.logger.debug(

808 f"Skipping command to set {param} to 0, which would cause"

809 f"ControllerErrors.PARAM_MISSING_OR_INVALID error. "

810 f"{param} will be set to 0 automatically anyway."

811 )

812 else:

813 cmd = NewportConfigCommands(param).name

814 self.com.send_command(add, cmd, config[param])

815

816 def exit_configuration(self, add: int = None) -> None:

817 """

818 Exit the CONFIGURATION state and go back to the NOT REFERENCED state. All

819 configuration parameters are saved to the device"s memory.

820

821 :param add: controller address (1 to 31)

822 :raises SerialCommunicationIOError: if the com is closed

823 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

824 :raises NewportControllerError: if the controller reports an error

825 """

826

827 if add is None:

828 add = self.address

829

830 self.logger.info(f"Newport controller {add} leaving CONFIG state.")

831 with self.com.access_lock:

832 self.com._send_command_without_checking_error(add, "PW", 0)

833 sleep(self.config.exit_configuration_wait_sec)

834 self.com.check_for_error(add)

835 self.state = self.States.NO_REF

836

837 def initialize(self, add: int = None) -> None:

838 """

839 Puts the controller from the NOT_REF state to the READY state.

840 Sends the motor to its "home" position.

841

842 :param add: controller address (1 to 31)

843 :raises SerialCommunicationIOError: if the com is closed

844 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

845 :raises NewportControllerError: if the controller reports an error

846 """

847

848 if add is None:

849 add = self.address

850

851 self.logger.info(f"Newport controller {add} is HOMING.")

852 self.com.send_command(add, "OR")

853 self.state = self.States.READY

854

855 def wait_until_motor_initialized(self, add: int = None) -> None:

856 """

857 Wait until the motor leaves the HOMING state (at which point it should

858 have arrived to the home position).

859

860 :param add: controller address (1 to 31)

861 :raises SerialCommunicationIOError: if the com is closed

862 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

863 :raises NewportControllerError: if the controller reports an error

864 """

865

866 if add is None:

867 add = self.address

868

869 poll = True

870 elapsed_time = 0.0

871 start_time = time()

872 while poll:

873 state_message = self.get_state(add)

874 elapsed_time += time() - start_time

875 poll = (state_message.state == self.States.HOMING) and (

876 elapsed_time < self.config.home_search_timeout

877 )

878 if poll:

879 sleep(self.config.home_search_polling_interval)

880

881 if state_message != self.StateMessages.READY_FROM_HOMING:

882 raise NewportControllerError(

883 f"Newport motor {add} should be READY from"

884 f" HOMING but is {state_message}."

885 )

886

887 def reset(self, add: int = None) -> None:

888 """

889 Resets the controller, equivalent to a power-up. This puts the controller

890 back to NOT REFERENCED state, which is necessary for configuring the controller.

891

892 :param add: controller address (1 to 31)

893 :raises SerialCommunicationIOError: if the com is closed

894 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

895 :raises NewportControllerError: if the controller reports an error

896 """

897

898 if add is None:

899 add = self.address

900

901 self.logger.info(f"Newport controller {add} is being reset to NO_REF.")

902 self.com.send_command(add, "RS")

903 # an additional read_text is needed to clean the buffer after reset()

904 strange_char = self.com.read_text()

905 self.logger.debug(f"{self} sent this: '{strange_char}' after reset()")

906 self.state = self.States.NO_REF

907

908 def get_position(self, add: int = None) -> float:

909 """

910 Returns the value of the current position.

911

912 :param add: controller address (1 to 31)

913 :raises SerialCommunicationIOError: if the com is closed

914 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

915 :raises NewportControllerError: if the controller reports an error

916 :raises NewportUncertainPositionError: if the position is ambiguous

917 """

918

919 if add is None:

920 add = self.address

921

922 ans = float(self.com.query(add, "TP"))

923

924 # if zero, check motor state (answer 0 is not reliable in NO_REF state)

925 if ans == 0 and self.get_state().state == NewportStates.NO_REF:

926 message = ("Motor claiming to be at home position in NO_REF state"

927 "is not reliable. Initialization needed.")

928 self.logger.error(message)

929 raise NewportUncertainPositionError(message)

930

931 self.position = (

932 ans * self.config.screw_scaling + self.config.user_position_offset

933 )

934 self.logger.info(f"Newport motor {add} position is {self.position}.")

935 return self.position

936

937 def move_to_absolute_position(self, pos: Number, add: int = None) -> None:

938 """

939 Move the motor to the specified position.

940

941 :param pos: target absolute position (affected by the configured offset)

942 :param add: controller address (1 to 31), defaults to self.address

943 :raises SerialCommunicationIOError: if the com is closed

944 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

945 :raises NewportControllerError: if the controller reports an error

946 """

947

948 if add is None:

949 add = self.address

950

951 self.logger.info(

952 f"Newport motor {add} moving from absolute position "

953 f"{self.get_position()} to absolute position {pos}."

954 )

955

956 # translate user-position into hardware-position

957 hard_pos = pos - self.config.user_position_offset

958 self.com.send_command(add, "PA", hard_pos)

959 sleep(self.config.move_wait_sec)

960

961 def go_home(self, add: int = None) -> None:

962 """

963 Move the motor to its home position.

964

965 :param add: controller address (1 to 31), defaults to self.address

966 :raises SerialCommunicationIOError: if the com is closed

967 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

968 :raises NewportControllerError: if the controller reports an error

969 """

970

971 if add is None:

972 add = self.address

973

974 self.logger.info(

975 f"Newport motor {add} moving from absolute position {self.get_position()} "

976 f"to home position {self.config.user_position_offset}."

977 )

978

979 self.com.send_command(add, "PA", 0)

980 sleep(self.config.move_wait_sec)

981

982 def move_to_relative_position(self, pos: Number, add: int = None) -> None:

983 """

984 Move the motor of the specified distance.

985

986 :param pos: distance to travel (the sign gives the direction)

987 :param add: controller address (1 to 31), defaults to self.address

988 :raises SerialCommunicationIOError: if the com is closed

989 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

990 :raises NewportControllerError: if the controller reports an error

991 """

992

993 if add is None:

994 add = self.address

995

996 self.logger.info(f"Newport motor {add} moving of {pos} units.")

997 self.com.send_command(add, "PR", pos)

998 sleep(self.config.move_wait_sec)

999

1000 def get_move_duration(self, dist: Number, add: int = None) -> float:

1001 """

1002 Estimate the time necessary to move the motor of the specified distance.

1003

1004 :param dist: distance to travel

1005 :param add: controller address (1 to 31), defaults to self.address

1006 :raises SerialCommunicationIOError: if the com is closed

1007 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1008 :raises NewportControllerError: if the controller reports an error

1009 """

1010

1011 if add is None:

1012 add = self.address

1013

1014 dist = round(dist, 2)

1015 duration = float(self.com.query(add, "PT", abs(dist)))

1016 self.logger.info(

1017 f"Newport motor {add} will need {duration}s to move {dist} units."

1018 )

1019 return duration

1020

1021 def stop_motion(self, add: int = None) -> None:

1022 """

1023 Stop a move in progress by decelerating the positioner immediately with the

1024 configured acceleration until it stops. If a controller address is provided,

1025 stops a move in progress on this controller, else stops the moves on all

1026 controllers.

1027

1028 :param add: controller address (1 to 31)

1029 :raises SerialCommunicationIOError: if the com is closed

1030 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1031 :raises NewportControllerError: if the controller reports an error

1032 """

1033

1034 if add is None:

1035 add = self.address

1036 self.logger.info("Stopping motion of all Newport motors.")

1037 self.com.send_stop(add)

1038 else:

1039 self.logger.info(f"Stopping motion of Newport motor {add}.")

1040 self.com.send_command(add, "ST")

1041

1042 def get_acceleration(self, add: int = None) -> Number:

1043 """

1044 Leave the configuration state. The configuration parameters are saved to

1045 the device"s memory.

1046

1047 :param add: controller address (1 to 31)

1048 :return: acceleration (preset units/s^2), value between 1e-6 and 1e12

1049 :raises SerialCommunicationIOError: if the com is closed

1050 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1051 :raises NewportControllerError: if the controller reports an error

1052 """

1053

1054 if add is None:

1055 add = self.address

1056

1057 acc = float(self.com.query(add, "AC", "?"))

1058 self.logger.info(f"Newport motor {add} acceleration is {acc}.")

1059 return acc

1060

1061 def set_acceleration(self, acc: Number, add: int = None) -> None:

1062 """

1063 Leave the configuration state. The configuration parameters are saved to

1064 the device"s memory.

1065

1066 :param acc: acceleration (preset units/s^2), value between 1e-6 and 1e12

1067 :param add: controller address (1 to 31)

1068 :raises SerialCommunicationIOError: if the com is closed

1069 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1070 :raises NewportControllerError: if the controller reports an error

1071 """

1072

1073 if add is None:

1074 add = self.address

1075

1076 self.com.send_command(add, "AC", acc)

1077 self.logger.info(f"Newport motor {add} acceleration set to {acc}.")

1078

1079 def get_controller_information(self, add: int = None) -> str:

1080 """

1081 Get information on the controller name and driver version

1082

1083 :param add: controller address (1 to 31)

1084 :return: controller information

1085 :raises SerialCommunicationIOError: if the com is closed

1086 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1087 :raises NewportControllerError: if the controller reports an error

1088 """

1089

1090 if add is None:

1091 add = self.address

1092

1093 return self.com.query(add, "VE", "?")

1094

1095 def get_positive_software_limit(self, add: int = None) -> Number:

1096 """

1097 Get the positive software limit (the maximum position that the motor is allowed

1098 to travel to towards the right).

1099

1100 :param add: controller address (1 to 31)

1101 :return: positive software limit (preset units), value between 0 and 1e12

1102 :raises SerialCommunicationIOError: if the com is closed

1103 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1104 :raises NewportControllerError: if the controller reports an error

1105 """

1106

1107 if add is None:

1108 add = self.address

1109

1110 lim = float(self.com.query(add, "SR", "?"))

1111 self.logger.info(f"Newport motor {add} positive software limit is {lim}.")

1112 return lim

1113

1114 def set_positive_software_limit(self, lim: Number, add: int = None) -> None:

1115 """

1116 Set the positive software limit (the maximum position that the motor is allowed

1117 to travel to towards the right).

1118

1119 :param lim: positive software limit (preset units), value between 0 and 1e12

1120 :param add: controller address (1 to 31)

1121 :raises SerialCommunicationIOError: if the com is closed

1122 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1123 :raises NewportControllerError: if the controller reports an error

1124 """

1125

1126 if add is None:

1127 add = self.address

1128

1129 self.com.send_command(add, "SR", lim)

1130 self.logger.info(f"Newport {add} positive software limit set to {lim}.")

1131

1132 def get_negative_software_limit(self, add: int = None) -> Number:

1133 """

1134 Get the negative software limit (the maximum position that the motor is allowed

1135 to travel to towards the left).

1136

1137 :param add: controller address (1 to 31)

1138 :return: negative software limit (preset units), value between -1e12 and 0

1139 :raises SerialCommunicationIOError: if the com is closed

1140 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1141 :raises NewportControllerError: if the controller reports an error

1142 """

1143

1144 if add is None:

1145 add = self.address

1146

1147 lim = float(self.com.query(add, "SL", "?"))

1148 self.logger.info(f"Newport motor {add} negative software limit is {lim}.")

1149 return lim

1150

1151 def set_negative_software_limit(self, lim: Number, add: int = None) -> None:

1152 """

1153 Set the negative software limit (the maximum position that the motor is allowed

1154 to travel to towards the left).

1155

1156 :param lim: negative software limit (preset units), value between -1e12 and 0

1157 :param add: controller address (1 to 31)

1158 :raises SerialCommunicationIOError: if the com is closed

1159 :raises NewportSerialCommunicationError: if an unexpected answer is obtained

1160 :raises NewportControllerError: if the controller reports an error

1161 """

1162

1163 if add is None:

1164 add = self.address

1165

1166 self.com.send_command(add, "SL", lim)

1167 self.logger.info(f"Newport {add} negative software limit set to {lim}.")

1168

1169

1170 class NewportMotorError(Exception):

1171 """

1172 Error with the Newport motor.

1173 """

1174

1175 pass

1176

1177

1178 class NewportUncertainPositionError(Exception):

1179 """

1180 Error with the position of the Newport motor.

1181 """

1182

1183 pass

1184

1185

1186 class NewportMotorPowerSupplyWasCutError(Exception):

1187 """

1188 Error with the Newport motor after the power supply was cut and then restored,

1189 without interrupting the communication with the controller.

1190 """

1191

1192 pass

1193

1194

1195 class NewportControllerError(Exception):

1196 """

1197 Error with the Newport controller.

1198 """

1199

1200 pass

1201

1202

1203 class NewportSerialCommunicationError(Exception):

1204 """

1205 Communication error with the Newport controller.

1206 """

1207

1208 pass