File size: 12,084 Bytes
168e232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# Stringman (Neufangled), "Arpeggio" layout, carbon 270 mm pole. Every number says where it came from.
# fw = github.com/nhnifong/cranebot3-firmware @ 370287a (2026-10-07). fw-sim = its simulator/ (MuJoCo model + bridge),
# whose own README lists which of its numbers are guesses. MEASURE = not in the docs or the firmware: a guess until
# measured on the real machine. Full table with reasoning: docs/MACHINE.md.

room:
  # fw-sim: a 4 m square room, anchors (2,2,2) and (-2,-2,2). Docs: anchors ~13 cm below the ceiling. We keep the
  # fw-sim footprint but put the mount points at 2.30 m (a 2.43 m ceiling, the common one): fw-sim's 2.0 m leaves the
  # gripper no reach to a high shelf (lines flatten, tension grows as 1/sin(angle)).
  side_m: 4.0
  mount_z_m: 2.30
  # corner mount points, xy, inset from the walls by the anchor's eyelet offset (fw definitions.arp_anchor_*_eyelet,
  # ~5 cm). Order = the firmware's line order: 0 anchor0 direct, 1 anchor0 indirect (via post A), 2 anchor1 direct,
  # 3 anchor1 indirect (via post B) (fw-sim tendons line_0..3).
  mounts_xy: [[1.95, 1.95], [-1.95, 1.95], [-1.95, -1.95], [1.95, -1.95]]
  indirect_run_m: [0.0, 3.93, 0.0, 3.93]   # fixed cross-room run of the indirect lines (fw-sim: 3.9301 m)

lines:
  # fw-sim: 4000 N/m per line at ~3 m (its README: "may be too stiff; real monofilament stretches considerably more").
  # We write it as EA = k * L so a longer line (indirect, + 3.93 m) is softer: EA = 4000 * 3.0. MEASURE.
  EA_N: 12000.0
  damping_Ns_m: 40.0            # fw-sim kv 80 + tendon damping 4; halved: kv acts on a slack line too. MEASURE
  max_force_N: 40.0             # fw spool_dm NO_CONN_TENSION_LIMIT 40 N (motor torque cut); fw-sim caps at 25 N
  max_safe_tension_N: 28.0      # fw config_loader default (16 N backfill on older configs; observer default 16)
  tension_floor_N: 0.1          # fw spool_dm TENSION_FLOOR_N: the spool reels in to keep this on every line
  tension_kp_mps_per_N: 0.3     # fw spool_dm TENSION_KP
  tension_corr_max_mps: 0.5     # fw spool_dm MAX_TENSION_CORRECTION_MPS

spool:
  loop_hz: 50                   # fw spool_dm LOOP_FREQ_HZ
  max_line_speed_mps: 2.0       # fw spool_dm MAX_SAFE_LINE_SPEED
  # Line acceleration. fw spool_dm MAX_ACCEL 0.01 and config max_accel 0.3 go to the DaMiao ACC register in units the
  # code does not state; fw-sim floors it at 5 rad/s^2 "so the simulator is usable". 5 rad/s^2 x 0.229 m/rev / 2pi
  # (72-73 mm spool, fw definitions.damiao_*) = 0.18 m/s^2 of line. MEASURE (step a line speed, log encoder).
  line_accel_mps2: 0.18
  meters_per_rev: 0.229         # pi x 73 mm (fw definitions.damiao_full_spool_diameter_fishing_line)

host:
  # fw observer.speed_limit(): 0.35 m/s (0.45 with firmware gate "speed_0.45"); the norm of the summed velocity.
  speed_limit_mps: 0.35
  velocity_key_ttl_s: 2.0       # fw observer INPUT_VELOCITY_TTL_S: a keyed velocity expires 2 s after its last update
  estimator_hz: 60              # fw position_estimator.predict_forwards (visual update 30 Hz, commanded-vel 30 Hz)
  # move_direction_speed adds downward_bias -0.04 to unit vectors; with speed=None (the LeRobot path, and ours) the
  # velocity is used as given. Line speeds come from finite differences at the ESTIMATED gantry position, once per
  # command (KINEMATICS_STEP_SCALE 10): our plant does the same every planner tick.

