Meshtastic-Android/app/src/main/java/com/geeksville/mesh/model/Node.kt
2025-09-24 15:43:46 +00:00

174 lines
6.9 KiB
Kotlin

/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.model
import android.graphics.Color
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.TelemetryProtos.DeviceMetrics
import com.geeksville.mesh.TelemetryProtos.EnvironmentMetrics
import com.geeksville.mesh.TelemetryProtos.PowerMetrics
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.util.toDistanceString
import com.google.protobuf.ByteString
import com.google.protobuf.kotlin.isNotEmpty
import org.meshtastic.core.model.util.GPSFormat
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.model.util.latLongToMeter
@Suppress("MagicNumber")
data class Node(
val num: Int,
val metadata: MeshProtos.DeviceMetadata? = null,
val user: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
val position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(),
val snr: Float = Float.MAX_VALUE,
val rssi: Int = Int.MAX_VALUE,
val lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
val deviceMetrics: DeviceMetrics = DeviceMetrics.getDefaultInstance(),
val channel: Int = 0,
val viaMqtt: Boolean = false,
val hopsAway: Int = -1,
val isFavorite: Boolean = false,
val isIgnored: Boolean = false,
val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics.getDefaultInstance(),
val powerMetrics: PowerMetrics = PowerMetrics.getDefaultInstance(),
val paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
val publicKey: ByteString? = null,
val notes: String = "",
) {
val colors: Pair<Int, Int>
get() { // returns foreground and background @ColorInt for each 'num'
val r = (num and 0xFF0000) shr 16
val g = (num and 0x00FF00) shr 8
val b = num and 0x0000FF
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b)
}
val isUnknownUser
get() = user.hwModel == MeshProtos.HardwareModel.UNSET
val hasPKC
get() = (publicKey ?: user.publicKey).isNotEmpty()
val mismatchKey
get() = (publicKey ?: user.publicKey) == NodeEntity.ERROR_BYTE_STRING
val hasEnvironmentMetrics: Boolean
get() = environmentMetrics != EnvironmentMetrics.getDefaultInstance()
val hasPowerMetrics: Boolean
get() = powerMetrics != PowerMetrics.getDefaultInstance()
val batteryLevel
get() = deviceMetrics.batteryLevel
val voltage
get() = deviceMetrics.voltage
val batteryStr
get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
val latitude
get() = position.latitudeI * 1e-7
val longitude
get() = position.longitudeI * 1e-7
private fun hasValidPosition(): Boolean = 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: Node): Int? = when {
validPosition == null || o.validPosition == null -> null
else -> latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt()
}
// @return formatted distance string to another node, using the given display units
fun distanceStr(o: Node, displayUnits: DisplayConfig.DisplayUnits): String? =
distance(o)?.toDistanceString(displayUnits)
// @return bearing to the other position in degrees
fun bearing(o: Node?): Int? = when {
validPosition == null || o?.validPosition == null -> null
else -> org.meshtastic.core.model.util.bearing(latitude, longitude, o.latitude, o.longitude).toInt()
}
fun gpsString(): String = GPSFormat.toDec(latitude, longitude)
private fun EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String {
val temp =
if (temperature != 0f) {
if (isFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(temperature))
} else {
"%.1f°C".format(temperature)
}
} else {
null
}
val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null
val soilTemperatureStr =
if (soilTemperature != 0f) {
if (isFahrenheit) {
"%.1f°F".format(celsiusToFahrenheit(soilTemperature))
} else {
"%.1f°C".format(soilTemperature)
}
} else {
null
}
val soilMoistureRange = 0..100
val soilMoisture =
if (soilMoisture in soilMoistureRange && soilTemperature != 0f) {
"%d%%".format(soilMoisture)
} 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, soilTemperatureStr, soilMoisture, 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 =
listOfNotNull(paxcounter.getDisplayString(), environmentMetrics.getDisplayString(isFahrenheit))
.joinToString(" ")
}
fun ConfigProtos.Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in
listOf(
ConfigProtos.Config.DeviceConfig.Role.REPEATER,
ConfigProtos.Config.DeviceConfig.Role.ROUTER,
ConfigProtos.Config.DeviceConfig.Role.ROUTER_LATE,
ConfigProtos.Config.DeviceConfig.Role.SENSOR,
ConfigProtos.Config.DeviceConfig.Role.TRACKER,
ConfigProtos.Config.DeviceConfig.Role.TAK_TRACKER,
)