Skip to content

Mod System

konodoki edited this page Jul 23, 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: 挥手示例
    index: 20
    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

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

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

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

入口插件

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

from bxi_example_py_elf3.utils.mod_system 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.utils.state_library 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.utils.mod_system 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/ 内存在的文件,并把真实使用路径记录到 ResourceManager.loaded_paths;热重载器另行监控整个 Mod 根目录。

共享资源可以由一个无状态 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 不得导出同名包。热重载关闭旧运行时时会移除对应模块和路径。

依赖和版本

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

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

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

校验规则

日常使用:

cd ~/bxi_rl_controller_ros2_example
./tools/bxi-mod new com.example.wave --state wave --template procedural
./tools/bxi-mod validate path/to/com.example.wave
./tools/bxi-mod inspect path/to/com.example.wave

CLI 的仓库实现位于 tools/bxi-mod,可在仓库根目录直接执行,不要求预先安装。构建时它也会安装到前缀的 bin/;执行 source install/setup.bash 后才可使用裸命令 bxi-mod。它不是机器人运行时 Python 包的一部分。

编辑器可使用安装到 share/bxi_example_py_elf3/schema/mod.schema.json 的 JSON Schema。

启动或热重载时会检查:

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

manifest.index 必须是非负整数。发生重复时,首次声明者保留原值,后续状态从原值加一开始寻找未占用 index,并输出 RuntimeWarning,不会终止加载。分配器会提前保留所有合法显式 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