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 controlling a Schneider Electric ILS2T stepper drive over modbus TCP. 

5""" 

6 

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 

13 

14import aenum 

15from bitstring import BitArray 

16from pymodbus.constants import Endian 

17from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder 

18 

19from .base import SingleCommDevice 

20from ..comm import ( 

21 ModbusTcpCommunication, 

22 ModbusTcpConnectionFailedException, 

23 ModbusTcpCommunicationConfig, 

24) 

25from ..configuration import configdataclass 

26from ..utils.typing import Number 

27 

28 

29class ILS2TException(Exception): 

30 """ 

31 Exception to indicate problems with the SE ILS2T stepper motor. 

32 """ 

33 

34 pass 

35 

36 

37class IoScanningModeValueError(ILS2TException): 

38 """ 

39 Exception to indicate that the selected IO scanning mode is invalid. 

40 """ 

41 

42 pass 

43 

44 

45class ScalingFactorValueError(ILS2TException): 

46 """ 

47 Exception to indicate that a scaling factor value is invalid. 

48 """ 

49 

50 pass 

51 

52 

53@configdataclass 

54class ILS2TModbusTcpCommunicationConfig(ModbusTcpCommunicationConfig): 

55 """ 

56 Configuration dataclass for Modbus/TCP communciation specific for the Schneider 

57 Electric ILS2T stepper motor. 

58 """ 

59 

60 #: The unit has to be 255 such that IO scanning mode works. 

61 unit: int = 255 

62 

63 

64class ILS2TModbusTcpCommunication(ModbusTcpCommunication): 

65 """ 

66 Specific implementation of Modbus/TCP for the Schneider Electric ILS2T stepper 

67 motor. 

68 """ 

69 

70 @staticmethod 

71 def config_cls(): 

72 return ILS2TModbusTcpCommunicationConfig 

73 

74 

75@configdataclass 

76class ILS2TConfig: 

77 """ 

78 Configuration for the ILS2T stepper motor device. 

79 """ 

80 

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 

90 

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 ) 

120 

121 

122class ILS2TRegDatatype(aenum.Enum): 

123 """ 

124 Modbus Register Datatypes for Schneider Electric ILS2T stepper drive. 

125 

126 From the manual of the drive: 

127 

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

139 

140 """ 

141 

142 _init_ = "min max" 

143 INT32 = -2_147_483_648, 2_147_483_647 

144 

145 def is_in_range(self, value: int) -> bool: 

146 return self.min <= value <= self.max 

147 

148 

149class ILS2TRegAddr(IntEnum): 

150 """ 

151 Modbus Register Adresses for for Schneider Electric ILS2T stepper drive. 

152 """ 

153 

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) 

165 

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 

173 

174 

175class ILS2T(SingleCommDevice): 

176 """ 

177 Schneider Electric ILS2T stepper drive class. 

178 """ 

179 

180 RegDatatype = ILS2TRegDatatype 

181 """Modbus Register Datatypes 

182 """ 

183 RegAddr = ILS2TRegAddr 

184 """Modbus Register Adresses 

185 """ 

186 

187 class Mode(IntEnum): 

188 """ 

189 ILS2T device modes 

190 """ 

191 

192 PTP = 3 # point to point 

193 JOG = 1 

194 

195 class ActionsPtp(IntEnum): 

196 """ 

197 Allowed actions in the point to point mode (`ILS2T.Mode.PTP`). 

198 """ 

199 

200 ABSOLUTE_POSITION = 0 

201 RELATIVE_POSITION_TARGET = 1 

202 RELATIVE_POSITION_MOTOR = 2 

203 

204 ACTION_JOG_VALUE = 0 

205 """ 

206 The single action value for `ILS2T.Mode.JOG` 

207 """ 

208 

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

215 

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 

223 

224 class State(IntEnum): 

225 """ 

226 State machine status values 

227 """ 

228 

229 QUICKSTOP = 7 

230 READY = 4 

231 ON = 6 

232 

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

249 

250 def __init__(self, com, dev_config=None) -> None: 

251 """ 

252 Constructor for ILS2T. 

253 

254 :param com: object to use as communication protocol. 

