mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(NodeItem): replace NodeInfo with NodeEntity
This commit is contained in:
parent
89a3171b58
commit
83dc389d6d
12 changed files with 373 additions and 160 deletions
|
|
@ -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<NodeEntity> { it.validPosition == null }.thenBy { it.distance(ourNode) }
|
||||
)
|
||||
assertEquals(sortedNodes, nodes)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<MyNodeInfo?> get() = _myNodeInfo
|
||||
|
||||
// our node info
|
||||
private val _ourNodeInfo = MutableStateFlow<NodeInfo?>(null)
|
||||
val ourNodeInfo: StateFlow<NodeInfo?> get() = _ourNodeInfo
|
||||
private val _ourNodeInfo = MutableStateFlow<NodeEntity?>(null)
|
||||
val ourNodeInfo: StateFlow<NodeEntity?> get() = _ourNodeInfo
|
||||
|
||||
// The unique userId of our node
|
||||
private val _myId = MutableStateFlow<String?>(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)
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ class UIViewModel @Inject constructor(
|
|||
|
||||
// hardware info about our local device (can be null)
|
||||
val myNodeInfo: StateFlow<MyNodeInfo?> get() = nodeDB.myNodeInfo
|
||||
val ourNodeInfo: StateFlow<NodeInfo?> get() = nodeDB.ourNodeInfo
|
||||
val ourNodeInfo: StateFlow<NodeEntity?> 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}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Int> {
|
||||
override val values: Sequence<Int>
|
||||
get() = sequenceOf(0, 1, 2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ fun MapView(
|
|||
|
||||
fun MapView.onNodesChanged(nodes: Collection<NodeInfo>): List<MarkerWithLabel> {
|
||||
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 ->
|
||||
|
|
|
|||
|
|
@ -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<NodeEntity> {
|
||||
|
||||
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<NodeEntity>
|
||||
get() = sequenceOf(
|
||||
mickeyMouse, // "this" node
|
||||
unknown,
|
||||
almostNothing,
|
||||
minnieMouse,
|
||||
donaldDuck
|
||||
)
|
||||
}
|
||||
|
|
@ -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<String>) = "%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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue