Skip to content

Hands On Sim2Real

konodoki edited this page Jul 2, 2026 · 11 revisions

手把手 8:IsaacLab 模型从仿真到真机

这篇补上完整的 sim2real 流程:先把新的 IsaacLab .onnx 推理器接进 BxiExample,再写一个状态使用这个推理器,先在 MuJoCo 仿真里验证,最后再上 ELF3 真机。

本文用下面名字做例子:

my_walk.onnx
self.my_walk
MyWalkState
my_walk
my_walk_event

实际接模型时,把 my_walk 换成你的模型名。

0. 先明确边界

第一次接新模型时,不要直接改 initial_state 到新状态,更不要直接上真机。推荐顺序是:

放模型文件
  -> BxiExample.load_models() 加推理器
  -> robot_states.py 加状态
  -> elf3_state_machine.yaml 加事件和状态
  -> remote_controller/config/xbox_default.yaml 加按键
  -> 仿真验证
  -> 真机低风险验证

会修改的文件:

src/bxi_example_py_elf3/bxi_example_py_elf3/bxi_example_demo.py
src/bxi_example_py_elf3/bxi_example_py_elf3/robot_states.py
src/bxi_example_py_elf3/config/elf3_state_machine.yaml
src/remote_controller/config/xbox_default.yaml

会新增或替换模型文件:

src/bxi_example_py_elf3/data/isaaclab_model/my_walk.onnx

如果模型是带参考动作的 motion policy,还会有:

src/bxi_example_py_elf3/data/isaaclab_model/my_motion.npz
src/bxi_example_py_elf3/data/isaaclab_model/my_motion.onnx

1. 放入模型文件

把 IsaacLab 导出的 ONNX 放到:

src/bxi_example_py_elf3/data/isaaclab_model/my_walk.onnx

当前 package 会把 data/ 下的文件安装到:

install/bxi_example_py_elf3/share/bxi_example_py_elf3/data/

所以新增模型文件后要重新 build:

colcon build --packages-select bxi_example_py_elf3
source install/setup.bash

热重载只会监控已经安装后的模型路径。第一次新增文件时,仍然需要 build 一次。

2. 在 BxiExample.load_models() 加推理器

打开:

src/bxi_example_py_elf3/bxi_example_py_elf3/bxi_example_demo.py

找到 load_models(),现在模型是在这里直接声明的。参考现有的 withoutarm

self.withoutarm: HumanoidGaitPolicyLiteIsaaclab = HumanoidGaitPolicyLiteIsaaclab(
    model_file("isaaclab_model/withoutarm.onnx")
)

添加你的推理器:

self.my_walk: HumanoidGaitPolicyLiteIsaaclab = HumanoidGaitPolicyLiteIsaaclab(
    model_file("isaaclab_model/my_walk.onnx")
)

这里有三个要点:

  • model_file("isaaclab_model/my_walk.onnx") 是相对 data/ 的路径。
  • self.my_walk 后面会作为 policy_attr 被状态类访问。
  • 只改这里,不需要在 launch 里新增 onnx_file_dict;当前工程已经不走旧的 launch 模型字典。

如果你的模型需要 .npz + .onnx,参考 back_flipballet

self.my_motion: DanceMotionPolicyGravityIsaaclab = DanceMotionPolicyGravityIsaaclab(
    model_file("isaaclab_model/my_motion.npz"),
    model_file("isaaclab_model/my_motion.onnx"),
    start_frame=0,
)

3. 在 robot_states.py 加状态

打开:

src/bxi_example_py_elf3/bxi_example_py_elf3/robot_states.py

如果你的模型和 withoutarmnormalamp_run 一样是速度控制 gait policy,可以先按下面这个状态接入:

