Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2652c5b
Add Local Mesh Discovery feature
jamesarich Apr 29, 2026
052bb05
feat(discovery): improve scan metrics, node enrichment, and configura…
jamesarich Apr 30, 2026
70ba022
refactor(discovery): improve KMP compatibility and clean up icon imports
jamesarich Apr 30, 2026
35286d8
refactor(discovery): reorder imports for clarity and consistency
jamesarich Apr 30, 2026
2c1d4cd
feat(discovery): align state machine with spec, add deep links, fix t…
jamesarich May 7, 2026
a91b462
feat(discovery): add DiscoveryRankingEngine with 6-level deterministi…
jamesarich May 7, 2026
c21ed4f
docs(discovery): update tasks.md to reflect actual implementation status
jamesarich May 7, 2026
f7879c5
feat(discovery): wire DiscoveryRankingEngine into summary UI (D030)
jamesarich May 7, 2026
25e1bdf
docs(discovery): mark D030 complete in tasks.md
jamesarich May 7, 2026
9cd89ab
feat(discovery): add DiscoveryPrefs for persistent user defaults (D012)
jamesarich May 7, 2026
7c78b41
docs(discovery): mark D012 complete
jamesarich May 7, 2026
5afffba
feat(discovery): add 2.4 GHz hardware gating and AI provider tests (D…
jamesarich May 8, 2026
7d9290a
test(discovery): add DAO, packet collection, history, and deep-link t…
jamesarich May 8, 2026
95430ec
feat(discovery): add neighbor info requests at dwell boundaries and m…
jamesarich May 8, 2026
bd204ab
test(discovery): add map preset filter and topology toggle tests (D028)
jamesarich May 8, 2026
8328320
feat(discovery): replace hardcoded UI strings with string resources (…
jamesarich May 8, 2026
10da02f
fix(discovery): resolve all detekt and lint issues across discovery m…
jamesarich May 8, 2026
bc9de0a
[Spec Kit] Implementation progress: D044 accessibility polish
jamesarich May 8, 2026
5493a58
fix(discovery): unregister packet collector on success, use string re…
jamesarich May 18, 2026
f917d0a
feat(discovery): extract hardcoded UI strings to resources
jamesarich May 18, 2026
74285d7
docs(spec): update discovery spec to reflect implementation state
jamesarich May 18, 2026
a05fc32
feat(discovery): wire Gemini Nano via ML Kit GenAI Prompt API
jamesarich May 18, 2026
6e94a87
test(discovery): add comprehensive DiscoverySummaryGenerator tests
jamesarich May 18, 2026
4db6184
fix(discovery): address design standards audit findings
jamesarich May 19, 2026
dcd0a88
feat(discovery): add Apple parity fixes - infrastructure tracking, se…
jamesarich May 19, 2026
600abad
feat(discovery): wire 2.4 GHz gating and export file-save, update spec
jamesarich May 20, 2026
4c60e5d
fix(navigation): correct SettingsGraph → Settings route reference pos…
jamesarich May 20, 2026
33bd343
fix(database): use BundledSQLiteDriver for consistent test behavior
jamesarich May 31, 2026
e52244e
fix: align proto submodule with main (a0a2239)
jamesarich May 31, 2026
bcd8f7f
fix: resolve compilation errors from rebase onto main
jamesarich May 31, 2026
1edc6de
fix: resolve CI failures — missing drawables, spotless, and test nati…
jamesarich May 31, 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
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()
},
)
}
9 changes: 9 additions & 0 deletions androidApp/src/fdroid/res/drawable/ic_person.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,720L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,720Q800,753 776.5,776.5Q753,800 720,800L240,800Q207,800 183.5,776.5Q160,753 160,720Z"/>
</vector>
9 changes: 9 additions & 0 deletions androidApp/src/fdroid/res/drawable/ic_thermostat.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M560,440Q543,440 531.5,428.5Q520,417 520,400Q520,383 531.5,371.5Q543,360 560,360L680,360Q697,360 708.5,371.5Q720,383 720,400Q720,417 708.5,428.5Q697,440 680,440L560,440ZM560,280Q543,280 531.5,268.5Q520,257 520,240Q520,223 531.5,211.5Q543,200 560,200L800,200Q817,200 828.5,211.5Q840,223 840,240Q840,257 828.5,268.5Q817,280 800,280L560,280ZM320,840Q237,840 178.5,781.5Q120,723 120,640Q120,592 141,550.5Q162,509 200,480L200,240Q200,190 235,155Q270,120 320,120Q370,120 405,155Q440,190 440,240L440,480Q478,509 499,550.5Q520,592 520,640Q520,723 461.5,781.5Q403,840 320,840ZM200,640L440,640Q440,611 427.5,586Q415,561 392,544L360,520L360,240Q360,223 348.5,211.5Q337,200 320,200Q303,200 291.5,211.5Q280,223 280,240L280,520L248,544Q225,561 212.5,586Q200,611 200,640Z"/>
</vector>
Loading
Loading