Skip to content

Custom State

konodoki edited this page Jul 28, 2026 · 27 revisions

自定义状态

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

核心状态模型:RobotControlState

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

from __future__ import annotations

from bxi_example_py_elf3.framework.mod_api import RobotControlContext, RobotControlState


class HoldState(RobotControlState):
    def on_update(self, ctx: RobotControlContext, dt: float) -> None:
        ctx.set_motor_target(
            self._motor_frame(ctx, ctx.pos_last, ctx.kp_last, ctx.kd_last)
        )

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

便捷状态基类

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

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

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

schema: 1
id: com.example.hold
name: 保持姿态
version: 1.0.0
api: ">=1,<2"
enable: true
entrypoint: null
visibility: public
requires: []
conflicts: []
python_exports: []
runtime_requirements:
  python: []
  ros: []
  system: []
states:
  hold:
    factory: state:HoldState
    label: 保持姿态
    priority: 100
from bxi_example_py_elf3.framework.mod_api import PoseState


class HoldState(PoseState):
    def gains(self, ctx):
        return ctx.kp_last, ctx.kd_last

    def target_position(self, ctx):
        return ctx.pos_last

PoseStateon_update() 内部会调用 target_position() 和状态自己的 gains(),把结果 写入状态长期复用的 MotorFrame,再调用 ctx.set_motor_target(frame)。因此上例可以直接返回 已有数组,不需要 .copy()。它还提供稳定的进入帧和运行帧,可直接配合需要 EntryFrameProviderRunningFrameProvider 的 Transition。

框架不提供隐藏的全局 kp/kd。上例明确选择沿用上一控制帧的增益,适合从正常受控状态进入;如果允许从零力矩状态进入,上一帧增益可能全为零,状态必须改为提供经过真机验证的自身增益。也可以不覆盖 gains(),在 self.frame(..., kp=..., kd=...) 中逐帧显式传入。

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

显式工厂

from bxi_example_py_elf3.framework.mod_api 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
name: 保持姿态
version: 1.0.0
api: ">=1,<2"
enable: true
entrypoint: plugin:create_mod
visibility: public
requires:
  - id: com.bxi.basic_actions
    version: ">=1,<2"
conflicts: []
python_exports: []
runtime_requirements:
  python: []
  ros: []
  system: []

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

states:
  hold:
    manifest:
      label: 保持姿态
      priority: 100
      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) actions 规则触发时 执行动作并返回是否处理
on_exit(ctx) 成功切出时 默认保存上一个状态电机帧
on_unbind(ctx) 节点关闭 释放 on_bind 创建的资源

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

强类型参数

简单 Mod 推荐 dataclass:

from dataclasses import dataclass

from bxi_example_py_elf3.framework.mod_api 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: RobotControlContext) -> MotorFrame:
        return self._motor_frame(
            ctx.pos_last,
            ctx.kp_last,
            ctx.kd_last,
        )

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

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

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、priority 与界面 index

  • states.<local>.id:运行时 int32 id;默认由完整状态名稳定计算。
  • states.<local>.manifest.priority:界面顺序优先级,数值越大越靠前,默认 0
  • states.<local>.manifest.index:框架生成的界面位置;仅在必须固定绝对位置时显式配置。

相同 priority 按完整状态名升序排列。两个显式 index 重复、index 非法或真正的 state id 冲突都会阻止加载。

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

Clone this wiki locally