
Orcahand
- 1 installs
- Updated May 26, 2026
- broomva/orcahand-skill
OrcaHand is a Claude Code skill for the 17-DOF ORCA tendon-driven robotic hand that implements an agentic control-kernel Plant interface with typed schemas, safety shields and multi-rate control loops.
About
This skill is a full-stack control layer for the 17-DOF ORCA tendon-driven robotic hand, implementing an agentic control-kernel Plant interface with typed schemas and safety shields. It covers the full lifecycle: assembly, MuJoCo simulation, RL training, teleoperation and sim-to-real transfer. A developer uses it to build, simulate, teleoperate and train grasp policies for the hand through a shared physical/simulated interface.
- Full-stack skill for the 17-DOF ORCA tendon-driven robotic hand
- Typed state/action schemas, safety shields and multi-rate control loops
- Covers sim (MuJoCo), RL training, teleoperation and sim-to-real
Orcahand by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
orcahand capabilities & compatibility
- Capabilities
- robotic control · rl training · teleoperation · sim to real
- Use cases
- orchestration
- Platforms
- macOS
- Pricing
- Free
What orcahand says it does
Full-stack skill for the ORCA Hand — 17-DOF tendon-driven robotic hand (ETH Zurich).
Dual-backend: physical (`orca_core`) and simulated (`orca_sim`) share identical typed schemas.
**Emergency fallback**: `hand.disable_torque()` — hand goes limp.
npx skills add https://github.com/broomva/orcahand-skill --skill orcahandAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | May 26, 2026 |
| Repository | broomva/orcahand-skill ↗ |
What it does
Use it to build, simulate, teleoperate and train grasp policies for the ORCA robotic hand via a typed control-kernel interface.
Who is it for?
Building or assembling the OrcaHand, running orca_sim, teleoperating, training grasp policies and sim-to-real transfer
Skip if: General software product work; it is specific to the ORCA robotic hand hardware and control stack
When should I use this skill?
Building the hand, simulating in orca_sim, teleoperating, training grasp policies, or defining control-kernel Plant schemas
What you get
A shared physical and simulated Plant interface with safety shields for building, teleoperating and training the ORCA hand
- Plant control schemas
- safety shields
- trained grasp policy
By the numbers
- 17-DOF hand
- 5 safety shields
- 4 control loop rates (servo/mid/outer/meta)
Files
OrcaHand
Full-stack skill for the ORCA Hand — 17-DOF tendon-driven dexterous robotic hand. Compounds on agentic-control-kernel (plant/shield/trace) and bstack (governance).
Plant Interface
Dual-backend: physical (orca_core) and simulated (orca_sim) share identical typed schemas.
observe() → OrcaHandState (schemas/orcahand-state.schema.json)
measured:
joint_positions: {thumb_mcp, thumb_abd, thumb_pip, thumb_dip,
index_abd, index_mcp, index_pip,
middle_abd, middle_mcp, middle_pip,
ring_abd, ring_mcp, ring_pip,
pinky_abd, pinky_mcp, pinky_pip, wrist} # degrees
motor_currents: 17 motors, mA
motor_temperatures: 17 motors, celsius
tactile_readings: per-sensor [fx, fy, fz] in N (touch model only)
estimated:
grasp_state: "open" | "contact" | "secured" | "slipping"
context:
backend: "physical" | "simulated"
control_mode: "position" | "current" | "current_based_position"
torque_enabled: bool
apply(action) → ActuationResult (schemas/orcahand-action.schema.json)
directive_type: setpoint_update | experiment_request | mode_switch
target_controller: "orca_core" | "orca_sim"
payload:
joint_targets: {joint_name: degrees} # partial dict OK
num_steps: 25, step_size: 0.001s
grasp_type: "power" | "precision" | "pinch"
reset(seed?) → neutral position (physical) or env.reset (simulated)
constraints():
joint_roms: per-joint [min_deg, max_deg] from config.yaml
max_current: 200mA, max_temperature: 70°CSafety Shields
Implements kernel SafetyShield contract: filter(), feasible(), fallback().
| Shield | Invariant | filter() | feasible() |
|---|---|---|---|
| Joint ROM | Targets within bounds | Clamp to valid range | >= 1 joint can move |
| Max Current | < 200mA per motor | Disable torque, alert | Current below threshold |
| Temperature | < 70°C per motor | Disable torque, cooldown | All motors < 65°C |
| Velocity | < safe joint velocity | Reduce step_size | Velocity achievable |
| Tactile | Force < sensor max | Release grasp, back off | Force within range |
Emergency fallback: hand.disable_torque() — hand goes limp. Safe due to popping joints. Cascade: filter() -> feasible() -> if infeasible -> emergency fallback + alert outer loop.
Multi-Rate Loop Mapping
SERVO (ms) Dynamixel PID firmware. Agent never touches this.
|
MID (10-100ms) Retargeter @ 30Hz / RL policy @ 60Hz / Replay @ 60Hz
Safety shields run HERE: ROM clamp + current check per frame
|
OUTER (sec) LLM supervisory: grasp strategy, mode switch, task goals
Outputs ControlDirective -> mid-loop controller
|
META (min-day) EGRI: problem-spec -> train in sim -> evaluate -> promote to physical
Runs on remote GPU, validated on local macOSQuick Starts
- Build a hand: Read references/hardware-build.md — BOM, 3D printing, Dynamixel sourcing, assembly, wiring
- Simulate: Read references/simulation-setup.md —
pip install orca_sim, MuJoCo on macOS, environment catalog - Teleoperate: Read references/teleoperation.md — AVP / Rokoko / MediaPipe -> retargeter -> hand
- Train RL policies: Read references/rl-training.md — local CPU/MPS or remote GPU, reward design
- Improve controllers: Read references/egri-controller-loop.md — EGRI problem-spec, evaluator, promotion
Scope Router
Load the relevant reference based on user intent. Max 3 references at once.
| Intent | Keywords | Reference |
|---|---|---|
| Build hand | build, print, assemble, BOM, servo, wire | hardware-build.md |
| Calibrate | calibrate, tension, neutral, config.yaml, serial | calibration-pipeline.md |
| Simulate | simulate, mujoco, orca_sim, gymnasium, render | simulation-setup.md |
| Train | train, RL, PPO, SAC, reward, policy, GPU | rl-training.md |
| Teleoperate | teleoperate, retarget, vision pro, rokoko, mediapipe | teleoperation.md |
| Sim-to-real | sim-to-real, domain randomization, joint reorder | sim-to-real.md |
| EGRI | improve, optimize, EGRI, problem-spec, evaluator | egri-controller-loop.md |
| API | OrcaHand class, set_joint_pos, REST API, joint names | api-reference.md |
| Install | install, clone, dependencies, which repo, version | dependency-graph.md |
| Debug | servo not found, segfault, drift, error, stuck | troubleshooting.md |
Scripts
scripts/orcahand_init.py— Bootstrap workspace: clone repos, install deps, detect serial, generate.control/plant.yamlscripts/orcahand_check.py— Health check for bstack integration (JSON output, exit 0/1)
Schemas
schemas/orcahand-state.schema.json— extends kernelstate.schema.jsonschemas/orcahand-action.schema.json— extends kernelaction.schema.jsonschemas/orcahand-trace.schema.json— extends kerneltrace.schema.json
Templates
assets/templates/problem-spec.orcahand.yaml— EGRI template for grasp optimizationassets/templates/config.orcahand.yaml— starter config for new hand builds
# OrcaHand starter configuration
# Copy this to your workspace and adjust for your hand build.
# Maps to orca_core/models/<model_name>/config.yaml
hand:
model: orcahand_v1_right # or orcahand_v2_right, orcahand_v1_left
version: v1
serial:
port: auto # auto-detect, or /dev/tty.usbserial-XXXXX (macOS), /dev/ttyUSB0 (Linux)
baudrate: 3000000
motors:
type: dynamixel # or feetech
model: XC330-T288
count: 17
max_current: 200 # mA
control_mode: current_based_position # recommended: position, current, current_based_position
# Joint naming convention: {finger}_{joint_type}
# Fingers: thumb, index, middle, ring, pinky, wrist
# Joint types: mcp (metacarpophalangeal), pip (proximal interphalangeal),
# dip (distal interphalangeal), abd (abduction)
joints:
- thumb_mcp
- thumb_abd
- thumb_pip
- thumb_dip
- index_abd
- index_mcp
- index_pip
- middle_abd
- middle_mcp
- middle_pip
- ring_abd
- ring_mcp
- ring_pip
- pinky_abd
- pinky_mcp
- pinky_pip
- wrist
safety:
max_temperature: 70 # celsius, disable torque above this
emergency_fallback: disable_all_torque
simulation:
environment: OrcaHandRight-v2
render_mode: human # human (interactive) or rgb_array (headless)
# OrcaHand EGRI Problem Spec
# Extends agentic-control-kernel problem-spec.control.yaml
# Fill in and adjust values for your specific optimization target.
objective:
description: "Improve grasp success rate for target object set"
metric: "grasp_success_rate"
target: 0.85
direction: maximize
plant:
type: physical
name: orcahand
shield_type: rule-based # ROM/current/temp shields are rule-based, not CBF-QP
dual_backend: true # sim and physical share same Plant interface
constraints:
- joint_roms from config.yaml (never exceed)
- max_current 200mA per motor
- max_temperature 70°C per motor
- human approval required before physical deployment
artifacts:
- name: grasp_policy
type: rl_policy
path: checkpoints/grasp_policy_latest.pt
backend: orca_sim.OrcaHandRightCubeOrientation
- name: retargeter_config
type: config
path: orca_retargeter/models/orcahand_v1_right/retargeter.yaml
evaluator:
outputs:
sim_success_rate:
metric: terminated_success / total_episodes
threshold: 0.85
episodes: 100
sim_to_real_gap:
metric: abs(sim_success - real_success)
threshold: 0.15
episodes: 20
execution:
backend: simulator # Training on remote GPU
validation: physical # Validation on local macOS + real hand
budget:
max_trials: 50
max_hours: 24
promotion:
gate: "sim_success_rate > 0.85 AND sim_to_real_gap < 0.15"
action: deploy_to_physical
rollback: revert_to_previous_checkpoint
approval: manual # Human approves before deploying to physical hand
autonomy:
level: supervised # LLM proposes, human approves physical deployment
escalation: alert_on_shield_intervention
search:
strategy: curriculum # Progressive difficulty in sim environments
hyperparams:
- learning_rate
- reward_scale
- domain_randomization_range
ledger:
backend: lago # Append-only event journal for all trials
event_kind: "Custom" # EGRI events stored as EventKind::Custom
domain:
hardware: "ORCA Hand v1/v2, 17-DOF, Dynamixel XC330-T288"
simulation: "MuJoCo via orca_sim, Gymnasium interface"
retargeting: "orca_retargeter, pytorch_kinematics FK chain"
meta:
created_by: skill-creator
version: "1.0"
compounds_on:
- agentic-control-kernel
- bstack
API Reference
Table of Contents
- OrcaHand Class
- MockOrcaHand
- FastAPI REST Endpoints
- Joint Naming Convention
- Config File Schemas
OrcaHand Class
Main controller class in orca_core/core.py:
from orca_core import OrcaHand
hand = OrcaHand("orca_core/models/orcahand_v1_right")Connection
hand.connect() # Open serial connection
hand.disconnect() # Close serial connectionTorque Control
hand.enable_torque() # Enable all motors
hand.disable_torque() # Disable all motors (hand goes limp)Joint Control
# Move specific joints (partial dict OK)
hand.set_joint_pos(
{"index_mcp": 90, "middle_pip": 30},
num_steps=25, # interpolation steps (smoother = more steps)
step_size=0.001 # seconds between steps
)
# Read current joint positions
positions = hand.get_joint_pos() # Returns dict: {joint_name: degrees}Motor Telemetry
motor_pos = hand.get_motor_pos() # Raw Dynamixel positions
motor_cur = hand.get_motor_current() # Current draw in mA
motor_tmp = hand.get_motor_temp() # Temperature in celsiusCalibration
hand.calibrate() # Run auto-calibration sequence
hand.set_neutral_position() # Move to neutral (open hand)
hand.set_zero_position() # Move to zero positionControl Mode
hand.set_control_mode("position") # Pure position control
hand.set_control_mode("current_based_position") # Recommended: position + current limit
hand.set_max_current(200) # Set current limit in mAMockOrcaHand
For testing without hardware:
from orca_core import MockOrcaHand
hand = MockOrcaHand("orca_core/models/orcahand_v1_right")
hand.connect() # No serial required
hand.set_joint_pos({"thumb_mcp": 45}) # Simulated, no hardwareSame API as OrcaHand. Useful for unit tests and CI.
FastAPI REST Endpoints
Start server: python -m orca_core.api.api (port 8000)
| Method | Endpoint | Description |
|---|---|---|
| POST | /connect | Open serial connection |
| POST | /disconnect | Close connection |
| GET | /status | Connection + torque status |
| POST | /torque/enable | Enable torque |
| POST | /torque/disable | Disable torque |
| GET | /joints/position | Get all joint positions |
| POST | /joints/position | Set joint positions (JSON body: {joint_targets, num_steps, step_size}) |
| GET | /motors/position | Get raw motor positions |
| POST | /calibrate | Run calibration |
Joint Naming Convention
Format: {finger}_{joint_type}
| Finger | Joints | Total |
|---|---|---|
| thumb | mcp, abd, pip, dip | 4 |
| index | abd, mcp, pip | 3 |
| middle | abd, mcp, pip | 3 |
| ring | abd, mcp, pip | 3 |
| pinky | abd, mcp, pip | 3 |
| wrist | (single joint) | 1 |
| Total | 17 |
Joint types:
- mcp: Metacarpophalangeal (knuckle)
- pip: Proximal interphalangeal (first bend)
- dip: Distal interphalangeal (fingertip bend, thumb only)
- abd: Abduction (side-to-side spread)
Config File Schemas
config.yaml (per model)
Primary configuration. Contains: baudrate, port, max_current, control_mode, motor_ids (17), joint_ids (17), joint_to_motor_map (with sign for tendon inversion), joint_roms, neutral_position, calibration_sequence.
calibration.yaml (per model)
Written by calibrate.py. Contains: calibrated (bool), timestamp, motor_limits, joint_to_motor_ratios.
hand_scheme.yaml (retargeter)
Kinematic structure. Contains: gc_tendons, finger_to_tip, finger_to_base, gc_limits_lower, gc_limits_upper, wrist_name.
retargeter.yaml (retargeter)
Optimization tuning. Contains: lr, use_scalar_distance_palm, loss_coeffs, mano_adjustments, joint_regularizers.
Calibration Pipeline
Table of Contents
- Serial Port Detection
- 3-Step Pipeline
- config.yaml Schema
- calibration.yaml Output
- When to Re-Calibrate
- Troubleshooting
Serial Port Detection
# macOS
ls /dev/tty.usbserial-*
# Linux
ls /dev/ttyUSB*The U2D2 adapter uses FTDI VID 0x0403. If multiple serial devices, check with:
# macOS
system_profiler SPUSBDataType | grep -A5 "U2D2"Update config.yaml with the detected port path.
3-Step Pipeline
Run from the orca_core/ directory. Each script takes the model path as argument.
Step 1: Tension Tendons
python scripts/tension.py orca_core/models/orcahand_v1_rightApplies light current to each motor to take up tendon slack. Hold the hand steady during this step.
Step 2: Auto-Calibrate
python scripts/calibrate.py orca_core/models/orcahand_v1_rightRuns a 28-step flex/extend sequence across all joints. Records motor positions at extreme positions to compute joint_to_motor_ratios. Writes results to calibration.yaml.
Important: The hand will move through its full range of motion. Ensure nothing is blocking the fingers.
Step 3: Move to Neutral
python scripts/neutral.py orca_core/models/orcahand_v1_rightMoves all joints to the neutral (open hand) position defined in config.yaml. Verifies calibration is correct visually.
config.yaml Schema
baudrate: 3000000
port: /dev/tty.usbserial-XXXXX
max_current: 200 # mA
control_mode: 5 # current_based_position (recommended)
motor_ids: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
joint_ids: [thumb_mcp, thumb_abd, thumb_pip, thumb_dip,
index_abd, index_mcp, index_pip,
middle_abd, middle_mcp, middle_pip,
ring_abd, ring_mcp, ring_pip,
pinky_abd, pinky_mcp, pinky_pip, wrist]
joint_to_motor_map:
thumb_mcp: {motor: 0, sign: 1}
thumb_abd: {motor: 1, sign: -1} # negative sign = inverted tendon
# ... (17 mappings total)
joint_roms:
thumb_mcp: [0, 90]
thumb_abd: [-30, 30]
# ... (17 joint ROMs)
neutral_position:
thumb_mcp: 0
thumb_abd: 0
# ... (17 neutral positions)calibration.yaml Output
Written by calibrate.py:
calibrated: true
timestamp: 2026-03-25T10:00:00
motor_limits:
0: {min: 1024, max: 3072} # raw Dynamixel positions
# ... (17 motors)
joint_to_motor_ratios:
thumb_mcp: 22.75 # degrees per Dynamixel unit
# ... (17 ratios)When to Re-Calibrate
- After replacing a tendon
- After significant use (>1000 grasp cycles)
- If grasps feel imprecise or joints drift
- After any mechanical repair
- Weekly if running experiments (calibration drift is real)
Troubleshooting
Motor not found: Check U2D2 connection, verify baudrate matches config, try scripts/check_motor.py.
Calibration drift: Tendons stretch over time. Re-run tension.py + calibrate.py. If persistent, replace the tendon.
Motor overheating: Reduce max_current in config.yaml. Check for tendon friction at routing points.
Dependency Graph
Table of Contents
- Repo Relationships
- Version Matrix
- Install Order
- Python Requirements
- Known Conflicts
Repo Relationships
orcahand_description
(URDF/MJCF models)
/ | \
/ | \
orca_sim orca_retargeter rwr_system
(Gymnasium (Teleoperation (ROS2 pipeline)
+ MuJoCo) retargeting)
| | |
v v v
[RL training] [Human input] [Deployment]
(AVP/Rokoko)
|
v
orca_core
(Hardware control)
|
v
[Physical ORCA Hand]Version Matrix
| Repo | Latest | Python | Key Dependencies |
|---|---|---|---|
| orcahand_description | main | N/A | mujoco (viewing only) |
| orca_core | 0.2.1 | 3.10+ | dynamixel-sdk >=3.7.31, fastapi, numpy >=2 |
| orca_sim | 0.1.0 | 3.10+ | gymnasium >=0.29, mujoco >=3.1, numpy >=1.26 |
| orca_retargeter | 0.1.0 | 3.10+ | torch ==2.2.0, pytorch-kinematics >=0.7.5, numpy <2 |
| rwr_system | 0.1.0 | 3.10+ | ROS2, colcon, h5py, depthai, mediapipe |
Install Order
Must be installed in dependency order:
1. orcahand_description — no deps, just git clone 2. orca_sim — depends on orcahand_description for MJCF models 3. orca_core — independent, but install after sim for testing 4. orca_retargeter — depends on orcahand_description for URDF 5. rwr_system — depends on ROS2, optional
Use scripts/orcahand_init.py to automate this.
Python Requirements
- Python 3.10+ (all repos)
- MuJoCo:
pip install mujoco(3.1+) - PyTorch: 2.2.0 specifically for orca_retargeter
- ROS2: Required only for rwr_system (Humble or newer)
Known Conflicts
numpy version conflict
orca_corerequiresnumpy >=2.2.6orca_retargeterrequiresnumpy <2- Resolution: Use separate virtual environments, or install in a single env with
numpy==1.26.4(works for both with a warning from orca_core)
torch version pinning
orca_retargeterpinstorch ==2.2.0- If you need a newer torch for training, use a separate venv for training vs retargeting
Recommended setup
# Option A: Single venv (some version warnings)
python -m venv .venv && source .venv/bin/activate
pip install numpy==1.26.4 # compromise version
pip install -e orca_core/ -e orca_sim/
# Option B: Separate venvs (cleaner, recommended)
python -m venv .venv-sim && .venv-sim/bin/pip install -e orca_sim/
python -m venv .venv-core && .venv-core/bin/pip install -e orca_core/
python -m venv .venv-retarget && .venv-retarget/bin/pip install -e orca_retargeter/EGRI Controller Improvement Loop
Table of Contents
- EGRI Overview for OrcaHand
- Problem-Spec Template
- Evaluator Design
- Promotion Policy
- Integration with autoany
- Running an EGRI Cycle
EGRI Overview for OrcaHand
EGRI (Evaluator-Governed Recursive Improvement) applied to the OrcaHand:
Problem-spec → Train in sim (remote GPU) → Evaluate (sim metrics)
↑ ↓
Update artifact Gate: pass/fail?
↑ ↓
Rollback if failed Deploy to physical (human approval)
↓
Evaluate (real metrics)
↓
Update sim-to-real gapMutable artifacts (what EGRI improves):
- RL policy weights (
.ptfiles) - Retargeter configuration (
retargeter.yamlloss_coeffs, lr, mano_adjustments) - Grasp primitive parameters (pre-defined joint angle sequences)
Problem-Spec Template
Use assets/templates/problem-spec.orcahand.yaml as your starting point. Key sections to customize:
- objective.metric: What you're optimizing (grasp_success_rate, reorientation_accuracy, grasp_stability_duration)
- objective.target: Threshold for success (0.85 = 85%)
- artifacts: Which files the EGRI loop can modify
- evaluator.outputs: Metrics to compute and their thresholds
- execution.budget: Time and compute limits
Evaluator Design
Sim evaluator (runs on every iteration):
def evaluate_sim(policy, env, n_episodes=100):
successes = 0
for _ in range(n_episodes):
obs, info = env.reset()
done = False
while not done:
action, _ = policy.predict(obs, deterministic=True)
obs, reward, terminated, truncated, info = env.step(action)
done = terminated or truncated
if info.get("success", False):
successes += 1
return {"sim_success_rate": successes / n_episodes}Real evaluator (runs only after sim gate passes):
def evaluate_real(policy, hand, n_grasps=20):
# Requires human supervision
successes = 0
for i in range(n_grasps):
print(f"Trial {i+1}/{n_grasps} — place object and press Enter")
input()
# Execute policy...
result = input("Success? (y/n): ")
if result.lower() == "y":
successes += 1
return {"real_success_rate": successes / n_grasps}Composite evaluator:
def evaluate(sim_results, real_results):
gap = abs(sim_results["sim_success_rate"] - real_results["real_success_rate"])
return {
"sim_success_rate": sim_results["sim_success_rate"],
"real_success_rate": real_results["real_success_rate"],
"sim_to_real_gap": gap,
"promoted": sim_results["sim_success_rate"] > 0.85 and gap < 0.15,
}Promotion Policy
IF sim_success_rate > 0.85:
→ Request human approval for physical trial
IF human_approved:
→ Run real evaluator (20 grasps, supervised)
IF sim_to_real_gap < 0.15:
→ PROMOTE: save as production policy
ELSE:
→ ROLLBACK: revert to previous checkpoint
→ Increase domain randomization, re-train
ELSE:
→ HOLD: keep training in sim
ELSE:
→ CONTINUE: more training iterationsHuman-in-the-loop: Physical deployment always requires human approval. This is enforced by the approval: manual field in the problem-spec.
Integration with autoany
Wire into the existing autoany EGRI harness:
# From the autoany workspace
autoany egri run --problem-spec path/to/problem-spec.orcahand.yamlThe autoany harness handles:
- Iteration management (train → evaluate → decide)
- Artifact versioning (checkpoint management)
- Ledger persistence (via Lago)
- Budget enforcement (max_trials, max_hours)
Running an EGRI Cycle
1. Setup: Copy problem-spec template, customize objective and budget 2. Train: python train.py --config problem-spec.orcahand.yaml 3. Evaluate (sim): Automatic after training completes 4. Gate check: Script reports pass/fail 5. Deploy (if passed): Human approves, run real-world trials 6. Record: All results logged to Lago trace entries 7. Iterate: Adjust hyperparameters, domain randomization, re-run
Hardware Build Guide
Table of Contents
- Bill of Materials
- 3D Printing
- Sourcing Servos
- Assembly
- Wiring
- First Power-On
Bill of Materials
| Component | Qty | Approx Cost | Source |
|---|---|---|---|
| Dynamixel XC330-T288 servo | 17 | $1,190 | ROBOTIS |
| U2D2 USB-to-serial adapter | 1 | $35 | ROBOTIS |
| U2D2 Power Hub | 1 | $20 | ROBOTIS |
| 3D printed structural parts | 1 set | ~$50 filament | Self-print |
| Tendons (Dyneema fishing line) | ~5m | $10 | Amazon |
| M2/M3 screws + hardware | assorted | $15 | Amazon |
| 12V 5A power supply | 1 | $25 | Amazon |
| Total (self-build) | ~$1,345 |
Budget alternative: Feetech SCS0009 servos (~$8 each vs $70 for Dynamixel). Lower quality, less precise, but functional. Use FeetechClient in orca_core.
Pre-assembled: $3,500 from orcahand.com or ROBOTIS (orcahand standard).
3D Printing
STL files: orcahand_description/ repo, under v1/meshes/ or v2/meshes/.
Recommended settings:
- Material: PLA or PETG (PETG preferred for durability)
- Layer height: 0.2mm
- Infill: 30-40% (structural parts), 20% (cosmetic)
- Supports: required for finger segments and palm
- Estimated print time: ~24-30 hours total
Key parts: main tower (~15K faces), base (~2K), finger segments, skin covers.
Popping joints: Print these at 100% infill — they must survive repeated dislocation/relocation cycles without deforming.
Sourcing Servos
- ROBOTIS direct: Best price, reliable shipping. Order XC330-T288-T (TTL protocol).
- Amazon/ROBOTIS resellers: Faster shipping, slight markup.
- Feetech alternative: SCS0009 — budget option, requires
FeetechClientdriver in orca_core.
All 17 servos must be the same model. Do not mix Dynamixel and Feetech.
Assembly
Official video guides available at orcahand.com.
Key steps: 1. Assemble finger segments — thread tendons through channels 2. Mount servos into the palm/tower structure 3. Route tendons through low-friction channels at joint rotation centers 4. Attach finger assemblies to palm 5. Wire servo daisy-chain (TTL bus) 6. Connect U2D2 adapter
Tendon routing: Route tendons through joint rotation centers to minimize friction. This is the most critical assembly step — poor routing causes tendon wear and calibration drift.
Assembly time: ~6-8 hours for first build, ~3-4 hours with experience.
Wiring
12V PSU → U2D2 Power Hub → Servo daisy-chain (TTL bus)
Motor 0 → Motor 1 → ... → Motor 16
|
U2D2 USB adapter ← ─────────────── TTL data bus ─────────┘
|
USB to Mac/PC- Protocol: Dynamixel Protocol 2.0 (TTL)
- Baudrate: 3,000,000 (set via
scripts/configure_motor_chain.py) - Motor IDs: 0-16, pre-configured or set with
configure_motor_chain.py
First Power-On
1. Connect U2D2 USB to your computer 2. Detect serial port: ls /dev/tty.usbserial-* (macOS) or ls /dev/ttyUSB* (Linux) 3. Update config.yaml with your serial port 4. Test motor detection: python scripts/check_motor.py orca_core/models/orcahand_v1_right 5. Proceed to calibration: see calibration-pipeline.md
RL Training
Table of Contents
- Local Training (macOS)
- Remote GPU Setup
- Framework Choice
- Reward Design
- Curriculum Training
- Checkpointing
Local Training (macOS)
Good for prototyping small policies. Apple Silicon MPS backend available for PyTorch.
pip install stable-baselines3 torchfrom stable_baselines3 import PPO
from orca_sim import OrcaHandRightCubeOrientation
env = OrcaHandRightCubeOrientation(version="v2", render_mode="rgb_array")
model = PPO("MlpPolicy", env, verbose=1, device="mps") # or "cpu"
model.learn(total_timesteps=100_000)
model.save("checkpoints/grasp_policy_v1")Limitations: CPU/MPS is 5-20x slower than GPU for training. Fine for <500K timesteps, use remote GPU for serious training.
Remote GPU Setup
1. Provision a GPU box (Lambda, Vast.ai, RunPod, etc.) with CUDA + PyTorch 2. Mirror the environment:
ssh gpu-box "pip install orca_sim stable-baselines3 torch"3. Start training:
ssh gpu-box "python train.py --timesteps 10_000_000 --device cuda"4. Sync checkpoints back:
rsync -av gpu-box:~/checkpoints/ ./checkpoints/Alternative: Use a Jupyter notebook on Colab/Lambda for interactive development.
Framework Choice
| Framework | Strengths | Best for |
|---|---|---|
| Stable-Baselines3 | Quick start, well-tested PPO/SAC/TD3 | First experiments |
| CleanRL | Single-file implementations, transparent | Understanding algorithms |
| Custom PyTorch | Full control, custom architectures | Research |
Recommended: Start with SB3 PPO, move to CleanRL or custom if you need specific modifications.
Reward Design
CubeOrientation task (built-in):
# orca_sim/task_envs.py reward logic:
# reward = alignment_to_target + lift_bonus - drop_penalty
# terminated on: success (15° tolerance) or dropCustom grasp rewards — design principles:
- Alignment: Reward reducing angle between current and target object pose
- Stability: Reward maintaining grasp over multiple timesteps
- Efficiency: Penalize energy (motor current) usage
- Safety: Large penalty for exceeding joint ROM or temperature limits
def compute_reward(self):
alignment = self._compute_alignment() # 0-1
stability = self._grasp_duration / 100 # 0-1
energy = -0.01 * self._total_current # penalty
return alignment + 0.5 * stability + energyCurriculum Training
Progressive difficulty for complex manipulation tasks:
1. Stage 1: Large, easy-to-grasp objects (cube, sphere), wide success tolerance 2. Stage 2: Smaller objects, tighter tolerance, varied starting positions 3. Stage 3: In-hand manipulation (rotation, reorientation) 4. Stage 4: Domain randomization (friction, mass, motor delay)
Advance to next stage when success rate > 80% for 1000 episodes.
Checkpointing
# Save during training
model.save(f"checkpoints/grasp_policy_step_{step}")
# Load for evaluation
model = PPO.load("checkpoints/grasp_policy_v1")
obs, info = env.reset()
action, _ = model.predict(obs, deterministic=True)Best practice: Save checkpoints every 100K steps. Keep the last 5 + the best-performing one.
Evaluation:
from stable_baselines3.common.evaluation import evaluate_policy
mean_reward, std_reward = evaluate_policy(model, env, n_eval_episodes=100)
print(f"Success rate proxy: {mean_reward:.2f} +/- {std_reward:.2f}")Sim-to-Real Transfer
Table of Contents
- Joint Reordering Map
- Domain Randomization
- Action Scaling
- Validation Protocol
- Common Failure Modes
- Zero-Shot vs Fine-Tuned
Joint Reordering Map
Simulation joint ordering differs from physical motor ordering. Apply this map when deploying sim-trained policies to real hardware:
# Sim index → Real index (from rwr_system)
SIM_TO_REAL = [0, 13, 14, 15, 16, 10, 11, 12, 4, 5, 6, 1, 2, 3, 7, 8, 9]
def reorder_sim_to_real(sim_actions):
"""Reorder action vector from sim ordering to real motor ordering."""
return [sim_actions[i] for i in SIM_TO_REAL]Why they differ: Simulation models joints by kinematic chain (thumb → index → middle → ring → pinky), while the physical motor IDs follow the wiring daisy-chain topology.
Domain Randomization
Randomize these parameters during training to improve transfer robustness:
| Parameter | Sim Default | Randomization Range | Why |
|---|---|---|---|
| Joint friction | 0.01 | [0.005, 0.05] | Real tendons have variable friction |
| Tendon stiffness | 100 | [50, 200] | Tendon tension varies with age |
| Motor delay | 0ms | [0, 20ms] | Serial bus latency |
| Observation noise | 0 | N(0, 0.5°) | Encoder quantization |
| Object mass | varies | [0.5x, 2x] | Grasp diverse objects |
| Object friction | 1.0 | [0.3, 1.5] | Surface variability |
Apply in your training script:
# Randomize at episode reset
env.model.geom_friction[:] *= np.random.uniform(0.5, 1.5)
env.model.dof_damping[:] *= np.random.uniform(0.5, 2.0)Action Scaling
Simulation actions are in actuator control range. Convert to degrees for OrcaHand.set_joint_pos():
def sim_action_to_degrees(action, joint_roms):
"""Scale normalized [-1, 1] action to joint ROM in degrees."""
degrees = {}
for i, (joint, (lo, hi)) in enumerate(joint_roms.items()):
degrees[joint] = lo + (action[i] + 1) / 2 * (hi - lo)
return degreesValidation Protocol
Before deploying a sim-trained policy to physical hardware:
1. Sim evaluation (automated):
- Run 100 episodes in sim with deterministic policy
- Record success rate, average reward, episode length
- Threshold: success rate > 85%
2. Supervised real evaluation (human present):
- Run 20 grasps on physical hand
- Human monitors for anomalies (servo overheating, tendon issues)
- Record success rate, compare to sim
3. Gap metric:
sim_to_real_gap = abs(sim_success_rate - real_success_rate)- Gap < 15%: deploy
- Gap 15-30%: more domain randomization needed
- Gap > 30%: fundamental sim-real mismatch, investigate
Common Failure Modes
| Symptom | Likely Cause | Fix |
|---|---|---|
| Policy works in sim, fails on real | Insufficient domain randomization | Increase friction/mass randomization |
| Fingers overshoot on real hand | Action scaling mismatch | Check joint ROM mapping, reduce step_size |
| Grasp unstable on real hand | Tendon friction not modeled | Add tendon damping to sim, re-train |
| Servo overheating during policy | Policy uses too much current | Add energy penalty to reward, reduce max_current |
| Policy ignores certain fingers | Training reward doesn't penalize | Add per-finger activity regularization |
Zero-Shot vs Fine-Tuned
Zero-shot: Train only in sim with heavy domain randomization. Directly deploy to real. Simpler pipeline, works for robust policies.
Fine-tuned: Train in sim, then collect real-world episodes and fine-tune. Better performance, requires data collection infrastructure (rwr_system).
Recommendation: Start zero-shot. Only fine-tune if the sim-to-real gap exceeds 15% after thorough domain randomization.
Simulation Setup
Table of Contents
- Installation
- MuJoCo on macOS (Apple Silicon)
- Environment Catalog
- Custom Environments
- Version Pinning
- Headless Rendering
Installation
# From PyPI (stable)
pip install orca_sim
# From source (dev)
git clone https://github.com/orcahand/orca_sim.git
cd orca_sim && pip install -e .Dependencies: gymnasium >=0.29, mujoco >=3.1, numpy >=1.26.
MuJoCo on macOS (Apple Silicon)
MuJoCo runs natively on Apple Silicon. For interactive rendering (render_mode="human"), use mjpython:
pip install mujoco
# Interactive viewer requires mjpython on macOS:
mjpython your_script.pyFor headless rendering (render_mode="rgb_array"), regular python works fine.
Visualize hand model directly:
python -m mujoco.viewer --mjcf=$(pwd)/orcahand_description/v2/scene_combined.xmlEnvironment Catalog
All environments registered as Gymnasium envs. Default version: v2.
| Environment | DoF | Description |
|---|---|---|
OrcaHandRight | 17 | Right hand, no objects |
OrcaHandLeft | 17 | Left hand, no objects |
OrcaHandCombined | 34 | Both hands |
OrcaHandRightExtended | 17 | Right + camera mount, U2D2, fans |
OrcaHandLeftExtended | 17 | Left extended |
OrcaHandCombinedExtended | 34 | Both extended |
OrcaHandRightCubeOrientation | 17 | Task: reorient cube red-face-up |
Basic usage:
from orca_sim import OrcaHandRight
env = OrcaHandRight(version="v2", render_mode="human")
obs, info = env.reset(seed=42)
for _ in range(1000):
action = env.action_space.sample()
obs, reward, terminated, truncated, info = env.step(action)
if terminated or truncated:
obs, info = env.reset()
env.close()Observation space: [qpos, qvel] — joint positions and velocities. Action space: Actuator control range (continuous). Frame skip: 5 physics steps per env step.
Custom Environments
Subclass BaseOrcaHandEnv to create task-specific environments:
from orca_sim.envs import BaseOrcaHandEnv
class MyGraspEnv(BaseOrcaHandEnv):
def __init__(self, **kwargs):
super().__init__(hand="right", version="v2", **kwargs)
def _get_reward(self):
# Define your reward function
return 0.0
def _get_terminated(self):
# Define termination conditions
return FalseAdd objects by modifying the MJCF XML or using MuJoCo's runtime API.
Version Pinning
# Use v1 models (legacy)
env = OrcaHandRight(version="v1")
# Use v2 models (default, recommended)
env = OrcaHandRight(version="v2")
# Check available versions
from orca_sim.versions import list_versions, latest_version
print(list_versions()) # ["v1", "v2"]
print(latest_version()) # "v2"Headless Rendering
For recording videos or remote servers:
env = OrcaHandRight(render_mode="rgb_array")
obs, info = env.reset()
frame = env.render() # returns numpy array (H, W, 3)Combine with imageio or cv2 for video recording.
Teleoperation
Table of Contents
- Architecture Overview
- Apple Vision Pro Setup
- Rokoko Glove Setup
- MediaPipe Webcam Fallback
- Retargeter Configuration
- End-to-End Demo
Architecture Overview
Human hand input → Retargeter → OrcaHand
(gradient-based optimization)
MANO keypoints → joint anglesThree input sources, one retargeter, one output: 1. Apple Vision Pro via avp-stream — best quality, wireless 2. Rokoko glove via ROS2 ingress — professional, wired/wireless 3. MediaPipe webcam — no hardware required, lower accuracy
Apple Vision Pro Setup
pip install avp-streamfrom avp_stream import VisionProStreamer
from orca_retargeter import Retargeter
from orca_core import OrcaHand
# Initialize
streamer = VisionProStreamer(ip="<AVP_IP>")
retargeter = Retargeter("orca_retargeter/models/orcahand_v1_right")
hand = OrcaHand("orca_core/models/orcahand_v1_right")
hand.connect()
hand.enable_torque()
# Stream loop
while True:
avp_data = streamer.get_latest()
if avp_data is not None:
joint_angles = retargeter.retarget(avp_data)
hand.set_joint_pos(joint_angles, num_steps=5, step_size=0.001)Latency: ~30-50ms end-to-end (AVP tracking → WiFi → retarget → serial → servo).
Rokoko Glove Setup
Requires ROS2 and rwr_system:
# In ROS2 workspace
cd ~/ros2_ws/src
git clone https://github.com/orcahand/rwr_system.git
pip install -e .[all]
colcon build --symlink-install
source install/setup.bash
# Run teleoperation
ros2 launch experiments record_demonstration.launch.py
ros2 run experiments run_teleop_rokoko.pyCalibration (3 steps in the Rokoko teleop script): 1. Robot init — move to neutral 2. Robot calibrate — map joint limits 3. Glove calibrate — map human hand range
ROS2 topics:
/ingress/mano(Float32MultiArray, 21x3) — MANO keypoints/ingress/wrist(PoseStamped) — wrist pose/hand/policy_output(Float32MultiArray) — joint angles in radians
MediaPipe Webcam Fallback
No special hardware — just a webcam. Lower accuracy, higher latency.
Available via rwr_system/ingress/webcam or standalone with MediaPipe:
import mediapipe as mp
import cv2
mp_hands = mp.solutions.hands.Hands(max_num_hands=1)
cap = cv2.VideoCapture(0)
while cap.isOpened():
ret, frame = cap.read()
results = mp_hands.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if results.multi_hand_landmarks:
landmarks = results.multi_hand_landmarks[0]
# Convert to MANO format and feed to retargeterLimitations: 2D tracking (depth estimated), sensitive to lighting, ~100ms latency.
Retargeter Configuration
Three config files in orca_retargeter/models/<model_name>/:
hand_scheme.yaml
Defines tendon-joint coupling and kinematic structure:
gc_tendons: # tendon → joint coupling matrix
finger_to_tip: # finger name → tip link name
finger_to_base: # finger name → base link name
gc_limits_lower: # generalized coordinate lower bounds
gc_limits_upper: # generalized coordinate upper bounds
wrist_name: wrist # wrist joint nameretargeter.yaml
Tuning parameters for the optimization:
lr: 2.5 # RMSprop learning rate
use_scalar_distance_palm: true
loss_coeffs: # per-finger loss weights
thumb: 1.0
index: 1.0
middle: 0.8
ring: 0.6
pinky: 0.4
mano_adjustments: # per-finger corrections
thumb:
translation: [0, 0, 0]
rotation: [0, 0, 0]
scale: 1.0
joint_regularizers: # prevent extreme angles
thumb_mcp: 0.01Tuning tips
- Increase
lrfor faster tracking (may overshoot) - Adjust
loss_coeffsto prioritize certain fingers (thumb > pinky for most tasks) - Modify
mano_adjustmentsif mapping feels offset for your hand size
End-to-End Demo
The ret_demo.py script in orca_retargeter/ connects everything:
cd orca_retargeter
python ret_demo.py --model models/orcahand_v1_right --input avpOptions: --input avp (Apple Vision Pro), --input replay (recorded data).
Troubleshooting
Table of Contents
- Hardware Issues
- Simulation Issues
- Calibration Issues
- Teleoperation Issues
- Common Error Messages
Hardware Issues
Motor not found
DynamixelError: Motor ID X not found on port /dev/tty.usbserial-XXXXX- Check U2D2 USB connection
- Verify serial port:
ls /dev/tty.usbserial-*(macOS) - Verify baudrate matches config.yaml (must be 3000000)
- Run
python scripts/check_motor.pyto scan all IDs - Check power supply is connected and on (motors need 12V)
Servo overheating
- Reduce
max_currentin config.yaml (try 150mA) - Check for tendon friction at routing points — re-route if needed
- Allow cooldown period (temperature shield triggers at 70°C)
- If persistent: check for mechanical binding in the joint
Tendon snapped
- Replace with Dyneema fishing line (same gauge)
- Re-route through low-friction channels
- Run full calibration after replacement: tension → calibrate → neutral
Popping joint stuck
- Gently push joint back into socket (designed to pop back)
- If repeatedly stuck: check 3D print quality, reprint at 100% infill
Simulation Issues
MuJoCo segfault on macOS
Segmentation fault: 11- Use
mjpythoninstead ofpythonfor interactive rendering - For headless: use
render_mode="rgb_array"(no display needed) - Update MuJoCo:
pip install --upgrade mujoco
Environment won't load
FileNotFoundError: MJCF file not found- Ensure
orcahand_descriptionis cloned in the expected location - Check
orca_simversion matches description version (v1 vs v2) - Reinstall:
pip install -e orca_sim/
Rendering blank/black
- macOS: Must use
mjpythonforrender_mode="human" - Remote server: Use
render_mode="rgb_array"(no display) - Check:
python -c "import mujoco; print(mujoco.glfw)"— should not error
Calibration Issues
Calibration drift
- Tendons stretch over time — re-tension periodically
- Run
tension.py→calibrate.pyweekly during active use - If drift is severe: replace tendon
Motor position jumps during calibration
- Motor may be in wrong control mode — ensure
control_mode: 5in config.yaml - Check motor daisy-chain wiring for loose connections
- Verify motor IDs match config.yaml ordering
calibration.yaml not updating
- Check file permissions on the model directory
- Verify
calibrate.pycompleted without errors - Look for
calibrated: false— indicates calibration failed
Teleoperation Issues
Apple Vision Pro not connecting
- Verify AVP and Mac are on the same WiFi network
- Check IP address:
streamer = VisionProStreamer(ip="<correct_IP>") - Restart AVP streaming app
Retargeter output looks wrong
- Check
hand_scheme.yaml— joint coupling may be misconfigured - Verify URDF path in retargeter matches your hand version (v1/v2)
- Adjust
mano_adjustmentsinretargeter.yamlfor hand size differences - Increase optimization steps for better convergence (reduce
lr)
High latency during teleoperation
- Reduce
num_stepsinset_joint_pos(try 5 instead of 25) - Check WiFi latency (AVP): should be <10ms
- Serial latency: U2D2 at 3M baud is ~0.5ms per command
Common Error Messages
| Error | Cause | Fix |
|---|---|---|
PacketError: [TxRxResult] Incorrect status packet! | Baudrate mismatch | Set baudrate to 3000000 in config.yaml |
GroupSyncRead: Parameter length does not match | Motor count mismatch | Verify 17 motor IDs in config.yaml |
PortError: Failed to open port | Serial port busy or wrong | Check port path, close other serial apps |
ImportError: No module named 'dynamixel_sdk' | SDK not installed | pip install dynamixel-sdk |
mujoco.FatalError: Nan in simulation | Physics instability | Reduce step size, check model XML |
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "orcahand-action.schema.json",
"title": "OrcaHandAction",
"description": "Input to apply() — extends agentic-control-kernel action.schema.json",
"type": "object",
"properties": {
"directive_id": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this directive (required by kernel contract)"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"directive_type": {
"enum": ["setpoint_update", "experiment_request", "mode_switch"],
"description": "Kernel directive vocabulary. grasp/release/neutral -> setpoint_update, calibrate -> experiment_request"
},
"target_controller": {
"enum": ["orca_core", "orca_sim"],
"description": "Which backend to send the action to"
},
"priority": {
"enum": ["normal", "safety", "emergency"],
"default": "normal"
},
"rationale": {
"type": "string",
"description": "LLM's reasoning for this directive (outer loop provenance)"
},
"payload": {
"type": "object",
"description": "OrcaHand-specific action payload",
"properties": {
"joint_targets": {
"type": "object",
"description": "Partial dict of {joint_name: degrees}. Only specified joints move.",
"additionalProperties": { "type": "number" }
},
"num_steps": {
"type": "integer",
"minimum": 1,
"default": 25,
"description": "Interpolation steps for smooth motion"
},
"step_size": {
"type": "number",
"minimum": 0.0001,
"default": 0.001,
"description": "Step timing in seconds"
},
"control_mode": {
"enum": ["position", "current", "current_based_position"],
"description": "Override control mode for this action"
},
"max_current": {
"type": "number",
"minimum": 0,
"maximum": 500,
"description": "Current limit override in mA"
},
"grasp_type": {
"enum": ["power", "precision", "pinch"],
"description": "High-level grasp strategy (when directive_type = setpoint_update)"
}
},
"required": ["joint_targets"]
}
},
"required": ["directive_id", "timestamp", "directive_type", "target_controller", "payload"]
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "orcahand-state.schema.json",
"title": "OrcaHandState",
"description": "Output of observe() — extends agentic-control-kernel state.schema.json",
"type": "object",
"properties": {
"observation_id": {
"type": "string",
"format": "uuid",
"description": "Unique identifier for this observation (required by kernel contract)"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"measured": {
"type": "object",
"description": "Kernel's measured container — raw sensor readings with units",
"properties": {
"joint_positions": {
"type": "object",
"description": "17 joint positions in degrees",
"properties": {
"thumb_mcp": { "$ref": "#/$defs/measurement_degrees" },
"thumb_abd": { "$ref": "#/$defs/measurement_degrees" },
"thumb_pip": { "$ref": "#/$defs/measurement_degrees" },
"thumb_dip": { "$ref": "#/$defs/measurement_degrees" },
"index_abd": { "$ref": "#/$defs/measurement_degrees" },
"index_mcp": { "$ref": "#/$defs/measurement_degrees" },
"index_pip": { "$ref": "#/$defs/measurement_degrees" },
"middle_abd": { "$ref": "#/$defs/measurement_degrees" },
"middle_mcp": { "$ref": "#/$defs/measurement_degrees" },
"middle_pip": { "$ref": "#/$defs/measurement_degrees" },
"ring_abd": { "$ref": "#/$defs/measurement_degrees" },
"ring_mcp": { "$ref": "#/$defs/measurement_degrees" },
"ring_pip": { "$ref": "#/$defs/measurement_degrees" },
"pinky_abd": { "$ref": "#/$defs/measurement_degrees" },
"pinky_mcp": { "$ref": "#/$defs/measurement_degrees" },
"pinky_pip": { "$ref": "#/$defs/measurement_degrees" },
"wrist": { "$ref": "#/$defs/measurement_degrees" }
},
"required": ["thumb_mcp", "index_mcp", "middle_mcp", "ring_mcp", "pinky_mcp", "wrist"]
},
"motor_currents": {
"type": "object",
"description": "17 motor currents in mA",
"additionalProperties": { "$ref": "#/$defs/measurement_mA" }
},
"motor_temperatures": {
"type": "object",
"description": "17 motor temperatures in celsius",
"additionalProperties": { "$ref": "#/$defs/measurement_celsius" }
},
"tactile_readings": {
"type": "object",
"description": "Per-sensor 3D force vectors in Newtons. Only present on touch model (351 sensors).",
"additionalProperties": {
"type": "object",
"properties": {
"value": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 },
"unit": { "const": "N" }
},
"required": ["value", "unit"]
}
}
},
"required": ["joint_positions"]
},
"estimated": {
"type": "object",
"description": "Estimator output. Initially pass-through; extension point for grasp state inference.",
"properties": {
"grasp_state": {
"enum": ["open", "contact", "secured", "slipping"],
"description": "Estimated grasp state fused from joint positions + motor currents"
}
}
},
"context": {
"type": "object",
"description": "Kernel's context container — non-measurement state",
"properties": {
"backend": { "enum": ["physical", "simulated"] },
"control_mode": { "enum": ["position", "current", "current_based_position", "velocity"] },
"torque_enabled": { "type": "boolean" }
},
"required": ["backend", "control_mode", "torque_enabled"]
}
},
"required": ["observation_id", "timestamp", "measured", "context"],
"$defs": {
"measurement_degrees": {
"type": "object",
"properties": {
"value": { "type": "number" },
"unit": { "const": "degrees" }
},
"required": ["value", "unit"]
},
"measurement_mA": {
"type": "object",
"properties": {
"value": { "type": "number" },
"unit": { "const": "mA" }
},
"required": ["value", "unit"]
},
"measurement_celsius": {
"type": "object",
"properties": {
"value": { "type": "number" },
"unit": { "const": "celsius" }
},
"required": ["value", "unit"]
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "orcahand-trace.schema.json",
"title": "OrcaHandTrace",
"description": "Trace ledger entry — extends agentic-control-kernel trace.schema.json. Lago-compatible.",
"type": "object",
"properties": {
"trace_id": {
"type": "string",
"format": "uuid"
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"loop_level": {
"enum": ["servo", "mid", "outer", "meta"],
"description": "Which control loop generated this trace entry"
},
"state_snapshot": {
"$ref": "orcahand-state.schema.json",
"description": "Kernel field: state at decision time"
},
"directive": {
"$ref": "orcahand-action.schema.json",
"description": "Kernel field: the directive issued"
},
"action_proposed": {
"$ref": "orcahand-action.schema.json",
"description": "Raw action before shield filtering"
},
"action_applied": {
"$ref": "orcahand-action.schema.json",
"description": "Safe action after shield filtering"
},
"outcome": {
"type": "object",
"properties": {
"observation_after": {
"$ref": "orcahand-state.schema.json",
"description": "Kernel field: state after action was applied"
}
}
},
"shield_interventions": {
"type": "array",
"description": "Record of any safety shield filter actions taken",
"items": {
"type": "object",
"properties": {
"shield": { "enum": ["joint_rom", "max_current", "temperature", "velocity", "tactile_overload"] },
"joint": { "type": "string" },
"original_value": { "type": "number" },
"clamped_value": { "type": "number" }
},
"required": ["shield"]
}
},
"evaluator_score": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Task-specific success score (0-1)"
},
"backend": {
"enum": ["physical", "simulated"]
}
},
"required": ["trace_id", "timestamp", "loop_level", "state_snapshot", "backend"]
}
#!/usr/bin/env python3
"""Health check for OrcaHand workspace — outputs JSON for bstack-check integration."""
import argparse
import json
import os
import sys
import time
from pathlib import Path
def check_repos(workspace: Path) -> dict:
repos = ["orcahand_description", "orca_sim", "orca_core", "orca_retargeter"]
results = {}
for repo in repos:
path = workspace / repo
if path.exists() and (path / ".git").exists():
results[repo] = "PASS"
elif path.exists():
results[repo] = "WARN" # exists but not a git repo
else:
results[repo] = "SKIP" # may be sim-only
# At minimum, orcahand_description and orca_sim should exist
if results.get("orcahand_description") == "SKIP" or results.get("orca_sim") == "SKIP":
return {"status": "FAIL", "details": results}
return {"status": "PASS", "details": results}
def check_python_deps() -> dict:
deps = {}
for module in ["mujoco", "gymnasium", "numpy"]:
try:
__import__(module)
deps[module] = "PASS"
except ImportError:
deps[module] = "FAIL"
# Optional deps
for module in ["dynamixel_sdk", "torch", "pytorch_kinematics"]:
try:
__import__(module)
deps[module] = "PASS"
except ImportError:
deps[module] = "SKIP"
has_fail = any(v == "FAIL" for v in deps.values())
return {"status": "FAIL" if has_fail else "PASS", "details": deps}
def check_mujoco() -> dict:
try:
import mujoco
# Try loading a simple model to verify it actually works
xml = '<mujoco><worldbody><body><geom type="sphere" size="0.1"/></body></worldbody></mujoco>'
mujoco.MjModel.from_xml_string(xml)
return {"status": "PASS", "version": mujoco.__version__}
except Exception as e:
return {"status": "FAIL", "error": str(e)}
def check_serial() -> dict:
import platform
from glob import glob
if platform.system() == "Darwin":
ports = glob("/dev/tty.usbserial-*")
else:
ports = glob("/dev/ttyUSB*")
if ports:
return {"status": "PASS", "port": ports[0]}
return {"status": "SKIP", "reason": "No U2D2 serial device detected"}
def check_calibration(workspace: Path) -> dict:
cal_files = list(workspace.glob("**/calibration.yaml"))
if not cal_files:
return {"status": "SKIP", "reason": "No calibration.yaml found"}
newest = max(cal_files, key=lambda p: p.stat().st_mtime)
age_days = (time.time() - newest.stat().st_mtime) / 86400
if age_days > 7:
return {"status": "WARN", "file": str(newest), "age_days": round(age_days, 1)}
return {"status": "PASS", "file": str(newest), "age_days": round(age_days, 1)}
def check_sim_env() -> dict:
try:
from orca_sim import OrcaHandRight
env = OrcaHandRight(render_mode="rgb_array")
obs, info = env.reset(seed=0)
env.close()
return {"status": "PASS", "obs_shape": list(obs.shape)}
except Exception as e:
return {"status": "FAIL", "error": str(e)}
def check_schemas(skill_dir: Path) -> dict:
schema_dir = skill_dir / "schemas"
if not schema_dir.exists():
return {"status": "FAIL", "error": "schemas/ directory not found"}
results = {}
for schema_file in schema_dir.glob("*.json"):
try:
with open(schema_file) as f:
data = json.load(f)
if "$schema" in data and "properties" in data:
results[schema_file.name] = "PASS"
else:
results[schema_file.name] = "WARN"
except json.JSONDecodeError as e:
results[schema_file.name] = f"FAIL: {e}"
has_fail = any("FAIL" in str(v) for v in results.values())
return {"status": "FAIL" if has_fail else "PASS", "details": results}
def check_plant_yaml(workspace: Path) -> dict:
plant_yaml = workspace / ".control" / "plant.yaml"
if not plant_yaml.exists():
return {"status": "FAIL", "error": ".control/plant.yaml not found"}
try:
import yaml
with open(plant_yaml) as f:
data = yaml.safe_load(f)
if "plant" in data and "name" in data["plant"]:
return {"status": "PASS", "plant_name": data["plant"]["name"]}
return {"status": "FAIL", "error": "Missing plant.name in .control/plant.yaml"}
except ImportError:
return {"status": "SKIP", "reason": "pyyaml not installed"}
except Exception as e:
return {"status": "FAIL", "error": str(e)}
def main():
parser = argparse.ArgumentParser(description="OrcaHand workspace health check")
parser.add_argument("--workspace", default=os.path.expanduser("~/broomva/experiments/orcahand"))
parser.add_argument("--skill-dir", default=None, help="Path to the orcahand skill directory")
parser.add_argument("--json", action="store_true", help="Output JSON only")
args = parser.parse_args()
workspace = Path(args.workspace)
skill_dir = Path(args.skill_dir) if args.skill_dir else Path(__file__).parent.parent
checks = {
"repos_present": check_repos(workspace),
"python_deps": check_python_deps(),
"mujoco_working": check_mujoco(),
"serial_connected": check_serial(),
"calibration_fresh": check_calibration(workspace),
"sim_env_loadable": check_sim_env(),
"schemas_valid": check_schemas(skill_dir),
"plant_yaml_present": check_plant_yaml(workspace),
}
any_fail = any(c["status"] == "FAIL" for c in checks.values())
if args.json:
print(json.dumps({"overall": "FAIL" if any_fail else "PASS", "checks": checks}, indent=2))
else:
for name, result in checks.items():
status = result["status"]
icon = {"PASS": "OK", "FAIL": "XX", "SKIP": "--", "WARN": "!!"}.get(status, "??")
print(f" [{icon}] {name}: {status}")
if "error" in result:
print(f" {result['error']}")
print(f"\nOverall: {'FAIL' if any_fail else 'PASS'}")
sys.exit(1 if any_fail else 0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Bootstrap an OrcaHand workspace: clone repos, install deps, detect hardware."""
import argparse
import os
import platform
import shutil
import subprocess
import sys
from glob import glob
from pathlib import Path
REPOS = [
("orcahand_description", "https://github.com/orcahand/orcahand_description.git", False),
("orca_sim", "https://github.com/orcahand/orca_sim.git", False),
("orca_core", "https://github.com/orcahand/orca_core.git", True),
("orca_retargeter", "https://github.com/orcahand/orca_retargeter.git", True),
("rwr_system", "https://github.com/orcahand/rwr_system.git", True),
]
def run(cmd: list[str], cwd: str | None = None) -> subprocess.CompletedProcess:
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
def clone_repos(workspace: Path, sim_only: bool) -> dict[str, str]:
results = {}
for name, url, hardware_only in REPOS:
if sim_only and hardware_only:
results[name] = "SKIPPED (--sim-only)"
continue
dest = workspace / name
if dest.exists():
results[name] = "EXISTS"
continue
r = run(["git", "clone", url, str(dest)])
results[name] = "OK" if r.returncode == 0 else f"FAIL: {r.stderr.strip()}"
return results
def install_deps(workspace: Path, sim_only: bool) -> dict[str, str]:
results = {}
use_uv = shutil.which("uv") is not None
installer = "uv pip install" if use_uv else "pip install"
if not use_uv:
print(" Warning: uv not found, falling back to pip")
packages = [("orca_sim", workspace / "orca_sim")]
if not sim_only:
packages.insert(0, ("orca_core", workspace / "orca_core"))
packages.append(("orca_retargeter", workspace / "orca_retargeter"))
for name, path in packages:
if not path.exists():
results[name] = "SKIPPED (not cloned)"
continue
cmd = installer.split() + ["-e", str(path)]
r = run(cmd)
results[name] = "OK" if r.returncode == 0 else f"FAIL: {r.stderr.strip()[:200]}"
return results
def detect_serial() -> str | None:
if platform.system() == "Darwin":
ports = glob("/dev/tty.usbserial-*")
else:
ports = glob("/dev/ttyUSB*")
return ports[0] if ports else None
def verify_mujoco() -> bool:
try:
r = run([sys.executable, "-c", "import mujoco; print(mujoco.__version__)"])
return r.returncode == 0
except Exception:
return False
def generate_plant_yaml(workspace: Path, sim_only: bool, serial_port: str | None):
plant_dir = workspace / ".control"
plant_dir.mkdir(exist_ok=True)
config = {
"plant": {
"name": "orcahand",
"type": "simulated" if sim_only else "physical",
"interface": "orcahand-plant",
"state_schema": "schemas/orcahand-state.schema.json",
"action_schema": "schemas/orcahand-action.schema.json",
"trace_schema": "schemas/orcahand-trace.schema.json",
"shields": ["joint_rom", "max_current", "temperature", "velocity", "tactile_overload"],
"emergency_fallback": "disable_all_torque",
"estimator": "pass-through",
"backends": {
"simulated": {
"driver": "orca_sim",
"environment": "OrcaHandRight-v2",
"render_mode": "human",
},
},
}
}
if not sim_only:
config["plant"]["backends"]["physical"] = {
"driver": "orca_core",
"serial_port": serial_port or "auto-detect",
"baudrate": 3000000,
}
import yaml # noqa: delayed import — pyyaml may not be installed yet
(plant_dir / "plant.yaml").write_text(yaml.dump(config, default_flow_style=False, sort_keys=False))
def main():
parser = argparse.ArgumentParser(description="Bootstrap an OrcaHand workspace")
parser.add_argument("--workspace", default=os.path.expanduser("~/broomva/experiments/orcahand"))
parser.add_argument("--sim-only", action="store_true", help="Skip hardware-related repos")
args = parser.parse_args()
workspace = Path(args.workspace)
workspace.mkdir(parents=True, exist_ok=True)
print(f"Workspace: {workspace}")
print("\n1. Cloning repos...")
clone_results = clone_repos(workspace, args.sim_only)
for name, status in clone_results.items():
print(f" {name}: {status}")
print("\n2. Installing dependencies...")
install_results = install_deps(workspace, args.sim_only)
for name, status in install_results.items():
print(f" {name}: {status}")
serial_port = None
if not args.sim_only:
print("\n3. Detecting serial port...")
serial_port = detect_serial()
print(f" {'Found: ' + serial_port if serial_port else 'No U2D2 detected (connect hardware later)'}")
print("\n4. Verifying MuJoCo...")
mujoco_ok = verify_mujoco()
print(f" {'OK' if mujoco_ok else 'Not found — run: pip install mujoco'}")
print("\n5. Generating .control/plant.yaml...")
try:
generate_plant_yaml(workspace, args.sim_only, serial_port)
print(" OK")
except ImportError:
print(" SKIP (pyyaml not installed yet — run manually after install)")
print("\n--- Summary ---")
any_fail = any("FAIL" in v for v in {**clone_results, **install_results}.values())
if any_fail:
print("Some steps failed. Check output above.")
sys.exit(1)
else:
print("Workspace ready!")
if args.sim_only:
print("\nNext: python -c 'from orca_sim import OrcaHandRight; env = OrcaHandRight(); env.reset()'")
else:
print("\nNext: cd orca_core && python scripts/tension.py orca_core/models/orcahand_v1_right")
if __name__ == "__main__":
main()
Related skills
FAQ
How many degrees of freedom does the hand have?
17 DOF; the tendon-driven ORCA Hand from ETH Zurich.
What are the safety shields?
Joint ROM, max current (<200mA), temperature (<70C), velocity and tactile shields that filter actions and trigger an emergency torque-disable fallback.
Does it support both simulation and hardware?
Yes. The Plant interface is dual-backend, with physical (orca_core) and simulated (orca_sim) sharing identical typed schemas.