update
This commit is contained in:
@@ -9,9 +9,11 @@ import com.zklh.dronecontroller.core.media.CameraControlService
|
|||||||
import com.zklh.dronecontroller.core.media.CameraMediaService
|
import com.zklh.dronecontroller.core.media.CameraMediaService
|
||||||
import com.zklh.dronecontroller.core.mission.WaypointMissionService
|
import com.zklh.dronecontroller.core.mission.WaypointMissionService
|
||||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||||
|
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||||
import com.zklh.dronecontroller.core.virtualstick.StickPosition
|
import com.zklh.dronecontroller.core.virtualstick.StickPosition
|
||||||
import com.zklh.dronecontroller.core.virtualstick.VirtualStickService
|
import com.zklh.dronecontroller.core.virtualstick.VirtualStickService
|
||||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||||
|
import dji.sdk.keyvalue.value.flightcontroller.LookAtMode
|
||||||
import dji.v5.manager.aircraft.virtualstick.Stick
|
import dji.v5.manager.aircraft.virtualstick.Stick
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -28,6 +30,10 @@ import org.json.JSONObject
|
|||||||
|
|
||||||
private const val CloudCommandLogTag = "ZklhCloudCommand"
|
private const val CloudCommandLogTag = "ZklhCloudCommand"
|
||||||
private const val DefaultFlyToHeightMeters = 20.0
|
private const val DefaultFlyToHeightMeters = 20.0
|
||||||
|
private const val MinimumTakeoffTargetHeightMeters = 2.0
|
||||||
|
private const val CoordPi = 3.1415926535897932384626
|
||||||
|
private const val CoordSemiMajorAxis = 6378245.0
|
||||||
|
private const val CoordEccentricity = 0.00669342162296594323
|
||||||
|
|
||||||
class CloudCommandExecutor(
|
class CloudCommandExecutor(
|
||||||
private val flightControl: FlightControlService,
|
private val flightControl: FlightControlService,
|
||||||
@@ -37,12 +43,15 @@ class CloudCommandExecutor(
|
|||||||
private val virtualStick: VirtualStickService,
|
private val virtualStick: VirtualStickService,
|
||||||
private val waypointMission: WaypointMissionService,
|
private val waypointMission: WaypointMissionService,
|
||||||
private val cameraControl: CameraControlService = CameraControlService(),
|
private val cameraControl: CameraControlService = CameraControlService(),
|
||||||
private val gimbalControl: GimbalControlService = GimbalControlService()
|
private val gimbalControl: GimbalControlService = GimbalControlService(),
|
||||||
|
private val mediaUpload: CloudMediaUploadService? = null,
|
||||||
|
private val telemetryProvider: () -> TelemetrySnapshot = { TelemetrySnapshot() }
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private val virtualStickEnabling = AtomicBoolean(false)
|
private val virtualStickEnabling = AtomicBoolean(false)
|
||||||
private val preparedMissionIds = ConcurrentHashMap<String, String>()
|
private val preparedMissionIds = ConcurrentHashMap<String, String>()
|
||||||
private var lastStickAt = 0L
|
private var lastStickAt = 0L
|
||||||
|
private var lastStickLogAt = 0L
|
||||||
private var stickTimeoutJob: Job? = null
|
private var stickTimeoutJob: Job? = null
|
||||||
|
|
||||||
suspend fun execute(request: CloudCommandRequest): DjiCommandResult {
|
suspend fun execute(request: CloudCommandRequest): DjiCommandResult {
|
||||||
@@ -73,8 +82,9 @@ class CloudCommandExecutor(
|
|||||||
command == "drc_mode_enter" -> enterDrcMode(data)
|
command == "drc_mode_enter" -> enterDrcMode(data)
|
||||||
command == "drc_mode_exit" -> exitDrcMode()
|
command == "drc_mode_exit" -> exitDrcMode()
|
||||||
command in StickCommands -> executeStickControl(data)
|
command in StickCommands -> executeStickControl(data)
|
||||||
|
command in TakeoffToPointCommands -> startFlyTo(data)
|
||||||
command in TakeoffCommands && data.hasFlyToTarget() -> startFlyTo(data)
|
command in TakeoffCommands && data.hasFlyToTarget() -> startFlyTo(data)
|
||||||
command in TakeoffCommands -> flightControl.startTakeoff()
|
command in TakeoffCommands -> startTakeoff(data)
|
||||||
command in StopTakeoffCommands -> flightControl.stopTakeoff()
|
command in StopTakeoffCommands -> flightControl.stopTakeoff()
|
||||||
command in GoHomeCommands -> flightControl.startGoHome()
|
command in GoHomeCommands -> flightControl.startGoHome()
|
||||||
command in StopGoHomeCommands -> flightControl.stopGoHome()
|
command in StopGoHomeCommands -> flightControl.stopGoHome()
|
||||||
@@ -82,23 +92,33 @@ class CloudCommandExecutor(
|
|||||||
command in ForceLandingCommands -> forceLanding()
|
command in ForceLandingCommands -> forceLanding()
|
||||||
command in StopLandingCommands -> flightControl.stopAutoLanding()
|
command in StopLandingCommands -> flightControl.stopAutoLanding()
|
||||||
command in ConfirmLandingCommands -> flightControl.confirmLanding()
|
command in ConfirmLandingCommands -> flightControl.confirmLanding()
|
||||||
command in PhotoCommands -> cameraMedia.takePhoto()
|
command in PhotoCommands -> takePhotoAndUpload()
|
||||||
command in RecordStartCommands -> cameraMedia.startRecord()
|
command in RecordStartCommands -> cameraMedia.startRecord()
|
||||||
command in RecordStopCommands -> cameraMedia.stopRecord()
|
command in RecordStopCommands -> stopRecordAndUpload()
|
||||||
command in RecordToggleCommands -> cameraMedia.toggleRecord()
|
command in RecordToggleCommands -> toggleRecordAndMaybeUpload()
|
||||||
command in CameraModeCommands -> setCameraMode(data)
|
command in CameraModeCommands -> setCameraMode(data)
|
||||||
command in PanoramaCommands -> cameraMedia.shootPanorama()
|
command in PanoramaCommands -> cameraMedia.shootPanorama()
|
||||||
command in LaserFillLightCommands -> setLaserFillLight(data)
|
command in LaserFillLightCommands -> setLaserFillLight(data)
|
||||||
command in LaserMeasureCommands -> setLaserMeasure(data)
|
command in LaserMeasureCommands -> setLaserMeasure(data)
|
||||||
|
command in FlyToUpdateCommands -> updateFlyTo(data)
|
||||||
command in FlyToCommands -> startFlyTo(data)
|
command in FlyToCommands -> startFlyTo(data)
|
||||||
command in StopFlyToCommands -> flyToService.stopFlyTo()
|
command in StopFlyToCommands -> stopFlyTo()
|
||||||
command in WaylinePrepareCommands -> prepareWayline(data)
|
command in WaylinePrepareCommands -> prepareWayline(data)
|
||||||
command in WaylineExecuteCommands -> executeWayline(data)
|
command in WaylineExecuteCommands -> executeWayline(data)
|
||||||
command in WaylinePauseCommands -> waypointMission.pauseMission()
|
command in WaylinePauseCommands -> waypointMission.pauseMission()
|
||||||
command in WaylineRecoveryCommands -> waypointMission.resumeMission()
|
command in WaylineRecoveryCommands -> waypointMission.resumeMission()
|
||||||
command in WaylineUndoCommands -> stopWayline(data)
|
command in WaylineUndoCommands -> stopWayline(data)
|
||||||
command in CameraZoomCommands -> setCameraZoom(data)
|
command in CameraZoomCommands -> setCameraZoom(data)
|
||||||
|
command in LinkageZoomCommands -> setLinkageZoom(data)
|
||||||
command in LensSwitchCommands -> switchLens(data)
|
command in LensSwitchCommands -> switchLens(data)
|
||||||
|
command in CameraLookAtCommands -> lookAt(data)
|
||||||
|
command in CameraAimCommands -> cameraAim(data)
|
||||||
|
command in CameraFrameZoomCommands -> frameZoom(data)
|
||||||
|
command in CameraExposureCommands -> setCameraExposure(data)
|
||||||
|
command in CameraExposureModeCommands -> setCameraExposureMode(data)
|
||||||
|
command in CameraFocusModeCommands -> setCameraFocusMode(data)
|
||||||
|
command in CameraScreenSplitCommands -> setCameraScreenSplit(data)
|
||||||
|
command in CameraNightModeCommands -> setCameraNightMode(data)
|
||||||
command in GimbalDragCommands -> rotateGimbal(data)
|
command in GimbalDragCommands -> rotateGimbal(data)
|
||||||
command in GimbalResetCommands -> gimbalControl.reset()
|
command in GimbalResetCommands -> gimbalControl.reset()
|
||||||
command in LiveStartCommands -> liveStreaming.startFromCloud(data)
|
command in LiveStartCommands -> liveStreaming.startFromCloud(data)
|
||||||
@@ -109,6 +129,7 @@ class CloudCommandExecutor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun enterDrcMode(data: JSONObject): DjiCommandResult {
|
private suspend fun enterDrcMode(data: JSONObject): DjiCommandResult {
|
||||||
|
stopFlyToForManualControl()
|
||||||
val result = ensureVirtualStickEnabled()
|
val result = ensureVirtualStickEnabled()
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
virtualStick.setSpeedLevel(data.optDoubleAny("speed_level", "speedLevel").takeIf { it > 0.0 } ?: 15.0)
|
virtualStick.setSpeedLevel(data.optDoubleAny("speed_level", "speedLevel").takeIf { it > 0.0 } ?: 15.0)
|
||||||
@@ -136,6 +157,7 @@ class CloudCommandExecutor(
|
|||||||
private suspend fun executeStickControl(
|
private suspend fun executeStickControl(
|
||||||
data: JSONObject
|
data: JSONObject
|
||||||
): DjiCommandResult {
|
): DjiCommandResult {
|
||||||
|
stopFlyToForManualControl()
|
||||||
val enabled = ensureVirtualStickEnabled()
|
val enabled = ensureVirtualStickEnabled()
|
||||||
if (!enabled.success) return enabled
|
if (!enabled.success) return enabled
|
||||||
val speedLevel = data.optDoubleAny("speed_level", "speedLevel", "speed")
|
val speedLevel = data.optDoubleAny("speed_level", "speedLevel", "speed")
|
||||||
@@ -149,12 +171,48 @@ class CloudCommandExecutor(
|
|||||||
} else {
|
} else {
|
||||||
data.toProtocolStickPosition()
|
data.toProtocolStickPosition()
|
||||||
}
|
}
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastStickLogAt > 500L) {
|
||||||
|
Log.d(
|
||||||
|
CloudCommandLogTag,
|
||||||
|
"stick_control data=$data position=$position virtualStickEnabled=${virtualStick.status.value.enabled}"
|
||||||
|
)
|
||||||
|
lastStickLogAt = now
|
||||||
|
}
|
||||||
virtualStick.sendStickPosition(position)
|
virtualStick.sendStickPosition(position)
|
||||||
lastStickAt = System.currentTimeMillis()
|
lastStickAt = now
|
||||||
scheduleStickTimeout()
|
scheduleStickTimeout()
|
||||||
return DjiCommandResult.ok("杆量控制已下发")
|
return DjiCommandResult.ok("杆量控制已下发")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun stopFlyToForManualControl() {
|
||||||
|
if (!flyToService.isMissionActive()) return
|
||||||
|
val stopResult = flyToService.stopFlyTo()
|
||||||
|
if (!stopResult.success) {
|
||||||
|
Log.w(CloudCommandLogTag, "stop flyTo before manual control failed: ${stopResult.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startTakeoff(data: JSONObject): DjiCommandResult {
|
||||||
|
val targetHeight = data.optDoubleAnyOrNull(
|
||||||
|
"targetHeight",
|
||||||
|
"target_height",
|
||||||
|
"height",
|
||||||
|
"altitude",
|
||||||
|
"alt",
|
||||||
|
"commander_flight_height",
|
||||||
|
"commanderFlightHeight"
|
||||||
|
)?.takeIf { it > MinimumTakeoffTargetHeightMeters }
|
||||||
|
|
||||||
|
if (targetHeight == null) {
|
||||||
|
return flightControl.startTakeoff()
|
||||||
|
}
|
||||||
|
|
||||||
|
return DjiCommandResult.failed(
|
||||||
|
"一键起飞带目标高度时必须同时下发经纬度,请使用 takeoff_to_point/fly_to_point;MSDK 端不再用虚拟杆兜底爬升"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun scheduleStickTimeout() {
|
private fun scheduleStickTimeout() {
|
||||||
if (stickTimeoutJob?.isActive == true) return
|
if (stickTimeoutJob?.isActive == true) return
|
||||||
stickTimeoutJob = scope.launch {
|
stickTimeoutJob = scope.launch {
|
||||||
@@ -185,6 +243,36 @@ class CloudCommandExecutor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun takePhotoAndUpload(): DjiCommandResult {
|
||||||
|
val capture = cameraMedia.takePhoto()
|
||||||
|
if (!capture.success) return capture
|
||||||
|
return appendMediaUploadResult(capture)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopRecordAndUpload(): DjiCommandResult {
|
||||||
|
val stop = cameraMedia.stopRecord()
|
||||||
|
if (!stop.success) return stop
|
||||||
|
return appendMediaUploadResult(stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun toggleRecordAndMaybeUpload(): DjiCommandResult {
|
||||||
|
val shouldStop = cameraMedia.isRecording()
|
||||||
|
val result = if (shouldStop) cameraMedia.stopRecord() else cameraMedia.startRecord()
|
||||||
|
if (!result.success || !shouldStop) return result
|
||||||
|
return appendMediaUploadResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun appendMediaUploadResult(captureResult: DjiCommandResult): DjiCommandResult {
|
||||||
|
val uploader = mediaUpload
|
||||||
|
?: return DjiCommandResult.ok("${captureResult.message};未配置平台上传")
|
||||||
|
val upload = uploader.uploadLatestMedia(cameraMedia.currentCameraIndex())
|
||||||
|
return if (upload.success) {
|
||||||
|
DjiCommandResult.ok("${captureResult.message};${upload.message}")
|
||||||
|
} else {
|
||||||
|
DjiCommandResult.failed("${captureResult.message};${upload.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun setLaserFillLight(data: JSONObject): DjiCommandResult {
|
private suspend fun setLaserFillLight(data: JSONObject): DjiCommandResult {
|
||||||
if (!data.hasAny("enable", "enabled", "ir_fill_light_enable", "irFillLightEnable")) {
|
if (!data.hasAny("enable", "enabled", "ir_fill_light_enable", "irFillLightEnable")) {
|
||||||
return cameraMedia.toggleLaserFillLight()
|
return cameraMedia.toggleLaserFillLight()
|
||||||
@@ -200,52 +288,55 @@ class CloudCommandExecutor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startFlyTo(data: JSONObject): DjiCommandResult {
|
private suspend fun startFlyTo(data: JSONObject): DjiCommandResult {
|
||||||
val target = data.optJSONObject("target_location")
|
val command = parseFlyToCommand(data)
|
||||||
?: data.optJSONObject("targetLocation")
|
if (command == null) {
|
||||||
?: 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")
|
return DjiCommandResult.failed("指点飞行参数无效:需要 latitude、longitude")
|
||||||
}
|
}
|
||||||
Log.d(
|
Log.d(
|
||||||
CloudCommandLogTag,
|
CloudCommandLogTag,
|
||||||
"startFlyTo lat=$latitude lon=$longitude height=$height requestedHeight=$requestedHeight maxSpeed=$maxSpeed securityTakeoffHeight=$securityTakeoffHeight flyToMode=$flyToMode data=$data"
|
"startFlyTo rawLat=${command.rawLatitude} rawLon=${command.rawLongitude} lat=${command.latitude} lon=${command.longitude} " +
|
||||||
|
"coordType=${command.coordinateType} height=${command.height} requestedHeight=${command.requestedHeight} " +
|
||||||
|
"maxSpeed=${command.maxSpeed} securityTakeoffHeight=${command.securityTakeoffHeight} flyToMode=${command.flyToMode} data=$data"
|
||||||
)
|
)
|
||||||
val releaseResult = releaseVirtualStickForAutonomousFlight()
|
val releaseResult = releaseVirtualStickForAutonomousFlight()
|
||||||
if (!releaseResult.success) {
|
if (!releaseResult.success) {
|
||||||
Log.w(CloudCommandLogTag, "release virtual stick before flyTo failed: ${releaseResult.message}")
|
Log.w(CloudCommandLogTag, "release virtual stick before flyTo failed: ${releaseResult.message}")
|
||||||
}
|
}
|
||||||
return flyToService.startFlyTo(
|
val result = flyToService.startFlyTo(
|
||||||
latitude = latitude,
|
latitude = command.latitude,
|
||||||
longitude = longitude,
|
longitude = command.longitude,
|
||||||
height = height,
|
height = command.height,
|
||||||
maxSpeed = maxSpeed,
|
maxSpeed = command.maxSpeed,
|
||||||
securityTakeoffHeight = securityTakeoffHeight,
|
securityTakeoffHeight = command.securityTakeoffHeight,
|
||||||
flyToMode = flyToMode
|
flyToMode = command.flyToMode
|
||||||
)
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateFlyTo(data: JSONObject): DjiCommandResult {
|
||||||
|
val command = parseFlyToCommand(data)
|
||||||
|
?: return DjiCommandResult.failed("更新指点飞行参数无效:需要 latitude、longitude")
|
||||||
|
if (!flyToService.isMissionActive()) {
|
||||||
|
Log.d(CloudCommandLogTag, "flyTo update received without active mission, start instead")
|
||||||
|
return startFlyTo(data)
|
||||||
|
}
|
||||||
|
val target = flyToService.updateFlyToTarget(
|
||||||
|
latitude = command.latitude,
|
||||||
|
longitude = command.longitude,
|
||||||
|
height = command.height,
|
||||||
|
maxSpeed = command.maxSpeed,
|
||||||
|
securityTakeoffHeight = command.securityTakeoffHeight
|
||||||
|
)
|
||||||
|
if (!target.success) return target
|
||||||
|
val param = flyToService.updateFlyToParam(
|
||||||
|
height = command.height.roundToInt().coerceAtLeast(1),
|
||||||
|
mode = command.flyToMode
|
||||||
|
)
|
||||||
|
return if (param.success) {
|
||||||
|
DjiCommandResult.ok("${target.message};${param.message}")
|
||||||
|
} else {
|
||||||
|
DjiCommandResult.failed("${target.message};${param.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun releaseVirtualStickForAutonomousFlight(): DjiCommandResult {
|
private suspend fun releaseVirtualStickForAutonomousFlight(): DjiCommandResult {
|
||||||
@@ -257,6 +348,11 @@ class CloudCommandExecutor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun stopFlyTo(): DjiCommandResult {
|
||||||
|
sendNeutralStick()
|
||||||
|
return flyToService.stopFlyTo()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun setCameraMode(data: JSONObject): DjiCommandResult {
|
private suspend fun setCameraMode(data: JSONObject): DjiCommandResult {
|
||||||
if (data.hasAny("camera_mode", "cameraMode")) {
|
if (data.hasAny("camera_mode", "cameraMode")) {
|
||||||
return cameraMedia.setCloudCameraMode(data.optIntAny(-1, "camera_mode", "cameraMode"))
|
return cameraMedia.setCloudCameraMode(data.optIntAny(-1, "camera_mode", "cameraMode"))
|
||||||
@@ -330,6 +426,15 @@ class CloudCommandExecutor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun setLinkageZoom(data: JSONObject): DjiCommandResult {
|
||||||
|
val enabled = when {
|
||||||
|
data.hasAny("state") -> data.optIntAny(0, "state") != 0
|
||||||
|
data.hasAny("enable", "enabled") -> data.optBooleanAny("enable", "enabled")
|
||||||
|
else -> return DjiCommandResult.failed("联动变焦参数无效:缺少 state/enable")
|
||||||
|
}
|
||||||
|
return cameraControl.setLinkZoomEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun switchLens(data: JSONObject): DjiCommandResult {
|
private suspend fun switchLens(data: JSONObject): DjiCommandResult {
|
||||||
val lens = data.optStringAny("lens", "camera_type", "cameraType", "video_type", "videoType", "type")
|
val lens = data.optStringAny("lens", "camera_type", "cameraType", "video_type", "videoType", "type")
|
||||||
.ifBlank {
|
.ifBlank {
|
||||||
@@ -341,6 +446,84 @@ class CloudCommandExecutor(
|
|||||||
return cameraControl.setLens(lens)
|
return cameraControl.setLens(lens)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun lookAt(data: JSONObject): DjiCommandResult {
|
||||||
|
val command = parseFlyToCommand(data)
|
||||||
|
?: return DjiCommandResult.failed("看向目标点参数无效:需要 latitude、longitude、height")
|
||||||
|
val mode = data.optStringAny("look_at_mode", "lookAtMode", "mode")
|
||||||
|
.toLookAtMode()
|
||||||
|
return flightControl.lookAt(command.latitude, command.longitude, command.height, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun cameraAim(data: JSONObject): DjiCommandResult {
|
||||||
|
val cameraType = data.optStringAny("camera_type", "cameraType", "lens", "video_type", "videoType")
|
||||||
|
return cameraControl.aimAt(
|
||||||
|
cameraType = cameraType,
|
||||||
|
locked = data.optBooleanAny("locked", "lock_gimbal", "lockGimbal"),
|
||||||
|
x = data.optDoubleAny("x"),
|
||||||
|
y = data.optDoubleAny("y")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun frameZoom(data: JSONObject): DjiCommandResult {
|
||||||
|
val cameraType = data.optStringAny("camera_type", "cameraType", "lens", "video_type", "videoType")
|
||||||
|
val x = data.optDoubleAny("x")
|
||||||
|
val y = data.optDoubleAny("y")
|
||||||
|
val width = data.optDoubleAny("width")
|
||||||
|
.takeIf { it > 0.0 }
|
||||||
|
?: data.optDoubleAny("length").takeIf { it > 0.0 }
|
||||||
|
?: return DjiCommandResult.failed("框选变焦参数无效:缺少 width")
|
||||||
|
val height = data.optDoubleAny("height")
|
||||||
|
.takeIf { it > 0.0 }
|
||||||
|
?: width
|
||||||
|
return cameraControl.frameZoom(
|
||||||
|
cameraType = cameraType,
|
||||||
|
locked = data.optBooleanAny("locked", "lock_gimbal", "lockGimbal"),
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
width = width,
|
||||||
|
height = height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setCameraExposure(data: JSONObject): DjiCommandResult {
|
||||||
|
val value = data.optIntAny(Int.MIN_VALUE, "exposure_value", "exposureValue")
|
||||||
|
if (value == Int.MIN_VALUE) return DjiCommandResult.failed("曝光补偿参数无效:缺少 exposure_value")
|
||||||
|
return cameraControl.setExposureCompensation(
|
||||||
|
cameraType = data.optStringAny("camera_type", "cameraType", "lens"),
|
||||||
|
value = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setCameraExposureMode(data: JSONObject): DjiCommandResult {
|
||||||
|
val mode = data.optIntAny(Int.MIN_VALUE, "exposure_mode", "exposureMode", "mode")
|
||||||
|
if (mode == Int.MIN_VALUE) return DjiCommandResult.failed("曝光模式参数无效:缺少 exposure_mode")
|
||||||
|
return cameraControl.setExposureMode(
|
||||||
|
cameraType = data.optStringAny("camera_type", "cameraType", "lens"),
|
||||||
|
mode = mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setCameraFocusMode(data: JSONObject): DjiCommandResult {
|
||||||
|
val mode = data.optIntAny(Int.MIN_VALUE, "focus_mode", "focusMode", "mode")
|
||||||
|
if (mode == Int.MIN_VALUE) return DjiCommandResult.failed("对焦模式参数无效:缺少 focus_mode")
|
||||||
|
return cameraControl.setFocusMode(
|
||||||
|
cameraType = data.optStringAny("camera_type", "cameraType", "lens"),
|
||||||
|
mode = mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setCameraScreenSplit(data: JSONObject): DjiCommandResult =
|
||||||
|
cameraControl.setThermalDisplaySplit(data.optBooleanAny("enable", "enabled", "screen_split_enable", "screenSplitEnable"))
|
||||||
|
|
||||||
|
private suspend fun setCameraNightMode(data: JSONObject): DjiCommandResult {
|
||||||
|
val mode = when {
|
||||||
|
data.hasAny("mode") -> data.optIntAny(0, "mode")
|
||||||
|
data.hasAny("enable", "enabled") -> if (data.optBooleanAny("enable", "enabled")) 1 else 0
|
||||||
|
else -> return DjiCommandResult.failed("夜景模式参数无效:缺少 mode/enable")
|
||||||
|
}
|
||||||
|
return cameraControl.setNightSceneMode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun rotateGimbal(data: JSONObject): DjiCommandResult {
|
private suspend fun rotateGimbal(data: JSONObject): DjiCommandResult {
|
||||||
val pitchSpeed = data.optDoubleAny("pitch_speed", "pitchSpeed", "gimbal_pitch_speed", "gimbalPitchSpeed")
|
val pitchSpeed = data.optDoubleAny("pitch_speed", "pitchSpeed", "gimbal_pitch_speed", "gimbalPitchSpeed")
|
||||||
val yawSpeed = data.optDoubleAny("yaw_speed", "yawSpeed", "gimbal_yaw_speed", "gimbalYawSpeed")
|
val yawSpeed = data.optDoubleAny("yaw_speed", "yawSpeed", "gimbal_yaw_speed", "gimbalYawSpeed")
|
||||||
@@ -348,6 +531,79 @@ class CloudCommandExecutor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class ParsedFlyToCommand(
|
||||||
|
val rawLatitude: Double,
|
||||||
|
val rawLongitude: Double,
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val coordinateType: String,
|
||||||
|
val requestedHeight: Double?,
|
||||||
|
val height: Double,
|
||||||
|
val maxSpeed: Int,
|
||||||
|
val securityTakeoffHeight: Int,
|
||||||
|
val flyToMode: FlyToMode
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseFlyToCommand(data: JSONObject): ParsedFlyToCommand? {
|
||||||
|
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 rawLatitude = target.optDoubleAnyOrNull("latitude", "lat", "target_latitude", "targetLatitude")
|
||||||
|
?: data.optDoubleAnyOrNull("targetLatitude", "target_latitude", "latitude", "lat")
|
||||||
|
?: 0.0
|
||||||
|
val rawLongitude = target.optDoubleAnyOrNull("longitude", "lng", "lon", "target_longitude", "targetLongitude")
|
||||||
|
?: data.optDoubleAnyOrNull("targetLongitude", "target_longitude", "longitude", "lng", "lon")
|
||||||
|
?: 0.0
|
||||||
|
val coordinateType = data.optStringAny(
|
||||||
|
"coordinate_type",
|
||||||
|
"coordinateType",
|
||||||
|
"coord_type",
|
||||||
|
"coordType",
|
||||||
|
"coordinate_system",
|
||||||
|
"coordinateSystem"
|
||||||
|
)
|
||||||
|
val (latitude, longitude) = convertCloudTargetCoordinate(rawLatitude, rawLongitude, coordinateType)
|
||||||
|
if (!isValidCoordinate(latitude, longitude)) return null
|
||||||
|
|
||||||
|
val requestedHeight = target.optDoubleAnyOrNull("height", "altitude", "alt", "target_height", "targetHeight", "target_altitude")
|
||||||
|
?: data.optDoubleAnyOrNull(
|
||||||
|
"targetHeight",
|
||||||
|
"target_height",
|
||||||
|
"height",
|
||||||
|
"altitude",
|
||||||
|
"alt",
|
||||||
|
"commander_flight_height",
|
||||||
|
"commanderFlightHeight"
|
||||||
|
)
|
||||||
|
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")
|
||||||
|
.coerceIn(1, 15)
|
||||||
|
val securityTakeoffHeight = data.optIntAny(20, "security_takeoff_height", "securityTakeoffHeight", "safe_height", "safeHeight")
|
||||||
|
return ParsedFlyToCommand(
|
||||||
|
rawLatitude = rawLatitude,
|
||||||
|
rawLongitude = rawLongitude,
|
||||||
|
latitude = latitude,
|
||||||
|
longitude = longitude,
|
||||||
|
coordinateType = coordinateType,
|
||||||
|
requestedHeight = requestedHeight,
|
||||||
|
height = height,
|
||||||
|
maxSpeed = maxSpeed,
|
||||||
|
securityTakeoffHeight = securityTakeoffHeight,
|
||||||
|
flyToMode = data.toFlyToMode(defaultMode = FlyToMode.SET_HEIGHT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val PassiveAckCommands = setOf(
|
private val PassiveAckCommands = setOf(
|
||||||
"flight_authority_grab",
|
"flight_authority_grab",
|
||||||
"payload_authority_grab",
|
"payload_authority_grab",
|
||||||
@@ -357,6 +613,7 @@ private val PassiveAckCommands = setOf(
|
|||||||
"drc_initial_state_subscribe"
|
"drc_initial_state_subscribe"
|
||||||
)
|
)
|
||||||
private val StickCommands = setOf("stick_control", "drone_control", "drc_drone_stick_control")
|
private val StickCommands = setOf("stick_control", "drone_control", "drc_drone_stick_control")
|
||||||
|
private val TakeoffToPointCommands = setOf("takeoff_to_point")
|
||||||
private val TakeoffCommands = setOf("start_takeoff", "takeoff", "take_off", "auto_takeoff")
|
private val TakeoffCommands = setOf("start_takeoff", "takeoff", "take_off", "auto_takeoff")
|
||||||
private val StopTakeoffCommands = setOf("stop_takeoff", "cancel_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 GoHomeCommands = setOf("start_go_home", "go_home", "return_home", "start_rth", "return_auto")
|
||||||
@@ -378,15 +635,25 @@ private val LaserFillLightCommands = setOf(
|
|||||||
"drc_infrared_fill_light_enable"
|
"drc_infrared_fill_light_enable"
|
||||||
)
|
)
|
||||||
private val LaserMeasureCommands = setOf("laser_measure", "toggle_laser_measure")
|
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 FlyToCommands = setOf("fly_to_point", "fly_to", "go_to_point", "gotargetpoint")
|
||||||
|
private val FlyToUpdateCommands = setOf("fly_to_point_update", "flytopointtaskupdate")
|
||||||
private val StopFlyToCommands = setOf("stop_fly_to", "cancel_fly_to", "fly_to_point_stop")
|
private val StopFlyToCommands = setOf("stop_fly_to", "cancel_fly_to", "fly_to_point_stop")
|
||||||
private val WaylinePrepareCommands = setOf("flighttask_prepare", "wayline_prepare")
|
private val WaylinePrepareCommands = setOf("flighttask_prepare", "wayline_prepare")
|
||||||
private val WaylineExecuteCommands = setOf("flighttask_execute", "wayline_execute", "start_wayline")
|
private val WaylineExecuteCommands = setOf("flighttask_execute", "wayline_execute", "start_wayline")
|
||||||
private val WaylinePauseCommands = setOf("flighttask_pause", "wayline_pause")
|
private val WaylinePauseCommands = setOf("flighttask_pause", "wayline_pause")
|
||||||
private val WaylineRecoveryCommands = setOf("flighttask_recovery", "wayline_resume", "wayline_recovery")
|
private val WaylineRecoveryCommands = setOf("flighttask_recovery", "wayline_resume", "wayline_recovery")
|
||||||
private val WaylineUndoCommands = setOf("flighttask_undo", "wayline_stop", "stop_wayline")
|
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 CameraZoomCommands = setOf("camera_focal_length_set", "drc_camera_focal_length_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 LinkageZoomCommands = setOf("drc_linkage_zoom_set", "linkage_zoom_set")
|
||||||
|
private val LensSwitchCommands = setOf("camera_lens_change", "lens_change", "live_lens_change", "camera_video_stream_source_set")
|
||||||
|
private val CameraLookAtCommands = setOf("camera_look_at", "drc_camera_look_at")
|
||||||
|
private val CameraAimCommands = setOf("camera_aim", "drc_camera_aim")
|
||||||
|
private val CameraFrameZoomCommands = setOf("camera_frame_zoom", "drc_camera_frame_zoom")
|
||||||
|
private val CameraExposureCommands = setOf("camera_exposure_set", "drc_camera_exposure_set")
|
||||||
|
private val CameraExposureModeCommands = setOf("camera_exposure_mode_set", "drc_camera_exposure_mode_set")
|
||||||
|
private val CameraFocusModeCommands = setOf("camera_focus_mode_set", "drc_camera_focus_mode_set")
|
||||||
|
private val CameraScreenSplitCommands = setOf("camera_screen_split", "drc_camera_screen_split")
|
||||||
|
private val CameraNightModeCommands = setOf("drc_camera_night_mode_set", "drc_camera_night_vision_enable", "camera_night_mode_set")
|
||||||
private val GimbalDragCommands = setOf("camera_screen_drag", "drc_camera_screen_drag", "gimbal_rotate_by_speed")
|
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 GimbalResetCommands = setOf("gimbal_reset", "drc_gimbal_reset")
|
||||||
private val LiveStartCommands = setOf("live_start_push", "live_start", "start_live", "start_livestream")
|
private val LiveStartCommands = setOf("live_start_push", "live_start", "start_live", "start_livestream")
|
||||||
@@ -456,6 +723,18 @@ private fun JSONObject.optBooleanAny(vararg names: String): Boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun JSONObject.toFlyToMode(defaultMode: FlyToMode): FlyToMode {
|
||||||
|
val modeText = optStringAny("fly_to_mode", "flyToMode", "mode")
|
||||||
|
if (modeText.isNotBlank()) {
|
||||||
|
return modeText.toFlyToMode(defaultMode)
|
||||||
|
}
|
||||||
|
return when (optIntAny(-1, "commander_flight_mode", "commanderFlightMode")) {
|
||||||
|
0 -> FlyToMode.SMART_HEIGHT
|
||||||
|
1 -> FlyToMode.SET_HEIGHT
|
||||||
|
else -> defaultMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun String.toFlyToMode(defaultMode: FlyToMode): FlyToMode =
|
private fun String.toFlyToMode(defaultMode: FlyToMode): FlyToMode =
|
||||||
when (normalizeCloudCommand()) {
|
when (normalizeCloudCommand()) {
|
||||||
"smart_height", "smartheight", "smart" -> FlyToMode.SMART_HEIGHT
|
"smart_height", "smartheight", "smart" -> FlyToMode.SMART_HEIGHT
|
||||||
@@ -463,9 +742,78 @@ private fun String.toFlyToMode(defaultMode: FlyToMode): FlyToMode =
|
|||||||
else -> defaultMode
|
else -> defaultMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.toLookAtMode(): LookAtMode =
|
||||||
|
when (normalizeCloudCommand()) {
|
||||||
|
"following", "follow", "gimbal_following", "look_at_gimbal_following", "1" -> LookAtMode.LOOK_AT_GIMBAL_FOLLOWING
|
||||||
|
"zoom_circle", "circle", "look_at_zoom_circle", "2" -> LookAtMode.LOOK_AT_ZOOM_CIRCLE
|
||||||
|
else -> LookAtMode.LOOK_AT_GIMBAL_FREE
|
||||||
|
}
|
||||||
|
|
||||||
private fun JSONObject.hasAny(vararg names: String): Boolean =
|
private fun JSONObject.hasAny(vararg names: String): Boolean =
|
||||||
names.any { has(it) && !isNull(it) }
|
names.any { has(it) && !isNull(it) }
|
||||||
|
|
||||||
|
private fun isValidCoordinate(latitude: Double, longitude: Double): Boolean =
|
||||||
|
!latitude.isNaN() &&
|
||||||
|
!longitude.isNaN() &&
|
||||||
|
!(latitude == 0.0 && longitude == 0.0) &&
|
||||||
|
latitude in -90.0..90.0 &&
|
||||||
|
longitude in -180.0..180.0
|
||||||
|
|
||||||
|
private fun convertCloudTargetCoordinate(
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
coordinateType: String
|
||||||
|
): Pair<Double, Double> {
|
||||||
|
val normalized = coordinateType.normalizeCloudCommand()
|
||||||
|
return if (normalized in setOf("gcj02", "gcj_02", "gcj-02", "mars")) {
|
||||||
|
gcj02ToWgs84(latitude, longitude)
|
||||||
|
} else {
|
||||||
|
latitude to longitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gcj02ToWgs84(latitude: Double, longitude: Double): Pair<Double, Double> {
|
||||||
|
if (isOutOfChina(latitude, longitude)) return latitude to longitude
|
||||||
|
val dLat = transformLatitude(longitude - 105.0, latitude - 35.0)
|
||||||
|
val dLon = transformLongitude(longitude - 105.0, latitude - 35.0)
|
||||||
|
val radLat = latitude / 180.0 * CoordPi
|
||||||
|
var magic = kotlin.math.sin(radLat)
|
||||||
|
magic = 1 - CoordEccentricity * magic * magic
|
||||||
|
val sqrtMagic = kotlin.math.sqrt(magic)
|
||||||
|
val adjustedLat = (dLat * 180.0) / ((CoordSemiMajorAxis * (1 - CoordEccentricity)) / (magic * sqrtMagic) * CoordPi)
|
||||||
|
val adjustedLon = (dLon * 180.0) / (CoordSemiMajorAxis / sqrtMagic * kotlin.math.cos(radLat) * CoordPi)
|
||||||
|
val mgLat = latitude + adjustedLat
|
||||||
|
val mgLon = longitude + adjustedLon
|
||||||
|
return (latitude * 2 - mgLat) to (longitude * 2 - mgLon)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isOutOfChina(latitude: Double, longitude: Double): Boolean =
|
||||||
|
longitude < 72.004 ||
|
||||||
|
longitude > 137.8347 ||
|
||||||
|
latitude < 0.8293 ||
|
||||||
|
latitude > 55.8271
|
||||||
|
|
||||||
|
private fun transformLatitude(longitude: Double, latitude: Double): Double {
|
||||||
|
var value = -100.0 + 2.0 * longitude + 3.0 * latitude + 0.2 * latitude * latitude +
|
||||||
|
0.1 * longitude * latitude + 0.2 * kotlin.math.sqrt(kotlin.math.abs(longitude))
|
||||||
|
value += (20.0 * kotlin.math.sin(6.0 * longitude * CoordPi) + 20.0 * kotlin.math.sin(2.0 * longitude * CoordPi)) * 2.0 / 3.0
|
||||||
|
value += (20.0 * kotlin.math.sin(latitude * CoordPi) + 40.0 * kotlin.math.sin(latitude / 3.0 * CoordPi)) * 2.0 / 3.0
|
||||||
|
value += (160.0 * kotlin.math.sin(latitude / 12.0 * CoordPi) + 320.0 * kotlin.math.sin(latitude * CoordPi / 30.0)) * 2.0 / 3.0
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transformLongitude(longitude: Double, latitude: Double): Double {
|
||||||
|
var value = 300.0 + longitude + 2.0 * latitude + 0.1 * longitude * longitude +
|
||||||
|
0.1 * longitude * latitude + 0.1 * kotlin.math.sqrt(kotlin.math.abs(longitude))
|
||||||
|
value += (20.0 * kotlin.math.sin(6.0 * longitude * CoordPi) + 20.0 * kotlin.math.sin(2.0 * longitude * CoordPi)) * 2.0 / 3.0
|
||||||
|
value += (20.0 * kotlin.math.sin(longitude * CoordPi) + 40.0 * kotlin.math.sin(longitude / 3.0 * CoordPi)) * 2.0 / 3.0
|
||||||
|
value += (150.0 * kotlin.math.sin(longitude / 12.0 * CoordPi) + 300.0 * kotlin.math.sin(longitude / 30.0 * CoordPi)) * 2.0 / 3.0
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Double.formatMeters(): String =
|
||||||
|
"%.1f".format(this)
|
||||||
|
|
||||||
private fun JSONObject.firstPoint(): JSONObject? {
|
private fun JSONObject.firstPoint(): JSONObject? {
|
||||||
val points = optJSONArray("points") ?: return null
|
val points = optJSONArray("points") ?: return null
|
||||||
if (points.length() <= 0) return null
|
if (points.length() <= 0) return null
|
||||||
|
|||||||
@@ -40,10 +40,13 @@ data class CloudCommandRequest(
|
|||||||
companion object {
|
companion object {
|
||||||
fun parse(topic: String, payload: String): CloudCommandRequest {
|
fun parse(topic: String, payload: String): CloudCommandRequest {
|
||||||
val request = JSONObject(payload)
|
val request = JSONObject(payload)
|
||||||
val data = request.optJSONObject("data") ?: JSONObject()
|
val rootData = request.optJSONObject("data") ?: JSONObject()
|
||||||
|
val data = rootData.unwrapNestedCommandData()
|
||||||
val method = request.optString("method")
|
val method = request.optString("method")
|
||||||
.ifBlank { data.optString("method") }
|
.ifBlank { rootData.optString("method") }
|
||||||
.ifBlank { request.optString("cmd") }
|
.ifBlank { request.optString("cmd") }
|
||||||
|
.ifBlank { rootData.optString("cmd") }
|
||||||
|
.ifBlank { data.optString("method") }
|
||||||
.ifBlank { data.optString("cmd") }
|
.ifBlank { data.optString("cmd") }
|
||||||
return CloudCommandRequest(
|
return CloudCommandRequest(
|
||||||
topic = topic,
|
topic = topic,
|
||||||
@@ -53,7 +56,7 @@ data class CloudCommandRequest(
|
|||||||
data = data,
|
data = data,
|
||||||
tid = request.optString("tid"),
|
tid = request.optString("tid"),
|
||||||
bid = request.optString("bid"),
|
bid = request.optString("bid"),
|
||||||
seq = request.optLongOrNull("seq") ?: data.optLongOrNull("seq")
|
seq = request.optLongOrNull("seq") ?: rootData.optLongOrNull("seq") ?: data.optLongOrNull("seq")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,3 +90,15 @@ fun String.normalizeCloudCommand(): String =
|
|||||||
|
|
||||||
private fun JSONObject.optLongOrNull(name: String): Long? =
|
private fun JSONObject.optLongOrNull(name: String): Long? =
|
||||||
if (has(name) && !isNull(name)) optLong(name) else null
|
if (has(name) && !isNull(name)) optLong(name) else null
|
||||||
|
|
||||||
|
private fun JSONObject.unwrapNestedCommandData(): JSONObject {
|
||||||
|
val nested = optJSONObject("data") ?: return this
|
||||||
|
if (!has("cmd") && !has("method")) return this
|
||||||
|
val merged = JSONObject(nested.toString())
|
||||||
|
for (field in SnFields + listOf("seq", "payload_index", "payloadIndex", "cmd", "method")) {
|
||||||
|
if (has(field) && !isNull(field) && !merged.has(field)) {
|
||||||
|
merged.put(field, opt(field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
package com.zklh.dronecontroller.core.cloud
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
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.media.MediaFile
|
||||||
|
import dji.v5.manager.datacenter.media.MediaFileDownloadListener
|
||||||
|
import dji.v5.manager.datacenter.media.MediaFileListDataSource
|
||||||
|
import dji.v5.manager.datacenter.media.PullMediaFileListParam
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URI
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private const val CloudMediaUploadLogTag = "ZklhCloudMediaUpload"
|
||||||
|
private const val MediaFetchTimeoutMs = 25_000L
|
||||||
|
private const val MediaDownloadMinTimeoutMs = 120_000L
|
||||||
|
private const val MediaDownloadMaxTimeoutMs = 600_000L
|
||||||
|
private const val UploadReadTimeoutMs = 600_000
|
||||||
|
private const val PlatformHost = "uav.zklhjs.com"
|
||||||
|
private const val PlatformFallbackIp = "221.226.33.58"
|
||||||
|
|
||||||
|
class CloudMediaUploadService(
|
||||||
|
context: Context,
|
||||||
|
private val sessionProvider: () -> CloudSession?
|
||||||
|
) {
|
||||||
|
private val appContext = context.applicationContext
|
||||||
|
|
||||||
|
suspend fun uploadLatestMedia(
|
||||||
|
cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||||
|
): DjiCommandResult = withContext(Dispatchers.IO) {
|
||||||
|
val session = sessionProvider()
|
||||||
|
?: return@withContext DjiCommandResult.failed("第三方云未登录,媒体未上传平台")
|
||||||
|
if (session.accessToken.isBlank()) {
|
||||||
|
return@withContext DjiCommandResult.failed("第三方云登录态缺少 token,媒体未上传平台")
|
||||||
|
}
|
||||||
|
|
||||||
|
val enableResult = enableMediaManager()
|
||||||
|
if (!enableResult.success) {
|
||||||
|
return@withContext DjiCommandResult.failed("媒体管理模块开启失败,媒体未上传平台:${enableResult.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val localFileResult = try {
|
||||||
|
val mediaFile = withTimeoutOrNull(MediaFetchTimeoutMs) {
|
||||||
|
fetchLatestMediaFile(cameraIndex)
|
||||||
|
} ?: return@withContext DjiCommandResult.failed("相机文件列表拉取超时,媒体未上传平台")
|
||||||
|
|
||||||
|
Log.i(
|
||||||
|
CloudMediaUploadLogTag,
|
||||||
|
"latest media selected camera=$cameraIndex name=${mediaFile.fileName} index=${mediaFile.fileIndex} size=${mediaFile.fileSize}"
|
||||||
|
)
|
||||||
|
|
||||||
|
downloadOriginalMedia(mediaFile)
|
||||||
|
} finally {
|
||||||
|
disableMediaManager()
|
||||||
|
}
|
||||||
|
val localFile = localFileResult.getOrElse { error ->
|
||||||
|
return@withContext DjiCommandResult.failed("媒体文件已在飞机存储,但下载到遥控器失败:${error.message ?: error.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadMultipart(session, localFile).fold(
|
||||||
|
onSuccess = { platformId ->
|
||||||
|
DjiCommandResult.ok("媒体已上传平台:${localFile.name},平台文件ID:$platformId")
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
DjiCommandResult.failed("媒体已下载到遥控器,但上传平台失败:${error.message ?: error.javaClass.simpleName}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchLatestMediaFile(cameraIndex: ComponentIndexType): MediaFile? {
|
||||||
|
val manager = MediaDataCenter.getInstance().mediaManager
|
||||||
|
val source = MediaFileListDataSource.Builder().setIndexType(cameraIndex).build()
|
||||||
|
manager.setMediaFileDataSource(source)
|
||||||
|
val pullResult = pullMediaFileList()
|
||||||
|
return if (!pullResult.success) {
|
||||||
|
Log.w(CloudMediaUploadLogTag, "pull media list failed: ${pullResult.message}")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
waitLatestMediaFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun enableMediaManager(): DjiCommandResult =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
MediaDataCenter.getInstance().mediaManager.enable(object : 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 : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
continuation.resume(DjiCommandResult.ok("媒体管理已关闭"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(CloudMediaUploadLogTag, "disable media manager failed: ${error.description()}")
|
||||||
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun pullMediaFileList(): DjiCommandResult =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
MediaDataCenter.getInstance().mediaManager.pullMediaFileListFromCamera(
|
||||||
|
PullMediaFileListParam.Builder().mediaFileIndex(0).count(30).build(),
|
||||||
|
object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
continuation.resume(DjiCommandResult.ok("相机文件列表已拉取"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun waitLatestMediaFile(): MediaFile? {
|
||||||
|
val startAt = System.currentTimeMillis()
|
||||||
|
while (System.currentTimeMillis() - startAt < 8_000L) {
|
||||||
|
val data = MediaDataCenter.getInstance().mediaManager.mediaFileListData.data
|
||||||
|
val latest = data.maxByOrNull { it.fileIndex }
|
||||||
|
if (latest != null) {
|
||||||
|
Log.d(
|
||||||
|
CloudMediaUploadLogTag,
|
||||||
|
"media list ready count=${data.size} latest=${latest.fileName} index=${latest.fileIndex}"
|
||||||
|
)
|
||||||
|
return latest
|
||||||
|
}
|
||||||
|
delay(300)
|
||||||
|
}
|
||||||
|
Log.w(CloudMediaUploadLogTag, "media list empty after pull")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadOriginalMedia(mediaFile: MediaFile): Result<File> {
|
||||||
|
val timeoutMs = ((mediaFile.fileSize / 1024L / 1024L) * 4_000L)
|
||||||
|
.coerceIn(MediaDownloadMinTimeoutMs, MediaDownloadMaxTimeoutMs)
|
||||||
|
return withTimeoutOrNull(timeoutMs) {
|
||||||
|
downloadOriginalMediaOnce(mediaFile)
|
||||||
|
} ?: Result.failure(IllegalStateException("下载超时:${mediaFile.fileName}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadOriginalMediaOnce(mediaFile: MediaFile): Result<File> =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val dir = File(appContext.cacheDir, "msdk-media")
|
||||||
|
if (!dir.exists()) dir.mkdirs()
|
||||||
|
val fileName = mediaFile.fileName?.takeIf { it.isNotBlank() } ?: "DJI_${mediaFile.fileIndex}.dat"
|
||||||
|
val outputFile = File(dir, fileName)
|
||||||
|
var completed = false
|
||||||
|
val outputStream = FileOutputStream(outputFile, false)
|
||||||
|
val bufferedOutput = BufferedOutputStream(outputStream)
|
||||||
|
|
||||||
|
fun closeStreams() {
|
||||||
|
runCatching { bufferedOutput.flush() }
|
||||||
|
runCatching { bufferedOutput.close() }
|
||||||
|
runCatching { outputStream.close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resumeOnce(result: Result<File>) {
|
||||||
|
if (completed) return
|
||||||
|
completed = true
|
||||||
|
closeStreams()
|
||||||
|
if (continuation.isActive) continuation.resume(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
closeStreams()
|
||||||
|
runCatching { outputFile.delete() }
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaFile.pullOriginalMediaFileFromCamera(0L, object : MediaFileDownloadListener {
|
||||||
|
override fun onStart() {
|
||||||
|
Log.i(CloudMediaUploadLogTag, "download start name=${mediaFile.fileName} size=${mediaFile.fileSize}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgress(total: Long, current: Long) {
|
||||||
|
if (current == total || current % (1024 * 1024) == 0L) {
|
||||||
|
Log.d(CloudMediaUploadLogTag, "download progress name=${mediaFile.fileName} current=$current total=$total")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRealtimeDataUpdate(data: ByteArray, position: Long) {
|
||||||
|
runCatching {
|
||||||
|
bufferedOutput.write(data)
|
||||||
|
}.onFailure { error ->
|
||||||
|
resumeOnce(Result.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
Log.i(CloudMediaUploadLogTag, "download finish path=${outputFile.absolutePath} size=${outputFile.length()}")
|
||||||
|
resumeOnce(Result.success(outputFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError?) {
|
||||||
|
val message = error?.description() ?: error?.toString() ?: "未知下载错误"
|
||||||
|
Log.w(CloudMediaUploadLogTag, "download failed name=${mediaFile.fileName}: $message")
|
||||||
|
resumeOnce(Result.failure(IllegalStateException(message)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uploadMultipart(session: CloudSession, file: File): Result<String> =
|
||||||
|
runCatching {
|
||||||
|
val boundary = "----ZKLH-MSDK-${UUID.randomUUID()}"
|
||||||
|
val url = "${session.baseUrl.trimEnd('/')}/system/base/uploadFile"
|
||||||
|
val connection = URI(url).toURL().openConnection() as HttpURLConnection
|
||||||
|
connection.requestMethod = "POST"
|
||||||
|
connection.connectTimeout = 20_000
|
||||||
|
connection.readTimeout = UploadReadTimeoutMs
|
||||||
|
connection.doInput = true
|
||||||
|
connection.doOutput = true
|
||||||
|
connection.useCaches = false
|
||||||
|
connection.setChunkedStreamingMode(1024 * 1024)
|
||||||
|
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
|
||||||
|
connection.setRequestProperty("Accept", "application/json")
|
||||||
|
platformHostHeaderForUploadUrl(url)?.let { connection.setRequestProperty("Host", it) }
|
||||||
|
connection.setRequestProperty("Tenant-Id", session.tenantId)
|
||||||
|
connection.setRequestProperty("tenant-id", session.tenantId)
|
||||||
|
connection.setRequestProperty("x-auth-token", session.accessToken)
|
||||||
|
connection.setRequestProperty("Authorization", "Bearer ${session.accessToken}")
|
||||||
|
|
||||||
|
DataOutputStream(BufferedOutputStream(connection.outputStream)).use { output ->
|
||||||
|
output.writeBytes("--$boundary\r\n")
|
||||||
|
output.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"${file.name}\"\r\n")
|
||||||
|
output.writeBytes("Content-Type: ${file.contentType()}\r\n\r\n")
|
||||||
|
BufferedInputStream(FileInputStream(file)).use { input ->
|
||||||
|
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
while (true) {
|
||||||
|
val read = input.read(buffer)
|
||||||
|
if (read <= 0) break
|
||||||
|
output.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.writeBytes("\r\n--$boundary--\r\n")
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
val responseText = (if (responseCode in 200..299) connection.inputStream else connection.errorStream ?: connection.inputStream)
|
||||||
|
.bufferedReader(Charsets.UTF_8)
|
||||||
|
.use { it.readText() }
|
||||||
|
Log.i(CloudMediaUploadLogTag, "upload response code=$responseCode body=$responseText")
|
||||||
|
if (responseCode !in 200..299) {
|
||||||
|
error("HTTP $responseCode: $responseText")
|
||||||
|
}
|
||||||
|
parseUploadResponse(responseText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseUploadResponse(responseText: String): String {
|
||||||
|
val text = responseText.trim()
|
||||||
|
if (!text.startsWith("{")) return text.ifBlank { "unknown" }
|
||||||
|
val root = JSONObject(text)
|
||||||
|
val code = root.optInt("code", 0)
|
||||||
|
if (code != 0 && code != 200) {
|
||||||
|
error(root.optString("message", root.optString("msg", "平台上传失败")))
|
||||||
|
}
|
||||||
|
return root.opt("data")?.toString()?.takeIf { it.isNotBlank() && it != "null" } ?: "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun File.contentType(): String =
|
||||||
|
when (extension.lowercase()) {
|
||||||
|
"jpg", "jpeg" -> "image/jpeg"
|
||||||
|
"png" -> "image/png"
|
||||||
|
"dng" -> "image/x-adobe-dng"
|
||||||
|
"mp4" -> "video/mp4"
|
||||||
|
"mov" -> "video/quicktime"
|
||||||
|
else -> "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun platformHostHeaderForUploadUrl(url: String): String? {
|
||||||
|
val uri = runCatching { URI(url) }.getOrNull() ?: return null
|
||||||
|
if (!uri.host.equals(PlatformFallbackIp, 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 "$PlatformHost$port"
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.zklh.dronecontroller.core.cloud
|
package com.zklh.dronecontroller.core.cloud
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.zklh.dronecontroller.core.diagnostics.DroneWarningItem
|
||||||
|
import com.zklh.dronecontroller.core.diagnostics.DroneWarningState
|
||||||
import com.zklh.dronecontroller.core.msdk.DroneSdkState
|
import com.zklh.dronecontroller.core.msdk.DroneSdkState
|
||||||
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||||
|
import dji.v5.manager.diagnostic.WarningLevel
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
@@ -20,6 +23,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
|
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
|
||||||
import org.eclipse.paho.client.mqttv3.MqttCallback
|
import org.eclipse.paho.client.mqttv3.MqttCallback
|
||||||
|
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended
|
||||||
import org.eclipse.paho.client.mqttv3.MqttClient
|
import org.eclipse.paho.client.mqttv3.MqttClient
|
||||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
|
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
|
||||||
import org.eclipse.paho.client.mqttv3.MqttException
|
import org.eclipse.paho.client.mqttv3.MqttException
|
||||||
@@ -35,11 +39,15 @@ private const val RcPlus2Type = 174
|
|||||||
private const val Matrice4Type = 99
|
private const val Matrice4Type = 99
|
||||||
private const val RcSubType = 0
|
private const val RcSubType = 0
|
||||||
private const val Matrice4SubType = 1
|
private const val Matrice4SubType = 1
|
||||||
|
private const val Matrice4DeviceType = "0-99-1"
|
||||||
private const val Matrice4CameraPayloadIndex = "89-0-0"
|
private const val Matrice4CameraPayloadIndex = "89-0-0"
|
||||||
private const val Matrice4LiveVideoIndex = "normal-0"
|
private const val Matrice4LiveVideoIndex = "normal-0"
|
||||||
private const val ThingVersion = "1.2.0"
|
private const val ThingVersion = "1.2.0"
|
||||||
private const val CloudAccessType = "msdk"
|
private const val CloudAccessType = "msdk"
|
||||||
private const val OSD_MIN_INTERVAL_MS = 1_000L
|
private const val OSD_MIN_INTERVAL_MS = 1_000L
|
||||||
|
private const val MQTT_MAX_INFLIGHT = 200
|
||||||
|
private const val MQTT_REASON_MAX_INFLIGHT = 32202
|
||||||
|
private const val MQTT_PUBLISH_CONGESTION_COOLDOWN_MS = 5_000L
|
||||||
private const val JsonClassKey = "@class"
|
private const val JsonClassKey = "@class"
|
||||||
private const val OsdRemoteControlClass = "com.dji.sdk.cloudapi.device.OsdRemoteControl"
|
private const val OsdRemoteControlClass = "com.dji.sdk.cloudapi.device.OsdRemoteControl"
|
||||||
private const val WirelessLinkClass = "com.dji.sdk.cloudapi.device.WirelessLink"
|
private const val WirelessLinkClass = "com.dji.sdk.cloudapi.device.WirelessLink"
|
||||||
@@ -51,7 +59,6 @@ private const val RcDistanceLimitStatusClass = "com.dji.sdk.cloudapi.device.RcDi
|
|||||||
private const val StorageClass = "com.dji.sdk.cloudapi.device.Storage"
|
private const val StorageClass = "com.dji.sdk.cloudapi.device.Storage"
|
||||||
private const val OsdCameraClass = "com.dji.sdk.cloudapi.device.OsdCamera"
|
private const val OsdCameraClass = "com.dji.sdk.cloudapi.device.OsdCamera"
|
||||||
private const val RcDronePayloadClass = "com.dji.sdk.cloudapi.device.RcDronePayload"
|
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
|
private const val EarthRadiusMeters = 6_371_000.0
|
||||||
|
|
||||||
class CloudMqttService(
|
class CloudMqttService(
|
||||||
@@ -67,10 +74,19 @@ class CloudMqttService(
|
|||||||
private var session: CloudSession? = null
|
private var session: CloudSession? = null
|
||||||
private var deviceIdentity = CloudDeviceIdentity()
|
private var deviceIdentity = CloudDeviceIdentity()
|
||||||
private var telemetry = TelemetrySnapshot()
|
private var telemetry = TelemetrySnapshot()
|
||||||
|
private var warningState = DroneWarningState()
|
||||||
private var subscribedControlKey = ""
|
private var subscribedControlKey = ""
|
||||||
private var onlineKey = ""
|
private var onlineKey = ""
|
||||||
private var liveCapacityKey = ""
|
private var liveCapacityKey = ""
|
||||||
|
private var warningHmsKey = ""
|
||||||
|
private var warningDebugRaw = ""
|
||||||
|
private var hmsDebugKey = ""
|
||||||
private var lastOsdAt = 0L
|
private var lastOsdAt = 0L
|
||||||
|
@Volatile
|
||||||
|
private var lastPublishCongestedAt = 0L
|
||||||
|
private val publishLock = Any()
|
||||||
|
private val osdThrottleLock = Any()
|
||||||
|
private val clientInstanceId = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
|
||||||
fun connect(
|
fun connect(
|
||||||
session: CloudSession,
|
session: CloudSession,
|
||||||
@@ -88,10 +104,10 @@ class CloudMqttService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
var attemptClient: MqttClient? = null
|
||||||
runCatching {
|
runCatching {
|
||||||
client?.takeIf { it.isConnected }?.disconnect()
|
closeCurrentClient()
|
||||||
client?.close()
|
val clientId = buildClientId()
|
||||||
val clientId = "zklh-rc-${deviceIdentity.remoteControllerSn.ifBlank { UUID.randomUUID().toString() }}"
|
|
||||||
val mqttAddresses = CloudLoginClient.mqttAddressCandidates(session.mqttAddress)
|
val mqttAddresses = CloudLoginClient.mqttAddressCandidates(session.mqttAddress)
|
||||||
.ifEmpty { listOf(session.mqttAddress) }
|
.ifEmpty { listOf(session.mqttAddress) }
|
||||||
var connectedClient: MqttClient? = null
|
var connectedClient: MqttClient? = null
|
||||||
@@ -99,14 +115,19 @@ class CloudMqttService(
|
|||||||
var lastConnectError: Throwable? = null
|
var lastConnectError: Throwable? = null
|
||||||
for (mqttAddress in mqttAddresses) {
|
for (mqttAddress in mqttAddresses) {
|
||||||
val result = runCatching {
|
val result = runCatching {
|
||||||
Log.i(CloudMqttLogTag, "connect attempt address=$mqttAddress username=${session.mqttUsername}")
|
Log.i(
|
||||||
|
CloudMqttLogTag,
|
||||||
|
"connect attempt address=$mqttAddress clientId=$clientId username=${session.mqttUsername}"
|
||||||
|
)
|
||||||
val mqttClient = MqttClient(mqttAddress, clientId, MemoryPersistence())
|
val mqttClient = MqttClient(mqttAddress, clientId, MemoryPersistence())
|
||||||
|
attemptClient = mqttClient
|
||||||
mqttClient.setCallback(callback())
|
mqttClient.setCallback(callback())
|
||||||
val options = MqttConnectOptions().apply {
|
val options = MqttConnectOptions().apply {
|
||||||
isAutomaticReconnect = true
|
isAutomaticReconnect = true
|
||||||
isCleanSession = true
|
isCleanSession = true
|
||||||
connectionTimeout = 10
|
connectionTimeout = 10
|
||||||
keepAliveInterval = 20
|
keepAliveInterval = 20
|
||||||
|
maxInflight = MQTT_MAX_INFLIGHT
|
||||||
userName = session.mqttUsername
|
userName = session.mqttUsername
|
||||||
password = session.mqttPassword.toCharArray()
|
password = session.mqttPassword.toCharArray()
|
||||||
}
|
}
|
||||||
@@ -120,6 +141,8 @@ class CloudMqttService(
|
|||||||
} else {
|
} else {
|
||||||
val error = result.exceptionOrNull()
|
val error = result.exceptionOrNull()
|
||||||
Log.w(CloudMqttLogTag, "connect attempt failed address=$mqttAddress: ${error?.message}")
|
Log.w(CloudMqttLogTag, "connect attempt failed address=$mqttAddress: ${error?.message}")
|
||||||
|
closeMqttClient(attemptClient)
|
||||||
|
attemptClient = null
|
||||||
lastConnectError = error
|
lastConnectError = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +161,10 @@ class CloudMqttService(
|
|||||||
subscribeControlTopicsIfReady()
|
subscribeControlTopicsIfReady()
|
||||||
publishOnlineIfReady(force = true)
|
publishOnlineIfReady(force = true)
|
||||||
publishOsdIfReady(force = true)
|
publishOsdIfReady(force = true)
|
||||||
|
publishHmsIfReady(force = true)
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.e(CloudMqttLogTag, "connect failed", error)
|
Log.e(CloudMqttLogTag, "connect failed", error)
|
||||||
|
closeMqttClient(attemptClient)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
connected = false,
|
connected = false,
|
||||||
@@ -156,12 +181,7 @@ class CloudMqttService(
|
|||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching { publishStatusOnline(online = false) }
|
runCatching { publishStatusOnline(online = false) }
|
||||||
runCatching { client?.disconnect() }
|
closeCurrentClient()
|
||||||
runCatching { client?.close() }
|
|
||||||
client = null
|
|
||||||
subscribedControlKey = ""
|
|
||||||
onlineKey = ""
|
|
||||||
liveCapacityKey = ""
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
connected = false,
|
connected = false,
|
||||||
@@ -175,12 +195,7 @@ class CloudMqttService(
|
|||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
runCatching { publishStatusOnline(online = false) }
|
runCatching { publishStatusOnline(online = false) }
|
||||||
runCatching { client?.disconnectForcibly(500, 500, false) }
|
closeCurrentClient()
|
||||||
runCatching { client?.close() }
|
|
||||||
client = null
|
|
||||||
subscribedControlKey = ""
|
|
||||||
onlineKey = ""
|
|
||||||
liveCapacityKey = ""
|
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +209,8 @@ class CloudMqttService(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
subscribeControlTopicsIfReady()
|
subscribeControlTopicsIfReady()
|
||||||
publishOnlineIfReady(force = false)
|
publishOnlineIfReady(force = false)
|
||||||
|
publishLiveCapacityIfReady(deviceIdentity, force = false)
|
||||||
|
publishHmsIfReady(force = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,10 +221,51 @@ class CloudMqttService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateWarnings(state: DroneWarningState) {
|
||||||
|
warningState = state
|
||||||
|
if (state.raw != warningDebugRaw) {
|
||||||
|
warningDebugRaw = state.raw
|
||||||
|
Log.d(
|
||||||
|
CloudMqttLogTag,
|
||||||
|
"warning state updated active=${state.active} count=${state.items.size} raw=${state.raw.ifBlank { "empty" }}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
publishHmsIfReady(force = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun callback(): MqttCallback =
|
private fun callback(): MqttCallback =
|
||||||
object : MqttCallback {
|
object : MqttCallbackExtended {
|
||||||
|
override fun connectComplete(reconnect: Boolean, serverURI: String?) {
|
||||||
|
Log.i(CloudMqttLogTag, "connect complete reconnect=$reconnect serverURI=$serverURI")
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
connected = true,
|
||||||
|
connecting = false,
|
||||||
|
message = if (reconnect) "MQTT 已重连" else "MQTT 已连接",
|
||||||
|
lastError = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (reconnect) {
|
||||||
|
subscribedControlKey = ""
|
||||||
|
onlineKey = ""
|
||||||
|
liveCapacityKey = ""
|
||||||
|
scope.launch {
|
||||||
|
subscribeControlTopicsIfReady()
|
||||||
|
publishOnlineIfReady(force = true)
|
||||||
|
publishOsdIfReady(force = true)
|
||||||
|
publishHmsIfReady(force = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun connectionLost(cause: Throwable?) {
|
override fun connectionLost(cause: Throwable?) {
|
||||||
Log.e(CloudMqttLogTag, "connection lost", cause)
|
Log.e(CloudMqttLogTag, "connection lost", cause)
|
||||||
|
subscribedControlKey = ""
|
||||||
|
onlineKey = ""
|
||||||
|
liveCapacityKey = ""
|
||||||
|
warningHmsKey = ""
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
connected = false,
|
connected = false,
|
||||||
@@ -240,6 +298,7 @@ class CloudMqttService(
|
|||||||
if (aircraftSn.isNotBlank()) {
|
if (aircraftSn.isNotBlank()) {
|
||||||
add("thing/product/$aircraftSn/services")
|
add("thing/product/$aircraftSn/services")
|
||||||
add("thing/product/$aircraftSn/property/set")
|
add("thing/product/$aircraftSn/property/set")
|
||||||
|
add("thing/product/$aircraftSn/drc/down")
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -289,7 +348,7 @@ class CloudMqttService(
|
|||||||
onlineKey = key
|
onlineKey = key
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
_state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") }
|
_state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") }
|
||||||
publishLiveCapacityIfReady(identity)
|
publishLiveCapacityIfReady(identity, force = true)
|
||||||
bindDevices(identity)
|
bindDevices(identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,13 +394,14 @@ class CloudMqttService(
|
|||||||
publish("sys/product/$rcSn/status", payload, qos = 1)
|
publish("sys/product/$rcSn/status", payload, qos = 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun publishLiveCapacityIfReady(identity: CloudDeviceIdentity) {
|
private fun publishLiveCapacityIfReady(identity: CloudDeviceIdentity, force: Boolean) {
|
||||||
val cloudSession = session ?: return
|
val cloudSession = session ?: return
|
||||||
|
if (!isConnected()) return
|
||||||
val rcSn = identity.remoteControllerSn
|
val rcSn = identity.remoteControllerSn
|
||||||
val aircraftSn = identity.aircraftSn
|
val aircraftSn = identity.aircraftSn
|
||||||
if (rcSn.isBlank() || aircraftSn.isBlank()) return
|
if (rcSn.isBlank() || aircraftSn.isBlank()) return
|
||||||
val key = "$rcSn/$aircraftSn/${cloudSession.workspaceId}"
|
val key = "$rcSn/$aircraftSn/${cloudSession.workspaceId}"
|
||||||
if (liveCapacityKey == key) return
|
if (!force && liveCapacityKey == key) return
|
||||||
|
|
||||||
val video = JSONObject()
|
val video = JSONObject()
|
||||||
.put("video_index", Matrice4LiveVideoIndex)
|
.put("video_index", Matrice4LiveVideoIndex)
|
||||||
@@ -371,9 +431,59 @@ class CloudMqttService(
|
|||||||
.put("device_list", JSONArray().put(device))
|
.put("device_list", JSONArray().put(device))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
publish("thing/product/$rcSn/state", payload, qos = 1)
|
if (publish("thing/product/$rcSn/state", payload, qos = 1)) {
|
||||||
liveCapacityKey = key
|
liveCapacityKey = key
|
||||||
Log.d(CloudMqttLogTag, "live capacity published rc=$rcSn aircraft=$aircraftSn")
|
Log.d(CloudMqttLogTag, "live capacity published rc=$rcSn aircraft=$aircraftSn force=$force")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun publishHmsIfReady(force: Boolean) {
|
||||||
|
if (!isConnected()) {
|
||||||
|
logHmsDebug("skip:not_connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val cloudSession = session ?: run {
|
||||||
|
logHmsDebug("skip:no_session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val identity = deviceIdentity
|
||||||
|
if (!identity.readyForOnline) {
|
||||||
|
logHmsDebug("skip:identity_not_ready:${identity.remoteControllerSn}/${identity.aircraftSn}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val hmsItems = JSONArray().apply {
|
||||||
|
val inTheSky = telemetry.isFlying || telemetry.motorsOn
|
||||||
|
warningState.items
|
||||||
|
.map { it.toHmsJson(inTheSky) }
|
||||||
|
.forEach { put(it) }
|
||||||
|
}
|
||||||
|
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${hmsItems.hmsSignature()}"
|
||||||
|
if (!force && key == warningHmsKey) {
|
||||||
|
logHmsDebug("skip:unchanged:$key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!warningState.active && hmsItems.length() == 0 && warningHmsKey.isBlank() && !force) {
|
||||||
|
logHmsDebug("skip:no_warning")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logHmsDebug("ready:force=$force active=${warningState.active} count=${hmsItems.length()} key=$key")
|
||||||
|
|
||||||
|
val payload = topicRequest(cloudSession)
|
||||||
|
.put("method", "hms")
|
||||||
|
.put("gateway", identity.remoteControllerSn)
|
||||||
|
.put("from", identity.aircraftSn)
|
||||||
|
.put("need_reply", false)
|
||||||
|
.put("data", JSONObject().put("list", hmsItems))
|
||||||
|
if (publish("thing/product/${identity.aircraftSn}/events", payload, qos = 1)) {
|
||||||
|
warningHmsKey = key
|
||||||
|
Log.d(CloudMqttLogTag, "hms published aircraft=${identity.aircraftSn} count=${hmsItems.length()} force=$force")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logHmsDebug(message: String) {
|
||||||
|
if (message == hmsDebugKey) return
|
||||||
|
hmsDebugKey = message
|
||||||
|
Log.d(CloudMqttLogTag, "hms $message")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun publishOsdIfReady(force: Boolean) {
|
private fun publishOsdIfReady(force: Boolean) {
|
||||||
@@ -381,8 +491,17 @@ class CloudMqttService(
|
|||||||
val identity = deviceIdentity
|
val identity = deviceIdentity
|
||||||
if (!identity.readyForOnline) return
|
if (!identity.readyForOnline) return
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (!force && now - lastOsdAt < OSD_MIN_INTERVAL_MS) return
|
val shouldPublish = synchronized(osdThrottleLock) {
|
||||||
|
when {
|
||||||
|
!force && now - lastOsdAt < OSD_MIN_INTERVAL_MS -> false
|
||||||
|
!force && isPublishCongested(now) -> false
|
||||||
|
else -> {
|
||||||
lastOsdAt = now
|
lastOsdAt = now
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!shouldPublish) return
|
||||||
publishRcOsd(identity)
|
publishRcOsd(identity)
|
||||||
publishDroneOsd(identity)
|
publishDroneOsd(identity)
|
||||||
_state.update { it.copy(lastOsdAt = now) }
|
_state.update { it.copy(lastOsdAt = now) }
|
||||||
@@ -432,7 +551,9 @@ class CloudMqttService(
|
|||||||
CloudMqttLogTag,
|
CloudMqttLogTag,
|
||||||
"DroneOsdPosition source=${telemetry.droneOsdPositionSource()} lat=$latitude lon=$longitude " +
|
"DroneOsdPosition source=${telemetry.droneOsdPositionSource()} lat=$latitude lon=$longitude " +
|
||||||
"gpsValid=${telemetry.gpsValid} gps=${telemetry.gpsSatelliteCount} rtkHealthy=${telemetry.rtkHealthy} " +
|
"gpsValid=${telemetry.gpsValid} gps=${telemetry.gpsSatelliteCount} rtkHealthy=${telemetry.rtkHealthy} " +
|
||||||
"rtkUsable=${telemetry.rtkFusionDataUsable} solution=${telemetry.rtkPositioningSolution}"
|
"rtkUsable=${telemetry.rtkFusionDataUsable} solution=${telemetry.rtkPositioningSolution} " +
|
||||||
|
"simulator=${telemetry.simulatorStarted} fc=${telemetry.latitude},${telemetry.longitude} " +
|
||||||
|
"rtk=${telemetry.rtkLatitude},${telemetry.rtkLongitude} rc=${telemetry.rcLatitude},${telemetry.rcLongitude}/${telemetry.rcLocationValid}"
|
||||||
)
|
)
|
||||||
val relativeAltitude = telemetry.altitude.toFloat()
|
val relativeAltitude = telemetry.altitude.toFloat()
|
||||||
val payload = topicRequest(cloudSession)
|
val payload = topicRequest(cloudSession)
|
||||||
@@ -516,7 +637,7 @@ class CloudMqttService(
|
|||||||
if (telemetry.batteryHighVoltageStorageKnown) {
|
if (telemetry.batteryHighVoltageStorageKnown) {
|
||||||
batteryItem.put("high_voltage_storage_days", telemetry.batteryHighVoltageStorageDays())
|
batteryItem.put("high_voltage_storage_days", telemetry.batteryHighVoltageStorageDays())
|
||||||
}
|
}
|
||||||
return if (batteryItem.length() > 2) cloudArrayList(batteryItem) else cloudArrayList()
|
return if (batteryItem.length() > 2) JSONArray().put(batteryItem) else JSONArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dronePositionStateJson(): JSONObject =
|
private fun dronePositionStateJson(): JSONObject =
|
||||||
@@ -619,9 +740,15 @@ class CloudMqttService(
|
|||||||
publish(replyTopic, payload, qos = 1)
|
publish(replyTopic, payload, qos = 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun publish(topic: String, payload: JSONObject, qos: Int) {
|
private fun publish(topic: String, payload: JSONObject, qos: Int): Boolean {
|
||||||
val mqttClient = client ?: return
|
val mqttClient = client ?: return false
|
||||||
if (!mqttClient.isConnected) return
|
if (!mqttClient.isConnected) return false
|
||||||
|
if (qos == 0 && isPublishCongested(System.currentTimeMillis())) {
|
||||||
|
Log.d(CloudMqttLogTag, "skip non-critical publish while congested topic=$topic")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return synchronized(publishLock) {
|
||||||
|
if (!mqttClient.isConnected) return@synchronized false
|
||||||
runCatching {
|
runCatching {
|
||||||
val message = MqttMessage(payload.toString().toByteArray(Charsets.UTF_8)).apply {
|
val message = MqttMessage(payload.toString().toByteArray(Charsets.UTF_8)).apply {
|
||||||
this.qos = qos
|
this.qos = qos
|
||||||
@@ -629,13 +756,130 @@ class CloudMqttService(
|
|||||||
}
|
}
|
||||||
mqttClient.publish(topic, message)
|
mqttClient.publish(topic, message)
|
||||||
Log.d(CloudMqttLogTag, "published topic=$topic payload=$payload")
|
Log.d(CloudMqttLogTag, "published topic=$topic payload=$payload")
|
||||||
}.onFailure { error ->
|
}.fold(
|
||||||
if (error is MqttException) Log.e(CloudMqttLogTag, "publish failed reason=${error.reasonCode}", error)
|
onSuccess = { true },
|
||||||
_state.update { it.copy(lastError = error.message ?: error.toString()) }
|
onFailure = { error ->
|
||||||
|
handlePublishFailure(error, topic)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isConnected(): Boolean = client?.isConnected == true && session != null
|
private fun isConnected(): Boolean = client?.isConnected == true && session != null
|
||||||
|
|
||||||
|
private fun handlePublishFailure(error: Throwable, topic: String) {
|
||||||
|
val message = error.message ?: error.toString()
|
||||||
|
val maxInflight = error is MqttException &&
|
||||||
|
(error.reasonCode == MQTT_REASON_MAX_INFLIGHT || message.contains("too many publishes", ignoreCase = true))
|
||||||
|
if (maxInflight) {
|
||||||
|
lastPublishCongestedAt = System.currentTimeMillis()
|
||||||
|
Log.w(CloudMqttLogTag, "publish congested topic=$topic reason=${(error as MqttException).reasonCode}: $message")
|
||||||
|
_state.update { it.copy(message = "MQTT 发布队列拥塞,已限流实时数据", lastError = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (error is MqttException) {
|
||||||
|
Log.e(CloudMqttLogTag, "publish failed topic=$topic reason=${error.reasonCode}", error)
|
||||||
|
} else {
|
||||||
|
Log.e(CloudMqttLogTag, "publish failed topic=$topic", error)
|
||||||
|
}
|
||||||
|
_state.update { it.copy(lastError = message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPublishCongested(now: Long): Boolean =
|
||||||
|
now - lastPublishCongestedAt < MQTT_PUBLISH_CONGESTION_COOLDOWN_MS
|
||||||
|
|
||||||
|
private fun buildClientId(): String {
|
||||||
|
val sn = deviceIdentity.remoteControllerSn.ifBlank { "pending-${clientInstanceId.take(8)}" }
|
||||||
|
return "zklh-msdk-$sn".take(64)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeCurrentClient() {
|
||||||
|
val oldClient = client
|
||||||
|
client = null
|
||||||
|
resetPublishState()
|
||||||
|
closeMqttClient(oldClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeMqttClient(mqttClient: MqttClient?) {
|
||||||
|
mqttClient ?: return
|
||||||
|
runCatching { mqttClient.setCallback(null) }
|
||||||
|
runCatching {
|
||||||
|
if (mqttClient.isConnected) {
|
||||||
|
mqttClient.disconnectForcibly(500, 500, false)
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(CloudMqttLogTag, "force disconnect mqtt failed: ${error.message}")
|
||||||
|
}
|
||||||
|
runCatching { mqttClient.close() }
|
||||||
|
.onFailure { error -> Log.w(CloudMqttLogTag, "close mqtt failed: ${error.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetPublishState() {
|
||||||
|
subscribedControlKey = ""
|
||||||
|
onlineKey = ""
|
||||||
|
liveCapacityKey = ""
|
||||||
|
warningHmsKey = ""
|
||||||
|
hmsDebugKey = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DroneWarningItem.toHmsJson(inTheSky: Boolean): JSONObject {
|
||||||
|
val hmsCode = code.toHmsCodeOrFallback(message)
|
||||||
|
return JSONObject()
|
||||||
|
.put("code", hmsCode)
|
||||||
|
.put("device_type", Matrice4DeviceType)
|
||||||
|
.put("imminent", level.isHmsAlarm())
|
||||||
|
.put("in_the_sky", inTheSky)
|
||||||
|
.put("level", level.toHmsLevel())
|
||||||
|
.put("module", 3)
|
||||||
|
.put("args", hmsArgsJson())
|
||||||
|
.put("title", title)
|
||||||
|
.put("description", description)
|
||||||
|
.put("message_zh", message)
|
||||||
|
.put("message_en", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DroneWarningItem.hmsArgsJson(): JSONObject =
|
||||||
|
JSONObject().apply {
|
||||||
|
componentId.toIntOrNull()?.let { put("component_index", it) }
|
||||||
|
sensorIndex.toIntOrNull()?.let { put("sensor_index", it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toHmsCodeOrFallback(fallbackText: String): String {
|
||||||
|
val value = trim()
|
||||||
|
if (value.isBlank()) return fallbackText.toStableHmsCode()
|
||||||
|
if (value.startsWith("0x", ignoreCase = true)) return value
|
||||||
|
val decimal = value.toLongOrNull() ?: return value
|
||||||
|
return "0x" + decimal.toString(16).uppercase().padStart(8, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toStableHmsCode(): String {
|
||||||
|
val basis = ifBlank { "msdk_unknown_warning" }
|
||||||
|
val unsignedHash = basis.hashCode().toLong() and 0xffffffffL
|
||||||
|
return "0x" + unsignedHash.toString(16).uppercase().padStart(8, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WarningLevel.toHmsLevel(): Int =
|
||||||
|
when (this) {
|
||||||
|
WarningLevel.SERIOUS_WARNING,
|
||||||
|
WarningLevel.WARNING -> 2
|
||||||
|
WarningLevel.CAUTION,
|
||||||
|
WarningLevel.NOTICE -> 1
|
||||||
|
WarningLevel.UNKNOWN,
|
||||||
|
WarningLevel.NORMAL -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WarningLevel.isHmsAlarm(): Boolean = toHmsLevel() >= 2
|
||||||
|
|
||||||
|
private fun JSONArray.hmsSignature(): String {
|
||||||
|
if (length() == 0) return "empty"
|
||||||
|
val parts = mutableListOf<String>()
|
||||||
|
for (index in 0 until length()) {
|
||||||
|
val item = optJSONObject(index) ?: continue
|
||||||
|
parts += "${item.optString("code")}:${item.optInt("level")}:${item.optString("message_zh")}"
|
||||||
|
}
|
||||||
|
return parts.sorted().joinToString("|")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TelemetrySnapshot.toModeCode(productConnected: Boolean): Int {
|
private fun TelemetrySnapshot.toModeCode(productConnected: Boolean): Int {
|
||||||
@@ -716,7 +960,7 @@ private fun TelemetrySnapshot.droneOsdPositionSource(): String =
|
|||||||
if (hasUsableRtkPosition()) "RTK" else "FC"
|
if (hasUsableRtkPosition()) "RTK" else "FC"
|
||||||
|
|
||||||
private fun TelemetrySnapshot.hasFixedPosition(): Boolean =
|
private fun TelemetrySnapshot.hasFixedPosition(): Boolean =
|
||||||
gpsValid || hasUsableRtkPosition()
|
(gpsValid && locationValid) || hasUsableRtkPosition()
|
||||||
|
|
||||||
private fun TelemetrySnapshot.hasUsableRtkPosition(): Boolean =
|
private fun TelemetrySnapshot.hasUsableRtkPosition(): Boolean =
|
||||||
rtkLocationValid && (rtkHealthy || rtkFusionDataUsable || rtkPositioningSolution.isSolvedRtkSolution())
|
rtkLocationValid && (rtkHealthy || rtkFusionDataUsable || rtkPositioningSolution.isSolvedRtkSolution())
|
||||||
@@ -753,15 +997,6 @@ private fun TelemetrySnapshot.batteryHighVoltageStorageDays(): Int =
|
|||||||
.coerceIn(0L, Int.MAX_VALUE.toLong())
|
.coerceIn(0L, Int.MAX_VALUE.toLong())
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
private fun cloudArrayList(vararg items: Any): JSONArray =
|
|
||||||
JSONArray()
|
|
||||||
.put(JavaArrayListClass)
|
|
||||||
.put(
|
|
||||||
JSONArray().apply {
|
|
||||||
items.forEach { put(it) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun TelemetrySnapshot.cameraModeCode(): Int {
|
private fun TelemetrySnapshot.cameraModeCode(): Int {
|
||||||
val mode = cameraWorkMode.uppercase()
|
val mode = cameraWorkMode.uppercase()
|
||||||
return when {
|
return when {
|
||||||
|
|||||||
@@ -1,37 +1,60 @@
|
|||||||
package com.zklh.dronecontroller.core.diagnostics
|
package com.zklh.dronecontroller.core.diagnostics
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import dji.v5.manager.diagnostic.DeviceHealthManager
|
import dji.v5.manager.diagnostic.DeviceHealthManager
|
||||||
|
import dji.v5.manager.diagnostic.DeviceStatusManager
|
||||||
import dji.v5.manager.diagnostic.DJIDeviceHealthInfo
|
import dji.v5.manager.diagnostic.DJIDeviceHealthInfo
|
||||||
import dji.v5.manager.diagnostic.DJIDeviceHealthInfoChangeListener
|
import dji.v5.manager.diagnostic.DJIDeviceHealthInfoChangeListener
|
||||||
|
import dji.v5.manager.diagnostic.DJIDeviceStatus
|
||||||
|
import dji.v5.manager.diagnostic.DJIDeviceStatusChangeListener
|
||||||
import dji.v5.manager.diagnostic.WarningLevel
|
import dji.v5.manager.diagnostic.WarningLevel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
private const val DroneWarningLogTag = "ZklhDroneWarning"
|
||||||
|
|
||||||
data class DroneWarningState(
|
data class DroneWarningState(
|
||||||
val active: Boolean = false,
|
val active: Boolean = false,
|
||||||
val level: WarningLevel = WarningLevel.NORMAL,
|
val level: WarningLevel = WarningLevel.NORMAL,
|
||||||
val message: String = "无实时告警",
|
val message: String = "无实时告警",
|
||||||
val count: Int = 0,
|
val count: Int = 0,
|
||||||
val messages: List<String> = emptyList(),
|
val messages: List<String> = emptyList(),
|
||||||
|
val items: List<DroneWarningItem> = emptyList(),
|
||||||
val raw: String = ""
|
val raw: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class DroneWarningItem(
|
||||||
|
val code: String,
|
||||||
|
val level: WarningLevel,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val message: String,
|
||||||
|
val componentId: String = "",
|
||||||
|
val sensorIndex: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
class DroneWarningRepository {
|
class DroneWarningRepository {
|
||||||
private val _state = MutableStateFlow(DroneWarningState())
|
private val _state = MutableStateFlow(DroneWarningState())
|
||||||
val state: StateFlow<DroneWarningState> = _state.asStateFlow()
|
val state: StateFlow<DroneWarningState> = _state.asStateFlow()
|
||||||
|
|
||||||
private val healthInfoChangeListener = DJIDeviceHealthInfoChangeListener {
|
private val healthInfoChangeListener = DJIDeviceHealthInfoChangeListener { infos ->
|
||||||
|
updateWarnings(healthInfos = infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val deviceStatusChangeListener = DJIDeviceStatusChangeListener { _, _ ->
|
||||||
updateWarnings()
|
updateWarnings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var started = false
|
private var started = false
|
||||||
|
private var lastLoggedRaw = ""
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
if (started) return
|
if (started) return
|
||||||
started = true
|
started = true
|
||||||
DeviceHealthManager.getInstance().addDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
DeviceHealthManager.getInstance().addDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
||||||
|
DeviceStatusManager.getInstance().addDJIDeviceStatusChangeListener(deviceStatusChangeListener)
|
||||||
updateWarnings()
|
updateWarnings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,31 +62,43 @@ class DroneWarningRepository {
|
|||||||
if (!started) return
|
if (!started) return
|
||||||
started = false
|
started = false
|
||||||
DeviceHealthManager.getInstance().removeDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
DeviceHealthManager.getInstance().removeDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
||||||
|
DeviceStatusManager.getInstance().removeDJIDeviceStatusChangeListener(deviceStatusChangeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_state.value = DroneWarningState()
|
_state.value = DroneWarningState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWarnings() {
|
private fun updateWarnings(
|
||||||
val messages = DeviceHealthManager.getInstance().currentDJIDeviceHealthInfos
|
healthInfos: List<DJIDeviceHealthInfo> = DeviceHealthManager.getInstance().currentDJIDeviceHealthInfos
|
||||||
|
) {
|
||||||
|
val messages = healthInfos
|
||||||
.map { it.toMessage() }
|
.map { it.toMessage() }
|
||||||
|
.plus(DeviceStatusManager.getInstance().currentDJIDeviceStatus.toMessage())
|
||||||
|
.filterNotNull()
|
||||||
.filter { it.text.isNotBlank() }
|
.filter { it.text.isNotBlank() }
|
||||||
|
.filterNot { it.isNormalNotice() }
|
||||||
.sortedWith(compareByDescending<WarningMessage> { warningSeverity(it.level) }.thenBy { it.text })
|
.sortedWith(compareByDescending<WarningMessage> { warningSeverity(it.level) }.thenBy { it.text })
|
||||||
|
|
||||||
if (messages.isEmpty()) {
|
if (messages.isEmpty()) {
|
||||||
|
logIfChanged("")
|
||||||
_state.value = DroneWarningState()
|
_state.value = DroneWarningState()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val top = messages.first()
|
val top = messages.first()
|
||||||
|
val raw = messages.joinToString(separator = "\n") {
|
||||||
|
"${it.source}:${it.level}:${it.code}:${it.text}"
|
||||||
|
}
|
||||||
|
logIfChanged(raw)
|
||||||
_state.value = DroneWarningState(
|
_state.value = DroneWarningState(
|
||||||
active = true,
|
active = true,
|
||||||
level = top.level,
|
level = top.level,
|
||||||
message = top.text,
|
message = top.text,
|
||||||
count = messages.size,
|
count = messages.size,
|
||||||
messages = messages.map { it.text },
|
messages = messages.map { it.text },
|
||||||
raw = messages.joinToString(separator = "\n") { "${it.level}:${it.code}:${it.text}" }
|
items = messages.map { it.toItem() },
|
||||||
|
raw = raw
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,15 +111,69 @@ class DroneWarningRepository {
|
|||||||
title.isNotBlank() -> title
|
title.isNotBlank() -> title
|
||||||
else -> code
|
else -> code
|
||||||
}
|
}
|
||||||
return WarningMessage(text = text, level = warningLevel(), code = code)
|
return WarningMessage(
|
||||||
|
text = text,
|
||||||
|
level = warningLevel(),
|
||||||
|
code = code,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
componentId = componentId().toString(),
|
||||||
|
sensorIndex = sensorIndex().toString(),
|
||||||
|
source = "health"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DJIDeviceStatus.toMessage(): WarningMessage? {
|
||||||
|
if (this == DJIDeviceStatus.NORMAL || warningLevel() == WarningLevel.NORMAL) return null
|
||||||
|
val description = description()
|
||||||
|
val code = statusCode()
|
||||||
|
val text = description.ifBlank { toString() }
|
||||||
|
return WarningMessage(
|
||||||
|
text = text,
|
||||||
|
level = warningLevel(),
|
||||||
|
code = code,
|
||||||
|
title = text,
|
||||||
|
description = description,
|
||||||
|
source = "status"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class WarningMessage(
|
private data class WarningMessage(
|
||||||
val text: String,
|
val text: String,
|
||||||
val level: WarningLevel,
|
val level: WarningLevel,
|
||||||
val code: String
|
val code: String,
|
||||||
|
val title: String = "",
|
||||||
|
val description: String = "",
|
||||||
|
val componentId: String = "",
|
||||||
|
val sensorIndex: String = "",
|
||||||
|
val source: String
|
||||||
|
) {
|
||||||
|
fun toItem(): DroneWarningItem =
|
||||||
|
DroneWarningItem(
|
||||||
|
code = code.trim(),
|
||||||
|
level = level,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
message = text,
|
||||||
|
componentId = componentId,
|
||||||
|
sensorIndex = sensorIndex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun isNormalNotice(): Boolean =
|
||||||
|
level == WarningLevel.NOTICE &&
|
||||||
|
listOf(text, title, description).any { it.contains("正常") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logIfChanged(raw: String) {
|
||||||
|
if (raw == lastLoggedRaw) return
|
||||||
|
lastLoggedRaw = raw
|
||||||
|
if (raw.isBlank()) {
|
||||||
|
Log.d(DroneWarningLogTag, "warnings cleared")
|
||||||
|
} else {
|
||||||
|
Log.d(DroneWarningLogTag, "warnings changed:\n$raw")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun warningSeverity(level: WarningLevel): Int =
|
private fun warningSeverity(level: WarningLevel): Int =
|
||||||
when (level) {
|
when (level) {
|
||||||
WarningLevel.SERIOUS_WARNING -> 5
|
WarningLevel.SERIOUS_WARNING -> 5
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package com.zklh.dronecontroller.core.flight
|
|||||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||||
|
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||||
|
import dji.sdk.keyvalue.value.flightcontroller.LookAtInfo
|
||||||
|
import dji.sdk.keyvalue.value.flightcontroller.LookAtMode
|
||||||
import dji.v5.common.error.IDJIError
|
import dji.v5.common.error.IDJIError
|
||||||
import dji.v5.et.action
|
import dji.v5.et.action
|
||||||
import dji.v5.et.create
|
import dji.v5.et.create
|
||||||
@@ -100,4 +103,26 @@ class FlightControlService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun lookAt(
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
altitude: Double,
|
||||||
|
mode: LookAtMode = LookAtMode.LOOK_AT_GIMBAL_FREE
|
||||||
|
): DjiCommandResult =
|
||||||
|
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||||
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
|
} else {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val info = LookAtInfo().apply {
|
||||||
|
this.mode = mode
|
||||||
|
location = LocationCoordinate3D(latitude, longitude, altitude)
|
||||||
|
}
|
||||||
|
FlightControllerKey.KeyLookAt.create().action(info, {
|
||||||
|
continuation.resume(DjiCommandResult.ok("云台/机头看向目标点指令已下发"))
|
||||||
|
}, { error: IDJIError ->
|
||||||
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,96 @@ import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
|||||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||||
|
import dji.sdk.keyvalue.value.flightcontroller.FlyToMissionState
|
||||||
import dji.v5.common.callback.CommonCallbacks
|
import dji.v5.common.callback.CommonCallbacks
|
||||||
import dji.v5.common.error.IDJIError
|
import dji.v5.common.error.IDJIError
|
||||||
|
import dji.v5.manager.intelligent.IMissionCapabilityListener
|
||||||
|
import dji.v5.manager.intelligent.IMissionInfoListener
|
||||||
import dji.v5.manager.intelligent.IntelligentFlightManager
|
import dji.v5.manager.intelligent.IntelligentFlightManager
|
||||||
|
import dji.v5.manager.intelligent.flyto.FlyToCapability
|
||||||
|
import dji.v5.manager.intelligent.flyto.FlyToInfo
|
||||||
import dji.v5.manager.intelligent.flyto.FlyToParam
|
import dji.v5.manager.intelligent.flyto.FlyToParam
|
||||||
import dji.v5.manager.intelligent.flyto.FlyToTarget
|
import dji.v5.manager.intelligent.flyto.FlyToTarget
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
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 kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
private const val FlyToLogTag = "ZklhFlyTo"
|
private const val FlyToLogTag = "ZklhFlyTo"
|
||||||
|
|
||||||
|
data class FlyToStatus(
|
||||||
|
val state: FlyToMissionState = FlyToMissionState.UNKNOWN,
|
||||||
|
val mode: FlyToMode = FlyToMode.UNKNOWN,
|
||||||
|
val height: Int = 0,
|
||||||
|
val targetLatitude: Double = 0.0,
|
||||||
|
val targetLongitude: Double = 0.0,
|
||||||
|
val targetAltitude: Double = 0.0,
|
||||||
|
val heightRangeText: String = ""
|
||||||
|
) {
|
||||||
|
val active: Boolean
|
||||||
|
get() = state !in setOf(
|
||||||
|
FlyToMissionState.IDLE,
|
||||||
|
FlyToMissionState.FINISHED,
|
||||||
|
FlyToMissionState.UNKNOWN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class FlyToService {
|
class FlyToService {
|
||||||
|
private val manager = IntelligentFlightManager.getInstance().flyToMissionManager
|
||||||
|
private val _status = MutableStateFlow(FlyToStatus())
|
||||||
|
val status: StateFlow<FlyToStatus> = _status.asStateFlow()
|
||||||
|
|
||||||
|
private val missionInfoListener = object : IMissionInfoListener<FlyToInfo, FlyToTarget> {
|
||||||
|
override fun onMissionInfoUpdate(info: FlyToInfo) {
|
||||||
|
_status.update {
|
||||||
|
it.copy(
|
||||||
|
state = info.state ?: FlyToMissionState.UNKNOWN,
|
||||||
|
mode = info.flyToMode ?: FlyToMode.UNKNOWN,
|
||||||
|
height = info.flyToHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d(FlyToLogTag, "info state=${info.state} mode=${info.flyToMode} height=${info.flyToHeight} exit=${info.exitReason}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMissionTargetUpdate(target: FlyToTarget) {
|
||||||
|
val location = target.targetLocation
|
||||||
|
_status.update {
|
||||||
|
it.copy(
|
||||||
|
targetLatitude = location?.latitude ?: 0.0,
|
||||||
|
targetLongitude = location?.longitude ?: 0.0,
|
||||||
|
targetAltitude = location?.altitude ?: 0.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d(FlyToLogTag, "target=$location maxSpeed=${target.maxSpeed} securityTakeoffHeight=${target.securityTakeoffHeight}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val capabilityListener = object : IMissionCapabilityListener<FlyToCapability> {
|
||||||
|
override fun onMissionCapabilityUpdate(capability: FlyToCapability) {
|
||||||
|
_status.update {
|
||||||
|
it.copy(
|
||||||
|
heightRangeText = capability.heightRange?.toString().orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d(
|
||||||
|
FlyToLogTag,
|
||||||
|
"capability heightRange=${capability.heightRange}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
runCatching { manager.addMissionInfoListener(missionInfoListener) }
|
||||||
|
.onFailure { Log.w(FlyToLogTag, "addMissionInfoListener failed: ${it.message}") }
|
||||||
|
runCatching { manager.addMissionCapabilityListener(capabilityListener) }
|
||||||
|
.onFailure { Log.w(FlyToLogTag, "addMissionCapabilityListener failed: ${it.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isMissionActive(): Boolean = status.value.active
|
||||||
|
|
||||||
suspend fun startFlyTo(
|
suspend fun startFlyTo(
|
||||||
latitude: Double,
|
latitude: Double,
|
||||||
longitude: Double,
|
longitude: Double,
|
||||||
@@ -28,35 +107,126 @@ class FlyToService {
|
|||||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
} else {
|
} else {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val manager = IntelligentFlightManager.getInstance().flyToMissionManager
|
val targetHeight = height.roundToInt().coerceAtLeast(1)
|
||||||
|
val safeTakeoffHeight = securityTakeoffHeight
|
||||||
|
.coerceAtLeast(1)
|
||||||
|
.coerceAtMost(targetHeight)
|
||||||
val target = FlyToTarget().apply {
|
val target = FlyToTarget().apply {
|
||||||
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
||||||
this.maxSpeed = maxSpeed
|
this.maxSpeed = maxSpeed
|
||||||
this.securityTakeoffHeight = securityTakeoffHeight
|
this.securityTakeoffHeight = safeTakeoffHeight
|
||||||
|
}
|
||||||
|
val param = FlyToParam().apply {
|
||||||
|
this.flyToMode = flyToMode
|
||||||
|
this.height = targetHeight
|
||||||
}
|
}
|
||||||
Log.d(
|
Log.d(
|
||||||
FlyToLogTag,
|
FlyToLogTag,
|
||||||
"startFlyTo lat=$latitude lon=$longitude height=$height mode=$flyToMode maxSpeed=$maxSpeed securityTakeoffHeight=$securityTakeoffHeight"
|
"startFlyTo lat=$latitude lon=$longitude height=$height mode=$flyToMode maxSpeed=$maxSpeed securityTakeoffHeight=$safeTakeoffHeight"
|
||||||
)
|
)
|
||||||
manager.startMission(
|
manager.startMission(
|
||||||
target,
|
target,
|
||||||
null,
|
param,
|
||||||
callback(continuation, "指点飞行已开始")
|
callback(
|
||||||
|
continuation,
|
||||||
|
"指点飞行已开始",
|
||||||
|
onSuccess = {
|
||||||
|
_status.update {
|
||||||
|
it.copy(
|
||||||
|
state = FlyToMissionState.PREPARE,
|
||||||
|
mode = flyToMode,
|
||||||
|
height = targetHeight,
|
||||||
|
targetLatitude = latitude,
|
||||||
|
targetLongitude = longitude,
|
||||||
|
targetAltitude = height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
_status.update { it.copy(state = FlyToMissionState.IDLE) }
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopFlyTo(): DjiCommandResult =
|
suspend fun stopFlyTo(): DjiCommandResult =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
IntelligentFlightManager.getInstance().flyToMissionManager.stopMission(
|
manager.stopMission(
|
||||||
callback(continuation, "指点飞行已停止")
|
callback(
|
||||||
|
continuation,
|
||||||
|
"指点飞行已停止",
|
||||||
|
onSuccess = { _status.update { it.copy(state = FlyToMissionState.IDLE) } }
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateFlyToTarget(
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
height: Double,
|
||||||
|
maxSpeed: Int = 14,
|
||||||
|
securityTakeoffHeight: Int = 20
|
||||||
|
): DjiCommandResult =
|
||||||
|
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||||
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
|
} else {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val targetHeight = height.roundToInt().coerceAtLeast(1)
|
||||||
|
val safeTakeoffHeight = securityTakeoffHeight
|
||||||
|
.coerceAtLeast(1)
|
||||||
|
.coerceAtMost(targetHeight)
|
||||||
|
val target = FlyToTarget().apply {
|
||||||
|
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
||||||
|
this.maxSpeed = maxSpeed
|
||||||
|
this.securityTakeoffHeight = safeTakeoffHeight
|
||||||
|
}
|
||||||
|
Log.d(
|
||||||
|
FlyToLogTag,
|
||||||
|
"updateFlyToTarget lat=$latitude lon=$longitude height=$height maxSpeed=$maxSpeed securityTakeoffHeight=$safeTakeoffHeight"
|
||||||
|
)
|
||||||
|
manager.updateMissionTarget(
|
||||||
|
target,
|
||||||
|
callback(
|
||||||
|
continuation,
|
||||||
|
"指点飞行目标点已更新",
|
||||||
|
onSuccess = {
|
||||||
|
_status.update {
|
||||||
|
it.copy(
|
||||||
|
targetLatitude = latitude,
|
||||||
|
targetLongitude = longitude,
|
||||||
|
targetAltitude = height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateFlyToParam(
|
||||||
|
height: Int? = null,
|
||||||
|
mode: FlyToMode? = null
|
||||||
|
): DjiCommandResult =
|
||||||
|
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||||
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
|
} else {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val param = FlyToParam().apply {
|
||||||
|
if (mode != null) flyToMode = mode
|
||||||
|
if (height != null) this.height = height.coerceAtLeast(1)
|
||||||
|
}
|
||||||
|
manager.updateMissionParam(
|
||||||
|
param,
|
||||||
|
callback(continuation, "指点飞行参数已更新")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setFlyToMode(mode: FlyToMode): DjiCommandResult =
|
suspend fun setFlyToMode(mode: FlyToMode): DjiCommandResult =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val param = FlyToParam().apply { flyToMode = mode }
|
val param = FlyToParam().apply { flyToMode = mode }
|
||||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(
|
manager.updateMissionParam(
|
||||||
param,
|
param,
|
||||||
callback(continuation, "指点飞行模式已更新:${mode.name}")
|
callback(continuation, "指点飞行模式已更新:${mode.name}")
|
||||||
)
|
)
|
||||||
@@ -65,7 +235,7 @@ class FlyToService {
|
|||||||
suspend fun setFlyToHeight(height: Int): DjiCommandResult =
|
suspend fun setFlyToHeight(height: Int): DjiCommandResult =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val param = FlyToParam().apply { this.height = height }
|
val param = FlyToParam().apply { this.height = height }
|
||||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(
|
manager.updateMissionParam(
|
||||||
param,
|
param,
|
||||||
callback(continuation, "指点飞行高度已更新:${height}m")
|
callback(continuation, "指点飞行高度已更新:${height}m")
|
||||||
)
|
)
|
||||||
@@ -73,14 +243,18 @@ class FlyToService {
|
|||||||
|
|
||||||
private fun callback(
|
private fun callback(
|
||||||
continuation: kotlinx.coroutines.CancellableContinuation<DjiCommandResult>,
|
continuation: kotlinx.coroutines.CancellableContinuation<DjiCommandResult>,
|
||||||
successMessage: String
|
successMessage: String,
|
||||||
|
onSuccess: () -> Unit = {},
|
||||||
|
onFailure: () -> Unit = {}
|
||||||
): CommonCallbacks.CompletionCallback =
|
): CommonCallbacks.CompletionCallback =
|
||||||
object : CommonCallbacks.CompletionCallback {
|
object : CommonCallbacks.CompletionCallback {
|
||||||
override fun onSuccess() {
|
override fun onSuccess() {
|
||||||
|
onSuccess()
|
||||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(error: IDJIError) {
|
override fun onFailure(error: IDJIError) {
|
||||||
|
onFailure()
|
||||||
continuation.resume(DjiCommandResult.failed(error))
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import dji.sdk.keyvalue.key.DJIKey
|
|||||||
import dji.sdk.keyvalue.key.GimbalKey
|
import dji.sdk.keyvalue.key.GimbalKey
|
||||||
import dji.sdk.keyvalue.key.KeyTools
|
import dji.sdk.keyvalue.key.KeyTools
|
||||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
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.CtrlInfo
|
||||||
|
import dji.sdk.keyvalue.value.gimbal.GimbalResetType
|
||||||
import dji.sdk.keyvalue.value.gimbal.GimbalSpeedRotation
|
import dji.sdk.keyvalue.value.gimbal.GimbalSpeedRotation
|
||||||
import dji.v5.common.error.IDJIError
|
import dji.v5.common.error.IDJIError
|
||||||
import dji.v5.et.action
|
import dji.v5.et.action
|
||||||
@@ -33,8 +33,8 @@ class GimbalControlService(
|
|||||||
|
|
||||||
suspend fun reset(): DjiCommandResult =
|
suspend fun reset(): DjiCommandResult =
|
||||||
runAction(
|
runAction(
|
||||||
KeyTools.createKey(GimbalKey.KeyRestoreFactorySettings, gimbalIndex),
|
KeyTools.createKey(GimbalKey.KeyGimbalReset, gimbalIndex),
|
||||||
EmptyMsg(),
|
GimbalResetType.RECENTER,
|
||||||
"云台复位已下发"
|
"云台复位已下发"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,4 +55,3 @@ class GimbalControlService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,8 +165,8 @@ class LiveStreamingService {
|
|||||||
|
|
||||||
val cameraIndex = config.videoId.toCameraIndex()
|
val cameraIndex = config.videoId.toCameraIndex()
|
||||||
runCatching {
|
runCatching {
|
||||||
cameraStreamManager().enableStream(cameraIndex, true)
|
|
||||||
manager.cameraIndex = cameraIndex
|
manager.cameraIndex = cameraIndex
|
||||||
|
cameraStreamManager().enableStream(cameraIndex, true)
|
||||||
manager.liveStreamSettings = LiveStreamSettings.Builder()
|
manager.liveStreamSettings = LiveStreamSettings.Builder()
|
||||||
.setLiveStreamType(LiveStreamType.RTMP)
|
.setLiveStreamType(LiveStreamType.RTMP)
|
||||||
.setRtmpSettings(RtmpSettings.Builder().setUrl(config.url).build())
|
.setRtmpSettings(RtmpSettings.Builder().setUrl(config.url).build())
|
||||||
@@ -328,10 +328,19 @@ private fun JSONObject.optValueAsString(vararg names: String): String {
|
|||||||
private fun JSONArray.arrayValueAsString(): String {
|
private fun JSONArray.arrayValueAsString(): String {
|
||||||
if (length() == 0) return ""
|
if (length() == 0) return ""
|
||||||
for (i in length() - 1 downTo 0) {
|
for (i in length() - 1 downTo 0) {
|
||||||
val value = opt(i)
|
when (val value = opt(i)) {
|
||||||
if (value is String && value.isNotBlank()) return value
|
is String -> if (value.isNotBlank() && !value.startsWith("com.", ignoreCase = true)) return value
|
||||||
|
is JSONObject -> {
|
||||||
|
val nested = value.optStringAny("url", "rtmp_url", "rtmpUrl", "push_url", "pushUrl", "value", "id")
|
||||||
|
if (nested.isNotBlank()) return nested
|
||||||
}
|
}
|
||||||
return optString(length() - 1, "")
|
is JSONArray -> {
|
||||||
|
val nested = value.arrayValueAsString()
|
||||||
|
if (nested.isNotBlank()) return nested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JSONObject.optIntAny(defaultValue: Int, vararg names: String): Int {
|
private fun JSONObject.optIntAny(defaultValue: Int, vararg names: String): Int {
|
||||||
|
|||||||
@@ -2,12 +2,25 @@ package com.zklh.dronecontroller.core.media
|
|||||||
|
|
||||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||||
import dji.sdk.keyvalue.key.CameraKey
|
import dji.sdk.keyvalue.key.CameraKey
|
||||||
|
import dji.sdk.keyvalue.key.DJIKey
|
||||||
|
import dji.sdk.keyvalue.key.DJICameraKey
|
||||||
import dji.sdk.keyvalue.key.KeyTools
|
import dji.sdk.keyvalue.key.KeyTools
|
||||||
|
import dji.sdk.keyvalue.value.camera.CameraExposureCompensation
|
||||||
|
import dji.sdk.keyvalue.value.camera.CameraExposureMode
|
||||||
|
import dji.sdk.keyvalue.value.camera.CameraFocusMode
|
||||||
|
import dji.sdk.keyvalue.value.camera.CameraNightSceneMode
|
||||||
import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType
|
import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType
|
||||||
|
import dji.sdk.keyvalue.value.camera.FrameZoomMsg
|
||||||
|
import dji.sdk.keyvalue.value.camera.LiveViewSourceCameraType
|
||||||
|
import dji.sdk.keyvalue.value.camera.TapZoomMode
|
||||||
|
import dji.sdk.keyvalue.value.camera.ThermalDisplayMode
|
||||||
|
import dji.sdk.keyvalue.value.camera.ZoomTargetPointInfo
|
||||||
import dji.sdk.keyvalue.value.common.CameraLensType
|
import dji.sdk.keyvalue.value.common.CameraLensType
|
||||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||||
import dji.sdk.keyvalue.key.DJIKey
|
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||||
import dji.v5.common.error.IDJIError
|
import dji.v5.common.error.IDJIError
|
||||||
|
import dji.v5.et.action
|
||||||
|
import dji.v5.et.create
|
||||||
import dji.v5.et.set
|
import dji.v5.et.set
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -43,6 +56,131 @@ class CameraControlService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setLinkZoomEnabled(enabled: Boolean): DjiCommandResult =
|
||||||
|
setValueKey(
|
||||||
|
KeyTools.createKey(CameraKey.KeyLinkZoomEnabled, cameraIndex),
|
||||||
|
enabled,
|
||||||
|
if (enabled) "联动变焦已开启" else "联动变焦已关闭"
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun frameZoom(
|
||||||
|
cameraType: String,
|
||||||
|
locked: Boolean,
|
||||||
|
x: Double,
|
||||||
|
y: Double,
|
||||||
|
width: Double,
|
||||||
|
height: Double
|
||||||
|
): DjiCommandResult {
|
||||||
|
val targetCamera = cameraType.toLiveViewSourceCameraType()
|
||||||
|
val message = FrameZoomMsg().apply {
|
||||||
|
lockGimbal = locked
|
||||||
|
switchLiveview = true
|
||||||
|
this.targetCamera = targetCamera
|
||||||
|
this.x = x.coerceIn(0.0, 1.0)
|
||||||
|
this.y = y.coerceIn(0.0, 1.0)
|
||||||
|
this.width = width.coerceIn(0.0, 1.0)
|
||||||
|
length = height.coerceIn(0.0, 1.0)
|
||||||
|
}
|
||||||
|
return runAction(
|
||||||
|
CameraKey.KeyFrameZoom.create(cameraIndex),
|
||||||
|
message,
|
||||||
|
"框选变焦已下发"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun aimAt(
|
||||||
|
cameraType: String,
|
||||||
|
locked: Boolean,
|
||||||
|
x: Double,
|
||||||
|
y: Double
|
||||||
|
): DjiCommandResult {
|
||||||
|
if (cameraType.isNotBlank()) {
|
||||||
|
val lens = setLens(cameraType)
|
||||||
|
if (!lens.success) return lens
|
||||||
|
}
|
||||||
|
val enable = setValueKey(
|
||||||
|
KeyTools.createKey(CameraKey.KeyTapZoomEnable, cameraIndex),
|
||||||
|
true,
|
||||||
|
"点选变焦已开启"
|
||||||
|
)
|
||||||
|
if (!enable.success) return enable
|
||||||
|
val target = ZoomTargetPointInfo().apply {
|
||||||
|
this.x = x.coerceIn(0.0, 1.0)
|
||||||
|
this.y = y.coerceIn(0.0, 1.0)
|
||||||
|
tapZoomModeEnable = true
|
||||||
|
mode = if (locked) TapZoomMode.GIMBAL_LOCK else TapZoomMode.GIMBAL_FOLLOW
|
||||||
|
}
|
||||||
|
return runAction(
|
||||||
|
CameraKey.KeyTapZoomAtTarget.create(cameraIndex),
|
||||||
|
target,
|
||||||
|
"相机点选瞄准已下发"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setExposureCompensation(
|
||||||
|
cameraType: String,
|
||||||
|
value: Int
|
||||||
|
): DjiCommandResult {
|
||||||
|
val compensation = CameraExposureCompensation.find(value)
|
||||||
|
if (compensation == CameraExposureCompensation.UNKNOWN) {
|
||||||
|
return DjiCommandResult.failed("未知曝光补偿值:$value")
|
||||||
|
}
|
||||||
|
return setValueKey(
|
||||||
|
KeyTools.createCameraKey(CameraKey.KeyExposureCompensation, cameraIndex, cameraType.toLensType()),
|
||||||
|
compensation,
|
||||||
|
"曝光补偿已设置:${compensation.name}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setExposureMode(
|
||||||
|
cameraType: String,
|
||||||
|
mode: Int
|
||||||
|
): DjiCommandResult {
|
||||||
|
val exposureMode = CameraExposureMode.find(mode)
|
||||||
|
if (exposureMode == CameraExposureMode.UNKNOWN) {
|
||||||
|
return DjiCommandResult.failed("未知曝光模式:$mode")
|
||||||
|
}
|
||||||
|
return setValueKey(
|
||||||
|
KeyTools.createCameraKey(CameraKey.KeyExposureMode, cameraIndex, cameraType.toLensType()),
|
||||||
|
exposureMode,
|
||||||
|
"曝光模式已设置:${exposureMode.name}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setFocusMode(
|
||||||
|
cameraType: String,
|
||||||
|
mode: Int
|
||||||
|
): DjiCommandResult {
|
||||||
|
val focusMode = CameraFocusMode.find(mode)
|
||||||
|
if (focusMode == CameraFocusMode.UNKNOWN) {
|
||||||
|
return DjiCommandResult.failed("未知对焦模式:$mode")
|
||||||
|
}
|
||||||
|
return setValueKey(
|
||||||
|
KeyTools.createCameraKey(CameraKey.KeyCameraFocusMode, cameraIndex, cameraType.toLensType()),
|
||||||
|
focusMode,
|
||||||
|
"对焦模式已设置:${focusMode.name}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setThermalDisplaySplit(enabled: Boolean): DjiCommandResult =
|
||||||
|
setValueKey(
|
||||||
|
KeyTools.createCameraKey(CameraKey.KeyThermalDisplayMode, cameraIndex, CameraLensType.CAMERA_LENS_THERMAL),
|
||||||
|
if (enabled) ThermalDisplayMode.PIP else ThermalDisplayMode.VISUAL_ONLY,
|
||||||
|
if (enabled) "红外分屏已开启" else "红外分屏已关闭"
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun setNightSceneMode(mode: Int): DjiCommandResult {
|
||||||
|
val nightMode = CameraNightSceneMode.find(mode)
|
||||||
|
if (nightMode == CameraNightSceneMode.UNKNOWN) {
|
||||||
|
return DjiCommandResult.failed("未知夜景模式:$mode")
|
||||||
|
}
|
||||||
|
return setValueKey(
|
||||||
|
KeyTools.createKey(DJICameraKey.KeyCameraNightSceneMode, cameraIndex),
|
||||||
|
nightMode,
|
||||||
|
"夜景模式已设置:${nightMode.name}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun setDoubleKey(
|
private suspend fun setDoubleKey(
|
||||||
key: DJIKey<Double>,
|
key: DJIKey<Double>,
|
||||||
value: Double,
|
value: Double,
|
||||||
@@ -66,6 +204,23 @@ class CameraControlService(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.normalizeLensName(): String =
|
private fun String.normalizeLensName(): String =
|
||||||
@@ -73,3 +228,17 @@ private fun String.normalizeLensName(): String =
|
|||||||
.replace("-", "_")
|
.replace("-", "_")
|
||||||
.lowercase()
|
.lowercase()
|
||||||
|
|
||||||
|
private fun String.toLensType(): CameraLensType =
|
||||||
|
when (normalizeLensName()) {
|
||||||
|
"ir", "infrared", "thermal", "camera_lens_thermal" -> CameraLensType.CAMERA_LENS_THERMAL
|
||||||
|
"zoom", "visible_zoom", "camera_lens_zoom" -> CameraLensType.CAMERA_LENS_ZOOM
|
||||||
|
else -> CameraLensType.CAMERA_LENS_WIDE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toLiveViewSourceCameraType(): LiveViewSourceCameraType =
|
||||||
|
when (normalizeLensName()) {
|
||||||
|
"wide", "normal", "visible", "visible_wide", "camera_lens_wide" -> LiveViewSourceCameraType.WIDE_CAMERA
|
||||||
|
"ir", "infrared", "thermal", "camera_lens_thermal" -> LiveViewSourceCameraType.INFRARED_CAMERA
|
||||||
|
"zoom", "visible_zoom", "camera_lens_zoom" -> LiveViewSourceCameraType.ZOOM_CAMERA
|
||||||
|
else -> LiveViewSourceCameraType.DEFAULT_CAMERA
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ import dji.v5.et.action
|
|||||||
import dji.v5.et.create
|
import dji.v5.et.create
|
||||||
import dji.v5.et.get
|
import dji.v5.et.get
|
||||||
import dji.v5.et.set
|
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 kotlin.coroutines.resume
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@@ -38,7 +34,6 @@ class CameraMediaService(
|
|||||||
Log.d(CameraMediaLogTag, "takePhoto candidates=${candidateCameraIndexes()}")
|
Log.d(CameraMediaLogTag, "takePhoto candidates=${candidateCameraIndexes()}")
|
||||||
for (index in candidateCameraIndexes()) {
|
for (index in candidateCameraIndexes()) {
|
||||||
val previousMedia = readLatestMediaFile(index)
|
val previousMedia = readLatestMediaFile(index)
|
||||||
val previousSnapshot = readCachedMediaSnapshot("before photo")
|
|
||||||
val modeResult = setPhotoCaptureMode(index)
|
val modeResult = setPhotoCaptureMode(index)
|
||||||
if (!modeResult.success) {
|
if (!modeResult.success) {
|
||||||
lastFailure = modeResult
|
lastFailure = modeResult
|
||||||
@@ -61,7 +56,6 @@ class CameraMediaService(
|
|||||||
val saved = waitForGeneratedMediaFile(
|
val saved = waitForGeneratedMediaFile(
|
||||||
cameraIndex = index,
|
cameraIndex = index,
|
||||||
previousMedia = previousMedia,
|
previousMedia = previousMedia,
|
||||||
previousSnapshot = previousSnapshot,
|
|
||||||
successMessage = "拍照完成,文件已写入飞机存储",
|
successMessage = "拍照完成,文件已写入飞机存储",
|
||||||
timeoutMessage = "拍照指令已下发,但没有检测到新照片文件。请检查飞机 SD 卡/内置存储、相机存储位置和电量告警。"
|
timeoutMessage = "拍照指令已下发,但没有检测到新照片文件。请检查飞机 SD 卡/内置存储、相机存储位置和电量告警。"
|
||||||
)
|
)
|
||||||
@@ -122,7 +116,6 @@ class CameraMediaService(
|
|||||||
val indexes = if (recordingIndexes.isNotEmpty()) recordingIndexes else candidateCameraIndexes()
|
val indexes = if (recordingIndexes.isNotEmpty()) recordingIndexes else candidateCameraIndexes()
|
||||||
for (index in indexes) {
|
for (index in indexes) {
|
||||||
val previousMedia = readLatestMediaFile(index)
|
val previousMedia = readLatestMediaFile(index)
|
||||||
val previousSnapshot = readCachedMediaSnapshot("before stop record")
|
|
||||||
val stopResult = runEmptyAction(
|
val stopResult = runEmptyAction(
|
||||||
CameraKey.KeyStopRecord.create(index),
|
CameraKey.KeyStopRecord.create(index),
|
||||||
"录像停止指令已下发,正在等待文件写入"
|
"录像停止指令已下发,正在等待文件写入"
|
||||||
@@ -142,7 +135,6 @@ class CameraMediaService(
|
|||||||
val saved = waitForGeneratedMediaFile(
|
val saved = waitForGeneratedMediaFile(
|
||||||
cameraIndex = index,
|
cameraIndex = index,
|
||||||
previousMedia = previousMedia,
|
previousMedia = previousMedia,
|
||||||
previousSnapshot = previousSnapshot,
|
|
||||||
successMessage = "录像已停止,文件已写入飞机存储",
|
successMessage = "录像已停止,文件已写入飞机存储",
|
||||||
timeoutMessage = "录像已停止,但没有检测到新视频文件。请检查飞机 SD 卡/内置存储和相机存储状态。",
|
timeoutMessage = "录像已停止,但没有检测到新视频文件。请检查飞机 SD 卡/内置存储和相机存储状态。",
|
||||||
timeoutMs = 12_000L
|
timeoutMs = 12_000L
|
||||||
@@ -252,6 +244,9 @@ class CameraMediaService(
|
|||||||
fun isRecording(): Boolean =
|
fun isRecording(): Boolean =
|
||||||
isRecording(activeCameraIndex)
|
isRecording(activeCameraIndex)
|
||||||
|
|
||||||
|
fun currentCameraIndex(): ComponentIndexType =
|
||||||
|
activeCameraIndex
|
||||||
|
|
||||||
private fun isRecording(index: ComponentIndexType): Boolean =
|
private fun isRecording(index: ComponentIndexType): Boolean =
|
||||||
runCatching {
|
runCatching {
|
||||||
DJICameraKey.KeyIsRecording.create(index).get(false)
|
DJICameraKey.KeyIsRecording.create(index).get(false)
|
||||||
@@ -282,7 +277,6 @@ class CameraMediaService(
|
|||||||
private suspend fun waitForGeneratedMediaFile(
|
private suspend fun waitForGeneratedMediaFile(
|
||||||
cameraIndex: ComponentIndexType,
|
cameraIndex: ComponentIndexType,
|
||||||
previousMedia: GeneratedMediaFileInfo?,
|
previousMedia: GeneratedMediaFileInfo?,
|
||||||
previousSnapshot: MediaSnapshot?,
|
|
||||||
successMessage: String,
|
successMessage: String,
|
||||||
timeoutMessage: String,
|
timeoutMessage: String,
|
||||||
timeoutMs: Long = 8_000L
|
timeoutMs: Long = 8_000L
|
||||||
@@ -306,23 +300,13 @@ class CameraMediaService(
|
|||||||
if (afterStoreIdentity.isNotBlank() && afterStoreIdentity != previousIdentity) {
|
if (afterStoreIdentity.isNotBlank() && afterStoreIdentity != previousIdentity) {
|
||||||
return DjiCommandResult.ok("$successMessage:${afterStoreMedia.mediaDisplayName()}")
|
return DjiCommandResult.ok("$successMessage:${afterStoreMedia.mediaDisplayName()}")
|
||||||
}
|
}
|
||||||
val afterStoreSnapshot = fetchMediaSnapshot(cameraIndex, "after store")
|
return DjiCommandResult.ok(successMessage)
|
||||||
return if (afterStoreSnapshot.isNewerThan(previousSnapshot)) {
|
|
||||||
DjiCommandResult.ok("$successMessage:${afterStoreSnapshot?.latestDisplayName ?: "飞机存储"}")
|
|
||||||
} else {
|
|
||||||
DjiCommandResult.ok(successMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(300)
|
delay(300)
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalSnapshot = fetchMediaSnapshot(cameraIndex, "after timeout")
|
return DjiCommandResult.failed(timeoutMessage)
|
||||||
return if (finalSnapshot.isNewerThan(previousSnapshot)) {
|
|
||||||
DjiCommandResult.ok("$successMessage:${finalSnapshot?.latestDisplayName ?: "飞机存储"}")
|
|
||||||
} else {
|
|
||||||
DjiCommandResult.failed(timeoutMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readLatestMediaFile(index: ComponentIndexType): GeneratedMediaFileInfo? =
|
private fun readLatestMediaFile(index: ComponentIndexType): GeneratedMediaFileInfo? =
|
||||||
@@ -330,116 +314,6 @@ class CameraMediaService(
|
|||||||
DJICameraKey.KeyNewlyGeneratedMediaFile.create(index).get(GeneratedMediaFileInfo())
|
DJICameraKey.KeyNewlyGeneratedMediaFile.create(index).get(GeneratedMediaFileInfo())
|
||||||
}.getOrNull()
|
}.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 =
|
private fun isCameraStoringFile(index: ComponentIndexType): Boolean =
|
||||||
runCatching {
|
runCatching {
|
||||||
DJICameraKey.KeyCameraStoringFile.create(index).get(false)
|
DJICameraKey.KeyCameraStoringFile.create(index).get(false)
|
||||||
@@ -607,29 +481,4 @@ class CameraMediaService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.zklh.dronecontroller.core.simulator
|
package com.zklh.dronecontroller.core.simulator
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||||
import dji.v5.common.callback.CommonCallbacks
|
import dji.v5.common.callback.CommonCallbacks
|
||||||
@@ -15,6 +16,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
private const val SimulatorLogTag = "ZklhSimulator"
|
||||||
|
|
||||||
data class SimulatorStatus(
|
data class SimulatorStatus(
|
||||||
val enabled: Boolean = false,
|
val enabled: Boolean = false,
|
||||||
val motorsOn: Boolean = false,
|
val motorsOn: Boolean = false,
|
||||||
@@ -49,6 +52,7 @@ class SimulatorService {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
SimulatorManager.getInstance().addSimulatorStateListener(listener)
|
SimulatorManager.getInstance().addSimulatorStateListener(listener)
|
||||||
|
disableStaleSimulator()
|
||||||
_status.update {
|
_status.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
enabled = SimulatorManager.getInstance().isSimulatorEnabled,
|
enabled = SimulatorManager.getInstance().isSimulatorEnabled,
|
||||||
@@ -57,9 +61,27 @@ class SimulatorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun disableStaleSimulator() {
|
||||||
|
if (!SimulatorManager.getInstance().isSimulatorEnabled) return
|
||||||
|
SimulatorManager.getInstance().disableSimulator(
|
||||||
|
object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
Log.d(SimulatorLogTag, "disabled stale simulator on startup")
|
||||||
|
_status.update {
|
||||||
|
it.copy(enabled = false, flying = false, motorsOn = false, message = "模拟器未开启")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(SimulatorLogTag, "disable stale simulator failed: $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun enable(
|
suspend fun enable(
|
||||||
latitude: Double = DEFAULT_LATITUDE,
|
latitude: Double,
|
||||||
longitude: Double = DEFAULT_LONGITUDE,
|
longitude: Double,
|
||||||
satelliteCount: Int = 12
|
satelliteCount: Int = 12
|
||||||
): DjiCommandResult =
|
): DjiCommandResult =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
@@ -106,8 +128,4 @@ class SimulatorService {
|
|||||||
SimulatorManager.getInstance().removeSimulatorStateListener(listener)
|
SimulatorManager.getInstance().removeSimulatorStateListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val DEFAULT_LATITUDE = 31.2304
|
|
||||||
const val DEFAULT_LONGITUDE = 121.4737
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ import dji.v5.manager.aircraft.rtk.RTKLocationInfoListener
|
|||||||
import dji.v5.manager.aircraft.rtk.RTKSystemState
|
import dji.v5.manager.aircraft.rtk.RTKSystemState
|
||||||
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
|
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
|
||||||
import dji.v5.manager.aircraft.rtk.network.INetworkServiceInfoListener
|
import dji.v5.manager.aircraft.rtk.network.INetworkServiceInfoListener
|
||||||
|
import dji.v5.manager.aircraft.rtk.station.ConnectedRTKStationInfoListener
|
||||||
|
import dji.v5.manager.aircraft.rtk.station.RTKStationConnectStatusListener
|
||||||
|
import dji.v5.manager.aircraft.rtk.station.SearchRTKStationListener
|
||||||
|
import dji.v5.utils.common.LocationUtil
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -59,6 +63,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
|
|
||||||
private const val TelemetryLogTag = "ZklhTelemetry"
|
private const val TelemetryLogTag = "ZklhTelemetry"
|
||||||
private const val RTK_START_RETRY_INTERVAL_MS = 20_000L
|
private const val RTK_START_RETRY_INTERVAL_MS = 20_000L
|
||||||
|
private const val RTK_BOOTSTRAP_RETRY_INTERVAL_MS = 15_000L
|
||||||
|
|
||||||
data class TelemetrySnapshot(
|
data class TelemetrySnapshot(
|
||||||
val latitude: Double = 0.0,
|
val latitude: Double = 0.0,
|
||||||
@@ -162,6 +167,18 @@ class TelemetryRepository(
|
|||||||
private val rtkCenter = RTKCenter.getInstance()
|
private val rtkCenter = RTKCenter.getInstance()
|
||||||
private val rtkLocationInfoListener = RTKLocationInfoListener { info -> updateRtkLocationInfo(info) }
|
private val rtkLocationInfoListener = RTKLocationInfoListener { info -> updateRtkLocationInfo(info) }
|
||||||
private val rtkSystemStateListener = RTKSystemStateListener { state -> updateRtkSystemState(state) }
|
private val rtkSystemStateListener = RTKSystemStateListener { state -> updateRtkSystemState(state) }
|
||||||
|
private val searchRtkStationListener = SearchRTKStationListener { stations ->
|
||||||
|
Log.d(
|
||||||
|
TelemetryLogTag,
|
||||||
|
"RTKStationSearch stations=${stations?.joinToString { "${it.stationName}:${it.stationId}:signal=${it.signalLevel}" }.orEmpty()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private val rtkStationConnectStatusListener = RTKStationConnectStatusListener { status ->
|
||||||
|
Log.d(TelemetryLogTag, "RTKStationConnectStatus=$status")
|
||||||
|
}
|
||||||
|
private val connectedRtkStationInfoListener = ConnectedRTKStationInfoListener { info ->
|
||||||
|
Log.d(TelemetryLogTag, "ConnectedRTKStationInfo=$info")
|
||||||
|
}
|
||||||
private val rtkNetworkServiceInfoListener = object : INetworkServiceInfoListener {
|
private val rtkNetworkServiceInfoListener = object : INetworkServiceInfoListener {
|
||||||
override fun onServiceStateUpdate(state: RTKServiceState?) {
|
override fun onServiceStateUpdate(state: RTKServiceState?) {
|
||||||
Log.d(TelemetryLogTag, "RTKNetworkServiceState=$state")
|
Log.d(TelemetryLogTag, "RTKNetworkServiceState=$state")
|
||||||
@@ -205,6 +222,8 @@ class TelemetryRepository(
|
|||||||
private var rtkEnableInProgress = false
|
private var rtkEnableInProgress = false
|
||||||
private var rtkSourceSetInProgress = false
|
private var rtkSourceSetInProgress = false
|
||||||
private var lastRtkStartAtMs = 0L
|
private var lastRtkStartAtMs = 0L
|
||||||
|
private var lastRtkStartSource = RTKReferenceStationSource.UNKNOWN
|
||||||
|
private var lastRtkBootstrapAtMs = 0L
|
||||||
private var lastRtkSystemState: RTKSystemState? = null
|
private var lastRtkSystemState: RTKSystemState? = null
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -221,20 +240,18 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
listenSafely("KeyAircraftLocation3D", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D)) { location: LocationCoordinate3D? ->
|
listenSafely("KeyAircraftLocation3D", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D)) { location: LocationCoordinate3D? ->
|
||||||
location ?: return@listenSafely
|
location ?: return@listenSafely
|
||||||
val altitude = location.altitude ?: _state.value.altitude
|
|
||||||
val latitude = location.latitude ?: 0.0
|
val latitude = location.latitude ?: 0.0
|
||||||
val longitude = location.longitude ?: 0.0
|
val longitude = location.longitude ?: 0.0
|
||||||
val valid = isValidCoordinate(latitude, longitude)
|
val valid = isValidCoordinate(latitude, longitude)
|
||||||
Log.d(
|
Log.d(
|
||||||
TelemetryLogTag,
|
TelemetryLogTag,
|
||||||
"KeyAircraftLocation3D lat=$latitude lon=$longitude altitude=$altitude valid=$valid"
|
"KeyAircraftLocation3D lat=$latitude lon=$longitude rawAltitude=${location.altitude} valid=$valid"
|
||||||
)
|
)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
latitude = latitude,
|
latitude = latitude,
|
||||||
longitude = longitude,
|
longitude = longitude,
|
||||||
locationValid = valid,
|
locationValid = valid,
|
||||||
altitude = altitude,
|
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -347,6 +364,7 @@ class TelemetryRepository(
|
|||||||
_state.update { it.copy(landingConfirmationNeeded = value, error = null) }
|
_state.update { it.copy(landingConfirmationNeeded = value, error = null) }
|
||||||
}
|
}
|
||||||
listenSafely("KeyIsSimulatorStarted", KeyTools.createKey(FlightControllerKey.KeyIsSimulatorStarted)) { started: Boolean? ->
|
listenSafely("KeyIsSimulatorStarted", KeyTools.createKey(FlightControllerKey.KeyIsSimulatorStarted)) { started: Boolean? ->
|
||||||
|
Log.d(TelemetryLogTag, "KeyIsSimulatorStarted=$started")
|
||||||
_state.update { it.copy(simulatorStarted = started == true, error = null) }
|
_state.update { it.copy(simulatorStarted = started == true, error = null) }
|
||||||
}
|
}
|
||||||
listenSafely("KeyHeightLimit", KeyTools.createKey(FlightControllerKey.KeyHeightLimit)) { limit: Int? ->
|
listenSafely("KeyHeightLimit", KeyTools.createKey(FlightControllerKey.KeyHeightLimit)) { limit: Int? ->
|
||||||
@@ -496,6 +514,9 @@ class TelemetryRepository(
|
|||||||
runCatching { rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
runCatching { rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||||
runCatching { rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
runCatching { rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||||
runCatching { rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
runCatching { rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||||
|
runCatching { rtkCenter.rtkStationManager.removeSearchRTKStationListener(searchRtkStationListener) }
|
||||||
|
runCatching { rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(rtkStationConnectStatusListener) }
|
||||||
|
runCatching { rtkCenter.rtkStationManager.removeConnectedRTKStationInfoListener(connectedRtkStationInfoListener) }
|
||||||
started = false
|
started = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,13 +600,15 @@ class TelemetryRepository(
|
|||||||
val location = info?.location
|
val location = info?.location
|
||||||
val latitude = location?.latitude ?: 0.0
|
val latitude = location?.latitude ?: 0.0
|
||||||
val longitude = location?.longitude ?: 0.0
|
val longitude = location?.longitude ?: 0.0
|
||||||
val valid = info?.isValid == true && isValidCoordinate(latitude, longitude)
|
val coordinateValid = isValidCoordinate(latitude, longitude)
|
||||||
|
val accepted = coordinateValid
|
||||||
Log.d(
|
Log.d(
|
||||||
TelemetryLogTag,
|
TelemetryLogTag,
|
||||||
"KeyRcGPSInfo valid=${info?.isValid} lat=$latitude lon=$longitude satellites=${info?.satelliteCount} accuracy=${info?.accuracy}"
|
"KeyRcGPSInfo valid=${info?.isValid} coordinateValid=$coordinateValid accepted=$accepted " +
|
||||||
|
"lat=$latitude lon=$longitude satellites=${info?.satelliteCount} accuracy=${info?.accuracy}"
|
||||||
)
|
)
|
||||||
_state.update {
|
_state.update {
|
||||||
if (valid) {
|
if (accepted) {
|
||||||
it.copy(
|
it.copy(
|
||||||
rcLatitude = latitude,
|
rcLatitude = latitude,
|
||||||
rcLongitude = longitude,
|
rcLongitude = longitude,
|
||||||
@@ -646,7 +669,33 @@ class TelemetryRepository(
|
|||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.w(TelemetryLogTag, "register RTK network service listeners failed: ${error.message}")
|
Log.w(TelemetryLogTag, "register RTK network service listeners failed: ${error.message}")
|
||||||
}
|
}
|
||||||
|
listenRtkStationTelemetry()
|
||||||
listenRtkMobileStationKeys()
|
listenRtkMobileStationKeys()
|
||||||
|
bootstrapAllPositioning("telemetry_start")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listenRtkStationTelemetry() {
|
||||||
|
runCatching {
|
||||||
|
rtkCenter.rtkStationManager.addSearchRTKStationListener(searchRtkStationListener)
|
||||||
|
rtkCenter.rtkStationManager.addRTKStationConnectStatusListener(rtkStationConnectStatusListener)
|
||||||
|
rtkCenter.rtkStationManager.addConnectedRTKStationInfoListener(connectedRtkStationInfoListener)
|
||||||
|
Log.d(TelemetryLogTag, "registered RTK station listeners")
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(TelemetryLogTag, "register RTK station listeners failed: ${error.message}")
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
rtkCenter.rtkStationManager.startSearchRTKStation(object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
Log.d(TelemetryLogTag, "start RTK station search success")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(TelemetryLogTag, "start RTK station search failed: $error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(TelemetryLogTag, "start RTK station search exception: ${error.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenRtkMobileStationKeys() {
|
private fun listenRtkMobileStationKeys() {
|
||||||
@@ -784,7 +833,10 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureRtkServiceStarted(state: RTKSystemState?) {
|
private fun ensureRtkServiceStarted(state: RTKSystemState?) {
|
||||||
if (state == null) return
|
if (state == null) {
|
||||||
|
bootstrapAllPositioning("rtk_state_null")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (state.rtkHealthy) {
|
if (state.rtkHealthy) {
|
||||||
rtkStartInProgress = false
|
rtkStartInProgress = false
|
||||||
rtkServiceStarted = true
|
rtkServiceStarted = true
|
||||||
@@ -792,21 +844,48 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
if (!state.isRTKEnabled) {
|
if (!state.isRTKEnabled) {
|
||||||
enableRtkModuleIfPossible()
|
enableRtkModuleIfPossible()
|
||||||
|
bootstrapAllPositioning("rtk_disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val source = state.rtkReferenceStationSource ?: RTKReferenceStationSource.UNKNOWN
|
val source = state.rtkReferenceStationSource ?: RTKReferenceStationSource.UNKNOWN
|
||||||
if (!source.isNetworkRtkSource()) {
|
if (source == RTKReferenceStationSource.UNKNOWN) {
|
||||||
setPreferredNetworkRtkSource(source)
|
setPreferredNetworkRtkSource(source)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!source.isNetworkRtkSource()) {
|
||||||
|
Log.d(TelemetryLogTag, "RTK source is $source, station mode active")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (rtkServiceStarted) return
|
if (rtkServiceStarted) return
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (rtkStartInProgress || now - lastRtkStartAtMs < RTK_START_RETRY_INTERVAL_MS) return
|
if (rtkStartInProgress || now - lastRtkStartAtMs < RTK_START_RETRY_INTERVAL_MS) return
|
||||||
startNetworkRtkService(source)
|
startNetworkRtkService(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bootstrapAllPositioning(reason: String) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastRtkBootstrapAtMs < RTK_BOOTSTRAP_RETRY_INTERVAL_MS) return
|
||||||
|
lastRtkBootstrapAtMs = now
|
||||||
|
Log.d(TelemetryLogTag, "bootstrap positioning reason=$reason")
|
||||||
|
enableRtkModuleIfPossible()
|
||||||
|
runCatching {
|
||||||
|
rtkCenter.setRTKMaintainAccuracyEnabled(true, object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
Log.d(TelemetryLogTag, "enable RTK maintain accuracy success")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(TelemetryLogTag, "enable RTK maintain accuracy failed: $error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(TelemetryLogTag, "enable RTK maintain accuracy exception: ${error.message}")
|
||||||
|
}
|
||||||
|
setPreferredNetworkRtkSource(lastRtkSystemState?.rtkReferenceStationSource ?: RTKReferenceStationSource.UNKNOWN)
|
||||||
|
}
|
||||||
|
|
||||||
private fun enableRtkModuleIfPossible() {
|
private fun enableRtkModuleIfPossible() {
|
||||||
if (rtkEnableInProgress || _state.value.motorsOn) return
|
if (rtkEnableInProgress) return
|
||||||
rtkEnableInProgress = true
|
rtkEnableInProgress = true
|
||||||
Log.d(TelemetryLogTag, "enable RTK module")
|
Log.d(TelemetryLogTag, "enable RTK module")
|
||||||
rtkCenter.setAircraftRTKModuleEnabled(true, object : CommonCallbacks.CompletionCallback {
|
rtkCenter.setAircraftRTKModuleEnabled(true, object : CommonCallbacks.CompletionCallback {
|
||||||
@@ -814,6 +893,7 @@ class TelemetryRepository(
|
|||||||
rtkEnableInProgress = false
|
rtkEnableInProgress = false
|
||||||
Log.d(TelemetryLogTag, "enable RTK module success")
|
Log.d(TelemetryLogTag, "enable RTK module success")
|
||||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||||
|
setPreferredNetworkRtkSource(lastRtkSystemState?.rtkReferenceStationSource ?: RTKReferenceStationSource.UNKNOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(error: IDJIError) {
|
override fun onFailure(error: IDJIError) {
|
||||||
@@ -824,7 +904,11 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setPreferredNetworkRtkSource(currentSource: RTKReferenceStationSource) {
|
private fun setPreferredNetworkRtkSource(currentSource: RTKReferenceStationSource) {
|
||||||
if (rtkSourceSetInProgress || rtkStartInProgress) return
|
if (rtkSourceSetInProgress) return
|
||||||
|
if (currentSource == DefaultNetworkRtkSource) {
|
||||||
|
startNetworkRtkService(DefaultNetworkRtkSource)
|
||||||
|
return
|
||||||
|
}
|
||||||
rtkSourceSetInProgress = true
|
rtkSourceSetInProgress = true
|
||||||
Log.d(
|
Log.d(
|
||||||
TelemetryLogTag,
|
TelemetryLogTag,
|
||||||
@@ -850,21 +934,54 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startNetworkRtkService(source: RTKReferenceStationSource) {
|
private fun startNetworkRtkService(source: RTKReferenceStationSource) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (now - lastRtkStartAtMs < 1_000L && source == lastRtkStartSource) {
|
||||||
|
Log.d(TelemetryLogTag, "skip duplicate RTK network service source=$source")
|
||||||
|
return
|
||||||
|
}
|
||||||
rtkStartInProgress = true
|
rtkStartInProgress = true
|
||||||
rtkServiceStarted = false
|
rtkServiceStarted = false
|
||||||
lastRtkStartAtMs = System.currentTimeMillis()
|
lastRtkStartAtMs = now
|
||||||
|
lastRtkStartSource = source
|
||||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||||
Log.d(TelemetryLogTag, "start RTK network service source=$source coordinate=$DefaultNetworkRtkCoordinateSystem")
|
Log.d(TelemetryLogTag, "start RTK network service source=$source coordinate=$DefaultNetworkRtkCoordinateSystem")
|
||||||
when (source) {
|
when (source) {
|
||||||
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
|
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
|
||||||
|
rtkCenter.qxrtkManager.stopNetworkRTKService(object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
rtkCenter.qxrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("QX"))
|
rtkCenter.qxrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("QX"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(TelemetryLogTag, "stop QX RTK service before restart failed: $error")
|
||||||
|
rtkCenter.qxrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("QX"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
|
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
|
||||||
|
rtkCenter.cmccrtkManager.stopNetworkRTKService(object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
rtkCenter.cmccrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("NTRIP"))
|
rtkCenter.cmccrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("NTRIP"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(TelemetryLogTag, "stop NTRIP RTK service before restart failed: $error")
|
||||||
|
rtkCenter.cmccrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("NTRIP"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> {
|
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> {
|
||||||
|
rtkCenter.customRTKManager.stopNetworkRTKService(object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
rtkCenter.customRTKManager.startNetworkRTKService(rtkStartCallback("CUSTOM"))
|
rtkCenter.customRTKManager.startNetworkRTKService(rtkStartCallback("CUSTOM"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
Log.w(TelemetryLogTag, "stop CUSTOM RTK service before restart failed: $error")
|
||||||
|
rtkCenter.customRTKManager.startNetworkRTKService(rtkStartCallback("CUSTOM"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
rtkStartInProgress = false
|
rtkStartInProgress = false
|
||||||
}
|
}
|
||||||
@@ -890,6 +1007,7 @@ class TelemetryRepository(
|
|||||||
private fun startAndroidLocationFallback() {
|
private fun startAndroidLocationFallback() {
|
||||||
val manager = locationManager ?: return
|
val manager = locationManager ?: return
|
||||||
val context = appContext ?: return
|
val context = appContext ?: return
|
||||||
|
Log.d(TelemetryLogTag, "start RC Android location fallback")
|
||||||
if (
|
if (
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|
||||||
@@ -898,7 +1016,20 @@ class TelemetryRepository(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
runCatching {
|
||||||
|
LocationUtil.getLastLocation()?.let { location ->
|
||||||
|
Log.d(
|
||||||
|
TelemetryLogTag,
|
||||||
|
"LocationUtilLastLocation provider=${location.provider} lat=${location.latitude} " +
|
||||||
|
"lon=${location.longitude} accuracy=${location.accuracy}"
|
||||||
|
)
|
||||||
|
updateRcLocationFromAndroid(location)
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
Log.w(TelemetryLogTag, "read DJI LocationUtil last location failed: ${error.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val providers = (
|
||||||
listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, LocationManager.PASSIVE_PROVIDER) +
|
listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, LocationManager.PASSIVE_PROVIDER) +
|
||||||
manager.allProviders
|
manager.allProviders
|
||||||
)
|
)
|
||||||
@@ -907,7 +1038,11 @@ class TelemetryRepository(
|
|||||||
provider == LocationManager.PASSIVE_PROVIDER ||
|
provider == LocationManager.PASSIVE_PROVIDER ||
|
||||||
runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)
|
runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)
|
||||||
}
|
}
|
||||||
.forEach { provider ->
|
Log.d(TelemetryLogTag, "RC Android location providers=$providers all=${manager.allProviders}")
|
||||||
|
if (providers.isEmpty()) {
|
||||||
|
Log.w(TelemetryLogTag, "no enabled RC Android location providers")
|
||||||
|
}
|
||||||
|
providers.forEach { provider ->
|
||||||
runCatching {
|
runCatching {
|
||||||
manager.getLastKnownLocation(provider)?.let(::updateRcLocationFromAndroid)
|
manager.getLastKnownLocation(provider)?.let(::updateRcLocationFromAndroid)
|
||||||
manager.requestLocationUpdates(
|
manager.requestLocationUpdates(
|
||||||
@@ -970,7 +1105,7 @@ private fun isValidCoordinate(latitude: Double?, longitude: Double?): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val DefaultNetworkRtkSource = RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
|
private val DefaultNetworkRtkSource = RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
|
||||||
private val DefaultNetworkRtkCoordinateSystem = CoordinateSystem.CGCS2000
|
private val DefaultNetworkRtkCoordinateSystem = CoordinateSystem.WGS84
|
||||||
|
|
||||||
private fun RTKReferenceStationSource.isNetworkRtkSource(): Boolean =
|
private fun RTKReferenceStationSource.isNetworkRtkSource(): Boolean =
|
||||||
this == RTKReferenceStationSource.QX_NETWORK_SERVICE ||
|
this == RTKReferenceStationSource.QX_NETWORK_SERVICE ||
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.zklh.dronecontroller.ui
|
package com.zklh.dronecontroller.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -148,6 +149,7 @@ import androidx.compose.ui.zIndex
|
|||||||
import com.zklh.dronecontroller.core.cloud.CloudCommandExecutor
|
import com.zklh.dronecontroller.core.cloud.CloudCommandExecutor
|
||||||
import com.zklh.dronecontroller.core.cloud.CloudLoginClient
|
import com.zklh.dronecontroller.core.cloud.CloudLoginClient
|
||||||
import com.zklh.dronecontroller.core.cloud.CloudLoginRequest
|
import com.zklh.dronecontroller.core.cloud.CloudLoginRequest
|
||||||
|
import com.zklh.dronecontroller.core.cloud.CloudMediaUploadService
|
||||||
import com.zklh.dronecontroller.core.cloud.CloudMqttService
|
import com.zklh.dronecontroller.core.cloud.CloudMqttService
|
||||||
import com.zklh.dronecontroller.core.cloud.CloudMqttState
|
import com.zklh.dronecontroller.core.cloud.CloudMqttState
|
||||||
import com.zklh.dronecontroller.core.flight.FlightControlService
|
import com.zklh.dronecontroller.core.flight.FlightControlService
|
||||||
@@ -201,6 +203,19 @@ private const val DefaultVirtualStickSpeedLevel = 1.0
|
|||||||
private const val CameraLogTag = "ZklhCamera"
|
private const val CameraLogTag = "ZklhCamera"
|
||||||
private const val CloudLogTag = "ZklhCloud"
|
private const val CloudLogTag = "ZklhCloud"
|
||||||
private const val FlightControlLogTag = "ZklhFlightControl"
|
private const val FlightControlLogTag = "ZklhFlightControl"
|
||||||
|
private const val DefaultThirdCloudUrl = "http://uav.zklhjs.com/p"
|
||||||
|
private const val DefaultThirdCloudTenantName = "中科联航"
|
||||||
|
private const val DefaultThirdCloudUsername = "zklh"
|
||||||
|
private const val DefaultThirdCloudPassword = "zklh@123"
|
||||||
|
private const val SimulatorDefaultLatitude = 31.230402481870684
|
||||||
|
private const val SimulatorDefaultLongitude = 121.47369280461436
|
||||||
|
private const val CloudPrefsName = "zklh_third_cloud"
|
||||||
|
private const val CloudPrefsAutoConnect = "auto_connect"
|
||||||
|
private const val CloudPrefsUrl = "url"
|
||||||
|
private const val CloudPrefsTenantName = "tenant_name"
|
||||||
|
private const val CloudPrefsUsername = "username"
|
||||||
|
private const val CloudPrefsPassword = "password"
|
||||||
|
private const val CloudPrefsMqttAddress = "mqtt_address"
|
||||||
|
|
||||||
private enum class PilotPage {
|
private enum class PilotPage {
|
||||||
Home,
|
Home,
|
||||||
@@ -222,10 +237,78 @@ private enum class CameraQuickAction {
|
|||||||
Panorama
|
Panorama
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class SavedCloudLoginConfig(
|
||||||
|
val url: String = DefaultThirdCloudUrl,
|
||||||
|
val tenantName: String = DefaultThirdCloudTenantName,
|
||||||
|
val username: String = DefaultThirdCloudUsername,
|
||||||
|
val password: String = DefaultThirdCloudPassword,
|
||||||
|
val mqttAddress: String = "",
|
||||||
|
val autoConnect: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Context.readSavedCloudLoginConfig(): SavedCloudLoginConfig {
|
||||||
|
val prefs = getSharedPreferences(CloudPrefsName, Context.MODE_PRIVATE)
|
||||||
|
return SavedCloudLoginConfig(
|
||||||
|
url = prefs.getString(CloudPrefsUrl, DefaultThirdCloudUrl).orEmpty().ifBlank { DefaultThirdCloudUrl },
|
||||||
|
tenantName = prefs.getString(CloudPrefsTenantName, DefaultThirdCloudTenantName).orEmpty()
|
||||||
|
.ifBlank { DefaultThirdCloudTenantName },
|
||||||
|
username = prefs.getString(CloudPrefsUsername, DefaultThirdCloudUsername).orEmpty().ifBlank { DefaultThirdCloudUsername },
|
||||||
|
password = prefs.getString(CloudPrefsPassword, DefaultThirdCloudPassword).orEmpty().ifBlank { DefaultThirdCloudPassword },
|
||||||
|
mqttAddress = prefs.getString(CloudPrefsMqttAddress, "").orEmpty(),
|
||||||
|
autoConnect = prefs.getBoolean(CloudPrefsAutoConnect, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.saveCloudLoginConfig(request: CloudLoginRequest, mqttAddress: String) {
|
||||||
|
getSharedPreferences(CloudPrefsName, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(CloudPrefsAutoConnect, true)
|
||||||
|
.putString(CloudPrefsUrl, request.baseUrl)
|
||||||
|
.putString(CloudPrefsTenantName, request.tenantName)
|
||||||
|
.putString(CloudPrefsUsername, request.username)
|
||||||
|
.putString(CloudPrefsPassword, request.password)
|
||||||
|
.putString(CloudPrefsMqttAddress, mqttAddress)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.disableCloudAutoConnect() {
|
||||||
|
getSharedPreferences(CloudPrefsName, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(CloudPrefsAutoConnect, false)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SimulatorStartCoordinate(
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val source: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun simulatorStartCoordinate(telemetry: TelemetrySnapshot): SimulatorStartCoordinate =
|
||||||
|
when {
|
||||||
|
isUsableCoordinate(telemetry.latitude, telemetry.longitude) ->
|
||||||
|
SimulatorStartCoordinate(telemetry.latitude, telemetry.longitude, "飞机坐标")
|
||||||
|
isUsableCoordinate(telemetry.homeLatitude, telemetry.homeLongitude) ->
|
||||||
|
SimulatorStartCoordinate(telemetry.homeLatitude, telemetry.homeLongitude, "返航点坐标")
|
||||||
|
isUsableCoordinate(telemetry.rcLatitude, telemetry.rcLongitude) ->
|
||||||
|
SimulatorStartCoordinate(telemetry.rcLatitude, telemetry.rcLongitude, "遥控器坐标")
|
||||||
|
else ->
|
||||||
|
SimulatorStartCoordinate(SimulatorDefaultLatitude, SimulatorDefaultLongitude, "默认坐标")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isUsableCoordinate(latitude: Double, longitude: Double): Boolean =
|
||||||
|
!latitude.isNaN() &&
|
||||||
|
!longitude.isNaN() &&
|
||||||
|
(latitude != 0.0 || longitude != 0.0) &&
|
||||||
|
latitude in -90.0..90.0 &&
|
||||||
|
longitude in -180.0..180.0
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DroneControllerScreen() {
|
fun DroneControllerScreen() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val appContext = context.applicationContext
|
||||||
|
val savedCloudLoginConfig = remember(appContext) { appContext.readSavedCloudLoginConfig() }
|
||||||
val sdkState by DroneSdkManager.state.collectAsState()
|
val sdkState by DroneSdkManager.state.collectAsState()
|
||||||
val flightControl = remember { FlightControlService() }
|
val flightControl = remember { FlightControlService() }
|
||||||
val virtualStick = remember { VirtualStickService() }
|
val virtualStick = remember { VirtualStickService() }
|
||||||
@@ -238,13 +321,26 @@ fun DroneControllerScreen() {
|
|||||||
val warningRepository = remember { DroneWarningRepository() }
|
val warningRepository = remember { DroneWarningRepository() }
|
||||||
val cloudLoginClient = remember { CloudLoginClient() }
|
val cloudLoginClient = remember { CloudLoginClient() }
|
||||||
val cloudMqttService = remember { CloudMqttService(cloudLoginClient) }
|
val cloudMqttService = remember { CloudMqttService(cloudLoginClient) }
|
||||||
val cloudCommandExecutor = remember(flightControl, cameraMedia, flyToService, liveStreaming, virtualStick, waypointMission) {
|
val cloudMediaUpload = remember(appContext, cloudMqttService) {
|
||||||
CloudCommandExecutor(flightControl, cameraMedia, flyToService, liveStreaming, virtualStick, waypointMission)
|
CloudMediaUploadService(appContext) { cloudMqttService.state.value.session }
|
||||||
|
}
|
||||||
|
val telemetry by telemetryRepository.state.collectAsState()
|
||||||
|
val latestTelemetry by rememberUpdatedState(telemetry)
|
||||||
|
val cloudCommandExecutor = remember(flightControl, cameraMedia, flyToService, liveStreaming, virtualStick, waypointMission, cloudMediaUpload) {
|
||||||
|
CloudCommandExecutor(
|
||||||
|
flightControl = flightControl,
|
||||||
|
cameraMedia = cameraMedia,
|
||||||
|
flyToService = flyToService,
|
||||||
|
liveStreaming = liveStreaming,
|
||||||
|
virtualStick = virtualStick,
|
||||||
|
waypointMission = waypointMission,
|
||||||
|
mediaUpload = cloudMediaUpload,
|
||||||
|
telemetryProvider = { latestTelemetry }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val stickStatus by virtualStick.status.collectAsState()
|
val stickStatus by virtualStick.status.collectAsState()
|
||||||
val simulatorStatus by simulator.status.collectAsState()
|
val simulatorStatus by simulator.status.collectAsState()
|
||||||
val warningState by warningRepository.state.collectAsState()
|
val warningState by warningRepository.state.collectAsState()
|
||||||
val telemetry by telemetryRepository.state.collectAsState()
|
|
||||||
val cloudState by cloudMqttService.state.collectAsState()
|
val cloudState by cloudMqttService.state.collectAsState()
|
||||||
val liveState by liveStreaming.state.collectAsState()
|
val liveState by liveStreaming.state.collectAsState()
|
||||||
|
|
||||||
@@ -260,16 +356,17 @@ fun DroneControllerScreen() {
|
|||||||
var returnAltitude by remember { mutableStateOf(100) }
|
var returnAltitude by remember { mutableStateOf(100) }
|
||||||
var maxAltitude by remember { mutableStateOf(500) }
|
var maxAltitude by remember { mutableStateOf(500) }
|
||||||
var farLimitEnabled by remember { mutableStateOf(false) }
|
var farLimitEnabled by remember { mutableStateOf(false) }
|
||||||
var thirdCloudUrl by remember { mutableStateOf("http://uav.zklhjs.com/p") }
|
var thirdCloudUrl by remember { mutableStateOf(savedCloudLoginConfig.url) }
|
||||||
var thirdCloudTenantName by remember { mutableStateOf("中科联航") }
|
var thirdCloudTenantName by remember { mutableStateOf(savedCloudLoginConfig.tenantName) }
|
||||||
var thirdCloudUsername by remember { mutableStateOf("zklh") }
|
var thirdCloudUsername by remember { mutableStateOf(savedCloudLoginConfig.username) }
|
||||||
var thirdCloudPassword by remember { mutableStateOf("zklh@123") }
|
var thirdCloudPassword by remember { mutableStateOf(savedCloudLoginConfig.password) }
|
||||||
var thirdCloudMqttAddress by remember { mutableStateOf("") }
|
var thirdCloudMqttAddress by remember { mutableStateOf(savedCloudLoginConfig.mqttAddress) }
|
||||||
var thirdCloudConnected by remember { mutableStateOf(false) }
|
var thirdCloudConnected by remember { mutableStateOf(false) }
|
||||||
var thirdCloudLoginVisible by remember { mutableStateOf(false) }
|
var thirdCloudLoginVisible by remember { mutableStateOf(false) }
|
||||||
var thirdCloudStatusMessage by remember { mutableStateOf("") }
|
var thirdCloudStatusMessage by remember { mutableStateOf("") }
|
||||||
var thirdCloudStatusIsError by remember { mutableStateOf(false) }
|
var thirdCloudStatusIsError by remember { mutableStateOf(false) }
|
||||||
var thirdCloudReloadTick by remember { mutableStateOf(0) }
|
var thirdCloudReloadTick by remember { mutableStateOf(0) }
|
||||||
|
var thirdCloudAutoLoginStarted by remember { mutableStateOf(false) }
|
||||||
var cameraRecording by remember { mutableStateOf(false) }
|
var cameraRecording by remember { mutableStateOf(false) }
|
||||||
var laserFillLightEnabled by remember { mutableStateOf(false) }
|
var laserFillLightEnabled by remember { mutableStateOf(false) }
|
||||||
var landingConfirmationAutoSent by remember { mutableStateOf(false) }
|
var landingConfirmationAutoSent by remember { mutableStateOf(false) }
|
||||||
@@ -341,6 +438,15 @@ fun DroneControllerScreen() {
|
|||||||
cloudMqttService.updateTelemetry(telemetry)
|
cloudMqttService.updateTelemetry(telemetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
warningState,
|
||||||
|
cloudState.connected,
|
||||||
|
sdkState.remoteControllerSerialNumber,
|
||||||
|
sdkState.aircraftSerialNumber
|
||||||
|
) {
|
||||||
|
cloudMqttService.updateWarnings(warningState)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(cloudState.connected) {
|
LaunchedEffect(cloudState.connected) {
|
||||||
thirdCloudConnected = cloudState.connected
|
thirdCloudConnected = cloudState.connected
|
||||||
}
|
}
|
||||||
@@ -576,7 +682,17 @@ fun DroneControllerScreen() {
|
|||||||
Log.e(CameraLogTag, "takePhoto exception", error)
|
Log.e(CameraLogTag, "takePhoto exception", error)
|
||||||
DjiCommandResult.failed("拍照异常:${error.message ?: error.javaClass.simpleName}")
|
DjiCommandResult.failed("拍照异常:${error.message ?: error.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
commandMessage = result.message
|
val finalResult = if (result.success) {
|
||||||
|
val uploadResult = cloudMediaUpload.uploadLatestMedia(cameraMedia.currentCameraIndex())
|
||||||
|
if (uploadResult.success) {
|
||||||
|
DjiCommandResult.ok("${result.message};${uploadResult.message}")
|
||||||
|
} else {
|
||||||
|
DjiCommandResult.failed("${result.message};${uploadResult.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
commandMessage = finalResult.message
|
||||||
Log.d(CameraLogTag, "takePhoto success=${result.success}, message=${result.message}")
|
Log.d(CameraLogTag, "takePhoto success=${result.success}, message=${result.message}")
|
||||||
laserFillLightEnabled = cameraMedia.isLaserFillLightEnabled()
|
laserFillLightEnabled = cameraMedia.isLaserFillLightEnabled()
|
||||||
toast(commandMessage)
|
toast(commandMessage)
|
||||||
@@ -592,7 +708,17 @@ fun DroneControllerScreen() {
|
|||||||
Log.e(CameraLogTag, "toggleRecord exception", error)
|
Log.e(CameraLogTag, "toggleRecord exception", error)
|
||||||
DjiCommandResult.failed("录像异常:${error.message ?: error.javaClass.simpleName}")
|
DjiCommandResult.failed("录像异常:${error.message ?: error.javaClass.simpleName}")
|
||||||
}
|
}
|
||||||
commandMessage = result.message
|
val finalResult = if (result.success && shouldStop) {
|
||||||
|
val uploadResult = cloudMediaUpload.uploadLatestMedia(cameraMedia.currentCameraIndex())
|
||||||
|
if (uploadResult.success) {
|
||||||
|
DjiCommandResult.ok("${result.message};${uploadResult.message}")
|
||||||
|
} else {
|
||||||
|
DjiCommandResult.failed("${result.message};${uploadResult.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
commandMessage = finalResult.message
|
||||||
Log.d(CameraLogTag, "toggleRecord shouldStop=$shouldStop, success=${result.success}, message=${result.message}")
|
Log.d(CameraLogTag, "toggleRecord shouldStop=$shouldStop, success=${result.success}, message=${result.message}")
|
||||||
cameraRecording = if (result.success) !shouldStop else cameraMedia.isRecording()
|
cameraRecording = if (result.success) !shouldStop else cameraMedia.isRecording()
|
||||||
laserFillLightEnabled = cameraMedia.isLaserFillLightEnabled()
|
laserFillLightEnabled = cameraMedia.isLaserFillLightEnabled()
|
||||||
@@ -635,9 +761,13 @@ fun DroneControllerScreen() {
|
|||||||
}
|
}
|
||||||
val enableSimulator: () -> Unit = {
|
val enableSimulator: () -> Unit = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val latitude = telemetry.latitude.takeIf { it != 0.0 } ?: 31.2304
|
val start = simulatorStartCoordinate(telemetry)
|
||||||
val longitude = telemetry.longitude.takeIf { it != 0.0 } ?: 121.4737
|
val result = simulator.enable(start.latitude, start.longitude)
|
||||||
commandMessage = simulator.enable(latitude, longitude).message
|
commandMessage = if (start.source == "默认坐标") {
|
||||||
|
"${result.message}(未获取到定位,使用默认坐标)"
|
||||||
|
} else {
|
||||||
|
"${result.message}(${start.source})"
|
||||||
|
}
|
||||||
toast(commandMessage)
|
toast(commandMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,7 +784,7 @@ fun DroneControllerScreen() {
|
|||||||
toast(commandMessage)
|
toast(commandMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val loginThirdCloud: () -> Unit = {
|
fun loginThirdCloud(showToast: Boolean = true) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val request = CloudLoginRequest(
|
val request = CloudLoginRequest(
|
||||||
baseUrl = thirdCloudUrl,
|
baseUrl = thirdCloudUrl,
|
||||||
@@ -667,28 +797,43 @@ fun DroneControllerScreen() {
|
|||||||
commandMessage = "正在登录第三方云"
|
commandMessage = "正在登录第三方云"
|
||||||
thirdCloudStatusMessage = commandMessage
|
thirdCloudStatusMessage = commandMessage
|
||||||
thirdCloudStatusIsError = false
|
thirdCloudStatusIsError = false
|
||||||
toast(commandMessage)
|
if (showToast) toast(commandMessage)
|
||||||
cloudLoginClient.login(request)
|
cloudLoginClient.login(request)
|
||||||
.onSuccess { session ->
|
.onSuccess { session ->
|
||||||
thirdCloudUrl = session.baseUrl
|
thirdCloudUrl = session.baseUrl
|
||||||
if (thirdCloudMqttAddress.isBlank()) {
|
if (thirdCloudMqttAddress.isBlank()) {
|
||||||
thirdCloudMqttAddress = session.mqttAddress
|
thirdCloudMqttAddress = session.mqttAddress
|
||||||
}
|
}
|
||||||
|
appContext.saveCloudLoginConfig(request.copy(baseUrl = session.baseUrl), session.mqttAddress)
|
||||||
commandMessage = "第三方云登录成功,正在连接 MQTT"
|
commandMessage = "第三方云登录成功,正在连接 MQTT"
|
||||||
thirdCloudStatusMessage = commandMessage
|
thirdCloudStatusMessage = commandMessage
|
||||||
thirdCloudStatusIsError = false
|
thirdCloudStatusIsError = false
|
||||||
toast(commandMessage)
|
if (showToast) toast(commandMessage)
|
||||||
cloudMqttService.connect(session, cloudCommandExecutor)
|
cloudMqttService.connect(session, cloudCommandExecutor)
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
commandMessage = "第三方云登录失败:${error.message ?: error.javaClass.simpleName}"
|
commandMessage = "第三方云登录失败:${error.message ?: error.javaClass.simpleName}"
|
||||||
thirdCloudStatusMessage = commandMessage
|
thirdCloudStatusMessage = commandMessage
|
||||||
thirdCloudStatusIsError = true
|
thirdCloudStatusIsError = true
|
||||||
toast(commandMessage)
|
if (showToast) toast(commandMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val loginThirdCloudFromUi: () -> Unit = {
|
||||||
|
loginThirdCloud(showToast = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(savedCloudLoginConfig.autoConnect) {
|
||||||
|
if (savedCloudLoginConfig.autoConnect && !thirdCloudAutoLoginStarted) {
|
||||||
|
thirdCloudAutoLoginStarted = true
|
||||||
|
thirdCloudStatusMessage = "正在自动连接第三方云"
|
||||||
|
thirdCloudStatusIsError = false
|
||||||
|
delay(600)
|
||||||
|
loginThirdCloud(showToast = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (page) {
|
when (page) {
|
||||||
PilotPage.Home -> HomeScreen(
|
PilotPage.Home -> HomeScreen(
|
||||||
sdkState = sdkState,
|
sdkState = sdkState,
|
||||||
@@ -751,7 +896,7 @@ fun DroneControllerScreen() {
|
|||||||
onCloseWeb = {
|
onCloseWeb = {
|
||||||
thirdCloudLoginVisible = false
|
thirdCloudLoginVisible = false
|
||||||
},
|
},
|
||||||
onLogin = loginThirdCloud,
|
onLogin = loginThirdCloudFromUi,
|
||||||
onConnected = {
|
onConnected = {
|
||||||
if (!thirdCloudConnected) {
|
if (!thirdCloudConnected) {
|
||||||
toast("网页登录成功,请确认 MQTT 接入状态")
|
toast("网页登录成功,请确认 MQTT 接入状态")
|
||||||
@@ -765,11 +910,13 @@ fun DroneControllerScreen() {
|
|||||||
} else {
|
} else {
|
||||||
cloudMqttService.updateDeviceState(sdkState)
|
cloudMqttService.updateDeviceState(sdkState)
|
||||||
cloudMqttService.updateTelemetry(telemetry)
|
cloudMqttService.updateTelemetry(telemetry)
|
||||||
|
cloudMqttService.updateWarnings(warningState)
|
||||||
toast("已刷新第三方云接入状态")
|
toast("已刷新第三方云接入状态")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
cloudMqttService.disconnect()
|
cloudMqttService.disconnect()
|
||||||
|
appContext.disableCloudAutoConnect()
|
||||||
CookieManager.getInstance().removeAllCookies(null)
|
CookieManager.getInstance().removeAllCookies(null)
|
||||||
CookieManager.getInstance().flush()
|
CookieManager.getInstance().flush()
|
||||||
WebStorage.getInstance().deleteAllData()
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
|||||||
Reference in New Issue
Block a user