robonix

Robonix 具身智能操作系统

犀照世界 灵通万物 为机器筑心 为具身立智

Robonix 从系统层面构造具身智能的运行时底座,将 AI 模型与异构硬件软硬解耦,使模型成为可在运行时加载与组合的程序,朝"一次训练,任意机器部署运行"的方向演进。

围绕"感知–理解–规划–行动"全链路中的数据处理与环境交互等共性问题,Robonix 设计了"感知–互联–认知–控制"系统服务框架,降低模型与技能的开发及运行成本,推动软硬件独立演进的生态。

Robonix 是《具身智能操作系统技术白皮书》(CCF 泛在操作系统开放社区,2026)的参考实现。白皮书提出 EAIOS(Embodied AI Operating System)架构,采用"原语–服务–技能–任务"四级抽象体系。更多背景参阅白皮书原文EAIOS 架构背景

本手册导读

  • 快速上手——从克隆代码到运行 Tiago 仿真 Demo 的完整流程
  • 系统全景——控制平面与数据面、组件关系、一次请求的完整链路
  • Crate 索引——各 Rust crate 的职责与关键 API
  • 命名空间与接口模型——namespace 树、契约 ID(contract_id)、rust/contracts 与 robonix-codegen、多传输、通道协商
  • 接口目录——primitive(Primitive,原语)与 service(Service,服务)分目录说明
  • 接入指南——硬件厂商与算法开发者将自身组件接入 Robonix 的完整流程

EAIOS 架构背景

Robonix 是 EAIOS(Embodied AI Operating System)架构的参考实现。EAIOS 由 CCF 泛在操作系统开放社区于 2026 年发布的《具身智能操作系统技术白皮书》中提出,其核心目标是在机器人系统层面建立统一的抽象体系,实现 AI 模型与硬件之间的软硬解耦。

白皮书原文:gitlink.org.cn/zone/uos/source/292

四级抽象

EAIOS 采用"原语–服务–技能–任务"四层抽象体系(白皮书 §3.2),实现从高层语义目标到底层硬件控制的系统化分层建模。

原语(Primitive)构成硬件抽象层,定义"抽象机器人"的标准化接口。白皮书将原语分为两类:感知原语(Perception Primitive)负责采集原始观测数据(相机 RGB/深度、IMU、关节编码器、力觉触觉等);动作原语(Action Primitive)定义确定性的执行指令(底盘速度控制、机械臂关节控制、夹爪开合等)。不同厂商的同类硬件实现同一套原语接口,上层应用与模型通过原语与抽象机器人交互,无需感知底层差异。

服务(Service)是由操作系统统一注册、调度与管理的功能组件,为任务的决策与执行提供全链路能力支撑。白皮书定义的典型服务包括:语义地图、空间地图、任务规划、方案推演、决策、数据采集、物体识别、校准、记忆、结果反馈,以及人机交互与协同服务。开发者遵循统一的接口规范开发并接入服务,系统在运行时根据任务需求动态完成服务的实例化与编排。

技能(Skill)封装具有特定语义的、可复用的行为操作序列,连接高层任务与底层原语。白皮书将技能分为两种形式:基本技能以独立执行单元注册到系统中(如预训练的 VLA 模型、ROS 2 执行算法等),具有固定运行实现的静态技能;RTDL 技能则是在任务执行过程中由系统动态生成的控制流程描述,在运行时由 RTDL 解释器调度与组合执行。当某类任务的执行流程在多次运行中被验证为稳定可靠时,系统可将其固化为可复用的技能存入技能库。

任务(Task)是具身智能系统运行的顶层逻辑单元,由用户或系统自主提出,具备明确的目标状态与终止条件。系统使用任务描述语言 RTDL(Robot Task Description Language)对任务方案进行形式化描述,经由完整的生命周期管理:任务规划(生成 RTDL 方案)→ 方案推演(世界模型验证可行性与安全性)→ 决策执行(调度器分配资源、协调技能与原语)→ 结果反馈(评估执行效果并反馈至系统闭环)。

Robonix 中的对应

Robonix 作为 EAIOS 架构的参考实现,其组件与四级抽象的对应关系如下:

