Skip to content

Architecture

sputlab edited this page Nov 13, 2026 · 1 revision

Architecture

Clean MVVM in Kotlin and Jetpack Compose (Material 3), Coroutines and Flow/StateFlow throughout. The only non-Kotlin code is the vendored Codec2 speech codec (C, LGPL-2.1) behind a tiny JNI wrapper.

Layers

UI (Compose)  ->  MainViewModel (StateFlow)  ->  MeshEngine  ->  MeshMessenger  ->  MeshTransport  ->  radio
                                                    |                |
                                              storage, identity   protocol rules (JVM-testable)
  • The UI only observes MainViewModel. Anything with a rule in it (conversations, known nodes, positions, activity) is computed in the protocol package so it is JVM-tested.
  • Hardware radio APIs are strictly isolated behind MeshTransport, which moves opaque bytes between peers identified by a stable node ID.
  • The application protocol (framing, chat, ACKs, relay) lives in com.nodewave.mesh.protocol and must stay free of radio/BLE APIs.

Process model

The radio, identity, storage and messenger belong to MeshEngine (owned by NodeWaveApp) and run under RadioService, a foreground service. View models only observe. The radio is never tied to an activity or view-model lifecycle.

Package map (com.nodewave.mesh)

Package / file Role
MainActivity, MainViewModel Compose host, permission requests, thin StateFlow adapter
NodeWaveApp, MeshEngine, RadioService process-wide engine and foreground service
PositionTracker, VoiceController location and voice orchestration
ui/ AppShell and the five tabs (chats, network, map, nodes, settings), components (shared tactical look), theme
crypto/ Crypto, Hkdf, Sealed, HandshakeMessage, SessionManager, LocalIdentity, ContactCode. Pure JVM, no Android APIs
data/ framework SQLite (NodeWaveDb), MessageStore/PeerKeyStore implementations, KeystoreCipher, AppSettings, map files and downloader
protocol/ Packet, Router, RoutingTable, HelloPayload, MeshMessenger, Group, Position, DataFrame, TransferAssembler, Blob policy, LinkClass, LinkCodec, Directory, ChatMessage
transport/ MeshTransport, BleTransport, CompositeTransport, FastLink, WifiDirectLink, LinkBenchmark, RadioSettings, MeshLog
audio/ the only place with Android audio APIs: recorder, player, Opus and Codec2 codecs, image compressor

Key design rules

  • Concurrency: BLE callbacks run on arbitrary system threads, so they are wrapped with callbackFlow or run on Dispatchers.IO. Every GATT connection and write has a strict timeout (withTimeoutOrNull).
  • Logging: every radio state transition, error and permission check is logged with the tag MESH_RADIO.
  • Persistence: SQLite behind store interfaces (JVM tests use in-memory fakes). Message text is encrypted at rest with the Keystore.
  • Sessions: keys exist only in memory (SessionManager), are wiped when a session ends, and are never written to disk or logs. Only the identity key (Keystore-encrypted) and public keys are stored.
  • Transport links: the transport hands out opaque link ids. A packet may be sent back on the link it arrived on (SendOptions.viaLink), and a send that expects an answer keeps the link open briefly (expectReply). The messenger trusts a link hint only after the packet was authenticated, and never for relayed packets.
  • Bulk data: the transport reports a LinkClass per peer. Chunking, acknowledgement bitmaps, class rules for relays and reassembly limits live in protocol. A new kind of blob must go through BlobPolicy and be refused on slow links.
  • Fast link: Wi-Fi Direct is an optional FastLinkTransport behind MeshTransport.fastLink. The main radio finds peers and carries the sealed negotiation (LINK packets); the fast radio only moves bytes between two nodes that agreed. CompositeTransport hides the difference.
  • Dependencies: only open-source libraries under an AGPL-compatible licence: Codec2 (LGPL-2.1), AndroidX CameraX and ZXing core (Apache-2.0, contact QR codes), osmdroid with the Mapsforge renderer (Apache-2.0 / LGPL-3.0, maps). No proprietary dependency.

Relay

Multi-hop logic (routing, seen-cache, TTL, HELLO) lives in MeshMessenger, Router and RoutingTable. Router picks the next hop: direct neighbour, learned route, or flood. Routes come from neighbours' sealed HELLOs and expire after 150 s. Authenticated packets enter a seen-cache keyed by (senderId, messageId). Relay behaviour is covered by the in-memory MeshNetworkTest.

BLE transport

Each node advertises a GATT service (6e0d7a10-4e57-4c0e-9b1f-6d6573680001) with a write characteristic (...0002) and a notify characteristic (...0003), carries its node ID and name in the scan response, scans for others and connects on demand. A reply goes back over the same connection by notification. A watchdog restarts a scan or advertisement that stays silent, and idle links close quickly to free the radio.

Clone this wiki locally