Move some repo classes to :core:data (#3214)

This commit is contained in:
Phil Oliver 2025-09-26 17:45:11 -04:00 committed by GitHub
parent af8e1daa5d
commit 3e83e61a1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 159 additions and 140 deletions

View file

@ -1,146 +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 com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.TelemetryProtos.Telemetry
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")
class MeshLogRepository
@Inject
constructor(
private val meshLogDaoLazy: dagger.Lazy<MeshLogDao>,
private val dispatchers: CoroutineDispatchers,
) {
private val meshLogDao by lazy { meshLogDaoLazy.get() }
fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
meshLogDao.getAllLogs(maxItems).flowOn(dispatchers.io).conflate()
fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
meshLogDao.getAllLogsInReceiveOrder(maxItems).flowOn(dispatchers.io).conflate()
private fun parseTelemetryLog(log: MeshLog): Telemetry? = runCatching {
Telemetry.parseFrom(log.fromRadio.packet.decoded.payload)
.toBuilder()
.apply {
if (hasEnvironmentMetrics()) {
// Handle float metrics that default to 0.0f when not explicitly set or when 0.0f means no
// data
if (!environmentMetrics.hasTemperature()) {
environmentMetrics = environmentMetrics.toBuilder().setTemperature(Float.NaN).build()
}
if (!environmentMetrics.hasRelativeHumidity()) {
environmentMetrics =
environmentMetrics.toBuilder().setRelativeHumidity(Float.NaN).build()
}
if (!environmentMetrics.hasSoilTemperature()) {
environmentMetrics =
environmentMetrics.toBuilder().setSoilTemperature(Float.NaN).build()
}
if (!environmentMetrics.hasBarometricPressure()) {
environmentMetrics =
environmentMetrics.toBuilder().setBarometricPressure(Float.NaN).build()
}
if (!environmentMetrics.hasGasResistance()) {
environmentMetrics = environmentMetrics.toBuilder().setGasResistance(Float.NaN).build()
}
if (!environmentMetrics.hasVoltage()) {
environmentMetrics = environmentMetrics.toBuilder().setVoltage(Float.NaN).build()
}
if (!environmentMetrics.hasCurrent()) {
environmentMetrics = environmentMetrics.toBuilder().setCurrent(Float.NaN).build()
}
if (!environmentMetrics.hasLux()) {
environmentMetrics = environmentMetrics.toBuilder().setLux(Float.NaN).build()
}
if (!environmentMetrics.hasUvLux()) {
environmentMetrics = environmentMetrics.toBuilder().setUvLux(Float.NaN).build()
}
// Handle uint32 metrics that default to 0 when not explicitly set or when 0 means no data
if (!environmentMetrics.hasIaq()) {
environmentMetrics = environmentMetrics.toBuilder().setIaq(Int.MIN_VALUE).build()
}
if (!environmentMetrics.hasSoilMoisture()) {
environmentMetrics =
environmentMetrics.toBuilder().setSoilMoisture(Int.MIN_VALUE).build()
}
}
// Leaving in case we have need of nulling any in device metrics.
// if (hasDeviceMetrics()) {
// deviceMetrics =
// deviceMetrics.toBuilder().setBatteryLevel(Int.MIN_VALUE).build()
// }
}
.setTime((log.received_date / MILLIS_TO_SECONDS).toInt())
.build()
}
.getOrNull()
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = meshLogDao
.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
.distinctUntilChanged()
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
.flowOn(dispatchers.io)
fun getLogsFrom(
nodeNum: Int,
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
maxItem: Int = MAX_MESH_PACKETS,
): Flow<List<MeshLog>> =
meshLogDao.getLogsFrom(nodeNum, portNum, maxItem).distinctUntilChanged().flowOn(dispatchers.io)
/*
* Retrieves MeshPackets matching 'nodeNum' and 'portNum'.
* If 'portNum' is not specified, returns all MeshPackets. Otherwise, filters by 'portNum'.
*/
fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE): Flow<List<MeshPacket>> =
getLogsFrom(nodeNum, portNum).mapLatest { list -> list.map { it.fromRadio.packet } }.flowOn(dispatchers.io)
fun getMyNodeInfo(): Flow<MeshProtos.MyNodeInfo?> = getLogsFrom(0, 0)
.mapLatest { list -> list.firstOrNull { it.myNodeInfo != null }?.myNodeInfo }
.flowOn(dispatchers.io)
suspend fun insert(log: MeshLog) = withContext(dispatchers.io) { meshLogDao.insert(log) }
suspend fun deleteAll() = withContext(dispatchers.io) { meshLogDao.deleteAll() }
suspend fun deleteLog(uuid: String) = withContext(dispatchers.io) { meshLogDao.deleteLog(uuid) }
suspend fun deleteLogs(nodeNum: Int, portNum: Int) =
withContext(dispatchers.io) { meshLogDao.deleteLogs(nodeNum, portNum) }
companion object {
private const val MAX_ITEMS = 500
private const val MAX_MESH_PACKETS = 10000
private const val MILLIS_TO_SECONDS = 1000
}
}

