【踩坑记录-4】Rock 5+ RK3588运行mavsdk控制无人机 + 本地WSL运行PX4 + 本地Win运行QGC和控制界面

记录一下在板卡上代码的部署过程

1. 板卡环境搭建

板卡用的rock 5b+ rk3588,之后飞无人机的时候也是用3588飞。

2025/3/19 5bp断货了,用5t也一样,板卡大小重量都差不多,主要重量都在铝坨坨散热器上。

rock 5b+安装过程参考官方教程,建议完全按照官方教程来,国产板卡调试起来还是太炸裂了,还是怀念用orin nx和intel nuc的日子。

第一步:安装系统

安装系统到 MicroSD 卡 | Radxa Docs

我安装的是ROCK 5B+ 系统镜像: rock-5b-plus_bookworm_kde_b2,这个是6.1内核的debian12,和Ubuntu22.04差不多。然后按照教程把系统安装到nvme中。我装的时候Update SPI Bootloader的位置和教程里面的不一样,总之找找就找到了。

然后按照教程快速设置 | Radxa Docs配一下中文环境输入法,以及ssh和vnc。

我在使用过程中,大概15分钟左右系统会突然suspend,屏幕直接黑屏,需要点击鼠标才能恢复,ssh的时候也是,会提醒系统将要suspend,过几秒就休眠了。我的解决办法是在系统设置-节能里把所有选项全部勾掉,重启后解决。

第二步:安装python虚拟环境

Python 虚拟环境使用 | Radxa Docs

RKNN Ultralytics YOLOv11 | Radxa Docs

按照上面的教程可以把yolov11和python虚拟环境都装好。我之后跑的所有python代码都是在虚拟环境中实现的。

yolov11n的模型在rk3588上能跑到12帧左右,和ultralytics官方标称的结果一致。

此时rk3588的npu占用约为40%(npu0)

sudo cat /sys/kernel/debug/rknpu/load # NPU load: Core0: 41%, Core1: 0%, Core2: 0%, 

我们组23年的时候也用过3588,当时按照官方教程跑yolov5,调用了3个npu,能跑到120帧,现在不知道发展到啥程度了,如果有时间我再研究一下。现在做到开箱即用就行,而且10帧似乎也够用了,当然能到120帧肯定更好,就看要花多少精力了,过于复杂就直接放弃。

以上就是板卡搭建过程,全程只需要不到1天的时间,比我想象的要简单的多。

第三步:优化工作(可选)

一些便捷设置

1. 开机自动登录

 首先你需要判断你的桌面是什么,我的是GDM3

systemctl status display-manager

开启图形界面自动登录

sudo nano /etc/gdm3/daemon.conf

把下面三行写到 [daemon] 段里(没有就补上;把 radxa 换成你要自动登录的用户名):

[daemon] AutomaticLoginEnable=True AutomaticLogin=radxa

保存退出后重启 GDM(会踢下当前图形会话),建议配置好后重启一次:

sudo systemctl restart gdm

2. 开机自启vncserver(GDM3)

输入以下内容

sudo tee /usr/local/bin/start-vnc.sh >/dev/null <<'EOF' #!/bin/sh /usr/bin/vncserver -localhost no -geometry 1280x720 -depth 24 EOF sudo chmod +x /usr/local/bin/start-vnc.sh

如果 vncserver 不在 /usr/bin/vncserver,用 which vncserver 看路径,把上面改成真实路径。如果你是按照radxa教程安装的,这些可以直接复制。

sudo tee /etc/systemd/system/start-vnc.service >/dev/null <<'EOF' [Unit] Description=Start VNC once at boot After=network-online.target Wants=network-online.target [Service] Type=oneshot User=radxa ExecStart=/usr/local/bin/start-vnc.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now start-vnc.service

如果你要调整vnc的分辨率,可以用

xrandr --display :1 --output VNC-0 --mode 1920x1080 # 查看当前分辨率 xrandr --display :1

