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,我的建议还是那句:

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

这样排错效率最高。


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

相关推荐
曾阿伦2 小时前
Python项目管理从Poetry迁移到uv:极速体验与实操指南
开发语言·python·uv
Be for thing2 小时前
Android 屏幕硬件原理 + 显示驱动与功耗优化实战(手机 / 手表通用)
android·学习·智能手机
2401_891482172 小时前
C++中的观察者模式
开发语言·c++·算法
冰语竹2 小时前
滚动视图HorizontalScrollView和ScrollView
android
AnalogElectronic2 小时前
windows文件加解密工具,python实现,速度极快,篡改文件头尾信息以及还原
开发语言·windows·python
xyq20242 小时前
《jQuery UI 设计主题》
开发语言
myloveasuka2 小时前
Object&Objects
java·开发语言
sibylyue2 小时前
JDK 17 +spiring boot+ maven 应用服务 高并发调优
java·开发语言·maven
studyForMokey2 小时前
【跨端技术ReactNative】JavaScript学习
android·javascript·学习·react native·react.js