这篇只讲这条线.看完你应该能回答三个问题:Q1 有哪些内置动作,每个动作该传什么值,传完之后客户端怎么知道成功了.
一,这次的验证边界
上一篇把验证拆成了四层,这篇沿用同一套框架,但要注意一个差别:动作类接口的"做没做到"完全落在第四层.
能验证的 | 不能验证的 |
字段类型,取值范围,必填与否 | 动作是否真的执行出来 |
服务名与命名空间 | 姿态是否与动作名称相符 |
客户端判定成败的条件 | 动作时长,平滑度,安全性 |
枚举值能否被服务端接受 | 部位掩码的真实作用范围 |
本文的替身服务端对每个请求都做两件事:记录字段,按 srv 声明校验范围.它不会假装动作执行成功之外的任何事,SetMcPresetMotion返回code=0只说明"请求被受理",不说明"机器人挥手了".
这个区分很重要,后面每张截图都要按这个前提读.
二,37 个枚举,编号里藏着产品线
McPresetMotion.msg是一个只有int32 value字段的消息,真正的信息在常量里.用ros2 interface show看,Q 系列长这样:
全部 37 个枚举按编号分成三组:
编号 | 数量 | 归属 |
| 1 |
|
| 20 | T 系列,单手与双手的旧编号 |
| 16 | Q 系列,Q1 自己的上肢动作 |
Q1 该用第三组.这不是推测,3xxx 段的名字全部带Q_前缀(Q_BECKON,Q_MAKE_HEART,Q_DANCE_A),2xxx 段里则明写着T1_TALK1到T1_TALK6.
编号还有一处值得注意:3006,3009,3011,3015 到 3017,3019 到 3020,3022 到 3045 全部没有定义.这份枚举是从一个更大的动作库里裁出来的,不是为 Q1 重新排的号.所以千万不要按"3001 到 3048 一共 48 个"去循环,实际只有 16 个.
三,官方示例里那张速查表对不上
这一节是这次实验里最意外的发现.
set_preset_motion.py在让你选动作之前,会先打印一张速查表.代码是这样的:
robot_series = input("\nEnter robot series (Q/T): ").strip().upper() if robot_series == "T": motion_map = {1001: "raise", 1002: "wave", 1003: "handshake", 1004: "airkiss"} elif robot_series == "Q": motion_map = {3001: "wave", 3002: "handshake", 3003: "bump", 3004: "wave_hand"}把这张表和.msg的定义逐条对照:
T 分支四条全错:示例说 1001 是raise,定义里 1001 是RIGHT_ARM_WAVE(右手挥手);示例说 1002 是wave,定义里 1002 是RIGHT_ARM_HANDSHAKE(右手握手);示例说 1004 是airkiss,定义里根本没有 1004 这个值.
Q 分支的编号都对,但名字有一处偏差:示例把 3001 标成wave(挥手),而.msg里 3001 是Q_BECKON(招手),3004 才是Q_WAVE_HAND(挥手).招手和挥手在中文里是两个动作,写代码时按示例理解就会调错.
结论很直接:这张表不能当文档用.要确认某个编号是什么动作,只信一条命令,ros2 interface show aimdk_msgs/msg/McPresetMotion.
四,播放之前,必须先切到 BIPED_WHOLE_BODY_CTRL
预设动作不是插上就能用的.set_preset_motion.py在发请求前,先做了这一步:
def ensure_ready_state(self) -> bool: switcher = McActionSwitcher(self) options = McActionSwitchOptions(source="sdk_rpc", total_timeout=30.0) result = switcher.switch_to("BIPED_WHOLE_BODY_CTRL", options) if not result.success: self.get_logger().error( "Failed to switch from " f"{result.current_action} to BIPED_WHOLE_BODY_CTRL: {result.message}") return result.successswitch_to失败就直接返回,后面连请求都不会发.这是上一篇讲的动作状态机在这条链路上的位置,预设动作是状态机的下游,不先把身体切进全身控制态,动作请求没有落点.
BIPED_WHOLE_BODY_CTRL这个名字本身也说明了问题:它控制的是全身,而不是"手臂".
五,16 个 Q 系列动作逐个请求
把 Q 系列的 16 个值全部发一遍,结果一致:
三列数字各有含义:
code=0是响应头里的成功码,来自ResponseHeader.codestate=400是CommonState.RUNNING,表示任务正在跑task_id从 1001 开始递增,每个请求一个新的
这里有一个必须点出来的差异.SetMcPresetMotion和SetMcAction的响应都是CommonTaskResponse,形态一样,但官方客户端判断成败的条件完全不同:
服务 | 客户端判定条件 | 回 RUNNING 会怎样 |
|
| 判失败,且不重试 |
|
| 受理成功 |
set_preset_motion.py里写的是if res and res.response.header.code == 0:,根本没看state.所以替身服务端回400是安全的.而上一篇里SetMcAction必须回SUCCESS,回400会被切换器判失败.同一个响应形态,两套判定规则,这是写通用封装时最容易出错的地方.
六,部位掩码:官方示例从没赋过值
SetMcPresetMotion的请求里有六个字段:
request.header = RequestHeader() request.source = 'sdk_rpc' request.area = McControlArea() # 部位掩码 request.motion = McPresetMotion() # 动作枚举 request.interrupt = True request.ani_path = ani_path # 自定义动作文件McControlArea是位掩码,五个值:NONE=0,LEFT_HAND=1,RIGHT_HAND=2,HEAD=4,WAIST=8.名字说明设计意图,预设动作可以只驱动身体的一部分.
但官方示例从头到尾没有给area赋过值.request.area = McControlArea()之后就没再动过,等于永远是NONE.
把八种组合都发一遍:
替身服务端能确认的只有一件事:这个字段传得到,位组合解析正确.至于area=NONE到底是"全身"还是"不控制任何部位",area=3是不是"双手同时",srv 定义里没写,只能等真机.这一点本文不做推定.
七,三种寻址:整数,资源名,文件路径
动作从哪来,SDK 给了三种完全不同的寻址方式.
第一种是整数枚举,也就是前面讲的McPresetMotion.动作固化在机器人本地,客户端只需传一个数字.
第二种是资源名加模型路径,走SetMcMotion:
req.source = "sdk_rpc" req.type = 1 # MIMIC_QY=1 自有模型, MIMIC_LC=2 灵创模型 req.motion = "INTRO_POSE6" # 动作资源名,不是数字 req.model = "/path/to/model.onnx" req.resource = "/path/to/trajectory" req.operation = 1 # PRELOAD=1 预先加载, EXECUTION=2 开始执行注意motion在这里是字符串,官方 set_mc_action.py 的示例参数写的就是INTRO_POSE6.operation是两段式的:先PRELOAD=1把模型加载进内存,再EXECUTION=2触发执行.
第三种是文件路径,还是SetMcPresetMotion,但走另一个分支:把motion.value设成 0,用ani_path指向一个 CSV 文件.示例的交互提示把这个路径的规则写出来了:
/robot/userdata/sd/custom_motions/<用户自定义名称>_<时间戳>_<编号>.csv这张图的重点在最后两行:ani_path为空,motion也为 0,服务端照样回code=0.也就是说这条通路没有必填校验,传个空值进去也会被受理,然后什么都不发生.这类"静默受理"是最难排查的一类问题,写的时候必须自己判空.
三种寻址对应三种动作来源:内置动作库,外部推理模型,示教录制文件.选哪种取决于动作是现成的,训练出来的,还是手把手教出来的.
八,左右不对称:写动作必踩的镜像坑
这一节跟 3D 模型有关,也是这篇最有实操价值的部分.
把 Q1 的 22 个可动关节从 URDF 里读出来,会发现左右两条手臂的shoulder_roll限位是镜像的:
关节 | 下限 | 上限 |
| -5° | 160° |
| -160° | 5° |
左臂能往正方向转 160 度,右臂能往负方向转 160 度.这是镜像对称的正常结果,但落到代码上就是:做一个双臂对称的动作,两边的值必须异号.
这张图是逐个关节单独驱动的结果,几何与材质全部来自官方 URDF 与 STL.用它可以直接读出四个方向:
shoulder_pitch取负值,手臂向前抬shoulder_roll左边取正,右边取负,手臂向外展shoulder_yaw取正值,手臂向前摆elbow取正值,肘部弯曲
还有一个细节容易被忽略:left_wrist_yaw_joint和right_wrist_yaw_joint的type是fixed,但 URDF 里照样写了-90° 到 90°的限位.限位声明了却不会生效,这两个关节永远不会动,所以手腕无法旋转.做"挥手"这类需要手腕摆动的动作时,只能靠肩和肘去凑.
另外一处数据矛盾值得一提.官方joint_control.py的示例参数里,left_elbow_joint填的是2.2弧度,而 URDF 声明的上限是1.6581弧度(95 度),超出约 31 度.这可能是示例参数早于当前 URDF,也可能是建模取了保守值.真机上会怎样本文没有验证,但提醒一件事:从示例里抄关节角之前,先用 URDF 的 limit 校一遍.
九,按 Q 系列语义渲染的四个动作
既然限位摸清了,就能用官方模型把 Q 系列的动作画出来.下面四个动画由render/urdf_action.py生成,几何,材质,装配关系全部来自q1_urdf,关节角按动作语义构造,每一帧都先过 URDF 限位再渲染,越界会被 clamp 并打印.
四张图都要强调同一件事:这些是示意姿态,不是官方动作轨迹.真机上Q_BECKON具体怎么动,动多久,幅度多大,SDK 里没有任何地方写,本文也不猜.动画能证明的只有一条,按这套关节与限位,这些姿态在运动学上是成立的.
十,两个零示例的接口
18 个官方示例里,有两个服务从头到尾没被调用过:SetServo和SetNeckLight.
SetServo的定义是这样的:
CommonRequest request uint8 position # 0-90 度,0 度为四足形态的位置,90 度为双足形态的位置 uint16 speed # 100-1000,每秒运动的步数,1 步为 0.225 度 CommonResponse header注释写得很明确:这个舵机负责双足与四足两种形态之间的切换,0 度是四足位置,90 度是双足位置.全行程 90 度,按 1 步 0.225 度算正好 400 步.
把边界值都发一遍:
这张图里有两列值得看.steps是按定义换算出的步数,400 步对应全行程;eta是步数除以 speed 得到的理论耗时,speed 取最小值 100 时要 4 秒,取最大值 1000 时只要 0.4 秒.
position是uint8,字段本身能装到 255,但语义只到 90.91 和 120 都会被拒,说明校验在服务端而不是字段类型层.
SetNeckLight更简单,两个字段:bool enable和uint8 brightness(0-100):
顺带说一句SetServo的一个背景事实:这个舵机不在 URDF 的 25 个关节里.URDF 里是 22 个 revolute 加 3 个 fixed,全部属于腿,腰,臂,头,没有任何一个关节叫 servo 或者与形态转换相关.也就是说双足四足互转是关节树之外的一套机构,3D 模型表达不了它,这也是本文没做形态转换动画的原因.
十一,小结
三句话收尾.
第一,动作类接口的"枚举"和"动作名"不能混用.预设动作用整数(3001 这种),全身动作用字符串资源名(INTRO_POSE6这种),自定义动作用文件路径.三套寻址混在同一个motion字段名下,但类型和来源完全不同.
第二,同一个CommonTaskResponse形态下有两套成败判定.SetMcAction的客户端看state==SUCCESS,SetMcPresetMotion的客户端只看header.code.写通用封装时不能按响应形态套模板.
第三,官方示例里的速查表不可信.T 系列的 4 条全部错位,Q 系列的 3001 语义也有偏差.唯一可信的是ros2 interface show的输出.
未验证的部分集中在这里:动作是否真的执行,姿态是否与名称相符,area掩码的真实作用,ani_path的 CSV 格式,形态舵机的机械行程.这些全部需要真机,本文一律留白.
配套仓库包含替身服务端lab/mock_hal.py,动作渲染脚本render/urdf_action.py,取证脚本lab/probe_actions.py,以及本文全部截图与渲染产物:
https://atomgit.com/2301_76341691/primebot-sdk-lab
启元机器人开发者社区https://dev.primebot.com/