View file

@ -1,155 +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.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
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
import javax.inject.Singleton
@Singleton
@Suppress("TooManyFunctions")
class NodeRepository
@Inject
constructor(
processLifecycle: Lifecycle,
private val nodeInfoDao: NodeInfoDao,
private val dispatchers: CoroutineDispatchers,
) {
// hardware info about our local device (can be null)
val myNodeInfo: StateFlow<MyNodeEntity?> =
nodeInfoDao
.getMyNodeInfo()
.flowOn(dispatchers.io)
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null)
// our node info
private val _ourNodeInfo = MutableStateFlow<Node?>(null)
val ourNodeInfo: StateFlow<Node?>
get() = _ourNodeInfo
// The unique userId of our node
private val _myId = MutableStateFlow<String?>(null)
val myId: StateFlow<String?>
get() = _myId
fun getNodeDBbyNum() = nodeInfoDao.nodeDBbyNum().map { map -> map.mapValues { (_, it) -> it.toEntity() } }
// A map from nodeNum to Node
val nodeDBbyNum: StateFlow<Map<Int, Node>> =
nodeInfoDao
.nodeDBbyNum()
.mapLatest { map -> map.mapValues { (_, it) -> it.toModel() } }
.onEach {
val ourNodeInfo = it.values.firstOrNull()
_ourNodeInfo.value = ourNodeInfo
_myId.value = ourNodeInfo?.user?.id
}
.flowOn(dispatchers.io)
.conflate()
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId }
?: Node(num = DataPacket.idToDefaultNodeNum(userId) ?: 0, user = getUser(userId))
fun getUser(nodeNum: Int): MeshProtos.User = getUser(DataPacket.nodeNumToDefaultId(nodeNum))
fun getUser(userId: String): MeshProtos.User = nodeDBbyNum.value.values.find { it.user.id == userId }?.user
?: MeshProtos.User.newBuilder()
.setId(userId)
.setLongName("Meshtastic ${userId.takeLast(n = 4)}")
.setShortName(userId.takeLast(n = 4))
.setHwModel(MeshProtos.HardwareModel.UNSET)
.build()
fun getNodes(
sort: NodeSortOption = NodeSortOption.LAST_HEARD,
filter: String = "",
includeUnknown: Boolean = true,
onlyOnline: Boolean = false,
onlyDirect: Boolean = false,
) = nodeInfoDao
.getNodes(
sort = sort.sqlValue,
filter = filter,
includeUnknown = includeUnknown,
hopsAwayMax = if (onlyDirect) 0 else -1,
lastHeardMin = if (onlyOnline) onlineTimeThreshold() else -1,
)
.mapLatest { list -> list.map { it.toModel() } }
.flowOn(dispatchers.io)
.conflate()
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoDao.upsert(node) }
suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
withContext(dispatchers.io) { nodeInfoDao.installConfig(mi, nodes) }
suspend fun clearNodeDB() = withContext(dispatchers.io) { nodeInfoDao.clearNodeInfo() }
suspend fun deleteNode(num: Int) = withContext(dispatchers.io) {
nodeInfoDao.deleteNode(num)
nodeInfoDao.deleteMetadata(num)
}
suspend fun deleteNodes(nodeNums: List<Int>) = withContext(dispatchers.io) {
nodeInfoDao.deleteNodes(nodeNums)
nodeNums.forEach { nodeInfoDao.deleteMetadata(it) }
}
suspend fun getNodesOlderThan(lastHeard: Int): List<NodeEntity> =
withContext(dispatchers.io) { nodeInfoDao.getNodesOlderThan(lastHeard) }
suspend fun getUnknownNodes(): List<NodeEntity> = withContext(dispatchers.io) { nodeInfoDao.getUnknownNodes() }
suspend fun insertMetadata(metadata: MetadataEntity) = withContext(dispatchers.io) { nodeInfoDao.upsert(metadata) }
val onlineNodeCount: Flow<Int> =
nodeInfoDao
.nodeDBbyNum()
.mapLatest { map -> map.values.count { it.node.lastHeard > onlineTimeThreshold() } }
.flowOn(dispatchers.io)
.conflate()
val totalNodeCount: Flow<Int> =
nodeInfoDao.nodeDBbyNum().mapLatest { map -> map.values.count() }.flowOn(dispatchers.io).conflate()
suspend fun setNodeNotes(num: Int, notes: String) =
withContext(dispatchers.io) { nodeInfoDao.setNodeNotes(num, notes) }
}

