Move Node info completely to Compose (#886)

* Move battery info to compose - always show voltage level and icons to match battery percentage
Use tool text in preview, rather than actually set text value
Simplify node info layout to avoid defining margins on everything

* Move node position to Compose

* Update hyperlink color to match previous value

* Use compose preview in layout editor

* Use compose preview in layout editor

* Add simple preview for use in layout

* Move last heard node info to Compose
Clean up layout of node info

* Move signal info to Compose and simplify bind

* Prevent long coordinates from colliding with signal info

* Move the rest of the node info card to compose
Breaks the blinking feature when navigating from chat
Wrap position to new line if overflow

* Adjust layout and text sizing to closer match original

* Use constraint layout for tighter display on busy nodes

* Construct environment metrics so that there aren't trailing spaces if current is zero

* Swap viewholder root for compose view rather than inflating layout
Fix padding lost when changing out view holder root
Intelligently update the list with only nodes that changed

* Remove unused method, and adjust replacement method to match the same decimal precisions as before

* Use previous string for denoting unknown node names

* Mark unknown short name as non-translatable
This commit is contained in:
Davis 2024-03-07 01:39:02 -07:00 committed by GitHub
parent 4ba67beb53
commit 9e54787a7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 465 additions and 239 deletions

View file

@ -179,6 +179,32 @@ data class EnvironmentMetrics(
override fun toString(): String {
return "EnvironmentMetrics(time=${time}, temperature=${temperature}, humidity=${relativeHumidity}, pressure=${barometricPressure}), resistance=${gasResistance}, voltage=${voltage}, current=${current}"
}
fun getDisplayString(inFahrenheit: Boolean = false): String {
val temp = if (temperature != 0f) {
if (inFahrenheit) {
val fahrenheit = temperature * 1.8F + 32
String.format("%.1f°F", fahrenheit)
} else {
String.format("%.1f°C", temperature)
}
} else null
val humidity = if (relativeHumidity != 0f) String.format("%.0f%%", relativeHumidity) else null
val pressure = if (barometricPressure != 0f) String.format("%.1fhPa", barometricPressure) else null
val gas = if (gasResistance != 0f) String.format("%.0fMΩ", gasResistance) else null
val voltage = if (voltage != 0f) String.format("%.2fV", voltage) else null
val current = if (current != 0f) String.format("%.1fmA", current) else null
return listOfNotNull(
temp,
humidity,
pressure,
gas,
voltage,
current
).joinToString(" ")
}
}
@Parcelize
@ -213,22 +239,6 @@ data class NodeInfo(
val voltage get() = deviceMetrics?.voltage
val batteryStr get() = if (batteryLevel in 1..100) String.format("%d%%", batteryLevel) else ""
private fun Float.envFormat(unit: String, decimalPlaces: Int = 1): String =
if (this != 0f) String.format("%.${decimalPlaces}f$unit", this) else ""
fun envMetricStr(isFahrenheit: Boolean = false): String = buildString {
val env = environmentMetrics ?: return ""
if (env.temperature != 0f) append(
if (!isFahrenheit) env.temperature.envFormat("°C ")
else (env.temperature * 1.8f + 32).envFormat("°F ")
)
append(env.relativeHumidity.envFormat("%% ", 0))
append(env.barometricPressure.envFormat("hPa "))
append(env.gasResistance.envFormat("", 0))
append(env.voltage.envFormat("V ", 2))
append(env.current.envFormat("mA"))
}
/**
* true if the device was heard from recently
*/

View file

@ -19,7 +19,11 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun BatteryInfo(batteryLevel: Int?, voltage: Float?) {
fun BatteryInfo(
modifier: Modifier = Modifier,
batteryLevel: Int?,
voltage: Float?
) {
val infoString = "%d%% %.1fV".format(batteryLevel, voltage)
val (image, level) = when (batteryLevel) {
in 0 .. 4 -> R.drawable.ic_battery_alert to " $infoString"
@ -32,6 +36,7 @@ fun BatteryInfo(batteryLevel: Int?, voltage: Float?) {
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@ -56,7 +61,10 @@ fun BatteryInfoPreview(
batteryInfo: Pair<Int?, Float?>
) {
AppTheme {
BatteryInfo(batteryInfo.first, batteryInfo.second)
BatteryInfo(
batteryLevel = batteryInfo.first,
voltage = batteryInfo.second
)
}
}
@ -64,7 +72,10 @@ fun BatteryInfoPreview(
@Preview
fun BatteryInfoPreviewSimple() {
AppTheme {
BatteryInfo(85, 3.7F)
BatteryInfo(
batteryLevel = 85,
voltage = 3.7F
)
}
}

View file

@ -19,9 +19,11 @@ import com.geeksville.mesh.util.formatAgo
@Composable
fun LastHeardInfo(
modifier: Modifier = Modifier,
lastHeard: Int
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
@ -44,6 +46,6 @@ fun LastHeardInfo(
@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
fun LastHeardInfoPreview() {
AppTheme {
LastHeardInfo((System.currentTimeMillis() / 1000).toInt() - 8600)
LastHeardInfo(lastHeard = (System.currentTimeMillis() / 1000).toInt() - 8600)
}
}

View file

@ -3,6 +3,7 @@ package com.geeksville.mesh.ui
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
@ -21,11 +22,12 @@ import java.net.URLEncoder
@Composable
fun LinkedCoordinates(
modifier : Modifier = Modifier,
position: Position?,
format: Int,
nodeName: String?
) {
if (position != null) {
if (position?.isValid() == true) {
val uriHandler = LocalUriHandler.current
val style = SpanStyle(
color = HyperlinkBlue,
@ -46,8 +48,8 @@ fun LinkedCoordinates(
pop()
}
ClickableText(
modifier = modifier,
text = annotatedString,
maxLines = 1,
onClick = { offset ->
debug("Clicked on link")
annotatedString.getStringAnnotations(tag = "gps", start = offset, end = offset)

View file

@ -0,0 +1,248 @@
package com.geeksville.mesh.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card
import androidx.compose.material.Chip
import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider
import com.geeksville.mesh.ui.theme.AppTheme
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun NodeInfo(
thisNodeInfo: NodeInfo,
thatNodeInfo: NodeInfo,
gpsFormat: Int,
distanceUnits: Int,
tempInFahrenheit: Boolean,
isIgnored: Boolean = false,
onClicked: () -> Unit = {}
) {
val unknownShortName = stringResource(id = R.string.unknown_node_short_name)
val unknownLongName = stringResource(id = R.string.unknown_username)
val nodeName = thatNodeInfo.user?.longName ?: unknownLongName
val isThisNode = thisNodeInfo.num == thatNodeInfo.num
val distance = thisNodeInfo.distanceStr(thatNodeInfo, distanceUnits)
val (textColor, nodeColor) = thatNodeInfo.colors
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.defaultMinSize(minHeight = 80.dp)
) {
Surface {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
val (chip, dist, name, pos, batt, heard, sig, env) = createRefs()
val barrierBattHeard = createStartBarrier(batt, heard)
val sigBarrier = createBottomBarrier(pos, heard)
Box(
// removes the extra spacing above the chip
modifier = Modifier
.height(32.dp)
.constrainAs(chip) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
) {
Chip(
modifier = Modifier.width(72.dp),
onClick = onClicked,
colors = ChipDefaults.chipColors(
backgroundColor = Color(nodeColor),
contentColor = Color(textColor)
),
content = {
Text(
modifier = Modifier.fillMaxWidth(),
text = (thatNodeInfo.user?.shortName ?: unknownShortName).strikeIf(isIgnored),
fontWeight = FontWeight.Normal,
fontSize = MaterialTheme.typography.button.fontSize,
textAlign = TextAlign.Center,
)
},
)
}
if (distance != null) {
Text(
modifier = Modifier.constrainAs(dist) {
top.linkTo(chip.bottom, 8.dp)
start.linkTo(chip.start)
end.linkTo(chip.end)
},
text = distance,
fontSize = MaterialTheme.typography.button.fontSize,
)
}
val style = if (nodeName == unknownLongName) {
LocalTextStyle.current.copy(fontStyle = FontStyle.Italic)
} else {
LocalTextStyle.current
}
Text(
modifier = Modifier.constrainAs(name) {
top.linkTo(parent.top)
linkTo(
start = chip.end,
end = barrierBattHeard,
bias = 0F,
startMargin = 8.dp,
endMargin = 8.dp,
)
width = Dimension.preferredWrapContent
},
text = nodeName.strikeIf(isIgnored),
style = style
)
LinkedCoordinates(
modifier = Modifier.constrainAs(pos) {
linkTo(
top = name.bottom,
bottom = sig.top,
bias = 0F,
topMargin = 4.dp,
bottomMargin = 4.dp
)
linkTo(
start = name.start,
end = barrierBattHeard,
bias = 0F,
endMargin = 8.dp
)
width = Dimension.preferredWrapContent
},
position = thatNodeInfo.position,
format = gpsFormat,
nodeName = nodeName
)
BatteryInfo(
modifier = Modifier.constrainAs(batt) {
top.linkTo(parent.top)
end.linkTo(parent.end)
},
batteryLevel = thatNodeInfo.batteryLevel,
voltage = thatNodeInfo.voltage
)
LastHeardInfo(
modifier = Modifier.constrainAs(heard) {
top.linkTo(batt.bottom, 4.dp)
end.linkTo(parent.end)
},
lastHeard = thatNodeInfo.lastHeard
)
SignalInfo(
modifier = Modifier.constrainAs(sig) {
top.linkTo(sigBarrier, 4.dp)
bottom.linkTo(env.top, 4.dp)
end.linkTo(parent.end)
},
nodeInfo = thatNodeInfo,
isThisNode = isThisNode
)
val envMetrics = thatNodeInfo.environmentMetrics
?.getDisplayString(tempInFahrenheit) ?: ""
if (envMetrics.isNotBlank()) {
Text(
modifier = Modifier.constrainAs(env) {
top.linkTo(sig.bottom, 4.dp)
end.linkTo(parent.end)
},
text = envMetrics,
color = MaterialTheme.colors.onSurface,
fontSize = MaterialTheme.typography.button.fontSize
)
}
}
}
}
}
private fun String.strike() = AnnotatedString(
this,
spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(textDecoration = TextDecoration.LineThrough),
start = 0,
end = this.length
)
)
)
private fun String.strikeIf(isIgnored: Boolean): AnnotatedString = if (isIgnored) strike() else AnnotatedString(this)
@Composable
@Preview(showBackground = false)
fun NodeInfoSimplePreview() {
AppTheme {
val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first()
val thatNodeInfo = NodeInfoPreviewParameterProvider().values.last()
NodeInfo(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
1,
0,
true
)
}
}
@Composable
@Preview(
showBackground = true,
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
)
fun NodeInfoPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
thatNodeInfo: NodeInfo
) {
AppTheme {
val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first()
NodeInfo(
thisNodeInfo,
thatNodeInfo,
0,
1,
true
)
}
}

View file

@ -3,6 +3,7 @@ package com.geeksville.mesh.ui
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.geeksville.mesh.NodeInfo
@ -11,6 +12,7 @@ import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun SignalInfo(
modifier: Modifier = Modifier,
nodeInfo: NodeInfo,
isThisNode: Boolean
) {
@ -30,6 +32,7 @@ fun SignalInfo(
}
if (text.isNotEmpty()) {
Text(
modifier = modifier,
text = text,
color = MaterialTheme.colors.onSurface,
fontSize = MaterialTheme.typography.button.fontSize
@ -41,16 +44,19 @@ fun SignalInfo(
@Preview(showBackground = true)
fun SignalInfoSimplePreview() {
AppTheme {
SignalInfo(NodeInfo(
num = 1,
position = null,
lastHeard = 0,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = null,
user = null
), false)
SignalInfo(
nodeInfo = NodeInfo(
num = 1,
position = null,
lastHeard = 0,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = null,
user = null
),
isThisNode = false
)
}
}
@ -62,7 +68,10 @@ fun SignalInfoPreview(
nodeInfo: NodeInfo
) {
AppTheme {
SignalInfo(nodeInfo, false)
SignalInfo(
nodeInfo = nodeInfo,
isThisNode = false
)
}
}
@ -74,6 +83,9 @@ fun SignalInfoSelfPreview(
nodeInfo: NodeInfo
) {
AppTheme {
SignalInfo(nodeInfo, true)
SignalInfo(
nodeInfo = nodeInfo,
isThisNode = true
)
}
}

View file

@ -12,6 +12,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import androidx.appcompat.widget.PopupMenu
import androidx.compose.ui.platform.ComposeView
import androidx.core.animation.doOnEnd
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asLiveData
@ -20,10 +21,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
import com.geeksville.mesh.databinding.NodelistFragmentBinding
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.theme.AppTheme
@ -48,21 +47,11 @@ class UsersFragment : ScreenFragment("Users"), Logging {
private var displayUnits = 0
private var displayFahrenheit = false
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
val chipNode = itemView.chipNode
val nodeNameView = itemView.nodeNameView
val distanceView = itemView.distanceView
val envMetrics = itemView.envMetrics
val background = itemView.nodeCard
val nodePosition = itemView.nodePosition
val batteryInfo = itemView.batteryInfo
val lastHeard = itemView.lastHeardInfo
val signalInfo = itemView.signalInfo
class ViewHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView) {
// TODO not working with compose changes
fun blink() {
val bg = background.backgroundTintList
val bg = composeView.backgroundTintList
ValueAnimator.ofArgb(
Color.parseColor("#00FFFFFF"),
Color.parseColor("#33FFFFFF")
@ -73,38 +62,33 @@ class UsersFragment : ScreenFragment("Users"), Logging {
repeatCount = 3
repeatMode = ValueAnimator.REVERSE
addUpdateListener {
background.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int)
composeView.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int)
}
start()
doOnEnd {
background.backgroundTintList = bg
composeView.backgroundTintList = bg
}
}
}
fun bind(
nodeInfo: NodeInfo,
isThisNode: Boolean,
thisNodeInfo: NodeInfo,
thatNodeInfo: NodeInfo,
gpsFormat: Int,
distanceUnits: Int,
tempInFahrenheit: Boolean,
onChipClicked: () -> Unit
) {
batteryInfo.setContent {
composeView.setContent {
AppTheme {
BatteryInfo(nodeInfo.batteryLevel, nodeInfo.voltage)
}
}
nodePosition.setContent {
AppTheme {
LinkedCoordinates(nodeInfo.validPosition, gpsFormat, nodeInfo.user?.longName)
}
}
this.lastHeard.setContent {
AppTheme {
LastHeardInfo(nodeInfo.lastHeard)
}
}
this.signalInfo.setContent {
AppTheme {
SignalInfo(nodeInfo, isThisNode)
NodeInfo(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
gpsFormat = gpsFormat,
distanceUnits = distanceUnits,
tempInFahrenheit = tempInFahrenheit,
onClicked = onChipClicked
)
}
}
}
@ -213,13 +197,7 @@ class UsersFragment : ScreenFragment("Users"), Logging {
* @see .onBindViewHolder
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
val contactView = AdapterNodeLayoutBinding.inflate(inflater, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
return ViewHolder(ComposeView(parent.context))
}
/**
@ -251,53 +229,40 @@ class UsersFragment : ScreenFragment("Users"), Logging {
* @param position The position of the item within the adapter's data set.
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val n = nodes[position]
val (textColor, nodeColor) = n.colors
val isIgnored: Boolean = ignoreIncomingList.contains(n.num)
val isThisNode = n.num == nodes[0].num
val thisNode = nodes[0]
val thatNode = nodes[position]
holder.bind(n, isThisNode, gpsFormat)
holder.nodeNameView.text = n.user?.longName
with(holder.chipNode) {
text = (n.user?.shortName ?: "UNK").strikeIf(isIgnored)
chipBackgroundColor = ColorStateList.valueOf(nodeColor)
setTextColor(textColor)
}
val distance = nodes[0].distanceStr(n, displayUnits)
if (distance != null) {
holder.distanceView.text = distance
holder.distanceView.visibility = View.VISIBLE
} else {
holder.distanceView.visibility = View.INVISIBLE
}
val envMetrics = n.envMetricStr(displayFahrenheit)
if (envMetrics.isNotEmpty()) {
holder.envMetrics.text = envMetrics
holder.envMetrics.visibility = View.VISIBLE
} else {
holder.envMetrics.visibility = View.GONE
}
holder.chipNode.setOnClickListener {
popup(it, position)
}
holder.itemView.setOnLongClickListener {
popup(it, position)
true
holder.bind(
thisNodeInfo = thisNode,
thatNodeInfo = thatNode,
gpsFormat = gpsFormat,
distanceUnits = displayUnits,
tempInFahrenheit = displayFahrenheit
) {
popup(holder.composeView, position)
}
}
/// Called when our node DB changes
// Called when our node DB changes
fun onNodesChanged(nodesIn: Array<NodeInfo>) {
if (nodesIn.size > 1)
if (nodesIn.size > 1) {
nodesIn.sortWith(compareByDescending { it.lastHeard }, 1)
}
val previousNodes = nodes
val indexChanged = nodesIn.mapIndexed { index, nodeInfo ->
previousNodes.getOrNull(index) != nodeInfo
}
if (indexChanged.isEmpty()) return
nodes = nodesIn
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
for (i in indexChanged.indices) {
if (indexChanged[i]) {
notifyItemChanged(i)
}
}
}
}
override fun onCreateView(

View file

@ -2,26 +2,104 @@ package com.geeksville.mesh.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.DeviceMetrics
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
import com.geeksville.mesh.EnvironmentMetrics
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
import kotlin.random.Random
class NodeInfoPreviewParameterProvider: PreviewParameterProvider<NodeInfo> {
val mickeyMouse = NodeInfo(
num = 1955,
position = Position(
latitude = 33.812511,
longitude = -117.918976,
altitude = 138,
),
lastHeard = currentTime(),
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = DeviceMetrics(
channelUtilization = 2.4F,
airUtilTx = 3.5F,
batteryLevel = 85,
voltage = 3.7F
),
user = MeshUser(
longName = "Micky Mouse",
shortName = "MM",
id = "mickeyMouseId",
hwModel = MeshProtos.HardwareModel.TBEAM
)
)
private val minnieMouse = mickeyMouse.copy(
num = Random.nextInt(),
user = MeshUser(
longName = "Minnie Mouse",
shortName = "MiMo",
id = "minnieMouseId",
hwModel = MeshProtos.HardwareModel.HELTEC_V3
),
snr = 12.5F,
rssi = -42,
position = null
)
private val donaldDuck = NodeInfo(
num = Random.nextInt(),
position = Position(
latitude = 33.80523471893125,
longitude = -117.92084605996297,
altitude = 121,
),
lastHeard = currentTime() - 300,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = DeviceMetrics(
channelUtilization = 2.4F,
airUtilTx = 3.5F,
batteryLevel = 85,
voltage = 3.7F
),
user = MeshUser(
longName = "Donald Duck, the Grand Duck of the Ducks",
shortName = "DoDu",
id = "donaldDuckId",
hwModel = MeshProtos.HardwareModel.HELTEC_V3,
),
environmentMetrics = EnvironmentMetrics(
temperature = 28.0F,
relativeHumidity = 50.0F,
barometricPressure = 1013.25F,
gasResistance = 0.0F,
voltage = 3.7F,
current = 0.0F
)
)
private val unknown = donaldDuck.copy(
user = null,
environmentMetrics = null
)
private val almostNothing = NodeInfo(
num = Random.nextInt(),
)
override val values: Sequence<NodeInfo>
get() = sequenceOf(
NodeInfo(
num = 1,
position = null,
lastHeard = 0,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = DeviceMetrics(
channelUtilization = 2.4F,
airUtilTx = 3.5F,
batteryLevel = 85,
voltage = 3.7F
),
user = null
)
mickeyMouse, // "this" node
unknown,
almostNothing,
minnieMouse,
donaldDuck
)
}