diff --git a/sample/src/main/java/com/zklh/dronecontroller/core/cloud/CloudMqttService.kt b/sample/src/main/java/com/zklh/dronecontroller/core/cloud/CloudMqttService.kt index 99c36b3..0434a1e 100644 --- a/sample/src/main/java/com/zklh/dronecontroller/core/cloud/CloudMqttService.kt +++ b/sample/src/main/java/com/zklh/dronecontroller/core/cloud/CloudMqttService.kt @@ -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,8 +1009,56 @@ 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) return JSONObject() diff --git a/sample/src/main/java/com/zklh/dronecontroller/core/msdk/DroneSdkManager.kt b/sample/src/main/java/com/zklh/dronecontroller/core/msdk/DroneSdkManager.kt index b2c9397..0380743 100644 --- a/sample/src/main/java/com/zklh/dronecontroller/core/msdk/DroneSdkManager.kt +++ b/sample/src/main/java/com/zklh/dronecontroller/core/msdk/DroneSdkManager.kt @@ -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 { val signals = mutableListOf() 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 ) } } diff --git a/sample/src/main/java/com/zklh/dronecontroller/core/telemetry/TelemetryRepository.kt b/sample/src/main/java/com/zklh/dronecontroller/core/telemetry/TelemetryRepository.kt index 4ea9c9b..53bd6fa 100644 --- a/sample/src/main/java/com/zklh/dronecontroller/core/telemetry/TelemetryRepository.kt +++ b/sample/src/main/java/com/zklh/dronecontroller/core/telemetry/TelemetryRepository.kt @@ -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 currentValue(name: String, key: DJIKey): 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? -> diff --git a/sample/src/main/java/com/zklh/dronecontroller/ui/DroneControllerScreen.kt b/sample/src/main/java/com/zklh/dronecontroller/ui/DroneControllerScreen.kt index e083080..56a7309 100644 --- a/sample/src/main/java/com/zklh/dronecontroller/ui/DroneControllerScreen.kt +++ b/sample/src/main/java/com/zklh/dronecontroller/ui/DroneControllerScreen.kt @@ -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() }