注意上面第2步的bash也需要用sudo nano /usr/local/bin/start-vnc.sh同步修改分辨率,不然开机就又刷回去了(此步存疑,原则上来说我修改了start-vnc.sh,重启板卡就应该立即生效。Claude和我说是因为“脚本改了但旧 session 还在跑,参数没重新应用”。有些逻辑上的问题,但是感觉问题不大,直接忽略了)

3.有线设置

2026/3/19 rock 5b+缺货,我又买了两个rock 5t,←这是题外话,理论上对5bp和5t进行有线网的操作设置是一致的。

2026/3/19 考虑到后续板卡比较多,再加上调试场地经常变,所以要用zerotier做ip统一,详见下一小节。

我发现有线网卡经常掉驱动,所以开机不管能不能找到驱动,我都手动挂载。

Rock 5B Plus 的 2.5G 网卡通常走 PCIe,执行

lspci -nn | grep -i -E 'ethernet|realtek|network' || true dmesg -T | grep -i -E 'r8125|r8169|realtek|ethernet|pci' | tail -n 80

如果 lspci 能看到网卡,但 ip a 没接口:手动加载驱动试一下
先看当前是否有相关模块:

lsmod | grep -i -E 'r8125|r8169' || true

尝试加载(两种常见驱动都试一下,不会把系统搞坏):我这里成功的是r8169。

sudo modprobe r8169 || true sudo modprobe r8125 || true dmesg -T | tail -n 80 ip -br link

因此,我让 r8169 开机自动加载。

sudo tee /etc/modules-load.d/r8169.conf >/dev/null <<'EOF' r8169 EOF

4.设备跨网络互通

我们考虑无人机做自组网通讯的方案是使用5g,为了快速进入实飞测试阶段,我们直接给无人机装了5g随身wifi,大概长这样:

原则上来说肯定是用5g模块更好,但是这一款随身wifi比较方便,开机即用,机卡一体,连板卡上直接当有线连接用了,能省不少事,而且重量也不是很重。

因为每个飞机(板卡)还有地面控制端都在相互隔离的NAT网络里,没有直接路由可达,所以需要做网络穿透,让所有设备都在同一个虚拟局域网里。这里我选择用ZeroTier,免费(最多1网络10节点)、P2P打洞、无需自建服务器,而且操作界面一看就懂,这里就不做过多解释了。

https://central.zerotier.com/注册后一步一步操作即可。x86上就不说了,RK3588上这么执行,设置开机自启,下面代码执行完一次就ok。

curl -s https://install.zerotier.com | sudo bash sudo systemctl enable zerotier-one # 开机自启 sudo systemctl start zerotier-one sudo zerotier-cli join <NetworkID> # NetworkID就是那16位的码

ping了一会,平均延迟大概是10~20ms,丢包 0% ,说明打洞成功,走的是 P2P 直连而不是中继。会有较大抖动(100ms),原因可能是5G随身WiFi本身的特性,信号质量波动、运营商调度都会造成这个现象。

后续我一下mavlink好不好用,尤其是在挂着梯子,从wsl2-Windows-zerotier-3588这条路走的顺不顺利。

5.SSH与VNC协同

我们可以在本地SSH代码,视频窗口出现在 VNC 上。但是SSH 登录后默认没有DISPLAY,或者不指向 VNC 的 :1,所以 GUI 窗口不知道该画到哪里去。

要让脚本的视频窗口出现在 VNC 上,运行时加上:

export DISPLAY=:1 python your_script.py # (.pyvenv) radxa@rock-5t:~/drones-formation/detect-yolo$ python visdroneRun-orin.py

然后就是下面这样了,ssh发指令,vnc只负责显示画面。

不得不说 X11 的设计太优雅了,没想到一秒就解决了我“ssh写代码,vnc负责输出”的核心诉求。很难想象这是40年前就设计好的架构,太超前了。

使用nano ~/.bashrc编辑bashrc,在末尾加上export DISPLAY=:1,就可以永久保留这个变量了,不用每次启动都输入。

