Skip to content

Custom Transition

konodoki edited this page Jul 24, 2026 · 12 revisions

自定义过渡

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

当前有两种放置方式:

  • 业务专用:放在对应 Mod 中,由 entrypoint 模块导入,支持 Mod 热重载和回滚。
  • 框架通用:放在 bxi_example_py_elf3/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 typing import TYPE_CHECKING

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

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


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.pos_last, ctx.kp_last, ctx.kd_last)

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

Mod 的 plugin.py 必须导入该模块:

from . import pose_hold  # noqa: F401,导入时注册 type_name

Mod 加载器会对动态过渡注册做快照;热重载失败时恢复旧插件,关闭旧 Mod 时移除旧动态类型。

配置 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 已导入插件模块。
  3. 配置由 ConfigReader 严格读取。
  4. Session 数据只在 on_start() 初始化。
  5. 所需状态能力在 validate_states() 检查。
  6. 资源释放和热重载失败回滚没有副作用。

Clone this wiki locally