-
Notifications
You must be signed in to change notification settings - Fork 12
Mod System
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: 1
enable: true
entrypoint: null
visibility: public
requires:
- {id: com.bxi.basic_actions, version: ">=1,<2"}
conflicts: []
python_exports: []
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_blendroute 决定状态切换的方向,transition 决定切换期间如何生成电机帧。之所以把两者分开,是因为同一对状态可能需要不同的安全切换方式:直接替换不连续的 qpos/kp/kd 可能产生突跳,而保持、增益渐变或双状态混合可以分别适配不同动作。
描述 Mod 的 schema/id/name/version/api/enable/entrypoint/visibility/requires/conflicts/python_exports 必须全部显式填写。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: falseenable 只能是 YAML 布尔值 true/false,不能写成字符串,也不能省略。
禁用后,该 Mod 的状态、事件、routes、actions、nodes、资源、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 可以贡献:
stateseventsroutesactionsnodesspeed_profilestransition_profilespython_exports
本地状态、事件和 profile 会自动加 id/ 前缀。系统级 transition profile(如 soft_switch)保持全局名称。
独立后台能力放在 nodes,不需要塞进某个状态的 on_bind()。in_process 节点共享主 Executor;process 节点由框架作为子进程启动、监控并有限重启。节点代码和资产仍随整个 Mod 安装、禁用和删除。
python_exports 用于把 Mod 内的顶层 Python 包临时加入导入路径。导出的目录必须包含 __init__.py,不同 Mod 不得导出同名包。节点关闭 ModRuntime 时会移除对应模块和路径。
requires 支持字符串和带版本约束的对象:
requires:
- com.example.common
- id: com.bxi.basic_actions
version: ">=1,<2"版本必须是数字点格式。加载器进行拓扑排序,并拒绝缺失依赖、版本不匹配和依赖环。
conflicts 是显式的 Mod id 列表,没有冲突时也要写空列表:
conflicts:
- com.example.legacy_wave声明是单向书写、双向生效的:只要任意一方声明冲突,双方同时启用就会在加载任何 Mod 代码前报错。冲突对象未安装或处于禁用状态时不影响当前 Mod;Mod 不能与自身冲突。
节点启动时会检查:
- 描述头的 11 个字段全部存在、无未知顶层字段,
schema/api为1,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 不依赖被删除的功能。详见 发布保护。