EAIOS 层白皮书定义Robonix 对应
原语硬件抽象接口:感知原语(相机、IMU 等)与动作原语(底盘、机械臂等)robonix/primitive/* 下的接口契约与 Provider 实现(如 tiago_bridge 提供底盘、相机、力觉接口)
服务操作系统统一注册与调度的功能组件:感知、认知、互联、控制等robonix/service/* 下的默认服务(Cognition / Memory / Map / 数据采集 / 系统监控等),以及按部署场景注册的场景服务节点
技能可复用行为单元:基本技能(预训练 VLA 等)与 RTDL 技能(运行时动态生成)技能(进程形态的基本技能,如 VLA 策略,通过 MCP 暴露执行入口)+ 结构化技能图(对应 RTDL 技能,规划中),均独立于包注册到 Atlas
任务用户请求的结构化表达,经由规划→推演→决策→执行→反馈的完整生命周期Liaison 把客户端输入构造为任务(Task)发给 Pilot;Pilot 的 ReAct 推理循环把任务分解为 RTDL 方案下发 Executor;Executor 承担任务执行与工具分发;方案推演与结果反馈为规划中功能

Liaison 对应白皮书 §3.4.3 的人机交互与协同服务,负责多模态输入的语义解析与任务生成。Atlas 作为控制平面(节点注册、接口声明、通道协商、技能库),是 Robonix 特有的实现机制,不直接对应 EAIOS 四级抽象中的某一层,而是贯穿各层的基础设施。

系统作用域与非目标

Robonix 聚焦于具身智能系统的运行时框架。以下事项不在当前作用域内,或属于规划中的集成方向。

启动与上电

Robonix 的"启动"指功能启动:rbnx start 管理 Linux 进程生命周期,拉起对应本体的驱动(Primitive Node)、系统服务(Service)与技能节点(技能)。硬件级上电管理(例如主控板向关节控制板发送电源指令)不在当前作用域内,未来可由启动管理层或专用的电源管理原语承接。

训练与部署

Robonix 现阶段的核心目标是部署(inference / execution),即以 技能 形态运行预训练模型(VLA、RL 策略、VLM 等)。训练与数据采集链路不在当前作用域内。

规划中的集成方向为 LeRobot(Hugging Face)具身数据采集与微调框架。Robonix 的接口设计将与 LeRobot 数据规范对齐,使 Robonix 上运行的机器人可直接作为 LeRobot 数据源,并加载 LeRobot 训练产物作为 技能 部署。对应的系统服务为"数据采集服务"(robonix/service/common/data_collection,规划中)。

控制面定位

Atlas 仅承担注册、发现、协商与技能库功能,不参与数据面转发,亦不强制进程监控或编排调度。所有数据面通信由 Provider 与 Consumer 在协商端点后直连完成。

快速上手

5 分钟跑起一个完整的 Tiago 仿真 + VLM 对话 demo。

1. 前置

需要:

  • Linux x86_64 + Rust stable + Python ≥ 3.10
  • Docker + Compose v2(仿真容器)
  • 一个 OpenAI 兼容的 VLM API key(Qwen / GPT-4o / Gemini / Claude via 兼容网关 都行)

推荐:NVIDIA GPU + nvidia-container-toolkit(Webots 3D 渲染);只跑对话不跑仿真可跳过 Docker。

2. 构建

git clone --recursive https://github.com/syswonder/robonix
cd robonix/rust
make install

make install 会:

  • 编译并把 rbnxrobonix-atlasrobonix-pilotrobonix-executorrobonix-liaisonrobonix-codegen 装到 ~/.cargo/bin/
  • 自动登记当前 clone 为 robonix 源码根目录,让其他位置的包做 codegen 时能找到 contracts/IDL(见 Build 与 Codegen

Python 依赖按包内的 package_manifest.yaml 自行管理;rbnx start 在 spawn driver 子进程前会把包的 rbnx-build/codegen/proto_gen 加进 PYTHONPATH

3. 配 VLM

rbnx boot 通过环境变量读取 VLM endpoint(manifest 里以 ${VLM_*} 形式引用):

# OpenAI(或任意 OpenAI 兼容网关)
export VLM_API_KEY=sk-xxx
export VLM_BASE_URL=https://api.openai.com/v1
export VLM_MODEL=gpt-5.4-mini

# Qwen(阿里 DashScope 提供 OpenAI 兼容网关)
export VLM_API_KEY=sk-xxx
export VLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
export VLM_MODEL=qwen3-vl-plus

把这几行写进 ~/.bashrc / ~/.zshrc 或部署目录的 .env 都可以。

4. 跑起来

整个栈分两个终端,仿真和 Robonix 系统服务/驱动各占一个:

# T1:仿真容器(Webots + ROS 2 + Nav2,docker compose 栈,Ctrl-C 停)
bash examples/webots/sim/start.sh

# T2:Robonix 系统服务 + Tiago 驱动 + Nav2 wrapper
export VLM_BASE_URL=https://api.openai.com/v1
export VLM_API_KEY=sk-...
export VLM_MODEL=gpt-5.4-mini
cd examples/webots
rbnx boot        # atlas + executor + pilot + 4 个 driver + nav2

T1 不是 Robonix 包——它就是个 docker compose 栈。Robonix 不管它的生命周期。

T2 的 rbnx bootexamples/webots/robonix_manifest.yaml,按声明顺序起 system 块(atlas / executor / pilot)和所有 primitive / service 包。driver 进程跑在仿真容器里(通过 docker exec),与 Webots 共享同一份 DDS graph,host 上不需要 ROS 2 环境。

rbnx boot 报告 ✓ 7 component(s) up 后即可进入下一步。具体启动时序见 系统部署与启动流程

5. 跟机器人对话

第三个终端:

rbnx caps          # 列出所有注册的 capability + interface
rbnx tools         # LLM 看到的工具列表(MCP transport 子集)
rbnx chat          # 直连 pilot 的 ratatui TUI

rbnx chat 里输入问题即可。典型一轮:

You:   what can you see?
Pilot: I'll capture a current RGB camera snapshot to see what's in view.
       > [r0] camera_snapshot({})
Pilot: The camera shows a potted plant near a beige wall …

Esc 中断当前推理(AbortSession)。退出 chat:Ctrl+C

清栈:

bash examples/webots/sim/stop.sh   # 一键 kill 容器内 driver + rbnx boot + docker compose down

只起子集 / 调试

# 跳过 system 块(atlas/pilot 等已外部运行时)
rbnx boot --skip-system

# 单独起一个包(调试)
rbnx start -p ./primitives/tiago_chassis

去看看里面发生了什么

栈跑起来之后:

rbnx caps                    # 所有注册的 capability(旧别名 rbnx nodes 仍可用)
rbnx tools                   # agent 可见的 MCP 工具
rbnx describe --cap <id>     # 某个 cap 的 CAPABILITY.md 全文
rbnx channels                # 当前活跃的 consumer→provider 通道
rbnx inspect                 # 完整 runtime 快照(JSON)

下一步

  • 系统全景——控制面 / 数据面、一次请求的完整链路
  • 接入指南——把自己的硬件或算法接入 Robonix
  • Build 与 Codegen——包作者必读(rbnx setuprbnx codegen、自定义 contract)
  • 接口目录——primitive/* 原语与 service/* 服务的契约定义

常见问题

Webots 没显示 GUI:确认 echo $DISPLAY 非空,运行 xhost +local:docker

Webots 卡顿:确认 nvidia-smi 可用且装了 nvidia-container-toolkit;否则跑在纯 CPU 软光栅上会很慢。

MCP 工具暂时不可见(rbnx tools 空):T1 仿真 + T2 rbnx boot 全部就绪需 ~10 s,等一会儿;如果一直空,看 rbnx-boot/logs/<name>.log

LLM 调工具被 422 拒绝:driver 端 schema 与函数签名不一致。driver 应该用 mcp_contract 装饰器 + codegen IO 类,schema 由契约自动决定,不要手写。详见 命名空间与接口模型 · MCP 与 mcp_contract

LLM 跑几轮就停了,但任务没完成:Pilot 的 system prompt 已经包含 "persistence" 段落要求 LLM 持续迭代直到任务可验证完成;如果还停,多半是 LLM 模型本身倾向短回合(换更强的 reasoner,或者 prompt 里追加任务可验证条件)。

VLM 报错但 pilot 没崩:符合预期——错误以普通消息出现在 chat 里,session 不死,直接发下一条即可。

系统全景

系统架构

能力与能力接口

Robonix 把所有可发现的功能统一抽象成"能力(capability)",能力包括原语、服务、技能。

命名空间

每个 capability 注册时声明一个 namespace 前缀,所有它的 interface 的 contract_id 必须从这个前缀开始。一级命名空间分四类:

前缀含义谁实现
robonix/primitive/*原语:低层硬件抽象(chassis、camera、lidar、gripper 等)设备驱动包
robonix/service/*服务:场景级算法/能力(slam、navigation、semantic_map 等)Robonix 提供默认实现,可被替换,可自定义场景服务并自行实现
robonix/skill/*技能:封装了特定语义功能、调用原语和服务来完成用户特定任务的可复用功能单元用户/算法开发者自行定义接口和进行实现
robonix/system/*系统(服务):Robonix 自身的核心服务(pilot、executor、liaison、memory)仓库内置,不可替换

契约(contract)

contract 是 interface 的身份证——contract_id 在整个系统里全局唯一,能力提供方和消费方都按 contract_id 对话,跟具体进程、传输无关。每个 contract 由一份 TOML 描述:

# rust/contracts/primitive/chassis_move.v1.toml
[contract]
id      = "robonix/primitive/chassis/move"
version = "1"
kind    = "primitive"

[io.srv]
srv = "prm_chassis/srv/MoveCommand"

[mode]
type = "rpc"

字段含义:

  • [contract]:身份。id 采用 Path-style 命名空间,如 robonix/primitive/chassis/move,version 为接口版本,kind 为这个能力所属于的大类。
  • [io.srv][io.msg]:载荷形状。指向 ROS IDL(std_msgs/Emptyprm_chassis/MoveCommand)。官方 contract(在 robonix 源码仓库的 capabilities/)的 IDL 路径解析到 robonix 源码仓库的 rust/crates/robonix-interfaces/lib/包内 contract(在 package 的 capabilities/)解析到包自己的 capabilities/srv/capabilities/msg/
  • [mode]:交互形态:
    • rpc:一元 RPC,如 ROS2 service 或 grpc unary。
    • rpc_server_stream:服务器端流式 RPC,目前仅支持生成 grpc 对应的通信 stub,ROS2 暂不原生支持。
    • rpc_client_stream:客户端流式 RPC,目前仅支持生成 grpc 对应的通信 stub,ROS2 暂不原生支持。
    • rpc_bidirectional_stream:双向流式 RPC,目前仅支持生成 grpc 对应的通信 stub,ROS2 暂不原生支持。
    • topic_out:流式发布者,如 ROS2 topic 发布或 grpc server-stream(且 request 为空)。
    • topic_in:流式订阅者,如 ROS2 topic 订阅或 grpc client-stream(且 response 为空)。

contract 文件本身不包含传输信息——同一个 robonix/primitive/camera/rgb 既可以由一个 ROS 2 桥提供,也可以由一个独立的 gRPC 服务提供,也可以由一个 MCP 服务或共享内存提供。不过受限于不同具体通信模式的情况,某些 contract 只能在特定传输上使用(如 rpc_server_stream, rpc_client_stream, rpc_bidirectional_stream 当前仅支持 grpc,而无法原始翻译为 ROS2/MCP 的通信,ROS2 action codegen 暂未支持)。

codegen:契约 → 代码

rbnx codegen(详见 Build 与 Codegen)从包的 capabilities: 列表读 contract,生成 stub(通过参数开关):

  1. proto / Python stubs:把 ROS IDL 翻成 protobuf,再用 grpc_tools.protoc 生成 proto_gen/*_pb2.py。包代码 from proto_gen.prm_chassis_pb2 import MoveCommand 即可。
  2. MCP types--mcp):把 contract 的 IO 类型生成为 pydantic-like 类,含 .json_schema().model_validate(dict).model_dump()。落到 <pkg>/robonix_mcp_types/

codegen 全部产出到 <pkg>/rbnx-build/codegen/(proto stubs)和 <pkg>/robonix_mcp_types/(MCP types)。rbnx start 在 spawn 子进程前会把这两个目录加进 PYTHONPATH

通道(channel)

consumer 要使用某个 cap 的 interface 时,先调 Atlas 的 ConnectCapability(consumer_id, namespace_filter, transport),Atlas 选一个匹配的 provider,记录这条 consumer→provider 的 channel,并把 endpoint 返回给 consumer。consumer 拿到 endpoint 自己 dial(gRPC)或订阅(ROS)或起 HTTP client(MCP)。channel 在 Atlas 侧只是一条记账记录,可以用 rbnx channels 查看。

不需要走 Connect 也能用:所有的只读发现都用 QueryCapabilities 直接拿 endpoint。Connect 主要给系统服务用——pilot / executor 启动时连一次 cognition 和 memory,channel 帮 atlas 跟踪谁在用谁,方便 rbnx inspect 时给运维一个完整图。

Atlas 能力目录

Atlas 是 Robonix 的唯一控制平面——所有进程(不管是 Robonix 自身的系统服务,还是用户提供的 primitive / service / skill 包)启动时第一件事就是连 Atlas,把自己的能力(capability)登记上去;其他进程要找它们,也只能通过 Atlas。Atlas 本身不转发数据面流量,它只做"目录 + 通道协商"。

数据模型:capability + interface

一个 capability instance(能力实例)是 Atlas 里的最小注册单位,对应一个进程或一台设备的某种能力。每个 capability 有:

  • capability_id:实例的反向 DNS 名字,进程级唯一。例如 com.robonix.primitive.tiago_chassis。空值时 Atlas 分配 com.robonix.ephemeral.<uuid>
  • namespace:能力所属命名空间前缀,例如 robonix/primitive/chassis。后续声明的所有 interface 的 contract_id 必须在这个前缀下,Atlas 强制校验。
  • capability_md_path:可选。指向包根目录下的 CAPABILITY.md 绝对路径。Pilot 在构造 system prompt 时把这条路径列出来,由 LLM 通过 executor 的 read_file builtin 按需懒加载(详见下文"capability 文档懒加载")。
  • 0 到 N 个 interface:每个 interface 把一个契约(contract_id)绑到一种传输(transport)和一个端点(endpoint),同时携带传输专属参数(TransportParams)。

一个进程典型的注册流程:

RegisterCapability(capability_id, namespace, capability_md_path)
  └─ DeclareInterface(capability_id, contract_id, transport, endpoint, params)
  └─ DeclareInterface(capability_id, contract_id, transport, endpoint, params)
  └─ ...
Heartbeat(capability_id) ← 每 N 秒续约

同一个 capability 下的多个 interface 可以走不同传输。例如 tiago_chassis 既可以暴露 robonix/primitive/chassis/state(MCP,给 LLM 调)又暴露 robonix/primitive/chassis/odom(ROS 2,给 SLAM 订阅)。

三种传输

Atlas 不在乎传输细节,但它必须能把 provider 的 endpoint 完整告诉 consumer。TransportParams 是个 oneof,按传输各塞一份元数据:

传输TransportParams.kind典型 endpoint用途
GrpcGrpcParams { proto_file, service_name, method }host:port系统服务(pilot / executor / vlm)、PRM 的二进制流式接口
Ros2Ros2Params { qos_profile }冲突时采用/rbnx/ch/<uuid>,其他情况直接用能力发起人提供的 endpoint容器内 ROS 节点间通信
McpMcpParams { description, input_schema_json }host:port (HTTP) 或 stdio://cmdLLM 可调工具

RPC 接口一览

Atlas 服务定义在 rust/proto/atlas.proto,关键 RPC:

RPC调用方作用
RegisterCapabilityProvider登记 capability_id + namespace + capability_md_path
DeclareInterfaceProvider把一个 contract_id 绑到一种 transport+endpoint
HeartbeatProvider续约,超时(默认 30 s)后该 capability 被驱逐
UnregisterCapabilityProvider主动注销(也可以让心跳超时)
QueryCapabilitiesConsumercapability_id / namespace / transport 过滤检索
QueryCapabilityMdConsumer读取 capability_md_path 指向的 markdown 内容(Pilot 不用——LLM 用 read_file 懒加载更省 token)
ConnectCapabilityConsumer提交"我要用 cap X 的 contract Y 走 Z 传输",Atlas 记录通道并返回 endpoint
DisconnectCapabilityConsumer释放通道(Atlas 仅做记账)
InspectAtlas调试一次性 dump 当前所有 capabilities + interfaces + channels(JSON)

Connect / Disconnect 只是 Atlas 侧的记账:真正的数据面连接(gRPC dial、ROS topic sub、MCP HTTP 客户端)由 consumer 自己用拿到的 endpoint 建。

注册流程示例:tiago_chassis driver

examples/webots/primitives/tiago_chassis/chassis_driver/driver.py 的注册顺序(伪代码):

stub = AtlasStub(channel)

# 1. 登记一个 capability instance
stub.RegisterCapability(RegisterCapabilityRequest(
    capability_id     = "com.robonix.primitive.tiago_chassis",
    namespace         = "robonix/primitive/chassis",
    capability_md_path = "/abs/path/to/tiago_chassis/CAPABILITY.md",
))

# 2. 登记两个 MCP interface
def state(msg: Empty) -> RobotState: ...     # @mcp_contract 装饰
def move(msg: MoveCommand) -> String: ...    # @mcp_contract 装饰

for fn, port in [(state, 50111), (move, 50112)]:
    stub.DeclareInterface(DeclareInterfaceRequest(
        capability_id = "com.robonix.primitive.tiago_chassis",
        contract_id   = fn._robonix_contract_id,    # mcp_contract 写入
        transport     = Transport.Mcp,
        endpoint      = f"127.0.0.1:{port}",
        params        = TransportParams(mcp=McpParams(
            description       = (fn.__doc__ or "").strip(),
            input_schema_json = fn._robonix_input_cls.json_schema(),
        )),
    ))

# 3. 后台心跳
asyncio.create_task(heartbeat_loop(stub, "com.robonix.primitive.tiago_chassis"))

contract_id(robonix/primitive/chassis/staterobonix/primitive/chassis/move)必须在 namespace robonix/primitive/chassis 前缀下,Atlas 在 DeclareInterface 时会校验。

capability 文档懒加载

每个包都鼓励在根目录写一份 CAPABILITY.md,描述:自己提供的工具、推荐使用模式("先 snapshot 再 reason 再下指令"之类)、参数语义、典型陷阱("navigate 是阻塞的,交互场景别用")。

注册时通过 capability_md_path 字段把这份文档的绝对路径告诉 Atlas。Pilot 在每个 turn 构造 system prompt 时调 discovery::cap_md_index(atlas) 拉一遍,把每条 cap 的 path 用一行列在 system prompt 末尾:

## Capability docs (lazy-load via `read_file`)
Each capability below ships a CAPABILITY.md ...

- `com.robonix.primitive.tiago_chassis` (robonix/primitive/chassis): `/.../tiago_chassis/CAPABILITY.md`
- `com.robonix.primitive.tiago_camera`   (robonix/primitive/camera):  `/.../tiago_camera/CAPABILITY.md`
- ...

具体内容不进 system prompt——LLM 只在确认要用某个 cap 时,通过 executor 的 read_file builtin 按需读。这种懒加载策略主要是为了控制 system prompt 大小(已经观察到 tool 描述本身就是大头 token 消耗);同时让 cap 作者可以写很长的文档而不用担心污染所有 turn 的 prompt。

系统部署与启动流程

本页讲清楚一次完整的 Robonix 部署在终端里发生了什么——从 rbnx boot 第一行 log 到 rbnx chat 收到第一个工具调用之间的所有事件。读完应该能:自己写一份 deploy manifest、看懂 rbnx-boot/logs/ 里的输出、定位"组件起不来"或"LLM 看不到工具"这类问题在哪个阶段。

两层 manifest

文件谁读范围
<deployment>/robonix_manifest.yamlrbnx boot一次部署:列系统服务的配置、哪些设备、服务,对应的代码包路径,实例名……
<package>/package_manifest.yamlrbnx start单个包:build 命令、start 命令、提供哪些 capability、依赖哪些其他包

Webots Tiago 例子的两个终端

examples/webots/ 是仓库内置的端到端样例——驱动在仿真容器里跑,Robonix 系统服务和 Pilot 在主机上跑。部署 layout 见 快速上手。整个栈分两个终端启动:

# T1:仿真环境(Ctrl-C 停)
bash examples/webots/sim/start.sh

# T2:Robonix(atlas + executor + pilot + 4 个 driver + nav2)
cd examples/webots
export VLM_BASE_URL=https://api.openai.com/v1
export VLM_API_KEY=sk-...
export VLM_MODEL=gpt-5.4-mini
rbnx boot

仿真容器(Webots + ROS 2)不是 Robonix 包,它就是个 docker compose 栈。Robonix 不管它的生命周期;T1 终端 Ctrl-C 即可停。

driver 进程(chassis、camera、lidar、nav2)跑在仿真容器里面——rbnx boot 通过 docker exec robonix_tiago_sim ... 把 Python driver 起在容器进程空间,让它们与 Webots 共享同一份 DDS graph。host 上不需要 ROS 2 环境。

整个栈起来后开第三个终端:

rbnx caps        # 列出所有注册的 capability 和它们的 interface
rbnx tools       # LLM 看到的工具列表(MCP transport 子集)
rbnx chat        # ratatui TUI,直连 Pilot

清栈:bash examples/webots/sim/stop.sh——脚本会一并 kill 容器内 driver 进程、rbnx boot 子进程组、并 docker compose down 仿真栈。

rbnx boot 生命周期

rbnx boot 主流程在 rust/crates/robonix-cli/src/cmd/deploy.rs,七步:

  1. 解析 manifest:读 robonix_manifest.yaml,展开 ${VAR} 环境变量,校验声明的 package 存在、capability 引用合法。
  2. 初始化日志目录:默认 <manifest-dir>/rbnx-boot/logs/,每个组件一个 <name>.log 文件。可用 --log-dir 改路径。
  3. 起 system 块:按 system: 下的字段顺序起 atlas → executor → pilot → liaison → memory → ……。
  4. 轮询 atlas 就绪:调 QueryCapabilities("") 直到返回非错(atlas 完全起来需 ~200 ms)。
  5. 逐个起 primitive / service / skill:按 manifest 声明顺序,一条一条 spawn,每条等它在 atlas 里 RegisterCapability 上来才进入下一条
  6. driver init dance:如果新注册的 cap 声明了 */driver 接口(如 robonix/primitive/lidar/driver),调一次 LifecycleDriver.Driver(CMD_INIT, config_json) 完成硬件初始化。
  7. 守候:sit-on-Ctrl-C/SIGTERM 循环,收到信号后向所有子进程发 SIGTERM、等回收,再退出。

