Skip to main content

Navigation & Deep Links

The app uses Navigation 3 with typed, serializable routes and centralized deep link resolution.

Route Architecture

All routes are defined in core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt.

Route Hierarchy

interface Route : NavKey // All routes implement NavKey
interface Graph : Route // Graph roots for navigation hierarchies

@Serializable
sealed interface SettingsRoute : Route {
@Serializable data class Settings(val destNum: Int? = null) : SettingsRoute, Graph
@Serializable data object DeviceConfiguration : SettingsRoute
@Serializable data object HelpDocs : SettingsRoute
@Serializable data class HelpDocPage(val pageId: String) : SettingsRoute
// ...
}

Conventions

  • Routes are @Serializable for state restoration
  • Use data object for routes without parameters
  • Use data class for parameterized routes
  • Group related routes under a sealed interface
  • Graph entry points implement both the route interface and Graph

DeepLinkRouter in core/navigation maps URI deep links to typed backstack lists.

URI Format

Both forms resolve through the same DeepLinkRouter, so any path below works with either scheme:

meshtastic://meshtastic/{path}
https://meshtastic.org/{path} # App Link, android:autoVerify — also opens in-app on a real device/adb

adb shell am start -a android.intent.action.VIEW -d "meshtastic://meshtastic/{path}" is the fastest way to trigger any route below from a shell or automation script without touching the UI.

For the https form to open in-app, each top-level path segment must also be declared as an android:pathPrefix in the android:autoVerify intent-filter in androidApp/src/main/AndroidManifest.xml — otherwise the link opens in the browser. Adding a new top-level route therefore takes three steps: add the segment to DeepLinkRouter.topLevelPathSegments (the router refuses to dispatch segments outside that set), add its when branch in DeepLinkRouter.route(), and add the matching pathPrefix to the manifest. DeepLinkManifestConsistencyTest (androidApp unit tests) checks the manifest against the set, so a missing manifest entry fails CI.

Source of truth: the always-current list of top-level segments is topLevelPathSegments in DeepLinkRouter — sub-paths live in the route() when block plus its helper maps (settingsSubRoutes, nodeDetailSubRoutes); the class-level KDoc is illustrative, not exhaustive. It also exists as executable spec in DeepLinkRouterTest.kt. The table below is a snapshot for quick reference — check those two files if it looks out of date.

URI PathRouteNotes
/connectionsConnectionsRoute.Connections(null)Connections screen
/connections?address={prefixedAddress}ConnectionsRoute.Connections(address)Auto-connects to a device without manual selection — the address uses the app's internal transport-prefixed format: t192.168.1.1:4403 (TCP), xAA:BB:CC:DD:EE:FF (BLE), s/dev/ttyUSB0 (serial). Intended for scripts/AI tooling driving the app.
/connections?address=nConnectionsRoute.Connections("n")Disconnects the current device instead of connecting (n = the internal "no device selected" sentinel).
/wifi-provisionWifiProvisionRoute.WifiProvision(null)WiFi provisioning screen
/wifi-provision?address={mac}WifiProvisionRoute.WifiProvision(mac)Provisioning targeting a specific device MAC
/settingsSettingsRoute.Settings(null)Settings root
/settings/helpDocsSettingsRoute.HelpDocsDocs browser
/settings/helpDocs/{pageId}SettingsRoute.HelpDocPage(pageId)Specific doc page
/settings/help-docsSettingsRoute.HelpDocsCompatibility alias
/discoveryDiscoveryRoute.DiscoveryGraphLocal Mesh Discovery entry point
/settings/local-mesh-discovery/session/{sessionId}DiscoveryRoute.DiscoverySummary(sessionId)Discovery session result
/nodesNodesRoute.NodesNode list
/nodes/{destNum}NodesRoute.NodeDetail(destNum)Node detail
/nodes/{destNum}/{metric}e.g. NodeDetailRoute.DeviceMetrics(destNum)Specific node metric tab (device-metrics, signal, power, traceroute, pax, neighbors, ...)
/messagesContactsRoute.ContactsConversation list
/messages/{contactKey}ContactsRoute.Messages(contactKey)Specific conversation
/share?message={text}ContactsRoute.Share(message)Share-to-contact composer
/quickchatContactsRoute.QuickChatQuick chat picker
/mapMapRoute.Map(null)Map view
/map/{waypointId}MapRoute.Map(waypointId)Map centered on a waypoint
/channelsChannelsRoute.ChannelsChannel list
/firmwareFirmwareRoute.FirmwareGraphFirmware screen
/firmware/updateFirmwareRoute.FirmwareUpdateFirmware update flow

Backstack Synthesis

Deep links synthesize a full backstack, not just the target screen:

// /settings/helpDocs/messages-and-channels produces:
listOf(
SettingsRoute.Settings(null),
SettingsRoute.HelpDocs,
SettingsRoute.HelpDocPage("messages-and-channels"),
)

This ensures the user can navigate "up" correctly.

  1. Define the typed route in Routes.kt.
  2. Add the mapping in DeepLinkRouter.settingsSubRoutes (or equivalent for other graphs).
  3. Add a test in DeepLinkRouterTest.kt.
  4. Register the navigation entry in the appropriate feature module.
  5. Update the KDoc list on DeepLinkRouter.route() and the table above — they're the two places tooling/agents look to discover what deep links exist.

Navigation Entry Registration

Each feature module provides entries via an extension function:

fun EntryProviderScope<NavKey>.docsEntries(backStack: NavBackStack<NavKey>) {
entry<SettingsRoute.HelpDocs> { DocsBrowserScreen(backStack) }
entry<SettingsRoute.HelpDocPage> { route -> DocsPageRouteScreen(route.pageId, backStack) }
}

These are called from the settings navigation composition.

Testing

Deep link routing is tested in:

core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt