The vision layer sends a carefully normalized, smooth 0-to-1 signal. The simulated finger turns it into two states: open and closed. This is the most important thing to understand about the twin — and every number below was measured on the model in the repository. The measurement # The same flexion was applied to all five motors, the model simulated for 3 s from rest, and the steady-state joint angles recorded — 41 flexion values from 0 to 1.
Flexion Force Index MCP / PIP / DIP Thumb CMC / MP / IP 0.000 +50.0 N −3.3° / −3.2° / −3.3° −2.9° / −2.7° / −2.7° 0.250 +25.0 N −1.7° / −1.6° / −1.7° −1.5° / −1.3° / −1.4° 0.500 0.0 N 0.0° / 0.0° / 0.0° 0.0° / 0.0° / 0.0° 0.625 −12.5 N 91.1° / 91.0° / 91.0° 90.6° / 53.9° / 45.6° 1.000 −50.0 N 94.0° / 93.8° / 93.9° 92.3° / 54.2° / 46.0° Joint limits are 90°; values just above it are MuJoCo’s soft limit being pressed.
A raw CAD export gives you bodies, joints, meshes and sites — and a hand that does nothing. Four edits and one extra file turn it into a tendon-driven twin. One of those edits, it turns out, does nothing at all — which is worth understanding too. Which edits survive a re-export # flowchart TB subgraph generated["robot.xml — regenerated by onshape-to-robot"] D["① joint defaults
manual — re-apply"] T["② tendons + ③ contacts
auto-injected from tendons.xml"] B["bodies · joints · sites · meshes
generated"] A["④ actuators
manual — re-apply"] end S["⑤ scene.xml
floor · lights · skybox
never regenerated"] -->|"include robot.xml"| generated # Edit Lives in Survives re-export? ① Joint friction / armature / damping defaults robot.xml <default> ✘ re-apply ② Flexor + extensor spatial tendons tendons.xml → injected ✔ ③ Contact exclusions tendons.xml → injected ✔ ④ Five tendon motors robot.xml <actuator> ✘ re-apply ⑤ Environment scene.xml ✔ separate file tendons.xml is kept byte-identical to the <tendon> and <contact> region of robot.xml. Tune a tendon in robot.xml without copying it back and the next export silently reverts it.
① Joint defaults — stability # <default class="ros2-tendon-driven-hand-gazebo-digital-twin"> <joint frictionloss="0.001" armature="0.0001" damping="0.01"/> Attribute Value Role damping 0.01 N·m·s/rad stops a 2 g phalanx reaching absurd speed when 50 N yanks it armature 0.0001 kg·m² rotor-like inertia on each joint’s diagonal — conditions the solver for very light bodies frictionloss 0.001 N·m a small dry-friction dead-band so joints settle instead of creeping Phalanges weigh 1.8–5.4 g. Without these, tiny inertias under large tendon forces blow up the integrator — an earlier, larger revision of the model logged Nan, Inf or huge value in QACC at DOF 128. The simulation is unstable.
A fixed palm, five three-segment digits, five free-spinning servo horns, ten strings through seventy points, five motors. Every number here was read from the compiled model with MuJoCo 3.13.0. Open (+50 N) Neutral (0 N) Fist (−50 N) Index tendon only Middle tendon only Ring tendon only Pinky tendon only Thumb tendon only Previous Next At a glance # Quantity Value Notes Bodies 22 world, palm, 15 phalanges, 5 servo horns Joints / DoF 20 / 20 all hinges; no free joint — the palm is welded to the world Knuckle joints 15 passive, 90° limits Servo horn joints 5 unlimited and unactuated Geoms 87 43 visual + 43 collision meshes + floor Meshes 14 STL Sites 70 tendon via-points and anchors Tendons 10 5 flexor (actuated) + 5 extensor (passive) Actuators 5 <motor> on flexors, ±50 N Timestep / integrator 0.002 s / Euler 500 steps per simulated second Total mass 0.398 kg palm block 341.8 g The kinematic tree # flowchart TB W["world"] --> P["part_1 · palm + servo block
341.8 g · 20 sites"] P --> I1["part_2_4 · index proximal
4.6 g · index_mcp"] --> I2["part_3_4
1.8 g · index_pip"] --> I3["part_4_4
4.6 g · index_dip"] P --> M1["part_2_3 · middle
middle_mcp"] --> M2["part_3_3
middle_pip"] --> M3["part_4_3
middle_dip"] P --> R1["part_2_2 · ring
ring_mcp"] --> R2["part_3_2
ring_pip"] --> R3["part_4_2
ring_dip"] P --> K1["part_2 · pinky
pinky_mcp"] --> K2["part_3
pinky_pip"] --> K3["part_4
pinky_dip"] P --> T1["part_5 · thumb
5.4 g · thumb_cmc"] --> T2["part_6
2.4 g · thumb_mp"] --> T3["part_7
2.5 g · thumb_ip"] P --> H["servo_horn … servo_horn_5
0.3 g each"] Body names are CAD part names, not finger names. Repeated instances get _2, _3, … appended, so the pinky chain is part_2 → part_3 → part_4 and the index chain is part_2_4 → part_3_4 → part_4_4. Address joints, sites, tendons and actuators in code — never bodies. Joints and their signs # Joint Range (rad) Bend direction index_mcp, index_pip, index_dip [−1.571, 0] negative middle_mcp [−1.571, 0] negative middle_pip, middle_dip [0, +1.571] positive ring_mcp, ring_dip [−1.571, 0] negative ring_pip [0, +1.571] positive pinky_mcp, pinky_dip [−1.571, 0] negative pinky_pip [0, +1.571] positive thumb_cmc, thumb_mp, thumb_ip [−1.571, 0] negative servo_* × 5 unlimited — Bend direction is inconsistent, because each Onshape mate’s axis was exported as-is. Anything that reads joint angles must normalize per joint: