<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>duduuu.xyz</title><description>技术 &amp; 杂谈</description><link>https://duduuu.xyz</link><item><title>[DeepSeek-V4正式版灰度测试]Claude Code中一句话生成网页版我的世界游戏</title><link>https://duduuu.xyz/zh/posts/deepseek-v4-minecraft</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/deepseek-v4-minecraft</guid><description>使用简单prompt生成我的世界网页版游戏，测试DeepSeek-V4的&quot;许愿式编程&quot;和Plan-Build-Debug能力，文末附试玩地址</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;7月17日，根据笔者帐号API的特征，确定已经被灰度测试DeepSeek-V4正式版，故模仿前期网络上的相关测试，使用简单prompt生成我的世界网页版游戏，测试该模型的“许愿式编程”和Plan-Build-Debug能力，验证网络上的测试是否为炒作。在文末，笔者将相关代码部署到了服务器，大家可以在浏览器进行试玩进行评价。&lt;/p&gt;
&lt;h2&gt;整体情况及配置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;环境：&lt;/strong&gt;
Claude Code 2.1.117 + DeepSeek-V4-Pro + max 思考强度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示词：&lt;/strong&gt;
&lt;blockquote&gt;
&lt;p&gt;为我还原一个网页版我的世界，越还原越好。如果有不确定的地方请进行联网搜索&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动操作：&lt;/strong&gt;
Plan环节接受所有“推荐”的建议，不进行额外输入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;费用及Tokens：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;费用总消耗：1.78RMB（实际可能更少，同时间笔者在进行其他科研任务，仍在使用缓存命中较低的预览版）&lt;/li&gt;
&lt;li&gt;Token(缓存命中-未命中-输出）:56105280-329411-225048&lt;/li&gt;
&lt;li&gt;缓存命中率：99.42%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测评体验&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;思维链密度更高，整段内容更充实，多为&quot;I&apos;m doing&quot;结构（这也是用来验证正式版API的一个标准），与之相比，预览版的提示词多为&quot;Let me&quot;结构，且段落较短；思维链不再像预览版在思维链写代码。&lt;/li&gt;
&lt;li&gt;工具调用和Skill触发更多，在Plan阶段很快的触发了brainstorming和wrting-plans，执行阶段会开subagent（同时，有部分测试者反应执行阶段思维链会切换回&quot;Let me&quot;结构，但笔者测试没有出现，我认为为模型路由不稳定导致）&lt;/li&gt;
&lt;li&gt;思维链返回会等待一轮思考结束完全返回，而非预览版一段一段返回，体现在Claude Code里就是thinking时间过长，一度让笔者以为卡了，不过最终思维过程输出很严谨&lt;/li&gt;
&lt;li&gt;出现编译过程timeout的情况，不再像预览版多轮命令检查一直timeout，会主动检查环境配置和依赖问题，Debug能力增强&lt;/li&gt;
&lt;li&gt;虽然思维链密度很大，但是没有出现雷霆大思考和左右脑互搏，后训练效果极佳&lt;/li&gt;
&lt;li&gt;缓存命中率超级无敌高，单论本项目的缓存命中率，有可能接近99.7%，正式版上线调价后大概率价格还更低&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：在整个过程中，笔者进行过一次手动输入，那是因为笔者忘了我的世界怎么操作了，以为下面的物品框是空的是bug，最后DeepSeek为我加了一个操作说明&lt;/p&gt;
&lt;h2&gt;测试视频&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;视频 1：测试生成视频&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;视频 2：游戏画面截图&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;试玩地址&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://duduuu.xyz/mc/&quot;&gt;https://duduuu.xyz/mc/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;因为号被A畜封了，笔者无法给出与Opus 4.8的比较。但是，在“许愿式编程”的流畅实现上，DeepSeek-V4已经越来越近。同时，在笔者的科研任务（有很多软硬件的Debug）中，这版模型已经强于笔者使用过的所有模型。至于缓存命中率，已经无人可敌。也许低价高质的AI模型即将触手可及。&lt;/p&gt;
&lt;p&gt;声明：本测评仅代表个人观点。&lt;/p&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2+PX4 Real-Hardware: UXRCE-DDS Deployment (Pixhawk 6C)</title><link>https://duduuu.xyz/en/posts/px4-ros2-uxrce-dds</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-uxrce-dds</guid><description>Deploy uXRCE-DDS on Pixhawk 6C and Jetson Orin NX — firmware flashing, serial config, Client setup, WiFi hotspot, and Agent connectivity</description><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Note&lt;/h2&gt;
&lt;p&gt;This section involves relatively complex operations. If anything is unclear, jump to the companion video tutorial at the end of the article.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In our previous simulation series and MAVROS2 real-hardware articles, we covered simulation environment setup and MAVROS2-based offboard control. Starting from PX4 v1.14, the &lt;strong&gt;official recommendation is to use uXRCE-DDS middleware instead of MAVROS&lt;/strong&gt; as the communication bridge between ROS2 and PX4.&lt;/p&gt;
&lt;p&gt;This article takes the &lt;strong&gt;Pixhawk 6C&lt;/strong&gt; (official flight controller) and &lt;strong&gt;Nvidia Jetson Orin NX&lt;/strong&gt; (companion computer) as an example, providing a complete walkthrough of deploying the uXRCE-DDS middleware on real hardware. Topics include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uXRCE-DDS architecture (Client/Agent model, DDS Global Data Space)&lt;/li&gt;
&lt;li&gt;PX4 firmware flashing (command-line &lt;code&gt;make upload&lt;/code&gt; + QGC ground station)&lt;/li&gt;
&lt;li&gt;Flight controller serial port parameter configuration and uXRCE-DDS Client activation&lt;/li&gt;
&lt;li&gt;Companion computer WiFi hotspot auto-start configuration&lt;/li&gt;
&lt;li&gt;Agent startup and connectivity testing&lt;/li&gt;
&lt;li&gt;Troubleshooting common issues&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: This article assumes you have completed the simulation environment setup per the &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 Simulation Environment Development Tutorial&lt;/a&gt;, and have flashed Ubuntu 22.04 onto your companion computer per &lt;a href=&quot;/en/posts/px4-ros2-jetson-flash&quot;&gt;ROS2 Hands-On: Flashing the Companion Computer&lt;/a&gt;. If this is your first encounter with PX4 on real hardware, we recommend reading &lt;a href=&quot;/en/posts/px4-ros2-mavros&quot;&gt;Controlling PX4 Drones with a Companion Computer (MAVROS2)&lt;/a&gt; first to understand the basic real-hardware workflow.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;uXRCE-DDS Architecture&lt;/h2&gt;
&lt;h3&gt;Why Replace MAVROS with DDS?&lt;/h3&gt;
&lt;p&gt;During the ROS1 era and early ROS2 days, MAVROS was the standard solution for connecting ROS with PX4 flight controllers. However, MAVROS is essentially a &lt;strong&gt;protocol bridge&lt;/strong&gt; — it &quot;translates&quot; the MAVLink protocol from the flight controller into ROS topics. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You must publish/subscribe to topics under the dedicated &lt;code&gt;/mavros&lt;/code&gt; namespace (e.g., &lt;code&gt;/mavros/setpoint_position/local&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A centralized MAVROS node is required to maintain all communication mappings (equivalent to &lt;code&gt;ROS_Master&lt;/code&gt; in the ROS1 era)&lt;/li&gt;
&lt;li&gt;Scalability is limited in multi-drone scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;uXRCE-DDS takes a fundamentally different approach. It is built on DDS (Data Distribution Service) — a &lt;strong&gt;decentralized real-time data distribution protocol&lt;/strong&gt; that directly maps PX4&apos;s internal uORB topics to ROS2 topics. The benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Native ROS2 topics&lt;/strong&gt;: Sensor data from the flight controller is published directly as standard ROS2 topics, with no MAVROS translation layer. Offboard control code written during simulation can be transferred to real hardware without changing a single topic name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decentralized architecture&lt;/strong&gt;: All ROS2 nodes automatically discover each other within the same DDS data space — no single point of failure (no ROS1-era Master node).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low latency&lt;/strong&gt;: DDS uses UDP multicast and shared memory for data transmission, with latency far lower than MAVLink serialization overhead.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Client/Agent Communication Model&lt;/h3&gt;
&lt;p&gt;uXRCE-DDS adopts a &lt;strong&gt;Client-Agent&lt;/strong&gt; architecture:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flight Controller Side (Client)&lt;/strong&gt;: Runs inside PX4, responsible for serializing uORB topic messages into DDS format. It does exactly one thing — bridge the flight controller&apos;s internal uORB messages to the Agent. The Client itself does not participate in DDS discovery or maintain any topology.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Companion Computer Side (Agent)&lt;/strong&gt;: Runs Micro-XRCE-DDS-Agent (the same one we launched with &lt;code&gt;MicroXRCEAgent udp4 -p 8888&lt;/code&gt; during simulation), responsible for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Receiving serialized uORB messages from the Client&lt;/li&gt;
&lt;li&gt;Publishing them into ROS2&apos;s DDS Global Data Space&lt;/li&gt;
&lt;li&gt;Receiving ROS2 control commands from the DDS space and forwarding them to the Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DDS Global Data Space&lt;/strong&gt;: As long as ROS2 nodes run under the same &lt;code&gt;ROS_DOMAIN_ID&lt;/code&gt;, they automatically discover each other&apos;s topics, services, and actions. No manual connection configuration is needed — publishers and subscribers are matched automatically.&lt;/p&gt;
&lt;h3&gt;The Role of ROS_DOMAIN_ID&lt;/h3&gt;
&lt;p&gt;DDS treats all nodes using the same &lt;code&gt;ROS_DOMAIN_ID&lt;/code&gt; on the same local network as being &quot;in the same data space.&quot; Check your current domain ID:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo $ROS_DOMAIN_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the output is empty, the default value is 0. In multi-drone formation scenarios, you can assign different domain IDs to each drone-companion-computer pair for isolated communication. As long as the domain ID is consistent and the nodes are on the same local network, ROS2 nodes discover each other automatically — this is the core advantage of the decentralized architecture.&lt;/p&gt;
&lt;h2&gt;Hardware Requirements&lt;/h2&gt;
&lt;p&gt;Required hardware:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pixhawk 6C (or other PX4-native flight controller, firmware version ≥ v1.14)&lt;/li&gt;
&lt;li&gt;Nvidia Jetson Orin NX (or other companion computer, running Ubuntu 22.04)&lt;/li&gt;
&lt;li&gt;USB-TypeC cable (connect flight controller to local computer for flashing and parameter configuration)&lt;/li&gt;
&lt;li&gt;Dupont wires ×3 (connect flight controller TELEM2 to companion computer UART)&lt;/li&gt;
&lt;li&gt;Companion computer power cable (12V)&lt;/li&gt;
&lt;li&gt;Flight controller battery&lt;/li&gt;
&lt;li&gt;Telemetry radio module (for ground station; optional but strongly recommended)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Firmware version requirement&lt;/strong&gt;: The uXRCE-DDS middleware is only included in PX4 source code v1.14 and above. The latest stable version has reached v1.16 (v1.17 appeared as of July 2026); we recommend using the latest version.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flashing PX4 Firmware&lt;/h2&gt;
&lt;p&gt;Ensure the flight controller firmware version is ≥ v1.14. If your existing firmware meets this requirement, you may skip this section. Otherwise, choose one of the two methods below to re-flash.&lt;/p&gt;
&lt;h3&gt;Method 1: Command-Line Build &amp;#x26; Upload (Recommended)&lt;/h3&gt;
&lt;p&gt;Clone the PX4 source and compile + upload. This method suits readers who already have a PX4 development environment set up.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before flashing, confirm the board target for your flight controller. The target for Pixhawk 6C is &lt;code&gt;px4_fmu-v6c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# List all available build targets
make list_config_targets | grep -i &quot;v6c&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After confirming the flight controller is connected to your computer via USB-TypeC, check the serial port:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /dev/ttyACM*
# Should output something like /dev/ttyACM0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;make upload&lt;/code&gt; to compile and flash in one step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make px4_fmu-v6c upload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The terminal will show build progress. After completion, the flight controller reboots automatically. You can verify a successful flash by checking the NSH (NuttShell) messages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check serial port settings; confirm baud rate is 115200
stty -F /dev/ttyACM0 115200 raw -echo
cat /dev/ttyACM0
# Press Enter; seeing the nsh&gt; prompt means the flight controller is running normally
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common issue&lt;/strong&gt;: If &lt;code&gt;make upload&lt;/code&gt; reports a permission error, run &lt;code&gt;sudo usermod -a -G dialout $USER&lt;/code&gt; and log in again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Method 2: QGroundControl Ground Station Flashing&lt;/h3&gt;
&lt;p&gt;If you prefer not to compile from the command line, you can flash a pre-built firmware directly using QGC.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open QGroundControl and connect the flight controller via USB&lt;/li&gt;
&lt;li&gt;Go to &quot;Vehicle Setup&quot; → &quot;Firmware&quot;&lt;/li&gt;
&lt;li&gt;Unplug the USB and plug it back in; QGC will automatically detect the flight controller model&lt;/li&gt;
&lt;li&gt;Confirm the displayed version is ≥ v1.14, then click &quot;OK&quot; to start flashing&lt;/li&gt;
&lt;li&gt;Wait for the progress bar to complete&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Ground station flashing resets all parameters. Export a backup first if you have previously configured parameters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flight Controller Serial Port Parameter Configuration&lt;/h2&gt;
&lt;p&gt;This section is the most error-prone part of the deployment — we need to accurately understand the flight controller&apos;s internal serial port naming and mapping.&lt;/p&gt;
&lt;h3&gt;Serial Port Mapping&lt;/h3&gt;
&lt;p&gt;The Pixhawk 6C has three TELEM ports, each corresponding to a different TTY device internally:&lt;/p&gt;
&lt;p&gt;| Physical Port | Internal TTY | Default Use | Default Baud Rate |
|--------------|-------------|-------------|-------------------|
| TELEM1 | &lt;code&gt;/dev/ttyS5&lt;/code&gt; | Telemetry (MAVLink) | 57600 |
| TELEM2 | &lt;code&gt;/dev/ttyS3&lt;/code&gt; | Companion Computer | 921600 |
| TELEM3 | &lt;code&gt;/dev/ttyS0&lt;/code&gt; | Spare | — |&lt;/p&gt;
&lt;p&gt;This article uses TELEM2 to connect to the companion computer. The TELEM2 6-pin layout is: VCC (5V), TX, RX, PWM, GPIO, GND. We only need three wires:&lt;/p&gt;
&lt;p&gt;| TELEM2 Pin | Connects To | Jetson Orin NX UART2 Pin |
|-----------|-------------|--------------------------|
| TX | → | Pin 10 (RX) |
| RX | → | Pin 8 (TX) |
| GND | → | Pin 6 (GND) |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do NOT connect VCC!&lt;/strong&gt; The flight controller and companion computer each have independent power supplies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Different companion computers have different UART pinouts and TTY device names. For the Jetson Orin NX:&lt;/p&gt;
&lt;p&gt;| UART | TX Pin | RX Pin | GND Pin | TTY Device |
|------|--------|--------|---------|------------|
| UART1 (default debug console) | Pin 8 | Pin 10 | Pin 6 | &lt;code&gt;/dev/ttyTHS0&lt;/code&gt; |
| UART2 | Pin 12 | Pin 14 | Pin 6 | &lt;code&gt;/dev/ttyTHS1&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;We use UART2 (&lt;code&gt;/dev/ttyTHS1&lt;/code&gt;) in this article, reserving UART1 as the debug console. Please consult the pinout diagram for your specific companion computer model.&lt;/p&gt;
&lt;h3&gt;Entering the Flight Controller Terminal via NSH&lt;/h3&gt;
&lt;p&gt;With the flight controller connected to your local computer via USB, use the &lt;code&gt;mavlink_shell&lt;/code&gt; tool to enter the NSH terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
./Tools/mavlink_shell.py /dev/ttyACM0:9600
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The baud rate for entering NSH is &lt;strong&gt;9600&lt;/strong&gt; (not 115200 nor 921600). If QGC has occupied the serial port, close the ground station first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After entering, you should see the &lt;code&gt;nsh&gt;&lt;/code&gt; prompt. First, check the current MAVLink instance allocation with &lt;code&gt;mavlink status&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; mavlink status
instance #0:
    TX queue: ...
    ...
    UART: /dev/ttyS5, 57600 baud    ← TELEM1 (Telemetry)
instance #1:
    ...
    UART: /dev/ttyS3, 921600 baud   ← TELEM2 (Companion Computer)
instance #2:
    ...
    UART: /dev/ttyACM0, 9600 baud   ← USB (Current Connection)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the flight controller has automatically assigned MAVLink instance #1 to TELEM2 at 921600 baud. However, we need TELEM2 to &lt;strong&gt;carry uXRCE-DDS instead of MAVLink&lt;/strong&gt;, otherwise the two will conflict.&lt;/p&gt;
&lt;h3&gt;Disabling the MAVLink Instance on TELEM2&lt;/h3&gt;
&lt;p&gt;Check the current MAVLink parameter configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; param show MAV_1_CONFIG
MAV_1_CONFIG = 102    ← 102 corresponds to TELEM2

nsh&gt; param show MAV_1_MODE
MAV_1_MODE = 0        ← 0 = Normal (MAVLink)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parameter value 102 is the hardware port identifier for TELEM2. We need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;MAV_1_CONFIG&lt;/code&gt; to 0 (disable this MAVLink instance)&lt;/li&gt;
&lt;li&gt;Stop the currently running MAVLink instance on TELEM2 (ttyS3)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Disable MAVLink on TELEM2
param set MAV_1_CONFIG 0

# 2. Stop the running MAVLink instance on ttyS3
mavlink stop -d /dev/ttyS3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check &lt;code&gt;mavlink status&lt;/code&gt; again — only two instances should remain (TELEM1 + USB), with TELEM2&apos;s MAVLink now stopped.&lt;/p&gt;
&lt;h3&gt;Enabling the uXRCE-DDS Client&lt;/h3&gt;
&lt;p&gt;Now start the uXRCE-DDS Client on TELEM2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; uxrce_dds_client start -d /dev/ttyS3 -b 921600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Parameter explanation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d /dev/ttyS3&lt;/code&gt;: Specifies the serial device corresponding to TELEM2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-b 921600&lt;/code&gt;: Baud rate; must match MAV_1_RATE&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Optional namespace parameter&lt;/strong&gt;: Adding &lt;code&gt;-n &amp;#x3C;namespace&gt;&lt;/code&gt; prefixes all published topics with a namespace (e.g., &lt;code&gt;/px4_1/fmu/...&lt;/code&gt;). This is very useful in multi-drone scenarios to distinguish between different flight controller instances. Single-drone setups can omit this.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Set the DDS configuration parameter to ensure the DDS Client binds to the correct port:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; param set UXRCE_DDS_CFG 102
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why 102?&lt;/strong&gt; This parameter specifies which hardware port the uXRCE-DDS Client should bind to. 102 = TELEM2 (the same port as MAV_1_CONFIG=102). The default value is 0 (unconfigured); it must be set explicitly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Verify the Client is running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; uxrce_dds_client status
# Output should show &quot;running&quot; status, but not yet connected to the Agent (&quot;not connected&quot; is normal at this stage)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The flight controller configuration is now complete. To recap the key commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Complete command sequence executed in the flight controller NSH
param set MAV_1_CONFIG 0                                             # Disable MAVLink on TELEM2
mavlink stop -d /dev/ttyS3                                           # Stop the existing MAVLink instance
uxrce_dds_client start -d /dev/ttyS3 -b 921600                       # Start uXRCE-DDS Client
param set UXRCE_DDS_CFG 102                                          # Bind DDS to TELEM2
uxrce_dds_client status                                              # Verify status
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hardware Wiring&lt;/h2&gt;
&lt;p&gt;Connect the TELEM2 TX, RX, and GND pins from the flight controller to the corresponding UART2 pins on the companion computer:&lt;/p&gt;
&lt;p&gt;| TELEM2 Pin | Connects To | Jetson Orin NX UART2 Pin |
|-----------|-------------|--------------------------|
| TX | → | Pin 10 (RX) |
| RX | → | Pin 8 (TX) |
| GND | → | Pin 6 (GND) |&lt;/p&gt;
&lt;p&gt;If you are using a USB-TTL adapter (similar to Method 1 in the &lt;a href=&quot;/en/posts/px4-ros2-mavros&quot;&gt;MAVROS article&lt;/a&gt;), the companion computer side will use &lt;code&gt;/dev/ttyUSB0&lt;/code&gt;. The following steps use a direct UART connection (&lt;code&gt;/dev/ttyTHS1&lt;/code&gt;) as the example.&lt;/p&gt;
&lt;h2&gt;Companion Computer WiFi Hotspot Configuration&lt;/h2&gt;
&lt;p&gt;During actual flights, we cannot connect a monitor, keyboard, and mouse to the companion computer — all operations must be performed remotely via SSH. This requires the companion computer and your local computer to be on the same local network. The most reliable approach is to &lt;strong&gt;have the companion computer create its own WiFi hotspot&lt;/strong&gt;, which your local computer connects to, enabling SSH via a fixed IP address.&lt;/p&gt;
&lt;h3&gt;Creating the Hotspot&lt;/h3&gt;
&lt;p&gt;Open a terminal on the companion computer (Jetson Orin NX):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nm-connection-editor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the GUI that appears:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &quot;+&quot; → choose &quot;Wi-Fi&quot; → click &quot;Create&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection name&lt;/strong&gt;: choose any name, e.g., &lt;code&gt;drone-hotspot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSID&lt;/strong&gt;: same as the connection name&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode&lt;/strong&gt;: select &lt;strong&gt;Hotspot&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wi-Fi Security&lt;/strong&gt;: choose &quot;WPA &amp;#x26; WPA2 Personal&quot;, set a password of at least 8 characters&lt;/li&gt;
&lt;li&gt;Switch to the &lt;strong&gt;IPv4 Settings&lt;/strong&gt; tab, set Method to &quot;Manual&quot;:
&lt;ul&gt;
&lt;li&gt;Address: &lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Netmask: &lt;code&gt;24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Gateway: &lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6 Settings&lt;/strong&gt;: set Method to &quot;Ignore&quot;&lt;/li&gt;
&lt;li&gt;Click &quot;Save&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Setting the Hotspot to Auto-Start on Boot&lt;/h3&gt;
&lt;p&gt;First, delete previously saved WiFi connections (to prevent the companion computer from connecting to old WiFi networks on power-up):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# List all saved connections
nmcli connection show

# Delete unwanted connections (keep the hotspot you just created)
sudo nmcli connection delete &quot;&amp;#x3C;old WiFi name&gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set the hotspot to auto-connect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli connection modify &quot;drone-hotspot&quot; connection.autoconnect yes
nmcli connection modify &quot;drone-hotspot&quot; connection.autoconnect-priority 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Activate the hotspot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli connection up &quot;drone-hotspot&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, on your local computer, search for WiFi networks — you should see the &lt;code&gt;drone-hotspot&lt;/code&gt; signal. After connecting, verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ping 192.168.10.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From now on, each time: power the flight controller → power the companion computer → hotspot starts automatically → connect from local computer → SSH:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh jetson@192.168.10.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Starting the Agent and Connectivity Testing&lt;/h2&gt;
&lt;h3&gt;Pre-Connection Checks&lt;/h3&gt;
&lt;p&gt;After SSH-ing into the companion computer, do not power the flight controller yet. Perform the following preparation steps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Add your user to the dialout group (prevents serial port permission issues)
sudo usermod -a -G dialout $USER
# Log out and back in for this to take effect

# 2. Check available serial devices
ls -l /dev/ttyTHS*
# Should output /dev/ttyTHS1 (UART2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confirm that Micro-XRCE-DDS-Agent is installed on the companion computer (it should already be present from your simulation environment; if not, refer to the MicroXRCE-DDS Agent installation section in the &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 Simulation Tutorial&lt;/a&gt;).&lt;/p&gt;
&lt;h3&gt;Verifying Hardware Connectivity&lt;/h3&gt;
&lt;p&gt;First, test whether the flight controller → companion computer serial communication is working. After powering the flight controller, use &lt;code&gt;cat&lt;/code&gt; to check for a data stream:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stty -F /dev/ttyTHS1 921600 raw -echo
cat /dev/ttyTHS1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see garbled output (MAVLink or uXRCE-DDS binary data), the physical connection and serial port configuration are correct:&lt;/p&gt;
&lt;p&gt;If there is no output, check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Whether the flight controller TELEM2 TX ↔ companion computer UART RX are correctly cross-connected&lt;/li&gt;
&lt;li&gt;Whether the baud rates match (both the flight controller and companion computer should use 921600)&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;uxrce_dds_client start&lt;/code&gt; has been executed on the flight controller&lt;/li&gt;
&lt;li&gt;Whether another process (e.g., ground station) is occupying the serial port&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Starting Micro-XRCE-DDS-Agent&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Activate the ROS2 environment
source /opt/ros/humble/setup.bash

# Start the Agent, connecting to the flight controller via the serial port
MicroXRCEAgent serial --dev /dev/ttyTHS1 -b 921600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a successful connection, the terminal will output something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[info] Serial agent starting...
[info] Connect to serial device: /dev/ttyTHS1, baudrate: 921600
[info] Connection established
[info] Client connected: 0x00000001
[info] Topic created: /fmu/out/vehicle_odometry
[info] Topic created: /fmu/out/imu
[info] Topic created: /fmu/out/...
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The uXRCE-DDS middleware deployment is now complete.&lt;/strong&gt; The Agent will automatically publish the flight controller&apos;s uORB topics as ROS2 topics. Verify in another terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /opt/ros/humble/setup.bash
ros2 topic list | grep fmu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see a large number of topics prefixed with &lt;code&gt;/fmu/out/...&lt;/code&gt; carrying flight controller data. Your ROS2 offboard control code can now run directly on the companion computer, with topic names identical to those used in the simulation environment!&lt;/p&gt;
&lt;h2&gt;Complete Startup Sequence&lt;/h2&gt;
&lt;p&gt;Before each real flight, follow this sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Connect your local computer to the companion computer&apos;s hotspot &lt;code&gt;drone-hotspot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Confirm you can ping &lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Power the flight controller (via battery) and wait for it to finish booting&lt;/li&gt;
&lt;li&gt;Confirm the ground station (QGC) is connected to the flight controller via telemetry&lt;/li&gt;
&lt;li&gt;SSH into the companion computer: &lt;code&gt;ssh jetson@192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In the SSH terminal, start the Agent:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /opt/ros/humble/setup.bash
MicroXRCEAgent serial --dev /dev/ttyTHS1 -b 921600
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;Open a second SSH terminal and run the ROS2 control node:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 run offboard_control simple_offboard   # Using the earlier Offboard node as an example
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important tip&lt;/strong&gt;: Based on experience, once the Agent establishes a successful connection, try to keep the flight controller powered and connected. Repeatedly disconnecting and reconnecting the serial port mid-flight sometimes causes subsequent connection failures. It is recommended to connect once when everything is ready and avoid changes in between.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Parameter Quick Reference&lt;/h2&gt;
&lt;p&gt;| Parameter | Value | Description |
|-----------|-------|-------------|
| &lt;code&gt;MAV_1_CONFIG&lt;/code&gt; | 0 (disabled) | Decommission MAVLink on TELEM2 |
| &lt;code&gt;MAV_2_CONFIG&lt;/code&gt; | 101 (TELEM1) | Keep telemetry on TELEM1 |
| &lt;code&gt;UXRCE_DDS_CFG&lt;/code&gt; | 102 (TELEM2) | Bind DDS Client to TELEM2 |
| &lt;code&gt;SER_TEL2_BAUD&lt;/code&gt; | 921600 | TELEM2 baud rate |&lt;/p&gt;
&lt;h2&gt;uXRCE-DDS vs. MAVROS Comparison&lt;/h2&gt;
&lt;p&gt;| | uXRCE-DDS | MAVROS |
|---|---|---|
| PX4 Recommendation | Recommended since v1.14+ | Legacy, still maintained |
| Topic Naming | Native ROS2 topics | &lt;code&gt;/mavros/&lt;/code&gt; namespace |
| Architecture | Decentralized DDS | Centralized bridge |
| Multi-Drone Scaling | DDS auto-discovery | Manual configuration required |
| Sim-to-Real Code Reuse | Identical topics; no code changes needed | Topic prefix substitution required |
| Stability | Under active development | Mature and stable (from ROS1 era) |
| Real-Hardware Latency | Low (UDP multicast) | Moderate (MAVLink serialization) |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;: Prefer uXRCE-DDS for new projects. If your flight controller firmware version is &amp;#x3C; v1.14 and cannot be upgraded, or if you have a large body of legacy MAVROS code that needs compatibility, MAVROS remains a viable choice. See &lt;a href=&quot;/en/posts/px4-ros2-mavros&quot;&gt;Controlling PX4 Drones with a Companion Computer (MAVROS2)&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Troubleshooting Common Issues&lt;/h2&gt;
&lt;p&gt;| Symptom | Likely Cause | Troubleshooting |
|---------|-------------|-----------------|
| NSH terminal unreachable; serial port occupied | QGC or another process has occupied ACM0 | Close QGC ground station and retry |
| &lt;code&gt;uxrce_dds_client start&lt;/code&gt; reports an error | Baud rate mismatch or TELEM2 still in use by MAVLink | Confirm &lt;code&gt;MAV_1_CONFIG=0&lt;/code&gt; and that &lt;code&gt;mavlink stop -d /dev/ttyS3&lt;/code&gt; has been executed |
| Agent always shows &quot;not connected&quot; after startup | &lt;code&gt;UXRCE_DDS_CFG&lt;/code&gt; not correctly set | &lt;code&gt;param set UXRCE_DDS_CFG 102&lt;/code&gt; |
| cat /dev/ttyTHS1 produces no output | Wiring error or baud rate mismatch | Check TX/RX cross-connection and that the baud rate is 921600 |
| Agent is connected but &lt;code&gt;ros2 topic list&lt;/code&gt; shows no &lt;code&gt;/fmu/&lt;/code&gt; topics | ROS2 environment not sourced, or domain ID mismatch | &lt;code&gt;source /opt/ros/humble/setup.bash&lt;/code&gt;, check &lt;code&gt;echo $ROS_DOMAIN_ID&lt;/code&gt; |
| Serial device is not named ttyTHS1 | A different UART is in use | &lt;code&gt;ls /dev/ttyTHS*&lt;/code&gt; to list available devices |&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This article provided a complete walkthrough of deploying the uXRCE-DDS middleware on a Pixhawk 6C + Jetson Orin NX setup — covering firmware flashing, flight controller NSH terminal operations, MAVLink decommissioning and DDS Client activation, hardware wiring, companion computer hotspot configuration, and Agent startup. As the PX4-recommended ROS2 communication solution, uXRCE-DDS offers significant advantages in code portability (simulation code works directly on real hardware), scalability (automatic DDS discovery), and latency.&lt;/p&gt;
&lt;p&gt;That said, MAVROS, as a time-tested solution, still holds value in terms of stability and community documentation. Readers can choose flexibly based on their flight controller firmware version and project requirements. If you experiment with both approaches and compare their performance, we welcome you to share your experience in the comments.&lt;/p&gt;
&lt;p&gt;Thank you for reading! Corrections and feedback are always appreciated.&lt;/p&gt;
&lt;h2&gt;Companion Video Tutorial&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;If the video below loads slowly, watch it on Zhihu: &lt;a href=&quot;https://www.zhihu.com/zvideo/1981084557504701099&quot;&gt;https://www.zhihu.com/zvideo/1981084557504701099&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/en/middleware/uxrce_dds.html&quot;&gt;PX4 Official Docs — uXRCE-DDS (PX4-ROS2/DDS Bridge)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/en/ros2/user_guide.html&quot;&gt;PX4 Official Docs — ROS 2 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://micro-xrce-dds.docs.eprosima.com/&quot;&gt;eProsima Micro-XRCE-DDS Official Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 Simulation Tutorial — MicroXRCE-DDS Agent Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/en/posts/px4-ros2-mavros&quot;&gt;Controlling PX4 Drones with a Companion Computer (MAVROS2)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/en/posts/px4-ros2-jetson-flash&quot;&gt;ROS2 Hands-On: Flashing the Companion Computer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2+PX4 真机部署：UXRCE-DDS 中间件的部署（以 Pixhawk 6C 为例）</title><link>https://duduuu.xyz/zh/posts/px4-ros2-uxrce-dds</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-uxrce-dds</guid><description>在真机上用 uXRCE-DDS 替代 MAVROS，从飞控固件烧录、串口参数配置、Client 启用到机载电脑 Agent 连通的全流程教程</description><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;注意&lt;/h2&gt;
&lt;p&gt;本节操作较为复杂，如有不理解的地方可以跳转到文末的配套讲解视频进行辅助学习。&lt;/p&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在前面的仿真系列和 MAVROS2 真机文章中，我们分别介绍了仿真环境搭建和基于 MAVROS2 的 真机Offboard 控制。PX4 从 v1.14 版本开始，&lt;strong&gt;官方推荐使用 uXRCE-DDS 中间件替代 MAVROS&lt;/strong&gt;，作为 ROS2 与 PX4 之间的通信桥梁。&lt;/p&gt;
&lt;p&gt;本文以 &lt;strong&gt;Pixhawk 6C&lt;/strong&gt;（官方飞控板）和 &lt;strong&gt;Nvidia Jetson Orin NX&lt;/strong&gt;（机载电脑）为例，完整演示如何在真机上部署 uXRCE-DDS 中间件。内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uXRCE-DDS 架构原理（Client/Agent 模型、DDS 全局数据空间）&lt;/li&gt;
&lt;li&gt;PX4 固件烧录（命令行 &lt;code&gt;make upload&lt;/code&gt; + 地面站两种方式）&lt;/li&gt;
&lt;li&gt;飞控端串口参数配置与 uXRCE-DDS Client 启用&lt;/li&gt;
&lt;li&gt;机载电脑 WiFi 热点自动启动配置&lt;/li&gt;
&lt;li&gt;Agent 启动与连通性测试&lt;/li&gt;
&lt;li&gt;常见问题排查&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;前提条件&lt;/strong&gt;：本文假设读者已按照 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真环境开发教程&lt;/a&gt; 完成仿真环境搭建，并按 &lt;a href=&quot;/zh/posts/px4-ros2-jetson-flash&quot;&gt;ROS2 真机实践：为机载电脑刷机&lt;/a&gt; 完成机载电脑的 Ubuntu 22.04 刷机。如果你是第一次接触 PX4 真机，建议先阅读 &lt;a href=&quot;/zh/posts/px4-ros2-mavros&quot;&gt;使用机载电脑控制 PX4 无人机（MAVROS2）&lt;/a&gt; 了解真机操作的基本流程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;uXRCE-DDS 架构原理&lt;/h2&gt;
&lt;h3&gt;为什么用 DDS 替代 MAVROS？&lt;/h3&gt;
&lt;p&gt;在 ROS1 时代和 ROS2 早期，MAVROS 是连接 ROS 与 PX4 飞控的标准方案。但 MAVROS 本质上是一个&lt;strong&gt;协议桥接器&lt;/strong&gt;——它将飞控端的 MAVLink 协议&quot;翻译&quot;成 ROS 话题，这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你需要发布/订阅 &lt;code&gt;mavros&lt;/code&gt; 命名空间下的专用话题（如 &lt;code&gt;/mavros/setpoint_position/local&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;系统中需要一个中心化的 MAVROS 节点来维护所有通信映射（在ROS1时代的ROS_Master)&lt;/li&gt;
&lt;li&gt;在多机场景中扩展性受限&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;uXRCE-DDS 采用完全不同的思路。它基于 DDS（Data Distribution Service）——一种&lt;strong&gt;去中心化的实时数据分发协议&lt;/strong&gt;，让 PX4 内部的 uORB 话题直接映射为 ROS2 话题。这带来的好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;原生 ROS2 话题&lt;/strong&gt;：飞控的传感器数据直接以 ROS2 标准话题发布，无需 MAVROS 转译层。之前在仿真中写的 Offboard 控制代码可以直接搬到真机上用，话题名都不需要改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式架构&lt;/strong&gt;：所有 ROS2 节点在同一个 DDS 数据空间中自动发现彼此，不存在单点瓶颈（没有 ROS1 时代的 Master 节点）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;低延迟&lt;/strong&gt;：DDS 使用 UDP 多播和共享内存进行数据传输，延迟远低于 MAVLink 串行化开销。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Client/Agent 通信模型&lt;/h3&gt;
&lt;p&gt;uXRCE-DDS 采用 &lt;strong&gt;Client-Agent&lt;/strong&gt; 架构：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;飞控端（Client）&lt;/strong&gt;：运行在 PX4 内部，负责将 uORB 话题的消息序列化为 DDS 格式。它只做一件事——把飞控内部的 uORB 消息桥接到 Agent。Client 本身不参与 DDS 发现、不维护拓扑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;机载电脑端（Agent）&lt;/strong&gt;：运行 Micro-XRCE-DDS-Agent（之前我们仿真中用 &lt;code&gt;MicroXRCEAgent udp4 -p 8888&lt;/code&gt; 启动的那个），负责：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收来自 Client 的串行化 uORB 消息&lt;/li&gt;
&lt;li&gt;将它们发布到 ROS2 的 DDS 全局数据空间&lt;/li&gt;
&lt;li&gt;从 DDS 空间接收 ROS2 控制指令并转发给 Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;DDS 全局数据空间（Global Data Space）&lt;/strong&gt;：只要 ROS2 节点运行在同一个 &lt;code&gt;ROS_DOMAIN_ID&lt;/code&gt; 下，它们就能自动发现彼此的话题、服务和 action。你不需要手动配置任何连接关系——发布者和订阅者自动匹配。&lt;/p&gt;
&lt;h3&gt;ROS_DOMAIN_ID 的作用&lt;/h3&gt;
&lt;p&gt;DDS 将同一个局域网内使用相同 &lt;code&gt;ROS_DOMAIN_ID&lt;/code&gt; 的所有节点视为&quot;在同一数据空间内&quot;。可以用以下命令查看当前 domain ID：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo $ROS_DOMAIN_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果输出为空，默认值为 0。在多机编队场景中，你可以为每组无人机-机载电脑分配不同的 domain ID 来隔离通信。只要 domain ID 一致且在同一局域网下，ROS2 节点之间就能互相发现——这正是分布式架构的核心优势。&lt;/p&gt;
&lt;h2&gt;硬件准备&lt;/h2&gt;
&lt;p&gt;需要的硬件设备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pixhawk 6C（或其他 PX4 原生飞控板，固件版本 ≥ v1.14）&lt;/li&gt;
&lt;li&gt;Nvidia Jetson Orin NX（或其他机载电脑，已刷 Ubuntu 22.04）&lt;/li&gt;
&lt;li&gt;USB-TypeC 线（连接飞控到本地电脑，用于烧录和参数配置）&lt;/li&gt;
&lt;li&gt;杜邦线 ×3（连接飞控 TELEM2 到机载电脑 UART）&lt;/li&gt;
&lt;li&gt;机载电脑供电线（12V）&lt;/li&gt;
&lt;li&gt;飞控供电电池&lt;/li&gt;
&lt;li&gt;数传模块（用于地面站，可选但强烈建议保留）&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;飞控固件版本要求&lt;/strong&gt;：uXRCE-DDS 中间件在 PX4 v1.14 及以上版本的源码中才包含。目前最新稳定版已到 v1.16（26年7月已出现v1.17），建议使用最新版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;PX4 固件烧录&lt;/h2&gt;
&lt;p&gt;确保飞控固件版本 ≥ v1.14。如果已有的固件版本满足要求可以跳过本节，否则选择以下两种方式之一重新烧录。&lt;/p&gt;
&lt;h3&gt;方式一：命令行编译上传（推荐）&lt;/h3&gt;
&lt;p&gt;克隆 PX4 源码并编译上传。这种方式适合已经搭建好 PX4 开发环境的读者。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在烧录之前，先确认飞控板对应的编译目标（board target）。Pixhawk 6C 的目标为 &lt;code&gt;px4_fmu-v6c&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看所有可用的编译目标
make list_config_targets | grep -i &quot;v6c&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认飞控通过 USB-TypeC 连接到电脑后，查看串口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /dev/ttyACM*
# 应输出类似 /dev/ttyACM0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;make upload&lt;/code&gt; 一键编译并烧录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make px4_fmu-v6c upload
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看串口参数，确认波特率为 115200
stty -F /dev/ttyACM0 115200 raw -echo
cat /dev/ttyACM0
# 按回车后出现 nsh&gt; 提示符表示飞控正常运行
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;常见问题&lt;/strong&gt;：如果 &lt;code&gt;make upload&lt;/code&gt; 报权限错误，执行 &lt;code&gt;sudo usermod -a -G dialout $USER&lt;/code&gt; 后重新登录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;方式二：QGroundControl 地面站烧录&lt;/h3&gt;
&lt;p&gt;如果不想通过命令行编译，也可以用 QGC 直接烧录预编译固件。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 QGroundControl，通过 USB 连接飞控&lt;/li&gt;
&lt;li&gt;进入 &quot;Vehicle Setup&quot; → &quot;Firmware&quot; 页面&lt;/li&gt;
&lt;li&gt;拔掉 USB 再重新插入，QGC 会自动识别飞控型号&lt;/li&gt;
&lt;li&gt;确认显示的版本号 ≥ v1.14，点击 &quot;OK&quot; 开始烧录&lt;/li&gt;
&lt;li&gt;等待进度条完成&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：地面站烧录会重置所有参数，如果之前配置过参数请先导出备份。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;飞控端串口参数配置&lt;/h2&gt;
&lt;p&gt;这一节是部署中最容易出错的环节——我们需要准确理解飞控内部串口的命名映射关系。&lt;/p&gt;
&lt;h3&gt;串口映射关系&lt;/h3&gt;
&lt;p&gt;Pixhawk 6C 有三个 TELEM 端口，每个端口在飞控 NSH 内部对应不同的 TTY 设备：&lt;/p&gt;
&lt;p&gt;| 物理端口 | 飞控内部 TTY | 默认用途 | 默认波特率 |
|---------|-------------|---------|-----------|
| TELEM1 | &lt;code&gt;/dev/ttyS5&lt;/code&gt; | 数传（MAVLink） | 57600 |
| TELEM2 | &lt;code&gt;/dev/ttyS3&lt;/code&gt; | 机载电脑 | 921600 |
| TELEM3 | &lt;code&gt;/dev/ttyS0&lt;/code&gt; | 备用 | — |&lt;/p&gt;
&lt;p&gt;本文使用 TELEM2 连接机载电脑。TELEM2 的六针引脚排列为：VCC(5V)、TX、RX、PWM、GPIO、GND。我们只需要接三根线：&lt;/p&gt;
&lt;p&gt;| TELEM2 引脚 | 连接到 | Jetson Orin NX UART2 引脚 |
|------------|--------|--------------------------|
| TX | → | Pin 10 (RX) |
| RX | → | Pin 8 (TX) |
| GND | → | Pin 6 (GND) |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;不要连接 VCC&lt;/strong&gt;！飞控和机载电脑各自独立供电。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不同机载电脑的 UART 引脚和 TTY 设备名不同。以 Jetson Orin NX 为例：&lt;/p&gt;
&lt;p&gt;| UART | TX 引脚 | RX 引脚 | GND 引脚 | TTY 设备 |
|------|--------|--------|---------|---------|
| UART1（默认调试口） | Pin 8 | Pin 10 | Pin 6 | &lt;code&gt;/dev/ttyTHS0&lt;/code&gt; |
| UART2 | Pin 12 | Pin 14 | Pin 6 | &lt;code&gt;/dev/ttyTHS1&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;本文使用 UART2（&lt;code&gt;/dev/ttyTHS1&lt;/code&gt;），以保留 UART1 作为调试口。读者请根据自己的机载电脑型号查阅对应引脚图。&lt;/p&gt;
&lt;p&gt;飞控通过 USB 连到本地电脑后，使用 &lt;code&gt;mavlink_shell&lt;/code&gt; 工具进入飞控的 NSH 终端：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
./Tools/mavlink_shell.py /dev/ttyACM0:9600
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：进入 NSH 的波特率是 &lt;strong&gt;9600&lt;/strong&gt;（不是 115200 也不是 921600）。如果之前 QGC 占用了串口，需要先关闭地面站。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;进入后出现 &lt;code&gt;nsh&gt;&lt;/code&gt; 提示符。先用 &lt;code&gt;mavlink status&lt;/code&gt; 查看当前 MAVLink 实例分配情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; mavlink status
instance #0:
    TX queue: ...
    ...
    UART: /dev/ttyS5, 57600 baud    ← TELEM1 (数传)
instance #1:
    ...
    UART: /dev/ttyS3, 921600 baud   ← TELEM2 (机载电脑)
instance #2:
    ...
    UART: /dev/ttyACM0, 9600 baud   ← USB (当前连接)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到飞控已自动为 TELEM2 分配了 MAVLink 实例 #1，波特率 921600。但我们需要让 TELEM2 &lt;strong&gt;走 uXRCE-DDS 而不是 MAVLink&lt;/strong&gt;，否则两者会冲突。&lt;/p&gt;
&lt;p&gt;查看当前 MAVLink 参数配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; param show MAV_1_CONFIG
MAV_1_CONFIG = 102    ← 102 对应 TELEM2

nsh&gt; param show MAV_1_MODE
MAV_1_MODE = 0        ← 0 = Normal (MAVLink)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数 102 是 TELEM2 的硬件端口编号。我们需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将 &lt;code&gt;MAV_1_CONFIG&lt;/code&gt; 设为 0（禁用该 MAVLink 实例）&lt;/li&gt;
&lt;li&gt;停止当前已在 TELEM2 (ttyS3) 上运行的 MAVLink 实例&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 禁用 TELEM2 上的 MAVLink
param set MAV_1_CONFIG 0

# 2. 停止正在运行的 ttyS3 上的 MAVLink 实例
mavlink stop -d /dev/ttyS3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次检查 &lt;code&gt;mavlink status&lt;/code&gt;，应只剩两个实例（TELEM1 + USB），TELEM2 的 MAVLink 已停下。&lt;/p&gt;
&lt;h3&gt;启用 uXRCE-DDS Client&lt;/h3&gt;
&lt;p&gt;现在在 TELEM2 上启动 uXRCE-DDS Client：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; uxrce_dds_client start -d /dev/ttyS3 -b 921600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d /dev/ttyS3&lt;/code&gt;：指定 TELEM2 对应的串口设备&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-b 921600&lt;/code&gt;：波特率，需与 MAV_1_RATE 保持一致&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;可选的命名空间参数&lt;/strong&gt;：加 &lt;code&gt;-n &amp;#x3C;namespace&gt;&lt;/code&gt; 可在所有发布的话题前添加命名空间前缀（如 &lt;code&gt;/px4_1/fmu/...&lt;/code&gt;），这在多机场景中区分不同飞控实例时非常有用。单机可以不指定。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设置 DDS 配置参数，确保 DDS Client 绑定正确的端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; param set UXRCE_DDS_CFG 102
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么设为 102？&lt;/strong&gt; 这个参数指定 uXRCE-DDS Client 应该绑定到哪个硬件端口。102 = TELEM2（与 MAV_1_CONFIG=102 对应同一个端口）。默认值为 0（未配置），需要显式设置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;验证 Client 已启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nsh&gt; uxrce_dds_client status
# 输出应显示 running 状态，但此时尚未连接到 Agent（&quot;not connected&quot; 是正常的）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 飞控 NSH 中执行的完整命令序列
param set MAV_1_CONFIG 0                                             # 禁用 MAVLink on TELEM2
mavlink stop -d /dev/ttyS3                                           # 停止现有 MAVLink 实例
uxrce_dds_client start -d /dev/ttyS3 -b 921600                       # 启动 uXRCE-DDS Client
param set UXRCE_DDS_CFG 102                                          # 绑定 DDS 到 TELEM2
uxrce_dds_client status                                              # 验证状态
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;硬件接线&lt;/h2&gt;
&lt;p&gt;将飞控 TELEM2 的 TX、RX、GND 三根杜邦线连接到机载电脑 UART2 对应引脚：&lt;/p&gt;
&lt;p&gt;| TELEM2 引脚 | 连接到 | Jetson Orin NX UART2 引脚 |
|------------|--------|--------------------------|
| TX | → | Pin 10 (RX) |
| RX | → | Pin 8 (TX) |
| GND | → | Pin 6 (GND) |&lt;/p&gt;
&lt;p&gt;如果你使用 USB-TTL 转接线（方式类似 &lt;a href=&quot;/zh/posts/px4-ros2-mavros&quot;&gt;MAVROS 文章中的方法一&lt;/a&gt;），则机载电脑端走 &lt;code&gt;/dev/ttyUSB0&lt;/code&gt;。下面以 UART 直连（&lt;code&gt;/dev/ttyTHS1&lt;/code&gt;）为例。&lt;/p&gt;
&lt;h2&gt;机载电脑 WiFi 热点配置&lt;/h2&gt;
&lt;p&gt;在实际飞行中，我们无法给机载电脑接显示器和键鼠，必须通过远程 SSH 操作。这就要求机载电脑和本地电脑在同一个局域网内。最可靠的方式是&lt;strong&gt;让机载电脑自己创建一个 WiFi 热点&lt;/strong&gt;，本地电脑连接这个热点后通过固定 IP SSH 过去。&lt;/p&gt;
&lt;h3&gt;创建热点&lt;/h3&gt;
&lt;p&gt;在机载电脑（Jetson Orin NX）上打开终端：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nm-connection-editor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在弹出的图形界面中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;点击 &quot;+&quot; → 选择 &quot;Wi-Fi&quot; → 点击 &quot;Create&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection name&lt;/strong&gt;：任取，如 &lt;code&gt;北航中法工程师学院是XX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSID&lt;/strong&gt;：与 Connection name 相同即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode&lt;/strong&gt;：选择 &lt;strong&gt;Hotspot&lt;/strong&gt;（热点模式）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wi-Fi Security&lt;/strong&gt;：选择 &quot;WPA &amp;#x26; WPA2 Personal&quot;，设置一个 8 位以上密码&lt;/li&gt;
&lt;li&gt;切换到 &lt;strong&gt;IPv4 Settings&lt;/strong&gt; 选项卡，Method 选择 &quot;Manual&quot;：
&lt;ul&gt;
&lt;li&gt;Address: &lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Netmask: &lt;code&gt;24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Gateway: &lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv6 Settings&lt;/strong&gt;：Method 选择 &quot;Ignore&quot;&lt;/li&gt;
&lt;li&gt;点击 &quot;Save&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;设置开机自动启动热点&lt;/h3&gt;
&lt;p&gt;首先删除之前保存的 WiFi 连接记录（防止上电后自动连到其他 WiFi）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 列出所有已保存的连接
nmcli connection show

# 删除不需要的连接（保留刚创建的热点）
sudo nmcli connection delete &quot;&amp;#x3C;旧 WiFi 名称&gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将热点设为自动连接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli connection modify &quot;北航中法工程师学院是XX&quot; connection.autoconnect yes
nmcli connection modify &quot;北航中法工程师学院是XX&quot; connection.autoconnect-priority 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;激活热点：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli connection up &quot;北航中法工程师学院是XX&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时在本地电脑上搜索 WiFi，应能看到 &lt;code&gt;北航中法工程师学院是XX&lt;/code&gt; 信号。连接后验证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ping 192.168.10.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后每次飞控上电 → 机载电脑供电 → 自动启动热点 → 本地电脑连接 → SSH：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh jetson@192.168.10.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动 Agent 与连通测试&lt;/h2&gt;
&lt;h3&gt;连接前检查&lt;/h3&gt;
&lt;p&gt;在 SSH 进入机载电脑后，先不要给飞控上电。执行以下准备步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 将用户加入 dialout 组（防止串口权限问题）
sudo usermod -a -G dialout $USER
# 重新登录使生效

# 2. 查看可用的串口设备
ls -l /dev/ttyTHS*
# 应输出 /dev/ttyTHS1（UART2）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认机载电脑上已安装 Micro-XRCE-DDS-Agent（仿真环境中应该已装好，若未安装参考 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真环境开发教程&lt;/a&gt; 中 MicroXRCE-DDS Agent 安装部分）。&lt;/p&gt;
&lt;h3&gt;验证硬件连通性&lt;/h3&gt;
&lt;p&gt;先测试飞控 → 机载电脑的串口通信是否正常。给飞控上电后，用 &lt;code&gt;cat&lt;/code&gt; 检测是否有数据流：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stty -F /dev/ttyTHS1 921600 raw -echo
cat /dev/ttyTHS1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现乱码（MAVLink 或 uXRCE-DDS 二进制数据），说明物理连接和串口配置正确：&lt;/p&gt;
&lt;p&gt;如果没有任何输出，请检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;飞控 TELEM2 TX ↔ 机载电脑 UART RX 是否正确交叉连接&lt;/li&gt;
&lt;li&gt;波特率是否匹配（飞控端和机载电脑端都应为 921600）&lt;/li&gt;
&lt;li&gt;飞控端 &lt;code&gt;uxrce_dds_client start&lt;/code&gt; 是否已执行&lt;/li&gt;
&lt;li&gt;是否被其他进程（如地面站）占用了串口&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;启动 Micro-XRCE-DDS-Agent&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 激活 ROS2 环境
source /opt/ros/humble/setup.bash

# 启动 Agent，通过串口连接飞控
MicroXRCEAgent serial --dev /dev/ttyTHS1 -b 921600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功连接时，终端会输出类似以下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[info] Serial agent starting...
[info] Connect to serial device: /dev/ttyTHS1, baudrate: 921600
[info] Connection established
[info] Client connected: 0x00000001
[info] Topic created: /fmu/out/vehicle_odometry
[info] Topic created: /fmu/out/imu
[info] Topic created: /fmu/out/...
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;至此，uXRCE-DDS 中间件部署完成。&lt;/strong&gt; Agent 会自动将飞控的 uORB 话题发布为 ROS2 话题。在另一个终端中验证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /opt/ros/humble/setup.bash
ros2 topic list | grep fmu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应看到大量 &lt;code&gt;/fmu/out/...&lt;/code&gt; 开头的飞控数据话题。现在你的 ROS2 Offboard 控制代码可以直接在机载电脑上运行，话题名和仿真环境完全一致！&lt;/p&gt;
&lt;h2&gt;完整启动流程&lt;/h2&gt;
&lt;p&gt;每次真机飞行前，按以下顺序操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;本地电脑连接机载电脑热点 &lt;code&gt;北航中法工程师学院是XX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;确认本地电脑能 ping 通 &lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;给飞控上电（通过电池），等待飞控启动完成&lt;/li&gt;
&lt;li&gt;确认地面站（QGC）通过数传连接到飞控&lt;/li&gt;
&lt;li&gt;SSH 进入机载电脑：&lt;code&gt;ssh jetson@192.168.10.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 SSH 终端中启动 Agent：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /opt/ros/humble/setup.bash
MicroXRCEAgent serial --dev /dev/ttyTHS1 -b 921600
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;开另一个 SSH 终端，运行 ROS2 控制节点：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 run offboard_control simple_offboard   # 以之前的 Offboard 节点为例
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要提示&lt;/strong&gt;：根据经验，Agent 第一次连接成功后尽量保持飞控通电不断开。如果在飞行中反复断开重连串口，有时会导致后续连接失败。建议准备好后一次连通，中间不要改动。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参数速查表&lt;/h2&gt;
&lt;p&gt;| 参数 | 值 | 说明 |
|------|-----|------|
| &lt;code&gt;MAV_1_CONFIG&lt;/code&gt; | 0（禁用） | 将 TELEM2 上的 MAVLink 卸任 |
| &lt;code&gt;MAV_2_CONFIG&lt;/code&gt; | 101（TELEM1） | 数传保留在 TELEM1 |
| &lt;code&gt;UXRCE_DDS_CFG&lt;/code&gt; | 102（TELEM2） | DDS Client 绑定 TELEM2 |
| &lt;code&gt;SER_TEL2_BAUD&lt;/code&gt; | 921600 | TELEM2 波特率 |&lt;/p&gt;
&lt;h2&gt;uXRCE-DDS vs MAVROS 对比&lt;/h2&gt;
&lt;p&gt;| | uXRCE-DDS | MAVROS |
|---|---|---|
| PX4 官方推荐 | v1.14+ 推荐 | 旧方案，仍在维护 |
| 话题命名 | 原生 ROS2 话题 | &lt;code&gt;/mavros/&lt;/code&gt; 命名空间 |
| 架构 | 去中心化 DDS | 中心化桥接 |
| 多机扩展 | DDS 自动发现 | 需手动配置 |
| 仿真/真机代码复用 | 话题完全一致，代码无需修改 | 需替换话题前缀 |
| 稳定性 | 仍在活跃开发 | 成熟稳定（ROS1 时代积累） |
| 真机延迟 | 低（UDP 多播） | 较低（MAVLink 串行化） |&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;建议&lt;/strong&gt;：新项目优先选择 uXRCE-DDS。如果飞控固件版本 &amp;#x3C; v1.14 无法升级，或有大量遗留 MAVROS 代码需要兼容，可以继续使用 MAVROS。参考 &lt;a href=&quot;/zh/posts/px4-ros2-mavros&quot;&gt;使用机载电脑控制 PX4 无人机（MAVROS2）&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;常见问题排查&lt;/h2&gt;
&lt;p&gt;| 现象 | 可能原因 | 排查方法 |
|------|---------|---------|
| NSH 终端连接不上，串口被占用 | QGC 或其他进程占用了 ACM0 | 关闭 QGC 地面站后重试 |
| &lt;code&gt;uxrce_dds_client start&lt;/code&gt; 报错 | 波特率不匹配或 TELEM2 仍被 MAVLink 占用 | 确认 &lt;code&gt;MAV_1_CONFIG=0&lt;/code&gt;，确认 &lt;code&gt;mavlink stop -d /dev/ttyS3&lt;/code&gt; 已执行 |
| Agent 启动后一直是 &quot;not connected&quot; | &lt;code&gt;UXRCE_DDS_CFG&lt;/code&gt; 未正确设置 | &lt;code&gt;param set UXRCE_DDS_CFG 102&lt;/code&gt; |
| cat /dev/ttyTHS1 无输出 | 接线错误或波特率不对 | 检查 TX/RX 是否交叉连接，波特率是否 921600 |
| Agent 连上但 &lt;code&gt;ros2 topic list&lt;/code&gt; 无 &lt;code&gt;/fmu/&lt;/code&gt; 话题 | ROS2 环境未 source 或 domain ID 不匹配 | &lt;code&gt;source /opt/ros/humble/setup.bash&lt;/code&gt;，检查 &lt;code&gt;echo $ROS_DOMAIN_ID&lt;/code&gt; |
| 串口设备名不是 ttyTHS1 | 使用了 UART 编号不同 | &lt;code&gt;ls /dev/ttyTHS*&lt;/code&gt; 查看可用设备 |&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文完整演示了 Pixhawk 6C + Jetson Orin NX 上部署 uXRCE-DDS 中间件的全流程——从固件烧录、飞控 NSH 终端操作、MAVLink 卸任与 DDS Client 启用、硬件接线，到机载电脑热点配置和 Agent 启动。uXRCE-DDS 作为 PX4 官方推荐的 ROS2 通信方案，在代码复用性（仿真代码直接上真机）、扩展性（DDS 自动发现）和延迟方面都有显著优势。&lt;/p&gt;
&lt;p&gt;当然，MAVROS 作为久经考验的老方案，在稳定性和社区文档积累上仍有其价值。读者可以根据自己的飞控固件版本和项目需求灵活选择。如果你同时使用两种方案进行对比测试，欢迎在评论区分享经验。&lt;/p&gt;
&lt;p&gt;最后，感谢大家的耐心阅读，如有错漏请批评指正！&lt;/p&gt;
&lt;h2&gt;完整讲解视频&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;如果下方视频加载较慢，可前往知乎观看：&lt;a href=&quot;https://www.zhihu.com/zvideo/1981084557504701099&quot;&gt;https://www.zhihu.com/zvideo/1981084557504701099&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/en/middleware/uxrce_dds.html&quot;&gt;PX4 官方文档 — uXRCE-DDS (PX4-ROS2/DDS Bridge)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/en/ros2/user_guide.html&quot;&gt;PX4 官方文档 — ROS 2 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://micro-xrce-dds.docs.eprosima.com/&quot;&gt;eProsima Micro-XRCE-DDS 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真环境开发教程 — MicroXRCE-DDS Agent 安装&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/posts/px4-ros2-mavros&quot;&gt;使用机载电脑控制 PX4 无人机（MAVROS2）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/zh/posts/px4-ros2-jetson-flash&quot;&gt;ROS2 真机实践：为机载电脑刷机&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Controlling PX4 with an Onboard Computer via MAVROS2</title><link>https://duduuu.xyz/en/posts/px4-ros2-mavros</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-mavros</guid><description>Using Jetson Orin NX+JCV-600 to connect onboard computer to PX4, with pre-flight checks, SSH, and MAVROS2 Offboard takeoff</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In the &lt;a href=&quot;/en/posts/px4-ros2-jetson-flash&quot;&gt;previous real-hardware article&lt;/a&gt;, we covered flashing the onboard computer. This guide builds on that work to walk through the complete ROS2 + PX4 real-flight takeoff workflow, using the onboard computer together with the low-level flight controller. If you&apos;re new to PX4 simulation, start with the &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 Simulation Setup Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The author uses an AMOVLAB JCV-600 research quadrotor (with a CodevDynamics Codev-autopilot based on PX4) and an Nvidia Jetson Orin NX as the onboard computer for ROS2 control. Readers can follow this tutorial and adapt it to their own hardware.&lt;/p&gt;
&lt;p&gt;Required hardware (adjust according to your setup):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A complete drone kit or a drone with a self-flashed flight controller&lt;/li&gt;
&lt;li&gt;A pair of telemetry radios&lt;/li&gt;
&lt;li&gt;A local PC and an onboard computer&lt;/li&gt;
&lt;li&gt;Appropriate connecting cables (refer to the I/O port specifications of your onboard computer and drone)&lt;/li&gt;
&lt;li&gt;Pinout diagrams / manuals for the relevant drone interfaces&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Process Overview&lt;/h2&gt;
&lt;p&gt;The diagram below gives a high-level summary of the real-flight takeoff workflow used in this article. First, establish a stable connection between the ground station and the flight controller via the telemetry radio, ensuring that sensor data, GPS signals, and RC commands are transmitted properly. Next, the onboard computer communicates with the flight controller over a serial port -- install drivers, configure permissions, and match baud rates. After completing pre-flight checks in the ground station, connect to the onboard computer remotely via SSH, launch the MAVROS2 node to bridge ROS2 and PX4, and finally control the drone in Offboard mode. Throughout the entire process, keep the telemetry link active, monitor ground station data, and be ready to take manual control at any moment to guarantee flight safety.&lt;/p&gt;
&lt;h2&gt;Establishing a Connection with the Ground Station&lt;/h2&gt;
&lt;p&gt;Throughout the entire experiment, PX4 requires a stable connection to a ground station. The author uses a pair of RFD900x telemetry radios. The radio on the drone connects to the flight controller&apos;s I/O port (GND, RX-to-TX, TX-to-RX cross-connected); the flight controller provides a 5V power pin for the radio (internal power distribution is already handled).&lt;/p&gt;
&lt;p&gt;On your local PC, open QGroundControl. Under &quot;Application Settings&quot; -&gt; &quot;Comm Links&quot; -&gt; &quot;Add New Link&quot;, set Type to &quot;Serial&quot;, select the serial port containing &quot;USB0&quot;, and use the default baud rate of 57600. Once connected, the &quot;ACT&quot; LED on both radios stays solid green (power normal) and the &quot;COM&quot; LED blinks (link established). The ground station should display the drone&apos;s status.&lt;/p&gt;
&lt;h2&gt;Connecting the Onboard Computer to the Flight Controller&lt;/h2&gt;
&lt;p&gt;First, connect the flight controller to your PC via USB-TypeC. In the Parameters page, add a MAVLINK instance for the onboard computer (the default serial port is usually TELEM2):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAV_1_CONFIG = TELEM2
MAV_1_MODE = Onboard
MAV_1_RATE = 115200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three connection methods are described below.&lt;/p&gt;
&lt;h3&gt;Method 1: USB-TTL Cable&lt;/h3&gt;
&lt;p&gt;Use the flight controller&apos;s TELEM2 interface, which provides six pins: VCC (5V), TX, RX, PWM, GPIO, and GND. Connect the USB end to the onboard computer, and use Dupont-to-terminal wires to cross-connect GND, TX, and RX between the flight controller and the adapter. Do not supply power through this connection. The onboard computer should be powered separately from the reserved 12V power port.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check USB connection
lsusb
ls -l /dev/tty*
# You should see a CH340 USB-Serial adapter and /dev/ttyS0

# Add user to the dialout group
sudo usermod -a -G dialout $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Download and compile the CH340 driver:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install build-essential git linux-headers-$(uname -r)
git clone https://github.com/juliagoda/CH341SER
cd CH341SER
make &amp;#x26;&amp;#x26; sudo make install
sudo insmod ch341.ko
sudo cp ch341.ko /lib/modules/$(uname -r)/kernel/drivers/usb/serial/
sudo depmod -a
echo &quot;ch341&quot; | sudo tee /etc/modules-load.d/ch341.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the udev rule at &lt;code&gt;/etc/udev/rules.d/99-ch341.rules&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SUBSYSTEM==&quot;tty&quot;, ATTRS{idVendor}==&quot;1a86&quot;, ATTRS{idProduct}==&quot;7523&quot;, MODE=&quot;0666&quot;, SYMLINK+=&quot;ttyCH341&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo udevadm control --reload-rules &amp;#x26;&amp;#x26; sudo udevadm trigger
ls -l /dev/ttyCH341  # Verify the driver is installed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test communication (MAVLINK garbled output means success):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stty -F /dev/ttyUSB0 115200 raw -echo
cat /dev/ttyUSB0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you encounter issues with the USB-TTL connection method above, refer to the following link for driver installation guidance: https://blog.csdn.net/qq_52102933/article/details/126839474&lt;/p&gt;
&lt;h3&gt;Method 2: UART Serial Cable&lt;/h3&gt;
&lt;p&gt;The Jetson Orin NX has a 40-pin UART header, where Pin 8 (TX) and Pin 10 (RX) serve as the default UART1 debug port. We will use UART2 for this connection: Pin 14 (RX), Pin 12 (TX), Pin 6 (GND). Do not connect VCC; power the onboard computer separately.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# List available UART devices
ls /dev/ttyTHS*

# If ttyTHS1 is missing, edit /boot/extlinux/extlinux.conf
# Append console=ttyTHS1,115200 to the kernel boot line
sudo reboot

# Configure permissions and baud rate
sudo usermod -aG dialout $USER
stty -F /dev/ttyTHS1 115200 raw -echo
cat /dev/ttyTHS1  # Test communication
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Method 3: USB-TypeC Cable&lt;/h3&gt;
&lt;p&gt;On the author&apos;s JCV-600, the TypeC port can connect directly to the onboard computer (for reference only; may not apply to all hardware).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;lsusb  # Should show ID 26ac:0032 The Autopilot PX4 CODEV DP1000
sudo usermod -aG dialout $USER
stty -F /dev/ttyACM0 115200 raw -echo
cat /dev/ttyACM0  # Test communication
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pre-Flight Checks with the Ground Station&lt;/h2&gt;
&lt;p&gt;Perform the following checks on the &quot;Vehicle Setup&quot; page:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sensor calibration&lt;/strong&gt;: accelerometer, gyroscope, magnetometer, and level-horizon calibration; verify GPS signal quality (typically 6 or more satellites).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parameter checks&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SYS_COMPANION = 921600&lt;/code&gt; (ROS2 communication baud rate)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAV_1_CONFIG = TELEM2&lt;/code&gt; (matches the serial port connected to the onboard computer)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BAT_*&lt;/code&gt; parameters match the actual battery specifications&lt;/li&gt;
&lt;li&gt;Verify that the emergency stop switch functions correctly&lt;/li&gt;
&lt;li&gt;Confirm that the RC transmitter channel mapping is correct&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: When using Offboard mode in real flights, always be ready to take over manually with the RC transmitter to ensure safety. Once all checks are complete, QGroundControl should display &quot;Ready for Fly&quot;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Remote Connection to the Onboard Computer via SSH&lt;/h2&gt;
&lt;p&gt;SSH (Secure Shell) is a network security protocol that provides secure remote access and file transfer through encryption and authentication. Because the onboard computer is attached to the drone and flies with it while issuing real-time ROS control commands to the flight controller, we cannot operate it directly. Instead, we use SSH to connect from the local PC to the remote onboard computer, enabling remote control of the flight controller through the onboard computer.&lt;/p&gt;
&lt;p&gt;On the onboard computer, enable SSH and set it to start automatically at boot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl start ssh
sudo systemctl enable ssh
sudo systemctl status ssh  # Should show active (running)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensure both computers are on the same local network. Using a personal hotspot is recommended to avoid client isolation on public networks. Look up the IP address:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ifconfig  # Look for output such as inet 10.192.90.46 netmask 255.255.0.0 broadcast 10.192.255.255
# Here 10.192.90.46 is the IPv4 address; netmask and broadcast are the subnet mask and broadcast address respectively
# Verify the IPv4 addresses of both computers to ensure they are on the same large subnet
ping 10.192.90.46  # From the local PC, ping the onboard computer to verify connectivity
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSH connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh jetson@10.192.90.46  # First time: type &quot;yes&quot; to confirm, then enter the password
# Once the terminal prompt changes to jetson@ubuntu:~$, the connection is successful
# Type exit to close the SSH session
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Offboard Mode Control with MAVROS&lt;/h2&gt;
&lt;p&gt;In the simulation articles we used MicroXRCEAgent for ROS2-PX4 communication. The author&apos;s flight controller firmware includes MAVROS (rather than the XRCE middleware). MAVROS is a crucial tool bridging ROS and the MAVLINK protocol. The MAVROS2 package for ROS2 is still under active development and maintenance, but it is already usable for simple tasks.&lt;/p&gt;
&lt;p&gt;In the future, the author plans to explore development with the XRCE middleware, which will require re-flashing the PX4 firmware. Update: ROS2+PX4 Drone Swarm Real-Flight (Part 3): Deploying the UXRCE-DDS Middleware (with Pixhawk 6C)&lt;/p&gt;
&lt;h3&gt;Installing MAVROS2&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install ros-humble-mavros ros-humble-mavros-msgs

# Install the GeographicLib geographic datasets (required dependency)
wget https://raw.githubusercontent.com/mavlink/mavros/master/mavros/scripts/install_geographiclib_datasets.sh
chmod +x install_geographiclib_datasets.sh
sudo ./install_geographiclib_datasets.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Creating the Offboard Node&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python offboard_control \
    --dependencies rclpy geometry_msgs mavros_msgs
cd offboard_control/offboard_control
touch simple_offboard.py &amp;#x26;&amp;#x26; chmod +x simple_offboard.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Complete source code of &lt;code&gt;simple_offboard.py&lt;/code&gt; (Offboard takeoff and hover at 5 meters):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3

import rclpy
from rclpy.node import Node
from rclpy.clock import Clock
from geometry_msgs.msg import PoseStamped
from mavros_msgs.msg import State
from mavros_msgs.srv import CommandBool, SetMode

class SimpleOffboard(Node):

    def __init__(self):
        super().__init__(&apos;simple_offboard&apos;)

        # mavros
        self.declare_parameter(&apos;mavros_ns&apos;, &apos;/mavros&apos;)
        self.mavros_ns = self.get_parameter(&apos;mavros_ns&apos;).get_parameter_value().string_value

        # publishers
        self.local_pos_pub = self.create_publisher(
            PoseStamped,
            f&apos;{self.mavros_ns}/setpoint_position/local&apos;,
            10
        )

        # subscribers
        self.state_sub = self.create_subscription(
            State,
            f&apos;{self.mavros_ns}/state&apos;,
            self.state_callback,
            10
        )

        # service clients
        self.arming_client = self.create_client(
            CommandBool,
            f&apos;{self.mavros_ns}/cmd/arming&apos;
        )
        while not self.arming_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info(&apos;mavros/arming service not available, waiting...&apos;)

        self.set_mode_client = self.create_client(
            SetMode,
            f&apos;{self.mavros_ns}/set_mode&apos;
        )
        while not self.set_mode_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info(&apos;mavros/set_mode service not available, waiting...&apos;)

        self.current_state = State()  
        self.timer = self.create_timer(0.05, self.control_loop)  
        self.offboard_setpoint_counter = 0
        self.last_call_time = self.get_clock().now()
        self.target_altitude = 5.0 

        self.get_logger().info(&quot;Offboard Node Initialized. Waiting for MAVROS state...&quot;)

    def state_callback(self, msg):
        self.current_state = msg

    def arm_drone(self):
        self.get_logger().info(&quot;Attempting to ARM drone...&quot;)
        arm_request = CommandBool.Request()
        arm_request.value = True
        future = self.arming_client.call_async(arm_request)
        future.add_done_callback(self.arm_response_callback)

    def arm_response_callback(self, future):
        try:
            response = future.result()
            if response.success:
                self.get_logger().info(&quot;Arming successful!&quot;)
            else:
                self.get_logger().warn(f&quot;Arming failed: {response.result}&quot;)
        except Exception as e:
            self.get_logger().error(f&quot;Arming service call failed: {str(e)}&quot;)

    def set_offboard_mode(self):
        self.get_logger().info(&quot;Attempting to set OFFBOARD mode...&quot;)
        mode_request = SetMode.Request()
        mode_request.custom_mode = &quot;OFFBOARD&quot;
        future = self.set_mode_client.call_async(mode_request)
        future.add_done_callback(self.set_mode_response_callback)

    def set_mode_response_callback(self, future):
        try:
            response = future.result()
            if response.mode_sent:
                self.get_logger().info(&quot;OFFBOARD mode enabled!&quot;)
            else:
                self.get_logger().warn(f&quot;Failed to set OFFBOARD mode.&quot;)
        except Exception as e:
            self.get_logger().error(f&quot;SetMode service call failed: {str(e)}&quot;)

    def control_loop(self):
        now = self.get_clock().now()

        self.get_logger().info(
            f&quot;[State] Connected: {self.current_state.connected}, &quot;
            f&quot;Armed: {self.current_state.armed}, &quot;
            f&quot;Mode: {self.current_state.mode}&quot;,
            throttle_duration_sec=1.0  
        )

        pose = PoseStamped()
        pose.header.stamp = now.to_msg()
        pose.header.frame_id = &quot;map&quot;  
        pose.pose.position.x = 0.0
        pose.pose.position.y = 0.0
        pose.pose.position.z = float(self.target_altitude)
        self.local_pos_pub.publish(pose)

        if not self.current_state.connected:
            return

        time_since_last_call = (now - self.last_call_time).nanoseconds / 1e9  

        if time_since_last_call &gt; 5.0:  
            if not self.current_state.armed:  
                self.arm_drone()
                self.last_call_time = now
            elif self.current_state.mode != &quot;OFFBOARD&quot;: 
                self.set_offboard_mode()
                self.last_call_time = now

def main(args=None):
    rclpy.init(args=args)
    offboard_node = SimpleOffboard()

    try:
        rclpy.spin(offboard_node)
    except KeyboardInterrupt:
        offboard_node.get_logger().info(&apos;Offboard control interrupted by user.&apos;)
    finally:
        offboard_node.destroy_node()
        rclpy.shutdown()
        print(&quot;Offboard Node Shutdown.&quot;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following to &lt;code&gt;entry_points&lt;/code&gt; in &lt;code&gt;setup.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&apos;console_scripts&apos;: [
    &apos;simple_offboard = offboard_control.simple_offboard:main&apos;,
],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
colcon build --symlink-install --packages-select offboard_control
source install/setup.bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Takeoff&lt;/h2&gt;
&lt;p&gt;With all hardware and software preparations complete, execute the full takeoff procedure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Connect the drone to the ground station via the telemetry radio&lt;/li&gt;
&lt;li&gt;Connect the onboard computer to the flight controller using any of the three methods&lt;/li&gt;
&lt;li&gt;Complete the pre-flight checks in the ground station&lt;/li&gt;
&lt;li&gt;Use SSH to remotely connect from the local PC to the onboard computer&lt;/li&gt;
&lt;li&gt;In the SSH terminal, execute:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Activate the ROS2 environment
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash

# Launch the MAVROS2 node (serial port and baud rate must match the flight controller configuration)
ros2 run mavros mavros_node --ros-args -p fcu_url:=&quot;serial:///dev/ttyACM0:115200&quot;
# When you see [INFO] [mavros]: FCU connection established, the link is up

# Launch the Offboard control node
ros2 run offboard_control simple_offboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point you should see the drone arm, take off, and ascend to 5 meters. Due to the inherent risk of Offboard mode, always be ready to take over with the RC transmitter.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Practice makes perfect -- get hands-on and iterate. This article has presented one complete workflow: from telemetry radio connection, through three hardware connection methods between the onboard computer and flight controller, pre-flight checks, SSH remote access, and finally MAVROS2 Offboard takeoff. Corrections and feedback are welcome. Thank you for reading!&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.amovlab.com/static/pdf/JCV-600%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8C.pdf&quot;&gt;AMOVLAB JCV-600 User Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/&quot;&gt;PX4 Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ROS2+PX4 Drone Swarm Real-Flight (Part 3): Deploying the UXRCE-DDS Middleware&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>使用机载电脑控制PX4无人机（基于MAVROS2）</title><link>https://duduuu.xyz/zh/posts/px4-ros2-mavros</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-mavros</guid><description>以Jetson Orin NX+JCV-600为例，介绍机载电脑与飞控的三种连接方式、地面站预检查、SSH远程连接及MAVROS2 Offboard起飞流程</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在真机部分的 &lt;a href=&quot;/zh/posts/px4-ros2-jetson-flash&quot;&gt;上篇文章&lt;/a&gt; 中，我们完成了机载电脑的刷机。本文基于已有工作，利用机载电脑和底层飞控，跑通 ROS2 + PX4 的真机起飞流程。如果你是第一次接触 PX4 仿真，建议先从 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真环境开发教程&lt;/a&gt; 开始。&lt;/p&gt;
&lt;p&gt;笔者使用阿木实验室（AMOVLAB）的 JCV-600 科研实验四旋翼飞机（底层飞控为 CodevDynamics 基于 PX4 的 Codev-autopilot），使用 Nvidia Jetson Orin NX 作为机载电脑进行 ROS2 控制。读者可以仿照本教程，利用自己的硬件设备进行相似实验。&lt;/p&gt;
&lt;p&gt;需要的硬件设备（根据自己的情况调整）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无人机整机套件或自行刷入飞控的无人机&lt;/li&gt;
&lt;li&gt;一对数传&lt;/li&gt;
&lt;li&gt;本地电脑与机载电脑&lt;/li&gt;
&lt;li&gt;相应的连接线若干（参考机载电脑与无人机 I/O 口的具体说明）&lt;/li&gt;
&lt;li&gt;飞机相关接口的示意图/说明书&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;流程总述&lt;/h2&gt;
&lt;p&gt;下图是本文采用的真机起飞流程的简要示意图。首先通过数传建立地面站与飞控的稳定连接，确保传感器数据、GPS 信号和遥控指令正常传输。随后，机载电脑通过串口与飞控通信，完成驱动安装、权限配置及波特率匹配。在地面站进行飞行前检查通过后，通过 SSH 远程连接机载电脑，启动 MAVROS2 节点，建立 ROS2 与 PX4 的通信链路。最后，使用 Offboard 模式控制无人机。整个过程需全程连接数传，关注地面站数据，随时准备手动接管，确保飞行安全。&lt;/p&gt;
&lt;h2&gt;与地面站建立连接&lt;/h2&gt;
&lt;p&gt;在整个实验过程中，PX4 飞控严格要求与地面站的稳定连接。笔者使用一对 RFD900x 数传进行连接。无人机上的数传与飞控的 I/O 口连接（GND、RX-TX、TX-RX 交叉连接），飞控预留了 5V 电源口为数传供电，这是内部已经完成了电源分配。&lt;/p&gt;
&lt;p&gt;在本地电脑中，打开 QGroundControl 地面站，在 &quot;Application Settings&quot; → &quot;Comm Links&quot; → &quot;Add New Link&quot; 中，Type 选择 &quot;Serial&quot;，Serial Port 选择含 &quot;USB0&quot; 的选项，波特率默认 57600。连接后，两个数传 &quot;ACT&quot; 灯常绿（供电正常），&quot;COM&quot; 灯闪烁（连接成功），地面站可以看到飞机各项状态。&lt;/p&gt;
&lt;h2&gt;机载电脑与飞控连接&lt;/h2&gt;
&lt;p&gt;首先，用 USB-TypeC 线将飞控连接到电脑地面站，在参数页面为机载电脑添加 MAVLINK 实例（一般默认为 TELEM2 串口）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MAV_1_CONFIG = TELEM2
MAV_1_MODE = Onboard
MAV_1_RATE = 115200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下介绍三种连接方式。&lt;/p&gt;
&lt;h3&gt;方式一：USB-TTL 线&lt;/h3&gt;
&lt;p&gt;使用 PX4 的 TELEM2 接口，用于连接机载电脑、数传等，有 VCC(5V)、TX、RX、PWM、GPIO、GND 六个引脚可供选择。将 USB 连接在机载电脑端，用杜邦转端子线交叉连接飞控的 GND、TX、RX，不要进行供电。机载电脑使用预留的 12V 供电口单独供电。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 检查 USB 连接
lsusb
ls -l /dev/tty*
# 应出现 CH340 USB-Serial adapter 和 /dev/ttyS0

# 将用户加入 dialout 组
sudo usermod -a -G dialout $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载并编译 CH340 驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install build-essential git linux-headers-$(uname -r)
git clone https://github.com/juliagoda/CH341SER
cd CH341SER
make &amp;#x26;&amp;#x26; sudo make install
sudo insmod ch341.ko
sudo cp ch341.ko /lib/modules/$(uname -r)/kernel/drivers/usb/serial/
sudo depmod -a
echo &quot;ch341&quot; | sudo tee /etc/modules-load.d/ch341.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 udev 规则 &lt;code&gt;/etc/udev/rules.d/99-ch341.rules&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SUBSYSTEM==&quot;tty&quot;, ATTRS{idVendor}==&quot;1a86&quot;, ATTRS{idProduct}==&quot;7523&quot;, MODE=&quot;0666&quot;, SYMLINK+=&quot;ttyCH341&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo udevadm control --reload-rules &amp;#x26;&amp;#x26; sudo udevadm trigger
ls -l /dev/ttyCH341  # 验证驱动安装
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试通信（如出现 MAVLINK 乱码则说明成功）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stty -F /dev/ttyUSB0 115200 raw -echo
cat /dev/ttyUSB0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果上述 USB-TTL 连接方式出现问题，可以参考以下链接进行驱动安装：https://blog.csdn.net/qq_52102933/article/details/126839474&lt;/p&gt;
&lt;h3&gt;方式二：UART 串口线&lt;/h3&gt;
&lt;p&gt;Jetson Orin NX 有 40 针 UART 引脚，其中 Pin8(TX)、Pin10(RX) 为 UART1 默认调试口。我们使用 UART2 进行连接：Pin 14(RX)、Pin 12(TX)、Pin 6(GND)，不连 VCC，单独供电。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看可用 UART
ls /dev/ttyTHS*

# 如缺少 ttyTHS1，编辑 /boot/extlinux/extlinux.conf
# 在内核启动行末尾添加 console=ttyTHS1,115200
sudo reboot

# 配置权限和波特率
sudo usermod -aG dialout $USER
stty -F /dev/ttyTHS1 115200 raw -echo
cat /dev/ttyTHS1  # 测试通信
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方式三：USB-TypeC 线&lt;/h3&gt;
&lt;p&gt;笔者的 JCV-600 的 TypeC 接口可直接连接机载电脑（仅供参考，不一定适用于所有设备）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;lsusb  # 应出现 ID 26ac:0032 The Autopilot PX4 CODEV DP1000
sudo usermod -aG dialout $USER
stty -F /dev/ttyACM0 115200 raw -echo
cat /dev/ttyACM0  # 测试通信
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;利用地面站做预检查&lt;/h2&gt;
&lt;p&gt;在 &quot;Vehicle Setup&quot; 页面进行飞行前检查：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传感器校准&lt;/strong&gt;：加速度计、陀螺仪、磁罗盘、水平校准；确认 GPS 信号质量（一般 6 颗星以上）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参数检查&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SYS_COMPANION = 921600&lt;/code&gt;（ROS2 通信波特率）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAV_1_CONFIG = TELEM2&lt;/code&gt;（对应机载电脑连接的串口）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BAT_*&lt;/code&gt; 参数与实际电池规格匹配&lt;/li&gt;
&lt;li&gt;检查紧急停止开关功能正常&lt;/li&gt;
&lt;li&gt;验证遥控器各通道映射正确&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要&lt;/strong&gt;：在实机飞行中使用 Offboard 模式时，需随时准备用遥控器手动接管飞行，以确保安全。检查完成后，QGC 地面站应显示 &quot;Ready for Fly&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用 SSH 远程连接机载电脑&lt;/h2&gt;
&lt;p&gt;SSH（Secure Shell）是一种网络安全协议，通过加密和认证机制实现安全的访问和文件传输等业务。由于机载电脑连接在飞控上，并且直接向飞控实时发布 ROS 控制指令，与无人机一起飞行，因此我们不能够直接在机载电脑上发布控制指令。在这里我们使用 SSH 协议将本地电脑与远程的机载电脑进行连接，实现本地电脑远程控制机载电脑从而实现对飞控的控制。&lt;/p&gt;
&lt;p&gt;在机载电脑的终端启用 SSH 并且设置开机自启：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl start ssh
sudo systemctl enable ssh
sudo systemctl status ssh  # 应显示 active (running)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保两台电脑位于同一局域网内，使用个人热点避免公网的客户端隔离。查询 IP 地址：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ifconfig  # 查找形如 inet 10.192.90.46 netmask 255.255.0.0 broadcast 10.192.255.255 的输出
# 其中 10.192.90.46 为 IPv4 地址，netmask 和 broadcast 分别为子网掩码和广播地址
# 验证两台电脑的 IPv4 地址，确保在同一大子网下
ping 10.192.90.46  # 在本地电脑 ping 机载电脑，验证网络连通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SSH 连接：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh jetson@10.192.90.46  # 首次连接需输入 yes 确认，然后输入密码
# 终端用户名变为 jetson@ubuntu:~$ 说明连接成功
# 输入 exit 退出 SSH 连接
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 MAVROS 进行 Offboard 模式控制&lt;/h2&gt;
&lt;p&gt;在仿真篇章中我们使用 MicroXRCEAgent 进行 ROS2 与 PX4 的通信。笔者的飞控固件中刷入了 MAVROS（而非 XRCE 中间件）。MAVROS 是连接 ROS 与 MAVLINK 协议的重要工具。针对 ROS2 的 MAVROS2 功能包仍在开发维护中，但对于简单任务已可使用。&lt;/p&gt;
&lt;p&gt;后续，笔者会针对 XRCE 中间件的开发进行探索，这需要重新刷写 PX4 固件。已更新：ROS2+PX4 无人机编队实机（三）UXRCE-DDS 中间件的部署（以 Pixhawk 6C 为例）&lt;/p&gt;
&lt;h3&gt;安装 MAVROS2&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install ros-humble-mavros ros-humble-mavros-msgs

# 安装 GeographicLib 地理数据集（必需依赖）
wget https://raw.githubusercontent.com/mavlink/mavros/master/mavros/scripts/install_geographiclib_datasets.sh
chmod +x install_geographiclib_datasets.sh
sudo ./install_geographiclib_datasets.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建 Offboard 节点&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python offboard_control \
    --dependencies rclpy geometry_msgs mavros_msgs
cd offboard_control/offboard_control
touch simple_offboard.py &amp;#x26;&amp;#x26; chmod +x simple_offboard.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;simple_offboard.py&lt;/code&gt; 完整源码（Offboard 起飞并悬停 5 米）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3

import rclpy
from rclpy.node import Node
from rclpy.clock import Clock
from geometry_msgs.msg import PoseStamped
from mavros_msgs.msg import State
from mavros_msgs.srv import CommandBool, SetMode

class SimpleOffboard(Node):

    def __init__(self):
        super().__init__(&apos;simple_offboard&apos;)

        # mavros
        self.declare_parameter(&apos;mavros_ns&apos;, &apos;/mavros&apos;)
        self.mavros_ns = self.get_parameter(&apos;mavros_ns&apos;).get_parameter_value().string_value

        # publishers
        self.local_pos_pub = self.create_publisher(
            PoseStamped,
            f&apos;{self.mavros_ns}/setpoint_position/local&apos;,
            10
        )

        # subscribers
        self.state_sub = self.create_subscription(
            State,
            f&apos;{self.mavros_ns}/state&apos;,
            self.state_callback,
            10
        )

        # service clients
        self.arming_client = self.create_client(
            CommandBool,
            f&apos;{self.mavros_ns}/cmd/arming&apos;
        )
        while not self.arming_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info(&apos;mavros/arming service not available, waiting...&apos;)

        self.set_mode_client = self.create_client(
            SetMode,
            f&apos;{self.mavros_ns}/set_mode&apos;
        )
        while not self.set_mode_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info(&apos;mavros/set_mode service not available, waiting...&apos;)

        self.current_state = State()  
        self.timer = self.create_timer(0.05, self.control_loop)  
        self.offboard_setpoint_counter = 0
        self.last_call_time = self.get_clock().now()
        self.target_altitude = 5.0 

        self.get_logger().info(&quot;Offboard Node Initialized. Waiting for MAVROS state...&quot;)

    def state_callback(self, msg):
        self.current_state = msg

    def arm_drone(self):
        self.get_logger().info(&quot;Attempting to ARM drone...&quot;)
        arm_request = CommandBool.Request()
        arm_request.value = True
        future = self.arming_client.call_async(arm_request)
        future.add_done_callback(self.arm_response_callback)

    def arm_response_callback(self, future):
        try:
            response = future.result()
            if response.success:
                self.get_logger().info(&quot;Arming successful!&quot;)
            else:
                self.get_logger().warn(f&quot;Arming failed: {response.result}&quot;)
        except Exception as e:
            self.get_logger().error(f&quot;Arming service call failed: {str(e)}&quot;)

    def set_offboard_mode(self):
        self.get_logger().info(&quot;Attempting to set OFFBOARD mode...&quot;)
        mode_request = SetMode.Request()
        mode_request.custom_mode = &quot;OFFBOARD&quot;
        future = self.set_mode_client.call_async(mode_request)
        future.add_done_callback(self.set_mode_response_callback)

    def set_mode_response_callback(self, future):
        try:
            response = future.result()
            if response.mode_sent:
                self.get_logger().info(&quot;OFFBOARD mode enabled!&quot;)
            else:
                self.get_logger().warn(f&quot;Failed to set OFFBOARD mode.&quot;)
        except Exception as e:
            self.get_logger().error(f&quot;SetMode service call failed: {str(e)}&quot;)

    def control_loop(self):
        now = self.get_clock().now()

        self.get_logger().info(
            f&quot;[State] Connected: {self.current_state.connected}, &quot;
            f&quot;Armed: {self.current_state.armed}, &quot;
            f&quot;Mode: {self.current_state.mode}&quot;,
            throttle_duration_sec=1.0  
        )

        pose = PoseStamped()
        pose.header.stamp = now.to_msg()
        pose.header.frame_id = &quot;map&quot;  
        pose.pose.position.x = 0.0
        pose.pose.position.y = 0.0
        pose.pose.position.z = float(self.target_altitude)
        self.local_pos_pub.publish(pose)

        if not self.current_state.connected:
            return

        time_since_last_call = (now - self.last_call_time).nanoseconds / 1e9  

        if time_since_last_call &gt; 5.0:  
            if not self.current_state.armed:  
                self.arm_drone()
                self.last_call_time = now
            elif self.current_state.mode != &quot;OFFBOARD&quot;: 
                self.set_offboard_mode()
                self.last_call_time = now

def main(args=None):
    rclpy.init(args=args)
    offboard_node = SimpleOffboard()

    try:
        rclpy.spin(offboard_node)
    except KeyboardInterrupt:
        offboard_node.get_logger().info(&apos;Offboard control interrupted by user.&apos;)
    finally:
        offboard_node.destroy_node()
        rclpy.shutdown()
        print(&quot;Offboard Node Shutdown.&quot;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;setup.py&lt;/code&gt; 的 &lt;code&gt;entry_points&lt;/code&gt; 中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&apos;console_scripts&apos;: [
    &apos;simple_offboard = offboard_control.simple_offboard:main&apos;,
],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
colcon build --symlink-install --packages-select offboard_control
source install/setup.bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;起飞&lt;/h2&gt;
&lt;p&gt;所有软硬件准备完成，执行完整起飞流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过数传将无人机与地面站连接&lt;/li&gt;
&lt;li&gt;任选一种方式将机载电脑与飞控连接&lt;/li&gt;
&lt;li&gt;地面站完成飞行前预检查&lt;/li&gt;
&lt;li&gt;使用 SSH 将本地电脑与机载电脑远程连接&lt;/li&gt;
&lt;li&gt;在 SSH 终端中执行：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 激活 ROS2 环境
source /opt/ros/humble/setup.bash
source ~/ros2_ws/install/setup.bash

# 启动 MAVROS2 节点（串口和波特率需与飞控配置一致）
ros2 run mavros mavros_node --ros-args -p fcu_url:=&quot;serial:///dev/ttyACM0:115200&quot;
# 出现 [INFO] [mavros]: FCU connection established 说明连接成功

# 启动 Offboard 控制节点
ros2 run offboard_control simple_offboard
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时应看到无人机解锁、起飞、飞至 5 米高度。由于 Offboard 模式的危险性，需随时准备使用遥控器接管飞行。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;勤能补拙，多动手尝试。本文给出了笔者自己的一种操作方式——从数传连接、机载电脑与飞控的三种硬件连接方式、飞行前检查、SSH 远程连接，到最终的 MAVROS2 Offboard 起飞。如有错漏请指正，感谢各位读者的耐心阅读！&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://wiki.amovlab.com/static/pdf/JCV-600%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8C.pdf&quot;&gt;AMOVLAB JCV-600 使用手册&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/&quot;&gt;PX4 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ROS2+PX4 无人机编队实机（三）UXRCE-DDS 中间件的部署&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Betaflight + Gazebo Software-In-The-Loop Simulation Tutorial</title><link>https://duduuu.xyz/en/posts/px4-ros2-betaflight-sitl</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-betaflight-sitl</guid><description>Complete guide to Betaflight SITL + Gazebo Harmonic on Ubuntu 22.04, including plugin compilation, CLI arming config, and Python closed-loop control</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;About ROS2&lt;/strong&gt;: This tutorial&apos;s simulation link (Gazebo ↔ SITL ↔ Controller) uses direct UDP communication without ROS2. ROS2&apos;s ros_gz_bridge can be added for multi-drone, distributed communication, or ROS2 ecosystem integration.&lt;/p&gt;
&lt;p&gt;This tutorial is fairly complex and focuses on explaining the overall framework. Full source files are available at the &lt;a href=&quot;https://github.com/Jinyao-Chen/bf_sitl&quot;&gt;GitHub repository&lt;/a&gt; — contributions and stars appreciated!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Betaflight is the most popular open-source flight controller firmware in FPV Racing and Freestyle drone communities, widely deployed on STM32/AT32 MCUs. Betaflight provides SITL (Software-In-The-Loop) mode, enabling developers to run the complete Betaflight firmware within the Gazebo physics simulation engine.&lt;/p&gt;
&lt;p&gt;The core idea behind SITL: compile the Betaflight firmware as an x86_64 Linux executable. The flight controller&apos;s sensor inputs (IMU, barometer, GPS) come from the Gazebo simulation environment via UDP packets containing simulated sensor data. The flight controller&apos;s motor outputs (PWM commands) are likewise sent back to Gazebo via UDP to drive the propellers in the physics engine. This enables developers to fully validate flight controller logic and control algorithms on a PC, with no real hardware required.&lt;/p&gt;
&lt;p&gt;This article provides a complete walkthrough for setting up a Betaflight SITL + Gazebo Harmonic + Python autonomous control simulation environment on Ubuntu 22.04. Before we begin, credit goes to the OSRF vehicle_gateway project and the Betaflight community for their open-source code and documentation — this tutorial draws heavily on those resources.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Following the author&apos;s earlier &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 simulation tutorials&lt;/a&gt;, the following foundational environment should already be configured:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04&lt;/li&gt;
&lt;li&gt;ROS2 Humble (recommended: FishROS one-line installer — &lt;code&gt;wget http://fishros.com/install -O fishros &amp;#x26;&amp;#x26; . fishros&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Gazebo Harmonic (gz-sim8, version 8.9.0)&lt;/li&gt;
&lt;li&gt;ROS2-Gazebo communication bridge &lt;code&gt;ros-humble-ros-gzharmonic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verify in a terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 --version       # Output should contain &quot;humble&quot;
gz sim --versions    # Output should be &quot;Gazebo Sim 8.9.0&quot;
dpkg -l | grep ros-humble-ros-gzharmonic  # Should show installed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install required tools and &lt;strong&gt;Gazebo dev packages&lt;/strong&gt; (needed for compiling plugins — the &lt;code&gt;gz-harmonic&lt;/code&gt; runtime does NOT include these headers):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip3 install --user websockify
sudo apt-get install -y socat

# Gazebo Harmonic dev packages — required for compiling custom Gazebo plugins!
sudo apt-get install -y \
    libgz-sim8-dev \
    libgz-transport13-dev \
    libgz-msgs10-dev \
    libgz-math7-dev \
    libgz-plugin2-dev \
    libsdformat14-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;websockify&lt;/code&gt;: provides WebSocket → TCP proxying, used to connect to the Betaflight Configurator ground-station tool in a browser&lt;/li&gt;
&lt;li&gt;&lt;code&gt;socat&lt;/code&gt;: creates virtual serial ports (pty), mapping Betaflight&apos;s TCP UART to a local virtual serial device&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ensure your network can reliably access GitHub, otherwise large repository clones may fail partway through.&lt;/p&gt;
&lt;h2&gt;Simulation Link Architecture&lt;/h2&gt;
&lt;p&gt;Before diving into configuration, let us examine the full simulation link — this helps with diagnosing issues later. The simulation link consists of four processes that communicate via UDP ports:&lt;/p&gt;
&lt;p&gt;Port allocation and data flow (Betaflight source &lt;code&gt;sitl.c&lt;/code&gt; lines 192–195):&lt;/p&gt;
&lt;p&gt;| Port | Direction | Purpose | Packet Type |
|------|-----------|---------|-------------|
| 9001 | SITL → External | Raw PWM (reserved for RealFlightBridge) | &lt;code&gt;servo_packet_raw&lt;/code&gt; |
| 9002 | SITL → Gazebo | Motor speed commands [0.0, 1.0] (3D mode: [-1.0, 1.0]) | &lt;code&gt;servo_packet&lt;/code&gt; |
| 9003 | Gazebo → SITL | Flight state data (IMU, position, velocity, attitude, pressure) | &lt;code&gt;fdm_packet&lt;/code&gt; |
| 9004 | Controller → SITL | 16-channel RC control signals [1000–2000] | &lt;code&gt;rc_packet&lt;/code&gt; |
| TCP 5761 | Bidirectional | MSP protocol (configuration + telemetry feedback) | N/A |&lt;/p&gt;
&lt;p&gt;Packet structures (Betaflight source &lt;code&gt;target.h&lt;/code&gt; lines 224–246):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// fdm_packet: Gazebo plugin → SITL
typedef struct {
    double timestamp;
    double imu_angular_velocity_rpy[3];    // Angular velocity (rad/s)
    double imu_linear_acceleration_xyz[3]; // Linear acceleration (m/s²)
    double imu_orientation_quat[4];        // Attitude quaternion (w,x,y,z)
    double velocity_xyz[3];                // Velocity (m/s, ENU frame)
    double position_xyz[3];                // Position
    double pressure;                       // Pressure (Pa)
} fdm_packet;

// servo_packet: SITL → Gazebo plugin
typedef struct {
    float motor_speed[4];   // [0.0, 1.0] or 3D mode [-1.0, 1.0]
} servo_packet;

// rc_packet: Controller → SITL
typedef struct {
    double timestamp;
    uint16_t channels[16];   // PWM [1000, 2000]
} rc_packet;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Gazebo plugin (BetaflightPlugin) runs two phases per simulation update cycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PreUpdate (before simulation step)&lt;/strong&gt;: Calls &lt;code&gt;ReceiveServoPacket()&lt;/code&gt; to read &lt;code&gt;servo_packet&lt;/code&gt; from UDP 9002, converts motor speeds into joint force/velocity commands&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostUpdate (after simulation step)&lt;/strong&gt;: Reads angular velocity and acceleration from the IMU sensor, obtains pose and linear velocity from the Gazebo Entity Component Manager, populates an &lt;code&gt;fdm_packet&lt;/code&gt;, applies coordinate frame transformations, and sends it via UDP to 127.0.0.1:9003&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Compiling Betaflight SITL&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~
git clone https://github.com/betaflight/betaflight.git
cd betaflight
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: We use the master branch (version 2026.6.0-alpha), not the 4.5.x release, because the 4.5.x stable release&apos;s SITL code lacks native Gazebo support — the &lt;code&gt;ENABLE_GAZEBO_BRIDGE&lt;/code&gt; feature was only introduced into master after 4.5.4.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unlike compiling PX4 which requires an ARM cross-compilation toolchain, the SITL mode compiles directly with the host GCC:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make TARGET=SITL -j$(nproc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Successful build output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Linking SITL
   text    data     bss     dec     hex  filename
 389784   21804   77376  488964   77604  ./obj/main/betaflight_SITL.elf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./obj/main/betaflight_SITL.elf --help
# Output: Betaflight SITL Usage: ./obj/main/betaflight_SITL.elf [options]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why can SITL run standalone? Because Betaflight SITL emulates the real flight controller&apos;s hardware peripherals in software: virtual IMU (&lt;code&gt;accgyro_virtual.c&lt;/code&gt;) receives angular velocity and acceleration data from Gazebo; virtual barometer (&lt;code&gt;barometer_virtual.c&lt;/code&gt;) receives pressure/altitude data; virtual GPS (&lt;code&gt;gps_virtual.c&lt;/code&gt;) receives position data; virtual EEPROM (file I/O) stores configuration as &lt;code&gt;eeprom.bin&lt;/code&gt;; virtual UART (&lt;code&gt;serial_tcp.c&lt;/code&gt;) replaces physical UART with TCP.&lt;/p&gt;
&lt;p&gt;After compilation, it is advisable to back up: &lt;code&gt;zip -r betaflight.zip betaflight/&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Obtaining and Compiling the Gazebo Plugin&lt;/h2&gt;
&lt;p&gt;Betaflight itself does not include a Gazebo plugin. We need the &lt;code&gt;betaflight_gazebo&lt;/code&gt; plugin from OSRF&apos;s vehicle_gateway project. Important: vehicle_gateway officially targets Gazebo Garden (gz-sim7), while our environment uses Gazebo Harmonic (gz-sim8), requiring compatibility modifications.&lt;/p&gt;
&lt;h3&gt;Step 1: Clone and exclude unneeded sub-packages&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
git clone https://github.com/osrf/vehicle_gateway.git vehicle_gateway

cd ~/ros2_ws/src/vehicle_gateway
for pkg in gz_aerial_plugins vehicle_gateway vehicle_gateway_betaflight \
    vehicle_gateway_demo vehicle_gateway_integration_test vehicle_gateway_multi \
    vehicle_gateway_px4 vehicle_gateway_python vehicle_gateway_python_helpers \
    vehicle_gateway_sim_performance px4_sim betaflight_configurator \
    betaflight_demo qgroundcontrol; do
    touch &quot;$pkg/COLCON_IGNORE&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Adapting for Gazebo Harmonic (2 files need version number updates; the other 3 code fixes are already upstream)&lt;/h3&gt;
&lt;p&gt;The current upstream OSRF vehicle_gateway code already includes most of the Gazebo Harmonic compatibility fixes. Only the library version numbers in two files still need manual changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Required changes — Update library version numbers in both CMakeLists.txt and package.xml&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(1) Edit &lt;code&gt;~/ros2_ws/src/vehicle_gateway/betaflight_gazebo/CMakeLists.txt&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Replace all occurrences (including &lt;code&gt;find_package&lt;/code&gt;, &lt;code&gt;target_link_libraries&lt;/code&gt;, and any other references). Search globally for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All &lt;code&gt;gz-sim7&lt;/code&gt; → &lt;code&gt;gz-sim8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;All &lt;code&gt;gz-transport12&lt;/code&gt; → &lt;code&gt;gz-transport13&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;(2) Edit &lt;code&gt;~/ros2_ws/src/vehicle_gateway/betaflight_gazebo/package.xml&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Update the &lt;code&gt;&amp;#x3C;depend&gt;&lt;/code&gt; declarations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;depend&gt;gz-sim7&amp;#x3C;/depend&gt;&lt;/code&gt; → &lt;code&gt;&amp;#x3C;depend&gt;gz-sim8&amp;#x3C;/depend&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;depend&gt;gz-transport12&amp;#x3C;/depend&gt;&lt;/code&gt; → &lt;code&gt;&amp;#x3C;depend&gt;gz-transport13&amp;#x3C;/depend&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Gazebo Garden (gz-sim7, gz-transport12) and Gazebo Harmonic (gz-sim8, gz-transport13) have incompatible ABIs. The plugin must link against the same library versions as the Gazebo server process. gz-math7 and gz-plugin2 are shared across both versions. The &lt;code&gt;package.xml&lt;/code&gt; must also be updated, otherwise &lt;code&gt;colcon&lt;/code&gt;/&lt;code&gt;rosdep&lt;/code&gt; will try to resolve &lt;code&gt;gz-sim7&lt;/code&gt; as a dependency and fail in a Harmonic-only environment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Verification items — The following three are already fixed upstream. No manual changes needed, but verify your clone includes them:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check 1: Header includes (BetaflightPlugin.cpp lines 35–39)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/ros2_ws/src/vehicle_gateway/betaflight_gazebo/src/BetaflightPlugin.cpp&lt;/code&gt; should already contain:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;functional&gt;
#include &amp;#x3C;gz/msgs/imu.pb.h&gt;
#include &amp;#x3C;gz/msgs/fluid_pressure.pb.h&gt;
#include &amp;#x3C;gz/msgs/double.pb.h&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;: Gazebo Harmonic (gz-sim8)&apos;s &lt;code&gt;System.hh&lt;/code&gt; no longer transitively includes protobuf message headers, so explicit includes are needed. If your clone is missing these, add them manually.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Check 2: IMU and fluid pressure subscriptions (PreUpdate lines 440–443; Configure lines 296–301)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The IMU subscription is in &lt;code&gt;PreUpdate&lt;/code&gt; (around line 440), and the pressure sensor subscription is in &lt;code&gt;Configure&lt;/code&gt; (around line 296). Both already use the &lt;code&gt;std::function&lt;/code&gt; + &lt;code&gt;std::bind&lt;/code&gt; pattern — no changes needed:&lt;/p&gt;
&lt;p&gt;IMU subscription in &lt;code&gt;PreUpdate&lt;/code&gt; (around line 440):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::function&amp;#x3C;void(const gz::msgs::IMU&amp;#x26;)&gt; imuCb =
    std::bind(&amp;#x26;BetaFlightPluginPrivate::ImuCb,
              this-&gt;dataPtr.get(), std::placeholders::_1);
this-&gt;dataPtr-&gt;node.Subscribe(imuTopicName, imuCb);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fluid pressure subscription in &lt;code&gt;Configure&lt;/code&gt; (around line 296):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::function&amp;#x3C;void(const gz::msgs::FluidPressure&amp;#x26;)&gt; airPressureCb =
    std::bind(&amp;#x26;BetaFlightPluginPrivate::onAirPressureMessageReceived,
              this-&gt;dataPtr.get(), std::placeholders::_1);
this-&gt;dataPtr-&gt;node.Subscribe(
    &quot;/world/empty_betaflight_world/model/iris_with_Betaflight/model/iris_with_standoffs/&quot;
    &quot;link/imu_link/sensor/air_pressure_sensor/air_pressure&quot;,
    airPressureCb);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Context&lt;/strong&gt;: Gazebo Garden&apos;s gz-transport12 allowed &lt;code&gt;Node::Subscribe&lt;/code&gt; to accept member function pointers directly for template deduction. gz-transport13 tightened the rules, requiring an explicit &lt;code&gt;std::function&lt;/code&gt; wrapper. The IMU callback &lt;code&gt;ImuCb&lt;/code&gt; has signature &lt;code&gt;void(const gz::msgs::IMU&amp;#x26;)&lt;/code&gt;, and the pressure callback &lt;code&gt;onAirPressureMessageReceived&lt;/code&gt; has signature &lt;code&gt;void(const gz::msgs::FluidPressure&amp;#x26;)&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Check 3: Deadlock fix in PostUpdate (lines 478–484)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;PostUpdate&lt;/code&gt; function should NOT contain the &lt;code&gt;betaflightOnline&lt;/code&gt; condition:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (!_info.paused &amp;#x26;&amp;#x26; _info.simTime &gt; this-&gt;dataPtr-&gt;lastControllerUpdateTime)
{
    double t = ...;
    this-&gt;SendState(t, _ecm);
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see &lt;code&gt;&amp;#x26;&amp;#x26; this-&gt;dataPtr-&gt;betaflightOnline&lt;/code&gt;, delete it. &lt;strong&gt;Why&lt;/strong&gt;: Initially SITL waits for Gazebo to send an &lt;code&gt;fdm_packet&lt;/code&gt;, while Gazebo skips sending &lt;code&gt;fdm_packet&lt;/code&gt; because &lt;code&gt;betaflightOnline == false&lt;/code&gt; — a deadlock that never resolves.&lt;/p&gt;
&lt;h3&gt;Step 3: Build the plugin&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
source /opt/ros/humble/setup.bash
colcon build --packages-select betaflight_gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify: &lt;code&gt;ls -lh ~/ros2_ws/install/betaflight_gazebo/lib/libBetaflightPlugin.so&lt;/code&gt; (approximately 8.7M)&lt;/p&gt;
&lt;h2&gt;Simulation World Configuration&lt;/h2&gt;
&lt;p&gt;Create the launch package directory structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/bf_sitl/{launch,worlds,models,config}
ln -sf ~/ros2_ws/src/vehicle_gateway/betaflight_sim/models/iris_with_standoffs \
    ~/bf_sitl/models/iris_with_standoffs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Core plugin configuration in the world file &lt;code&gt;betaflight_world.sdf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;plugin name=&quot;BetaFlightPlugin&quot; filename=&quot;BetaflightPlugin&quot;&gt;
    &amp;#x3C;fdm_addr&gt;127.0.0.1&amp;#x3C;/fdm_addr&gt;
    &amp;#x3C;fdm_port_in&gt;9002&amp;#x3C;/fdm_port_in&gt;
    &amp;#x3C;listen_addr&gt;127.0.0.1&amp;#x3C;/listen_addr&gt;
    &amp;#x3C;modelXYZToAirplaneXForwardZDown&gt;0 0 0 3.141593 0 0&amp;#x3C;/modelXYZToAirplaneXForwardZDown&gt;
    &amp;#x3C;gazeboXYZToNED&gt;0 0 0 3.141593 0 0&amp;#x3C;/gazeboXYZToNED&gt;
    &amp;#x3C;imuName&gt;iris_with_standoffs::imu_link::imu_sensor&amp;#x3C;/imuName&gt;
    &amp;#x3C;control channel=&quot;0&quot;&gt;
        &amp;#x3C;jointName&gt;iris_with_standoffs::rotor_0_joint&amp;#x3C;/jointName&gt;
        &amp;#x3C;multiplier&gt;838&amp;#x3C;/multiplier&gt;
    &amp;#x3C;/control&gt;
    &amp;#x3C;!-- channels 1-3 similarly configured, multipliers: 838, -838, -838 --&gt;
&amp;#x3C;/plugin&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Coordinate frame transform explained&lt;/strong&gt;: Gazebo uses ENU (X=East, Y=North, Z=Up); Betaflight expects NED (X=North, Y=East, Z=Down). &lt;code&gt;gazeboXYZToNED&lt;/code&gt; with Yaw=180° performs the heading rotation, and the plugin&apos;s internal &lt;code&gt;SendState()&lt;/code&gt; further applies an &lt;code&gt;Rz(π/2)&lt;/code&gt; rotation to complete the full transform.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Configuring ARM — Enabling the Flight Controller to Arm&lt;/h2&gt;
&lt;p&gt;Betaflight defaults to disarmed (ARM disabled). We need to configure AUX1 to enter ARM mode when its channel value is in the 1700–2100 range. First, start SITL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/bf_sitl/config
~/betaflight/obj/main/betaflight_SITL.elf --ip 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wait for &lt;code&gt;bind port 5761 for UART1&lt;/code&gt;, then run the CLI configuration script &lt;code&gt;bf_cli_config.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;Configure Betaflight SITL via TCP CLI — set ARM on AUX1, save to eeprom&quot;&quot;&quot;
import socket, time, sys

HOST, PORT = &apos;127.0.0.1&apos;, 5761

print(&quot;Waiting for SITL TCP port 5761...&quot;)
for i in range(30):
    try:
        s = socket.socket(); s.settimeout(1)
        s.connect((HOST, PORT)); s.close()
        print(f&quot;Connected after {i+1}s&quot;); break
    except:
        time.sleep(1)
else:
    print(&quot;ERROR: SITL not ready after 30s&quot;); sys.exit(1)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((HOST, PORT))
time.sleep(2)

# Drain initial output
try: sock.recv(8192)
except: pass

# Enter CLI mode (send &apos;#&apos; instead of MSP header &apos;$&apos;)
print(&quot;Entering CLI mode...&quot;)
sock.send(b&apos;#\n&apos;)
time.sleep(2)
try:
    data = sock.recv(4096)
    print(data.decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)[:300])
except: pass

# Set ARM on AUX1 (mode=0=ARM, aux=0=AUX1, range=1700-2100)
print(&quot;\nSetting ARM on AUX1 (1700-2100)...&quot;)
sock.send(b&apos;aux 0 0 0 1700 2100\n&apos;)
time.sleep(1)

# Save to EEPROM and reboot
print(&quot;Saving to EEPROM...&quot;)
sock.send(b&apos;save\n&apos;)
time.sleep(5)
try:
    data = sock.recv(8192)
    print(data.decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)[-300:])
except Exception as e:
    print(f&quot;Save response: {e}&quot;)

sock.close()
print(&quot;\nDone! eeprom.bin configured with ARM on AUX1.&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What &lt;code&gt;aux 0 0 0 1700 2100&lt;/code&gt; means&lt;/strong&gt;: 1st 0 = configuration slot; 2nd 0 = mode ID (0 = ARM); 3rd 0 = AUX channel index (0 = AUX1); 1700 2100 = trigger range.&lt;/p&gt;
&lt;h2&gt;AUX Channel Mode Reference&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;aux &amp;#x3C;slot&gt; &amp;#x3C;mode ID&gt; &amp;#x3C;AUX channel&gt; &amp;#x3C;range low&gt; &amp;#x3C;range high&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Common mode IDs:&lt;/p&gt;
&lt;p&gt;| ID | Name | Function |
|----|------|----------|
| 0 | ARM | Arm/disarm the flight controller |
| 1 | ANGLE | Self-stabilizing mode |
| 2 | HORIZON | Semi-stabilizing mode |
| 5 | HEADFREE | Head-free mode |
| 11 | GPS RESCUE | GPS rescue return-to-home |
| 22 | AIRMODE | Air mode |
| 26 | CRASH FLIP | Turtle mode (flip after crash) |
| 27 | PREARM | Pre-arm |&lt;/p&gt;
&lt;p&gt;Example — adding ANGLE mode on AUX2 (1500–2100):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aux 1 1 1 1500 2100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Launching the Simulation — Three-Terminal Workflow&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Terminal 1 — Betaflight SITL:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/bf_sitl/config
~/betaflight/obj/main/betaflight_SITL.elf --ip 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Terminal 2 — Gazebo Harmonic:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export GZ_SIM_SYSTEM_PLUGIN_PATH=$HOME/ros2_ws/install/betaflight_gazebo/lib
export GZ_SIM_RESOURCE_PATH=$HOME/ros2_ws/src/betaflight_sim/models
gz sim -r -v 4 ~/bf_sitl/worlds/betaflight_world.sdf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Terminal 3 — Closed-loop altitude controller:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 ~/bf_sitl/autonomy/msp_closed_loop_controller.py \
    --target-alt 2.0 --hover-base 1450 --kp 3 --ki 1 --kd 2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RC packet send frequency&lt;/strong&gt;: The controller must send RC packets at a minimum of 50Hz. Betaflight&apos;s receiver timeout window is approximately 100ms — exceeding this triggers RXLOSS. This controller runs on a dedicated thread at 100Hz.
&lt;strong&gt;Altitude sign convention&lt;/strong&gt;: Betaflight&apos;s &lt;code&gt;getEstimatedAltitudeCm()&lt;/code&gt; returns NED altitude (positive = downwards). The controller internally negates the value so that &quot;up is positive&quot;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Controller Code Walkthrough&lt;/h2&gt;
&lt;p&gt;The closed-loop controller &lt;code&gt;msp_closed_loop_controller.py&lt;/code&gt;. Place this file under &lt;code&gt;~/bf_sitl/autonomy/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;File: &lt;code&gt;msp_closed_loop_controller.py&lt;/code&gt; (closed-loop altitude hold controller)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
Closed-loop altitude hold controller for Betaflight SITL.
- Reads drone state via MSP over TCP 5761 (altitude, attitude, IMU, battery)
- Sends RC via UDP 9004 (100Hz)
- PID altitude control with attitude monitoring

This is the SAME architecture used on real hardware (TCP -&gt; UART serial).
&quot;&quot;&quot;
import socket, struct, time, signal, sys, argparse, threading
from msp_reader import MSPReader

BF_IP, BF_PORT = &apos;127.0.0.1&apos;, 9004
NUM_CH = 16

class ClosedLoopController:
    def __init__(self, target_alt=2.0, hover_base=1450, kp=80, ki=5, kd=40):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.msp = MSPReader()
        self.target_alt = target_alt
        self.hover_base = hover_base
        self.kp, self.ki, self.kd = kp, ki, kd
        self.integral = 0.0
        self.last_error = 0.0
        self.lock = threading.Lock()
        self.throttle = 1000
        self.aux1 = 1000
        self.running = True
        signal.signal(signal.SIGINT, self.stop)
        signal.signal(signal.SIGTERM, self.stop)

    def stop(self, *args):
        print(&quot;\nDisarming...&quot;)
        self.running = False
        with self.lock:
            self.throttle = 1000
            self.aux1 = 1000
        time.sleep(0.3)
        self.msp.stop()
        self.sock.close()
        sys.exit(0)

    def send_rc(self, channels):
        ts = time.time()
        pkt = struct.pack(&apos;&amp;#x3C;d&apos; + &apos;H&apos; * NUM_CH, ts, *channels)
        try:
            self.sock.sendto(pkt, (BF_IP, BF_PORT))
        except:
            pass

    def rc_thread(self):
        &quot;&quot;&quot;Send RC at 100Hz independently of state reading&quot;&quot;&quot;
        while self.running:
            with self.lock:
                t, a = self.throttle, self.aux1
            ch = [1500] * NUM_CH
            ch[2], ch[4] = t, a
            self.send_rc(ch)
            time.sleep(0.01)

    def run(self):
        print(&quot;=&quot; * 60)
        print(&quot;Betaflight SITL Closed-Loop Controller (MSP + UDP)&quot;)
        print(f&quot;Target altitude: {self.target_alt}m&quot;)
        print(f&quot;Hover base: {self.hover_base}  PID: kp={self.kp} ki={self.ki} kd={self.kd}&quot;)
        print(&quot;=&quot; * 60)

        if not self.msp.start():
            print(&quot;ERROR: Failed to connect MSP reader. Is SITL running?&quot;)
            sys.exit(1)

        threading.Thread(target=self.rc_thread, daemon=True).start()

        # Phase 1: Disarmed init (3s)
        print(&quot;\nPhase 1: Init (disarmed, 3s)...&quot;)
        with self.lock:
            self.throttle = 1000
            self.aux1 = 1000
        time.sleep(3)

        # Phase 2: Arm (2s) -- throttle MUST be 1000 for arming
        print(&quot;Phase 2: Arming (2s)...&quot;)
        with self.lock:
            self.throttle = 1000
            self.aux1 = 2000
        time.sleep(2)

        # Phase 3: Altitude hold with PID
        print(f&quot;Phase 3: Altitude hold at {self.target_alt}m...&quot;)
        last_time = time.time()

        while self.running:
            state = self.msp.get_state()
            now = time.time()
            dt = now - last_time
            if dt &amp;#x3C;= 0 or dt &gt; 1.0:
                dt = 0.05

            # Get altitude from MSP (cm -&gt; m).
            # Betaflight reports NED (positive=down), negate so &quot;up&quot; is positive.
            alt = -state.get(&apos;alt_cm&apos;, 0) / 100.0 if state else 0.0

            # PID control
            error = self.target_alt - alt
            self.integral += error * dt
            self.integral = max(-200, min(200, self.integral))
            deriv = (error - self.last_error) / dt
            pid = self.kp * error + self.ki * self.integral + self.kd * deriv
            thr = int(self.hover_base + pid)
            thr = max(1000, min(1900, thr))

            with self.lock:
                self.throttle = thr
                self.aux1 = 2000

            # Display
            roll = state.get(&apos;roll_deg&apos;, 0)
            pitch = state.get(&apos;pitch_deg&apos;, 0)
            volt = state.get(&apos;voltage_v&apos;, 0)
            rssi = state.get(&apos;rssi&apos;, 0)
            mode = state.get(&apos;flight_mode&apos;, 0)
            armed = bool(mode &amp;#x26; (1 &amp;#x3C;&amp;#x3C; 0)) if &apos;flight_mode&apos; in state else False

            print(f&quot;\r  alt={alt:.2f}m err={error:+.2f} thr={thr} &quot;
                  f&quot;r={roll:.0f}° p={pitch:.0f}° &quot;
                  f&quot;v={volt:.1f}V RSSI={rssi} armed={armed}  &quot;,
                  end=&apos;&apos;, flush=True)

            self.last_error = error
            last_time = now
            time.sleep(0.05)

def main():
    p = argparse.ArgumentParser()
    p.add_argument(&apos;--target-alt&apos;, type=float, default=2.0)
    p.add_argument(&apos;--hover-base&apos;, type=int, default=1450)
    p.add_argument(&apos;--kp&apos;, type=float, default=80)
    p.add_argument(&apos;--ki&apos;, type=float, default=5)
    p.add_argument(&apos;--kd&apos;, type=float, default=40)
    args = p.parse_args()
    ClosedLoopController(args.target_alt, args.hover_base,
                         args.kp, args.ki, args.kd).run()

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Core logic of the closed-loop controller &lt;code&gt;msp_closed_loop_controller.py&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RC send thread (100Hz)&lt;/strong&gt;: Reads throttle and AUX values from shared variables, assembles a 16-channel array using AETR channel mapping, packs with &lt;code&gt;struct.pack&lt;/code&gt;, and sends via UDP 9004&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSP telemetry thread (~20Hz)&lt;/strong&gt;: Loops sending MSPv1 request frames, parses responses to obtain altitude, attitude, voltage, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Main PID loop (20Hz)&lt;/strong&gt;: Reads altitude, compares with target to get error, runs through PID computation, superimposes result onto the hover throttle baseline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MSPv1 request frame format (6 bytes): &lt;code&gt;$ M &amp;#x3C; size(0) cmd CRC(cmd)&lt;/code&gt;. Response frame: &lt;code&gt;$ M &gt; size cmd payload(N bytes) CRC&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Real-hardware correspondence: UDP 9004 → UART serial (MSP_SET_RAW_RC), TCP 5761 → UART serial (MSP telemetry). &lt;code&gt;msp_reader.py&lt;/code&gt; and PID logic are fully reusable — simply replace the socket with a serial port.&lt;/p&gt;
&lt;h2&gt;Troubleshooting Quick Reference&lt;/h2&gt;
&lt;p&gt;| Symptom | Most Likely Cause | Diagnosis |
|---------|-------------------|-----------|
| No &quot;new fdm&quot; messages in SITL after Gazebo starts | Plugin not loaded or UDP unreachable | Run &lt;code&gt;gz sim -v 4&lt;/code&gt; and check for &quot;Loaded system [BetaFlightPlugin]&quot; |
| SITL persistently shows RXLOSS | RC controller not running | Look for &lt;code&gt;new rc&lt;/code&gt; in SITL terminal — indicates first RC packet received |
| SITL shows &quot;Arming disabled: THROTTLE&quot; | Throttle channel value too high during arm attempt | Verify Phase 2 &lt;code&gt;ch[2]&lt;/code&gt; is at 1000 |
| SITL shows FAILSAFE RXLOSS | RC send interval exceeds 100ms | Ensure sending at ≥50Hz. Blocking operations (e.g., file I/O) must run in separate threads |
| Props spinning but no takeoff | Hover throttle too low | Gradually increase &lt;code&gt;--hover-base&lt;/code&gt;; IRIS needs approximately 1450–1600 |
| Controller reads alt=0.00 and never changes | MSP response parsing failed or altitude sign error | Run &lt;code&gt;msp_reader.py&lt;/code&gt; standalone to verify MSP communication |
| Controller alt values correct but drone flies erratically | PID parameters too aggressive | Start with conservative values: &lt;code&gt;--kp 2 --ki 1 --kd 1&lt;/code&gt; |
| Betaflight SITL compilation fails | Missing build dependencies | &lt;code&gt;sudo apt install build-essential&lt;/code&gt; |
| Plugin compilation fails with &lt;code&gt;Could not find gz-sim8&lt;/code&gt; | Gazebo dev packages not installed | &lt;code&gt;sudo apt install -y libgz-sim8-dev libgz-transport13-dev libgz-msgs10-dev libgz-math7-dev libgz-plugin2-dev libsdformat14-dev&lt;/code&gt; |
| Plugin compilation fails with colcon &lt;code&gt;Cannot find package gz-sim7&lt;/code&gt; | &lt;code&gt;package.xml&lt;/code&gt; version numbers not updated | Change &lt;code&gt;gz-sim7&lt;/code&gt;→&lt;code&gt;gz-sim8&lt;/code&gt; and &lt;code&gt;gz-transport12&lt;/code&gt;→&lt;code&gt;gz-transport13&lt;/code&gt; in &lt;code&gt;package.xml&lt;/code&gt; |
| Plugin compilation fails with &lt;code&gt;gz/msgs/imu.pb.h not found&lt;/code&gt; | &lt;code&gt;gz-msgs10-dev&lt;/code&gt; not installed | &lt;code&gt;sudo apt install libgz-msgs10-dev&lt;/code&gt; |
| Plugin compilation fails with &quot;no matching function for Subscribe&quot; | &lt;code&gt;std::function&lt;/code&gt; + &lt;code&gt;std::bind&lt;/code&gt; not applied correctly | Cross-check against Check 2 code examples |
| &lt;code&gt;gz sim --versions&lt;/code&gt; shows multiple versions | Multiple Gazebo versions installed | Keep only &lt;code&gt;gz-harmonic&lt;/code&gt; |&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This article provided a complete walkthrough for the Betaflight SITL + Gazebo Harmonic simulation environment, covering SITL compilation, four critical Gazebo plugin compatibility adaptations, CLI arming configuration, AUX channel mode setup, and a Python closed-loop altitude controller implementation. Betaflight&apos;s SITL mode delivers hardware-free, closed-loop simulation capability for flight controller algorithm validation — when paired with the Gazebo physics engine, it can verify the complete control pipeline from sensor input to motor output. To add visual perception to your simulation, check out &lt;a href=&quot;/en/posts/px4-ros2-yolo&quot;&gt;YOLOv8 Object Detection in PX4 Drone Simulation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Contributions and feedback welcome at the &lt;a href=&quot;https://github.com/Jinyao-Chen/bf_sitl&quot;&gt;GitHub repository&lt;/a&gt;!&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://betaflight.com/docs/development/SITL&quot;&gt;Betaflight SITL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/osrf/vehicle_gateway&quot;&gt;OSRF vehicle_gateway Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Betaflight source code -- SITL platform implementation -- sitl.c, target.h, udplink.c&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gazebosim.org/docs/harmonic&quot;&gt;Gazebo Harmonic Installation Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ROS2 + Gazebo bridge package -- ros-humble-ros-gzharmonic API documentation&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://app.betaflight.com/&quot;&gt;Betaflight Configurator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Betaflight+Gazebo 软件在环仿真教程</title><link>https://duduuu.xyz/zh/posts/px4-ros2-betaflight-sitl</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-betaflight-sitl</guid><description>在Ubuntu 22.04上搭建Betaflight SITL+Gazebo Harmonic仿真环境，含插件编译适配、CLI配置解锁及Python闭环控制</description><pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于 ROS2&lt;/strong&gt;：本教程的仿真链路（Gazebo ↔ SITL ↔ Controller）通过 UDP 直连，&lt;strong&gt;运行时完全不需要 ROS2&lt;/strong&gt;。ROS2 仅在编译 Gazebo 插件时用到（因为上游 vehicle_gateway 项目使用 colcon 构建系统），插件编译完成后不再依赖 ROS2。&lt;/p&gt;
&lt;p&gt;教程较为复杂，主要是讲解其中的框架。完整代码和配置文件欢迎在 &lt;a href=&quot;https://github.com/Jinyao-Chen/bf_sitl&quot;&gt;GitHub 仓库&lt;/a&gt; 上找到，欢迎提交 commit 改进。如果喜欢，麻烦 star 一下支持作者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;Betaflight 是目前无人机竞速（FPV Racing）和自由飞（Freestyle）领域最流行的开源飞控固件，广泛运行在 STM32/AT32 等 MCU 上。Betaflight 提供 SITL（Software-In-The-Loop，软件在环）模式，允许开发者在 Gazebo 物理仿真引擎中运行完整的 Betaflight 飞控固件。&lt;/p&gt;
&lt;p&gt;SITL 的核心思路是：将 Betaflight 固件编译为 x86_64 Linux 可执行文件，飞控的传感器输入（IMU、气压计、GPS）来自 Gazebo 仿真环境通过 UDP 发送的仿真数据，飞控的电机输出（PWM 指令）同样通过 UDP 发回 Gazebo 驱动物理引擎中的螺旋桨。这样开发者可以在 PC 上完整验证飞控逻辑和控制算法，无需真实硬件。&lt;/p&gt;
&lt;p&gt;本文提供一个在 Ubuntu 22.04 上搭建 Betaflight SITL + Gazebo Harmonic + Python 自主控制仿真环境的完整流程。在开始之前，感谢 OSRF vehicle_gateway 项目和 Betaflight 社区提供的开源代码和文档，本教程大量参考了这些资料。&lt;/p&gt;
&lt;h2&gt;前置环境&lt;/h2&gt;
&lt;p&gt;按照笔者之前的 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真教程&lt;/a&gt; 完成了以下基础环境的配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04 系统&lt;/li&gt;
&lt;li&gt;ROS2 Humble（推荐使用鱼香ROS一键安装：&lt;code&gt;wget http://fishros.com/install -O fishros &amp;#x26;&amp;#x26; . fishros&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Gazebo Harmonic（gz-sim8，版本 8.9.0）&lt;/li&gt;
&lt;li&gt;ROS2-Gazebo 通信桥 &lt;code&gt;ros-humble-ros-gzharmonic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在终端中确认环境正确：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 --version       # 输出应包含 &quot;humble&quot;
gz sim --versions    # 输出应为 &quot;Gazebo Sim 8.9.0&quot;
dpkg -l | grep ros-humble-ros-gzharmonic  # 应显示已安装
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;克隆本教程配套仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~
git clone https://github.com/Jinyao-Chen/bf_sitl.git ~/bf_sitl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仓库目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/bf_sitl/
├── autonomy/
│   ├── msp_reader.py                    # MSP 遥测读取器
│   ├── msp_closed_loop_controller.py    # 闭环定高控制器
│   └── autonomous_controller.py         # 开环起飞控制器
├── worlds/
│   └── betaflight_world.sdf             # Gazebo 世界文件
├── config/
│   └── bf_cli_config.py                 # 首次配置脚本
└── betaflight_gazebo_patches/           # 插件补丁（供编译参考）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装必要工具和 &lt;strong&gt;Gazebo 开发包&lt;/strong&gt;（编译插件必需，运行时 &lt;code&gt;gz-harmonic&lt;/code&gt; 不包含这些头文件）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip3 install --user websockify
sudo apt-get install -y socat

# Gazebo Harmonic 开发包——编译自定义 Gazebo 插件必需！
sudo apt-get install -y \
    libgz-sim8-dev \
    libgz-transport13-dev \
    libgz-msgs10-dev \
    libgz-math7-dev \
    libgz-plugin2-dev \
    libsdformat14-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;websockify&lt;/code&gt;：提供 WebSocket → TCP 的代理功能，用于在浏览器中连接 Betaflight 地面站配置工具&lt;/li&gt;
&lt;li&gt;&lt;code&gt;socat&lt;/code&gt;：用于创建虚拟串口（pty），可将 Betaflight 的 TCP UART 映射为本地虚拟串口设备&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确保网络可以正常访问 GitHub，否则克隆大型仓库时会中途闪退。&lt;/p&gt;
&lt;h2&gt;仿真链路架构&lt;/h2&gt;
&lt;p&gt;在开始动手配置之前，我们先介绍整个仿真链路，这有助于后续遇到问题时定位原因。仿真链路由四个进程组成，它们通过 UDP 端口互相通信：&lt;/p&gt;
&lt;p&gt;端口分配与数据流（Betaflight 源码 &lt;code&gt;sitl.c&lt;/code&gt; 第 192-195 行）：&lt;/p&gt;
&lt;p&gt;| 端口 | 方向 | 用途 | 数据包类型 |
|------|------|------|-----------|
| 9001 | SITL → 外部 | Raw PWM（备用于 RealFlightBridge） | &lt;code&gt;servo_packet_raw&lt;/code&gt; |
| 9002 | SITL → Gazebo | 电机转速指令 [0.0, 1.0]（3D 模式为 [-1.0, 1.0]） | &lt;code&gt;servo_packet&lt;/code&gt; |
| 9003 | Gazebo → SITL | 飞行状态数据（IMU、位置、速度、姿态、气压） | &lt;code&gt;fdm_packet&lt;/code&gt; |
| 9004 | Controller → SITL | 16 通道 RC 遥控信号 [1000-2000] | &lt;code&gt;rc_packet&lt;/code&gt; |
| TCP 5761 | 双向 | MSP 协议（配置 + 遥测回传） | N/A |&lt;/p&gt;
&lt;p&gt;数据包结构体（Betaflight 源码 &lt;code&gt;target.h&lt;/code&gt; 第 224-246 行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// fdm_packet: Gazebo 插件 → SITL
typedef struct {
    double timestamp;
    double imu_angular_velocity_rpy[3];    // 角速度 (rad/s)
    double imu_linear_acceleration_xyz[3]; // 线加速度 (m/s²)
    double imu_orientation_quat[4];        // 姿态四元数 (w,x,y,z)
    double velocity_xyz[3];                // 速度 (m/s, ENU 坐标系)
    double position_xyz[3];                // 位置
    double pressure;                       // 气压 (Pa)
} fdm_packet;

// servo_packet: SITL → Gazebo 插件
typedef struct {
    float motor_speed[4];   // [0.0, 1.0] 或 3D 模式 [-1.0, 1.0]
} servo_packet;

// rc_packet: Controller → SITL
typedef struct {
    double timestamp;
    uint16_t channels[16];   // PWM [1000, 2000]
} rc_packet;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gazebo 插件（BetaflightPlugin）在每个仿真更新周期中执行两个阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PreUpdate（仿真步进前）&lt;/strong&gt;：调用 &lt;code&gt;ReceiveServoPacket()&lt;/code&gt; 从 UDP 9002 读取 &lt;code&gt;servo_packet&lt;/code&gt;，将电机转速换算为关节力/速度指令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostUpdate（仿真步进后）&lt;/strong&gt;：从 IMU 传感器获取角速度和加速度数据，从 Gazebo Entity Component Manager 获取位姿和线速度，填充为 &lt;code&gt;fdm_packet&lt;/code&gt;，进行坐标系变换后通过 UDP 发送到 127.0.0.1:9003&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Betaflight SITL 源码下载及编译&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~
git clone https://github.com/betaflight/betaflight.git
cd betaflight
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：我们使用的不是 4.5.x 版，而是 master 分支（版本号 2026.6.0-alpha），因为 4.5.x 正式版的 SITL 代码缺少 Gazebo 的原生支持（&lt;code&gt;ENABLE_GAZEBO_BRIDGE&lt;/code&gt; 特性在 4.5.4 之后才引入 master）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;与编译 PX4 需要 ARM 交叉编译工具链不同，SITL 模式直接使用主机 GCC 编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make TARGET=SITL -j$(nproc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译成功标志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Linking SITL
   text    data     bss     dec     hex  filename
 389784   21804   77376  488964   77604  ./obj/main/betaflight_SITL.elf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./obj/main/betaflight_SITL.elf --help
# 输出: Betaflight SITL Usage: ./obj/main/betaflight_SITL.elf [options]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么 SITL 可以单独运行？因为 Betaflight SITL 用软件模拟了真实飞控的硬件外设：虚拟 IMU（&lt;code&gt;accgyro_virtual.c&lt;/code&gt;）接收 Gazebo 的角速度和加速度数据；虚拟气压计（&lt;code&gt;barometer_virtual.c&lt;/code&gt;）接收气压/高度数据；虚拟 GPS（&lt;code&gt;gps_virtual.c&lt;/code&gt;）接收位置数据；虚拟 EEPROM（文件读写）保存配置为 &lt;code&gt;eeprom.bin&lt;/code&gt;；虚拟串口（&lt;code&gt;serial_tcp.c&lt;/code&gt;）以 TCP 替代真实 UART。&lt;/p&gt;
&lt;p&gt;编译完成后建议备份：&lt;code&gt;zip -r betaflight.zip betaflight/&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Gazebo 插件的获取与编译&lt;/h2&gt;
&lt;p&gt;Betaflight 本身不包含 Gazebo 插件。我们需要 OSRF 的 vehicle_gateway 项目中的 &lt;code&gt;betaflight_gazebo&lt;/code&gt; 插件。需要注意：vehicle_gateway 官方针对 Gazebo Garden（gz-sim7），而我们的环境是 Gazebo Harmonic（gz-sim8），需要进行适配修改。&lt;/p&gt;
&lt;h3&gt;步骤 1：创建 ROS2 工作空间并克隆插件&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明&lt;/strong&gt;：Gazebo 插件 &lt;code&gt;libBetaflightPlugin.so&lt;/code&gt; 需要通过 colcon（ROS2 的构建工具）编译，因此需要放在一个 ROS2 工作空间中。如果你已有 &lt;code&gt;~/ros2_ws&lt;/code&gt;，可以直接使用；如果没有，执行以下命令创建：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/ros2_ws/src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后克隆 vehicle_gateway 并跳过不需要的子包：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
git clone https://github.com/osrf/vehicle_gateway.git vehicle_gateway

cd ~/ros2_ws/src/vehicle_gateway
for pkg in gz_aerial_plugins vehicle_gateway vehicle_gateway_betaflight \
    vehicle_gateway_demo vehicle_gateway_integration_test vehicle_gateway_multi \
    vehicle_gateway_px4 vehicle_gateway_python vehicle_gateway_python_helpers \
    vehicle_gateway_sim_performance px4_sim betaflight_configurator \
    betaflight_demo qgroundcontrol; do
    touch &quot;$pkg/COLCON_IGNORE&quot;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;步骤 2：适配 Gazebo Harmonic（需改 2 个文件的版本号，其余 3 项代码修复已在最新上游代码中完成）&lt;/h3&gt;
&lt;p&gt;当前 OSRF vehicle_gateway 上游代码已包含了大部分 Gazebo Harmonic 兼容修复。只有 CMakeLists.txt 的库版本号仍需要手动修改。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;必须修改 — 适配库版本号&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要改两个文件：&lt;code&gt;CMakeLists.txt&lt;/code&gt; 和 &lt;code&gt;package.xml&lt;/code&gt;。二者中的版本号必须同步，否则 colcon 依赖解析会失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（1）编辑 &lt;code&gt;~/ros2_ws/src/vehicle_gateway/betaflight_gazebo/CMakeLists.txt&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;全文搜索替换以下两对版本号。不仅 &lt;code&gt;find_package&lt;/code&gt; 行要改，&lt;code&gt;target_link_libraries&lt;/code&gt; 中带版本的 target 名（如 &lt;code&gt;gz-sim7::gz-sim7&lt;/code&gt;）也要同步替换：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有 &lt;code&gt;gz-sim7&lt;/code&gt; → &lt;code&gt;gz-sim8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;所有 &lt;code&gt;gz-transport12&lt;/code&gt; → &lt;code&gt;gz-transport13&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;（2）编辑 &lt;code&gt;~/ros2_ws/src/vehicle_gateway/betaflight_gazebo/package.xml&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;&amp;#x3C;depend&gt;&lt;/code&gt; 中的版本号也同步更新：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;depend&gt;gz-sim7&amp;#x3C;/depend&gt;&lt;/code&gt; → &lt;code&gt;&amp;#x3C;depend&gt;gz-sim8&amp;#x3C;/depend&gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C;depend&gt;gz-transport12&amp;#x3C;/depend&gt;&lt;/code&gt; → &lt;code&gt;&amp;#x3C;depend&gt;gz-transport13&amp;#x3C;/depend&gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么？&lt;/strong&gt; Gazebo Garden (gz-sim7, gz-transport12) 和 Gazebo Harmonic (gz-sim8, gz-transport13) 的 ABI 不兼容，插件必须链接与 Gazebo 服务器进程相同版本的库。gz-math7 和 gz-plugin2 两个版本共享，无需修改。&lt;code&gt;package.xml&lt;/code&gt; 不改的话 colcon 会尝试用 rosdep 解析 &lt;code&gt;gz-sim7&lt;/code&gt;，在只装了 Harmonic 的环境中必然失败。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;验证项 — 以下三项已在最新上游代码中修复，无需手动改，但建议逐项确认你的克隆包含它们：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;检查一：头文件引用（BetaflightPlugin.cpp 第 35-39 行）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/ros2_ws/src/vehicle_gateway/betaflight_gazebo/src/BetaflightPlugin.cpp&lt;/code&gt; 中应已有以下头文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;functional&gt;
#include &amp;#x3C;gz/msgs/imu.pb.h&gt;
#include &amp;#x3C;gz/msgs/fluid_pressure.pb.h&gt;
#include &amp;#x3C;gz/msgs/double.pb.h&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;背景说明&lt;/strong&gt;：Gazebo Harmonic (gz-sim8) 的 &lt;code&gt;System.hh&lt;/code&gt; 不再传递包含 protobuf 消息头文件，因此需要显式 include。上游已修复，如果你的克隆缺少这几行，手动补上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;检查二：IMU 和气压传感器订阅（PreUpdate 第 440-443 行，Configure 第 296-301 行）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;IMU 订阅在 &lt;code&gt;PreUpdate&lt;/code&gt; 函数中（约第 440 行），气压传感器订阅在 &lt;code&gt;Configure&lt;/code&gt; 函数中（约第 296 行）。二者已使用 &lt;code&gt;std::function&lt;/code&gt; + &lt;code&gt;std::bind&lt;/code&gt; 写法，无需改动：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PreUpdate&lt;/code&gt; 中 IMU 订阅（约第 440 行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::function&amp;#x3C;void(const gz::msgs::IMU&amp;#x26;)&gt; imuCb =
    std::bind(&amp;#x26;BetaFlightPluginPrivate::ImuCb,
              this-&gt;dataPtr.get(), std::placeholders::_1);
this-&gt;dataPtr-&gt;node.Subscribe(imuTopicName, imuCb);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Configure&lt;/code&gt; 中气压传感器订阅（约第 296 行）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::function&amp;#x3C;void(const gz::msgs::FluidPressure&amp;#x26;)&gt; airPressureCb =
    std::bind(&amp;#x26;BetaFlightPluginPrivate::onAirPressureMessageReceived,
              this-&gt;dataPtr.get(), std::placeholders::_1);
this-&gt;dataPtr-&gt;node.Subscribe(
    &quot;/world/empty_betaflight_world/model/iris_with_Betaflight/model/iris_with_standoffs/&quot;
    &quot;link/imu_link/sensor/air_pressure_sensor/air_pressure&quot;,
    airPressureCb);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;背景说明&lt;/strong&gt;：Gazebo Garden 的 gz-transport12 允许 &lt;code&gt;Node::Subscribe&lt;/code&gt; 直接接受成员函数指针进行模板推导。gz-transport13 收紧后，必须用 &lt;code&gt;std::function&lt;/code&gt; 显式包装。IMU 回调 &lt;code&gt;ImuCb&lt;/code&gt; 签名为 &lt;code&gt;void(const gz::msgs::IMU&amp;#x26;)&lt;/code&gt;，气压回调 &lt;code&gt;onAirPressureMessageReceived&lt;/code&gt; 签名为 &lt;code&gt;void(const gz::msgs::FluidPressure&amp;#x26;)&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;检查三：PostUpdate 死锁修复（PostUpdate 第 478-484 行）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PostUpdate&lt;/code&gt; 函数中不应包含 &lt;code&gt;betaflightOnline&lt;/code&gt; 条件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;if (!_info.paused &amp;#x26;&amp;#x26; _info.simTime &gt; this-&gt;dataPtr-&gt;lastControllerUpdateTime)
{
    double t = ...;
    this-&gt;SendState(t, _ecm);
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果出现 &lt;code&gt;&amp;#x26;&amp;#x26; this-&gt;dataPtr-&gt;betaflightOnline&lt;/code&gt;，请删除。&lt;strong&gt;原因&lt;/strong&gt;：初始时 SITL 等 Gazebo 发 &lt;code&gt;fdm_packet&lt;/code&gt;，而 Gazebo 因为 &lt;code&gt;betaflightOnline == false&lt;/code&gt; 不发 &lt;code&gt;fdm_packet&lt;/code&gt;，形成死锁。&lt;/p&gt;
&lt;h3&gt;步骤 3：编译插件&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
source /opt/ros/humble/setup.bash
colcon build --packages-select betaflight_gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：&lt;code&gt;ls -lh ~/ros2_ws/install/betaflight_gazebo/lib/libBetaflightPlugin.so&lt;/code&gt;（约 8.7M）&lt;/p&gt;
&lt;h2&gt;仿真世界的配置&lt;/h2&gt;
&lt;p&gt;世界文件和无人机模型已包含在本教程配套仓库中。只需软链接 IRIS 四旋翼模型（由 vehicle_gateway 提供）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ln -sf ~/ros2_ws/src/vehicle_gateway/betaflight_sim/models/iris_with_standoffs \
    ~/ros2_ws/src/vehicle_gateway/betaflight_sim/models/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;世界文件位于 &lt;code&gt;~/bf_sitl/worlds/betaflight_world.sdf&lt;/code&gt;，核心插件配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;plugin name=&quot;BetaFlightPlugin&quot; filename=&quot;BetaflightPlugin&quot;&gt;
    &amp;#x3C;fdm_addr&gt;127.0.0.1&amp;#x3C;/fdm_addr&gt;
    &amp;#x3C;fdm_port_in&gt;9002&amp;#x3C;/fdm_port_in&gt;
    &amp;#x3C;listen_addr&gt;127.0.0.1&amp;#x3C;/listen_addr&gt;
    &amp;#x3C;modelXYZToAirplaneXForwardZDown&gt;0 0 0 3.141593 0 0&amp;#x3C;/modelXYZToAirplaneXForwardZDown&gt;
    &amp;#x3C;gazeboXYZToNED&gt;0 0 0 3.141593 0 0&amp;#x3C;/gazeboXYZToNED&gt;
    &amp;#x3C;imuName&gt;iris_with_standoffs::imu_link::imu_sensor&amp;#x3C;/imuName&gt;
    &amp;#x3C;control channel=&quot;0&quot;&gt;
        &amp;#x3C;jointName&gt;iris_with_standoffs::rotor_0_joint&amp;#x3C;/jointName&gt;
        &amp;#x3C;multiplier&gt;838&amp;#x3C;/multiplier&gt;
    &amp;#x3C;/control&gt;
    &amp;#x3C;!-- channels 1-3 similarly configured, multipliers: 838, -838, -838 --&gt;
&amp;#x3C;/plugin&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;坐标系变换详解&lt;/strong&gt;：Gazebo 使用 ENU（X=East, Y=North, Z=Up），Betaflight 期望 NED（X=North, Y=East, Z=Down）。&lt;code&gt;gazeboXYZToNED&lt;/code&gt; 的 Yaw=180° 进行朝向旋转，插件内部的 &lt;code&gt;SendState()&lt;/code&gt; 进一步应用 &lt;code&gt;Rz(π/2)&lt;/code&gt; 旋转完成完整变换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;配置解锁 —— 让飞控能够 ARM&lt;/h2&gt;
&lt;p&gt;Betaflight 的安全设计原则是&quot;默认禁止一切飞行操作&quot;——出厂固件没有配置 ARM（解锁）开关，即使收到 RC 信号中 AUX1=2000，飞控也不知道这表示&quot;解锁请求&quot;。我们需要告诉飞控：当 AUX1 通道值在 1700-2100 范围时，激活 ARM 模式。&lt;/p&gt;
&lt;p&gt;配置保存在 &lt;code&gt;eeprom.bin&lt;/code&gt; 文件中（相当于飞控的&quot;设置记忆&quot;），通过 TCP 端口 5761 的 CLI（命令行接口）进行设置。&lt;/p&gt;
&lt;h3&gt;第一步：清除旧配置（重要！）&lt;/h3&gt;
&lt;p&gt;如果你之前运行过 SITL，旧的 &lt;code&gt;eeprom.bin&lt;/code&gt; 可能包含错误配置（典型问题是接收机模式被改成了串口 RX_SERIAL 而非 UDP），导致飞控永久无法解锁。首次配置前务必删除旧文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/bf_sitl/config
rm -f eeprom.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么要删？&lt;/strong&gt; &lt;code&gt;eeprom.bin&lt;/code&gt; 是飞控的持久化存储。SITL 每次启动时优先读取 &lt;code&gt;eeprom.bin&lt;/code&gt;，只有在文件不存在时才使用出厂默认值。出厂默认已正确启用 UDP 接收机模式（&lt;code&gt;FEATURE_RX_UDP&lt;/code&gt;），确保飞控能通过 UDP 端口 9004 接收 RC 遥控信号。如果旧的 &lt;code&gt;eeprom.bin&lt;/code&gt; 里存了错误的接收机设置，SITL 就会&lt;strong&gt;忽略 RC 信号&lt;/strong&gt;，飞控永远显示 RXLOSS、永远无法解锁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;第二步：启动 SITL 并运行配置脚本&lt;/h3&gt;
&lt;p&gt;在终端 1 中启动 SITL（此时不需要 Gazebo）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/bf_sitl/config
~/betaflight/obj/main/betaflight_SITL.elf --ip 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待出现 &lt;code&gt;bind port 5761 for UART1&lt;/code&gt;，然后打开终端 2 运行配置脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 ~/bf_sitl/config/bf_cli_config.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本内容（&lt;code&gt;bf_cli_config.py&lt;/code&gt;，仓库 &lt;code&gt;config/&lt;/code&gt; 目录下）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;Configure Betaflight SITL via CLI — set ARM on AUX1&quot;&quot;&quot;
import socket, time, sys

HOST, PORT = &apos;127.0.0.1&apos;, 5761

def recv_all(sock, timeout=1.0):
    &quot;&quot;&quot;读取所有可用数据，超时后返回&quot;&quot;&quot;
    sock.settimeout(timeout)
    data = b&apos;&apos;
    try:
        while True:
            chunk = sock.recv(4096)
            if not chunk: break
            data += chunk
    except socket.timeout: pass
    except Exception: pass
    return data

def main():
    # 1. 等待 SITL 的 TCP 端口就绪（最多等 30 秒）
    print(&quot;Waiting for SITL TCP port 5761...&quot;)
    for i in range(30):
        try:
            s = socket.socket(); s.settimeout(1)
            s.connect((HOST, PORT)); s.close()
            print(f&quot;Connected after {i+1}s&quot;); break
        except Exception:
            time.sleep(1)
    else:
        print(&quot;ERROR: SITL not ready after 30s&quot;); sys.exit(1)

    # 2. 建立 TCP 连接，排空初始欢迎信息
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(5)
    sock.connect((HOST, PORT))
    time.sleep(1)
    recv_all(sock, 0.5)

    # 3. 发送单独一个 &apos;#&apos; 进入 CLI 模式
    #    注意：只发 &apos;#&apos;，不带 \n。Betaflight 收到 &apos;#&apos;（而非 MSP 帧头 &apos;$&apos;）时进入 CLI
    print(&quot;Entering CLI mode...&quot;)
    sock.sendall(b&apos;#&apos;)
    time.sleep(2)
    resp = recv_all(sock, 1.0)
    if resp:
        print(resp.decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)[:300])

    # 4. 配置 ARM 到 AUX1（通道值 1700-2100 时触发 ARM 模式）
    #    aux 0 0 0 1700 2100 参数：
    #      槽位0, 模式0(ARM), AUX通道0(AUX1=第5通道), 触发范围1700-2100μs
    print(&quot;\nSetting ARM on AUX1 (1700-2100)...&quot;)
    sock.sendall(b&apos;aux 0 0 0 1700 2100\n&apos;)
    time.sleep(0.5)
    resp = recv_all(sock, 1.0)
    if resp:
        print(resp.decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)[-200:])

    # 5. 保存到 eeprom.bin 并重启飞控固件
    print(&quot;Saving to EEPROM...&quot;)
    try:
        sock.sendall(b&apos;save\n&apos;)
        time.sleep(1)
        resp = recv_all(sock, 3.0)
        if resp:
            print(resp.decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)[-300:])
    except Exception as e:
        print(f&quot;Save: {e} (reboot is normal)&quot;)

    try: sock.close()
    except: pass
    print(&quot;\nDone! ARM on AUX1 configured.&quot;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第三步：重启 SITL 使配置生效&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;save&lt;/code&gt; 命令会让飞控内部重新初始化，但 &lt;strong&gt;Linux 进程本身不会自动退出&lt;/strong&gt;。你需要在终端 1 按 &lt;code&gt;Ctrl+C&lt;/code&gt; 手动停止，然后重新启动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;~/betaflight/obj/main/betaflight_SITL.elf --ip 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么需要手动重启？&lt;/strong&gt; &lt;code&gt;save&lt;/code&gt; 让飞控固件内部重新初始化并加载新配置到内存，但终端中运行的 Linux 进程仍然占用着端口。Ctrl+C 杀掉进程再重启，确保端口绑定和运行状态都是干净的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;重启后日志中应出现 &lt;code&gt;[FLASH_Unlock] loaded &apos;eeprom.bin&apos;&lt;/code&gt;，表示配置已正确加载。至此 ARM 配置完成，后续正常启动仿真时直接跳到下一步即可。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;aux 0 0 0 1700 2100&lt;/code&gt; 命令含义：第 1 个 0 = 配置槽位编号；第 2 个 0 = 模式 ID（0 = ARM）；第 3 个 0 = AUX 通道索引（0 = AUX1）；1700 2100 = 触发范围（当 AUX1 通道值在此范围内时激活 ARM 模式）。&lt;/p&gt;
&lt;h2&gt;AUX 通道模式对照表&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;aux &amp;#x3C;槽位&gt; &amp;#x3C;模式ID&gt; &amp;#x3C;AUX通道&gt; &amp;#x3C;范围低&gt; &amp;#x3C;范围高&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;常用模式 ID：&lt;/p&gt;
&lt;p&gt;| ID | 名称 | 功能 |
|----|------|------|
| 0 | ARM | 解锁/上锁飞控 |
| 1 | ANGLE | 自稳模式 |
| 2 | HORIZON | 半自稳模式 |
| 5 | HEADFREE | 无头模式 |
| 11 | GPS RESCUE | GPS 救援返航 |
| 22 | AIRMODE | 空中模式 |
| 26 | CRASH FLIP | 反乌龟模式 |
| 27 | PREARM | 预解锁 |&lt;/p&gt;
&lt;p&gt;示例——添加 ANGLE 模式在 AUX2（1500-2100）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aux 1 1 1 1500 2100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动仿真 —— 三个终端流程&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;终端 1 —— Betaflight SITL：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/bf_sitl/config
~/betaflight/obj/main/betaflight_SITL.elf --ip 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;终端 2 —— Gazebo Harmonic：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export GZ_SIM_SYSTEM_PLUGIN_PATH=$HOME/ros2_ws/install/betaflight_gazebo/lib
export GZ_SIM_RESOURCE_PATH=$HOME/ros2_ws/src/vehicle_gateway/betaflight_sim/models
gz sim -r -v 4 ~/bf_sitl/worlds/betaflight_world.sdf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;终端 3 —— 闭环定高控制器：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 闭环定高控制器（推荐，需 MSP 遥测）
python3 ~/bf_sitl/autonomy/msp_closed_loop_controller.py \
    --target-alt 2.0 --hover-base 1450 --kp 80 --ki 5 --kd 40

# 或开环起飞控制器（无需 MSP 遥测，仅 RC 控制）
python3 ~/bf_sitl/autonomy/autonomous_controller.py --hover-throttle 1450
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RC 包发送频率&lt;/strong&gt;：控制器必须以 ≥50Hz 的频率发送 RC 包。Betaflight 接收机超时窗口约 100ms，超时即触发 RXLOSS。本控制器在独立线程中以 100Hz 运行。
&lt;strong&gt;高度符号约定&lt;/strong&gt;：Betaflight 的 &lt;code&gt;getEstimatedAltitudeCm()&lt;/code&gt; 返回 NED 高度（正值=向下），控制器内部取反使&quot;向上为正&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;控制器代码解析&lt;/h2&gt;
&lt;p&gt;控制器代码均已包含在本教程配套仓库的 &lt;code&gt;autonomy/&lt;/code&gt; 目录中，克隆后即可使用，无需手动复制。&lt;/p&gt;
&lt;h3&gt;msp_closed_loop_controller.py（闭环定高控制器）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
Closed-loop altitude hold controller for Betaflight SITL.
- Reads drone state via MSP over TCP 5761 (altitude, attitude, IMU, battery)
- Sends RC via UDP 9004 (100Hz)
- PID altitude control with attitude monitoring

This is the SAME architecture used on real hardware (TCP -&gt; UART serial).
&quot;&quot;&quot;
import socket, struct, time, signal, sys, argparse, threading
from msp_reader import MSPReader

BF_IP, BF_PORT = &apos;127.0.0.1&apos;, 9004
NUM_CH = 16

class ClosedLoopController:
    def __init__(self, target_alt=2.0, hover_base=1450, kp=80, ki=5, kd=40):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.msp = MSPReader()
        self.target_alt = target_alt
        self.hover_base = hover_base
        self.kp, self.ki, self.kd = kp, ki, kd
        self.integral = 0.0
        self.last_error = 0.0
        self.lock = threading.Lock()
        self.throttle = 1000
        self.aux1 = 1000
        self.running = True
        signal.signal(signal.SIGINT, self.stop)
        signal.signal(signal.SIGTERM, self.stop)

    def stop(self, *args):
        print(&quot;\nDisarming...&quot;)
        self.running = False
        with self.lock:
            self.throttle = 1000
            self.aux1 = 1000
        time.sleep(0.3)
        self.msp.stop()
        self.sock.close()
        sys.exit(0)

    def send_rc(self, channels):
        ts = time.time()
        pkt = struct.pack(&apos;&amp;#x3C;d&apos; + &apos;H&apos; * NUM_CH, ts, *channels)
        try:
            self.sock.sendto(pkt, (BF_IP, BF_PORT))
        except:
            pass

    def rc_thread(self):
        &quot;&quot;&quot;Send RC at 100Hz independently of state reading&quot;&quot;&quot;
        while self.running:
            with self.lock:
                t, a = self.throttle, self.aux1
            ch = [1500] * NUM_CH
            ch[2], ch[4] = t, a
            self.send_rc(ch)
            time.sleep(0.01)

    def run(self):
        print(&quot;=&quot; * 60)
        print(&quot;Betaflight SITL Closed-Loop Controller (MSP + UDP)&quot;)
        print(f&quot;Target altitude: {self.target_alt}m&quot;)
        print(f&quot;Hover base: {self.hover_base}  PID: kp={self.kp} ki={self.ki} kd={self.kd}&quot;)
        print(&quot;=&quot; * 60)

        if not self.msp.start():
            print(&quot;ERROR: Failed to connect MSP reader. Is SITL running?&quot;)
            sys.exit(1)

        threading.Thread(target=self.rc_thread, daemon=True).start()

        # Phase 1: Disarmed init (3s)
        print(&quot;\nPhase 1: Init (disarmed, 3s)...&quot;)
        with self.lock:
            self.throttle = 1000
            self.aux1 = 1000
        time.sleep(3)

        # Phase 2: Arm (2s) -- throttle MUST be 1000 for arming
        print(&quot;Phase 2: Arming (2s)...&quot;)
        with self.lock:
            self.throttle = 1000
            self.aux1 = 2000
        time.sleep(2)

        # Phase 3: Altitude hold with PID
        print(f&quot;Phase 3: Altitude hold at {self.target_alt}m...&quot;)
        last_time = time.time()

        while self.running:
            state = self.msp.get_state()
            now = time.time()
            dt = now - last_time
            if dt &amp;#x3C;= 0 or dt &gt; 1.0:
                dt = 0.05

            # Get altitude from MSP (cm -&gt; m).
            # Betaflight reports NED (positive=down), negate so &quot;up&quot; is positive.
            alt = -state.get(&apos;alt_cm&apos;, 0) / 100.0 if state else 0.0

            # PID control
            error = self.target_alt - alt
            self.integral += error * dt
            self.integral = max(-200, min(200, self.integral))
            deriv = (error - self.last_error) / dt
            pid = self.kp * error + self.ki * self.integral + self.kd * deriv
            thr = int(self.hover_base + pid)
            thr = max(1000, min(1900, thr))

            with self.lock:
                self.throttle = thr
                self.aux1 = 2000

            # Display
            roll = state.get(&apos;roll_deg&apos;, 0)
            pitch = state.get(&apos;pitch_deg&apos;, 0)
            volt = state.get(&apos;voltage_v&apos;, 0)
            rssi = state.get(&apos;rssi&apos;, 0)
            mode = state.get(&apos;flight_mode&apos;, 0)
            armed = bool(mode &amp;#x26; (1 &amp;#x3C;&amp;#x3C; 0)) if &apos;flight_mode&apos; in state else False

            print(f&quot;\r  alt={alt:.2f}m err={error:+.2f} thr={thr} &quot;
                  f&quot;r={roll:.0f}° p={pitch:.0f}° &quot;
                  f&quot;v={volt:.1f}V RSSI={rssi} armed={armed}  &quot;,
                  end=&apos;&apos;, flush=True)

            self.last_error = error
            last_time = now
            time.sleep(0.05)

def main():
    p = argparse.ArgumentParser()
    p.add_argument(&apos;--target-alt&apos;, type=float, default=2.0)
    p.add_argument(&apos;--hover-base&apos;, type=int, default=1450)
    p.add_argument(&apos;--kp&apos;, type=float, default=80)
    p.add_argument(&apos;--ki&apos;, type=float, default=5)
    p.add_argument(&apos;--kd&apos;, type=float, default=40)
    args = p.parse_args()
    ClosedLoopController(args.target_alt, args.hover_base,
                         args.kp, args.ki, args.kd).run()

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RC 发送线程（100Hz）&lt;/strong&gt;：从共享变量读取油门和 AUX 值，按 AETR 通道映射组装 16 通道数组，用 &lt;code&gt;struct.pack&lt;/code&gt; 封包通过 UDP 9004 发送&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSP 遥测线程（~20Hz）&lt;/strong&gt;：循环发送 MSPv1 请求帧，解析响应获取高度、姿态、电压等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主 PID 循环（20Hz）&lt;/strong&gt;：读取高度，与目标值比较得 error，经 PID 计算叠加到悬停油门基准值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MSPv1 请求帧格式（6 字节）：&lt;code&gt;$ M &amp;#x3C; size(0) cmd CRC(cmd)&lt;/code&gt;。响应帧：&lt;code&gt;$ M &gt; size cmd payload(N字节) CRC&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;真机对应关系：UDP 9004 → UART 串口（MSP_SET_RAW_RC），TCP 5761 → UART 串口（MSP 遥测）。&lt;code&gt;msp_reader.py&lt;/code&gt; 和 PID 逻辑完全复用，仅将 socket 替换为 serial。&lt;/p&gt;
&lt;h3&gt;msp_reader.py（MSP 遥测读取器）&lt;/h3&gt;
&lt;p&gt;MSP 遥测读取器是整个控制链路的关键组件。它通过 TCP 5761 端口连接 SITL，循环查询 5 条 MSP 命令（高度、姿态、IMU、状态、电压），将飞控遥测数据解析为 Python 字典供控制器使用。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
MSP telemetry reader for Betaflight SITL.
Connects to TCP 5761 and queries multiple MSP commands to read full drone state.
&quot;&quot;&quot;
import socket, struct, time, threading, select

class MSPReader:
    &quot;&quot;&quot;Connects to SITL TCP 5761 and reads full drone telemetry via MSP&quot;&quot;&quot;

    MSP_CMDS = {
        &apos;altitude&apos;: (109, &apos;&amp;#x3C;ih&apos;),           # alt_cm (int32), vario_cm_s (int16)
        &apos;attitude&apos;: (108, &apos;&amp;#x3C;hhh&apos;),          # roll_ddeg, pitch_ddeg, yaw_deg
        &apos;raw_imu&apos;:  (102, &apos;&amp;#x3C;hhhhhhhhh&apos;),    # acc[3], gyro[3], mag[3] (raw int16)
        &apos;status&apos;:   (101, &apos;&amp;#x3C;HHHIB&apos;),       # cycleTime, i2cErr, sensors, mode, profile
        &apos;analog&apos;:   (110, &apos;&amp;#x3C;BHHh&apos;),         # voltage_dV, mAh, rssi, amperage_cA
    }

    def __init__(self, host=&apos;127.0.0.1&apos;, port=5761):
        self.state = {}
        self.state_lock = threading.Lock()
        self.running = False
        self.host, self.port = host, port
        self.sock = None

    def connect(self, timeout=10):
        deadline = time.time() + timeout
        while time.time() &amp;#x3C; deadline:
            try:
                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.sock.settimeout(0.5)
                self.sock.connect((self.host, self.port))
                print(f&quot;[MSP] Connected to {self.host}:{self.port}&quot;)
                return True
            except:
                self.sock.close()
                time.sleep(0.5)
        print(&quot;[MSP] Connection timeout&quot;)
        return False

    def _build_msp_request(self, cmd):
        &quot;&quot;&quot;Build MSPv1 request frame (no payload). CRC = cmd ^ 0x00.&quot;&quot;&quot;
        return b&apos;$M&amp;#x3C;&apos; + bytes([0]) + bytes([cmd]) + bytes([cmd])

    def _parse_msp_response(self, data, cmd_id, fmt):
        &quot;&quot;&quot;Parse MSP response. Handles both v1 and v2 framing.&quot;&quot;&quot;
        idx = data.find(b&apos;$M&gt;&apos;)
        if idx &amp;#x3C; 0:
            idx = data.find(b&apos;$X&gt;&apos;)
        if idx &amp;#x3C; 0:
            return None
        if data[idx:idx+3] == b&apos;$M&gt;&apos;:
            if idx + 5 &gt; len(data): return None
            size = data[idx + 3]
            cmd = data[idx + 4]
            if cmd != cmd_id or idx + 5 + size &gt; len(data): return None
            payload = data[idx + 5: idx + 5 + size]
            try:
                # slice to exact format size — MSP payload may be larger
                return struct.unpack(fmt, payload[:struct.calcsize(fmt)])
            except: return None
        elif data[idx:idx+3] == b&apos;$X&gt;&apos;:
            if idx + 8 &gt; len(data): return None
            cmd = data[idx + 4] | (data[idx + 5] &amp;#x3C;&amp;#x3C; 8)
            size = data[idx + 6] | (data[idx + 7] &amp;#x3C;&amp;#x3C; 8)
            if cmd != cmd_id or idx + 8 + size &gt; len(data): return None
            payload = data[idx + 8: idx + 8 + size]
            try:
                return struct.unpack(fmt, payload[:struct.calcsize(fmt)])
            except: return None
        return None

    def _read_all(self):
        data = b&apos;&apos;
        try:
            while True:
                ready, _, _ = select.select([self.sock], [], [], 0.01)
                if not ready: break
                chunk = self.sock.recv(4096)
                if not chunk: break
                data += chunk
        except: pass
        return data

    def _send_and_recv(self, cmd_id, fmt):
        try:
            self.sock.sendall(self._build_msp_request(cmd_id))
            for _ in range(5):
                time.sleep(0.02)
                data = self._read_all()
                if data:
                    result = self._parse_msp_response(data, cmd_id, fmt)
                    if result is not None: return result
        except: pass
        return None

    def query_all(self):
        result = {}
        r = self._send_and_recv(109, &apos;&amp;#x3C;ih&apos;)
        if r: result[&apos;alt_cm&apos;] = r[0]; result[&apos;vario_cm_s&apos;] = r[1]
        r = self._send_and_recv(108, &apos;&amp;#x3C;hhh&apos;)
        if r: result[&apos;roll_deg&apos;] = r[0] / 10.0; result[&apos;pitch_deg&apos;] = r[1] / 10.0; result[&apos;yaw_deg&apos;] = r[2]
        r = self._send_and_recv(102, &apos;&amp;#x3C;hhhhhhhhh&apos;)
        if r: result[&apos;acc_x&apos;] = r[0]; result[&apos;acc_y&apos;] = r[1]; result[&apos;acc_z&apos;] = r[2]; result[&apos;gyro_x&apos;] = r[3]; result[&apos;gyro_y&apos;] = r[4]; result[&apos;gyro_z&apos;] = r[5]
        r = self._send_and_recv(101, &apos;&amp;#x3C;HHHIB&apos;)
        if r: result[&apos;cycle_time_us&apos;] = r[0]; result[&apos;sensors&apos;] = r[2]; result[&apos;flight_mode&apos;] = r[3]; result[&apos;pid_profile&apos;] = r[4]
        r = self._send_and_recv(110, &apos;&amp;#x3C;BHHh&apos;)
        if r: result[&apos;voltage_v&apos;] = r[0] / 10.0; result[&apos;mah&apos;] = r[1]; result[&apos;rssi&apos;] = r[2]
        return result

    def reader_thread(self):
        self.running = True
        while self.running:
            result = self.query_all()
            if result:
                with self.state_lock: self.state.update(result)
            time.sleep(0.05)

    def start(self):
        if not self.connect(): return False
        t = threading.Thread(target=self.reader_thread, daemon=True); t.start()
        return True

    def stop(self):
        self.running = False
        if self.sock: self.sock.close()

    def get_state(self):
        with self.state_lock: return dict(self.state)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MSP_STATUS 格式要点&lt;/strong&gt;：&lt;code&gt;&amp;#x3C;HHHIB&gt;&lt;/code&gt; = cycleTime(2B) + i2cError(2B) + sensors(2B) + flightModeFlags(4B) + pidProfile(1B) = 11 字节。&lt;code&gt;flightModeFlags&lt;/code&gt; 的 bit 0 对应 BOXARM（ARM 状态）。注意 &lt;code&gt;struct.unpack&lt;/code&gt; 要求 buffer 大小精确匹配格式，必须用 &lt;code&gt;payload[:struct.calcsize(fmt)]&lt;/code&gt; 截断，因为 MSP_STATUS 的实际 payload 远超 11 字节。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;常见问题排查汇总&lt;/h2&gt;
&lt;p&gt;| 现象 | 最可能的原因 | 排查方法 |
|------|-------------|---------|
| Gazebo 启动后 SITL 无 new fdm 消息 | 插件未加载或 UDP 不通 | &lt;code&gt;gz sim -v 4&lt;/code&gt; 查看是否出现 &quot;Loaded system [BetaFlightPlugin]&quot; |
| SITL 一直显示 RXLOSS（RC 已发但 RXLOSS 不消失） | 旧 &lt;code&gt;eeprom.bin&lt;/code&gt; 接收机模式设置错误 | &lt;code&gt;rm ~/bf_sitl/config/eeprom.bin&lt;/code&gt; 删除后重启 SITL |
| SITL 一直显示 RXLOSS（无 &lt;code&gt;new rc&lt;/code&gt; 消息） | RC 控制器未启动 | 确认控制器已在终端 3 运行，SITL 终端应出现 &lt;code&gt;new rc&lt;/code&gt; |
| SITL 显示 Arming disabled: THROTTLE | 解锁时油门通道值过高 | 检查 Phase 2 阶段 &lt;code&gt;ch[2]&lt;/code&gt; 是否为 1000 |
| SITL 显示 FAILSAFE RXLOSS | RC 发送间隔超过 100ms | 确保以 ≥50Hz 发送。阻塞操作（如文件读写）必须放入独立线程 |
| 螺旋桨转了但不起飞 | 悬停油门过低 | 逐步增加 &lt;code&gt;--hover-base&lt;/code&gt;，IRIS 约需 1450-1600 |
| 闭环控制器 alt=0.00 不变 | MSP 响应解析失败或高度符号错误 | 单独跑 &lt;code&gt;msp_reader.py&lt;/code&gt; 验证 MSP 通信 |
| 闭环控制器 alt 数值正确但飞机失控 | PID 参数过于激进 | 从小参数开始：&lt;code&gt;--kp 2 --ki 1 --kd 1&lt;/code&gt; |
| 编译 Betaflight SITL 失败 | 缺少构建依赖 | &lt;code&gt;sudo apt install build-essential&lt;/code&gt; |
| 编译插件时 &lt;code&gt;Could not find gz-sim8&lt;/code&gt; | 缺少 Gazebo 开发包 | &lt;code&gt;sudo apt install -y libgz-sim8-dev libgz-transport13-dev libgz-msgs10-dev libgz-math7-dev libgz-plugin2-dev libsdformat14-dev&lt;/code&gt; |
| 编译插件时 colcon 报 &lt;code&gt;Cannot find package gz-sim7&lt;/code&gt; | &lt;code&gt;package.xml&lt;/code&gt; 未改版本号 | 将 &lt;code&gt;package.xml&lt;/code&gt; 中 &lt;code&gt;gz-sim7&lt;/code&gt; 改为 &lt;code&gt;gz-sim8&lt;/code&gt;，&lt;code&gt;gz-transport12&lt;/code&gt; 改为 &lt;code&gt;gz-transport13&lt;/code&gt; |
| 编译插件失败，&lt;code&gt;gz/msgs/imu.pb.h not found&lt;/code&gt; | &lt;code&gt;gz-msgs10-dev&lt;/code&gt; 未安装 | &lt;code&gt;sudo apt install libgz-msgs10-dev&lt;/code&gt; |
| 编译插件失败，no matching function for Subscribe | &lt;code&gt;std::function&lt;/code&gt; + &lt;code&gt;std::bind&lt;/code&gt; 未正确实施 | 对照检查二的代码示例逐一核实 |
| &lt;code&gt;gz sim --versions&lt;/code&gt; 显示多版本 | 混装了多个 Gazebo 版本 | 只保留 &lt;code&gt;gz-harmonic&lt;/code&gt; |&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文完整介绍了 Betaflight SITL + Gazebo Harmonic 仿真环境的搭建流程，包括 SITL 编译、Gazebo 插件的四处关键适配修改、CLI 配置解锁、AUX 通道模式设置以及 Python 闭环定高控制器的实现。Betaflight 的 SITL 模式为飞控算法验证提供了无需硬件的完整闭环仿真能力，配合 Gazebo 物理引擎可以验证从传感器输入到电机输出的完整控制链路。如果需要在仿真中集成视觉感知，可以参考 &lt;a href=&quot;/zh/posts/px4-ros2-yolo&quot;&gt;无人机仿真环境调用 YOLO 的简单示例&lt;/a&gt;，为目标识别任务做准备。&lt;/p&gt;
&lt;p&gt;欢迎在 &lt;a href=&quot;https://github.com/Jinyao-Chen/bf_sitl&quot;&gt;GitHub 仓库&lt;/a&gt; 上提交改进和反馈！&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://betaflight.com/docs/development/SITL&quot;&gt;Betaflight SITL 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/osrf/vehicle_gateway&quot;&gt;OSRF vehicle_gateway 仓库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Betaflight 源码 — SITL 平台实现 — sitl.c、target.h、udplink.c&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gazebosim.org/docs/harmonic&quot;&gt;Gazebo Harmonic 安装文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ROS2 + Gazebo 桥接包 — ros-humble-ros-gzharmonic API 文档&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://app.betaflight.com/&quot;&gt;Betaflight Configurator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2 Hardware: Flashing the Jetson Onboard Computer</title><link>https://duduuu.xyz/en/posts/px4-ros2-jetson-flash</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-jetson-flash</guid><description>Complete guide to flashing Ubuntu 22.04 onto Nvidia Jetson Orin NX using SDK Manager, covering recovery mode, installation settings, and USB stability</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;After covering simulation workflows in the previous five articles, we now begin a new section: real drone flight. Real flight requires significantly more configuration and higher safety standards than simulation, and we&apos;ll cover each aspect in turn. This article starts with the essential first step: flashing the onboard computer.&lt;/p&gt;
&lt;p&gt;When controlling drones with ROS, the onboard computer handles critical computation tasks — providing sufficient processing power without interfering with the flight controller, enabling real-time path planning, multi-drone cooperative decision-making, visual SLAM, and more. If you haven&apos;t set up a simulation environment yet, start with the &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 Simulation Setup Guide&lt;/a&gt;. This guide uses the Nvidia Jetson Orin NX and flashes Ubuntu 22.04.&lt;/p&gt;
&lt;p&gt;Hardware required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Onboard computer (Nvidia Jetson Orin NX used here)&lt;/li&gt;
&lt;li&gt;Power supply for the onboard computer&lt;/li&gt;
&lt;li&gt;Mouse, keyboard, and monitor&lt;/li&gt;
&lt;li&gt;At least one Dupont (jumper) wire&lt;/li&gt;
&lt;li&gt;A personal computer with Ubuntu dual-boot or virtual machine&lt;/li&gt;
&lt;li&gt;USB to Type-C cable&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Host Environment Dependencies&lt;/h2&gt;
&lt;p&gt;Install required packages on the host Ubuntu system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install sshpass abootimg nfs-kernel-server libxml2-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Flashing via a virtual machine will download many files. Ensure at least &lt;strong&gt;100GB&lt;/strong&gt; of free disk space!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Installing SDK Manager&lt;/h2&gt;
&lt;p&gt;SDK Manager is the standard flashing tool for Nvidia onboard computers. Download it from &lt;a href=&quot;https://developer.nvidia.com/sdk-manager&quot;&gt;Nvidia SDK Manager&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If prompted to log in, register a free Nvidia account. Download the &lt;code&gt;.deb&lt;/code&gt; package for your Ubuntu version.&lt;/p&gt;
&lt;p&gt;Install the package:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/Download
sudo dpkg -i sdkmanager_2.3.0-12617_amd64.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you encounter dependency errors, fix and update:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reboot and reinstall. Once the installation completes, log in again:&lt;/p&gt;
&lt;h2&gt;Entering Recovery Mode&lt;/h2&gt;
&lt;p&gt;For boards with a Recovery button, press and hold as per the documentation. For the Orin NX (which lacks a Recovery button), we short specific pins during power-on.&lt;/p&gt;
&lt;p&gt;According to the Nvidia Jetson Orin NX documentation, short the GND and FC REC pins for 10 seconds. These pins are located directly beneath the fan:&lt;/p&gt;
&lt;p&gt;GND is the second pin, FC REC is the third:&lt;/p&gt;
&lt;p&gt;Short the two pins, connect the USB to your host PC and the Type-C to the Jetson, then power on for 10 seconds. SDK Manager will show the following screen, indicating successful recovery mode entry.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Do NOT click &quot;Continue&quot; yet. Remove the Dupont wire now — it is no longer needed and could damage the board. Keep the power supply connected throughout the flashing process.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Flashing Process&lt;/h2&gt;
&lt;p&gt;Back in the initial screen, configure the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Uncheck &lt;strong&gt;Host Machine&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Target Hardware&lt;/strong&gt;, select your board model (should auto-detect after USB and Type-C connection)&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;JetPack version&lt;/strong&gt; matching your Ubuntu target — JetPack 6.2.1 supports Ubuntu 22.04&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Click Continue to proceed to step 2. Leave the default component selections. Check both options at the bottom (&quot;Download now&quot; and &quot;Install later&quot;) — SDK Manager will download the required packages first. If prompted that no Nvidia directory exists, click &quot;Create.&quot;&lt;/p&gt;
&lt;p&gt;When the progress bar completes and &quot;Download Completed successfully&quot; appears, click &quot;Finish&quot; to proceed to installation.&lt;/p&gt;
&lt;p&gt;SDK Manager returns to the first step. Repeat the earlier configuration (uncheck Host Machine, select JetPack version) and proceed. In the component list, all items should show &quot;Downloaded.&quot; Now &lt;strong&gt;uncheck&lt;/strong&gt; &quot;Download now. Install later&quot; since all components are already downloaded, and proceed directly to installation.&lt;/p&gt;
&lt;p&gt;SDK Manager will now begin the lengthy flashing process. The first popup asks for a username and password — similar to a fresh Ubuntu install. Use &lt;code&gt;jetson&lt;/code&gt; or &lt;code&gt;nvidia&lt;/code&gt; to avoid potential SSH issues later.&lt;/p&gt;
&lt;p&gt;Subsequent popups will appear. Since we&apos;re using USB (not Ethernet), ensure &quot;USB&quot; is selected. The username and password should match the first entry (usually no changes needed).&lt;/p&gt;
&lt;h2&gt;Critical: Maintaining USB Connection Stability&lt;/h2&gt;
&lt;p&gt;Stay at your screen during the flashing process, especially VM users. The system will repeatedly disconnect (Removed) and reconnect (added) the USB device. Dual-boot users just need to monitor automatic reconnection. VM users must manually reconnect the USB to the virtual machine each time — &lt;strong&gt;never select &quot;Remember my choice&quot;&lt;/strong&gt;:&lt;/p&gt;
&lt;h2&gt;Flashing Complete&lt;/h2&gt;
&lt;p&gt;Connect the monitor, keyboard, and mouse to the Jetson. As flashing progresses, the display driver will install and you&apos;ll see the Ubuntu installation process on the monitor. Now just wait — keeping the USB connection stable.&lt;/p&gt;
&lt;p&gt;When SDK Manager&apos;s progress bar completes and shows &quot;Finished,&quot; flashing is done. The monitor will display the Ubuntu login screen — username &lt;code&gt;jetson&lt;/code&gt;. Mission accomplished!&lt;/p&gt;
&lt;p&gt;Now connect your peripherals and configure the system. Clone the Micro-XRCE-DDS-Agent, ROS2 workspace, px4_msgs, and px4_ros_com source code onto the Jetson as described in the simulation setup articles. Once flashing is complete, the next step is &lt;a href=&quot;/en/posts/px4-ros2-mavros&quot;&gt;Controlling PX4 via MAVROS2&lt;/a&gt; to achieve Offboard takeoff.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Like setting up a simulation environment, real-flight experiments have higher safety requirements and correspondingly more involved procedures. This article aims to make it easier for readers to begin real-flight experiments with ROS. In hands-on engineering fields like ROS and UAVs, experiential knowledge is indispensable — it lets us focus our energy on the core work. The author looks forward to continuously supplementing, correcting, and updating this knowledge together with the community.&lt;/p&gt;
&lt;p&gt;As always, suggestions and feedback are sincerely welcomed. Thank you for reading and for your continued support!&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.nvidia.com/sdk-manager&quot;&gt;Nvidia SDK Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1904187016243045984&quot;&gt;Zhihu: Jetson Flashing Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1DMTWzSEto/&quot;&gt;Bilibili: Jetson Flashing Video Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vrlab.buaa.edu.cn/&quot;&gt;BUAA VR Lab&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2真机实践：为机载电脑刷机（以Jetson NX为例）</title><link>https://duduuu.xyz/zh/posts/px4-ros2-jetson-flash</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-jetson-flash</guid><description>以Nvidia Jetson Orin NX为例，使用SDK Manager刷入Ubuntu 22.04的完整步骤，包含烧录模式进入、安装配置、USB连接等重要细节</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在前五篇文章介绍完仿真的基本流程后，我们开始一个新的部分：真机飞行。真机相对于仿真需要更多的配置，对安全性有更高的要求，我们会依次介绍。今天首先介绍机载电脑的刷机流程。&lt;/p&gt;
&lt;p&gt;在利用ROS控制无人机时，机载电脑承担着关键的计算任务，可以在不影响底层飞控的同时提供足够的算力，实现实时路径规划、多机协同决策、视觉SLAM等任务。如果还未搭建仿真环境，建议先参考 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真环境开发教程&lt;/a&gt; 完成仿真端的准备工作。本文以Nvidia Jetson Orin NX为例，刷入Ubuntu 22.04系统。&lt;/p&gt;
&lt;p&gt;需要准备的硬件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;机载电脑（此处使用Nvidia Jetson Orin NX）&lt;/li&gt;
&lt;li&gt;机载电脑电源&lt;/li&gt;
&lt;li&gt;鼠标、键盘外设以及显示器&lt;/li&gt;
&lt;li&gt;至少一根杜邦线&lt;/li&gt;
&lt;li&gt;配备有Ubuntu双系统或者虚拟机的个人电脑&lt;/li&gt;
&lt;li&gt;USB转type-C线&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;主机环境依赖&lt;/h2&gt;
&lt;p&gt;主机的Ubuntu系统需要安装必要的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install sshpass abootimg nfs-kernel-server libxml2-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：虚拟机烧录时会下载大量的烧录文件，请使用虚拟机进行烧录的朋友务必留够不少于100G的磁盘空间！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;安装SDK Manager&lt;/h2&gt;
&lt;p&gt;SDK Manager是常见的Nvidia机载电脑的刷机工具，我们先前往 &lt;a href=&quot;https://developer.nvidia.com/sdk-manager&quot;&gt;Nvidia SDK Manager&lt;/a&gt; 下载。&lt;/p&gt;
&lt;p&gt;如果进入后需要登录注册，大家自行注册一个Nvidia账号即可。进入页面后，直接下载对应Ubuntu系统的deb安装包。&lt;/p&gt;
&lt;p&gt;下载后的文件统一储存在 &lt;code&gt;~/Download&lt;/code&gt; 目录下，进入并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/Download
sudo dpkg -i sdkmanager_2.3.0-12617_amd64.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果此时出现报错，多为依赖项缺失或者依赖冲突（dependencies problems），需要修复并更新依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成之后重新启动系统并再次安装。安装完成后出现以下界面，此处又需要登录，直接登录即可：&lt;/p&gt;
&lt;p&gt;至此，用于刷机的SDK Manager就安装完成了。&lt;/p&gt;
&lt;h2&gt;启动烧录模式&lt;/h2&gt;
&lt;p&gt;对于有Recovery按键的机载电脑，根据具体说明长按或短按按键即可。对于Orin NX这类无Recovery按键的机载电脑，通常采用短接引脚通电的方式，此处笔者给出说明。&lt;/p&gt;
&lt;p&gt;根据Nvidia Jetson Orin NX官方文档的说明，我们需要将GND和FC REC两个引脚短接通电10秒。这两个引脚位于机载电脑风扇的正下方：&lt;/p&gt;
&lt;p&gt;GND引脚为第二个引脚，FC REC为第三个：&lt;/p&gt;
&lt;p&gt;用一根杜邦线将两个引脚短接，随后将USB口接入本机电脑，type-C口接入机载电脑，为机载电脑接入电源10秒后，SDK Manager上会进入以下页面，此时进入烧录模式成功。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：请不要急于点&quot;Continue&quot;，后续还有一些说明。现在可以将短接的杜邦线拔掉，后续不再需要，防止烧坏机载电脑。后续烧录过程中，请保持电源的连接。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;进行烧录&lt;/h2&gt;
&lt;p&gt;回到刚刚进入的页面，现在需要做以下几个设置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取消勾选 &lt;strong&gt;Host Machine&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;strong&gt;Target Hardware&lt;/strong&gt; 中选择自己的机载电脑型号（在USB和type-C连接成功后，应该会自动显示）&lt;/li&gt;
&lt;li&gt;根据自己的Jetpack版本号选择SDK Version，笔者在之前的ROS2仿真中使用的是Ubuntu 22.04，此处也对应选择 &lt;strong&gt;Jetpack 6.2.1&lt;/strong&gt;，支持Ubuntu 22.04&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时可以选择Continue进行第二步，这里会检查相应组件的安装情况，大家直接默认不动就好。同时勾选底部以下两个选项，表示先下载组件，再进行安装，此时SDK Manager会开始下载刷机所需的包。如果弹出主目录里没有nvidia的目录，直接点&quot;Create&quot;继续即可。&lt;/p&gt;
&lt;p&gt;当底部进度条走完并且出现&quot;Download Completed successfully&quot;的时候，说明下载完成，点击&quot;Finish&quot;进入安装。&lt;/p&gt;
&lt;p&gt;随后SDK Manager会再次进入第一步选择各种型号的页面，像最先开始一样取消勾选&quot;Host Machine&quot;并选择对应Jetpack版本号，进入下一步。此时在第3栏中，SDK Manager会检测下载状态，如图所示均为&quot;Downloaded&quot;，表示均已下载完成。现在需要取消勾选&quot;Download now. Install later&quot;，因为已经下载好了，直接安装刷机即可。&lt;/p&gt;
&lt;p&gt;随后SDK Manager会进入长时间的刷机过程，需要保持等待。第一个弹窗需要填写用户名和密码，和首次装Ubuntu系统时基本类似。这里推荐使用&lt;code&gt;jetson&lt;/code&gt;或&lt;code&gt;nvidia&lt;/code&gt;，防止后续使用SSH进行本地电脑与机载电脑连接时出现奇怪的bug。&lt;/p&gt;
&lt;p&gt;后续还会弹出几个弹窗，因为我们使用的是USB连接而非以太网连接，只需确保弹窗中的选项是USB，同时用户名和密码为第一次输入的用户名和密码即可（一般而言不需要修改）。&lt;/p&gt;
&lt;h2&gt;烧录关键：保持USB连接稳定&lt;/h2&gt;
&lt;p&gt;在烧录过程中，需要一直在屏幕前，尤其是虚拟机的用户。系统会显示USB不断地被弹出（Removed）和重新添加（added）。如果使用双系统进行烧录，只需注意USB被弹出后是否及时自动重新添加；而如果使用虚拟机，还要将USB连接到虚拟机而非主机，此过程不断反复，需要不断操作，&lt;strong&gt;不可以选择&quot;记住我的选择&quot;&lt;/strong&gt;：&lt;/p&gt;
&lt;h2&gt;烧录完毕&lt;/h2&gt;
&lt;p&gt;将鼠标、键盘、显示器等外设连接到机载电脑上。随着烧录的进行，显示器驱动会成功安装，可以在显示器上看见Ubuntu系统的安装进程。现在要做的就是等待，并随时保证USB的连接稳定。&lt;/p&gt;
&lt;p&gt;等到SDK Manager的安装进度条走完，显示&quot;Finished&quot;，则说明烧录完成。此时连接显示器，可以看见Ubuntu的登录界面，用户名为jetson，大功告成！&lt;/p&gt;
&lt;p&gt;现在，大家可以连接鼠标、键盘外设，对机载电脑进行设置操作。同时，按照我们之前给出的ROS2+PX4仿真开发流程，将实机飞行需要用到的Micro-XRCE-DDS-Agent和需要的ROS2工作空间及px4_msgs、px4_ros_com源码克隆到机载电脑上。刷机完成后，下一步就是 &lt;a href=&quot;/zh/posts/px4-ros2-mavros&quot;&gt;使用 MAVROS2 让机载电脑控制 PX4 无人机&lt;/a&gt;，实现 Offboard 模式起飞。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;与配置开发仿真环境一样，真机的实验有更高的安全性要求，因此相应的流程也会更加繁琐。希望通过本篇文章，能够让大家更加轻松地上手使用ROS进行真机实验。在类似ROS、无人机这些工程性质较浓的领域，一些经验性的知识显得必不可少，可以帮助我们将更多精力放在核心的工作中，笔者期望与大家一起不断补充、修改、更新这些知识。&lt;/p&gt;
&lt;p&gt;最后，诚挚地希望各位读者能够提出意见或者建议，感谢大家的阅读和不断的支持鼓励！&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.nvidia.com/sdk-manager&quot;&gt;Nvidia SDK Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/1904187016243045984&quot;&gt;知乎：Jetson刷机教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1DMTWzSEto/&quot;&gt;B站：Jetson烧录视频教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vrlab.buaa.edu.cn/&quot;&gt;北航VR实验室&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>Distributed Control &amp; Second-Order Consensus for UAV Swarms</title><link>https://duduuu.xyz/en/posts/px4-ros2-distributed-consensus</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-distributed-consensus</guid><description>ROS2 distributed control with second-order consensus for UAV swarms, LMI-based gain synthesis and PX4 velocity-layer integration</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;This article presents a distributed control framework for multi-UAV formation systems, built on the &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 simulation environment&lt;/a&gt; and &lt;a href=&quot;/en/posts/px4-ros2-multi-offboard&quot;&gt;Multi-Drone Offboard Control&lt;/a&gt; foundations. By integrating ROS2&apos;s distributed communication architecture with PX4&apos;s MAVLink protocol, the system enables real-time state sharing and cooperative control across multiple drones. The consensus-based control law allows each drone to rely solely on local neighbor information to achieve formation keeping and trajectory tracking, offering a highly reliable inter-drone communication method for large-scale swarm missions.&lt;/p&gt;
&lt;p&gt;UAV formation control generally falls into two architectural paradigms: centralized and distributed. Their differences lie primarily in decision hierarchy, communication topology, and system robustness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Centralized control&lt;/strong&gt; relies on a single central node (e.g., a ground station or a lead UAV) for global decision-making. All drones execute commands issued by this central node. It offers high control precision and excellent formation consistency, making it suitable for complex missions. However, it suffers from single-point failure risk, high communication load on the central node, and poor scalability -- especially critical when the central node fails or comes under attack, which can severely compromise formation safety.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Distributed control&lt;/strong&gt; has no central node. Each drone makes autonomous decisions through local communication, interacting only with neighboring drones. This approach provides strong robustness, high scalability, and dynamic adaptability for large-scale formations, at the cost of higher coordination complexity and weaker global optimization capability.&lt;/p&gt;
&lt;p&gt;This article is based on a formation of six UAVs. We first establish a first-order swarm cooperative control law, then extend it to a more general second-order model (acceleration input), solve for the feedback gain matrix via the LMI method, and finally validate the approach in Gazebo simulation. The final section provides a rigorous proof of the error dynamics under constant formation offsets.&lt;/p&gt;
&lt;h2&gt;Mathematical Preliminaries&lt;/h2&gt;
&lt;p&gt;Extensive literature exists on the mathematical background of distributed cooperative control systems; this section only provides the necessary review. Readers are referred to the following for deeper understanding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Graph theory fundamentals: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/32312200&quot;&gt;Multi-Agent System Control (1) -- Graph Theory&lt;/a&gt; (Chinese)&lt;/li&gt;
&lt;li&gt;Properties of the Laplacian matrix: &lt;a href=&quot;https://blog.csdn.net/qq_43391414/article/details/112277987&quot;&gt;CSDN Blog&lt;/a&gt; (Chinese)&lt;/li&gt;
&lt;li&gt;Consensus control lemmas: &lt;a href=&quot;https://blog.csdn.net/hongliyu_lvliyu/article/details/106973870&quot;&gt;CSDN Blog&lt;/a&gt; (Chinese), which can be used to prove the convergence of the control law&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From a theoretical perspective, it can be shown that the swarm cooperative system constructed in this article is capable of achieving cooperative UAV formation tasks.&lt;/p&gt;
&lt;h2&gt;First-Order Consensus Control&lt;/h2&gt;
&lt;h3&gt;System Model&lt;/h3&gt;
&lt;p&gt;Consider a formation of $N+1$ UAVs. The drone labeled $0$ is the leader, and the remaining $N$ drones labeled $i = 1, 2, \dots, N$ are followers.&lt;/p&gt;
&lt;p&gt;Each drone uses a first-order kinematic model in which the state consists only of position information. Let $p_i \in \mathbb{R}^3$ denote the position of drone $i$, and let the control input be a velocity command $v_i \in \mathbb{R}^3$:&lt;/p&gt;
&lt;p&gt;$$
\dot{p}_i = v_i
$$&lt;/p&gt;
&lt;h3&gt;First-Order Consensus Protocol&lt;/h3&gt;
&lt;p&gt;We adopt the classical linear consensus protocol, where the control input of drone $i$ is determined by the weighted sum of neighbor state deviations:&lt;/p&gt;
&lt;p&gt;$$
v_i = k_p \sum_{j \in \mathcal{N}&lt;em&gt;i} w&lt;/em&gt;{ij}(p_j - p_i)
$$&lt;/p&gt;
&lt;p&gt;where $\mathcal{N}&lt;em&gt;i$ is the neighbor set of drone $i$, $w&lt;/em&gt;{ij} \ge 0$ are communication weights, and $k_p &gt; 0$ is the position gain.&lt;/p&gt;
&lt;p&gt;For a connected graph, it can be proved that the position states satisfy $p_i(t) - p_j(t) \to 0$ as $t \to \infty$, meaning all followers asymptotically converge to the leader&apos;s position. This is the most fundamental application of distributed consensus in swarm formation -- global position synchronization achieved using only local neighbor position deviations.&lt;/p&gt;
&lt;h2&gt;Second-Order Distributed Control&lt;/h2&gt;
&lt;h3&gt;Theoretical Modeling&lt;/h3&gt;
&lt;h4&gt;Single-UAV Dynamics&lt;/h4&gt;
&lt;p&gt;We consider a formation of $N+1$ UAVs. Drone $0$ serves as the leader, chosen as a virtual leader (not physically flying). Drones labeled $i = 1, 2, \dots, N$ are followers.&lt;/p&gt;
&lt;p&gt;Each drone is modeled with second-order dynamics (containing both position and velocity states). The state of drone $i$ is:&lt;/p&gt;
&lt;p&gt;$$
\xi_i = \begin{bmatrix} p_i \ v_i \end{bmatrix} \in \mathbb{R}^6, \quad p_i \in \mathbb{R}^3, \quad v_i \in \mathbb{R}^3
$$&lt;/p&gt;
&lt;p&gt;where $p_i$ is the position and $v_i$ is the velocity. The control input is a second-order acceleration command $u_i \in \mathbb{R}^3$.&lt;/p&gt;
&lt;p&gt;The linearized second-order model of each drone can be written in matrix form:&lt;/p&gt;
&lt;p&gt;$$
\dot{\xi}_i = A \xi_i + B u_i \tag{1}
$$&lt;/p&gt;
&lt;p&gt;The matrices $A \in \mathbb{R}^{6 \times 6}$ and $B \in \mathbb{R}^{6 \times 3}$ are block matrices, obtained by simple derivation:&lt;/p&gt;
&lt;p&gt;$$
\dot{\xi}&lt;em&gt;i = \begin{bmatrix} \dot{p}&lt;em&gt;i \ \dot{v}&lt;em&gt;i \end{bmatrix} = \begin{bmatrix} v_i \ u_i \end{bmatrix} = \begin{bmatrix} 0&lt;/em&gt;{3\times3} &amp;#x26; I_3 \ 0&lt;/em&gt;{3\times3} &amp;#x26; 0&lt;/em&gt;{3\times3} \end{bmatrix} \begin{bmatrix} p_i \ v_i \end{bmatrix} + \begin{bmatrix} 0_{3\times3} \ I_3 \end{bmatrix} u_i
$$&lt;/p&gt;
&lt;p&gt;$$
\Rightarrow A = \begin{bmatrix} 0_{3\times3} &amp;#x26; I_3 \ 0_{3\times3} &amp;#x26; 0_{3\times3} \end{bmatrix}, \quad B = \begin{bmatrix} 0_{3\times3} \ I_3 \end{bmatrix} \tag{2}
$$&lt;/p&gt;
&lt;h4&gt;Linear Consensus Protocol&lt;/h4&gt;
&lt;p&gt;We adopt the classical linear consensus protocol, where the input of each follower drone ($i = 1, 2, \dots, N$) can be expressed as the weighted sum of neighbor state deviations:&lt;/p&gt;
&lt;p&gt;$$
u_i = K \sum_{j=0}^{N} w_{ij}(\xi_j - \xi_i) \tag{3}
$$&lt;/p&gt;
&lt;p&gt;where $K$ is the state-feedback gain matrix.&lt;/p&gt;
&lt;p&gt;Rewrite this in Laplacian matrix form. Define the weight matrix $W = (w_{ij})$, degree matrix $D = \text{diag}(d_0, \dots, d_N)$ where $d_i = \sum_j w_{ij}$, and Laplacian matrix $L = D - W$ whose entries satisfy $l_{ii} = \sum_{j=0}^N w_{ij}$, $l_{ij} = -w_{ij}$ ($i \ne j$), $w_{ii} = 0$.&lt;/p&gt;
&lt;p&gt;Expand equation (3):&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
u_i &amp;#x26;= K \sum_{j=0}^{N} w_{ij}(\xi_j - \xi_i) \
&amp;#x26;= K\left( \sum_{j=0}^{N} w_{ij} \xi_j - \left(\sum_{j=0}^{N} w_{ij}\right)\xi_i \right) \
&amp;#x26;= K\left( \sum_{j=0, j \ne i}^{N} w_{ij} \xi_j + w_{ii}\xi_i - \left(\sum_{j=0}^{N} w_{ij}\right)\xi_i \right) \
&amp;#x26;= K\left( -\sum_{j=0, j \ne i}^{N} l_{ij} \xi_j + 0 - l_{ii}\xi_i \right) \
&amp;#x26;= -K \sum_{j=0}^{N} l_{ij} \xi_j
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;p&gt;$$
u_i = -K \sum_{j=0}^{N} l_{ij} \xi_j, \quad i = 1, 2, \dots, N \tag{4}
$$&lt;/p&gt;
&lt;h4&gt;Error System Definition&lt;/h4&gt;
&lt;p&gt;Define the error of each follower relative to the leader (labeled $0$):&lt;/p&gt;
&lt;p&gt;$$
e_i = \xi_i - \xi_0, \quad i = 1, \dots, N \tag{5}
$$&lt;/p&gt;
&lt;p&gt;From the single-drone dynamics (equation (1)):&lt;/p&gt;
&lt;p&gt;$$
\dot{e}_i = \dot{\xi}_i - \dot{\xi}_0 = A\xi_i + Bu_i - \dot{\xi}_0
$$&lt;/p&gt;
&lt;p&gt;Substitute the control law (equation (4)):&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A\xi_i - BK\sum&lt;/em&gt;{j=0}^{N} l_{ij} \xi_j - \dot{\xi}_0
$$&lt;/p&gt;
&lt;p&gt;For all $j \ge 1$, we have $\xi_j = e_j + \xi_0$. Split into $j=0$ and $j \ge 1$:&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} l_{ij} \xi_j = \sum_{j=1}^{N} l_{ij} e_j + \left(l_{i0} + \sum_{j=1}^{N} l_{ij}\right)\xi_0
$$&lt;/p&gt;
&lt;p&gt;Using the Laplacian&apos;s zero row-sum property $\sum_{j=0}^N l_{ij} = 0$, we therefore have $l_{i0} + \sum_{j=1}^N l_{ij} = 0$, so:&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} l_{ij} \xi_j = \sum_{j=1}^{N} l_{ij} e_j
$$&lt;/p&gt;
&lt;p&gt;Substituting back yields the error dynamics for a single follower $i$:&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A e_i - BK \sum&lt;/em&gt;{j=1}^{N} l_{ij} e_j + (A\xi_0 - \dot{\xi}_0) \tag{6}
$$&lt;/p&gt;
&lt;p&gt;Stack all $N$ followers:&lt;/p&gt;
&lt;p&gt;$$
e = \begin{bmatrix} e_1 \ \vdots \ e_N \end{bmatrix} \in \mathbb{R}^{6N}, \quad d = \mathbf{1}&lt;em&gt;N \otimes \left(A\xi_0 - \dot{\xi}&lt;em&gt;0\right) \in \mathbb{R}^{6N}, \quad \tilde{L} = [-l&lt;/em&gt;{ij}]&lt;/em&gt;{i,j=1}^N
$$&lt;/p&gt;
&lt;p&gt;Writing the error dynamics in matrix form:&lt;/p&gt;
&lt;p&gt;$$
\dot{e} = \left(I_N \otimes A + \tilde{L} \otimes BK\right) e + d \tag{7}
$$&lt;/p&gt;
&lt;h4&gt;Lyapunov Function and LMI Condition&lt;/h4&gt;
&lt;p&gt;Choose the Lyapunov function $V(e) = e^\top P e$, where $P = I_N \otimes X^{-1}$ and $X \succ 0$ is a symmetric positive-definite matrix:&lt;/p&gt;
&lt;p&gt;$$
V(e) = e^\top \left(I_N \otimes X^{-1}\right) e \tag{8}
$$&lt;/p&gt;
&lt;p&gt;The closed-loop system (homogeneous part) of the error dynamics is:&lt;/p&gt;
&lt;p&gt;$$
\dot{e} = A_{cl} e, \quad A_{cl} = I_N \otimes A + \tilde{L} \otimes BK \tag{9}
$$&lt;/p&gt;
&lt;p&gt;Compute the Lyapunov derivative:&lt;/p&gt;
&lt;p&gt;$$
\dot{V} = e^\top \left( A_{cl}^\top P + P A_{cl} \right) e
$$&lt;/p&gt;
&lt;p&gt;To guarantee asymptotic stability, we require $\dot{V} \prec 0$, i.e.:&lt;/p&gt;
&lt;p&gt;$$
A_{cl}^\top P + P A_{cl} \prec 0 \tag{10}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Congruence Transformation Note:&lt;/strong&gt; Let $S \in \mathbb{R}^{n \times n}$ be a symmetric matrix and $M \in \mathbb{R}^{n \times n}$ be an invertible matrix. Define $\bar{S} = M S M^\top$. Then $S \succ 0 \iff \bar{S} \succ 0$ and $S \prec 0 \iff \bar{S} \prec 0$. For any nonzero vector $z$, $z^\top \bar{S} z = (M^\top z)^\top S (M^\top z)$, and since $M$ is invertible the sign is preserved.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Apply the congruence transformation to eliminate $X^{-1}$:&lt;/p&gt;
&lt;p&gt;$$
\bar{S} = (I_N \otimes X) \left( P A_{cl} + A_{cl}^\top P \right) (I_N \otimes X)
$$&lt;/p&gt;
&lt;p&gt;Then $P A_{cl} + A_{cl}^\top P \prec 0 \iff \bar{S} \prec 0$. Expanding $\bar{S}$:&lt;/p&gt;
&lt;p&gt;$$
\bar{S} = I_N \otimes (AX + XA^\top) + \tilde{L} \otimes (BKX) + \tilde{L}^\top \otimes \big(X(BK)^\top\big) \tag{11}
$$&lt;/p&gt;
&lt;p&gt;Apply the variable substitution $Y = KX$ to eliminate bilinear coupling:&lt;/p&gt;
&lt;p&gt;$$
BKX = BY, \quad X(BK)^\top = (BY)^\top
$$&lt;/p&gt;
&lt;p&gt;Thus the LMI condition is:&lt;/p&gt;
&lt;p&gt;$$
I_N \otimes (AX + XA^\top) + \tilde{L} \otimes (BY) + \tilde{L}^\top \otimes (BY)^\top \prec 0, \quad X \succ 0 \tag{12}
$$&lt;/p&gt;
&lt;h4&gt;Control Law with Formation Offsets&lt;/h4&gt;
&lt;p&gt;Assume the formation has desired relative positions. Define $r_{ij}$ as the desired relative offset between follower $i$ and follower $j$. The control law is modified to:&lt;/p&gt;
&lt;p&gt;$$
u_i = K \sum_{j=0}^{N} w_{ij} \big[(\xi_j - \xi_i) + r_{ij}\big] = -K \sum_{j=0}^{N} l_{ij} \xi_j + K \sum_{j=0}^{N} w_{ij} r_{ij} \tag{13}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redefine the error term.&lt;/strong&gt; Let $r_{ij} = r_i - r_j$ (with $r_0 = 0$), where $r_i$ is the desired formation position offset of follower $i$ relative to the leader. Define the new error as:&lt;/p&gt;
&lt;p&gt;$$
e_i = \xi_i - (\xi_0 + r_i), \quad i = 1, \dots, N
$$&lt;/p&gt;
&lt;p&gt;Differentiate the error, substituting the single-drone dynamics (1) and the control law (13):&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A\xi_i - BK \sum&lt;/em&gt;{j=0}^{N} l_{ij} \xi_j + BK \sum_{j=0}^{N} w_{ij} r_{ij} - \dot{\xi}_0 - \dot{r}_i \tag{14}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simplify Sum 1 (Laplacian term).&lt;/strong&gt; For $j \ge 1$, substitute $\xi_j = e_j + \xi_0 + r_j$, and $\xi_0 = \xi_0 + r_0$:&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\sum_{j=0}^{N} l_{ij} \xi_j &amp;#x26;= l_{i0} \xi_0 + \sum_{j=1}^{N} l_{ij} (e_j + \xi_0 + r_j) \
&amp;#x26;= \sum_{j=1}^{N} l_{ij} e_j + \Big(l_{i0} + \sum_{j=1}^{N} l_{ij}\Big) \xi_0 + \sum_{j=1}^{N} l_{ij} r_j
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;From the Laplacian zero row-sum property ($\sum_{j=0}^N l_{ij} = 0$), we obtain $l_{i0} + \sum_{j=1}^N l_{ij} = 0$, hence:&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} l_{ij} \xi_j = \sum_{j=1}^{N} l_{ij} e_j + \sum_{j=1}^{N} l_{ij} r_j \tag{15}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simplify Sum 2 (formation weighting term).&lt;/strong&gt; Using $r_{ij} = r_i - r_j$, $w_{ii} = 0$, and $l_{ij} = -w_{ij}$ ($i \ne j$):&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\sum_{j=0}^{N} w_{ij} r_{ij} &amp;#x26;= \sum_{j=0}^{N} w_{ij}(r_i - r_j) = r_i \sum_{j=0}^{N} w_{ij} - \sum_{j=0}^{N} w_{ij} r_j \
&amp;#x26;= l_{ii} r_i - \sum_{j=0, j \ne i}^{N} w_{ij} r_j = l_{ii} r_i + \sum_{j=0, j \ne i}^{N} l_{ij} r_j = \sum_{j=0}^{N} l_{ij} r_j
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} w_{ij} r_{ij} = \sum_{j=0}^{N} l_{ij} r_j \tag{16}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Substitute into the error equation.&lt;/strong&gt; Substitute $\xi_i = e_i + \xi_0 + r_i$ (for $i \ge 1$), along with (15) and (16), into (14). After expansion, note that the $BK \sum_{j=1}^N l_{ij} r_j$ term and the $BK \sum l_{ij} r_j$ term (the part with $j \ge 1$) cancel each other, and $r_0 = 0$ means $BK l_{i0} r_0 = 0$. The result is:&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A e_i - BK \sum&lt;/em&gt;{j=1}^{N} l_{ij} e_j + (A\xi_0 - \dot{\xi}_0) + (A r_i - \dot{r}_i) \tag{17}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constant offset case -- LMI condition unchanged.&lt;/strong&gt; If $r_i$ is a constant position offset (the most common case), i.e.:&lt;/p&gt;
&lt;p&gt;$$
r_i = \begin{bmatrix} p_{r_i} \ 0 \end{bmatrix}, \quad \dot{r}_i = 0, \quad A = \begin{bmatrix} 0 &amp;#x26; I \ 0 &amp;#x26; 0 \end{bmatrix} \Rightarrow A r_i = \begin{bmatrix} 0 \ 0 \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;The extra bias term $(A r_i - \dot{r}_i)$ vanishes, and the error dynamics reduce to the classic homogeneous form:&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A e_i - BK \sum&lt;/em&gt;{j=1}^{N} l_{ij} e_j + (A\xi_0 - \dot{\xi}_0) \tag{18}
$$&lt;/p&gt;
&lt;p&gt;Stacking all followers:&lt;/p&gt;
&lt;p&gt;$$
\dot{e} = \left(I_N \otimes A + \tilde{L} \otimes BK\right) e + \mathbf{1}_N \otimes (A\xi_0 - \dot{\xi}_0) \tag{19}
$$&lt;/p&gt;
&lt;p&gt;This is identical to equation (7). Therefore, &lt;strong&gt;when formation-internal relative positions (constant offsets) are present, the Lyapunov analysis and LMI condition (12) remain unchanged&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Simulation Implementation&lt;/h3&gt;
&lt;h4&gt;Solving the LMI Inequality&lt;/h4&gt;
&lt;p&gt;The above LMI condition is solved in MATLAB using the YALMIP toolbox. The chosen communication topology is: drone $0$ is the virtual leader, drone $1$ subscribes to drones $0$, $2$, and $3$ (relative formation position $(0, 0)$), drone $2$ subscribes to drone $1$ (relative formation position $(-5, -10)$), and drone $3$ subscribes to drone $1$ (relative formation position $(5, -10)$).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% ------------ Parameters ------------
N = 3;               % Number of followers
% System matrices A (6x6) and B (6x3)
A = [zeros(3) eye(3); zeros(3) zeros(3)];
B = [zeros(3); eye(3)];

% Construct L_sub and Ltilde
L_sub = [ 3 -1 -1;
         -1  1  0;
         -1  0  1 ];
Ltilde = -L_sub;

% ------------- YALMIP Variables -------------
n = size(A,1);      % =6
m = size(B,2);      % =3

X = sdpvar(n,n,&apos;symmetric&apos;);    % X ≻ 0
Y = sdpvar(m,n,&apos;full&apos;);         % Y = K*X (m x n)

% Construct large matrix S = I_N ⊗ (A X + X A&apos;) + Ltilde ⊗ (B Y) + Ltilde&apos; ⊗ (B Y)&apos;
S = kron(eye(N), A*X + X*A&apos;) + kron(Ltilde, B*Y) + kron(Ltilde&apos;, (B*Y)&apos;);

% LMI: S &amp;#x3C; 0, X &gt; 0
eps = 1e-6;
Constraints = [S &amp;#x3C;= -eps*eye(N*n), X &gt;= eps*eye(n)];
Constraints = [Constraints, X == X&apos;];

% ------------- Solve -------------
options = sdpsettings(&apos;verbose&apos;, 1, &apos;solver&apos;, &apos;sedumi&apos;);
Objective = [];
sol = optimize(Constraints, Objective, options);

if sol.problem == 0
    Xopt = value(X);
    Yopt = value(Y);
    K = Yopt * (Xopt \ eye(n));   % K = Y * X^{-1}
    disp(&apos;Found feasible K:&apos;);
    disp(K);
else
    disp(&apos;Solver failed; return information:&apos;);
    yalmiperror(sol.problem)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The solution for the formation positions used in this article (readers should solve according to their own models):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Found feasible K:
    0.6983    0.0000    0.0000    2.1929    0.0000    0.0000
   -0.0000    0.6983    0.0000   -0.0000    2.1929    0.0000
    0.0000   -0.0000    0.6983   -0.0000   -0.0000    2.1929
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hence $K_p = 0.6983$, $K_v = 2.1929$.&lt;/p&gt;
&lt;h4&gt;From Acceleration to Velocity Commands&lt;/h4&gt;
&lt;p&gt;PX4 employs a typical cascaded PID control structure: position controller → velocity controller → acceleration controller → attitude controller → angular rate controller → motors.&lt;/p&gt;
&lt;p&gt;If one were to directly use second-order acceleration input, gravity compensation and attitude resolution would be required to convert world-frame acceleration commands into body-frame attitude commands:&lt;/p&gt;
&lt;p&gt;$$
a_{body}^{des} = R_b^{w^{-1}} \left( a_{world}^{des} - g \right)
$$&lt;/p&gt;
&lt;p&gt;In Offboard mode, direct acceleration control is uncommon in engineering practice (for instance, $a_z = 0$ does not produce hover; a gravity compensation empirical value must be provided).&lt;/p&gt;
&lt;p&gt;Therefore, in this article the virtual acceleration command obtained from the linear consensus protocol is integrated to generate the desired velocity input:&lt;/p&gt;
&lt;p&gt;$$
a_i(k) = k_p \sum_{j \in \mathcal{N}&lt;em&gt;i} \big(p_j(k) - p_i(k) + r&lt;/em&gt;{ij}(k)\big) + k_v \sum_{j \in \mathcal{N}_i} \big(v_j(k) - v_i(k)\big) \tag{20}
$$&lt;/p&gt;
&lt;p&gt;$$
v_i^{des}(k+1) = v_i^{des}(k) + a_i(k) \Delta t \tag{21}
$$&lt;/p&gt;
&lt;p&gt;The result is then smoothed through a first-order filter:&lt;/p&gt;
&lt;p&gt;$$
v_i^{cmd}(k+1) = \alpha v_i^{des}(k+1) + (1 - \alpha) v_i^{cmd}(k) \tag{22}
$$&lt;/p&gt;
&lt;p&gt;where $\alpha \in (0, 1)$ is the filter factor, set to $0.5$ in this article.&lt;/p&gt;
&lt;p&gt;Code example for the $x$ and $y$ directions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float ax = position_gain_ * sum_dx + velocity_gain_ * sum_dvx;
float ay = position_gain_ * sum_dy + velocity_gain_ * sum_dvy;

float vx_integrated = desired_velocity_[0] + ax * dt;
float vy_integrated = desired_velocity_[1] + ay * dt;

desired_velocity_[0] = filter_alpha_ * vx_integrated
                     + (1.0f - filter_alpha_) * desired_velocity_[0];
desired_velocity_[1] = filter_alpha_ * vy_integrated
                     + (1.0f - filter_alpha_) * desired_velocity_[1];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach preserves the dynamic characteristics of second-order consensus control while being compatible with PX4&apos;s velocity control layer, offering good engineering feasibility and stability.&lt;/p&gt;
&lt;h4&gt;Filter Discretization Derivation&lt;/h4&gt;
&lt;p&gt;The continuous-time first-order low-pass filter equation is:&lt;/p&gt;
&lt;p&gt;$$
\tau \dot{v}_f(t) + v_f(t) = v(t)
$$&lt;/p&gt;
&lt;p&gt;where $v_f(t)$ is the filtered velocity signal, $v(t)$ is the input signal, and $\tau$ is the time constant. Rewriting:&lt;/p&gt;
&lt;p&gt;$$
\dot{v}_f(t) = \frac{1}{\tau}\big(v(t) - v_f(t)\big)
$$&lt;/p&gt;
&lt;p&gt;Under discrete conditions with sampling period $dt$:&lt;/p&gt;
&lt;p&gt;$$
\dot{v}_f(t) \approx \frac{v_f[k] - v_f[k-1]}{dt}
$$&lt;/p&gt;
&lt;p&gt;Substituting:&lt;/p&gt;
&lt;p&gt;$$
\frac{v_f[k] - v_f[k-1]}{dt} = \frac{1}{\tau}\big(v[k] - v_f[k-1]\big)
$$&lt;/p&gt;
&lt;p&gt;Multiplying both sides by $dt$ and rearranging:&lt;/p&gt;
&lt;p&gt;$$
v_f[k] - v_f[k-1] = \frac{dt}{\tau}\big(v[k] - v_f[k-1]\big)
$$&lt;/p&gt;
&lt;p&gt;Define $\alpha = \frac{dt}{\tau + dt}$, yielding:&lt;/p&gt;
&lt;p&gt;$$
v_f[k] = (1 - \alpha) v_f[k-1] + \alpha v[k] \tag{23}
$$&lt;/p&gt;
&lt;p&gt;This matches the form of equation (22).&lt;/p&gt;
&lt;h4&gt;ROS2 Coordinate Transformation Note&lt;/h4&gt;
&lt;p&gt;In a ROS2 + PX4 launch, each drone instance by default starts with its own takeoff point as the origin $(0, 0)$. Therefore, each time a position is received in a subscription callback, one must manually subtract the takeoff point coordinates to obtain the global coordinates before substituting into the control law. A code example is provided in the next section.&lt;/p&gt;
&lt;h4&gt;Reference Code&lt;/h4&gt;
&lt;p&gt;The following is the complete C++ code for UAV $1$ (which subscribes to the virtual leader as well as neighboring UAVs $2$ and $3$), corresponding to the model described above. The code for UAVs $2$ and $3$ is similar, requiring only modifications to the subscription topology and formation offsets.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;rclcpp/rclcpp.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/offboard_control_mode.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/trajectory_setpoint.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/vehicle_command.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/vehicle_odometry.hpp&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;map&gt;
#include &amp;#x3C;array&gt;
#include &amp;#x3C;limits&gt;
#include &amp;#x3C;chrono&gt;

struct NeighborState {
    std::array&amp;#x3C;float, 3&gt; position = {0.0f, 0.0f, 0.0f};
    std::array&amp;#x3C;float, 3&gt; velocity = {0.0f, 0.0f, 0.0f};
    bool valid = false;
};

class UAV1Controller : public rclcpp::Node {
public:
    UAV1Controller() : Node(&quot;uav1_controller&quot;) {
        rclcpp::QoS qos(10);
        qos.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT);
        qos.durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL);
        qos.history(RMW_QOS_POLICY_HISTORY_KEEP_LAST);

        offboard_control_mode_publisher_ = create_publisher&amp;#x3C;
            px4_msgs::msg::OffboardControlMode&gt;(
            &quot;/px4_1/fmu/in/offboard_control_mode&quot;, qos);
        trajectory_setpoint_publisher_ = create_publisher&amp;#x3C;
            px4_msgs::msg::TrajectorySetpoint&gt;(
            &quot;/px4_1/fmu/in/trajectory_setpoint&quot;, qos);
        vehicle_command_publisher_ = create_publisher&amp;#x3C;
            px4_msgs::msg::VehicleCommand&gt;(
            &quot;/px4_1/fmu/in/vehicle_command&quot;, qos);

        own_odometry_sub_ = create_subscription&amp;#x3C;
            px4_msgs::msg::VehicleOdometry&gt;(
            &quot;/px4_1/fmu/out/vehicle_odometry&quot;, qos,
            [this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
                own_position_ = {msg-&gt;position[0], msg-&gt;position[1],
                                 msg-&gt;position[2]};
                own_velocity_ = {msg-&gt;velocity[0], msg-&gt;velocity[1],
                                 msg-&gt;velocity[2]};
                own_state_valid_ = true;
            });

        uav2_sub_ = create_subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;(
            &quot;/px4_2/fmu/out/vehicle_odometry&quot;, qos,
            [this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
                neighbor_states_[2] = {
                    {msg-&gt;position[0] - 5.0f, msg-&gt;position[1] - 10.0f,
                     msg-&gt;position[2]},
                    {msg-&gt;velocity[0], msg-&gt;velocity[1], msg-&gt;velocity[2]},
                    true
                };
            });

        uav3_sub_ = create_subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;(
            &quot;/px4_3/fmu/out/vehicle_odometry&quot;, qos,
            [this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
                neighbor_states_[3] = {
                    {msg-&gt;position[0] + 5.0f, msg-&gt;position[1] - 10.0f,
                     msg-&gt;position[2]},
                    {msg-&gt;velocity[0], msg-&gt;velocity[1], msg-&gt;velocity[2]},
                    true
                };
            });

        position_gain_ = 0.6983f;
        velocity_gain_ = 2.1929f;
        filter_alpha_ = 0.5f;

        start_time_ = this-&gt;now();
        control_timer_ = create_wall_timer(std::chrono::milliseconds(20),
                                           [this]() { this-&gt;controlLoop(); });
        last_print_time_ = this-&gt;now();
        last_control_time_ = this-&gt;now();
        desired_velocity_ = {0.0f, 0.0f, 0.0f};
    }

private:
    NeighborState getVirtualLeaderState() {
        auto current_time = this-&gt;now();
        double t = (current_time - start_time_).seconds();

        NeighborState leader_state;
        leader_state.valid = true;
        leader_state.position[2] = 0.0f;
        leader_state.velocity[2] = 0.0f;

        if (t &amp;#x3C;= 10.0) {
            leader_state.velocity[0] = 3.0f;
            leader_state.position[0] = 3.0f * t;
            leader_state.velocity[1] = 0.3f * t;
            leader_state.position[1] = 0.5f * 0.3f * t * t;
        } else if (t &amp;#x3C;= 20.0) {
            leader_state.velocity[0] = 0.0f;
            leader_state.position[0] = 30.0f;
            leader_state.velocity[1] = 3.0f;
            leader_state.position[1] = 15.0f + 3.0f * (t - 10.0f);
        } else if (t &amp;#x3C;= 30.0) {
            float d = t - 20.0f;
            leader_state.velocity[0] = 0.0f;
            leader_state.position[0] = 30.0f;
            leader_state.velocity[1] = 3.0f - 0.3f * d;
            leader_state.position[1] = 45.0f + 3.0f * d
                                       - 0.5f * 0.3f * d * d;
        } else {
            leader_state.velocity[0] = 0.0f;
            leader_state.position[0] = 30.0f;
            leader_state.velocity[1] = 0.0f;
            leader_state.position[1] = 60.0f;
        }
        return leader_state;
    }

    void controlLoop() {
        NeighborState leader_state = getVirtualLeaderState();
        neighbor_states_[0] = leader_state;

        if (!own_state_valid_ || !neighbor_states_[2].valid
            || !neighbor_states_[3].valid) return;

        double dt = (this-&gt;now() - last_control_time_).seconds();
        last_control_time_ = this-&gt;now();

        px4_msgs::msg::OffboardControlMode offboard_msg;
        offboard_msg.position = false;
        offboard_msg.velocity = true;
        offboard_msg.acceleration = false;
        offboard_msg.timestamp = this-&gt;now().nanoseconds() / 1000;
        offboard_control_mode_publisher_-&gt;publish(offboard_msg);

        static int init_counter = 0;
        if (init_counter &amp;#x3C; 20) {
            px4_msgs::msg::TrajectorySetpoint setpoint{};
            setpoint.timestamp = get_clock()-&gt;now().nanoseconds() / 1000;
            setpoint.position = {std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN()};
            setpoint.velocity = {0.0f, 0.0f, 0.0f};
            setpoint.acceleration = {std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                    std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                    std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN()};
            trajectory_setpoint_publisher_-&gt;publish(setpoint);
            init_counter++;
            if (init_counter == 20) {
                px4_msgs::msg::VehicleCommand cmd;
                cmd.command = px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE;
                cmd.param1 = 1.0f; cmd.param2 = 6.0f;
                cmd.target_system = 2; cmd.target_component = 1;
                cmd.source_system = 1; cmd.source_component = 1;
                cmd.from_external = true;
                cmd.timestamp = this-&gt;now().nanoseconds() / 1000;
                vehicle_command_publisher_-&gt;publish(cmd);
            }
            return;
        }

        float sum_dx = (neighbor_states_[0].position[0] - own_position_[0])
                     + (neighbor_states_[2].position[0] - own_position_[0]
                        + 5.0f)
                     + (neighbor_states_[3].position[0] - own_position_[0]
                        - 5.0f);

        float sum_dy = (neighbor_states_[0].position[1] - own_position_[1])
                     + (neighbor_states_[2].position[1] - own_position_[1]
                        + 10.0f)
                     + (neighbor_states_[3].position[1] - own_position_[1]
                        + 10.0f);

        float sum_dvx = (neighbor_states_[0].velocity[0] - own_velocity_[0])
                      + (neighbor_states_[2].velocity[0] - own_velocity_[0])
                      + (neighbor_states_[3].velocity[0] - own_velocity_[0]);

        float sum_dvy = (neighbor_states_[0].velocity[1] - own_velocity_[1])
                      + (neighbor_states_[2].velocity[1] - own_velocity_[1])
                      + (neighbor_states_[3].velocity[1] - own_velocity_[1]);

        float ax = position_gain_ * sum_dx + velocity_gain_ * sum_dvx;
        float ay = position_gain_ * sum_dy + velocity_gain_ * sum_dvy;

        float vx_integrated = desired_velocity_[0] + ax * dt;
        float vy_integrated = desired_velocity_[1] + ay * dt;

        desired_velocity_[0] = filter_alpha_ * vx_integrated
                             + (1.0f - filter_alpha_) * desired_velocity_[0];
        desired_velocity_[1] = filter_alpha_ * vy_integrated
                             + (1.0f - filter_alpha_) * desired_velocity_[1];
        desired_velocity_[2] = 0.0f;

        px4_msgs::msg::TrajectorySetpoint setpoint;
        setpoint.timestamp = this-&gt;now().nanoseconds() / 1000;
        setpoint.velocity = {desired_velocity_[0], desired_velocity_[1], 0.0f};
        setpoint.position = {NAN, NAN, NAN};
        setpoint.acceleration = {NAN, NAN, NAN};
        trajectory_setpoint_publisher_-&gt;publish(setpoint);
    }

    // Publishers
    rclcpp::Publisher&amp;#x3C;px4_msgs::msg::OffboardControlMode&gt;::SharedPtr
        offboard_control_mode_publisher_;
    rclcpp::Publisher&amp;#x3C;px4_msgs::msg::TrajectorySetpoint&gt;::SharedPtr
        trajectory_setpoint_publisher_;
    rclcpp::Publisher&amp;#x3C;px4_msgs::msg::VehicleCommand&gt;::SharedPtr
        vehicle_command_publisher_;

    // Subscribers
    rclcpp::Subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;::SharedPtr
        own_odometry_sub_;
    rclcpp::Subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;::SharedPtr uav2_sub_;
    rclcpp::Subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;::SharedPtr uav3_sub_;

    rclcpp::TimerBase::SharedPtr control_timer_;

    std::array&amp;#x3C;float, 3&gt; own_position_ = {0.0f, 0.0f, 0.0f};
    std::array&amp;#x3C;float, 3&gt; own_velocity_ = {0.0f, 0.0f, 0.0f};
    std::array&amp;#x3C;float, 3&gt; desired_velocity_ = {0.0f, 0.0f, 0.0f};
    bool own_state_valid_ = false;

    std::map&amp;#x3C;int, NeighborState&gt; neighbor_states_;
    float position_gain_, velocity_gain_, filter_alpha_;
    rclcpp::Time last_print_time_, last_control_time_, start_time_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared&amp;#x3C;UAV1Controller&gt;();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Simulation Results&lt;/h3&gt;
&lt;p&gt;At startup, all three drones are manually commanded to take off via &lt;code&gt;commander takeoff&lt;/code&gt; and hover at $2.5\text{m}$ altitude. The consensus controller then takes over in the $xy$-plane, driving the formation to follow the virtual leader&apos;s trajectory.&lt;/p&gt;
&lt;p&gt;The overall formation tracking performance is satisfactory. There is some overshoot but overall stability is maintained, confirming the effectiveness of the second-order control law.&lt;/p&gt;
&lt;p&gt;Notably, the unique advantages of distributed communication enable further robustness testing: after the formation begins moving, killing UAV $1$&apos;s simulation (simulating a shoot-down scenario) leaves the remaining drones unaffected, continuing along their planned paths. This is precisely the core reliability benefit of distributed communication -- using a connected communication topology for data exchange, the impact of a single-point failure on the overall formation can be effectively mitigated in practical applications.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This article began with a first-order swarm system and progressively introduced second-order dynamics to build a distributed communication-based UAV formation control system. Starting from first-order position consensus, we derived the error dynamics and Lyapunov stability conditions for the second-order model, converted the stability conditions into a solvable LMI via congruence transformation (obtaining the feedback gain matrix through YALMIP in MATLAB), adapted acceleration-form commands to PX4-compatible velocity commands via integration and filtering, and provided a rigorous derivation for the case with constant formation offsets, verifying that the LMI condition remains invariant.&lt;/p&gt;
&lt;p&gt;Compared to centralized control, the distributed approach offers superior robustness and scalability -- advantages that are decisive in complex, dynamically changing real-world scenarios. In emerging fields such as low-altitude economy and swarm combat systems, this integrated communication-and-control architecture will play an increasingly important role.&lt;/p&gt;
&lt;p&gt;This article presents only an initial control scheme, and many areas for improvement were identified during simulation -- such as how to further suppress overshoot, how to adapt to non-connected (switching) communication topologies, and how to deploy the computation on real onboard computers. For deploying these algorithms on real hardware, see &lt;a href=&quot;/en/posts/px4-ros2-jetson-flash&quot;&gt;ROS2 Hardware: Flashing the Jetson&lt;/a&gt; and &lt;a href=&quot;/en/posts/px4-ros2-mavros&quot;&gt;Controlling PX4 with MAVROS2&lt;/a&gt; for the complete simulation-to-real workflow.&lt;/p&gt;
&lt;p&gt;Finally, thank you for your patient reading and constructive feedback!&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/32312200&quot;&gt;Multi-Agent System Control (1) -- Graph Theory - ZhenYue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_43391414/article/details/112277987&quot;&gt;Properties of the Laplacian Matrix - CSDN Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/hongliyu_lvliyu/article/details/106973870&quot;&gt;Consensus Control Notes 2.2 -- Laplacian-related Lemmas - CSDN Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>无人机分布式通信与二阶一致性协议</title><link>https://duduuu.xyz/zh/posts/px4-ros2-distributed-consensus</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-distributed-consensus</guid><description>基于ROS2分布式通信框架和二阶一致性算法，实现多无人机编队协同控制，含LMI求解与PX4速度层集成方案</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;本文基于 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 无人机编队仿真环境&lt;/a&gt; 和 &lt;a href=&quot;/zh/posts/px4-ros2-multi-offboard&quot;&gt;多机 Offboard 控制&lt;/a&gt; 的基础，介绍分布式通信在无人机集群协同控制系统中的应用。通过Gazebo仿真环境和ROS2的分布式通信框架，结合PX4飞控的MAVLink协议，实现多无人机间的实时状态共享与协同控制。系统采用一致性算法，使无人机仅依赖局部邻居节点的信息即可达成编队队形保持、沿轨迹飞行等目标，为大规模无人机集群的协同任务提供一种高可靠性的编队间通信方式。&lt;/p&gt;
&lt;p&gt;常见的无人机编队通信方式有集中式控制和分布式控制两种核心架构，主要区别在于决策层级、通信结构和系统鲁棒性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;集中式控制&lt;/strong&gt;依赖单一中央节点（如地面站或主无人机）进行全局决策，所有无人机根据中央节点指令执行统一动作。其优势在于控制精度高、队形一致性佳，适合复杂任务。然而，这种通信方式存在单点故障风险，且单一节点通信压力大，扩展性较差。尤其是当中央节点出现故障或受到攻击时，编队控制的安全性会受到极大影响。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;分布式控制&lt;/strong&gt;则无中心节点，各无人机通过局部通信自主决策，仅需与邻近无人机交互信息。这种方式具有强鲁棒性、高扩展性和动态适应性，适合大规模编队，但协调复杂度较高，全局优化能力较弱。&lt;/p&gt;
&lt;p&gt;本文基于六架无人机组成的编队，首先建立一阶集群协同控制律，然后拓展至更一般化的二阶模型（加速度输入），并通过LMI方法求解反馈增益矩阵，最终在Gazebo仿真中进行验证。文章末尾给出含常定编队偏置的严格误差动力学证明。&lt;/p&gt;
&lt;h2&gt;相关数学知识&lt;/h2&gt;
&lt;p&gt;有关分布式协同控制系统的数学背景，已有大量优秀的文章进行介绍，本文仅做必要的回顾。读者可参考以下资料深入理解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图论基础：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/32312200&quot;&gt;Multi-Agent System 控制 (1)—— 图论&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;拉普拉斯矩阵的性质：&lt;a href=&quot;https://blog.csdn.net/qq_43391414/article/details/112277987&quot;&gt;CSDN 博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;协同控制相关引理：&lt;a href=&quot;https://blog.csdn.net/hongliyu_lvliyu/article/details/106973870&quot;&gt;CSDN 博客&lt;/a&gt;，可以证明控制律的收敛性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从理论层面可以说明，本文所构造的集群协同系统能够实现无人机编队的协同任务。&lt;/p&gt;
&lt;h2&gt;一阶集群协同控制&lt;/h2&gt;
&lt;h3&gt;系统建模&lt;/h3&gt;
&lt;p&gt;考虑由 $N+1$ 架无人机组成的编队。编号为 $0$ 的无人机为领航者（leader），其余 $N$ 架编号为 $i=1,2,\dots,N$ 的无人机为跟随者（followers）。&lt;/p&gt;
&lt;p&gt;每架无人机采用一阶运动学模型，其状态仅包含位置信息。设第 $i$ 架无人机的位置为 $p_i \in \mathbb{R}^3$，控制输入为速度指令 $v_i \in \mathbb{R}^3$，则：&lt;/p&gt;
&lt;p&gt;$$
\dot{p}_i = v_i
$$&lt;/p&gt;
&lt;h3&gt;一阶一致性协议&lt;/h3&gt;
&lt;p&gt;选取经典线性一致性协议，第 $i$ 架无人机的控制输入由其邻居状态偏差的加权和决定：&lt;/p&gt;
&lt;p&gt;$$
v_i = k_p \sum_{j \in \mathcal{N}&lt;em&gt;i} w&lt;/em&gt;{ij}(p_j - p_i)
$$&lt;/p&gt;
&lt;p&gt;其中 $\mathcal{N}&lt;em&gt;i$ 为第 $i$ 架无人机的邻居集合，$w&lt;/em&gt;{ij} \ge 0$ 为通信权重，$k_p &gt; 0$ 为位置增益。&lt;/p&gt;
&lt;p&gt;可以证明，对于连通图，各无人机的位置状态满足 $p_i(t) - p_j(t) \to 0$（$t \to \infty$），即所有跟随者渐近收敛到领航者的位置。这正是分布式一致性算法在集群编队中最基础的应用形式——仅依赖邻居位置偏差，即可实现全局位置同步。&lt;/p&gt;
&lt;h2&gt;二阶分布式控制集群系统&lt;/h2&gt;
&lt;h3&gt;理论建模&lt;/h3&gt;
&lt;h4&gt;单无人机系统建模&lt;/h4&gt;
&lt;p&gt;我们考虑由 $N+1$ 架无人机组成的编队，$0$ 号无人机作为领航者（leader），选取为虚拟领航者（不实际参与飞行）。编号 $i = 1, 2, \dots, N$ 的无人机为跟随者（followers）。&lt;/p&gt;
&lt;p&gt;每架无人机采用二阶动力学模型（包含位置与速度状态）。记第 $i$ 架无人机的状态为：&lt;/p&gt;
&lt;p&gt;$$
\xi_i = \begin{bmatrix} p_i \ v_i \end{bmatrix} \in \mathbb{R}^6, \quad p_i \in \mathbb{R}^3, \quad v_i \in \mathbb{R}^3
$$&lt;/p&gt;
&lt;p&gt;其中 $p_i$ 为位置，$v_i$ 为速度。控制输入为二阶加速度指令 $u_i \in \mathbb{R}^3$。&lt;/p&gt;
&lt;p&gt;每架无人机的二阶线性化模型可以写为矩阵形式：&lt;/p&gt;
&lt;p&gt;$$
\dot{\xi}_i = A \xi_i + B u_i \tag{1}
$$&lt;/p&gt;
&lt;p&gt;矩阵 $A \in \mathbb{R}^{6 \times 6}$ 与 $B \in \mathbb{R}^{6 \times 3}$ 为块矩阵，简单推导即可得：&lt;/p&gt;
&lt;p&gt;$$
\dot{\xi}&lt;em&gt;i = \begin{bmatrix} \dot{p}&lt;em&gt;i \ \dot{v}&lt;em&gt;i \end{bmatrix} = \begin{bmatrix} v_i \ u_i \end{bmatrix} = \begin{bmatrix} 0&lt;/em&gt;{3\times3} &amp;#x26; I_3 \ 0&lt;/em&gt;{3\times3} &amp;#x26; 0&lt;/em&gt;{3\times3} \end{bmatrix} \begin{bmatrix} p_i \ v_i \end{bmatrix} + \begin{bmatrix} 0_{3\times3} \ I_3 \end{bmatrix} u_i
$$&lt;/p&gt;
&lt;p&gt;$$
\Rightarrow A = \begin{bmatrix} 0_{3\times3} &amp;#x26; I_3 \ 0_{3\times3} &amp;#x26; 0_{3\times3} \end{bmatrix}, \quad B = \begin{bmatrix} 0_{3\times3} \ I_3 \end{bmatrix} \tag{2}
$$&lt;/p&gt;
&lt;h4&gt;线性一致性协议&lt;/h4&gt;
&lt;p&gt;选取经典线性一致性协议，每架跟随者无人机（$i=1,2,\dots,N$）的输入可以表示为邻居状态偏差的加权和：&lt;/p&gt;
&lt;p&gt;$$
u_i = K \sum_{j=0}^{N} w_{ij}(\xi_j - \xi_i) \tag{3}
$$&lt;/p&gt;
&lt;p&gt;其中 $K$ 为状态反馈增益矩阵。&lt;/p&gt;
&lt;p&gt;将其写为拉普拉斯矩阵形式。定义权重矩阵 $W = (w_{ij})$，度矩阵 $D = \text{diag}(d_0, \dots, d_N)$ 其中 $d_i = \sum_j w_{ij}$，拉普拉斯矩阵 $L = D - W$，元素满足 $l_{ii} = \sum_{j=0}^N w_{ij}$，$l_{ij} = -w_{ij}$（$i \ne j$），$w_{ii}=0$。&lt;/p&gt;
&lt;p&gt;将式（3）展开：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
u_i &amp;#x26;= K \sum_{j=0}^{N} w_{ij}(\xi_j - \xi_i) \
&amp;#x26;= K\left( \sum_{j=0}^{N} w_{ij} \xi_j - \left(\sum_{j=0}^{N} w_{ij}\right)\xi_i \right) \
&amp;#x26;= K\left( \sum_{j=0, j \ne i}^{N} w_{ij} \xi_j + w_{ii}\xi_i - \left(\sum_{j=0}^{N} w_{ij}\right)\xi_i \right) \
&amp;#x26;= K\left( -\sum_{j=0, j \ne i}^{N} l_{ij} \xi_j + 0 - l_{ii}\xi_i \right) \
&amp;#x26;= -K \sum_{j=0}^{N} l_{ij} \xi_j
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;p&gt;$$
u_i = -K \sum_{j=0}^{N} l_{ij} \xi_j, \quad i = 1, 2, \dots, N \tag{4}
$$&lt;/p&gt;
&lt;h4&gt;误差系统定义&lt;/h4&gt;
&lt;p&gt;定义每架跟随者相对于领航者（编号 $0$）的误差：&lt;/p&gt;
&lt;p&gt;$$
e_i = \xi_i - \xi_0, \quad i = 1, \dots, N \tag{5}
$$&lt;/p&gt;
&lt;p&gt;由单机动力学（式（1））可得：&lt;/p&gt;
&lt;p&gt;$$
\dot{e}_i = \dot{\xi}_i - \dot{\xi}_0 = A\xi_i + Bu_i - \dot{\xi}_0
$$&lt;/p&gt;
&lt;p&gt;代入控制律（式（4））：&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A\xi_i - BK\sum&lt;/em&gt;{j=0}^{N} l_{ij} \xi_j - \dot{\xi}_0
$$&lt;/p&gt;
&lt;p&gt;对所有 $j \ge 1$ 有 $\xi_j = e_j + \xi_0$，拆分为 $j=0$ 和 $j \ge 1$ 两项：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} l_{ij} \xi_j = \sum_{j=1}^{N} l_{ij} e_j + \left(l_{i0} + \sum_{j=1}^{N} l_{ij}\right)\xi_0
$$&lt;/p&gt;
&lt;p&gt;利用拉普拉斯矩阵的行和为零 $\sum_{j=0}^N l_{ij} = 0$，因此 $l_{i0} + \sum_{j=1}^N l_{ij} = 0$，故：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} l_{ij} \xi_j = \sum_{j=1}^{N} l_{ij} e_j
$$&lt;/p&gt;
&lt;p&gt;代回得单个跟随者 $i$ 的误差动力学方程：&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A e_i - BK \sum&lt;/em&gt;{j=1}^{N} l_{ij} e_j + (A\xi_0 - \dot{\xi}_0) \tag{6}
$$&lt;/p&gt;
&lt;p&gt;堆叠所有 $N$ 个跟随者：&lt;/p&gt;
&lt;p&gt;$$
e = \begin{bmatrix} e_1 \ \vdots \ e_N \end{bmatrix} \in \mathbb{R}^{6N}, \quad d = \mathbf{1}&lt;em&gt;N \otimes \left(A\xi_0 - \dot{\xi}&lt;em&gt;0\right) \in \mathbb{R}^{6N}, \quad \tilde{L} = [-l&lt;/em&gt;{ij}]&lt;/em&gt;{i,j=1}^N
$$&lt;/p&gt;
&lt;p&gt;将误差动力学方程写成矩阵形式：&lt;/p&gt;
&lt;p&gt;$$
\dot{e} = \left(I_N \otimes A + \tilde{L} \otimes BK\right) e + d \tag{7}
$$&lt;/p&gt;
&lt;h4&gt;Lyapunov 函数与 LMI 条件&lt;/h4&gt;
&lt;p&gt;选取 Lyapunov 函数 $V(e) = e^\top P e$，其中 $P = I_N \otimes X^{-1}$，$X \succ 0$ 为对称正定阵：&lt;/p&gt;
&lt;p&gt;$$
V(e) = e^\top \left(I_N \otimes X^{-1}\right) e \tag{8}
$$&lt;/p&gt;
&lt;p&gt;误差动力学方程的闭环系统（齐次部分）为：&lt;/p&gt;
&lt;p&gt;$$
\dot{e} = A_{cl} e, \quad A_{cl} = I_N \otimes A + \tilde{L} \otimes BK \tag{9}
$$&lt;/p&gt;
&lt;p&gt;计算 Lyapunov 导数：&lt;/p&gt;
&lt;p&gt;$$
\dot{V} = e^\top \left( A_{cl}^\top P + P A_{cl} \right) e
$$&lt;/p&gt;
&lt;p&gt;为保证渐近稳定性，需要 $\dot{V} \prec 0$ ，即：&lt;/p&gt;
&lt;p&gt;$$
A_{cl}^\top P + P A_{cl} \prec 0 \tag{10}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;同余变换说明：&lt;/strong&gt; 设 $S \in \mathbb{R}^{n \times n}$ 为对称矩阵，$M \in \mathbb{R}^{n \times n}$ 为可逆矩阵，定义 $\bar{S} = M S M^\top$，则满足 $S \succ 0 \iff \bar{S} \succ 0$ 和 $S \prec 0 \iff \bar{S} \prec 0$。对任意非零向量 $z$，$z^\top \bar{S} z = (M^\top z)^\top S (M^\top z)$，因 $M$ 可逆二者符号完全相同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;运用同余变换去掉 $X^{-1}$：&lt;/p&gt;
&lt;p&gt;$$
\bar{S} = (I_N \otimes X) \left( P A_{cl} + A_{cl}^\top P \right) (I_N \otimes X)
$$&lt;/p&gt;
&lt;p&gt;则 $P A_{cl} + A_{cl}^\top P \prec 0 \iff \bar{S} \prec 0$。展开 $\bar{S}$：&lt;/p&gt;
&lt;p&gt;$$
\bar{S} = I_N \otimes (AX + XA^\top) + \tilde{L} \otimes (BKX) + \tilde{L}^\top \otimes \big(X(BK)^\top\big) \tag{11}
$$&lt;/p&gt;
&lt;p&gt;使用变量替换 $Y = KX$ 消除双线性耦合：&lt;/p&gt;
&lt;p&gt;$$
BKX = BY, \quad X(BK)^\top = (BY)^\top
$$&lt;/p&gt;
&lt;p&gt;于是 LMI 条件为：&lt;/p&gt;
&lt;p&gt;$$
I_N \otimes (AX + XA^\top) + \tilde{L} \otimes (BY) + \tilde{L}^\top \otimes (BY)^\top \prec 0, \quad X \succ 0 \tag{12}
$$&lt;/p&gt;
&lt;h4&gt;包含编队位置的控制律&lt;/h4&gt;
&lt;p&gt;假设编队内存在期望相对位置，定义 $r_{ij}$ 为跟随者 $i$ 与 $j$ 之间的期望相对偏差。控制律修改为：&lt;/p&gt;
&lt;p&gt;$$
u_i = K \sum_{j=0}^{N} w_{ij} \big[(\xi_j - \xi_i) + r_{ij}\big] = -K \sum_{j=0}^{N} l_{ij} \xi_j + K \sum_{j=0}^{N} w_{ij} r_{ij} \tag{13}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重新定义误差项。&lt;/strong&gt; 设 $r_{ij} = r_i - r_j$（$r_0 = 0$），$r_i$ 为跟随者 $i$ 相对于领航者的期望编队位置偏置。定义新的误差为：&lt;/p&gt;
&lt;p&gt;$$
e_i = \xi_i - (\xi_0 + r_i), \quad i = 1, \dots, N
$$&lt;/p&gt;
&lt;p&gt;对误差求导，代入单机动力学（1）和控制律（13）：&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A\xi_i - BK \sum&lt;/em&gt;{j=0}^{N} l_{ij} \xi_j + BK \sum_{j=0}^{N} w_{ij} r_{ij} - \dot{\xi}_0 - \dot{r}_i \tag{14}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;化简和式 1（拉普拉斯项）。&lt;/strong&gt; 对 $j \ge 1$，代入 $\xi_j = e_j + \xi_0 + r_j$，$\xi_0 = \xi_0 + r_0$：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\sum_{j=0}^{N} l_{ij} \xi_j &amp;#x26;= l_{i0} \xi_0 + \sum_{j=1}^{N} l_{ij} (e_j + \xi_0 + r_j) \
&amp;#x26;= \sum_{j=1}^{N} l_{ij} e_j + \Big(l_{i0} + \sum_{j=1}^{N} l_{ij}\Big) \xi_0 + \sum_{j=1}^{N} l_{ij} r_j
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由拉普拉斯矩阵行和为零（$\sum_{j=0}^N l_{ij} = 0$）得 $l_{i0} + \sum_{j=1}^N l_{ij} = 0$，从而：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} l_{ij} \xi_j = \sum_{j=1}^{N} l_{ij} e_j + \sum_{j=1}^{N} l_{ij} r_j \tag{15}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;化简和式 2（编队加权项）。&lt;/strong&gt; 利用 $r_{ij} = r_i - r_j$ 及 $w_{ii} = 0$、$l_{ij} = -w_{ij}$（$i \ne j$）：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\sum_{j=0}^{N} w_{ij} r_{ij} &amp;#x26;= \sum_{j=0}^{N} w_{ij}(r_i - r_j) = r_i \sum_{j=0}^{N} w_{ij} - \sum_{j=0}^{N} w_{ij} r_j \
&amp;#x26;= l_{ii} r_i - \sum_{j=0, j \ne i}^{N} w_{ij} r_j = l_{ii} r_i + \sum_{j=0, j \ne i}^{N} l_{ij} r_j = \sum_{j=0}^{N} l_{ij} r_j
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;p&gt;$$
\sum_{j=0}^{N} w_{ij} r_{ij} = \sum_{j=0}^{N} l_{ij} r_j \tag{16}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代入误差方程。&lt;/strong&gt; 将 $\xi_i = e_i + \xi_0 + r_i$（$i \ge 1$）、（15）、（16）代入（14），展开后注意 $BK \sum_{j=1}^N l_{ij} r_j$ 项与 $BK \sum l_{ij} r_j$ 中含 $j \ge 1$ 的部分相互抵消，且 $r_0 = 0$ 使 $BK l_{i0} r_0 = 0$，得：&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A e_i - BK \sum&lt;/em&gt;{j=1}^{N} l_{ij} e_j + (A\xi_0 - \dot{\xi}_0) + (A r_i - \dot{r}_i) \tag{17}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;常定偏置情形——LMI 条件不变。&lt;/strong&gt; 若 $r_i$ 为常定位置偏置（最常见情况），即：&lt;/p&gt;
&lt;p&gt;$$
r_i = \begin{bmatrix} p_{r_i} \ 0 \end{bmatrix}, \quad \dot{r}_i = 0, \quad A = \begin{bmatrix} 0 &amp;#x26; I \ 0 &amp;#x26; 0 \end{bmatrix} \Rightarrow A r_i = \begin{bmatrix} 0 \ 0 \end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;额外的偏置项 $(A r_i - \dot{r}_i)$ 消失，误差动力学化归为经典的齐次形式：&lt;/p&gt;
&lt;p&gt;$$
\dot{e}&lt;em&gt;i = A e_i - BK \sum&lt;/em&gt;{j=1}^{N} l_{ij} e_j + (A\xi_0 - \dot{\xi}_0) \tag{18}
$$&lt;/p&gt;
&lt;p&gt;堆叠所有跟随者：&lt;/p&gt;
&lt;p&gt;$$
\dot{e} = \left(I_N \otimes A + \tilde{L} \otimes BK\right) e + \mathbf{1}_N \otimes (A\xi_0 - \dot{\xi}_0) \tag{19}
$$&lt;/p&gt;
&lt;p&gt;与式（7）完全一致。因此 &lt;strong&gt;存在编队内部相对位置（常定偏置）时，Lyapunov 分析与 LMI 条件（12）保持不变&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;仿真实现&lt;/h3&gt;
&lt;h4&gt;求解 LMI 不等式&lt;/h4&gt;
&lt;p&gt;将上述 LMI 条件在 MATLAB 的 YALMIP 工具中求解。选取的拓扑结构为：$0$ 号无人机为虚拟领航者，$1$ 号无人机订阅 $0$、$2$、$3$ 号无人机（相对编队位置 $(0, 0)$），$2$ 号无人机订阅 $1$ 号（相对编队位置 $(-5, -10)$），$3$ 号无人机订阅 $1$ 号（相对编队位置 $(5, -10)$）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-matlab&quot;&gt;% ------------ 设置参数 ------------
N = 3;               % follower 数量
% 系统矩阵 A (6x6) 和 B (6x3)
A = [zeros(3) eye(3); zeros(3) zeros(3)];
B = [zeros(3); eye(3)];

% 构造 L_sub 和 Ltilde
L_sub = [ 3 -1 -1;
         -1  1  0;
         -1  0  1 ];
Ltilde = -L_sub;

% ------------- YALMIP 变量 -------------
n = size(A,1);      % =6
m = size(B,2);      % =3

X = sdpvar(n,n,&apos;symmetric&apos;);    % X ≻ 0
Y = sdpvar(m,n,&apos;full&apos;);         % Y = K*X (m x n)

% 构造大矩阵 S = I_N ⊗ (A X + X A&apos;) + Ltilde ⊗ (B Y) + Ltilde&apos; ⊗ (B Y)&apos;
S = kron(eye(N), A*X + X*A&apos;) + kron(Ltilde, B*Y) + kron(Ltilde&apos;, (B*Y)&apos;);

% LMI: S &amp;#x3C; 0, X &gt; 0
eps = 1e-6;
Constraints = [S &amp;#x3C;= -eps*eye(N*n), X &gt;= eps*eye(n)];
Constraints = [Constraints, X == X&apos;];

% ------------- 求解 -------------
options = sdpsettings(&apos;verbose&apos;, 1, &apos;solver&apos;, &apos;sedumi&apos;);
Objective = [];
sol = optimize(Constraints, Objective, options);

if sol.problem == 0
    Xopt = value(X);
    Yopt = value(Y);
    K = Yopt * (Xopt \ eye(n));   % K = Y * X^{-1}
    disp(&apos;Found feasible K:&apos;);
    disp(K);
else
    disp(&apos;求解失败；返回信息：&apos;);
    yalmiperror(sol.problem)
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本文的编队位置求解结果（读者需根据自己建模求解）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Found feasible K:
    0.6983    0.0000    0.0000    2.1929    0.0000    0.0000
   -0.0000    0.6983    0.0000   -0.0000    2.1929    0.0000
    0.0000   -0.0000    0.6983   -0.0000   -0.0000    2.1929
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此 $K_p = 0.6983$，$K_v = 2.1929$。&lt;/p&gt;
&lt;h4&gt;加速度积分&lt;/h4&gt;
&lt;p&gt;PX4 采用典型的级联 PID 控制结构：位置控制器 → 速度控制器 → 加速度控制器 → 姿态控制器 → 角速率控制器 → 电机。&lt;/p&gt;
&lt;p&gt;若直接使用二阶加速度输入，需通过重力补偿和姿态解算将世界坐标系下的加速度指令转换为机体姿态指令：&lt;/p&gt;
&lt;p&gt;$$
a_{body}^{des} = R_b^{w^{-1}} \left( a_{world}^{des} - g \right)
$$&lt;/p&gt;
&lt;p&gt;在 Offboard 模式下直接采用加速度控制在工程实践中并不多（例如 $a_z = 0$ 并不能直接悬停，需给予重力补偿的经验值）。&lt;/p&gt;
&lt;p&gt;因此，本文将线性一致性协议得到的虚拟加速度指令通过积分变换生成期望的速度输入：&lt;/p&gt;
&lt;p&gt;$$
a_i(k) = k_p \sum_{j \in \mathcal{N}&lt;em&gt;i} \big(p_j(k) - p_i(k) + r&lt;/em&gt;{ij}(k)\big) + k_v \sum_{j \in \mathcal{N}_i} \big(v_j(k) - v_i(k)\big) \tag{20}
$$&lt;/p&gt;
&lt;p&gt;$$
v_i^{des}(k+1) = v_i^{des}(k) + a_i(k) \Delta t \tag{21}
$$&lt;/p&gt;
&lt;p&gt;再通过一阶滤波器进行平滑处理：&lt;/p&gt;
&lt;p&gt;$$
v_i^{cmd}(k+1) = \alpha v_i^{des}(k+1) + (1 - \alpha) v_i^{cmd}(k) \tag{22}
$$&lt;/p&gt;
&lt;p&gt;其中 $\alpha \in (0, 1)$ 为滤波因子，本文取 $0.5$。&lt;/p&gt;
&lt;p&gt;在 $x$、$y$ 方向的代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;float ax = position_gain_ * sum_dx + velocity_gain_ * sum_dvx;
float ay = position_gain_ * sum_dy + velocity_gain_ * sum_dvy;

float vx_integrated = desired_velocity_[0] + ax * dt;
float vy_integrated = desired_velocity_[1] + ay * dt;

desired_velocity_[0] = filter_alpha_ * vx_integrated
                     + (1.0f - filter_alpha_) * desired_velocity_[0];
desired_velocity_[1] = filter_alpha_ * vy_integrated
                     + (1.0f - filter_alpha_) * desired_velocity_[1];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法保留了二阶一致性控制的动力学特性，同时兼容 PX4 的速度控制层，具有良好的工程可实现性与稳定性。&lt;/p&gt;
&lt;h4&gt;离散化滤波器推导&lt;/h4&gt;
&lt;p&gt;连续时间的一阶低通滤波器方程为：&lt;/p&gt;
&lt;p&gt;$$
\tau \dot{v}_f(t) + v_f(t) = v(t)
$$&lt;/p&gt;
&lt;p&gt;其中 $v_f(t)$ 为滤波后的速度信号，$v(t)$ 为输入信号，$\tau$ 为时间常数。改写为：&lt;/p&gt;
&lt;p&gt;$$
\dot{v}_f(t) = \frac{1}{\tau}\big(v(t) - v_f(t)\big)
$$&lt;/p&gt;
&lt;p&gt;在离散条件下，采样周期为 $dt$：&lt;/p&gt;
&lt;p&gt;$$
\dot{v}_f(t) \approx \frac{v_f[k] - v_f[k-1]}{dt}
$$&lt;/p&gt;
&lt;p&gt;代入得：&lt;/p&gt;
&lt;p&gt;$$
\frac{v_f[k] - v_f[k-1]}{dt} = \frac{1}{\tau}\big(v[k] - v_f[k-1]\big)
$$&lt;/p&gt;
&lt;p&gt;两边乘以 $dt$ 并整理：&lt;/p&gt;
&lt;p&gt;$$
v_f[k] - v_f[k-1] = \frac{dt}{\tau}\big(v[k] - v_f[k-1]\big)
$$&lt;/p&gt;
&lt;p&gt;定义 $\alpha = \frac{dt}{\tau + dt}$，得：&lt;/p&gt;
&lt;p&gt;$$
v_f[k] = (1 - \alpha) v_f[k-1] + \alpha v[k] \tag{23}
$$&lt;/p&gt;
&lt;p&gt;与式（22）形式一致。&lt;/p&gt;
&lt;h4&gt;ROS2 中的坐标变换注意&lt;/h4&gt;
&lt;p&gt;在 ROS2 + PX4 启动中，每架无人机实例启动时默认以自身启动点坐标为原点 $(0, 0)$。因此，在每次回调订阅位置时，需人为将坐标减去启动点的坐标，获得全局坐标后再代入控制律。代码示例见下节。&lt;/p&gt;
&lt;h4&gt;参考代码&lt;/h4&gt;
&lt;p&gt;以下给出上文建模中 $1$ 号无人机（同时订阅虚拟领航者及 $2$、$3$ 号邻居无人机）的完整 C++ 代码。$2$、$3$ 号无人机的代码类似，仅需修改订阅拓扑和编队偏置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;rclcpp/rclcpp.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/offboard_control_mode.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/trajectory_setpoint.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/vehicle_command.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/vehicle_odometry.hpp&gt;
#include &amp;#x3C;vector&gt;
#include &amp;#x3C;map&gt;
#include &amp;#x3C;array&gt;
#include &amp;#x3C;limits&gt;
#include &amp;#x3C;chrono&gt;

struct NeighborState {
    std::array&amp;#x3C;float, 3&gt; position = {0.0f, 0.0f, 0.0f};
    std::array&amp;#x3C;float, 3&gt; velocity = {0.0f, 0.0f, 0.0f};
    bool valid = false;
};

class UAV1Controller : public rclcpp::Node {
public:
    UAV1Controller() : Node(&quot;uav1_controller&quot;) {
        rclcpp::QoS qos(10);
        qos.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT);
        qos.durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL);
        qos.history(RMW_QOS_POLICY_HISTORY_KEEP_LAST);

        offboard_control_mode_publisher_ = create_publisher&amp;#x3C;
            px4_msgs::msg::OffboardControlMode&gt;(
            &quot;/px4_1/fmu/in/offboard_control_mode&quot;, qos);
        trajectory_setpoint_publisher_ = create_publisher&amp;#x3C;
            px4_msgs::msg::TrajectorySetpoint&gt;(
            &quot;/px4_1/fmu/in/trajectory_setpoint&quot;, qos);
        vehicle_command_publisher_ = create_publisher&amp;#x3C;
            px4_msgs::msg::VehicleCommand&gt;(
            &quot;/px4_1/fmu/in/vehicle_command&quot;, qos);

        own_odometry_sub_ = create_subscription&amp;#x3C;
            px4_msgs::msg::VehicleOdometry&gt;(
            &quot;/px4_1/fmu/out/vehicle_odometry&quot;, qos,
            [this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
                own_position_ = {msg-&gt;position[0], msg-&gt;position[1],
                                 msg-&gt;position[2]};
                own_velocity_ = {msg-&gt;velocity[0], msg-&gt;velocity[1],
                                 msg-&gt;velocity[2]};
                own_state_valid_ = true;
            });

        uav2_sub_ = create_subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;(
            &quot;/px4_2/fmu/out/vehicle_odometry&quot;, qos,
            [this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
                neighbor_states_[2] = {
                    {msg-&gt;position[0] - 5.0f, msg-&gt;position[1] - 10.0f,
                     msg-&gt;position[2]},
                    {msg-&gt;velocity[0], msg-&gt;velocity[1], msg-&gt;velocity[2]},
                    true
                };
            });

        uav3_sub_ = create_subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;(
            &quot;/px4_3/fmu/out/vehicle_odometry&quot;, qos,
            [this](const px4_msgs::msg::VehicleOdometry::SharedPtr msg) {
                neighbor_states_[3] = {
                    {msg-&gt;position[0] + 5.0f, msg-&gt;position[1] - 10.0f,
                     msg-&gt;position[2]},
                    {msg-&gt;velocity[0], msg-&gt;velocity[1], msg-&gt;velocity[2]},
                    true
                };
            });

        position_gain_ = 0.6983f;
        velocity_gain_ = 2.1929f;
        filter_alpha_ = 0.5f;

        start_time_ = this-&gt;now();
        control_timer_ = create_wall_timer(std::chrono::milliseconds(20),
                                           [this]() { this-&gt;controlLoop(); });
        last_print_time_ = this-&gt;now();
        last_control_time_ = this-&gt;now();
        desired_velocity_ = {0.0f, 0.0f, 0.0f};
    }

private:
    NeighborState getVirtualLeaderState() {
        auto current_time = this-&gt;now();
        double t = (current_time - start_time_).seconds();

        NeighborState leader_state;
        leader_state.valid = true;
        leader_state.position[2] = 0.0f;
        leader_state.velocity[2] = 0.0f;

        if (t &amp;#x3C;= 10.0) {
            leader_state.velocity[0] = 3.0f;
            leader_state.position[0] = 3.0f * t;
            leader_state.velocity[1] = 0.3f * t;
            leader_state.position[1] = 0.5f * 0.3f * t * t;
        } else if (t &amp;#x3C;= 20.0) {
            leader_state.velocity[0] = 0.0f;
            leader_state.position[0] = 30.0f;
            leader_state.velocity[1] = 3.0f;
            leader_state.position[1] = 15.0f + 3.0f * (t - 10.0f);
        } else if (t &amp;#x3C;= 30.0) {
            float d = t - 20.0f;
            leader_state.velocity[0] = 0.0f;
            leader_state.position[0] = 30.0f;
            leader_state.velocity[1] = 3.0f - 0.3f * d;
            leader_state.position[1] = 45.0f + 3.0f * d
                                       - 0.5f * 0.3f * d * d;
        } else {
            leader_state.velocity[0] = 0.0f;
            leader_state.position[0] = 30.0f;
            leader_state.velocity[1] = 0.0f;
            leader_state.position[1] = 60.0f;
        }
        return leader_state;
    }

    void controlLoop() {
        NeighborState leader_state = getVirtualLeaderState();
        neighbor_states_[0] = leader_state;

        if (!own_state_valid_ || !neighbor_states_[2].valid
            || !neighbor_states_[3].valid) return;

        double dt = (this-&gt;now() - last_control_time_).seconds();
        last_control_time_ = this-&gt;now();

        px4_msgs::msg::OffboardControlMode offboard_msg;
        offboard_msg.position = false;
        offboard_msg.velocity = true;
        offboard_msg.acceleration = false;
        offboard_msg.timestamp = this-&gt;now().nanoseconds() / 1000;
        offboard_control_mode_publisher_-&gt;publish(offboard_msg);

        static int init_counter = 0;
        if (init_counter &amp;#x3C; 20) {
            px4_msgs::msg::TrajectorySetpoint setpoint{};
            setpoint.timestamp = get_clock()-&gt;now().nanoseconds() / 1000;
            setpoint.position = {std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN()};
            setpoint.velocity = {0.0f, 0.0f, 0.0f};
            setpoint.acceleration = {std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                    std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN(),
                                    std::numeric_limits&amp;#x3C;float&gt;::quiet_NaN()};
            trajectory_setpoint_publisher_-&gt;publish(setpoint);
            init_counter++;
            if (init_counter == 20) {
                px4_msgs::msg::VehicleCommand cmd;
                cmd.command = px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE;
                cmd.param1 = 1.0f; cmd.param2 = 6.0f;
                cmd.target_system = 2; cmd.target_component = 1;
                cmd.source_system = 1; cmd.source_component = 1;
                cmd.from_external = true;
                cmd.timestamp = this-&gt;now().nanoseconds() / 1000;
                vehicle_command_publisher_-&gt;publish(cmd);
            }
            return;
        }

        float sum_dx = (neighbor_states_[0].position[0] - own_position_[0])
                     + (neighbor_states_[2].position[0] - own_position_[0]
                        + 5.0f)
                     + (neighbor_states_[3].position[0] - own_position_[0]
                        - 5.0f);

        float sum_dy = (neighbor_states_[0].position[1] - own_position_[1])
                     + (neighbor_states_[2].position[1] - own_position_[1]
                        + 10.0f)
                     + (neighbor_states_[3].position[1] - own_position_[1]
                        + 10.0f);

        float sum_dvx = (neighbor_states_[0].velocity[0] - own_velocity_[0])
                      + (neighbor_states_[2].velocity[0] - own_velocity_[0])
                      + (neighbor_states_[3].velocity[0] - own_velocity_[0]);

        float sum_dvy = (neighbor_states_[0].velocity[1] - own_velocity_[1])
                      + (neighbor_states_[2].velocity[1] - own_velocity_[1])
                      + (neighbor_states_[3].velocity[1] - own_velocity_[1]);

        float ax = position_gain_ * sum_dx + velocity_gain_ * sum_dvx;
        float ay = position_gain_ * sum_dy + velocity_gain_ * sum_dvy;

        float vx_integrated = desired_velocity_[0] + ax * dt;
        float vy_integrated = desired_velocity_[1] + ay * dt;

        desired_velocity_[0] = filter_alpha_ * vx_integrated
                             + (1.0f - filter_alpha_) * desired_velocity_[0];
        desired_velocity_[1] = filter_alpha_ * vy_integrated
                             + (1.0f - filter_alpha_) * desired_velocity_[1];
        desired_velocity_[2] = 0.0f;

        px4_msgs::msg::TrajectorySetpoint setpoint;
        setpoint.timestamp = this-&gt;now().nanoseconds() / 1000;
        setpoint.velocity = {desired_velocity_[0], desired_velocity_[1], 0.0f};
        setpoint.position = {NAN, NAN, NAN};
        setpoint.acceleration = {NAN, NAN, NAN};
        trajectory_setpoint_publisher_-&gt;publish(setpoint);
    }

    // Publishers
    rclcpp::Publisher&amp;#x3C;px4_msgs::msg::OffboardControlMode&gt;::SharedPtr
        offboard_control_mode_publisher_;
    rclcpp::Publisher&amp;#x3C;px4_msgs::msg::TrajectorySetpoint&gt;::SharedPtr
        trajectory_setpoint_publisher_;
    rclcpp::Publisher&amp;#x3C;px4_msgs::msg::VehicleCommand&gt;::SharedPtr
        vehicle_command_publisher_;

    // Subscribers
    rclcpp::Subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;::SharedPtr
        own_odometry_sub_;
    rclcpp::Subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;::SharedPtr uav2_sub_;
    rclcpp::Subscription&amp;#x3C;px4_msgs::msg::VehicleOdometry&gt;::SharedPtr uav3_sub_;

    rclcpp::TimerBase::SharedPtr control_timer_;

    std::array&amp;#x3C;float, 3&gt; own_position_ = {0.0f, 0.0f, 0.0f};
    std::array&amp;#x3C;float, 3&gt; own_velocity_ = {0.0f, 0.0f, 0.0f};
    std::array&amp;#x3C;float, 3&gt; desired_velocity_ = {0.0f, 0.0f, 0.0f};
    bool own_state_valid_ = false;

    std::map&amp;#x3C;int, NeighborState&gt; neighbor_states_;
    float position_gain_, velocity_gain_, filter_alpha_;
    rclcpp::Time last_print_time_, last_control_time_, start_time_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared&amp;#x3C;UAV1Controller&gt;();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真效果&lt;/h3&gt;
&lt;p&gt;开始时，在三架无人机的终端中使用 &lt;code&gt;commander takeoff&lt;/code&gt; 手动启动并在 $2.5\text{m}$ 高度悬停，随后在 $xy$ 方向进行控制，使其按编队跟随虚拟领航者运动。&lt;/p&gt;
&lt;p&gt;整体编队运动效果良好，存在一定超调但保持了稳定性，说明二阶控制律有效。&lt;/p&gt;
&lt;p&gt;值得关注的是，利用分布式通信的独特优势，我们还可以做进一步的鲁棒性测试：在整体开始运动后，关掉第 $1$ 号无人机的仿真（模拟被击落场景），剩余的无人机仍然按固定路线飞行，不受影响。这正是分布式通信的核心可靠性所在——利用连通的通信拓扑网络实现数据交换，在实际应用中可以有效减少单点故障对编队整体的影响。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本文从一阶集群系统出发，逐步引入二阶动力学模型，搭建了一个基于分布式通信的无人机编队控制系统。从一阶位置一致性推导至二阶模型的误差动力学方程和 Lyapunov 稳定性条件，通过同余变换将稳定性条件转化为可求解的 LMI，在 MATLAB 中利用 YALMIP 工具箱求得反馈增益矩阵；同时将加速度形式的控制指令通过积分和滤波转化为速度指令，兼容 PX4 的级联 PID 控制架构，并对含常定编队偏置的情形进行了严格推导，验证了 LMI 条件不变。&lt;/p&gt;
&lt;p&gt;分布式控制相比于集中式控制具有更高的鲁棒性和可扩展性，在复杂、突变的实际应用场景中具有显著优势。在低空经济、蜂群作战等场景中，这种通信控制一体化的架构将发挥越来越重要的作用。&lt;/p&gt;
&lt;p&gt;本文只是一个初步的控制方案，在仿真中也发现了许多可以改进的地方——例如如何进一步抑制超调、如何适应非连通的通信拓扑切换、如何在真实机载电脑上进行计算等。关于如何将仿真算法部署到真实无人机上，可以参考 &lt;a href=&quot;/zh/posts/px4-ros2-jetson-flash&quot;&gt;ROS2 真机实践：为机载电脑刷机&lt;/a&gt; 和 &lt;a href=&quot;/zh/posts/px4-ros2-mavros&quot;&gt;使用机载电脑控制 PX4 无人机（MAVROS2）&lt;/a&gt;，了解从仿真到实机的完整流程。&lt;/p&gt;
&lt;p&gt;最后，再次感谢大家的耐心阅读和批评指正！&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/32312200&quot;&gt;Multi-Agent System 控制(1)——图论 - 枕月&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_43391414/article/details/112277987&quot;&gt;拉普拉斯矩阵的性质 - CSDN 博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/hongliyu_lvliyu/article/details/106973870&quot;&gt;协同控制笔记 2.2——拉普拉斯矩阵相关引理 - CSDN 博客&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2+PX4 Multi-Drone Offboard Control Tutorial</title><link>https://duduuu.xyz/en/posts/px4-ros2-multi-offboard</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-multi-offboard</guid><description>Implement leader-follower dual-drone formation flight with ROS2+PX4 Offboard mode, covering topic naming and MAVLINK ID setup</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Following up on the multi-drone simulation setup covered in the previous article, this article explains how to control multiple drones using Offboard mode in ROS2. Multi-drone simulation involves several subtle naming conventions and code details that are easy to overlook — this article covers them all. If you encounter issues during simulation, use this as a troubleshooting reference.&lt;/p&gt;
&lt;h2&gt;Communication Overview&lt;/h2&gt;
&lt;p&gt;Multi-drone communication is more convenient in ROS2. Let&apos;s reference the PX4 official documentation:&lt;/p&gt;
&lt;p&gt;XRCE-DDS allows multiple clients to connect to the same agent via UDP. This is particularly useful in simulation since only one agent needs to be started.&lt;/p&gt;
&lt;p&gt;Following the previous article, each drone instance already has an independent &lt;code&gt;px4_instance&lt;/code&gt; number assigned, corresponding to the &lt;code&gt;-i 0/1/2...&lt;/code&gt; flag in our launch command. There&apos;s no need to manually assign instance numbers. Let&apos;s now proceed to Offboard control.&lt;/p&gt;
&lt;h2&gt;Starting the Simulation&lt;/h2&gt;
&lt;p&gt;Following the workflow from the previous article, we start two x_500 drone instances. Launch QGC ground station and Gazebo first, then open two terminals to start instances at positions (0,0) and (0,2) with px4_instance numbers 0 and 1:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1
cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,0&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 0

# Terminal 2
cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,2&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now check the ROS2 topics:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 topic list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows the published topics from both PX4 instances:&lt;/p&gt;
&lt;p&gt;We can see that topics from both PX4 instances are successfully published. Note the naming convention — you&apos;ll need this when writing control code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Naming Rule&lt;/strong&gt;: When the instance number is 0, topic names start with &lt;code&gt;/fmu/&lt;/code&gt;. When the instance number is ≥ 1, topic names start with &lt;code&gt;/px4_i/fmu/&lt;/code&gt;, where &lt;code&gt;i&lt;/code&gt; is the instance number from the launch command. This is critical.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Starting Communication&lt;/h2&gt;
&lt;p&gt;Now add communication for both drones. As explained above, use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A single agent instance handles both drones. From the terminal output we can see two client IDs: &lt;code&gt;0x00000001&lt;/code&gt; and &lt;code&gt;0x00000002&lt;/code&gt;. These correspond to the MAVLINK IDs, as explained in the official documentation:&lt;/p&gt;
&lt;p&gt;From the first table: when the agent starts, it automatically assigns &lt;code&gt;UXRCE_DDS_KEY&lt;/code&gt; values to each instance. This value equals the MAVLINK ID, and both equal &lt;code&gt;px4_instance + 1&lt;/code&gt;. So our drone 0 maps to 1, and drone 1 maps to 2.&lt;/p&gt;
&lt;p&gt;From the second table: the &lt;code&gt;target_system&lt;/code&gt; number also equals the MAVLINK ID, which is &lt;code&gt;px4_instance + 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With these naming conventions clarified, we can now move on to the code.&lt;/p&gt;
&lt;h2&gt;Dual-Drone Offboard Code Walkthrough&lt;/h2&gt;
&lt;p&gt;We&apos;ll write a leader-follower formation flight controller using Offboard mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;px4_msgs/msg/offboard_control_mode.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/trajectory_setpoint.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/vehicle_command.hpp&gt;
#include &amp;#x3C;rclcpp/rclcpp.hpp&gt;
#include &amp;#x3C;stdint.h&gt;

#include &amp;#x3C;chrono&gt;
#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;iostream&gt;

using namespace std::chrono;
using namespace std::chrono_literals;
using namespace px4_msgs::msg;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Include headers and declare namespaces. &lt;code&gt;px4_msgs&lt;/code&gt; is the most heavily used PX4 message package — we primarily use three types: &lt;code&gt;OffboardControlMode&lt;/code&gt;, &lt;code&gt;TrajectorySetpoint&lt;/code&gt;, and &lt;code&gt;VehicleCommand&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MultiOffboardControl : public rclcpp::Node
{
public:
    MultiOffboardControl() : Node(&quot;multi_offboard_control&quot;)
    {
        offboard_control_mode_publisher_uav0_ = this-&gt;create_publisher&amp;#x3C;OffboardControlMode&gt;(&quot;/fmu/in/offboard_control_mode&quot;, 10);
        trajectory_setpoint_publisher_uav0_ = this-&gt;create_publisher&amp;#x3C;TrajectorySetpoint&gt;(&quot;/fmu/in/trajectory_setpoint&quot;, 10);
        vehicle_command_publisher_uav0_ = this-&gt;create_publisher&amp;#x3C;VehicleCommand&gt;(&quot;/fmu/in/vehicle_command&quot;, 10);

        offboard_control_mode_publisher_uav1_ = this-&gt;create_publisher&amp;#x3C;OffboardControlMode&gt;(&quot;/px4_1/fmu/in/offboard_control_mode&quot;, 10);
        trajectory_setpoint_publisher_uav1_ = this-&gt;create_publisher&amp;#x3C;TrajectorySetpoint&gt;(&quot;/px4_1/fmu/in/trajectory_setpoint&quot;, 10);
        vehicle_command_publisher_uav1_ = this-&gt;create_publisher&amp;#x3C;VehicleCommand&gt;(&quot;/px4_1/fmu/in/vehicle_command&quot;, 10);

        offboard_setpoint_counter_ = 0;
        time_start_ = this-&gt;get_clock()-&gt;now();

        auto timer_callback = [this]() -&gt; void {
            if (offboard_setpoint_counter_ == 10 || offboard_setpoint_counter_ % 50 == 0) {
                this-&gt;publish_vehicle_command_uav0(VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1, 6);
                this-&gt;arm_uav0();
                this-&gt;publish_vehicle_command_uav1(VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1, 6);
                this-&gt;arm_uav1();
            }

            publish_offboard_control_mode_uav0();
            publish_trajectory_setpoint_uav0();
            publish_offboard_control_mode_uav1();
            publish_trajectory_setpoint_uav1();

            if (offboard_setpoint_counter_ &amp;#x3C; 11) {
                offboard_setpoint_counter_++;
            }
        };
        timer_ = this-&gt;create_wall_timer(100ms, timer_callback);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Define the &lt;code&gt;MultiOffboardControl&lt;/code&gt; class and constructor, naming the node &lt;code&gt;&quot;multi_offboard_control&quot;&lt;/code&gt;. The constructor creates publishers for both drone instances&apos; ROS2 topics with a queue size of 10. This is where the topic naming rule matters: drone 0 uses &lt;code&gt;/fmu/&lt;/code&gt; prefix, drone 1 uses &lt;code&gt;/px4_1/fmu/&lt;/code&gt; prefix. Pay close attention here.&lt;/p&gt;
&lt;p&gt;A 100ms timer is set up: on the 10th cycle it arms both drones, and thereafter it continuously publishes Offboard control commands.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The timer frequency must meet the Offboard minimum of 2Hz. Our 100ms (10Hz) satisfies this requirement. Lower frequencies combined with position-only control may result in larger trajectory deviations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void arm_uav0() { publish_vehicle_command_uav0(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0); }
    void disarm_uav0() { publish_vehicle_command_uav0(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0); }
    void arm_uav1() { publish_vehicle_command_uav1(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0); }
    void disarm_uav1() { publish_vehicle_command_uav1(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0); }

private:
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher&amp;#x3C;OffboardControlMode&gt;::SharedPtr offboard_control_mode_publisher_uav0_;
    rclcpp::Publisher&amp;#x3C;TrajectorySetpoint&gt;::SharedPtr trajectory_setpoint_publisher_uav0_;
    rclcpp::Publisher&amp;#x3C;VehicleCommand&gt;::SharedPtr vehicle_command_publisher_uav0_;
    rclcpp::Publisher&amp;#x3C;OffboardControlMode&gt;::SharedPtr offboard_control_mode_publisher_uav1_;
    rclcpp::Publisher&amp;#x3C;TrajectorySetpoint&gt;::SharedPtr trajectory_setpoint_publisher_uav1_;
    rclcpp::Publisher&amp;#x3C;VehicleCommand&gt;::SharedPtr vehicle_command_publisher_uav1_;
    uint64_t offboard_setpoint_counter_;
    rclcpp::Time time_start_;
    float leader_position_[3] = {0.0, 0.0, -5.0};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;arm_uav0()&lt;/code&gt; and &lt;code&gt;disarm_uav0()&lt;/code&gt; arm and disarm drone 0 respectively — the same commands we saw in the PX4 terminal in the previous article.&lt;/p&gt;
&lt;p&gt;For trajectory generation, we use the simplest leader-follower algorithm: the leader&apos;s initial position is &lt;code&gt;(0, 0, -5)&lt;/code&gt;. Drone 0 (leader) flies in a circular pattern at altitude 5m, radius 5m, period 20s. Drone 1 (follower) tracks the leader&apos;s position with a fixed offset — 2m behind and 2m to the right.&lt;/p&gt;
&lt;p&gt;Each drone instance has three corresponding functions: &lt;code&gt;publish_offboard_control_mode_uav0/1()&lt;/code&gt;, &lt;code&gt;publish_trajectory_setpoint_uav0/1()&lt;/code&gt;, and &lt;code&gt;publish_vehicle_command_uav0/1()&lt;/code&gt;, corresponding to the three main message types. Let&apos;s examine each:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_offboard_control_mode_uav0()
    {
        OffboardControlMode msg{};
        msg.position = true;
        msg.velocity = false;
        msg.acceleration = false;
        msg.attitude = false;
        msg.body_rate = false;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        offboard_control_mode_publisher_uav0_-&gt;publish(msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;publish_offboard_control_mode_uav0()&lt;/code&gt; sends the control mode to drone 0. Setting &lt;code&gt;msg.position = true&lt;/code&gt; enables position control mode in Offboard. Other modes (velocity, acceleration, attitude, body rate) are set to &lt;code&gt;false&lt;/code&gt;. For more advanced flight control algorithm research, modify these flags to use multiple control modes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_trajectory_setpoint_uav0()
    {
        auto now = this-&gt;get_clock()-&gt;now();
        double t = (now - time_start_).seconds();
        const double radius = 5.0;
        const double period = 20.0;
        double omega = 2.0 * M_PI / period;

        TrajectorySetpoint msg{};
        msg.position = {
            static_cast&amp;#x3C;float&gt;(radius * std::cos(omega * t)),
            static_cast&amp;#x3C;float&gt;(radius * std::sin(omega * t)),
            -5.0f
        };
        msg.yaw = -omega * t;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        trajectory_setpoint_publisher_uav0_-&gt;publish(msg);

        leader_position_[0] = msg.position[0];
        leader_position_[1] = msg.position[1];
        leader_position_[2] = msg.position[2];
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav0 position: x=%f, y=%f, z=%f&quot;,
                    leader_position_[0], leader_position_[1], leader_position_[2]);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;publish_trajectory_setpoint_uav0()&lt;/code&gt; sends trajectory setpoints to drone 0 (the leader), which flies in a circle at constant angular velocity.&lt;/p&gt;
&lt;p&gt;A note on &lt;code&gt;msg.yaw&lt;/code&gt;: this represents the quadrotor&apos;s yaw angle using Euler angle attitude representation. Quadrotors use the NED right-hand coordinate system with counterclockwise as the positive rotation direction. The three Euler angles — roll, pitch, and yaw — represent the drone&apos;s orientation. In both drones&apos; trajectory code, yaw is set to align the drone with the flight path tangent/normal. Below are Euler angle reference diagrams:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_vehicle_command_uav0(uint16_t command, float param1 = 0.0, float param2 = 0.0)
    {
        VehicleCommand msg{};
        msg.param1 = param1;
        msg.param2 = param2;
        msg.command = command;
        msg.target_system = 1;
        msg.target_component = 1;
        msg.source_system = 1;
        msg.source_component = 1;
        msg.from_external = true;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav0 command: %u, param1=%f, param2=%f, target_system=%u&quot;,
                    command, param1, param2, msg.target_system);
        vehicle_command_publisher_uav0_-&gt;publish(msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;publish_vehicle_command_uav0()&lt;/code&gt; sends vehicle commands to drone 0. Two parameters deserve attention: &lt;code&gt;msg.target_system&lt;/code&gt; follows the &lt;code&gt;px4_instance + 1&lt;/code&gt; rule (drone 0 → 1, drone 1 → 2, etc.). &lt;code&gt;msg.from_external&lt;/code&gt; must be &lt;code&gt;true&lt;/code&gt; since we&apos;re using external Offboard control mode.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_offboard_control_mode_uav1()
    {
        OffboardControlMode msg{};
        msg.position = true;
        msg.velocity = false;
        msg.acceleration = false;
        msg.attitude = false;
        msg.body_rate = false;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        offboard_control_mode_publisher_uav1_-&gt;publish(msg);
    }

    void publish_trajectory_setpoint_uav1()
    {
        const float offset_x = 2.0;
        const float offset_y = 2.0;
        const float offset_z = 0.0;

        TrajectorySetpoint msg{};
        msg.position = {
            leader_position_[0] + offset_x,
            leader_position_[1] + offset_y,
            leader_position_[2] + offset_z
        };
        msg.yaw = -std::atan2(msg.position[1], msg.position[0]);
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav1 setpoint: x=%f, y=%f, z=%f&quot;,
                    msg.position[0], msg.position[1], msg.position[2]);
        trajectory_setpoint_publisher_uav1_-&gt;publish(msg);
    }

    void publish_vehicle_command_uav1(uint16_t command, float param1 = 0.0, float param2 = 0.0)
    {
        VehicleCommand msg{};
        msg.param1 = param1;
        msg.param2 = param2;
        msg.command = command;
        msg.target_system = 2;
        msg.target_component = 1;
        msg.source_system = 1;
        msg.source_component = 1;
        msg.from_external = true;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav1 command: %u, param1=%f, param2=%f, target_system=%u&quot;,
                    command, param1, param2, msg.target_system);
        vehicle_command_publisher_uav1_-&gt;publish(msg);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drone 1&apos;s (follower) three functions mirror the leader&apos;s, with the topic prefix changed to &lt;code&gt;/px4_1/fmu/&lt;/code&gt;, &lt;code&gt;target_system&lt;/code&gt; set to 2, and trajectory set to the leader&apos;s current position plus a fixed offset of &lt;code&gt;(2.0, 2.0, 0.0)&lt;/code&gt; for rear-right formation flight.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main(int argc, char *argv[])
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;Starting multi offboard control node...&quot; &amp;#x3C;&amp;#x3C; std::endl;
    setvbuf(stdout, NULL, _IONBF, BUFSIZ);
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared&amp;#x3C;MultiOffboardControl&gt;());
    rclcpp::shutdown();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;main()&lt;/code&gt; function: &lt;code&gt;rclcpp::init&lt;/code&gt; initializes the ROS2 environment, &lt;code&gt;rclcpp::spin&lt;/code&gt; runs the &lt;code&gt;multi_offboard_control&lt;/code&gt; node, and &lt;code&gt;rclcpp::shutdown&lt;/code&gt; cleans up on exit.&lt;/p&gt;
&lt;p&gt;After compiling and running as described in the previous article, both drones follow the programmed trajectories and maintain formation, confirming the dual-drone formation simulation is working correctly. Below is the simulation video:&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Drone formation simulation is more convenient in ROS2, primarily due to integrated communication — no need to manually add communication for each drone instance. However, subtle naming convention mistakes often cause failures: ROS2 topic naming rules, MAVLINK ID rules, and &lt;code&gt;target_system&lt;/code&gt; rules. This article has covered these conventions with reference to PX4 official documentation.&lt;/p&gt;
&lt;p&gt;For clarity, this article used a simple two-drone leader-follower formation. Real-world applications demand far more sophisticated approaches. In a later article, we introduce &lt;a href=&quot;/en/posts/px4-ros2-distributed-consensus&quot;&gt;Distributed Control &amp;#x26; Second-Order Consensus&lt;/a&gt;, using LMI-based gain synthesis to achieve more robust multi-drone formation control. We look forward to seeing what you build.&lt;/p&gt;
&lt;p&gt;Thank you for reading. Feedback, corrections, and suggestions are always welcome.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BUAA Reliable Flight Control Group&lt;/li&gt;
&lt;li&gt;PX4 Official Documentation: &lt;a href=&quot;https://docs.px4.io/main/en/ros2/multi_vehicle.html&quot;&gt;ROS2 Multi-Vehicle Simulation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2+PX4 多机OFFBOARD模式教程</title><link>https://duduuu.xyz/zh/posts/px4-ros2-multi-offboard</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-multi-offboard</guid><description>在ROS2+PX4仿真中基于Offboard模式实现leader-follower双无人机编队飞行，详解话题命名与MAVLINK ID配置</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;书接上文，我们基于已经介绍过的在ROS2+PX4环境下启动多机仿真的流程，介绍一下如何使用Offboard模式在ROS2中控制多机仿真。在多机仿真中，有一些细小的命名规则和代码细节需要注意，本文会一一讲解。如果读者在仿真过程中发现没有能够成功实现多无人机仿真，可以对照本文进行问题排查。&lt;/p&gt;
&lt;h2&gt;通信说明&lt;/h2&gt;
&lt;p&gt;在ROS2中进行多机通信会更加便捷，在此处我们参考一下PX4官方文档对此部分给出的说明：&lt;/p&gt;
&lt;p&gt;XRCE-DDS允许多个客户端通过 UDP 连接到同一个代理。这在模拟中特别有用，因为只需要启动一个代理。&lt;/p&gt;
&lt;p&gt;因此，在我们前一篇文章添加通信时，其实已经为不同的无人机实例分配好了一个独立的px4_instance编号，对应着我们启动命令的 &lt;code&gt;-i 0/1/2...&lt;/code&gt;。因此我们不再需要手动的去为各个实例分配编号。现在，我们开始进行Offboard控制。&lt;/p&gt;
&lt;h2&gt;启动仿真&lt;/h2&gt;
&lt;p&gt;我们仿照前一篇文章的流程，启动两个x_500无人机实例。按照步骤启动QGC地面站、Gazebo仿真环境。打开两个终端启动实例，分别放置在(0,0)和(0,2)处，px4_instance编号分别为0,1：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 第一个终端
cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,0&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 0

# 第二个终端
cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,2&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们在终端输入：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 topic list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以查看px4实例的话题名称：&lt;/p&gt;
&lt;p&gt;我们可以发现，px4的两个实例的话题成功发布，现在我们需要记录一下这个命名规则，后续编写代码时需要用到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;命名规则&lt;/strong&gt;：当实例编号为0时，话题的名称以 &lt;code&gt;/fmu/&lt;/code&gt; 开始；而当实例编号大于等于1时，话题的名称以 &lt;code&gt;/px4_i/fmu/&lt;/code&gt; 开始，其中 &lt;code&gt;i&lt;/code&gt; 为我们启动仿真时的实例编号。这个规则需要注意。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;启动通信&lt;/h2&gt;
&lt;p&gt;现在我们为两个无人机添加通信，按照上一篇文章和本文开头的说明，使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动一个通信客户端即可完成通信，此时我们根据终端的输出可以发现通信成功，同时有两个客户端的ID，一个为 &lt;code&gt;0x00000001&lt;/code&gt;，一个为 &lt;code&gt;0x00000002&lt;/code&gt;，这与两个实例的MAVLINK ID相关，此处可以在官方文档上找到答案：&lt;/p&gt;
&lt;p&gt;根据第一张图我们得知，在启动通信客户端时，自动为两个实例分配了UXRCE_DDS_KEY值，且与MAVLINK ID相同，值均为 &lt;code&gt;px4_instance + 1&lt;/code&gt;，因此我们的0号无人机对应的是1，而1号无人机对应的是2。&lt;/p&gt;
&lt;p&gt;根据第二张图我们得知，&lt;code&gt;target_system&lt;/code&gt; 编号与MAVLINK ID相同，值也为 &lt;code&gt;px4_instance + 1&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;至此，关于多机通信的一些命名规则，以及一些容易被忽视的细节问题就解决好了。&lt;/p&gt;
&lt;h2&gt;双机Offboard代码解释&lt;/h2&gt;
&lt;p&gt;我们编写一个双无人机编队的飞行代码，使用Offboard模式进行外部控制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;#x3C;px4_msgs/msg/offboard_control_mode.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/trajectory_setpoint.hpp&gt;
#include &amp;#x3C;px4_msgs/msg/vehicle_command.hpp&gt;
#include &amp;#x3C;rclcpp/rclcpp.hpp&gt;
#include &amp;#x3C;stdint.h&gt;

#include &amp;#x3C;chrono&gt;
#include &amp;#x3C;cmath&gt;
#include &amp;#x3C;iostream&gt;

using namespace std::chrono;
using namespace std::chrono_literals;
using namespace px4_msgs::msg;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加头文件和命名空间，其中 &lt;code&gt;px4_msgs&lt;/code&gt; 是我们使用最多的px4的消息类型，我们主要使用其中的三个：&lt;code&gt;OffboardControlMode&lt;/code&gt;、&lt;code&gt;TrajectorySetpoint&lt;/code&gt; 和 &lt;code&gt;VehicleCommand&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class MultiOffboardControl : public rclcpp::Node
{
public:
    MultiOffboardControl() : Node(&quot;multi_offboard_control&quot;)
    {
        offboard_control_mode_publisher_uav0_ = this-&gt;create_publisher&amp;#x3C;OffboardControlMode&gt;(&quot;/fmu/in/offboard_control_mode&quot;, 10);
        trajectory_setpoint_publisher_uav0_ = this-&gt;create_publisher&amp;#x3C;TrajectorySetpoint&gt;(&quot;/fmu/in/trajectory_setpoint&quot;, 10);
        vehicle_command_publisher_uav0_ = this-&gt;create_publisher&amp;#x3C;VehicleCommand&gt;(&quot;/fmu/in/vehicle_command&quot;, 10);

        offboard_control_mode_publisher_uav1_ = this-&gt;create_publisher&amp;#x3C;OffboardControlMode&gt;(&quot;/px4_1/fmu/in/offboard_control_mode&quot;, 10);
        trajectory_setpoint_publisher_uav1_ = this-&gt;create_publisher&amp;#x3C;TrajectorySetpoint&gt;(&quot;/px4_1/fmu/in/trajectory_setpoint&quot;, 10);
        vehicle_command_publisher_uav1_ = this-&gt;create_publisher&amp;#x3C;VehicleCommand&gt;(&quot;/px4_1/fmu/in/vehicle_command&quot;, 10);

        offboard_setpoint_counter_ = 0;
        time_start_ = this-&gt;get_clock()-&gt;now();

        auto timer_callback = [this]() -&gt; void {
            if (offboard_setpoint_counter_ == 10 || offboard_setpoint_counter_ % 50 == 0) {
                this-&gt;publish_vehicle_command_uav0(VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1, 6);
                this-&gt;arm_uav0();
                this-&gt;publish_vehicle_command_uav1(VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1, 6);
                this-&gt;arm_uav1();
            }

            publish_offboard_control_mode_uav0();
            publish_trajectory_setpoint_uav0();
            publish_offboard_control_mode_uav1();
            publish_trajectory_setpoint_uav1();

            if (offboard_setpoint_counter_ &amp;#x3C; 11) {
                offboard_setpoint_counter_++;
            }
        };
        timer_ = this-&gt;create_wall_timer(100ms, timer_callback);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后定义 &lt;code&gt;MultiOffboardControl&lt;/code&gt; 类，将节点命名为 &lt;code&gt;&quot;multi_offboard_control&quot;&lt;/code&gt; 并进行初始化。在初始化时创建了发布者，发布两个无人机实例对应的ROS2话题，消息队列长为10。这个地方就要与上文我们介绍过的话题命名规则相对应，第0号无人机的话题以 &lt;code&gt;/fmu/&lt;/code&gt; 开头，后续以 &lt;code&gt;/px4_i/fmu/&lt;/code&gt; 开头，其中 &lt;code&gt;i&lt;/code&gt; 为每个无人机实例的编号，此处需要小心。&lt;/p&gt;
&lt;p&gt;随后是一个定时器的逻辑，我们使用一个100ms的定时器，在第十次循环发送解锁无人机的指令，后续持续发布Offboard控制指令，向无人机发送控制模式和轨迹信息。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：此处的定时器频率需要设定一个合适的值，由于Offboard是外部控制模式，其本身有很大的安全性挑战，所以其要求一个至少为2Hz的消息频率；此处我们是100ms（10Hz），满足要求。同时，如果我们使用的消息频率较低，且只使用位置控制（position_control），有可能会造成轨迹上较大的偏差。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void arm_uav0() { publish_vehicle_command_uav0(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0); }
    void disarm_uav0() { publish_vehicle_command_uav0(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0); }
    void arm_uav1() { publish_vehicle_command_uav1(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 1.0); }
    void disarm_uav1() { publish_vehicle_command_uav1(VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0); }

private:
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher&amp;#x3C;OffboardControlMode&gt;::SharedPtr offboard_control_mode_publisher_uav0_;
    rclcpp::Publisher&amp;#x3C;TrajectorySetpoint&gt;::SharedPtr trajectory_setpoint_publisher_uav0_;
    rclcpp::Publisher&amp;#x3C;VehicleCommand&gt;::SharedPtr vehicle_command_publisher_uav0_;
    rclcpp::Publisher&amp;#x3C;OffboardControlMode&gt;::SharedPtr offboard_control_mode_publisher_uav1_;
    rclcpp::Publisher&amp;#x3C;TrajectorySetpoint&gt;::SharedPtr trajectory_setpoint_publisher_uav1_;
    rclcpp::Publisher&amp;#x3C;VehicleCommand&gt;::SharedPtr vehicle_command_publisher_uav1_;
    uint64_t offboard_setpoint_counter_;
    rclcpp::Time time_start_;
    float leader_position_[3] = {0.0, 0.0, -5.0};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;arm_uav0()&lt;/code&gt; 和 &lt;code&gt;disarm_uav0()&lt;/code&gt; 两个函数是用于第0号无人机解锁和上锁，我们在上一篇文章的px4终端曾经看到过这些命令。&lt;/p&gt;
&lt;p&gt;随后我们为两个无人机实例分别编写运动轨迹的逻辑，此处我们用一个最简单的leader-follower算法，即初始时 &lt;code&gt;leader_position&lt;/code&gt; 为 &lt;code&gt;(0, 0, -5)&lt;/code&gt;，后续我们选取第0号无人机为leader，按照半径为5、周期为20秒，在5米空中进行盘旋运动，同时将它的实时位置作为1号无人机（follower）的目标位置进行发送，让其在第0号无人机后方2米、右侧2米进行跟随伴飞。&lt;/p&gt;
&lt;p&gt;我们可以看到，两个无人机实例分别对应三个函数，分别是 &lt;code&gt;publish_offboard_control_mode_uav0/1()&lt;/code&gt;、&lt;code&gt;publish_trajectory_setpoint_uav0/1()&lt;/code&gt; 以及 &lt;code&gt;publish_vehicle_command_uav0/1()&lt;/code&gt;，与我们之前讲的三种主要的msgs消息类型相对应。他们分别用来发布控制模式、运动轨迹及速度和加速度、车辆命令（解锁、起飞、切换模式等信息），我们一一来看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_offboard_control_mode_uav0()
    {
        OffboardControlMode msg{};
        msg.position = true;
        msg.velocity = false;
        msg.acceleration = false;
        msg.attitude = false;
        msg.body_rate = false;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        offboard_control_mode_publisher_uav0_-&gt;publish(msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;publish_offboard_control_mode_uav0()&lt;/code&gt; 函数是向第0号无人机发送控制模式的函数，函数的内部我们将 &lt;code&gt;msg.position&lt;/code&gt; 的值设为 &lt;code&gt;true&lt;/code&gt;，用以启动Offboard中的位置控制模式，即向无人机发布轨迹信息，其他值设为 &lt;code&gt;false&lt;/code&gt;，代表我们暂时不使用速度控制、加速度控制、姿态控制等。如果需要对无人机进行更细致的飞行控制算法的研究，可以在此进行修改，使用多种控制模式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_trajectory_setpoint_uav0()
    {
        auto now = this-&gt;get_clock()-&gt;now();
        double t = (now - time_start_).seconds();
        const double radius = 5.0;
        const double period = 20.0;
        double omega = 2.0 * M_PI / period;

        TrajectorySetpoint msg{};
        msg.position = {
            static_cast&amp;#x3C;float&gt;(radius * std::cos(omega * t)),
            static_cast&amp;#x3C;float&gt;(radius * std::sin(omega * t)),
            -5.0f
        };
        msg.yaw = -omega * t;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        trajectory_setpoint_publisher_uav0_-&gt;publish(msg);

        leader_position_[0] = msg.position[0];
        leader_position_[1] = msg.position[1];
        leader_position_[2] = msg.position[2];
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav0 position: x=%f, y=%f, z=%f&quot;,
                    leader_position_[0], leader_position_[1], leader_position_[2]);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;publish_trajectory_setpoint_uav0()&lt;/code&gt; 函数是向第0号无人机发送轨迹信息的函数，第0号无人机作为我们选定的领航者，按照固定的角速度进行盘旋飞行。这个函数的轨迹逻辑较为简单，我们不再赘述，可以在上述的示例中找到详细的代码。&lt;/p&gt;
&lt;p&gt;此处有一个小的说明，在该函数中有一个 &lt;code&gt;msg.yaw&lt;/code&gt;，这表示四旋翼无人机的偏航角，这是使用欧拉角进行姿态表示。在四旋翼中，我们使用NED右手坐标系，取逆时针为旋转正方向，用英文roll、pitch、yaw分别表示无人机的滚转角、俯仰角和偏航角。在两架无人机的轨迹代码中，我们添加了偏航角的表达，这是为了让飞机沿着飞行轨迹的法向/切向进行运动，特此说明。在此处给出一个欧拉角的表示方法示意图：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_vehicle_command_uav0(uint16_t command, float param1 = 0.0, float param2 = 0.0)
    {
        VehicleCommand msg{};
        msg.param1 = param1;
        msg.param2 = param2;
        msg.command = command;
        msg.target_system = 1;
        msg.target_component = 1;
        msg.source_system = 1;
        msg.source_component = 1;
        msg.from_external = true;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav0 command: %u, param1=%f, param2=%f, target_system=%u&quot;,
                    command, param1, param2, msg.target_system);
        vehicle_command_publisher_uav0_-&gt;publish(msg);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;publish_vehicle_command_uav0()&lt;/code&gt; 函数是向第0号无人机发送车辆命令的函数。在这个函数中，我们需要关注的有两个比较重要的参数：&lt;code&gt;msg.target_system&lt;/code&gt; 和 &lt;code&gt;msg.from_external&lt;/code&gt;；前一个参数是我们在文章开头时就已经提到过的，他的命名规则为 &lt;code&gt;px4_instance + 1&lt;/code&gt;，与MAVLINK ID、UXRCE_DDS_KEY的值均相同。因此，第0号无人机的 &lt;code&gt;target_system&lt;/code&gt; 值为1，第1号无人机的 &lt;code&gt;target_system&lt;/code&gt; 值为2，以此类推，在后续添加多个无人机实例时，在 &lt;code&gt;vehicle_command&lt;/code&gt; 函数里修改对应的值即可。第二个参数 &lt;code&gt;from_external&lt;/code&gt; 是外部控制指令的参数，我们设为 &lt;code&gt;true&lt;/code&gt;，因为我们此时使用的是外部控制模式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;    void publish_offboard_control_mode_uav1()
    {
        OffboardControlMode msg{};
        msg.position = true;
        msg.velocity = false;
        msg.acceleration = false;
        msg.attitude = false;
        msg.body_rate = false;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        offboard_control_mode_publisher_uav1_-&gt;publish(msg);
    }

    void publish_trajectory_setpoint_uav1()
    {
        const float offset_x = 2.0;
        const float offset_y = 2.0;
        const float offset_z = 0.0;

        TrajectorySetpoint msg{};
        msg.position = {
            leader_position_[0] + offset_x,
            leader_position_[1] + offset_y,
            leader_position_[2] + offset_z
        };
        msg.yaw = -std::atan2(msg.position[1], msg.position[0]);
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav1 setpoint: x=%f, y=%f, z=%f&quot;,
                    msg.position[0], msg.position[1], msg.position[2]);
        trajectory_setpoint_publisher_uav1_-&gt;publish(msg);
    }

    void publish_vehicle_command_uav1(uint16_t command, float param1 = 0.0, float param2 = 0.0)
    {
        VehicleCommand msg{};
        msg.param1 = param1;
        msg.param2 = param2;
        msg.command = command;
        msg.target_system = 2;
        msg.target_component = 1;
        msg.source_system = 1;
        msg.source_component = 1;
        msg.from_external = true;
        msg.timestamp = this-&gt;get_clock()-&gt;now().nanoseconds() / 1000;
        RCLCPP_INFO(this-&gt;get_logger(), &quot;uav1 command: %u, param1=%f, param2=%f, target_system=%u&quot;,
                    command, param1, param2, msg.target_system);
        vehicle_command_publisher_uav1_-&gt;publish(msg);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第1号无人机（follower）的三个函数与leader同理，只是话题前缀改为 &lt;code&gt;/px4_1/fmu/&lt;/code&gt;，&lt;code&gt;target_system&lt;/code&gt; 改为2，轨迹设定为leader当前位置加上固定偏移量 &lt;code&gt;(2.0, 2.0, 0.0)&lt;/code&gt;，实现后方右侧伴飞。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int main(int argc, char *argv[])
{
    std::cout &amp;#x3C;&amp;#x3C; &quot;Starting multi offboard control node...&quot; &amp;#x3C;&amp;#x3C; std::endl;
    setvbuf(stdout, NULL, _IONBF, BUFSIZ);
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared&amp;#x3C;MultiOffboardControl&gt;());
    rclcpp::shutdown();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后运行 &lt;code&gt;main()&lt;/code&gt; 函数，&lt;code&gt;rclcpp::init&lt;/code&gt; 用于初始化ROS2环境，&lt;code&gt;rclcpp::spin&lt;/code&gt; 用于运行 &lt;code&gt;multi_offboard_control&lt;/code&gt; 节点，最后在程序结束时使用 &lt;code&gt;rclcpp::shutdown&lt;/code&gt; 清理环境。整个代码流程结束。&lt;/p&gt;
&lt;p&gt;我们按照上一篇文章所述将代码编译成可执行文件并运行，可以发现两架无人机成功按照我们的代码逻辑进行飞行，并保持一定的编队轨迹，证明我们的双无人机编队仿真代码成功运行。以下是仿真视频：&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在ROS2中，无人机编队的仿真会更加的容易，主要体现在其对于通信的集成，不需要我们再单独地为每架无人机实例添加通信。但是，在编写实际工程代码时，一些细微的命名规则的混淆往往会造成失败，主要体现在ROS2话题的命名规则、MAVLINK ID的命名规则、以及 &lt;code&gt;target_system&lt;/code&gt; 的命名规则。本文参照PX4的官方文档，对这些规则进行了介绍，读者朋友们可以参考和排查。&lt;/p&gt;
&lt;p&gt;同时，本文为了简明扼要地介绍多无人机Offboard控制流程，选择了一个简单的leader-follower两机的编队，在实际工程和科学研究中，这是远远不能够满足现实产品需求的。在后续文章中，我们将引入 &lt;a href=&quot;/zh/posts/px4-ros2-distributed-consensus&quot;&gt;分布式通信与二阶一致性协议&lt;/a&gt;，利用 LMI 方法求解反馈增益矩阵，实现更高鲁棒性的多机编队协同控制。期待读者朋友们的优秀成果。&lt;/p&gt;
&lt;p&gt;最后，再次感谢读者朋友们的支持和耐心的阅读，欢迎大家对本文进行批评指正、补充及建议。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;北京航空航天大学可靠飞行控制研究组（BUAA Reliable Flight Control Group）&lt;/li&gt;
&lt;li&gt;PX4官方文档：&lt;a href=&quot;https://docs.px4.io/main/en/ros2/multi_vehicle.html&quot;&gt;ROS2 Multi-Vehicle Simulation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>YOLOv8 Object Detection in PX4 Drone Simulation</title><link>https://duduuu.xyz/en/posts/px4-ros2-yolo</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-yolo</guid><description>Integrate YOLOv8 real-time object detection into the ROS2+PX4 simulation environment using Gazebo camera topics for drone imagery analysis</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In the third part of our drone formation simulation series, we focus on integrating object detection into the &lt;a href=&quot;/en/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 simulation environment&lt;/a&gt; using YOLOv8 for efficient real-time detection. YOLO (You Only Look Once) is a high-performance real-time object detection algorithm widely used in computer vision tasks such as drone target recognition, autonomous driving, and surveillance.&lt;/p&gt;
&lt;p&gt;We use PX4 SITL (Software-In-The-Loop) with Gazebo Ignition and the &lt;code&gt;gz_x500_depth&lt;/code&gt; quadrotor model. The system subscribes to the RGB camera (OakD-Lite) feed, applies YOLOv8 for target detection, and visualizes results in real-time on Ubuntu 22.04. This provides a foundation for more advanced cooperative drone tasks.&lt;/p&gt;
&lt;h2&gt;Creating a Python Virtual Environment&lt;/h2&gt;
&lt;p&gt;This setup uses Python 3.10.12 — adjust commands for your Python version as needed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Create virtual environment
python3 -m venv ~/px4-venv
# Activate
source ~/px4-venv/bin/activate
# Link ROS2 environment
source /opt/ros/humble/setup.bash
export PYTHONPATH=/opt/ros/humble/lib/python3.10/site-packages:$PYTHONPATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Installing MAVSDK&lt;/h2&gt;
&lt;p&gt;If you encounter NumPy version conflicts, uninstall the current version with &lt;code&gt;pip uninstall numpy -y&lt;/code&gt; first, then install NumPy 1.x as shown below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install mavsdk
pip install aioconsole
pip install pygame
sudo apt install ros-humble-ros-gzgarden
pip install &quot;numpy&amp;#x3C;2&quot;
pip install opencv-python
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Installing YOLO&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install ultralytics
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Launching the Quadrotor with Camera and Adding Camera Topics&lt;/h2&gt;
&lt;p&gt;We modify the launch command slightly from the previous articles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,0&quot; PX4_SIM_MODEL=gz_x500_depth ./build/px4_sitl_default/bin/px4 -i 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the x500 model, 4001/4002 correspond to two different quadrotor configurations (&quot;X&quot; frame and cross frame). Here we use the &lt;code&gt;gz_x500_depth&lt;/code&gt; model with a depth camera, which differs from the previous setup.&lt;/p&gt;
&lt;p&gt;Now let&apos;s examine the &lt;code&gt;model.sdf&lt;/code&gt; file for &lt;code&gt;x500_depth&lt;/code&gt; and its camera module &lt;code&gt;OakD_Lite&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;The OakD-Lite camera module has two camera sensors: the IMX214 (RGB camera) and the StereoOV7251 (depth camera). The RGB camera outputs &lt;code&gt;RGB_INT8&lt;/code&gt; format, while the depth camera outputs &lt;code&gt;R_FLOAT32&lt;/code&gt; format. RGB cameras are commonly used for image recognition, while depth cameras measure distance and are useful for obstacle avoidance. Here, we&apos;ll use the RGB camera first.&lt;/p&gt;
&lt;p&gt;Key parameters from the SDF file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;width&gt;1920&amp;#x3C;/width&gt;
&amp;#x3C;height&gt;1080&amp;#x3C;/height&gt;
&amp;#x3C;format&gt;RGB_INT8&amp;#x3C;/format&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These three parameters represent image dimensions and format — critical for the code that follows. RGB_INT8 is a three-channel format.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;always_on&gt;1&amp;#x3C;/always_on&gt;
&amp;#x3C;update_rate&gt;30&amp;#x3C;/update_rate&gt;
&amp;#x3C;visualize&gt;true&amp;#x3C;/visualize&gt;
&amp;#x3C;topic&gt;image_raw&amp;#x3C;/topic&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;always_on&gt;&lt;/code&gt; keeps the camera continuously active. The camera has a ROS2 topic named &lt;code&gt;image_raw&lt;/code&gt; (added manually here), which we&apos;ll use for message transport.&lt;/p&gt;
&lt;h2&gt;Running YOLOv8 Object Detection&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/YOLO
cd ~/YOLO
touch uav_camera_det.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;/YOLO&lt;/code&gt; directory and write a Python script for YOLOv8 object detection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
from ultralytics import YOLO

model = YOLO(&apos;yolov8m.pt&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Import the required libraries and image conversion tools. &lt;code&gt;rclpy&lt;/code&gt; is the ROS2 Python client library, &lt;code&gt;CvBridge&lt;/code&gt; converts between ROS and OpenCV image formats, and &lt;code&gt;yolov8m.pt&lt;/code&gt; is the medium-sized model trained on the COCO dataset (80 object classes).&lt;/p&gt;
&lt;p&gt;COCO dataset categories:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: person         1: bicycle       2: car           3: motorcycle
4: airplane       5: bus           6: train         7: truck
8: boat           9: traffic light 10: fire hydrant 11: stop sign
...
76: scissors      77: teddy bear   78: hair drier   79: toothbrush
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To reduce computation and improve detection speed, we can limit the target classes — more on that below.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class ImageSubscriber(Node):
    def __init__(self):
        super().__init__(&apos;image_subscriber&apos;)
        self.subscription = self.create_subscription(
            Image,
            &apos;/image_raw&apos;,
            self.listener_callback,
            10)
        self.br = CvBridge()
        self.frame_count = 0
        self.process_interval = 1

        cv2.namedWindow(&apos;YOLOv8 Detection&apos;, cv2.WINDOW_NORMAL)
        cv2.resizeWindow(&apos;YOLOv8 Detection&apos;, 640, 480)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similar to the C++ Offboard code in the previous article, we define a new class &lt;code&gt;image_subscriber&lt;/code&gt; as a ROS2 subscriber instance. The topic name must match the one we added to the RGB camera&apos;s SDF file: &lt;code&gt;/image_raw&lt;/code&gt;. CvBridge converts ROS image format (RGB8) to OpenCV format (BGR8). The display window is set to 640×480.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def listener_callback(self, data):
    self.frame_count += 1
    if self.frame_count % self.process_interval != 0:
        self.get_logger().info(&quot;Skipping frame&quot;)
        return
    try:
        self.get_logger().info(f&quot;Received frame, encoding: {data.encoding}&quot;)
        image = self.br.imgmsg_to_cv2(data, desired_encoding=&quot;bgr8&quot;)
        self.get_logger().info(f&quot;Image shape: {image.shape}&quot;)
        image = cv2.resize(image, (640, 480))
        cv2.imwrite(&quot;input_image.jpg&quot;, image)
        results = model.predict(image, classes=[0, 2])
        annotated_image = results[0].plot()
        cv2.imwrite(&quot;yolo_output.jpg&quot;, annotated_image)
        cv2.imshow(&apos;YOLOv8 Detection&apos;, annotated_image)

        if cv2.waitKey(1) &amp;#x26; 0xFF == ord(&apos;q&apos;):
            raise KeyboardInterrupt
        self.get_logger().info(f&quot;Detected {len(results[0].boxes)} objects&quot;)
        for box in results[0].boxes:
            self.get_logger().info(f&quot;Detected: class={box.cls}, conf={box.conf}, bbox={box.xyxy}&quot;)
    except Exception as e:
        self.get_logger().error(f&quot;Image processing error: {str(e)}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The callback function: &lt;code&gt;cv2.resize(image, (640, 480))&lt;/code&gt; preprocesses the 1920×1080 image from the SDF config down to 640×480. &lt;code&gt;results = model.predict(image, classes=[0, 2])&lt;/code&gt; runs YOLOv8 with detection limited to classes 0 and 2 (person, car) for faster performance. To detect all 80 classes, use &lt;code&gt;results = model.predict(image)&lt;/code&gt; without the &lt;code&gt;classes&lt;/code&gt; parameter — though this is computationally expensive.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def destroy_node(self):
    cv2.destroyAllWindows()
    super().destroy_node()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Override the node cleanup method to close OpenCV windows and clean up ROS2 resources.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def main(args=None):
    rclpy.init(args=args)
    image_subscriber = ImageSubscriber()
    try:
        rclpy.spin(image_subscriber)
    except KeyboardInterrupt:
        pass
    finally:
        image_subscriber.destroy_node()
        rclpy.shutdown()

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;main()&lt;/code&gt; function initializes the ROS2 environment, spins the subscriber node, and cleans up on exit.&lt;/p&gt;
&lt;h2&gt;Running the Simulation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1
python3 simulation-gazebo

# Terminal 2
cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,0&quot; PX4_SIM_MODEL=gz_x500_depth ./build/px4_sitl_default/bin/px4 -i 0

# Terminal 3
MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start Gazebo, the depth-camera quadrotor, and communication as before. Then run the ROS2-Gazebo bridge:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run ros_gz_image image_bridge /image_raw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This bridges Gazebo&apos;s camera topics to ROS2 &lt;code&gt;sensor_msgs.msg.Image&lt;/code&gt; messages, publishing to the &lt;code&gt;/image_raw&lt;/code&gt; topic we configured in the SDF file.&lt;/p&gt;
&lt;p&gt;Now run YOLOv8 detection and Offboard control:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Terminal 1 — Activate virtual environment and run YOLO
source ~/px4-venv/bin/activate
cd ~/YOLO
python3 uav_camera_det.py

# Terminal 2 — Run Offboard control
ros2 run multioffboardcontrol uav0_1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;The depth-camera quadrotor starts successfully and YOLOv8 detection is active. Since no vehicles or people were placed in the simulation world, no targets are detected — users can add objects to verify detection.&lt;/p&gt;
&lt;p&gt;Note that since the camera is rigidly attached to the drone&apos;s body frame and the Offboard control uses circular motion, the camera rotates with the drone, causing unstable imagery. Using level flight with adjustable camera gimbal would improve detection stability.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In this article, we integrated YOLOv8 for object detection in the ROS2+PX4 simulation environment. Key points include understanding the difference between RGB and depth cameras, ensuring the SDF file has the required topic configuration (add it manually if not), and considerations for practical improvements such as compensating for body-frame camera rotation and improving detection accuracy.&lt;/p&gt;
&lt;p&gt;With both RGB and depth cameras available, the quadrotor can perform object detection, autonomous obstacle avoidance, and more. To integrate visual perception into multi-drone formations, see the earlier &lt;a href=&quot;/en/posts/px4-ros2-multi-offboard&quot;&gt;ROS2+PX4 Multi-Drone Offboard Control&lt;/a&gt; guide — we look forward to seeing your applications.&lt;/p&gt;
&lt;p&gt;Thank you for reading. Feedback, corrections, and suggestions are always welcome.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ultralytics.com/&quot;&gt;Ultralytics YOLO11 Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/monemati/PX4-ROS2-Gazebo-YOLOv8&quot;&gt;GitHub: PX4-ROS2-Gazebo-YOLOv8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>无人机仿真环境调用YOLO的简单示例</title><link>https://duduuu.xyz/zh/posts/px4-ros2-yolo</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-yolo</guid><description>在ROS2+PX4仿真环境中集成YOLOv8实时目标检测，通过Gazebo相机话题实现无人机图像识别</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在无人机编队仿真的第三部分，我们聚焦于将目标识别技术集成到 &lt;a href=&quot;/zh/posts/px4-ros2-gazebo-simulation&quot;&gt;ROS2+PX4 仿真环境&lt;/a&gt; 中，利用YOLOv8实现高效的实时目标检测。YOLO（You Only Look Once）是一种高效、实时的目标检测算法，广泛应用于计算机视觉任务，如无人机目标识别、自动驾驶和监控。&lt;/p&gt;
&lt;p&gt;我们基于PX4 SITL（软件在环仿真）和Gazebo Ignition，结合gz_x500_depth四旋翼模型，展示如何通过ROS2订阅无人机搭载的RGB相机（OakD-Lite）图像，应用YOLOv8模型检测特定目标，并在Ubuntu 22.04环境中实时可视化检测结果。这可以为后续编队协同任务提供更多技术支持。&lt;/p&gt;
&lt;h2&gt;创建Python虚拟环境&lt;/h2&gt;
&lt;p&gt;我们使用的Python版本号为3.10.12，读者可以根据自己的Python版本更改命令。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建虚拟环境
python3 -m venv ~/px4-venv
# 激活
source ~/px4-venv/bin/activate
# 链接ROS2环境
source /opt/ros/humble/setup.bash
export PYTHONPATH=/opt/ros/humble/lib/python3.10/site-packages:$PYTHONPATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装MAVSDK&lt;/h2&gt;
&lt;p&gt;如果后续出现Numpy的版本号冲突的问题，建议先使用 &lt;code&gt;pip uninstall numpy -y&lt;/code&gt; 卸载当前版本，再按照以下命令安装Numpy 1.x版本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install mavsdk
pip install aioconsole
pip install pygame
sudo apt install ros-humble-ros-gzgarden
pip install &quot;numpy&amp;#x3C;2&quot;
pip install opencv-python
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装YOLO&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install ultralytics
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动带相机的四旋翼模型并添加相机话题&lt;/h2&gt;
&lt;p&gt;我们稍微修改一下前两文的启动仿真命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,0&quot; PX4_SIM_MODEL=gz_x500_depth ./build/px4_sitl_default/bin/px4 -i 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们再来看一下这个启动命令，对于x500模型，4001/4002分别对应两种不同的四旋翼配置（&quot;X&quot;型和十字型机架），在此处，我们使用带深度相机的 &lt;code&gt;gz_x500_depth&lt;/code&gt; 的x_500模型，这与之前的有所不同。&lt;/p&gt;
&lt;p&gt;我们前往models所在的文件夹，找到 &lt;code&gt;x500_depth&lt;/code&gt; 的 &lt;code&gt;model.sdf&lt;/code&gt; 文件以及其相机模块 &lt;code&gt;OakD_Lite&lt;/code&gt; 的sdf文件：&lt;/p&gt;
&lt;p&gt;我们发现，相机模块OakD_Lite一共搭载了两个相机传感器，分别为IMX214（RGB相机）和StereoOV7251（深度相机）。其中RGB相机的输出图像格式为RGB_INT8，深度相机的输出图像格式为R_FLOAT32。RGB相机是常用的进行图像识别的传感器，而深度相机用于探测距离，在避障任务中有很好的应用。在此处，我们先使用RGB相机。&lt;/p&gt;
&lt;p&gt;现在我们需要关注sdf文件的这几行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;width&gt;1920&amp;#x3C;/width&gt;
&amp;#x3C;height&gt;1080&amp;#x3C;/height&gt;
&amp;#x3C;format&gt;RGB_INT8&amp;#x3C;/format&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三个是比较重要的参数，代表图像的尺寸和格式，这在后续的代码中需要注意，尤其是图像的格式需要匹配，RGB_INT8是一个三通道的参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;#x3C;always_on&gt;1&amp;#x3C;/always_on&gt;
&amp;#x3C;update_rate&gt;30&amp;#x3C;/update_rate&gt;
&amp;#x3C;visualize&gt;true&amp;#x3C;/visualize&gt;
&amp;#x3C;topic&gt;image_raw&amp;#x3C;/topic&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;always_on&gt;&lt;/code&gt; 代表相机持续保持开启状态，我们在最后为这个相机添加一个ROS2话题（此处为笔者自行添加），名称为 &lt;code&gt;image_raw&lt;/code&gt;，后续我们会利用这个话题进行消息的传输。&lt;/p&gt;
&lt;h2&gt;调用YOLOv8进行目标识别&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/YOLO
cd ~/YOLO
touch uav_camera_det.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个/YOLO目录，现在编写一个调用YOLOv8进行目标检测的Python代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
from ultralytics import YOLO

model = YOLO(&apos;yolov8m.pt&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用一些需要的库和图像转换工具，类似于上一篇文章，rclpy是ROS2的Python客户端，CvBridge是ROS与openCV的图像转换工具，&lt;code&gt;yolov8m.pt&lt;/code&gt; 是中等规模模型（Medium），基于COCO数据集训练，支持80类目标。以下是COCO数据集的80个完整类别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: person         1: bicycle       2: car           3: motorcycle
4: airplane       5: bus           6: train         7: truck
8: boat           9: traffic light 10: fire hydrant 11: stop sign
12: parking meter 13: bench        14: bird         15: cat
16: dog           17: horse        18: sheep        19: cow
20: elephant      21: bear         22: zebra        23: giraffe
24: backpack      25: umbrella     26: handbag      27: tie
28: suitcase      29: frisbee      30: skis         31: snowboard
32: sports ball   33: kite         34: baseball bat 35: baseball glove
36: skateboard    37: surfboard    38: tennis racket 39: bottle
40: wine glass    41: cup          42: fork         43: knife
44: spoon         45: bowl         46: banana       47: apple
48: sandwich      49: orange       50: broccoli     51: carrot
52: hot dog       53: pizza        54: donut        55: cake
56: chair         57: couch        58: potted plant 59: bed
60: dining table  61: toilet       62: tv           63: laptop
64: mouse         65: remote       66: keyboard     67: cell phone
68: microwave     69: oven         70: toaster      71: sink
72: refrigerator  73: book         74: clock        75: vase
76: scissors      77: teddy bear   78: hair drier   79: toothbrush
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了减少计算消耗，提高检测速度，我们往往将检测目标进行缩小，我们稍后讲。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class ImageSubscriber(Node):
    def __init__(self):
        super().__init__(&apos;image_subscriber&apos;)
        self.subscription = self.create_subscription(
            Image,
            &apos;/image_raw&apos;,
            self.listener_callback,
            10)
        self.br = CvBridge()
        self.frame_count = 0
        self.process_interval = 1

        cv2.namedWindow(&apos;YOLOv8 Detection&apos;, cv2.WINDOW_NORMAL)
        cv2.resizeWindow(&apos;YOLOv8 Detection&apos;, 640, 480)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似于上一篇文章使用C++编写的Offboard指令，这里我们定义一个新的类，发布一个名为 &lt;code&gt;image_subscriber&lt;/code&gt; 的ROS2订阅者实例，进行初始化。我们在这里需要注意，在创建订阅者实例时，话题名称要与我们之前修改的RGB相机的sdf文件的话题名称相同，这里为我们之前发布的 &lt;code&gt;/image_raw&lt;/code&gt;，需要匹配。调用CvBridge函数，这是因为我们需要将ROS的图像格式（RGB8）转换为OpenCV的图像格式（BGR8），最后将窗口可视化，以640×480的尺寸进行可视化。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def listener_callback(self, data):
    self.frame_count += 1
    if self.frame_count % self.process_interval != 0:
        self.get_logger().info(&quot;跳过帧&quot;)
        return
    try:
        self.get_logger().info(f&quot;收到视频帧，编码: {data.encoding}&quot;)
        image = self.br.imgmsg_to_cv2(data, desired_encoding=&quot;bgr8&quot;)
        self.get_logger().info(f&quot;图像尺寸: {image.shape}&quot;)
        image = cv2.resize(image, (640, 480))
        cv2.imwrite(&quot;input_image.jpg&quot;, image)
        results = model.predict(image, classes=[0, 2])
        annotated_image = results[0].plot()
        cv2.imwrite(&quot;yolo_output.jpg&quot;, annotated_image)
        cv2.imshow(&apos;YOLOv8 Detection&apos;, annotated_image)

        if cv2.waitKey(1) &amp;#x26; 0xFF == ord(&apos;q&apos;):
            raise KeyboardInterrupt
        self.get_logger().info(f&quot;检测到 {len(results[0].boxes)} 个目标&quot;)
        for box in results[0].boxes:
            self.get_logger().info(f&quot;检测到: 类别={box.cls}, 置信度={box.conf}, 坐标={box.xyxy}&quot;)
    except Exception as e:
        self.get_logger().error(f&quot;处理图像错误: {str(e)}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定义回调函数，&lt;code&gt;image = cv2.resize(image, (640, 480))&lt;/code&gt; 是用于图像的预处理的函数，将我们的sdf里定义的1920×1080的图像压缩为640×480。随后使用 &lt;code&gt;results = model.predict(image, classes=[0, 2])&lt;/code&gt; 运行YOLOv8，限制检测类别为0,2（对应人、车），这样可以提高检测的速度。如果要检测其他物体，可以按照上述的80个类别编号进行添加。如果80个类别都要检测，可以使用 &lt;code&gt;results = model.predict(image)&lt;/code&gt; 实现全部检测，最后进行可视化，但这样肯定会极大地浪费计算效率。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def destroy_node(self):
    cv2.destroyAllWindows()
    super().destroy_node()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重写一个销毁ROS2节点的方法，关闭OpenCV的窗口，清理ROS2节点和环境。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def main(args=None):
    rclpy.init(args=args)
    image_subscriber = ImageSubscriber()
    try:
        rclpy.spin(image_subscriber)
    except KeyboardInterrupt:
        pass
    finally:
        image_subscriber.destroy_node()
        rclpy.shutdown()

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后执行 &lt;code&gt;main()&lt;/code&gt; 函数，实现对话题的订阅以及调用YOLOv8实现无人机的目标识别及检测。至此，我们进行ROS2+PX4+YOLOv8的目标识别任务的准备就做好了。&lt;/p&gt;
&lt;h2&gt;实现仿真&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 终端1
python3 simulation-gazebo

# 终端2
cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,0&quot; PX4_SIM_MODEL=gz_x500_depth ./build/px4_sitl_default/bin/px4 -i 0

# 终端3
MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照之前的步骤启动Gazebo仿真、带深度相机的四旋翼模型以及打开通信，现在我们需要运行这个命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run ros_gz_image image_bridge /image_raw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是一个将ROS2与Gazebo进行桥接的命令，可以将Gazebo里的话题消息转换为ROS2的 &lt;code&gt;sensor_msgs.msg.Image&lt;/code&gt; 消息，并发布到ROS2话题。此时我们订阅的就是我们之前在sdf文件里添加的 &lt;code&gt;/image_raw&lt;/code&gt; 话题。&lt;/p&gt;
&lt;p&gt;运行这个命令后，我们执行一下我们的YOLOv8检测的代码，并且运行一下我们的Offboard控制，让飞机飞起来，在可视化窗口查看一下摄像头和检测信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 终端1 — 激活虚拟环境并运行YOLO检测
source ~/px4-venv/bin/activate
cd ~/YOLO
python3 uav_camera_det.py

# 终端2 — 运行Offboard控制
ros2 run multioffboardcontrol uav0_1
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结果展示&lt;/h2&gt;
&lt;p&gt;我们发现，带深度相机的四旋翼飞机成功启动仿真，并且YOLOv8成功启动了检测。由于笔者的世界搭建里还没有加入车和人，所以没有目标探测到，读者朋友们可以进行添加并验证。&lt;/p&gt;
&lt;p&gt;同时，由于相机是固连在机身坐标系上，而作者此时的Offboard模式使用的是盘旋指令，因此相机会跟着飞机姿态进行旋转，图像不稳定。后续可以采取平飞+调整相机姿态的方式实现更稳定的目标识别。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在这篇文章里，我们通过调用YOLOv8实现了在ROS2+PX4中进行目标检测的任务。需要着重注意的是相机种类（RGB相机和深度相机）的区别，同时注意sdf文件里是否有相应的话题配置，如果没有，需要手动添加。在仿真过程中我们发现还有一些细节可以调整，例如如何适应相机随着机身坐标系的偏转，如何提高识别成功率等，读者朋友们可以在实际应用场景中进行改进。&lt;/p&gt;
&lt;p&gt;在后续的任务中，搭配RGB相机和深度相机的四旋翼飞机可以实现场景下的目标识别、自主避障（深度相机）等任务。如果需要在多机编队中集成视觉感知，可以参考前文的 &lt;a href=&quot;/zh/posts/px4-ros2-multi-offboard&quot;&gt;ROS2+PX4 多机 Offboard 控制&lt;/a&gt;，将目标检测与编队飞行结合。期待大家的实际应用。&lt;/p&gt;
&lt;p&gt;最后，再次感谢读者朋友们的支持和耐心的阅读，欢迎大家对本文进行批评指正、补充及建议。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.ultralytics.com/&quot;&gt;Ultralytics YOLO11&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/monemati/PX4-ROS2-Gazebo-YOLOv8&quot;&gt;GitHub: PX4-ROS2-Gazebo-YOLOv8&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2+PX4 Drone Formation Simulation Setup Guide</title><link>https://duduuu.xyz/en/posts/px4-ros2-gazebo-simulation</link><guid isPermaLink="true">https://duduuu.xyz/en/posts/px4-ros2-gazebo-simulation</guid><description>Step-by-step guide to set up ROS2 Humble + PX4 + Gazebo Harmonic simulation environment for single drone Offboard control and multi-drone formation</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;PX4, as one of the most popular open-source flight controllers worldwide, provides convenience for developers in drone design and control. ROS, as a robot operating system, simplifies the drone formation simulation workflow through its distributed communication architecture, while the Gazebo physics simulation engine provides a realistic simulation environment.&lt;/p&gt;
&lt;p&gt;ROS has now been updated to ROS2, which offers greater applicability and maintainability compared to ROS1. There are currently few ROS2+PX4 simulation tutorials available, and continuous updates of both platforms bring many environment configuration challenges. This article provides a personal development workflow.&lt;/p&gt;
&lt;h2&gt;Installing Ubuntu&lt;/h2&gt;
&lt;p&gt;This environment is built on &lt;strong&gt;Ubuntu 22.04&lt;/strong&gt;. You can install it as a virtual machine or set up a dual-boot system alongside Windows. Here is a video tutorial for installing Ubuntu 22.04 as a dual-boot system on Windows 11:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1wo4y177Gk/&quot;&gt;Step-by-step dual-boot installation: Windows 11 + Ubuntu 22.04&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we begin the environment setup process. For reference, here is another developer&apos;s workflow that is similar to ours and quite concise. If you only need single-drone simulation, this tutorial is also a good reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_55944949/article/details/140627640&quot;&gt;Ubuntu PX4 Drone Simulation Environment Setup (5) -- Simulation Environment Setup (with Ubuntu 22.04, ROS2 Humble)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Before starting the following steps, ensure you have reliable internet access (with proper network tools if needed), otherwise cloning repositories from GitHub may fail midway.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Downloading &amp;#x26; Compiling PX4 Source&lt;/h2&gt;
&lt;p&gt;First, clone the PX4 source code from GitHub and update all submodules:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/PX4/PX4-Autopilot.git --recursive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the subsequent environment setup will install many packages, and it may be difficult to fully clean them up if something goes wrong, it is recommended to back up the &lt;code&gt;~/PX4-Autopilot&lt;/code&gt; directory at this point, so you don&apos;t need to re-clone the entire repository if the environment needs a fresh start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zip -r PX4-Autopilot.zip PX4-Autopilot/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, install the required dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bash ./PX4-Autopilot/Tools/setup/ubuntu.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the PX4 source code is downloaded and all dependencies are installed. We can now try compiling and launching the simulation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot/
make px4_sitl         # Build the SITL firmware first (required!)
make px4_sitl jmavsim # Once built, launch jmavsim
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common error&lt;/strong&gt;: If you run &lt;code&gt;make px4_sitl jmavsim&lt;/code&gt; directly and encounter &lt;code&gt;ninja: error: unknown target &apos;jmavsim&apos;&lt;/code&gt;, it means the PX4 CMake/Ninja build system hasn&apos;t generated the jmavsim target yet. You must &lt;strong&gt;run &lt;code&gt;make px4_sitl&lt;/code&gt; first&lt;/strong&gt; to complete the initial build, then &lt;code&gt;make px4_sitl jmavsim&lt;/code&gt; to launch the simulator. After this first-time setup, &lt;code&gt;make px4_sitl jmavsim&lt;/code&gt; will work directly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You will see the PX4 source code compile and the jmavsim physics simulation engine start up. This works because the &lt;code&gt;ubuntu.sh&lt;/code&gt; dependency script already installed jmavsim and Gazebo-classic for us. If you see green &lt;code&gt;ready for takeoff&lt;/code&gt; text in the terminal and the jmavsim window appears, compilation is successful. If this step fails, check your network tools.&lt;/p&gt;
&lt;p&gt;Now press Enter in the terminal, and at the &lt;code&gt;pxh&gt;&lt;/code&gt; prompt, enter PX4 commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commander takeoff/land/shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These commands control the drone&apos;s takeoff, landing, and shutdown of the simulation. You should see the drone taking off and landing in the jmavsim UI. At this point, the PX4 source download and compilation are complete.&lt;/p&gt;
&lt;h2&gt;Installing ROS2 and Related Dependencies&lt;/h2&gt;
&lt;p&gt;We use &lt;strong&gt;ROS2 Humble&lt;/strong&gt; for development. The installation and environment configuration for ROS2 and its dependencies can be somewhat tedious, so I &lt;strong&gt;strongly recommend&lt;/strong&gt; using the &quot;FishROS&quot; one-click installation tool. Many thanks to FishROS for providing this convenience for the ROS community. Simply run the following command in your Ubuntu terminal and follow the on-screen instructions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget http://fishros.com/install -O fishros &amp;#x26;&amp;#x26; . fishros
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one-click installation tool also includes options for Docker and other tools -- feel free to install them as needed. Again, many thanks. Additionally, developers using the ROS2 Foxy distribution can also use this tool for installation and environment configuration.&lt;/p&gt;
&lt;h2&gt;Installing Gazebo Sim 8.9.0&lt;/h2&gt;
&lt;p&gt;According to the official PX4 documentation, the former Gazebo Ignition has been renamed to Gazebo, and the old Gazebo is now called Gazebo-classic. Ubuntu 22.04 supports the Gazebo (Ignition) series. When we installed PX4 dependencies earlier, &lt;code&gt;ubuntu.sh&lt;/code&gt; automatically installed jmavsim and Gazebo-classic, but for multi-drone simulation we need &lt;strong&gt;Gazebo Harmonic&lt;/strong&gt; (v8.x, i.e., Gazebo Sim 8.9.0).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: The Gazebo version and the ROS2 bridge package must match. Mixing multiple versions is the primary cause of environment crashes. Gazebo Garden (v7), Harmonic (v8), and Ionic (v9) &lt;strong&gt;cannot coexist&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;First, add the OSRF repository and install Gazebo Harmonic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
sudo apt install wget
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main&quot; | sudo tee /etc/apt/sources.list.d/gazebo-stable.list &gt; /dev/null
sudo apt update
sudo apt-get install gz-harmonic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then install the corresponding Gazebo-ROS2 communication bridge (Harmonic maps to &lt;code&gt;gzharmonic&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install ros-humble-ros-gzharmonic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installation, verify version uniqueness (only one version number should appear):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gz sim --versions   # Should only show 8.9.0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common mistake&lt;/strong&gt;: Do not install &lt;code&gt;gz-garden&lt;/code&gt; or &lt;code&gt;ros-humble-ros-gzgarden&lt;/code&gt;. That is Gazebo Garden (v7), which is incompatible with PX4&apos;s current &lt;code&gt;gz-sim8&lt;/code&gt; compilation target.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Installing MicroXRCE-DDS Agent&lt;/h2&gt;
&lt;p&gt;According to the official documentation, in ROS2, PX4 uses the uXRCE-DDS middleware to allow publishing and subscribing to uORB messages on the companion computer, replacing MAVROS. We follow the official example to download, compile, and install.&lt;/p&gt;
&lt;p&gt;Download the source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone -b v2.4.2 https://github.com/eProsima/Micro-XRCE-DDS-Agent.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compile:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd Micro-XRCE-DDS-Agent
mkdir build
cd build
cmake ..
make
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;There will likely be a build error here. Go to &lt;code&gt;build/fastdds/tmp/fastdds-gitclone.cmake&lt;/code&gt;, change &lt;code&gt;2.12.x&lt;/code&gt; to &lt;code&gt;v2.12.1&lt;/code&gt;, then re-run &lt;code&gt;make&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The compilation process takes a while, so be patient. After compilation finishes, install:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo make install
sudo ldconfig /usr/local/lib/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Installing QGC Ground Station&lt;/h2&gt;
&lt;p&gt;When starting the PX4+Gazebo simulation, losing connection to the ground station will prevent PX4 from controlling the drone. Therefore, you need to install QGC Ground Station. Here is a CSDN guide that walks through the steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_55944949/article/details/130895363&quot;&gt;(Latest) Ubuntu PX4 Drone Simulation Environment Setup (3) -- Installing QGC Ground Station on Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Single Drone Offboard Test and Development Guide&lt;/h2&gt;
&lt;p&gt;All development environment components are now set up. Let&apos;s verify PX4 Offboard mode.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Offboard mode&lt;/strong&gt; is a special flight mode in PX4 that allows external systems (such as companion computers, ROS2 nodes, etc.) to send control commands directly via MAVLink or uXRCE-DDS, enabling real-time control of the drone&apos;s attitude, position, or velocity for autonomous flight or advanced missions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;First, create a ROS2 workspace:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/ros2_ws/src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then download the source code (make sure your network is working):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
git clone https://github.com/PX4/px4_msgs.git -b release/1.14
git clone https://github.com/PX4/px4_ros_com.git -b release/v1.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compile:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
colcon build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update environment and configure environment variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;source ~/ros2_ws/install/setup.bash&quot; &gt;&gt; ~/.bashrc
echo &quot;export GZ_SIM_RESOURCE_PATH=\$HOME/.simulation-gazebo/models:\$GZ_SIM_RESOURCE_PATH&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;Launching the Offboard Simulation&lt;/h3&gt;
&lt;p&gt;Now we start the Offboard simulation. First, open the already installed &lt;strong&gt;QGC Ground Station&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terminal 1&lt;/strong&gt; -- Start communication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Terminal 2&lt;/strong&gt; -- Start simulation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
make px4_sitl gz_x500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the ground station will connect to PX4, Gazebo will launch, and the x500 drone will appear. When the PX4 terminal shows green &lt;code&gt;ready for takeoff&lt;/code&gt;, the simulation has started successfully. We will explain this &lt;code&gt;make&lt;/code&gt;-based simulation startup in more detail in the multi-drone simulation section.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Terminal 3&lt;/strong&gt; -- Run Offboard control source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run px4_ros_com offboard_control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the drone ascend 5 meters in Gazebo, confirming Offboard mode control is successful.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Offboard Mode Development Workflow&lt;/h3&gt;
&lt;p&gt;In the &lt;code&gt;~/ros2_ws&lt;/code&gt; workspace we just created, navigate to the &lt;code&gt;px4_ros_com&lt;/code&gt; directory and open &lt;code&gt;CMakeLists.txt&lt;/code&gt;. Find these three lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(offboard_control src/examples/offboard/offboard_control.cpp)
ament_target_dependencies(offboard_control rclcpp px4_msgs)
install(TARGETS offboard_control DESTINATION lib/${PROJECT_NAME})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This compiles &lt;code&gt;offboard_control.cpp&lt;/code&gt; from &lt;code&gt;src/examples/offboard&lt;/code&gt; into an executable named &lt;code&gt;offboard_control&lt;/code&gt;, with its dependency on &lt;code&gt;px4_msgs&lt;/code&gt;. You can open the &lt;code&gt;src/examples/offboard&lt;/code&gt; directory to find the &lt;code&gt;offboard_control.cpp&lt;/code&gt; source code.&lt;/p&gt;
&lt;p&gt;Therefore, to develop single-drone Offboard mode simulations, follow this workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inside the already created and configured &lt;code&gt;~/ros2_ws&lt;/code&gt; ROS2 workspace, create a new package (at the same level as the &lt;code&gt;px4_ros_com&lt;/code&gt; directory).&lt;/li&gt;
&lt;li&gt;In the new package, write your Offboard mode control code (C++/Python) under the &lt;code&gt;/src&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;In the new package&apos;s &lt;code&gt;CMakeLists.txt&lt;/code&gt;, find these lines:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(offboard_control src/examples/offboard/offboard_control.cpp)
ament_target_dependencies(offboard_control rclcpp px4_msgs)
install(TARGETS offboard_control DESTINATION lib/${PROJECT_NAME})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Change the path to the location of your C++/Python code, and give it an executable name. &lt;strong&gt;Before running the simulation, you must recompile the ROS2 workspace!&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
colcon build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, following the earlier method, open QGC Ground Station, start communication and simulation, then open a new terminal and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run &amp;#x3C;package_name&gt; &amp;#x3C;executable_name&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see the drone flying according to your code logic in Gazebo, the simulation is successful.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Supplementary Notes&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Supplement 1&lt;/strong&gt;: When using the &lt;code&gt;make px4_sitl gz_x500&lt;/code&gt; command to start the simulation, the automatically launched &lt;code&gt;gz sim&lt;/code&gt; simulator world is the default world. Its sdf file is no longer in &lt;code&gt;~/.gz/worlds&lt;/code&gt;, but in &lt;code&gt;./PX4-Autopilot/Tools/simulation/gz/worlds&lt;/code&gt;. Similarly, all models are also located under &lt;code&gt;Tools/simulation&lt;/code&gt;. Therefore, when building your simulation environment, you should go to these directories to directly modify the sdf files.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Supplement 2&lt;/strong&gt;: When writing C++/Python code for Offboard mode control, pay attention to coordinate system transformations. In PX4, the coordinate system is &lt;strong&gt;NED&lt;/strong&gt; (North-East-Down), meaning the x-axis points north, the y-axis points east, and the z-axis points down (all altitudes are negative). In Gazebo Ignition (now Gazebo), the coordinate system is &lt;strong&gt;ENU&lt;/strong&gt; (East-North-Up), meaning the x-axis points east, the y-axis points north, and the z-axis points up (altitudes are positive). Make sure to account for coordinate conversions when writing your control logic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At this point, the basic workflow for ROS2+PX4 single-drone Offboard mode simulation is fully covered. If you have successfully run through the above workflow, you can now proceed with further single-drone simulation development.&lt;/p&gt;
&lt;h2&gt;Multi-Drone (Formation) Development Workflow&lt;/h2&gt;
&lt;p&gt;In the previous section, we completed the single-drone simulation environment setup using ROS2+PX4. Now we proceed to the multi-drone environment setup in preparation for formation control logic simulation.&lt;/p&gt;
&lt;h3&gt;Reviewing Single-Drone Startup&lt;/h3&gt;
&lt;p&gt;Let&apos;s first review the primary command for starting a single-drone simulation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
make px4_sitl gz_x500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;code&gt;make&lt;/code&gt; command actually performs three tasks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Compiles&lt;/strong&gt; the PX4 source code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starts&lt;/strong&gt; PX4&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Launches&lt;/strong&gt; the Gazebo simulation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;However, for multi-drone simulation, we cannot simply run &lt;code&gt;make&lt;/code&gt; repeatedly -- that would cause each x500 drone to launch its own separate Gazebo instance, which is not what we want. Therefore, for multi-drone simulation in the Gazebo environment, our goals are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start Gazebo simulation separately&lt;/li&gt;
&lt;li&gt;Launch PX4 instances individually, adding drones to the Gazebo environment&lt;/li&gt;
&lt;li&gt;Establish communication&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Starting Gazebo Separately&lt;/h3&gt;
&lt;p&gt;First, we need to download the Gazebo launch script (&lt;code&gt;simulation-gazebo&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget https://raw.githubusercontent.com/PX4/PX4-gazebo-models/main/simulation-gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, in the directory where the script is located, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 simulation-gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;: If the Gazebo window fails to open (crash or black screen), it&apos;s usually due to incompatible GPU drivers or a VM without GPU passthrough. Prefix the command with &lt;code&gt;LIBGL_ALWAYS_SOFTWARE=1&lt;/code&gt; to force CPU software rendering:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;LIBGL_ALWAYS_SOFTWARE=1 python3 simulation-gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This environment variable tells Mesa/OpenGL to use the LLVMpipe software rasterizer instead of hardware GPU acceleration. The tradeoff is lower frame rate, but simulation functionality is unaffected.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first time you run the script, it will download some components into &lt;code&gt;.simulation-gazebo&lt;/code&gt; (use &lt;code&gt;Ctrl+H&lt;/code&gt; in your home directory to reveal hidden folders). The &lt;code&gt;/worlds/default.sdf&lt;/code&gt; file inside is the simulation world sdf file -- you will modify this file later.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Supplement 3&lt;/strong&gt;: Note the distinction from Supplement 1 in the single-drone simulation section. This is a significant difference between single-drone and multi-drone setup. When starting a single-drone simulation, we use the &lt;code&gt;make&lt;/code&gt; command, and the world sdf file is in &lt;code&gt;./PX4-Autopilot/Tools/simulation/gz/worlds&lt;/code&gt;. However, if you examine the &lt;code&gt;simulation-gazebo.py&lt;/code&gt; script we downloaded for multi-drone simulation, you will find that the world sdf file is under &lt;code&gt;.simulation-gazebo&lt;/code&gt;. Make sure to distinguish between these two.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Adding Multiple Drones&lt;/h3&gt;
&lt;p&gt;After starting the Gazebo environment separately, we add drones to it. This time we do not use the &lt;code&gt;make&lt;/code&gt; command, but the following command instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,2&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Parameter explanation:&lt;/p&gt;
&lt;p&gt;| Parameter | Description |
|-----------|-------------|
| &lt;code&gt;PX4_GZ_STANDALONE=1&lt;/code&gt; | Use standalone mode (start only PX4 without launching its own simulation, connect to the separately started simulation) |
| &lt;code&gt;PX4_SYS_AUTOSTART=4001&lt;/code&gt; | Required airframe configuration field |
| &lt;code&gt;PX4_GZ_MODEL_POSE=&quot;0,2&quot;&lt;/code&gt; | Initial drone position -- different drones should have different positions |
| &lt;code&gt;PX4_SIM_MODEL=gz_x500&lt;/code&gt; | Drone model is x500 |
| &lt;code&gt;-i 0&lt;/code&gt; | Drone instance ID (ID 0) |&lt;/p&gt;
&lt;p&gt;After running this command, you should see a drone appear in the previously launched Gazebo window.&lt;/p&gt;
&lt;p&gt;Now add a second drone (ID &lt;code&gt;-i 1&lt;/code&gt;, position &lt;code&gt;&quot;0,1&quot;&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,1&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To add more drones, follow the same pattern, or write a unified script to add them all at once.&lt;/p&gt;
&lt;p&gt;Now you should see two drones side by side in Gazebo. In each drone&apos;s PX4 terminal, you can enter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commander takeoff/land
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to observe each drone&apos;s takeoff and landing. Finally, add the communication:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;At this point, the entire environment setup for ROS2+PX4 single-drone and multi-drone simulation in Gazebo is complete. In the next article, we build on this environment to implement &lt;a href=&quot;/en/posts/px4-ros2-multi-offboard&quot;&gt;ROS2+PX4 Multi-Drone Offboard Control&lt;/a&gt;, achieving dual-drone formation flight using a leader-follower algorithm. Beyond that, you can also pursue target recognition, path planning, and many other projects. The first step is always the hardest -- I hope this article can contribute even a small amount to research in this direction.&lt;/p&gt;
&lt;p&gt;If you still encounter simulation issues, please refer to the official PX4 documentation and related GitHub projects. Also, if you&apos;re interested in Betaflight, check out the &lt;a href=&quot;/en/posts/px4-ros2-betaflight-sitl&quot;&gt;Betaflight + Gazebo SITL Tutorial&lt;/a&gt; for an alternative SITL simulation approach. Environment configuration and workflow familiarization is a simple but tedious process. As a beginner, I have provided my personal development workflow, and there are certainly better approaches out there. Corrections and suggestions for better methods are always welcome -- they will help those who come after us.&lt;/p&gt;
&lt;h2&gt;Gazebo Environment Troubleshooting Guide&lt;/h2&gt;
&lt;p&gt;Based on bugs reported over the past year, here is a summary of troubleshooting and repair methods for dependency conflicts caused by multiple coexisting Gazebo versions.&lt;/p&gt;
&lt;h3&gt;1. Check Which Gazebo Versions Are Installed&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# List all Gazebo-related packages
dpkg -l | grep -E &quot;^ii&quot; | awk &apos;{print $2}&apos; | grep -E &quot;^gz-|^libgz-|^sdformat|^libsdformat|python3-gz&quot; | sort

# Check available Gazebo Sim versions (multiple versions = conflict)
gz sim --versions

# Check the current default version
gz sim --version
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Healthy state: &lt;code&gt;gz sim --versions&lt;/code&gt; should output only one version number (e.g., &lt;code&gt;8.9.0&lt;/code&gt;), and all &lt;code&gt;libgz-*-X&lt;/code&gt; should have the same major version (e.g., all 7 or all 8).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. Typical Symptoms of Version Conflicts&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Symptom 1: Multiple versions coexist
gz sim --versions
# Output:
# 8.9.0
# 7.9.0        ← Garden and Harmonic coexist -- conflict!

# Symptom 2: CMake finds the wrong version
grep &quot;gz-sim_DIR&quot; ~/PX4-Autopilot/build/px4_sitl_default/CMakeCache.txt
# Should match gz sim --version: Harmonic → gz-sim8, Garden → gz-sim7

# Symptom 3: ROS2 bridge package version mismatch
dpkg -l | grep ros-humble-ros-gz
# Should only have gzharmonic or only gzgarden, not both simultaneously
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Fix: Unify to a Single Gazebo Version&lt;/h3&gt;
&lt;p&gt;Using Harmonic (v8) as an example (recommended for ROS2 Humble, as it has a corresponding bridge package):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. Remove all Garden (v7) packages
sudo apt-get purge -y gz-garden gz-sim7-cli gz-launch6-cli gz-transport12-cli \
  libgz-sim7 libgz-sim7-dev libgz-sim7-plugins \
  libgz-launch6 libgz-launch6-dev \
  libgz-transport12 libgz-transport12-dev \
  libgz-fuel-tools8 libgz-fuel-tools8-dev \
  libgz-gui7 libgz-gui7-dev \
  libgz-msgs9 libgz-msgs9-dev \
  libgz-physics6 libgz-physics6-dev \
  libgz-rendering7 libgz-rendering7-dev \
  libgz-sensors7 libgz-sensors7-dev \
  libsdformat13 libsdformat13-dev sdformat13-sdf \
  python3-gz-sim7

# 2. Remove Garden ROS2 bridge
sudo apt-get purge -y ros-humble-ros-gzgarden*

# 3. Install Harmonic ROS2 bridge
sudo apt-get install -y ros-humble-ros-gzharmonic

# 4. Verify
gz sim --versions  # Should only show 8.9.0
dpkg -l | grep gz-garden    # Should output nothing
dpkg -l | grep gz-harmonic  # Should show as installed
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Common &lt;code&gt;.bashrc&lt;/code&gt; Issues&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Problem 1: Duplicate source entries (repeated appending inflates PATH)
grep &quot;source.*setup.bash&quot; ~/.bashrc | sort | uniq -c
# If any line appears more than once, manually clean up ~/.bashrc

# Problem 2: Incorrect ROS2 loading order
# Correct order: source /opt/ros/humble/setup.bash first, then source ~/ros2_ws/install/setup.bash
# The workspace overlay must come after the base environment

# Problem 3: Model path variable
# Gazebo Garden/Harmonic use GZ_SIM_RESOURCE_PATH, not GAZEBO_MODEL_PATH
echo $GZ_SIM_RESOURCE_PATH  # Should point to your model directory
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Check if PX Build Target Matches Gazebo Version&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# PX4 CMake cache records the gz-sim version
grep &quot;gz-sim_DIR&quot; ~/PX4-Autopilot/build/px4_sitl_default/CMakeCache.txt
# gz-sim8 → Harmonic
# gz-sim7 → Garden

# If the version does not match the installed Gazebo, recompile PX4:
cd ~/PX4-Autopilot
make clean
make px4_sitl gz_x500
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. Check ROS2 Bridge Package Dependencies&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Confirm the bridge package&apos;s SDFormat dependency matches the Gazebo version
apt-cache depends ros-humble-ros-gzharmonic | grep sdformat
# Harmonic → libsdformat14-dev

# Confirm no residual Garden bridge packages
dpkg -l ros-humble-ros-gzgarden* 2&gt;/dev/null
# Should show &quot;no packages found&quot; or status &quot;un&quot; (uninstalled)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/en/ros2/user_guide.html&quot;&gt;PX4 Official Documentation -- ROS 2 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_55944949/article/details/140627640&quot;&gt;Ubuntu PX4 Drone Simulation Environment Setup (5) -- Simulation Environment Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/PX4/PX4-Autopilot/issues/24033&quot;&gt;Gazebo Simulator Issue [Bug] WARN [health_and_arming_checks] Preflight Fail: ekf2 missing data -- Issue #24033&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Jinyao-Chen/ROS2-PX4-multivehicles-sim-tutorial&quot;&gt;GitHub: ROS2-PX4-multivehicles-sim-tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item><item><title>ROS2+PX4 仿真环境开发教程</title><link>https://duduuu.xyz/zh/posts/px4-ros2-gazebo-simulation</link><guid isPermaLink="true">https://duduuu.xyz/zh/posts/px4-ros2-gazebo-simulation</guid><description>从零搭建 ROS2 Humble + PX4 + Gazebo Harmonic 仿真环境，涵盖单机 Offboard 控制与多机编队流程，附排错指南</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;PX4 作为目前全世界广泛流行的开源飞控，在无人机设计和控制方面为开发人员带来便利。ROS 作为机器人操作系统，其提供的分布式通信架构简化了无人机编队仿真流程，通过 Gazebo 物理仿真引擎，可以提供真实的仿真环境。&lt;/p&gt;
&lt;p&gt;ROS 目前已经更新到了 ROS2，相较于 ROS1，其应用性和可维护性会更加强大。目前已有的 ROS2+PX4 的仿真案例并不多，且伴随二者的不断更新，有很多环境配置的问题。笔者在这里提供一种个人的开发流程。&lt;/p&gt;
&lt;h2&gt;Ubuntu 系统的安装&lt;/h2&gt;
&lt;p&gt;本环境基于 Ubuntu 22.04 系统进行搭建。大家可以安装虚拟机或者在 Windows 系统中安装双系统。在这里，笔者给出一个在 Windows11 下安装 Ubuntu 22.04 双系统的视频教程链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1wo4y177Gk/&quot;&gt;手把手教你安装双系统 Windows11+Ubuntu 22.04&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在开始进行环境开发流程，这里给出一个博主的环境开发流程，与我们的环境搭建较为相似，简洁明了。如果只有单机仿真需求，也可以参考这篇资料进行环境搭建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_55944949/article/details/140627640&quot;&gt;Ubuntu 搭建 PX4 无人机仿真环境(5) -- 仿真环境搭建(以 Ubuntu 22.04, ROS2 Humble 为例)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：在开始下列步骤之前，建议首先保证能够科学上网，且网速流畅，否则后续从 Github 克隆仓库时会中途闪退。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;PX4 源码下载及编译&lt;/h2&gt;
&lt;p&gt;首先从 Github 上克隆 PX4 源码，并且更新子模块：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/PX4/PX4-Autopilot.git --recursive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于后续环境配置会装很多东西，有可能难以完全清干净，建议在这一步完成后运行以下命令，备份 &lt;code&gt;~/PX4-Autopilot&lt;/code&gt;，以免后续环境配错需要重新克隆代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;zip -r PX4-Autopilot.zip PX4-Autopilot/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着安装相关依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bash ./PX4-Autopilot/Tools/setup/ubuntu.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 PX4 的源码已经下载好并且安装好相关依赖了，我们可以试着运行以下命令进行编译及启动仿真：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot/
make px4_sitl         # 先编译 SITL 固件（必须！）
make px4_sitl jmavsim # 编译完成后启动 jmavsim 仿真
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;常见错误&lt;/strong&gt;：如果直接运行 &lt;code&gt;make px4_sitl jmavsim&lt;/code&gt; 出现 &lt;code&gt;ninja: error: unknown target &apos;jmavsim&apos;&lt;/code&gt;，说明 PX4 的 CMake/Ninja 构建系统尚未生成 jmavsim 目标。必须&lt;strong&gt;先执行 &lt;code&gt;make px4_sitl&lt;/code&gt;&lt;/strong&gt; 完成首次编译，再运行 &lt;code&gt;make px4_sitl jmavsim&lt;/code&gt; 启动仿真，之后便可直接使用后者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们会发现，开始编译 PX4 的源码，并且开始启动 jmavsim 物理仿真引擎，这是因为在我们安装依赖时就已经为我们装好了 jmavsim 和 Gazebo-classic。如果发现终端中出现了绿色的 &lt;code&gt;ready for takeoff&lt;/code&gt;，且 jmavsim 出现，则说明编译成功。如果这一步失败，请检查自己的科学上网工具。&lt;/p&gt;
&lt;p&gt;现在我们在终端中回车，在 &lt;code&gt;pxh&gt;&lt;/code&gt; 后面输入 PX4 的指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commander takeoff/land/shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则可以控制无人机的起飞、降落以及关闭仿真。此时可以在 jmavsim 的 UI 中看到无人机的起降。到此步，PX4 的源代码下载和编译全部完成。&lt;/p&gt;
&lt;h2&gt;ROS2 及其相关依赖的安装&lt;/h2&gt;
&lt;p&gt;我们使用 ROS2 Humble 进行开发，ROS2 及其依赖的安装和环境配置有些繁琐，在此我&lt;strong&gt;强烈建议&lt;/strong&gt;各位使用&quot;鱼香 ROS&quot;的一键安装功能，在这里感谢其为 ROS 开发提供的便利。直接在 Ubuntu 的系统终端输入以下命令并按终端返回的指令安装即可：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget http://fishros.com/install -O fishros &amp;#x26;&amp;#x26; . fishros
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其一键安装工具中还有 Docker 等工具的一键安装选项，大家可以按需使用，再次感谢。另外，使用 ROS2 Foxy 版本进行开发的朋友也可以使用该工具进行安装和环境配置。&lt;/p&gt;
&lt;h2&gt;Gazebo Sim 8.9.0 的安装&lt;/h2&gt;
&lt;p&gt;根据 PX4 的官方文档，之前的 Gazebo Ignition 更名为 Gazebo，以前的 Gazebo 现在叫 Gazebo-classic。Ubuntu 22.04 支持 Gazebo (Ignition) 系列。在之前安装 PX4 依赖时，&lt;code&gt;ubuntu.sh&lt;/code&gt; 会自动安装 jmavsim 和 Gazebo-classic，而我们多机仿真需要使用的是 &lt;strong&gt;Gazebo Harmonic&lt;/strong&gt;（v8.x，即 Gazebo Sim 8.9.0）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;重要&lt;/strong&gt;：Gazebo 的版本与 ROS2 桥接包必须一致，混装多个版本是环境崩溃的首要原因。Gazebo Garden (v7)、Harmonic (v8)、Ionic (v9) &lt;strong&gt;不能共存&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先添加 OSRF 源并安装 Gazebo Harmonic：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
sudo apt install wget
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main&quot; | sudo tee /etc/apt/sources.list.d/gazebo-stable.list &gt; /dev/null
sudo apt update
sudo apt-get install gz-harmonic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后安装对应版本的 Gazebo-ROS2 通信桥（Harmonic 对应 &lt;code&gt;gzharmonic&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install ros-humble-ros-gzharmonic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后验证版本唯一性（只应出现一个版本号）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gz sim --versions   # 应只显示 8.9.0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;常见错误&lt;/strong&gt;：不要安装 &lt;code&gt;gz-garden&lt;/code&gt; 或 &lt;code&gt;ros-humble-ros-gzgarden&lt;/code&gt;，那是 Gazebo Garden (v7)，与 PX4 当前的 &lt;code&gt;gz-sim8&lt;/code&gt; 编译目标不兼容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;MicroXRCE-DDS Agent 的安装&lt;/h2&gt;
&lt;p&gt;根据官方文档说明，在 ROS2 中 PX4 使用 uXRCE-DDS 中间件来允许在配套计算机上发布和订阅 uORB 消息，而不再使用 MAVROS，我们按照官方案例进行下载、编译及安装。&lt;/p&gt;
&lt;p&gt;下载源码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone -b v2.4.2 https://github.com/eProsima/Micro-XRCE-DDS-Agent.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd Micro-XRCE-DDS-Agent
mkdir build
cd build
cmake ..
make
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里应该会有个报错，大家去 &lt;code&gt;build/fastdds/tmp/fastdds-gitclone.cmake&lt;/code&gt; 里把 &lt;code&gt;2.12.x&lt;/code&gt; 改为 &lt;code&gt;v2.12.1&lt;/code&gt; 后重新 &lt;code&gt;make&lt;/code&gt; 一下就可以了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;编译的过程会较长，需要进行一定等待，编译结束后进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo make install
sudo ldconfig /usr/local/lib/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;QGC 地面站的安装&lt;/h2&gt;
&lt;p&gt;在后续启动 PX4+Gazebo 仿真时，如果丢失地面站的连接，会导致无法启动 PX4 飞控对无人机进行控制，因此需要在系统中安装 QGC 地面站。安装的方法我给出一个 CSDN 博主的链接，按照上面的步骤操作即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_55944949/article/details/130895363&quot;&gt;(最新) Ubuntu 搭建 PX4 无人机仿真环境(3) -- Ubuntu 安装 QGC 地面站&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;单机 Offboard 测试流程及开发简介&lt;/h2&gt;
&lt;p&gt;现在所有的开发环境已经配置好了，我们现在进行 PX4 的 Offboard 模式的验证。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Offboard 模式&lt;/strong&gt;是 PX4 中的一种特殊飞行模式，允许外部系统（如机载计算机、ROS2 节点等）通过 MAVLink 或 uXRCE-DDS 直接发送控制指令，实时控制无人机的姿态、位置或速度，从而实现自主飞行或高级任务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先创建 ROS2 的工作空间：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/ros2_ws/src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后下载源码（注意科学上网）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws/src
git clone https://github.com/PX4/px4_msgs.git -b release/1.14
git clone https://github.com/PX4/px4_ros_com.git -b release/v1.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进行编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
colcon build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新环境并配置环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;source ~/ros2_ws/install/setup.bash&quot; &gt;&gt; ~/.bashrc
echo &quot;export GZ_SIM_RESOURCE_PATH=\$HOME/.simulation-gazebo/models:\$GZ_SIM_RESOURCE_PATH&quot; &gt;&gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;启动 Offboard 仿真&lt;/h3&gt;
&lt;p&gt;现在我们启动 Offboard 仿真。首先打开已经装好的 &lt;strong&gt;QGC 地面站&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;终端 1&lt;/strong&gt; —— 打开通信：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;终端 2&lt;/strong&gt; —— 启动仿真：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
make px4_sitl gz_x500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时地面站会与 PX4 进行连接，Gazebo 启动，出现了 x500 的无人机。当 PX4 终端出现绿色的 &lt;code&gt;ready for takeoff&lt;/code&gt; 时，说明仿真启动成功。有关这个使用 &lt;code&gt;make&lt;/code&gt; 命令启动仿真的环节，我们在后续进行多无人机仿真时会详细解释。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;终端 3&lt;/strong&gt; —— 运行 Offboard 模式源码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run px4_ros_com offboard_control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以发现 Gazebo 中无人机上升 5 米，Offboard 模式控制成功。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Offboard 模式开发流程&lt;/h3&gt;
&lt;p&gt;我们在刚刚创建并下载好源码的 &lt;code&gt;~/ros2_ws&lt;/code&gt; 里找到 &lt;code&gt;px4_ros_com&lt;/code&gt; 目录下的 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 文件，找到这三行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(offboard_control src/examples/offboard/offboard_control.cpp)
ament_target_dependencies(offboard_control rclcpp px4_msgs)
install(TARGETS offboard_control DESTINATION lib/${PROJECT_NAME})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是将 &lt;code&gt;src/examples/offboard&lt;/code&gt; 中的 &lt;code&gt;offboard_control.cpp&lt;/code&gt; 文件编译成可执行文件并命名为 &lt;code&gt;offboard_control&lt;/code&gt;，同时依赖来源于 &lt;code&gt;px4_msgs&lt;/code&gt;，我们可以打开 &lt;code&gt;src/examples/offboard&lt;/code&gt; 目录便可以找到 &lt;code&gt;offboard_control.cpp&lt;/code&gt; 源代码。&lt;/p&gt;
&lt;p&gt;因此如果进行单机使用 Offboard 模式进行仿真，便可以按照以下流程开发：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在已经创建并且配置好环境的 &lt;code&gt;~/ros2_ws&lt;/code&gt; 这个 ROS2 工作空间下再创建一个包（和 &lt;code&gt;px4_ros_com&lt;/code&gt; 目录并行）&lt;/li&gt;
&lt;li&gt;在新创建的包中，在 &lt;code&gt;/src&lt;/code&gt; 目录下编写基于 Offboard 模式进行控制的 C++/Python 代码&lt;/li&gt;
&lt;li&gt;在新创建的包中的 &lt;code&gt;CMakeLists.txt&lt;/code&gt; 中找到以下几行：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cmake&quot;&gt;add_executable(offboard_control src/examples/offboard/offboard_control.cpp)
ament_target_dependencies(offboard_control rclcpp px4_msgs)
install(TARGETS offboard_control DESTINATION lib/${PROJECT_NAME})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将路径改为 C++/Python 代码的地址，并给他命名一个可执行文件的名字。&lt;strong&gt;在进行仿真前，一定要重新编译 ROS2 工作空间！&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/ros2_ws
colcon build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，仿照之前的方法打开 QGC 地面站、通信以及启动仿真，随后再打开一个新的终端：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2 run &amp;#x3C;包名称&gt; &amp;#x3C;可执行文件名称&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果在 Gazebo 中看见无人机按照代码逻辑进行飞行，则仿真成功。&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;补充说明&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;补充 1&lt;/strong&gt;：使用 &lt;code&gt;make px4_sitl gz_x500&lt;/code&gt; 命令启动仿真时，自动启动的 &lt;code&gt;gz sim&lt;/code&gt; 仿真器的世界为 default，其 sdf 文件不再在 &lt;code&gt;~/.gz/worlds&lt;/code&gt; 里，而是在 &lt;code&gt;./PX4-Autopilot/Tools/simulation/gz/worlds&lt;/code&gt; 中。同理，所有的模型也均在 &lt;code&gt;Tools/simulation&lt;/code&gt; 里，因此，在搭建仿真环境时，应该去到这些文件夹中直接修改 sdf 文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;补充 2&lt;/strong&gt;：在编写 C++/Python 代码进行 Offboard 模式控制时，需要注意坐标系的变换。在 PX4 中，坐标系为 &lt;strong&gt;NED 坐标系&lt;/strong&gt;，即 North-East-Down，因此 x 坐标指向北，y 坐标指向东，z 坐标指向下（所有的高度为负值）；而在 Gazebo Ignition 中，坐标系为 &lt;strong&gt;ENU 坐标系&lt;/strong&gt;，此时 x 坐标指向东，y 坐标指向北，z 坐标指向上（高度为正值）。整理控制逻辑时需要注意坐标的换算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至此，ROS2+PX4 在 Offboard 模式下进行单机仿真的基本流程就全部介绍完了。大家如果成功地跑通以上流程，接下来就可以基于此进行更多的单机仿真开发了。&lt;/p&gt;
&lt;h2&gt;多机（编队）的开发流程&lt;/h2&gt;
&lt;p&gt;在前面的部分，我们完成了基于 ROS2+PX4 的单机仿真环境开发。现在我们开始进行多机的环境开发，为后续编队的控制逻辑仿真进行预先准备。&lt;/p&gt;
&lt;h3&gt;回顾单机启动&lt;/h3&gt;
&lt;p&gt;我们先回顾一下启动单机仿真最主要的指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
make px4_sitl gz_x500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;make&lt;/code&gt; 指令其实一共完成了三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编译&lt;/strong&gt; PX4 源代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动&lt;/strong&gt; PX4&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动&lt;/strong&gt; Gazebo 仿真&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但在多机仿真中，我们不需要反复地使用 &lt;code&gt;make&lt;/code&gt; 指令，这样会导致每一架 x500 无人机单独启动了一个 Gazebo 仿真，而这不是我们想要的。因此在 Gazebo 环境中启动多机仿真，我们的目标为以下几个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单独启动 Gazebo 仿真&lt;/li&gt;
&lt;li&gt;分别启动 PX4，添加无人机进入 Gazebo 环境&lt;/li&gt;
&lt;li&gt;建立通信&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;单独启动 Gazebo&lt;/h3&gt;
&lt;p&gt;首先我们需要下载一个 Gazebo 的启动脚本（&lt;code&gt;simulation-gazebo&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;wget https://raw.githubusercontent.com/PX4/PX4-gazebo-models/main/simulation-gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后在脚本所在的目录终端运行以下指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python3 simulation-gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;常见问题&lt;/strong&gt;：如果 Gazebo 窗口无法打开（闪退或黑屏），通常是因为显卡驱动不兼容或虚拟机缺少 GPU 直通。可以在命令前加上 &lt;code&gt;LIBGL_ALWAYS_SOFTWARE=1&lt;/code&gt;，强制使用 CPU 软件渲染：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;LIBGL_ALWAYS_SOFTWARE=1 python3 simulation-gazebo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个环境变量告诉 Mesa/OpenGL 使用 LLVMpipe 软件光栅化器代替硬件 GPU 加速。缺点是帧率会有所下降，但不影响仿真功能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首次运行脚本后，会下载一些组件放在 &lt;code&gt;.simulation-gazebo&lt;/code&gt; 下（在主目录里用 &lt;code&gt;Ctrl+H&lt;/code&gt; 打开隐藏文件夹），其中的 &lt;code&gt;/worlds/default.sdf&lt;/code&gt; 即为仿真的世界 sdf 文件，后续在这个文件里进行修改。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;补充 3&lt;/strong&gt;：此处需与单机仿真部分的补充 1 进行区别。这是单机和多机一个较大的不同。启动单机仿真时我们使用的是 &lt;code&gt;make&lt;/code&gt; 指令，世界的 sdf 文件在 &lt;code&gt;./PX4-Autopilot/Tools/simulation/gz/worlds&lt;/code&gt; 中，但如果我们仔细看多机仿真时我们下载好的 &lt;code&gt;simulation-gazebo.py&lt;/code&gt; 代码，会发现世界的 sdf 文件在 &lt;code&gt;.simulation-gazebo&lt;/code&gt; 下，需要进行区别。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;添加多架无人机&lt;/h3&gt;
&lt;p&gt;在单独启动了 Gazebo 环境后，我们往其中添加无人机，此时我们不再使用 &lt;code&gt;make&lt;/code&gt; 命令，而是用以下指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/PX4-Autopilot
PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,2&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数说明：&lt;/p&gt;
&lt;p&gt;| 参数 | 含义 |
|------|------|
| &lt;code&gt;PX4_GZ_STANDALONE=1&lt;/code&gt; | 使用 standalone 模式（不启动仿真，只启动 PX4，等待单独启动的仿真） |
| &lt;code&gt;PX4_SYS_AUTOSTART=4001&lt;/code&gt; | 必须字段 |
| &lt;code&gt;PX4_GZ_MODEL_POSE=&quot;0,2&quot;&lt;/code&gt; | 无人机初始位置，不同飞机位置应不同 |
| &lt;code&gt;PX4_SIM_MODEL=gz_x500&lt;/code&gt; | 飞机模型为 x500 |
| &lt;code&gt;-i 0&lt;/code&gt; | 无人机编号（第 0 号） |&lt;/p&gt;
&lt;p&gt;运行该指令后，我们应该可以在之前单独打开的 Gazebo 里看见出现了一架飞机。&lt;/p&gt;
&lt;p&gt;现在我们再添加第二架飞机（编号为 &lt;code&gt;-i 1&lt;/code&gt;，位置为 &lt;code&gt;&quot;0,1&quot;&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;PX4_GZ_STANDALONE=1 PX4_SYS_AUTOSTART=4001 PX4_GZ_MODEL_POSE=&quot;0,1&quot; PX4_SIM_MODEL=gz_x500 ./build/px4_sitl_default/bin/px4 -i 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要添加更多的无人机，按此方法添加即可，也可以编写统一的脚本进行添加。&lt;/p&gt;
&lt;p&gt;现在我们会发现在 Gazebo 里出现了两架并排的无人机，可以分别在他们启动 PX4 后的终端输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;commander takeoff/land
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以分别观察他们的起降情况。最后添加通信：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;MicroXRCEAgent udp4 -p 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;至此，基于 ROS2+PX4 在 Gazebo 里进行无人机单机、多机仿真的环境开发就全部结束了。在下一篇文章中，我们将基于此环境实现 &lt;a href=&quot;/zh/posts/px4-ros2-multi-offboard&quot;&gt;ROS2+PX4 多机 Offboard 编队控制&lt;/a&gt;，使用 leader-follower 算法进行双无人机编队飞行。除此之外，还可以基于此完成目标识别、路径规划等更多项目。万事开头难，希望本文能够给此方向的研究贡献一点微薄之力。&lt;/p&gt;
&lt;p&gt;如果仿真仍有问题，大家可以参考 PX4 官方文档以及 Github 上相关项目资料。另外，如果你对 Betaflight 飞控感兴趣，也可以参考 &lt;a href=&quot;/zh/posts/px4-ros2-betaflight-sitl&quot;&gt;Betaflight + Gazebo 软件在环仿真教程&lt;/a&gt;，了解另一种 SITL 仿真方案。环境的配置以及开发流程的熟悉是简单但是繁琐的过程，笔者作为初学者，提供一种自己的开发流程，同时也肯定会有其他更好的方式。如有错漏，欢迎指出，为后来者提供一个教训；如有更佳方式，也希望大家分享。&lt;/p&gt;
&lt;h2&gt;Gazebo 环境排错指南&lt;/h2&gt;
&lt;p&gt;根据一年多来大家的 bug，这里汇总 Gazebo 多版本共存导致依赖冲突的排查和修复方法。&lt;/p&gt;
&lt;h3&gt;1. 检查当前安装了哪些 Gazebo 版本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 查看所有 Gazebo 相关包
dpkg -l | grep -E &quot;^ii&quot; | awk &apos;{print $2}&apos; | grep -E &quot;^gz-|^libgz-|^sdformat|^libsdformat|python3-gz&quot; | sort

# 查看可用的 Gazebo Sim 版本（多版本意味着冲突）
gz sim --versions

# 查看当前默认版本
gz sim --version
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;正常状态：&lt;code&gt;gz sim --versions&lt;/code&gt; 只输出一个版本号（如 &lt;code&gt;8.9.0&lt;/code&gt;），所有 &lt;code&gt;libgz-*-X&lt;/code&gt; 的 &lt;code&gt;X&lt;/code&gt; 都指向同一代（如全是 7 或全是 8）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 版本冲突的典型表现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 症状 1：多个版本共存
gz sim --versions
# 输出：
# 8.9.0
# 7.9.0        ← 说明 Garden 和 Harmonic 共存，冲突！

# 症状 2：CMake 找到错误版本
grep &quot;gz-sim_DIR&quot; ~/PX4-Autopilot/build/px4_sitl_default/CMakeCache.txt
# 应与 gz sim --version 对应：Harmonic → gz-sim8，Garden → gz-sim7

# 症状 3：ROS2 桥接包版本不匹配
dpkg -l | grep ros-humble-ros-gz
# 应只有 gzharmonic 或只有 gzgarden，不能同时存在
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 修复：统一到单一 Gazebo 版本&lt;/h3&gt;
&lt;p&gt;以 Harmonic (v8) 为例（ROS2 Humble 推荐，因为有对应桥接包）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 移除 Garden (v7) 所有包
sudo apt-get purge -y gz-garden gz-sim7-cli gz-launch6-cli gz-transport12-cli \
  libgz-sim7 libgz-sim7-dev libgz-sim7-plugins \
  libgz-launch6 libgz-launch6-dev \
  libgz-transport12 libgz-transport12-dev \
  libgz-fuel-tools8 libgz-fuel-tools8-dev \
  libgz-gui7 libgz-gui7-dev \
  libgz-msgs9 libgz-msgs9-dev \
  libgz-physics6 libgz-physics6-dev \
  libgz-rendering7 libgz-rendering7-dev \
  libgz-sensors7 libgz-sensors7-dev \
  libsdformat13 libsdformat13-dev sdformat13-sdf \
  python3-gz-sim7

# 2. 移除 Garden 的 ROS2 桥接
sudo apt-get purge -y ros-humble-ros-gzgarden*

# 3. 安装 Harmonic 的 ROS2 桥接
sudo apt-get install -y ros-humble-ros-gzharmonic

# 4. 验证
gz sim --versions  # 应只显示 8.9.0
dpkg -l | grep gz-garden    # 应无输出
dpkg -l | grep gz-harmonic  # 应显示已安装
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. &lt;code&gt;.bashrc&lt;/code&gt; 常见问题&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 问题 1：source 重复（多次追加导致 PATH 越来越长）
grep &quot;source.*setup.bash&quot; ~/.bashrc | sort | uniq -c
# 如果某行出现超过 1 次，需要手动清理 ~/.bashrc

# 问题 2：ROS2 加载顺序错误
# 正确顺序：先 source /opt/ros/humble/setup.bash，再 source ~/ros2_ws/install/setup.bash
# 工作空间必须放在基础环境之后

# 问题 3：模型路径变量
# Gazebo Garden/Harmonic 使用 GZ_SIM_RESOURCE_PATH，不是 GAZEBO_MODEL_PATH
echo $GZ_SIM_RESOURCE_PATH  # 应指向模型目录
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 检查 PX4 编译目标与 Gazebo 版本是否一致&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# PX4 CMake 缓存记录的 gz-sim 版本
grep &quot;gz-sim_DIR&quot; ~/PX4-Autopilot/build/px4_sitl_default/CMakeCache.txt
# gz-sim8 → Harmonic
# gz-sim7 → Garden

# 如果版本与安装的 Gazebo 不一致，需要重新编译 PX4：
cd ~/PX4-Autopilot
make clean
make px4_sitl gz_x500
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 检查 ROS2 桥接包依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 确认桥接包的 SDFormat 依赖与 Gazebo 版本匹配
apt-cache depends ros-humble-ros-gzharmonic | grep sdformat
# Harmonic → libsdformat14-dev

# 确认无残留的 Garden 桥接
dpkg -l ros-humble-ros-gzgarden* 2&gt;/dev/null
# 应显示 &quot;没有找到&quot; 或状态为 &quot;un&quot; (已卸载)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.px4.io/main/en/ros2/user_guide.html&quot;&gt;PX4 官方文档 ROS 2 User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_55944949/article/details/140627640&quot;&gt;Ubuntu 搭建 PX4 无人机仿真环境(5) -- 仿真环境搭建&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/PX4/PX4-Autopilot/issues/24033&quot;&gt;Gazebo 仿真器问题 [Bug] WARN  [health_and_arming_checks] Preflight Fail: ekf2 missing data · Issue #24033&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Jinyao-Chen/ROS2-PX4-multivehicles-sim-tutorial&quot;&gt;GitHub: ROS2-PX4-multivehicles-sim-tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="undefined"/><enclosure url="undefined"/></item></channel></rss>