---
name: project_feetech_calib_bootstrap
description: feetech_calib agent (forked from cad 2026-07-11) owns STS3215 servo calibration/ID/comm software; controller repo + zeroing-fixture state.
metadata: 
  node_type: memory
  type: project
  originSessionId: 74b3b2e9-c857-4b95-b1ef-65409e9280bb
---

**feetech_calib** forked from cad 2026-07-11 (see [[feedback_fork_agent_pattern]]). Owns the SOFTWARE/procedural
side of Feetech STS3215 servo calibration: zero-offset, direction signs, ID assignment, baud/comm, register
reads, diagnostics. cad still owns mechanical/URDF/mesh. Line: servo-tick/register = me; joint-frame/geometry = cad.

**Controller repo:** `our_feetech_controller` — rescued from Cameron's Mac 2026-07-11, now a git repo at
**VPS `/data/cameron/our_feetech_controller`** (code lives on the VPS/omid-fleet now, NOT the lab box — Cameron
corrected the old "code lives on lab" assumption for this repo). Uses `scservo_sdk` (pip `feetech-servo-sdk`) +
local `scservo_patch.py` (portHandler auto-inject). Key scripts: `which_motors.py` (bus scan), `set_motor_id.py`
(EEPROM ID reassign), `calibrate_motors.py` (INST_OFSCAL sets current pos = 2048), `write_motors_from_goals.py`
(MuJoCo-in-loop control w/ temp + tick-range safety), `feetech_read_motor.py` (live diagnostics).

**STS3215 facts:** 12-bit absolute encoder, 4096 ticks/rev, 2048=center, scale = 651.9 ticks/rad (HARDWARE
CONSTANT). No hardware direction-reversal register → software `signs` array (guess `[-1,1,1,1,1,1]`).
Default `/dev/tty.usbserial-0001` @ 115200. COMM_RX_TIMEOUT=-6 = wrong baud/not connected.

**Calibration approach (decided w/ Cameron 2026-07-11):** SINGLE-POINT ZERO, not min/max endstop mapping —
scale is a known constant so only offset+sign are unknown; one good reference pose suffices. Sim/URDF min/max =
SAFETY tick-clamps only (arm URDF has placeholder ±1.74 rad on all 6 joints, not real limits). Cameron then chose
BY-HAND software zeroing over the printed fixture (fixture parked): move servo to zero pose → INST_OFSCAL; move
in URDF-negative direction → record sign.

