init
This commit is contained in:
116
sample/src/main/AndroidManifest.xml
Normal file
116
sample/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Sample permission requirement -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />-->
|
||||
<!-- <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />-->
|
||||
<!-- <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />-->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- 自安装使用 -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<!-- Google Maps -->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${GMAP_API_KEY}" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.accessory"
|
||||
android:required="true"/>
|
||||
|
||||
<application
|
||||
android:name="dji.sampleV5.aircraft.DJIAircraftApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_main"
|
||||
android:label="@string/app_name_aircraft"
|
||||
android:supportsRtl="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||
|
||||
<meta-data
|
||||
android:name="com.dji.sdk.API_KEY"
|
||||
android:value="${API_KEY}"/>
|
||||
|
||||
<!-- Maplibre Token-->
|
||||
<meta-data
|
||||
android:name="com.dji.mapkit.maplibre.apikey"
|
||||
android:value="${MAPLIBRE_TOKEN}" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name="dji.sampleV5.aircraft.DJIAircraftMainActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.zklh.dronecontroller.MainActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="dji.sampleV5.aircraft.UsbAttachActivity"
|
||||
android:theme="@style/translucent_theme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
|
||||
android:resource="@xml/accessory_filter" />
|
||||
</activity>
|
||||
|
||||
<activity android:name="dji.v5.ux.sample.showcase.defaultlayout.DefaultLayoutActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="dji.v5.ux.sample.showcase.widgetlist.WidgetsActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="dji.sampleV5.aircraft.AircraftTestingToolsActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
563809
sample/src/main/assets/flysafe/France.json
Normal file
563809
sample/src/main/assets/flysafe/France.json
Normal file
File diff suppressed because it is too large
Load Diff
1485
sample/src/main/assets/flysafe/de.geojson
Normal file
1485
sample/src/main/assets/flysafe/de.geojson
Normal file
File diff suppressed because one or more lines are too long
BIN
sample/src/main/assets/mop/mopSample.jpeg
Normal file
BIN
sample/src/main/assets/mop/mopSample.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 MiB |
BIN
sample/src/main/assets/mop/mopSample.mp4
Normal file
BIN
sample/src/main/assets/mop/mopSample.mp4
Normal file
Binary file not shown.
BIN
sample/src/main/assets/waypointsample.kmz
Normal file
BIN
sample/src/main/assets/waypointsample.kmz
Normal file
Binary file not shown.
115
sample/src/main/java/com/zklh/dronecontroller/MainActivity.kt
Normal file
115
sample/src/main/java/com/zklh/dronecontroller/MainActivity.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package com.zklh.dronecontroller
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.content.ContextCompat
|
||||
import dji.sampleV5.aircraft.models.MSDKManagerVM
|
||||
import dji.sampleV5.aircraft.models.globalViewModels
|
||||
import dji.v5.common.register.DJISDKInitEvent
|
||||
import com.zklh.dronecontroller.core.msdk.DroneSdkManager
|
||||
import com.zklh.dronecontroller.ui.DroneControllerScreen
|
||||
import com.zklh.dronecontroller.ui.ZklhDroneTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val msdkManagerVM: MSDKManagerVM by globalViewModels()
|
||||
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) {
|
||||
DroneSdkManager.initMobileSdk(application)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
observeOfficialMsdkManager()
|
||||
enterImmersiveMode()
|
||||
requestRuntimePermissions()
|
||||
setContent {
|
||||
ZklhDroneTheme {
|
||||
DroneControllerScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeOfficialMsdkManager() {
|
||||
msdkManagerVM.lvInitProcess.observe(this) { (event, totalProcess) ->
|
||||
DroneSdkManager.onOfficialInitProcess(
|
||||
eventName = event.name,
|
||||
totalProcess = totalProcess,
|
||||
initialized = event == DJISDKInitEvent.INITIALIZE_COMPLETE
|
||||
)
|
||||
}
|
||||
msdkManagerVM.lvRegisterState.observe(this) { (success, error) ->
|
||||
DroneSdkManager.onOfficialRegisterState(
|
||||
success = success,
|
||||
errorMessage = error?.toString()
|
||||
)
|
||||
}
|
||||
msdkManagerVM.lvProductConnectionState.observe(this) { (connected, productId) ->
|
||||
DroneSdkManager.onOfficialProductConnection(connected, productId)
|
||||
}
|
||||
msdkManagerVM.lvDBDownloadProgress.observe(this) { (current, total) ->
|
||||
DroneSdkManager.onOfficialDatabaseProgress(current, total)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
enterImmersiveMode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestRuntimePermissions() {
|
||||
val permissions = buildList {
|
||||
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
add(Manifest.permission.RECORD_AUDIO)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (permissions.isNotEmpty()) {
|
||||
permissionLauncher.launch(permissions.toTypedArray())
|
||||
} else {
|
||||
DroneSdkManager.initMobileSdk(application)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterImmersiveMode() {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.decorView.post {
|
||||
window.decorView.windowInsetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.flight.FlightControlService
|
||||
import com.zklh.dronecontroller.core.flight.FlyToService
|
||||
import com.zklh.dronecontroller.core.gimbal.GimbalControlService
|
||||
import com.zklh.dronecontroller.core.livestream.LiveStreamingService
|
||||
import com.zklh.dronecontroller.core.media.CameraControlService
|
||||
import com.zklh.dronecontroller.core.media.CameraMediaService
|
||||
import com.zklh.dronecontroller.core.mission.WaypointMissionService
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.virtualstick.StickPosition
|
||||
import com.zklh.dronecontroller.core.virtualstick.VirtualStickService
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||
import dji.v5.manager.aircraft.virtualstick.Stick
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val CloudCommandLogTag = "ZklhCloudCommand"
|
||||
private const val DefaultFlyToHeightMeters = 20.0
|
||||
|
||||
class CloudCommandExecutor(
|
||||
private val flightControl: FlightControlService,
|
||||
private val cameraMedia: CameraMediaService,
|
||||
private val flyToService: FlyToService,
|
||||
private val liveStreaming: LiveStreamingService,
|
||||
private val virtualStick: VirtualStickService,
|
||||
private val waypointMission: WaypointMissionService,
|
||||
private val cameraControl: CameraControlService = CameraControlService(),
|
||||
private val gimbalControl: GimbalControlService = GimbalControlService()
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val virtualStickEnabling = AtomicBoolean(false)
|
||||
private val preparedMissionIds = ConcurrentHashMap<String, String>()
|
||||
private var lastStickAt = 0L
|
||||
private var stickTimeoutJob: Job? = null
|
||||
|
||||
suspend fun execute(request: CloudCommandRequest): DjiCommandResult {
|
||||
val command = request.normalizedMethod
|
||||
if (command.shouldLogCommand()) {
|
||||
Log.d(
|
||||
CloudCommandLogTag,
|
||||
"execute topic=${request.topic} type=${request.type} method=${request.method} normalized=$command data=${request.data}"
|
||||
)
|
||||
}
|
||||
val result = execute(command, request.data, request.type)
|
||||
if (command.shouldLogCommand()) {
|
||||
Log.d(CloudCommandLogTag, "execute result method=$command success=${result.success} message=${result.message}")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun execute(method: String, data: JSONObject): DjiCommandResult =
|
||||
execute(method.normalizeCloudCommand(), data, CloudCommandTopicType.Unknown)
|
||||
|
||||
private suspend fun execute(
|
||||
command: String,
|
||||
data: JSONObject,
|
||||
topicType: CloudCommandTopicType
|
||||
): DjiCommandResult {
|
||||
return when {
|
||||
command in PassiveAckCommands -> DjiCommandResult.ok("云控命令已确认:$command")
|
||||
command == "drc_mode_enter" -> enterDrcMode(data)
|
||||
command == "drc_mode_exit" -> exitDrcMode()
|
||||
command in StickCommands -> executeStickControl(data)
|
||||
command in TakeoffCommands && data.hasFlyToTarget() -> startFlyTo(data)
|
||||
command in TakeoffCommands -> flightControl.startTakeoff()
|
||||
command in StopTakeoffCommands -> flightControl.stopTakeoff()
|
||||
command in GoHomeCommands -> flightControl.startGoHome()
|
||||
command in StopGoHomeCommands -> flightControl.stopGoHome()
|
||||
command in AutoLandingCommands -> flightControl.startAutoLanding()
|
||||
command in ForceLandingCommands -> forceLanding()
|
||||
command in StopLandingCommands -> flightControl.stopAutoLanding()
|
||||
command in ConfirmLandingCommands -> flightControl.confirmLanding()
|
||||
command in PhotoCommands -> cameraMedia.takePhoto()
|
||||
command in RecordStartCommands -> cameraMedia.startRecord()
|
||||
command in RecordStopCommands -> cameraMedia.stopRecord()
|
||||
command in RecordToggleCommands -> cameraMedia.toggleRecord()
|
||||
command in CameraModeCommands -> setCameraMode(data)
|
||||
command in PanoramaCommands -> cameraMedia.shootPanorama()
|
||||
command in LaserFillLightCommands -> setLaserFillLight(data)
|
||||
command in LaserMeasureCommands -> setLaserMeasure(data)
|
||||
command in FlyToCommands -> startFlyTo(data)
|
||||
command in StopFlyToCommands -> flyToService.stopFlyTo()
|
||||
command in WaylinePrepareCommands -> prepareWayline(data)
|
||||
command in WaylineExecuteCommands -> executeWayline(data)
|
||||
command in WaylinePauseCommands -> waypointMission.pauseMission()
|
||||
command in WaylineRecoveryCommands -> waypointMission.resumeMission()
|
||||
command in WaylineUndoCommands -> stopWayline(data)
|
||||
command in CameraZoomCommands -> setCameraZoom(data)
|
||||
command in LensSwitchCommands -> switchLens(data)
|
||||
command in GimbalDragCommands -> rotateGimbal(data)
|
||||
command in GimbalResetCommands -> gimbalControl.reset()
|
||||
command in LiveStartCommands -> liveStreaming.startFromCloud(data)
|
||||
command in LiveStopCommands -> liveStreaming.stop()
|
||||
command in LiveQualityCommands -> liveStreaming.setQualityFromCloud(data)
|
||||
else -> DjiCommandResult.failed("暂未支持云控命令:$command")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enterDrcMode(data: JSONObject): DjiCommandResult {
|
||||
val result = ensureVirtualStickEnabled()
|
||||
if (result.success) {
|
||||
virtualStick.setSpeedLevel(data.optDoubleAny("speed_level", "speedLevel").takeIf { it > 0.0 } ?: 15.0)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun exitDrcMode(): DjiCommandResult {
|
||||
sendNeutralStick()
|
||||
return virtualStick.disable()
|
||||
}
|
||||
|
||||
private suspend fun ensureVirtualStickEnabled(): DjiCommandResult {
|
||||
if (virtualStick.status.value.enabled) return DjiCommandResult.ok("虚拟摇杆已开启")
|
||||
if (!virtualStickEnabling.compareAndSet(false, true)) {
|
||||
return DjiCommandResult.ok("虚拟摇杆正在开启")
|
||||
}
|
||||
return try {
|
||||
virtualStick.enable()
|
||||
} finally {
|
||||
virtualStickEnabling.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeStickControl(
|
||||
data: JSONObject
|
||||
): DjiCommandResult {
|
||||
val enabled = ensureVirtualStickEnabled()
|
||||
if (!enabled.success) return enabled
|
||||
val speedLevel = data.optDoubleAny("speed_level", "speedLevel", "speed")
|
||||
if (speedLevel > 0.0) {
|
||||
virtualStick.setSpeedLevel(speedLevel)
|
||||
} else {
|
||||
virtualStick.setSpeedLevel(15.0)
|
||||
}
|
||||
val position = if (data.hasAny("roll", "pitch", "throttle", "yaw")) {
|
||||
data.toDrcStickPosition()
|
||||
} else {
|
||||
data.toProtocolStickPosition()
|
||||
}
|
||||
virtualStick.sendStickPosition(position)
|
||||
lastStickAt = System.currentTimeMillis()
|
||||
scheduleStickTimeout()
|
||||
return DjiCommandResult.ok("杆量控制已下发")
|
||||
}
|
||||
|
||||
private fun scheduleStickTimeout() {
|
||||
if (stickTimeoutJob?.isActive == true) return
|
||||
stickTimeoutJob = scope.launch {
|
||||
while (true) {
|
||||
delay(120)
|
||||
if (System.currentTimeMillis() - lastStickAt > 280L) {
|
||||
sendNeutralStick()
|
||||
stickTimeoutJob = null
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendNeutralStick() {
|
||||
virtualStick.sendStickPosition(StickPosition())
|
||||
}
|
||||
|
||||
private suspend fun forceLanding(): DjiCommandResult {
|
||||
val landing = flightControl.startAutoLanding()
|
||||
if (!landing.success) return landing
|
||||
delay(500)
|
||||
val confirm = flightControl.confirmLanding()
|
||||
return if (confirm.success) {
|
||||
DjiCommandResult.ok("强制降落指令已下发并确认")
|
||||
} else {
|
||||
DjiCommandResult.ok("强制降落指令已下发,确认降落返回:${confirm.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setLaserFillLight(data: JSONObject): DjiCommandResult {
|
||||
if (!data.hasAny("enable", "enabled", "ir_fill_light_enable", "irFillLightEnable")) {
|
||||
return cameraMedia.toggleLaserFillLight()
|
||||
}
|
||||
return cameraMedia.setLaserFillLightEnabled(data.optBooleanAny("enable", "enabled", "ir_fill_light_enable", "irFillLightEnable"))
|
||||
}
|
||||
|
||||
private suspend fun setLaserMeasure(data: JSONObject): DjiCommandResult {
|
||||
if (!data.hasAny("enable", "enabled")) {
|
||||
return cameraMedia.toggleLaserMeasure()
|
||||
}
|
||||
return cameraMedia.setLaserMeasureEnabled(data.optBooleanAny("enable", "enabled"))
|
||||
}
|
||||
|
||||
private suspend fun startFlyTo(data: JSONObject): DjiCommandResult {
|
||||
val target = data.optJSONObject("target_location")
|
||||
?: data.optJSONObject("targetLocation")
|
||||
?: data.optJSONObject("target_point")
|
||||
?: data.optJSONObject("targetPoint")
|
||||
?: data.optJSONObject("target")
|
||||
?: data.optJSONObject("location")
|
||||
?: data.optJSONObject("position")
|
||||
?: data.firstPoint()
|
||||
?: data
|
||||
val latitude = target.optDoubleAnyOrNull("latitude", "lat", "target_latitude", "targetLatitude")
|
||||
?: data.optDoubleAnyOrNull("targetLatitude", "target_latitude", "latitude", "lat")
|
||||
?: 0.0
|
||||
val longitude = target.optDoubleAnyOrNull("longitude", "lng", "lon", "target_longitude", "targetLongitude")
|
||||
?: data.optDoubleAnyOrNull("targetLongitude", "target_longitude", "longitude", "lng", "lon")
|
||||
?: 0.0
|
||||
val requestedHeight = target.optDoubleAnyOrNull("height", "altitude", "alt", "target_height", "targetHeight", "target_altitude")
|
||||
?: data.optDoubleAnyOrNull("targetHeight", "target_height", "height", "altitude", "alt", "commander_flight_height")
|
||||
val fallbackHeight = data.optDoubleAnyOrNull("security_takeoff_height", "securityTakeoffHeight", "safe_height", "safeHeight")
|
||||
?.takeIf { it > 0.0 }
|
||||
?: DefaultFlyToHeightMeters
|
||||
val height = requestedHeight?.takeIf { it > 0.0 } ?: fallbackHeight
|
||||
val maxSpeed = target.optIntAny(0, "max_speed", "maxSpeed")
|
||||
.takeIf { it > 0 }
|
||||
?: data.optIntAny(15, "max_speed", "maxSpeed")
|
||||
val securityTakeoffHeight = data.optIntAny(20, "security_takeoff_height", "securityTakeoffHeight")
|
||||
val flyToMode = data.optStringAny("fly_to_mode", "flyToMode")
|
||||
.toFlyToMode(defaultMode = FlyToMode.SET_HEIGHT)
|
||||
if (latitude == 0.0 || longitude == 0.0) {
|
||||
return DjiCommandResult.failed("指点飞行参数无效:需要 latitude、longitude")
|
||||
}
|
||||
Log.d(
|
||||
CloudCommandLogTag,
|
||||
"startFlyTo lat=$latitude lon=$longitude height=$height requestedHeight=$requestedHeight maxSpeed=$maxSpeed securityTakeoffHeight=$securityTakeoffHeight flyToMode=$flyToMode data=$data"
|
||||
)
|
||||
val releaseResult = releaseVirtualStickForAutonomousFlight()
|
||||
if (!releaseResult.success) {
|
||||
Log.w(CloudCommandLogTag, "release virtual stick before flyTo failed: ${releaseResult.message}")
|
||||
}
|
||||
return flyToService.startFlyTo(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
height = height,
|
||||
maxSpeed = maxSpeed,
|
||||
securityTakeoffHeight = securityTakeoffHeight,
|
||||
flyToMode = flyToMode
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun releaseVirtualStickForAutonomousFlight(): DjiCommandResult {
|
||||
sendNeutralStick()
|
||||
return if (virtualStick.status.value.enabled) {
|
||||
virtualStick.disable()
|
||||
} else {
|
||||
DjiCommandResult.ok("虚拟摇杆未开启")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setCameraMode(data: JSONObject): DjiCommandResult {
|
||||
if (data.hasAny("camera_mode", "cameraMode")) {
|
||||
return cameraMedia.setCloudCameraMode(data.optIntAny(-1, "camera_mode", "cameraMode"))
|
||||
}
|
||||
val mode = data.optStringAny("mode", "camera_mode", "cameraMode", "type").normalizeCloudCommand()
|
||||
return when (mode) {
|
||||
"photo", "shoot_photo", "shootphoto", "picture", "0" -> cameraMedia.setPhotoCaptureMode()
|
||||
"video", "record", "record_video", "recordvideo", "1" -> cameraMedia.setVideoCaptureMode()
|
||||
else -> DjiCommandResult.failed("相机模式参数无效:需要 camera_mode=0/1 或 mode=photo/video")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareWayline(data: JSONObject): DjiCommandResult {
|
||||
val flightId = data.optStringAny("flight_id", "flightId", "job_id", "jobId", "mission_id", "missionId")
|
||||
val kmzPath = data.optStringAny("kmz_path", "kmzPath", "file_path", "filePath", "path")
|
||||
val fileUrl = data.optStringAny("file_url", "fileUrl", "url")
|
||||
.ifBlank {
|
||||
data.optJSONObject("file")?.optStringAny("url", "file_url", "fileUrl").orEmpty()
|
||||
}
|
||||
val localKmzPath = when {
|
||||
kmzPath.isNotBlank() -> kmzPath
|
||||
fileUrl.isNotBlank() -> downloadKmz(fileUrl)
|
||||
else -> return DjiCommandResult.failed("航线准备失败:未收到 KMZ 本地路径或下载地址")
|
||||
}
|
||||
val upload = waypointMission.uploadKmzFile(localKmzPath)
|
||||
if (!upload.success) return upload
|
||||
val missionId = data.optStringAny("mission_id", "missionId")
|
||||
.ifBlank { waypointMission.missionIdFromPath(localKmzPath) }
|
||||
if (flightId.isNotBlank()) {
|
||||
preparedMissionIds[flightId] = missionId
|
||||
}
|
||||
preparedMissionIds[missionId] = missionId
|
||||
return DjiCommandResult.ok("航线准备完成:$missionId")
|
||||
}
|
||||
|
||||
private suspend fun executeWayline(data: JSONObject): DjiCommandResult {
|
||||
val missionId = resolveMissionId(data)
|
||||
?: return DjiCommandResult.failed("航线执行失败:缺少 flight_id 或 mission_id")
|
||||
return waypointMission.startMission(missionId, data.optWaylineIds())
|
||||
}
|
||||
|
||||
private suspend fun stopWayline(data: JSONObject): DjiCommandResult {
|
||||
val missionId = resolveMissionId(data)
|
||||
?: return DjiCommandResult.failed("航线停止失败:缺少 flight_id 或 mission_id")
|
||||
return waypointMission.stopMission(missionId)
|
||||
}
|
||||
|
||||
private fun resolveMissionId(data: JSONObject): String? {
|
||||
val raw = data.optStringAny("mission_id", "missionId", "flight_id", "flightId", "job_id", "jobId")
|
||||
if (raw.isBlank()) return null
|
||||
return preparedMissionIds[raw] ?: raw
|
||||
}
|
||||
|
||||
private fun downloadKmz(fileUrl: String): String {
|
||||
val file = File.createTempFile("cloud-wayline-", ".kmz")
|
||||
URL(fileUrl).openStream().use { input ->
|
||||
file.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
return file.absolutePath
|
||||
}
|
||||
|
||||
private suspend fun setCameraZoom(data: JSONObject): DjiCommandResult {
|
||||
val zoomFactor = data.optDoubleAny("zoom_factor", "zoomFactor", "focal_length", "focalLength")
|
||||
if (zoomFactor <= 0.0) return DjiCommandResult.failed("变焦参数无效:缺少 zoom_factor")
|
||||
val cameraType = data.optStringAny("camera_type", "cameraType", "lens", "video_type", "videoType")
|
||||
.normalizeCloudCommand()
|
||||
return if (cameraType in setOf("ir", "thermal", "infrared")) {
|
||||
cameraControl.setThermalZoomRatio(zoomFactor)
|
||||
} else {
|
||||
cameraControl.setVisibleZoomRatio(zoomFactor)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun switchLens(data: JSONObject): DjiCommandResult {
|
||||
val lens = data.optStringAny("lens", "camera_type", "cameraType", "video_type", "videoType", "type")
|
||||
.ifBlank {
|
||||
when {
|
||||
data.optBooleanAny("enable", "enabled") -> "ir"
|
||||
else -> "wide"
|
||||
}
|
||||
}
|
||||
return cameraControl.setLens(lens)
|
||||
}
|
||||
|
||||
private suspend fun rotateGimbal(data: JSONObject): DjiCommandResult {
|
||||
val pitchSpeed = data.optDoubleAny("pitch_speed", "pitchSpeed", "gimbal_pitch_speed", "gimbalPitchSpeed")
|
||||
val yawSpeed = data.optDoubleAny("yaw_speed", "yawSpeed", "gimbal_yaw_speed", "gimbalYawSpeed")
|
||||
return gimbalControl.rotateBySpeed(pitchSpeed, yawSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
private val PassiveAckCommands = setOf(
|
||||
"flight_authority_grab",
|
||||
"payload_authority_grab",
|
||||
"cloud_control_auth_request",
|
||||
"cloud_control_release",
|
||||
"heart_beat",
|
||||
"drc_initial_state_subscribe"
|
||||
)
|
||||
private val StickCommands = setOf("stick_control", "drone_control", "drc_drone_stick_control")
|
||||
private val TakeoffCommands = setOf("start_takeoff", "takeoff", "take_off", "auto_takeoff")
|
||||
private val StopTakeoffCommands = setOf("stop_takeoff", "cancel_takeoff")
|
||||
private val GoHomeCommands = setOf("start_go_home", "go_home", "return_home", "start_rth", "return_auto")
|
||||
private val StopGoHomeCommands = setOf("stop_go_home", "cancel_go_home", "stop_rth", "cancel_return_home", "return_home_cancel")
|
||||
private val AutoLandingCommands = setOf("start_auto_landing", "auto_landing", "land", "landing_auto")
|
||||
private val ForceLandingCommands = setOf("drc_emergency_landing", "drc_force_landing", "emergency_landing", "force_landing")
|
||||
private val StopLandingCommands = setOf("stop_auto_landing", "cancel_landing")
|
||||
private val ConfirmLandingCommands = setOf("confirm_landing", "landing_confirm")
|
||||
private val PhotoCommands = setOf("take_photo", "photo", "camera_photo_take", "drc_camera_photo_take", "start_shoot_photo")
|
||||
private val RecordStartCommands = setOf("start_record", "camera_recording_start", "drc_camera_recording_start", "start_recording")
|
||||
private val RecordStopCommands = setOf("stop_record", "camera_recording_stop", "drc_camera_recording_stop", "stop_recording")
|
||||
private val RecordToggleCommands = setOf("toggle_record", "record")
|
||||
private val CameraModeCommands = setOf("camera_mode_switch", "drc_camera_mode_switch", "set_camera_mode", "camera_mode_set")
|
||||
private val PanoramaCommands = setOf("panorama", "shoot_panorama", "panorama_photo")
|
||||
private val LaserFillLightCommands = setOf(
|
||||
"laser_fill_light",
|
||||
"toggle_laser_fill_light",
|
||||
"infrared_fill_light_enable",
|
||||
"drc_infrared_fill_light_enable"
|
||||
)
|
||||
private val LaserMeasureCommands = setOf("laser_measure", "toggle_laser_measure")
|
||||
private val FlyToCommands = setOf("fly_to_point", "fly_to_point_update", "takeoff_to_point", "fly_to", "go_to_point", "gotargetpoint")
|
||||
private val StopFlyToCommands = setOf("stop_fly_to", "cancel_fly_to", "fly_to_point_stop")
|
||||
private val WaylinePrepareCommands = setOf("flighttask_prepare", "wayline_prepare")
|
||||
private val WaylineExecuteCommands = setOf("flighttask_execute", "wayline_execute", "start_wayline")
|
||||
private val WaylinePauseCommands = setOf("flighttask_pause", "wayline_pause")
|
||||
private val WaylineRecoveryCommands = setOf("flighttask_recovery", "wayline_resume", "wayline_recovery")
|
||||
private val WaylineUndoCommands = setOf("flighttask_undo", "wayline_stop", "stop_wayline")
|
||||
private val CameraZoomCommands = setOf("camera_focal_length_set", "drc_camera_focal_length_set", "drc_linkage_zoom_set", "linkage_zoom_set")
|
||||
private val LensSwitchCommands = setOf("camera_lens_change", "lens_change", "live_lens_change", "camera_video_stream_source_set", "camera_screen_split", "drc_camera_screen_split")
|
||||
private val GimbalDragCommands = setOf("camera_screen_drag", "drc_camera_screen_drag", "gimbal_rotate_by_speed")
|
||||
private val GimbalResetCommands = setOf("gimbal_reset", "drc_gimbal_reset")
|
||||
private val LiveStartCommands = setOf("live_start_push", "live_start", "start_live", "start_livestream")
|
||||
private val LiveStopCommands = setOf("live_stop_push", "live_stop", "stop_live", "stop_livestream")
|
||||
private val LiveQualityCommands = setOf("live_set_quality", "live_quality", "set_live_quality")
|
||||
|
||||
private fun JSONObject.toDrcStickPosition(): StickPosition {
|
||||
fun axis(name: String): Int =
|
||||
(((optDouble(name, 1024.0) - 1024.0) / 1024.0) * Stick.MAX_STICK_POSITION_ABS)
|
||||
.roundToInt()
|
||||
.coerceIn(-Stick.MAX_STICK_POSITION_ABS, Stick.MAX_STICK_POSITION_ABS)
|
||||
|
||||
return StickPosition(
|
||||
leftHorizontal = axis("yaw"),
|
||||
leftVertical = axis("throttle"),
|
||||
rightHorizontal = axis("roll"),
|
||||
rightVertical = axis("pitch")
|
||||
)
|
||||
}
|
||||
|
||||
private fun JSONObject.toProtocolStickPosition(): StickPosition {
|
||||
fun scaled(name: String, fullScale: Double): Int =
|
||||
((optDouble(name, 0.0) / fullScale) * Stick.MAX_STICK_POSITION_ABS)
|
||||
.roundToInt()
|
||||
.coerceIn(-Stick.MAX_STICK_POSITION_ABS, Stick.MAX_STICK_POSITION_ABS)
|
||||
|
||||
return StickPosition(
|
||||
leftHorizontal = scaled("w", 20.0),
|
||||
leftVertical = scaled("h", 5.0),
|
||||
rightHorizontal = scaled("y", 5.0),
|
||||
rightVertical = scaled("x", 5.0)
|
||||
)
|
||||
}
|
||||
|
||||
private fun JSONObject.optDoubleAny(vararg names: String): Double {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optDouble(name)
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
private fun JSONObject.optDoubleAnyOrNull(vararg names: String): Double? {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optDouble(name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun JSONObject.optIntAny(defaultValue: Int, vararg names: String): Int {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optInt(name, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
private fun JSONObject.optStringAny(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optString(name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optBooleanAny(vararg names: String): Boolean {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optBoolean(name)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun String.toFlyToMode(defaultMode: FlyToMode): FlyToMode =
|
||||
when (normalizeCloudCommand()) {
|
||||
"smart_height", "smartheight", "smart" -> FlyToMode.SMART_HEIGHT
|
||||
"set_height", "setheight", "fixed_height", "fixedheight", "height", "" -> defaultMode
|
||||
else -> defaultMode
|
||||
}
|
||||
|
||||
private fun JSONObject.hasAny(vararg names: String): Boolean =
|
||||
names.any { has(it) && !isNull(it) }
|
||||
|
||||
private fun JSONObject.firstPoint(): JSONObject? {
|
||||
val points = optJSONArray("points") ?: return null
|
||||
if (points.length() <= 0) return null
|
||||
return points.optJSONObject(0)
|
||||
}
|
||||
|
||||
private fun JSONObject.hasFlyToTarget(): Boolean {
|
||||
val target = optJSONObject("target_location")
|
||||
?: optJSONObject("targetLocation")
|
||||
?: optJSONObject("target_point")
|
||||
?: optJSONObject("targetPoint")
|
||||
?: optJSONObject("target")
|
||||
?: optJSONObject("location")
|
||||
?: optJSONObject("position")
|
||||
?: firstPoint()
|
||||
?: this
|
||||
return target.hasAny("latitude", "lat", "target_latitude", "targetLatitude") &&
|
||||
target.hasAny("longitude", "lng", "lon", "target_longitude", "targetLongitude")
|
||||
}
|
||||
|
||||
private fun String.shouldLogCommand(): Boolean =
|
||||
this !in PassiveAckCommands && this !in StickCommands
|
||||
|
||||
private fun JSONObject.optWaylineIds(): List<Int> {
|
||||
val ids = optJSONArray("wayline_ids") ?: optJSONArray("waylineIds") ?: return listOf(0)
|
||||
return buildList {
|
||||
for (index in 0 until ids.length()) {
|
||||
add(ids.optInt(index, 0))
|
||||
}
|
||||
}.ifEmpty { listOf(0) }
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
enum class CloudCommandTopicType {
|
||||
Services,
|
||||
PropertySet,
|
||||
DrcDown,
|
||||
Unknown
|
||||
}
|
||||
|
||||
data class CloudCommandRequest(
|
||||
val topic: String,
|
||||
val targetSn: String,
|
||||
val type: CloudCommandTopicType,
|
||||
val method: String,
|
||||
val data: JSONObject,
|
||||
val tid: String,
|
||||
val bid: String,
|
||||
val seq: Long?
|
||||
) {
|
||||
val normalizedMethod: String = method.normalizeCloudCommand()
|
||||
|
||||
internal fun mismatchedSn(identity: CloudDeviceIdentity): String? {
|
||||
val localSns = setOf(identity.remoteControllerSn, identity.aircraftSn)
|
||||
.filter { it.isNotBlank() }
|
||||
.toSet()
|
||||
if (targetSn.isNotBlank() && targetSn !in localSns) {
|
||||
return targetSn
|
||||
}
|
||||
for (field in SnFields) {
|
||||
val value = data.optString(field).trim()
|
||||
if (value.isNotBlank() && value !in localSns) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(topic: String, payload: String): CloudCommandRequest {
|
||||
val request = JSONObject(payload)
|
||||
val data = request.optJSONObject("data") ?: JSONObject()
|
||||
val method = request.optString("method")
|
||||
.ifBlank { data.optString("method") }
|
||||
.ifBlank { request.optString("cmd") }
|
||||
.ifBlank { data.optString("cmd") }
|
||||
return CloudCommandRequest(
|
||||
topic = topic,
|
||||
targetSn = topic.split("/").getOrNull(2).orEmpty(),
|
||||
type = topic.type(),
|
||||
method = method,
|
||||
data = data,
|
||||
tid = request.optString("tid"),
|
||||
bid = request.optString("bid"),
|
||||
seq = request.optLongOrNull("seq") ?: data.optLongOrNull("seq")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SnFields = listOf(
|
||||
"sn",
|
||||
"device_sn",
|
||||
"deviceSn",
|
||||
"gateway_sn",
|
||||
"gatewaySn",
|
||||
"dock_sn",
|
||||
"dockSn",
|
||||
"drone_sn",
|
||||
"droneSn"
|
||||
)
|
||||
|
||||
private fun String.type(): CloudCommandTopicType =
|
||||
when {
|
||||
endsWith("/services") -> CloudCommandTopicType.Services
|
||||
endsWith("/property/set") -> CloudCommandTopicType.PropertySet
|
||||
endsWith("/drc/down") -> CloudCommandTopicType.DrcDown
|
||||
else -> CloudCommandTopicType.Unknown
|
||||
}
|
||||
|
||||
fun String.normalizeCloudCommand(): String =
|
||||
trim()
|
||||
.replace("-", "_")
|
||||
.replace(".", "_")
|
||||
.lowercase()
|
||||
|
||||
private fun JSONObject.optLongOrNull(name: String): Long? =
|
||||
if (has(name) && !isNull(name)) optLong(name) else null
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import android.util.Log
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
import java.net.URLEncoder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val CloudLoginLogTag = "ZklhCloudLogin"
|
||||
private const val CloudPlatformHost = "uav.zklhjs.com"
|
||||
private const val CloudPlatformFallbackIp = "221.226.33.58"
|
||||
|
||||
class CloudLoginClient {
|
||||
suspend fun login(request: CloudLoginRequest): Result<CloudSession> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val originCandidates = apiOriginCandidates(request.baseUrl)
|
||||
val djiBaseUrlCandidates = djiApiBaseUrlCandidates(request.baseUrl)
|
||||
val tenantName = request.tenantName.trim()
|
||||
val username = request.username.trim()
|
||||
Log.i(
|
||||
CloudLoginLogTag,
|
||||
"login start tenantName=$tenantName username=$username originCandidates=$originCandidates djiBaseUrlCandidates=$djiBaseUrlCandidates mqttOverride=${request.mqttAddressOverride.isNotBlank()}"
|
||||
)
|
||||
require(tenantName.isNotBlank()) { "请输入租户名称" }
|
||||
require(username.isNotBlank()) { "请输入账号" }
|
||||
require(request.password.isNotBlank()) { "请输入密码" }
|
||||
|
||||
val tenant = resolveTenantId(originCandidates, tenantName)
|
||||
Log.i(CloudLoginLogTag, "tenant resolved tenantId=${tenant.tenantId} baseUrl=${tenant.baseUrl}")
|
||||
val payload = JSONObject()
|
||||
.put("username", username)
|
||||
.put("password", request.password)
|
||||
.put("flag", request.flag)
|
||||
val login = loginManage(djiBaseUrlCandidates, tenant.baseUrl, tenant.tenantId, payload)
|
||||
val data = login.data
|
||||
val mqttAddress = request.mqttAddressOverride.trim().ifBlank {
|
||||
data.optStringAny("mqtt_addr", "mqttAddr")
|
||||
}
|
||||
val rawMqttUsername = data.optStringAny("mqtt_username", "mqttUsername").ifBlank { username }
|
||||
CloudSession(
|
||||
baseUrl = login.baseUrl,
|
||||
tenantId = tenant.tenantId,
|
||||
tenantName = tenantName,
|
||||
username = data.optStringAny("username").ifBlank { username },
|
||||
workspaceId = data.optStringAny("workspace_id", "workspaceId"),
|
||||
accessToken = data.optStringAny("access_token", "accessToken"),
|
||||
mqttAddress = normalizeMqttAddress(mqttAddress),
|
||||
mqttUsername = normalizeMqttUsername(tenant.tenantId, rawMqttUsername),
|
||||
mqttPassword = data.optStringAny("mqtt_password", "mqttPassword").ifBlank { request.password },
|
||||
deptId = data.optLongAny("dept_id", "deptId")
|
||||
).also { session ->
|
||||
require(session.workspaceId.isNotBlank()) { "登录成功但未返回 workspace_id" }
|
||||
require(session.mqttAddress.isNotBlank()) { "登录成功但未返回 mqtt_addr,请手动填写 MQTT 地址" }
|
||||
require(session.accessToken.isNotBlank()) { "登录成功但未返回 access_token" }
|
||||
Log.i(
|
||||
CloudLoginLogTag,
|
||||
"login success baseUrl=${session.baseUrl} tenantId=${session.tenantId} workspaceId=${session.workspaceId} mqttAddress=${session.mqttAddress} mqttUsername=${session.mqttUsername}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveTenantId(baseUrls: List<String>, tenantName: String): TenantResolution {
|
||||
if (tenantName.all { it.isDigit() }) {
|
||||
Log.d(CloudLoginLogTag, "tenantName is numeric, use as tenantId=$tenantName")
|
||||
return TenantResolution(tenantId = tenantName, baseUrl = baseUrls.first())
|
||||
}
|
||||
var lastError: Throwable? = null
|
||||
val tenantNameCandidates = if (tenantName == "管理平台") {
|
||||
listOf("管理平台", "中科联航")
|
||||
} else {
|
||||
listOf(tenantName)
|
||||
}
|
||||
Log.d(CloudLoginLogTag, "resolve tenant candidates=$tenantNameCandidates baseUrls=$baseUrls")
|
||||
for (candidate in tenantNameCandidates) {
|
||||
val encodedName = URLEncoder.encode(candidate, Charsets.UTF_8.name())
|
||||
for (baseUrl in baseUrls) {
|
||||
val result = runCatching {
|
||||
Log.i(CloudLoginLogTag, "resolve tenant request baseUrl=$baseUrl tenantName=$candidate")
|
||||
val response = getJson(
|
||||
url = "$baseUrl/saas/admin-api/system/tenant/get-id-by-name?name=$encodedName",
|
||||
tenantId = "1",
|
||||
token = null
|
||||
)
|
||||
val tenantId = parseTenantIdResponse(response)
|
||||
Log.i(CloudLoginLogTag, "resolve tenant success baseUrl=$baseUrl tenantName=$candidate tenantId=$tenantId")
|
||||
TenantResolution(tenantId = tenantId, baseUrl = baseUrl)
|
||||
}
|
||||
result.onSuccess { return it }
|
||||
result.onFailure {
|
||||
Log.w(CloudLoginLogTag, "resolve tenant failed baseUrl=$baseUrl tenantName=$candidate: ${it.message}")
|
||||
lastError = it
|
||||
}
|
||||
}
|
||||
}
|
||||
error(lastError?.message ?: "租户名称解析失败")
|
||||
}
|
||||
|
||||
private fun loginManage(
|
||||
djiBaseUrls: List<String>,
|
||||
preferredOrigin: String,
|
||||
tenantId: String,
|
||||
payload: JSONObject
|
||||
): LoginResponse {
|
||||
var lastError: Throwable? = null
|
||||
val orderedBaseUrls = (listOf("$preferredOrigin/dji") + djiBaseUrls).distinct()
|
||||
for (baseUrl in orderedBaseUrls) {
|
||||
val result = runCatching {
|
||||
Log.i(CloudLoginLogTag, "manage login request baseUrl=$baseUrl tenantId=$tenantId username=${payload.optString("username")}")
|
||||
val response = postJson(
|
||||
url = "$baseUrl/manage/api/v1/login",
|
||||
tenantId = tenantId,
|
||||
token = null,
|
||||
body = payload
|
||||
)
|
||||
val root = JSONObject(response)
|
||||
val code = root.optInt("code", 0)
|
||||
Log.i(CloudLoginLogTag, "manage login response baseUrl=$baseUrl code=$code message=${root.optMessage("")}")
|
||||
if (code != 0 && code != 200) {
|
||||
error(root.optMessage("第三方云登录失败"))
|
||||
}
|
||||
LoginResponse(
|
||||
baseUrl = baseUrl,
|
||||
data = root.optJSONObject("data") ?: root
|
||||
)
|
||||
}
|
||||
result.onSuccess { return it }
|
||||
result.onFailure {
|
||||
Log.w(CloudLoginLogTag, "manage login failed baseUrl=$baseUrl tenantId=$tenantId: ${it.message}")
|
||||
lastError = it
|
||||
}
|
||||
}
|
||||
error(lastError?.message ?: "第三方云登录失败")
|
||||
}
|
||||
|
||||
private fun parseTenantIdResponse(response: String): String {
|
||||
val text = response.trim()
|
||||
if (!text.startsWith("{")) {
|
||||
return text.trim('"').also { require(it.isNotBlank()) { "租户名称解析失败:返回为空" } }
|
||||
}
|
||||
val root = JSONObject(text)
|
||||
val code = root.optInt("code", 0)
|
||||
if (code != 0 && code != 200) {
|
||||
error(root.optMessage("租户名称解析失败"))
|
||||
}
|
||||
val tenantId = root.opt("data")?.toString()?.trim().orEmpty()
|
||||
require(tenantId.isNotBlank() && tenantId != "null") { "租户名称解析失败:未返回租户 ID" }
|
||||
return tenantId
|
||||
}
|
||||
|
||||
private fun getJson(
|
||||
url: String,
|
||||
tenantId: String,
|
||||
token: String?
|
||||
): String {
|
||||
val connection = URI(url).toURL().openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10_000
|
||||
connection.readTimeout = 15_000
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
platformHostHeaderForUrl(url)?.let { connection.setRequestProperty("Host", it) }
|
||||
if (tenantId.isNotBlank()) {
|
||||
connection.setRequestProperty("Tenant-Id", tenantId)
|
||||
connection.setRequestProperty("tenant-id", tenantId)
|
||||
}
|
||||
if (!token.isNullOrBlank()) connection.setRequestProperty("x-auth-token", token)
|
||||
val stream = if (connection.responseCode in 200..299) {
|
||||
connection.inputStream
|
||||
} else {
|
||||
connection.errorStream ?: connection.inputStream
|
||||
}
|
||||
val text = BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
|
||||
if (connection.responseCode !in 200..299) {
|
||||
error("HTTP ${connection.responseCode}: $text")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
suspend fun bindDevice(session: CloudSession, deviceSn: String): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
if (deviceSn.isBlank() || session.workspaceId.isBlank()) return@runCatching
|
||||
val encodedSn = URLEncoder.encode(deviceSn, Charsets.UTF_8.name())
|
||||
val body = JSONObject()
|
||||
.put("workspace_id", session.workspaceId)
|
||||
.put("workspaceId", session.workspaceId)
|
||||
postJson(
|
||||
url = "${session.baseUrl}/manage/api/v1/devices/$encodedSn/binding",
|
||||
tenantId = session.tenantId,
|
||||
token = session.accessToken,
|
||||
body = body
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postJson(
|
||||
url: String,
|
||||
tenantId: String,
|
||||
token: String?,
|
||||
body: JSONObject
|
||||
): String {
|
||||
val connection = URI(url).toURL().openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.connectTimeout = 10_000
|
||||
connection.readTimeout = 15_000
|
||||
connection.doOutput = true
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
platformHostHeaderForUrl(url)?.let { connection.setRequestProperty("Host", it) }
|
||||
if (tenantId.isNotBlank()) {
|
||||
connection.setRequestProperty("Tenant-Id", tenantId)
|
||||
connection.setRequestProperty("tenant-id", tenantId)
|
||||
}
|
||||
if (!token.isNullOrBlank()) connection.setRequestProperty("x-auth-token", token)
|
||||
OutputStreamWriter(connection.outputStream, Charsets.UTF_8).use { writer ->
|
||||
writer.write(body.toString())
|
||||
}
|
||||
val stream = if (connection.responseCode in 200..299) {
|
||||
connection.inputStream
|
||||
} else {
|
||||
connection.errorStream ?: connection.inputStream
|
||||
}
|
||||
val text = BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
|
||||
if (connection.responseCode !in 200..299) {
|
||||
error("HTTP ${connection.responseCode}: $text")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun normalizeApiBaseUrl(rawUrl: String): String {
|
||||
return djiApiBaseUrlCandidates(rawUrl).first()
|
||||
}
|
||||
|
||||
private fun apiOriginCandidates(rawUrl: String): List<String> {
|
||||
val origin = normalizeOrigin(rawUrl)
|
||||
return (platformFallbackOrigin(origin)?.let { listOf(origin, it) } ?: listOf(origin)).distinct()
|
||||
}
|
||||
|
||||
private fun djiApiBaseUrlCandidates(rawUrl: String): List<String> {
|
||||
return apiOriginCandidates(rawUrl).map { "$it/dji" }
|
||||
}
|
||||
|
||||
private fun normalizeOrigin(rawUrl: String): String {
|
||||
val withScheme = rawUrl.trim().ifBlank { "http://uav.zklhjs.com" }.let {
|
||||
if (it.startsWith("http://", true) || it.startsWith("https://", true)) it else "http://$it"
|
||||
}
|
||||
val uri = URI(withScheme)
|
||||
val scheme = uri.scheme ?: "http"
|
||||
val host = uri.host ?: withScheme.removePrefix("$scheme://").substringBefore('/').substringBefore(':')
|
||||
val port = if (uri.port > 0) ":${uri.port}" else ""
|
||||
return "$scheme://$host$port"
|
||||
}
|
||||
|
||||
private fun normalizeMqttUsername(tenantId: String, rawUsername: String): String {
|
||||
val username = rawUsername.trim()
|
||||
return if (username.startsWith("${tenantId}_")) username else "${tenantId}_$username"
|
||||
}
|
||||
|
||||
fun normalizeMqttAddress(rawAddress: String): String {
|
||||
val value = rawAddress.trim()
|
||||
if (value.isBlank()) return ""
|
||||
return when {
|
||||
value.startsWith("tcp://", true) ||
|
||||
value.startsWith("ssl://", true) ||
|
||||
value.startsWith("ws://", true) ||
|
||||
value.startsWith("wss://", true) -> value
|
||||
value.startsWith("mqtt://", true) -> "tcp://" + value.substringAfter("://")
|
||||
else -> "tcp://$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun mqttAddressCandidates(rawAddress: String): List<String> {
|
||||
val normalized = normalizeMqttAddress(rawAddress)
|
||||
if (normalized.isBlank()) return emptyList()
|
||||
return listOfNotNull(normalized, platformFallbackMqttAddress(normalized)).distinct()
|
||||
}
|
||||
|
||||
private fun platformFallbackOrigin(origin: String): String? {
|
||||
val uri = URI(origin)
|
||||
val scheme = uri.scheme ?: return null
|
||||
val host = uri.host ?: return null
|
||||
if (!scheme.equals("http", ignoreCase = true) || !host.equals(CloudPlatformHost, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
val port = if (uri.port > 0) ":${uri.port}" else ""
|
||||
return "$scheme://$CloudPlatformFallbackIp$port"
|
||||
}
|
||||
|
||||
private fun platformFallbackMqttAddress(address: String): String? {
|
||||
val uri = runCatching { URI(address) }.getOrNull() ?: return null
|
||||
val host = uri.host ?: return null
|
||||
if (!host.equals(CloudPlatformHost, ignoreCase = true)) return null
|
||||
return URI(uri.scheme, uri.userInfo, CloudPlatformFallbackIp, uri.port, uri.path, uri.query, uri.fragment).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun platformHostHeaderForUrl(url: String): String? {
|
||||
val uri = runCatching { URI(url) }.getOrNull() ?: return null
|
||||
if (!uri.host.equals(CloudPlatformFallbackIp, ignoreCase = true)) return null
|
||||
val defaultPort = (uri.scheme.equals("http", ignoreCase = true) && uri.port == 80) ||
|
||||
(uri.scheme.equals("https", ignoreCase = true) && uri.port == 443)
|
||||
val port = if (uri.port > 0 && !defaultPort) ":${uri.port}" else ""
|
||||
return "$CloudPlatformHost$port"
|
||||
}
|
||||
|
||||
private data class TenantResolution(
|
||||
val tenantId: String,
|
||||
val baseUrl: String
|
||||
)
|
||||
|
||||
private data class LoginResponse(
|
||||
val baseUrl: String,
|
||||
val data: JSONObject
|
||||
)
|
||||
|
||||
private fun JSONObject.optStringAny(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optString(name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optLongAny(vararg names: String): Long? {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optLong(name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun JSONObject.optMessage(fallback: String): String =
|
||||
optStringAny("msg", "message").ifBlank { fallback }
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
data class CloudLoginRequest(
|
||||
val baseUrl: String,
|
||||
val tenantName: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val flag: Int = 2,
|
||||
val mqttAddressOverride: String = ""
|
||||
)
|
||||
|
||||
data class CloudSession(
|
||||
val baseUrl: String,
|
||||
val tenantId: String,
|
||||
val tenantName: String,
|
||||
val username: String,
|
||||
val workspaceId: String,
|
||||
val accessToken: String,
|
||||
val mqttAddress: String,
|
||||
val mqttUsername: String,
|
||||
val mqttPassword: String,
|
||||
val deptId: Long? = null
|
||||
) {
|
||||
val payloadUsername: String
|
||||
get() = if (tenantId.isNotBlank() && username.isNotBlank()) {
|
||||
"${tenantId}_${username}"
|
||||
} else {
|
||||
mqttUsername
|
||||
}
|
||||
}
|
||||
|
||||
data class CloudMqttState(
|
||||
val connected: Boolean = false,
|
||||
val connecting: Boolean = false,
|
||||
val session: CloudSession? = null,
|
||||
val message: String = "未连接",
|
||||
val lastOnlineAt: Long = 0L,
|
||||
val lastOsdAt: Long = 0L,
|
||||
val lastCommand: String = "",
|
||||
val lastError: String? = null
|
||||
)
|
||||
|
||||
internal data class CloudDeviceIdentity(
|
||||
val remoteControllerSn: String = "",
|
||||
val aircraftSn: String = "",
|
||||
val productConnected: Boolean = false,
|
||||
val productLinkConnected: Boolean = false
|
||||
) {
|
||||
val readyForOnline: Boolean
|
||||
get() = remoteControllerSn.isNotBlank() && aircraftSn.isNotBlank()
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DroneSdkState
|
||||
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallback
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
|
||||
import org.eclipse.paho.client.mqttv3.MqttException
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val CloudMqttLogTag = "ZklhCloudMqtt"
|
||||
private const val RcDomain = 2
|
||||
private const val DroneDomain = 0
|
||||
private const val RcPlus2Type = 174
|
||||
private const val Matrice4Type = 99
|
||||
private const val RcSubType = 0
|
||||
private const val Matrice4SubType = 1
|
||||
private const val Matrice4CameraPayloadIndex = "89-0-0"
|
||||
private const val Matrice4LiveVideoIndex = "normal-0"
|
||||
private const val ThingVersion = "1.2.0"
|
||||
private const val CloudAccessType = "msdk"
|
||||
private const val OSD_MIN_INTERVAL_MS = 1_000L
|
||||
private const val JsonClassKey = "@class"
|
||||
private const val OsdRemoteControlClass = "com.dji.sdk.cloudapi.device.OsdRemoteControl"
|
||||
private const val WirelessLinkClass = "com.dji.sdk.cloudapi.device.WirelessLink"
|
||||
private const val OsdRcDroneClass = "com.dji.sdk.cloudapi.device.OsdRcDrone"
|
||||
private const val DroneBatteryClass = "com.dji.sdk.cloudapi.device.DroneBattery"
|
||||
private const val BatteryClass = "com.dji.sdk.cloudapi.device.Battery"
|
||||
private const val DronePositionStateClass = "com.dji.sdk.cloudapi.device.DronePositionState"
|
||||
private const val RcDistanceLimitStatusClass = "com.dji.sdk.cloudapi.device.RcDistanceLimitStatus"
|
||||
private const val StorageClass = "com.dji.sdk.cloudapi.device.Storage"
|
||||
private const val OsdCameraClass = "com.dji.sdk.cloudapi.device.OsdCamera"
|
||||
private const val RcDronePayloadClass = "com.dji.sdk.cloudapi.device.RcDronePayload"
|
||||
private const val JavaArrayListClass = "java.util.ArrayList"
|
||||
private const val EarthRadiusMeters = 6_371_000.0
|
||||
|
||||
class CloudMqttService(
|
||||
private val loginClient: CloudLoginClient = CloudLoginClient()
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val _state = MutableStateFlow(CloudMqttState())
|
||||
val state: StateFlow<CloudMqttState> = _state.asStateFlow()
|
||||
|
||||
private val connecting = AtomicBoolean(false)
|
||||
private var client: MqttClient? = null
|
||||
private var commandExecutor: CloudCommandExecutor? = null
|
||||
private var session: CloudSession? = null
|
||||
private var deviceIdentity = CloudDeviceIdentity()
|
||||
private var telemetry = TelemetrySnapshot()
|
||||
private var subscribedControlKey = ""
|
||||
private var onlineKey = ""
|
||||
private var liveCapacityKey = ""
|
||||
private var lastOsdAt = 0L
|
||||
|
||||
fun connect(
|
||||
session: CloudSession,
|
||||
commandExecutor: CloudCommandExecutor
|
||||
) {
|
||||
if (!connecting.compareAndSet(false, true)) return
|
||||
this.session = session
|
||||
this.commandExecutor = commandExecutor
|
||||
_state.update {
|
||||
it.copy(
|
||||
connecting = true,
|
||||
session = session,
|
||||
message = "正在连接 MQTT:${session.mqttAddress}",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
scope.launch {
|
||||
runCatching {
|
||||
client?.takeIf { it.isConnected }?.disconnect()
|
||||
client?.close()
|
||||
val clientId = "zklh-rc-${deviceIdentity.remoteControllerSn.ifBlank { UUID.randomUUID().toString() }}"
|
||||
val mqttAddresses = CloudLoginClient.mqttAddressCandidates(session.mqttAddress)
|
||||
.ifEmpty { listOf(session.mqttAddress) }
|
||||
var connectedClient: MqttClient? = null
|
||||
var connectedSession = session
|
||||
var lastConnectError: Throwable? = null
|
||||
for (mqttAddress in mqttAddresses) {
|
||||
val result = runCatching {
|
||||
Log.i(CloudMqttLogTag, "connect attempt address=$mqttAddress username=${session.mqttUsername}")
|
||||
val mqttClient = MqttClient(mqttAddress, clientId, MemoryPersistence())
|
||||
mqttClient.setCallback(callback())
|
||||
val options = MqttConnectOptions().apply {
|
||||
isAutomaticReconnect = true
|
||||
isCleanSession = true
|
||||
connectionTimeout = 10
|
||||
keepAliveInterval = 20
|
||||
userName = session.mqttUsername
|
||||
password = session.mqttPassword.toCharArray()
|
||||
}
|
||||
mqttClient.connect(options)
|
||||
mqttClient
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
connectedClient = result.getOrThrow()
|
||||
connectedSession = session.copy(mqttAddress = mqttAddress)
|
||||
break
|
||||
} else {
|
||||
val error = result.exceptionOrNull()
|
||||
Log.w(CloudMqttLogTag, "connect attempt failed address=$mqttAddress: ${error?.message}")
|
||||
lastConnectError = error
|
||||
}
|
||||
}
|
||||
val mqttClient = connectedClient ?: throw lastConnectError ?: IllegalStateException("MQTT 连接失败")
|
||||
client = mqttClient
|
||||
this@CloudMqttService.session = connectedSession
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = true,
|
||||
connecting = false,
|
||||
session = connectedSession,
|
||||
message = "MQTT 已连接",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
subscribeControlTopicsIfReady()
|
||||
publishOnlineIfReady(force = true)
|
||||
publishOsdIfReady(force = true)
|
||||
}.onFailure { error ->
|
||||
Log.e(CloudMqttLogTag, "connect failed", error)
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
connecting = false,
|
||||
message = "MQTT 连接失败",
|
||||
lastError = error.message ?: error.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
connecting.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
scope.launch {
|
||||
runCatching { publishStatusOnline(online = false) }
|
||||
runCatching { client?.disconnect() }
|
||||
runCatching { client?.close() }
|
||||
client = null
|
||||
subscribedControlKey = ""
|
||||
onlineKey = ""
|
||||
liveCapacityKey = ""
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
connecting = false,
|
||||
message = "第三方云已断开",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
runCatching { publishStatusOnline(online = false) }
|
||||
runCatching { client?.disconnectForcibly(500, 500, false) }
|
||||
runCatching { client?.close() }
|
||||
client = null
|
||||
subscribedControlKey = ""
|
||||
onlineKey = ""
|
||||
liveCapacityKey = ""
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
fun updateDeviceState(sdkState: DroneSdkState) {
|
||||
deviceIdentity = CloudDeviceIdentity(
|
||||
remoteControllerSn = sdkState.remoteControllerSerialNumber,
|
||||
aircraftSn = sdkState.aircraftSerialNumber,
|
||||
productConnected = sdkState.productConnected,
|
||||
productLinkConnected = sdkState.productLinkConnected
|
||||
)
|
||||
scope.launch {
|
||||
subscribeControlTopicsIfReady()
|
||||
publishOnlineIfReady(force = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTelemetry(snapshot: TelemetrySnapshot) {
|
||||
telemetry = snapshot
|
||||
scope.launch {
|
||||
publishOsdIfReady(force = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun callback(): MqttCallback =
|
||||
object : MqttCallback {
|
||||
override fun connectionLost(cause: Throwable?) {
|
||||
Log.e(CloudMqttLogTag, "connection lost", cause)
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
connecting = false,
|
||||
message = "MQTT 连接已断开",
|
||||
lastError = cause?.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String, message: MqttMessage) {
|
||||
val payload = message.payload.toString(Charsets.UTF_8)
|
||||
Log.d(CloudMqttLogTag, "message arrived topic=$topic payload=$payload")
|
||||
scope.launch { handleCommand(topic, payload) }
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) = Unit
|
||||
}
|
||||
|
||||
private fun subscribeControlTopicsIfReady() {
|
||||
val mqttClient = client ?: return
|
||||
val rcSn = deviceIdentity.remoteControllerSn
|
||||
val aircraftSn = deviceIdentity.aircraftSn
|
||||
val key = "$rcSn/$aircraftSn"
|
||||
if (!mqttClient.isConnected || rcSn.isBlank() || subscribedControlKey == key) return
|
||||
val topics = buildList {
|
||||
add("thing/product/$rcSn/services")
|
||||
add("thing/product/$rcSn/property/set")
|
||||
add("thing/product/$rcSn/drc/down")
|
||||
if (aircraftSn.isNotBlank()) {
|
||||
add("thing/product/$aircraftSn/services")
|
||||
add("thing/product/$aircraftSn/property/set")
|
||||
}
|
||||
}.toTypedArray()
|
||||
runCatching {
|
||||
mqttClient.subscribe(topics, IntArray(topics.size) { 1 })
|
||||
subscribedControlKey = key
|
||||
_state.update { it.copy(message = "MQTT 已订阅云控命令:${topics.size} 个 topic") }
|
||||
Log.d(CloudMqttLogTag, "subscribed control topics=${topics.joinToString()}")
|
||||
}.onFailure { error ->
|
||||
Log.e(CloudMqttLogTag, "subscribe failed", error)
|
||||
_state.update { it.copy(lastError = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleCommand(topic: String, payload: String) {
|
||||
val executor = commandExecutor ?: return
|
||||
val request = runCatching { CloudCommandRequest.parse(topic, payload) }.getOrElse { error ->
|
||||
publishReply(topic, "", "", "", null, false, "云控命令 JSON 解析失败:${error.message}")
|
||||
return
|
||||
}
|
||||
val mismatchedSn = request.mismatchedSn(deviceIdentity)
|
||||
if (mismatchedSn != null) {
|
||||
publishReply(request, false, "云控命令目标 SN 不属于本机:$mismatchedSn")
|
||||
return
|
||||
}
|
||||
if (request.method.isBlank()) {
|
||||
publishReply(request, false, "云控命令缺少 method")
|
||||
return
|
||||
}
|
||||
val result = runCatching { executor.execute(request) }
|
||||
.getOrElse { error -> com.zklh.dronecontroller.core.msdk.DjiCommandResult.failed(error.message ?: error.toString()) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
lastCommand = "${request.method}:${result.message}",
|
||||
message = if (result.success) "云控命令已执行" else "云控命令执行失败"
|
||||
)
|
||||
}
|
||||
publishReply(request, result.success, result.message)
|
||||
}
|
||||
|
||||
private fun publishOnlineIfReady(force: Boolean) {
|
||||
if (!isConnected()) return
|
||||
val identity = deviceIdentity
|
||||
if (!identity.readyForOnline) return
|
||||
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${session?.workspaceId}"
|
||||
if (!force && onlineKey == key) return
|
||||
publishStatusOnline(online = true)
|
||||
onlineKey = key
|
||||
val now = System.currentTimeMillis()
|
||||
_state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") }
|
||||
publishLiveCapacityIfReady(identity)
|
||||
bindDevices(identity)
|
||||
}
|
||||
|
||||
private fun bindDevices(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
scope.launch {
|
||||
loginClient.bindDevice(cloudSession, identity.remoteControllerSn)
|
||||
.onFailure { Log.w(CloudMqttLogTag, "bind rc failed: ${it.message}") }
|
||||
loginClient.bindDevice(cloudSession, identity.aircraftSn)
|
||||
.onFailure { Log.w(CloudMqttLogTag, "bind drone failed: ${it.message}") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishStatusOnline(online: Boolean) {
|
||||
val cloudSession = session ?: return
|
||||
val identity = deviceIdentity
|
||||
val rcSn = identity.remoteControllerSn
|
||||
if (rcSn.isBlank()) return
|
||||
val subDevices = JSONArray()
|
||||
if (online && identity.aircraftSn.isNotBlank()) {
|
||||
subDevices.put(
|
||||
JSONObject()
|
||||
.put("sn", identity.aircraftSn)
|
||||
.put("domain", DroneDomain)
|
||||
.put("type", Matrice4Type)
|
||||
.put("sub_type", Matrice4SubType)
|
||||
.put("index", "A")
|
||||
.put("thing_version", ThingVersion)
|
||||
)
|
||||
}
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("method", "update_topo")
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put("domain", RcDomain)
|
||||
.put("type", RcPlus2Type)
|
||||
.put("sub_type", RcSubType)
|
||||
.put("thing_version", ThingVersion)
|
||||
.put("access_type", CloudAccessType)
|
||||
.put("sub_devices", subDevices)
|
||||
)
|
||||
publish("sys/product/$rcSn/status", payload, qos = 1)
|
||||
}
|
||||
|
||||
private fun publishLiveCapacityIfReady(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
val rcSn = identity.remoteControllerSn
|
||||
val aircraftSn = identity.aircraftSn
|
||||
if (rcSn.isBlank() || aircraftSn.isBlank()) return
|
||||
val key = "$rcSn/$aircraftSn/${cloudSession.workspaceId}"
|
||||
if (liveCapacityKey == key) return
|
||||
|
||||
val video = JSONObject()
|
||||
.put("video_index", Matrice4LiveVideoIndex)
|
||||
.put("video_type", "normal")
|
||||
.put("switchable_video_types", JSONArray().put("normal"))
|
||||
val camera = JSONObject()
|
||||
.put("available_video_number", 1)
|
||||
.put("coexist_video_number_max", 1)
|
||||
.put("camera_index", Matrice4CameraPayloadIndex)
|
||||
.put("video_list", JSONArray().put(video))
|
||||
val device = JSONObject()
|
||||
.put("sn", aircraftSn)
|
||||
.put("available_video_number", 1)
|
||||
.put("coexist_video_number_max", 1)
|
||||
.put("camera_list", JSONArray().put(camera))
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("gateway", rcSn)
|
||||
.put("method", "livestream_ability_update")
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put(
|
||||
"live_capacity",
|
||||
JSONObject()
|
||||
.put("available_video_number", 1)
|
||||
.put("coexist_video_number_max", 1)
|
||||
.put("device_list", JSONArray().put(device))
|
||||
)
|
||||
)
|
||||
publish("thing/product/$rcSn/state", payload, qos = 1)
|
||||
liveCapacityKey = key
|
||||
Log.d(CloudMqttLogTag, "live capacity published rc=$rcSn aircraft=$aircraftSn")
|
||||
}
|
||||
|
||||
private fun publishOsdIfReady(force: Boolean) {
|
||||
if (!isConnected()) return
|
||||
val identity = deviceIdentity
|
||||
if (!identity.readyForOnline) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (!force && now - lastOsdAt < OSD_MIN_INTERVAL_MS) return
|
||||
lastOsdAt = now
|
||||
publishRcOsd(identity)
|
||||
publishDroneOsd(identity)
|
||||
_state.update { it.copy(lastOsdAt = now) }
|
||||
}
|
||||
|
||||
private fun publishRcOsd(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
Log.d(
|
||||
CloudMqttLogTag,
|
||||
"RcOsdPosition lat=${telemetry.rcLatitude} lon=${telemetry.rcLongitude} valid=${telemetry.rcLocationValid} satellites=${telemetry.rcGpsSatelliteCount}"
|
||||
)
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("gateway", identity.remoteControllerSn)
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put(JsonClassKey, OsdRemoteControlClass)
|
||||
.put("latitude", telemetry.rcLatitude)
|
||||
.put("longitude", telemetry.rcLongitude)
|
||||
.put("height", telemetry.rcAltitude.toFloat())
|
||||
.put("capacity_percent", telemetry.rcBatteryPercent.takeIf { it in 0..100 } ?: 0)
|
||||
.put("is_cloud_control_auth", 1)
|
||||
.put(
|
||||
"wireless_link",
|
||||
JSONObject()
|
||||
.put(JsonClassKey, WirelessLinkClass)
|
||||
.put("dongle_number", 0)
|
||||
.put("link_workmode", 0)
|
||||
.put("sdr_link_state", identity.productLinkConnected)
|
||||
.put("sdr_quality", if (identity.productLinkConnected) 5 else 0)
|
||||
.put("4g_freq_band", 0.0)
|
||||
.put("4g_gnd_quality", 0)
|
||||
.put("4g_link_state", false)
|
||||
.put("4g_quality", 0)
|
||||
.put("4g_uav_quality", 0)
|
||||
)
|
||||
)
|
||||
publish("thing/product/${identity.remoteControllerSn}/osd", payload, qos = 0)
|
||||
}
|
||||
|
||||
private fun publishDroneOsd(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
val horizontalSpeed = sqrt(telemetry.speedX * telemetry.speedX + telemetry.speedY * telemetry.speedY)
|
||||
val latitude = telemetry.droneOsdLatitude()
|
||||
val longitude = telemetry.droneOsdLongitude()
|
||||
Log.d(
|
||||
CloudMqttLogTag,
|
||||
"DroneOsdPosition source=${telemetry.droneOsdPositionSource()} lat=$latitude lon=$longitude " +
|
||||
"gpsValid=${telemetry.gpsValid} gps=${telemetry.gpsSatelliteCount} rtkHealthy=${telemetry.rtkHealthy} " +
|
||||
"rtkUsable=${telemetry.rtkFusionDataUsable} solution=${telemetry.rtkPositioningSolution}"
|
||||
)
|
||||
val relativeAltitude = telemetry.altitude.toFloat()
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("gateway", identity.remoteControllerSn)
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put(JsonClassKey, OsdRcDroneClass)
|
||||
.put("attitude_head", telemetry.heading.toFloat())
|
||||
.put("attitude_pitch", telemetry.attitudePitch)
|
||||
.put("attitude_roll", telemetry.attitudeRoll)
|
||||
.put("elevation", relativeAltitude)
|
||||
.put("gear", telemetry.gear)
|
||||
.put("height", relativeAltitude)
|
||||
.put("original_height", relativeAltitude)
|
||||
.put("home_distance", telemetry.homeDistanceMeters())
|
||||
.put("horizontal_speed", horizontalSpeed.toFloat())
|
||||
.put("vertical_speed", telemetry.speedZ.toFloat())
|
||||
.put("latitude", latitude)
|
||||
.put("longitude", longitude)
|
||||
.put("mode_code", telemetry.toModeCode(identity.productConnected))
|
||||
.put("total_flight_distance", telemetry.totalFlightDistance)
|
||||
.put("total_flight_time", telemetry.totalFlightTime.toFloat())
|
||||
.put("total_flight_sorties", telemetry.totalFlightSorties)
|
||||
.put("wind_direction", telemetry.windDirection)
|
||||
.put("wind_speed", telemetry.windSpeed.toFloat())
|
||||
.put("firmware_version", telemetry.firmwareVersion)
|
||||
.put("battery", droneBatteryJson())
|
||||
.put("position_state", dronePositionStateJson())
|
||||
.put("payload", JSONArray().put(rcDronePayloadJson()))
|
||||
.put("storage", storageJson())
|
||||
.put("cameras", JSONArray().put(osdCameraJson()))
|
||||
.put("height_limit", telemetry.heightLimit)
|
||||
.put("distance_limit_status", rcDistanceLimitStatusJson())
|
||||
.put("track_id", "")
|
||||
)
|
||||
publish("thing/product/${identity.aircraftSn}/osd", payload, qos = 0)
|
||||
}
|
||||
|
||||
private fun droneBatteryJson(): JSONObject {
|
||||
val battery = JSONObject()
|
||||
.put(JsonClassKey, DroneBatteryClass)
|
||||
.put("batteries", droneBatteryItemsJson())
|
||||
telemetry.batteryPercent.takeIf { it in 0..100 }?.let {
|
||||
battery.put("capacity_percent", it)
|
||||
}
|
||||
if (telemetry.batteryPercentNeededToLandKnown) {
|
||||
battery.put("landing_power", telemetry.batteryPercentNeededToLand)
|
||||
}
|
||||
if (telemetry.remainingFlightTimeKnown) {
|
||||
battery.put("remain_flight_time", telemetry.remainingFlightTime)
|
||||
}
|
||||
if (telemetry.batteryPercentNeededToGoHomeKnown) {
|
||||
battery.put("return_home_power", telemetry.batteryPercentNeededToGoHome)
|
||||
}
|
||||
return battery
|
||||
}
|
||||
|
||||
private fun droneBatteryItemsJson(): JSONArray {
|
||||
val batteryItem = JSONObject()
|
||||
.put(JsonClassKey, BatteryClass)
|
||||
.put("index", 0)
|
||||
telemetry.batteryFirmwareVersion.takeIf { it.isNotBlank() }?.let {
|
||||
batteryItem.put("firmware_version", it)
|
||||
}
|
||||
if (telemetry.batteryLoopTimesKnown) {
|
||||
batteryItem.put("loop_times", telemetry.batteryLoopTimes)
|
||||
}
|
||||
telemetry.batteryPercent.takeIf { it in 0..100 }?.let {
|
||||
batteryItem.put("capacity_percent", it)
|
||||
}
|
||||
telemetry.batterySerialNumber.takeIf { it.isNotBlank() }?.let {
|
||||
batteryItem.put("sn", it)
|
||||
}
|
||||
if (telemetry.batteryTemperatureKnown) {
|
||||
batteryItem.put("temperature", telemetry.batteryTemperature)
|
||||
}
|
||||
if (telemetry.batteryVoltageKnown) {
|
||||
batteryItem.put("voltage", telemetry.batteryVoltageMv)
|
||||
}
|
||||
if (telemetry.batteryHighVoltageStorageKnown) {
|
||||
batteryItem.put("high_voltage_storage_days", telemetry.batteryHighVoltageStorageDays())
|
||||
}
|
||||
return if (batteryItem.length() > 2) cloudArrayList(batteryItem) else cloudArrayList()
|
||||
}
|
||||
|
||||
private fun dronePositionStateJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, DronePositionStateClass)
|
||||
.put("gps_number", telemetry.gpsSatelliteCount)
|
||||
.put("is_fixed", if (telemetry.hasFixedPosition()) 1 else 0)
|
||||
.put("quality", telemetry.gpsSignalLevel)
|
||||
.put("rtk_number", telemetry.rtkSatelliteCount)
|
||||
|
||||
private fun rcDistanceLimitStatusJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, RcDistanceLimitStatusClass)
|
||||
.put("state", if (telemetry.distanceLimitEnabled) 1 else 0)
|
||||
.put("distance_limit", telemetry.distanceLimit)
|
||||
|
||||
private fun storageJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, StorageClass)
|
||||
.put("total", telemetry.storageTotal.toCloudStorageUnit())
|
||||
.put("used", telemetry.storageUsed().toCloudStorageUnit())
|
||||
|
||||
private fun osdCameraJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, OsdCameraClass)
|
||||
.put("payload_index", Matrice4CameraPayloadIndex)
|
||||
.put("camera_mode", telemetry.cameraModeCode())
|
||||
.put("photo_state", telemetry.photoState)
|
||||
.put("recording_state", telemetry.recordingState)
|
||||
.put("record_time", telemetry.recordingTime)
|
||||
.put("remain_photo_num", telemetry.remainPhotoNum.toLong())
|
||||
.put("remain_record_duration", telemetry.remainRecordDuration)
|
||||
.put("zoom_factor", telemetry.zoomFactor)
|
||||
.put("ir_zoom_factor", telemetry.irZoomFactor)
|
||||
.put("screen_split_enable", false)
|
||||
|
||||
private fun rcDronePayloadJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, RcDronePayloadClass)
|
||||
.put("payload_index", Matrice4CameraPayloadIndex)
|
||||
.put("gimbal_pitch", telemetry.gimbalPitch.toFloat())
|
||||
.put("gimbal_roll", telemetry.gimbalRoll.toFloat())
|
||||
.put("gimbal_yaw", telemetry.gimbalYaw.toFloat())
|
||||
.put("measure_target_altitude", telemetry.laserTargetAltitude.toFloat())
|
||||
.put("measure_target_distance", telemetry.laserDistance.toFloat())
|
||||
.put("measure_target_error_state", telemetry.laserTargetErrorState())
|
||||
.put("measure_target_latitude", telemetry.laserTargetLatitude.toFloat())
|
||||
.put("measure_target_longitude", telemetry.laserTargetLongitude.toFloat())
|
||||
.put("smart_track_point", JSONArray())
|
||||
.put("thermal_global_temperature_max", telemetry.thermalMaxTemperature.toFloat())
|
||||
|
||||
private fun topicRequest(cloudSession: CloudSession): JSONObject =
|
||||
JSONObject()
|
||||
.put("tid", UUID.randomUUID().toString())
|
||||
.put("bid", UUID.randomUUID().toString())
|
||||
.put("timestamp", System.currentTimeMillis())
|
||||
.put("username", cloudSession.payloadUsername)
|
||||
|
||||
private fun publishReply(
|
||||
request: CloudCommandRequest,
|
||||
success: Boolean,
|
||||
message: String
|
||||
) {
|
||||
publishReply(request.topic, request.tid, request.bid, request.method, request.seq, success, message)
|
||||
}
|
||||
|
||||
private fun publishReply(
|
||||
topic: String,
|
||||
tid: String,
|
||||
bid: String,
|
||||
method: String,
|
||||
seq: Long?,
|
||||
success: Boolean,
|
||||
message: String
|
||||
) {
|
||||
val payloadUsername = session?.payloadUsername.orEmpty()
|
||||
val isDrcReply = topic.endsWith("/drc/down")
|
||||
val replyTopic = when {
|
||||
isDrcReply -> topic.removeSuffix("/down") + "/up"
|
||||
topic.endsWith("/services") -> "${topic}_reply"
|
||||
topic.endsWith("/property/set") -> "${topic}_reply"
|
||||
else -> "${topic}_reply"
|
||||
}
|
||||
val data = JSONObject()
|
||||
.put("result", if (success) 0 else -1)
|
||||
.put("message", message)
|
||||
if (isDrcReply && seq != null) {
|
||||
data.put("output", JSONObject().put("seq", seq))
|
||||
}
|
||||
val payload = JSONObject()
|
||||
.put("tid", tid.ifBlank { UUID.randomUUID().toString() })
|
||||
.put("bid", bid.ifBlank { UUID.randomUUID().toString() })
|
||||
.put("timestamp", System.currentTimeMillis())
|
||||
.put("username", payloadUsername)
|
||||
.put("method", method.ifBlank { "reply" })
|
||||
.put("data", data)
|
||||
if (isDrcReply && seq != null) {
|
||||
payload.put("seq", seq)
|
||||
}
|
||||
publish(replyTopic, payload, qos = 1)
|
||||
}
|
||||
|
||||
private fun publish(topic: String, payload: JSONObject, qos: Int) {
|
||||
val mqttClient = client ?: return
|
||||
if (!mqttClient.isConnected) return
|
||||
runCatching {
|
||||
val message = MqttMessage(payload.toString().toByteArray(Charsets.UTF_8)).apply {
|
||||
this.qos = qos
|
||||
isRetained = false
|
||||
}
|
||||
mqttClient.publish(topic, message)
|
||||
Log.d(CloudMqttLogTag, "published topic=$topic payload=$payload")
|
||||
}.onFailure { error ->
|
||||
if (error is MqttException) Log.e(CloudMqttLogTag, "publish failed reason=${error.reasonCode}", error)
|
||||
_state.update { it.copy(lastError = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isConnected(): Boolean = client?.isConnected == true && session != null
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.toModeCode(productConnected: Boolean): Int {
|
||||
if (!productConnected) return 14
|
||||
if (!isFlying && !motorsOn && !landingMode) return 0
|
||||
val flightModeStringCompact = flightModeString.cloudModeCompact()
|
||||
val fcFlightModeCompact = fcFlightMode.cloudModeCompact()
|
||||
val flightModeCompact = flightMode.cloudModeCompact()
|
||||
val compact = listOf(flightModeStringCompact, fcFlightModeCompact, flightModeCompact)
|
||||
.joinToString("|")
|
||||
val keyModeIsManualControl = flightModeCompact.isManualControlCloudMode()
|
||||
val stringModeIsManualControl = flightModeStringCompact.isManualControlCloudMode()
|
||||
return when {
|
||||
compact.contains("TAKEOFFPREPARATION") || compact.contains("TAKEOFFPREPARE") -> 1
|
||||
compact.contains("READYFORTAKEOFF") || compact.contains("TAKEOFFFINISHED") -> 2
|
||||
compact.contains("AUTOTAKEOFF") || compact.contains("TAKEOFFAUTO") -> 4
|
||||
compact.contains("WAYLINE") || compact.contains("ROUTE") || compact.contains("WAYPOINT") -> 5
|
||||
compact.contains("PANORAMA") -> 6
|
||||
compact.contains("ACTIVETRACK") -> 7
|
||||
compact.contains("ADSB") -> 8
|
||||
compact.contains("GOHOME") || compact.contains("RETURN") || compact.contains("RTH") -> 9
|
||||
compact.contains("AUTOLANDING") || compact.contains("LANDINGAUTO") || landingMode -> 10
|
||||
compact.contains("FORCEDLANDING") || compact.contains("LANDINGFORCED") -> 11
|
||||
compact.contains("THREEBLADE") || compact.contains("THREEPROPELLER") -> 12
|
||||
compact.contains("UPDATING") || compact.contains("UPGRADING") -> 13
|
||||
compact.contains("DISCONNECTED") -> 14
|
||||
compact.contains("APAS") -> 15
|
||||
compact.contains("VIRTUALJOYSTICK") -> 16
|
||||
compact.contains("COMMAND") || compact.contains("LIVEFLIGHTCONTROL") -> 17
|
||||
compact.contains("POI") || compact.contains("ORBIT") -> 20
|
||||
compact.contains("FLYTOLIVETARGET") ||
|
||||
compact.contains("GOTARGETPOINT") ||
|
||||
compact.contains("CLICKGO") ||
|
||||
compact.contains("TAPFLY") -> 17
|
||||
keyModeIsManualControl || stringModeIsManualControl -> 3
|
||||
compact.contains("AIRBORNERTKCONVERGENCE") || compact.contains("RTK") -> 18
|
||||
else -> 3
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.cloudModeCompact(): String =
|
||||
uppercase()
|
||||
.replace(" ", "")
|
||||
.replace("_", "")
|
||||
.replace("-", "")
|
||||
|
||||
private fun String.isManualControlCloudMode(): Boolean =
|
||||
this == "MANUAL" ||
|
||||
this == "JOYSTICK" ||
|
||||
this == "PGPS" ||
|
||||
contains("手动飞行") ||
|
||||
contains("空中悬停") ||
|
||||
contains("运动") ||
|
||||
contains("姿态") ||
|
||||
contains("平稳") ||
|
||||
contains("MANUALFLIGHT") ||
|
||||
contains("GPSNORMAL") ||
|
||||
contains("GPSATTI") ||
|
||||
contains("GPSCL") ||
|
||||
contains("GPSHOMELOCK") ||
|
||||
contains("GPSBRAKE") ||
|
||||
contains("GPS") ||
|
||||
contains("ATTI") ||
|
||||
contains("HOVER") ||
|
||||
contains("SPORT") ||
|
||||
contains("TRIPOD") ||
|
||||
contains("CINEMATIC") ||
|
||||
contains("CINE") ||
|
||||
contains("FPV")
|
||||
|
||||
private fun TelemetrySnapshot.droneOsdLatitude(): Double =
|
||||
if (hasUsableRtkPosition()) rtkLatitude else latitude
|
||||
|
||||
private fun TelemetrySnapshot.droneOsdLongitude(): Double =
|
||||
if (hasUsableRtkPosition()) rtkLongitude else longitude
|
||||
|
||||
private fun TelemetrySnapshot.droneOsdPositionSource(): String =
|
||||
if (hasUsableRtkPosition()) "RTK" else "FC"
|
||||
|
||||
private fun TelemetrySnapshot.hasFixedPosition(): Boolean =
|
||||
gpsValid || hasUsableRtkPosition()
|
||||
|
||||
private fun TelemetrySnapshot.hasUsableRtkPosition(): Boolean =
|
||||
rtkLocationValid && (rtkHealthy || rtkFusionDataUsable || rtkPositioningSolution.isSolvedRtkSolution())
|
||||
|
||||
private fun String.isSolvedRtkSolution(): Boolean {
|
||||
val compact = uppercase().replace("_", "")
|
||||
return compact == "SINGLEPOINT" || compact == "FLOAT" || compact == "FIXEDPOINT"
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.homeDistanceMeters(): Float {
|
||||
if (!isValidCoordinate(latitude, longitude) || !isValidCoordinate(homeLatitude, homeLongitude)) return 0f
|
||||
val dLat = Math.toRadians(latitude - homeLatitude)
|
||||
val dLon = Math.toRadians(longitude - homeLongitude)
|
||||
val startLat = Math.toRadians(homeLatitude)
|
||||
val endLat = Math.toRadians(latitude)
|
||||
val a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(startLat) * cos(endLat) * sin(dLon / 2) * sin(dLon / 2)
|
||||
return (EarthRadiusMeters * 2 * atan2(sqrt(a), sqrt(1 - a))).toFloat()
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.storageUsed(): Int =
|
||||
(storageTotal - storageRemain).coerceAtLeast(0)
|
||||
|
||||
private fun Int.toCloudStorageUnit(): Long {
|
||||
val value = coerceAtLeast(0).toLong()
|
||||
return if (value in 1..999_999) value * 1000L else value
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.laserTargetErrorState(): Int =
|
||||
if (laserDistance > 0.0 && isValidCoordinate(laserTargetLatitude, laserTargetLongitude)) 0 else 3
|
||||
|
||||
private fun TelemetrySnapshot.batteryHighVoltageStorageDays(): Int =
|
||||
(batteryHighVoltageStorageSeconds / 86_400L)
|
||||
.coerceIn(0L, Int.MAX_VALUE.toLong())
|
||||
.toInt()
|
||||
|
||||
private fun cloudArrayList(vararg items: Any): JSONArray =
|
||||
JSONArray()
|
||||
.put(JavaArrayListClass)
|
||||
.put(
|
||||
JSONArray().apply {
|
||||
items.forEach { put(it) }
|
||||
}
|
||||
)
|
||||
|
||||
private fun TelemetrySnapshot.cameraModeCode(): Int {
|
||||
val mode = cameraWorkMode.uppercase()
|
||||
return when {
|
||||
mode.contains("RECORD") || mode.contains("VIDEO") -> 1
|
||||
mode.contains("PANO") -> 3
|
||||
mode.contains("INTERVAL") || mode.contains("TIMED") -> 4
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidCoordinate(latitude: Double, longitude: Double): Boolean {
|
||||
if (latitude.isNaN() || longitude.isNaN()) return false
|
||||
if (latitude == 0.0 && longitude == 0.0) return false
|
||||
return latitude in -90.0..90.0 && longitude in -180.0..180.0
|
||||
}
|
||||
|
||||
private fun JSONObject.optLongOrNull(name: String): Long? =
|
||||
if (has(name) && !isNull(name)) optLong(name) else null
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.zklh.dronecontroller.core.diagnostics
|
||||
|
||||
import dji.v5.manager.diagnostic.DeviceHealthManager
|
||||
import dji.v5.manager.diagnostic.DJIDeviceHealthInfo
|
||||
import dji.v5.manager.diagnostic.DJIDeviceHealthInfoChangeListener
|
||||
import dji.v5.manager.diagnostic.WarningLevel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
data class DroneWarningState(
|
||||
val active: Boolean = false,
|
||||
val level: WarningLevel = WarningLevel.NORMAL,
|
||||
val message: String = "无实时告警",
|
||||
val count: Int = 0,
|
||||
val messages: List<String> = emptyList(),
|
||||
val raw: String = ""
|
||||
)
|
||||
|
||||
class DroneWarningRepository {
|
||||
private val _state = MutableStateFlow(DroneWarningState())
|
||||
val state: StateFlow<DroneWarningState> = _state.asStateFlow()
|
||||
|
||||
private val healthInfoChangeListener = DJIDeviceHealthInfoChangeListener {
|
||||
updateWarnings()
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var started = false
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
DeviceHealthManager.getInstance().addDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
||||
updateWarnings()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!started) return
|
||||
started = false
|
||||
DeviceHealthManager.getInstance().removeDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_state.value = DroneWarningState()
|
||||
}
|
||||
|
||||
private fun updateWarnings() {
|
||||
val messages = DeviceHealthManager.getInstance().currentDJIDeviceHealthInfos
|
||||
.map { it.toMessage() }
|
||||
.filter { it.text.isNotBlank() }
|
||||
.sortedWith(compareByDescending<WarningMessage> { warningSeverity(it.level) }.thenBy { it.text })
|
||||
|
||||
if (messages.isEmpty()) {
|
||||
_state.value = DroneWarningState()
|
||||
return
|
||||
}
|
||||
|
||||
val top = messages.first()
|
||||
_state.value = DroneWarningState(
|
||||
active = true,
|
||||
level = top.level,
|
||||
message = top.text,
|
||||
count = messages.size,
|
||||
messages = messages.map { it.text },
|
||||
raw = messages.joinToString(separator = "\n") { "${it.level}:${it.code}:${it.text}" }
|
||||
)
|
||||
}
|
||||
|
||||
private fun DJIDeviceHealthInfo.toMessage(): WarningMessage {
|
||||
val description = description()
|
||||
val title = title()
|
||||
val code = informationCode()
|
||||
val text = when {
|
||||
description.isNotBlank() -> description
|
||||
title.isNotBlank() -> title
|
||||
else -> code
|
||||
}
|
||||
return WarningMessage(text = text, level = warningLevel(), code = code)
|
||||
}
|
||||
|
||||
private data class WarningMessage(
|
||||
val text: String,
|
||||
val level: WarningLevel,
|
||||
val code: String
|
||||
)
|
||||
|
||||
private fun warningSeverity(level: WarningLevel): Int =
|
||||
when (level) {
|
||||
WarningLevel.SERIOUS_WARNING -> 5
|
||||
WarningLevel.WARNING -> 4
|
||||
WarningLevel.CAUTION -> 3
|
||||
WarningLevel.NOTICE -> 2
|
||||
WarningLevel.UNKNOWN -> 1
|
||||
WarningLevel.NORMAL -> 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.zklh.dronecontroller.core.flight
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class FlightControlService {
|
||||
suspend fun startTakeoff(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStartTakeoff.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动起飞已开始"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopTakeoff(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStopTakeoff.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("起飞已取消"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startAutoLanding(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStartAutoLanding.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动降落已开始"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopAutoLanding(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStopAutoLanding.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动降落已取消"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun confirmLanding(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyConfirmLanding.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动降落已确认"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startGoHome(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStartGoHome.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动返航已开始"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopGoHome(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStopGoHome.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("返航已取消"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.zklh.dronecontroller.core.flight
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.intelligent.IntelligentFlightManager
|
||||
import dji.v5.manager.intelligent.flyto.FlyToParam
|
||||
import dji.v5.manager.intelligent.flyto.FlyToTarget
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
private const val FlyToLogTag = "ZklhFlyTo"
|
||||
|
||||
class FlyToService {
|
||||
suspend fun startFlyTo(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
height: Double,
|
||||
maxSpeed: Int = 14,
|
||||
securityTakeoffHeight: Int = 20,
|
||||
flyToMode: FlyToMode = FlyToMode.SET_HEIGHT
|
||||
): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val manager = IntelligentFlightManager.getInstance().flyToMissionManager
|
||||
val target = FlyToTarget().apply {
|
||||
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
||||
this.maxSpeed = maxSpeed
|
||||
this.securityTakeoffHeight = securityTakeoffHeight
|
||||
}
|
||||
Log.d(
|
||||
FlyToLogTag,
|
||||
"startFlyTo lat=$latitude lon=$longitude height=$height mode=$flyToMode maxSpeed=$maxSpeed securityTakeoffHeight=$securityTakeoffHeight"
|
||||
)
|
||||
manager.startMission(
|
||||
target,
|
||||
null,
|
||||
callback(continuation, "指点飞行已开始")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopFlyTo(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.stopMission(
|
||||
callback(continuation, "指点飞行已停止")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setFlyToMode(mode: FlyToMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val param = FlyToParam().apply { flyToMode = mode }
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(
|
||||
param,
|
||||
callback(continuation, "指点飞行模式已更新:${mode.name}")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setFlyToHeight(height: Int): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val param = FlyToParam().apply { this.height = height }
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(
|
||||
param,
|
||||
callback(continuation, "指点飞行高度已更新:${height}m")
|
||||
)
|
||||
}
|
||||
|
||||
private fun callback(
|
||||
continuation: kotlinx.coroutines.CancellableContinuation<DjiCommandResult>,
|
||||
successMessage: String
|
||||
): CommonCallbacks.CompletionCallback =
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.zklh.dronecontroller.core.gimbal
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.sdk.keyvalue.key.GimbalKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||
import dji.sdk.keyvalue.value.gimbal.CtrlInfo
|
||||
import dji.sdk.keyvalue.value.gimbal.GimbalSpeedRotation
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class GimbalControlService(
|
||||
private val gimbalIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||
) {
|
||||
suspend fun rotateBySpeed(
|
||||
pitchSpeed: Double,
|
||||
yawSpeed: Double
|
||||
): DjiCommandResult =
|
||||
runAction(
|
||||
KeyTools.createKey(GimbalKey.KeyRotateBySpeed, gimbalIndex),
|
||||
GimbalSpeedRotation(
|
||||
pitchSpeed.coerceIn(-100.0, 100.0),
|
||||
yawSpeed.coerceIn(-100.0, 100.0),
|
||||
0.0,
|
||||
CtrlInfo()
|
||||
),
|
||||
"云台控制已下发"
|
||||
)
|
||||
|
||||
suspend fun reset(): DjiCommandResult =
|
||||
runAction(
|
||||
KeyTools.createKey(GimbalKey.KeyRestoreFactorySettings, gimbalIndex),
|
||||
EmptyMsg(),
|
||||
"云台复位已下发"
|
||||
)
|
||||
|
||||
private suspend fun <Param, Result> runAction(
|
||||
key: DJIKey.ActionKey<Param, Result>,
|
||||
param: Param,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.action(
|
||||
param,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
package com.zklh.dronecontroller.core.livestream
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamSettings
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamStatus
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamStatusListener
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamType
|
||||
import dji.v5.manager.datacenter.livestream.LiveVideoBitrateMode
|
||||
import dji.v5.manager.datacenter.livestream.StreamQuality
|
||||
import dji.v5.manager.datacenter.livestream.settings.RtmpSettings
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val LiveStreamLogTag = "ZklhLiveStream"
|
||||
private const val UrlTypeRtmp = 1
|
||||
private const val UrlTypeWhip = 4
|
||||
private const val ZlmRtmpPort = 1935
|
||||
private const val ZlmDefaultApp = "live"
|
||||
private const val DefaultLiveQuality = 3
|
||||
|
||||
data class LiveStreamingState(
|
||||
val streaming: Boolean = false,
|
||||
val busy: Boolean = false,
|
||||
val url: String = "",
|
||||
val urlType: String = "",
|
||||
val videoId: String = "",
|
||||
val quality: Int = DefaultLiveQuality,
|
||||
val fps: Int = 0,
|
||||
val vbps: Int = 0,
|
||||
val resolution: String = "",
|
||||
val message: String = "直播未开启",
|
||||
val lastError: String? = null
|
||||
)
|
||||
|
||||
private data class LiveStartConfig(
|
||||
val url: String,
|
||||
val urlType: Int,
|
||||
val videoId: String,
|
||||
val quality: Int
|
||||
)
|
||||
|
||||
class LiveStreamingService {
|
||||
private val _state = MutableStateFlow(LiveStreamingState())
|
||||
val state: StateFlow<LiveStreamingState> = _state.asStateFlow()
|
||||
|
||||
private var listenerRegistered = false
|
||||
private var lastConfig: LiveStartConfig? = null
|
||||
|
||||
private val statusListener = object : LiveStreamStatusListener {
|
||||
override fun onLiveStreamStatusUpdate(status: LiveStreamStatus?) {
|
||||
val resolution = status?.resolution?.let { "${it.width}x${it.height}" }.orEmpty()
|
||||
_state.update {
|
||||
it.copy(
|
||||
streaming = status?.isStreaming ?: liveStreamManager().isStreaming,
|
||||
fps = status?.fps ?: 0,
|
||||
vbps = status?.vbps ?: 0,
|
||||
resolution = resolution,
|
||||
message = if (status?.isStreaming == true) "直播推流中" else it.message,
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: IDJIError?) {
|
||||
val message = error?.toString() ?: "直播推流异常"
|
||||
Log.e(LiveStreamLogTag, "live stream error=$message")
|
||||
_state.update { it.copy(lastError = message, message = message) }
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
ensureListener()
|
||||
_state.update { it.copy(streaming = liveStreamManager().isStreaming) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
if (listenerRegistered) {
|
||||
runCatching { liveStreamManager().removeLiveStreamStatusListener(statusListener) }
|
||||
listenerRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun toggleManual(): DjiCommandResult {
|
||||
ensureListener()
|
||||
if (liveStreamManager().isStreaming || _state.value.streaming) {
|
||||
return stop()
|
||||
}
|
||||
val config = lastConfig
|
||||
?: return DjiCommandResult.failed("请先从平台下发 RTMP 推流地址,再在遥控器侧开启直播")
|
||||
return startWithConfig(config, manual = true)
|
||||
}
|
||||
|
||||
suspend fun startFromCloud(data: JSONObject): DjiCommandResult {
|
||||
ensureListener()
|
||||
val rawConfig = data.toLiveStartConfig()
|
||||
?: return DjiCommandResult.failed("直播参数无效:需要直播推流参数")
|
||||
val config = rawConfig.toMsdkRtmpConfig()
|
||||
?: return DjiCommandResult.failed("直播参数无效:无法转换为 MSDK 可用的 RTMP 推流地址")
|
||||
if (rawConfig.urlType == UrlTypeWhip || rawConfig.url.contains("whip", ignoreCase = true)) {
|
||||
Log.i(LiveStreamLogTag, "converted WHIP push url to RTMP: ${config.url}")
|
||||
}
|
||||
if (!config.isRtmpPush()) {
|
||||
return DjiCommandResult.failed("当前仅支持 RTMP 推流,收到 url_type=${config.urlType}")
|
||||
}
|
||||
return startWithConfig(config, manual = false)
|
||||
}
|
||||
|
||||
suspend fun stop(): DjiCommandResult {
|
||||
ensureListener()
|
||||
if (!liveStreamManager().isStreaming && !_state.value.streaming) {
|
||||
_state.update { it.copy(streaming = false, busy = false, message = "直播未开启", lastError = null) }
|
||||
return DjiCommandResult.ok("直播未开启")
|
||||
}
|
||||
_state.update { it.copy(busy = true, message = "正在关闭直播", lastError = null) }
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
liveStreamManager().stopStream(object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_state.update { it.copy(streaming = false, busy = false, message = "直播已关闭", lastError = null) }
|
||||
continuation.resume(DjiCommandResult.ok("直播已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
val message = error.toString()
|
||||
_state.update { it.copy(busy = false, lastError = message, message = "关闭直播失败:$message") }
|
||||
continuation.resume(DjiCommandResult.failed("关闭直播失败:$message"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun setQualityFromCloud(data: JSONObject): DjiCommandResult {
|
||||
ensureListener()
|
||||
val quality = data.optIntAny(_state.value.quality, "video_quality", "videoQuality", "quality")
|
||||
val streamQuality = quality.toStreamQuality()
|
||||
liveStreamManager().liveStreamQuality = streamQuality
|
||||
lastConfig = lastConfig?.copy(quality = quality)
|
||||
_state.update { it.copy(quality = quality, message = "直播清晰度已设置:${streamQuality.name}", lastError = null) }
|
||||
return DjiCommandResult.ok("直播清晰度已设置:${streamQuality.name}")
|
||||
}
|
||||
|
||||
private suspend fun startWithConfig(config: LiveStartConfig, manual: Boolean): DjiCommandResult {
|
||||
lastConfig = config
|
||||
val manager = liveStreamManager()
|
||||
if (manager.isStreaming && _state.value.url == config.url) {
|
||||
_state.update { it.copy(streaming = true, busy = false, message = "直播已在推流中", lastError = null) }
|
||||
return DjiCommandResult.ok("直播已在推流中")
|
||||
}
|
||||
if (manager.isStreaming) {
|
||||
val stopResult = stop()
|
||||
if (!stopResult.success) return stopResult
|
||||
}
|
||||
|
||||
val cameraIndex = config.videoId.toCameraIndex()
|
||||
runCatching {
|
||||
cameraStreamManager().enableStream(cameraIndex, true)
|
||||
manager.cameraIndex = cameraIndex
|
||||
manager.liveStreamSettings = LiveStreamSettings.Builder()
|
||||
.setLiveStreamType(LiveStreamType.RTMP)
|
||||
.setRtmpSettings(RtmpSettings.Builder().setUrl(config.url).build())
|
||||
.build()
|
||||
manager.liveStreamQuality = config.quality.toStreamQuality()
|
||||
manager.liveStreamScaleType = ICameraStreamManager.ScaleType.CENTER_CROP
|
||||
manager.liveVideoBitrateMode = LiveVideoBitrateMode.AUTO
|
||||
manager.setLiveAudioEnabled(false)
|
||||
}.onFailure { error ->
|
||||
val message = error.message ?: error.toString()
|
||||
_state.update { it.copy(busy = false, lastError = message, message = "直播配置失败:$message") }
|
||||
return DjiCommandResult.failed("直播配置失败:$message")
|
||||
}
|
||||
|
||||
Log.d(
|
||||
LiveStreamLogTag,
|
||||
"start live stream manual=$manual url=${config.url} videoId=${config.videoId} quality=${config.quality} camera=$cameraIndex"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
busy = true,
|
||||
url = config.url,
|
||||
urlType = "RTMP",
|
||||
videoId = config.videoId,
|
||||
quality = config.quality,
|
||||
message = "正在开启直播",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
manager.startStream(object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_state.update { it.copy(streaming = true, busy = false, message = "直播已开启", lastError = null) }
|
||||
continuation.resume(DjiCommandResult.ok("直播已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
val message = error.toString()
|
||||
_state.update { it.copy(streaming = false, busy = false, lastError = message, message = "开启直播失败:$message") }
|
||||
continuation.resume(DjiCommandResult.failed("开启直播失败:$message"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureListener() {
|
||||
if (listenerRegistered) return
|
||||
liveStreamManager().addLiveStreamStatusListener(statusListener)
|
||||
listenerRegistered = true
|
||||
}
|
||||
|
||||
private fun liveStreamManager() = MediaDataCenter.getInstance().liveStreamManager
|
||||
|
||||
private fun cameraStreamManager() = MediaDataCenter.getInstance().cameraStreamManager
|
||||
}
|
||||
|
||||
private fun JSONObject.toLiveStartConfig(): LiveStartConfig? {
|
||||
val url = optObjectString("url", "address")
|
||||
.ifBlank { optStringAny("rtmp_url", "rtmpUrl", "push_url", "pushUrl", "stream_url", "streamUrl") }
|
||||
val urlType = optIntAny(
|
||||
defaultValue = if (url.startsWith("rtmp", ignoreCase = true)) UrlTypeRtmp else -1,
|
||||
"url_type",
|
||||
"urlType",
|
||||
"type"
|
||||
)
|
||||
val normalizedUrlType = when {
|
||||
urlType > -1 -> urlType
|
||||
url.startsWith("rtmp", ignoreCase = true) -> UrlTypeRtmp
|
||||
url.contains("whip", ignoreCase = true) -> UrlTypeWhip
|
||||
else -> -1
|
||||
}
|
||||
if (url.isBlank() && normalizedUrlType != UrlTypeWhip) return null
|
||||
return LiveStartConfig(
|
||||
url = url,
|
||||
urlType = normalizedUrlType,
|
||||
videoId = optValueAsString("video_id", "videoId", "videoID"),
|
||||
quality = optIntAny(DefaultLiveQuality, "video_quality", "videoQuality", "quality")
|
||||
)
|
||||
}
|
||||
|
||||
private fun LiveStartConfig.isRtmpPush(): Boolean =
|
||||
urlType == UrlTypeRtmp || url.startsWith("rtmp", ignoreCase = true)
|
||||
|
||||
private fun LiveStartConfig.toMsdkRtmpConfig(): LiveStartConfig? {
|
||||
if (url.startsWith("rtmp", ignoreCase = true)) {
|
||||
return copy(urlType = UrlTypeRtmp)
|
||||
}
|
||||
if (urlType != UrlTypeWhip && !url.contains("whip", ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
val rtmpUrl = url.toZlmRtmpUrl(videoId)
|
||||
return rtmpUrl.takeIf { it.isNotBlank() }?.let { copy(url = it, urlType = UrlTypeRtmp) }
|
||||
}
|
||||
|
||||
private fun String.toZlmRtmpUrl(videoId: String): String {
|
||||
val uri = runCatching { Uri.parse(this) }.getOrNull() ?: return ""
|
||||
val host = uri.host.orEmpty()
|
||||
val app = uri.getQueryParameter("app")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: uri.pathSegments.firstOrNull { it == ZlmDefaultApp }
|
||||
?: ZlmDefaultApp
|
||||
val stream = uri.getQueryParameter("stream")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: uri.pathSegments.lastOrNull()
|
||||
?.removeSuffix(".live.flv")
|
||||
?.removeSuffix(".live.mp4")
|
||||
?.removeSuffix(".m3u8")
|
||||
?.takeIf { it.isNotBlank() && it != app }
|
||||
?: videoId.toZlmStreamName()
|
||||
if (host.isBlank() || stream.isBlank()) return ""
|
||||
return "rtmp://$host:$ZlmRtmpPort/$app/$stream"
|
||||
}
|
||||
|
||||
private fun String.toZlmStreamName(): String =
|
||||
trim()
|
||||
.replace("/", "_")
|
||||
|
||||
private fun JSONObject.optStringAny(vararg names: String): String {
|
||||
for (name in names) {
|
||||
val value = optValueAsString(name)
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optObjectString(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (!has(name) || isNull(name)) continue
|
||||
val raw = opt(name)
|
||||
when (raw) {
|
||||
is JSONObject -> {
|
||||
val value = raw.optStringAny("url", "rtmp_url", "rtmpUrl", "push_url", "pushUrl")
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
is JSONArray -> {
|
||||
val value = raw.arrayValueAsString()
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
else -> raw?.toString()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optValueAsString(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (!has(name) || isNull(name)) continue
|
||||
val raw = opt(name)
|
||||
val value = when (raw) {
|
||||
is JSONArray -> raw.arrayValueAsString()
|
||||
is JSONObject -> raw.optStringAny("value", "id", "url")
|
||||
else -> raw?.toString().orEmpty()
|
||||
}
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONArray.arrayValueAsString(): String {
|
||||
if (length() == 0) return ""
|
||||
for (i in length() - 1 downTo 0) {
|
||||
val value = opt(i)
|
||||
if (value is String && value.isNotBlank()) return value
|
||||
}
|
||||
return optString(length() - 1, "")
|
||||
}
|
||||
|
||||
private fun JSONObject.optIntAny(defaultValue: Int, vararg names: String): Int {
|
||||
for (name in names) {
|
||||
if (!has(name) || isNull(name)) continue
|
||||
val raw = opt(name)
|
||||
val value = when (raw) {
|
||||
is Number -> raw.toInt()
|
||||
is String -> raw.toIntOrNull() ?: raw.liveEnumValueOrNull()
|
||||
is JSONArray -> raw.arrayValueAsString().toIntOrNull() ?: raw.arrayValueAsString().liveEnumValueOrNull()
|
||||
else -> null
|
||||
}
|
||||
if (value != null) return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
private fun String.liveEnumValueOrNull(): Int? =
|
||||
when (trim().uppercase()) {
|
||||
"RTMP" -> UrlTypeRtmp
|
||||
"WHIP" -> UrlTypeWhip
|
||||
"AUTO" -> 0
|
||||
"SMOOTH" -> 1
|
||||
"STANDARD_DEFINITION", "SD" -> 2
|
||||
"HIGH_DEFINITION", "HD" -> 3
|
||||
"ULTRA_HD", "FULL_HD", "FHD" -> 4
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun Int.toStreamQuality(): StreamQuality =
|
||||
when (this) {
|
||||
1, 2 -> StreamQuality.SD
|
||||
3 -> StreamQuality.HD
|
||||
4 -> StreamQuality.FULL_HD
|
||||
100 -> StreamQuality.ORIGINAL
|
||||
else -> StreamQuality.HD
|
||||
}
|
||||
|
||||
private fun String.toCameraIndex(): ComponentIndexType =
|
||||
when {
|
||||
contains("/FPV", ignoreCase = true) -> ComponentIndexType.FPV
|
||||
contains("/right", ignoreCase = true) -> ComponentIndexType.RIGHT
|
||||
else -> ComponentIndexType.LEFT_OR_MAIN
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.zklh.dronecontroller.core.media
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType
|
||||
import dji.sdk.keyvalue.value.common.CameraLensType
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.set
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class CameraControlService(
|
||||
private val cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||
) {
|
||||
suspend fun setVisibleZoomRatio(zoomRatio: Double): DjiCommandResult =
|
||||
setDoubleKey(
|
||||
KeyTools.createCameraKey(CameraKey.KeyCameraZoomRatios, cameraIndex, CameraLensType.CAMERA_LENS_ZOOM),
|
||||
zoomRatio,
|
||||
"可见光变焦已设置:${"%.1f".format(zoomRatio)}x"
|
||||
)
|
||||
|
||||
suspend fun setThermalZoomRatio(zoomRatio: Double): DjiCommandResult =
|
||||
setDoubleKey(
|
||||
KeyTools.createCameraKey(CameraKey.KeyThermalZoomRatios, cameraIndex, CameraLensType.CAMERA_LENS_THERMAL),
|
||||
zoomRatio,
|
||||
"红外变焦已设置:${"%.1f".format(zoomRatio)}x"
|
||||
)
|
||||
|
||||
suspend fun setLens(lens: String): DjiCommandResult {
|
||||
val source = when (lens.normalizeLensName()) {
|
||||
"wide", "normal", "visible", "visible_wide", "camera_lens_wide" -> CameraVideoStreamSourceType.WIDE_CAMERA
|
||||
"zoom", "visible_zoom", "camera_lens_zoom" -> CameraVideoStreamSourceType.ZOOM_CAMERA
|
||||
"ir", "infrared", "thermal", "camera_lens_thermal" -> CameraVideoStreamSourceType.INFRARED_CAMERA
|
||||
else -> return DjiCommandResult.failed("未知镜头类型:$lens")
|
||||
}
|
||||
return setValueKey(
|
||||
KeyTools.createKey(CameraKey.KeyCameraVideoStreamSource, cameraIndex),
|
||||
source,
|
||||
"镜头已切换:$lens"
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setDoubleKey(
|
||||
key: DJIKey<Double>,
|
||||
value: Double,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
setValueKey(key, value, successMessage)
|
||||
|
||||
private suspend fun <T> setValueKey(
|
||||
key: DJIKey<T>,
|
||||
value: T,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.set(
|
||||
value,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.normalizeLensName(): String =
|
||||
trim()
|
||||
.replace("-", "_")
|
||||
.lowercase()
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
package com.zklh.dronecontroller.core.media
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.DJICameraKey
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.sdk.keyvalue.value.camera.CameraFlatMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraWorkMode
|
||||
import dji.sdk.keyvalue.value.camera.GeneratedMediaFileInfo
|
||||
import dji.sdk.keyvalue.value.camera.PhotoPanoramaMode
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.et.set
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.datacenter.media.MediaFile
|
||||
import dji.v5.manager.datacenter.media.MediaFileListDataSource
|
||||
import dji.v5.manager.datacenter.media.PullMediaFileListParam
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
private const val CameraMediaLogTag = "ZklhCameraMedia"
|
||||
|
||||
class CameraMediaService(
|
||||
defaultCameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||
) {
|
||||
private var activeCameraIndex: ComponentIndexType = defaultCameraIndex
|
||||
|
||||
suspend fun takePhoto(): DjiCommandResult {
|
||||
var lastFailure: DjiCommandResult? = null
|
||||
Log.d(CameraMediaLogTag, "takePhoto candidates=${candidateCameraIndexes()}")
|
||||
for (index in candidateCameraIndexes()) {
|
||||
val previousMedia = readLatestMediaFile(index)
|
||||
val previousSnapshot = readCachedMediaSnapshot("before photo")
|
||||
val modeResult = setPhotoCaptureMode(index)
|
||||
if (!modeResult.success) {
|
||||
lastFailure = modeResult
|
||||
Log.w(CameraMediaLogTag, "set photo mode failed camera=$index message=${modeResult.message}")
|
||||
continue
|
||||
}
|
||||
delay(700)
|
||||
|
||||
Log.d(CameraMediaLogTag, "start shoot photo camera=$index")
|
||||
val shootResult = runEmptyAction(
|
||||
CameraKey.KeyStartShootPhoto.create(index),
|
||||
"拍照指令已下发,正在等待文件写入"
|
||||
)
|
||||
if (!shootResult.success) {
|
||||
lastFailure = shootResult
|
||||
Log.w(CameraMediaLogTag, "start shoot photo failed camera=$index message=${shootResult.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
val saved = waitForGeneratedMediaFile(
|
||||
cameraIndex = index,
|
||||
previousMedia = previousMedia,
|
||||
previousSnapshot = previousSnapshot,
|
||||
successMessage = "拍照完成,文件已写入飞机存储",
|
||||
timeoutMessage = "拍照指令已下发,但没有检测到新照片文件。请检查飞机 SD 卡/内置存储、相机存储位置和电量告警。"
|
||||
)
|
||||
if (saved.success) {
|
||||
activeCameraIndex = index
|
||||
Log.d(CameraMediaLogTag, "photo saved camera=$index message=${saved.message}")
|
||||
return saved
|
||||
}
|
||||
lastFailure = saved
|
||||
Log.w(CameraMediaLogTag, "photo not saved camera=$index message=${saved.message}")
|
||||
}
|
||||
return lastFailure ?: DjiCommandResult.failed("拍照失败:未找到可用相机")
|
||||
}
|
||||
|
||||
suspend fun startRecord(): DjiCommandResult {
|
||||
var lastFailure: DjiCommandResult? = null
|
||||
for (index in candidateCameraIndexes()) {
|
||||
val modeResult = setVideoCaptureMode(index)
|
||||
if (!modeResult.success) {
|
||||
lastFailure = modeResult
|
||||
Log.w(CameraMediaLogTag, "set video mode failed camera=$index message=${modeResult.message}")
|
||||
continue
|
||||
}
|
||||
delay(900)
|
||||
|
||||
var startResult = runEmptyAction(
|
||||
CameraKey.KeyStartRecord.create(index),
|
||||
"录像已开始"
|
||||
)
|
||||
if (startResult.isRetryableStartRecordError()) {
|
||||
delay(1200)
|
||||
startResult = runEmptyAction(
|
||||
CameraKey.KeyStartRecord.create(index),
|
||||
"录像已开始"
|
||||
)
|
||||
}
|
||||
if (!startResult.success) {
|
||||
lastFailure = startResult
|
||||
Log.w(CameraMediaLogTag, "start record failed camera=$index message=${startResult.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
if (waitForRecordingState(index, recording = true, timeoutMs = 3_000L)) {
|
||||
activeCameraIndex = index
|
||||
Log.d(CameraMediaLogTag, "recording started camera=$index")
|
||||
return startResult
|
||||
}
|
||||
|
||||
lastFailure = DjiCommandResult.failed("录像指令已下发,但没有检测到相机进入录像状态")
|
||||
Log.w(CameraMediaLogTag, "recording state unchanged camera=$index")
|
||||
}
|
||||
return lastFailure ?: DjiCommandResult.failed("录像失败:未找到可用相机")
|
||||
}
|
||||
|
||||
suspend fun stopRecord(): DjiCommandResult {
|
||||
var lastFailure: DjiCommandResult? = null
|
||||
val recordingIndexes = candidateCameraIndexes().filter { isRecording(it) }
|
||||
val indexes = if (recordingIndexes.isNotEmpty()) recordingIndexes else candidateCameraIndexes()
|
||||
for (index in indexes) {
|
||||
val previousMedia = readLatestMediaFile(index)
|
||||
val previousSnapshot = readCachedMediaSnapshot("before stop record")
|
||||
val stopResult = runEmptyAction(
|
||||
CameraKey.KeyStopRecord.create(index),
|
||||
"录像停止指令已下发,正在等待文件写入"
|
||||
)
|
||||
if (!stopResult.success) {
|
||||
lastFailure = stopResult
|
||||
Log.w(CameraMediaLogTag, "stop record failed camera=$index message=${stopResult.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
if (!waitForRecordingState(index, recording = false, timeoutMs = 3_000L)) {
|
||||
lastFailure = DjiCommandResult.failed("停止录像指令已下发,但相机仍处于录像状态")
|
||||
Log.w(CameraMediaLogTag, "recording still true camera=$index")
|
||||
continue
|
||||
}
|
||||
|
||||
val saved = waitForGeneratedMediaFile(
|
||||
cameraIndex = index,
|
||||
previousMedia = previousMedia,
|
||||
previousSnapshot = previousSnapshot,
|
||||
successMessage = "录像已停止,文件已写入飞机存储",
|
||||
timeoutMessage = "录像已停止,但没有检测到新视频文件。请检查飞机 SD 卡/内置存储和相机存储状态。",
|
||||
timeoutMs = 12_000L
|
||||
)
|
||||
if (saved.success) {
|
||||
activeCameraIndex = index
|
||||
Log.d(CameraMediaLogTag, "recording stopped camera=$index message=${saved.message}")
|
||||
return saved
|
||||
}
|
||||
lastFailure = saved
|
||||
Log.w(CameraMediaLogTag, "record file not detected camera=$index message=${saved.message}")
|
||||
}
|
||||
return lastFailure ?: DjiCommandResult.failed("停止录像失败:未找到可用相机")
|
||||
}
|
||||
|
||||
suspend fun setCloudCameraMode(cameraMode: Int): DjiCommandResult =
|
||||
when (cameraMode) {
|
||||
0 -> setPhotoCaptureMode()
|
||||
1 -> setVideoCaptureMode()
|
||||
else -> DjiCommandResult.failed("未知相机模式:$cameraMode")
|
||||
}
|
||||
|
||||
suspend fun setPhotoCaptureMode(): DjiCommandResult =
|
||||
setPhotoCaptureMode(activeCameraIndex)
|
||||
|
||||
private suspend fun setPhotoCaptureMode(index: ComponentIndexType): DjiCommandResult {
|
||||
val cameraModeResult = setCameraMode(index, CameraMode.PHOTO_NORMAL)
|
||||
if (cameraModeResult.success) return cameraModeResult
|
||||
|
||||
val flatModeResult = setCameraFlatMode(index, CameraFlatMode.PHOTO_NORMAL)
|
||||
if (flatModeResult.success) return flatModeResult
|
||||
if (flatModeResult.isUnsupportedCameraFlatMode()) {
|
||||
val workModeResult = setCameraWorkMode(index, CameraWorkMode.SHOOT_PHOTO)
|
||||
return if (workModeResult.success || workModeResult.isUnsupportedCameraWorkMode()) {
|
||||
DjiCommandResult.ok("相机拍照模式已准备")
|
||||
} else {
|
||||
workModeResult
|
||||
}
|
||||
}
|
||||
return flatModeResult
|
||||
}
|
||||
|
||||
suspend fun setVideoCaptureMode(): DjiCommandResult =
|
||||
setVideoCaptureMode(activeCameraIndex)
|
||||
|
||||
private suspend fun setVideoCaptureMode(index: ComponentIndexType): DjiCommandResult {
|
||||
val cameraModeResult = setCameraMode(index, CameraMode.VIDEO_NORMAL)
|
||||
if (cameraModeResult.success) return cameraModeResult
|
||||
|
||||
val flatModeResult = setCameraFlatMode(index, CameraFlatMode.VIDEO_NORMAL)
|
||||
if (flatModeResult.success) return flatModeResult
|
||||
if (flatModeResult.isUnsupportedCameraFlatMode()) {
|
||||
val workModeResult = setCameraWorkMode(index, CameraWorkMode.RECORD_VIDEO)
|
||||
return if (workModeResult.success || workModeResult.isUnsupportedCameraWorkMode()) {
|
||||
DjiCommandResult.ok("相机录像模式已准备")
|
||||
} else {
|
||||
workModeResult
|
||||
}
|
||||
}
|
||||
return flatModeResult
|
||||
}
|
||||
|
||||
suspend fun toggleRecord(): DjiCommandResult =
|
||||
if (isRecording()) stopRecord() else startRecord()
|
||||
|
||||
suspend fun shootPanorama(
|
||||
mode: PhotoPanoramaMode = PhotoPanoramaMode.MODE_SPHERE
|
||||
): DjiCommandResult {
|
||||
val workModeResult = setCameraWorkMode(activeCameraIndex, CameraWorkMode.SHOOT_PHOTO)
|
||||
if (!workModeResult.success && !workModeResult.isUnsupportedCameraWorkMode()) return workModeResult
|
||||
|
||||
val panoramaModeResult = setPhotoPanoramaMode(activeCameraIndex, mode)
|
||||
if (!panoramaModeResult.success && !panoramaModeResult.isUnsupportedCameraPhotoMode()) return panoramaModeResult
|
||||
|
||||
return runEmptyAction(
|
||||
DJICameraKey.KeyStartShootPanoPhoto.create(activeCameraIndex),
|
||||
"全景拍照指令已下发"
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun toggleLaserFillLight(): DjiCommandResult {
|
||||
val next = !isLaserFillLightEnabled()
|
||||
return setLaserFillLightEnabled(next)
|
||||
}
|
||||
|
||||
suspend fun setLaserFillLightEnabled(enabled: Boolean): DjiCommandResult {
|
||||
return setBooleanKey(
|
||||
DJICameraKey.KeyLaserFillLightEnabled.create(activeCameraIndex),
|
||||
enabled,
|
||||
if (enabled) "蓝光/补光已开启" else "蓝光/补光已关闭"
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun toggleLaserMeasure(): DjiCommandResult {
|
||||
val next = !isLaserMeasureEnabled()
|
||||
return setLaserMeasureEnabled(next)
|
||||
}
|
||||
|
||||
suspend fun setLaserMeasureEnabled(enabled: Boolean): DjiCommandResult {
|
||||
return setBooleanKey(
|
||||
DJICameraKey.KeyLaserMeasureEnabled.create(activeCameraIndex),
|
||||
enabled,
|
||||
if (enabled) "激光测距已开启" else "激光测距已关闭"
|
||||
)
|
||||
}
|
||||
|
||||
fun isRecording(): Boolean =
|
||||
isRecording(activeCameraIndex)
|
||||
|
||||
private fun isRecording(index: ComponentIndexType): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyIsRecording.create(index).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun isLaserFillLightEnabled(): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyLaserFillLightEnabled.create(activeCameraIndex).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun isLaserMeasureEnabled(): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyLaserMeasureEnabled.create(activeCameraIndex).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
private fun candidateCameraIndexes(): List<ComponentIndexType> =
|
||||
listOf(
|
||||
activeCameraIndex,
|
||||
ComponentIndexType.LEFT_OR_MAIN,
|
||||
ComponentIndexType.RIGHT,
|
||||
ComponentIndexType.UP,
|
||||
ComponentIndexType.PORT_1,
|
||||
ComponentIndexType.PORT_2,
|
||||
ComponentIndexType.PORT_3,
|
||||
ComponentIndexType.PORT_4
|
||||
).distinct()
|
||||
|
||||
private suspend fun waitForGeneratedMediaFile(
|
||||
cameraIndex: ComponentIndexType,
|
||||
previousMedia: GeneratedMediaFileInfo?,
|
||||
previousSnapshot: MediaSnapshot?,
|
||||
successMessage: String,
|
||||
timeoutMessage: String,
|
||||
timeoutMs: Long = 8_000L
|
||||
): DjiCommandResult {
|
||||
val previousIdentity = previousMedia.mediaIdentity()
|
||||
var sawStoring = false
|
||||
val startAt = System.currentTimeMillis()
|
||||
|
||||
while (System.currentTimeMillis() - startAt < timeoutMs) {
|
||||
val currentMedia = readLatestMediaFile(cameraIndex)
|
||||
val currentIdentity = currentMedia.mediaIdentity()
|
||||
if (currentIdentity.isNotBlank() && currentIdentity != previousIdentity) {
|
||||
return DjiCommandResult.ok("$successMessage:${currentMedia.mediaDisplayName()}")
|
||||
}
|
||||
|
||||
val storing = isCameraStoringFile(cameraIndex)
|
||||
if (storing) sawStoring = true
|
||||
if (sawStoring && !storing && System.currentTimeMillis() - startAt > 800L) {
|
||||
val afterStoreMedia = readLatestMediaFile(cameraIndex)
|
||||
val afterStoreIdentity = afterStoreMedia.mediaIdentity()
|
||||
if (afterStoreIdentity.isNotBlank() && afterStoreIdentity != previousIdentity) {
|
||||
return DjiCommandResult.ok("$successMessage:${afterStoreMedia.mediaDisplayName()}")
|
||||
}
|
||||
val afterStoreSnapshot = fetchMediaSnapshot(cameraIndex, "after store")
|
||||
return if (afterStoreSnapshot.isNewerThan(previousSnapshot)) {
|
||||
DjiCommandResult.ok("$successMessage:${afterStoreSnapshot?.latestDisplayName ?: "飞机存储"}")
|
||||
} else {
|
||||
DjiCommandResult.ok(successMessage)
|
||||
}
|
||||
}
|
||||
|
||||
delay(300)
|
||||
}
|
||||
|
||||
val finalSnapshot = fetchMediaSnapshot(cameraIndex, "after timeout")
|
||||
return if (finalSnapshot.isNewerThan(previousSnapshot)) {
|
||||
DjiCommandResult.ok("$successMessage:${finalSnapshot?.latestDisplayName ?: "飞机存储"}")
|
||||
} else {
|
||||
DjiCommandResult.failed(timeoutMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLatestMediaFile(index: ComponentIndexType): GeneratedMediaFileInfo? =
|
||||
runCatching {
|
||||
DJICameraKey.KeyNewlyGeneratedMediaFile.create(index).get(GeneratedMediaFileInfo())
|
||||
}.getOrNull()
|
||||
|
||||
private fun readCachedMediaSnapshot(reason: String): MediaSnapshot? {
|
||||
val files = runCatching { MediaDataCenter.getInstance().mediaManager.mediaFileListData.data }.getOrNull().orEmpty()
|
||||
val latest = files.latestMediaFile()
|
||||
val snapshot = MediaSnapshot(
|
||||
count = files.size,
|
||||
latestIdentity = latest.mediaIdentity(),
|
||||
latestDisplayName = latest.mediaDisplayName()
|
||||
)
|
||||
Log.d(
|
||||
CameraMediaLogTag,
|
||||
"media cached snapshot reason=$reason count=${snapshot.count} latest=${snapshot.latestDisplayName}"
|
||||
)
|
||||
return snapshot.takeIf { it.latestIdentity.isNotBlank() || it.count > 0 }
|
||||
}
|
||||
|
||||
private suspend fun fetchMediaSnapshot(index: ComponentIndexType, reason: String): MediaSnapshot? {
|
||||
val manager = MediaDataCenter.getInstance().mediaManager
|
||||
val dataSourceResult = runCatching {
|
||||
val mediaSource = MediaFileListDataSource.Builder().setIndexType(index).build()
|
||||
manager.setMediaFileDataSource(mediaSource)
|
||||
}
|
||||
if (dataSourceResult.isFailure) {
|
||||
Log.w(CameraMediaLogTag, "set media data source failed camera=$index reason=$reason", dataSourceResult.exceptionOrNull())
|
||||
return null
|
||||
}
|
||||
|
||||
val enableResult = enableMediaManager()
|
||||
if (!enableResult.success) {
|
||||
Log.w(CameraMediaLogTag, "enable media manager failed camera=$index reason=$reason message=${enableResult.message}")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val pullResult = pullMediaFileListFromCamera()
|
||||
if (!pullResult.success) {
|
||||
Log.w(CameraMediaLogTag, "pull media list failed camera=$index reason=$reason message=${pullResult.message}")
|
||||
null
|
||||
} else {
|
||||
delay(350)
|
||||
val files = runCatching { manager.mediaFileListData.data }.getOrNull().orEmpty()
|
||||
val latest = files.latestMediaFile()
|
||||
val snapshot = MediaSnapshot(
|
||||
count = files.size,
|
||||
latestIdentity = latest.mediaIdentity(),
|
||||
latestDisplayName = latest.mediaDisplayName()
|
||||
)
|
||||
Log.d(
|
||||
CameraMediaLogTag,
|
||||
"media snapshot camera=$index reason=$reason count=${snapshot.count} latest=${snapshot.latestDisplayName}"
|
||||
)
|
||||
snapshot
|
||||
}
|
||||
} finally {
|
||||
val disableResult = disableMediaManager()
|
||||
if (!disableResult.success) {
|
||||
Log.w(CameraMediaLogTag, "disable media manager failed camera=$index reason=$reason message=${disableResult.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enableMediaManager(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
MediaDataCenter.getInstance().mediaManager.enable(
|
||||
object : dji.v5.common.callback.CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok("媒体文件管理已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun disableMediaManager(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
MediaDataCenter.getInstance().mediaManager.disable(
|
||||
object : dji.v5.common.callback.CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok("媒体文件管理已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun pullMediaFileListFromCamera(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val param = PullMediaFileListParam.Builder()
|
||||
.mediaFileIndex(-1)
|
||||
.count(-1)
|
||||
.build()
|
||||
MediaDataCenter.getInstance().mediaManager.pullMediaFileListFromCamera(
|
||||
param,
|
||||
object : dji.v5.common.callback.CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok("媒体文件列表已刷新"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun isCameraStoringFile(index: ComponentIndexType): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyCameraStoringFile.create(index).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
private suspend fun waitForRecordingState(
|
||||
index: ComponentIndexType,
|
||||
recording: Boolean,
|
||||
timeoutMs: Long
|
||||
): Boolean {
|
||||
val startAt = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startAt < timeoutMs) {
|
||||
if (isRecording(index) == recording) return true
|
||||
delay(250)
|
||||
}
|
||||
return isRecording(index) == recording
|
||||
}
|
||||
|
||||
private suspend fun setCameraFlatMode(index: ComponentIndexType, mode: CameraFlatMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyCameraFlatMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("相机平铺模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setCameraMode(index: ComponentIndexType, mode: CameraMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
CameraKey.KeyCameraMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("相机模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setCameraWorkMode(index: ComponentIndexType, mode: CameraWorkMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyCameraWorkMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("相机模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setShootPhotoMode(index: ComponentIndexType, mode: CameraShootPhotoMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyShootPhotoMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("拍照模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setPhotoPanoramaMode(index: ComponentIndexType, mode: PhotoPanoramaMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyPhotoPanoramaMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("全景模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setBooleanKey(
|
||||
key: DJIKey<Boolean>,
|
||||
value: Boolean,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.set(
|
||||
value,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun runEmptyAction(
|
||||
key: DJIKey.ActionKey<EmptyMsg, EmptyMsg>,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.action(
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun DjiCommandResult.isUnsupportedCameraWorkMode(): Boolean =
|
||||
!success &&
|
||||
message.contains("UNSUPPORTED_COMMAND", ignoreCase = true) &&
|
||||
message.contains("CameraWorkMode", ignoreCase = true)
|
||||
|
||||
private fun DjiCommandResult.isUnsupportedCameraFlatMode(): Boolean =
|
||||
!success &&
|
||||
message.contains("CameraFlatMode", ignoreCase = true) &&
|
||||
(
|
||||
message.contains("UNSUPPORTED_COMMAND", ignoreCase = true) ||
|
||||
message.contains("COMMAND_NOT_SUPPORT_NOW", ignoreCase = true)
|
||||
)
|
||||
|
||||
private fun DjiCommandResult.isUnsupportedCameraPhotoMode(): Boolean =
|
||||
!success &&
|
||||
message.contains("UNSUPPORTED_COMMAND", ignoreCase = true) &&
|
||||
(
|
||||
message.contains("ShootPhotoMode", ignoreCase = true) ||
|
||||
message.contains("PhotoPanoramaMode", ignoreCase = true)
|
||||
)
|
||||
|
||||
private fun DjiCommandResult.isRetryableStartRecordError(): Boolean =
|
||||
!success &&
|
||||
message.contains("StartRecord", ignoreCase = true) &&
|
||||
message.contains("-7", ignoreCase = true)
|
||||
|
||||
private fun GeneratedMediaFileInfo?.mediaIdentity(): String {
|
||||
if (this == null) return ""
|
||||
val identityParts = listOfNotNull(
|
||||
getType()?.name,
|
||||
getIndex()?.toString(),
|
||||
getDir_no()?.toString(),
|
||||
getFile_no()?.toString(),
|
||||
getFileSize()?.toString(),
|
||||
getVideo_time_ms()?.toString(),
|
||||
getCreateTime()?.toString()
|
||||
)
|
||||
return identityParts.joinToString("|")
|
||||
}
|
||||
|
||||
private fun GeneratedMediaFileInfo?.mediaDisplayName(): String {
|
||||
if (this == null) return "飞机存储"
|
||||
val type = getType()?.name ?: "MEDIA"
|
||||
val dirNo = getDir_no()
|
||||
val fileNo = getFile_no()
|
||||
return if (dirNo != null && fileNo != null) {
|
||||
"$type D$dirNo-F$fileNo"
|
||||
} else {
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
private data class MediaSnapshot(
|
||||
val count: Int,
|
||||
val latestIdentity: String,
|
||||
val latestDisplayName: String
|
||||
)
|
||||
|
||||
private fun MediaSnapshot?.isNewerThan(previous: MediaSnapshot?): Boolean {
|
||||
if (this == null) return false
|
||||
if (previous == null) return latestIdentity.isNotBlank()
|
||||
return count > previous.count ||
|
||||
(latestIdentity.isNotBlank() && latestIdentity != previous.latestIdentity)
|
||||
}
|
||||
|
||||
private fun List<MediaFile>.latestMediaFile(): MediaFile? =
|
||||
maxByOrNull { it.fileIndex }
|
||||
|
||||
private fun MediaFile?.mediaIdentity(): String {
|
||||
if (this == null) return ""
|
||||
return "${fileIndex}|${fileName.orEmpty()}"
|
||||
}
|
||||
|
||||
private fun MediaFile?.mediaDisplayName(): String {
|
||||
if (this == null) return "飞机存储"
|
||||
return fileName?.takeIf { it.isNotBlank() } ?: "Media#$fileIndex"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.File
|
||||
|
||||
object MissionFileStore {
|
||||
fun copyToMissionCache(context: Context, uri: Uri): File {
|
||||
val fileName = queryDisplayName(context, uri)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let(::safeFileName)
|
||||
?: "mission-${System.currentTimeMillis()}.kmz"
|
||||
|
||||
val outDir = File(context.cacheDir, "missions").apply { mkdirs() }
|
||||
val outFile = File(outDir, fileName)
|
||||
context.contentResolver.openInputStream(uri).use { input ->
|
||||
requireNotNull(input) { "无法打开航线文件" }
|
||||
outFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return outFile
|
||||
}
|
||||
|
||||
private fun queryDisplayName(context: Context, uri: Uri): String? {
|
||||
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex >= 0 && cursor.moveToFirst()) cursor.getString(nameIndex) else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun safeFileName(value: String): String =
|
||||
value.replace(Regex("[^A-Za-z0-9._-]"), "_")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
data class MissionPlan(
|
||||
val name: String,
|
||||
val waypoints: List<MissionWaypoint>,
|
||||
val globalSpeed: Double = 5.0,
|
||||
val takeoffHeight: Double = 20.0,
|
||||
val globalHeight: Double = 100.0,
|
||||
val finishAction: MissionFinishAction = MissionFinishAction.GoHome,
|
||||
val lostAction: MissionLostAction = MissionLostAction.GoBack
|
||||
)
|
||||
|
||||
data class MissionWaypoint(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Double,
|
||||
val speed: Double? = null,
|
||||
val heading: Double? = null,
|
||||
val gimbalPitch: Double = -30.0,
|
||||
val actions: List<MissionAction> = emptyList()
|
||||
)
|
||||
|
||||
sealed interface MissionAction {
|
||||
data object TakePhoto : MissionAction
|
||||
data object StartRecord : MissionAction
|
||||
data object StopRecord : MissionAction
|
||||
data class Hover(val seconds: Int) : MissionAction
|
||||
data class GimbalPitch(val degrees: Double) : MissionAction
|
||||
data class CameraZoom(val factor: Int) : MissionAction
|
||||
}
|
||||
|
||||
enum class MissionFinishAction {
|
||||
NoAction,
|
||||
GoHome,
|
||||
AutoLand
|
||||
}
|
||||
|
||||
enum class MissionLostAction {
|
||||
GoBack,
|
||||
Landing,
|
||||
Hover
|
||||
}
|
||||
|
||||
data class MissionUploadUpdate(
|
||||
val progress: Double = 0.0,
|
||||
val completed: Boolean = false,
|
||||
val message: String = "",
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.waypoint3.WaypointMissionManager
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class WaypointMissionService {
|
||||
fun uploadKmzFile(
|
||||
kmzPath: String,
|
||||
onUpdate: (MissionUploadUpdate) -> Unit
|
||||
) {
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
onUpdate(MissionUploadUpdate(error = SafetyInterlock.LOCKED_MESSAGE, message = SafetyInterlock.LOCKED_MESSAGE))
|
||||
return
|
||||
}
|
||||
WaypointMissionManager.getInstance().pushKMZFileToAircraft(
|
||||
kmzPath,
|
||||
object : CommonCallbacks.CompletionCallbackWithProgress<Double> {
|
||||
override fun onProgressUpdate(progress: Double) {
|
||||
onUpdate(MissionUploadUpdate(progress = progress, message = "航线上传中"))
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
onUpdate(
|
||||
MissionUploadUpdate(
|
||||
progress = 1.0,
|
||||
completed = true,
|
||||
message = "航线上传完成"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
onUpdate(MissionUploadUpdate(error = error.toString(), message = "航线上传失败"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun uploadKmzFile(kmzPath: String): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
var resumed = false
|
||||
uploadKmzFile(kmzPath) { update ->
|
||||
if (resumed) {
|
||||
return@uploadKmzFile
|
||||
}
|
||||
when {
|
||||
update.completed -> {
|
||||
resumed = true
|
||||
continuation.resume(DjiCommandResult.ok("航线上传完成"))
|
||||
}
|
||||
!update.error.isNullOrBlank() -> {
|
||||
resumed = true
|
||||
continuation.resume(DjiCommandResult.failed(update.error.orEmpty()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startMission(
|
||||
missionId: String,
|
||||
waylineIds: List<Int> = listOf(0)
|
||||
): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().startMission(
|
||||
missionId,
|
||||
waylineIds,
|
||||
callback(continuation, "航线任务已启动")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pauseMission(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().pauseMission(
|
||||
callback(continuation, "航线任务已暂停")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resumeMission(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().resumeMission(
|
||||
callback(continuation, "航线任务已继续")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopMission(missionId: String): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().stopMission(
|
||||
missionId,
|
||||
callback(continuation, "航线任务已停止")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun missionIdFromPath(path: String): String = File(path).nameWithoutExtension
|
||||
|
||||
private fun callback(
|
||||
continuation: kotlinx.coroutines.CancellableContinuation<DjiCommandResult>,
|
||||
successMessage: String
|
||||
): CommonCallbacks.CompletionCallback {
|
||||
return object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
import com.dji.wpmzsdk.common.data.Template
|
||||
import com.dji.wpmzsdk.manager.WPMZManager
|
||||
import dji.sdk.wpmz.value.mission.ActionAircraftHoverParam
|
||||
import dji.sdk.wpmz.value.mission.ActionGimbalRotateParam
|
||||
import dji.sdk.wpmz.value.mission.ActionStartRecordParam
|
||||
import dji.sdk.wpmz.value.mission.ActionStopRecordParam
|
||||
import dji.sdk.wpmz.value.mission.ActionTakePhotoParam
|
||||
import dji.sdk.wpmz.value.mission.ActionZoomParam
|
||||
import dji.sdk.wpmz.value.mission.CameraLensType
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionGroup
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionNodeList
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionTreeNode
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionTrigger
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionTriggerType
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionType
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionsRelationType
|
||||
import dji.sdk.wpmz.value.mission.WaylineAltitudeMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineCoordinateMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineCoordinateParam
|
||||
import dji.sdk.wpmz.value.mission.WaylineDroneInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylineExitOnRCLostAction
|
||||
import dji.sdk.wpmz.value.mission.WaylineExitOnRCLostBehavior
|
||||
import dji.sdk.wpmz.value.mission.WaylineFinishedAction
|
||||
import dji.sdk.wpmz.value.mission.WaylineFlyToWaylineMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineGimbalActuatorRotateMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineLocationCoordinate2D
|
||||
import dji.sdk.wpmz.value.mission.WaylineLocationCoordinate3D
|
||||
import dji.sdk.wpmz.value.mission.WaylineMission
|
||||
import dji.sdk.wpmz.value.mission.WaylineMissionConfig
|
||||
import dji.sdk.wpmz.value.mission.WaylinePayloadInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylinePayloadParam
|
||||
import dji.sdk.wpmz.value.mission.WaylinePositioningType
|
||||
import dji.sdk.wpmz.value.mission.WaylineTemplateWaypointInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypoint
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointPitchMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointTurnMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointYawMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointYawParam
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointYawPathMode
|
||||
import java.io.File
|
||||
|
||||
class WpmlKmzBuilder {
|
||||
fun generateKmz(plan: MissionPlan, outputFile: File): File {
|
||||
require(plan.waypoints.size >= 2) { "航线至少需要 2 个航点" }
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
val waylineMission = WaylineMission().apply {
|
||||
createTime = System.currentTimeMillis().toDouble()
|
||||
updateTime = System.currentTimeMillis().toDouble()
|
||||
}
|
||||
|
||||
WPMZManager.getInstance().generateKMZFile(
|
||||
outputFile.absolutePath,
|
||||
waylineMission,
|
||||
createMissionConfig(plan),
|
||||
createTemplate(plan)
|
||||
)
|
||||
return outputFile
|
||||
}
|
||||
|
||||
private fun createMissionConfig(plan: MissionPlan): WaylineMissionConfig {
|
||||
return WaylineMissionConfig().apply {
|
||||
flyToWaylineMode = WaylineFlyToWaylineMode.SAFELY
|
||||
finishAction = plan.finishAction.toDji()
|
||||
droneInfo = WaylineDroneInfo()
|
||||
securityTakeOffHeight = plan.takeoffHeight
|
||||
isSecurityTakeOffHeightSet = true
|
||||
exitOnRCLostBehavior = WaylineExitOnRCLostBehavior.EXCUTE_RC_LOST_ACTION
|
||||
exitOnRCLostType = plan.lostAction.toDji()
|
||||
globalTransitionalSpeed = plan.globalSpeed
|
||||
payloadInfo = ArrayList<WaylinePayloadInfo>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTemplate(plan: MissionPlan): Template {
|
||||
return Template().apply {
|
||||
waypointInfo = createWaypointInfo(plan)
|
||||
coordinateParam = createCoordinateParam()
|
||||
useGlobalTransitionalSpeed = true
|
||||
autoFlightSpeed = plan.globalSpeed
|
||||
payloadParam = ArrayList<WaylinePayloadParam>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCoordinateParam(): WaylineCoordinateParam {
|
||||
return WaylineCoordinateParam().apply {
|
||||
coordinateMode = WaylineCoordinateMode.WGS84
|
||||
positioningType = WaylinePositioningType.GPS
|
||||
isWaylinePositioningTypeSet = true
|
||||
altitudeMode = WaylineAltitudeMode.RELATIVE_TO_START_POINT
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWaypointInfo(plan: MissionPlan): WaylineTemplateWaypointInfo {
|
||||
val waypoints = plan.waypoints.mapIndexed { index, point ->
|
||||
createWaypoint(index, point, plan)
|
||||
}
|
||||
val poi = plan.waypoints.first()
|
||||
|
||||
return WaylineTemplateWaypointInfo().apply {
|
||||
this.waypoints = waypoints
|
||||
actionGroups = createActionGroups(plan)
|
||||
globalFlightHeight = plan.globalHeight
|
||||
isGlobalFlightHeightSet = true
|
||||
globalTurnMode = WaylineWaypointTurnMode.TO_POINT_AND_STOP_WITH_DISCONTINUITY_CURVATURE
|
||||
useStraightLine = true
|
||||
isTemplateGlobalTurnModeSet = true
|
||||
globalYawParam = WaylineWaypointYawParam().apply {
|
||||
yawMode = WaylineWaypointYawMode.FOLLOW_WAYLINE
|
||||
poiLocation = WaylineLocationCoordinate3D(
|
||||
poi.latitude,
|
||||
poi.longitude,
|
||||
poi.altitude
|
||||
)
|
||||
}
|
||||
isTemplateGlobalYawParamSet = true
|
||||
pitchMode = WaylineWaypointPitchMode.USE_POINT_SETTING
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWaypoint(
|
||||
index: Int,
|
||||
point: MissionWaypoint,
|
||||
plan: MissionPlan
|
||||
): WaylineWaypoint {
|
||||
return WaylineWaypoint().apply {
|
||||
waypointIndex = index
|
||||
location = WaylineLocationCoordinate2D(point.latitude, point.longitude)
|
||||
height = point.altitude
|
||||
ellipsoidHeight = point.altitude
|
||||
speed = point.speed ?: plan.globalSpeed
|
||||
useGlobalTurnParam = true
|
||||
gimbalPitchAngle = point.gimbalPitch
|
||||
yawParam = WaylineWaypointYawParam().apply {
|
||||
enableYawAngle = point.heading != null
|
||||
yawAngle = point.heading ?: 0.0
|
||||
yawMode = if (point.heading == null) {
|
||||
WaylineWaypointYawMode.FOLLOW_WAYLINE
|
||||
} else {
|
||||
WaylineWaypointYawMode.SMOOTH_TRANSITION
|
||||
}
|
||||
yawPathMode = WaylineWaypointYawPathMode.FOLLOW_BAD_ARC
|
||||
poiLocation = WaylineLocationCoordinate3D(
|
||||
point.latitude,
|
||||
point.longitude,
|
||||
point.altitude
|
||||
)
|
||||
}
|
||||
useGlobalYawParam = false
|
||||
isWaylineWaypointYawParamSet = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun createActionGroups(plan: MissionPlan): List<WaylineActionGroup> {
|
||||
val groups = ArrayList<WaylineActionGroup>()
|
||||
plan.waypoints.forEachIndexed { index, point ->
|
||||
val actionInfos = point.actions.mapNotNull { it.toDji() }
|
||||
if (actionInfos.isEmpty()) return@forEachIndexed
|
||||
|
||||
groups.add(WaylineActionGroup().apply {
|
||||
trigger = WaylineActionTrigger().apply {
|
||||
triggerType = WaylineActionTriggerType.REACH_POINT
|
||||
}
|
||||
groupId = groups.size
|
||||
startIndex = index
|
||||
endIndex = index
|
||||
actions = actionInfos
|
||||
nodeLists = createActionNodeLists(actionInfos.size)
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
private fun createActionNodeLists(actionCount: Int): List<WaylineActionNodeList> {
|
||||
val root = WaylineActionNodeList().apply {
|
||||
nodes = listOf(WaylineActionTreeNode().apply {
|
||||
nodeType = WaylineActionsRelationType.SEQUENCE
|
||||
childrenNum = actionCount
|
||||
})
|
||||
}
|
||||
val children = WaylineActionNodeList().apply {
|
||||
nodes = (0 until actionCount).map { index ->
|
||||
WaylineActionTreeNode().apply {
|
||||
nodeType = WaylineActionsRelationType.LEAF
|
||||
actionIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
return listOf(root, children)
|
||||
}
|
||||
|
||||
private fun MissionAction.toDji(): WaylineActionInfo {
|
||||
return when (this) {
|
||||
MissionAction.TakePhoto -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.TAKE_PHOTO
|
||||
takePhotoParam = ActionTakePhotoParam(
|
||||
0,
|
||||
true,
|
||||
ArrayList<CameraLensType>(),
|
||||
"zklh"
|
||||
)
|
||||
}
|
||||
|
||||
MissionAction.StartRecord -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.START_RECORD
|
||||
startRecordParam = ActionStartRecordParam(
|
||||
0,
|
||||
true,
|
||||
ArrayList<CameraLensType>(),
|
||||
"zklh"
|
||||
)
|
||||
}
|
||||
|
||||
MissionAction.StopRecord -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.STOP_RECORD
|
||||
stopRecordParam = ActionStopRecordParam().apply {
|
||||
payloadPositionIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
is MissionAction.Hover -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.HOVER
|
||||
aircraftHoverParam = ActionAircraftHoverParam().apply {
|
||||
hoverTime = seconds.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
is MissionAction.GimbalPitch -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.GIMBAL_ROTATE
|
||||
gimbalRotateParam = ActionGimbalRotateParam().apply {
|
||||
enablePitch = true
|
||||
pitch = degrees
|
||||
rotateMode = WaylineGimbalActuatorRotateMode.ABSOLUTE_ANGLE
|
||||
payloadPositionIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
is MissionAction.CameraZoom -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.ZOOM
|
||||
zoomParam = ActionZoomParam().apply {
|
||||
payloadPositionIndex = 0
|
||||
focalFactor = factor.toDouble()
|
||||
isUseFocalFactor = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MissionFinishAction.toDji(): WaylineFinishedAction {
|
||||
return when (this) {
|
||||
MissionFinishAction.NoAction -> WaylineFinishedAction.NO_ACTION
|
||||
MissionFinishAction.GoHome -> WaylineFinishedAction.GO_HOME
|
||||
MissionFinishAction.AutoLand -> WaylineFinishedAction.AUTO_LAND
|
||||
}
|
||||
}
|
||||
|
||||
private fun MissionLostAction.toDji(): WaylineExitOnRCLostAction {
|
||||
return when (this) {
|
||||
MissionLostAction.GoBack -> WaylineExitOnRCLostAction.GO_BACK
|
||||
MissionLostAction.Landing -> WaylineExitOnRCLostAction.LANDING
|
||||
MissionLostAction.Hover -> WaylineExitOnRCLostAction.HOVER
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.zklh.dronecontroller.core.msdk
|
||||
|
||||
import dji.sdk.keyvalue.key.ComponentType
|
||||
import dji.v5.manager.capability.CapabilityManager
|
||||
|
||||
class CapabilityService {
|
||||
fun isProductSupported(productType: String): Boolean =
|
||||
CapabilityManager.getInstance().isProductSupported(productType)
|
||||
|
||||
fun isKeySupported(productType: String, keyName: String): Boolean =
|
||||
CapabilityManager.getInstance().isKeySupported(productType, keyName)
|
||||
|
||||
fun isComponentKeySupported(
|
||||
productType: String,
|
||||
componentType: ComponentType,
|
||||
keyName: String
|
||||
): Boolean =
|
||||
CapabilityManager.getInstance().isKeySupported(
|
||||
productType,
|
||||
"",
|
||||
componentType,
|
||||
keyName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.zklh.dronecontroller.core.msdk
|
||||
|
||||
import dji.v5.common.error.IDJIError
|
||||
|
||||
data class DjiCommandResult(
|
||||
val success: Boolean,
|
||||
val message: String
|
||||
) {
|
||||
companion object {
|
||||
fun ok(message: String): DjiCommandResult = DjiCommandResult(true, message)
|
||||
|
||||
fun failed(error: IDJIError): DjiCommandResult =
|
||||
DjiCommandResult(false, error.toString())
|
||||
|
||||
fun failed(message: String): DjiCommandResult =
|
||||
DjiCommandResult(false, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.zklh.dronecontroller.core.msdk
|
||||
|
||||
import android.app.Application
|
||||
import dji.sampleV5.aircraft.BuildConfig
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlightMode
|
||||
import dji.sdk.keyvalue.value.remotecontroller.RcMutilDeviceState
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.SDKManager
|
||||
import dji.v5.network.DJINetworkManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class DroneSdkState(
|
||||
val initEvent: String = "Idle",
|
||||
val initProgress: Int = 0,
|
||||
val isInitialized: Boolean = false,
|
||||
val isRegistered: Boolean = false,
|
||||
val registerError: String? = null,
|
||||
val productConnected: Boolean = false,
|
||||
val productLinkConnected: Boolean = false,
|
||||
val aircraftConnectionDetail: String = "未检测",
|
||||
val aircraftSerialNumber: String = "",
|
||||
val remoteControllerSerialNumber: String = "",
|
||||
val productId: Int? = null,
|
||||
val productChangedId: Int? = null,
|
||||
val networkAvailable: Boolean = false,
|
||||
val databaseProgress: String = ""
|
||||
)
|
||||
|
||||
object DroneSdkManager {
|
||||
private const val API_KEY_PLACEHOLDER = "Please add your DJI MSDK app key here."
|
||||
|
||||
private val _state = MutableStateFlow(DroneSdkState())
|
||||
val state: StateFlow<DroneSdkState> = _state.asStateFlow()
|
||||
|
||||
@Volatile
|
||||
private var initStarted = false
|
||||
@Volatile
|
||||
private var productConnectionListenerStarted = false
|
||||
|
||||
private val productConnectionListenerOwner = Any()
|
||||
|
||||
private data class AircraftConnectionProbe(
|
||||
val connected: Boolean,
|
||||
val detail: String
|
||||
)
|
||||
|
||||
fun reportNativeHelperError(error: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "MSDK Helper 初始化失败",
|
||||
registerError = error.message ?: error.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onOfficialInitProcess(eventName: String, totalProcess: Int, initialized: Boolean) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = eventName,
|
||||
initProgress = totalProcess,
|
||||
isInitialized = initialized
|
||||
)
|
||||
}
|
||||
if (initialized) {
|
||||
syncOfficialRegistrationState()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOfficialRegisterState(success: Boolean, errorMessage: String?) {
|
||||
if (success) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_SUCCESS",
|
||||
isInitialized = true,
|
||||
isRegistered = true,
|
||||
registerError = null
|
||||
)
|
||||
}
|
||||
startProductConnectionMonitor()
|
||||
refreshProductConnection()
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_FAILURE",
|
||||
isRegistered = false,
|
||||
registerError = errorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onOfficialProductConnection(connected: Boolean, productId: Int) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = if (connected) "PRODUCT_CONNECT" else "PRODUCT_DISCONNECT",
|
||||
productLinkConnected = connected,
|
||||
productId = productId
|
||||
)
|
||||
}
|
||||
refreshProductConnection()
|
||||
}
|
||||
|
||||
fun onOfficialDatabaseProgress(current: Long, total: Long) {
|
||||
_state.update { it.copy(databaseProgress = "$current/$total") }
|
||||
}
|
||||
|
||||
fun initMobileSdk(application: Application) {
|
||||
if (initStarted) return
|
||||
initStarted = true
|
||||
|
||||
if (BuildConfig.AIRCRAFT_API_KEY.isBlank() ||
|
||||
BuildConfig.AIRCRAFT_API_KEY == API_KEY_PLACEHOLDER
|
||||
) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "DJI App Key 未配置",
|
||||
registerError = "请先在 gradle.properties 填写 AIRCRAFT_API_KEY"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "等待官方 MSDK 注册",
|
||||
isInitialized = true
|
||||
)
|
||||
}
|
||||
syncOfficialRegistrationState()
|
||||
|
||||
DJINetworkManager.getInstance().addNetworkStatusListener { isAvailable ->
|
||||
_state.update { it.copy(networkAvailable = isAvailable) }
|
||||
if (
|
||||
isAvailable &&
|
||||
_state.value.isInitialized &&
|
||||
!SDKManager.getInstance().isRegistered
|
||||
) {
|
||||
SDKManager.getInstance().registerApp()
|
||||
} else {
|
||||
syncOfficialRegistrationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retryRegister() {
|
||||
syncOfficialRegistrationState()
|
||||
if (!_state.value.isInitialized) return
|
||||
runCatching {
|
||||
SDKManager.getInstance().registerApp()
|
||||
refreshProductConnection()
|
||||
}.onFailure { error ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_RETRY_FAILED",
|
||||
registerError = error.message ?: error.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshProductConnection() {
|
||||
syncOfficialRegistrationState()
|
||||
if (!_state.value.isRegistered) {
|
||||
updateConnectionState(
|
||||
productLinkConnected = false,
|
||||
aircraftConnected = false,
|
||||
detail = "SDK 未注册"
|
||||
)
|
||||
return
|
||||
}
|
||||
val productLinkConnected = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(ProductKey.KeyConnection),
|
||||
false
|
||||
)
|
||||
}.getOrElse { error ->
|
||||
_state.update {
|
||||
it.copy(registerError = error.message ?: error.toString())
|
||||
}
|
||||
false
|
||||
}
|
||||
val aircraftProbe = readAircraftConnectionProbe()
|
||||
updateConnectionState(
|
||||
productLinkConnected = productLinkConnected == true,
|
||||
aircraftConnected = aircraftProbe.connected,
|
||||
detail = aircraftProbe.detail
|
||||
)
|
||||
}
|
||||
|
||||
private fun startProductConnectionMonitor() {
|
||||
if (productConnectionListenerStarted) return
|
||||
productConnectionListenerStarted = true
|
||||
runCatching {
|
||||
KeyManager.getInstance().listen(
|
||||
KeyTools.createKey(ProductKey.KeyConnection),
|
||||
productConnectionListenerOwner,
|
||||
true,
|
||||
object : CommonCallbacks.KeyListener<Boolean> {
|
||||
override fun onValueChange(oldValue: Boolean?, newValue: Boolean?) {
|
||||
val aircraftProbe = readAircraftConnectionProbe()
|
||||
updateConnectionState(
|
||||
productLinkConnected = newValue == true,
|
||||
aircraftConnected = aircraftProbe.connected,
|
||||
detail = aircraftProbe.detail
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
refreshProductConnection()
|
||||
}.onFailure { error ->
|
||||
productConnectionListenerStarted = false
|
||||
_state.update {
|
||||
it.copy(registerError = error.message ?: error.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncOfficialRegistrationState() {
|
||||
if (SDKManager.getInstance().isRegistered) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_SUCCESS",
|
||||
isInitialized = true,
|
||||
isRegistered = true,
|
||||
registerError = null
|
||||
)
|
||||
}
|
||||
startProductConnectionMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAircraftConnectionProbe(): AircraftConnectionProbe {
|
||||
val rcAircraftState = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(RemoteControllerKey.KeyRcMultiDeviceAircraftState)
|
||||
)
|
||||
}.getOrNull()
|
||||
|
||||
if (rcAircraftState != null) {
|
||||
val mainStates = listOf(
|
||||
rcAircraftState.sdrConnectState,
|
||||
rcAircraftState.lteConnectState
|
||||
)
|
||||
val aircraftStates = rcAircraftState.aircraftStates.orEmpty().flatMap {
|
||||
listOf(it.sdrConnectState, it.lteConnectState)
|
||||
}
|
||||
val connectedByRc = (mainStates + aircraftStates).any(::isAircraftLinkActive)
|
||||
val detail = "RC链路 sdr=${rcAircraftState.sdrConnectState ?: "UNKNOWN"} lte=${rcAircraftState.lteConnectState ?: "UNKNOWN"}"
|
||||
if (!connectedByRc) {
|
||||
return AircraftConnectionProbe(false, detail)
|
||||
}
|
||||
return AircraftConnectionProbe(true, detail)
|
||||
}
|
||||
|
||||
val signals = mutableListOf<String>()
|
||||
val serialNumber = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeySerialNumber),
|
||||
""
|
||||
)
|
||||
}.getOrDefault("")
|
||||
if (serialNumber.isNotBlank()) signals += "飞控SN"
|
||||
|
||||
val flightMode = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeyFlightMode),
|
||||
FlightMode.UNKNOWN
|
||||
)
|
||||
}.getOrDefault(FlightMode.UNKNOWN)
|
||||
if (flightMode != FlightMode.UNKNOWN) signals += "模式:$flightMode"
|
||||
|
||||
val batteryPercent = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeyBatteryPowerPercent),
|
||||
-1
|
||||
)
|
||||
}.getOrDefault(-1)
|
||||
if (batteryPercent in 0..100) signals += "电量:$batteryPercent%"
|
||||
|
||||
val connected = signals.size >= 2
|
||||
return AircraftConnectionProbe(
|
||||
connected = connected,
|
||||
detail = if (signals.isEmpty()) "未读到飞控实时信号" else signals.joinToString(" / ")
|
||||
)
|
||||
}
|
||||
|
||||
private fun isAircraftLinkActive(state: RcMutilDeviceState?): Boolean =
|
||||
state == RcMutilDeviceState.CONNECTED || state == RcMutilDeviceState.USING
|
||||
|
||||
private fun updateConnectionState(
|
||||
productLinkConnected: Boolean,
|
||||
aircraftConnected: Boolean,
|
||||
detail: String
|
||||
) {
|
||||
_state.update { current ->
|
||||
val event = when {
|
||||
current.productConnected == aircraftConnected &&
|
||||
current.productLinkConnected == productLinkConnected -> current.initEvent
|
||||
aircraftConnected -> "AIRCRAFT_CONNECTION_ON"
|
||||
productLinkConnected -> "PRODUCT_LINK_ON_AIRCRAFT_OFF"
|
||||
else -> "AIRCRAFT_CONNECTION_OFF"
|
||||
}
|
||||
current.copy(
|
||||
initEvent = event,
|
||||
productConnected = aircraftConnected,
|
||||
productLinkConnected = productLinkConnected,
|
||||
aircraftConnectionDetail = detail,
|
||||
aircraftSerialNumber = if (aircraftConnected) readAircraftSerialNumber() else "",
|
||||
remoteControllerSerialNumber = if (productLinkConnected) readRemoteControllerSerialNumber() else ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAircraftSerialNumber(): String =
|
||||
runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeySerialNumber),
|
||||
""
|
||||
)
|
||||
}.getOrDefault("").orEmpty()
|
||||
|
||||
private fun readRemoteControllerSerialNumber(): String =
|
||||
runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(RemoteControllerKey.KeySerialNumber),
|
||||
""
|
||||
)
|
||||
}.getOrDefault("").orEmpty()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.zklh.dronecontroller.core.safety
|
||||
|
||||
object SafetyInterlock {
|
||||
const val FLIGHT_COMMANDS_ENABLED: Boolean = true
|
||||
const val LOCKED_MESSAGE: String = "飞控指令未启用"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.zklh.dronecontroller.core.simulator
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.simulator.InitializationSettings
|
||||
import dji.v5.manager.aircraft.simulator.SimulatorManager
|
||||
import dji.v5.manager.aircraft.simulator.SimulatorState
|
||||
import dji.v5.manager.aircraft.simulator.SimulatorStatusListener
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
data class SimulatorStatus(
|
||||
val enabled: Boolean = false,
|
||||
val motorsOn: Boolean = false,
|
||||
val flying: Boolean = false,
|
||||
val positionX: Float = 0f,
|
||||
val positionY: Float = 0f,
|
||||
val positionZ: Float = 0f,
|
||||
val yaw: Float = 0f,
|
||||
val message: String = "模拟器未开启"
|
||||
)
|
||||
|
||||
class SimulatorService {
|
||||
private val _status = MutableStateFlow(SimulatorStatus())
|
||||
val status: StateFlow<SimulatorStatus> = _status.asStateFlow()
|
||||
|
||||
private val listener = object : SimulatorStatusListener {
|
||||
override fun onUpdate(state: SimulatorState) {
|
||||
_status.update {
|
||||
it.copy(
|
||||
enabled = SimulatorManager.getInstance().isSimulatorEnabled,
|
||||
motorsOn = state.areMotorsOn(),
|
||||
flying = state.isFlying,
|
||||
positionX = state.positionX,
|
||||
positionY = state.positionY,
|
||||
positionZ = state.positionZ,
|
||||
yaw = state.yaw,
|
||||
message = "模拟器=${if (SimulatorManager.getInstance().isSimulatorEnabled) "开启" else "关闭"} 飞行=${if (state.isFlying) "是" else "否"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
SimulatorManager.getInstance().addSimulatorStateListener(listener)
|
||||
_status.update {
|
||||
it.copy(
|
||||
enabled = SimulatorManager.getInstance().isSimulatorEnabled,
|
||||
message = if (SimulatorManager.getInstance().isSimulatorEnabled) "模拟器已开启" else "模拟器未开启"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun enable(
|
||||
latitude: Double = DEFAULT_LATITUDE,
|
||||
longitude: Double = DEFAULT_LONGITUDE,
|
||||
satelliteCount: Int = 12
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val settings = InitializationSettings.createInstance(
|
||||
LocationCoordinate2D(latitude, longitude),
|
||||
satelliteCount
|
||||
)
|
||||
SimulatorManager.getInstance().enableSimulator(
|
||||
settings,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update {
|
||||
it.copy(enabled = true, message = "模拟器已开启")
|
||||
}
|
||||
continuation.resume(DjiCommandResult.ok("模拟器已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun disable(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
SimulatorManager.getInstance().disableSimulator(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update {
|
||||
it.copy(enabled = false, flying = false, motorsOn = false, message = "模拟器已关闭")
|
||||
}
|
||||
continuation.resume(DjiCommandResult.ok("模拟器已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
SimulatorManager.getInstance().removeSimulatorStateListener(listener)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DEFAULT_LATITUDE = 31.2304
|
||||
const val DEFAULT_LONGITUDE = 121.4737
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
package com.zklh.dronecontroller.core.telemetry
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import dji.rtk.CoordinateSystem
|
||||
import dji.sdk.keyvalue.key.BatteryKey
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.DJICameraKey
|
||||
import dji.sdk.keyvalue.key.DJIGimbalKey
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.sdk.keyvalue.key.RtkMobileStationKey
|
||||
import dji.sdk.keyvalue.value.camera.CameraWorkMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||
import dji.sdk.keyvalue.value.camera.LaserMeasureInformation
|
||||
import dji.sdk.keyvalue.value.camera.PhotoState
|
||||
import dji.sdk.keyvalue.value.camera.RecordingState
|
||||
import dji.sdk.keyvalue.value.common.Attitude
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.sdk.keyvalue.value.common.Velocity3D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FCFlightMode
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlightMode
|
||||
import dji.sdk.keyvalue.value.flightcontroller.GPSSignalLevel
|
||||
import dji.sdk.keyvalue.value.flightcontroller.HeightAboveSeaLevelMsg
|
||||
import dji.sdk.keyvalue.value.flightcontroller.RemoteControllerFlightMode
|
||||
import dji.sdk.keyvalue.value.flightcontroller.WindDirection
|
||||
import dji.sdk.keyvalue.value.remotecontroller.BatteryInfo
|
||||
import dji.sdk.keyvalue.value.remotecontroller.RcGPSInfo
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKServiceState
|
||||
import dji.sdk.keyvalue.value.rtkmobilestation.RTKLocation
|
||||
import dji.sdk.keyvalue.value.rtkmobilestation.RTKSatelliteInfo
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.aircraft.rtk.RTKCenter
|
||||
import dji.v5.manager.aircraft.rtk.RTKLocationInfo
|
||||
import dji.v5.manager.aircraft.rtk.RTKLocationInfoListener
|
||||
import dji.v5.manager.aircraft.rtk.RTKSystemState
|
||||
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
|
||||
import dji.v5.manager.aircraft.rtk.network.INetworkServiceInfoListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
private const val TelemetryLogTag = "ZklhTelemetry"
|
||||
private const val RTK_START_RETRY_INTERVAL_MS = 20_000L
|
||||
|
||||
data class TelemetrySnapshot(
|
||||
val latitude: Double = 0.0,
|
||||
val longitude: Double = 0.0,
|
||||
val locationValid: Boolean = false,
|
||||
val homeLatitude: Double = 0.0,
|
||||
val homeLongitude: Double = 0.0,
|
||||
val homeLocationValid: Boolean = false,
|
||||
val rcLatitude: Double = 0.0,
|
||||
val rcLongitude: Double = 0.0,
|
||||
val rcAltitude: Double = 0.0,
|
||||
val rcLocationValid: Boolean = false,
|
||||
val rcGpsSatelliteCount: Int = 0,
|
||||
val rcGpsAccuracy: Double = 0.0,
|
||||
val rtkLatitude: Double = 0.0,
|
||||
val rtkLongitude: Double = 0.0,
|
||||
val rtkAltitude: Double = 0.0,
|
||||
val rtkLocationValid: Boolean = false,
|
||||
val rtkEnabled: Boolean = false,
|
||||
val rtkHealthy: Boolean = false,
|
||||
val rtkWorking: Boolean = false,
|
||||
val rtkBeingUsed: Boolean = false,
|
||||
val rtkFusionDataUsable: Boolean = false,
|
||||
val rtkSatelliteCount: Int = 0,
|
||||
val rtkPositioningSolution: String = "",
|
||||
val altitude: Double = 0.0,
|
||||
val elevation: Double = 0.0,
|
||||
val heading: Double = 0.0,
|
||||
val attitudePitch: Double = 0.0,
|
||||
val attitudeRoll: Double = 0.0,
|
||||
val speedX: Double = 0.0,
|
||||
val speedY: Double = 0.0,
|
||||
val speedZ: Double = 0.0,
|
||||
val flightMode: String = "UNKNOWN",
|
||||
val flightModeString: String = "",
|
||||
val fcFlightMode: String = "UNKNOWN",
|
||||
val isFlying: Boolean = false,
|
||||
val motorsOn: Boolean = false,
|
||||
val landingMode: Boolean = false,
|
||||
val landingConfirmationNeeded: Boolean = false,
|
||||
val simulatorStarted: Boolean = false,
|
||||
val firmwareVersion: String = "",
|
||||
val gear: Int = 1,
|
||||
val gpsSatelliteCount: Int = 0,
|
||||
val gpsSignalLevel: Int = 0,
|
||||
val gpsValid: Boolean = false,
|
||||
val heightLimit: Int = 500,
|
||||
val distanceLimit: Int = 5000,
|
||||
val distanceLimitEnabled: Boolean = true,
|
||||
val totalFlightDistance: Double = 0.0,
|
||||
val totalFlightTime: Double = 0.0,
|
||||
val totalFlightSorties: Int = 0,
|
||||
val windDirection: Int = 0,
|
||||
val windSpeed: Int = 0,
|
||||
val remainingFlightTime: Int = 0,
|
||||
val batteryPercentNeededToLand: Int = 7,
|
||||
val batteryPercentNeededToLandKnown: Boolean = false,
|
||||
val batteryPercentNeededToGoHome: Int = 14,
|
||||
val batteryPercentNeededToGoHomeKnown: Boolean = false,
|
||||
val batteryPercent: Int = -1,
|
||||
val batteryVoltageMv: Int = 0,
|
||||
val batteryVoltageKnown: Boolean = false,
|
||||
val batteryFirmwareVersion: String = "",
|
||||
val batterySerialNumber: String = "",
|
||||
val batteryTemperature: Double = 0.0,
|
||||
val batteryTemperatureKnown: Boolean = false,
|
||||
val batteryLoopTimes: Int = 0,
|
||||
val batteryLoopTimesKnown: Boolean = false,
|
||||
val batteryHighVoltageStorageSeconds: Long = 0L,
|
||||
val batteryHighVoltageStorageKnown: Boolean = false,
|
||||
val remainingFlightTimeKnown: Boolean = false,
|
||||
val rcBatteryPercent: Int = -1,
|
||||
val cameraWorkMode: String = "",
|
||||
val photoState: Int = 0,
|
||||
val recordingState: Int = 0,
|
||||
val recordingTime: Int = 0,
|
||||
val remainPhotoNum: Int = 0,
|
||||
val remainRecordDuration: Int = 0,
|
||||
val storageTotal: Int = 0,
|
||||
val storageRemain: Int = 0,
|
||||
val zoomFactor: Double = 1.0,
|
||||
val irZoomFactor: Double = 1.0,
|
||||
val gimbalPitch: Double = 0.0,
|
||||
val gimbalRoll: Double = 0.0,
|
||||
val gimbalYaw: Double = 0.0,
|
||||
val laserDistance: Double = 0.0,
|
||||
val laserTargetLatitude: Double = 0.0,
|
||||
val laserTargetLongitude: Double = 0.0,
|
||||
val laserTargetAltitude: Double = 0.0,
|
||||
val thermalMaxTemperature: Double = 0.0,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
class TelemetryRepository(
|
||||
context: Context? = null
|
||||
) {
|
||||
private val listenHolder = Any()
|
||||
private val appContext = context?.applicationContext
|
||||
private val locationManager = appContext?.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
|
||||
private val androidLocationListener = LocationListener { location -> updateRcLocationFromAndroid(location) }
|
||||
private val rtkCenter = RTKCenter.getInstance()
|
||||
private val rtkLocationInfoListener = RTKLocationInfoListener { info -> updateRtkLocationInfo(info) }
|
||||
private val rtkSystemStateListener = RTKSystemStateListener { state -> updateRtkSystemState(state) }
|
||||
private val rtkNetworkServiceInfoListener = object : INetworkServiceInfoListener {
|
||||
override fun onServiceStateUpdate(state: RTKServiceState?) {
|
||||
Log.d(TelemetryLogTag, "RTKNetworkServiceState=$state")
|
||||
when (state) {
|
||||
RTKServiceState.TRANSMITTING,
|
||||
RTKServiceState.RTCM_NORMAL,
|
||||
-> {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = true
|
||||
}
|
||||
RTKServiceState.DISABLED,
|
||||
RTKServiceState.UNKNOWN,
|
||||
RTKServiceState.RTCM_AUTH_FAILED,
|
||||
RTKServiceState.NETWORK_NOT_REACHABLE,
|
||||
RTKServiceState.SERVER_NOT_REACHABLE,
|
||||
RTKServiceState.ACCOUNT_EXPIRED,
|
||||
-> {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = false
|
||||
ensureRtkServiceStarted(lastRtkSystemState)
|
||||
}
|
||||
else -> {
|
||||
// READY/CONNECTING/PROCESSING are transitional states; keep the current flags.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onErrorCodeUpdate(error: IDJIError?) {
|
||||
Log.w(TelemetryLogTag, "RTKNetworkServiceError=$error")
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = false
|
||||
ensureRtkServiceStarted(lastRtkSystemState)
|
||||
}
|
||||
}
|
||||
private val _state = MutableStateFlow(TelemetrySnapshot())
|
||||
val state: StateFlow<TelemetrySnapshot> = _state.asStateFlow()
|
||||
|
||||
private var started = false
|
||||
private var rtkStartInProgress = false
|
||||
private var rtkServiceStarted = false
|
||||
private var rtkEnableInProgress = false
|
||||
private var rtkSourceSetInProgress = false
|
||||
private var lastRtkStartAtMs = 0L
|
||||
private var lastRtkSystemState: RTKSystemState? = null
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
Log.d(TelemetryLogTag, "start telemetry listeners")
|
||||
listenSafely("KeyAircraftLocation", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation)) { location: LocationCoordinate2D? ->
|
||||
location ?: return@listenSafely
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyAircraftLocation lat=${location.latitude} lon=${location.longitude}"
|
||||
)
|
||||
updateAircraftLocation(location.latitude, location.longitude)
|
||||
}
|
||||
listenSafely("KeyAircraftLocation3D", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D)) { location: LocationCoordinate3D? ->
|
||||
location ?: return@listenSafely
|
||||
val altitude = location.altitude ?: _state.value.altitude
|
||||
val latitude = location.latitude ?: 0.0
|
||||
val longitude = location.longitude ?: 0.0
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyAircraftLocation3D lat=$latitude lon=$longitude altitude=$altitude valid=$valid"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
locationValid = valid,
|
||||
altitude = altitude,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyAltitude", KeyTools.createKey(FlightControllerKey.KeyAltitude)) { altitude: Double? ->
|
||||
_state.update { it.copy(altitude = altitude ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyHeightAboveSeaLevel", KeyTools.createKey(FlightControllerKey.KeyHeightAboveSeaLevel)) { height: HeightAboveSeaLevelMsg? ->
|
||||
_state.update { it.copy(elevation = height?.height ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyCompassHeading", KeyTools.createKey(FlightControllerKey.KeyCompassHeading)) { heading: Double? ->
|
||||
_state.update { it.copy(heading = heading ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftAttitude", KeyTools.createKey(FlightControllerKey.KeyAircraftAttitude)) { attitude: Attitude? ->
|
||||
attitude ?: return@listenSafely
|
||||
_state.update {
|
||||
it.copy(
|
||||
attitudePitch = attitude.pitch ?: 0.0,
|
||||
attitudeRoll = attitude.roll ?: 0.0,
|
||||
heading = attitude.yaw ?: it.heading,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyAircraftVelocity", KeyTools.createKey(FlightControllerKey.KeyAircraftVelocity)) { velocity: Velocity3D? ->
|
||||
velocity ?: return@listenSafely
|
||||
_state.update {
|
||||
it.copy(speedX = velocity.x, speedY = velocity.y, speedZ = velocity.z, error = null)
|
||||
}
|
||||
}
|
||||
listenSafely("ProductKeyFirmwareVersion", KeyTools.createKey(ProductKey.KeyFirmwareVersion)) { firmware: String? ->
|
||||
val value = firmware.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update { it.copy(firmwareVersion = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyFirmwareVersion", KeyTools.createKey(FlightControllerKey.KeyFirmwareVersion)) { firmware: String? ->
|
||||
val value = firmware.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update {
|
||||
if (it.firmwareVersion.isBlank()) {
|
||||
it.copy(firmwareVersion = value, error = null)
|
||||
} else {
|
||||
it.copy(error = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRemoteControllerFlightMode", KeyTools.createKey(FlightControllerKey.KeyRemoteControllerFlightMode)) { mode: RemoteControllerFlightMode? ->
|
||||
_state.update { it.copy(gear = mode?.value() ?: it.gear, error = null) }
|
||||
}
|
||||
listenSafely("KeyGPSSatelliteCount", KeyTools.createKey(FlightControllerKey.KeyGPSSatelliteCount)) { count: Int? ->
|
||||
val value = count ?: 0
|
||||
Log.d(TelemetryLogTag, "KeyGPSSatelliteCount=$value")
|
||||
_state.update { it.copy(gpsSatelliteCount = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyGPSSignalLevel", KeyTools.createKey(FlightControllerKey.KeyGPSSignalLevel)) { level: GPSSignalLevel? ->
|
||||
val value = level?.value() ?: 0
|
||||
Log.d(TelemetryLogTag, "KeyGPSSignalLevel=$level value=$value")
|
||||
_state.update { it.copy(gpsSignalLevel = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyGPSIsValid", KeyTools.createKey(FlightControllerKey.KeyGPSIsValid)) { valid: Boolean? ->
|
||||
Log.d(TelemetryLogTag, "KeyGPSIsValid=$valid")
|
||||
_state.update { it.copy(gpsValid = valid == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyHomeLocation", KeyTools.createKey(FlightControllerKey.KeyHomeLocation)) { location: LocationCoordinate2D? ->
|
||||
location ?: return@listenSafely
|
||||
val latitude = location.latitude
|
||||
val longitude = location.longitude
|
||||
_state.update {
|
||||
if (isValidCoordinate(latitude, longitude)) {
|
||||
it.copy(
|
||||
homeLatitude = latitude,
|
||||
homeLongitude = longitude,
|
||||
homeLocationValid = true,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
it.copy(homeLocationValid = false, error = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyFlightMode", KeyTools.createKey(FlightControllerKey.KeyFlightMode)) { mode: FlightMode? ->
|
||||
_state.update { it.copy(flightMode = mode?.name ?: "UNKNOWN", error = null) }
|
||||
}
|
||||
listenSafely("KeyFlightModeString", KeyTools.createKey(FlightControllerKey.KeyFlightModeString)) { modeString: String? ->
|
||||
val value = modeString.orEmpty()
|
||||
Log.d(TelemetryLogTag, "KeyFlightModeString=$value")
|
||||
_state.update { it.copy(flightModeString = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyFCFlightMode", KeyTools.createKey(FlightControllerKey.KeyFCFlightMode)) { mode: FCFlightMode? ->
|
||||
val value = mode?.name ?: "UNKNOWN"
|
||||
Log.d(TelemetryLogTag, "KeyFCFlightMode=$value")
|
||||
_state.update { it.copy(fcFlightMode = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsFlying", KeyTools.createKey(FlightControllerKey.KeyIsFlying)) { flying: Boolean? ->
|
||||
_state.update { it.copy(isFlying = flying == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyAreMotorsOn", KeyTools.createKey(FlightControllerKey.KeyAreMotorsOn)) { motorsOn: Boolean? ->
|
||||
_state.update { it.copy(motorsOn = motorsOn == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsInLandingMode", KeyTools.createKey(FlightControllerKey.KeyIsInLandingMode)) { landingMode: Boolean? ->
|
||||
val value = landingMode == true
|
||||
Log.d(TelemetryLogTag, "KeyIsInLandingMode=$value")
|
||||
_state.update { it.copy(landingMode = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsLandingConfirmationNeeded", KeyTools.createKey(FlightControllerKey.KeyIsLandingConfirmationNeeded)) { needed: Boolean? ->
|
||||
val value = needed == true
|
||||
Log.d(TelemetryLogTag, "KeyIsLandingConfirmationNeeded=$value")
|
||||
_state.update { it.copy(landingConfirmationNeeded = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsSimulatorStarted", KeyTools.createKey(FlightControllerKey.KeyIsSimulatorStarted)) { started: Boolean? ->
|
||||
_state.update { it.copy(simulatorStarted = started == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyHeightLimit", KeyTools.createKey(FlightControllerKey.KeyHeightLimit)) { limit: Int? ->
|
||||
_state.update { it.copy(heightLimit = limit ?: it.heightLimit, error = null) }
|
||||
}
|
||||
listenSafely("KeyDistanceLimit", KeyTools.createKey(FlightControllerKey.KeyDistanceLimit)) { limit: Int? ->
|
||||
_state.update { it.copy(distanceLimit = limit ?: it.distanceLimit, error = null) }
|
||||
}
|
||||
listenSafely("KeyDistanceLimitEnabled", KeyTools.createKey(FlightControllerKey.KeyDistanceLimitEnabled)) { enabled: Boolean? ->
|
||||
_state.update { it.copy(distanceLimitEnabled = enabled == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftTotalFlightDistance", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightDistance)) { distance: Double? ->
|
||||
_state.update { it.copy(totalFlightDistance = distance ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftTotalFlightDuration", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightDuration)) { duration: Double? ->
|
||||
_state.update { it.copy(totalFlightTime = duration ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftTotalFlightTimes", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightTimes)) { times: Int? ->
|
||||
_state.update { it.copy(totalFlightSorties = times ?: 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyRemainingFlightTime", KeyTools.createKey(FlightControllerKey.KeyRemainingFlightTime)) { seconds: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
remainingFlightTime = seconds ?: 0,
|
||||
remainingFlightTimeKnown = seconds != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyBatteryPercentNeededToLand", KeyTools.createKey(FlightControllerKey.KeyBatteryPercentNeededToLand)) { percent: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryPercentNeededToLand = percent ?: it.batteryPercentNeededToLand,
|
||||
batteryPercentNeededToLandKnown = percent != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyBatteryPercentNeededToGoHome", KeyTools.createKey(FlightControllerKey.KeyBatteryPercentNeededToGoHome)) { percent: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryPercentNeededToGoHome = percent ?: it.batteryPercentNeededToGoHome,
|
||||
batteryPercentNeededToGoHomeKnown = percent != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyWindDirection", KeyTools.createKey(FlightControllerKey.KeyWindDirection)) { direction: WindDirection? ->
|
||||
_state.update { it.copy(windDirection = direction?.value() ?: 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyWindSpeed", KeyTools.createKey(FlightControllerKey.KeyWindSpeed)) { speed: Int? ->
|
||||
_state.update { it.copy(windSpeed = speed ?: 0, error = null) }
|
||||
}
|
||||
listenSafely(
|
||||
"KeyChargeRemainingInPercent",
|
||||
KeyTools.createKey(BatteryKey.KeyChargeRemainingInPercent, ComponentIndexType.AGGREGATION)
|
||||
) { percent: Int? ->
|
||||
val value = percent ?: return@listenSafely
|
||||
if (value in 0..100) {
|
||||
_state.update { it.copy(batteryPercent = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyBatteryPowerPercent", KeyTools.createKey(FlightControllerKey.KeyBatteryPowerPercent)) { percent: Int? ->
|
||||
val value = percent ?: return@listenSafely
|
||||
if (value in 0..100 && _state.value.batteryPercent !in 0..100) {
|
||||
_state.update { it.copy(batteryPercent = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyVoltageMain", KeyTools.createKey(BatteryKey.KeyVoltage, ComponentIndexType.LEFT_OR_MAIN)) { voltage: Int? ->
|
||||
val value = voltage ?: return@listenSafely
|
||||
if (value > 0) {
|
||||
_state.update { it.copy(batteryVoltageMv = value, batteryVoltageKnown = true, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyVoltageRight", KeyTools.createKey(BatteryKey.KeyVoltage, ComponentIndexType.RIGHT)) { voltage: Int? ->
|
||||
val value = voltage ?: return@listenSafely
|
||||
if (value > 0 && _state.value.batteryVoltageMv <= 0) {
|
||||
_state.update { it.copy(batteryVoltageMv = value, batteryVoltageKnown = true, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatteryFirmwareVersion",
|
||||
KeyTools.createKey(BatteryKey.KeyFirmwareVersion, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { version: String? ->
|
||||
val value = version.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update { it.copy(batteryFirmwareVersion = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatterySerialNumber",
|
||||
KeyTools.createKey(BatteryKey.KeySerialNumber, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { serialNumber: String? ->
|
||||
val value = serialNumber.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update { it.copy(batterySerialNumber = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatteryTemperature",
|
||||
KeyTools.createKey(BatteryKey.KeyBatteryTemperature, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { temperature: Double? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryTemperature = temperature ?: 0.0,
|
||||
batteryTemperatureKnown = temperature != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyNumberOfDischarges",
|
||||
KeyTools.createKey(BatteryKey.KeyNumberOfDischarges, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { times: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryLoopTimes = times ?: 0,
|
||||
batteryLoopTimesKnown = times != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatteryHighVoltageStorageTime",
|
||||
KeyTools.createKey(BatteryKey.KeyBatteryHighVoltageStorageTime, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { seconds: Long? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryHighVoltageStorageSeconds = seconds ?: 0L,
|
||||
batteryHighVoltageStorageKnown = seconds != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenRemoteControllerTelemetry()
|
||||
startAndroidLocationFallback()
|
||||
listenCameraTelemetry()
|
||||
listenGimbalTelemetry()
|
||||
listenRtkTelemetry()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
runCatching { KeyManager.getInstance().cancelListen(listenHolder) }
|
||||
runCatching { locationManager?.removeUpdates(androidLocationListener) }
|
||||
runCatching { rtkCenter.removeRTKLocationInfoListener(rtkLocationInfoListener) }
|
||||
runCatching { rtkCenter.removeRTKSystemStateListener(rtkSystemStateListener) }
|
||||
runCatching { rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||
runCatching { rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||
runCatching { rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||
started = false
|
||||
}
|
||||
|
||||
private fun <T> listenSafely(name: String, key: DJIKey<T>, onUpdate: (T?) -> Unit) {
|
||||
runCatching {
|
||||
KeyManager.getInstance().listen(key, listenHolder, true) { _, newValue ->
|
||||
onUpdate(newValue)
|
||||
}
|
||||
Log.d(TelemetryLogTag, "registered $name")
|
||||
}.onFailure { error ->
|
||||
_state.update { it.copy(error = error.message ?: error.toString()) }
|
||||
Log.e(TelemetryLogTag, "register $name failed", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenCameraTelemetry(cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
|
||||
listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? ->
|
||||
_state.update { it.copy(cameraWorkMode = mode?.name.orEmpty(), error = null) }
|
||||
}
|
||||
listenSafely("KeyCameraMode", KeyTools.createKey(CameraKey.KeyCameraMode, cameraIndex)) { mode: CameraMode? ->
|
||||
_state.update { it.copy(cameraWorkMode = mode?.name.orEmpty(), error = null) }
|
||||
}
|
||||
listenSafely("KeyShootPhotoState", KeyTools.createKey(DJICameraKey.KeyShootPhotoState, cameraIndex)) { shooting: Boolean? ->
|
||||
if (shooting == true) _state.update { it.copy(photoState = 1, error = null) }
|
||||
}
|
||||
listenSafely("KeyPhotoState", KeyTools.createKey(DJICameraKey.KeyPhotoState, cameraIndex)) { state: PhotoState? ->
|
||||
_state.update { it.copy(photoState = if (state == PhotoState.IN_SHOOTING) 1 else 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsRecording", KeyTools.createKey(DJICameraKey.KeyIsRecording, cameraIndex)) { recording: Boolean? ->
|
||||
_state.update { it.copy(recordingState = if (recording == true) 1 else 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyRecordingState", KeyTools.createKey(DJICameraKey.KeyRecordingState, cameraIndex)) { state: RecordingState? ->
|
||||
_state.update { it.copy(recordingState = if (state == RecordingState.RECORDING) 1 else 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyRecordingTime", KeyTools.createKey(DJICameraKey.KeyRecordingTime, cameraIndex)) { seconds: Int? ->
|
||||
_state.update { it.copy(recordingTime = seconds ?: 0, error = null) }
|
||||
}
|
||||
listenSafely("KeySDCardAvailablePhotoCount", KeyTools.createKey(DJICameraKey.KeySDCardAvailablePhotoCount, cameraIndex)) { count: Int? ->
|
||||
_state.update { it.copy(remainPhotoNum = count ?: it.remainPhotoNum, error = null) }
|
||||
}
|
||||
listenSafely("KeySDCardAvailableVideoDuration", KeyTools.createKey(DJICameraKey.KeySDCardAvailableVideoDuration, cameraIndex)) { seconds: Int? ->
|
||||
_state.update { it.copy(remainRecordDuration = seconds ?: it.remainRecordDuration, error = null) }
|
||||
}
|
||||
listenSafely("KeySDCardTotalSpace", KeyTools.createKey(DJICameraKey.KeySDCardTotalSpace, cameraIndex)) { total: Int? ->
|
||||
updateStorage(total = total, remain = null)
|
||||
}
|
||||
listenSafely("KeySDCardRemainSpace", KeyTools.createKey(DJICameraKey.KeySDCardRemainSpace, cameraIndex)) { remain: Int? ->
|
||||
updateStorage(total = null, remain = remain)
|
||||
}
|
||||
listenSafely("KeyInternalStorageTotalSpace", KeyTools.createKey(DJICameraKey.KeyInternalStorageTotalSpace, cameraIndex)) { total: Int? ->
|
||||
if ((total ?: 0) > 0) updateStorage(total = total, remain = null)
|
||||
}
|
||||
listenSafely("KeyInternalStorageRemainSpace", KeyTools.createKey(DJICameraKey.KeyInternalStorageRemainSpace, cameraIndex)) { remain: Int? ->
|
||||
if ((remain ?: 0) > 0) updateStorage(total = null, remain = remain)
|
||||
}
|
||||
listenSafely("KeyCameraZoomRatios", KeyTools.createKey(CameraKey.KeyCameraZoomRatios, cameraIndex)) { zoom: Double? ->
|
||||
_state.update { it.copy(zoomFactor = zoom?.takeIf { value -> value > 0.0 } ?: it.zoomFactor, error = null) }
|
||||
}
|
||||
listenSafely("KeyThermalZoomRatios", KeyTools.createKey(CameraKey.KeyThermalZoomRatios, cameraIndex)) { zoom: Double? ->
|
||||
_state.update { it.copy(irZoomFactor = zoom?.takeIf { value -> value > 0.0 } ?: it.irZoomFactor, error = null) }
|
||||
}
|
||||
listenSafely("KeyLaserMeasureInformation", KeyTools.createKey(DJICameraKey.KeyLaserMeasureInformation, cameraIndex)) { info: LaserMeasureInformation? ->
|
||||
val location = info?.location3D
|
||||
_state.update {
|
||||
it.copy(
|
||||
laserDistance = info?.distance ?: 0.0,
|
||||
laserTargetLatitude = location?.latitude ?: 0.0,
|
||||
laserTargetLongitude = location?.longitude ?: 0.0,
|
||||
laserTargetAltitude = location?.altitude ?: 0.0,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyThermalGlobalMaxTemperature", KeyTools.createKey(DJICameraKey.KeyThermalGlobalMaxTemperature, cameraIndex)) { temperature: Double? ->
|
||||
_state.update { it.copy(thermalMaxTemperature = temperature ?: 0.0, error = null) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenRemoteControllerTelemetry() {
|
||||
listenSafely("KeyRcGPSInfo", KeyTools.createKey(RemoteControllerKey.KeyRcGPSInfo)) { info: RcGPSInfo? ->
|
||||
val location = info?.location
|
||||
val latitude = location?.latitude ?: 0.0
|
||||
val longitude = location?.longitude ?: 0.0
|
||||
val valid = info?.isValid == true && isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyRcGPSInfo valid=${info?.isValid} lat=$latitude lon=$longitude satellites=${info?.satelliteCount} accuracy=${info?.accuracy}"
|
||||
)
|
||||
_state.update {
|
||||
if (valid) {
|
||||
it.copy(
|
||||
rcLatitude = latitude,
|
||||
rcLongitude = longitude,
|
||||
rcLocationValid = true,
|
||||
rcGpsSatelliteCount = info?.satelliteCount ?: 0,
|
||||
rcGpsAccuracy = info?.accuracy ?: 0.0,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
rcGpsSatelliteCount = info?.satelliteCount ?: 0,
|
||||
rcGpsAccuracy = info?.accuracy ?: it.rcGpsAccuracy,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRcBatteryInfo", KeyTools.createKey(RemoteControllerKey.KeyBatteryInfo)) { info: BatteryInfo? ->
|
||||
val percent = info?.batteryPercent ?: return@listenSafely
|
||||
if (percent in 0..100) {
|
||||
_state.update { it.copy(rcBatteryPercent = percent, error = null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenGimbalTelemetry(gimbalIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
|
||||
listenSafely("KeyGimbalAttitude", KeyTools.createKey(DJIGimbalKey.KeyGimbalAttitude, gimbalIndex)) { attitude: Attitude? ->
|
||||
attitude ?: return@listenSafely
|
||||
_state.update {
|
||||
it.copy(
|
||||
gimbalPitch = attitude.pitch ?: 0.0,
|
||||
gimbalRoll = attitude.roll ?: 0.0,
|
||||
gimbalYaw = attitude.yaw ?: 0.0,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenRtkTelemetry() {
|
||||
runCatching {
|
||||
rtkCenter.addRTKLocationInfoListener(rtkLocationInfoListener)
|
||||
Log.d(TelemetryLogTag, "registered RTKLocationInfoListener")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RTKLocationInfoListener failed: ${error.message}")
|
||||
}
|
||||
runCatching {
|
||||
rtkCenter.addRTKSystemStateListener(rtkSystemStateListener)
|
||||
Log.d(TelemetryLogTag, "registered RTKSystemStateListener")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RTKSystemStateListener failed: ${error.message}")
|
||||
}
|
||||
runCatching {
|
||||
rtkCenter.qxrtkManager.addNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener)
|
||||
rtkCenter.customRTKManager.addNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener)
|
||||
rtkCenter.cmccrtkManager.addNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener)
|
||||
Log.d(TelemetryLogTag, "registered RTK network service listeners")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RTK network service listeners failed: ${error.message}")
|
||||
}
|
||||
listenRtkMobileStationKeys()
|
||||
}
|
||||
|
||||
private fun listenRtkMobileStationKeys() {
|
||||
listenSafely("KeyRTKEnable", KeyTools.createKey(RtkMobileStationKey.KeyRTKEnable)) { enabled: Boolean? ->
|
||||
_state.update { it.copy(rtkEnabled = enabled == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyRTKEnable=$enabled")
|
||||
}
|
||||
listenSafely("KeyIsRTKWorking", KeyTools.createKey(RtkMobileStationKey.KeyIsRTKWorking)) { working: Boolean? ->
|
||||
_state.update { it.copy(rtkWorking = working == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyIsRTKWorking=$working")
|
||||
}
|
||||
listenSafely("KeyIsRTKBeingUsed", KeyTools.createKey(RtkMobileStationKey.KeyIsRTKBeingUsed)) { beingUsed: Boolean? ->
|
||||
_state.update { it.copy(rtkBeingUsed = beingUsed == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyIsRTKBeingUsed=$beingUsed")
|
||||
}
|
||||
listenSafely("KeyIsRTKFusionDataUsable", KeyTools.createKey(RtkMobileStationKey.KeyIsRTKFusionDataUsable)) { usable: Boolean? ->
|
||||
_state.update { it.copy(rtkFusionDataUsable = usable == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyIsRTKFusionDataUsable=$usable")
|
||||
}
|
||||
listenSafely("KeyRTKSatelliteCount", KeyTools.createKey(RtkMobileStationKey.KeyRTKSatelliteCount)) { count: Int? ->
|
||||
val value = count ?: 0
|
||||
_state.update { it.copy(rtkSatelliteCount = value, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyRTKSatelliteCount=$value")
|
||||
}
|
||||
listenSafely(
|
||||
"KeyRTKFusionMobileStationLocation",
|
||||
KeyTools.createKey(RtkMobileStationKey.KeyRTKFusionMobileStationLocation)
|
||||
) { location: LocationCoordinate3D? ->
|
||||
val latitude = location?.latitude ?: 0.0
|
||||
val longitude = location?.longitude ?: 0.0
|
||||
val altitude = location?.altitude ?: 0.0
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(TelemetryLogTag, "KeyRTKFusionMobileStationLocation lat=$latitude lon=$longitude alt=$altitude valid=$valid")
|
||||
_state.update {
|
||||
it.copy(
|
||||
rtkLatitude = latitude,
|
||||
rtkLongitude = longitude,
|
||||
rtkAltitude = altitude,
|
||||
rtkLocationValid = valid,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRTKLocation", KeyTools.createKey(RtkMobileStationKey.KeyRTKLocation)) { location: RTKLocation? ->
|
||||
val mobileLocation = location?.mobileStationLocation
|
||||
val solution = location?.positioningSolution?.name.orEmpty()
|
||||
val latitude = mobileLocation?.latitude ?: 0.0
|
||||
val longitude = mobileLocation?.longitude ?: 0.0
|
||||
val altitude = mobileLocation?.altitude ?: 0.0
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyRTKLocation mobile=$latitude,$longitude,$altitude solution=$solution valid=$valid"
|
||||
)
|
||||
_state.update {
|
||||
if (valid) {
|
||||
it.copy(
|
||||
rtkLatitude = latitude,
|
||||
rtkLongitude = longitude,
|
||||
rtkAltitude = altitude,
|
||||
rtkLocationValid = true,
|
||||
rtkPositioningSolution = solution,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
it.copy(rtkPositioningSolution = solution, error = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRTKSatelliteInfo", KeyTools.createKey(RtkMobileStationKey.KeyRTKSatelliteInfo)) { info: RTKSatelliteInfo? ->
|
||||
val count = info?.mobileStationReceiver1Info?.sumOf { it.count } ?: 0
|
||||
_state.update { it.copy(rtkSatelliteCount = count, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyRTKSatelliteInfo receiver1Count=$count")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAircraftLocation(latitude: Double?, longitude: Double?) {
|
||||
val lat = latitude ?: 0.0
|
||||
val lon = longitude ?: 0.0
|
||||
val valid = isValidCoordinate(lat, lon)
|
||||
_state.update {
|
||||
it.copy(latitude = lat, longitude = lon, locationValid = valid, error = null)
|
||||
}
|
||||
Log.d(TelemetryLogTag, "KeyAircraftLocation raw lat=$lat lon=$lon valid=$valid")
|
||||
}
|
||||
|
||||
private fun updateRtkLocationInfo(info: RTKLocationInfo?) {
|
||||
val realLocation = info?.real3DLocation
|
||||
val mobileLocation = info?.rtkLocation?.mobileStationLocation
|
||||
val latitude = realLocation?.latitude ?: mobileLocation?.latitude ?: 0.0
|
||||
val longitude = realLocation?.longitude ?: mobileLocation?.longitude ?: 0.0
|
||||
val altitude = realLocation?.altitude ?: mobileLocation?.altitude ?: 0.0
|
||||
val solution = info?.rtkLocation?.positioningSolution?.name.orEmpty()
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"RTKLocationInfo real3D=${realLocation?.latitude},${realLocation?.longitude},${realLocation?.altitude} " +
|
||||
"mobile=${mobileLocation?.latitude},${mobileLocation?.longitude},${mobileLocation?.altitude} solution=$solution valid=$valid"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
rtkLatitude = latitude,
|
||||
rtkLongitude = longitude,
|
||||
rtkAltitude = altitude,
|
||||
rtkLocationValid = valid,
|
||||
rtkPositioningSolution = solution,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRtkSystemState(state: RTKSystemState?) {
|
||||
lastRtkSystemState = state
|
||||
val enabled = state?.isRTKEnabled == true
|
||||
val healthy = state?.rtkHealthy == true
|
||||
val satelliteCount = if (enabled) {
|
||||
state?.satelliteInfo?.mobileStationReceiver1Info?.map { it.count }?.sum() ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"RTKSystemState enabled=$enabled healthy=$healthy satellites=$satelliteCount source=${state?.rtkReferenceStationSource} error=${state?.error}"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
rtkEnabled = enabled,
|
||||
rtkHealthy = healthy,
|
||||
rtkSatelliteCount = satelliteCount,
|
||||
rtkLocationValid = if (enabled) it.rtkLocationValid else false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
ensureRtkServiceStarted(state)
|
||||
}
|
||||
|
||||
private fun ensureRtkServiceStarted(state: RTKSystemState?) {
|
||||
if (state == null) return
|
||||
if (state.rtkHealthy) {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = true
|
||||
return
|
||||
}
|
||||
if (!state.isRTKEnabled) {
|
||||
enableRtkModuleIfPossible()
|
||||
return
|
||||
}
|
||||
val source = state.rtkReferenceStationSource ?: RTKReferenceStationSource.UNKNOWN
|
||||
if (!source.isNetworkRtkSource()) {
|
||||
setPreferredNetworkRtkSource(source)
|
||||
return
|
||||
}
|
||||
if (rtkServiceStarted) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (rtkStartInProgress || now - lastRtkStartAtMs < RTK_START_RETRY_INTERVAL_MS) return
|
||||
startNetworkRtkService(source)
|
||||
}
|
||||
|
||||
private fun enableRtkModuleIfPossible() {
|
||||
if (rtkEnableInProgress || _state.value.motorsOn) return
|
||||
rtkEnableInProgress = true
|
||||
Log.d(TelemetryLogTag, "enable RTK module")
|
||||
rtkCenter.setAircraftRTKModuleEnabled(true, object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
rtkEnableInProgress = false
|
||||
Log.d(TelemetryLogTag, "enable RTK module success")
|
||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
rtkEnableInProgress = false
|
||||
Log.w(TelemetryLogTag, "enable RTK module failed: $error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setPreferredNetworkRtkSource(currentSource: RTKReferenceStationSource) {
|
||||
if (rtkSourceSetInProgress || rtkStartInProgress) return
|
||||
rtkSourceSetInProgress = true
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"set RTK reference station source current=$currentSource target=$DefaultNetworkRtkSource"
|
||||
)
|
||||
rtkCenter.setRTKReferenceStationSource(
|
||||
DefaultNetworkRtkSource,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
rtkSourceSetInProgress = false
|
||||
rtkServiceStarted = false
|
||||
Log.d(TelemetryLogTag, "set RTK reference station source success target=$DefaultNetworkRtkSource")
|
||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||
startNetworkRtkService(DefaultNetworkRtkSource)
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
rtkSourceSetInProgress = false
|
||||
Log.w(TelemetryLogTag, "set RTK reference station source failed target=$DefaultNetworkRtkSource error=$error")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun startNetworkRtkService(source: RTKReferenceStationSource) {
|
||||
rtkStartInProgress = true
|
||||
rtkServiceStarted = false
|
||||
lastRtkStartAtMs = System.currentTimeMillis()
|
||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||
Log.d(TelemetryLogTag, "start RTK network service source=$source coordinate=$DefaultNetworkRtkCoordinateSystem")
|
||||
when (source) {
|
||||
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
|
||||
rtkCenter.qxrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("QX"))
|
||||
}
|
||||
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
|
||||
rtkCenter.cmccrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("NTRIP"))
|
||||
}
|
||||
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> {
|
||||
rtkCenter.customRTKManager.startNetworkRTKService(rtkStartCallback("CUSTOM"))
|
||||
}
|
||||
else -> {
|
||||
rtkStartInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rtkStartCallback(label: String): CommonCallbacks.CompletionCallback =
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = true
|
||||
Log.d(TelemetryLogTag, "start $label RTK service success")
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = false
|
||||
Log.w(TelemetryLogTag, "start $label RTK service failed: $error")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startAndroidLocationFallback() {
|
||||
val manager = locationManager ?: return
|
||||
val context = appContext ?: return
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log.w(TelemetryLogTag, "Android location permission not granted, RC system location disabled")
|
||||
return
|
||||
}
|
||||
|
||||
(
|
||||
listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, LocationManager.PASSIVE_PROVIDER) +
|
||||
manager.allProviders
|
||||
)
|
||||
.distinct()
|
||||
.filter { provider ->
|
||||
provider == LocationManager.PASSIVE_PROVIDER ||
|
||||
runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)
|
||||
}
|
||||
.forEach { provider ->
|
||||
runCatching {
|
||||
manager.getLastKnownLocation(provider)?.let(::updateRcLocationFromAndroid)
|
||||
manager.requestLocationUpdates(
|
||||
provider,
|
||||
1_000L,
|
||||
0.5f,
|
||||
androidLocationListener,
|
||||
Looper.getMainLooper()
|
||||
)
|
||||
Log.d(TelemetryLogTag, "registered RC Android location provider=$provider")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RC Android location provider=$provider failed: ${error.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRcLocationFromAndroid(location: Location) {
|
||||
val latitude = location.latitude
|
||||
val longitude = location.longitude
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"RcAndroidLocation provider=${location.provider} lat=$latitude lon=$longitude accuracy=${location.accuracy} valid=$valid"
|
||||
)
|
||||
if (!valid) return
|
||||
_state.update {
|
||||
if (it.rcLocationValid && it.rcGpsSatelliteCount > 0) {
|
||||
it
|
||||
} else {
|
||||
it.copy(
|
||||
rcLatitude = latitude,
|
||||
rcLongitude = longitude,
|
||||
rcAltitude = if (location.hasAltitude()) location.altitude else it.rcAltitude,
|
||||
rcLocationValid = true,
|
||||
rcGpsAccuracy = location.accuracy.toDouble(),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStorage(total: Int?, remain: Int?) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
storageTotal = total ?: it.storageTotal,
|
||||
storageRemain = remain ?: it.storageRemain,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun isValidCoordinate(latitude: Double?, longitude: Double?): Boolean {
|
||||
val lat = latitude ?: return false
|
||||
val lon = longitude ?: return false
|
||||
if (lat.isNaN() || lon.isNaN()) return false
|
||||
if (lat == 0.0 && lon == 0.0) return false
|
||||
return lat in -90.0..90.0 && lon in -180.0..180.0
|
||||
}
|
||||
|
||||
private val DefaultNetworkRtkSource = RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
|
||||
private val DefaultNetworkRtkCoordinateSystem = CoordinateSystem.CGCS2000
|
||||
|
||||
private fun RTKReferenceStationSource.isNetworkRtkSource(): Boolean =
|
||||
this == RTKReferenceStationSource.QX_NETWORK_SERVICE ||
|
||||
this == RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE ||
|
||||
this == RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.zklh.dronecontroller.core.video
|
||||
|
||||
import android.view.Surface
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class VideoPreviewState(
|
||||
val cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN,
|
||||
val availableCameras: List<ComponentIndexType> = emptyList(),
|
||||
val surfaceBound: Boolean = false,
|
||||
val message: String = "等待图传画面"
|
||||
)
|
||||
|
||||
class DjiVideoPreviewService {
|
||||
private val _state = MutableStateFlow(VideoPreviewState())
|
||||
val state: StateFlow<VideoPreviewState> = _state.asStateFlow()
|
||||
|
||||
private var surface: Surface? = null
|
||||
private var surfaceWidth: Int = 0
|
||||
private var surfaceHeight: Int = 0
|
||||
private var listenerRegistered = false
|
||||
|
||||
private val cameraListener = object : ICameraStreamManager.AvailableCameraUpdatedListener {
|
||||
override fun onAvailableCameraUpdated(cameras: List<ComponentIndexType>) {
|
||||
val selected = selectCamera(cameras)
|
||||
_state.update {
|
||||
it.copy(
|
||||
cameraIndex = selected,
|
||||
availableCameras = cameras,
|
||||
message = if (cameras.isEmpty()) "未发现可用相机码流" else "相机码流可用:$selected"
|
||||
)
|
||||
}
|
||||
bindSurface()
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(enableMap: Map<ComponentIndexType, Boolean>) {
|
||||
_state.update {
|
||||
it.copy(message = "相机码流状态:$enableMap")
|
||||
}
|
||||
bindSurface()
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
runCatching {
|
||||
cameraStreamManager().setKeepAliveDecoding(true)
|
||||
if (!listenerRegistered) {
|
||||
cameraStreamManager().addAvailableCameraUpdatedListener(cameraListener)
|
||||
listenerRegistered = true
|
||||
}
|
||||
bindSurface()
|
||||
}.onFailure { error ->
|
||||
_state.update { it.copy(surfaceBound = false, message = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun attachSurface(surface: Surface, width: Int, height: Int) {
|
||||
this.surface = surface
|
||||
surfaceWidth = width
|
||||
surfaceHeight = height
|
||||
bindSurface()
|
||||
}
|
||||
|
||||
fun detachSurface(surface: Surface) {
|
||||
runCatching { cameraStreamManager().removeCameraStreamSurface(surface) }
|
||||
if (this.surface == surface) {
|
||||
this.surface = null
|
||||
_state.update { it.copy(surfaceBound = false, message = "图传 Surface 已释放") }
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
surface?.let { currentSurface ->
|
||||
runCatching { cameraStreamManager().removeCameraStreamSurface(currentSurface) }
|
||||
}
|
||||
if (listenerRegistered) {
|
||||
runCatching { cameraStreamManager().removeAvailableCameraUpdatedListener(cameraListener) }
|
||||
listenerRegistered = false
|
||||
}
|
||||
surface = null
|
||||
_state.update { it.copy(surfaceBound = false, message = "图传预览已停止") }
|
||||
}
|
||||
|
||||
private fun bindSurface() {
|
||||
val currentSurface = surface ?: return
|
||||
if (!currentSurface.isValid || surfaceWidth <= 0 || surfaceHeight <= 0) return
|
||||
|
||||
runCatching {
|
||||
val manager = cameraStreamManager()
|
||||
val cameraIndex = _state.value.cameraIndex
|
||||
manager.enableStream(cameraIndex, true)
|
||||
manager.putCameraStreamSurface(
|
||||
cameraIndex,
|
||||
currentSurface,
|
||||
surfaceWidth,
|
||||
surfaceHeight,
|
||||
ICameraStreamManager.ScaleType.CENTER_CROP
|
||||
)
|
||||
_state.update { it.copy(surfaceBound = true, message = "正在显示 $cameraIndex 图传") }
|
||||
}.onFailure { error ->
|
||||
_state.update { it.copy(surfaceBound = false, message = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun cameraStreamManager(): ICameraStreamManager =
|
||||
MediaDataCenter.getInstance().cameraStreamManager
|
||||
|
||||
private fun selectCamera(cameras: List<ComponentIndexType>): ComponentIndexType =
|
||||
when {
|
||||
cameras.contains(ComponentIndexType.LEFT_OR_MAIN) -> ComponentIndexType.LEFT_OR_MAIN
|
||||
cameras.isNotEmpty() -> cameras.first()
|
||||
else -> ComponentIndexType.LEFT_OR_MAIN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.zklh.dronecontroller.core.virtualstick
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlightControlAuthorityChangeReason
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.virtualstick.VirtualStickManager
|
||||
import dji.v5.manager.aircraft.virtualstick.VirtualStickState
|
||||
import dji.v5.manager.aircraft.virtualstick.VirtualStickStateListener
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
data class StickPosition(
|
||||
val leftHorizontal: Int = 0,
|
||||
val leftVertical: Int = 0,
|
||||
val rightHorizontal: Int = 0,
|
||||
val rightVertical: Int = 0
|
||||
)
|
||||
|
||||
data class VirtualStickStatus(
|
||||
val rawState: String = "",
|
||||
val changeReason: String = "",
|
||||
val enabled: Boolean = false,
|
||||
val advancedEnabled: Boolean = false,
|
||||
val authorityOwner: String = ""
|
||||
)
|
||||
|
||||
class VirtualStickService {
|
||||
private val _status = MutableStateFlow(VirtualStickStatus())
|
||||
val status: StateFlow<VirtualStickStatus> = _status.asStateFlow()
|
||||
|
||||
private val listener = object : VirtualStickStateListener {
|
||||
override fun onVirtualStickStateUpdate(stickState: VirtualStickState) {
|
||||
val enabled = stickState.isVirtualStickEnable
|
||||
val advancedEnabled = stickState.isVirtualStickAdvancedModeEnabled
|
||||
val owner = stickState.currentFlightControlAuthorityOwner?.toString().orEmpty()
|
||||
_status.update {
|
||||
it.copy(
|
||||
rawState = "虚拟摇杆=${if (enabled) "开启" else "关闭"} 高级模式=${if (advancedEnabled) "开启" else "关闭"} 控制权=$owner",
|
||||
enabled = enabled,
|
||||
advancedEnabled = advancedEnabled,
|
||||
authorityOwner = owner
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeReasonUpdate(reason: FlightControlAuthorityChangeReason) {
|
||||
_status.update { it.copy(changeReason = reason.name) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching { VirtualStickManager.getInstance().init() }
|
||||
VirtualStickManager.getInstance().setVirtualStickStateListener(listener)
|
||||
}
|
||||
|
||||
suspend fun enable(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
VirtualStickManager.getInstance().enableVirtualStick(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update { it.copy(enabled = true, rawState = "虚拟摇杆=开启") }
|
||||
continuation.resume(DjiCommandResult.ok("虚拟摇杆已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disable(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
VirtualStickManager.getInstance().disableVirtualStick(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update { it.copy(enabled = false, rawState = "虚拟摇杆=关闭") }
|
||||
continuation.resume(DjiCommandResult.ok("虚拟摇杆已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun setSpeedLevel(speedLevel: Double) {
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) return
|
||||
VirtualStickManager.getInstance().speedLevel = speedLevel
|
||||
}
|
||||
|
||||
fun sendStickPosition(position: StickPosition) {
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) return
|
||||
VirtualStickManager.getInstance().leftStick.horizontalPosition = position.leftHorizontal
|
||||
VirtualStickManager.getInstance().leftStick.verticalPosition = position.leftVertical
|
||||
VirtualStickManager.getInstance().rightStick.horizontalPosition = position.rightHorizontal
|
||||
VirtualStickManager.getInstance().rightStick.verticalPosition = position.rightVertical
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
VirtualStickManager.getInstance().clearAllVirtualStickStateListener()
|
||||
runCatching { VirtualStickManager.getInstance().destroy() }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
28
sample/src/main/java/com/zklh/dronecontroller/ui/Theme.kt
Normal file
28
sample/src/main/java/com/zklh/dronecontroller/ui/Theme.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.zklh.dronecontroller.ui
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val ControllerColors = lightColorScheme(
|
||||
primary = Color(0xFF1D4ED8),
|
||||
secondary = Color(0xFF047857),
|
||||
tertiary = Color(0xFFB45309),
|
||||
error = Color(0xFFB91C1C),
|
||||
background = Color(0xFFF7F8FA),
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ZklhDroneTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colorScheme = ControllerColors,
|
||||
typography = MaterialTheme.typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dji.sampleV5.aircraft.views.MSDKInfoFragment
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/12/16
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class AircraftMSDKInfoFragment : MSDKInfoFragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = inflater.inflate(R.layout.frag_aircraft_main_title, container, false)
|
||||
initView(v)
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import androidx.fragment.app.commit
|
||||
import dji.sampleV5.aircraft.data.AircraftFragmentPageInfoFactory
|
||||
import dji.sampleV5.aircraft.data.CommonFragmentPageInfoFactory
|
||||
import dji.sampleV5.aircraft.data.FragmentPageItemList
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/3/9
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class AircraftTestingToolsActivity : TestingToolsActivity() {
|
||||
|
||||
override fun loadPages() {
|
||||
msdkCommonOperateVm.apply {
|
||||
val itemList = LinkedHashSet<FragmentPageItemList>().also {
|
||||
it.add(CommonFragmentPageInfoFactory().createPageInfo())
|
||||
it.add(AircraftFragmentPageInfoFactory().createPageInfo())
|
||||
}
|
||||
loaderItem(itemList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadTitleView() {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.main_info_fragment_container, AircraftMSDKInfoFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/3/2
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIAircraftApplication : DJIApplication() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
com.cySdkyc.clx.Helper.install(this)
|
||||
loadDjiJniLibraries()
|
||||
}
|
||||
|
||||
private fun loadDjiJniLibraries() {
|
||||
runCatching {
|
||||
System.loadLibrary("djisdk_jni")
|
||||
}.onFailure { error ->
|
||||
Log.w("DJIAircraftApplication", "load djisdk_jni failed", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import dji.v5.common.utils.GeoidManager
|
||||
import dji.v5.ux.core.communication.DefaultGlobalPreferences
|
||||
import dji.v5.ux.core.communication.GlobalPreferencesManager
|
||||
import dji.v5.ux.core.util.UxSharedPreferencesUtil
|
||||
import dji.v5.ux.sample.showcase.defaultlayout.DefaultLayoutActivity
|
||||
import dji.v5.ux.sample.showcase.widgetlist.WidgetsActivity
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/2/14
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIAircraftMainActivity : DJIMainActivity() {
|
||||
|
||||
override fun prepareUxActivity() {
|
||||
UxSharedPreferencesUtil.initialize(this)
|
||||
GlobalPreferencesManager.initialize(DefaultGlobalPreferences(this))
|
||||
GeoidManager.getInstance().init(this)
|
||||
|
||||
enableDefaultLayout(DefaultLayoutActivity::class.java)
|
||||
enableWidgetList(WidgetsActivity::class.java)
|
||||
}
|
||||
|
||||
override fun prepareTestingToolsActivity() {
|
||||
enableTestingTools(AircraftTestingToolsActivity::class.java)
|
||||
}
|
||||
}
|
||||
26
sample/src/main/java/dji/sampleV5/aircraft/DJIApplication.kt
Normal file
26
sample/src/main/java/dji/sampleV5/aircraft/DJIApplication.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.app.Application
|
||||
import dji.sampleV5.aircraft.models.MSDKManagerVM
|
||||
import dji.sampleV5.aircraft.models.globalViewModels
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/3/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
open class DJIApplication : Application() {
|
||||
|
||||
private val msdkManagerVM: MSDKManagerVM by globalViewModels()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Ensure initialization is called first
|
||||
msdkManagerVM.initMobileSDK(this)
|
||||
}
|
||||
|
||||
}
|
||||
233
sample/src/main/java/dji/sampleV5/aircraft/DJIMainActivity.kt
Normal file
233
sample/src/main/java/dji/sampleV5/aircraft/DJIMainActivity.kt
Normal file
@@ -0,0 +1,233 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dji.sampleV5.aircraft.databinding.ActivityMainBinding
|
||||
import dji.sampleV5.aircraft.models.BaseMainActivityVm
|
||||
import dji.sampleV5.aircraft.models.MSDKInfoVm
|
||||
import dji.sampleV5.aircraft.models.MSDKManagerVM
|
||||
import dji.sampleV5.aircraft.models.globalViewModels
|
||||
import dji.sampleV5.aircraft.util.Helper
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import dji.v5.utils.common.PermissionUtil
|
||||
import dji.v5.utils.common.StringUtils
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/2/10
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
abstract class DJIMainActivity : AppCompatActivity() {
|
||||
|
||||
val tag: String = LogUtils.getTag(this)
|
||||
private val permissionArray = arrayListOf(
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.KILL_BACKGROUND_PROCESSES,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
)
|
||||
|
||||
init {
|
||||
permissionArray.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// add(Manifest.permission.READ_MEDIA_IMAGES)
|
||||
// add(Manifest.permission.READ_MEDIA_VIDEO)
|
||||
// add(Manifest.permission.READ_MEDIA_AUDIO)
|
||||
} else {
|
||||
add(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val baseMainActivityVm: BaseMainActivityVm by viewModels()
|
||||
private val msdkInfoVm: MSDKInfoVm by viewModels()
|
||||
private val msdkManagerVM: MSDKManagerVM by globalViewModels()
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
private val disposable = CompositeDisposable()
|
||||
|
||||
abstract fun prepareUxActivity()
|
||||
|
||||
abstract fun prepareTestingToolsActivity()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// 有一些手机从系统桌面进入的时候可能会重启main类型的activity
|
||||
// 需要校验这种情况,业界标准做法,基本所有app都需要这个
|
||||
if (!isTaskRoot && intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN == intent.action) {
|
||||
|
||||
finish()
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
window.decorView.apply {
|
||||
systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
|
||||
initMSDKInfoView()
|
||||
observeSDKManager()
|
||||
checkPermissionAndRequest()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (checkPermission()) {
|
||||
handleAfterPermissionPermitted()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (checkPermission()) {
|
||||
handleAfterPermissionPermitted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAfterPermissionPermitted() {
|
||||
prepareTestingToolsActivity()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun initMSDKInfoView() {
|
||||
msdkInfoVm.msdkInfo.observe(this) {
|
||||
binding.textViewVersion.text = StringUtils.getResStr(R.string.sdk_version, it.SDKVersion + " " + it.buildVer)
|
||||
binding.textViewProductName.text = StringUtils.getResStr(R.string.product_name, it.productType.name)
|
||||
binding.textViewPackageProductCategory.text = StringUtils.getResStr(R.string.package_product_category, it.packageProductCategory)
|
||||
binding.textViewIsDebug.text = StringUtils.getResStr(R.string.is_sdk_debug, it.isDebug)
|
||||
binding.textCoreInfo.text = it.coreInfo.toString()
|
||||
}
|
||||
|
||||
binding.iconSdkForum.setOnClickListener {
|
||||
Helper.startBrowser(this, StringUtils.getResStr(R.string.sdk_forum_url))
|
||||
}
|
||||
|
||||
binding.iconReleaseNode.setOnClickListener {
|
||||
Helper.startBrowser(this, StringUtils.getResStr(R.string.release_node_url))
|
||||
}
|
||||
binding.iconTechSupport.setOnClickListener {
|
||||
Helper.startBrowser(this, StringUtils.getResStr(R.string.tech_support_url))
|
||||
}
|
||||
binding.viewBaseInfo.setOnClickListener {
|
||||
baseMainActivityVm.doPairing {
|
||||
showToast(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeSDKManager() {
|
||||
msdkManagerVM.lvRegisterState.observe(this) { resultPair ->
|
||||
val statusText: String?
|
||||
if (resultPair.first) {
|
||||
ToastUtils.showToast("Register Success")
|
||||
statusText = StringUtils.getResStr(this, R.string.registered)
|
||||
msdkInfoVm.initListener()
|
||||
handler.postDelayed({
|
||||
prepareUxActivity()
|
||||
}, 5000)
|
||||
} else {
|
||||
showToast("Register Failure: ${resultPair.second}")
|
||||
statusText = StringUtils.getResStr(this, R.string.unregistered)
|
||||
}
|
||||
binding.textViewRegistered.text = StringUtils.getResStr(R.string.registration_status, statusText)
|
||||
}
|
||||
|
||||
msdkManagerVM.lvProductConnectionState.observe(this) { resultPair ->
|
||||
showToast("Product: ${resultPair.second} ,ConnectionState: ${resultPair.first}")
|
||||
}
|
||||
|
||||
msdkManagerVM.lvProductChanges.observe(this) { productId ->
|
||||
showToast("Product: $productId Changed")
|
||||
}
|
||||
|
||||
msdkManagerVM.lvInitProcess.observe(this) { processPair ->
|
||||
showToast("Init Process event: ${processPair.first.name}")
|
||||
}
|
||||
|
||||
msdkManagerVM.lvDBDownloadProgress.observe(this) { resultPair ->
|
||||
showToast("Database Download Progress current: ${resultPair.first}, total: ${resultPair.second}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(content: String) {
|
||||
ToastUtils.showToast(content)
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun <T> enableDefaultLayout(cl: Class<T>) {
|
||||
enableShowCaseButton(binding.defaultLayoutButton, cl)
|
||||
}
|
||||
|
||||
fun <T> enableWidgetList(cl: Class<T>) {
|
||||
enableShowCaseButton(binding.widgetListButton, cl)
|
||||
}
|
||||
|
||||
fun <T> enableTestingTools(cl: Class<T>) {
|
||||
enableShowCaseButton(binding.testingToolButton, cl)
|
||||
}
|
||||
|
||||
private fun <T> enableShowCaseButton(view: View, cl: Class<T>) {
|
||||
view.isEnabled = true
|
||||
view.setOnClickListener {
|
||||
Intent(this, cl).also {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissionAndRequest() {
|
||||
if (!checkPermission()) {
|
||||
requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermission(): Boolean {
|
||||
for (i in permissionArray.indices) {
|
||||
if (!PermissionUtil.isPermissionGranted(this, permissionArray[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
result?.entries?.forEach {
|
||||
if (!it.value) {
|
||||
requestPermission()
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermission() {
|
||||
requestPermissionLauncher.launch(permissionArray.toArray(arrayOf()))
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
18
sample/src/main/java/dji/sampleV5/aircraft/TestToolsVM.kt
Normal file
18
sample/src/main/java/dji/sampleV5/aircraft/TestToolsVM.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
|
||||
/**
|
||||
* Description :TestingToolsActivity对应的ViewModel,主要用于创建djiToastResult,用于统一发送和观察需要Toast的内容
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/6/16
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class TestToolsVM : ViewModel() {
|
||||
val djiToastResult = MutableLiveData<DJIToastResult>()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.navigation.Navigation
|
||||
import dji.sampleV5.aircraft.databinding.ActivityMainBinding
|
||||
import dji.sampleV5.aircraft.databinding.ActivityTestingToolsBinding
|
||||
import dji.sampleV5.aircraft.models.MSDKCommonOperateVm
|
||||
import dji.sampleV5.aircraft.util.DJIToastUtil
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.sampleV5.aircraft.views.MSDKInfoFragment
|
||||
import dji.v5.ux.core.util.ViewUtil
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/7/23
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
abstract class TestingToolsActivity : AppCompatActivity() {
|
||||
|
||||
protected lateinit var binding: ActivityTestingToolsBinding
|
||||
protected val msdkCommonOperateVm: MSDKCommonOperateVm by viewModels()
|
||||
|
||||
private val testToolsVM: TestToolsVM by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityTestingToolsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
window.decorView.apply {
|
||||
systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
|
||||
// 设置Listener防止系统UI获取焦点后进入到非全屏状态
|
||||
window.decorView.setOnSystemUiVisibilityChangeListener() {
|
||||
if (it and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
||||
}
|
||||
}
|
||||
|
||||
loadTitleView()
|
||||
|
||||
DJIToastUtil.dJIToastLD = testToolsVM.djiToastResult
|
||||
testToolsVM.djiToastResult.observe(this) { result ->
|
||||
result?.msg?.let {
|
||||
ToastUtils.showToast(it)
|
||||
}
|
||||
}
|
||||
|
||||
msdkCommonOperateVm.mainPageInfoList.observe(this) { list ->
|
||||
list.iterator().forEach {
|
||||
addDestination(it.vavGraphId)
|
||||
}
|
||||
}
|
||||
|
||||
loadPages()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ViewUtil.setKeepScreen(this, true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ViewUtil.setKeepScreen(this, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 本activity的NavController,都是基于nav_host_fragment_container的
|
||||
*/
|
||||
private fun addDestination(id: Int) {
|
||||
val v = Navigation.findNavController(binding.navHostFragmentContainer).navInflater.inflate(id)
|
||||
Navigation.findNavController(binding.navHostFragmentContainer).graph.addAll(v)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
DJIToastUtil.dJIToastLD = null
|
||||
}
|
||||
|
||||
open fun loadTitleView() {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.main_info_fragment_container, MSDKInfoFragment())
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun loadPages()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dji.sampleV5.aircraft;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.zklh.dronecontroller.MainActivity;
|
||||
|
||||
public class UsbAttachActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sampleV5.aircraft.R
|
||||
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/7
|
||||
*F
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class AircraftFragmentPageInfoFactory : IFragmentPageInfoFactory {
|
||||
|
||||
override fun createPageInfo(): FragmentPageItemList {
|
||||
return FragmentPageItemList(R.navigation.nav_aircraft).apply {
|
||||
items.add(FragmentPageItem(R.id.virtual_stick_page, R.string.item_virtual_stick_title, R.string.item_virtual_description))
|
||||
items.add(FragmentPageItem(R.id.flight_record_page, R.string.item_flight_record_title, R.string.item_flight_record_description))
|
||||
items.add(FragmentPageItem(R.id.flight_upgrade_page, R.string.item_upgrade_title, R.string.item_upgrade_description))
|
||||
items.add(FragmentPageItem(R.id.flight_simulator_page, R.string.item_simulator_title, R.string.item_simulator_description))
|
||||
items.add(FragmentPageItem(R.id.psdk_center_page, R.string.item_psdk_title, R.string.item_psdk_description))
|
||||
items.add(FragmentPageItem(R.id.megaphone_page, R.string.item_megaphone_title, R.string.item_megaphone_description))
|
||||
items.add(FragmentPageItem(R.id.waypoint_v3_page, R.string.item_waypoint_title, R.string.item_waypoint_description))
|
||||
items.add(FragmentPageItem(R.id.waypoint_v3_page, R.string.item_waypoint_title, R.string.item_waypoint_description))
|
||||
items.add(FragmentPageItem(R.id.rtk_center_page, R.string.item_trk_center_title, R.string.item_trk_center_description))
|
||||
items.add(FragmentPageItem(R.id.perception_page, R.string.item_perception_title, R.string.item_perception_description))
|
||||
items.add(FragmentPageItem(R.id.uas_page, R.string.item_uas_title, R.string.item_uas_description))
|
||||
items.add(FragmentPageItem(R.id.lte_page, R.string.item_lte_title, R.string.item_lte_description))
|
||||
items.add(FragmentPageItem(R.id.fly_safe_page, R.string.item_fly_safe_title, R.string.item_fly_safe_description))
|
||||
items.add(FragmentPageItem(R.id.security_code_page, R.string.item_security_code_title, R.string.item_security_code_description))
|
||||
items.add(FragmentPageItem(R.id.mop_center_page, R.string.item_mop_title, R.string.item_mop_description))
|
||||
items.add(FragmentPageItem(R.id.look_at_page, R.string.item_look_at_title, R.string.item_look_at_description))
|
||||
items.add(FragmentPageItem(R.id.intelligent_flight_page, R.string.item_intelligent_flight_title, R.string.item_intelligent_flight__description))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sampleV5.aircraft.R
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/7
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class CommonFragmentPageInfoFactory : IFragmentPageInfoFactory {
|
||||
|
||||
override fun createPageInfo(): FragmentPageItemList {
|
||||
return FragmentPageItemList(R.navigation.nav_common).apply {
|
||||
items.add(FragmentPageItem(R.id.key_value_page, R.string.item_key_value_title, R.string.item_key_value_description))
|
||||
items.add(FragmentPageItem(R.id.key_multi_camera_stream_page, R.string.item_multi_camera_stream_title, R.string.item_multi_camera_stream_description))
|
||||
items.add(FragmentPageItem(R.id.key_multi_video_decoding_page, R.string.item_multi_video_decoding_title, R.string.item_multi_video_decoding_description, true))
|
||||
items.add(FragmentPageItem(R.id.key_data_protection_page, R.string.item_data_protection_title, R.string.item_data_protection_description))
|
||||
items.add(FragmentPageItem(R.id.key_diagnostic_page, R.string.item_diagnostic_title, R.string.item_diagnostic_description))
|
||||
items.add(FragmentPageItem(R.id.key_media_playback_page, R.string.item_media_playback_title, R.string.item_media_playback_description))
|
||||
items.add(FragmentPageItem(R.id.key_live_stream_page, R.string.item_live_stream_title, R.string.item_live_stream_description))
|
||||
items.add(FragmentPageItem(R.id.key_login_account_page, R.string.item_login_account_title, R.string.item_login_account_description))
|
||||
items.add(FragmentPageItem(R.id.key_log_info_page, R.string.item_log_info_title, R.string.item_log_info_description))
|
||||
items.add(FragmentPageItem(R.id.key_diagnostic_page, R.string.item_diagnostic_title, R.string.item_diagnostic_description))
|
||||
items.add(FragmentPageItem(R.id.key_app_silently_upgrade_page, R.string.item_app_silently_upgrade_title, R.string.item_app_silently_upgrade_description))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.BaseStationDeviceType
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationInfo
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
|
||||
|
||||
/**
|
||||
* Description :驱动UI的数据模型,基于RTKBaseStationConnectInfo新增一个连接状态属性
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/3/6
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIRTKBaseStationConnectInfo(
|
||||
var baseStationId: Int,
|
||||
var signalLevel: Int,
|
||||
var rtkStationName: String,
|
||||
var connectStatus: RTKStationConnetState = RTKStationConnetState.IDLE
|
||||
) : RTKStationInfo(baseStationId, signalLevel, rtkStationName, BaseStationDeviceType.BS_RTK2) {
|
||||
constructor() : this(0, 0, "")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
|
||||
/**
|
||||
* Description :面向Model使用的执行结果封装类,简单封装了外部需要Toast的信息
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/2/22
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIToastResult(var isSuccess: Boolean, var msg: String? = null) {
|
||||
|
||||
companion object {
|
||||
fun success(msg: String? = null): DJIToastResult {
|
||||
return DJIToastResult(true, "success ${msg ?: ""}")
|
||||
}
|
||||
|
||||
fun failed(msg: String): DJIToastResult {
|
||||
return DJIToastResult(false, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.flightcontroller.AccessLockerDeviceType
|
||||
|
||||
/**
|
||||
* Description :设备的锁状态
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/8/10
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 设备的锁状态
|
||||
*/
|
||||
data class DeviceLockStatus(
|
||||
var deviceIndex: AccessLockerDeviceType = AccessLockerDeviceType.UNKNOWN,
|
||||
//功能是否支持
|
||||
var isFeatureSupported: Boolean = false,
|
||||
//功能是否开启
|
||||
var isFeatureEnabled: Boolean = false,
|
||||
//功能是否需要验证
|
||||
var isFeatureNeedToBeVerified: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* 修改密码数据类
|
||||
*/
|
||||
data class ModifyPasswordBean(val currentPassword: String, val newPassword: String){
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/19 6:30 下午
|
||||
* @description: 飞机位置信息,坐标,朝向
|
||||
*/
|
||||
data class FlightControlState(
|
||||
var longtitude : Double = 0.0
|
||||
, var latitude: Double = 0.0
|
||||
, val head : Float = 0.0f
|
||||
, val height : Double = 0.0
|
||||
, val distance: Double= 0.0
|
||||
, val speed :Double = 0.0
|
||||
, val homeLocation: LocationCoordinate2D
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
data class FragmentPageItemList(
|
||||
val vavGraphId: Int = DEFAULT_RES_ID,
|
||||
val items: LinkedHashSet<FragmentPageItem> = LinkedHashSet()
|
||||
)
|
||||
|
||||
data class FragmentPageItem(
|
||||
val id: Int = DEFAULT_RES_ID,
|
||||
val title: Int = DEFAULT_RES_ID,
|
||||
val description: Int = DEFAULT_RES_ID,
|
||||
val isStrike: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/7
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
interface IFragmentPageInfoFactory {
|
||||
fun createPageInfo(): FragmentPageItemList
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/09/14 2:32 下午
|
||||
* @description: key的检查结果信息
|
||||
*/
|
||||
data class KeyCheckInfo(
|
||||
var keyName:String ,
|
||||
var isPass : Boolean ,
|
||||
var failedReson: String
|
||||
) {}
|
||||
@@ -0,0 +1,543 @@
|
||||
package dji.sampleV5.aircraft.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
|
||||
import dji.v5.manager.mop.DataResult;
|
||||
import dji.v5.manager.mop.Pipeline;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
|
||||
/**
|
||||
* 利用MOP通道,实现自定的文件上传下载协议
|
||||
*/
|
||||
public class MOPCmdHelper {
|
||||
private static final byte CMD_REQ = 0x50;
|
||||
private static final byte CMD_ACK = 0x51;
|
||||
private static final byte CMD_TRANS_ACK = 0x52;
|
||||
private static final byte CMD_FILE_INFO = 0x60;
|
||||
private static final byte CMD_DOWNLOAD = 0x61;
|
||||
private static final byte CMD_FILE_DATA = 0x62;
|
||||
private static final byte CMD_FILE_TRANS_FAIL = 0x63;
|
||||
private static final byte CMD_FILE_TRANS_FAIL_ACK = 0x64;
|
||||
private static final byte CMD_0 = 0x00;
|
||||
private static final byte CMD_1 = 0x01;
|
||||
public static final int PACK_HEADER_SIZE = 8;
|
||||
public static final int PACK_FILE_INFO_SIZE = 53;
|
||||
public static final String UPLOAD_FILE = "uploadFile: ";
|
||||
|
||||
private static final int FILE_NAME_LENGTH = 32;
|
||||
|
||||
private static final String TAG = MOPCmdHelper.class.getSimpleName();
|
||||
|
||||
public static byte[] getUploadFileHeader() {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_REQ;
|
||||
cmd[1] = CMD_0;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static byte[] getDownloadFileHeader() {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_REQ;
|
||||
cmd[1] = CMD_1;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static byte[] getDownloadFile() {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_DOWNLOAD;
|
||||
cmd[1] = (byte) 0xFF;
|
||||
cmd[4] = (byte) 0x20;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static byte[] getFileDataHeader(int size, int flag, int seq) {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_FILE_DATA;
|
||||
cmd[1] = (byte) flag;
|
||||
cmd[2] = (byte) seq;
|
||||
cmd[4] = (byte) (size & 0xff);
|
||||
cmd[5] = (byte) (size >> 8 & 0xff);
|
||||
cmd[6] = (byte) (size >> 16 & 0xff);
|
||||
cmd[7] = (byte) (size >> 24 & 0xff);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static boolean sendDownloadCmd(Pipeline p) {
|
||||
byte[] cmd = getDownloadFileHeader();
|
||||
DataResult result = p.writeData(cmd);
|
||||
if (result.getLength() > 0) {
|
||||
return parseCommonAck(p);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static FileInfo sendDownloadFileReq(Pipeline p, String filename, PipelineAdapter.OnEventListener listener) {
|
||||
if (!sendDownloadCmd(p)) {
|
||||
return null;
|
||||
}
|
||||
byte[] header = getDownloadFile();
|
||||
byte[] chars = filename.getBytes();
|
||||
|
||||
byte[] req = new byte[header.length + FILE_NAME_LENGTH];
|
||||
System.arraycopy(header, 0, req, 0, header.length);
|
||||
System.arraycopy(chars, 0, req, header.length, chars.length);
|
||||
|
||||
if (chars.length < FILE_NAME_LENGTH) {
|
||||
req[header.length + chars.length] = '\0';
|
||||
}
|
||||
// 发送请求,要下载的文件
|
||||
DataResult result = p.writeData(req);
|
||||
if (result.getLength() > 0) {
|
||||
|
||||
LogUtils.i(TAG, "sendDownloadFileReq ack: Success", "/MOP");
|
||||
// 读取文件信息
|
||||
byte[] fileInfoBuff = new byte[MOPCmdHelper.PACK_HEADER_SIZE + MOPCmdHelper.PACK_FILE_INFO_SIZE];
|
||||
int sum = 0;
|
||||
while (sum < fileInfoBuff.length) {
|
||||
// 能获取MD5等信息
|
||||
int len = p.readData(fileInfoBuff).getLength();
|
||||
|
||||
if (len > 0) {
|
||||
// 由于read不支持offset,这里处理fileInfoBuff的拼接,应该使用临时的byte[]来copyArray
|
||||
sum += len;
|
||||
LogUtils.e(TAG, "sendDownloadFileReq download:" + sum, "/MOP");
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FileInfo.parse(fileInfoBuff);
|
||||
|
||||
} else {
|
||||
postResultTipEvent(TipEvent.DOWNLOAD, "request failure", null, listener);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int sendTransFileFailReq(Pipeline pipeline, long length) {
|
||||
byte[] buff = new byte[12];
|
||||
buff[0] = CMD_FILE_TRANS_FAIL;
|
||||
buff[1] = (byte) 0xFF;
|
||||
buff[7] = 0x04;
|
||||
buff[8] = (byte) (length >> 0 & 0xff);
|
||||
buff[9] = (byte) (length >> 8 & 0xff);
|
||||
buff[10] = (byte) (length >> 16 & 0xff);
|
||||
buff[11] = (byte) (length >> 24 & 0xff);
|
||||
|
||||
int len = getInt(buff, PACK_HEADER_SIZE, 4);
|
||||
int result = pipeline.writeData(buff).getLength();
|
||||
LogUtils.i(TAG, "sendTransFileFailReq:" + result, "/MOP");
|
||||
LogUtils.i(TAG, "len:" + len, "/MOP");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int sendTransFileFailAck(Pipeline pipeline, long length) {
|
||||
byte[] buff = new byte[12];
|
||||
buff[0] = CMD_FILE_TRANS_FAIL_ACK;
|
||||
buff[1] = (byte) 0xFF;
|
||||
buff[7] = 0x04;
|
||||
buff[8] = (byte) (length >> 0 & 0xff);
|
||||
buff[9] = (byte) (length >> 8 & 0xff);
|
||||
buff[10] = (byte) (length >> 16 & 0xff);
|
||||
buff[11] = (byte) (length >> 24 & 0xff);
|
||||
int result = pipeline.writeData(buff).getLength();
|
||||
LogUtils.i(TAG, "sendTransFileFailAck:" + result, "/MOP");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int parseTransFileFailAck(Pipeline p) {
|
||||
byte[] buff = new byte[12];
|
||||
int size;
|
||||
// 读取回包
|
||||
while (((size = p.readData(buff).getLength()) < 0)) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
LogUtils.i(TAG, "parseTransFileFailAck wait ack: " + size, "/MOP");
|
||||
}
|
||||
return getInt(buff, PACK_HEADER_SIZE, 4);
|
||||
}
|
||||
|
||||
|
||||
public static boolean parseCommonAck(Pipeline p) {
|
||||
// 读取文件下载的信息
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
int size;
|
||||
// 读取回包
|
||||
while ((size = p.readData(buff).getLength()) < 0) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
LogUtils.e(TAG, "sendDownloadFileReq wait ack: " + size, "/MOP");
|
||||
}
|
||||
return (buff[0] == CMD_ACK) && (buff[1] == CMD_0);
|
||||
}
|
||||
|
||||
public static boolean parseUploadAck(Pipeline p) {
|
||||
// 读取文件下载的信息
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
int size;
|
||||
// 读取回包
|
||||
while ((size = p.readData(buff).getLength()) < 0) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
LogUtils.i(TAG, "upload ack: " + size, "/MOP");
|
||||
}
|
||||
return (buff[0] == CMD_TRANS_ACK) && (buff[1] == CMD_0);
|
||||
}
|
||||
|
||||
public static FileTransResult parseFileDataCmd(byte[] buff) {
|
||||
if (buff[0] == CMD_FILE_DATA) {
|
||||
int length = getInt(buff, 4, 4);
|
||||
return new FileTransResult(true, length);
|
||||
}
|
||||
if (buff[0] == CMD_FILE_TRANS_FAIL) {
|
||||
int length = getInt(buff, 4, 4);
|
||||
return new FileTransResult(false, length);
|
||||
}
|
||||
LogUtils.e(TAG, "parseFileDataCmd error", "/MOP");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int sendTransAck(Pipeline p, boolean result) {
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
buff[0] = CMD_TRANS_ACK;
|
||||
buff[1] = result ? CMD_0 : CMD_1;
|
||||
return p.writeData(buff).getLength();
|
||||
}
|
||||
|
||||
public static int sendAck(Pipeline p, byte cmd) {
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
buff[0] = CMD_ACK;
|
||||
buff[1] = cmd;
|
||||
return p.writeData(buff).getLength();
|
||||
}
|
||||
|
||||
public static boolean isFileEnd(byte[] buff) {
|
||||
return buff[0] == CMD_FILE_DATA && buff[1] == CMD_1;
|
||||
}
|
||||
|
||||
public static int sendUploadFileReq(Pipeline data, String filename, byte[] buff, long time, byte[] md5,
|
||||
PipelineAdapter.OnEventListener listener) {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.filename = filename;
|
||||
fileInfo.fileLength = buff.length;
|
||||
fileInfo.md5 = md5;
|
||||
LogUtils.i(TAG, "sendUploadFileReq fileInfo:" + fileInfo, "/MOP");
|
||||
listener.onFileInfoEvent(fileInfo);
|
||||
|
||||
DataResult dataResult = data.writeData(getUploadFileHeader());
|
||||
int result = dataResult.getLength();
|
||||
if (result < 0) {
|
||||
postResultTipEvent(TipEvent.UPLOAD, "Upload Failure: " + dataResult.toString(), null, listener);
|
||||
return result;
|
||||
}
|
||||
if (parseCommonAck(data)) {
|
||||
// 发送md5等
|
||||
byte[] fileHeader = fileInfo.getHeader();
|
||||
DataResult writeData = data.writeData(fileHeader);
|
||||
result = writeData.getLength();
|
||||
LogUtils.i(TAG, "sendUploadFileReq send md5:" + writeData.toString(), "/MOP");
|
||||
if (result > 0) {
|
||||
if (parseCommonAck(data)) {
|
||||
// 上传文件
|
||||
uploadFile(data, buff, time, listener);
|
||||
// 上传完的ack
|
||||
if (parseUploadAck(data)) {
|
||||
postResultTipEvent(TipEvent.UPLOAD, "Upload Success", null, listener);
|
||||
}
|
||||
} else {
|
||||
postResultTipEvent(TipEvent.UPLOAD, "Upload Failure", null, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void postResultTipEvent(int type, String result, String progress, PipelineAdapter.OnEventListener listener) {
|
||||
TipEvent event = new TipEvent(type);
|
||||
event.result = result;
|
||||
event.progress = progress;
|
||||
listener.onTipEvent(event);
|
||||
}
|
||||
|
||||
private static int uploadFile(Pipeline pipeline, byte[] buff, long time, PipelineAdapter.OnEventListener listener) {
|
||||
int size = 3072;
|
||||
byte[] header;
|
||||
int hadWrote = 0;
|
||||
int seq = 0;
|
||||
if (buff.length <= size) {
|
||||
header = getFileDataHeader(buff.length, CMD_1, seq);
|
||||
int result = writeData(pipeline, buff, time, header, hadWrote, buff.length, listener);
|
||||
LogUtils.i(TAG, UPLOAD_FILE + result, "/MOP", ",seq:", seq);
|
||||
return result;
|
||||
} else {
|
||||
while (buff.length - hadWrote > size) {
|
||||
header = getFileDataHeader(size, CMD_0, seq);
|
||||
int result = writeData(pipeline, buff, time, header, hadWrote, size, listener);
|
||||
|
||||
hadWrote += size;
|
||||
LogUtils.i(TAG, UPLOAD_FILE + hadWrote + " result:" + result, ",seq:", seq);
|
||||
seq++;
|
||||
}
|
||||
int length = buff.length - hadWrote;
|
||||
header = getFileDataHeader(length, CMD_1, seq);
|
||||
int result = writeData(pipeline, buff, time, header, hadWrote, length, listener);
|
||||
hadWrote += result;
|
||||
LogUtils.i(TAG, UPLOAD_FILE + length, "/MOP");
|
||||
}
|
||||
|
||||
String progress = String.format("uploadSize:%d, useTime:%d(ms)", hadWrote, System.currentTimeMillis() - time);
|
||||
postResultTipEvent(TipEvent.UPLOAD, null, progress, listener);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int writeData(Pipeline pipeline, byte[] buff, long time, byte[] header, int hadWrote, int length,
|
||||
PipelineAdapter.OnEventListener listener) {
|
||||
byte[] d = new byte[length + header.length];
|
||||
System.arraycopy(header, 0, d, 0, header.length);
|
||||
System.arraycopy(buff, hadWrote, d, header.length, length);
|
||||
DataResult dataResult = pipeline.writeData(d);
|
||||
int result = dataResult.getLength();
|
||||
|
||||
String progress = String.format("uploadSize:%d, useTime:%d(ms)", hadWrote, System.currentTimeMillis() - time);
|
||||
postResultTipEvent(TipEvent.UPLOAD, null, progress, listener);
|
||||
|
||||
if (result < 0) {
|
||||
LogUtils.e(TAG, "writeData miss: " + dataResult.toString());
|
||||
// 发送上传出错的req
|
||||
sendTransFileFailReq(pipeline, hadWrote);
|
||||
// 读取对端返回的已读长度
|
||||
int len = parseTransFileFailAck(pipeline);
|
||||
if (len == hadWrote) {
|
||||
sendAck(pipeline, CMD_0);
|
||||
return writeData(pipeline, buff, time, header, hadWrote, length, listener);
|
||||
} else {
|
||||
LogUtils.e(TAG, "writeData error: " + len);
|
||||
sendAck(pipeline, CMD_1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int parseFileFailIndex(Pipeline pipeline) {
|
||||
byte[] buff = new byte[4];
|
||||
pipeline.readData(buff);
|
||||
return getInt(buff, 0, 4);
|
||||
}
|
||||
|
||||
|
||||
public static byte[] getMD5(File file) {
|
||||
byte[] buffer = new byte[8192];
|
||||
byte[] desc = new byte[16];
|
||||
try (InputStream ins = new FileInputStream(file)) {
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
int len;
|
||||
while ((len = ins.read(buffer)) != -1) {
|
||||
md5.update(buffer, 0, len);
|
||||
}
|
||||
byte[] source = md5.digest();
|
||||
System.arraycopy(source, 0, desc, 0, desc.length);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
public static int getInt(byte[] bytes, final int offset, int length) {
|
||||
if (null == bytes) {
|
||||
return 0;
|
||||
}
|
||||
final int bytesLen = bytes.length;
|
||||
if (bytesLen == 0 || offset < 0 || bytesLen <= offset) {
|
||||
return 0;
|
||||
}
|
||||
if (length > bytesLen - offset) {
|
||||
length = bytesLen - offset;
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
for (int i = length + offset - 1; i >= offset; i--) {
|
||||
value = (value << 8 | (bytes[i] & 0xff));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static class FileInfo {
|
||||
private boolean isExist;
|
||||
private int fileLength;
|
||||
private String filename;
|
||||
private byte[] md5;
|
||||
|
||||
public static FileInfo parse(byte[] data) {
|
||||
if (data[0] != CMD_FILE_INFO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("mop_cmd_file:");
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
sb.append(data[i] + ",");
|
||||
}
|
||||
|
||||
FileInfo info = new FileInfo();
|
||||
info.isExist = data[8] == 1;
|
||||
info.fileLength = getInt(data, 9, 4);
|
||||
byte[] d = new byte[32];
|
||||
System.arraycopy(data, 13, d, 0, 32);
|
||||
info.filename = getString(d);
|
||||
byte[] md5 = new byte[16];
|
||||
System.arraycopy(data, 45, md5, 0, 16);
|
||||
info.md5 = md5;
|
||||
return info;
|
||||
}
|
||||
|
||||
public byte[] getHeader() {
|
||||
byte[] buff = new byte[61];
|
||||
buff[0] = CMD_FILE_INFO;
|
||||
buff[1] = (byte) 0xFF;
|
||||
buff[4] = 0x35;
|
||||
buff[8] = 0;
|
||||
buff[9] = (byte) (fileLength & 0xff);
|
||||
buff[10] = (byte) (fileLength >> 8 & 0xff);
|
||||
buff[11] = (byte) (fileLength >> 16 & 0xff);
|
||||
buff[12] = (byte) (fileLength >> 24 & 0xff);
|
||||
|
||||
byte[] chars = filename.getBytes();
|
||||
System.arraycopy(chars, 0, buff, 13, chars.length);
|
||||
System.arraycopy(md5, 0, buff, 45, 16);
|
||||
if (chars.length < 32) {
|
||||
buff[13 + chars.length] = '\0';
|
||||
}
|
||||
return buff;
|
||||
}
|
||||
|
||||
public static String getString(byte[] bytes) {
|
||||
if (null == bytes) {
|
||||
return "";
|
||||
}
|
||||
// 去除NULL字符
|
||||
byte zero = 0x00;
|
||||
byte no = (byte) 0xFF;
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
if (bytes[i] == zero || bytes[i] == no) {
|
||||
bytes = readBytes(bytes, 0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new String(bytes, Charset.forName("GBK"));
|
||||
}
|
||||
|
||||
public static byte[] readBytes(byte[] source, int from, int length) {
|
||||
byte[] result = new byte[length];
|
||||
System.arraycopy(source, from, result, 0, length);
|
||||
/**
|
||||
for (int i = 0; i < length; i++) {
|
||||
result[i] = source[from + i];
|
||||
}
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FileInfo{" +
|
||||
"isExist=" + isExist +
|
||||
", fileLength=" + fileLength +
|
||||
", filename='" + filename + '\'' +
|
||||
", md5=" + Arrays.toString(md5) +
|
||||
'}';
|
||||
}
|
||||
|
||||
public boolean isExist() {
|
||||
return isExist;
|
||||
}
|
||||
|
||||
public int getFileLength() {
|
||||
return fileLength;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public byte[] getMd5() {
|
||||
return md5;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ProcessCallback {
|
||||
void callback(int length);
|
||||
}
|
||||
|
||||
public static class FileTransResult {
|
||||
// true代表读了多少数据(length),false重新读写,并根据之后length个字节锁代表的int值去移动游标到指定位置
|
||||
private boolean success;
|
||||
private int length;
|
||||
|
||||
public FileTransResult(boolean success, int length) {
|
||||
this.success = success;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TipEvent {
|
||||
public static final int UPLOAD = 0;
|
||||
public static final int DOWNLOAD = 1;
|
||||
|
||||
public TipEvent(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private int type;
|
||||
private String state;
|
||||
private String result;
|
||||
private String progress;
|
||||
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
sample/src/main/java/dji/sampleV5/aircraft/data/MSDKInfo.kt
Normal file
27
sample/src/main/java/dji/sampleV5/aircraft/data/MSDKInfo.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.product.ProductType
|
||||
import dji.v5.common.register.PackageProductCategory
|
||||
import dji.v5.utils.inner.SDKConfig
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/6
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
|
||||
data class MSDKInfo(val SDKVersion: String = DEFAULT_STR) {
|
||||
var buildVer: String = DEFAULT_STR
|
||||
var isDebug: Boolean = false
|
||||
var packageProductCategory: PackageProductCategory? = null
|
||||
var productType: ProductType = ProductType.UNKNOWN
|
||||
var networkInfo: String = DEFAULT_STR
|
||||
var countryCode: String = DEFAULT_STR
|
||||
var firmwareVer: String = DEFAULT_STR
|
||||
var isLDMLicenseLoaded: String = DEFAULT_STR
|
||||
var isLDMEnabled: String = DEFAULT_STR
|
||||
var coreInfo: SDKConfig.CoreInfo? = null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.v5.common.error.IDJIError
|
||||
|
||||
|
||||
|
||||
|
||||
data class MissionUploadStateInfo(
|
||||
var tips: String = ""
|
||||
, var updateProgress: Double = 0.0
|
||||
, val error: IDJIError? = null
|
||||
) {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import dji.sampleV5.aircraft.R
|
||||
|
||||
|
||||
/**
|
||||
* Description :PayloadWidgetIcon数据处理类
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/12/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class PayloadWidgetIconAdapter :
|
||||
ListAdapter<PayloadWidgetItem, PayloadWidgetIconAdapter.ViewHolder>(DiffCallback) {
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_payload_wdiget_item, parent, false)
|
||||
return ViewHolder(view, parent.context)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.bind(item)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, val context: Context) : RecyclerView.ViewHolder(itemView) {
|
||||
private val iconImage: ImageView = itemView.findViewById(R.id.iv_widget_icon)
|
||||
private val iconDesc: TextView = itemView.findViewById(R.id.tv_widget_desc)
|
||||
|
||||
fun bind(item: PayloadWidgetItem) {
|
||||
Glide.with(context).load(item.imgPath).into(iconImage)
|
||||
iconDesc.text = item.des
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object DiffCallback : DiffUtil.ItemCallback<PayloadWidgetItem>() {
|
||||
override fun areItemsTheSame(oldItem: PayloadWidgetItem, newItem: PayloadWidgetItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: PayloadWidgetItem, newItem: PayloadWidgetItem): Boolean {
|
||||
return oldItem.des == newItem.des && oldItem.imgPath == newItem.imgPath
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
/**
|
||||
* Description :
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/12/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
data class PayloadWidgetItem(var des: String, var imgPath: String?)
|
||||
@@ -0,0 +1,564 @@
|
||||
package dji.sampleV5.aircraft.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import dji.sampleV5.aircraft.R;
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType;
|
||||
import dji.v5.common.error.DJIPipeLineError;
|
||||
import dji.v5.manager.mop.DataResult;
|
||||
import dji.v5.manager.mop.Pipeline;
|
||||
import dji.v5.manager.mop.PipelineManager;
|
||||
import dji.v5.utils.common.BytesUtil;
|
||||
import dji.v5.utils.common.DiskUtil;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
import dji.v5.ux.core.util.ToastUtils;
|
||||
|
||||
public class PipelineAdapter extends RecyclerView.Adapter<PipelineAdapter.ViewHolder> {
|
||||
private final List<Pair<ComponentIndexType, Pipeline>> data;
|
||||
private final LayoutInflater mInflater;
|
||||
private ViewHolder curHolder;
|
||||
|
||||
private final OnDisconnectListener listener = new OnDisconnectListener() {
|
||||
@Override
|
||||
public void onDisconnect(Pipeline d) {
|
||||
if (data.contains(d)) {
|
||||
data.remove(d);
|
||||
notifyItemRemoved(data.indexOf(d));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
public PipelineAdapter(Context context, List<Pair<ComponentIndexType, Pipeline>> data) {
|
||||
this.mInflater = LayoutInflater.from(context);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void addItem(ComponentIndexType indexType, Pipeline action) {
|
||||
if (action == null || data == null || data.contains(action)) {
|
||||
return;
|
||||
}
|
||||
data.add(new Pair<>(indexType, action));
|
||||
notifyItemInserted(getItemCount() - 1);
|
||||
}
|
||||
|
||||
public List<Pair<ComponentIndexType, Pipeline>> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
curHolder = new ViewHolder(mInflater.inflate(R.layout.adapter_pipeline_item, parent, false));
|
||||
return curHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
holder.setData(data.get(position));
|
||||
holder.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data == null ? 0 : data.size();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
ViewHolder viewholder = (ViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
|
||||
if (viewholder != null) {
|
||||
viewholder.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
holder.destroy();
|
||||
}
|
||||
|
||||
public static String getTime() {
|
||||
String patten = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
SimpleDateFormat format = new SimpleDateFormat(patten);
|
||||
return format.format(new Date());
|
||||
}
|
||||
|
||||
// @Override
|
||||
public void onDisconnect(Pipeline data) {
|
||||
this.data.remove(data);
|
||||
notifyItemRemoved(this.data.indexOf(data));
|
||||
}
|
||||
|
||||
public void onReset(Pipeline pipeline) {
|
||||
this.data.remove(pipeline);
|
||||
curHolder.destroy();
|
||||
notifyItemRemoved(this.data.indexOf(pipeline));
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private String tag = LogUtils.getTag(this);
|
||||
private final TextView nameTv;
|
||||
private final TextView downloadTv;
|
||||
private final TextView uploadTv;
|
||||
private final TextView downloadLogTv;
|
||||
private final TextView uploadLogTv;
|
||||
private final TextView disconnectTv;
|
||||
private final TextView filenameTv;
|
||||
private final Switch autoDownloadSwitch;
|
||||
|
||||
HandlerThread uploadThread;
|
||||
HandlerThread downloadThread;
|
||||
Handler uploadHandler;
|
||||
Handler downloadHandler;
|
||||
private WeakReference<OnDisconnectListener> listenerWeakReference;
|
||||
|
||||
private boolean uploading;
|
||||
private boolean downloading;
|
||||
|
||||
private String uploadFileInfoLog;
|
||||
private String uploadProgress;
|
||||
private String uploadResult;
|
||||
|
||||
private String downloadFileInfoLog;
|
||||
private String downloadProgress;
|
||||
private String downloadResult;
|
||||
private int downloadPackCount;
|
||||
private int downloadSize;
|
||||
|
||||
private int downloadSuccessCount;
|
||||
private int downloadCount;
|
||||
|
||||
private int uploadSuccessCount;
|
||||
private int uploadCount;
|
||||
|
||||
private String uploadFileName = "mopSample.log";
|
||||
private final OnEventListener listener = new OnEventListener() {
|
||||
@Override
|
||||
public void onTipEvent(MOPCmdHelper.TipEvent event) {
|
||||
handlerTipEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileInfoEvent(MOPCmdHelper.FileInfo event) {
|
||||
onEvent3BackgroundThread(event);
|
||||
}
|
||||
};
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
nameTv = itemView.findViewById(R.id.tv_name);
|
||||
downloadTv = itemView.findViewById(R.id.tv_download);
|
||||
uploadTv = itemView.findViewById(R.id.tv_upload);
|
||||
downloadLogTv = itemView.findViewById(R.id.tv_download_log);
|
||||
uploadLogTv = itemView.findViewById(R.id.tv_upload_log);
|
||||
disconnectTv = itemView.findViewById(R.id.tv_disconnect);
|
||||
filenameTv = itemView.findViewById(R.id.et_file_name);
|
||||
autoDownloadSwitch = itemView.findViewById(R.id.switch_auto_download);
|
||||
}
|
||||
|
||||
public void setData(Pair<ComponentIndexType, Pipeline> pair) {
|
||||
destroy();
|
||||
Pipeline pipeline = pair.second;
|
||||
uploadThread = new HandlerThread("upload");
|
||||
downloadThread = new HandlerThread("download");
|
||||
uploadThread.start();
|
||||
downloadThread.start();
|
||||
uploadHandler = new Handler(uploadThread.getLooper());
|
||||
downloadHandler = new Handler(downloadThread.getLooper());
|
||||
View.OnClickListener localListener = v -> {
|
||||
int id = v.getId();
|
||||
if (id == R.id.tv_upload) {
|
||||
showDialog(itemView.getContext(), pipeline);
|
||||
} else if (id == R.id.tv_download) {
|
||||
downloadHandler.post(() -> {
|
||||
resetDownInfo();
|
||||
downloadFile(pipeline, filenameTv.getText().toString());
|
||||
});
|
||||
} else if (id == R.id.tv_disconnect) {
|
||||
destroy();
|
||||
disconnect(pair.first, pipeline);
|
||||
}
|
||||
};
|
||||
|
||||
String title = String.format("Id=%d, MOPType = %s, trans_type=%s", pipeline.getId(), pipeline.getPipelineDeviceType(),
|
||||
pipeline.getTransmissionControlType());
|
||||
nameTv.setText(title);
|
||||
downloadTv.setOnClickListener(localListener);
|
||||
uploadTv.setOnClickListener(localListener);
|
||||
disconnectTv.setOnClickListener(localListener);
|
||||
}
|
||||
|
||||
public void setListener(OnDisconnectListener listener) {
|
||||
listenerWeakReference = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
private void disconnect(ComponentIndexType indexType, Pipeline pipeline) {
|
||||
PipelineManager.getInstance().disconnectPipeline(indexType, pipeline.getId(), pipeline.getPipelineDeviceType(),
|
||||
pipeline.getTransmissionControlType());
|
||||
if (listenerWeakReference != null && listenerWeakReference.get() != null) {
|
||||
listenerWeakReference.get().onDisconnect(pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
public void destroy() {
|
||||
if (uploadHandler != null && uploadThread != null) {
|
||||
uploadHandler.removeCallbacksAndMessages(null);
|
||||
uploadThread.quitSafely();
|
||||
}
|
||||
if (downloadHandler != null && downloadThread != null) {
|
||||
downloadHandler.removeCallbacksAndMessages(null);
|
||||
downloadThread.quitSafely();
|
||||
}
|
||||
resetDownInfo();
|
||||
resetUploadInfo();
|
||||
}
|
||||
|
||||
private void resetDownInfo() {
|
||||
downloadPackCount = 0;
|
||||
downloadSize = 0;
|
||||
|
||||
downloadFileInfoLog = "";
|
||||
downloadProgress = "";
|
||||
downloadResult = "";
|
||||
downloading = false;
|
||||
}
|
||||
|
||||
private void resetUploadInfo() {
|
||||
uploadFileInfoLog = "";
|
||||
uploadProgress = "";
|
||||
uploadResult = "";
|
||||
uploading = false;
|
||||
}
|
||||
|
||||
private void uploadFile(Pipeline data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (uploading) {
|
||||
toast("uploading");
|
||||
return;
|
||||
}
|
||||
uploading = true;
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
InputStream inputStream = null;
|
||||
FileOutputStream out = null;
|
||||
ByteArrayOutputStream outputStream = null;
|
||||
try {
|
||||
byte[] buff = new byte[3072];
|
||||
inputStream = itemView.getContext().getAssets().open("mop/" + uploadFileName);
|
||||
File tmp = new File(itemView.getContext().getCacheDir(), "mop.tmp");
|
||||
out = new FileOutputStream(tmp);
|
||||
outputStream = new ByteArrayOutputStream();
|
||||
int len;
|
||||
while ((len = inputStream.read(buff, 0, 3072)) > 0) {
|
||||
outputStream.write(buff, 0, len);
|
||||
out.write(buff, 0, len);
|
||||
}
|
||||
MOPCmdHelper.sendUploadFileReq(data, uploadFileName, outputStream.toByteArray(), time, MOPCmdHelper.getMD5(tmp), listener);
|
||||
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
uploadCount++;
|
||||
updateUploadUI();
|
||||
uploading = false;
|
||||
}
|
||||
|
||||
private void downloadFile(Pipeline pipeline, String filename) {
|
||||
if (pipeline == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
toast("downloading");
|
||||
return;
|
||||
}
|
||||
downloading = true;
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
// 获取文件信息
|
||||
MOPCmdHelper.FileInfo fileInfo = MOPCmdHelper.sendDownloadFileReq(pipeline, filename, listener);
|
||||
if (fileInfo == null || !fileInfo.isExist()) {
|
||||
LogUtils.e(tag, "downloadFile fail", "/MOP");
|
||||
downloading = false;
|
||||
return;
|
||||
}
|
||||
downloadFileInfoLog = fileInfo.toString();
|
||||
updateDownloadUI();
|
||||
|
||||
RandomAccessFile stream = null;
|
||||
try {
|
||||
LogUtils.i(tag, " fileInfo=" + fileInfo);
|
||||
File file = DiskUtil.getDiskCacheDir(itemView.getContext(), fileInfo.getFilename());
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
file.createNewFile();
|
||||
stream = new RandomAccessFile(file, "rw");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// 开始读取文件数据
|
||||
byte[] headBuff = new byte[MOPCmdHelper.PACK_HEADER_SIZE];
|
||||
DataResult readData = pipeline.readData(headBuff);
|
||||
if (readData.getError() != null && readData.getError().errorCode().equals(DJIPipeLineError.CLOSING)) {
|
||||
LogUtils.e(tag, "Pipeline is closing,finish down");
|
||||
return;
|
||||
}
|
||||
if (readData.getLength() < MOPCmdHelper.PACK_HEADER_SIZE) {
|
||||
LogUtils.e(tag, "readData.getLength()=" + readData.getLength() + " <8,jump over!result=" + readData);
|
||||
continue;
|
||||
}
|
||||
// 这个包带有的文件字节
|
||||
MOPCmdHelper.FileTransResult result = MOPCmdHelper.parseFileDataCmd(headBuff);
|
||||
if (result == null) {
|
||||
LogUtils.e(tag, "FileTransResult=null ,finish down");
|
||||
downloading = false;
|
||||
return;
|
||||
}
|
||||
if (result.isSuccess()) {
|
||||
int length = result.getLength();
|
||||
int sum = 0;
|
||||
int readLength = length;
|
||||
while (sum < length) {
|
||||
byte[] dataBuff = new byte[readLength];
|
||||
DataResult dataResult = pipeline.readData(dataBuff);
|
||||
int len = dataResult.getLength();
|
||||
if (len > 0) {
|
||||
downloadPackCount++;
|
||||
downloadSize += len;
|
||||
byte[] subArray = BytesUtil.subArray(dataBuff, 0, 3);
|
||||
sum += len;
|
||||
readLength -= len;
|
||||
String tmp = BytesUtil.toHexStringLowercase(subArray);
|
||||
LogUtils.i(tag, "pack seq:" + downloadPackCount + ", data:" + tmp + ", length:" + len);
|
||||
LogUtils.i(tag, filename + " download : " + sum);
|
||||
downloadLogTv.post(() -> {
|
||||
downloadProgress = String.format("have downloadPack = %d, downloadSize:%d/%d, useTime:%d(ms)", downloadPackCount,
|
||||
downloadSize, fileInfo.getFileLength(), System.currentTimeMillis() - time);
|
||||
updateDownloadUI();
|
||||
});
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.write(dataBuff, 0, len);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
} else if (dataResult.getError() != null && dataResult.getError().errorCode().equals(DJIPipeLineError.CLOSING)) {
|
||||
LogUtils.e(tag, "Pipeline is closing,finish down");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 解析出错位置
|
||||
int position = MOPCmdHelper.parseFileFailIndex(pipeline);
|
||||
// ack
|
||||
MOPCmdHelper.sendTransFileFailAck(pipeline, position);
|
||||
// 确认是否能接着传
|
||||
if (MOPCmdHelper.parseCommonAck(pipeline)) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.seek(position);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
downloadLogTv.post(() -> downloadLogTv.setText("transfer failure"));
|
||||
downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
if (MOPCmdHelper.isFileEnd(headBuff)) {
|
||||
LogUtils.i(tag, "Download success ,updateDownloadUI");
|
||||
downloadResult = "Download success";
|
||||
updateDownloadUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
downloading = false;
|
||||
|
||||
boolean result = verifyMd5(fileInfo, DiskUtil.getDiskCacheDir(itemView.getContext(), fileInfo.getFilename()));
|
||||
downloadResult = "verify md5 :" + result;
|
||||
downloadCount++;
|
||||
if (downloadSize == fileInfo.getFileLength() && result) {
|
||||
downloadSuccessCount++;
|
||||
MOPCmdHelper.sendTransAck(pipeline, true);
|
||||
} else {
|
||||
MOPCmdHelper.sendTransAck(pipeline, false);
|
||||
}
|
||||
updateDownloadUI();
|
||||
if (autoDownloadSwitch.isChecked()) {
|
||||
downloadTv.postDelayed(() -> downloadTv.performClick(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyMd5(MOPCmdHelper.FileInfo fileInfo, File file) {
|
||||
String md5 = BytesUtil.toHexStringLowercase(fileInfo.getMd5());
|
||||
String md5_1 = BytesUtil.toHexStringLowercase(MOPCmdHelper.getMD5(file));
|
||||
String log = String.format("MOPCmdHelper.FileInfo_md5: %s, file_md5:%s", md5, md5_1);
|
||||
LogUtils.e("PipelineAdapter", log, "/MOP");
|
||||
return md5.equals(md5_1);
|
||||
}
|
||||
|
||||
private void toast(String text) {
|
||||
itemView.post(() -> ToastUtils.INSTANCE.showShortToast(text));
|
||||
}
|
||||
|
||||
public void handlerTipEvent(MOPCmdHelper.TipEvent event) {
|
||||
switch (event.getType()) {
|
||||
case MOPCmdHelper.TipEvent.UPLOAD:
|
||||
if (event.getResult() != null) {
|
||||
uploadResult = event.getResult();
|
||||
if (uploadResult.contains("Success")) {
|
||||
// hard code
|
||||
uploadSuccessCount++;
|
||||
}
|
||||
}
|
||||
if (event.getProgress() != null) {
|
||||
uploadProgress = event.getProgress();
|
||||
}
|
||||
updateUploadUI();
|
||||
break;
|
||||
case MOPCmdHelper.TipEvent.DOWNLOAD:
|
||||
if (event.getResult() != null) {
|
||||
downloadResult = event.getResult();
|
||||
}
|
||||
if (event.getProgress() != null) {
|
||||
downloadProgress = event.getProgress();
|
||||
}
|
||||
downloadLogTv.post(() -> downloadLogTv.setText(downloadLogTv.getText().toString() + "\n" + event.getResult()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void onEvent3BackgroundThread(MOPCmdHelper.FileInfo event) {
|
||||
uploadFileInfoLog = event.toString();
|
||||
updateUploadUI();
|
||||
}
|
||||
|
||||
private void updateUploadUI() {
|
||||
StringBuilder sb = new StringBuilder("Upload:" + "\n")
|
||||
.append(uploadFileInfoLog == null ? "" : uploadFileInfoLog).append("\n")
|
||||
.append(uploadProgress == null ? "" : uploadProgress).append("\n")
|
||||
.append(uploadResult == null ? "" : uploadResult).append("\n")
|
||||
.append("成功/次数:" + uploadSuccessCount + "/" + uploadCount);
|
||||
|
||||
uploadLogTv.post(() -> uploadLogTv.setText(sb.toString()));
|
||||
}
|
||||
|
||||
private void updateDownloadUI() {
|
||||
StringBuilder sb = new StringBuilder("Download:" + "\n")
|
||||
.append(downloadFileInfoLog == null ? "" : downloadFileInfoLog).append("\n")
|
||||
.append(downloadProgress == null ? "" : downloadProgress).append("\n")
|
||||
.append(downloadResult == null ? "" : downloadResult).append("\n")
|
||||
.append("成功/次数:" + downloadSuccessCount + "/" + downloadCount);
|
||||
downloadLogTv.post(() -> downloadLogTv.setText(sb.toString()));
|
||||
}
|
||||
|
||||
|
||||
private void showDialog(Context context, Pipeline pipeline) {
|
||||
View root = LayoutInflater.from(context).inflate(R.layout.dialog_mop_upload, null, false);
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle("Select File Size")
|
||||
.setView(root)
|
||||
.setPositiveButton("Confirm", (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
RadioGroup group = root.findViewById(R.id.group_file);
|
||||
int checkedRadioButtonId = group.getCheckedRadioButtonId();
|
||||
if (checkedRadioButtonId == R.id.rb_1) {
|
||||
uploadFileName = "mopSample.log";
|
||||
} else if (checkedRadioButtonId == R.id.rb_2) {
|
||||
uploadFileName = "mopSample.jpeg";
|
||||
} else if (checkedRadioButtonId == R.id.rb_3) {
|
||||
uploadFileName = "mopSample.mp4";
|
||||
}
|
||||
uploadHandler.post(() -> {
|
||||
resetUploadInfo();
|
||||
uploadFile(pipeline);
|
||||
});
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface OnDisconnectListener {
|
||||
void onDisconnect(Pipeline data);
|
||||
}
|
||||
|
||||
public interface OnEventListener {
|
||||
void onTipEvent(MOPCmdHelper.TipEvent event);
|
||||
|
||||
void onFileInfoEvent(MOPCmdHelper.FileInfo event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.v5.manager.aircraft.lte.LTEPrivatizationServerInfo
|
||||
import dji.v5.manager.areacode.AreaCode
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.DjiSharedPreferencesManager
|
||||
import dji.v5.utils.common.JsonUtil
|
||||
|
||||
object QuickTestConfig {
|
||||
|
||||
private const val KEY_LTE_AUTHENTICATION_INFO = "key_lte_authentication_info"
|
||||
private const val KEY_AC_LTE_PRIVATIZATION_SERVER_INFO = "key_ac_lte_privatization_server_info"
|
||||
private const val KEY_RC_LTE_PRIVATIZATION_SERVER_INFO = "key_rc_lte_privatization_server_info"
|
||||
|
||||
val simulatorAreaList = listOf(
|
||||
SimulatorArea("中国", LocationCoordinate2D(22.5797650, 113.9411710), AreaCode.CHINA),
|
||||
SimulatorArea("美国", LocationCoordinate2D(34.063191, -118.121621), AreaCode.UNITED_STATES_OF_AMERICA),
|
||||
SimulatorArea("日本", LocationCoordinate2D(35.658890, 139.746074), AreaCode.JAPAN),
|
||||
SimulatorArea("法国", LocationCoordinate2D(48.860284, 2.336282), AreaCode.FRANCE),
|
||||
SimulatorArea("德国", LocationCoordinate2D(52.516294, 13.376631), AreaCode.GERMANY),
|
||||
SimulatorArea("禁飞区", LocationCoordinate2D(22.645945, 113.816311), AreaCode.CHINA),
|
||||
SimulatorArea("授权区", LocationCoordinate2D(22.395237, 114.203203), AreaCode.CHINA),
|
||||
SimulatorArea("加强警告区", LocationCoordinate2D(22.208262, 114.03056), AreaCode.CHINA),
|
||||
)
|
||||
|
||||
fun getCacheLTEAuthenticationInfo(): LTEAuthCacheInfo? {
|
||||
val str = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_LTE_AUTHENTICATION_INFO, "")
|
||||
if (str.isEmpty()) {
|
||||
return LTEAuthCacheInfo()
|
||||
}
|
||||
return JsonUtil.toBean(DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_LTE_AUTHENTICATION_INFO, ""), LTEAuthCacheInfo::class.java)
|
||||
}
|
||||
|
||||
fun updateCacheLTEAuthenticationInfo(info: LTEAuthCacheInfo) {
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), KEY_LTE_AUTHENTICATION_INFO, JsonUtil.toJson(info))
|
||||
}
|
||||
|
||||
fun getCacheACLTEPrivatizationServerInfo(): LTEPrivatizationServerInfo? {
|
||||
val str = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_AC_LTE_PRIVATIZATION_SERVER_INFO, "")
|
||||
if (str.isEmpty()) {
|
||||
return LTEPrivatizationServerInfo()
|
||||
}
|
||||
return JsonUtil.toBean(DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_AC_LTE_PRIVATIZATION_SERVER_INFO, ""), LTEPrivatizationServerInfo::class.java)
|
||||
}
|
||||
|
||||
fun updateCacheACLTEPrivatizationServerInfo(info: LTEPrivatizationServerInfo) {
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), KEY_AC_LTE_PRIVATIZATION_SERVER_INFO, JsonUtil.toJson(info))
|
||||
}
|
||||
|
||||
fun getCacheRCLTEPrivatizationServerInfo(): LTEPrivatizationServerInfo? {
|
||||
val str = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_RC_LTE_PRIVATIZATION_SERVER_INFO, "")
|
||||
if (str.isEmpty()) {
|
||||
return LTEPrivatizationServerInfo()
|
||||
}
|
||||
return JsonUtil.toBean(DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_RC_LTE_PRIVATIZATION_SERVER_INFO, ""), LTEPrivatizationServerInfo::class.java)
|
||||
}
|
||||
|
||||
fun updateCacheRCLTEPrivatizationServerInfo(info: LTEPrivatizationServerInfo) {
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), KEY_RC_LTE_PRIVATIZATION_SERVER_INFO, JsonUtil.toJson(info))
|
||||
}
|
||||
|
||||
data class SimulatorArea(
|
||||
val name: String, val location: LocationCoordinate2D, val areaCode: AreaCode
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
data class LTEAuthCacheInfo(
|
||||
val phoneAreaCode: String = "86",
|
||||
val phoneNumber: String = "1234567890",
|
||||
val verificationCode: String = "123456"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dji.sampleV5.aircraft.R
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
|
||||
import dji.v5.utils.common.LogUtils
|
||||
|
||||
/**
|
||||
* Description :基站RTK的Adapter,展示扫码到的基站情况
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/3/6
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class RtkStationScanAdapter(val context: Context, list: List<DJIRTKBaseStationConnectInfo>?) :
|
||||
RecyclerView.Adapter<RtkStationScanAdapter.RtkViewHolder>() {
|
||||
|
||||
private val LEVEL_0 = 0
|
||||
private val LEVEL_1 = 1
|
||||
private val LEVEL_2 = 2
|
||||
private val LEVEL_3 = 3
|
||||
private val LEVEL_4 = 4
|
||||
private var baseStationInfoList: List<DJIRTKBaseStationConnectInfo>? = list
|
||||
private val TAG = "RtkStationScanAdapter"
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RtkViewHolder {
|
||||
val view: View = LayoutInflater.from(context)
|
||||
.inflate(R.layout.adapter_rtk_connect_status_item, parent, false)
|
||||
return RtkViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RtkViewHolder, position: Int) {
|
||||
baseStationInfoList?.get(position)?.let { info ->
|
||||
holder.mRtkStationNameTv.text = info.rtkStationName
|
||||
holder.mConnectSignalIv.setBackgroundResource(getSignalLevelDrawable(info.signalLevel))
|
||||
holder.itemView.setOnClickListener {
|
||||
val pos = holder.layoutPosition
|
||||
val isConnecting = checkConnecting()
|
||||
val hasConnected = checkConnected(pos)
|
||||
if (!isConnecting && !hasConnected) {//上一笔连接还没结束或者基站已连接则不响应新的连接请求
|
||||
mOnItemClickListener?.onItemClick(holder.itemView, pos)
|
||||
} else if (checkConnecting()) {
|
||||
ToastUtils.showToast("The station is currently connecting, please try to connect later!")
|
||||
} else {
|
||||
ToastUtils.showToast("The station has connected!")
|
||||
}
|
||||
}
|
||||
when (info.connectStatus) {
|
||||
RTKStationConnetState.IDLE -> {
|
||||
holder.mConnectStatusIv.gone()
|
||||
}
|
||||
RTKStationConnetState.CONNECTING -> {
|
||||
holder.mConnectStatusIv.visible()
|
||||
holder.mConnectStatusIv.setImageResource(R.drawable.ic_rotate_progress_circle)
|
||||
}
|
||||
RTKStationConnetState.CONNECTED -> {
|
||||
holder.mConnectStatusIv.visible()
|
||||
holder.mConnectStatusIv.setImageResource(R.drawable.ic_confirm)
|
||||
}
|
||||
else -> {
|
||||
holder.mConnectStatusIv.gone()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (baseStationInfoList == null) 0 else baseStationInfoList!!.size
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(view: View?, position: Int)
|
||||
}
|
||||
|
||||
private var mOnItemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(onItemClickListener: OnItemClickListener?) {
|
||||
mOnItemClickListener = onItemClickListener
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getSignalLevelDrawable(signalLevel: Int): Int {
|
||||
LogUtils.i(TAG, "getSignalLevelDrawable,signalLevel=$signalLevel")
|
||||
return when (signalLevel) {
|
||||
LEVEL_0 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_0
|
||||
LEVEL_1 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_1
|
||||
LEVEL_2 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_2
|
||||
LEVEL_3 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_3
|
||||
LEVEL_4 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_4
|
||||
else -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RtkViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var mConnectStatusIv: ImageView
|
||||
var mRtkStationNameTv: TextView
|
||||
var mConnectSignalIv: ImageView
|
||||
|
||||
init {
|
||||
mConnectStatusIv = itemView.findViewById(R.id.connect_status_iv)
|
||||
mRtkStationNameTv = itemView.findViewById(R.id.station_name_tv)
|
||||
mConnectSignalIv = itemView.findViewById(R.id.connect_signal_iv)
|
||||
mConnectStatusIv.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.visible() {
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.gone() {
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun checkConnecting(): Boolean {
|
||||
baseStationInfoList?.let {
|
||||
for (station in it) {
|
||||
if (station.connectStatus == RTKStationConnetState.CONNECTING) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkConnected(position: Int): Boolean {
|
||||
baseStationInfoList?.run {
|
||||
val stationInfo = get(position)
|
||||
if (stationInfo.connectStatus == RTKStationConnetState.CONNECTED) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import dji.sampleV5.aircraft.R
|
||||
import dji.sdk.errorcode.DJIErrorCode
|
||||
import dji.v5.utils.common.StringUtils
|
||||
|
||||
enum class SecurityCodeOperationResult(val resultCode: Int, @param:StringRes val desResId: Int) {
|
||||
NO_ERR(0, R.string.securitycode_operation_result_success),
|
||||
BUSY(DJIErrorCode.ACCESS_LOCKER_V1_BUSY.value(), R.string.securitycode_operation_result_busy),
|
||||
SET_FAILED(DJIErrorCode.ACCESS_LOCKER_V1_PW_SET_FAILED.value(), R.string.securitycode_operation_result_set_failed),
|
||||
VERIFY_FAILED(DJIErrorCode.ACCESS_LOCKER_V1_PW_VERIFY_FAILED.value(), R.string.securitycode_operation_result_verify_failed),
|
||||
NEW_PW_REPEAT(DJIErrorCode.ACCESS_LOCKER_V1_NEW_PW_REPEAT.value(), R.string.securitycode_operation_result_new_pw_repeat),
|
||||
RESET_FAILED(DJIErrorCode.ACCESS_LOCKER_V1_RESET_FAILED.value(), R.string.securitycode_operation_result_reset_failed),
|
||||
USERNAME_INVALID(DJIErrorCode.ACCESS_LOCKER_USER_NAME_FORMAT_INVALID.value(), R.string.securitycode_operation_result_user_name_format_invalid),
|
||||
CONTROL_NOT_SUPPORT(DJIErrorCode.ACCESS_LOCKER_V1_CONTROL_NOT_SUPPORT.value(), R.string.securitycode_operation_result_control_not_support),
|
||||
FEATURE_NOT_SUPPORT(DJIErrorCode.FEATURE_NOT_SUPPORTED.value(), R.string.securitycode_operation_result_feature_not_support),
|
||||
COMMAND_NOT_SUPPORT(DJIErrorCode.COMMAND_NOT_SUPPORT_NOW.value(), R.string.securitycode_operation_result_command_not_support),
|
||||
NOT_CURRENT_DEVICE(0xFFFF, R.string.securitycode_operation_result_not_current_device_result),
|
||||
UNKNOWN(DJIErrorCode.UNKNOWN.value(), R.string.securitycode_operation_result_unknown);
|
||||
|
||||
val resultDes: String
|
||||
get() = StringUtils.getResStr(desResId)
|
||||
|
||||
companion object {
|
||||
fun find(code: Int): SecurityCodeOperationResult {
|
||||
for (operationResult in values()) {
|
||||
if (operationResult.resultCode == code) {
|
||||
return operationResult
|
||||
}
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.v5.common.video.channel.VideoChannelState
|
||||
import dji.v5.common.video.channel.VideoChannelType
|
||||
import dji.v5.common.video.decoder.DecoderState
|
||||
import dji.v5.common.video.stream.StreamSource
|
||||
|
||||
data class VideoChannelInfo(var videoChannelState: VideoChannelState = VideoChannelState.CLOSE) {
|
||||
var streamSource: StreamSource? = null
|
||||
var videoChannelType: VideoChannelType = VideoChannelType.PRIMARY_STREAM_CHANNEL
|
||||
var decoderState: DecoderState = DecoderState.INITIALIZED
|
||||
var resolution: String = DEFAULT_STR
|
||||
var format: String = DEFAULT_STR
|
||||
var fps: Int = -1
|
||||
var bitRate: Int = -1
|
||||
var socket: String = DEFAULT_STR
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package dji.sampleV5.aircraft.data.source
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.network.DJIHttpCallback
|
||||
import dji.v5.network.DJIHttpRequest
|
||||
import dji.v5.network.DJIHttpResponse
|
||||
import dji.v5.network.DJINetworkManager
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.JsonUtil
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 该Repository负责简单的HTTP数据获取能力,以及数据存储和获取能力
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
class VersionInfoRepository {
|
||||
|
||||
|
||||
/**
|
||||
* 用于观察网路模块(DJINetworkManager)是否初始化完成,并不关心网络实际是否有效
|
||||
*/
|
||||
private val networkInitSubject: BehaviorSubject<Boolean> = BehaviorSubject.create()
|
||||
|
||||
private var hostConfig: HostConfig? = null
|
||||
|
||||
private var currentVersionInfo: VersionInfo? = null
|
||||
|
||||
init {
|
||||
Completable.create { emitter ->
|
||||
DJINetworkManager.getInstance().addNetworkStatusListener {
|
||||
if (it) {
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
}.mergeWith(Completable.fromAction {
|
||||
hostConfig = getHostConfig()
|
||||
}.observeOn(Schedulers.io())).subscribe {
|
||||
// 通知网络初始化完成
|
||||
networkInitSubject.onNext(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本信息
|
||||
*/
|
||||
fun fetchCurrentVersionInfo(versionName: String, language: String = getLanguageHeaderValue()): Single<VersionInfo> {
|
||||
val versionNameEx = getCurrentVersionExTest().ifEmpty { versionName }
|
||||
LogUtils.i(LOG_TAG, "getCurrentVersionInfo, $versionNameEx")
|
||||
|
||||
val fileName = "version_info_${versionNameEx}_${language}.json"
|
||||
return Single.create<VersionInfo> {
|
||||
if (currentVersionInfo == null) {
|
||||
val currentVersionInfo = loadVersionInfoFromLocal(fileName)
|
||||
this.currentVersionInfo = currentVersionInfo
|
||||
it.onSuccess(currentVersionInfo)
|
||||
return@create
|
||||
}
|
||||
it.onError(VersionInfoCacheFileNotFound())
|
||||
}.onErrorResumeNext { fetchVersionInfoAndSave(versionNameEx, fileName, false, language) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制拉去最新版本信息
|
||||
*/
|
||||
fun fetchLatestVersionInfo(versionName: String, language: String = getLanguageHeaderValue()): Observable<VersionInfo> {
|
||||
return Observable.create { emitter ->
|
||||
val fileName = "${FILE_NAME_LATEST_VERSION_INFO_CACHE}_${language}.json"
|
||||
// 1. 从本地文件加载并通知给客户端,用于尽快刷新UI
|
||||
Schedulers.io().scheduleDirect {
|
||||
try {
|
||||
val versionInfo = loadVersionInfoFromLocal(fileName)
|
||||
emitter.onNext(versionInfo)
|
||||
} catch (throwable: Throwable) {
|
||||
//忽略
|
||||
LogUtils.i(LOG_TAG, "error, $throwable")
|
||||
}
|
||||
}
|
||||
// 2. 从云端获取最新数据,需要则通知给客户端
|
||||
val fetchD = fetchVersionInfoAndSave(versionName, fileName, true, language)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
emitter.onNext(it)
|
||||
emitter.onComplete()
|
||||
}, {
|
||||
if (emitter.isDisposed) {
|
||||
return@subscribe
|
||||
}
|
||||
emitter.onError(it)
|
||||
})
|
||||
// 3. 请求被取消时取消云端请求
|
||||
emitter.setCancellable {
|
||||
fetchD.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchVersionInfoAndSave(
|
||||
versionName: String,
|
||||
fileNameForCache: String,
|
||||
isLatest: Boolean,
|
||||
language: String,
|
||||
): Single<VersionInfo> {
|
||||
return fetchVersionInfoFromRemote(versionName, isLatest, language)
|
||||
.delaySubscription(networkInitSubject.filter {
|
||||
it
|
||||
}.firstOrError())
|
||||
.observeOn(Schedulers.io())
|
||||
.doOnSuccess {
|
||||
saveVersionInfoToLocal(fileNameForCache, it)
|
||||
currentVersionInfo = it
|
||||
}
|
||||
.onErrorResumeNext { Single.error(FetchVersionInfoError()) }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
private fun loadVersionInfoFromLocal(fileName: String): VersionInfo {
|
||||
// 获取文件
|
||||
val versionInfoFile = ContextUtil.getContext().getFileStreamPath(fileName)
|
||||
|
||||
// 判断本地是否有该文件,加载本地文件
|
||||
if (!versionInfoFile.exists()) {
|
||||
// 不存在,返回空列表
|
||||
throw VersionInfoCacheFileNotFound()
|
||||
}
|
||||
|
||||
// 存在,则加载本地文件
|
||||
return JsonUtil.toBean(versionInfoFile, VersionInfo::class.java) ?: throw VersionInfoCacheFileNotFound()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
private fun saveVersionInfoToLocal(fileName: String, versionInfoList: VersionInfo) {
|
||||
// 获取文件
|
||||
val versionInfoFile = ContextUtil.getContext().getFileStreamPath(fileName)
|
||||
|
||||
// 重新创建文件
|
||||
FileUtils.delFile(versionInfoFile, false)
|
||||
FileUtils.createFile(versionInfoFile)
|
||||
|
||||
// 将版本信息写入文件
|
||||
versionInfoFile.writer().use {
|
||||
it.write(JsonUtil.toJson(versionInfoList) ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从云端拉取版本信息
|
||||
* @param versionName 当前版本信息
|
||||
* @param isLatest true:拉取当前大版本(major version)的最新版本信息;false:拉取versionName所指定版本信息(可能拉取到空数据)
|
||||
*/
|
||||
private fun fetchVersionInfoFromRemote(versionName: String, isLatest: Boolean, l: String): Single<VersionInfo> {
|
||||
return Single.create {
|
||||
DJINetworkManager.getInstance().enqueue(buildHttpRequestExTest(versionName, isLatest, l), object : DJIHttpCallback<DJIHttpResponse> {
|
||||
override fun onFailure(error: IDJIError) {
|
||||
LogUtils.i(LOG_TAG, "error, ${error.errorCode()} ${error.errorType()} ${error.description()}")
|
||||
it.onError(FetchVersionInfoError())
|
||||
}
|
||||
|
||||
override fun onResponse(response: DJIHttpResponse) {
|
||||
LogUtils.i(LOG_TAG, "success, ${response.code()}")
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
it.onError(FetchVersionInfoError())
|
||||
return
|
||||
}
|
||||
val httpResult = HttpResult.fromJson(response.body())
|
||||
if (httpResult == null) {
|
||||
it.onError(FetchVersionInfoError())
|
||||
return
|
||||
}
|
||||
LogUtils.i(LOG_TAG, "success, $httpResult")
|
||||
it.onSuccess(httpResult.data)
|
||||
}
|
||||
|
||||
override fun onLoading(current: Long, total: Long) {
|
||||
// super.onLoading(current, total)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据入参构建不同的请求体
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun buildHttpRequest(host: String, versionName: String, isLatest: Boolean, language: String): DJIHttpRequest {
|
||||
val versionSplits = versionName.split(".").toMutableList()
|
||||
|
||||
// 检查VersionName是否是三段式版本号,不是则补齐
|
||||
val diff = 3 - versionSplits.size
|
||||
for (i in 0 until diff) {
|
||||
versionSplits.add("0")
|
||||
}
|
||||
|
||||
val httpRequest = DJIHttpRequest.Builder.newBuilder()
|
||||
.requestType(DJIHttpRequest.RequestType.GET)
|
||||
.url(
|
||||
if (isLatest) {
|
||||
"$host$URL_PATH_TCH_LATEPATHST_VERSION_INFO"
|
||||
} else {
|
||||
"$host$URL_PATH_FETCH_CURRENT_VERSION_INFO"
|
||||
}
|
||||
)
|
||||
.params(
|
||||
if (isLatest) {
|
||||
mapOf(
|
||||
"sdk_name" to "mobile sdk",
|
||||
"platform" to "android",
|
||||
"major_version" to versionSplits[0],
|
||||
)
|
||||
} else {
|
||||
mapOf(
|
||||
"sdk_name" to "mobile sdk",
|
||||
"platform" to "android",
|
||||
"major_version" to versionSplits[0],
|
||||
"minor_version" to versionSplits[1],
|
||||
"patch_version" to versionSplits[2],
|
||||
)
|
||||
}
|
||||
)
|
||||
.headers(mapOf("Language" to language, "content-type" to "application/json"))
|
||||
.build()
|
||||
LogUtils.i(LOG_TAG, "request, $httpRequest")
|
||||
return httpRequest
|
||||
}
|
||||
|
||||
private fun getHostConfig(): HostConfig? {
|
||||
val hostConfigFile = getHostConfigFile() ?: return null
|
||||
JsonUtil.toBean(hostConfigFile, HostConfig::class.java)?.let {
|
||||
// 做一个简单的URL校验
|
||||
if (it.host.endsWith('/') && it.host.startsWith("https://")) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getHostConfigFile(): File? {
|
||||
val dir = ContextUtil.getContext().getExternalFilesDir("")
|
||||
val configFile = File(dir, FILE_NAME_HOST_VERSION_CONFIG)
|
||||
if (configFile.exists()) {
|
||||
return configFile
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun buildHttpRequestExTest(versionName: String, isLatest: Boolean, language: String): DJIHttpRequest {
|
||||
hostConfig?.let {
|
||||
return buildHttpRequest(it.host, it.versionName.ifEmpty { versionName }, isLatest, language)
|
||||
}
|
||||
return buildHttpRequest(URL_HOST, versionName, isLatest, language)
|
||||
}
|
||||
|
||||
/**
|
||||
* 目前MSDK只有中英文,经测试发现
|
||||
* - string.xml 只有在系统设置为简体中文(国家无所谓)时才会选择中文的string.xml
|
||||
* 所以该处添加两项判断
|
||||
*/
|
||||
private fun getLanguageHeaderValue(): String {
|
||||
//"ZH" means Chinese
|
||||
val currentLocale = Locale.getDefault()
|
||||
val country: String = currentLocale.country.lowercase(Locale.ROOT)
|
||||
LogUtils.i(LOG_TAG,currentLocale.language,currentLocale.script)
|
||||
return if ("zh".equals(currentLocale.language, true) && "cn".equals(country)) {
|
||||
"cn"
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentVersionExTest(): String {
|
||||
return hostConfig?.versionName ?: ""
|
||||
}
|
||||
|
||||
class VersionInfoCacheFileNotFound : Exception()
|
||||
class FetchVersionInfoError : Exception()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 服务器host
|
||||
*/
|
||||
private const val URL_HOST = "https://dev.dji.com/"
|
||||
|
||||
/**
|
||||
* 获取最新版本信息
|
||||
*/
|
||||
private const val URL_PATH_TCH_LATEPATHST_VERSION_INFO = "api/v1/release/latest"
|
||||
|
||||
/**
|
||||
* 获取指定版本信息
|
||||
*/
|
||||
private const val URL_PATH_FETCH_CURRENT_VERSION_INFO = "api/v1/release"
|
||||
|
||||
/**
|
||||
* 用于存储最新版本信息的文件
|
||||
*/
|
||||
private const val FILE_NAME_LATEST_VERSION_INFO_CACHE = "version_info_latest"
|
||||
|
||||
/**
|
||||
* 提供给测试使用的配置文件路径
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
private const val FILE_NAME_HOST_VERSION_CONFIG = "config_host_version.json"
|
||||
private const val LOG_TAG = "VersionInfoRepository"
|
||||
}
|
||||
}
|
||||
|
||||
private data class HostConfig(
|
||||
@SerializedName("host")
|
||||
val host: String = "",
|
||||
@SerializedName("version")
|
||||
val versionName: String = "",
|
||||
)
|
||||
|
||||
data class HttpResult(val code: Int, val message: String, val data: VersionInfo) {
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String): HttpResult? {
|
||||
return JsonUtil.toBean(json, HttpResult::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "HttpResult(code=$code, message='$message', data='$data')"
|
||||
}
|
||||
}
|
||||
|
||||
data class VersionInfo(
|
||||
val versionCode: Int = 0,
|
||||
@SerializedName("MajorVersion")
|
||||
val versionMajor: String = "",
|
||||
@SerializedName("MinorVersion")
|
||||
val versionMinor: String = "",
|
||||
@SerializedName("PatchVersion")
|
||||
val versionPatch: String = "",
|
||||
|
||||
/**
|
||||
* UTC时间戳(单位s)
|
||||
*/
|
||||
@SerializedName("ReleaseDate")
|
||||
val releaseTimeStamp: Long = 0,
|
||||
@SerializedName("SupportedProducts")
|
||||
val supportProducts: String = "",
|
||||
@SerializedName("Highlights")
|
||||
val releaseNode: String = "",
|
||||
) {
|
||||
val versionName: String
|
||||
get() {
|
||||
return "$versionMajor.$versionMinor.$versionPatch"
|
||||
}
|
||||
}
|
||||
10
sample/src/main/java/dji/sampleV5/aircraft/data/utils.kt
Normal file
10
sample/src/main/java/dji/sampleV5/aircraft/data/utils.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
const val DEFAULT_STR = "N/A"
|
||||
const val DEFAULT_RES_ID = -1
|
||||
const val IN_INNER_NETWORK_STR = "IN_INNER"
|
||||
const val IN_OUT_NETWORK_STR = "IN_OUT"
|
||||
const val NO_NETWORK_STR = "NO_NETWORK"
|
||||
const val ONLINE_STR = "ONLINE"
|
||||
const val MAIN_FRAGMENT_PAGE_TITLE = "MAIN_FRAGMENT_PAGE_TITLE"
|
||||
const val MEDIA_FILE_DETAILS_STR = "MEDIA_FILE_DETAILS"
|
||||
@@ -0,0 +1,173 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
|
||||
import dji.sdk.keyvalue.key.ComponentType
|
||||
import dji.v5.manager.capability.CapabilityManager
|
||||
import dji.v5.manager.capability.CapabilityParser
|
||||
import dji.v5.utils.common.*
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.CompletableObserver
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.lang.Exception
|
||||
import java.lang.StringBuilder
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/09/13 5:43 下午
|
||||
* @description: 能力集key 测试,结果中只展示未通过的key
|
||||
* 结果存储在包目录/keycheck/result.txt
|
||||
*/
|
||||
object CapabilityKeyChecker {
|
||||
|
||||
|
||||
private val TAG = LogUtils.getTag(this)
|
||||
|
||||
|
||||
/**
|
||||
* 根据key名称从能力集获取测试用例json集合 每个对象可反序列化为key的参数对象
|
||||
*/
|
||||
fun getKeyParamList(keyName: String): MutableList<String> {
|
||||
return CapabilityParser.getInstance().getValueParamList(keyName)
|
||||
}
|
||||
|
||||
|
||||
fun getKeyItem(keyName: String): KeyItem<*, *>? {
|
||||
val allList: MutableList<KeyItem<*, *>> = ArrayList()
|
||||
var item: KeyItem<*, *>? = null
|
||||
KeyItemDataUtil.getAllKeyList(allList)
|
||||
allList.forEach {
|
||||
if (it.toString() == keyName) {
|
||||
item = it;
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
data class ItemDecoder(
|
||||
var componetIndex: Int = 0, // LEFT_OR_MAIN
|
||||
var subComponetType: Int = 65534, // DEFAULT
|
||||
var subComponetIndex: Int = 0,
|
||||
var jsonString: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取枚举对应的Json list字符串
|
||||
* eg:CameraMode 对象 obj cameramodeMsg field[obj] CameraMode
|
||||
*/
|
||||
fun getDJIValueBeanStr(item: KeyItem<*, *>): String {
|
||||
var tagBegin = "{\"valueParamList\": ["
|
||||
var tagEnd = "]}"
|
||||
try {
|
||||
val pFields = item.param?.javaClass?.declaredFields
|
||||
if (pFields != null) {
|
||||
for (field in pFields) {
|
||||
|
||||
field.isAccessible = true
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
val itemList =
|
||||
(item.subItemMap as Map<String?, List<EnumItem>>)[clazz.canonicalName]!!
|
||||
var jsonList = StringBuilder(tagBegin)
|
||||
itemList.forEach {
|
||||
field[item.param] = KeyItemHelper.getEnumData(
|
||||
clazz as Class<Enum<*>>,
|
||||
it.getName().toString()
|
||||
)
|
||||
var jsonString =
|
||||
"\"" + item.param.toString().replace("\"", "\\\"") + "\""
|
||||
|
||||
var result = jsonString + ","
|
||||
if (!result.contains("65535")) {//过滤unknown
|
||||
jsonList.append(result)
|
||||
}
|
||||
}
|
||||
jsonList.deleteAt(jsonList.lastIndex)
|
||||
jsonList.append(tagEnd)
|
||||
return jsonList.toString()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(KeyItemHelper.TAG, e.message)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据item生成 枚举类型的json文件
|
||||
*/
|
||||
fun generateAllEnumList(productType: String) {
|
||||
val allList: MutableList<KeyItem<*, *>> = ArrayList()
|
||||
DJIExecutor.getExecutorFor(DJIExecutor.Purpose.IO).execute {
|
||||
KeyItemDataUtil.getAllKeyList(allList)
|
||||
allList
|
||||
.filter {
|
||||
val keyName = "Key$it"
|
||||
it.canSet()
|
||||
&& CapabilityManager.getInstance().isKeySupported(
|
||||
productType,
|
||||
"",
|
||||
ComponentType.find(it.getKeyInfo().componentType),
|
||||
keyName
|
||||
)
|
||||
|
||||
}
|
||||
.forEach() { item ->
|
||||
val jsonStr = getDJIValueBeanStr(item)
|
||||
if (jsonStr.isNotEmpty()) {
|
||||
var filePath = DiskUtil.getExternalCacheDirPath(
|
||||
ContextUtil.getContext(),
|
||||
"keycheck/$item.json"
|
||||
)
|
||||
FileUtils.writeFile(filePath, jsonStr, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOneType(
|
||||
productType: String,
|
||||
componentTypeName: String,
|
||||
keyCheckType: KeyCheckType,
|
||||
componentIndex: Int
|
||||
): Completable {
|
||||
|
||||
var keyOperatorCommand = when (keyCheckType) {
|
||||
KeyCheckType.SET -> KeySetCommand(productType, componentTypeName, componentIndex)
|
||||
KeyCheckType.ACTION -> KeyActionCommand(productType, componentTypeName, componentIndex)
|
||||
KeyCheckType.GET -> KeyGetCommand(productType, componentTypeName, componentIndex)
|
||||
}
|
||||
return keyOperatorCommand.execute()
|
||||
}
|
||||
|
||||
fun check(
|
||||
productType: String,
|
||||
componentTypeName: String,
|
||||
componentIndex: Int
|
||||
) {
|
||||
checkOneType(productType, componentTypeName, KeyCheckType.SET, componentIndex)
|
||||
.andThen(checkOneType(productType, componentTypeName, KeyCheckType.SET, componentIndex))
|
||||
.andThen(checkOneType(productType, componentTypeName, KeyCheckType.ACTION, componentIndex))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : CompletableObserver {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
LogUtils.e(TAG, "begin check")
|
||||
ToastUtils.showToast("begin check")
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
LogUtils.e(TAG, "-check finish-")
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
LogUtils.e(TAG, "check error${e.message}")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/11 9:42 上午
|
||||
* @description:
|
||||
*/
|
||||
enum class ChannelType(name: String) {
|
||||
/**
|
||||
* 电池
|
||||
*/
|
||||
CHANNEL_TYPE_BATTERY("BATTERY"),
|
||||
|
||||
/**
|
||||
* 云台
|
||||
*/
|
||||
CHANNEL_TYPE_GIMBAL("GIMBAL"),
|
||||
|
||||
/**
|
||||
* 相机
|
||||
*/
|
||||
CHANNEL_TYPE_CAMERA("CAMERA"),
|
||||
|
||||
|
||||
/**
|
||||
* Airlink
|
||||
*/
|
||||
CHANNEL_TYPE_AIRLINK("AIRLINK"),
|
||||
|
||||
/**
|
||||
* Flight Assistant
|
||||
*/
|
||||
CHANNEL_TYPE_FLIGHT_ASSISTANT("ASSISTANT"),
|
||||
|
||||
/**
|
||||
* Flight Control
|
||||
*/
|
||||
CHANNEL_TYPE_FLIGHT_CONTROL("FLIGHT CONTROL"),
|
||||
|
||||
/**
|
||||
* Remote Controller
|
||||
*/
|
||||
CHANNEL_TYPE_REMOTE_CONTROLLER("REMOTE CONTROLLER"),
|
||||
|
||||
/**
|
||||
* BLE
|
||||
*/
|
||||
CHANNEL_TYPE_BLE("BLE"),
|
||||
|
||||
/**
|
||||
* RTK
|
||||
*/
|
||||
CHANNEL_TYPE_RTK_BASE_STATION("RTK BASE STATION"),
|
||||
|
||||
/**
|
||||
* RTK
|
||||
*/
|
||||
CHANNEL_TYPE_RTK_MOBILE_STATION("RTK MOBILE STATION"),
|
||||
|
||||
/**
|
||||
* Product
|
||||
*/
|
||||
CHANNEL_TYPE_PRODUCT("PRODUCT"),
|
||||
|
||||
/**
|
||||
* OcuSync
|
||||
*/
|
||||
CHANNEL_TYPE_OCU_SYNC("OCU SYNC"),
|
||||
|
||||
/**
|
||||
* Radar
|
||||
*/
|
||||
CHANNEL_TYPE_RADAR("RADAR"),
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Mobile Network
|
||||
*/
|
||||
CHANNEL_TYPE_MOBILE_NETWORK("MOBILE NETWORK"),
|
||||
|
||||
|
||||
/**
|
||||
* on board
|
||||
*/
|
||||
CHANNEL_TYPE_ON_BOARD("BOARD"),
|
||||
|
||||
/**
|
||||
* Payload
|
||||
*/
|
||||
CHANNEL_TYPE_ON_PAYLOAD("PAYLOAD"),
|
||||
|
||||
/**
|
||||
* lidar
|
||||
*/
|
||||
CHANNEL_TYPE_LIDAR("LIDAR"),
|
||||
|
||||
/**
|
||||
* IntelligentBox
|
||||
*/
|
||||
INTELLIGENT_BOX("INTELLIGENT BOX");
|
||||
|
||||
private val value: String
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
|
||||
init {
|
||||
value = name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/10 9:58 上午
|
||||
* @description:
|
||||
*/
|
||||
class EnumItem : Serializable {
|
||||
/**
|
||||
* 需要显示的条目描述
|
||||
*/
|
||||
private var description: String? = null
|
||||
|
||||
/**
|
||||
* SDK对应的枚举
|
||||
*/
|
||||
private var name: String? = null
|
||||
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
private var selected = false
|
||||
fun getName(): String? {
|
||||
return name
|
||||
}
|
||||
|
||||
fun setName(name: String?) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun getDescription(): String? {
|
||||
return description
|
||||
}
|
||||
|
||||
fun setDescription(description: String?) {
|
||||
this.description = description
|
||||
}
|
||||
|
||||
fun isSelected(): Boolean {
|
||||
return selected
|
||||
}
|
||||
|
||||
fun setSelected(selected: Boolean) {
|
||||
this.selected = selected
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 876323262645176354L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:25 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeyActionCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) : KeyOperatorCommand(productType, componentTypeName, componentIndex) {
|
||||
|
||||
private val TAG_GET = "【ACTION】"
|
||||
private val TAG_ERROR = "ActionErrorMsg"
|
||||
|
||||
override fun filter(item: KeyItem<*, *>): Boolean {
|
||||
return item.canAction()
|
||||
}
|
||||
|
||||
override fun run(item: KeyItem<*, *>) {
|
||||
super.doKeyParam(item, KeyCheckType.ACTION)
|
||||
}
|
||||
|
||||
override fun getTAG(): String {
|
||||
return TAG_GET
|
||||
}
|
||||
|
||||
override fun getErrorTAG(): String {
|
||||
return TAG_ERROR
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import dji.sdk.keyvalue.key.DJIActionKeyInfo;
|
||||
import dji.sdk.keyvalue.key.DJIKey;
|
||||
import dji.sdk.keyvalue.key.DJIKeyInfo;
|
||||
import dji.sdk.keyvalue.key.KeyTools;
|
||||
import dji.v5.common.callback.CommonCallbacks;
|
||||
import dji.v5.manager.KeyManager;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
|
||||
public class KeyBaseStructure<P, R> {
|
||||
|
||||
private static final String TAG = KeyBaseStructure.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* 设置参数
|
||||
*/
|
||||
protected P param;
|
||||
|
||||
/**
|
||||
* 返回结果
|
||||
*/
|
||||
protected R result;
|
||||
|
||||
/**
|
||||
* 推送数据记录
|
||||
*/
|
||||
protected String listenRecord = "";
|
||||
|
||||
/**
|
||||
* 推送Listener宿主
|
||||
*/
|
||||
protected Object listenHolder;
|
||||
|
||||
public int getComponetIndex() {
|
||||
return componetIndex;
|
||||
}
|
||||
|
||||
public void setComponetIndex(int componetIndex) {
|
||||
this.componetIndex = componetIndex;
|
||||
}
|
||||
|
||||
public int getSubComponetType() {
|
||||
return subComponetType;
|
||||
}
|
||||
|
||||
public void setSubComponetType(int subComponetType) {
|
||||
this.subComponetType = subComponetType;
|
||||
}
|
||||
|
||||
public int getSubComponetIndex() {
|
||||
return subComponetIndex;
|
||||
}
|
||||
|
||||
public void setSubComponetIndex(int subComponetIndex) {
|
||||
this.subComponetIndex = subComponetIndex;
|
||||
}
|
||||
|
||||
protected int componetIndex = -1;
|
||||
|
||||
protected int subComponetType = -1;
|
||||
|
||||
protected int subComponetIndex = -1;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举列表
|
||||
*/
|
||||
protected Map<String, List<EnumItem>> subItemMap = new HashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* 通过反射获取泛型类型数据并实例化
|
||||
*/
|
||||
protected void initGenericInstance() {
|
||||
try {
|
||||
KeyItemHelper.INSTANCE.initClassData(param);
|
||||
KeyItemHelper.INSTANCE.initClassData(result);
|
||||
initSubItemData();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果为枚举或者枚举嵌套列表,则初始化该列表数据
|
||||
*/
|
||||
protected void initSubItemData() {
|
||||
if (param == null) {
|
||||
return;
|
||||
}
|
||||
subItemMap.clear();
|
||||
subItemMap.putAll(KeyItemHelper.INSTANCE.initSubItemData(param));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取需要设置的参数实例
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public P getParam() {
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数映射列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, List<EnumItem>> getSubItemMap() {
|
||||
return subItemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推送数据记录
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getListenRecord() {
|
||||
return listenRecord;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 异步get
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param getCallback
|
||||
*/
|
||||
protected void get(DJIKeyInfo<R> keyInfo, CommonCallbacks.CompletionCallbackWithParam<R> getCallback) {
|
||||
DJIKey<R> key = createKey(keyInfo);
|
||||
KeyManager.getInstance().getValue(key, getCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步get
|
||||
*
|
||||
* @param keyInfo
|
||||
* @return
|
||||
*/
|
||||
protected R get(DJIKeyInfo<R> keyInfo) {
|
||||
DJIKey<R> key = createKey(keyInfo);
|
||||
return KeyManager.getInstance().getValue(key);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param setCallback
|
||||
*/
|
||||
protected void set(DJIKeyInfo<P> keyInfo, P param, CommonCallbacks.CompletionCallback setCallback) {
|
||||
|
||||
DJIKey<P> key = createKey(keyInfo);
|
||||
KeyManager.getInstance().setValue(key, param, setCallback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置Listener
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param listenHolder
|
||||
* @param listenCallback
|
||||
*/
|
||||
protected void listen(DJIKeyInfo<R> keyInfo, Object listenHolder, CommonCallbacks.KeyListener<R> listenCallback) {
|
||||
this.listenHolder = listenHolder;
|
||||
|
||||
DJIKey<R> key = createKey(keyInfo);
|
||||
KeyManager.getInstance().listen(key, listenHolder, listenCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消Listener
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param listenHolder
|
||||
*/
|
||||
protected void cancelListen(DJIKeyInfo<R> keyInfo, Object listenHolder) {
|
||||
|
||||
KeyManager.getInstance().cancelListen(createKey(keyInfo), listenHolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参action
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param param
|
||||
* @param actonCallback
|
||||
*/
|
||||
protected void action(DJIActionKeyInfo<P, R> keyInfo, P param, CommonCallbacks.CompletionCallbackWithParam<R> actonCallback) {
|
||||
|
||||
DJIKey.ActionKey<P,R> key = createActionKey(keyInfo);
|
||||
KeyManager.getInstance().performAction(key, param, actonCallback);
|
||||
}
|
||||
|
||||
protected DJIKey.ActionKey<P,R> createActionKey(DJIActionKeyInfo<P,R> keyInfo) {
|
||||
DJIKey.ActionKey<P,R> key = null;
|
||||
key = KeyTools.createKey(keyInfo, 0, getComponetIndex(),getSubComponetType(), getSubComponetIndex());
|
||||
return key;
|
||||
}
|
||||
|
||||
protected<Parame> DJIKey<Parame> createKey(DJIKeyInfo<Parame> keyInfo ) {
|
||||
DJIKey<Parame> key = null;
|
||||
key = KeyTools.createKey(keyInfo, 0 , getComponetIndex(),getSubComponetType(), getSubComponetIndex());
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:09 下午
|
||||
* @description:
|
||||
*/
|
||||
enum class KeyCheckType {
|
||||
GET,
|
||||
SET,
|
||||
ACTION
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:25 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeyGetCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) : KeyOperatorCommand(productType, componentTypeName, componentIndex) {
|
||||
|
||||
private val TAG_GET = "【GET】"
|
||||
private val TAG_ERROR = "GetErrorMsg"
|
||||
|
||||
override fun filter(item: KeyItem<*, *>): Boolean {
|
||||
return item.canGet()
|
||||
}
|
||||
|
||||
override fun run(item: KeyItem<*, *>) {
|
||||
super.doKeyParam(item, KeyCheckType.GET)
|
||||
}
|
||||
|
||||
override fun getTAG(): String {
|
||||
return TAG_GET
|
||||
}
|
||||
|
||||
override fun getErrorTAG(): String {
|
||||
return TAG_ERROR
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
415
sample/src/main/java/dji/sampleV5/aircraft/keyvalue/KeyItem.java
Normal file
415
sample/src/main/java/dji/sampleV5/aircraft/keyvalue/KeyItem.java
Normal file
@@ -0,0 +1,415 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import dji.v5.ux.core.util.ToastUtils;
|
||||
import dji.sampleV5.aircraft.util.Util;
|
||||
import dji.sdk.keyvalue.converter.DJIValueConverter;
|
||||
import dji.sdk.keyvalue.converter.EmptyValueConverter;
|
||||
import dji.sdk.keyvalue.converter.SingleValueConverter;
|
||||
import dji.sdk.keyvalue.key.DJIActionKeyInfo;
|
||||
import dji.sdk.keyvalue.key.DJIKeyInfo;
|
||||
import dji.sdk.keyvalue.value.base.DJIValue;
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg;
|
||||
import dji.v5.common.callback.CommonCallbacks;
|
||||
import dji.v5.common.error.IDJIError;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* KeyItem作为key能力和动作的载体来进行封装
|
||||
*/
|
||||
|
||||
public class KeyItem<P, R> extends KeyBaseStructure<P , R> implements Comparable<KeyItem<?,?>>{
|
||||
|
||||
private static final String TAG = KeyItem.class.getSimpleName();
|
||||
public KeyItem(DJIKeyInfo<?> keyInfo) {
|
||||
super();
|
||||
this.keyInfo = (DJIKeyInfo<R>)keyInfo;
|
||||
this.keyInfoSet = (DJIKeyInfo<P>)keyInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性展示名
|
||||
*/
|
||||
protected String name;
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(long count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用次数,用户排序
|
||||
*/
|
||||
private long count;
|
||||
public boolean isSingleDJIValue;
|
||||
|
||||
/**
|
||||
* 参数key的能力携带实体
|
||||
*/
|
||||
protected DJIKeyInfo<R> keyInfo;
|
||||
|
||||
protected DJIKeyInfo<P> keyInfoSet;
|
||||
|
||||
|
||||
/**
|
||||
* 需要调用者注入的回调接口,用于结果通知
|
||||
*/
|
||||
protected KeyItemActionListener<Object> keyOperateCallBack;
|
||||
private boolean isItemSelected ;
|
||||
public boolean isItemSelected() {
|
||||
return isItemSelected;
|
||||
}
|
||||
|
||||
public void setItemSelected(boolean itemSelected) {
|
||||
isItemSelected = itemSelected;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 推送数据回调(需要调用者注入)
|
||||
*/
|
||||
protected KeyItemActionListener<String> pushCallBack;
|
||||
|
||||
public String getName() {
|
||||
return Util.isBlank(name) ? keyInfo.getIdentifier() : name;
|
||||
}
|
||||
public void setName(String name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public DJIKeyInfo<P> getKeyInfo() {
|
||||
return keyInfoSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取listen宿主
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Object getListenHolder() {
|
||||
return listenHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以Get
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canGet() {
|
||||
return keyInfo.isCanGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以Set
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canSet() {
|
||||
return keyInfo.isCanSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以Listen
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canListen() {
|
||||
return keyInfo.isCanListen();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为action
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canAction() {
|
||||
return keyInfo.isCanPerformAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要调用者注入的回调接口,用于结果通知
|
||||
*
|
||||
* @param keyOperateCallBack
|
||||
*/
|
||||
public void setKeyOperateCallBack(KeyItemActionListener<Object> keyOperateCallBack) {
|
||||
this.keyOperateCallBack = keyOperateCallBack;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送回调
|
||||
*
|
||||
* @param pushCallBack
|
||||
*/
|
||||
public void setPushCallBack(KeyItemActionListener<String> pushCallBack) {
|
||||
this.pushCallBack = pushCallBack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get请求
|
||||
*/
|
||||
public void doGet() {
|
||||
try {
|
||||
|
||||
get(keyInfo, new CommonCallbacks.CompletionCallbackWithParam<R>() {
|
||||
@Override
|
||||
public void onSuccess(R data) {
|
||||
|
||||
if (keyOperateCallBack != null && data != null) {
|
||||
keyOperateCallBack.actionChange(getName()+"【GET】 == success " + data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull IDJIError error) {
|
||||
if (keyOperateCallBack != null) {
|
||||
keyOperateCallBack.actionChange(getName() + "【GET】 GetErrorMsg ==" + error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncGet请求
|
||||
*/
|
||||
public void doSyncGet() {
|
||||
try {
|
||||
DJIValue getResult = (DJIValue) get(keyInfo);
|
||||
if(keyOperateCallBack != null){
|
||||
keyOperateCallBack.actionChange(null == getResult ? "" : getResult.toJson());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set请求
|
||||
*/
|
||||
public void doSet(String jsonStr) {
|
||||
try {
|
||||
final P p = validPrams(jsonStr);
|
||||
if (p == null) {
|
||||
keyOperateCallBack.actionChange(getName() + "【SET】SetErrorMsg== json error");
|
||||
return;
|
||||
}
|
||||
|
||||
set(keyInfoSet, p, new CommonCallbacks.CompletionCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
LogUtils.e(TAG, "set success : " + getName());
|
||||
if (keyOperateCallBack != null) {
|
||||
// 保存上一次设置成功的对象,下次set时可使用保存过的对象 序列化json
|
||||
if (getKeyInfo().getTypeConverter() instanceof DJIValueConverter) {
|
||||
param = p;
|
||||
}
|
||||
keyOperateCallBack.actionChange(getName() + "【SET】==" + p.toString() + " | " + " success");
|
||||
}
|
||||
ToastUtils.INSTANCE.showToast("set " + p.getClass().getSimpleName() + " success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull IDJIError error) {
|
||||
LogUtils.e(TAG, "set error : " + error);
|
||||
if (keyOperateCallBack != null) {
|
||||
keyOperateCallBack.actionChange(getName() + "【SET】SetErrorMsg== " + p.toString() + "|" + error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
ToastUtils.INSTANCE.showToast("输入参数出错");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* action请求
|
||||
*/
|
||||
public void doAction(String jsonStr) {
|
||||
|
||||
DJIActionKeyInfo<P,R> actionKeyInfo = (DJIActionKeyInfo<P,R>) keyInfo;
|
||||
P p = null;
|
||||
if(jsonStr != null && !jsonStr.isEmpty()){
|
||||
p = validPrams(jsonStr);
|
||||
}
|
||||
if (p == null && actionKeyInfo.getTypeConverter()!= EmptyValueConverter.converter) {
|
||||
return;
|
||||
}
|
||||
|
||||
P pRes = p;
|
||||
action(actionKeyInfo, p, new CommonCallbacks.CompletionCallbackWithParam<R>() {
|
||||
@Override
|
||||
public void onSuccess(Object data) {
|
||||
if (keyOperateCallBack != null) {
|
||||
if (data != null && !(data instanceof EmptyMsg)) {
|
||||
keyOperateCallBack.actionChange(getName() + "【ACTION】== " + getActionTipsStr(pRes) + " success: " + data.toString());
|
||||
} else {
|
||||
keyOperateCallBack.actionChange(getName() + "【ACTION】== " + getActionTipsStr(pRes) + " result: success");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull IDJIError error) {
|
||||
if (keyOperateCallBack != null) {
|
||||
keyOperateCallBack.actionChange(getName() +"【ACTION】 ActionErrorMsg==" + error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getActionTipsStr(P pRes){
|
||||
return pRes == null ? "" : pRes.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送回调
|
||||
*/
|
||||
private CommonCallbacks.KeyListener<R> listenSDKCallback =
|
||||
( oldValue ,newValue) -> {
|
||||
StringBuffer sb = new StringBuffer("【LISTEN】");
|
||||
sb.append(getName());
|
||||
sb.append(" result:");
|
||||
sb.append("oldValue:").append(oldValue);
|
||||
sb.append(" newValue:").append(newValue);
|
||||
|
||||
if (pushCallBack != null) {
|
||||
pushCallBack.actionChange(sb.toString());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 注册Listen(新接口)
|
||||
*
|
||||
* @param listenHolder
|
||||
*/
|
||||
public void listen(Object listenHolder) {
|
||||
listen(keyInfo, listenHolder, listenSDKCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消Listen(新接口)
|
||||
*
|
||||
* @param listenHolder
|
||||
*/
|
||||
public void cancelListen(Object listenHolder) {
|
||||
if (this.listenHolder == listenHolder) {
|
||||
this.listenHolder = null;
|
||||
cancelListen(keyInfo, listenHolder);
|
||||
pushCallBack = null;
|
||||
listenRecord = "";
|
||||
}
|
||||
//listenSDKCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证参数
|
||||
*
|
||||
* @param jsonStr
|
||||
* @return
|
||||
*/
|
||||
private P validPrams(String jsonStr) {
|
||||
if (Util.isBlank(jsonStr)) {
|
||||
ToastUtils.INSTANCE.showToast("请先设置参数");
|
||||
return null;
|
||||
}
|
||||
final P p = buildParamFromJsonStr(jsonStr);
|
||||
if (p == null) {
|
||||
ToastUtils.INSTANCE.showToast("请先设置" + jsonStr + " 参数");
|
||||
return null;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化:根据JSON串获取对象
|
||||
*
|
||||
* @param jsonStr
|
||||
* @return
|
||||
*/
|
||||
public P buildParamFromJsonStr(String jsonStr) {
|
||||
P p;
|
||||
if (keyInfo.getTypeConverter() instanceof SingleValueConverter && !isSingleDJIValue) {
|
||||
p = (P) keyInfo.getTypeConverter().fromStr(getSingleJsonValue(jsonStr));
|
||||
} else {
|
||||
p = (P) keyInfo.getTypeConverter().fromStr(jsonStr);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SingleValue 中原始类型包装类的value值
|
||||
* @param jsonStr
|
||||
* @return
|
||||
*/
|
||||
private String getSingleJsonValue(String jsonStr) {
|
||||
String value = "";
|
||||
try {
|
||||
JSONObject jsonObj = new JSONObject(jsonStr);
|
||||
value = jsonObj.getString("value");
|
||||
}catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化:获取默认JSON串
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getParamJsonStr() {
|
||||
String jsonStr = null;
|
||||
try {
|
||||
jsonStr = param.toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除回调
|
||||
*/
|
||||
public void removeCallBack() {
|
||||
cancelListen(listenHolder);
|
||||
keyOperateCallBack = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(KeyItem keyItem) {
|
||||
if (keyItem.count - this.count > 0) {
|
||||
return 1;
|
||||
} else if (keyItem.count - this.count < 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isSingleDJIValue() {
|
||||
return isSingleDJIValue;
|
||||
}
|
||||
|
||||
public void setSingleDJIValue(boolean singleDJIValue) {
|
||||
isSingleDJIValue = singleDJIValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return Util.isBlank(name) ? keyInfo.getIdentifier() : name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/11 9:55 上午
|
||||
* @description:
|
||||
*/
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
*/
|
||||
public interface KeyItemActionListener<T> {
|
||||
|
||||
void actionChange(@Nullable T t);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.SparseArrayCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import dji.sampleV5.aircraft.R;
|
||||
|
||||
|
||||
public class KeyItemAdapter extends RecyclerView.Adapter<KeyItemAdapter.ComViewHolder> implements Filterable {
|
||||
|
||||
private KeyItemActionListener<KeyItem<?, ?>> callback;
|
||||
|
||||
protected List<KeyItem<?, ?>> dataList;
|
||||
protected List<KeyItem<?, ?>> mFilterList;
|
||||
protected Context context;
|
||||
|
||||
|
||||
public KeyItemAdapter(Context context, List<KeyItem<?, ?>> dataList, KeyItemActionListener<KeyItem<?, ?>> callback) {
|
||||
this.context = context;
|
||||
this.dataList = dataList;
|
||||
this.mFilterList = dataList;
|
||||
this.callback = callback;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ComViewHolder(LayoutInflater.from(context).inflate(R.layout.item_camera_key_list, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ComViewHolder holder, int position) {
|
||||
if (mFilterList.size() <= position) {
|
||||
return;
|
||||
}
|
||||
convert(holder, mFilterList.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mFilterList == null ? 0 : mFilterList.size();
|
||||
}
|
||||
|
||||
|
||||
public void convert(ComViewHolder viewHolder, final KeyItem<?, ?> keyItem) {
|
||||
if (viewHolder == null || keyItem == null) {
|
||||
return;
|
||||
}
|
||||
TextView textView = viewHolder.getView(R.id.tv_item_name);
|
||||
textView.setText(keyItem.getName());
|
||||
if (keyItem.isItemSelected()) {
|
||||
textView.setBackgroundColor(Color.GRAY);
|
||||
} else {
|
||||
textView.setBackgroundColor(Color.TRANSPARENT);
|
||||
}
|
||||
textView.setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.actionChange(keyItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return new Filter() {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence charSequence) {
|
||||
String charString = charSequence.toString();
|
||||
if (charString.isEmpty()) {
|
||||
mFilterList = dataList;
|
||||
} else {
|
||||
List<KeyItem<?, ?>> filteredList = new ArrayList<>();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0, length = charSequence.length(); i < length; i++) {
|
||||
if (stringBuilder.length() > 0) {
|
||||
stringBuilder.append("+.*");
|
||||
}
|
||||
stringBuilder.append(charSequence.charAt(i));
|
||||
}
|
||||
Pattern pattern = Pattern.compile(stringBuilder.toString(), Pattern.CASE_INSENSITIVE);
|
||||
for (KeyItem<?, ?> item : dataList) {
|
||||
if (pattern.matcher(item.keyInfo.getIdentifier()).find()) {
|
||||
filteredList.add(item);
|
||||
}
|
||||
|
||||
}
|
||||
mFilterList = filteredList;
|
||||
}
|
||||
|
||||
FilterResults filterResults = new FilterResults();
|
||||
filterResults.values = mFilterList;
|
||||
return filterResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
||||
mFilterList = (ArrayList<KeyItem<?, ?>>) filterResults.values;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 缓存容器
|
||||
*/
|
||||
public class ComViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private View convertView;
|
||||
private SparseArrayCompat<View> views;
|
||||
|
||||
public ComViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.convertView = itemView;
|
||||
this.views = new SparseArrayCompat<>();
|
||||
}
|
||||
|
||||
|
||||
public <T extends View> T getView(int layoutId) {
|
||||
View view = views.get(layoutId);
|
||||
if (view == null) {
|
||||
view = convertView.findViewById(layoutId);
|
||||
views.put(layoutId, view);
|
||||
}
|
||||
return (T) view;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import dji.sdk.keyvalue.converter.DJIValueConverter;
|
||||
import dji.sdk.keyvalue.converter.IDJIValueConverter;
|
||||
import dji.sdk.keyvalue.converter.SingleValueConverter;
|
||||
import dji.sdk.keyvalue.key.AIBoxKey;
|
||||
import dji.sdk.keyvalue.key.AirLinkKey;
|
||||
import dji.sdk.keyvalue.key.BatteryKey;
|
||||
import dji.sdk.keyvalue.key.BleKey;
|
||||
import dji.sdk.keyvalue.key.CameraKey;
|
||||
import dji.sdk.keyvalue.key.DJIKeyInfo;
|
||||
import dji.sdk.keyvalue.key.FlightAssistantKey;
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey;
|
||||
import dji.sdk.keyvalue.key.GimbalKey;
|
||||
import dji.sdk.keyvalue.key.LidarKey;
|
||||
import dji.sdk.keyvalue.key.MobileNetworkKey;
|
||||
import dji.sdk.keyvalue.key.MobileNetworkLinkRCKey;
|
||||
import dji.sdk.keyvalue.key.OcuSyncKey;
|
||||
import dji.sdk.keyvalue.key.OnboardKey;
|
||||
import dji.sdk.keyvalue.key.PayloadKey;
|
||||
import dji.sdk.keyvalue.key.ProductKey;
|
||||
import dji.sdk.keyvalue.key.RadarKey;
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey;
|
||||
import dji.sdk.keyvalue.key.RtkBaseStationKey;
|
||||
import dji.sdk.keyvalue.key.RtkMobileStationKey;
|
||||
import dji.sdk.keyvalue.value.base.DJIValue;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/16 3:36 下午
|
||||
* @description:
|
||||
*/
|
||||
public class KeyItemDataUtil {
|
||||
private static final String TAG = KeyItemDataUtil.class.getSimpleName();
|
||||
private static final List<KeyItem<?, ?>> allKeyList = new ArrayList<>();
|
||||
|
||||
private KeyItemDataUtil() {
|
||||
//do something
|
||||
}
|
||||
|
||||
public static void initBatteryKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, BatteryKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initAirlinkKeyList(List<KeyItem<?, ?>> keylist) {
|
||||
initList(keylist, AirLinkKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initGimbalKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, GimbalKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initCameraKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, CameraKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initWiFiKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
// initList(keyList , WiFiKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initFlightAssistantKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, FlightAssistantKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initFlightControllerKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, FlightControllerKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRemoteControllerKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RemoteControllerKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initBleKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, BleKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initProductKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, ProductKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRtkBaseStationKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RtkBaseStationKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRtkMobileStationKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RtkMobileStationKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initOcuSyncKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, OcuSyncKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRadarKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RadarKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initIntelligentBoxList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, AIBoxKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initMobileNetworkKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, MobileNetworkKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initMobileNetworkLinkRCKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, MobileNetworkLinkRCKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initOnboardKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, OnboardKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initPayloadKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, PayloadKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initLidarKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, LidarKey.getKeyList());
|
||||
}
|
||||
|
||||
private static void initList(List<KeyItem<?, ?>> keyList, List<DJIKeyInfo<?>> keyInfoList) {
|
||||
if (keyList == null || !keyList.isEmpty()){
|
||||
return;
|
||||
}
|
||||
for (DJIKeyInfo<?> info : keyInfoList) {
|
||||
KeyItem<DJIValue, DJIValue> item = new KeyItem<>(info);
|
||||
genericItem(item, info);
|
||||
keyList.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static <P extends DJIValue, R extends DJIValue> void genericItem(KeyItem<P, R> item, DJIKeyInfo<?> keyInfo) {
|
||||
|
||||
Class<?> tdClazz;
|
||||
Field field = null;
|
||||
boolean isDjiValue = false;
|
||||
try {
|
||||
IDJIValueConverter<P, R> clazzConvert = keyInfo.getTypeConverter();
|
||||
if (clazzConvert instanceof SingleValueConverter) {
|
||||
field = clazzConvert.getClass().getDeclaredField("dClass");
|
||||
Field tmp = clazzConvert.getClass().getDeclaredField("isDJIValue");
|
||||
tmp.setAccessible(true);
|
||||
isDjiValue = tmp.getBoolean(clazzConvert);
|
||||
} else if (clazzConvert instanceof DJIValueConverter) {
|
||||
field = clazzConvert.getClass().getDeclaredField("tClass");
|
||||
}
|
||||
|
||||
if (field != null) {
|
||||
field.setAccessible(true);
|
||||
tdClazz = (Class<?>) field.get(clazzConvert);
|
||||
item.param = (P) tdClazz.newInstance();
|
||||
item.result = (R) tdClazz.newInstance();
|
||||
item.setSingleDJIValue(isDjiValue);
|
||||
item.initGenericInstance();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static int getAllKeyListCount() {
|
||||
List<KeyItem<?, ?>> allKeyList = new ArrayList<>();
|
||||
getAllKeyList(allKeyList);
|
||||
return allKeyList.size();
|
||||
}
|
||||
|
||||
public static void getAllKeyList(List<KeyItem<?, ?>> keylist) {
|
||||
if (!allKeyList.isEmpty()) {
|
||||
keylist.addAll(allKeyList);
|
||||
return;
|
||||
}
|
||||
List<KeyItem<?, ?>> keyList = new ArrayList<>();
|
||||
initBatteryKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initAirlinkKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initGimbalKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initCameraKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initFlightAssistantKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initFlightControllerKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRemoteControllerKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initBleKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initProductKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRtkBaseStationKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRtkMobileStationKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initOcuSyncKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRadarKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initMobileNetworkKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initMobileNetworkLinkRCKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initOnboardKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initPayloadKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initLidarKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initIntelligentBoxList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
|
||||
|
||||
import dji.sampleV5.aircraft.util.Util
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import java.lang.Exception
|
||||
import java.lang.StringBuilder
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/10 11:23 上午
|
||||
* @description:
|
||||
*/
|
||||
object KeyItemHelper {
|
||||
|
||||
val TAG = LogUtils.getTag(this)
|
||||
val LISTEN_RECORD_MAX_LENGTH = 2000
|
||||
val FILED_CHANGE = "\$change"
|
||||
/**
|
||||
* 通过反射给字段赋值
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
fun initSubItemData(obj: Any): Map<String?, List<EnumItem>> {
|
||||
val dataMap: MutableMap<String?, List<EnumItem>> = HashMap()
|
||||
try {
|
||||
val fields = obj.javaClass.declaredFields
|
||||
for (field in fields) {
|
||||
if (field.name == FILED_CHANGE || field.name == "serialVersionUID") {
|
||||
continue
|
||||
}
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
dataMap[clazz.canonicalName] =
|
||||
buildParamsSubItemListWithEnum(field.type as Class<Enum<*>>)
|
||||
} else if (isEnumList(field)) {
|
||||
val type = field.genericType
|
||||
if (type is ParameterizedType) {
|
||||
val subObject: Class<out Enum<*>> =
|
||||
type.actualTypeArguments[0] as Class<Enum<*>>
|
||||
dataMap[clazz.canonicalName] = buildParamsSubItemListWithEnum(subObject)
|
||||
}
|
||||
} else {
|
||||
dataMap.clear()
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG,e.message)
|
||||
}
|
||||
return dataMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理界面参数选择和设置
|
||||
*
|
||||
* @param anchor
|
||||
* @param dataMap
|
||||
*/
|
||||
fun <P> processSubListLogic(
|
||||
anchor: View,
|
||||
param: P,
|
||||
dataMap: Map<String?, List<EnumItem>>,
|
||||
callBack: KeyItemActionListener<String?>
|
||||
) {
|
||||
try {
|
||||
val nameList = Util.getMapKeyList(dataMap)
|
||||
val subItemList = Util.getMapValueList(dataMap)
|
||||
if (dataMap.size == 1) {
|
||||
//简单列表
|
||||
val list = subItemList[0]
|
||||
val clazz = Class.forName(nameList[0]!!) as Class<Enum<*>>
|
||||
showSimpleSubItemList(anchor.context, list, clazz, object :
|
||||
KeyItemActionListener<List<String>?> {
|
||||
override fun actionChange(t: List<String>?) {
|
||||
updateClassData(param, dataMap)
|
||||
callBack.actionChange(param.toString())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
//复合列表
|
||||
KeyValueDialogUtil.showListConfirmWindow(
|
||||
anchor,
|
||||
getSimpleNameList(nameList),
|
||||
"select item for setting",
|
||||
object :
|
||||
KeyItemActionListener<String?> {
|
||||
override fun actionChange(msg: String?) {
|
||||
if ("confirm" == msg) {
|
||||
updateClassData(param, dataMap)
|
||||
callBack.actionChange(param.toString())
|
||||
} else {
|
||||
val clazz = getClassWithName(msg, nameList) as Class<Enum<*>>?
|
||||
val list = dataMap[clazz!!.canonicalName]!!
|
||||
showSimpleSubItemList(
|
||||
anchor.context,
|
||||
list,
|
||||
clazz
|
||||
) { updateClassData(param, dataMap) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类名列表获取:通过全名list获取简名list
|
||||
*
|
||||
* @param nameList
|
||||
* @return
|
||||
*/
|
||||
fun getSimpleNameList(nameList: List<String?>): List<String> {
|
||||
val simpleNameList: MutableList<String> = ArrayList()
|
||||
for (str in nameList) {
|
||||
simpleNameList.add(str!!.substring(str!!.lastIndexOf(".") + 1))
|
||||
}
|
||||
return simpleNameList
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过简名获取类字节码
|
||||
*
|
||||
* @param simpleName
|
||||
* @param nameList
|
||||
* @return
|
||||
*/
|
||||
fun getClassWithName(simpleName: String?, nameList: List<String?>): Class<*>? {
|
||||
var clazz: Class<*>? = null
|
||||
try {
|
||||
for (str in nameList) {
|
||||
if (str!!.endsWith(simpleName!!)) {
|
||||
clazz = Class.forName(str!!)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
return clazz
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数条目的选项子列表
|
||||
*
|
||||
* @param <E>
|
||||
* @return
|
||||
</E> */
|
||||
fun <E : Enum<*>?> buildParamsSubItemListWithEnum(clazz: Class<E>): List<EnumItem> {
|
||||
val list: MutableList<EnumItem> = ArrayList()
|
||||
try {
|
||||
var item: EnumItem
|
||||
val objs: Array<out E>? = clazz.getEnumConstants()
|
||||
for (obj in objs!!) {
|
||||
item = EnumItem()
|
||||
item.setName(obj.toString())
|
||||
list.add(item)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG,e.message)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性子列表数据
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
fun getSubItemNameList(data: List<EnumItem>): List<String> {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
for (item in data) {
|
||||
result.add(item.getName().toString())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中属性的顺序值
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedIndex(data: List<EnumItem>): Int {
|
||||
var index = 0
|
||||
for (i in data.indices) {
|
||||
if (data[i].isSelected()) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中属性的value
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedValue(data: List<EnumItem>): String {
|
||||
var value = ""
|
||||
for (i in data.indices) {
|
||||
if (data[i].isSelected()) {
|
||||
value = data[i].getName().toString()
|
||||
break
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选择状态
|
||||
*
|
||||
* @param data
|
||||
* @param name
|
||||
*/
|
||||
fun updatedSelectedInfo(data: List<EnumItem>, names: List<String?>) {
|
||||
for (item in data) {
|
||||
item.setSelected(false)
|
||||
for (name in names) {
|
||||
if (item.getName().equals(name)) {
|
||||
item.setSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单参数列表,逻辑处理
|
||||
*/
|
||||
fun <E : Enum<*>?> showSimpleSubItemList(
|
||||
context: Context?,
|
||||
simpleItemList: List<EnumItem>,
|
||||
clazz: Class<E>,
|
||||
callBack: KeyItemActionListener<List<String>?>
|
||||
) {
|
||||
val StrList = getSubItemNameList(simpleItemList)
|
||||
if (StrList.size == 0) {
|
||||
return
|
||||
}
|
||||
val selectedIndex = getSelectedIndex(simpleItemList)
|
||||
if (clazz.isEnum) {
|
||||
KeyValueDialogUtil.showSingleChoiceDialog(
|
||||
context,
|
||||
StrList,
|
||||
selectedIndex,
|
||||
object : KeyItemActionListener<List<String>?> {
|
||||
override fun actionChange(values: List<String>?) {
|
||||
updatedSelectedInfo(simpleItemList, values!!)
|
||||
callBack.actionChange(values)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
KeyValueDialogUtil.showMultiChoiceDialog(
|
||||
context,
|
||||
StrList,
|
||||
) { values ->
|
||||
updatedSelectedInfo(simpleItemList, values!!)
|
||||
callBack.actionChange(values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射给字段赋值
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
fun initClassData(obj: Any?) {
|
||||
if (obj == null || Util.isBlank(obj.toString())) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val pFields = obj.javaClass.declaredFields
|
||||
for (field in pFields) {
|
||||
if (field.name == FILED_CHANGE || field.name == "serialVersionUID") {
|
||||
continue
|
||||
}
|
||||
field.isAccessible = true
|
||||
val clazz = field.type
|
||||
if (setFieldPro(field , obj)){
|
||||
continue
|
||||
}
|
||||
|
||||
val subObj = clazz.newInstance()
|
||||
field[obj] = subObj
|
||||
initClassData(subObj)
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFieldPro(field:Field , obj: Any?):Boolean{
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
field[obj] = clazz.enumConstants!![0]
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Util.isWrapClass(clazz)) {
|
||||
if (clazz == Boolean::class.javaObjectType) {
|
||||
field[obj] = false
|
||||
}
|
||||
//如果需要 可在else 中可给Integer Double 等设置初始值
|
||||
return true
|
||||
}
|
||||
if (clazz == MutableList::class.java) {
|
||||
field[obj] = ArrayList<Any>()// todo
|
||||
return true
|
||||
} else if (clazz == String::class.java) {
|
||||
field[obj] = ""
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isEnumList(field: Field): Boolean {
|
||||
if (field.type == MutableList::class.java) {
|
||||
val type = field.genericType
|
||||
if (type is ParameterizedType) {
|
||||
val subType = type.actualTypeArguments[0]
|
||||
val clazz = subType as Class<*>
|
||||
if (clazz.isEnum) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射给字段赋值
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
fun updateClassData(obj: Any?, subItemMap: Map<String?, List<EnumItem>>) {
|
||||
try {
|
||||
val pFields = obj?.javaClass?.declaredFields
|
||||
if (pFields != null) {
|
||||
for (field in pFields) {
|
||||
if (field.name == FILED_CHANGE || field.name == "serialVersionUID") {
|
||||
continue
|
||||
}
|
||||
field.isAccessible = true
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
val itemList = subItemMap[clazz.canonicalName]!!
|
||||
field[obj] = getEnumData(clazz as Class<Enum<*>>, getSelectedValue(itemList))
|
||||
} else if (isEnumList(field)) {
|
||||
setEnumListProperty(field , obj , subItemMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnumListProperty(field: Field , obj: Any? , subItemMap: Map<String?, List<EnumItem>>) {
|
||||
val type = field.genericType
|
||||
val clazz = field.type
|
||||
if (type is ParameterizedType) {
|
||||
val subObject = type.actualTypeArguments[0] as Class<Enum<*>>
|
||||
val itemList = subItemMap[clazz.canonicalName]
|
||||
val list: MutableList<Any> = ArrayList()
|
||||
val values: List<String> = getSelectedValues(itemList!!)
|
||||
for (value in values) {
|
||||
val test: Any = getEnumData(subObject, value)!!
|
||||
list.add(test)
|
||||
}
|
||||
field[obj] = list
|
||||
}
|
||||
}
|
||||
fun getSelectedValues(data: List<EnumItem>): List<String> {
|
||||
var value: String = ""
|
||||
val values: MutableList<String> = ArrayList()
|
||||
for (i in data.indices) {
|
||||
if (data[i].isSelected()) {
|
||||
value = data[i].getName()!!
|
||||
values.add(value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加携带日期的推送字符串
|
||||
*
|
||||
* @param targetStr
|
||||
* @param appendStr
|
||||
* @return
|
||||
*/
|
||||
fun appendListenRecord(targetStr: String, appendStr: String?): String {
|
||||
if (Util.isBlank(appendStr)) {
|
||||
return targetStr
|
||||
}
|
||||
val sb = StringBuilder(targetStr)
|
||||
sb.append("\n")
|
||||
sb.append(Util.getDateStr(Date()) + ":")
|
||||
sb.append("\n")
|
||||
sb.append(appendStr)
|
||||
//长度限制
|
||||
var result = sb.toString()
|
||||
if (result.length > LISTEN_RECORD_MAX_LENGTH) {
|
||||
result = result.substring(result.length - LISTEN_RECORD_MAX_LENGTH)
|
||||
}
|
||||
val title = "push info:"
|
||||
if (!result.startsWith(title)) {
|
||||
result = """
|
||||
$title
|
||||
$result
|
||||
""".trimIndent()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过字节码和值来获取实例
|
||||
*
|
||||
* @param clazz
|
||||
* @param value
|
||||
* @param <E>
|
||||
* @return
|
||||
</E> */
|
||||
fun <E : Enum<*>?> getEnumData(clazz: Class<E>, value: String): E? {
|
||||
var data: E? = null
|
||||
try {
|
||||
val objs: Array<out E>? = clazz.getEnumConstants()
|
||||
for (obj in objs!!) {
|
||||
if (obj.toString() == value) {
|
||||
data = obj as E
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
import dji.sdk.keyvalue.key.ComponentType
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.sdk.keyvalue.utils.MultiComponentManager
|
||||
import dji.sdk.keyvalue.value.common.CameraLensType
|
||||
import dji.sdk.keyvalue.value.product.ProductType
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.DJICommonError
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.manager.capability.CapabilityManager
|
||||
import dji.v5.manager.capability.CapabilityParser
|
||||
import dji.v5.utils.common.DateUtils
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.CompletableEmitter
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 11:18 上午
|
||||
* @description: key的操作基类
|
||||
*/
|
||||
abstract class KeyOperatorCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) {
|
||||
|
||||
private val INTERVAL_TIME = 500L // 连续快速调用key时 可能会导致失败
|
||||
private val TAG_EQUAL = "=="
|
||||
private val TAG = LogUtils.getTag(this)
|
||||
private lateinit var competableEmitter: CompletableEmitter
|
||||
private var unPassedCount = 0;
|
||||
private lateinit var curCheckType: KeyCheckType
|
||||
private var keyCount = 0
|
||||
|
||||
private var whiteList = mutableListOf<String>(
|
||||
"GimbalCalibrationStatus",
|
||||
"IsShootingPhotoPanorama",
|
||||
"PhotoPanoramaMode",
|
||||
"PhotoPanoramaProgress",
|
||||
"ThermalContrast",
|
||||
"ThermalDDE",
|
||||
"ThermalRegionMetersureTemperature",
|
||||
"ThermalBrightness",
|
||||
"ThermalGainModeTemperatureRange",
|
||||
"AircraftLocation3D",
|
||||
"PhotoRatio"
|
||||
)
|
||||
|
||||
/**
|
||||
* 过滤Key类型条件
|
||||
*/
|
||||
abstract fun filter(item: KeyItem<*, *>): Boolean
|
||||
|
||||
/**
|
||||
* 指定指定动作类型
|
||||
*/
|
||||
abstract fun run(item: KeyItem<*, *>)
|
||||
|
||||
/**
|
||||
* 写文件需要
|
||||
*/
|
||||
abstract fun getTAG(): String
|
||||
|
||||
/**
|
||||
* Key执行结果回调TAG,用来改key执行错误或者失败
|
||||
*/
|
||||
abstract fun getErrorTAG(): String
|
||||
|
||||
open fun getIntervalTime(): Long {
|
||||
return INTERVAL_TIME
|
||||
}
|
||||
|
||||
fun execute(): Completable {
|
||||
|
||||
return Completable.create { emitter ->
|
||||
competableEmitter = emitter;
|
||||
unPassedCount = 0
|
||||
val allList: MutableList<KeyItem<*, *>> = ArrayList()
|
||||
KeyItemDataUtil.getAllKeyList(allList)
|
||||
val capabilityKeyCount =
|
||||
CapabilityManager.getInstance().getCapabilityKeyCount(productType)
|
||||
|
||||
LogUtils.i(TAG, "begin check $capabilityKeyCount")
|
||||
saveResult(" ----- begin ${getTAG()} check -----\n\n", false, false)
|
||||
saveResult(" ----- begin ${getTAG()} check -----\n\n", true, false)
|
||||
|
||||
allList.filter { item ->
|
||||
filter(item) && (item.toString() !in whiteList) && CapabilityManager.getInstance().isKeySupported(
|
||||
productType, componentTypeName, ComponentType.find(item.getKeyInfo().componentType), "Key$item"
|
||||
)
|
||||
}.forEach { item ->
|
||||
LogUtils.e(TAG, "${++keyCount} doCheck $item ")
|
||||
LogUtils.e(TAG, "Thread name is 1 " + Thread.currentThread().name)
|
||||
val lock = CountDownLatch(1)
|
||||
item.componetIndex = if (MultiComponentManager.isMultiKey(item.keyInfo.componentType)) {
|
||||
componentIndex
|
||||
} else {
|
||||
0
|
||||
}
|
||||
dependKeySet(item, object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
//从主线程再切回io线程
|
||||
LogUtils.e(TAG, "Thread name is 2 " + Thread.currentThread().name)
|
||||
lock.countDown()
|
||||
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
LogUtils.e(TAG, "Set $item depend key failed!")
|
||||
lock.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
lock.await()
|
||||
Thread.sleep(getIntervalTime()) // 设置完后,立即设置可能会异常如FrequencyBand
|
||||
run(item)
|
||||
}
|
||||
Thread.sleep(getIntervalTime())
|
||||
saveResult(" --------finish ${getTAG()}---------\n", true, true)
|
||||
saveResult(" --------finish ${getTAG()}---------\n", false, true)
|
||||
//遍历完成即完成
|
||||
competableEmitter.onComplete()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行依赖key的 Set方法
|
||||
*/
|
||||
private fun dependKeySet(item: KeyItem<*, *>, callback: CommonCallbacks.CompletionCallback) {
|
||||
// 首先获取具体的依赖的keyitem
|
||||
val keyName = item.toString()
|
||||
val valueBean = CapabilityParser.getInstance().getValueBean(keyName)
|
||||
val dependKeyItem = valueBean?.dependKeyName?.let { CapabilityKeyChecker.getKeyItem(it) }
|
||||
LogUtils.e(TAG, "dependKeyItem key : " + (valueBean?.dependKeyName ?: "null"))
|
||||
|
||||
if (dependKeyItem != null) {
|
||||
dependKeyItem.setKeyOperateCallBack { res ->
|
||||
if (res.toString().contains("SetErrorMsg")) {
|
||||
callback.onFailure(DJICommonError.FACTORY.build(res.toString()))
|
||||
} else {
|
||||
callback.onSuccess()
|
||||
}
|
||||
}
|
||||
dependKeyItem.setComponetIndex(item.getComponetIndex())
|
||||
dependKeyItem.setSubComponetType(item.getSubComponetType())
|
||||
dependKeyItem.setSubComponetIndex(item.getSubComponetIndex())
|
||||
dependKeyItem.doSet(valueBean.dependKeyValue.replace("\\", ""))
|
||||
} else {
|
||||
// 没有找到前置条件,返回成功。
|
||||
callback.onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
fun doKeyParam(item: KeyItem<*, *>, type: KeyCheckType) {
|
||||
curCheckType = type
|
||||
getItemDecoderList(item).forEach {
|
||||
val lock = CountDownLatch(1)
|
||||
item.setKeyOperateCallBack {
|
||||
var result = StringBuilder()
|
||||
val resStr = it.toString()
|
||||
val keyNameIndex = resStr.indexOf(getTAG())
|
||||
if (keyNameIndex <= -1 ) return@setKeyOperateCallBack
|
||||
val keyName = resStr.substring(0, keyNameIndex)
|
||||
val isPassed: Boolean
|
||||
val failedReson = if (resStr.contains(getErrorTAG())) {
|
||||
isPassed = false
|
||||
resStr.substring(resStr.indexOf(TAG_EQUAL))
|
||||
} else {
|
||||
isPassed = true
|
||||
resStr.substring(resStr.indexOf(TAG_EQUAL))
|
||||
}
|
||||
var componentTYpe = ComponentType.find(item.getKeyInfo().componentType)
|
||||
|
||||
result.append("${++unPassedCount} KeyName :${keyName} - ${componentTYpe}\n")
|
||||
.append("SubType:${getLensName(item)}\n")
|
||||
.append("Details:${failedReson}\n")
|
||||
.append("\n ----------------------- \n")
|
||||
|
||||
saveResult(result.toString(), isPassed, true)
|
||||
LogUtils.e(TAG, "SubType:${getLensName(item)} KeyName :${keyName}} ComponentType : $componentTYpe " + resStr)
|
||||
lock.countDown()
|
||||
}
|
||||
item.setComponetIndex(it.componetIndex)
|
||||
item.setSubComponetType(it.subComponetType)
|
||||
item.setSubComponetIndex(it.subComponetIndex)
|
||||
when (type) {
|
||||
KeyCheckType.ACTION -> item.doAction(it.jsonString)
|
||||
KeyCheckType.SET -> item.doSet(it.jsonString)
|
||||
KeyCheckType.GET -> item.doGet()
|
||||
}
|
||||
lock.await()
|
||||
Thread.sleep(getIntervalTime())
|
||||
}
|
||||
|
||||
LogUtils.e(TAG, "check finish!")
|
||||
}
|
||||
|
||||
private fun getLensName(keyItem: KeyItem<*, *>): String {
|
||||
return if (keyItem.keyInfo.componentType == ComponentType.CAMERA.value()) {
|
||||
CameraLensType.find(keyItem.getSubComponetType()).name
|
||||
} else {
|
||||
"DEFAULT"
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveResult(content: String, saveType: Boolean, append: Boolean) {
|
||||
val product = ProductKey.KeyProductType.create().get(ProductType.UNRECOGNIZED)
|
||||
var filePath = LogUtils.getLogPath() + getTAG() + product.name + "【${DateUtils.getSystemTimeOnlyYMD()}】"
|
||||
|
||||
filePath += if (saveType) {
|
||||
"Success.txt"
|
||||
} else {
|
||||
"Failed.txt"
|
||||
}
|
||||
FileUtils.writeFile(filePath, content, append)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过key名称从能力集中获取keyItem需要的参数,包括lenstype ,用例json
|
||||
* 如果在能力集中没有找到用例 需要返回一个默认的(无参的action 返回空)
|
||||
*/
|
||||
private fun getItemDecoderList(keyItem: KeyItem<*, *>): MutableList<CapabilityKeyChecker.ItemDecoder> {
|
||||
val resList: MutableList<CapabilityKeyChecker.ItemDecoder> = ArrayList()
|
||||
//通过item获取 支持的lensType 列表
|
||||
var lensTypeList = if (keyItem.keyInfo.componentType == ComponentType.CAMERA.value()) {
|
||||
CapabilityManager.getInstance().getSupportLens("Key$keyItem", productType, componentTypeName)
|
||||
} else {
|
||||
arrayListOf("DEFAULT")
|
||||
}
|
||||
//获取javaBean 字符列表 用例中没有文件则返回空集合
|
||||
var keyParamList = CapabilityKeyChecker.getKeyParamList(keyItem.toString())
|
||||
|
||||
// 用例文件存在(set类型 都会有) ;action 不在用例文件中的则不自动测试需要人为测试 支持set get
|
||||
if (keyParamList.isNotEmpty() || keyItem.canGet()) {
|
||||
lensTypeList.map {
|
||||
transCameraLensTypeStr(it)
|
||||
}.forEach { subComponentType ->
|
||||
val index = if (MultiComponentManager.isMultiKey(keyItem.keyInfo.componentType)) {
|
||||
componentIndex
|
||||
} else {
|
||||
0
|
||||
}
|
||||
if (curCheckType == KeyCheckType.SET || curCheckType == KeyCheckType.ACTION) {
|
||||
keyParamList
|
||||
.forEach {
|
||||
resList.add(
|
||||
CapabilityKeyChecker.ItemDecoder(
|
||||
componetIndex = index,
|
||||
subComponetType = subComponentType,
|
||||
jsonString = it
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (curCheckType == KeyCheckType.GET) {
|
||||
resList.add(
|
||||
CapabilityKeyChecker.ItemDecoder(
|
||||
componetIndex = index,
|
||||
subComponetType = subComponentType,
|
||||
jsonString = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//添加默认 确保每个key都可以执行到set方法 , action无参数不执行(在用例中未找到)
|
||||
}
|
||||
return resList
|
||||
}
|
||||
|
||||
/**
|
||||
* 将能力集中CameraLensType 字符串转为对应的value
|
||||
*/
|
||||
fun transCameraLensTypeStr(lensName: String): Int {
|
||||
CameraLensType.values().forEach {
|
||||
if (it.name.contains(lensName)) {
|
||||
return it.value()
|
||||
}
|
||||
}
|
||||
return CameraLensType.UNKNOWN.value()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:25 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeySetCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) : KeyOperatorCommand(productType, componentTypeName, componentIndex) {
|
||||
|
||||
private val TAG_GET = "【SET】"
|
||||
private val TAG_ERROR = "SetErrorMsg"
|
||||
private val INTERVAL_TIME = 3000L
|
||||
|
||||
//&& ("CameraMode" ==item.toString() || "RegionMeteringArea" == item.toString())
|
||||
override fun filter(item: KeyItem<*, *>): Boolean {
|
||||
return item.canSet()
|
||||
}
|
||||
|
||||
override fun run(item: KeyItem<*, *>) {
|
||||
super.doKeyParam(item, KeyCheckType.SET)
|
||||
}
|
||||
|
||||
override fun getTAG(): String {
|
||||
return TAG_GET
|
||||
}
|
||||
|
||||
override fun getErrorTAG(): String {
|
||||
return TAG_ERROR
|
||||
}
|
||||
|
||||
override fun getIntervalTime(): Long {
|
||||
return INTERVAL_TIME
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import dji.sampleV5.aircraft.R;
|
||||
import dji.sampleV5.aircraft.util.Util;
|
||||
import dji.v5.utils.common.ContextUtil;
|
||||
import dji.v5.utils.common.DisplayUtil;
|
||||
|
||||
|
||||
public class KeyValueDialogUtil {
|
||||
|
||||
private static final int LIST_Y_OFF_SET = 3;
|
||||
|
||||
private KeyValueDialogUtil(){
|
||||
// init something
|
||||
}
|
||||
/**
|
||||
* 显示单选对话框
|
||||
*/
|
||||
public static void showSingleChoiceDialog(Context context, List<String> data, int selectedIndex, final KeyItemActionListener<List<String>> callBack) {
|
||||
AlertDialog dialog;
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
final String[] items = data.toArray(new String[data.size()]);
|
||||
builder.setSingleChoiceItems(items, selectedIndex, (dialog1, which) -> {
|
||||
if (callBack != null) {
|
||||
callBack.actionChange(Arrays.asList(items[which]));
|
||||
dialog1.dismiss();
|
||||
}
|
||||
});
|
||||
builder.setCancelable(true);
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
public static void showMultiChoiceDialog(Context context, List<String> data, final KeyItemActionListener<List<String>> callBack){
|
||||
AlertDialog dialog;
|
||||
List<String> values = new ArrayList<>();
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
final String[] items = data.toArray(new String[data.size()]);
|
||||
builder.setMultiChoiceItems(items, null, (dialog1, which, isChecked) -> {
|
||||
if (isChecked) {
|
||||
values.add(items[which]);
|
||||
} else {
|
||||
values.remove(items[which]);
|
||||
}
|
||||
|
||||
});
|
||||
builder.setPositiveButton(R.string.confirm, (dialog12, which) -> callBack.actionChange(values));
|
||||
builder.setCancelable(true);
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
|
||||
}
|
||||
/**
|
||||
* 显示简单列表弹窗
|
||||
*
|
||||
* @param anchor
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
public static void showListConfirmWindow(View anchor, final List<String> data, String title, final KeyItemActionListener<String> callback) {
|
||||
if (anchor == null || anchor.getContext() == null) {
|
||||
return;
|
||||
}
|
||||
Context context = anchor.getContext();
|
||||
View rootView = View.inflate(context, R.layout.dialog_list_confirm, null);
|
||||
final PopupWindow window = new PopupWindow(context);
|
||||
window.setWidth(Util.getHeight(ContextUtil.getContext()) / 2);
|
||||
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
window.setOutsideTouchable(false);
|
||||
window.setTouchable(true);
|
||||
window.setFocusable(true);
|
||||
window.setBackgroundDrawable(new ColorDrawable(0xffffff));
|
||||
window.setContentView(rootView);
|
||||
window.showAsDropDown(anchor, 0, LIST_Y_OFF_SET, Gravity.CENTER | Gravity.BOTTOM);
|
||||
|
||||
ListView listView = rootView.findViewById(R.id.list_view);
|
||||
TextView titleView = rootView.findViewById(R.id.title);
|
||||
if (Util.isNotBlank(title)) {
|
||||
titleView.setText(title);
|
||||
titleView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
titleView.setVisibility(View.GONE);
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, R.layout.item_textview, data);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> callback.actionChange(data.get(position)));
|
||||
rootView.findViewById(R.id.button).setOnClickListener(v -> {
|
||||
window.dismiss();
|
||||
callback.actionChange("confirm");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示输入确认弹窗
|
||||
*
|
||||
* @param context
|
||||
* @param item
|
||||
*/
|
||||
public static void showInputDialog(Activity context, KeyItem<? ,?> item, final KeyItemActionListener<String> callback) {
|
||||
showInputDialog(context, context.getResources().getString(R.string.key_value_set) + item.getName() + ":", item.getParamJsonStr(), "", false, callback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显示输入确认弹窗
|
||||
*
|
||||
* @param context
|
||||
* @param title
|
||||
* @param msg
|
||||
*/
|
||||
public static void showInputDialog(Activity context, String title, final String msg, String hint, boolean singleLine, final KeyItemActionListener<String> callback) {
|
||||
View dialogView = context.getLayoutInflater().inflate(R.layout.dialog_param_input, null);
|
||||
dialogView.setBackgroundColor(context.getResources().getColor(R.color.gray));
|
||||
|
||||
final AlertDialog dialog = new AlertDialog.Builder(context).setView(dialogView).create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.setCancelable(false);
|
||||
|
||||
TextView tvTitle = dialogView.findViewById(R.id.title);
|
||||
tvTitle.setText(title);
|
||||
|
||||
final EditText input = dialogView.findViewById(R.id.input);
|
||||
input.setSingleLine(singleLine);
|
||||
if (Util.isNotBlank(msg)) {
|
||||
input.setText(msg);
|
||||
}
|
||||
if (Util.isNotBlank(hint)) {
|
||||
input.setHint(hint);
|
||||
}
|
||||
input.setMovementMethod(ScrollingMovementMethod.getInstance());
|
||||
dialogView.findViewById(R.id.confirm).setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.actionChange(input.getText().toString().trim());
|
||||
}
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialogView.findViewById(R.id.cancel).setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.actionChange(null);
|
||||
}
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显示通用弹窗
|
||||
*
|
||||
* @param anchor
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
public static void showFilterListWindow(View anchor, final List<KeyItem<?,?>> data, final KeyItemActionListener<KeyItem<?,?>> callback) {
|
||||
if (anchor == null || anchor.getContext() == null) {
|
||||
return;
|
||||
}
|
||||
Context context = anchor.getContext();
|
||||
View rootView = View.inflate(context, R.layout.window_simple_listview, null);
|
||||
final PopupWindow window = new PopupWindow(context);
|
||||
window.setWidth(Util.getHeight(ContextUtil.getContext()) / 2);
|
||||
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
window.setOutsideTouchable(true);
|
||||
window.setTouchable(true);
|
||||
window.setFocusable(true);
|
||||
window.setBackgroundDrawable(new ColorDrawable(0xffffff));
|
||||
|
||||
window.setContentView(rootView);
|
||||
window.showAsDropDown(anchor, 0, - DisplayUtil.dip2px(anchor.getContext(), 47), Gravity.LEFT | Gravity.TOP );
|
||||
|
||||
ListView listView = rootView.findViewById(R.id.list_view);
|
||||
TextView titleView = rootView.findViewById(R.id.tv_title);
|
||||
|
||||
titleView.setText(R.string.commonlyused_key);
|
||||
titleView.setVisibility(View.VISIBLE);
|
||||
|
||||
ArrayAdapter<KeyItem<?,?>> adapter = new ArrayAdapter<KeyItem<?,?>> (context, R.layout.item_textview, data);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (callback != null ) {
|
||||
callback.actionChange(data.get(position));
|
||||
}
|
||||
if (window != null) {
|
||||
window.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通用弹窗
|
||||
*
|
||||
* @param anchor
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
public static void showChannelFilterListWindow(View anchor, final List<ChannelType> data , final KeyItemActionListener<ChannelType> callback) {
|
||||
if (anchor == null || anchor.getContext() == null) {
|
||||
return;
|
||||
}
|
||||
Context context = anchor.getContext();
|
||||
View rootView = View.inflate(context, R.layout.window_simple_listview, null);
|
||||
final PopupWindow window = new PopupWindow(context);
|
||||
window.setWidth(Util.getHeight(ContextUtil.getContext()) / 2);
|
||||
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
window.setOutsideTouchable(true);
|
||||
window.setTouchable(true);
|
||||
window.setFocusable(true);
|
||||
window.setBackgroundDrawable(new ColorDrawable(0xffffff));
|
||||
|
||||
window.setContentView(rootView);
|
||||
window.showAsDropDown(anchor, (int) (anchor.getWidth() * 1.5), - DisplayUtil.dip2px(anchor.getContext(), 37), Gravity.LEFT | Gravity.BOTTOM);
|
||||
ListView listView = rootView.findViewById(R.id.list_view);
|
||||
|
||||
ArrayAdapter<ChannelType> adapter = new ArrayAdapter<ChannelType>(context, R.layout.item_textview, data);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (callback != null ) {
|
||||
callback.actionChange(data.get(position));
|
||||
}
|
||||
if (window != null) {
|
||||
window.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
EditText filter = rootView.findViewById(R.id.et_filter);
|
||||
filter.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// dosomething
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// dosomething
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
adapter.getFilter().filter(s.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void showNormalDialog(Activity context, String title) {
|
||||
View dialogView = context.getLayoutInflater().inflate(R.layout.dialog_tips, null);
|
||||
final AlertDialog dialog = new AlertDialog.Builder(context).setView(dialogView).create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.setCancelable(false);
|
||||
|
||||
TextView tvTitle = dialogView.findViewById(R.id.title);
|
||||
tvTitle.setText(title);
|
||||
|
||||
dialogView.findViewById(R.id.confirm).setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.DeviceInfoUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* ClassName : MSDKLogVM
|
||||
* Description : Log展示
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2022/5/7 12:17 下午
|
||||
* Copyright : ©2022 DJI All Rights Reserved.
|
||||
*/
|
||||
class APPSilentlyUpgradeVM : DJIViewModel() {
|
||||
|
||||
private val testPackageName = "com.dji.test"
|
||||
private val testApkName = "app-debug.apk"
|
||||
|
||||
/**
|
||||
* 只适配了M350和Mavic3行业版本的遥控器。
|
||||
* 设置以后,对应包名的apk,通过代码安装时,遥控不再需要用户操作。
|
||||
* 重启遥控后设置清空。
|
||||
*/
|
||||
fun setAPPSilentlyUpgrade(context: Context) {
|
||||
RemoteControllerKey.KeyAPPSilentlyUpgrade.create().action(testPackageName, {
|
||||
toastResult?.postValue(DJIToastResult.success(testPackageName))
|
||||
}) {
|
||||
toastResult?.postValue(DJIToastResult.failed("$it $testPackageName"))
|
||||
}
|
||||
}
|
||||
|
||||
//本示例,安装包路径: /sdcard/Android/data/你的app的包名/files/你的apk文件名.apk
|
||||
//实际路径请按自身需求设置,通过FileProvider安装APK
|
||||
//测试apk(app-debug.apk)在sample的assets目录下
|
||||
fun installApkWithOutNotice(context: Context) {
|
||||
FileUtils.copyAssetsFileIfNeed(context, "apk/$testApkName", File(context.getExternalFilesDir("/"), testApkName))
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val file = File(context.getExternalFilesDir("/"), testApkName)
|
||||
val uri = FileProvider.getUriForFile(context, DeviceInfoUtil.getPackageName() + ".fileProvider", file)
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.R
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.sdk.keyvalue.value.remotecontroller.PairingState
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.manager.SDKManager
|
||||
import dji.v5.utils.common.StringUtils
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/2/14
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class BaseMainActivityVm : DJIViewModel() {
|
||||
|
||||
val sdkNews = MutableLiveData<SDKNews>()
|
||||
|
||||
init {
|
||||
updateNews()
|
||||
}
|
||||
|
||||
private fun updateNews() {
|
||||
sdkNews.postValue(SDKNews(R.string.news_title, R.string.news_description, StringUtils.getResStr(R.string.news_date)))
|
||||
}
|
||||
|
||||
fun doPairing(callback: ((String) -> Unit)? = null) {
|
||||
if (!SDKManager.getInstance().isRegistered) {
|
||||
return
|
||||
}
|
||||
RemoteControllerKey.KeyPairingStatus.create().get({
|
||||
if (it == PairingState.PAIRING) {
|
||||
RemoteControllerKey.KeyStopPairing.create().action()
|
||||
callback?.invoke(StringUtils.getResStr(R.string.stop_pairing))
|
||||
} else {
|
||||
RemoteControllerKey.KeyRequestPairing.create().action()
|
||||
callback?.invoke(StringUtils.getResStr(R.string.start_pairing))
|
||||
}
|
||||
}) {
|
||||
callback?.invoke(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
data class SDKNews(
|
||||
var title: Int,
|
||||
var description: Int,
|
||||
var date: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
|
||||
class BasicAircraftControlVM : DJIViewModel() {
|
||||
|
||||
fun startTakeOff(callback: CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>) {
|
||||
FlightControllerKey.KeyStartTakeoff.create().action({
|
||||
callback.onSuccess(it)
|
||||
}, { e: IDJIError ->
|
||||
callback.onFailure(e)
|
||||
})
|
||||
}
|
||||
|
||||
fun startLanding(callback: CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>) {
|
||||
FlightControllerKey.KeyStartAutoLanding.create().action({
|
||||
callback.onSuccess(it)
|
||||
}, { e: IDJIError ->
|
||||
callback.onFailure(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.view.Surface
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||
import dji.sdk.keyvalue.value.airlink.ChannelPriority
|
||||
import dji.sdk.keyvalue.value.camera.CameraType
|
||||
import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.flightassistant.VisionAssistDirection
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.listen
|
||||
import dji.v5.et.set
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager.FrameFormat
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager.ScaleType
|
||||
import dji.v5.utils.common.DJIExecutor
|
||||
import dji.v5.utils.common.DateUtils
|
||||
import dji.v5.utils.common.LogPath
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import dji.v5.utils.common.StringUtils
|
||||
import dji.v5.ux.R
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Locale
|
||||
|
||||
const val TAG = "CameraStreamDetailFragmentVM"
|
||||
|
||||
class CameraStreamDetailVM : DJIViewModel() {
|
||||
|
||||
private val _availableLensListData = MutableLiveData<List<CameraVideoStreamSourceType>>(ArrayList())
|
||||
private val _currentLensData = MutableLiveData(CameraVideoStreamSourceType.DEFAULT_CAMERA)
|
||||
private val _cameraName = MutableLiveData("Unknown")
|
||||
private val _isVisionAssistEnabled = MutableLiveData(false)
|
||||
private val _visionAssistViewDirection = MutableLiveData(VisionAssistDirection.UNKNOWN)
|
||||
private val _visionAssistViewDirectionRange = MutableLiveData<List<VisionAssistDirection>>(ArrayList())
|
||||
val cameraStreamEnableMap = MutableLiveData(emptyMap<ComponentIndexType, Boolean>())
|
||||
|
||||
private var cameraIndex = ComponentIndexType.UNKNOWN
|
||||
private var cameraType = ""
|
||||
private var isMotorOn = false
|
||||
private val visionAssistStatusListener = object :
|
||||
ICameraStreamManager.VisionAssistStatusListener {
|
||||
override fun onVisionAssistEnabled(isEnable: Boolean) {
|
||||
_isVisionAssistEnabled.postValue(isEnable)
|
||||
}
|
||||
|
||||
override fun onVisionAssistViewDirectionUpdated(mode: VisionAssistDirection) {
|
||||
_visionAssistViewDirection.postValue(mode)
|
||||
}
|
||||
|
||||
override fun onVisionAssistViewDirectionRangeUpdated(modes: MutableList<VisionAssistDirection>) {
|
||||
_visionAssistViewDirectionRange.postValue(modes)
|
||||
}
|
||||
}
|
||||
|
||||
private val availableCameraUpdatedListener = object :
|
||||
ICameraStreamManager.AvailableCameraUpdatedListener {
|
||||
override fun onAvailableCameraUpdated(availableCameraList: MutableList<ComponentIndexType>) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(map: MutableMap<ComponentIndexType, Boolean>) {
|
||||
cameraStreamEnableMap.postValue(map)
|
||||
}
|
||||
}
|
||||
|
||||
private var streamFile: File? = null
|
||||
private var streamFileOutputStream: FileOutputStream? = null
|
||||
|
||||
private val streamListener = ICameraStreamManager.ReceiveStreamListener { data, offset, length, info ->
|
||||
if (streamFile == null) {
|
||||
val fileName = "[${cameraIndex.name}]${DateUtils.getSystemTime()}.${info.mimeType.name.lowercase(Locale.ROOT)}"
|
||||
ToastUtils.showToast("begin to save,$fileName")
|
||||
streamFile = File(LogUtils.getLogPath(), fileName)
|
||||
streamFileOutputStream = FileOutputStream(streamFile)
|
||||
return@ReceiveStreamListener
|
||||
}
|
||||
DJIExecutor.getExecutor().execute {
|
||||
try {
|
||||
streamFileOutputStream?.write(data, offset, length)
|
||||
} catch (e: Exception) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
setCameraIndex(ComponentIndexType.UNKNOWN)
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeAvailableCameraUpdatedListener(availableCameraUpdatedListener)
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeVisionAssistStatusListener(visionAssistStatusListener)
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
doStopDownloadStreamToLocal()
|
||||
}
|
||||
|
||||
fun setCameraIndex(cameraIndex: ComponentIndexType) {
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
if (this.cameraIndex == cameraIndex) {
|
||||
return
|
||||
}
|
||||
this.cameraIndex = cameraIndex
|
||||
if (this.cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
listenCameraName()
|
||||
listenAvailableLens()
|
||||
listenCurrentLens()
|
||||
listenVisionAssistStatus()
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addAvailableCameraUpdatedListener(availableCameraUpdatedListener)
|
||||
}
|
||||
|
||||
private fun listenCameraName() {
|
||||
CameraKey.KeyCameraType.create(cameraIndex).listen(this) {
|
||||
cameraType = it?.name ?: CameraType.NOT_SUPPORTED.name
|
||||
updateCameraName()
|
||||
}
|
||||
|
||||
FlightControllerKey.KeyAreMotorsOn.create().listen(this) {
|
||||
isMotorOn = it == true
|
||||
updateCameraName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCameraName() {
|
||||
_cameraName.postValue("")
|
||||
if (cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
if (cameraIndex == ComponentIndexType.FPV) {
|
||||
_cameraName.postValue(ComponentIndexType.FPV.name)
|
||||
return
|
||||
}
|
||||
if (cameraIndex == ComponentIndexType.VISION_ASSIST) {
|
||||
var msg = ComponentIndexType.VISION_ASSIST.name
|
||||
if (!isMotorOn) {
|
||||
msg = "$msg(${StringUtils.getResStr(R.string.uxsdk_assistant_video_empty_text)})"
|
||||
}
|
||||
_cameraName.postValue(msg)
|
||||
return
|
||||
}
|
||||
_cameraName.postValue(cameraType)
|
||||
}
|
||||
|
||||
private fun listenAvailableLens() {
|
||||
_availableLensListData.postValue(arrayListOf())
|
||||
if (cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
CameraKey.KeyCameraVideoStreamSourceRange.create(cameraIndex).listen(this) {
|
||||
val list: List<CameraVideoStreamSourceType> = it ?: arrayListOf()
|
||||
_availableLensListData.postValue(list)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenCurrentLens() {
|
||||
_currentLensData.postValue(CameraVideoStreamSourceType.DEFAULT_CAMERA)
|
||||
if (cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
CameraKey.KeyCameraVideoStreamSource.create(cameraIndex).listen(this) {
|
||||
if (it != null) {
|
||||
_currentLensData.postValue(it)
|
||||
} else {
|
||||
_currentLensData.postValue(CameraVideoStreamSourceType.DEFAULT_CAMERA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenVisionAssistStatus() {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addVisionAssistStatusListener(visionAssistStatusListener)
|
||||
}
|
||||
|
||||
fun changeCameraLens(lensType: CameraVideoStreamSourceType) {
|
||||
CameraKey.KeyCameraVideoStreamSource.create(cameraIndex).set(lensType)
|
||||
}
|
||||
|
||||
fun putCameraStreamSurface(
|
||||
surface: Surface, width: Int, height: Int, scaleType: ScaleType
|
||||
) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.putCameraStreamSurface(cameraIndex, surface, width, height, scaleType)
|
||||
}
|
||||
|
||||
fun removeCameraStreamSurface(surface: Surface) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeCameraStreamSurface(surface)
|
||||
}
|
||||
|
||||
fun downloadYUVImageToLocal(format: FrameFormat, formatName: String) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addFrameListener(
|
||||
cameraIndex,
|
||||
format,
|
||||
object : ICameraStreamManager.CameraFrameListener {
|
||||
override fun onFrame(frameData: ByteArray, offset: Int, length: Int, width: Int, height: Int, format: FrameFormat) {
|
||||
try {
|
||||
val dirs = File(LogUtils.getLogPath() + "STREAM_PIC")
|
||||
if (!dirs.exists()) {
|
||||
dirs.mkdirs()
|
||||
}
|
||||
val fileName = "[${cameraIndex.name}][$width x $height]${DateUtils.getSystemTime()}.${formatName}"
|
||||
val file = File(dirs.absolutePath, fileName)
|
||||
FileOutputStream(file).use { stream ->
|
||||
stream.write(frameData, offset, length)
|
||||
stream.flush()
|
||||
stream.close()
|
||||
ToastUtils.showToast("Save to : ${file.path}")
|
||||
}
|
||||
LogUtils.i(TAG, "Save to : ${file.path}")
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showToast("Save fail : $e")
|
||||
}
|
||||
// Because only one frame needs to be saved, you need to call removeOnFrameListener here
|
||||
// If you need to read frame data for a long time, you can choose to actually call remove OnFrameListener according to your needs
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeFrameListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun enableVisionAssist(enable: Boolean) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.enableVisionAssist(enable, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
ToastUtils.showToast("enableVisionAssist onSuccess $enable")
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
ToastUtils.showToast("enableVisionAssist onFailure $enable,error:$error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setVisionAssistViewDirection(direction: VisionAssistDirection) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.setVisionAssistViewDirection(direction, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
ToastUtils.showToast("set Direction onSuccess $direction")
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
ToastUtils.showToast("set Direction onFailure $direction,error:$error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun beginDownloadStreamToLocal() {
|
||||
if (streamFile != null) {
|
||||
ToastUtils.showToast("Pls stop first.")
|
||||
return
|
||||
}
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addReceiveStreamListener(cameraIndex, streamListener)
|
||||
}
|
||||
|
||||
fun stopDownloadStreamToLocal() {
|
||||
if (streamFile == null) {
|
||||
ToastUtils.showToast("Pls begin first.")
|
||||
return
|
||||
}
|
||||
ToastUtils.showToast("stop to save,${streamFile?.name}")
|
||||
doStopDownloadStreamToLocal()
|
||||
}
|
||||
|
||||
fun setStreamEncoderBitrate(bitrate: Int) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.setStreamEncoderBitrate(cameraIndex, bitrate)
|
||||
}
|
||||
|
||||
fun getStreamEncoderBitrate() = MediaDataCenter.getInstance().cameraStreamManager.getStreamEncoderBitrate(cameraIndex)
|
||||
|
||||
fun changeCameraMode(mode: CameraMode) {
|
||||
CameraKey.KeyCameraMode.create().set(mode)
|
||||
}
|
||||
|
||||
fun setStreamPriority(priority: ChannelPriority) = MediaDataCenter.getInstance().cameraStreamManager.setStreamPriority(cameraIndex, priority)
|
||||
|
||||
fun getStreamPriority() = MediaDataCenter.getInstance().cameraStreamManager.getStreamPriority(cameraIndex)
|
||||
|
||||
fun enableStream(enable: Boolean) = MediaDataCenter.getInstance().cameraStreamManager.enableStream(cameraIndex, enable)
|
||||
|
||||
private fun doStopDownloadStreamToLocal() {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeReceiveStreamListener(streamListener)
|
||||
try {
|
||||
streamFileOutputStream?.flush()
|
||||
streamFileOutputStream?.close()
|
||||
streamFileOutputStream = null
|
||||
streamFile = null
|
||||
} catch (e: Exception) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
val availableLensListData: LiveData<List<CameraVideoStreamSourceType>>
|
||||
get() = _availableLensListData
|
||||
|
||||
val currentLensData: LiveData<CameraVideoStreamSourceType>
|
||||
get() = _currentLensData
|
||||
|
||||
val cameraName: LiveData<String>
|
||||
get() = _cameraName
|
||||
|
||||
val isVisionAssistEnabled: LiveData<Boolean>
|
||||
get() = _isVisionAssistEnabled
|
||||
|
||||
val visionAssistViewDirection: LiveData<VisionAssistDirection>
|
||||
get() = _visionAssistViewDirection
|
||||
|
||||
val visionAssistViewDirectionRange: LiveData<List<VisionAssistDirection>>
|
||||
get() = _visionAssistViewDirectionRange
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager.AvailableCameraUpdatedListener
|
||||
|
||||
class CameraStreamListVM : DJIViewModel(), AvailableCameraUpdatedListener {
|
||||
|
||||
private val _availableCameraListData = MutableLiveData<List<ComponentIndexType>>(ArrayList())
|
||||
|
||||
init {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addAvailableCameraUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeAvailableCameraUpdatedListener(this)
|
||||
}
|
||||
|
||||
val availableCameraListData: LiveData<List<ComponentIndexType>>
|
||||
get() = _availableCameraListData
|
||||
|
||||
override fun onAvailableCameraUpdated(availableCameraList: List<ComponentIndexType>) {
|
||||
_availableCameraListData.postValue(availableCameraList)
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(cameraStreamEnableMap: MutableMap<ComponentIndexType, Boolean>) {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dji.sampleV5.aircraft.util.DJIToastUtil
|
||||
import dji.v5.utils.common.LogUtils
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/7/5
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
open class DJIViewModel : ViewModel() {
|
||||
val toastResult
|
||||
get() = DJIToastUtil.dJIToastLD
|
||||
|
||||
val logTag = LogUtils.getTag(this)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.v5.manager.dataprotect.DataProtectionManager
|
||||
import dji.v5.utils.common.DJIExecutor
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import dji.v5.utils.common.LogUtils.OnExportLogProgressCallback
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/6/30
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class DataProtectionVm : DJIViewModel() {
|
||||
|
||||
fun agreeToProductImprovement(isAgree: Boolean) {
|
||||
DataProtectionManager.getInstance().agreeToProductImprovement(isAgree)
|
||||
}
|
||||
|
||||
fun isAgreeToProductImprovement(): Boolean {
|
||||
return DataProtectionManager.getInstance().isAgreeToProductImprovement
|
||||
}
|
||||
|
||||
fun enableLog(enable: Boolean) {
|
||||
DataProtectionManager.getInstance().enableMSDKLog(enable)
|
||||
}
|
||||
|
||||
fun isLogEnable(): Boolean {
|
||||
return DataProtectionManager.getInstance().isMSDKLogEnabled
|
||||
}
|
||||
|
||||
fun logPath(): String {
|
||||
return DataProtectionManager.getInstance().msdkLogPath
|
||||
}
|
||||
|
||||
fun clearLog(): Boolean {
|
||||
return DataProtectionManager.getInstance().clearMSDKLog()
|
||||
}
|
||||
|
||||
fun zipAndExportLog() {
|
||||
DJIExecutor.getExecutorFor(DJIExecutor.Purpose.IO).execute {
|
||||
LogUtils.zipAndExportLog("logs", object : OnExportLogProgressCallback {
|
||||
override fun onExportBegin() {
|
||||
ToastUtils.showToast("ZipAndExportLog begin")
|
||||
}
|
||||
|
||||
override fun onExportProgress(progress: Int) {
|
||||
ToastUtils.showToast("ZipAndExportLog Progress:$progress")
|
||||
}
|
||||
|
||||
override fun onExportFailed(result: String?) {
|
||||
ToastUtils.showToast("ZipAndExportLog Failed:$result")
|
||||
}
|
||||
|
||||
override fun onExportSuccess(filePath: String?) {
|
||||
ToastUtils.showToast("ZipAndExportLog Success:$filePath")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.v5.manager.diagnostic.*
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/6/30
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class DiagnosticVm : DJIViewModel() {
|
||||
|
||||
val deviceHealthInfos = MutableLiveData(ArrayList<DJIDeviceHealthInfo>())
|
||||
|
||||
val lastDeviceStatus = MutableLiveData(DJIDeviceStatus.NORMAL)
|
||||
val currentDeviceStatus = MutableLiveData(DJIDeviceStatus.NORMAL)
|
||||
|
||||
private val deviceHealthInfoChangeListener = DJIDeviceHealthInfoChangeListener {
|
||||
updateDeviceHealthInfo(it as ArrayList<DJIDeviceHealthInfo>)
|
||||
}
|
||||
|
||||
private val deviceStatusChangeListener = DJIDeviceStatusChangeListener { from, to ->
|
||||
updateDeviceStatus(from, to)
|
||||
}
|
||||
|
||||
private fun updateDeviceHealthInfo(infos: ArrayList<DJIDeviceHealthInfo>) {
|
||||
deviceHealthInfos.value?.clear()
|
||||
deviceHealthInfos.value?.addAll(infos)
|
||||
deviceHealthInfos.postValue(deviceHealthInfos.value)
|
||||
}
|
||||
|
||||
private fun updateDeviceStatus(form: DJIDeviceStatus, to: DJIDeviceStatus) {
|
||||
lastDeviceStatus.value = form
|
||||
currentDeviceStatus.value = to
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
stopListenDeviceHealthInfoChange()
|
||||
stopListenDeviceStatusChange()
|
||||
}
|
||||
|
||||
fun startListenDeviceHealthInfoChange() {
|
||||
DeviceHealthManager.getInstance()
|
||||
.addDJIDeviceHealthInfoChangeListener(deviceHealthInfoChangeListener)
|
||||
}
|
||||
|
||||
fun stopListenDeviceHealthInfoChange() {
|
||||
DeviceHealthManager.getInstance()
|
||||
.removeDJIDeviceHealthInfoChangeListener(deviceHealthInfoChangeListener)
|
||||
}
|
||||
|
||||
fun getCurrentDeviceHealthInfos() {
|
||||
updateDeviceHealthInfo(DeviceHealthManager.getInstance().currentDJIDeviceHealthInfos as ArrayList<DJIDeviceHealthInfo>)
|
||||
}
|
||||
|
||||
fun startListenDeviceStatusChange() {
|
||||
DeviceStatusManager.getInstance()
|
||||
.addDJIDeviceStatusChangeListener(deviceStatusChangeListener)
|
||||
}
|
||||
|
||||
fun stopListenDeviceStatusChange() {
|
||||
DeviceStatusManager.getInstance()
|
||||
.removeDJIDeviceStatusChangeListener(deviceStatusChangeListener)
|
||||
}
|
||||
|
||||
fun getCurrentDeviceStatus() {
|
||||
currentDeviceStatus.value = DeviceStatusManager.getInstance().currentDJIDeviceStatus
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import dji.v5.manager.aircraft.flightrecord.FlightLogManager
|
||||
|
||||
/**
|
||||
* Description : FLightReecordVM
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2021/7/15 10:51 上午
|
||||
* Copyright : ©2021 DJI All Rights Reserved.
|
||||
*/
|
||||
class FlightRecordVM : DJIViewModel() {
|
||||
fun getFlightLogPath(): String {
|
||||
return FlightLogManager.getInstance().flightRecordPath
|
||||
}
|
||||
|
||||
fun getFlyClogPath(): String {
|
||||
return FlightLogManager.getInstance().flyClogPath
|
||||
}
|
||||
}
|
||||
313
sample/src/main/java/dji/sampleV5/aircraft/models/FlySafeVM.kt
Normal file
313
sample/src/main/java/dji/sampleV5/aircraft/models/FlySafeVM.kt
Normal file
@@ -0,0 +1,313 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sampleV5.aircraft.util.Util
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseComponent
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseListener
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseState
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseUpgradeMode
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeNotificationListener
|
||||
import dji.v5.manager.aircraft.flysafe.FlyZoneManager
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeDatabaseInfo
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeReturnToHomeInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeSeriousWarningInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeTipInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeWarningInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlyZoneInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlyZoneLicenseInfo
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/8/12
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class FlySafeVM : DJIViewModel() {
|
||||
|
||||
val flySafeWarningInformation = MutableLiveData<FlySafeWarningInformation>()
|
||||
val flySafeSeriousWarningInformation = MutableLiveData<FlySafeSeriousWarningInformation>()
|
||||
val flySafeReturnToHomeInformation = MutableLiveData<FlySafeReturnToHomeInformation>()
|
||||
val flySafeTipInformation = MutableLiveData<FlySafeTipInformation>()
|
||||
val flyZoneInformation = MutableLiveData<MutableList<FlyZoneInformation>>()
|
||||
val serverFlyZoneLicenseInfo = MutableLiveData<MutableList<FlyZoneLicenseInfo>>()
|
||||
val aircraftFlyZoneLicenseInfo = MutableLiveData<MutableList<FlyZoneLicenseInfo>>()
|
||||
|
||||
val importAndSyncState = MutableLiveData<ImportAndSyncState>()
|
||||
val dataBaseInfo = MutableLiveData<DataBaseInfo>()
|
||||
val dataUpgradeState = MutableLiveData<FlySafeDatabaseState>()
|
||||
|
||||
private val availableTestFlySafeDynamicDatabaseName = "de.geojson"
|
||||
private val unAvailableTestFlySafeDynamicDatabaseName = "France.json"
|
||||
|
||||
private val flySafeNotificationListener = object : FlySafeNotificationListener {
|
||||
|
||||
override fun onWarningNotificationUpdate(info: FlySafeWarningInformation) {
|
||||
flySafeWarningInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onSeriousWarningNotificationUpdate(info: FlySafeSeriousWarningInformation) {
|
||||
flySafeSeriousWarningInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onReturnToHomeNotificationUpdate(info: FlySafeReturnToHomeInformation) {
|
||||
flySafeReturnToHomeInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onTipNotificationUpdate(info: FlySafeTipInformation) {
|
||||
flySafeTipInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onSurroundingFlyZonesUpdate(infos: MutableList<FlyZoneInformation>) {
|
||||
flyZoneInformation.postValue(infos)
|
||||
}
|
||||
}
|
||||
|
||||
fun initListener() {
|
||||
FlyZoneManager.getInstance().addFlySafeNotificationListener(flySafeNotificationListener)
|
||||
addFlySafeDatabaseListener()
|
||||
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
FlyZoneManager.getInstance().removeFlySafeNotificationListener(flySafeNotificationListener)
|
||||
removeFlySafeDatabaseListener()
|
||||
}
|
||||
|
||||
fun getAircraftLocation(): LocationCoordinate3D = FlightControllerKey.KeyAircraftLocation3D.create().get(LocationCoordinate3D(0.0, 0.0,0.0))
|
||||
|
||||
fun getFlyZonesInSurroundingArea(location: LocationCoordinate2D) {
|
||||
FlyZoneManager.getInstance().getFlyZonesInSurroundingArea(location, object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<MutableList<FlyZoneInformation>> {
|
||||
|
||||
override fun onSuccess(infos: MutableList<FlyZoneInformation>?) {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
flyZoneInformation.postValue(infos ?: arrayListOf())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun downloadFlyZoneLicensesFromServer() {
|
||||
FlyZoneManager.getInstance().downloadFlyZoneLicensesFromServer(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<MutableList<FlyZoneLicenseInfo>> {
|
||||
|
||||
override fun onSuccess(infos: MutableList<FlyZoneLicenseInfo>?) {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
serverFlyZoneLicenseInfo.postValue(infos ?: arrayListOf())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pushFlyZoneLicensesToAircraft() {
|
||||
FlyZoneManager.getInstance().pushFlyZoneLicensesToAircraft(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
pullFlyZoneLicensesFromAircraft()
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pullFlyZoneLicensesFromAircraft() {
|
||||
FlyZoneManager.getInstance().pullFlyZoneLicensesFromAircraft(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<MutableList<FlyZoneLicenseInfo>> {
|
||||
|
||||
override fun onSuccess(infos: MutableList<FlyZoneLicenseInfo>?) {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
|
||||
aircraftFlyZoneLicenseInfo.postValue(infos ?: arrayListOf())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun deleteFlyZoneLicensesFromAircraft() {
|
||||
FlyZoneManager.getInstance().deleteFlyZoneLicensesFromAircraft(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlyZoneLicensesEnabled(info: FlyZoneLicenseInfo, isEnable: Boolean) {
|
||||
FlyZoneManager.getInstance().setFlyZoneLicensesEnabled(info, isEnable, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
pullFlyZoneLicensesFromAircraft()
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun unlockAuthorizationFlyZone(flyZoneID: Int) {
|
||||
FlyZoneManager.getInstance().unlockAuthorizationFlyZone(flyZoneID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun unlockAllEnhancedWarningFlyZone() {
|
||||
FlyZoneManager.getInstance().unlockAllEnhancedWarningFlyZone(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pushFlySafeDynamicDatabaseToAircraftAndApp(fileName: String) {
|
||||
FlyZoneManager.getInstance().importFlySafeDynamicDatabaseToMSDK(fileName, object :
|
||||
CommonCallbacks.CompletionCallbackWithProgress<Double> {
|
||||
override fun onProgressUpdate(progress: Double?) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(progress!!.toFloat().roundToInt()))
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
importAndSyncState.postValue(ImportAndSyncState(100))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(-1, error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun syncFlySafeMSDKDatabaseToAircraft() {
|
||||
FlyZoneManager.getInstance().pushFlySafeDynamicDatabaseToAircraft(object :
|
||||
CommonCallbacks.CompletionCallbackWithProgress<Double> {
|
||||
override fun onProgressUpdate(progress: Double?) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(Math.round(progress!!.toFloat())))
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
importAndSyncState.postValue(ImportAndSyncState(100))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(-1, error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlySafeDynamicDatabaseUpgradeMode(flySafeDynamicDatabaseUpgradeMode: FlySafeDatabaseUpgradeMode) {
|
||||
FlyZoneManager.getInstance().setFlySafeDynamicDatabaseUpgradeMode(flySafeDynamicDatabaseUpgradeMode, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
importAndSyncState.postValue(ImportAndSyncState(100))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(-1, error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun addFlySafeDatabaseListener() {
|
||||
FlyZoneManager.getInstance().addFlySafeDatabaseListener(object : FlySafeDatabaseListener {
|
||||
override fun onFlySafeDatabaseInfoUpdate(flySafeDatabaseInfo: FlySafeDatabaseInfo) {
|
||||
LogUtils.i("testFly", "dataName :" + flySafeDatabaseInfo.databaseName + " compnent :" + flySafeDatabaseInfo.component)
|
||||
dataBaseInfo.postValue(
|
||||
DataBaseInfo(
|
||||
flySafeDatabaseInfo.databaseName,
|
||||
formatCEDBTime(flySafeDatabaseInfo.databaseTimeStamp * 1000),
|
||||
Util.byte2AdaptiveUnitStrDefault(flySafeDatabaseInfo.databaseSize),
|
||||
flySafeDatabaseInfo.component, flySafeDatabaseInfo.flySafeDatabaseUpgradeMode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFlySafeDatabaseStateUpdate(flySafeDatabaseState: FlySafeDatabaseState) {
|
||||
dataUpgradeState.postValue(flySafeDatabaseState)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun pushAvailableTestFlySafeDynamicDatabaseToApp() {
|
||||
val file = File(ContextUtil.getContext().getExternalFilesDir("/"), availableTestFlySafeDynamicDatabaseName)
|
||||
FileUtils.copyAssetsFileIfNeed(ContextUtil.getContext(), "flysafe/$availableTestFlySafeDynamicDatabaseName", file)
|
||||
pushFlySafeDynamicDatabaseToAircraftAndApp(file.path)
|
||||
}
|
||||
|
||||
fun pushUnAvailableTestFlySafeDynamicDatabaseToApp() {
|
||||
val file = File(ContextUtil.getContext().getExternalFilesDir("/"), unAvailableTestFlySafeDynamicDatabaseName)
|
||||
FileUtils.copyAssetsFileIfNeed(ContextUtil.getContext(), "flysafe/$unAvailableTestFlySafeDynamicDatabaseName", file)
|
||||
pushFlySafeDynamicDatabaseToAircraftAndApp(file.path)
|
||||
}
|
||||
|
||||
private fun formatCEDBTime(timestamp: Long): String {
|
||||
val format = SimpleDateFormat("yyyy/MM/dd")
|
||||
return format.format(timestamp)
|
||||
}
|
||||
|
||||
private fun removeFlySafeDatabaseListener() {
|
||||
FlyZoneManager.getInstance().clearAllFlySafeDatabaseListener()
|
||||
}
|
||||
|
||||
data class DataBaseInfo(
|
||||
var dataBaseName: String,
|
||||
var dataBaseTime: String,
|
||||
var dataBaseSize: String,
|
||||
var component: FlySafeDatabaseComponent,
|
||||
var upgradeMode: FlySafeDatabaseUpgradeMode
|
||||
)
|
||||
|
||||
data class ImportAndSyncState(
|
||||
var importAndSyncProgress: Int,
|
||||
var error: IDJIError? = null,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelLazy
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
|
||||
// ViewModelStore for Global Use
|
||||
val globalViewModelStore = ViewModelStore()
|
||||
|
||||
/**
|
||||
* Create a global ViewModel in the activity
|
||||
*/
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ComponentActivity.globalViewModels(
|
||||
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
|
||||
): Lazy<VM> {
|
||||
val factoryPromise = factoryProducer ?: {
|
||||
defaultViewModelProviderFactory
|
||||
}
|
||||
return ViewModelLazy(VM::class, { globalViewModelStore }, factoryPromise)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a global ViewModel in the fragment
|
||||
*/
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Fragment.globalViewModels(
|
||||
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
|
||||
): Lazy<VM> {
|
||||
val factoryPromise = factoryProducer ?: {
|
||||
defaultViewModelProviderFactory
|
||||
}
|
||||
return ViewModelLazy(VM::class, { globalViewModelStore }, factoryPromise)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a global ViewModel in the fragment
|
||||
*/
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Application.globalViewModels(): Lazy<VM> {
|
||||
val factory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
|
||||
return ViewModelLazy(VM::class, { globalViewModelStore }, { factory })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.aibox.IntelligentBoxAppInfo
|
||||
import dji.v5.manager.aircraft.aibox.IntelligentBoxInfo
|
||||
import dji.v5.manager.aircraft.aibox.IntelligentBoxInfoListener
|
||||
import dji.v5.manager.aircraft.payload.PayloadCenter
|
||||
import dji.v5.manager.aircraft.payload.PayloadIndexType
|
||||
|
||||
|
||||
/**
|
||||
* Description :
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/12/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class IntelligentBoxVM : DJIViewModel() {
|
||||
private lateinit var payloadIndexType: PayloadIndexType
|
||||
private val intelligentBoxMap = PayloadCenter.getInstance().intelligentBoxManager
|
||||
val intelligentBoxInfo = MutableLiveData<IntelligentBoxInfo>()
|
||||
val intelligentBoxAppInfos = MutableLiveData<List<IntelligentBoxAppInfo>>()
|
||||
|
||||
private val intelligentBoxInfoListener: IntelligentBoxInfoListener = object :
|
||||
IntelligentBoxInfoListener {
|
||||
|
||||
override fun onBoxInfoUpdate(info: IntelligentBoxInfo) {
|
||||
intelligentBoxInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onBoxAppInfoUpdate(infos: List<IntelligentBoxAppInfo>) {
|
||||
intelligentBoxAppInfos.postValue(infos)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoxSerialNumber() {
|
||||
intelligentBoxMap[payloadIndexType]?.getBoxSerialNumber(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<String> {
|
||||
override fun onSuccess(t: String) {
|
||||
sendToastMsg(DJIToastResult.success("getBoxSerialNumber: $t"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("getBoxSerialNumber,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun enableApp(appID: String) {
|
||||
intelligentBoxMap[payloadIndexType]?.enableApp(appID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
sendToastMsg(DJIToastResult.success("enableApp,success"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("enableApp,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun disableApp(appID: String) {
|
||||
intelligentBoxMap[payloadIndexType]?.disableApp(appID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
sendToastMsg(DJIToastResult.success("disableApp,success"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("disableApp,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun uninstallApp(appID: String) {
|
||||
intelligentBoxMap[payloadIndexType]?.uninstallApp(appID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
sendToastMsg(DJIToastResult.success("uninstallApp,success"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("uninstallApp,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun initListener(payloadIndexType: PayloadIndexType) {
|
||||
this.payloadIndexType = payloadIndexType
|
||||
val iPayloadManager = intelligentBoxMap[payloadIndexType]
|
||||
iPayloadManager?.addBoxInfoListener(intelligentBoxInfoListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
intelligentBoxMap[payloadIndexType]?.removeBoxInfoListener(intelligentBoxInfoListener)
|
||||
}
|
||||
|
||||
private fun sendToastMsg(djiToastResult: DJIToastResult) {
|
||||
toastResult?.postValue(djiToastResult)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.common.DoubleRect
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.listen
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.intelligent.AutoSensingInfo
|
||||
import dji.v5.manager.intelligent.AutoSensingInfoListener
|
||||
import dji.v5.manager.intelligent.AutoSensingTarget
|
||||
import dji.v5.manager.intelligent.IMissionCapabilityListener
|
||||
import dji.v5.manager.intelligent.IMissionInfoListener
|
||||
import dji.v5.manager.intelligent.IntelligentFlightInfo
|
||||
import dji.v5.manager.intelligent.IntelligentFlightInfoListener
|
||||
import dji.v5.manager.intelligent.IntelligentFlightManager
|
||||
import dji.v5.manager.intelligent.IntelligentModel
|
||||
import dji.v5.manager.intelligent.TargetType
|
||||
import dji.v5.manager.intelligent.flyto.FlyToInfo
|
||||
import dji.v5.manager.intelligent.flyto.FlyToParam
|
||||
import dji.v5.manager.intelligent.flyto.FlyToTarget
|
||||
import dji.v5.manager.intelligent.poi.POICapability
|
||||
import dji.v5.manager.intelligent.poi.POIInfo
|
||||
import dji.v5.manager.intelligent.poi.POIParam
|
||||
import dji.v5.manager.intelligent.poi.POITarget
|
||||
import dji.v5.manager.intelligent.smarttrack.SmartTrackInfo
|
||||
import dji.v5.manager.intelligent.smarttrack.SmartTrackTarget
|
||||
import dji.v5.manager.intelligent.spotlight.SpotLightInfo
|
||||
import dji.v5.manager.intelligent.spotlight.SpotLightTarget
|
||||
|
||||
class IntelligentFlightVM : DJIViewModel() {
|
||||
|
||||
val aircraftHeight = MutableLiveData<Double>()
|
||||
val aircraftLocation = MutableLiveData<LocationCoordinate2D>()
|
||||
|
||||
val intelligentFlightInfo = MutableLiveData<IntelligentFlightInfo>()
|
||||
val autoSensingInfo = MutableLiveData<AutoSensingInfo>()
|
||||
val intelligentModels = MutableLiveData<List<IntelligentModel>>()
|
||||
val runningModelIndex = MutableLiveData<Int>()
|
||||
|
||||
val poiInfo = MutableLiveData<POIInfo>()
|
||||
val poiTarget = MutableLiveData<POITarget>()
|
||||
|
||||
val flyToInfo = MutableLiveData<FlyToInfo>()
|
||||
val flyToTarget = MutableLiveData<FlyToTarget>()
|
||||
|
||||
val spotLightInfo = MutableLiveData<SpotLightInfo>()
|
||||
val spotLightTarget = MutableLiveData<SpotLightTarget>()
|
||||
|
||||
val smartTrackInfo = MutableLiveData<SmartTrackInfo>()
|
||||
val smartTrackTarget = MutableLiveData<SmartTrackTarget>()
|
||||
|
||||
private val intelligentFlightInfoListener: IntelligentFlightInfoListener = object :
|
||||
IntelligentFlightInfoListener {
|
||||
override fun onIntelligentFlightInfoUpdate(info: IntelligentFlightInfo) {
|
||||
intelligentFlightInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onIntelligentFlightErrorUpdate(error: IDJIError) {
|
||||
}
|
||||
}
|
||||
|
||||
private val autoSensingInfoListener: AutoSensingInfoListener = object :
|
||||
AutoSensingInfoListener {
|
||||
override fun onAutoSensingInfoUpdate(info: AutoSensingInfo) {
|
||||
autoSensingInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onTrackingTargetUpdate(target: AutoSensingTarget) {
|
||||
// super.onTrackingTargetUpdate(target)
|
||||
}
|
||||
|
||||
override fun onIntelligentModelUpdate(models: MutableList<IntelligentModel>) {
|
||||
intelligentModels.postValue(models)
|
||||
}
|
||||
|
||||
override fun onRunningIntelligentModelUpdate(modelId: Int) {
|
||||
runningModelIndex.postValue(modelId)
|
||||
}
|
||||
}
|
||||
|
||||
private val poiInfoListener: IMissionInfoListener<POIInfo, POITarget> = object :
|
||||
IMissionInfoListener<POIInfo, POITarget> {
|
||||
override fun onMissionInfoUpdate(info: POIInfo) {
|
||||
poiInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: POITarget) {
|
||||
poiTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
private val poiCapabilityListener: IMissionCapabilityListener<POICapability> = IMissionCapabilityListener<POICapability> {
|
||||
//kk
|
||||
}
|
||||
|
||||
private val flyToInfoListener: IMissionInfoListener<FlyToInfo, FlyToTarget> = object :
|
||||
IMissionInfoListener<FlyToInfo, FlyToTarget> {
|
||||
override fun onMissionInfoUpdate(info: FlyToInfo) {
|
||||
flyToInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: FlyToTarget) {
|
||||
flyToTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
private val spotLightInfoListener: IMissionInfoListener<SpotLightInfo, SpotLightTarget> = object :
|
||||
IMissionInfoListener<SpotLightInfo, SpotLightTarget> {
|
||||
override fun onMissionInfoUpdate(info: SpotLightInfo) {
|
||||
spotLightInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: SpotLightTarget) {
|
||||
spotLightTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
private val smartTrackListener: IMissionInfoListener<SmartTrackInfo, SmartTrackTarget> = object :
|
||||
IMissionInfoListener<SmartTrackInfo, SmartTrackTarget> {
|
||||
override fun onMissionInfoUpdate(info: SmartTrackInfo) {
|
||||
smartTrackInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: SmartTrackTarget) {
|
||||
smartTrackTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
fun initListener() {
|
||||
IntelligentFlightManager.getInstance().addIntelligentFlightInfoListener(intelligentFlightInfoListener)
|
||||
IntelligentFlightManager.getInstance().addAutoSensingInfoListener(autoSensingInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.addMissionInfoListener(poiInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.addMissionCapabilityListener(poiCapabilityListener)
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.addMissionInfoListener(flyToInfoListener)
|
||||
IntelligentFlightManager.getInstance().spotLightManager.addMissionInfoListener(spotLightInfoListener)
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.addMissionInfoListener(smartTrackListener)
|
||||
FlightControllerKey.KeyAltitude.create().listen(this) { height ->
|
||||
height?.let {
|
||||
aircraftHeight.postValue(it)
|
||||
}
|
||||
}
|
||||
FlightControllerKey.KeyAircraftLocation.create().listen(this) { location ->
|
||||
location?.let {
|
||||
aircraftLocation.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanListener() {
|
||||
IntelligentFlightManager.getInstance().removeIntelligentFlightInfoListener(intelligentFlightInfoListener)
|
||||
IntelligentFlightManager.getInstance().removeAutoSensingInfoListener(autoSensingInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.removeMissionInfoListener(poiInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.removeMissionCapabilityListener(poiCapabilityListener)
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.removeMissionInfoListener(flyToInfoListener)
|
||||
IntelligentFlightManager.getInstance().spotLightManager.removeMissionInfoListener(spotLightInfoListener)
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.removeMissionInfoListener(smartTrackListener)
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
}
|
||||
|
||||
fun startAutoSensing() {
|
||||
IntelligentFlightManager.getInstance().startAutoSensing(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startAutoSensing"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startAutoSensing,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopAutoSensing() {
|
||||
IntelligentFlightManager.getInstance().stopAutoSensing(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopAutoSensing"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopAutoSensing,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectIntelligentModel(model: Int) {
|
||||
IntelligentFlightManager.getInstance().selectIntelligentModel(model, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectIntelligentModel,$model"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectIntelligentModel,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startPOIMission(target: POITarget) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.startMission(target, null,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startPOIMission"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startPOIMission,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopPOIMission() {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.stopMission(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopPOIMission"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopPOIMission,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updatePOIMissionTarget(target: POITarget) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.updateMissionTarget(target,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("updatePOIMissionTarget,$target"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("updatePOIMissionTarget,$target,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updatePOICircleSpeed(speed: Double) {
|
||||
val param = POIParam()
|
||||
param.circleSpeed = speed
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.updateMissionParam(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("updatePOICircleSpeed,$param"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("updatePOICircleSpeed,$param,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun lockCircularVelocity(lock: Boolean) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.lockCircularVelocity(
|
||||
lock,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("lockCircularVelocity:$lock"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("lockCircularVelocity:$lock,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun lockGimbalPitch(lock: Boolean) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.lockGimbalPitch(
|
||||
lock,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("lockGimbalPitch:$lock"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("lockGimbalPitch:$lock,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startFlyTo(target: FlyToTarget) {
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.startMission(target, null,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startFlyTo"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startFlyTo,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopFlyTo() {
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.stopMission(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopFlyTo"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopFlyTo,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlyToMode(mode: FlyToMode) {
|
||||
val param = FlyToParam()
|
||||
param.flyToMode = mode
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("setFlyToMode:$mode"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("setFlyToMode:$mode,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlyToHeight(height: Int) {
|
||||
val param = FlyToParam()
|
||||
param.height = height
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("setFlyToMode:$height"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("setFlyToMode:$height,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun enterSpotLightMode() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.enterSpotLightMode(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("enterSpotLightMode"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("enterSpotLightMode,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun exitSpotLightMode() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.exitSpotLightMode(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("exitSpotLightMode"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("exitSpotLightMode,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startSpotlight() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.startMission(null, null,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startSpotlight"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startSpotlight,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectAutoTarget(index: Int) {
|
||||
val param = SpotLightTarget()
|
||||
param.targetIndex = index
|
||||
param.targetType = TargetType.INDEX
|
||||
IntelligentFlightManager.getInstance().spotLightManager.updateMissionTarget(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectAutoTarget:$index"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectAutoTarget:$index,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectManualTarget(bound: DoubleRect) {
|
||||
val param = SpotLightTarget()
|
||||
param.targetRect = bound
|
||||
param.targetType = TargetType.RECT
|
||||
IntelligentFlightManager.getInstance().spotLightManager.updateMissionTarget(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectManualTarget:$bound"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectManualTarget:$bound,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun confirmTarget() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.confirmTarget(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("confirmTarget"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("confirmTarget,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopSpotlight() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.stopMission(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopSpotlight"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopSpotlight,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startSmartTrack() {
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.startMission(null, null, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startSmartTrack"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startSmartTrack,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopSmartTrack() {
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.stopMission(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopSmartTrack"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopSmartTrack,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectTrackingTarget(index: Int) {
|
||||
val param = SmartTrackTarget()
|
||||
param.index = index
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.updateMissionTarget(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectTrackingTarget:$index"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectTrackingTarget:$index,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/11 1:05 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeyValueVM : DJIViewModel() {
|
||||
|
||||
|
||||
}
|
||||
170
sample/src/main/java/dji/sampleV5/aircraft/models/LTEVM.kt
Normal file
170
sample/src/main/java/dji/sampleV5/aircraft/models/LTEVM.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sdk.keyvalue.value.airlink.WlmDongleInfo
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.aircraft.lte.*
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/8/12
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class LTEVM : DJIViewModel() {
|
||||
|
||||
val lteAuthenticationInfo = MutableLiveData<LTEAuthenticationInfo>()
|
||||
val lteLinkInfo = MutableLiveData<LTELinkInfo>()
|
||||
val acWlmDongleInfo = MutableLiveData<MutableList<WlmDongleInfo>>()
|
||||
val rcWlmDongleInfo = MutableLiveData<MutableList<WlmDongleInfo>>()
|
||||
|
||||
private val lteAuthenticationInfoListener = LTEAuthenticationInfoListener { info -> lteAuthenticationInfo.postValue(info) }
|
||||
|
||||
private val lteLinkInfoListener = LTELinkInfoListener { info: LTELinkInfo -> lteLinkInfo.postValue(info) }
|
||||
|
||||
private val lteDongleInfoListener = object :
|
||||
LTEDongleInfoListener {
|
||||
override fun onLTEAircraftDongleInfoUpdate(aircraftDongleInfos: MutableList<WlmDongleInfo>) {
|
||||
acWlmDongleInfo.postValue(aircraftDongleInfos)
|
||||
}
|
||||
|
||||
override fun onLTERemoteControllerDongleInfoUpdate(remoteControllerDongleInfos: MutableList<WlmDongleInfo>) {
|
||||
rcWlmDongleInfo.postValue(remoteControllerDongleInfos)
|
||||
}
|
||||
}
|
||||
|
||||
fun initListener() {
|
||||
LTEManager.getInstance().addLTEAuthenticationInfoListener(lteAuthenticationInfoListener)
|
||||
LTEManager.getInstance().addLTELinkInfoListener(lteLinkInfoListener)
|
||||
LTEManager.getInstance().addLTEDongleInfoListener(lteDongleInfoListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
LTEManager.getInstance().removeLTEAuthenticationInfoListener(lteAuthenticationInfoListener)
|
||||
LTEManager.getInstance().removeLTELinkInfoListener(lteLinkInfoListener)
|
||||
LTEManager.getInstance().removeLTEDongleInfoListener(lteDongleInfoListener)
|
||||
}
|
||||
|
||||
fun updateLTEAuthenticationInfo() {
|
||||
LTEManager.getInstance().updateLTEAuthenticationInfo(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun getLTEAuthenticationVerificationCode(phoneAreaCode: String, phoneNumber: String) {
|
||||
LTEManager.getInstance().getLTEAuthenticationVerificationCode(phoneAreaCode, phoneNumber, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startLTEAuthentication(phoneAreaCode: String, phoneNumber: String, verificationCode: String) {
|
||||
LTEManager.getInstance().startLTEAuthentication(phoneAreaCode, phoneNumber, verificationCode, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setLTEEnhancedTransmissionType(lteLinkType: LTELinkType) {
|
||||
LTEManager.getInstance().setLTEEnhancedTransmissionType(lteLinkType, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun getLTEEnhancedTransmissionType() {
|
||||
LTEManager.getInstance().getLTEEnhancedTransmissionType(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<LTELinkType> {
|
||||
override fun onSuccess(lteLinkType: LTELinkType) {
|
||||
toastResult?.postValue(DJIToastResult.success(lteLinkType.name))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setLTEAcPrivatizationServerMsg(serverInfo: LTEPrivatizationServerInfo) {
|
||||
LTEManager.getInstance().setLTEAircraftPrivatizationServerInfo(serverInfo, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setLTERcPrivatizationServerMsg(serverInfo: LTEPrivatizationServerInfo) {
|
||||
LTEManager.getInstance().setLTERemoteControllerPrivatizationServerInfo(serverInfo, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun clearLTEAcPrivatizationServerMsg() {
|
||||
LTEManager.getInstance().clearLTEAircraftPrivatizationServer(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun clearLTERcPrivatizationServerMsg() {
|
||||
LTEManager.getInstance().clearLTERemoteControllerPrivatizationServer(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.common.utils.CallbackUtils
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ILiveStreamManager
|
||||
import dji.v5.manager.datacenter.livestream.*
|
||||
import dji.v5.manager.datacenter.livestream.settings.AgoraSettings
|
||||
import dji.v5.manager.datacenter.livestream.settings.GB28181Settings
|
||||
import dji.v5.manager.datacenter.livestream.settings.RtmpSettings
|
||||
import dji.v5.manager.datacenter.livestream.settings.RtspSettings
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.DjiSharedPreferencesManager
|
||||
|
||||
/**
|
||||
* ClassName : LiveStreamVM
|
||||
* Description : 直播VM
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2022/3/23 11:04 上午
|
||||
* Copyright : ©2022 DJI All Rights Reserved.
|
||||
*/
|
||||
class LiveStreamVM : DJIViewModel() {
|
||||
private val availableCameraUpdatedListener: ICameraStreamManager.AvailableCameraUpdatedListener
|
||||
private val liveStreamStatusListener: LiveStreamStatusListener
|
||||
private val RTMP_KEY = "livestream-rtmp"
|
||||
private val RTSP_KEY = "livestream-rtsp"
|
||||
private val GB28181_KEY = "livestream-gb28181"
|
||||
private val AGORA_KEY = "livestream-agora"
|
||||
val liveStreamStatus = MutableLiveData<LiveStreamStatus?>()
|
||||
val liveStreamError = MutableLiveData<IDJIError?>()
|
||||
val availableCameraList = MutableLiveData<List<ComponentIndexType>>()
|
||||
val streamManager: ILiveStreamManager = MediaDataCenter.getInstance().liveStreamManager
|
||||
val cameraManager: ICameraStreamManager = MediaDataCenter.getInstance().cameraStreamManager
|
||||
|
||||
init {
|
||||
liveStreamStatusListener = object : LiveStreamStatusListener {
|
||||
override fun onLiveStreamStatusUpdate(status: LiveStreamStatus?) {
|
||||
status?.let {
|
||||
liveStreamStatus.postValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: IDJIError?) {
|
||||
error?.let {
|
||||
liveStreamError.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableCameraUpdatedListener = object :ICameraStreamManager.AvailableCameraUpdatedListener {
|
||||
override fun onAvailableCameraUpdated(list: MutableList<ComponentIndexType>) {
|
||||
availableCameraList.postValue(list)
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(cameraStreamEnableMap: MutableMap<ComponentIndexType, Boolean>) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
addListener()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
removeListener()
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
liveStreamError.postValue(null)
|
||||
liveStreamStatus.postValue(null)
|
||||
}
|
||||
|
||||
fun startStream(callback: CommonCallbacks.CompletionCallback?) {
|
||||
streamManager.startStream(object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
CallbackUtils.onSuccess(callback)
|
||||
reset()
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
CallbackUtils.onFailure(callback, error)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun stopStream(callback: CommonCallbacks.CompletionCallback?) {
|
||||
streamManager.stopStream(object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
CallbackUtils.onSuccess(callback)
|
||||
reset()
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
CallbackUtils.onFailure(callback, error)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun isStreaming(): Boolean {
|
||||
return streamManager.isStreaming;
|
||||
}
|
||||
|
||||
fun setCameraIndex(cameraIndex: ComponentIndexType) {
|
||||
streamManager.cameraIndex = cameraIndex
|
||||
}
|
||||
|
||||
fun setLiveStreamConfig(liveStreamSettings: LiveStreamSettings) {
|
||||
streamManager.liveStreamSettings = liveStreamSettings;
|
||||
}
|
||||
|
||||
fun setLiveStreamQuality(liveStreamQuality: StreamQuality) {
|
||||
streamManager.liveStreamQuality = liveStreamQuality
|
||||
}
|
||||
|
||||
fun setLiveStreamScaleType(scaleType: ICameraStreamManager.ScaleType) {
|
||||
streamManager.liveStreamScaleType = scaleType
|
||||
}
|
||||
|
||||
fun setLiveVideoBitRateMode(bitRateMode: LiveVideoBitrateMode) {
|
||||
streamManager.liveVideoBitrateMode = bitRateMode
|
||||
}
|
||||
|
||||
fun setLiveVideoBitRate(bitrate: Int) {
|
||||
streamManager.liveVideoBitrate = bitrate
|
||||
}
|
||||
|
||||
fun setRTMPConfig(rtmpUrl: String) {
|
||||
val liveStreamConfig = LiveStreamSettings.Builder()
|
||||
.setLiveStreamType(LiveStreamType.RTMP)
|
||||
.setRtmpSettings(
|
||||
RtmpSettings.Builder()
|
||||
.setUrl(rtmpUrl)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), RTMP_KEY, rtmpUrl)
|
||||
setLiveStreamConfig(liveStreamConfig)
|
||||
}
|
||||
|
||||
fun getRtmpUrl(): String {
|
||||
return DjiSharedPreferencesManager.getString(ContextUtil.getContext(), RTMP_KEY, "")
|
||||
}
|
||||
|
||||
fun setRTSPConfig(userName: String, password: String, port: Int) {
|
||||
val rtspConfig = RtspSettings.Builder()
|
||||
.setUserName(userName)
|
||||
.setPassWord(password)
|
||||
.setPort(port)
|
||||
.build()
|
||||
val liveStreamConfig = LiveStreamSettings.Builder()
|
||||
.setLiveStreamType(LiveStreamType.RTSP)
|
||||
.setRtspSettings(rtspConfig)
|
||||
.build()
|
||||
val rtspSettings = userName + "^_^" + password + "^_^" + port.toString()
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), RTSP_KEY, rtspSettings)
|
||||
setLiveStreamConfig(liveStreamConfig)
|
||||
}
|
||||
|
||||
fun getRtspSettings(): String {
|
||||
return DjiSharedPreferencesManager.getString(ContextUtil.getContext(), RTSP_KEY, "")
|
||||
}
|
||||
|
||||
fun setGB28181(
|
||||
serverIP: String,
|
||||
serverPort: Int,
|
||||
serverID: String,
|
||||
agentID: String,
|
||||
channel: String,
|
||||
localPort: Int,
|
||||
password: String
|
||||
) {
|
||||
val gb28181Config = GB28181Settings.Builder()
|
||||
.setServerIP(serverIP)
|
||||
.setServerPort(serverPort)
|
||||
.setServerID(serverID)
|
||||
.setAgentID(agentID)
|
||||
.setChannel(channel)
|
||||
.setLocalPort(localPort)
|
||||
.setPassword(password)
|
||||
.build()
|
||||
val liveStreamConfig = LiveStreamSettings.Builder()
|
||||
.setLiveStreamType(LiveStreamType.GB28181)
|
||||
.setGB28181Settings(gb28181Config)
|
||||
.build()
|
||||
val gb28181Settings =
|
||||
serverIP + "^_^" + serverPort.toString() + "^_^" + serverID + "^_^" + agentID + "^_^" + channel + "^_^" + localPort.toString() + "^_^" + password
|
||||
DjiSharedPreferencesManager.putString(
|
||||
ContextUtil.getContext(),
|
||||
GB28181_KEY,
|
||||
gb28181Settings
|
||||
)
|
||||
setLiveStreamConfig(liveStreamConfig)
|
||||
}
|
||||
|
||||
fun getGb28181Settings(): String {
|
||||
return DjiSharedPreferencesManager.getString(ContextUtil.getContext(), GB28181_KEY, "")
|
||||
}
|
||||
|
||||
fun setAgoraConfig(channelId: String, token: String, uid: String) {
|
||||
val agoraConfig = AgoraSettings.Builder()
|
||||
.setChannelId(channelId)
|
||||
.setToken(token)
|
||||
.setUid(uid)
|
||||
.setEnableSafety(false)
|
||||
.build()
|
||||
val liveStreamConfig = LiveStreamSettings.Builder()
|
||||
.setLiveStreamType(LiveStreamType.AGORA)
|
||||
.setAgoraSettings(agoraConfig)
|
||||
.build()
|
||||
val agoraSettings = channelId + "^_^" + token + "^_^" + uid
|
||||
DjiSharedPreferencesManager.putString(
|
||||
ContextUtil.getContext(),
|
||||
AGORA_KEY,
|
||||
agoraSettings
|
||||
)
|
||||
setLiveStreamConfig(liveStreamConfig)
|
||||
}
|
||||
|
||||
fun getAgoraSettings(): String {
|
||||
return DjiSharedPreferencesManager.getString(ContextUtil.getContext(), AGORA_KEY, "")
|
||||
}
|
||||
|
||||
fun addListener() {
|
||||
streamManager.addLiveStreamStatusListener(liveStreamStatusListener)
|
||||
cameraManager.addAvailableCameraUpdatedListener(availableCameraUpdatedListener)
|
||||
}
|
||||
|
||||
fun removeListener() {
|
||||
streamManager.removeLiveStreamStatusListener(liveStreamStatusListener)
|
||||
cameraManager.removeAvailableCameraUpdatedListener(availableCameraUpdatedListener)
|
||||
}
|
||||
|
||||
fun getAircraftStreamFrameInfo(cameraIndex: ComponentIndexType) = cameraManager.getAircraftStreamFrameInfo(cameraIndex)
|
||||
}
|
||||
55
sample/src/main/java/dji/sampleV5/aircraft/models/LoginVM.kt
Normal file
55
sample/src/main/java/dji/sampleV5/aircraft/models/LoginVM.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.manager.account.LoginInfo
|
||||
import dji.v5.manager.account.LoginInfoUpdateListener
|
||||
import dji.v5.manager.account.UserAccountManager
|
||||
|
||||
/**
|
||||
* Description :
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/4/24
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class LoginVM : DJIViewModel() {
|
||||
private val userAccountManager = UserAccountManager.getInstance()
|
||||
|
||||
|
||||
fun loginAccount(fragmentActivity: FragmentActivity, callbacks: CommonCallbacks.CompletionCallback) {
|
||||
userAccountManager.logInDJIUserAccount(fragmentActivity, false, callbacks)
|
||||
}
|
||||
|
||||
fun logoutAccount(callbacks: CommonCallbacks.CompletionCallback) {
|
||||
userAccountManager.logOutDJIUserAccount(callbacks)
|
||||
}
|
||||
|
||||
fun addLoginStateChangeListener(loginInfoUpdateListener: LoginInfoUpdateListener) {
|
||||
userAccountManager.addLoginInfoUpdateListener(loginInfoUpdateListener)
|
||||
}
|
||||
|
||||
fun clearAllLoginStateChangeListener() {
|
||||
userAccountManager.clearAllLoginInfoUpdateListener()
|
||||
}
|
||||
|
||||
fun getLoginInfo(): LoginInfo {
|
||||
return userAccountManager.loginInfo
|
||||
}
|
||||
|
||||
fun userLogin(
|
||||
userName: String,
|
||||
password: String,
|
||||
verificationCode: String? = null,
|
||||
callbacks: CommonCallbacks.CompletionCallback,
|
||||
) {
|
||||
userAccountManager.logInDJIUserAccount(userName, password, verificationCode, callbacks)
|
||||
}
|
||||
|
||||
fun getVerificationCodeImageURL(): String {
|
||||
return userAccountManager.verificationCodeImageURL
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.camera.TapZoomMode
|
||||
import dji.sdk.keyvalue.value.camera.ZoomTargetPointInfo
|
||||
import dji.sdk.keyvalue.value.common.CameraLensType
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.LookAtInfo
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.common.utils.CallbackUtils
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.createCamera
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.datacenter.camera.view.PinPoint
|
||||
import dji.v5.manager.datacenter.camera.view.PinPointInfo
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* ClassName : MSDKLogVM
|
||||
* Description : Log展示
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2022/5/7 12:17 下午
|
||||
* Copyright : ©2022 DJI All Rights Reserved.
|
||||
*/
|
||||
class LookAtVM : DJIViewModel() {
|
||||
val pointInfos = MutableLiveData<MutableList<Point>>()
|
||||
val currentComponentIndexType = MutableLiveData(ComponentIndexType.UNKNOWN)
|
||||
|
||||
init {
|
||||
pointInfos.value = CopyOnWriteArrayList()
|
||||
}
|
||||
|
||||
fun addNewPinPoint(pointPos: LocationCoordinate3D) {
|
||||
pointInfos.value?.add(Point(pointPos, getLiveViewLocationWithGPS(pointPos), currentComponentIndexType.value ?: ComponentIndexType.UNKNOWN))
|
||||
pointInfos.postValue(pointInfos.value)
|
||||
}
|
||||
|
||||
fun getLiveViewLocationWithGPS(pointPos: LocationCoordinate3D): PinPointInfo {
|
||||
return MediaDataCenter.getInstance().cameraStreamManager.getLiveViewLocationWithGPS(
|
||||
pointPos,
|
||||
currentComponentIndexType.value ?: ComponentIndexType.UNKNOWN
|
||||
)
|
||||
}
|
||||
|
||||
fun clearPointInfos() {
|
||||
pointInfos.value = CopyOnWriteArrayList()
|
||||
pointInfos.postValue(pointInfos.value)
|
||||
}
|
||||
|
||||
fun lookAt(info: LookAtInfo) {
|
||||
FlightControllerKey.KeyLookAt.create().action(info, {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}, { error: IDJIError ->
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
})
|
||||
}
|
||||
|
||||
fun startTapZoomPoint(x: Double, y: Double) {
|
||||
CameraKey.KeyTapZoomAtTarget.createCamera(currentComponentIndexType.value!!, CameraLensType.CAMERA_LENS_ZOOM).action(ZoomTargetPointInfo(x, y, false, TapZoomMode.UNKNOWN), {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}, { error: IDJIError ->
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
})
|
||||
}
|
||||
|
||||
class Point(
|
||||
var pos: LocationCoordinate3D = LocationCoordinate3D(),
|
||||
var pinPointInfo: PinPointInfo = PinPointInfo(),
|
||||
var componentIndexType: ComponentIndexType = ComponentIndexType.UNKNOWN
|
||||
) {
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("<-------------------Point------------------->\n")
|
||||
sb.append("Lng:").append(pos.longitude).append(" ")
|
||||
sb.append("Lat:").append(pos.latitude).append(" ")
|
||||
sb.append("Alt:").append(pos.altitude).append("\n")
|
||||
sb.append("Result:").append(pinPointInfo.result).append("\n")
|
||||
for (i in 0 until pinPointInfo.pinPoints.size) {
|
||||
val vector2: PinPoint = pinPointInfo.pinPoints[i]
|
||||
sb.append("PinPoint index:$i ")
|
||||
sb.append("X:").append(String.format("%.2f", vector2.x)).append(" ")
|
||||
sb.append("Y:").append(String.format("%.2f", vector2.y)).append(" \n")
|
||||
}
|
||||
sb.append("PointDirection:").append(pinPointInfo.pointDirection).append("\n")
|
||||
sb.append("ComponentIndexType:").append(componentIndexType)
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.FragmentPageItemList
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.ldm.LDMExemptModule
|
||||
import dji.v5.manager.SDKManager
|
||||
import dji.v5.manager.ldm.LDMManager
|
||||
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/10
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class MSDKCommonOperateVm : DJIViewModel() {
|
||||
|
||||
val mainPageInfoList = MutableLiveData<LinkedHashSet<FragmentPageItemList>>()
|
||||
|
||||
fun loaderItem(itemList: LinkedHashSet<FragmentPageItemList>) {
|
||||
mainPageInfoList.postValue(itemList)
|
||||
}
|
||||
|
||||
fun enableLDM(context: Context, callback: CommonCallbacks.CompletionCallback, ldmExemptModuleList: Array<LDMExemptModule?>) {
|
||||
LDMManager.getInstance().enableLDM(context, callback, * ldmExemptModuleList)
|
||||
}
|
||||
|
||||
fun disableLDM(callback: CommonCallbacks.CompletionCallback) {
|
||||
LDMManager.getInstance().disableLDM(callback);
|
||||
}
|
||||
|
||||
fun registerApp() {
|
||||
SDKManager.getInstance().registerApp()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.v5.inner.analytics.crash.CrashReport
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.DJIExecutor
|
||||
import dji.v5.utils.common.DiskUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* ClassName : MSDKLogVM
|
||||
* Description : Log展示
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2022/5/7 12:17 下午
|
||||
* Copyright : ©2022 DJI All Rights Reserved.
|
||||
*/
|
||||
class MSDKCrashLogVM : DJIViewModel() {
|
||||
val logInfo = MutableLiveData<String>()
|
||||
val logMsg = MutableLiveData<String>()
|
||||
|
||||
fun updateLogInfo() {
|
||||
loadLatestLog()
|
||||
}
|
||||
|
||||
fun testJavaCrash(runInNewThread: Boolean) {
|
||||
CrashReport.testJavaCrash(runInNewThread)
|
||||
}
|
||||
|
||||
fun testNativeCrash(runInNewThread: Boolean) {
|
||||
CrashReport.testNativeCrash(runInNewThread)
|
||||
}
|
||||
|
||||
private fun loadLatestLog() {
|
||||
DJIExecutor.getExecutorFor(DJIExecutor.Purpose.URGENT).execute {
|
||||
var log = "N/A"
|
||||
val logDir = LogUtils.getCrashLogPath()
|
||||
val file = File(logDir)
|
||||
val list = FileUtils.getAllFile(file)
|
||||
logMsg.postValue("get ${list.size} crash log,show recent one.pls wait, reading!")
|
||||
if (list.size >= 1) {
|
||||
list.sortWith { o1, o2 ->
|
||||
val diff = o1.lastModified() - o2.lastModified()
|
||||
-diff.toInt()
|
||||
}
|
||||
val f = list[0]
|
||||
val stringBuilder = StringBuilder()
|
||||
stringBuilder.append("-----------------Crash Info: ${f.name}}----------------------")
|
||||
stringBuilder.append("\n")
|
||||
stringBuilder.append(f.absolutePath)
|
||||
stringBuilder.append("\n")
|
||||
stringBuilder.append(FileUtils.readFile(f.absolutePath, "\n", 200))
|
||||
stringBuilder.append("\n")
|
||||
stringBuilder.append("\n")
|
||||
log = stringBuilder.toString()
|
||||
}
|
||||
logInfo.postValue(log)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import dji.v5.common.register.PackageProductCategory
|
||||
import dji.v5.utils.inner.SDKConfig
|
||||
|
||||
/**
|
||||
* 封装和MSDK交互的逻辑,读取MSDK Info的相关信息
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class MSDKInfoModel {
|
||||
|
||||
fun isDebug(): Boolean {
|
||||
return SDKConfig.getInstance().isDebug
|
||||
}
|
||||
|
||||
fun getPackageProductCategory(): PackageProductCategory {
|
||||
return SDKConfig.getInstance().packageProductCategory
|
||||
}
|
||||
|
||||
fun getSDKVersion(): String {
|
||||
return SDKConfig.getInstance().registrationSDKVersion
|
||||
}
|
||||
|
||||
fun getBuildVersion(): String {
|
||||
return SDKConfig.getInstance().buildVersion
|
||||
}
|
||||
|
||||
fun getCoreInfo(): SDKConfig.CoreInfo {
|
||||
return SDKConfig.getInstance().coreInfo
|
||||
}
|
||||
}
|
||||
123
sample/src/main/java/dji/sampleV5/aircraft/models/MSDKInfoVm.kt
Normal file
123
sample/src/main/java/dji/sampleV5/aircraft/models/MSDKInfoVm.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DEFAULT_STR
|
||||
import dji.sampleV5.aircraft.data.MSDKInfo
|
||||
import dji.sampleV5.aircraft.data.NO_NETWORK_STR
|
||||
import dji.sampleV5.aircraft.data.ONLINE_STR
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.et.listen
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.SDKManager
|
||||
import dji.v5.manager.areacode.AreaCodeChangeListener
|
||||
import dji.v5.manager.areacode.AreaCodeManager
|
||||
import dji.v5.manager.ldm.LDMManager
|
||||
import dji.v5.network.DJINetworkManager
|
||||
import dji.v5.network.IDJINetworkStatusListener
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* 提供SDKInfo View相关接口,操作MSDKInfoModel相关功能
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class MSDKInfoVm : DJIViewModel() {
|
||||
|
||||
val tag: String = LogUtils.getTag(this)
|
||||
|
||||
val msdkInfo = MutableLiveData<MSDKInfo>()
|
||||
val mainTitle = MutableLiveData<String>()
|
||||
private val isInited = AtomicBoolean(false)
|
||||
private val msdkInfoModel: MSDKInfoModel = MSDKInfoModel()
|
||||
private var areaCodeChangeListener: AreaCodeChangeListener
|
||||
private var netWorkStatusListener: IDJINetworkStatusListener
|
||||
|
||||
init {
|
||||
msdkInfo.value = MSDKInfo(msdkInfoModel.getSDKVersion())
|
||||
msdkInfo.value?.buildVer = msdkInfoModel.getBuildVersion()
|
||||
msdkInfo.value?.isDebug = msdkInfoModel.isDebug()
|
||||
msdkInfo.value?.packageProductCategory = msdkInfoModel.getPackageProductCategory()
|
||||
msdkInfo.value?.isLDMEnabled = LDMManager.getInstance().isLDMEnabled.toString()
|
||||
msdkInfo.value?.isLDMLicenseLoaded = LDMManager.getInstance().isLDMLicenseLoaded.toString()
|
||||
msdkInfo.value?.coreInfo = msdkInfoModel.getCoreInfo()
|
||||
|
||||
areaCodeChangeListener = AreaCodeChangeListener { _, changed ->
|
||||
LogUtils.i(logTag, "areaCodeData", changed)
|
||||
msdkInfo.value?.countryCode = if (changed == null) DEFAULT_STR else changed.areaCode
|
||||
refreshMSDKInfo()
|
||||
}
|
||||
netWorkStatusListener = IDJINetworkStatusListener {
|
||||
LogUtils.i(logTag, "isNetworkAvailable", it)
|
||||
updateNetworkInfo(it)
|
||||
refreshMSDKInfo()
|
||||
}
|
||||
|
||||
refreshMSDKInfo()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
removeListener()
|
||||
}
|
||||
|
||||
fun refreshMSDKInfo() {
|
||||
msdkInfo.postValue(msdkInfo.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要在register成功后,再调用
|
||||
*/
|
||||
fun initListener() {
|
||||
if (!SDKManager.getInstance().isRegistered) {
|
||||
return
|
||||
}
|
||||
if (isInited.getAndSet(true)) {
|
||||
return
|
||||
}
|
||||
FlightControllerKey.KeyConnection.create().listen(this) {
|
||||
LogUtils.i(tag, "KeyConnection:$it")
|
||||
updateFirmwareVersion()
|
||||
}
|
||||
|
||||
AreaCodeManager.getInstance().addAreaCodeChangeListener(areaCodeChangeListener)
|
||||
DJINetworkManager.getInstance().addNetworkStatusListener(netWorkStatusListener)
|
||||
ProductKey.KeyProductType.create().listen(this) {
|
||||
LogUtils.i(tag, "KeyProductType:$it")
|
||||
it?.let {
|
||||
msdkInfo.value?.productType = it
|
||||
refreshMSDKInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeListener() {
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
AreaCodeManager.getInstance().removeAreaCodeChangeListener(areaCodeChangeListener)
|
||||
DJINetworkManager.getInstance().removeNetworkStatusListener(netWorkStatusListener)
|
||||
}
|
||||
|
||||
private fun updateNetworkInfo(isAvailable: Boolean) {
|
||||
msdkInfo.value?.networkInfo = if(isAvailable) ONLINE_STR else NO_NETWORK_STR
|
||||
}
|
||||
|
||||
private fun updateFirmwareVersion() {
|
||||
ProductKey.KeyFirmwareVersion.create().get({
|
||||
LogUtils.i(tag, "updateFirmwareVersion onSuccess:$it")
|
||||
msdkInfo.value?.firmwareVer = it ?: DEFAULT_STR
|
||||
refreshMSDKInfo()
|
||||
}) {
|
||||
LogUtils.i(tag, "updateFirmwareVersion onFailure:$it")
|
||||
msdkInfo.value?.firmwareVer = DEFAULT_STR
|
||||
refreshMSDKInfo()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLDMStatus() {
|
||||
msdkInfo.value?.isLDMEnabled = LDMManager.getInstance().isLDMEnabled.toString()
|
||||
msdkInfo.value?.isLDMLicenseLoaded = LDMManager.getInstance().isLDMLicenseLoaded.toString()
|
||||
refreshMSDKInfo()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user