Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\se_ils2t.py : 35%

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 controlling a Schneider Electric ILS2T stepper drive over modbus TCP.
5"""
7import logging
8from datetime import timedelta
9from enum import Flag, IntEnum
10from numbers import Integral
11from time import sleep, time
12from typing import Dict, List, Any, cast, Optional
14import aenum
15from bitstring import BitArray
16from pymodbus.constants import Endian
17from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder
19from .base import SingleCommDevice
20from ..comm import (
21 ModbusTcpCommunication,
22 ModbusTcpConnectionFailedException,
23 ModbusTcpCommunicationConfig,
24)
25from ..configuration import configdataclass
26from ..utils.typing import Number
29class ILS2TException(Exception):
30 """
31 Exception to indicate problems with the SE ILS2T stepper motor.
32 """
34 pass
37class IoScanningModeValueError(ILS2TException):
38 """
39 Exception to indicate that the selected IO scanning mode is invalid.
40 """
42 pass
45class ScalingFactorValueError(ILS2TException):
46 """
47 Exception to indicate that a scaling factor value is invalid.
48 """
50 pass
53@configdataclass
54class ILS2TModbusTcpCommunicationConfig(ModbusTcpCommunicationConfig):
55 """
56 Configuration dataclass for Modbus/TCP communciation specific for the Schneider
57 Electric ILS2T stepper motor.
58 """
60 #: The unit has to be 255 such that IO scanning mode works.
61 unit: int = 255
64class ILS2TModbusTcpCommunication(ModbusTcpCommunication):
65 """
66 Specific implementation of Modbus/TCP for the Schneider Electric ILS2T stepper
67 motor.
68 """
70 @staticmethod
71 def config_cls():
72 return ILS2TModbusTcpCommunicationConfig
75@configdataclass
76class ILS2TConfig:
77 """
78 Configuration for the ILS2T stepper motor device.
79 """
81 #: initial maximum RPM for the motor, can be set up to 3000 RPM. The user is
82 #: allowed to set a new max RPM at runtime using :meth:`ILS2T.set_max_rpm`,
83 #: but the value must never exceed this configuration setting.
84 rpm_max_init: Integral = cast(Integral, 1500)
85 wait_sec_post_enable: Number = 1
86 wait_sec_max_disable: Number = 10
87 wait_sec_post_cannot_disable: Number = 1
88 wait_sec_post_relative_step: Number = 2
89 wait_sec_post_absolute_position: Number = 2
91 def clean_values(self):
92 if not 0 < self.rpm_max_init <= 3000:
93 raise ValueError(
94 "Maximum RPM for the motor must be integer number between 1 and 3000."
95 )
96 if self.wait_sec_post_enable <= 0:
97 raise ValueError(
98 "Wait time post motor enabling must be a positive value (in seconds)."
99 )
100 if self.wait_sec_max_disable < 0:
101 raise ValueError(
102 "Maximal wait time for attempting to disable motor must be a "
103 "non-negative value (in seconds)."
104 )
105 if self.wait_sec_post_cannot_disable <= 0:
106 raise ValueError(
107 "Wait time post failed motor disable attempt must be a positive value "
108 "(in seconds)."
109 )
110 if self.wait_sec_post_relative_step <= 0:
111 raise ValueError(
112 "Wait time post motor relative step must be a positive value "
113 "(in seconds)."
114 )
115 if self.wait_sec_post_absolute_position <= 0:
116 raise ValueError(
117 "Wait time post motor absolute position change must be a positive "
118 "value (in seconds)."
119 )
122class ILS2TRegDatatype(aenum.Enum):
123 """
124 Modbus Register Datatypes for Schneider Electric ILS2T stepper drive.
126 From the manual of the drive:
128 =========== =========== ============== =============
129 datatype byte min max
130 =========== =========== ============== =============
131 INT8 1 Byte -128 127
132 UINT8 1 Byte 0 255
133 INT16 2 Byte -32_768 32_767
134 UINT16 2 Byte 0 65_535
135 INT32 4 Byte -2_147_483_648 2_147_483_647
136 UINT32 4 Byte 0 4_294_967_295
137 BITS just 32bits N/A N/A
138 =========== =========== ============== =============
140 """
142 _init_ = "min max"
143 INT32 = -2_147_483_648, 2_147_483_647
145 def is_in_range(self, value: int) -> bool:
146 return self.min <= value <= self.max
149class ILS2TRegAddr(IntEnum):
150 """
151 Modbus Register Adresses for for Schneider Electric ILS2T stepper drive.
152 """
154 POSITION = 7706 # INT32 position of the motor in user defined units
155 IO_SCANNING = 6922 # BITS start register for IO scanning control
156 # and status
157 TEMP = 7200 # INT16 temperature of motor
158 VOLT = 7198 # UINT16 dc voltage of motor
159 SCALE = 1550 # INT32 user defined steps per revolution
160 ACCESS_ENABLE = 282 # BITS not documented register
161 # to enable access via IO scanning
162 JOGN_FAST = 10506 # UINT16 revolutions per minute for fast Jog (1 to 3000)
163 JOGN_SLOW = 10504 # UINT16 revolutions per minute
164 # for slow Jog (1 to 3000)
166 RAMP_TYPE = 1574 # INT16 ramp type, 0: linear / -1: motor optimized
167 RAMP_ACC = 1556 # UINT32 acceleration
168 RAMP_DECEL = 1558 # UINT32 deceleration
169 RAMP_N_MAX = 1554 # UINT16 max rpm
170 FLT_INFO = 15362 # 22 registers, code for error
171 FLT_MEM_RESET = 15114 # UINT16 reset fault memory
172 FLT_MEM_DEL = 15112 # UINT16 delete fault memory
175class ILS2T(SingleCommDevice):
176 """
177 Schneider Electric ILS2T stepper drive class.
178 """
180 RegDatatype = ILS2TRegDatatype
181 """Modbus Register Datatypes
182 """
183 RegAddr = ILS2TRegAddr
184 """Modbus Register Adresses
185 """
187 class Mode(IntEnum):
188 """
189 ILS2T device modes
190 """
192 PTP = 3 # point to point
193 JOG = 1
195 class ActionsPtp(IntEnum):
196 """
197 Allowed actions in the point to point mode (`ILS2T.Mode.PTP`).
198 """
200 ABSOLUTE_POSITION = 0
201 RELATIVE_POSITION_TARGET = 1
202 RELATIVE_POSITION_MOTOR = 2
204 ACTION_JOG_VALUE = 0
205 """
206 The single action value for `ILS2T.Mode.JOG`
207 """
209 # Note: don't use IntFlag here - it allows other then enumerated values
210 class Ref16Jog(Flag):
211 """
212 Allowed values for ILS2T ref_16 register (the shown values are the integer
213 representation of the bits), all in Jog mode = 1
214 """
216 NONE = 0
217 POS = 1
218 NEG = 2
219 FAST = 4
220 # allowed combinations
221 POS_FAST = POS | FAST
222 NEG_FAST = NEG | FAST
224 class State(IntEnum):
225 """
226 State machine status values
227 """
229 QUICKSTOP = 7
230 READY = 4
231 ON = 6
233 DEFAULT_IO_SCANNING_CONTROL_VALUES = {
234 "action": ActionsPtp.RELATIVE_POSITION_MOTOR.value,
235 "mode": Mode.PTP.value,
236 "disable_driver_di": 0,
237 "enable_driver_en": 0,
238 "quick_stop_qs": 0,
239 "fault_reset_fr": 0,
240 "execute_stop_sh": 0,
241 "reset_stop_ch": 0,
242 "continue_after_stop_cu": 0,
243 "ref_16": ILS2TConfig.rpm_max_init,
244 "ref_32": 0,
245 }
246 """
247 Default IO Scanning control mode values
248 """
250 def __init__(self, com, dev_config=None) -> None:
251 """
252 Constructor for ILS2T.
254 :param com: object to use as communication protocol.
255 """
257 # Call superclass constructor
258 super().__init__(com, dev_config)
260 # toggle reminder bit
261 self._mode_toggle_mt = 0
263 self.flt_list: List[Dict[int, Dict[str, Any]]] = []
265 @staticmethod
266 def default_com_cls():
267 return ILS2TModbusTcpCommunication
269 @staticmethod
270 def config_cls():
271 return ILS2TConfig
273 def start(self) -> None:
274 """
275 Start this device.
276 """
278 logging.info("Starting device " + str(self))
279 try:
280 # try opening the port
281 super().start()
282 except ModbusTcpConnectionFailedException as exc:
283 logging.error(exc)
284 raise
286 # writing 1 to register ACCESS_ENABLE allows to use the IO scanning mode.
287 # This is not documented in the manual!
288 self.com.write_registers(self.RegAddr.ACCESS_ENABLE.value, [0, 1])
290 # set maximum RPM from init config
291 self.set_max_rpm(self.config.rpm_max_init)
293 def stop(self) -> None:
294 """
295 Stop this device. Disables the motor (applies brake), disables access and
296 closes the communication protocol.
297 """
299 logging.info("Stopping device " + str(self))
300 self.disable()
301 self.com.write_registers(self.RegAddr.ACCESS_ENABLE.value, [0, 0])
302 super().stop()
304 def get_status(self) -> Dict[str, int]:
305 """
306 Perform an IO Scanning read and return the status of the motor.
308 :return: dict with status information.
309 """
311 registers = self.com.read_holding_registers(self.RegAddr.IO_SCANNING.value, 8)
312 return self._decode_status_registers(registers)
314 def do_ioscanning_write(self, **kwargs: int) -> None:
315 """
316 Perform a write operation using IO Scanning mode.
318 :param kwargs:
319 Keyword-argument list with options to send, remaining are taken
320 from the defaults.
321 """
323 self._toggle()
324 values = self._generate_control_registers(**kwargs)
325 self.com.write_registers(self.RegAddr.IO_SCANNING.value, values)
327 def _generate_control_registers(self, **kwargs: int) -> List[int]:
328 """
329 Generates the control registers for the IO scanning mode.
330 It is necessary to write all 64 bit at the same time, so a list of 4 registers
331 is generated.
333 :param kwargs: Keyword-argument list with options different than the defaults.
334 :return: List of registers for the IO scanning mode.
335 """
337 cleaned_io_scanning_mode = self._clean_ioscanning_mode_values(kwargs)
339 action_bits = f"{cleaned_io_scanning_mode['action']:03b}"
340 mode_bits = f"{cleaned_io_scanning_mode['mode']:04b}"
341 builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big)
343 # add the first byte: Drive control
344 builder.add_bits(
345 [
346 cleaned_io_scanning_mode["disable_driver_di"],
347 cleaned_io_scanning_mode["enable_driver_en"],
348 cleaned_io_scanning_mode["quick_stop_qs"],
349 cleaned_io_scanning_mode["fault_reset_fr"],
350 0, # has to be 0 per default, no meaning
351 cleaned_io_scanning_mode["execute_stop_sh"],
352 cleaned_io_scanning_mode["reset_stop_ch"],
353 cleaned_io_scanning_mode["continue_after_stop_cu"],
354 ]
355 )
357 # add the second byte: Mode control
358 builder.add_bits(
359 [
360 int(mode_bits[3]),
361 int(mode_bits[2]),
362 int(mode_bits[1]),
363 int(mode_bits[0]),
364 int(action_bits[2]),
365 int(action_bits[1]),
366 int(action_bits[0]),
367 self._mode_toggle_mt,
368 ]
369 )
371 # add the third and fourth byte:
372 # Ref_16 (either JOG direction/speed, or RPM...)
373 builder.add_16bit_uint(cleaned_io_scanning_mode["ref_16"])
375 # add 4 bytes Ref_32, Target position
376 builder.add_32bit_int(cleaned_io_scanning_mode["ref_32"])
378 return builder.to_registers()
380 def _clean_ioscanning_mode_values(
381 self, io_scanning_values: Dict[str, int]
382 ) -> Dict[str, int]:
383 """
384 Checks if the constructed mode is valid.
386 :param io_scanning_values: Dictionary with register values to check
387 :return: Dictionary with cleaned register values
388 :raises ValueError: if `io_scanning_values` has unrecognized keys
389 :raises IoScanningModeValueError: if either `'mode'` or either of corresponding
390 `'action'`, `'ref_16'`, or `'ref_32'` keys of `io_scanning_values` has
391 an invalid value.
392 """
394 # check if there are too much keys that are not recognized
395 io_scanning_keys = set(io_scanning_values.keys())
396 all_keys = set(self.DEFAULT_IO_SCANNING_CONTROL_VALUES.keys())
397 superfluous_keys = io_scanning_keys.difference(all_keys)
398 if superfluous_keys:
399 raise ValueError(f"Unrecognized mode keys: {list(superfluous_keys)}")
401 # fill up io_scanning_values with defaults, if they are not set
402 for mode_key, default_value in self.DEFAULT_IO_SCANNING_CONTROL_VALUES.items():
403 if mode_key not in io_scanning_values:
404 io_scanning_values[mode_key] = cast(int, default_value)
406 # perform checks depending on mode
407 # JOG mode
408 if io_scanning_values["mode"] == self.Mode.JOG:
410 io_scanning_value = io_scanning_values["action"]
411 if not io_scanning_value == self.ACTION_JOG_VALUE:
412 raise IoScanningModeValueError(f"Wrong action: {io_scanning_value}")
414 io_scanning_value = io_scanning_values["ref_16"]
415 try:
416 self.Ref16Jog(io_scanning_value)
417 except ValueError:
418 raise IoScanningModeValueError(
419 f"Wrong value in ref_16 ({io_scanning_value})"
420 )
422 io_scanning_value = io_scanning_values["ref_32"]
423 if not io_scanning_value == 0:
424 raise IoScanningModeValueError(
425 f"Wrong value in ref_32 ({io_scanning_value})"
426 )
428 return io_scanning_values
430 # PTP mode
431 if io_scanning_values["mode"] == self.Mode.PTP:
433 io_scanning_value = io_scanning_values["action"]
434 try:
435 self.ActionsPtp(io_scanning_value)
436 except ValueError:
437 raise IoScanningModeValueError(f"Wrong action: {io_scanning_value}")
439 io_scanning_value = io_scanning_values["ref_16"]
440 if not self._is_valid_rpm(io_scanning_value):
441 raise IoScanningModeValueError(
442 f"Wrong value in ref_16 ({io_scanning_value})"
443 )
445 io_scanning_value = io_scanning_values["ref_32"]
446 if not self._is_int32(io_scanning_value):
447 raise IoScanningModeValueError(
448 f"Wrong value in ref_32 ({io_scanning_value})"
449 )
451 return io_scanning_values
453 # default
454 raise IoScanningModeValueError(f"Wrong mode: {io_scanning_values['mode']}")
456 def _is_valid_rpm(self, num: int) -> bool:
457 """
458 Checks whether `num` is a valid RPM value.
460 :param num: RPM value to check
461 :return: `True` if `num` is a valid RPM value, `False` otherwise
462 """
464 return isinstance(num, Integral) and 0 < num <= self.config.rpm_max_init
466 @classmethod
467 def _is_int32(cls, num: int) -> bool:
468 """
469 Checks whether a number fits in a signed 32-bit integer.
471 :param num: is the number to check.
472 :return: check result.
473 """
474 return (
475 isinstance(num, Integral) and
476 cast(ILS2TRegDatatype, ILS2TRegDatatype.INT32).is_in_range(num)
477 )
479 @staticmethod
480 def _decode_status_registers(registers: List[int]) -> Dict[str, int]:
481 """
482 Decodes the the status of the stepper drive, derived from IOscanning.
484 :param registers: List of 8 registers (6922-6930)
485 :return: dict
486 """
488 decoder = BinaryPayloadDecoder.fromRegisters(registers, byteorder=Endian.Big)
489 decoded = {
490 "drive_control": decoder.decode_bits(),
491 "mode_control": decoder.decode_bits(),
492 "ref_16": decoder.decode_16bit_int(),
493 "ref_32": decoder.decode_32bit_int(),
494 "drive_status_1": decoder.decode_bits(),
495 "drive_status_2": decoder.decode_bits(),
496 "mode_status": decoder.decode_bits(),
497 "drive_input": decoder.decode_bits(),
498 "action_word_1": decoder.decode_bits(),
499 "action_word_2": decoder.decode_bits(),
500 "special_function_1": decoder.decode_bits(),
501 "special_function_2": decoder.decode_bits(),
502 }
504 return {
505 "mode": BitArray(decoded["mode_status"][3::-1]).int,
506 "action": BitArray(decoded["mode_control"][6:3:-1]).int,
507 "ref_16": decoded["ref_16"],
508 "ref_32": decoded["ref_32"],
509 "state": BitArray(decoded["drive_status_2"][3::-1]).int,
510 "fault": decoded["drive_status_2"][6],
511 "warn": decoded["drive_status_2"][7],
512 "halt": decoded["drive_status_1"][0],
513 "motion_zero": decoded["action_word_2"][6],
514 "turning_positive": decoded["action_word_2"][7],
515 "turning_negative": decoded["action_word_1"][0],
516 }
518 def _toggle(self) -> None:
519 """
520 To activate a command it is necessary to toggle the MT bit first.
521 """
523 self._mode_toggle_mt = 0 if self._mode_toggle_mt else 1
525 def write_relative_step(self, steps: int) -> None:
526 """
527 Write instruction to turn the motor the relative amount of steps. This function
528 does not enable or disable the motor automatically.
530 :param steps: Number of steps to turn the motor.
531 """
532 max_step = self.RegDatatype.INT32.max # type: ignore
533 # use _is_int32 instead?
534 if not abs(steps) < max_step:
535 logging.warning(f"number of steps is too big: {steps}")
537 logging.info(f"Perform number of steps: {steps}")
539 self.do_ioscanning_write(
540 enable_driver_en=1,
541 mode=self.Mode.PTP.value,
542 action=self.ActionsPtp.RELATIVE_POSITION_MOTOR.value,
543 ref_32=steps,
544 )
546 def write_absolute_position(self, position: int) -> None:
547 """
548 Write instruction to turn the motor until it reaches the absolute position.
549 This function does not enable or disable the motor automatically.
551 :param position: absolute position of motor in user defined steps.
552 """
554 max_position = self.RegDatatype.INT32.max # type: ignore
555 # use _is_int32 instead?
556 if not abs(position) < max_position:
557 logging.warning(f"position is out of range: {position}")
559 logging.info(f"Absolute position: {position}")
561 self.do_ioscanning_write(
562 enable_driver_en=1,
563 mode=self.Mode.PTP.value,
564 action=self.ActionsPtp.ABSOLUTE_POSITION.value,
565 ref_32=position,
566 )
568 def _is_position_as_expected(
569 self, position_expected: int, position_actual: int, err_msg: str
570 ) -> bool:
571 """
572 Check if actual drive position is a expected. If expectation is not met,
573 check for possible drive error and log the given error message with appropriate
574 level of severity. Do not raise error; instead, return `bool` stating if
575 expectation was met.
577 :param position_expected: Expected drive position.
578 :param position_actual: Actual drive position.
579 :param err_msg: Error message to log if expectation is not met.
580 :return: `True` if actual position is as expected, `False` otherwise.
581 """
582 as_expected = position_expected == position_actual
583 if not as_expected:
584 flt_dict = self.get_error_code()
585 self.flt_list.append(flt_dict)
586 if "empty" in flt_dict[0].keys():
587 logging.warning(
588 "no error in drive, something different must have gone wrong"
589 )
590 logging.warning(err_msg)
591 else:
592 logging.critical("error in drive, drive is know maybe locked")
593 logging.critical(err_msg)
594 return as_expected
596 def execute_relative_step(self, steps: int) -> bool:
597 """
598 Execute a relative step, i.e. enable motor, perform relative steps,
599 wait until done and disable motor afterwards.
601 Check position at the end if wrong do not raise error; instead just log and
602 return check result.
604 :param steps: Number of steps.
605 :return: `True` if actual position is as expected, `False` otherwise.
606 """
607 logging.info(f"Motor steps requested: {steps}")
609 with self.com.access_lock:
610 position_before = self.get_position()
612 self.enable()
613 sleep(self.config.wait_sec_post_enable)
614 self.write_relative_step(steps)
615 sleep(self.config.wait_sec_post_relative_step)
616 self.disable(log_warn=False)
618 # check if steps were made
619 position_after = self.get_position()
620 return self._is_position_as_expected(
621 position_before + steps, position_after, (
622 "The position does not align with the requested step number. "
623 f"Before: {position_before}, after: {position_after}, "
624 f"requested: {steps}, "
625 f"real difference: {position_after - position_before}."
626 )
627 )
629 def execute_absolute_position(self, position: int) -> bool:
630 """
631 Execute a absolute position change, i.e. enable motor, perform absolute
632 position change, wait until done and disable motor afterwards.
634 Check position at the end if wrong do not raise error; instead just log and
635 return check result.
637 :param position: absolute position of motor in user defined steps.
638 :return: `True` if actual position is as expected, `False` otherwise.
639 """
640 logging.info(f"absolute position requested: {position}")
642 with self.com.access_lock:
643 position_before = self.get_position()
645 self.enable()
646 sleep(self.config.wait_sec_post_enable)
647 self.write_absolute_position(position)
648 sleep(self.config.wait_sec_post_absolute_position)
649 self.disable(log_warn=False)
651 # check if steps were made
652 position_after = self.get_position()
653 return self._is_position_as_expected(
654 position, position_after, (
655 "The position does not align with the requested absolute position."
656 f"Before: {position_before}, after: {position_after}, "
657 f"requested: {position}."
658 )
659 )
661 def disable(
662 self, log_warn: bool = True, wait_sec_max: Optional[int] = None,
663 ) -> bool:
664 """
665 Disable the driver of the stepper motor and enable the brake.
667 Note: the driver cannot be disabled if the motor is still running.
669 :param log_warn: if log a warning in case the motor cannot be disabled.
670 :param wait_sec_max: maximal wait time for the motor to stop running and to
671 disable it; by default, with `None`, use a config value
672 :return: `True` if disable request could and was sent, `False` otherwise.
673 """
674 if wait_sec_max is None:
675 wait_sec_max = self.config.wait_sec_max_disable
677 try_disable = True
678 elapsed_time = 0.0
679 start_time = time()
680 while try_disable:
682 can_disable = bool(self.get_status()["motion_zero"])
683 if can_disable:
684 logging.info("Disable motor, brake.")
685 self.do_ioscanning_write(enable_driver_en=0, disable_driver_di=1)
686 elif log_warn:
687 logging.warning("Cannot disable motor, still running!")
688 elapsed_time += time() - start_time
690 try_disable = not can_disable and elapsed_time < wait_sec_max
691 if try_disable:
692 sleep(self.config.wait_sec_post_cannot_disable)
694 return can_disable
696 def enable(self) -> None:
697 """
698 Enable the driver of the stepper motor and disable the brake.
699 """
701 self.do_ioscanning_write(enable_driver_en=1, disable_driver_di=0)
702 logging.info("Enable motor, disable brake.")
704 def get_position(self) -> int:
705 """
706 Read the position of the drive and store into status.
708 :return: Position step value
709 """
711 value = self.com.read_input_registers(self.RegAddr.POSITION.value, 2)
712 return self._decode_32bit(value, True)
714 def get_temperature(self) -> int:
715 """
716 Read the temperature of the motor.
718 :return: Temperature in degrees Celsius.
719 """
721 value = self.com.read_input_registers(self.RegAddr.TEMP.value, 2)
722 return self._decode_32bit(value, True)
724 def get_dc_volt(self) -> float:
725 """
726 Read the DC supply voltage of the motor.
728 :return: DC input voltage.
729 """
731 value = self.com.read_input_registers(self.RegAddr.VOLT.value, 2)
732 return self._decode_32bit(value, True) / 10
734 @staticmethod
735 def _decode_32bit(registers: List[int], signed: bool = True) -> int:
736 """
737 Decode two 16-bit ModBus registers to a 32-bit integer.
739 :param registers: list of two register values
740 :param signed: True, if register containes a signed value
741 :return: integer representation of the 32-bit register
742 """
744 decoder = BinaryPayloadDecoder.fromRegisters(registers, byteorder=Endian.Big)
745 if signed:
746 return decoder.decode_32bit_int()
747 else:
748 return decoder.decode_32bit_uint()
750 def user_steps(self, steps: int = 16384, revolutions: int = 1) -> None:
751 """
752 Define steps per revolution.
753 Default is 16384 steps per revolution.
754 Maximum precision is 32768 steps per revolution.
756 :param steps: number of steps in `revolutions`.
757 :param revolutions: number of revolutions corresponding to `steps`.
758 """
760 if not self._is_int32(revolutions):
761 err_msg = f"Wrong scaling factor: revolutions = {revolutions}"
762 logging.error(err_msg)
763 raise ScalingFactorValueError(err_msg)
765 if not self._is_int32(steps):
766 err_msg = f"Wrong scaling factor: steps = {steps}"
767 logging.error(err_msg)
768 raise ScalingFactorValueError(err_msg)
770 builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big)
771 builder.add_32bit_int(steps)
772 builder.add_32bit_int(revolutions)
773 values = builder.to_registers()
774 self.com.write_registers(self.RegAddr.SCALE.value, values)
776 def quickstop(self) -> None:
777 """
778 Stops the motor with high deceleration rate and falls into error state. Reset
779 with `reset_error` to recover into normal state.
780 """
782 logging.warning("Motor QUICK STOP.")
783 self.do_ioscanning_write(quick_stop_qs=1)
785 def reset_error(self) -> None:
786 """
787 Resets the motor into normal state after quick stop or another error occured.
788 """
790 logging.info("Reset motor after fault or quick stop.")
791 self.do_ioscanning_write(fault_reset_fr=1)
793 def jog_run(self, direction: bool = True, fast: bool = False) -> None:
794 """
795 Slowly turn the motor in positive direction.
796 """
798 status = self.get_status()
800 if status["mode"] != self.Mode.JOG and not status["motion_zero"]:
801 logging.error("Motor is not in Jog mode or standstill, abort.")
802 return
804 if status["state"] != self.State.ON:
805 # need to enable first
806 logging.error("Motor is not enabled or in error state. Try .enable()")
807 return
809 ref_16 = self.Ref16Jog.NONE
811 if direction:
812 ref_16 = ref_16 | self.Ref16Jog.POS
813 logging.info("Jog mode in positive direction enabled.")
814 else:
815 ref_16 = ref_16 | self.Ref16Jog.NEG
816 logging.info("Jog mode in negative direction enabled.")
818 if fast:
819 ref_16 = ref_16 | self.Ref16Jog.FAST
821 self.do_ioscanning_write(
822 mode=self.Mode.JOG.value,
823 action=self.ACTION_JOG_VALUE,
824 enable_driver_en=1,
825 ref_16=ref_16.value,
826 )
828 def jog_stop(self) -> None:
829 """
830 Stop turning the motor in Jog mode.
831 """
833 logging.info("Stop in Jog mode.")
835 self.do_ioscanning_write(
836 mode=self.Mode.JOG.value,
837 action=self.ACTION_JOG_VALUE,
838 enable_driver_en=1,
839 ref_16=0,
840 )
842 def set_jog_speed(self, slow: int = 60, fast: int = 180) -> None:
843 """
844 Set the speed for jog mode. Default values correspond to startup values of
845 the motor.
847 :param slow: RPM for slow jog mode.
848 :param fast: RPM for fast jog mode.
849 """
851 logging.info(f"Setting Jog RPM. Slow = {slow} RPM, Fast = {fast} RPM.")
852 self.com.write_registers(self.RegAddr.JOGN_SLOW.value, [0, slow])
853 self.com.write_registers(self.RegAddr.JOGN_FAST.value, [0, fast])
855 def get_error_code(self) -> Dict[int, Dict[str, Any]]:
856 """
857 Read all messages in fault memory.
858 Will read the full error message and return the decoded values.
859 At the end the fault memory of the motor will be deleted.
860 In addition, reset_error is called to re-enable the motor for operation.
862 :return: Dictionary with all information
863 """
865 ret_dict = {}
866 self.com.write_registers(self.RegAddr.FLT_MEM_RESET.value, [0, 1])
867 for i in range(10):
868 registers = self.com.read_input_registers(self.RegAddr.FLT_INFO.value, 22)
869 decoder = BinaryPayloadDecoder.fromRegisters(
870 registers, byteorder=Endian.Big
871 )
872 decoded = {
873 "ignored0": decoder.skip_bytes(2),
874 "error_code": decoder.decode_16bit_uint(),
875 "ignored1": decoder.skip_bytes(2),
876 "error_class": decoder.decode_16bit_uint(),
877 "error_time": decoder.decode_32bit_uint(),
878 "ignored2": decoder.skip_bytes(2),
879 "error_addition": decoder.decode_16bit_uint(),
880 "ignored3": decoder.skip_bytes(2),
881 "error_no_cycle": decoder.decode_16bit_uint(),
882 "ignored4": decoder.skip_bytes(2),
883 "error_after_enable": decoder.decode_16bit_uint(),
884 "ignored5": decoder.skip_bytes(2),
885 "error_voltage_dc": decoder.decode_16bit_uint(),
886 "ignored6": decoder.skip_bytes(2),
887 "error_rpm": decoder.decode_16bit_int(),
888 "ignored7": decoder.skip_bytes(2),
889 "error_current": decoder.decode_16bit_uint(),
890 "ignored8": decoder.skip_bytes(2),
891 "error_amplifier_temperature": decoder.decode_16bit_int(),
892 "ignored9": decoder.skip_bytes(2),
893 "error_device_temperature": decoder.decode_16bit_int(),
894 }
895 flt_dict = {
896 "error_code": hex(decoded["error_code"])[2:],
897 "error_class": decoded["error_class"],
898 "error_time": timedelta(seconds=decoded["error_time"]),
899 "error_addition": decoded["error_addition"],
900 "error_no_cycle": decoded["error_no_cycle"],
901 "error_after_enable": timedelta(seconds=decoded["error_after_enable"]),
902 "error_voltage_dc": decoded["error_voltage_dc"] / 10,
903 "error_rpm": decoded["error_rpm"],
904 "error_current": decoded["error_current"] / 100,
905 "error_amplifier_temperature": decoded["error_amplifier_temperature"],
906 "error_device_temperature": decoded["error_device_temperature"],
907 }
908 ret_dict[i] = flt_dict
909 if flt_dict["error_code"] == "0":
910 flt_dict = {"empty": None}
911 ret_dict = {i: flt_dict}
912 break
913 self.com.write_registers(self.RegAddr.FLT_MEM_DEL.value, [0, 1])
914 self.reset_error()
915 return ret_dict
917 def set_max_rpm(self, rpm: int) -> None:
918 """
919 Set the maximum RPM.
921 :param rpm: revolution per minute ( 0 < rpm <= RPM_MAX)
922 :raises ILS2TException: if RPM is out of range
923 """
925 if self._is_valid_rpm(rpm):
926 self.DEFAULT_IO_SCANNING_CONTROL_VALUES["ref_16"] = rpm
927 self.com.write_registers(self.RegAddr.RAMP_N_MAX.value, [0, rpm])
928 else:
929 raise ILS2TException(
930 f"RPM out of range: {rpm} not in (0, {self.config.rpm_max_init}]"
931 )
933 def set_ramp_type(self, ramp_type: int = -1) -> None:
934 """
935 Set the ramp type. There are two options available:
936 0: linear ramp
937 -1: motor optimized ramp
939 :param ramp_type: 0: linear ramp | -1: motor optimized ramp
940 """
942 self.com.write_registers(self.RegAddr.RAMP_TYPE.value, [0, ramp_type])
944 def set_max_acceleration(self, rpm_minute: int) -> None:
945 """
946 Set the maximum acceleration of the motor.
948 :param rpm_minute: revolution per minute per minute
949 """
951 builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big)
952 builder.add_32bit_uint(rpm_minute)
953 values = builder.to_registers()
954 self.com.write_registers(self.RegAddr.RAMP_ACC.value, values)
956 def set_max_deceleration(self, rpm_minute: int) -> None:
957 """
958 Set the maximum deceleration of the motor.
960 :param rpm_minute: revolution per minute per minute
961 """
963 builder = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big)
964 builder.add_32bit_uint(rpm_minute)
965 values = builder.to_registers()
966 self.com.write_registers(self.RegAddr.RAMP_DECEL.value, values)