Avoid blocking reads in SDK panel dongle diagnostics
This commit is contained in:
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user