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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Raw SocketCAN transport — no dependencies beyond Python stdlib + Linux SocketCAN.
|
|
2
|
+
|
|
3
|
+
Supports classic CAN (MTU 16) and CAN FD (MTU 72) with automatic mode
|
|
4
|
+
detection. Provides send/receive with optional timeout.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import fcntl
|
|
10
|
+
import logging
|
|
11
|
+
import select
|
|
12
|
+
import socket
|
|
13
|
+
import struct
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import IntEnum
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger("litegrip.can.transport")
|
|
20
|
+
|
|
21
|
+
# ── SocketCAN constants ──────────────────────────────────────────────────
|
|
22
|
+
CAN_RAW = getattr(socket, "CAN_RAW", 1)
|
|
23
|
+
CAN_RAW_FD_FRAMES = getattr(socket, "CAN_RAW_FD_FRAMES", 5)
|
|
24
|
+
CAN_RAW_FILTER = getattr(socket, "CAN_RAW_FILTER", 1)
|
|
25
|
+
SOL_CAN_RAW = getattr(socket, "SOL_CAN_RAW", 101)
|
|
26
|
+
CAN_SFF_MASK = 0x7FF
|
|
27
|
+
CAN_EFF_MASK = 0x1FFFFFFF
|
|
28
|
+
|
|
29
|
+
# struct can_filter { canid_t can_id; canid_t can_mask; } — two native uint32.
|
|
30
|
+
_CAN_FILTER_FMT = "=II"
|
|
31
|
+
|
|
32
|
+
# Classic CAN frame: can_id(4B) + dlc(1B) + padding(3B) + data(8B) = 16B
|
|
33
|
+
CAN_FRAME_FMT = "=IB3x8s"
|
|
34
|
+
CAN_MTU = 16
|
|
35
|
+
|
|
36
|
+
# CAN FD frame: can_id(4B) + dlc(1B) + flags(1B) + len8_flag(1B) + reserved(1B) + data(64B) = 72B
|
|
37
|
+
CANFD_FRAME_FMT = "=IBBBB64s"
|
|
38
|
+
CANFD_FRAME_SIZE = struct.calcsize(CANFD_FRAME_FMT)
|
|
39
|
+
CANFD_BRS = 0x01
|
|
40
|
+
CANFD_MTU = 72
|
|
41
|
+
|
|
42
|
+
_SIOCGIFMTU = 0x8921
|
|
43
|
+
|
|
44
|
+
# sizeof(struct ifreq) — 40 bytes on 64-bit Linux, 32 on 32-bit. SIOCGIFMTU
|
|
45
|
+
# writes back the whole struct, not just ifr_name plus ifr_mtu.
|
|
46
|
+
_IFREQ_SIZE = 40
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _iface_mtu(sock: socket.socket, iface: str) -> Optional[int]:
|
|
50
|
+
"""Read interface MTU (16=classic CAN, 72=CAN FD). Returns None on failure."""
|
|
51
|
+
try:
|
|
52
|
+
ifr = struct.pack("16sH", iface.encode("utf-8")[:15], 0)
|
|
53
|
+
# Hand over a whole struct ifreq: the kernel writes all of it back, and
|
|
54
|
+
# since 3.14 fcntl.ioctl() raises SystemError (not OSError) when
|
|
55
|
+
# anything lands past the end of the argument. The reply is exactly as
|
|
56
|
+
# long as the argument, so slice the mtu back out of the front.
|
|
57
|
+
ifr = ifr.ljust(_IFREQ_SIZE, b"\x00")
|
|
58
|
+
res = fcntl.ioctl(sock.fileno(), _SIOCGIFMTU, ifr)
|
|
59
|
+
return struct.unpack("16sH", res[:18])[1]
|
|
60
|
+
except OSError:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CanMode(IntEnum):
|
|
65
|
+
"""CAN interface mode."""
|
|
66
|
+
CAN = 0
|
|
67
|
+
CANFD = 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CanFrame:
|
|
72
|
+
"""A single CAN frame."""
|
|
73
|
+
can_id: int
|
|
74
|
+
data: bytes
|
|
75
|
+
is_extended: bool = False
|
|
76
|
+
is_fd: bool = False
|
|
77
|
+
timestamp: float = 0.0
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def dlc(self) -> int:
|
|
81
|
+
return len(self.data)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CanTransport:
|
|
85
|
+
"""SocketCAN transport — opens a CAN socket and provides send/recv.
|
|
86
|
+
|
|
87
|
+
Usage::
|
|
88
|
+
|
|
89
|
+
transport = CanTransport("can0")
|
|
90
|
+
transport.open()
|
|
91
|
+
transport.send(0x108, bytes([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC]))
|
|
92
|
+
frame = transport.recv(timeout_s=0.1)
|
|
93
|
+
transport.close()
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, channel: str, mode: CanMode = CanMode.CAN):
|
|
97
|
+
self._channel = channel
|
|
98
|
+
self._mode = mode
|
|
99
|
+
self._sock: Optional[socket.socket] = None
|
|
100
|
+
self._is_open = False
|
|
101
|
+
# Set of CAN IDs (SFF) we want the kernel to deliver. Empty = accept
|
|
102
|
+
# all (no hardware filter). Populated via set_id_filter() once the
|
|
103
|
+
# motor's mst_id is known, so that on a shared bus (e.g. a full arm
|
|
104
|
+
# chattering on other IDs) foreign frames don't flood the RX buffer
|
|
105
|
+
# and starve/drop the motor's status frames.
|
|
106
|
+
self._accept_ids: set = set()
|
|
107
|
+
|
|
108
|
+
# ── properties ──────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def channel(self) -> str:
|
|
112
|
+
return self._channel
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def mode(self) -> CanMode:
|
|
116
|
+
return self._mode
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def is_open(self) -> bool:
|
|
120
|
+
return self._is_open
|
|
121
|
+
|
|
122
|
+
# ── open / close ────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def open(self) -> None:
|
|
125
|
+
"""Open and bind the CAN socket.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
OSError: if the interface does not exist or is down.
|
|
129
|
+
"""
|
|
130
|
+
if self._is_open:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
sock = socket.socket(socket.PF_CAN, socket.SOCK_RAW, CAN_RAW)
|
|
134
|
+
|
|
135
|
+
# Check interface MTU. Some USB-CAN adapters (gs_usb) and Linux
|
|
136
|
+
# kernel versions require CAN FD socket mode on FD-capable
|
|
137
|
+
# interfaces — classic CAN sockets may silently drop frames.
|
|
138
|
+
# Match the old damiao_socketcan behaviour: upgrade CAN→CANFD
|
|
139
|
+
# when the interface is FD, downgrade CANFD→CAN when not.
|
|
140
|
+
mtu = _iface_mtu(sock, self._channel)
|
|
141
|
+
if mtu is not None:
|
|
142
|
+
if mtu >= CANFD_MTU and self._mode == CanMode.CAN:
|
|
143
|
+
log.info(
|
|
144
|
+
"Interface %s is CAN FD (MTU=%d); switching to CANFD mode.",
|
|
145
|
+
self._channel, mtu,
|
|
146
|
+
)
|
|
147
|
+
self._mode = CanMode.CANFD
|
|
148
|
+
elif mtu < CANFD_MTU and self._mode == CanMode.CANFD:
|
|
149
|
+
log.warning(
|
|
150
|
+
"Interface %s MTU is %d (classic CAN) but CanMode.CANFD "
|
|
151
|
+
"requested; switching to classic CAN.",
|
|
152
|
+
self._channel, mtu,
|
|
153
|
+
)
|
|
154
|
+
self._mode = CanMode.CAN
|
|
155
|
+
|
|
156
|
+
if self._mode == CanMode.CANFD:
|
|
157
|
+
try:
|
|
158
|
+
sock.setsockopt(SOL_CAN_RAW, CAN_RAW_FD_FRAMES, 1)
|
|
159
|
+
except OSError:
|
|
160
|
+
log.warning("Failed to enable CAN FD on %s; falling back to classic CAN.", self._channel)
|
|
161
|
+
self._mode = CanMode.CAN
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
socket.if_nametoindex(self._channel) # verify interface exists
|
|
165
|
+
except OSError as e:
|
|
166
|
+
sock.close()
|
|
167
|
+
raise OSError(f"CAN interface '{self._channel}' not found: {e}")
|
|
168
|
+
|
|
169
|
+
sock.bind((self._channel,))
|
|
170
|
+
sock.setblocking(False)
|
|
171
|
+
|
|
172
|
+
self._sock = sock
|
|
173
|
+
self._is_open = True
|
|
174
|
+
# Re-apply any ID filter requested before the socket existed.
|
|
175
|
+
if self._accept_ids:
|
|
176
|
+
self._apply_id_filter()
|
|
177
|
+
log.info("CAN transport opened on %s (mode=%s)", self._channel, self._mode.name)
|
|
178
|
+
|
|
179
|
+
# ── receive filtering ─────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
def set_id_filter(self, can_ids) -> None:
|
|
182
|
+
"""Restrict which CAN IDs the kernel delivers to this socket.
|
|
183
|
+
|
|
184
|
+
On a shared bus (e.g. a full arm), other devices chatter on their own
|
|
185
|
+
IDs at high rate. Without a hardware filter every one of those frames
|
|
186
|
+
lands in this socket's RX buffer; a caller that reads one frame per
|
|
187
|
+
control cycle cannot keep up, the buffer saturates, and the motor's
|
|
188
|
+
own status frames are delayed or dropped — making read-back appear
|
|
189
|
+
"frozen" while the motor is actually moving.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
can_ids: Iterable of SFF CAN IDs to accept. Empty/None clears the
|
|
193
|
+
filter (accept all frames again).
|
|
194
|
+
"""
|
|
195
|
+
self._accept_ids = {int(i) & CAN_SFF_MASK for i in (can_ids or [])}
|
|
196
|
+
if self._is_open:
|
|
197
|
+
self._apply_id_filter()
|
|
198
|
+
|
|
199
|
+
def add_id_filter(self, can_id: int) -> None:
|
|
200
|
+
"""Add a single CAN ID to the accept set (see :meth:`set_id_filter`)."""
|
|
201
|
+
self.set_id_filter(self._accept_ids | {int(can_id) & CAN_SFF_MASK})
|
|
202
|
+
|
|
203
|
+
def _apply_id_filter(self) -> None:
|
|
204
|
+
"""Push the current accept-id set to the socket via CAN_RAW_FILTER.
|
|
205
|
+
|
|
206
|
+
An empty set installs a zero-length filter list, which SocketCAN
|
|
207
|
+
interprets as "receive nothing" — so instead we remove the filter by
|
|
208
|
+
installing a single match-all entry (id=0, mask=0).
|
|
209
|
+
"""
|
|
210
|
+
if self._sock is None:
|
|
211
|
+
return
|
|
212
|
+
if self._accept_ids:
|
|
213
|
+
fdata = b"".join(
|
|
214
|
+
struct.pack(_CAN_FILTER_FMT, cid, CAN_SFF_MASK)
|
|
215
|
+
for cid in sorted(self._accept_ids)
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
fdata = struct.pack(_CAN_FILTER_FMT, 0, 0) # match-all
|
|
219
|
+
try:
|
|
220
|
+
self._sock.setsockopt(SOL_CAN_RAW, CAN_RAW_FILTER, fdata)
|
|
221
|
+
log.info("CAN RX filter set on %s: %s", self._channel,
|
|
222
|
+
[hex(i) for i in sorted(self._accept_ids)] or "all")
|
|
223
|
+
except OSError as e:
|
|
224
|
+
log.warning("Failed to set CAN RX filter on %s: %s", self._channel, e)
|
|
225
|
+
|
|
226
|
+
def close(self) -> None:
|
|
227
|
+
"""Close the CAN socket."""
|
|
228
|
+
if not self._is_open or self._sock is None:
|
|
229
|
+
return
|
|
230
|
+
try:
|
|
231
|
+
self._sock.close()
|
|
232
|
+
except OSError:
|
|
233
|
+
pass
|
|
234
|
+
self._sock = None
|
|
235
|
+
self._is_open = False
|
|
236
|
+
|
|
237
|
+
# ── send ────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
def send(self, can_id: int, data: bytes, timeout: float = 0.1) -> None:
|
|
240
|
+
"""Send a CAN frame.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
can_id: 11-bit or 29-bit CAN ID.
|
|
244
|
+
data: Payload (0–8 bytes classic, 0–64 bytes FD).
|
|
245
|
+
timeout: Max wait time if send buffer is full (seconds).
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
TimeoutError: the send buffer stayed full for ``timeout`` seconds and
|
|
249
|
+
the frame could not be sent. ``TimeoutError`` is an ``OSError``.
|
|
250
|
+
The frame is *not* dropped silently: a caller that cannot send
|
|
251
|
+
must find out, or it reports motion that never happened.
|
|
252
|
+
"""
|
|
253
|
+
if not self._is_open or self._sock is None:
|
|
254
|
+
raise RuntimeError("Transport not open")
|
|
255
|
+
|
|
256
|
+
if self._mode == CanMode.CANFD:
|
|
257
|
+
flags = 0
|
|
258
|
+
frame = struct.pack(CANFD_FRAME_FMT,
|
|
259
|
+
can_id & CAN_EFF_MASK,
|
|
260
|
+
len(data), flags, 0, 0,
|
|
261
|
+
data.ljust(64, b'\x00'))
|
|
262
|
+
else:
|
|
263
|
+
if len(data) > 8:
|
|
264
|
+
raise ValueError(f"Classic CAN frames max 8 bytes, got {len(data)}")
|
|
265
|
+
frame = struct.pack(CAN_FRAME_FMT,
|
|
266
|
+
can_id & CAN_SFF_MASK,
|
|
267
|
+
len(data),
|
|
268
|
+
data.ljust(8, b'\x00'))
|
|
269
|
+
|
|
270
|
+
# Retry on BlockingIOError (send buffer full under high load).
|
|
271
|
+
deadline = time.monotonic() + timeout
|
|
272
|
+
while True:
|
|
273
|
+
try:
|
|
274
|
+
self._sock.send(frame)
|
|
275
|
+
return
|
|
276
|
+
except BlockingIOError:
|
|
277
|
+
remaining = deadline - time.monotonic()
|
|
278
|
+
if remaining <= 0:
|
|
279
|
+
# 丢帧改报错:缓冲满意味着这一帧**没发出去**,而调用方拿到
|
|
280
|
+
# 一个 None 就会当成发出去了(实测里正是这条链让夹爪
|
|
281
|
+
# 「位移 0.000 mm 却返回 True」)。docstring 一直承诺的是
|
|
282
|
+
# OSError,这里只是把实现改成与它一致。
|
|
283
|
+
log.warning(
|
|
284
|
+
"CAN send timeout on %s (buffer full >%.1fs), "
|
|
285
|
+
"frame to 0x%03X not sent",
|
|
286
|
+
self._channel, timeout, can_id & CAN_SFF_MASK,
|
|
287
|
+
)
|
|
288
|
+
raise TimeoutError(
|
|
289
|
+
f"CAN 发送超时:{self._channel} 的发送缓冲在 {timeout:.1f} s "
|
|
290
|
+
f"内一直满,帧 0x{can_id & CAN_SFF_MASK:03X} 没发出去。")
|
|
291
|
+
select.select([], [self._sock], [], remaining)
|
|
292
|
+
|
|
293
|
+
def send_multi(self, can_id: int, data: bytes, count: int,
|
|
294
|
+
interval_s: float = 0.002) -> None:
|
|
295
|
+
"""Send the same frame multiple times (for reliability of config commands)."""
|
|
296
|
+
for _ in range(count):
|
|
297
|
+
self.send(can_id, data)
|
|
298
|
+
if count > 1 and interval_s > 0:
|
|
299
|
+
time.sleep(interval_s)
|
|
300
|
+
|
|
301
|
+
# ── recv ────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
def recv(self, timeout_s: float = 0.0) -> Optional[CanFrame]:
|
|
304
|
+
"""Receive a single CAN frame.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
timeout_s: Max wait time in seconds. 0 = non-blocking poll.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
CanFrame if a frame was received, None if timeout.
|
|
311
|
+
"""
|
|
312
|
+
if not self._is_open or self._sock is None:
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
ready, _, _ = select.select([self._sock], [], [], timeout_s)
|
|
317
|
+
if not ready:
|
|
318
|
+
return None
|
|
319
|
+
except (OSError, ValueError):
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
raw = self._sock.recv(CANFD_FRAME_SIZE)
|
|
324
|
+
except OSError:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
ts = time.monotonic()
|
|
328
|
+
|
|
329
|
+
if self._mode == CanMode.CANFD:
|
|
330
|
+
can_id, dlc, flags, _len8, _res, data = struct.unpack(
|
|
331
|
+
CANFD_FRAME_FMT, raw)
|
|
332
|
+
is_fd = True
|
|
333
|
+
is_extended = bool(can_id & CAN_EFF_MASK)
|
|
334
|
+
data = data[:dlc]
|
|
335
|
+
else:
|
|
336
|
+
can_id, dlc, data = struct.unpack(CAN_FRAME_FMT, raw)
|
|
337
|
+
is_fd = False
|
|
338
|
+
is_extended = bool(can_id & CAN_EFF_MASK)
|
|
339
|
+
data = data[:dlc]
|
|
340
|
+
|
|
341
|
+
return CanFrame(
|
|
342
|
+
can_id=can_id & CAN_SFF_MASK,
|
|
343
|
+
data=data,
|
|
344
|
+
is_extended=is_extended,
|
|
345
|
+
is_fd=is_fd,
|
|
346
|
+
timestamp=ts,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def drain(self) -> None:
|
|
350
|
+
"""Read and discard all pending frames from the receive buffer."""
|
|
351
|
+
while self.recv(timeout_s=0.0) is not None:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
# ── context manager ─────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
def __enter__(self) -> "CanTransport":
|
|
357
|
+
self.open()
|
|
358
|
+
return self
|
|
359
|
+
|
|
360
|
+
def __exit__(self, *args) -> None:
|
|
361
|
+
self.close()
|
|
362
|
+
|
|
363
|
+
def __repr__(self) -> str:
|
|
364
|
+
state = "open" if self._is_open else "closed"
|
|
365
|
+
return f"CanTransport({self._channel!r}, {self._mode.name}, {state})"
|
litegrip/constants.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""LiteGrip SDK — constants and protocol definitions.
|
|
2
|
+
|
|
3
|
+
Self-contained. Uses litegrip.can for DM motor types; does NOT import
|
|
4
|
+
damiao_socketcan or any arm library.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
from .can.motor import MotorType
|
|
10
|
+
from .can.protocol import ControlMode as _ControlMode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
14
|
+
# Re-export DM protocol types for convenience
|
|
15
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
DM_Motor_Type = MotorType
|
|
18
|
+
Control_Mode = _ControlMode
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
22
|
+
# LiteGrip gripper parameters
|
|
23
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
24
|
+
|
|
25
|
+
class GripperParams:
|
|
26
|
+
"""LiteGrip default parameters."""
|
|
27
|
+
|
|
28
|
+
CAN_ID: Final = 0x08
|
|
29
|
+
MST_ID: Final = 0x18
|
|
30
|
+
MOTOR_TYPE: Final = MotorType.DM4310
|
|
31
|
+
CONTROL_MODE: Final = Control_Mode.MIT_MODE
|
|
32
|
+
|
|
33
|
+
# Position limits (rad) — nominal placeholders only. The real limits come
|
|
34
|
+
# from the calibration; do not treat these as a direction convention, since
|
|
35
|
+
# a reverse-mounted motor has them the other way round.
|
|
36
|
+
POS_CLOSED_RAD: Final = 1.14
|
|
37
|
+
POS_OPEN_RAD: Final = 0.0
|
|
38
|
+
|
|
39
|
+
# MIT quantization limits (DM4310)
|
|
40
|
+
Q_MAX: Final = 12.5 # rad
|
|
41
|
+
DQ_MAX: Final = 30.0 # rad/s
|
|
42
|
+
TAU_MAX: Final = 10.0 # Nm
|
|
43
|
+
|
|
44
|
+
# Default control gains
|
|
45
|
+
DEFAULT_KP: Final = 100.0
|
|
46
|
+
DEFAULT_KD: Final = 2.0
|
|
47
|
+
|
|
48
|
+
# Fault recovery
|
|
49
|
+
FAULT_CLEAR_RETRIES: Final = 5
|
|
50
|
+
FAULT_CLEAR_DELAY_S: Final = 0.02
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
54
|
+
# Gripper geometry and unit conversion
|
|
55
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
class GripperGeometry:
|
|
58
|
+
"""The LiteGrip two-finger gripper's nominal geometry.
|
|
59
|
+
|
|
60
|
+
These are the numbers every uncalibrated default in this package comes
|
|
61
|
+
from. They are the measured geometry of the reference unit, not a
|
|
62
|
+
tolerance to be picked per gripper: an ``85 mm`` jaw travel is what the
|
|
63
|
+
calipers read, and the ``1 mm`` inset is how far the probe presses into
|
|
64
|
+
the open stop while looking for it.
|
|
65
|
+
|
|
66
|
+
**Keep the two apart; do not fold the inset into the travel.** The
|
|
67
|
+
calibration probe measures the span *between the two stops*, which is
|
|
68
|
+
``SPAN_MM`` (86 mm) — the jaws at rest on the closed stop, and pressed
|
|
69
|
+
~1 mm past where the jaws have run out of travel on the open one. The
|
|
70
|
+
millimetres-per-radian scale is that span over the measured travel in
|
|
71
|
+
radians (``rad_to_mm``); deriving it from the 85 mm jaw travel instead
|
|
72
|
+
would leave every mm-based move 1.2% short — 1 mm lost per full stroke.
|
|
73
|
+
The factory calibration's own numbers show the pair: 1.170748 rad of
|
|
74
|
+
travel is 86 mm at ``73.45731105242119`` mm/rad, and 85 mm at
|
|
75
|
+
``72.604``.
|
|
76
|
+
|
|
77
|
+
``work_stroke_mm`` is measured from the closed zero like
|
|
78
|
+
:attr:`JAW_TRAVEL_MM`, so a full-stroke ``work_stroke_mm`` is 85 mm, not
|
|
79
|
+
``SPAN_MM``.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
#: Jaw travel measured with calipers, from the closed stop (mm).
|
|
83
|
+
JAW_TRAVEL_MM: Final = 85.0
|
|
84
|
+
|
|
85
|
+
#: How far the calibration probe presses into the open stop (mm).
|
|
86
|
+
STOP_INSET_MM: Final = 1.0
|
|
87
|
+
|
|
88
|
+
#: What the recorded extremes span (mm) — the numerator of the scale.
|
|
89
|
+
SPAN_MM: Final = JAW_TRAVEL_MM + STOP_INSET_MM
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class UnitConversion:
|
|
93
|
+
"""Unit conversion coefficients.
|
|
94
|
+
|
|
95
|
+
Nominal values for this gripper — see :class:`GripperGeometry`. They are
|
|
96
|
+
the span over the uncalibrated placeholder travel
|
|
97
|
+
(:attr:`~litegrip.models.GripperConfig.pos_closed_rad`, ``1.14 rad``), and
|
|
98
|
+
every motion is refused until a real calibration replaces them. Run
|
|
99
|
+
``calibrate()`` to get accurate per-unit values.
|
|
100
|
+
"""
|
|
101
|
+
RAD_TO_MM: Final = GripperGeometry.SPAN_MM / 1.14 # ≈ 75.44 mm/rad
|
|
102
|
+
MM_TO_RAD: Final = 1.14 / GripperGeometry.SPAN_MM # ≈ 0.01326 rad/mm
|
|
103
|
+
NM_TO_N: Final = 10.0 # approximate N per Nm
|
|
104
|
+
N_TO_NM: Final = 0.1 # approximate Nm per N
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
108
|
+
# Error codes
|
|
109
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
class ErrorCode:
|
|
112
|
+
"""Damiao motor error codes (extracted from status frame data[0] >> 4)."""
|
|
113
|
+
DISABLED: Final = 0
|
|
114
|
+
ENABLED: Final = 1
|
|
115
|
+
UV_FAULT: Final = 0x9
|
|
116
|
+
OC_FAULT: Final = 0xA
|
|
117
|
+
MOS_OT: Final = 0xB
|
|
118
|
+
COIL_OT: Final = 0xC
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
ERROR_DESCRIPTIONS = {
|
|
122
|
+
0x0: "已失能",
|
|
123
|
+
0x1: "已使能",
|
|
124
|
+
0x9: "欠压故障 (UV)",
|
|
125
|
+
0xA: "过流故障 (OC)",
|
|
126
|
+
0xB: "MOS 过温故障",
|
|
127
|
+
0xC: "线圈过温故障",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def describe_error(code: int) -> str:
|
|
132
|
+
"""Return a human-readable description for a motor error code."""
|
|
133
|
+
return ERROR_DESCRIPTIONS.get(code, f"未知错误 (0x{code:X})")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
137
|
+
# Default configuration
|
|
138
|
+
# ═════════════════════════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
class DefaultParams:
|
|
141
|
+
"""System defaults."""
|
|
142
|
+
CAN_CHANNEL: Final = "can0"
|
|
143
|
+
CAN_BITRATE: Final = 1_000_000 # 1 Mbps classic CAN
|
|
144
|
+
CANFD_MODE: Final = False
|
|
145
|
+
TIMEOUT_S: Final = 0.1
|
|
146
|
+
INIT_TIMEOUT_S: Final = 2.0
|
litegrip/exceptions.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""LiteGrip SDK - 异常定义"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LiteGripError(Exception):
|
|
7
|
+
"""LiteGrip 基础异常"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, error_code: int | None = None):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.error_code = error_code
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
if self.error_code is not None:
|
|
16
|
+
return f"{self.message} [0x{self.error_code:04X}]"
|
|
17
|
+
return self.message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CommError(LiteGripError):
|
|
21
|
+
"""通信错误(CAN 总线读写失败)"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConnectError(LiteGripError):
|
|
26
|
+
"""连接错误(CAN 接口不可用、电机无应答等)"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CommandError(LiteGripError):
|
|
31
|
+
"""指令错误(电机拒绝执行或参数越界)"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CANTimeoutError(LiteGripError):
|
|
36
|
+
"""超时错误(CAN 总线无应答)"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HardwareError(LiteGripError):
|
|
41
|
+
"""硬件故障(欠压/过流/过温等达妙电机故障码)"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NotInitializedError(LiteGripError):
|
|
46
|
+
"""未初始化错误(未连接/未使能时调用了需要使能的操作)"""
|
|
47
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"channel": "can0",
|
|
3
|
+
"can_id": 8,
|
|
4
|
+
"canfd_mode": false,
|
|
5
|
+
"calibrated": true,
|
|
6
|
+
"zero_position_rad": 0.029564,
|
|
7
|
+
"max_position_rad": -1.141184,
|
|
8
|
+
"travel_range_rad": 1.170748,
|
|
9
|
+
"rad_to_mm": 73.45731105242119,
|
|
10
|
+
"work_stroke_mm": 80.0,
|
|
11
|
+
"motor_type": "DM4310",
|
|
12
|
+
"kp": 100.0,
|
|
13
|
+
"kd": 2.0,
|
|
14
|
+
"grasp_torque_threshold": 0.5,
|
|
15
|
+
"mst_id": 24
|
|
16
|
+
}
|