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
|
* 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.
|
* while the service is still binding leaves the matrix dark until the next tick.
|
||||||
*
|
*
|
||||||
* * **`setAppMatrixFrame`, not `setMatrixFrame`.** The developer kit is explicit:
|
* * **`setAppMatrixFrame` does not work on this firmware.** The developer kit says
|
||||||
* app-driven control must use the `App` variants, because `setMatrixFrame` fights
|
* app-driven control must use the `App` variants. On Phone (3) build 2608141733 it
|
||||||
* with whatever Glyph Toy the user has selected. `KadenceGlyphToy` is the one place
|
* is accepted without an exception and then dropped — 0 hardware writes against 232
|
||||||
* that legitimately calls the plain variant, because there it *is* the toy.
|
* 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
|
* * **Every call throws `GlyphException`.** Including after the service dies under
|
||||||
* you, which happens on a system update. A rear-display failure must never take
|
* 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
|
@Volatile
|
||||||
private var lastFrame: IntArray? = null
|
private var lastFrame: IntArray? = null
|
||||||
|
|
||||||
@Volatile
|
/** One line per connection, not one per second. */
|
||||||
private var lastAsToy: Boolean = false
|
private var loggedFirstPush = false
|
||||||
|
|
||||||
override val available: Boolean get() = connected
|
override val available: Boolean get() = connected
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay {
|
|||||||
val registered = runCatching { gm.register(target) }.getOrDefault(false)
|
val registered = runCatching { gm.register(target) }.getOrDefault(false)
|
||||||
connected = registered
|
connected = registered
|
||||||
Log.i(TAG, "glyph matrix connected (target=$target registered=$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?) {
|
override fun onServiceDisconnected(component: ComponentName?) {
|
||||||
@@ -107,18 +108,31 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay {
|
|||||||
|
|
||||||
override fun show(dots: IntArray, asToy: Boolean) {
|
override fun show(dots: IntArray, asToy: Boolean) {
|
||||||
lastFrame = dots
|
lastFrame = dots
|
||||||
lastAsToy = asToy
|
|
||||||
if (!connected) return
|
if (!connected) return
|
||||||
push(dots, asToy)
|
push(dots, asToy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER") // see the note below: both paths are the same call
|
||||||
private fun push(dots: IntArray, asToy: Boolean) {
|
private fun push(dots: IntArray, asToy: Boolean) {
|
||||||
val gm = manager ?: return
|
val gm = manager ?: return
|
||||||
try {
|
try {
|
||||||
// The developer kit's rule: the App variant for app-driven control, the
|
// `setMatrixFrame` for both paths, which contradicts the developer kit's
|
||||||
// plain one only when we are the selected toy. Using setMatrixFrame for
|
// "always use setAppMatrixFrame for app control" — deliberately, because
|
||||||
// app control visibly fights whatever toy the user has chosen.
|
// on this firmware (Phone (3), build 2608141733) the App variant does
|
||||||
if (asToy) gm.setMatrixFrame(dots) else gm.setAppMatrixFrame(dots)
|
// 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) {
|
} catch (e: Throwable) {
|
||||||
// Includes GlyphException. The rear display is a nicety; the interval is not.
|
// Includes GlyphException. The rear display is a nicety; the interval is not.
|
||||||
Log.d(TAG, "glyph frame rejected: ${e.message}")
|
Log.d(TAG, "glyph frame rejected: ${e.message}")
|
||||||
@@ -126,14 +140,15 @@ internal class MatrixGlyphDisplay(context: Context) : GlyphDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun clear() {
|
override fun clear() {
|
||||||
val wasToy = lastAsToy
|
|
||||||
lastFrame = null
|
lastFrame = null
|
||||||
|
loggedFirstPush = false
|
||||||
val gm = manager ?: return
|
val gm = manager ?: return
|
||||||
try {
|
try {
|
||||||
// A toy blanks by drawing nothing; closeAppMatrix is for app control and
|
// Blank by drawing nothing rather than `closeAppMatrix()`: we are not using
|
||||||
// would hand the matrix back to the shell mid-toy.
|
// the app-matrix channel at all (see `push`), so closing it would be a
|
||||||
if (wasToy) gm.setMatrixFrame(IntArray(glyphMatrixSize() * glyphMatrixSize()))
|
// no-op on a channel we never opened, leaving the last frame lit.
|
||||||
else gm.closeAppMatrix()
|
val side = glyphMatrixSize()
|
||||||
|
gm.setMatrixFrame(IntArray(side * side))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.d(TAG, "glyph clear failed: ${e.message}")
|
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)
|
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 =
|
internal fun glyphMatrixSize(): Int =
|
||||||
runCatching { Common.getDeviceMatrixLength() }
|
runCatching { Common.getDeviceMatrixLength() }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
?.takeIf { it > 0 }
|
?.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
|
?: GlyphFrame.DEFAULT_SIZE
|
||||||
Reference in new issue
Block a user