class MyWalkState(RobotControlState):
    def __init__(self, name: str, state_id: int, policy_attr: str = "my_walk"):
        super().__init__(name, state_id)
        self.policy_attr = policy_attr

    def _policy(self, ctx: BxiExample) -> Any:
        return getattr(ctx, self.policy_attr)

    def on_prepare_enter(
        self,
        ctx: BxiExample,
        from_state: StateBehavior[BxiExample],
        transition: TransitionProfile,
    ) -> None:
        super().on_prepare_enter(ctx, from_state, transition)
        policy = self._policy(ctx)
        ctx.preheat_model(
            policy,
            with_cmd_vel=True,
            cmd_vel=self.get_cmd_vel(ctx),
        )

    def get_first_frame(self, ctx: BxiExample) -> Optional[MotorFrame]:
        policy = self._policy(ctx)
        qpos = getattr(policy, "target_dof_pos", None)
        if qpos is None:
            qpos = getattr(policy, "default_dof_pos", None)
        if qpos is None:
            return None
        return self._motor_frame(qpos, policy.kps, policy.kds)

    def get_motor_frame(
        self,
        ctx: BxiExample,
        dt: float,
        on_translation: bool,
    ) -> Optional[MotorFrame]:
        policy = self._policy(ctx)
        cmd_vel = self.get_cmd_vel(ctx)
        qpos, _vel = policy.inference_step(
            ctx.current_q,
            ctx.current_dq,
            ctx.current_quat_wxyz,
            ctx.current_omega,
            cmd_vel,
        )
        return self._motor_frame(qpos, policy.kps, policy.kds)

    def on_update(self, ctx: BxiExample, dt: float) -> None:
        if ctx.is_orientation_unsafe(ctx.current_quat_xyzw):
            ctx.request_state("zero_torque", trigger="safety")
            return

        frame = self.get_motor_frame(ctx, dt, False)
        if frame is not None:
            ctx.set_motor_target(*frame)

这段代码依赖的 AnyOptionalMotorFrameRobotControlStateStateBehaviorTransitionProfile,当前 robot_states.py 顶部已经有。

如果你的模型不吃速度命令,就不要传 cmd_vel

qpos = policy.inference_step(
    ctx.current_q,
    ctx.current_dq,
    ctx.current_quat_wxyz,
    ctx.current_omega,
)

如果你的模型返回的不是 (qpos, vel),按实际返回值改这一行;状态最终只需要输出:

ctx.set_motor_target(qpos, kp, kd)

4. 在状态机 YAML 注册状态

打开:

src/bxi_example_py_elf3/config/elf3_state_machine.yaml

先在 remote_events 里加一个事件。选择一个当前遥控器配置里还没被其他 event 使用的 MotionCommands slot/value:

  my_walk_event:
    slot: <unused_motion_command_slot>
    value: <unused_value>

states.normal.transitions.on_event 里加入口。第一次验证只建议从 normal 进入新模型:

        my_walk_event:
          to: my_walk
          transition: soft_switch

再在 states: 下添加新状态:

  my_walk:
    manifest:
      label: 新步态
      index: 14
      group: Advanced
      icon: directions_walk
      confirm: true
      confirm_message: 新模型首次验证,请确保机器人周围没有障碍物
    behavior: MyWalkState
    speed_profile: normal
    params:
      policy_attr: my_walk
    transitions:
      on_event:
        normal_event:
          to: normal
          transition: soft_switch
        zero_torque_event: zero_torque

关键点:

  • behavior: MyWalkState 必须和 Python class 名一致。
  • params.policy_attr: my_walk 对应 BxiExample.load_models() 里的 self.my_walk
  • speed_profile: normal 表示状态会读取速度输入,并按 speed_profiles.normal 约束速度。
  • confirm: true 适合所有第一次上机的新模型。

5. 绑定一个测试按键

打开:

src/remote_controller/config/xbox_default.yaml

键盘 source 里加一个键,例如 0

      keyboard.my_walk: {from: keyboard.key, key: "0"}

controls 里加:

  keyboard.my_walk_event: {type: bool, source: keyboard.my_walk}

outputs 的 level: 里加:

    - output: <unused_motion_command_slot>=<unused_value>
      when:
        any:
          - [trigger.right_event, button.y_event]
          - [keyboard.my_walk_event]

这样键盘按 0,或手柄按 右扳机 + Y,会输出上面声明的 MotionCommands slot/value。这个输出会被上一节的 my_walk_event 接住。

6. 先在仿真验证

重新 build 并 source:

colcon build --packages-select bxi_example_py_elf3 remote_controller
source install/setup.bash

终端 1 启动仿真:

ros2 launch bxi_example_py_elf3 example_demo.launch.py