**SHIPPED calibration tooling `claude_feetech_controller`** (2026-07-11) — Cameron's chosen home
`Mac ~/Projects/robotics_testing/claude_feetech_controller` (git source of record on VPS
`/data/cameron/claude_feetech_controller`). Refactor of our_feetech_controller into: `feetech_bus.py` (shared:
open/read_pos/read_temp/torque/set_zero_here=INST_OFSCAL/ticks_to_rad), `calibrate_servo.py` (by-hand 2-step
zero+sign → merges calib/<config>.json keyed by servo id), `live_mirror.py` (torque-off back-drive read →
drives MuJoCo model joint in passive viewer — reuses read_and_vis_motors loop), `configs.py` (config→model+joint).
Started with `gripper` config, single servo, actuated joint `jaw` (range -0.9..0.1). live_mirror uses Cameron's
existing LIT model `Mac stl_transfers/mj_umi_v2/umi_gripper_v2.xml` (a bare-URDF bundle rendered all black — use
his mjcf; `live_mirror --model` overrides). RUN WITH `/opt/homebrew/bin/python3.11` on Mac (has scservo_sdk + mujoco 3.5.0; default python3 lacks
scservo_sdk). Verified on Mac: imports, model load, compile all clean; hardware serial + viewer untested (needs
Cameron). Sign convention: `rad = sign*(tick-2048)*2π/4096`; sign=+1 if tick decreased moving negative, else -1.
Also `data_recording_stuff/record.py <task> --config gripper` (2026-07-11): opens camera 0 (Continuity), torque
off, per-tick saves `datasets/<task>/frames/NNNNNN.jpg` + `states/NNNNNN.npy` (calibrated joint radians) +
timestamps.npy + meta.json; live preview, q/Ctrl-C to stop. Reuses feetech_bus + the config calibration.
`set_id.py <old> <new>` (one servo on bus) + `scan_ids.py` (bus scan) ported from set_motor_id/which_motors.
`ar_view/arview.py` (2026-07-11): live real-vs-sim AR overlay — localizes cam0 (third view) + cam1 (wrist) from
the arm-base ArUco board (PnP), poses arm_umi_v2 from the live calibrated servos, renders from each viewpoint,
blends real|sim (wrist shows raw RGB when board not seen). Reuses cad's calib_utils/aruco_boards (copied into
ar_view) + board-frame render loop-closure. Verified on Mac by _selftest.py (board 0.9px, cam pose ~1.2mm).
mujoco.Renderer works on Mac even over ssh. `ar_view/calibrate_camera.py <role> --index N`: multi-frame
PINHOLE solve — reuses `calib_utils.estimate_focal` (fix pp at center, square px, ZERO distortion, solve only
focal) over ~60 auto-captured board views → `intrinsics/<role>.json` + `cameras.json` (role→index+resolution).
(An earlier `calibrateCameraAruco` version estimated full k1..k3/p1,p2 off the small board and went fisheye in
the viewer — reverted to the pinhole model 2026-07-12; distortion saved as zeros.)
Large calib board (2026-07-13): `ar_view/calib_board.py` CALIB_BOARD — DICT_6X6_250, **ids 109-132** (reserved
w/ cad, free block between arm 100-108 & umi 150-161; DICT max id 249 so 300+ was invalid), 4x6 of 35mm markers,
190x280mm A4 sheet, exact 8px/mm (no letterbox). `gen_calib_board.py` → grid PNG + print page (50mm scale bar) +
standalone `calib_board.xml` (board+floor, no arm — this is what arview uses) + composite
`mj_arm_v2/arm_calib_board.xml` (arm+board, now UNUSED). `calibrate_camera.py` + `arview.py` both DEFAULT to
`--board calib`; arview's scene layer renders the STANDALONE calib board only (it's a separate sheet, not on the
arm — no arm overlay, no BOARD_POSE needed). Print `calib_board_model/calib_board.pdf` at 100% (DPI-tagged; cv2
PNGs had no DPI so 100% zoomed to 1 marker). Verified: marker 34.9mm, loop-closure 0.36mm; iPhone photo detects
109-132 (24/24) at 1920. arview detects at FULL capture res (open_index requests 1920x1080; 640 dropped to 14/24).
`ar_view/record_dataset.py <task>` (2026-07-13): localization dataset — per frame @ --fps(4): scene+wrist RGB
(JPEG, downscaled to ~500px, aspect kept) + scene_cam_pose + wrist_cam_pose + umi_pose (all 4x4 local→world in
the calib-board ArUco frame, OpenCV convention, PnP, last-known+visible flag when occluded) + gripper tick/rad.
meta.json: per-camera native_wh + K_native + K_save (scaled to saved imgs) + board specs; scene intrinsics live
each run, wrist from saved intrinsics/wrist.json. Format doc: `ar_view/DATASET_FORMAT.md`. Sync Mac→puget via
`ar_view/sync_datasets.sh [task]` (openrsync `-av`, incremental, ON-DEMAND not auto) → `puget:~/ar_datasets/`.
Mac↔puget direct ssh works over Tailscale; both have rsync (macOS = openrsync 2.6.9-compat, no --info/--partial). `cameras.py`: role-based camera resolve with resolution-fingerprint
auto-heal for shuffled macOS indices (differently-sized cams self-ID; same-size need stable ports).
arview references cameras by role scene/wrist, loads saved intrinsics (undistort + K scaled to working width),
falls back to live focal estimate if uncalibrated. NOTE `calibrateCameraAruco` exists on Mac cv2 4.8 but NOT
on puget's newer cv2 (removed) — camera-calib code only runs/tests on the Mac; validated synthetically
(project board from 24 poses → recovers fx/fy/cx/cy exactly). ids must be reshaped (N,1) for the call. `arm` config (2026-07-11): servos are 0-indexed → convention servo id i → joint output_i (ids 0..5 == output_0..5), lit model
`stl_transfers/mj_arm_v2/arm_umi_v2.xml` (joints output_0..5 + gripper_jaw; id 6→gripper_jaw);
calibrate_servo auto-resolves the joint from the id (no --joint). live_mirror drives only the connected+calibrated
SUBSET (pings each; reports LIVE / MISSING / NOT-CALIBRATED; uncalibrated joints held at rest).

**Delivered `servo_zero_fixture`** (I did the CAD myself, Cameron's call — the ONLY CAD I do, calibration-only):
universal printed jig, one tool zeroes all 6 servos. v2 = a 180° DROP-ON SADDLE (v1 full-silhouette pocket
enclosed 360° and couldn't go over an assembled joint — Cameron corrected). Top-half voxel-offset shell of
servo+yoke, open underneath, drops on / lifts off along +Z; keys q=0 via the yoke arm + servo body (limp yoke
only lets it seat at zero), then INST_OFSCAL. Verified: seats @0 interference, rejects yoke @±6/±12°, lifts
off by 20mm. Generator `gen_servo_saddle.py` (supersedes gen_servo_zero_fixture.py) needs scikit-image in
~/cad_venv (installed 2026-07-11); runs from the cad tree (reads cad's st3215.stl/yoke_fillet.stl);
owned source-of-record copy in `our_feetech_controller/fixtures/`. STL on Mac `stl_transfers/calib_fixtures/`.
Viewer: cad.omidlab.net/?dir=/home/cameronsmith/cad_recovery_mnt&file=servo_zero_fixture{,_demo,_install}.urdf.
NOTE the arm URDF yoke placement is correct: joint output_N origin −0.0255 is cancelled by yoke visual origin
+0.0255 (gen_two_servo.py:40) → net-zero mate; yoke_fillet.stl is effectively authored in the servo frame.

**Next:** `calibrate_arm.py` (ping 6 → seat each in fixture → INST_OFSCAL → determine signs → write
`arm_calib.json` that write_motors_from_goals.py reads); assign servo IDs (likely output_0..5 → 1..6).
