refactor: introduce NodeEntity protobuf-based database entity (#1250)

This commit is contained in:
Andre K 2024-09-16 17:57:30 -03:00 committed by GitHub
parent 2433cbc00a
commit 396195a1b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1029 additions and 151 deletions

View file

@ -2,11 +2,14 @@ package com.geeksville.mesh.database
import androidx.room.TypeConverter
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.google.protobuf.TextFormat
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.android.Logging
import com.google.protobuf.InvalidProtocolBufferException
import kotlinx.serialization.json.Json
class Converters {
class Converters : Logging {
@TypeConverter
fun dataFromString(value: String): DataPacket {
val json = Json { isLenient = true }
@ -20,14 +23,62 @@ class Converters {
}
@TypeConverter
fun protoFromString(value: String): MeshPacket {
val builder = MeshPacket.newBuilder()
TextFormat.getParser().merge(value, builder)
return builder.build()
fun bytesToUser(bytes: ByteArray): MeshProtos.User {
return try {
MeshProtos.User.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
errormsg("bytesToUser TypeConverter error:", ex)
MeshProtos.User.getDefaultInstance()
}
}
@TypeConverter
fun protoToString(value: MeshPacket): String {
return value.toString()
fun userToBytes(value: MeshProtos.User): ByteArray? {
return value.toByteArray()
}
@TypeConverter
fun bytesToPosition(bytes: ByteArray): MeshProtos.Position {
return try {
MeshProtos.Position.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
errormsg("bytesToPosition TypeConverter error:", ex)
MeshProtos.Position.getDefaultInstance()
}
}
@TypeConverter
fun positionToBytes(value: MeshProtos.Position): ByteArray? {
return value.toByteArray()
}
@TypeConverter
fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry {
return try {
TelemetryProtos.Telemetry.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
errormsg("bytesToTelemetry TypeConverter error:", ex)
TelemetryProtos.Telemetry.getDefaultInstance()
}
}
@TypeConverter
fun telemetryToBytes(value: TelemetryProtos.Telemetry): ByteArray? {
return value.toByteArray()
}
@TypeConverter
fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount {
return try {
PaxcountProtos.Paxcount.parseFrom(bytes)
} catch (ex: InvalidProtocolBufferException) {
errormsg("bytesToPaxcounter TypeConverter error:", ex)
PaxcountProtos.Paxcount.getDefaultInstance()
}
}
@TypeConverter
fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? {
return value.toByteArray()
}
}

View file

@ -14,6 +14,7 @@ import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
@ -21,6 +22,7 @@ import com.geeksville.mesh.database.entity.QuickChatAction
entities = [
MyNodeInfo::class,
NodeInfo::class,
NodeEntity::class,
Packet::class,
ContactSettings::class,
MeshLog::class,
@ -33,8 +35,9 @@ import com.geeksville.mesh.database.entity.QuickChatAction
AutoMigration (from = 6, to = 7),
AutoMigration (from = 7, to = 8),
AutoMigration (from = 8, to = 9),
AutoMigration (from = 9, to = 10),
],
version = 9,
version = 10,
exportSchema = true,
)
@TypeConverters(Converters::class)

View file

@ -6,9 +6,8 @@ import androidx.room.MapColumn
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.database.entity.NodeEntity
import kotlinx.coroutines.flow.Flow
@Dao
@ -23,62 +22,70 @@ interface NodeInfoDao {
@Query("DELETE FROM MyNodeInfo")
fun clearMyNodeInfo()
@Query("SELECT * FROM NodeInfo ORDER BY CASE WHEN num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1) THEN 0 ELSE 1 END, lastHeard DESC")
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeInfo>>
@Query(
"""
SELECT * FROM nodes
ORDER BY CASE
WHEN num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1) THEN 0
ELSE 1
END,
last_heard DESC
"""
)
fun nodeDBbyNum(): Flow<Map<@MapColumn(columnName = "num") Int, NodeEntity>>
@Query(
"""
WITH OurNode AS (
SELECT position_latitude, position_longitude
FROM NodeInfo
SELECT latitude, longitude
FROM nodes
WHERE num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1)
)
SELECT * FROM NodeInfo
WHERE (:includeUnknown = 1 OR user_hwModel != :unknownHwModel)
SELECT * FROM nodes
WHERE (:includeUnknown = 1 OR short_name IS NOT NULL)
AND (:filter = ''
OR (user_longName LIKE '%' || :filter || '%'
OR user_shortName LIKE '%' || :filter || '%'))
OR (long_name LIKE '%' || :filter || '%'
OR short_name LIKE '%' || :filter || '%'))
ORDER BY CASE
WHEN num = (SELECT myNodeNum FROM MyNodeInfo LIMIT 1) THEN 0
ELSE 1
END,
CASE
WHEN :sort = 'last_heard' THEN lastHeard * -1
WHEN :sort = 'alpha' THEN UPPER(user_longName)
WHEN :sort = 'last_heard' THEN last_heard * -1
WHEN :sort = 'alpha' THEN UPPER(long_name)
WHEN :sort = 'distance' THEN
CASE
WHEN position_latitude IS NULL OR position_longitude IS NULL OR
(position_latitude = 0 AND position_longitude = 0) THEN 999999999
WHEN latitude IS NULL OR longitude IS NULL OR
(latitude = 0.0 AND longitude = 0.0) THEN 999999999
ELSE
(position_latitude - (SELECT position_latitude FROM OurNode)) *
(position_latitude - (SELECT position_latitude FROM OurNode)) +
(position_longitude - (SELECT position_longitude FROM OurNode)) *
(position_longitude - (SELECT position_longitude FROM OurNode))
(latitude - (SELECT latitude FROM OurNode)) *
(latitude - (SELECT latitude FROM OurNode)) +
(longitude - (SELECT longitude FROM OurNode)) *
(longitude - (SELECT longitude FROM OurNode))
END
WHEN :sort = 'hops_away' THEN hopsAway
WHEN :sort = 'hops_away' THEN hops_away
WHEN :sort = 'channel' THEN channel
WHEN :sort = 'via_mqtt' THEN user_longName LIKE '%(MQTT)' -- viaMqtt
WHEN :sort = 'via_mqtt' THEN long_name LIKE '%(MQTT)' -- viaMqtt
ELSE 0
END ASC,
lastHeard DESC
last_heard DESC
"""
)
fun getNodes(
sort: String,
filter: String,
includeUnknown: Boolean,
unknownHwModel: MeshProtos.HardwareModel
): Flow<List<NodeInfo>>
): Flow<List<NodeEntity>>
@Upsert
fun upsert(node: NodeInfo)
fun upsert(node: NodeEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun putAll(nodes: List<NodeInfo>)
fun putAll(nodes: List<NodeEntity>)
@Query("DELETE FROM NodeInfo")
@Query("DELETE FROM nodes")
fun clearNodeInfo()
@Query("DELETE FROM NodeInfo WHERE num=:num")
@Query("DELETE FROM nodes WHERE num=:num")
fun deleteNode(num: Int)
}

View file

@ -0,0 +1,156 @@
package com.geeksville.mesh.database.entity
import android.graphics.Color
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.geeksville.mesh.DeviceMetrics
import com.geeksville.mesh.EnvironmentMetrics
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.PaxcountProtos
import com.geeksville.mesh.Position
import com.geeksville.mesh.TelemetryProtos
import com.geeksville.mesh.copy
import com.geeksville.mesh.util.latLongToMeter
@Suppress("MagicNumber")
@Entity(tableName = "nodes")
data class NodeEntity(
@PrimaryKey(autoGenerate = false)
val num: Int, // This is immutable, and used as a key
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var user: MeshProtos.User = MeshProtos.User.getDefaultInstance(),
@ColumnInfo(name = "long_name") var longName: String? = null,
@ColumnInfo(name = "short_name") var shortName: String? = null, // used in includeUnknown filter
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var position: MeshProtos.Position = MeshProtos.Position.getDefaultInstance(),
var latitude: Double = 0.0,
var longitude: Double = 0.0,
var snr: Float = Float.MAX_VALUE,
var rssi: Int = Int.MAX_VALUE,
@ColumnInfo(name = "last_heard")
var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
@ColumnInfo(name = "device_metrics", typeAffinity = ColumnInfo.BLOB)
var deviceTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(),
var channel: Int = 0,
@ColumnInfo(name = "via_mqtt")
var viaMqtt: Boolean = false,
@ColumnInfo(name = "hops_away")
var hopsAway: Int = 0,
@ColumnInfo(name = "is_favorite")
var isFavorite: Boolean = false,
@ColumnInfo(name = "environment_metrics", typeAffinity = ColumnInfo.BLOB)
var environmentTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(),
@ColumnInfo(name = "power_metrics", typeAffinity = ColumnInfo.BLOB)
var powerTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.getDefaultInstance(),
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
var paxcounter: PaxcountProtos.Paxcount = PaxcountProtos.Paxcount.getDefaultInstance(),
) {
val deviceMetrics: TelemetryProtos.DeviceMetrics
get() = deviceTelemetry.deviceMetrics
val environmentMetrics: TelemetryProtos.EnvironmentMetrics
get() = environmentTelemetry.environmentMetrics
val powerMetrics: TelemetryProtos.PowerMetrics
get() = powerTelemetry.powerMetrics
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 batteryLevel get() = deviceMetrics.batteryLevel
val voltage get() = deviceMetrics.voltage
val batteryStr get() = if (batteryLevel in 1..100) "$batteryLevel%" else ""
fun setPosition(p: MeshProtos.Position, defaultTime: Int = currentTime()) {
position = p.copy { time = if (p.time != 0) p.time else defaultTime }
latitude = degD(p.latitudeI)
longitude = degD(p.longitudeI)
}
// @return distance in meters to some other node (or null if unknown)
fun distance(o: NodeEntity) = latLongToMeter(latitude, longitude, o.latitude, o.longitude)
/**
* true if the device was heard from recently
*/
val isOnline: Boolean
get() {
val now = System.currentTimeMillis() / 1000
val timeout = 15 * 60
return (now - lastHeard <= timeout)
}
companion object {
/// Convert to a double representation of degrees
fun degD(i: Int) = i * 1e-7
fun degI(d: Double) = (d * 1e7).toInt()
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
}
}
fun NodeEntity.toNodeInfo() = NodeInfo(
num = num,
user = MeshUser(
id = user.id,
longName = user.longName,
shortName = user.shortName,
hwModel = user.hwModel,
role = user.roleValue,
).takeIf { user.id.isNotEmpty() },
position = Position(
latitude = latitude,
longitude = longitude,
altitude = position.altitude,
time = position.time,
satellitesInView = position.satsInView,
groundSpeed = position.groundSpeed,
groundTrack = position.groundTrack,
precisionBits = position.precisionBits,
).takeIf { it.isValid() },
snr = snr,
rssi = rssi,
lastHeard = lastHeard,
deviceMetrics = DeviceMetrics(
time = deviceTelemetry.time,
batteryLevel = deviceMetrics.batteryLevel,
voltage = deviceMetrics.voltage,
channelUtilization = deviceMetrics.channelUtilization,
airUtilTx = deviceMetrics.airUtilTx,
uptimeSeconds = deviceMetrics.uptimeSeconds,
),
channel = channel,
environmentMetrics = EnvironmentMetrics(
time = environmentTelemetry.time,
temperature = environmentMetrics.temperature,
relativeHumidity = environmentMetrics.relativeHumidity,
barometricPressure = environmentMetrics.barometricPressure,
gasResistance = environmentMetrics.gasResistance,
voltage = environmentMetrics.voltage,
current = environmentMetrics.current,
iaq = environmentMetrics.iaq,
),
hopsAway = hopsAway,
)

View file

@ -2,12 +2,12 @@ package com.geeksville.mesh.model
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.MeshProtos
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.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
@ -33,12 +33,12 @@ class NodeDB @Inject constructor(
private val _myId = MutableStateFlow<String?>(null)
val myId: StateFlow<String?> get() = _myId
// A map from nodeNum to NodeInfo
private val _nodeDBbyNum = MutableStateFlow<Map<Int, NodeInfo>>(mapOf())
val nodeDBbyNum: StateFlow<Map<Int, NodeInfo>> get() = _nodeDBbyNum
// A map from nodeNum to NodeEntity
private val _nodeDBbyNum = MutableStateFlow<Map<Int, NodeEntity>>(mapOf())
val nodeDBbyNum: StateFlow<Map<Int, NodeEntity>> get() = _nodeDBbyNum
fun getUser(userId: String?) = userId?.let { id ->
nodeDBbyNum.value.values.find { it.user?.id == id }?.user
nodeDBbyNum.value.values.find { it.user.id == id }?.user
}
init {
@ -47,7 +47,7 @@ class NodeDB @Inject constructor(
nodeInfoDao.nodeDBbyNum().onEach {
_nodeDBbyNum.value = it
val ourNodeInfo = it.values.firstOrNull()
val ourNodeInfo = it.values.firstOrNull()?.toNodeInfo()
_ourNodeInfo.value = ourNodeInfo
_myId.value = ourNodeInfo?.user?.id
}.launchIn(processLifecycle.coroutineScope)
@ -61,16 +61,13 @@ class NodeDB @Inject constructor(
sort = sort.sqlValue,
filter = filter,
includeUnknown = includeUnknown,
unknownHwModel = MeshProtos.HardwareModel.UNSET
)
fun myNodeInfoFlow(): Flow<MyNodeInfo?> = nodeInfoDao.getMyNodeInfo()
suspend fun upsert(node: NodeInfo) = withContext(Dispatchers.IO) {
suspend fun upsert(node: NodeEntity) = withContext(Dispatchers.IO) {
nodeInfoDao.upsert(node)
}
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeInfo>) = withContext(Dispatchers.IO) {
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeEntity>) = withContext(Dispatchers.IO) {
nodeInfoDao.clearMyNodeInfo()
nodeInfoDao.setMyNodeInfo(mi) // set MyNodeInfo first
nodeInfoDao.clearNodeInfo()

View file

@ -18,6 +18,7 @@ import com.geeksville.mesh.Portnums
import com.geeksville.mesh.Position
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.config
import com.geeksville.mesh.database.entity.toNodeInfo
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
@ -85,7 +86,7 @@ class RadioConfigViewModel @Inject constructor(
init {
combine(_destNum, radioConfigRepository.nodeDBbyNum) { destNum, nodes ->
nodes[destNum] ?: nodes.values.firstOrNull()
}.onEach { _destNode.value = it }.launchIn(viewModelScope)
}.onEach { _destNode.value = it?.toNodeInfo() }.launchIn(viewModelScope)
radioConfigRepository.deviceProfileFlow.onEach {
_currentDeviceProfile.value = it

View file

@ -23,6 +23,7 @@ import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.database.entity.toNodeInfo
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshService
@ -230,7 +231,7 @@ class UIViewModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
val nodeList: StateFlow<List<NodeInfo>> = nodesUiState.flatMapLatest { state ->
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
}.stateIn(
}.mapLatest { list -> list.map { it.toNodeInfo() } }.stateIn(
scope = viewModelScope,
started = Eagerly,
initialValue = emptyList(),
@ -239,14 +240,16 @@ 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 nodesByNum get() = nodeDB.nodeDBbyNum.value // FIXME only used in MapFragment
fun getUser(userId: String?) = nodeDB.getUser(userId) ?: MeshUser(
userId ?: DataPacket.ID_LOCAL,
app.getString(R.string.unknown_username),
app.getString(R.string.unknown_node_short_name),
MeshProtos.HardwareModel.UNSET,
)
// FIXME only used in MapFragment
val initialNodes get() = nodeDB.nodeDBbyNum.value.values.map { it.toNodeInfo() }
fun getUser(userId: String?) = nodeDB.getUser(userId) ?: user {
id = userId.orEmpty()
longName = app.getString(R.string.unknown_username)
shortName = app.getString(R.string.unknown_node_short_name)
hwModel = MeshProtos.HardwareModel.UNSET
}
private val _snackbarText = MutableLiveData<Any?>(null)
val snackbarText: LiveData<Any?> get() = _snackbarText
@ -330,7 +333,7 @@ class UIViewModel @Inject constructor(
Message(
uuid = it.uuid,
receivedTime = it.received_time,
user = getUser(it.data.from),
user = MeshUser(getUser(it.data.from)), // FIXME convert to proto User
text = it.data.text.orEmpty(),
time = it.data.time,
read = it.read,

View file

@ -12,6 +12,7 @@ import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.getChannelUrl
@ -54,10 +55,10 @@ class RadioConfigRepository @Inject constructor(
/**
* Flow representing the [NodeInfo] database.
*/
val nodeDBbyNum: StateFlow<Map<Int, NodeInfo>> get() = nodeDB.nodeDBbyNum
val nodeDBbyNum: StateFlow<Map<Int, NodeEntity>> get() = nodeDB.nodeDBbyNum
suspend fun upsert(node: NodeInfo) = nodeDB.upsert(node)
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeInfo>) {
suspend fun upsert(node: NodeEntity) = nodeDB.upsert(node)
suspend fun installNodeDB(mi: MyNodeInfo, nodes: List<NodeEntity>) {
nodeDB.installNodeDB(mi, nodes)
}

View file

@ -23,7 +23,9 @@ import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.toNodeInfo
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
@ -347,7 +349,7 @@ class MeshService : Service(), Logging {
// The database of active nodes, index is the node user ID string
// NOTE: some NodeInfos might be in only nodeDBbyNodeNum (because we don't yet know an ID).
private val nodeDBbyID get() = nodeDBbyNodeNum.mapKeys { it.value.user?.id }
private val nodeDBbyID get() = nodeDBbyNodeNum.mapKeys { it.value.user.id }
///
/// END OF MODEL
@ -368,22 +370,23 @@ class MeshService : Service(), Logging {
if (n == DataPacket.NODENUM_BROADCAST) DataPacket.ID_BROADCAST
else nodeDBbyNodeNum[n]?.user?.id ?: DataPacket.nodeNumToDefaultId(n)
private fun defaultUser(num: Int) = MeshUser(
id = DataPacket.nodeNumToDefaultId(num),
longName = getString(R.string.unknown_username),
shortName = getString(R.string.unknown_node_short_name),
hwModel = MeshProtos.HardwareModel.UNSET,
)
private fun defaultUser(num: Int) = user {
val userId = DataPacket.nodeNumToDefaultId(num)
id = userId
longName = "Meshtastic ${userId.takeLast(n = 4)}"
shortName = userId.takeLast(n = 4)
hwModel = MeshProtos.HardwareModel.UNSET
}
// given a nodeNum, return a db entry - creating if necessary
private fun getOrCreateNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: NodeInfo(n, defaultUser(n))
private fun getOrCreateNodeInfo(n: Int) = nodeDBbyNodeNum[n] ?: NodeEntity(n, defaultUser(n))
private val hexIdRegex = """\!([0-9A-Fa-f]+)""".toRegex()
private val rangeTestRegex = Regex("seq (\\d{1,10})")
/// Map a userid to a node/ node num, or throw an exception if not found
/// We prefer to find nodes based on their assigned IDs, but if no ID has been assigned to a node, we can also find it based on node number
private fun toNodeInfo(id: String): NodeInfo {
private fun toNodeInfo(id: String): NodeEntity {
// If this is a valid hexaddr will be !null
val hexStr = hexIdRegex.matchEntire(id)?.groups?.get(1)?.value
@ -421,22 +424,19 @@ class MeshService : Service(), Logging {
private inline fun updateNodeInfo(
nodeNum: Int,
withBroadcast: Boolean = true,
crossinline updateFn: (NodeInfo) -> Unit,
crossinline updateFn: (NodeEntity) -> Unit,
) {
val info = getOrCreateNodeInfo(nodeNum)
updateFn(info)
// This might have been the first time we know an ID for this node, so also update the by ID map
val userId = info.user?.id.orEmpty()
if (userId.isNotEmpty()) {
if (info.user.id.isNotEmpty()) {
if (haveNodeDB) serviceScope.handledLaunch {
radioConfigRepository.upsert(info)
}
}
// parcelable is busted
if (withBroadcast)
serviceBroadcasts.broadcastNodeChange(info)
serviceBroadcasts.broadcastNodeChange(info.toNodeInfo())
}
/// My node num
@ -488,6 +488,12 @@ class MeshService : Service(), Logging {
decoded = MeshProtos.Data.newBuilder().also {
initFn(it)
}.build()
if (decoded.portnum in setOf(Portnums.PortNum.TEXT_MESSAGE_APP, Portnums.PortNum.ADMIN_APP)) {
nodeDBbyNodeNum[to]?.user?.publicKey?.let { publicKey ->
pkiEncrypted = !publicKey.isEmpty
this.publicKey = publicKey
}
}
return build()
}
@ -642,7 +648,8 @@ class MeshService : Service(), Logging {
// Handle new telemetry info
Portnums.PortNum.TELEMETRY_APP_VALUE -> {
val u = TelemetryProtos.Telemetry.parseFrom(data.payload)
handleReceivedTelemetry(packet.from, u, dataPacket.time)
.copy { if (time == 0) time = (dataPacket.time / 1000L).toInt() }
handleReceivedTelemetry(packet.from, u)
}
Portnums.PortNum.ROUTING_APP_VALUE -> {
@ -665,6 +672,12 @@ class MeshService : Service(), Logging {
shouldBroadcast = false
}
Portnums.PortNum.PAXCOUNTER_APP_VALUE -> {
val p = PaxcountProtos.Paxcount.parseFrom(data.payload)
handleReceivedPaxcounter(packet.from, p)
shouldBroadcast = false
}
Portnums.PortNum.STORE_FORWARD_APP_VALUE -> {
val u = StoreAndForwardProtos.StoreAndForward.parseFrom(data.payload)
handleReceivedStoreAndForward(dataPacket, u)
@ -742,7 +755,9 @@ class MeshService : Service(), Logging {
/// Update our DB of users based on someone sending out a User subpacket
private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User, channel: Int = 0) {
updateNodeInfo(fromNum) {
it.user = MeshUser(p)
it.user = p
it.longName = p.longName
it.shortName = p.shortName
it.channel = channel
}
}
@ -762,8 +777,8 @@ class MeshService : Service(), Logging {
debug("Ignoring nop position update for the local node")
} else {
updateNodeInfo(fromNum) {
debug("update position: ${it.user?.longName?.toPIIString()} with ${p.toPIIString()}")
it.position = Position(p, (defaultTime / 1000L).toInt())
debug("update position: ${it.longName?.toPIIString()} with ${p.toPIIString()}")
it.setPosition(p, (defaultTime / 1000L).toInt())
}
}
}
@ -772,18 +787,20 @@ class MeshService : Service(), Logging {
private fun handleReceivedTelemetry(
fromNum: Int,
t: TelemetryProtos.Telemetry,
defaultTime: Long = System.currentTimeMillis()
) {
updateNodeInfo(fromNum) {
if (t.hasDeviceMetrics()) it.deviceMetrics = DeviceMetrics(
t.deviceMetrics, if (t.time != 0) t.time else (defaultTime / 1000L).toInt()
)
if (t.hasEnvironmentMetrics()) it.environmentMetrics = EnvironmentMetrics(
t.environmentMetrics, if (t.time != 0) t.time else (defaultTime / 1000L).toInt()
)
when {
t.hasDeviceMetrics() -> it.deviceTelemetry = t
t.hasEnvironmentMetrics() -> it.environmentTelemetry = t
t.hasPowerMetrics() -> it.powerTelemetry = t
}
}
}
private fun handleReceivedPaxcounter(fromNum: Int, p: PaxcountProtos.Paxcount) {
updateNodeInfo(fromNum) { it.paxcounter = p }
}
private fun handleReceivedStoreAndForward(
dataPacket: DataPacket,
s: StoreAndForwardProtos.StoreAndForward,
@ -1311,27 +1328,21 @@ class MeshService : Service(), Logging {
radioConfigRepository.setStatusMessage("Channels (${ch.index + 1} / $maxChannels)")
}
private fun MeshProtos.NodeInfo.toEntity() = NodeInfo(
private fun MeshProtos.NodeInfo.toEntity() = NodeEntity(
num = num,
user = if (hasUser()) {
MeshUser(user.copy { if (viaMqtt) longName = "$longName (MQTT)" })
} else {
defaultUser(num)
},
position = if (hasPosition()) {
Position(position)
} else {
null
},
snr = snr,
user = if (hasUser()) user else defaultUser(num)
.copy { if (viaMqtt) longName = "$longName (MQTT)" },
longName = user.longName,
shortName = user.shortName.takeIf { hasUser() },
position = position,
latitude = position.latitudeI * 1e-7,
longitude = position.longitudeI * 1e-7,
lastHeard = lastHeard,
deviceMetrics = if(hasDeviceMetrics()) {
DeviceMetrics(deviceMetrics)
} else {
null
},
deviceTelemetry = telemetry { deviceMetrics = deviceMetrics },
channel = channel,
viaMqtt = viaMqtt,
hopsAway = hopsAway,
isFavorite = isFavorite,
)
private fun handleNodeInfo(info: MeshProtos.NodeInfo) {
@ -1589,7 +1600,7 @@ class MeshService : Service(), Logging {
private fun setOwner(packetId: Int, user: MeshProtos.User) = with(user) {
val dest = nodeDBbyID[id]
?: throw Exception("Can't set user without a NodeInfo") // this shouldn't happen
val old = dest.user!!
val old = dest.user
if (longName == old.longName && shortName == old.shortName && isLicensed == old.isLicensed) {
debug("Ignoring nop owner change")
} else {
@ -1665,7 +1676,12 @@ class MeshService : Service(), Logging {
override fun getPacketId() = toRemoteExceptions { generatePacketId() }
override fun setOwner(user: MeshUser) = toRemoteExceptions {
setOwner(generatePacketId(), user.toProto())
setOwner(generatePacketId(), user {
id = user.id
longName = user.longName
shortName = user.shortName
isLicensed = user.isLicensed
})
}
override fun setRemoteOwner(id: Int, payload: ByteArray) = toRemoteExceptions {
@ -1813,7 +1829,7 @@ class MeshService : Service(), Logging {
}
override fun getNodes(): MutableList<NodeInfo> = toRemoteExceptions {
val r = nodeDBbyNodeNum.values.toMutableList()
val r = nodeDBbyNodeNum.values.map { it.toNodeInfo() }.toMutableList()
info("in getOnline, count=${r.size}")
// return arrayOf("+16508675309")
r

View file

@ -467,7 +467,7 @@ fun MapView(
}
fun MapView.zoomToNodes() {
val nodeMarkers = onNodesChanged(model.nodesByNum.values)
val nodeMarkers = onNodesChanged(model.initialNodes)
if (nodeMarkers.isNotEmpty()) {
val box = BoundingBox.fromGeoPoints(nodeMarkers.map { it.position })
val center = GeoPoint(box.centerLatitude, box.centerLongitude)