Architecture
The Meshtastic Android and Desktop apps follow a modular Kotlin Multiplatform (KMP) architecture with clear layer boundaries. iOS is a compile-only validation target; no iOS app ships.
Layer Overview
┌─────────────────────────────────────────────┐
│ androidApp / desktopApp │ Platform entry points
├─────────────────────────────────────────────┤
│ feature/* modules │ UI + Business Logic
├─────────────────────────────────────────────┤
│ core/* modules │ Shared infrastructure
├─────────────────────────────────────────────┤
│ Platform (Android/JVM/iOS) │ OS-specific bindings
└─────────────────────────────────────────────┘
Module Categories
androidApp/ — Android Application
The Android application entry point:
- Activity, Application, and Manifest definitions
- Koin DI module composition (
AppKoinModule) - Flavor-specific bindings (
google/,fdroid/) - Android-only integrations (widgets, services)
desktopApp/ — Desktop JVM Application
The Desktop (Linux/macOS/Windows) entry point:
- Compose Desktop window management
- Desktop-specific DI (
DesktopKoinModule) - Platform stubs for Android-only capabilities
DesktopRadioTransportFactoryand a jSerialComm-based serial transport; the BLEBLE (Bluetooth Low Energy). The default wireless connection between a node and a phone client. and TCPTCP (Transmission Control Protocol). One of the three ways a client reaches a node, alongside serial and Bluetooth. Clients connect over TCP on port 4403. transport implementations it wires up are shared code — they live incore:network, built oncore:ble's BLEBLE (Bluetooth Low Energy). The default wireless connection between a node and a phone client. primitives — not desktopApp-owned
feature/* — Feature Modules
Each feature/ module owns a vertical slice of functionality:
| Module | Responsibility |
|---|---|
feature:intro | Onboarding/welcome flow |
feature:messaging | Messages, channels, contacts, quick chat |
feature:connections | Bluetooth/USBUSB (Universal Serial Bus). The wired connection used to power a device, flash firmware onto it, and communicate with it over serial./TCPTCP (Transmission Control Protocol). One of the three ways a client reaches a node, alongside serial and Bluetooth. Clients connect over TCP on port 4403. connection management |
feature:map | Map display, waypoints — shared state, policy and the waypoint editor |
feature:map-maplibre | MapLibre map surfaces — used by the fdroid flavor and Desktop; the google flavor uses Google Maps instead. Tile-source definitions and the custom-source editor are in feature:map, so both renderers share them |
feature:map-terrain | Offline terrain: elevation decode, hillshade shading and contour lines, shared by both map flavors |
feature:node | Node list, node detail, metrics |
feature:settings | All configuration screens |
feature:firmware | Firmware update flow |
feature:docs | In-app documentation browser |
feature:wifi-provision | Wi-Fi provisioning |
feature:widget | Android home screen widgets |
feature:discovery | Mesh network discovery |
Feature modules:
- Use the
meshtastic.kmp.featureconvention plugin - Depend on
coremodules, not on otherfeaturemodules; the one exception isfeature:map-maplibre, which builds onfeature:mapandfeature:map-terrain - Own their navigation entries and DI registrations
- Contain platform-specific implementations in
androidMain/jvmMain/iosMain
core/* — Core Modules
Shared infrastructure used by all features:
| Module | Responsibility |
|---|---|
core:common | Utilities, extensions, build config |
core:navigation | Routes, deep links, Navigation 3 |
core:ui | Shared Compose components, icons, theme |
core:resources | Shared string resources |
core:model | Domain models |
core:data | Data layer abstractions |
core:domain | Use cases / business logic |
core:database | Room KMP database |
core:datastore | DataStore preferences |
core:prefs | App preferences |
core:repository | Repository interfaces |
core:service | Mesh service layer |
core:di | DI utilities |
core:network | HTTP/serial/transport |
core:ble | Bluetooth LE abstractions |
core:barcode | QRQRA scannable square code. In Meshtastic it encodes the Channel URL, which carries the channel and LoRa settings needed to join a mesh. / barcode scanning (channel-share QRQRA scannable square code. In Meshtastic it encodes the Channel URL, which carries the channel and LoRa settings needed to join a mesh. codes) |
core:nfc | NFCNFC (Near Field Communication). A very short-range wireless protocol. Some clients expose NFC-related settings. read/write support |
core:takserver | Embedded TAKTAK (Team Awareness Kit). A situational awareness ecosystem. Meshtastic bridges to it by converting mesh messages to and from Cursor on Target format. server integration |
core:testing | Test utilities |
core:konsist | Konsist architecture/convention tests |
ProtobufProtobuf (Protocol Buffers). A method developed by Google for serializing structured data, used in Meshtastic for efficient communication protocol between devices. models come from the external org.meshtastic:protobufs Maven artifact (pinned in gradle/libs.versions.toml).
KMP Source Sets
Each module uses the standard KMP source set hierarchy:
src/
├── commonMain/ ← Shared code (all platforms)
├── commonTest/ ← Shared tests
├── androidMain/ ← Android-specific
├── jvmMain/ ← Desktop JVM-specific
├── jvmAndroidMain/ ← Shared by Android and desktop JVM
├── iosMain/ ← iOS-specific
├── nativeMain/ ← Kotlin/Native code, built for the one iOS target (iosSimulatorArm64)
├── jvmTest/ ← Desktop test host
├── androidHostTest/ ← Android host (JVM) unit tests
└── androidDeviceTest/ ← Instrumented tests (core:database, core:model)
jvmAndroidMain exists only in modules that apply meshtastic.kmp.jvm.android. nativeMain,
androidHostTest (modules that call withHostTest) and androidDeviceTest exist only in the
modules that need them.
Golden Rules:
- No
android.*imports incommonMain - Platform-specific code goes in appropriate source set
- Prefer interfaces + DI over
expect/actualfor complex behaviors - Use
expect/actualonly for simple declarations
Dependency Injection
The project uses Koin with annotation processing:
@Module,@Single,@Factoryannotations@ComponentScanfor automatic registration- Feature modules export their own
Feature*Moduleclass - App/Desktop compose all modules in their root DI configuration
Radio Control
Features issue radio commands through RadioController (core:repository), a composite of four
focused sub-interfaces so callers can depend on just the slice they need:
| Sub-interface | Responsibility |
|---|---|
AdminController | Config, channels, owner, device lifecycle, editSettings { } transactions |
MessagingController | Send packets, reactions, shared contacts |
NodeController | Favorite, ignore, mute, remove nodes |
QueryController | Telemetry, tracerouteTracerouteA request that reports the path a message took through the mesh. The result lists the nodes along the way in both directions, with the signal-to-noise ratio at each hop., position/user-info queries |
RadioControllerImpl (core:service) is the in-process composition root for all targets
(Desktop, iOS, single-process Android). It assembles the four sub-controllers via Kotlin interface
delegation and adds the cross-cutting concerns (connection state, packet-id, location,
device-address switching). Commands are direct suspend calls; admin writes are fire-and-forget
because the device is the source of truth (local persistence is an optimistic cache). The layered
shape mirrors the meshtastic-sdk
AdminApi/TelemetryApi design to ease a future SDK migration.
Service Repository
ServiceRepository is the reactive bridge between the mesh service and all feature/UI layers.
It is decomposed into focused provider interfaces following the Interface Segregation Principle:
| Interface | Responsibility |
|---|---|
ConnectionStateProvider | Read-only connectionState: StateFlow<ConnectionState> |
TracerouteResponseProvider | TracerouteTracerouteA request that reports the path a message took through the mesh. The result lists the nodes along the way in both directions, with the signal-to-noise ratio at each hop. response state + clear |
NeighborInfoResponseProvider | Neighbor info response state + clear |
ServiceStateWriter | Write-side for handlers (set*, emit*, clear*) |
ServiceRepository extends all four interfaces — consumers inject the narrowest interface
they actually need. For example, ContactsViewModel injects only ConnectionStateProvider
rather than the entire ServiceRepository, preventing accidental access to write operations
from UI code. RadioController also extends ConnectionStateProvider so VMs that already
inject a controller sub-interface can read connection state without a separate dependency.
Navigation
Navigation uses Navigation 3 with typed routes:
- All routes defined in
core/navigation/Routes.kt - Routes are
@Serializabledata classes/objects - Deep links resolved through
DeepLinkRouter - Each feature registers its own navigation entries
See Navigation & Deep Links for details.