-
Notifications
You must be signed in to change notification settings - Fork 12
Mod Nodes
本文说明 BXI ROS 2 控制框架中,一个 Mod 如何在自己的 mod.yaml 里声明、启动和
管理辅助 ROS 2 节点和普通后台命令。这里的“Mod 子节点”特指顶层 nodes: 字段中的
受管运行单元,例如相机发布器、感知节点、通信桥接程序或设备 SDK 服务。
本文不是整个工作区的 ROS 2 节点清单,也不是 states: 状态清单,更不是 inference
推理类清单。若你的 Mod 只有动作状态,不需要额外的 ROS 2 发布者或订阅者,可以完全
不写 nodes:。
| 项目 | 位置或含义 |
|---|---|
| 所属项目 |
src/bxi_example_py_elf3/ 中的 BXI Mod 框架 |
| 配置入口 | 每个 Mod 自己的 <mod目录>/mod.yaml 顶层 nodes: 字段 |
| 公共类型 |
bxi_example_py_elf3/framework/mod_api/node.py 中的 NodeBuildContext
|
| 运行时实现 |
bxi_example_py_elf3/framework/runtime/mod_nodes.py 中的 ModNodeManager
|
| 管理范围 | 节点创建、按状态启停、独立进程、参数、重映射、异常重启和关闭 |
例如,一个带独立感知进程的客户 Mod 可以组织为:
com.example.perception Mod
├── nodes.detector # 发布感知结果的辅助 ROS 2 节点
│ └── detector_node:create_node
└── states.avoidance # 消费感知结果的控制状态
两者职责不同:detector 负责产生 ROS 消息,avoidance 是控制状态;
Mod 节点管理器根据 lifecycle 决定前者何时启动和停止。这一页讲的是上图的 nodes
分支。关于 states 请阅读自定义状态,关于完整 Mod 结构请阅读
Mod 系统。
当前 com.bxi.normal_depth 不再内置相机节点。真机相机由独立
bxi_depth_camera 包管理,MuJoCo 负责仿真图像;深度状态只订阅标准 ROS 话题。
Mod API 4.0 支持四种节点运行时,同时复用相同的生命周期、重启和展示字段:
-
python:Mod 内的 Python 工厂,返回一个rclpy.Node。 -
executable:Mod 内随包分发的原生可执行文件,适用于 C++ ROS 2 节点。 -
ros:通过 ament index 查找已安装 ROS 2 包中的可执行文件,行为等价于ros2 run。 -
command:执行 Mod 内的普通脚本或命令,不追加 ROS 参数;支持独立解释器、环境 变量、工作目录、依赖顺序和分阶段关闭。
使用 executable、ros 或 command 的 Mod 应声明当前公共 API 范围
api: ">=4,<5"。
不声明 runtime_profiles 时保持最低门槛:节点使用宿主 Python、ROS 和系统库,依赖由
用户自行通过 pip/apt 等方式准备。需要随目录移动依赖时,Mod 才选择 vendor 或
portable;三种模式最终进入同一个进程管理器:
| mode | 用途 | 是否携带 Python | 是否允许 in_process
|
|---|---|---|---|
host |
默认;使用宿主环境 | 否 | 是 |
vendor |
宿主 Python 加 vendor/python、vendor/lib
|
否 | 是,但原生符号可能冲突 |
portable |
Mod 内完整运行根,适用于 Python/C++/厂商 SDK | 可选 | 否,必须独立进程 |
Portable 示例:
runtime_profiles:
device:
candidates:
- mode: portable
root: runtime/{platform}
python: python/bin/python3
executable_paths: [bin, python/bin]
library_paths: [lib, python/lib]
- mode: host
nodes:
device_manager:
runtime: command
entrypoint: scripts/device_manager.py
interpreter: bundled-python
execution: process
runtime_profile: device
lifecycle: mod
manifest:
label: 设备管理器{platform} 使用框架当前的平台标签;也可在路径中使用 {python_tag}。所有路径都必须
是 Mod 内的安全相对路径,移动整个 Mod 后会按新根目录重新解析。Portable 目录不存在
时才会尝试后续 candidate;目录已经存在但 Python 不可执行、声明目录缺失或符号链接
逃出运行根时,节点直接标记为不可用,不会静默退回宿主环境。
bundled-python 表示使用选中 profile 的 python,框架会直接启动该解释器并向完整
进程树传播 BXI_RUNTIME_ROOT、BXI_PYTHON_EXECUTABLE、PATH、PYTHONHOME 和
LD_LIBRARY_PATH。Portable 默认清除继承的 PYTHONPATH/PYTHONHOME 并关闭 user site;
节点级 runtime_requirements 也使用同一个解释器和最终环境做真实 import 检查。
一个 Mod 可以按节点混用三种模式。例如控制状态和 ROS bridge 使用 Host,厂商 SDK
manager 使用 Portable。自定义 Python 或原生依赖不能放进 in_process 节点;一个
进程无法同时拥有彼此隔离的 Python 解释器和动态库命名空间。
nodes:
detector:
runtime: executable
entrypoint: detector_node
execution: process
scheduling:
cpu_affinity: compute
lifecycle: state
states: [detect]
arguments: [--device, "0"]
namespace: /vision
remappings:
image: /camera/image_raw
params:
threshold: 0.5
enabled: true
manifest:
label: C++ 检测节点
runtime_requirements:
python: []
ros:
- package: sensor_msgs
system: []
restart:
max_attempts: 3
delay: 1.0
non_retryable_exit_codes: [78]字段说明:
-
runtime:python、executable、ros或command。为兼容旧 Mod,省略时默认为python。 -
runtime_profile:可选的顶层runtime_profiles名称;省略时沿用 Host/既有 Vendor 行为。 -
entrypoint:其格式由runtime决定,具体见下文。 -
execution:Python 节点支持in_process和process;原生节点必须是process。原生节点省略该字段时默认使用process。 -
scheduling.cpu_affinity:独立进程使用的跨平台 CPU 角色。省略时默认shared; 常用值为compute、background、shared。只适用于execution: process。 -
lifecycle:mod表示随 Mod 常驻,state表示仅在states指定的状态 活跃或预加载时运行。 -
arguments:放在可执行文件之后、--ros-args之前的普通程序参数。 -
namespace:空字符串或以/开头的绝对 ROS namespace。 -
remappings:ROS 名称重映射表。 -
params:节点参数。原生节点会收到自动生成的标准 ROS 2 参数文件。 -
restart:仅适用于进程节点,沿用现有退出监控和重启策略。non_retryable_exit_codes可声明确定性配置故障的退出码;命中后节点立即进入faulted,不消耗重试次数。建议使用 sysexits 的EX_CONFIG=78表示配置错误,普通 运行时故障仍应使用其他非零退出码以保留自动恢复能力。 -
depends_on:依赖的节点名列表。框架按依赖拓扑顺序启动、逆序停止;局部名称会自动 加上当前 Mod ID。 -
shutdown:进程关闭策略,可设置初始信号、升级为SIGTERM的时间和最终SIGKILL时间。
所有原生节点都会收到框架生成的唯一节点名。实际命令形如:
<executable> <arguments...> --ros-args \
-r __node:=com_example_detector_detector \
-r __ns:=/vision \
-r image:=/camera/image_raw \
--params-file <temporary-file>
临时参数文件由节点管理器持有,在管理器关闭时删除。
Mod 不应根据 RK3588、x86 或 Jetson 自己选择 CPU 编号。需要偏向计算核时只声明用途:
nodes:
inference_worker:
runtime: command
entrypoint: bin/inference_worker
execution: process
scheduling:
cpu_affinity: compute
lifecycle: mod
manifest:
label: 推理工作进程可用角色是 control、compute、background、shared、all 和 inherit;详细解析
规则见框架控制调度。独立节点默认 shared,通常无需配置。相机、
推理等持续计算节点可选择 compute,低频设备服务可选择 background。除非专门诊断,
不要给 Mod 写数字或 CPU 列表,也不要在 entrypoint 中再次执行 taskset。
in_process 节点与 Framework 共用进程和 executor,不能拥有独立的进程 affinity;
为其声明 scheduling 会在加载阶段报错。需要独立 CPU 角色时改用
execution: process。节点的 affinity 会发布为 cpu_affinity 和
resolved_cpu_affinity,便于在状态快照中确认角色及本机实际 CPU 集合。
nodes:
camera:
runtime: python
entrypoint: camera_node:create_node
execution: process
lifecycle: mod
arguments: []
namespace: ""
remappings: {}
params:
fps: 30
manifest:
label: Python 相机节点Python 工厂通过 NodeBuildContext 获得 node_name、params、arguments、
namespace 和 remappings。Python 工厂负责在构造 rclpy.Node 时使用这些值。
command 用于不实现 rclpy.Node、也不接受 --ros-args 的长期后台程序:
nodes:
device_manager:
runtime: command
entrypoint: scripts/device_manager.py
interpreter: "${DEVICE_PYTHON:-python3}"
lifecycle: state
states: [teleop]
arguments: [--port, "${DEVICE_PORT:-5556}"]
cwd: .
environment:
PYTHONUNBUFFERED: "1"
LD_LIBRARY_PATH:
prepend:
- "${DEVICE_SDK_ROOT:-/opt/device}/lib"
existing_only: true
manifest:
label: 设备管理器
restart:
max_attempts: 3
delay: 2.0
non_retryable_exit_codes: [78]
shutdown:
signal: SIGINT
terminate_after: 3.0
kill_after: 5.0
protocol_bridge:
runtime: command
entrypoint: scripts/protocol_bridge.py
interpreter: "${DEVICE_PYTHON:-python3}"
lifecycle: state
states: [teleop]
depends_on: [device_manager]
manifest:
label: 协议桥entrypoint 必须是 Mod 目录内的安全相对路径,不能包含 ..,符号链接解析后也不能
逃出 Mod。设置 interpreter 后脚本不要求可执行位;不设置时 entrypoint 自身必须
可执行。cwd 同样只能指向 Mod 内已有目录,默认是 Mod 根目录。
字符串支持只读环境展开:$NAME、${NAME} 和 ${NAME:-default}。框架不会经过
shell,因此不会执行命令替换、重定向或管道。子进程还会获得 BXI_MOD_ROOT,可在
arguments、interpreter 和 environment 中引用。
environment 的字符串值表示直接赋值;映射值支持:
-
set:先设置变量。 -
unset:从子进程环境删除变量;不能与set、prepend或append同时使用。 -
prepend/append:按separator拼接多个值;默认分隔符是平台路径分隔符。 -
existing_only:只保留当前文件系统上存在的 prepend/append 项,适合可选 SDK 路径。
command 不接收 ROS 参数,因此不能声明非空 params、namespace 或 remappings。
若程序本身需要 ROS 参数,应自行放入 arguments,或者改用其他三种运行时。
shutdown.signal 可为 SIGHUP、SIGINT、SIGQUIT、SIGTERM,默认
SIGTERM。到达 terminate_after 后框架发送 SIGTERM,到达 kill_after 后发送
SIGKILL。所有信号都发往该命令独立的进程组,以便清理其子进程。
独立进程的 stdout/stderr 由框架持续排空,并为每行添加来源:
[com.example.device/device_manager:out] device ready
[com.example.device/device_manager:err] connection lost
这同样覆盖 Python print()、C/C++ stdout、厂商 SDK 和 traceback。输出采集在独立
线程运行;超长单行会分段,日志洪泛会丢弃超额行并输出汇总。详细配置见
日志系统。
一个 Mod 可以同时携带多个平台的同名二进制:
com.example.detector/
├── mod.yaml
├── bin/
│ ├── linux-x86_64/
│ │ └── detector_node
│ └── linux-aarch64/
│ └── detector_node
└── vendor/
└── lib/
├── linux-x86_64/
└── linux-aarch64/
对应清单只写平台无关的文件名:
runtime: executable
entrypoint: detector_node运行时使用与 runtime_platform_tag() 相同的平台标签,自动解析
bin/<platform>/<entrypoint>。不会回退到其他平台。文件缺失或没有执行权限时,
节点状态为 unavailable,其他可用节点仍可继续加载。
entrypoint 必须是相对于平台 bin 目录的安全路径;绝对路径、.. 和解析后
逃出该目录的符号链接都会被拒绝。Mod 私有动态库放在
vendor/lib/<platform>/,该目录会被优先加入子进程的 LD_LIBRARY_PATH。
nodes:
detector:
runtime: ros
entrypoint: detector_package:detector_node
execution: process
lifecycle: mod
arguments: []
namespace: ""
remappings: {}
params: {}
manifest:
label: 系统检测节点运行时通过 ament index 获取包前缀,并直接执行
<prefix>/lib/detector_package/detector_node。直接执行避免了监管 ros2 run
包装进程,停止和异常重启仍由同一个 Mod 节点管理器负责。