View file

@ -1,103 +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 com.geeksville.mesh.Portnums.PortNum
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
class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Lazy<PacketDao>) {
private val packetDao by lazy { packetDaoLazy.get() }
fun getWaypoints(): Flow<List<Packet>> = packetDao.getAllPackets(PortNum.WAYPOINT_APP_VALUE)
fun getContacts(): Flow<Map<String, Packet>> = packetDao.getContactKeys()
suspend fun getMessageCount(contact: String): Int =
withContext(Dispatchers.IO) { packetDao.getMessageCount(contact) }
suspend fun getUnreadCount(contact: String): Int = withContext(Dispatchers.IO) { packetDao.getUnreadCount(contact) }
suspend fun clearUnreadCount(contact: String, timestamp: Long) =
withContext(Dispatchers.IO) { packetDao.clearUnreadCount(contact, timestamp) }
suspend fun getQueuedPackets(): List<DataPacket>? = withContext(Dispatchers.IO) { packetDao.getQueuedPackets() }
suspend fun insert(packet: Packet) = withContext(Dispatchers.IO) { packetDao.insert(packet) }
suspend fun getMessagesFrom(contact: String, getNode: suspend (String?) -> Node) = withContext(Dispatchers.IO) {
packetDao.getMessagesFrom(contact).mapLatest { packets ->
packets.map { packet ->
val message = packet.toMessage(getNode)
message.replyId
.takeIf { it != null && it != 0 }
?.let { getPacketByPacketId(it) }
?.toMessage(getNode)
?.let { originalMessage -> message.copy(originalMessage = originalMessage) } ?: message
}
}
}
suspend fun updateMessageStatus(d: DataPacket, m: MessageStatus) =
withContext(Dispatchers.IO) { packetDao.updateMessageStatus(d, m) }
suspend fun updateMessageId(d: DataPacket, id: Int) =
withContext(Dispatchers.IO) { packetDao.updateMessageId(d, id) }
suspend fun getPacketById(requestId: Int) = withContext(Dispatchers.IO) { packetDao.getPacketById(requestId) }
suspend fun getPacketByPacketId(packetId: Int) =
withContext(Dispatchers.IO) { packetDao.getPacketByPacketId(packetId) }
suspend fun deleteMessages(uuidList: List<Long>) = withContext(Dispatchers.IO) {
for (chunk in uuidList.chunked(500)) { // limit number of UUIDs per query
packetDao.deleteMessages(chunk)
}
}
suspend fun deleteContacts(contactList: List<String>) =
withContext(Dispatchers.IO) { packetDao.deleteContacts(contactList) }
suspend fun deleteWaypoint(id: Int) = withContext(Dispatchers.IO) { packetDao.deleteWaypoint(id) }
suspend fun delete(packet: Packet) = withContext(Dispatchers.IO) { packetDao.delete(packet) }
suspend fun update(packet: Packet) = withContext(Dispatchers.IO) { packetDao.update(packet) }
fun getContactSettings(): Flow<Map<String, ContactSettings>> = packetDao.getContactSettings()
suspend fun getContactSettings(contact: String) =
withContext(Dispatchers.IO) { packetDao.getContactSettings(contact) ?: ContactSettings(contact) }
suspend fun setMuteUntil(contacts: List<String>, until: Long) =
withContext(Dispatchers.IO) { packetDao.setMuteUntil(contacts, until) }
suspend fun insertReaction(reaction: ReactionEntity) = withContext(Dispatchers.IO) { packetDao.insert(reaction) }
suspend fun clearPacketDB() = withContext(Dispatchers.IO) { packetDao.deleteAll() }
}

