Project

General

Profile

User Story #67 » 0002-feat-engine-client-self-seed-render-readiness-model-.patch

knight8241, 08/07/2026 18:32

View differences:

common/src/commonMain/kotlin/com/aether/mofe/engine/MultiObserverFusionEngine.kt
* not zero (its placement carries the seed/calibration error). σ ≈ 5 cm. */
private val REFERENCE_ANCHOR_VARIANCE = 0.0025
/** Measurement variance (m²) for a CLIENT self-seed from the constellation's broadcast solve
* ([seedSelfFromExternalPosition]). The position is band-relayed and a fresh multilateration
* result, so it is less certain than a surveyed anchor coordinate — trust it less. σ ≈ 20 cm;
* still tight enough to initialize and anchor the self-EKF, while the IMU predict smooths between
* the ~band-rate updates. */
private val EXTERNAL_SELF_SEED_VARIANCE = 0.04
/** Aggregates pre-filtered measurements per target across all anchors.
* Honours the configured retention window — previously it silently used the
* default (500 ms), so a wider real-device window (set to survive intermittent
......
)
}
/**
* Seed the SELF target's EKF from an EXTERNAL position solution — the constellation's broadcast
* solve of THIS client — for a mobile OBSERVER that is NOT itself a reference point. This is the
* client analogue of [maintainSelfReferenceAnchor]: that path only initializes the self-EKF when
* self IS a reference point (an anchor), reading its known position from the frame. A client is
* never a reference point, and its own UWB only ranges peers (every local range is targetId=peer),
* so it depends entirely on the client-multilateration branch — which needs ≥
* [MofeConfig.minAnchorsForFusion] concurrent peer-supplied `targetId=self` observations in one
* window. A late/observer client rarely has that many fresh at once, so [ImuFusionEKF] never
* initializes and [getState]/[selfAttitude] stay null: the device can neither place itself nor
* anything relative to it, and the view falls back to raw/garbage.
*
* Feeding the constellation's already-computed position for this client (the very value the anchors
* broadcast and the HUD already trusts) as a [PositionSolution] initializes the EKF on the first
* call and keeps it anchored thereafter; the IMU predict smooths between updates. Call it on a
* cadence (e.g. each maintenance tick) with the freshest broadcast position.
*
* No-op when there is no self target, when [position] is non-finite, or when self IS already a
* reference point (then [maintainSelfReferenceAnchor] owns the seed — don't double-feed). Uses
* [varianceMeters2] ≥ [REFERENCE_ANCHOR_VARIANCE] since a band-relayed solve is less certain than a
* surveyed anchor coordinate.
*/
fun seedSelfFromExternalPosition(
position: Vector3D,
varianceMeters2: Double = EXTERNAL_SELF_SEED_VARIANCE,
) {
val self = selfTargetId ?: return
if (!position.x.isFinite() || !position.y.isFinite() || !position.z.isFinite()) return
if (frameManager.getAllReferencePoints().any { it.id == self }) return // an anchor self-seeds instead
val ctx = targets[self] ?: return
val v = varianceMeters2
ctx.ekf.update(
PositionSolution(
targetId = self, timestamp = clock.now(), position = position,
covariance = Matrix.diagonal(v, v, v), gdop = 1.0, converged = true,
),
)
}
/**
* Periodic maintenance — should be called on a timer (e.g. every 5s).
* Detects anchors that have stopped reporting and removes them.
common/src/commonMain/kotlin/com/aether/mofe/engine/render/MeshRenderReadiness.kt
package com.aether.mofe.engine.render
/**
* Whether a CLIENT may render the spatial mesh scene (3-D / FPV nodes + camera), or must instead HOLD
* with a status message. This is the safeguard that keeps a device from drawing invalid or garbage
* visualizations before its MOFE engine and the mesh are actually ready — the failure the field showed
* (a mobile observer with no self pose drew raw, jittery, spatially-wrong nodes and an un-tracking
* camera, indistinguishable to the user from a working view).
*
* The rule: a client may place objects in space only when (1) its engine has started, (2) the mesh has
* a trustworthy solved frame (quorum + enough reference anchors), AND (3) THIS device has localized
* itself (a finite self pose / initialized self-EKF) — because every node is drawn RELATIVE to self, so
* a missing self pose makes the whole scene meaningless. Any earlier state renders a labelled holding
* screen instead, and reports the reason for the dev.
*
* Pure and platform-agnostic: the host supplies the four booleans/counts from its own StateFlows; this
* decides the state + user label + the single `canRenderSpatial` gate the UI checks.
*/
enum class RenderReadinessState {
/** The MOFE runtime hasn't started (or produced anything) yet. */
INITIALIZING,
/** No trustworthy solved frame yet — the constellation hasn't reached solvable quorum. */
ACQUIRING_MESH,
/** The mesh is solved, but THIS device has no self position yet (self-EKF not initialized). The
* scene is self-relative, so it cannot be placed until self localizes. */
ACQUIRING_SELF,
/** Everything needed to place objects accurately is present — render the spatial scene. */
READY,
}
data class MeshRenderReadiness(
val state: RenderReadinessState,
/** Short, user-facing label for the holding overlay (empty when [canRenderSpatial]). */
val label: String,
/** One-line reason for the dev diagnostic (empty when [READY]). */
val detail: String,
) {
/** The single gate the scene checks: may it render node positions + drive the FPV camera? */
val canRenderSpatial: Boolean get() = state == RenderReadinessState.READY
companion object {
/** Reference anchors the frame needs before a client should trust placement (matches the
* engine's min-reference-points-for-solving; a client renders relative to the solved set). */
const val DEFAULT_MIN_REFERENCES: Int = 4
/**
* @param engineStarted the MOFE runtime has started for this device
* @param meshSolved the constellation reached solvable quorum (QUORUM / canSolvePositions)
* @param referenceCount solved reference anchors currently backing the frame
* @param selfLocalized this device has a finite self pose (its self-EKF has initialized)
*/
fun evaluate(
engineStarted: Boolean,
meshSolved: Boolean,
referenceCount: Int,
selfLocalized: Boolean,
minReferences: Int = DEFAULT_MIN_REFERENCES,
): MeshRenderReadiness = when {
!engineStarted -> MeshRenderReadiness(
RenderReadinessState.INITIALIZING, "Starting…",
"engine not started",
)
!meshSolved || referenceCount < minReferences -> MeshRenderReadiness(
RenderReadinessState.ACQUIRING_MESH, "Acquiring mesh…",
"no solved quorum (references $referenceCount/$minReferences, solved=$meshSolved)",
)
!selfLocalized -> MeshRenderReadiness(
RenderReadinessState.ACQUIRING_SELF, "Locating this device…",
"mesh solved but self not localized (no self pose)",
)
else -> MeshRenderReadiness(RenderReadinessState.READY, "", "")
}
}
}
common/src/commonTest/kotlin/com/aether/mofe/engine/ClientSelfSeedTest.kt
package com.aether.mofe.engine
import com.aether.mofe.integration.MofeTestHarness
import com.aether.mofe.model.DeviceId
import com.aether.mofe.model.Vector3D
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* F4 · client self-seed. A mobile OBSERVER is never a reference point and its own UWB only ranges peers,
* so its self-EKF starves on the ≥4 concurrent peer-supplied self-observations the client-multilateration
* branch needs — it never initializes, `getState(self)` stays null, and the device renders raw/garbage
* (the field "no self pose"). [MultiObserverFusionEngine.seedSelfFromExternalPosition] feeds the
* constellation's broadcast solve of this client into the self-EKF so it initializes + tracks.
*/
class ClientSelfSeedTest {
private val self = DeviceId("observer")
private lateinit var harness: MofeTestHarness
private lateinit var engine: MultiObserverFusionEngine
@BeforeTest
fun setup() {
harness = MofeTestHarness().build()
harness.bootstrapMesh()
engine = harness.engine
engine.registerTarget(self) // a mobile observer: a tracked target…
engine.setSelfId(self) // …that is THIS device, but never a reference anchor
}
@Test
fun unseeded_client_self_ekf_has_no_state() {
// No self ranging fed → the EKF never initialized → the field bug (null self pose).
assertNull(engine.getState(self), "an unseeded client self-EKF returns null")
}
@Test
fun seed_initializes_and_tracks_the_broadcast_position() {
val broadcast = Vector3D(1.5, 2.0, 0.8)
repeat(5) { engine.seedSelfFromExternalPosition(broadcast) }
val state = engine.getState(self)
assertNotNull(state, "self-seed initializes the client self-EKF")
assertTrue(
state.position.distanceTo(broadcast) < 0.15,
"the self pose tracks the broadcast solve; got ${state.position}",
)
}
@Test
fun non_finite_broadcast_is_ignored() {
engine.seedSelfFromExternalPosition(Vector3D(Double.NaN, 0.0, 0.0))
assertNull(engine.getState(self), "a NaN broadcast never initializes the EKF")
}
}
common/src/commonTest/kotlin/com/aether/mofe/engine/render/MeshRenderReadinessTest.kt
package com.aether.mofe.engine.render
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* The client render-readiness safeguard. Proves the gate only opens when the engine, the mesh frame,
* AND this device's own localization are all present — so a client can never draw spatial nodes/camera
* from missing or garbage state (the field failure: a mobile observer with no self pose drew raw,
* spatially-wrong nodes that looked like a working view).
*/
class MeshRenderReadinessTest {
private fun eval(started: Boolean, solved: Boolean, refs: Int, self: Boolean) =
MeshRenderReadiness.evaluate(started, solved, refs, self)
@Test
fun ready_only_when_everything_present() {
val r = eval(started = true, solved = true, refs = 4, self = true)
assertEquals(RenderReadinessState.READY, r.state)
assertTrue(r.canRenderSpatial)
assertEquals("", r.label)
}
@Test
fun engine_not_started_initializing() {
val r = eval(started = false, solved = true, refs = 4, self = true)
assertEquals(RenderReadinessState.INITIALIZING, r.state)
assertFalse(r.canRenderSpatial)
assertTrue(r.label.isNotEmpty() && r.detail.isNotEmpty())
}
@Test
fun no_quorum_acquires_mesh() {
assertEquals(RenderReadinessState.ACQUIRING_MESH, eval(true, false, 4, true).state)
// Solved flag true but too few reference anchors is still "acquiring mesh".
assertEquals(RenderReadinessState.ACQUIRING_MESH, eval(true, true, 3, true).state)
assertFalse(eval(true, false, 4, true).canRenderSpatial)
}
@Test
fun mesh_solved_but_no_self_pose_acquires_self() {
// THE Pixel-8 field case: mesh is fine, this device just isn't localized → HOLD, don't render.
val r = eval(started = true, solved = true, refs = 4, self = false)
assertEquals(RenderReadinessState.ACQUIRING_SELF, r.state)
assertFalse(r.canRenderSpatial)
assertTrue(r.detail.contains("self"), "dev reason names the self-pose gap: ${r.detail}")
}
@Test
fun precedence_engine_then_mesh_then_self() {
// Worst-present condition wins, so the label always names the FIRST thing to fix.
assertEquals(RenderReadinessState.INITIALIZING, eval(false, false, 0, false).state)
assertEquals(RenderReadinessState.ACQUIRING_MESH, eval(true, false, 0, false).state)
assertEquals(RenderReadinessState.ACQUIRING_SELF, eval(true, true, 4, false).state)
assertEquals(RenderReadinessState.READY, eval(true, true, 4, true).state)
}
@Test
fun only_ready_state_opens_the_spatial_gate() {
for (started in listOf(false, true)) for (solved in listOf(false, true))
for (refs in listOf(0, 4)) for (self in listOf(false, true)) {
val r = eval(started, solved, refs, self)
assertEquals(
r.state == RenderReadinessState.READY, r.canRenderSpatial,
"canRenderSpatial must equal (state==READY) for $started/$solved/$refs/$self",
)
}
}
}
    (1-1/1)