Compare commits

...
2 Commits
25 changed files with 3374 additions and 18 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
# LeRobot 双臂移动操作项目方案
> 决策与阶段验收以本文件为准。更新:2026-09-08,设计评审修订。
> 当前 P0:已有规划与环境脚本,尚无 GPU 环境、训练效果或真机验收结果。
> 2026-09-10 执行进展:P1 遥操与真实数据采集已完成基础验证,3080 正在训练 ACT;自主抓放未验收。详情见 [进度追踪](docs/progress.md)。3090 仍待验收,当前基线训练先在 3080 完成。
## 1. 目标与边界
@@ -59,9 +59,9 @@ P1 起验证停止手段、限位、限速与人工接管。P3 前定义最大
### P0:环境与采购确认
- [ ] 两台补齐 OS、驱动、GPU、Python、uv 信息([机器记录](env/machines.md))。
- [ ] 3080 用 `env/setup_lerobot.sh --resolve` 生成依赖锁并通过软件冒烟检查,保留报告。
- [x] 3080 用 `env/setup_lerobot.sh --resolve` 生成依赖锁并通过软件冒烟检查,保留报告。
- [ ] 将锁文件和输入指纹同步到 3090,以默认模式安装,核对两台锁的 SHA256。
- [ ] 完成短视频编码/解码、小样本训练和模型加载;解决 CUDA/FFmpeg/TorchCodec 兼容性后再认可该锁。
- [x] 完成短视频编码/解码、小样本训练和模型加载;解决 CUDA/FFmpeg/TorchCodec 兼容性后再认可该锁。
- [ ] 验证过的锁、指纹与精简环境记录入 git,不提交凭据或原始日志。
- [ ] 确认实际套件的 leader/follower 电机、供电规格,先买一对臂、相机和夹具。
@@ -119,7 +119,7 @@ P1 起验证停止手段、限位、限速与人工接管。P3 前定义最大
## 6. 参考与下一步
下一步:3080 完成 P0 首次解析和报告,再同步 3090;同时确认 P1 型号。未经测试的项保持未勾选。
下一步:将已验证的锁同步至 3090 并复验;机械臂到货后完成 P1 装配、校准及少量真实示范检查。未经测试的项保持未勾选。
- [v0.6.1 依赖](https://github.com/huggingface/lerobot/blob/v0.6.1/pyproject.toml)
- [v0.6.1 双臂源码](https://github.com/huggingface/lerobot/blob/v0.6.1/src/lerobot/robots/bi_so_follower/bi_so_follower.py)
+33 -5
View File
@@ -2,12 +2,13 @@
基于 LeRobot,建立人工遥操示范、采集、训练与真机执行闭环。先完成单臂抓放,再扩展桌面双臂,最后实现“移动到位 → 停稳 → 操作”的分阶段任务。
当前处于 P0:规划与环境脚本已准备,GPU、数据管线和机器人效果尚未验收。
当前进入 P1:SO101 主从臂遥操与双摄像头采集已跑通,5 条真实示范已检查,RTX 3080 正在训练 ACT。自主抓放尚未评测;实时状态快照见 [进度追踪](docs/progress.md)。历史环境验收见 [P0 记录](docs/p0-3080-20260908.md)。
## 导览
| 文件 | 内容 |
|---|---|
| [docs/progress.md](docs/progress.md) | 当前状态、训练快照、问题与下一步 |
| [PLAN.md](PLAN.md) | 决策、阶段范围、验收与风险 |
| [hw/bom.md](hw/bom.md) | 分批采购、供电和机械验证 |
| [env/machines.md](env/machines.md) | 机器分工、依赖锁流程和环境记录 |
@@ -16,11 +17,11 @@
| [docs/cli_reference.md](docs/cli_reference.md) | 单臂设置、采集、训练和 rollout 示例 |
| [docs/evaluation.md](docs/evaluation.md) | 20 次评测及故障验证记录模板 |
`robots/`、`configs/`、`server/`、`client/` 当前为占位。后续优先复用上游驱动与网络推理,仅实现组合硬件适配、任务调度和故障处理。
`robots/` 已提供遥操、采集、训练和评测启动器;`configs/`、`server/`、`client/` 仍为后续扩展目录。后续优先复用上游驱动与网络推理,仅实现组合硬件适配、任务调度和故障处理。
## 机器分工
- 3080 10GB:遥操、采集与推理,P1/P2 USB 直连。
- 3080 10GB:当前 ACT 基线训练、遥操、采集与推理,P1/P2 USB 直连。
- 3090 24GB:训练,与 3080 使用同一经过验证的依赖锁。
- RPi 5:P3 起采图和动作执行,ARM64 环境单独验证。
- ACT:固定任务基线;SmolVLA:预训练基座微调和语言条件探索。
@@ -33,7 +34,34 @@
```bash
mkdir -p ~/robot
git clone https://git.hyonglin.top/ylhan/lerobot_learn.git ~/robot/proj
bash ~/robot/proj/env/setup_lerobot.sh --resolve
bash ~/robot/proj/env/setup_lerobot.sh
```
第一次在目标主机解析完整依赖并生成锁;通过 P0 验证后,把锁和输入指纹同步至 3090,再执行不带 `--resolve` 的脚本。当前未附经过 GPU 验证的完整锁,不能声称两机已可复现。脚本不会自动拉取上游最新代码,也不会操作机器人。
仓库已附本机验证的依赖锁及输入指纹,默认按锁安装;不要再次使用 `--resolve` 覆盖锁。3090 仍需同锁独立验收。已有本仓库时无需重复 clone,可直接在项目目录运行 `bash env/setup_lerobot.sh`。脚本不会自动拉取上游最新代码,也不会操作机器人。
安装完成后,在项目目录激活环境并加载本机主从臂串口:
```bash
source env/activate_lerobot.sh
```
## SO101 遥操作
以下脚本会自动激活环境,使用固定串口路径及 `zihao_follower_arm` / `zihao_leader_arm` 校准文件。在两条臂姿态大致一致、从臂周围有活动空间时运行;按 Ctrl+C 停止。
```bash
# 无摄像头
bash robots/teleoperate_no_camera.sh
# 单摄像头:腕部 Integrated Webcam,640×480 MJPG,30 FPS
bash robots/teleoperate_one_camera.sh
# 双摄像头:腕部相机 + 外部 USB2.0 CAM1(640×480 MJPG,25 FPS)
bash robots/teleoperate_two_cameras.sh
```
摄像头使用 `/dev/v4l/by-id/` 路径,避免 video 编号随插拔变化。单摄像头画面名为 `wrist`;双摄像头额外包含 `front`。带摄像头的脚本默认显示画面。脚本支持追加 LeRobot 参数,例如 `bash robots/teleoperate_no_camera.sh --fps=30`。
## ACT 积木抓放
先执行 `bash robots/record_block_white_box.sh` 录制 5 条检查数据。采集、续录、训练与真机评测说明见 [ACT 积木放入白盒](docs/act_block_white_box.md)。
+59
View File
@@ -0,0 +1,59 @@
# ACT:积木放入白色盒子
任务:`Pick up the block and place it in the white box.`
固定盒子和两台摄像头;起初只在小区域改变积木位置。腕部 `wrist` 使用 Integrated Webcam(30 FPS),外部 `front` 使用 CAM1(25 FPS)。数据与控制按 25 FPS 记录。脚本自动激活本机环境、加载 zihao 主从臂配置,使用固定 USB 路径,不自动上传数据或模型。
## 先录 5 条
在本机交互终端运行(先退出其他遥操作进程):
```bash
bash robots/record_block_white_box.sh
```
脚本先等待按 Enter,随后连接设备并自动开始。绿色中文横幅表示录制,黄色表示复位或重录;请在运行脚本的终端操作方向键。每条最多 30 秒,复位阶段默认 30 秒;复位期间仍会跟随主臂。操作键:
- 右方向键:结束当前阶段。录制时按表示结束本条;复位时按表示准备好进入下一阶段。
- 左方向键:标记当前条重录;按屏幕提示完成复位后重新录制。
- Esc:结束采集并走保存流程,当前未标记重录的片段也可能保存;检查是否混入不完整示范。
执行完整的靠近、抓取、抬起、放入盒子、松开、退开,再结束本条。复位时把积木取出,主从臂回到相近起始姿态。录完等待程序编码、保存完再关闭终端。
数据保存在 `data/block_white_box_v1`。先检查 5 条的画面清晰度、动作完整性、时间戳和帧率,再追加示范:
```bash
# 追加 45 条,num_episodes 表示本次新增数量
RESUME=true NUM_EPISODES=45 bash robots/record_block_white_box.sh
```
50 条仅作为初始目标,不保证成功率。原数据存在时脚本默认拒绝覆盖。新建数据集须同时指定 `DATASET_ROOT` 和 `DATASET_ID`;训练时保持一致。
## ACT 训练(本机 3080)
数据检查通过后,先做短训练验证:
```bash
STEPS=200 BATCH_SIZE=4 bash robots/train_act_block_white_box.sh
```
短训练仅验证链路,不用于自主抓取。正式训练起步:
```bash
bash robots/train_act_block_white_box.sh
```
默认 batch_size=8、20000 步,每 2000 步保存;显存不足可改 `BATCH_SIZE=4`。每次使用独立的 `outputs/train/act_block_white_box_<时间>` 目录。首次加载视觉骨干可能需要下载权重。本次训练的实测速度、显存和输出路径见进度追踪。
## 真机评测
完成正式训练并确认检查点后,在相同相机布局与校准下执行:
```bash
MODEL_DIR=/绝对路径/checkpoints/last/pretrained_model \
bash robots/rollout_act_block_white_box.sh
```
默认自主运行 30 秒、25 FPS,按 Ctrl+C 停止。这个阶段会由模型控制从臂,不需要操纵主臂。先单次观察,再按 `docs/evaluation.md` 记录多次成功率。初次试跑应留出活动空间,并能及时断电。
所有启动器支持 `DRY_RUN=1` 打印命令,不连接硬件;可追加 LeRobot 参数。2026-09-10 已录制并检查 5 条示范,短训练及模型重载通过,正式训练进行中。当前运行目录、服务及下一步见 [进度追踪](progress.md)。
+40
View File
@@ -0,0 +1,40 @@
# 机械臂到货前的软件准备
本机项目位于 `/home/ubuntu/lerobot_learn`,运行环境位于 `~/robot/.venv-lerobot-0.6.1`。
## 复验命令
```bash
cd /home/ubuntu/lerobot_learn
export PATH="$HOME/.local/bin:$PATH"
bash env/setup_lerobot.sh
source "$HOME/robot/.venv-lerobot-0.6.1/bin/activate"
python env/smoke_pipeline.py --output "$HOME/robot/reports/pipeline-$(date +%Y%m%dT%H%M%S)"
```
每次使用新输出目录;脚本拒绝覆盖。安装检查使用已有锁,不重新解析。原始安装日志位于 `~/robot/reports/`。
管线检查生成 3 段共 72 帧合成数据(6 维状态/动作,单相机 128×96),编码 H.264,分别通过 PyAV 和 TorchCodec 读取,再用 batch=2、chunk=8 跑 10 步 ACT。关闭预训练视觉权重和 Hub 联网,保存 checkpoint 与处理器,再重载并检查有限值动作。输出训练日志、训练峰值显存和推理延迟报告。
这验证的是软件通路;合成数据模型不能用于机械臂。小图像、短 chunk 的显存不能作为正式 640×480 采集训练的预算,真实配置需再次测量。SmolVLA 当前只做模块导入检查,预训练权重下载、微调和推理另行验证。
也可指定接近正式 ACT 的参数:
```bash
python env/smoke_pipeline.py \
--output "$HOME/robot/reports/pipeline-640x480-$(date +%Y%m%dT%H%M%S)" \
--height 480 --width 640 --episodes 2 --frames-per-episode 120 \
--batch-size 8 --steps 10 --chunk-size 100 --pretrained-backbone
```
该命令仍使用合成画面,但会验证 640×480 输入、ACT 默认动作块和预训练 ResNet18 的下载与加载。
## 到货后顺序
1. 按实际套件确认 leader/follower 电机和供电,组装固定并确认停止手段。
2. 在本机发现端口和相机,回填 `docs/cli_reference.md` 中的设备参数。
3. 按官方装配流程逐个设置新电机,校准两条臂并备份校准文件。
4. 验证遥操与相机视野,先录 2–3 段真实示范;检查回放、状态动作和时间对齐。
5. 正式采约 50 条同任务示范,再同步至 3090 训练 ACT,回传完整模型与处理器。
双臂、底盘与远程推理在后续阶段开展。3090 仍需使用相同锁独立验证,不能凭 3080 的结果勾选两机验收。
+13 -2
View File
@@ -1,7 +1,7 @@
# SO-101 CLI 参考(LeRobot 0.6.1)
> 按 v0.6.1 官方文档核对;尚未在目标 GPU/真机上执行。安装报告包含 CLI 帮助,实际差异必须回填。
> 遥操、采集和 rollout 在 3080;训练在 3090。电机设置/校准会写硬件或校准数据,先按装配文档确认接线。
> 通用命令模板;本机实际已验证的任务流程见 [ACT 抓放指南](act_block_white_box.md),执行状态见 [进度追踪](progress.md)。
> 当前遥操、采集和 ACT 基线训练在 3080;3090 是后续训练机。电机设置/校准会写硬件或校准数据,先按装配文档确认接线。
## 0. 环境与变量
@@ -44,6 +44,17 @@ lerobot-teleoperate \
先清空工作区,低速验证限位与停止手段,再采数。数据采集中的控制键以当前 CLI 提示为准,不沿用旧版“空格开始、回车保存”的说法。
### 首次小幅运动
从臂完成校准后,可先让腕部旋转关节移动 3 度并回到起点:
```bash
source env/activate_lerobot.sh
python robots/test_follower_motion.py --robot-id=zihao_follower_arm
```
脚本要求本地校准文件存在且串口可读写;测试幅度最大允许 5 度。默认测试 `wrist_roll`,运行前需要输入 `MOVE`。脚本直接使用电机总线,只使能所选关节,并在使能前将目标寄存器设为当前位置;其余关节保持无扭矩。机械臂应放在有支撑的姿态,退出时会释放所选关节的扭矩。
## 3. 采集(3080)
```bash
+44
View File
@@ -0,0 +1,44 @@
# 3080 软件验收记录 · 2026-09-08
状态:本机基础环境及离线合成数据管线通过。机械臂未到货,3090 未验收,整个 P0 尚未完成。
## 环境
- Ubuntu 24.04.3 x86_64;RTX 3080 10240 MiB;驱动 550.163.01。
- Python 3.12.3;uv 0.12.10;FFmpeg 6.1.1-3ubuntu5;已安装 build-essential/python3-dev。
- LeRobot 0.6.1;PyTorch 2.7.1+cu126;TorchVision 0.22.1;TorchCodec 0.4.0;PyAV 15.1.0。
- 112 个依赖包;`uv pip check` 通过;按哈希同步安装通过。
- 锁 SHA256:`d3876e8ff74ba8356618bf6f6059fc32be33e3ed6301a50d164b668884405e18`。
原始输入会选择 PyTorch 2.11 / CUDA 13,因此约束为 CUDA 12 的版本组合。配对依据:[TorchCodec 官方兼容表](https://github.com/pytorch/torchcodec/tree/v0.5.0#installing-torchcodec)、[NVIDIA CUDA minor compatibility](https://docs.nvidia.com/deploy/cuda-compatibility/minor-version-compatibility.html);本机 CUDA 运算及 ACT 训练已实际通过。未升级系统驱动。
## 通过的检查
- CUDA 矩阵运算;ACT、SmolVLA、SO follower/leader 和视频库导入。
- 电机设置、校准、遥操、录制、训练、rollout CLI 帮助加载;端口/相机发现 CLI 存在,未连接硬件。
- 3 段共 72 帧合成 LeRobot 数据写入、H.264 编码、PyAV/TorchCodec 读取及跨 episode 边界取样。
- ACT batch=2、chunk=8、128×96 单相机、6 维状态/动作、seed=42、无预训练视觉权重,10 步 CUDA 训练完成。
- 保存及重载完整模型/处理器;输出形状为 (1, 6),无 NaN/Inf。
## 测量
- 训练子进程含初始化和保存:4.55 秒;逐步日志峰值 allocated 显存约 1.38 GiB(上游日志精度 0.01 GiB)。
- 重载推理:预热 5 次后测 20 次,每次清空动作缓存并重新生成 chunk;包括预处理和后处理,不包括图像解码/硬件采集。
- 推理 P50/P95/P99:2.19 / 3.48 / 3.56 ms;推理峰值 allocated 显存 214.49 MiB。
- 最后一步显存另列于 JSON;上游逐步重置峰值计数,不能拿末步值替代全程峰值。
2026-09-09 又完成一轮更接近正式配置的单相机基准:640×480、batch=8、chunk=100、预训练 ResNet18、10 步训练。训练子进程含初始化和保存共 8.26 秒,逐步日志峰值 allocated 显存约 2.17 GiB;重载推理 P50/P95/P99 为 5.17 / 5.19 / 5.22 ms,推理峰值 allocated 显存 265.21 MiB。结果见 [640×480 基准 JSON](p0-3080-benchmark-640x480.json)。
上述数字只适用于合成小图像配置,不能推断真实任务成功率、640×480 多相机显存或端到端控制延迟。SmolVLA 只通过导入,尚未下载基座或微调。
## 本机证据路径
- 基础环境:`/home/ubuntu/robot/reports/20260908T090146Z-51776/`
- 最终管线:`/home/ubuntu/robot/reports/pipeline-20260908-final/`
- 管线内含 `result.json`、`train.log`、合成数据及 `training/checkpoints/000010/pretrained_model/`。
- 可移植精简结果:[p0-3080-result.json](p0-3080-result.json)。复验命令:[到货前准备](before_hardware.md)。
- 640×480 基准:`/home/ubuntu/robot/reports/pipeline-640x480-b8/`。
## 剩余
3090 同锁安装及验收;真实相机和示范数据检查;机械臂组装、校准、遥操和停止验证;真实 ACT 训练及 20 次评测。合成 checkpoint 不用于机械臂。
+31
View File
@@ -0,0 +1,31 @@
{
"status": "PASS",
"scope": "synthetic software pipeline only; not robot/task validation",
"frames": 240,
"episodes": 2,
"train_steps": 10,
"batch_size": 8,
"chunk_size": 100,
"image_shape": [
480,
640,
3
],
"pretrained_backbone": true,
"training_wall_seconds": 8.26456517999759,
"training_peak_allocated_gib_from_step_logs": 2.17,
"training_last_step_memory": {
"allocated_mib": 2144.20751953125,
"reserved_mib": 2588.0
},
"checkpoint": "/home/ubuntu/robot/reports/pipeline-640x480-b8/training/checkpoints/000010/pretrained_model",
"inference_ms_p50_p95_p99": [
5.1704970028367825,
5.194654352089856,
5.221867668733466
],
"inference_peak_allocated_mib": 265.20556640625,
"torch": "2.7.1+cu126",
"cuda": "12.6",
"gpu": "NVIDIA GeForce RTX 3080"
}
+30
View File
@@ -0,0 +1,30 @@
{
"status": "PASS",
"scope": "synthetic software pipeline only; not robot/task validation",
"frames": 72,
"episodes": 3,
"train_steps": 10,
"batch_size": 2,
"image_shape": [
96,
128,
3
],
"pretrained_backbone": false,
"training_wall_seconds": 4.55074907900007,
"training_peak_allocated_gib_from_step_logs": 1.38,
"training_last_step_memory": {
"allocated_mib": 959.85107421875,
"reserved_mib": 1040.0
},
"checkpoint": "/home/ubuntu/robot/reports/pipeline-20260908-final/training/checkpoints/000010/pretrained_model",
"inference_ms_p50_p95_p99": [
2.192092500081344,
3.4814052502724735,
3.5563298500483143
],
"inference_peak_allocated_mib": 214.486328125,
"torch": "2.7.1+cu126",
"cuda": "12.6",
"gpu": "NVIDIA GeForce RTX 3080"
}
+84
View File
@@ -0,0 +1,84 @@
# 项目进度追踪
更新于 2026-09-10 16:46:43(Asia/Shanghai)。本文件记录执行状态与证据;架构和验收标准见 [PLAN](../PLAN.md),操作步骤见 [ACT 抓放指南](act_block_white_box.md)。状态是更新时快照,不会随后台训练自动刷新。
## 当前状态
已进入 P1:一套 SO101 主从臂完成遥操、双摄像头采集;正在 RTX 3080 上用 5 条真实示范训练 ACT。训练尚未完成,尚未执行自主抓放或测量任务成功率。3090 未验收,桌面双从臂及移动底盘仍在后续阶段。
| 项目 | 状态 | 证据 / 说明 |
|---|---|---|
| 3080 软件基础环境 | 已验证 | LeRobot 0.6.1;早期合成数据检查见 P0 报告 |
| 主从臂装配、编号、校准 | 已完成基础调试 | zihao 主从 ID;当前采集链路可用 |
| wrist_roll 主从零点 | 已对齐 | 两边各 5 次回读均为 2047,约 −0.044° |
| 无 / 单 / 双摄像头遥操 | 启动脚本已提供 | 自动激活环境,固定 USB 路径 |
| 双摄像头采集 | 已验证 | 两路同时各读 80 帧无失败;真实数据全量解码通过 |
| 5 条积木放入白盒示范 | 已保存并检查结构 | 3379 帧,25 FPS,135.16 秒 |
| NVIDIA 驱动修复 | 已验证 | 580.173.02;重启后 CUDA 训练可用 |
| ACT 20 步短训练 | 已通过 | 检查点保存及重新加载通过 |
| ACT 20000 步训练 | 进行中 | 用户级 systemd 服务;见下方快照 |
| 真机自主执行 / 成功率 | 未开始 | 正式模型完成后验证 |
## 当前训练
- 数据:`local/block_white_box_v1`,本机 `data/block_white_box_v1`,未上传 Hub。
- 任务:`Pick up the block and place it in the white box.`
- 输入:`wrist`、`front` 两路 RGB + 6 维关节状态;输出 6 维动作。
- ACT:实际配置 51,597,190 参数;batch=8,steps=20000,lr=1e-5,seed=42,chunk_size=100,FP32。
- 每 2000 步保存检查点;没有保留验证集,这 5 条全部参与训练。
- 输出:`outputs/train/act_block_white_box_20260910_1642/`。
- 日志:`outputs/train/act_block_white_box_20260910_1642.log`。
- 服务:`act-block-white-box.service`,用户级临时服务;不会在重启后自动恢复。
- 最近指标:`step:2K smpl:18K ep:26 epch:5.21 loss:1.063 grdn:49.164 lr:1.0e-05 updt_s:0.156 data_s:0.002 smp/s:51 mem_gb:3.71 l1_loss:0.209 kld_loss:0.085`。
- 初始速度约 6~7 步/秒;PyTorch 报告训练显存约 3.73 GB,nvidia-smi 整卡占用约 4.9 GB。预计约 50 分钟,实际以日志为准。
- 短训练输出:`outputs/train/act_block_white_box_smoke_20260910_1640/`;20 步检查点已重载。
- `act_block_white_box_20260910_1641` 是未持续运行的启动尝试,不是当前有效训练目录。
```bash
systemctl --user status act-block-white-box --no-pager
tail -f outputs/train/act_block_white_box_20260910_1642.log
```
训练损失不代表真机成功率。完成后需要验证最终检查点和处理器加载,再单次真机执行、逐次记录结果。模型和数据在 git 忽略目录中,不随代码提交。
## 数据检查与已知限制
| 示范(从 1 开始) | 帧数 | 时长(秒) |
|---|---:|---:|
| 1 | 653 | 26.12 |
| 2 | 714 | 28.56 |
| 3 | 669 | 26.76 |
| 4 | 618 | 24.72 |
| 5 | 725 | 29.00 |
两路视频各解码 3379 帧,与动作表一致;所有状态、动作均为有限值,frame_index 连续,记录时间戳步长约 0.04 秒。时间戳规则不能单独证明真实采集无延迟或两路曝光严格同步。
已查看样帧:腕部夹爪和积木清楚;外部视角被机械臂遮挡较多,盒子部分出框。尚未逐帧确认每条任务成功。用户决定保留当前视角并直接使用这组数据训练,先取得基线。不要把这 5 条当作泛化能力证据。
## 硬件与校准记录
- 从臂 ID:`zihao_follower_arm`;固定串口末尾 `5B91044223-if00`。
- 主臂 ID:`zihao_leader_arm`;固定串口末尾 `5B8E115693-if00`。
- 校准文件:`~/.cache/huggingface/lerobot/calibration/robots/so_follower/zihao_follower_arm.json` 和 `~/.cache/huggingface/lerobot/calibration/teleoperators/so_leader/zihao_leader_arm.json`。
- 2026-09-09 wrist_roll 在相同物理姿态重新定零:从臂偏移 1687 → 737;主臂 1630 → 1749。旧文件备份后缀 `.bak_wrist_zero_20260909_183517_030072`。这些数值仅适用于本机,不应复制到其他机械臂。
- 腕部相机 Integrated Webcam:640×480 MJPG、30 FPS;外部 USB2.0 CAM1:640×480 MJPG、25 FPS。脚本固定 by-id 路径,数据按 25 FPS 记录。
- 历史故障:从臂 2 号电机启用扭矩曾收到 `Incorrect status packet!`;之后完成采集,但根因未最终确认,不标记为彻底修复。
## 驱动故障与恢复
2026-09-10 发现运行内核为 `7.0.0-31-generic`,原 NVIDIA 550 模块仅存在于旧内核 `6.14.0-27-generic`。Secure Boot 关闭。安装当前内核对应的 580 驱动后重启,nvidia-smi 显示 580.173.02,真实 ACT CUDA 训练通过。
后续内核升级后先检查 `nvidia-smi` 及环境中的 `torch.cuda.is_available()`。训练脚本已加入 CUDA 检查,避免无声回退到 CPU。
## 下一步
- [ ] 确认 20000 步训练正常结束、最终检查点可加载。
- [ ] 使用当前相机布局进行单次自主抓放,记录结果与异常。
- [ ] 统计多次成功率,区分抓取失败、放置失败和通信失败。
- [ ] 根据基线结果决定改善外部视角、补充示范及保留验证任务。
## 更新记录
- 2026-09-08:3080 基础环境、视频与合成 ACT 流程验收。
- 2026-09-09:主从臂调试、wrist_roll 对齐、双摄像头连通与三个遥操作脚本。
- 2026-09-10:中文录制阶段横幅、5 条示范检查、驱动恢复、真实数据 ACT 短训练及正式训练启动。
Vendored Executable
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Source this file to activate the project's LeRobot environment.
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
echo "This script must be sourced so it can update the current shell:" >&2
echo " source env/activate_lerobot.sh" >&2
exit 1
fi
LEROBOT_PROJECT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
LEROBOT_VENV="${ROBOT_WORKDIR:-${HOME}/robot}/.venv-lerobot-0.6.1"
if [[ ! -f "$LEROBOT_VENV/bin/activate" ]]; then
echo "LeRobot environment not found: $LEROBOT_VENV" >&2
echo "Install it first with: bash $LEROBOT_PROJECT_DIR/env/setup_lerobot.sh" >&2
return 1
fi
# shellcheck disable=SC1091
source "$LEROBOT_VENV/bin/activate"
export LEROBOT_PROJECT_DIR
export FOLLOWER_PORT="/dev/serial/by-id/usb-1a86_USB_Single_Serial_5B91044223-if00"
export LEADER_PORT="/dev/serial/by-id/usb-1a86_USB_Single_Serial_5B8E115693-if00"
cd "$LEROBOT_PROJECT_DIR" || return 1
show_lerobot_port() {
local role="$1"
local stable_path="$2"
local current_device
if [[ -e "$stable_path" ]]; then
current_device="$(readlink -f -- "$stable_path")"
echo "$role: $stable_path -> $current_device"
else
echo "$role: $stable_path (not connected)"
fi
}
echo "LeRobot environment activated: $VIRTUAL_ENV"
echo "Project: $LEROBOT_PROJECT_DIR"
show_lerobot_port "Follower" "$FOLLOWER_PORT"
show_lerobot_port "Leader " "$LEADER_PORT"
unset -f show_lerobot_port
+10 -6
View File
@@ -1,13 +1,13 @@
# GPU 环境与机器记录
基线:LeRobot 0.6.1 / Python 3.12。脚本适用 Ubuntu x86_64 GPU 主机;不用于 Windows 或 RPi。当前 GPU 环境与模型训练均未验收。
基线:LeRobot 0.6.1 / Python 3.12。脚本适用 Ubuntu x86_64 GPU 主机;不用于 Windows 或 RPi。3080 的 CUDA、视频和合成 ACT 短训练已验收,详见 [记录](../docs/p0-3080-20260908.md);3090 未验收;真实任务采集和 ACT 短训练已通过,正式训练进行中,见 [进度追踪](../docs/progress.md)。
## 分工与路径
| 机器 | 用途 | 环境记录 |
|---|---|---|
| Windows | 编辑、静态检查、git 同步 | 本仓库 `D:\robot\lerobot` |
| 3080 10GB | USB 遥操、采集、推理,首次解析依赖 | OS/驱动/Python/uv/锁 SHA256:待填 |
| 3080 10GB | USB 遥操、采集、推理,首次解析依赖 | Ubuntu 24.04.3 / 550.163.01 / Python 3.12.3 / uv 0.12.10;锁见实测记录 |
| 3090 24GB | 训练,同局域网 | OS/驱动/Python/uv/锁 SHA256:待填 |
| RPi 5 | P3 起硬件驱动、采图、执行和故障处理 | ARM64 环境另行验证,不复用 x86_64 锁 |
| DRIVE Orin | 可选实验 | 不阻塞主线 |
@@ -16,9 +16,9 @@ GPU 机统一路径:项目 `~/robot/proj`;专用环境 `~/robot/.venv-lerobo
## 首次安装与依赖锁
前置:安装 NVIDIA 驱动、git、uv、FFmpeg。脚本用 uv 创建 Python 3.12 环境(必要时 uv 下载 Python),不自动 sudo 安装系统组件。缺少前置即退出。
前置:安装 NVIDIA 驱动、git、uv、FFmpeg,以及构建遥操依赖 evdev 所需的 `build-essential`、`python3-dev`。Ubuntu 可执行 `sudo apt-get install -y ffmpeg build-essential python3-dev`。脚本用 uv 创建 Python 3.12 环境(必要时 uv 下载 Python),不自动 sudo 安装系统组件。缺少前置即退出。
先在 3080:
首次解析流程(本机已完成;新机器有仓库附带锁时直接使用下方默认安装命令):
```bash
mkdir -p ~/robot
@@ -35,7 +35,7 @@ bash ~/robot/proj/env/setup_lerobot.sh
source ~/robot/.venv-lerobot-0.6.1/bin/activate
```
默认安装严格使用锁与包哈希,不重新解析。两台比较报告中的锁 SHA256;验收通过后把锁、输入指纹与精简环境信息提交 git。**当前仓库尚未包含经过 Ubuntu/GPU 验证的传递依赖锁,默认安装会明确拒绝继续。**
默认安装严格使用锁与包哈希,不重新解析。两台比较报告中的锁 SHA256;验收通过后把锁、输入指纹与精简环境信息提交 git。**当前仓库已包含 3080 验证的完整依赖锁和指纹,默认安装可直接使用。不要对现有锁执行 `--resolve`。**
两台尽量统一 Ubuntu 版本,记录 Python 补丁版本、uv 版本与 NVIDIA 驱动;同一 Python 包锁不能锁定系统库或驱动。脚本 `uv pip sync` 仅作用于新命名的项目专用环境,会使其中包与锁一致,请勿把它用于其他项目。
@@ -53,10 +53,14 @@ rsync -avP ~/robot/data/ USER@TRAIN_HOST:~/robot/data/
rsync -avP USER@TRAIN_HOST:~/robot/outputs/ ~/robot/outputs/
```
每个实验保留数据版本/校验值、模型与处理器、配置、校准备份和评测记录。模型快照记录来源 revision,不自动随 Hub main 更新。同步代码使用 git;当前未配置 GPU 主机远程访问,因此 GPU 执行结果需从目标机器回传。
每个实验保留数据版本/校验值、模型与处理器、配置、校准备份和评测记录。模型快照记录来源 revision,不自动随 Hub main 更新。同步代码使用 git;3080 为当前本机,3090 的远程访问尚未验证。
## 验收报告
脚本失败即非零退出,不把 MISS 当通过;输出完整日志、包列表、Python/FFmpeg 版本、CLI 帮助与锁哈希。脚本验证 CUDA 矩阵运算、实际策略模块和视频库导入,不连接电机,也不下载模型权重。
后续必须记录:录制/解码结果、训练步耗时、训练和推理峰值显存、单次推理 P50/P95/P99,以及 20 次真机评测。不要将包可导入或显存参考值当作任务能力证据。
## 2026-09-10 本机更新
内核 `7.0.0-31-generic`,NVIDIA 驱动 `580.173.02`,CUDA 实际训练已通过。上表 550 驱动为首次验收的历史配置。当前项目路径 `~/lerobot_learn`,任务数据在项目 `data/`,模型与日志在项目 `outputs/train/`;环境仍为 `~/robot/.venv-lerobot-0.6.1`。本次 ACT 基线在 3080 训练,不依赖 3090。
+2456
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
e236f8a3986eecd6bafb490108f52dd8adc1c9f76fd85d30c49a67ea00e3fd80 requirements.in
+5
View File
@@ -2,3 +2,8 @@
# Resolve once on 3080; share the lock and its input fingerprint with 3090.
# ACT uses the base dependencies; v0.6.1 has no 'act' extra.
lerobot[core_scripts,training,feetech,smolvla]==0.6.1
# Keep CUDA 12 wheels for the 3080 host's 550 driver; unconstrained resolution
# selects torch 2.11 / CUDA 13. TorchCodec must match PyTorch's ABI.
torch==2.7.1
torchvision==0.22.1
torchcodec==0.4.0
+8 -1
View File
@@ -18,7 +18,7 @@ esac
[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || {
echo "Only Linux x86_64 GPU hosts are supported; RPi needs a separate environment." >&2; exit 1;
}
for cmd in uv nvidia-smi ffmpeg sha256sum; do
for cmd in uv nvidia-smi ffmpeg sha256sum gcc; do
command -v "$cmd" >/dev/null || { echo "Missing prerequisite: $cmd" >&2; exit 1; }
done
if ! $RESOLVE && [[ ! -s "$LOCK" ]]; then
@@ -34,12 +34,19 @@ exec > >(tee "$REPORT/setup.log") 2>&1
echo "[1/5] Prerequisites"
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
uv --version
cat /etc/os-release > "$REPORT/os-release.txt"
uname -srmo > "$REPORT/kernel.txt"
nvidia-smi > "$REPORT/nvidia-smi.txt"
ffmpeg -version > "$REPORT/ffmpeg.txt"
# Set UV_DEFAULT_INDEX explicitly if a mirror is needed. No automatic Hub/token routing.
echo "[2/5] Dedicated Python 3.12 environment"
if [[ ! -d "$VENV" ]]; then uv venv --python 3.12 "$VENV"; fi
PYTHON="${VENV}/bin/python"
"$PYTHON" -c 'import sys; assert sys.version_info[:2] == (3, 12), sys.version'
PYTHON_INCLUDE="$("$PYTHON" -c 'import sysconfig; print(sysconfig.get_path("include"))')"
[[ -f "$PYTHON_INCLUDE/Python.h" ]] || {
echo "Missing Python development headers: $PYTHON_INCLUDE/Python.h" >&2; exit 1;
}
if $RESOLVE; then
TEMP_LOCK="$(mktemp "${LOCK}.tmp.XXXXXX")"
trap 'rm -f -- "$TEMP_LOCK"' EXIT
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Offline synthetic-data acceptance. No robot access; no task-quality claim."""
import argparse
import json
import os
import re
from pathlib import Path
import subprocess
import sys
import time
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--height", type=int, default=96)
parser.add_argument("--width", type=int, default=128)
parser.add_argument("--episodes", type=int, default=3)
parser.add_argument("--frames-per-episode", type=int, default=24)
parser.add_argument("--batch-size", type=int, default=2)
parser.add_argument("--steps", type=int, default=10)
parser.add_argument("--chunk-size", type=int, default=8)
parser.add_argument("--pretrained-backbone", action="store_true")
args = parser.parse_args()
root = args.output.resolve()
root.mkdir(parents=True, exist_ok=False)
os.environ["HF_HUB_OFFLINE"] = "1"
os.environ["WANDB_MODE"] = "disabled"
import numpy as np
import torch
from lerobot.datasets.lerobot_dataset import LeRobotDataset
from lerobot.configs.video import RGBEncoderConfig
torch.set_num_threads(4)
assert torch.cuda.is_available()
features = {
"observation.state": {"dtype": "float32", "shape": (6,), "names": [f"joint_{i}" for i in range(6)]},
"action": {"dtype": "float32", "shape": (6,), "names": [f"joint_{i}" for i in range(6)]},
"observation.images.front": {"dtype": "video", "shape": (args.height, args.width, 3), "names": ["height", "width", "channels"]},
}
dataset = LeRobotDataset.create(
repo_id="local/p0_synthetic", root=root / "dataset", fps=10,
features=features, robot_type="synthetic", use_videos=True,
rgb_encoder=RGBEncoderConfig(vcodec="h264"), encoder_threads=2,
)
for episode in range(args.episodes):
for frame in range(args.frames_per_episode):
phase = frame / 10 + episode
state = np.sin(phase + np.arange(6)).astype(np.float32)
image = np.zeros((args.height, args.width, 3), dtype=np.uint8)
image[:, :, 1] = 40 + episode * 50
box = max(8, min(args.height, args.width) // 6)
x = frame * max(1, (args.width - box) // max(1, args.frames_per_episode - 1))
y = (args.height - box) // 2
image[y:y+box, x:x+box, 0] = 220
dataset.add_frame({"observation.state": state,
"action": np.sin(phase + 0.1 + np.arange(6)).astype(np.float32),
"observation.images.front": image, "task": "synthetic pipeline check"})
dataset.save_episode()
dataset.finalize()
from torchcodec.decoders import VideoDecoder
videos = list((root / "dataset").rglob("*.mp4"))
assert videos
for video in videos:
decoded = VideoDecoder(str(video))
assert len(decoded) > 0 and decoded[0].shape == (3, args.height, args.width)
frame_count = args.episodes * args.frames_per_episode
for backend in ("pyav", "torchcodec"):
loaded = LeRobotDataset("local/p0_synthetic", root=root / "dataset", video_backend=backend)
assert len(loaded) == frame_count
boundary_indices = sorted({0, frame_count - 1} | {
i for episode in range(1, args.episodes)
for i in (episode * args.frames_per_episode - 1, episode * args.frames_per_episode)
})
for index in boundary_indices:
item = loaded[index]
assert item["observation.images.front"].shape == (3, args.height, args.width)
assert torch.isfinite(item["action"]).all()
train_wrapper = (
"import json,torch; from pathlib import Path; "
"from lerobot.scripts.lerobot_train import main; "
"torch.cuda.reset_peak_memory_stats(); main(); "
f"Path({str(root / 'training_memory.json')!r}).write_text(json.dumps("
"{'allocated_mib':torch.cuda.max_memory_allocated()/2**20,"
"'reserved_mib':torch.cuda.max_memory_reserved()/2**20}))"
)
command = [sys.executable, "-c", train_wrapper,
"--policy.type=act", "--policy.device=cuda", "--policy.push_to_hub=false",
f"--policy.chunk_size={args.chunk_size}", f"--policy.n_action_steps={args.chunk_size}",
"--dataset.repo_id=local/p0_synthetic",
f"--dataset.root={root / 'dataset'}", "--dataset.video_backend=torchcodec",
f"--batch_size={args.batch_size}", f"--steps={args.steps}", "--num_workers=0", "--seed=42",
"--log_freq=1", "--save_freq=10", "--wandb.enable=false",
f"--output_dir={root / 'training'}", "--job_name=p0_synthetic"]
if not args.pretrained_backbone:
command.append("--policy.pretrained_backbone_weights=null")
env = dict(os.environ, OMP_NUM_THREADS="4", MKL_NUM_THREADS="4")
start = time.perf_counter()
with (root / "train.log").open("w") as log:
subprocess.run(command, env=env, stdout=log, stderr=subprocess.STDOUT, check=True)
elapsed = time.perf_counter() - start
checkpoints = sorted((root / "training" / "checkpoints").glob("[0-9]*/pretrained_model"))
assert checkpoints, "No checkpoint saved"
from lerobot.policies.act.modeling_act import ACTPolicy
from lerobot.policies.factory import make_pre_post_processors
checkpoint = checkpoints[-1]
policy = ACTPolicy.from_pretrained(checkpoint).to("cuda").eval()
pre, post = make_pre_post_processors(policy.config, pretrained_path=checkpoint,
preprocessor_overrides={"device_processor": {"device": "cuda"}})
observation = {k: v.unsqueeze(0) for k, v in loaded[0].items()
if k.startswith("observation.")}
timings = []
torch.cuda.reset_peak_memory_stats()
with torch.inference_mode():
for i in range(25):
policy.reset()
torch.cuda.synchronize()
begin = time.perf_counter()
action = post(policy.select_action(pre(observation)))
torch.cuda.synchronize()
assert action.shape == (1, 6) and torch.isfinite(action).all()
if i >= 5:
timings.append((time.perf_counter() - begin) * 1000)
report = {"status": "PASS", "scope": "synthetic software pipeline only; not robot/task validation",
"frames": frame_count, "episodes": args.episodes, "train_steps": args.steps,
"batch_size": args.batch_size, "chunk_size": args.chunk_size,
"image_shape": [args.height, args.width, 3],
"pretrained_backbone": args.pretrained_backbone,
"training_wall_seconds": elapsed,
# Upstream resets CUDA peak counters each step. Its per-step log is rounded to 0.01 GiB.
"training_peak_allocated_gib_from_step_logs": max(float(v) for v in re.findall(
r"mem_gb:([0-9.]+)", (root / "train.log").read_text())),
"training_last_step_memory": json.loads((root / "training_memory.json").read_text()),
"checkpoint": str(checkpoint),
"inference_ms_p50_p95_p99": np.percentile(timings, [50, 95, 99]).tolist(),
"inference_peak_allocated_mib": torch.cuda.max_memory_allocated() / 2**20,
"torch": torch.__version__, "cuda": torch.version.cuda,
"gpu": torch.cuda.get_device_name(0)}
(root / "result.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Shared task configuration; source from a launcher.
set -eo pipefail
TASK_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "$TASK_SCRIPT_DIR/../env/activate_lerobot.sh"
set -u
TASK_TEXT='Pick up the block and place it in the white box.'
DATASET_ID="${DATASET_ID:-local/block_white_box_v1}"
DATASET_ROOT="${DATASET_ROOT:-$LEROBOT_PROJECT_DIR/data/block_white_box_v1}"
TASK_CAMERAS='{"wrist": {"type": "opencv", "index_or_path": "/dev/v4l/by-id/usb-ZSKJ-260319-K_Integrated_Webcam_HD-video-index0", "width": 640, "height": 480, "fps": 30, "fourcc": "MJPG"}, "front": {"type": "opencv", "index_or_path": "/dev/v4l/by-id/usb-Sonix_Technology_Co.__Ltd._USB2.0_CAM1_USB2.0_CAM1-video-index0", "width": 640, "height": 480, "fps": 25, "fourcc": "MJPG"}}'
ROBOT_ARGS=(--robot.type=so101_follower "--robot.port=$FOLLOWER_PORT" --robot.id=zihao_follower_arm "--robot.cameras=$TASK_CAMERAS")
run_task() {
if [[ "${DRY_RUN:-0}" == 1 ]]; then
printf '%q ' "$@"
printf '\n'
return
fi
exec "$@"
}
check_task_devices() {
[[ "${DRY_RUN:-0}" == 1 ]] && return 0
local device
for device in "$FOLLOWER_PORT" "/dev/v4l/by-id/usb-ZSKJ-260319-K_Integrated_Webcam_HD-video-index0" "/dev/v4l/by-id/usb-Sonix_Technology_Co.__Ltd._USB2.0_CAM1_USB2.0_CAM1-video-index0"; do
[[ -r "$device" && -w "$device" ]] || { echo "设备未连接或缺少读写权限:$device" >&2; exit 1; }
done
}
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/block_white_box_common.sh"
if [[ "${RESUME:-false}" == true ]]; then
[[ -f "$DATASET_ROOT/meta/info.json" ]] || { echo "找不到可续录的数据:$DATASET_ROOT" >&2; exit 1; }
else
[[ ! -e "$DATASET_ROOT" ]] || { echo "目录已存在;续录请设置 RESUME=true,或指定新的 DATASET_ROOT / DATASET_ID。" >&2; exit 1; }
fi
check_task_devices
if [[ "${DRY_RUN:-0}" != 1 ]]; then
[[ -r "$LEADER_PORT" && -w "$LEADER_PORT" ]] || { echo "主臂未连接或缺少权限:$LEADER_PORT" >&2; exit 1; }
fi
printf '\n========== 积木放入白盒:准备采集 ==========\n'
echo "本次 ${NUM_EPISODES:-5} 条;每条最多 ${EPISODE_SECONDS:-30} 秒;复位最多 ${RESET_SECONDS:-30} 秒。"
echo "绿色横幅 = 正在录制;黄色横幅 = 复位 / 重录。"
echo "→ 完成当前阶段 ← 重录当前条 Esc 结束采集"
echo "请在此终端操作方向键,并留意阶段横幅。"
if [[ "${DRY_RUN:-0}" != 1 ]]; then
read -r -p "摆好积木、盒子和主从臂后,按 Enter 连接设备并开始录制:" _ready
fi
run_task python "$TASK_SCRIPT_DIR/record_with_prompts.py" "${ROBOT_ARGS[@]}" \
--teleop.type=so101_leader "--teleop.port=$LEADER_PORT" --teleop.id=zihao_leader_arm \
"--dataset.repo_id=$DATASET_ID" "--dataset.root=$DATASET_ROOT" \
"--dataset.single_task=$TASK_TEXT" --dataset.no_stamp=true \
--dataset.fps=25 "--dataset.num_episodes=${NUM_EPISODES:-5}" \
"--dataset.episode_time_s=${EPISODE_SECONDS:-30}" "--dataset.reset_time_s=${RESET_SECONDS:-30}" \
--dataset.push_to_hub=false --dataset.rgb_encoder.vcodec=h264 \
--display_data=true "--resume=${RESUME:-false}" "$@"
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Chinese phase banners for LeRobot recording; no changes to robot control."""
import sys
def show_phase(message: str) -> None:
if message.startswith("Recording episode "):
number = int(message.rsplit(" ", 1)[1]) + 1
title, color = f"● 正在录制|数据集第 {number} 条", "32"
lines = [
"现在操作主臂:抓起积木 → 放入白盒 → 松开 → 退开",
"完成:按 → 结束本条,进入复位(最后一条将保存退出)",
"失败:按 ← 标记重录,然后按提示复位",
"停止:按 Esc;当前片段可能保存,之后检查完整性",
]
elif message == "Reset the environment":
title, color = "● 复位阶段|当前不录入示范", "33"
lines = [
"把积木取出放回起点,并通过主臂让从臂回到起始姿态",
"注意:从臂仍跟随主臂!",
"准备好:按 → 继续;超时也会自动继续",
"上一条失败:按 ← 标记重录;按 Esc 结束采集",
]
elif message == "Re-record episode":
title, color = "● 已丢弃本条|即将重录", "33"
lines = ["等待绿色“正在录制”提示后,再开始抓取。"]
elif message == "Stop recording":
title, color = "● 正在结束采集|请等待保存与清理", "36"
lines = ["不要关闭终端;完成后会返回命令行。"]
elif message == "Exiting":
title, color = "● 采集程序已退出", "36"
lines = ["请检查实际保存条数及视频;提前结束的片段可能不完整。"]
else:
return
use_color = sys.stdout.isatty()
start = f"\033[1;{color}m" if use_color else ""
end = "\033[0m" if use_color else ""
print("\n" + start + "━" * 60, flush=True)
print(title, flush=True)
print("━" * 60, flush=True)
for line in lines:
print(" " + line, flush=True)
print("━" * 60 + end + "\n", flush=True)
if use_color:
print("\a", end="", flush=True)
def main() -> None:
if sys.argv[1:] == ["--preview"]:
for message in ["Recording episode 0", "Reset the environment", "Re-record episode", "Stop recording", "Exiting"]:
show_phase(message)
return
from lerobot.scripts import lerobot_record
original_log_say = lerobot_record.log_say
def log_with_banner(message, *args, **kwargs):
result = original_log_say(message, *args, **kwargs)
show_phase(message)
return result
lerobot_record.log_say = log_with_banner
lerobot_record.main()
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/block_white_box_common.sh"
if [[ -z "${MODEL_DIR:-}" ]]; then
echo "请设置 MODEL_DIR 为检查点的 pretrained_model 目录。" >&2; exit 1
fi
if [[ "${DRY_RUN:-0}" != 1 && ! -f "$MODEL_DIR/config.json" ]]; then
echo "模型配置不存在:$MODEL_DIR/config.json" >&2; exit 1
fi
check_task_devices
echo "自主执行 ${DURATION:-30} 秒;按 Ctrl+C 停止。"
run_task lerobot-rollout --strategy.type=base "${ROBOT_ARGS[@]}" \
"--policy.path=$MODEL_DIR" --policy.device=cuda "--task=$TASK_TEXT" \
--fps=25 "--duration=${DURATION:-30}" --display_data=true "$@"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Launch SO101 teleoperation with this machine's calibrated arms.
set -eo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "$script_dir/../env/activate_lerobot.sh"
set -u
check_device() {
if [[ ! -e "$1" ]]; then
echo "设备未连接:$1" >&2
exit 1
fi
if [[ ! -r "$1" || ! -w "$1" ]]; then
echo "设备缺少读写权限:$1,请检查串口 dialout / 摄像头 video 权限。" >&2
exit 1
fi
}
check_device "$FOLLOWER_PORT"
check_device "$LEADER_PORT"
echo "启动遥操作;按 Ctrl+C 停止。"
exec lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port="$FOLLOWER_PORT" \
--robot.id=zihao_follower_arm \
--robot.cameras='{}' \
--teleop.type=so101_leader \
--teleop.port="$LEADER_PORT" \
--teleop.id=zihao_leader_arm \
--display_data=false \
"$@"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Launch SO101 teleoperation with this machine's calibrated arms.
set -eo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "$script_dir/../env/activate_lerobot.sh"
set -u
check_device() {
if [[ ! -e "$1" ]]; then
echo "设备未连接:$1" >&2
exit 1
fi
if [[ ! -r "$1" || ! -w "$1" ]]; then
echo "设备缺少读写权限:$1,请检查串口 dialout / 摄像头 video 权限。" >&2
exit 1
fi
}
check_device "$FOLLOWER_PORT"
check_device "$LEADER_PORT"
check_device "/dev/v4l/by-id/usb-ZSKJ-260319-K_Integrated_Webcam_HD-video-index0"
echo "启动遥操作;按 Ctrl+C 停止。"
exec lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port="$FOLLOWER_PORT" \
--robot.id=zihao_follower_arm \
--robot.cameras='{"wrist": {"type": "opencv", "index_or_path": "/dev/v4l/by-id/usb-ZSKJ-260319-K_Integrated_Webcam_HD-video-index0", "width": 640, "height": 480, "fps": 30, "fourcc": "MJPG"}}' \
--teleop.type=so101_leader \
--teleop.port="$LEADER_PORT" \
--teleop.id=zihao_leader_arm \
--display_data=true \
"$@"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Launch SO101 teleoperation with this machine's calibrated arms.
set -eo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "$script_dir/../env/activate_lerobot.sh"
set -u
check_device() {
if [[ ! -e "$1" ]]; then
echo "设备未连接:$1" >&2
exit 1
fi
if [[ ! -r "$1" || ! -w "$1" ]]; then
echo "设备缺少读写权限:$1,请检查串口 dialout / 摄像头 video 权限。" >&2
exit 1
fi
}
check_device "$FOLLOWER_PORT"
check_device "$LEADER_PORT"
check_device "/dev/v4l/by-id/usb-ZSKJ-260319-K_Integrated_Webcam_HD-video-index0"
check_device "/dev/v4l/by-id/usb-Sonix_Technology_Co.__Ltd._USB2.0_CAM1_USB2.0_CAM1-video-index0"
echo "启动遥操作;按 Ctrl+C 停止。"
exec lerobot-teleoperate \
--robot.type=so101_follower \
--robot.port="$FOLLOWER_PORT" \
--robot.id=zihao_follower_arm \
--robot.cameras='{"wrist": {"type": "opencv", "index_or_path": "/dev/v4l/by-id/usb-ZSKJ-260319-K_Integrated_Webcam_HD-video-index0", "width": 640, "height": 480, "fps": 30, "fourcc": "MJPG"}, "front": {"type": "opencv", "index_or_path": "/dev/v4l/by-id/usb-Sonix_Technology_Co.__Ltd._USB2.0_CAM1_USB2.0_CAM1-video-index0", "width": 640, "height": 480, "fps": 25, "fourcc": "MJPG"}}' \
--teleop.type=so101_leader \
--teleop.port="$LEADER_PORT" \
--teleop.id=zihao_leader_arm \
--display_data=true \
"$@"
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Move one calibrated SO-101 follower joint a few degrees and return."""
import argparse
import os
import time
from pathlib import Path
from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
JOINTS = (
"shoulder_pan",
"shoulder_lift",
"elbow_flex",
"wrist_flex",
"wrist_roll",
"gripper",
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--port",
default=os.environ.get("FOLLOWER_PORT"),
help="Follower serial port; defaults to FOLLOWER_PORT from env/activate_lerobot.sh.",
)
parser.add_argument("--robot-id", default="my_follower", help="ID used when calibrating the follower.")
parser.add_argument("--joint", choices=JOINTS, default="wrist_roll")
parser.add_argument("--delta", type=float, default=3.0, help="Signed movement in degrees (gripper: percent).")
parser.add_argument("--duration", type=float, default=1.0, help="Seconds for each outbound/return motion.")
return parser.parse_args()
def interpolate(robot: SO101Follower, joint: str, start: float, delta: float, duration: float) -> None:
steps = max(2, round(duration * 20))
for step in range(1, steps + 1):
target = start + delta * step / steps
robot.bus.write("Goal_Position", joint, target)
time.sleep(duration / steps)
def main() -> None:
args = parse_args()
if not args.port:
raise SystemExit("Missing port. Run: source env/activate_lerobot.sh")
port = Path(args.port)
if not port.exists():
raise SystemExit(f"Follower port is not connected: {port}")
if not os.access(port, os.R_OK | os.W_OK):
raise SystemExit(
f"No read/write permission for {port}. Add the user to dialout, then log in again or run newgrp dialout."
)
if not 0 < abs(args.delta) <= 5:
raise SystemExit("For this first-motion test, --delta must be non-zero and at most 5.")
if not 0.5 <= args.duration <= 5:
raise SystemExit("--duration must be between 0.5 and 5 seconds.")
config = SO101FollowerConfig(port=str(port), id=args.robot_id)
robot = SO101Follower(config)
if not robot.calibration_fpath.is_file():
raise SystemExit(
f"Calibration file not found: {robot.calibration_fpath}\n"
"Calibrate first with lerobot-calibrate, using the same --robot.id."
)
print(f"Calibration: {robot.calibration_fpath}")
print(f"Test: {args.joint} {args.delta:+g}, then return to the measured start position.")
print("Place the arm in a supported pose, clear the workspace, and keep power cutoff within reach.")
if input("Type MOVE and press Enter to continue: ").strip() != "MOVE":
raise SystemExit("Cancelled; no movement command was sent.")
selected_torque_enabled = False
try:
# Use the motor bus directly: SOFollower.connect() configures and enables
# every motor, which is inappropriate for a first single-joint test.
robot.bus.connect()
robot.bus.disable_torque()
if not robot.bus.is_calibrated:
raise RuntimeError("Motor calibration does not match the local calibration file.")
start = float(robot.bus.read("Present_Position", args.joint))
print(f"Start {args.joint}.pos: {start:.2f}")
# Set a non-moving goal before enabling torque, avoiding a stale goal
# register from a previous session.
robot.bus.write("Goal_Position", args.joint, start)
robot.bus.enable_torque(args.joint)
selected_torque_enabled = True
interpolate(robot, args.joint, start, args.delta, args.duration)
time.sleep(0.3)
outbound = float(robot.bus.read("Present_Position", args.joint))
actual_delta = outbound - start
print(f"Outbound {args.joint}.pos: {outbound:.2f} (actual {actual_delta:+.2f})")
if actual_delta * args.delta <= 0 or abs(actual_delta) < abs(args.delta) * 0.5:
raise RuntimeError(
f"Joint feedback did not follow the command: requested {args.delta:+.2f}, observed {actual_delta:+.2f}."
)
interpolate(robot, args.joint, start + args.delta, -args.delta, args.duration)
time.sleep(0.3)
returned = float(robot.bus.read("Present_Position", args.joint))
return_error = returned - start
print(f"Returned {args.joint}.pos: {returned:.2f} (error {return_error:+.2f})")
if abs(return_error) > 1.0:
raise RuntimeError(f"Joint did not return within 1 degree of start; error is {return_error:+.2f}.")
print("Motion test completed and verified by position feedback.")
finally:
if robot.bus.is_connected:
try:
if selected_torque_enabled:
print(f"Disabling torque on {args.joint}.")
robot.bus.disable_torque(args.joint, num_retry=5)
finally:
robot.bus.disconnect(disable_torque=False)
if __name__ == "__main__":
main()
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/block_white_box_common.sh"
if [[ "${DRY_RUN:-0}" != 1 && ! -f "$DATASET_ROOT/meta/info.json" ]]; then
echo "请先录制并检查数据:$DATASET_ROOT" >&2; exit 1
fi
if [[ "${DRY_RUN:-0}" != 1 ]]; then
python -c 'import torch; import sys; sys.exit(0 if torch.cuda.is_available() else "CUDA 不可用,请先修复 NVIDIA 驱动;已停止训练以避免回退到 CPU。")'
fi
TRAIN_OUTPUT="${TRAIN_OUTPUT:-$LEROBOT_PROJECT_DIR/outputs/train/act_block_white_box_$(date +%Y%m%d_%H%M%S)}"
run_task lerobot-train --policy.type=act --policy.device=cuda --policy.push_to_hub=false \
"--dataset.repo_id=$DATASET_ID" "--dataset.root=$DATASET_ROOT" \
"--output_dir=$TRAIN_OUTPUT" --job_name=act_block_white_box \
"--batch_size=${BATCH_SIZE:-8}" "--steps=${STEPS:-20000}" --num_workers=4 \
--save_freq=2000 --env_eval_freq=0 --seed=42 --wandb.enable=false "$@"