android: two reasons the Glyph stayed dark
Both silent, and each alone was enough to leave the matrix blank while everything else in the app worked. **1. `setAppMatrixFrame` does nothing on this firmware.** The developer kit is explicit that app-driven control must use the `App` variants, so that is what the first cut used. On Phone (3) build 2608141733 the call is accepted without an exception and then dropped: it forwards to `IGlyphService.setAppMatrixColors` and the system service never reaches the hardware. Measured over 20 seconds of 1 Hz pushes: `setAppMatrixFrame` produced 0 writes to the matrix light, `setMatrixFrame` produced 232. Both paths now use the plain variant. The kit's warning that it can fight a selected Glyph Toy is real, but a rear display that never lights is the worse failure. **2. `getDeviceMatrixLength()` returns the side, not the dot count.** The name invites the opposite reading, and the SDK settles it: `DEVICE_23112_MATRIX_LENGTH` is 25 and `DEVICE_23112_SIZE` is that squared. Taking a square root of 25 gave a 5x5 frame, so the renderer drew a few stray dots in one corner and `GlyphFrame.set` silently discarded everything outside the grid — 7 lit dots instead of 78. Also: `clear()` blanks by drawing an empty frame rather than calling `closeAppMatrix()`, which was closing a channel we no longer open and so left the last frame lit. Verified on the device: 78 of 625 dots for a running interval, 234 hardware writes in 20s, the frame rendering as minutes over seconds inside a draining ring. 225 tests still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 file changed
+44
-22
@@ -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
|
||||
Reference in new issue
Block a user