简介:这是一份基于 Java 和 Kotlin 开发的大疆移动端 SDK Android V5 版无人机操作演示源码,面向 Android 应用开发者、无人机爱好者以及需要深度定制大疆功能的工程师,可用于快速熟悉 SDK 接口与构建自己的控制应用。项目从界面布局到底层控制逻辑均有完整实现,包含七百七十六个 XML 配置文件、四百六十个 Java 源文件、三百余个 Kotlin 源文件,以及 PNG、webp 图片素材、MP3、WAV 音频资源,压缩包共两千个文件,大小约四十四兆。项目中的说明文档提供了安装和使用指引,android-sdk-v5-uxsdk、android-sdk-v5-as、android-sdk-v5-sample 三个模块分别对应用户界面、自动飞行和示例代码,结构清晰,便于按需查阅。目前已有四百三十人学习下载,通过源码可学习 UXSDK 界面模块、AS 自动飞行模块和 Sample 示例的调用方式,理解无人机控制、地图组件和 HSIMarkerLayer 图层等关键功能的实现思路。代码开源、可自由修改复用,是无人机应用入门与进阶的实用参考。
1. 为什么说DJI Mobile SDK V5的开发版Demo值得先研究源码再动手
做无人机Android端开发,最怕的不是飞行控制逻辑写不复杂,而是SDK注册第一步就卡住。DJI Mobile SDK Android V5开发版Demo的设计,恰好能解决你后面八成对接问题。很多人拿到SDK直接写业务,然后在注册、搜索绑定、回调线程三个环节来回翻车,耗掉一周才发现,不如先把一个已成型的Demo源码完整过一遍。
这篇笔记会从Java和Kotlin混编的角度,把V5的注册链路、工程拆分、飞行控制与相机操作、高频踩坑点拆开讲。适合两类人:刚拿到开发板想跑通首次起飞的新手,以及在V4时代写过业务、准备迁移到V5的老手。看完你能照着搭工程,也能提前知道自己代码通常在哪个环节出问题。
2. DJI Mobile SDK V5的注册连接链路:从App Key到设备绑定
2.1 SDK注册流程中各环节为什么不能省
DJI Mobile SDK V5在真机上的启动流程比大部分Android蓝牙SDK严格得多。App Key和应用包名强绑定,注册回调里返回非0错误码,八成问题出在包名或签名上。开发版Demo把注册放在Application里,有两个直接好处:一是任何Activity启动前SDK已初始化;二是注册状态全局可查,不必每个页面重复判断。
实际代码写起来不长:
class App : Application() { override fun onCreate() { super.onCreate() initSdk() } private fun initSdk() { // 注册App Key,回调里只更新全局状态,不弹UI SDKManager.getInstance().registerApp(this, object : SDKManager.Callback { override fun onRegisterSuccess() { SdkState.registered = true } override fun onRegisterFailure(errorCode: Int) { SdkState.registered = false Log.e("SdkRegister", "errorCode=$errorCode") } }) } }逻辑说明:这段代码的思路是注册结果先落全局状态,主界面启动时再读取。参数上registerApp第一个参数传Application自身,第二个参数是注册回调。成败都要把errorCode打日志,这个错误码是后续排查的核心线索。onRegisterSuccess只表示SDK完成了App Key认证,不表示数据链路已经连通,所以这里不用做任何跳转。
这里有个边界情况容易忽略:App Key校验走公网,首次注册可能因为网络代理、路由器拦截等原因失败,重试一次就通过。开发版Demo通常不写重试逻辑,但我建议在自己的工程里包一层最多两次重试的注册方法,户外调试会很受用。另外多说一句,V5和V4的注册流程表面上很像,但内部签名验证逻辑换过一套,V4时期能用的调试技巧,比如直接替换debug签名,在V5上未必生效。开发版Demo的价值就在这里:它把所有可变的配置项固定成了你自己的包名和签名组合,照着改参数就能跑通。
注册成功后才能触发设备搜索绑定。这个顺序是SDK内部强制的,没注册成功就调用搜索,回调会一直挂在空状态。Demo入口页一般做成:收到注册成功回调后,才把"连接设备"按钮置为可点。
2.2 searchAndBind的触发时机与Product对象获取
拿到飞行器对象是后续一切操作的前提。DJI Mobile SDK V5的标准链路是:遥控器连上手机、飞行器开机、发起搜索绑定、回调返回Product实例。这个Product是飞行控制、相机、图传等模块的共同入口。
常见的封装是单独一个管理类:
public class ProductManager { private Product current; public void bind() { SDKManager.getInstance().searchAndBind(device -> { current = device; if (current != null) { current.connect(); } }); } public Product get() { return current; } }逻辑说明:searchAndBind的入参是回调,返回布尔值表示是否找到设备。把Product保存在单例类里,各个页面就不必反复搜索。参数上要注意,该方法要求飞行器与遥控器之间已经建立数据链路,使用开发板直连时要用USB线连飞行器,否则回调返回false。
拿到Product之后,还需要注册连接状态订阅。V5 SDK的连接状态回调是全局的,不建议挂在某个Activity里。我的习惯是单独写一个ConnectionMonitor,在应用级注册,页面销毁时也不要反复注册。订阅代码类似这样:
SDKManager.getInstance().addConnectionStateCallback { isConnected -> runOnUiThread { if (isConnected) { // 刷新UI为已连接 } else { // 提示设备断开 } } }逻辑说明:回调线程不保证在主线程,所以用runOnUiThread包裹UI操作。参数上isConnected只代表数据链路是否通,不能代表固件健康,固件问题要等FlightController状态回调来判断。如果搜索绑定总是失败,先不要怀疑SDK,观察遥控器屏幕有没有提示USB已连接。很多Android设备第一次插入时需要授权USB调试权限,这个弹窗不点掉,SDK即使搜索到也会立刻断开。到这一步,注册、绑定、订阅的主干链路闭环了,后面飞行控制和相机模块都建立在这条链路上。
3. 搭建Demo工程:Java与Kotlin共存的模块划分
3.1 最小工程结构与Gradle依赖选择
标题里的Java和Kotlin不是花架子。DJI Mobile SDK V5底层是Java,官方示例偏向Java,但新业务团队写Kotlin更顺手。开发版Demo的典型解法是:SDK封装层保持Java,业务层用Kotlin。这样封装层能和SDK类型系统无缝对接,业务层写起来省代码。
我自己的工程目录习惯是这样分:
- java/sdk:注册管理、Product管理、日志管理,这一层不依赖任何UI。
- kotlin/ui:主界面、飞行控制界面、相机界面,只调用封装层。
- kotlin/vm:各个界面的ViewModel,业务状态在这里流转。
Gradle里DJI SDK的引入方式不同版本差异很大,有给aar的,也有给远端坐标的。这里只示意一段最小依赖:
dependencies { implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' }逻辑说明:kotlin-stdlib是Kotlin工程的运行时依赖,版本跟随你用的Gradle Kotlin插件;appcompat和material负责基础主题与控件。参数上注意SDK的minSdk要求,V5一般要求Android 6.0以上,project里defaultConfig最好保持一致,避免低端机在SDK初始化时直接闪退。引入aar后Jetifier的开关也要确认,否则老SDK包里的support库引用转换不过去。
还有一个经常翻车的位置是AndroidManifest的App Key配置,很多开发者把meta-data写进activity节点,结果注册永远失败。正确位置是application节点下:
<application android:name=".App" android:usesCleartextTraffic="false"> <meta-data android:name="DJI_APP_KEY" android:value="替换为你的AppKey" /> </application>逻辑说明:meta-data的name固定,value对应开发者后台生成的那串Key。usesCleartextTraffic在Android 9以上会影响SDK内部的部分本地通信,如果后面要拉图传本地预览流,建议用networkSecurityConfig做白名单,而不是直接给整个应用开明文。
3.2 Kotlin调用Java回调时泛型与空安全的处理
Kotlin与Java互操作在这里最容易出错的是平台类型。Java方法没有标注@NonNull或@Nullable,Kotlin编译器把返回值当平台类型,编译期不做强制判断,运行期依然可能抛空指针。飞行控制代码里一旦NPE,出现的位置和时机无法预估。所以我在封装层对SDK返回对象一律显式判空。
以取飞行控制器为例:
fun getFlightController(): FlightController? { val product = ProductManager.get() ?: return null return product.getFlightController() }逻辑说明:ProductManager.get()返回的平台类型在Kotlin里用?:转成安全调用。参数上注意getFlightController本身也可能返回null,比如当前产品不带飞控能力。拿到FlightController以后,调用具体方法前还要检查连接状态,因为设备断开后对象引用非空,底层链路已经不可用。
另一个互操作坑是泛型回调。SDK里常见Callback 接口,Java侧实现体会自动处理泛型,Kotlin侧用匿名对象实现时若类型对不上,回调会被SDK内部走错误分支。我建议在Java封装层多做一层适配,把泛型接口收敛成业务语义明确的普通接口。
public interface DeviceConnectListener { void onDeviceConnected(); void onDeviceDisconnected(); }把SDK里的泛型回调在Java层适配成这个,Kotlin端只需要用lambda实现两个方法。这层适配不复杂,但能把大半空安全与泛型问题挡在外面,也能让封装层更容易写单元测试。Demo里尤其值得保留这层,因为它展示的既是结构,也是踩坑后的解法。
4. 核心功能模块设计与实现:飞行控制与相机的关键链路
4.1 飞行控制模块:状态机判断与起飞降落
飞行控制是Demo最核心的部分,也是最容易出事故的部分。DJI Mobile SDK V5里飞行控制集中在FlightController对象上,所有命令异步执行,回调只表示SDK是否收下指令,不代表飞行器已经完成动作。这个区别会直接影响你后续状态机设计。
起飞命令的常见写法:
fun takeOff() { val controller = getFlightController() ?: return val state = controller.getState() ?: return if (state.isFlying) { toast("飞行中") return } controller.startTakeoff(object : CommonCallbacks.CompletionCallback { override fun onSuccess() { // 指令已下发,等状态回调确认 } override fun onFailure(error: Error) { logError(error) } }) }逻辑说明:先判空,再查状态,最后发指令。isFlying判断防止重复起飞。参数上CompletionCallback里的Error对象带错误码和描述,排查问题时优先看错误码。startTakeoff的onSuccess不要当成"起飞成功",真正判断要看后面状态订阅里isFlying从false变true。
降落比起飞更值得较真。如果航线任务还在执行,直接请求降落会被SDK驳回。Demo里我会把所有降落请求收敛到一个safeLanding方法里:
fun safeLanding() { val controller = getFlightController() ?: return if (controller.isWaypointMissionRunning()) { controller.stopWaypointMission(object : CommonCallbacks.CompletionCallback { override fun onSuccess() { controller.startLanding(landingCallback) } override fun onFailure(error: Error) { logError(error) } }) } else { controller.startLanding(landingCallback) } }逻辑说明:先判断航点任务是否在跑,在跑就先停再降,没跑就直接降。参数上isWaypointMissionRunning要在设备绑定状态下调用,否则返回不可用。不管是哪条路径,降落完成都要等到状态订阅里isLanding变化,不要刚发完指令就执行断电收桨,那会直接摔桨。
这里补充一个状态机设计的建议:不要在每个按钮点击方法里单独判断状态,而是把"当前是否可起飞、是否可降落、是否可拍照"统一放到一个ViewModel的StateFlow里,按SDK状态回调更新。这样UI层只管订阅,状态判断只有一个出口,逻辑不容易出现分歧。
4.2 相机控制与图传预览:关键参数与线程切换
相机模块需要优先考虑的是模式切换时机。拍照前必须先把相机切到拍照模式,录像前切到录像模式,模式切换是异步的,必须等成功回调后再发下一步指令。
camera?.setMode(CameraMode.SHOOT_PHOTO, object : CommonCallbacks.CompletionCallback { override fun onSuccess() { camera?.startShootPhoto(object : CommonCallbacks.CompletionCallback { override fun onSuccess() { // 拍照指令已下发 } override fun onFailure(error: Error) { logError(error) } }) } override fun onFailure(error: Error) { logError(error) } })逻辑说明:setMode成功回调里再发startShootPhoto,两层嵌套安全但是不够优雅,Demo里可以保留,生产项目建议换成状态机。参数上SHOOT_PHOTO模式不是所有相机都支持,比如热成像相机或某些变焦机型,正式产品要按机型处理,Demo可以先忽略。
图传预览流是另一个高频问题。V5的图传数据通过VideoFeeder监听,拿到数据后调SurfaceHolder绘制。这块的线程要求很严格:VideoDataListener回调频率高,不要在回调里做JSON序列化、日志打印或任何分配内存的操作,否则帧率会肉眼可见地掉。
VideoFeeder.getInstance().getPrimaryVideoFeed().addVideoDataListener((data, size) -> { // 这里只做绘制,不做耗时操作 });逻辑说明:addVideoDataListener的入参是数据和长度,回调频率高,绘制实现要直接操作Surface。参数上注意在页面销毁时移出监听,不然后台线程还在拉流,会引发内存抖动。SDK的图传数据是私有格式,不要试图自己去解码,用SDK配套的渲染接口即可。
媒体文件落盘也需要提前规划。拍照文件默认存到机身SD卡,Demo可用MediaManager拉取缩略图列表。缓存目录建议指向应用私有目录,不要写到公共下载目录,省掉存储权限的申请。这个细节会让演示时省很多口水。
5. DJI Mobile SDK V5 Demo开发避坑:注册失败、连接掉线与回调不触发
5.1 坑一:真机注册一直报包名不匹配
现象:代码和配置都看着没问题,同事电脑上注册成功,自己电脑上一模一样的工程就是注册失败,错误码指向包名不对。
原因:开发者后台创建应用时填的包名,和当前AndroidManifest里的applicationId不一致。多数团队里release和debug用不同签名,后台却只登记了一个签名指纹,于是debug包一直失败。
解决:先核对后台包名,再看build.gradle里applicationId,最后确认签名指纹是不是和后台一致。三步按顺序查,不要只盯着meta-data里的App Key。这是最典型的"看起来是配置问题,其实是签名问题"的案例,血泪经验。
5.2 坑二:searchAndBind搜不到设备
现象:飞行器开机、遥控器也连了手机,日志没有任何错误,但searchAndBind回调返回false,产品始终为null。
原因:第一次连接时系统弹了USB调试授权框,没人点确认;或者手机只认了充电模式,数据通路没建立。这个问题在台架测试时不容易发现,因为户外顶着太阳看不清屏幕就漏点了。
解决:把手机调成文件传输模式,重新插拔一次数据线,确保授权弹窗被点掉。如果还不行,检查遥控器固件和SDK版本是否匹配。这里的坑不在代码,在操作流程,所以我会在Demo的连接页放一个"USB连接检查"按钮,把当前连接模式显式显示出来。
5.3 坑三:命令回调不触发,像石沉大海
现象:点击起飞后,既没有onSuccess也没有onFailure,日志被吞掉,整个命令没有反馈。
原因:回调发生在非主线程,而主线程此时卡死,或该回调被SDK内部用于同步处理,在你当前调用时机下被忽略了。某些SDK版本里,连续多次调用同一个命令,超时后只回调最后一次。
解决:在回调入口打一套时间戳日志,先确认回调线程。把超时逻辑做成最外层保障,比如3秒没有onSuccess就置为失败,不要干等。这条解决思路在Demo里值得直接写进基类回调,让所有命令统一走超时处理,避免把SDK当黑匣子。
5.4 坑四:Kotlin lambda捕获过期Activity
现象:飞行状态日志一直正常,但UI上的高度、速度长时间不刷新,甚至偶发崩溃。
原因:lambda捕获了Activity实例,Activity重建后匿名对象还持有旧引用,更新的全是旧界面。这在Kotlin里比Java更容易写出来,因为lambda隐式捕获太自然了。
解决:UI更新前统一走Lifecycle组件,让回调只更新ViewModel,不直接碰Activity;必须碰UI时用WeakReference。Demo里如果看到了好解决,但更推荐从一开始就约定:所有SDK回调只做状态更新,不直接操作View。
5.5 坑五:图传预览黑屏但日志无异常
现象:进入相机页面后Surface黑屏,日志没有任何错误,飞行控制正常。
原因:SurfaceView的surfaceCreated回调时序没处理好,图传数据被绑定到了还没准备好的Surface上;或者页面重建后Surface被销毁,但旧的VideoDataListener还在往旧Surface上写。
解决:在surfaceCreated之后再去绑定VideoFeed,surfaceDestroyed时先移除监听再释放Surface。这个坑在普通App开发里完全不存在,只有做图传渲染才会遇到,建议把Surface的生命周期处理单独抽成一个类,不要散落在Activity里。
6. 从Demo到可用产品:日志分级、台架测试与断线重连验证
Demo跑通只代表功能链路通了,离产品还差一层验证。这里分享三个我常用的手段。
第一,日志分级。把SDK日志等级调到DEBUG,Tag统一前缀,然后过滤查看。起飞、拍照、图传每类操作都单独打点,方便连续调试。用命令行过滤能少翻很多无用的系统消息。
adb logcat -s SdkRegister:D DJI_SDK:V *:S这条命令只保留SdkRegister和DJI_SDK两个Tag的日志,其他全部静音。逻辑上是为了把SDK内部状态和业务日志隔离开,参数上SdkRegister是我自定义的业务Tag,实际使用时换成你工程里的Tag名。
第二,台架测试。把桨叶拆掉,用绑带把机架固定在工作台上。起飞指令照发,观察是否有离地趋势。关键不是看飞机动不动,而是验证回调时序:isFlying状态字段有没有按预期跳变,onSuccess和状态回调的先后顺序对不对。这一步能暴露大量异步问题。
第三,断线重连验证。直接关掉遥控器电源,等三秒再打开,看Product是否重建、订阅是否恢复、UI状态是否跟随变化。这个流程建议写成检查清单,每轮改动都跑一遍。我记得某次户外演示,开发好的App第一次试飞很顺利,第二次开机后图传一直黑屏,最后发现是Surface重建后VideoFeed没重新绑定。这个bug在台架上根本触发不了,只有真机断线重连才复现。从那以后我把断线重连纳入必测项。
希望这篇笔记里的链路拆解和踩坑记录能帮到你,让你的DJI Mobile SDK V5开发版Demo少走几步弯路。
本文还有配套的精品资源,点击获取