body:
  # Gantry + pole + gripper hang as ONE rigid body from the point where the 4 lines meet (fw-sim; fw swing.py treats it
  # as a pendulum). Masses: fw-sim estimates ("nothing in the firmware repo records them"). MEASURE (kitchen scale).
  gantry_mass_kg: 0.35
  pole_mass_kg: 0.045
  gripper_mass_kg: 0.60
  finger_mass_kg: 0.04
  hook_r_m: 0.055               # fw-sim (from gantry.glb): lines clip to cross arms 55 mm out, on the diagonals
  hook_z_m: 0.02
  pole_top_z_m: -0.06
  pole_bottom_z_m: -0.329       # fw-sim: pole_offset_carbon270 - grommet height 0.1167
  gripper_origin_z_m: -0.4457   # fw definitions.pole_offset_carbon270 (0.5757 - 0.13)
  gripper_cog_dz_m: 0.055       # fw definitions.gripper_cog (y-up gripper frame: 55 mm above the gripper origin)
  # Swing: fw definitions.pole_length_carbon270 = 0.3089 m effective (measured with 'polecal'): period 1.115 s. The
  # gripper inertia is solved at build time so the twin's swing period matches it (fw-sim is ~10 % long, its README).
  swing_length_m: 0.3089
  # swing damping on the body's rotation (N m s): zeta ~ 0.02 = c / (2 I_O omega), I_O ~ 0.12 kg m^2, omega 5.8 rad/s,
  # 1/e in ~9 s. fw-sim has none (its swing never decays); fw observer fits a decay during 'polecal'. MEASURE.
  swing_damping_Nms: 0.03
  finger_pivot_x_m: 0.025       # fw-sim: two fingers pivot at x = +-0.025, close side to side, geared on one servo
  finger_pivot_dz_m: 0.041      # pivot above the gripper origin (fw-sim, gripper.glb)
  finger_len_m: 0.110           # pivot to pad centre, down (fw-sim pad_left pos y -0.110)
  pad_inset_m: 0.019            # pad centre inside the pivot when closed (fw-sim pad pos x -0.019)
  pad_half: [0.006, 0.022, 0.030]   # fw-sim pad box half sizes (x across, y along, z vertical)
  finger_travel_deg: 59.0       # fw gripper_arp_server.FINGER_TRAVEL_DEG; firmware reports -90 (open)..90 (closed)
  camera_dy_m: 0.026            # fw definitions.gripper_camera: 27 mm toward the nose, tilted 9.06 deg back
  camera_dz_m: 0.0026
  camera_tilt_deg: 9.06
  camera_fovy_deg: 42.557       # fw-sim: 2 atan(192/493), cameraCalWide; stream 684x384 @ 54 fps
  camera_res: [684, 384]
  laser_dy_m: -0.050            # fw-sim rangefinder site (0, 0.0025, 0.0502) in the gripper frame
  laser_dz_m: 0.0025

gripper:
  loop_hz: 60                   # fw gripper_arp_server DT = 1/60
  wrist_range_deg: [0, 1080]    # docs python_api; fw-sim joint +-3 pi about the neutral heading
  wrist_max_dps: 180.0          # MEASURE: docs ease_wrist peak 90 deg/s default; servo ST3215 class ~ 300 deg/s
  finger_max_dps: 360.0         # MEASURE (firmware-degrees per s on the -90..90 scale, ~118 real deg/s)
  action_timeout_s: 0.2         # fw ACTION_TIMEOUT: wrist/finger speeds zero after 0.2 s without a command

# ----------------------------------------------------------------------------------------------- the hand (M3)
# Which hand hangs under the pole. stringman = the firmware gripper above (two fingers geared on one servo): M0-M2 and
# cdpr_wm_g1 were built on it. so101 = the SO-101's wrist_roll + gripper (two STS3215) and its wrist camera, for the
# pick-waste missions with real objects (2026-10-09). load_config(hand=...) overrides; so101 replaces `body`'s gripper
# numbers and `gripper`'s loop numbers with the ones below.
hand: stringman

