Project

General

Profile

User Story #42 » 0003-feat-netcode-on-device-MeshNode-bring-up-construct-s.patch

knight8241, 08/07/2026 18:32

View differences:

androidApp/src/main/java/com/aether/mofe/AetherApp.kt
// engines share one frame. Works offline (no server register needed);
// without it the engine rejects every measurement ("Mesh not initialized").
meshRepository.currentAnchorId()?.let { mofeEngineHost.setRootAnchor(DeviceId(it)) }
// Netcode go-live: (re)attempt peer-to-peer netcode bring-up as the roster/root and
// the cached trust material become available. Idempotent, and fails safe — when trust
// isn't ready or no leader is known yet, the netcode stays down and the band is
// unaffected. The band feeds NetcodePeerBook (peer source IPs) from its receive loop.
com.aether.mofe.platform.netcode.NetcodeBringUp.maybeStart(
this@AetherApp, mofeEngineHost, deviceIdentity, meshRepository, appScope)
// REAPING: a node that dropped off the roster (dormant timeout or an
// instant leave — see MeshRepository.reapDormantNodes / deactivateMesh)
// must stop consuming engine cycles and telemetry. Untrack it from the
androidApp/src/main/java/com/aether/mofe/platform/band/BroadcastBand.kt
import android.net.wifi.WifiManager
import android.util.Base64
import android.util.Log
import com.aether.mofe.platform.netcode.NetcodePeerBook
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
......
// meshId filter; everything else is scoped to our own mesh.
if (frame is BandFrame.MeshMetaPut) { onAdvert(frame); continue }
if (frame.meshId != meshId()) continue
// Netcode bootstrap: learn each peer's source IP (+ root flag) from the datagram
// the band already delivered — the netcode's peer-discovery seam. The band never
// carried IPs in-payload, but every UDP packet has one; NetcodePeerBook maps it to
// the netcode control port so a member can dial the leader (startAsMember).
if (frame is BandFrame.NodePut) {
NetcodePeerBook.observe(
frame.node.nodeId,
pkt.address?.hostAddress.orEmpty(),
isLeader = "ROOT" in frame.node.meshRoles,
)
}
onFrame(s, frame)
}
} catch (_: CancellationException) {
androidApp/src/main/java/com/aether/mofe/platform/netcode/NetcodeBringUp.kt
package com.aether.mofe.platform.netcode
import android.content.Context
import android.util.Log
import com.aether.mofe.data.MeshRepository
import com.aether.mofe.messaging.MeshNode
import com.aether.mofe.messaging.MeshNodeDependencies
import com.aether.mofe.messaging.MeshNodeRole
import com.aether.mofe.messaging.MeshTrustMaterial
import com.aether.mofe.messaging.MeshTrustReadiness
import com.aether.mofe.messaging.MessagingConfig
import com.aether.mofe.model.DeviceId
import com.aether.mofe.platform.AndroidMeshCryptoProvider
import com.aether.mofe.platform.AndroidPlatformClock
import com.aether.mofe.platform.AndroidRaftPersistence
import com.aether.mofe.platform.MofeEngineHost
import com.aether.mofe.platform.identity.DeviceIdentityProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.Inet4Address
import java.net.NetworkInterface
/**
* Constructs and starts the peer-to-peer netcode `MeshNode` on-device — the final go-live step. Every
* input now exists: the live engine ([MofeEngineHost.currentEngine]), the trust material (pinned CA
* key + this device's credential, cached from the last online enrollment — see [NetcodeTrust] and
* `MeshRepository`), the bootstrap leader address ([NetcodePeerBook], learned from band source IPs),
* and the Android platform impls (`AndroidMeshCryptoProvider`/`AndroidRaftPersistence`/
* `AndroidPlatformClock`, all of which satisfy the netcode interfaces).
*
* [maybeStart] is IDEMPOTENT and safe to call repeatedly: it no-ops once the node is up, and until
* then re-evaluates readiness on each call, so the app calls it after engine start AND on every band
* roster update. That retry is what handles join-time ordering — a non-root member cannot construct
* until the band has heard the root's address, and that sighting usually arrives AFTER startup. It
* fails SAFE: when trust isn't [MeshTrustReadiness.Ready], no leader is known, or the device has no
* LAN address, the netcode stays down and the band keeps operating exactly as before.
*
* NOTE: androidApp is not compilable in the headless authoring env — this is a reviewed wiring patch.
* The join-time retry and the single-clock bootstrap roster want an on-device compile + run to
* validate end to end (two devices exchanging `FusedStateSample`, clock offset/RTT converging).
*/
object NetcodeBringUp {
private const val TAG = "NetcodeBringUp"
@Volatile private var node: MeshNode? = null
/** Whether the netcode node is currently up (for diagnostics/UI). */
val isUp: Boolean get() = node != null
fun maybeStart(
context: Context,
engineHost: MofeEngineHost,
deviceIdentity: DeviceIdentityProvider,
repo: MeshRepository,
scope: CoroutineScope,
) {
if (node != null) return
val engine = engineHost.currentEngine() ?: return
val selfDeviceId = deviceIdentity.deviceIdV5()
val credentialJson = repo.gatewayCredential.value?.toString()
// Gate on cached trust material (cache from last enrollment): CA key pinned, credential
// present, matching THIS device + the active mesh, and unexpired. Offline we can't re-fetch,
// so a mismatch/expiry here means "re-enroll online", not "come up".
val readiness = MeshTrustMaterial.evaluate(
caKeyPresent = NetcodeTrust.meshCaPublicKey() != null,
credentialJson = credentialJson,
selfDeviceId = selfDeviceId,
selfEd25519PubB64 = deviceIdentity.ed25519PubB64,
activeMeshId = repo.currentMeshId ?: "",
nowMicros = System.currentTimeMillis() * 1_000L, // wall-clock epoch micros (credential.notAfter)
)
if (readiness !is MeshTrustReadiness.Ready) {
Log.i(TAG, "netcode deferred: trust not ready ($readiness)")
return
}
val identity = NetcodeTrust.deviceIdentity(deviceIdentity, credentialJson!!) ?: run {
Log.w(TAG, "netcode deferred: could not build DeviceIdentity (no raw seed?)")
return
}
val caKey = NetcodeTrust.meshCaPublicKey() ?: return
val role = resolveRole(repo, selfDeviceId)
val leader = if (role == MeshNodeRole.ROOT_ANCHOR) null else NetcodePeerBook.leaderAddress()
if (role != MeshNodeRole.ROOT_ANCHOR && leader == null) {
Log.i(TAG, "netcode deferred: leader address not learned from the band yet")
return
}
val selfHost = siteLocalIpv4() ?: run {
Log.w(TAG, "netcode deferred: no site-local IPv4 (no Wi-Fi/LAN?)")
return
}
val deps = MeshNodeDependencies(
selfId = DeviceId(selfDeviceId),
role = role,
engine = engine,
clock = AndroidPlatformClock(),
scope = scope,
ioDispatcher = Dispatchers.IO,
crypto = AndroidMeshCryptoProvider(),
identity = identity,
meshCaPublicKey = caKey,
persistence = AndroidRaftPersistence(context.applicationContext),
selfHost = selfHost,
leaderAddress = leader,
uplink = null, // gateway sync stays on GatewayUplinkController
config = MessagingConfig(),
)
val built = MeshNode.build(deps)
node = built
Log.i(TAG, "netcode up: role=$role selfHost=$selfHost leader=$leader")
scope.launch {
runCatching { built.start() }.onFailure {
Log.e(TAG, "netcode start failed — will retry on next band update", it)
node = null // allow a later maybeStart to retry
}
}
}
/** ROOT_ANCHOR if this device is the mesh root, else ANCHOR if it holds the anchor role, else LEARNER. */
private fun resolveRole(repo: MeshRepository, selfId: String): MeshNodeRole = when {
repo.currentAnchorId() == selfId -> MeshNodeRole.ROOT_ANCHOR
repo.isAnchorRole(selfId) -> MeshNodeRole.ANCHOR
else -> MeshNodeRole.LEARNER
}
}
/**
* This device's site-local IPv4 (192.168.x / 10.x / 172.16–31.x) for `NodeHello`'s `datagramHost`
* advertisement, or null if none (no Wi-Fi/LAN). Skips loopback and down interfaces and prefers a
* site-local address — a link-local (169.254.x) or public address would not be reachable by LAN peers.
*/
fun siteLocalIpv4(): String? = runCatching {
NetworkInterface.getNetworkInterfaces().asSequence()
.filter { it.isUp && !it.isLoopback }
.flatMap { it.inetAddresses.asSequence() }
.filterIsInstance<Inet4Address>()
.firstOrNull { it.isSiteLocalAddress && !it.isLoopbackAddress }
?.hostAddress
}.getOrNull()
androidApp/src/main/java/com/aether/mofe/platform/netcode/NetcodePeerBook.kt
package com.aether.mofe.platform.netcode
import com.aether.mofe.messaging.MessagingConfig
import com.aether.mofe.messaging.NodeAddress
import com.aether.mofe.messaging.PeerAddressBook
import com.aether.mofe.model.DeviceId
import com.aether.mofe.platform.AndroidPlatformClock
/**
* Process-wide netcode bootstrap roster. The band's UDP receive loop feeds every peer sighting here —
* the sender's SOURCE IP (which the band never carried in-payload, but every datagram delivers for
* free) plus whether that sender is the mesh root — and the netcode bring-up reads [leaderAddress] to
* open the member control-plane link (`MeshCoordinator.startAsMember`). This bridges the join-time
* chicken-and-egg gap (the band advertises UWB addresses, not IPs); once connected, peers exchange
* their advertised datagram addresses via `NodeHello`/the registry.
*
* Wraps the pure, clock-injected [PeerAddressBook] and owns ONE clock so `observe`/[leaderAddress]
* share a timebase — staleness is meaningless across two different clocks. The port is the netcode
* CONTROL port (47475): `startAsMember(leader, …)` dials the leader's control plane, and the returned
* [NodeAddress] carries that port.
*/
object NetcodePeerBook {
private val clock = AndroidPlatformClock()
private val book = PeerAddressBook(netcodePort = MessagingConfig().controlPort)
/** A band sighting: [nodeId] was heard from [sourceHost] (its UDP source IP); it is the mesh
* root/leader iff [isLeader] (the band node record carries the ROOT role). Blank hosts ignored. */
fun observe(nodeId: String, sourceHost: String, isLeader: Boolean) {
if (nodeId.isBlank() || sourceHost.isBlank()) return
book.observe(DeviceId(nodeId), sourceHost, isLeader, clock.now().microseconds)
}
/** The freshest non-stale root/leader control address, or null — the `startAsMember` bootstrap. */
fun leaderAddress(): NodeAddress? = book.leaderAddress(clock.now().microseconds)
/** A specific peer's control address if a fresh sighting exists, else null. */
fun address(nodeId: String): NodeAddress? = book.address(DeviceId(nodeId), clock.now().microseconds)
/** Drop all sightings — call on a mesh switch so old-mesh peers can't bootstrap the new mesh. */
fun clear() = book.clear()
}
(1-1/2)