Skip to content

Custom State

konodoki edited this page Jul 25, 2026 · 27 revisions

自定义状态

自定义状态必须由某个 Mod 提供。最小组成只有状态类和 mod.yaml;共享资源或注册高级扩展时再增加 plugin.py。完整渐进路线见 Mod 状态开发进阶指南

核心状态模型:RobotControlState

RobotControlState 是所有机器人状态的核心基类。状态构建器、状态机和 Transition 最终处理的都是 RobotControlState;其他状态基类只是建立在它之上的复用封装。

from __future__ import annotations

from typing import TYPE_CHECKING

from bxi_example_py_elf3.utils.robot_state_base import RobotControlState

if TYPE_CHECKING:
    from bxi_example_py_elf3.bxi_example_demo import BxiExample


class HoldState(RobotControlState):
    def on_update(self, ctx: BxiExample, dt: float) -> None:
        ctx.set_motor_target(ctx.joint_nominal_pos, ctx.joint_kp, ctx.joint_kd)

构造函数若没有额外依赖,可以直接继承基类的 (name, state_id)

便捷状态基类

这些类都继承 RobotControlState,不会改变状态机模型,也不会限制以后切换到完整 API:

开发者主要实现 基类代为实现 适用场景
PoseState target_position() 固定姿态的 MotorFrame、进入帧、运行帧和 on_update() 保持姿态、零位、单一目标姿态
ProceduralState compute_frame(ctx, elapsed) 时间重置与累计、无副作用采样和 on_update() 公式轨迹、插值、周期动作
PolicyState 策略创建、进入位置和推理 惰性策略解析、重置、预热、增益和 frame 输出 在线策略、带 history 的模型
MotionReplayState 策略资源和结束配置 播放游标、预热、暂停、结束返回 固定模型动作回放

例如,固定姿态可以用最薄的 PoseState

id: com.example.hold
version: 1.0.0
states:
  hold:
    factory: state:HoldState
    label: 保持姿态
    index: 20
from bxi_example_py_elf3.utils.state_library import PoseState


class HoldState(PoseState):
    def target_position(self, ctx):
        return ctx.pos_last.copy()

PoseStateon_update() 内部会调用 target_position(),用默认 kp/kd 构造 MotorFrame,再调用 ctx.set_motor_target()。它还提供稳定的进入帧和运行帧,因此可直接配合需要 EntryFrameProviderRunningFrameProvider 的 Transition。

当需要自行控制生命周期、传感器处理、输出时序或特殊 capability 时,直接继承 RobotControlState。从便捷类切换到核心类不要求修改 Mod 清单中的状态名和 routes。

显式工厂

from bxi_example_py_elf3.utils.mod_system import ModDefinition, ModLoadContext

from .state import HoldState


def create_mod(context: ModLoadContext) -> ModDefinition:
    return ModDefinition(
        state_factories={
            "hold": lambda state: HoldState(state.name, state.state_id),
        }
    )

框架不再扫描所有 RobotControlState 子类。工厂键必须与清单的本地状态名完全一致。

清单声明

schema: 1
id: com.example.hold
version: 1.0.0
api: 1
entrypoint: plugin:create_mod

requires:
  - id: com.bxi.basic_actions
    version: ">=1,<2"

events:
  activate: {slot: btn_10, value: 8}

states:
  hold:
    manifest:
      label: 保持姿态
      index: 20
      group: Customer
      icon: pause

routes:
  - {from: com.bxi.basic_actions/normal, event: activate, to: hold}
  - {from: hold, event: com.bxi.basic_actions/normal, to: com.bxi.basic_actions/normal}

运行时完整状态名是 com.example.hold/hold

生命周期

方法 时机 用途
on_bind(ctx) 状态构建后一次 创建订阅、client、timer 等 ROS 资源
on_prepare(ctx, from_state) 过渡 Session 创建前 预热模型、选择动作、准备缓存;不要输出电机
on_prepare_cancel(...) 准备后的过渡被中断 撤销准备副作用
on_enter(ctx) 成为当前状态后 重置播放进度和状态私有变量
on_update(ctx, dt) 当前状态每个控制周期 生成电机输出
on_action(ctx, name) route 指定 action 时 执行动作并返回是否处理
on_exit(ctx) 成功切出时 默认保存上一个状态电机帧
on_unbind(ctx) 节点关闭 释放 on_bind 创建的资源

