refactor: migrate `nodeDB` to Room database (#717)

pull/721/head
Andre K 2023-09-05 08:19:26 -03:00 zatwierdzone przez GitHub
rodzic 99d7147efe
commit 83722159be
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 742 dodań i 148 usunięć

Wyświetl plik

@ -0,0 +1,140 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "5a779656cf00ccb63e4140845e25be09",
"entities": [
{
"tableName": "packet",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "port_num",
"columnName": "port_num",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contact_key",
"columnName": "contact_key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "received_time",
"columnName": "received_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uuid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message_type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "received_date",
"columnName": "received_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "raw_message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uuid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "quick_chat",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uuid"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5a779656cf00ccb63e4140845e25be09')"
]
}
}

Wyświetl plik

@ -0,0 +1,402 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "935bed00304e3ea7c0398f8e1c19992a",
"entities": [
{
"tableName": "MyNodeInfo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`myNodeNum` INTEGER NOT NULL, `hasGPS` INTEGER NOT NULL, `model` TEXT, `firmwareVersion` TEXT, `couldUpdate` INTEGER NOT NULL, `shouldUpdate` INTEGER NOT NULL, `currentPacketId` INTEGER NOT NULL, `messageTimeoutMsec` INTEGER NOT NULL, `minAppVersion` INTEGER NOT NULL, `maxChannels` INTEGER NOT NULL, `hasWifi` INTEGER NOT NULL, `channelUtilization` REAL NOT NULL, `airUtilTx` REAL NOT NULL, PRIMARY KEY(`myNodeNum`))",
"fields": [
{
"fieldPath": "myNodeNum",
"columnName": "myNodeNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasGPS",
"columnName": "hasGPS",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "model",
"columnName": "model",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "firmwareVersion",
"columnName": "firmwareVersion",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "couldUpdate",
"columnName": "couldUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shouldUpdate",
"columnName": "shouldUpdate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentPacketId",
"columnName": "currentPacketId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "messageTimeoutMsec",
"columnName": "messageTimeoutMsec",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minAppVersion",
"columnName": "minAppVersion",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maxChannels",
"columnName": "maxChannels",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hasWifi",
"columnName": "hasWifi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "channelUtilization",
"columnName": "channelUtilization",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "airUtilTx",
"columnName": "airUtilTx",
"affinity": "REAL",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"myNodeNum"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "NodeInfo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`num` INTEGER NOT NULL, `snr` REAL NOT NULL, `rssi` INTEGER NOT NULL, `lastHeard` INTEGER NOT NULL, `channel` INTEGER NOT NULL, `user_id` TEXT, `user_longName` TEXT, `user_shortName` TEXT, `user_hwModel` TEXT, `user_isLicensed` INTEGER, `position_latitude` REAL, `position_longitude` REAL, `position_altitude` INTEGER, `position_time` INTEGER, `devMetrics_time` INTEGER, `devMetrics_batteryLevel` INTEGER, `devMetrics_voltage` REAL, `devMetrics_channelUtilization` REAL, `devMetrics_airUtilTx` REAL, `envMetrics_time` INTEGER, `envMetrics_temperature` REAL, `envMetrics_relativeHumidity` REAL, `envMetrics_barometricPressure` REAL, `envMetrics_gasResistance` REAL, `envMetrics_voltage` REAL, `envMetrics_current` REAL, PRIMARY KEY(`num`))",
"fields": [
{
"fieldPath": "num",
"columnName": "num",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "snr",
"columnName": "snr",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "rssi",
"columnName": "rssi",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastHeard",
"columnName": "lastHeard",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "channel",
"columnName": "channel",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user.id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.longName",
"columnName": "user_longName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.shortName",
"columnName": "user_shortName",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.hwModel",
"columnName": "user_hwModel",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.isLicensed",
"columnName": "user_isLicensed",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "position.latitude",
"columnName": "position_latitude",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "position.longitude",
"columnName": "position_longitude",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "position.altitude",
"columnName": "position_altitude",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "position.time",
"columnName": "position_time",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "deviceMetrics.time",
"columnName": "devMetrics_time",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "deviceMetrics.batteryLevel",
"columnName": "devMetrics_batteryLevel",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "deviceMetrics.voltage",
"columnName": "devMetrics_voltage",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "deviceMetrics.channelUtilization",
"columnName": "devMetrics_channelUtilization",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "deviceMetrics.airUtilTx",
"columnName": "devMetrics_airUtilTx",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "environmentMetrics.time",
"columnName": "envMetrics_time",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "environmentMetrics.temperature",
"columnName": "envMetrics_temperature",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "environmentMetrics.relativeHumidity",
"columnName": "envMetrics_relativeHumidity",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "environmentMetrics.barometricPressure",
"columnName": "envMetrics_barometricPressure",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "environmentMetrics.gasResistance",
"columnName": "envMetrics_gasResistance",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "environmentMetrics.voltage",
"columnName": "envMetrics_voltage",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "environmentMetrics.current",
"columnName": "envMetrics_current",
"affinity": "REAL",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"num"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "packet",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `port_num` INTEGER NOT NULL, `contact_key` TEXT NOT NULL, `received_time` INTEGER NOT NULL, `data` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "port_num",
"columnName": "port_num",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contact_key",
"columnName": "contact_key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "received_time",
"columnName": "received_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uuid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "log",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `type` TEXT NOT NULL, `received_date` INTEGER NOT NULL, `message` TEXT NOT NULL, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message_type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "received_date",
"columnName": "received_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "raw_message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uuid"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "quick_chat",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `message` TEXT NOT NULL, `mode` TEXT NOT NULL, `position` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"uuid"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '935bed00304e3ea7c0398f8e1c19992a')"
]
}
}

Wyświetl plik

@ -311,7 +311,6 @@ class MainActivity : AppCompatActivity(), Logging {
unregisterMeshReceiver()
val filter = IntentFilter()
filter.addAction(MeshService.ACTION_MESH_CONNECTED)
filter.addAction(MeshService.ACTION_NODE_CHANGE)
registerReceiver(meshServiceReceiver, filter)
receiverRegistered = true
}
@ -489,17 +488,6 @@ class MainActivity : AppCompatActivity(), Logging {
debug("Received from mesh service $intent")
when (intent.action) {
MeshService.ACTION_NODE_CHANGE -> {
val info: NodeInfo? = intent.getParcelableExtraCompat(EXTRA_NODEINFO)
debug("UI nodechange $info")
// We only care about nodes that have user info
info?.user?.id?.let {
val nodes = model.nodeDB.nodes.value!! + Pair(it, info)
model.nodeDB.setNodes(nodes)
}
}
MeshService.ACTION_MESH_CONNECTED -> {
val extra = intent.getStringExtra(EXTRA_CONNECTED)
if (extra != null) {

Wyświetl plik

@ -1,12 +1,20 @@
package com.geeksville.mesh
import android.os.Parcel
import android.os.Parcelable
import kotlinx.serialization.Serializable
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
/**
* Room [Entity] and [PrimaryKey] annotations and imports can be removed when only using the API.
* For details check the AIDL interface in [com.geeksville.mesh.IMeshService]
*/
// MyNodeInfo sent via special protobuf from radio
@Serializable
@Parcelize
@Entity(tableName = "MyNodeInfo")
data class MyNodeInfo(
@PrimaryKey(autoGenerate = false)
val myNodeNum: Int,
val hasGPS: Boolean,
val model: String?,
@ -23,52 +31,4 @@ data class MyNodeInfo(
) : Parcelable {
/** A human readable description of the software/hardware version */
val firmwareString: String get() = "$model $firmwareVersion"
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readByte() != 0.toByte(),
parcel.readString(),
parcel.readString(),
parcel.readByte() != 0.toByte(),
parcel.readByte() != 0.toByte(),
parcel.readLong(),
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readByte() != 0.toByte(),
parcel.readFloat(),
parcel.readFloat()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(myNodeNum)
parcel.writeByte(if (hasGPS) 1 else 0)
parcel.writeString(model)
parcel.writeString(firmwareVersion)
parcel.writeByte(if (couldUpdate) 1 else 0)
parcel.writeByte(if (shouldUpdate) 1 else 0)
parcel.writeLong(currentPacketId)
parcel.writeInt(messageTimeoutMsec)
parcel.writeInt(minAppVersion)
parcel.writeInt(maxChannels)
parcel.writeByte(if (hasWifi) 1 else 0)
parcel.writeFloat(channelUtilization)
parcel.writeFloat(airUtilTx)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MyNodeInfo> {
override fun createFromParcel(parcel: Parcel): MyNodeInfo {
return MyNodeInfo(parcel)
}
override fun newArray(size: Int): Array<MyNodeInfo?> {
return arrayOfNulls(size)
}
}
}
}

Wyświetl plik

@ -2,20 +2,25 @@ package com.geeksville.mesh
import android.graphics.Color
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.geeksville.mesh.MeshProtos.User
import com.geeksville.mesh.util.GPSFormat
import com.geeksville.mesh.util.bearing
import com.geeksville.mesh.util.latLongToMeter
import com.geeksville.mesh.util.anonymize
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Room [Embedded], [Entity] and [PrimaryKey] annotations and imports can be removed when only using the API.
* For details check the AIDL interface in [com.geeksville.mesh.IMeshService]
*/
//
// model objects that directly map to the corresponding protobufs
//
@Serializable
@Parcelize
data class MeshUser(
val id: String,
@ -41,7 +46,6 @@ data class MeshUser(
else hwModel.name.replace('_', '-').replace('p', '.').lowercase()
}
@Serializable
@Parcelize
data class Position(
val latitude: Double,
@ -94,7 +98,6 @@ data class Position(
}
@Serializable
@Parcelize
data class DeviceMetrics(
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
@ -122,7 +125,6 @@ data class DeviceMetrics(
}
}
@Serializable
@Parcelize
data class EnvironmentMetrics(
val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
@ -154,16 +156,22 @@ data class EnvironmentMetrics(
}
}
@Serializable
@Parcelize
@Entity(tableName = "NodeInfo")
data class NodeInfo(
@PrimaryKey(autoGenerate = false)
val num: Int, // This is immutable, and used as a key
@Embedded(prefix = "user_")
var user: MeshUser? = null,
@Embedded(prefix = "position_")
var position: Position? = null,
var snr: Float = Float.MAX_VALUE,
var rssi: Int = Int.MAX_VALUE,
var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
@Embedded(prefix = "devMetrics_")
var deviceMetrics: DeviceMetrics? = null,
val channel: Int = 0,
@Embedded(prefix = "envMetrics_")
var environmentMetrics: EnvironmentMetrics? = null,
) : Parcelable {

Wyświetl plik

@ -2,6 +2,8 @@ package com.geeksville.mesh.database
import android.app.Application
import com.geeksville.mesh.database.dao.MeshLogDao
import com.geeksville.mesh.database.dao.MyNodeInfoDao
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import dagger.Module
@ -18,6 +20,16 @@ class DatabaseModule {
fun provideDatabase(app: Application): MeshtasticDatabase =
MeshtasticDatabase.getDatabase(app)
@Provides
fun provideMyNodeInfoDao(database: MeshtasticDatabase): MyNodeInfoDao {
return database.myNodeInfoDao()
}
@Provides
fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao {
return database.nodeInfoDao()
}
@Provides
fun providePacketDao(database: MeshtasticDatabase): PacketDao {
return database.packetDao()

Wyświetl plik

@ -1,20 +1,40 @@
package com.geeksville.mesh.database
import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.MeshLogDao
import com.geeksville.mesh.database.dao.MyNodeInfoDao
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
@Database(entities = [Packet::class, MeshLog::class, QuickChatAction::class], version = 3, exportSchema = false)
@Database(
entities = [
MyNodeInfo::class,
NodeInfo::class,
Packet::class,
MeshLog::class,
QuickChatAction::class
],
autoMigrations = [
AutoMigration (from = 3, to = 4),
],
version = 4,
exportSchema = true,
)
@TypeConverters(Converters::class)
abstract class MeshtasticDatabase : RoomDatabase() {
abstract fun myNodeInfoDao(): MyNodeInfoDao
abstract fun nodeInfoDao(): NodeInfoDao
abstract fun packetDao(): PacketDao
abstract fun meshLogDao(): MeshLogDao
abstract fun quickChatActionDao(): QuickChatActionDao
@ -31,4 +51,4 @@ abstract class MeshtasticDatabase : RoomDatabase() {
.build()
}
}
}
}

Wyświetl plik

@ -0,0 +1,21 @@
package com.geeksville.mesh.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.geeksville.mesh.MyNodeInfo
import kotlinx.coroutines.flow.Flow
@Dao
interface MyNodeInfoDao {
@Query("SELECT * FROM MyNodeInfo")
fun getMyNodeInfo(): Flow<MyNodeInfo?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setMyNodeInfo(myInfo: MyNodeInfo?)
@Query("DELETE FROM MyNodeInfo")
fun clearMyNodeInfo()
}

Wyświetl plik

@ -0,0 +1,42 @@
package com.geeksville.mesh.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Upsert
import com.geeksville.mesh.NodeInfo
import kotlinx.coroutines.flow.Flow
@Dao
interface NodeInfoDao {
@Query("SELECT * FROM NodeInfo")
fun getNodes(): Flow<List<NodeInfo>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(node: NodeInfo)
@Upsert
fun upsert(node: NodeInfo)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun putAll(nodes: List<NodeInfo>)
@Query("DELETE FROM NodeInfo")
fun clearNodeInfo()
@Query("SELECT * FROM NodeInfo WHERE num=:num")
fun getNodeInfo(num: Int): NodeInfo?
// @Transaction
// suspend fun updateUser(num: Int, updatedUser: MeshUser) {
// getNodeInfo(num)?.let {
// val updatedNodeInfo = it.copy(user = updatedUser)
// upsert(updatedNodeInfo)
// }
// }
// @Query("Update node_info set position=:position WHERE num=:num")
// fun updatePosition(num: Int, position: MeshProtos.Position)
}

Wyświetl plik

@ -2,11 +2,12 @@ package com.geeksville.mesh.model
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
import kotlinx.coroutines.flow.MutableStateFlow
/// NodeDB lives inside the UIViewModel, but it needs a backpointer to reach the service
class NodeDB(private val ui: UIViewModel) {
@ -40,11 +41,11 @@ class NodeDB(private val ui: UIViewModel) {
),
it
)
}
}.associateBy { it.user?.id!! }
private val seedWithTestNodes = false
/// The unique ID of our node
// The unique userId of our node
private val _myId = MutableLiveData<String?>(if (seedWithTestNodes) "+16508765309" else null)
val myId: LiveData<String?> get() = _myId
@ -52,13 +53,16 @@ class NodeDB(private val ui: UIViewModel) {
_myId.value = myId
}
/// A map from nodeid to to nodeinfo
private val _nodes = MutableLiveData<Map<String, NodeInfo>>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it }
.toTypedArray()))
val nodes: LiveData<Map<String, NodeInfo>> get() = _nodes
// A map from userId to NodeInfo
private val _nodes = MutableStateFlow(if (seedWithTestNodes) testNodes else mapOf())
val nodes: LiveData<Map<String, NodeInfo>> = _nodes.asLiveData()
val nodesByNum get() = nodes.value?.values?.associateBy { it.num }
fun setNodes(nodes: Map<String, NodeInfo>) {
_nodes.value = nodes
}
}
fun setNodes(list: List<NodeInfo>) {
setNodes(list.associateBy { it.user?.id!! })
}
}

Wyświetl plik

@ -123,6 +123,9 @@ class UIViewModel @Inject constructor(
val packetResponse: StateFlow<MeshLog?> = _packetResponse
init {
radioConfigRepository.nodeInfoFlow().onEach(nodeDB::setNodes)
.launchIn(viewModelScope)
viewModelScope.launch {
meshLogRepository.getAllLogs().collect { logs ->
_meshLog.value = logs

Wyświetl plik

@ -7,18 +7,63 @@ import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.database.dao.MyNodeInfoDao
import com.geeksville.mesh.database.dao.NodeInfoDao
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import javax.inject.Inject
/**
* Class responsible for radio configuration data.
* Combines access to [ChannelSet], [LocalConfig] & [LocalModuleConfig] data stores.
* Combines access to [MyNodeInfo] & [NodeInfo] Room databases
* and [ChannelSet], [LocalConfig] & [LocalModuleConfig] data stores.
*/
class RadioConfigRepository @Inject constructor(
private val myNodeInfoDao: MyNodeInfoDao,
private val nodeInfoDao: NodeInfoDao,
private val channelSetRepository: ChannelSetRepository,
private val localConfigRepository: LocalConfigRepository,
private val moduleConfigRepository: ModuleConfigRepository,
) {
suspend fun clearNodeDB() = withContext(Dispatchers.IO) {
myNodeInfoDao.clearMyNodeInfo()
nodeInfoDao.clearNodeInfo()
}
/**
* Flow representing the [MyNodeInfo] database.
*/
fun myNodeInfoFlow(): Flow<MyNodeInfo?> = myNodeInfoDao.getMyNodeInfo()
suspend fun getMyNodeInfo(): MyNodeInfo? = myNodeInfoFlow().firstOrNull()
suspend fun setMyNodeInfo(myInfo: MyNodeInfo?) = withContext(Dispatchers.IO) {
myNodeInfoDao.setMyNodeInfo(myInfo)
}
/**
* Flow representing the [NodeInfo] database.
*/
fun nodeInfoFlow(): Flow<List<NodeInfo>> = nodeInfoDao.getNodes()
suspend fun getNodes(): List<NodeInfo>? = nodeInfoFlow().firstOrNull()
suspend fun upsert(node: NodeInfo) = withContext(Dispatchers.IO) {
nodeInfoDao.upsert(node)
}
suspend fun putAll(nodes: List<NodeInfo>) = withContext(Dispatchers.IO) {
nodeInfoDao.putAll(nodes)
}
suspend fun installNodeDB(mi: MyNodeInfo?, nodes: List<NodeInfo>) {
clearNodeDB()
putAll(nodes)
setMyNodeInfo(mi) // set MyNodeInfo last
}
/**
* Flow representing the [ChannelSet] data store.
*/

Wyświetl plik

@ -7,7 +7,6 @@ import android.content.pm.ServiceInfo
import android.os.IBinder
import android.os.RemoteException
import androidx.core.app.ServiceCompat
import androidx.core.content.edit
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
@ -41,7 +40,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
@ -242,9 +240,6 @@ class MeshService : Service(), Logging {
// Switch to the IO thread
serviceScope.handledLaunch {
loadSettings() // Load our last known node DB
// We in turn need to use the radiointerface service
radioInterfaceService.connect()
}
radioInterfaceService.connectionState.onEach(::onRadioConnectionState)
@ -256,6 +251,8 @@ class MeshService : Service(), Logging {
radioConfigRepository.channelSetFlow.onEach { channelSet = it }
.launchIn(serviceScope)
loadSettings() // Load our last known node DB
// the rest of our init will happen once we are in radioConnection.onServiceConnected
}
@ -299,8 +296,6 @@ class MeshService : Service(), Logging {
override fun onDestroy() {
info("Destroying mesh service")
saveSettings()
// Make sure we aren't using the notification first
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
serviceNotifications.close()
@ -313,26 +308,8 @@ class MeshService : Service(), Logging {
/// BEGINNING OF MODEL - FIXME, move elsewhere
///
private fun getPrefs() = getSharedPreferences("service-prefs", Context.MODE_PRIVATE)
private fun installNewNodeDB(ni: MyNodeInfo, nodes: List<NodeInfo>) {
/// Save information about our mesh to disk, so we will have it when we next start the service (even before we hear from our device)
private fun saveSettings() = synchronized(nodeDBbyNodeNum) {
myNodeInfo?.let { myInfo ->
val settings = MeshServiceSettingsData(
myInfo = myInfo,
nodeDB = nodeDBbyNodeNum.values.toTypedArray(),
)
val json = Json { isLenient = true; allowSpecialFloatingPointValues = true }
val asString = json.encodeToString(MeshServiceSettingsData.serializer(), settings)
debug("Saving settings")
getPrefs().edit {
// FIXME, not really ideal to store this bigish blob in preferences
putString("json", asString)
}
}
}
private fun installNewNodeDB(ni: MyNodeInfo, nodes: Array<NodeInfo>) {
discardNodeDB() // Get rid of any old state
myNodeInfo = ni
@ -347,11 +324,11 @@ class MeshService : Service(), Logging {
private fun loadSettings() {
try {
getPrefs().getString("json", null)?.let { asString ->
serviceScope.handledLaunch {
val json = Json { isLenient = true; allowSpecialFloatingPointValues = true }
val settings = json.decodeFromString(MeshServiceSettingsData.serializer(), asString)
installNewNodeDB(settings.myInfo, settings.nodeDB)
val myInfo = radioConfigRepository.getMyNodeInfo()
val nodeDB = radioConfigRepository.getNodes()
if (myInfo != null && nodeDB != null) installNewNodeDB(myInfo, nodeDB)
// Note: we do not haveNodeDB = true because that means we've got a valid db from a real device (rather than this possibly stale hint)
}
@ -455,8 +432,12 @@ class MeshService : Service(), Logging {
// 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 (userId.isNotEmpty()) {
nodeDBbyID[userId] = info
if (haveNodeDB) serviceScope.handledLaunch {
radioConfigRepository.upsert(info)
}
}
// parcelable is busted
if (withBroadcast)
@ -1027,9 +1008,6 @@ class MeshService : Service(), Logging {
/// Perform all the steps needed once we start waiting for device sleep to complete
fun startDeviceSleep() {
// Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy)
saveSettings()
stopPacketQueue()
stopLocationRequests()
@ -1063,9 +1041,6 @@ class MeshService : Service(), Logging {
}
fun startDisconnect() {
// Just in case the user uncleanly reboots the phone, save now (we normally save in onDestroy)
saveSettings()
stopPacketQueue()
stopLocationRequests()
@ -1405,6 +1380,10 @@ class MeshService : Service(), Logging {
regenMyNodeInfo() // we have a node db now, so can possibly find a better hwmodel
myNodeInfo = newMyNodeInfo // we might have just updated myNodeInfo
serviceScope.handledLaunch {
radioConfigRepository.installNodeDB(newMyNodeInfo, nodeDBbyID.values.toList())
}
sendAnalytics()
if (deviceVersion < minDeviceVersion || appVersion < minAppVersion) {

Wyświetl plik

@ -1,30 +0,0 @@
package com.geeksville.mesh.service
import com.geeksville.mesh.MyNodeInfo
import com.geeksville.mesh.NodeInfo
import kotlinx.serialization.Serializable
/// Our saved preferences as stored on disk
@Serializable
data class MeshServiceSettingsData(
val nodeDB: Array<NodeInfo>,
val myInfo: MyNodeInfo,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MeshServiceSettingsData
if (!nodeDB.contentEquals(other.nodeDB)) return false
if (myInfo != other.myInfo) return false
return true
}
override fun hashCode(): Int {
var result = nodeDB.contentHashCode()
result = 31 * result + myInfo.hashCode()
return result
}
}