Qt/QGroundControl 实战:接入 Skydroid(云卓) G20 遥控器 Android SDK 并实时显示摇杆与信号质量

文章目录

    • [0. 最终效果](#0. 最终效果)
    • [1. 背景](#1. 背景)
    • [2. 需求拆解](#2. 需求拆解)
    • [3. 先看官方 demo,别凭感觉接](#3. 先看官方 demo,别凭感觉接)
      • [3.1 SDK 初始化链路](#3.1 SDK 初始化链路)
      • [3.2 G20 摇杆值读取方式](#3.2 G20 摇杆值读取方式)
      • [3.3 G20 原始信号质量读取方式](#3.3 G20 原始信号质量读取方式)
    • [4. 在 Qt Android 工程里怎么接](#4. 在 Qt Android 工程里怎么接)
      • [4.1 Java 层](#4.1 Java 层)
      • [4.2 C++/Qt 桥接层](#4.2 C++/Qt 桥接层)
      • [4.3 QML 显示层](#4.3 QML 显示层)
    • [5. Java 层实现](#5. Java 层实现)
    • [6. Qt / JNI 桥接](#6. Qt / JNI 桥接)
    • [7. 注册到 QML](#7. 注册到 QML)
    • [8. FlyView 临时显示](#8. FlyView 临时显示)
    • [9. Android 打包接入 SDK](#9. Android 打包接入 SDK)
      • [9.1 提取 jar](#9.1 提取 jar)
      • [9.2 提取 so](#9.2 提取 so)
      • [9.3 提取 assets](#9.3 提取 assets)
    • [10. 第一个坑:编译时报找不到 `SDKManagerCallBack`](#10. 第一个坑:编译时报找不到 SDKManagerCallBack)
    • [11. 第二个坑:程序启动即崩溃,缺 `kotlin.jvm.internal.Intrinsics`](#11. 第二个坑:程序启动即崩溃,缺 kotlin.jvm.internal.Intrinsics)
    • [12. 验证链路](#12. 验证链路)
      • [12.1 启动阶段](#12.1 启动阶段)
      • [12.2 摇杆值](#12.2 摇杆值)
      • [12.3 原始信号质量](#12.3 原始信号质量)
    • [13. 最终效果](#13. 最终效果)
    • [14. 这次接入的几个结论](#14. 这次接入的几个结论)
      • [14.1 第三方 Android SDK 接到 Qt 项目里,最难的通常不是接口调用](#14.1 第三方 Android SDK 接到 Qt 项目里,最难的通常不是接口调用)
      • [14.2 先跑通"可见的最小闭环"很重要](#14.2 先跑通“可见的最小闭环”很重要)
      • [14.3 Qt Android 的构建缓存必须重视](#14.3 Qt Android 的构建缓存必须重视)
    • [15. 相关文件](#15. 相关文件)
    • [15. 小结](#15. 小结)

0. 最终效果

左侧为 SDK 获取到的数据,主要为用户最关注的连接状态、通道值、信号强度;

右侧为云卓 G20 设备助手中查看的数据,对比左侧,验证数据无误;

1. 背景

最近在 QGC 二次开发项目里,需要把 Skydroid G20 遥控器 SDK 接入到 Android 版本中,并临时在飞行界面里显示两类数据:

  1. 遥控器摇杆感量
  2. G20 遥控器原始信号质量

项目本身是一个基于 Qt/QGroundControl 改造的 Android 工程,界面层以 QML 为主,Android 平台能力通过 Java + Qt JNI 桥接到 QML。

这篇文章记录一下完整接入过程,包括:

  • SDK 能力点梳理
  • Qt Android 工程里的接法
  • Java 和 QML 之间的数据桥
  • Android 打包时踩到的坑
  • 最终 FlyView 页面实时显示效果

2. 需求拆解

本次目标很明确:

需求1:获取遥控器摇杆感量

SDK 文档里对应的是:

kotlin 复制代码
/**
 * 遥控器摇杆感量
 * 访问方式
 * GET
 * 支持H12/H12Pro/H30/H20/G12/G20/G30
 */
val KeyChannels: KeyInfo<IntArray> = KeyInfo.Builder<IntArray>()
    .canGet(true)

也就是说:

  • G20 的摇杆值不是监听推送
  • 必须主动 GET
  • 官方 demo 建议至少每 100ms 读取一次

需求2:获取 G20 遥控器的原始信号质量

SDK 文档里对应的是:

kotlin 复制代码
val KeyRawSignalQuality: KeyInfo<String> = KeyInfo.Builder<String>()
    .canListen(true)

原始 JSON 里重点关注这些字段:

json 复制代码
{
  "dev_connect": true,
  "ap_snr": "0",
  "ap_gain_a": "0",
  "ap_gain_b": "0",
  "signal": 0
}

这几个字段里:

  • dev_connect:接收机是否连接
  • ap_snr:遥控器 SNR
  • ap_gain_a:A 路天线接收信号强度
  • ap_gain_b:B 路天线接收信号强度
  • signal:根据信噪比换算出的参考信号质量百分比

3. 先看官方 demo,别凭感觉接

在接任何 Android SDK 之前,我一般都先看官方 demo 的真实调用方式。

Skydroid demo 里几个关键结论:

3.1 SDK 初始化链路

demo 里是这样做的:

java 复制代码
RCSDKManager.INSTANCE.initSDK(this, callback);
RCSDKManager.INSTANCE.setMainThreadCallBack(true);
RCSDKManager.INSTANCE.connectToRC();

也就是:

  1. 初始化 SDK
  2. 设置回调线程
  3. 发起连接遥控器

3.2 G20 摇杆值读取方式

demo 里对 KeyChannels 的处理分了机型:

  • H16:LISTEN
  • 其他机型:GET

G20 属于后者,所以应该使用:

java 复制代码
KeyManager.INSTANCE.get(RemoteControllerKey.INSTANCE.getKeyChannels(), callback);

而且需要轮询,不是监听。

3.3 G20 原始信号质量读取方式

信号质量原始数据使用:

java 复制代码
KeyManager.INSTANCE.listen(AirLinkKey.INSTANCE.getKeyRawSignalQuality(), listener);

这个是监听型接口,回调给的是 JSON 字符串。


4. 在 Qt Android 工程里怎么接

这个项目不是纯 Android 工程,而是 Qt/QML 工程,所以不能简单照搬 demo Activity。

我的处理思路是分三层:

4.1 Java 层

写一个 Android Java 管理器类:

text 复制代码
android/src/org/mavlink/qgroundcontrol/SkydroidRCSDKManager.java

它负责:

  • 初始化 RCSDK
  • 连接遥控器
  • 每 100ms 轮询 KeyChannels
  • 监听 KeyRawSignalQuality
  • 缓存最新值
  • 暴露静态 getter 给 C++/Qt 读取

4.2 C++/Qt 桥接层

写一个 Qt QObject:

text 复制代码
xsrc/XModule/AndroidRCInput.h
xsrc/XModule/AndroidRCInput.cc

它负责:

  • 通过 JNI 调 Java 静态方法
  • 把 Java 缓存值转成 Qt 属性
  • 暴露给 QML
  • 定时刷新
  • 变化时打印调试日志

4.3 QML 显示层

在 FlyView 的自定义 UI 层里临时加一块调试面板:

用于显示:

  • SDK 状态
  • 是否已连接
  • 摇杆值数组
  • dev_connect
  • ap_snr
  • ap_gain_a
  • ap_gain_b
  • signal

5. Java 层实现

5.1 在 Activity 生命周期中接入

项目主 Activity 是:

text 复制代码
android/src/org/mavlink/qgroundcontrol/QGCActivity.java

onCreate() 里初始化:

java 复制代码
SkydroidRCSDKManager.initialize(this);

onDestroy() 里释放:

java 复制代码
SkydroidRCSDKManager.shutdown();

这样做的好处是:

  • 不改 Qt 主流程
  • Android 生命周期清晰
  • 资源释放时机明确

5.2 Java 管理器职责

SkydroidRCSDKManager.java 里主要做了这些事:

初始化 SDK
java 复制代码
RCSDKManager.INSTANCE.initSDK(activity, sSdkCallback);
RCSDKManager.INSTANCE.setMainThreadCallBack(true);
RCSDKManager.INSTANCE.connectToRC();
轮询摇杆值

G20 不是监听型,所以我用 ScheduledExecutorService 每 100ms 拉一次:

java 复制代码
KeyManager.INSTANCE.get(RemoteControllerKey.INSTANCE.getKeyChannels(), sChannelsCallback);

拿到结果后:

  • 缓存成字符串
  • 打印日志

例如:

java 复制代码
Log.i(TAG, "KeyChannels=" + channelsText);
监听原始信号质量
java 复制代码
KeyManager.INSTANCE.listen(AirLinkKey.INSTANCE.getKeyRawSignalQuality(), sRawSignalListener);

收到 JSON 后只保留需要的字段:

  • dev_connect
  • ap_snr
  • ap_gain_a
  • ap_gain_b
  • signal

同时打印:

java 复制代码
Log.i(TAG, "KeyRawSignalQuality dev_connect=..., ap_snr=..., ap_gain_a=..., ap_gain_b=..., signal=...");
暴露静态读取方法

为了让 Qt 侧简单读数据,Java 层提供了:

java 复制代码
public static boolean isRCConnected()
public static String getChannelsText()
public static String getSignalInfoJson()
public static String getStatusText()

这样 Qt 只需要 JNI 读缓存,不直接和 RCSDK 的异步接口耦合。


6. Qt / JNI 桥接

在 C++ 侧我没有直接把 RCSDK 接口写成 native callback,而是保持简单:

  • Java 负责采集
  • Qt 负责显示

桥接类是:

text 复制代码
xsrc/XModule/AndroidRCInput.cc

它通过 QAndroidJniObject 调 Java 静态方法:

cpp 复制代码
QAndroidJniObject::callStaticMethod<jboolean>(
    "org/mavlink/qgroundcontrol/SkydroidRCSDKManager",
    "isRCConnected",
    "()Z");

以及:

cpp 复制代码
QAndroidJniObject::callStaticObjectMethod(
    "org/mavlink/qgroundcontrol/SkydroidRCSDKManager",
    "getChannelsText",
    "()Ljava/lang/String;");

原始信号质量是 JSON,所以 Qt 侧再用 QJsonDocument 解析:

cpp 复制代码
const QJsonDocument document = QJsonDocument::fromJson(signalInfoJson.toUtf8());

然后转成 QML 属性:

  • connected
  • statusText
  • channelsText
  • devConnect
  • apSnr
  • apGainA
  • apGainB
  • signal

7. 注册到 QML

QGCApplication.cc 里把桥接类型注册到 XUI 模块:

cpp 复制代码
qmlRegisterType<AndroidRCInput>("XUI", 1, 0, "AndroidRCInput");

然后在 XUiManager.qml 中直接使用:

qml 复制代码
AndroidRCInput {
    id: androidRCInput
}

8. FlyView 临时显示

调试阶段我没有做复杂 UI,只是在 FlyView 里放了一个临时信息面板,直接显示这些值:

qml 复制代码
text: "G20 RC: " + (androidRCInput.connected ? "connected" : "disconnected")
text: "Status: " + androidRCInput.statusText
text: "Channels: " + androidRCInput.channelsText
text: "Signal: dev_connect=" + androidRCInput.devConnect
      + ", ap_snr=" + androidRCInput.apSnr
      + ", A=" + androidRCInput.apGainA
      + ", B=" + androidRCInput.apGainB
      + ", signal=" + androidRCInput.signal

这样做的好处是:

  • 联调非常直接
  • 不依赖日志也能看状态
  • 后面产品化时再替换正式 UI 即可

9. Android 打包接入 SDK

这个步骤反而是整个过程中最容易踩坑的地方。

Skydroid SDK 给的是 .aar,里面有:

  • classes.jar
  • assets
  • jni/*.so

Qt Android 工程不能像标准 Android Studio 工程那样直接 implementation files("xxx.aar") 就完事,所以我采用了拆包接法:

9.1 提取 jar

rcsdk-v1.8.4.aar 中的 classes.jar 提取后放到:

text 复制代码
android/libs/rcsdk-v1.8.4.jar

9.2 提取 so

jni/arm64-v8a/*.sojni/armeabi-v7a/*.so 放到:

text 复制代码
android/libs/arm64-v8a/
android/libs/armeabi-v7a/

并在 android.pri 中加入:

qmake 复制代码
ANDROID_EXTRA_LIBS += ...

9.3 提取 assets

把 AAR 中的 assets 放到:

text 复制代码
android/assets/

10. 第一个坑:编译时报找不到 SDKManagerCallBack

一开始我把 import 写成了:

java 复制代码
import com.skydroid.rcsdk.common.callback.SDKManagerCallBack;

但实际 SDK 里的类路径是:

java 复制代码
import com.skydroid.rcsdk.SDKManagerCallBack;

这个问题通过对照官方 demo 很快就修掉了。

结论很简单:

  • 对第三方 SDK 的类路径,不要猜
  • 直接以官方 demo 为准

11. 第二个坑:程序启动即崩溃,缺 kotlin.jvm.internal.Intrinsics

这是整个接入过程中最关键的坑。

崩溃日志

text 复制代码
java.lang.NoClassDefFoundError: Failed resolution of: Lkotlin/jvm/internal/Intrinsics;
at com.skydroid.rcsdk.RCSDKManager.initSDK(...)

原因

rcsdk-v1.8.4.jar 是 Kotlin 编译产物的一部分,运行时依赖 Kotlin 标准库,但 Qt Android 工程默认没有把 Kotlin runtime 打进 APK。

解决方案

把 Kotlin runtime jars 一起放进 android/libs/

text 复制代码
kotlin-stdlib-1.3.72.jar
kotlin-stdlib-jdk7-1.3.72.jar
kotlin-stdlib-jdk8-1.3.72.jar

并确保重新完整打包 APK,而不是直接运行旧产物。

经验

只要你在 Android 崩溃日志里看到:

text 复制代码
kotlin.jvm.internal.Intrinsics

几乎就可以直接判断:

  • Kotlin 运行时没打进去
  • 或安装到设备上的还是旧 APK

12. 验证链路

接好后,验证分三层:

12.1 启动阶段

先看程序是否能正常启动,FlyView 面板里的 Status 是否变化,例如:

  • sdk initializing
  • connectToRC called
  • rc connected
  • channels ok
  • raw signal ok

12.2 摇杆值

推动 G20 摇杆,看 Channels 数组是否变化。

因为 G20 是 GET 方式,所以表现应该是:

  • 面板值持续刷新
  • 日志不断打印 KeyChannels=[...]

12.3 原始信号质量

观察这些字段是否有值:

  • dev_connect
  • ap_snr
  • ap_gain_a
  • ap_gain_b
  • signal

同时日志里能看到:

text 复制代码
KeyRawSignalQuality dev_connect=..., ap_snr=..., ap_gain_a=..., ap_gain_b=..., signal=...

13. 最终效果

最终实现效果是:

  • Android 平台启动后自动初始化 Skydroid RCSDK
  • 自动尝试连接 G20 遥控器
  • 每 100ms 拉取一次摇杆感量
  • 持续监听原始信号质量
  • 在 FlyView 中临时显示实时值
  • 同时打印关键日志,便于联调

这样后面不管是继续做:

  • 正式 UI 展示
  • 遥控器状态栏
  • 异常告警
  • 遥控器链路质量诊断

都有了现成基础。


14. 这次接入的几个结论

14.1 第三方 Android SDK 接到 Qt 项目里,最难的通常不是接口调用

真正麻烦的通常是:

  • AAR 资源拆包
  • so 打包
  • assets 打包
  • Java/Gradle 缓存
  • Kotlin runtime 依赖

14.2 先跑通"可见的最小闭环"很重要

这次没有一上来做复杂业务逻辑,而是先让 FlyView 显示:

  • 状态
  • 摇杆值
  • 信号质量字段

这样问题非常容易定位。

14.3 Qt Android 的构建缓存必须重视

如果你改了 android/ 目录内容,却没有触发 ANDROID_PACKAGE_SOURCE_DIR 重新刷新,很容易一直运行旧包。


15. 相关文件

这次接入涉及的关键文件:

text 复制代码
android/src/org/mavlink/qgroundcontrol/QGCActivity.java
android/src/org/mavlink/qgroundcontrol/SkydroidRCSDKManager.java
android.pri
xsrc/XModule/AndroidRCInput.h
xsrc/XModule/AndroidRCInput.cc
src/QGCApplication.cc

15. 小结

这次在 Qt/QGroundControl 项目中接入 Skydroid G20 Android SDK,核心思路是:

  • Java 层采集
  • Qt 层桥接
  • QML 层显示
  • Android 包层补齐 jar/so/assets/Kotlin runtime

功能本身不复杂,但工程接线、打包和运行时依赖处理非常关键。

如果你也在 Qt Android 项目里接第三方遥控器、地图、语音、串口或者厂商 SDK,我的建议还是那句:

先把"最小可见闭环"做出来,再逐步产品化。

这样排错效率最高。


如果这篇文章对你有帮助,欢迎交流。

相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai10 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜10 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa10 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio