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,636 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litegrip-python
|
|
3
|
+
Version: 1.1.2
|
|
4
|
+
Summary: Python SDK for the LiteGrip lightweight robotic gripper series
|
|
5
|
+
Author: NEXFORM ROBOTICS
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/nexform-tech/litegrip-python
|
|
8
|
+
Project-URL: Issues, https://github.com/nexform-tech/litegrip-python/issues
|
|
9
|
+
Keywords: litegrip,litearm,gripper,robotics,socketcan,can,damiao,dm4310
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: zenoh
|
|
26
|
+
Requires-Dist: eclipse-zenoh>=1.0; extra == "zenoh"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# litegrip-python
|
|
30
|
+
|
|
31
|
+
[English](https://github.com/nexform-tech/litegrip-python/blob/main/README.md) | [简体中文](https://github.com/nexform-tech/litegrip-python/blob/main/README.zh-CN.md)
|
|
32
|
+
|
|
33
|
+
Python SDK for the **LiteGrip lightweight robotic gripper series**, and the primary public
|
|
34
|
+
interface to it. This is the reference: what each interface is, and how to use it.
|
|
35
|
+
|
|
36
|
+
You do not need to know CAN or the DM4310 protocol: every move, calibration and force control is
|
|
37
|
+
one method call, and each call verifies its own outcome before reporting success. Every signature
|
|
38
|
+
below is the real one, with its default.
|
|
39
|
+
|
|
40
|
+
## What you need
|
|
41
|
+
|
|
42
|
+
| | |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Motor | Damiao DM4310, CAN ID `0x08`, MIT mode, classic CAN at 1 Mbit/s |
|
|
45
|
+
| Host | Linux with SocketCAN — this is the only supported platform |
|
|
46
|
+
| Python | 3.8 or newer |
|
|
47
|
+
| Dependencies | none: standard library only |
|
|
48
|
+
| Optional | `litegrip-python[zenoh]` — the point-to-point teleoperation link |
|
|
49
|
+
|
|
50
|
+
## 1. Install
|
|
51
|
+
|
|
52
|
+
Install it from a checkout:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/nexform-tech/litegrip-python.git
|
|
56
|
+
cd litegrip-python
|
|
57
|
+
python3 -m pip install .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or skip the install and point `PYTHONPATH` at the source tree:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
PYTHONPATH=/path/to/litegrip-python/src python3 your_script.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 2. Bring up CAN
|
|
67
|
+
|
|
68
|
+
The gripper is a CAN device. Bring the interface up at 1 Mbit/s before any SDK call, and do it
|
|
69
|
+
again after every reboot:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
sudo ip link set can0 type can bitrate 1000000
|
|
73
|
+
sudo ip link set can0 up
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Both commands need root. If `can0` does not exist at all, the CAN adapter is not loaded yet. In a
|
|
77
|
+
product, run these two commands at boot from your init system, so the application never has to
|
|
78
|
+
start the interface itself.
|
|
79
|
+
|
|
80
|
+
## 3. Quick start
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from litegrip import LiteGrip
|
|
84
|
+
|
|
85
|
+
with LiteGrip("can0") as gripper: # connect() on entry, disconnect() on exit
|
|
86
|
+
gripper.load_calibration() # the shipped factory calibration — no arguments
|
|
87
|
+
gripper.enable() # retries until the status frame says err == 1
|
|
88
|
+
|
|
89
|
+
gripper.open() # 50 mm/s toward the open-side stop
|
|
90
|
+
gripper.close() # 50 mm/s toward the closed-side stop
|
|
91
|
+
result = gripper.grasp(force_n=20.0, hold_s=3.0)
|
|
92
|
+
print(result.ok, result.stalled, result.cycles)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Two things to know before going further:
|
|
96
|
+
|
|
97
|
+
- **The factory calibration is the default.** A no-argument `load_calibration()` reads the
|
|
98
|
+
calibration file shipped inside the SDK, which describes the shipped units (normal mount). You
|
|
99
|
+
name a different file only when you have one — see [Calibration](#calibration).
|
|
100
|
+
- **Nothing moves before a calibration is loaded.** Every motion method raises `CommandError` on
|
|
101
|
+
an uncalibrated config, because the placeholder limits describe no real travel.
|
|
102
|
+
|
|
103
|
+
`with` calls `disconnect()` on exit, which disables the motor by default. Pass
|
|
104
|
+
`LiteGrip(..., disable_on_disconnect=False)` to leave it energised after the block.
|
|
105
|
+
|
|
106
|
+
## Interfaces
|
|
107
|
+
|
|
108
|
+
### LiteGrip
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
LiteGrip(channel="can0", can_id=0x08, mst_id=None, canfd_mode=None,
|
|
112
|
+
motor_type=MotorType.DM4310, config=None, motion_config=None,
|
|
113
|
+
disable_on_disconnect=True, mount=None)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `channel` — CAN interface name. Every LiteGrip shares CAN ID `0x08`, so the channel is the only
|
|
117
|
+
identity key when two sit on one machine.
|
|
118
|
+
- `mount` — `"normal"` or `"reverse"`, loading that template's limits. The shipped units are
|
|
119
|
+
normal mount, which is what the factory calibration describes, so leave it unset unless a unit
|
|
120
|
+
is reverse-mounted (its jaws open when you `close()`).
|
|
121
|
+
- `mst_id` — master ID; `None` auto-detects it.
|
|
122
|
+
- `config` / `motion_config` — replace the two dataclasses up front instead of editing them after
|
|
123
|
+
construction.
|
|
124
|
+
- `disable_on_disconnect` — `disconnect()` disables the motor (crash safety). Set `False` only
|
|
125
|
+
when something else keeps the gripper under control.
|
|
126
|
+
|
|
127
|
+
| Attribute | What it is |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `channel`, `can_id`, `mst_id` | The identity this instance drives |
|
|
130
|
+
| `config` | `GripperConfig` — limits, scale, gains |
|
|
131
|
+
| `motion_config` | `MotionConfig` — every tunable; settable as a whole or field by field |
|
|
132
|
+
| `actions` | `GripperActions` — the engine the high-level methods forward to |
|
|
133
|
+
| `mount` | `"normal"`, `"reverse"`, or `None` until calibrated (derived from the limits) |
|
|
134
|
+
| `is_connected`, `is_enabled` | Session state |
|
|
135
|
+
| `last_send_error` | Why the last `send_mit_frame` did not send, or `None`. The exception the send reported — an `OSError` from the transport, or the `NotInitializedError` naming the step that was skipped. |
|
|
136
|
+
| `session` | The CAN session, for advanced use |
|
|
137
|
+
|
|
138
|
+
### Connection and state
|
|
139
|
+
|
|
140
|
+
| Method | What it does |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `connect()` → `bool` | Opens the CAN session. The `with` block does this for you. |
|
|
143
|
+
| `disconnect()` → `None` | Closes it, disabling the motor unless `disable_on_disconnect=False`. |
|
|
144
|
+
| `poll(timeout_s=0.0)` → `bool` | Reads one frame; `True` if one arrived. |
|
|
145
|
+
| `get_state(wait=True)` → `GripperState` | Position, velocity, torque, temperature, error bits, plus how fresh they are. `wait=True` waits up to 50 ms for the frame that updates them — it does not guarantee one. |
|
|
146
|
+
| `refresh_status()` → `bool` | Asks the motor for a status frame (0xCC) without moving the jaws. Use it before `get_state()` when the gripper has been idle. |
|
|
147
|
+
| `get_position()` / `get_position_rad()` → `float` | Measured opening, in mm / rad. |
|
|
148
|
+
| `get_force()` → `float` | `get_torque()` × 10, in N. |
|
|
149
|
+
| `get_torque()` → `float` | Measured torque, Nm. |
|
|
150
|
+
| `get_temperature()` → `tuple[int, int]` | `(MOS °C, coil °C)`. |
|
|
151
|
+
| `get_error()` → `int` | The status code the motor reported: `1` enabled, `0` disabled, `0x9`–`0xC` for faults. `describe_error(code)` names one. |
|
|
152
|
+
| `get_info()` → `GripperInfo` | Static device metadata. |
|
|
153
|
+
| `is_moving()` / `is_grasped()` → `bool` | Motion and grip predicates. |
|
|
154
|
+
| `wait_for_ready(timeout=5.0)` → `bool` | Blocks until the motor accepts commands. |
|
|
155
|
+
|
|
156
|
+
The readings `get_state()` returns are a cache, and only a status frame updates it. A DM motor
|
|
157
|
+
answers the control frames it receives, so a gripper that is being driven reports on its own; one
|
|
158
|
+
holding a pose, or disabled, sends nothing, and then `wait=True` waits out its 50 ms and hands back
|
|
159
|
+
the previous frame. The snapshot says which happened:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
state = gripper.get_state()
|
|
163
|
+
if not state.fresh:
|
|
164
|
+
gripper.refresh_status() # 0xCC: ask for a frame; the jaws do not move
|
|
165
|
+
state = gripper.get_state()
|
|
166
|
+
|
|
167
|
+
if state.is_stale(): # older than 0.5 s, or never had a frame
|
|
168
|
+
raise RuntimeError(f"no status frame for {state.data_age_s} s")
|
|
169
|
+
print(state.position_mm, state.force_n, state.timestamp)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- `fresh` — a frame answered *this* read. False means the numbers are the previous frame's, however
|
|
173
|
+
old that is.
|
|
174
|
+
- `data_age_s` — seconds since the frame behind the snapshot arrived, or `None` when none ever has.
|
|
175
|
+
- `timestamp` — when that frame arrived, not when you asked. `0.0` means there is no frame at all and
|
|
176
|
+
every reading is a constructor default. **Do not** time a loop with it; it is for a human reading a
|
|
177
|
+
log, and the age arithmetic is what `data_age_s` is for.
|
|
178
|
+
- `is_stale(stale_after_s=STALE_AFTER_S)` — age past the threshold (`STALE_AFTER_S` is 0.5 s). It is
|
|
179
|
+
about the snapshot, not the read: a snapshot 20 ms old can still be `fresh=False`.
|
|
180
|
+
|
|
181
|
+
`fresh` and `is_stale()` answer different questions, and neither substitutes for the other — the wait
|
|
182
|
+
window is 50 ms while the staleness threshold is 0.5 s, so a read no frame answered can still look
|
|
183
|
+
young. Ask both when the difference matters.
|
|
184
|
+
|
|
185
|
+
### Enable and faults
|
|
186
|
+
|
|
187
|
+
| Method | What it does |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `enable(retries=None)` → `EnableResult` | Enables, then re-reads the status frame until it reports `err == 1`. `retries=None` uses `MotionConfig.enable_retries` (3). |
|
|
190
|
+
| `disable()` → `bool` | Zero torque; the jaws are back-drivable by hand. |
|
|
191
|
+
| `clear_fault()` → `bool` | Clears a latched motor fault. |
|
|
192
|
+
| `stop()` → `None` | Sends one zero-torque frame. |
|
|
193
|
+
|
|
194
|
+
### Motion
|
|
195
|
+
|
|
196
|
+
| Method | What it does |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| `open(speed_mm_s=None, *, progress=None)` → `MoveResult` | Ramps toward the calibrated open-side stop. With `GripperConfig.work_stroke_mm` set it stops at that opening instead — the shipped calibration sets `80.0` against the unit's 85 mm of travel, so a plain `open()` ends 5 mm short of the open end. |
|
|
199
|
+
| `close(speed_mm_s=None, *, progress=None)` → `MoveResult` | The same, toward the closed side. `work_stroke_mm` is an open-side number and does not change where a close ends. |
|
|
200
|
+
| `home()` → `MoveResult` | Ramps to the calibrated closed position. |
|
|
201
|
+
| `grasp(force_n=None, hold_s=0.0, *, progress=None)` → `GraspResult` | Closes onto the object — or onto the closed-side calibrated stop if there is nothing there — then ramps to and holds `force_n` N. The closing leg carries `force_n` as its torque budget, so meeting the object presses no harder than the setpoint. The hold takes over only once the jaw is stopped: it is `stalled=True` (something is between the jaws) or `reached=True` (the jaws closed empty). `hold_s=0` holds until a fault, the runaway watch, or Ctrl+C. `force_n=None` uses `MotionConfig.force_n` (20 N). |
|
|
202
|
+
| `goto(position_mm, kp=None, kd=None, duration=0.5)` → `MoveResult` | Ramp to a position in millimetres. |
|
|
203
|
+
| `goto_rad(position_rad, kp=None, kd=None, dq_target=0.0, tau_feedforward=0.0, duration=0.5)` → `MoveResult` | The same in radians. `dq_target` and `tau_feedforward` must be 0: the ramp derives the velocity feed-forward from its own schedule, so both are refused with `CommandError` rather than ignored. |
|
|
204
|
+
| `move_to(target_rad, kp=None, kd=None, tau_feedforward=0.0, duration=1.0)` → `MoveResult` | The same as `goto_rad`, with a longer default `duration`. |
|
|
205
|
+
| `move_at_speed(target_mm, speed_mm_s=30.0, kp=None, kd=None)` → `MoveResult` | Ramp to a millimetre target at `speed_mm_s`. |
|
|
206
|
+
| `move_at_speed_rad(target_rad, speed_rad_s=0.5, kp=None, kd=None)` → `MoveResult` | The same in radians. |
|
|
207
|
+
|
|
208
|
+
`kp` and `kd` fall back to `GripperConfig.kp` (`100.0`) and `.kd` (`2.0`) on every call that takes
|
|
209
|
+
them. Every positioning call — the `goto` family and the `move_at_speed` family alike — runs one
|
|
210
|
+
ramp of one frame per `MotionConfig.frame_interval`, and **`duration` is the length of that ramp**,
|
|
211
|
+
not a deadline: a 40 mm target with `duration=0.5` travels at 80 mm/s and takes 0.5 s, then holds
|
|
212
|
+
the target for `settle_s` (`0.3`) while arrival is decided. The two families differ only in how the
|
|
213
|
+
speed is given (a duration, a speed in mm/s, or a speed in rad/s).
|
|
214
|
+
|
|
215
|
+
The ramp is bounded, and that bound is the point of it. Each frame's command leads the measured
|
|
216
|
+
position by at most `MotionConfig.max_lead_mm` (4 mm), which caps the frame's torque near
|
|
217
|
+
`kp × 4 mm` — about 6.6 N·m against the shipped `rad_to_mm` of `61.01`, under the DM4310's ±10 N·m
|
|
218
|
+
frame limit. A hard step to the target is not bounded: at the shipped `kp` of `100 Nm/rad` any
|
|
219
|
+
error past about `0.1 rad` saturates that limit, and a saturated frame overshoots the target
|
|
220
|
+
instead of stopping on it. `open` and `close` press onto a calibrated stop on purpose; they cap the
|
|
221
|
+
emitted command at the calibrated extreme, so the jaw stops on the limit and is never driven past
|
|
222
|
+
it. A NaN or ±inf position is refused with `CommandError`, never clamped.
|
|
223
|
+
|
|
224
|
+
### Gripping and force
|
|
225
|
+
|
|
226
|
+
`grasp(force_n, hold_s)` is the only way to press with a force. It closes first and holds second,
|
|
227
|
+
and the hold frames carry `kp = kd = 0`, so the drive's output *is* the feed-forward torque
|
|
228
|
+
`force_n × 0.1 Nm`. That is what makes the force independent of the workpiece: as a soft object
|
|
229
|
+
yields, the jaws follow it and the grip stays at the setpoint.
|
|
230
|
+
|
|
231
|
+
**A force is only meaningful against something.** There is no call that applies a force at the
|
|
232
|
+
current position, and that is deliberate. Such a call cannot tell whether anything is between the
|
|
233
|
+
jaws, and the hold frames give the drive no position or velocity error to work against — with
|
|
234
|
+
nothing there, the torque is the only acting force and the jaws accelerate. `grasp()` avoids it
|
|
235
|
+
by construction: the closing leg runs on the position ramp, so it arrives at the object (or at
|
|
236
|
+
the closed-side stop) and stops there before the hold takes over.
|
|
237
|
+
|
|
238
|
+
- **The force is approximate.** `1 N = 0.1 Nm` is marked approximate in the SDK, and the real
|
|
239
|
+
clamping force also depends on your finger geometry. Treat `force_n` as a repeatable setting,
|
|
240
|
+
not a calibrated measurement.
|
|
241
|
+
- **The jaws stay compliant.** An external push back-drives them while the motor keeps pushing
|
|
242
|
+
with `force_n`, and an empty `grasp()` closes onto the mechanical stop at that force. That is
|
|
243
|
+
the price of a force that does not drift when the workpiece moves.
|
|
244
|
+
- **Below about 3.7 N the approach speed becomes the limit**, because one frame of travel at
|
|
245
|
+
`grasp_speed_mm_s` already spends the whole torque budget. Cap the travel time with
|
|
246
|
+
`grasp_speed_mm_s` rather than asking for a force the mechanism's own friction absorbs.
|
|
247
|
+
|
|
248
|
+
**The runaway watch.** The hold frames carry no position error, so no position limit can act on
|
|
249
|
+
them; the only lever is removing the torque. Once the measured position is `hold_runaway_rad`
|
|
250
|
+
(`0.05 rad`, about 3 mm) past the closed-side calibrated stop, `grasp()` streams zero-torque
|
|
251
|
+
release frames and returns with `GraspResult.travel_guard` set. Only the closing side is checked,
|
|
252
|
+
because that is the side the feed-forward pushes.
|
|
253
|
+
This is a runaway detector, not an accuracy bound: raise `hold_runaway_rad` only if a jaw that is
|
|
254
|
+
genuinely holding something is being released. The watch is off until the config is calibrated.
|
|
255
|
+
|
|
256
|
+
**An empty `grasp()` is the case that reaches it.** The closing leg presses onto the closed-side
|
|
257
|
+
stop and the hold keeps pushing there, so a stop that is not where the calibration says — or one
|
|
258
|
+
that has been knocked out — lets the hold walk the jaws past it. The trip leaves them sitting
|
|
259
|
+
outside the calibrated travel, and the release frames leave the drive *enabled* but carrying no
|
|
260
|
+
torque: it holds nothing, but it is not off either. Get the jaws back inside the travel before
|
|
261
|
+
commanding anything else — `disable()` the motor, push them clear of the stop by hand, then
|
|
262
|
+
`enable()` and carry on. Do not just call `grasp()` again: the closing leg re-approaches the same
|
|
263
|
+
stop and the hold presses it again.
|
|
264
|
+
|
|
265
|
+
Need more force than the default? `MotionConfig.force_n` is `20.0` N (`2.0 Nm`), inside the
|
|
266
|
+
DM4310's `3 Nm` rating. Its peak is `7 Nm` and the protocol ceiling is `10 Nm`.
|
|
267
|
+
|
|
268
|
+
### Calibration
|
|
269
|
+
|
|
270
|
+
The factory calibration shipped inside the SDK is the standard value, and a no-argument load reads
|
|
271
|
+
it and nothing else:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from litegrip import LiteGrip
|
|
275
|
+
|
|
276
|
+
with LiteGrip("can0") as gripper:
|
|
277
|
+
gripper.load_calibration() # the shipped file, describing a normal-mount unit
|
|
278
|
+
gripper.enable()
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
| Call | What it reads or writes |
|
|
282
|
+
| --- | --- |
|
|
283
|
+
| `load_calibration()` → `bool` | The shipped factory calibration, plus `LITEGRIP_CALIB` when that is set (the env var wins). |
|
|
284
|
+
| `load_calibration(path="/abs/file.json")` → `bool` | That file. Falls back to the factory file only if the path cannot deliver a usable calibration, and warns on a channel mismatch. |
|
|
285
|
+
| `load_calibration(template="normal")` / `load_template(name)` → `bool` | The limits of that template, whatever channel it is on. Strict: an unknown name raises `CommandError`. |
|
|
286
|
+
| `list_templates()` → `List[str]` | `['normal', 'reverse']` — every name the SDK accepts, for a UI to offer. A module-level function, imported by name. |
|
|
287
|
+
| `default_calib_path(channel="can0")` → `str` | Where a calibration is saved: `~/.litegrip/<channel>_calibration.json`, or `$LITEGRIP_CALIB` when set. |
|
|
288
|
+
| `save_calibration(path=None)` → `str` | Writes the current config to that path. |
|
|
289
|
+
| `zero()` → `CalibrationData` | Probes both mechanical stops, derives the scale, saves the result. |
|
|
290
|
+
| `calibrate_guided(kp=20.0, kd=2.0, step_rad=0.05, ...)` → `CalibrationData` | Two-step probe that stops at each limit and waits for you to confirm. |
|
|
291
|
+
| `calibrate_manual(duration=30.0, settle_time=2.0, sample_interval=0.01)` → `CalibrationData` | Hand-teach: goes slack, you push the jaws through the full stroke, it samples the extremes. |
|
|
292
|
+
| `write_zero()` → `WriteZeroResult` | Declares the *current* encoder angle as the motor's zero, in place. Writes the motor's flash and **cannot be undone**. |
|
|
293
|
+
|
|
294
|
+
What follows from that:
|
|
295
|
+
|
|
296
|
+
- **A calibration measured on your machine is never picked up by accident.** `zero()` writes
|
|
297
|
+
`~/.litegrip/<channel>_calibration.json`, and that file is read only when you name it:
|
|
298
|
+
`load_calibration(path=default_calib_path("can0"))`, or `LITEGRIP_CALIB=/path/to/file.json` for
|
|
299
|
+
every channel at once. **Do not expect it to be picked up automatically.**
|
|
300
|
+
- **Only `<channel>_calibration.json` is recognized in that directory.** A calibration file under
|
|
301
|
+
any other name — `can0_08_calibration.json`, say — is not a candidate at all: not tried, not
|
|
302
|
+
merged, not a later fallback stage. Whenever a no-argument load passes over a local file, the
|
|
303
|
+
warning names it and repeats the two ways to use it.
|
|
304
|
+
- `zero()` drives the jaws onto both real stops, so keep hands and objects out of the travel
|
|
305
|
+
range, and support the jaws if the gripper is mounted in a way they would fall. Set
|
|
306
|
+
`GripperConfig.max_stroke_mm` (`85.0`, the shipped hardware) when your jaws are not 85 mm; it is
|
|
307
|
+
the one per-unit measurement no calibration file carries, and it matters only when you re-probe.
|
|
308
|
+
- The factory file declares `channel: can0`, so a `can1` unit has no implicit source and fails
|
|
309
|
+
loudly — `load_calibration()` returns `False` — rather than silently adopting `can0`'s
|
|
310
|
+
direction. Give it a template, or a `zero()` of its own named back as above.
|
|
311
|
+
|
|
312
|
+
### Reading the results
|
|
313
|
+
|
|
314
|
+
The result types are dataclasses that also define `__bool__`, so `if gripper.close():` works.
|
|
315
|
+
What it *means*, and what to check, is per action:
|
|
316
|
+
|
|
317
|
+
| Type | Fields | `bool()` is |
|
|
318
|
+
| --- | --- | --- |
|
|
319
|
+
| `MoveResult` | `ok`, `reached`, `stalled`, `state`, `target_rad`, `limit_rad`, `final_cmd_rad`, `steps`, `protected`, `released` | `ok` |
|
|
320
|
+
| `GraspResult` | `ok`, `reached`, `stalled`, `state`, `target_rad`, `force_n`, `cycles`, `travel_guard` | `ok` |
|
|
321
|
+
| `EnableResult` | `ok`, `state`, `tries` | `ok` |
|
|
322
|
+
|
|
323
|
+
**Read `ok`.** Three combinations look alarming and are correct:
|
|
324
|
+
|
|
325
|
+
- **`close()` on an empty gripper** returns `ok=True, stalled=True, reached=False`. Success for
|
|
326
|
+
`open`/`close` is *stalling with the command capped at the calibrated limit and the jaw parked
|
|
327
|
+
within `stop_tol` of it*. `reached` is essentially always `False` here, because the target sits
|
|
328
|
+
deliberately past the limit.
|
|
329
|
+
- **`grasp()` on an object** returns `stalled=True, reached=False` with `ok=True`. Stopping short
|
|
330
|
+
of the empty-jaw target is the point of gripping.
|
|
331
|
+
- **`grasp()` on an empty gripper** can return `reached=True` while the jaw sits about
|
|
332
|
+
`0.010 rad` short of `target_rad` — the close-side stick-slip dead band, which is why
|
|
333
|
+
`reach_tol` defaults to `0.02`. `target_rad` is the closed-side calibrated stop itself.
|
|
334
|
+
|
|
335
|
+
A fourth combination is a failure, and new in this version:
|
|
336
|
+
|
|
337
|
+
- **`grasp()` with `ok=False, reached=False, stalled=False`** means the closing leg ran out of
|
|
338
|
+
frames without reaching the stop and without stalling — the jaw is still in free travel. The
|
|
339
|
+
hold is *not* entered: its frames carry no position and no velocity term, so handing a moving
|
|
340
|
+
jaw over to them leaves the feed-forward torque as the only thing acting on it, and the jaw
|
|
341
|
+
accelerates until the runaway watch releases it. The call releases in place instead and reports
|
|
342
|
+
`cycles=0`. Nothing was gripped. If you see this, the closing leg is being starved — a stale
|
|
343
|
+
state read, or friction the mechanism's own budget cannot overcome.
|
|
344
|
+
|
|
345
|
+
Stalling far from the limit gives `ok=False`: something blocked the travel. The diagnostic fields
|
|
346
|
+
— `final_cmd_rad`, `steps`, `state` — are what you log when it is false.
|
|
347
|
+
|
|
348
|
+
The positioning calls read differently, because their target is a point inside the travel rather
|
|
349
|
+
than a stop: `goto`, `goto_rad`, `move_to`, `home` and `move_at_speed*` are `ok` only when the
|
|
350
|
+
jaws arrived and did not stall (`reached and not stalled`). Blocked halfway, they come back
|
|
351
|
+
`ok=False, stalled=True` — and with `released=True`, meaning the drive's force was taken off
|
|
352
|
+
(`kp = kd = tau = 0`) after the stall instead of being left pressing the part. `protected=True` is
|
|
353
|
+
the other way a move can end early: the in-travel torque guard (~7 N, see
|
|
354
|
+
`MotionConfig.stop_torque_nm`) tripped. That guard takes the force off on its own, so `protected`
|
|
355
|
+
leaves `released` at `False`, and it always forces `ok=False`.
|
|
356
|
+
|
|
357
|
+
### Progress reporting
|
|
358
|
+
|
|
359
|
+
The motion actions never print; they report through a `progress` callback, invoked with a
|
|
360
|
+
`MoveProgress` snapshot at the sampling rate.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
def show(p):
|
|
364
|
+
print(f"[{p.phase}] {p.i}/{p.total_steps} cmd={p.cmd_rad:+.4f} "
|
|
365
|
+
f"pos={p.pos_rad:+.4f} tau={p.torque_nm:+.3f}")
|
|
366
|
+
|
|
367
|
+
gripper.open(progress=show)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
For `grasp`, `p.phase` is `"hold"` while force is being held, and `p.total_steps` is `0`. `zero()`
|
|
371
|
+
is the exception to the rule: it prints its own probe progress to stdout.
|
|
372
|
+
|
|
373
|
+
### Tuning
|
|
374
|
+
|
|
375
|
+
Every tunable lives on one dataclass, `MotionConfig`. The defaults are the values validated on
|
|
376
|
+
real hardware. Override them per instance through `gripper.motion_config`, or per call.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from litegrip import LiteGrip, MotionConfig
|
|
380
|
+
|
|
381
|
+
with LiteGrip("can0") as gripper:
|
|
382
|
+
gripper.motion_config = MotionConfig(speed_mm_s=25.0, force_n=10.0)
|
|
383
|
+
gripper.close()
|
|
384
|
+
|
|
385
|
+
gripper.motion_config.force_n = 5.0 # also writable field by field
|
|
386
|
+
gripper.grasp()
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
| Field | Default | Meaning |
|
|
390
|
+
| --- | --- | --- |
|
|
391
|
+
| `speed_mm_s` | `50.0` | `open` / `close` jaw speed |
|
|
392
|
+
| `grasp_speed_mm_s` | `50.0` | closing speed of the `grasp` approach |
|
|
393
|
+
| `force_n` | `20.0` | default `grasp` force |
|
|
394
|
+
| `force_ramp_n_s` | `20.0` | rate the held force climbs to its setpoint, N/s |
|
|
395
|
+
| `hold_interval` | `0.2` | force-hold slice length, seconds |
|
|
396
|
+
| `hold_runaway_rad` | `0.05` | past the closed stop by this much, the force hold drops the torque and stops |
|
|
397
|
+
| `margin` | `0.05` | fraction of travel kept in reserve from the stop by the `limit_target` helper; no shipped call reaches it |
|
|
398
|
+
| `max_lead_mm` | `4.0` | cap on how far the command may lead the measured position, on moves that carry no force setpoint |
|
|
399
|
+
| `stop_lead_mm` | `0.7` | the same cap on the approach to a stop, so the pressing torque stays bounded |
|
|
400
|
+
| `press_safety` | `0.9` | fraction of the setpoint a force-carrying approach may spend |
|
|
401
|
+
| `reach_tol` | `0.02` | position tolerance for `reached`, radians |
|
|
402
|
+
| `stop_tol` | `0.03` | how close to the calibrated stop the jaw must park to count as pressed home |
|
|
403
|
+
| `settle_s` | `0.3` | time spent holding the target after the ramp |
|
|
404
|
+
|
|
405
|
+
Every other field — the stall window, the probe settings, the frame timing — is documented on the
|
|
406
|
+
dataclass itself, with the reasoning for its default.
|
|
407
|
+
|
|
408
|
+
`sleep_fn` and `monotonic_fn` are the supported seams for simulation: the engine calls only those
|
|
409
|
+
two for time, so `sleep_fn=lambda _: None` collapses a whole ramp to no wall-clock time.
|
|
410
|
+
|
|
411
|
+
### Teleoperation
|
|
412
|
+
|
|
413
|
+
Two grippers can be linked so one follows the other. The **master** (leader) motor goes slack —
|
|
414
|
+
you push its jaws by hand — and publishes how far open it is. The **slave** (follower) drives its
|
|
415
|
+
own jaws to match. What travels over the wire is a normalized opening in `[0, 1]`, not an angle,
|
|
416
|
+
so the two ends do not need the same calibration, mount, or zero point.
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from litegrip import LiteGrip
|
|
420
|
+
|
|
421
|
+
# Leader: listen and publish this gripper's opening.
|
|
422
|
+
with LiteGrip("can0") as master:
|
|
423
|
+
master.load_calibration()
|
|
424
|
+
master.enable()
|
|
425
|
+
master.teleop_start("master") # zenoh, gripA, port 17448
|
|
426
|
+
|
|
427
|
+
# Follower: connect to the leader, ramp to the first frame, then follow.
|
|
428
|
+
with LiteGrip("can0") as slave:
|
|
429
|
+
slave.load_calibration()
|
|
430
|
+
slave.enable()
|
|
431
|
+
slave.teleop_start("slave", host="192.168.1.20")
|
|
432
|
+
while True:
|
|
433
|
+
print(slave.teleop_status()) # frames, openness, loop_hz, stale, ...
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| Method | What it does |
|
|
437
|
+
| --- | --- |
|
|
438
|
+
| `teleop_start(mode, *, transport=None, link="zenoh", host=None, port=17448, grip_id="gripA", kp=None, kd=None, align=True, watchdog_s=0.2, dq_max=10.0, torque_limit_nm=0.0, require_ready=True, ...)` → `dict` | Starts the background loop for `mode="master"` or `"slave"`. Both ends must share `grip_id` and be connected and enabled first. |
|
|
439
|
+
| `teleop_stop(timeout=2.0)` → `dict` | Stops the loop and leaves the jaws holding. |
|
|
440
|
+
| `teleop_status()` → `dict` | `active`, `mode`, `frames`, `last_frame_age_ms`, `stale`, `openness`, `position_mm`, `force_n`, `loop_hz`, `rejected`, `send_failed`, `fault`, `torque_nm`, `over_torque`, `torque_trips`, `ready`, and more. |
|
|
441
|
+
|
|
442
|
+
The link is a point-to-point **zenoh** session, an optional dependency:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
pip install 'litegrip-python[zenoh]'
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Both ends use `mode="peer"` with all discovery off, so the only way they find each other is an
|
|
449
|
+
explicit endpoint. `link="udp"` selects a plain-UDP fallback for a trusted LAN; it has no
|
|
450
|
+
authentication or encryption, so keep either one on a trusted network.
|
|
451
|
+
|
|
452
|
+
`examples/teleop.py` runs one end from the command line — one process per machine:
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
python3 examples/teleop.py --mode master --channel can0
|
|
456
|
+
python3 examples/teleop.py --mode slave --channel can0 --host 192.168.1.20
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
To bench a **single** gripper, `--fake-leader` replaces the leader with a synthetic one on an
|
|
460
|
+
in-process bus. Put a rigid object between the jaws first — with nothing to press against, the
|
|
461
|
+
follower closes freely and the torque guard has nothing to demonstrate.
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
python3 examples/teleop.py --mode slave --channel can0 --fake-leader --torque-limit 1.0
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- **The leader holds its jaws under gain until the follower reports ready**, so the operator
|
|
468
|
+
cannot push the target out from under a follower that is still travelling. `ready_timeout_s`
|
|
469
|
+
(default `10.0`) bounds the wait; `require_ready=False` restores the immediate hand-back.
|
|
470
|
+
- **An optional torque guard releases a follower that is pressing too hard.** Set
|
|
471
|
+
`torque_limit_nm` and it goes to zero stiffness and damping *in place* after 60 ms over the
|
|
472
|
+
limit, then re-arms once the leader has reopened. The default is `0` — **the guard is off
|
|
473
|
+
unless you ask for it**. Pick the value per machine: it depends on how fragile the part between
|
|
474
|
+
the jaws is.
|
|
475
|
+
- **A follower that loses the leader holds its position, it does not go slack.** After
|
|
476
|
+
`watchdog_s` (default `0.2`) without a fresh frame, `stale` goes true and the jaws stay put.
|
|
477
|
+
- **The follower feeds the leader's velocity forward** by differencing successive frames, so it
|
|
478
|
+
does not trail a moving leader. `dq_max=0` disables that.
|
|
479
|
+
- **Non-finite frames are dropped, never clamped**, on both ends.
|
|
480
|
+
- Teleop owns the CAN I/O while it runs: do not drive the gripper from the caller until
|
|
481
|
+
`teleop_stop()`.
|
|
482
|
+
|
|
483
|
+
### Recording and replay
|
|
484
|
+
|
|
485
|
+
A motion you teach by hand can be captured once and repeated later. Recording puts the motor into
|
|
486
|
+
zero-gravity so you can push the jaws through the motion; replay streams the captured openings
|
|
487
|
+
back as command frames. What is stored is the normalized opening in `[0, 1]`, exactly as teleop
|
|
488
|
+
sends it, so a trajectory taught on one gripper replays on another with a different mount or
|
|
489
|
+
calibration.
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
from litegrip import LiteGrip
|
|
493
|
+
|
|
494
|
+
with LiteGrip("can0") as gripper:
|
|
495
|
+
gripper.load_calibration()
|
|
496
|
+
gripper.enable()
|
|
497
|
+
|
|
498
|
+
taught = gripper.record(5.0) # 5 s of hand-teaching; the jaws are slack
|
|
499
|
+
taught.save("pick") # ~/.litegrip/trajectories/pick.lgt
|
|
500
|
+
gripper.play(taught) # repeat it
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
| Method | Behaviour |
|
|
504
|
+
| --- | --- |
|
|
505
|
+
| `record(duration_s, rate_hz=100.0, zero_gravity=True)` → `Trajectory` | Blocking hand-teach. |
|
|
506
|
+
| `record_start(rate_hz=100.0, zero_gravity=True, max_samples=None)` → `dict` | Background recording. |
|
|
507
|
+
| `record_stop(allow_empty=False)` → `Trajectory` | Stops and returns the capture. |
|
|
508
|
+
| `play(trajectory, speed=1.0, kp=None, kd=None, loop=False, align=True)` → `dict` | Blocking replay. `loop=True` raises `ValueError` — a blocking replay would never return. |
|
|
509
|
+
| `play_start(trajectory, speed=1.0, kp=None, kd=None, loop=False, align=True)` → `dict` | Background replay. |
|
|
510
|
+
| `play_stop(timeout=2.0)` → `dict` | Stops a replay and leaves the gripper holding. |
|
|
511
|
+
| `trajectory_status()` → `dict` | One snapshot for both directions: `active`, `kind`, `samples`, `error`, plus `rate_hz` / `loop_hz` while recording and `frames` / `speed` / `openness` / `completed` while playing. |
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
python3 examples/trajectory.py --record 5 --save pick # hand-teach, then save
|
|
515
|
+
python3 examples/trajectory.py --list # no hardware needed
|
|
516
|
+
python3 examples/trajectory.py --play pick --repeat 3
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
- **Replay commands position, not force.** The recorded torque is stored for diagnostics and never
|
|
520
|
+
fed forward, so a squeeze recorded against an object repeats as a position path. Follow the
|
|
521
|
+
replay with `grasp(force_n=...)` if the grip force matters.
|
|
522
|
+
- **`record()` is exclusive and the jaws are slack for its whole duration.** Keep a hand on the
|
|
523
|
+
gripper: nothing is holding the jaws, and whatever is between them will drop.
|
|
524
|
+
- **A capture that did not fill raises** rather than returning a short recording as if it were
|
|
525
|
+
whole.
|
|
526
|
+
- **Record without zero-gravity when something else drives.** `record_start(zero_gravity=False)`
|
|
527
|
+
only *reads* state, so the caller may run a `grasp()` or a move sequence from another thread and
|
|
528
|
+
capture it.
|
|
529
|
+
- `LITEGRIP_TRAJ_DIR` moves the trajectory directory. Recording, replay and teleop are mutually
|
|
530
|
+
exclusive and raise `TrajectoryBusyError` / `TeleopBusyError`; `disconnect()` stops whichever is
|
|
531
|
+
running.
|
|
532
|
+
|
|
533
|
+
### Errors
|
|
534
|
+
|
|
535
|
+
| Exception | Raised when |
|
|
536
|
+
| --- | --- |
|
|
537
|
+
| `LiteGripError` | Base class. `str()` appends the motor's code as `[0xXXXX]` when there is one. |
|
|
538
|
+
| `ConnectError` | The CAN interface is unavailable, or the motor never replies. |
|
|
539
|
+
| `CommError` | A CAN read or write failed mid-session. |
|
|
540
|
+
| `CANTimeoutError` | No reply within the timeout. |
|
|
541
|
+
| `CommandError` | The command was rejected: bad argument, uncalibrated config, unknown template, or a NaN / ±inf value. |
|
|
542
|
+
| `HardwareError` | The motor reported a fault code — for instance `enable()` never saw `err == 1`. |
|
|
543
|
+
| `NotInitializedError` | Not connected, or not enabled. |
|
|
544
|
+
| `TeleopError` / `TrajectoryError` | Teleop and recording / replay; each has busy, not-active and format subclasses. |
|
|
545
|
+
|
|
546
|
+
Errors are typed and never swallowed. `describe_error(code)` turns the motor's own error code into
|
|
547
|
+
text. The SDK logs through the standard `logging` module — the calibration it loaded, the
|
|
548
|
+
direction it derived, every guard that fired; attach a handler to the `litegrip` logger to see
|
|
549
|
+
them, and note that warnings reach stderr even with no handler configured.
|
|
550
|
+
|
|
551
|
+
A frame that fails to go out follows the table rather than one fixed message. The send reports
|
|
552
|
+
`False` and keeps the reason in `gripper.last_send_error`: a transport failure raises `CommError`
|
|
553
|
+
carrying that reason, and a gripper that is not connected or not enabled raises
|
|
554
|
+
`NotInitializedError` naming the missing step. When nothing recorded a reason — a coarse
|
|
555
|
+
`send_mit_frame` stub, say — the message says so instead of guessing.
|
|
556
|
+
|
|
557
|
+
### Advanced
|
|
558
|
+
|
|
559
|
+
| Interface | What it is for |
|
|
560
|
+
| --- | --- |
|
|
561
|
+
| `gripper.actions` | The `GripperActions` engine behind the high-level methods — the layer to build another driver on. |
|
|
562
|
+
| `send_mit_frame(q, kp, kd, dq=0.0, tau=0.0)` | One raw MIT frame, when you want to compose the control law yourself. |
|
|
563
|
+
| `read_param(rid, timeout_s=0.5)` | Reads a motor register. |
|
|
564
|
+
| `enter_zero_gravity(duration=0.0)` / `exit_zero_gravity()` | Slack the motor by hand, then take it back. |
|
|
565
|
+
| `clamp_to_travel(config, q)` | Clamps an angle into the calibrated travel — the rule `open` and `close` emit under. |
|
|
566
|
+
| `limit_target`, `press_target`, `work_limit_target` | The target math behind `open`, `close` and the work-stroke limit. |
|
|
567
|
+
| `force_approach_terms(kp, kd, speed_rad_s, frame_interval, budget_nm, lead_ceiling_rad)` | The speed, damping and lead a force-carrying approach may spend. |
|
|
568
|
+
| `closing_travel_excursion_rad(config, q)` | How far past the closed-side limit a position is. |
|
|
569
|
+
| `CALIB_TEMPLATES`, `UnitConversion`, `GripperParams`, `GripperGeometry`, `ErrorCode` | Template paths, unit conversions, motor limits, geometry and alarm codes. |
|
|
570
|
+
| `litegrip.can` | Raw SocketCAN transport and the DM protocol codec, for multi-motor setups. |
|
|
571
|
+
|
|
572
|
+
`clamp_to_travel`, `limit_target`, `press_target`, `work_limit_target`, `force_approach_terms` and
|
|
573
|
+
`closing_travel_excursion_rad` are module-level functions — import them from `litegrip`. They take
|
|
574
|
+
the config as their first argument rather than being called on the gripper.
|
|
575
|
+
|
|
576
|
+
## Troubleshooting
|
|
577
|
+
|
|
578
|
+
| Symptom | What it means |
|
|
579
|
+
| --- | --- |
|
|
580
|
+
| `CommandError` naming a calibration | No calibration loaded for this channel. Call `load_calibration()`. |
|
|
581
|
+
| `ConnectError`, or no status frame ever | `can0` is down, or the bitrate is not 1 Mbit/s. See step 2. |
|
|
582
|
+
| `HardwareError` from `enable()` | The motor never reported `err == 1`. Check power, then `clear_fault()`. |
|
|
583
|
+
| The jaws move the wrong way | The unit is reverse-mounted, not normal. Construct with `mount="reverse"`. |
|
|
584
|
+
| `close()` returns `ok=False` | The jaw parked further than `stop_tol` from the limit. The gap comes from `zero()` probing harder than `close()` presses, so raising `stop_lead_mm` barely moves it — check that the calibration describes the real travel, and raise `stop_tol` if the unit is genuinely parked on the stop. |
|
|
585
|
+
| `travel_guard` is set | The runaway watch tripped: the jaw walked past the closed-side stop while a force was held — an empty `grasp()` does this when the stop is not where the calibration says. Check the calibration describes the real travel, and that nothing has removed the stop. The jaws are now outside the calibrated travel: `disable()`, push them back clear of the stop by hand, then `enable()`, before commanding anything else. |
|
|
586
|
+
| A `can1` unit's `load_calibration()` returns `False` | A no-argument load reads the factory file, which declares `channel: can0`, so this unit has no implicit source. Give it a template, or name its own file with `path=` or `LITEGRIP_CALIB`. |
|
|
587
|
+
| The held force sags below the setpoint | You changed the hold gains, or something is back-driving the jaws faster than the feed-forward. `hold_kp` / `hold_kd` are deprecated and ignored. |
|
|
588
|
+
| `import litegrip` loads older code than the checkout you are reading | An installed copy does not update itself across `git pull`. Check `litegrip.__file__`, or `pip install -e .` in the checkout. |
|
|
589
|
+
|
|
590
|
+
## Safety and limits
|
|
591
|
+
|
|
592
|
+
- **Load a calibration before any motion.** Until then every motion method raises `CommandError`.
|
|
593
|
+
This is the whole gate: on the placeholder defaults a `goto(40.0)` maps to `+0.61 rad`, past the
|
|
594
|
+
real closed stop, and would press there.
|
|
595
|
+
- **Keep hands and objects out of the travel range** during `open`, `close`, `grasp` and `zero()`.
|
|
596
|
+
`open` and `close` stop on the calibrated limit; `zero()` probes the real mechanical stops under
|
|
597
|
+
power.
|
|
598
|
+
- **A calibration that is wrong limits the travel; it cannot let the jaw run past it.** `open` and
|
|
599
|
+
`close` cap the emitted command at the calibrated extreme, so once the jaw is there the residual
|
|
600
|
+
error is zero and the steady pressing torque is zero.
|
|
601
|
+
- **A held force keeps pushing.** Nothing about a hold is position-bounded: the frames carry no
|
|
602
|
+
gains, so the jaws are compliant and the motor pushes with `force_n` wherever they end up. The
|
|
603
|
+
runaway watch is the only thing that stops it, and it is a backstop, not an operating limit.
|
|
604
|
+
- **`write_zero()` is irreversible.** It writes an encoder offset into the motor's flash.
|
|
605
|
+
- **Sustained pressing heats the coil.** `open` and `close` hold against the limit for the settle
|
|
606
|
+
phase every time, so check the coil temperature if they run back to back.
|
|
607
|
+
- **A non-finite value is refused, never clamped.** `goto`, `goto_rad`, `move_at_speed*` and
|
|
608
|
+
`grasp` raise `CommandError` for a NaN or ±inf argument, and the CAN boundary refuses any such
|
|
609
|
+
field of a frame before it can become bytes.
|
|
610
|
+
- **`disconnect()` disables the motor by default**, so a process that dies does not leave the motor
|
|
611
|
+
holding torque. Set `disable_on_disconnect=False` only when something else keeps the gripper
|
|
612
|
+
under control.
|
|
613
|
+
|
|
614
|
+
## Examples
|
|
615
|
+
|
|
616
|
+
Every script below is runnable from the repository root and prints what it measured.
|
|
617
|
+
|
|
618
|
+
| Script | What it answers |
|
|
619
|
+
| --- | --- |
|
|
620
|
+
| `examples/stroke_test.py` | Does this unit work? Both ends of the travel, exit 0 on success. |
|
|
621
|
+
| `examples/grasp_test.py` | Does `grasp()` still do what it says — on clear jaws and on an object? |
|
|
622
|
+
| `examples/hold_force_test.py` | Does the held grip hold its force, or drift to a smaller one? |
|
|
623
|
+
| `examples/zero_closed.py` | Low-level CAN bench tool, one subcommand per operation: `status`, `release`, `goto`, `zero`, `open`, `close`, `enable`, `disable`. |
|
|
624
|
+
| `examples/teleop.py` | One end of a leader/follower link. |
|
|
625
|
+
| `examples/trajectory.py` | Hand-teach a motion, save it, play it back. |
|
|
626
|
+
|
|
627
|
+
The test suite is hardware-free: it drives the engine through a kinematic fake CAN in
|
|
628
|
+
`tests/fake_can.py`, so it runs anywhere without a gripper or a CAN interface.
|
|
629
|
+
|
|
630
|
+
```bash
|
|
631
|
+
python3 -m unittest discover -s tests -t tests -v
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## License
|
|
635
|
+
|
|
636
|
+
Copyright © 2026 NEXFORM ROBOTICS. Licensed under the [Apache License 2.0](https://github.com/nexform-tech/litegrip-python/blob/main/LICENSE).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
litegrip/__init__.py,sha256=td31c0tK5Db1y6cvhfG3l_tIfzhwaUX3RYaS2eDxxsM,9346
|
|
2
|
+
litegrip/actions.py,sha256=aKc2rCloJKp9n0gLEz6PNQs6w9gz6c--6K-DR5i62YM,80278
|
|
3
|
+
litegrip/calibration_normal.json,sha256=rL9XGWD-aATZWUqE3MSXuQ2wR_Oz1L0EF78kJgQpnd0,209
|
|
4
|
+
litegrip/calibration_reverse.json,sha256=HVweCi6LVBy7QQGxHa5NeaJfoOAfnzPMmVtXXIiCE74,209
|
|
5
|
+
litegrip/constants.py,sha256=A1-gt-bEk4xsX00LJtE-0uQQu-Brz8LRIJOAVQfBpfg,6660
|
|
6
|
+
litegrip/exceptions.py,sha256=2fQtANhxat0lVbhhU_SD-6nTcA4r-NLCpfmd06TGe3U,1144
|
|
7
|
+
litegrip/factory_calibration.json,sha256=j4kbECLTXRPyfrApraDm2PlNRa1w7mnuhO6sQVQXSxU,345
|
|
8
|
+
litegrip/gripper.py,sha256=UNoD0xG0-U0KFbBOpy57leUfnm8xqWJ8wI9PEGNV-mc,120533
|
|
9
|
+
litegrip/models.py,sha256=UzBtvje9r6Rq6t_eqJz3fuI9XxGHTeY9xVn5lCCJ3ZM,7659
|
|
10
|
+
litegrip/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
litegrip/teleop.py,sha256=epCwCGnANFF9ySl5kCCNxp5LdOGvsNYBe2z7cSzKY4c,71737
|
|
12
|
+
litegrip/trajectory.py,sha256=n1m97jADWvyk6FdUQGA69g8D2mFwD0bg0o5Zh9Mrpgg,44081
|
|
13
|
+
litegrip/zenoh_link.py,sha256=t9NzlDhG0vJmUuw6B0B7WkXSNgxDH5EhiKIJAmuHDaY,16264
|
|
14
|
+
litegrip/can/__init__.py,sha256=MmK9W41RVx8tmwBMUbezlBbeMUC34pgsw6LveKQ8FKg,1019
|
|
15
|
+
litegrip/can/controller.py,sha256=E8kPcKn0vHOVyxGhrZiiECEKFoY5tUEBvr6-_rw3hMM,16343
|
|
16
|
+
litegrip/can/motor.py,sha256=rdcYK3rA3-yu4vWHA3sVM_DZY-rEeSGXGM5mLtff7AI,5753
|
|
17
|
+
litegrip/can/protocol.py,sha256=dt69WYOtVGSlmNv_Vxszt4YLkazUvl4dWs4S75wgZqg,11954
|
|
18
|
+
litegrip/can/transport.py,sha256=4-0XjPpguEaFVMVHsD5iLBTjgKN8Cb-X1MYTZJkvhPE,14188
|
|
19
|
+
litegrip/protocols/__init__.py,sha256=aregvFK6W76DGGrKCS1tccWsqWWCFBROXZPQ9CcPI1k,304
|
|
20
|
+
litegrip/protocols/can_bus.py,sha256=UhGhVMrgQ--hC-hZNvPscrT2sjdMX3rgspOdCeKXAnY,26383
|
|
21
|
+
litegrip_python-1.1.2.dist-info/licenses/LICENSE,sha256=yVuuHRzgI17MzTVgt3LsHvuX80innw--CmNPDCzO_iw,11358
|
|
22
|
+
litegrip_python-1.1.2.dist-info/METADATA,sha256=Gxp0sNKME4uVDASP5xKTy7hcuY84zqIKRoJVAJ4NPsY,39000
|
|
23
|
+
litegrip_python-1.1.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
24
|
+
litegrip_python-1.1.2.dist-info/top_level.txt,sha256=P4onDW9LkBEwiiQw4u1ui2fZSjQ4_XL5RZeCUh7RGoE,9
|
|
25
|
+
litegrip_python-1.1.2.dist-info/RECORD,,
|