智元灵犀X2机器人二开03:端侧接口实战,控制、交互与感知

前置:第 02 篇(环境已通、能跑自己的节点)。


一、控制模块的七个接口

控制模块是 AIMDK 的核心。七个子模块按使用频率排列如下:

控制模块的七个子模块与依赖关系 控制模块下辖运动模式切换、走跑控制、MC 控制信号、预设动作、灵创动作、末端执行器与关节控制七个子模块;其中走跑控制依赖 MC 控制信号,灵创动作依赖运动模式切换,关节控制可能需要关闭 MC。 控制模块 · 七个子模块与依赖关系 控制模块 运动模式切换 查询 / 设置 走跑控制 速度级控制 MC 控制信号 输入源管理 / 仲裁 预设动作控制 灵创动作 动作资源播放 末端执行器控制 夹爪 / 灵巧手 关节控制 关节级控制与状态 依赖 依赖 可能需关闭 实线:从属关系 虚线:依赖关系(箭头指向被依赖的模块)

虚线标出的是真实存在的依赖关系,也是新手容易忽略的部分:走跑控制依赖 MC 输入源注册;动作播放依赖运动模式正确;关节控制有时需要临时关闭 MC。

下面按”用得多 → 用得少”展开。

1.1 运动模式切换

机器人存在一个运动模式状态,并非任何时候都能接受任意指令。标准流程是:查询当前模式 → 确认可操作 → 设置到目标模式。

需要特别注意 v0.8 起的状态码变更:零力矩、软急停、关节冻结、跑步等旧模式码已不对二开开放,必须替换为 PASSIVE_DEFAULT / DAMPING_DEFAULT / JOINT_DEFAULT / STAND_DEFAULT / LOCOMOTION_DEFAULT。

经验:任何运动相关程序,第一步都应该查模式。跳过这一步直接下发速度指令,是端侧调试中最常见的翻车点——失败时往往没有明确报错。

1.2 走跑控制

走跑控制通过话题下发速度指令(话题前缀 /aima/),是持续性的速度流,而非一次性请求。

关键机制是多输入源管理:机器人可能同时收到手柄、APP、你的程序三类指令,必须先注册一个输入源,你的指令才会被 MC 采纳。

1.3 MC 控制信号与输入源仲裁

MC(Motion Control)模块负责输入源配置与仲裁:

  • 输入源配置:声明身份并注册到 MC 模块
  • 仲裁流程:多个输入源同时请求控制时,由 MC 决定优先级

配套有专门的输入源管理服务做注册 / 注销。

取舍:做”语音控制 + 手柄接管”这类多来源控制时,仲裁逻辑绕不开。一个常见设计是让手柄持有最高优先级,程序侧只在中低优先级区间竞争。

1.4 预设动作控制

官方提供一批预设动作,通过预设动作控制服务传入动作 ID 即可播放,不需要自己上传资源。

经验:验证”动作链路是否通”时,先用预设动作,不要一上来就调灵创资源。前者排除了资源匹配、resource_key 解析等变量,能快速区分是”链路问题”还是”资源问题”。

1.5 灵创动作

灵创动作是部署在机器人上的动作资源,包含全身动作(如舞蹈)与手臂动作。这是二开中最常用的动作能力,由两个服务配合完成。

灵创动作播放流程 play_action_by_keyword 先拉取资源列表,按名称做精确与包含两级匹配,再按 resource_key 是否含 onnx 判定全身或手臂动作,最后调用 ExecuteActionResource 播放并校验返回码。 灵创动作播放 · play_action_by_keyword(name) play_action_by_keyword(name) GetRobotResources 拉取全部资源 拉取成功? 返回 False 按 name 匹配资源 精确匹配命中? 包含匹配命中? 未找到,返回 False 取 resource_key / version resource_key 含 onnx? meta: BODY_MONTION 全身动作 meta: ARM_MONTION 手臂动作 ExecuteActionResource 播放 header.header.code == 0? 播放成功 播放失败 否 是 否 是 否 是 是 否 是 否

关键规则:resource_key 中含 onnx 的是全身动作(BODY_MONTION),否则是手臂动作(ARM_MONTION)。

完整实现:

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
#!/usr/bin/env python3
"""灵创动作播放器:按关键词匹配并播放"""

import json

import rclpy
from rclpy.node import Node
from aimdk_msgs.srv import GetRobotResources, ExecuteActionResource

SVC_GET_RES = "/aimdk_5Fmsgs/srv/GetRobotResources"
SVC_PLAY_ACT = "/aimdk_5Fmsgs/srv/ExecuteActionResource"


class LinkCraftPlayer(Node):
def __init__(self):
super().__init__("linkcraft_player")
self.srv_get_res = self.create_client(GetRobotResources, SVC_GET_RES)
self.srv_play_act = self.create_client(ExecuteActionResource, SVC_PLAY_ACT)
self.resource_list = []

