Skip to content

Hands On Custom Transition

konodoki edited this page Jul 28, 2026 · 13 revisions

手把手 4:在 Mod 内写自定义过渡

Transition 负责在状态切换期间临时生成电机帧。需要它是因为源状态和目标状态的 qpos/kp/kd 不一定连续,直接切换可能造成突跳;内置过渡可以保持、渐增增益或混合两个运行状态。

当这些内置策略无法表达业务需要的切换过程时,才需要自定义 Transition。本课为 com.example.sin_wave 增加 pose_gain_blend,平滑混合过渡开始帧和目标进入帧。

1. 新建模块

mods/com.example.sin_wave/pose_gain_blend.py
from collections.abc import Mapping
import numpy as np

from bxi_example_py_elf3.framework.mod_api.transition import (
    ConfigReader,
    MotorFrame,
    SingleClassTransition,
    require_entry_frame_provider,
)


class PoseGainBlendTransition(SingleClassTransition):
    type_name = "com.example.sin_wave.pose_gain_blend"

    def __init__(self, name, duration, curve):
        super().__init__(name, duration)
        self._curve = curve
        self._start = None
        self._target = None
        self._output = None

    @classmethod
    def from_config(cls, name: str, raw: Mapping[str, object]):
        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, to_state):
        require_entry_frame_provider(to_state)

    def on_start(self, ctx, from_state, to_state):
        self._start = MotorFrame.create(
            ctx.control_layout,
            ctx.pos_last,
            ctx.kp_last,
            ctx.kd_last,
        )
        self._target = require_entry_frame_provider(to_state).get_entry_frame(ctx)
        self._output = MotorFrame.empty(ctx.control_layout)

    def apply(self, ctx, dt, progress):
        if self._start is None or self._target is None or self._output is None:
            raise RuntimeError("pose gain blend has not started")
        alpha = progress
        if self._curve == "smoothstep":
            alpha = progress * progress * (3.0 - 2.0 * progress)
        for start, target, output in (
            (self._start.qpos, self._target.qpos, self._output.qpos),
            (self._start.kp, self._target.kp, self._output.kp),
            (self._start.kd, self._target.kd, self._output.kd),
        ):
            np.subtract(target, start, out=output)
            output *= alpha
            output += start
        ctx.set_motor_target(self._output)

2. 显式注册

plugin.py 导入类,并加入返回的 ModDefinition

from bxi_example_py_elf3.framework.mod_api import ModDefinition
from .pose_gain_blend import PoseGainBlendTransition


def create_mod(ctx):
    return ModDefinition(
        state_factories={
            # 保留这个 Mod 原有的状态工厂
        },
        transition_plugins={
            PoseGainBlendTransition.type_name: PoseGainBlendTransition,
        },
    )

注册关系属于当前 Mod,不存在“导入模块时修改全局注册表”的隐式副作用。遗漏 transition_plugins 会在加载配置时得到 unknown transition type,不会悄悄依赖导入顺序。

3. 目标状态提供进入帧

class SinWaveState(RobotControlState, EntryFrameProvider):
    def get_entry_frame(self, ctx):
        return self._motor_frame(
            ctx.pos_last, ctx.kp_last, ctx.kd_last
        )

4. 清单 profile 和 route

transition_profiles:
  pose_blend:
    type: com.example.sin_wave.pose_gain_blend
    duration: 0.5
    curve: smoothstep

routes:
  - from: com.bxi.basic_actions/normal
    event: activate
    to: sin_wave
    transition: pose_blend

单次覆盖:

transition:
  profile: pose_blend
  duration: 1.0
  curve: linear

5. 验证

修改插件后重启控制节点;启动日志应显示 ModRuntime 构建成功。常见错误:

  • unknown transition type:插件没有加入 transition_plugins
  • transition key ... does not match:字典键与类的 type_name 不一致。
  • duplicate transition type:名称未包含唯一 Mod 命名空间。
  • unknown fields:字段未被 reader 消费。
  • must implement EntryFrameProvider:某条目标边能力不足。

下一课:双状态运行混合

Clone this wiki locally