feat(cloud): 优化云端MQTT服务的设备在线状态管理

- 添加了设备身份变更检测和处理机制
- 实现了离线拓扑的防抖延迟发布功能
- 优化了设备上下线状态的同步逻辑
- 修复了设备序列号获取和更新的问题
- 改进了飞行器连接状态的判断策略
- 添加了遥测数据的实时值刷新机制
- 优化了UI界面中的遥测启动停止逻辑
This commit is contained in:
zyp
2026-07-15 16:44:11 +08:00
parent e284fc10a4
commit 17429254ad
4 changed files with 377 additions and 27 deletions

View File

@@ -16,8 +16,10 @@ import kotlin.math.sin
import kotlin.math.sqrt import kotlin.math.sqrt
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
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
@@ -53,6 +55,7 @@ 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
private const val TOPO_OFFLINE_DEBOUNCE_MS = 10_000L
private const val MQTT_MAX_INFLIGHT = 200 private const val MQTT_MAX_INFLIGHT = 200
private const val MQTT_REASON_MAX_INFLIGHT = 32202 private const val MQTT_REASON_MAX_INFLIGHT = 32202
private const val MQTT_PUBLISH_CONGESTION_COOLDOWN_MS = 5_000L private const val MQTT_PUBLISH_CONGESTION_COOLDOWN_MS = 5_000L
@@ -98,6 +101,9 @@ class CloudMqttService(
private var warningHmsKey = "" private var warningHmsKey = ""
private var warningDebugRaw = "" private var warningDebugRaw = ""
private var hmsDebugKey = "" private var hmsDebugKey = ""
private var lastOnlineIdentity: CloudDeviceIdentity? = null
private var pendingOfflineTopoJob: Job? = null
private var pendingOfflineIdentity: CloudDeviceIdentity? = null
private var lastOsdAt = 0L private var lastOsdAt = 0L
@Volatile @Volatile
private var lastPublishCongestedAt = 0L private var lastPublishCongestedAt = 0L
@@ -197,7 +203,11 @@ class CloudMqttService(
fun disconnect() { fun disconnect() {
scope.launch { scope.launch {
runCatching { publishStatusOnline(online = false) } cancelPendingOfflineTopo()
runCatching {
publishStatusOnline(online = false, identity = lastOnlineIdentity ?: deviceIdentity)
}
lastOnlineIdentity = null
closeCurrentClient() closeCurrentClient()
_state.update { _state.update {
it.copy( it.copy(
@@ -211,21 +221,42 @@ class CloudMqttService(
} }
fun clear() { fun clear() {
runCatching { publishStatusOnline(online = false) } cancelPendingOfflineTopo()
runCatching {
publishStatusOnline(online = false, identity = lastOnlineIdentity ?: deviceIdentity)
}
lastOnlineIdentity = null
closeCurrentClient() closeCurrentClient()
scope.cancel() scope.cancel()
} }
fun updateDeviceState(sdkState: DroneSdkState) { fun updateDeviceState(sdkState: DroneSdkState) {
deviceIdentity = CloudDeviceIdentity( val previousIdentity = deviceIdentity
val nextIdentity = CloudDeviceIdentity(
remoteControllerSn = sdkState.remoteControllerSerialNumber, remoteControllerSn = sdkState.remoteControllerSerialNumber,
aircraftSn = sdkState.aircraftSerialNumber, aircraftSn = sdkState.aircraftSerialNumber,
productConnected = sdkState.productConnected, productConnected = sdkState.productConnected,
productLinkConnected = sdkState.productLinkConnected productLinkConnected = sdkState.productLinkConnected
) )
val identityChanged = previousIdentity != nextIdentity
val shouldSchedulePreviousOffline = previousIdentity.readyForOnline && (
previousIdentity.remoteControllerSn != nextIdentity.remoteControllerSn ||
previousIdentity.aircraftSn != nextIdentity.aircraftSn ||
!nextIdentity.productConnected
)
deviceIdentity = nextIdentity
if (identityChanged) {
resetPublishState()
}
if (shouldSchedulePreviousOffline) {
scheduleOfflineTopo(previousIdentity)
}
if (nextIdentity.readyForOnline && nextIdentity.productConnected) {
cancelPendingOfflineTopo(nextIdentity)
}
scope.launch { scope.launch {
subscribeControlTopicsIfReady() subscribeControlTopicsIfReady()
publishOnlineIfReady(force = false) publishOnlineIfReady(force = identityChanged && nextIdentity.readyForOnline && nextIdentity.productConnected)
publishLiveCapacityIfReady(deviceIdentity, force = false) publishLiveCapacityIfReady(deviceIdentity, force = false)
publishHmsIfReady(force = false) publishHmsIfReady(force = false)
} }
@@ -355,18 +386,21 @@ class CloudMqttService(
publishReply(request, result.success, result.message) publishReply(request, result.success, result.message)
} }
private fun publishOnlineIfReady(force: Boolean) { private fun publishOnlineIfReady(force: Boolean): Boolean {
if (!isConnected()) return if (!isConnected()) return false
val identity = deviceIdentity val identity = deviceIdentity
if (!identity.readyForOnline) return if (!identity.readyForOnline || !identity.productConnected) return false
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${session?.workspaceId}" val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${session?.workspaceId}"
if (!force && onlineKey == key) return if (!force && onlineKey == key) return true
publishStatusOnline(online = true) if (!publishStatusOnline(online = true, identity = identity)) return false
onlineKey = key onlineKey = key
lastOnlineIdentity = identity
cancelPendingOfflineTopo(identity)
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
_state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") } _state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") }
publishLiveCapacityIfReady(identity, force = true) publishLiveCapacityIfReady(identity, force = true)
bindDevices(identity) bindDevices(identity)
return true
} }
private fun bindDevices(identity: CloudDeviceIdentity) { private fun bindDevices(identity: CloudDeviceIdentity) {
@@ -379,11 +413,10 @@ class CloudMqttService(
} }
} }
private fun publishStatusOnline(online: Boolean) { private fun publishStatusOnline(online: Boolean, identity: CloudDeviceIdentity = deviceIdentity): Boolean {
val cloudSession = session ?: return val cloudSession = session ?: return false
val identity = deviceIdentity
val rcSn = identity.remoteControllerSn val rcSn = identity.remoteControllerSn
if (rcSn.isBlank()) return if (rcSn.isBlank()) return false
val subDevices = JSONArray() val subDevices = JSONArray()
if (online && identity.aircraftSn.isNotBlank()) { if (online && identity.aircraftSn.isNotBlank()) {
val droneDescriptor = telemetry.cloudDroneDescriptor() val droneDescriptor = telemetry.cloudDroneDescriptor()
@@ -410,7 +443,7 @@ class CloudMqttService(
.put("access_type", CloudAccessType) .put("access_type", CloudAccessType)
.put("sub_devices", subDevices) .put("sub_devices", subDevices)
) )
publish("sys/product/$rcSn/status", payload, qos = 1) return publish("sys/product/$rcSn/status", payload, qos = 1)
} }
private fun publishLiveCapacityIfReady(identity: CloudDeviceIdentity, force: Boolean) { private fun publishLiveCapacityIfReady(identity: CloudDeviceIdentity, force: Boolean) {
@@ -627,7 +660,8 @@ class CloudMqttService(
private fun publishOsdIfReady(force: Boolean) { private fun publishOsdIfReady(force: Boolean) {
if (!isConnected()) return if (!isConnected()) return
val identity = deviceIdentity val identity = deviceIdentity
if (!identity.readyForOnline) return if (!identity.readyForOnline || !identity.productConnected) return
if (onlineKey.isBlank() && !publishOnlineIfReady(force = true)) return
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val shouldPublish = synchronized(osdThrottleLock) { val shouldPublish = synchronized(osdThrottleLock) {
when { when {
@@ -947,6 +981,7 @@ class CloudMqttService(
} }
private fun closeCurrentClient() { private fun closeCurrentClient() {
cancelPendingOfflineTopo()
val oldClient = client val oldClient = client
client = null client = null
resetPublishState() resetPublishState()
@@ -974,7 +1009,55 @@ class CloudMqttService(
warningHmsKey = "" warningHmsKey = ""
hmsDebugKey = "" hmsDebugKey = ""
} }
private fun scheduleOfflineTopo(identity: CloudDeviceIdentity) {
cancelPendingOfflineTopo()
pendingOfflineIdentity = identity
pendingOfflineTopoJob = scope.launch {
delay(TOPO_OFFLINE_DEBOUNCE_MS)
val currentIdentity = deviceIdentity
val recoveredSameDevice = currentIdentity.readyForOnline &&
currentIdentity.productConnected &&
currentIdentity.sameCloudDevice(identity)
if (!recoveredSameDevice) {
val published = publishStatusOnline(online = false, identity = identity)
if (published && lastOnlineIdentity?.sameCloudDevice(identity) == true) {
lastOnlineIdentity = null
} }
Log.i(
CloudMqttLogTag,
"published delayed offline topo rc=${identity.remoteControllerSn} aircraft=${identity.aircraftSn} " +
"published=$published currentRc=${currentIdentity.remoteControllerSn} currentAircraft=${currentIdentity.aircraftSn} " +
"currentConnected=${currentIdentity.productConnected}"
)
} else {
Log.d(
CloudMqttLogTag,
"skip offline topo because aircraft recovered rc=${identity.remoteControllerSn} aircraft=${identity.aircraftSn}"
)
}
if (pendingOfflineIdentity?.sameCloudDevice(identity) == true) {
pendingOfflineTopoJob = null
pendingOfflineIdentity = null
}
}
Log.d(
CloudMqttLogTag,
"scheduled offline topo debounce=${TOPO_OFFLINE_DEBOUNCE_MS}ms rc=${identity.remoteControllerSn} aircraft=${identity.aircraftSn}"
)
}
private fun cancelPendingOfflineTopo(identity: CloudDeviceIdentity? = null) {
val pendingIdentity = pendingOfflineIdentity
if (identity != null && pendingIdentity?.sameCloudDevice(identity) != true) return
pendingOfflineTopoJob?.cancel()
pendingOfflineTopoJob = null
pendingOfflineIdentity = null
}
}
private fun CloudDeviceIdentity.sameCloudDevice(other: CloudDeviceIdentity): Boolean =
remoteControllerSn == other.remoteControllerSn && aircraftSn == other.aircraftSn
private fun DroneWarningItem.toHmsJson(inTheSky: Boolean, deviceType: String): JSONObject { private fun DroneWarningItem.toHmsJson(inTheSky: Boolean, deviceType: String): JSONObject {
val hmsCode = code.toHmsCodeOrFallback(message) val hmsCode = code.toHmsCodeOrFallback(message)

View File

@@ -238,12 +238,20 @@ object DroneSdkManager {
} }
private fun readAircraftConnectionProbe(): AircraftConnectionProbe { private fun readAircraftConnectionProbe(): AircraftConnectionProbe {
val flightControllerConnected = runCatching {
KeyManager.getInstance().getValue(
KeyTools.createKey(FlightControllerKey.KeyConnection),
false
)
}.getOrNull()
val rcAircraftState = runCatching { val rcAircraftState = runCatching {
KeyManager.getInstance().getValue( KeyManager.getInstance().getValue(
KeyTools.createKey(RemoteControllerKey.KeyRcMultiDeviceAircraftState) KeyTools.createKey(RemoteControllerKey.KeyRcMultiDeviceAircraftState)
) )
}.getOrNull() }.getOrNull()
val signals = readAircraftRealtimeSignals()
if (rcAircraftState != null) { if (rcAircraftState != null) {
val mainStates = listOf( val mainStates = listOf(
rcAircraftState.sdrConnectState, rcAircraftState.sdrConnectState,
@@ -257,9 +265,35 @@ object DroneSdkManager {
if (!connectedByRc) { if (!connectedByRc) {
return AircraftConnectionProbe(false, detail) return AircraftConnectionProbe(false, detail)
} }
return AircraftConnectionProbe(true, detail) // M350 + RC Plus may report the aircraft link as CONNECTED/USING before
// FlightControllerKey.KeyConnection or other FC telemetry keys are readable.
// Treating that transient as disconnected blocks manual/keyboard virtual-stick
// control and clears the aircraft SN used by cloud-control topics.
if (flightControllerConnected == true) {
return AircraftConnectionProbe(true, "$detail / 飞控Key已连接")
}
if (signals.size >= 2) {
return AircraftConnectionProbe(true, "$detail / ${signals.joinToString(" / ")}")
}
val fallbackDetail = if (signals.isEmpty()) {
"RC链路已连接等待飞控实时信号"
} else {
"RC链路已连接飞控实时信号不足${signals.joinToString(" / ")}"
}
return AircraftConnectionProbe(true, "$detail / $fallbackDetail")
} }
if (flightControllerConnected == true) {
return AircraftConnectionProbe(true, "飞控Key已连接")
}
val connected = signals.size >= 2
return AircraftConnectionProbe(
connected = connected,
detail = if (signals.isEmpty()) "未读到飞控实时信号" else signals.joinToString(" / ")
)
}
private fun readAircraftRealtimeSignals(): List<String> {
val signals = mutableListOf<String>() val signals = mutableListOf<String>()
val serialNumber = runCatching { val serialNumber = runCatching {
KeyManager.getInstance().getValue( KeyManager.getInstance().getValue(
@@ -285,11 +319,7 @@ object DroneSdkManager {
}.getOrDefault(-1) }.getOrDefault(-1)
if (batteryPercent in 0..100) signals += "电量:$batteryPercent%" if (batteryPercent in 0..100) signals += "电量:$batteryPercent%"
val connected = signals.size >= 2 return signals
return AircraftConnectionProbe(
connected = connected,
detail = if (signals.isEmpty()) "未读到飞控实时信号" else signals.joinToString(" / ")
)
} }
private fun isAircraftLinkActive(state: RcMutilDeviceState?): Boolean = private fun isAircraftLinkActive(state: RcMutilDeviceState?): Boolean =
@@ -301,6 +331,16 @@ object DroneSdkManager {
detail: String detail: String
) { ) {
_state.update { current -> _state.update { current ->
val aircraftSerialNumber = if (aircraftConnected) {
readAircraftSerialNumber().ifBlank { current.aircraftSerialNumber }
} else {
""
}
val remoteControllerSerialNumber = if (productLinkConnected) {
readRemoteControllerSerialNumber().ifBlank { current.remoteControllerSerialNumber }
} else {
""
}
val event = when { val event = when {
current.productConnected == aircraftConnected && current.productConnected == aircraftConnected &&
current.productLinkConnected == productLinkConnected -> current.initEvent current.productLinkConnected == productLinkConnected -> current.initEvent
@@ -313,8 +353,8 @@ object DroneSdkManager {
productConnected = aircraftConnected, productConnected = aircraftConnected,
productLinkConnected = productLinkConnected, productLinkConnected = productLinkConnected,
aircraftConnectionDetail = detail, aircraftConnectionDetail = detail,
aircraftSerialNumber = if (aircraftConnected) readAircraftSerialNumber() else "", aircraftSerialNumber = aircraftSerialNumber,
remoteControllerSerialNumber = if (productLinkConnected) readRemoteControllerSerialNumber() else "" remoteControllerSerialNumber = remoteControllerSerialNumber
) )
} }
} }

View File

@@ -69,6 +69,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 private const val RTK_BOOTSTRAP_RETRY_INTERVAL_MS = 15_000L
private const val CURRENT_VALUE_REFRESH_MIN_INTERVAL_MS = 1_000L
// Telemetry must not change the aircraft RTK configuration. In particular, an // Telemetry must not change the aircraft RTK configuration. In particular, an
// enabled-but-unfixed M350 RTK module prevents waypoint takeoff. // enabled-but-unfixed M350 RTK module prevents waypoint takeoff.
private const val AutoManageRtk = false private const val AutoManageRtk = false
@@ -264,12 +265,14 @@ class TelemetryRepository(
private var lastRtkStartAtMs = 0L private var lastRtkStartAtMs = 0L
private var lastRtkStartSource = RTKReferenceStationSource.UNKNOWN private var lastRtkStartSource = RTKReferenceStationSource.UNKNOWN
private var lastRtkBootstrapAtMs = 0L private var lastRtkBootstrapAtMs = 0L
private var lastCurrentValueRefreshAtMs = 0L
private var lastRtkSystemState: RTKSystemState? = null private var lastRtkSystemState: RTKSystemState? = null
fun start() { fun start() {
if (started) return if (started) return
started = true started = true
Log.d(TelemetryLogTag, "start telemetry listeners") Log.d(TelemetryLogTag, "start telemetry listeners")
listenConnectionTelemetry()
listenSafely("KeyAircraftLocation", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation)) { location: LocationCoordinate2D? -> listenSafely("KeyAircraftLocation", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation)) { location: LocationCoordinate2D? ->
location ?: return@listenSafely location ?: return@listenSafely
Log.d( Log.d(
@@ -282,16 +285,18 @@ class TelemetryRepository(
location ?: return@listenSafely location ?: return@listenSafely
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 altitude = location.altitude
val valid = isValidCoordinate(latitude, longitude) val valid = isValidCoordinate(latitude, longitude)
Log.d( Log.d(
TelemetryLogTag, TelemetryLogTag,
"KeyAircraftLocation3D lat=$latitude lon=$longitude rawAltitude=${location.altitude} valid=$valid" "KeyAircraftLocation3D lat=$latitude lon=$longitude rawAltitude=$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 ?: it.altitude,
error = null error = null
) )
} }
@@ -581,6 +586,7 @@ class TelemetryRepository(
listenAdditionalCameraIdentities() listenAdditionalCameraIdentities()
listenGimbalTelemetry() listenGimbalTelemetry()
listenRtkTelemetry() listenRtkTelemetry()
refreshCurrentValues("telemetry_start", force = true)
} }
fun stop() { fun stop() {
@@ -594,6 +600,7 @@ class TelemetryRepository(
runCatching { rtkCenter.rtkStationManager.removeSearchRTKStationListener(searchRtkStationListener) } runCatching { rtkCenter.rtkStationManager.removeSearchRTKStationListener(searchRtkStationListener) }
runCatching { rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(rtkStationConnectStatusListener) } runCatching { rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(rtkStationConnectStatusListener) }
runCatching { rtkCenter.rtkStationManager.removeConnectedRTKStationInfoListener(connectedRtkStationInfoListener) } runCatching { rtkCenter.rtkStationManager.removeConnectedRTKStationInfoListener(connectedRtkStationInfoListener) }
lastCurrentValueRefreshAtMs = 0L
started = false started = false
} }
@@ -609,6 +616,222 @@ class TelemetryRepository(
} }
} }
private fun listenConnectionTelemetry() {
listenSafely("KeyFlightControllerConnection", KeyTools.createKey(FlightControllerKey.KeyConnection)) { connected: Boolean? ->
Log.d(TelemetryLogTag, "KeyFlightControllerConnection=$connected")
if (connected == true) {
refreshCurrentValues("flight_controller_connected")
}
}
listOf(ComponentIndexType.LEFT_OR_MAIN, ComponentIndexType.RIGHT).forEach { index ->
listenSafely("KeyBatteryConnection[$index]", KeyTools.createKey(BatteryKey.KeyConnection, index)) { connected: Boolean? ->
Log.d(TelemetryLogTag, "KeyBatteryConnection[$index]=$connected")
if (connected == true) {
refreshCurrentValues("battery_connected_$index")
}
}
}
}
private fun refreshCurrentValues(reason: String, force: Boolean = false) {
val now = System.currentTimeMillis()
if (!force && now - lastCurrentValueRefreshAtMs < CURRENT_VALUE_REFRESH_MIN_INTERVAL_MS) return
lastCurrentValueRefreshAtMs = now
Log.d(TelemetryLogTag, "refresh current telemetry values reason=$reason")
currentValue("KeyAircraftLocation", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation))
?.let { location: LocationCoordinate2D -> updateAircraftLocation(location.latitude, location.longitude) }
currentValue("KeyAircraftLocation3D", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D))
?.let { location: LocationCoordinate3D ->
val latitude = location.latitude ?: 0.0
val longitude = location.longitude ?: 0.0
val altitude = location.altitude
val valid = isValidCoordinate(latitude, longitude)
_state.update {
it.copy(
latitude = latitude,
longitude = longitude,
locationValid = valid,
altitude = altitude ?: it.altitude,
error = null
)
}
}
currentValue("KeyAltitude", KeyTools.createKey(FlightControllerKey.KeyAltitude))
?.let { altitude: Double -> _state.update { it.copy(altitude = altitude, error = null) } }
currentValue("KeyHeightAboveSeaLevel", KeyTools.createKey(FlightControllerKey.KeyHeightAboveSeaLevel))
?.let { height: HeightAboveSeaLevelMsg -> _state.update { it.copy(elevation = height.height ?: 0.0, error = null) } }
currentValue("KeyCompassHeading", KeyTools.createKey(FlightControllerKey.KeyCompassHeading))
?.let { heading: Double -> _state.update { it.copy(heading = heading, error = null) } }
currentValue("KeyAircraftAttitude", KeyTools.createKey(FlightControllerKey.KeyAircraftAttitude))
?.let { attitude: Attitude ->
_state.update {
it.copy(
attitudePitch = attitude.pitch ?: 0.0,
attitudeRoll = attitude.roll ?: 0.0,
heading = attitude.yaw ?: it.heading,
error = null
)
}
}
currentValue("KeyAircraftVelocity", KeyTools.createKey(FlightControllerKey.KeyAircraftVelocity))
?.let { velocity: Velocity3D -> _state.update { it.copy(speedX = velocity.x, speedY = velocity.y, speedZ = velocity.z, error = null) } }
currentValue("ProductKeyFirmwareVersion", KeyTools.createKey(ProductKey.KeyFirmwareVersion))
?.takeIf { it.isNotBlank() }
?.let { value -> _state.update { it.copy(firmwareVersion = value, error = null) } }
currentValue("KeyFirmwareVersion", KeyTools.createKey(FlightControllerKey.KeyFirmwareVersion))
?.takeIf { it.isNotBlank() }
?.let { value ->
_state.update {
if (it.firmwareVersion.isBlank()) it.copy(firmwareVersion = value, error = null) else it.copy(error = null)
}
}
currentValue("KeyProductType", KeyTools.createKey(ProductKey.KeyProductType))
?.let { productType: ProductType ->
_state.update { it.copy(productType = productType.name, productTypeValue = productType.value(), error = null) }
}
currentValue("KeyRemoteControllerType", KeyTools.createKey(RemoteControllerKey.KeyRemoteControllerType))
?.let { type: RemoteControllerType ->
_state.update { it.copy(remoteControllerType = type.name, remoteControllerTypeValue = type.value(), error = null) }
}
currentValue("KeyGroundDeviceIdentity", KeyTools.createKey(RemoteControllerKey.KeyGroundDeviceIdentity))
?.takeIf { it > 0 }
?.let { identity -> _state.update { it.copy(groundDeviceIdentity = identity, error = null) } }
currentValue("KeyUAVDeviceIdentity", KeyTools.createKey(RemoteControllerKey.KeyUAVDeviceIdentity))
?.takeIf { it > 0 }
?.let { identity -> _state.update { it.copy(uavDeviceIdentity = identity, error = null) } }
currentValue("KeyRemoteControllerFlightMode", KeyTools.createKey(FlightControllerKey.KeyRemoteControllerFlightMode))
?.let { mode: RemoteControllerFlightMode -> _state.update { it.copy(gear = mode.value(), error = null) } }
currentValue("KeyGPSSatelliteCount", KeyTools.createKey(FlightControllerKey.KeyGPSSatelliteCount))
?.let { count: Int -> _state.update { it.copy(gpsSatelliteCount = count, error = null) } }
currentValue("KeyGPSSignalLevel", KeyTools.createKey(FlightControllerKey.KeyGPSSignalLevel))
?.let { level: GPSSignalLevel -> _state.update { it.copy(gpsSignalLevel = level.value(), error = null) } }
currentValue("KeyGPSIsValid", KeyTools.createKey(FlightControllerKey.KeyGPSIsValid))
?.let { valid: Boolean -> _state.update { it.copy(gpsValid = valid, error = null) } }
currentValue("KeyHomeLocation", KeyTools.createKey(FlightControllerKey.KeyHomeLocation))
?.let { location: LocationCoordinate2D ->
_state.update {
if (isValidCoordinate(location.latitude, location.longitude)) {
it.copy(homeLatitude = location.latitude, homeLongitude = location.longitude, homeLocationValid = true, error = null)
} else {
it.copy(homeLocationValid = false, error = null)
}
}
}
currentValue("KeyFlightMode", KeyTools.createKey(FlightControllerKey.KeyFlightMode))
?.let { mode: FlightMode -> _state.update { it.copy(flightMode = mode.name, error = null) } }
currentValue("KeyFlightModeString", KeyTools.createKey(FlightControllerKey.KeyFlightModeString))
?.let { modeString: String -> _state.update { it.copy(flightModeString = modeString, error = null) } }
currentValue("KeyFCFlightMode", KeyTools.createKey(FlightControllerKey.KeyFCFlightMode))
?.let { mode: FCFlightMode -> _state.update { it.copy(fcFlightMode = mode.name, error = null) } }
currentValue("KeyIsFlying", KeyTools.createKey(FlightControllerKey.KeyIsFlying))
?.let { flying: Boolean -> _state.update { it.copy(isFlying = flying, error = null) } }
currentValue("KeyAreMotorsOn", KeyTools.createKey(FlightControllerKey.KeyAreMotorsOn))
?.let { motorsOn: Boolean -> _state.update { it.copy(motorsOn = motorsOn, error = null) } }
currentValue("KeyIsInLandingMode", KeyTools.createKey(FlightControllerKey.KeyIsInLandingMode))
?.let { landingMode: Boolean -> _state.update { it.copy(landingMode = landingMode, error = null) } }
currentValue("KeyIsLandingConfirmationNeeded", KeyTools.createKey(FlightControllerKey.KeyIsLandingConfirmationNeeded))
?.let { needed: Boolean -> _state.update { it.copy(landingConfirmationNeeded = needed, error = null) } }
currentValue("KeyIsSimulatorStarted", KeyTools.createKey(FlightControllerKey.KeyIsSimulatorStarted))
?.let { simulatorStarted: Boolean -> _state.update { it.copy(simulatorStarted = simulatorStarted, error = null) } }
currentValue("KeyHeightLimit", KeyTools.createKey(FlightControllerKey.KeyHeightLimit))
?.let { limit: Int -> _state.update { it.copy(heightLimit = limit, error = null) } }
currentValue("KeyDistanceLimit", KeyTools.createKey(FlightControllerKey.KeyDistanceLimit))
?.let { limit: Int -> _state.update { it.copy(distanceLimit = limit, error = null) } }
currentValue("KeyDistanceLimitEnabled", KeyTools.createKey(FlightControllerKey.KeyDistanceLimitEnabled))
?.let { enabled: Boolean -> _state.update { it.copy(distanceLimitEnabled = enabled, error = null) } }
currentValue("KeyAircraftTotalFlightDistance", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightDistance))
?.let { distance: Double -> _state.update { it.copy(totalFlightDistance = distance, error = null) } }
currentValue("KeyAircraftTotalFlightDuration", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightDuration))
?.let { duration: Double -> _state.update { it.copy(totalFlightTime = duration, error = null) } }
currentValue("KeyAircraftTotalFlightTimes", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightTimes))
?.let { times: Int -> _state.update { it.copy(totalFlightSorties = times, error = null) } }
currentValue("KeyRemainingFlightTime", KeyTools.createKey(FlightControllerKey.KeyRemainingFlightTime))
?.let { seconds: Int -> _state.update { it.copy(remainingFlightTime = seconds, remainingFlightTimeKnown = true, error = null) } }
currentValue("KeyBatteryPercentNeededToLand", KeyTools.createKey(FlightControllerKey.KeyBatteryPercentNeededToLand))
?.let { percent: Int -> _state.update { it.copy(batteryPercentNeededToLand = percent, batteryPercentNeededToLandKnown = true, error = null) } }
currentValue("KeyBatteryPercentNeededToGoHome", KeyTools.createKey(FlightControllerKey.KeyBatteryPercentNeededToGoHome))
?.let { percent: Int -> _state.update { it.copy(batteryPercentNeededToGoHome = percent, batteryPercentNeededToGoHomeKnown = true, error = null) } }
currentValue("KeyWindDirection", KeyTools.createKey(FlightControllerKey.KeyWindDirection))
?.let { direction: WindDirection -> _state.update { it.copy(windDirection = direction.value(), error = null) } }
currentValue("KeyWindSpeed", KeyTools.createKey(FlightControllerKey.KeyWindSpeed))
?.let { speed: Int -> _state.update { it.copy(windSpeed = speed, error = null) } }
refreshBatteryValues()
refreshRemoteControllerValues()
}
private fun refreshBatteryValues() {
val aggregationPercent = currentValue(
"KeyChargeRemainingInPercent[AGGREGATION]",
KeyTools.createKey(BatteryKey.KeyChargeRemainingInPercent, ComponentIndexType.AGGREGATION)
)
val mainPercent = currentValue(
"KeyChargeRemainingInPercent[LEFT_OR_MAIN]",
KeyTools.createKey(BatteryKey.KeyChargeRemainingInPercent, ComponentIndexType.LEFT_OR_MAIN)
)
val fcPercent = currentValue("KeyBatteryPowerPercent", KeyTools.createKey(FlightControllerKey.KeyBatteryPowerPercent))
listOf(aggregationPercent, mainPercent, fcPercent)
.firstOrNull { it in 0..100 }
?.let { percent -> _state.update { it.copy(batteryPercent = percent, error = null) } }
currentValue("KeyVoltageMain", KeyTools.createKey(BatteryKey.KeyVoltage, ComponentIndexType.LEFT_OR_MAIN))
?.takeIf { it > 0 }
?.let { voltage -> _state.update { it.copy(batteryVoltageMv = voltage, batteryVoltageKnown = true, error = null) } }
currentValue("KeyVoltageRight", KeyTools.createKey(BatteryKey.KeyVoltage, ComponentIndexType.RIGHT))
?.takeIf { it > 0 && _state.value.batteryVoltageMv <= 0 }
?.let { voltage -> _state.update { it.copy(batteryVoltageMv = voltage, batteryVoltageKnown = true, error = null) } }
currentValue("KeyBatteryFirmwareVersion", KeyTools.createKey(BatteryKey.KeyFirmwareVersion, ComponentIndexType.LEFT_OR_MAIN))
?.takeIf { it.isNotBlank() }
?.let { version -> _state.update { it.copy(batteryFirmwareVersion = version, error = null) } }
currentValue("KeyBatterySerialNumber", KeyTools.createKey(BatteryKey.KeySerialNumber, ComponentIndexType.LEFT_OR_MAIN))
?.takeIf { it.isNotBlank() }
?.let { serial -> _state.update { it.copy(batterySerialNumber = serial, error = null) } }
currentValue("KeyBatteryTemperature", KeyTools.createKey(BatteryKey.KeyBatteryTemperature, ComponentIndexType.LEFT_OR_MAIN))
?.let { temperature -> _state.update { it.copy(batteryTemperature = temperature, batteryTemperatureKnown = true, error = null) } }
currentValue("KeyNumberOfDischarges", KeyTools.createKey(BatteryKey.KeyNumberOfDischarges, ComponentIndexType.LEFT_OR_MAIN))
?.let { times -> _state.update { it.copy(batteryLoopTimes = times, batteryLoopTimesKnown = true, error = null) } }
currentValue("KeyBatteryHighVoltageStorageTime", KeyTools.createKey(BatteryKey.KeyBatteryHighVoltageStorageTime, ComponentIndexType.LEFT_OR_MAIN))
?.let { seconds -> _state.update { it.copy(batteryHighVoltageStorageSeconds = seconds, batteryHighVoltageStorageKnown = true, error = null) } }
}
private fun refreshRemoteControllerValues() {
currentValue("KeyRcGPSInfo", KeyTools.createKey(RemoteControllerKey.KeyRcGPSInfo))
?.let { info: RcGPSInfo ->
val location = info.location
val latitude = location?.latitude ?: 0.0
val longitude = location?.longitude ?: 0.0
val coordinateValid = isValidCoordinate(latitude, longitude)
_state.update {
if (coordinateValid) {
it.copy(
rcLatitude = latitude,
rcLongitude = longitude,
rcLocationValid = true,
rcGpsSatelliteCount = info.satelliteCount,
rcGpsAccuracy = info.accuracy,
error = null
)
} else {
it.copy(rcGpsSatelliteCount = info.satelliteCount, rcGpsAccuracy = info.accuracy, error = null)
}
}
}
currentValue("KeyRcBatteryInfo", KeyTools.createKey(RemoteControllerKey.KeyBatteryInfo))
?.batteryPercent
?.takeIf { it in 0..100 }
?.let { percent -> _state.update { it.copy(rcBatteryPercent = percent, error = null) } }
}
private fun <T> currentValue(name: String, key: DJIKey<T>): T? =
runCatching {
KeyManager.getInstance().getValue(key)
}.onFailure { error ->
Log.d(TelemetryLogTag, "read current $name failed: ${error.message ?: error.toString()}")
}.getOrNull()
private fun listenCameraTelemetry(cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) { private fun listenCameraTelemetry(cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
listenCameraIdentity(cameraIndex) listenCameraIdentity(cameraIndex)
listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? -> listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? ->

View File

@@ -413,12 +413,16 @@ fun DroneControllerScreen() {
} }
} }
DisposableEffect(sdkState.isRegistered) { DisposableEffect(sdkState.isRegistered, sdkState.productConnected) {
if (sdkState.isRegistered) { if (sdkState.isRegistered && sdkState.productConnected) {
telemetryRepository.start() telemetryRepository.start()
warningRepository.start()
} else { } else {
telemetryRepository.stop() telemetryRepository.stop()
}
if (sdkState.isRegistered) {
warningRepository.start()
} else {
warningRepository.stop() warningRepository.stop()
warningRepository.clear() warningRepository.clear()
} }