2. 无人机控制代码

2.1 无人机代码

见【踩坑记录-3】,除了ip和端口外基本不需要更改,有不理解的地方建议问codex。

我还录了一个视频,你们可以看一下代码的演示效果WSL2+PX4+MAVSDK+Gazebo+YOLOV8n固定翼编队与目标识别仿真演示

2.2 通讯框图

3588只负责无人机控制代码,px4和仿真还是在wsl2里面实现。实飞的时候其实也是如此。

建议关闭Windows的防火墙以获得更好的连接体验

我大致说一下操作流程吧,年底了不一定有时间更新【踩坑记录-3】了,因为代码还没调好,所以这个教程还是以手动操作为主。

2.3 启动步骤

第一步:打开win的控制界面和QGC

注意【涉及到ip地址】的代码都需要修改。

第二步:打开wsl的start_cam

这个代码会用tmux打开gazbo的default世界,还有一个px4节点,px4节点默认是从14540开始。然后在这个节点所在命令行输入commander:

mavlink start -u 14600 -t 169.254.12.100 -p 14550 -w

8.98和12.100分别是我的板卡在同一网段下的wifi地址和有线地址,此时px4节点除了会向本地QGC所在的14550传数据外,还会额外开一路link,向板卡的14550发数据。我发现终端上写的是通过8.98回传的,而不是通过有线回传,不知道会不会有影响,后续测试

有warning不用管,到时候简单编译一下mavlinkrc的源码就能取消报错。

👆之前我写的是 ... -u 14540 -t ...,导致出现“WARN [mavlink] ODOMETRY: estimator_type 8 unsupported”的循环调用的错误,这因为占了14540的仿真口(即第一个飞机的端口),导致外部数据循环发送。改成14600就可以了。

第一个px4节点用的是14600的话,第二个节点就是14601,以此类推。drone.connect不需要变,每架机都是14550。

板卡上runtime脚本中的connect函数要把无人机的connect改为:

await drone.connect(system_address="udpin://0.0.0.0:14550")

实飞的时候,飞控是通过tpyec连接到板卡上的,那个时候connect连的就是tty0usb,ttyACM0之类的,然后飞控通过数传和地面站连,机载电脑(rk3588)通过数据链或者wifi/ap组网和地面连。

第三步:打开wsl的gz_pub_zmq

注意【涉及到ip地址】的代码都需要修改。

这里需要对gz_pub_zmq添加一行代码,用来防止视频流积压

def run_zmq_inference( linux_ip, port, output_path, show_window, on_detections: Optional[Callable[[list, int], None]] = None, stop_event=None, poll_timeout_ms: int = 100, ): """Run VisDrone model on frames received over a ZeroMQ JPEG stream.""" ctx = zmq.Context.instance() sock = ctx.socket(zmq.SUB) # 应该是添加了下面这两行 sock.setsockopt(zmq.CONFLATE, 1) sock.setsockopt(zmq.RCVHWM, 1) #应该是添加了上面这两行 sock.connect(f"tcp://{linux_ip}:{port}") sock.setsockopt(zmq.SUBSCRIBE, b"")

因为涉及到端口数据的转发,因此需要在Windows端进行设置,首先在Windows端查看视频流端口是否开放

netsh interface portproxy show all

比如zmq用5561端口的话,那么应该会显示

 侦听 ipv4: 连接到 ipv4: 地址 端口 地址 端口 --------------- ---------- --------------- ---------- 0.0.0.0 5561 127.0.0.1 5561

没有的话,就用管理员身份执行

 # 添加 5561 转发(与已有规则风格一致) netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=5561 connectaddress=127.0.0.1 connectport=5561 # 开放防火墙 netsh advfirewall firewall add rule name="ZMQ 5561" dir=in action=allow protocol=TCP localport=5561

这样3588就能获取视频流了。

第四步:打开3588的drone_id_0

此时就能看到无人机连到win的控制界面了。

