Avoid blocking reads in SDK panel dongle diagnostics

This commit is contained in:
2026-06-13 18:51:16 +08:00
parent 6500e0108f
commit 0baf371bbd
2 changed files with 60 additions and 20 deletions
@@ -10,6 +10,16 @@ import org.json.JSONObject
class BridgeRepository {
private companion object {
const val HANDLE_CACHE_MAX_AGE_MS = 5000L
@Volatile
private var cachedHandleStateJson: String = ""
@Volatile
private var cachedHandleStateAtMs: Long = 0L
}
fun bind(activity: Activity): Boolean = KiwiiRuntimeClientBridge.bind(activity)
fun unbind() = KiwiiRuntimeClientBridge.unbind()
fun isBound(): Boolean = KiwiiRuntimeClientBridge.isBound()
@@ -130,7 +140,15 @@ class BridgeRepository {
fun submitRightHeStream(streamId: Int, payload: String): String =
KiwiiRuntimeClientBridge.submitRightHeStream(streamId, buildHeStreamPatternJson(streamId, payload))
fun getHandleStateJson(): String = KiwiiRuntimeClientBridge.getHandleStateJson()
fun getHandleStateJson(): String {
val json = KiwiiRuntimeClientBridge.getHandleStateJson()
if (json.isNotBlank() && !json.contains("\"error\"")) {
cachedHandleStateJson = json
cachedHandleStateAtMs = System.currentTimeMillis()
}
return json
}
fun setMotorWeightKg(weight: Float): com.kiwii.bridge.MotorCommandResult = KiwiiRuntimeClientBridge.setMotorWeightKg(weight)
fun setMotorTrainingMode(mode: Int, param: Float): String = KiwiiRuntimeClientBridge.setMotorTrainingMode(mode, param)
@@ -170,7 +188,7 @@ class BridgeRepository {
/**
* Phase4B-v11.5 SDK Panel safe detail-state view for Dongle.
* Phase4B-v11.6 SDK Panel no-blocking detail-state view for Dongle.
*
* Important design decision: this UI path must NOT call raw getDongleStateJson().
* On current RuntimeHost builds that raw dongle query can block behind debug/global
@@ -184,35 +202,52 @@ class BridgeRepository {
* diagnostic view and must remain responsive.
*/
fun getDongleDetailStateJson(): String {
val handleRaw = try {
getHandleStateJson()
} catch (t: Throwable) {
// Phase4B-v11.6: readDongleState and Dongle detail State must be zero-blocking.
// Do not call getDongleStateJson() or getHandleStateJson() here. Both are binder calls
// and either one can become slow after long idle periods. Use the latest cached
// HandleState populated by the home-card poller; if the cache is stale, return a
// responsive safe state instead of timing out the UI.
val now = System.currentTimeMillis()
val cacheAgeMs = if (cachedHandleStateAtMs > 0L) now - cachedHandleStateAtMs else Long.MAX_VALUE
val handleRaw = if (cachedHandleStateJson.isNotBlank() && cacheAgeMs <= HANDLE_CACHE_MAX_AGE_MS) {
cachedHandleStateJson
} else {
JSONObject()
.put("error", "getHandleStateJson failed: ${escapeJson(t.message ?: t.javaClass.simpleName)}")
.put("error", "no-fresh-cached-handle-state")
.put("handleCacheAgeMs", if (cacheAgeMs == Long.MAX_VALUE) -1L else cacheAgeMs)
.put("detail", "Safe Dongle detail path uses cached HandleState only; live binder query skipped to avoid idle timeout.")
.toString()
}
return buildDongleDetailStateJson(buildSafeDonglePlaceholderJson(), handleRaw)
return buildDongleDetailStateJson(
buildSafeDonglePlaceholderJson(),
handleRaw,
if (cacheAgeMs == Long.MAX_VALUE) -1L else cacheAgeMs
)
}
fun getDongleSafePlaceholderStateJson(): String = buildSafeDonglePlaceholderJson()
private fun buildSafeDonglePlaceholderJson(): String {
return JSONObject()
.put("contractVersion", "kiwii.sdk-panel.dongle-safe-placeholder.v11.5")
.put("contractVersion", "kiwii.sdk-panel.dongle-safe-placeholder.v11.6")
.put("source", "SDK_Panel.non-blocking-dongle-detail")
.put("rawDongleQuery", "disabled-on-detail-ui-path")
.put("transport", JSONObject()
.put("state", "NOT_QUERIED")
.put("rawDongleQuery", "disabled-on-detail-ui-path")
.put("detail", "Raw transport query skipped on safe detail path."))
.put("detail", "Live raw transport query skipped on no-blocking detail path."))
.toString()
}
private fun buildDongleDetailStateJson(dongleRaw: String, handleRaw: String): String {
private fun buildDongleDetailStateJson(dongleRaw: String, handleRaw: String, handleCacheAgeMs: Long): String {
val dongle = parseJsonOrEmpty(dongleRaw)
val handle = parseJsonOrEmpty(handleRaw)
val out = JSONObject()
out.put("contractVersion", "kiwii.sdk-panel.dongle-detail-state.v11.5")
out.put("contractVersion", "kiwii.sdk-panel.dongle-detail-state.v11.6")
out.put("source", "SDK_Panel.safe-handle-state-dongle-detail")
out.put("diagnosticSemantics", "operational-state-first; raw-dongle-query-disabled-on-safe-ui-path; raw-slot-report-secondary")
out.put("diagnosticSemantics", "operational-state-first; no-live-binder-call-on-dongle-detail; raw-slot-report-secondary")
out.put("handleCacheAgeMs", handleCacheAgeMs)
out.put("handleCacheFresh", handleCacheAgeMs in 0..HANDLE_CACHE_MAX_AGE_MS)
out.put("generatedAtMs", System.currentTimeMillis())
out.put("dongle", dongle)
out.put("handleState", handle)
@@ -295,7 +330,7 @@ class BridgeRepository {
slot.put("connected", false)
slot.put("source", "missing-slot-report")
slot.put("inferred", false)
slot.put("detail", if (dongle.optString("rawDongleQuery", "") == "disabled-on-detail-ui-path") "Raw slot not queried" else "No raw slot report")
slot.put("detail", if (dongle.optString("rawDongleQuery", "") == "disabled-on-detail-ui-path") "Raw slot skipped" else "No raw slot report")
}
slots.put(slot)
@@ -314,7 +349,7 @@ class BridgeRepository {
it.optString("status", "").equals("CONNECTED", ignoreCase = true)
} ?: false
return if (rightFresh && !reportedSlot1Connected) {
"Slot 1 inferred from Right Handle IMU; raw slot not queried."
"Slot 1 inferred from cached Right Handle data."
} else {
""
}
@@ -30,7 +30,7 @@ class ComponentRegistry(
const val HANDLE_DATA_AGE_FRESH_TIMEOUT_MS = 1500L
const val HANDLE_SEQUENCE_PROGRESS_TIMEOUT_MS = 2500L
const val DONGLE_SEQUENCE_PROGRESS_TIMEOUT_MS = 2500L
const val DONGLE_RULE = "USB_TRANSPORT_ACTIVE_OR_RECENT_HANDLE_PROGRESS"
const val DONGLE_RULE = "RECENT_HANDLE_PROGRESS_NO_RAW_DONGLE_QUERY"
}
private data class HandleSignal(
@@ -84,7 +84,12 @@ class ComponentRegistry(
val handleJson = if (bound) runCatching { repo.getHandleStateJson() }.getOrDefault("{\"error\":\"handle_state_exception\"}") else "{\"error\":\"NOT_BOUND\"}"
val runtimeStats = if (bound) runCatching { repo.getLatestRuntimeStats() }.getOrDefault(FloatArray(0)) else FloatArray(0)
val cameraJson = if (bound) runCatching { repo.getCameraStateJson() }.getOrDefault("{\"error\":\"camera_state_exception\"}") else "{\"error\":\"NOT_BOUND\"}"
val dongleJson = if (bound) runCatching { repo.getDongleStateJson() }.getOrDefault("{\"error\":\"dongle_state_exception\"}") else "{\"error\":\"NOT_BOUND\"}"
// Phase4B-v11.6: never call raw getDongleStateJson() from the periodic home-card
// detector. After long idle it can block a binder/debug path and indirectly make
// Dongle detail read timeout. Dongle card can still become ACTIVE from recent
// handle progress; raw USB/slot inspection should be moved to a separate
// RuntimeHost non-blocking API later.
val dongleJson = if (bound) repo.getDongleSafePlaceholderStateJson() else "{\"error\":\"NOT_BOUND\"}"
val handleSignals = updateHandleSignals(bound, handleJson, dongleJson)
val result = ComponentType.entries.associateWith { type ->
@@ -289,7 +294,7 @@ class ComponentRegistry(
Log.i(
TAG,
"Phase4B-v6 dashboard availability; bound=$bound; " +
"Phase4B-v11.6 dashboard availability; bound=$bound; " +
"camera=${states[ComponentType.CAMERA]}; right=${states[ComponentType.RIGHT_HANDLE]}; " +
"dongle=${states[ComponentType.DONGLE]}; telemetry=${states[ComponentType.TELEMETRY_SAFETY]}; motor=${states[ComponentType.MOTOR]}; " +
"right.available=${handles.right.available}; right.connected=${handles.right.connected}; right.streaming=${handles.right.streaming}; " +
@@ -320,7 +325,7 @@ class ComponentRegistry(
result = "${oldState.name} -> ${newState.name}",
isStateChange = true
))
Log.i(TAG, "Phase4B-v6 availability_change; component=${type.name}; ${oldState.name}->${newState.name}")
Log.i(TAG, "Phase4B-v11.6 availability_change; component=${type.name}; ${oldState.name}->${newState.name}")
}
}
}
@@ -409,7 +414,7 @@ class ComponentRegistry(
val active = liveFlags && dataFresh && sequenceProgressFresh && hasPayload
Log.i(
TAG,
"Phase4B-v6 checkHandle; side=${signal.side}; available=${signal.available}; connected=${signal.connected}; streaming=${signal.streaming}; " +
"Phase4B-v11.6 checkHandle; side=${signal.side}; available=${signal.available}; connected=${signal.connected}; streaming=${signal.streaming}; " +
"sequenceId=${signal.sequenceId}; queueSize=${signal.queueSize}; dataAgeMs=${signal.dataAgeMs}; progressAgeMs=${printAge(progressAgeMs)}; " +
"dataFresh=$dataFresh; sequenceProgressFresh=$sequenceProgressFresh; decision=${if (active) "ACTIVE" else "GREY"}; rule=REQUIRE_FRESH_DATA_AND_SEQUENCE_PROGRESS"
)
@@ -431,7 +436,7 @@ class ComponentRegistry(
latestDongleCardStateForMotorGate = state
Log.i(
TAG,
"Phase4B-v6 checkDongle; valid=${handles.valid}; dongleStateLocated=${handles.dongleStateLocated}; observationBufferLocated=${handles.observationBufferLocated}; " +
"Phase4B-v11.6 checkDongle; valid=${handles.valid}; dongleStateLocated=${handles.dongleStateLocated}; observationBufferLocated=${handles.observationBufferLocated}; " +
"transportActive=$transportActive; usbPresent=${handles.dongle.usbPresent}; bridge=${handles.dongle.usbBridgeRegistered}; readLoop=${handles.dongle.readLoopRunning}; transportState=${handles.dongle.state}; " +
"rightFresh=$rightFresh; leftFresh=$leftFresh; recentHandleProgress=$recentHandleProgress; dongleProgressAgeMs=${printAge(handles.dongleProgressAgeMs)}; decision=${state.name}; " +
"rule=$DONGLE_RULE; evidence=${handles.dongle.evidence}"