def refresh_all_resources(self) -> bool:
if not self.srv_get_res.wait_for_service(timeout_sec=1.0):
self.get_logger().warn("灵创资源服务未就绪")
return False

future = self.srv_get_res.call_async(GetRobotResources.Request())
rclpy.spin_until_future_complete(self, future, timeout_sec=3.0)
res = future.result()

if res is None or res.header.header.code != 0:
self.get_logger().error(
f"获取资源失败:{res.header.message if res else '无响应'}")
return False

self.resource_list = res.robot_resources
self.get_logger().info(f"已加载 {len(self.resource_list)} 个动作资源")
return True

def _match(self, name: str):
"""两级匹配:精确优先,其次包含。

只做包含匹配会出现 '爱你' 误命中 '新版爱你' 这类问题。
"""
for item in self.resource_list:
if item.current_version.name == name:
return item
for item in self.resource_list:
if name in item.current_version.name:
return item
return None

def play_action_by_keyword(self, name: str) -> bool:
if not self.refresh_all_resources():
return False

target = self._match(name)
if target is None:
self.get_logger().warn(f"未找到动作:{name}")
return False

req = ExecuteActionResource.Request()
req.resource_key = target.resource_key
req.resource_version = target.current_version.version
req.slaves = []

# 官方 meta 规则:resource_key 含 onnx 即全身动作
resource_type = (
"BODY_MONTION" if "onnx" in req.resource_key else "ARM_MONTION"
)
req.meta = json.dumps({"resource_type": resource_type})

future = self.srv_play_act.call_async(req)
rclpy.spin_until_future_complete(self, future, timeout_sec=5.0)
resp = future.result()

if resp is not None and resp.header.header.code == 0:
self.get_logger().info(f"播放成功:{target.current_version.name}")
return True
self.get_logger().error(
f"播放失败:{resp.header.message if resp else '服务无返回'}")
return False

两点设计说明:

  1. 两级匹配。只做包含匹配会导致 爱你 误命中 新版爱你。精确匹配优先、包含匹配兜底,是成本最低的改进。
  2. onnx 判类型属于脆弱依赖。它是官方约定,但依赖资源命名规范。生产环境建议在拉列表时就把 resource_key → resource_type 的映射缓存下来,避免每次播放都重新推断。

1.6 末端执行器控制

控制夹爪与灵巧手,通过话题下发指令,另有手部状态信息话题与查询手部类型服务。

1.7 关节控制

提供关节级控制(话题下发)与关节状态查询(服务读取)。

⚠️ 官方安全提示:某些关节控制示例需临时关闭 MC 模块。这类操作风险较高,必须先在仿真环境验证,并明确恢复方式。


二、交互模块

2.1 语音控制

三块能力:ASR(语音转文本)、TTS(文本转语音)、MIC 音频流采集(直接订阅麦克风音频流,含 VAD 相关能力)。

⚠️ 语音合成存在优先级限制,需避免同时播放多条语音。做”边播报边执行动作”时,必须考虑 TTS 的排队与打断策略。

取舍:自研语音系统需要先关闭内置交互(切到 only_voice)以释放音频流。但关闭后端侧 VAD、唤醒、播报排队都需要自行补齐——除非确实需要定制语音链路,否则优先复用内置能力。

2.2 屏幕控制

通过 /face_ui_proxy/ 前缀的服务控制屏幕显示(表情、UI 元素)。

表情播放会占用显示资源。多个表情源并发时会出现互相覆盖,需要在应用层做互斥。

2.3 灯带控制

控制灯带颜色与效果。用于情感表达和状态指示,例如”导航中”用呼吸灯、”等待唤醒”用低亮度常亮。


三、感知模块与其他

模块 状态 注意
感知模块 选装 写依赖视觉的代码前,先确认目标机器是否装配
硬件抽象模块 可用 底层硬件统一抽象层
故障与系统管理 待发布 当前不可用
开发者模式 可用 开发者相关配置

四、动手:动作序列编排

把前面的能力串起来。真实实现中,走跑控制涉及”查模式 → 注册输入源 → 发布速度 → 零速停止”四步,下面给出编排骨架。

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
#!/usr/bin/env python3
"""端侧组合动作:走一段 → 播灵创动作 → 说一句话"""

import json
import time

import rclpy
from rclpy.node import Node
from aimdk_msgs.srv import GetRobotResources, ExecuteActionResource


class ActionSequence(Node):
def __init__(self):
super().__init__("action_sequence")
self.get_res = self.create_client(
GetRobotResources, "/aimdk_5Fmsgs/srv/GetRobotResources")
self.play_act = self.create_client(
ExecuteActionResource, "/aimdk_5Fmsgs/srv/ExecuteActionResource")

