Skip to content

Custom Transition

konodoki edited this page May 23, 2026 · 12 revisions

自定义过渡行为

过渡行为负责状态切换过程中电机目标怎么输出。常见需求:

  • 退出旧状态时保持上一帧电机目标。
  • 进入新状态时先对齐新状态第一帧。
  • kp 从 0 缓慢增加,避免突然硬切。
  • 某些动作需要自定义姿态插值或特殊收尾。

1. 状态切换生命周期

状态机切换时调用顺序:

旧状态.on_exit(ctx)
新状态.on_prepare_enter(ctx, from_state, transition)

过渡期间每个周期:
  旧状态.on_exit_transition(ctx, to_state, exit_progress, transition)
  新状态.on_enter_transition(ctx, from_state, enter_progress, transition)

过渡结束:
  新状态.on_transition_commit(ctx, from_state, transition)

注意:进入侧在退出侧之后调用。如果两边都写 ctx.set_motor_target(),进入侧通常会覆盖退出侧。

建议:

  • 退出侧负责保存、保持、收尾。
  • 进入侧负责真正的过渡目标。

2. TransitionProfile 字段

YAML:

transition_profiles:
  first_frame_switch:
    exit_duration: 0.02
    enter_duration: 0.1
    exit_behavior: hold_last_motor
    enter_behavior: first_frame_ramp_kp
    data:
      kp_start: zero
      kd_start: target

Python 数据结构:

TransitionProfile(
    name="first_frame_switch",
    duration=0.1,
    exit_duration=0.02,
    enter_duration=0.1,
    exit_behavior="hold_last_motor",
    enter_behavior="first_frame_ramp_kp",
    data={"kp_start": "zero", "kd_start": "target"},
)

data 是行为私有数据。状态机不解释它,只负责合并和传递。

3. 内置机器人过渡行为

当前 utils/robot_state_base.py 中有:

  • hold_last_motor:保持上一帧电机目标。
  • first_frame_ramp_kp:目标角度使用新状态第一帧,kp/kdprogress 增大。
  • dual_running_blend:旧状态和新状态都继续生成电机目标,然后把两边的 (qpos, kp, kd) 按进入进度混合输出。

first_frame_ramp_kp 会调用目标状态的:

get_first_frame(ctx)

如果目标状态没有第一帧,就退化成保持上一帧目标。

dual_running_blend 会调用状态的:

get_transition_frame(ctx, role, transition)

RobotControlState 的默认实现会调用:

get_motor_frame(ctx, ctx.dt)

所以推荐把“计算一帧电机目标”的代码放进 get_motor_frame(),让 on_update()dual_running_blend 共用。get_motor_frame()get_transition_frame() 只返回 (qpos, kp, kd),不要调用 ctx.set_motor_target(),也不要请求切状态。

常用配置:

transition_profiles:
  dual_running_blend:
    duration: 0.3
    exit_behavior: none
    enter_behavior: dual_running_blend
    data:
      curve: smoothstep
      run_from: true
      run_to: true
      from_fallback: last_motor
      to_fallback: first_frame

字段含义:

  • curve:混合曲线,支持 linearsmoothstepsmootherstep
  • run_from:是否运行旧状态并采样,默认 true
  • run_to:是否运行新状态并采样,默认 true
  • from_fallback:旧状态采样不到时的退路,默认 last_motor
  • to_fallback:新状态采样不到时的退路,默认 first_frame

fallback 可选:

  • last_motor / hold_last_motor:上一帧实际发给电机的目标。
  • first_frame:目标状态 get_first_frame(ctx)
  • none:没有退路,另一侧有帧就直接用另一侧。

dual_running_blend 的目标状态会在过渡期间提前运行。提交进入时,RobotControlState.on_transition_commit() 会跳过重复的 on_enter(),避免目标状态在过渡结束瞬间重新回到第一帧。

目标状态第一次被采样前会调用 on_transition_runtime_enter(ctx, transition)。默认实现会调用 on_enter(ctx),如果目标状态过渡采样前需要特殊初始化,可以重写这个接口。

