diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 836dd9d..87b08fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ android { } dependencies { - implementation(files("libs/unity-bridge.aar")) + implementation(files("libs/unity-bridge-aar-debug-v1.3.0.aar")) implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.fragment:fragment-ktx:1.6.2") diff --git a/app/libs/unity-bridge-aar-debug-v1.3.0.aar b/app/libs/unity-bridge-aar-debug-v1.3.0.aar new file mode 100644 index 0000000..11380d7 Binary files /dev/null and b/app/libs/unity-bridge-aar-debug-v1.3.0.aar differ diff --git a/app/libs/unity-bridge.aar b/app/libs/unity-bridge.aar deleted file mode 100644 index 8fb521b..0000000 Binary files a/app/libs/unity-bridge.aar and /dev/null differ 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 558bc1a..55c3dad 100644 --- a/app/src/main/java/com/kiwii/controlpanel/data/BridgeRepository.kt +++ b/app/src/main/java/com/kiwii/controlpanel/data/BridgeRepository.kt @@ -36,23 +36,21 @@ class BridgeRepository { fun getLatestPoseFrameJson(): String = KiwiiRuntimeClientBridge.getLatestPoseFrameJson() fun getLatestBodyPoseFrameJson(): String = KiwiiRuntimeClientBridge.getLatestBodyPoseFrameJson() - fun getLeftHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getLatestLeftHandleIMU() + fun getLeftHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getLeftHandleIMULatest() fun getLeftHandleIMUQueue(): Array { - val latest = KiwiiRuntimeClientBridge.getLatestLeftHandleIMU() + val latest = KiwiiRuntimeClientBridge.getLeftHandleIMULatest() 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(buildHeStreamPatternJson(streamId, payload)) + KiwiiRuntimeClientBridge.submitLeftHeStream(streamId, payload) - fun getRightHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getLatestRightHandleIMU() + fun getRightHandleIMULatest(): FloatArray = KiwiiRuntimeClientBridge.getRightHandleIMULatest() fun getRightHandleIMUQueue(): Array { - val latest = KiwiiRuntimeClientBridge.getLatestRightHandleIMU() + val latest = KiwiiRuntimeClientBridge.getRightHandleIMULatest() 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(buildHeStreamPatternJson(streamId, payload)) + KiwiiRuntimeClientBridge.submitRightHeStream(streamId, payload) fun getHandleStateJson(): String = KiwiiRuntimeClientBridge.getHandleStateJson() @@ -68,13 +66,6 @@ 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 callAsync(block: () -> T): OperationResult = withContext(Dispatchers.IO) { try { val result = block() diff --git a/app/src/main/java/com/kiwii/controlpanel/ui/components/LeftHandleOps.kt b/app/src/main/java/com/kiwii/controlpanel/ui/components/LeftHandleOps.kt index 6c0358f..d9e2009 100644 --- a/app/src/main/java/com/kiwii/controlpanel/ui/components/LeftHandleOps.kt +++ b/app/src/main/java/com/kiwii/controlpanel/ui/components/LeftHandleOps.kt @@ -30,16 +30,6 @@ class LeftHandleOps( } }) - layout.addView(createSection("Haptics")) - val effectInput = createParameterInput("Effect ID (int)", android.text.InputType.TYPE_CLASS_NUMBER) - val staticEffectAction = createButton("submitLeftStaticEffect") { - val id = effectInput.text.toString().toIntOrNull() ?: 0 - viewModel.executeOperation(ComponentType.LEFT_HANDLE, "submitLeftStaticEffect", "effectId=$id") { - viewModel.bridgeRepo.submitLeftStaticEffect(id) - } - } - layout.addView(createParameterActionRow(staticEffectAction, effectInput)) - val streamIdInput = createParameterInput("Stream ID (int)", android.text.InputType.TYPE_CLASS_NUMBER) val payloadInput = createParameterInput("Payload (String)", android.text.InputType.TYPE_CLASS_TEXT) val heStreamAction = createButton("submitLeftHeStream") { diff --git a/app/src/main/java/com/kiwii/controlpanel/ui/components/RightHandleOps.kt b/app/src/main/java/com/kiwii/controlpanel/ui/components/RightHandleOps.kt index ffa2b05..93b6859 100644 --- a/app/src/main/java/com/kiwii/controlpanel/ui/components/RightHandleOps.kt +++ b/app/src/main/java/com/kiwii/controlpanel/ui/components/RightHandleOps.kt @@ -30,16 +30,6 @@ class RightHandleOps( } }) - layout.addView(createSection("Haptics")) - val effectInput = createParameterInput("Effect ID (int)", android.text.InputType.TYPE_CLASS_NUMBER) - val staticEffectAction = createButton("submitRightStaticEffect") { - val id = effectInput.text.toString().toIntOrNull() ?: 0 - viewModel.executeOperation(ComponentType.RIGHT_HANDLE, "submitRightStaticEffect", "effectId=$id") { - viewModel.bridgeRepo.submitRightStaticEffect(id) - } - } - layout.addView(createParameterActionRow(staticEffectAction, effectInput)) - val streamIdInput = createParameterInput("Stream ID (int)", android.text.InputType.TYPE_CLASS_NUMBER) val payloadInput = createParameterInput("Payload (String)", android.text.InputType.TYPE_CLASS_TEXT) val heStreamAction = createButton("submitRightHeStream") { diff --git a/app/src/main/java/com/kiwii/controlpanel/ui/detail/ComponentDetailFragment.kt b/app/src/main/java/com/kiwii/controlpanel/ui/detail/ComponentDetailFragment.kt index a144424..7023f63 100644 --- a/app/src/main/java/com/kiwii/controlpanel/ui/detail/ComponentDetailFragment.kt +++ b/app/src/main/java/com/kiwii/controlpanel/ui/detail/ComponentDetailFragment.kt @@ -143,7 +143,7 @@ class ComponentDetailFragment : Fragment() { viewModel.stateSnapshot.collect { snapshot -> if (snapshot != null) { latestStateText = JsonFormatter.format(snapshot.data) - val items = RuntimeHostStatusParser.parse(snapshot.data, viewModel.bridgeRepo.isBound()) + val items = parseStateItems(snapshot.data) .ifEmpty { listOf(StatItem("State", "Updated")) } stateAdapter.submitList(items) val sdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) @@ -179,6 +179,16 @@ class ComponentDetailFragment : Fragment() { } } + private fun parseStateItems(json: String): List { + return if (componentType == ComponentType.MOTOR) { + MotorRuntimeStateParser.parse(json).ifEmpty { + RuntimeHostStatusParser.parse(json, viewModel.bridgeRepo.isBound()) + } + } else { + RuntimeHostStatusParser.parse(json, viewModel.bridgeRepo.isBound()) + } + } + private suspend fun observeLogs() { viewModel.logEntries.collect { entries -> val visibleEntries = entries.takeLast(80).ifEmpty { diff --git a/app/src/main/java/com/kiwii/controlpanel/ui/detail/MotorRuntimeStateParser.kt b/app/src/main/java/com/kiwii/controlpanel/ui/detail/MotorRuntimeStateParser.kt new file mode 100644 index 0000000..e2ce3bd --- /dev/null +++ b/app/src/main/java/com/kiwii/controlpanel/ui/detail/MotorRuntimeStateParser.kt @@ -0,0 +1,48 @@ +package com.kiwii.controlpanel.ui.detail + +import com.kiwii.bridge.MotorRuntimeState +import java.util.Locale + +/** 将 AAR 的 MotorRuntimeState 映射成详情页状态卡片。 */ +object MotorRuntimeStateParser { + + fun parse(json: String): List { + val state = try { + MotorRuntimeState.fromJson(json) + } catch (_: Throwable) { + return emptyList() + } + + return listOf( + StatItem("Available", state.available.toString()), + StatItem("Last Key", formatKey(state.lastKey), "motor uplink key"), + StatItem("Runtime", formatFloat(state.runtimeSec), "s"), + StatItem("Current", formatFloat(state.currentA), "A"), + StatItem("Torque", formatFloat(state.torqueNm), "N.m"), + StatItem("Velocity Rad", formatFloat(state.velocityRadps), "rad/s"), + StatItem("Velocity", formatFloat(state.velocityMps), "m/s"), + StatItem("Position", formatFloat(state.positionRad), "rad"), + StatItem("Extension", formatFloat(state.extensionM), "m"), + StatItem("Voltage", formatFloat(state.voltageV), "V"), + StatItem("Temperature", formatFloat(state.tempC), "C"), + StatItem("Sys", state.sys.toString()), + StatItem("Merr", state.merr.toString()), + StatItem("OBD", state.obd.toString()), + StatItem("Seq ID", state.seqId.toString()), + StatItem("T Data", state.tDataUnixMs.toString(), "Unix ms"), + StatItem("Fault", state.faultSummary.ifBlank { "none" }), + StatItem("Host Arrival", state.hostArrivalNs.toString(), "ns"), + StatItem("Last Upload", state.lastUploadNs.toString(), "ns"), + StatItem("Data Age", state.dataAgeMs.toString(), "ms"), + StatItem("Raw Line", state.rawLine.compact(), "raw debug") + ) + } + + private fun formatKey(key: Int): String = String.format(Locale.US, "0x%02X (%d)", key, key) + + private fun formatFloat(value: Float): String = String.format(Locale.US, "%.4f", value) + + private fun String.compact(): String { + return if (length > 48) "${take(45)}..." else this + } +} diff --git a/app/src/test/java/com/kiwii/controlpanel/MotorRuntimeStateParserTest.kt b/app/src/test/java/com/kiwii/controlpanel/MotorRuntimeStateParserTest.kt new file mode 100644 index 0000000..aa8311c --- /dev/null +++ b/app/src/test/java/com/kiwii/controlpanel/MotorRuntimeStateParserTest.kt @@ -0,0 +1,49 @@ +package com.kiwii.controlpanel + +import com.kiwii.controlpanel.ui.detail.MotorRuntimeStateParser +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class MotorRuntimeStateParserTest { + + @Test + fun `parse maps new motor runtime fields`() { + val json = """ + { + "contractVersion": "kiwii.motor-runtime-state.v1", + "available": true, + "lastKey": 175, + "runtimeSec": 12.5, + "currentA": 1.25, + "torqueNm": 2.5, + "velocityRadps": 3.5, + "velocityMps": 4.5, + "positionRad": 5.5, + "extensionM": 0.25, + "voltageV": 24.0, + "tempC": 36.5, + "sys": 7, + "merr": 8, + "obd": 9, + "seqId": 10, + "tDataUnixMs": 1710000000000, + "faultSummary": "ok", + "hostArrivalNs": 111, + "lastUploadNs": 222, + "dataAgeMs": 333, + "rawLine": "motor raw debug line", + "rawJson": "{\"available\":true}" + } + """.trimIndent() + + val items = MotorRuntimeStateParser.parse(json) + + assertEquals("true", items.first { it.label == "Available" }.value) + assertEquals("0xAF (175)", items.first { it.label == "Last Key" }.value) + assertEquals("12.5000", items.first { it.label == "Runtime" }.value) + assertEquals("ms", items.first { it.label == "Data Age" }.detail) + assertEquals("motor raw debug line", items.first { it.label == "Raw Line" }.value) + assertTrue(items.none { it.label == "Raw Json" }) + } +}