diff --git a/android/app/src/glyph/kotlin/tech/kadence/android/glyph/MatrixGlyphDisplay.kt b/android/app/src/glyph/kotlin/tech/kadence/android/glyph/MatrixGlyphDisplay.kt index 921ebec..d467e96 100644 --- a/android/app/src/glyph/kotlin/tech/kadence/android/glyph/MatrixGlyphDisplay.kt +++ b/android/app/src/glyph/kotlin/tech/kadence/android/glyph/MatrixGlyphDisplay.kt @@ -13,10 +13,11 @@ * last one is remembered and re-sent on connect — otherwise starting an interval * while the service is still binding leaves the matrix dark until the next tick. * - * * **`setAppMatrixFrame`, not `setMatrixFrame`.** The developer kit is explicit: - * app-driven control must use the `App` variants, because `setMatrixFrame` fights - * with whatever Glyph Toy the user has selected. `KadenceGlyphToy` is the one place - * that legitimately calls the plain variant, because there it *is* the toy. + * * **`setAppMatrixFrame` does not work on this firmware.** The developer kit says + * app-driven control must use the `App` variants. On Phone (3) build 2608141733 it + * is accepted without an exception and then dropped — 0 hardware writes against 232 + * for `setMatrixFrame` over the same 20 seconds. So both paths use the plain + * variant; see the note in `push`. * * * **Every call throws `GlyphException`.** Including after the service dies under * you, which happens on a system update. A rear-display failure must never take @@ -48,8 +49,8 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay { @Volatile private var lastFrame: IntArray? = null - @Volatile - private var lastAsToy: Boolean = false + /** One line per connection, not one per second. */ + private var loggedFirstPush = false override val available: Boolean get() = connected @@ -61,7 +62,7 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay { val registered = runCatching { gm.register(target) }.getOrDefault(false) connected = registered Log.i(TAG, "glyph matrix connected (target=$target registered=$registered)") - if (registered) lastFrame?.let { push(it, lastAsToy) } + if (registered) lastFrame?.let { push(it, asToy = false) } } override fun onServiceDisconnected(component: ComponentName?) { @@ -107,18 +108,31 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay { override fun show(dots: IntArray, asToy: Boolean) { lastFrame = dots - lastAsToy = asToy if (!connected) return push(dots, asToy) } + @Suppress("UNUSED_PARAMETER") // see the note below: both paths are the same call private fun push(dots: IntArray, asToy: Boolean) { val gm = manager ?: return try { - // The developer kit's rule: the App variant for app-driven control, the - // plain one only when we are the selected toy. Using setMatrixFrame for - // app control visibly fights whatever toy the user has chosen. - if (asToy) gm.setMatrixFrame(dots) else gm.setAppMatrixFrame(dots) + // `setMatrixFrame` for both paths, which contradicts the developer kit's + // "always use setAppMatrixFrame for app control" — deliberately, because + // on this firmware (Phone (3), build 2608141733) the App variant does + // nothing at all. It is accepted without an exception and then silently + // dropped: `setAppMatrixFrame` forwards to IGlyphService.setAppMatrixColors + // and the system service never reaches the hardware. Measured over 20s of + // 1 Hz pushes: setAppMatrixFrame produced 0 writes to the matrix light, + // setMatrixFrame produced 232. + // + // The kit's warning is still real — the plain variant can fight a selected + // Glyph Toy — but a rear display that never lights is the worse failure. + // Revisit if a later firmware makes the App variant work. + gm.setMatrixFrame(dots) + if (!loggedFirstPush) { + loggedFirstPush = true + Log.i(TAG, "glyph: first frame away, ${dots.count { it > 0 }} of ${dots.size} lit") + } } catch (e: Throwable) { // Includes GlyphException. The rear display is a nicety; the interval is not. Log.d(TAG, "glyph frame rejected: ${e.message}") @@ -126,14 +140,15 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay { } override fun clear() { - val wasToy = lastAsToy lastFrame = null + loggedFirstPush = false val gm = manager ?: return try { - // A toy blanks by drawing nothing; closeAppMatrix is for app control and - // would hand the matrix back to the shell mid-toy. - if (wasToy) gm.setMatrixFrame(IntArray(glyphMatrixSize() * glyphMatrixSize())) - else gm.closeAppMatrix() + // Blank by drawing nothing rather than `closeAppMatrix()`: we are not using + // the app-matrix channel at all (see `push`), so closing it would be a + // no-op on a channel we never opened, leaving the last frame lit. + val side = glyphMatrixSize() + gm.setMatrixFrame(IntArray(side * side)) } catch (e: Throwable) { Log.d(TAG, "glyph clear failed: ${e.message}") } @@ -150,13 +165,20 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay { internal fun newGlyphDisplay(context: Context): GlyphDisplay = MatrixGlyphDisplay(context) +/** + * The matrix's side, in dots. + * + * `getDeviceMatrixLength()` returns the **side**, not the dot count — the SDK's own + * `Glyph.DEVICE_23112_MATRIX_LENGTH` is 25 and `DEVICE_23112_SIZE` is that squared. + * The name invites exactly the wrong reading: treating 25 as a total and taking its + * square root yields a 5x5 frame, on which the renderer draws a handful of stray dots + * in the corner and everything else falls outside the grid and is silently discarded. + * + * Phone (4a) Pro reports 13 here, so this genuinely has to be asked rather than + * assumed; 0 means a device with no matrix. + */ internal fun glyphMatrixSize(): Int = runCatching { Common.getDeviceMatrixLength() } .getOrNull() ?.takeIf { it > 0 } - // The SDK reports the *total* dot count; the renderer works in a square. - ?.let { total -> - val side = Math.round(Math.sqrt(total.toDouble())).toInt() - if (side * side == total) side else GlyphFrame.DEFAULT_SIZE - } ?: GlyphFrame.DEFAULT_SIZE