-
Notifications
You must be signed in to change notification settings - Fork 12
Hands On Model State
上一课的 SinWaveState 不依赖模型,适合理解状态机。真实动作通常会依赖:
data/*.npz
data/*.onnx
本课用一个具体例子:
wave_motion
WaveMotionState
它代表一个由 .npz 动作数据和 .onnx 策略模型驱动的动作状态。
这一课仍然保持低门槛:状态类继续直接写在 robot_states.py 里。不新建 states/ 子包,不新建 wave_motion_state.py,不改 setup.py。
新增或放入模型文件:
src/bxi_example_py_elf3/data/wave_motion.npz
src/bxi_example_py_elf3/data/wave_motion.onnx
修改文件:
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/config/release_protection.yaml
把文件放到:
src/bxi_example_py_elf3/data/wave_motion.npz
src/bxi_example_py_elf3/data/wave_motion.onnx
data/ 目录里的文件会被当前 package 安装逻辑自动安装到 share/bxi_example_py_elf3/data,所以这里不需要改 setup.py。
打开:
src/bxi_example_py_elf3/bxi_example_py_elf3/bxi_example_demo.py
找到 load_models()。当前工程不再通过 launch 的 npz_file_dict / onnx_file_dict 传模型路径,而是在 load_models() 里用 model_file() 直接声明:
self.withoutarm: HumanoidGaitPolicyLiteIsaaclab = HumanoidGaitPolicyLiteIsaaclab(
model_file("isaaclab_model/withoutarm.onnx")
)添加:
self.wave_motion = DanceMotionPolicyGravityIsaaclab(
model_file("wave_motion.npz"),
model_file("wave_motion.onnx"),
start_frame=0,
)为什么这里用 DanceMotionPolicyGravityIsaaclab:
- 它已经在当前文件 import。
- 它接收
.npz和.onnx。 - 它有
timestep、start_frame、end_frame。 - 它有
inference_step()、target_dof_pos、kps、kds。
如果你的模型使用其他已有推理类,也可以替换这里的 policy 类型;状态类里要按该 policy 的实际接口取 target_dof_pos、kp/kd、播放帧和结束帧。
打开:
src/bxi_example_py_elf3/bxi_example_py_elf3/robot_states.py
推荐把 WaveMotionState 放在 DanceState 附近,因为它们都是模型动作状态。
直接添加:
class WaveMotionState(RobotControlState):
def __init__(
self,
name: str,
state_id: int,
policy_attr: str = "wave_motion",
reset_on_finish: bool = True,
):
super().__init__(name, state_id)
self.policy_attr = policy_attr
self.reset_on_finish = reset_on_finish
self.playing = True
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)
policy.timestep = policy.start_frame
if hasattr(policy, "timeinit"):
policy.timeinit = 0.0
ctx.preheat_model(policy)
def on_enter(self, ctx: BxiExample) -> None:
self.playing = True
policy = self._policy(ctx)
policy.timestep = policy.start_frame
if hasattr(policy, "timeinit"):
policy.timeinit = 0.0
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)
if policy.timestep > policy.end_frame:
return None
qpos = policy.inference_step(
ctx.current_q,
ctx.current_dq,
ctx.current_quat_wxyz,
ctx.current_omega,
)
if self.playing and not on_translation:
policy.timestep += 50 * dt
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
policy = self._policy(ctx)
if policy.timestep > policy.end_frame:
if self.reset_on_finish:
policy.timestep = policy.start_frame
ctx.request_state(
"normal",
trigger=f"{self.name}_finished",
transition="soft_switch",
)
return
frame = self.get_motor_frame(ctx, dt, False)
if frame is not None:
ctx.set_motor_target(*frame)
def on_action(self, ctx: BxiExample, action_name: str) -> bool:
if action_name != "toggle_motion_pause":
return False
self.playing = not self.playing
return True这段代码使用的 Any、Optional、MotorFrame、RobotControlState、StateBehavior、TransitionProfile 在当前 robot_states.py 顶部已经有,不需要额外新建文件或导入子包。
这个状态类展示了真实动作状态常见结构:
-
policy_attr指向ctx.wave_motion。 -
on_prepare_enter()预热模型。 -
on_enter()重置 timestep。 -
get_first_frame()给过渡态使用。 -
get_motor_frame()只负责计算电机帧;on_translation=True表示过渡采样中,通常不要推进播放帧。 -
on_update()做安全检查、播放结束检查和输出。 -
on_action()支持暂停。
有些模型的推理接口需要 cmd_vel。这种状态不要直接读 ctx.raw_cmd_vel,而是在状态里调用 self.get_cmd_vel(ctx):
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)
policy.timestep = policy.start_frame
ctx.preheat_model(
policy,
with_cmd_vel=True,
cmd_vel=self.get_cmd_vel(ctx),
)
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 = 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)并在状态 YAML 里声明:
wave_motion:
behavior: WaveMotionState
speed_profile: normal没有 speed_profile 时,get_cmd_vel(ctx) 会返回零速度。
如果这个模型还需要状态私有的速度滤波,不要重写 get_cmd_vel(),而是重写:
def process_cmd_vel(
self,
ctx: BxiExample,
cmd_vel: np.ndarray,
) -> Optional[np.ndarray]:
cmd_vel[1] = 0.0
return cmd_velget_cmd_vel() 会自动把返回值写入 ctx.current_cmd_vel。
例如模型状态要订阅一个外部播放速度、目标点或动作选择话题,实现 on_bind(ctx):
def on_bind(self, ctx: BxiExample) -> None:
self.selector_sub = ctx.create_subscription(
String,
"wave_motion_selector",
self._selector_callback,
10,
)
def _selector_callback(self, msg: String) -> None:
self.selected_clip = msg.dataon_bind(ctx) 只执行一次,适合创建 ROS 订阅、client 或 timer。__init__() 里只放 policy_attr、playing 这类普通私有变量。
如果使用上面的 String 示例,记得在 robot_states.py 顶部加:
from std_msgs.msg import String打开:
src/bxi_example_py_elf3/config/elf3_state_machine.yaml
添加 event:
remote_events:
wave_motion_event:
slot: <unused_motion_command_slot>
value: <unused_value>在 normal 中添加入口:
states:
normal:
transitions:
on_event:
wave_motion_event:
to: wave_motion
transition: first_frame_switch添加状态:
wave_motion:
behavior: WaveMotionState
params:
policy_attr: wave_motion
reset_on_finish: true
transitions:
on_event:
normal_event:
to: normal
transition: first_frame_switch
zero_torque_event: zero_torque
toggle_dance_pause_event:
action: toggle_motion_pause注意:
-
behavior: WaveMotionState必须和 Python 类名一致。 -
params.policy_attr: wave_motion表示状态会访问ctx.wave_motion。 -
ctx.wave_motion来自上一节在BxiExample里添加的self.wave_motion = ...。
打开:
src/remote_controller/config/xbox_default.yaml
键盘 source:
sources:
keyboard:
signals:
keyboard.wave_motion: {from: keyboard.key, key: "v"}control:
controls:
keyboard.wave_motion_event: {type: bool, source: keyboard.wave_motion}output:
outputs:
level:
- output: <unused_motion_command_slot>=<unused_value>
when: [keyboard.wave_motion_event]现在按键盘 v 会触发:
keyboard.wave_motion
-> <unused_motion_command_slot>=<unused_value>
-> remote_events.wave_motion_event
-> normal 切到 wave_motion
编译:
colcon build --symlink-install --packages-select bxi_example_py_elf3 remote_controller加载:
source install/setup.bash启动遥控器:
ros2 launch remote_controller remote_controller_keyboard.launch.py启动 example:
ros2 launch bxi_example_py_elf3 example_demo.launch.py观察:
ros2 topic echo /simulation/state_machine_info按 v 后应看到:
{
"events": ["wave_motion_event"],
"current": {
"name": "wave_motion"
}
}如果你在真实硬件上验证,把 launch 换成:
ros2 launch bxi_example_py_elf3 example_demo_hw.launch.py硬件验证前必须先在仿真里确认动作安全。
如果这个动作是内部动作,打开:
src/bxi_example_py_elf3/config/release_protection.yaml
添加:
protected_states:
<state_name>:
behavior: <StateClassName>
model_keys: [<model_member_name>]
files:
- ../data/<model_or_motion_file>model_keys: [<model_member_name>] 会影响:
-
bxi_example_demo.py中的self.<model_member_name> = ...初始化代码块。 - 发布保护脚本删除状态时能同时删除该模型对象引用。
如果多个受保护状态共用一个基类或辅助类,可以把 behavior 写成数组:
behavior:
- <SharedBaseStateClassName>
- <StateClassName>但入门阶段一般只写当前状态类即可。
启动时报 KeyError: 'wave_motion':
-
BxiExample.load_models()没有添加self.wave_motion。 -
params.policy_attr写的名字和self.wave_motion不一致。 - 没重新
colcon build。
状态找不到 WaveMotionState:
-
WaveMotionState没写进robot_states.py。 - YAML 里的
behavior拼错了。 - 修改后没重新
colcon build和source install/setup.bash。
进入状态后第一帧不对:
-
get_first_frame()时 policy 还没推理出target_dof_pos。 - 检查
on_prepare_enter()是否调用ctx.preheat_model(policy)。
动作播放完没有返回:
- 检查 policy 是否有
end_frame。 - 检查
policy.timestep是否递增。 - 检查
self.playing是否被 action 暂停。
1. data/wave_motion.npz 和 data/wave_motion.onnx 存在。
2. BxiExample.load_models() 加载 self.wave_motion。
3. self.wave_motion 使用 model_file() 解析模型路径。
4. WaveMotionState 直接写在 robot_states.py 里。
5. 没有新建 states/ 文件夹。
6. 没有修改 setup.py。
7. elf3_state_machine.yaml 有 remote_events.wave_motion_event。
8. normal 能切到 wave_motion。
9. xbox_default.yaml 或自定义遥控器配置能输出上面声明的 MotionCommands slot/value。
10. /simulation/state_machine_info 能看到 wave_motion。
11. 如果是内部动作,release_protection.yaml 写了 model_keys 和 files。
下一课:手把手 3:从零绑定按键并接入状态机。