βš™οΈ
βš™οΈ Hardware πŸ”¬ Technical πŸ‘οΈ Needs Review Alexei Brown Β· 17 Apr 2026 Β· 18 min read

Why I Scored 20 Open-Source Robot Hands Against a MediaPipe Compatibility Matrix Before Printing Anything

Design viability scoring, InMoov i2 adaptation for STS3215 servos, closed-loop pose error feedback via dual MediaPipe instances, and tendon routing.

⚠️ Pending Before Publish

  • Single-finger prototype printed and tested?
  • MediaPipe detection rate on InMoov i2 print confirmed?
  • Actual latency numbers from feedback loop?
  • Closed-loop correction algorithm implemented?

The Viability Matrix

Before printing, 20 open-source robot hand designs were scored on a weighted 6-criterion matrix. Weights were set by project requirements, then each design scored 1–5:

Criterion Weight Rationale
Servo compatibility (STS3215 fit) 25% Drives motor selection
MediaPipe recognisability 25% Required for closed-loop feedback
Print difficulty (support use, bridges) 20% Single Bambu X1C, PETG
Part count 10% Assembly time
Human-like appearance 10% Affects tracking + aesthetics
Modification potential 10% Future arm integration

Why MediaPipe recognisability at 25%? If a second camera can't detect landmarks on the robot hand, there's no closed-loop feedback. The entire architecture requires the robot to be visible to the same model that tracks the operator. This is an unusual design constraint β€” most robot hand projects don't consider it at all.

Winning design: InMoov i2 (GaΓ«l Langevin, CC-BY-NC-3.0). Scores: servo compatibility 5/5 (with pocket depth modification), MediaPipe recognisability 5/5 (sculpted from real human anatomy β€” sits within model's training distribution), print difficulty 3/5, part count 3/5, appearance 5/5, modification 4/5. Weighted total: 4.35/5.

Second place: Dexterous Hand v2 (4.10/5) β€” loses on MediaPipe recognisability due to mechanical finger joints that deviate from human geometry.

InMoov i2 Adaptation for STS3215

The original InMoov i2 was designed for SG90 micro servos. The STS3215 is significantly larger (40Γ—20Γ—40mm vs 23Γ—12Γ—29mm). Adaptation required:

  • Servo pocket in wrist assembly: +1mm depth, new M3 mount positions
  • Horn adapter: STS3215 uses 25T spline, InMoov expects SG90 horn geometry β€” custom OpenSCAD adapter
  • Tendon spool: redesigned for 0.4mm Dyneema (original uses 0.3mm mono)

OpenSCAD files for the servo bed variants are in hardware/openscad/. Print PETG for the servo mounts (higher heat resistance near motor, better tolerance for M3 thread inserts).

Tendon Routing

Each finger: two 0.4mm Dyneema tendons (flexion and extension) running through 1.5mm ID PTFE tubes. PTFE reduces friction and eliminates binding around curves. Tendons anchor to servo horn spools and to the fingertip via integrated anchor holes in the InMoov STL.

Critical: Dyneema doesn't stretch under load. This means there's essentially no compliance in the tendon β€” a blocked finger stalls the motor immediately (rather than absorbing force in cable stretch). The STS3215 stall detection is therefore important for safety.

Finger β†’ PTFE tube β†’ palm channel β†’ wrist exit β†’ STS3215 spool
                          ↑
                   Conduit clips at each knuckle joint

STS3215 Serial Bus Architecture

The STS3215 uses UART TTL half-duplex protocol (Waveshare Serial Bus Servo Driver Board, USB-C). All 10 servos chain on a single cable. Each servo has a pre-assigned ID (0–9).

from ftservo import SMS_STS

port_handler = PortHandler('/dev/ttyUSB0')
port_handler.openPort()
port_handler.setBaudRate(1000000)  # 1Mbaud

sms_sts = SMS_STS(port_handler, PacketHandler())

def set_finger_angle(servo_id: int, angle_deg: float):
    # STS3215: 4096 steps / 360Β° β†’ steps/degree = 11.378
    position = int(angle_deg * 4096 / 360)
    sms_sts.WritePosEx(servo_id, position, speed=1000, acc=50)

def read_current_position(servo_id: int) -> float:
    pos, result, _ = sms_sts.ReadPos(servo_id)
    return pos * 360 / 4096  # convert back to degrees

Synchronised multi-servo write for natural hand poses:

# Broadcast position table to all servos simultaneously
positions = [angle_to_steps(angle) for angle in finger_angles]
ids = list(range(10))
sms_sts.SyncWritePosEx(ids, positions, speeds=[800]*10, accs=[50]*10)

Dual MediaPipe Feedback Architecture

Two cv2.VideoCapture instances, two MediaPipe Hands solutions, one comparison loop:

import mediapipe as mp
import cv2
import numpy as np

mp_hands = mp.solutions.hands

# Operator camera
hands_operator = mp_hands.Hands(max_num_hands=1, min_detection_confidence=0.7)
cap_operator = cv2.VideoCapture(0)

# Robot camera
hands_robot = mp_hands.Hands(max_num_hands=1, min_detection_confidence=0.5)
cap_robot = cv2.VideoCapture(2)  # v4l2loopback or second USB camera

def get_finger_angles(landmarks) -> dict:
    """Convert 21 MediaPipe landmarks to per-finger curl angles."""
    angles = {}
    finger_bases  = [1, 5, 9, 13, 17]   # MCP joint indices
    finger_mids   = [2, 6, 10, 14, 18]  # PIP joints
    finger_tips   = [4, 8, 12, 16, 20]  # DIP/tip
    names = ['thumb', 'index', 'middle', 'ring', 'pinky']

    for name, base, mid, tip in zip(names, finger_bases, finger_mids, finger_tips):
        v1 = np.array([landmarks[base].x - landmarks[mid].x,
                       landmarks[base].y - landmarks[mid].y])
        v2 = np.array([landmarks[tip].x  - landmarks[mid].x,
                       landmarks[tip].y  - landmarks[mid].y])
        cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)
        angles[name] = np.degrees(np.arccos(np.clip(cos_angle, -1, 1)))

    return angles

