refactor: delete 6 dead packet handlers post-SDK cutover (-1420 lines)

Remove handler interfaces, implementations, and tests that have zero production
callers after the SDK became authoritative for protocol handling:

- TelemetryPacketHandler/Impl — SDK owns telemetry via NodeChange.Updated
- StoreForwardPacketHandler/Impl — SDK owns S&F lifecycle + SFPP
- NeighborInfoHandler/Impl — SDK owns NeighborInfo model
- TracerouteHandler/Impl — SDK owns traceroute via AdminResult flow
- MeshDataHandler/MessagePersistenceHandler — handleReceivedData was no-op
- HistoryManager/Impl — only caller was deleted StoreForwardPacketHandlerImpl

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-05 18:03:21 -05:00
parent 31f792c71e
commit b874873f10
15 changed files with 0 additions and 1420 deletions

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Int, Int> {
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",
)
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<PacketRepository>,
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
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Int, Long>())
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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<PacketRepository>,
private val historyManager: HistoryManager,
private val dataHandler: Lazy<MeshDataHandler>,
@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 -> {}
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Int, Long>()
@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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Int, Long>())
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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<PacketRepository>(MockMode.autofill)
private val historyManager = mock<HistoryManager>(MockMode.autofill)
private val dataHandler = mock<MeshDataHandler>(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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<NodeRepository>(MockMode.autofill)
private val notificationManager = mock<NotificationManager>(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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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?)
}