mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: introduce NodeEntity protobuf-based database entity (#1250)
This commit is contained in:
parent
2433cbc00a
commit
396195a1b8
12 changed files with 1029 additions and 151 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue