diff --git a/README.md b/README.md
index dc2f904..01e0468 100644
--- a/README.md
+++ b/README.md
@@ -221,7 +221,7 @@ UDP JSON,端口 `127.0.0.1:5005`。
| `gripper_position` | float | 夹爪指位置目标(m),各发送端有各自的换算方式(见下表) |
**夹爪控制链:**
-接收端将收到的 `gripper_position` 直接作为左右两个滑动关节的位置目标,并按各指裁剪到 `[0, 上限]`(USD 上限:`joint_left` 0.05 m,`joint_right` 0.0715 m)。接收端不做额外缩放。各发送端到 `gripper_position` 的换算如下:
+接收端将收到的 `gripper_position` 直接作为左右两个滑动关节的位置目标,并按各指裁剪到 `[0, 上限]`(USD 上限:两指均为 0.05 m;两指由同一电机通过单个小齿轮驱动,行程严格 1:1)。接收端不做额外缩放。各发送端到 `gripper_position` 的换算如下:
| 发送端 | 到 `gripper_position`(m)的换算 |
|------|------|
diff --git a/README_EN.md b/README_EN.md
index e9dca3b..51dbceb 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -221,7 +221,7 @@ UDP JSON on `127.0.0.1:5005`.
| `gripper_position` | float | Gripper finger position target (m); each sender computes it with its own mapping (see below) |
**Gripper control chain:**
-The receiver applies the received `gripper_position` directly as the position target of both prismatic finger joints, clipped per finger to `[0, upper limit]` (USD upper limits: `joint_left` 0.05 m, `joint_right` 0.0715 m). There is no extra scaling on the receiver side. The senders map their input to `gripper_position` as follows:
+The receiver applies the received `gripper_position` directly as the position target of both prismatic finger joints, clipped per finger to `[0, upper limit]` (USD upper limit: 0.05 m on both fingers; a single motor drives both through one pinion, so their travel is rigidly 1:1). There is no extra scaling on the receiver side. The senders map their input to `gripper_position` as follows:
| Sender | Mapping to `gripper_position` (m) |
|------|------|
diff --git a/README_ES.md b/README_ES.md
index 528d278..9a73d03 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -221,7 +221,7 @@ JSON sobre UDP en `127.0.0.1:5005`.
| `gripper_position` | float | Objetivo de posición de los dedos de la pinza (m); cada emisor lo calcula con su propia conversión (véase más abajo) |
**Cadena de control de la pinza:**
-El receptor aplica el `gripper_position` recibido directamente como objetivo de posición de las dos articulaciones prismáticas de los dedos, recortado por dedo a `[0, límite superior]` (límites superiores del USD: `joint_left` 0,05 m, `joint_right` 0,0715 m). El receptor no aplica ninguna escala adicional. Los emisores convierten su entrada a `gripper_position` de la siguiente manera:
+El receptor aplica el `gripper_position` recibido directamente como objetivo de posición de las dos articulaciones prismáticas de los dedos, recortado por dedo a `[0, límite superior]` (límite superior del USD: 0,05 m en ambos dedos; un solo motor mueve los dos a través de un único piñón, por lo que su recorrido es rígidamente 1:1). El receptor no aplica ninguna escala adicional. Los emisores convierten su entrada a `gripper_position` de la siguiente manera:
| Emisor | Conversión a `gripper_position` (m) |
|------|------|
diff --git a/mjcf/rebot_devarm/build_mjcf.py b/mjcf/rebot_devarm/build_mjcf.py
index 40e2e6b..03eb144 100644
--- a/mjcf/rebot_devarm/build_mjcf.py
+++ b/mjcf/rebot_devarm/build_mjcf.py
@@ -36,7 +36,15 @@
DYNAMICS = {
"rs06": ("5", "900", "60", "-36 36"),
"rs00": ("2", "120", "10", "-14 14"),
- "gripper": ("1", "100", "4", "-500 500"),
+ # Gripper gains come from the physical actuator, not from a tuned guess.
+ # Motor 7 (RobStride, limit_torque = 14 Nm read from firmware) drives both
+ # racks through one pinion of r = 7.353 mm/rad, so it presents
+ # K = kp_motor / r^2 = 50 / 0.007353^2 = 925 kN/m and 14 / 0.007353 = 1904 N
+ # at the finger. kp is capped at the stiffest value that stays stable at the
+ # solver step; kv gives a damping ratio of 1 against the 0.0752 kg finger.
+ # The previous (100, 4, 500) left the fingers at zeta = 0.685 -- underdamped,
+ # f_n = 5.45 Hz -- so they swung whenever the arm moved.
+ "gripper": ("1", "5000", "41.28", "-1904 1904"),
}
JOINT_CLASS = {
"joint1": "rs06",
@@ -271,6 +279,27 @@ def build(self):
}
ET.SubElement(actuator, "position", attrs)
+ # The two fingers are one mechanism, not two: a single motor drives two
+ # opposed racks through one pinion (hardware BOM: 02_Rack.step x2), so
+ # their travel is rigidly 1:1. Modelling them as independent prismatic
+ # joints let the jaws drift apart whenever the arm accelerated.
+ # solref uses the standard positive (timeconst, dampratio) form with
+ # timeconst = 2*dt. Direct constraint gains (negative solref) couple
+ # ~11x tighter in MuJoCo but make Newton's SolverMuJoCo diverge to NaN,
+ # and this model is published for both engines.
+ equality = ET.SubElement(root, "equality")
+ ET.SubElement(
+ equality,
+ "joint",
+ {
+ "joint1": "joint_left",
+ "joint2": "joint_right",
+ "polycoef": "0 1 0 0 0",
+ "solref": "0.004 1",
+ "solimp": "0.9999 0.99999 0.001 0.5 2",
+ },
+ )
+
keyframe = ET.SubElement(root, "keyframe")
for name, qpos in KEYFRAMES:
ET.SubElement(keyframe, "key", {"name": name, "qpos": qpos, "ctrl": qpos})
diff --git a/mjcf/rebot_devarm/rebot_devarm.xml b/mjcf/rebot_devarm/rebot_devarm.xml
index dec1360..cb85eb2 100644
--- a/mjcf/rebot_devarm/rebot_devarm.xml
+++ b/mjcf/rebot_devarm/rebot_devarm.xml
@@ -18,7 +18,7 @@