Project

General

Profile

User Story #26 » 0001-fix-engine-antenna-delay-calibration-refreshes-inste.patch

knight8241, 08/07/2026 18:31

View differences:

common/src/commonMain/kotlin/com/aether/mofe/engine/MultiObserverFusionEngine.kt
* b_a + b_b un-distorts the frame. Empty (identity) until the surveyed layout is entered. */
private var antennaDelays: Map<DeviceId, Double> = emptyMap()
private var _antennaDelayCalibration: AntennaDelayCalibrator.Result? = null
/** Edge count + maintenance tick at the last successful antenna-delay solve — drive the refresh
* triggers in [maybeCalibrateAntennaDelays] (re-solve on an edge-set change or periodic cadence). */
private var antennaCalibrationEdgeCount = 0
private var antennaCalibrationTick = 0
/** Single mesh-wide event detector — predicates apply to every target. */
private val eventDetector = EventDetector()
......
/** Auto-calibrate once the surveyed layout is present and enough edges have accumulated; retries
* each maintenance tick until it succeeds, then holds (re-run [calibrateAntennaDelays] to redo). */
private fun maybeCalibrateAntennaDelays() {
if (antennaDelays.isNotEmpty()) return
if (MeshGroundTruth.isEmpty()) return
if (interAnchorDistances.size < config.anchorConstellationMinEdges) return
calibrateAntennaDelays()
val edges = interAnchorDistances.size
if (edges < config.anchorConstellationMinEdges) return
// One-shot fast path, then REFRESH (previously latched forever after the first solve): re-solve
// when the inter-anchor edge SET changes (a fuller/altered graph sharpens it — anchor added or
// reaped), or on a gentle periodic cadence so a slowly-drifting per-device bias is tracked
// rather than frozen at the first estimate. calibrateAntennaDelays() always solves from the RAW
// ranges, so re-running only sharpens it.
val firstTime = antennaDelays.isEmpty()
val edgeSetChanged = edges != antennaCalibrationEdgeCount
val cadence = config.antennaDelayRecalibrateEveryNthMaintenance
val cadenceDue = cadence > 0 && !firstTime && maintenanceTicks - antennaCalibrationTick >= cadence
if (!firstTime && !edgeSetChanged && !cadenceDue) return
val result = calibrateAntennaDelays()
if (result.ok) {
antennaCalibrationEdgeCount = edges
antennaCalibrationTick = maintenanceTicks
}
}
// ═════════════════════════════════════════════════════════════════════
common/src/commonMain/kotlin/com/aether/mofe/model/ConfigTypes.kt
val anchorConstellationRefineEveryNthMaintenance: Int = 3,
/** Minimum distinct inter-anchor edges required before attempting a refine. */
val anchorConstellationMinEdges: Int = 3,
/** After the first antenna-delay solve, re-solve at least every Nth performMaintenance() call so a
* slowly-changing per-device range bias (temperature, a re-mount) is tracked instead of frozen at
* the first estimate. The solve also re-runs immediately whenever the inter-anchor edge SET changes
* (a fuller/altered graph sharpens it). 0 disables the periodic refresh (keeps one-shot + edge
* change). ~60 ≈ 5 min at a 5 s maintenance cadence. */
val antennaDelayRecalibrateEveryNthMaintenance: Int = 60,
/** Only push a corrected anchor whose shift from its current position exceeds
* this (metres) — avoids churning the frame version for sub-noise nudges. */
val anchorConstellationMinCorrectionMeters: Double = 0.005,
common/src/commonTest/kotlin/com/aether/mofe/engine/AntennaDelayRecalibrationTest.kt
package com.aether.mofe.engine
import com.aether.mofe.integration.MofeTestHarness
import com.aether.mofe.model.DeviceId
import com.aether.mofe.model.MofeConfig
import com.aether.mofe.model.Vector3D
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertTrue
/**
* Antenna-delay calibration must REFRESH, not latch at the first solve. A per-device UWB range bias
* drifts (temperature, a re-mount); once solved, the old code returned early forever
* (`if (antennaDelays.isNotEmpty()) return`), so the frame stayed corrected against a stale estimate.
* The refresh re-solves on a periodic cadence (and on any inter-anchor edge-set change), tracking the
* drift. Fixture = the real field survey.
*/
class AntennaDelayRecalibrationTest {
private val a1 = DeviceId("A1")
private val a2 = DeviceId("A2")
private val a3 = DeviceId("A3")
private val a4 = DeviceId("A4")
private val truth = mapOf(
a1 to Vector3D(0.0, 0.0, 1.2318),
a2 to Vector3D(2.286, 0.0, 1.3843),
a3 to Vector3D(0.0, 3.6957, 1.4478),
a4 to Vector3D(2.286, 2.921, 0.889),
)
private val ids = listOf(a1, a2, a3, a4)
private fun MofeTestHarness.feed(a4bias: Double, reps: Int) {
val bias = mapOf(a1 to 0.10, a2 to -0.02, a3 to 0.05, a4 to a4bias)
repeat(reps) {
for (i in ids.indices) for (j in i + 1 until ids.size) {
engine.processInterAnchorRanging(
ids[i], ids[j],
truth.getValue(ids[i]).distanceTo(truth.getValue(ids[j])) +
bias.getValue(ids[i]) + bias.getValue(ids[j]),
)
}
}
}
@Test
fun antenna_delay_recalibrates_instead_of_latching_at_the_first_solve() {
MeshGroundTruth.clear()
try {
for ((id, p) in truth) MeshGroundTruth.set(id.value, MeshGroundTruth.Pose(p))
val h = MofeTestHarness(MofeConfig(antennaDelayRecalibrateEveryNthMaintenance = 1)).build()
h.engine.initializeAsRoot(a1)
// First solve: A4's antenna bias is +0.39 m (the field-observed worst case).
h.feed(a4bias = 0.39, reps = 12)
h.engine.performMaintenance()
assertTrue(
abs(h.engine.antennaDelays().getValue(a4) - 0.39) < 0.05,
"first calibration recovers A4 = +0.39; got ${h.engine.antennaDelays()[a4]}",
)
// A4's antenna 'drifts' to +0.10. Before the fix the estimate was frozen; now the
// calibration re-runs on the cadence and tracks the new bias.
h.feed(a4bias = 0.10, reps = 40)
h.engine.performMaintenance()
assertTrue(
abs(h.engine.antennaDelays().getValue(a4) - 0.10) < 0.06,
"recalibration tracks A4's drift to +0.10 (would stay ~0.39 if latched); " +
"got ${h.engine.antennaDelays()[a4]}",
)
} finally {
MeshGroundTruth.clear()
}
}
@Test
fun recalibration_can_be_disabled_by_config() {
MeshGroundTruth.clear()
try {
for ((id, p) in truth) MeshGroundTruth.set(id.value, MeshGroundTruth.Pose(p))
// 0 disables the periodic refresh; with a fixed edge set the first estimate is kept.
val h = MofeTestHarness(MofeConfig(antennaDelayRecalibrateEveryNthMaintenance = 0)).build()
h.engine.initializeAsRoot(a1)
h.feed(a4bias = 0.39, reps = 12)
h.engine.performMaintenance()
val first = h.engine.antennaDelays().getValue(a4)
h.feed(a4bias = 0.10, reps = 40)
h.engine.performMaintenance() // no cadence, same 6 edges → no re-solve
assertTrue(
abs(h.engine.antennaDelays().getValue(a4) - first) < 1e-9,
"with cadence disabled and a fixed edge set the calibration is not refreshed",
)
} finally {
MeshGroundTruth.clear()
}
}
}
(2-2/2)