From 2a9a3126a4955dffc40bc04ab88a5ccdb3da77c9 Mon Sep 17 00:00:00 2001 From: Elizabeth Alexander Hunt Date: Thu, 12 Nov 2026 21:41:08 -0800 Subject: GAN: never stall on lost packets; fix packaged Bluetooth runtime Live testing on a GANicE3 (Gen4) showed a move held for 32 s and a deliberate disconnect once 16 moves backed up, both waiting on a move-history reply that never came (inherited from gan-web-bluetooth). - Retry history requests every 300 ms while stalled; after 1.5 s release held moves in order, flag the first with Move.afterGap, and request facelets so consumers can resync. Never disconnect over a backlog; a driver tick runs every 150 ms under a shared lock. - Bundle jdk.security.auth (and the other modules Compose suggests) in the packaged runtime: dbus-java needs it to authenticate to BlueZ. - `--cube monitor`: print facelets only on change, mark gaps, and accept r (reset to solved) / f (refresh) on stdin. Co-Authored-By: Claude Opus 5.5 --- .../kotlin/coffee/liz/rookery/desktop/CubeCli.kt | 23 ++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'desktop/src/main') diff --git a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/CubeCli.kt b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/CubeCli.kt index b183420..171a6d9 100644 --- a/desktop/src/main/kotlin/coffee/liz/rookery/desktop/CubeCli.kt +++ b/desktop/src/main/kotlin/coffee/liz/rookery/desktop/CubeCli.kt @@ -47,6 +47,8 @@ private fun scan(seconds: Long, all: Boolean) = BlueZTransport().use { transport } } +private const val SOLVED_FACELETS = "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB" + private fun monitor(mac: String) = BlueZTransport().use { transport -> runBlocking { println("Connecting to $mac...") @@ -55,14 +57,20 @@ private fun monitor(mac: String) = BlueZTransport().use { transport -> val done = CompletableDeferred() Runtime.getRuntime().addShutdownHook(Thread { runBlocking { withTimeoutOrNull(2000) { cube.disconnect() } } }) var firstMoveHost: Long? = null + var lastFacelets: String? = null val collector = launch(start = CoroutineStart.UNDISPATCHED) { cube.events.collect { e -> when (e) { is SmartCubeEvent.Move -> { val t0 = firstMoveHost ?: e.hostTimestampMs.also { firstMoveHost = it } + if (e.afterGap) println(" … moves missing here (the cube never re-sent them); resyncing from facelets") println("move %-3s host +%6d ms cube %s%s".format(e.move, e.hostTimestampMs - t0, e.cubeTimestampMs?.let { "$it ms" } ?: "-", if (e.recovered) " (recovered)" else "")) } - is SmartCubeEvent.Facelets -> println("facelets ${e.facelets}") + // The cube repeats facelets periodically; only show changes. + is SmartCubeEvent.Facelets -> if (e.facelets != lastFacelets) { + lastFacelets = e.facelets + println("facelets ${e.facelets}" + if (e.facelets == SOLVED_FACELETS) " (solved)" else "") + } is SmartCubeEvent.Battery -> println("battery ${e.percent}%") is SmartCubeEvent.Hardware -> println("hardware ${e.name} hw ${e.hardwareVersion} sw ${e.softwareVersion}" + (e.productDate?.let { " made $it" } ?: "") + if (e.gyroSupported) " gyro" else "") is SmartCubeEvent.Gyro -> Unit // too chatty to print @@ -73,7 +81,18 @@ private fun monitor(mac: String) = BlueZTransport().use { transport -> cube.requestHardware(); delay(300) cube.requestBattery(); delay(300) cube.requestFacelets() - println("Streaming moves; Ctrl-C to quit.") + println("Streaming moves. Type r+Enter when the cube is physically solved to resync it, f+Enter to re-read state, Ctrl-C to quit.") + // stdin commands, read on a plain thread so the coroutine side never blocks + val scope = this + Thread { + while (true) { + when (readlnOrNull()?.trim()) { + null -> break + "r" -> scope.launch { cube.resetToSolved(); delay(200); cube.requestFacelets(); println("told the cube it's solved") } + "f" -> scope.launch { lastFacelets = null; cube.requestFacelets() } + } + } + }.apply { isDaemon = true }.start() done.await() collector.cancel() } -- cgit v1.3