feat: auto retry text message send on max retransmit (#4124)

This commit is contained in:
Mac DeCourcy
2026-01-03 04:21:43 -08:00
committed by GitHub
parent c9259c793f
commit 6bb40e4d20
7 changed files with 64 additions and 8 deletions

View File

@@ -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

View File

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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<StringResource, StringResource> {
val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status

View File

@@ -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()
}

View File

@@ -51,6 +51,7 @@
<string name="unrecognized">Unrecognized</string>
<string name="message_status_enroute">Waiting to be acknowledged</string>
<string name="message_status_queued">Queued for sending</string>
<string name="message_retry_count">Retries: %1$d / %2$d</string>
<string name="routing_error_none">Acknowledged</string>
<string name="routing_error_no_route">No route</string>
<string name="routing_error_got_nak">Received a negative acknowledgment</string>

View File

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

View File

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