From cbf51facff50e5aa35d84b03eba4ed39b23ecce6 Mon Sep 17 00:00:00 2001 From: ylhan Date: Thu, 10 Sep 2026 16:46:58 +0800 Subject: [PATCH] feat: add SO101 recording and ACT workflow with progress tracking --- PLAN.md | 2 +- README.md | 34 +++++++- docs/act_block_white_box.md | 59 +++++++++++++ docs/cli_reference.md | 15 +++- docs/progress.md | 84 ++++++++++++++++++ env/activate_lerobot.sh | 45 ++++++++++ env/machines.md | 6 +- robots/block_white_box_common.sh | 26 ++++++ robots/record_block_white_box.sh | 27 ++++++ robots/record_with_prompts.py | 67 ++++++++++++++ robots/rollout_act_block_white_box.sh | 13 +++ robots/teleoperate_no_camera.sh | 32 +++++++ robots/teleoperate_one_camera.sh | 33 +++++++ robots/teleoperate_two_cameras.sh | 34 ++++++++ robots/test_follower_motion.py | 120 ++++++++++++++++++++++++++ robots/train_act_block_white_box.sh | 14 +++ 16 files changed, 604 insertions(+), 7 deletions(-) create mode 100644 docs/act_block_white_box.md create mode 100644 docs/progress.md create mode 100755 env/activate_lerobot.sh create mode 100644 robots/block_white_box_common.sh create mode 100755 robots/record_block_white_box.sh create mode 100644 robots/record_with_prompts.py create mode 100755 robots/rollout_act_block_white_box.sh create mode 100755 robots/teleoperate_no_camera.sh create mode 100755 robots/teleoperate_one_camera.sh create mode 100755 robots/teleoperate_two_cameras.sh create mode 100755 robots/test_follower_motion.py create mode 100755 robots/train_act_block_white_box.sh diff --git a/PLAN.md b/PLAN.md index cc94977..f381a92 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # LeRobot 双臂移动操作项目方案 > 决策与阶段验收以本文件为准。更新:2026-09-08,设计评审修订。 -> 当前 P0:3080 基础环境与合成数据 ACT 管线通过,见 [实测记录](docs/p0-3080-20260908.md);3090 和真机未验收。 +> 2026-09-10 执行进展:P1 遥操与真实数据采集已完成基础验证,3080 正在训练 ACT;自主抓放未验收。详情见 [进度追踪](docs/progress.md)。3090 仍待验收,当前基线训练先在 3080 完成。 ## 1. 目标与边界 diff --git a/README.md b/README.md index 883dc46..a8f379e 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ 基于 LeRobot,建立人工遥操示范、采集、训练与真机执行闭环。先完成单臂抓放,再扩展桌面双臂,最后实现“移动到位 → 停稳 → 操作”的分阶段任务。 -当前处于 P0:3080 基础环境、视频读写、合成数据 ACT 短训练及模型重载已通过;3090 与真机尚未验收。详见 [本机验收记录](docs/p0-3080-20260908.md) 和 [到货前复验步骤](docs/before_hardware.md)。 +当前进入 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:预训练基座微调和语言条件探索。 @@ -37,3 +38,30 @@ bash ~/robot/proj/env/setup_lerobot.sh ``` 仓库已附本机验证的依赖锁及输入指纹,默认按锁安装;不要再次使用 `--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)。 diff --git a/docs/act_block_white_box.md b/docs/act_block_white_box.md new file mode 100644 index 0000000..7066df0 --- /dev/null +++ b/docs/act_block_white_box.md @@ -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)。 diff --git a/docs/cli_reference.md b/docs/cli_reference.md index 59f7273..181a5bf 100644 --- a/docs/cli_reference.md +++ b/docs/cli_reference.md @@ -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 diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 0000000..924a567 --- /dev/null +++ b/docs/progress.md @@ -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 短训练及正式训练启动。 diff --git a/env/activate_lerobot.sh b/env/activate_lerobot.sh new file mode 100755 index 0000000..745d877 --- /dev/null +++ b/env/activate_lerobot.sh @@ -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 diff --git a/env/machines.md b/env/machines.md index f24ceba..1d1f387 100644 --- a/env/machines.md +++ b/env/machines.md @@ -1,6 +1,6 @@ # GPU 环境与机器记录 -基线:LeRobot 0.6.1 / Python 3.12。脚本适用 Ubuntu x86_64 GPU 主机;不用于 Windows 或 RPi。3080 的 CUDA、视频和合成 ACT 短训练已验收,详见 [记录](../docs/p0-3080-20260908.md);3090 与真实任务未验收。 +基线: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)。 ## 分工与路径 @@ -60,3 +60,7 @@ rsync -avP USER@TRAIN_HOST:~/robot/outputs/ ~/robot/outputs/ 脚本失败即非零退出,不把 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。 diff --git a/robots/block_white_box_common.sh b/robots/block_white_box_common.sh new file mode 100644 index 0000000..e017ad4 --- /dev/null +++ b/robots/block_white_box_common.sh @@ -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 +} diff --git a/robots/record_block_white_box.sh b/robots/record_block_white_box.sh new file mode 100755 index 0000000..6faa774 --- /dev/null +++ b/robots/record_block_white_box.sh @@ -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}" "$@" diff --git a/robots/record_with_prompts.py b/robots/record_with_prompts.py new file mode 100644 index 0000000..b295de4 --- /dev/null +++ b/robots/record_with_prompts.py @@ -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() diff --git a/robots/rollout_act_block_white_box.sh b/robots/rollout_act_block_white_box.sh new file mode 100755 index 0000000..0b5476c --- /dev/null +++ b/robots/rollout_act_block_white_box.sh @@ -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 "$@" diff --git a/robots/teleoperate_no_camera.sh b/robots/teleoperate_no_camera.sh new file mode 100755 index 0000000..f0fa955 --- /dev/null +++ b/robots/teleoperate_no_camera.sh @@ -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 \ + "$@" diff --git a/robots/teleoperate_one_camera.sh b/robots/teleoperate_one_camera.sh new file mode 100755 index 0000000..2971abb --- /dev/null +++ b/robots/teleoperate_one_camera.sh @@ -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 \ + "$@" diff --git a/robots/teleoperate_two_cameras.sh b/robots/teleoperate_two_cameras.sh new file mode 100755 index 0000000..b39c4de --- /dev/null +++ b/robots/teleoperate_two_cameras.sh @@ -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 \ + "$@" diff --git a/robots/test_follower_motion.py b/robots/test_follower_motion.py new file mode 100755 index 0000000..efb9441 --- /dev/null +++ b/robots/test_follower_motion.py @@ -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() diff --git a/robots/train_act_block_white_box.sh b/robots/train_act_block_white_box.sh new file mode 100755 index 0000000..4a883c6 --- /dev/null +++ b/robots/train_act_block_white_box.sh @@ -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 "$@"