From 4fd240deabde986e4fa0f6c21843fed382e95cb0 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 3 Aug 2026 09:38:46 +0200 Subject: [PATCH] test(android): make the pad-audio self test reachable without a host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self test shipped in the previous commit was gated behind a capture, which needs a stream, which needs a host — so it depended on precisely the thing it exists to rule out. It could not have been run in the situation that motivated it. It is now a "Test haptics" button on the DualSense passthrough card in Settings → Controllers → Connected controllers, which is reachable with no session at all. It opens its OWN connection to the pad — the same rule the renderer follows, and the rule whose violation caused the fault this test looks for — runs the tone on a worker thread, and reports a plain-language result: which of open / write / no-data failed, or how many frames reached the pad. The debug-property trigger stays for the in-session case; this is the one that answers "can this phone drive this pad at all" before a host is even involved. --- .../io/unom/punktfunk/ControllersScreen.kt | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt index 239e80d0..6c56bee5 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ControllersScreen.kt @@ -410,17 +410,68 @@ private fun DsRow(usbDev: android.hardware.usb.UsbDevice) { Text("Grant USB access") } } - else -> Text( - if (model == DsDevice.Model.DUALSHOCK4) { - "Ready — captured at stream start: rumble, lightbar and gyro are " + - "driven directly." - } else { - "Ready — captured at stream start: rumble, adaptive triggers, lightbar " + - "and gyro are driven directly." - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + else -> { + Text( + if (model == DsDevice.Model.DUALSHOCK4) { + "Ready — captured at stream start: rumble, lightbar and gyro are " + + "driven directly." + } else { + "Ready — captured at stream start: rumble, adaptive triggers, lightbar " + + "and gyro are driven directly." + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + // Pad-audio self test. Deliberately reachable WITHOUT a stream: it exists to + // answer "can this phone drive this pad's audio endpoint at all", and gating + // that behind a live session would make it depend on the very thing one wants + // to rule out when a session misbehaves. DualSense only — the DS4 has no + // 4-channel haptics device. + if (model != DsDevice.Model.DUALSHOCK4) { + var testing by remember { mutableStateOf(false) } + var result by remember { mutableStateOf(null) } + result?.let { + Text( + it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + OutlinedButton( + enabled = !testing, + onClick = { + testing = true + result = null + Thread({ + // Its OWN connection: the renderer's descriptor must never be + // shared with another transfer engine, and that applies to + // this test as much as to the real path. + val conn = runCatching { usbManager.openDevice(usbDev) }.getOrNull() + val fd = conn?.fileDescriptor ?: -1 + val r = if (fd >= 0) { + io.unom.punktfunk.kit.NativeBridge.nativePadAudioSelfTest(fd, 3, 60) + } else { + -1 + } + conn?.close() + val msg = when { + r > 0 -> "Haptics test passed — $r frames to the pad." + r == -1 -> "Could not open the pad's audio interface. " + + "Some kernels refuse it; the pad still works normally." + r == -2 -> "The audio stream stopped part-way." + else -> "The stream opened but no audio reached the pad." + } + android.os.Handler(android.os.Looper.getMainLooper()).post { + result = msg + testing = false + } + }, "pf-pad-selftest-ui").start() + }, + ) { + Text(if (testing) "Testing…" else "Test haptics") + } + } + } } } }