View file

@ -1,45 +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 com.geeksville.mesh.CoroutineDispatchers
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(
private val quickChatDaoLazy: dagger.Lazy<QuickChatActionDao>,
private val dispatchers: CoroutineDispatchers,
) {
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 deleteAll() = withContext(dispatchers.io) { quickChatActionDao.deleteAll() }
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) }
}

View file

@ -1,91 +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.Bitmap
import android.net.Uri
import android.util.Base64
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.android.BuildUtils.errormsg
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder
import org.meshtastic.core.model.Channel
import java.net.MalformedURLException
import kotlin.jvm.Throws
private const val MESHTASTIC_HOST = "meshtastic.org"
private const val CHANNEL_PATH = "/e/"
internal const val URL_PREFIX = "https://$MESHTASTIC_HOST$CHANNEL_PATH"
private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
/**
* Return a [ChannelSet] that represents the ChannelSet encoded by the URL.
*
* @throws MalformedURLException when not recognized as a valid Meshtastic URL
*/
@Throws(MalformedURLException::class)
fun Uri.toChannelSet(): ChannelSet {
if (fragment.isNullOrBlank() || !host.equals(MESHTASTIC_HOST, true) || !path.equals(CHANNEL_PATH, true)) {
throw MalformedURLException("Not a valid Meshtastic URL: ${toString().take(40)}")
}
// Older versions of Meshtastic clients (Apple/web) included `?add=true` within the URL fragment.
// This gracefully handles those cases until the newer version are generally available/used.
val url = ChannelSet.parseFrom(Base64.decode(fragment!!.substringBefore('?'), BASE64FLAGS))
val shouldAdd =
fragment?.substringAfter('?', "")?.takeUnless { it.isBlank() }?.equals("add=true")
?: getBooleanQueryParameter("add", false)
return url.toBuilder().apply { if (shouldAdd) clearLoraConfig() }.build()
}
/** @return A list of globally unique channel IDs usable with MQTT subscribe() */
val ChannelSet.subscribeList: List<String>
get() = settingsList.filter { it.downlinkEnabled }.map { Channel(it, loraConfig).name }
fun ChannelSet.getChannel(index: Int): Channel? =
if (settingsCount > index) Channel(getSettings(index), loraConfig) else null
/** Return the primary channel info */
val ChannelSet.primaryChannel: Channel?
get() = getChannel(0)
/**
* Return a URL that represents the [ChannelSet]
*
* @param upperCasePrefix portions of the URL can be upper case to make for more efficient QR codes
*/
fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false, shouldAdd: Boolean = false): Uri {
val channelBytes = this.toByteArray() ?: ByteArray(0) // if unset just use empty
val enc = Base64.encodeToString(channelBytes, BASE64FLAGS)
val p = if (upperCasePrefix) URL_PREFIX.uppercase() else URL_PREFIX
val query = if (shouldAdd) "?add=true" else ""
return Uri.parse("$p$query#$enc")
}
fun ChannelSet.qrCode(shouldAdd: Boolean): Bitmap? = try {
val multiFormatWriter = MultiFormatWriter()
val bitMatrix =
multiFormatWriter.encode(getChannelUrl(false, shouldAdd).toString(), BarcodeFormat.QR_CODE, 960, 960)
val barcodeEncoder = BarcodeEncoder()
barcodeEncoder.createBitmap(bitMatrix)
} catch (ex: Throwable) {
errormsg("URL was too complex to render as barcode")
null
}

View file

@ -27,8 +27,6 @@ import com.geeksville.mesh.Portnums.PortNum
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.NodeRepository
import com.geeksville.mesh.ui.debug.FilterMode
import com.google.protobuf.InvalidProtocolBufferException
import dagger.hilt.android.lifecycle.HiltViewModel
@ -44,6 +42,8 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.MeshLog
import java.text.DateFormat
import java.util.Date

