Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a960f9a
Add Local Mesh Discovery feature
jamesarich Apr 29, 2026
0907e63
feat(discovery): improve scan metrics, node enrichment, and configura…
jamesarich Apr 30, 2026
edee2c9
refactor(discovery): improve KMP compatibility and clean up icon imports
jamesarich Apr 30, 2026
182cc33
refactor(discovery): reorder imports for clarity and consistency
jamesarich Apr 30, 2026
5792e09
feat(discovery): align state machine with spec, add deep links, fix t…
jamesarich May 7, 2026
b918a91
feat(discovery): add DiscoveryRankingEngine with 6-level deterministi…
jamesarich May 7, 2026
a90a6a5
docs(discovery): update tasks.md to reflect actual implementation status
jamesarich May 7, 2026
941ae3c
feat(discovery): wire DiscoveryRankingEngine into summary UI (D030)
jamesarich May 7, 2026
f8d98f1
docs(discovery): mark D030 complete in tasks.md
jamesarich May 7, 2026
531ea83
feat(discovery): add DiscoveryPrefs for persistent user defaults (D012)
jamesarich May 7, 2026
8be3c8a
docs(discovery): mark D012 complete
jamesarich May 7, 2026
e2e8483
feat(discovery): add 2.4 GHz hardware gating and AI provider tests (D…
jamesarich May 8, 2026
d1c5033
test(discovery): add DAO, packet collection, history, and deep-link t…
jamesarich May 8, 2026
d3eccf1
feat(discovery): add neighbor info requests at dwell boundaries and m…
jamesarich May 8, 2026
ee91bcd
test(discovery): add map preset filter and topology toggle tests (D028)
jamesarich May 8, 2026
349094e
feat(discovery): replace hardcoded UI strings with string resources (…
jamesarich May 8, 2026
d6f44f2
fix(discovery): resolve all detekt and lint issues across discovery m…
jamesarich May 8, 2026
2d1fc98
[Spec Kit] Implementation progress: D044 accessibility polish
jamesarich May 8, 2026
99ff328
fix(discovery): unregister packet collector on success, use string re…
jamesarich May 18, 2026
728678d
feat(discovery): extract hardcoded UI strings to resources
jamesarich May 18, 2026
4bf8aaf
docs(spec): update discovery spec to reflect implementation state
jamesarich May 18, 2026
f6bfefd
feat(discovery): wire Gemini Nano via ML Kit GenAI Prompt API
jamesarich May 18, 2026
8556fcc
test(discovery): add comprehensive DiscoverySummaryGenerator tests
jamesarich May 18, 2026
08885b7
fix(discovery): address design standards audit findings
jamesarich May 19, 2026
57eaa3c
feat(discovery): add Apple parity fixes - infrastructure tracking, se…
jamesarich May 19, 2026
48c58f3
feat(discovery): wire 2.4 GHz gating and export file-save, update spec
jamesarich May 20, 2026
80d3a1a
fix(navigation): correct SettingsGraph → Settings route reference pos…
jamesarich May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .skills/compose-ui/strings-index.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ dependencies {
implementation(projects.feature.map)
implementation(projects.feature.node)
implementation(projects.feature.settings)
implementation(projects.feature.discovery)
implementation(projects.feature.docs)
implementation(projects.feature.firmware)
implementation(projects.feature.wifiProvision)
Expand Down
6 changes: 6 additions & 0 deletions androidApp/src/main/kotlin/org/meshtastic/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider
import org.meshtastic.core.ui.util.LocalBarcodeScannerSupported
import org.meshtastic.core.ui.util.LocalDiscoveryMapProvider
import org.meshtastic.core.ui.util.LocalEventBranding
import org.meshtastic.core.ui.util.LocalInlineMapProvider
import org.meshtastic.core.ui.util.LocalMapMainScreenProvider
Expand Down Expand Up @@ -180,6 +181,7 @@ class MainActivity : AppCompatActivity() {

@Suppress("LongMethod")
@Composable
@Suppress("LongMethod")
private fun AppCompositionLocals(content: @Composable () -> Unit) {
val eventEdition by model.eventEdition.collectAsStateWithLifecycle()
CompositionLocalProvider(
Expand Down Expand Up @@ -211,6 +213,10 @@ class MainActivity : AppCompatActivity() {
modifier = modifier,
)
},
LocalDiscoveryMapProvider provides
{ userLat, userLon, nodes, modifier ->
org.meshtastic.app.map.discovery.DiscoveryMap(userLat, userLon, nodes, modifier)
},
LocalNodeMapScreenProvider provides
{ destNum, onNavigateUp ->
val vm = koinViewModel<NodeMapViewModel>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import org.meshtastic.core.service.di.CoreServiceModule
import org.meshtastic.core.takserver.di.CoreTakServerModule
import org.meshtastic.core.ui.di.CoreUiModule
import org.meshtastic.feature.connections.di.FeatureConnectionsModule
import org.meshtastic.feature.discovery.di.FeatureDiscoveryModule
import org.meshtastic.feature.docs.di.FeatureDocsModule
import org.meshtastic.feature.firmware.di.FeatureFirmwareModule
import org.meshtastic.feature.intro.di.FeatureIntroModule
Expand Down Expand Up @@ -86,6 +87,7 @@ import org.meshtastic.feature.wifiprovision.di.FeatureWifiProvisionModule
FeatureConnectionsModule::class,
FeatureMapModule::class,
FeatureSettingsModule::class,
FeatureDiscoveryModule::class,
FeatureDocsModule::class,
FeatureFirmwareModule::class,
FeatureIntroModule::class,
Expand Down
2 changes: 2 additions & 0 deletions androidApp/src/main/kotlin/org/meshtastic/app/ui/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.meshtastic.core.ui.component.MeshtasticNavDisplay
import org.meshtastic.core.ui.component.MeshtasticNavigationSuite
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.feature.connections.navigation.connectionsGraph
import org.meshtastic.feature.discovery.navigation.discoveryGraph
import org.meshtastic.feature.docs.navigation.docsEntries
import org.meshtastic.feature.firmware.navigation.firmwareGraph
import org.meshtastic.feature.map.navigation.mapGraph
Expand Down Expand Up @@ -88,6 +89,7 @@ fun MainScreen() {
mapGraph(backStack)
channelsGraph(backStack)
connectionsGraph(backStack)
discoveryGraph(backStack)
settingsGraph(backStack)
docsEntries(backStack)
firmwareGraph(backStack)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.navigation.NodesRoute
import org.meshtastic.feature.connections.navigation.connectionsGraph
import org.meshtastic.feature.discovery.navigation.discoveryGraph
import org.meshtastic.feature.firmware.navigation.firmwareGraph
import org.meshtastic.feature.map.navigation.mapGraph
import org.meshtastic.feature.messaging.navigation.contactsGraph
Expand All @@ -50,6 +51,7 @@ class NavigationAssemblyTest {
mapGraph(backStack)
channelsGraph(backStack)
connectionsGraph(backStack)
discoveryGraph(backStack)
settingsGraph(backStack)
firmwareGraph(backStack)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.app.map.discovery

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.meshtastic.core.ui.util.DiscoveryMapNode

/** Flavor-unified entry point for the discovery map. OSMDroid implementation. */
@Composable
fun DiscoveryMap(
userLatitude: Double,
userLongitude: Double,
nodes: List<DiscoveryMapNode>,
modifier: Modifier = Modifier,
) {
DiscoveryOsmMap(userLatitude = userLatitude, userLongitude = userLongitude, nodes = nodes, modifier = modifier)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@file:Suppress("MagicNumber")

package org.meshtastic.app.map.discovery

import android.graphics.Paint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import org.meshtastic.app.map.addCopyright
import org.meshtastic.app.map.addScaleBarOverlay
import org.meshtastic.app.map.model.CustomTileSource
import org.meshtastic.app.map.rememberMapViewWithLifecycle
import org.meshtastic.app.map.zoomIn
import org.meshtastic.core.ui.theme.DiscoveryMapColors
import org.meshtastic.core.ui.util.DiscoveryMapNode
import org.meshtastic.core.ui.util.DiscoveryNeighborType
import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline

private const val SINGLE_POINT_ZOOM = 14.0
private const val ZOOM_OUT_LEVELS = 0.5

/**
* OSMDroid implementation of the discovery map. Renders discovered node markers color-coded by neighbor type (green =
* direct, blue = mesh) with polylines from the user position to direct neighbors. Auto-zooms to fit all markers.
*/
@Composable
fun DiscoveryOsmMap(
userLatitude: Double,
userLongitude: Double,
nodes: List<DiscoveryMapNode>,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val density = LocalDensity.current
val hasValidUserPosition = userLatitude != 0.0 || userLongitude != 0.0
val userGeoPoint = remember(userLatitude, userLongitude) { GeoPoint(userLatitude, userLongitude) }
val validNodes = remember(nodes) { nodes.filter { it.latitude != 0.0 || it.longitude != 0.0 } }

// Build bounding box from all points
val allGeoPoints =
remember(validNodes, hasValidUserPosition) {
buildList {
if (hasValidUserPosition) add(userGeoPoint)
validNodes.forEach { add(GeoPoint(it.latitude, it.longitude)) }
}
}
val initialBounds =
remember(allGeoPoints) {
if (allGeoPoints.isEmpty()) BoundingBox() else BoundingBox.fromGeoPoints(allGeoPoints)
}

var hasCentered by remember { mutableStateOf(false) }

val mapView =
rememberMapViewWithLifecycle(
applicationId = context.packageName,
box = initialBounds,
tileSource = CustomTileSource.getTileSource(0),
)

// Camera auto-center once
LaunchedEffect(allGeoPoints) {
if (hasCentered || allGeoPoints.isEmpty()) return@LaunchedEffect
if (allGeoPoints.size == 1) {
mapView.controller.setCenter(allGeoPoints.first())
mapView.controller.setZoom(SINGLE_POINT_ZOOM)
} else {
mapView.zoomToBoundingBox(BoundingBox.fromGeoPoints(allGeoPoints).zoomIn(-ZOOM_OUT_LEVELS), true)
}
hasCentered = true
}

AndroidView(
modifier = modifier,
factory = { mapView.apply { setDestroyMode(false) } },
update = { map ->
map.overlays.clear()
map.addCopyright()
map.addScaleBarOverlay(density)

// User position marker
if (hasValidUserPosition) {
val userMarker =
Marker(map).apply {
position = userGeoPoint
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
title = "Your Position"
icon = context.getDrawable(android.R.drawable.ic_menu_mylocation)
}
map.overlays.add(userMarker)
}

// Node markers
validNodes.forEach { node ->
val nodeGeoPoint = GeoPoint(node.latitude, node.longitude)
val marker =
Marker(map).apply {
position = nodeGeoPoint
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
title = node.longName ?: node.shortName ?: "Unknown"
snippet = "SNR: ${node.snr} dB / RSSI: ${node.rssi} dBm"

val drawableId =
if (node.isSensorNode) {
org.meshtastic.app.R.drawable.ic_thermostat
} else {
org.meshtastic.app.R.drawable.ic_person
}
icon = context.getDrawable(drawableId)

// Default OSM marker handles color tinting via icon overlay or custom drawables if needed,
// but setting the icon directly overrides the default teardrop pin.
}
map.overlays.add(marker)
}

// Polylines from user to direct neighbors
if (hasValidUserPosition) {
validNodes
.filter { it.neighborType == DiscoveryNeighborType.DIRECT }
.forEach { node ->
val polyline =
Polyline().apply {
setPoints(listOf(userGeoPoint, GeoPoint(node.latitude, node.longitude)))
outlinePaint.apply {
color = DiscoveryMapColors.DirectLine.toArgb()
strokeWidth = with(density) { 3.dp.toPx() }
strokeCap = Paint.Cap.ROUND
style = Paint.Style.STROKE
}
}
map.overlays.add(polyline)
}
}

map.invalidate()
},
)
}
Loading
Loading