diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
deleted file mode 100644
index aed060531..000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
+++ /dev/null
@@ -1,105 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import co.touchlab.kermit.Logger
-import kotlinx.coroutines.CoroutineScope
-import org.koin.core.annotation.Named
-import org.koin.core.annotation.Single
-import org.meshtastic.core.common.util.handledLaunch
-import org.meshtastic.core.model.RadioController
-import org.meshtastic.core.repository.HistoryManager
-import org.meshtastic.core.repository.MeshPrefs
-import org.meshtastic.proto.ModuleConfig
-
-@Single
-class HistoryManagerImpl(
- private val meshPrefs: MeshPrefs,
- private val radioController: RadioController,
- @Named("ServiceScope") private val scope: CoroutineScope,
-) : HistoryManager {
-
- companion object {
- private const val HISTORY_TAG = "HistoryReplay"
- private const val DEFAULT_HISTORY_RETURN_WINDOW_MINUTES = 60 * 24
- private const val DEFAULT_HISTORY_RETURN_MAX_MESSAGES = 100
- private const val NO_DEVICE_SELECTED = "No device selected"
-
- fun resolveHistoryRequestParameters(window: Int, max: Int): Pair {
- val resolvedWindow = if (window > 0) window else DEFAULT_HISTORY_RETURN_WINDOW_MINUTES
- val resolvedMax = if (max > 0) max else DEFAULT_HISTORY_RETURN_MAX_MESSAGES
- return resolvedWindow to resolvedMax
- }
- }
-
- private val logger = Logger.withTag(HISTORY_TAG)
-
- private fun historyLog(message: String, throwable: Throwable? = null) {
- logger.i(throwable) { message }
- }
-
- private fun activeDeviceAddress(): String? =
- meshPrefs.deviceAddress.value?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() }
-
- override fun requestHistoryReplay(
- trigger: String,
- myNodeNum: Int?,
- storeForwardConfig: ModuleConfig.StoreForwardConfig?,
- transport: String,
- ) {
- val address = activeDeviceAddress()
- if (address == null || myNodeNum == null) {
- val reason = if (address == null) "no_addr" else "no_my_node"
- historyLog("requestHistory skipped trigger=$trigger reason=$reason")
- return
- }
-
- val lastRequest = meshPrefs.getStoreForwardLastRequest(address).value.takeIf { it > 0 }
- val (window, max) =
- resolveHistoryRequestParameters(
- storeForwardConfig?.history_return_window ?: 0,
- storeForwardConfig?.history_return_max ?: 0,
- )
-
- historyLog(
- "requestHistory trigger=$trigger transport=$transport addr=$address " +
- "since=${lastRequest ?: "all"} window=$window max=$max via=sdk",
- )
-
- scope.handledLaunch {
- val accepted = radioController.requestStoreForwardHistory(since = lastRequest)
- if (!accepted) {
- logger.w {
- "requestHistory failed trigger=$trigger transport=$transport addr=$address since=${lastRequest ?: "all"}"
- }
- }
- }
- }
-
- override fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) {
- if (lastRequest <= 0) return
- val address = activeDeviceAddress() ?: return
- val current = meshPrefs.getStoreForwardLastRequest(address).value
- if (lastRequest != current) {
- meshPrefs.setStoreForwardLastRequest(address, lastRequest)
- historyLog(
- "historyMarker updated source=$source transport=$transport " +
- "addr=$address from=$current to=$lastRequest",
- )
- }
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessagePersistenceHandler.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessagePersistenceHandler.kt
deleted file mode 100644
index 3d4dd73e2..000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MessagePersistenceHandler.kt
+++ /dev/null
@@ -1,193 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import co.touchlab.kermit.Logger
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-import org.koin.core.annotation.Named
-import org.koin.core.annotation.Single
-import org.meshtastic.core.common.util.handledLaunch
-import org.meshtastic.core.common.util.nowMillis
-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.NodeRepository
-import org.meshtastic.core.repository.Notification
-import org.meshtastic.core.repository.NotificationManager
-import org.meshtastic.core.repository.PacketRepository
-import org.meshtastic.core.repository.RadioConfigRepository
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.critical_alert
-import org.meshtastic.core.resources.getStringSuspend
-import org.meshtastic.core.resources.unknown_username
-import org.meshtastic.core.resources.waypoint_received
-import org.meshtastic.proto.PortNum
-
-/**
- * SDK-era implementation of [MeshDataHandler] focused on message persistence and notifications.
- *
- * The full packet-routing logic (handleReceivedData) is no longer needed — the SDK's packet flow
- * is consumed directly by VMs and SdkStateBridge. This class retains only [rememberDataPacket]
- * which is called by [StoreForwardPacketHandlerImpl] to persist forwarded messages.
- */
-@Single
-class MessagePersistenceHandler(
- private val nodeRepository: NodeRepository,
- private val packetRepository: Lazy,
- private val notificationManager: NotificationManager,
- private val serviceNotifications: MeshServiceNotifications,
- private val radioConfigRepository: RadioConfigRepository,
- private val messageFilter: MessageFilter,
- @Named("ServiceScope") private val scope: CoroutineScope,
-) : MeshDataHandler {
-
- private val rememberDataType =
- setOf(
- PortNum.TEXT_MESSAGE_APP.value,
- PortNum.ALERT_APP.value,
- PortNum.WAYPOINT_APP.value,
- PortNum.NODE_STATUS_APP.value,
- )
-
- override fun handleReceivedData(
- packet: org.meshtastic.proto.MeshPacket,
- myNodeNum: Int,
- logUuid: String?,
- logInsertJob: kotlinx.coroutines.Job?,
- ) {
- // No-op: Incoming packet routing is handled by SdkStateBridge / VM packet observers.
- // This method exists only to satisfy the MeshDataHandler interface contract.
- }
-
- override fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean) {
- if (dataPacket.dataType !in rememberDataType) return
- val fromLocal = dataPacket.from == DataPacket.LOCAL || dataPacket.from == myNodeNum
- val toBroadcast = dataPacket.to == DataPacket.BROADCAST
- val contactId = if (fromLocal || toBroadcast) dataPacket.to else dataPacket.from
-
- val contactKey = "${dataPacket.channel}${DataPacket.nodeNumToId(contactId)}"
-
- scope.handledLaunch {
- packetRepository.value.apply {
- val existingPackets = findPacketsWithId(dataPacket.id)
- if (existingPackets.isNotEmpty()) {
- Logger.d {
- "Skipping duplicate packet: packetId=${dataPacket.id} from=${dataPacket.from} " +
- "to=${dataPacket.to} contactKey=$contactKey" +
- " (already have ${existingPackets.size} packet(s))"
- }
- return@handledLaunch
- }
-
- val isFiltered = shouldFilterMessage(dataPacket, contactKey)
-
- insert(
- dataPacket,
- myNodeNum,
- contactKey,
- nowMillis,
- read = fromLocal || isFiltered,
- filtered = isFiltered,
- )
- if (!isFiltered) {
- handlePacketNotification(dataPacket, contactKey, updateNotification)
- }
- }
- }
- }
-
- @Suppress("ReturnCount")
- private suspend fun PacketRepository.shouldFilterMessage(dataPacket: DataPacket, contactKey: String): Boolean {
- val isIgnored = nodeRepository.nodeDBbyNum.value[dataPacket.from]?.isIgnored == true
- if (isIgnored) return true
-
- if (dataPacket.dataType != PortNum.TEXT_MESSAGE_APP.value) return false
- val isFilteringDisabled = getContactSettings(contactKey).filteringDisabled
- return messageFilter.shouldFilter(dataPacket.text.orEmpty(), isFilteringDisabled)
- }
-
- private suspend fun handlePacketNotification(
- dataPacket: DataPacket,
- contactKey: String,
- updateNotification: Boolean,
- ) {
- val conversationMuted = packetRepository.value.getContactSettings(contactKey).isMuted
- val nodeMuted = nodeRepository.nodeDBbyNum.value[dataPacket.from]?.isMuted == true
- val isSilent = conversationMuted || nodeMuted
- if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
- scope.launch {
- notificationManager.dispatch(
- Notification(
- title = getSenderName(dataPacket),
- message = dataPacket.alert ?: getStringSuspend(Res.string.critical_alert),
- category = Notification.Category.Alert,
- contactKey = contactKey,
- ),
- )
- }
- } else if (updateNotification && !isSilent) {
- scope.handledLaunch { updateNotification(contactKey, dataPacket, isSilent) }
- }
- }
-
- private suspend fun getSenderName(packet: DataPacket): String {
- if (packet.from == DataPacket.LOCAL) {
- return nodeRepository.ourNodeInfo.value?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
- }
- return nodeRepository.nodeDBbyNum.value[packet.from]?.user?.long_name ?: getStringSuspend(Res.string.unknown_username)
- }
-
- private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket, isSilent: Boolean) {
- when (dataPacket.dataType) {
- PortNum.TEXT_MESSAGE_APP.value -> {
- val message = dataPacket.text!!
- val channelName =
- if (dataPacket.to == DataPacket.BROADCAST) {
- radioConfigRepository.channelSetFlow.first().settings.getOrNull(dataPacket.channel)?.name
- } else {
- null
- }
- serviceNotifications.updateMessageNotification(
- contactKey,
- getSenderName(dataPacket),
- message,
- dataPacket.to == DataPacket.BROADCAST,
- channelName,
- isSilent,
- )
- }
-
- PortNum.WAYPOINT_APP.value -> {
- val message = getStringSuspend(Res.string.waypoint_received, dataPacket.waypoint!!.name)
- notificationManager.dispatch(
- Notification(
- title = getSenderName(dataPacket),
- message = message,
- category = Notification.Category.Message,
- contactKey = contactKey,
- isSilent = isSilent,
- ),
- )
- }
-
- else -> return
- }
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
deleted file mode 100644
index a74dbd849..000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
+++ /dev/null
@@ -1,88 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import co.touchlab.kermit.Logger
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.update
-import kotlinx.collections.immutable.persistentMapOf
-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.NodeRepository
-import org.meshtastic.core.repository.ServiceRepository
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.NeighborInfo
-import org.meshtastic.sdk.NeighborInfo as SdkNeighborInfo
-
-@Single
-class NeighborInfoHandlerImpl(
- private val nodeRepository: NodeRepository,
- private val serviceRepository: ServiceRepository,
-) : NeighborInfoHandler {
-
- private val startTimes = atomic(persistentMapOf())
-
- override var lastNeighborInfo: NeighborInfo? = null
-
- override fun recordStartTime(requestId: Int) {
- startTimes.update { it.put(requestId, nowMillis) }
- }
-
- override fun handleNeighborInfo(packet: MeshPacket) {
- val payload = packet.decoded?.payload ?: return
- val ni = NeighborInfo.ADAPTER.decode(payload)
-
- val from = packet.from
- if (from == nodeRepository.myNodeNum.value) {
- lastNeighborInfo = ni
- Logger.d { "Stored last neighbor info from connected radio" }
- }
-
- val requestId = packet.decoded?.request_id ?: 0
- val start = startTimes.value[requestId]
- startTimes.update { it.remove(requestId) }
-
- val formatted =
- SdkNeighborInfo
- .fromProto(
- reportingNode = from,
- neighborNodeIds = ni.neighbors.map { it.node_id },
- snrValues = ni.neighbors.map { it.snr },
- ).format { nodeId ->
- val user = nodeRepository.getUser(nodeId.raw)
- "${user.long_name} (${user.short_name})"
- }
-
- val responseText =
- if (start != null) {
- val elapsedMs = nowMillis - start
- val seconds = elapsedMs / MILLIS_PER_SECOND
- Logger.i { "Neighbor info $requestId complete in $seconds s" }
- "$formatted\nDuration: ${NumberFormatter.format(seconds, 1)} s"
- } else {
- formatted
- }
-
- serviceRepository.setNeighborInfoResponse(responseText)
- }
-
- companion object {
- private const val MILLIS_PER_SECOND = 1000.0
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt
deleted file mode 100644
index 503a045ad..000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt
+++ /dev/null
@@ -1,98 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import co.touchlab.kermit.Logger
-import kotlinx.coroutines.CoroutineScope
-import okio.ByteString.Companion.toByteString
-import org.koin.core.annotation.Named
-import org.koin.core.annotation.Single
-import org.meshtastic.core.model.DataPacket
-import org.meshtastic.core.repository.HistoryManager
-import org.meshtastic.core.repository.MeshDataHandler
-import org.meshtastic.core.repository.PacketRepository
-import org.meshtastic.core.repository.StoreForwardPacketHandler
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.PortNum
-import org.meshtastic.proto.StoreAndForward
-import kotlin.time.Duration.Companion.milliseconds
-
-/**
- * Implementation of [StoreForwardPacketHandler] that keeps legacy S&F parsing for backward compatibility.
- *
- * SF++ parsing/status updates are delegated to the SDK and consumed via [org.meshtastic.core.data.radio.SdkStateBridge].
- */
-@Single
-class StoreForwardPacketHandlerImpl(
- private val packetRepository: Lazy,
- private val historyManager: HistoryManager,
- private val dataHandler: Lazy,
- @Named("ServiceScope") private val scope: CoroutineScope,
-) : StoreForwardPacketHandler {
-
- override fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
- val payload = packet.decoded?.payload ?: return
- val u = StoreAndForward.ADAPTER.decode(payload)
- handleReceivedStoreAndForward(dataPacket, u, myNodeNum)
- }
-
- private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForward, myNodeNum: Int) {
- val lastRequest = s.history?.last_request ?: 0
- Logger.d { "StoreAndForward from=${dataPacket.from} lastRequest=$lastRequest" }
- when {
- s.stats != null -> {
- val text = s.stats.toString()
- val u =
- dataPacket.copy(
- bytes = text.encodeToByteArray().toByteString(),
- dataType = PortNum.TEXT_MESSAGE_APP.value,
- )
- dataHandler.value.rememberDataPacket(u, myNodeNum)
- }
-
- s.history != null -> {
- val h = s.history!!
- val text =
- "Total messages: ${h.history_messages}\n" +
- "History window: ${h.window.milliseconds.inWholeMinutes} min\n" +
- "Last request: ${h.last_request}"
- val u =
- dataPacket.copy(
- bytes = text.encodeToByteArray().toByteString(),
- dataType = PortNum.TEXT_MESSAGE_APP.value,
- )
- dataHandler.value.rememberDataPacket(u, myNodeNum)
- historyManager.updateStoreForwardLastRequest("router_history", h.last_request, "Unknown")
- }
-
- s.heartbeat != null -> {
- val hb = s.heartbeat!!
- Logger.d { "rxHeartbeat from=${dataPacket.from} period=${hb.period} secondary=${hb.secondary}" }
- }
-
- s.text != null -> {
- if (s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) {
- dataPacket.to = DataPacket.BROADCAST
- }
- val u = dataPacket.copy(bytes = s.text, dataType = PortNum.TEXT_MESSAGE_APP.value)
- dataHandler.value.rememberDataPacket(u, myNodeNum)
- }
-
- else -> {}
- }
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt
deleted file mode 100644
index 57be6f731..000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt
+++ /dev/null
@@ -1,167 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import co.touchlab.kermit.Logger
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import org.koin.core.annotation.Named
-import org.koin.core.annotation.Single
-import org.meshtastic.core.common.util.nowSeconds
-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.NodeRepository
-import org.meshtastic.core.repository.Notification
-import org.meshtastic.core.repository.NotificationManager
-import org.meshtastic.core.repository.TelemetryPacketHandler
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.getStringSuspend
-import org.meshtastic.core.resources.low_battery_message
-import org.meshtastic.core.resources.low_battery_title
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.Telemetry
-import kotlin.time.Duration.Companion.milliseconds
-
-/**
- * Implementation of [TelemetryPacketHandler] that processes telemetry packets and manages battery-level notifications
- * with cooldown logic.
- */
-@Single
-class TelemetryPacketHandlerImpl(
- private val nodeRepository: NodeRepository,
- private val notificationManager: NotificationManager,
- @Named("ServiceScope") private val scope: CoroutineScope,
-) : TelemetryPacketHandler {
-
- private val batteryMutex = Mutex()
- private val batteryPercentCooldowns = mutableMapOf()
-
- @Suppress("LongMethod", "CyclomaticComplexMethod")
- override fun handleTelemetry(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
- val payload = packet.decoded?.payload ?: return
- val t =
- (Telemetry.ADAPTER.decodeOrNull(payload, Logger) ?: return).let {
- if (it.time == 0) it.copy(time = (dataPacket.time.milliseconds.inWholeSeconds).toInt()) else it
- }
- Logger.d { "Telemetry from ${packet.from}: ${Telemetry.ADAPTER.toOneLiner(t)}" }
- val fromNum = packet.from
- val isRemote = (fromNum != myNodeNum)
- // Note: Local telemetry notification update was previously handled by
- // MeshConnectionManager.updateTelemetry(), now managed via SDK flows.
-
- nodeRepository.updateNode(fromNum) { node: Node ->
- val metrics = t.device_metrics
- val environment = t.environment_metrics
- val power = t.power_metrics
-
- var nextNode = node
- when {
- metrics != null -> {
- nextNode = nextNode.copy(deviceMetrics = metrics)
- if (fromNum == myNodeNum || (isRemote && node.isFavorite)) {
- if (
- (metrics.voltage ?: 0f) > BATTERY_PERCENT_UNSUPPORTED &&
- (metrics.battery_level ?: 0) <= BATTERY_PERCENT_LOW_THRESHOLD
- ) {
- scope.launch {
- if (shouldBatteryNotificationShow(fromNum, t, myNodeNum)) {
- notificationManager.dispatch(
- Notification(
- title =
- getStringSuspend(
- Res.string.low_battery_title,
- nextNode.user.short_name,
- ),
- message =
- getStringSuspend(
- Res.string.low_battery_message,
- nextNode.user.long_name,
- nextNode.deviceMetrics.battery_level ?: 0,
- ),
- category = Notification.Category.Battery,
- ),
- )
- }
- }
- } else {
- scope.launch {
- batteryMutex.withLock {
- if (batteryPercentCooldowns.containsKey(fromNum)) {
- batteryPercentCooldowns.remove(fromNum)
- }
- }
- notificationManager.cancel(nextNode.num)
- }
- }
- }
- }
-
- environment != null -> nextNode = nextNode.copy(environmentMetrics = environment)
-
- power != null -> nextNode = nextNode.copy(powerMetrics = power)
- }
-
- val telemetryTime = if (t.time != 0) t.time else nextNode.lastHeard
- val newLastHeard = maxOf(nextNode.lastHeard, telemetryTime)
- nextNode.copy(lastHeard = newLastHeard)
- }
- }
-
- @Suppress("ReturnCount")
- private suspend fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean {
- val isRemote = (fromNum != myNodeNum)
- var shouldDisplay = false
- var forceDisplay = false
- val metrics = t.device_metrics ?: return false
- val batteryLevel = metrics.battery_level ?: 0
- when {
- batteryLevel <= BATTERY_PERCENT_CRITICAL_THRESHOLD -> {
- shouldDisplay = true
- forceDisplay = true
- }
-
- batteryLevel == BATTERY_PERCENT_LOW_THRESHOLD -> shouldDisplay = true
-
- batteryLevel.mod(BATTERY_PERCENT_LOW_DIVISOR) == 0 && !isRemote -> shouldDisplay = true
-
- isRemote -> shouldDisplay = true
- }
- if (shouldDisplay) {
- val now = nowSeconds
- batteryMutex.withLock {
- if (!batteryPercentCooldowns.containsKey(fromNum)) batteryPercentCooldowns[fromNum] = 0L
- if ((now - batteryPercentCooldowns[fromNum]!!) >= BATTERY_PERCENT_COOLDOWN_SECONDS || forceDisplay) {
- batteryPercentCooldowns[fromNum] = now
- return true
- }
- }
- }
- return false
- }
-
- companion object {
- private const val BATTERY_PERCENT_UNSUPPORTED = 0.0
- private const val BATTERY_PERCENT_LOW_THRESHOLD = 20
- private const val BATTERY_PERCENT_LOW_DIVISOR = 5
- private const val BATTERY_PERCENT_CRITICAL_THRESHOLD = 5
- private const val BATTERY_PERCENT_COOLDOWN_SECONDS = 1500
- }
-}
diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
deleted file mode 100644
index e3668df39..000000000
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
+++ /dev/null
@@ -1,118 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import co.touchlab.kermit.Logger
-import kotlinx.atomicfu.atomic
-import kotlinx.atomicfu.update
-import kotlinx.collections.immutable.persistentMapOf
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import org.koin.core.annotation.Named
-import org.koin.core.annotation.Single
-import org.meshtastic.core.common.util.NumberFormatter
-import org.meshtastic.core.common.util.handledLaunch
-import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.model.fullRouteDiscovery
-import org.meshtastic.core.model.getTracerouteResponse
-import org.meshtastic.core.model.service.TracerouteResponse
-import org.meshtastic.core.repository.NodeRepository
-import org.meshtastic.core.repository.ServiceRepository
-import org.meshtastic.core.repository.TracerouteHandler
-import org.meshtastic.core.repository.TracerouteSnapshotRepository
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.getStringSuspend
-import org.meshtastic.core.resources.traceroute_route_back_to_us
-import org.meshtastic.core.resources.traceroute_route_towards_dest
-import org.meshtastic.proto.MeshPacket
-
-@Single
-class TracerouteHandlerImpl(
- private val serviceRepository: ServiceRepository,
- private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
- private val nodeRepository: NodeRepository,
- @Named("ServiceScope") private val scope: CoroutineScope,
-) : TracerouteHandler {
-
- private val startTimes = atomic(persistentMapOf())
-
- override fun recordStartTime(requestId: Int) {
- startTimes.update { it.put(requestId, nowMillis) }
- }
-
- override fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: Job?) {
- // Decode the route discovery once — avoids triple protobuf decode
- val routeDiscovery = packet.fullRouteDiscovery ?: return
- val forwardRoute = routeDiscovery.route
- val returnRoute = routeDiscovery.route_back
-
- // Require both directions for a "full" traceroute response
- if (forwardRoute.isEmpty() || returnRoute.isEmpty()) return
-
- scope.handledLaunch {
- val full =
- routeDiscovery.getTracerouteResponse(
- getUser = { num ->
- val user = nodeRepository.getUser(num)
- "${user.long_name} (${user.short_name})"
- },
- headerTowards = getStringSuspend(Res.string.traceroute_route_towards_dest),
- headerBack = getStringSuspend(Res.string.traceroute_route_back_to_us),
- )
-
- val requestId = packet.decoded?.request_id ?: 0
-
- if (logUuid != null) {
- logInsertJob?.join()
- val routeNodeNums = (forwardRoute + returnRoute).distinct()
- val nodeDbByNum = nodeRepository.nodeDBbyNum.value
- val snapshotPositions =
- routeNodeNums.mapNotNull { num -> nodeDbByNum[num]?.validPosition?.let { num to it } }.toMap()
- tracerouteSnapshotRepository.upsertSnapshotPositions(logUuid, requestId, snapshotPositions)
- }
-
- val start = startTimes.value[requestId]
- startTimes.update { it.remove(requestId) }
- val responseText =
- if (start != null) {
- val elapsedMs = nowMillis - start
- val seconds = elapsedMs / MILLIS_PER_SECOND
- Logger.i { "Traceroute $requestId complete in $seconds s" }
- "$full\n\nDuration: ${NumberFormatter.format(seconds, 1)} s"
- } else {
- full
- }
-
- val destination = forwardRoute.firstOrNull() ?: returnRoute.lastOrNull() ?: 0
-
- serviceRepository.setTracerouteResponse(
- TracerouteResponse(
- message = responseText,
- destinationNodeNum = destination,
- requestId = requestId,
- forwardRoute = forwardRoute,
- returnRoute = returnRoute,
- logUuid = logUuid,
- ),
- )
- }
- }
-
- companion object {
- private const val MILLIS_PER_SECOND = 1000.0
- }
-}
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt
deleted file mode 100644
index ec62f687c..000000000
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/HistoryManagerImplTest.kt
+++ /dev/null
@@ -1,39 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class HistoryManagerImplTest {
-
- @Test
- fun `resolveHistoryRequestParameters uses config values when positive`() {
- val (window, max) = HistoryManagerImpl.resolveHistoryRequestParameters(window = 30, max = 10)
-
- assertEquals(30, window)
- assertEquals(10, max)
- }
-
- @Test
- fun `resolveHistoryRequestParameters falls back to defaults when non-positive`() {
- val (window, max) = HistoryManagerImpl.resolveHistoryRequestParameters(window = 0, max = -5)
-
- assertEquals(1440, window)
- assertEquals(100, max)
- }
-}
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt
deleted file mode 100644
index 215ac6c3f..000000000
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt
+++ /dev/null
@@ -1,188 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import dev.mokkery.MockMode
-import dev.mokkery.matcher.any
-import dev.mokkery.mock
-import dev.mokkery.verify.VerifyMode
-import dev.mokkery.verify
-import dev.mokkery.verifySuspend
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-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.HistoryManager
-import org.meshtastic.core.repository.MeshDataHandler
-import org.meshtastic.core.repository.PacketRepository
-import org.meshtastic.proto.Data
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.PortNum
-import org.meshtastic.proto.StoreAndForward
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class StoreForwardPacketHandlerImplTest {
-
- private val packetRepository = mock(MockMode.autofill)
- private val historyManager = mock(MockMode.autofill)
- private val dataHandler = mock(MockMode.autofill)
-
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
-
- private lateinit var handler: StoreForwardPacketHandlerImpl
-
- private val myNodeNum = 12345
-
- @BeforeTest
- fun setUp() {
- handler =
- StoreForwardPacketHandlerImpl(
- packetRepository = lazy { packetRepository },
- historyManager = historyManager,
- dataHandler = lazy { dataHandler },
- scope = testScope,
- )
- }
-
- private fun makeSfPacket(from: Int, sf: StoreAndForward): MeshPacket {
- val payload = StoreAndForward.ADAPTER.encode(sf).toByteString()
- return MeshPacket(from = from, decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = payload))
- }
-
- private fun makeDataPacket(from: Int): DataPacket = DataPacket(
- id = 1,
- time = 1700000000000L,
- to = DataPacket.BROADCAST,
- from = from,
- bytes = null,
- dataType = PortNum.STORE_FORWARD_APP.value,
- )
-
- // ---------- Legacy S&F: stats ----------
-
- @Test
- fun `handleStoreAndForward stats creates text data packet`() = testScope.runTest {
- val sf =
- StoreAndForward(
- stats = StoreAndForward.Statistics(messages_total = 100, messages_saved = 50, messages_max = 200),
- )
- val packet = makeSfPacket(999, sf)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { dataHandler.rememberDataPacket(any(), myNodeNum) }
- }
-
- // ---------- Legacy S&F: history ----------
-
- @Test
- fun `handleStoreAndForward history creates text packet and updates last request`() = testScope.runTest {
- val sf =
- StoreAndForward(
- history =
- StoreAndForward.History(history_messages = 42, window = 3600000, last_request = 1700000000),
- )
- val packet = makeSfPacket(999, sf)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { dataHandler.rememberDataPacket(any(), myNodeNum) }
- verify { historyManager.updateStoreForwardLastRequest("router_history", 1700000000, "Unknown") }
- }
-
- // ---------- Legacy S&F: heartbeat ----------
-
- @Test
- fun `handleStoreAndForward heartbeat does not crash`() = testScope.runTest {
- val sf = StoreAndForward(heartbeat = StoreAndForward.Heartbeat(period = 900, secondary = 1))
- val packet = makeSfPacket(999, sf)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
- // No crash, just logs
- }
-
- // ---------- Legacy S&F: text ----------
-
- @Test
- fun `handleStoreAndForward text with broadcast rr sets to broadcast`() = testScope.runTest {
- val sf =
- StoreAndForward(
- text = "Hello from router".encodeToByteArray().toByteString(),
- rr = StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST,
- )
- val packet = makeSfPacket(999, sf)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { dataHandler.rememberDataPacket(any(), myNodeNum) }
- }
-
- @Test
- fun `handleStoreAndForward text without broadcast rr preserves destination`() = testScope.runTest {
- val sf =
- StoreAndForward(
- text = "Direct message".encodeToByteArray().toByteString(),
- rr = StoreAndForward.RequestResponse.ROUTER_TEXT_DIRECT,
- )
- val packet = makeSfPacket(999, sf)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { dataHandler.rememberDataPacket(any(), myNodeNum) }
- }
-
- // ---------- Legacy S&F: null payload ----------
-
- @Test
- fun `handleStoreAndForward with null payload returns early`() = testScope.runTest {
- val packet = MeshPacket(from = 999, decoded = null)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
- // No crash
- }
-
- // ---------- Legacy S&F: empty message ----------
-
- @Test
- fun `handleStoreAndForward with no fields set does not crash`() = testScope.runTest {
- val sf = StoreAndForward()
- val packet = makeSfPacket(999, sf)
- val dataPacket = makeDataPacket(999)
-
- handler.handleStoreAndForward(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
- // No crash — falls through to else branch
- }
-}
diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt
deleted file mode 100644
index 6c013b697..000000000
--- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt
+++ /dev/null
@@ -1,200 +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 .
- */
-package org.meshtastic.core.data.manager
-
-import dev.mokkery.MockMode
-import dev.mokkery.matcher.any
-import dev.mokkery.mock
-import dev.mokkery.verify
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-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.NodeRepository
-import org.meshtastic.core.repository.NotificationManager
-import org.meshtastic.proto.Data
-import org.meshtastic.proto.DeviceMetrics
-import org.meshtastic.proto.EnvironmentMetrics
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.PortNum
-import org.meshtastic.proto.PowerMetrics
-import org.meshtastic.proto.Telemetry
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class TelemetryPacketHandlerImplTest {
-
- private val nodeRepository = mock(MockMode.autofill)
- private val notificationManager = mock(MockMode.autofill)
-
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
-
- private lateinit var handler: TelemetryPacketHandlerImpl
-
- private val myNodeNum = 12345
- private val remoteNodeNum = 99999
-
- @BeforeTest
- fun setUp() {
- handler =
- TelemetryPacketHandlerImpl(
- nodeRepository = nodeRepository,
- notificationManager = notificationManager,
- scope = testScope,
- )
- }
-
- private fun makeTelemetryPacket(from: Int, telemetry: Telemetry): MeshPacket {
- val payload = Telemetry.ADAPTER.encode(telemetry).toByteString()
- return MeshPacket(
- from = from,
- decoded = Data(portnum = PortNum.TELEMETRY_APP, payload = payload),
- rx_time = 1700000000,
- )
- }
-
- private fun makeDataPacket(from: Int): DataPacket = DataPacket(
- id = 1,
- time = 1700000000000L,
- to = DataPacket.BROADCAST,
- from = from,
- bytes = null,
- dataType = PortNum.TELEMETRY_APP.value,
- )
-
- // ---------- Device metrics from local node ----------
-
- @Test
- fun `local device metrics updates node`() = testScope.runTest {
- val telemetry =
- Telemetry(time = 1700000000, device_metrics = DeviceMetrics(battery_level = 80, voltage = 4.1f))
- val packet = makeTelemetryPacket(myNodeNum, telemetry)
- val dataPacket = makeDataPacket(myNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { nodeRepository.updateNode(myNodeNum, any(), any(), any()) }
- }
-
- // ---------- Device metrics from remote node ----------
-
- @Test
- fun `remote device metrics updates node`() = testScope.runTest {
- val telemetry =
- Telemetry(time = 1700000000, device_metrics = DeviceMetrics(battery_level = 90, voltage = 4.2f))
- val packet = makeTelemetryPacket(remoteNodeNum, telemetry)
- val dataPacket = makeDataPacket(remoteNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { nodeRepository.updateNode(remoteNodeNum, any(), any(), any()) }
- }
-
- // ---------- Environment metrics ----------
-
- @Test
- fun `environment metrics updates node with environment data`() = testScope.runTest {
- val telemetry =
- Telemetry(
- time = 1700000000,
- environment_metrics = EnvironmentMetrics(temperature = 25.5f, relative_humidity = 60.0f),
- )
- val packet = makeTelemetryPacket(remoteNodeNum, telemetry)
- val dataPacket = makeDataPacket(remoteNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { nodeRepository.updateNode(remoteNodeNum, any(), any(), any()) }
- }
-
- // ---------- Power metrics ----------
-
- @Test
- fun `power metrics updates node with power data`() = testScope.runTest {
- val telemetry = Telemetry(time = 1700000000, power_metrics = PowerMetrics(ch1_voltage = 3.3f))
- val packet = makeTelemetryPacket(remoteNodeNum, telemetry)
- val dataPacket = makeDataPacket(remoteNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { nodeRepository.updateNode(remoteNodeNum, any(), any(), any()) }
- }
-
- // ---------- Telemetry time handling ----------
-
- @Test
- fun `telemetry with time 0 gets time from dataPacket`() = testScope.runTest {
- val telemetry = Telemetry(time = 0, device_metrics = DeviceMetrics(battery_level = 50, voltage = 3.8f))
- val packet = makeTelemetryPacket(myNodeNum, telemetry)
- val dataPacket = makeDataPacket(myNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- verify { nodeRepository.updateNode(myNodeNum, any(), any(), any()) }
- }
-
- // ---------- Null payload ----------
-
- @Test
- fun `handleTelemetry with null decoded payload returns early`() = testScope.runTest {
- val packet = MeshPacket(from = myNodeNum, decoded = null)
- val dataPacket = makeDataPacket(myNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
- // No crash
- }
-
- @Test
- fun `handleTelemetry with empty payload bytes returns early`() = testScope.runTest {
- val packet =
- MeshPacket(
- from = myNodeNum,
- decoded = Data(portnum = PortNum.TELEMETRY_APP, payload = okio.ByteString.EMPTY),
- )
- val dataPacket = makeDataPacket(myNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
- // No crash — decodeOrNull returns null for empty payload
- }
-
- // ---------- Battery notification: healthy battery does NOT trigger ----------
-
- @Test
- fun `healthy battery level does not trigger low battery notification`() = testScope.runTest {
- val telemetry =
- Telemetry(time = 1700000000, device_metrics = DeviceMetrics(battery_level = 80, voltage = 4.0f))
- val packet = makeTelemetryPacket(myNodeNum, telemetry)
- val dataPacket = makeDataPacket(myNodeNum)
-
- handler.handleTelemetry(packet, dataPacket, myNodeNum)
- advanceUntilIdle()
-
- // No dispatch call — battery is healthy
- }
-}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt
deleted file mode 100644
index 0087dde97..000000000
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HistoryManager.kt
+++ /dev/null
@@ -1,46 +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 .
- */
-package org.meshtastic.core.repository
-
-import org.meshtastic.proto.ModuleConfig
-
-/** Interface for managing store-and-forward history replay requests. */
-interface HistoryManager {
- /**
- * Requests a history replay from the radio.
- *
- * @param trigger A string identifying the trigger for the request (for logging).
- * @param myNodeNum The local node number.
- * @param storeForwardConfig The store-and-forward module configuration.
- * @param transport The transport method being used (for logging).
- */
- fun requestHistoryReplay(
- trigger: String,
- myNodeNum: Int?,
- storeForwardConfig: ModuleConfig.StoreForwardConfig?,
- transport: String,
- )
-
- /**
- * Updates the last requested history marker.
- *
- * @param source A string identifying the source of the update (for logging).
- * @param lastRequest The timestamp or sequence number of the last received history message.
- * @param transport The transport method being used (for logging).
- */
- fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String)
-}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt
deleted file mode 100644
index 4879d757e..000000000
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt
+++ /dev/null
@@ -1,43 +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 .
- */
-package org.meshtastic.core.repository
-
-import kotlinx.coroutines.Job
-import org.meshtastic.core.model.DataPacket
-import org.meshtastic.proto.MeshPacket
-
-/** Interface for handling incoming mesh data packets and routing them to the appropriate handlers. */
-interface MeshDataHandler {
- /**
- * Processes a received mesh packet.
- *
- * @param packet The received mesh packet.
- * @param myNodeNum The local node number.
- * @param logUuid Optional UUID for logging purposes.
- * @param logInsertJob Optional job that tracks the insertion of the packet into the log.
- */
- fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String? = null, logInsertJob: Job? = null)
-
- /**
- * Persists a data packet in the history and triggers notifications if necessary.
- *
- * @param dataPacket The data packet to remember.
- * @param myNodeNum The local node number.
- * @param updateNotification Whether to trigger a notification for this packet.
- */
- fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean = true)
-}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt
deleted file mode 100644
index b924d510a..000000000
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt
+++ /dev/null
@@ -1,36 +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 .
- */
-package org.meshtastic.core.repository
-
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.NeighborInfo
-
-/** Interface for handling neighbor info responses from the mesh. */
-interface NeighborInfoHandler {
- /** Records the start time for a neighbor info request. */
- fun recordStartTime(requestId: Int)
-
- /** The latest neighbor info received from the connected radio. */
- var lastNeighborInfo: NeighborInfo?
-
- /**
- * Processes a neighbor info packet.
- *
- * @param packet The received mesh packet.
- */
- fun handleNeighborInfo(packet: MeshPacket)
-}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt
deleted file mode 100644
index b7fefddae..000000000
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt
+++ /dev/null
@@ -1,32 +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 .
- */
-package org.meshtastic.core.repository
-
-import org.meshtastic.core.model.DataPacket
-import org.meshtastic.proto.MeshPacket
-
-/** Interface for handling Store & Forward (legacy) packets. */
-interface StoreForwardPacketHandler {
- /**
- * Handles a legacy Store & Forward packet.
- *
- * @param packet The received mesh packet.
- * @param dataPacket The decoded data packet.
- * @param myNodeNum The local node number.
- */
- fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int)
-}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt
deleted file mode 100644
index 31ec30db8..000000000
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt
+++ /dev/null
@@ -1,32 +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 .
- */
-package org.meshtastic.core.repository
-
-import org.meshtastic.core.model.DataPacket
-import org.meshtastic.proto.MeshPacket
-
-/** Interface for handling telemetry packets from the mesh, including battery notifications. */
-interface TelemetryPacketHandler {
- /**
- * Processes a telemetry packet.
- *
- * @param packet The received mesh packet.
- * @param dataPacket The decoded data packet.
- * @param myNodeNum The local node number.
- */
- fun handleTelemetry(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int)
-}
diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt
deleted file mode 100644
index c0cc42aa2..000000000
--- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt
+++ /dev/null
@@ -1,35 +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 .
- */
-package org.meshtastic.core.repository
-
-import kotlinx.coroutines.Job
-import org.meshtastic.proto.MeshPacket
-
-/** Interface for handling traceroute responses from the mesh. */
-interface TracerouteHandler {
- /** Records the start time for a traceroute request. */
- fun recordStartTime(requestId: Int)
-
- /**
- * Processes a traceroute packet.
- *
- * @param packet The received mesh packet.
- * @param logUuid Optional UUID for the associated log entry.
- * @param logInsertJob Optional job for the log entry insertion, to ensure ordering.
- */
- fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: Job?)
-}