每个子进程的 stdout / stderr 重定向到 <log-dir>/<name>.log,前台终端只看 [deploy] 自己的状态行。组件 panic 或 register 超时时 rbnx boot 会打印失败摘要并指向对应 log 文件。

时间线大致如下(host = 主 Robonix 终端,sim = 仿真容器):

T+0     T1: bash sim/start.sh           # docker compose up,Webots GUI 弹出
T+10s   sim:  Webots + ROS 2 + Nav2 全部 up,DDS graph 准备好
T+15s   T2: rbnx boot                 # 读 robonix_manifest.yaml
T+15s   host: spawn robonix-atlas       # listen 50051
T+16s   host: atlas RegisterCapability "robonix/system/atlas"   # self-register
T+16s   host: spawn robonix-executor    # connect to atlas,RegisterCapability
T+17s   host: spawn robonix-pilot       # discover vlm/memory caps from atlas
T+18s   host: docker exec sim python chassis_driver/driver.py
T+19s   sim:  chassis driver RegisterCapability + DeclareInterface (state, move) MCP
T+19s   host: deploy 收到 register 通知 → 进入下一条 primitive
T+20s   ...重复 camera / lidar / nav2...
T+24s   host: ✓ 7 component(s) up
T+25s   T3: rbnx caps                  # 看到全部能力
T+25s   T3: rbnx chat                  # 直连 pilot SubmitTask
T+26s   user: "what can you see?"      # → pilot → vlm → tool_calls
T+27s   pilot: read_file CAPABILITY.md(懒加载) → camera_snapshot →
                 executor → docker exec MCP HTTP → driver → image

