Skip to content

Hands On Custom Transition

konodoki edited this page Jul 23, 2026 · 13 revisions

手把手 4:从零写一个自定义过渡

本课新增 pose_gain_blend:从过渡开始时的电机位置和增益,平滑插值到目标状态提供的进入帧。

1. 新建插件文件

新建:

src/bxi_example_py_elf3/bxi_example_py_elf3/transitions/pose_gain_blend.py

不要修改注册表或 transitions/__init__.py

2. 实现插件

from __future__ import annotations

from collections.abc import Mapping
from typing import TYPE_CHECKING, Literal

from bxi_example_py_elf3.utils.transition_core import (
    ConfigReader,
    MotorFrame,
    SingleClassTransition,
    require_entry_frame_provider,
)

if TYPE_CHECKING:
    from bxi_example_py_elf3.bxi_example_demo import BxiExample
    from bxi_example_py_elf3.utils.state_machine import StateBehavior


Curve = Literal["linear", "smoothstep"]


class PoseGainBlendTransition(SingleClassTransition):
    type_name = "pose_gain_blend"

    def __init__(self, name: str, duration: float, curve: Curve):
        super().__init__(name, duration)
        self._curve = curve
        self._start: MotorFrame | None = None
        self._target: MotorFrame | None = None

    @classmethod
    def from_config(
        cls,
        name: str,
        raw: Mapping[str, object],
    ) -> "PoseGainBlendTransition":
        reader = ConfigReader(raw, name)
        duration = reader.float("duration", minimum=0.0)
        curve = reader.literal(
            "curve",
            ("linear", "smoothstep"),
            default="smoothstep",
        )
        reader.finish()
        return cls(name, duration, curve)

    def validate_states(
        self,
        from_state: "StateBehavior[BxiExample]",
        to_state: "StateBehavior[BxiExample]",
    ) -> None:
        require_entry_frame_provider(to_state)

    def on_start(
        self,
        ctx: "BxiExample",
        from_state: "StateBehavior[BxiExample]",
        to_state: "StateBehavior[BxiExample]",
    ) -> None:
        self._start = MotorFrame.create(ctx.pos_last, ctx.kp_last, ctx.kd_last)
        provider = require_entry_frame_provider(to_state)
        self._target = provider.get_entry_frame(ctx)

    def apply(self, ctx: "BxiExample", dt: float, progress: float) -> None:
        start = self._start
        target = self._target
        if start is None or target is None:
            raise RuntimeError("pose gain blend transition has not started")

        alpha = progress
        if self._curve == "smoothstep":
            alpha = progress * progress * (3.0 - 2.0 * progress)

        ctx.set_motor_target(
            start.qpos + (target.qpos - start.qpos) * alpha,
            start.kp + (target.kp - start.kp) * alpha,
            start.kd + (target.kd - start.kd) * alpha,
        )

    def config_snapshot(self) -> dict[str, object]:
        return {"curve": self._curve}

3. 让目标状态提供能力

class SinWaveState(RobotControlState, EntryFrameProvider):
    def get_entry_frame(self, ctx: BxiExample) -> MotorFrame:
        return self._motor_frame(
            ctx.joint_nominal_pos,
            ctx.joint_kp,
            ctx.joint_kd,
        )

    def on_update(self, ctx: BxiExample, dt: float) -> None:
        ...

其他状态不需要实现这个方法。状态图加载时会验证实际目标状态是否具备能力。

4. 添加 profile

transition_profiles:
  pose_blend_switch:
    type: pose_gain_blend
    duration: 0.5
    curve: smoothstep

5. 使用

sin_wave_event:
  to: sin_wave
  transition: pose_blend_switch

单次覆盖:

sin_wave_event:
  to: sin_wave
  transition:
    profile: pose_blend_switch
    duration: 1.0
    curve: linear

也可以完全内联:

transition:
  type: pose_gain_blend
  duration: 0.8
  curve: smoothstep

6. 验证自动发现

启动状态机。成功时 profile 会显示:

{
  "name": "pose_blend_switch",
  "type": "pose_gain_blend",
  "duration": 0.5,
  "curve": "smoothstep"
}

常见错误:

  • unknown transition type:文件没有安装、type_name 拼错,或模块导入失败。
  • unknown fields:YAML 字段没有被 ConfigReader 读取。
  • must implement EntryFrameProvider:某条边的目标状态没有实现能力。
  • duplicate transition type:两个类用了相同 type_name

7. 插件独有的状态函数

如果新过渡需要一个只属于它的函数,在同一个插件文件中声明 @runtime_checkable Protocol,让需要该过渡的状态显式继承并实现。不要把该函数加入 RobotControlState

完整模式见 自定义过渡 的“状态能力”章节。

8. 本课检查清单

  1. 一个插件类对应一个文件。
  2. type_name 唯一。
  3. 配置读取有明确类型和范围检查。
  4. reader.finish() 已调用。
  5. 状态能力在图加载时验证。
  6. Session 数据只在 on_start() 初始化。
  7. YAML 使用 typeprofile

下一课:手把手 5:从零使用双状态运行混合过渡

Clone this wiki locally