-
Notifications
You must be signed in to change notification settings - Fork 12
Custom State
自定义状态必须由某个 Mod 提供。最小组成只有状态类和 mod.yaml;共享资源或注册高级扩展时再增加 plugin.py。完整渐进路线见 Mod 状态开发进阶指南。
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: 20from bxi_example_py_elf3.utils.state_library import PoseState
class HoldState(PoseState):
def target_position(self, ctx):
return ctx.pos_last.copy()PoseState 的 on_update() 内部会调用 target_position(),用默认 kp/kd 构造 MotorFrame,再调用 ctx.set_motor_target()。它还提供稳定的进入帧和运行帧,因此可直接配合需要 EntryFrameProvider 或 RunningFrameProvider 的 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_param、float_param、string_param、bool_param 和通用 param。未知参数会在构建时失败。
从一个模型直接切到另一个模型时,qpos、kp 或 kd 可能在一帧内跳变,导致机器人猛地动一下。内置 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 提供可独立取得的帧数据,它们分别来自 EntryFrameProvider 与 RunningFrameProvider 协议,不是任意命名的辅助函数。
RobotControlState 仍是状态的主要抽象,它本身只要求正常运行所需的生命周期。Provider 是按 Transition 需要添加的可选能力;其他过渡方式也可以通过类似方式规定它需要的额外接口。当前内置过渡的对应关系如下:
| Transition 配置 | 状态必须提供 |
|---|---|
instant、hold
|
无额外 Provider |
entry_gain_ramp 的目标状态 |
EntryFrameProvider |
running_blend 的目标状态 |
EntryFrameProvider |
running_blend 且 sample_from: true
|
来源状态的 RunningFrameProvider
|
running_blend 且 sample_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。
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-
states.<local>.id:运行时 int32 id;默认由完整状态名稳定计算。 -
states.<local>.manifest.index:界面排序号。
重复的界面 index 会自动移动到未占用值并告警;非法 index 或真正 state id 冲突仍会阻止加载。
完整可运行步骤见 手把手自定义状态。