Skip to content

Custom Transition

konodoki edited this page Jul 28, 2026 · 12 revisions

自定义过渡

过渡插件决定从状态机收到切换请求到目标状态正式进入之间,每个控制周期如何生成电机帧。需要这层机制,是因为两边的 qpos/kp/kd 不一定连续,直接换帧可能让机器人突然动作;保持当前帧、逐步建立增益或混合两端输出,适合的算法也会因状态而异。

当前有两种放置方式:

  • 业务专用:放在对应 Mod 中,由 entrypoint 模块在节点启动时导入。
  • 框架通用:放在 bxi_example_py_elf3/framework/transitions/,修改后需重启。

生命周期

plan.validate_states(source, target)
target.on_prepare(ctx, source)
session = plan.create_session(ctx, source, target)

每周期 session.update(ctx, dt)

完成:source.on_exit() -> current=target -> target.on_enter()

被中断或 Session 抛错时调用目标的 on_prepare_cancel()

Mod 内最小插件

文件 pose_hold.py

from __future__ import annotations

from collections.abc import Mapping
from bxi_example_py_elf3.framework.mod_api import RobotControlContext
from bxi_example_py_elf3.framework.mod_api.transition import (
    ConfigReader,
    MotorFrame,
    SingleClassTransition,
)

class PoseHoldTransition(SingleClassTransition):
    type_name = "com.example.motion.pose_hold"

    def __init__(self, name: str, duration: float):
        super().__init__(name, duration)
        self._frame: MotorFrame | None = None

    @classmethod
    def from_config(cls, name, raw: Mapping[str, object]):
        reader = ConfigReader(raw, name)
        duration = reader.float("duration", minimum=0.0)
        reader.finish()
        return cls(name, duration)

    def on_start(self, ctx, from_state, to_state):
        self._frame = MotorFrame.create(
            ctx.control_layout,
            ctx.pos_last,
            ctx.kp_last,
            ctx.kd_last,
        )

    def apply(self, ctx: RobotControlContext, dt: float, progress: float):
        if self._frame is None:
            raise RuntimeError("pose hold has not started")
        ctx.set_motor_target(self._frame)

Mod 的 plugin.py 显式注册该类型:

from bxi_example_py_elf3.framework.mod_api import ModDefinition
from .pose_hold import PoseHoldTransition


def create_mod(ctx):
    return ModDefinition(
        state_factories={...},
        transition_plugins={
            PoseHoldTransition.type_name: PoseHoldTransition,
        },
    )

加载器会校验字典键与 type_name 一致、类型名全局唯一,并在启动失败或 Runtime 关闭时清理当前 Mod 的动态类型。仅导入 Transition 类不会注册;必须把它显式加入当前 Mod 的 transition_plugins

配置 profile

在同一个 mod.yaml

transition_profiles:
  pose_hold:
    type: com.example.motion.pose_hold
    duration: 0.1

routes:
  - from: source
    event: activate
    to: target
    transition: pose_hold

profile 运行时名称是 com.example.motion/pose_hold。route 内本地引用会自动解析。

ConfigReader

duration = reader.float("duration", minimum=0.0)
enabled = reader.boolean("enabled", default=True)
curve = reader.literal(
    "curve", ("linear", "smoothstep"), default="smoothstep"
)
steps = reader.mappings("steps")
reader.finish()

始终调用 finish(),让未知字段在加载阶段暴露。

Plan、Session 和状态能力

多数插件继承 SingleClassTransition:配置编译得到 plan 原型,每次切换浅复制为独立 Session。运行数据在 on_start() 初始化,apply() 每帧输出。

目标需要进入帧时:

def validate_states(self, from_state, to_state):
    require_entry_frame_provider(to_state)

需要动态运行帧时使用 require_running_frame_provider()。状态只在确实需要时实现 EntryFrameProviderRunningFrameProvider,不要把插件专有函数塞进公共状态基类。

当前内置类型

instant
hold
entry_gain_ramp
running_blend
sequence

sequence 的每个子 Session 在轮到它时才创建。

检查清单

  1. type_name 全局唯一;业务插件建议包含 Mod id。
  2. Mod entrypoint 已把插件加入 transition_plugins
  3. 配置由 ConfigReader 严格读取。
  4. Session 数据只在 on_start() 初始化。
  5. 所需状态能力在 validate_states() 检查。
  6. 启动加载失败后的资源和注册项清理没有副作用。

Clone this wiki locally