View file

@ -35,11 +35,6 @@ import com.geeksville.mesh.Portnums
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.NodeRepository
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.util.safeNumber
@ -58,6 +53,11 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.data.repository.DeviceHardwareRepository
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.model.Node

View file

@ -42,13 +42,6 @@ import com.geeksville.mesh.channelSet
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
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.repository.api.DeviceHardwareRepository
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
@ -70,6 +63,13 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.DeviceHardwareRepository
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.entity.QuickChatAction
@ -78,7 +78,9 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.model.util.getShortDate
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.strings.R
import javax.inject.Inject

View file

@ -1,33 +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.repository.api
import android.app.Application
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.meshtastic.core.model.NetworkDeviceHardware
import javax.inject.Inject
class DeviceHardwareJsonDataSource @Inject constructor(private val application: Application) {
@OptIn(ExperimentalSerializationApi::class)
fun loadDeviceHardwareFromJsonAsset(): List<NetworkDeviceHardware> {
val inputStream = application.assets.open("device_hardware.json")
return Json.decodeFromStream<List<NetworkDeviceHardware>>(inputStream)
}
}

View file

@ -1,43 +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.repository.api
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
class DeviceHardwareLocalDataSource
@Inject
constructor(
private val deviceHardwareDaoLazy: dagger.Lazy<DeviceHardwareDao>,
) {
private val deviceHardwareDao by lazy { deviceHardwareDaoLazy.get() }
suspend fun insertAllDeviceHardware(deviceHardware: List<NetworkDeviceHardware>) = withContext(Dispatchers.IO) {
deviceHardware.forEach { deviceHardware -> deviceHardwareDao.insert(deviceHardware.asEntity()) }
}
suspend fun deleteAllDeviceHardware() = withContext(Dispatchers.IO) { deviceHardwareDao.deleteAll() }
suspend fun getByHwModel(hwModel: Int): DeviceHardwareEntity? =
withContext(Dispatchers.IO) { deviceHardwareDao.getByHwModel(hwModel) }
}

View file

