User Story #38 » 0005-feat-netcode-carry-peer-orientation-through-the-fuse.patch
| common/src/commonMain/kotlin/com/aether/mofe/engine/netcode/NetcodeAdapter.kt | ||
|---|---|---|
|
package com.aether.mofe.engine.netcode
|
||
|
import com.aether.mofe.model.DeviceId
|
||
|
import com.aether.mofe.model.Quaternion
|
||
|
import com.aether.mofe.model.Timestamp
|
||
|
import com.aether.mofe.model.messaging.FusedStateSample
|
||
|
import com.aether.mofe.model.messaging.MeshPayload
|
||
| ... | ... | |
|
/**
|
||
|
* Record every sample in a fused-state batch into its device's history. The wire
|
||
|
* timestamp is treated as mesh time (the sender stamps it — companion edit #2).
|
||
|
* The fused wire sample carries no orientation, so remote poses use identity; the
|
||
|
* geometric predicates are positional, so this is sufficient for them.
|
||
|
* The fused wire sample now carries the sender's orientation, so remote poses render
|
||
|
* with real facing (Gap 1); pre-orientation senders decode to identity via the field
|
||
|
* default, leaving the positional predicates unaffected.
|
||
|
*/
|
||
|
fun onFusedSample(sample: FusedStateSample) {
|
||
|
for (w in sample.samples) {
|
||
|
session.recordSample(
|
||
|
DeviceId(w.targetId),
|
||
|
Timestamp(w.timestampMicros),
|
||
|
TemporalPose(w.position, w.velocity, Quaternion.IDENTITY),
|
||
|
TemporalPose(w.position, w.velocity, w.orientation),
|
||
|
)
|
||
|
}
|
||
|
}
|
||
| common/src/commonMain/kotlin/com/aether/mofe/model/CoreTypes.kt | ||
|---|---|---|
|
* Quaternion for 3D rotation representation.
|
||
|
* Avoids gimbal lock and provides smooth interpolation.
|
||
|
*/
|
||
|
@Serializable
|
||
|
data class Quaternion(
|
||
|
val w: Double,
|
||
|
val x: Double,
|
||
| common/src/commonMain/kotlin/com/aether/mofe/model/messaging/WireFragments.kt | ||
|---|---|---|
|
import com.aether.mofe.model.AnchorObservation
|
||
|
import com.aether.mofe.model.DeviceId
|
||
|
import com.aether.mofe.model.FusedState
|
||
|
import com.aether.mofe.model.Quaternion
|
||
|
import com.aether.mofe.model.Timestamp
|
||
|
import com.aether.mofe.model.Vector3D
|
||
|
import kotlinx.serialization.Serializable
|
||
| ... | ... | |
|
val velocity: Vector3D,
|
||
|
/** 1-σ per axis — lets receivers render confidence without the full EKF state. */
|
||
|
val positionSigma: Vector3D,
|
||
|
/** Body-frame orientation (unit quaternion) so peers render with real facing
|
||
|
* instead of identity. Defaulted for schema evolution: a pre-orientation sender
|
||
|
* omits it and decodes to identity, and the mesh codec ignores unknown keys, so
|
||
|
* a newer sender stays compatible with an older receiver too. */
|
||
|
val orientation: Quaternion = Quaternion.IDENTITY,
|
||
|
)
|
||
|
|
||
|
/**
|
||
| ... | ... | |
|
position = position,
|
||
|
velocity = velocity,
|
||
|
positionSigma = positionUncertainty,
|
||
|
orientation = orientation,
|
||
|
)
|
||
| common/src/commonTest/kotlin/com/aether/mofe/engine/netcode/NetcodeAdapterTest.kt | ||
|---|---|---|
|
package com.aether.mofe.engine.netcode
|
||
|
import com.aether.mofe.model.DeviceId
|
||
|
import com.aether.mofe.model.Quaternion
|
||
|
import com.aether.mofe.model.Timestamp
|
||
|
import com.aether.mofe.model.Vector3D
|
||
|
import com.aether.mofe.model.messaging.FusedStateSample
|
||
| ... | ... | |
|
import kotlin.test.Test
|
||
|
import kotlin.test.assertEquals
|
||
|
import kotlin.test.assertFalse
|
||
|
import kotlin.test.assertNotEquals
|
||
|
import kotlin.test.assertTrue
|
||
|
/**
|
||
| ... | ... | |
|
*/
|
||
|
class NetcodeAdapterTest {
|
||
|
private fun wire(id: String, tMicros: Long, x: Double) = WireFusedSample(
|
||
|
private fun wire(
|
||
|
id: String,
|
||
|
tMicros: Long,
|
||
|
x: Double,
|
||
|
orientation: Quaternion = Quaternion.IDENTITY,
|
||
|
) = WireFusedSample(
|
||
|
targetId = id,
|
||
|
timestampMicros = tMicros,
|
||
|
position = Vector3D(x, 0.0, 0.0),
|
||
|
velocity = Vector3D.ZERO,
|
||
|
positionSigma = Vector3D.ZERO,
|
||
|
orientation = orientation,
|
||
|
)
|
||
|
@Test
|
||
| ... | ... | |
|
assertTrue(session.trackedDevices().containsAll(listOf(DeviceId("A"), DeviceId("B"))))
|
||
|
}
|
||
|
@Test
|
||
|
fun onFusedSampleCarriesPeerOrientationNotIdentity() {
|
||
|
val session = NetcodeSession()
|
||
|
val adapter = NetcodeAdapter(session)
|
||
|
// A real facing (180° about Z) — must survive the wire instead of collapsing to identity (Gap 1).
|
||
|
val facing = Quaternion(0.0, 0.0, 0.0, 1.0)
|
||
|
adapter.onFusedSample(FusedStateSample(listOf(wire("A", 500_000, 1.0, facing))))
|
||
|
val pose = session.poseAt(DeviceId("A"), Timestamp(500_000))!!
|
||
|
assertEquals(facing, pose.orientation)
|
||
|
assertNotEquals(Quaternion.IDENTITY, pose.orientation)
|
||
|
}
|
||
|
@Test
|
||
|
fun onTimeSyncResponseFeedsTheClock() {
|
||
|
val session = NetcodeSession()
|
||
| common/src/commonTest/kotlin/com/aether/mofe/messaging/MeshCodecTest.kt | ||
|---|---|---|
|
package com.aether.mofe.messaging
|
||
|
import com.aether.mofe.model.Quaternion
|
||
|
import com.aether.mofe.model.Vector3D
|
||
|
import com.aether.mofe.model.messaging.DeviceCredential
|
||
|
import com.aether.mofe.model.messaging.FusedStateSample
|
||
|
import com.aether.mofe.model.messaging.MeshChannel
|
||
|
import com.aether.mofe.model.messaging.MeshPayload
|
||
|
import com.aether.mofe.model.messaging.MessageEnvelope
|
||
| ... | ... | |
|
import com.aether.mofe.model.messaging.SecHelloDone
|
||
|
import com.aether.mofe.model.messaging.SecHelloInit
|
||
|
import com.aether.mofe.model.messaging.StateDelta
|
||
|
import com.aether.mofe.model.messaging.WireFusedSample
|
||
|
import com.aether.mofe.model.messaging.WireObservation
|
||
|
import kotlin.test.Test
|
||
|
import kotlin.test.assertEquals
|
||
| ... | ... | |
|
assertEquals(batch, MeshCodec.decodeOrNull(MeshCodec.encode(envelope(batch)))?.payload)
|
||
|
}
|
||
|
@Test
|
||
|
fun fusedSampleRoundTripsPreservingOrientation() {
|
||
|
// A peer's real facing must survive the wire (Gap 1) — this exercises the
|
||
|
// @Serializable on Quaternion end-to-end through the CBOR codec.
|
||
|
val facing = Quaternion(0.0, 0.0, 0.0, 1.0) // 180° about Z, non-identity
|
||
|
val sample = FusedStateSample(listOf(WireFusedSample(
|
||
|
targetId = "t", timestampMicros = 42,
|
||
|
position = Vector3D(1.0, 2.0, 3.0), velocity = Vector3D.ZERO,
|
||
|
positionSigma = Vector3D.ZERO, orientation = facing,
|
||
|
)))
|
||
|
val decoded = MeshCodec.decodeOrNull(MeshCodec.encode(envelope(sample)))?.payload as? FusedStateSample
|
||
|
assertEquals(facing, decoded?.samples?.single()?.orientation,
|
||
|
"orientation must round-trip through CBOR, not reset to identity")
|
||
|
}
|
||
|
@Test
|
||
|
fun garbageDecodesToNullWithoutThrowing() {
|
||
|
assertNull(MeshCodec.decodeOrNull(byteArrayOf(1, 2, 3, 4, 5)))
|
||