的完整实现)
启元 Q1 场景化技能包开发实战基于 JSON 配置驱动的模板渲染方案与 .rpk 打包流程——三套情绪助手技能鼓励/早安/倒计时的完整实现本文记录基于启元 Q1 机器人 SDK v1.0.0.0通过 JSON 配置驱动 Python 模板渲染的方式从零构建三套场景化情绪助手技能包鼓励模式、早安模式、倒计时模式的完整过程——涵盖 .rpk 技能包结构设计、皮肤配置 JSON 定义、main.py 模板渲染与字面量注入、build.py 自动化打包、PrimeConsole 1.0.2 静态扫描机制适配、仿真验证流程以及与前期人格皮肤项目的架构复用与差异化设计。项目仓库https://atomgit.com/weixin_62765017/emotion_assistant一、技术架构分析1.1 整体架构本项目采用 JSON 配置层 Python 模板渲染层 .rpk 打包层的三层分离架构JSON 配置层每套技能对应一份 skin.json定义台词、动作序列、表情、灯效、包名五个维度与代码完全解耦模板渲染层build.py 读取 JSON 配置将参数注入 main.py.tpl 模板生成可直接运行的 Python 入口代码打包层将渲染后的 cfg/pkg.yaml main.py README.md skin.json 按 ZIP 格式封装为 .rpk 技能包这种架构的核心优势在于新增一套技能只需编写一份 25 行的 JSON 配置无需修改任何 Python 代码或模板文件。三套技能共用同一套代码框架差异化完全由配置驱动。1.2 技术方案对比对比项人格皮肤前期项目情绪助手本项目设计定位身份展示“机器人是谁”场景回应“何时回应你”配置方式JSON 配置 模板渲染同架构复用台词风格陈述型自我介绍对话型对用户说话灯效设计三种模式区分身份三种节奏营造氛围动作编排展示型招手→挥手叙事型加油→比心 鼓励技能数量3 套日常/生日/节日3 套鼓励/早安/倒计时单包大小3075~3112 B3092~3143 B1.3 核心功能清单功能模块具体能力配置驱动一份 JSON 定义台词/动作/表情/灯效/包名五个维度模板渲染build.py 将配置注入 main.py.tpl生成字面量代码自动打包一次构建生成三个 .rpk保持根目录结构静态扫描适配动作编号以字面量写入 play_sequence()供 PrimeConsole 正则匹配四项能力声明PlayTts / SetMcPresetMotion / PlayEmotion / LedStripCommand仿真验证PrimeConsole 1.0.2 导入解析 动作轨迹仿真二、环境准备2.1 开发环境要求项目版本/信息操作系统Windows 10/11核心框架Python 3.10构建脚本目标平台PrimeConsole 1.0.2目标机型启元 Q1robot_model: q1SDK 版本Q1 SDK v1.0.0.0打包格式.rpkZIP 压缩包扩展名替换运行依赖ROS2 Humble真机执行时需要2.2 项目结构emotion_assistant/ ├── skins/# 技能配置目录│ ├── encourage.json# 鼓励模式配置│ ├── morning.json# 早安模式配置│ └── countdown.json# 倒计时模式配置├── template/ │ └── main.py.tpl# Python 入口代码模板├── dist/# 构建产物目录│ ├── q1_emotion_encourage.rpk# 鼓励模式技能包3092 B│ ├── q1_emotion_morning.rpk# 早安模式技能包3143 B│ └── q1_emotion_countdown.rpk# 倒计时模式技能包3122 B└── build.py# 构建脚本配置读取 模板渲染 打包2.3 依赖说明本项目构建阶段零第三方依赖仅使用 Python 标准库json / zipfile / tempfile / pathlib。运行时依赖 Q1 SDK 的四个 ROS2 服务接口接口类型用途/aimdk_5Fmsgs/srv/PlayTtsService语音合成TTS/aimdk_5Fmsgs/srv/SetMcPresetMotionService预设动作播放/aimdk_5Fmsgs/srv/PlayEmotionService屏幕表情切换/aimdk_5Fmsgs/srv/LedStripCommandServiceLED 灯带控制三、核心开发流程3.1 技能配置设计skin.json每套技能的核心定义收敛为一份 JSON 文件包含五个维度。以下为鼓励模式的完整配置与项目文件 skins/encourage.json 一致{id:encourage,display_name:鼓励模式,package_name:q1_emotion_encourage,description:情绪助手·鼓励模式加油、比心暖粉呼吸灯温柔陪伴。,lines:[没关系你已经很棒了。,加油我相信你],motions:[3008,3007],emotion_id:3003,led:{mode:254,rgb:[255,100,150],period:2500}}字段说明字段类型作用对应接口idstring技能唯一标识—display_namestring显示名称—package_namestring技能包名需与 .rpk 文件名、main.py 节点名一致—descriptionstring技能描述cfg/pkg.yamllinesstring[]台词池运行时随机抽取两句PlayTtsmotionsint[]动作序列预设动作编号必须为上肢动作SetMcPresetMotionemotion_idint屏幕表情编号PlayEmotionled.modeint灯带模式编号LedStripCommandled.rgbint[3]灯带颜色R/G/B 各 0~255LedStripCommandled.periodint灯效周期毫秒LedStripCommand三套技能的配置差异技能动作序列台词表情灯效 RGB灯效周期鼓励模式3008(加油) → 3007(比心)“没关系你已经很棒了。” / “加油我相信你”3003(开心)(255,100,150) 暖粉2500ms 慢呼吸早安模式3001(招手) → 3046(舞蹈1)“早上好新的一天元气满满” / “起床啦今天也要开心哦”3003(开心)(255,180,50) 元气橙1500ms 中速呼吸倒计时模式3004(挥手) → 3003(碰拳) → 3008(加油)“三、二、一开始” / “准备好了吗冲”3002(耍酷)(0,180,255) 冰蓝300ms 快闪灯效设计逻辑鼓励模式采用暖粉色 2.5 秒慢呼吸周期营造温柔陪伴感早安模式采用元气橙 1.5 秒中速呼吸提供唤醒节奏感倒计时模式采用冰蓝色 300 毫秒快闪制造紧张期待感。三套技能通过 RGB 颜色与呼吸周期的组合实现氛围差异化。3.2 模板代码实现main.py.tplmain.py.tpl 是三套技能共用的入口代码模板包含 10 个占位符SKIN_NAME、PACKAGE_NAME、LINES等由 build.py 在构建时替换为实际配置值。以下为模板中与项目文件 template/main.py.tpl 一致的关键片段节点初始化与四项能力客户端classSkinNode(Node):def__init__(self)-None:super().__init__(__PACKAGE_NAME__)self.tts_clientself.create_client(PlayTts,/aimdk_5Fmsgs/srv/PlayTts)self.motion_clientself.create_client(SetMcPresetMotion,/aimdk_5Fmsgs/srv/SetMcPresetMotion)self.emotion_clientself.create_client(PlayEmotion,/aimdk_5Fmsgs/srv/PlayEmotion)self.led_clientself.create_client(LedStripCommand,/aimdk_5Fmsgs/srv/LedStripCommand)执行顺序固定为灯效 → 表情 → 台词 → 动作序列。defmain()-int:globalNODE rclpy.init()NODESkinNode()try:set_led(LED_MODE,LED_RGB,LED_PERIOD)set_emotion(EMOTION_ID)forlineinrandom.sample(LINES,kmin(2,len(LINES))):speak(line)play_sequence()finally:NODE.destroy_node()rclpy.shutdown()return0动作序列的字面量注入关键设计以鼓励模式为例build.py 渲染后 play_sequence() 的内容为defplay_sequence()-None:set_preset_motion(3008)set_preset_motion(3007)set_preset_motion() 的参数必须为整数字面量不能为变量或表达式。原因见 3.5 节 PrimeConsole 静态扫描机制说明。3.3 构建脚本实现build.pybuild.py 负责读取 skins/ 目录下的 JSON 配置、渲染模板、按 .rpk 结构打包。以下代码与项目文件 build.py 一致。模板渲染函数defrender_main_py(skin:dict)-str:将皮肤配置渲染到 main.py 模板中。motions\n.join(f set_preset_motion({m})forminskin[motions])linesjson.dumps(skin[lines],ensure_asciiFalse)rgbtuple(skin[led][rgb])textTEMPLATE.read_text(encodingutf-8)forkey,valuein{__SKIN_NAME__:skin[display_name],__SKIN_ID__:skin[id],__PACKAGE_NAME__:skin[package_name],__DESCRIPTION__:skin[description],__LINES__:lines,__EMOTION_ID__:str(skin[emotion_id]),__LED_MODE__:str(skin[led][mode]),__LED_RGB__:repr(rgb),__LED_PERIOD__:str(skin[led][period]),__MOTION_CALLS__:motions,}.items():texttext.replace(key,value)returntext打包函数defbuild_rpk(skin:dict,out_path:Path)-None:构建单个 .rpk 包。withtempfile.TemporaryDirectory()astmp:workPath(tmp)# cfg/pkg.yamlcfg_dirwork/cfgcfg_dir.mkdir()(cfg_dir/pkg.yaml).write_text(render_pkg_yaml(skin),encodingutf-8)# main.py(work/main.py).write_text(render_main_py(skin),encodingutf-8)# README.md(work/README.md).write_text(render_readme(skin),encodingutf-8)# skin.json原始配置一并打包(work/skin.json).write_text(json.dumps(skin,ensure_asciiFalse,indent2),encodingutf-8,)# 打包为 .rpkZIPwithzipfile.ZipFile(out_path,w,zipfile.ZIP_DEFLATED)aszf:forpathinsorted(work.rglob(*)):ifpath.is_file():zf.write(path,path.relative_to(work).as_posix())构建输出$ python build.py 找到 3 份皮肤配置 皮肤 包名 大小 ------------------------------------------------------ 倒计时模式 q1_emotion_countdown.rpk 3122 B 鼓励模式 q1_emotion_encourage.rpk 3092 B 早安模式 q1_emotion_morning.rpk 3143 B 已生成 3 个 .rpk 到 dist/ 目录3.4 .rpk 包结构设计.rpk 本质为 ZIP 压缩包仅替换扩展名。PrimeConsole 1.0.2 实际读取的文件结构如下q1_emotion_encourage.rpk (ZIP) ├── cfg/ │ └── pkg.yaml # 技能元数据包名/版本/机型/描述 ├── main.py # 入口代码含字面量动作编号 ├── README.md # 技能说明文档 └── skin.json # 原始配置文件便于后续修改pkg.yaml 格式要求以鼓励模式为例user_id:robot_localversion:1.0.0sdk_version:v1.0.0package_name:q1_emotion_encourageauthor:Halcyonrobot_model:q1description:情绪助手·鼓励模式加油、比心暖粉呼吸灯温柔陪伴。关键约束cfg/pkg.yaml 必须位于压缩包根目录。若多套一层文件夹PrimeConsole 无法读取元数据包名显示 unknownrobot_model 字段必须填写否则机型无法识别package_name 必须与 main.py 中 SkinNode 的节点名一致3.5 PrimeConsole 静态扫描机制适配PrimeConsole 1.0.2 的技能解析流程为打开 .rpkZIP → 读取 cfg/pkg.yaml获取包名/版本/机型 → 拼接所有 .py 文件内容 → 正则匹配 import 语句识别四项能力声明 → 正则提取 set_preset_motion(\d)获取动作编号 → 匹配本地预设动作索引preset-actions-q1/index.json → 仿真阶段播放对应 CSV 轨迹核心约束动作编号必须以整数字面量出现在 set_preset_motion() 调用中。以下写法均会导致匹配失败# × 变量引用 —— 正则无法提取motion_id3008set_preset_motion(motion_id)# × 字符串拼接 —— 正则无法提取set_preset_motion(int(3008))# × 配置读取 —— 正则无法提取forminskin[motions]:set_preset_motion(m)# √ 字面量写入 —— 正则匹配 set_preset_motion(3008)set_preset_motion(3008)set_preset_motion(3007)这一约束决定了模板渲染方案build.py 必须在构建阶段将 JSON 配置中的动作编号直接写入 Python 源码的字面量位置而非运行时读取。四、仿真验证4.1 导入与解析在 PrimeConsole 1.0.2 中依次导入三个 .rpk 文件解析结果如下技能包名识别版本机型能力声明动作匹配鼓励模式q1_emotion_encourage1.0.0q14/4加油 2.8s 比心 5s早安模式q1_emotion_morning1.0.0q14/4招手 4s 舞蹈1 5.2s倒计时模式q1_emotion_countdown1.0.0q14/4挥手 3.5s 碰拳 5s 加油 2.8s四项能力声明均被正确识别语音合成PlayTts、预设动作SetMcPresetMotion、表情播放PlayEmotion、LED 灯效LedStripCommand。4.2 仿真执行PrimeConsole 仿真仅验证动作层轨迹与时序不执行 Python 代码。仿真结果技能进度条格数动作序列总时长鼓励模式2 格加油 → 双手比心7.8s早安模式2 格招手 → 上半身舞蹈19.2s倒计时模式3 格挥手 → 碰拳 → 加油11.3s仿真与真机的分工验证对象PrimeConsole 仿真真机技能包结构与元数据支持支持能力声明与动作匹配支持支持动作轨迹与时序支持支持TTS 台词发声不支持支持表情切换不支持支持LED 灯效不支持支持代码运行时错误不支持支持仿真负责动作层验证真机负责效果层验证二者为接力关系而非替代关系。五、构建与部署5.1 本地构建cdemotion_assistant python build.py构建产物位于 dist/ 目录三个 .rpk 文件总大小约 9.3 KB。5.2 PrimeConsole 导入打开 PrimeConsole → Skill 验证页点击「导入 .rpk」→ 选择 dist/ 目录下的目标文件等待「已解析」提示 → 检查能力列表与动作匹配点击「开始模拟仿真」→ 观察动作执行进度5.3 真机执行source/opt/ros/humble/setup.bashsource/path/to/sdk/install/setup.bash python3 main.py真机执行时可观察完整的四项效果灯效颜色与节奏、屏幕表情切换、TTS 语音播放、上肢动作轨迹。5.4 发布到 PrimeHub字段值版本号1.0.0机器人类型Q1分类动作标签情绪助手、鼓励、早安、倒计时、预设动作、二次开发六、常见问题与解决方案Q1导入 .rpk 后包名显示 unknown问题现象在 PrimeConsole 中导入 .rpk 文件后技能列表中包名显示为 unknown版本号和机型均为空。根因分析cfg/pkg.yaml 未位于压缩包根目录。常见原因是打包时将所有文件放入了一个外层文件夹如 my_skill/cfg/pkg.yaml导致 PrimeConsole 在根路径查找 cfg/pkg.yaml 时失败。解决确保 ZIP 包内第一级目录即为 cfg/而非嵌套的外层文件夹。build.py 中的打包逻辑已通过 path.relative_to(work) 保证条目在根目录withzipfile.ZipFile(out_path,w,zipfile.ZIP_DEFLATED)aszf:forpathinsorted(work.rglob(*)):ifpath.is_file():zf.write(path,path.relative_to(work).as_posix())Q2仿真中动作全部未匹配问题现象导入 .rpk 后能力声明识别正确但仿真时动作进度条为空提示动作未匹配。根因分析main.py 中 set_preset_motion() 的参数使用了变量而非字面量。PrimeConsole 通过正则 set_preset_motion((\d)) 提取动作编号变量引用无法被正则匹配。解决确保所有动作编号以整数字面量直接写入源码。本项目通过 build.py 模板渲染自动完成字面量注入# build.py 渲染逻辑motions\n.join(f set_preset_motion({m})forminskin[motions])渲染结果以鼓励模式为例defplay_sequence()-None:set_preset_motion(3008)set_preset_motion(3007)Q3提示只能仿真上肢动作问题现象PrimeConsole 仿真时弹出警告提示仅支持上肢动作仿真部分动作无法播放。根因分析Q1 在 PrimeConsole 1.0.2 中仅支持上肢预设动作的仿真。若 motions 中包含下肢或全身移动动作编号仿真无法匹配。解决确保 JSON 配置中的 motions 数组仅包含上肢预设动作编号。当前已验证的上肢动作编号清单编号动作名称时长3001招手~4s3003碰拳~5s3004挥手~3.5s3007双手比心~5s3008加油~2.8s3046上半身舞蹈1~5.2s3047上半身舞蹈2~5sQ4新增技能后 build.py 未生成对应 .rpk问题现象在 skins/ 目录下新增了一份 JSON 配置文件运行 build.py 后 dist/ 目录中未出现对应的 .rpk 文件。根因分析JSON 文件缺少必要字段。build.py 通过 SKINS_DIR.glob(“*.json”) 扫描所有 JSON 文件并要求每份配置包含 id、display_name、package_name、description、lines、motions、emotion_id、led 八个必要字段。解决检查新增 JSON 文件是否位于 skins/ 目录下且包含全部必要字段。可参照已有的 encourage.json 格式{id:新技能标识,display_name:显示名称,package_name:q1_emotion_xxx,description:技能描述,lines:[台词1,台词2],motions:[3001,3004],emotion_id:3003,led:{mode:254,rgb:[255,220,160],period:2000}}七、总结本文完整记录了基于启元 Q1 机器人 SDK 开发三套情绪助手技能包的过程。核心特性与技术方案如下特性实现方案配置驱动JSON 五维度定义台词/动作/表情/灯效/包名新增技能零代码修改模板渲染build.py 读取配置 → 注入 main.py.tpl → 生成字面量动作调用自动打包ZIP 封装为 .rpkcfg/pkg.yaml 保持根目录结构静态扫描适配set_preset_motion(数字) 字面量写入正则匹配可靠三套技能鼓励模式2 动作/ 早安模式2 动作/ 倒计时模式3 动作灯效差异化暖粉慢呼吸 / 元气橙中速呼吸 / 冰蓝快闪氛围区分明确单包大小3092~3143 B总计约 9.3 KB开发过程中最关键的三个设计决策字面量注入而非运行时读取——PrimeConsole 的静态扫描机制决定了动作编号必须写死在源码中这一约束直接影响了模板渲染方案的设计JSON 与代码分离——五维度配置收敛为一份 25 行 JSON代码框架完全复用新增技能的边际成本降至最低仿真与真机分工——PrimeConsole 仿真聚焦动作轨迹验证TTS/表情/灯效留给真机二者为接力关系而非替代关系与前期人格皮肤项目相比本项目复用了完全相同的技术架构JSON 模板 .rpk差异仅在设计出发点——从身份展示转向场景回应。这验证了配置驱动方案的可扩展性当新增技能的成本从编写 150 行 Python降至修改 5 个 JSON 字段时决定产出数量的不再是技术能力而是场景想象力。