Skip to content
This repository was archived by the owner on Aug 21, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
fdcbce0
feat(lumi-final): update calibration and recordings for lumi_final me…
kyle-aus Jun 9, 2026
11a1587
feat(lumi-final): clamp hardcoded aim/rest positions to lumi_final jo…
kyle-aus Jun 9, 2026
bf71c4e
feat(lumi-final): update calibration and recordings after re-calibration
kyle-aus Jun 9, 2026
6530952
fix(servo): slow resume transition by 2x and sync arm position on resume
kyle-aus Jun 9, 2026
1275a29
fix(servo): fix resume interpolation denominator and eliminate race c…
kyle-aus Jun 9, 2026
bb5bf95
fix(servo): update rest_pos to match natural gravity position of lumi…
kyle-aus Jun 9, 2026
ef3fb4e
fix(servo): slow resume transition by additional 30% (2.86x base dura…
kyle-aus Jun 9, 2026
7296a25
fix(servo): add move_to_unclamped for release position beyond calibra…
kyle-aus Jun 9, 2026
dd4bcc9
fix(servo): replace move_to_unclamped with move_to_raw using pre-comp…
kyle-aus Jun 9, 2026
b57b80c
fix(servo): adjust release wrist_pitch to -94.17deg (raw 1516)
kyle-aus Jun 9, 2026
9c99d6d
fix(servo): set release wrist_pitch to -42.16deg (raw 2108)
kyle-aus Jun 9, 2026
639da3a
feat(servo): add resume startup position — arm wakes to defined pose …
kyle-aus Jun 9, 2026
36357f5
fix(servo): redefine zero position to physical rest pose of lumi_final
kyle-aus Jun 9, 2026
6fd8190
fix(servo): adjust zero base_pitch to -20.25deg (raw 2082)
kyle-aus Jun 9, 2026
90d16b3
fix(servo): update zero position elbow_pitch to -30.54deg (raw 2019)
kyle-aus Jun 9, 2026
4e93e2d
add script test touch sensor
kyle-aus Jun 9, 2026
a9b53d2
fix(servo): stop event loop before move_to_raw and verify rest positi…
kyle-aus Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions hardware/calibration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Calibration

One-time per-unit setup after assembly. Skeleton — paste actual numbers from your build into the TODOs.

## Servo zero positions

Each STS3215 needs a zero-offset captured for the kinematics in `lelamp/follower/` and `lelamp/leader/`. Procedure:

1. Power up the lamp. Do not start the lelamp service yet (it grabs the bus).
2. Manually move each joint to its mechanical zero (defined per joint — TODO: photo per joint).
3. Run the calibration tool: TODO command.
4. Calibration files land in `~/.cache/huggingface/lerobot/calibration/` and are read by `lelamp/calibration/`.

| Servo | Joint | Mechanical zero | Recorded offset |
|---|---|---|---|
| 1 | base yaw | TODO | TODO |
| 2 | shoulder | TODO | TODO |
| 3 | elbow | TODO | TODO |
| 4 | wrist | TODO | TODO |
| 5 | head tilt | TODO | TODO |

## Microphone gain

### Voice mic (USB)
- Initial: `amixer -c <usb_card> sset Mic 50%`
- Tune until utterances peak at ~−12 dBFS in your room.
- Recorded value: TODO

### Sensing mic (onboard)
- On Raspberry Pi: a watchdog clamps wm8960 capture gain to 160 — see `project_lumi_pcm_watchdog.md`. Don't fight it.
- On OrangePi: TODO — measure ambient noise floor and pick gain.

## Speaker volume

- Set DAC level once, then leave it. lelamp's `set_volume` adjusts a software gain on top.
- OrangePi safe defaults (from `project_orangepi_lelamp_dac_cap.md`):
- `amixer -c 1 sset 'ADC2DAC Mixer' 0`
- `amixer -c 1 sset 'ADCL/R PGA' 10` (≈ 30 dB)
- `amixer -c 1 sset DACL/R 70%` (≈ −6.5 dB)
- Pi defaults: TODO

## LED brightness cap

- Maximum allowed: TODO % of max (limited by 5 V rail headroom — see [`power.md`](power.md)).
- Capped in `lelamp/service/rgb/rgb_service.py` — confirm the constant matches what the rail can deliver.

## Camera focus

- IMX307 USB module is fixed-focus; nothing to do.
- If image is soft, check the lens isn't loose in its mount.

## Verification

After calibration:
- [ ] Each joint reaches its commanded position within ±2°
- [ ] No clipping during normal-volume TTS
- [ ] LED ring at full white doesn't brown out the SBC
- [ ] No audible hum at idle volume

## TODO

- [ ] Capture command names for the servo calibration tool
- [ ] Joint-zero photos
- [ ] Final amixer values per board
31 changes: 21 additions & 10 deletions lelamp/calibration/calibration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ LeLamp uses 5x Feetech STS3215 servos controlled via the `lerobot` library. Each

## Calibration files

On the Pi, lerobot looks for calibration at:
lerobot looks for calibration at `~/.cache/huggingface/lerobot/calibration/` for the user running the service:

```
/root/.cache/huggingface/lerobot/calibration/
<user_home>/.cache/huggingface/lerobot/calibration/
├── robots/lelamp_follower/lelamp.json
└── teleoperators/lelamp_leader/lelamp.json
```

The path resolves from `~/.cache/huggingface/lerobot/calibration/` — since the service runs as **root**, it uses `/root/`.
| Device | Service user | Calibration path |
|--------|-------------|-----------------|
| lumi2 / lumi3 (RPi 5) | `root` | `/root/.cache/huggingface/lerobot/calibration/` |
| lumi4 (OrangePi 4P) | `root` | `/root/.cache/huggingface/lerobot/calibration/` |
| lumi_final (OrangePi) | `orangepi` | `/home/orangepi/.cache/huggingface/lerobot/calibration/` |

Each JSON contains per-servo values:

Expand All @@ -34,26 +38,33 @@ Each JSON contains per-servo values:
| 4 | `wrist_roll` | Wrist rotation |
| 5 | `wrist_pitch` | Wrist tilt |

## Deploy to a new Pi (copy existing calibration)
## Deploy to a device (copy existing calibration)

If the new Pi uses the same servo hardware as an already-calibrated lamp, copy the calibration files:
If the device uses the same servo hardware as an already-calibrated lamp, copy the calibration files.

**Root-user devices (lumi2, lumi3, lumi4):**
```bash
# On the new Pi, create the directories
sudo mkdir -p /root/.cache/huggingface/lerobot/calibration/robots/lelamp_follower
sudo mkdir -p /root/.cache/huggingface/lerobot/calibration/teleoperators/lelamp_leader

# Copy from this repo
sudo cp calibration/robots/lelamp_follower/lelamp.json \
/root/.cache/huggingface/lerobot/calibration/robots/lelamp_follower/lelamp.json

sudo cp calibration/teleoperators/lelamp_leader/lelamp.json \
/root/.cache/huggingface/lerobot/calibration/teleoperators/lelamp_leader/lelamp.json
```

**orangepi-user devices (lumi_final):**
```bash
mkdir -p /home/orangepi/.cache/huggingface/lerobot/calibration/robots/lelamp_follower
mkdir -p /home/orangepi/.cache/huggingface/lerobot/calibration/teleoperators/lelamp_leader
cp calibration/robots/lelamp_follower/lelamp.json \
/home/orangepi/.cache/huggingface/lerobot/calibration/robots/lelamp_follower/lelamp.json
cp calibration/teleoperators/lelamp_leader/lelamp.json \
/home/orangepi/.cache/huggingface/lerobot/calibration/teleoperators/lelamp_leader/lelamp.json
```

Then restart the lelamp service. The servos will use the copied calibration values.

> **Note:** Copied calibration will be close but may not be exact — each servo has slightly different mechanical offsets. For precise movement, run a fresh calibration (see below).
> **Note:** The calibration files in this branch (`feat/lumi-final`) are tuned for **lumi_final's** servo hardware. The `wrist_pitch` recordings are scaled to ±55.8° to match lumi_final's physical range (vs ±113° on the original hardware).

## Run fresh calibration on a Pi

Expand Down
32 changes: 16 additions & 16 deletions lelamp/calibration/robots/lelamp_follower/lelamp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,36 @@
"base_yaw": {
"id": 1,
"drive_mode": 0,
"homing_offset": -962,
"range_min": 574,
"range_max": 3052
"homing_offset": 1928,
"range_min": 828,
"range_max": 3255
},
"base_pitch": {
"id": 2,
"drive_mode": 0,
"homing_offset": -475,
"range_min": 1516,
"range_max": 2468
"homing_offset": -381,
"range_min": 1608,
"range_max": 3017
},
"elbow_pitch": {
"id": 3,
"drive_mode": 0,
"homing_offset": -1697,
"range_min": 1184,
"range_max": 2620
"homing_offset": 518,
"range_min": 1651,
"range_max": 3082
},
"wrist_roll": {
"id": 4,
"drive_mode": 0,
"homing_offset": 912,
"range_min": 897,
"range_max": 3013
"homing_offset": 1962,
"range_min": 976,
"range_max": 3164
},
"wrist_pitch": {
"id": 5,
"drive_mode": 0,
"homing_offset": -587,
"range_min": 1750,
"range_max": 3310
"homing_offset": -1837,
"range_min": 2044,
"range_max": 3132
}
}
}
32 changes: 16 additions & 16 deletions lelamp/calibration/teleoperators/lelamp_leader/lelamp.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,36 @@
"base_yaw": {
"id": 1,
"drive_mode": 0,
"homing_offset": -1064,
"range_min": 963,
"range_max": 3177
"homing_offset": 1909,
"range_min": 974,
"range_max": 3142
},
"base_pitch": {
"id": 2,
"drive_mode": 0,
"homing_offset": -586,
"range_min": 1770,
"range_max": 2596
"homing_offset": -556,
"range_min": 1645,
"range_max": 3367
},
"elbow_pitch": {
"id": 3,
"drive_mode": 0,
"homing_offset": -1554,
"range_min": 673,
"range_max": 2598
"homing_offset": 431,
"range_min": 1741,
"range_max": 3325
},
"wrist_roll": {
"id": 4,
"drive_mode": 0,
"homing_offset": 865,
"range_min": 1038,
"range_max": 2912
"homing_offset": 1957,
"range_min": 924,
"range_max": 3071
},
"wrist_pitch": {
"id": 5,
"drive_mode": 0,
"homing_offset": -723,
"range_min": 1651,
"range_max": 3354
"homing_offset": -1794,
"range_min": 1828,
"range_max": 3360
}
}
}
4 changes: 2 additions & 2 deletions lelamp/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@
AIM_PRESETS = {
AIM_CENTER: {"base_yaw.pos": 3.0, "base_pitch.pos": -20.0, "elbow_pitch.pos": 32.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": 0.0},
AIM_DESK: {"base_yaw.pos": 3.0, "base_pitch.pos": 5.0, "elbow_pitch.pos": 20.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": 40.0},
AIM_WALL: {"base_yaw.pos": 3.0, "base_pitch.pos": 5.0, "elbow_pitch.pos": -20.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": -60.0},
AIM_WALL: {"base_yaw.pos": 3.0, "base_pitch.pos": 5.0, "elbow_pitch.pos": -20.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": -47.82},
AIM_LEFT: {"base_yaw.pos": -90.0, "base_pitch.pos": -30.0, "elbow_pitch.pos": 57.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": 18.0},
AIM_RIGHT: {"base_yaw.pos": 90.0, "base_pitch.pos": -30.0, "elbow_pitch.pos": 57.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": 18.0},
AIM_UP: {"base_yaw.pos": 3.0, "base_pitch.pos": 10.0, "elbow_pitch.pos": -15.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": 25.0},
AIM_DOWN: {"base_yaw.pos": 3.0, "base_pitch.pos": -90.0, "elbow_pitch.pos": 90.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": -90.0},
AIM_DOWN: {"base_yaw.pos": 3.0, "base_pitch.pos": -61.93, "elbow_pitch.pos": 62.9, "wrist_roll.pos": 0.0, "wrist_pitch.pos": -47.82},
AIM_USER: {"base_yaw.pos": 0.0, "base_pitch.pos": 0.0, "elbow_pitch.pos": 0.0, "wrist_roll.pos": 0.0, "wrist_pitch.pos": -45.0},
}
Loading