mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Modularize database classes (#3192)
This commit is contained in:
parent
989a6bc820
commit
613714cdb4
94 changed files with 384 additions and 431 deletions
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
* 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.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
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.ByteString
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class Converters : Logging {
|
||||
@TypeConverter
|
||||
fun dataFromString(value: String): DataPacket {
|
||||
val json = Json { isLenient = true }
|
||||
return json.decodeFromString(DataPacket.serializer(), value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun dataToString(value: DataPacket): String {
|
||||
val json = Json { isLenient = true }
|
||||
return json.encodeToString(DataPacket.serializer(), value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio = try {
|
||||
MeshProtos.FromRadio.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToFromRadio TypeConverter error:", ex)
|
||||
MeshProtos.FromRadio.getDefaultInstance()
|
||||
}
|
||||
|
||||
@TypeConverter fun fromRadioToBytes(value: MeshProtos.FromRadio): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToUser(bytes: ByteArray): MeshProtos.User = try {
|
||||
MeshProtos.User.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToUser TypeConverter error:", ex)
|
||||
MeshProtos.User.getDefaultInstance()
|
||||
}
|
||||
|
||||
@TypeConverter fun userToBytes(value: MeshProtos.User): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToPosition(bytes: ByteArray): MeshProtos.Position = try {
|
||||
MeshProtos.Position.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToPosition TypeConverter error:", ex)
|
||||
MeshProtos.Position.getDefaultInstance()
|
||||
}
|
||||
|
||||
@TypeConverter fun positionToBytes(value: MeshProtos.Position): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToTelemetry(bytes: ByteArray): TelemetryProtos.Telemetry = try {
|
||||
TelemetryProtos.Telemetry.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToTelemetry TypeConverter error:", ex)
|
||||
TelemetryProtos.Telemetry.newBuilder().build() // Return an empty Telemetry object
|
||||
}
|
||||
|
||||
@TypeConverter fun telemetryToBytes(value: TelemetryProtos.Telemetry): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToPaxcounter(bytes: ByteArray): PaxcountProtos.Paxcount = try {
|
||||
PaxcountProtos.Paxcount.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToPaxcounter TypeConverter error:", ex)
|
||||
PaxcountProtos.Paxcount.getDefaultInstance()
|
||||
}
|
||||
|
||||
@TypeConverter fun paxCounterToBytes(value: PaxcountProtos.Paxcount): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToMetadata(bytes: ByteArray): MeshProtos.DeviceMetadata = try {
|
||||
MeshProtos.DeviceMetadata.parseFrom(bytes)
|
||||
} catch (ex: InvalidProtocolBufferException) {
|
||||
errormsg("bytesToMetadata TypeConverter error:", ex)
|
||||
MeshProtos.DeviceMetadata.getDefaultInstance()
|
||||
}
|
||||
|
||||
@TypeConverter fun metadataToBytes(value: MeshProtos.DeviceMetadata): ByteArray? = value.toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringList(value: String?): List<String>? {
|
||||
if (value == null) {
|
||||
return null
|
||||
}
|
||||
return Json.decodeFromString<List<String>>(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toStringList(list: List<String>?): String? {
|
||||
if (list == null) {
|
||||
return null
|
||||
}
|
||||
return Json.encodeToString(list)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun bytesToByteString(bytes: ByteArray?): ByteString? = if (bytes == null) null else ByteString.copyFrom(bytes)
|
||||
|
||||
@TypeConverter fun byteStringToBytes(value: ByteString?): ByteArray? = value?.toByteArray()
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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.database
|
||||
|
||||
import android.app.Application
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(app: Application): MeshtasticDatabase =
|
||||
MeshtasticDatabase.getDatabase(app)
|
||||
|
||||
@Provides
|
||||
fun provideNodeInfoDao(database: MeshtasticDatabase): NodeInfoDao {
|
||||
return database.nodeInfoDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providePacketDao(database: MeshtasticDatabase): PacketDao {
|
||||
return database.packetDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideMeshLogDao(database: MeshtasticDatabase): MeshLogDao {
|
||||
return database.meshLogDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideQuickChatActionDao(database: MeshtasticDatabase): QuickChatActionDao {
|
||||
return database.quickChatActionDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideDeviceHardwareDao(database: MeshtasticDatabase): DeviceHardwareDao {
|
||||
return database.deviceHardwareDao()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideFirmwareReleaseDao(database: MeshtasticDatabase): FirmwareReleaseDao {
|
||||
return database.firmwareReleaseDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -22,14 +22,14 @@ import com.geeksville.mesh.MeshProtos
|
|||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.MeshLogDao
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
/*
|
||||
* 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.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.dao.MeshLogDao
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
|
||||
@Database(
|
||||
entities =
|
||||
[
|
||||
MyNodeEntity::class,
|
||||
NodeEntity::class,
|
||||
Packet::class,
|
||||
ContactSettings::class,
|
||||
MeshLog::class,
|
||||
QuickChatAction::class,
|
||||
ReactionEntity::class,
|
||||
MetadataEntity::class,
|
||||
DeviceHardwareEntity::class,
|
||||
FirmwareReleaseEntity::class,
|
||||
],
|
||||
autoMigrations =
|
||||
[
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(from = 4, to = 5),
|
||||
AutoMigration(from = 5, to = 6),
|
||||
AutoMigration(from = 6, to = 7),
|
||||
AutoMigration(from = 7, to = 8),
|
||||
AutoMigration(from = 8, to = 9),
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11),
|
||||
AutoMigration(from = 11, to = 12),
|
||||
AutoMigration(from = 12, to = 13, spec = AutoMigration12to13::class),
|
||||
AutoMigration(from = 13, to = 14),
|
||||
AutoMigration(from = 14, to = 15),
|
||||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
AutoMigration(from = 18, to = 19),
|
||||
AutoMigration(from = 19, to = 20),
|
||||
],
|
||||
version = 20,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class MeshtasticDatabase : RoomDatabase() {
|
||||
abstract fun nodeInfoDao(): NodeInfoDao
|
||||
|
||||
abstract fun packetDao(): PacketDao
|
||||
|
||||
abstract fun meshLogDao(): MeshLogDao
|
||||
|
||||
abstract fun quickChatActionDao(): QuickChatActionDao
|
||||
|
||||
abstract fun deviceHardwareDao(): DeviceHardwareDao
|
||||
|
||||
abstract fun firmwareReleaseDao(): FirmwareReleaseDao
|
||||
|
||||
companion object {
|
||||
fun getDatabase(context: Context): MeshtasticDatabase =
|
||||
Room.databaseBuilder(context.applicationContext, MeshtasticDatabase::class.java, "meshtastic_database")
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteTable.Entries(DeleteTable(tableName = "NodeInfo"), DeleteTable(tableName = "MyNodeInfo"))
|
||||
class AutoMigration12to13 : AutoMigrationSpec
|
||||
|
|
@ -21,12 +21,6 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.dao.NodeInfoDao
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -38,6 +32,12 @@ import kotlinx.coroutines.flow.mapLatest
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.NodeInfoDao
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import com.geeksville.mesh.Portnums.PortNum
|
||||
import com.geeksville.mesh.database.dao.PacketDao
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.PacketDao
|
||||
import org.meshtastic.core.database.entity.ContactSettings
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -18,35 +18,28 @@
|
|||
package com.geeksville.mesh.database
|
||||
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.database.dao.QuickChatActionDao
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.QuickChatActionDao
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import javax.inject.Inject
|
||||
|
||||
class QuickChatActionRepository @Inject constructor(
|
||||
class QuickChatActionRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val quickChatDaoLazy: dagger.Lazy<QuickChatActionDao>,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
private val quickChatActionDao by lazy {
|
||||
quickChatDaoLazy.get()
|
||||
}
|
||||
private val quickChatActionDao by lazy { quickChatDaoLazy.get() }
|
||||
|
||||
fun getAllActions() = quickChatActionDao.getAll().flowOn(dispatchers.io)
|
||||
|
||||
suspend fun upsert(action: QuickChatAction) = withContext(dispatchers.io) {
|
||||
quickChatActionDao.upsert(action)
|
||||
}
|
||||
suspend fun upsert(action: QuickChatAction) = withContext(dispatchers.io) { quickChatActionDao.upsert(action) }
|
||||
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) {
|
||||
quickChatActionDao.deleteAll()
|
||||
}
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { quickChatActionDao.deleteAll() }
|
||||
|
||||
suspend fun delete(action: QuickChatAction) = withContext(dispatchers.io) {
|
||||
quickChatActionDao.delete(action)
|
||||
}
|
||||
suspend fun delete(action: QuickChatAction) = withContext(dispatchers.io) { quickChatActionDao.delete(action) }
|
||||
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) = withContext(dispatchers.io) {
|
||||
quickChatActionDao.updateActionPosition(uuid, newPos)
|
||||
}
|
||||
}
|
||||
suspend fun setItemPosition(uuid: Long, newPos: Int) =
|
||||
withContext(dispatchers.io) { quickChatActionDao.updateActionPosition(uuid, newPos) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
|
||||
@Dao
|
||||
interface DeviceHardwareDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(deviceHardware: DeviceHardwareEntity)
|
||||
|
||||
@Query("SELECT * FROM device_hardware WHERE hwModel = :hwModel")
|
||||
suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity?
|
||||
|
||||
@Query("DELETE FROM device_hardware")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
|
||||
@Dao
|
||||
interface FirmwareReleaseDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(firmwareReleaseEntity: FirmwareReleaseEntity)
|
||||
|
||||
@Query("DELETE FROM firmware_release")
|
||||
suspend fun deleteAll()
|
||||
|
||||
@Query("SELECT * FROM firmware_release")
|
||||
suspend fun getAllReleases(): List<FirmwareReleaseEntity>
|
||||
|
||||
@Query("SELECT * FROM firmware_release WHERE release_type = :releaseType")
|
||||
suspend fun getReleasesByType(releaseType: FirmwareReleaseType): List<FirmwareReleaseEntity>
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface MeshLogDao {
|
||||
|
||||
@Query("SELECT * FROM log ORDER BY received_date DESC LIMIT 0,:maxItem")
|
||||
fun getAllLogs(maxItem: Int): Flow<List<MeshLog>>
|
||||
|
||||
@Query("SELECT * FROM log ORDER BY received_date ASC LIMIT 0,:maxItem")
|
||||
fun getAllLogsInReceiveOrder(maxItem: Int): Flow<List<MeshLog>>
|
||||
|
||||
/**
|
||||
* Retrieves [MeshLog]s matching 'from_num' (nodeNum) and 'port_num' (PortNum).
|
||||
*
|
||||
* @param portNum If 0, returns all MeshPackets. Otherwise, filters by 'port_num'.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM log
|
||||
WHERE from_num = :fromNum AND (:portNum = 0 AND port_num != 0 OR port_num = :portNum)
|
||||
ORDER BY received_date DESC LIMIT 0,:maxItem
|
||||
"""
|
||||
)
|
||||
fun getLogsFrom(fromNum: Int, portNum: Int, maxItem: Int): Flow<List<MeshLog>>
|
||||
|
||||
@Insert
|
||||
fun insert(log: MeshLog)
|
||||
|
||||
@Query("DELETE FROM log")
|
||||
fun deleteAll()
|
||||
|
||||
@Query("DELETE FROM log WHERE uuid = :uuid")
|
||||
fun deleteLog(uuid: String)
|
||||
|
||||
@Query("DELETE FROM log WHERE from_num = :fromNum AND port_num = :portNum")
|
||||
fun deleteLogs(fromNum: Int, portNum: Int)
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeWithRelations
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Dao
|
||||
interface NodeInfoDao {
|
||||
|
||||
/**
|
||||
* Verifies a [NodeEntity] before an upsert operation. It handles populating the publicKey for lazy migration,
|
||||
* checks for public key conflicts with new nodes, and manages updates to existing nodes, particularly in cases of
|
||||
* public key mismatches to prevent potential impersonation or data corruption.
|
||||
*
|
||||
* @param incomingNode The node entity to be verified.
|
||||
* @return A [NodeEntity] that is safe to upsert, or null if the upsert should be aborted (e.g., due to an
|
||||
* impersonation attempt, though this logic is currently commented out).
|
||||
*/
|
||||
private fun getVerifiedNodeForUpsert(incomingNode: NodeEntity): NodeEntity {
|
||||
// Populate the NodeEntity.publicKey field from the User.publicKey for consistency
|
||||
// and to support lazy migration.
|
||||
incomingNode.publicKey = incomingNode.user.publicKey
|
||||
|
||||
val existingNodeEntity = getNodeByNum(incomingNode.num)?.node
|
||||
|
||||
return if (existingNodeEntity == null) {
|
||||
handleNewNodeUpsertValidation(incomingNode)
|
||||
} else {
|
||||
handleExistingNodeUpsertValidation(existingNodeEntity, incomingNode)
|
||||
}
|
||||
}
|
||||
|
||||
/** Validates a new node before it is inserted into the database. */
|
||||
private fun handleNewNodeUpsertValidation(newNode: NodeEntity): NodeEntity {
|
||||
// Check if the new node's public key (if present and not empty)
|
||||
// is already claimed by another existing node.
|
||||
if (newNode.publicKey?.isEmpty == false) {
|
||||
val nodeWithSamePK = findNodeByPublicKey(newNode.publicKey)
|
||||
if (nodeWithSamePK != null && nodeWithSamePK.num != newNode.num) {
|
||||
// This is a potential impersonation attempt.
|
||||
return nodeWithSamePK
|
||||
}
|
||||
}
|
||||
// If no conflicting public key is found, or if the impersonation check is not active,
|
||||
// the new node is considered safe to add.
|
||||
return newNode
|
||||
}
|
||||
|
||||
private fun handleExistingNodeUpsertValidation(existingNode: NodeEntity, incomingNode: NodeEntity): NodeEntity {
|
||||
// A public key is considered matching if the incoming key equals the existing key,
|
||||
// OR if the existing key is empty (allowing a new key to be set or an update to proceed).
|
||||
val isPublicKeyMatchingOrExistingIsEmpty =
|
||||
existingNode.user.publicKey == incomingNode.publicKey || existingNode.user.publicKey.isEmpty
|
||||
|
||||
return if (isPublicKeyMatchingOrExistingIsEmpty) {
|
||||
// Keys match or existing key was empty: trust the incoming node data completely.
|
||||
// This allows for legitimate updates to user info and other fields.
|
||||
val resolvedNotes = if (incomingNode.notes.isBlank()) existingNode.notes else incomingNode.notes
|
||||
incomingNode.copy(notes = resolvedNotes)
|
||||
} else {
|
||||
existingNode.copy(
|
||||
lastHeard = incomingNode.lastHeard,
|
||||
snr = incomingNode.snr,
|
||||
position = incomingNode.position,
|
||||
// Preserve the existing user object, but update its internal public key to EMPTY
|
||||
// to reflect the conflict state.
|
||||
user = existingNode.user.toBuilder().setPublicKey(ByteString.EMPTY).build(),
|
||||
publicKey = ByteString.EMPTY,
|
||||
notes = existingNode.notes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM my_node")
|
||||
fun getMyNodeInfo(): Flow<MyNodeEntity?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun setMyNodeInfo(myInfo: MyNodeEntity)
|
||||
|
||||
@Query("DELETE FROM my_node")
|
||||
fun clearMyNodeInfo()
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM nodes
|
||||
ORDER BY CASE
|
||||
WHEN num = (SELECT myNodeNum FROM my_node LIMIT 1) THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
last_heard DESC
|
||||
""",
|
||||
)
|
||||
@Transaction
|
||||
fun nodeDBbyNum(): Flow<
|
||||
Map<
|
||||
@MapColumn(columnName = "num")
|
||||
Int,
|
||||
NodeWithRelations,
|
||||
>,
|
||||
>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
WITH OurNode AS (
|
||||
SELECT latitude, longitude
|
||||
FROM nodes
|
||||
WHERE num = (SELECT myNodeNum FROM my_node LIMIT 1)
|
||||
)
|
||||
SELECT * FROM nodes
|
||||
WHERE (:includeUnknown = 1 OR short_name IS NOT NULL)
|
||||
AND (:filter = ''
|
||||
OR (long_name LIKE '%' || :filter || '%'
|
||||
OR short_name LIKE '%' || :filter || '%'
|
||||
OR printf('!%08x', CASE WHEN num < 0 THEN num + 4294967296 ELSE num END) LIKE '%' || :filter || '%'
|
||||
OR CAST(CASE WHEN num < 0 THEN num + 4294967296 ELSE num END AS TEXT) LIKE '%' || :filter || '%'))
|
||||
AND (:lastHeardMin = -1 OR last_heard >= :lastHeardMin)
|
||||
AND (:hopsAwayMax = -1 OR (hops_away <= :hopsAwayMax AND hops_away >= 0) OR num = (SELECT myNodeNum FROM my_node LIMIT 1))
|
||||
ORDER BY CASE
|
||||
WHEN num = (SELECT myNodeNum FROM my_node LIMIT 1) THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
CASE
|
||||
WHEN :sort = 'last_heard' THEN last_heard * -1
|
||||
WHEN :sort = 'alpha' THEN UPPER(long_name)
|
||||
WHEN :sort = 'distance' THEN
|
||||
CASE
|
||||
WHEN latitude IS NULL OR longitude IS NULL OR
|
||||
(latitude = 0.0 AND longitude = 0.0) THEN 999999999
|
||||
ELSE
|
||||
(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
|
||||
CASE
|
||||
WHEN hops_away = -1 THEN 999999999
|
||||
ELSE hops_away
|
||||
END
|
||||
WHEN :sort = 'channel' THEN channel
|
||||
WHEN :sort = 'via_mqtt' THEN via_mqtt
|
||||
WHEN :sort = 'via_favorite' THEN is_favorite * -1
|
||||
ELSE 0
|
||||
END ASC,
|
||||
last_heard DESC
|
||||
""",
|
||||
)
|
||||
@Transaction
|
||||
fun getNodes(
|
||||
sort: String,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
hopsAwayMax: Int,
|
||||
lastHeardMin: Int,
|
||||
): Flow<List<NodeWithRelations>>
|
||||
|
||||
@Query("DELETE FROM nodes")
|
||||
fun clearNodeInfo()
|
||||
|
||||
@Query("DELETE FROM nodes WHERE num=:num")
|
||||
fun deleteNode(num: Int)
|
||||
|
||||
@Query("DELETE FROM nodes WHERE num IN (:nodeNums)")
|
||||
fun deleteNodes(nodeNums: List<Int>)
|
||||
|
||||
@Query("SELECT * FROM nodes WHERE last_heard < :lastHeard")
|
||||
fun getNodesOlderThan(lastHeard: Int): List<NodeEntity>
|
||||
|
||||
@Query("SELECT * FROM nodes WHERE short_name IS NULL")
|
||||
fun getUnknownNodes(): List<NodeEntity>
|
||||
|
||||
@Upsert fun upsert(meta: MetadataEntity)
|
||||
|
||||
@Query("DELETE FROM metadata WHERE num=:num")
|
||||
fun deleteMetadata(num: Int)
|
||||
|
||||
@Query("SELECT * FROM nodes WHERE num=:num")
|
||||
@Transaction
|
||||
fun getNodeByNum(num: Int): NodeWithRelations?
|
||||
|
||||
@Query("SELECT * FROM nodes WHERE public_key = :publicKey LIMIT 1")
|
||||
fun findNodeByPublicKey(publicKey: ByteString?): NodeEntity?
|
||||
|
||||
@Upsert fun doUpsert(node: NodeEntity)
|
||||
|
||||
fun upsert(node: NodeEntity) {
|
||||
val verifiedNode = getVerifiedNodeForUpsert(node)
|
||||
doUpsert(verifiedNode)
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun putAll(nodes: List<NodeEntity>)
|
||||
|
||||
@Query("UPDATE nodes SET notes = :notes WHERE num = :num")
|
||||
fun setNodeNotes(num: Int, notes: String)
|
||||
}
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.MapColumn
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import com.geeksville.mesh.database.entity.ContactSettings
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.PacketEntity
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
|
||||
@Dao
|
||||
interface PacketDao {
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = :portNum
|
||||
ORDER BY received_time ASC
|
||||
""",
|
||||
)
|
||||
fun getAllPackets(portNum: Int): Flow<List<Packet>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1
|
||||
ORDER BY received_time DESC
|
||||
""",
|
||||
)
|
||||
fun getContactKeys(): Flow<
|
||||
Map<
|
||||
@MapColumn(columnName = "contact_key")
|
||||
String,
|
||||
Packet,
|
||||
>,
|
||||
>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact
|
||||
""",
|
||||
)
|
||||
suspend fun getMessageCount(contact: String): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact AND read = 0
|
||||
""",
|
||||
)
|
||||
suspend fun getUnreadCount(contact: String): Int
|
||||
|
||||
@Query(
|
||||
"""
|
||||
UPDATE packet
|
||||
SET read = 1
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact AND read = 0 AND received_time <= :timestamp
|
||||
""",
|
||||
)
|
||||
suspend fun clearUnreadCount(contact: String, timestamp: Long)
|
||||
|
||||
@Upsert suspend fun insert(packet: Packet)
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 1 AND contact_key = :contact
|
||||
ORDER BY received_time DESC
|
||||
""",
|
||||
)
|
||||
fun getMessagesFrom(contact: String): Flow<List<PacketEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND data = :data
|
||||
""",
|
||||
)
|
||||
suspend fun findDataPacket(data: DataPacket): Packet?
|
||||
|
||||
@Query("DELETE FROM packet WHERE uuid in (:uuidList)")
|
||||
suspend fun deletePackets(uuidList: List<Long>)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND contact_key IN (:contactList)
|
||||
""",
|
||||
)
|
||||
suspend fun deleteContacts(contactList: List<String>)
|
||||
|
||||
@Query("DELETE FROM packet WHERE uuid=:uuid")
|
||||
suspend fun delete(uuid: Long)
|
||||
|
||||
@Transaction
|
||||
suspend fun delete(packet: Packet) {
|
||||
delete(packet.uuid)
|
||||
}
|
||||
|
||||
@Query("SELECT packet_id FROM packet WHERE uuid IN (:uuidList)")
|
||||
suspend fun getPacketIdsFrom(uuidList: List<Long>): List<Int>
|
||||
|
||||
@Query("DELETE FROM reactions WHERE reply_id IN (:packetIds)")
|
||||
suspend fun deleteReactions(packetIds: List<Int>)
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteMessages(uuidList: List<Long>) {
|
||||
val packetIds = getPacketIdsFrom(uuidList)
|
||||
if (packetIds.isNotEmpty()) {
|
||||
deleteReactions(packetIds)
|
||||
}
|
||||
deletePackets(uuidList)
|
||||
}
|
||||
|
||||
@Update suspend fun update(packet: Packet)
|
||||
|
||||
@Transaction
|
||||
suspend fun updateMessageStatus(data: DataPacket, m: MessageStatus) {
|
||||
val new = data.copy(status = m)
|
||||
findDataPacket(data)?.let { update(it.copy(data = new)) }
|
||||
}
|
||||
|
||||
@Transaction
|
||||
suspend fun updateMessageId(data: DataPacket, id: Int) {
|
||||
val new = data.copy(id = id)
|
||||
findDataPacket(data)?.let { update(it.copy(data = new)) }
|
||||
}
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT data FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
ORDER BY received_time ASC
|
||||
""",
|
||||
)
|
||||
suspend fun getDataPackets(): List<DataPacket>
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND packet_id = :requestId
|
||||
ORDER BY received_time DESC
|
||||
""",
|
||||
)
|
||||
suspend fun getPacketById(requestId: Int): Packet?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM packet WHERE packet_id = :packetId LIMIT 1")
|
||||
suspend fun getPacketByPacketId(packetId: Int): PacketEntity?
|
||||
|
||||
@Transaction
|
||||
suspend fun getQueuedPackets(): List<DataPacket>? = getDataPackets().filter { it.status == MessageStatus.QUEUED }
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM packet
|
||||
WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM my_node))
|
||||
AND port_num = 8
|
||||
ORDER BY received_time ASC
|
||||
""",
|
||||
)
|
||||
suspend fun getAllWaypoints(): List<Packet>
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteWaypoint(id: Int) {
|
||||
val uuidList = getAllWaypoints().filter { it.data.waypoint?.id == id }.map { it.uuid }
|
||||
deleteMessages(uuidList)
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM contact_settings")
|
||||
fun getContactSettings(): Flow<
|
||||
Map<
|
||||
@MapColumn(columnName = "contact_key")
|
||||
String,
|
||||
ContactSettings,
|
||||
>,
|
||||
>
|
||||
|
||||
@Query("SELECT * FROM contact_settings WHERE contact_key = :contact")
|
||||
suspend fun getContactSettings(contact: String): ContactSettings?
|
||||
|
||||
@Upsert suspend fun upsertContactSettings(contacts: List<ContactSettings>)
|
||||
|
||||
@Transaction
|
||||
suspend fun setMuteUntil(contacts: List<String>, until: Long) {
|
||||
val contactList =
|
||||
contacts.map { contact ->
|
||||
getContactSettings(contact)?.copy(muteUntil = until)
|
||||
?: ContactSettings(contact_key = contact, muteUntil = until)
|
||||
}
|
||||
upsertContactSettings(contactList)
|
||||
}
|
||||
|
||||
@Upsert suspend fun insert(reaction: ReactionEntity)
|
||||
|
||||
@Query("DELETE FROM packet")
|
||||
suspend fun deleteAll()
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* 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.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface QuickChatActionDao {
|
||||
|
||||
@Query("Select * from quick_chat order by position asc")
|
||||
fun getAll(): Flow<List<QuickChatAction>>
|
||||
|
||||
@Upsert
|
||||
fun upsert(action: QuickChatAction)
|
||||
|
||||
@Query("Delete from quick_chat")
|
||||
fun deleteAll()
|
||||
|
||||
@Query("Delete from quick_chat where uuid=:uuid")
|
||||
fun _delete(uuid: Long)
|
||||
|
||||
@Transaction
|
||||
fun delete(action: QuickChatAction) {
|
||||
_delete(action.uuid)
|
||||
decrementPositionsAfter(action.position)
|
||||
}
|
||||
|
||||
@Query("Update quick_chat set position=:position WHERE uuid=:uuid")
|
||||
fun updateActionPosition(uuid: Long, position: Int)
|
||||
|
||||
@Query("Update quick_chat set position=position-1 where position>=:position")
|
||||
fun decrementPositionsAfter(position: Int)
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "device_hardware")
|
||||
data class DeviceHardwareEntity(
|
||||
@ColumnInfo(name = "actively_supported") val activelySupported: Boolean,
|
||||
val architecture: String,
|
||||
@ColumnInfo(name = "display_name") val displayName: String,
|
||||
@ColumnInfo(name = "has_ink_hud") val hasInkHud: Boolean? = null,
|
||||
@ColumnInfo(name = "has_mui") val hasMui: Boolean? = null,
|
||||
@PrimaryKey val hwModel: Int,
|
||||
@ColumnInfo(name = "hw_model_slug") val hwModelSlug: String,
|
||||
val images: List<String>?,
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "partition_scheme") val partitionScheme: String? = null,
|
||||
@ColumnInfo(name = "platformio_target") val platformioTarget: String,
|
||||
@ColumnInfo(name = "requires_dfu") val requiresDfu: Boolean?,
|
||||
@ColumnInfo(name = "support_level") val supportLevel: Int?,
|
||||
val tags: List<String>?,
|
||||
)
|
||||
|
||||
fun NetworkDeviceHardware.asEntity() = DeviceHardwareEntity(
|
||||
activelySupported = activelySupported,
|
||||
architecture = architecture,
|
||||
displayName = displayName,
|
||||
hasInkHud = hasInkHud,
|
||||
hasMui = hasMui,
|
||||
hwModel = hwModel,
|
||||
hwModelSlug = hwModelSlug,
|
||||
images = images,
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
partitionScheme = partitionScheme,
|
||||
platformioTarget = platformioTarget,
|
||||
requiresDfu = requiresDfu,
|
||||
supportLevel = supportLevel,
|
||||
tags = tags,
|
||||
)
|
||||
|
||||
fun DeviceHardwareEntity.asExternalModel() = DeviceHardware(
|
||||
activelySupported = activelySupported,
|
||||
architecture = architecture,
|
||||
displayName = displayName,
|
||||
hasInkHud = hasInkHud,
|
||||
hasMui = hasMui,
|
||||
hwModel = hwModel,
|
||||
hwModelSlug = hwModelSlug,
|
||||
images = images,
|
||||
partitionScheme = partitionScheme,
|
||||
platformioTarget = platformioTarget,
|
||||
requiresDfu = requiresDfu,
|
||||
supportLevel = supportLevel,
|
||||
tags = tags,
|
||||
)
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.NetworkFirmwareRelease
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "firmware_release")
|
||||
data class FirmwareReleaseEntity(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: String = "",
|
||||
@ColumnInfo(name = "page_url") val pageUrl: String = "",
|
||||
@ColumnInfo(name = "release_notes") val releaseNotes: String = "",
|
||||
@ColumnInfo(name = "title") val title: String = "",
|
||||
@ColumnInfo(name = "zip_url") val zipUrl: String = "",
|
||||
@ColumnInfo(name = "last_updated") val lastUpdated: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "release_type") val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
|
||||
)
|
||||
|
||||
fun NetworkFirmwareRelease.asEntity(releaseType: FirmwareReleaseType) = FirmwareReleaseEntity(
|
||||
id = id,
|
||||
pageUrl = pageUrl,
|
||||
releaseNotes = releaseNotes,
|
||||
title = title,
|
||||
zipUrl = zipUrl,
|
||||
lastUpdated = System.currentTimeMillis(),
|
||||
releaseType = releaseType,
|
||||
)
|
||||
|
||||
fun FirmwareReleaseEntity.asExternalModel() = FirmwareRelease(
|
||||
id = id,
|
||||
pageUrl = pageUrl,
|
||||
releaseNotes = releaseNotes,
|
||||
title = title,
|
||||
zipUrl = zipUrl,
|
||||
lastUpdated = lastUpdated,
|
||||
releaseType = releaseType,
|
||||
)
|
||||
|
||||
data class FirmwareRelease(
|
||||
val id: String = "",
|
||||
val pageUrl: String = "",
|
||||
val releaseNotes: String = "",
|
||||
val title: String = "",
|
||||
val zipUrl: String = "",
|
||||
val lastUpdated: Long = System.currentTimeMillis(),
|
||||
val releaseType: FirmwareReleaseType = FirmwareReleaseType.STABLE,
|
||||
)
|
||||
|
||||
fun FirmwareReleaseEntity.asDeviceVersion(): DeviceVersion = DeviceVersion(id.substringBeforeLast(".").replace("v", ""))
|
||||
|
||||
fun FirmwareRelease.asDeviceVersion(): DeviceVersion = DeviceVersion(id.substringBeforeLast(".").replace("v", ""))
|
||||
|
||||
enum class FirmwareReleaseType {
|
||||
STABLE,
|
||||
ALPHA,
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MeshProtos.FromRadio
|
||||
import com.geeksville.mesh.Portnums
|
||||
import com.google.protobuf.TextFormat
|
||||
import java.io.IOException
|
||||
|
||||
@Entity(
|
||||
tableName = "log",
|
||||
indices = [
|
||||
Index(value = ["from_num"]),
|
||||
Index(value = ["port_num"]),
|
||||
],
|
||||
)
|
||||
data class MeshLog(
|
||||
@PrimaryKey val uuid: String,
|
||||
@ColumnInfo(name = "type") val message_type: String,
|
||||
@ColumnInfo(name = "received_date") val received_date: Long,
|
||||
@ColumnInfo(name = "message") val raw_message: String,
|
||||
@ColumnInfo(name = "from_num", defaultValue = "0") val fromNum: Int = 0,
|
||||
@ColumnInfo(name = "port_num", defaultValue = "0") val portNum: Int = 0,
|
||||
@ColumnInfo(name = "from_radio", typeAffinity = ColumnInfo.BLOB, defaultValue = "x''")
|
||||
val fromRadio: FromRadio = FromRadio.getDefaultInstance(),
|
||||
) {
|
||||
|
||||
val meshPacket: MeshProtos.MeshPacket?
|
||||
get() {
|
||||
if (message_type == "Packet") {
|
||||
val builder = MeshProtos.MeshPacket.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val nodeInfo: MeshProtos.NodeInfo?
|
||||
get() {
|
||||
if (message_type == "NodeInfo") {
|
||||
val builder = MeshProtos.NodeInfo.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val myNodeInfo: MeshProtos.MyNodeInfo?
|
||||
get() {
|
||||
if (message_type == "MyNodeInfo") {
|
||||
val builder = MeshProtos.MyNodeInfo.newBuilder()
|
||||
try {
|
||||
TextFormat.getParser().merge(raw_message, builder)
|
||||
return builder.build()
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val position: MeshProtos.Position?
|
||||
get() {
|
||||
return meshPacket?.run {
|
||||
if (hasDecoded() && decoded.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) {
|
||||
return MeshProtos.Position.parseFrom(decoded.payload)
|
||||
}
|
||||
return null
|
||||
} ?: nodeInfo?.position
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
|
||||
@Entity(tableName = "my_node")
|
||||
data class MyNodeEntity(
|
||||
@PrimaryKey(autoGenerate = false) val myNodeNum: Int,
|
||||
val model: String?,
|
||||
val firmwareVersion: String?,
|
||||
val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want
|
||||
val shouldUpdate: Boolean, // this device has old firmware
|
||||
val currentPacketId: Long,
|
||||
val messageTimeoutMsec: Int,
|
||||
val minAppVersion: Int,
|
||||
val maxChannels: Int,
|
||||
val hasWifi: Boolean,
|
||||
val deviceId: String? = "unknown",
|
||||
) {
|
||||
/** A human readable description of the software/hardware version */
|
||||
val firmwareString: String
|
||||
get() = "$model $firmwareVersion"
|
||||
|
||||
fun toMyNodeInfo() = MyNodeInfo(
|
||||
myNodeNum = myNodeNum,
|
||||
hasGPS = false,
|
||||
model = model,
|
||||
firmwareVersion = firmwareVersion,
|
||||
couldUpdate = couldUpdate,
|
||||
shouldUpdate = shouldUpdate,
|
||||
currentPacketId = currentPacketId,
|
||||
messageTimeoutMsec = messageTimeoutMsec,
|
||||
minAppVersion = minAppVersion,
|
||||
maxChannels = maxChannels,
|
||||
hasWifi = hasWifi,
|
||||
channelUtilization = 0f,
|
||||
airUtilTx = 0f,
|
||||
deviceId = deviceId,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.kotlin.isNotEmpty
|
||||
import org.meshtastic.core.model.DeviceMetrics
|
||||
import org.meshtastic.core.model.EnvironmentMetrics
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
|
||||
data class NodeWithRelations(
|
||||
@Embedded val node: NodeEntity,
|
||||
@Relation(entity = MetadataEntity::class, parentColumn = "num", entityColumn = "num")
|
||||
val metadata: MetadataEntity? = null,
|
||||
) {
|
||||
fun toModel() = with(node) {
|
||||
Node(
|
||||
num = num,
|
||||
metadata = metadata?.proto,
|
||||
user = user,
|
||||
position = position,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = deviceTelemetry.deviceMetrics,
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
environmentMetrics = environmentTelemetry.environmentMetrics,
|
||||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
paxcounter = paxcounter,
|
||||
notes = notes,
|
||||
)
|
||||
}
|
||||
|
||||
fun toEntity() = with(node) {
|
||||
NodeEntity(
|
||||
num = num,
|
||||
user = user,
|
||||
position = position,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceTelemetry = deviceTelemetry,
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
environmentTelemetry = environmentTelemetry,
|
||||
powerTelemetry = powerTelemetry,
|
||||
paxcounter = paxcounter,
|
||||
notes = notes,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(tableName = "metadata", indices = [Index(value = ["num"])])
|
||||
data class MetadataEntity(
|
||||
@PrimaryKey val num: Int,
|
||||
@ColumnInfo(name = "proto", typeAffinity = ColumnInfo.BLOB) val proto: MeshProtos.DeviceMetadata,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
@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 = -1,
|
||||
@ColumnInfo(name = "is_favorite") var isFavorite: Boolean = false,
|
||||
@ColumnInfo(name = "is_ignored", defaultValue = "0") var isIgnored: Boolean = false,
|
||||
@ColumnInfo(name = "environment_metrics", typeAffinity = ColumnInfo.BLOB)
|
||||
var environmentTelemetry: TelemetryProtos.Telemetry = TelemetryProtos.Telemetry.newBuilder().build(),
|
||||
@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(),
|
||||
@ColumnInfo(name = "public_key") var publicKey: ByteString? = null,
|
||||
@ColumnInfo(name = "notes", defaultValue = "") var notes: String = "",
|
||||
) {
|
||||
val deviceMetrics: TelemetryProtos.DeviceMetrics
|
||||
get() = deviceTelemetry.deviceMetrics
|
||||
|
||||
val environmentMetrics: TelemetryProtos.EnvironmentMetrics
|
||||
get() = environmentTelemetry.environmentMetrics
|
||||
|
||||
val isUnknownUser
|
||||
get() = user.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
|
||||
val hasPKC
|
||||
get() = (publicKey ?: user.publicKey).isNotEmpty()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/** true if the device was heard from recently */
|
||||
val isOnline: Boolean
|
||||
get() {
|
||||
return lastHeard > onlineTimeThreshold()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Convert to a double representation of degrees
|
||||
fun degD(i: Int) = i * 1e-7
|
||||
|
||||
fun degI(d: Double) = (d * 1e7).toInt()
|
||||
|
||||
val ERROR_BYTE_STRING: ByteString = ByteString.copyFrom(ByteArray(32) { 0 })
|
||||
|
||||
fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
|
||||
}
|
||||
|
||||
fun toModel() = Node(
|
||||
num = num,
|
||||
user = user,
|
||||
position = position,
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
lastHeard = lastHeard,
|
||||
deviceMetrics = deviceTelemetry.deviceMetrics,
|
||||
channel = channel,
|
||||
viaMqtt = viaMqtt,
|
||||
hopsAway = hopsAway,
|
||||
isFavorite = isFavorite,
|
||||
isIgnored = isIgnored,
|
||||
environmentMetrics = environmentTelemetry.environmentMetrics,
|
||||
powerMetrics = powerTelemetry.powerMetrics,
|
||||
paxcounter = paxcounter,
|
||||
publicKey = publicKey ?: user.publicKey,
|
||||
notes = notes,
|
||||
)
|
||||
|
||||
fun 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.fromTelemetryProto(
|
||||
environmentTelemetry.environmentMetrics,
|
||||
environmentTelemetry.time,
|
||||
),
|
||||
hopsAway = hopsAway,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Relation
|
||||
import com.geeksville.mesh.MeshProtos.User
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.util.getShortDateTime
|
||||
|
||||
data class PacketEntity(
|
||||
@Embedded val packet: Packet,
|
||||
@Relation(entity = ReactionEntity::class, parentColumn = "packet_id", entityColumn = "reply_id")
|
||||
val reactions: List<ReactionEntity> = emptyList(),
|
||||
) {
|
||||
suspend fun toMessage(getNode: suspend (userId: String?) -> Node) = with(packet) {
|
||||
val node = getNode(data.from)
|
||||
Message(
|
||||
uuid = uuid,
|
||||
receivedTime = received_time,
|
||||
node = node,
|
||||
fromLocal = node.user.id == DataPacket.ID_LOCAL,
|
||||
text = data.text.orEmpty(),
|
||||
time = getShortDateTime(data.time),
|
||||
snr = snr,
|
||||
rssi = rssi,
|
||||
hopsAway = hopsAway,
|
||||
read = read,
|
||||
status = data.status,
|
||||
routingError = routingError,
|
||||
packetId = packetId,
|
||||
emojis = reactions.toReaction(getNode),
|
||||
replyId = data.replyId,
|
||||
viaMqtt = node.viaMqtt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
tableName = "packet",
|
||||
indices = [Index(value = ["myNodeNum"]), Index(value = ["port_num"]), Index(value = ["contact_key"])],
|
||||
)
|
||||
data class Packet(
|
||||
@PrimaryKey(autoGenerate = true) val uuid: Long,
|
||||
@ColumnInfo(name = "myNodeNum", defaultValue = "0") val myNodeNum: Int,
|
||||
@ColumnInfo(name = "port_num") val port_num: Int,
|
||||
@ColumnInfo(name = "contact_key") val contact_key: String,
|
||||
@ColumnInfo(name = "received_time") val received_time: Long,
|
||||
@ColumnInfo(name = "read", defaultValue = "1") val read: Boolean,
|
||||
@ColumnInfo(name = "data") val data: DataPacket,
|
||||
@ColumnInfo(name = "packet_id", defaultValue = "0") val packetId: Int = 0,
|
||||
@ColumnInfo(name = "routing_error", defaultValue = "-1") var routingError: Int = -1,
|
||||
@ColumnInfo(name = "reply_id", defaultValue = "0") val replyId: Int = 0,
|
||||
@ColumnInfo(name = "snr", defaultValue = "0") val snr: Float = 0f,
|
||||
@ColumnInfo(name = "rssi", defaultValue = "0") val rssi: Int = 0,
|
||||
@ColumnInfo(name = "hopsAway", defaultValue = "-1") val hopsAway: Int = -1,
|
||||
)
|
||||
|
||||
@Entity(tableName = "contact_settings")
|
||||
data class ContactSettings(@PrimaryKey val contact_key: String, val muteUntil: Long = 0L) {
|
||||
val isMuted
|
||||
get() = System.currentTimeMillis() <= muteUntil
|
||||
}
|
||||
|
||||
data class Reaction(val replyId: Int, val user: User, val emoji: String, val timestamp: Long)
|
||||
|
||||
@Entity(
|
||||
tableName = "reactions",
|
||||
primaryKeys = ["reply_id", "user_id", "emoji"],
|
||||
indices = [Index(value = ["reply_id"])],
|
||||
)
|
||||
data class ReactionEntity(
|
||||
@ColumnInfo(name = "reply_id") val replyId: Int,
|
||||
@ColumnInfo(name = "user_id") val userId: String,
|
||||
val emoji: String,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
||||
private suspend fun ReactionEntity.toReaction(getNode: suspend (userId: String?) -> Node) =
|
||||
Reaction(replyId = replyId, user = getNode(userId).user, emoji = emoji, timestamp = timestamp)
|
||||
|
||||
private suspend fun List<ReactionEntity>.toReaction(getNode: suspend (userId: String?) -> Node) =
|
||||
this.map { it.toReaction(getNode) }
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* 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.database.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "quick_chat")
|
||||
data class QuickChatAction(
|
||||
@PrimaryKey(autoGenerate = true) val uuid: Long = 0L,
|
||||
@ColumnInfo(name = "name") val name: String = "",
|
||||
@ColumnInfo(name = "message") val message: String = "",
|
||||
@ColumnInfo(name = "mode") val mode: Mode = Mode.Instant,
|
||||
@ColumnInfo(name = "position") val position: Int
|
||||
) {
|
||||
enum class Mode {
|
||||
Append,
|
||||
Instant,
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,6 @@ import com.geeksville.mesh.StoreAndForwardProtos
|
|||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.ui.debug.FilterMode
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
|
|
@ -45,6 +44,7 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import java.text.DateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* 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 androidx.annotation.StringRes
|
||||
import com.geeksville.mesh.MeshProtos.Routing
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@StringRes
|
||||
fun getStringResFrom(routingError: Int): Int = when (routingError) {
|
||||
Routing.Error.NONE_VALUE -> R.string.routing_error_none
|
||||
Routing.Error.NO_ROUTE_VALUE -> R.string.routing_error_no_route
|
||||
Routing.Error.GOT_NAK_VALUE -> R.string.routing_error_got_nak
|
||||
Routing.Error.TIMEOUT_VALUE -> R.string.routing_error_timeout
|
||||
Routing.Error.NO_INTERFACE_VALUE -> R.string.routing_error_no_interface
|
||||
Routing.Error.MAX_RETRANSMIT_VALUE -> R.string.routing_error_max_retransmit
|
||||
Routing.Error.NO_CHANNEL_VALUE -> R.string.routing_error_no_channel
|
||||
Routing.Error.TOO_LARGE_VALUE -> R.string.routing_error_too_large
|
||||
Routing.Error.NO_RESPONSE_VALUE -> R.string.routing_error_no_response
|
||||
Routing.Error.DUTY_CYCLE_LIMIT_VALUE -> R.string.routing_error_duty_cycle_limit
|
||||
Routing.Error.BAD_REQUEST_VALUE -> R.string.routing_error_bad_request
|
||||
Routing.Error.NOT_AUTHORIZED_VALUE -> R.string.routing_error_not_authorized
|
||||
Routing.Error.PKI_FAILED_VALUE -> R.string.routing_error_pki_failed
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY_VALUE -> R.string.routing_error_pki_unknown_pubkey
|
||||
Routing.Error.ADMIN_BAD_SESSION_KEY_VALUE -> R.string.routing_error_admin_bad_session_key
|
||||
Routing.Error.ADMIN_PUBLIC_KEY_UNAUTHORIZED_VALUE -> R.string.routing_error_admin_public_key_unauthorized
|
||||
Routing.Error.RATE_LIMIT_EXCEEDED_VALUE -> R.string.routing_error_rate_limit_exceeded
|
||||
else -> R.string.unrecognized
|
||||
}
|
||||
|
||||
data class Message(
|
||||
val uuid: Long,
|
||||
val receivedTime: Long,
|
||||
val node: Node,
|
||||
val text: String,
|
||||
val fromLocal: Boolean,
|
||||
val time: String,
|
||||
val read: Boolean,
|
||||
val status: MessageStatus?,
|
||||
val routingError: Int,
|
||||
val packetId: Int,
|
||||
val emojis: List<Reaction>,
|
||||
val snr: Float,
|
||||
val rssi: Int,
|
||||
val hopsAway: Int,
|
||||
val replyId: Int?,
|
||||
val originalMessage: Message? = null,
|
||||
val viaMqtt: Boolean = false,
|
||||
) {
|
||||
fun getStatusStringRes(): Pair<Int, Int> {
|
||||
val title = if (routingError > 0) R.string.error else R.string.message_delivery_status
|
||||
val text =
|
||||
when (status) {
|
||||
MessageStatus.RECEIVED -> R.string.delivery_confirmed
|
||||
MessageStatus.QUEUED -> R.string.message_status_queued
|
||||
MessageStatus.ENROUTE -> R.string.message_status_enroute
|
||||
else -> getStringResFrom(routingError)
|
||||
}
|
||||
return title to text
|
||||
}
|
||||
}
|
||||
|
|
@ -36,8 +36,6 @@ import com.geeksville.mesh.Portnums.PortNum
|
|||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
|
||||
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
|
|
@ -58,6 +56,9 @@ import kotlinx.coroutines.flow.toList
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
* 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,
|
||||
)
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* 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 androidx.annotation.StringRes
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
enum class NodeSortOption(val sqlValue: String, @StringRes val stringRes: Int) {
|
||||
LAST_HEARD("last_heard", R.string.node_sort_last_heard),
|
||||
ALPHABETICAL("alpha", R.string.node_sort_alpha),
|
||||
DISTANCE("distance", R.string.node_sort_distance),
|
||||
HOPS_AWAY("hops_away", R.string.node_sort_hops_away),
|
||||
CHANNEL("channel", R.string.node_sort_channel),
|
||||
VIA_MQTT("via_mqtt", R.string.node_sort_via_mqtt),
|
||||
VIA_FAVORITE("via_favorite", R.string.node_sort_via_favorite),
|
||||
}
|
||||
|
|
@ -46,10 +46,6 @@ import com.geeksville.mesh.database.MeshLogRepository
|
|||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.QuickChatActionRepository
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.database.entity.asDeviceVersion
|
||||
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
|
||||
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
|
|
@ -78,6 +74,13 @@ import kotlinx.coroutines.flow.shareIn
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
|
||||
package com.geeksville.mesh.repository.api
|
||||
|
||||
import com.geeksville.mesh.database.dao.DeviceHardwareDao
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.asEntity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.DeviceHardwareDao
|
||||
import org.meshtastic.core.database.entity.DeviceHardwareEntity
|
||||
import org.meshtastic.core.database.entity.asEntity
|
||||
import org.meshtastic.core.model.NetworkDeviceHardware
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ package com.geeksville.mesh.repository.api
|
|||
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
|
||||
import com.geeksville.mesh.database.entity.asExternalModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.entity.DeviceHardwareEntity
|
||||
import org.meshtastic.core.database.entity.asExternalModel
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@
|
|||
|
||||
package com.geeksville.mesh.repository.api
|
||||
|
||||
import com.geeksville.mesh.database.dao.FirmwareReleaseDao
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
import com.geeksville.mesh.database.entity.asDeviceVersion
|
||||
import com.geeksville.mesh.database.entity.asEntity
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.dao.FirmwareReleaseDao
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseType
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.database.entity.asEntity
|
||||
import org.meshtastic.core.model.NetworkFirmwareRelease
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ package com.geeksville.mesh.repository.api
|
|||
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseEntity
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
import com.geeksville.mesh.database.entity.asExternalModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
|
||||
import org.meshtastic.core.database.entity.FirmwareReleaseType
|
||||
import org.meshtastic.core.database.entity.asExternalModel
|
||||
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
|
|
|||
|
|
@ -30,11 +30,7 @@ import com.geeksville.mesh.MeshProtos.DeviceMetadata
|
|||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.entity.MetadataEntity
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.getChannelUrl
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
|
|
@ -45,6 +41,10 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.datastore.ChannelSetDataSource
|
||||
import org.meshtastic.core.datastore.LocalConfigDataSource
|
||||
import org.meshtastic.core.datastore.ModuleConfigDataSource
|
||||
|
|
|
|||
|
|
@ -56,14 +56,8 @@ import com.geeksville.mesh.concurrent.handledLaunch
|
|||
import com.geeksville.mesh.copy
|
||||
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.MyNodeEntity
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.ReactionEntity
|
||||
import com.geeksville.mesh.fromRadio
|
||||
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.position
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.repository.location.LocationRepository
|
||||
|
|
@ -90,6 +84,12 @@ import kotlinx.coroutines.flow.flatMapLatest
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.model.MeshUser
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ import com.geeksville.mesh.MainActivity
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R.raw
|
||||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.service.ReplyReceiver.Companion.KEY_TEXT_REPLY
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.strings.R
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import com.geeksville.mesh.android.BuildUtils.info
|
|||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.fromRadio
|
||||
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
||||
import dagger.Lazy
|
||||
|
|
@ -36,6 +35,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.toOneLineString
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ import com.geeksville.mesh.android.AddNavigationTracking
|
|||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.setAttributes
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.navigation.channelsGraph
|
||||
import com.geeksville.mesh.navigation.connectionsGraph
|
||||
|
|
@ -111,6 +110,7 @@ import com.google.accompanist.permissions.isGranted
|
|||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.navigation.ConnectionsRoutes
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import androidx.navigation.NavDestination
|
|||
import androidx.navigation.NavDestination.Companion.hasRoute
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.navigation.isConfigRoute
|
||||
import com.geeksville.mesh.navigation.isNodeDetailRoute
|
||||
|
|
@ -55,6 +54,7 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
|
|||
import com.geeksville.mesh.ui.debug.DebugMenuActions
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.util.DistanceUnit
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import org.meshtastic.core.model.util.DistanceUnit
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.strings.R
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.model.Node
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
||||
/** Simple [PreviewParameterProvider] that provides true and false values. */
|
||||
class BooleanProvider : PreviewParameterProvider<Boolean> {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
const val MAX_VALID_SNR = 100F
|
||||
|
|
|
|||
|
|
@ -22,11 +22,11 @@ import com.geeksville.mesh.ConfigProtos
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.deviceMetrics
|
||||
import com.geeksville.mesh.environmentMetrics
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.paxcount
|
||||
import com.geeksville.mesh.position
|
||||
import com.geeksville.mesh.user
|
||||
import com.google.protobuf.ByteString
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime
|
||||
import kotlin.random.Random
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.DeviceListEntry
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.navigation.ConfigRoute
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
|
|
@ -80,6 +79,7 @@ import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialo
|
|||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import kotlinx.coroutines.delay
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.strings.R
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -31,6 +29,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
|
|||
|
|
@ -43,12 +43,12 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.common.components.MaterialBatteryInfo
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.PacketRepository
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
|
@ -31,6 +29,8 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.meshtastic.core.database.entity.Packet
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
|
|
|||
|
|
@ -95,9 +95,6 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.AppOnlyProtos
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.model.getChannel
|
||||
import com.geeksville.mesh.ui.common.components.SecurityIcon
|
||||
|
|
@ -107,6 +104,9 @@ import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
|||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
|
|
|||
|
|
@ -47,8 +47,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.message.components.MessageItem
|
||||
import com.geeksville.mesh.ui.message.components.ReactionDialog
|
||||
|
|
@ -57,6 +55,8 @@ import kotlinx.coroutines.FlowPreview
|
|||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.entity.Reaction
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.common.components.dragContainer
|
||||
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
|
||||
import com.geeksville.mesh.ui.common.components.rememberDragDropState
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.ui.common.components.MDText
|
||||
import com.geeksville.mesh.ui.common.components.Rssi
|
||||
import com.geeksville.mesh.ui.common.components.Snr
|
||||
|
|
@ -60,6 +57,9 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
|
|||
import com.geeksville.mesh.ui.common.theme.MessageItemColors
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||
import org.meshtastic.core.database.entity.Reaction
|
||||
import org.meshtastic.core.database.model.Message
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
|
|
|
|||
|
|
@ -52,44 +52,25 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.ui.common.components.BottomSheetDialog
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import org.meshtastic.core.database.entity.Reaction
|
||||
|
||||
@Composable
|
||||
private fun ReactionItem(
|
||||
emoji: String,
|
||||
emojiCount: Int = 1,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
) {
|
||||
private fun ReactionItem(emoji: String, emojiCount: Int = 1, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (emojiCount > 1) {
|
||||
Badge {
|
||||
Text(
|
||||
fontWeight = FontWeight.Bold,
|
||||
text = emojiCount.toString()
|
||||
)
|
||||
}
|
||||
Badge { Text(fontWeight = FontWeight.Bold, text = emojiCount.toString()) }
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
),
|
||||
modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Text(
|
||||
text = emoji,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape),
|
||||
)
|
||||
Text(text = emoji, modifier = Modifier.padding(4.dp).clip(CircleShape))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,10 +83,7 @@ fun ReactionRow(
|
|||
onSendReaction: (String) -> Unit = {},
|
||||
onShowReactions: () -> Unit = {},
|
||||
) {
|
||||
val emojiList =
|
||||
reduceEmojis(
|
||||
reactions.reversed().map { it.emoji }
|
||||
).entries
|
||||
val emojiList = reduceEmojis(reactions.reversed().map { it.emoji }).entries
|
||||
|
||||
AnimatedVisibility(emojiList.isNotEmpty()) {
|
||||
LazyRow(
|
||||
|
|
@ -113,16 +91,12 @@ fun ReactionRow(
|
|||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
items(
|
||||
emojiList.size
|
||||
) { index ->
|
||||
items(emojiList.size) { index ->
|
||||
val entry = emojiList.elementAt(index)
|
||||
ReactionItem(
|
||||
emoji = entry.key,
|
||||
emojiCount = entry.value,
|
||||
onClick = {
|
||||
onSendReaction(entry.key)
|
||||
},
|
||||
onClick = { onSendReaction(entry.key) },
|
||||
onLongClick = onShowReactions,
|
||||
)
|
||||
}
|
||||
|
|
@ -133,68 +107,47 @@ fun ReactionRow(
|
|||
fun reduceEmojis(emojis: List<String>): Map<String, Int> = emojis.groupingBy { it }.eachCount()
|
||||
|
||||
@Composable
|
||||
fun ReactionDialog(
|
||||
reactions: List<Reaction>,
|
||||
onDismiss: () -> Unit = {}
|
||||
) = BottomSheetDialog(
|
||||
onDismiss = onDismiss,
|
||||
modifier = Modifier.fillMaxHeight(fraction = .3f),
|
||||
) {
|
||||
val groupedEmojis = reactions.groupBy { it.emoji }
|
||||
var selectedEmoji by remember { mutableStateOf<String?>(null) }
|
||||
val filteredReactions = selectedEmoji?.let { groupedEmojis[it] ?: emptyList() } ?: reactions
|
||||
fun ReactionDialog(reactions: List<Reaction>, onDismiss: () -> Unit = {}) =
|
||||
BottomSheetDialog(onDismiss = onDismiss, modifier = Modifier.fillMaxHeight(fraction = .3f)) {
|
||||
val groupedEmojis = reactions.groupBy { it.emoji }
|
||||
var selectedEmoji by remember { mutableStateOf<String?>(null) }
|
||||
val filteredReactions = selectedEmoji?.let { groupedEmojis[it] ?: emptyList() } ?: reactions
|
||||
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(groupedEmojis.entries.toList()) { (emoji, reactions) ->
|
||||
Text(
|
||||
text = "$emoji${reactions.size}",
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(if (selectedEmoji == emoji) Color.Gray else Color.Transparent)
|
||||
.padding(8.dp)
|
||||
.clickable {
|
||||
selectedEmoji = if (selectedEmoji == emoji) null else emoji
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(filteredReactions) { reaction ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||
items(groupedEmojis.entries.toList()) { (emoji, reactions) ->
|
||||
Text(
|
||||
text = reaction.user.longName,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = reaction.emoji,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
text = "$emoji${reactions.size}",
|
||||
modifier =
|
||||
Modifier.clip(CircleShape)
|
||||
.background(if (selectedEmoji == emoji) Color.Gray else Color.Transparent)
|
||||
.padding(8.dp)
|
||||
.clickable { selectedEmoji = if (selectedEmoji == emoji) null else emoji },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
items(filteredReactions) { reaction ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = reaction.user.longName, style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = reaction.emoji, style = MaterialTheme.typography.titleLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun ReactionItemPreview() {
|
||||
AppTheme {
|
||||
Column(
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background)
|
||||
) {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
ReactionItem(emoji = "\uD83D\uDE42")
|
||||
ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2)
|
||||
ReactionButton()
|
||||
|
|
@ -207,20 +160,21 @@ fun ReactionItemPreview() {
|
|||
fun ReactionRowPreview() {
|
||||
AppTheme {
|
||||
ReactionRow(
|
||||
reactions = listOf(
|
||||
reactions =
|
||||
listOf(
|
||||
Reaction(
|
||||
replyId = 1,
|
||||
user = MeshProtos.User.getDefaultInstance(),
|
||||
emoji = "\uD83D\uDE42",
|
||||
timestamp = 1L
|
||||
timestamp = 1L,
|
||||
),
|
||||
Reaction(
|
||||
replyId = 1,
|
||||
user = MeshProtos.User.getDefaultInstance(),
|
||||
emoji = "\uD83D\uDE42",
|
||||
timestamp = 1L
|
||||
timestamp = 1L,
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.Portnums.PortNum
|
||||
import com.geeksville.mesh.database.entity.MeshLog
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.TimeFrame
|
||||
import com.geeksville.mesh.ui.common.components.OptionLabel
|
||||
import com.geeksville.mesh.ui.common.components.SlidingSelector
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.text.DateFormat
|
||||
|
|
|
|||
|
|
@ -65,8 +65,8 @@ import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.util.metersIn
|
||||
import com.geeksville.mesh.util.toString
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.strings.R
|
||||
import java.text.DateFormat
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
|
|
|||
|
|
@ -123,13 +123,9 @@ import coil3.compose.AsyncImage
|
|||
import coil3.request.ImageRequest
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.asDeviceVersion
|
||||
import com.geeksville.mesh.model.MetricsState
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.service.ServiceAction
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.common.components.TitledCard
|
||||
|
|
@ -147,10 +143,11 @@ import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
|
|||
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.geeksville.mesh.util.thenIf
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import com.geeksville.mesh.util.toSmallDistanceString
|
||||
import com.geeksville.mesh.util.toSpeedString
|
||||
import com.mikepenz.markdown.m3.Markdown
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
|
|
@ -158,6 +155,9 @@ import org.meshtastic.core.model.util.UnitConversions
|
|||
import org.meshtastic.core.model.util.UnitConversions.toTempString
|
||||
import org.meshtastic.core.model.util.formatAgo
|
||||
import org.meshtastic.core.model.util.formatUptime
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.model.util.toSmallDistanceString
|
||||
import org.meshtastic.core.model.util.toSpeedString
|
||||
import org.meshtastic.core.navigation.NodeDetailRoutes
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.ConnectionState
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
|
|
@ -57,6 +56,7 @@ import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
|||
import com.geeksville.mesh.ui.sharing.AddContactFAB
|
||||
import com.geeksville.mesh.ui.sharing.SharedContactDialog
|
||||
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
|
|
|
|||
|
|
@ -25,40 +25,21 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import com.geeksville.mesh.util.metersIn
|
||||
import com.geeksville.mesh.util.toString
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
|
||||
@Composable
|
||||
fun ElevationInfo(
|
||||
modifier: Modifier = Modifier,
|
||||
altitude: Int,
|
||||
system: DisplayUnits,
|
||||
suffix: String
|
||||
) {
|
||||
fun ElevationInfo(modifier: Modifier = Modifier, altitude: Int, system: DisplayUnits, suffix: String) {
|
||||
val annotatedString = buildAnnotatedString {
|
||||
append(altitude.metersIn(system).toString(system))
|
||||
MaterialTheme.typography.labelSmall.toSpanStyle().let { style ->
|
||||
withStyle(style) {
|
||||
append(" $suffix")
|
||||
}
|
||||
}
|
||||
MaterialTheme.typography.labelSmall.toSpanStyle().let { style -> withStyle(style) { append(" $suffix") } }
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
text = annotatedString,
|
||||
)
|
||||
Text(modifier = modifier, fontSize = MaterialTheme.typography.labelLarge.fontSize, text = annotatedString)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun ElevationInfoPreview() {
|
||||
MaterialTheme {
|
||||
ElevationInfo(
|
||||
altitude = 100,
|
||||
system = DisplayUnits.METRIC,
|
||||
suffix = "ASL"
|
||||
)
|
||||
}
|
||||
MaterialTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.PaxcountProtos
|
||||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.model.Node
|
||||
import org.meshtastic.core.database.model.Node
|
||||
|
||||
@Composable
|
||||
fun NodeChip(
|
||||
|
|
|
|||
|
|
@ -56,9 +56,9 @@ import androidx.compose.ui.text.font.FontWeight
|
|||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.model.NodeSortOption
|
||||
import com.geeksville.mesh.ui.common.preview.LargeFontPreview
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import org.meshtastic.core.database.model.NodeSortOption
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
|||
|
|
@ -51,13 +51,13 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.ui.common.components.BatteryInfo
|
||||
import com.geeksville.mesh.ui.common.components.SignalInfo
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ import com.geeksville.mesh.Portnums
|
|||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.database.MeshLogRepository
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -45,6 +43,8 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.util.positionToMeter
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.ui.node.components.NodeChip
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ package com.geeksville.mesh.ui.settings.radio
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.database.NodeRepository
|
||||
import com.geeksville.mesh.database.entity.NodeEntity
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
|
|
|||
|
|
@ -45,11 +45,8 @@ import com.geeksville.mesh.android.GeeksvilleApplication
|
|||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.android.isAnalyticsAvailable
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.database.entity.MyNodeEntity
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.getChannelList
|
||||
import com.geeksville.mesh.model.getStringResFrom
|
||||
import com.geeksville.mesh.model.toChannelSet
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.navigation.ConfigRoute
|
||||
|
|
@ -73,6 +70,9 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.database.model.getStringResFrom
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ import androidx.compose.ui.unit.dp
|
|||
import com.geeksville.mesh.ui.common.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.common.components.SwitchPreference
|
||||
import com.geeksville.mesh.ui.common.components.precisionBitsToMeters
|
||||
import com.geeksville.mesh.util.DistanceUnit
|
||||
import com.geeksville.mesh.util.toDistanceString
|
||||
import org.meshtastic.core.model.util.DistanceUnit
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.core.strings.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.model.isUnmessageableRole
|
||||
import com.geeksville.mesh.ui.common.components.EditTextPreference
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceCategory
|
||||
import com.geeksville.mesh.ui.common.components.RegularPreference
|
||||
import com.geeksville.mesh.ui.common.components.SwitchPreference
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.core.database.model.isUnmessageableRole
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import com.geeksville.mesh.AdminProtos
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.errormsg
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.ui.common.components.CopyIconButton
|
||||
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
|
||||
|
|
@ -69,6 +68,7 @@ import com.google.zxing.WriterException
|
|||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceVersion
|
||||
import org.meshtastic.core.strings.R
|
||||
import timber.log.Timber
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* 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.util
|
||||
|
||||
import android.icu.util.LocaleData
|
||||
import android.icu.util.ULocale
|
||||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import java.util.Locale
|
||||
|
||||
enum class DistanceUnit(
|
||||
val symbol: String,
|
||||
val multiplier: Float,
|
||||
val system: Int
|
||||
) {
|
||||
METER("m", multiplier = 1F, DisplayUnits.METRIC_VALUE),
|
||||
KILOMETER("km", multiplier = 0.001F, DisplayUnits.METRIC_VALUE),
|
||||
FOOT("ft", multiplier = 3.28084F, DisplayUnits.IMPERIAL_VALUE),
|
||||
MILE("mi", multiplier = 0.000621371F, DisplayUnits.IMPERIAL_VALUE),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun getFromLocale(locale: Locale = Locale.getDefault()): DisplayUnits {
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
when (LocaleData.getMeasurementSystem(ULocale.forLocale(locale))) {
|
||||
LocaleData.MeasurementSystem.SI -> DisplayUnits.METRIC
|
||||
else -> DisplayUnits.IMPERIAL
|
||||
}
|
||||
} else {
|
||||
when (locale.country.uppercase(locale)) {
|
||||
"US", "LR", "MM", "GB" -> DisplayUnits.IMPERIAL
|
||||
else -> DisplayUnits.METRIC
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Int.metersIn(unit: DistanceUnit): Float {
|
||||
return this * unit.multiplier
|
||||
}
|
||||
|
||||
fun Int.metersIn(system: DisplayUnits): Float {
|
||||
val unit = when (system.number) {
|
||||
DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT
|
||||
else -> DistanceUnit.METER
|
||||
}
|
||||
return this.metersIn(unit)
|
||||
}
|
||||
|
||||
fun Float.toString(unit: DistanceUnit): String {
|
||||
return if (unit in setOf(DistanceUnit.METER, DistanceUnit.FOOT)) {
|
||||
"%.0f %s"
|
||||
} else {
|
||||
"%.1f %s"
|
||||
}.format(this, unit.symbol)
|
||||
}
|
||||
|
||||
fun Float.toString(system: DisplayUnits): String {
|
||||
val unit = when (system.number) {
|
||||
DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FOOT
|
||||
else -> DistanceUnit.METER
|
||||
}
|
||||
return this.toString(unit)
|
||||
}
|
||||
|
||||
private const val KILOMETER_THRESHOLD = 1000
|
||||
private const val MILE_THRESHOLD = 1609
|
||||
fun Int.toDistanceString(system: DisplayUnits): String {
|
||||
val unit = if (system.number == DisplayUnits.METRIC_VALUE) {
|
||||
if (this < KILOMETER_THRESHOLD) DistanceUnit.METER else DistanceUnit.KILOMETER
|
||||
} else {
|
||||
if (this < MILE_THRESHOLD) DistanceUnit.FOOT else DistanceUnit.MILE
|
||||
}
|
||||
val valueInUnit = this * unit.multiplier
|
||||
return valueInUnit.toString(unit)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun Float.toSpeedString(system: DisplayUnits): String =
|
||||
if (system == DisplayUnits.METRIC) {
|
||||
"%.0f km/h".format(this * 3.6)
|
||||
} else {
|
||||
"%.0f mph".format(this * 2.23694f)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun Float.toSmallDistanceString(system: DisplayUnits): String {
|
||||
return if (system == DisplayUnits.IMPERIAL) {
|
||||
"%.2f in".format(this / 25.4f)
|
||||
} else {
|
||||
"%.0f mm".format(this)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue