概述

项目介绍和功能特性

项目介绍和功能特性 Desktop-Wanderer 是一个基于视觉引导的小型机器人系统,实现了自主目标检测、导航和抓取功能。系统采用模块化设计,支持多种硬件加速方案,具备以下核心功能特性:

  • 视觉感知:基于YOLO的目标检测系统,支持ONNX CPU推理和Atlas 310B、RK3588 NPU硬件加速 process.py
  • 智能导航:视觉引导的移动控制,能够自动导航至目标位置 move_controller.py
  • 精确操作:支持两种机械臂控制模式 - ACT学习策略和逆运动学控制 setup.py
  • 状态管理:基于状态机的行为控制,实现搜索-抓取的自动化流程 setup.py

系统架构概览

Desktop-Wanderer 采用四层分层架构设计,从应用层到硬件层逐级抽象:

img.png

系统核心是位于 src/main.py 的主控制循环 main.py ,负责协调各个子系统:

  • 初始化阶段:加载配置、初始化机器人连接、读取初始关节角度
  • 感知阶段:获取机器人观测数据,执行YOLO目标检测
  • 决策阶段:根据当前状态选择相应控制器(移动控制或机械臂控制)
  • 执行阶段:发送控制指令到机器人,维持指定FPS控制频率

快速开始指南

环境准备

  1. 硬件要求:
  • LeKiwi机器人平台
  • USB串口连接舵机控制板
  • 12V8A电源连接舵机控制板
  1. 软件依赖

pip install -r requirements.txt

  1. lerobot 平台安装

python 3.10环境准备

wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash Miniforge3-$(uname)-$(uname -m).sh
conda create -y -n lerobot python=3.10

lerobot 安装

conda activate lerobot
conda install ffmpeg -c conda-forge
git clone https://github.com/huggingface/lerobot.git
cd lerobot
pip install -e .
# 舵机驱动安装
pip install -e ".[feetech]"

详细说明可看安装与配置部分

配置设置

修改 config.yaml 配置文件:

port: /dev/tty.usbmodem5AE60581751 # 串口号
fps: 20 # 帧率
log_level: INFO # 日志级别
hardware_mode: normal # normal, 310b, rk3588
control_mode: inverse # inverse, act

安装与配置 (Installation & Configuration)

环境要求和依赖

在PC上进行调试需要安装lerobot平台,具体安装方法可以参考 lerobot_Installation,下文也会给出。

安装miniforge

wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash Miniforge3-$(uname)-$(uname -m).sh

创建python3.10环境

conda create -y -n lerobot python=3.10

激活环境

conda activate lerobot

conda安装ffmpeg

conda install ffmpeg -c conda-forge

从源码下载安装lerobot

克隆仓库

git clone https://github.com/huggingface/lerobot.git
cd lerobot

安装

pip install -e .

安装舵机控制SDK

pip install -e ".[feetech]"

基础环境准备完毕,在代码根目录运行

python -m src.main

配置文件说明

配置文件内容

port: /dev/ttyACM0 # 在控制板上的默认端口号
fps: 20 # 每秒识别20帧
log_level: INFO # 日志级别
hardware_mode: normal # normal, rk3588 支持的硬件
control_mode: inverse # inverse, act 行动模式

其中

  • port:在安装lerobot后,处于lerobot环境下,将设备连接到电脑运行lerobot-find-port,之后会出现一堆设备号,根据指示,拔掉连接线后按回车,即可获得串口号,控制板上的环境已经安装完毕,端口号为固定的 '/dev/ttyACM0'
  • hardware_mode: 在PC上调试的时候选择normal模式,在控制板上运行的时候,为rk3588

硬件连接设置

核心架构 (Core Architecture)

四层架构设计

模块依赖关系

初始化流程

状态机 (State Machine)

RobotStatus 枚举定义

文件: src/setup.py

class RobotStatus(Enum):
    SEARCH = "search"       # 找球模式
    PICK = "pick"          # 捡球模式
    FIND_BUCKET = "find_bucket"  # 找桶模式
    PUT_BALL = "put_ball"  # 放球模式

状态转换图

                    ┌─────────────────────────────────────────────────────┐
                    │                                                     │
                    ▼                                                     │
┌─────────┐    ┌─────────┐    ┌──────────────┐    ┌──────────┐    ┌─────────┐
│ SEARCH  │───▶│  PICK   │───▶│ FIND_BUCKET  │───▶│ PUT_BALL │───▶│ SEARCH  │
└─────────┘    └─────────┘    └──────────────┘    └──────────┘    └─────────┘
     ▲              │                 │                │              │
     │              │                 │                │              │
     └──────────────┴─────────────────┴────────────────┴──────────────┘
                              (reset_robot)

状态详细说明

1. SEARCH(找球模式)

进入条件

  • 系统启动后初始状态
  • PUT_BALL 完成后返回

行为

  • 使用 move_controller() 控制底盘移动
  • 调用 yolo_infer() 进行目标检测(找球)
  • 检测到球并在视野中央稳定 10 帧后进入 PICK 状态

目标检测区域

