diff --git a/app/src/main/java/com/kiwii/controlpanel/data/BridgeRepository.kt b/app/src/main/java/com/kiwii/controlpanel/data/BridgeRepository.kt index 228027e..9f5cd03 100644 --- a/app/src/main/java/com/kiwii/controlpanel/data/BridgeRepository.kt +++ b/app/src/main/java/com/kiwii/controlpanel/data/BridgeRepository.kt @@ -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 { "" } diff --git a/app/src/main/java/com/kiwii/controlpanel/data/ComponentRegistry.kt b/app/src/main/java/com/kiwii/controlpanel/data/ComponentRegistry.kt index 1e0f75a..6323557 100644 --- a/app/src/main/java/com/kiwii/controlpanel/data/ComponentRegistry.kt +++ b/app/src/main/java/com/kiwii/controlpanel/data/ComponentRegistry.kt @@ -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}"