ros2微雪SO-ARM101机械臂
nodaoliSO-ARM100/101 - Waveshare Wiki
https://wiki.seeedstudio.com/cn/lerobot_so100m_new
【SO-ARM机械臂Lerobot超详细上手教程:1序言】 https://www.bilibili.com/video/BV1H6UUBcErT/?share_source=copy_web&vd_source=9c987ed6ab5014f976c5bd5305300cc0
LeRobot SO-101 机械臂实践 · MyBlog
启用MuJoCo仿真
拉取官方lerobot仓库
1
| git clone https://github.com/huggingface/lerobot.git ~/lerobot
|
使用UV安装GPU-Torch 和 MuJoCo
1 2 3 4 5 6
| uv venv uv pip install torch --torch-backend=auto uv add mujoco feetech-servo-sdk uv sync
uv pip install -e .
|
配置舵机id
SO-ARM100/101 设置舵机的 ID - Waveshare Wiki
跟着教程,其实就是先电脑连接上控制板,使用命令查看端口号,因为使用uv虚拟环境,所以最好使用uv run
1
| uv run lerobot-find-port
|
开始配置
1
| uv run lerobot-setup-motors --robot.type=so101_follower --robot.port=COM13
|
其中robot.type指的是从臂:so101_follower
改成teleop.type就可以配置主臂,端口也要改成teleop.port
然后从爪子开始,每次只连接一个到控制板上面,然后按回车会依次进行编号
校准
1
| uv run lerobot-calibrate --teleop.type=so101_leader --teleop.port=COM12 --teleop.id=so101_leader
|
需要先把每个关节舵机调整成中间,然后在进行校准,扭动舵机,从最小到最大
进行测试
先去收录仓库下载MuJoCo的文件,选中的是scene.xml
如果在上面的步骤中使用uv add mujoco的话,那么把下面的代码在lerobot目录下创建
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
| import time import mujoco import mujoco.viewer import numpy as np
from lerobot.teleoperators.so_leader.so_leader import SOLeader from lerobot.teleoperators.so_leader.config_so_leader import SOLeaderTeleopConfig
XML_PATH = r"D:\tempfolder\arm\101\scene.xml" LEADER_PORT = "COM12"
JOINT_NAMES = [ "shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper", ]
def main(): print(f"正在初始化 SOLeader,端口: {LEADER_PORT}...") leader_config = SOLeaderTeleopConfig(port=LEADER_PORT, use_degrees=True) leader = SOLeader(leader_config) try: leader.connect() print("主臂连接成功!") except Exception as e: print(f"主臂连接失败: {e}") print("请检查端口和连接是否正常。") return
print(f"正在加载 Mujoco 模型: {XML_PATH}...") try: m = mujoco.MjModel.from_xml_path(XML_PATH) d = mujoco.MjData(m) except Exception as e: print(f"Mujoco 模型加载失败: {e}") leader.disconnect() return
print("开始遥操作循环...") print("按 Ctrl+C 停止。")
with mujoco.viewer.launch_passive(m, d) as viewer: start_time = time.time() try: while viewer.is_running(): leader_action = leader.get_action() for i, name in enumerate(JOINT_NAMES): motor_key = f"{name}.pos" if motor_key in leader_action: deg_val = leader_action[motor_key] rad_val = np.deg2rad(deg_val) joint_id = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_JOINT, name) if joint_id != -1: qpos_adr = m.jnt_qposadr[joint_id] d.qpos[qpos_adr] = rad_val mujoco.mj_step(m, d) viewer.sync() time.sleep(1.0/60.0)
except KeyboardInterrupt: print("正在停止...") finally: leader.disconnect() print("断开连接。")
if __name__ == "__main__": main()
|
Python引用
1 2 3 4 5 6 7
| from lerobot.teleoperators.so_leader.so_leader import SOLeader from lerobot.teleoperators.so_leader.config_so_leader import SOLeaderTeleopConfig
leader_config = SOLeaderTeleopConfig(port=LEADER_PORT, use_degrees=False, id=TELEOP_ID, calibration_dir=CALIBRATION_DIR)
leader = SOLeader(leader_config)
|
遥控
1 2 3 4 5 6 7 8 9
| lerobot-teleoperate \ --robot.type=so101_follower \ --robot.port=/dev/tty.usbmodem58760431541 \ --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 1920, height: 1080, fps: 30}}" \ --robot.id=black \ --teleop.type=so101_leader \ --teleop.port=/dev/tty.usbmodem58760431551 \ --teleop.id=blue \ --display_data=true
|
采集数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| lerobot-record \ --robot.type=so101_follower \ --robot.port=/dev/tty.usbmodem585A0076841 \ --robot.id=my_awesome_follower_arm \ --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 1920, height: 1080, fps: 30}}" \ --teleop.type=so101_leader \ --teleop.port=/dev/tty.usbmodem58760431551 \ --teleop.id=my_awesome_leader_arm \ --display_data=true \ --dataset.repo_id=${HF_USER}/record-test \ --dataset.num_episodes=5 \ --dataset.single_task="Grab the black cube" \ --dataset.streaming_encoding=true \ # --dataset.vcodec=auto \ --dataset.encoder_threads=2
|
训练
1 2 3 4 5 6 7 8 9 10 11 12
| uv run lerobot-train \ --policy.type=act \ --dataset.repo_id=nodaoli/my_so101_dataset \ --steps=20000 \ --save_freq=5000 \ --eval_freq=-1 \ --batch_size=8 \ --output_dir=outputs/train/my_so101_act_model \ --wandb.enable=false \ --policy.device=cuda \ --policy.push_to_hub=false \ --policy.repo_id=nodaoli/my_local_act_model
|
推荐添加wandb来监视
1 2 3 4 5 6 7
| wandb login
--dataset_repo_id=your_username/your_dataset \ --policy.type=act \ --wandb.enable=True \ --wandb.project=my_robot_project \ --wandb.notes="第一次尝试 ACT 训练"
|
推理
需要使用一个“==异步推理==”
这是服务端代码
1 2 3 4 5 6
| uv run python -m lerobot.async_inference.policy_server \ --host=127.0.0.1 \ --port=8080 \ --fps=10 \ --inference_latency=0.1 \ --obs_queue_timeout=2
|
这是客户端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| uv run python -m lerobot.async_inference.robot_client \ --robot.type=so101_follower \ --robot.port=/dev/lerobot_follower \ --robot.cameras="{ top: {type: opencv, index_or_path: /dev/camera_top, width: 640, height: 480, fps: 30, fourcc: 'MJPG'}, hand: {type: opencv, index_or_path: /dev/camera_hand, width: 640, height: 480, fps: 30, fourcc: 'MJPG'} }" \ --robot.id=follower_petg \ --robot.calibration_dir=calibration \ --task="grasp tomato" \ --server_address=127.0.0.1:8080 \ --policy_type=act \ --pretrained_name_or_path=/home/qwe/nodaoli/EmbodiedAgriNet/robot/outputs/train/tomato_normal/checkpoints/020000/pretrained_model \ --policy_device=cuda \ --client_device=cpu \ --actions_per_chunk=300 \ --chunk_size_threshold=0.5 \ --aggregate_fn_name=weighted_average \ --fps=30
|
数据集
默认在~/.cache/huggingface/lerobot/calibration,是根据id来查找数据集的
- id: 电机 ID
- drive_mode: 驱动模式
- homing_offset: 归位偏移量
- range_min: 最小位置范围
- range_max: 最大位置范围
dataset.repo_id 是数据集的名称标识符,定义在 DatasetConfig 中,格式约定为 “{用户名}/{数据集名称}”(例如 lerobot/my_dataset)。
这个 ID 不一定非得是你的 HuggingFace 用户名,它本质上只是一个”路径标识”,用于确定数据存在本地哪个文件夹。
LeRobotDataset 和 LeRobotDatasetMetadata 初始化时,会这样计算本地路径:
如果你没有指定 root:路径 = HF_LEROBOT_HOME / repo_id,即默认是 ~/.cache/huggingface/lerobot/{repo_id}
如果你指定了 root:路径 = root / repo_id(你自定义的目录)
解释
为什么需要分配id
简单来说,舵机分配 ID 的过程就是通过串口通讯修改舵机内部芯片的“身份证号”(EEPROM 配置)。
以下是这一过程的技术原理和操作逻辑拆解:
- 为什么需要分配 ID?
• 出厂默认 ID 都是 1:所有的 Feetech STS3215 舵机出厂时,内部存储器里的 ID 号都被写死为 1。
• 总线通讯原理:这些舵机使用的是**串行总线(Serial Bus)**通信。这意味着所有舵机都挂在同一根通讯线上(Tx/Rx)。当电脑发送指令“ID 为 1 的舵机请转动”时,如果线上挂着 6 个 ID 都是 1 的舵机,它们会同时响应,或者返回的数据会互相干扰(总线冲突),导致通信失败。
• 区分关节:为了让电脑能单独指挥“手腕”或“底座”,我们必须给每个关节分配一个独一无二的数字(1 到 6)。
- 分配 ID 的核心逻辑(如何改写)
无论是使用 Python 脚本还是 Windows 软件,底层的操作步骤都是一样的:
1. 物理隔离(关键步骤):
◦ 因为所有新舵机 ID 都是 1,你不能把它们串联起来一次性改。如果串联了,电脑发“把 ID 1 改成 6”,所有舵机都会变成 6。
◦ 因此,必须只连接一个舵机到驱动板上,。
2. 发送改写指令:
◦ 电脑通过 USB 转串口(驱动板)发送一串十六进制指令。
◦ 指令含义大致是:[广播/针对ID 1] [写入内存] [地址: ID寄存器] [新数值: 6]。
◦ 这个新 ID 会被写入舵机的 EEPROM(非易失性存储器),这意味着断电后 ID 依然会保存,不需要每次开机都重设。
3. 验证:
◦ 写入后,程序通常会尝试用新 ID(比如 6)去 ping 一下舵机。如果舵机有回应,说明修改成功。
- LeRobot 的分配策略(倒序法)
LeRobot 官方脚本采用了一种特定的顺序,为了防止混淆,通常建议您遵循这个映射表,:
| 关节名称 (Joint Name) |
部位描述 |
分配 ID |
| Gripper |
夹爪(最末端) |
6 |
| Wrist Roll |
手腕旋转 |
5 |
| Wrist Flex |
手腕弯曲 |
4 |
| Elbow |
肘部 |
3 |
| Shoulder Lift |
肩部升降 |
2 |
| Shoulder Pan |
底座旋转 |
1 |
脚本的操作流程是这样的,:
1. 程序提示:Connect ... 'gripper' motor only。
2. 您只插入夹爪舵机。
3. 程序扫描(通常扫描 ID 1),找到舵机。
4. 程序发送指令:将 ID 1 改为 6。
5. 程序提示:Connect ... 'wrist_roll' motor only。
6. 您拔掉夹爪舵机,换上第二个舵机(它是新的,ID 还是 1)。
7. 程序发送指令:将 ID 1 改为 5。
8. ……以此类推,直到底座(ID 1)。
- 如果您使用 Windows GUI 工具 (Feetech/Hiwonder)
既然您在 Windows 上,且之前的脚本有报错,您也可以用“上帝视角”手动修改,原理更直观:
1. 打开 BusLinker 或 FD (Feetech Debugger) 软件。
2. 连接一个舵机。
3. 点击 “Search” (搜索) -> 软件会显示探测到 ID: 1。
4. 在 ID 设置栏输入 6,点击 “Set” (设置/写入)。
5. 此时软件再搜索,就会显示探测到 ID: 6。
6. 拔掉它,贴上标签“6”,换下一个,重复步骤改为 5、4、3…
总结
这个过程本质上就是给每个刚出生的“克隆人”(ID 1)发一张不同的身份证(ID 1-6),这样以后它们坐在同一条船(总线)上时,电脑叫谁的名字,谁才会答应。
⚠️ 重要提示:一旦您把 ID 改乱了(比如两个舵机都设成了 3),把它们串联在一起时就会报错。解决方法是单独把其中一个连上电脑,重新把 ID 改掉。