def compute_pose_error(op_angles: dict, robot_angles: dict) -> dict:
    return {k: abs(op_angles[k] - robot_angles.get(k, 0)) for k in op_angles}

The feedback dashboard runs in a separate thread, plotting per-finger error over time using matplotlib. When a finger's error exceeds ERROR_THRESHOLD_DEG for more than HOLD_FRAMES, an automatic correction is applied:

correction_factor = 1.0 + (error / 90.0) * 0.3  # up to 30% overcorrection
set_finger_angle(servo_id, target_angle * correction_factor)

Phase Plan

Phase Goal Status
0 Printer calibration βœ… Done
A Single index finger β€” print, track, validate πŸ”„ In progress
B Full hand assembly, tendon routing Pending
C Operator tracking + servo control loop Pending
D Dual-camera feedback, correction algorithm Pending
E Arm integration (SO-ARM100 compatible mount) Future

Key Open Question

Phase A's critical test: point Camera 2 at a freshly printed InMoov i2 index finger (PETG structure, skin-tone PLA phalanges, 0.15mm layer height) and run MediaPipe Hands. Do landmarks appear reliably?

If yes: the architecture is validated and the rest of the build is execution.
If no: investigate XTC-3D epoxy coat to smooth layer lines, try glossy PLA, or modify the finger segment geometry to more closely match the landmark model's expected proportions.

The viability report predicts success based on InMoov's anatomical design heritage. But real hardware confirmation is pending.

References

  • InMoov i2: GaΓ«l Langevin. inmoov.fr. License: CC-BY-NC-3.0.
  • MediaPipe Hands: Zhang, F. et al. (2020). MediaPipe Hands: On-device real-time hand tracking. Google.
  • Buchholz, B. et al. (1992). Hand anthropometry across industries. Ergonomics, 35(7–8), 861–876.
  • FEETECH STS3215 servo datasheet. ftservo-python-sdk. Compatible with HuggingFace LeRobot SO-ARM100.
  • Dyneema SK65 fishing line, 0.4mm. Dyneema BV.
Not reviewed locally