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,590 @@
1
+ """Emm固件步进电机参数类.
2
+
3
+ 基于ZDT_X42S第二代闭环步进电机用户手册V1.0.3_251224。
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional
8
+
9
+ from serial import Serial
10
+
11
+ from .configs import (
12
+ Address,
13
+ ChecksumMode,
14
+ Direction,
15
+ SyncFlag,
16
+ StoreFlag,
17
+ MotionMode,
18
+ HomingMode,
19
+ HomingDirection,
20
+ ControlMode,
21
+ MotorType,
22
+ FirmwareType,
23
+ BaudRate,
24
+ CanRate,
25
+ ResponseMode,
26
+ StallProtect,
27
+ PulsePortMode,
28
+ SerialPortMode,
29
+ EnableLevel,
30
+ DirLevel,
31
+ )
32
+
33
+
34
+ def to_int(data: bytes) -> int:
35
+ """将字节转换为整数."""
36
+ return int.from_bytes(data, "big")
37
+
38
+
39
+ def to_signed_int(data: bytes) -> int:
40
+ """将带符号字节转换为有符号整数.
41
+
42
+ 第一个字节为符号位: 00=正, 01=负
43
+ """
44
+ if len(data) < 2:
45
+ return to_int(data)
46
+ sign = -1 if data[0] == 1 else 1
47
+ return sign * to_int(data[1:])
48
+
49
+
50
+ @dataclass
51
+ class DeviceParams:
52
+ """设备参数类.
53
+
54
+ Args:
55
+ serial_connection: 串口连接对象
56
+ address: 电机地址 (1-255, 0为广播地址)
57
+ checksum_mode: 校验模式
58
+ delay: 通讯延迟(秒)
59
+ """
60
+ serial_connection: Serial
61
+ address: int = Address.DEFAULT
62
+ checksum_mode: ChecksumMode = ChecksumMode.FIXED
63
+ delay: Optional[float] = None
64
+
65
+ def __post_init__(self):
66
+ if isinstance(self.address, int) and not isinstance(self.address, Address):
67
+ self.address = Address(self.address)
68
+
69
+
70
+ @dataclass
71
+ class JogParams:
72
+ """速度模式参数 (Emm固件).
73
+
74
+ 对应命令: 5.3.7 速度模式控制(Emm)
75
+
76
+ Args:
77
+ direction: 运动方向 (CW/CCW)
78
+ speed: 速度 (0-3000 RPM)
79
+ acceleration: 加速度档位 (0-255)
80
+ sync_flag: 同步标志
81
+ """
82
+ direction: Direction = Direction.CW
83
+ speed: int = 100 # 0-3000 RPM
84
+ acceleration: int = 10 # 0-255档位
85
+ sync_flag: SyncFlag = SyncFlag.IMMEDIATE
86
+
87
+ def __post_init__(self):
88
+ # 速度范围检查
89
+ if not 0 <= self.speed <= 3000:
90
+ raise ValueError("速度必须在 0-3000 RPM 之间")
91
+ # 加速度范围检查
92
+ if not 0 <= self.acceleration <= 255:
93
+ raise ValueError("加速度必须在 0-255 之间")
94
+
95
+ @property
96
+ def bytes(self) -> bytes:
97
+ """返回命令字节."""
98
+ return bytes([
99
+ self.direction,
100
+ (self.speed >> 8) & 0xFF,
101
+ self.speed & 0xFF,
102
+ self.acceleration,
103
+ self.sync_flag,
104
+ ])
105
+
106
+
107
+ @dataclass
108
+ class PositionParams:
109
+ """位置模式参数 (Emm固件).
110
+
111
+ 对应命令: 5.3.12 位置模式控制(Emm)
112
+
113
+ Args:
114
+ direction: 运动方向 (CW/CCW)
115
+ speed: 速度 (0-3000 RPM)
116
+ acceleration: 加速度档位 (0-255)
117
+ pulse_count: 脉冲数 (默认16细分下,3200个脉冲=一圈360°)
118
+ motion_mode: 运动模式 (相对/绝对)
119
+ sync_flag: 同步标志
120
+ """
121
+ direction: Direction = Direction.CW
122
+ speed: int = 100 # 0-3000 RPM
123
+ acceleration: int = 10 # 0-255档位
124
+ pulse_count: int = 3200 # 脉冲数
125
+ motion_mode: MotionMode = MotionMode.RELATIVE_LAST
126
+ sync_flag: SyncFlag = SyncFlag.IMMEDIATE
127
+
128
+ def __post_init__(self):
129
+ if not 0 <= self.speed <= 3000:
130
+ raise ValueError("速度必须在 0-3000 RPM 之间")
131
+ if not 0 <= self.acceleration <= 255:
132
+ raise ValueError("加速度必须在 0-255 之间")
133
+ if not 0 <= self.pulse_count <= 0xFFFFFFFF:
134
+ raise ValueError("脉冲数必须在 0-4294967295 之间")
135
+
136
+ @property
137
+ def bytes(self) -> bytes:
138
+ """返回命令字节."""
139
+ return bytes([
140
+ self.direction,
141
+ (self.speed >> 8) & 0xFF,
142
+ self.speed & 0xFF,
143
+ self.acceleration,
144
+ (self.pulse_count >> 24) & 0xFF,
145
+ (self.pulse_count >> 16) & 0xFF,
146
+ (self.pulse_count >> 8) & 0xFF,
147
+ self.pulse_count & 0xFF,
148
+ self.motion_mode,
149
+ self.sync_flag,
150
+ ])
151
+
152
+
153
+ @dataclass
154
+ class HomingParams:
155
+ """回零参数.
156
+
157
+ 对应命令: 5.4.6 修改回零参数
158
+
159
+ Args:
160
+ homing_mode: 回零模式
161
+ homing_direction: 回零方向
162
+ homing_speed: 回零速度 (0-3000 RPM)
163
+ homing_timeout: 回零超时时间 (毫秒)
164
+ collision_speed: 碰撞回零检测转速 (RPM)
165
+ collision_current: 碰撞回零检测电流 (mA)
166
+ collision_time: 碰撞回零检测时间 (ms)
167
+ auto_home: 是否使能上电自动触发回零
168
+ """
169
+ homing_mode: HomingMode = HomingMode.NEAREST
170
+ homing_direction: HomingDirection = HomingDirection.CW
171
+ homing_speed: int = 30 # RPM
172
+ homing_timeout: int = 10000 # ms
173
+ collision_speed: int = 300 # RPM
174
+ collision_current: int = 800 # mA
175
+ collision_time: int = 60 # ms
176
+ auto_home: bool = False
177
+
178
+ @property
179
+ def bytes(self) -> bytes:
180
+ """返回命令字节."""
181
+ return bytes([
182
+ self.homing_mode,
183
+ self.homing_direction,
184
+ (self.homing_speed >> 8) & 0xFF,
185
+ self.homing_speed & 0xFF,
186
+ (self.homing_timeout >> 24) & 0xFF,
187
+ (self.homing_timeout >> 16) & 0xFF,
188
+ (self.homing_timeout >> 8) & 0xFF,
189
+ self.homing_timeout & 0xFF,
190
+ (self.collision_speed >> 8) & 0xFF,
191
+ self.collision_speed & 0xFF,
192
+ (self.collision_current >> 8) & 0xFF,
193
+ self.collision_current & 0xFF,
194
+ (self.collision_time >> 8) & 0xFF,
195
+ self.collision_time & 0xFF,
196
+ 1 if self.auto_home else 0,
197
+ ])
198
+
199
+ @classmethod
200
+ def from_bytes(cls, data: bytes) -> "HomingParams":
201
+ """从字节数据解析回零参数."""
202
+ return cls(
203
+ homing_mode=HomingMode(data[0]),
204
+ homing_direction=HomingDirection(data[1]),
205
+ homing_speed=to_int(data[2:4]),
206
+ homing_timeout=to_int(data[4:8]),
207
+ collision_speed=to_int(data[8:10]),
208
+ collision_current=to_int(data[10:12]),
209
+ collision_time=to_int(data[12:14]),
210
+ auto_home=bool(data[14]),
211
+ )
212
+
213
+
214
+ @dataclass
215
+ class VersionParams:
216
+ """版本参数.
217
+
218
+ 对应命令: 5.5.2 读取固件版本和硬件版本
219
+ """
220
+ firmware_version: int = 0
221
+ hw_series: int = 0 # 0=X系列, 1=Y系列
222
+ hw_type: int = 0 # 0/1/2/3/4/5/6 = 20/28/35/42/57/86
223
+ hw_version: int = 0
224
+
225
+ @classmethod
226
+ def from_bytes(cls, data: bytes) -> "VersionParams":
227
+ """从字节数据解析版本参数."""
228
+ fw_ver = to_int(data[0:2])
229
+ hw_info = to_int(data[2:4])
230
+ return cls(
231
+ firmware_version=fw_ver,
232
+ hw_series=(hw_info >> 12) & 0x0F,
233
+ hw_type=(hw_info >> 8) & 0x0F,
234
+ hw_version=hw_info & 0xFF,
235
+ )
236
+
237
+ @property
238
+ def firmware_version_str(self) -> str:
239
+ """返回固件版本字符串."""
240
+ major = self.firmware_version // 100
241
+ minor = (self.firmware_version % 100) // 10
242
+ patch = self.firmware_version % 10
243
+ return f"V{major}.{minor}.{patch}"
244
+
245
+ @property
246
+ def hw_series_str(self) -> str:
247
+ """返回硬件系列字符串."""
248
+ return "X系列" if self.hw_series == 0 else "Y系列"
249
+
250
+ @property
251
+ def hw_type_str(self) -> str:
252
+ """返回硬件类型字符串."""
253
+ types = {0: "20", 1: "28", 2: "35", 3: "42", 4: "57", 5: "86"}
254
+ series = "X" if self.hw_series == 0 else "Y"
255
+ return f"{series}{types.get(self.hw_type, 'Unknown')}"
256
+
257
+
258
+ @dataclass
259
+ class MotorRHParams:
260
+ """电机相电阻和相电感参数.
261
+
262
+ 对应命令: 5.5.3 读取相电阻和相电感
263
+ """
264
+ phase_resistance: int = 0 # mΩ
265
+ phase_inductance: int = 0 # uH
266
+
267
+ @classmethod
268
+ def from_bytes(cls, data: bytes) -> "MotorRHParams":
269
+ """从字节数据解析."""
270
+ return cls(
271
+ phase_resistance=to_int(data[0:2]),
272
+ phase_inductance=to_int(data[2:4]),
273
+ )
274
+
275
+
276
+ @dataclass
277
+ class PIDParams:
278
+ """PID参数 (Emm固件).
279
+
280
+ 对应命令: 5.6.16 读取PID参数(Emm)
281
+ """
282
+ kp: int = 18000 # 比例系数
283
+ ki: int = 10 # 积分系数
284
+ kd: int = 18000 # 微分系数
285
+
286
+ @property
287
+ def bytes(self) -> bytes:
288
+ """返回命令字节."""
289
+ return bytes([
290
+ (self.kp >> 24) & 0xFF,
291
+ (self.kp >> 16) & 0xFF,
292
+ (self.kp >> 8) & 0xFF,
293
+ self.kp & 0xFF,
294
+ (self.ki >> 24) & 0xFF,
295
+ (self.ki >> 16) & 0xFF,
296
+ (self.ki >> 8) & 0xFF,
297
+ self.ki & 0xFF,
298
+ (self.kd >> 24) & 0xFF,
299
+ (self.kd >> 16) & 0xFF,
300
+ (self.kd >> 8) & 0xFF,
301
+ self.kd & 0xFF,
302
+ ])
303
+
304
+ @classmethod
305
+ def from_bytes(cls, data: bytes) -> "PIDParams":
306
+ """从字节数据解析PID参数."""
307
+ return cls(
308
+ kp=to_int(data[0:4]),
309
+ ki=to_int(data[4:8]),
310
+ kd=to_int(data[8:12]),
311
+ )
312
+
313
+
314
+ @dataclass
315
+ class HomingStatus:
316
+ """回零状态标志.
317
+
318
+ 对应命令: 5.4.4 读取回零状态标志
319
+ """
320
+ encoder_ready: bool = False # 编码器就绪
321
+ calibrated: bool = False # 校准表就绪
322
+ is_homing: bool = False # 正在回零
323
+ homing_failed: bool = False # 回零失败
324
+ over_temp: bool = False # 过热保护
325
+ over_current: bool = False # 过流保护
326
+
327
+ @classmethod
328
+ def from_byte(cls, data: int) -> "HomingStatus":
329
+ """从字节数据解析回零状态."""
330
+ return cls(
331
+ encoder_ready=bool(data & 0x01),
332
+ calibrated=bool(data & 0x02),
333
+ is_homing=bool(data & 0x04),
334
+ homing_failed=bool(data & 0x08),
335
+ over_temp=bool(data & 0x10),
336
+ over_current=bool(data & 0x20),
337
+ )
338
+
339
+ @property
340
+ def homing_state(self) -> str:
341
+ """返回回零状态字符串."""
342
+ if self.is_homing:
343
+ return "正在回零"
344
+ elif self.homing_failed:
345
+ return "回零失败"
346
+ else:
347
+ return "回零成功/未回零"
348
+
349
+
350
+ @dataclass
351
+ class MotorStatus:
352
+ """电机状态标志.
353
+
354
+ 对应命令: 5.5.15 读取电机状态标志
355
+ """
356
+ enabled: bool = False # 使能状态
357
+ position_reached: bool = False # 位置到达
358
+ stall_detected: bool = False # 堵转标志
359
+ stall_protected: bool = False # 堵转保护
360
+ left_limit: bool = False # 左限位开关状态
361
+ right_limit: bool = False # 右限位开关状态
362
+ power_off_flag: bool = True # 掉电标志
363
+
364
+ @classmethod
365
+ def from_byte(cls, data: int) -> "MotorStatus":
366
+ """从字节数据解析电机状态."""
367
+ return cls(
368
+ enabled=bool(data & 0x01),
369
+ position_reached=bool(data & 0x02),
370
+ stall_detected=bool(data & 0x04),
371
+ stall_protected=bool(data & 0x08),
372
+ left_limit=bool(data & 0x10),
373
+ right_limit=bool(data & 0x20),
374
+ power_off_flag=bool(data & 0x80),
375
+ )
376
+
377
+
378
+ @dataclass
379
+ class SystemStatusParams:
380
+ """系统状态参数 (Emm固件).
381
+
382
+ 对应命令: 5.8.2 读取系统状态参数(Emm)
383
+ """
384
+ bus_voltage: int = 0 # mV
385
+ phase_current: int = 0 # mA
386
+ encoder_value: int = 0 # 线性化编码器值
387
+ target_position: int = 0 # 电机目标位置
388
+ target_position_sign: int = 0 # 符号
389
+ realtime_speed: int = 0 # 电机实时转速 RPM
390
+ realtime_speed_sign: int = 0 # 符号
391
+ realtime_position: int = 0 # 电机实时位置
392
+ realtime_position_sign: int = 0 # 符号
393
+ position_error: int = 0 # 电机位置误差
394
+ position_error_sign: int = 0 # 符号
395
+ homing_status: HomingStatus = field(default_factory=HomingStatus)
396
+ motor_status: MotorStatus = field(default_factory=MotorStatus)
397
+
398
+ @classmethod
399
+ def from_bytes(cls, data: bytes) -> "SystemStatusParams":
400
+ """从字节数据解析系统状态参数."""
401
+ # Emm固件返回格式: 1F 09 + 数据
402
+ return cls(
403
+ bus_voltage=to_int(data[0:2]),
404
+ phase_current=to_int(data[2:4]),
405
+ encoder_value=to_int(data[4:6]),
406
+ target_position_sign=data[6],
407
+ target_position=to_int(data[7:11]),
408
+ realtime_speed_sign=data[11],
409
+ realtime_speed=to_int(data[12:14]),
410
+ realtime_position_sign=data[14],
411
+ realtime_position=to_int(data[15:19]),
412
+ position_error_sign=data[19],
413
+ position_error=to_int(data[20:24]),
414
+ homing_status=HomingStatus.from_byte(data[24]),
415
+ motor_status=MotorStatus.from_byte(data[25]),
416
+ )
417
+
418
+ @property
419
+ def target_position_deg(self) -> float:
420
+ """返回目标位置角度(度)."""
421
+ sign = -1 if self.target_position_sign == 1 else 1
422
+ return sign * (self.target_position * 360) / 65536
423
+
424
+ @property
425
+ def realtime_position_deg(self) -> float:
426
+ """返回实时位置角度(度)."""
427
+ sign = -1 if self.realtime_position_sign == 1 else 1
428
+ return sign * (self.realtime_position * 360) / 65536
429
+
430
+ @property
431
+ def position_error_deg(self) -> float:
432
+ """返回位置误差角度(度)."""
433
+ sign = -1 if self.position_error_sign == 1 else 1
434
+ return sign * (self.position_error * 360) / 65536
435
+
436
+ @property
437
+ def realtime_speed_rpm(self) -> int:
438
+ """返回实时转速(RPM)."""
439
+ sign = -1 if self.realtime_speed_sign == 1 else 1
440
+ return sign * self.realtime_speed
441
+
442
+
443
+ @dataclass
444
+ class ConfigParams:
445
+ """驱动配置参数 (Emm固件).
446
+
447
+ 对应命令: 5.8.5 读取驱动配置参数(Emm)
448
+ """
449
+ motor_type: MotorType = MotorType.DEGREE_18
450
+ pulse_port_mode: PulsePortMode = PulsePortMode.FOC
451
+ serial_port_mode: SerialPortMode = SerialPortMode.UART
452
+ enable_level: EnableLevel = EnableLevel.HOLD
453
+ dir_level: DirLevel = DirLevel.CW
454
+ microstep: int = 16
455
+ microstep_interp: bool = True
456
+ open_loop_current: int = 1200 # mA
457
+ closed_loop_current: int = 3000 # mA
458
+ max_voltage: int = 4000 # *3 mV
459
+ baud_rate: BaudRate = BaudRate.BAUD_115200
460
+ can_rate: CanRate = CanRate.CAN_500K
461
+ motor_id: int = 1
462
+ checksum_mode: ChecksumMode = ChecksumMode.FIXED
463
+ response_mode: ResponseMode = ResponseMode.RECEIVE
464
+ stall_protect: StallProtect = StallProtect.ENABLE
465
+ stall_speed: int = 8 # RPM
466
+ stall_current: int = 2200 # mA
467
+ stall_time: int = 2000 # ms
468
+ position_window: int = 8 # *0.1度
469
+
470
+ @property
471
+ def bytes(self) -> bytes:
472
+ """返回命令字节."""
473
+ return bytes([
474
+ self.motor_type,
475
+ self.pulse_port_mode,
476
+ self.serial_port_mode,
477
+ self.enable_level,
478
+ self.dir_level,
479
+ self.microstep,
480
+ 1 if self.microstep_interp else 0,
481
+ 0, # 保留
482
+ (self.open_loop_current >> 8) & 0xFF,
483
+ self.open_loop_current & 0xFF,
484
+ (self.closed_loop_current >> 8) & 0xFF,
485
+ self.closed_loop_current & 0xFF,
486
+ (self.max_voltage >> 8) & 0xFF,
487
+ self.max_voltage & 0xFF,
488
+ self.baud_rate,
489
+ self.can_rate,
490
+ self.motor_id,
491
+ self.checksum_mode,
492
+ self.response_mode,
493
+ self.stall_protect,
494
+ (self.stall_speed >> 8) & 0xFF,
495
+ self.stall_speed & 0xFF,
496
+ (self.stall_current >> 8) & 0xFF,
497
+ self.stall_current & 0xFF,
498
+ (self.stall_time >> 8) & 0xFF,
499
+ self.stall_time & 0xFF,
500
+ (self.position_window >> 8) & 0xFF,
501
+ self.position_window & 0xFF,
502
+ ])
503
+
504
+ @classmethod
505
+ def from_bytes(cls, data: bytes) -> "ConfigParams":
506
+ """从字节数据解析配置参数."""
507
+ return cls(
508
+ motor_type=MotorType(data[0]),
509
+ pulse_port_mode=PulsePortMode(data[1]),
510
+ serial_port_mode=SerialPortMode(data[2]),
511
+ enable_level=EnableLevel(data[3]),
512
+ dir_level=DirLevel(data[4]),
513
+ microstep=data[5],
514
+ microstep_interp=bool(data[6]),
515
+ open_loop_current=to_int(data[8:10]),
516
+ closed_loop_current=to_int(data[10:12]),
517
+ max_voltage=to_int(data[12:14]),
518
+ baud_rate=BaudRate(data[14]),
519
+ can_rate=CanRate(data[15]),
520
+ motor_id=data[16],
521
+ checksum_mode=ChecksumMode(data[17]),
522
+ response_mode=ResponseMode(data[18]),
523
+ stall_protect=StallProtect(data[19]),
524
+ stall_speed=to_int(data[20:22]),
525
+ stall_current=to_int(data[22:24]),
526
+ stall_time=to_int(data[24:26]),
527
+ position_window=to_int(data[26:28]),
528
+ )
529
+
530
+ @property
531
+ def position_window_deg(self) -> float:
532
+ """返回位置到达窗口(度)."""
533
+ return self.position_window * 0.1
534
+
535
+
536
+ @dataclass
537
+ class ProtectionThreshold:
538
+ """过热过流保护检测阈值.
539
+
540
+ 对应命令: 5.6.22 读取过热过流保护检测阈值
541
+ """
542
+ over_temp_threshold: int = 100 # °C
543
+ over_current_threshold: int = 6600 # mA
544
+ detection_time: int = 1000 # ms
545
+
546
+ @property
547
+ def bytes(self) -> bytes:
548
+ """返回命令字节."""
549
+ return bytes([
550
+ (self.over_temp_threshold >> 8) & 0xFF,
551
+ self.over_temp_threshold & 0xFF,
552
+ (self.over_current_threshold >> 8) & 0xFF,
553
+ self.over_current_threshold & 0xFF,
554
+ (self.detection_time >> 8) & 0xFF,
555
+ self.detection_time & 0xFF,
556
+ ])
557
+
558
+ @classmethod
559
+ def from_bytes(cls, data: bytes) -> "ProtectionThreshold":
560
+ """从字节数据解析."""
561
+ return cls(
562
+ over_temp_threshold=to_int(data[0:2]),
563
+ over_current_threshold=to_int(data[2:4]),
564
+ detection_time=to_int(data[4:6]),
565
+ )
566
+
567
+
568
+ @dataclass
569
+ class AutoRunParams:
570
+ """上电自动运行参数 (Emm固件).
571
+
572
+ 对应命令: 5.7.2 存储一组速度参数,上电自动运行(Emm)
573
+ """
574
+ store: bool = True # 存储/清除
575
+ direction: Direction = Direction.CW
576
+ speed: int = 600 # RPM
577
+ acceleration: int = 100 # 档位
578
+ enable_en_control: bool = False # 是否使能En引脚控制启停
579
+
580
+ @property
581
+ def bytes(self) -> bytes:
582
+ """返回命令字节."""
583
+ return bytes([
584
+ 1 if self.store else 0,
585
+ self.direction,
586
+ (self.speed >> 8) & 0xFF,
587
+ self.speed & 0xFF,
588
+ self.acceleration,
589
+ 1 if self.enable_en_control else 0,
590
+ ])