mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-18 19:56:34 -04:00
refactor: merge NodeManager into NodeRepository
Eliminate the vestigial NodeManager/NodeRepository interface split. All runtime node state management methods (handleReceivedUser, handleReceivedPosition, handleReceivedTelemetry, updateNode, etc.) now live directly on NodeRepository alongside the query surface. - Delete NodeManager.kt (82 LOC) - Extend NodeRepository with NodeIdLookup and add all manager methods - Update 8 consumers to inject NodeRepository instead of NodeManager - Remove dead nodeManager param from MeshServiceOrchestrator - Add NodeManager methods to FakeNodeRepository test double - Update all tests (mocks, constructor params, verifications) - SdkNodeRepositoryImpl now binds [NodeRepository, NodeIdLookup] Build: assembleDebug ✅, desktop:compileKotlin ✅, all jvmTests ✅ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -28,7 +28,7 @@ import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.Notification
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
@@ -49,7 +49,7 @@ import org.meshtastic.proto.PortNum
|
||||
*/
|
||||
@Single
|
||||
class MessagePersistenceHandler(
|
||||
private val nodeManager: NodeManager,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val notificationManager: NotificationManager,
|
||||
private val serviceNotifications: MeshServiceNotifications,
|
||||
@@ -116,7 +116,7 @@ class MessagePersistenceHandler(
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
private suspend fun PacketRepository.shouldFilterMessage(dataPacket: DataPacket, contactKey: String): Boolean {
|
||||
val isIgnored = nodeManager.nodeDBbyID[dataPacket.from]?.isIgnored == true
|
||||
val isIgnored = nodeRepository.nodeDBbyID[dataPacket.from]?.isIgnored == true
|
||||
if (isIgnored) return true
|
||||
|
||||
if (dataPacket.dataType != PortNum.TEXT_MESSAGE_APP.value) return false
|
||||
@@ -130,7 +130,7 @@ class MessagePersistenceHandler(
|
||||
updateNotification: Boolean,
|
||||
) {
|
||||
val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted
|
||||
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
|
||||
val nodeMuted = nodeRepository.nodeDBbyID[dataPacket.from]?.isMuted == true
|
||||
val isSilent = conversationMuted || nodeMuted
|
||||
if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
|
||||
scope.launch {
|
||||
@@ -150,10 +150,10 @@ class MessagePersistenceHandler(
|
||||
|
||||
private suspend fun getSenderName(packet: DataPacket): String {
|
||||
if (packet.from == DataPacket.ID_LOCAL) {
|
||||
val myId = nodeManager.getMyId()
|
||||
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
val myId = nodeRepository.getMyId()
|
||||
return nodeRepository.nodeDBbyID[myId]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
}
|
||||
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
return nodeRepository.nodeDBbyID[packet.from]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
|
||||
}
|
||||
|
||||
private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket, isSilent: Boolean) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.repository.NeighborInfoHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
@@ -32,9 +31,8 @@ import org.meshtastic.proto.NeighborInfo
|
||||
|
||||
@Single
|
||||
class NeighborInfoHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
) : NeighborInfoHandler {
|
||||
|
||||
private val startTimes = atomic(persistentMapOf<Int, Long>())
|
||||
@@ -51,13 +49,13 @@ class NeighborInfoHandlerImpl(
|
||||
|
||||
// Store the last neighbor info from our connected radio
|
||||
val from = packet.from
|
||||
if (from == nodeManager.myNodeNum.value) {
|
||||
if (from == nodeRepository.myNodeNum.value) {
|
||||
lastNeighborInfo = ni
|
||||
Logger.d { "Stored last neighbor info from connected radio" }
|
||||
}
|
||||
|
||||
// Update Node DB
|
||||
nodeManager.nodeDBbyNodeNum[from]?.let { /* SDK client.nodes is canonical source */ }
|
||||
nodeRepository.nodeDBbyNodeNum[from]?.let { /* SDK client.nodes is canonical source */ }
|
||||
|
||||
// Format for UI response
|
||||
val requestId = packet.decoded?.request_id ?: 0
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.SfppHasher
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.StoreForwardPacketHandler
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
@@ -40,7 +40,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
/** Implementation of [StoreForwardPacketHandler] that handles both legacy S&F and SF++ packets. */
|
||||
@Single
|
||||
class StoreForwardPacketHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val historyManager: HistoryManager,
|
||||
private val dataHandler: Lazy<MeshDataHandler>,
|
||||
@@ -111,7 +111,7 @@ class StoreForwardPacketHandlerImpl(
|
||||
|
||||
Logger.d {
|
||||
"SFPP updateStatus: packetId=${sfpp.encapsulated_id} from=${sfpp.encapsulated_from} " +
|
||||
"to=${sfpp.encapsulated_to} myNodeNum=${nodeManager.myNodeNum.value} status=$status"
|
||||
"to=${sfpp.encapsulated_to} myNodeNum=${nodeRepository.myNodeNum.value} status=$status"
|
||||
}
|
||||
scope.handledLaunch {
|
||||
packetRepository.value.updateSFPPStatus(
|
||||
@@ -121,7 +121,7 @@ class StoreForwardPacketHandlerImpl(
|
||||
hash = hash,
|
||||
status = status,
|
||||
rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
|
||||
myNodeNum = nodeManager.myNodeNum.value ?: 0,
|
||||
myNodeNum = nodeRepository.myNodeNum.value ?: 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.toOneLiner
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.Notification
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.core.repository.TelemetryPacketHandler
|
||||
@@ -46,7 +46,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
*/
|
||||
@Single
|
||||
class TelemetryPacketHandlerImpl(
|
||||
private val nodeManager: NodeManager,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val notificationManager: NotificationManager,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : TelemetryPacketHandler {
|
||||
@@ -67,7 +67,7 @@ class TelemetryPacketHandlerImpl(
|
||||
// Note: Local telemetry notification update was previously handled by
|
||||
// MeshConnectionManager.updateTelemetry(), now managed via SDK flows.
|
||||
|
||||
nodeManager.updateNode(fromNum) { node: Node ->
|
||||
nodeRepository.updateNode(fromNum) { node: Node ->
|
||||
val metrics = t.device_metrics
|
||||
val environment = t.environment_metrics
|
||||
val power = t.power_metrics
|
||||
|
||||
@@ -53,7 +53,7 @@ import org.meshtastic.sdk.RadioClient
|
||||
* [RadioClient.telemetry], and [RadioClient.routing] respectively.
|
||||
*
|
||||
* **State distribution:** Handled by [SdkStateBridge], which feeds SDK flows into
|
||||
* [ServiceRepository] and [org.meshtastic.core.repository.NodeManager].
|
||||
* [ServiceRepository] and [org.meshtastic.core.repository.NodeRepository].
|
||||
*/
|
||||
@Single(binds = [RadioController::class])
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.service.ServiceAction
|
||||
import org.meshtastic.core.repository.MeshLocationManager
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
@@ -50,11 +50,11 @@ import org.meshtastic.sdk.NodeChange
|
||||
import org.meshtastic.sdk.NodeId
|
||||
|
||||
/**
|
||||
* Bridges SDK reactive flows into the legacy repository layer and routes [ServiceAction]s
|
||||
* Bridges SDK reactive flows into the repository layer and routes [ServiceAction]s
|
||||
* directly through the SDK, bypassing the old CommandSender/MeshActionHandler pipeline.
|
||||
*
|
||||
* The SDK owns the transport and all state; this bridge maps SDK emissions into [ServiceRepository]
|
||||
* and [NodeManager] so that existing feature-module UI code (which observes those repositories)
|
||||
* and [NodeRepository] so that existing feature-module UI code (which observes those repositories)
|
||||
* continues to work without modification.
|
||||
*
|
||||
* **Lifecycle:** Created as a Koin `@Single`. Automatically subscribes to [RadioClientAccessor.client]
|
||||
@@ -65,7 +65,7 @@ import org.meshtastic.sdk.NodeId
|
||||
class SdkStateBridge(
|
||||
private val accessor: RadioClientAccessor,
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeManager: NodeManager,
|
||||
private val nodeRepository: NodeRepository,
|
||||
private val packetRepository: Lazy<PacketRepository>,
|
||||
private val locationManager: MeshLocationManager,
|
||||
private val uiPrefs: UiPrefs,
|
||||
@@ -93,15 +93,15 @@ class SdkStateBridge(
|
||||
.onEach { change ->
|
||||
when (change) {
|
||||
is NodeChange.Snapshot -> {
|
||||
nodeManager.clear()
|
||||
nodeRepository.clear()
|
||||
change.nodes.forEach { (_, nodeInfo) ->
|
||||
nodeManager.installNodeInfo(nodeInfo, withBroadcast = false)
|
||||
nodeRepository.installNodeInfo(nodeInfo, withBroadcast = false)
|
||||
}
|
||||
nodeManager.setNodeDbReady(true)
|
||||
nodeRepository.setNodeDbReady(true)
|
||||
}
|
||||
is NodeChange.Added -> nodeManager.installNodeInfo(change.node, withBroadcast = true)
|
||||
is NodeChange.Updated -> nodeManager.installNodeInfo(change.node, withBroadcast = true)
|
||||
is NodeChange.Removed -> nodeManager.removeByNodenum(change.nodeId.raw)
|
||||
is NodeChange.Added -> nodeRepository.installNodeInfo(change.node, withBroadcast = true)
|
||||
is NodeChange.Updated -> nodeRepository.installNodeInfo(change.node, withBroadcast = true)
|
||||
is NodeChange.Removed -> nodeRepository.removeByNodenum(change.nodeId.raw)
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
@@ -109,7 +109,7 @@ class SdkStateBridge(
|
||||
// ── Own node identity ───────────────────────────────────────────────
|
||||
accessor.client
|
||||
.flatMapLatest { client -> client?.ownNode ?: flowOf(null) }
|
||||
.onEach { ownNode -> if (ownNode != null) nodeManager.setMyNodeNum(ownNode.num) }
|
||||
.onEach { ownNode -> if (ownNode != null) nodeRepository.setMyNodeNum(ownNode.num) }
|
||||
.launchIn(scope)
|
||||
|
||||
// ── Raw packet forward (for RadioConfigViewModel + TAK) ─────────────
|
||||
@@ -188,21 +188,21 @@ class SdkStateBridge(
|
||||
is ServiceAction.Favorite -> {
|
||||
val node = action.node
|
||||
client.admin.setFavorite(NodeId(node.num), !node.isFavorite)
|
||||
nodeManager.updateNode(node.num) { it.copy(isFavorite = !node.isFavorite) }
|
||||
nodeRepository.updateNode(node.num) { it.copy(isFavorite = !node.isFavorite) }
|
||||
}
|
||||
|
||||
is ServiceAction.Ignore -> {
|
||||
val node = action.node
|
||||
val newIgnored = !node.isIgnored
|
||||
client.admin.setIgnored(NodeId(node.num), newIgnored)
|
||||
nodeManager.updateNode(node.num) { it.copy(isIgnored = newIgnored) }
|
||||
nodeRepository.updateNode(node.num) { it.copy(isIgnored = newIgnored) }
|
||||
packetRepository.value.updateFilteredBySender(node.user.id, newIgnored)
|
||||
}
|
||||
|
||||
is ServiceAction.Mute -> {
|
||||
val node = action.node
|
||||
client.admin.toggleMuted(NodeId(node.num))
|
||||
nodeManager.updateNode(node.num) { it.copy(isMuted = !node.isMuted) }
|
||||
nodeRepository.updateNode(node.num) { it.copy(isMuted = !node.isMuted) }
|
||||
}
|
||||
|
||||
is ServiceAction.Reaction -> {
|
||||
@@ -228,7 +228,7 @@ class SdkStateBridge(
|
||||
is ServiceAction.ImportContact -> {
|
||||
val verified = action.contact.copy(manually_verified = true)
|
||||
client.admin.addContact(verified)
|
||||
nodeManager.handleReceivedUser(
|
||||
nodeRepository.handleReceivedUser(
|
||||
verified.node_num,
|
||||
verified.user ?: User(),
|
||||
manuallyVerified = true,
|
||||
|
||||
@@ -43,7 +43,6 @@ import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.model.util.NodeIdLookup
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.Notification
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
@@ -61,7 +60,7 @@ import org.meshtastic.proto.Position as ProtoPosition
|
||||
/**
|
||||
* Unified node repository and manager — single source of truth for all mesh node state.
|
||||
*
|
||||
* Replaces the previous split between `NodeManagerImpl` (write operations, in-memory atomicfu maps)
|
||||
* Replaces the previous split between a write-operation layer (in-memory atomicfu maps)
|
||||
* and `SdkNodeRepositoryImpl` (repository interface, StateFlows). Now uses a single StateFlow
|
||||
* with metadata enrichment on every write.
|
||||
*
|
||||
@@ -69,14 +68,14 @@ import org.meshtastic.proto.Position as ProtoPosition
|
||||
* database in-memory, populated by SdkStateBridge from the SDK's NodeChange flow.
|
||||
* Node metadata (favorites, notes, ignored, muted) persists via Room's node_metadata table.
|
||||
*/
|
||||
@Single(binds = [NodeRepository::class, NodeManager::class, NodeIdLookup::class])
|
||||
@Single(binds = [NodeRepository::class, NodeIdLookup::class])
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
class SdkNodeRepositoryImpl(
|
||||
private val localStatsDataSource: LocalStatsDataSource,
|
||||
private val dbManager: DatabaseProvider,
|
||||
private val notificationManager: NotificationManager,
|
||||
@Named("ServiceScope") private val scope: CoroutineScope,
|
||||
) : NodeRepository, NodeManager {
|
||||
) : NodeRepository {
|
||||
|
||||
private val _nodeDBbyNum = MutableStateFlow<Map<Int, Node>>(emptyMap())
|
||||
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
|
||||
@@ -227,7 +226,7 @@ class SdkNodeRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
// ── NodeManager surface ─────────────────────────────────────────────────
|
||||
// ── Runtime node state management ────────────────────────────────────────
|
||||
|
||||
override val nodeDBbyNodeNum: Map<Int, Node>
|
||||
get() = _nodeDBbyNum.value
|
||||
|
||||
@@ -34,7 +34,7 @@ import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.repository.HistoryManager
|
||||
import org.meshtastic.core.repository.MeshDataHandler
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
@@ -47,7 +47,7 @@ import kotlin.test.Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class StoreForwardPacketHandlerImplTest {
|
||||
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val nodeRepository = mock<NodeRepository>(MockMode.autofill)
|
||||
private val packetRepository = mock<PacketRepository>(MockMode.autofill)
|
||||
private val historyManager = mock<HistoryManager>(MockMode.autofill)
|
||||
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
|
||||
@@ -61,11 +61,11 @@ class StoreForwardPacketHandlerImplTest {
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow<Int?>(myNodeNum)
|
||||
every { nodeRepository.myNodeNum } returns MutableStateFlow<Int?>(myNodeNum)
|
||||
|
||||
handler =
|
||||
StoreForwardPacketHandlerImpl(
|
||||
nodeManager = nodeManager,
|
||||
nodeRepository = nodeRepository,
|
||||
packetRepository = lazy { packetRepository },
|
||||
historyManager = historyManager,
|
||||
dataHandler = lazy { dataHandler },
|
||||
|
||||
@@ -27,7 +27,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.NotificationManager
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
@@ -42,7 +42,7 @@ import kotlin.test.Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TelemetryPacketHandlerImplTest {
|
||||
|
||||
private val nodeManager = mock<NodeManager>(MockMode.autofill)
|
||||
private val nodeRepository = mock<NodeRepository>(MockMode.autofill)
|
||||
private val notificationManager = mock<NotificationManager>(MockMode.autofill)
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
@@ -57,7 +57,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
fun setUp() {
|
||||
handler =
|
||||
TelemetryPacketHandlerImpl(
|
||||
nodeManager = nodeManager,
|
||||
nodeRepository = nodeRepository,
|
||||
notificationManager = notificationManager,
|
||||
scope = testScope,
|
||||
)
|
||||
@@ -93,7 +93,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any(), any()) }
|
||||
verify { nodeRepository.updateNode(myNodeNum, any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Device metrics from remote node ----------
|
||||
@@ -108,7 +108,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
verify { nodeRepository.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Environment metrics ----------
|
||||
@@ -126,7 +126,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
verify { nodeRepository.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Power metrics ----------
|
||||
@@ -140,7 +140,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
verify { nodeRepository.updateNode(remoteNodeNum, any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Telemetry time handling ----------
|
||||
@@ -154,7 +154,7 @@ class TelemetryPacketHandlerImplTest {
|
||||
handler.handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { nodeManager.updateNode(myNodeNum, any(), any(), any()) }
|
||||
verify { nodeRepository.updateNode(myNodeNum, any(), any(), any()) }
|
||||
}
|
||||
|
||||
// ---------- Null payload ----------
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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 org.meshtastic.core.repository
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.NodeIdLookup
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
/** Interface for managing the in-memory node database and processing received node information. */
|
||||
@Suppress("TooManyFunctions")
|
||||
interface NodeManager : NodeIdLookup {
|
||||
/** Reactive map of all nodes by their number. */
|
||||
val nodeDBbyNodeNum: Map<Int, Node>
|
||||
|
||||
/** Reactive map of all nodes by their ID string. */
|
||||
val nodeDBbyID: Map<String, Node>
|
||||
|
||||
/** Whether the node database is ready. */
|
||||
val isNodeDbReady: StateFlow<Boolean>
|
||||
|
||||
/** Sets whether the node database is ready. */
|
||||
fun setNodeDbReady(ready: Boolean)
|
||||
|
||||
/** The local node number as a thread-safe [StateFlow]. */
|
||||
val myNodeNum: StateFlow<Int?>
|
||||
|
||||
/** Sets the local node number. */
|
||||
fun setMyNodeNum(num: Int?)
|
||||
|
||||
/** The firmware edition reported by the connected device. */
|
||||
val firmwareEdition: StateFlow<FirmwareEdition?>
|
||||
|
||||
/** Sets the firmware edition of the connected device. */
|
||||
fun setFirmwareEdition(edition: FirmwareEdition?)
|
||||
|
||||
/** Clears the in-memory node database. */
|
||||
fun clear()
|
||||
|
||||
/** Returns information about the local node. */
|
||||
fun getMyNodeInfo(): MyNodeInfo?
|
||||
|
||||
/** Returns the local node ID. */
|
||||
fun getMyId(): String
|
||||
|
||||
/** Processes a received user packet. */
|
||||
fun handleReceivedUser(fromNum: Int, p: User, channel: Int = 0, manuallyVerified: Boolean = false)
|
||||
|
||||
/** Processes a received position packet. */
|
||||
fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long)
|
||||
|
||||
/** Processes a received telemetry packet. */
|
||||
fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry)
|
||||
|
||||
/** Updates a node using a transformation function. */
|
||||
fun updateNode(nodeNum: Int, withBroadcast: Boolean = true, channel: Int = 0, transform: (Node) -> Node)
|
||||
|
||||
/** Removes a node from the in-memory database by its number. */
|
||||
fun removeByNodenum(nodeNum: Int)
|
||||
|
||||
/** Installs node information from a ProtoNodeInfo object. */
|
||||
fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true)
|
||||
}
|
||||
@@ -21,19 +21,25 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.model.util.NodeIdLookup
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
/**
|
||||
* Repository interface for managing node-related data.
|
||||
*
|
||||
* This component provides access to the mesh's node database, local device information, and mesh-wide statistics. It
|
||||
* supports reactive queries for node lists, counts, and filtered/sorted views.
|
||||
* supports reactive queries for node lists, counts, and filtered/sorted views, as well as runtime in-memory state
|
||||
* management for processing incoming node packets from the radio.
|
||||
*
|
||||
* This interface is shared across platforms via Kotlin Multiplatform (KMP).
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface NodeRepository {
|
||||
interface NodeRepository : NodeIdLookup {
|
||||
/** Reactive flow of hardware info about our local radio device. */
|
||||
val myNodeInfo: StateFlow<MyNodeInfo?>
|
||||
|
||||
@@ -165,4 +171,57 @@ interface NodeRepository {
|
||||
* Used during the initial connection handshake.
|
||||
*/
|
||||
suspend fun installConfig(mi: MyNodeInfo, nodes: List<Node>)
|
||||
|
||||
// ── Runtime node state management ───────────────────────────────────────
|
||||
|
||||
/** Reactive map of all nodes by their number (snapshot access). */
|
||||
val nodeDBbyNodeNum: Map<Int, Node>
|
||||
|
||||
/** Reactive map of all nodes by their ID string. */
|
||||
val nodeDBbyID: Map<String, Node>
|
||||
|
||||
/** Whether the node database is ready. */
|
||||
val isNodeDbReady: StateFlow<Boolean>
|
||||
|
||||
/** Sets whether the node database is ready. */
|
||||
fun setNodeDbReady(ready: Boolean)
|
||||
|
||||
/** The local node number as a thread-safe [StateFlow]. */
|
||||
val myNodeNum: StateFlow<Int?>
|
||||
|
||||
/** Sets the local node number. */
|
||||
fun setMyNodeNum(num: Int?)
|
||||
|
||||
/** The firmware edition reported by the connected device. */
|
||||
val firmwareEdition: StateFlow<FirmwareEdition?>
|
||||
|
||||
/** Sets the firmware edition of the connected device. */
|
||||
fun setFirmwareEdition(edition: FirmwareEdition?)
|
||||
|
||||
/** Clears the in-memory node database. */
|
||||
fun clear()
|
||||
|
||||
/** Returns information about the local node. */
|
||||
fun getMyNodeInfo(): MyNodeInfo?
|
||||
|
||||
/** Returns the local node ID. */
|
||||
fun getMyId(): String
|
||||
|
||||
/** Processes a received user packet. */
|
||||
fun handleReceivedUser(fromNum: Int, p: User, channel: Int = 0, manuallyVerified: Boolean = false)
|
||||
|
||||
/** Processes a received position packet. */
|
||||
fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long)
|
||||
|
||||
/** Processes a received telemetry packet. */
|
||||
fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry)
|
||||
|
||||
/** Updates a node using a transformation function. */
|
||||
fun updateNode(nodeNum: Int, withBroadcast: Boolean = true, channel: Int = 0, transform: (Node) -> Node)
|
||||
|
||||
/** Removes a node from the in-memory database by its number. */
|
||||
fun removeByNodenum(nodeNum: Int)
|
||||
|
||||
/** Installs node information from a ProtoNodeInfo object. */
|
||||
fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.meshtastic.core.common.util.handledLaunch
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.repository.AppWidgetUpdater
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.RadioPrefs
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.TakPrefs
|
||||
@@ -48,7 +47,6 @@ import org.meshtastic.core.takserver.TAKServerManager
|
||||
@Single
|
||||
class MeshServiceOrchestrator(
|
||||
private val radioPrefs: RadioPrefs,
|
||||
private val nodeManager: NodeManager,
|
||||
private val serviceNotifications: MeshServiceNotifications,
|
||||
private val takServerManager: TAKServerManager,
|
||||
private val takMeshIntegration: TAKMeshIntegration,
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.AppWidgetUpdater
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.RadioPrefs
|
||||
@@ -51,7 +50,6 @@ import kotlin.test.assertTrue
|
||||
class MeshServiceOrchestratorTest {
|
||||
|
||||
private val radioPrefs: RadioPrefs = mock(MockMode.autofill)
|
||||
private val nodeManager: NodeManager = mock(MockMode.autofill)
|
||||
private val serviceNotifications: MeshServiceNotifications = mock(MockMode.autofill)
|
||||
private val takServerManager: TAKServerManager = mock(MockMode.autofill)
|
||||
private val takPrefs: TakPrefs = mock(MockMode.autofill)
|
||||
@@ -94,7 +92,6 @@ class MeshServiceOrchestratorTest {
|
||||
|
||||
return MeshServiceOrchestrator(
|
||||
radioPrefs = radioPrefs,
|
||||
nodeManager = nodeManager,
|
||||
serviceNotifications = serviceNotifications,
|
||||
takServerManager = takServerManager,
|
||||
takMeshIntegration = takMeshIntegration,
|
||||
|
||||
@@ -24,8 +24,12 @@ import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.proto.FirmwareEdition
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
/**
|
||||
* A test double for [NodeRepository] that provides an in-memory implementation.
|
||||
@@ -163,6 +167,73 @@ class FakeNodeRepository :
|
||||
_nodeDBbyNum.value = nodes.associateBy { it.num }
|
||||
}
|
||||
|
||||
// ── Runtime node state management (from NodeManager merge) ──────────────
|
||||
|
||||
override val nodeDBbyNodeNum: Map<Int, Node>
|
||||
get() = _nodeDBbyNum.value
|
||||
|
||||
override val nodeDBbyID: Map<String, Node>
|
||||
get() = _nodeDBbyNum.value.values.associateBy { it.user.id }
|
||||
|
||||
private val _isNodeDbReady = MutableStateFlow(false)
|
||||
override val isNodeDbReady: StateFlow<Boolean> = _isNodeDbReady
|
||||
|
||||
override fun setNodeDbReady(ready: Boolean) {
|
||||
_isNodeDbReady.value = ready
|
||||
}
|
||||
|
||||
private val _myNodeNum = MutableStateFlow<Int?>(null)
|
||||
override val myNodeNum: StateFlow<Int?> = _myNodeNum
|
||||
|
||||
override fun setMyNodeNum(num: Int?) {
|
||||
_myNodeNum.value = num
|
||||
}
|
||||
|
||||
private val _firmwareEdition = MutableStateFlow<FirmwareEdition?>(null)
|
||||
override val firmwareEdition: StateFlow<FirmwareEdition?> = _firmwareEdition
|
||||
|
||||
override fun setFirmwareEdition(edition: FirmwareEdition?) {
|
||||
_firmwareEdition.value = edition
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
_nodeDBbyNum.value = emptyMap()
|
||||
}
|
||||
|
||||
override fun getMyNodeInfo(): MyNodeInfo? = _myNodeInfo.value
|
||||
|
||||
override fun getMyId(): String = _myId.value ?: ""
|
||||
|
||||
override fun handleReceivedUser(fromNum: Int, p: User, channel: Int, manuallyVerified: Boolean) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
override fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
override fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) {
|
||||
// no-op for tests
|
||||
}
|
||||
|
||||
override fun updateNode(nodeNum: Int, withBroadcast: Boolean, channel: Int, transform: (Node) -> Node) {
|
||||
val current = _nodeDBbyNum.value[nodeNum] ?: return
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value + (nodeNum to transform(current))
|
||||
}
|
||||
|
||||
override fun removeByNodenum(nodeNum: Int) {
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value - nodeNum
|
||||
}
|
||||
|
||||
override fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean) {
|
||||
// Simplified: just store the node number
|
||||
val num = info.num
|
||||
val existing = _nodeDBbyNum.value[num] ?: Node(num = num, user = User())
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value + (num to existing)
|
||||
}
|
||||
|
||||
override fun toNodeID(nodeNum: Int): String = "!%08x".format(nodeNum)
|
||||
|
||||
// --- Helper methods for testing ---
|
||||
|
||||
fun setNodes(nodes: List<Node>) {
|
||||
|
||||
@@ -25,17 +25,17 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.repository.NodeManager
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
|
||||
class RefreshLocalStatsAction :
|
||||
ActionCallback,
|
||||
KoinComponent {
|
||||
|
||||
private val radioController: RadioController by inject()
|
||||
private val nodeManager: NodeManager by inject()
|
||||
private val nodeRepository: NodeRepository by inject()
|
||||
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
val myNodeNum = nodeManager.myNodeNum.value
|
||||
val myNodeNum = nodeRepository.myNodeNum.value
|
||||
if (myNodeNum == null) {
|
||||
Logger.w { "RefreshLocalStatsAction: myNodeNum is null, skipping telemetry request" }
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user