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 会:
- 编译并把
rbnx、robonix-atlas、robonix-pilot、robonix-executor、robonix-liaison、robonix-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 boot 读 examples/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 setup、rbnx 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/Empty、prm_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(通过参数开关):
- proto / Python stubs:把 ROS IDL 翻成 protobuf,再用
grpc_tools.protoc生成proto_gen/*_pb2.py。包代码from proto_gen.prm_chassis_pb2 import MoveCommand即可。 - 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_filebuiltin 按需懒加载(详见下文"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 | 用途 |
|---|---|---|---|
Grpc | GrpcParams { proto_file, service_name, method } | host:port | 系统服务(pilot / executor / vlm)、PRM 的二进制流式接口 |
Ros2 | Ros2Params { qos_profile } | 冲突时采用/rbnx/ch/<uuid>,其他情况直接用能力发起人提供的 endpoint | 容器内 ROS 节点间通信 |
Mcp | McpParams { description, input_schema_json } | host:port (HTTP) 或 stdio://cmd | LLM 可调工具 |
RPC 接口一览
Atlas 服务定义在 rust/proto/atlas.proto,关键 RPC:
| RPC | 调用方 | 作用 |
|---|---|---|
RegisterCapability | Provider | 登记 capability_id + namespace + capability_md_path |
DeclareInterface | Provider | 把一个 contract_id 绑到一种 transport+endpoint |
Heartbeat | Provider | 续约,超时(默认 30 s)后该 capability 被驱逐 |
UnregisterCapability | Provider | 主动注销(也可以让心跳超时) |
QueryCapabilities | Consumer | 按 capability_id / namespace / transport 过滤检索 |
QueryCapabilityMd | Consumer | 读取 capability_md_path 指向的 markdown 内容(Pilot 不用——LLM 用 read_file 懒加载更省 token) |
ConnectCapability | Consumer | 提交"我要用 cap X 的 contract Y 走 Z 传输",Atlas 记录通道并返回 endpoint |
DisconnectCapability | Consumer | 释放通道(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/state、robonix/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.yaml | rbnx boot | 一次部署:列系统服务的配置、哪些设备、服务,对应的代码包路径,实例名…… |
<package>/package_manifest.yaml | rbnx 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,七步:
- 解析 manifest:读
robonix_manifest.yaml,展开${VAR}环境变量,校验声明的 package 存在、capability 引用合法。 - 初始化日志目录:默认
<manifest-dir>/rbnx-boot/logs/,每个组件一个<name>.log文件。可用--log-dir改路径。 - 起 system 块:按
system:下的字段顺序起 atlas → executor → pilot → liaison → memory → ……。 - 轮询 atlas 就绪:调
QueryCapabilities("")直到返回非错(atlas 完全起来需 ~200 ms)。 - 逐个起 primitive / service / skill:按 manifest 声明顺序,一条一条 spawn,每条等它在 atlas 里
RegisterCapability上来才进入下一条。 - driver init dance:如果新注册的 cap 声明了
*/driver接口(如robonix/primitive/lidar/driver),调一次LifecycleDriver.Driver(CMD_INIT, config_json)完成硬件初始化。 - 守候: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/move | rpc | prm_chassis/MoveCommand → std_msgs/String | primitive/chassis/move.v1.toml |
robonix/primitive/chassis/state | rpc | std_msgs/Empty → prm_chassis/RobotState | primitive/chassis/state.v1.toml |
robonix/primitive/chassis/twist_in | pub-sub (in) | geometry_msgs/Twist | primitive/chassis/twist_in.v1.toml |
robonix/primitive/chassis/odom | pub-sub (out) | nav_msgs/Odometry | primitive/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/rgb | pub-sub (out) / rpc_server_stream | sensor_msgs/Image | primitive/camera_rgb.v1.toml |
robonix/primitive/camera/depth | pub-sub (out) / rpc_server_stream | sensor_msgs/Image | primitive/camera_depth.v1.toml |
robonix/primitive/camera/ir | pub-sub (out) | sensor_msgs/Image | — |
robonix/primitive/camera/intrinsics | pub-sub (out) | sensor_msgs/CameraInfo | — |
robonix/primitive/camera/rgbd | pub-sub (out) | robonix_msg/RGBD | — |
robonix/primitive/camera/snapshot | rpc | std_msgs/Empty → sensor_msgs/Image | primitive/camera_snapshot.v1.toml |
robonix/primitive/camera/depth_snapshot | rpc | std_msgs/Empty → sensor_msgs/Image | primitive/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 作为序列化格式
场景几何 + 机器人描述需要一个共识格式。候选:
| 格式 | 归属 | 问题 |
|---|---|---|
| USD | Pixar / NVIDIA | 二进制,需要 Pixar SDK,和 Isaac Sim 深度绑定,LLM 不友好 |
| WBT | Webots / Cyberbotics | 纯文本但生态封闭,转出工具少 |
| SDF | Gazebo | 文本 OK,但 Gazebo 生态在萎缩,新仿真器少有支持 |
| MJCF | MuJoCo / Google DeepMind | 纯 XML、与 URDF 互转成熟、MuJoCo/Brax/dm_control 官方支持、可被 Isaac Sim / Webots 通过 adapter 读入 |
选 MJCF 的决定因素:
- 不和 NVIDIA 绑定,避免把 Robonix 的仿真能力锁死在 Isaac Sim
- URDF→MJCF 在
mujocoCLI 里一行命令,绝大多数机器人的 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 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id | ObjectId | 唯一稳定标识,重新加载后不变 |
name | string | 人类可读名("red cup"),允许重复 |
class | string | 语义类别(cup / table / aisle) |
pose | 7-DoF (xyz + quat) | 世界系位姿 |
bbox | 包围盒(轴对齐 XYZ min/max,或带旋转的 OBB) | 用于 raycast、邻近查询 |
mesh_ref | 可选 URI | 指向外部几何 asset |
attributes | Map<string, Value> | VLM/上层可任意扩展 |
affordances | List | graspable / container / pushable ... |
state | Map<string, Value> | 运行时状态(open、held_by=robot0) |
Relation 字段(a 和 b 用来代替语法上的 subject/object,避免和 Object 类型重名):
| 字段 | 类型 | 说明 |
|---|---|---|
kind | string | on / in / near / left_of / 用户扩展 |
a | ObjectId | 关系的第一方 |
b | ObjectId | 关系的第二方 |
metadata | Map<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_name、query_by_class、query_relations)不能每次重算,需要从 SemanticScene 派生一层索引:
by_name:ObjectId → object_refby_class:class → [ObjectId]spatial_kd: object 质心的 KD-tree,用于k-nearest、within_radius查询rel_index:(subject, kind) → [object]反向表
索引随 scene 增量更新;每次 label / load 之后重建相关 bucket。
对接不同仿真环境
Agent/Pilot 只认 SemanticScene 这一层接口。每个仿真环境或真机数据源通过一个"挂载驱动"把自己原生的世界描述桥接进来,类似 Linux VFS 下挂不同文件系统的思路:上层文件操作语义统一,具体读写由对应驱动处理。Robonix 这里的"挂载驱动"做两件事:读原生格式转成 SemanticScene;接到 save 时反向把 SemanticScene 写回原生格式(如果需要)。
| 环境 | 原生世界描述 | 挂载驱动做的事 |
|---|---|---|
| MuJoCo / Brax / dm_control | MJCF | 直接读写,无转换 |
| Isaac Sim | USD | USD 解析成 Object + 几何,或先 usd → mjcf 再走 MJCF 路径 |
| Webots | WBT / Proto | wbt → urdf → mjcf,然后走 MJCF 路径 |
| Gazebo | SDF | sdf → 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:历史观察可以回灌labelRPC。primitive/sensor/camera:VLM pipeline 拿帧 → 生成 objects/relations 增量 →label。
当前状态 · TODO
- schema 版本 v1 的字段确定
rust/contracts/srv/semantic_map_*.v1.toml写完并跑rbnx codegenSemanticScene内存结构 Rust + Python 两端实现- 索引层(FLT)实现
save/loadMJCF 读写路径- 至少一个 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/memorysearch | rpc | std_msgs/String → std_msgs/String | sys/memory_search.v1.toml |
robonix/system/memorysave | rpc | std_msgs/String → std_msgs/String | sys/memory_save.v1.toml |
robonix/system/memorycompact | rpc | std_msgs/String → std_msgs/String | sys/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_memory | robonix/system/memorysearch |
save_memory | robonix/system/memorysave |
compact_memory | robonix/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.yaml | 包 | rbnx start |
命令
# 整个栈一键起(读 deploy manifest,依次启动 system + 所有包)
rbnx boot -f robonix_manifest.yaml
# 只起单个包(开发调试)
rbnx start -p ./service/slam_fastlio2
rbnx boot 的流程:
- 展开
${VAR}环境变量 - 起
system:服务(atlas / executor / pilot / liaison / memory / vlm 等 cargo 二进制) - 对每个
primitive/service/skill条目:把它的config块写到rbnx-boot/instances/<name>.json,然后rbnx start -p <path>,env 里带两个变量:RBNX_CONFIG_FILE=<json-path>和RBNX_INSTANCE_NAME=<name> - 日志落到
rbnx-boot/logs/<component>.log - 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一致path或url:二选一。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,name 和 config 不同。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 包内
| 层 | 接口在哪 | 说明 |
|---|---|---|
| primitive | rust/contracts/primitive/ | 官方标准,接入新硬件按已有接口实现;有空缺提 PR 新增 |
| service | rust/contracts/service/(多数)+ 少量包内 | 场景服务大多复用官方接口(SLAM / nav / perception),只有明确私有的才自定义 |
| system | rust/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/*.msg 和 capabilities/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.toml、rust/contracts、
rust/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 内部完成,不需要你手写):
- 调
robonix-codegen --lang proto,重刷<source>/rust/crates/robonix-interfaces/robonix_proto/ - 若加
--mcp,调robonix-codegen --lang mcp生成<pkg>/robonix_mcp_types/(或<pkg>/<out-dir>/robonix_mcp_types/) - 调
grpc_tools.protoc把所有 proto 翻成 Python stubs,落到<pkg>/proto_gen/ - 写
<pkg>/rbnx-build/ws/install/setup.bash,导出PYTHONPATH,rbnx 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_stack | docker compose build(--out-dir tiago_bridge) |
examples/packages/maniskill_vla_demo | 多一步编 demo-local 的 maniskill_env.proto |
examples/packages/zero_copy_demo | cargo 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
⚠️ TODO:
rbnx 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)