so101:
  # Source: armpilot/third_party/so101/so101_new_calib_camera.xml (onshape-to-robot from the SO-101 CAD) unless noted;
  # geometry measured from its meshes in MuJoCo (2026-10-09). Hand frame = the SO-101 gripper link frame: origin on the
  # wrist_roll axis at the follower's top face, z up along that axis (= the pole), jaws close along x, camera at +y.
  hand_origin_z_m: -0.374       # MEASURE: pole bottom (-0.329) - 45 mm for the roll servo + adapter plate
  roll_servo_mass_kg: 0.079     # the SO-101 `wrist` link (roll servo + bracket): stands in for servo + adapter. MEASURE
  roll_servo_half: [0.0124, 0.0175, 0.0225]   # STS3215 45.2 x 24.7 x 35 mm (datasheet), shaft along the pole
  gripper_mass_kg: 0.087        # `gripper` link: gripper servo + fixed jaw (wrist_roll_follower)
  gripper_com: [0.000214, 0.000245, -0.025187]
  gripper_fullinertia: [2.75087e-05, 4.33657e-05, 3.45059e-05, -3.35241e-07, -5.7352e-06, -5.17847e-08]
  jaw_mass_kg: 0.012            # `moving_jaw_so101_v1`
  jaw_com: [-0.00157, -0.0193, -0.0300]       # its inertial, rotated into the hand frame (jaw link is +90 deg about x)
  jaw_diaginertia: [6.61e-06, 5.29e-06, 1.89e-06]
  camera_mass_kg: 0.035         # wrist_camera_mount 0.020 + wrist_camera 0.015 (32x32 UVC module)
  # gripper servo body (mesh bounds): x -0.015..0.0304, y -0.0193..0.0203, z -0.0358..-0.011
  servo_box: {pos: [0.0077, 0.0005, -0.0234], half: [0.0227, 0.0198, 0.0124]}
  # fixed jaw finger: inner face x = -0.0079 (the lerobot `gripperframe` site sits on it at z -0.098), tip z -0.1044
  fixed_jaw: {x: [-0.0235, -0.0079], y: [-0.008, 0.008], z: [-0.1044, -0.011]}
  # moving jaw: hinge at the anchor about hand -y (SO-101 `gripper` joint); finger in the hand frame at q = 0 relative to
  # the anchor; inner face 15.8 mm from the fixed jaw's at q = 0, 1.9 mm at the joint's lower limit -10 deg (the pads,
  # 1 mm each here, meet there)
  jaw_anchor: [0.0202, 0.0188, -0.0234]
  moving_jaw: {x: [-0.0123, 0.0045], y: [-0.0266, -0.0110], z: [-0.082, 0.010]}
  pad_len_m: 0.030              # bottom 3 cm of each jaw's inner face is the pad
  pad_mu: 1.0                   # MEASURE: PLA jaws (often taped); Stringman's rubber pads were 1.5
  jaw_closed_deg: -10.0         # SO-101 gripper joint lower limit: the pads touch (lerobot 0 = fully closed)
  jaw_full_open_deg: 100.0      # joint upper limit: the moving jaw horizontal, tips 10 cm out
  jaw_open_deg: 50.0            # closure 0 (MEASURE with the real toys and socks). Tip gap from the meshes
                                # (2026-10-09): 0 deg 16 mm, 30 deg 53, 50 deg 75, 70 deg 83 (widest), 100 deg 18. Opening
                                # lifts the moving tip: 50 deg puts it 19 mm above the fixed jaw's (-85 vs -104 mm)
  swing_length_m: 0.217         # the twin's pendulum with this hand: period 0.935 s (2026-10-09). MEASURE: polecal
  jaw_open_gap_m: 0.0745        # tip gap at jaw_open_deg (the mesh table above): the task centres the object in it
  wrist_range_deg: [-157.2, 162.8]   # wrist_roll joint limits (-2.7438, 2.8412 rad)
  # STS3215. Joint side (damping, armature, friction): the `sts3215` class, a fit adapted from Open Duck Mini (so101
  # README). Its kp 998 / kv 2.73 are NOT what the real gripper does: they saturate at 0.17 deg of error, so in the twin
  # the load banged +-100 % while closing and read 100 % on every hold (hand_feel 2026-10-09). LeRobot's so_follower
  # configure() sets P_Coefficient 16 (default 32) on every servo and, on the gripper, Max_Torque_Limit 500 (50 %),
  # Protection_Current 250, Overload_Torque 25. Refit on the REAL gripper holding an 11 mm pen (armpilot runs
  # 2026-10-07/08, Present_Load / 10 = % of max torque): squeezed 0.020 rad past contact -> 8.8-10.4 %; told fully
  # closed, stopped 0.08 rad short -> 26-28 % held (31-44 % peak while closing); kp ~10 N m/rad fits both (6.8 + friction,
  # 27 %). Slow free close (~0.2 rad/s, feel_grasp 2026-10-07): ~4 % load; damping 0.60 predicts 4.3 %, kept.
  servo: {stall_Nm: 2.94, damping: 0.60, armature: 0.028, frictionloss: 0.052}
  jaw_servo: {kp: 10.0, kv: 0.3, torque_limit_frac: 0.5}     # kv: MEASURE (lerobot D_Coefficient 32, units unknown)
  wrist_servo: {kp: 10.0, kv: 0.3, torque_limit_frac: 1.0}   # MEASURE: same P_Coefficient 16, assumed the jaw's kp
  servo_max_dps: 270.0          # MEASURE: STS3215 12 V datasheet 0.222 s / 60 deg no load. The host's ramp on the
                                # jaw and the wrist (lerobot sends goal positions; the servo moves at its own speed)
  camera_pos: [0.0025, 0.0734, 0.0066]        # `wrist_camera` link origin; its +z (optical axis) points at the jaw tips
  camera_xyaxes: [1, 0, 0, 0, 0.906, -0.424]  # MuJoCo looks along -z: z_cam = -(optical axis); 25 deg off straight down
  camera_fovy_deg: 70.70        # armpilot calib/wrist.json (640x480, 1.54 px rms, 2026-10)
  camera_res: [640, 480]
  grasp_local: [-0.0079, 0.0, -0.098]   # lerobot `gripperframe`: on the fixed jaw's face. An object of width w sits
                                        # w/2 further along +x: OFF the pole axis (the tasks must rotate it by the wrist)
  load_lsb_pct: 0.1             # STS3215 Present_Load: 0.1 % of max torque per count (Feetech memory table, 0-1000)

sim:
  timestep_s: 0.004             # plant; checked against 0.002 (fw-sim) in tests/test_scene.py