Gate motor status on dongle and fresh telemetry
This commit is contained in:
Binary file not shown.
@@ -16,6 +16,15 @@ class BridgeRepository {
|
||||
fun getRuntimeStateJson(): String = KiwiiRuntimeClientBridge.getRuntimeStateJson()
|
||||
fun getHealthStateJson(): String = KiwiiRuntimeClientBridge.getHealthStateJson()
|
||||
fun getDebugSnapshotJson(): String = KiwiiRuntimeClientBridge.getDebugSnapshotJson()
|
||||
fun getDongleStateJson(): String {
|
||||
return try {
|
||||
val method = KiwiiRuntimeClientBridge::class.java.getMethod("getDongleStateJson")
|
||||
method.invoke(null) as? String ?: getDebugSnapshotJson()
|
||||
} catch (_: Throwable) {
|
||||
// Backward-compatible fallback for old unity-bridge.aar builds.
|
||||
getDebugSnapshotJson()
|
||||
}
|
||||
}
|
||||
fun getRuntimeLifecycleStateJson(): String = KiwiiRuntimeClientBridge.getRuntimeLifecycleStateJson()
|
||||
fun requestRuntimeWarmStart(reason: String = ""): String = KiwiiRuntimeClientBridge.requestRuntimeWarmStart(reason)
|
||||
|
||||
@@ -27,15 +36,23 @@ class BridgeRepository {
|
||||
fun getLatestPoseFrameJson(): String = KiwiiRuntimeClientBridge.getLatestPoseFrameJson()
|
||||
fun getLatestBodyPoseFrameJson(): String = KiwiiRuntimeClientBridge.getLatestBodyPoseFrameJson()
|
||||
|
||||
fun getLeftHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getLeftHandleIMULatest()
|
||||
fun getLeftHandleIMUQueue(): Array<FloatArray> = KiwiiRuntimeClientBridge.getLeftHandleIMUQueue()
|
||||
fun getLeftHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getLatestLeftHandleIMU()
|
||||
fun getLeftHandleIMUQueue(): Array<FloatArray> {
|
||||
val latest = KiwiiRuntimeClientBridge.getLatestLeftHandleIMU()
|
||||
return if (latest.isEmpty()) emptyArray() else arrayOf(latest)
|
||||
}
|
||||
fun submitLeftStaticEffect(effectId: Int): String = KiwiiRuntimeClientBridge.submitLeftStaticEffect(effectId)
|
||||
fun submitLeftHeStream(streamId: Int, payload: String): String = KiwiiRuntimeClientBridge.submitLeftHeStream(streamId, payload)
|
||||
fun submitLeftHeStream(streamId: Int, payload: String): String =
|
||||
KiwiiRuntimeClientBridge.submitLeftHeStream(buildHeStreamPatternJson(streamId, payload))
|
||||
|
||||
fun getRightHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getRightHandleIMULatest()
|
||||
fun getRightHandleIMUQueue(): Array<FloatArray> = KiwiiRuntimeClientBridge.getRightHandleIMUQueue()
|
||||
fun getRightHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getLatestRightHandleIMU()
|
||||
fun getRightHandleIMUQueue(): Array<FloatArray> {
|
||||
val latest = KiwiiRuntimeClientBridge.getLatestRightHandleIMU()
|
||||
return if (latest.isEmpty()) emptyArray() else arrayOf(latest)
|
||||
}
|
||||
fun submitRightStaticEffect(effectId: Int): String = KiwiiRuntimeClientBridge.submitRightStaticEffect(effectId)
|
||||
fun submitRightHeStream(streamId: Int, payload: String): String = KiwiiRuntimeClientBridge.submitRightHeStream(streamId, payload)
|
||||
fun submitRightHeStream(streamId: Int, payload: String): String =
|
||||
KiwiiRuntimeClientBridge.submitRightHeStream(buildHeStreamPatternJson(streamId, payload))
|
||||
|
||||
fun getHandleStateJson(): String = KiwiiRuntimeClientBridge.getHandleStateJson()
|
||||
|
||||
@@ -51,6 +68,13 @@ class BridgeRepository {
|
||||
fun submitDeviceCommandDryRun(command: String): String = KiwiiRuntimeClientBridge.submitDeviceCommandDryRun(command)
|
||||
fun getDeviceCommandStateJson(): String = KiwiiRuntimeClientBridge.getDeviceCommandStateJson()
|
||||
|
||||
private fun buildHeStreamPatternJson(streamId: Int, payload: String): String {
|
||||
val trimmed = payload.trim()
|
||||
if (trimmed.startsWith("{")) return trimmed
|
||||
val escaped = payload.replace("\\", "\\\\").replace("\"", "\\\"")
|
||||
return "{\"streamId\":$streamId,\"payload\":\"$escaped\"}"
|
||||
}
|
||||
|
||||
suspend fun <T> callAsync(block: () -> T): OperationResult<T> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val result = block()
|
||||
|
||||
@@ -25,6 +25,16 @@ class ComponentRegistry(
|
||||
|
||||
private var previousStates = emptyMap<ComponentType, ComponentState>()
|
||||
|
||||
private companion object {
|
||||
const val MOTOR_TELEMETRY_FRESH_TIMEOUT_MS = 5000L
|
||||
}
|
||||
|
||||
// Motor is physically downstream of the SmartBase Dongle.
|
||||
// Keep the latest Dongle card decision from the same detection loop so Motor cannot remain green
|
||||
// after the Dongle transport is disconnected, while preserving v4 Dongle detection behavior.
|
||||
@Volatile
|
||||
private var latestDongleCardStateForMotorGate: ComponentState = ComponentState.GREY
|
||||
|
||||
fun startDetection() {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
@@ -95,11 +105,12 @@ class ComponentRegistry(
|
||||
private fun dumpDebugOnce() {
|
||||
if (debugDumped || !repo.isBound()) return
|
||||
debugDumped = true
|
||||
// 首页首次检测不要调用 getDebugSnapshotJson / camera / motor 等重 API。
|
||||
// 这些 API 会触发 native snapshot 或 camera state 路径;RuntimeHost 刚启动时容易与
|
||||
// native init/camera warm-start 形成 Binder 等待,导致 NOT_BOUND 或 service ANR。
|
||||
val apis = mapOf(
|
||||
"getCameraStateJson" to runCatching { repo.getCameraStateJson() }.getOrDefault("EXCEPTION"),
|
||||
"getLatestMotorStateJson" to runCatching { repo.getLatestMotorStateJson() }.getOrDefault("EXCEPTION"),
|
||||
"getDebugSnapshotJson(first200)" to runCatching { repo.getDebugSnapshotJson().take(200) }.getOrDefault("EXCEPTION"),
|
||||
"getHandleStateJson" to runCatching { repo.getHandleStateJson() }.getOrDefault("EXCEPTION"),
|
||||
"isBound" to repo.isBound().toString(),
|
||||
"getDongleStateJson(first400)" to runCatching { repo.getDongleStateJson().take(400) }.getOrDefault("EXCEPTION")
|
||||
)
|
||||
for ((name, value) in apis) {
|
||||
logger.log(LogEntry(
|
||||
@@ -153,12 +164,32 @@ class ComponentRegistry(
|
||||
}
|
||||
|
||||
private fun checkDongle(): ComponentState {
|
||||
val state = computeDongleState()
|
||||
latestDongleCardStateForMotorGate = state
|
||||
return state
|
||||
}
|
||||
|
||||
private fun computeDongleState(): ComponentState {
|
||||
return try {
|
||||
// Dongle 是 USB 外设,状态在 getDebugSnapshotJson 的全量数据中
|
||||
val json = repo.getDebugSnapshotJson()
|
||||
val json = repo.getDongleStateJson()
|
||||
if (isUnavailable(json)) return ComponentState.GREY
|
||||
// dongleState 字段存在说明 Dongle USB 已连接并被 RuntimeHost 识别
|
||||
if (json.contains("\"dongleState\"")) {
|
||||
val obj = JSONObject(json)
|
||||
val transport = obj.optJSONObject("transport") ?: return ComponentState.GREY
|
||||
|
||||
val active = obj.optBoolean("active", false) || obj.optBoolean("available", false) || obj.optBoolean("connected", false)
|
||||
val transportActive = transport.optBoolean("active", false) || transport.optBoolean("connected", false)
|
||||
val usbBridgeRegistered = transport.optBoolean("usbBridgeRegistered", false)
|
||||
val readLoopRunning = transport.optBoolean("readLoopRunning", false)
|
||||
val usbPresent = transport.optBoolean("usbPresent", false)
|
||||
val state = transport.optString("state", "")
|
||||
|
||||
// Dongle 首页卡片表达“USB transport 已经被 RuntimeHost 打开并注册”,
|
||||
// 不要求 lastUsbInAgeMs < 3s。是否有上行数据滞后由 detail page 的 dataStale/lastUsbInAgeMs 显示。
|
||||
if ((active || transportActive || state == "CONNECTED") &&
|
||||
usbPresent &&
|
||||
usbBridgeRegistered &&
|
||||
readLoopRunning
|
||||
) {
|
||||
ComponentState.ACTIVE
|
||||
} else {
|
||||
ComponentState.GREY
|
||||
@@ -170,11 +201,23 @@ class ComponentRegistry(
|
||||
|
||||
private fun checkMotor(): ComponentState {
|
||||
return try {
|
||||
// Motor is physically downstream of the SmartBase Dongle.
|
||||
// V7 requires fresh motor telemetry; stale cached telemetry after Dongle re-plug must not turn Motor green.
|
||||
if (latestDongleCardStateForMotorGate != ComponentState.ACTIVE) return ComponentState.GREY
|
||||
|
||||
val json = repo.getLatestMotorStateJson()
|
||||
if (isUnavailable(json)) return ComponentState.GREY
|
||||
// motor-runtime-state.v1 has top-level "available" field
|
||||
|
||||
val obj = JSONObject(json)
|
||||
if (obj.optBoolean("available", false)) ComponentState.ACTIVE else ComponentState.GREY
|
||||
if (obj.optString("contractVersion", "") != "kiwii.motor-runtime-state.v1") return ComponentState.GREY
|
||||
if (obj.optBoolean("blockedByDongleTransport", false)) return ComponentState.GREY
|
||||
if (obj.optBoolean("blockedByMotorTelemetry", false)) return ComponentState.GREY
|
||||
if (!obj.optBoolean("available", false)) return ComponentState.GREY
|
||||
|
||||
val dataAgeMs = obj.optLong("dataAgeMs", Long.MAX_VALUE)
|
||||
if (dataAgeMs < 0L || dataAgeMs > MOTOR_TELEMETRY_FRESH_TIMEOUT_MS) return ComponentState.GREY
|
||||
|
||||
ComponentState.ACTIVE
|
||||
} catch (_: Exception) {
|
||||
ComponentState.GREY
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class AarProxyStateSource(private val bridge: BridgeRepository) : RuntimeStateSo
|
||||
ComponentType.LEFT_HANDLE, ComponentType.RIGHT_HANDLE -> bridge.getHandleStateJson()
|
||||
ComponentType.MOTOR -> bridge.getLatestMotorStateJson()
|
||||
ComponentType.BALANCE_BOARD -> bridge.getDebugSnapshotJson()
|
||||
ComponentType.DONGLE -> bridge.getDebugSnapshotJson()
|
||||
ComponentType.DONGLE -> bridge.getDongleStateJson()
|
||||
ComponentType.TELEMETRY_SAFETY -> bridge.getTelemetryStateJson()
|
||||
ComponentType.SESSION_LOG -> "{}"
|
||||
}
|
||||
|
||||
@@ -2,20 +2,44 @@ package com.kiwii.controlpanel.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kiwii.controlpanel.R
|
||||
import com.kiwii.controlpanel.model.ComponentType
|
||||
import com.kiwii.controlpanel.ui.detail.ComponentDetailViewModel
|
||||
|
||||
class DongleOps(context: Context) : BaseOps(context) {
|
||||
class DongleOps(
|
||||
context: Context,
|
||||
private val viewModel: ComponentDetailViewModel
|
||||
) : BaseOps(context) {
|
||||
|
||||
override fun createView(): View {
|
||||
val layout = verticalLayout()
|
||||
layout.addView(TextView(context).apply {
|
||||
text = "暂无 SDK API"
|
||||
textSize = 12f
|
||||
setTextColor(ContextCompat.getColor(context, R.color.rhs_subtitle))
|
||||
setPadding(0, 32, 0, 32)
|
||||
|
||||
layout.addView(createSection("Dongle Transport"))
|
||||
layout.addView(createButton("getDongleStateJson") {
|
||||
viewModel.executeOperation(ComponentType.DONGLE, "getDongleStateJson") {
|
||||
viewModel.bridgeRepo.getDongleStateJson()
|
||||
}
|
||||
})
|
||||
|
||||
layout.addView(createButton("getDebugSnapshotJson") {
|
||||
viewModel.executeOperation(ComponentType.DONGLE, "getDebugSnapshotJson") {
|
||||
viewModel.bridgeRepo.getDebugSnapshotJson()
|
||||
}
|
||||
})
|
||||
|
||||
layout.addView(createSection("Transport Probe"))
|
||||
layout.addView(createButton("queryMotorTrainingMode") {
|
||||
viewModel.executeOperation(ComponentType.DONGLE, "queryMotorTrainingMode", "axis=0") {
|
||||
viewModel.bridgeRepo.queryMotorTrainingMode(0)
|
||||
}
|
||||
})
|
||||
|
||||
layout.addView(createSection("Expected fields"))
|
||||
layout.addView(createButton("Inspect transport + slots") {
|
||||
viewModel.executeOperation(ComponentType.DONGLE, "inspectDongleDiagnostics") {
|
||||
viewModel.bridgeRepo.getDongleStateJson()
|
||||
}
|
||||
})
|
||||
|
||||
return layout
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user