255 """ 

256 

257 # Call superclass constructor 

258 super().__init__(com, dev_config) 

259 

260 # toggle reminder bit 

261 self._mode_toggle_mt = 0 

262 

263 self.flt_list: List[Dict[int, Dict[str, Any]]] = [] 

264 

265 @staticmethod 

266 def default_com_cls(): 

267 return ILS2TModbusTcpCommunication 

268 

269 @staticmethod 

270 def config_cls(): 

271 return ILS2TConfig 

272 

273 def start(self) -> None: 

274 """ 

275 Start this device. 

276 """ 

277 

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 

285 

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]) 

289 

290 # set maximum RPM from init config 

291 self.set_max_rpm(self.config.rpm_max_init) 

292 

293 def stop(self) -> None: 

294 """ 

295 Stop this device. Disables the motor (applies brake), disables access and 

296 closes the communication protocol. 

297 """ 

298 

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() 

303 

304 def get_status(self) -> Dict[str, int]: 

305 """ 

306 Perform an IO Scanning read and return the status of the motor. 

307 

308 :return: dict with status information. 

309 """ 

310 

311 registers = self.com.read_holding_registers(self.RegAddr.IO_SCANNING.value, 8) 

312 return self._decode_status_registers(registers) 

313 

314 def do_ioscanning_write(self, **kwargs: int) -> None: 

315 """ 

316 Perform a write operation using IO Scanning mode. 

317 

318 :param kwargs: 

319 Keyword-argument list with options to send, remaining are taken 

320 from the defaults. 

321 """ 

322 

323 self._toggle() 

324 values = self._generate_control_registers(**kwargs) 

325 self.com.write_registers(self.RegAddr.IO_SCANNING.value, values) 

326 

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. 

332 

333 :param kwargs: Keyword-argument list with options different than the defaults. 

334 :return: List of registers for the IO scanning mode. 

335 """ 

336 

337 cleaned_io_scanning_mode = self._clean_ioscanning_mode_values(kwargs) 

338 

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) 

342 

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 ) 

356 

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 ) 

370 

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

374 

375 # add 4 bytes Ref_32, Target position 

376 builder.add_32bit_int(cleaned_io_scanning_mode["ref_32"]) 

377 

378 return builder.to_registers() 

379 

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. 

385 

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

393 

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

400 

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) 

405 

406 # perform checks depending on mode 

407 # JOG mode 

408 if io_scanning_values["mode"] == self.Mode.JOG: 

409 

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

413 

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 ) 

421 

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 ) 

427 

428 return io_scanning_values 

429 

430 # PTP mode 

431 if io_scanning_values["mode"] == self.Mode.PTP: 

432 

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

438 

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 ) 

444 

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 ) 

450 

451 return io_scanning_values 

452 

453 # default 

454 raise IoScanningModeValueError(f"Wrong mode: {io_scanning_values['mode']}") 

455 

456 def _is_valid_rpm(self, num: int) -> bool: 

457 """ 

458 Checks whether `num` is a valid RPM value. 

459 

460 :param num: RPM value to check 

461 :return: `True` if `num` is a valid RPM value, `False` otherwise 

462 """ 

463 

464 return isinstance(num, Integral) and 0 < num <= self.config.rpm_max_init 

465 

466 @classmethod 

467 def _is_int32(cls, num: int) -> bool: 

468 """ 

469 Checks whether a number fits in a signed 32-bit integer. 

470 

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 ) 

478 

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. 

483 

484 :param registers: List of 8 registers (6922-6930) 

485 :return: dict 

486 """ 

487 

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 } 

503 

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 } 

517 

518 def _toggle(self) -> None: 

519 """ 

520 To activate a command it is necessary to toggle the MT bit first. 

521 """ 

522 

523 self._mode_toggle_mt = 0 if self._mode_toggle_mt else 1 

524 

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. 

529 

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

536 

537 logging.info(f"Perform number of steps: {steps}") 

538 

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 ) 

545 

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. 

550 

551 :param position: absolute position of motor in user defined steps. 