第一次部署慢主要在仿真容器拉镜像 + Webots 启动。后续 deploy 在已有容器上 docker exec,从 rbnx boot 到全部 ready 通常 5–8 秒。

硬件原语(robonix/primitive

底盘 robonix/primitive/chassis

底盘原语覆盖移动机器人的低层运动控制和位姿反馈。IDL 定义在 rust/crates/robonix-interfaces/lib/prm_chassis/,契约 TOML 在 capabilities/primitive/chassis/

注意:目标式导航(navigate / status / cancel)已经不属于底盘原语。它们是规划/决策层能力,由 robonix/service/navigation/* 服务承担,通常由 Nav2 等导航栈提供——详见 导航服务。原语层只负责下发瞬时速度(move / twist_in)和反馈底盘状态(state / odom)。

接口

契约 ID(contract_id模式载荷(IDL)契约 TOML
robonix/primitive/chassis/moverpcprm_chassis/MoveCommandstd_msgs/Stringprimitive/chassis/move.v1.toml
robonix/primitive/chassis/staterpcstd_msgs/Emptyprm_chassis/RobotStateprimitive/chassis/state.v1.toml
robonix/primitive/chassis/twist_inpub-sub (in)geometry_msgs/Twistprimitive/chassis/twist_in.v1.toml
robonix/primitive/chassis/odompub-sub (out)nav_msgs/Odometryprimitive/chassis/odom.v1.toml

move 是底盘的唯一运动入口——既支持瞬时速度(linear/angular 一次写入直接下发到 cmd_vel),也支持封顶执行时长(duration_sec)让一次"转 30°"或"前进 20 cm"这种短动作能完整跑完。LLM 通过 MCP 直接调它;用法见 tiago_chassis/CAPABILITY.md 里的 burst pattern。

典型组合

基础移动底盘实现 move + state

  • move:MCP,LLM 直接下发瞬时速度命令
  • state:MCP,LLM 拿位姿确认动作执行结果("snapshot → reason → cmd → snapshot")

用作导航底盘时,把 odom 作为输出给 robonix/service/map/* 做融合定位、twist_in 作为 Nav2 控制器的输入(cmd_vel)。

相机 robonix/primitive/camera

接口

契约 ID(contract_id模式载荷(IDL)契约 TOML
robonix/primitive/camera/rgbpub-sub (out) / rpc_server_streamsensor_msgs/Imageprimitive/camera_rgb.v1.toml
robonix/primitive/camera/depthpub-sub (out) / rpc_server_streamsensor_msgs/Imageprimitive/camera_depth.v1.toml
robonix/primitive/camera/irpub-sub (out)sensor_msgs/Image
robonix/primitive/camera/intrinsicspub-sub (out)sensor_msgs/CameraInfo
robonix/primitive/camera/rgbdpub-sub (out)robonix_msg/RGBD
robonix/primitive/camera/snapshotrpcstd_msgs/Emptysensor_msgs/Imageprimitive/camera_snapshot.v1.toml
robonix/primitive/camera/depth_snapshotrpcstd_msgs/Emptysensor_msgs/Imageprimitive/camera_depth_snapshot.v1.toml

机械臂

服务(robonix/service

Pilot 服务

Executor 服务

Liaison 服务

地图

导航服务

语义地图服务(设计稿 / TODO)

Status:设计中,未实现。

核心目标

语义地图是 Robonix 里纯粹的"语义信息数据结构 + 查询服务",不是仿真器的控制接口。它的职责很窄:在内存里维护一份场景的语义视图(物体、类别、属性、状态、物体间关系),提供统一的查询 RPC 供大脑使用,能落盘加载跨进程交换。

三件事是:

一,一套统一的数据结构,把空间几何 + 物体语义 + 物体间拓扑关系装进一份 SemanticScene。大脑推理只查这一份("cup 在哪个 table 上"、"forklift 离我多近"),不关心这些信息是来自哪条感知链路。

二,一套统一的 RPC 接口(srv/semantic_map/*)。所有对语义地图的读写都走这个接口。上层只认这组 RPC,跨进程、跨机器也都一致。

三,save / load / 跨进程交换都可用,落盘格式固定(MJCF + <custom> 语义段),任何 Robonix 节点之间能传一份完整场景。

语义地图和仿真器是两回事:

  • 仿真器是语义地图的一个数据来源:把 MuJoCo/Isaac Sim/Webots 的场景导入成 SemanticScene(初始化或定期同步)。
  • 仿真器也是语义地图的一个消费者:给定一份 SemanticScene,仿真器能做场景重建(load 成可跑的 scene)或预演(rollout 一段假设的操作看会发生什么)。
  • 真机同样两边都可以:SLAM + VLM 产 SemanticScene(数据源),或者拿一份已有的 SemanticScene 做导航规划(消费者)。

仿真器/真机对物理世界本身的操作(让机械臂动、让门打开)走的是 srv/navigation/*srv/manipulation/* 这些执行类服务,不是语义地图的职责。语义地图里的"写"只是对这份数据结构本身的写(新增标注、更新观察到的 pose 估计、记录一条新的 on 关系)。

Robonix 内存里的数据结构 ≠ 序列化文件格式

这里先把最关键的区分讲清楚,后面的所有设计都围绕这个点展开:

Robonix 运行时在内存里维护的是自己定义的数据结构(Rust struct / Python class / gRPC message),由 rust/contracts 下的 IDL 生成,语言无关,可以扩展任意字段(空间关系、affordance、时序状态等)。这个结构的形状由我们自己决定,和 MuJoCo 无关。

只有当数据要写入磁盘、或者通过一条 ROS2/gRPC 通路送到另一台机器 / 另一个进程时,我们才把内存结构序列化成一个约定的 wire format。wire format 选择了 MuJoCo MJCF XML 作为场景几何部分的底层表示,并在其上挂我们自己的语义扩展(<custom> 节 + JSON payload)。

也就是说:

  • 内存态:robonix::SemanticScene { bodies: Vec<Body>, labels: HashMap<...>, relations: Vec<Relation>, ... }
  • 序列化态:一份 MJCF XML + 附带的 JSON 语义 payload + 可选的 occupancy PNG

同一个语义信息在内存里可能用 HashMap<String, SemanticLabel> 存,落盘时把它挤进 MJCF 的 <custom><text name="robonix.semantic.labels" data='{...}'/>。加载回来时再反序列化成内存结构。两侧形状不对等,而且以内存侧为准。

为什么选 MJCF 作为序列化格式

场景几何 + 机器人描述需要一个共识格式。候选:

格式归属问题
USDPixar / NVIDIA二进制,需要 Pixar SDK,和 Isaac Sim 深度绑定,LLM 不友好
WBTWebots / Cyberbotics纯文本但生态封闭,转出工具少
SDFGazebo文本 OK,但 Gazebo 生态在萎缩,新仿真器少有支持
MJCFMuJoCo / Google DeepMind纯 XML、与 URDF 互转成熟、MuJoCo/Brax/dm_control 官方支持、可被 Isaac Sim / Webots 通过 adapter 读入

选 MJCF 的决定因素:

  • 不和 NVIDIA 绑定,避免把 Robonix 的仿真能力锁死在 Isaac Sim
  • URDF→MJCF 在 mujoco CLI 里一行命令,绝大多数机器人的 URDF 可以零代价接入
  • XML 格式可以 diff、可以 code review、可以被 LLM 生成和修改
  • <custom> 节提供了语义扩展挂载点,不需要 fork MJCF 规范
  • 社区适配器矩阵(MJCF → USD、MJCF → WBT、MJCF → SDF)已经存在或容易写

MJCF 本身不完备,尤其在语义侧(它只描述物理几何)。所以我们在它上面加一层 Robonix-specific 语义 schema,用 <custom> 节承载。序列化时一并写入,加载时一并解析。

抽象接口(contract + IDL 草案)

所有接口走 Robonix 标准的 srv/semantic_map/* 命名空间,IDL 定义在 rust/contracts/srv/semantic_map_*.v1.toml。每条服务对应一个内存操作,返回类型都是 Robonix 自己的 message,不是 MJCF 片段。分三组:

读:

Contract行为
srv/semantic_map/get_scene拉取当前内存里的整个场景或一个局部区域
srv/semantic_map/query_by_name按 object 名查单个物体,返回 pose + bbox + labels + relations
srv/semantic_map/query_by_class按语义类别查一组物体,可叠空间过滤
srv/semantic_map/query_relations查两个 object 之间的空间关系(on / in / near / left_of ...)
srv/semantic_map/raycast从 origin 沿方向投射,返回首个命中 object 及距离

写(只改语义地图自己这份数据结构,不驱动任何仿真器或物理动作):

Contract行为
srv/semantic_map/add_object在语义地图里新增一个 Object,返回 ObjectId
srv/semantic_map/remove_object从语义地图里删除一个 Object 及其所有 Relation
srv/semantic_map/update_pose更新一个 Object 在语义地图里记录的位姿(比如感知新观察到的值)
srv/semantic_map/update_state更新一个 Object 的 state 字段(VLM 观察到 drawer.open=true 等)
srv/semantic_map/upsert_relation新增/更新一条 Relation
srv/semantic_map/remove_relation删除一条 Relation
srv/semantic_map/label批量 upsert 语义标签(感知链路回灌的快捷入口)

持久化:

Contract行为
srv/semantic_map/save把当前内存结构序列化到磁盘(MJCF + <custom> 语义段 + 可选 occ PNG)
srv/semantic_map/load从磁盘或 URL 反序列化一份场景到内存

get_scene 返回 Robonix 的 SemanticScene message(IDL 生成),不是 MJCF 字符串。需要原始 MJCF 的场合走 save,或者后续加 export_mjcf

这里所有"写"都停留在语义地图自己这层。让物理世界真的发生改变(把门打开、让机械臂去抓杯子)是另外几个服务的事:srv/navigation/* 负责底盘,srv/manipulation/* 负责末端,primitive/gripper/* 之类负责更底层的执行。这些执行服务完成后,感知链路会再把新的观察写回 srv/semantic_map/* 保持一致。

Robonix 内存侧的数据结构

顶层类型 SemanticScene 包含四部分:frame_info(坐标系元信息)、objects(场景里的语义物体)、relations(物体间关系)、version(版本号用于增量同步)。可选再带一个 occupancy 缓存(2D/3D 占据栅格,由 SLAM 层派生)。

Object 字段:

字段类型说明
idObjectId唯一稳定标识,重新加载后不变
namestring人类可读名("red cup"),允许重复
classstring语义类别(cup / table / aisle
pose7-DoF (xyz + quat)世界系位姿
bbox包围盒(轴对齐 XYZ min/max,或带旋转的 OBB)用于 raycast、邻近查询
mesh_ref可选 URI指向外部几何 asset
attributesMap<string, Value>VLM/上层可任意扩展
affordancesListgraspable / container / pushable ...
stateMap<string, Value>运行时状态(openheld_by=robot0

Relation 字段(ab 用来代替语法上的 subject/object,避免和 Object 类型重名):

字段类型说明
kindstringon / in / near / left_of / 用户扩展
aObjectId关系的第一方
bObjectId关系的第二方
metadataMap<string, Value>置信度、观察时间等

查询 "cup 在哪个 table 上" = 一次 query_relations(kind="on", a=<cup_id>),返回 b 列表。这是大脑推理最常走的入口。

MJCF 序列化约定

落盘或跨进程传输时按下面的约定写 MJCF。每个 Robonix Object 对应一个 MJCF <body>(MJCF 原生物体节点),name 属性就是 ObjectId。Robonix 专属的语义字段(class / affordances / state / relations)挂在 <custom> 下,key 带 robonix. 前缀以免和上层 MJCF 规范冲突:

<mujoco model="robonix_scene_001">
  <worldbody>
    <body name="table_01" pos="0 0 0">
      <geom type="box" size="0.6 0.4 0.02"/>
    </body>
    <body name="cup_red" pos="0.2 0.1 0.05">
      <geom type="mesh" mesh="cup"/>
    </body>
  </worldbody>

  <custom>
    <numeric name="robonix.schema_version" data="1"/>
    <text name="robonix.objects" data='{
      "table_01": {"class":"table", "affordances":["support"]},
      "cup_red":  {"class":"cup",   "affordances":["graspable"], "state":{"held":false}}
    }'/>
    <text name="robonix.relations" data='[
      {"kind":"on", "a":"cup_red", "b":"table_01"}
    ]'/>
  </custom>
</mujoco>

robonix.objects 是一个 JSON map,key 是 MJCF body name,value 放 Robonix 独有的字段。加载时先走标准 MJCF 解析拿到每个 body 的几何骨架(pose、bbox),再从 <custom> 读 JSON 补上语义字段得到完整的 Object

MJCF → USD / MJCF → WBT 的适配器只处理 <worldbody> 部分,<custom> 里的 robonix.* 对它们透明。

Fast Lookup Table

在线查询(query_by_namequery_by_classquery_relations)不能每次重算,需要从 SemanticScene 派生一层索引:

  • by_name: ObjectId → object_ref
  • by_class: class → [ObjectId]
  • spatial_kd: object 质心的 KD-tree,用于 k-nearestwithin_radius 查询
  • rel_index: (subject, kind) → [object] 反向表

索引随 scene 增量更新;每次 label / load 之后重建相关 bucket。

对接不同仿真环境

Agent/Pilot 只认 SemanticScene 这一层接口。每个仿真环境或真机数据源通过一个"挂载驱动"把自己原生的世界描述桥接进来,类似 Linux VFS 下挂不同文件系统的思路:上层文件操作语义统一,具体读写由对应驱动处理。Robonix 这里的"挂载驱动"做两件事:读原生格式转成 SemanticScene;接到 save 时反向把 SemanticScene 写回原生格式(如果需要)。

环境原生世界描述挂载驱动做的事
MuJoCo / Brax / dm_controlMJCF直接读写,无转换
Isaac SimUSDUSD 解析成 Object + 几何,或先 usd → mjcf 再走 MJCF 路径
WebotsWBT / Protowbt → urdf → mjcf,然后走 MJCF 路径
GazeboSDFsdf → urdf → mjcf 或直接 sdf → Object
真机无统一格式SLAM 产几何 + VLM 产 class/relations,合并成 Object

MJCF 在这套体系里是 Robonix 规范的"默认磁盘格式"——用它作为 save/load 默认序列化路径,并不意味着内存里就用 MJCF。需要切换到另一种原生格式时只挂另一个驱动,SemanticScene 接口不变。

与其他服务的关系

  • srv/slam/*:产出几何(point cloud、occupancy grid)。语义地图消费它 + VLM 标注 → SemanticScene
  • srv/navigation/*:接到"去 A 附近"时先调 query_by_name("A") 拿 pose,再喂给 Nav2。
  • srv/memory_search:历史观察可以回灌 label RPC。
  • primitive/sensor/camera:VLM pipeline 拿帧 → 生成 objects/relations 增量 → label

当前状态 · TODO

  • schema 版本 v1 的字段确定
  • rust/contracts/srv/semantic_map_*.v1.toml 写完并跑 rbnx codegen
  • SemanticScene 内存结构 Rust + Python 两端实现
  • 索引层(FLT)实现
  • save/load MJCF 读写路径
  • 至少一个 adapter 跑通:mjcf → usd 用于 Isaac Sim demo
  • Carter warehouse 作为参考场景写一份 Robonix MJCF

Memory Search 服务

长期记忆服务,支持向量检索、写入与压缩归纳。有两种接入形态,可单独使用或并存:

  • MCP 形态(推荐,Agent 直接调用):以 MCP 工具暴露 search / save / compact,无需 ConnectCapability
  • gRPC 形态(系统内部或非 MCP 消费者):一元 RPC,载荷均为 std_msgs/String

接口

契约 ID(contract_id模式载荷(IDL)契约 TOML
robonix/system/memorysearchrpcstd_msgs/Stringstd_msgs/Stringsys/memory_search.v1.toml
robonix/system/memorysaverpcstd_msgs/Stringstd_msgs/Stringsys/memory_save.v1.toml
robonix/system/memorycompactrpcstd_msgs/Stringstd_msgs/Stringsys/memory_compact.v1.toml

契约 TOML 路径省略 rust/contracts/ 前缀。索引参数、embedding 模型、top_k 等为部署侧元数据,不在契约内。

MCP 形态(示例包 memsearch_service

示例包基于 memsearch[onnx] + milvus-lite,节点 ID com.robonix.services.memsearch。三个工具对应契约 ID:

MCP 工具名契约 ID
search_memoryrobonix/system/memorysearch
save_memoryrobonix/system/memorysave
compact_memoryrobonix/system/memorycompact

载荷用 robonix-codegen --lang mcp 生成的 std_msgs_mcp.String(线格式 {"data": "<UTF-8 文本>"}),通过 @mcp_contract(mcp, contract_id=...) 注册,与 to_dict() 一致。

启动示例:START_MEMSEARCH=1 ./examples/run.sh

注册

gRPC 形态:contract_id 填对应契约 ID(search / save / compact)。MCP 形态须遵守 接口目录首页 中的 MCP 线格式与 mcp_contract 约定。

接入指南

Robonix 包与部署配置规范

Robonix 部署分两层 manifest:

文件范围谁读
部署根目录 robonix_manifest.yaml部署rbnx boot
每个包内 package_manifest.yamlrbnx start

命令

# 整个栈一键起(读 deploy manifest,依次启动 system + 所有包)
rbnx boot -f robonix_manifest.yaml

# 只起单个包(开发调试)
rbnx start -p ./service/slam_fastlio2

rbnx boot 的流程:

  1. 展开 ${VAR} 环境变量
  2. system: 服务(atlas / executor / pilot / liaison / memory / vlm 等 cargo 二进制)
  3. 对每个 primitive / service / skill 条目:把它的 config 块写到 rbnx-boot/instances/<name>.json,然后 rbnx start -p <path>,env 里带两个变量:RBNX_CONFIG_FILE=<json-path>RBNX_INSTANCE_NAME=<name>
  4. 日志落到 rbnx-boot/logs/<component>.log
  5. Ctrl-C 统一 kill

config 用文件传递(不是 env 里直接塞 JSON)——避开 bash 对引号/换行的 escape、ARG_MAX 限制、以及 printenv | jq 这种不直观的 debug 路径。包里一行 jq 就能读配置,同一个包的多个 instance(name 不同)各有自己的 json 文件,互不干扰。

Deploy manifest 示例

name: my-robot

env:
  ROS_DISTRO: humble

# 每个 system 服务的 config 直接写在它自己下面,不重复。
# pilot 会从 vlm.listen 自己推断该 dial 哪里,不需要再在 pilot 里写一遍。
system:
  atlas:    { listen: 127.0.0.1:50051 }
  executor: { listen: 127.0.0.1:50061 }
  pilot:    { listen: 127.0.0.1:50071 }
  liaison:  { listen: 127.0.0.1:50081 }
  memory:   { backend: sqlite, path: ${HOME}/.robonix/memory.db }
  vlm:
    listen: 127.0.0.1:50091
    upstream: ${VLM_BASE_URL}
    api_key:  ${VLM_API_KEY}
    model:    ${VLM_MODEL}
    api_format: openai

# 硬件。每个条目是一个硬件实例(设备)
primitive:
  - package: com.robonix.primitive.sensor.lidar3d.mid360
    path: ./primitive/sensor_lidar3d_mid360
    name: lidar3d
    config:
      ip: 192.168.1.161
      mounted_frame: livox_frame

# 场景服务。path 是本地路径;url 是 git 地址(首次 clone 到 rbnx-boot/cache/)。
service:
  - package: com.robonix.service.slam.fastlio2
    path: ./service/slam_fastlio2
    name: slam
    config:
      mode: mapping
      cube_len: 100
      map_dir: ${HOME}/.robonix/maps/current

  - package: com.robonix.service.nav.nav2
    url: https://github.com/syswonder/robonix-nav.git
    branch: v0.3
    name: nav
    config:
      planner: SmacPlanner2D

skill:
  - package: com.robonix.skill.navigation.navigate_to_landmark
    path: ./skill/navigate_to_landmark
    name: navigate_to_landmark
    config:
      vlm_model: claude-opus-4-7
      retry_on_ambiguous: true

条目字段

  • package:包名,要和包 manifest 里的 package.name 一致
  • pathurl:二选一。path 相对 manifest 目录;url 是 git,首次 clone 到 rbnx-boot/cache/<name>/,可加 branch: 锁分支
  • name:instance 名字 / 日志前缀
  • config:任意 YAML 字典,JSON 化成 RBNX_CAP_CONFIG_JSON 给包

Package manifest 示例

manifestVersion: 1

package:
  name: com.robonix.service.slam.fastlio2
  version: 0.1.0
  vendor: syswonder
  description: FASTLIO2 3D LiDAR-Inertial SLAM + PGO
  license: MulanPSL-2.0

build: bash scripts/build.sh     # build 入口
start: bash bin/start.sh         # start 入口

# 提供的能力。name 是 contract id,可选 path 指向包本地 TOML(当你自定义接口时)。
# 不写 path 就去 $(rbnx path capabilities) 找官方 TOML。
capabilities:
  - name: robonix/service/map/lio_odom
  - name: robonix/service/map/save_map
  - name: robonix/service/map/switch_mode

depends: # 库依赖,即需要用到另一个库的代码/数据(如model)
  - name: com.robonix.primitive.sensor.lidar3d.mid360
    path: ../primitive/sensor_lidar3d_mid360
  - name: com.robonix.system.xx
    url: https://github.com/syswonder/robonix-xx.git
    branch: v0.1

包里读 config

rbnx 传两个 env 变量:

  • RBNX_CONFIG_FILE:本 instance 的配置 json 绝对路径
  • RBNX_INSTANCE_NAME:本 instance 名字(同包多实例时用来区分)
# bin/start.sh
set -eo pipefail
: "${RBNX_CONFIG_FILE:=/dev/null}"      # 单独 rbnx start 的 fallback
MODE=$(jq -r '.mode // "mapping"' < "$RBNX_CONFIG_FILE")
CUBE=$(jq -r '.cube_len // 100'  < "$RBNX_CONFIG_FILE")
echo "[start] instance=$RBNX_INSTANCE_NAME mode=$MODE cube=$CUBE"
exec ros2 launch my_pkg.launch.py mode:="$MODE" cube_len:="$CUBE"

或者 Python:

import json, os
cfg = json.load(open(os.environ["RBNX_CONFIG_FILE"]))
mode = cfg.get("mode", "mapping")

设计说明

为什么 config 是透传,不做 schema

每个包最清楚自己的配置。Robonix 核心不定 schema,包自己 parse RBNX_CAP_CONFIG_JSON,加字段不用改核心代码。

多实例(primitive 一机多件)

一台车两个 MID360,或同一个 camera 驱动挂两个摄像头:deploy manifest 里写两条就行。

primitive:
  - package: com.robonix.primitive.sensor.lidar3d.mid360
    path: ./primitive/sensor_lidar3d_mid360
    name: lidar_front
    config: { ip: 192.168.1.161, mounted_frame: livox_front, topic_prefix: /lidar_front }
  - package: com.robonix.primitive.sensor.lidar3d.mid360
    path: ./primitive/sensor_lidar3d_mid360
    name: lidar_rear
    config: { ip: 192.168.1.162, mounted_frame: livox_rear, topic_prefix: /lidar_rear }

两条用同一个包、同一个 path,nameconfig 不同。rbnx 会分别 spawn 两个 rbnx start,每个拿到自己的 RBNX_CAP_CONFIG_JSON。包里要根据 config(比如 topic_prefix)决定发什么 topic、以什么 instance id 注册到 atlas(如 robonix/primitive/lidar/lidar3d@front)。

Primitive 的 driver 生命周期

每个抽象硬件类别对应一个 driver contract(如 robonix/primitive/lidar/lidar3d/driver)。driver 是普通 RPC 接口,里面通过 command 字段区分 INIT / RESET / SHUTDOWN / PROBE 四个操作。Robonix 启动 primitive 包后,会先调它的 driver INIT(config_json) 做硬件初始化 / 自检 / 参数下发;失败则不进入数据面。PROBE 可随时被调用查询状态。config_json 是从 manifest 透传下来的字符串,包自己解析。

Driver 的 IDL(共享的)在 rust/crates/robonix-interfaces/lib/robonix_msg/srv/Driver.srv

uint8 CMD_INIT = 0
uint8 CMD_RESET = 1
uint8 CMD_SHUTDOWN = 2
uint8 CMD_PROBE = 3
uint8 command
string config_json
---
bool ok
string state      # uninit | ready | error | shutdown
string error

开发自己的包

接口来源:官方 vs 包内

接口在哪说明
primitiverust/contracts/primitive/官方标准,接入新硬件按已有接口实现;有空缺提 PR 新增
servicerust/contracts/service/(多数)+ 少量包内场景服务大多复用官方接口(SLAM / nav / perception),只有明确私有的才自定义
systemrust/contracts/system/控制面,全官方(pilot / executor / memory / vlm 等)
skill全部在包内skill 是 agent 层,每个包自己定义,不进主仓库

Primitive / Service 包:实现官方接口

capabilities:只写 name,不写 path:

capabilities:
  - name: robonix/primitive/lidar/lidar3d
  - name: robonix/primitive/lidar/lidar3d/driver

Robonix 去 rust/contracts/primitive/sensor/lidar3d.v1.toml 查接口形状,你的代码按 TOML 里指向的 ROS IDL / proto 实现。别人看到你的包立刻知道它"提供什么",接口互通。

Skill 包:TOML 写在包里

skill 的接口是 agent 层面的,每个应用都不一样,没有官方标准。TOML 放包里,capabilities:path 指:

capabilities:
  - name: robonix/skill/my_stack/weird_thing
    path: capabilities/weird_thing.v1.toml

TOML 格式和 primitive/service 的官方结构一致([contract] + [io.srv] / [io.msg] + [mode] + [semantics]),但 srv/msg 的路径解析规则不同

  • 官方 TOML(在 robonix 源码仓库 capabilities/ 里):[io.srv] srv = "robonix_msg/srv/Foo" → 去 rust/crates/robonix-interfaces/lib/robonix_msg/srv/Foo.srv
  • 包内 TOML(在你本地 package 的 capabilities/ 里):[io.srv] srv = "srv/Foo" → 去包的 capabilities/srv/Foo.srv

典型的 skill 包 capabilities/ 布局:

capabilities/
├── weird_thing.v1.toml
├── msg/
│   └── MyStructure.msg
└── srv/
    └── MyRequest.srv

weird_thing.v1.toml

[contract]
id      = "robonix/skill/my_stack/weird_thing"
version = "1"
kind    = "skill"

[io.srv]
srv = "srv/MyRequest"    # 指向 capabilities/srv/MyRequest.srv(包内)

[mode]
type = "rpc"

[semantics]
user_invocable = true

rbnx codegen 会把包内的 capabilities/msg/*.msgcapabilities/srv/*.srv 也 codegen 到包的 proto_gen/,和官方接口一样导入使用。(TODO)

Package 构建与代码生成

TODO:本页内容待更新。

本页讲怎么用 rbnx 命令把一个 package 从源码构建成可运行状态——包括定位 Robonix 主仓源码、生成 proto/MCP stubs、以及 package 自己的 build.sh 模板。

第一步:登记 Robonix 源码根(rbnx setup

把当前 clone 登记为 robonix 源码根目录,写入 ~/.robonix/config.yaml

cd /path/to/robonix   # 仓库根目录
rbnx setup

rbnx setup 从当前目录向上寻找 rust/Cargo.tomlrust/contractsrust/crates/robonix-interfaces/lib 三个 marker;找到就登记,找不到就报错。 也支持显式传参 rbnx setup /abs/path/to/robonix

make install 会自动帮你跑一次 rbnx setup,clone 完只要 cd robonix/rust && make install,终端会打印:

[make install] registering robonix_source_path → /home/xxx/dev/robonix
✓ robonix source path registered:
  /home/xxx/dev/robonix

旧配置文件迁移

如果 ~/.robonix/config.yaml 没有 robonix_source_path 字段(老版本留下的), 任何 rbnx build/start/validate/install/codegen 会立即停止并提示:

[rbnx] config is missing robonix_source_path (legacy config from before the `rbnx setup` migration).
Fix:  cd /path/to/robonix
      rbnx setup

按提示跑一次即可。

第二步:生成 package 的 stubs(rbnx codegen

一条命令代替整个 build.sh 模板

rbnx codegen -p /path/to/my_package                 # 基础 proto + Python stubs
rbnx codegen -p /path/to/my_package --mcp           # 再生成 robonix_mcp_types/(MCP 包需要)
rbnx codegen -p /path/to/my_package --mcp --clean   # 先清理再生成
rbnx codegen -p /path/to/my_package --mcp --out-dir bridge   # 生成物放到 bridge/ 子目录

做的事(所有都在 rbnx 内部完成,不需要你手写):

  1. robonix-codegen --lang proto,重刷 <source>/rust/crates/robonix-interfaces/robonix_proto/
  2. 若加 --mcp,调 robonix-codegen --lang mcp 生成 <pkg>/robonix_mcp_types/(或 <pkg>/<out-dir>/robonix_mcp_types/
  3. grpc_tools.protoc 把所有 proto 翻成 Python stubs,落到 <pkg>/proto_gen/
  4. <pkg>/rbnx-build/ws/install/setup.bash,导出 PYTHONPATHrbnx start 会自动 source

第三步:package 的 build.sh 模板

绝大多数 package 只需要这几行:

#!/usr/bin/env bash
set -euo pipefail
PKG="${RBNX_PACKAGE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"

FLAGS=()
[[ "${RBNX_BUILD_CLEAN:-}" == "1" ]] && FLAGS+=(--clean)
# Add --mcp if your package uses MCP tools (robonix_py.mcp_contract).
FLAGS+=(--mcp)

rbnx codegen -p "$PKG" "${FLAGS[@]}"
echo "[build] done."

如果 package 还需要额外构建步骤(cargo build rust 库、docker compose build、 pip install -e),加在 rbnx codegen 之后即可。

参考:现有 package 的 build.sh

改造后的 build.sh 基本都在 10 行左右:

Package额外步骤
examples/packages/vlm_service
examples/packages/memsearch_service无(只需 --mcp
examples/packages/tiago_sim_stackdocker compose build--out-dir tiago_bridge
examples/packages/maniskill_vla_demo多一步编 demo-local 的 maniskill_env.proto
examples/packages/zero_copy_democargo build -p robonix-buffer + pip install -e

进阶:package 自带 contract / IDL

按照约定,package 自己声明的 contract 放在:

my_package/
├── capabilities/              # 本地 contract TOML(mirror 主仓 rust/capabilities/)
│   └── my_custom.v1.toml
│   ├── msg/
│   └── srv/
└── scripts/build.sh

⚠️ TODOrbnx codegen 目前不会自动 union 这两个目录到 system paths。 如果你的 package 有 capabilities/interfaces/lib/ 里的自定义内容,暂时需要 在 build.sh 里自己做合并(maniskill_vla_demo 保留了这段 staging 逻辑作为参考模板)。 未来 rbnx codegen 会原生支持。

Atlas 不会在 ROBO_SYSTEM_INTERFACE_CATALOG 里强制限制 package-local contract id —— 它们出场时 atlas 只会打一条 "unknown contract (not in catalog) — allowing anyway" warning,不影响功能。

常用命令速查

# 一次性初始化
cd /path/to/robonix && rbnx setup       # 自动 by `make install`

# 每次改了 contract/IDL 后重新生成所有 stubs
rbnx codegen -p /path/to/my_package --mcp --clean

# 查询当前配置
rbnx config --show

# 取某个路径(用于自己的脚本)
$(rbnx path contracts)
$(rbnx path interfaces-lib)
$(rbnx path robonix-py)