第五步:打开3588的visdroneRun_access

注意有点地方的代码要修改,ultralyticsplus不太兼容,需要from ultralytics import YOLO

然后model要改成model = YOLO("./yolov11n-visdrone_rknn_model"),注意这是一个文件夹。

同理,需要在run_zmq_inference方法里加入这个,和发送端是一致的。

 ctx = zmq.Context.instance() sock = ctx.socket(zmq.SUB) # 应该是新增下面两行 sock.setsockopt(zmq.CONFLATE, 1) sock.setsockopt(zmq.RCVHWM, 1) # 应该是新增上面两行 sock.connect(f"tcp://{linux_ip}:{port}") sock.setsockopt(zmq.SUBSCRIBE, b"")

然后就可以实现3588运行控制代码,wsl2运行仿真环境和px4节点,Windows运行地面控制的全流程了

这里讲一下操作流程,先在monitor里面选择编队0,之后在第一或者第三个页面点击起飞,飞到50米左右就可以在第三个页面点击模拟信息输入,指令确认,指令上传,随后无人机就会朝着目标点飞去,直到锁定并完成任务。

因为3588用yolov11n的只能跑到12帧,所以锁定过程飞机飘来飘去的,远比不上在纯sitl里面的30帧的控制精度。有时间我研究一下能不能提高帧数,比如换回yolov5lite。。。

3. 思翼云台设置

思翼 A8mini 的默认 IP 是 192.168.144.25,端口8554。

一开始我通过vlc获取视频流,打开 VLC → 媒体 → 打开网络串流(Ctrl+N),输入rtsp://192.168.144.25:8554/main.264就可以打开云台相机画面。但是,VLC延迟就高,它是为流畅播放设计的,不擅长低延迟直播流,所以不管怎么调延迟都很高(其实比ffmpeg没高多少)

然后我选择用ffmpeg,在Windows里安装ffmpeg:

winget install ffmpeg

然后运行:

ffplay -fflags nobuffer -flags low_delay -framedrop -rtsp_transport tcp rtsp://192.168.144.25:8554/main.264

我忘记截图了,总之就是通过对比云台传过来的流画面和现实时钟对比,大概是慢了400ms。典型的rtsp延迟就是300-500ms,因为思翼相机的关键帧间隔(GOP)大概是10帧,25fps下,每帧=1000ms/25=40ms,等待时间=10×40,差不多就是400ms,一般平均等待时间是半个GOP,实际延迟就是200~400ms。想要进一步降低延迟,得用思翼自家的软件。

4. 其他问题

4.1 ZeroTier打洞失败

ZeroTier 的 P2P 路径有超时机制,如果两台设备长时间没有通信,缓存的路径会过期,下次重新通信又会有短暂的第一次打洞延迟。

目前没找到有什么好的接近方法,我写了个每分钟ping它的powershell,但是似乎毫无用处,能不能ping通完全随缘,ping不通怎么都不通,能ping通就不需要一直ping了。但是这个问题出现概率也不是很高,而且经常自己等一会就好了,不知道为什么。

4.2 Gz Sim异常接入ZeroTier

