diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt index a973888b3..a53500e64 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics @@ -326,6 +327,37 @@ constructor( scope.handledLaunch { val isAck = routingError == MeshProtos.Routing.Error.NONE_VALUE val p = packetRepository.get().getPacketById(requestId) + val isMaxRetransmit = routingError == MeshProtos.Routing.Error.MAX_RETRANSMIT_VALUE + val shouldRetry = + isMaxRetransmit && + p != null && + p.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE && + p.data.from == DataPacket.ID_LOCAL && + p.data.retryCount < MAX_RETRY_ATTEMPTS + + Logger.d { + val retryInfo = "packetId=${p?.packetId} dataId=${p?.data?.id} retry=${p?.data?.retryCount}" + val statusInfo = "status=${p?.data?.status}" + "[ackNak] req=$requestId routeErr=$routingError isAck=$isAck " + + "maxRetransmit=$isMaxRetransmit shouldRetry=$shouldRetry $retryInfo $statusInfo" + } + + if (shouldRetry && p != null) { + val newRetryCount = p.data.retryCount + 1 + val newId = commandSender.generatePacketId() + val updatedData = + p.data.copy(id = newId, status = MessageStatus.QUEUED, retryCount = newRetryCount, relayNode = null) + val updatedPacket = + p.copy(packetId = newId, data = updatedData, routingError = MeshProtos.Routing.Error.NONE_VALUE) + packetRepository.get().update(updatedPacket) + + Logger.w { "[ackNak] retrying req=$requestId newId=$newId retry=$newRetryCount" } + + delay(RETRY_DELAY_MS) + commandSender.sendData(updatedData) + return@handledLaunch + } + val m = when { isAck && fromId == p?.data?.to -> MessageStatus.RECEIVED @@ -528,6 +560,8 @@ constructor( } companion object { + private const val MAX_RETRY_ATTEMPTS = 5 + private const val RETRY_DELAY_MS = 5_000L private const val MILLISECONDS_IN_SECOND = 1000L private const val HOPS_AWAY_UNAVAILABLE = -1 diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt index 986359359..3c4bdfa51 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/entity/Packet.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.database.entity import androidx.room.ColumnInfo @@ -55,6 +54,7 @@ data class PacketEntity( viaMqtt = data.viaMqtt, relayNode = data.relayNode, relays = data.relays, + retryCount = data.retryCount, ) } } diff --git a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt index 0d5610fd7..06faca24a 100644 --- a/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt +++ b/core/database/src/main/kotlin/org/meshtastic/core/database/model/Message.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.database.model import org.jetbrains.compose.resources.StringResource @@ -88,6 +87,7 @@ data class Message( val viaMqtt: Boolean = false, val relayNode: Int? = null, val relays: Int = 0, + val retryCount: Int = 0, ) { fun getStatusStringRes(): Pair { val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt index 80e7e97f9..f795eef01 100644 --- a/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt +++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DataPacket.kt @@ -63,6 +63,7 @@ data class DataPacket( var relayNode: Int? = null, var relays: Int = 0, var viaMqtt: Boolean = false, // True if this packet passed via MQTT somewhere along its path + var retryCount: Int = 0, // Number of automatic retry attempts var emoji: Int = 0, ) : Parcelable { @@ -139,6 +140,7 @@ data class DataPacket( parcel.readInt().let { if (it == -1) null else it }, parcel.readInt(), // relays parcel.readInt() == 1, // viaMqtt + parcel.readInt(), // retryCount parcel.readInt(), // emoji ) @@ -164,6 +166,7 @@ data class DataPacket( if (rssi != other.rssi) return false if (replyId != other.replyId) return false if (relayNode != other.relayNode) return false + if (retryCount != other.retryCount) return false if (emoji != other.emoji) return false return true @@ -185,6 +188,7 @@ data class DataPacket( result = 31 * result + rssi result = 31 * result + replyId.hashCode() result = 31 * result + relayNode.hashCode() + result = 31 * result + retryCount result = 31 * result + emoji return result } @@ -207,6 +211,7 @@ data class DataPacket( parcel.writeInt(relayNode ?: -1) parcel.writeInt(relays) parcel.writeInt(if (viaMqtt) 1 else 0) + parcel.writeInt(retryCount) parcel.writeInt(emoji) } @@ -231,6 +236,7 @@ data class DataPacket( relayNode = parcel.readInt().let { if (it == -1) null else it } relays = parcel.readInt() viaMqtt = parcel.readInt() == 1 + retryCount = parcel.readInt() emoji = parcel.readInt() } diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml index d89f5b4e0..2d3f112d8 100644 --- a/core/strings/src/commonMain/composeResources/values/strings.xml +++ b/core/strings/src/commonMain/composeResources/values/strings.xml @@ -51,6 +51,7 @@ Unrecognized Waiting to be acknowledged Queued for sending + Retries: %1$d / %2$d Acknowledged No route Received a negative acknowledgment diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt index 83d1da2a0..f27a798be 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/DeliveryInfoDialog.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.messaging import androidx.compose.foundation.layout.Column @@ -34,6 +33,7 @@ import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.strings.Res import org.meshtastic.core.strings.close +import org.meshtastic.core.strings.message_retry_count import org.meshtastic.core.strings.relays import org.meshtastic.core.strings.resend @@ -45,6 +45,8 @@ fun DeliveryInfo( text: StringResource? = null, relayNodeName: String? = null, relays: Int = 0, + retryCount: Int = 0, + maxRetries: Int = 0, onConfirm: (() -> Unit) = {}, onDismiss: () -> Unit = {}, ) = AlertDialog( @@ -78,6 +80,14 @@ fun DeliveryInfo( style = MaterialTheme.typography.bodyMedium, ) } + if (maxRetries > 0) { + Text( + text = stringResource(Res.string.message_retry_count, retryCount, maxRetries), + modifier = Modifier.padding(top = 8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + } if (relays != 0) { Text( text = pluralStringResource(Res.plurals.relays, relays, relays), diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt index 7a342be9f..ed17ec107 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageListPaged.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-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 @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.feature.messaging import androidx.compose.foundation.layout.Arrangement @@ -118,6 +117,8 @@ internal fun MessageListPaged( nodes = state.nodes, ourNode = state.ourNode, resendOption = message.status?.equals(MessageStatus.ERROR) ?: false, + retryCount = message.retryCount, + maxRetries = 5, onResend = { handlers.onDeleteMessages(listOf(message.uuid)) handlers.onSendMessage(message.text, state.contactKey) @@ -436,6 +437,8 @@ internal fun MessageStatusDialog( nodes: List, ourNode: Node?, resendOption: Boolean, + retryCount: Int, + maxRetries: Int, onResend: () -> Unit, onDismiss: () -> Unit, ) { @@ -454,6 +457,8 @@ internal fun MessageStatusDialog( text = text, relayNodeName = relayNodeName, relays = message.relays, + retryCount = retryCount, + maxRetries = maxRetries, onConfirm = onResend, onDismiss = onDismiss, )