RMCS(RoboMaster Control System)的 RL 策略桥:在「传统 RMCS 结构」与「独立进程里的 ONNX 策略」之间做翻译。
桥只做一件事:按 YAML 词条把 RMCS 侧接口拼成有序 obs,发出去;把收到的 action 逐项写回 RMCS 侧的 RL 专属接口。
推理在另一个进程(policy_server),PD / FSM / 关节语义在 RMCS 侧消费组件(P1,见「现状与边界」)。
详细文档:
| 边界 | 通道 | 数量 |
|---|---|---|
| 桥 ↔ RMCS 侧(executor 内的组件) | 只有 register_input / register_output 接口,没有任何 topic |
N 个观测输入 + M 个动作输出 + 使能/复位/事实位 |
| 桥 ↔ 策略进程(不是 RMCS 组件,独立可执行文件) | ROS topic | 2 条必需(<rl_base>/obs、<rl_base>/action)+ 1 条可选运维(<rl_base>/policy_status,缺省关) |
topic 存在的唯一原因是「策略进程独立」。想消除这 3 条 topic,只能把 ONNX 放回控制进程(进程内推理)—— 那正是桥要避免的事(ORT 会跟着 1 kHz 控制进程一起加载/崩溃/重启)。
| 名称 | 位置 | 依赖 ONNX Runtime | 职责 |
|---|---|---|---|
rmcs::rl::RlBridge |
库 rmcs_rl_bridge |
否 | 观测组装、定频发布、动作回写、valid/healthy/action_age 事实位、合同指纹 |
policy_server |
可执行文件(ros2 run rmcs_rl policy_server) |
是(唯一链接 ORT 的可执行文件) | 收一帧 obs → 归一化 → ONNX → 回一帧 action(纯反应式,无定时器) |
rmcs::rl::RlController |
库 rmcs_rl_legacy |
是 | 遗留控制器(旧配置格式,P1 切换完成后删除) |
消息 rmcs_rl/msg/* |
本包 msg/(rosidl 生成,类型全名如 rmcs_rl/msg/Observation) |
否 | Observation / Action / PolicyStatus |
⚠️ RlController是遗留路径:它自带 ONNX 推理、PD、FSM,配置键(rl_inference_frequency、position_pd_joints、action_terms: joint=... mode=... kp=...)与本文档描述的桥式配置完全不同, 不要混用、也不要与RlBridge同时挂载。桥式链路取代它之后即删除。
RMCS 侧 output 接口 ──(桥:按词条拼 obs)──> obs 向量 ──topic <rl>/obs──> policy_server ──ONNX──> action
▲ │
└─ RMCS 侧 consumer(P1:FSM/PD/限位,写电机 control_*)<──(桥:逐项回写)── action 向量 <─┘
本包不含台架夹具:链路必须挂到真实 RMCS 组件上跑。简要步骤(完整流程见 doc/deployment.md):
- 改
config/executor.yaml:观测/动作词条与joint_*指向真机的接口路径,policy_server.rl_model_path指向已盖章的模型; - 把该文件复制到
rmcs_bringup/config/<robot>.yaml,或真机上--params-file直接加载; - 真机起两个进程:
ros2 run rmcs_executor rmcs_executor --ros-args --params-file <deploy.yaml>
ros2 run rmcs_rl policy_server --ros-args --params-file <deploy.yaml>layout_hash 两侧必须一致;valid=0 的原因会直接打在桥的日志里(见
doc/deployment.md 的排障表)。注意 P1 的 RMCS 侧 consumer 尚未实现,
真机上桥恒 valid=0、不会输出权威动作(见「现状与边界」)。
- 在部署 YAML 的
rl_bridge.ros__parameters里写observation_terms(顺序就是 obs 索引顺序) 与action_terms(index=必须恰好是0..M-1的完整置换)。 - 只写路径 + 取值语义,不要写
type=:接口的 C++ 类型由桥在before_pairing里按output_map的typeid自省(take=x/take=vec3/transform=projected_gravity决定取值语义与 dim)。type=是逃生口,只在自省失败(提示「无 producer 或类型不在候选表」)时才显式写。 - 词条引用的接口必须在启动时已存在(配对只在启动做一次,运行期热加接口不支持);
确实可能缺失的可选接口用
default=兜底(仅标量路径词条)。 - 尺寸自检:
Σdim(observation_terms)与rl_obs_size、action_terms条数与rl_action_size写了就必须一致。 - 改完重新盖章 + 重新校验,然后重启 executor(新接口/新词条都要重新配对):
python3 src/rmcs_rl/tool/stamp_layout_metadata.py --model <model.onnx> --from-config <deploy.yaml> --node rl_bridge
python3 src/rmcs_rl/tool/check_policy_contract.py <model.onnx> --config <deploy.yaml> --node rl_bridge词条增删或维度变化等于换了策略输入维度 → 必须重新导出模型(盖章只改 metadata,不改张量 shape)。 仅「维度不变」的改名/换实现(例如 v1 的类型前缀 id → v2 去类型化 id)才只需要重盖章,不需要重训。
# 不需要 executor:词条语法自检 + gen/stamp/check 正例 + 6 个负例
bash src/rmcs_rl/tool/test_layout_contract.sh实测 PASS=21 FAIL=0。CI 里也会跑这一项,外加对 models/*.onnx 逐个做模型自检。
| 工具 | 用途 |
|---|---|
tool/rl_layout.py |
词条语法 / 规范串 / layout_hash 的单一真源(python3 tool/rl_layout.py 跑自检) |
tool/stamp_layout_metadata.py |
部署 YAML → 写进 ONNX metadata_props(rmcs_obs_layout / rmcs_actions_layout / policy_layout_hash …) |
tool/check_policy_contract.py |
两种模式:MODEL(模型自检:张量契约 + metadata 自洽,不比对 YAML,CI 用)/ MODEL --config X [--node N] [--print-layout] [--expect-model-id H](YAML ↔ 张量 ↔ metadata 完整校验) |
tool/gen_synthetic_policy.py |
--from-config X -o M.onnx:生成零动作合成模型(obs[1,N] → actions[1,M];ONNX 后端强制 ir_version=10,因为 ORT 1.20 拒收 IR 13) |
tool/test_layout_contract.sh |
Python 侧布局契约回归(含 6 个必须 FAIL 的负例) |
tool/install_rl_deps.sh |
bash tool/install_rl_deps.sh local|remote:装 libonnxruntime.so.1(仅 policy_server 需要) |
- P0 已完成:桥、策略进程、消息定义、合同指纹(v2)、工具链。
- P1 未实现:RMCS 侧 consumer(FSM / PREPARE / kp,kd / 限位 / NaN 让位)与
RlController→ 桥的切换。 目前没有组件写/wheel_leg/rl/enable,也没有组件消费/wheel_leg/rl/action/*,因此实车上enable_default: false→ 桥恒valid=0,不会输出权威动作(这是刻意的:没人宣告权威就不许输出)。 - 桥从不写电机
control_*接口:executor 禁止同名 output,电机控制权始终在 RMCS 侧消费组件手上, 所以「RL 接管 / 退回传统控制」不需要仲裁组件(P1 的 consumer 用写 NaN 让位)。 RlController(库rmcs_rl_legacy)仍然是唯一的整机可用路径,P1 完成后删除; 它带 ORT 进控制进程,配置格式与桥式完全不同,不要混配。- 观测接口运行期热加不支持(配对只在启动做一次);
contract_ok一旦因合同不符锁存为 false, 当前实现只能靠重启 executor 进程恢复。
rmcs_rl/
├── README.md # 本页(入口)
├── config/
│ └── executor.yaml # 实车配置模板(轮腿;复制到 rmcs_bringup/config/<robot>.yaml)
├── doc/
│ ├── architecture.md # 进程/组件、数据流、接口清单、valid 状态机
│ ├── bridge-design.md # 重构定稿方案(权威设计)
│ ├── deployment.md # 构建 → 模型 → 配置 → 运行 → 验证 → 交接
│ └── model-contract.md # 模型张量合同与 metadata 清单
├── models/ # 策略 ONNX(安装到 share/rmcs_rl/models/)
├── msg/ # Observation / Action / PolicyStatus(rosidl 生成,类型名 rmcs_rl/msg/*)
├── src/
│ ├── rl_bridge.cpp # 桥
│ ├── rl_layout.hpp # FNV-1a64 / layout_hash / model_id(与 tool/rl_layout.py 同构)
│ ├── policy_server.cpp # 策略进程
│ ├── onnxruntime_inference.hpp
│ └── rl_controller.cpp # 遗留控制器(P1 删除)
├── tool/ # 见上表
├── plugins.xml # rmcs_rl_bridge / rmcs_rl_legacy 的 pluginlib 导出
└── CMakeLists.txt
- 集成示例:
rmcs_bringup/config/wheel-leg-infantry-rl.yaml(当前是遗留RlController配置) - 训练侧:任何能导出
obs[1,N] → actions[1,M]且带rmcs_obs_layout/rmcs_actions_layoutmetadata 的 ONNX 仓库(Isaac Lab / legged_gym / rsl_rl …)都可以接