Sim2Real
本页介绍将 LAV2 从“仅仿真验证”推进到软件在环与贴近实机部署时,需要使用的中间件与部署侧软件栈。相关入口位于 sim2real/ 之下。
范围
本页关注如何组装并运行部署侧软件栈,包括:
- 模拟器
- 飞控
- ROS 工作区
- 地面站
- LAV2 中间件与策略节点
sim2real/ 下的 ROS 工作区与主 lav2/ 包是彼此独立的。这种分离是有意的:lav2/ 包承载核心仿真、控制与训练侧逻辑,而 ROS 工作区则承载中间件与面向部署的集成层。
flowchart LR
Sim[模拟器]
PX4[PX4 SITL]
MAVROS[MAVROS 与 ROS 工作区]
Node[LAV2 部署节点]
Policy[策略或控制器]
Mixer[Mixer 与映射]
GCS[地面站]
Sim <--> PX4
PX4 <--> MAVROS
MAVROS --> Node
Node --> Policy
Policy --> Mixer
Mixer --> Node
Node --> PX4
GCS <--> PX4
SITL 栈
概览
当 sim-to-real 验证进入 SITL 阶段时,整套软件通常会扩展为数个独立演化的层。完整栈通常包括:
- 模拟器:MuJoCo、Isaac Sim、Gazebo 或其它物理后端
- 飞控:PX4 以及 LAV2 一侧使用的控制器或策略
- ROS:中间件、MAVROS 以及其它机器人软件包
- 地面站:通常是 QGroundControl
ROS 往往还会继续拉入定位、视觉、激光雷达等附加包,因此明确编排这些层之间的关系很重要。
推荐安装路径
推荐路径是 Linux 环境下的 ROS 2、PX4 与基于 Pegasus Simulator 的 Isaac Sim 后端。目标组合是:
- Ubuntu 22.04 + ROS 2 Humble,或 Ubuntu 24.04 + ROS 2 Jazzy
- 已安装并可运行 SITL 的 PX4
- 本地可用的 Isaac Sim
- 安装在其上的 Pegasus Simulator
Pegasus Simulator 是 PX4 SITL 路径的主要后端,因为 LAV2 当前的仿真到部署链路围绕它组织。
组件安装
可以把安装工作视作四组相关组件:
- 模拟器安装与配置
- PX4 安装与 SITL 就绪
- ROS 2 安装与 Python 环境准备
- 地面站可用性
按这种分组理解是有帮助的,因为很多 SITL 问题其实来自这些层之间的版本漂移,而不是某个单独包的错误。
模拟器与飞控之间的桥接是整个栈里变动最大的部分之一。最相关的参考资料包括 Pegasus Simulator 与 PX4-Isaac-Sim。
对 LAV2 而言,更推荐手工搭建环境。容器确实可以减少环境漂移,但在模拟器端需要较多自定义调整时,手工环境更容易迭代。容器化方案可参考 nvidia_isaac-sim_ros2_docker 以及 Pegasus Simulator Docker PR for Isaac Sim 4.5.0。
Pegasus Simulator 安装说明
Pegasus Simulator 应当按照其官方安装指南进行安装,但这里还有一些 LAV2 特有的额外约束。基础流程仍应遵循 Pegasus Simulator installation guide。
如果 Isaac Sim 是通过 Python 或 uv / pip 风格的方式安装,而非通过上游 Pegasus 文档默认的预构建目录结构安装,就需要额外调整 Pegasus 配置脚本,使其中的 ISAACSIM_PATH、ISAACSIM_PYTHON 等路径指向本地真实的 Isaac Sim 安装位置。
为了与 LAV2 平台配置保持一致,说明中默认使用 LAV2 维护的 Pegasus fork,不直接使用上游默认版本。
之后还需要调整诸如 PX4 路径、仿真步长以及 LAV2 模型路径等配置,例如:
extensions/pegasus.simulator/config/configs.yamlextensions/pegasus.simulator/pegasus/simulator/params.py
这些设置正是模拟器后端与 LAV2 平台描述交汇的边界。
如何启动模拟器侧
对于 Pegasus Simulator,主要有两种运行方式。
Pegasus 可以通过 Isaac Sim 图形界面运行,这种方式便于手动检查。对应说明见 extension mode tutorial。
Pegasus 也可以通过独立 Python 运行。这是 LAV2 SITL 更推荐的路径,因为更容易脚本化、复现与开发期修改。对应说明见 standalone application tutorial,面向 LAV2 的示例位于维护分支中的 examples/100_sitl_lav2.py。
本页最重要的结论是:模拟器必须以一种足够稳定的形式运行,使得 PX4、ROS 与 LAV2 中间件都能可靠接入。standalone Python 路径最符合这一要求。
ROS 工作区
实机构建与部署(Jetson Orin NX)
如果部署目标是 NVIDIA Jetson Orin NX,那么在进入 ROS 工作区构建之前,应该先决定平台发行版。对当前 LAV2 路径而言,这一步不只是选择 Ubuntu 版本,也是在同时选择 ROS 版本路线、推理运行时可用性,以及后续能否平滑接入 Isaac Lab、Isaac ROS 与 PX4 官方支持链路。
目前 Orin NX 上最常见的两条发行版路径是:
- JetPack 5,对应 Ubuntu 20.04,通常配套 ROS 1
- JetPack 6,对应 Ubuntu 22.04,通常配套 ROS 2
两者的差异不只是 ROS 工作区布局不同。更关键的是,部署侧运行时与推理库的可获得性也不同,这会直接影响实机部署时的工程复杂度。
如果不存在强制性的历史兼容负担,LAV2 文档推荐把 JetPack 6 + ROS 2 作为 Orin NX 的默认部署基线。新项目、长期维护链路以及需要对接 Isaac Lab、Isaac ROS、PX4 官方工作流的场景,应优先选择这一组合;只有在必须复用 ROS 1 资产、旧 MAVROS 节点或既有部署镜像时,才继续保留 JetPack 5 + ROS 1。
对于新的 sim2real 与实机部署链路,更推荐 JetPack 6 与 ROS 2 的组合。更关键的原因是这条路径在上游生态中的支持最完整,且与 LAV2 当前的仿真验证路线更一致。
ROS 2 是 Isaac Lab、Isaac ROS 与 PX4 当前官方主支持的版本。对 LAV2 而言,这意味着只要已经在 Pegasus Simulator 中完成了 ROS 2 Python 包的接口验证,后续迁移到 Orin NX 实机时通常就可以沿用几乎相同的中间件与节点组织方式,不必再为 ROS 1 单独维护一套平行部署逻辑。
在运行时层面,JetPack 6 也更适合继续保留 Python 推理链。Orin NX 上所需的 Python 运行时依赖通常可以直接由 jetson containers 提供,因此 onnxruntime、torch 或其他配套推理依赖的获取与复现都更直接。与此同时,ROS 2 路径还可以直接使用 px4_msgs 与 PX4 原生 DDS 接口传递控制指令,减少对 MAVROS 适配层的依赖,使部署回路更短。
从工程实践角度,这条路径可以概括为:
- 在 Pegasus Simulator 中用 ROS 2 Python 包完成 SITL 验证
- 在同一套 ROS 2 节点接口上切换到 Orin NX 实机
- 视需要选择继续经由 MAVROS,或直接使用
px4_msgs
这样做的好处是,仿真验证与实机部署之间的接口漂移最小,额外维护的桥接层也更少。
JetPack 5 仍然可以用于部署,但它更适合作为历史兼容路径,不适合作为新项目的默认选择。当前保留这一分支,主要是为了复用既有 ROS 1 工作区、MAVROS 节点与历史部署资产。
实际问题在于,JetPack 5 上的 Python 推理运行时已经不再像过去那样容易获得。由于 jetson containers 已经放弃对 JetPack 5 pip wheel 的托管,onnxruntime 等常见计算库通常无法直接通过常规 Python 包安装流程获得可用版本。torch 仍然可以通过 NVIDIA 官方 JetPack 5 渠道获取预编译包,但可用版本通常较老,例如:https://developer.download.nvidia.com/compute/redist/jp/v512/pytorch/。
因此,如果继续使用 JetPack 5,通常只剩下两类方案:
- 自行重新构建相关 Python 预编译包,或者依赖历史缓存中的旧构建结果
- 尽量把 Python 推理节点迁移为 C++ 部署节点,优先依赖设备上稳定可用的 TensorRT runtime
另外,如果部署路径需要依赖系统级 Python,还要额外注意 JetPack 5 通常停留在 Python 3.8。这可能与 LAV2 当前依赖版本产生约束冲突。这个问题通常不是完全不可解,但往往需要额外调整依赖版本、隔离运行环境,或者单独处理部分 Python 包的安装来源。
两类方案都能工作,但灵活性都不高。前一种会显著增加环境维护与版本绑定成本,后一种则会把模型接入、调试与迭代成本转移到 C++ 工程侧;系统级 Python 版本较旧也会进一步增加部署侧适配成本。因此,JetPack 5 在文档中应被明确视为“兼容旧系统”的保留选项,不属于推荐路径。
构建环境
当模拟器侧可用之后,下一步就是构建并运行 sim2real/ 下的 ROS 工作区。
这里有一个关键环境约束:ROS 工作区中的 MAVROS 集成依赖系统级 ROS Python 环境。因此,用于该工作区的虚拟环境应基于 system Python 创建,不应基于一个完全隔离、看不到 ROS 包的 Python 环境。
建议使用基于 system Python 的虚拟环境,例如:
uv venv --no-managed-python --system-site-packages
随后在 sim2real/ros2_ws 内构建 ROS 2 工作区,例如执行 colcon build。
关于在 ROS 2 workspace 内构建 PX4 uXRCE-DDS Agent,请直接参考 PX4 官方文档: https://docs.px4.io/main/en/middleware/uxrce_dds#build-run-within-ros-2-workspace
如果走 ROS 1 MAVROS 路径,对应的 catkin 包位于 sim2real/ros1_ws/src/lav2_mavros。在 source ROS 1 环境后,使用 catkin_make 或 catkin build 构建该工作区即可。
与 ROS 2 部署路径一致,该工作区默认主 lav2 Python 包由虚拟环境提供,不会打包进 ROS 包本身。
之所以需要如此谨慎,是因为 ROS 2 在不少场景中仍不能完美支持任意形式的 Python 虚拟环境布局,因此工作区的构建与运行环境必须显式处理。
两条主要工作区路径均提供了环境准备脚本:
scripts/sim2real/px4_ros1_setup.sh用于为 ROS 1 的lav2_mavros准备工作区环境,包括 PX4 Gazebo Classic 配置以及所需的包路径、模型路径scripts/sim2real/px4_ros2_setup.sh用于准备 ROS 2 工作区环境,以及 ROS 2 MAVROS 工作流所需的 PX4 Gazebo Classic 相关路径scripts/sim2real/open_ros2_tabs.sh用于打开一个 GNOME Terminal 窗口,并分别以标签页启动mavros、livox_ros_driver2、fast_lio与lav2_mavros,标签页中的命令按顺序错峰启动
ROS 工作区环境细节
如果 ROS 工作区能够构建,但安装后的入口脚本仍然使用了错误的解释器,就修改 sim2real/ros2_ws/install/ 下生成脚本的 shebang,使其指向预期的虚拟环境 Python。
LAV2 ROS 入口
工作区构建完成后,主要 launch 入口包括:
ros2 launch lav2_mavros base_play.launch.pyros2 launch lav2_px4_msgs base_play.launch.py
如果使用 ROS 1,对应入口为:
roslaunch lav2_mavros base_play.launchroslaunch lav2_mavros base_play_sitl.launch
这些 launch 文件是部署路径的实际控制中枢:策略模型、中间件 topic、PX4 连接和运行模式选择,都是在这里被装配成一个统一 ROS launch 入口的。
包级别的区分保持在较小范围内:
- ROS 1 的
lav2_mavros通过 MAVROS 部署base_play策略,并提供打包好的 Gazebo SITL 启动路径 - ROS 2 的
lav2_mavros通过 MAVROS 部署同一套base_play策略 - ROS 2 的
lav2_px4_msgs通过 PX4 原生px4_msgs话题部署同一套base_play策略,而不是经过 MAVROS
因此,这些包的职责保持接近,变化主要集中在 ROS 与 PX4 的接口边界上。
对应的 base_play_node.py 实现则负责把以下几部分串起来:
- 来自所选 ROS 桥接层的里程计与状态输入
- 目标生成或外部目标订阅
- 策略推理
- 控制器侧命令映射
- 面向 PX4 的 setpoint 发布
其中最重要的结构点是节点围绕飞行模式与解锁状态实现了一套小型有限状态机,而不是 launch 参数细节。正是这套逻辑,让部署节点能够区分“等待”、“保持”和“激活 Offboard”等阶段,而不是始终盲目发布命令。
stateDiagram-v2
state "等待状态" as WaitingForState
state "保持" as Hold
state "Offboard 激活" as ActiveOffboard
[*] --> WaitingForState
WaitingForState --> Hold: 里程计可用
Hold --> ActiveOffboard: 已解锁且 Offboard 已就绪
ActiveOffboard --> Hold: 模式切换或解除解锁
Hold --> WaitingForState: 状态丢失
WaitingForState --> [*]
运行时流程
sequenceDiagram
autonumber
participant Sim as 模拟器
participant PX4 as PX4 SITL
participant MAVROS as MAVROS
participant Node as LAV2 部署节点
participant Policy as 策略或控制器
participant Mixer as Mixer 与映射
participant GCS as 地面站
Sim->>PX4: 推进 SITL 物理与传感器桥接
PX4-->>MAVROS: 发布飞行器状态
MAVROS-->>Node: 转发里程计与模式状态
Node->>Policy: 构造观测并执行推理
Policy-->>Node: 返回策略空间中的命令
Node->>Mixer: 转换并归一化命令
Mixer-->>Node: 返回面向 PX4 的设定值
Node-->>PX4: 发布 Offboard 设定值
GCS->>PX4: 发送解锁或模式切换
PX4-->>GCS: 回报状态与日志流
启动顺序
从高层看,SITL 运行顺序通常是:
- 启动模拟器后端
- 启动 PX4 SITL,并确认模拟器与飞控连接正常
- 通过匹配的
base_play.launch或base_play_sitl.launch启动对应 ROS 包与 LAV2 ROS 节点 - 连接地面站
- 在其余栈健康的前提下切入 Offboard
- 执行策略控制飞行
- 测试结束后切回非 Offboard 模式并检查日志
这个顺序之所以重要,是因为只有当模拟器、PX4、所选 ROS 接口层与地面站对飞行状态和命令流的理解一致时,部署侧软件栈才真正可用。
与训练环境的时间尺度对齐
SITL 能否“跑起来”只是第一步。对策略部署更关键的是,SITL、状态反馈与动作发布的时间尺度必须与训练环境保持一致。对于 LAV2,这种一致性应直接对应训练侧的 sim_dt、step_dt 与 decimation 假设,不能只看某个节点是否在持续收发消息。
建议把对齐检查拆成四项:
- SITL 仿真引擎自身的物理步进频率要与训练时的仿真频率对应
- 状态发布频率要足够高,保证构造观测时拿到的是当前环境状态,而不是已经滞后的状态
- 动作输出频率要与训练时策略实际推理频率对齐
- 执行器与动力学实现细节要尽量接近训练环境,而不是默认不同仿真器会给出等价结果
仿真频率对齐
训练侧的时间尺度在 LAV2 中通常由 VehicleParams 中的 sim_dt 与 step_dt 表达;在 Isaac Lab 等环境里,step_dt 还会进一步由 decimation * sim.dt 得到。进入 SITL 后,首要确认项是仿真引擎自己的物理步进是否与训练时假设一致,而不是仅仅确认 launch 是否成功。
例如,如果训练时物理积分频率是 100 Hz,而控制步长是 50 Hz,那么 SITL 侧也应尽量保持相同或等效的节拍关系。否则即便观测维度、动作维度完全一致,策略面对的闭环系统仍然已经不是训练时那套系统。
MAVROS 状态发布频率对齐
通过 MAVROS 构造观测时,状态发布频率过低会直接带来观测时延问题。策略推理读到的并非“当前状态”,而是若干个控制周期之前的状态,这会让本来稳定的闭环在 SITL 中表现为跟踪滞后、相位延迟甚至振荡。
因此,MAVROS 的状态信息发布频率应至少高于策略动作发布频率,通常还应留出一定裕量。仓库里的 ROS 1/ROS 2 SITL launch 已经提供了 set_message_interval 相关入口,也可以在运行时手动调用服务,例如:
ros2 service call /mavros/set_message_interval mavros_msgs/srv/MessageInterval \
"{message_id: 32, message_rate: 100}"
rosservice call /mavros/set_message_interval 32 100
关键点不在于“100 Hz”这个数字本身。真正重要的是观测更新速率必须足够支撑模型推理频率。实际部署时,应根据训练时的 step_dt 与桥接链路时延,一并评估 MAVROS 侧需要的最小发布频率。
动作输出频率对齐
部署节点的动作发布频率由 timer_hz 控制。这个参数不应被当成任意可调的“经验值”,而应直接对应训练时策略的有效动作频率,也就是训练侧 step_dt 的倒数。
例如,若训练时策略每 0.02 s 更新一次动作,那么部署侧就应把 timer_hz 设为 50.0。如果部署时把该频率改成更高,实际效果通常并非“控制更细”,而是让同一策略在比训练时更快的节奏下重复发布或过度响应;如果改得更低,则会表现为动作保持时间过长和外环明显变钝。
ros2 launch lav2_mavros base_play.launch.py \
model_path:=/abs/path/to/checkpoint.pt \
timer_hz:=50.0
如果走 lav2_px4_msgs 路径,timer_hz 的含义相同,仍然代表部署节点的推理与动作发布频率。
不同 SITL 引擎的动力学差异
即使频率全部对齐,不同 SITL 仿真引擎之间仍然可能存在不可忽略的动力学差异。最常见的例子就是电机或执行器时间常数。LAV2 自身动力学实现里使用了 tau_m、tau_up、tau_down 等参数来描述旋翼响应,而 Gazebo、Pegasus、MuJoCo 或其它后端可能采用不同的电机模型、滤波方式或默认参数。
这意味着“同样是 PX4 SITL”并不自动等价于“同样的执行器闭环”。例如 Gazebo 的电机时间常数就有可能与我们自己的实现不一致,从而导致:
- 油门建立速度不同
- 总推力与角速度响应滞后不同
- 相同策略在不同仿真器中的相位裕度不同
因此,Sim2Real 文档里的“对齐”不应只理解为 topic 名称与接口打通,还应包括执行器与动力学侧的等效性检查。
地面站与验证
由于 sim-to-real 验证应尽量贴近真实部署条件,因此通常将地面站(例如 QGroundControl)纳入闭环。
当 MAVROS 与模拟器启动后,应:
- 将地面站连接到 PX4
- 只在其余软件栈健康时解锁并切入 Offboard
- 通过 ROS 节点运行 LAV2 策略
- 飞行后切回非 Offboard 模式
- 导出飞控日志用于后续分析
这些日志可以在本地检查,也可以上传到诸如 PX4 Flight Review 这类分析工具中进行事后验证。
PX4 控制模式对齐
为什么对齐重要
SITL 可以验证通信链路与编排是否正确,但要实现可用部署,还必须保证整条控制路径上的命令语义一致。策略、控制器、Mixer、ROS 中间件与 PX4 至少要在三个问题上达成一致:
- 命令的控制量究竟是什么
- 该控制量处于哪个坐标系中
- 到达 PX4 之前,数值范围是如何被归一化的
任何一个假设发生漂移,都可能让一个在本地回放中看似稳定的策略,在 Offboard 控制下变得不可用。
面向 PX4 的命令族
对于 LAV2 控制栈,最有用的 PX4 命令族包括:
cmd_motor_thrustscmd_ctbmcmd_ctbrcmd_ctattcmd_acccmd_velcmd_pos
较低层的模式与 LAV2 控制栈更直接对应,而较高层模式则允许 PX4 在内部闭合更多控制环。
cmd_motor_thrusts 最接近执行器侧控制。它要求在发布前先做归一化,而物理上有意义的推力范围必须被压缩进 PX4 所期望的归一化区间中。
cmd_ctbm、cmd_ctbr 与 cmd_ctatt 则更适合那些已经在“总推力 + 旋转控制”层面工作的 LAV2 策略或控制器。在这些模式下,总推力仍需要映射到 PX4 所期待的归一化范围,而姿态相关命令也必须遵守 PX4 的坐标系约定。
cmd_acc、cmd_vel 与 cmd_pos 则工作在更高层抽象上。当外环仍放在 PX4 之外,但又希望与 PX4 所采用的 NED 风格加速度、速度、位置目标语义保持一致时,这些模式会更有用。
归一化与映射
最关键的归一化问题是总推力。在 LAV2 中,Mixer 不仅负责把横滚、俯仰、偏航与总推力需求分配到各旋翼,还负责计算与 PX4 风格命令接口保持一致所需的归一化推力量。
因此,部署路径应当尽量保持清晰的职责分层:
- 控制器或策略先在 LAV2 内部表示下计算期望命令
- mixer 与 mapping 层将其转换为面向 PX4 的归一化表示
- ROS 节点最终发布 PX4 可接受的 setpoint
这种组织方式可以让命令语义保持清晰,避免把归一化逻辑散落在多个层中。
对于 cmd_ctatt 这类姿态相关接口,姿态本身也必须先转换成 PX4 所期望的表示形式,桥接层在发布前还需要仔细处理坐标系与通道顺序。
对齐应当放在哪一层
PX4 控制模式对齐通常有两个实际落点。
第一种做法是在策略动作空间中直接对齐。此时,学习得到的策略本身就会输出 PX4 所期望的、在归一化与坐标系上已经一致的命令。优点是 ROS 节点中的翻译工作更少,整条控制链更容易端到端检查;代价是策略会更强地耦合到 PX4 接口。
第二种做法是让策略继续输出训练时最方便使用的表示,然后在 ROS bridge 节点中完成对齐。这样训练侧与部署侧的解耦会更好,但中间件层就必须承担更严格、更稳定的命令翻译责任。
在 LAV2 结构中,更实际的分工通常是:
- 保持策略输出与训练环境兼容
- 复用 lav2.controller.mapping 与 Mixer 进行命令解释与归一化
- 在 ROS 节点中完成最终面向 PX4 控制模式的发布
这种切分方式既能让部署节点保持足够薄,也能在系统边界上完成 PX4 特定的对齐工作。
Offboard FSM 与发布策略
在 base_play_node.py 中,真正重要的实现细节不在于 launch 参数列表,而在于围绕 Offboard 激活所设计的有限状态机。
节点不应该表现为一个无状态的命令转发器。它需要显式跟踪 odometry 是否可用、飞行器是否已经进入适合发布命令的阶段,以及何时才可以安全地进入主动 Offboard 控制。实践中,这通常会将运行时拆成如下阶段:
- 等待有效状态反馈
- Offboard 接管前的保持 / 待命
- 飞行器已解锁且 ready 后的主动命令发布
也正是在这个状态机边界上,命令对齐才真正从“配置假设”变成“运行时行为”。在平台进入主动阶段之前,节点不应把策略输出当作有效飞行命令;而一旦 Offboard 激活,该节点就必须持续地以所选 PX4 控制模式发布 setpoint。
实际验证重点
在 SITL 或实机中验证本节内容时,最值得优先检查的是:
- 选用的 PX4 控制模式,是否与策略或控制器输出的命令表示一致
- 总推力与旋转相关项是否只被归一化了一次
- 从状态估计到最终 setpoint 发布的坐标系是否始终一致
- Offboard FSM 是否能够干净地进入与退出激活阶段
如果这四点保持一致,剩余问题通常更偏向控制器调参与参数对齐,不再主要是接口语义失配。