feat(cloud): 优化云端MQTT服务的设备在线状态管理
- 添加了设备身份变更检测和处理机制 - 实现了离线拓扑的防抖延迟发布功能 - 优化了设备上下线状态的同步逻辑 - 修复了设备序列号获取和更新的问题 - 改进了飞行器连接状态的判断策略 - 添加了遥测数据的实时值刷新机制 - 优化了UI界面中的遥测启动停止逻辑
This commit is contained in:
@@ -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,8 +1009,56 @@ 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)
|
||||||
return JSONObject()
|
return JSONObject()
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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? ->
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user