┌─────────────────────────────────┐
│                                 │
│    ┌───────────────────┐       │
│    │                   │       │
│    │   TARGET ZONE     │       │  ← 球需要在这个区域内
│    │                   │       │
│    └───────────────────┘       │
│                                 │
└─────────────────────────────────┘

2. PICK(捡球模式)

进入条件

  • SEARCH 状态下检测到球并稳定 10 帧

行为

  • 使用 p_control_loop() 执行 CATCH_ACTION 动作序列
  • 逆运动学控制机械臂移动到球的位置
  • 执行夹爪动作抓住球
  • 动作序列完成后进入 FIND_BUCKET 状态

动作序列

CATCH_ACTION = [
    ("move_to", (0.0989, 0.125)),    # 移动到初始位置
    ("shoulder_pan", -12),            # 肩部旋转
    ("gripper", 60),                   # 张开夹爪
    ("wrist_flex", 80),               # 腕部弯曲
    ("move_to", (0.140, 0.1211)),     # 移动到球上方
    ("move_to", (0.140, -0.05)),      # 下降到球位置
    ("gap", 0),                        # 停顿
    ("gripper", -60),                  # 闭合夹爪(夹住球)
    ("gap", 0),                        # 停顿
    ("shoulder_pan", 12),              # 肩部归位
    ("move_to", (-0.1, 0.2)),          # 举起球
    ("wrist_flex", -20)                # 腕部配合
]

3. FIND_BUCKET(找桶模式)

进入条件

  • PICK 状态动作序列执行完成

