summaryrefslogtreecommitdiff
path: root/desktop/src
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2026-11-12 21:41:08 -0800
committerElizabeth Alexander Hunt <me@liz.coffee>2026-11-12 21:41:08 -0800
commit2a9a3126a4955dffc40bc04ab88a5ccdb3da77c9 (patch)
treeb19e42dde8eccf6c653495bd103db75e78b19201 /desktop/src
parent3afb04de43726d71580b626e856c68b6ed79984e (diff)
downloadrookery-2a9a3126a4955dffc40bc04ab88a5ccdb3da77c9.tar.gz
rookery-2a9a3126a4955dffc40bc04ab88a5ccdb3da77c9.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'desktop/src')
-rw-r--r--desktop/src/main/kotlin/coffee/liz/rookery/desktop/CubeCli.kt23
1 files changed, 21 insertions, 2 deletions
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<Unit>()
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()
}