-
Notifications
You must be signed in to change notification settings - Fork 12
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()。
文件 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_nameMod 加载器会对动态过渡注册做快照;热重载失败时恢复旧插件,关闭旧 Mod 时移除旧动态类型。
在同一个 mod.yaml:
transition_profiles:
pose_hold:
type: com.example.motion.pose_hold
duration: 0.1
routes:
- from: source
event: activate
to: target
transition: pose_holdprofile 运行时名称是 com.example.motion/pose_hold。route 内本地引用会自动解析。
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(),让未知字段在加载阶段暴露。
多数插件继承 SingleClassTransition:配置编译得到 plan 原型,每次切换浅复制为独立 Session。运行数据在 on_start() 初始化,apply() 每帧输出。
目标需要进入帧时:
def validate_states(self, from_state, to_state):
require_entry_frame_provider(to_state)需要动态运行帧时使用 require_running_frame_provider()。状态只在确实需要时实现 EntryFrameProvider 或 RunningFrameProvider,不要把插件专有函数塞进公共状态基类。
instant
hold
entry_gain_ramp
running_blend
sequence
sequence 的每个子 Session 在轮到它时才创建。
-
type_name全局唯一;业务插件建议包含 Mod id。 - Mod entrypoint 已导入插件模块。
- 配置由
ConfigReader严格读取。 - Session 数据只在
on_start()初始化。 - 所需状态能力在
validate_states()检查。 - 资源释放和热重载失败回滚没有副作用。