update
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,14 @@ import android.util.Log
|
|||||||
import com.zklh.dronecontroller.core.diagnostics.DroneWarningItem
|
import com.zklh.dronecontroller.core.diagnostics.DroneWarningItem
|
||||||
import com.zklh.dronecontroller.core.diagnostics.DroneWarningState
|
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.TelemetryCameraIdentity
|
||||||
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||||
import dji.v5.manager.diagnostic.WarningLevel
|
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
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -35,13 +37,19 @@ import org.json.JSONObject
|
|||||||
private const val CloudMqttLogTag = "ZklhCloudMqtt"
|
private const val CloudMqttLogTag = "ZklhCloudMqtt"
|
||||||
private const val RcDomain = 2
|
private const val RcDomain = 2
|
||||||
private const val DroneDomain = 0
|
private const val DroneDomain = 0
|
||||||
private const val RcPlus2Type = 174
|
|
||||||
private const val Matrice4Type = 99
|
|
||||||
private const val RcSubType = 0
|
private const val RcSubType = 0
|
||||||
private const val Matrice4SubType = 1
|
private const val CloudRcType = 56
|
||||||
private const val Matrice4DeviceType = "0-99-1"
|
private const val CloudRcPlusType = 119
|
||||||
private const val Matrice4CameraPayloadIndex = "89-0-0"
|
private const val CloudRcProType = 144
|
||||||
private const val Matrice4LiveVideoIndex = "normal-0"
|
private const val CloudRcPlus2Type = 174
|
||||||
|
private val KnownCloudRcTypes = setOf(CloudRcType, CloudRcPlusType, CloudRcProType, CloudRcPlus2Type)
|
||||||
|
private const val CloudMatrice4Type = 99
|
||||||
|
private const val CloudMatrice4DockType = 100
|
||||||
|
private const val CloudMatrice4ThermalPayloadType = 89
|
||||||
|
private const val CloudMatrice4DockThermalPayloadType = 99
|
||||||
|
private const val MsdkMatrice4SeriesProductType = 150
|
||||||
|
private const val MsdkMatrice4ThermalCameraType = 89
|
||||||
|
private const val DefaultLiveVideoIndex = "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
|
||||||
@@ -61,9 +69,18 @@ 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 EarthRadiusMeters = 6_371_000.0
|
private const val EarthRadiusMeters = 6_371_000.0
|
||||||
|
|
||||||
|
private data class CloudDeviceDescriptor(
|
||||||
|
val domain: Int,
|
||||||
|
val type: Int,
|
||||||
|
val subType: Int
|
||||||
|
) {
|
||||||
|
val deviceType: String
|
||||||
|
get() = "$domain-$type-$subType"
|
||||||
|
}
|
||||||
|
|
||||||
class CloudMqttService(
|
class CloudMqttService(
|
||||||
private val loginClient: CloudLoginClient = CloudLoginClient()
|
private val loginClient: CloudLoginClient = CloudLoginClient()
|
||||||
) {
|
) : CloudMissionProgressPublisher {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
private val _state = MutableStateFlow(CloudMqttState())
|
private val _state = MutableStateFlow(CloudMqttState())
|
||||||
val state: StateFlow<CloudMqttState> = _state.asStateFlow()
|
val state: StateFlow<CloudMqttState> = _state.asStateFlow()
|
||||||
@@ -369,24 +386,26 @@ class CloudMqttService(
|
|||||||
if (rcSn.isBlank()) return
|
if (rcSn.isBlank()) return
|
||||||
val subDevices = JSONArray()
|
val subDevices = JSONArray()
|
||||||
if (online && identity.aircraftSn.isNotBlank()) {
|
if (online && identity.aircraftSn.isNotBlank()) {
|
||||||
|
val droneDescriptor = telemetry.cloudDroneDescriptor()
|
||||||
subDevices.put(
|
subDevices.put(
|
||||||
JSONObject()
|
JSONObject()
|
||||||
.put("sn", identity.aircraftSn)
|
.put("sn", identity.aircraftSn)
|
||||||
.put("domain", DroneDomain)
|
.put("domain", droneDescriptor.domain)
|
||||||
.put("type", Matrice4Type)
|
.put("type", droneDescriptor.type)
|
||||||
.put("sub_type", Matrice4SubType)
|
.put("sub_type", droneDescriptor.subType)
|
||||||
.put("index", "A")
|
.put("index", "A")
|
||||||
.put("thing_version", ThingVersion)
|
.put("thing_version", ThingVersion)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val rcDescriptor = telemetry.cloudRcDescriptor()
|
||||||
val payload = topicRequest(cloudSession)
|
val payload = topicRequest(cloudSession)
|
||||||
.put("method", "update_topo")
|
.put("method", "update_topo")
|
||||||
.put(
|
.put(
|
||||||
"data",
|
"data",
|
||||||
JSONObject()
|
JSONObject()
|
||||||
.put("domain", RcDomain)
|
.put("domain", rcDescriptor.domain)
|
||||||
.put("type", RcPlus2Type)
|
.put("type", rcDescriptor.type)
|
||||||
.put("sub_type", RcSubType)
|
.put("sub_type", rcDescriptor.subType)
|
||||||
.put("thing_version", ThingVersion)
|
.put("thing_version", ThingVersion)
|
||||||
.put("access_type", CloudAccessType)
|
.put("access_type", CloudAccessType)
|
||||||
.put("sub_devices", subDevices)
|
.put("sub_devices", subDevices)
|
||||||
@@ -400,23 +419,30 @@ class CloudMqttService(
|
|||||||
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 cameraIndexes = telemetry.cloudCameraPayloadIndexes()
|
||||||
|
val key = "$rcSn/$aircraftSn/${cloudSession.workspaceId}/${cameraIndexes.joinToString(",")}"
|
||||||
if (!force && liveCapacityKey == key) return
|
if (!force && liveCapacityKey == key) return
|
||||||
|
|
||||||
val video = JSONObject()
|
val video = JSONObject()
|
||||||
.put("video_index", Matrice4LiveVideoIndex)
|
.put("video_index", DefaultLiveVideoIndex)
|
||||||
.put("video_type", "normal")
|
.put("video_type", "normal")
|
||||||
.put("switchable_video_types", JSONArray().put("normal"))
|
.put("switchable_video_types", JSONArray().put("normal"))
|
||||||
val camera = JSONObject()
|
val cameras = JSONArray().apply {
|
||||||
.put("available_video_number", 1)
|
cameraIndexes.forEach { cameraIndex ->
|
||||||
.put("coexist_video_number_max", 1)
|
put(
|
||||||
.put("camera_index", Matrice4CameraPayloadIndex)
|
JSONObject()
|
||||||
.put("video_list", JSONArray().put(video))
|
.put("available_video_number", 1)
|
||||||
|
.put("coexist_video_number_max", 1)
|
||||||
|
.put("camera_index", cameraIndex)
|
||||||
|
.put("video_list", JSONArray().put(JSONObject(video.toString())))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
val device = JSONObject()
|
val device = JSONObject()
|
||||||
.put("sn", aircraftSn)
|
.put("sn", aircraftSn)
|
||||||
.put("available_video_number", 1)
|
.put("available_video_number", cameraIndexes.size)
|
||||||
.put("coexist_video_number_max", 1)
|
.put("coexist_video_number_max", 1)
|
||||||
.put("camera_list", JSONArray().put(camera))
|
.put("camera_list", cameras)
|
||||||
val payload = topicRequest(cloudSession)
|
val payload = topicRequest(cloudSession)
|
||||||
.put("gateway", rcSn)
|
.put("gateway", rcSn)
|
||||||
.put("method", "livestream_ability_update")
|
.put("method", "livestream_ability_update")
|
||||||
@@ -426,7 +452,7 @@ class CloudMqttService(
|
|||||||
.put(
|
.put(
|
||||||
"live_capacity",
|
"live_capacity",
|
||||||
JSONObject()
|
JSONObject()
|
||||||
.put("available_video_number", 1)
|
.put("available_video_number", cameraIndexes.size)
|
||||||
.put("coexist_video_number_max", 1)
|
.put("coexist_video_number_max", 1)
|
||||||
.put("device_list", JSONArray().put(device))
|
.put("device_list", JSONArray().put(device))
|
||||||
)
|
)
|
||||||
@@ -453,8 +479,9 @@ class CloudMqttService(
|
|||||||
}
|
}
|
||||||
val hmsItems = JSONArray().apply {
|
val hmsItems = JSONArray().apply {
|
||||||
val inTheSky = telemetry.isFlying || telemetry.motorsOn
|
val inTheSky = telemetry.isFlying || telemetry.motorsOn
|
||||||
|
val droneDeviceType = telemetry.cloudDroneDescriptor().deviceType
|
||||||
warningState.items
|
warningState.items
|
||||||
.map { it.toHmsJson(inTheSky) }
|
.map { it.toHmsJson(inTheSky, droneDeviceType) }
|
||||||
.forEach { put(it) }
|
.forEach { put(it) }
|
||||||
}
|
}
|
||||||
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${hmsItems.hmsSignature()}"
|
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${hmsItems.hmsSignature()}"
|
||||||
@@ -480,6 +507,117 @@ class CloudMqttService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun publishFlightTaskProgress(
|
||||||
|
flightId: String,
|
||||||
|
status: String,
|
||||||
|
percent: Int,
|
||||||
|
waylineId: Int,
|
||||||
|
currentWaypointIndex: Int,
|
||||||
|
resultCode: Int,
|
||||||
|
resultMessage: String
|
||||||
|
) {
|
||||||
|
if (flightId.isBlank()) return
|
||||||
|
val data = JSONObject()
|
||||||
|
.put("result", cloudApiResult(resultCode, resultMessage))
|
||||||
|
.put(
|
||||||
|
"output",
|
||||||
|
JSONObject()
|
||||||
|
.put("status", status)
|
||||||
|
.put(
|
||||||
|
"progress",
|
||||||
|
JSONObject()
|
||||||
|
.put("percent", percent.coerceIn(0, 100))
|
||||||
|
.put("wayline_id", waylineId)
|
||||||
|
.put("waylineId", waylineId)
|
||||||
|
.put("current_waypoint_index", currentWaypointIndex)
|
||||||
|
.put("currentWaypointIndex", currentWaypointIndex)
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
"ext",
|
||||||
|
JSONObject()
|
||||||
|
.put("flight_id", flightId)
|
||||||
|
.put("flightId", flightId)
|
||||||
|
.put("media_count", 0)
|
||||||
|
.put("mediaCount", 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
publishCloudEvent("flighttask_progress", flightId, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun publishPointFlightProgress(
|
||||||
|
method: String,
|
||||||
|
commandId: String,
|
||||||
|
status: String,
|
||||||
|
waypointIndex: Int?,
|
||||||
|
remainingDistance: Float?,
|
||||||
|
remainingTime: Float?,
|
||||||
|
resultCode: Int,
|
||||||
|
resultMessage: String
|
||||||
|
) {
|
||||||
|
val isTakeoffToPointProgress = method == "takeoff_to_point_progress"
|
||||||
|
val effectiveWaypointIndex = waypointIndex ?: if (isTakeoffToPointProgress) 1 else null
|
||||||
|
if (method.isBlank() || (commandId.isBlank() && effectiveWaypointIndex == null)) return
|
||||||
|
val data = JSONObject()
|
||||||
|
.put("status", status.toCloudApiEnumValue())
|
||||||
|
.put("result", resultCode)
|
||||||
|
if (commandId.isNotBlank()) {
|
||||||
|
if (isTakeoffToPointProgress) {
|
||||||
|
data.put("flight_id", commandId)
|
||||||
|
.put("flightId", commandId)
|
||||||
|
} else {
|
||||||
|
data.put("fly_to_id", commandId)
|
||||||
|
.put("flyToId", commandId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effectiveWaypointIndex?.let {
|
||||||
|
data.put("way_point_index", it)
|
||||||
|
.put("wayPointIndex", it)
|
||||||
|
.put("waypoint_index", it)
|
||||||
|
.put("waypointIndex", it)
|
||||||
|
}
|
||||||
|
remainingDistance?.let {
|
||||||
|
data.put("remaining_distance", it)
|
||||||
|
.put("remainingDistance", it)
|
||||||
|
}
|
||||||
|
remainingTime?.let {
|
||||||
|
val cloudValue: Any = if (isTakeoffToPointProgress) {
|
||||||
|
it.roundToInt().coerceAtLeast(0)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
data.put("remaining_time", cloudValue)
|
||||||
|
.put("remainingTime", cloudValue)
|
||||||
|
}
|
||||||
|
publishCloudEvent(method, commandId.ifBlank { "waypoint-$effectiveWaypointIndex" }, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun publishCloudEvent(method: String, bid: String, data: JSONObject): Boolean {
|
||||||
|
val cloudSession = session ?: return false
|
||||||
|
if (!isConnected()) return false
|
||||||
|
val identity = deviceIdentity
|
||||||
|
val rcSn = identity.remoteControllerSn
|
||||||
|
if (rcSn.isBlank()) return false
|
||||||
|
val fromSn = identity.aircraftSn.ifBlank { rcSn }
|
||||||
|
val topicSn = fromSn
|
||||||
|
val payload = topicRequest(cloudSession)
|
||||||
|
.put("bid", bid.ifBlank { UUID.randomUUID().toString() })
|
||||||
|
.put("method", method)
|
||||||
|
.put("gateway", rcSn)
|
||||||
|
.put("from", fromSn)
|
||||||
|
.put("need_reply", false)
|
||||||
|
.put("data", data)
|
||||||
|
return publish("thing/product/$topicSn/events", payload, qos = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cloudApiResult(code: Int, message: String): JSONObject =
|
||||||
|
JSONObject()
|
||||||
|
.put("code", code)
|
||||||
|
.put("msg", message)
|
||||||
|
.put("message", message)
|
||||||
|
|
||||||
|
private fun String.toCloudApiEnumValue(): String =
|
||||||
|
trim().lowercase()
|
||||||
|
|
||||||
private fun logHmsDebug(message: String) {
|
private fun logHmsDebug(message: String) {
|
||||||
if (message == hmsDebugKey) return
|
if (message == hmsDebugKey) return
|
||||||
hmsDebugKey = message
|
hmsDebugKey = message
|
||||||
@@ -583,9 +721,9 @@ class CloudMqttService(
|
|||||||
.put("firmware_version", telemetry.firmwareVersion)
|
.put("firmware_version", telemetry.firmwareVersion)
|
||||||
.put("battery", droneBatteryJson())
|
.put("battery", droneBatteryJson())
|
||||||
.put("position_state", dronePositionStateJson())
|
.put("position_state", dronePositionStateJson())
|
||||||
.put("payload", JSONArray().put(rcDronePayloadJson()))
|
.put("payload", rcDronePayloadListJson())
|
||||||
.put("storage", storageJson())
|
.put("storage", storageJson())
|
||||||
.put("cameras", JSONArray().put(osdCameraJson()))
|
.put("cameras", osdCameraListJson())
|
||||||
.put("height_limit", telemetry.heightLimit)
|
.put("height_limit", telemetry.heightLimit)
|
||||||
.put("distance_limit_status", rcDistanceLimitStatusJson())
|
.put("distance_limit_status", rcDistanceLimitStatusJson())
|
||||||
.put("track_id", "")
|
.put("track_id", "")
|
||||||
@@ -660,10 +798,17 @@ class CloudMqttService(
|
|||||||
.put("total", telemetry.storageTotal.toCloudStorageUnit())
|
.put("total", telemetry.storageTotal.toCloudStorageUnit())
|
||||||
.put("used", telemetry.storageUsed().toCloudStorageUnit())
|
.put("used", telemetry.storageUsed().toCloudStorageUnit())
|
||||||
|
|
||||||
private fun osdCameraJson(): JSONObject =
|
private fun osdCameraListJson(): JSONArray =
|
||||||
|
JSONArray().apply {
|
||||||
|
telemetry.cloudCameraPayloadIndexes().forEach { payloadIndex ->
|
||||||
|
put(osdCameraJson(payloadIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun osdCameraJson(payloadIndex: String): JSONObject =
|
||||||
JSONObject()
|
JSONObject()
|
||||||
.put(JsonClassKey, OsdCameraClass)
|
.put(JsonClassKey, OsdCameraClass)
|
||||||
.put("payload_index", Matrice4CameraPayloadIndex)
|
.put("payload_index", payloadIndex)
|
||||||
.put("camera_mode", telemetry.cameraModeCode())
|
.put("camera_mode", telemetry.cameraModeCode())
|
||||||
.put("photo_state", telemetry.photoState)
|
.put("photo_state", telemetry.photoState)
|
||||||
.put("recording_state", telemetry.recordingState)
|
.put("recording_state", telemetry.recordingState)
|
||||||
@@ -674,10 +819,17 @@ class CloudMqttService(
|
|||||||
.put("ir_zoom_factor", telemetry.irZoomFactor)
|
.put("ir_zoom_factor", telemetry.irZoomFactor)
|
||||||
.put("screen_split_enable", false)
|
.put("screen_split_enable", false)
|
||||||
|
|
||||||
private fun rcDronePayloadJson(): JSONObject =
|
private fun rcDronePayloadListJson(): JSONArray =
|
||||||
|
JSONArray().apply {
|
||||||
|
telemetry.cloudCameraPayloadIndexes().forEach { payloadIndex ->
|
||||||
|
put(rcDronePayloadJson(payloadIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rcDronePayloadJson(payloadIndex: String): JSONObject =
|
||||||
JSONObject()
|
JSONObject()
|
||||||
.put(JsonClassKey, RcDronePayloadClass)
|
.put(JsonClassKey, RcDronePayloadClass)
|
||||||
.put("payload_index", Matrice4CameraPayloadIndex)
|
.put("payload_index", payloadIndex)
|
||||||
.put("gimbal_pitch", telemetry.gimbalPitch.toFloat())
|
.put("gimbal_pitch", telemetry.gimbalPitch.toFloat())
|
||||||
.put("gimbal_roll", telemetry.gimbalRoll.toFloat())
|
.put("gimbal_roll", telemetry.gimbalRoll.toFloat())
|
||||||
.put("gimbal_yaw", telemetry.gimbalYaw.toFloat())
|
.put("gimbal_yaw", telemetry.gimbalYaw.toFloat())
|
||||||
@@ -824,11 +976,11 @@ class CloudMqttService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DroneWarningItem.toHmsJson(inTheSky: Boolean): JSONObject {
|
private fun DroneWarningItem.toHmsJson(inTheSky: Boolean, deviceType: String): JSONObject {
|
||||||
val hmsCode = code.toHmsCodeOrFallback(message)
|
val hmsCode = code.toHmsCodeOrFallback(message)
|
||||||
return JSONObject()
|
return JSONObject()
|
||||||
.put("code", hmsCode)
|
.put("code", hmsCode)
|
||||||
.put("device_type", Matrice4DeviceType)
|
.put("device_type", deviceType)
|
||||||
.put("imminent", level.isHmsAlarm())
|
.put("imminent", level.isHmsAlarm())
|
||||||
.put("in_the_sky", inTheSky)
|
.put("in_the_sky", inTheSky)
|
||||||
.put("level", level.toHmsLevel())
|
.put("level", level.toHmsLevel())
|
||||||
@@ -959,6 +1111,186 @@ private fun TelemetrySnapshot.droneOsdLongitude(): Double =
|
|||||||
private fun TelemetrySnapshot.droneOsdPositionSource(): String =
|
private fun TelemetrySnapshot.droneOsdPositionSource(): String =
|
||||||
if (hasUsableRtkPosition()) "RTK" else "FC"
|
if (hasUsableRtkPosition()) "RTK" else "FC"
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudRcDescriptor(): CloudDeviceDescriptor =
|
||||||
|
CloudDeviceDescriptor(
|
||||||
|
domain = RcDomain,
|
||||||
|
type = cloudApiRcType(),
|
||||||
|
subType = RcSubType
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudApiRcType(): Int {
|
||||||
|
val rcText = remoteControllerType.uppercase()
|
||||||
|
return when {
|
||||||
|
groundDeviceIdentity in KnownCloudRcTypes -> groundDeviceIdentity
|
||||||
|
remoteControllerTypeValue in KnownCloudRcTypes -> remoteControllerTypeValue
|
||||||
|
groundDeviceIdentity == 120 || remoteControllerTypeValue == 120 -> CloudRcPlus2Type
|
||||||
|
rcText.contains("RC_PLUS_2") || rcText.contains("RC PLUS 2") || rcText.contains("RCPLUS2") -> CloudRcPlus2Type
|
||||||
|
rcText.contains("RC_PLUS") || rcText.contains("RC PLUS") || rcText.contains("RCPLUS") -> CloudRcPlusType
|
||||||
|
rcText.contains("RC_PRO") || rcText.contains("RC PRO") || rcText.contains("RCPRO") -> CloudRcProType
|
||||||
|
rcText.contains("RC") -> CloudRcType
|
||||||
|
else -> CloudRcPlus2Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudDroneDescriptor(): CloudDeviceDescriptor {
|
||||||
|
val type = cloudApiDroneType()
|
||||||
|
return CloudDeviceDescriptor(
|
||||||
|
domain = DroneDomain,
|
||||||
|
type = type,
|
||||||
|
subType = cloudDroneSubType()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudApiDroneType(): Int {
|
||||||
|
val product = productType.uppercase()
|
||||||
|
val camera = cloudCameraText()
|
||||||
|
return when {
|
||||||
|
product.contains("MATRICE_350") || product.contains("M350") -> 89
|
||||||
|
product.contains("MATRICE_300") || product.contains("M300") -> 60
|
||||||
|
product.contains("M30") -> 67
|
||||||
|
product.contains("MAVIC_3_ENTERPRISE") -> 77
|
||||||
|
product.contains("MATRICE_400") || product.contains("M400") -> 103
|
||||||
|
product.contains("MATRICE_4TD") || product.contains("M4TD") || product.contains("4TD") -> CloudMatrice4DockType
|
||||||
|
product.contains("MATRICE_4D") || product.contains("M4D") || product.contains("4D") -> CloudMatrice4DockType
|
||||||
|
isMsdkMatrice4SeriesProduct(product) && isMatrice4ThermalCamera(camera) -> CloudMatrice4DockType
|
||||||
|
product.contains("MATRICE_4T") || product.contains("M4T") || product.contains("4T") -> CloudMatrice4Type
|
||||||
|
product.contains("MATRICE_4E") || product.contains("M4E") || product.contains("4E") -> CloudMatrice4Type
|
||||||
|
product.contains("MATRICE_4") -> CloudMatrice4Type
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudDroneSubType(): Int {
|
||||||
|
val camera = cloudCameraText()
|
||||||
|
val product = productType.uppercase()
|
||||||
|
return when {
|
||||||
|
camera.contains("M4TD") || camera.contains("M4T") -> 1
|
||||||
|
camera.contains("M4D") || camera.contains("M4E") -> 0
|
||||||
|
camera.contains("M3TD") || camera.contains("M3T") -> 1
|
||||||
|
camera.contains("M30T") -> 1
|
||||||
|
product.contains("MATRICE_4TD") || product.contains("M4TD") || product.contains("4TD") -> 1
|
||||||
|
product.contains("MATRICE_4D") || product.contains("M4D") || product.contains("4D") -> 0
|
||||||
|
product.contains("MATRICE_4T") || product.contains("M4T") || product.contains("4T") -> 1
|
||||||
|
product.contains("MATRICE_4E") || product.contains("M4E") || product.contains("4E") -> 0
|
||||||
|
product.contains("MATRICE_4D") || product.contains("MATRICE_4") -> {
|
||||||
|
if (camera.contains("M4D") || camera.contains("M4E")) 0 else 1
|
||||||
|
}
|
||||||
|
product.contains("M30_SERIES") || product.contains("MAVIC_3_ENTERPRISE") -> {
|
||||||
|
if (camera.contains("M30") || camera.contains("M3E") || camera.contains("M3M")) 0 else 1
|
||||||
|
}
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudCameraPayloadIndexes(): List<String> {
|
||||||
|
val detectedIndexes = cameraIdentities
|
||||||
|
.mapNotNull { it.cloudCameraPayloadIndex(productType) }
|
||||||
|
.distinct()
|
||||||
|
return detectedIndexes.ifEmpty { listOf(cloudCameraPayloadIndex()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudCameraPayloadIndex(): String {
|
||||||
|
val payloadType = cloudPayloadTypeFor(
|
||||||
|
cameraTypeValue = cameraTypeValue,
|
||||||
|
cameraText = cloudCameraText(),
|
||||||
|
productText = productType
|
||||||
|
) ?: fallbackCloudPayloadType()
|
||||||
|
return "${payloadType.type}-${payloadType.subType}-${cameraComponent.toCloudPayloadPositionIndex()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetryCameraIdentity.cloudCameraPayloadIndex(productType: String): String? {
|
||||||
|
val payloadType = cloudPayloadTypeFor(
|
||||||
|
cameraTypeValue = cameraTypeValue,
|
||||||
|
cameraText = listOf(cameraType, payloadCameraType).joinToString(" "),
|
||||||
|
productText = productType
|
||||||
|
) ?: return null
|
||||||
|
return "${payloadType.type}-${payloadType.subType}-${component.toCloudPayloadPositionIndex()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.cloudCameraText(): String =
|
||||||
|
(
|
||||||
|
listOf(cameraType, payloadCameraType) +
|
||||||
|
cameraIdentities.flatMap { listOf(it.cameraType, it.payloadCameraType) }
|
||||||
|
)
|
||||||
|
.joinToString(" ")
|
||||||
|
.uppercase()
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.fallbackCloudPayloadType(): CloudPayloadType =
|
||||||
|
cloudPayloadTypeFor(cameraTypeValue = 0, cameraText = "", productText = productType)
|
||||||
|
?: CloudPayloadType(type = 0, subType = 0)
|
||||||
|
|
||||||
|
private data class CloudPayloadType(
|
||||||
|
val type: Int,
|
||||||
|
val subType: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun cloudPayloadTypeFor(cameraTypeValue: Int, cameraText: String, productText: String): CloudPayloadType? {
|
||||||
|
val camera = cameraText.uppercase()
|
||||||
|
val product = productText.uppercase()
|
||||||
|
val mapped = when {
|
||||||
|
camera.contains("P1") || camera.contains("ZENMUSE_P1") -> CloudPayloadType(50, 65535)
|
||||||
|
camera.contains("H30T") || camera.contains("ZENMUSE_H30T") -> CloudPayloadType(83)
|
||||||
|
camera.contains("H30") || camera.contains("ZENMUSE_H30") -> CloudPayloadType(82)
|
||||||
|
camera.contains("H20N") || camera.contains("ZENMUSE_H20N") -> CloudPayloadType(61)
|
||||||
|
camera.contains("H20T") || camera.contains("ZENMUSE_H20T") -> CloudPayloadType(43)
|
||||||
|
camera.contains("H20") || camera.contains("ZENMUSE_H20") -> CloudPayloadType(42)
|
||||||
|
camera.contains("L1") || camera.contains("ZENMUSE_L1") -> CloudPayloadType(53)
|
||||||
|
camera.contains("L2") || camera.contains("ZENMUSE_L2") -> CloudPayloadType(84)
|
||||||
|
camera.contains("L3") || camera.contains("ZENMUSE_L3") -> CloudPayloadType(165)
|
||||||
|
camera.contains("M4TD") -> CloudPayloadType(99)
|
||||||
|
camera.contains("M4D") -> CloudPayloadType(98)
|
||||||
|
isMsdkMatrice4SeriesProductText(product) && isMatrice4ThermalCamera(camera) -> CloudPayloadType(CloudMatrice4DockThermalPayloadType)
|
||||||
|
camera.contains("M4T") -> CloudPayloadType(CloudMatrice4ThermalPayloadType)
|
||||||
|
camera.contains("M4E") -> CloudPayloadType(88)
|
||||||
|
camera.contains("M3TD") -> CloudPayloadType(81)
|
||||||
|
camera.contains("M3D") -> CloudPayloadType(80)
|
||||||
|
camera.contains("M3TA") -> CloudPayloadType(129)
|
||||||
|
camera.contains("M3T") -> CloudPayloadType(67)
|
||||||
|
camera.contains("M3E") -> CloudPayloadType(66)
|
||||||
|
camera.contains("M30T") -> CloudPayloadType(53)
|
||||||
|
camera == "M30" || camera.contains(" M30 ") -> CloudPayloadType(52)
|
||||||
|
product.contains("MATRICE_350") || product.contains("M350") -> CloudPayloadType(43)
|
||||||
|
product.contains("MATRICE_4TD") || product.contains("M4TD") || product.contains("4TD") -> CloudPayloadType(99)
|
||||||
|
product.contains("MATRICE_4D") || product.contains("M4D") || product.contains("4D") -> CloudPayloadType(98)
|
||||||
|
product.contains("MATRICE_4T") || product.contains("M4T") || product.contains("4T") -> CloudPayloadType(89)
|
||||||
|
product.contains("MATRICE_4E") || product.contains("M4E") || product.contains("4E") -> CloudPayloadType(88)
|
||||||
|
product.contains("MATRICE_4") -> CloudPayloadType(89)
|
||||||
|
product.contains("MAVIC_3_ENTERPRISE") -> CloudPayloadType(66)
|
||||||
|
product.contains("M30_SERIES") -> CloudPayloadType(53)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (mapped != null) {
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
if (cameraTypeValue > 0) {
|
||||||
|
return CloudPayloadType(
|
||||||
|
type = cameraTypeValue,
|
||||||
|
subType = cameraText.cloudPayloadSubType()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TelemetrySnapshot.isMsdkMatrice4SeriesProduct(product: String): Boolean =
|
||||||
|
productTypeValue == MsdkMatrice4SeriesProductType || isMsdkMatrice4SeriesProductText(product)
|
||||||
|
|
||||||
|
private fun isMsdkMatrice4SeriesProductText(product: String): Boolean =
|
||||||
|
product.contains("MATRICE_4_SERIES")
|
||||||
|
|
||||||
|
private fun isMatrice4ThermalCamera(camera: String): Boolean =
|
||||||
|
camera.contains("M4TD") || camera.contains("M4T") || camera.contains(MsdkMatrice4ThermalCameraType.toString())
|
||||||
|
|
||||||
|
private fun String.cloudPayloadSubType(): Int =
|
||||||
|
if (uppercase().contains("P1")) 65535 else 0
|
||||||
|
|
||||||
|
private fun String.toCloudPayloadPositionIndex(): Int =
|
||||||
|
when (uppercase()) {
|
||||||
|
"RIGHT", "PORT_2" -> 1
|
||||||
|
"UP", "PORT_3" -> 2
|
||||||
|
"PORT_4" -> 3
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
private fun TelemetrySnapshot.hasFixedPosition(): Boolean =
|
private fun TelemetrySnapshot.hasFixedPosition(): Boolean =
|
||||||
(gpsValid && locationValid) || hasUsableRtkPosition()
|
(gpsValid && locationValid) || hasUsableRtkPosition()
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,41 @@ 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 com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||||
|
import com.zklh.dronecontroller.core.telemetry.isRtkBlockingTakeoff
|
||||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||||
import dji.sdk.keyvalue.value.flightcontroller.LookAtInfo
|
import dji.sdk.keyvalue.value.flightcontroller.LookAtInfo
|
||||||
import dji.sdk.keyvalue.value.flightcontroller.LookAtMode
|
import dji.sdk.keyvalue.value.flightcontroller.LookAtMode
|
||||||
|
import dji.v5.common.callback.CommonCallbacks
|
||||||
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
|
||||||
|
import dji.v5.manager.aircraft.rtk.RTKCenter
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
class FlightControlService {
|
class FlightControlService(
|
||||||
|
private val telemetryProvider: () -> TelemetrySnapshot = { TelemetrySnapshot() }
|
||||||
|
) {
|
||||||
suspend fun startTakeoff(): DjiCommandResult =
|
suspend fun startTakeoff(): DjiCommandResult =
|
||||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
} else {
|
} else {
|
||||||
|
val rtkAdjustment = disableUnreadyRtkBeforeTakeoff()
|
||||||
|
if (rtkAdjustment?.success == false) {
|
||||||
|
rtkAdjustment
|
||||||
|
} else {
|
||||||
|
val takeoffResult = performStartTakeoff()
|
||||||
|
if (takeoffResult.success && rtkAdjustment != null) {
|
||||||
|
DjiCommandResult.ok("${rtkAdjustment.message},${takeoffResult.message}")
|
||||||
|
} else {
|
||||||
|
takeoffResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun performStartTakeoff(): DjiCommandResult =
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
FlightControllerKey.KeyStartTakeoff.create().action({
|
FlightControllerKey.KeyStartTakeoff.create().action({
|
||||||
continuation.resume(DjiCommandResult.ok("自动起飞已开始"))
|
continuation.resume(DjiCommandResult.ok("自动起飞已开始"))
|
||||||
@@ -24,6 +44,36 @@ class FlightControlService {
|
|||||||
continuation.resume(DjiCommandResult.failed(error))
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun disableUnreadyRtkBeforeTakeoff(): DjiCommandResult? {
|
||||||
|
val telemetry = telemetryProvider()
|
||||||
|
if (!telemetry.isRtkBlockingTakeoff()) return null
|
||||||
|
if (telemetry.motorsOn || telemetry.isFlying) {
|
||||||
|
return DjiCommandResult.failed("RTK 已开启但尚未就绪,电机已起转,不能自动关闭 RTK")
|
||||||
|
}
|
||||||
|
|
||||||
|
val disableResult = setAircraftRtkModuleEnabled(false)
|
||||||
|
return if (disableResult.success) {
|
||||||
|
DjiCommandResult.ok("RTK 信号未就绪,已先关闭 RTK")
|
||||||
|
} else {
|
||||||
|
DjiCommandResult.failed("RTK 信号未就绪,自动关闭 RTK 失败:${disableResult.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setAircraftRtkModuleEnabled(enabled: Boolean): DjiCommandResult =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
RTKCenter.getInstance().setAircraftRTKModuleEnabled(
|
||||||
|
enabled,
|
||||||
|
object : CommonCallbacks.CompletionCallback {
|
||||||
|
override fun onSuccess() {
|
||||||
|
continuation.resume(DjiCommandResult.ok(if (enabled) "RTK 已开启" else "RTK 已关闭"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: IDJIError) {
|
||||||
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stopTakeoff(): DjiCommandResult =
|
suspend fun stopTakeoff(): DjiCommandResult =
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ data class FlyToStatus(
|
|||||||
val state: FlyToMissionState = FlyToMissionState.UNKNOWN,
|
val state: FlyToMissionState = FlyToMissionState.UNKNOWN,
|
||||||
val mode: FlyToMode = FlyToMode.UNKNOWN,
|
val mode: FlyToMode = FlyToMode.UNKNOWN,
|
||||||
val height: Int = 0,
|
val height: Int = 0,
|
||||||
|
val heightMin: Int = 1,
|
||||||
|
val heightMax: Int = 1500,
|
||||||
val targetLatitude: Double = 0.0,
|
val targetLatitude: Double = 0.0,
|
||||||
val targetLongitude: Double = 0.0,
|
val targetLongitude: Double = 0.0,
|
||||||
val targetAltitude: Double = 0.0,
|
val targetAltitude: Double = 0.0,
|
||||||
@@ -74,8 +76,12 @@ class FlyToService {
|
|||||||
|
|
||||||
private val capabilityListener = object : IMissionCapabilityListener<FlyToCapability> {
|
private val capabilityListener = object : IMissionCapabilityListener<FlyToCapability> {
|
||||||
override fun onMissionCapabilityUpdate(capability: FlyToCapability) {
|
override fun onMissionCapabilityUpdate(capability: FlyToCapability) {
|
||||||
|
val heightMin = capability.heightRange?.min?.roundToInt()?.coerceAtLeast(1) ?: 1
|
||||||
|
val heightMax = capability.heightRange?.max?.roundToInt()?.coerceAtLeast(heightMin) ?: 1500
|
||||||
_status.update {
|
_status.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
heightMin = heightMin,
|
||||||
|
heightMax = heightMax,
|
||||||
heightRangeText = capability.heightRange?.toString().orEmpty()
|
heightRangeText = capability.heightRange?.toString().orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -107,26 +113,24 @@ class FlyToService {
|
|||||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
} else {
|
} else {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val targetHeight = height.roundToInt().coerceAtLeast(1)
|
val targetHeight = height.normalizedFlyToHeight()
|
||||||
val safeTakeoffHeight = securityTakeoffHeight
|
val safeTakeoffHeight = securityTakeoffHeight
|
||||||
.coerceAtLeast(1)
|
.coerceAtLeast(1)
|
||||||
.coerceAtMost(targetHeight)
|
.coerceAtMost(targetHeight)
|
||||||
val target = FlyToTarget().apply {
|
val target = FlyToTarget().apply {
|
||||||
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
targetLocation = LocationCoordinate3D(latitude, longitude, targetHeight.toDouble())
|
||||||
this.maxSpeed = maxSpeed
|
this.maxSpeed = maxSpeed
|
||||||
this.securityTakeoffHeight = safeTakeoffHeight
|
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=$safeTakeoffHeight"
|
"startFlyTo lat=$latitude lon=$longitude requestedHeight=$height targetHeight=$targetHeight " +
|
||||||
|
"mode=$flyToMode maxSpeed=$maxSpeed securityTakeoffHeight=$safeTakeoffHeight " +
|
||||||
|
"heightRange=${status.value.heightMin}..${status.value.heightMax}"
|
||||||
)
|
)
|
||||||
manager.startMission(
|
manager.startMission(
|
||||||
target,
|
target,
|
||||||
param,
|
null,
|
||||||
callback(
|
callback(
|
||||||
continuation,
|
continuation,
|
||||||
"指点飞行已开始",
|
"指点飞行已开始",
|
||||||
@@ -138,7 +142,7 @@ class FlyToService {
|
|||||||
height = targetHeight,
|
height = targetHeight,
|
||||||
targetLatitude = latitude,
|
targetLatitude = latitude,
|
||||||
targetLongitude = longitude,
|
targetLongitude = longitude,
|
||||||
targetAltitude = height
|
targetAltitude = targetHeight.toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -172,18 +176,20 @@ class FlyToService {
|
|||||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||||
} else {
|
} else {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val targetHeight = height.roundToInt().coerceAtLeast(1)
|
val targetHeight = height.normalizedFlyToHeight()
|
||||||
val safeTakeoffHeight = securityTakeoffHeight
|
val safeTakeoffHeight = securityTakeoffHeight
|
||||||
.coerceAtLeast(1)
|
.coerceAtLeast(1)
|
||||||
.coerceAtMost(targetHeight)
|
.coerceAtMost(targetHeight)
|
||||||
val target = FlyToTarget().apply {
|
val target = FlyToTarget().apply {
|
||||||
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
targetLocation = LocationCoordinate3D(latitude, longitude, targetHeight.toDouble())
|
||||||
this.maxSpeed = maxSpeed
|
this.maxSpeed = maxSpeed
|
||||||
this.securityTakeoffHeight = safeTakeoffHeight
|
this.securityTakeoffHeight = safeTakeoffHeight
|
||||||
}
|
}
|
||||||
Log.d(
|
Log.d(
|
||||||
FlyToLogTag,
|
FlyToLogTag,
|
||||||
"updateFlyToTarget lat=$latitude lon=$longitude height=$height maxSpeed=$maxSpeed securityTakeoffHeight=$safeTakeoffHeight"
|
"updateFlyToTarget lat=$latitude lon=$longitude requestedHeight=$height targetHeight=$targetHeight " +
|
||||||
|
"maxSpeed=$maxSpeed securityTakeoffHeight=$safeTakeoffHeight " +
|
||||||
|
"heightRange=${status.value.heightMin}..${status.value.heightMax}"
|
||||||
)
|
)
|
||||||
manager.updateMissionTarget(
|
manager.updateMissionTarget(
|
||||||
target,
|
target,
|
||||||
@@ -195,7 +201,7 @@ class FlyToService {
|
|||||||
it.copy(
|
it.copy(
|
||||||
targetLatitude = latitude,
|
targetLatitude = latitude,
|
||||||
targetLongitude = longitude,
|
targetLongitude = longitude,
|
||||||
targetAltitude = height
|
targetAltitude = targetHeight.toDouble()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,7 +220,7 @@ class FlyToService {
|
|||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val param = FlyToParam().apply {
|
val param = FlyToParam().apply {
|
||||||
if (mode != null) flyToMode = mode
|
if (mode != null) flyToMode = mode
|
||||||
if (height != null) this.height = height.coerceAtLeast(1)
|
if (height != null) this.height = height.toDouble().normalizedFlyToHeight()
|
||||||
}
|
}
|
||||||
manager.updateMissionParam(
|
manager.updateMissionParam(
|
||||||
param,
|
param,
|
||||||
@@ -234,10 +240,11 @@ 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 targetHeight = height.toDouble().normalizedFlyToHeight()
|
||||||
|
val param = FlyToParam().apply { this.height = targetHeight }
|
||||||
manager.updateMissionParam(
|
manager.updateMissionParam(
|
||||||
param,
|
param,
|
||||||
callback(continuation, "指点飞行高度已更新:${height}m")
|
callback(continuation, "指点飞行高度已更新:${targetHeight}m")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +262,15 @@ class FlyToService {
|
|||||||
|
|
||||||
override fun onFailure(error: IDJIError) {
|
override fun onFailure(error: IDJIError) {
|
||||||
onFailure()
|
onFailure()
|
||||||
|
Log.w(FlyToLogTag, "$successMessage failed: $error")
|
||||||
continuation.resume(DjiCommandResult.failed(error))
|
continuation.resume(DjiCommandResult.failed(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Double.normalizedFlyToHeight(): Int {
|
||||||
|
val range = status.value
|
||||||
|
return roundToInt()
|
||||||
|
.coerceAtLeast(range.heightMin)
|
||||||
|
.coerceAtMost(range.heightMax)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import dji.v5.manager.datacenter.livestream.LiveVideoBitrateMode
|
|||||||
import dji.v5.manager.datacenter.livestream.StreamQuality
|
import dji.v5.manager.datacenter.livestream.StreamQuality
|
||||||
import dji.v5.manager.datacenter.livestream.settings.RtmpSettings
|
import dji.v5.manager.datacenter.livestream.settings.RtmpSettings
|
||||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
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
|
||||||
@@ -58,17 +59,35 @@ class LiveStreamingService {
|
|||||||
|
|
||||||
private var listenerRegistered = false
|
private var listenerRegistered = false
|
||||||
private var lastConfig: LiveStartConfig? = null
|
private var lastConfig: LiveStartConfig? = null
|
||||||
|
private var availableCameraIndexes: List<ComponentIndexType> = emptyList()
|
||||||
|
|
||||||
|
private val cameraListener = object : ICameraStreamManager.AvailableCameraUpdatedListener {
|
||||||
|
override fun onAvailableCameraUpdated(cameras: List<ComponentIndexType>) {
|
||||||
|
availableCameraIndexes = cameras
|
||||||
|
Log.d(LiveStreamLogTag, "available camera streams=$cameras")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraStreamEnableUpdate(enableMap: Map<ComponentIndexType, Boolean>) {
|
||||||
|
Log.d(LiveStreamLogTag, "camera stream enable=$enableMap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val statusListener = object : LiveStreamStatusListener {
|
private val statusListener = object : LiveStreamStatusListener {
|
||||||
override fun onLiveStreamStatusUpdate(status: LiveStreamStatus?) {
|
override fun onLiveStreamStatusUpdate(status: LiveStreamStatus?) {
|
||||||
val resolution = status?.resolution?.let { "${it.width}x${it.height}" }.orEmpty()
|
val resolution = status?.resolution?.let { "${it.width}x${it.height}" }.orEmpty()
|
||||||
|
val fps = status?.fps ?: 0
|
||||||
|
val vbps = status?.vbps ?: 0
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
streaming = status?.isStreaming ?: liveStreamManager().isStreaming,
|
streaming = status?.isStreaming ?: liveStreamManager().isStreaming,
|
||||||
fps = status?.fps ?: 0,
|
fps = fps,
|
||||||
vbps = status?.vbps ?: 0,
|
vbps = vbps,
|
||||||
resolution = resolution,
|
resolution = resolution,
|
||||||
message = if (status?.isStreaming == true) "直播推流中" else it.message,
|
message = when {
|
||||||
|
status?.isStreaming != true -> it.message
|
||||||
|
fps <= 0 || vbps <= 0 -> "直播已开启,但暂未收到视频帧"
|
||||||
|
else -> "直播推流中"
|
||||||
|
},
|
||||||
lastError = null
|
lastError = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,6 +108,7 @@ class LiveStreamingService {
|
|||||||
fun clear() {
|
fun clear() {
|
||||||
if (listenerRegistered) {
|
if (listenerRegistered) {
|
||||||
runCatching { liveStreamManager().removeLiveStreamStatusListener(statusListener) }
|
runCatching { liveStreamManager().removeLiveStreamStatusListener(statusListener) }
|
||||||
|
runCatching { cameraStreamManager().removeAvailableCameraUpdatedListener(cameraListener) }
|
||||||
listenerRegistered = false
|
listenerRegistered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +183,8 @@ class LiveStreamingService {
|
|||||||
if (!stopResult.success) return stopResult
|
if (!stopResult.success) return stopResult
|
||||||
}
|
}
|
||||||
|
|
||||||
val cameraIndex = config.videoId.toCameraIndex()
|
waitForAvailableCameraStreams()
|
||||||
|
val cameraIndex = selectCameraIndex(config.videoId)
|
||||||
runCatching {
|
runCatching {
|
||||||
manager.cameraIndex = cameraIndex
|
manager.cameraIndex = cameraIndex
|
||||||
cameraStreamManager().enableStream(cameraIndex, true)
|
cameraStreamManager().enableStream(cameraIndex, true)
|
||||||
@@ -183,7 +204,8 @@ class LiveStreamingService {
|
|||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
LiveStreamLogTag,
|
LiveStreamLogTag,
|
||||||
"start live stream manual=$manual url=${config.url} videoId=${config.videoId} quality=${config.quality} camera=$cameraIndex"
|
"start live stream manual=$manual url=${config.url} videoId=${config.videoId} " +
|
||||||
|
"quality=${config.quality} camera=$cameraIndex available=$availableCameraIndexes"
|
||||||
)
|
)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -215,12 +237,37 @@ class LiveStreamingService {
|
|||||||
private fun ensureListener() {
|
private fun ensureListener() {
|
||||||
if (listenerRegistered) return
|
if (listenerRegistered) return
|
||||||
liveStreamManager().addLiveStreamStatusListener(statusListener)
|
liveStreamManager().addLiveStreamStatusListener(statusListener)
|
||||||
|
cameraStreamManager().addAvailableCameraUpdatedListener(cameraListener)
|
||||||
listenerRegistered = true
|
listenerRegistered = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun liveStreamManager() = MediaDataCenter.getInstance().liveStreamManager
|
private fun liveStreamManager() = MediaDataCenter.getInstance().liveStreamManager
|
||||||
|
|
||||||
private fun cameraStreamManager() = MediaDataCenter.getInstance().cameraStreamManager
|
private fun cameraStreamManager() = MediaDataCenter.getInstance().cameraStreamManager
|
||||||
|
|
||||||
|
private suspend fun waitForAvailableCameraStreams(timeoutMs: Long = 1_500L) {
|
||||||
|
val startAt = System.currentTimeMillis()
|
||||||
|
while (availableCameraIndexes.isEmpty() && System.currentTimeMillis() - startAt < timeoutMs) {
|
||||||
|
delay(100L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectCameraIndex(videoId: String): ComponentIndexType {
|
||||||
|
val explicitIndex = videoId.toExplicitCameraIndex()
|
||||||
|
val available = availableCameraIndexes
|
||||||
|
if (explicitIndex != null && (available.isEmpty() || available.contains(explicitIndex))) {
|
||||||
|
return explicitIndex
|
||||||
|
}
|
||||||
|
if (videoId.hasCloudPayloadIndex() && available.isNotEmpty()) {
|
||||||
|
return available.firstNonFpvOrFirst()
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
available.contains(ComponentIndexType.LEFT_OR_MAIN) -> ComponentIndexType.LEFT_OR_MAIN
|
||||||
|
available.isNotEmpty() -> available.firstNonFpvOrFirst()
|
||||||
|
explicitIndex != null -> explicitIndex
|
||||||
|
else -> ComponentIndexType.LEFT_OR_MAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JSONObject.toLiveStartConfig(): LiveStartConfig? {
|
private fun JSONObject.toLiveStartConfig(): LiveStartConfig? {
|
||||||
@@ -379,9 +426,20 @@ private fun Int.toStreamQuality(): StreamQuality =
|
|||||||
else -> StreamQuality.HD
|
else -> StreamQuality.HD
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toCameraIndex(): ComponentIndexType =
|
private fun String.toExplicitCameraIndex(): ComponentIndexType? =
|
||||||
when {
|
when {
|
||||||
contains("/FPV", ignoreCase = true) -> ComponentIndexType.FPV
|
contains("/FPV", ignoreCase = true) -> ComponentIndexType.FPV
|
||||||
contains("/right", ignoreCase = true) -> ComponentIndexType.RIGHT
|
contains("/right", ignoreCase = true) -> ComponentIndexType.RIGHT
|
||||||
else -> ComponentIndexType.LEFT_OR_MAIN
|
contains("/up", ignoreCase = true) -> ComponentIndexType.UP
|
||||||
|
contains("/port_1", ignoreCase = true) || contains("/port1", ignoreCase = true) -> ComponentIndexType.PORT_1
|
||||||
|
contains("/port_2", ignoreCase = true) || contains("/port2", ignoreCase = true) -> ComponentIndexType.PORT_2
|
||||||
|
contains("/port_3", ignoreCase = true) || contains("/port3", ignoreCase = true) -> ComponentIndexType.PORT_3
|
||||||
|
contains("/port_4", ignoreCase = true) || contains("/port4", ignoreCase = true) -> ComponentIndexType.PORT_4
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.hasCloudPayloadIndex(): Boolean =
|
||||||
|
Regex("""\d+-\d+-\d+""").containsMatchIn(this)
|
||||||
|
|
||||||
|
private fun List<ComponentIndexType>.firstNonFpvOrFirst(): ComponentIndexType =
|
||||||
|
firstOrNull { it != ComponentIndexType.FPV } ?: first()
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import dji.sdk.keyvalue.key.DJIGimbalKey
|
|||||||
import dji.sdk.keyvalue.key.DJIKey
|
import dji.sdk.keyvalue.key.DJIKey
|
||||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||||
import dji.sdk.keyvalue.key.KeyTools
|
import dji.sdk.keyvalue.key.KeyTools
|
||||||
|
import dji.sdk.keyvalue.key.PayloadKey
|
||||||
import dji.sdk.keyvalue.key.ProductKey
|
import dji.sdk.keyvalue.key.ProductKey
|
||||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||||
|
import dji.sdk.keyvalue.value.camera.CameraType
|
||||||
import dji.sdk.keyvalue.key.RtkMobileStationKey
|
import dji.sdk.keyvalue.key.RtkMobileStationKey
|
||||||
import dji.sdk.keyvalue.value.camera.CameraWorkMode
|
import dji.sdk.keyvalue.value.camera.CameraWorkMode
|
||||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||||
@@ -37,8 +39,11 @@ import dji.sdk.keyvalue.value.flightcontroller.GPSSignalLevel
|
|||||||
import dji.sdk.keyvalue.value.flightcontroller.HeightAboveSeaLevelMsg
|
import dji.sdk.keyvalue.value.flightcontroller.HeightAboveSeaLevelMsg
|
||||||
import dji.sdk.keyvalue.value.flightcontroller.RemoteControllerFlightMode
|
import dji.sdk.keyvalue.value.flightcontroller.RemoteControllerFlightMode
|
||||||
import dji.sdk.keyvalue.value.flightcontroller.WindDirection
|
import dji.sdk.keyvalue.value.flightcontroller.WindDirection
|
||||||
|
import dji.sdk.keyvalue.value.payload.PayloadCameraType
|
||||||
|
import dji.sdk.keyvalue.value.product.ProductType
|
||||||
import dji.sdk.keyvalue.value.remotecontroller.BatteryInfo
|
import dji.sdk.keyvalue.value.remotecontroller.BatteryInfo
|
||||||
import dji.sdk.keyvalue.value.remotecontroller.RcGPSInfo
|
import dji.sdk.keyvalue.value.remotecontroller.RcGPSInfo
|
||||||
|
import dji.sdk.keyvalue.value.remotecontroller.RemoteControllerType
|
||||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
|
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
|
||||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKServiceState
|
import dji.sdk.keyvalue.value.rtkbasestation.RTKServiceState
|
||||||
import dji.sdk.keyvalue.value.rtkmobilestation.RTKLocation
|
import dji.sdk.keyvalue.value.rtkmobilestation.RTKLocation
|
||||||
@@ -64,6 +69,21 @@ 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
|
private const val RTK_BOOTSTRAP_RETRY_INTERVAL_MS = 15_000L
|
||||||
|
// Telemetry must not change the aircraft RTK configuration. In particular, an
|
||||||
|
// enabled-but-unfixed M350 RTK module prevents waypoint takeoff.
|
||||||
|
private const val AutoManageRtk = false
|
||||||
|
// Restore only the already-selected network RTK service. This mirrors the
|
||||||
|
// recovery performed by DJI Pilot without force-enabling RTK, changing its
|
||||||
|
// source, or changing the aircraft's maintain-accuracy setting.
|
||||||
|
private const val RestoreExistingNetworkRtkService = true
|
||||||
|
|
||||||
|
data class TelemetryCameraIdentity(
|
||||||
|
val component: String,
|
||||||
|
val cameraType: String = "",
|
||||||
|
val cameraTypeValue: Int = 0,
|
||||||
|
val payloadCameraType: String = "",
|
||||||
|
val payloadCameraTypeValue: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
data class TelemetrySnapshot(
|
data class TelemetrySnapshot(
|
||||||
val latitude: Double = 0.0,
|
val latitude: Double = 0.0,
|
||||||
@@ -119,6 +139,18 @@ data class TelemetrySnapshot(
|
|||||||
val windDirection: Int = 0,
|
val windDirection: Int = 0,
|
||||||
val windSpeed: Int = 0,
|
val windSpeed: Int = 0,
|
||||||
val remainingFlightTime: Int = 0,
|
val remainingFlightTime: Int = 0,
|
||||||
|
val productType: String = "",
|
||||||
|
val productTypeValue: Int = 0,
|
||||||
|
val remoteControllerType: String = "",
|
||||||
|
val remoteControllerTypeValue: Int = 0,
|
||||||
|
val groundDeviceIdentity: Int = 0,
|
||||||
|
val uavDeviceIdentity: Int = 0,
|
||||||
|
val cameraType: String = "",
|
||||||
|
val cameraTypeValue: Int = 0,
|
||||||
|
val payloadCameraType: String = "",
|
||||||
|
val payloadCameraTypeValue: Int = 0,
|
||||||
|
val cameraComponent: String = ComponentIndexType.LEFT_OR_MAIN.name,
|
||||||
|
val cameraIdentities: List<TelemetryCameraIdentity> = emptyList(),
|
||||||
val batteryPercentNeededToLand: Int = 7,
|
val batteryPercentNeededToLand: Int = 7,
|
||||||
val batteryPercentNeededToLandKnown: Boolean = false,
|
val batteryPercentNeededToLandKnown: Boolean = false,
|
||||||
val batteryPercentNeededToGoHome: Int = 14,
|
val batteryPercentNeededToGoHome: Int = 14,
|
||||||
@@ -198,7 +230,11 @@ class TelemetryRepository(
|
|||||||
-> {
|
-> {
|
||||||
rtkStartInProgress = false
|
rtkStartInProgress = false
|
||||||
rtkServiceStarted = false
|
rtkServiceStarted = false
|
||||||
ensureRtkServiceStarted(lastRtkSystemState)
|
if (AutoManageRtk) {
|
||||||
|
ensureRtkServiceStarted(lastRtkSystemState)
|
||||||
|
} else if (RestoreExistingNetworkRtkService) {
|
||||||
|
ensureExistingNetworkRtkServiceStarted(lastRtkSystemState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// READY/CONNECTING/PROCESSING are transitional states; keep the current flags.
|
// READY/CONNECTING/PROCESSING are transitional states; keep the current flags.
|
||||||
@@ -210,7 +246,11 @@ class TelemetryRepository(
|
|||||||
Log.w(TelemetryLogTag, "RTKNetworkServiceError=$error")
|
Log.w(TelemetryLogTag, "RTKNetworkServiceError=$error")
|
||||||
rtkStartInProgress = false
|
rtkStartInProgress = false
|
||||||
rtkServiceStarted = false
|
rtkServiceStarted = false
|
||||||
ensureRtkServiceStarted(lastRtkSystemState)
|
if (AutoManageRtk) {
|
||||||
|
ensureRtkServiceStarted(lastRtkSystemState)
|
||||||
|
} else if (RestoreExistingNetworkRtkService) {
|
||||||
|
ensureExistingNetworkRtkServiceStarted(lastRtkSystemState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val _state = MutableStateFlow(TelemetrySnapshot())
|
private val _state = MutableStateFlow(TelemetrySnapshot())
|
||||||
@@ -288,6 +328,42 @@ class TelemetryRepository(
|
|||||||
_state.update { it.copy(firmwareVersion = value, error = null) }
|
_state.update { it.copy(firmwareVersion = value, error = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
listenSafely("KeyProductType", KeyTools.createKey(ProductKey.KeyProductType)) { productType: ProductType? ->
|
||||||
|
val value = productType?.name.orEmpty()
|
||||||
|
if (value.isNotBlank()) {
|
||||||
|
val typeValue = productType?.value() ?: 0
|
||||||
|
_state.update { it.copy(productType = value, productTypeValue = typeValue, error = null) }
|
||||||
|
Log.d(TelemetryLogTag, "KeyProductType=$value value=$typeValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listenSafely("KeyRemoteControllerType", KeyTools.createKey(RemoteControllerKey.KeyRemoteControllerType)) { type: RemoteControllerType? ->
|
||||||
|
val value = type?.name.orEmpty()
|
||||||
|
if (value.isNotBlank()) {
|
||||||
|
val typeValue = type?.value() ?: 0
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
remoteControllerType = value,
|
||||||
|
remoteControllerTypeValue = typeValue,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d(TelemetryLogTag, "KeyRemoteControllerType=$value value=$typeValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listenSafely("KeyGroundDeviceIdentity", KeyTools.createKey(RemoteControllerKey.KeyGroundDeviceIdentity)) { value: Int? ->
|
||||||
|
val identity = value ?: 0
|
||||||
|
if (identity > 0) {
|
||||||
|
_state.update { it.copy(groundDeviceIdentity = identity, error = null) }
|
||||||
|
Log.d(TelemetryLogTag, "KeyGroundDeviceIdentity=$identity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listenSafely("KeyUAVDeviceIdentity", KeyTools.createKey(RemoteControllerKey.KeyUAVDeviceIdentity)) { value: Int? ->
|
||||||
|
val identity = value ?: 0
|
||||||
|
if (identity > 0) {
|
||||||
|
_state.update { it.copy(uavDeviceIdentity = identity, error = null) }
|
||||||
|
Log.d(TelemetryLogTag, "KeyUAVDeviceIdentity=$identity")
|
||||||
|
}
|
||||||
|
}
|
||||||
listenSafely("KeyFirmwareVersion", KeyTools.createKey(FlightControllerKey.KeyFirmwareVersion)) { firmware: String? ->
|
listenSafely("KeyFirmwareVersion", KeyTools.createKey(FlightControllerKey.KeyFirmwareVersion)) { firmware: String? ->
|
||||||
val value = firmware.orEmpty()
|
val value = firmware.orEmpty()
|
||||||
if (value.isNotBlank()) {
|
if (value.isNotBlank()) {
|
||||||
@@ -502,6 +578,7 @@ class TelemetryRepository(
|
|||||||
listenRemoteControllerTelemetry()
|
listenRemoteControllerTelemetry()
|
||||||
startAndroidLocationFallback()
|
startAndroidLocationFallback()
|
||||||
listenCameraTelemetry()
|
listenCameraTelemetry()
|
||||||
|
listenAdditionalCameraIdentities()
|
||||||
listenGimbalTelemetry()
|
listenGimbalTelemetry()
|
||||||
listenRtkTelemetry()
|
listenRtkTelemetry()
|
||||||
}
|
}
|
||||||
@@ -533,6 +610,7 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun listenCameraTelemetry(cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
|
private fun listenCameraTelemetry(cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
|
||||||
|
listenCameraIdentity(cameraIndex)
|
||||||
listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? ->
|
listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? ->
|
||||||
_state.update { it.copy(cameraWorkMode = mode?.name.orEmpty(), error = null) }
|
_state.update { it.copy(cameraWorkMode = mode?.name.orEmpty(), error = null) }
|
||||||
}
|
}
|
||||||
@@ -595,6 +673,69 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun listenAdditionalCameraIdentities() {
|
||||||
|
listOf(
|
||||||
|
ComponentIndexType.RIGHT,
|
||||||
|
ComponentIndexType.UP,
|
||||||
|
ComponentIndexType.PORT_1,
|
||||||
|
ComponentIndexType.PORT_2,
|
||||||
|
ComponentIndexType.PORT_3,
|
||||||
|
ComponentIndexType.PORT_4
|
||||||
|
).forEach { listenCameraIdentity(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listenCameraIdentity(cameraIndex: ComponentIndexType) {
|
||||||
|
listenSafely("KeyCameraType[$cameraIndex]", KeyTools.createKey(CameraKey.KeyCameraType, cameraIndex)) { type: CameraType? ->
|
||||||
|
val value = type?.name.orEmpty()
|
||||||
|
if (value.isBlank() || value == CameraType.NOT_SUPPORTED.name) return@listenSafely
|
||||||
|
val typeValue = type?.value() ?: 0
|
||||||
|
updateCameraIdentity(cameraIndex, cameraType = value, cameraTypeValue = typeValue)
|
||||||
|
Log.d(TelemetryLogTag, "KeyCameraType camera=$cameraIndex type=$value value=$typeValue")
|
||||||
|
}
|
||||||
|
listenSafely(
|
||||||
|
"KeyPayloadCameraType[$cameraIndex]",
|
||||||
|
KeyTools.createKey(PayloadKey.KeyPayloadCameraType, cameraIndex)
|
||||||
|
) { type: PayloadCameraType? ->
|
||||||
|
val value = type?.name.orEmpty()
|
||||||
|
if (value.isBlank() || value == PayloadCameraType.UNKNOWN.name) return@listenSafely
|
||||||
|
val typeValue = type?.value() ?: 0
|
||||||
|
updateCameraIdentity(cameraIndex, payloadCameraType = value, payloadCameraTypeValue = typeValue)
|
||||||
|
Log.d(TelemetryLogTag, "KeyPayloadCameraType camera=$cameraIndex type=$value value=$typeValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateCameraIdentity(
|
||||||
|
cameraIndex: ComponentIndexType,
|
||||||
|
cameraType: String? = null,
|
||||||
|
cameraTypeValue: Int? = null,
|
||||||
|
payloadCameraType: String? = null,
|
||||||
|
payloadCameraTypeValue: Int? = null
|
||||||
|
) {
|
||||||
|
val component = cameraIndex.name
|
||||||
|
_state.update { snapshot ->
|
||||||
|
val identities = snapshot.cameraIdentities
|
||||||
|
.associateBy { it.component }
|
||||||
|
.toMutableMap()
|
||||||
|
val previous = identities[component] ?: TelemetryCameraIdentity(component = component)
|
||||||
|
val updated = previous.copy(
|
||||||
|
cameraType = cameraType ?: previous.cameraType,
|
||||||
|
cameraTypeValue = cameraTypeValue?.takeIf { it > 0 } ?: previous.cameraTypeValue,
|
||||||
|
payloadCameraType = payloadCameraType ?: previous.payloadCameraType,
|
||||||
|
payloadCameraTypeValue = payloadCameraTypeValue?.takeIf { it > 0 } ?: previous.payloadCameraTypeValue
|
||||||
|
)
|
||||||
|
identities[component] = updated
|
||||||
|
snapshot.copy(
|
||||||
|
cameraType = updated.cameraType.ifBlank { snapshot.cameraType },
|
||||||
|
cameraTypeValue = updated.cameraTypeValue.takeIf { it > 0 } ?: snapshot.cameraTypeValue,
|
||||||
|
payloadCameraType = updated.payloadCameraType.ifBlank { snapshot.payloadCameraType },
|
||||||
|
payloadCameraTypeValue = updated.payloadCameraTypeValue.takeIf { it > 0 } ?: snapshot.payloadCameraTypeValue,
|
||||||
|
cameraComponent = component,
|
||||||
|
cameraIdentities = identities.values.sortedBy { it.component.cameraComponentSortOrder() },
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun listenRemoteControllerTelemetry() {
|
private fun listenRemoteControllerTelemetry() {
|
||||||
listenSafely("KeyRcGPSInfo", KeyTools.createKey(RemoteControllerKey.KeyRcGPSInfo)) { info: RcGPSInfo? ->
|
listenSafely("KeyRcGPSInfo", KeyTools.createKey(RemoteControllerKey.KeyRcGPSInfo)) { info: RcGPSInfo? ->
|
||||||
val location = info?.location
|
val location = info?.location
|
||||||
@@ -671,7 +812,11 @@ class TelemetryRepository(
|
|||||||
}
|
}
|
||||||
listenRtkStationTelemetry()
|
listenRtkStationTelemetry()
|
||||||
listenRtkMobileStationKeys()
|
listenRtkMobileStationKeys()
|
||||||
bootstrapAllPositioning("telemetry_start")
|
if (AutoManageRtk) {
|
||||||
|
bootstrapAllPositioning("telemetry_start")
|
||||||
|
} else {
|
||||||
|
Log.d(TelemetryLogTag, "RTK telemetry is observe-only; skip automatic RTK enable/source/service changes")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenRtkStationTelemetry() {
|
private fun listenRtkStationTelemetry() {
|
||||||
@@ -829,7 +974,32 @@ class TelemetryRepository(
|
|||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ensureRtkServiceStarted(state)
|
if (AutoManageRtk) {
|
||||||
|
ensureRtkServiceStarted(state)
|
||||||
|
} else if (RestoreExistingNetworkRtkService) {
|
||||||
|
ensureExistingNetworkRtkServiceStarted(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only restart the network service selected by the operator/Pilot. This
|
||||||
|
* must never enable RTK, change reference source, or force accuracy mode:
|
||||||
|
* those operations can make an M350 reject takeoff while RTK is unfixed.
|
||||||
|
*/
|
||||||
|
private fun ensureExistingNetworkRtkServiceStarted(state: RTKSystemState?) {
|
||||||
|
if (state?.isRTKEnabled != true || state.rtkHealthy) {
|
||||||
|
if (state?.rtkHealthy == true) {
|
||||||
|
rtkStartInProgress = false
|
||||||
|
rtkServiceStarted = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val source = state.rtkReferenceStationSource ?: return
|
||||||
|
if (!source.isNetworkRtkSource() || rtkServiceStarted) return
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (rtkStartInProgress || now - lastRtkStartAtMs < RTK_START_RETRY_INTERVAL_MS) return
|
||||||
|
Log.d(TelemetryLogTag, "restore existing RTK network service source=$source")
|
||||||
|
startNetworkRtkService(source, setMaintainAccuracy = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureRtkServiceStarted(state: RTKSystemState?) {
|
private fun ensureRtkServiceStarted(state: RTKSystemState?) {
|
||||||
@@ -933,7 +1103,10 @@ class TelemetryRepository(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNetworkRtkService(source: RTKReferenceStationSource) {
|
private fun startNetworkRtkService(
|
||||||
|
source: RTKReferenceStationSource,
|
||||||
|
setMaintainAccuracy: Boolean = true
|
||||||
|
) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (now - lastRtkStartAtMs < 1_000L && source == lastRtkStartSource) {
|
if (now - lastRtkStartAtMs < 1_000L && source == lastRtkStartSource) {
|
||||||
Log.d(TelemetryLogTag, "skip duplicate RTK network service source=$source")
|
Log.d(TelemetryLogTag, "skip duplicate RTK network service source=$source")
|
||||||
@@ -943,7 +1116,9 @@ class TelemetryRepository(
|
|||||||
rtkServiceStarted = false
|
rtkServiceStarted = false
|
||||||
lastRtkStartAtMs = now
|
lastRtkStartAtMs = now
|
||||||
lastRtkStartSource = source
|
lastRtkStartSource = source
|
||||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
if (setMaintainAccuracy) {
|
||||||
|
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 -> {
|
||||||
@@ -1096,6 +1271,18 @@ class TelemetryRepository(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.cameraComponentSortOrder(): Int =
|
||||||
|
when (uppercase()) {
|
||||||
|
ComponentIndexType.LEFT_OR_MAIN.name -> 0
|
||||||
|
ComponentIndexType.RIGHT.name -> 1
|
||||||
|
ComponentIndexType.UP.name -> 2
|
||||||
|
ComponentIndexType.PORT_1.name -> 3
|
||||||
|
ComponentIndexType.PORT_2.name -> 4
|
||||||
|
ComponentIndexType.PORT_3.name -> 5
|
||||||
|
ComponentIndexType.PORT_4.name -> 6
|
||||||
|
else -> 100
|
||||||
|
}
|
||||||
|
|
||||||
private fun isValidCoordinate(latitude: Double?, longitude: Double?): Boolean {
|
private fun isValidCoordinate(latitude: Double?, longitude: Double?): Boolean {
|
||||||
val lat = latitude ?: return false
|
val lat = latitude ?: return false
|
||||||
val lon = longitude ?: return false
|
val lon = longitude ?: return false
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.zklh.dronecontroller.core.telemetry
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
fun TelemetrySnapshot.hasReliableRtkPosition(): Boolean =
|
||||||
|
rtkLocationValid &&
|
||||||
|
(rtkHealthy || rtkFusionDataUsable || rtkPositioningSolution.isSolvedRtkSolution())
|
||||||
|
|
||||||
|
fun TelemetrySnapshot.isRtkBlockingTakeoff(): Boolean =
|
||||||
|
rtkEnabled && !hasReliableRtkPosition()
|
||||||
|
|
||||||
|
fun TelemetrySnapshot.isRtkBlockingWaypointMission(): Boolean =
|
||||||
|
isRtkBlockingTakeoff()
|
||||||
|
|
||||||
|
private fun String.isSolvedRtkSolution(): Boolean {
|
||||||
|
val value = trim()
|
||||||
|
.replace("-", "_")
|
||||||
|
.replace(".", "_")
|
||||||
|
.lowercase(Locale.US)
|
||||||
|
return value == "fixed_point" ||
|
||||||
|
value == "float_point" ||
|
||||||
|
value == "fixed" ||
|
||||||
|
value == "float"
|
||||||
|
}
|
||||||
@@ -172,6 +172,7 @@ import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
|||||||
import com.zklh.dronecontroller.core.simulator.SimulatorService
|
import com.zklh.dronecontroller.core.simulator.SimulatorService
|
||||||
import com.zklh.dronecontroller.core.telemetry.TelemetryRepository
|
import com.zklh.dronecontroller.core.telemetry.TelemetryRepository
|
||||||
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||||
|
import com.zklh.dronecontroller.core.telemetry.hasReliableRtkPosition
|
||||||
import com.zklh.dronecontroller.core.video.DjiVideoPreviewService
|
import com.zklh.dronecontroller.core.video.DjiVideoPreviewService
|
||||||
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
|
||||||
@@ -310,22 +311,22 @@ fun DroneControllerScreen() {
|
|||||||
val appContext = context.applicationContext
|
val appContext = context.applicationContext
|
||||||
val savedCloudLoginConfig = remember(appContext) { appContext.readSavedCloudLoginConfig() }
|
val savedCloudLoginConfig = remember(appContext) { appContext.readSavedCloudLoginConfig() }
|
||||||
val sdkState by DroneSdkManager.state.collectAsState()
|
val sdkState by DroneSdkManager.state.collectAsState()
|
||||||
val flightControl = remember { FlightControlService() }
|
val telemetryRepository = remember(context) { TelemetryRepository(context.applicationContext) }
|
||||||
|
val telemetry by telemetryRepository.state.collectAsState()
|
||||||
|
val latestTelemetry by rememberUpdatedState(telemetry)
|
||||||
|
val flightControl = remember { FlightControlService { latestTelemetry } }
|
||||||
val virtualStick = remember { VirtualStickService() }
|
val virtualStick = remember { VirtualStickService() }
|
||||||
val waypointMission = remember { WaypointMissionService() }
|
val waypointMission = remember { WaypointMissionService() }
|
||||||
val cameraMedia = remember { CameraMediaService() }
|
val cameraMedia = remember { CameraMediaService() }
|
||||||
val flyToService = remember { FlyToService() }
|
val flyToService = remember { FlyToService() }
|
||||||
val liveStreaming = remember { LiveStreamingService() }
|
val liveStreaming = remember { LiveStreamingService() }
|
||||||
val simulator = remember { SimulatorService() }
|
val simulator = remember { SimulatorService() }
|
||||||
val telemetryRepository = remember(context) { TelemetryRepository(context.applicationContext) }
|
|
||||||
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 cloudMediaUpload = remember(appContext, cloudMqttService) {
|
val cloudMediaUpload = remember(appContext, cloudMqttService) {
|
||||||
CloudMediaUploadService(appContext) { cloudMqttService.state.value.session }
|
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) {
|
val cloudCommandExecutor = remember(flightControl, cameraMedia, flyToService, liveStreaming, virtualStick, waypointMission, cloudMediaUpload) {
|
||||||
CloudCommandExecutor(
|
CloudCommandExecutor(
|
||||||
flightControl = flightControl,
|
flightControl = flightControl,
|
||||||
@@ -335,7 +336,8 @@ fun DroneControllerScreen() {
|
|||||||
virtualStick = virtualStick,
|
virtualStick = virtualStick,
|
||||||
waypointMission = waypointMission,
|
waypointMission = waypointMission,
|
||||||
mediaUpload = cloudMediaUpload,
|
mediaUpload = cloudMediaUpload,
|
||||||
telemetryProvider = { latestTelemetry }
|
telemetryProvider = { latestTelemetry },
|
||||||
|
progressPublisher = cloudMqttService
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val stickStatus by virtualStick.status.collectAsState()
|
val stickStatus by virtualStick.status.collectAsState()
|
||||||
@@ -2209,7 +2211,7 @@ private fun PreflightScreen(
|
|||||||
StatusChip(Icons.Filled.BatteryFull, batteryStatusText(sdkState.productConnected, telemetry), if (sdkState.productConnected) Color.Black else PilotMuted)
|
StatusChip(Icons.Filled.BatteryFull, batteryStatusText(sdkState.productConnected, telemetry), if (sdkState.productConnected) Color.Black else PilotMuted)
|
||||||
StatusChip(Icons.Filled.Radio, if (sdkState.productConnected) "100%" else "--", if (sdkState.productConnected) Color.Black else PilotMuted)
|
StatusChip(Icons.Filled.Radio, if (sdkState.productConnected) "100%" else "--", if (sdkState.productConnected) Color.Black else PilotMuted)
|
||||||
StatusChip(Icons.Filled.Home, if (sdkState.productConnected) "未设置" else "--", if (sdkState.productConnected) PilotOrange else PilotMuted)
|
StatusChip(Icons.Filled.Home, if (sdkState.productConnected) "未设置" else "--", if (sdkState.productConnected) PilotOrange else PilotMuted)
|
||||||
StatusChip(Icons.Filled.Warning, if (sdkState.productConnected) "RTK 未连接" else "RTK --", if (sdkState.productConnected) PilotDanger else PilotMuted)
|
StatusChip(Icons.Filled.Warning, rtkStatusText(sdkState.productConnected, telemetry), rtkStatusColor(sdkState.productConnected, telemetry))
|
||||||
StatusChip(Icons.Filled.UploadFile, if (sdkState.productConnected) "39.8G" else "--", if (sdkState.productConnected) Color.Black else PilotMuted)
|
StatusChip(Icons.Filled.UploadFile, if (sdkState.productConnected) "39.8G" else "--", if (sdkState.productConnected) Color.Black else PilotMuted)
|
||||||
StatusChip(Icons.Filled.Person, "A控", Color.Black)
|
StatusChip(Icons.Filled.Person, "A控", Color.Black)
|
||||||
}
|
}
|
||||||
@@ -3002,6 +3004,8 @@ private fun FlightTopBar(
|
|||||||
val batteryPercent = batteryPercentText(sdkState.productConnected, telemetry)
|
val batteryPercent = batteryPercentText(sdkState.productConnected, telemetry)
|
||||||
val batteryVoltage = batteryVoltageText(sdkState.productConnected, telemetry)
|
val batteryVoltage = batteryVoltageText(sdkState.productConnected, telemetry)
|
||||||
val alertCount = effectiveAlertCount(alertState)
|
val alertCount = effectiveAlertCount(alertState)
|
||||||
|
val rtkColor = rtkStatusColor(sdkState.productConnected, telemetry)
|
||||||
|
val rtkCount = rtkSatelliteText(sdkState.productConnected, telemetry)
|
||||||
|
|
||||||
Column(modifier.background(PilotBlack)) {
|
Column(modifier.background(PilotBlack)) {
|
||||||
Row(
|
Row(
|
||||||
@@ -3062,8 +3066,8 @@ private fun FlightTopBar(
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.width(44.dp)) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.width(44.dp)) {
|
||||||
Text("RTK", color = if (sdkState.productConnected) PilotDanger else Color(0xFF9CA3AF), fontSize = 14.sp, fontWeight = FontWeight.Black)
|
Text("RTK", color = rtkColor, fontSize = 14.sp, fontWeight = FontWeight.Black)
|
||||||
Text(if (sdkState.productConnected) "0" else "--", color = if (sdkState.productConnected) PilotDanger else Color(0xFF9CA3AF), fontSize = 15.sp, fontWeight = FontWeight.Black)
|
Text(rtkCount, color = rtkColor, fontSize = 15.sp, fontWeight = FontWeight.Black)
|
||||||
}
|
}
|
||||||
Text(if (sdkState.productLinkConnected) "RC 在线" else "RC --", color = Color.White, fontSize = 15.sp, fontWeight = FontWeight.Black, modifier = Modifier.width(72.dp))
|
Text(if (sdkState.productLinkConnected) "RC 在线" else "RC --", color = Color.White, fontSize = 15.sp, fontWeight = FontWeight.Black, modifier = Modifier.width(72.dp))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.width(68.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.width(68.dp)) {
|
||||||
@@ -4240,6 +4244,31 @@ private fun batteryVoltageText(connected: Boolean, telemetry: TelemetrySnapshot)
|
|||||||
"--"
|
"--"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun rtkStatusText(connected: Boolean, telemetry: TelemetrySnapshot): String =
|
||||||
|
when {
|
||||||
|
!connected -> "RTK --"
|
||||||
|
!telemetry.rtkEnabled -> "RTK 关"
|
||||||
|
telemetry.hasReliableRtkPosition() -> "RTK 正常"
|
||||||
|
telemetry.rtkWorking || telemetry.rtkBeingUsed || telemetry.rtkSatelliteCount > 0 -> "RTK 收敛"
|
||||||
|
else -> "RTK 未就绪"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rtkStatusColor(connected: Boolean, telemetry: TelemetrySnapshot): Color =
|
||||||
|
when {
|
||||||
|
!connected -> PilotMuted
|
||||||
|
!telemetry.rtkEnabled -> PilotMuted
|
||||||
|
telemetry.hasReliableRtkPosition() -> PilotGreen
|
||||||
|
telemetry.rtkWorking || telemetry.rtkBeingUsed || telemetry.rtkSatelliteCount > 0 -> PilotOrange
|
||||||
|
else -> PilotDanger
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rtkSatelliteText(connected: Boolean, telemetry: TelemetrySnapshot): String =
|
||||||
|
when {
|
||||||
|
!connected -> "--"
|
||||||
|
!telemetry.rtkEnabled -> "关"
|
||||||
|
else -> telemetry.rtkSatelliteCount.coerceAtLeast(0).toString()
|
||||||
|
}
|
||||||
|
|
||||||
private fun effectiveCommandMessage(sdkState: DroneSdkState, rawMessage: String): String {
|
private fun effectiveCommandMessage(sdkState: DroneSdkState, rawMessage: String): String {
|
||||||
val passiveMessage = rawMessage.isBlank() ||
|
val passiveMessage = rawMessage.isBlank() ||
|
||||||
rawMessage.contains("等待设备") ||
|
rawMessage.contains("等待设备") ||
|
||||||
|
|||||||
Reference in New Issue
Block a user