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 { 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 bind(activity: Activity): Boolean = KiwiiRuntimeClientBridge.bind(activity)
fun unbind() = KiwiiRuntimeClientBridge.unbind() fun unbind() = KiwiiRuntimeClientBridge.unbind()
fun isBound(): Boolean = KiwiiRuntimeClientBridge.isBound() fun isBound(): Boolean = KiwiiRuntimeClientBridge.isBound()
@@ -130,7 +140,15 @@ class BridgeRepository {
fun submitRightHeStream(streamId: Int, payload: String): String = fun submitRightHeStream(streamId: Int, payload: String): String =
KiwiiRuntimeClientBridge.submitRightHeStream(streamId, buildHeStreamPatternJson(streamId, payload)) 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 setMotorWeightKg(weight: Float): com.kiwii.bridge.MotorCommandResult = KiwiiRuntimeClientBridge.setMotorWeightKg(weight)
fun setMotorTrainingMode(mode: Int, param: Float): String = KiwiiRuntimeClientBridge.setMotorTrainingMode(mode, param) 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(). * Important design decision: this UI path must NOT call raw getDongleStateJson().
* On current RuntimeHost builds that raw dongle query can block behind debug/global * 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. * diagnostic view and must remain responsive.
*/ */
fun getDongleDetailStateJson(): String { fun getDongleDetailStateJson(): String {
val handleRaw = try { // Phase4B-v11.6: readDongleState and Dongle detail State must be zero-blocking.
getHandleStateJson() // Do not call getDongleStateJson() or getHandleStateJson() here. Both are binder calls
} catch (t: Throwable) { // 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() 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() .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 { private fun buildSafeDonglePlaceholderJson(): String {
return JSONObject() 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("source", "SDK_Panel.non-blocking-dongle-detail")
.put("rawDongleQuery", "disabled-on-detail-ui-path") .put("rawDongleQuery", "disabled-on-detail-ui-path")
.put("transport", JSONObject() .put("transport", JSONObject()
.put("state", "NOT_QUERIED") .put("state", "NOT_QUERIED")
.put("rawDongleQuery", "disabled-on-detail-ui-path") .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() .toString()
} }
private fun buildDongleDetailStateJson(dongleRaw: String, handleRaw: String): String { private fun buildDongleDetailStateJson(dongleRaw: String, handleRaw: String, handleCacheAgeMs: Long): String {
val dongle = parseJsonOrEmpty(dongleRaw) val dongle = parseJsonOrEmpty(dongleRaw)
val handle = parseJsonOrEmpty(handleRaw) val handle = parseJsonOrEmpty(handleRaw)
val out = JSONObject() 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("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("generatedAtMs", System.currentTimeMillis())
out.put("dongle", dongle) out.put("dongle", dongle)
out.put("handleState", handle) out.put("handleState", handle)
@@ -295,7 +330,7 @@ class BridgeRepository {
slot.put("connected", false) slot.put("connected", false)
slot.put("source", "missing-slot-report") slot.put("source", "missing-slot-report")
slot.put("inferred", false) 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) slots.put(slot)
@@ -314,7 +349,7 @@ class BridgeRepository {
it.optString("status", "").equals("CONNECTED", ignoreCase = true) it.optString("status", "").equals("CONNECTED", ignoreCase = true)
} ?: false } ?: false
return if (rightFresh && !reportedSlot1Connected) { return if (rightFresh && !reportedSlot1Connected) {
"Slot 1 inferred from Right Handle IMU; raw slot not queried." "Slot 1 inferred from cached Right Handle data."
} else { } else {
"" ""
} }
@@ -30,7 +30,7 @@ class ComponentRegistry(
const val HANDLE_DATA_AGE_FRESH_TIMEOUT_MS = 1500L const val HANDLE_DATA_AGE_FRESH_TIMEOUT_MS = 1500L
const val HANDLE_SEQUENCE_PROGRESS_TIMEOUT_MS = 2500L const val HANDLE_SEQUENCE_PROGRESS_TIMEOUT_MS = 2500L
const val DONGLE_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( 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 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 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 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 handleSignals = updateHandleSignals(bound, handleJson, dongleJson)
val result = ComponentType.entries.associateWith { type -> val result = ComponentType.entries.associateWith { type ->
@@ -289,7 +294,7 @@ class ComponentRegistry(
Log.i( Log.i(
TAG, TAG,
"Phase4B-v6 dashboard availability; bound=$bound; " + "Phase4B-v11.6 dashboard availability; bound=$bound; " +
"camera=${states[ComponentType.CAMERA]}; right=${states[ComponentType.RIGHT_HANDLE]}; " + "camera=${states[ComponentType.CAMERA]}; right=${states[ComponentType.RIGHT_HANDLE]}; " +
"dongle=${states[ComponentType.DONGLE]}; telemetry=${states[ComponentType.TELEMETRY_SAFETY]}; motor=${states[ComponentType.MOTOR]}; " + "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}; " + "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}", result = "${oldState.name} -> ${newState.name}",
isStateChange = true 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 val active = liveFlags && dataFresh && sequenceProgressFresh && hasPayload
Log.i( Log.i(
TAG, 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)}; " + "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" "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 latestDongleCardStateForMotorGate = state
Log.i( Log.i(
TAG, 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}; " + "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}; " + "rightFresh=$rightFresh; leftFresh=$leftFresh; recentHandleProgress=$recentHandleProgress; dongleProgressAgeMs=${printAge(handles.dongleProgressAgeMs)}; decision=${state.name}; " +
"rule=$DONGLE_RULE; evidence=${handles.dongle.evidence}" "rule=$DONGLE_RULE; evidence=${handles.dongle.evidence}"