The formula #
$$F(t) = F_\text{open} + t\,(F_\text{closed} - F_\text{open}) = 50 + t\,(-50 - 50) = 50 - 100\,t$$FORCE_OPEN = 50.0
FORCE_CLOSED = -50.0
def _lerp(self, start_val, end_val, t):
return start_val + t * (end_val - start_val)
def apply_flexions(self, flexions):
for finger, flexion_amount in flexions.items():
target_force = self._lerp(FORCE_OPEN, FORCE_CLOSED, flexion_amount)
self.data.ctrl[self.motors[finger]] = target_forceThree parts of one line #
With \(t = 0.75\), a finger 75% closed:
- Range: \(F_\text{closed} - F_\text{open} = -100\) N — the whole span of the scale.
- Distance along it: \(0.75 \times -100 = -75\) N.
- Anchor: the scale starts at +50, not 0, so \(50 + (-75) = -25\) N.
| Flexion \(t\) | Force | Meaning |
|---|---|---|
| 0.00 | +50 N | open — the motor pushes the tendon |
| 0.50 | 0 N | no actuation |
| 1.00 | −50 N | fist — maximum pull |
Seen live #

Looking actuators up by name #
self.motors = {
"thumb": mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_ACTUATOR, "pull_thumb"),
"index": mujoco.mj_name2id(self.model, mujoco.mjtObj.mjOBJ_ACTUATOR, "pull_index"),
...
}data.ctrl is a flat array ordered by the XML — here pinky, ring, middle, index, thumb (IDs 0–4).
Resolving names once at startup means reordering or adding actuators never breaks the Python.
mj_name2id returns -1 — no exception. Then
data.ctrl[-1] writes to the last actuator: one finger’s command drives another finger. Assert
that every ID is >= 0 at startup.
On the ROS side, incoming names are filtered against the same dictionary, so an unknown finger name in a message is ignored instead of crashing the callback:
flexions = {name: pos for name, pos in zip(msg.name, msg.position) if name in self.sim.motors}A string that pushes #
A MuJoCo <motor> on a tendon applies force along the tendon’s length (gear = 1):
- negative → shortens the tendon → pulls, exactly what a servo winding a string does;
- positive → lengthens it → pushes.
Real strings can’t push. MuJoCo lets a tendon motor apply compression anyway, and this model uses that to drive fingers open at +50 N — which presses them about 3° past their open stop into the soft joint limit. On hardware the equivalent is a second, antagonistic tendon or a return spring. The model has extensor tendons, but they’re passive — and that has consequences (Part 11).
ctrlrange must agree with Python
#
<motor name="pull_index" tendon="tendon_flex_index" ctrlrange="-50 50" .../>The limits live in two files. MuJoCo clamps ctrl to ctrlrange (the model uses
autolimits="true"), so raising FORCE_CLOSED to −80 without editing the XML silently saturates at −50.