This commit is contained in:
zyp
2026-07-09 10:11:25 +08:00
commit f632730452
2156 changed files with 764537 additions and 0 deletions

View 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Binary file not shown.

View 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
}
}
}
}
}

View File

@@ -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) }
}

View File

@@ -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

View File

@@ -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 }

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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))
})
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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))
}
)
}
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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._-]"), "_")
}

View File

@@ -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
)

View File

@@ -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))
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
)
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -0,0 +1,6 @@
package com.zklh.dronecontroller.core.safety
object SafetyInterlock {
const val FLIGHT_COMMANDS_ENABLED: Boolean = true
const val LOCKED_MESSAGE: String = "飞控指令未启用"
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View 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
)
}

View File

@@ -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
}
}

View File

@@ -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())
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View 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)
}
}

View 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()
}
}

View 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>()
}

View File

@@ -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()
}

View File

@@ -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();
}
}

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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, "")
}

View File

@@ -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)
}
}
}

View File

@@ -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){
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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
) {}

View File

@@ -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;
}
}
}

View 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
}

View File

@@ -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
) {
}

View File

@@ -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
}
}
}

View File

@@ -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?)

View File

@@ -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);
}
}

View File

@@ -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"
)
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View 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"

View File

@@ -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}")
}
})
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View 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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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();
}
}

View File

@@ -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
//测试apkapp-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)
}
}

View File

@@ -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,
)
}

View File

@@ -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)
})
}
}

View File

@@ -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
}

View File

@@ -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>) {
//
}
}

View File

@@ -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)
}

View File

@@ -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")
}
})
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View 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,
)
}

View File

@@ -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 })
}

View File

@@ -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)
}
}

View File

@@ -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"))
}
})
}
}

View File

@@ -0,0 +1,12 @@
package dji.sampleV5.aircraft.models
/**
* @author feel.feng
* @time 2022/03/11 1:05 下午
* @description:
*/
class KeyValueVM : DJIViewModel() {
}

View 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()))
}
})
}
}

View File

@@ -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)
}

View 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
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View 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