Skip to content

Mod Nodes

konodoki edited this page Aug 2, 2026 · 8 revisions

Mod 子节点声明与运行(mod.yamlnodes 字段)

本文说明 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 参数;支持独立解释器、环境 变量、工作目录、依赖顺序和分阶段关闭。

使用 executableroscommand 的 Mod 应声明当前公共 API 范围 api: ">=4,<5"

可选运行环境 profile

不声明 runtime_profiles 时保持最低门槛:节点使用宿主 Python、ROS 和系统库,依赖由 用户自行通过 pip/apt 等方式准备。需要随目录移动依赖时,Mod 才选择 vendorportable;三种模式最终进入同一个进程管理器:

mode 用途 是否携带 Python 是否允许 in_process
host 默认;使用宿主环境
vendor 宿主 Python 加 vendor/pythonvendor/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_ROOTBXI_PYTHON_EXECUTABLEPATHPYTHONHOMELD_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]

字段说明:

  • runtimepythonexecutableroscommand。为兼容旧 Mod,省略时默认为 python
  • runtime_profile:可选的顶层 runtime_profiles 名称;省略时沿用 Host/既有 Vendor 行为。
  • entrypoint:其格式由 runtime 决定,具体见下文。
  • execution:Python 节点支持 in_processprocess;原生节点必须是 process。原生节点省略该字段时默认使用 process
  • scheduling.cpu_affinity:独立进程使用的跨平台 CPU 角色。省略时默认 shared; 常用值为 computebackgroundshared。只适用于 execution: process
  • lifecyclemod 表示随 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>

临时参数文件由节点管理器持有,在管理器关闭时删除。

CPU 调度角色

Mod 不应根据 RK3588、x86 或 Jetson 自己选择 CPU 编号。需要偏向计算核时只声明用途:

nodes:
  inference_worker:
    runtime: command
    entrypoint: bin/inference_worker
    execution: process
    scheduling:
      cpu_affinity: compute
    lifecycle: mod
    manifest:
      label: 推理工作进程

可用角色是 controlcomputebackgroundsharedallinherit;详细解析 规则见框架控制调度。独立节点默认 shared,通常无需配置。相机、 推理等持续计算节点可选择 compute,低频设备服务可选择 background。除非专门诊断, 不要给 Mod 写数字或 CPU 列表,也不要在 entrypoint 中再次执行 taskset

in_process 节点与 Framework 共用进程和 executor,不能拥有独立的进程 affinity; 为其声明 scheduling 会在加载阶段报错。需要独立 CPU 角色时改用 execution: process。节点的 affinity 会发布为 cpu_affinityresolved_cpu_affinity,便于在状态快照中确认角色及本机实际 CPU 集合。

Python 节点

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_nameparamsargumentsnamespaceremappings。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,可在 argumentsinterpreterenvironment 中引用。

environment 的字符串值表示直接赋值;映射值支持:

  • set:先设置变量。
  • unset:从子进程环境删除变量;不能与 setprependappend 同时使用。
  • prepend / append:按 separator 拼接多个值;默认分隔符是平台路径分隔符。
  • existing_only:只保留当前文件系统上存在的 prepend/append 项,适合可选 SDK 路径。

command 不接收 ROS 参数,因此不能声明非空 paramsnamespaceremappings。 若程序本身需要 ROS 参数,应自行放入 arguments,或者改用其他三种运行时。

shutdown.signal 可为 SIGHUPSIGINTSIGQUITSIGTERM,默认 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 内原生节点

一个 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

已安装 ROS 包节点

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 节点管理器负责。

相关文档

Clone this wiki locally