Python SDK for the LiteGrip lightweight robotic gripper series, and the primary public interface to it. This is the reference: what each interface is, and how to use it.
You do not need to know CAN or the DM4310 protocol: every move, calibration and force control is one method call, and each call verifies its own outcome before reporting success. Every signature below is the real one, with its default.
| Motor | Damiao DM4310, CAN ID 0x08, MIT mode, classic CAN at 1 Mbit/s |
| Host | Linux with SocketCAN — this is the only supported platform |
| Python | 3.8 or newer |
| Dependencies | none: standard library only |
| Optional | litegrip[zenoh] — the point-to-point teleoperation link |
The package is not on PyPI yet. Install it from a checkout:
git clone https://github.com/nexform-tech/litegrip-python.git
cd litegrip-python
python3 -m pip install .Or skip the install and point PYTHONPATH at the source tree:
PYTHONPATH=/path/to/litegrip-python/src python3 your_script.pyThe gripper is a CAN device. Bring the interface up at 1 Mbit/s before any SDK call, and do it again after every reboot:
sudo ip link set can0 type can bitrate 1000000
sudo ip link set can0 upBoth commands need root. If can0 does not exist at all, the CAN adapter is not loaded yet. In a
product, run these two commands at boot from your init system, so the application never has to
start the interface itself.
from litegrip import LiteGrip
with LiteGrip("can0") as gripper: # connect() on entry, disconnect() on exit
gripper.load_calibration() # the shipped factory calibration — no arguments
gripper.enable() # retries until the status frame says err == 1
gripper.open() # 50 mm/s toward the open-side stop
gripper.close() # 50 mm/s toward the closed-side stop
result = gripper.grasp(force_n=20.0, hold_s=3.0)
print(result.ok, result.stalled, result.cycles)Two things to know before going further:
- The factory calibration is the default. A no-argument
load_calibration()reads the calibration file shipped inside the SDK, which describes the shipped units (normal mount). You name a different file only when you have one — see Calibration. - Nothing moves before a calibration is loaded. Every motion method raises
CommandErroron an uncalibrated config, because the placeholder limits describe no real travel.
with calls disconnect() on exit, which disables the motor by default. Pass
LiteGrip(..., disable_on_disconnect=False) to leave it energised after the block.
LiteGrip(channel="can0", can_id=0x08, mst_id=None, canfd_mode=None,
motor_type=MotorType.DM4310, config=None, motion_config=None,
disable_on_disconnect=True, mount=None)channel— CAN interface name. Every LiteGrip shares CAN ID0x08, so the channel is the only identity key when two sit on one machine.mount—"normal"or"reverse", loading that template's limits. The shipped units are normal mount, which is what the factory calibration describes, so leave it unset unless a unit is reverse-mounted (its jaws open when youclose()).mst_id— master ID;Noneauto-detects it.config/motion_config— replace the two dataclasses up front instead of editing them after construction.disable_on_disconnect—disconnect()disables the motor (crash safety). SetFalseonly when something else keeps the gripper under control.
| Attribute | What it is |
|---|---|
channel, can_id, mst_id |
The identity this instance drives |
config |
GripperConfig — limits, scale, gains |
motion_config |
MotionConfig — every tunable; settable as a whole or field by field |
actions |
GripperActions — the engine the high-level methods forward to |
mount |
"normal", "reverse", or None until calibrated (derived from the limits) |
is_connected, is_enabled |
Session state |
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. |
session |
The CAN session, for advanced use |
| Method | What it does |
|---|---|
connect() → bool |
Opens the CAN session. The with block does this for you. |
disconnect() → None |
Closes it, disabling the motor unless disable_on_disconnect=False. |
poll(timeout_s=0.0) → bool |
Reads one frame; True if one arrived. |
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. |
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. |
get_position() / get_position_rad() → float |
Measured opening, in mm / rad. |
get_force() → float |
get_torque() × 10, in N. |
get_torque() → float |
Measured torque, Nm. |
get_temperature() → tuple[int, int] |
(MOS °C, coil °C). |
get_error() → int |
The status code the motor reported: 1 enabled, 0 disabled, 0x9–0xC for faults. describe_error(code) names one. |
get_info() → GripperInfo |
Static device metadata. |
is_moving() / is_grasped() → bool |
Motion and grip predicates. |
wait_for_ready(timeout=5.0) → bool |
Blocks until the motor accepts commands. |
The readings get_state() returns are a cache, and only a status frame updates it. A DM motor
answers the control frames it receives, so a gripper that is being driven reports on its own; one
holding a pose, or disabled, sends nothing, and then wait=True waits out its 50 ms and hands back
the previous frame. The snapshot says which happened:
state = gripper.get_state()
if not state.fresh:
gripper.refresh_status() # 0xCC: ask for a frame; the jaws do not move
state = gripper.get_state()
if state.is_stale(): # older than 0.5 s, or never had a frame
raise RuntimeError(f"no status frame for {state.data_age_s} s")
print(state.position_mm, state.force_n, state.timestamp)fresh— a frame answered this read. False means the numbers are the previous frame's, however old that is.data_age_s— seconds since the frame behind the snapshot arrived, orNonewhen none ever has.timestamp— when that frame arrived, not when you asked.0.0means there is no frame at all and every reading is a constructor default. Do not time a loop with it; it is for a human reading a log, and the age arithmetic is whatdata_age_sis for.is_stale(stale_after_s=STALE_AFTER_S)— age past the threshold (STALE_AFTER_Sis 0.5 s). It is about the snapshot, not the read: a snapshot 20 ms old can still befresh=False.
fresh and is_stale() answer different questions, and neither substitutes for the other — the wait
window is 50 ms while the staleness threshold is 0.5 s, so a read no frame answered can still look
young. Ask both when the difference matters.
| Method | What it does |
|---|---|
enable(retries=None) → EnableResult |
Enables, then re-reads the status frame until it reports err == 1. retries=None uses MotionConfig.enable_retries (3). |
disable() → bool |
Zero torque; the jaws are back-drivable by hand. |
clear_fault() → bool |
Clears a latched motor fault. |
stop() → None |
Sends one zero-torque frame. |
| Method | What it does |
|---|---|
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. |
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. |
home() → MoveResult |
Ramps to the calibrated closed position. |
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). |
goto(position_mm, kp=None, kd=None, duration=0.5) → MoveResult |
Ramp to a position in millimetres. |
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. |
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. |
move_at_speed(target_mm, speed_mm_s=30.0, kp=None, kd=None) → MoveResult |
Ramp to a millimetre target at speed_mm_s. |
move_at_speed_rad(target_rad, speed_rad_s=0.5, kp=None, kd=None) → MoveResult |
The same in radians. |
kp and kd fall back to GripperConfig.kp (100.0) and .kd (2.0) on every call that takes
them. Every positioning call — the goto family and the move_at_speed family alike — runs one
ramp of one frame per MotionConfig.frame_interval, and duration is the length of that ramp,
not a deadline: a 40 mm target with duration=0.5 travels at 80 mm/s and takes 0.5 s, then holds
the target for settle_s (0.3) while arrival is decided. The two families differ only in how the
speed is given (a duration, a speed in mm/s, or a speed in rad/s).
The ramp is bounded, and that bound is the point of it. Each frame's command leads the measured
position by at most MotionConfig.max_lead_mm (4 mm), which caps the frame's torque near
kp × 4 mm — about 6.6 N·m against the shipped rad_to_mm of 61.01, under the DM4310's ±10 N·m
frame limit. A hard step to the target is not bounded: at the shipped kp of 100 Nm/rad any
error past about 0.1 rad saturates that limit, and a saturated frame overshoots the target
instead of stopping on it. open and close press onto a calibrated stop on purpose; they cap the
emitted command at the calibrated extreme, so the jaw stops on the limit and is never driven past
it. A NaN or ±inf position is refused with CommandError, never clamped.
grasp(force_n, hold_s) is the only way to press with a force. It closes first and holds second,
and the hold frames carry kp = kd = 0, so the drive's output is the feed-forward torque
force_n × 0.1 Nm. That is what makes the force independent of the workpiece: as a soft object
yields, the jaws follow it and the grip stays at the setpoint.
A force is only meaningful against something. There is no call that applies a force at the
current position, and that is deliberate. Such a call cannot tell whether anything is between the
jaws, and the hold frames give the drive no position or velocity error to work against — with
nothing there, the torque is the only acting force and the jaws accelerate. grasp() avoids it
by construction: the closing leg runs on the position ramp, so it arrives at the object (or at
the closed-side stop) and stops there before the hold takes over.
- The force is approximate.
1 N = 0.1 Nmis marked approximate in the SDK, and the real clamping force also depends on your finger geometry. Treatforce_nas a repeatable setting, not a calibrated measurement. - The jaws stay compliant. An external push back-drives them while the motor keeps pushing
with
force_n, and an emptygrasp()closes onto the mechanical stop at that force. That is the price of a force that does not drift when the workpiece moves. - Below about 3.7 N the approach speed becomes the limit, because one frame of travel at
grasp_speed_mm_salready spends the whole torque budget. Cap the travel time withgrasp_speed_mm_srather than asking for a force the mechanism's own friction absorbs.
The runaway watch. The hold frames carry no position error, so no position limit can act on
them; the only lever is removing the torque. Once the measured position is hold_runaway_rad
(0.05 rad, about 3 mm) past the closed-side calibrated stop, grasp() streams zero-torque
release frames and returns with GraspResult.travel_guard set. Only the closing side is checked,
because that is the side the feed-forward pushes.
This is a runaway detector, not an accuracy bound: raise hold_runaway_rad only if a jaw that is
genuinely holding something is being released. The watch is off until the config is calibrated.
An empty grasp() is the case that reaches it. The closing leg presses onto the closed-side
stop and the hold keeps pushing there, so a stop that is not where the calibration says — or one
that has been knocked out — lets the hold walk the jaws past it. The trip leaves them sitting
outside the calibrated travel, and the release frames leave the drive enabled but carrying no
torque: it holds nothing, but it is not off either. Get the jaws back inside the travel before
commanding anything else — disable() the motor, push them clear of the stop by hand, then
enable() and carry on. Do not just call grasp() again: the closing leg re-approaches the same
stop and the hold presses it again.
Need more force than the default? MotionConfig.force_n is 20.0 N (2.0 Nm), inside the
DM4310's 3 Nm rating. Its peak is 7 Nm and the protocol ceiling is 10 Nm.
The factory calibration shipped inside the SDK is the standard value, and a no-argument load reads it and nothing else:
from litegrip import LiteGrip
with LiteGrip("can0") as gripper:
gripper.load_calibration() # the shipped file, describing a normal-mount unit
gripper.enable()| Call | What it reads or writes |
|---|---|
load_calibration() → bool |
The shipped factory calibration, plus LITEGRIP_CALIB when that is set (the env var wins). |
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. |
load_calibration(template="normal") / load_template(name) → bool |
The limits of that template, whatever channel it is on. Strict: an unknown name raises CommandError. |
list_templates() → List[str] |
['normal', 'reverse'] — every name the SDK accepts, for a UI to offer. A module-level function, imported by name. |
default_calib_path(channel="can0") → str |
Where a calibration is saved: ~/.litegrip/<channel>_calibration.json, or $LITEGRIP_CALIB when set. |
save_calibration(path=None) → str |
Writes the current config to that path. |
zero() → CalibrationData |
Probes both mechanical stops, derives the scale, saves the result. |
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. |
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. |
write_zero() → WriteZeroResult |
Declares the current encoder angle as the motor's zero, in place. Writes the motor's flash and cannot be undone. |
What follows from that:
- A calibration measured on your machine is never picked up by accident.
zero()writes~/.litegrip/<channel>_calibration.json, and that file is read only when you name it:load_calibration(path=default_calib_path("can0")), orLITEGRIP_CALIB=/path/to/file.jsonfor every channel at once. Do not expect it to be picked up automatically. - Only
<channel>_calibration.jsonis recognized in that directory. A calibration file under any other name —can0_08_calibration.json, say — is not a candidate at all: not tried, not merged, not a later fallback stage. Whenever a no-argument load passes over a local file, the warning names it and repeats the two ways to use it. zero()drives the jaws onto both real stops, so keep hands and objects out of the travel range, and support the jaws if the gripper is mounted in a way they would fall. SetGripperConfig.max_stroke_mm(85.0, the shipped hardware) when your jaws are not 85 mm; it is the one per-unit measurement no calibration file carries, and it matters only when you re-probe.- The factory file declares
channel: can0, so acan1unit has no implicit source and fails loudly —load_calibration()returnsFalse— rather than silently adoptingcan0's direction. Give it a template, or azero()of its own named back as above.
The result types are dataclasses that also define __bool__, so if gripper.close(): works.
What it means, and what to check, is per action:
| Type | Fields | bool() is |
|---|---|---|
MoveResult |
ok, reached, stalled, state, target_rad, limit_rad, final_cmd_rad, steps, protected, released |
ok |
GraspResult |
ok, reached, stalled, state, target_rad, force_n, cycles, travel_guard |
ok |
EnableResult |
ok, state, tries |
ok |
Read ok. Three combinations look alarming and are correct:
close()on an empty gripper returnsok=True, stalled=True, reached=False. Success foropen/closeis stalling with the command capped at the calibrated limit and the jaw parked withinstop_tolof it.reachedis essentially alwaysFalsehere, because the target sits deliberately past the limit.grasp()on an object returnsstalled=True, reached=Falsewithok=True. Stopping short of the empty-jaw target is the point of gripping.grasp()on an empty gripper can returnreached=Truewhile the jaw sits about0.010 radshort oftarget_rad— the close-side stick-slip dead band, which is whyreach_toldefaults to0.02.target_radis the closed-side calibrated stop itself.
A fourth combination is a failure, and new in this version:
grasp()withok=False, reached=False, stalled=Falsemeans the closing leg ran out of frames without reaching the stop and without stalling — the jaw is still in free travel. The hold is not entered: its frames carry no position and no velocity term, so handing a moving jaw over to them leaves the feed-forward torque as the only thing acting on it, and the jaw accelerates until the runaway watch releases it. The call releases in place instead and reportscycles=0. Nothing was gripped. If you see this, the closing leg is being starved — a stale state read, or friction the mechanism's own budget cannot overcome.
Stalling far from the limit gives ok=False: something blocked the travel. The diagnostic fields
— final_cmd_rad, steps, state — are what you log when it is false.
The positioning calls read differently, because their target is a point inside the travel rather
than a stop: goto, goto_rad, move_to, home and move_at_speed* are ok only when the
jaws arrived and did not stall (reached and not stalled). Blocked halfway, they come back
ok=False, stalled=True — and with released=True, meaning the drive's force was taken off
(kp = kd = tau = 0) after the stall instead of being left pressing the part. protected=True is
the other way a move can end early: the in-travel torque guard (~7 N, see
MotionConfig.stop_torque_nm) tripped. That guard takes the force off on its own, so protected
leaves released at False, and it always forces ok=False.
The motion actions never print; they report through a progress callback, invoked with a
MoveProgress snapshot at the sampling rate.
def show(p):
print(f"[{p.phase}] {p.i}/{p.total_steps} cmd={p.cmd_rad:+.4f} "
f"pos={p.pos_rad:+.4f} tau={p.torque_nm:+.3f}")
gripper.open(progress=show)For grasp, p.phase is "hold" while force is being held, and p.total_steps is 0. zero()
is the exception to the rule: it prints its own probe progress to stdout.
Every tunable lives on one dataclass, MotionConfig. The defaults are the values validated on
real hardware. Override them per instance through gripper.motion_config, or per call.
from litegrip import LiteGrip, MotionConfig
with LiteGrip("can0") as gripper:
gripper.motion_config = MotionConfig(speed_mm_s=25.0, force_n=10.0)
gripper.close()
gripper.motion_config.force_n = 5.0 # also writable field by field
gripper.grasp()| Field | Default | Meaning |
|---|---|---|
speed_mm_s |
50.0 |
open / close jaw speed |
grasp_speed_mm_s |
50.0 |
closing speed of the grasp approach |
force_n |
20.0 |
default grasp force |
force_ramp_n_s |
20.0 |
rate the held force climbs to its setpoint, N/s |
hold_interval |
0.2 |
force-hold slice length, seconds |
hold_runaway_rad |
0.05 |
past the closed stop by this much, the force hold drops the torque and stops |
margin |
0.05 |
fraction of travel kept in reserve from the stop by the limit_target helper; no shipped call reaches it |
max_lead_mm |
4.0 |
cap on how far the command may lead the measured position, on moves that carry no force setpoint |
stop_lead_mm |
0.7 |
the same cap on the approach to a stop, so the pressing torque stays bounded |
press_safety |
0.9 |
fraction of the setpoint a force-carrying approach may spend |
reach_tol |
0.02 |
position tolerance for reached, radians |
stop_tol |
0.03 |
how close to the calibrated stop the jaw must park to count as pressed home |
settle_s |
0.3 |
time spent holding the target after the ramp |
Every other field — the stall window, the probe settings, the frame timing — is documented on the dataclass itself, with the reasoning for its default.
sleep_fn and monotonic_fn are the supported seams for simulation: the engine calls only those
two for time, so sleep_fn=lambda _: None collapses a whole ramp to no wall-clock time.
Two grippers can be linked so one follows the other. The master (leader) motor goes slack —
you push its jaws by hand — and publishes how far open it is. The slave (follower) drives its
own jaws to match. What travels over the wire is a normalized opening in [0, 1], not an angle,
so the two ends do not need the same calibration, mount, or zero point.
from litegrip import LiteGrip
# Leader: listen and publish this gripper's opening.
with LiteGrip("can0") as master:
master.load_calibration()
master.enable()
master.teleop_start("master") # zenoh, gripA, port 17448
# Follower: connect to the leader, ramp to the first frame, then follow.
with LiteGrip("can0") as slave:
slave.load_calibration()
slave.enable()
slave.teleop_start("slave", host="192.168.1.20")
while True:
print(slave.teleop_status()) # frames, openness, loop_hz, stale, ...| Method | What it does |
|---|---|
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. |
teleop_stop(timeout=2.0) → dict |
Stops the loop and leaves the jaws holding. |
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. |
The link is a point-to-point zenoh session, an optional dependency:
pip install 'litegrip[zenoh]'Both ends use mode="peer" with all discovery off, so the only way they find each other is an
explicit endpoint. link="udp" selects a plain-UDP fallback for a trusted LAN; it has no
authentication or encryption, so keep either one on a trusted network.
examples/teleop.py runs one end from the command line — one process per machine:
python3 examples/teleop.py --mode master --channel can0
python3 examples/teleop.py --mode slave --channel can0 --host 192.168.1.20To bench a single gripper, --fake-leader replaces the leader with a synthetic one on an
in-process bus. Put a rigid object between the jaws first — with nothing to press against, the
follower closes freely and the torque guard has nothing to demonstrate.
python3 examples/teleop.py --mode slave --channel can0 --fake-leader --torque-limit 1.0- The leader holds its jaws under gain until the follower reports ready, so the operator
cannot push the target out from under a follower that is still travelling.
ready_timeout_s(default10.0) bounds the wait;require_ready=Falserestores the immediate hand-back. - An optional torque guard releases a follower that is pressing too hard. Set
torque_limit_nmand it goes to zero stiffness and damping in place after 60 ms over the limit, then re-arms once the leader has reopened. The default is0— the guard is off unless you ask for it. Pick the value per machine: it depends on how fragile the part between the jaws is. - A follower that loses the leader holds its position, it does not go slack. After
watchdog_s(default0.2) without a fresh frame,stalegoes true and the jaws stay put. - The follower feeds the leader's velocity forward by differencing successive frames, so it
does not trail a moving leader.
dq_max=0disables that. - Non-finite frames are dropped, never clamped, on both ends.
- Teleop owns the CAN I/O while it runs: do not drive the gripper from the caller until
teleop_stop().
A motion you teach by hand can be captured once and repeated later. Recording puts the motor into
zero-gravity so you can push the jaws through the motion; replay streams the captured openings
back as command frames. What is stored is the normalized opening in [0, 1], exactly as teleop
sends it, so a trajectory taught on one gripper replays on another with a different mount or
calibration.
from litegrip import LiteGrip
with LiteGrip("can0") as gripper:
gripper.load_calibration()
gripper.enable()
taught = gripper.record(5.0) # 5 s of hand-teaching; the jaws are slack
taught.save("pick") # ~/.litegrip/trajectories/pick.lgt
gripper.play(taught) # repeat it| Method | Behaviour |
|---|---|
record(duration_s, rate_hz=100.0, zero_gravity=True) → Trajectory |
Blocking hand-teach. |
record_start(rate_hz=100.0, zero_gravity=True, max_samples=None) → dict |
Background recording. |
record_stop(allow_empty=False) → Trajectory |
Stops and returns the capture. |
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. |
play_start(trajectory, speed=1.0, kp=None, kd=None, loop=False, align=True) → dict |
Background replay. |
play_stop(timeout=2.0) → dict |
Stops a replay and leaves the gripper holding. |
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. |
python3 examples/trajectory.py --record 5 --save pick # hand-teach, then save
python3 examples/trajectory.py --list # no hardware needed
python3 examples/trajectory.py --play pick --repeat 3- Replay commands position, not force. The recorded torque is stored for diagnostics and never
fed forward, so a squeeze recorded against an object repeats as a position path. Follow the
replay with
grasp(force_n=...)if the grip force matters. record()is exclusive and the jaws are slack for its whole duration. Keep a hand on the gripper: nothing is holding the jaws, and whatever is between them will drop.- A capture that did not fill raises rather than returning a short recording as if it were whole.
- Record without zero-gravity when something else drives.
record_start(zero_gravity=False)only reads state, so the caller may run agrasp()or a move sequence from another thread and capture it. LITEGRIP_TRAJ_DIRmoves the trajectory directory. Recording, replay and teleop are mutually exclusive and raiseTrajectoryBusyError/TeleopBusyError;disconnect()stops whichever is running.
| Exception | Raised when |
|---|---|
LiteGripError |
Base class. str() appends the motor's code as [0xXXXX] when there is one. |
ConnectError |
The CAN interface is unavailable, or the motor never replies. |
CommError |
A CAN read or write failed mid-session. |
CANTimeoutError |
No reply within the timeout. |
CommandError |
The command was rejected: bad argument, uncalibrated config, unknown template, or a NaN / ±inf value. |
HardwareError |
The motor reported a fault code — for instance enable() never saw err == 1. |
NotInitializedError |
Not connected, or not enabled. |
TeleopError / TrajectoryError |
Teleop and recording / replay; each has busy, not-active and format subclasses. |
Errors are typed and never swallowed. describe_error(code) turns the motor's own error code into
text. The SDK logs through the standard logging module — the calibration it loaded, the
direction it derived, every guard that fired; attach a handler to the litegrip logger to see
them, and note that warnings reach stderr even with no handler configured.
A frame that fails to go out follows the table rather than one fixed message. The send reports
False and keeps the reason in gripper.last_send_error: a transport failure raises CommError
carrying that reason, and a gripper that is not connected or not enabled raises
NotInitializedError naming the missing step. When nothing recorded a reason — a coarse
send_mit_frame stub, say — the message says so instead of guessing.
| Interface | What it is for |
|---|---|
gripper.actions |
The GripperActions engine behind the high-level methods — the layer to build another driver on. |
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. |
read_param(rid, timeout_s=0.5) |
Reads a motor register. |
enter_zero_gravity(duration=0.0) / exit_zero_gravity() |
Slack the motor by hand, then take it back. |
clamp_to_travel(config, q) |
Clamps an angle into the calibrated travel — the rule open and close emit under. |
limit_target, press_target, work_limit_target |
The target math behind open, close and the work-stroke limit. |
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. |
closing_travel_excursion_rad(config, q) |
How far past the closed-side limit a position is. |
CALIB_TEMPLATES, UnitConversion, GripperParams, GripperGeometry, ErrorCode |
Template paths, unit conversions, motor limits, geometry and alarm codes. |
litegrip.can |
Raw SocketCAN transport and the DM protocol codec, for multi-motor setups. |
clamp_to_travel, limit_target, press_target, work_limit_target, force_approach_terms and
closing_travel_excursion_rad are module-level functions — import them from litegrip. They take
the config as their first argument rather than being called on the gripper.
| Symptom | What it means |
|---|---|
CommandError naming a calibration |
No calibration loaded for this channel. Call load_calibration(). |
ConnectError, or no status frame ever |
can0 is down, or the bitrate is not 1 Mbit/s. See step 2. |
HardwareError from enable() |
The motor never reported err == 1. Check power, then clear_fault(). |
| The jaws move the wrong way | The unit is reverse-mounted, not normal. Construct with mount="reverse". |
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. |
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. |
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. |
| 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. |
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. |
- Load a calibration before any motion. Until then every motion method raises
CommandError. This is the whole gate: on the placeholder defaults agoto(40.0)maps to+0.61 rad, past the real closed stop, and would press there. - Keep hands and objects out of the travel range during
open,close,graspandzero().openandclosestop on the calibrated limit;zero()probes the real mechanical stops under power. - A calibration that is wrong limits the travel; it cannot let the jaw run past it.
openandclosecap the emitted command at the calibrated extreme, so once the jaw is there the residual error is zero and the steady pressing torque is zero. - A held force keeps pushing. Nothing about a hold is position-bounded: the frames carry no
gains, so the jaws are compliant and the motor pushes with
force_nwherever they end up. The runaway watch is the only thing that stops it, and it is a backstop, not an operating limit. write_zero()is irreversible. It writes an encoder offset into the motor's flash.- Sustained pressing heats the coil.
openandclosehold against the limit for the settle phase every time, so check the coil temperature if they run back to back. - A non-finite value is refused, never clamped.
goto,goto_rad,move_at_speed*andgraspraiseCommandErrorfor a NaN or ±inf argument, and the CAN boundary refuses any such field of a frame before it can become bytes. disconnect()disables the motor by default, so a process that dies does not leave the motor holding torque. Setdisable_on_disconnect=Falseonly when something else keeps the gripper under control.
Every script below is runnable from the repository root and prints what it measured.
| Script | What it answers |
|---|---|
examples/stroke_test.py |
Does this unit work? Both ends of the travel, exit 0 on success. |
examples/grasp_test.py |
Does grasp() still do what it says — on clear jaws and on an object? |
examples/hold_force_test.py |
Does the held grip hold its force, or drift to a smaller one? |
examples/zero_closed.py |
Low-level CAN bench tool, one subcommand per operation: status, release, goto, zero, open, close, enable, disable. |
examples/teleop.py |
One end of a leader/follower link. |
examples/trajectory.py |
Hand-teach a motion, save it, play it back. |
The test suite is hardware-free: it drives the engine through a kinematic fake CAN in
tests/fake_can.py, so it runs anywhere without a gripper or a CAN interface.
python3 -m unittest discover -s tests -t tests -vCopyright © 2026 NEXFORM ROBOTICS. Licensed under the Apache License 2.0.