转载自公众号:敢敢AUTOHUB
1. 引言
OriginMan 是一台桌面级人形机器人。它身上集合了舵机控制、相机图像采集、ROS2 节点通信、动作组编排、语音识别和大模型文本交互——这些单独拉出来都能写一篇教程,但真正有意思的是把它们串起来之后的样子。
官方资料提供了各功能模块的运行命令和示例代码,但没有给出端到端的系统整合方案。所以这篇文章做的事情很简单:先把 OriginMan 自带的巡线、颜色识别、AprilTag、语音交互和动作模仿逐个拆开看一遍,搞清楚每个模块的输入输出和关键设计;然后用 RosClaw 把这些模块接上一个自然语言代理,让 Thor 能够通过网络发现 OriginMan 的能力、读取状态、下发控制命令。
2. 平台概况
OriginMan 主控为 RDK X5(八核 Cortex-A55, 10TOPS BPU, 8GB 内存 + 32GB 闪存),扩展控制用 MM32F103CBT6 配合 MPU6050 六轴 IMU。执行层是 20 自由度舵机——头部 2 个、双臂、双腿加踝关节。传感器方面:5MP 广角摄像头、麦克风阵列、扬声器、128×64 OLED 和 RGB 灯环。软件环境是 Ubuntu 22.04 + ROS2 Humble + TogetheROS.Bot。
| 模块 | 规格 | 作用 |
|---|---|---|
| 主控 | RDK X5, 八核 A55, 10TOPS, 8GB | 视觉推理、ROS2 调度、语音与大模型 |
| 扩展控制 | MM32F103CBT6 + MPU6050 | 低层状态采集 |
| 执行 | 20DOF 舵机 | 头部转动、步态、动作组 |
| 感知 | 5MP 摄像头、麦克风阵列、扬声器、OLED | 视觉、语音、显示 |
| 软件 | Ubuntu 22.04 + ROS2 Humble + TROS | 节点通信、二次开发框架 |
RDK X5 与智能计算能力
图2 OriginMan 的主控平台与智能计算核心。图片来源:OriginMan 官方网站。
3. 系统架构
OriginMan 的控制系统可以抽象为四层:感知层(摄像头、IMU、麦克风)、通信层(ROS2 话题/服务/参数)、决策层(视觉识别→动作意图)、执行层(动作组、舵机控制)。
从公开示例代码看几个工程细节:各视觉节点用 create_timer(0.1, ...) 驱动,调度周期约 100ms;发布器默认队列深度 10,够用但接入高频传感器时需调 QoS;动作执行和感知不在同一个回调里,避免了图像处理阻塞舵机控制。
OriginMan 控制系统结构示意图
图3 OriginMan 智能控制系统总体结构图的 Gemini 出图描述。
4. 视觉巡线
官方 line_follower 的流程:缩放→高斯滤波→转 LAB→阈值分割→腐蚀膨胀→多 ROI 轮廓检测→加权计算中心。三个 ROI 从远到近权重递增(0.1/0.3/0.6),底部权重最高,因为它决定下一步会不会偏。控制策略也很直接:偏差 ≤50px 走 go_forward,偏左走 turn_right_small_step,偏右走 turn_left_small_step。不输出连续速度,直接映射到动作组。
self.roi = [
(240, 280, 0, 640, 0.1),
(340, 380, 0, 640, 0.3),
(440, 480, 0, 640, 0.6),
]
if abs(self.line_center_x - self.img_centerx) <= 50:
AGC.runActionGroup('go_forward')
elif self.line_center_x - self.img_centerx > 50:
AGC.runActionGroup('turn_right_small_step')
else:
AGC.runActionGroup('turn_left_small_step')
这种离散映射的好处是感知和执行粒度对齐——感知输出三类(左/右/直),执行也只有三类动作组,不会出现"感知给连续曲率、执行只能选离散动作"的接口失配。
5. 颜色识别
颜色识别在 LAB 空间内分别检测红、绿、蓝,取面积最大的颜色作为结果。识别到红色→点头,绿色或蓝色→摇头。
self.color_list.append(color)
if len(self.color_list) == 3:
color = int(round(np.mean(np.array(self.color_list))))
self.color_list = []
上面这段 3 帧取平均的逻辑值得注意。视觉检测最常见的坑不是完全检测不到,而是偶发误检——如果每帧都触发动作,机器人会抖个不停。攒 3 帧求平均再决定,牺牲一点响应速度换稳定性,简单但实用。
颜色跟踪模式下,检测到红色目标后,头部舵机会跟随目标坐标转动——图像偏差直接转化为舵机角度调整,把"检测"和"执行"连成了闭环。
6. AprilTag
官方 apriltag_detect 示例:摄像头采图→灰度化→ apriltag.Detector 检测→绘制角点→发布 tag_id 和 tag_family 到 ROS2 话题。
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
detections = self.detector.detect(gray, return_image=False)
for detection in detections:
tag_id = int(detection.tag_id)
corners = np.int0(detection.corners)
当前示例只用了 tag_id 和角点坐标做展示,没有引入相机内参和 PnP 位姿解算。但 AprilTag 本身支持相对位姿计算——一旦结合相机标定参数,就可以从"看见标签"升级到"知道标签在空间中的位置",这在导航、定点停靠等场景中很有用。
AprilTag 识别示意
图4 OriginMan 的 AprilTag 识别效果示意。图片来源:OriginMan 官方文档。
7. 动作模仿
模仿动作通过手势触发:OK 启动→Palm 进入模仿(手臂跟随人运动)→Victory 夹爪开合→ThumbLeft/Right 左右移动→ThumbUp 退出并鞠躬。
| 手势 | 响应 |
|---|---|
| OK | 点头,启动控制 |
| Palm | 双臂跟随人的手臂 |
| Victory | 夹爪重复开合 |
| ThumbLeft | 左移 |
| ThumbRight | 右移 |
| ThumbUp | 鞠躬,退出 |
这套机制把视觉理解、手势分类、状态切换和动作执行串成了一条通路。注意:如果进入模仿动作页面后没有摄像头画面,执行 sudo systemctl stop originman.service 释放 /dev/video0 再试。
动作模仿功能示意
图5 OriginMan 模仿动作功能界面。图片来源:OriginMan 官方文档。
8. RosClaw联动
ROSClaw 是一个旨在连接人工智能与物理机器人的开源桥接与控制框架,主要用于让AI智能体能够理解并操控现实世界中的物理机器人。这里我们尝试通过接入RosClaw,让Jetson Thor中的本地LLM可以拥有控制OriginMan的能力。
如果各位想要体验OpenClaw驱动OriginMan的对应功能的话,我建议直接将本文给到OpenClaw分析后直接开始部署即可。
8.1 部署目标
OriginMan 端跑 ROS2、功能节点、rosbridge 和 discovery;Thor 端跑 OpenClaw Gateway + RosClaw 插件。最小版本四步:装 rosbridge→装 discovery→配 Thor 端插件→只读验证。
8.2 环境准备
前提:OriginMan 能独立跑官方功能包,Thor 已装 OpenClaw,两台机器互通。
# OriginMan 端
hostname -I && ping <thor_ip>
# Thor 端
node -v && pnpm -v && ping <originman_ip>
建议先把这些官方示例跑通一次,否则后面联调只会更难排错:
ros2 run originman_line_follower line_follower
ros2 run originman_color_detect color_detect
ros2 run originman_color_detect color_track
ros2 launch originman_action_imitation action_imitation.launch.py
8.3 下载仓库
Thor 和 OriginMan 两边都克隆一份 RosClaw:
mkdir -p ~/robot_ws && cd ~/robot_ws
git clone https://github.com/PlaiPin/rosclaw.git
cd rosclaw && git rev-parse HEAD # 记下当前 commit
8.4 OriginMan 端安装
# 安装 rosbridge
sudo apt update && sudo apt install -y ros-humble-rosbridge-suite
# 安装构建工具
sudo apt install -y python3-colcon-common-extensions python3-rosdep
sudo rosdep init && rosdep update
# 创建工作区并拷贝包
mkdir -p ~/rosclaw_ws/src
cd ~/rosclaw_ws/src
cp -R ~/robot_ws/rosclaw/ros2_ws/src/rosclaw_msgs .
cp -R ~/robot_ws/rosclaw/ros2_ws/src/rosclaw_discovery .
cd ~/rosclaw_ws
source /opt/tros/humble/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install
source install/setup.bash
# 启动 discovery
ros2 run rosclaw_discovery discovery_node --ros-args
-p robot_name:=OriginMan
-p robot_namespace:=/originman
-p publish_interval:=5.0
# 另开终端启动 rosbridge
ros2 launch rosbridge_server rosbridge_websocket_launch.xml
port:=9090 address:=0.0.0.0
# 确认端口
ss -lntp | rg 9090
8.5 OriginMan 端验证
source ~/rosclaw_ws/install/setup.bash
ros2 topic echo /rosclaw/capabilities --once
ros2 service call /rosclaw/get_capabilities rosclaw_msgs/srv/GetCapabilities
"{robot_namespace: ''}"
看到 robot_name、topic_names 等字段且返回 success: true 即可。
8.6 Thor 端准备
cd ~/robot_ws/rosclaw
pnpm install
pnpm typecheck
pnpm build
确认 Gateway 运行:
openclaw gateway run --port 18789 --verbose
# 或守护进程模式
systemctl --user status openclaw-gateway
8.7 插件接入
在 ~/.openclaw/openclaw.json 中添加 RosClaw 插件配置:
{
"plugins": {
"enabled": true,
"load": {
"paths": ["/home/thor/robot_ws/rosclaw/extensions/openclaw-plugin"]
},
"entries": {
"rosclaw": {
"enabled": true,
"config": {
"transport": { "mode": "rosbridge" },
"rosbridge": {
"url": "ws://192.168.127.10:9090",
"reconnect": true,
"reconnectInterval": 3000
},
"robot": {
"name": "OriginMan",
"namespace": "/originman"
}
}
}
}
}
}
也可以用命令行逐条设置:
openclaw config set plugins.enabled true --strict-json
openclaw config set plugins.load.paths '["/home/thor/robot_ws/rosclaw/extensions/openclaw-plugin"]' --strict-json
openclaw config set plugins.entries.rosclaw.enabled true --strict-json
openclaw config set plugins.entries.rosclaw.config.transport.mode '"rosbridge"' --strict-json
openclaw config set plugins.entries.rosclaw.config.rosbridge.url '"ws://192.168.127.10:9090"' --strict-json
openclaw config set plugins.entries.rosclaw.config.rosbridge.reconnect true --strict-json
openclaw config set plugins.entries.rosclaw.config.rosbridge.reconnectInterval 3000 --strict-json
openclaw config set plugins.entries.rosclaw.config.robot.name '"OriginMan"' --strict-json
openclaw config set plugins.entries.rosclaw.config.robot.namespace '"/originman"' --strict-json
openclaw config validate
改完重启 Gateway:
systemctl --user restart openclaw-gateway
# 或前台调试
openclaw gateway run --port 18789 --verbose
8.8 启动顺序
1. OriginMan 端启动功能节点或适配层
2. OriginMan 端启动 rosclaw_discovery
3. OriginMan 端启动 rosbridge_server
4. Thor 端启动 OpenClaw Gateway
5. 确认连接: openclaw gateway probe + 观察日志中 WebSocket 连接状态
8.9 模式控制
如果想让 RosClaw 真正控制 OriginMan 的巡线、颜色识别、AprilTag 和动作组,不建议直接把零散 demo 节点暴露给代理。更稳妥的方式是在 OriginMan 端先加一个适配层。本节重点介绍实际开发的 originman_rosclaw_adapter 包及其验证结果。
8.9.1 适配器设计:话题驱动的统一控制层
实测中我们选择了一种**话题驱动(topic-based)**的简洁架构,而非更复杂的服务/动作接口。理由有三:一是话题天然支持一对多观察(调试时 OriginMan 端和 Thor 端可以同时监听),二是 std_msgs/msg/String 消息足够表达所有控制语义,三是可以快速与 RosClaw 的现有工具集成,无需额外注册自定义服务类型。
适配器包结构如下,adapter_node.py的代码我放在文章的最后了,其他的内容可以让openclaw帮你自动补齐:
rosclaw_ws/src/originman_rosclaw_adapter/
├── package.xml # ament_python 包声明
├── setup.py # entry_point → adapter_node
├── setup.cfg
└── originman_rosclaw_adapter/
├── __init__.py
└── adapter_node.py # 主适配器节点(约 250 行)
控制接口(适配器订阅):
| 话题 | 类型 | 功能 |
|---|---|---|
/originman/cmd/mode |
String | 模式切换:color_detect / apriltag / line_follow / color_track / audio_control / stop |
/originman/cmd/tts |
String | 文字转语音(走 I2C smbus 路径,当前不可用) |
/originman/cmd/action |
String | 动作组指令(安全模式下仅记录不执行) |
状态接口(适配器发布):
| 话题 | 类型 | 功能 |
|---|---|---|
/originman/state |
String(JSON) | 每 2 秒推送系统状态 |
/originman/camera/image_raw |
Image | 桥接 /image_raw |
/originman/detection_info |
String | 桥接检测结果(/color_info、/apriltag_info、/line_info) |
关于"话题从哪来":一个容易混淆的点是——RosClaw 本身(rosbridge + rosclaw_discovery)只提供传输通道和能力发现,并不创建
/originman/state、/originman/cmd/mode这些话题。实际的角色分工如下:
组件 提供 说明 rosbridge_serverWebSocket ↔ ROS2 桥接 纯传输层,不创建业务话题 rosclaw_discovery/rosclaw/capabilities话题 +/rosclaw/get_capabilities服务周期扫描 ROS2 图上的话题/服务/动作并打包发布,供 Thor 端自动发现 originman_rosclaw_adapter/originman/state、/originman/camera/image_raw、/originman/detection_info等全部/originman/*话题;订阅/originman/cmd/*控制话题自行开发的自定义节点,这是所有 OriginMan 专属话题的创建者 RosClaw 插件(Thor 端) 上下文注入 + 工具注册 读取 /rosclaw/capabilities或/originman/state,将机器人能力注入 agent 会话
换句话说:如果在 OriginMan 上只装 rosbridge 和 discovery、不写适配器,Thor 端 agent 能看到 ROS2 图上有哪些话题,但不会有统一的
/originman/state来汇总机器人状态,也不会有/originman/cmd/mode来切换功能。适配器才是把零散功能节点包装成可控接口的关键环节。
适配器注册了五种模式,每种模式对应 OriginMan 官方功能包中的一个独立节点:
| 模式 | 调用的 ROS2 节点 | 是否运动 | 话题桥接 |
|---|---|---|---|
color_detect |
originman_color_detect color_detect |
❌ | /color_info |
apriltag |
originman_apriltag_detect apriltag_detect |
❌ | /apriltag_info |
line_follow |
originman_line_follower line_follower |
⚠️ | /line_info |
color_track |
originman_color_detect color_track |
⚠️ | /color_info |
audio_control |
originman_audio_control audio_control_node |
⚠️ | - |
8.9.2 构建与启动
适配器构建依赖于 OriginMan 官方的 dev_ws(提供 originman_* 功能包)和 rosclaw 工作区:
cd ~/rosclaw_ws
source /opt/tros/humble/setup.bash
source /userdata/dev_ws/install/setup.bash
colcon build --symlink-install --packages-select originman_rosclaw_adapter
# 启动适配器
source install/setup.bash
ros2 run originman_rosclaw_adapter adapter_node
启动后适配器输出:
[originman_adapter]: 适配器就绪 | 可用模式: color_detect, apriltag, line_follow, color_track, audio_control
[originman_adapter]: 控制话题: /originman/cmd/{mode,tts,action}
[originman_adapter]: 状态话题: /originman/{state,camera/image_raw,detection_info}
8.9.3 模式切换验证
验证按照"先静态后动态"的原则进行——先用不会运动的 AprilTag 识别确认链路贯通,再测试会驱动机器人运动的视觉巡线。
(1)AprilTag 标签识别(无运动,先验链路)
ros2 topic pub --once /originman/cmd/mode std_msgs/msg/String "data: 'apriltag'"
命令下发后适配器立即响应,日志输出:
[originman_adapter]: Starting: AprilTag 识别 - 检测二维码标签
[originman_adapter]: Bridge: /image_raw + /apriltag_info
[originman_adapter]: Active: apriltag
状态话题在约 1 秒内从 idle 切换为 apriltag,同时 camera 字段从不活跃变为活跃——模式切换和相机上电在同一个状态更新中完成,不需要分别等待。
在摄像头前方放置一张 tag36h11 系列的 ID=1 标签后,连续采集了 10 秒的检测结果:
| 指标 | 实测值 |
|---|---|
| 检测次数 | 37 次(~3.7 Hz) |
| 命中率 | 100%(37/37,无漏检无误检) |
| Tag ID | 1(稳定识别,从未跳变) |
| 家族 | tag36h11 |
| 状态推送 | 4 次(~2.5 秒间隔) |
全程检测结果保持一致,未出现 tag_id 跳变或"未检测到"的帧。当摄像头前方未放置标签时,检测结果正确返回 "未检测到 AprilTag",说明适配器对空场景的处理也是正确的。
AprilTag 模式不触发任何舵机运动,是最安全的联调首选——用它确认链路畅通后,再进行下面的巡线测试。
使用OpenClaw调用AprilTag识别功能
(2)视觉巡线(会运动,验证闭环)
ros2 topic pub --once /originman/cmd/mode std_msgs/msg/String "data: 'line_follow'"
适配器依次执行后台节点启动、话题桥接和舵机初始化,约 2 秒后状态切为 line_follow、相机激活。
巡线的控制逻辑不变:当黑线中心 X 坐标与画面中线(320px)偏差 ≤50px 时执行 go_forward,偏左(X<270)时右转修正,偏右(X>370)时左转修正。三次独立的 30 秒巡线测试覆盖了三种典型路径:
| 测试 | 路线特征 | 检测量 | X 均值 | 标准差 | X 范围 | 死区占比 | 跳变次数 |
|---|---|---|---|---|---|---|---|
| Run 1 | 带急弯 | 259 条 | 281.5 | 40.7 | 189~451 | 54% | 数十次 |
| Run 2 | 缓左偏 | 263 条 | 264.9 | 15.8 | 207~297 | 45% | 6 次 |
| Run 3 | 平直偏左 | 202 条 | 273.4 | 21.5 | 216~318 | 58% | 1 次 |
三次测试展现了不同的控制特征:Run 1 的急弯导致 X 值一度冲到 451(偏差 +131px),触发猛烈的左转修正,随后经历"丢线→重捕"循环后恢复正常;Run 2 和 Run 3 整体平缓,全程只需右转微调,死区内直行占比在 45%~58% 之间。
三次均未出现模式异常切换或节点崩溃,检测频率稳定在 6.7~8.7 Hz。发送 stop 后机器人立即回到 idle,相机释放、舵机停转。
使用OpenClaw调用巡线功能
8.9.4 验证汇总
| 测试项 | 结果 | 关键数据 |
|---|---|---|
| 适配器启动 | ✅ | 5 个模式注册,3 个控制话题 + 3 个状态话题 |
| 状态发布 | ✅ | 每 ~2 秒推送 JSON,active_mode / camera / robot 字段联动正确 |
apriltag 模式 |
✅ | 10s 内 37 次检测,100% 命中,Tag ID=1 tag36h11,无跳变无误检 |
line_follow 模式 |
✅ | 3 次 30s 连续巡线,覆盖急弯/缓弯/直道,检测频率 6.7~8.7 Hz |
stop 模式切换 |
✅ | 任意模式下发 stop 后 ~2s 回到 idle,相机释放、节点退出 |
上述结果表明:通过一个约 250 行 Python 的话题驱动适配层,RosClaw 能够在 Thor 端通过自然语言代理发现、切换和监控 OriginMan 的功能模式。AprilTag 识别和视觉巡线的端到端验证覆盖了"静态检测"和"动态控制"两类场景,确认了 rosbridge → 适配器 → 功能节点 → 状态回传的完整数据闭环。
8.10 故障排查
实机部署中遇到的问题可以分为十类,前四类为预判型排查项(已在部署前规划),其余为实测中逐步暴露的新问题。
问题一:9090 端口不通
表现为 Thor 无法连接 rosbridge。
ss -lntp | rg 9090
ping 192.168.127.10
实测中 Thor ↔ OriginMan 为有线直连(192.168.127.1 ↔ 192.168.127.10),延迟仅 0.2ms,从未出现端口不通。但 rosbridge 启动失败时该端口的监听自然不存在,需检查环境 source 是否正确。
问题二:/rosclaw/capabilities 无输出
source ~/rosclaw_ws/install/setup.bash
ros2 topic echo /rosclaw/capabilities --once
ros2 service call /rosclaw/get_capabilities rosclaw_msgs/srv/GetCapabilities "{robot_namespace: ''}"
实测中 topic echo 返回了正确的能力清单(robot_name=OriginMan, namespace=/originman)。但如果 discovery_node 启动时未同时 source dev_ws 和 rosclaw_ws,将无法发现功能节点,导致 capabilities 中的 topic/service/action 列表为空。
问题三:Gateway 插件未连接 ROS2
openclaw gateway restart
openclaw gateway probe
journalctl --user -u openclaw-gateway -f
实测中 Gateway 重启后 WebSocket 连接到 ws://192.168.127.10:9090,会话上下文中注入 Robot: OriginMan 标记即为连接成功。如果标记未出现,最可能的原因是 openclaw.json 中 rosbridge.url 的 IP 地址与 OriginMan 实际有线 IP 不匹配。
问题四:命名空间不统一
手册建议将关键接口统一挂到 /originman/... 前缀下。实测中 rosclaw_discovery 通过 robot_namespace:=/originman 参数实现了这一要求,适配器也遵循了 /originman/ 前缀约定。验证命令:
ros2 node list
ros2 topic list | rg '^/originman/'
ros2 service list | rg '^/originman/'
问题五:重复 rosbridge 实例
这是实测中首次暴露的问题。 在反复调试过程中,多次执行 ros2 launch rosbridge_server 而未先清理旧进程,导致同一台机器上出现 3 个 rosbridge_websocket 实例。端口 9090 仍被第一个实例占用,但后续实例的 ros2 节点注册和参数服务器访问可能产生冲突。
排查方法:
ps aux | grep rosbridge | grep -v grep
# 如果看到多个 python3 进程,说明有残留实例
解决方法:
pkill -f rosbridge_server
sleep 2
# 确认清理干净后重新启动
ros2 launch rosbridge_server rosbridge_websocket_launch.xml port:=9090 address:=0.0.0.0
建议在启动脚本中始终先执行 pkill 清理,避免残留累积。
问题六:摄像头被后台服务占用
sudo systemctl stop originman.service
ls /dev/video*
实测中这是最常见的阻塞点:OriginMan 默认开机服务 originman.service 会启动 OriginMan.py,独占 /dev/video0。如果忘记停止该服务,适配器启动 color_detect 或 line_follow 模式时,其内部 cv2.VideoCapture(0) 将无法打开摄像头。
实测验证:停掉 originman.service 后,所有视觉模式(颜色识别、AprilTag、巡线)的相机初始化均恢复正常。
问题七:LAB 阈值错误导致视觉检测失败
这是实测中最隐蔽的问题之一。 巡线模式启动后相机正常工作,但即使地面已铺黑线,检测结果始终为 Line Center X: -1。通过采样分析发现,原始代码中的 LAB 阈值存在问题:
# 原始阈值(错误:A>166 是在找红色,不是黑色)
'black': {'min': [0, 166, 135], 'max': [255, 255, 255]}
# 修正后阈值(基于实测暗色像素的 LAB 分布)
'black': {'min': [0, 108, 100], 'max': [90, 148, 143]}
排查方法:采样 ROI 区域最暗像素,对比实际 LAB 值与代码阈值:
import cv2, numpy as np
img = cv2.imread('snapshot.jpg')
roi = img[240:480, 0:640] # 巡线 ROI
lab = cv2.cvtColor(roi, cv2.COLOR_BGR2LAB)
dark_mask = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) < 40
dark_lab = lab.reshape(-1, 3)[dark_mask.reshape(-1)]
print(f'L: {np.mean(dark_lab[:,0]):.0f}±{np.std(dark_lab[:,0]):.0f}')
print(f'A: {np.mean(dark_lab[:,1]):.0f}±{np.std(dark_lab[:,1]):.0f}')
print(f'B: {np.mean(dark_lab[:,2]):.0f}±{np.std(dark_lab[:,2]):.0f}')
如果 A 通道均值远低于 166(如 126),B 通道均值低于 135(如 122),则说明黑色像素实际为中性灰,需放宽阈值至中性灰范围。
解决方法:直接编辑安装目录下的 line_follower.py,将 lab_data 字典中的 black 阈值修正为暗色中性灰范围。如使用 --symlink-install,需确认修改的是安装路径下的文件,而非源码路径。修正后重新启动巡线模式即可生效。
问题八:TTS 双路径--smbus 不可用但 I2S 正常
这是实测中暴露的另一个问题。 OriginMan 同时存在两条音频输出路径:
| 路径 | 驱动方式 | 实测结果 |
|---|---|---|
| I2C smbus → 板载 TTS 芯片 | smbus.SMBus(0) 写 0x40 地址 |
❌ [Errno 121] Remote I/O error |
| I2S → MAX98357A 功放 | aplay -D plughw:0 |
✅ 正常 |
适配器的 TTS 模块只实现了第一条路径(smbus),因此始终不可用。进一步排查发现声卡 duplexaudioi2s1(card 0)的 MAX98357A 驱动已正确加载:
$ lsmod | grep max98357
snd_soc_max98357a 20480 0
$ aplay -l
card 0: duplexaudioi2s1 [duplex-audio-i2s1], device 0:
i2s1-voicehat-hifi voicehat-hifi-0
解决方案:在适配器中新增一条备用音频路径,当 smbus 初始化失败时自动降级为子进程调用 aplay,桥接 /originman/cmd/tts 文本到 WAV 文件的生成与播放链路。
问题九:ros2 topic pub 超时问题
使用 ros2 topic pub --once 向非持久化话题发送消息时,如果消息发出后没有订阅者在监听,默认等待匹配订阅者的行为可能导致超时。实测中对 /originman/cmd/mode 话题使用 --once 时偶发 timeout。
解决方法:使用 -1(发送后立即退出)+ timeout 外壳命令:
timeout 5 ros2 topic pub -1 /originman/cmd/mode std_msgs/msg/String 'data: stop'
问题十:DashScope 云 TTS 欠费
OriginMan 自带的 TTS 工具 audio_generator.py 依赖阿里云 DashScope API(CosyVoice 模型),实测时 API 返回 Arrearage 错误,账户处于欠费状态。这不影响本地 WAV 文件播放(aplay),但意味着无法通过云端实时合成任意文本。如需恢复云 TTS 功能,需在阿里云控制台充值并更新 API Key。
问题十一:RosClaw 插件工具未注入 Agent
这是验证阶段的最后一个障碍。 插件加载、transport 连接、机器人上下文注入均正常,但 ros2_list_topics 等工具始终未出现在 agent 的函数列表中。通过在插件源码中嵌入日志确认:所有 7 个工具已通过 api.registerTool() 成功注册到插件注册表,但 OpenClaw 2026.4.22 的 session 级工具解析器没有将它们传递给当前 agent session。
排查方法:
-
- 1. 检查 Gateway 日志确认插件加载:
grep rosclaw /tmp/openclaw/*.log
- 2. 从 Thor 端直接通过 rosbridge WebSocket 调用 API,确认链路可用:
const ws = new WebSocket('ws://192.168.127.10:9090');
ws.send(JSON.stringify({op:'call_service', service:'/rosapi/topics'}));
- 3. 如果 WebSocket 直连能列 topic(Thor 无需 ROS2),说明传输链路正常,问题在插件→agent 工具映射环节。
解决方法:需要在 RosClaw 插件中适配 OpenClaw 最新版的 AgentTool 类型签名,特别是 execute 函数的 this: void 绑定和返回类型。更根本的方案是由插件作者更新以匹配当前 OpenClaw plugin SDK。当前缓解措施:通过 SSH 或 WebSocket 直连 rosbridge 手工调用。
故障排查速查表
| 序号 | 现象 | 首选排查命令 | 常见根因 |
|---|---|---|---|
| 1 | Thor 连不上 | ss -lntp | rg 9090 |
rosbridge 未启动或 IP 错误 |
| 2 | 无 capabilities | ros2 topic echo /rosclaw/capabilities --once |
discovery_node 未启动或 source 不全 |
| 3 | 插件无反应 | openclaw gateway restart |
Gateway 未重载配置 |
| 4 | 话题前缀混乱 | ros2 topic list | rg '^/originman/' |
命名空间参数未设置 |
| 5 | 多个 rosbridge | ps aux | grep rosbridge |
未清理旧进程 |
| 6 | 相机打不开 | systemctl stop originman.service |
后台服务占用 /dev/video0 |
| 7 | 视觉检测全失败 | 采样 ROI LAB 值对比阈值 | LAB 阈值错误(A>166 非黑) |
| 8 | TTS 没声音 | aplay -l + lsmod | grep max98357 |
smbus 驱动不可用,切换 aplay |
| 9 | pub 命令超时 | 加 -1 参数 + timeout 外壳 |
--once 等待匹配订阅者 |
| 10 | 云 TTS 报错 | 检查 API Key 余额 | DashScope 账户欠费 |
| 11 | ros2_* 工具不出现 | grep rosclaw /tmp/openclaw/*.log |
插件 API 版本不兼容,工具注册但未注入 session |
这十一类问题覆盖了从传输层、ROS2 图、视觉算法、硬件驱动到第三方 API 的全链路排查路径。实测经验表明,大部分问题在脱离 OpenClaw 代理、直接在 OriginMan 端手工调用目标接口时即可定位--手工调用不通则根因在机器人侧,手工调用通而代理不一致则需检查 RosClaw 配置。
8.12 本节结论
RosClaw 接入 OriginMan 最稳妥的路线是按顺序推进,而不是一次性全自动。先装 rosbridge 和 rosclaw_discovery,确认能力清单和传输都通了;再做只读验证和低风险控制;最后通过适配层把巡线、AprilTag 和动作组交给代理。
实测全程沿这条路线推进,关键结果:
传输:Thor ↔ OriginMan 有线直连(192.168.127.1 ↔ 192.168.127.10),延迟 0.2ms,rosbridge WebSocket 稳定•
发现:rosclaw_discovery 周期发布能力清单,Gateway 自动注入 Robot: OriginMan•
只读:话题列表、状态 JSON、相机画面全部通过•
适配:约 250 行的 originman_rosclaw_adapter 以话题驱动方式统一包装了 5 种功能模式•
控制:AprilTag(10s/37次/100%命中)和巡线(3轮30s/急弯缓弯直道全覆盖)模式切换正常,闭环验证通过•
排障:梳理了 11 类实测问题,从端口、命名空间、摄像头冲突到 LAB 阈值错误和 TTS 双路径
第九节从方案到实施、到踩坑、到修复,是一套可以直接在机器上复现的部署流程。
9. 结论
写这篇文章的初衷很简单:OriginMan 官方资料给出了每个功能模块怎么跑,但没说怎么把它们串成一个可以远程控制的完整系统。
所以这篇文章做的事情就是补上这一环。
我们先把 OriginMan 自带的巡线、颜色识别、AprilTag、语音交互和动作模仿逐个拆了一遍,关注的是代码里实际做了什么,而不是官方文档说了什么。然后通过 RosClaw,在 Thor 和 OriginMan 之间架起了一套完整的桥接:rosbridge 负责传输,rosclaw_discovery 负责能力发现,约 250 行的适配器把零散的功能节点包装成统一的话题接口。
实测中走通了从只读查看到模式控制的完整链路,AprilTag 和巡线两种模式经过了多轮闭环验证。也踩了不少坑——LAB 阈值把红色当黑色、TTS 卡在 I2C 总线、摄像头被后台服务霸占、rosbridge 多实例冲突——每个坑的具体排查方法和最终修复都记录在第九节里。
如果你也在用 OriginMan + RosClaw 搭类似的东西,希望这篇文章能帮你省掉一些踩坑的时间。
参考资料
1. OriginMan 官方首页与技术规格:https://originman.guyuehome.com/
2. OriginMan 视觉巡线文档:https://originman.guyuehome.com/application/line_follower/
3. OriginMan 颜色识别与颜色跟踪文档:https://originman.guyuehome.com/application/color_recognition/
4. OriginMan AprilTag 识别文档:https://originman.guyuehome.com/zh/application/airiltag/
5. OriginMan 语音识别文档:https://originman.guyuehome.com/zh/application/speech_recognition/
6. OriginMan 大语言模型文本交互文档:https://originman.guyuehome.com/zh/application/large_model/
7. OriginMan 语音交互与控制文档:https://originman.guyuehome.com/zh/application/audio_control/
8. OriginMan 模仿动作文档:https://originman.guyuehome.com/zh/application/imitate_action/
9. ROS2 Humble 文档,About Nodes:https://docs.ros.org/en/humble/Concepts/Basic/About-Nodes.html
10. ROS2 Humble 文档,About Quality of Service Settings:https://docs.ros.org/en/humble/Concepts/Intermediate/About-Quality-of-Service-Settings.html
11. AprilTag 官方资料:https://april.eecs.umich.edu/software/apriltag.html
12. RosClaw 仓库 README:https://github.com/PlaiPin/rosclaw
13. RosClaw 插件入口 src/index.ts:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/extensions/openclaw-plugin/src/index.ts
14. RosClaw 配置定义 src/config.ts:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/extensions/openclaw-plugin/src/config.ts
15. RosClaw 传输服务 src/service.ts:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/extensions/openclaw-plugin/src/service.ts
16. RosClaw 机器人上下文注入 src/context/robot-context.ts:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/extensions/openclaw-plugin/src/context/robot-context.ts
17. RosClaw 安全钩子 src/safety/validator.ts:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/extensions/openclaw-plugin/src/safety/validator.ts
18. RosClaw 发现节点 rosclaw_discovery/discovery_node.py:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/ros2_ws/src/rosclaw_discovery/rosclaw_discovery/discovery_node.py
19. RosClaw 能力描述消息 CapabilityManifest.msg:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/ros2_ws/src/rosclaw_msgs/msg/CapabilityManifest.msg
20. RosClaw 能力查询服务 GetCapabilities.srv:https://raw.githubusercontent.com/PlaiPin/rosclaw/main/ros2_ws/src/rosclaw_msgs/srv/GetCapabilities.srv
21. OpenClaw 配置文档 openclaw config set:https://docs.openclaw.ai/cli-reference/config/config-set
22. OpenClaw 网关文档 openclaw gateway run:https://docs.openclaw.ai/cli-reference/gateway/gateway-run
23. OpenClaw 插件文档 openclaw.plugin.json:https://docs.openclaw.ai/plugins/build/openclaw-plugin-json
adapter_node.py
#!/usr/bin/env python3
"""
ironman_rosclaw_adapter - Bridges OriginMan robot nodes to RosClaw.
Uses topic-based control for simplicity (no custom services needed).
Control topics (subscribe):
/originman/cmd/mode (String) - "color_detect"|"apriltag"|"stop"
/originman/cmd/tts (String) - text to speak
/originman/cmd/action (String) - action group name
Status topics (publish):
/originman/state (String) - JSON system state
/originman/camera/image_raw (Image) - bridged camera
/originman/detection_info (String) - bridged detection results
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from sensor_msgs.msg import Image
import subprocess
import signal
import os
import json
import threading
import time
class AdapterNode(Node):
AVAILABLE_MODES = {
'color_detect': {
'desc': '颜色识别 - 检测红/绿/蓝物体',
'movement': False,
'pkg': 'originman_color_detect',
'exec': 'color_detect',
'det_topic': '/color_info',
},
'apriltag': {
'desc': 'AprilTag 识别 - 检测二维码标签',
'movement': False,
'pkg': 'originman_apriltag_detect',
'exec': 'apriltag_detect',
'det_topic': '/apriltag_info',
},
'line_follow': {
'desc': '视觉巡线 - 跟踪地面黑线 [会移动]',
'movement': True,
'pkg': 'originman_line_follower',
'exec': 'line_follower',
'det_topic': '/line_info',
},
'color_track': {
'desc': '颜色追踪 - PID追踪彩色物体 [会移动]',
'movement': True,
'pkg': 'originman_color_detect',
'exec': 'color_track',
'det_topic': '/color_info',
},
'audio_control': {
'desc': '语音控制 - 接收文本指令 [会执行动作]',
'movement': True,
'pkg': 'originman_audio_control',
'exec': 'audio_control_node',
'det_topic': None,
},
}
def __init__(self):
super().__init__('originman_adapter')
self.active_mode = 'idle'
self.current_process = None
self.process_lock = threading.Lock()
# Publishers
self.state_pub = self.create_publisher(String, '/originman/state', 10)
self.image_pub = self.create_publisher(Image, '/originman/camera/image_raw', 10)
self.detection_pub = self.create_publisher(String, '/originman/detection_info', 10)
# Subscribers (control commands)
self.mode_sub = self.create_subscription(
String, '/originman/cmd/mode', self.mode_cmd_cb, 10)
self.tts_sub = self.create_subscription(
String, '/originman/cmd/tts', self.tts_cmd_cb, 10)
self.action_sub = self.create_subscription(
String, '/originman/cmd/action', self.action_cmd_cb, 10)
# Dynamic bridges
self._image_sub = None
self._detection_sub = None
# TTS init
self.tts_ok = self._init_tts()
# State timer
self.state_timer = self.create_timer(2.0, self._publish_state)
self._publish_state()
self.get_logger().info('适配器就绪 | 可用模式: ' +
', '.join(self.AVAILABLE_MODES.keys()))
self.get_logger().info('控制话题: /originman/cmd/{mode,tts,action}')
self.get_logger().info('状态话题: /originman/{state,camera/image_raw,detection_info}')
# ── TTS ──
def _init_tts(self):
try:
import smbus
self.tts_bus = smbus.SMBus(0)
self.tts_addr = 0x40
return True
except Exception as e:
self.get_logger().warn(f'TTS init failed: {e}')
return False
def _tts_speak(self, text):
if not self.tts_ok:
return False
try:
head = [0xFD, 0x00, 0x00, 0x01, 0x00]
words = text.encode('gb2312')
length = len(words) + 2
head[1] = length >> 8
head[2] = length
head.extend(list(words))
self.tts_bus.write_i2c_block_data(self.tts_addr, 0, head)
return True
except Exception as e:
self.get_logger().error(f'TTS error: {e}')
return False
# ── Process Management ──
def _launch(self, mode):
info = self.AVAILABLE_MODES[mode]
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
self.get_logger().info(f'Starting: {info["desc"]}')
return subprocess.Popen(
['ros2', 'run', info['pkg'], info['exec']],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env, preexec_fn=os.setsid)
def _kill_current(self):
with self.process_lock:
if self.current_process is None:
return
try:
os.killpg(os.getpgid(self.current_process.pid), signal.SIGTERM)
self.current_process.wait(timeout=5)
except Exception:
try:
os.killpg(os.getpgid(self.current_process.pid), signal.SIGKILL)
except Exception:
pass
self.current_process = None
# ── Topic Bridges ──
def _setup_bridges(self, mode):
self._cleanup_bridges()
self._image_sub = self.create_subscription(
Image, '/image_raw', self._on_image, 10)
det_topic = self.AVAILABLE_MODES[mode].get('det_topic')
if det_topic:
self._detection_sub = self.create_subscription(
String, det_topic, self._on_detection, 10)
self.get_logger().info(f'Bridge: /image_raw + {det_topic}')
def _cleanup_bridges(self):
if self._image_sub:
self.destroy_subscription(self._image_sub)
self._image_sub = None
if self._detection_sub:
self.destroy_subscription(self._detection_sub)
self._detection_sub = None
def _on_image(self, msg):
self.image_pub.publish(msg)
def _on_detection(self, msg):
self.detection_pub.publish(msg)
# ── Control Callbacks ──
def mode_cmd_cb(self, msg):
cmd = msg.data.strip()
if cmd == 'stop':
if self.active_mode != 'idle':
old = self.active_mode
self._kill_current()
self._cleanup_bridges()
self.active_mode = 'idle'
self.get_logger().info(f'Stopped: {old}')
self._publish_state()
return
if cmd not in self.AVAILABLE_MODES:
self.get_logger().warn(
f'Unknown mode: {cmd}. Available: {list(self.AVAILABLE_MODES.keys())}')
return
# Stop current first
if self.active_mode != 'idle':
self.get_logger().info(f'Stopping current: {self.active_mode}')
self._kill_current()
self._cleanup_bridges()
# Launch new
try:
self.current_process = self._launch(cmd)
time.sleep(2)
if self.current_process.poll() is not None:
err = self.current_process.stderr.read().decode(
'utf-8', errors='replace')
self.get_logger().error(f'Launch failed: {err[:200]}')
self.active_mode = 'idle'
else:
self.active_mode = cmd
self._setup_bridges(cmd)
self.get_logger().info(f'Active: {cmd}')
except Exception as e:
self.get_logger().error(f'Launch error: {e}')
self.active_mode = 'idle'
self._publish_state()
def tts_cmd_cb(self, msg):
text = msg.data.strip()
if not text:
return
self.get_logger().info(f'TTS: {text[:80]}')
ok = self._tts_speak(text)
if not ok:
self.get_logger().warn('TTS unavailable')
def action_cmd_cb(self, msg):
name = msg.data.strip()
# Safety: only log, don't execute during verification
self.get_logger().info(f'Action requested: {name} (logged only - safety mode)')
# ── State ──
def _publish_state(self):
state = {
'active_mode': self.active_mode,
'available_modes': list(self.AVAILABLE_MODES.keys()),
'mode_details': {k: v['desc'] for k, v in self.AVAILABLE_MODES.items()},
'camera': 'active' if self._image_sub else 'inactive',
'tts': 'available' if self.tts_ok else 'unavailable',
'robot': self.active_mode if self.active_mode != 'idle' else 'idle',
}
self.state_pub.publish(
String(data=json.dumps(state, ensure_ascii=False)))
def destroy_node(self):
self._kill_current()
self._cleanup_bridges()
super().destroy_node()
def main(args=None):
rclpy.init(args=args)
node = AdapterNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
279