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

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.
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.
10Manufacturer homepage:
11https://www.newport.com/f/smc100-single-axis-dc-or-stepper-motion-controller
12"""
14import logging
15from time import sleep, time
16from typing import Union, Dict, List
18# Note: PyCharm does not recognize the dependency correctly, it is added as pyserial.
19import serial
20from aenum import Enum, IntEnum
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
34Param = Union[Number, str, None]
37@configdataclass
38class NewportSMC100PPSerialCommunicationConfig(SerialCommunicationConfig):
39 #: Baudrate for Heinzinger power supplies is 9600 baud
40 baudrate: int = 57600
42 #: Heinzinger does not use parity
43 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
45 #: Heinzinger uses one stop bit
46 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
48 #: One byte is eight bits long
49 bytesize: Union[
50 int, SerialCommunicationBytesize
51 ] = SerialCommunicationBytesize.EIGHTBITS
53 #: The terminator is CR/LF
54 terminator: bytes = b"\r\n"
56 #: use 10 seconds timeout as default
57 timeout: Number = 10
60class NewportSMC100PPSerialCommunication(SerialCommunication):
61 """
62 Specific communication protocol implementation Heinzinger power supplies.
63 Already predefines device-specific protocol parameters in config.
64 """
66 class ControllerErrors(Enum):
67 """
68 Possible controller errors with values as returned by the device in response
69 to sent commands.
70 """
72 _init_ = "value message"
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."
113 def __init__(self, configuration):
114 """
115 Constructor for NewportSMC100PPSerialCommunication.
116 """
118 super().__init__(configuration)
120 self.logger = logging.getLogger(__name__)
122 @staticmethod
123 def config_cls():
124 return NewportSMC100PPSerialCommunicationConfig
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.
132 This method uses `self.access_lock` to ensure thread-safety.
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 """
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
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.
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 """
160 if param is None:
161 param = ""
163 with self.access_lock:
164 self.write_text(f"{add}{cmd}{param}")
165 self.logger.debug(f"sent: {add}{cmd}{param}")
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.
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 """
183 if param is None:
184 param = ""
186 prefix = f"{add}{cmd}"
187 query = f"{add}{cmd}{param}"
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()
208 def check_for_error(self, add: int) -> None:
209 """
210 Ask the Newport controller for the last error it recorded.
212 This method is called after every command or query.
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 """
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)
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.
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 """
240 if param is None:
241 param = ""
243 with self.access_lock:
244 self._send_command_without_checking_error(add, cmd, param)
245 self.check_for_error(add)
247 def send_stop(self, add: int) -> None:
248 """
249 Send the general stop ST command to the controller, and check for errors.
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 """
257 with self.access_lock:
258 self.write_text("ST")
259 self.check_for_error(add)
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.
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 """
275 with self.access_lock:
277 try:
278 answer = self._query_without_checking_errors(add, cmd, param)
279 finally:
280 self.check_for_error(add)
282 return answer
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.
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 """
298 with self.access_lock:
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)
318 return answer
321class NewportConfigCommands(NameEnum):
322 """
323 Commands predefined by the communication protocol of the SMC100PP
324 """
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"
344@configdataclass
345class NewportSMC100PPConfig:
346 """
347 Configuration dataclass for the Newport motor controller SMC100PP.
348 """
350 class HomeSearch(IntEnum):
351 """
352 Different methods for the motor to search its home position during
353 initialization.
354 """
356 HomeSwitch_and_Index = 0
357 CurrentPosition = 1
358 HomeSwitch = 2
359 EndOfRunSwitch_and_Index = 3
360 EndOfRunSwitch = 4
362 class EspStageConfig(IntEnum):
363 """
364 Different configurations to check or not the motor configuration upon power-up.
365 """
367 DisableEspStageCheck = 1
368 UpdateEspStageInfo = 2
369 EnableEspStageCheck = 3
371 # The following parameters are added for convenience, they do not correspond to any
372 # actual hardware configuration:
374 # controller address (1 to 31)
375 address: int = 1
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
381 # correction for the scaling between screw turns and distance (should be close to 1)
382 screw_scaling: Number = 1
384 # nr of seconds to wait after exit configuration command has been issued
385 exit_configuration_wait_sec: Number = 5
387 # waiting time for a move
388 move_wait_sec: Number = 1
390 # The following parameters are actual hardware configuration parameters:
392 # acceleration (preset units/s^2)
393 acceleration: Number = 10
395 # backlash compensation (preset units)
396 # either backlash compensation or hysteresis compensation can be used, not both.
397 backlash_compensation: Number = 0
399 # hysteresis compensation (preset units)
400 # either backlash compensation or hysteresis compensation can be used, not both.
401 hysteresis_compensation: Number = 0.015
403 # micro step per full step factor, integer between 1 and 2000
404 micro_step_per_full_step_factor: int = 100
406 # motion distance per full step (preset units)
407 motion_distance_per_full_step: Number = 0.01
409 # home search type
410 home_search_type: Union[int, HomeSearch] = HomeSearch.HomeSwitch
412 # jerk time (s) -> time to reach the needed acceleration
413 jerk_time: Number = 0.04
415 # home search velocity (preset units/s)
416 home_search_velocity: Number = 4
418 # home search time-out (s)
419 home_search_timeout: Number = 27.5
421 # home search polling interval (s)
422 home_search_polling_interval: Number = 1
424 # peak output current delivered to the motor (A)
425 peak_output_current_limit: Number = 0.4
427 # RS485 address, integer between 2 and 31
428 rs485_address: int = 2
430 # lower limit for the motor position (mm)
431 negative_software_limit: Number = -23.5
433 # upper limit for the motor position (mm)
434 positive_software_limit: Number = 25
436 # maximum velocity (preset units/s), this is also the default velocity unless a
437 # lower value is set
438 velocity: Number = 4
440 # profile generator base velocity (preset units/s)
441 base_velocity: Number = 0
443 # ESP stage configuration
444 stage_configuration: Union[int, EspStageConfig] = EspStageConfig.EnableEspStageCheck
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 )
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 }
526 @property
527 def motor_config(self) -> Dict[str, float]:
528 """
529 Gather the configuration parameters of the motor into a dictionary.
531 :return: dict containing the configuration parameters of the motor
532 """
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
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
547class NewportStates(AutoNumberNameEnum):
548 """
549 States of the Newport controller. Certain commands are allowed only in certain
550 states.
551 """
553 NO_REF = ()
554 HOMING = ()
555 CONFIG = ()
556 READY = ()
557 MOVING = ()
558 DISABLE = ()
559 JOGGING = ()
562class NewportSMC100PP(SingleCommDevice):
563 """
564 Device class of the Newport motor controller SMC100PP
565 """
567 States = NewportStates
569 class MotorErrors(Enum):
570 """
571 Possible motor errors reported by the motor during get_state().
572 """
574 _init_ = "value message"
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"
587 class StateMessages(Enum):
588 """
589 Possible messages returned by the controller on get_state() query.
590 """
592 _init_ = "value message state"
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
632 def __init__(self, com, dev_config=None):
634 # Call superclass constructor
635 super().__init__(com, dev_config)
637 # address of the controller
638 self.address = self.config.address
640 # State of the controller (see state diagram in manual)
641 self.state = self.States.NO_REF
643 # position of the motor
644 self.position = None
646 # logger
647 self.logger = logging.getLogger(__name__)
649 def __repr__(self):
650 return f"Newport motor controller SMC100PP {self.address}"
652 @staticmethod
653 def default_com_cls():
654 return NewportSMC100PPSerialCommunication
656 @staticmethod
657 def config_cls():
658 return NewportSMC100PPConfig
660 def start(self):
661 """
662 Opens the communication protocol and applies the config.
664 :raises SerialCommunicationIOError: when communication port cannot be opened
665 """
667 self.logger.info(f"Starting {self}")
668 super().start()
670 self.get_state()
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()
680 if self.state == self.States.NO_REF:
681 self.initialize()
682 self.wait_until_motor_initialized()
684 def stop(self) -> None:
685 """
686 Stop the device. Close the communication protocol.
687 """
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()
697 def get_state(self, add: int = None) -> "StateMessages":
698 """
699 Check on the motor errors and the controller state
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 """
709 if add is None:
710 add = self.address
712 try:
713 ans = self.com.query(add, "TS")
714 except NewportMotorPowerSupplyWasCutError:
715 # simply try again once
716 ans = self.com.query(add, "TS")
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
736 def get_motor_configuration(self, add: int = None) -> Dict[str, float]:
737 """
738 Query the motor configuration and returns it in a dictionary.
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 """
747 if add is None:
748 add = self.address
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]
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
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.
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 """
781 if add is None:
782 add = self.address
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
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.
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 """
799 if add is None:
800 add = self.address
801 if config is None:
802 config = self.config.motor_config
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])
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.
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 """
827 if add is None:
828 add = self.address
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
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.
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 """
848 if add is None:
849 add = self.address
851 self.logger.info(f"Newport controller {add} is HOMING.")
852 self.com.send_command(add, "OR")
853 self.state = self.States.READY
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).
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 """
866 if add is None:
867 add = self.address
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)
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 )
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.
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 """
898 if add is None:
899 add = self.address
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
908 def get_position(self, add: int = None) -> float:
909 """
910 Returns the value of the current position.
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 """
919 if add is None:
920 add = self.address
922 ans = float(self.com.query(add, "TP"))
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)
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
937 def move_to_absolute_position(self, pos: Number, add: int = None) -> None:
938 """
939 Move the motor to the specified position.
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 """
948 if add is None:
949 add = self.address
951 self.logger.info(
952 f"Newport motor {add} moving from absolute position "
953 f"{self.get_position()} to absolute position {pos}."
954 )
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)
961 def go_home(self, add: int = None) -> None:
962 """
963 Move the motor to its home position.
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 """
971 if add is None:
972 add = self.address
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 )
979 self.com.send_command(add, "PA", 0)
980 sleep(self.config.move_wait_sec)
982 def move_to_relative_position(self, pos: Number, add: int = None) -> None:
983 """
984 Move the motor of the specified distance.
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 """
993 if add is None:
994 add = self.address
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)
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.
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 """
1011 if add is None:
1012 add = self.address
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
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.
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 """
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")
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.
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 """
1054 if add is None:
1055 add = self.address
1057 acc = float(self.com.query(add, "AC", "?"))
1058 self.logger.info(f"Newport motor {add} acceleration is {acc}.")
1059 return acc
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.
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 """
1073 if add is None:
1074 add = self.address
1076 self.com.send_command(add, "AC", acc)
1077 self.logger.info(f"Newport motor {add} acceleration set to {acc}.")
1079 def get_controller_information(self, add: int = None) -> str:
1080 """
1081 Get information on the controller name and driver version
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 """
1090 if add is None:
1091 add = self.address
1093 return self.com.query(add, "VE", "?")
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).
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 """
1107 if add is None:
1108 add = self.address
1110 lim = float(self.com.query(add, "SR", "?"))
1111 self.logger.info(f"Newport motor {add} positive software limit is {lim}.")
1112 return lim
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).
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 """
1126 if add is None:
1127 add = self.address
1129 self.com.send_command(add, "SR", lim)
1130 self.logger.info(f"Newport {add} positive software limit set to {lim}.")
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).
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 """
1144 if add is None:
1145 add = self.address
1147 lim = float(self.com.query(add, "SL", "?"))
1148 self.logger.info(f"Newport motor {add} negative software limit is {lim}.")
1149 return lim
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).
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 """
1163 if add is None:
1164 add = self.address
1166 self.com.send_command(add, "SL", lim)
1167 self.logger.info(f"Newport {add} negative software limit set to {lim}.")
1170class NewportMotorError(Exception):
1171 """
1172 Error with the Newport motor.
1173 """
1175 pass
1178class NewportUncertainPositionError(Exception):
1179 """
1180 Error with the position of the Newport motor.
1181 """
1183 pass
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 """
1192 pass
1195class NewportControllerError(Exception):
1196 """
1197 Error with the Newport controller.
1198 """
1200 pass
1203class NewportSerialCommunicationError(Exception):
1204 """
1205 Communication error with the Newport controller.
1206 """
1208 pass