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 --- .../coffee/liz/rookery/bt/GanProtocolTest.kt | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) (limited to 'core/src/commonTest/kotlin/coffee') diff --git a/core/src/commonTest/kotlin/coffee/liz/rookery/bt/GanProtocolTest.kt b/core/src/commonTest/kotlin/coffee/liz/rookery/bt/GanProtocolTest.kt index 5e45af3..3ba2417 100644 --- a/core/src/commonTest/kotlin/coffee/liz/rookery/bt/GanProtocolTest.kt +++ b/core/src/commonTest/kotlin/coffee/liz/rookery/bt/GanProtocolTest.kt @@ -197,6 +197,42 @@ class GanProtocolTest { ) } + @Test fun gen4RetriesHistoryThenGivesUpInsteadOfHanging() = runTest { + // The failure seen live on a GANicE3: a move after a lost packet waited until the next turn. + val d = GanGen4ProtocolDriver(clock) + val c = RecordingRawConnection() + d.handleStateEvent(c, Packets.gen4Facelets(10, SOLVED_STATE)) + d.handleStateEvent(c, Packets.gen4Move(11, 4, 1, 100)) + assertEquals(emptyList(), d.handleStateEvent(c, Packets.gen4Move(13, 5, 0, 155))) // 12 lost + assertEquals(1, c.sent.size) + + now += 100; assertEquals(emptyList(), d.tick(c)); assertEquals(1, c.sent.size) // too soon to retry + now += 250; assertEquals(emptyList(), d.tick(c)); assertEquals(2, c.sent.size) // retried + assertEquals(0xD1.toByte(), c.sent.last()[0]) + + // History never comes: after 1.5 s the held move is released, flagged, and facelets requested. + now += 1_200 + val released = d.tick(c) + assertEquals(listOf(Move("B", 155, 1_000, 13, afterGap = true)), released) + assertContentEquals(byteArrayOf(0xDD.toByte(), 0x04, 0x00, 0xED.toByte(), 0x00, 0x00) + ByteArray(14), c.sent.last()) + assertEquals(0, c.disconnects) + + // Live moves flow again immediately afterwards. + assertEquals(listOf("U"), d.handleStateEvent(c, Packets.gen4Move(14, 0, 0, 900)).map { (it as Move).move }) + assertEquals(emptyList(), d.tick(c)) + } + + @Test fun gen4BigBacklogIsReleasedNotDisconnected() = runTest { + val d = GanGen4ProtocolDriver(clock) + val c = RecordingRawConnection() + d.handleStateEvent(c, Packets.gen4Facelets(10, SOLVED_STATE)) + d.handleStateEvent(c, Packets.gen4Move(11, 0, 0, 1)) + val out = (13..30).flatMap { d.handleStateEvent(c, Packets.gen4Move(it, 0, 0, it.toLong())) } + assertEquals((13..30).toList(), out.map { (it as Move).serial }) // 13-29 released, then 30 flows live + assertTrue((out.first() as Move).afterGap) + assertEquals(0, c.disconnects) + } + @Test fun gen4FaceletsTriggerHistoryWhenIdle() = runTest { val d = GanGen4ProtocolDriver(clock) val c = RecordingRawConnection() -- cgit v1.3