@ -1,109 +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.repository.api
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.warn
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
import javax.inject.Inject
import javax.inject.Singleton
// Annotating with Singleton to ensure a single instance manages the cache
@Singleton
class DeviceHardwareRepository
@Inject
constructor(
private val remoteDataSource: DeviceHardwareRemoteDataSource,
private val localDataSource: DeviceHardwareLocalDataSource,
private val jsonDataSource: DeviceHardwareJsonDataSource,
) {
/**
* Retrieves device hardware information by its model ID.
*
* This function implements a cache-aside pattern with a fallback mechanism:
* 1. Check for a valid, non-expired local cache entry.
* 2. If not found or expired, fetch fresh data from the remote API.
* 3. If the remote fetch fails, attempt to use stale data from the cache.
* 4. If the cache is empty, fall back to loading data from a bundled JSON asset.
*
* @param hwModel The hardware model identifier.
* @param forceRefresh If true, the local cache will be invalidated and data will be fetched remotely.
* @return A [Result] containing the [DeviceHardware] on success (or null if not found), or an exception on failure.
*/
suspend fun getDeviceHardwareByModel(hwModel: Int, forceRefresh: Boolean = false): Result<DeviceHardware?> =
withContext(Dispatchers.IO) {
if (forceRefresh) {
localDataSource.deleteAllDeviceHardware()
} else {
// 1. Attempt to retrieve from cache first
val cachedEntity = localDataSource.getByHwModel(hwModel)
if (cachedEntity != null && !cachedEntity.isStale()) {
debug("Using fresh cached device hardware for model $hwModel")
return@withContext Result.success(cachedEntity.asExternalModel())
}
}
// 2. Fetch from remote API
runCatching {
debug("Fetching device hardware from remote API.")
val remoteHardware = remoteDataSource.getAllDeviceHardware()
localDataSource.insertAllDeviceHardware(remoteHardware)
localDataSource.getByHwModel(hwModel)?.asExternalModel()
}
.onSuccess {
// Successfully fetched and found the model
return@withContext Result.success(it)
}
.onFailure { e ->
warn("Failed to fetch device hardware from server: ${e.message}")
// 3. Attempt to use stale cache as a fallback
val staleEntity = localDataSource.getByHwModel(hwModel)
if (staleEntity != null) {
debug("Using stale cached device hardware for model $hwModel")
return@withContext Result.success(staleEntity.asExternalModel())
}
// 4. Fallback to bundled JSON if cache is empty
debug("Cache is empty, falling back to bundled JSON asset.")
return@withContext loadFromBundledJson(hwModel)
}
}
private suspend fun loadFromBundledJson(hwModel: Int): Result<DeviceHardware?> = runCatching {
val jsonHardware = jsonDataSource.loadDeviceHardwareFromJsonAsset()
localDataSource.insertAllDeviceHardware(jsonHardware)
localDataSource.getByHwModel(hwModel)?.asExternalModel()
}
/** Extension function to check if the cached entity is stale. */
private fun DeviceHardwareEntity.isStale(): Boolean =
(System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
companion object {
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.DAYS.toMillis(1)
}
}

View file

@ -1,34 +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.repository.api
import android.app.Application
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import org.meshtastic.core.model.NetworkFirmwareReleases
import javax.inject.Inject
class FirmwareReleaseJsonDataSource @Inject constructor(private val application: Application) {
@OptIn(ExperimentalSerializationApi::class)
fun loadFirmwareReleaseFromJsonAsset(): NetworkFirmwareReleases {
val inputStream = application.assets.open("firmware_releases.json")
val result = inputStream.use { Json.decodeFromStream<NetworkFirmwareReleases>(inputStream) }
return result
}
}

View file

@ -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.repository.api
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
class FirmwareReleaseLocalDataSource @Inject constructor(private val firmwareReleaseDaoLazy: Lazy<FirmwareReleaseDao>) {
private val firmwareReleaseDao by lazy { firmwareReleaseDaoLazy.get() }
suspend fun insertFirmwareReleases(
firmwareReleases: List<NetworkFirmwareRelease>,
releaseType: FirmwareReleaseType,
) = withContext(Dispatchers.IO) {
if (firmwareReleases.isNotEmpty()) {
firmwareReleaseDao.deleteAll()
firmwareReleases.forEach { firmwareRelease ->
firmwareReleaseDao.insert(firmwareRelease.asEntity(releaseType))
}
}
}
suspend fun deleteAllFirmwareReleases() = withContext(Dispatchers.IO) { firmwareReleaseDao.deleteAll() }
suspend fun getLatestRelease(releaseType: FirmwareReleaseType): FirmwareReleaseEntity? =
withContext(Dispatchers.IO) {
val releases = firmwareReleaseDao.getReleasesByType(releaseType)
if (releases.isEmpty()) {
return@withContext null
} else {
val latestRelease = releases.maxBy { it.asDeviceVersion() }
return@withContext latestRelease
}
}
}

View file

@ -1,132 +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.repository.api
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.warn
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
import javax.inject.Singleton
@Singleton
class FirmwareReleaseRepository
@Inject
constructor(
private val remoteDataSource: FirmwareReleaseRemoteDataSource,
private val localDataSource: FirmwareReleaseLocalDataSource,
private val jsonDataSource: FirmwareReleaseJsonDataSource,
) {
/**
* A flow that provides the latest STABLE firmware release. It follows a "cache-then-network" strategy:
* 1. Immediately emits the cached version (if any).
* 2. If the cached version is stale, triggers a network fetch in the background.
* 3. Emits the updated version upon successful fetch. Collectors should use `.distinctUntilChanged()` to avoid
* redundant UI updates.
*/
val stableRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.STABLE)
/**
* A flow that provides the latest ALPHA firmware release.
*
* @see stableRelease for behavior details.
*/
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
private fun getLatestFirmware(
releaseType: FirmwareReleaseType,
forceRefresh: Boolean = false,
): Flow<FirmwareRelease?> = flow {
if (forceRefresh) {
invalidateCache()
}
// 1. Emit cached data first, regardless of staleness.
// This gives the UI something to show immediately.
val cachedRelease = localDataSource.getLatestRelease(releaseType)
cachedRelease?.let {
debug("Emitting cached firmware for $releaseType (isStale=${it.isStale()})")
emit(it.asExternalModel())
}
// 2. If the cache was fresh and we are not forcing a refresh, we're done.
if (cachedRelease != null && !cachedRelease.isStale() && !forceRefresh) {
return@flow
}
// 3. Cache is stale, empty, or refresh is forced. Fetch new data.
updateCacheFromSources()
// 4. Emit the final, updated value from the cache.
// The `distinctUntilChanged()` operator on the collector side will prevent
// re-emitting the same data if the cache wasn't actually updated.
val finalRelease = localDataSource.getLatestRelease(releaseType)
debug("Emitting final firmware for $releaseType from cache.")
emit(finalRelease?.asExternalModel())
}
/**
* Updates the local cache by fetching from the remote API, with a fallback to a bundled JSON asset if the remote
* fetch fails.
*
* This method is efficient because it fetches and caches all release types (stable, alpha, etc.) in a single
* operation.
*/
private suspend fun updateCacheFromSources() {
val remoteFetchSuccess =
runCatching {
debug("Fetching fresh firmware releases from remote API.")
val networkReleases = remoteDataSource.getFirmwareReleases()
// The API fetches all release types, so we cache them all at once.
localDataSource.insertFirmwareReleases(networkReleases.releases.stable, FirmwareReleaseType.STABLE)
localDataSource.insertFirmwareReleases(networkReleases.releases.alpha, FirmwareReleaseType.ALPHA)
}
.isSuccess
// If remote fetch failed, try the JSON fallback as a last resort.
if (!remoteFetchSuccess) {
warn("Remote fetch failed, attempting to cache from bundled JSON.")
runCatching {
val jsonReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
localDataSource.insertFirmwareReleases(jsonReleases.releases.stable, FirmwareReleaseType.STABLE)
localDataSource.insertFirmwareReleases(jsonReleases.releases.alpha, FirmwareReleaseType.ALPHA)
}
.onFailure { warn("Failed to cache from JSON: ${it.message}") }
}
}
suspend fun invalidateCache() {
localDataSource.deleteAllFirmwareReleases()
}
/** Extension function to check if the cached entity is stale. */
private fun FirmwareReleaseEntity.isStale(): Boolean =
(System.currentTimeMillis() - this.lastUpdated) > CACHE_EXPIRATION_TIME_MS
companion object {
private val CACHE_EXPIRATION_TIME_MS = TimeUnit.HOURS.toMillis(1)
}
}

