文章目录
-
- [0. 最终效果](#0. 最终效果)
- [1. 背景](#1. 背景)
- [2. 需求拆解](#2. 需求拆解)
-
- 需求1:获取遥控器摇杆感量
- [需求2:获取 G20 遥控器的原始信号质量](#需求2:获取 G20 遥控器的原始信号质量)
- [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 版本中,并临时在飞行界面里显示两类数据:
- 遥控器摇杆感量
- 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:遥控器 SNRap_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();
也就是:
- 初始化 SDK
- 设置回调线程
- 发起连接遥控器
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_connectap_snrap_gain_aap_gain_bsignal
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_connectap_snrap_gain_aap_gain_bsignal
同时打印:
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 属性:
connectedstatusTextchannelsTextdevConnectapSnrapGainAapGainBsignal
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.jarassetsjni/*.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/*.so、jni/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 initializingconnectToRC calledrc connectedchannels okraw signal ok
12.2 摇杆值
推动 G20 摇杆,看 Channels 数组是否变化。
因为 G20 是 GET 方式,所以表现应该是:
- 面板值持续刷新
- 日志不断打印
KeyChannels=[...]
12.3 原始信号质量
观察这些字段是否有值:
dev_connectap_snrap_gain_aap_gain_bsignal
同时日志里能看到:
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,我的建议还是那句:
先把"最小可见闭环"做出来,再逐步产品化。
这样排错效率最高。
如果这篇文章对你有帮助,欢迎交流。