Skip to content

Mod System

konodoki edited this page Jul 25, 2026 · 17 revisions

Mod 系统

Mod 是当前状态侧的最小部署单元。一个目录同时携带清单、Python 代码和资产,可以在节点启动时被发现、校验、加载和整体移除。

目录结构

com.example.wave/
  mod.yaml
  state.py
  plugin.py       # 仅资源/自定义扩展需要
  assets/
    wave.onnx

内置根目录是安装包的 share/bxi_example_py_elf3/mods/。系统配置中的 mod_paths 可追加客户目录,例如 /opt/bxi/mods。根目录本身是 Mod 或其任意子目录包含 mod.yaml 都可被发现。

最小清单

id: com.example.wave
version: 1.0.0

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

states:
  wave:
    factory: state:WaveState
    label: 挥手示例
    priority: 100
    group: Customer
    icon: waving_hand

routes:
  - from: com.bxi.basic_actions/normal
    event: activate
    to: wave
    transition: soft_switch
  - from: wave
    event: com.bxi.basic_actions/normal
    to: com.bxi.basic_actions/normal
    transition: dual_running_blend

route 决定状态切换的方向,transition 决定切换期间如何生成电机帧。之所以把两者分开,是因为同一对状态可能需要不同的安全切换方式:直接替换不连续的 qpos/kp/kd 可能产生突跳,而保持、增益渐变或双状态混合可以分别适配不同动作。

schema/api 缺省为 1。没有 entrypoint 时,每个状态用 factory: module:Class;如果目录已有 plugin.py,则缺省入口为 plugin:create_mod。简写的 label/priority/index/group/icon/confirm/confirm_message 会归一化到内部 manifest,完整写法继续兼容。

跨 Mod 使用基础状态和事件时,应声明依赖:

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

启用和禁用

Mod 默认启用。要临时关闭整个 Mod,在 mod.yaml 顶层设置:

id: com.example.wave
version: 1.0.0
enable: false

enable 只能是 YAML 布尔值 true/false,不能写成字符串。省略该字段等价于:

enable: true

禁用后,该 Mod 的状态、事件、routes、资源、Transition 和 Python exports 都不会进入运行时。启用的 Mod 如果依赖已禁用 Mod,加载器会明确报错。修改 enable 后需要重启控制节点。

启动日志会分别列出已启用和已禁用的 Mod。

入口插件

只有需要 Resource、自定义 Transition 导入或依赖注入时才需要入口插件:

from bxi_example_py_elf3.mod_api import ModDefinition, ModLoadContext

from .state import WaveState


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

states 声明和 state_factories 的键必须完全相同。工厂在状态机构建阶段调用,不应自行修改全局状态。

状态参数

约定类可声明 dataclass Params,框架会自动强类型构造:

from dataclasses import dataclass

from bxi_example_py_elf3.mod_api import ProceduralState


@dataclass(frozen=True)
class WaveParams:
    amplitude: float = 0.3
    loop: bool = False

class WaveState(ProceduralState[WaveParams]):
    Params = WaveParams

高级显式工厂仍可逐字段读取:

清单:

states:
  wave:
    params:
      amplitude: 0.4
      loop: true

工厂:

"wave": lambda state: WaveState(
    state.name,
    state.state_id,
    amplitude=state.float_param("amplitude", 0.3),
    loop=state.bool_param("loop", False),
)

还可使用 int_param()string_param()param()。工厂返回后框架会调用 finish();未消费或类型错误的参数会阻止加载。

惰性资源

资源键必须是全局命名:

from bxi_example_py_elf3.mod_api import ResourceKey, ResourceLoadContext

POLICY = ResourceKey[WavePolicy]("com.example.wave/policy")


def _load_policy(context: ResourceLoadContext) -> WavePolicy:
    return WavePolicy(str(context.asset("assets/wave.onnx")))


def create_mod(context: ModLoadContext) -> ModDefinition:
    context.register_resource(POLICY, _load_policy)
    policy = context.resource(POLICY)
    return ModDefinition(
        state_factories={
            "wave": lambda state: WaveState(state.name, state.state_id, policy),
        }
    )

context.resource() 返回 ResourceHandle,不会立即加载模型。第一次 handle.get() 时才调用加载函数,之后复用缓存实例。context.asset() 只允许访问当前 Mod 的 assets/ 内存在的文件。

共享资源可以由一个无状态 Mod 提供,使用方通过 requires 依赖它。

清单贡献

一个 Mod 可以贡献:

  • states
  • events
  • routes
  • speed_profiles
  • transition_profiles
  • python_exports

本地状态、事件和 profile 会自动加 id/ 前缀。系统级 transition profile(如 soft_switch)保持全局名称。

python_exports 用于把 Mod 内的顶层 Python 包临时加入导入路径。导出的目录必须包含 __init__.py,不同 Mod 不得导出同名包。节点关闭 ModRuntime 时会移除对应模块和路径。

依赖和版本

requires 支持字符串和带版本约束的对象:

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

版本必须是数字点格式。加载器进行拓扑排序,并拒绝缺失依赖、版本不匹配和依赖环。

校验规则

节点启动时会检查:

  • schema/api 缺省或显式值为 1、合法且唯一的 Mod id。
  • enable 是布尔值,且启用 Mod 不依赖已禁用 Mod。
  • entrypoint 文件和函数存在,返回 ModDefinition
  • 依赖完整且版本匹配。
  • 清单状态与工厂一一对应。
  • 本地名称、资源键、事件槽位和字段类型合法。
  • route 的源、事件和目标存在,同一状态同一事件没有重复边。
  • 同一状态下不会有可同时触发的遥控输入冲突。
  • 所有事件至少被一条 route 使用。
  • 初始状态存在,speed profile 引用有效。

普通状态只需配置整数 manifest.priority,数值越大越靠前;没有配置时默认为 0。加载器按 priority 降序排列,同一 priority 再按完整状态名升序排列,从 0 开始生成唯一的 UI index,因此 Mod 的发现顺序不会影响界面顺序。

manifest.index 是高级固定位置选项,通常不要配置。显式 index 必须是非负整数,加载器会先保留这些位置,再把自动排序的状态填入其余位置;两个状态显式声明相同 index 会直接阻止启动。

状态内部的 id 与 UI manifest.index 不同:未显式配置 states.<name>.id 时,state id 根据完整状态名计算稳定 CRC32;真正 CRC 冲突时才要求显式指定。

当前内置布局

mods/
  com.bxi.basic_actions/   # 11 个基础动作状态和共享模型
  com.bxi.back_flip/       # protected
  com.bxi.forward_flip/    # public
  com.bxi.ballet/          # protected

基础包的事件名采用 com.bxi.basic_actions/<action>。三个独立包仍使用自己的 activate 事件。

发布属性

visibility: protected

运行时会正常加载 protected Mod;该字段供公开发布工具使用。发布工具按完整目录删除保护 Mod,并验证公开 Mod 不依赖被删除的功能。详见 发布保护

Clone this wiki locally