测试时发现,gazeb仿真环境还有所有的传感器数据全都跑到zerotier虚拟网络上了,导致我的zerotier吃掉了接近200Mbps的网络。解决方法是定义gz的ip,绑定到本地回环即可,代码改动如下,最近在做半实物仿真,代码打了好多补丁,看起来不优雅。等全流程都跑通了我再上传最终代码以及具体操作流程。

 #!/bin/bash tmux new-session -d -s formation # new-session named "formation",-d means 不立刻附着,-s means 命名会话 PX4_PLUGINS=~/PX4-Autopilot/build/px4_sitl_default/src/modules/simulation/gz_plugins tmux new-window -t formation -n Gazebo "GZ_IP=127.0.0.1 GZ_SIM_SYSTEM_PLUGIN_PATH=$PX4_PLUGINS python3 simgz" # 改动【【【【固定gz的ip】】】 export GALLIUM_DRIVER=d3d12 export MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA for i in $(seq 0 2); do # 启动PX4实例 pose_y=0 pose_x=$((i * 5)) pose="$pose_x,$pose_y,0.15,0,0,0" if [ "$i" -eq 999 ]; then MODEL=x500_cam else MODEL=x500 fi cmd="cd ~/PX4-Autopilot && \ GZ_IP=127.0.0.1 \ PX4_GZ_STANDALONE=1 \ PX4_GZ_MODEL_POSE=$pose \ PX4_SYS_AUTOSTART=4001 \ PX4_SIM_MODEL=$MODEL \ ./build/px4_sitl_default/bin/px4 -i $i | sed '1,13d'" # 改动【【【【固定gz的ip】】】 tmux new-window -t formation -n PX4_$i "$cmd" # new-window means 新建窗口,-t means 在哪个session,-n means 窗口名称 sleep 2 # wait for px4 shell prompt before injecting custom command sleep 0.1 done # ---- Mavlink forwarding(不需要时注释掉这段) ---- tmux send-keys -t formation:PX4_0 "mavlink start -u 14600 -t 10.144.1.1 -p 14550 -w" Enter tmux send-keys -t formation:PX4_1 "mavlink start -u 14601 -t 10.144.1.2 -p 14550 -w" Enter # -------------------------------------------------- tmux attach -t formation # -t means 把之前的窗口附着到指定的会话 

4.3 板卡与WSL2建立连接

4.3.1 问题分析

WSL2设置中的默认的网络形式是镜像(mirrored),这个模式下,WSL 的网络接口会镜像宿主机的接口,所以很多时候看起来就像一台和 Windows 主机共用网络环境的真实机器。官方文档也说明了,mirrored 模式支持从局域网直接访问 WSL。但这里有一个关键前提:外部设备发往 WSL 的入站流量,默认可能会被 Windows / Hyper-V 防火墙拦住。

也就是说,Windows -> WSL 往往比较容易通,但 外部板卡 -> WSL 不一定默认能通,表现就是,板卡之间互发 UDP 正常,Windows 地面站给 WSL 正常,板卡给 WSL 发数据不通。

解决办法其实很简单,就是在Windows宿主机开放部分允许流量入站WSL的端口即可。上面4.2里面mavlink额外开一路通讯给板卡也会遇到同样的问题,我之前应该是遇到过这个问题,然后很幸运随手开放了14550附近的端口给解决了,结果导致现在又忘记了,又被卡了一次。。。

但是具体怎么解决的,我也不太确定,我昨天用codex改了好多好多设置,最有可能是下面这个方式:

# 管理员 powershell # Hyper-V 防火墙放行 Set-NetFirewallHyperVVMSetting -Name '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' -DefaultInboundAction Allow 

然后开放端口,比如57002

New-NetFirewallHyperVRule ` -Name "WSL-UDP-57002" ` -DisplayName "WSL UDP 57002" ` -Direction Inbound ` -VMCreatorId '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' ` -Protocol UDP ` -LocalPorts 57002 

测试方法则是,先在 WSL 里启动一个最简单的 UDP 监听程序(必须)

nc -ul -p 57002

再开另一个终端抓包

sudo tcpdump -ni any udp port 57002

然后在板卡侧发数据,因此3588的板卡(Debian12)默认是没有装nc的(sudo apt install -y netcat-openbsd),我图省事就使用bash自带的指令发数据

echo "hello" > /dev/udp/10.144.1.0/57002

WSL 能收到数据,即代表链路被打通。

4.3.2 其他疑惑

昨天一直在折腾上面的问题,其实解决方式很简单,但是我用了错误的验证方法,导致我可能早就解决了问题,但不知道。具体来说,就是我在wsl终端只开了tcpdump抓包指令,然后我就从板卡给wsl发数据了。为什么我会这么操作?因为我用两个板卡测试的时候,A板卡发数据,B板卡tcpdump抓包,是通的,那么我推出的结论就是,只要wsl的tcpdump能抓到数据,就说明我板卡 -> wsl链路打通,否则就是不通。

