-
Notifications
You must be signed in to change notification settings - Fork 12
Hands On Custom Transition
konodoki edited this page May 14, 2026
·
13 revisions
本课目标:从零写一个新的进入过渡行为:
first_frame_blend_pose_gain
它做三件事:
- 读取目标状态的第一帧。
- 让 qpos 从当前姿态平滑插值到第一帧。
- 让
kp/kd按进度从起始值过渡到目标值。
这比当前 first_frame_ramp_kp 更进一步:不仅 kp/kd 渐变,目标角度也渐变。
修改:
src/bxi_example_py_elf3/bxi_example_py_elf3/robot_state_base.py
src/bxi_example_py_elf3/config/elf3_state_machine.yaml
不改:
src/bxi_example_py_elf3/bxi_example_py_elf3/state_machine.py
原因:
-
state_machine.py是通用状态机。 - 机器人电机过渡属于机器人层,应该放
robot_state_base.py。 - 行为参数放
transition.data,不要把专用字段硬编码进TransitionProfile。
切状态时调用顺序:
旧状态.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_enter(ctx)
进入侧在退出侧后面调用。如果两边都 ctx.set_motor_target(),进入侧会覆盖退出侧。
所以我们把新行为写在:
RobotControlState.on_enter_transition()在写新行为前,先确认目标状态实现了:
get_first_frame(ctx)例如 SinWaveState:
def get_first_frame(self, ctx: BxiExample) -> Optional[MotorFrame]:
qpos = ctx.joint_nominal_pos.copy()
return self._motor_frame(qpos, ctx.joint_kp, ctx.joint_kd)YAML 先用已有 profile:
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如果这个都不工作,先修 get_first_frame(),不要急着写新行为。
打开:
src/bxi_example_py_elf3/bxi_example_py_elf3/robot_state_base.py
找到:
def on_enter_transition(
self,
ctx: BxiExample,
from_state: StateBehavior[BxiExample],
progress: float,
transition: TransitionProfile,
) -> None:
if transition.enter_behavior == "hold_last_motor":
ctx.hold_last_motor_target()
elif transition.enter_behavior == "first_frame_ramp_kp":
self._enter_first_frame_ramp_kp(ctx, progress, transition)改成:
def on_enter_transition(
self,
ctx: BxiExample,
from_state: StateBehavior[BxiExample],
progress: float,
transition: TransitionProfile,
) -> None:
if transition.enter_behavior == "hold_last_motor":
ctx.hold_last_motor_target()
elif transition.enter_behavior == "first_frame_ramp_kp":
self._enter_first_frame_ramp_kp(ctx, progress, transition)
elif transition.enter_behavior == "first_frame_blend_pose_gain":
self._enter_first_frame_blend_pose_gain(ctx, progress, transition)现在状态基类知道有这个 behavior 名了。
在 RobotControlState 类中添加:
def _transition_alpha(self, progress: float, transition: TransitionProfile) -> float:
alpha = min(max(float(progress), 0.0), 1.0)
curve = str(transition.data.get("curve", "smoothstep"))
if curve == "linear":
return alpha
if curve == "smoothstep":
return alpha * alpha * (3.0 - 2.0 * alpha)
raise ValueError(f"unsupported transition curve: {curve}")支持:
data:
curve: linear或:
data:
curve: smoothstep继续在 RobotControlState 中添加:
def _qpos_start(
self,
ctx: BxiExample,
mode: str,
target: np.ndarray,
) -> np.ndarray:
if mode == "target":
return target.copy()
if mode == "nominal":
return np.asarray(ctx.joint_nominal_pos, dtype=np.float32).copy()
if mode == "last_state":
return np.asarray(ctx.pos_last_state, dtype=np.float32).copy()
if mode == "current":
return np.asarray(ctx.current_q, dtype=np.float32).copy()
raise ValueError(f"unsupported transition qpos start mode: {mode}")支持模式:
-
current:从当前真实关节位置开始。 -
last_state:从旧状态退出时缓存的位置开始。 -
nominal:从默认站立姿态开始。 -
target:不插值位置,直接使用目标第一帧。
推荐默认用 current。
继续在 RobotControlState 中添加:
def _enter_first_frame_blend_pose_gain(
self,
ctx: BxiExample,
progress: float,
transition: TransitionProfile,
) -> None:
if self._prepared_first_frame is None:
first_frame = self.get_first_frame(ctx)
if first_frame is None:
ctx.hold_last_motor_target()
return
self._prepared_first_frame = self._motor_frame(*first_frame)
qpos_target, kp_target, kd_target = self._prepared_first_frame
alpha = self._transition_alpha(progress, transition)
qpos_start_mode = str(transition.data.get("qpos_start", "current"))
qpos_start = self._qpos_start(ctx, qpos_start_mode, qpos_target)
if qpos_start.shape != qpos_target.shape:
raise ValueError(
f"qpos start shape {qpos_start.shape} does not match target shape {qpos_target.shape}"
)
kp_start_mode = str(transition.data.get("kp_start", "zero"))
kd_start_mode = str(transition.data.get("kd_start", "target"))
kp_start = self._gain_start(kp_start_mode, kp_target, ctx.kp_last)
kd_start = self._gain_start(kd_start_mode, kd_target, ctx.kd_last)
qpos = qpos_start + (qpos_target - qpos_start) * alpha
kp = kp_start + (kp_target - kp_start) * alpha
kd = kd_start + (kd_target - kd_start) * alpha
ctx.set_motor_target(
qpos.astype(np.float32),
kp.astype(np.float32),
kd.astype(np.float32),
)这个函数没有改 state_machine.py。它只使用:
transition.enter_behaviortransition.data- 目标状态的
get_first_frame() ctx.set_motor_target()
打开:
src/bxi_example_py_elf3/config/elf3_state_machine.yaml
在 transition_profiles: 下添加:
pose_blend_switch:
exit_duration: 0.02
enter_duration: 0.4
exit_behavior: hold_last_motor
enter_behavior: first_frame_blend_pose_gain
data:
qpos_start: current
kp_start: zero
kd_start: target
curve: smoothstep字段含义:
-
enter_behavior:刚刚写的新行为。 -
qpos_start:位置插值起点。 -
kp_start:kp 起点。 -
kd_start:kd 起点。 -
curve:插值曲线。
让 normal -> sin_wave 使用新 profile:
states:
normal:
transitions:
on_event:
sin_wave:
to: sin_wave
transition: pose_blend_switch现在切换时:
当前姿态
-> 平滑插值到 SinWaveState.get_first_frame()
-> kp 从 0 增加到目标 kp
-> kd 从目标 kd 开始
如果大部分状态使用 0.4 秒,但某一次想 1 秒:
sin_wave:
to: sin_wave
transition:
name: slow_sin_wave_entry
base: pose_blend_switch
enter_duration: 1.0不用新建全局 profile。
例如这次不想从 current 插值,而是从 last_state:
transition:
name: sin_wave_from_last_state
base: pose_blend_switch
data:
qpos_start: last_state
curve: lineardata 会和 base profile 的 data 合并,覆盖同名字段。
如果某个过渡只服务 SinWaveState,不要写到 robot_state_base.py,直接写状态类:
from bxi_example_py_elf3.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_private_entry":
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_private_entry:
enter_duration: 0.3
enter_behavior: sin_wave_private_entry判断标准:
- 多个状态复用:写
robot_state_base.py。 - 只有一个状态使用:写状态类。
启动后观察:
ros2 topic echo /simulation/state_machine_info切换中应该看到:
{
"mode": "transition",
"transition": {
"from": {"name": "normal"},
"to": {"name": "sin_wave"},
"profile": "pose_blend_switch",
"enter_behavior": "first_frame_blend_pose_gain",
"enter_progress": 0.5,
"data": {
"qpos_start": "current",
"kp_start": "zero",
"curve": "smoothstep"
}
}
}如果没有看到:
- 检查 transition profile 名是否写对。
- 检查当前状态是否真的走了那条 transition。
- 检查
enter_behavior拼写是否和代码一致。
读完本课后,你应该能写出三种层级的过渡:
1. 直接使用已有 profile
2. 在 YAML inline 覆盖单次 transition
3. 在 robot_state_base.py 或状态类中新增 enter_behavior / exit_behavior
你也应该理解为什么:
transition.data 是行为私有参数
state_machine.py 不应该硬编码 enter_kp_start 这类字段