不要在 __init__() 中访问 ROS node 上下文;构造阶段只有工厂依赖和清单参数。

强类型参数

简单 Mod 推荐 dataclass:

from dataclasses import dataclass

from bxi_example_py_elf3.utils.state_library import PoseState

@dataclass(frozen=True)
class HoldParams:
    gain_scale: float = 0.5
    allow_motion: bool = False

class HoldState(PoseState[HoldParams]):
    Params = HoldParams

约定 factory 会自动构造参数对象,保留默认值并拒绝未知字段。显式 plugin.py 工厂仍可使用下面的逐字段 API:

states:
  hold:
    params:
      gain_scale: 0.5
      allow_motion: false
"hold": lambda state: HoldState(
    state.name,
    state.state_id,
    gain_scale=state.float_param("gain_scale", 1.0),
    allow_motion=state.bool_param("allow_motion", False),
)

支持 int_paramfloat_paramstring_parambool_param 和通用 param。未知参数会在构建时失败。

MotorFrame 和过渡能力

从一个模型直接切到另一个模型时,qposkpkd 可能在一帧内跳变,导致机器人猛地动一下。内置 Transition 有两种常用解法:

  • first_frame_switch 先取得目标状态的第一帧,再逐步增加控制增益;因此目标状态需要 EntryFrameProvider.get_entry_frame()
  • dual_running_blend 分别采样两个运行中的状态,再混合两边的完整 MotorFrame;因此被配置为动态采样的两端需要 RunningFrameProvider.sample_running_frame(),目标状态还需要 entry frame 作为初始值和回退值。

状态切换期间由 Transition 暂时接管电机输出,所以它不能直接调用会发布命令、推进模型的 on_update()get_entry_frame()sample_running_frame() 正是为 Transition 提供可独立取得的帧数据,它们分别来自 EntryFrameProviderRunningFrameProvider 协议,不是任意命名的辅助函数。

RobotControlState 仍是状态的主要抽象,它本身只要求正常运行所需的生命周期。Provider 是按 Transition 需要添加的可选能力;其他过渡方式也可以通过类似方式规定它需要的额外接口。当前内置过渡的对应关系如下:

Transition 配置 状态必须提供
instanthold 无额外 Provider
entry_gain_ramp 的目标状态 EntryFrameProvider
running_blend 的目标状态 EntryFrameProvider
running_blendsample_from: true 来源状态的 RunningFrameProvider
running_blendsample_to: true 目标状态的 RunningFrameProvider

完整的因果链、命名解释和三种简写方式见 深度感知行走 Mod 开发实录:第 9 步

需要进入帧的状态实现 EntryFrameProvider

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

需要 running_blend 动态采样时再实现 RunningFrameProvider

def sample_running_frame(self, ctx, dt, *, advance):
    qpos = self._calculate(ctx, dt, advance=advance)
    return self._motor_frame(qpos, ctx.joint_kp, ctx.joint_kd)

advance=False 必须是观察操作,不应推进 timestep 或 history。普通 on_update() 可用 _apply_frame() 发布采样结果。

速度输入

清单给状态指定 speed_profile 后,状态调用 self.get_cmd_vel(ctx)。如需额外滤波,覆盖 process_cmd_vel()。不要直接读取遥控器按钮或绕过 profile。

主动切换和 action

ctx.request_state(
    "com.bxi.basic_actions/normal",
    trigger="motion_finished",
    transition={"profile": "dual_running_blend", "duration": 0.5},
)
def on_action(self, ctx, action_name):
    if action_name != "toggle_pause":
        return False
    self.playing = not self.playing
    return True

状态 id 与界面 index

  • states.<local>.id:运行时 int32 id;默认由完整状态名稳定计算。
  • states.<local>.manifest.index:界面排序号。

重复的界面 index 会自动移动到未占用值并告警;非法 index 或真正 state id 冲突仍会阻止加载。

完整可运行步骤见 手把手自定义状态

Clone this wiki locally