emm-stepper 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
emm_stepper/device.py ADDED
@@ -0,0 +1,900 @@
1
+ """Emm固件步进电机设备类.
2
+
3
+ 基于ZDT_X42S第二代闭环步进电机用户手册V1.0.3_251224。
4
+ 提供高级API接口用于控制步进电机。
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional
9
+
10
+ from serial import Serial
11
+
12
+ from .configs import (
13
+ Address,
14
+ ChecksumMode,
15
+ Direction,
16
+ SyncFlag,
17
+ StoreFlag,
18
+ MotionMode,
19
+ HomingMode,
20
+ HomingDirection,
21
+ )
22
+ from .parameters import (
23
+ DeviceParams,
24
+ JogParams,
25
+ PositionParams,
26
+ HomingParams,
27
+ VersionParams,
28
+ MotorRHParams,
29
+ PIDParams,
30
+ HomingStatus,
31
+ MotorStatus,
32
+ SystemStatusParams,
33
+ ConfigParams,
34
+ AutoRunParams,
35
+ )
36
+ from .commands import (
37
+ # 触发动作命令
38
+ CalibrateEncoder,
39
+ Restart,
40
+ ZeroPosition,
41
+ ClearProtection,
42
+ FactoryReset,
43
+ # 运动控制命令
44
+ Enable,
45
+ Disable,
46
+ Jog,
47
+ Position,
48
+ EStop,
49
+ SyncMove,
50
+ # 原点回零命令
51
+ SetHomeZero,
52
+ Home,
53
+ StopHome,
54
+ GetHomingStatus,
55
+ GetHomingParams,
56
+ SetHomingParams,
57
+ # 读取系统参数命令
58
+ GetVersion,
59
+ GetMotorRH,
60
+ GetBusVoltage,
61
+ GetBusCurrent,
62
+ GetPhaseCurrent,
63
+ GetEncoder,
64
+ GetPulseCount,
65
+ GetTargetPosition,
66
+ GetRealtimeSpeed,
67
+ GetRealtimePosition,
68
+ GetPositionError,
69
+ GetTemperature,
70
+ GetMotorStatus,
71
+ GetPID,
72
+ GetConfig,
73
+ GetSystemStatus,
74
+ # 设置命令
75
+ SetID,
76
+ SetMicrostep,
77
+ SetLoopMode,
78
+ SetOpenLoopCurrent,
79
+ SetClosedLoopCurrent,
80
+ SetPID,
81
+ SetMotorDirection,
82
+ SetPositionWindow,
83
+ SetHeartbeatTime,
84
+ SetAutoRun,
85
+ SetConfig,
86
+ SetScaleInput,
87
+ SetLockButton,
88
+ BroadcastGetID,
89
+ )
90
+
91
+ logger = logging.getLogger(__name__)
92
+
93
+
94
+ class EmmDevice:
95
+ """Emm固件步进电机设备类.
96
+
97
+ 提供高级API接口用于控制ZDT X42S第二代闭环步进电机(Emm固件)。
98
+
99
+ 注意: 串口连接需要在外部创建,这样可以支持一个串口连接多个电机(通过不同地址)。
100
+
101
+ 使用示例:
102
+ ```python
103
+ from serial import Serial
104
+ from emm_stepper import EmmDevice, ChecksumMode
105
+
106
+ # 创建串口连接
107
+ ser = Serial('COM3', 115200, timeout=1)
108
+
109
+ # 创建设备实例(可以用同一个串口创建多个不同地址的电机)
110
+ motor1 = EmmDevice(ser, address=1)
111
+ motor2 = EmmDevice(ser, address=2)
112
+
113
+ # 使能电机
114
+ motor1.enable()
115
+
116
+ # 速度模式运动
117
+ motor1.jog(speed=100, direction=0)
118
+
119
+ # 位置模式运动
120
+ motor1.move_pulses(pulses=3200, speed=500) # 转一圈
121
+
122
+ # 停止
123
+ motor1.stop()
124
+
125
+ # 失能电机
126
+ motor1.disable()
127
+
128
+ # 关闭串口(由用户管理)
129
+ ser.close()
130
+ ```
131
+ """
132
+
133
+ def __init__(
134
+ self,
135
+ serial_connection: Serial,
136
+ address: int = 1,
137
+ checksum_mode: ChecksumMode = ChecksumMode.FIXED,
138
+ delay: Optional[float] = None,
139
+ auto_test: bool = True,
140
+ ):
141
+ """初始化设备.
142
+
143
+ Args:
144
+ serial_connection: 串口连接对象
145
+ address: 电机地址 (1-255)
146
+ checksum_mode: 校验模式
147
+ delay: 通讯延迟(秒)
148
+ auto_test: 是否自动测试连接 (默认True)
149
+ """
150
+ self._serial = serial_connection
151
+ self._device_params = DeviceParams(
152
+ serial_connection=serial_connection,
153
+ address=address,
154
+ checksum_mode=checksum_mode,
155
+ delay=delay,
156
+ )
157
+
158
+ # 测试连接
159
+ if auto_test:
160
+ self._test_connection()
161
+
162
+ logger.info(f"已连接到电机 (地址: {address})")
163
+
164
+ def _test_connection(self) -> None:
165
+ """测试与电机的连接."""
166
+ try:
167
+ version = self.get_version()
168
+ logger.info(f"固件版本: {version.firmware_version_str}, 硬件: {version.hw_type_str}")
169
+ except Exception as e:
170
+ raise ConnectionError(f"无法连接到电机: {e}")
171
+
172
+ @property
173
+ def device_params(self) -> DeviceParams:
174
+ """返回设备参数."""
175
+ return self._device_params
176
+
177
+ @property
178
+ def address(self) -> int:
179
+ """返回电机地址."""
180
+ return self._device_params.address
181
+
182
+ # ==================== 触发动作命令 ====================
183
+
184
+ def calibrate_encoder(self) -> bool:
185
+ """触发编码器校准.
186
+
187
+ 电机将缓慢正转一圈然后反转一圈对编码器进行线性化校准。
188
+
189
+ Returns:
190
+ 是否成功
191
+ """
192
+ cmd = CalibrateEncoder(self._device_params)
193
+ return cmd.is_success
194
+
195
+ def restart(self) -> bool:
196
+ """重启电机.
197
+
198
+ Returns:
199
+ 是否成功
200
+ """
201
+ cmd = Restart(self._device_params)
202
+ return cmd.is_success
203
+
204
+ def zero_position(self) -> bool:
205
+ """将当前位置角度清零.
206
+
207
+ Returns:
208
+ 是否成功
209
+ """
210
+ cmd = ZeroPosition(self._device_params)
211
+ return cmd.is_success
212
+
213
+ def clear_protection(self) -> bool:
214
+ """解除堵转/过热/过流保护.
215
+
216
+ Returns:
217
+ 是否成功
218
+ """
219
+ cmd = ClearProtection(self._device_params)
220
+ return cmd.is_success
221
+
222
+ def factory_reset(self) -> bool:
223
+ """恢复出厂设置.
224
+
225
+ 注意: 恢复后需要断电重新上电,再空载校准电机编码器。
226
+
227
+ Returns:
228
+ 是否成功
229
+ """
230
+ cmd = FactoryReset(self._device_params)
231
+ return cmd.is_success
232
+
233
+ # ==================== 运动控制命令 ====================
234
+
235
+ def enable(self, sync: bool = False) -> bool:
236
+ """使能电机(锁轴).
237
+
238
+ Args:
239
+ sync: 是否使用同步模式
240
+
241
+ Returns:
242
+ 是否成功
243
+ """
244
+ sync_flag = SyncFlag.SYNC if sync else SyncFlag.IMMEDIATE
245
+ cmd = Enable(self._device_params, enable=True, sync_flag=sync_flag)
246
+ return cmd.is_success
247
+
248
+ def disable(self, sync: bool = False) -> bool:
249
+ """失能电机(松轴).
250
+
251
+ Args:
252
+ sync: 是否使用同步模式
253
+
254
+ Returns:
255
+ 是否成功
256
+ """
257
+ sync_flag = SyncFlag.SYNC if sync else SyncFlag.IMMEDIATE
258
+ cmd = Disable(self._device_params, sync_flag=sync_flag)
259
+ return cmd.is_success
260
+
261
+ def jog(
262
+ self,
263
+ speed: int = 100,
264
+ direction: Direction = Direction.CW,
265
+ acceleration: int = 10,
266
+ sync: bool = False,
267
+ ) -> bool:
268
+ """速度模式运动.
269
+
270
+ 电机以指定速度持续运动,直到收到停止命令。
271
+
272
+ Args:
273
+ speed: 速度 (0-3000 RPM)
274
+ direction: 运动方向
275
+ acceleration: 加速度档位 (0-255)
276
+ sync: 是否使用同步模式
277
+
278
+ Returns:
279
+ 是否成功
280
+ """
281
+ params = JogParams(
282
+ direction=direction,
283
+ speed=speed,
284
+ acceleration=acceleration,
285
+ sync_flag=SyncFlag.SYNC if sync else SyncFlag.IMMEDIATE,
286
+ )
287
+ cmd = Jog(self._device_params, params=params)
288
+ return cmd.is_success
289
+
290
+ def move_pulses(
291
+ self,
292
+ pulse_count: int,
293
+ speed: int = 100,
294
+ direction: Optional[Direction] = None,
295
+ acceleration: int = 10,
296
+ motion_mode: MotionMode = MotionMode.RELATIVE_LAST,
297
+ sync: bool = False,
298
+ ) -> bool:
299
+ """位置模式运动(脉冲数).
300
+
301
+ 电机运动指定的脉冲数。默认16细分下,3200个脉冲=一圈360°。
302
+
303
+ Args:
304
+ pulse_count: 脉冲数 (正数为CW方向,负数为CCW方向)
305
+ speed: 速度 (0-3000 RPM)
306
+ direction: 运动方向 (如果为None,则根据pulse_count的正负自动判断)
307
+ acceleration: 加速度档位 (0-255)
308
+ motion_mode: 运动模式
309
+ sync: 是否使用同步模式
310
+
311
+ Returns:
312
+ 是否成功
313
+ """
314
+ if direction is None:
315
+ if pulse_count < 0:
316
+ direction = Direction.CCW
317
+ pulse_count = abs(pulse_count)
318
+ else:
319
+ direction = Direction.CW
320
+
321
+ params = PositionParams(
322
+ direction=direction,
323
+ speed=speed,
324
+ acceleration=acceleration,
325
+ pulse_count=pulse_count,
326
+ motion_mode=motion_mode,
327
+ sync_flag=SyncFlag.SYNC if sync else SyncFlag.IMMEDIATE,
328
+ )
329
+ cmd = Position(self._device_params, params=params)
330
+ return cmd.is_success
331
+
332
+ def move_degrees(
333
+ self,
334
+ degrees: float,
335
+ speed: int = 100,
336
+ acceleration: int = 10,
337
+ motion_mode: MotionMode = MotionMode.RELATIVE_LAST,
338
+ microstep: int = 16,
339
+ sync: bool = False,
340
+ ) -> bool:
341
+ """位置模式运动(角度).
342
+
343
+ 电机运动指定的角度。
344
+
345
+ Args:
346
+ degrees: 角度 (正数为CW方向,负数为CCW方向)
347
+ speed: 速度 (0-3000 RPM)
348
+ acceleration: 加速度档位 (0-255)
349
+ motion_mode: 运动模式
350
+ microstep: 细分值 (用于计算脉冲数)
351
+ sync: 是否使用同步模式
352
+
353
+ Returns:
354
+ 是否成功
355
+ """
356
+ # 计算脉冲数: 1.8°电机,一圈200步,细分后一圈 = 200 * microstep 脉冲
357
+ pulses_per_revolution = 200 * microstep
358
+ pulse_count = int(degrees / 360 * pulses_per_revolution)
359
+
360
+ return self.move_pulses(
361
+ pulse_count=pulse_count,
362
+ speed=speed,
363
+ acceleration=acceleration,
364
+ motion_mode=motion_mode,
365
+ sync=sync,
366
+ )
367
+
368
+ def move_revolutions(
369
+ self,
370
+ revolutions: float,
371
+ speed: int = 100,
372
+ acceleration: int = 10,
373
+ motion_mode: MotionMode = MotionMode.RELATIVE_LAST,
374
+ microstep: int = 16,
375
+ sync: bool = False,
376
+ ) -> bool:
377
+ """位置模式运动(圈数).
378
+
379
+ 电机运动指定的圈数。
380
+
381
+ Args:
382
+ revolutions: 圈数 (正数为CW方向,负数为CCW方向)
383
+ speed: 速度 (0-3000 RPM)
384
+ acceleration: 加速度档位 (0-255)
385
+ motion_mode: 运动模式
386
+ microstep: 细分值 (用于计算脉冲数)
387
+ sync: 是否使用同步模式
388
+
389
+ Returns:
390
+ 是否成功
391
+ """
392
+ return self.move_degrees(
393
+ degrees=revolutions * 360,
394
+ speed=speed,
395
+ acceleration=acceleration,
396
+ motion_mode=motion_mode,
397
+ microstep=microstep,
398
+ sync=sync,
399
+ )
400
+
401
+ def stop(self, sync: bool = False) -> bool:
402
+ """立即停止电机.
403
+
404
+ Args:
405
+ sync: 是否使用同步模式
406
+
407
+ Returns:
408
+ 是否成功
409
+ """
410
+ sync_flag = SyncFlag.SYNC if sync else SyncFlag.IMMEDIATE
411
+ cmd = EStop(self._device_params, sync_flag=sync_flag)
412
+ return cmd.is_success
413
+
414
+ @staticmethod
415
+ def sync_move(device_params: DeviceParams) -> bool:
416
+ """触发多机同步运动.
417
+
418
+ 使用广播地址发送,使所有缓存了命令的电机同步开始执行。
419
+
420
+ Args:
421
+ device_params: 任意一个设备的参数(用于获取串口连接)
422
+
423
+ Returns:
424
+ 是否成功
425
+ """
426
+ cmd = SyncMove(device_params)
427
+ return cmd.is_success
428
+
429
+ # ==================== 原点回零命令 ====================
430
+
431
+ def set_home_zero(self, store: bool = True) -> bool:
432
+ """设置单圈回零的零点位置.
433
+
434
+ 将当前位置设置为单圈回零的零点位置。
435
+
436
+ Args:
437
+ store: 是否存储(掉电不丢失)
438
+
439
+ Returns:
440
+ 是否成功
441
+ """
442
+ cmd = SetHomeZero(self._device_params, store=store)
443
+ return cmd.is_success
444
+
445
+ def home(
446
+ self,
447
+ mode: HomingMode = HomingMode.NEAREST,
448
+ sync: bool = False,
449
+ ) -> bool:
450
+ """触发回零.
451
+
452
+ Args:
453
+ mode: 回零模式
454
+ sync: 是否使用同步模式
455
+
456
+ Returns:
457
+ 是否成功
458
+ """
459
+ sync_flag = SyncFlag.SYNC if sync else SyncFlag.IMMEDIATE
460
+ cmd = Home(self._device_params, mode=mode, sync_flag=sync_flag)
461
+ return cmd.is_success
462
+
463
+ def stop_home(self) -> bool:
464
+ """强制中断并退出回零操作.
465
+
466
+ Returns:
467
+ 是否成功
468
+ """
469
+ cmd = StopHome(self._device_params)
470
+ return cmd.is_success
471
+
472
+ def get_homing_status(self) -> HomingStatus:
473
+ """读取回零状态标志.
474
+
475
+ Returns:
476
+ 回零状态
477
+ """
478
+ cmd = GetHomingStatus(self._device_params)
479
+ return cmd.data
480
+
481
+ def get_homing_params(self) -> HomingParams:
482
+ """读取回零参数.
483
+
484
+ Returns:
485
+ 回零参数
486
+ """
487
+ cmd = GetHomingParams(self._device_params)
488
+ return cmd.data
489
+
490
+ def set_homing_params(self, params: HomingParams, store: bool = True) -> bool:
491
+ """修改回零参数.
492
+
493
+ Args:
494
+ params: 回零参数
495
+ store: 是否存储
496
+
497
+ Returns:
498
+ 是否成功
499
+ """
500
+ cmd = SetHomingParams(self._device_params, params=params, store=store)
501
+ return cmd.is_success
502
+
503
+ # ==================== 读取系统参数命令 ====================
504
+
505
+ def get_version(self) -> VersionParams:
506
+ """读取固件版本和硬件版本.
507
+
508
+ Returns:
509
+ 版本参数
510
+ """
511
+ cmd = GetVersion(self._device_params)
512
+ return cmd.data
513
+
514
+ def get_motor_rh(self) -> MotorRHParams:
515
+ """读取相电阻和相电感.
516
+
517
+ Returns:
518
+ 电机RH参数
519
+ """
520
+ cmd = GetMotorRH(self._device_params)
521
+ return cmd.data
522
+
523
+ def get_bus_voltage(self) -> int:
524
+ """读取总线电压.
525
+
526
+ Returns:
527
+ 总线电压(mV)
528
+ """
529
+ cmd = GetBusVoltage(self._device_params)
530
+ return cmd.data
531
+
532
+ def get_bus_current(self) -> int:
533
+ """读取总线电流.
534
+
535
+ Returns:
536
+ 总线电流(mA)
537
+ """
538
+ cmd = GetBusCurrent(self._device_params)
539
+ return cmd.data
540
+
541
+ def get_phase_current(self) -> int:
542
+ """读取相电流.
543
+
544
+ Returns:
545
+ 相电流(mA)
546
+ """
547
+ cmd = GetPhaseCurrent(self._device_params)
548
+ return cmd.data
549
+
550
+ def get_encoder(self) -> int:
551
+ """读取线性化编码器值.
552
+
553
+ Returns:
554
+ 编码器值(0-65535表示0-360度)
555
+ """
556
+ cmd = GetEncoder(self._device_params)
557
+ return cmd.data
558
+
559
+ def get_encoder_degrees(self) -> float:
560
+ """读取编码器角度.
561
+
562
+ Returns:
563
+ 编码器角度(度)
564
+ """
565
+ encoder = self.get_encoder()
566
+ return (encoder * 360) / 65536
567
+
568
+ def get_pulse_count(self) -> int:
569
+ """读取输入脉冲数.
570
+
571
+ Returns:
572
+ 输入脉冲数(带符号)
573
+ """
574
+ cmd = GetPulseCount(self._device_params)
575
+ return cmd.data
576
+
577
+ def get_target_position(self) -> float:
578
+ """读取电机目标位置.
579
+
580
+ Returns:
581
+ 目标位置角度(度)
582
+ """
583
+ cmd = GetTargetPosition(self._device_params)
584
+ return cmd.data
585
+
586
+ def get_realtime_speed(self) -> int:
587
+ """读取电机实时转速.
588
+
589
+ Returns:
590
+ 实时转速(RPM, 带符号)
591
+ """
592
+ cmd = GetRealtimeSpeed(self._device_params)
593
+ return cmd.data
594
+
595
+ def get_realtime_position(self) -> float:
596
+ """读取电机实时位置.
597
+
598
+ Returns:
599
+ 实时位置角度(度)
600
+ """
601
+ cmd = GetRealtimePosition(self._device_params)
602
+ return cmd.data
603
+
604
+ def get_position_error(self) -> float:
605
+ """读取电机位置误差.
606
+
607
+ Returns:
608
+ 位置误差角度(度)
609
+ """
610
+ cmd = GetPositionError(self._device_params)
611
+ return cmd.data
612
+
613
+ def get_temperature(self) -> int:
614
+ """读取驱动温度.
615
+
616
+ Returns:
617
+ 温度(°C)
618
+ """
619
+ cmd = GetTemperature(self._device_params)
620
+ return cmd.data
621
+
622
+ def get_motor_status(self) -> MotorStatus:
623
+ """读取电机状态标志.
624
+
625
+ Returns:
626
+ 电机状态
627
+ """
628
+ cmd = GetMotorStatus(self._device_params)
629
+ return cmd.data
630
+
631
+ def get_pid(self) -> PIDParams:
632
+ """读取PID参数.
633
+
634
+ Returns:
635
+ PID参数
636
+ """
637
+ cmd = GetPID(self._device_params)
638
+ return cmd.data
639
+
640
+ def get_config(self) -> ConfigParams:
641
+ """读取驱动配置参数.
642
+
643
+ Returns:
644
+ 配置参数
645
+ """
646
+ cmd = GetConfig(self._device_params)
647
+ return cmd.data
648
+
649
+ def get_system_status(self) -> SystemStatusParams:
650
+ """读取系统状态参数.
651
+
652
+ Returns:
653
+ 系统状态参数
654
+ """
655
+ cmd = GetSystemStatus(self._device_params)
656
+ return cmd.data
657
+
658
+ # ==================== 设置命令 ====================
659
+
660
+ def set_id(self, new_id: int, store: bool = True) -> bool:
661
+ """修改电机ID/地址.
662
+
663
+ Args:
664
+ new_id: 新ID (1-255)
665
+ store: 是否存储
666
+
667
+ Returns:
668
+ 是否成功
669
+ """
670
+ cmd = SetID(self._device_params, new_id=new_id, store=store)
671
+ if cmd.is_success:
672
+ self._device_params.address = Address(new_id)
673
+ return cmd.is_success
674
+
675
+ def set_microstep(self, microstep: int, store: bool = True) -> bool:
676
+ """修改细分值.
677
+
678
+ Args:
679
+ microstep: 细分值 (1-256)
680
+ store: 是否存储
681
+
682
+ Returns:
683
+ 是否成功
684
+ """
685
+ cmd = SetMicrostep(self._device_params, microstep=microstep, store=store)
686
+ return cmd.is_success
687
+
688
+ def set_loop_mode(self, closed_loop: bool = True, store: bool = True) -> bool:
689
+ """修改开环/闭环控制模式.
690
+
691
+ Args:
692
+ closed_loop: True为闭环模式,False为开环模式
693
+ store: 是否存储
694
+
695
+ Returns:
696
+ 是否成功
697
+ """
698
+ cmd = SetLoopMode(self._device_params, closed_loop=closed_loop, store=store)
699
+ return cmd.is_success
700
+
701
+ def set_open_loop_current(self, current_ma: int, store: bool = True) -> bool:
702
+ """修改开环模式工作电流.
703
+
704
+ Args:
705
+ current_ma: 电流(mA, 0-5000)
706
+ store: 是否存储
707
+
708
+ Returns:
709
+ 是否成功
710
+ """
711
+ cmd = SetOpenLoopCurrent(self._device_params, current_ma=current_ma, store=store)
712
+ return cmd.is_success
713
+
714
+ def set_closed_loop_current(self, current_ma: int, store: bool = True) -> bool:
715
+ """修改闭环模式最大电流.
716
+
717
+ Args:
718
+ current_ma: 电流(mA, 0-5000)
719
+ store: 是否存储
720
+
721
+ Returns:
722
+ 是否成功
723
+ """
724
+ cmd = SetClosedLoopCurrent(self._device_params, current_ma=current_ma, store=store)
725
+ return cmd.is_success
726
+
727
+ def set_pid(self, params: PIDParams, store: bool = True) -> bool:
728
+ """修改PID参数.
729
+
730
+ Args:
731
+ params: PID参数
732
+ store: 是否存储
733
+
734
+ Returns:
735
+ 是否成功
736
+ """
737
+ cmd = SetPID(self._device_params, params=params, store=store)
738
+ return cmd.is_success
739
+
740
+ def set_motor_direction(self, direction: Direction = Direction.CW, store: bool = True) -> bool:
741
+ """修改电机运动正方向.
742
+
743
+ Args:
744
+ direction: 运动方向
745
+ store: 是否存储
746
+
747
+ Returns:
748
+ 是否成功
749
+ """
750
+ cmd = SetMotorDirection(self._device_params, direction=direction, store=store)
751
+ return cmd.is_success
752
+
753
+ def set_position_window(self, window_deg: float = 0.8, store: bool = True) -> bool:
754
+ """修改位置到达窗口.
755
+
756
+ Args:
757
+ window_deg: 位置到达窗口(度)
758
+ store: 是否存储
759
+
760
+ Returns:
761
+ 是否成功
762
+ """
763
+ cmd = SetPositionWindow(self._device_params, window_deg=window_deg, store=store)
764
+ return cmd.is_success
765
+
766
+ def set_heartbeat_time(self, time_ms: int = 0, store: bool = True) -> bool:
767
+ """修改心跳保护功能时间.
768
+
769
+ Args:
770
+ time_ms: 心跳保护时间(毫秒), 0表示关闭
771
+ store: 是否存储
772
+
773
+ Returns:
774
+ 是否成功
775
+ """
776
+ cmd = SetHeartbeatTime(self._device_params, time_ms=time_ms, store=store)
777
+ return cmd.is_success
778
+
779
+ def set_auto_run(self, params: AutoRunParams) -> bool:
780
+ """存储一组速度参数,上电自动运行.
781
+
782
+ Args:
783
+ params: 自动运行参数
784
+
785
+ Returns:
786
+ 是否成功
787
+ """
788
+ cmd = SetAutoRun(self._device_params, params=params)
789
+ return cmd.is_success
790
+
791
+ def set_config(self, params: ConfigParams, store: bool = True) -> bool:
792
+ """修改驱动配置参数.
793
+
794
+ Args:
795
+ params: 配置参数
796
+ store: 是否存储
797
+
798
+ Returns:
799
+ 是否成功
800
+ """
801
+ cmd = SetConfig(self._device_params, params=params, store=store)
802
+ return cmd.is_success
803
+
804
+ def set_scale_input(self, enable: bool = False, store: bool = True) -> bool:
805
+ """修改命令速度值是否缩小10倍输入.
806
+
807
+ Args:
808
+ enable: 是否使能(使能后输入1RPM实际为0.1RPM)
809
+ store: 是否存储
810
+
811
+ Returns:
812
+ 是否成功
813
+ """
814
+ cmd = SetScaleInput(self._device_params, enable=enable, store=store)
815
+ return cmd.is_success
816
+
817
+ def set_lock_button(self, lock: bool = False, store: bool = True) -> bool:
818
+ """修改锁定按键功能.
819
+
820
+ Args:
821
+ lock: 是否锁定
822
+ store: 是否存储
823
+
824
+ Returns:
825
+ 是否成功
826
+ """
827
+ cmd = SetLockButton(self._device_params, lock=lock, store=store)
828
+ return cmd.is_success
829
+
830
+ @staticmethod
831
+ def broadcast_get_id(serial_connection: Serial) -> int:
832
+ """广播读取ID地址.
833
+
834
+ 当忘记电机ID地址时,可以单独接线该电机,广播读取该电机的地址。
835
+
836
+ Args:
837
+ serial_connection: 串口连接
838
+
839
+ Returns:
840
+ 电机ID
841
+ """
842
+ device_params = DeviceParams(
843
+ serial_connection=serial_connection,
844
+ address=Address.BROADCAST,
845
+ )
846
+ cmd = BroadcastGetID(device_params)
847
+ return cmd.data
848
+
849
+ # ==================== 便捷方法 ====================
850
+
851
+ def is_enabled(self) -> bool:
852
+ """检查电机是否使能.
853
+
854
+ Returns:
855
+ 是否使能
856
+ """
857
+ status = self.get_motor_status()
858
+ return status.enabled
859
+
860
+ def is_position_reached(self) -> bool:
861
+ """检查是否到达目标位置.
862
+
863
+ Returns:
864
+ 是否到达
865
+ """
866
+ status = self.get_motor_status()
867
+ return status.position_reached
868
+
869
+ def is_stalled(self) -> bool:
870
+ """检查是否堵转.
871
+
872
+ Returns:
873
+ 是否堵转
874
+ """
875
+ status = self.get_motor_status()
876
+ return status.stall_detected or status.stall_protected
877
+
878
+ def wait_for_position(self, timeout: float = 10.0, poll_interval: float = 0.1) -> bool:
879
+ """等待电机到达目标位置.
880
+
881
+ Args:
882
+ timeout: 超时时间(秒)
883
+ poll_interval: 轮询间隔(秒)
884
+
885
+ Returns:
886
+ 是否到达目标位置
887
+ """
888
+ import time
889
+ start_time = time.time()
890
+ while time.time() - start_time < timeout:
891
+ if self.is_position_reached():
892
+ return True
893
+ time.sleep(poll_interval)
894
+ return False
895
+
896
+ def __repr__(self) -> str:
897
+ return f"EmmDevice(address={self.address})"
898
+
899
+ def __str__(self) -> str:
900
+ return f"Emm步进电机 (地址: {self.address})"