litegrip-python 1.1.2__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.
- litegrip/__init__.py +298 -0
- litegrip/actions.py +1444 -0
- litegrip/calibration_normal.json +9 -0
- litegrip/calibration_reverse.json +9 -0
- litegrip/can/__init__.py +41 -0
- litegrip/can/controller.py +413 -0
- litegrip/can/motor.py +188 -0
- litegrip/can/protocol.py +380 -0
- litegrip/can/transport.py +365 -0
- litegrip/constants.py +146 -0
- litegrip/exceptions.py +47 -0
- litegrip/factory_calibration.json +16 -0
- litegrip/gripper.py +2678 -0
- litegrip/models.py +210 -0
- litegrip/protocols/__init__.py +10 -0
- litegrip/protocols/can_bus.py +621 -0
- litegrip/py.typed +0 -0
- litegrip/teleop.py +1469 -0
- litegrip/trajectory.py +985 -0
- litegrip/zenoh_link.py +400 -0
- litegrip_python-1.1.2.dist-info/METADATA +636 -0
- litegrip_python-1.1.2.dist-info/RECORD +25 -0
- litegrip_python-1.1.2.dist-info/WHEEL +5 -0
- litegrip_python-1.1.2.dist-info/licenses/LICENSE +202 -0
- litegrip_python-1.1.2.dist-info/top_level.txt +1 -0
litegrip/can/protocol.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Damiao motor CAN protocol encoder/decoder — pure functions, no state.
|
|
2
|
+
|
|
3
|
+
Implements the complete DM motor protocol:
|
|
4
|
+
- MIT frame pack/unpack (q/dq/kp/kd/tau → 8 bytes)
|
|
5
|
+
- Command frames (enable/disable/clear fault/set zero)
|
|
6
|
+
- Parameter read/write/save frames (broadcast to 0x7FF)
|
|
7
|
+
- Status frame parsing
|
|
8
|
+
- Parameter response frame parsing
|
|
9
|
+
|
|
10
|
+
Reference: DM4310/DM4340/DM6248P CAN protocol specification.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import struct
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from enum import IntEnum
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
# ── CAN ID constants ─────────────────────────────────────────────────────
|
|
22
|
+
BROADCAST_ID = 0x7FF
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ControlMode(IntEnum):
|
|
26
|
+
"""CAN ID offsets for control frames."""
|
|
27
|
+
MIT_MODE = 0x000
|
|
28
|
+
POS_VEL_MODE = 0x100
|
|
29
|
+
VEL_MODE = 0x200
|
|
30
|
+
POS_FORCE_MODE = 0x300
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ControlModeCode(IntEnum):
|
|
34
|
+
"""CTRL_MODE register values."""
|
|
35
|
+
MIT = 1
|
|
36
|
+
POS_VEL = 2
|
|
37
|
+
VEL = 3
|
|
38
|
+
POS_FORCE = 4
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DM_REG(IntEnum):
|
|
42
|
+
"""Damiao motor register IDs."""
|
|
43
|
+
UV_Value = 0
|
|
44
|
+
KT_Value = 1
|
|
45
|
+
OT_Value = 2
|
|
46
|
+
OC_Value = 3
|
|
47
|
+
ACC = 4
|
|
48
|
+
DEC = 5
|
|
49
|
+
MAX_SPD = 6
|
|
50
|
+
MST_ID = 7
|
|
51
|
+
ESC_ID = 8
|
|
52
|
+
TIMEOUT = 9
|
|
53
|
+
CTRL_MODE = 10
|
|
54
|
+
Damp = 11
|
|
55
|
+
Inertia = 12
|
|
56
|
+
hw_ver = 13
|
|
57
|
+
sw_ver = 14
|
|
58
|
+
SN = 15
|
|
59
|
+
NPP = 16
|
|
60
|
+
Rs = 17
|
|
61
|
+
LS = 18
|
|
62
|
+
Flux = 19
|
|
63
|
+
Gr = 20
|
|
64
|
+
PMAX = 21
|
|
65
|
+
VMAX = 22
|
|
66
|
+
TMAX = 23
|
|
67
|
+
I_BW = 24
|
|
68
|
+
KP_ASR = 25
|
|
69
|
+
KI_ASR = 26
|
|
70
|
+
KP_APR = 27
|
|
71
|
+
KI_APR = 28
|
|
72
|
+
OV_Value = 29
|
|
73
|
+
GREF = 30
|
|
74
|
+
Deta = 31
|
|
75
|
+
V_BW = 32
|
|
76
|
+
IQ_c1 = 33
|
|
77
|
+
VL_c1 = 34
|
|
78
|
+
can_br = 35
|
|
79
|
+
sub_ver = 36
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Integer register ranges (big-endian decode on read response) ─────────
|
|
83
|
+
_INT_REG_RANGES = (
|
|
84
|
+
(7, 10),
|
|
85
|
+
(13, 16),
|
|
86
|
+
(35, 36),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_int_register(rid: int) -> bool:
|
|
91
|
+
"""Return True if the register ID stores an integer value."""
|
|
92
|
+
for lo, hi in _INT_REG_RANGES:
|
|
93
|
+
if lo <= rid <= hi:
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Quantization helpers ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def float_to_uint(value: float, value_min: float, value_max: float,
|
|
101
|
+
bits: int) -> int:
|
|
102
|
+
"""Quantize a float to unsigned integer of given bit width.
|
|
103
|
+
|
|
104
|
+
Out-of-range values are clamped — that is the documented saturation of
|
|
105
|
+
this codec. Non-finite ones are **rejected**: a NaN has no position on
|
|
106
|
+
the scale at all, and the clamp would silently hand it the top of the
|
|
107
|
+
range (``value_max``), which is a real command the caller never asked for.
|
|
108
|
+
On a motion frame that is ``q = +12.5 rad`` or ``tau = +tau_max``.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: ``value`` is NaN or infinite.
|
|
112
|
+
"""
|
|
113
|
+
if not math.isfinite(value):
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"cannot encode a non-finite value: value={value!r} "
|
|
116
|
+
f"(range [{value_min}, {value_max}], {bits} bits)")
|
|
117
|
+
value = max(value_min, min(value_max, value))
|
|
118
|
+
span = value_max - value_min
|
|
119
|
+
offset = value - value_min
|
|
120
|
+
return int(offset * ((1 << bits) - 1) / span)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def uint_to_float(value: int, value_min: float, value_max: float,
|
|
124
|
+
bits: int) -> float:
|
|
125
|
+
"""Dequantize an unsigned integer back to float."""
|
|
126
|
+
span = value_max - value_min
|
|
127
|
+
return float(value) * span / float((1 << bits) - 1) + value_min
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── MIT control frame ────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def pack_mit_frame(
|
|
133
|
+
q: float,
|
|
134
|
+
dq: float,
|
|
135
|
+
kp: float,
|
|
136
|
+
kd: float,
|
|
137
|
+
tau: float,
|
|
138
|
+
q_max: float = 12.5,
|
|
139
|
+
dq_max: float = 30.0,
|
|
140
|
+
tau_max: float = 10.0,
|
|
141
|
+
) -> bytes:
|
|
142
|
+
"""Pack position/torque targets into an 8-byte MIT control frame.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
q: Target position (rad), mapped to [-q_max, +q_max] in 16 bits.
|
|
146
|
+
dq: Target velocity (rad/s), mapped to [-dq_max, +dq_max] in 12 bits.
|
|
147
|
+
kp: Position stiffness (0–500), mapped to [0, 500] in 12 bits.
|
|
148
|
+
kd: Velocity damping (0–5), mapped to [0, 5] in 12 bits.
|
|
149
|
+
tau: Feed-forward torque (Nm), mapped to [-tau_max, +tau_max] in 12 bits.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
8-byte CAN payload.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: any field is NaN or infinite. Nothing non-finite ever
|
|
156
|
+
becomes bytes: see :func:`float_to_uint`.
|
|
157
|
+
"""
|
|
158
|
+
q_uint = float_to_uint(q, -q_max, q_max, 16)
|
|
159
|
+
dq_uint = float_to_uint(dq, -dq_max, dq_max, 12)
|
|
160
|
+
kp_uint = float_to_uint(kp, 0.0, 500.0, 12)
|
|
161
|
+
kd_uint = float_to_uint(kd, 0.0, 5.0, 12)
|
|
162
|
+
tau_uint = float_to_uint(tau, -tau_max, tau_max, 12)
|
|
163
|
+
|
|
164
|
+
# Bit packing per protocol spec
|
|
165
|
+
data = bytearray(8)
|
|
166
|
+
data[0] = (q_uint >> 8) & 0xFF
|
|
167
|
+
data[1] = q_uint & 0xFF
|
|
168
|
+
data[2] = (dq_uint >> 4) & 0xFF
|
|
169
|
+
data[3] = ((dq_uint & 0x0F) << 4) | ((kp_uint >> 8) & 0x0F)
|
|
170
|
+
data[4] = kp_uint & 0xFF
|
|
171
|
+
data[5] = (kd_uint >> 4) & 0xFF
|
|
172
|
+
data[6] = ((kd_uint & 0x0F) << 4) | ((tau_uint >> 8) & 0x0F)
|
|
173
|
+
data[7] = tau_uint & 0xFF
|
|
174
|
+
|
|
175
|
+
return bytes(data)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── Status frame (motor → PC) ────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class ParsedStatus:
|
|
182
|
+
"""Parsed motor status frame."""
|
|
183
|
+
err: int # 4-bit error code (0=disabled, 1=enabled, 0x9=UV, etc.)
|
|
184
|
+
can_id: int # 4-bit CAN ID (low nibble of data[0])
|
|
185
|
+
q: float # Position (rad)
|
|
186
|
+
dq: float # Velocity (rad/s)
|
|
187
|
+
tau: float # Torque (Nm)
|
|
188
|
+
t_mos: int # MOS temperature (°C)
|
|
189
|
+
t_coil: int # Coil temperature (°C)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def unpack_status_frame(
|
|
193
|
+
data: bytes,
|
|
194
|
+
q_max: float = 12.5,
|
|
195
|
+
dq_max: float = 30.0,
|
|
196
|
+
tau_max: float = 10.0,
|
|
197
|
+
) -> ParsedStatus:
|
|
198
|
+
"""Parse an 8-byte motor status frame.
|
|
199
|
+
|
|
200
|
+
Byte layout::
|
|
201
|
+
|
|
202
|
+
[0] err(4) | can_id(4)
|
|
203
|
+
[1] q[15:8] [2] q[7:0]
|
|
204
|
+
[3] dq[11:4] [4] dq[3:0] | tau[11:8]
|
|
205
|
+
[5] tau[7:0]
|
|
206
|
+
[6] t_mos [7] t_coil
|
|
207
|
+
"""
|
|
208
|
+
if len(data) < 8:
|
|
209
|
+
raise ValueError(f"Status frame needs 8 bytes, got {len(data)}")
|
|
210
|
+
|
|
211
|
+
err = (data[0] >> 4) & 0x0F
|
|
212
|
+
cid = data[0] & 0x0F
|
|
213
|
+
|
|
214
|
+
q_uint = ((data[1] << 8) | data[2]) & 0xFFFF
|
|
215
|
+
dq_uint = ((data[3] << 4) | (data[4] >> 4)) & 0xFFF
|
|
216
|
+
tau_uint = (((data[4] & 0x0F) << 8) | data[5]) & 0xFFF
|
|
217
|
+
|
|
218
|
+
q = uint_to_float(q_uint, -q_max, q_max, 16)
|
|
219
|
+
dq = uint_to_float(dq_uint, -dq_max, dq_max, 12)
|
|
220
|
+
tau = uint_to_float(tau_uint, -tau_max, tau_max, 12)
|
|
221
|
+
|
|
222
|
+
t_mos = data[6]
|
|
223
|
+
t_coil = data[7]
|
|
224
|
+
|
|
225
|
+
return ParsedStatus(err=err, can_id=cid, q=q, dq=dq, tau=tau,
|
|
226
|
+
t_mos=t_mos, t_coil=t_coil)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── Command frames (PC → motor) ──────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
# Command byte values (placed in byte 7)
|
|
232
|
+
CMD_ENABLE = 0xFC
|
|
233
|
+
CMD_DISABLE = 0xFD
|
|
234
|
+
CMD_CLEAR_FAULT = 0xFB
|
|
235
|
+
CMD_SET_ZERO = 0xFE
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def pack_command_frame(cmd: int) -> bytes:
|
|
239
|
+
"""Pack a command frame (enable/disable/clear fault/set zero).
|
|
240
|
+
|
|
241
|
+
All 8 bytes are 0xFF except byte 7 which holds the command.
|
|
242
|
+
"""
|
|
243
|
+
data = bytearray([0xFF] * 7)
|
|
244
|
+
data.append(cmd)
|
|
245
|
+
return bytes(data)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ── Status refresh frame ────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
def pack_refresh_frame(can_id: int) -> bytes:
|
|
251
|
+
"""Pack a status refresh request (sent to broadcast ID 0x7FF).
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
4-byte frame: [can_id_lo, can_id_hi, 0xCC, 0x00]
|
|
255
|
+
"""
|
|
256
|
+
return bytes([can_id & 0xFF, (can_id >> 8) & 0xFF, 0xCC, 0x00])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ── Parameter read/write/save frames ────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
def pack_read_param_frame(can_id: int, rid: int) -> bytes:
|
|
262
|
+
"""Pack a parameter read request (sent to broadcast ID 0x7FF).
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
8-byte frame: [can_id_lo, can_id_hi, 0x33, RID, 0, 0, 0, 0]
|
|
266
|
+
"""
|
|
267
|
+
return bytes([can_id & 0xFF, (can_id >> 8) & 0xFF, 0x33, rid & 0xFF,
|
|
268
|
+
0, 0, 0, 0])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def pack_write_param_frame(can_id: int, rid: int, value) -> bytes:
|
|
272
|
+
"""Pack a parameter write request (sent to broadcast ID 0x7FF).
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
can_id: Motor CAN ID.
|
|
276
|
+
rid: Register ID.
|
|
277
|
+
value: int or float — int registers packed as little-endian uint32,
|
|
278
|
+
float registers packed as IEEE 754 little-endian float32.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
8-byte frame: [can_id_lo, can_id_hi, 0x55, RID, data[0..3]]
|
|
282
|
+
"""
|
|
283
|
+
if _is_int_register(rid):
|
|
284
|
+
data_bytes = int(value).to_bytes(4, "little", signed=False)
|
|
285
|
+
else:
|
|
286
|
+
data_bytes = struct.pack("<f", float(value))
|
|
287
|
+
|
|
288
|
+
return bytes([can_id & 0xFF, (can_id >> 8) & 0xFF, 0x55, rid & 0xFF,
|
|
289
|
+
data_bytes[0], data_bytes[1], data_bytes[2], data_bytes[3]])
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def pack_save_param_frame(can_id: int) -> bytes:
|
|
293
|
+
"""Pack a parameter save-to-flash request (sent to broadcast ID 0x7FF).
|
|
294
|
+
|
|
295
|
+
Note: Motor must be disabled before saving.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
8-byte frame: [can_id_lo, can_id_hi, 0xAA, 0x01, 0, 0, 0, 0]
|
|
299
|
+
"""
|
|
300
|
+
return bytes([can_id & 0xFF, (can_id >> 8) & 0xFF, 0xAA, 0x01,
|
|
301
|
+
0, 0, 0, 0])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ── Parameter response frame (motor → PC) ───────────────────────────────
|
|
305
|
+
|
|
306
|
+
@dataclass
|
|
307
|
+
class ParsedParamResponse:
|
|
308
|
+
"""Parsed parameter read/write response."""
|
|
309
|
+
can_id: int # Motor CAN ID (from data[0] & 0x0F)
|
|
310
|
+
opcode: int # 0x33 = read response, 0x55 = write response, 0xAA = save response
|
|
311
|
+
rid: int # Register ID
|
|
312
|
+
value: float # Decoded value (float for float regs, int cast to float for int regs)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def unpack_param_response(data: bytes) -> Optional[ParsedParamResponse]:
|
|
316
|
+
"""Parse a parameter response frame.
|
|
317
|
+
|
|
318
|
+
Byte layout::
|
|
319
|
+
|
|
320
|
+
[0] motor_id (low nibble = can_id)
|
|
321
|
+
[1] can_id_hi
|
|
322
|
+
[2] opcode (0x33=read, 0x55=write, 0xAA=save)
|
|
323
|
+
[3] RID
|
|
324
|
+
[4..7] data (4 bytes)
|
|
325
|
+
|
|
326
|
+
Integer registers: little-endian uint32 decode (matching damiao firmware
|
|
327
|
+
convention on the wire).
|
|
328
|
+
Float registers: little-endian IEEE 754 float32.
|
|
329
|
+
"""
|
|
330
|
+
if len(data) < 8:
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
can_id = data[0] & 0x0F
|
|
334
|
+
opcode = data[2]
|
|
335
|
+
rid = data[3]
|
|
336
|
+
|
|
337
|
+
if opcode not in (0x33, 0x55, 0xAA):
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
if _is_int_register(rid):
|
|
341
|
+
value = float((data[7] << 24) | (data[6] << 16) |
|
|
342
|
+
(data[5] << 8) | data[4])
|
|
343
|
+
else:
|
|
344
|
+
try:
|
|
345
|
+
value = struct.unpack("<f", bytes(data[4:8]))[0]
|
|
346
|
+
except struct.error:
|
|
347
|
+
value = 0.0
|
|
348
|
+
|
|
349
|
+
return ParsedParamResponse(can_id=can_id, opcode=opcode, rid=rid,
|
|
350
|
+
value=value)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ── Motor type parameters ────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
@dataclass
|
|
356
|
+
class MotorLimits:
|
|
357
|
+
"""MIT frame quantization limits for a motor type."""
|
|
358
|
+
q_max: float # rad
|
|
359
|
+
dq_max: float # rad/s
|
|
360
|
+
tau_max: float # Nm
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Per-motor-type limits (matching DM4310/DM4340/DM6248P datasheet)
|
|
364
|
+
MOTOR_LIMITS: dict[int, MotorLimits] = {
|
|
365
|
+
1: MotorLimits(q_max=12.5, dq_max=30.0, tau_max=10.0), # DM4310
|
|
366
|
+
3: MotorLimits(q_max=12.5, dq_max=10.0, tau_max=28.0), # DM4340
|
|
367
|
+
6: MotorLimits(q_max=12.566, dq_max=20.0, tau_max=120.0), # DM6248P
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def get_motor_limits(motor_type: int) -> MotorLimits:
|
|
372
|
+
"""Get quantization limits for a given motor type index.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
motor_type: DM_Motor_Type enum value (1=DM4310, 3=DM4340, 6=DM6248P, etc.)
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
MotorLimits for that type. Falls back to DM4310 limits for unknown types.
|
|
379
|
+
"""
|
|
380
|
+
return MOTOR_LIMITS.get(motor_type, MOTOR_LIMITS[1])
|