View file

@ -1,130 +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.repository.datastore
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.ChannelProtos.Channel
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.ConfigProtos.Config
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.getChannelUrl
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import org.meshtastic.core.datastore.ChannelSetDataSource
import org.meshtastic.core.datastore.LocalConfigDataSource
import org.meshtastic.core.datastore.ModuleConfigDataSource
import javax.inject.Inject
/**
* Class responsible for radio configuration data. Combines access to [nodeDB], [ChannelSet], [LocalConfig] &
* [LocalModuleConfig].
*/
class RadioConfigRepository
@Inject
constructor(
private val nodeDB: NodeRepository,
private val channelSetDataSource: ChannelSetDataSource,
private val localConfigDataSource: LocalConfigDataSource,
private val moduleConfigDataSource: ModuleConfigDataSource,
) {
/** Flow representing the [ChannelSet] data store. */
val channelSetFlow: Flow<ChannelSet> = channelSetDataSource.channelSetFlow
/** Clears the [ChannelSet] data in the data store. */
suspend fun clearChannelSet() {
channelSetDataSource.clearChannelSet()
}
/** Replaces the [ChannelSettings] list with a new [settingsList]. */
suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
channelSetDataSource.clearSettings()
channelSetDataSource.addAllSettings(settingsList)
}
/**
* Updates the [ChannelSettings] list with the provided channel and returns the index of the admin channel after the
* update (if not found, returns 0).
*
* @param channel The [Channel] provided.
* @return the index of the admin channel after the update (if not found, returns 0).
*/
suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel)
/** Flow representing the [LocalConfig] data store. */
val localConfigFlow: Flow<LocalConfig> = localConfigDataSource.localConfigFlow
/** Clears the [LocalConfig] data in the data store. */
suspend fun clearLocalConfig() {
localConfigDataSource.clearLocalConfig()
}
/**
* Updates [LocalConfig] from each [Config] oneOf.
*
* @param config The [Config] to be set.
*/
suspend fun setLocalConfig(config: Config) {
localConfigDataSource.setLocalConfig(config)
if (config.hasLora()) channelSetDataSource.setLoraConfig(config.lora)
}
/** Flow representing the [LocalModuleConfig] data store. */
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigDataSource.moduleConfigFlow
/** Clears the [LocalModuleConfig] data in the data store. */
suspend fun clearLocalModuleConfig() {
moduleConfigDataSource.clearLocalModuleConfig()
}
/**
* Updates [LocalModuleConfig] from each [ModuleConfig] oneOf.
*
* @param config The [ModuleConfig] to be set.
*/
suspend fun setLocalModuleConfig(config: ModuleConfig) {
moduleConfigDataSource.setLocalModuleConfig(config)
}
/** Flow representing the combined [DeviceProfile] protobuf. */
val deviceProfileFlow: Flow<DeviceProfile> =
combine(nodeDB.ourNodeInfo, channelSetFlow, localConfigFlow, moduleConfigFlow) {
node,
channels,
localConfig,
localModuleConfig,
->
deviceProfile {
node?.user?.let {
longName = it.longName
shortName = it.shortName
}
channelUrl = channels.getChannelUrl().toString()
config = localConfig
moduleConfig = localModuleConfig
if (node != null && localConfig.position.fixedPosition) {
fixedPosition = node.position
}
}
}
}

