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.
@@ -0,0 +1,1284 @@
1
+ """Emm固件步进电机命令类.
2
+
3
+ 基于ZDT_X42S第二代闭环步进电机用户手册V1.0.3_251224。
4
+ """
5
+
6
+ import logging
7
+ from abc import ABC, abstractmethod
8
+ from time import sleep, time
9
+ from typing import Optional, TypeVar, Generic, Any
10
+
11
+ from .configs import (
12
+ Code,
13
+ Protocol,
14
+ StatusCode,
15
+ ChecksumMode,
16
+ Address,
17
+ SyncFlag,
18
+ StoreFlag,
19
+ Direction,
20
+ EnableFlag,
21
+ HomingMode,
22
+ MotionMode,
23
+ add_checksum,
24
+ calculate_checksum,
25
+ SystemConstants,
26
+ )
27
+ from .parameters import (
28
+ DeviceParams,
29
+ JogParams,
30
+ PositionParams,
31
+ HomingParams,
32
+ VersionParams,
33
+ MotorRHParams,
34
+ PIDParams,
35
+ HomingStatus,
36
+ MotorStatus,
37
+ SystemStatusParams,
38
+ ConfigParams,
39
+ ProtectionThreshold,
40
+ AutoRunParams,
41
+ to_int,
42
+ to_signed_int,
43
+ )
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+ T = TypeVar('T')
48
+
49
+
50
+ class CommandError(Exception):
51
+ """命令执行错误."""
52
+ pass
53
+
54
+
55
+ class Command(ABC, Generic[T]):
56
+ """命令基类."""
57
+
58
+ _code: Code
59
+ _protocol: Optional[Protocol] = None
60
+ _response_length: int = 4 # 默认: 地址 + 功能码 + 状态 + 校验
61
+
62
+ def __init__(self, device: DeviceParams):
63
+ """初始化命令.
64
+
65
+ Args:
66
+ device: 设备参数
67
+ """
68
+ self._timestamp = time()
69
+ self._response: Optional[bytes] = None
70
+ self._data: Optional[T] = None
71
+ self._status: StatusCode = StatusCode.FORMAT_ERROR
72
+
73
+ self.device = device
74
+ self.address = device.address
75
+ self.checksum_mode = device.checksum_mode
76
+ self.delay = device.delay
77
+ self.serial = device.serial_connection
78
+
79
+ # 构建并执行命令
80
+ self._command = self._build_command()
81
+ self._execute()
82
+
83
+ @abstractmethod
84
+ def _build_command_body(self) -> bytes:
85
+ """构建命令体(不含校验码)."""
86
+ pass
87
+
88
+ @abstractmethod
89
+ def _parse_response(self, data: bytes) -> T:
90
+ """解析响应数据."""
91
+ pass
92
+
93
+ def _build_command(self) -> bytes:
94
+ """构建完整命令(含校验码)."""
95
+ body = self._build_command_body()
96
+ return add_checksum(body, self.checksum_mode)
97
+
98
+ def _execute(self) -> None:
99
+ """执行命令."""
100
+ tries = 0
101
+ while tries < SystemConstants.MAX_RETRIES:
102
+ try:
103
+ # 清空缓冲区
104
+ self.serial.reset_input_buffer()
105
+ self.serial.reset_output_buffer()
106
+
107
+ # 发送命令
108
+ logger.debug(f"发送命令: {self._command.hex()}")
109
+ self.serial.write(self._command)
110
+
111
+ # 读取响应
112
+ response = self._read_response()
113
+ if response:
114
+ self._response = response
115
+ self._status = StatusCode.SUCCESS
116
+ break
117
+
118
+ except Exception as e:
119
+ logger.warning(f"命令执行失败 (尝试 {tries + 1}): {e}")
120
+ tries += 1
121
+
122
+ if self.delay:
123
+ sleep(self.delay)
124
+
125
+ if tries >= SystemConstants.MAX_RETRIES:
126
+ logger.error("命令执行失败: 超过最大重试次数")
127
+
128
+ def _read_response(self) -> Optional[bytes]:
129
+ """读取响应."""
130
+ # 读取地址
131
+ addr = self.serial.read(1)
132
+ if not addr:
133
+ raise CommandError("未收到响应")
134
+
135
+ # 验证地址
136
+ expected_addr = 1 if self.address == Address.BROADCAST else self.address
137
+ if addr[0] != expected_addr:
138
+ raise CommandError(f"地址不匹配: 期望 {expected_addr}, 收到 {addr[0]}")
139
+
140
+ # 读取功能码
141
+ code = self.serial.read(1)
142
+ if not code:
143
+ raise CommandError("未收到功能码")
144
+
145
+ logger.debug(f"收到功能码: 0x{code[0]:02X}")
146
+
147
+ # 读取数据
148
+ data_length = self._response_length - 3 # 减去地址、功能码、校验码
149
+ data = self.serial.read(data_length) if data_length > 0 else b''
150
+
151
+ # 读取校验码
152
+ checksum = self.serial.read(1)
153
+ if not checksum:
154
+ raise CommandError("未收到校验码")
155
+
156
+ # 验证校验码
157
+ response_body = addr + code + data
158
+ expected_checksum = calculate_checksum(response_body, self.checksum_mode)
159
+ if checksum[0] != expected_checksum:
160
+ raise CommandError(f"校验码不匹配: 期望 0x{expected_checksum:02X}, 收到 0x{checksum[0]:02X}")
161
+
162
+ # 解析数据
163
+ if data:
164
+ self._data = self._parse_response(data)
165
+
166
+ return response_body + checksum
167
+
168
+ @property
169
+ def response(self) -> Optional[bytes]:
170
+ """返回原始响应."""
171
+ return self._response
172
+
173
+ @property
174
+ def data(self) -> Optional[T]:
175
+ """返回解析后的数据."""
176
+ return self._data
177
+
178
+ @property
179
+ def is_success(self) -> bool:
180
+ """命令是否成功."""
181
+ return self._status == StatusCode.SUCCESS
182
+
183
+ @property
184
+ def status(self) -> str:
185
+ """返回状态字符串."""
186
+ return self._status.name
187
+
188
+
189
+ class SimpleCommand(Command[bool]):
190
+ """简单命令(只返回成功/失败)."""
191
+
192
+ def _parse_response(self, data: bytes) -> bool:
193
+ """解析响应."""
194
+ if data[0] == StatusCode.SUCCESS:
195
+ return True
196
+ elif data[0] == StatusCode.PARAM_ERROR:
197
+ logger.warning("命令参数错误")
198
+ return False
199
+ elif data[0] == StatusCode.FORMAT_ERROR:
200
+ logger.warning("命令格式错误")
201
+ return False
202
+ return False
203
+
204
+
205
+ class ReadCommand(Command[T]):
206
+ """读取命令基类."""
207
+
208
+ def _build_command_body(self) -> bytes:
209
+ """构建命令体."""
210
+ return bytes([self.address, self._code])
211
+
212
+
213
+ # ==================== 触发动作命令 ====================
214
+
215
+ class CalibrateEncoder(SimpleCommand):
216
+ """触发编码器校准.
217
+
218
+ 对应命令: 5.2.1 触发编码器校准
219
+ 发送: 01 06 45 6B
220
+ 返回: 01 06 02 6B
221
+ """
222
+ _code = Code.CAL_ENCODER
223
+ _protocol = Protocol.CAL_ENCODER
224
+
225
+ def _build_command_body(self) -> bytes:
226
+ return bytes([self.address, self._code, self._protocol])
227
+
228
+
229
+ class Restart(SimpleCommand):
230
+ """重启电机.
231
+
232
+ 对应命令: 5.2.2 重启电机(X42S/Y42)
233
+ 发送: 01 08 97 6B
234
+ 返回: 01 08 02 6B
235
+ """
236
+ _code = Code.RESTART
237
+ _protocol = Protocol.RESTART
238
+
239
+ def _build_command_body(self) -> bytes:
240
+ return bytes([self.address, self._code, self._protocol])
241
+
242
+
243
+ class ZeroPosition(SimpleCommand):
244
+ """将当前位置角度清零.
245
+
246
+ 对应命令: 5.2.3 将当前位置角度清零
247
+ 发送: 01 0A 6D 6B
248
+ 返回: 01 0A 02 6B
249
+ """
250
+ _code = Code.ZERO_POSITION
251
+ _protocol = Protocol.ZERO_POSITION
252
+
253
+ def _build_command_body(self) -> bytes:
254
+ return bytes([self.address, self._code, self._protocol])
255
+
256
+
257
+ class ClearProtection(SimpleCommand):
258
+ """解除堵转/过热/过流保护.
259
+
260
+ 对应命令: 5.2.4 解除堵转/过热/过流保护
261
+ 发送: 01 0E 52 6B
262
+ 返回: 01 0E 02 6B
263
+ """
264
+ _code = Code.CLEAR_PROTECTION
265
+ _protocol = Protocol.CLEAR_PROTECTION
266
+
267
+ def _build_command_body(self) -> bytes:
268
+ return bytes([self.address, self._code, self._protocol])
269
+
270
+
271
+ class FactoryReset(SimpleCommand):
272
+ """恢复出厂设置.
273
+
274
+ 对应命令: 5.2.5 恢复出厂设置
275
+ 发送: 01 0F 5F 6B
276
+ 返回: 01 0F 02 6B
277
+ """
278
+ _code = Code.FACTORY_RESET
279
+ _protocol = Protocol.FACTORY_RESET
280
+
281
+ def _build_command_body(self) -> bytes:
282
+ return bytes([self.address, self._code, self._protocol])
283
+
284
+
285
+ # ==================== 运动控制命令 ====================
286
+
287
+ class Enable(SimpleCommand):
288
+ """电机使能控制.
289
+
290
+ 对应命令: 5.3.2 电机使能控制
291
+ 发送: 01 F3 AB 01 00 6B (使能)
292
+ 返回: 01 F3 02 6B
293
+ """
294
+ _code = Code.ENABLE
295
+ _protocol = Protocol.ENABLE
296
+
297
+ def __init__(self, device: DeviceParams, enable: bool = True,
298
+ sync_flag: SyncFlag = SyncFlag.IMMEDIATE):
299
+ self.enable = enable
300
+ self.sync_flag = sync_flag
301
+ super().__init__(device)
302
+
303
+ def _build_command_body(self) -> bytes:
304
+ return bytes([
305
+ self.address,
306
+ self._code,
307
+ self._protocol,
308
+ EnableFlag.ENABLE if self.enable else EnableFlag.DISABLE,
309
+ self.sync_flag,
310
+ ])
311
+
312
+
313
+ class Disable(Enable):
314
+ """电机失能(松轴)."""
315
+
316
+ def __init__(self, device: DeviceParams, sync_flag: SyncFlag = SyncFlag.IMMEDIATE):
317
+ super().__init__(device, enable=False, sync_flag=sync_flag)
318
+
319
+
320
+ class Jog(SimpleCommand):
321
+ """速度模式控制 (Emm固件).
322
+
323
+ 对应命令: 5.3.7 速度模式控制(Emm)
324
+ 发送: 01 F6 01 05DC 0A 00 6B
325
+ 返回: 01 F6 02 6B
326
+ """
327
+ _code = Code.JOG
328
+
329
+ def __init__(self, device: DeviceParams, params: Optional[JogParams] = None,
330
+ direction: Direction = Direction.CW, speed: int = 100,
331
+ acceleration: int = 10, sync_flag: SyncFlag = SyncFlag.IMMEDIATE):
332
+ if params:
333
+ self.params = params
334
+ else:
335
+ self.params = JogParams(
336
+ direction=direction,
337
+ speed=speed,
338
+ acceleration=acceleration,
339
+ sync_flag=sync_flag,
340
+ )
341
+ super().__init__(device)
342
+
343
+ def _build_command_body(self) -> bytes:
344
+ return bytes([self.address, self._code]) + self.params.bytes
345
+
346
+
347
+ class Position(SimpleCommand):
348
+ """位置模式控制 (Emm固件).
349
+
350
+ 对应命令: 5.3.12 位置模式控制(Emm)
351
+ 发送: 01 FD 01 05DC 00 00007D00 00 00 6B
352
+ 返回: 01 FD 02 6B
353
+ """
354
+ _code = Code.POSITION
355
+
356
+ def __init__(self, device: DeviceParams, params: Optional[PositionParams] = None,
357
+ direction: Direction = Direction.CW, speed: int = 100,
358
+ acceleration: int = 10, pulse_count: int = 3200,
359
+ motion_mode: MotionMode = MotionMode.RELATIVE_LAST,
360
+ sync_flag: SyncFlag = SyncFlag.IMMEDIATE):
361
+ if params:
362
+ self.params = params
363
+ else:
364
+ self.params = PositionParams(
365
+ direction=direction,
366
+ speed=speed,
367
+ acceleration=acceleration,
368
+ pulse_count=pulse_count,
369
+ motion_mode=motion_mode,
370
+ sync_flag=sync_flag,
371
+ )
372
+ super().__init__(device)
373
+
374
+ def _build_command_body(self) -> bytes:
375
+ return bytes([self.address, self._code]) + self.params.bytes
376
+
377
+
378
+ class EStop(SimpleCommand):
379
+ """立即停止.
380
+
381
+ 对应命令: 5.3.13 立即停止
382
+ 发送: 01 FE 98 00 6B
383
+ 返回: 01 FE 02 6B
384
+ """
385
+ _code = Code.ESTOP
386
+ _protocol = Protocol.ESTOP
387
+
388
+ def __init__(self, device: DeviceParams, sync_flag: SyncFlag = SyncFlag.IMMEDIATE):
389
+ self.sync_flag = sync_flag
390
+ super().__init__(device)
391
+
392
+ def _build_command_body(self) -> bytes:
393
+ return bytes([self.address, self._code, self._protocol, self.sync_flag])
394
+
395
+
396
+ class SyncMove(SimpleCommand):
397
+ """触发多机同步运动.
398
+
399
+ 对应命令: 5.3.14 触发多机同步运动
400
+ 发送: 00 FF 66 6B
401
+ 返回: 01 FF 02 6B
402
+ """
403
+ _code = Code.SYNC_MOVE
404
+ _protocol = Protocol.SYNC_MOVE
405
+
406
+ def _build_command_body(self) -> bytes:
407
+ # 使用广播地址
408
+ return bytes([Address.BROADCAST, self._code, self._protocol])
409
+
410
+
411
+ # ==================== 原点回零命令 ====================
412
+
413
+ class SetHomeZero(SimpleCommand):
414
+ """设置单圈回零的零点位置.
415
+
416
+ 对应命令: 5.4.1 设置单圈回零的零点位置
417
+ 发送: 01 93 88 01 6B
418
+ 返回: 01 93 02 6B
419
+ """
420
+ _code = Code.SET_HOME_ZERO
421
+ _protocol = Protocol.SET_HOME_ZERO
422
+
423
+ def __init__(self, device: DeviceParams, store: bool = True):
424
+ self.store = store
425
+ super().__init__(device)
426
+
427
+ def _build_command_body(self) -> bytes:
428
+ return bytes([
429
+ self.address,
430
+ self._code,
431
+ self._protocol,
432
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
433
+ ])
434
+
435
+
436
+ class Home(SimpleCommand):
437
+ """触发回零.
438
+
439
+ 对应命令: 5.4.2 触发回零
440
+ 发送: 01 9A 00 00 6B
441
+ 返回: 01 9A 02 6B
442
+ """
443
+ _code = Code.HOME
444
+
445
+ def __init__(self, device: DeviceParams, mode: HomingMode = HomingMode.NEAREST,
446
+ sync_flag: SyncFlag = SyncFlag.IMMEDIATE):
447
+ self.mode = mode
448
+ self.sync_flag = sync_flag
449
+ super().__init__(device)
450
+
451
+ def _build_command_body(self) -> bytes:
452
+ return bytes([self.address, self._code, self.mode, self.sync_flag])
453
+
454
+
455
+ class StopHome(SimpleCommand):
456
+ """强制中断并退出回零操作.
457
+
458
+ 对应命令: 5.4.3 强制中断并退出回零操作
459
+ 发送: 01 9C 48 6B
460
+ 返回: 01 9C 02 6B
461
+ """
462
+ _code = Code.STOP_HOME
463
+ _protocol = Protocol.STOP_HOME
464
+
465
+ def _build_command_body(self) -> bytes:
466
+ return bytes([self.address, self._code, self._protocol])
467
+
468
+
469
+ class GetHomingStatus(Command[HomingStatus]):
470
+ """读取回零状态标志.
471
+
472
+ 对应命令: 5.4.4 读取回零状态标志
473
+ 发送: 01 3B 6B
474
+ 返回: 01 3B 03 6B
475
+ """
476
+ _code = Code.GET_HOME_STATUS
477
+ _response_length = 4
478
+
479
+ def _build_command_body(self) -> bytes:
480
+ return bytes([self.address, self._code])
481
+
482
+ def _parse_response(self, data: bytes) -> HomingStatus:
483
+ return HomingStatus.from_byte(data[0])
484
+
485
+
486
+ class GetHomingParams(Command[HomingParams]):
487
+ """读取回零参数.
488
+
489
+ 对应命令: 5.4.5 读取回零参数
490
+ 发送: 01 22 6B
491
+ 返回: 01 22 + 15字节数据 + 6B
492
+ """
493
+ _code = Code.GET_HOME_PARAM
494
+ _response_length = 18 # 地址 + 功能码 + 15字节数据 + 校验
495
+
496
+ def _build_command_body(self) -> bytes:
497
+ return bytes([self.address, self._code])
498
+
499
+ def _parse_response(self, data: bytes) -> HomingParams:
500
+ return HomingParams.from_bytes(data)
501
+
502
+
503
+ class SetHomingParams(SimpleCommand):
504
+ """修改回零参数.
505
+
506
+ 对应命令: 5.4.6 修改回零参数
507
+ """
508
+ _code = Code.SET_HOME_PARAM
509
+ _protocol = Protocol.SET_HOME_PARAM
510
+
511
+ def __init__(self, device: DeviceParams, params: HomingParams, store: bool = True):
512
+ self.params = params
513
+ self.store = store
514
+ super().__init__(device)
515
+
516
+ def _build_command_body(self) -> bytes:
517
+ return bytes([
518
+ self.address,
519
+ self._code,
520
+ self._protocol,
521
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
522
+ ]) + self.params.bytes
523
+
524
+
525
+ # ==================== 读取系统参数命令 ====================
526
+
527
+ class GetVersion(Command[VersionParams]):
528
+ """读取固件版本和硬件版本.
529
+
530
+ 对应命令: 5.5.2 读取固件版本和硬件版本
531
+ 发送: 01 1F 6B
532
+ 返回: 01 1F + 4字节数据 + 6B
533
+ """
534
+ _code = Code.GET_VERSION
535
+ _response_length = 7
536
+
537
+ def _build_command_body(self) -> bytes:
538
+ return bytes([self.address, self._code])
539
+
540
+ def _parse_response(self, data: bytes) -> VersionParams:
541
+ return VersionParams.from_bytes(data)
542
+
543
+
544
+ class GetMotorRH(Command[MotorRHParams]):
545
+ """读取相电阻和相电感.
546
+
547
+ 对应命令: 5.5.3 读取相电阻和相电感
548
+ 发送: 01 20 6B
549
+ 返回: 01 20 + 4字节数据 + 6B
550
+ """
551
+ _code = Code.GET_MOTOR_RH
552
+ _response_length = 7
553
+
554
+ def _build_command_body(self) -> bytes:
555
+ return bytes([self.address, self._code])
556
+
557
+ def _parse_response(self, data: bytes) -> MotorRHParams:
558
+ return MotorRHParams.from_bytes(data)
559
+
560
+
561
+ class GetBusVoltage(Command[int]):
562
+ """读取总线电压.
563
+
564
+ 对应命令: 5.5.4 读取总线电压
565
+ 发送: 01 24 6B
566
+ 返回: 01 24 + 2字节数据 + 6B
567
+ """
568
+ _code = Code.GET_BUS_VOLTAGE
569
+ _response_length = 5
570
+
571
+ def _build_command_body(self) -> bytes:
572
+ return bytes([self.address, self._code])
573
+
574
+ def _parse_response(self, data: bytes) -> int:
575
+ """返回总线电压(mV)."""
576
+ return to_int(data)
577
+
578
+
579
+ class GetBusCurrent(Command[int]):
580
+ """读取总线电流.
581
+
582
+ 对应命令: 5.5.5 读取总线电流(X42S/Y42)
583
+ 发送: 01 26 6B
584
+ 返回: 01 26 + 2字节数据 + 6B
585
+ """
586
+ _code = Code.GET_BUS_CURRENT
587
+ _response_length = 5
588
+
589
+ def _build_command_body(self) -> bytes:
590
+ return bytes([self.address, self._code])
591
+
592
+ def _parse_response(self, data: bytes) -> int:
593
+ """返回总线电流(mA)."""
594
+ return to_int(data)
595
+
596
+
597
+ class GetPhaseCurrent(Command[int]):
598
+ """读取相电流.
599
+
600
+ 对应命令: 5.5.6 读取相电流
601
+ 发送: 01 27 6B
602
+ 返回: 01 27 + 2字节数据 + 6B
603
+ """
604
+ _code = Code.GET_PHASE_CURRENT
605
+ _response_length = 5
606
+
607
+ def _build_command_body(self) -> bytes:
608
+ return bytes([self.address, self._code])
609
+
610
+ def _parse_response(self, data: bytes) -> int:
611
+ """返回相电流(mA)."""
612
+ return to_int(data)
613
+
614
+
615
+ class GetEncoder(Command[int]):
616
+ """读取线性化编码器值.
617
+
618
+ 对应命令: 5.5.7 读取经过线性化校准后的编码器值
619
+ 发送: 01 31 6B
620
+ 返回: 01 31 + 2字节数据 + 6B
621
+ """
622
+ _code = Code.GET_ENCODER
623
+ _response_length = 5
624
+
625
+ def _build_command_body(self) -> bytes:
626
+ return bytes([self.address, self._code])
627
+
628
+ def _parse_response(self, data: bytes) -> int:
629
+ """返回编码器值(0-65535表示0-360度)."""
630
+ return to_int(data)
631
+
632
+
633
+ class GetPulseCount(Command[int]):
634
+ """读取输入脉冲数.
635
+
636
+ 对应命令: 5.5.8 读取输入脉冲数
637
+ 发送: 01 32 6B
638
+ 返回: 01 32 + 5字节数据 + 6B
639
+ """
640
+ _code = Code.GET_PULSE_COUNT
641
+ _response_length = 8
642
+
643
+ def _build_command_body(self) -> bytes:
644
+ return bytes([self.address, self._code])
645
+
646
+ def _parse_response(self, data: bytes) -> int:
647
+ """返回输入脉冲数(带符号)."""
648
+ return to_signed_int(data)
649
+
650
+
651
+ class GetTargetPosition(Command[float]):
652
+ """读取电机目标位置.
653
+
654
+ 对应命令: 5.5.9 读取电机目标位置
655
+ 发送: 01 33 6B
656
+ 返回: 01 33 + 5字节数据 + 6B
657
+ """
658
+ _code = Code.GET_TARGET_POSITION
659
+ _response_length = 8
660
+
661
+ def _build_command_body(self) -> bytes:
662
+ return bytes([self.address, self._code])
663
+
664
+ def _parse_response(self, data: bytes) -> float:
665
+ """返回目标位置角度(度).
666
+
667
+ Emm固件: 0-65535表示一圈0-360°
668
+ """
669
+ value = to_signed_int(data)
670
+ return (value * 360) / 65536
671
+
672
+
673
+ class GetRealtimeSpeed(Command[int]):
674
+ """读取电机实时转速.
675
+
676
+ 对应命令: 5.5.11 读取电机实时转速
677
+ 发送: 01 35 6B
678
+ 返回: 01 35 + 3字节数据 + 6B
679
+ """
680
+ _code = Code.GET_REALTIME_SPEED
681
+ _response_length = 6
682
+
683
+ def _build_command_body(self) -> bytes:
684
+ return bytes([self.address, self._code])
685
+
686
+ def _parse_response(self, data: bytes) -> int:
687
+ """返回实时转速(RPM, 带符号)."""
688
+ sign = -1 if data[0] == 1 else 1
689
+ return sign * to_int(data[1:3])
690
+
691
+
692
+ class GetRealtimePosition(Command[float]):
693
+ """读取电机实时位置.
694
+
695
+ 对应命令: 5.5.13 读取电机实时位置
696
+ 发送: 01 36 6B
697
+ 返回: 01 36 + 5字节数据 + 6B
698
+ """
699
+ _code = Code.GET_REALTIME_POSITION
700
+ _response_length = 8
701
+
702
+ def _build_command_body(self) -> bytes:
703
+ return bytes([self.address, self._code])
704
+
705
+ def _parse_response(self, data: bytes) -> float:
706
+ """返回实时位置角度(度).
707
+
708
+ Emm固件: 0-65535表示一圈0-360°
709
+ """
710
+ value = to_signed_int(data)
711
+ return (value * 360) / 65536
712
+
713
+
714
+ class GetPositionError(Command[float]):
715
+ """读取电机位置误差.
716
+
717
+ 对应命令: 5.5.14 读取电机位置误差
718
+ 发送: 01 37 6B
719
+ 返回: 01 37 + 5字节数据 + 6B
720
+ """
721
+ _code = Code.GET_POSITION_ERROR
722
+ _response_length = 8
723
+
724
+ def _build_command_body(self) -> bytes:
725
+ return bytes([self.address, self._code])
726
+
727
+ def _parse_response(self, data: bytes) -> float:
728
+ """返回位置误差角度(度).
729
+
730
+ Emm固件: 0-65535表示一圈0-360°
731
+ """
732
+ value = to_signed_int(data)
733
+ return (value * 360) / 65536
734
+
735
+
736
+ class GetTemperature(Command[int]):
737
+ """读取驱动温度.
738
+
739
+ 对应命令: 5.5.12 读取驱动温度(X42S/Y42)
740
+ 发送: 01 39 6B
741
+ 返回: 01 39 + 2字节数据 + 6B
742
+ """
743
+ _code = Code.GET_TEMPERATURE
744
+ _response_length = 5
745
+
746
+ def _build_command_body(self) -> bytes:
747
+ return bytes([self.address, self._code])
748
+
749
+ def _parse_response(self, data: bytes) -> int:
750
+ """返回温度(°C, 带符号)."""
751
+ sign = -1 if data[0] == 0 else 1 # 00=负, 01=正
752
+ return sign * data[1]
753
+
754
+
755
+ class GetMotorStatus(Command[MotorStatus]):
756
+ """读取电机状态标志.
757
+
758
+ 对应命令: 5.5.15 读取电机状态标志
759
+ 发送: 01 3A 6B
760
+ 返回: 01 3A + 1字节数据 + 6B
761
+ """
762
+ _code = Code.GET_MOTOR_STATUS
763
+ _response_length = 4
764
+
765
+ def _build_command_body(self) -> bytes:
766
+ return bytes([self.address, self._code])
767
+
768
+ def _parse_response(self, data: bytes) -> MotorStatus:
769
+ return MotorStatus.from_byte(data[0])
770
+
771
+
772
+ class GetPID(Command[PIDParams]):
773
+ """读取PID参数 (Emm固件).
774
+
775
+ 对应命令: 5.6.16 读取PID参数(Emm)
776
+ 发送: 01 21 6B
777
+ 返回: 01 21 + 12字节数据 + 6B
778
+ """
779
+ _code = Code.GET_PID
780
+ _response_length = 15
781
+
782
+ def _build_command_body(self) -> bytes:
783
+ return bytes([self.address, self._code])
784
+
785
+ def _parse_response(self, data: bytes) -> PIDParams:
786
+ return PIDParams.from_bytes(data)
787
+
788
+
789
+ class DynamicLengthCommand(Command[T]):
790
+ """动态长度响应命令基类.
791
+
792
+ 用于响应长度在响应数据中指定的命令(如读取配置参数、系统状态等)。
793
+
794
+ 根据说明书 5.8.5 和 5.8.2,Emm固件返回格式:
795
+ - 字节1: 地址
796
+ - 字节2: 功能码 (0x42 或 0x43)
797
+ - 字节3: 字节数 (整个响应的总字节数,包括地址到校验码)
798
+ - 字节4: 参数个数
799
+ - 字节5-N: 数据
800
+ - 字节N+1: 校验码
801
+
802
+ 例如 Emm固件 get_config 返回:
803
+ - 字节数=0x21(33) 表示整个响应共33字节
804
+ - 数据长度 = 字节数 - 4 (减去地址、功能码、字节数、参数个数) - 1 (校验码)
805
+ - 即 33 - 5 = 28 字节数据
806
+ """
807
+
808
+ def _read_response(self) -> Optional[bytes]:
809
+ """读取动态长度响应."""
810
+ # 读取地址
811
+ addr = self.serial.read(1)
812
+ if not addr:
813
+ raise CommandError("未收到响应")
814
+
815
+ # 验证地址
816
+ expected_addr = 1 if self.address == Address.BROADCAST else self.address
817
+ if addr[0] != expected_addr:
818
+ raise CommandError(f"地址不匹配: 期望 {expected_addr}, 收到 {addr[0]}")
819
+
820
+ # 读取功能码
821
+ code = self.serial.read(1)
822
+ if not code:
823
+ raise CommandError("未收到功能码")
824
+
825
+ logger.debug(f"收到功能码: 0x{code[0]:02X}")
826
+
827
+ # 读取字节数(整个响应的总字节数)
828
+ byte_count = self.serial.read(1)
829
+ if not byte_count:
830
+ raise CommandError("未收到字节数")
831
+
832
+ total_response_length = byte_count[0]
833
+ logger.debug(f"响应总字节数: {total_response_length}")
834
+
835
+ # 读取参数个数
836
+ param_count = self.serial.read(1)
837
+ if not param_count:
838
+ raise CommandError("未收到参数个数")
839
+
840
+ logger.debug(f"参数个数: {param_count[0]}")
841
+
842
+ # 计算剩余数据长度
843
+ # 已读取: 地址(1) + 功能码(1) + 字节数(1) + 参数个数(1) = 4 字节
844
+ # 剩余: 数据 + 校验码 = 总长度 - 4
845
+ # 数据长度 = 总长度 - 4 - 1(校验码) = 总长度 - 5
846
+ data_length = total_response_length - 5
847
+ remaining_data = self.serial.read(data_length)
848
+ if len(remaining_data) < data_length:
849
+ raise CommandError(f"数据不完整: 期望 {data_length} 字节, 收到 {len(remaining_data)} 字节")
850
+
851
+ # 读取校验码
852
+ checksum = self.serial.read(1)
853
+ if not checksum:
854
+ raise CommandError("未收到校验码")
855
+
856
+ # 组合完整数据(包含字节数和参数个数)
857
+ data = byte_count + param_count + remaining_data
858
+
859
+ # 验证校验码
860
+ response_body = addr + code + data
861
+ expected_checksum = calculate_checksum(response_body, self.checksum_mode)
862
+ if checksum[0] != expected_checksum:
863
+ raise CommandError(f"校验码不匹配: 期望 0x{expected_checksum:02X}, 收到 0x{checksum[0]:02X}")
864
+
865
+ # 解析数据
866
+ if data:
867
+ self._data = self._parse_response(data)
868
+
869
+ return response_body + checksum
870
+
871
+
872
+ class GetConfig(DynamicLengthCommand[ConfigParams]):
873
+ """读取驱动配置参数 (Emm固件).
874
+
875
+ 对应命令: 5.8.5 读取驱动配置参数(Emm)
876
+ 发送: 01 42 6C 6B
877
+ 返回: 01 42 21 15 + 数据 + 6B
878
+
879
+ Emm固件返回: 字节数=0x21(33), 参数个数=0x15(21)
880
+ """
881
+ _code = Code.GET_CONFIG
882
+ _protocol = Protocol.GET_CONFIG
883
+
884
+ def _build_command_body(self) -> bytes:
885
+ return bytes([self.address, self._code, self._protocol])
886
+
887
+ def _parse_response(self, data: bytes) -> ConfigParams:
888
+ # data[0] = 字节数, data[1] = 参数个数, data[2:] = 实际数据
889
+ return ConfigParams.from_bytes(data[2:])
890
+
891
+
892
+ class GetSystemStatus(DynamicLengthCommand[SystemStatusParams]):
893
+ """读取系统状态参数 (Emm固件).
894
+
895
+ 对应命令: 5.8.2 读取系统状态参数(Emm)
896
+ 发送: 01 43 7A 6B
897
+ 返回: 01 43 1F 09 + 数据 + 6B
898
+
899
+ Emm固件返回: 字节数=0x1F(31), 参数个数=0x09(9)
900
+ """
901
+ _code = Code.GET_SYS_STATUS
902
+ _protocol = Protocol.GET_SYS_STATUS
903
+
904
+ def _build_command_body(self) -> bytes:
905
+ return bytes([self.address, self._code, self._protocol])
906
+
907
+ def _parse_response(self, data: bytes) -> SystemStatusParams:
908
+ # data[0] = 字节数, data[1] = 参数个数, data[2:] = 实际数据
909
+ return SystemStatusParams.from_bytes(data[2:])
910
+
911
+
912
+ # ==================== 设置命令 ====================
913
+
914
+ class SetID(SimpleCommand):
915
+ """修改电机ID/地址.
916
+
917
+ 对应命令: 5.6.1 修改电机ID/地址
918
+ 发送: 01 AE 4B 01 02 6B
919
+ 返回: 01 AE 02 6B
920
+ """
921
+ _code = Code.SET_ID
922
+ _protocol = Protocol.SET_ID
923
+
924
+ def __init__(self, device: DeviceParams, new_id: int, store: bool = True):
925
+ if not 1 <= new_id <= 255:
926
+ raise ValueError("ID必须在 1-255 之间")
927
+ self.new_id = new_id
928
+ self.store = store
929
+ super().__init__(device)
930
+
931
+ def _build_command_body(self) -> bytes:
932
+ return bytes([
933
+ self.address,
934
+ self._code,
935
+ self._protocol,
936
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
937
+ self.new_id,
938
+ ])
939
+
940
+
941
+ class SetMicrostep(SimpleCommand):
942
+ """修改细分值.
943
+
944
+ 对应命令: 5.6.2 修改细分值
945
+ 发送: 01 84 8A 01 10 6B
946
+ 返回: 01 84 02 6B
947
+ """
948
+ _code = Code.SET_MICROSTEP
949
+ _protocol = Protocol.SET_MICROSTEP
950
+
951
+ def __init__(self, device: DeviceParams, microstep: int, store: bool = True):
952
+ if not 1 <= microstep <= 256:
953
+ raise ValueError("细分值必须在 1-256 之间")
954
+ self.microstep = microstep if microstep < 256 else 0 # 256用0表示
955
+ self.store = store
956
+ super().__init__(device)
957
+
958
+ def _build_command_body(self) -> bytes:
959
+ return bytes([
960
+ self.address,
961
+ self._code,
962
+ self._protocol,
963
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
964
+ self.microstep,
965
+ ])
966
+
967
+
968
+ class SetLoopMode(SimpleCommand):
969
+ """修改开环/闭环控制模式.
970
+
971
+ 对应命令: 5.6.7 修改开环/闭环控制模式
972
+ 发送: 01 46 A6 01 01 6B
973
+ 返回: 01 46 02 6B
974
+ """
975
+ _code = Code.SET_LOOP_MODE
976
+ _protocol = Protocol.SET_LOOP_MODE
977
+
978
+ def __init__(self, device: DeviceParams, closed_loop: bool = True, store: bool = True):
979
+ self.closed_loop = closed_loop
980
+ self.store = store
981
+ super().__init__(device)
982
+
983
+ def _build_command_body(self) -> bytes:
984
+ from .configs import ControlMode
985
+ return bytes([
986
+ self.address,
987
+ self._code,
988
+ self._protocol,
989
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
990
+ ControlMode.CLOSED_LOOP if self.closed_loop else ControlMode.OPEN_LOOP,
991
+ ])
992
+
993
+
994
+ class SetOpenLoopCurrent(SimpleCommand):
995
+ """修改开环模式工作电流.
996
+
997
+ 对应命令: 5.6.12 修改开环模式工作电流
998
+ 发送: 01 44 33 01 04B0 6B
999
+ 返回: 01 44 02 6B
1000
+ """
1001
+ _code = Code.SET_OPEN_LOOP_CURRENT
1002
+ _protocol = Protocol.SET_OPEN_LOOP_CURRENT
1003
+
1004
+ def __init__(self, device: DeviceParams, current_ma: int, store: bool = True):
1005
+ if not 0 <= current_ma <= 5000:
1006
+ raise ValueError("电流必须在 0-5000 mA 之间")
1007
+ self.current = current_ma
1008
+ self.store = store
1009
+ super().__init__(device)
1010
+
1011
+ def _build_command_body(self) -> bytes:
1012
+ return bytes([
1013
+ self.address,
1014
+ self._code,
1015
+ self._protocol,
1016
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1017
+ (self.current >> 8) & 0xFF,
1018
+ self.current & 0xFF,
1019
+ ])
1020
+
1021
+
1022
+ class SetClosedLoopCurrent(SimpleCommand):
1023
+ """修改闭环模式最大电流.
1024
+
1025
+ 对应命令: 5.6.13 修改闭环模式最大电流
1026
+ 发送: 01 45 66 01 0BB8 6B
1027
+ 返回: 01 45 02 6B
1028
+ """
1029
+ _code = Code.SET_CLOSED_LOOP_CURRENT
1030
+ _protocol = Protocol.SET_CLOSED_LOOP_CURRENT
1031
+
1032
+ def __init__(self, device: DeviceParams, current_ma: int, store: bool = True):
1033
+ if not 0 <= current_ma <= 5000:
1034
+ raise ValueError("电流必须在 0-5000 mA 之间")
1035
+ self.current = current_ma
1036
+ self.store = store
1037
+ super().__init__(device)
1038
+
1039
+ def _build_command_body(self) -> bytes:
1040
+ return bytes([
1041
+ self.address,
1042
+ self._code,
1043
+ self._protocol,
1044
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1045
+ (self.current >> 8) & 0xFF,
1046
+ self.current & 0xFF,
1047
+ ])
1048
+
1049
+
1050
+ class SetPID(SimpleCommand):
1051
+ """修改PID参数 (Emm固件).
1052
+
1053
+ 对应命令: 5.6.17 修改PID参数(Emm)
1054
+ """
1055
+ _code = Code.SET_PID
1056
+ _protocol = Protocol.SET_PID
1057
+
1058
+ def __init__(self, device: DeviceParams, params: PIDParams, store: bool = True):
1059
+ self.params = params
1060
+ self.store = store
1061
+ super().__init__(device)
1062
+
1063
+ def _build_command_body(self) -> bytes:
1064
+ return bytes([
1065
+ self.address,
1066
+ self._code,
1067
+ self._protocol,
1068
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1069
+ ]) + self.params.bytes
1070
+
1071
+
1072
+ class SetMotorDirection(SimpleCommand):
1073
+ """修改电机运动正方向.
1074
+
1075
+ 对应命令: 5.6.8 修改电机运动正方向
1076
+ 发送: 01 D4 60 01 00 6B
1077
+ 返回: 01 D4 02 6B
1078
+ """
1079
+ _code = Code.SET_MOTOR_DIRECTION
1080
+ _protocol = Protocol.SET_MOTOR_DIRECTION
1081
+
1082
+ def __init__(self, device: DeviceParams, direction: Direction = Direction.CW, store: bool = True):
1083
+ self.direction = direction
1084
+ self.store = store
1085
+ super().__init__(device)
1086
+
1087
+ def _build_command_body(self) -> bytes:
1088
+ return bytes([
1089
+ self.address,
1090
+ self._code,
1091
+ self._protocol,
1092
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1093
+ self.direction,
1094
+ ])
1095
+
1096
+
1097
+ class SetPositionWindow(SimpleCommand):
1098
+ """修改位置到达窗口.
1099
+
1100
+ 对应命令: 5.6.21 修改位置到达窗口(X42S/Y42)
1101
+ 发送: 01 D1 07 01 0008 6B
1102
+ 返回: 01 D1 02 6B
1103
+ """
1104
+ _code = Code.SET_POSITION_WINDOW
1105
+ _protocol = Protocol.SET_POSITION_WINDOW
1106
+
1107
+ def __init__(self, device: DeviceParams, window_deg: float = 0.8, store: bool = True):
1108
+ """设置位置到达窗口.
1109
+
1110
+ Args:
1111
+ device: 设备参数
1112
+ window_deg: 位置到达窗口(度), 默认0.8度
1113
+ store: 是否存储
1114
+ """
1115
+ self.window = int(window_deg * 10) # 内部缩小10倍处理
1116
+ self.store = store
1117
+ super().__init__(device)
1118
+
1119
+ def _build_command_body(self) -> bytes:
1120
+ return bytes([
1121
+ self.address,
1122
+ self._code,
1123
+ self._protocol,
1124
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1125
+ (self.window >> 8) & 0xFF,
1126
+ self.window & 0xFF,
1127
+ ])
1128
+
1129
+
1130
+ class SetHeartbeatTime(SimpleCommand):
1131
+ """修改心跳保护功能时间.
1132
+
1133
+ 对应命令: 5.6.25 修改心跳保护功能时间(X42S/Y42)
1134
+ 发送: 01 68 38 01 00001388 6B
1135
+ 返回: 01 68 02 6B
1136
+ """
1137
+ _code = Code.SET_HEARTBEAT_TIME
1138
+ _protocol = Protocol.SET_HEARTBEAT_TIME
1139
+
1140
+ def __init__(self, device: DeviceParams, time_ms: int = 0, store: bool = True):
1141
+ """设置心跳保护时间.
1142
+
1143
+ Args:
1144
+ device: 设备参数
1145
+ time_ms: 心跳保护时间(毫秒), 0表示关闭
1146
+ store: 是否存储
1147
+ """
1148
+ self.time_ms = time_ms
1149
+ self.store = store
1150
+ super().__init__(device)
1151
+
1152
+ def _build_command_body(self) -> bytes:
1153
+ return bytes([
1154
+ self.address,
1155
+ self._code,
1156
+ self._protocol,
1157
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1158
+ (self.time_ms >> 24) & 0xFF,
1159
+ (self.time_ms >> 16) & 0xFF,
1160
+ (self.time_ms >> 8) & 0xFF,
1161
+ self.time_ms & 0xFF,
1162
+ ])
1163
+
1164
+
1165
+ class SetAutoRun(SimpleCommand):
1166
+ """存储一组速度参数,上电自动运行 (Emm固件).
1167
+
1168
+ 对应命令: 5.7.2 存储一组速度参数,上电自动运行(Emm)
1169
+ """
1170
+ _code = Code.SET_AUTO_RUN
1171
+ _protocol = Protocol.SET_AUTO_RUN
1172
+
1173
+ def __init__(self, device: DeviceParams, params: AutoRunParams):
1174
+ self.params = params
1175
+ super().__init__(device)
1176
+
1177
+ def _build_command_body(self) -> bytes:
1178
+ return bytes([
1179
+ self.address,
1180
+ self._code,
1181
+ self._protocol,
1182
+ ]) + self.params.bytes
1183
+
1184
+
1185
+ class SetConfig(SimpleCommand):
1186
+ """修改驱动配置参数 (Emm固件).
1187
+
1188
+ 对应命令: 5.8.6 修改驱动配置参数(Emm)
1189
+ """
1190
+ _code = Code.SET_CONFIG
1191
+ _protocol = Protocol.SET_CONFIG
1192
+
1193
+ def __init__(self, device: DeviceParams, params: ConfigParams, store: bool = True):
1194
+ self.params = params
1195
+ self.store = store
1196
+ super().__init__(device)
1197
+
1198
+ def _build_command_body(self) -> bytes:
1199
+ return bytes([
1200
+ self.address,
1201
+ self._code,
1202
+ self._protocol,
1203
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1204
+ ]) + self.params.bytes
1205
+
1206
+
1207
+ class SetScaleInput(SimpleCommand):
1208
+ """修改命令速度值是否缩小10倍输入 (Emm固件).
1209
+
1210
+ 对应命令: 5.6.11 修改命令速度值是否缩小10倍输入(Emm)
1211
+ 发送: 01 4F 71 01 01 6B
1212
+ 返回: 01 4F 02 6B
1213
+ """
1214
+ _code = Code.SET_SCALE_INPUT
1215
+ _protocol = Protocol.SET_SCALE_INPUT
1216
+
1217
+ def __init__(self, device: DeviceParams, enable: bool = False, store: bool = True):
1218
+ """设置速度值缩小10倍输入.
1219
+
1220
+ Args:
1221
+ device: 设备参数
1222
+ enable: 是否使能(使能后输入1RPM实际为0.1RPM)
1223
+ store: 是否存储
1224
+ """
1225
+ self.enable = enable
1226
+ self.store = store
1227
+ super().__init__(device)
1228
+
1229
+ def _build_command_body(self) -> bytes:
1230
+ return bytes([
1231
+ self.address,
1232
+ self._code,
1233
+ self._protocol,
1234
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1235
+ 1 if self.enable else 0,
1236
+ ])
1237
+
1238
+
1239
+ class SetLockButton(SimpleCommand):
1240
+ """修改锁定按键功能.
1241
+
1242
+ 对应命令: 5.6.9 修改锁定按键功能
1243
+ 发送: 01 D0 B3 01 01 6B
1244
+ 返回: 01 D0 02 6B
1245
+ """
1246
+ _code = Code.SET_LOCK_BUTTON
1247
+ _protocol = Protocol.SET_LOCK_BUTTON
1248
+
1249
+ def __init__(self, device: DeviceParams, lock: bool = False, store: bool = True):
1250
+ self.lock = lock
1251
+ self.store = store
1252
+ super().__init__(device)
1253
+
1254
+ def _build_command_body(self) -> bytes:
1255
+ return bytes([
1256
+ self.address,
1257
+ self._code,
1258
+ self._protocol,
1259
+ StoreFlag.STORE if self.store else StoreFlag.NO_STORE,
1260
+ 1 if self.lock else 0,
1261
+ ])
1262
+
1263
+
1264
+ class BroadcastGetID(Command[int]):
1265
+ """广播读取ID地址.
1266
+
1267
+ 对应命令: 5.6.30 广播读取ID地址(X42S/Y42)
1268
+ 发送: 00 15 6B
1269
+ 返回: 01 15 01 6B
1270
+ """
1271
+ _code = Code.BROADCAST_GET_ID
1272
+ _response_length = 4
1273
+
1274
+ def __init__(self, device: DeviceParams):
1275
+ # 强制使用广播地址
1276
+ device.address = Address(Address.BROADCAST)
1277
+ super().__init__(device)
1278
+
1279
+ def _build_command_body(self) -> bytes:
1280
+ return bytes([Address.BROADCAST, self._code])
1281
+
1282
+ def _parse_response(self, data: bytes) -> int:
1283
+ """返回电机ID."""
1284
+ return data[0]