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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
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 CloudAccessType = "msdk"
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_REASON_MAX_INFLIGHT = 32202
private const val MQTT_PUBLISH_CONGESTION_COOLDOWN_MS = 5_000L
@@ -98,6 +101,9 @@ class CloudMqttService(
private var warningHmsKey = ""
private var warningDebugRaw = ""
private var hmsDebugKey = ""
private var lastOnlineIdentity: CloudDeviceIdentity? = null
private var pendingOfflineTopoJob: Job? = null
private var pendingOfflineIdentity: CloudDeviceIdentity? = null
private var lastOsdAt = 0L
@Volatile
private var lastPublishCongestedAt = 0L
@@ -197,7 +203,11 @@ class CloudMqttService(
fun disconnect() {
scope.launch {
runCatching { publishStatusOnline(online = false) }
cancelPendingOfflineTopo()
runCatching {
publishStatusOnline(online = false, identity = lastOnlineIdentity ?: deviceIdentity)
}
lastOnlineIdentity = null
closeCurrentClient()
_state.update {
it.copy(
@@ -211,21 +221,42 @@ class CloudMqttService(
}
fun clear() {
runCatching { publishStatusOnline(online = false) }
cancelPendingOfflineTopo()
runCatching {
publishStatusOnline(online = false, identity = lastOnlineIdentity ?: deviceIdentity)
}
lastOnlineIdentity = null
closeCurrentClient()
scope.cancel()
}
fun updateDeviceState(sdkState: DroneSdkState) {
deviceIdentity = CloudDeviceIdentity(
val previousIdentity = deviceIdentity
val nextIdentity = CloudDeviceIdentity(
remoteControllerSn = sdkState.remoteControllerSerialNumber,
aircraftSn = sdkState.aircraftSerialNumber,
productConnected = sdkState.productConnected,
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 {
subscribeControlTopicsIfReady()
publishOnlineIfReady(force = false)
publishOnlineIfReady(force = identityChanged && nextIdentity.readyForOnline && nextIdentity.productConnected)
publishLiveCapacityIfReady(deviceIdentity, force = false)
publishHmsIfReady(force = false)
}
@@ -355,18 +386,21 @@ class CloudMqttService(
publishReply(request, result.success, result.message)
}
private fun publishOnlineIfReady(force: Boolean) {
if (!isConnected()) return
private fun publishOnlineIfReady(force: Boolean): Boolean {
if (!isConnected()) return false
val identity = deviceIdentity
if (!identity.readyForOnline) return
if (!identity.readyForOnline || !identity.productConnected) return false
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${session?.workspaceId}"
if (!force && onlineKey == key) return
publishStatusOnline(online = true)
if (!force && onlineKey == key) return true
if (!publishStatusOnline(online = true, identity = identity)) return false
onlineKey = key
lastOnlineIdentity = identity
cancelPendingOfflineTopo(identity)
val now = System.currentTimeMillis()
_state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") }
publishLiveCapacityIfReady(identity, force = true)
bindDevices(identity)
return true
}
private fun bindDevices(identity: CloudDeviceIdentity) {
@@ -379,11 +413,10 @@ class CloudMqttService(
}
}
private fun publishStatusOnline(online: Boolean) {
val cloudSession = session ?: return
val identity = deviceIdentity
private fun publishStatusOnline(online: Boolean, identity: CloudDeviceIdentity = deviceIdentity): Boolean {
val cloudSession = session ?: return false
val rcSn = identity.remoteControllerSn
if (rcSn.isBlank()) return
if (rcSn.isBlank()) return false
val subDevices = JSONArray()
if (online && identity.aircraftSn.isNotBlank()) {
val droneDescriptor = telemetry.cloudDroneDescriptor()
@@ -410,7 +443,7 @@ class CloudMqttService(
.put("access_type", CloudAccessType)
.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) {
@@ -627,7 +660,8 @@ class CloudMqttService(
private fun publishOsdIfReady(force: Boolean) {
if (!isConnected()) return
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 shouldPublish = synchronized(osdThrottleLock) {
when {
@@ -947,6 +981,7 @@ class CloudMqttService(
}
private fun closeCurrentClient() {
cancelPendingOfflineTopo()
val oldClient = client
client = null
resetPublishState()
@@ -974,7 +1009,55 @@ class CloudMqttService(
warningHmsKey = ""
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 {
val hmsCode = code.toHmsCodeOrFallback(message)

View File

@@ -238,12 +238,20 @@ object DroneSdkManager {
}
private fun readAircraftConnectionProbe(): AircraftConnectionProbe {
val flightControllerConnected = runCatching {
KeyManager.getInstance().getValue(
KeyTools.createKey(FlightControllerKey.KeyConnection),
false
)
}.getOrNull()
val rcAircraftState = runCatching {
KeyManager.getInstance().getValue(
KeyTools.createKey(RemoteControllerKey.KeyRcMultiDeviceAircraftState)
)
}.getOrNull()
val signals = readAircraftRealtimeSignals()
if (rcAircraftState != null) {
val mainStates = listOf(
rcAircraftState.sdrConnectState,
@@ -257,9 +265,35 @@ object DroneSdkManager {
if (!connectedByRc) {
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 serialNumber = runCatching {
KeyManager.getInstance().getValue(
@@ -285,11 +319,7 @@ object DroneSdkManager {
}.getOrDefault(-1)
if (batteryPercent in 0..100) signals += "电量:$batteryPercent%"
val connected = signals.size >= 2
return AircraftConnectionProbe(
connected = connected,
detail = if (signals.isEmpty()) "未读到飞控实时信号" else signals.joinToString(" / ")
)
return signals
}
private fun isAircraftLinkActive(state: RcMutilDeviceState?): Boolean =
@@ -301,6 +331,16 @@ object DroneSdkManager {
detail: String
) {
_state.update { current ->
val aircraftSerialNumber = if (aircraftConnected) {
readAircraftSerialNumber().ifBlank { current.aircraftSerialNumber }
} else {
""
}
val remoteControllerSerialNumber = if (productLinkConnected) {
readRemoteControllerSerialNumber().ifBlank { current.remoteControllerSerialNumber }
} else {
""
}
val event = when {
current.productConnected == aircraftConnected &&
current.productLinkConnected == productLinkConnected -> current.initEvent
@@ -313,8 +353,8 @@ object DroneSdkManager {
productConnected = aircraftConnected,
productLinkConnected = productLinkConnected,
aircraftConnectionDetail = detail,
aircraftSerialNumber = if (aircraftConnected) readAircraftSerialNumber() else "",
remoteControllerSerialNumber = if (productLinkConnected) readRemoteControllerSerialNumber() else ""
aircraftSerialNumber = aircraftSerialNumber,
remoteControllerSerialNumber = remoteControllerSerialNumber
)
}
}

View File

@@ -69,6 +69,7 @@ import kotlinx.coroutines.flow.update
private const val TelemetryLogTag = "ZklhTelemetry"
private const val RTK_START_RETRY_INTERVAL_MS = 20_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
// enabled-but-unfixed M350 RTK module prevents waypoint takeoff.
private const val AutoManageRtk = false
@@ -264,12 +265,14 @@ class TelemetryRepository(
private var lastRtkStartAtMs = 0L
private var lastRtkStartSource = RTKReferenceStationSource.UNKNOWN
private var lastRtkBootstrapAtMs = 0L
private var lastCurrentValueRefreshAtMs = 0L
private var lastRtkSystemState: RTKSystemState? = null
fun start() {
if (started) return
started = true
Log.d(TelemetryLogTag, "start telemetry listeners")
listenConnectionTelemetry()
listenSafely("KeyAircraftLocation", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation)) { location: LocationCoordinate2D? ->
location ?: return@listenSafely
Log.d(
@@ -282,16 +285,18 @@ class TelemetryRepository(
location ?: return@listenSafely
val latitude = location.latitude ?: 0.0
val longitude = location.longitude ?: 0.0
val altitude = location.altitude
val valid = isValidCoordinate(latitude, longitude)
Log.d(
TelemetryLogTag,
"KeyAircraftLocation3D lat=$latitude lon=$longitude rawAltitude=${location.altitude} valid=$valid"
"KeyAircraftLocation3D lat=$latitude lon=$longitude rawAltitude=$altitude valid=$valid"
)
_state.update {
it.copy(
latitude = latitude,
longitude = longitude,
locationValid = valid,
altitude = altitude ?: it.altitude,
error = null
)
}
@@ -581,6 +586,7 @@ class TelemetryRepository(
listenAdditionalCameraIdentities()
listenGimbalTelemetry()
listenRtkTelemetry()
refreshCurrentValues("telemetry_start", force = true)
}
fun stop() {
@@ -594,6 +600,7 @@ class TelemetryRepository(
runCatching { rtkCenter.rtkStationManager.removeSearchRTKStationListener(searchRtkStationListener) }
runCatching { rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(rtkStationConnectStatusListener) }
runCatching { rtkCenter.rtkStationManager.removeConnectedRTKStationInfoListener(connectedRtkStationInfoListener) }
lastCurrentValueRefreshAtMs = 0L
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) {
listenCameraIdentity(cameraIndex)
listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? ->

View File

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