mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-14 01:36:09 -04:00
feat: auto retry text message send on max retransmit (#4124)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user