注意:状态机仍沿用现有生命周期,旧状态的 on_exit() 会在过渡开始时调用。当前内置状态的 on_exit() 只保存上一状态电机信息,不会销毁模型,所以仍可被采样。如果你自己写的状态要用 dual_running_blend,不要在 on_exit() 里释放过渡采样还需要的模型、缓存或动作数据。

4. 什么时候改 utils/robot_state_base.py

通用行为放在:

src/bxi_example_py_elf3/bxi_example_py_elf3/utils/robot_state_base.py

适合放这里的行为:

  • 多个状态都会用。
  • 和机器人电机目标结构有关。
  • 不依赖某个具体动作的数据文件。

例如新增一个通用 first_frame_blend_pos_and_gain

5. 什么时候写在具体状态类里

只服务某个状态的行为,直接写在状态类里:

from bxi_example_py_elf3.utils.state_machine import StateBehavior, TransitionProfile


def on_enter_transition(
    self,
    ctx: BxiExample,
    from_state: StateBehavior[BxiExample],
    progress: float,
    transition: TransitionProfile,
) -> None:
    if transition.enter_behavior != "sin_wave_blend":
        super().on_enter_transition(ctx, from_state, progress, transition)
        return

    alpha = min(max(float(progress), 0.0), 1.0)
    qpos = ctx.joint_nominal_pos.copy()
    qpos[self.joint] += self.amplitude * alpha
    kp = ctx.joint_kp * alpha
    ctx.set_motor_target(qpos, kp, ctx.joint_kd)

YAML:

transition_profiles:
  sin_wave_blend:
    enter_duration: 0.3
    enter_behavior: sin_wave_blend

6. 使用 data 配行为参数

不要为了一个行为把专用字段加进 TransitionProfile。把行为参数放到 data

YAML:

transition_profiles:
  sin_wave_blend:
    enter_duration: 0.3
    enter_behavior: sin_wave_blend
    data:
      kp_start_ratio: 0.1
      target_offset: 0.2

状态代码:

kp_start_ratio = float(transition.data.get("kp_start_ratio", 0.0))
target_offset = float(transition.data.get("target_offset", 0.0))

好处:

  • utils/state_machine.py 不膨胀。
  • 不同行为可以有自己的参数。
  • YAML 可读。

7. inline transition 覆盖参数

预设:

transition_profiles:
  first_frame_switch:
    exit_duration: 0.02
    enter_duration: 0.1
    exit_behavior: hold_last_motor
    enter_behavior: first_frame_ramp_kp
    data:
      kp_start: zero
      kd_start: target

某个转移单独覆盖:

recover:
  to: recover
  transition:
    name: recover_long_entry
    base: first_frame_switch
    enter_duration: 1.0

也可以覆盖 data

transition:
  base: first_frame_switch
  enter_duration: 0.5
  data:
    kp_start: current

8. 调试过渡信息

状态机信息话题:

ros2 topic echo /simulation/state_machine_info

过渡中会看到:

{
  "mode": "transition",
  "transition": {
    "from": {"name": "normal"},
    "to": {"name": "sin_wave"},
    "profile": "first_frame_switch",
    "progress": 0.5,
    "exit_progress": 1.0,
    "enter_progress": 0.5,
    "enter_behavior": "first_frame_ramp_kp",
    "data": {"kp_start": "zero"}
  }
}

如果过渡效果不对,优先检查:

  • transition 是否真的是你期望的 profile。
  • enter_duration 是否被 inline 覆盖。
  • 目标状态是否实现 get_first_frame()
  • 自定义行为名是否拼写一致。
  • 行为是否调用了 super() 处理未知 behavior。

9. 设计建议

  • 常用过渡做成 profile。
  • 个别状态的小差异用 inline transition。
  • 行为专用参数放 data
  • 通用行为放 utils/robot_state_base.py
  • 状态专属行为放状态类。
  • 不要在 utils/state_machine.py 里写机器人电机细节。

Clone this wiki locally