戰(zhàn)指南)
簡介這是一份基于 Java 和 Kotlin 開發(fā)的大疆移動端 SDK Android V5 版無人機(jī)操作演示源碼面向 Android 應(yīng)用開發(fā)者、無人機(jī)愛好者以及需要深度定制大疆功能的工程師可用于快速熟悉 SDK 接口與構(gòu)建自己的控制應(yīng)用。項(xiàng)目從界面布局到底層控制邏輯均有完整實(shí)現(xiàn)包含七百七十六個(gè) XML 配置文件、四百六十個(gè) Java 源文件、三百余個(gè) Kotlin 源文件以及 PNG、webp 圖片素材、MP3、WAV 音頻資源壓縮包共兩千個(gè)文件大小約四十四兆。項(xiàng)目中的說明文檔提供了安裝和使用指引android-sdk-v5-uxsdk、android-sdk-v5-as、android-sdk-v5-sample 三個(gè)模塊分別對應(yīng)用戶界面、自動飛行和示例代碼結(jié)構(gòu)清晰便于按需查閱。目前已有四百三十人學(xué)習(xí)下載通過源碼可學(xué)習(xí) UXSDK 界面模塊、AS 自動飛行模塊和 Sample 示例的調(diào)用方式理解無人機(jī)控制、地圖組件和 HSIMarkerLayer 圖層等關(guān)鍵功能的實(shí)現(xiàn)思路。代碼開源、可自由修改復(fù)用是無人機(jī)應(yīng)用入門與進(jìn)階的實(shí)用參考。1. 為什么說DJI Mobile SDK V5的開發(fā)版Demo值得先研究源碼再動手做無人機(jī)Android端開發(fā)最怕的不是飛行控制邏輯寫不復(fù)雜而是SDK注冊第一步就卡住。DJI Mobile SDK Android V5開發(fā)版Demo的設(shè)計(jì)恰好能解決你后面八成對接問題。很多人拿到SDK直接寫業(yè)務(wù)然后在注冊、搜索綁定、回調(diào)線程三個(gè)環(huán)節(jié)來回翻車耗掉一周才發(fā)現(xiàn)不如先把一個(gè)已成型的Demo源碼完整過一遍。這篇筆記會從Java和Kotlin混編的角度把V5的注冊鏈路、工程拆分、飛行控制與相機(jī)操作、高頻踩坑點(diǎn)拆開講。適合兩類人剛拿到開發(fā)板想跑通首次起飛的新手以及在V4時(shí)代寫過業(yè)務(wù)、準(zhǔn)備遷移到V5的老手。看完你能照著搭工程也能提前知道自己代碼通常在哪個(gè)環(huán)節(jié)出問題。2. DJI Mobile SDK V5的注冊連接鏈路從App Key到設(shè)備綁定2.1 SDK注冊流程中各環(huán)節(jié)為什么不能省DJI Mobile SDK V5在真機(jī)上的啟動流程比大部分Android藍(lán)牙SDK嚴(yán)格得多。App Key和應(yīng)用包名強(qiáng)綁定注冊回調(diào)里返回非0錯(cuò)誤碼八成問題出在包名或簽名上。開發(fā)版Demo把注冊放在Application里有兩個(gè)直接好處一是任何Activity啟動前SDK已初始化二是注冊狀態(tài)全局可查不必每個(gè)頁面重復(fù)判斷。實(shí)際代碼寫起來不長class App : Application() { override fun onCreate() { super.onCreate() initSdk() } private fun initSdk() { // 注冊App Key回調(diào)里只更新全局狀態(tài)不彈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) } }) } }邏輯說明這段代碼的思路是注冊結(jié)果先落全局狀態(tài)主界面啟動時(shí)再讀取。參數(shù)上registerApp第一個(gè)參數(shù)傳Application自身第二個(gè)參數(shù)是注冊回調(diào)。成敗都要把errorCode打日志這個(gè)錯(cuò)誤碼是后續(xù)排查的核心線索。onRegisterSuccess只表示SDK完成了App Key認(rèn)證不表示數(shù)據(jù)鏈路已經(jīng)連通所以這里不用做任何跳轉(zhuǎn)。這里有個(gè)邊界情況容易忽略App Key校驗(yàn)走公網(wǎng)首次注冊可能因?yàn)榫W(wǎng)絡(luò)代理、路由器攔截等原因失敗重試一次就通過。開發(fā)版Demo通常不寫重試邏輯但我建議在自己的工程里包一層最多兩次重試的注冊方法戶外調(diào)試會很受用。另外多說一句V5和V4的注冊流程表面上很像但內(nèi)部簽名驗(yàn)證邏輯換過一套V4時(shí)期能用的調(diào)試技巧比如直接替換debug簽名在V5上未必生效。開發(fā)版Demo的價(jià)值就在這里它把所有可變的配置項(xiàng)固定成了你自己的包名和簽名組合照著改參數(shù)就能跑通。注冊成功后才能觸發(fā)設(shè)備搜索綁定。這個(gè)順序是SDK內(nèi)部強(qiáng)制的沒注冊成功就調(diào)用搜索回調(diào)會一直掛在空狀態(tài)。Demo入口頁一般做成收到注冊成功回調(diào)后才把連接設(shè)備按鈕置為可點(diǎn)。2.2 searchAndBind的觸發(fā)時(shí)機(jī)與Product對象獲取拿到飛行器對象是后續(xù)一切操作的前提。DJI Mobile SDK V5的標(biāo)準(zhǔn)鏈路是遙控器連上手機(jī)、飛行器開機(jī)、發(fā)起搜索綁定、回調(diào)返回Product實(shí)例。這個(gè)Product是飛行控制、相機(jī)、圖傳等模塊的共同入口。常見的封裝是單獨(dú)一個(gè)管理類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的入?yún)⑹腔卣{(diào)返回布爾值表示是否找到設(shè)備。把Product保存在單例類里各個(gè)頁面就不必反復(fù)搜索。參數(shù)上要注意該方法要求飛行器與遙控器之間已經(jīng)建立數(shù)據(jù)鏈路使用開發(fā)板直連時(shí)要用USB線連飛行器否則回調(diào)返回false。拿到Product之后還需要注冊連接狀態(tài)訂閱。V5 SDK的連接狀態(tài)回調(diào)是全局的不建議掛在某個(gè)Activity里。我的習(xí)慣是單獨(dú)寫一個(gè)ConnectionMonitor在應(yīng)用級注冊頁面銷毀時(shí)也不要反復(fù)注冊。訂閱代碼類似這樣SDKManager.getInstance().addConnectionStateCallback { isConnected - runOnUiThread { if (isConnected) { // 刷新UI為已連接 } else { // 提示設(shè)備斷開 } } }邏輯說明回調(diào)線程不保證在主線程所以用runOnUiThread包裹UI操作。參數(shù)上isConnected只代表數(shù)據(jù)鏈路是否通不能代表固件健康固件問題要等FlightController狀態(tài)回調(diào)來判斷。如果搜索綁定總是失敗先不要懷疑SDK觀察遙控器屏幕有沒有提示USB已連接。很多Android設(shè)備第一次插入時(shí)需要授權(quán)USB調(diào)試權(quán)限這個(gè)彈窗不點(diǎn)掉SDK即使搜索到也會立刻斷開。到這一步注冊、綁定、訂閱的主干鏈路閉環(huán)了后面飛行控制和相機(jī)模塊都建立在這條鏈路上。3. 搭建Demo工程Java與Kotlin共存的模塊劃分3.1 最小工程結(jié)構(gòu)與Gradle依賴選擇標(biāo)題里的Java和Kotlin不是花架子。DJI Mobile SDK V5底層是Java官方示例偏向Java但新業(yè)務(wù)團(tuán)隊(duì)寫Kotlin更順手。開發(fā)版Demo的典型解法是SDK封裝層保持Java業(yè)務(wù)層用Kotlin。這樣封裝層能和SDK類型系統(tǒng)無縫對接業(yè)務(wù)層寫起來省代碼。我自己的工程目錄習(xí)慣是這樣分java/sdk注冊管理、Product管理、日志管理這一層不依賴任何UI。kotlin/ui主界面、飛行控制界面、相機(jī)界面只調(diào)用封裝層。kotlin/vm各個(gè)界面的ViewModel業(yè)務(wù)狀態(tài)在這里流轉(zhuǎn)。Gradle里DJI SDK的引入方式不同版本差異很大有給aar的也有給遠(yuǎn)端坐標(biāo)的。這里只示意一段最小依賴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工程的運(yùn)行時(shí)依賴版本跟隨你用的Gradle Kotlin插件appcompat和material負(fù)責(zé)基礎(chǔ)主題與控件。參數(shù)上注意SDK的minSdk要求V5一般要求Android 6.0以上project里defaultConfig最好保持一致避免低端機(jī)在SDK初始化時(shí)直接閃退。引入aar后Jetifier的開關(guān)也要確認(rèn)否則老SDK包里的support庫引用轉(zhuǎn)換不過去。還有一個(gè)經(jīng)常翻車的位置是AndroidManifest的App Key配置很多開發(fā)者把meta-data寫進(jìn)activity節(jié)點(diǎn)結(jié)果注冊永遠(yuǎn)失敗。正確位置是application節(jié)點(diǎn)下application android:name.App android:usesCleartextTrafficfalse meta-data android:nameDJI_APP_KEY android:value替換為你的AppKey / /application邏輯說明meta-data的name固定value對應(yīng)開發(fā)者后臺生成的那串Key。usesCleartextTraffic在Android 9以上會影響SDK內(nèi)部的部分本地通信如果后面要拉圖傳本地預(yù)覽流建議用networkSecurityConfig做白名單而不是直接給整個(gè)應(yīng)用開明文。3.2 Kotlin調(diào)用Java回調(diào)時(shí)泛型與空安全的處理Kotlin與Java互操作在這里最容易出錯(cuò)的是平臺類型。Java方法沒有標(biāo)注NonNull或NullableKotlin編譯器把返回值當(dāng)平臺類型編譯期不做強(qiáng)制判斷運(yùn)行期依然可能拋空指針。飛行控制代碼里一旦NPE出現(xiàn)的位置和時(shí)機(jī)無法預(yù)估。所以我在封裝層對SDK返回對象一律顯式判空。以取飛行控制器為例fun getFlightController(): FlightController? { val product ProductManager.get() ?: return null return product.getFlightController() }邏輯說明ProductManager.get()返回的平臺類型在Kotlin里用?:轉(zhuǎn)成安全調(diào)用。參數(shù)上注意getFlightController本身也可能返回null比如當(dāng)前產(chǎn)品不帶飛控能力。拿到FlightController以后調(diào)用具體方法前還要檢查連接狀態(tài)因?yàn)樵O(shè)備斷開后對象引用非空底層鏈路已經(jīng)不可用。另一個(gè)互操作坑是泛型回調(diào)。SDK里常見Callback 接口Java側(cè)實(shí)現(xiàn)體會自動處理泛型Kotlin側(cè)用匿名對象實(shí)現(xiàn)時(shí)若類型對不上回調(diào)會被SDK內(nèi)部走錯(cuò)誤分支。我建議在Java封裝層多做一層適配把泛型接口收斂成業(yè)務(wù)語義明確的普通接口。public interface DeviceConnectListener { void onDeviceConnected(); void onDeviceDisconnected(); }把SDK里的泛型回調(diào)在Java層適配成這個(gè)Kotlin端只需要用lambda實(shí)現(xiàn)兩個(gè)方法。這層適配不復(fù)雜但能把大半空安全與泛型問題擋在外面也能讓封裝層更容易寫單元測試。Demo里尤其值得保留這層因?yàn)樗故镜募仁墙Y(jié)構(gòu)也是踩坑后的解法。4. 核心功能模塊設(shè)計(jì)與實(shí)現(xiàn)飛行控制與相機(jī)的關(guān)鍵鏈路4.1 飛行控制模塊狀態(tài)機(jī)判斷與起飛降落飛行控制是Demo最核心的部分也是最容易出事故的部分。DJI Mobile SDK V5里飛行控制集中在FlightController對象上所有命令異步執(zhí)行回調(diào)只表示SDK是否收下指令不代表飛行器已經(jīng)完成動作。這個(gè)區(qū)別會直接影響你后續(xù)狀態(tài)機(jī)設(shè)計(jì)。起飛命令的常見寫法fun takeOff() { val controller getFlightController() ?: return val state controller.getState() ?: return if (state.isFlying) { toast(飛行中) return } controller.startTakeoff(object : CommonCallbacks.CompletionCallback { override fun onSuccess() { // 指令已下發(fā)等狀態(tài)回調(diào)確認(rèn) } override fun onFailure(error: Error) { logError(error) } }) }邏輯說明先判空再查狀態(tài)最后發(fā)指令。isFlying判斷防止重復(fù)起飛。參數(shù)上CompletionCallback里的Error對象帶錯(cuò)誤碼和描述排查問題時(shí)優(yōu)先看錯(cuò)誤碼。startTakeoff的onSuccess不要當(dāng)成起飛成功真正判斷要看后面狀態(tài)訂閱里isFlying從false變true。降落比起飛更值得較真。如果航線任務(wù)還在執(zhí)行直接請求降落會被SDK駁回。Demo里我會把所有降落請求收斂到一個(gè)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) } }邏輯說明先判斷航點(diǎn)任務(wù)是否在跑在跑就先停再降沒跑就直接降。參數(shù)上isWaypointMissionRunning要在設(shè)備綁定狀態(tài)下調(diào)用否則返回不可用。不管是哪條路徑降落完成都要等到狀態(tài)訂閱里isLanding變化不要剛發(fā)完指令就執(zhí)行斷電收槳那會直接摔槳。這里補(bǔ)充一個(gè)狀態(tài)機(jī)設(shè)計(jì)的建議不要在每個(gè)按鈕點(diǎn)擊方法里單獨(dú)判斷狀態(tài)而是把當(dāng)前是否可起飛、是否可降落、是否可拍照統(tǒng)一放到一個(gè)ViewModel的StateFlow里按SDK狀態(tài)回調(diào)更新。這樣UI層只管訂閱狀態(tài)判斷只有一個(gè)出口邏輯不容易出現(xiàn)分歧。4.2 相機(jī)控制與圖傳預(yù)覽關(guān)鍵參數(shù)與線程切換相機(jī)模塊需要優(yōu)先考慮的是模式切換時(shí)機(jī)。拍照前必須先把相機(jī)切到拍照模式錄像前切到錄像模式模式切換是異步的必須等成功回調(diào)后再發(fā)下一步指令。camera?.setMode(CameraMode.SHOOT_PHOTO, object : CommonCallbacks.CompletionCallback { override fun onSuccess() { camera?.startShootPhoto(object : CommonCallbacks.CompletionCallback { override fun onSuccess() { // 拍照指令已下發(fā) } override fun onFailure(error: Error) { logError(error) } }) } override fun onFailure(error: Error) { logError(error) } })邏輯說明setMode成功回調(diào)里再發(fā)startShootPhoto兩層嵌套安全但是不夠優(yōu)雅Demo里可以保留生產(chǎn)項(xiàng)目建議換成狀態(tài)機(jī)。參數(shù)上SHOOT_PHOTO模式不是所有相機(jī)都支持比如熱成像相機(jī)或某些變焦機(jī)型正式產(chǎn)品要按機(jī)型處理Demo可以先忽略。圖傳預(yù)覽流是另一個(gè)高頻問題。V5的圖傳數(shù)據(jù)通過VideoFeeder監(jiān)聽拿到數(shù)據(jù)后調(diào)SurfaceHolder繪制。這塊的線程要求很嚴(yán)格VideoDataListener回調(diào)頻率高不要在回調(diào)里做JSON序列化、日志打印或任何分配內(nèi)存的操作否則幀率會肉眼可見地掉。VideoFeeder.getInstance().getPrimaryVideoFeed().addVideoDataListener((data, size) - { // 這里只做繪制不做耗時(shí)操作 });邏輯說明addVideoDataListener的入?yún)⑹菙?shù)據(jù)和長度回調(diào)頻率高繪制實(shí)現(xiàn)要直接操作Surface。參數(shù)上注意在頁面銷毀時(shí)移出監(jiān)聽不然后臺線程還在拉流會引發(fā)內(nèi)存抖動。SDK的圖傳數(shù)據(jù)是私有格式不要試圖自己去解碼用SDK配套的渲染接口即可。媒體文件落盤也需要提前規(guī)劃。拍照文件默認(rèn)存到機(jī)身SD卡Demo可用MediaManager拉取縮略圖列表。緩存目錄建議指向應(yīng)用私有目錄不要寫到公共下載目錄省掉存儲權(quán)限的申請。這個(gè)細(xì)節(jié)會讓演示時(shí)省很多口水。5. DJI Mobile SDK V5 Demo開發(fā)避坑注冊失敗、連接掉線與回調(diào)不觸發(fā)5.1 坑一真機(jī)注冊一直報(bào)包名不匹配現(xiàn)象代碼和配置都看著沒問題同事電腦上注冊成功自己電腦上一模一樣的工程就是注冊失敗錯(cuò)誤碼指向包名不對。原因開發(fā)者后臺創(chuàng)建應(yīng)用時(shí)填的包名和當(dāng)前AndroidManifest里的applicationId不一致。多數(shù)團(tuán)隊(duì)里release和debug用不同簽名后臺卻只登記了一個(gè)簽名指紋于是debug包一直失敗。解決先核對后臺包名再看build.gradle里applicationId最后確認(rèn)簽名指紋是不是和后臺一致。三步按順序查不要只盯著meta-data里的App Key。這是最典型的看起來是配置問題其實(shí)是簽名問題的案例血淚經(jīng)驗(yàn)。5.2 坑二searchAndBind搜不到設(shè)備現(xiàn)象飛行器開機(jī)、遙控器也連了手機(jī)日志沒有任何錯(cuò)誤但searchAndBind回調(diào)返回false產(chǎn)品始終為null。原因第一次連接時(shí)系統(tǒng)彈了USB調(diào)試授權(quán)框沒人點(diǎn)確認(rèn)或者手機(jī)只認(rèn)了充電模式數(shù)據(jù)通路沒建立。這個(gè)問題在臺架測試時(shí)不容易發(fā)現(xiàn)因?yàn)閼敉忭斨柨床磺迤聊痪吐c(diǎn)了。解決把手機(jī)調(diào)成文件傳輸模式重新插拔一次數(shù)據(jù)線確保授權(quán)彈窗被點(diǎn)掉。如果還不行檢查遙控器固件和SDK版本是否匹配。這里的坑不在代碼在操作流程所以我會在Demo的連接頁放一個(gè)USB連接檢查按鈕把當(dāng)前連接模式顯式顯示出來。5.3 坑三命令回調(diào)不觸發(fā)像石沉大海現(xiàn)象點(diǎn)擊起飛后既沒有onSuccess也沒有onFailure日志被吞掉整個(gè)命令沒有反饋。原因回調(diào)發(fā)生在非主線程而主線程此時(shí)卡死或該回調(diào)被SDK內(nèi)部用于同步處理在你當(dāng)前調(diào)用時(shí)機(jī)下被忽略了。某些SDK版本里連續(xù)多次調(diào)用同一個(gè)命令超時(shí)后只回調(diào)最后一次。解決在回調(diào)入口打一套時(shí)間戳日志先確認(rèn)回調(diào)線程。把超時(shí)邏輯做成最外層保障比如3秒沒有onSuccess就置為失敗不要干等。這條解決思路在Demo里值得直接寫進(jìn)基類回調(diào)讓所有命令統(tǒng)一走超時(shí)處理避免把SDK當(dāng)黑匣子。5.4 坑四Kotlin lambda捕獲過期Activity現(xiàn)象飛行狀態(tài)日志一直正常但UI上的高度、速度長時(shí)間不刷新甚至偶發(fā)崩潰。原因lambda捕獲了Activity實(shí)例Activity重建后匿名對象還持有舊引用更新的全是舊界面。這在Kotlin里比Java更容易寫出來因?yàn)閘ambda隱式捕獲太自然了。解決UI更新前統(tǒng)一走Lifecycle組件讓回調(diào)只更新ViewModel不直接碰Activity必須碰UI時(shí)用WeakReference。Demo里如果看到了好解決但更推薦從一開始就約定所有SDK回調(diào)只做狀態(tài)更新不直接操作View。5.5 坑五圖傳預(yù)覽黑屏但日志無異?,F(xiàn)象進(jìn)入相機(jī)頁面后Surface黑屏日志沒有任何錯(cuò)誤飛行控制正常。原因SurfaceView的surfaceCreated回調(diào)時(shí)序沒處理好圖傳數(shù)據(jù)被綁定到了還沒準(zhǔn)備好的Surface上或者頁面重建后Surface被銷毀但舊的VideoDataListener還在往舊Surface上寫。解決在surfaceCreated之后再去綁定VideoFeedsurfaceDestroyed時(shí)先移除監(jiān)聽再釋放Surface。這個(gè)坑在普通App開發(fā)里完全不存在只有做圖傳渲染才會遇到建議把Surface的生命周期處理單獨(dú)抽成一個(gè)類不要散落在Activity里。6. 從Demo到可用產(chǎn)品日志分級、臺架測試與斷線重連驗(yàn)證Demo跑通只代表功能鏈路通了離產(chǎn)品還差一層驗(yàn)證。這里分享三個(gè)我常用的手段。第一日志分級。把SDK日志等級調(diào)到DEBUGTag統(tǒng)一前綴然后過濾查看。起飛、拍照、圖傳每類操作都單獨(dú)打點(diǎn)方便連續(xù)調(diào)試。用命令行過濾能少翻很多無用的系統(tǒng)消息。adb logcat -s SdkRegister:D DJI_SDK:V *:S這條命令只保留SdkRegister和DJI_SDK兩個(gè)Tag的日志其他全部靜音。邏輯上是為了把SDK內(nèi)部狀態(tài)和業(yè)務(wù)日志隔離開參數(shù)上SdkRegister是我自定義的業(yè)務(wù)Tag實(shí)際使用時(shí)換成你工程里的Tag名。第二臺架測試。把槳葉拆掉用綁帶把機(jī)架固定在工作臺上。起飛指令照發(fā)觀察是否有離地趨勢。關(guān)鍵不是看飛機(jī)動不動而是驗(yàn)證回調(diào)時(shí)序isFlying狀態(tài)字段有沒有按預(yù)期跳變onSuccess和狀態(tài)回調(diào)的先后順序?qū)Σ粚Α_@一步能暴露大量異步問題。第三斷線重連驗(yàn)證。直接關(guān)掉遙控器電源等三秒再打開看Product是否重建、訂閱是否恢復(fù)、UI狀態(tài)是否跟隨變化。這個(gè)流程建議寫成檢查清單每輪改動都跑一遍。我記得某次戶外演示開發(fā)好的App第一次試飛很順利第二次開機(jī)后圖傳一直黑屏最后發(fā)現(xiàn)是Surface重建后VideoFeed沒重新綁定。這個(gè)bug在臺架上根本觸發(fā)不了只有真機(jī)斷線重連才復(fù)現(xiàn)。從那以后我把斷線重連納入必測項(xiàng)。希望這篇筆記里的鏈路拆解和踩坑記錄能幫到你讓你的DJI Mobile SDK V5開發(fā)版Demo少走幾步彎路。本文還有配套的精品資源點(diǎn)擊獲取