行为

  • 检查夹爪是否仍然夹住球(gripper_pos > 25
  • 如果球丢失,返回 SEARCH 状态重新寻找
  • 使用 move_controller_for_bucket() 控制底盘移动
  • 调用 get_black_bucket_local()get_red_bucket_local() 寻找桶
  • 检测到桶并在视野中央稳定 10 帧后进入 PUT_BALL 状态

找桶策略

  1. 优先检测黑色桶
  2. 如果找不到黑色桶,检测红色桶

4. PUT_BALL(放球模式)

进入条件

  • FIND_BUCKET 状态下检测到桶并稳定 10 帧

行为

  • 使用 p_control_loop() 执行 PUT_ACTION 动作序列
  • 逆运动学控制机械臂移动到桶上方
  • 执行夹爪动作释放球
  • 动作序列完成后返回 SEARCH 状态

动作序列

PUT_ACTION = [
    ("shoulder_lift", 50),      # 抬起机械臂
    ("gap", 0),                  # 停顿
    ("gripper", 60),             # 张开夹爪(放球)
    ("gap", 0),                  # 停顿
    ("move_to", (-0.1, 0.2)),    # 收回机械臂
    ("gripper", -60),            # 闭合夹爪
]

状态转换逻辑

文件: src/main.py

SEARCH → PICK

# move_controller.py:53-56
if position > (TARGET_POSITION + 10):  # 球太大(太近)
    action = direction.get_action("backward", 1)
elif center_x < left or center_x > right:  # 球偏离中心
    action = direction.get_action("rotate_left/right")
else:
    _cycle_time += 1
    if _cycle_time > 10:  # 稳定10帧
        set_robot_status(RobotStatus.PICK)

PICK → FIND_BUCKET

# main.py:121-123
if CATCH_ACTION[command_step][0] == "move_to":
    if abs(current_x - target_x) < 0.002 and abs(current_y - target_y) < 0.002:
        command_step += 1
        if command_step == len(CATCH_ACTION):
            set_robot_status(RobotStatus.FIND_BUCKET)

FIND_BUCKET → PUT_BALL

# move_controller.py:98-102
else:
    _cycle_time += 1
    if _cycle_time > 10:
        set_robot_status(RobotStatus.PUT_BALL)

FIND_BUCKET → SEARCH(球丢失)

# main.py:80-83
gripper_pos = current_obs.get('arm_gripper.pos', 5)
is_gripper_holding = gripper_pos > 25
if not is_gripper_holding:
    set_robot_status(RobotStatus.SEARCH)
    reset_robot()
# main.py:136-146
if command_step == len(PUT_ACTION):
    set_robot_status(RobotStatus.SEARCH)
    reset_robot()
    command_step = 0

稳定性计数器机制

文件: src/move_controller.py

_cycle_time = 0  # 全局计数器

def move_controller(...):
    if result:  # 检测到目标
        if 目标在视野中央:
            _cycle_time += 1
            if _cycle_time > 10:  # 稳定10帧
                触发状态转换
        else:
            _cycle_time = 0  # 目标偏离,重置计数器
    else:  # 未检测到目标
        _cycle_time = 0

作用

  • 防止误触发(目标一闪而过)
  • 确保机器人已稳定对准目标
  • 10 帧约等于 0.5 秒(20 FPS)

控制模式

文件: src/setup.py

class RobotControlModel(Enum):
    ACT = "act"       # ACT 学习策略控制
    INVERSE = "inverse"  # 逆运动学控制
模式说明使用场景
inverse逆运动学控制机械臂默认模式,精确位置控制
actACT 策略控制模仿学习模式

目标检测区域参数

文件: src/main.py:46-53

height, width = 480, 640
_target_w = (min(height, width) // 3) - 10  # 约 150
_target_h = min(height, width) // 3          # 约 160
_left = max(0, (width - _target_w) // 2)     # 约 245
_top = max(0, (height - _target_h) // 2)    # 约 160
_right = min(width, _left + _target_w)       # 约 395
_bottom = min(height, _top + _target_h)      # 约 320

这些参数定义了摄像头画面中央的目标区域,用于判断球/桶是否在视野中央。

控制系统 (Control Systems)

移动控制器

机械臂控制器

ACT策略控制器

逆运动学控制器

控制模式选择

视觉系统 (Vision System)

YOLO推理实现

硬件加速支持 (ONNX/Atlas 310B)

目标检测和跟踪

硬件抽象层 (Hardware Abstraction)

LeKiwi机器人接口

方向控制

键盘遥操作

主控制循环 (Main Control Loop)

控制循环实现

动作-观察循环

时序控制机制

开发指南 (Development Guide)

代码结构和规范

添加新功能

调试和测试

模型优化

参看1doc/03_Rockchip_RKNPU_API_Reference_RKNN_Toolkit2_V2.3.2_CN.pdf,这是官方的API使用文档。

把onnx模型转为rknn模型的极简脚本:

# file name: onnx2rknn.py
from rknn.api import RKNN

rknn = RKNN()
rknn.config(target_platform='rk3588')
rknn.load_onnx(model='tennis.onnx')
rknn.build(do_quantization=False)
rknn.export_rknn(export_path='tennis.rknn')
rknn.release()

连接主机

目前有adb, ssh, 调试串口三种连接方式。日常调试推荐用ssh连接。

用户名:orangepi 密码:orangepi

# 调试串口连接
minicom -D /dev/ttyUSB0 -b 1500000

# usb线adb连接
adb devices
adb shell

# ssh连接
sudo ip a add 192.168.1.2/24 dev <ethN>	# 设置主机的ip地址
ssh orangepi@192.168.1.20	# 开发板的两个网口分别设置为10和20,哪个能连上就用哪个

手动运行

系统启动后本程序会自动运行,可以手动运行观察输出。如下命令都是在开发板的系统上执行。

sudo systemctl stop my-car	# 停止本程序的运行,注意下次重启后本程序依然会自动运行
conda activate rknn			# 切换到本程序的运行环境
cd ~/Code/Desktop-Wanderer	# 切换到本程序所在的目录
python -m src.main			# 手动运行本程序

附录

1

https://github.com/airockchip/rknn-toolkit2.git

故障排除 (Troubleshooting)

常见问题

调试技巧

性能优化

LeKiwi 概述与代码架构

硬件组成

组件数量说明
机械臂16自由度,含夹爪
移动底盘1三全向轮结构
摄像头1前置 OpenCV 相机

模块关系

main.py (入口)
    │
    ├── setup.py          # 全局配置管理
    │       └── config.yaml
    │
    ├── robot_setup.py    # 机器人初始化
    │       │
    │       └── lekiwi/lekiwi.py  # LeKiwi 机器人类
    │               ├── connect()      # 连接并校准
    │               ├── calibrate()    # 校准流程
    │               ├── configure()    # 配置电机参数
    │               ├── get_observation()  # 获取传感器数据
    │               └── send_action()     # 发送控制指令
    │
    ├── arm_inverse_controller.py  # 逆运动学控制
    │
    ├── arm_act_controller.py      # ACT 模式控制
    │
    └── move_controller.py        # 底盘移动控制

核心类说明

类/模块文件职责
LeKiwilekiwi/lekiwi.py机器人主类,管理连接、校准、控制
LeKiwiConfiglekiwi/lekiwi_config.py机器人配置(端口、摄像头等)
DirectionControllekiwi/direction_control.py底盘速度方向控制
KeyboardTeleoplekiwi/key_board_teleop.py键盘遥控输入
Robotlekiwi/robot.py抽象基类,定义机器人接口

电机列表

机械臂电机(位置模式)

电机名称ID控制模式标准范围
arm_shoulder_pan1位置-100~100
arm_shoulder_lift2位置-100~100
arm_elbow_flex3位置-100~100
arm_wrist_flex4位置-100~100
arm_wrist_roll5位置-100~100
arm_gripper6位置0~100

底盘电机(速度模式)

电机名称ID控制模式标准范围
base_left_wheel7速度-100~100
base_back_wheel8速度-100~100
base_right_wheel9速度-100~100

键盘控制映射

文件: lekiwi/key_board_teleop.py

按键动作速度调节
W前进
S后退
A左移
D右移
Q左转
E右转
]加速切换到下一档
[减速切换到上一档
Esc退出

底盘速度档位

文件: lekiwi/direction_control.py

档位xy速度 (m/s)角速度 (deg/s)说明
00.0215特慢
10.0530
20.2550中(默认)
30.4080

LeKiwi 快速开始

环境要求

安装 Miniforge

wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"
bash Miniforge3-$(uname)-$(uname -m).sh

创建 Python 3.10 环境

conda create -y -n lerobot python=3.10
conda activate lerobot

安装依赖

conda install ffmpeg -c conda-forge
git clone https://github.com/huggingface/lerobot.git
cd lerobot
pip install -e .
pip install -e ".[feetech]"

硬件连接

  1. 使用 USB 线将控制板连接到 PC
  2. 连接 12V 8A 电源适配器
  3. 检查串口端口
lerobot-find-port

启动命令

conda activate lerobot
cd /path/to/Desktop-Wanderer
python -m src.main

初始化流程

代码层面初始化顺序

# 1. 加载配置 (main.py:48)
init_app()  # 从 config.yaml 读取配置到全局变量

# 2. 初始化机器人对象 (robot_setup.py:29)
init_robot()
#   └─ cfg = LeKiwiConfig(port=get_port())
#   └─ _robot = LeKiwi(cfg)

# 3. 连接硬件 (main.py:52)
robot.connect()
#   ├─ bus.connect()                    # 连接串口总线
#   ├─ calibrate()                      # 如未校准则校准
#   ├─ cam.connect()                    # 连接摄像头
#   └─ configure()                      # 配置电机参数

# 4. 获取初始状态 (main.py:54)
start_obs = robot.get_observation()

首次校准

校准触发条件

满足以下任一条件时进入校准:

  1. 首次使用设备(无校准文件)
  2. 校准文件与电机实际值不匹配
  3. 启动时输入 c 手动选择重新校准

校准步骤

第一步:居中机械臂

代码位置: lekiwi.py:152

Move robot to the middle of its range of motion and press ENTER....

将所有机械臂关节手动转动到行程中点位置,按回车确认。

第二步:记录关节行程

代码位置: lekiwi.py:162-169

Move all arm joints except '[full_turn_motor]' sequentially through their entire ranges of motion.

依次缓慢转动每个关节,确保经过上下限位,按回车停止。

注意:轮子电机和腕部滚动无需手动转动,系统自动跳过。

第三步:保存校准

校准文件保存至:

~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json

后续启动

已有校准文件时:

Press ENTER to use provided calibration file associated with the id None, or type 'c' and press ENTER to run calibration:
操作效果
按 Enter使用现有校准文件继续
输入 c + Enter重新校准
等待 3 秒自动使用现有校准文件

强制重新校准

rm ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json
python -m src.main

机械臂预设位置

文件: robot_setup.py

_start_positions = {
    'arm_shoulder_pan': 0.0,
    'arm_shoulder_lift': -31.70,
    'arm_elbow_flex': 27.69,
    'arm_wrist_flex': 80.00,
    'arm_wrist_roll': 0.0,
    'arm_gripper': 10.0
}

LeKiwi 工作流程

完整工作循环

┌─────────────────────────────────────────────────────────────────┐
│                         系统启动                                 │
│                    python -m src.main                           │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│  1. 初始化                                                      │
│     - init_app()        加载 config.yaml                        │
│     - init_robot()      创建 LeKiwi 实例                        │
│     - robot.connect()   连接硬件,执行校准(如需要)              │
│     - get_observation() 获取初始关节角度                         │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│  2. 机械臂归位                                                   │
│     - return_to_start_position()                                │
│     - 移动到预设位置 (robot_setup.py:5-12)                       │
└─────────────────────────────────────────────────────────────────┘
                                │
                                ▼
                    ┌───────────────────┐
                    │   主循环 while    │
                    │       True        │
                    └───────────────────┘
                                │
                ┌───────────────┼───────────────┐
                ▼               ▼               ▼
        ┌───────────┐   ┌───────────┐   ┌───────────┐
        │  SEARCH   │   │   PICK    │   │FIND_BUCKET│
        └───────────┘   └───────────┘   └───────────┘
                │               │               │
                │               │               │
                ▼               ▼               ▼
        ┌───────────────────────────────────────────────┐
        │            目标检测 (YOLO)                     │
        │  - yolo_infer()        找球                    │
        │  - get_black_bucket()   找黑桶                   │
        │  - get_red_bucket()    找红桶(备选)            │
        └───────────────────────────────────────────────┘
                                │
                                ▼
        ┌───────────────────────────────────────────────┐
        │            底盘移动控制                         │
        │  - move_controller()        搜索目标            │
        │  - move_controller_for_bucket() 跟随桶          │
        └───────────────────────────────────────────────┘
                                │
                                ▼
        ┌───────────────────────────────────────────────┐
        │            发送控制指令                         │
        │  - robot.send_action({**arm_action, **move})  │
        └───────────────────────────────────────────────┘

状态详解

SEARCH(搜索状态)

职责:寻找球

SEARCH:
    ├─ 检测: yolo_infer(frame) → 找球
    ├─ 控制: move_controller(direction, result)
    │         ├─ 球在左侧 → 旋转向左
    │         ├─ 球在右侧 → 旋转向右
    │         ├─ 球太小   → 前进
    │         ├─ 球太大   → 后退
    │         └─ 球居中且大小合适 → 稳定计数
    └─ 稳定10帧 → 切换到 PICK

PICK(捡球状态)

职责:执行捡球动作序列

PICK:
    ├─ 控制器: p_control_loop(CATCH_ACTION[step])
    ├─ 动作序列执行:
    │   1. move_to(0.0989, 0.125)   # 移动到初始位置
    │   2. shoulder_pan(-12)         # 调整肩部角度
    │   3. gripper(60)               # 张开夹爪
    │   4. wrist_flex(80)           # 弯曲腕部
    │   5. move_to(0.140, 0.1211)   # 移动到球上方
    │   6. move_to(0.140, -0.05)    # 下降到球位置
    │   7. gap()                    # 停顿
    │   8. gripper(-60)             # 闭合夹爪(夹球)
    │   9. gap()                    # 停顿
    │   10. shoulder_pan(12)        # 肩部归位
    │   11. move_to(-0.1, 0.2)      # 举起球
    │   12. wrist_flex(-20)         # 调整腕部
    └─ 序列完成 → 切换到 FIND_BUCKET

FIND_BUCKET(寻找桶状态)

职责:导航到桶的位置

FIND_BUCKET:
    ├─ 检测: get_black_bucket_local(frame)
    │         └─ 找不到 → get_red_bucket_local(frame)
    ├─ 检查: gripper_pos > 25 ?
    │         ├─ 否(球丢失)→ reset_robot() → SEARCH
    │         └─ 是(球还在)→ 继续
    ├─ 控制: move_controller_for_bucket(direction, result)
    │         ├─ 桶在左侧 → 旋转向左
    │         ├─ 桶在右侧 → 旋转向右
    │         ├─ 桶太小   → 前进
    │         ├─ 桶太大   → 后退
    │         └─ 桶居中且大小合适 → 稳定计数
    └─ 稳定10帧 → 切换到 PUT_BALL

PUT_BALL(放球状态)

职责:执行放球动作序列

PUT_BALL:
    ├─ 控制器: p_control_loop(PUT_ACTION[step])
    ├─ 动作序列执行:
    │   1. shoulder_lift(50)      # 抬起机械臂
    │   2. gap()                  # 停顿
    │   3. gripper(60)            # 张开夹爪(放球)
    │   4. gap()                  # 停顿
    │   5. move_to(-0.1, 0.2)     # 收回机械臂
    │   6. gripper(-60)           # 闭合夹爪
    └─ 序列完成 → reset_robot() → SEARCH

捡球动作详解

坐标系统

机械臂工作空间坐标(单位:米):

        y (0.22)
         │
         │
         │
(-0.22)──┼───────────────── x
         │
         │
         │
        y (-0.15)

注意:x 范围 -0.22 ~ 0.22,y 范围 -0.15 ~ 0.22

逆运动学控制

文件: arm_inverse_controller.py

def inverse_kinematics(x, y, l1=0.1159, l2=0.1350):
    """
    计算二维机械臂逆运动学
    x, y: 末端执行器目标坐标
    l1: 上臂长度 0.1159m
    l2: 下臂长度 0.1350m
    返回: (肩部角度, 肘部角度)
    """

P控制参数

MOTION_PARAMS = {
    'max_step': 0.01,    # 最大步长 1cm/帧
    'min_step': 0.001,   # 最小步长 1mm/帧
    'slow_dist': 0.05,   # 5cm 内开始减速
    'dead_zone': 0.0005  # 0.5mm 内认为到达
}

夹爪控制

动作说明
张开+60夹爪打开到 60°
闭合-60夹爪闭合,夹住物体
检测>25夹爪角度大于 25° 认为夹住物体

视觉目标检测

检测区域

摄像头画面中央的目标区域:

┌────────────────────────────────────────┐
│  (0,0)                                 │
│    ┌───────────────────────────────┐   │
│    │                               │   │
│    │     ┌───────────────┐         │   │
│    │     │   TARGET ZONE │         │   │ ← 球/桶需要在这个区域
│    │     │               │         │   │
│    │     └───────────────┘         │   │
│    │                               │   │
│    └───────────────────────────────┘   │
│                            (640, 480)  │
└────────────────────────────────────────┘

检测参数

TARGET_POSITION = max(target_w, target_h)  # 约 160 像素
TARGET_CX = left + target_w // 2            # 约 245 + 75 = 320
TARGET_CY = top + target_h // 2            # 约 160 + 80 = 240

目标大小判断

目标在画面中的大小判断机器人动作
max(w,h) < TARGET_POSITION - 10太远前进
max(w,h) > TARGET_POSITION + 10太近后退
居中且大小合适到位等待稳定

异常处理

球丢失检测

# main.py:76-83
if get_robot_status() == RobotStatus.FIND_BUCKET:
    gripper_pos = current_obs.get('arm_gripper.pos', 5)
    is_gripper_holding = gripper_pos > 25

    if not is_gripper_holding:
        set_robot_status(RobotStatus.SEARCH)
        reset_robot()

当检测到夹爪角度小于 25° 时,认为球已丢失,返回 SEARCH 状态重新寻找。

找不到目标

# move_controller.py:58-66
if not result:  # 没有检测到目标
    if _last_ball_center_x is not None:
        # 向最后看到目标的方向旋转
        action = direction.get_action("rotate_left/right")
    else:
        action = direction.get_action(None)  # 停止

机器人会记住最后看到目标的位置,并朝那个方向旋转继续搜索。

完整流程时序图

时间轴
  │
  ▼
SEARCH ────────────────────────────────────────────────────────
  │                                                           │
  │  [检测到球]                                                │
  │                                                           │
  ▼                                                           │
PICK  │                                                         │
  │                                                           │
  ├─ move_to(0.0989, 0.125)    [机械臂移动]                      │
  ├─ shoulder_pan(-12)          [调整角度]                        │
  ├─ gripper(60)                [张开夹爪]                        │
  ├─ move_to(0.140, 0.1211)     [下降到球上方]                     │
  ├─ move_to(0.140, -0.05)     [接触球]                          │
  ├─ gripper(-60)               [夹住球]                          │
  ├─ shoulder_pan(12)          [抬起]                            │
  ├─ move_to(-0.1, 0.2)        [举起球]                          │
  │                                                           │
  ▼                                                           │
FIND_BUCKET  │                                                     │
  │                                                           │
  │  [找桶导航]                                                  │
  │                                                           │
  ▼                                                           │
PUT_BALL  │                                                       │
  │                                                           │
  ├─ shoulder_lift(50)           [抬起臂]                        │
  ├─ gripper(60)                 [张开放球]                        │
  ├─ move_to(-0.1, 0.2)          [收回]                          │
  ├─ gripper(-60)                [闭合夹爪]                       │
  │                                                           │
  ▼                                                           │
SEARCH ────────────────────────────────────────────────────────

LeKiwi 配置参数详解

config.yaml

项目根目录的配置文件:

port: /dev/ttyACM0      # 串口设备路径
fps: 20                 # 控制频率 (Hz)
log_level: INFO         # 日志级别:DEBUG, INFO, WARNING, ERROR
hardware_mode: normal    # 硬件模式:normal (PC), rk3588 (控制板)
control_mode: inverse    # 控制模式:inverse (逆运动学), act (ACT策略)

配置项说明

配置项可选值说明
port/dev/ttyACM0, /dev/ttyUSB0串口设备路径
fps正整数控制频率,建议 20Hz
log_levelDEBUG, INFO, WARNING, ERROR日志详细程度
hardware_modenormal, rk3588normal=PC调试,rk3588=控制板运行
control_modeinverse, actinverse=逆运动学,act=ACT策略

LeKiwiConfig 配置类

文件: lekiwi/lekiwi_config.py

@dataclass
class LeKiwiConfig(RobotConfig):
    port: str = "/dev/ttyACM0"           # 串口端口
    disable_torque_on_disconnect: bool = True  # 断开时禁用扭矩
    max_relative_target: float | dict = None    # 相对目标限制
    cameras: dict = lekiwi_cameras_config()    # 摄像头配置
    use_degrees: bool = False            # 是否使用角度制

摄像头配置

文件: lekiwi/lekiwi_config.py

def lekiwi_cameras_config() -> dict[str, CameraConfig]:
    return {
        "front": OpenCVCameraConfig(
            index_or_path=0,    # 摄像头索引(0, 1, 2...)
            fps=30,             # 帧率
            width=640,          # 图像宽度
            height=480,         # 图像高度
            color_mode=ColorMode.BGR
        ),
    }

电机参数配置

文件: lekiwi/lekiwi.py

机械臂电机(位置模式)

for name in self.arm_motors:
    self.bus.write("Operating_Mode", name, OperatingMode.POSITION.value)
    self.bus.write("P_Coefficient", name, 16)   # P控制系数
    self.bus.write("I_Coefficient", name, 0)    # I控制系数
    self.bus.write("D_Coefficient", name, 32)  # D控制系数
参数默认值说明
P_Coefficient16比例系数,调低可减少抖动
I_Coefficient0积分系数
D_Coefficient32微分系数

底盘电机(速度模式)

for name in self.base_motors:
    self.bus.write("Operating_Mode", name, OperatingMode.VELOCITY.value)

关节校准参数

文件: arm_inverse_controller.py

JOINT_CALIBRATION = [
    ['arm_shoulder_pan', 6.0, 1.0],      # [电机名, 零位偏移, 缩放因子]
    ['arm_shoulder_lift', 2.0, 0.97],
    ['arm_elbow_flex', 0.0, 1.05],
    ['arm_wrist_flex', 0.0, 0.94],
    ['arm_wrist_roll', 0.0, 0.5],
    ['arm_gripper', 0.0, 1.0],
]
参数说明
零位偏移校正电机零点误差
缩放因子校正角度缩放比例

运动控制参数

文件: arm_inverse_controller.py

MOTION_PARAMS = {
    'max_step': 0.01,    # 最大步长 (m/帧)
    'min_step': 0.001,   # 最小步长 (m/帧)
    'slow_dist': 0.05,   # 减速距离 (m)
    'dead_zone': 0.0005  # 死区 (m)
}
参数默认值说明
max_step0.01远距离时每帧移动 2cm
min_step0.001精细调整时每帧移动 1mm
slow_dist0.05距离目标 5cm 以内开始减速
dead_zone0.0005误差小于 0.5mm 认为到达

逆运动学参数

文件: arm_inverse_controller.py

def inverse_kinematics(x, y, l1=0.1159, l2=0.1350):
    # l1: 上臂长度 (m),肩部到肘部
    # l2: 下臂长度 (m),肘部到腕部
参数说明
l10.1159 m上臂长度
l20.1350 m下臂长度

校准文件格式

路径: ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json

{
  "arm_shoulder_pan": {
    "id": 1,
    "drive_mode": 0,
    "homing_offset": 1234,
    "range_min": 0,
    "range_max": 4095
  },
  "arm_shoulder_lift": {
    "id": 2,
    "drive_mode": 0,
    "homing_offset": -567,
    "range_min": 0,
    "range_max": 4095
  }
}
字段说明
id电机 ID(在总线上的标识)
drive_mode驱动模式(0=电机模式)
homing_offset零位偏移量
range_min关节最小位置值
range_max关节最大位置值

LeKiwi 调试方法

日志调试

修改 config.yaml 中的 log_level

log_level: DEBUG  # 获取最详细的调试信息

日志级别说明

级别输出内容
DEBUG电机状态、观测数据、控制输出等详细信息
INFO连接成功、校准完成等关键事件
WARNING潜在问题但不影响运行
ERROR连接失败、控制异常等错误

摄像头调试

文件: main.py:91-105

hardware_mode: normal 模式下,会显示实时摄像头画面:

if get_hardware_mode() == 'normal':
    cv2.imshow("frame", frame)  # 显示画面
    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        break  # 按 q 退出

摄像头相关调试操作

  1. 显示画面: 设置 hardware_mode: normal
  2. 隐藏画面: 设置 hardware_mode: rk3588
  3. 按 q 退出: 在摄像头窗口按下 q 键

串口连接调试

查找端口

# 方法1:使用 lerobot 工具
lerobot-find-port

# 方法2:手动查看
ls -l /dev/ttyACM*  # Linux
ls -l /dev/ttyUSB*  # Linux
ls /dev/cu.*        # macOS

串口权限问题

# 添加读写权限
sudo chmod 666 /dev/ttyACM0

# 或将用户添加到 dialout 组
sudo usermod -a -G dialout $USER

常见串口问题

  • 权限不足: sudo chmod 666 /dev/ttyACM0
  • 端口被占用: 检查是否有其他程序占用串口
  • 端口名称错误: 确认 config.yamlport 与实际一致

电机状态调试

获取当前电机状态

# 获取当前电机观测数据
obs = robot.get_observation()
print(obs)

输出示例

{
  'arm_shoulder_pan.pos': 0.5,
  'arm_shoulder_lift.pos': -31.7,
  'arm_elbow_flex.pos': 27.69,
  'arm_wrist_flex.pos': 80.0,
  'arm_wrist_roll.pos': 0.0,
  'arm_gripper.pos': 10.0,
  'x.vel': 0.0,
  'y.vel': 0.0,
  'theta.vel': 0.0,
  'front': <numpy.ndarray shape=(480, 640, 3)>
}

手动发送控制指令

发送机械臂位置控制

robot.send_action({
    'arm_shoulder_pan.pos': 10.0,
    'arm_shoulder_lift.pos': -20.0,
    'arm_elbow_flex.pos': 15.0,
    'arm_wrist_flex.pos': 80.0,
    'arm_wrist_roll.pos': 0.0,
    'arm_gripper.pos': 50.0,
    'x.vel': 0.0,
    'y.vel': 0.0,
    'theta.vel': 0.0,
})

发送底盘速度控制

robot.send_action({
    'x.vel': 0.1,      # 前进 (m/s)
    'y.vel': 0.0,      # 侧移 (m/s)
    'theta.vel': 30.0, # 旋转 (deg/s)
})

P 控制调试

文件: arm_inverse_controller.py

def p_control_loop(cmd, current_x, current_y, current_obs, kp=0.5):
    # kp: 比例增益
    # 增大 kp:响应更快但可能振荡
    # 减小 kp:响应慢但更平稳

调参建议

问题调整方法
响应太慢增大 kp(建议 0.5~1.0)
出现振荡减小 kp(建议 0.2~0.4)
运动不平滑减小 max_step 参数

校准数据调试

查看校准文件

cat ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json

验证校准是否成功

  1. 启动后观察机械臂是否平滑移动到中点位置
  2. 检查日志中是否显示 "Calibration saved"
  3. 验证校准文件存在且内容完整

独立调试脚本

创建 debug_robot.py

#!/usr/bin/env python
import sys
sys.path.insert(0, 'src')

from lekiwi import LeKiwi, LeKiwiConfig

# 创建机器人实例
config = LeKiwiConfig(port="/dev/ttyACM0")
robot = LeKiwi(config)

# 连接
print("Connecting...")
robot.connect()

# 打印观测数据
print("\n=== Robot Observation ===")
obs = robot.get_observation()
for key, value in obs.items():
    if key != 'front':
        print(f"  {key}: {value}")

# 测试控制
print("\n=== Testing Control ===")
action = {
    'arm_shoulder_lift.pos': -10.0,
    'arm_elbow_flex.pos': 10.0,
    'x.vel': 0.0,
    'y.vel': 0.0,
    'theta.vel': 0.0,
}
robot.send_action(action)

# 断开
robot.disconnect()
print("Done")

运行调试脚本

python debug_robot.py

打印初始化信息

文件: main.py:54-64

启动时会打印初始关节角度:

Reading initial joint angles...
Initial joint angles:
  arm_shoulder_pan: 0°
  arm_shoulder_lift: -31.7°
  arm_elbow_flex: 27.69°
  arm_wrist_flex: 80°
  arm_wrist_roll: 0°
  arm_gripper: 10°

快速调试检查清单

  • 串口连接正常 (ls /dev/ttyACM*)
  • 摄像头可访问 (cv2.VideoCapture(0).isOpened())
  • 校准文件存在
  • 日志级别设置为 DEBUG
  • 电源电压稳定 (12V 8A)

LeKiwi 故障排除

Q1: 提示 "Device not connected" 或找不到设备

可能原因

  1. USB 线连接不良
  2. 串口端口号错误
  3. 串口权限不足
  4. 串口被其他程序占用

解决方案

# 1. 检查 USB 连接
ls -l /dev/ttyACM*

# 2. 重新检测端口
lerobot-find-port

# 3. 更新 config.yaml 中的 port
port: /dev/ttyACM0  # 确认端口号正确

# 4. 添加串口权限
sudo chmod 666 /dev/ttyACM0

Q2: 机械臂运动不顺畅或抖动

可能原因

  1. P 控制系数过高
  2. 电源电压不稳定
  3. 电机线缆接触不良

解决方案

检查电机线缆:确保所有电机连接牢固

Q3: 摄像头无法打开

可能原因

  1. 摄像头索引不正确
  2. 摄像头被其他程序占用
  3. 摄像头驱动问题

解决方案

# Linux: 查看可用摄像头
v4l2-ctl --list-devices

# macOS: 查看摄像头
system_profiler SPCameraDataType

修改 lekiwi_config.py 中的摄像头索引:

index_or_path=0,  # 尝试 0, 1, 2...

Q4: 底盘轮子不转动但机械臂正常

可能原因

  1. 底盘电机线缆问题
  2. 控制模式配置错误
  3. 轮子被卡住

解决方案

  1. 检查电机控制模式lekiwi.py:199-200):

    self.bus.write("Operating_Mode", name, OperatingMode.VELOCITY.value)
    
  2. 查看日志错误(设置 log_level: DEBUG

  3. 手动测试底盘

    robot.send_action({
        'x.vel': 0.1,
        'y.vel': 0.0,
        'theta.vel': 0.0,
    })
    

Q5: 如何确认校准是否成功?

检查方法

  1. 启动后观察机械臂是否平滑移动到中点位置

  2. 检查日志中是否显示:

    Calibration saved to ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json
    
  3. 验证校准文件存在且内容完整:

    cat ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json | head -20
    

Q6: 关节角度与预期不符

可能原因

  1. 零位偏移不正确
  2. 缩放因子不准确

解决方案

调整 arm_inverse_controller.py 中的校准参数:

JOINT_CALIBRATION = [
    ['arm_shoulder_pan', 6.0, 1.0],  # 调整偏移和缩放
    ['arm_shoulder_lift', 2.0, 0.97],
    ...
]

重新校准

rm ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json
python -m src.main

Q7: 运动时末端执行器轨迹不平滑

可能原因

  1. 步长设置过大
  2. P 控制增益过高

解决方案

  1. 调整运动控制参数arm_inverse_controller.py):

    MOTION_PARAMS = {
        'max_step': 0.005,   # 减小可提高平滑度
        'slow_dist': 0.08,   # 增大减速距离
        ...
    }
    
  2. 调整 P 控制增益

    p_control_loop(..., kp=0.3)  # 降低 kp 提高稳定性
    

Q8: 启动时卡在校准界面

可能原因

  1. 机械臂不在中点位置
  2. 关节行程记录不完整

解决方案

  1. 手动将机械臂所有关节置于中点位置
  2. 按回车继续
  3. 缓慢转动每个关节经过完整行程

Q9: 校准文件丢失或损坏

解决方案

删除损坏的校准文件并重新启动:

rm ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json
python -m src.main

快速恢复

如需完全恢复出厂设置:

# 删除所有校准缓存
rm -rf ~/.cache/huggingface/lerobot/

# 重置配置
# 编辑 config.yaml 确认参数正确

# 重新启动
python -m src.main

诊断命令汇总

命令用途
ls -l /dev/ttyACM*检查串口设备
lerobot-find-port自动查找端口
cat ~/.cache/huggingface/lerobot/calibration/robots/lekiwi/None.json查看校准文件
python -m src.main启动程序
sudo chmod 666 /dev/ttyACM0修复串口权限