diff --git a/app/src/androidTest/java/com/geeksville/mesh/NodeInfoDaoTest.kt b/app/src/androidTest/java/com/geeksville/mesh/NodeInfoDaoTest.kt index c6da0d415..059206b91 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/NodeInfoDaoTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/NodeInfoDaoTest.kt @@ -140,7 +140,9 @@ class NodeInfoDaoTest { @Test fun testSortByDistance() = runBlocking { val nodes = getNodes(sort = NodeSortOption.DISTANCE) - val sortedNodes = nodes.sortedBy { it.distance(ourNode) } + val sortedNodes = nodes.sortedWith( // nodes with invalid (null) positions at the end + compareBy { it.validPosition == null }.thenBy { it.distance(ourNode) } + ) assertEquals(sortedNodes, nodes) } diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt index 2570031d3..96d7408ee 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt @@ -89,8 +89,56 @@ data class NodeEntity( longitude = degD(p.longitudeI) } + private fun hasValidPosition(): Boolean { + return latitude != 0.0 && longitude != 0.0 && + (latitude >= -90 && latitude <= 90.0) && + (longitude >= -180 && longitude <= 180) + } + + val validPosition: MeshProtos.Position? get() = position.takeIf { hasValidPosition() } + // @return distance in meters to some other node (or null if unknown) - fun distance(o: NodeEntity) = latLongToMeter(latitude, longitude, o.latitude, o.longitude) + fun distance(o: NodeEntity): Int? { + return if (validPosition == null || o.validPosition == null) null + else latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt() + } + + private fun TelemetryProtos.EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String { + val temp = if (temperature != 0f) { + if (isFahrenheit) { + val fahrenheit = temperature * 1.8F + 32 + "%.1f°F".format(fahrenheit) + } else { + "%.1f°C".format(temperature) + } + } else null + val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null + val pressure = if (barometricPressure != 0f) "%.1fhPa".format(barometricPressure) else null + val gas = if (gasResistance != 0f) "%.0fMΩ".format(gasResistance) else null + val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null + val current = if (current != 0f) "%.1fmA".format(current) else null + val iaq = if (iaq != 0) "IAQ: $iaq" else null + + return listOfNotNull( + temp, + humidity, + pressure, + gas, + voltage, + current, + iaq, + ).joinToString(" ") + } + + private fun PaxcountProtos.Paxcount.getDisplayString() = + "PAX: ${ble + wifi} (B:$ble/W:$wifi)".takeIf { ble != 0 && wifi != 0 } + + fun getTelemetryString(isFahrenheit: Boolean = false): String { + return listOfNotNull( + paxcounter.getDisplayString(), + environmentMetrics.getDisplayString(isFahrenheit) + ).joinToString(" ") + } /** * true if the device was heard from recently diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt index 3234c0c96..6f9f3e328 100644 --- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt +++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt @@ -3,10 +3,8 @@ package com.geeksville.mesh.model import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.geeksville.mesh.MyNodeInfo -import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.database.dao.NodeInfoDao import com.geeksville.mesh.database.entity.NodeEntity -import com.geeksville.mesh.database.entity.toNodeInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,8 +24,8 @@ class NodeDB @Inject constructor( val myNodeInfo: StateFlow get() = _myNodeInfo // our node info - private val _ourNodeInfo = MutableStateFlow(null) - val ourNodeInfo: StateFlow get() = _ourNodeInfo + private val _ourNodeInfo = MutableStateFlow(null) + val ourNodeInfo: StateFlow get() = _ourNodeInfo // The unique userId of our node private val _myId = MutableStateFlow(null) @@ -47,7 +45,7 @@ class NodeDB @Inject constructor( nodeInfoDao.nodeDBbyNum().onEach { _nodeDBbyNum.value = it - val ourNodeInfo = it.values.firstOrNull()?.toNodeInfo() + val ourNodeInfo = it.values.firstOrNull() _ourNodeInfo.value = ourNodeInfo _myId.value = ourNodeInfo?.user?.id }.launchIn(processLifecycle.coroutineScope) diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index abdda95ed..71442fdb1 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -240,7 +240,7 @@ class UIViewModel @Inject constructor( // hardware info about our local device (can be null) val myNodeInfo: StateFlow get() = nodeDB.myNodeInfo - val ourNodeInfo: StateFlow get() = nodeDB.ourNodeInfo + val ourNodeInfo: StateFlow get() = nodeDB.ourNodeInfo // FIXME only used in MapFragment val initialNodes get() = nodeDB.nodeDBbyNum.value.values.map { it.toNodeInfo() } @@ -545,10 +545,15 @@ class UIViewModel @Inject constructor( } } - fun setOwner(user: MeshUser) { + fun setOwner(name: String) { + val user = ourNodeInfo.value?.user?.copy { + longName = name + shortName = getInitials(name) + } ?: return + try { // Note: we use ?. here because we might be running in the emulator - meshService?.setOwner(user) + meshService?.setRemoteOwner(myNodeNum ?: return, user.toByteArray()) } catch (ex: RemoteException) { errormsg("Can't set username on device, is device offline? ${ex.message}") } diff --git a/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt b/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt index d6e22c275..ce0781900 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt @@ -3,7 +3,6 @@ package com.geeksville.mesh.ui import android.content.ActivityNotFoundException import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -11,91 +10,79 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.geeksville.mesh.Position -import com.geeksville.mesh.R +import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.GpsCoordinateFormat import com.geeksville.mesh.android.BuildUtils.debug import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.HyperlinkBlue +import com.geeksville.mesh.util.GPSFormat import java.net.URLEncoder @OptIn(ExperimentalFoundationApi::class) @Composable fun LinkedCoordinates( modifier: Modifier = Modifier, - position: Position?, + latitude: Double, + longitude: Double, format: Int, - nodeName: String? + nodeName: String, ) { - if (position?.isValid() == true) { - val uriHandler = LocalUriHandler.current - val style = SpanStyle( - color = HyperlinkBlue, - fontSize = MaterialTheme.typography.button.fontSize, - textDecoration = TextDecoration.Underline + val uriHandler = LocalUriHandler.current + val style = SpanStyle( + color = HyperlinkBlue, + fontSize = MaterialTheme.typography.button.fontSize, + textDecoration = TextDecoration.Underline + ) + val annotatedString = buildAnnotatedString { + pushStringAnnotation( + tag = "gps", + // URI scheme is defined at: + // https://developer.android.com/guide/components/intents-common#Maps + annotation = "geo:0,0?q=${latitude},${longitude}&z=17&label=${ + URLEncoder.encode(nodeName, "utf-8") + }" ) - val name = nodeName ?: stringResource(id = R.string.unknown_username) - val annotatedString = buildAnnotatedString { - pushStringAnnotation( - tag = "gps", - // URI scheme is defined at: - // https://developer.android.com/guide/components/intents-common#Maps - annotation = "geo:0,0?q=${position.latitude},${position.longitude}&z=17&label=${ - URLEncoder.encode(name, "utf-8") - }" - ) - withStyle(style = style) { - append(position.gpsString(format)) + withStyle(style = style) { + val gpsString = when (format) { + GpsCoordinateFormat.DEC_VALUE -> GPSFormat.toDEC(latitude, longitude) + GpsCoordinateFormat.DMS_VALUE -> GPSFormat.toDMS(latitude, longitude) + GpsCoordinateFormat.UTM_VALUE -> GPSFormat.toUTM(latitude, longitude) + GpsCoordinateFormat.MGRS_VALUE -> GPSFormat.toMGRS(latitude, longitude) + else -> GPSFormat.toDEC(latitude, longitude) } - pop() + append(gpsString) } - val clipboardManager: ClipboardManager = LocalClipboardManager.current - Text( - modifier = modifier.combinedClickable( - onClick = { - annotatedString.getStringAnnotations( - tag = "gps", - start = 0, - end = annotatedString.length - ).firstOrNull()?.let { - try { - uriHandler.openUri(it.item) - } catch (ex: ActivityNotFoundException) { - debug("No application found: $ex") - } + pop() + } + val clipboardManager: ClipboardManager = LocalClipboardManager.current + Text( + modifier = modifier.combinedClickable( + onClick = { + annotatedString.getStringAnnotations( + tag = "gps", + start = 0, + end = annotatedString.length + ).firstOrNull()?.let { + try { + uriHandler.openUri(it.item) + } catch (ex: ActivityNotFoundException) { + debug("No application found: $ex") } - }, - onLongClick = { - clipboardManager.setText(annotatedString) - debug("Copied to clipboard") } - ), - text = annotatedString - ) - } else { - // Placeholder for ConstraintLayoutReference; renders no visible content - Box(modifier = modifier) - } -} - -@Composable -@Preview -fun LinkedCoordinatesSimplePreview() { - AppTheme { - LinkedCoordinates( - position = Position(37.7749, -122.4194, 0), - format = 1, - nodeName = "Test Node Name" - ) - } + }, + onLongClick = { + clipboardManager.setText(annotatedString) + debug("Copied to clipboard") + } + ), + text = annotatedString + ) } @PreviewLightDark @@ -105,7 +92,8 @@ fun LinkedCoordinatesPreview( ) { AppTheme { LinkedCoordinates( - position = Position(37.7749, -122.4194, 0), + latitude = 37.7749, + longitude = -122.4194, format = format, nodeName = "Test Node Name" ) @@ -115,4 +103,4 @@ fun LinkedCoordinatesPreview( class GPSFormatPreviewParameterProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf(0, 1, 2) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt index a2a384b5b..e429bf91d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeItem.kt @@ -1,8 +1,5 @@ @file:Suppress( - "FunctionNaming", "LongMethod", - "LongParameterList", - "DestructuringDeclarationWithTooManyEntries", "MagicNumber", "CyclomaticComplexMethod", ) @@ -53,20 +50,22 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.geeksville.mesh.ConfigProtos import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig +import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R +import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.ui.compose.ElevationInfo import com.geeksville.mesh.ui.compose.SatelliteCountInfo -import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider +import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.util.metersIn +import com.geeksville.mesh.util.toDistanceString @OptIn(ExperimentalMaterialApi::class) @Composable fun NodeItem( - thisNodeInfo: NodeInfo?, - thatNodeInfo: NodeInfo, + thisNode: NodeEntity?, + thatNode: NodeEntity, gpsFormat: Int, distanceUnits: Int, tempInFahrenheit: Boolean, @@ -75,28 +74,29 @@ fun NodeItem( blinking: Boolean = false, expanded: Boolean = false, currentTimeMillis: Long, - hasPublicKey: Boolean = false, ) { - val isUnknownUser = thatNodeInfo.user?.hwModel == MeshProtos.HardwareModel.UNSET + val hasPublicKey = !thatNode.user.publicKey.isEmpty + val isUnknownUser = thatNode.user.hwModel == MeshProtos.HardwareModel.UNSET val unknownShortName = stringResource(id = R.string.unknown_node_short_name) - val longName = thatNodeInfo.user?.longName ?: stringResource(id = R.string.unknown_username) + val longName = thatNode.user.longName.ifEmpty { stringResource(id = R.string.unknown_username) } - val nodeName = if (hasPublicKey) "🔒 $longName" else longName - val isThisNode = thisNodeInfo?.num == thatNodeInfo.num - val distance = thisNodeInfo?.distanceStr(thatNodeInfo, distanceUnits) - val (textColor, nodeColor) = thatNodeInfo.colors + val isThisNode = thisNode?.num == thatNode.num + val distance = thisNode?.distance(thatNode)?.let { + val system = DisplayConfig.DisplayUnits.forNumber(distanceUnits) + if (it == 0) null else it.toDistanceString(system) + } + val (textColor, nodeColor) = thatNode.colors - val position = thatNodeInfo.position - val hwInfoString = thatNodeInfo.user?.hwModelString ?: MeshProtos.HardwareModel.UNSET.name - val roleName = - if (isUnknownUser) { - DeviceConfig.Role.UNRECOGNIZED.name - } else { - thatNodeInfo.user?.role?.let { role -> - DeviceConfig.Role.forNumber(role)?.name - } ?: DeviceConfig.Role.UNRECOGNIZED.name - } - val nodeId = thatNodeInfo.user?.id ?: "???" + val hwInfoString = thatNode.user.hwModel.let { hwModel -> + if (hwModel == MeshProtos.HardwareModel.UNSET) MeshProtos.HardwareModel.UNSET.name + else hwModel.name.replace('_', '-').replace('p', '.').lowercase() + } + val roleName = if (isUnknownUser) { + DeviceConfig.Role.UNRECOGNIZED.name + } else { + thatNode.user.role.name + } + val nodeId = thatNode.user.id.ifEmpty { "???" } val highlight = Color(0x33FFFFFF) val bgColor by animateColorAsState( @@ -153,7 +153,7 @@ fun NodeItem( content = { Text( modifier = Modifier.fillMaxWidth(), - text = thatNodeInfo.user?.shortName ?: unknownShortName, + text = thatNode.user.shortName.ifEmpty { unknownShortName }, fontWeight = FontWeight.Normal, fontSize = MaterialTheme.typography.button.fontSize, textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, @@ -163,14 +163,14 @@ fun NodeItem( ) Text( modifier = Modifier.weight(1f), - text = nodeName, + text = if (hasPublicKey) "🔒 $longName" else longName, style = style, textDecoration = TextDecoration.LineThrough.takeIf { isIgnored }, softWrap = true, ) LastHeardInfo( - lastHeard = thatNodeInfo.lastHeard, + lastHeard = thatNode.lastHeard, currentTimeMillis = currentTimeMillis ) } @@ -188,8 +188,8 @@ fun NodeItem( Spacer(modifier = Modifier.width(16.dp)) } BatteryInfo( - batteryLevel = thatNodeInfo.batteryLevel, - voltage = thatNodeInfo.voltage + batteryLevel = thatNode.batteryLevel, + voltage = thatNode.voltage ) } Spacer(modifier = Modifier.height(4.dp)) @@ -198,11 +198,11 @@ fun NodeItem( horizontalArrangement = Arrangement.SpaceBetween ) { signalInfo( - nodeInfo = thatNodeInfo, + node = thatNode, isThisNode = isThisNode ) - if (position?.isValid() == true) { - val satCount = position.satellitesInView + thatNode.validPosition?.let { position -> + val satCount = position.satsInView if (satCount > 0) { SatelliteCountInfo( satCount = satCount @@ -215,9 +215,10 @@ fun NodeItem( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - thatNodeInfo.environmentMetrics?.getDisplayString(tempInFahrenheit)?.let { envMetrics -> + val telemetryString = thatNode.getTelemetryString(tempInFahrenheit) + if (telemetryString.isNotEmpty()) { Text( - text = envMetrics, + text = telemetryString, color = MaterialTheme.colors.onSurface, fontSize = MaterialTheme.typography.button.fontSize ) @@ -233,16 +234,19 @@ fun NodeItem( .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - DisableSelection { - LinkedCoordinates( - position = position, - format = gpsFormat, - nodeName = nodeName - ) + thatNode.validPosition?.let { + DisableSelection { + LinkedCoordinates( + latitude = thatNode.latitude, + longitude = thatNode.longitude, + format = gpsFormat, + nodeName = longName + ) + } } val system = ConfigProtos.Config.DisplayConfig.DisplayUnits.forNumber(distanceUnits) - if (position?.isValid() == true) { + thatNode.validPosition?.let { position -> val altitude = position.altitude.metersIn(system) val elevationSuffix = stringResource(id = R.string.elevation_suffix) ElevationInfo( @@ -266,13 +270,15 @@ fun NodeItem( modifier = Modifier.weight(1f), text = roleName, textAlign = TextAlign.Center, - fontSize = MaterialTheme.typography.button.fontSize + fontSize = MaterialTheme.typography.button.fontSize, + style = style, ) Text( modifier = Modifier.weight(1f), text = nodeId, textAlign = TextAlign.End, - fontSize = MaterialTheme.typography.button.fontSize + fontSize = MaterialTheme.typography.button.fontSize, + style = style, ) } } @@ -286,11 +292,11 @@ fun NodeItem( @Preview(showBackground = false) fun NodeInfoSimplePreview() { AppTheme { - val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first() - val thatNodeInfo = NodeInfoPreviewParameterProvider().values.last() + val thisNode = NodeEntityPreviewParameterProvider().values.first() + val thatNode = NodeEntityPreviewParameterProvider().values.last() NodeItem( - thisNodeInfo = thisNodeInfo, - thatNodeInfo = thatNodeInfo, + thisNode = thisNode, + thatNode = thatNode, 1, 0, true, @@ -305,19 +311,19 @@ fun NodeInfoSimplePreview() { uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, ) fun NodeInfoPreview( - @PreviewParameter(NodeInfoPreviewParameterProvider::class) - thatNodeInfo: NodeInfo + @PreviewParameter(NodeEntityPreviewParameterProvider::class) + thatNode: NodeEntity ) { AppTheme { - val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first() + val thisNode = NodeEntityPreviewParameterProvider().values.first() Column { Text( text = "Details Collapsed", color = MaterialTheme.colors.onBackground ) NodeItem( - thisNodeInfo = thisNodeInfo, - thatNodeInfo = thatNodeInfo, + thisNode = thisNode, + thatNode = thatNode, gpsFormat = 0, distanceUnits = 1, tempInFahrenheit = true, @@ -329,8 +335,8 @@ fun NodeInfoPreview( color = MaterialTheme.colors.onBackground ) NodeItem( - thisNodeInfo = thisNodeInfo, - thatNodeInfo = thatNodeInfo, + thisNode = thisNode, + thatNode = thatNode, gpsFormat = 0, distanceUnits = 1, tempInFahrenheit = true, diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 5911c91d8..eb1799adb 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -26,7 +26,6 @@ import com.geeksville.mesh.databinding.SettingsFragmentBinding import com.geeksville.mesh.model.BTScanModel import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.UIViewModel -import com.geeksville.mesh.model.getInitials import com.geeksville.mesh.repository.location.LocationRepository import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.util.exceptionToSnackbar @@ -218,10 +217,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { binding.usernameEditText.onEditorAction(EditorInfo.IME_ACTION_DONE) { debug("received IME_ACTION_DONE") val n = binding.usernameEditText.text.toString().trim() - model.ourNodeInfo.value?.user?.let { - val user = it.copy(longName = n, shortName = getInitials(n)) - if (n.isNotEmpty()) model.setOwner(user) - } + if (n.isNotEmpty()) model.setOwner(n) requireActivity().hideKeyboard() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt index 046128cfa..82e109811 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt @@ -8,31 +8,31 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter -import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R -import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider +import com.geeksville.mesh.database.entity.NodeEntity +import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider import com.geeksville.mesh.ui.theme.AppTheme @Composable fun signalInfo( modifier: Modifier = Modifier, - nodeInfo: NodeInfo, + node: NodeEntity, isThisNode: Boolean ): Boolean { val text = if (isThisNode) { stringResource(R.string.channel_air_util).format( - nodeInfo.deviceMetrics?.channelUtilization, - nodeInfo.deviceMetrics?.airUtilTx + node.deviceMetrics.channelUtilization, + node.deviceMetrics.airUtilTx ) } else { buildList { - if (nodeInfo.channel > 0) add("ch:${nodeInfo.channel}") - if (nodeInfo.hopsAway == 0) { - if (nodeInfo.snr < 100F && nodeInfo.rssi < 0) { - add("RSSI: %d SNR: %.1f".format(nodeInfo.rssi, nodeInfo.snr)) + if (node.channel > 0) add("ch:${node.channel}") + if (node.hopsAway == 0) { + if (node.snr < 100F && node.rssi < 0) { + add("RSSI: %d SNR: %.1f".format(node.rssi, node.snr)) } } else { - add("%s: %d".format(stringResource(R.string.hops_away), nodeInfo.hopsAway)) + add("%s: %d".format(stringResource(R.string.hops_away), node.hopsAway)) } }.joinToString(" ") } @@ -54,15 +54,12 @@ fun signalInfo( fun SignalInfoSimplePreview() { AppTheme { signalInfo( - nodeInfo = NodeInfo( + node = NodeEntity( num = 1, - position = null, lastHeard = 0, channel = 0, snr = 12.5F, rssi = -42, - deviceMetrics = null, - user = null, hopsAway = 0 ), isThisNode = false @@ -73,12 +70,12 @@ fun SignalInfoSimplePreview() { @PreviewLightDark @Composable fun SignalInfoPreview( - @PreviewParameter(NodeInfoPreviewParameterProvider::class) - nodeInfo: NodeInfo + @PreviewParameter(NodeEntityPreviewParameterProvider::class) + node: NodeEntity ) { AppTheme { signalInfo( - nodeInfo = nodeInfo, + node = node, isThisNode = false ) } @@ -87,12 +84,12 @@ fun SignalInfoPreview( @Composable @PreviewLightDark fun SignalInfoSelfPreview( - @PreviewParameter(NodeInfoPreviewParameterProvider::class) - nodeInfo: NodeInfo + @PreviewParameter(NodeEntityPreviewParameterProvider::class) + node: NodeEntity ) { AppTheme { signalInfo( - nodeInfo = nodeInfo, + node = node, isThisNode = true ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index 4e067d708..c7d794bde 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -133,7 +133,7 @@ fun NodesScreen( val state by model.nodesUiState.collectAsStateWithLifecycle() val nodes by model.nodeList.collectAsStateWithLifecycle() - val ourNodeInfo by model.ourNodeInfo.collectAsStateWithLifecycle() + val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle() val listState = rememberLazyListState() val focusedNode by model.focusedNode.collectAsStateWithLifecycle() @@ -172,8 +172,8 @@ fun NodesScreen( items(nodes, key = { it.num }) { node -> val nodeInfo = node.toNodeInfo() NodeItem( - thisNodeInfo = ourNodeInfo, - thatNodeInfo = nodeInfo, + thisNode = ourNode, + thatNode = node, gpsFormat = state.gpsFormat, distanceUnits = state.distanceUnits, tempInFahrenheit = state.tempInFahrenheit, @@ -185,7 +185,6 @@ fun NodesScreen( blinking = nodeInfo == focusedNode, expanded = state.showDetails, currentTimeMillis = currentTimeMillis, - hasPublicKey = !node.user.publicKey.isEmpty ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt index f4dbeb0c9..5bfe43658 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/MapFragment.kt @@ -318,7 +318,7 @@ fun MapView( fun MapView.onNodesChanged(nodes: Collection): List { val nodesWithPosition = nodes.filter { it.validPosition != null } - val ourNode = model.ourNodeInfo.value + val ourNode = model.ourNodeInfo.value?.toNodeInfo() val gpsFormat = model.config.display.gpsFormat.number val displayUnits = model.config.display.units.number return nodesWithPosition.map { node -> diff --git a/app/src/main/java/com/geeksville/mesh/ui/preview/NodeEntityPreviewParameterProvider.kt b/app/src/main/java/com/geeksville/mesh/ui/preview/NodeEntityPreviewParameterProvider.kt new file mode 100644 index 000000000..155ddc3ce --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/preview/NodeEntityPreviewParameterProvider.kt @@ -0,0 +1,146 @@ +package com.geeksville.mesh.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.geeksville.mesh.DeviceMetrics.Companion.currentTime +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.database.entity.NodeEntity +import com.geeksville.mesh.deviceMetrics +import com.geeksville.mesh.environmentMetrics +import com.geeksville.mesh.paxcount +import com.geeksville.mesh.position +import com.geeksville.mesh.telemetry +import com.geeksville.mesh.user +import kotlin.random.Random + +class NodeEntityPreviewParameterProvider : PreviewParameterProvider { + + val mickeyMouse = NodeEntity( + num = 1955, + user = user { + id = "mickeyMouseId" + longName = "Mickey Mouse" + shortName = "MM" + hwModel = MeshProtos.HardwareModel.TBEAM + }, + longName = "Mickey Mouse", + shortName = "MM", + position = position { + latitudeI = 338125110 + longitudeI = -1179189760 + altitude = 138 + satsInView = 4 + }, + latitude = 33.812511, + longitude = -117.918976, + lastHeard = currentTime(), + channel = 0, + snr = 12.5F, + rssi = -42, + deviceTelemetry = telemetry { + deviceMetrics = deviceMetrics { + channelUtilization = 2.4F + airUtilTx = 3.5F + batteryLevel = 85 + voltage = 3.7F + uptimeSeconds = 3600 + } + }, + hopsAway = 0 + ) + + private val minnieMouse = mickeyMouse.copy( + num = Random.nextInt(), + user = user { + longName = "Minnie Mouse" + shortName = "MiMo" + id = "minnieMouseId" + hwModel = MeshProtos.HardwareModel.HELTEC_V3 + }, + longName = "Minnie Mouse", + shortName = "MiMo", + snr = 12.5F, + rssi = -42, + position = position {}, + latitude = 0.0, + longitude = 0.0, + hopsAway = 1 + ) + + private val donaldDuck = NodeEntity( + num = Random.nextInt(), + position = position { + latitudeI = 338052347 + longitudeI = -1179208460 + altitude = 121 + satsInView = 66 + }, + latitude = 33.8052347, + longitude = -117.9208460, + lastHeard = currentTime() - 300, + channel = 0, + snr = 12.5F, + rssi = -42, + deviceTelemetry = telemetry { + deviceMetrics = deviceMetrics { + channelUtilization = 2.4F + airUtilTx = 3.5F + batteryLevel = 85 + voltage = 3.7F + uptimeSeconds = 3600 + } + }, + user = user { + id = "donaldDuckId" + longName = "Donald Duck, the Grand Duck of the Ducks" + shortName = "DoDu" + hwModel = MeshProtos.HardwareModel.HELTEC_V3 + }, + longName = "Donald Duck, the Grand Duck of the Ducks", + shortName = "DoDu", + environmentTelemetry = telemetry { + environmentMetrics = environmentMetrics { + temperature = 28.0F + relativeHumidity = 50.0F + barometricPressure = 1013.25F + gasResistance = 0.0F + voltage = 3.7F + current = 0.0F + iaq = 100 + } + }, + paxcounter = paxcount { + wifi = 30 + ble = 39 + uptime = 420 + }, + hopsAway = 2 + ) + + private val unknown = donaldDuck.copy( + user = user { + id = "myId" + longName = "Meshtastic myId" + shortName = "myId" + hwModel = MeshProtos.HardwareModel.UNSET + }, + longName = "Meshtastic myId", + shortName = null, + environmentTelemetry = telemetry { + environmentMetrics = environmentMetrics {} + }, + paxcounter = paxcount {}, + ) + + private val almostNothing = NodeEntity( + num = Random.nextInt(), + ) + + override val values: Sequence + get() = sequenceOf( + mickeyMouse, // "this" node + unknown, + almostNothing, + minnieMouse, + donaldDuck + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/util/LocationUtils.kt b/app/src/main/java/com/geeksville/mesh/util/LocationUtils.kt index 1d8cb8fc5..8fef9c518 100644 --- a/app/src/main/java/com/geeksville/mesh/util/LocationUtils.kt +++ b/app/src/main/java/com/geeksville/mesh/util/LocationUtils.kt @@ -58,6 +58,34 @@ object GPSFormat { MGRS.northing ) } + + fun toDEC(latitude: Double, longitude: Double): String { + return "%.5f %.5f".format(latitude, longitude).replace(",", ".") + } + + fun toDMS(latitude: Double, longitude: Double): String { + val lat = degreesToDMS(latitude, true) + val lon = degreesToDMS(longitude, false) + fun string(a: Array) = "%s°%s'%.5s\"%s".format(a[0], a[1], a[2], a[3]) + return string(lat) + " " + string(lon) + } + + fun toUTM(latitude: Double, longitude: Double): String { + val UTM = UTM.from(Point.point(longitude, latitude)) + return "%s%s %.6s %.7s".format(UTM.zone, UTM.toMGRS().band, UTM.easting, UTM.northing) + } + + fun toMGRS(latitude: Double, longitude: Double): String { + val MGRS = MGRS.from(Point.point(longitude, latitude)) + return "%s%s %s%s %05d %05d".format( + MGRS.zone, + MGRS.band, + MGRS.column, + MGRS.row, + MGRS.easting, + MGRS.northing + ) + } } /**