feat(wire): migrate from protobuf -> wire (#4401)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-03 18:01:12 -06:00 committed by GitHub
parent 9dbc8b7fbf
commit 25657e8f8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
239 changed files with 7149 additions and 6144 deletions

View file

@ -119,11 +119,9 @@ import org.meshtastic.feature.map.component.NodeClusterMarkers
import org.meshtastic.feature.map.component.WaypointMarkers
import org.meshtastic.feature.map.model.NodeClusterItem
import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.MeshProtos.Position
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
import org.meshtastic.proto.waypoint
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.Position
import org.meshtastic.proto.Waypoint
import kotlin.math.abs
import kotlin.math.max
@ -295,12 +293,12 @@ fun MapView(
val nodeClusterItems =
displayNodes.map { node ->
val latLng = LatLng(node.position.latitudeI * DEG_D, node.position.longitudeI * DEG_D)
val latLng = LatLng((node.position.latitude_i ?: 0) * DEG_D, (node.position.longitude_i ?: 0) * DEG_D)
NodeClusterItem(
node = node,
nodePosition = latLng,
nodeTitle = "${node.user.shortName} ${formatAgo(node.position.time)}",
nodeSnippet = "${node.user.longName}",
nodeTitle = "${node.user.short_name} ${formatAgo(node.position.time)}",
nodeSnippet = "${node.user.long_name}",
)
}
val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle()
@ -438,10 +436,11 @@ fun MapView(
MapProperties(mapType = effectiveGoogleMapType, isMyLocationEnabled = hasLocationPermission),
onMapLongClick = { latLng ->
if (isConnected) {
val newWaypoint = waypoint {
latitudeI = (latLng.latitude / DEG_D).toInt()
longitudeI = (latLng.longitude / DEG_D).toInt()
}
val newWaypoint =
Waypoint(
latitude_i = (latLng.latitude / DEG_D).toInt(),
longitude_i = (latLng.longitude / DEG_D).toInt(),
)
editingWaypoint = newWaypoint
}
},
@ -617,18 +616,18 @@ fun MapView(
onSendClicked = { updatedWp ->
var finalWp = updatedWp
if (updatedWp.id == 0) {
finalWp = finalWp.copy { id = mapViewModel.generatePacketId() ?: 0 }
finalWp = finalWp.copy(id = mapViewModel.generatePacketId() ?: 0)
}
if (updatedWp.icon == 0) {
finalWp = finalWp.copy { icon = 0x1F4CD }
if ((updatedWp.icon ?: 0) == 0) {
finalWp = finalWp.copy(icon = 0x1F4CD)
}
mapViewModel.sendWaypoint(finalWp)
editingWaypoint = null
},
onDeleteClicked = { wpToDelete ->
if (wpToDelete.lockedTo == 0 && isConnected && wpToDelete.id != 0) {
val deleteMarkerWp = wpToDelete.copy { expire = 1 }
if ((wpToDelete.locked_to ?: 0) == 0 && isConnected && wpToDelete.id != 0) {
val deleteMarkerWp = wpToDelete.copy(expire = 1)
mapViewModel.sendWaypoint(deleteMarkerWp)
}
mapViewModel.deleteWaypoint(wpToDelete.id)
@ -683,25 +682,25 @@ fun MapView(
followPhoneBearing = followPhoneBearing,
)
}
if (showLayersBottomSheet) {
ModalBottomSheet(onDismissRequest = { showLayersBottomSheet = false }) {
CustomMapLayersSheet(mapLayers, onToggleVisibility, onRemoveLayer, onAddLayerClicked)
}
}
if (showLayersBottomSheet) {
ModalBottomSheet(onDismissRequest = { showLayersBottomSheet = false }) {
CustomMapLayersSheet(mapLayers, onToggleVisibility, onRemoveLayer, onAddLayerClicked)
}
showClusterItemsDialog?.let {
ClusterItemsListDialog(
items = it,
onDismiss = { showClusterItemsDialog = null },
onItemClick = { item ->
navigateToNodeDetails(item.node.num)
showClusterItemsDialog = null
},
)
}
if (showCustomTileManagerSheet) {
ModalBottomSheet(onDismissRequest = { showCustomTileManagerSheet = false }) {
CustomTileProviderManagerSheet(mapViewModel = mapViewModel)
}
}
showClusterItemsDialog?.let {
ClusterItemsListDialog(
items = it,
onDismiss = { showClusterItemsDialog = null },
onItemClick = { item ->
navigateToNodeDetails(item.node.num)
showClusterItemsDialog = null
},
)
}
if (showCustomTileManagerSheet) {
ModalBottomSheet(onDismissRequest = { showCustomTileManagerSheet = false }) {
CustomTileProviderManagerSheet(mapViewModel = mapViewModel)
}
}
}
@ -763,25 +762,28 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayU
Card {
Column(modifier = Modifier.padding(8.dp)) {
PositionRow(label = stringResource(Res.string.latitude), value = "%.5f".format(position.latitudeI * DEG_D))
PositionRow(
label = stringResource(Res.string.latitude),
value = "%.5f".format((position.latitude_i ?: 0) * DEG_D),
)
PositionRow(
label = stringResource(Res.string.longitude),
value = "%.5f".format(position.longitudeI * DEG_D),
value = "%.5f".format((position.longitude_i ?: 0) * DEG_D),
)
PositionRow(label = stringResource(Res.string.sats), value = position.satsInView.toString())
PositionRow(label = stringResource(Res.string.sats), value = position.sats_in_view?.toString() ?: "")
PositionRow(
label = stringResource(Res.string.alt),
value = position.altitude.metersIn(displayUnits).toString(displayUnits),
value = (position.altitude ?: 0).metersIn(displayUnits).toString(displayUnits),
)
PositionRow(label = stringResource(Res.string.speed), value = speedFromPosition(position, displayUnits))
PositionRow(
label = stringResource(Res.string.heading),
value = "%.0f°".format(position.groundTrack * HEADING_DEG),
value = "%.0f°".format((position.ground_track ?: 0) * HEADING_DEG),
)
PositionRow(label = stringResource(Res.string.timestamp), value = position.formatPositionTime())
@ -791,13 +793,13 @@ private fun PositionInfoWindowContent(position: Position, displayUnits: DisplayU
@Composable
private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): String {
val speedInMps = position.groundSpeed
val speedInMps = position.ground_speed ?: 0
val mpsText = "%d m/s".format(speedInMps)
val speedText =
if (speedInMps > 10) {
when (displayUnits) {
DisplayUnits.METRIC -> "%.1f Km/h".format(position.groundSpeed.mpsToKmph())
DisplayUnits.IMPERIAL -> "%.1f mph".format(position.groundSpeed.mpsToMph())
DisplayUnits.METRIC -> "%.1f Km/h".format(speedInMps.mpsToKmph())
DisplayUnits.IMPERIAL -> "%.1f mph".format(speedInMps.mpsToMph())
else -> mpsText // Fallback or handle UNRECOGNIZED
}
} else {
@ -806,11 +808,11 @@ private fun speedFromPosition(position: Position, displayUnits: DisplayUnits): S
return speedText
}
internal fun Position.toLatLng(): LatLng = LatLng(this.latitudeI * DEG_D, this.longitudeI * DEG_D)
internal fun Position.toLatLng(): LatLng = LatLng((this.latitude_i ?: 0) * DEG_D, (this.longitude_i ?: 0) * DEG_D)
private fun Node.toLatLng(): LatLng? = this.position.toLatLng()
private fun Waypoint.toLatLng(): LatLng = LatLng(this.latitudeI * DEG_D, this.longitudeI * DEG_D)
private fun Waypoint.toLatLng(): LatLng = LatLng((this.latitude_i ?: 0) * DEG_D, (this.longitude_i ?: 0) * DEG_D)
private fun offsetPolyline(
points: List<LatLng>,

View file

@ -58,7 +58,7 @@ import org.meshtastic.core.prefs.map.GoogleMapsPrefs
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ConfigProtos
import org.meshtastic.proto.Config
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -133,8 +133,8 @@ constructor(
val displayUnits =
radioConfigRepository.deviceProfileFlow
.mapNotNull { it.config.display.units }
.stateInWhileSubscribed(initialValue = ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC)
.mapNotNull { it.config?.display?.units }
.stateInWhileSubscribed(initialValue = Config.DisplayConfig.DisplayUnits.METRIC)
fun addCustomTileProvider(name: String, urlTemplate: String) {
viewModelScope.launch {
@ -270,7 +270,7 @@ constructor(
val wpMap = waypoints.first { it.containsKey(wpId) }
wpMap[wpId]?.let { packet ->
val waypoint = packet.data.waypoint!!
val latLng = LatLng(waypoint.latitudeI / 1e7, waypoint.longitudeI / 1e7)
val latLng = LatLng((waypoint.latitude_i ?: 0) / 1e7, (waypoint.longitude_i ?: 0) / 1e7)
cameraPositionState.position = CameraPosition.fromLatLngZoom(latLng, 15f)
}
}

View file

@ -74,8 +74,7 @@ import org.meshtastic.core.strings.time
import org.meshtastic.core.strings.waypoint_edit
import org.meshtastic.core.strings.waypoint_new
import org.meshtastic.core.ui.emoji.EmojiPickerDialog
import org.meshtastic.proto.MeshProtos.Waypoint
import org.meshtastic.proto.copy
import org.meshtastic.proto.Waypoint
import java.util.Calendar
import java.util.TimeZone
@ -92,7 +91,7 @@ fun EditWaypointDialog(
var waypointInput by remember { mutableStateOf(waypoint) }
val title = if (waypoint.id == 0) Res.string.waypoint_new else Res.string.waypoint_edit
val defaultEmoji = 0x1F4CD // 📍 Round Pushpin
val currentEmojiCodepoint = if (waypointInput.icon == 0) defaultEmoji else waypointInput.icon
val currentEmojiCodepoint = if ((waypointInput.icon ?: 0) == 0) defaultEmoji else waypointInput.icon!!
var showEmojiPickerView by remember { mutableStateOf(false) }
val context = LocalContext.current
@ -102,7 +101,7 @@ fun EditWaypointDialog(
var selectedDateString by remember { mutableStateOf("") }
var selectedTimeString by remember { mutableStateOf("") }
var isExpiryEnabled by remember {
mutableStateOf(waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE)
mutableStateOf((waypointInput.expire ?: 0) != 0 && waypointInput.expire != Int.MAX_VALUE)
}
val dateFormat = remember { android.text.format.DateFormat.getDateFormat(context) }
@ -111,15 +110,16 @@ fun EditWaypointDialog(
timeFormat.timeZone = TimeZone.getDefault()
LaunchedEffect(waypointInput.expire, isExpiryEnabled) {
val expireValue = waypointInput.expire ?: 0
if (isExpiryEnabled) {
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
calendar.timeInMillis = waypointInput.expire * 1000L
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
calendar.timeInMillis = expireValue * 1000L
selectedDateString = dateFormat.format(calendar.time)
selectedTimeString = timeFormat.format(calendar.time)
} else { // If enabled but not set, default to 8 hours from now
calendar.timeInMillis = System.currentTimeMillis()
calendar.add(Calendar.HOUR_OF_DAY, 8)
waypointInput = waypointInput.copy { expire = (calendar.timeInMillis / 1000).toInt() }
waypointInput = waypointInput.copy(expire = (calendar.timeInMillis / 1000).toInt())
}
} else {
selectedDateString = ""
@ -141,8 +141,8 @@ fun EditWaypointDialog(
text = {
Column(modifier = modifier.fillMaxWidth()) {
OutlinedTextField(
value = waypointInput.name,
onValueChange = { waypointInput = waypointInput.copy { name = it.take(29) } },
value = waypointInput.name ?: "",
onValueChange = { waypointInput = waypointInput.copy(name = it.take(29)) },
label = { Text(stringResource(Res.string.name)) },
singleLine = true,
keyboardOptions =
@ -162,8 +162,8 @@ fun EditWaypointDialog(
)
Spacer(modifier = Modifier.size(8.dp))
OutlinedTextField(
value = waypointInput.description,
onValueChange = { waypointInput = waypointInput.copy { description = it.take(99) } },
value = waypointInput.description ?: "",
onValueChange = { waypointInput = waypointInput.copy(description = it.take(99)) },
label = { Text(stringResource(Res.string.description)) },
keyboardOptions =
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
@ -187,8 +187,8 @@ fun EditWaypointDialog(
Text(stringResource(Res.string.locked))
}
Switch(
checked = waypointInput.lockedTo != 0,
onCheckedChange = { waypointInput = waypointInput.copy { lockedTo = if (it) 1 else 0 } },
checked = (waypointInput.locked_to ?: 0) != 0,
onCheckedChange = { waypointInput = waypointInput.copy(locked_to = if (it) 1 else 0) },
)
}
Spacer(modifier = Modifier.size(8.dp))
@ -210,17 +210,17 @@ fun EditWaypointDialog(
onCheckedChange = { checked ->
isExpiryEnabled = checked
if (checked) {
val expireValue = waypointInput.expire ?: 0
// Default to 8 hours from now if not already set
if (waypointInput.expire == 0 || waypointInput.expire == Int.MAX_VALUE) {
if (expireValue == 0 || expireValue == Int.MAX_VALUE) {
val cal = Calendar.getInstance()
cal.timeInMillis = System.currentTimeMillis()
cal.add(Calendar.HOUR_OF_DAY, 8)
waypointInput =
waypointInput.copy { expire = (cal.timeInMillis / 1000).toInt() }
waypointInput = waypointInput.copy(expire = (cal.timeInMillis / 1000).toInt())
}
// LaunchedEffect will update date/time strings
} else {
waypointInput = waypointInput.copy { expire = Int.MAX_VALUE }
waypointInput = waypointInput.copy(expire = Int.MAX_VALUE)
}
},
)
@ -229,8 +229,9 @@ fun EditWaypointDialog(
if (isExpiryEnabled) {
val currentCalendar =
Calendar.getInstance().apply {
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
timeInMillis = waypointInput.expire * 1000L
val expireValue = waypointInput.expire ?: 0
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
timeInMillis = expireValue * 1000L
} else {
timeInMillis = System.currentTimeMillis()
add(Calendar.HOUR_OF_DAY, 8) // Default if re-enabling
@ -247,14 +248,14 @@ fun EditWaypointDialog(
context,
{ _: DatePicker, selectedYear: Int, selectedMonth: Int, selectedDay: Int ->
val tempCal = Calendar.getInstance()
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
tempCal.timeInMillis = waypointInput.expire * 1000L
val expireValue = waypointInput.expire ?: 0
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
tempCal.timeInMillis = expireValue * 1000L
} else {
tempCal.add(Calendar.HOUR_OF_DAY, 8)
}
tempCal.set(selectedYear, selectedMonth, selectedDay)
waypointInput =
waypointInput.copy { expire = (tempCal.timeInMillis / 1000).toInt() }
waypointInput = waypointInput.copy(expire = (tempCal.timeInMillis / 1000).toInt())
},
year,
month,
@ -267,15 +268,15 @@ fun EditWaypointDialog(
{ _: TimePicker, selectedHour: Int, selectedMinute: Int ->
// Keep the existing date part
val tempCal = Calendar.getInstance()
if (waypointInput.expire != 0 && waypointInput.expire != Int.MAX_VALUE) {
tempCal.timeInMillis = waypointInput.expire * 1000L
val expireValue = waypointInput.expire ?: 0
if (expireValue != 0 && expireValue != Int.MAX_VALUE) {
tempCal.timeInMillis = expireValue * 1000L
} else {
tempCal.add(Calendar.HOUR_OF_DAY, 8)
}
tempCal.set(Calendar.HOUR_OF_DAY, selectedHour)
tempCal.set(Calendar.MINUTE, selectedMinute)
waypointInput =
waypointInput.copy { expire = (tempCal.timeInMillis / 1000).toInt() }
waypointInput = waypointInput.copy(expire = (tempCal.timeInMillis / 1000).toInt())
},
hour,
minute,
@ -324,7 +325,10 @@ fun EditWaypointDialog(
TextButton(onClick = onDismissRequest, modifier = Modifier.padding(end = 8.dp)) {
Text(stringResource(Res.string.cancel))
}
Button(onClick = { onSendClicked(waypointInput) }, enabled = waypointInput.name.isNotBlank()) {
Button(
onClick = { onSendClicked(waypointInput) },
enabled = (waypointInput.name ?: "").isNotBlank(),
) {
Text(stringResource(Res.string.send))
}
}
@ -335,7 +339,7 @@ fun EditWaypointDialog(
} else {
EmojiPickerDialog(onDismiss = { showEmojiPickerView = false }) { selectedEmoji ->
showEmojiPickerView = false
waypointInput = waypointInput.copy { icon = selectedEmoji.codePointAt(0) }
waypointInput = waypointInput.copy(icon = selectedEmoji.codePointAt(0))
}
}
}

View file

@ -29,18 +29,18 @@ import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.locked
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.feature.map.BaseMapViewModel
import org.meshtastic.proto.MeshProtos
import org.meshtastic.proto.Waypoint
private const val DEG_D = 1e-7
@Composable
fun WaypointMarkers(
displayableWaypoints: List<MeshProtos.Waypoint>,
displayableWaypoints: List<Waypoint>,
mapFilterState: BaseMapViewModel.MapFilterState,
myNodeNum: Int,
isConnected: Boolean,
unicodeEmojiToBitmapProvider: (Int) -> BitmapDescriptor,
onEditWaypointRequest: (MeshProtos.Waypoint) -> Unit,
onEditWaypointRequest: (Waypoint) -> Unit,
selectedWaypointId: Int? = null,
) {
val scope = rememberCoroutineScope()
@ -48,7 +48,9 @@ fun WaypointMarkers(
if (mapFilterState.showWaypoints) {
displayableWaypoints.forEach { waypoint ->
val markerState =
rememberUpdatedMarkerState(position = LatLng(waypoint.latitudeI * DEG_D, waypoint.longitudeI * DEG_D))
rememberUpdatedMarkerState(
position = LatLng((waypoint.latitude_i ?: 0) * DEG_D, (waypoint.longitude_i ?: 0) * DEG_D),
)
LaunchedEffect(selectedWaypointId) {
if (selectedWaypointId == waypoint.id) {
@ -59,16 +61,16 @@ fun WaypointMarkers(
Marker(
state = markerState,
icon =
if (waypoint.icon == 0) {
if ((waypoint.icon ?: 0) == 0) {
unicodeEmojiToBitmapProvider(PUSHPIN) // Default icon (Round Pushpin)
} else {
unicodeEmojiToBitmapProvider(waypoint.icon)
unicodeEmojiToBitmapProvider(waypoint.icon!!)
},
title = waypoint.name.replace('\n', ' ').replace('\b', ' '),
snippet = waypoint.description.replace('\n', ' ').replace('\b', ' '),
title = (waypoint.name ?: "").replace('\n', ' ').replace('\b', ' '),
snippet = (waypoint.description ?: "").replace('\n', ' ').replace('\b', ' '),
visible = true,
onInfoWindowClick = {
if (waypoint.lockedTo == 0 || waypoint.lockedTo == myNodeNum || !isConnected) {
if ((waypoint.locked_to ?: 0) == 0 || waypoint.locked_to == myNodeNum || !isConnected) {
onEditWaypointRequest(waypoint)
} else {
scope.launch { context.showToast(Res.string.locked) }

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-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
@ -14,7 +14,6 @@
* 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.feature.map.model
import com.google.android.gms.maps.model.LatLng
@ -45,6 +44,6 @@ data class NodeClusterItem(val node: Node, val nodePosition: LatLng, val nodeTit
18 to 91.182212,
19 to 45.58554,
)
return precisionMap[this.node.position.precisionBits]
return precisionMap[this.node.position.precision_bits ?: 0]
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* Copyright (c) 2025-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
@ -14,7 +14,6 @@
* 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.feature.map.node
import androidx.compose.foundation.layout.Box
@ -36,7 +35,7 @@ fun NodeMapScreen(nodeMapViewModel: NodeMapViewModel, onNavigateUp: () -> Unit)
Scaffold(
topBar = {
MainAppBar(
title = node?.user?.longName ?: "",
title = node?.user?.long_name ?: "",
ourNode = null,
showNodeChip = false,
canNavigateUp = true,