View file

@ -19,10 +19,7 @@ package com.geeksville.mesh.repository.network
import com.geeksville.mesh.MeshProtos.MqttClientProxyMessage
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.model.subscribeList
import com.geeksville.mesh.mqttClientProxyMessage
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.util.ignoreException
import com.google.protobuf.ByteString
import kotlinx.coroutines.channels.awaitClose
@ -37,6 +34,9 @@ import org.eclipse.paho.client.mqttv3.MqttCallbackExtended
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.eclipse.paho.client.mqttv3.MqttMessage
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.util.subscribeList
import java.net.URI
import java.security.SecureRandom
import javax.inject.Inject

View file

@ -54,13 +54,9 @@ import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.copy
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.fromRadio
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
import com.geeksville.mesh.position
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.repository.network.MQTTRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService
@ -83,6 +79,10 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity

View file

@ -24,8 +24,6 @@ import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.errormsg
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.fromRadio
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import dagger.Lazy
@ -35,6 +33,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus

View file

@ -63,8 +63,8 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.model.getChannel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed

View file

@ -20,9 +20,7 @@ package com.geeksville.mesh.ui.connections
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@ -30,6 +28,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.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.prefs.ui.UiPrefs

View file

@ -21,8 +21,6 @@ import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.service.ServiceRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -33,6 +31,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket

View file

@ -95,7 +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.model.getChannel
import com.geeksville.mesh.ui.common.components.SecurityIcon
import com.geeksville.mesh.ui.node.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@ -106,6 +105,7 @@ 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.model.util.getChannel
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.theme.AppTheme
import java.nio.charset.StandardCharsets

View file

@ -21,10 +21,6 @@ import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.service.ServiceRepository
@ -40,6 +36,10 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.QuickChatActionRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket

View file

@ -20,7 +20,6 @@ package com.geeksville.mesh.ui.node
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@ -30,6 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Position
import timber.log.Timber

View file

@ -21,8 +21,6 @@ import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.AdminProtos
import com.geeksville.mesh.database.NodeRepository
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
@ -38,6 +36,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.model.Position

View file

@ -26,9 +26,6 @@ import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.MeshProtos
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.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -44,6 +41,9 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.UiPreferencesDataSource

View file

@ -19,12 +19,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.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.NodeEntity
import javax.inject.Inject
import kotlin.time.Duration.Companion.days

View file

@ -45,14 +45,11 @@ 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.NodeRepository
import com.geeksville.mesh.deviceProfile
import com.geeksville.mesh.model.getChannelList
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.ServiceRepository
@ -72,10 +69,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
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.model.util.toChannelSet
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.map.MapConsentPrefs

View file

@ -96,9 +96,6 @@ import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getChannelUrl
import com.geeksville.mesh.model.qrCode
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.service.ConnectionState
@ -112,6 +109,9 @@ import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import kotlinx.coroutines.launch
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.qrCode
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.AdaptiveTwoPane