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/models.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""LiteGrip SDK — data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import IntEnum, IntFlag
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .constants import GripperGeometry, UnitConversion
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GripperMode(IntEnum):
|
|
13
|
+
"""Gripper control mode."""
|
|
14
|
+
MIT = 0
|
|
15
|
+
POSITION = 1
|
|
16
|
+
VELOCITY = 2
|
|
17
|
+
FORCE = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GripperStatus(IntFlag):
|
|
21
|
+
"""Gripper status flags (bitmask)."""
|
|
22
|
+
NONE = 0x00
|
|
23
|
+
ENABLED = 0x01
|
|
24
|
+
MOVING = 0x02
|
|
25
|
+
AT_TARGET = 0x04
|
|
26
|
+
GRASPED = 0x08
|
|
27
|
+
ERROR = 0x10
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
#: Age past which a :class:`GripperState` counts as stale, in seconds. The same
|
|
31
|
+
#: threshold the C++ SDK uses, so a guard reads the same on either side.
|
|
32
|
+
STALE_AFTER_S = 0.5
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GripperState:
|
|
37
|
+
"""Gripper state snapshot, and how fresh it is.
|
|
38
|
+
|
|
39
|
+
The readings are a cache, updated only by a status frame. Whether such a
|
|
40
|
+
frame arrived is what :attr:`fresh`, :attr:`data_age_s` and
|
|
41
|
+
:meth:`is_stale` report — a snapshot from a bus that said nothing is not a
|
|
42
|
+
measurement, and it must not look like one. Check them before trusting the
|
|
43
|
+
numbers; :meth:`LiteGrip.refresh_status` elicits a frame when the bus is
|
|
44
|
+
idle.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
position_rad: float = 0.0
|
|
48
|
+
velocity_rad_s: float = 0.0
|
|
49
|
+
torque_nm: float = 0.0
|
|
50
|
+
temperature_mos: int = 0
|
|
51
|
+
temperature_coil: int = 0
|
|
52
|
+
error_code: int = 0
|
|
53
|
+
#: Wall-clock time the frame behind this snapshot arrived — not the time of
|
|
54
|
+
#: the call that returned it. ``0.0`` means no frame has ever arrived, so
|
|
55
|
+
#: every reading above is still a constructor default.
|
|
56
|
+
timestamp: float = 0.0
|
|
57
|
+
|
|
58
|
+
# Convenience — computed from raw values with unit conversion
|
|
59
|
+
position_mm: float = 0.0
|
|
60
|
+
force_n: float = 0.0
|
|
61
|
+
|
|
62
|
+
#: Whether a status frame answered *this* read. False means the readings
|
|
63
|
+
#: are the ones from the previous frame, however old that is.
|
|
64
|
+
fresh: bool = False
|
|
65
|
+
#: Seconds since the frame behind this snapshot arrived, or ``None`` when
|
|
66
|
+
#: there is no such frame at all.
|
|
67
|
+
data_age_s: Optional[float] = None
|
|
68
|
+
|
|
69
|
+
def is_stale(self, stale_after_s: float = STALE_AFTER_S) -> bool:
|
|
70
|
+
"""True when the newest frame is older than *stale_after_s*, or absent.
|
|
71
|
+
|
|
72
|
+
This is about the snapshot's age, not about the read that produced it:
|
|
73
|
+
a read no frame answered has ``fresh=False`` while the snapshot may
|
|
74
|
+
still be younger than the threshold. Ask both questions when the
|
|
75
|
+
difference matters — "how old is this" and "did anyone answer just now".
|
|
76
|
+
|
|
77
|
+
``stale_after_s`` defaults to :data:`STALE_AFTER_S`, the same threshold
|
|
78
|
+
the C++ SDK uses.
|
|
79
|
+
"""
|
|
80
|
+
return self.data_age_s is None or self.data_age_s > stale_after_s
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_enabled(self) -> bool:
|
|
84
|
+
"""True if the motor is enabled (error_code == 1)."""
|
|
85
|
+
return self.error_code == 1
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_error(self) -> bool:
|
|
89
|
+
"""True if a fault is active (error_code ∉ {0, 1})."""
|
|
90
|
+
return self.error_code not in (0, 1)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def is_moving(self) -> bool:
|
|
94
|
+
"""True if velocity exceeds a small threshold."""
|
|
95
|
+
return abs(self.velocity_rad_s) > 0.01
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def aperture_mm(self) -> float:
|
|
99
|
+
"""Opening distance in mm (single-side displacement).
|
|
100
|
+
|
|
101
|
+
For total jaw separation multiply by 2.
|
|
102
|
+
"""
|
|
103
|
+
return self.position_mm
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class GripperConfig:
|
|
108
|
+
"""Gripper configuration — tune these for your hardware."""
|
|
109
|
+
|
|
110
|
+
# CAN
|
|
111
|
+
can_channel: str = "can0"
|
|
112
|
+
can_id: int = 0x08
|
|
113
|
+
mst_id: Optional[int] = None # None = auto-detect
|
|
114
|
+
canfd_mode: bool = False
|
|
115
|
+
|
|
116
|
+
# Control gains (MIT mode)
|
|
117
|
+
kp: float = 100.0 # position stiffness [0, 500]
|
|
118
|
+
kd: float = 2.0 # velocity damping [0, 5]
|
|
119
|
+
|
|
120
|
+
# Position limits (rad) — updated by calibrate() / load_calibration().
|
|
121
|
+
# Which of the two is numerically larger is NOT assumed: a gripper whose
|
|
122
|
+
# motor is mounted the other way round (a "reverse mount") simply has
|
|
123
|
+
# pos_closed_rad < pos_open_rad. See :attr:`close_sign`.
|
|
124
|
+
pos_closed_rad: float = 1.14 # closed → 0 mm
|
|
125
|
+
pos_open_rad: float = 0.0 # open → full stroke
|
|
126
|
+
|
|
127
|
+
# True once the limits above come from a real calibration (or from a
|
|
128
|
+
# calibration template) rather than from the placeholder defaults. The
|
|
129
|
+
# motion actions refuse to run while this is False, because with the
|
|
130
|
+
# defaults still in place every direction is a guess.
|
|
131
|
+
calibrated: bool = False
|
|
132
|
+
|
|
133
|
+
# Jaw travel (mm) — the caliper measurement of your gripper's travel.
|
|
134
|
+
# :meth:`LiteGrip.calibrate` derives ``rad_to_mm`` from it plus the probe
|
|
135
|
+
# inset, because the recorded extremes span that inset more than the jaws
|
|
136
|
+
# actually open; see :class:`~litegrip.constants.GripperGeometry`.
|
|
137
|
+
max_stroke_mm: float = GripperGeometry.JAW_TRAVEL_MM
|
|
138
|
+
|
|
139
|
+
# Working stroke (mm) — the opening the motions are allowed to reach, from
|
|
140
|
+
# the closed zero. ``0`` (or anything ≥ the jaw travel) means "no limit":
|
|
141
|
+
# :meth:`actions.open` then presses onto the open mechanical stop as before.
|
|
142
|
+
# Set it below the jaw travel to keep a margin at the open end (e.g. 80 mm on
|
|
143
|
+
# an 85 mm travel leaves 5 mm), so a plain open no longer drives into the
|
|
144
|
+
# stop.
|
|
145
|
+
work_stroke_mm: float = 0.0
|
|
146
|
+
|
|
147
|
+
# Unit conversion — update after calibration
|
|
148
|
+
rad_to_mm: float = UnitConversion.RAD_TO_MM # rad → mm
|
|
149
|
+
nm_to_n: float = 10.0 # Nm → N (approximate)
|
|
150
|
+
|
|
151
|
+
# Grasp detection
|
|
152
|
+
grasp_torque_threshold: float = 0.5 # Nm
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def close_sign(self) -> float:
|
|
156
|
+
"""Which way the motor counts when the jaws close.
|
|
157
|
+
|
|
158
|
+
``+1.0`` when closing means increasing radians (the usual mounting),
|
|
159
|
+
``-1.0`` for a reverse mount. Derived from the ordering of the two
|
|
160
|
+
calibrated limits, so direction is data, not a separate switch — and
|
|
161
|
+
it is only meaningful once :attr:`calibrated` is True.
|
|
162
|
+
"""
|
|
163
|
+
return 1.0 if self.pos_closed_rad >= self.pos_open_rad else -1.0
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def mount(self) -> Optional[str]:
|
|
167
|
+
"""``"normal"`` / ``"reverse"`` — the name the limits spell out.
|
|
168
|
+
|
|
169
|
+
``None`` while :attr:`calibrated` is False: the placeholder defaults
|
|
170
|
+
also happen to order close above open, so reporting ``"normal"``
|
|
171
|
+
before a calibration has been loaded would be a claim, not a reading.
|
|
172
|
+
Handy for a caller that has to show the operator which way this unit
|
|
173
|
+
is mounted. Compare with the ``mount=`` argument of
|
|
174
|
+
:class:`~litegrip.LiteGrip`, which *declares* a mount by loading a
|
|
175
|
+
template rather than reading one back.
|
|
176
|
+
"""
|
|
177
|
+
if not self.calibrated:
|
|
178
|
+
return None
|
|
179
|
+
return "normal" if self.close_sign > 0 else "reverse"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class GripperInfo:
|
|
184
|
+
"""Static device information."""
|
|
185
|
+
|
|
186
|
+
model: str = "LiteGrip"
|
|
187
|
+
motor_type: str = "DM4310"
|
|
188
|
+
can_id: int = 0x08
|
|
189
|
+
mst_id: int = 0x18
|
|
190
|
+
firmware_version: str = ""
|
|
191
|
+
serial_number: str = ""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class CalibrationData:
|
|
196
|
+
"""Result of a gripper calibration run."""
|
|
197
|
+
|
|
198
|
+
zero_position: float = 1.14 # closed limit (rad)
|
|
199
|
+
max_position: float = 0.0 # open limit (rad)
|
|
200
|
+
travel_range: float = 1.14 # |max - zero| (rad)
|
|
201
|
+
rad_to_mm: float = UnitConversion.RAD_TO_MM # calibrated conversion
|
|
202
|
+
motor_type: str = "DM4310"
|
|
203
|
+
can_id: int = 0x08
|
|
204
|
+
mst_id: int = 0x18
|
|
205
|
+
calibration_time: str = ""
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def travel_mm(self) -> float:
|
|
209
|
+
"""Full stroke in mm."""
|
|
210
|
+
return self.travel_range * self.rad_to_mm
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""LiteGrip SDK — communication protocols.
|
|
2
|
+
|
|
3
|
+
The CAN bus layer (LiteGripCAN) is self-contained: it implements the
|
|
4
|
+
Damiao motor protocol directly over Linux SocketCAN with zero
|
|
5
|
+
dependencies on damiao_socketcan or any arm-specific library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .can_bus import LiteGripCAN
|
|
9
|
+
|
|
10
|
+
__all__ = ["LiteGripCAN"]
|