终端 2 启动键盘遥控器:

ros2 launch remote_controller remote_controller_keyboard.launch.py

终端 3 观察状态机:

ros2 topic echo /simulation/state_machine_info

推荐验证顺序:

1. 等待 reset 结束,状态仍是 zero_torque。
2. 进入 pd_brake 或 initial_pos,确认电机目标正常。
3. 切到 normal,确认机器人能稳定站立或行走。
4. 速度输入保持 0,触发 my_walk_event。
5. 观察 /simulation/state_machine_info.current.name 是否变成 my_walk。
6. 观察 qpos、kp、kd 是否连续,机器人是否立即摔倒。
7. 再逐步给很小的 vx、vy、yaw。
8. 用 normal_event 切回 normal。
9. 用 zero_torque_event 验证安全退出仍然有效。

如果仿真一进状态就摔:

  • 先检查 ONNX 输入输出维度是否和 29 DOF 一致。
  • 检查 IsaacLab 关节顺序是否和 bxi_example_demo.pyjoint_name 顺序一致。
  • 检查模型期望的四元数顺序。当前 IsaacLab policy 调用用的是 ctx.current_quat_wxyz
  • 检查模型输出是不是弧度位置目标,而不是 delta、力矩或归一化 action。
  • 检查 policy.kpspolicy.kds 是否合理。
  • speed_profile 的速度上限先调小,再验证。

7. 真机前检查

仿真稳定后,再准备真机。上机前至少确认:

仿真能多次进入和退出 my_walk
normal_event 能回 normal
zero_torque_event 能进入 zero_torque
机器人静止速度输入下不会明显抖动或前冲
速度输入从很小值开始不会失控
状态机没有 unknown state / unknown event 报错

真机不要把:

initial_state: my_walk

只保持:

initial_state: zero_torque

硬件 launch 当前默认:

{"/topic_prefix": "hardware/"}
{"/hot_reload": False}

这是正确的。真机首次验证不要开热重载。

8. 上真机

在机器人工作区 build 并 source:

colcon build --packages-select bxi_example_py_elf3 remote_controller
source install/setup.bash

启动硬件节点:

ros2 launch bxi_example_py_elf3 example_demo_hw.launch.py

如果需要单独启动遥控器:

ros2 launch remote_controller remote_controller.launch.py

观察硬件状态机:

ros2 topic echo /hardware/state_machine_info

第一次上机顺序:

1. 人和机器人保持安全距离,急停和 stop 按键可用。
2. 从 zero_torque 进入 pd_brake 或 initial_pos。
3. 再进入 normal。
4. 不给速度,触发 my_walk_event。
5. 只停留 1-2 秒,立即切回 normal。
6. 多次确认进入和退出都稳定。
7. 再给很小的 vx。
8. 最后再测试 vy 和 yaw。

如果出现明显抖动、前冲、左右脚顺序不对、髋膝方向反了,立即退出到 normalzero_torque,不要继续调速度。

9. 发布保护

如果这个模型属于内部或高危动作,打开:

src/bxi_example_py_elf3/config/release_protection.yaml

添加:

  my_walk:
    behavior:
      - MyWalkState
    model_keys: [my_walk]
    files:
      - ../data/isaaclab_model/my_walk.onnx

model_keys 对应 self.my_walkfiles 对应实际模型文件。要删除 .onnx.npz,必须写进 files

10. 最小检查清单

1. my_walk.onnx 已放进 data/isaaclab_model/。
2. bxi_example_demo.py 的 load_models() 有 self.my_walk。
3. robot_states.py 有 MyWalkState。
4. elf3_state_machine.yaml 有 my_walk_event。
5. states.normal 能通过 my_walk_event 切到 my_walk。
6. states.my_walk 的 behavior 是 MyWalkState。
7. states.my_walk.params.policy_attr 是 my_walk。
8. remote_controller/config/xbox_default.yaml 能输出上面声明的 MotionCommands slot/value。
9. colcon build 后 install/ 里能找到模型文件。
10. 仿真 /simulation/state_machine_info 能看到 current.name = my_walk。
11. 真机仍然从 zero_torque 启动。
12. 真机 /hardware/state_machine_info 能看到进入和退出 my_walk。

Clone this wiki locally