但是!板卡-板卡通讯和板卡-wsl通讯两个场景不是一回事。

板卡和板卡通信:是真实网卡对真实网卡;板卡和 WSL mirrored 通信:是先到 Windows/Hyper-V,再决定要不要交给 WSL。所以 tcpdump 在两个场景里的意义不一样。

板卡对板卡为什么不用监听?

在普通 Linux 主机上,tcpdump 挂在真实网卡上抓包。

只要包到了这张网卡,哪怕目标端口没有进程监听,tcpdump 也能先看到,然后内核才会决定这个 UDP 端口没人收,丢掉。也就是说,普通板卡上:

包先到网卡 -> tcpdump 能看到 -> 监听程序有没有开,是后面的事

WSL 为什么不一样?

WSL mirrored 里的 ethX 不是一块独立的真实物理网卡,它是 Windows/Hyper-V 镜像出来的虚拟接口。外部板卡发给 WSL的10.144.1.0:57002 的包,不是天然就直接进 WSL,而是:

先到 Windows 主机 -> Windows/Hyper-V 判断这包要不要转交给 WSL -> 只有真正转交了,WSL 里的 tcpdump 才能看到。

所以在WSL里只开tcpdump抓包,它不会向 Windows/Hyper-V 注册“57002 这个端口现在由 WSL 接收”。只有再开一个最简单的 UDP 监听,这一步才真正创建了一个 0.0.0.0:57002 的 socket。
从现象上看,Windows/Hyper-V 这时才会把这个端口的入站流量交给 WSL。

因此,由于我错误的验证方法,导致这个简单的问题卡了我一下午,希望各位不要犯这种低级错误。。。

5. 实飞前设置

5.1 本地版flight review

网页版Flight Review太慢了,因此我装了离线版本,实飞完的数据可以复盘。

官方提供的本地/私有部署版 Flight Review。PX4文档明确提到可以自建 private Flight Review server;官方 flight_review 仓库也写了本地用法,可以直接拿一个 .ulg 在本机打开,不用先上传再收链接,需要在Ubuntu或者WSL2里装依赖并拉仓库,使用python虚拟环境防止污染已经装好的PX4仿真:

sudo apt update sudo apt install -y git python3 python3-venv python3-pip sqlite3 libfftw3-bin libfftw3-dev git clone --recursive https://github.com/PX4/flight_review.git cd ~/flight_review/app python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt python setup_db.py 

如果你要支持加密日志,按官方文档放私钥,这一步我跳过了

mkdir -p ~/flight_review/app/private_key cp /mnt/c/你的路径/private_key.pem ~/flight_review/app/private_key/private_key.pem cat > ~/flight_review/app/config_user.ini <<'EOF' [general] ulge_private_key = ../private_key/private_key.pem EOF 

启动服务

cd ~/flight_review/app source .venv/bin/activate python serve.py --show 

在 Windows 浏览器里打开 http://localhost:5006

然后就和在网页上的操作一致了。

git过程中可能网络不稳定,如果获取失败,用下面的操作

git config --global http.version HTTP/1.1 git clone --depth 1 https://github.com/PX4/flight_review.git cd flight_review git submodule update --init --recursive --depth 1 

如果还是断,再补这两个配置再试:

git config --global core.compression 0 git config --global http.postBuffer 524288000 

然后再重新拉。再失败就不知道怎么办了,codex没告诉我

弄好以后我地图是刷不出来的。我让codex帮我补充了相关方法,改了好几个文档,我就不往上放了,要再装一边的话,就先把上面的执行完,然后再和codex说我要换卫星地图即可,需要声明你是要街道类型的OSM标准地图,还是World_Imagery 这类卫星影像服务底图。3D view还没弄好,我目前不需要,就没再设置。

Could not load content