552 """ 

553 

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

558 

559 logging.info(f"Absolute position: {position}") 

560 

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 ) 

567 

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. 

576 

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 

595 

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. 

600 

601 Check position at the end if wrong do not raise error; instead just log and 

602 return check result. 

603 

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

608 

609 with self.com.access_lock: 

610 position_before = self.get_position() 

611 

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) 

617 

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 ) 

628 

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. 

633 

634 Check position at the end if wrong do not raise error; instead just log and 

635 return check result. 

636 

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

641 

642 with self.com.access_lock: 

643 position_before = self.get_position() 

644 

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) 

650 

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 ) 

660 

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. 

666 

667 Note: the driver cannot be disabled if the motor is still running. 

668 

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 

676 

677 try_disable = True 

678 elapsed_time = 0.0 

679 start_time = time() 

680 while try_disable: 

681 

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 

689 

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) 

693 

694 return can_disable 

695 

696 def enable(self) -> None: 

697 """ 

698 Enable the driver of the stepper motor and disable the brake. 

699 """ 

700 

701 self.do_ioscanning_write(enable_driver_en=1, disable_driver_di=0) 

702 logging.info("Enable motor, disable brake.") 

703 

704 def get_position(self) -> int: 

705 """ 

706 Read the position of the drive and store into status. 

707 

708 :return: Position step value 

709 """ 

710 

711 value = self.com.read_input_registers(self.RegAddr.POSITION.value, 2) 

712 return self._decode_32bit(value, True) 

713 

714 def get_temperature(self) -> int: 

715 """ 

716 Read the temperature of the motor. 

717 

718 :return: Temperature in degrees Celsius. 

719 """ 

720 

721 value = self.com.read_input_registers(self.RegAddr.TEMP.value, 2) 

722 return self._decode_32bit(value, True) 

723 

724 def get_dc_volt(self) -> float: 

725 """ 

726 Read the DC supply voltage of the motor. 

727 

728 :return: DC input voltage. 

729 """ 

730 

731 value = self.com.read_input_registers(self.RegAddr.VOLT.value, 2) 

732 return self._decode_32bit(value, True) / 10 

733 

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. 

738 

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

743 

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() 

749 

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. 

755 

756 :param steps: number of steps in `revolutions`. 

757 :param revolutions: number of revolutions corresponding to `steps`. 

758 """ 

759 

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) 

764 

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) 

769 

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) 

775 

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

781 

782 logging.warning("Motor QUICK STOP.") 

783 self.do_ioscanning_write(quick_stop_qs=1) 

784 

785 def reset_error(self) -> None: 

786 """ 

787 Resets the motor into normal state after quick stop or another error occured. 

788 """ 

789 

790 logging.info("Reset motor after fault or quick stop.") 

791 self.do_ioscanning_write(fault_reset_fr=1) 

792 

793 def jog_run(self, direction: bool = True, fast: bool = False) -> None: 

794 """ 

795 Slowly turn the motor in positive direction. 

796 """ 

797 

798 status = self.get_status() 

799 

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 

803 

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 

808 

809 ref_16 = self.Ref16Jog.NONE 

810 

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

817 

818 if fast: 

819 ref_16 = ref_16 | self.Ref16Jog.FAST 

820 

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 ) 

827 

828 def jog_stop(self) -> None: 

829 """ 

830 Stop turning the motor in Jog mode. 

831 """ 

832 

833 logging.info("Stop in Jog mode.") 

834 

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 ) 

841 

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. 

846 

847 :param slow: RPM for slow jog mode. 

848 :param fast: RPM for fast jog mode. 

849 """ 

850 

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]) 

854 

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. 

861 

862 :return: Dictionary with all information 

863 """ 

864 

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 

916 

917 def set_max_rpm(self, rpm: int) -> None: 

918 """ 

919 Set the maximum RPM. 

920 

921 :param rpm: revolution per minute ( 0 < rpm <= RPM_MAX) 

922 :raises ILS2TException: if RPM is out of range 

923 """ 

924 

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 ) 

932 

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 

938 

939 :param ramp_type: 0: linear ramp | -1: motor optimized ramp 

940 """ 

941 

942 self.com.write_registers(self.RegAddr.RAMP_TYPE.value, [0, ramp_type]) 

943 

944 def set_max_acceleration(self, rpm_minute: int) -> None: 

945 """ 

946 Set the maximum acceleration of the motor. 

947 

948 :param rpm_minute: revolution per minute per minute 

949 """ 

950 

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) 

955 

956 def set_max_deceleration(self, rpm_minute: int) -> None: 

957 """ 

958 Set the maximum deceleration of the motor. 

959 

960 :param rpm_minute: revolution per minute per minute 

961 """ 

962 

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)