def walk(self, duration_sec: float = 2.0) -> bool:
"""走一段。真实实现四步:
① 查询运动模式,确认可运动
② 向 MC 模块注册输入源
③ 向走跑控制话题持续发布速度指令
④ 显式发送零速停止
具体服务 / 话题名以官方接口文档为准。
"""
self.get_logger().info("① 走一段")
time.sleep(duration_sec)
self.get_logger().info(" 已停止")
return True

def play_action(self, keyword: str) -> bool:
self.get_logger().info(f"② 播放灵创动作:{keyword}")

if not self.get_res.wait_for_service(timeout_sec=1.0):
self.get_logger().warn(" 资源服务未就绪,跳过动作")
return False

future = self.get_res.call_async(GetRobotResources.Request())
rclpy.spin_until_future_complete(self, future, timeout_sec=3.0)
res = future.result()
if res is None or res.header.header.code != 0:
self.get_logger().warn(" 资源拉取失败,跳过动作")
return False

target = next(
(it for it in res.robot_resources
if keyword in it.current_version.name), None)
if target is None:
self.get_logger().warn(f" 未找到动作 {keyword},跳过")
return False

req = ExecuteActionResource.Request()
req.resource_key = target.resource_key
req.resource_version = target.current_version.version
req.slaves = []
req.meta = json.dumps({
"resource_type": "BODY_MONTION"
if "onnx" in req.resource_key else "ARM_MONTION"
})

future = self.play_act.call_async(req)
rclpy.spin_until_future_complete(self, future, timeout_sec=5.0)
resp = future.result()
ok = resp is not None and resp.header.header.code == 0
self.get_logger().info(" 播放成功" if ok else " 播放失败")
return ok

def speak(self, text: str):
self.get_logger().info(f"③ 语音播报:{text}")
# 调用语音合成服务;注意 TTS 优先级限制,避免同时播多条
# 具体服务名以官方接口文档为准


def main():
rclpy.init()
node = ActionSequence()
try:
node.walk(duration_sec=2.0)
node.play_action("新年快乐")
node.speak("大家好,我是灵犀 X2")
finally:
node.destroy_node()
rclpy.shutdown()


if __name__ == "__main__":
main()
动作序列编排的调用时序 main 依次驱动 ActionSequence 完成走一段、播灵创动作与语音播报;走一段内部包含查模式、注册输入源、发布速度与零速停止,动作播放包含拉取资源与下发播放。 main ActionSequence AIMDK 端侧 walk(2.0) 查运动模式 → 注册输入源 → 发布速度 零速停止 play_action("新年快乐") GetRobotResources ExecuteActionResource speak("大家好") 语音合成 持续执行 resource_key / version code == 0 显式停止,不依赖超时 受 TTS 优先级限制

编排的三条原则:

  1. 失败要降级。动作找不到时跳过并记录,而不是让整个序列抛异常中断——演示场景下”少一个动作”远比”整体崩掉”可接受。
  2. 动作之间留间隔。机器人执行动作需要物理时间,连续下发会互相打断。
  3. 运动必须显式停止。不要依赖”超时自动停”这类隐式行为。

五、上真机前的检查项

检查项 判据
运动类程序已注册输入源 注册服务返回成功,且机器人能响应速度指令
运动前已查询运动模式 日志中能看到模式查询结果
未使用 v0.8 废弃状态码 代码中不出现已废弃的旧模式常量
关节控制已知悉 MC 开关方式 明确知道如何关闭、如何恢复
速度 / 力矩参数有依据 不是试出来的数值
遥控手柄在手边 能随时接管
已在仿真环境验证 有验证记录
运控计算单元(PC1)无二开程序 ps 检查确认

结论

  • 控制模块七个接口:模式切换 / 走跑 / MC 输入源 / 预设动作 / 灵创动作 / 末端执行器 / 关节控制。
  • 灵创动作 = GetRobotResources 拉列表 + ExecuteActionResource 播放;resource_key 含 onnx 即全身动作。
  • 走跑控制依赖 MC 输入源注册——未注册的失败现象是”无报错、机器人不动”。
  • 交互模块三类:语音(ASR / TTS / MIC)、屏幕、灯带;TTS 有优先级限制,表情有资源互斥。
  • 编排动作序列:失败降级、留间隔、显式停止。
  • 验证动作链路时先用预设动作,排除资源匹配带来的干扰变量。

下一篇转到云端:创建灵心应用、完成 v1.4.0 的验签,写出第一个”文本进 → 技能 + LLM 回复出”的云端 Agent。


声明

本文首发智元AIMA开放平台,并在博客同步

欢迎关注我的其它发布渠道