Skip to content

Mod System

konodoki edited this page Jul 30, 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 都可被发现。

最小清单

schema: 1
id: com.example.wave
name: 挥手示例
version: 1.0.0
api: ">=2,<3"
enable: true
entrypoint: null
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:
  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 可能产生突跳,而保持、增益渐变或双状态混合可以分别适配不同动作。

描述 Mod 的 schema/id/name/version/api/enable/entrypoint/visibility/requires/conflicts/python_exports/runtime_requirements 必须全部显式填写。entrypoint: null 表示每个状态使用 factory: module:Class 的约定式加载;高级插件必须明确写出 entrypoint: plugin:create_mod,框架不会根据目录中是否存在 plugin.py 猜测入口。简写的 label/priority/index/group/icon/confirm/confirm_message 会归一化到内部 manifest,完整写法继续兼容。

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

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

启用和禁用

要临时关闭整个 Mod,把显式的 enable 改为 false

enable: false

enable 只能是 YAML 布尔值 true/false,不能写成字符串,也不能省略。

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

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

入口插件

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

from bxi_example_py_elf3.framework.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.framework.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.framework.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, loading="eager")
    policy = context.resource(POLICY)
    return ModDefinition(
        state_factories={
            "wave": lambda state: WaveState(state.name, state.state_id, policy),
        }
    )

register_resource(..., loading="lazy") 在第一次 handle.get() 时调用加载函数; loading="eager" 在所有 Mod 完成资源注册后、控制循环启动前预加载。 loading 默认是 lazy,内置 Mod 会显式填写。两种方式都会缓存实例, context.resource() 始终只返回 handle。context.asset() 只允许访问当前 Mod 的 assets/ 内存在的文件。预加载失败会直接阻止框架启动,不会把模型 初始化延迟带进实时控制循环。

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

清单贡献

一个 Mod 可以贡献:

  • states
  • events
  • routes
  • actions
  • nodes
  • speed_profiles
  • transition_profiles
  • python_exports
  • runtime_requirements
  • runtime_profiles

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

独立后台能力放在 nodes,不需要塞进某个状态的 on_bind()in_process 节点共享主 Executor;process 节点由框架作为子进程启动、监控并有限重启。节点代码和资产仍随整个 Mod 安装、禁用和删除。

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

runtime_requirements 声明 Python import、ROS package 和系统动态库依赖。缺失的 Mod 或节点会进入 unavailable,检查不会自动安装软件。Mod 自带依赖按平台放在 vendor/python/<平台-Python ABI>vendor/lib/<平台>,跨平台纯 Python 包可放在 vendor/python/common。运行时只注入当前平台兼容目录,然后才回退宿主环境;其他架构的 bundle 会被忽略并产生明确警告。

runtime_profiles 是按节点选择的可选上限:简单 Mod 不声明时继续使用宿主环境;轻量 依赖可使用 Vendor;需要把 Python、C++ 程序和厂商用户态动态库随整个 Mod 目录迁移时 使用 Portable。Portable 只支持独立进程,不改变状态类和主控制进程的 Python 环境。

依赖和版本

框架公开独立的 Mod API 版本,当前为 1.1.0。每个 Mod 用 api 声明可兼容的框架 API 范围:

api: ">=2,<3"

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

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

Mod 版本必须是数字点格式;apirequires[].version 使用相同的 逗号分隔比较约束。加载器进行拓扑排序,并拒绝框架 API 不兼容、缺失依赖、 版本不匹配和依赖环。

Mod 冲突

conflicts 是显式的 Mod id 列表,没有冲突时也要写空列表:

conflicts:
  - com.example.legacy_wave

声明是单向书写、双向生效的:只要任意一方声明冲突,双方同时启用就会在加载任何 Mod 代码前报错。冲突对象未安装或处于禁用状态时不影响当前 Mod;Mod 不能与自身冲突。

校验规则

节点启动时会检查:

  • 描述头的 12 个字段全部存在、无未知顶层字段,schema1api 与当前框架兼容,name、id 和版本合法。
  • enable 是布尔值,且启用 Mod 不依赖已禁用 Mod。
  • 同时启用的 Mod 不命中任意一方的 conflicts 声明。
  • entrypoint 文件和函数存在,返回 ModDefinition
  • 依赖完整且版本匹配。
  • 清单状态与工厂一一对应。
  • 本地名称、资源键、事件槽位和字段类型合法。
  • route 的源、事件和目标存在,action 的源和事件存在且包含非空的 manifest.label;两者字段不混用。
  • node entrypoint 可调用,execution/lifecycle 合法,State 生命周期引用的状态存在。
  • 同一状态同一事件没有重复的 route/action。
  • 同一状态下不会有可同时触发的遥控输入冲突。
  • 所有事件至少被一条 route 或 action 使用。
  • 初始状态存在,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