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

5 

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

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

8communication link. 

9 

10Manufacturer homepage: 

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

12""" 

13 

14import logging 

15from time import sleep, time 

16from typing import Union, Dict, List 

17 

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

19import serial 

20from aenum import Enum, IntEnum 

21 

22from .base import SingleCommDevice 

23from ..comm import SerialCommunication, SerialCommunicationConfig 

24from ..comm.serial import ( 

25 SerialCommunicationParity, 

26 SerialCommunicationStopbits, 

27 SerialCommunicationBytesize, 

28 SerialCommunicationIOError, 

29) 

30from ..configuration import configdataclass 

31from ..utils.enum import NameEnum, AutoNumberNameEnum 

32from ..utils.typing import Number 

33 

34Param = Union[Number, str, None] 

35 

36 

37@configdataclass 

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

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

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

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

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

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

1170class NewportMotorError(Exception): 

1171 """ 

1172 Error with the Newport motor. 

1173 """ 

1174 

1175 pass 

1176 

1177 

1178class NewportUncertainPositionError(Exception): 

1179 """ 

1180 Error with the position of the Newport motor. 

1181 """ 

1182 

1183 pass 

1184 

1185 

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

1195class NewportControllerError(Exception): 

1196 """ 

1197 Error with the Newport controller. 

1198 """ 

1199 

1200 pass 

1201 

1202 

1203class NewportSerialCommunicationError(Exception): 

1204 """ 

1205 Communication error with the Newport controller. 

1206 """ 

1207 

1208 pass